The runtime in the header is the sum of the edit_seconds the writer typed,
which is a guess made from a word count. timingHtml renders what the lines
actually take when the cast speaks them, and how far over the target that
puts the trailer - which is the number a revision has to cut to.
Shots sharing a scene are drawn from one frame of that place. When the
grouping is wrong the symptom is a set that drifts, and until now the
slug doing the grouping was not on the page at all.
The writer now reads the book and commits to what the story IS before writing
a single shot - premise, protagonist, the emotional engine stated as a moment,
what a viewer must understand, which moments have to be SHOWN rather than
narrated, and how it ends so it knows what it is withholding.
That document is worth more to an author than the script is. It is where a
misreading of the book is visible and cheap to fix; by the time it is
twenty-six shots and a storyboard it is neither. So it sits above the shots,
collapsed, and the two lists that decide whether the trailer works - what a
viewer must understand, and what must be dramatised - are open by default.
"What a viewer must understand" is also literally the checklist the
comprehension test measures against, so the two panels now read as a pair: here
is what it must convey, and here is what three readers actually took away.
The review panel rendered the old critique shape - errors, suggestions,
strengths. The comprehension test produces something else: three summaries
written by readers who saw only the script, and the writer's own list of what
the trailer failed to convey.
The gap list leads, because that is what the revision consumes and what the
author needs to decide about. Each reader's summary is then shown verbatim,
with what they were still asking and whether they would watch the film -
reading three strangers describe your trailer badly is the most persuasive
evidence there is that it does not tell the story.
"Got wrong" is called out separately from "missed". A trailer that leaves
something out is incomplete; one that leads a viewer to the wrong conclusion is
worse, and they are different problems to fix.
The trailer was all-or-nothing. If one shot was wrong - a line cut short, a
character duplicated, a scene missing from the front - the only lever was to
film all twenty-four again.
Every shot now carries its own controls: describe a change and have the writer
turn it into a proper shot, draw its two frames, film just that one, watch the
clip that came back, and read the machine check that compared what it says with
what the script says. "Add a scene here" sits between shots, because the note
that prompted this was that the trailer needed an opening the writer never
thought to include.
Assemble is separate and cheap - it joins whatever has been filmed - so
re-cutting after changing one shot costs nothing but the one shot.
Prices are on the buttons because they differ by two orders of magnitude:
rewriting a shot is cents, drawing it is fourteen, filming it is about a
dollar.
The four scripts had no next step. Everything after "read them" lived in the
API and nothing on the page reached it.
The choice of head writer is the author's, and it is a judgement about concept
rather than a score, so it is a button on each tab rather than a leaderboard.
Naming the lead first also changes what the reviews are for: three models
arguing about how to fix THIS script is a more useful argument than three
models arguing about whose is better.
Reviews are split the way they come back - errors are things that are wrong
and are worth fixing, suggestions are matters of judgement, and "keeps" is
what a revision must not quietly remove. The author's shot notes outrank all
of it and the revise button says so.
Film is priced and gated on having a storyboard, because every shot needs an
opening frame to film from.
Once you navigated away from a trailer, the only route back to it was the URL.
The page offered a way to start work and no way to return to it, which meant
every finished run was effectively lost the moment you closed the tab.
The list leads with what someone actually wants to know at a glance - is it
finished, did every writer survive, has anything been drawn, what has it cost -
rather than with an id and a timestamp. A run still working shows what it is
doing and keeps polling, so this is also the page you land on to check
progress.
trailer.html gets a link back, so the two pages are a loop rather than a
one-way trip.
Uploading "Memory Road interior 12-28.pdf" produced a book, a run and a
storyboard all headed "Memory Road interior 12-28". The title is derived from
the filename because that is the only thing an upload carries, and publishers
name files after print runs.
Guessing harder is the wrong fix - stripping "interior 12-28" would be a rule
that works on one file. Letting the author type the title takes eight lines and
is right for every file. The author field goes in at the same time; it costs
nothing and a trailer should know whose book it is.
The draw button is live and priced from what that writer actually wrote -
three sheets a character plus an opening and a closing frame a shot - so a
14-shot trailer costs $3.33 and a 30-shot one costs more, rather than both
being quoted an average.
Cast sheets render as head/front/side, because they are the thing that stops a
face drifting between shots and the author should be able to reject a face
before seventy pictures are drawn from it.
Progress is per picture rather than a spinner: drawing takes minutes and a
spinner that might mean "working" or "hung" is worse than a count.
"Drop the manuscript here or click to choose a file" is a drop zone pretending
to be a button - it reads as an instruction rather than a control, and on a
laptop nobody drags a file from anywhere. There is now an actual button. The
zone still accepts a drop, because that costs nothing once the button exists.
The paste box is gone. It was there for a manuscript that would not upload
cleanly, but a book is 100,000 words and nobody pastes that; keeping it meant
offering a route that only works for the case it was not for. Word and PDF
cover it, and a PDF that turns out to be a scan is refused with the reason.
Takes a .docx, a text .pdf or a .txt up to 6MB, or pasted text for anything
that will not upload cleanly. The upload itself only stores the file and writes
a row - extraction runs afterwards on the server, because ptl_api is
synchronous with a five second budget and parsing a novel is not a five second
job. The page polls until the book says ready, or says why it could not be
read.
readFileB64 lives here rather than in app.js: app.js's resizeImage goes through
an <img> and a canvas, which is right for a phone photo and fatal for a .docx,
and this is the only page that uploads a document.
Reading a trailer tells you what happens; hearing it tells you the line is
four seconds too long and the grandmother sounds thirty. So the play button
ships before the draw button - speaking a whole trailer costs fractions of a
cent against five dollars to draw it - and it prints the spoken length against
the length the writer planned, which is the most common fault in these scripts.
The note box saves as you stop typing. A note you have to remember to save is
a note that gets lost when you switch tabs.
The draw button is priced from what the writer actually wrote - three sheets a
character plus an opening and a closing frame a shot - rather than from an
estimate, and is disabled until that stage lands.
The page has carried a commented-out copy of its own tail since a bad patch
spliced it in. It is inert, but 66KB of duplicated source in a file that gets
patched regularly is a hazard worth removing. Deleted in parts because the
patch API can only remove text by quoting all of it.
The previous repair re-inserted "var price = ..." at the point it replaced,
which landed between "var head = '...'" and its continuation. Semicolon
insertion then ended the head statement early, so the price and everything
after it fell out of the string and head rendered as a bare opening div.
var price is already declared above; the continuation line just needs to follow
the head assignment directly.
My previous patch put a dollar sign next to a quote in the REPLACEMENT text.
That is a substitution token, so the whole rest of the file was spliced in and
script.html went from 82KB to 148KB with render() cut in half. Writing it as a
$ escape did not avoid it either - that is decoded before the substitution
runs and springs exactly the same trap.
- The broken line is restored, with the currency sign as the HTML entity $
which contains no dollar character at all.
- The spliced duplicate is commented out to end of file. The live copy is the
one above it, so every future anchor still matches the live code first.
- Price now comes from the fresh quote get_project returns rather than the
price_usd stamped on the row at creation, which ignored the markup change.
Patch order matters here: the corrected line itself contains the fragment text,
so the fragment is dealt with first.
price_usd is written onto the project row when it is created, so changing the
markup constant left every existing project quoting the old number - the page
still said 40.85 after the server had recomputed it to 8.13.
get_project already returns a fresh quote. Use it, and fall back to the stored
value only for a response that predates it.
Making step 3 clickable was not enough: render clamped VIEW back down to the
project's stage on every draw, so the click set VIEW = 3 and the redraw
immediately put it back to 2. The Filming panel stayed unreachable.
- "off the clock" fired on ANY shot not within 1.2s of the clip length, so a
7.0s line in a 10s shot was flagged alongside a genuine 11.4s overrun. Under
is fine - the engine finishes early and waits. Only an overrun gets cut off
mid-word, and only that is now flagged, as "runs long".
- A cutaway could be made or remade but never removed, and the subject is the
writer's guess so it is often wrong. There is now a Remove button.
- The Filming step was disabled until the project was already filming, so the
only way to start was a button at the bottom of the Layout page. Approved now
reaches step 3.
- "Cutting to the clock ... rewrites how the lines are divided" is no longer
true - it measures and no longer moves boundaries, because Setup shots does
the division locally. Copy corrected.
- "speaking the words costs about a hundredth of a cent" was wrong by three
orders of magnitude; a measured run is about 16 cents.
The one button did two unrelated jobs and showed the result of neither.
- Setup shots is now local division only: Python at the engine's own measured
speaking rate, ~500ms, free, no LLM and no TTS. Answers "divide with local
code, because each voice has a different cadence".
- Script audio is a separate button that reads it aloud AND SHOWS THE PLAYER.
The audio was always being made and uploaded, then rendered into #alignout,
which only exists on the Layout view - so pressing the button on the Words
view produced fifteen seconds of nothing.
- The voice is now named. With an Omni engine the film ships Omni's own native
voice, so the ElevenLabs read-through is a stand-in and says so.
- Rewrites are no longer capped at three. The real cost from OpenRouter is
shown beside the buttons, added to the bill at cost with no markup.
- "Approve and lock it" now explains what it does.
A measured run of the 80s NIMBY script came back at $0.1589 for 946 characters
- the ElevenLabs rate of $22 per 131,000 - so the copy understated it by more
than tenfold. Compute it from the words actually in the box instead.
The whole piece is now editable at the top of the review page, the rewrite
controls sit directly under it rather than below eight shot cards, and cutting
the narrative into shots is an explicit Setup shots button.
- narrative editable via set_narrative; shots marked stale until re-cut
- writer dropdown (Grok 4.6/4.5, Claude Opus 5/Sonnet 5, GPT-5.6/5.5, Gemini 3.1)
- align no longer runs unasked on page load, which spoke the script and moved
every shot boundary for a cent nobody asked to spend
- align's fit check reads shot length from the model instead of assuming 8s,
which reported every Omni shot 2s short
Publishing wrote an HTML file into this repository and triggered a full site
deploy on every share. The page is now rendered per request on its own
hostname, so there is nothing to publish: share_link just marks the video
shared and hands back the address.
Preview crawlers do not run JavaScript, so the /v/<code> route — which is the
home page plus a client-side redirect — gives them nothing. The published
/v/<code>.html page carries real og:image, og:video and a real video element in
the served source. Five characters longer, and the difference between a card
someone taps and a bare grey link.