Remove the duplicated page tail left after the closing html tag

A dollar sign before a quote is a substitution token to patch_git_file, so a
price label spliced the rest of the page in after itself. trailer.html carried
33 KB of raw JavaScript after its closing tag, which rendered as garbled text
in the page body; script.html carried 65 KB, commented out. The copy above the
first closing tag was verified complete: it parses, and it defines every
function and calls every action the removed text did.
This commit is contained in:
gator 2026-09-19 13:51:23 +00:00
parent f8c72e94ae
commit 3f5bd70f24

View file

@ -1145,707 +1145,3 @@
</script>
</body>
</html>
+ (filmed * 0.02).toFixed(2) + '</button>');
}
if (unfilmed && !undrawn)
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
+ '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots')
+ ' &middot; $' + (unfilmed * 0.75).toFixed(2) + '</button>');
if (filmed)
out.push('<button type="button" class="btn primary" data-assemble="' + esc(meta.id)
+ '">Make the trailer &middot; ' + filmed + ' of ' + shots.length + ' shots</button>');
}
return out.join("");
}
// A cut may move shots as well as drop them, which makes it a different
// film rather than a shorter one. Worth saying out loud.
function reordered(ids) {
var a = (ids || []).map(String);
var b = a.slice().sort();
return a.join("") !== b.join("");
}
// What the storyboard will actually cost to draw. Three sheets for every
// character who is seen or heard, one more per extra day they are dressed
// differently, and two frames per shot.
function boardImages(shots, cast) {
var seen = {};
shots.forEach(function (s) {
(s.visible_characters || []).forEach(function (c) { seen[String(c)] = 1; });
(s.audio || []).forEach(function (a) {
if (String(a.text || "").trim()) seen[String(a.character_id)] = 1; });
});
var present = cast.filter(function (c) { return seen[String(c.id)]; });
var days = {};
shots.forEach(function (s) {
var d = String(s.day || "").trim();
if (!d) return;
(s.visible_characters || []).forEach(function (c) {
(days[String(c)] = days[String(c)] || {})[d] = 1; });
});
var extra = 0;
present.forEach(function (c) {
var wd = c.wardrobe_by_day || {};
var base = Object.keys(wd).sort()[0] || "";
Object.keys(days[String(c.id)] || {}).forEach(function (d) {
if (d !== base && wd[d]) extra += 1; });
});
return present.length * 3 + extra + shots.length * 2;
}
function mmss(n) {
return Math.floor(n / 60) + ":" + String(Math.round(n % 60)).padStart(2, "0");
}
// The header's runtime is the sum of what the writer typed. This is what the
// lines take when the cast actually says them, which is a different number
// and the only one worth cutting against.
function timingHtml() {
var t = PKG.timing;
if (!t || !t.measured_seconds) return "";
var over = Number(t.over_by || 0);
var line = "Spoken by the cast this script runs <b>" + mmss(t.measured_seconds)
+ "</b>, against a " + mmss(t.target_seconds) + " target";
if (over > 5) line += " — <b>" + Math.round(over) + " seconds over</b>."
+ " Revising the whole script will cut that much.";
else if (over < -15) line += " — " + Math.round(-over) + " seconds short.";
else line += " — that fits.";
var worst = (t.longest_lines || []).slice(0, 3).map(function (l) {
return "<li>" + Number(l.seconds || 0).toFixed(1) + "s &middot; shot "
+ esc(String(l.shot)) + " &middot; “"
+ esc(String(l.text || "").slice(0, 80)) + "”</li>"; }).join("");
return '<div class="timing' + (over > 5 ? " over" : "") + '"><div>' + line + "</div>"
+ (worst && over > 5 ? "<ul>" + worst + "</ul>" : "") + "</div>";
}
// A job lives in the worker's memory and dies with it. Every deploy kills
// one, and the flag it set stays set, so anything older than this is treated
// as gone rather than leaving the page waiting on it for ever.
function busyCuts() {
if (String(PKG.cuts_state || "") !== "thinking") return false;
var t = Date.parse(PKG.cuts_started || "");
if (!t) return false;
return (Date.now() - t) < 15 * 60 * 1000;
}
// ---- the storyboard player -------------------------------------------
// One call fetches the whole reel; after that nothing touches the network,
// which is what makes stop and jump-to instant.
var REEL = null, RAT = -1, RTIMER = null;
var reelAudio = new Audio();
function reelHtml() {
var st = String(PKG.reel_state || "");
var shots = (PKG.shots || []);
if (!shots.length) return "";
var running = st && st !== "ready" && !/^failed/.test(st);
var out = '<div class="reel"><h4>Watch the storyboard</h4>'
+ '<p class="lede">The frames in order with the lines spoken over them — '
+ 'the trailer before it is filmed. Sequence problems are obvious here '
+ 'and nearly invisible in the script below.</p>';
if (running)
out += '<p class="busy">Speaking every shot… ' + esc(st) + '</p>';
else if (st === "ready") {
var spoken = shots.filter(function (s) { return s.reel_url; }).length;
var silent = shots.length - spoken;
out += '<p class="lede">' + spoken + ' shots have dialogue and are spoken'
+ (silent ? '; ' + silent + ' are silent and hold on the frame' : '')
+ '.</p>';
}
else if (/^failed/.test(st))
out += '<p class="cont">' + esc(st) + '</p>';
else if (st)
out += '<p class="cont">The last attempt stopped before it finished — '
+ 'the server restarted under it. Ask again.</p>';
if (!REEL) {
out += '<div class="tools" style="justify-content:center">'
+ '<button type="button" class="btn primary" data-reelgo="1"'
+ (running ? ' disabled' : '') + '>'
+ (st === "ready" ? "Play the storyboard" : "Prepare and play")
+ '</button></div></div>';
return out;
}
var s = REEL[RAT] || REEL[0] || {};
var lines = (s.lines || []).map(function (l) {
return '<div><span class="who">' + esc(l.who || "")
+ (l.lip_sync ? "" : " (v.o.)") + '</span>' + esc(l.text || "") + '</div>';
}).join("") || '<div class="w">no dialogue in this shot</div>';
out += '<div class="reelframes">'
+ '<figure><img id="rfa" class="on" src="' + esc(s.opening_url || "")
+ '" alt=""><figcaption>opens on</figcaption></figure>'
+ '<figure><img id="rfb" src="' + esc(s.closing_url || "")
+ '" alt=""><figcaption>ends on</figcaption></figure></div>'
+ '<div class="reelnow"><div class="t">' + esc(s.id) + ' · '
+ esc(s.title || "") + '</div>'
+ '<div class="w">' + esc(s.setting || "")
+ (s.scene ? ' · ' + esc(s.scene) : "")
+ (s.day ? ' · ' + esc(s.day) : "") + '</div>'
+ '<div class="said">' + lines + '</div></div>'
+ '<div class="reelprog"><i id="rprog"></i></div>'
+ '<div class="reelbar">'
+ '<button type="button" class="btn" data-reelstep="-1">◀ back</button>'
+ '<button type="button" class="btn primary" data-reeltoggle="1">'
+ (reelAudio.paused ? "Play" : "Pause") + '</button>'
+ '<button type="button" class="btn" data-reelstep="1">next ▶</button>'
+ '<button type="button" class="btn" data-reelstop="1">Stop</button>'
+ '<button type="button" class="btn" data-reeljump="' + esc(s.id)
+ '">Jump to this shot</button>'
+ '<span class="pos">' + (RAT + 1) + ' of ' + REEL.length + '</span>'
+ '</div></div>';
return out;
}
function reelPaint() {
var host = document.getElementById("reelhost");
if (host) host.innerHTML = reelHtml();
}
// The two frames split the shot's running time: the opening frame holds for
// the first half, the closing frame for the second. It is a storyboard, not
// an animation, but it reads as movement and it marks where the shot lands.
function reelShow(i) {
if (!REEL || i < 0 || i >= REEL.length) { reelStop(); return; }
RAT = i;
reelPaint();
var s = REEL[i];
var dur = Math.max(Number(s.seconds) || 0, Number(s.edit_seconds) || 0, 2.5);
if (s.audio_url) { reelAudio.src = s.audio_url; reelAudio.play().catch(function(){}); }
else reelAudio.pause();
var t0 = Date.now(), swapped = false;
clearInterval(RTIMER);
RTIMER = setInterval(function () {
var gone = (Date.now() - t0) / 1000;
var bar = document.getElementById("rprog");
if (bar) bar.style.width = Math.min(100, 100 * gone / dur) + "%";
if (!swapped && gone > dur / 2) {
swapped = true;
var a = document.getElementById("rfa"), b = document.getElementById("rfb");
if (a && b && REEL[RAT].closing_url) { a.classList.remove("on"); b.classList.add("on"); }
}
if (gone >= dur) reelShow(RAT + 1);
}, 120);
}
function reelStop() {
clearInterval(RTIMER); RTIMER = null;
reelAudio.pause();
REEL = null; RAT = -1;
openTab(CUR);
}
// A cut is an ordering, not a render. Playing one is free, so all of them
// can be looked at before anything is joined into a file.
function cutsHtml(meta) {
if (meta.writer !== RUN.lead_writer) return "";
var shots = PKG.shots || [];
var filmed = shots.filter(function (s) { return s.clip_url; }).length;
var cuts = PKG.cuts || [];
var st = String(PKG.cuts_state || "");
var out = '<div class="cuts"><h4>Shorter cuts of this same shoot</h4>'
+ '<p class="lede">Nothing here is re-filmed. A cut is an order to play '
+ 'the shots in, so making another one costs nothing — which is why '
+ 'the question is not “what is the right length” but “which '
+ 'version do people watch to the end”.</p>';
if (busyCuts()) out += '<p class="busy">Working out what to drop…</p>';
else if (st === "thinking") out += '<p class="cont">The last attempt stopped '
+ 'before it finished — the server restarted under it. Ask again.</p>';
else if (/^failed/.test(st)) out += '<p class="cont">' + esc(st) + '</p>';
out += cuts.map(function (c) {
var stat = STATS[c.id];
var play = (PLAYING === c.id)
? '<div class="playbox"><video id="cutv" controls playsinline></video>'
+ '<div class="bar2"><span id="cutat">loading…</span>'
+ '<button type="button" class="btn" data-stop="1">Close</button></div></div>'
: '';
var tools = '<button type="button" class="btn" data-play="' + esc(c.id) + '">'
+ (PLAYING === c.id ? 'Playing' : 'Watch it · free') + '</button>';
if (c.state === "made" && c.video_url)
tools += '<a class="btn" href="/videos.html">Published · in My videos</a>';
else if (c.state === "making")
tools += '<span class="busy">joining…</span>';
else if (filmed)
tools += '<button type="button" class="btn primary" data-makecut="' + esc(c.id)
+ '">Publish this version</button>';
return '<div class="cut"><b>' + esc(c.name || "") + '</b>'
+ '<span class="len">' + mmss(c.seconds || 0) + ' · '
+ (c.shot_ids || []).length + ' shots</span>'
+ (c.strategy ? '<p class="strat">' + esc(c.strategy) + '</p>' : '')
+ (c.why ? '<p class="why">' + esc(c.why) + '</p>' : '')
+ (c.sacrifices ? '<p class="sac">gives up: ' + esc(c.sacrifices) + '</p>' : '')
+ (stat ? '<p class="stat">' + stat.views + ' viewer'
+ (stat.views === 1 ? '' : 's') + ' · median ' + stat.median_percent
+ '% watched · ' + stat.completion_rate + '% reached the end</p>' : '')
+ '<p class="ids">' + esc((c.shot_ids || []).join(" "))
+ (reordered(c.shot_ids) ? ' &middot; reordered' : '') + '</p>'
+ '<div class="tools">' + tools + '</div>' + play + '</div>';
}).join("");
if (!cuts.length && st !== "thinking")
out += '<p class="cont">No cuts proposed yet.</p>';
return out + '</div>';
}
function reviewsHtml(meta) {
var mine = REVIEWS.filter(function (r) {
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
if (!mine.length) return "";
var gaps = mine.filter(function (r) { return r.reviewer === "__gaps__"; })[0];
var readers = mine.filter(function (r) { return r.reviewer !== "__gaps__"; });
var out = "";
if (gaps) {
var v = gaps.strengths && gaps.strengths.verdict;
var list = gaps.suggestions || [];
out += '<div class="gaps"><h4>What this trailer does not tell a viewer</h4>'
+ (v ? '<p class="verdict">' + esc(v) + '</p>' : '')
+ (list.length ? '<ol>' + list.map(function (g) {
return '<li>' + esc(g.what || "")
+ '<span class="fix"><span class="sh">shot ' + esc(g.shot || "?") + '</span> &rarr; '
+ esc(g.fix || "") + '</span></li>'; }).join("") + '</ol>' : '')
+ '</div>';
}
out += readers.map(function (r) {
var s = r.strengths || {}, e = r.errors || {};
if (typeof s !== "object") s = {};
if (typeof e !== "object") e = {};
function ul(items, cls) {
return (!items || !items.length) ? ""
: '<ul class="' + cls + '">' + items.map(function (x) {
return '<li>' + esc(typeof x === "string" ? x : JSON.stringify(x)) + '</li>';
}).join("") + '</ul>';
}
return '<div class="rd"><h4>' + esc(r.label) + ' watched it without reading the book'
+ (r.score ? '<span class="n" title="how much of the real story they got">'
+ r.score + '/100</span>' : '') + '</h4>'
+ (s.summary ? '<blockquote>' + esc(s.summary) + '</blockquote>' : '')
+ '<div class="kv">main character: ' + esc(s.who || "not clear to them") + '</div>'
+ '<div class="kv">what they want: ' + esc(s.wants || "not clear to them") + '</div>'
+ '<div class="kv">what is in the way: ' + esc(s.obstacle || "not clear to them") + '</div>'
+ (s.would_watch ? '<div class="kv">would they watch the film: '
+ esc(s.would_watch) + '</div>' : '')
+ (s.unanswered && s.unanswered.length
? '<div class="kv">still asking:</div>' + ul(s.unanswered, "") : '')
+ (e.missed && e.missed.length ? '<div class="kv">they missed:</div>' + ul(e.missed, "") : '')
+ (e.got_wrong && e.got_wrong.length
? '<div class="kv wrong">the trailer misled them:</div>' + ul(e.got_wrong, "wrong") : '')
+ '</div>';
}).join("");
return '<div class="revs">' + out + '</div>';
}
function paneHtml(meta, draft) {
var p = PKG, shots = (p.shots || []).slice().sort(function (a, b) {
return String(a.id) < String(b.id) ? -1 : 1; });
var cast = p.characters || [], voice = {};
cast.forEach(function (c) { voice[c.id] = c; });
var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0);
var head = '<div class="head"><h2>' + esc(p.title || meta.label) + '</h2>'
+ (p.tagline ? '<span class="tag">' + esc(p.tagline) + '</span>' : '') + '</div>'
+ (p.logline ? '<p>' + esc(p.logline) + '</p>' : '')
+ '<p class="facts">' + esc(meta.label)
+ (Number(meta.iteration) > 1 ? ' &middot; revision ' + meta.iteration : '')
+ ' &middot; ' + shots.length + ' shots &middot; '
+ Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0")
+ ' &middot; ' + cast.length + ' characters</p>'
+ timingHtml();
var probs = draft.problems
? '<div class="probs"><b>The checker has objections</b><ul>'
+ draft.problems.split("\n").filter(Boolean).map(function (x) {
return '<li>' + esc(x) + '</li>'; }).join("") + '</ul></div>'
: "";
var castHtml = '<div class="cast">' + cast.map(function (c) {
var sheets = (c.head_url || c.front_url || c.side_url)
? '<div class="sheets">' + [c.head_url, c.front_url, c.side_url].map(function (u) {
return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("") + '</div>' : '';
return '<div class="ch">' + sheets + '<b>' + esc(c.name || c.id) + '</b>'
+ '<span class="vo">' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent")
+ '</span><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
}).join("") + '</div>';
var body = insertHtml("start");
shots.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); });
return head + probs + '<div class="bar">' + barHtml(meta) + '</div>'
+ '<div id="reelhost">' + reelHtml() + '</div>'
+ spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + castHtml + body;
}
function insertHtml(after) {
if (EDITING === "new:" + after) {
return '<div class="ins" style="text-align:left">'
+ '<textarea class="rough" id="newrough" rows="4" placeholder="'
+ 'What happens in this scene, who is in it and what they say. A sentence or two is enough '
+ '— the writer turns it into a proper shot."></textarea>'
+ '<div class="tools"><button type="button" class="btn primary" data-addgo="'
+ esc(after) + '">Write this scene</button>'
+ '<button type="button" class="btn" data-cancel="1">Cancel</button></div></div>';
}
return '<div class="ins"><button type="button" class="btn" data-add="' + esc(after)
+ '">+ Add a scene ' + (after === "start" ? "at the start" : "after " + esc(after)) + '</button></div>';
}
// The faults themselves, with the prompt change that would prevent each -
// which is the half that makes the check actionable rather than a verdict.
function faultsHtml(s) {
var vc = s.vision_check;
if (!vc || !(vc.faults || []).length) return "";
return '<div class="eyes">'
+ (vc.summary ? '<div class="sum">' + esc(vc.summary) + '</div>' : '')
+ vc.faults.map(function (f) {
return '<div class="f ' + (f.severity === "serious" ? "serious" : "")
+ '"><b>' + esc(f.kind || "") + '</b>'
+ (f.at != null ? ' at ' + Number(f.at).toFixed(1) + 's' : '')
+ ' &middot; ' + esc(f.what || "")
+ (f.prompt_fix ? '<span class="fix">fix: ' + esc(f.prompt_fix)
+ '</span>' : '') + '</div>';
}).join("") + '</div>';
}
function shotHtml(s, voice) {
if (s.writing) {
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
+ '<h3>' + esc(s.title || "New scene") + '</h3>'
+ '<p class="busy">The writer is turning your note into a shot…</p>'
+ '<p class="cont">' + esc(String(s.rough || "").slice(0, 300)) + '</p></div></div>';
}
var lines = (s.audio || []).map(function (a) {
var who = voice[a.character_id] || {};
return '<div class="line">'
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '">&#9654;</button>'
+ '<div class="say"><span class="who">' + esc(who.name || a.character_id) + '</span> '
+ (a.lip_sync ? '<span class="lip">on camera</span>' : '<span class="vobadge">voice over</span>')
+ '<br>' + esc(a.text || "")
+ (a.delivery ? '<br><span class="dir">' + esc(a.delivery) + '</span>' : '')
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
}).join("");
var media = "";
if (s.clip_url) {
media = '<div class="frames"><figure><video src="' + esc(s.clip_url) + '" controls '
+ 'preload="metadata"></video><figcaption>the filmed shot'
+ (s.clip_seconds ? ' &middot; ' + Number(s.clip_seconds).toFixed(1) + 's' : '')
+ '</figcaption></figure>'
+ (s.opening_url ? '<figure><img src="' + esc(s.opening_url) + '" alt=""><figcaption>filmed from this frame</figcaption></figure>' : '')
+ '</div>';
} else if (s.opening_url || s.closing_url) {
media = '<div class="frames">'
+ (s.opening_url ? '<figure><img src="' + esc(s.opening_url) + '" alt=""><figcaption>opens on</figcaption></figure>' : '')
+ (s.closing_url ? '<figure><img src="' + esc(s.closing_url) + '" alt=""><figcaption>ends on</figcaption></figure>' : '')
+ '</div>';
}
var chk = s.audio_check || {};
var chkHtml = "";
if (chk.verdict && chk.verdict !== "no dialogue") {
var good = chk.verdict === "ok" || chk.verdict === "drifted";
chkHtml = '<span class="chk ' + (good ? "ok" : "bad") + '" title="'
+ esc("heard: " + String(chk.heard || "")) + '">audio ' + esc(chk.verdict)
+ (chk.coverage != null ? ' ' + Math.round(chk.coverage * 100) + '%' : '')
+ (chk.missing ? ' &mdash; lost “' + esc(String(chk.missing).slice(0, 40)) + '”' : '')
+ '</span>';
}
// What the eyes found, if they have looked. Kinds: render (the picture is
// broken), continuity (the frames disagree), script (wrong shot, well made).
var vc = s.vision_check || null;
var eyeHtml = "";
if (vc && vc.verdict) {
var bad = vc.verdict === "broken";
var faults = vc.faults || [];
eyeHtml = '<span class="chk ' + (bad ? "bad" : (faults.length ? "" : "ok"))
+ '" title="' + esc(vc.summary || "") + '">picture ' + esc(vc.verdict)
+ (faults.length ? ' &middot; ' + faults.length + ' fault'
+ (faults.length === 1 ? '' : 's') : '')
+ (vc.people_on_screen != null ? ' &middot; ' + vc.people_on_screen
+ ' on screen' : '') + '</span>';
}
var tools;
if (s.drawing) tools = '<span class="busy">drawing…</span>';
else if (s.filming) tools = '<span class="busy">filming…</span>';
else if (EDITING === s.id) {
tools = '<div style="width:100%"><textarea class="rough" id="rough" rows="3" placeholder="'
+ 'What should change in this shot?">' + esc(s.rough || "") + '</textarea>'
+ '<div class="tools"><button type="button" class="btn primary" data-rewrite="' + esc(s.id)
+ '">Rewrite this shot</button>'
+ '<button type="button" class="btn" data-cancel="1">Cancel</button></div></div>';
} else {
tools = '<button type="button" class="btn" data-edit="' + esc(s.id) + '">Change this shot</button>'
+ '<button type="button" class="btn" data-drawone="' + esc(s.id) + '">'
+ (s.opening_url ? 'Redraw' : 'Draw') + ' &middot; $0.14</button>'
+ (s.opening_url ? '<button type="button" class="btn" data-filmone="' + esc(s.id) + '">'
+ (s.clip_url ? 'Film it again' : 'Film this shot') + ' &middot; $0.75</button>' : '')
+ chkHtml + eyeHtml
+ '<button type="button" class="btn" data-delete="' + esc(s.id) + '">Remove</button>';
}
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
+ '<h3>' + esc(s.title || "") + '</h3>'
+ '<div class="where">' + esc(s.setting || "") + ' &middot; '
+ Number(s.edit_seconds || 0).toFixed(1) + 's'
+ (s.scene ? ' &middot; <span class="scene">' + esc(s.scene) + '</span>' : '')
+ (s.day ? ' &middot; <span class="scene">' + esc(s.day) + '</span>' : '')
+ '</div>'
+ media
+ '<p class="act">' + esc(s.action || "") + '</p>'
+ (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '')
+ (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '')
+ lines
+ faultsHtml(s)
+ '<div class="tools">' + tools + '</div>'
+ '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" '
+ 'placeholder="Your note on this shot, for the next full revision.">'
+ esc(NOTES[s.id] || "") + '</textarea>'
+ '</div></div>';
}
async function act(name, payload) {
var r = await api(name, payload);
if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; }
hideMsg("msg");
return r;
}
document.getElementById("pane").addEventListener("click", async function (e) {
var hit = function (attr) { return e.target.closest("[" + attr + "]"); };
var el, r;
if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; }
if ((el = hit("data-add"))) { EDITING = "new:" + el.getAttribute("data-add"); openTab(CUR); return; }
if ((el = hit("data-edit"))) { EDITING = el.getAttribute("data-edit"); openTab(CUR); return; }
if ((el = hit("data-addgo"))) {
var rough = document.getElementById("newrough").value.trim();
if (rough.length < 20) { showMsg("msg", "Say what happens in the scene."); return; }
el.disabled = true;
r = await act("add_shot", { draft_id: CUR, after: el.getAttribute("data-addgo"), rough: rough });
EDITING = null; if (r) await refresh(); return;
}
if ((el = hit("data-rewrite"))) {
var rw = document.getElementById("rough").value.trim();
if (rw.length < 20) { showMsg("msg", "Say what should change."); return; }
el.disabled = true;
r = await act("write_shot", { draft_id: CUR, shot_id: el.getAttribute("data-rewrite"), rough: rw });
EDITING = null; if (r) await refresh(); return;
}
if ((el = hit("data-drawone"))) {
el.disabled = true;
r = await act("draw_one_shot", { draft_id: CUR, shot_id: el.getAttribute("data-drawone") });
if (r) await refresh(); return;
}
if ((el = hit("data-filmone"))) {
el.disabled = true;
r = await act("film_one_shot_now", { draft_id: CUR, shot_id: el.getAttribute("data-filmone") });
if (r) await refresh(); return;
}
if ((el = hit("data-delete"))) {
if (!confirm("Remove shot " + el.getAttribute("data-delete") + " from this trailer?")) return;
el.disabled = true;
r = await act("delete_shot", { draft_id: CUR, shot_id: el.getAttribute("data-delete") });
if (r) await refresh(); return;
}
if ((el = hit("data-assemble"))) {
el.disabled = true;
r = await act("assemble_trailer", { draft_id: CUR });
if (r) { showMsg("msg", "Joining " + r.shots + " shots. It appears in My videos.", "ok");
setTimeout(function () { location.href = "/videos.html"; }, 2200); }
return;
}
if ((el = hit("data-lead"))) {
el.disabled = true;
r = await act("set_lead", { run_id: runId, draft_id: el.getAttribute("data-lead") });
if (r) await refresh(); return;
}
if ((el = hit("data-draw"))) {
el.disabled = true;
r = await act("draw_storyboard", { draft_id: el.getAttribute("data-draw") });
if (r) await refresh(); return;
}
if ((el = hit("data-reelgo"))) {
el.disabled = true;
var rl = await api("reel", { draft_id: CUR });
if (!rl.ok) { showMsg("msg", rl.error || "Could not load the storyboard."); return; }
if (String(PKG.reel_state || "") !== "ready") {
el.textContent = "Speaking every shot…";
REELWANT = true;
r = await act("prepare_reel", { draft_id: CUR });
if (r) showMsg("msg", "Speaking " + r.shots + " shots. This is the same "
+ "audio the film will use, so it is only paid for once. It starts "
+ "playing on its own when it is ready.", "ok");
else REELWANT = false;
await refresh();
return;
}
hideMsg("msg");
REEL = rl.shots || []; RAT = -1;
reelShow(0);
return;
}
if ((el = hit("data-reeltoggle"))) {
if (reelAudio.paused) { reelAudio.play().catch(function(){}); reelShow(RAT); }
else { reelAudio.pause(); clearInterval(RTIMER); RTIMER = null; reelPaint(); }
return;
}
if ((el = hit("data-reelstep"))) {
var d = Number(el.getAttribute("data-reelstep")) || 1;
reelShow(Math.max(0, Math.min((REEL || []).length - 1, RAT + d)));
return;
}
if ((el = hit("data-reelstop"))) { reelStop(); return; }
if ((el = hit("data-reeljump"))) {
var jid = el.getAttribute("data-reeljump");
clearInterval(RTIMER); RTIMER = null; reelAudio.pause();
var target = [...document.querySelectorAll(".shot")].find(function (x) {
var nu = x.querySelector(".num");
return nu && nu.textContent.trim() === jid; });
if (target) {
document.querySelectorAll(".shot.lit").forEach(function (x) {
x.classList.remove("lit"); });
target.classList.add("lit");
target.scrollIntoView({ behavior: "smooth", block: "center" });
var ta = target.querySelector("textarea.note");
if (ta) setTimeout(function () { ta.focus(); }, 500);
}
return;
}
if ((el = hit("data-cuts"))) {
el.disabled = true; el.textContent = "Working out what to drop…";
r = await act("propose_cuts", { draft_id: el.getAttribute("data-cuts") });
if (r) { showMsg("msg", "Asking for " + r.count + " different cuts.", "ok");
setTimeout(refresh, 25000); }
return;
}
if ((el = hit("data-play"))) {
var cid = el.getAttribute("data-play");
el.disabled = true;
var cc = await api("cut_clips", { draft_id: CUR, cut_id: cid });
el.disabled = false;
if (!cc.ok) { showMsg("msg", cc.error || "That would not play."); return; }
QUEUE = (cc.shots || []).filter(function (s) { return s.clip_url; });
if (!QUEUE.length) {
showMsg("msg", "None of the shots in this cut are filmed yet, so there "
+ "is nothing to play. Film them first.");
return;
}
if (cc.filmed < cc.of)
showMsg("msg", "Playing the " + cc.filmed + " of " + cc.of
+ " shots that are filmed. The rest are skipped.");
PLAYING = cid; QAT = 0;
await openTab(CUR); // the video element does not exist until this lands
playCut();
return;
}
if ((el = hit("data-stop"))) { PLAYING = null; QUEUE = []; openTab(CUR); return; }
if ((el = hit("data-makecut"))) {
el.disabled = true;
r = await act("make_cut", { draft_id: CUR, cut_id: el.getAttribute("data-makecut") });
if (r) { showMsg("msg", "Joining " + r.shots + " shots into this version. "
+ "It appears in My videos with its own share page.", "ok");
await refresh(); }
return;
}
if ((el = hit("data-measure"))) {
el.disabled = true; el.textContent = "Speaking every line…";
r = await act("measure_script", { draft_id: el.getAttribute("data-measure") });
if (r) {
showMsg("msg", "Speaking every line in the cast voices. A minute or two.", "ok");
setTimeout(refresh, 20000);
}
return;
}
if ((el = hit("data-review"))) {
el.disabled = true;
r = await act("start_review", { run_id: runId }); if (r) await refresh(); return;
}
if ((el = hit("data-revise"))) {
el.disabled = true;
r = await act("start_revision", { run_id: runId }); if (r) await refresh(); return;
}
if ((el = hit("data-film"))) {
el.disabled = true;
r = await act("film_draft", { draft_id: el.getAttribute("data-film") });
if (r) { showMsg("msg", "Filming " + r.shots + " shots, about $" + r.usd
+ ". It appears in My videos as it goes.", "ok");
setTimeout(function () { location.href = "/videos.html"; }, 2500); }
return;
}
if ((el = hit("data-retry"))) {
el.disabled = true;
r = await act("retry_writer", { draft_id: el.getAttribute("data-retry") });
if (r) await refresh(); return;
}
var btn = e.target.closest(".play");
if (!btn) return;
var sid = btn.getAttribute("data-shot");
btn.disabled = true; btn.innerHTML = "&hellip;";
var a = await api("shot_audio", { draft_id: CUR, shot_id: sid });
btn.disabled = false; btn.innerHTML = "&#9654;";
if (!a.ok) { showMsg("msg", a.error || "That would not play."); return; }
hideMsg("msg");
if (!a.url) return;
document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (x) {
x.textContent = a.seconds.toFixed(1) + "s / " + Number(a.planned).toFixed(1) + "s";
x.className = "dur" + (a.seconds > Number(a.planned) ? " over" : "");
});
audio.pause(); audio.src = a.url; audio.play();
});
// Play the cut's clips one after another in a single element. This is the
// whole preview: no joining, no encoding, no upload, nothing to pay for.
function playCut() {
var v = document.getElementById("cutv");
if (!v || !QUEUE.length) return;
var at = document.getElementById("cutat");
function step() {
if (QAT >= QUEUE.length) {
if (at) at.textContent = "end of this cut";
return;
}
var s = QUEUE[QAT];
if (at) at.textContent = "shot " + s.id + " · " + (QAT + 1)
+ " of " + QUEUE.length + (s.title ? " · " + s.title : "");
v.src = s.clip_url;
v.play().catch(function () {});
}
v.onended = function () { QAT += 1; step(); };
v.onerror = function () { QAT += 1; step(); };
step();
}
var noteTimers = {};
document.getElementById("pane").addEventListener("input", function (e) {
var ta = e.target;
var sid = ta.getAttribute && ta.getAttribute("data-note");
if (!sid) return;
ta.classList.remove("saved");
clearTimeout(noteTimers[sid]);
noteTimers[sid] = setTimeout(async function () {
var r = await api("add_shot_note", { draft_id: CUR, shot_id: sid, note: ta.value });
if (r.ok) { NOTES[sid] = ta.value; ta.classList.add("saved"); }
}, 700);
});
load();
})();
</script>
</body>
</html>