How it holds up
22 books and 71 pictures since August 2026. 13 of them are fully illustrated, and those are the only ones the cost figures below use. Small numbers, and they are real ones — this is a thing one family uses, not a demo.
What a book costs
Provider-reported usage recorded on each row as it happens, never estimated: the text generation plus every picture. Only fully illustrated books count — a book with half its pictures is a measurement of half a book. Waste is included. A picture that was drawn, billed and then failed to store is money the book cost, and leaving it out would flatter the figure while the section below describes that exact spend.
- Median
- $0.14
- Cheapest
- $0.10
- Dearest
- $0.19
- Books measured
- 13
How long a picture takes
Gaps between consecutive finished pictures, kept when they fall between 5 and 150 seconds — the route aborts its own provider call at 120, so anything longer cannot be one drawing. Nothing records which run a job belonged to, so a slow image and a person pausing between two clicks are indistinguishable here. Read it as a distribution, not a stopwatch.
- Median
- 30s
- Fastest
- 20s
- Slowest
- 91s
- Gaps in range
- 40
8 checks, and the bug each one exists because of
Every one of these was written after something got through. They run in two places, doing different jobs: a local hook gives fast feedback and can be skipped deliberately, and CI runs them on every pull request, where they gate the merge and cannot be bypassed. That second layer exists because the first one was bypassed on nearly every commit while it was assumed to be running. This list comes from the same manifest CI asserts against the scripts themselves, so it cannot quietly describe checks that no longer exist.
Voice selection
The Web Speech API exposes no gender, no quality, and no way to ask whether a voice is even a real voice. Apple ships sound effects — Zarvox, Bubbles, Bad News — that are local, en-US, and indistinguishable by any field from a narrator. A naive “first local voice in the right region” lands on one of them at bedtime.
Read-aloud sequencing
speechSynthesis.cancel() fires the same “ended” event a finished page does, so stopping skipped ahead and kept reading. Three separate bugs, one cause. The first fix reintroduced it somewhere new; what held was a fake engine that can fire events in the wrong order on purpose.
Cover prompt
Every cover was drawn from page one’s art prompt: a lookup returned null for the cover slot and a ?? 0 quietly turned that into “page one”. Nothing threw, the story was right, and character consistency worked perfectly — the cover was simply the same picture as the first page.
Illustration sequencing
This decides what gets drawn, so its mistakes are billed. It refuses to re-fire a slot the provider may already have charged for, stops dead on a cap or an expired session, and cannot start a second run from a page that refreshes itself on a timer.
Progress honesty
A progress bar must never reach 100% before the thing finishes, and must keep moving when a call overruns rather than parking — a bar stuck at full is worse than no bar. The estimates behind it are measured from real job rows, not guessed.
Colour contrast
40 colour pairs across both themes, read out of the stylesheet so they cannot drift from what ships. It found four real failures, including a pair that passed at 4.503 against a 4.5 threshold — passing by three thousandths is not passing on purpose.
Open redirect
The sign-in screen returns you where you were going, which is the canonical open-redirect footgun. Twenty-two hostile inputs — protocol-relative, encoded, backslash-disguised — are enumerated and must all fall back to the default.
Anonymity
This page is public, and the only reason it can be is that nothing on it is about a child. Fixtures carrying a name and titles are pushed through the same functions the page uses, and the output is asserted to contain none of them.
Which versions made what
Model ids are pinned dated snapshots rather than floating aliases, and the prompt version is stored with each book, so any output can be traced back to what produced it. Prompt versions are listed oldest first with what each one changed — every bump was a correction to something that had gone wrong, which is the part a version number on its own does not tell you.
| Version | Kind | Books |
|---|---|---|
| gpt-image-2-2026-04-21 | image | 19 |
| 2026-08-01.1The ceiling on an image prompt moved from 500 to 1500 characters. The old limit made valid output impossible — the character description the model is required to repeat verbatim was longer than the whole budget — so it changed what output could be accepted at all. | prompt | 2 |
| 2026-08-02.1Requests started carrying a response schema, so the model is constrained by the API rather than only by the prompt asking nicely. Two production failures came from trusting the prompt alone, and drift is non-deterministic, so local runs passing had proved nothing. | prompt | 4 |
| 2026-08-02.2The model began reporting which language it wrote in, so a memory written in Spanish produces a Spanish story. Deliberately kept outside the part of the response the parent can edit, so it cannot be rewritten by the client. | prompt | 3 |
| 2026-08-02.3The model started writing the cover's own art prompt. Before this the cover was drawn from page one's prompt, so every book's cover and first page were the same picture — nothing threw, and no test caught it. | prompt | 13 |
| gemini-3.5-flash | text | 22 |
What went wrong
Grouped by who paid and what is safe to do next, which is more useful than any error string — each category is a decision the system had to make. No message text is printed: an unanticipated provider message must not be able to reach a public page, so anything matching no known shape is counted and nothing more. The full text is in the server logs. Resolution is not tracked: when a person resolves a locked job by hand the row changes and no transition is recorded, so these are the current state and not a history.
Known waste: $0.04 across 2 jobs — billed by the provider, never delivered to a reader, and counted in the cost figures above rather than quietly dropped. “Known” is doing real work in that sentence: an outcome that ended without a clear answer has no recorded cost, so it could never be part of this total. There are none right now.
| What happened | State | Count |
|---|---|---|
| Drawn and billed, but not stored The image was produced, and paid for, and then storing it failed. Retrying would pay for it a second time, so the slot stays locked for a person. | failed | 2 |