script: rebuild part 4 of 6
This commit is contained in:
parent
0dabc60006
commit
7ce21f6621
1 changed files with 193 additions and 0 deletions
193
script.html
193
script.html
|
|
@ -803,4 +803,197 @@ function tlBind() {
|
||||||
if (drag.mode === "seek") { seekTo(timeAt(e.clientX)); return; }
|
if (drag.mode === "seek") { seekTo(timeAt(e.clientX)); return; }
|
||||||
var dur = EDL.duration || 1, w = tl.clientWidth || 1;
|
var dur = EDL.duration || 1, w = tl.clientWidth || 1;
|
||||||
var d = ((e.clientX - drag.x0) / w) * dur;
|
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 = '<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 ? ' · ' + esc(v.voice_name) : '')
|
||||||
|
+ (v.cost_usd ? ' · $' + 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">▶ Play the run-through</button>'
|
||||||
|
+ '<span class="foot" style="margin-left:12px" id="pzwhere">'
|
||||||
|
+ PZ.length + ' lines · ' + 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-->
|
<!--PTLCHUNK-->
|
||||||
Loading…
Reference in a new issue