script: rebuild part 4 of 6

This commit is contained in:
gator 2026-09-14 13:33:51 +00:00
parent 0dabc60006
commit 7ce21f6621

View file

@ -803,4 +803,197 @@ function tlBind() {
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 = "&#9654; 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 = "&#9646;&#9646; 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 = '<h3 class="sec" style="font-size:14px;margin:0 0 10px">Earlier takes</h3>'
+ '<p class="foot" style="margin:0 0 12px">' + esc(r.status_text || "") + ' '
+ 'Bringing one back keeps the one you are leaving.</p>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px">'
+ r.versions.map(function (v, i) {
return '<div>'
+ '<video src="' + esc(v.cut_url || v.video_url) + '" controls playsinline preload="metadata" '
+ 'style="width:100%;aspect-ratio:9/16;border-radius:9px;border:1px solid var(--line);background:#000;display:block"></video>'
+ '<div class="foot" style="margin-top:6px">' + esc(when(v.at) || "earlier")
+ (v.voice_name ? ' &middot; ' + esc(v.voice_name) : '')
+ (v.cost_usd ? ' &middot; &#36;' + Number(v.cost_usd).toFixed(2) : '') + '</div>'
+ (v.note ? '<div class="foot" style="opacity:.7">' + esc(v.note) + '</div>' : '')
+ '<button type="button" class="btn ghost" data-restore="' + i + '" '
+ 'style="margin-top:8px;width:100%">Use this one</button>'
+ '</div>';
}).join("")
+ '</div>';
[].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 = '<div class="stage" style="margin-bottom:12px">'
+ '<img id="pzimg" alt="opening still" style="width:100%;aspect-ratio:9/16;'
+ 'border-radius:10px;background:#000;object-fit:cover;display:block">'
+ '</div>'
+ '<audio id="pzaud"></audio>'
+ '<button type="button" class="btn" id="pzplay">&#9654; Play the run-through</button>'
+ '<span class="foot" style="margin-left:12px" id="pzwhere">'
+ PZ.length + ' lines &middot; ' + total.toFixed(1) + 's spoken, about '
+ filmed.toFixed(1) + 's once filmed, against ' + bought + 's of shots</span>'
+ (filmed > bought
? '<div class="warn" style="margin-top:12px">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.</div>'
: (filmed < bought * 0.88
? '<div class="warn" style="margin-top:12px">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.</div>'
: ''))
+ (over.length
? '<div class="warn" style="margin-top:12px">Shot' + (over.length > 1 ? 's ' : ' ')
<!--PTLCHUNK-->