paytolaugh/script.html

593 lines
28 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Direct the shots — PayToLaugh</title>
<meta name="description" content="Read the script, set the scene and camera for each shot, then approve it.">
<link rel="stylesheet" href="/assets/app.css">
<style>
.shot{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:18px;
display:grid;grid-template-columns:190px 1fr;gap:22px;align-items:start}
@media(max-width:720px){.shot{grid-template-columns:1fr}}
.shot .still{width:100%;aspect-ratio:9/16;border-radius:10px;border:1px solid var(--line);
object-fit:cover;display:block;background:#000}
.shot .noimg{width:100%;aspect-ratio:9/16;border-radius:10px;border:1px dashed var(--line);
background:#0f1117;display:flex;align-items:center;justify-content:center;text-align:center;
color:var(--muted);font-size:12.5px;padding:14px}
.shot .n{font:600 11px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
.shot .cont{font:11px/1 var(--mono);color:var(--accent2);margin-left:8px}
.shot .line{font-size:17px;line-height:1.5;margin:10px 0 16px}
.shot .foot{font:11.5px/1 var(--mono);color:var(--muted)}
.mini{font-size:13px;font-weight:600;margin:0 0 5px;display:block}
.mini span{color:var(--muted);font-weight:400}
.shot textarea{min-height:62px}
.warn{background:#2a2112;border:1px solid #5b4520;color:#f0d9a7;border-radius:10px;padding:13px 16px;margin-bottom:16px;font-size:14px}
.drop{border:1px dashed var(--line);border-radius:12px;padding:26px 20px;text-align:center;color:var(--muted);
background:#0f1117;cursor:pointer;font-size:14.5px}
.drop.hot{border-color:var(--accent);color:var(--ink)}
.pics{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:16px}
.pic img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line);display:block}
.pic input{margin-top:6px;font-size:13px;padding:7px 9px}
.pic .rm{background:none;border:0;color:var(--muted);cursor:pointer;font:11px/1.6 var(--mono);text-decoration:underline;padding:0}
.place{border-top:1px solid var(--line);padding:8px 0;font:12.5px/1.6 var(--mono);color:var(--muted)}
.full{background:#0f1117;border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:24px;
color:var(--muted);font-size:15.5px;line-height:1.7}
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1 id="title">The script</h1>
<p id="sub">Read it through, then direct it. Every shot can have its own setting and camera move.</p>
</header>
<section>
<div class="msg" id="msg"></div>
<div id="out"><p style="color:var(--muted)">Loading…</p></div>
</section>
<footer>PayToLaugh &middot; your pet has opinions.</footer>
</div>
<script src="/assets/app.js"></script>
<script>
navBar("videos");
var email = requireSession();
var pid = new URLSearchParams(location.search).get("id");
var poll = null, PRESETS = [], PHOTOS = [], DATA = null;
if (!pid) {
document.getElementById("out").innerHTML =
'<div class="empty">No script chosen. <a href="/videos.html">Back to my videos</a>.</div>';
}
function photoOptions(sel) {
if (!PHOTOS.length) return '<option value="">No photos uploaded</option>';
return PHOTOS.map(function (p) {
var label = p.kind.charAt(0).toUpperCase() + p.kind.slice(1);
return '<option value="' + esc(p.id) + '"' + (p.id === sel ? ' selected' : '') + '>'
+ esc(label) + '</option>';
}).join("");
}
function shotHtml(s, locked) {
var img = s.plate_url
? '<img class="still" src="' + esc(s.plate_url) + '" alt="Shot ' + s.index + ' opening frame">'
: '<div class="noimg">No opening frame yet.<br>Set the scene, then make one.</div>';
var cont = s.source === "previous" && s.index > 1;
var toggle = s.index > 1
? '<div class="chips" data-srcgrp="' + s.index + '" style="margin-bottom:14px">'
+ '<span class="chip' + (cont ? '' : ' on') + '" data-src="photo">Start from a photo</span>'
+ '<span class="chip' + (cont ? ' on' : '') + '" data-src="previous">Continue from shot ' + (s.index - 1) + '</span>'
+ '</div>'
: '';
var settingLabel = cont
? '<label class="mini">The setting <span>— stays as it was; the camera is what moves</span></label>'
: '<label class="mini">The setting <span>— anywhere, including places a real pet could not be</span></label>';
var picker = cont
? '<p class="foot" style="margin:0">Continues the previous shot. Filming uses that shot&rsquo;s '
+ 'real last frame; the still here is a preview of the camera move.</p>'
: '<label class="mini">Which photo of them</label>'
+ '<select data-photo="' + s.index + '">' + photoOptions(s.photo_id) + '</select>';
// A cutaway belonging to this shot straddles the cut into the next one, so
// the registration jump happens off screen.
var br = s.broll || {};
var brollBlock = '';
if (br.subject) {
brollBlock = '<div style="margin-top:16px;padding-top:14px;border-top:1px solid var(--line)">'
+ '<label class="mini">Cutaway <span>— shown while this line is spoken, covering the cut</span></label>'
+ '<p class="foot" style="margin:0 0 8px">' + esc(br.subject) + ' · ' + esc(br.kind || 'image') + '</p>'
+ (s.broll_url
? (s.broll_kind === 'video'
? '<video src="' + esc(s.broll_url) + '" controls playsinline style="width:120px;border-radius:8px;border:1px solid var(--line)"></video>'
: '<img src="' + esc(s.broll_url) + '" alt="cutaway" style="width:120px;border-radius:8px;border:1px solid var(--line);display:block">')
: '')
+ (locked ? '' : '<button type="button" class="btn ghost" data-broll="' + s.index + '" style="margin-top:10px">'
+ (s.broll_url ? 'Remake cutaway' : 'Make cutaway') + '</button>')
+ '</div>';
}
var controls = locked ? '' :
toggle
+ settingLabel
+ '<textarea data-scene="' + s.index + '" placeholder="a field of tall grass at golden hour">' + esc(s.scene || "") + '</textarea>'
+ '<label class="mini" style="margin-top:12px">Camera</label>'
+ '<input type="text" list="cams" data-cam="' + s.index + '" value="' + esc(s.camera || "") + '" placeholder="tight head shot, camera static">'
+ '<div class="row" style="margin-top:12px">'
+ '<div>' + picker + '</div>'
+ '<div style="display:flex;align-items:flex-end">'
+ '<button type="button" class="btn ghost" data-make="' + s.index + '" style="width:100%">'
+ (s.plate_url ? "Remake this frame" : "Make the frame") + '</button></div>'
+ '</div>';
return '<div class="shot">'
+ '<div>' + img + '</div>'
+ '<div>'
+ '<span class="n">Shot ' + (s.index || "") + '</span>'
+ (s.continues ? '<span class="cont">continues the last shot</span>' : '')
+ '<p class="line">' + esc(s.line || "") + '</p>'
+ '<p class="foot">' + (s.word_count || 0) + ' words · about ' + (s.est_seconds || 0) + 's</p>'
+ '<div style="margin-top:16px">' + controls + '</div>'
+ brollBlock
+ '</div></div>';
}
function render(r) {
DATA = r;
var p = r.project, s = r.script, out = document.getElementById("out");
var used = parseInt(p.revisions || 0, 10);
var left = (r.max_revisions || 3) - used;
if (p.status === "scripting") {
out.innerHTML = '<div class="card"><p style="margin:0;color:var(--muted)">'
+ 'Writing now. This usually takes under a minute — the page updates itself.</p></div>';
if (!poll) poll = setInterval(load, 5000);
return;
}
if (poll) { clearInterval(poll); poll = null; }
if (p.status === "script_failed") {
out.innerHTML = '<div class="card"><p style="margin:0 0 16px">The writer could not finish this one.</p>'
+ '<a class="btn" style="display:inline-block;text-decoration:none" href="/create.html">Start again</a></div>';
return;
}
if (!s || !s.segments) { out.innerHTML = '<div class="empty">No script yet.</div>'; return; }
document.getElementById("title").textContent = s.title || "The script";
document.getElementById("sub").textContent = s.premise || "";
var locked = p.status === "approved";
var warn = (s._warnings && s._warnings.length)
? '<div class="warn"><strong>Worth a look:</strong> ' + s._warnings.map(esc).join(" · ") + '</div>' : "";
var head = '<div class="quote" style="margin-bottom:22px">'
+ '<div><div class="big">$' + Number(p.price_usd || 0).toFixed(2) + '</div>'
+ '<div class="sub">' + ((p.segments || 4) * 8) + ' seconds · ' + (p.segments || 4) + ' shots</div></div>'
+ '<div class="sub">' + (locked ? "Approved" : left + " rewrite" + (left === 1 ? "" : "s") + " left") + '</div></div>';
var full = s.full_text
? '<h2 class="sec">The whole piece</h2><div class="full">' + esc(s.full_text) + '</div>' : "";
var cams = '<datalist id="cams">' + PRESETS.map(function (c) {
return '<option value="' + esc(c) + '">'; }).join("") + '</datalist>';
// Post production only exists once there is footage to cut.
// The owner's own photographs, scoped to this video only. Described once,
// then matched to shots by reading the script rather than hand-assigned.
var own = locked ? '' :
'<h2 class="sec" style="margin-top:34px">Your own pictures</h2>'
+ '<div class="card">'
+ '<p class="foot" style="margin:0 0 14px">Paste or drop photographs here. Describe each one, '
+ 'and they can be cut in where the script talks about them, or laid across a join to cover it.</p>'
+ '<div class="drop" id="drop" tabindex="0">Paste an image, drop one here, or click to choose</div>'
+ '<input type="file" id="picker" accept="image/*" multiple style="display:none">'
+ '<div class="pics" id="pics"></div>'
+ '<div style="margin-top:16px">'
+ '<button type="button" class="btn ghost" id="suggest">Suggest where they fit</button>'
+ '<span id="sugnote" class="foot" style="margin-left:12px"></span></div>'
+ '<div id="places"></div>'
+ '</div>';
// Filming, from the browser. This was the last step that needed an operator.
var film = '';
if (p.status === 'approved' || p.status === 'queued_to_film'
|| p.status === 'filming' || p.status === 'film_failed') {
var busy = (p.status === 'queued_to_film' || p.status === 'filming');
film = '<h2 class="sec" style="margin-top:34px">Film it</h2><div class="card">'
+ '<div class="quote" style="margin-bottom:16px">'
+ '<div><div class="big">' + ((p.segments || 4) * 8) + 's</div>'
+ '<div class="sub">of your time credit</div></div>'
+ '<div class="sub" id="balsec">checking your balance…</div></div>'
+ (p.status === 'film_failed'
? '<div class="warn">That did not finish: ' + esc(p.film_note || 'unknown reason')
+ '. Your seconds were returned.</div>'
: '')
+ (busy
? '<p class="foot" id="filmnote">' + esc(p.film_note || 'Queued…') + '</p>'
: '<button type="button" class="btn" id="film">'
+ (p.status === 'film_failed' ? 'Try filming again' : 'Film it')
+ '</button>'
+ '<span class="foot" style="margin-left:12px">About a minute a shot.</span>')
+ '</div>';
}
var post = '';
if (p.status === 'ready' || p.cut_url) {
post = '<h2 class="sec" style="margin-top:34px">The cut</h2><div class="card">'
+ (p.cut_url
? '<video src="' + esc(p.cut_url) + '" controls playsinline style="width:190px;aspect-ratio:9/16;border-radius:10px;border:1px solid var(--line);background:#000;display:block;margin-bottom:14px"></video>'
: '')
+ '<p class="foot" style="margin:0 0 12px">Cutaways are laid across the joins between shots, '
+ 'so the picture changes while the voice keeps going and the join is never on screen.</p>'
+ '<button type="button" class="btn ghost" id="seams">Measure the joins</button>'
+ '<button type="button" class="btn" id="cut" style="margin-left:10px">'
+ (p.cut_url ? 'Cut it again' : 'Cut the B-roll in') + '</button>'
+ '<div id="seamout" style="margin-top:14px"></div>'
+ '</div>';
}
var actions = locked
? '<div class="card"><p style="margin:0"><strong>Approved.</strong> Filming is switched on once payment is live. Nothing has been charged.</p></div>'
: '<div class="card">'
+ '<h2 class="sec">Change the words</h2>'
+ '<div class="field"><label for="fb">What would you change</label>'
+ '<span class="hint">Be specific. “Too polite, he would be smugger” beats “make it funnier”. '
+ 'Setting and camera you can just edit directly above.</span>'
+ '<textarea id="fb" ' + (left <= 0 ? "disabled" : "") + ' placeholder="Shot three is too wordy, and he would never admit the robot is good at anything."></textarea></div>'
+ '<button class="btn ghost" id="revise" ' + (left <= 0 ? "disabled" : "") + '>'
+ (left > 0 ? "Rewrite the words" : "No rewrites left") + '</button>'
+ '<button class="btn" id="approve" style="margin-left:10px">Approve and lock it</button>'
+ '</div>';
out.innerHTML = cams + head + warn + full
+ '<h2 class="sec">The shots</h2>'
+ s.segments.map(function (x) { return shotHtml(x, locked); }).join("")
+ own
+ film
+ post
+ actions;
wire(locked);
}
var PICS = [], SUGGESTED = [];
function renderPics() {
var el = document.getElementById("pics");
if (!el) return;
el.innerHTML = PICS.map(function (p) {
return '<div class="pic">'
+ '<img src="' + esc(p.url) + '" alt="' + esc(p.description || "") + '">'
+ '<input type="text" value="' + esc(p.description || "") + '" readonly>'
+ '<button type="button" class="rm" data-rmpic="' + esc(p.id) + '">remove</button>'
+ '</div>';
}).join("");
[].forEach.call(el.querySelectorAll("[data-rmpic]"), function (b) {
b.onclick = async function () {
var r = await api("delete_project_image", { email: email, image_id: b.getAttribute("data-rmpic") });
if (!r.ok) { showMsg("msg", r.error || "Could not remove that."); return; }
await loadPics();
};
});
}
async function loadPics() {
var r = await api("list_project_images", { email: email, project_id: pid });
PICS = (r.ok && r.images) || [];
renderPics();
}
async function addPic(file) {
hideMsg("msg");
if (!file || !/^image\//.test(file.type)) { showMsg("msg", "That is not an image."); return; }
// Asked for up front: a picture with no description cannot be matched to
// anything, so it would just sit there unused.
var desc = prompt("What is in this picture? A short description is what lets it be matched to the script.");
if (!desc || desc.trim().length < 3) { showMsg("msg", "A description is needed to place the picture."); return; }
var drop = document.getElementById("drop");
if (drop) drop.textContent = "Uploading…";
var img;
try { img = await resizeImage(file, 1600, 0.85); }
catch (e) { showMsg("msg", e.message || "Could not read that image."); return; }
var r = await api("add_project_image", { email: email, project_id: pid,
description: desc.trim(), data_b64: img.b64 });
if (drop) drop.textContent = "Paste an image, drop one here, or click to choose";
if (!r.ok) { showMsg("msg", r.error || "That upload did not work."); return; }
await loadPics();
}
function bindPics() {
var drop = document.getElementById("drop");
if (!drop) return;
var picker = document.getElementById("picker");
drop.onclick = function () { picker.click(); };
picker.onchange = function () {
[].forEach.call(picker.files || [], addPic);
picker.value = "";
};
drop.addEventListener("dragover", function (e) { e.preventDefault(); drop.classList.add("hot"); });
drop.addEventListener("dragleave", function () { drop.classList.remove("hot"); });
drop.addEventListener("drop", function (e) {
e.preventDefault(); drop.classList.remove("hot");
[].forEach.call((e.dataTransfer && e.dataTransfer.files) || [], addPic);
});
// Paste anywhere on the page, which is how people actually do this.
document.addEventListener("paste", function (e) {
var items = (e.clipboardData && e.clipboardData.items) || [];
for (var i = 0; i < items.length; i++) {
if (items[i].type && items[i].type.indexOf("image") === 0) {
var f = items[i].getAsFile();
if (f) { e.preventDefault(); addPic(f); }
return;
}
}
});
var sg = document.getElementById("suggest");
if (sg) sg.onclick = async function () {
hideMsg("msg");
if (!PICS.length) { showMsg("msg", "Add a picture first."); return; }
sg.disabled = true; sg.textContent = "Reading the script…";
var r = await api("suggest_image_placements", { email: email, project_id: pid });
sg.disabled = false; sg.textContent = "Suggest where they fit";
document.getElementById("sugnote").textContent = r.status_text || "";
if (!r.ok) { showMsg("msg", r.error || "Could not suggest."); return; }
SUGGESTED = r.placements || [];
var byId = {};
PICS.forEach(function (p) { byId[p.id] = p; });
document.getElementById("places").innerHTML = SUGGESTED.length
? SUGGESTED.map(function (x) {
var p = byId[x.image_id] || {};
return '<div class="place">shot ' + x.index + ' · '
+ esc((p.description || "").slice(0, 60)) + ' — ' + esc(x.why || "") + '</div>';
}).join("")
+ '<button type="button" class="btn" id="applyp" style="margin-top:14px">Use these</button>'
: '<p class="foot">No picture fitted any shot well enough to use.</p>';
var ap = document.getElementById("applyp");
if (ap) ap.onclick = async function () {
ap.disabled = true; ap.textContent = "Applying…";
var res = await api("apply_image_placements", { email: email, project_id: pid,
placements: SUGGESTED });
if (!res.ok) { ap.disabled = false; ap.textContent = "Use these"; showMsg("msg", res.error || "Could not apply."); return; }
showMsg("msg", res.status_text || "Placed.", "ok");
load();
};
};
loadPics();
}
function wire(locked) {
bindPics();
bindFilm((DATA && DATA.project) || {});
bindPost();
if (locked) { bindBottom(); return; }
// Save edits when the field loses focus, so typing is never interrupted.
[].forEach.call(document.querySelectorAll("[data-scene],[data-cam],[data-photo]"), function (el) {
var evt = el.tagName === "SELECT" ? "change" : "blur";
el.addEventListener(evt, async function () {
var idx = el.getAttribute("data-scene") || el.getAttribute("data-cam") || el.getAttribute("data-photo");
var body = { email: email, project_id: pid, index: parseInt(idx, 10) };
if (el.hasAttribute("data-scene")) body.scene = el.value;
if (el.hasAttribute("data-cam")) body.camera = el.value;
if (el.hasAttribute("data-photo")) body.photo_id = el.value;
var r = await api("update_shot", body);
if (!r.ok) showMsg("msg", r.error || "That change did not save.");
else hideMsg("msg");
});
});
// Switching source changes which fields apply, so redraw the card after.
[].forEach.call(document.querySelectorAll("[data-srcgrp]"), function (g) {
g.addEventListener("click", async function (e) {
var c = e.target.closest("[data-src]");
if (!c) return;
var idx = parseInt(g.getAttribute("data-srcgrp"), 10);
var r = await api("update_shot", { email: email, project_id: pid, index: idx,
source: c.getAttribute("data-src") });
if (!r.ok) { showMsg("msg", r.error || "Could not change that."); return; }
hideMsg("msg");
load();
});
});
[].forEach.call(document.querySelectorAll("[data-broll]"), function (b) {
b.addEventListener("click", async function () {
hideMsg("msg");
var idx = parseInt(b.getAttribute("data-broll"), 10);
b.disabled = true; b.textContent = "Making…";
var r = await api("make_broll", { email: email, project_id: pid, index: idx });
if (!r.ok) { b.disabled = false; b.textContent = "Make cutaway"; showMsg("msg", r.error || "Could not make that."); return; }
var waited = 0;
var t = setInterval(async function () {
waited += 5;
var g = await api("get_project", { email: email, project_id: pid });
var seg = (g.ok && g.script && g.script.segments || []).filter(function (x) {
return parseInt(x.index, 10) === idx; })[0];
if (seg && seg.broll_url) { clearInterval(t); load(); return; }
if (waited > 180) {
clearInterval(t);
b.disabled = false; b.textContent = "Make cutaway";
showMsg("msg", "That cutaway is taking unusually long.");
}
}, 5000);
});
});
[].forEach.call(document.querySelectorAll("[data-make]"), function (b) {
b.addEventListener("click", async function () {
hideMsg("msg");
var idx = parseInt(b.getAttribute("data-make"), 10);
// Flush any pending edits in this card before drawing from them.
var card = b.closest(".shot");
var sc = card.querySelector("[data-scene]"), cm = card.querySelector("[data-cam]"),
ph = card.querySelector("[data-photo]");
await api("update_shot", { email: email, project_id: pid, index: idx,
scene: sc ? sc.value : undefined, camera: cm ? cm.value : undefined,
photo_id: ph ? ph.value : undefined });
b.disabled = true; b.textContent = "Drawing…";
var r = await api("make_shot_image", { email: email, project_id: pid, index: idx });
if (!r.ok) {
b.disabled = false; b.textContent = "Try again";
showMsg("msg", r.error || "Could not make that frame.");
return;
}
// Drawing takes about half a minute and runs on the server, so the call
// above returns straight away. Watch the shot until the frame appears.
var waited = 0;
var tick = setInterval(async function () {
waited += 4;
var g = await api("get_project", { email: email, project_id: pid });
var seg = (g.ok && g.script && g.script.segments || []).filter(function (x) {
return parseInt(x.index, 10) === idx; })[0];
if (seg && seg.draw_error) {
clearInterval(tick);
b.disabled = false; b.textContent = "Try again";
showMsg("msg", "That frame did not draw: " + seg.draw_error);
return;
}
if (seg && seg.plate_url) { clearInterval(tick); load(); return; }
if (waited > 120) {
clearInterval(tick);
b.disabled = false; b.textContent = "Try again";
showMsg("msg", "That frame is taking unusually long. Try again in a moment.");
}
}, 4000);
});
});
bindBottom();
}
async function bindFilm(p) {
var bal = document.getElementById("balsec");
if (bal) {
var b = await api("get_balance", { email: email });
var have = (b.ok && b.balance_seconds) || 0;
var need = (p.segments || 4) * 8;
bal.textContent = have + "s left" + (have < need ? " — not enough" : "");
if (have < need) bal.style.color = "var(--bad)";
}
// While it films, keep the page honest about where it has got to.
if (p.status === "queued_to_film" || p.status === "filming") {
var t = setInterval(async function () {
var g = await api("get_project", { email: email, project_id: pid });
if (!g.ok) return;
var st = g.project.status;
var note = document.getElementById("filmnote");
if (note) note.textContent = g.project.film_note || st;
if (st === "ready" || st === "film_failed") { clearInterval(t); load(); }
}, 8000);
}
var fb = document.getElementById("film");
if (fb) fb.onclick = async function () {
hideMsg("msg");
fb.disabled = true; fb.textContent = "Starting…";
var r = await api("request_render", { email: email, project_id: pid });
if (!r.ok) {
fb.disabled = false; fb.textContent = "Film it";
showMsg("msg", r.error || "Could not start filming.");
return;
}
showMsg("msg", r.status_text || "Filming.", "ok");
load();
};
}
function bindPost() {
var sb = document.getElementById("seams");
if (sb) sb.onclick = async function () {
sb.disabled = true; sb.textContent = "Measuring…";
var r = await api("seam_report", { email: email, project_id: pid });
sb.disabled = false; sb.textContent = "Measure the joins";
var el = document.getElementById("seamout");
if (!r.ok) { showMsg("msg", r.error || "Could not measure."); return; }
if (!r.seams || !r.seams.length) {
el.innerHTML = '<p class="foot">' + esc(r.status_text || "Nothing to measure.") + '</p>';
return;
}
el.innerHTML = '<p class="foot" style="margin:0 0 8px">' + esc(r.status_text || "") + '</p>'
+ r.seams.map(function (x) {
var col = x.verdict === "smooth" ? "var(--good)"
: (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)");
return '<div class="foot" style="padding:6px 0;border-top:1px solid var(--line)">'
+ 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · '
+ 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · '
+ '<span style="color:' + col + '">' + esc(x.verdict) + '</span>'
+ (x.recommend_broll ? ' · worth a cutaway' : '')
+ '</div>';
}).join("");
};
var cb = document.getElementById("cut");
if (cb) cb.onclick = async function () {
hideMsg("msg");
cb.disabled = true; cb.textContent = "Cutting…";
var r = await api("render_cut", { email: email, project_id: pid });
if (!r.ok) { cb.disabled = false; cb.textContent = "Cut the B-roll in"; showMsg("msg", r.error || "Could not cut."); return; }
var waited = 0;
var t = setInterval(async function () {
waited += 5;
var g = await api("get_project", { email: email, project_id: pid });
if (g.ok && g.project && g.project.cut_url) { clearInterval(t); load(); return; }
if (waited > 300) {
clearInterval(t);
cb.disabled = false; cb.textContent = "Cut the B-roll in";
showMsg("msg", "That is taking unusually long. Try again shortly.");
}
}, 5000);
};
}
function bindBottom() {
var rv = document.getElementById("revise");
if (rv) rv.onclick = async function () {
hideMsg("msg");
var fb = document.getElementById("fb").value.trim();
if (fb.length < 5) { showMsg("msg", "Tell us what you would like changed."); return; }
rv.disabled = true; rv.textContent = "Sending…";
var res = await api("revise_script", { email: email, project_id: pid, feedback: fb });
if (!res.ok) { rv.disabled = false; rv.textContent = "Rewrite the words"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", "Rewriting now…", "ok");
load();
};
var ap = document.getElementById("approve");
if (ap) ap.onclick = async function () {
hideMsg("msg");
ap.disabled = true; ap.textContent = "Approving…";
var res = await api("approve_script", { email: email, project_id: pid });
if (!res.ok) { ap.disabled = false; ap.textContent = "Approve and lock it"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", res.status_text || "Approved.", "ok");
load();
};
}
async function load() {
if (!pid || !email) return;
if (!PRESETS.length) {
var c = await api("camera_presets", {});
if (c.ok) PRESETS = c.presets || [];
}
var r = await api("get_project", { email: email, project_id: pid });
if (!r.ok) { showMsg("msg", r.error || "Could not load that script."); return; }
if (!PHOTOS.length && r.project && r.project.pet_id) {
var ph = await api("list_photos", { email: email, pet_id: r.project.pet_id });
if (ph.ok) PHOTOS = ph.photos || [];
}
render(r);
}
load();
</script>
</body>
</html>