The review panel rendered the old critique shape - errors, suggestions, strengths. The comprehension test produces something else: three summaries written by readers who saw only the script, and the writer's own list of what the trailer failed to convey. The gap list leads, because that is what the revision consumes and what the author needs to decide about. Each reader's summary is then shown verbatim, with what they were still asking and whether they would watch the film - reading three strangers describe your trailer badly is the most persuasive evidence there is that it does not tell the story. "Got wrong" is called out separately from "missed". A trailer that leaves something out is incomplete; one that leads a viewer to the wrong conclusion is worse, and they are different problems to fix.
577 lines
27 KiB
HTML
577 lines
27 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}
|
|
.gaps{border:1px solid var(--accent);border-radius:10px;padding:14px 16px;margin:0 0 14px}
|
|
.gaps h4{margin:0 0 4px;font-size:15px;color:var(--accent)}
|
|
.gaps .verdict{font-size:14px;margin:0 0 10px}
|
|
.gaps ol{margin:0;padding-left:20px;font-size:13px}
|
|
.gaps li{margin-bottom:7px}
|
|
.gaps .fix{color:var(--muted);display:block}
|
|
.gaps .sh{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
.rd{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
|
|
.rd h4{margin:0 0 6px;font-size:14px}
|
|
.rd h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
|
|
.rd blockquote{margin:0 0 8px;font-size:14px;border-left:2px solid var(--line);padding-left:10px}
|
|
.rd .kv{color:var(--muted);font-size:12.5px;margin:2px 0}
|
|
.rd ul{margin:6px 0 0;padding-left:18px;font-size:13px}
|
|
.rd .wrong{color:#e0685f}
|
|
.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…</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 || ""));
|
|
}
|
|
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; }
|
|
}
|
|
|
|
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();
|
|
var rv = await api("list_reviews", { run_id: runId });
|
|
REVIEWS = (rv && rv.reviews) || [];
|
|
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" title="how well readers understood it">'
|
|
+ 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 shot' + (undrawn === 1 ? '' : 's'))
|
|
+ ' · $' + (imgs * 0.068).toFixed(2) + '</button>');
|
|
else out.push('<span class="soon">storyboard drawn'
|
|
+ (meta.draw_usd ? ' · $' + 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>Readers are watching it…</button>');
|
|
else if (others)
|
|
out.push('<button type="button" class="btn" data-review="1" title="'
|
|
+ 'Each other model reads ONLY this script - no book - and says what it understood.">'
|
|
+ (REVIEWS.length ? "Test it again" : "Does it make sense? Ask " + others + " readers")
|
|
+ '</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')
|
|
+ ' · $' + (unfilmed * 0.75).toFixed(2) + '</button>');
|
|
if (filmed)
|
|
out.push('<button type="button" class="btn primary" data-assemble="' + esc(meta.id)
|
|
+ '">Make the trailer · ' + filmed + ' of ' + shots.length + ' shots</button>');
|
|
}
|
|
return out.join("");
|
|
}
|
|
|
|
// What readers understood, and what the writer says it failed to say.
|
|
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> → '
|
|
+ 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 ? ' · revision ' + meta.iteration : '')
|
|
+ ' · ' + shots.length + ' shots · '
|
|
+ Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0")
|
|
+ ' · ' + 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) + '">▶</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 ? ' · ' + 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 ? ' — 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') + ' · $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') + ' · $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 || "") + ' · '
|
|
+ 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>';
|
|
}
|
|
|
|
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) { 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-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();
|
|
});
|
|
|
|
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>
|