diff --git a/script.html b/script.html
index a69b320..bebb78c 100644
--- a/script.html
+++ b/script.html
@@ -394,4 +394,203 @@ function render(r) {
+ 'go straight to filming.
'
+ '
Hear it first
'
+ '
'
- + '
Filming costs real money; speaking the '
\ No newline at end of file
+ + '
Filming costs real money; speaking the '
+ + 'words costs about a hundredth of a cent. Run it through in your pet’s voice '
+ + 'over the opening stills first, and only film once the timing and the jokes land.
'
+ + ''
+ + ''
+ + ''
+ + '
Cutting to the clock speaks the whole piece once, '
+ + 'measures where every word actually falls, and moves the shot boundaries to real word '
+ + 'endings. It rewrites how the lines are divided — not the words themselves.
'
+ + ''
+ + ''
+ + '
'
+ + '
The shots
'
+ + s.segments.map(function (x) { return shotHtml(x, false); }).join("")
+ + own
+ + film;
+ } else if (VIEW === 3) {
+ body = head + film;
+ } else {
+ body = head + post + film;
+ }
+
+ out.innerHTML = cams + stepper(stage, VIEW) + body;
+
+ wire(locked);
+}
+
+var PICS = [], SUGGESTED = [];
+
+function renderPics() {
+ var el = document.getElementById("pics");
+ if (!el) return;
+ el.innerHTML = PICS.map(function (p) {
+ return '
'
+ + ''
+ + ''
+ + ''
+ + '
';
+ }).join("");
+ [].forEach.call(el.querySelectorAll("[data-rmpic]"), function (b) {
+ b.onclick = async function () {
+ var r = await api("delete_project_image", { email: email, image_id: b.getAttribute("data-rmpic") });
+ if (!r.ok) { showMsg("msg", r.error || "Could not remove that."); return; }
+ await loadPics();
+ };
+ });
+}
+
+async function loadPics() {
+ var r = await api("list_project_images", { email: email, project_id: pid });
+ PICS = (r.ok && r.images) || [];
+ renderPics();
+}
+
+async function addPic(file) {
+ hideMsg("msg");
+ if (!file || !/^image\//.test(file.type)) { showMsg("msg", "That is not an image."); return; }
+ // Asked for up front: a picture with no description cannot be matched to
+ // anything, so it would just sit there unused.
+ var desc = prompt("What is in this picture? A short description is what lets it be matched to the script.");
+ if (!desc || desc.trim().length < 3) { showMsg("msg", "A description is needed to place the picture."); return; }
+ var drop = document.getElementById("drop");
+ if (drop) drop.textContent = "Uploading…";
+ var img;
+ try { img = await resizeImage(file, 1600, 0.85); }
+ catch (e) { showMsg("msg", e.message || "Could not read that image."); return; }
+ var r = await api("add_project_image", { email: email, project_id: pid,
+ description: desc.trim(), data_b64: img.b64 });
+ if (drop) drop.textContent = "Paste an image, drop one here, or click to choose";
+ if (!r.ok) { showMsg("msg", r.error || "That upload did not work."); return; }
+ await loadPics();
+}
+
+function bindPics() {
+ var drop = document.getElementById("drop");
+ if (!drop) return;
+ var picker = document.getElementById("picker");
+
+ drop.onclick = function () { picker.click(); };
+ picker.onchange = function () {
+ [].forEach.call(picker.files || [], addPic);
+ picker.value = "";
+ };
+ drop.addEventListener("dragover", function (e) { e.preventDefault(); drop.classList.add("hot"); });
+ drop.addEventListener("dragleave", function () { drop.classList.remove("hot"); });
+ drop.addEventListener("drop", function (e) {
+ e.preventDefault(); drop.classList.remove("hot");
+ [].forEach.call((e.dataTransfer && e.dataTransfer.files) || [], addPic);
+ });
+ // Paste anywhere on the page, which is how people actually do this.
+ document.addEventListener("paste", function (e) {
+ var items = (e.clipboardData && e.clipboardData.items) || [];
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].type && items[i].type.indexOf("image") === 0) {
+ var f = items[i].getAsFile();
+ if (f) { e.preventDefault(); addPic(f); }
+ return;
+ }
+ }
+ });
+
+ var sg = document.getElementById("suggest");
+ if (sg) sg.onclick = async function () {
+ hideMsg("msg");
+ if (!PICS.length) { showMsg("msg", "Add a picture first."); return; }
+ sg.disabled = true; sg.textContent = "Reading the script…";
+ var r = await api("suggest_image_placements", { email: email, project_id: pid });
+ if (!r.ok) { sg.disabled = false; sg.textContent = "Suggest where they fit"; showMsg("msg", r.error || "Could not suggest."); return; }
+ // Reading the script is a model call, far too slow to answer inside one
+ // request, so it runs in the background and we wait for it here.
+ if (r.pending) {
+ document.getElementById("sugnote").textContent = r.status_text || "Reading the script…";
+ var waited = 0;
+ r = await new Promise(function (done) {
+ var t = setInterval(async function () {
+ waited += 4;
+ var g = await api("suggest_image_placements", { email: email, project_id: pid });
+ if (g.ok && !g.pending) { clearInterval(t); done(g); return; }
+ if (waited > 150) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); }
+ }, 4000);
+ });
+ }
+ sg.disabled = false; sg.textContent = "Suggest where they fit";
+ document.getElementById("sugnote").textContent = r.status_text || "";
+ if (!r.ok) { showMsg("msg", r.error || "Could not suggest."); return; }
+ SUGGESTED = r.placements || [];
+ var byId = {};
+ PICS.forEach(function (p) { byId[p.id] = p; });
+ document.getElementById("places").innerHTML = SUGGESTED.length
+ ? SUGGESTED.map(function (x) {
+ var p = byId[x.image_id] || {};
+ return '