Review page: the narrative first, the controls under it, the shots cut on request
The whole piece is now editable at the top of the review page, the rewrite controls sit directly under it rather than below eight shot cards, and cutting the narrative into shots is an explicit Setup shots button. - narrative editable via set_narrative; shots marked stale until re-cut - writer dropdown (Grok 4.6/4.5, Claude Opus 5/Sonnet 5, GPT-5.6/5.5, Gemini 3.1) - align no longer runs unasked on page load, which spoke the script and moved every shot boundary for a cent nobody asked to spend - align's fit check reads shot length from the model instead of assuming 8s, which reported every Omni shot 2s short
This commit is contained in:
parent
f19c3ea88b
commit
30fde992dc
1 changed files with 121 additions and 14 deletions
135
script.html
135
script.html
|
|
@ -86,7 +86,7 @@ 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 price
|
||||
// derived from "segments" has to ask which engine first.
|
||||
var MODELS = [];
|
||||
var MODELS = [], WRITERS = [];
|
||||
function modelFor(p) {
|
||||
var id = (p && p.render_model) || "";
|
||||
for (var i = 0; i < MODELS.length; i++) if (MODELS[i].id === id) return MODELS[i];
|
||||
|
|
@ -256,8 +256,39 @@ function render(r) {
|
|||
? left + " rewrite" + (left === 1 ? "" : "s") + " left"
|
||||
: "Words approved") + '</div></div>';
|
||||
|
||||
var full = s.full_text
|
||||
? '<h2 class="sec">The whole piece</h2><div class="full">' + esc(s.full_text) + '</div>' : "";
|
||||
// The whole piece, which is what was actually written - the shots below are
|
||||
// cut from it. Older scripts predate the field, so fall back to the shot
|
||||
// lines rather than showing an empty box.
|
||||
var narrative = s.full_text
|
||||
|| (s.segments || []).map(function (x) { return (x.line || "").trim(); }).join(" ").trim();
|
||||
var stale = !!s.shots_stale;
|
||||
var full = !narrative ? "" :
|
||||
'<h2 class="sec">The whole piece</h2>'
|
||||
+ (canEditScript
|
||||
? '<div class="card"><div class="field">'
|
||||
+ '<span class="hint">This is the script. The shots further down are cut from it, '
|
||||
+ 'so change the words here and press Setup shots to divide them again.</span>'
|
||||
+ '<textarea id="narr" style="min-height:260px">' + esc(narrative) + '</textarea></div>'
|
||||
+ '<button type="button" class="btn ghost" id="savenarr">Save the words</button>'
|
||||
+ '<span class="foot" style="margin-left:12px" id="narrnote"></span></div>'
|
||||
: '<div class="full">' + esc(narrative) + '</div>');
|
||||
|
||||
// Cutting the narrative into shots. Explicit, because it speaks the whole
|
||||
// piece through ElevenLabs to find real word endings - about a cent, fifteen
|
||||
// seconds, and it rewrites where every shot begins and ends. This used to
|
||||
// happen unasked on page load.
|
||||
var setup = !canEditScript ? '' :
|
||||
'<h2 class="sec" style="margin-top:34px">Setup shots</h2><div class="card">'
|
||||
+ '<p class="foot" style="margin:0 0 14px">Speaks the whole piece once in your pet’s '
|
||||
+ 'voice, measures where every word actually falls, and cuts the shots at real sentence '
|
||||
+ 'endings. It divides the words — it does not change them. About a cent.</p>'
|
||||
+ '<button type="button" class="btn' + (stale ? '' : ' ghost') + '" id="setupshots">'
|
||||
+ 'Setup shots</button>'
|
||||
+ '<span class="foot" style="margin-left:12px" id="setupnote"></span></div>';
|
||||
|
||||
var staleNote = (stale && canEditScript)
|
||||
? '<div class="warn">The words above have changed since these shots were cut, so this is '
|
||||
+ 'the previous division. Press Setup shots to cut the new words.</div>' : '';
|
||||
|
||||
var cams = '<datalist id="cams">' + PRESETS.map(function (c) {
|
||||
return '<option value="' + esc(c) + '">'; }).join("") + '</datalist>';
|
||||
|
|
@ -372,8 +403,20 @@ function render(r) {
|
|||
+ '<h2 class="sec">Change the words</h2>'
|
||||
+ '<div class="field"><label for="fb">What would you change</label>'
|
||||
+ '<span class="hint">Be specific. “Too polite, he would be smugger” beats “make it funnier”. '
|
||||
+ 'Setting and camera you can just edit directly above.</span>'
|
||||
+ 'To change a word yourself, edit the piece above and press Setup shots.</span>'
|
||||
+ '<textarea id="fb" ' + (left <= 0 ? "disabled" : "") + ' placeholder="Shot three is too wordy, and he would never admit the robot is good at anything."></textarea></div>'
|
||||
+ (WRITERS.length
|
||||
? '<div class="field"><label for="writer">Who rewrites it</label>'
|
||||
+ '<span class="hint">Different models are different comedians. The same note sent '
|
||||
+ 'to each one comes back with different jokes.</span>'
|
||||
+ '<select id="writer">'
|
||||
+ WRITERS.map(function (w) {
|
||||
return '<option value="' + esc(w.id) + '"'
|
||||
+ (w.id === (p.writer || "grok") ? ' selected' : '') + '>'
|
||||
+ esc(w.label) + '</option>';
|
||||
}).join("")
|
||||
+ '</select></div>'
|
||||
: '')
|
||||
+ '<button class="btn ghost" id="revise" ' + (left <= 0 ? "disabled" : "") + '>'
|
||||
+ (left > 0 ? "Rewrite the words" : "No rewrites left") + '</button>'
|
||||
+ '<button class="btn" id="approve" style="margin-left:10px">Approve and lock it</button>'
|
||||
|
|
@ -381,10 +424,15 @@ function render(r) {
|
|||
|
||||
var body;
|
||||
if (VIEW === 1) {
|
||||
// The narrative reads first, then what changes it, then what it is cut
|
||||
// into. The rewrite box used to sit at the very bottom, under eight shot
|
||||
// cards, which is a long way from the words it rewrites.
|
||||
body = head + warn + full
|
||||
+ '<h2 class="sec">The words, shot by shot</h2>'
|
||||
+ s.segments.map(linesHtml).join("")
|
||||
+ actions;
|
||||
+ actions
|
||||
+ setup
|
||||
+ '<h2 class="sec" style="margin-top:34px">The words, shot by shot</h2>'
|
||||
+ staleNote
|
||||
+ s.segments.map(linesHtml).join("");
|
||||
} else if (VIEW === 2) {
|
||||
body = head
|
||||
+ '<p class="note" style="margin:0 0 22px"><strong>How it will be laid out.</strong> '
|
||||
|
|
@ -1046,14 +1094,17 @@ function bindAlign() {
|
|||
var out = document.getElementById("alignout");
|
||||
|
||||
function paint(r) {
|
||||
var bought = ((DATA && DATA.script && DATA.script.segments) || []).length * 8;
|
||||
// The shot length belongs to the engine. Hardcoding 8 reported every Omni
|
||||
// shot 2s short and flagged well-fitting lines as overruns.
|
||||
var each = shotSecondsOf((DATA && DATA.project) || {});
|
||||
var bought = ((DATA && DATA.script && DATA.script.segments) || []).length * each;
|
||||
var filmed = r.filmed || 0;
|
||||
out.innerHTML = '<p class="foot" style="margin:0 0 8px">Measured: '
|
||||
+ (r.spoken || 0).toFixed(1) + 's spoken, about ' + filmed.toFixed(1)
|
||||
+ 's filmed, against ' + bought + 's of shots.</p>'
|
||||
+ (r.audio ? '<audio controls src="' + esc(r.audio) + '" style="width:100%;max-width:420px"></audio>' : '')
|
||||
+ (r.shots || []).map(function (s) {
|
||||
var fit = Math.abs((s.filmed || 0) - 8) <= 1.2;
|
||||
var fit = Math.abs((s.filmed || 0) - each) <= 1.2;
|
||||
return '<div class="foot" style="padding:6px 0;border-top:1px solid var(--line)">'
|
||||
+ 'shot ' + s.index + ' · ' + (s.start || 0).toFixed(1) + '-'
|
||||
+ (s.end || 0).toFixed(1) + 's spoken · ~' + (s.filmed || 0).toFixed(1) + 's filmed'
|
||||
|
|
@ -1096,9 +1147,14 @@ function bindAlign() {
|
|||
load();
|
||||
};
|
||||
|
||||
api("align_script", { email: email, project_id: pid }).then(function (r) {
|
||||
if (r && r.ok && !r.pending && r.audio) paint(r);
|
||||
});
|
||||
// Read the cached measurement only. This used to call without `refresh`,
|
||||
// which still STARTS a run when nothing is cached - so merely opening the
|
||||
// page spoke the script and moved every shot boundary, unasked and charged.
|
||||
if (DATA && DATA.project && DATA.project.align_url) {
|
||||
api("align_script", { email: email, project_id: pid }).then(function (r) {
|
||||
if (r && r.ok && !r.pending && r.audio) paint(r);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindPreviz() {
|
||||
|
|
@ -1350,13 +1406,61 @@ function bindPost() {
|
|||
}
|
||||
|
||||
function bindBottom() {
|
||||
// Saving the narrative does NOT re-cut the shots: cutting costs money and
|
||||
// takes fifteen seconds, so it is a button of its own.
|
||||
var sn = document.getElementById("savenarr");
|
||||
if (sn) sn.onclick = async function () {
|
||||
hideMsg("msg");
|
||||
var txt = (document.getElementById("narr") || {}).value || "";
|
||||
if (txt.trim().length < 10) { showMsg("msg", "There is no script there to save."); return; }
|
||||
sn.disabled = true; sn.textContent = "Saving…";
|
||||
var r = await api("set_narrative", { email: email, project_id: pid, text: txt });
|
||||
sn.disabled = false; sn.textContent = "Save the words";
|
||||
if (!r.ok) { showMsg("msg", r.error || "That did not save."); return; }
|
||||
showMsg("msg", r.status_text || "Saved.", "ok");
|
||||
load();
|
||||
};
|
||||
|
||||
var su = document.getElementById("setupshots");
|
||||
if (su) su.onclick = async function () {
|
||||
hideMsg("msg");
|
||||
// Save anything typed but not saved first, so the cut is of the words on
|
||||
// screen rather than the words on file.
|
||||
var box = document.getElementById("narr");
|
||||
if (box && box.value.trim().length >= 10) {
|
||||
await api("set_narrative", { email: email, project_id: pid, text: box.value });
|
||||
}
|
||||
su.disabled = true; su.textContent = "Speaking it…";
|
||||
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);
|
||||
});
|
||||
}
|
||||
su.disabled = false; su.textContent = "Setup shots";
|
||||
if (!r.ok) { showMsg("msg", r.error || "Could not cut that."); return; }
|
||||
if (r.note) { showMsg("msg", r.note); return; }
|
||||
showMsg("msg", "Cut into shots at real sentence endings.", "ok");
|
||||
load();
|
||||
};
|
||||
|
||||
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 });
|
||||
var wsel = document.getElementById("writer");
|
||||
var res = await api("revise_script", {
|
||||
email: email, project_id: pid, feedback: fb,
|
||||
writer: (wsel && wsel.value) || ""
|
||||
});
|
||||
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();
|
||||
|
|
@ -1376,7 +1480,10 @@ 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 || []; }
|
||||
if (c.ok) {
|
||||
PRESETS = c.presets || []; MODELS = c.models || [];
|
||||
WRITERS = c.writers || [];
|
||||
}
|
||||
}
|
||||
var r = await api("get_project", { email: email, project_id: pid });
|
||||
if (!r.ok) { showMsg("msg", r.error || "Could not load that script."); return; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue