paytolaugh/trailer.html
gator d21360ea2b trailer.html: edit, draw, film and reassemble one shot at a time
The trailer was all-or-nothing. If one shot was wrong - a line cut short, a
character duplicated, a scene missing from the front - the only lever was to
film all twenty-four again.

Every shot now carries its own controls: describe a change and have the writer
turn it into a proper shot, draw its two frames, film just that one, watch the
clip that came back, and read the machine check that compared what it says with
what the script says. "Add a scene here" sits between shots, because the note
that prompted this was that the trailer needed an opening the writer never
thought to include.

Assemble is separate and cheap - it joins whatever has been filmed - so
re-cutting after changing one shot costs nothing but the one shot.

Prices are on the buttons because they differ by two orders of magnitude:
rewriting a shot is cents, drawing it is fourteen, filming it is about a
dollar.
2026-09-17 06:31:29 +00:00

543 lines
25 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trailers — PayToLaugh</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="/assets/app.css">
<style>
.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:0 0 20px}
.tab{border:1px solid var(--line);border-bottom:0;background:none;color:var(--muted);
padding:10px 16px;border-radius:10px 10px 0 0;cursor:pointer;font:inherit;font-size:14px}
.tab.on{color:var(--ink);border-color:var(--line);background:var(--panel)}
.tab.lead{color:var(--accent)}
.tab .sc{color:var(--accent2);margin-left:7px;font-weight:600}
.tab .st{color:var(--muted);margin-left:7px;font-size:12px}
.head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px}
.head h2{margin:0;font-size:22px}
.tag{color:var(--muted);font-style:italic}
.facts{color:var(--muted);font-size:13px;margin:2px 0 18px}
.cast{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 22px}
.ch{border:1px solid var(--line);border-radius:10px;padding:10px;font-size:13px;width:250px}
.ch b{display:block}
.ch .vo{color:var(--muted);font-size:12px}
.ch .ap{color:var(--muted);font-size:12px;margin-top:4px}
.sheets{display:flex;gap:5px;margin-bottom:8px}
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
aspect-ratio:16/9;object-fit:cover}
.shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px}
.shot .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
.shot .body{flex:1;min-width:0}
.shot h3{margin:0 0 3px;font-size:15px}
.shot .where{color:var(--muted);font-size:12px;margin-bottom:8px}
.shot .act{font-size:14px;margin:0 0 8px}
.shot .cam,.shot .cont{color:var(--muted);font-size:12.5px;margin:0 0 5px}
.frames{display:flex;gap:10px;margin:10px 0}
.frames figure{margin:0;flex:1;min-width:0}
.frames img,.frames video{width:100%;border-radius:8px;border:1px solid var(--line);display:block}
.frames figcaption{color:var(--muted);font-size:11px;margin-top:3px}
.line{display:flex;gap:9px;align-items:flex-start;margin:7px 0}
.line .say{flex:1;min-width:0;font-size:14px}
.line .who{color:var(--accent);font-size:12px}
.line .dir{color:var(--muted);font-size:12px}
.play{border:1px solid var(--line);background:none;color:var(--ink);border-radius:999px;
width:30px;height:30px;flex:none;cursor:pointer;font:inherit;line-height:1}
.play:hover{border-color:var(--accent)}
.dur{color:var(--muted);font-size:12px;align-self:center}
.dur.over{color:#e0685f}
.note{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
background:transparent;color:var(--ink);border:1px dashed var(--line);
border-radius:8px;padding:8px 10px;resize:vertical}
.note:focus{border-style:solid;border-color:var(--accent);outline:none}
.note.saved{border-color:var(--accent2)}
.probs{border:1px solid #e0685f;border-radius:10px;padding:12px 14px;margin:0 0 18px;font-size:13px}
.probs b{color:#e0685f}
.probs ul{margin:6px 0 0;padding-left:18px}
.bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:0 0 20px}
.soon{color:var(--muted);font-size:12px}
.lip{color:var(--accent2);font-size:11px;border:1px solid var(--accent2);
border-radius:999px;padding:0 6px}
.vobadge{color:var(--muted);font-size:11px;border:1px solid var(--line);
border-radius:999px;padding:0 6px}
.revs{margin:0 0 22px}
.rev{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
.rev h4{margin:0 0 6px;font-size:14px}
.rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
.rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px}
.rev .er{color:#e0685f}
.rev .kp{color:var(--accent2)}
.rev .sh{color:var(--muted);font-variant-numeric:tabular-nums}
.tools{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:10px}
.tools button{font-size:12px;padding:5px 10px}
.chk{font-size:12px;border-radius:999px;padding:1px 8px;border:1px solid var(--line);color:var(--muted)}
.chk.ok{color:var(--accent2);border-color:var(--accent2)}
.chk.bad{color:#e0685f;border-color:#e0685f}
.ins{border-top:1px dashed var(--line);text-align:center;padding:6px 0}
.ins button{font-size:12px;padding:4px 12px;color:var(--muted)}
.rough{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
background:transparent;color:var(--ink);border:1px solid var(--accent);
border-radius:8px;padding:9px 11px;resize:vertical}
.busy{color:var(--accent);font-size:12px}
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1 id="title">Trailers</h1>
<p id="sub"></p>
</header>
<div class="msg" id="msg"></div>
<section><div class="card">
<div class="tabs" id="tabs"></div>
<div id="pane"><p class="empty">Loading&hellip;</p></div>
</div></section>
</div>
<script src="/assets/app.js"></script>
<script>
(function () {
if (!requireSession()) return;
navBar("trailer");
var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = [];
var timer = null, EDITING = null;
var runId = new URLSearchParams(location.search).get("run") || "";
var audio = new Audio();
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
function busyDraft(d) {
var s = String(d.draw_state || "");
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
}
function busyRun() {
return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || ""));
}
// A shot is busy while it is being written, drawn or filmed. Any of those
// means the package on screen is about to change.
function busyShots() {
return (PKG.shots || []).some(function (s) {
return s.writing || s.drawing || s.filming; });
}
async function load() {
var r = await api("run_status", { run_id: runId });
if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; }
RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("title").textContent = RUN.book_title || "Trailers";
document.getElementById("sub").textContent = sub();
if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id;
paintTabs();
var rv = await api("list_reviews", { run_id: runId });
REVIEWS = (rv && rv.reviews) || [];
await openTab(CUR);
tick();
}
function tick() {
var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots();
if (busy && !timer) timer = setInterval(refresh, 5000);
if (!busy && timer) { clearInterval(timer); timer = null; }
}
// A refresh keeps the tab you are on and the box you are typing in.
async function refresh() {
var r = await api("run_status", { run_id: runId });
if (!r.ok) return;
RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("sub").textContent = sub();
paintTabs();
await openTab(CUR);
tick();
}
function leadDraftId() {
var best = null;
DRAFTS.forEach(function (d) {
if (d.writer === RUN.lead_writer
&& (!best || Number(d.iteration) > Number(best.iteration))) best = d;
});
return best && best.id;
}
function sub() {
var bits = [];
if (RUN.stage) bits.push(RUN.stage);
bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s"));
var spent = DRAFTS.reduce(function (a, d) {
return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0);
(PKG.shots || []).forEach(function (s) { spent += Number(s.film_usd || 0); });
if (spent) bits.push("$" + spent.toFixed(2) + " so far");
if (RUN.created_at) bits.push(when(RUN.created_at));
return bits.join(" · ");
}
function paintTabs() {
document.getElementById("tabs").innerHTML = DRAFTS.map(function (d) {
var s = String(d.draw_state || "");
var extra = d.state === "writing" ? '<span class="st">writing…</span>'
: d.state === "failed" ? '<span class="st">failed</span>'
: (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>'
: d.score ? '<span class="sc">' + d.score + '</span>'
: '';
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
+ (d.writer === RUN.lead_writer ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
+ esc(d.label || d.writer)
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
+ (d.writer === RUN.lead_writer ? ' ★' : '') + extra + '</button>';
}).join("");
}
document.getElementById("tabs").addEventListener("click", function (e) {
var id = e.target.closest ? e.target.closest(".tab") : null;
if (!id) return;
CUR = id.getAttribute("data-tab"); EDITING = null;
paintTabs(); openTab(CUR);
});
async function openTab(id) {
var meta = DRAFTS.filter(function (d) { return d.id === id; })[0];
if (!meta) return;
var pane = document.getElementById("pane");
if (meta.state === "failed") {
pane.innerHTML = '<p class="empty"><b>' + esc(meta.label) + ' did not finish.</b><br>'
+ esc(meta.note || "No reason was recorded.") + '</p>'
+ '<div class="bar"><button type="button" class="btn" data-retry="'
+ esc(meta.id) + '">Try this writer again</button></div>';
return;
}
if (meta.state === "writing") {
pane.innerHTML = '<p class="empty">Working. Reading the whole book takes a few minutes.</p>';
return;
}
var r = await api("get_draft", { draft_id: id });
if (!r.ok) { pane.innerHTML = '<p class="empty">' + esc(r.error || "") + '</p>'; return; }
PKG = r.draft.package || {};
NOTES = r.draft.notes || {};
pane.innerHTML = paneHtml(meta, r.draft);
}
function barHtml(meta) {
var shots = (PKG.shots || []), cast = (PKG.characters || []);
var undrawn = shots.filter(function (s) { return !s.opening_url; }).length;
var unfilmed = shots.filter(function (s) { return !s.clip_url; }).length;
var filmed = shots.length - unfilmed;
var imgs = cast.length * 3 + shots.length * 2;
var s = String(meta.draw_state || "");
var drawing = s && !/^(drawn|failed)/.test(s);
var isLead = meta.writer === RUN.lead_writer;
var out = [];
if (!isLead) out.push('<button type="button" class="btn primary" data-lead="'
+ esc(meta.id) + '">Make this the head writer</button>');
else out.push('<span class="soon">★ head writer</span>');
if (drawing) out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
else if (undrawn) out.push('<button type="button" class="btn" data-draw="' + esc(meta.id)
+ '">Draw ' + (undrawn === shots.length ? 'this storyboard' : undrawn + ' undrawn shots')
+ ' &middot; $' + (imgs * 0.068).toFixed(2) + '</button>');
else out.push('<span class="soon">storyboard drawn'
+ (meta.draw_usd ? ' &middot; $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
if (isLead) {
var others = DRAFTS.filter(function (d) {
return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length;
if (RUN.stage === "reviewing")
out.push('<button type="button" class="btn" disabled>The others are reading it…</button>');
else if (others)
out.push('<button type="button" class="btn" data-review="1">'
+ (REVIEWS.length ? "Review it again" : "Have the others review this") + '</button>');
if (RUN.stage === "revising")
out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
else if (REVIEWS.length || Object.keys(NOTES).length)
out.push('<button type="button" class="btn" data-revise="1">Revise the whole script</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("");
}
function reviewsHtml(meta) {
var mine = REVIEWS.filter(function (r) {
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
if (!mine.length) return "";
return '<div class="revs">' + mine.map(function (r) {
function list(items, cls, fmt) {
return (!items || !items.length) ? ""
: '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>';
}
return '<div class="rev"><h4>' + esc(r.label) + '<span class="n">' + esc(r.score) + '/100</span></h4>'
+ list(r.errors, "er", function (e) {
return '<li>' + (e.shot ? '<span class="sh">shot ' + esc(e.shot) + '</span> ' : '')
+ esc(e.problem || "") + (e.fix ? ' &mdash; <i>' + esc(e.fix) + '</i>' : '') + '</li>'; })
+ list(r.suggestions, "", function (s) {
return '<li>' + (s.shot ? '<span class="sh">shot ' + esc(s.shot) + '</span> ' : '')
+ esc(s.change || "") + '</li>'; })
+ list(r.strengths, "kp", function (s) {
return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; })
+ '</div>';
}).join("") + '</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>';
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>'
+ 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>';
}
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>';
}
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
+ '<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</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
+ '<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>';
}
// ------------------------------------------------------------ actions
async function act(name, payload, msg) {
var r = await api(name, payload);
if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; }
hideMsg("msg");
if (msg) showMsg("msg", msg(r), "ok");
return r;
}
document.getElementById("pane").addEventListener("click", async function (e) {
var hit = function (attr) { var el = e.target.closest("[" + attr + "]"); return el; };
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-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();
});
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>