Architecture
Storyboards that keep each face the same.Every frame carries its score.
A filmmaker asks for a shot, and one FastAPI service pulls the character's frozen card and reference sheet from an in memory story bible and sends them to Nano Banana. A referee crops the face out of the new frame, embeds it and scores it against the sheet, and the frame is saved with that score on it.
30 s
a frame from Nano Banana, on a Vertex project shared with every team in the room
60 renders
per story at most, counted on the server so a loop bug cannot spend the budget
170 min
from the first commit at 11:06 to live on Cloud Run at 13:56
0.48
cosine between two different people's sheets on whole frames, which is why the referee crops the face first
New York --:-- simulated
Hover a part to trace what it touches. Click it for its notes.
- Built by me
- Managed by Google
- Outside system
- Main data path
- On demand
- Credentials cross
When it breaks
What each failure does, and what it does not do.
-
Vertex answers 429
The lab project was shared with every team in the room, so backoff is built in: 2, 4 and 8 seconds, then the shot is marked failed.
backend/app/consistency.py:140
-
A face drifts
The shot is redrawn once with a stronger instruction. If it still scores under 0.35, it is flagged on the board instead of retried again.
backend/app/consistency.py:362
-
A tab fails to import
The API mounts inside a try/except, so the app still boots, serves its screens and reports the error on its health check.
backend/app/main.py:64
What I tuned
Small choices that keep it fast and cheap.
-
Crop before you embed
Whole frame embeddings scored two different people's sheets at 0.48. A Gemini face box crop first leaves the identity signal.
backend/app/consistency.py:257
-
Thinking where it pays
Voice Cards are compiled once on Gemini 2.5 Pro, which always reasons. Lines are spoken on Flash with thinking off, because a character does not deliberate before speaking.
backend/app/voice.py:29
-
Search in numpy
About 50 chunks, so a full cosine scan in numpy, fused with token overlap by reciprocal rank so names and sluglines still match.
backend/app/bible.py:142
Guards
Rules the code enforces, not rules people remember.
-
A hard render cap
60 renders per story, counted on the server before each one, so a loop bug cannot spend the budget.
backend/app/api/board.py:38
-
Canon needs a person
The Archivist only proposes facts, with its reasoning attached. Nothing becomes canon until someone promotes it.
backend/app/extract.py:7
-
The voice bar
A line must score 0.62 against its character's fingerprint, set between the best impostor line at 0.613 and the worst real one at 0.647.
backend/app/voice.py:46
