From ac4ece69a5c447a3806623e13f2690f82e10fcfa Mon Sep 17 00:00:00 2001 From: gator Date: Thu, 17 Sep 2026 20:57:20 +0000 Subject: [PATCH] Add the button that runs the picture check Separate from filming on purpose: a re-film decision wants the audio and picture verdicts together, and an existing film can be checked without paying to shoot it again. --- trailer.html | 730 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 729 insertions(+), 1 deletion(-) diff --git a/trailer.html b/trailer.html index 69d19e0..16377a2 100644 --- a/trailer.html +++ b/trailer.html @@ -211,7 +211,11 @@ } function tick() { - var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots() || busyReel(); + var es = String(PKG.eyes_state || ""); + var busyEyes = es && es !== "ready" && !/^failed/.test(es) + && (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000; + var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots() || busyReel() + || busyEyes; if (busy && !timer) timer = setInterval(refresh, 5000); if (!busy && timer) { clearInterval(timer); timer = null; } } @@ -407,6 +411,730 @@ : 'Revise the whole script') + ''); } + if (filmed) { + var es = String(PKG.eyes_state || ""); + var looking = es && es !== "ready" && !/^failed/.test(es) + && (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000; + if (looking) + out.push(''); + else + out.push(''); + if (filmed) + out.push(''); + } + 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 " + mmss(t.measured_seconds) + + ", against a " + mmss(t.target_seconds) + " target"; + if (over > 5) line += " — " + Math.round(over) + " seconds over." + + " 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 "
  • " + Number(l.seconds || 0).toFixed(1) + "s · shot " + + esc(String(l.shot)) + " · “" + + esc(String(l.text || "").slice(0, 80)) + "”
  • "; }).join(""); + return '
    ' + line + "
    " + + (worst && over > 5 ? "" : "") + "
    "; + } + + // 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 = '

    Watch the storyboard

    ' + + '

    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.

    '; + + if (running) + out += '

    Speaking every shot… ' + esc(st) + '

    '; + else if (st === "ready") { + var spoken = shots.filter(function (s) { return s.reel_url; }).length; + var silent = shots.length - spoken; + out += '

    ' + spoken + ' shots have dialogue and are spoken' + + (silent ? '; ' + silent + ' are silent and hold on the frame' : '') + + '.

    '; + } + else if (/^failed/.test(st)) + out += '

    ' + esc(st) + '

    '; + else if (st) + out += '

    The last attempt stopped before it finished — ' + + 'the server restarted under it. Ask again.

    '; + + if (!REEL) { + out += '
    ' + + '
    '; + return out; + } + + var s = REEL[RAT] || REEL[0] || {}; + var lines = (s.lines || []).map(function (l) { + return '
    ' + esc(l.who || "") + + (l.lip_sync ? "" : " (v.o.)") + '' + esc(l.text || "") + '
    '; + }).join("") || '
    no dialogue in this shot
    '; + + out += '
    ' + + '
    opens on
    ' + + '
    ends on
    ' + + '
    ' + esc(s.id) + ' · ' + + esc(s.title || "") + '
    ' + + '
    ' + esc(s.setting || "") + + (s.scene ? ' · ' + esc(s.scene) : "") + + (s.day ? ' · ' + esc(s.day) : "") + '
    ' + + '
    ' + lines + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + (RAT + 1) + ' of ' + REEL.length + '' + + '
    '; + 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 = '

    Shorter cuts of this same shoot

    ' + + '

    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”.

    '; + + if (busyCuts()) out += '

    Working out what to drop…

    '; + else if (st === "thinking") out += '

    The last attempt stopped ' + + 'before it finished — the server restarted under it. Ask again.

    '; + else if (/^failed/.test(st)) out += '

    ' + esc(st) + '

    '; + + out += cuts.map(function (c) { + var stat = STATS[c.id]; + var play = (PLAYING === c.id) + ? '
    ' + + '
    loading…' + + '
    ' + : ''; + var tools = ''; + if (c.state === "made" && c.video_url) + tools += 'Published · in My videos'; + else if (c.state === "making") + tools += 'joining…'; + else if (filmed) + tools += ''; + + return '
    ' + esc(c.name || "") + '' + + '' + mmss(c.seconds || 0) + ' · ' + + (c.shot_ids || []).length + ' shots' + + (c.strategy ? '

    ' + esc(c.strategy) + '

    ' : '') + + (c.why ? '

    ' + esc(c.why) + '

    ' : '') + + (c.sacrifices ? '

    gives up: ' + esc(c.sacrifices) + '

    ' : '') + + (stat ? '

    ' + stat.views + ' viewer' + + (stat.views === 1 ? '' : 's') + ' · median ' + stat.median_percent + + '% watched · ' + stat.completion_rate + '% reached the end

    ' : '') + + '

    ' + esc((c.shot_ids || []).join(" ")) + + (reordered(c.shot_ids) ? ' · reordered' : '') + '

    ' + + '
    ' + tools + '
    ' + play + '
    '; + }).join(""); + + if (!cuts.length && st !== "thinking") + out += '

    No cuts proposed yet.

    '; + return out + '
    '; + } + + 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 += '

    What this trailer does not tell a viewer

    ' + + (v ? '

    ' + esc(v) + '

    ' : '') + + (list.length ? '
      ' + list.map(function (g) { + return '
    1. ' + esc(g.what || "") + + 'shot ' + esc(g.shot || "?") + ' → ' + + esc(g.fix || "") + '
    2. '; }).join("") + '
    ' : '') + + '
    '; + } + + 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) ? "" + : ''; + } + return '

    ' + esc(r.label) + ' watched it without reading the book' + + (r.score ? '' + + r.score + '/100' : '') + '

    ' + + (s.summary ? '
    ' + esc(s.summary) + '
    ' : '') + + '
    main character: ' + esc(s.who || "not clear to them") + '
    ' + + '
    what they want: ' + esc(s.wants || "not clear to them") + '
    ' + + '
    what is in the way: ' + esc(s.obstacle || "not clear to them") + '
    ' + + (s.would_watch ? '
    would they watch the film: ' + + esc(s.would_watch) + '
    ' : '') + + (s.unanswered && s.unanswered.length + ? '
    still asking:
    ' + ul(s.unanswered, "") : '') + + (e.missed && e.missed.length ? '
    they missed:
    ' + ul(e.missed, "") : '') + + (e.got_wrong && e.got_wrong.length + ? '
    the trailer misled them:
    ' + ul(e.got_wrong, "wrong") : '') + + '
    '; + }).join(""); + + return '
    ' + out + '
    '; + } + + 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 = '

    ' + esc(p.title || meta.label) + '

    ' + + (p.tagline ? '' + esc(p.tagline) + '' : '') + '
    ' + + (p.logline ? '

    ' + esc(p.logline) + '

    ' : '') + + '

    ' + esc(meta.label) + + (Number(meta.iteration) > 1 ? ' · revision ' + meta.iteration : '') + + ' · ' + shots.length + ' shots · ' + + Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0") + + ' · ' + cast.length + ' characters

    ' + + timingHtml(); + + var probs = draft.problems + ? '
    The checker has objections
    ' + : ""; + + var castHtml = '
    ' + cast.map(function (c) { + var sheets = (c.head_url || c.front_url || c.side_url) + ? '
    ' + [c.head_url, c.front_url, c.side_url].map(function (u) { + return u ? '' : ''; }).join("") + '
    ' : ''; + return '
    ' + sheets + '' + esc(c.name || c.id) + '' + + '' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent") + + '
    ' + esc((c.appearance || "").slice(0, 150)) + '
    '; + }).join("") + '
    '; + + var body = insertHtml("start"); + shots.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); }); + + return head + probs + '
    ' + barHtml(meta) + '
    ' + + '
    ' + reelHtml() + '
    ' + + spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + castHtml + body; + } + + function insertHtml(after) { + if (EDITING === "new:" + after) { + return '
    ' + + '' + + '
    ' + + '
    '; + } + return '
    '; + } + + // 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 '
    ' + + (vc.summary ? '
    ' + esc(vc.summary) + '
    ' : '') + + vc.faults.map(function (f) { + return '
    ' + esc(f.kind || "") + '' + + (f.at != null ? ' at ' + Number(f.at).toFixed(1) + 's' : '') + + ' · ' + esc(f.what || "") + + (f.prompt_fix ? 'fix: ' + esc(f.prompt_fix) + + '' : '') + '
    '; + }).join("") + '
    '; + } + + function shotHtml(s, voice) { + if (s.writing) { + return '
    ' + esc(s.id) + '
    ' + + '

    ' + esc(s.title || "New scene") + '

    ' + + '

    The writer is turning your note into a shot…

    ' + + '

    ' + esc(String(s.rough || "").slice(0, 300)) + '

    '; + } + + var lines = (s.audio || []).map(function (a) { + var who = voice[a.character_id] || {}; + return '
    ' + + '' + + '
    ' + esc(who.name || a.character_id) + ' ' + + (a.lip_sync ? 'on camera' : 'voice over') + + '
    ' + esc(a.text || "") + + (a.delivery ? '
    ' + esc(a.delivery) + '' : '') + + '
    '; + }).join(""); + + var media = ""; + if (s.clip_url) { + media = '
    the filmed shot' + + (s.clip_seconds ? ' · ' + Number(s.clip_seconds).toFixed(1) + 's' : '') + + '
    ' + + (s.opening_url ? '
    filmed from this frame
    ' : '') + + '
    '; + } else if (s.opening_url || s.closing_url) { + media = '
    ' + + (s.opening_url ? '
    opens on
    ' : '') + + (s.closing_url ? '
    ends on
    ' : '') + + '
    '; + } + + var chk = s.audio_check || {}; + var chkHtml = ""; + if (chk.verdict && chk.verdict !== "no dialogue") { + var good = chk.verdict === "ok" || chk.verdict === "drifted"; + chkHtml = 'audio ' + esc(chk.verdict) + + (chk.coverage != null ? ' ' + Math.round(chk.coverage * 100) + '%' : '') + + (chk.missing ? ' — lost “' + esc(String(chk.missing).slice(0, 40)) + '”' : '') + + ''; + } + + // 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 = 'picture ' + esc(vc.verdict) + + (faults.length ? ' · ' + faults.length + ' fault' + + (faults.length === 1 ? '' : 's') : '') + + (vc.people_on_screen != null ? ' · ' + vc.people_on_screen + + ' on screen' : '') + ''; + } + + var tools; + if (s.drawing) tools = 'drawing…'; + else if (s.filming) tools = 'filming…'; + else if (EDITING === s.id) { + tools = '
    ' + + '
    ' + + '
    '; + } else { + tools = '' + + '' + + (s.opening_url ? '' : '') + + chkHtml + eyeHtml + + ''; + } + + return '
    ' + esc(s.id) + '
    ' + + '

    ' + esc(s.title || "") + '

    ' + + '
    ' + esc(s.setting || "") + ' · ' + + Number(s.edit_seconds || 0).toFixed(1) + 's' + + (s.scene ? ' · ' + esc(s.scene) + '' : '') + + (s.day ? ' · ' + esc(s.day) + '' : '') + + '
    ' + + media + + '

    ' + esc(s.action || "") + '

    ' + + (s.camera ? '

    camera: ' + esc(s.camera) + '

    ' : '') + + (s.continuity ? '

    continuity: ' + esc(s.continuity) + '

    ' : '') + + lines + + faultsHtml(s) + + '
    ' + tools + '
    ' + + '' + + '
    '; + } + + 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-eyes"))) { + el.disabled = true; el.textContent = "Looking at the footage…"; + r = await act("check_clips", { draft_id: el.getAttribute("data-eyes") }); + if (r) showMsg("msg", "Sampling frames from " + r.shots + " clips and " + + "checking them against the script.", "ok"); + await refresh(); + 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 = "…"; + var a = await api("shot_audio", { draft_id: CUR, shot_id: sid }); + btn.disabled = false; btn.innerHTML = "▶"; + 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(); +})(); + + + + + (filmed * 0.02).toFixed(2) + ''); + } + if (unfilmed && !undrawn) out.push('