diff --git a/script.html b/script.html index d108c45..a69b320 100644 --- a/script.html +++ b/script.html @@ -83,9 +83,9 @@ navBar("videos"); var email = requireSession(); var pid = new URLSearchParams(location.search).get("id"); var poll = null, PRESETS = [], PHOTOS = [], DATA = null; -// Shot length is a property of the engine the project will be filmed on, not -// a constant. Veo shoots 8s, Omni 1.1 shoots 10s, so every duration and every -// price derived from "segments" has to ask which engine first. +// Shot length is a property of the engine the project will be filmed on, not a +// constant. Veo shoots 8s, Omni 1.1 shoots 10s, so every duration and price +// derived from "segments" has to ask which engine first. var MODELS = []; function modelFor(p) { var id = (p && p.render_model) || ""; @@ -249,7 +249,9 @@ function render(r) { ? '
Worth a look: ' + s._warnings.map(esc).join(" · ") + '
' : ""; var head = '
' - + '
+ + '
$' + Number(p.price_usd || 0).toFixed(2) + '
' + + '
' + videoSecondsOf(p) + ' seconds · ' + (p.segments || 4) + ' shots' + + (modelFor(p) ? ' · ' + esc(modelFor(p).label) : '') + '
' + '
' + (canEditScript ? left + " rewrite" + (left === 1 ? "" : "s") + " left" : "Words approved") + '
'; @@ -392,2140 +394,4 @@ function render(r) { + 'go straight to filming.

' + '

Hear it first

' + '
' - + '

Filming costs real money; speaking the ' - + 'words costs about a hundredth of a cent. Run it through in your pet’s voice ' - + 'over the opening stills first, and only film once the timing and the jokes land.

' - + '' - + '' - + '' - + '

Cutting to the clock speaks the whole piece once, ' - + 'measures where every word actually falls, and moves the shot boundaries to real word ' - + 'endings. It rewrites how the lines are divided — not the words themselves.

' - + '
' - + '
' - + '
' - + '

The shots

' - + s.segments.map(function (x) { return shotHtml(x, false); }).join("") - + own - + film; - } else if (VIEW === 3) { - body = head + film; - } else { - body = head + post + film; - } - - out.innerHTML = cams + stepper(stage, VIEW) + body; - - wire(locked); -} - -var PICS = [], SUGGESTED = []; - -function renderPics() { - var el = document.getElementById("pics"); - if (!el) return; - el.innerHTML = PICS.map(function (p) { - return '
' - + '' + esc(p.description || ' - + '' - + '' - + '
'; - }).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 }); - if (!r.ok) { sg.disabled = false; sg.textContent = "Suggest where they fit"; showMsg("msg", r.error || "Could not suggest."); return; } - // Reading the script is a model call, far too slow to answer inside one - // request, so it runs in the background and we wait for it here. - if (r.pending) { - document.getElementById("sugnote").textContent = r.status_text || "Reading the script…"; - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("suggest_image_placements", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 150) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - 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 '
shot ' + x.index + ' · ' - + esc((p.description || "").slice(0, 60)) + ' — ' + esc(x.why || "") + '
'; - }).join("") - + '' - : '

No picture fitted any shot well enough to use.

'; - 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) { - [].forEach.call(document.querySelectorAll("[data-stage]"), function (b) { - b.onclick = function () { - var want = parseInt(b.getAttribute("data-stage"), 10); - if (want === VIEW) return; - VIEW = want; - hideMsg("msg"); - render(DATA); - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - }); - bindPics(); - bindPreviz(); - bindAlign(); - 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 = videoSecondsOf(p); - 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(); - }; -} - -// ---------------------------------------------------------------- the cut -// EDL is {duration, cuts:[{shot,start,seconds,kind,src,in,moved}], segments}. -// Only the cut positions are editable; the server composes the rest, so the -// page never has to be trusted about source urls or trim points. -var EDL = null, DIRTY = false; - -function tlDraw() { - var tl = document.getElementById("tl"); - if (!tl || !EDL) return; - var dur = EDL.duration || 1; - - // Above the bar: where each shot begins, with the frame it opens on. The end - // of a shot is always the start of the next one, so only starts are marked. - var strip = document.getElementById("tlshots"); - if (strip) { - var segs = ((DATA && DATA.script && DATA.script.segments) || []); - strip.innerHTML = segs.map(function (s, k) { - var at = k * 8, wide = (8 / dur) * 100; - return '
' - + 'shot ' + s.index - + (s.plate_url ? 'shot ' + s.index + ' opening frame' : '') - + '
'; - }).join(""); - } - - var html = ""; - (EDL.cuts || []).forEach(function (c, i) { - var left = (c.start / dur) * 100, wide = (c.seconds / dur) * 100; - // Show the actual picture. A plain rectangle says nothing about what is - // going to be on screen, which is the only thing worth judging here. - var thumb = c.kind === "video" - ? '' - : ''; - html += '
' - + thumb + '' + c.seconds + 's' - + '
'; - }); - html += '
'; - html += '
'; - for (var t = 0; t <= dur; t += 8) { - // The end labels are centred on the track edges, so half of each falls - // outside it. Pin the first left and the last right instead. - var pos = (t / dur) * 100; - var style = t === 0 ? 'left:2px;transform:none' - : (t >= dur ? 'right:2px;left:auto;transform:none' - : 'left:' + pos.toFixed(2) + '%'); - html += '' + t + 's'; - } - html += '
'; - tl.innerHTML = html; - var n = (EDL.cuts || []).length; - document.getElementById("tlinfo").textContent = n - ? n + " cutaway" + (n === 1 ? "" : "s") + " over " + dur + " seconds. Drag to move one, or pull its right edge to change how long it runs. The voice underneath never stops." - : "No cutaways on this one, so the talking head runs the whole way."; - document.getElementById("edlsave").disabled = !DIRTY; -} - -// Move the picture to a moment without disturbing the edit. -function seekTo(t) { - var v = document.getElementById("pvid"); - if (!v || !EDL) return; - v.currentTime = Math.max(0, Math.min(t, EDL.duration || 0)); - v.dispatchEvent(new Event("timeupdate")); -} - -function tlBind() { - var tl = document.getElementById("tl"); - if (!tl) return; - var drag = null; - - var strip = document.getElementById("tlshots"); - if (strip) strip.addEventListener("click", function (e) { - var s = e.target.closest("[data-seek]"); - if (s) seekTo(parseFloat(s.getAttribute("data-seek"))); - }); - - function timeAt(clientX) { - var r = tl.getBoundingClientRect(); - return ((clientX - r.left) / (r.width || 1)) * (EDL.duration || 0); - } - - tl.addEventListener("pointerdown", function (e) { - var grip = e.target.closest("[data-grip]"); - var block = e.target.closest("[data-i]"); - if (!block) { - // Empty track: scrub. This is how you get to a particular moment. - drag = { mode: "seek" }; - seekTo(timeAt(e.clientX)); - tl.setPointerCapture(e.pointerId); - e.preventDefault(); - return; - } - var i = parseInt((grip || block).getAttribute(grip ? "data-grip" : "data-i"), 10); - drag = { i: i, mode: grip ? "len" : "move", x0: e.clientX, - start0: EDL.cuts[i].start, secs0: EDL.cuts[i].seconds }; - tl.setPointerCapture(e.pointerId); - e.preventDefault(); - }); - - tl.addEventListener("pointermove", function (e) { - if (!drag) return; - if (drag.mode === "seek") { seekTo(timeAt(e.clientX)); return; } - var dur = EDL.duration || 1, w = tl.clientWidth || 1; - var d = ((e.clientX - drag.x0) / w) * dur; - var c = EDL.cuts[drag.i]; - if (drag.mode === "move") { - c.start = Math.max(0, Math.min(drag.start0 + d, dur - c.seconds)); - } else { - c.seconds = Math.max(0.8, Math.min(drag.secs0 + d, Math.min(20, dur - c.start))); - } - c.start = Math.round(c.start * 20) / 20; // quarter-frame is plenty - c.seconds = Math.round(c.seconds * 20) / 20; - c.moved = true; DIRTY = true; - tlDraw(); - }); - - function end(e) { - if (!drag) return; - drag = null; - try { tl.releasePointerCapture(e.pointerId); } catch (x) {} - } - tl.addEventListener("pointerup", end); - tl.addEventListener("pointercancel", end); -} - -// The preview never renders anything. The talking head plays with its audio -// throughout and the picture is covered while a cutaway is due, which is -// exactly what the finished cut does. -function previewBind(p) { - var v = document.getElementById("pvid"); - if (!v) return; - if (p.video_url) v.src = p.video_url; - var ovl = document.getElementById("povl"), - oi = document.getElementById("povlimg"), - ov = document.getElementById("povlvid"); - - function activeCut(t) { - var cs = (EDL && EDL.cuts) || []; - for (var i = 0; i < cs.length; i++) { - if (t >= cs[i].start && t < cs[i].start + cs[i].seconds) return cs[i]; - } - return null; - } - - function resetPlayButton() { - var b = document.getElementById("pplay"); - if (b) b.innerHTML = "▶ Preview the cut"; - } - // Playing to the end used to leave the button reading "Pause" for good. - v.addEventListener("ended", resetPlayButton); - v.addEventListener("pause", resetPlayButton); - v.addEventListener("play", function () { - var b = document.getElementById("pplay"); - if (b) b.innerHTML = "▮▮ Pause"; - }); - - var showing = null; - v.addEventListener("timeupdate", function () { - var head = document.getElementById("tlhead"); - if (head && EDL) head.style.left = ((v.currentTime / (EDL.duration || 1)) * 100) + "%"; - var tm = document.getElementById("ptime"); - if (tm && EDL) tm.textContent = v.currentTime.toFixed(1) + " / " + (EDL.duration || 0).toFixed(1) + "s"; - var c = activeCut(v.currentTime); - if (c === showing) return; - showing = c; - if (!c) { ovl.style.display = "none"; ov.pause(); return; } - ovl.style.display = "block"; - if (c.kind === "video") { - oi.style.display = "none"; ov.style.display = "block"; - if (ov.src !== c.src) ov.src = c.src; - try { ov.currentTime = c["in"] || 0; } catch (x) {} - ov.play().catch(function () {}); - } else { - ov.style.display = "none"; ov.pause(); - oi.style.display = "block"; oi.src = c.src; - } - }); - - document.getElementById("pplay").onclick = function () { - // Starting again from the end should start again, not sit there. - if (v.ended || v.currentTime >= (EDL.duration || 0) - 0.05) v.currentTime = 0; - if (v.paused) v.play(); else v.pause(); - }; -} - -async function loadEdl(p) { - var r = await api("preview_edl", { email: email, project_id: pid }); - if (!r.ok) return; - EDL = r.edl; DIRTY = false; - tlDraw(); tlBind(); previewBind(p); - - document.getElementById("edlsave").onclick = async function () { - var b = this; b.disabled = true; b.textContent = "Saving…"; - var s = await api("save_edl_cuts", { email: email, project_id: pid, - cuts: (EDL.cuts || []).map(function (c) { - return { shot: c.shot, start: c.start, seconds: c.seconds }; }) }); - b.textContent = "Save these positions"; - if (!s.ok) { b.disabled = false; showMsg("msg", s.error || "Those positions were refused."); return; } - hideMsg("msg"); - EDL = s.edl; DIRTY = false; tlDraw(); - showMsg("msg", s.status_text || "Saved.", "ok"); - }; - - document.getElementById("edlreset").onclick = async function () { - var s = await api("reset_edl_cuts", { email: email, project_id: pid }); - if (!s.ok) { showMsg("msg", s.error || "Could not reset."); return; } - EDL = s.edl; DIRTY = false; tlDraw(); - showMsg("msg", s.status_text || "Back to the suggested cut.", "ok"); - }; -} - -// Every film keeps the take it replaced. Without this, a re-film to fix one -// thing could quietly make another worse with no way back. -async function loadTakes() { - var el = document.getElementById("takes"); - if (!el) return; - var r = await api("list_versions", { email: email, project_id: pid }); - if (!r.ok || !(r.versions || []).length) { el.innerHTML = ""; return; } - el.innerHTML = '

Earlier takes

' - + '

' + esc(r.status_text || "") + ' ' - + 'Bringing one back keeps the one you are leaving.

' - + '
' - + r.versions.map(function (v, i) { - return '
' - + '' - + '
' + esc(when(v.at) || "earlier") - + (v.voice_name ? ' · ' + esc(v.voice_name) : '') - + (v.cost_usd ? ' · $' + Number(v.cost_usd).toFixed(2) : '') + '
' - + (v.note ? '
' + esc(v.note) + '
' : '') - + '' - + '
'; - }).join("") - + '
'; - [].forEach.call(el.querySelectorAll("[data-restore]"), function (b) { - b.onclick = async function () { - if (!confirm("Bring that take back as the current video? The one you are leaving is kept.")) return; - b.disabled = true; b.textContent = "Restoring…"; - var s = await api("restore_version", { email: email, project_id: pid, - index: parseInt(b.getAttribute("data-restore"), 10) }); - if (!s.ok) { b.disabled = false; b.textContent = "Use this one"; showMsg("msg", s.error || "Could not restore that."); return; } - showMsg("msg", s.status_text || "Restored.", "ok"); - load(); - }; - }); -} - -// The run-through: each opening still held for exactly as long as its line -// takes to say. No video is generated, so this costs nothing to watch again. -var PZ = null, PZTIMER = null; - -function pzPaint(lines) { - var el = document.getElementById("pzout"); - if (!el) return; - var segs = ((DATA && DATA.script && DATA.script.segments) || []); - var byIdx = {}; - (lines || []).forEach(function (l) { byIdx[l.index] = l; }); - PZ = segs.map(function (s) { - var l = byIdx[s.index] || {}; - return { index: s.index, url: l.url || "", seconds: l.seconds || 0, - over: !!l.over, plate: s.plate_url || "", line: s.line || "" }; - }).filter(function (x) { return x.url; }); - - if (!PZ.length) { el.innerHTML = ""; return; } - var total = PZ.reduce(function (a, b) { return a + (b.seconds || 0); }, 0); - var bought = segs.length * 8; - // Spoken here, but filmed by Veo, which was asked to over-enunciate so the - // mouth is readable and therefore runs slower. Measured on a real clip: - // 17 words took 7.02s filmed against about 5.7s spoken, so ~1.25x. - var FILMED = 1.25; - var filmed = total * FILMED; - var over = PZ.filter(function (x) { return (x.seconds || 0) * FILMED > 8.0; }); - - el.innerHTML = '
' - + 'opening still' - + '
' - + '' - + '' - + '' - + PZ.length + ' lines · ' + total.toFixed(1) + 's spoken, about ' - + filmed.toFixed(1) + 's once filmed, against ' + bought + 's of shots' - + (filmed > bought - ? '
Filmed, this would run about ' - + filmed.toFixed(1) + ' seconds but only ' + bought + ' seconds are being ' - + 'shot. Something will be cut off. Shorten the words, or buy more length.
' - : (filmed < bought * 0.88 - ? '
Filmed, this would run about ' - + filmed.toFixed(1) + ' seconds across ' + bought + ' seconds of video, ' - + 'leaving roughly ' + ((bought - filmed) / PZ.length).toFixed(1) + ' seconds ' - + 'of silence at the end of every shot. That is where trailing sound effects ' - + 'and echo come from: the model is handed a gap and fills it. More words per ' - + 'shot is the fix.
' - : '')) - + (over.length - ? '
Shot' + (over.length > 1 ? 's ' : ' ') - + over.map(function (x) { return x.index; }).join(", ") - + ' will not fit in eight seconds once filmed: ' - + over.map(function (x) { return x.index + " would run " + (x.seconds * FILMED).toFixed(1) + "s"; }).join(", ") - + '. Trim those lines before filming.
' - : '') - + '
' - + PZ.map(function (x) { - var f = (x.seconds || 0) * FILMED; - return '
' - + 'shot ' + x.index + ' · ' + x.seconds.toFixed(1) + 's spoken, ~' - + f.toFixed(1) + 's filmed' - + (f > 8 ? ' over' : '') - + ' · ' + esc(x.line.slice(0, 60)) + '
'; - }).join("") - + '
'; - - var img = document.getElementById("pzimg"), - aud = document.getElementById("pzaud"), - btn = document.getElementById("pzplay"), - where = document.getElementById("pzwhere"); - - function stop() { - aud.pause(); - if (PZTIMER) { clearTimeout(PZTIMER); PZTIMER = null; } - btn.innerHTML = "▶ Play the run-through"; - } - - function playFrom(i) { - if (i >= PZ.length) { stop(); return; } - var x = PZ[i]; - img.src = x.plate || ""; - where.textContent = "shot " + x.index + " of " + PZ.length + " · " + x.seconds.toFixed(1) + "s"; - aud.src = x.url; - aud.onended = function () { playFrom(i + 1); }; - aud.play().catch(function () { stop(); }); - } - - btn.onclick = function () { - if (!aud.paused) { stop(); return; } - btn.innerHTML = "▮▮ Stop"; - playFrom(0); - }; -} - -function bindAlign() { - var b = document.getElementById("alignbtn"); - if (!b) return; - var out = document.getElementById("alignout"); - - function paint(r) { - var bought = ((DATA && DATA.script && DATA.script.segments) || []).length * 8; - var filmed = r.filmed || 0; - out.innerHTML = '

Measured: ' - + (r.spoken || 0).toFixed(1) + 's spoken, about ' + filmed.toFixed(1) - + 's filmed, against ' + bought + 's of shots.

' - + (r.audio ? '' : '') - + (r.shots || []).map(function (s) { - var fit = Math.abs((s.filmed || 0) - 8) <= 1.2; - return '
' - + 'shot ' + s.index + ' · ' + (s.start || 0).toFixed(1) + '-' - + (s.end || 0).toFixed(1) + 's spoken · ~' + (s.filmed || 0).toFixed(1) + 's filmed' - + (fit ? '' : ' off the clock') - + '
' + esc(s.line || "") + '
'; - }).join("") - + (filmed < bought - 1.5 - ? '
The whole piece only runs about ' - + filmed.toFixed(1) + ' seconds filmed, but ' + bought + ' seconds are being shot. ' - + 'No arrangement of these words fills the video — it needs more written, or a ' - + 'shorter length.
' - : '') - + (filmed > bought + 1.5 - ? '
The whole piece runs about ' - + filmed.toFixed(1) + ' seconds filmed against ' + bought + ' seconds of video, so the ' - + 'end would be cut off. Trim it, or buy more length.
' - : ''); - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Measuring…"; - var r = await api("align_script", { email: email, project_id: pid, refresh: true }); - if (r.ok && r.pending) { - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("align_script", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 240) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - b.disabled = false; b.textContent = "Cut it to the clock"; - if (!r.ok) { showMsg("msg", r.error || "Could not measure that."); return; } - if (r.note) { showMsg("msg", r.note); return; } - paint(r); - showMsg("msg", "Shots re-cut at real word endings.", "ok"); - load(); - }; - - api("align_script", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && r.audio) paint(r); - }); -} - -function bindPreviz() { - var b = document.getElementById("pzmake"); - if (!b) return; - var note = document.getElementById("pznote"); - - async function poll() { - var waited = 0; - return await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("make_previz", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (g.ok && g.pending && note) note.textContent = g.status_text || ""; - if (waited > 240) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Speaking…"; - var r = await api("make_previz", { email: email, project_id: pid }); - if (r.ok && r.pending) { note.textContent = r.status_text || ""; r = await poll(); } - b.disabled = false; b.textContent = "Speak it again"; - if (!r.ok) { note.textContent = ""; showMsg("msg", r.error || "Could not speak that."); return; } - note.textContent = r.status_text || ""; - pzPaint(r.lines || []); - }; - - // Already spoken? Show it without asking again. - api("make_previz", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && (r.lines || []).length) { - b.textContent = "Speak it again"; - note.textContent = r.status_text || ""; - pzPaint(r.lines); - } - }); -} - -// The picture is the hook; the link carries the video. Never the video itself -// over MMS - carriers transcode it into mush. -function bindShare() { - var b = document.getElementById("sharebtn"); - if (!b) return; - var note = document.getElementById("sharenote"), - out = document.getElementById("shareout"); - - // The file URL ends in /public with no filename. The headers are already - // correct (video/mp4, filename=final.mp4) so anything that reads them is - // fine, but clients that sniff the extension from the URL string are not. - // A filename query is accepted and ignored by the file service. - // Filled in by share_link, which owns the address. A link straight to the - // file would be invisible to us; this one is counted server side. - var SHARE_LINK = ""; - function named(url) { return SHARE_LINK || ""; } - - function paint(r) { - var kb = Math.round((r.bytes || 0) / 1024); - var link = named(r.video || ""); - var msg = ((DATA && DATA.script && DATA.script.title) - ? DATA.script.title + " — " : "") - + ((DATA && DATA.project && DATA.project.pet_name) || "your pet") - + " has something to say. " + link; - out.innerHTML = '
' - + 'share picture' - + '
' - + '

' + esc(r.size || "") + ' · ' + kb + ' KB' - + ' · well inside the 600KB most carriers allow, so it will not be ' - + 'downsampled.

' - + '' - + '' - + '' - + '' - + 'Download the picture' - + '

' + esc(link) - + '

' - + '
'; - api("share_link", { email: email, project_id: pid }).then(function (s) { - if (!(s && s.ok)) return; - SHARE_LINK = s.url || ""; - var el = document.getElementById("viewcount"); - if (el) { - el.innerHTML = '
' + esc(SHARE_LINK) + '
Opened ' + (s.views || 0) - + ' time' + ((s.views === 1) ? '' : 's') + '.'; - } - var t = document.getElementById("sharemsg"); - if (t && SHARE_LINK) t.value = t.value.replace(/https?:\/\/\S+/, SHARE_LINK); - }); - var sl = document.getElementById("sharelink"); - if (sl) sl.onclick = async function () { - var title = (DATA && DATA.script && DATA.script.title) || "A video"; - // The native sheet is the shortest path into a text message on a phone. - if (navigator.share) { - try { await navigator.share({ title: title, text: msg, url: link }); return; } - catch (e) { if (e && e.name === "AbortError") return; } - } - try { - await navigator.clipboard.writeText(link); - sl.textContent = "Link copied"; - setTimeout(function () { sl.textContent = "Share the video"; }, 1600); - } catch (e) { showMsg("msg", "Copy this: " + link); } - }; - var c = document.getElementById("sharecopy"); - if (c) c.onclick = function () { - var t = document.getElementById("sharemsg"); - t.select(); - navigator.clipboard.writeText(t.value).then(function () { - c.textContent = "Copied"; - setTimeout(function () { c.textContent = "Copy the message"; }, 1600); - }).catch(function () { c.textContent = "Select it and copy"; }); - }; - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Choosing a frame…"; - var r = await api("share_image", { email: email, project_id: pid, refresh: true }); - if (r.ok && r.pending) { - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("share_image", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 180) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - b.disabled = false; b.textContent = "Pick another frame"; - if (!r.ok) { showMsg("msg", r.error || "Could not make that picture."); return; } - if (r.note) { showMsg("msg", r.note); return; } - note.textContent = r.status_text || ""; - paint(r); - }; - - api("share_image", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && r.image) { - b.textContent = "Pick another frame"; - note.textContent = r.status_text || ""; - paint(r); - } - }); -} - -function bindPost() { - loadTakes(); - bindShare(); - // The editing bay is opt-in. Most people want to watch the thing and leave. - var eo = document.getElementById("edlopen"); - if (eo) eo.onclick = function () { - var w = document.getElementById("edlwrap"); - var open = w.style.display === "none"; - w.style.display = open ? "" : "none"; - this.textContent = open ? "Close the video editor" : "Open the video editor"; - if (open) { - if (!EDL) loadEdl((DATA && DATA.project) || {}); - else tlDraw(); - w.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }; - var sb = document.getElementById("seams"); - function paintSeams(r) { - var el = document.getElementById("seamout"); - if (!el) return; - el.innerHTML = '

' + esc(r.status_text || "") + '

' - + (r.seams || []).map(function (x) { - var col = x.verdict === "smooth" ? "var(--good)" - : (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)"); - return '
' - + 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · ' - + 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · ' - + '' + esc(x.verdict) + '' - + (x.recommend_broll ? ' · worth a cutaway' : '') - + '
'; - }).join(""); - } - - if (sb) sb.onclick = async function () { - sb.disabled = true; sb.textContent = "Measuring…"; - var el = document.getElementById("seamout"); - var r = await api("seam_report", { email: email, project_id: pid }); - if (!r.ok) { - sb.disabled = false; sb.textContent = "Measure the joins"; - showMsg("msg", r.error || "Could not measure."); - return; - } - // Measuring downloads every shot and searches each join for the best - // alignment, which is far too slow to answer inside one request. - if (r.pending) { - el.innerHTML = '

' + esc(r.status_text || "Measuring…") + '

'; - var waited = 0; - var t = setInterval(async function () { - waited += 4; - var g = await api("seam_report", { email: email, project_id: pid }); - if (g.ok && !g.pending) { - clearInterval(t); - sb.disabled = false; sb.textContent = "Measure the joins"; - paintSeams(g); - return; - } - if (waited > 180) { - clearInterval(t); - sb.disabled = false; sb.textContent = "Measure the joins"; - showMsg("msg", "Measuring is taking unusually long."); - } - }, 4000); - return; - } - sb.disabled = false; sb.textContent = "Measure the joins"; - if (!r.seams || !r.seams.length) { - el.innerHTML = '

' + esc(r.status_text || "Nothing to measure.") + '

'; - return; - } - el.innerHTML = '

' + esc(r.status_text || "") + '

' - + r.seams.map(function (x) { - var col = x.verdict === "smooth" ? "var(--good)" - : (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)"); - return '
' - + 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · ' - + 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · ' - + '' + esc(x.verdict) + '' - + (x.recommend_broll ? ' · worth a cutaway' : '') - + '
'; - }).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 || []; MODELS = c.models || []; } - } - 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(); - - - - + Number(p.price_usd || 0).toFixed(2) + '
' - + '
' + videoSecondsOf(p) + ' seconds · ' + (p.segments || 4) + ' shots' - + (modelFor(p) ? ' · ' + esc(modelFor(p).label) : '') + '
' - + '
' + (canEditScript - ? left + " rewrite" + (left === 1 ? "" : "s") + " left" - : "Words approved") + '
'; - - var full = s.full_text - ? '

The whole piece

' + esc(s.full_text) + '
' : ""; - - var cams = '' + PRESETS.map(function (c) { - return ''; - - // 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 ? '' : - '

Your own pictures

' - + '
' - + '

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.

' - + '
Paste an image, drop one here, or click to choose
' - + '' - + '
' - + '
' - + '' - + '
' - + '
' - + '
'; - - // 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' - || p.status === 'ready') { - var busy = (p.status === 'queued_to_film' || p.status === 'filming'); - // The opening frame is what carries the animal's likeness into the video. - // Without one the model works from the written description and invents a - // plausible stranger, so say so before any money is spent. - var noFrame = (s.segments || []).filter(function (x) { - return x.source !== 'previous' && !x.plate_url; - }).map(function (x) { return x.index; }); - film = '

Film it

' - + (noFrame.length && !busy - ? '
Shot' + (noFrame.length > 1 ? 's ' : ' ') + noFrame.join(', ') - + ' still ' + (noFrame.length > 1 ? 'have' : 'has') + ' no opening frame. ' - + 'That frame is what carries your pet’s actual likeness — their collar, ' - + 'their markings, their eyes — into the video. ' - + 'One will be drawn automatically before filming, which adds about half a minute ' - + 'per shot. Make them yourself above if you want to see them first.
' - : '') - + '
' - + '
' + ((p.segments || 4) * 8) + 's
' - + '
of your time credit
' - + '
checking your balance…
' - + (p.status === 'film_failed' - ? '
That did not finish: ' + esc(p.film_note || 'unknown reason') - + '. Your seconds were returned.
' - : '') - + (busy - ? '

' + esc(p.film_note || 'Queued…') + '

' - : '' - + 'About a minute a shot. ' - + 'Anything still missing — an opening still, a cutaway — is made ' - + 'automatically first, so nothing above is required.' - + (p.status === 'ready' - ? '

This has already been filmed. ' - + 'Filming again spends the seconds again and replaces the video.

' - : '')) - + '
'; - } - - var post = ''; - if (p.status === 'ready' || p.cut_url) { - post = '

The cut

' - + (p.cut_url - ? '' - : '') - + '

Cutaways are laid across the joins between shots, ' - + 'so the picture changes while the voice keeps going and the join is never on screen.

' - + '' - + '' - + '' - + '' - + '
' - + '
' - + '
' - + '

Send it to someone

' - + '
' - + '

Texting the video itself puts it through ' - + 'carrier transcoding and it arrives looking worse than it left. Send the picture as ' - + 'the hook and let the link carry the video at full quality.

' - + '' - + '' - + '
' - + '
'; - } - - var actions = !canEditScript - ? '

These words are settled. ' - + 'Everything after this arranges how they are shot, so the writing is left alone.

' - : '
' - + '

Change the words

' - + '
' - + 'Be specific. “Too polite, he would be smugger” beats “make it funnier”. ' - + 'Setting and camera you can just edit directly above.' - + '
' - + '' - + '' - + '
'; - - var body; - if (VIEW === 1) { - body = head + warn + full - + '

The words, shot by shot

' - + s.segments.map(linesHtml).join("") - + actions; - } else if (VIEW === 2) { - body = head - + '

How it will be laid out. ' - + 'Each shot has a setting, a camera move and an opening frame that carries your ' - + 'pet’s likeness. Cutaways sit across the joins so the cut is never on screen. ' - + 'All of it has been chosen for you — change anything you disagree with, or ' - + 'go straight to filming.

' - + '

Hear it first

' - + '
' - + '

Filming costs real money; speaking the ' - + 'words costs about a hundredth of a cent. Run it through in your pet’s voice ' - + 'over the opening stills first, and only film once the timing and the jokes land.

' - + '' - + '' - + '' - + '

Cutting to the clock speaks the whole piece once, ' - + 'measures where every word actually falls, and moves the shot boundaries to real word ' - + 'endings. It rewrites how the lines are divided — not the words themselves.

' - + '
' - + '
' - + '
' - + '

The shots

' - + s.segments.map(function (x) { return shotHtml(x, false); }).join("") - + own - + film; - } else if (VIEW === 3) { - body = head + film; - } else { - body = head + post + film; - } - - out.innerHTML = cams + stepper(stage, VIEW) + body; - - wire(locked); -} - -var PICS = [], SUGGESTED = []; - -function renderPics() { - var el = document.getElementById("pics"); - if (!el) return; - el.innerHTML = PICS.map(function (p) { - return '
' - + '' + esc(p.description || ' - + '' - + '' - + '
'; - }).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 }); - if (!r.ok) { sg.disabled = false; sg.textContent = "Suggest where they fit"; showMsg("msg", r.error || "Could not suggest."); return; } - // Reading the script is a model call, far too slow to answer inside one - // request, so it runs in the background and we wait for it here. - if (r.pending) { - document.getElementById("sugnote").textContent = r.status_text || "Reading the script…"; - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("suggest_image_placements", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 150) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - 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 '
shot ' + x.index + ' · ' - + esc((p.description || "").slice(0, 60)) + ' — ' + esc(x.why || "") + '
'; - }).join("") - + '' - : '

No picture fitted any shot well enough to use.

'; - 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) { - [].forEach.call(document.querySelectorAll("[data-stage]"), function (b) { - b.onclick = function () { - var want = parseInt(b.getAttribute("data-stage"), 10); - if (want === VIEW) return; - VIEW = want; - hideMsg("msg"); - render(DATA); - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - }); - bindPics(); - bindPreviz(); - bindAlign(); - 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(); - }; -} - -// ---------------------------------------------------------------- the cut -// EDL is {duration, cuts:[{shot,start,seconds,kind,src,in,moved}], segments}. -// Only the cut positions are editable; the server composes the rest, so the -// page never has to be trusted about source urls or trim points. -var EDL = null, DIRTY = false; - -function tlDraw() { - var tl = document.getElementById("tl"); - if (!tl || !EDL) return; - var dur = EDL.duration || 1; - - // Above the bar: where each shot begins, with the frame it opens on. The end - // of a shot is always the start of the next one, so only starts are marked. - var strip = document.getElementById("tlshots"); - if (strip) { - var segs = ((DATA && DATA.script && DATA.script.segments) || []); - strip.innerHTML = segs.map(function (s, k) { - var at = k * 8, wide = (8 / dur) * 100; - return '
' - + 'shot ' + s.index - + (s.plate_url ? 'shot ' + s.index + ' opening frame' : '') - + '
'; - }).join(""); - } - - var html = ""; - (EDL.cuts || []).forEach(function (c, i) { - var left = (c.start / dur) * 100, wide = (c.seconds / dur) * 100; - // Show the actual picture. A plain rectangle says nothing about what is - // going to be on screen, which is the only thing worth judging here. - var thumb = c.kind === "video" - ? '' - : ''; - html += '
' - + thumb + '' + c.seconds + 's' - + '
'; - }); - html += '
'; - html += '
'; - for (var t = 0; t <= dur; t += 8) { - // The end labels are centred on the track edges, so half of each falls - // outside it. Pin the first left and the last right instead. - var pos = (t / dur) * 100; - var style = t === 0 ? 'left:2px;transform:none' - : (t >= dur ? 'right:2px;left:auto;transform:none' - : 'left:' + pos.toFixed(2) + '%'); - html += '' + t + 's'; - } - html += '
'; - tl.innerHTML = html; - var n = (EDL.cuts || []).length; - document.getElementById("tlinfo").textContent = n - ? n + " cutaway" + (n === 1 ? "" : "s") + " over " + dur + " seconds. Drag to move one, or pull its right edge to change how long it runs. The voice underneath never stops." - : "No cutaways on this one, so the talking head runs the whole way."; - document.getElementById("edlsave").disabled = !DIRTY; -} - -// Move the picture to a moment without disturbing the edit. -function seekTo(t) { - var v = document.getElementById("pvid"); - if (!v || !EDL) return; - v.currentTime = Math.max(0, Math.min(t, EDL.duration || 0)); - v.dispatchEvent(new Event("timeupdate")); -} - -function tlBind() { - var tl = document.getElementById("tl"); - if (!tl) return; - var drag = null; - - var strip = document.getElementById("tlshots"); - if (strip) strip.addEventListener("click", function (e) { - var s = e.target.closest("[data-seek]"); - if (s) seekTo(parseFloat(s.getAttribute("data-seek"))); - }); - - function timeAt(clientX) { - var r = tl.getBoundingClientRect(); - return ((clientX - r.left) / (r.width || 1)) * (EDL.duration || 0); - } - - tl.addEventListener("pointerdown", function (e) { - var grip = e.target.closest("[data-grip]"); - var block = e.target.closest("[data-i]"); - if (!block) { - // Empty track: scrub. This is how you get to a particular moment. - drag = { mode: "seek" }; - seekTo(timeAt(e.clientX)); - tl.setPointerCapture(e.pointerId); - e.preventDefault(); - return; - } - var i = parseInt((grip || block).getAttribute(grip ? "data-grip" : "data-i"), 10); - drag = { i: i, mode: grip ? "len" : "move", x0: e.clientX, - start0: EDL.cuts[i].start, secs0: EDL.cuts[i].seconds }; - tl.setPointerCapture(e.pointerId); - e.preventDefault(); - }); - - tl.addEventListener("pointermove", function (e) { - if (!drag) return; - if (drag.mode === "seek") { seekTo(timeAt(e.clientX)); return; } - var dur = EDL.duration || 1, w = tl.clientWidth || 1; - var d = ((e.clientX - drag.x0) / w) * dur; - var c = EDL.cuts[drag.i]; - if (drag.mode === "move") { - c.start = Math.max(0, Math.min(drag.start0 + d, dur - c.seconds)); - } else { - c.seconds = Math.max(0.8, Math.min(drag.secs0 + d, Math.min(20, dur - c.start))); - } - c.start = Math.round(c.start * 20) / 20; // quarter-frame is plenty - c.seconds = Math.round(c.seconds * 20) / 20; - c.moved = true; DIRTY = true; - tlDraw(); - }); - - function end(e) { - if (!drag) return; - drag = null; - try { tl.releasePointerCapture(e.pointerId); } catch (x) {} - } - tl.addEventListener("pointerup", end); - tl.addEventListener("pointercancel", end); -} - -// The preview never renders anything. The talking head plays with its audio -// throughout and the picture is covered while a cutaway is due, which is -// exactly what the finished cut does. -function previewBind(p) { - var v = document.getElementById("pvid"); - if (!v) return; - if (p.video_url) v.src = p.video_url; - var ovl = document.getElementById("povl"), - oi = document.getElementById("povlimg"), - ov = document.getElementById("povlvid"); - - function activeCut(t) { - var cs = (EDL && EDL.cuts) || []; - for (var i = 0; i < cs.length; i++) { - if (t >= cs[i].start && t < cs[i].start + cs[i].seconds) return cs[i]; - } - return null; - } - - function resetPlayButton() { - var b = document.getElementById("pplay"); - if (b) b.innerHTML = "▶ Preview the cut"; - } - // Playing to the end used to leave the button reading "Pause" for good. - v.addEventListener("ended", resetPlayButton); - v.addEventListener("pause", resetPlayButton); - v.addEventListener("play", function () { - var b = document.getElementById("pplay"); - if (b) b.innerHTML = "▮▮ Pause"; - }); - - var showing = null; - v.addEventListener("timeupdate", function () { - var head = document.getElementById("tlhead"); - if (head && EDL) head.style.left = ((v.currentTime / (EDL.duration || 1)) * 100) + "%"; - var tm = document.getElementById("ptime"); - if (tm && EDL) tm.textContent = v.currentTime.toFixed(1) + " / " + (EDL.duration || 0).toFixed(1) + "s"; - var c = activeCut(v.currentTime); - if (c === showing) return; - showing = c; - if (!c) { ovl.style.display = "none"; ov.pause(); return; } - ovl.style.display = "block"; - if (c.kind === "video") { - oi.style.display = "none"; ov.style.display = "block"; - if (ov.src !== c.src) ov.src = c.src; - try { ov.currentTime = c["in"] || 0; } catch (x) {} - ov.play().catch(function () {}); - } else { - ov.style.display = "none"; ov.pause(); - oi.style.display = "block"; oi.src = c.src; - } - }); - - document.getElementById("pplay").onclick = function () { - // Starting again from the end should start again, not sit there. - if (v.ended || v.currentTime >= (EDL.duration || 0) - 0.05) v.currentTime = 0; - if (v.paused) v.play(); else v.pause(); - }; -} - -async function loadEdl(p) { - var r = await api("preview_edl", { email: email, project_id: pid }); - if (!r.ok) return; - EDL = r.edl; DIRTY = false; - tlDraw(); tlBind(); previewBind(p); - - document.getElementById("edlsave").onclick = async function () { - var b = this; b.disabled = true; b.textContent = "Saving…"; - var s = await api("save_edl_cuts", { email: email, project_id: pid, - cuts: (EDL.cuts || []).map(function (c) { - return { shot: c.shot, start: c.start, seconds: c.seconds }; }) }); - b.textContent = "Save these positions"; - if (!s.ok) { b.disabled = false; showMsg("msg", s.error || "Those positions were refused."); return; } - hideMsg("msg"); - EDL = s.edl; DIRTY = false; tlDraw(); - showMsg("msg", s.status_text || "Saved.", "ok"); - }; - - document.getElementById("edlreset").onclick = async function () { - var s = await api("reset_edl_cuts", { email: email, project_id: pid }); - if (!s.ok) { showMsg("msg", s.error || "Could not reset."); return; } - EDL = s.edl; DIRTY = false; tlDraw(); - showMsg("msg", s.status_text || "Back to the suggested cut.", "ok"); - }; -} - -// Every film keeps the take it replaced. Without this, a re-film to fix one -// thing could quietly make another worse with no way back. -async function loadTakes() { - var el = document.getElementById("takes"); - if (!el) return; - var r = await api("list_versions", { email: email, project_id: pid }); - if (!r.ok || !(r.versions || []).length) { el.innerHTML = ""; return; } - el.innerHTML = '

Earlier takes

' - + '

' + esc(r.status_text || "") + ' ' - + 'Bringing one back keeps the one you are leaving.

' - + '
' - + r.versions.map(function (v, i) { - return '
' - + '' - + '
' + esc(when(v.at) || "earlier") - + (v.voice_name ? ' · ' + esc(v.voice_name) : '') - + (v.cost_usd ? ' · $' + Number(v.cost_usd).toFixed(2) : '') + '
' - + (v.note ? '
' + esc(v.note) + '
' : '') - + '' - + '
'; - }).join("") - + '
'; - [].forEach.call(el.querySelectorAll("[data-restore]"), function (b) { - b.onclick = async function () { - if (!confirm("Bring that take back as the current video? The one you are leaving is kept.")) return; - b.disabled = true; b.textContent = "Restoring…"; - var s = await api("restore_version", { email: email, project_id: pid, - index: parseInt(b.getAttribute("data-restore"), 10) }); - if (!s.ok) { b.disabled = false; b.textContent = "Use this one"; showMsg("msg", s.error || "Could not restore that."); return; } - showMsg("msg", s.status_text || "Restored.", "ok"); - load(); - }; - }); -} - -// The run-through: each opening still held for exactly as long as its line -// takes to say. No video is generated, so this costs nothing to watch again. -var PZ = null, PZTIMER = null; - -function pzPaint(lines) { - var el = document.getElementById("pzout"); - if (!el) return; - var segs = ((DATA && DATA.script && DATA.script.segments) || []); - var byIdx = {}; - (lines || []).forEach(function (l) { byIdx[l.index] = l; }); - PZ = segs.map(function (s) { - var l = byIdx[s.index] || {}; - return { index: s.index, url: l.url || "", seconds: l.seconds || 0, - over: !!l.over, plate: s.plate_url || "", line: s.line || "" }; - }).filter(function (x) { return x.url; }); - - if (!PZ.length) { el.innerHTML = ""; return; } - var total = PZ.reduce(function (a, b) { return a + (b.seconds || 0); }, 0); - var bought = segs.length * 8; - // Spoken here, but filmed by Veo, which was asked to over-enunciate so the - // mouth is readable and therefore runs slower. Measured on a real clip: - // 17 words took 7.02s filmed against about 5.7s spoken, so ~1.25x. - var FILMED = 1.25; - var filmed = total * FILMED; - var over = PZ.filter(function (x) { return (x.seconds || 0) * FILMED > 8.0; }); - - el.innerHTML = '
' - + 'opening still' - + '
' - + '' - + '' - + '' - + PZ.length + ' lines · ' + total.toFixed(1) + 's spoken, about ' - + filmed.toFixed(1) + 's once filmed, against ' + bought + 's of shots' - + (filmed > bought - ? '
Filmed, this would run about ' - + filmed.toFixed(1) + ' seconds but only ' + bought + ' seconds are being ' - + 'shot. Something will be cut off. Shorten the words, or buy more length.
' - : (filmed < bought * 0.88 - ? '
Filmed, this would run about ' - + filmed.toFixed(1) + ' seconds across ' + bought + ' seconds of video, ' - + 'leaving roughly ' + ((bought - filmed) / PZ.length).toFixed(1) + ' seconds ' - + 'of silence at the end of every shot. That is where trailing sound effects ' - + 'and echo come from: the model is handed a gap and fills it. More words per ' - + 'shot is the fix.
' - : '')) - + (over.length - ? '
Shot' + (over.length > 1 ? 's ' : ' ') - + over.map(function (x) { return x.index; }).join(", ") - + ' will not fit in eight seconds once filmed: ' - + over.map(function (x) { return x.index + " would run " + (x.seconds * FILMED).toFixed(1) + "s"; }).join(", ") - + '. Trim those lines before filming.
' - : '') - + '
' - + PZ.map(function (x) { - var f = (x.seconds || 0) * FILMED; - return '
' - + 'shot ' + x.index + ' · ' + x.seconds.toFixed(1) + 's spoken, ~' - + f.toFixed(1) + 's filmed' - + (f > 8 ? ' over' : '') - + ' · ' + esc(x.line.slice(0, 60)) + '
'; - }).join("") - + '
'; - - var img = document.getElementById("pzimg"), - aud = document.getElementById("pzaud"), - btn = document.getElementById("pzplay"), - where = document.getElementById("pzwhere"); - - function stop() { - aud.pause(); - if (PZTIMER) { clearTimeout(PZTIMER); PZTIMER = null; } - btn.innerHTML = "▶ Play the run-through"; - } - - function playFrom(i) { - if (i >= PZ.length) { stop(); return; } - var x = PZ[i]; - img.src = x.plate || ""; - where.textContent = "shot " + x.index + " of " + PZ.length + " · " + x.seconds.toFixed(1) + "s"; - aud.src = x.url; - aud.onended = function () { playFrom(i + 1); }; - aud.play().catch(function () { stop(); }); - } - - btn.onclick = function () { - if (!aud.paused) { stop(); return; } - btn.innerHTML = "▮▮ Stop"; - playFrom(0); - }; -} - -function bindAlign() { - var b = document.getElementById("alignbtn"); - if (!b) return; - var out = document.getElementById("alignout"); - - function paint(r) { - var bought = ((DATA && DATA.script && DATA.script.segments) || []).length * 8; - var filmed = r.filmed || 0; - out.innerHTML = '

Measured: ' - + (r.spoken || 0).toFixed(1) + 's spoken, about ' + filmed.toFixed(1) - + 's filmed, against ' + bought + 's of shots.

' - + (r.audio ? '' : '') - + (r.shots || []).map(function (s) { - var fit = Math.abs((s.filmed || 0) - 8) <= 1.2; - return '
' - + 'shot ' + s.index + ' · ' + (s.start || 0).toFixed(1) + '-' - + (s.end || 0).toFixed(1) + 's spoken · ~' + (s.filmed || 0).toFixed(1) + 's filmed' - + (fit ? '' : ' off the clock') - + '
' + esc(s.line || "") + '
'; - }).join("") - + (filmed < bought - 1.5 - ? '
The whole piece only runs about ' - + filmed.toFixed(1) + ' seconds filmed, but ' + bought + ' seconds are being shot. ' - + 'No arrangement of these words fills the video — it needs more written, or a ' - + 'shorter length.
' - : '') - + (filmed > bought + 1.5 - ? '
The whole piece runs about ' - + filmed.toFixed(1) + ' seconds filmed against ' + bought + ' seconds of video, so the ' - + 'end would be cut off. Trim it, or buy more length.
' - : ''); - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Measuring…"; - var r = await api("align_script", { email: email, project_id: pid, refresh: true }); - if (r.ok && r.pending) { - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("align_script", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 240) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - b.disabled = false; b.textContent = "Cut it to the clock"; - if (!r.ok) { showMsg("msg", r.error || "Could not measure that."); return; } - if (r.note) { showMsg("msg", r.note); return; } - paint(r); - showMsg("msg", "Shots re-cut at real word endings.", "ok"); - load(); - }; - - api("align_script", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && r.audio) paint(r); - }); -} - -function bindPreviz() { - var b = document.getElementById("pzmake"); - if (!b) return; - var note = document.getElementById("pznote"); - - async function poll() { - var waited = 0; - return await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("make_previz", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (g.ok && g.pending && note) note.textContent = g.status_text || ""; - if (waited > 240) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Speaking…"; - var r = await api("make_previz", { email: email, project_id: pid }); - if (r.ok && r.pending) { note.textContent = r.status_text || ""; r = await poll(); } - b.disabled = false; b.textContent = "Speak it again"; - if (!r.ok) { note.textContent = ""; showMsg("msg", r.error || "Could not speak that."); return; } - note.textContent = r.status_text || ""; - pzPaint(r.lines || []); - }; - - // Already spoken? Show it without asking again. - api("make_previz", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && (r.lines || []).length) { - b.textContent = "Speak it again"; - note.textContent = r.status_text || ""; - pzPaint(r.lines); - } - }); -} - -// The picture is the hook; the link carries the video. Never the video itself -// over MMS - carriers transcode it into mush. -function bindShare() { - var b = document.getElementById("sharebtn"); - if (!b) return; - var note = document.getElementById("sharenote"), - out = document.getElementById("shareout"); - - // The file URL ends in /public with no filename. The headers are already - // correct (video/mp4, filename=final.mp4) so anything that reads them is - // fine, but clients that sniff the extension from the URL string are not. - // A filename query is accepted and ignored by the file service. - // Filled in by share_link, which owns the address. A link straight to the - // file would be invisible to us; this one is counted server side. - var SHARE_LINK = ""; - function named(url) { return SHARE_LINK || ""; } - - function paint(r) { - var kb = Math.round((r.bytes || 0) / 1024); - var link = named(r.video || ""); - var msg = ((DATA && DATA.script && DATA.script.title) - ? DATA.script.title + " — " : "") - + ((DATA && DATA.project && DATA.project.pet_name) || "your pet") - + " has something to say. " + link; - out.innerHTML = '
' - + 'share picture' - + '
' - + '

' + esc(r.size || "") + ' · ' + kb + ' KB' - + ' · well inside the 600KB most carriers allow, so it will not be ' - + 'downsampled.

' - + '' - + '' - + '' - + '' - + 'Download the picture' - + '

' + esc(link) - + '

' - + '
'; - api("share_link", { email: email, project_id: pid }).then(function (s) { - if (!(s && s.ok)) return; - SHARE_LINK = s.url || ""; - var el = document.getElementById("viewcount"); - if (el) { - el.innerHTML = '
' + esc(SHARE_LINK) + '
Opened ' + (s.views || 0) - + ' time' + ((s.views === 1) ? '' : 's') + '.'; - } - var t = document.getElementById("sharemsg"); - if (t && SHARE_LINK) t.value = t.value.replace(/https?:\/\/\S+/, SHARE_LINK); - }); - var sl = document.getElementById("sharelink"); - if (sl) sl.onclick = async function () { - var title = (DATA && DATA.script && DATA.script.title) || "A video"; - // The native sheet is the shortest path into a text message on a phone. - if (navigator.share) { - try { await navigator.share({ title: title, text: msg, url: link }); return; } - catch (e) { if (e && e.name === "AbortError") return; } - } - try { - await navigator.clipboard.writeText(link); - sl.textContent = "Link copied"; - setTimeout(function () { sl.textContent = "Share the video"; }, 1600); - } catch (e) { showMsg("msg", "Copy this: " + link); } - }; - var c = document.getElementById("sharecopy"); - if (c) c.onclick = function () { - var t = document.getElementById("sharemsg"); - t.select(); - navigator.clipboard.writeText(t.value).then(function () { - c.textContent = "Copied"; - setTimeout(function () { c.textContent = "Copy the message"; }, 1600); - }).catch(function () { c.textContent = "Select it and copy"; }); - }; - } - - b.onclick = async function () { - hideMsg("msg"); - b.disabled = true; b.textContent = "Choosing a frame…"; - var r = await api("share_image", { email: email, project_id: pid, refresh: true }); - if (r.ok && r.pending) { - var waited = 0; - r = await new Promise(function (done) { - var t = setInterval(async function () { - waited += 4; - var g = await api("share_image", { email: email, project_id: pid }); - if (g.ok && !g.pending) { clearInterval(t); done(g); return; } - if (waited > 180) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } - }, 4000); - }); - } - b.disabled = false; b.textContent = "Pick another frame"; - if (!r.ok) { showMsg("msg", r.error || "Could not make that picture."); return; } - if (r.note) { showMsg("msg", r.note); return; } - note.textContent = r.status_text || ""; - paint(r); - }; - - api("share_image", { email: email, project_id: pid }).then(function (r) { - if (r && r.ok && !r.pending && r.image) { - b.textContent = "Pick another frame"; - note.textContent = r.status_text || ""; - paint(r); - } - }); -} - -function bindPost() { - loadTakes(); - bindShare(); - // The editing bay is opt-in. Most people want to watch the thing and leave. - var eo = document.getElementById("edlopen"); - if (eo) eo.onclick = function () { - var w = document.getElementById("edlwrap"); - var open = w.style.display === "none"; - w.style.display = open ? "" : "none"; - this.textContent = open ? "Close the video editor" : "Open the video editor"; - if (open) { - if (!EDL) loadEdl((DATA && DATA.project) || {}); - else tlDraw(); - w.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }; - var sb = document.getElementById("seams"); - function paintSeams(r) { - var el = document.getElementById("seamout"); - if (!el) return; - el.innerHTML = '

' + esc(r.status_text || "") + '

' - + (r.seams || []).map(function (x) { - var col = x.verdict === "smooth" ? "var(--good)" - : (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)"); - return '
' - + 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · ' - + 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · ' - + '' + esc(x.verdict) + '' - + (x.recommend_broll ? ' · worth a cutaway' : '') - + '
'; - }).join(""); - } - - if (sb) sb.onclick = async function () { - sb.disabled = true; sb.textContent = "Measuring…"; - var el = document.getElementById("seamout"); - var r = await api("seam_report", { email: email, project_id: pid }); - if (!r.ok) { - sb.disabled = false; sb.textContent = "Measure the joins"; - showMsg("msg", r.error || "Could not measure."); - return; - } - // Measuring downloads every shot and searches each join for the best - // alignment, which is far too slow to answer inside one request. - if (r.pending) { - el.innerHTML = '

' + esc(r.status_text || "Measuring…") + '

'; - var waited = 0; - var t = setInterval(async function () { - waited += 4; - var g = await api("seam_report", { email: email, project_id: pid }); - if (g.ok && !g.pending) { - clearInterval(t); - sb.disabled = false; sb.textContent = "Measure the joins"; - paintSeams(g); - return; - } - if (waited > 180) { - clearInterval(t); - sb.disabled = false; sb.textContent = "Measure the joins"; - showMsg("msg", "Measuring is taking unusually long."); - } - }, 4000); - return; - } - sb.disabled = false; sb.textContent = "Measure the joins"; - if (!r.seams || !r.seams.length) { - el.innerHTML = '

' + esc(r.status_text || "Nothing to measure.") + '

'; - return; - } - el.innerHTML = '

' + esc(r.status_text || "") + '

' - + r.seams.map(function (x) { - var col = x.verdict === "smooth" ? "var(--good)" - : (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)"); - return '
' - + 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · ' - + 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · ' - + '' + esc(x.verdict) + '' - + (x.recommend_broll ? ' · worth a cutaway' : '') - + '
'; - }).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 || []; MODELS = c.models || []; } - } - 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(); - - - + + '

Filming costs real money; speaking the ' \ No newline at end of file