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.
Directory indexes fall back to the root page, but files with an extension are
served directly. If this resolves, per-video pages with real Open Graph tags in
the head are possible on a static host, at the cost of five characters.
Throwaway file to establish whether static per-video pages with baked-in
Open Graph tags can live at the short URL, or whether the host will keep
falling back to the root index.
A link straight to the file cannot be counted — the recipient fetches bytes
from the file host and we never hear about it. The short link runs through our
own domain, so every open is a page load, and it is 33 characters against 74.
The host serves this file for any extensionless path it does not recognise, so
/v/<code> lands here rather than at v/index.html. Four lines at the top of the
head hand it on before anything renders.
Deliberately the first thing in the head and wrapped in a try, so a share link
resolves before any paint and a failure here can never take the homepage down
with it.
A link straight to the file is untrackable — the recipient fetches bytes from
the file host and we never hear about it. Routing through paytolaugh.com makes
every open a page load that can be counted, and gives somewhere to put a real
player and a preview card.
The code is the first eight hex characters of the project id. Those are already
unique across every project and the space is 4.3 billion, so there is no
mapping table to build or keep in sync — the server resolves by prefix and
checks for ambiguity. 33 characters against 74 for the raw file.
Sharing stays deliberate: the page only resolves a project whose owner has
pressed Share.
Sharing from here is the common case — you are looking at the thing you want to
send. Uses the native share sheet where there is one, otherwise copies, and the
link carries a filename so it ends in .mp4 for clients that read the URL rather
than the headers.
The file URL ends in /public with no filename, which is fine for anything that
reads headers — it already serves Content-Type video/mp4 and a
Content-Disposition of final.mp4 — but clients that sniff the extension out of
the URL string get it wrong. A filename query is accepted and ignored by the
file service, so the link can end in .mp4 at a cost of a dozen characters.
The button uses the native share sheet where there is one, which on a phone is
the shortest path into a text message, and falls back to copying.
Sending the video itself by MMS puts it through carrier transcoding and it
arrives looking far worse than it left. So the picture carries the hook and a
link carries the video at full quality.
The still is 720x1280 and lands around 90KB at near-maximum quality — carrier
limits are typically 600KB and up, so there is no reason to compress hard. The
frame is chosen by sampling several moments and keeping the one that compresses
largest, which in practice means eyes open and in focus rather than mid-blink.
The play button sits at two thirds height so it never covers the face.
"I am not" and "extraordinarily unimpressed" are both three words and nowhere
near the same length, so any word budget is a guess about something we can
measure. Three attempts at tuning one were wrong in both directions.
Speaking the monologue once returns the start and end of every character, so
word boundaries are exact. This cuts each shot at the word whose end sits
nearest the boundary, preferring a sentence ending when one is close. On the
first real test it produced a 20 word shot landing at 7.8s and cutting on a
full stop, where the budget had said 18.
The run-through is spoken by ElevenLabs, but the finished audio is Veo's
performance re-voiced, and Veo enunciates more slowly - it was asked to, to
make the mouth movement readable. Measured on a real clip: 17 words ran 7.02s
filmed (about 2.4 words a second) against roughly 3.0 words a second spoken.
So the raw previz number understates the filmed length by about a quarter, and
the dead-air warning was correspondingly overstated - it claimed 2.4 seconds of
silence per shot where the filmed figure is nearer one. Show both numbers and
name the factor rather than quietly applying it.
The catalogue was 21 polished professional voices — podcast hosts and
narrators. None of them sounds like ninety pounds of jowly bulldog, which is
the actual complaint. Seven deep, gravelly, weathered voices are added, and
they go first because a talking animal usually wants weight rather than polish.
Without a group of their own they would not have appeared at all: the dropdown
only built male, female and neutral.
The first real measurement was the opposite of what was expected: four lines
spoke in 21.7s against 32s of video, so every eight second shot carries about
2.4 seconds of silence. That is almost certainly where the sound effects and
echo come from, since the model fills a gap it was given nothing to do with.
So flag short as loudly as long, and say what it means.