Skip to content
D · 04 · TECHNICAL WRITE-UP

Bedtime Stories.

Een Nederlandse AI-bedtijdverhalengenerator. Creature Team Verhalenmachine.

Bouw een fantasiewereld met personages. Stel een verhaal in. Krijg een geïllustreerd bedtijdverhaal van meerdere hoofdstukken, compleet met een voorleeshulp voor de ouder, quizzen, heldenportretten, kleurplaten en een perkamentlezer die als losse boekjes voor ouder en kind print. Nederlands erin, Nederlands eruit. Eerst voor één specifiek gezin; gepubliceerd als portfolioversie zodat het engineeringwerk leesbaar is.

01 · WAT HET DOET

Acht functies, één bedtijd.

De app is gebouwd rond het Creature Team-universum — een vooraf geladen cast van zo'n vijftig helden, sidekicks en schurken die de kinderen kunnen mixen en matchen. Al het andere is configuratie daarbovenop.

  • Conversational world-buildingChat met de wereldeditor (Gemini structured output) om personages, eigenschappen, plekken en items toe te voegen. Het wereldlog werkt zichzelf ter plekke bij — kinderen ontwerpen het universum voordat het verhaal begint.
  • Three-state character toggleElk personage staat op Mee (geforceerd erin), Thuis (geforceerd eruit) of Willekeurig (random). De cast van een verhaal is hard begrensd op 7 personages — daarboven stort de verhaallijn in.
  • Story generationKies het aantal hoofdstukken, de doelleeftijd, de verhaalboog en de leerdoelen. Eerst een outline, daarna worden hoofdstukken in parallelle batches van 3 gegenereerd, met retry per hoofdstuk. Elk hoofdstuk komt met een zelfleessectie, een ouderhulpsectie van 500–600 woorden, 5 quizopdrachten en een image prompt.
  • Image generationImagen 4 verzorgt covers, illustraties binnen hoofdstukken, kleurplaten, profielfoto's van personages en character sheets per pose. Gegenereerde assets worden bewaard in GCS en op aanvraag teruggehaald.
  • Parchment readerVerhaalweergave in de stijl van een prentenboek (Fredoka + Nunito, perkamentachtergrond). De printmodus genereert via window.print() losse boekjeslay-outs voor ouder en kind.
  • Parent lockBeveiligt met een pincode de instellingen, het opnieuw genereren van beelden en alle kostengevoelige onderdelen. De volwassenen houden de regie.
  • Story archive + variation trackerElk gegenereerd verhaal wordt bewaard. Eerdere titels en samenvattingen gaan terug in de prompt, zodat het volgende verhaal zichzelf niet herhaalt.
  • Cloud syncSynchronisatie op basis van Firestore voor werelden, personages en opgeslagen verhalen. Auto-sync draait op de achtergrond, zodat een verhaal dat op het ene apparaat begint, op het andere wordt afgemaakt.
02 · OPVALLENDE ONTWERPKEUZES

De invarianten die je moet kennen.

Dit zijn de niet voor de hand liggende regels waar de codebase op leunt — het soort detail dat je anders een weekend kost om op de harde manier te leren. Hier vastgelegd, zodat de volgende onderhouder (ikzelf in de toekomst) dat niet hoeft.

Eerst de outline, dan de hoofdstukken parallel
De pipeline genereert een outline en waaiert daarna per drie hoofdstukken tegelijk uit, met retry per hoofdstuk. Promise.allSettled voorkomt dat één mislukt hoofdstuk de hele run om zeep helpt.
De cast van een verhaal is hard begrensd op 7
Boven de zeven raakt het model het overzicht kwijt over wie er in de kamer is. De grens werd ingesteld nadat vroege verhalen met grotere casts steeds personages opleverden die één keer opdoken en weer verdwenen.
Druk het wereldlog nooit pretty-printed in een prompt af
De wereld bevat zo'n 50 personages met lange beschrijvingen. JSON.stringify met inspringing blaast het tokenbudget op. Compacte helpers in store.ts (buildWorldSummary, plus 200-char truncatie in verhaalprompts en 150-char in chatprompts) zijn verplicht.
De outline gaat als één regel per hoofdstuk de hoofdstukprompts in
Dezelfde reden van tokenbudget. De hoofdstukgenerator ziet de outline als een lijst met oneliners, niet als de gestructureerde JSON, zodat elke aanroep per hoofdstuk gefocust blijft.
Nederlands erin, Nederlands eruit — behalve imagePrompt
Alle UI-tekst en generatieprompts zijn in het Nederlands (de kinderen zijn het publiek). De enige Engelse string in de pipeline is de imagePrompt — Imagen 4 presteert merkbaar beter op Engelse beeldprompts dan op Nederlandse.
De modelnaam ligt vast
gemini-3.1-pro-preview is het enige AI-model dat is aangesloten. Het wisselen ervan vereist dat je opnieuw controleert of elke prompt en elk structured-output-schema nog correct parst. Geen achteloze upgrade.
Alleen client-side — geen SSR
De componenten zijn 'use client'. De Zustand-store leunt op localStorage, dat server-side niet bestaat. Persistentie: localStorage voor de actieve sessie, Firestore voor het archief over apparaten heen.
03 · STACK

Wat eronder draait.

LaagDetail
FrameworkNext.js 16.1.6 (App Router, React 19, server actions voor de 14 API-routes).
TaalTypeScript 5 (strict mode).
StylingTailwind CSS v4 via @tailwindcss/postcss. Donker thema met CSS-variabelen in globals.css. Fredoka (display) + Nunito (body).
StateZustand v5 met persist-middleware → localStorage (key: world-smid-store).
AIGoogle Gemini 3.1 Pro Preview via @google/genai voor tekst + structured output. Imagen 4 voor elk beeld.
OpslagFirestore voor werelden + archief, GCS voor gegenereerde beelden.
RuntimeGoogle Cloud Run europe-west4. Source deploy — geen Dockerfile, geen cloudbuild.yaml; gcloud run deploy containeriseert de Next.js-app automatisch.
WAAROM DIT BESTAAT

De eerlijke versie: kinderen vragen keer op keer om hetzelfde soort verhalen met dezelfde personages, en die om 21.00 uur allemaal hardop voorlezen kost echt tijd en energie. Bedtime Stories neemt het deel van die lus over dat mechanisch is — het genereren van hoofdstukken, het illustreren, het bijhouden van variatie — en laat het kiezen aan het gezin. De gepubliceerde portfolioversie is dezelfde engine die de privé-deployment voor het gezin draait; de privéversie draagt de eigen personages en werelden van het gezin, die op de homelab blijven. Die scheiding kan alleen omdat de verhaalengine niets weet van de werelden die hij rendert — dezelfde discipline waarop Luminary is gebouwd, hier zodat één engine twee totaal verschillende werelden kan dragen.

Lees de broncode op GitHub →