Add B-roll cutaways, seam measurement and final cut to the shot editor

This commit is contained in:
gator 2026-09-07 00:09:05 +00:00
parent dba954ad17
commit c0a7f46094

View file

@ -88,6 +88,24 @@ function shotHtml(s, locked) {
: '<label class="mini">Which photo of them</label>' : '<label class="mini">Which photo of them</label>'
+ '<select data-photo="' + s.index + '">' + photoOptions(s.photo_id) + '</select>'; + '<select data-photo="' + s.index + '">' + photoOptions(s.photo_id) + '</select>';
// A cutaway belonging to this shot straddles the cut into the next one, so
// the registration jump happens off screen.
var br = s.broll || {};
var brollBlock = '';
if (br.subject) {
brollBlock = '<div style="margin-top:16px;padding-top:14px;border-top:1px solid var(--line)">'
+ '<label class="mini">Cutaway <span>— shown while this line is spoken, covering the cut</span></label>'
+ '<p class="foot" style="margin:0 0 8px">' + esc(br.subject) + ' · ' + esc(br.kind || 'image') + '</p>'
+ (s.broll_url
? (s.broll_kind === 'video'
? '<video src="' + esc(s.broll_url) + '" controls playsinline style="width:120px;border-radius:8px;border:1px solid var(--line)"></video>'
: '<img src="' + esc(s.broll_url) + '" alt="cutaway" style="width:120px;border-radius:8px;border:1px solid var(--line);display:block">')
: '')
+ (locked ? '' : '<button type="button" class="btn ghost" data-broll="' + s.index + '" style="margin-top:10px">'
+ (s.broll_url ? 'Remake cutaway' : 'Make cutaway') + '</button>')
+ '</div>';
}
var controls = locked ? '' : var controls = locked ? '' :
toggle toggle
+ settingLabel + settingLabel
@ -109,6 +127,7 @@ function shotHtml(s, locked) {
+ '<p class="line">' + esc(s.line || "") + '</p>' + '<p class="line">' + esc(s.line || "") + '</p>'
+ '<p class="foot">' + (s.word_count || 0) + ' words · about ' + (s.est_seconds || 0) + 's</p>' + '<p class="foot">' + (s.word_count || 0) + ' words · about ' + (s.est_seconds || 0) + 's</p>'
+ '<div style="margin-top:16px">' + controls + '</div>' + '<div style="margin-top:16px">' + controls + '</div>'
+ brollBlock
+ '</div></div>'; + '</div></div>';
} }
@ -151,6 +170,22 @@ function render(r) {
var cams = '<datalist id="cams">' + PRESETS.map(function (c) { var cams = '<datalist id="cams">' + PRESETS.map(function (c) {
return '<option value="' + esc(c) + '">'; }).join("") + '</datalist>'; return '<option value="' + esc(c) + '">'; }).join("") + '</datalist>';
// Post production only exists once there is footage to cut.
var post = '';
if (p.status === 'ready' || p.cut_url) {
post = '<h2 class="sec" style="margin-top:34px">The cut</h2><div class="card">'
+ (p.cut_url
? '<video src="' + esc(p.cut_url) + '" controls playsinline style="width:190px;aspect-ratio:9/16;border-radius:10px;border:1px solid var(--line);background:#000;display:block;margin-bottom:14px"></video>'
: '')
+ '<p class="foot" style="margin:0 0 12px">Cutaways are laid across the joins between shots, '
+ 'so the picture changes while the voice keeps going and the join is never on screen.</p>'
+ '<button type="button" class="btn ghost" id="seams">Measure the joins</button>'
+ '<button type="button" class="btn" id="cut" style="margin-left:10px">'
+ (p.cut_url ? 'Cut it again' : 'Cut the B-roll in') + '</button>'
+ '<div id="seamout" style="margin-top:14px"></div>'
+ '</div>';
}
var actions = locked var actions = locked
? '<div class="card"><p style="margin:0"><strong>Approved.</strong> Filming is switched on once payment is live. Nothing has been charged.</p></div>' ? '<div class="card"><p style="margin:0"><strong>Approved.</strong> Filming is switched on once payment is live. Nothing has been charged.</p></div>'
: '<div class="card">' : '<div class="card">'
@ -167,12 +202,14 @@ function render(r) {
out.innerHTML = cams + head + warn + full out.innerHTML = cams + head + warn + full
+ '<h2 class="sec">The shots</h2>' + '<h2 class="sec">The shots</h2>'
+ s.segments.map(function (x) { return shotHtml(x, locked); }).join("") + s.segments.map(function (x) { return shotHtml(x, locked); }).join("")
+ post
+ actions; + actions;
wire(locked); wire(locked);
} }
function wire(locked) { function wire(locked) {
bindPost();
if (locked) { bindBottom(); return; } if (locked) { bindBottom(); return; }
// Save edits when the field loses focus, so typing is never interrupted. // Save edits when the field loses focus, so typing is never interrupted.
@ -204,6 +241,29 @@ function wire(locked) {
}); });
}); });
[].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) { [].forEach.call(document.querySelectorAll("[data-make]"), function (b) {
b.addEventListener("click", async function () { b.addEventListener("click", async function () {
hideMsg("msg"); hideMsg("msg");
@ -249,6 +309,51 @@ function wire(locked) {
bindBottom(); bindBottom();
} }
function bindPost() {
var sb = document.getElementById("seams");
if (sb) sb.onclick = async function () {
sb.disabled = true; sb.textContent = "Measuring…";
var r = await api("seam_report", { email: email, project_id: pid });
sb.disabled = false; sb.textContent = "Measure the joins";
var el = document.getElementById("seamout");
if (!r.ok) { showMsg("msg", r.error || "Could not measure."); return; }
if (!r.seams || !r.seams.length) {
el.innerHTML = '<p class="foot">' + esc(r.status_text || "Nothing to measure.") + '</p>';
return;
}
el.innerHTML = '<p class="foot" style="margin:0 0 8px">' + esc(r.status_text || "") + '</p>'
+ r.seams.map(function (x) {
var col = x.verdict === "smooth" ? "var(--good)"
: (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)");
return '<div class="foot" style="padding:6px 0;border-top:1px solid var(--line)">'
+ 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · '
+ 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · '
+ '<span style="color:' + col + '">' + esc(x.verdict) + '</span>'
+ (x.recommend_broll ? ' · worth a cutaway' : '')
+ '</div>';
}).join("");
};
var cb = document.getElementById("cut");
if (cb) cb.onclick = async function () {
hideMsg("msg");
cb.disabled = true; cb.textContent = "Cutting…";
var r = await api("render_cut", { email: email, project_id: pid });
if (!r.ok) { cb.disabled = false; cb.textContent = "Cut the B-roll in"; showMsg("msg", r.error || "Could not cut."); return; }
var waited = 0;
var t = setInterval(async function () {
waited += 5;
var g = await api("get_project", { email: email, project_id: pid });
if (g.ok && g.project && g.project.cut_url) { clearInterval(t); load(); return; }
if (waited > 300) {
clearInterval(t);
cb.disabled = false; cb.textContent = "Cut the B-roll in";
showMsg("msg", "That is taking unusually long. Try again shortly.");
}
}, 5000);
};
}
function bindBottom() { function bindBottom() {
var rv = document.getElementById("revise"); var rv = document.getElementById("revise");
if (rv) rv.onclick = async function () { if (rv) rv.onclick = async function () {