Hear the script before paying to film it

Filming is $0.40 a shot. Speaking the same words is a few hundredths of a cent.
So the narrative — does it flow, is the joke in the right place, does it drag —
can now be judged for about one percent of the cost, using the opening stills
as placeholders and the pet's real voice.

It also replaces a guess with a measurement. Line length was estimated from a
word count and hoped to fit eight seconds; speaking it says exactly how long it
takes, and a line that runs over is flagged before anything is filmed rather
than after.
This commit is contained in:
gator 2026-09-07 23:27:57 +00:00
parent 13a45d2e87
commit fb1d9f229e

View file

@ -371,7 +371,16 @@ function render(r) {
+ 'pet’s likeness. Cutaways sit across the joins so the cut is never on screen. '
+ 'All of it has been chosen for you — change anything you disagree with, or '
+ 'go straight to filming.</p>'
+ '<h2 class="sec">The shots</h2>'
+ '<h2 class="sec">Hear it first</h2>'
+ '<div class="card" id="pzcard">'
+ '<p class="foot" style="margin:0 0 14px">Filming costs real money; speaking the '
+ 'words costs about a hundredth of a cent. Run it through in your pet&rsquo;s voice '
+ 'over the opening stills first, and only film once the timing and the jokes land.</p>'
+ '<button type="button" class="btn ghost" id="pzmake">Speak the script</button>'
+ '<span class="foot" style="margin-left:12px" id="pznote"></span>'
+ '<div id="pzout" style="margin-top:16px"></div>'
+ '</div>'
+ '<h2 class="sec" style="margin-top:34px">The shots</h2>'
+ s.segments.map(function (x) { return shotHtml(x, false); }).join("")
+ own
+ film;
@ -521,6 +530,7 @@ function wire(locked) {
};
});
bindPics();
bindPreviz();
bindFilm((DATA && DATA.project) || {});
bindPost();
if (locked) { bindBottom(); return; }
@ -910,6 +920,124 @@ async function loadTakes() {
});
}
// The run-through: each opening still held for exactly as long as its line
// takes to say. No video is generated, so this costs nothing to watch again.
var PZ = null, PZTIMER = null;
function pzPaint(lines) {
var el = document.getElementById("pzout");
if (!el) return;
var segs = ((DATA && DATA.script && DATA.script.segments) || []);
var byIdx = {};
(lines || []).forEach(function (l) { byIdx[l.index] = l; });
PZ = segs.map(function (s) {
var l = byIdx[s.index] || {};
return { index: s.index, url: l.url || "", seconds: l.seconds || 0,
over: !!l.over, plate: s.plate_url || "", line: s.line || "" };
}).filter(function (x) { return x.url; });
if (!PZ.length) { el.innerHTML = ""; return; }
var total = PZ.reduce(function (a, b) { return a + (b.seconds || 0); }, 0);
var bought = segs.length * 8;
var over = PZ.filter(function (x) { return x.over; });
el.innerHTML = '<div class="stage" style="margin-bottom:12px">'
+ '<img id="pzimg" alt="opening still" style="width:100%;aspect-ratio:9/16;'
+ 'border-radius:10px;background:#000;object-fit:cover;display:block">'
+ '</div>'
+ '<audio id="pzaud"></audio>'
+ '<button type="button" class="btn" id="pzplay">&#9654; Play the run-through</button>'
+ '<span class="foot" style="margin-left:12px" id="pzwhere">'
+ PZ.length + ' lines &middot; ' + total.toFixed(1) + 's spoken, '
+ bought + 's of shots</span>'
+ (total > bought
? '<div class="warn" style="margin-top:12px">Spoken, this runs '
+ total.toFixed(1) + ' seconds but only ' + bought + ' seconds are being '
+ 'filmed. Something will be cut off. Shorten the words, or buy more length.</div>'
: '')
+ (over.length
? '<div class="warn" style="margin-top:12px">Shot' + (over.length > 1 ? 's ' : ' ')
+ over.map(function (x) { return x.index; }).join(", ")
+ ' will not fit in eight seconds: '
+ over.map(function (x) { return x.index + " runs " + x.seconds.toFixed(1) + "s"; }).join(", ")
+ '. Trim those lines before filming.</div>'
: '')
+ '<div style="margin-top:14px">'
+ PZ.map(function (x) {
return '<div class="foot" style="padding:6px 0;border-top:1px solid var(--line)">'
+ 'shot ' + x.index + ' &middot; ' + x.seconds.toFixed(1) + 's'
+ (x.over ? ' <span style="color:var(--accent2)">over</span>' : '')
+ ' &middot; ' + esc(x.line.slice(0, 64)) + '</div>';
}).join("")
+ '</div>';
var img = document.getElementById("pzimg"),
aud = document.getElementById("pzaud"),
btn = document.getElementById("pzplay"),
where = document.getElementById("pzwhere");
function stop() {
aud.pause();
if (PZTIMER) { clearTimeout(PZTIMER); PZTIMER = null; }
btn.innerHTML = "&#9654; Play the run-through";
}
function playFrom(i) {
if (i >= PZ.length) { stop(); return; }
var x = PZ[i];
img.src = x.plate || "";
where.textContent = "shot " + x.index + " of " + PZ.length + " · " + x.seconds.toFixed(1) + "s";
aud.src = x.url;
aud.onended = function () { playFrom(i + 1); };
aud.play().catch(function () { stop(); });
}
btn.onclick = function () {
if (!aud.paused) { stop(); return; }
btn.innerHTML = "&#9646;&#9646; Stop";
playFrom(0);
};
}
function bindPreviz() {
var b = document.getElementById("pzmake");
if (!b) return;
var note = document.getElementById("pznote");
async function poll() {
var waited = 0;
return await new Promise(function (done) {
var t = setInterval(async function () {
waited += 4;
var g = await api("make_previz", { email: email, project_id: pid });
if (g.ok && !g.pending) { clearInterval(t); done(g); return; }
if (g.ok && g.pending && note) note.textContent = g.status_text || "";
if (waited > 240) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); }
}, 4000);
});
}
b.onclick = async function () {
hideMsg("msg");
b.disabled = true; b.textContent = "Speaking…";
var r = await api("make_previz", { email: email, project_id: pid });
if (r.ok && r.pending) { note.textContent = r.status_text || ""; r = await poll(); }
b.disabled = false; b.textContent = "Speak it again";
if (!r.ok) { note.textContent = ""; showMsg("msg", r.error || "Could not speak that."); return; }
note.textContent = r.status_text || "";
pzPaint(r.lines || []);
};
// Already spoken? Show it without asking again.
api("make_previz", { email: email, project_id: pid }).then(function (r) {
if (r && r.ok && !r.pending && (r.lines || []).length) {
b.textContent = "Speak it again";
note.textContent = r.status_text || "";
pzPaint(r.lines);
}
});
}
function bindPost() {
loadTakes();
// The editing bay is opt-in. Most people want to watch the thing and leave.