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.
This commit is contained in:
gator 2026-09-17 06:31:29 +00:00
parent a20ed2d57b
commit d21360ea2b

View file

@ -14,7 +14,6 @@
.tab.lead{color:var(--accent)} .tab.lead{color:var(--accent)}
.tab .sc{color:var(--accent2);margin-left:7px;font-weight:600} .tab .sc{color:var(--accent2);margin-left:7px;font-weight:600}
.tab .st{color:var(--muted);margin-left:7px;font-size:12px} .tab .st{color:var(--muted);margin-left:7px;font-size:12px}
.tab .crown{margin-left:6px}
.head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px} .head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px}
.head h2{margin:0;font-size:22px} .head h2{margin:0;font-size:22px}
.tag{color:var(--muted);font-style:italic} .tag{color:var(--muted);font-style:italic}
@ -28,7 +27,7 @@
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block; .sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
aspect-ratio:16/9;object-fit:cover} aspect-ratio:16/9;object-fit:cover}
.shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px} .shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px}
.shot .num{width:34px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums} .shot .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
.shot .body{flex:1;min-width:0} .shot .body{flex:1;min-width:0}
.shot h3{margin:0 0 3px;font-size:15px} .shot h3{margin:0 0 3px;font-size:15px}
.shot .where{color:var(--muted);font-size:12px;margin-bottom:8px} .shot .where{color:var(--muted);font-size:12px;margin-bottom:8px}
@ -36,7 +35,7 @@
.shot .cam,.shot .cont{color:var(--muted);font-size:12.5px;margin:0 0 5px} .shot .cam,.shot .cont{color:var(--muted);font-size:12.5px;margin:0 0 5px}
.frames{display:flex;gap:10px;margin:10px 0} .frames{display:flex;gap:10px;margin:10px 0}
.frames figure{margin:0;flex:1;min-width:0} .frames figure{margin:0;flex:1;min-width:0}
.frames img{width:100%;border-radius:8px;border:1px solid var(--line);display:block} .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} .frames figcaption{color:var(--muted);font-size:11px;margin-top:3px}
.line{display:flex;gap:9px;align-items:flex-start;margin:7px 0} .line{display:flex;gap:9px;align-items:flex-start;margin:7px 0}
.line .say{flex:1;min-width:0;font-size:14px} .line .say{flex:1;min-width:0;font-size:14px}
@ -65,11 +64,21 @@
.rev{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px} .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{margin:0 0 6px;font-size:14px}
.rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px} .rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
.rev .v{color:var(--muted);font-size:13px;margin:0 0 8px}
.rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px} .rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px}
.rev .er{color:#e0685f} .rev .er{color:#e0685f}
.rev .kp{color:var(--accent2)} .rev .kp{color:var(--accent2)}
.rev .sh{color:var(--muted);font-variant-numeric:tabular-nums} .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> </style>
</head> </head>
<body> <body>
@ -92,14 +101,12 @@
navBar("trailer"); navBar("trailer");
var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = []; var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = [];
var timer = null; var timer = null, EDITING = null;
var runId = new URLSearchParams(location.search).get("run") || ""; var runId = new URLSearchParams(location.search).get("run") || "";
var audio = new Audio(); var audio = new Audio();
if (!runId) { showMsg("msg", "No run was named in the link."); return; } if (!runId) { showMsg("msg", "No run was named in the link."); return; }
// A draft is busy while it is being written or drawn; a run is busy while it
// is reviewing or revising. Both are minutes long and report on the row.
function busyDraft(d) { function busyDraft(d) {
var s = String(d.draw_state || ""); var s = String(d.draw_state || "");
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s)); return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
@ -107,28 +114,42 @@
function busyRun() { function busyRun() {
return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || "")); 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(repaint) { async function load() {
var r = await api("run_status", { run_id: runId }); var r = await api("run_status", { run_id: runId });
if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; } if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; }
var wasBusy = (DRAFTS.length && DRAFTS.some(busyDraft)) || busyRun();
RUN = r.run; DRAFTS = r.drafts || []; RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("title").textContent = RUN.book_title || "Trailers"; document.getElementById("title").textContent = RUN.book_title || "Trailers";
document.getElementById("sub").textContent = sub(); document.getElementById("sub").textContent = sub();
if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id; if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id;
paintTabs(); paintTabs();
var rv = await api("list_reviews", { run_id: runId }); var rv = await api("list_reviews", { run_id: runId });
REVIEWS = (rv && rv.reviews) || []; REVIEWS = (rv && rv.reviews) || [];
await openTab(CUR);
tick();
}
var busy = DRAFTS.some(busyDraft) || busyRun(); function tick() {
if (busy && !timer) timer = setInterval(load, 5000); var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots();
if (busy && !timer) timer = setInterval(refresh, 5000);
if (!busy && timer) { clearInterval(timer); timer = null; } if (!busy && timer) { clearInterval(timer); timer = null; }
}
var me = DRAFTS.filter(function (d) { return d.id === CUR; })[0]; // A refresh keeps the tab you are on and the box you are typing in.
if (me && me.state === "writing") { paintWriting(); return; } async function refresh() {
if (repaint !== false && (wasBusy || !document.querySelector(".shot"))) openTab(CUR); var r = await api("run_status", { run_id: runId });
else paintBar(); if (!r.ok) return;
RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("sub").textContent = sub();
paintTabs();
await openTab(CUR);
tick();
} }
function leadDraftId() { function leadDraftId() {
@ -146,6 +167,7 @@
bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s")); bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s"));
var spent = DRAFTS.reduce(function (a, d) { var spent = DRAFTS.reduce(function (a, d) {
return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0); 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 (spent) bits.push("$" + spent.toFixed(2) + " so far");
if (RUN.created_at) bits.push(when(RUN.created_at)); if (RUN.created_at) bits.push(when(RUN.created_at));
return bits.join(" · "); return bits.join(" · ");
@ -159,30 +181,21 @@
: (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>' : (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>'
: d.score ? '<span class="sc">' + d.score + '</span>' : d.score ? '<span class="sc">' + d.score + '</span>'
: ''; : '';
var isLead = d.writer === RUN.lead_writer;
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '') return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
+ (isLead ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">' + (d.writer === RUN.lead_writer ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
+ esc(d.label || d.writer) + esc(d.label || d.writer)
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '') + (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
+ (isLead ? '<span class="crown" title="head writer">★</span>' : '') + (d.writer === RUN.lead_writer ? ' ★' : '') + extra + '</button>';
+ extra + '</button>';
}).join(""); }).join("");
} }
function paintWriting() {
document.getElementById("pane").innerHTML =
'<p class="empty">Working. Reading the whole book takes a few minutes.</p>';
}
document.getElementById("tabs").addEventListener("click", function (e) { document.getElementById("tabs").addEventListener("click", function (e) {
var id = e.target.closest ? e.target.closest(".tab") : null; var id = e.target.closest ? e.target.closest(".tab") : null;
if (!id) return; if (!id) return;
CUR = id.getAttribute("data-tab"); CUR = id.getAttribute("data-tab"); EDITING = null;
paintTabs(); openTab(CUR); paintTabs(); openTab(CUR);
}); });
// ------------------------------------------------------------ one trailer
async function openTab(id) { async function openTab(id) {
var meta = DRAFTS.filter(function (d) { return d.id === id; })[0]; var meta = DRAFTS.filter(function (d) { return d.id === id; })[0];
if (!meta) return; if (!meta) return;
@ -194,8 +207,10 @@
+ esc(meta.id) + '">Try this writer again</button></div>'; + esc(meta.id) + '">Try this writer again</button></div>';
return; return;
} }
if (meta.state === "writing") { paintWriting(); return; } if (meta.state === "writing") {
pane.innerHTML = '<p class="empty">Loading…</p>'; 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 }); var r = await api("get_draft", { draft_id: id });
if (!r.ok) { pane.innerHTML = '<p class="empty">' + esc(r.error || "") + '</p>'; return; } if (!r.ok) { pane.innerHTML = '<p class="empty">' + esc(r.error || "") + '</p>'; return; }
PKG = r.draft.package || {}; PKG = r.draft.package || {};
@ -204,95 +219,77 @@
} }
function barHtml(meta) { function barHtml(meta) {
var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length; var shots = (PKG.shots || []), cast = (PKG.characters || []);
var imgs = cast * 3 + shots * 2; var undrawn = shots.filter(function (s) { return !s.opening_url; }).length;
var price = imgs * 0.068; 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 s = String(meta.draw_state || "");
var drawing = s && !/^(drawn|failed)/.test(s); var drawing = s && !/^(drawn|failed)/.test(s);
var drawn = /^drawn/.test(s);
var isLead = meta.writer === RUN.lead_writer; var isLead = meta.writer === RUN.lead_writer;
var out = []; var out = [];
// 1. Whose concept wins. The author's call, not a score's. if (!isLead) out.push('<button type="button" class="btn primary" data-lead="'
if (!isLead) { + esc(meta.id) + '">Make this the head writer</button>');
out.push('<button type="button" class="btn primary" data-lead="' + esc(meta.id) else out.push('<span class="soon">★ head writer</span>');
+ '">Make this the head writer</button>');
} else {
out.push('<span class="soon">★ head writer</span>');
}
// 2. Drawing. if (drawing) out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
if (drawing) { else if (undrawn) out.push('<button type="button" class="btn" data-draw="' + esc(meta.id)
out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>'); + '">Draw ' + (undrawn === shots.length ? 'this storyboard' : undrawn + ' undrawn shots')
} else { + ' &middot; $' + (imgs * 0.068).toFixed(2) + '</button>');
out.push('<button type="button" class="btn" data-draw="' + esc(meta.id) + '">' else out.push('<span class="soon">storyboard drawn'
+ (drawn ? "Draw it again" : "Draw this storyboard") + ' &middot; $'
+ price.toFixed(2) + '</button>');
if (!drawn) out.push('<span class="soon">' + imgs + ' pictures</span>');
else out.push('<span class="soon">' + esc(s)
+ (meta.draw_usd ? ' &middot; $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>'); + (meta.draw_usd ? ' &middot; $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
}
// 3. Review, revise, film - only meaningful on the head writer's tab.
if (isLead) { if (isLead) {
var others = DRAFTS.filter(function (d) { var others = DRAFTS.filter(function (d) {
return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length; return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length;
if (RUN.stage === "reviewing") { if (RUN.stage === "reviewing")
out.push('<button type="button" class="btn" disabled>The others are reading it…</button>'); out.push('<button type="button" class="btn" disabled>The others are reading it…</button>');
} else if (others) { else if (others)
out.push('<button type="button" class="btn" data-review="1">' out.push('<button type="button" class="btn" data-review="1">'
+ (REVIEWS.length ? "Review it again" : "Have the others review this") + (REVIEWS.length ? "Review it again" : "Have the others review this") + '</button>');
+ ' &middot; ' + others + ' readers</button>'); if (RUN.stage === "revising")
}
if (RUN.stage === "revising") {
out.push('<button type="button" class="btn" disabled>Rewriting…</button>'); out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
} else if (REVIEWS.length || Object.keys(NOTES).length) { else if (REVIEWS.length || Object.keys(NOTES).length)
out.push('<button type="button" class="btn primary" data-revise="1">' out.push('<button type="button" class="btn" data-revise="1">Revise the whole script</button>');
+ 'Revise with the notes</button>');
} if (unfilmed && !undrawn)
if (drawn) {
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id) out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
+ '">Film this trailer</button>'); + '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots')
} else { + ' &middot; $' + (unfilmed * 0.75).toFixed(2) + '</button>');
out.push('<span class="soon">Draw the storyboard before filming &mdash; ' if (filmed)
+ 'every shot is filmed from its opening frame.</span>'); 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(""); return out.join("");
} }
function paintBar() {
var el = document.querySelector(".bar");
var meta = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
if (el && meta) el.innerHTML = barHtml(meta);
}
function reviewsHtml(meta) { function reviewsHtml(meta) {
var mine = REVIEWS.filter(function (r) { var mine = REVIEWS.filter(function (r) {
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); }); return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
if (!mine.length) return ""; if (!mine.length) return "";
return '<div class="revs">' + mine.map(function (r) { return '<div class="revs">' + mine.map(function (r) {
var parts = ['<div class="rev"><h4>' + esc(r.label)
+ '<span class="n">' + esc(r.score) + '/100</span></h4>'];
function list(items, cls, fmt) { function list(items, cls, fmt) {
if (!items || !items.length) return ""; return (!items || !items.length) ? ""
return '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>'; : '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>';
} }
parts.push(list(r.errors, "er", function (e) { 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> ' : '') return '<li>' + (e.shot ? '<span class="sh">shot ' + esc(e.shot) + '</span> ' : '')
+ esc(e.problem || "") + (e.fix ? ' &mdash; <i>' + esc(e.fix) + '</i>' : '') + '</li>'; })); + esc(e.problem || "") + (e.fix ? ' &mdash; <i>' + esc(e.fix) + '</i>' : '') + '</li>'; })
parts.push(list(r.suggestions, "", function (s) { + list(r.suggestions, "", function (s) {
return '<li>' + (s.shot ? '<span class="sh">shot ' + esc(s.shot) + '</span> ' : '') return '<li>' + (s.shot ? '<span class="sh">shot ' + esc(s.shot) + '</span> ' : '')
+ esc(s.change || "") + (s.why ? ' <span class="sh">(' + esc(s.why) + ')</span>' : '') + '</li>'; })); + esc(s.change || "") + '</li>'; })
parts.push(list(r.strengths, "kp", function (s) { + list(r.strengths, "kp", function (s) {
return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; })); return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; })
return parts.join("") + '</div>'; + '</div>';
}).join("") + '</div>'; }).join("") + '</div>';
} }
function paneHtml(meta, draft) { function paneHtml(meta, draft) {
var p = PKG, shots = p.shots || [], cast = p.characters || []; var p = PKG, shots = (p.shots || []).slice().sort(function (a, b) {
var voice = {}; return String(a.id) < String(b.id) ? -1 : 1; });
var cast = p.characters || [], voice = {};
cast.forEach(function (c) { voice[c.id] = c; }); cast.forEach(function (c) { voice[c.id] = c; });
var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0); var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0);
@ -303,43 +300,58 @@
+ (Number(meta.iteration) > 1 ? ' &middot; revision ' + meta.iteration : '') + (Number(meta.iteration) > 1 ? ' &middot; revision ' + meta.iteration : '')
+ ' &middot; ' + shots.length + ' shots &middot; ' + ' &middot; ' + shots.length + ' shots &middot; '
+ Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0") + Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0")
+ ' &middot; ' + cast.length + ' characters' + ' &middot; ' + cast.length + ' characters</p>';
+ (meta.write_usd ? ' &middot; $' + Number(meta.write_usd).toFixed(2) + ' to write' : '')
+ (meta.note ? ' &middot; ' + esc(meta.note) : '')
+ '</p>';
var probs = ""; var probs = draft.problems
if (draft.problems) { ? '<div class="probs"><b>The checker has objections</b><ul>'
probs = '<div class="probs"><b>The checker still has objections</b><ul>'
+ draft.problems.split("\n").filter(Boolean).map(function (x) { + draft.problems.split("\n").filter(Boolean).map(function (x) {
return '<li>' + esc(x) + '</li>'; }).join("") return '<li>' + esc(x) + '</li>'; }).join("") + '</ul></div>'
+ '</ul></div>'; : "";
}
var castHtml = '<div class="cast">' + cast.map(function (c) { var castHtml = '<div class="cast">' + cast.map(function (c) {
var sheets = ""; var sheets = (c.head_url || c.front_url || c.side_url)
if (c.head_url || c.front_url || c.side_url) { ? '<div class="sheets">' + [c.head_url, c.front_url, c.side_url].map(function (u) {
sheets = '<div class="sheets">' return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("") + '</div>' : '';
+ [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>' return '<div class="ch">' + sheets + '<b>' + esc(c.name || c.id) + '</b>'
+ '<span class="vo">' + esc(c.role || "") + (c.age ? " · " + esc(c.age) : "") + '<span class="vo">' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent")
+ (c.voice_id ? " · voiced" : " · silent") + '</span>' + '</span><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
+ '<div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
}).join("") + '</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>' return head + probs + '<div class="bar">' + barHtml(meta) + '</div>'
+ reviewsHtml(meta) + castHtml + reviewsHtml(meta) + castHtml + body;
+ shots.map(function (s) { return shotHtml(s, voice); }).join(""); }
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) { 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 lines = (s.audio || []).map(function (a) {
var who = voice[a.character_id] || {}; var who = voice[a.character_id] || {};
return '<div class="line">' return '<div class="line">'
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '" title="Hear this shot">&#9654;</button>' + '<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> ' + '<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>') + (a.lip_sync ? '<span class="lip">on camera</span>' : '<span class="vobadge">voice over</span>')
+ '<br>' + esc(a.text || "") + '<br>' + esc(a.text || "")
@ -347,102 +359,168 @@
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>'; + '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
}).join(""); }).join("");
var frames = ""; var media = "";
if (s.opening_url || s.closing_url) { if (s.clip_url) {
frames = '<div class="frames">' 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.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>' : '') + (s.closing_url ? '<figure><img src="' + esc(s.closing_url) + '" alt=""><figcaption>ends on</figcaption></figure>' : '')
+ '</div>'; + '</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">' return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
+ '<h3>' + esc(s.title || "") + '</h3>' + '<h3>' + esc(s.title || "") + '</h3>'
+ '<div class="where">' + esc(s.setting || "") + ' &middot; ' + '<div class="where">' + esc(s.setting || "") + ' &middot; '
+ Number(s.edit_seconds || 0).toFixed(1) + 's</div>' + Number(s.edit_seconds || 0).toFixed(1) + 's</div>'
+ frames + media
+ '<p class="act">' + esc(s.action || "") + '</p>' + '<p class="act">' + esc(s.action || "") + '</p>'
+ (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '') + (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '')
+ (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '') + (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '')
+ lines + lines
+ '<div class="tools">' + tools + '</div>'
+ '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" ' + '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" '
+ 'placeholder="Your note on this shot. It outranks everything the other writers said.">' + 'placeholder="Your note on this shot, for the next full revision.">'
+ esc(NOTES[s.id] || "") + '</textarea>' + esc(NOTES[s.id] || "") + '</textarea>'
+ '</div></div>'; + '</div></div>';
} }
// ------------------------------------------------------------ actions // ------------------------------------------------------------ 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) { document.getElementById("pane").addEventListener("click", async function (e) {
var t = e.target.closest ? e.target : null; var hit = function (attr) { var el = e.target.closest("[" + attr + "]"); return el; };
if (!t) return; var el, r;
var lead = e.target.closest("[data-lead]"); if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; }
if (lead) { if ((el = hit("data-add"))) { EDITING = "new:" + el.getAttribute("data-add"); openTab(CUR); return; }
lead.disabled = true; if ((el = hit("data-edit"))) { EDITING = el.getAttribute("data-edit"); openTab(CUR); return; }
var a = await api("set_lead", { run_id: runId, draft_id: lead.getAttribute("data-lead") });
if (!a.ok) { lead.disabled = false; showMsg("msg", a.error || ""); return; } if ((el = hit("data-addgo"))) {
hideMsg("msg"); await load(); openTab(CUR); return; 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 draw = e.target.closest("[data-draw]"); var rw = document.getElementById("rough").value.trim();
if (draw) { if (rw.length < 20) { showMsg("msg", "Say what should change."); return; }
draw.disabled = true; el.disabled = true;
var d = await api("draw_storyboard", { draft_id: draw.getAttribute("data-draw") }); r = await act("write_shot", { draft_id: CUR, shot_id: el.getAttribute("data-rewrite"), rough: rw });
if (!d.ok) { draw.disabled = false; showMsg("msg", d.error || ""); return; } EDITING = null; if (r) { await refresh(); } return;
hideMsg("msg"); load(); return;
} }
if ((el = hit("data-drawone"))) {
var rev = e.target.closest("[data-review]"); el.disabled = true;
if (rev) { r = await act("draw_one_shot", { draft_id: CUR, shot_id: el.getAttribute("data-drawone") });
rev.disabled = true; if (r) await refresh(); return;
var v = await api("start_review", { run_id: runId });
if (!v.ok) { rev.disabled = false; showMsg("msg", v.error || ""); return; }
hideMsg("msg"); load(); return;
} }
if ((el = hit("data-filmone"))) {
var rvs = e.target.closest("[data-revise]"); el.disabled = true;
if (rvs) { r = await act("film_one_shot_now", { draft_id: CUR, shot_id: el.getAttribute("data-filmone") });
rvs.disabled = true; if (r) await refresh(); return;
var w = await api("start_revision", { run_id: runId });
if (!w.ok) { rvs.disabled = false; showMsg("msg", w.error || ""); return; }
hideMsg("msg"); load(); return;
} }
if ((el = hit("data-delete"))) {
var film = e.target.closest("[data-film]"); if (!confirm("Remove shot " + el.getAttribute("data-delete") + " from this trailer?")) return;
if (film) { el.disabled = true;
film.disabled = true; r = await act("delete_shot", { draft_id: CUR, shot_id: el.getAttribute("data-delete") });
var f = await api("film_draft", { draft_id: film.getAttribute("data-film") }); if (r) await refresh(); return;
if (!f.ok) { film.disabled = false; showMsg("msg", f.error || ""); return; } }
showMsg("msg", "Filming " + f.shots + " shots, about $" + f.usd if ((el = hit("data-assemble"))) {
+ ". It appears in My videos as it goes.", "ok"); el.disabled = true;
setTimeout(function () { location.href = "/videos.html"; }, 2500); 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; return;
} }
if ((el = hit("data-lead"))) {
var retry = e.target.closest("[data-retry]"); el.disabled = true;
if (retry) { r = await act("set_lead", { run_id: runId, draft_id: el.getAttribute("data-lead") });
retry.disabled = true; if (r) await refresh(); return;
var y = await api("retry_writer", { draft_id: retry.getAttribute("data-retry") }); }
if (!y.ok) { retry.disabled = false; showMsg("msg", y.error || ""); return; } if ((el = hit("data-draw"))) {
hideMsg("msg"); load(); return; 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"); var btn = e.target.closest(".play");
if (!btn) return; if (!btn) return;
var sid = btn.getAttribute("data-shot"); var sid = btn.getAttribute("data-shot");
btn.disabled = true; btn.innerHTML = "&hellip;"; btn.disabled = true; btn.innerHTML = "&hellip;";
var r = await api("shot_audio", { draft_id: CUR, shot_id: sid }); var a = await api("shot_audio", { draft_id: CUR, shot_id: sid });
btn.disabled = false; btn.innerHTML = "&#9654;"; btn.disabled = false; btn.innerHTML = "&#9654;";
if (!r.ok) { showMsg("msg", r.error || "That would not play."); return; } if (!a.ok) { showMsg("msg", a.error || "That would not play."); return; }
hideMsg("msg"); hideMsg("msg");
if (!r.url) return; if (!a.url) return;
// Spoken length against the length the writer planned for. A line that document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (x) {
// overruns its shot is the single most common fault in these scripts. x.textContent = a.seconds.toFixed(1) + "s / " + Number(a.planned).toFixed(1) + "s";
document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (el) { x.className = "dur" + (a.seconds > Number(a.planned) ? " over" : "");
el.textContent = r.seconds.toFixed(1) + "s / " + Number(r.planned).toFixed(1) + "s";
el.className = "dur" + (r.seconds > Number(r.planned) ? " over" : "");
}); });
audio.pause(); audio.src = r.url; audio.play(); audio.pause(); audio.src = a.url; audio.play();
}); });
var noteTimers = {}; var noteTimers = {};
@ -452,8 +530,6 @@
if (!sid) return; if (!sid) return;
ta.classList.remove("saved"); ta.classList.remove("saved");
clearTimeout(noteTimers[sid]); clearTimeout(noteTimers[sid]);
// Saved as you stop typing. A note the author has to remember to save is
// a note that gets lost between tabs.
noteTimers[sid] = setTimeout(async function () { noteTimers[sid] = setTimeout(async function () {
var r = await api("add_shot_note", { draft_id: CUR, shot_id: sid, note: ta.value }); 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"); } if (r.ok) { NOTES[sid] = ta.value; ta.classList.add("saved"); }