From 196f8cd80726473f3d02ccd2c9df3352a414441d Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:42:45 +0300 Subject: [PATCH] feat: Composer says when timed/weighted phrases are in the prompt, and that they only work on H3 Co-Authored-By: Claude Sonnet 5.5 --- app/legacy/composer.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/legacy/composer.js b/app/legacy/composer.js index 04bac095..aa3c0317 100644 --- a/app/legacy/composer.js +++ b/app/legacy/composer.js @@ -213,7 +213,10 @@ wrap.append(ta); wrap.append(el("div", "insp-hint", "Edits apply to the scenes as you type, and scene edits show here. The anchor is its own field. Shortcuts expand at generation time.")); + timedHintEl = el("div", "insp-hint compose-var-hint"); + wrap.append(timedHintEl); composeTextarea = ta; + updateTimedHint(); if (window.ShortcutAutocomplete) window.ShortcutAutocomplete.attach(ta); if (st.project && window.ShortcutSuggest) titleRow.append(window.ShortcutSuggest.bulb(ta)); @@ -255,6 +258,7 @@ } let composeTextarea = null; let varsOpen = false; // Variables panel expanded? + let timedHintEl = null; // timed phrases `[walks left@2.0-3.5]` found in the prompt let varHintEl = null; // undeclared / cycle hint line under the prompt // ── templates bar (above the global prompt) ───────────────────────────────────── @@ -433,8 +437,45 @@ return cycles; } + // Timed phrases: `[phrase@t0-t1]` / `[phrase:w@t0-t1]`, seconds from the scene start. Mirrors + // h3_token_weights._TIMED — the phrase is only allowed to shape the video inside its window. + const TIMED_RE = /(? t0) }); + } + // (phrase:1.5) — a weight with no window. Mirrors h3_token_weights._WEIGHTED. + const weighted = [...txt.matchAll(/(? ({ phrase: (m[1] ?? m[3]).trim(), w: parseFloat(m[2] ?? m[4]) })).filter((f) => f.phrase); + const PC = window.PipelineCaps; + const h3 = !!(PC && PC.isH3 && PC.isH3(S.get())); + let msg = ""; + let warn = false; + if (found.length || weighted.length) { + const bad = found.filter((f) => f.bad); + if (!h3) { msg = "⏱ Timed and weighted phrases are MiniMax H3 only — the words stay, the markup is dropped."; warn = true; } + else if (bad.length) { msg = `⚠ "${bad[0].phrase}" ends before it starts (${bad[0].t0}-${bad[0].t1}s) — window ignored.`; warn = true; } + else { + const parts = found.map((f) => `${f.phrase} ${f.t0}-${f.t1}s${f.w !== 1 ? ` ×${f.w}` : ""}`) + .concat(weighted.map((f) => `${f.phrase} ×${f.w}`)); + msg = "⏱ " + parts.join(" · ") + + (found.length ? " — a window only shapes the picture inside it; keep the subject outside the brackets." : ""); + } + } + timedHintEl.textContent = msg; + timedHintEl.classList.toggle("compose-var-warn", warn); + timedHintEl.style.display = msg ? "" : "none"; + } + // Live hint under the prompt: cycle warning takes priority, else list $vars used but not declared. function updateVarHint() { + updateTimedHint(); if (!varHintEl) return; const declared = new Set((S.projectVariables() || []) .map((v) => String((v && v.name) || "").replace(/^\$+/, "").trim()).filter(Boolean));