Show earlier takes, and let one be brought back

Filming again used to overwrite the video outright. Models drift between
generations, so re-filming to fix one thing can make another worse, and there
was no way back.

Every take is now kept and listed under the cut, newest first, with what it
cost and which voice it used. Restoring one keeps the take you are leaving, so
going back is reversible too.

This deliberately lands before the "remake this shot" button, because that is
what will make people re-film often enough to need it.
This commit is contained in:
gator 2026-09-07 23:15:09 +00:00
parent 133e79da1b
commit 862b864fce

View file

@ -295,6 +295,7 @@ function render(r) {
+ '<button type="button" class="btn" id="cut" style="margin-left:10px">' + '<button type="button" class="btn" id="cut" style="margin-left:10px">'
+ (p.cut_url ? 'Cut it again' : 'Cut the B-roll in') + '</button>' + (p.cut_url ? 'Cut it again' : 'Cut the B-roll in') + '</button>'
+ '<div id="seamout" style="margin-top:14px"></div>' + '<div id="seamout" style="margin-top:14px"></div>'
+ '<div id="takes" style="margin-top:20px"></div>'
+ '</div>'; + '</div>';
} }
@ -799,7 +800,185 @@ async function loadEdl(p) {
}; };
} }
// 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;
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 = '<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("");
}
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 = '<p class="foot">' + esc(r.status_text || "Measuring…") + '</p>';
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 = '<p class="foot">' + esc(r.status_text || "Nothing to measure.") + '</p>';
return;
}
el.innerHTML = '<p class="foot" style="margin:0 0 8px">' + esc(r.status_text || "") + '</p>'
+ r.seams.map(function (x) {
var col = x.verdict === "smooth" ? "var(--good)"
: (x.verdict === "visible" ? "var(--accent2)" : "var(--bad)");
return '<div class="foot" style="padding:6px 0;border-top:1px solid var(--line)">'
+ 'shot ' + x.after_shot + ' → ' + x.before_shot + ' at ' + x.at_seconds + 's · '
+ 'shift ' + x.shift_x + ',' + x.shift_y + ' · drift ' + x.drift + ' · '
+ '<span style="color:' + col + '">' + esc(x.verdict) + '</span>'
+ (x.recommend_broll ? ' · worth a cutaway' : '')
+ '</div>';
}).join("");
};
var cb = document.getElementById("cut");
if (cb) cb.onclick = async function () {
hideMsg("msg");
cb.disabled = true; cb.textContent = "Cutting…";
var r = await api("render_cut", { email: email, project_id: pid });
if (!r.ok) { cb.disabled = false; cb.textContent = "Cut the B-roll in"; showMsg("msg", r.error || "Could not cut."); return; }
var waited = 0;
var t = setInterval(async function () {
waited += 5;
var g = await api("get_project", { email: email, project_id: pid });
if (g.ok && g.project && g.project.cut_url) { clearInterval(t); load(); return; }
if (waited > 300) {
clearInterval(t);
cb.disabled = false; cb.textContent = "Cut the B-roll in";
showMsg("msg", "That is taking unusually long. Try again shortly.");
}
}, 5000);
};
}
function bindBottom() {
var rv = document.getElementById("revise");
if (rv) rv.onclick = async function () {
hideMsg("msg");
var fb = document.getElementById("fb").value.trim();
if (fb.length < 5) { showMsg("msg", "Tell us what you would like changed."); return; }
rv.disabled = true; rv.textContent = "Sending…";
var res = await api("revise_script", { email: email, project_id: pid, feedback: fb });
if (!res.ok) { rv.disabled = false; rv.textContent = "Rewrite the words"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", "Rewriting now…", "ok");
load();
};
var ap = document.getElementById("approve");
if (ap) ap.onclick = async function () {
hideMsg("msg");
ap.disabled = true; ap.textContent = "Approving…";
var res = await api("approve_script", { email: email, project_id: pid });
if (!res.ok) { ap.disabled = false; ap.textContent = "Approve and lock it"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", res.status_text || "Approved.", "ok");
load();
};
}
async function load() {
if (!pid || !email) return;
if (!PRESETS.length) {
var c = await api("camera_presets", {});
if (c.ok) PRESETS = c.presets || [];
}
var r = await api("get_project", { email: email, project_id: pid });
if (!r.ok) { showMsg("msg", r.error || "Could not load that script."); return; }
if (!PHOTOS.length && r.project && r.project.pet_id) {
var ph = await api("list_photos", { email: email, pet_id: r.project.pet_id });
if (ph.ok) PHOTOS = ph.photos || [];
}
render(r);
}
load();
</script>
</body>
</html>
+ 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();
};
});
}
function bindPost() { function bindPost() {
loadTakes();
// The editing bay is opt-in. Most people want to watch the thing and leave. // The editing bay is opt-in. Most people want to watch the thing and leave.
var eo = document.getElementById("edlopen"); var eo = document.getElementById("edlopen");
if (eo) eo.onclick = function () { if (eo) eo.onclick = function () {