diff --git a/script.html b/script.html
index 4fb89d8..7e8bbec 100644
--- a/script.html
+++ b/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") + '';
- var full = s.full_text
- ? '
The whole piece
' + esc(s.full_text) + '
' : "";
+ // 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 ? "" :
+ '
The whole piece
'
+ + (canEditScript
+ ? '
'
+ + '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.'
+ + '
'
+ + ''
+ + '
'
+ : '
' + esc(narrative) + '
');
+
+ // 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 ? '' :
+ '
Setup shots
'
+ + '
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.
The words above have changed since these shots were cut, so this is '
+ + 'the previous division. Press Setup shots to cut the new words.
' : '';
var cams = '';
@@ -372,8 +403,20 @@ function render(r) {
+ '
Change the words
'
+ '
'
+ 'Be specific. “Too polite, he would be smugger” beats “make it funnier”. '
- + 'Setting and camera you can just edit directly above.'
+ + 'To change a word yourself, edit the piece above and press Setup shots.'
+ '
'
+ + (WRITERS.length
+ ? '
'
+ + 'Different models are different comedians. The same note sent '
+ + 'to each one comes back with different jokes.'
+ + '
'
+ : '')
+ ''
+ ''
@@ -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
- + '
'
+ + staleNote
+ + s.segments.map(linesHtml).join("");
} else if (VIEW === 2) {
body = head
+ '
How it will be laid out. '
@@ -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 = '
Measured: '
+ (r.spoken || 0).toFixed(1) + 's spoken, about ' + filmed.toFixed(1)
+ 's filmed, against ' + bought + 's of shots.
'
+ (r.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 '
'
+ '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; }