Back to Index

AI Pictionary - Answer.AI dev chat #5


Transcript

>> All right. Hello, everybody. This is Jono here doing another dev chat. This one more a look at a work in progress rather than a demonstration of something finished. I'm going to be showing Jeremy through the latest iteration of the to-do app. Sorry. Not the to-do app. We've heard quite a lot of those.

The AIA Fictionary app. >> I was thinking yesterday over the weekend when I was sharing my most recent doll utility tool. You and Nate make all these really cool, fun things, and I make all these really tedious, dull things. I feel jealous. >> The mess behind the scenes. Okay.

So, hopefully you can see my screen. AIA Fictionary. >> I genuinely think this is going to be huge. I think this is going to be the next viral game everybody on the Internet plays. >> Yeah. I feel like it's kind of funny having that as a goal. We have to pay for every inference, but I guess it will be fun to be shared around.

Okay. So, this is the app. And actually, this is the live version here. So, Jeremy, I'll send you this in the Zoom chat, and you can play with that if you like. And we can go through the basic functionality, and then I'll show the code behind it. And so, how this works, when you click play a game, you've got some object that you're trying to draw.

Here, ladybug. A little timer to add a bit of pressure. >> That's really hard. Ladybug. >> Especially with a mouse. >> Oh, my gosh. You don't have a tablet. Okay. People with Microsoft services and iPads are going to do better here. And how come it hasn't guessed anything yet?

>> That's a bug. It should definitely be guessing things. I'm going to switch to the local version. I did take it down briefly, because there were some slightly less savory images popping through, just very briefly, I think. >> Of course. >> It doesn't take long on the internet to be...

>> Oh, I love the way you've got two different models showing their guesses. >> Yeah. So, okay. So, there we go. As we go in here, hooray, one of them must have got it. >> That's a hard one. I don't know if it's a lemon or an orange. >> Yeah, lemon is a tricky one to go.

So, okay. Turnip, onion. Yeah. So, you've got GPD 4.0, Claude Haiku, and Gemini Flash 1.5, all submitting their guesses. >> Haiku. Well played. >> Haiku, in this case, was the winner. Yeah. So, you can go and see the past games. >> Oh, nice. >> Ongoing games, you'd see them showing up there as well.

Yeah. It's kind of fun to see. Some of the words are definitely more challenging than others. >> My daughter's going to love this. >> Yeah. Yeah. >> Trident, that's too easy. >> Trident, well, yeah. I think the actual word was hook. Yeah. Oh, no, fork. >> Oh, I see.

>> So, it's easy enough to get it wrong as well. Okay. And then this is the... >> Oh, leaderboard. Nice. >> Oh, this is my local leaderboard. If you look at the one for the published app, I think it's mostly, yeah. I've been beaten up by a lot of players.

>> Whoa. Look at that, sub four seconds. >> Yeah. Yeah. See, I mean, line, it's kind of an easy... >> Oh, come on. That's ridiculous. >> We will have to have a leaderboard per word. >> Yeah. >> Yeah. So, anyway, this is the app as it is. >> It's really great.

It's like, it's got, you know, the spectate, the leaderboard. These little touches are nice. And then the kinds of things which, well, I'd be interested to hear what you thought. But I think, like, yeah, the kinds of small additions that with most things, they're not actually small additions. But hopefully with fast HTML, it wasn't too hard to add.

>> Mm. Sorry, I'm getting distracted trying to draw a zoo. I just want to draw a zoo. >> Yeah. And does it eventually time out? I see time left. Yes, it does. >> Yeah, yeah. So, I've got 30 seconds left. >> Is it, like, illegal to actually write zoo on it?

>> No. I saw some people on the public leaderboard using that trick. Oh, okay. Definitely some glitches that I've introduced in my tinkering around for the demo. >> Anyhow. >> Yeah. The zoo one showing up there. Okay. So, yeah. In the last half hour, I've introduced some bugs. But we can look at the -- the deployed one I'll try and have working by the time anyone sees this.

And we can just go through the code for those different pieces of functionality bit by bit. And I'll, like, kind of gradually reveal the complexity. Okay. So, this is the code. It's mostly in main.py. We'll look at later. There's a little bit of styling and a little bit of JavaScript for the canvas itself.

>> I see. And you've picked a word list. >> Yeah. >> Did that come from somewhere? >> I have a dump of words. I looked online at several different places for the, like, common dictionary words. There's also a lot of them are -- I kind of trimmed down many lists because you can have things that are easy enough to judge close enough for a human.

But I kind of just have this, like, string matching, basically. And so, I removed anything too crazy conceptual or with too many variants. >> Yeah. And presumably, the word list is not part of the prompt. So, the model doesn't know that thing. And also, it's important for people to remember that none of these online services are learning from the interactions.

So, they're never going to learn this word list. >> Right. Exactly. I mean, I'm kind of thinking this might be a fun eval for later. In which case, having, like, a known word list might make it interesting, like, easier or cheaty. And then also, we definitely need to do some, like, something I haven't added yet.

Keep track per session of what words you've seen so you don't see the same one again if the word list is too small. And if you've seen all of them, you should probably, like, be told to stop playing the game and go find something else to do with your time.

But, yeah. So, that's our list of words. Let's see. We've got different clients, API clients for the different models. The database, we're going to keep track of games, keep track of the images that are submitted, and keep track of the guesses that the models make. So, setting those up with the Fastlight database there.

>> Do you have any idea, like, about how expensive it is for us to run one game or how many tokens it takes? >> I could work backwards. Okay. So, I left it running for about 10 hours, constantly guessing two games simultaneously, which is what I've set as the max.

And that used about 5 million tokens, which was, I think, $1.25 on Haiku, maybe even less than $1 for Gemini Flash. And that's, I'd have to do the math, like, yeah, 10 hours, 30 seconds a game, and actually two games every 30 seconds. So, it's not free, but it's also, like, not crazy expensive if there's a limit on the total number of games.

>> Yeah, very inexpensive. >> And so on. But that would be a worry if you wanted, you know, like, if you wanted this to be, like, Word War or something, where there's hundreds of thousands of people playing simultaneously in the morning with a cup of coffee. >> Well, I liked your trick you did with your earlier captioning game, where we just have a balance, where if there's no money in the balance, people can just donate $5 to play themselves, and everybody else can play too.

>> Yeah. Yeah, we can see how that goes. At the moment, to keep the spend in check, and to keep it tempered, there is a queue. So, if there's more than two people playing, then other people will be put into the queue rather than starting a game immediately. >> Nice.

>> And so, we can look at the logic for that. That was one of the, like, yeah, like, oh, it's somewhat non-trivial over the base case where everyone always sees the canvas. Yeah, let's see. Session. There's a unique session ID that lets us do things per player and keep track of who's who.

>> Great. >> We're loading in some JavaScript, some styling. >> So, that session is kind of like a user, but it's attached to a browser, basically. There's no way to have the same jono across multiple browsers. But because it's using the starlet sessions, that will be stored as long as they maintain their cookies.

They'll have the same session. >> Yeah. And so, if you wanted to have, you know, leaderboards and high scores and avoid abuse, it might be better to have, like, a login with Google and create a proper user account. >> Yeah. >> But for now, this is nice. You can just load the page and start playing.

You don't even have to put in a name until after the game. Not at all. Okay. So, we have a game manager class. I should say a lot of this code was written by Sonic 3.5. It's not necessarily the style I do, but it picked up the fast HTML surprisingly fast.

Like, giving it the starting point that I had as a file, that was pretty good. >> Did you use our fast HTML board project with all the stuff in there? Okay. Because that also helps quite a bit. I just have a project with all the big kind of highly commented demo app loaded into the context, which it's really good at writing fast HTML with that.

>> Right. Yeah. By the time I started this, I already had an existing app that showed a lot of the functionality. And it was just like, oh, that does this. Could you add a thing that does, you know, so it like had the pieces already. >> I think the reason it's so good at this is because it's kind of like basically a style transfer problem.

Like, it literally is HTML, just written a different way. So, it's kind of like, all of its embeddings are still going to work, except for that last layer or two, where it has to use slightly different tokens. >> Yeah. And a lot of the routing, I think, is quite familiar from like fast API and things like that.

>> Yes, exactly. And Flask. >> All right. So, we have a way to keep track of active games, to start and end games. At the end of a game, I make a GIF. Totally unnecessary, but kind of nice for the previews. >> I really like those. Yeah. >> Yeah.

And so, then the way I've organized this is to have the main, like, roots. So, the home, the about page, the leaderboard, and the spectate page. And then all the supporting bits of infrastructure for that afterwards. I know you don't like the, like, out of order pieces, but... >> Sorry, the out of order pieces?

>> Oh, to have the home and then something that the home depends on, like, a bunch, far later in the code. >> It doesn't, though, does it? Like, it's not that it's, well, yeah, I guess it's going to call navbar, is it, for example? >> Yeah, exactly. So, home is going to use navbar, but because it's all executable...

>> There's no reason for me not to like it, Jono. It's just, like, you know, when I used to write C and stuff, you had to. And so, now I'm like, that ought to work. It works fine. No weird reference. >> Cool. Yeah. So, the main page, got a title, got a navbar.

This, again, thank you, Claude. Links to the different pages. We have this active area is kind of, like, the main place where the actual game happens. So, I'll come back to that. About page is just a markdown. >> So, sorry, the navbar, you just call manually from multiple places, or how does that work?

>> Yeah. So, in the about section, I call navbar, and I put in this thing where I can say, um, link to, you know, just do, like, a null link if you're on that page already. >> Nice. >> It's probably a better way to do that. >> It's nice and simple, isn't it?

That's great. >> And then the title changes to, like, the home button. So, this nice big, like, this is how you go home. This is the default. >> Yeah. Great. >> And that's nice. It's just little conditionals in the... >> And because you're using href rather than hxget, you're actually, the URL is changing.

It's cool. >> Yeah. Yeah. So, this is actually much more like a web 1.0 app than some of our demos. The only, you know, you'll see the htmx bits, but a lot of it's, like, separate pages and then shareable pages. So, if you're looking at, like, the summary of a game, you know, that's a special page that has, it has, yes, the game and who won it and all that.

And it also has a link that'll take you to Twitter and say, hey, I scored this in this game. And this is the URL. And it's got a little preview image. Yeah. So... >> That's really cool. I love it. And I saw, interestingly, on the about page, you were using markdown.

Are you using JavaScript to convert that or... >> Yes. So, we have this markdown.js. It's defined in the fasthtml.js thing. Yeah, this will take us there. >> And this is just a little convenience for you. Because, of course, you could perfectly well have just written that as h2 and p and whatever.

But it's just easier to write it in a markdown string. >> Yeah. Yeah. I just wanted to be able to, like, come and write this without having to think about that. It's very much an afterthought. This was... I can imagine, like, if you wanted to have a blog or something like that where you had more actual content, being able to author it in markdown is nice.

In this case, it doesn't really have much. This is... >> I mean, these minor things that make, like, writing an about page less annoying, for example, it's all, like, reducing the friction and making the coding more enjoyable. Yeah, I like it. It matters. >> Yeah. Yeah, exactly. And, you know, just knowing that, yeah, I just use the simple elements.

I've got some styling that means that typography is going to look okay. It's not going to fill the whole page. Yeah, we've got the markdown rendering so that I could put in images there if I didn't know how to do the image tag in fast HTML. Yeah, just makes life a little easier.

Okay, so maybe we should look at, like, the actual, like, the core game loop. You know, a lot of the other pieces are quite atomic, like the leaderboard, but the game active area... >> Well, just show the leaderboard. So how is that... is that being stored in a SQLite database?

Is that how that works? >> Right. So this is doing a query on the database. Like, oh, get many finished games with an end time, order by the time taken. It should probably have made that its own column so that it's not doing those calculations. And it's doing this every time someone goes to the page.

But I have the feeling that, like, you know, operations like this, like taking a database of a few hundred games and sorting it for every unique visitor is kind of dwarfed by the, like, waiting for big AI models to finish and, like, serving out these GIFs and all of that jazz.

You know, like, that's a lot more computation. Also, even, like, I shared this a little bit this morning, and there's quite a few people playing. The CPU usage on the machine that it's deployed on is, like, it just... the graph couldn't even read, like, 0.0. You know, 0.0 something.

>> Is this running on a little VPS or something? >> It's on railway. So just the... >> Oh, railway. Cool. Okay. >> Yeah. So it's really not... it doesn't seem to be... I mean, obviously, if it goes viral, we'll see how it handles that. But yeah, just at the moment, tricks like this gets it nice and fast.

Query the database, make a table. So I'm giving a row in the table for each game. Little title, heading, link. Yeah. So I don't think there's any HTMX here. It's just, again, rendering a page based on some data. We could do a thing to see if any of the games had the player ID that's the same as the person viewing the page to, like, flag your own games or something like that.

But you can also just look for your nickname or something. >> Or you could also do that thing that you often see in games where, like, it'll show you the top 10, and then it'll be, like, dot, dot, dot, and then it'll show you the five above and below yours, your best.

>> Right, right. So place 112 as me, my best game or something like that. Or my most recent game. Yeah. Cool. Okay. So the actual game, it comes into this area here. And so all the action is in this active area part, and then in the handling of the images.

>> I just like -- I'm still thinking -- sorry, I can't stop thinking about your -- it's so delightfully HTMX-y, this idea that, like, you don't have any fancy JavaScript to check, you know, change the class dynamically if blah, blah, blah. Like, it's just -- you know, it's static HTML generated from a function.

It's really great. >> Yeah. Yeah. And it's -- it's -- you know, yeah, I really like it. And it also is, like, okay, I'm sending a little bit of redundancy, right? I'm sending that on all three pages people visit. Like, what is that? 812 wasted bytes when I'm also loading a bunch of GIFs?

>> Exactly. So I'm just thinking -- so already -- I mean, that function you just showed us, you could change title -- yeah, you could change title to title, right? No, the one -- the top -- the up, up, up, up. >> This one here? >> Yeah, that one.

Rhymes164. Title could become titled. Then you wouldn't need main. You wouldn't need class container. But then you could write your own thing like titled that actually calls titled, which also adds the navbar, you know what I mean? So you can, like -- so titled already factors out the -- you know, the -- oh, actually, you don't have an h1, which is different.

So, yeah. Anyway, so that's kind of, like, the fast HTML idiomatic approach, I think, is to, like, not have templates, but more you just have functions to refactor out the commonly used UI elements. >> Yeah. Yeah. Okay. Right. Let's see. So I've gone to the home page, and there's -- I'm looking at that area there.

And so if I haven't started any games, I'm a refresh user, this is what I'm going to see. I'm going to see a button that says play a game. That's going to post to join, and it's going to update this active area that's, like, the sort of main play area.

>> Nice. >> So I guess we can look at join. Join is going to add them to the queue if there's a queue. Otherwise, it's going to start a game, and then it's going to return that same active area. But now, whereas before they were a new user, now there's a game on or they're in the queue, and so there's different pieces for them to see, depending on that.

If they're in the queue, we've got a little countdown saying, oh, all the games are full. You're this index in the queue. There's this many people in total. This is your estimated wait time. And that's going to just keep polling to check how many people are in the queue.

>> Yeah, so the H6 trigger every one second. And this is, like, this important insight that I've been thinking about a lot, which is, like, you only need to build a WebSocket app if the thing coming from the server, you don't know when it's coming, and you need to know about it immediately, you know.

But the vast majority of the time, either you know when it's coming, because it's, like, because you asked for it, or it's fine just to check every second or so, in which case you don't need the custom WebSockets. This every one second trick is perfect. Also, just, like, I really like that there's nothing clever about any of this code.

Do you know what I mean? It feels like the kind of thing you could write in, like, a boot camp week two. Okay, you've learned, you know, loops and if statements, so here's what happens when you put them together. Yeah, it's, like, it feels more like the complexity of 1990s PHP than, like, 2024 Next.js or something.

>> Yeah, and you can do things, you know, you can do things all sorts of ways, but it feels like it lends itself to this, like, oh, I'm going to return some content if one condition is met, otherwise I'm going to return some other content. There's lots of reality, I'll make a template that renders one or the other with only the important bits changed.

>> Yeah, it's just, it's nothing clever. Just say what you want. >> Yeah, and exactly, and it's also, like, this is, maybe I should have said this up front, like, this is what you get when you have something where it was, I had a starting point, right, it was a very simple app that didn't have any of the flashy bits, and then you just quickly add all the pieces you need.

>> So you didn't have to design the whole thing up front, you just were, like, more like, oh, now that's there, my heart says I want to add this now. >> Right, okay, now I should add a queue, all right, so now when I'm rendering that active area, I should check if there's a queue or not, and then adjust accordingly, you know, so it's, like, all these bits tacked on.

So I think, like, it's a difference, you end up with a lot of code, like, this is, like, a 700 lines of code. It's not, like, the most elegant thing, but it is very easy to understand, because you could, especially if you had the history, like, oh, we added this route for the leaderboard, right, and that's, like, boom, it's, like, its own thing.

And maybe, okay, I needed to add an extra column in one of the databases because I wanted to show something about that. You know, you do, like, touch other pieces of the system, but it feels like a lot of these are, like, oh, we add this piece, then we add this piece, then we add this piece.

>> Yeah, and then line 267 there is just, like, oh, here's a canvas. Okay. >> Yeah. >> So there must be some JavaScript somewhere, I assume. I did see you had a JavaScript earlier. >> Yeah, yeah. So if you're in an active game, we are now, like, displaying a canvas, and there's actually two bits of JavaScript going on.

There's a countdown, and there's a canvas. So we can look at the JavaScript for that. There's almost none of it written by me. When we first load this or any element, we find anything that matches this selector, and then we add some event listeners to it and color the background white.

We have some drawing code that you'll find in lots and lots of tutorials if you're looking for, like, how to draw on an HTML5 canvas. Again, very easy for someone like Claude to write. >> It's kind of nice that you actually -- I like the way you haven't attempted to use HTMX or any of this.

You're just, like, here, this is our JavaScript bit, so I'll just use JavaScript. >> Yeah. Yeah. So the one place that I used to have HTMX -- okay, so we're drawing some lines, and once we're done drawing, we're going to send the canvas data to the server. And so that's this piece here.

It's getting the canvas, it's turning it into a blob of data, and then it's posting it to a root, and we can intercept that. So you can also use HX trigger to, like, from JavaScript, trigger an HTMX event. But yeah, this is, like, actually, I found the easy way of doing it.

Just whenever I want from within my JavaScript, I'm posting this data. I do have some throttling so that it's not like someone's drawing a million dots. It's not sending million images. It's sending one every -- >> Yeah, I mean, I bet we could, you know, it might be fun, actually, even to look at refactoring that to make it smaller.

But, like, yeah, when you're just building something, and if Claude says, "Here's the code," then just use the code. >> Right. I pasted my previous one in. I said, "This sends a lot of events. If someone's doing lots of dots, could you add throttling?" So it's all this, like, iterative, like, "Oh, okay, we change sendCanvas to throttled sendCanvas data, and we add a new function." Instead of having this be in here.

Yeah, okay, so that's the main, like, Canvas piece. Let's see, there was a countdown as well. Yeah, it's just a little bit extra to have the, like -- >> Nice. >> Here's the countdown flashing there, and the reason it's here is because you can also do all sorts of other things easier in JavaScript on the web page if you wanted to have big lines counting down, and flashes, and all that sort of thing.

So in that JavaScript, we saw that it's sending the images to a different root, and so that's maybe then the next piece where a lot of important stuff is happening. We're getting an image. So first, okay, does this person have any right to be sending us images? Is it from a person who's in an ongoing game?

If so, go look up the game, and then -- >> Just a reminder that sessions are signed, so somebody can't take that. >> Right, yeah. So you can't put a new SID in your session. I mean, not that it would make too much of a difference here, but yeah.

For something more secure, you could potentially store, like, a username or a unique key to look up in your database. Yeah, so we're getting an image. We're writing it to a file. But importantly, we're not, like, querying those AI models, right? So this runs pretty fast, and it's just returning back some -- actually, I could return back nothing, but I'm returning some stuff to the JavaScript so that it knows, okay, the game is still ongoing.

And -- >> And, like, if you wanted to, you know, you could make this more performant by adding the word async before def on line 421, and image.file.read. You could use await there with, you know, async I/O, you know, stuff like that. So, like, there's -- and def.write, you know, so there's a few places you could definitely -- well, you'd have to use the -- yeah, I guess the async I/O version of I/O read, but -- yeah.

>> Okay. >> Might take five minutes. >> Yeah. >> Or if you're familiar with async, 20 seconds. >> I am not very familiar with it. >> I'm not very familiar with it either. Or at least not in Python. >> Well, that's a good segue into the process and the actual guessing.

So, we're storing these images. We're not sending anything back. We could be sending back a piece of HTML to say, like, we received your latest guess or something like that, or give some other sort of status update there. Okay. So, what's happening to those images? They're getting saved. They're getting stored in the games table.

We're keeping track of, like, the active games and the game manager thing. So, now the design of this is that I'm responding to the request straight away, but then in the background, like, kind of constantly, I've got some separate threads running that are, like, looking if there's any new images that have come in.

If so, they're sending them off to the different AI models. When they get guesses back, they're, like, responding with those guesses. So, all of that is happening kind of separate to the I'm responding to requests part of the app. It's in a different thread. And this is the part that's, like, very much ad hoc messy.

I got Sonnet to rewrite it, and I don't like how it rewrote it. So, it's a little bit, like, hodgepodge. But with that caveat, we can see what it's doing. Each of these functions is what you'd see in the, like, vision tutorial of the docs for the different APIs.

We have a prompt. Here's the picturing prompt. >> I definitely want to rewrite with this with Claudette. >> Yeah. Although, it's not, like, you need a history, right? You don't have any chat history. So, you'd only be using the Claudette, like, utility functions. >> Yeah. It's not a big deal.

Just a nice little thing, like, all the image handling and everything would be done for you. >> Yeah. Yeah. It'd definitely be a lot more convenient. >> That's totally fine. >> Yeah. Okay. So, these functions just take an image and an optional history of past guesses. They send the image to the model, and they get back the response.

>> And do they all get the same prompt? >> Yes. So, every single one has... >> No system message. >> Yeah. No system message. So, this is definitely, like... >> Oh, passing in the past guesses. That's a good idea. >> Yeah. So, if there are previous guesses for that game...

>> And is that the previous guesses from all the models, or just from that model? >> From all the models. It's kind of like the online picture where you see everyone's things flowing past. And it helps avoid duplicates, because otherwise, like, everyone guesses banana, and then they see the same image again, and they all guess banana again.

It's like, "Oh, no, no. You already tried that. It didn't work." >> Exactly. >> Yeah. So, that gets some variety. >> Nice. >> I'm saving the images and, like, the image sequences. I kind of want to, like... Separately to this project, you will have this data set, and we can try the different, like...

Prompt them with the whole image sequence, prompt them with guess history versus not, you know, like, experiment with that as, like, a task. At the moment, yeah. As you can see, just a simple prompt, and hope for the best. Oh, also, like, all of these should be doing things like, you know, checking for rate limiting.

I think I do have a little bit in this one. The others really do need, like, error catching. I noticed once we released it publicly, Jem and I were suddenly having, you know, obviously disapproved of some of the image content, the safety moderation. >> Of course. >> You didn't get any text back in a response.

You instead get some different thing. >> That's another thing you'll get for free with stuff like Claudette, kind of an exponential back off type thing. >> Right. Yeah. So, exponential back off, catching different errors, maybe, like, you know, banning a session if the person's drawings are regularly triggering a safety filter.

But, you know, some sort of, like, maybe separate prompt that's just a moderation prompt. Is this something that we want on the general, you know, spectate feed or not? Yeah. So, that's definitely lots of room for improvement there. And then what's happening is these different functions are being called in this background task.

And so, we're keeping track of, like, what games are running. There's, like, we don't want to repeatedly, like, guess too fast. We also don't want to wait too long. So, we've got a timeout and, like, a minimum time. And then at some point, we're, like, running these tasks with an image.

>> And presumably a lot of this is LLM written? >> Yeah. Pretty much all of this is. >> Yeah, you could. >> Yeah. So, I think this is the part that I feel like I'm sure I can rewrite this much neater. >> But, I mean, you know, it's fine.

Like, if it's a, you know, pretty well-defined blocker functionality that an LLM can write, then that's good. >> Yeah. Yeah. I think it could do with some thinking about, like, handling the errors better, logging, and maybe also, like, the way it's updating the database. At the moment, the Fastlight has the SQL set to, like, only allow access from a single thread.

And so, there's some, like, kludgy hacks around that that I did. Whereas, I think that's, like, a flag we can toggle. >> Okay. Also, I wrote this little Redis-based kind of Neo clone of Fastlight on the weekend, which I'm using for Vercel, which is another option. >> Yeah. And it might be nice to put this, you know, like, I think the way people build these apps often is, rather than having a volume, and, like, I'm storing the images to disk here.

Instead, you'd have, like, blob storage for the image files, and you'd have, yeah, like, Redis or some other KB cache for your database that you could, you know, have separate dashboards for it elsewhere and, you know, like, edit it and tweak it and export it. >> But that's hard to be set for, like, the simplicity of, like, SQL in a file system, you know?

They're things we're pretty familiar with, and they're not specific to the service either. >> Yeah. And it's quite nice, like, you know, you just zip up that data folder, and you've got your data export, so. >> Yeah. >> Yeah. Okay. I think that's pretty much it. >> Thanks, John.

>> There's another thread. Thank you. Kicking any players who haven't, like, accessed the website in over a minute. >> Okay. >> You know, so there's all these little pieces that are, like, nice to have. >> And you've got that with a start thread decorator, so it's running in the background, which is a nice, like, example of having a background task.

Very nice. >> Yeah. Yeah. I think that's pretty much it. I will have to go check why the live demo stopped, but it's done, I think, like, 200 or 300 games in the last few hours. >> And perfect timing. Time for me to go and teach my daughter math, so.

>> Oh, fantastic. >> Thanks for the walkthrough, Johnno. >> Yeah, thanks, Jeremy. Hopefully, it's useful. >> And I'll be around again after math. >> Cool. >> If you are. See you.