From 70fa5ae9bbfbe5578071591c18ce8da45b5423f4 Mon Sep 17 00:00:00 2001 From: gator Date: Mon, 14 Sep 2026 23:26:01 +0000 Subject: [PATCH] Split Setup shots from Script audio; name the voice; drop the rewrite cap 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. --- script.html | 136 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 23 deletions(-) diff --git a/script.html b/script.html index 8b7d1f9..a8e4e6d 100644 --- a/script.html +++ b/script.html @@ -253,7 +253,7 @@ function render(r) { + '
' + videoSecondsOf(p) + ' seconds · ' + (p.segments || 4) + ' shots' + (modelFor(p) ? ' · ' + esc(modelFor(p).label) : '') + '
' + '
' + (canEditScript - ? left + " rewrite" + (left === 1 ? "" : "s") + " left" + ? (used === 1 ? "1 rewrite" : used + " rewrites") + " so far" : "Words approved") + '
'; // The whole piece, which is what was actually written - the shots below are @@ -277,19 +277,50 @@ function render(r) { // piece through ElevenLabs to find real word endings - about a cent, fifteen // seconds, and it rewrites where every shot begins and ends. This used to // happen unasked on page load. + // Two jobs that used to be one button, separated because they are not the + // same thing and do not cost the same thing. + // + // Setup shots divides the words. Local Python at the engine's own + // measured speaking rate. Free, instant, no network call. + // Script audio reads it aloud so you can hear it. ElevenLabs, about 16c + // for an 80s piece. Measures; moves nothing. + var vp = r.voice || {}; + var voiceLine = vp.formula + ? '

The voice: ' + + esc(vp.formula) + '.
In the finished film: ' + esc(vp.film_voice || '') + '.

' + : ''; var setup = !canEditScript ? '' : '

Setup shots

' - + '

Speaks the whole piece once in your pet’s ' - + 'voice, measures where every word actually falls, and cuts the shots at real sentence ' - + 'endings. It divides the words — it does not change them. ' - // Measured, not guessed: an 80s piece came back at $0.1589 for 946 characters, - // which is the ElevenLabs rate of $22 per 131,000. "About a cent" was wrong by - // more than tenfold, so quote the real number for the words actually on screen. - + 'Costs about ' + Math.max(1, Math.round(narrative.length * 22 / 1310)) - + ' cents at this length.

' + + voiceLine + + '

Divides the piece into shots at ' + + esc(vp.engine || 'the engine') + '’s own measured speaking rate. Runs here in ' + + 'code — nothing is sent to a language model and nothing is spoken, so it is ' + + 'instant and free. Press it whenever the words change.

' + '' - + '
'; + + '' + + '
' + + + '

Script audio

' + + '

' + + (vp.readthrough_is_standin + ? 'Reads the whole piece aloud so you can hear the timing and the jokes. ' + + 'This is ElevenLabs standing in. ' + esc(vp.engine || 'The engine') + + ' performs the real thing itself from the written description above and will ' + + 'phrase it differently — there is no way to hear its real voice without ' + + 'paying to film it.' + : 'Reads the whole piece aloud in the same voice that will be dubbed onto the film.') + // The ElevenLabs rate is 22 dollars per 131,000 characters. A measured + // run came back at 15.89 cents for 946 characters, so quote it from what + // is actually on screen. Deliberately spelled out: a dollar sign before a + // quote is a substitution token to patch_git_file and once shipped a broken + // page - BROWSER_TESTS.md:265 - so none appear in this file's patch text. + + ' Costs about ' + Math.max(1, Math.round(narrative.length * 22 / 1310)) + + ' cents at this length, and changes none of the words.

' + + '' + + '' + + '
' + + ''; var staleNote = (stale && canEditScript) ? '
The words above have changed since these shots were cut, so this is ' @@ -409,7 +440,7 @@ function render(r) { + '
' + 'Be specific. “Too polite, he would be smugger” beats “make it funnier”. ' + 'To change a word yourself, edit the piece above and press Setup shots.' - + '
' + + '
' + (WRITERS.length ? '
' + 'Different models are different comedians. The same note sent ' @@ -422,9 +453,29 @@ function render(r) { }).join("") + '
' : '') - + '' + + '' + '' + // What the writing has cost so far. Rewrites are no longer capped at + // three - they are billed onto the video at cost, with no markup - so + // the number that matters is the running total, shown where the decision + // to spend another one is actually made. + + (Number(r.script_usd || 0) > 0 + ? 'Writing so far: ' + + Math.round(Number(r.script_usd) * 100) + 'c' + + (Number(r.last_write_usd || 0) > 0 + ? ' (last rewrite ' + Math.max(1, Math.round(Number(r.last_write_usd) * 100)) + 'c)' + : '') + + ' — added to the bill at cost, not marked up.' + : 'Rewrites are added to the ' + + 'bill at what they cost, not marked up.') + // "What happens if I approve?" was a fair question with no answer on the + // page. Two lines, next to the button that does it. + + '

Approve and lock it settles ' + + 'the writing: the words, the shot division and the running order stop being editable, ' + + 'and the project moves on to setting up each shot — the scene, the camera and ' + + 'the opening frame — and then to filming. Nothing is filmed and nothing is ' + + 'charged for filming at that point. It is the point after which rewriting means ' + + 'starting a new video.

' + ''; var body; @@ -1426,16 +1477,41 @@ function bindBottom() { load(); }; - var su = document.getElementById("setupshots"); - if (su) su.onclick = async function () { - hideMsg("msg"); - // Save anything typed but not saved first, so the cut is of the words on - // screen rather than the words on file. + // Flush an unsaved edit before either button acts, so both work on the + // words that are on screen rather than the words on file. + async function flushNarrative() { var box = document.getElementById("narr"); if (box && box.value.trim().length >= 10) { await api("set_narrative", { email: email, project_id: pid, text: box.value }); } - su.disabled = true; su.textContent = "Speaking it…"; + } + + var su = document.getElementById("setupshots"); + if (su) su.onclick = async function () { + hideMsg("msg"); + su.disabled = true; su.textContent = "Dividing…"; + await flushNarrative(); + var r = await api("setup_shots", { email: email, project_id: pid }); + su.disabled = false; su.textContent = "Setup shots"; + if (!r.ok) { showMsg("msg", r.error || "Could not divide that."); return; } + // Notes are the splitter telling you the script is the wrong length for + // the shots bought. That is a writing problem and only you can fix it, so + // it is shown rather than swallowed. + var out = document.getElementById("setupout"); + if (out) { + out.innerHTML = (r.notes || []).map(function (t) { + return '
' + esc(t) + '
'; }).join(""); + } + showMsg("msg", r.status_text || "Divided into shots.", "ok"); + load(); + }; + + // Reading it aloud. Separate button, separate money, and it moves nothing. + var ra = document.getElementById("readaloud"); + if (ra) ra.onclick = async function () { + hideMsg("msg"); + ra.disabled = true; ra.textContent = "Speaking it…"; + await flushNarrative(); var r = await api("align_script", { email: email, project_id: pid, refresh: true }); if (r.ok && r.pending) { var waited = 0; @@ -1448,11 +1524,25 @@ function bindBottom() { }, 4000); }); } - su.disabled = false; su.textContent = "Setup shots"; - if (!r.ok) { showMsg("msg", r.error || "Could not cut that."); return; } + ra.disabled = false; ra.textContent = "Read it aloud"; + if (!r.ok) { showMsg("msg", r.error || "Could not read that."); return; } if (r.note) { showMsg("msg", r.note); return; } - showMsg("msg", "Cut into shots at real sentence endings.", "ok"); - load(); + // THE PLAYER. This is what was missing: the audio was made and uploaded + // and then rendered into an element that only exists on the Layout view, + // so pressing the button produced fifteen seconds of nothing. + var out = document.getElementById("readout"); + if (out) { + out.innerHTML = + (r.audio ? '' : '') + + '

' + (r.spoken || 0).toFixed(1) + + 's spoken by ElevenLabs. The shot division above is unchanged — this is a ' + + 'read-through, not a re-cut.

' + + (r.shots || []).map(function (s) { + return '
' + + 'Shot ' + s.index + ' · ' + (s.start || 0).toFixed(1) + 's to ' + + (s.end || 0).toFixed(1) + 's
'; }).join(""); + } + showMsg("msg", "Read through in " + (r.spoken || 0).toFixed(1) + " seconds.", "ok"); }; var rv = document.getElementById("revise");