From 98a76a94f1a9cb6deef5b2bc521632e925fec1cb Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:39:39 +0300 Subject: [PATCH] =?UTF-8?q?opencut=5Fui:=20Composer=20revamp=20pt.1=20?= =?UTF-8?q?=E2=80=94=20Compose=20tab,=20drop=20Characters,=20declutter=20P?= =?UTF-8?q?roperties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Composer button moves next to the Assets/Preview/Properties tabs (was in the menu bar). - Remove Characters entirely from the UI (Composer tab + the scene inspector's character chips). Backend character code left dormant. - New "Compose" tab hosts the Global prompt (moved out of the inspector); stays in sync with the timeline, never clobbers active typing. - Properties decluttered: drop Video transition, Blend length, Generation split marker and "Pause before next clip" from a scene, and "Split before scene 1" from the project — these are driven by the global prompt and timeline. - Swap Import/Export arrows in Shortcuts & Splits (Import ↓ "to me", Export ↑). Co-Authored-By: Claude Opus 4.8 --- movie_editor/frontend/composer.js | 155 +++++++++-------------------- movie_editor/frontend/index.html | 6 +- movie_editor/frontend/inspector.js | 148 +-------------------------- movie_editor/frontend/styles.css | 2 + movie_editor/frontend/tour.js | 2 +- 5 files changed, 54 insertions(+), 259 deletions(-) diff --git a/movie_editor/frontend/composer.js b/movie_editor/frontend/composer.js index 47d78fe4..3c6197ad 100644 --- a/movie_editor/frontend/composer.js +++ b/movie_editor/frontend/composer.js @@ -1,19 +1,16 @@ -// Composer: the single home for prompt-craft tools — Characters, Shortcuts, Split markers. -// Lives in a draggable FloatingWindow, toggled by the "Composer" button in the menu bar. -// (Moved out of the Media Browser so the left dock is just media; these belong together.) +// Composer: the home for prompt-craft tools — Compose (global prompt), Shortcuts, Split markers. +// Lives in a draggable FloatingWindow, toggled by the "Composer" button next to the panel tabs. (function () { const { el, clear } = window.dom; const S = window.Store; const API = window.MovieEditorAPI; let win = null; // FloatingWindow instance (lazy) - let tab = "Characters"; - let q = { Characters: "", Shortcuts: "", Splits: "" }; - let editCharacter = null; // character being edited ({} = new), or null + let tab = "Compose"; + let q = { Shortcuts: "", Splits: "" }; + let gpDraft = null; // in-progress global-prompt text (Compose tab), or null let _modal = null; - function rerender() { if (win && win.isOpen()) render(); } - // ── generic editor modal (shortcut / split forms) ────────────────────────────── function closeModal() { if (_modal) { _modal.remove(); _modal = null; } } function openModal(title, build) { @@ -59,12 +56,28 @@ inp.oninput = () => { q[key] = inp.value; render(); }; return inp; } - function mediaRefPicker(st, value, onChange, opts) { - return window.MediaPicker.create({ - value, mediaBin: st.mediaBin, onChange, compact: true, - startOpen: !!(opts && opts.startOpen), - }); + + // ── compose (global prompt) ───────────────────────────────────────────────────── + // The whole montage as one editable prompt. Edits debounce into the timeline (which + // re-splits into scenes); external timeline edits flow back unless we're mid-type. + function composeTab(st) { + const wrap = el("div", "bin compose-bin"); + const pv = st.preview; + const live = st.project.global_prompt + || (pv && (pv.display_prompt != null ? pv.display_prompt : pv.combined_prompt)) || ""; + const val = gpDraft != null ? gpDraft : live; + + wrap.append(el("div", "lib-form-title", "Global prompt")); + const ta = el("textarea", "lib-in compose-ta"); ta.rows = 14; ta.value = val; + ta.placeholder = "Anchor, scene texts, and split markers — one combined montage prompt for generation."; + ta.oninput = () => { gpDraft = ta.value; S.scheduleGlobalPromptApply(ta.value); }; + wrap.append(ta); + wrap.append(el("div", "insp-hint", + "Edits apply automatically and stay in sync with the timeline's per-scene prompts. Shortcuts expand at generation time.")); + composeTextarea = ta; + return wrap; } + let composeTextarea = null; // ── shortcuts ───────────────────────────────────────────────────────────────── function openShortcutEditor(item) { @@ -113,7 +126,7 @@ wrap.append(searchRow("Shortcuts", "Filter shortcuts…")); const toolbar = el("div", "bin-toolbar"); const addBtn = el("button", "btn ghost tiny", "+ Add"); addBtn.onclick = () => openShortcutEditor({}); - const expBtn = el("a", "btn ghost tiny", "↓ Export"); + const expBtn = el("a", "btn ghost tiny", "↑ Export"); expBtn.href = API.exportShortcutsUrl(); expBtn.download = "funpack_shortcuts.json"; expBtn.title = "Download shortcuts as JSON"; const impFile = el("input"); impFile.type = "file"; impFile.accept = ".json"; impFile.style.display = "none"; impFile.onchange = async () => { @@ -121,7 +134,7 @@ const n = await S.importShortcuts(impFile.files[0]); impFile.value = ""; if (n != null) alert(`Imported ${n} shortcut(s).`); }; - const impBtn = el("button", "btn ghost tiny", "↑ Import"); impBtn.title = "Import shortcuts from JSON"; + const impBtn = el("button", "btn ghost tiny", "↓ Import"); impBtn.title = "Import shortcuts from JSON"; impBtn.onclick = () => impFile.click(); toolbar.append(addBtn, expBtn, impBtn, impFile); wrap.append(toolbar); @@ -182,7 +195,7 @@ wrap.append(searchRow("Splits", "Filter split markers…")); const toolbar = el("div", "bin-toolbar"); const addBtn = el("button", "btn ghost tiny", "+ Add"); addBtn.onclick = () => openSplitMarkerEditor({}); - const expBtn = el("a", "btn ghost tiny", "↓ Export"); + const expBtn = el("a", "btn ghost tiny", "↑ Export"); expBtn.href = API.exportTransitionsUrl(); expBtn.download = "funpack_promptsplit.json"; expBtn.title = "Download split markers as JSON"; const impFile = el("input"); impFile.type = "file"; impFile.accept = ".json"; impFile.style.display = "none"; impFile.onchange = async () => { @@ -190,7 +203,7 @@ const n = await S.importTransitions(impFile.files[0]); impFile.value = ""; if (n != null) alert(`Imported ${n} split marker(s).`); }; - const impBtn = el("button", "btn ghost tiny", "↑ Import"); impBtn.title = "Import split markers from JSON"; + const impBtn = el("button", "btn ghost tiny", "↓ Import"); impBtn.title = "Import split markers from JSON"; impBtn.onclick = () => impFile.click(); toolbar.append(addBtn, expBtn, impBtn, impFile); wrap.append(toolbar); @@ -217,108 +230,26 @@ return wrap; } - // ── characters ────────────────────────────────────────────────────────────────── - function characterForm(st, item) { - const isNew = !item.id; - const box = el("div", "lib-form"); - box.append(el("div", "lib-form-title", isNew ? "New character" : `Edit “${item.name || "character"}”`)); - const name = el("input", "lib-in"); name.placeholder = "Name"; name.value = item.name || ""; - const appearance = el("textarea", "lib-in"); appearance.rows = 2; appearance.placeholder = "Face, hair, eyes, skin…"; appearance.value = item.appearance || ""; - const body = el("textarea", "lib-in"); body.rows = 2; body.placeholder = "Build, height, proportions…"; body.value = item.body || ""; - const wardrobe = el("textarea", "lib-in"); wardrobe.rows = 2; wardrobe.placeholder = "Default outfit or style…"; wardrobe.value = item.wardrobe || ""; - const always = el("textarea", "lib-in"); always.rows = 2; always.placeholder = "Tags always included in this character's prompt"; always.value = item.always_include || ""; - const never = el("textarea", "lib-in"); never.rows = 2; never.placeholder = "Appended to negative when this character is on a scene"; never.value = item.never_include || ""; - let faceRef = item.face_ref || null, bodyRef = item.body_ref || null, detailRef = item.detail_ref || null; - box.append( - labeled("Name", name), labeled("Appearance", appearance), labeled("Body", body), - labeled("Wardrobe", wardrobe), labeled("Always include", always), labeled("Never include", never), - labeled("Face ref (identity pin)", mediaRefPicker(st, faceRef, (v) => { faceRef = v; }, { startOpen: true })), - labeled("Body ref", mediaRefPicker(st, bodyRef, (v) => { bodyRef = v; }, { startOpen: true })), - labeled("Detail ref", mediaRefPicker(st, detailRef, (v) => { detailRef = v; }, { startOpen: true })), - ); - const actions = el("div", "lib-form-actions"); - const save = el("button", "btn primary tiny", "Save"); - save.onclick = async () => { - const nm = name.value.trim(); - if (!nm) { alert("Character name is required."); return; } - await S.saveCharacter({ - id: item.id || undefined, original_id: item.id || undefined, name: nm, - appearance: appearance.value, body: body.value, wardrobe: wardrobe.value, - always_include: always.value, never_include: never.value, - face_ref: faceRef, body_ref: bodyRef, detail_ref: detailRef, - }); - editCharacter = null; render(); - }; - const cancel = el("button", "btn ghost tiny", "Cancel"); - cancel.onclick = () => { editCharacter = null; render(); }; - actions.append(save, cancel); box.append(actions); - return box; - } - - function charactersTab(st) { - if (editCharacter) return characterForm(st, editCharacter); - const wrap = el("div", "bin"); - wrap.append(searchRow("Characters", "Filter characters…")); - const hint = el("div", "pj-meta char-hint"); - hint.textContent = st.selectedSceneId - ? "Click a character to add/remove it on the selected scene." - : "Select a scene, then click characters to assign them."; - wrap.append(hint); - const head = el("div", "lib-head"); - const add = el("button", "btn ghost tiny", "+ New"); add.onclick = () => { editCharacter = {}; render(); }; - head.append(add); wrap.append(head); - - const assigned = st.selectedSceneId ? new Set(S.sceneCharacterIds(st.selectedSceneId)) : null; - const list = el("div", "char-list"); - const items = filtered(st.characters || [], q.Characters, (c) => - `${c.name} ${c.appearance || ""} ${c.body || ""} ${c.wardrobe || ""}`); - items.forEach((c) => { - const onScene = assigned && assigned.has(c.id); - const row = el("div", "char-row" + (onScene ? " on-scene" : "")); - const prev = el("div", "char-thumb"); - if (c.face_ref) { const img = el("img"); img.src = API.mediaUrl(c.face_ref); img.loading = "lazy"; prev.append(img); } - else prev.append(el("span", null, "◎")); - row.append(prev); - const info = el("div", "char-info"); - info.append(el("div", "char-name", c.name || "(unnamed)")); - const sub = [c.appearance, c.wardrobe].filter(Boolean).join(" · "); - if (sub) info.append(el("div", "char-sub", sub)); - row.append(info); - if (onScene) row.append(el("span", "char-badge", "on scene")); - row.onclick = () => { - if (st.selectedSceneId) S.toggleSceneCharacter(st.selectedSceneId, c.id); - else { editCharacter = { ...c }; render(); } - }; - const edit = el("button", "btn ghost tiny", "✎"); edit.title = "Edit character"; - edit.onclick = (e) => { e.stopPropagation(); editCharacter = { ...c }; render(); }; - const del = el("button", "btn ghost tiny danger", "✕"); del.title = "Delete character"; - del.onclick = async (e) => { e.stopPropagation(); if (confirm(`Delete character “${c.name}”?`)) await S.deleteCharacter(c.id); }; - const actions = el("div", "char-actions"); actions.append(edit, del); row.append(actions); - list.append(row); - }); - if (!items.length) list.append(el("div", "pj-meta", (st.characters || []).length ? "No match." : "No characters yet — create one.")); - wrap.append(list); - return wrap; - } - // ── window + tabs ──────────────────────────────────────────────────────────────── - const TABS = ["Characters", "Shortcuts", "Splits"]; + const TABS = ["Compose", "Shortcuts", "Splits"]; function render() { if (!win) return; const st = S.get(); + composeTextarea = null; const root = clear(win.body); const shell = el("div", "composer-shell"); const tabs = el("div", "bin-tabs composer-tabs"); TABS.forEach((name) => { const b = el("button", "bin-tab" + (tab === name ? " active" : ""), name); - b.title = name === "Splits" ? "Split markers (generation prompt)" : name; - b.onclick = () => { if (tab === name) return; tab = name; editCharacter = null; closeModal(); render(); }; + b.title = name === "Splits" ? "Split markers (generation prompt)" + : name === "Compose" ? "Global prompt — the whole montage" : name; + b.onclick = () => { if (tab === name) return; tab = name; closeModal(); render(); }; tabs.append(b); }); shell.append(tabs); const scroll = el("div", "composer-scroll"); scroll.append( - tab === "Characters" ? charactersTab(st) + tab === "Compose" ? composeTab(st) : tab === "Shortcuts" ? shortcutsTab(st) : splitMarkersTab(st), ); @@ -344,19 +275,27 @@ function registerButton(btn) { _btn = btn; btn.onclick = toggle; _syncButton(); } function _syncButton() { if (_btn) _btn.classList.toggle("on", isOpen()); } - // Re-render on relevant store changes (characters/shortcuts/splits/selection). + // Re-render on relevant store changes (shortcuts / splits / project switch). let lastFp = null; S.subscribe((st) => { if (!isOpen()) return; const fp = JSON.stringify({ - c: st.characters?.length, s: st.shortcuts?.length, t: st.transitions?.length, - sel: st.selectedSceneId, pid: st.project?.id, + s: st.shortcuts?.length, t: st.transitions?.length, pid: st.project?.id, }); if (fp === lastFp) return; lastFp = fp; render(); }); + // Keep the Compose textarea in sync with timeline-driven global-prompt changes, + // but never clobber what the user is actively typing. + window.addEventListener("funpack-global-prompt-updated", (e) => { + if (tab !== "Compose" || !composeTextarea) return; + if (document.activeElement === composeTextarea) return; + gpDraft = null; + composeTextarea.value = (e.detail && e.detail.text) || ""; + }); + window.Composer = { toggle, isOpen, registerButton }; // Self-wire the header button (present in index.html before this script runs). diff --git a/movie_editor/frontend/index.html b/movie_editor/frontend/index.html index 10673f0d..e179814d 100644 --- a/movie_editor/frontend/index.html +++ b/movie_editor/frontend/index.html @@ -48,9 +48,6 @@
◉ FunPack Cutting Room
@@ -92,6 +89,9 @@
+
diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index 784c45fa..38fa7d2e 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -14,28 +14,6 @@ ["mixed", "Mixed · Img2Video anchor + prior guides"], ]; - function splitMarkerSelect(value, onChange, opts) { - opts = opts || {}; - const sel = el("select"); - const noneLabel = opts.noneLabel || "— default cut —"; - const none = el("option", null, noneLabel); none.value = ""; sel.append(none); - (S.get().transitions || []).forEach((t) => { - const name = t.trigger || t.name || t.key; if (!name) return; - const o = el("option", null, name); o.value = name; if (name === value) o.selected = true; sel.append(o); - }); - if (value && ![...sel.options].some((o) => o.value === value)) { - const o = el("option", null, value); o.value = value; o.selected = true; sel.append(o); - } - sel.onchange = () => onChange(sel.value); - sel.title = opts.title || "Prompt marker: how Studio splits the montage before the next scene when generating"; - return sel; - } - - // Legacy alias used in a few call sites - function transitionSelect(value, onChange) { - return splitMarkerSelect(value, onChange); - } - function field(labelText, control) { const l = el("label", "field"); l.append(el("span", null, labelText)); l.append(control); return l; } @@ -213,7 +191,6 @@ const ref = scene.source?.media_ref; const asset = ref ? (st.mediaBin || []).find((m) => m.id === ref) : null; body.append(field("Source", el("span", null, asset?.name || ref || "From last render"))); - renderOutgoingSeam(st, scene); foldSection("More editing", false, (more) => { const fx = scene.effects || {}; @@ -291,9 +268,7 @@ (lenDrift ? `Rendered ≈ ${shownDur.toFixed(2)}s · plan ${planDur.toFixed(2)}s — Regenerate to apply` : `Duration ≈ ${planDur.toFixed(2)}s`) - + ` · trim on timeline · generation splits: global prompt or Outgoing seam below`)); - - renderOutgoingSeam(st, scene); + + ` · trim on timeline · generation splits live in the global prompt (Composer)`)); const actions = el("div", "insp-block"); const genBtn = el("button", "btn primary", "Generate this scene"); @@ -302,7 +277,6 @@ body.append(actions); foldSection("More editing", false, (more) => { - if (PC()?.usesFunpackStudio(st)) renderSceneCharacters(st, scene, more); const fxTag = el("div", "insp-tag"); fxTag.textContent = "Video effects"; more.append(fxTag); const fx = scene.effects || {}; @@ -415,87 +389,6 @@ }); } - const VIDEO_TRANSITIONS = [ - ["", "Hard cut"], - ["crossfade", "Dissolve"], - ["fadeblack", "Fade through black"], - ["wipeleft", "Wipe left"], - ["wiperight", "Wipe right"], - ]; - - function renderOutgoingSeam(st, scene) { - const p = st.project; - const fps = scene.fps || p.frame_rate || 24; - const type = (scene.video_transition || "").trim(); - const frames = scene.transition_frames > 0 ? scene.transition_frames : 16; - - const tag = el("div", "insp-tag"); tag.textContent = "Outgoing seam"; body.append(tag); - body.append(el("div", "insp-hint", - "Video blend on the cut to the next clip — drag the colored edge on the timeline to set length.")); - - const typeSel = el("select"); - VIDEO_TRANSITIONS.forEach(([v, lbl]) => { - const o = el("option", null, lbl); o.value = v; - if (v === type) o.selected = true; - typeSel.append(o); - }); - typeSel.onchange = () => { - const next = typeSel.value; - if (!next) S.patchScene(scene.id, { video_transition: "", transition_frames: null }); - else S.patchScene(scene.id, { video_transition: next, transition_frames: frames }); - }; - body.append(field("Video transition", typeSel)); - - const fr = el("input"); fr.type = "number"; fr.min = "1"; fr.max = "120"; fr.step = "1"; - fr.dataset.k = "sc-blend-frames"; - fr.value = type ? frames : ""; - fr.disabled = !type; - fr.placeholder = type ? "" : "—"; - fr.oninput = () => { - if (!type) return; - S.patchScene(scene.id, { transition_frames: Math.max(1, parseInt(fr.value, 10) || 16) }); - }; - body.append(field(`Blend length (frames · ${fps} fps)`, fr)); - - body.append(field("Generation split marker", splitMarkerSelect(scene.transition_to_next || "", - (v) => S.patchScene(scene.id, { transition_to_next: v }), - { noneLabel: "— default cut —", - title: "Prompt marker before the next scene when Studio splits the montage (not the video blend)" }))); - - const gapIn = el("input"); - gapIn.type = "number"; - gapIn.min = "0"; - gapIn.step = "0.05"; - gapIn.value = scene.gap_after_sec || 0; - gapIn.oninput = () => S.patchSceneQuiet(scene.id, { - gap_after_sec: Math.max(0, parseFloat(gapIn.value || "0")), - }); - body.append(field("Pause before next clip (s)", gapIn)); - body.append(el("div", "insp-hint", - "Drag a clip right on the timeline to open space before it. Drag back flush with the previous clip to remove the pause.")); - } - - function renderSceneCharacters(st, scene, parent) { - parent = parent || body; - const ids = S.sceneCharacterIds(scene.id); - const tag = el("div", "insp-tag"); tag.textContent = "Characters"; parent.append(tag); - const chips = el("div", "char-chips"); - if (!ids.length) { - chips.append(el("span", "char-chip empty", "None — assign in Characters bin")); - } else { - ids.forEach((cid) => { - const c = (st.characters || []).find((x) => x.id === cid); - const chip = el("span", "char-chip"); - chip.textContent = c?.name || cid; - const rm = el("button", "char-chip-rm", "✕"); - rm.onclick = () => S.toggleSceneCharacter(scene.id, cid); - chip.append(rm); - chips.append(chip); - }); - } - parent.append(chips); - } - function slotLabelFor(st, slotId) { if (!slotId || slotId === "funpack") return null; const slot = (st.models?.slots || []).find((s) => s.id === slotId); @@ -552,9 +445,6 @@ anchor.placeholder = "World / setting context prepended to every scene"; anchor.oninput = () => S.patchProjectQuiet({ anchor: anchor.value }); body.append(field("Anchor", anchor)); - body.append(field("Split before scene 1 (generation prompt)", splitMarkerSelect(p.intro_transition || "", - (v) => S.patchProject({ intro_transition: v }), - { noneLabel: "— default cut —", title: "Prompt marker between anchor and scene 1 when Studio splits a long montage" }))); const neg = el("textarea"); neg.rows = 2; neg.value = p.negative_prompt || ""; neg.dataset.k = "pj-neg"; neg.placeholder = "What to avoid in every scene"; neg.oninput = () => S.patchProjectQuiet({ negative_prompt: neg.value }); @@ -785,31 +675,6 @@ body.append(wrap); } - // Pinned global-prompt editor (always on top). Edits debounce into the timeline via store. - let gpDraft = null; - let gpProjectId = null; - function renderGlobalPrompt(st) { - if (st.project.id !== gpProjectId) { gpProjectId = st.project.id; gpDraft = null; } - const pv = st.preview; - const live = st.project.global_prompt - || (pv && (pv.display_prompt != null ? pv.display_prompt : pv.combined_prompt)) - || ""; - const dirty = gpDraft != null && gpDraft !== live; - const val = gpDraft != null ? gpDraft : live; - - const sec = el("div", "insp-global"); - sec.append(el("div", "insp-global-title", "Global prompt")); - - const ta = el("textarea", "insp-global-ta"); ta.rows = 3; ta.value = val; ta.dataset.k = "global-prompt"; - ta.placeholder = "Anchor, scene texts, and split markers — one combined montage prompt for generation."; - ta.oninput = () => { gpDraft = ta.value; S.scheduleGlobalPromptApply(ta.value); }; - sec.append(ta); - sec.append(el("div", "insp-hint", dirty - ? "Applying to timeline…" - : "Edits apply automatically. Stays in sync with per-scene prompts. Per-seam split markers: Outgoing seam in the scene inspector.")); - body.append(sec); - } - function renderSwitch(st, scene) { const sw = el("div", "insp-switch"); const mk = (label, active, on, disabled) => { @@ -955,7 +820,6 @@ if (!st.project) { title.textContent = "Inspector"; body.append(el("div", "pj-meta", "No project open.")); return; } const ov = st.selectedOverlayId ? S.overlayTrack(st.selectedOverlayId) : null; const scene = !ov && st.selectedSceneId ? S.scene(st.selectedSceneId) : null; - renderGlobalPrompt(st); renderSwitch(st, scene); renderEngineStrip(st); if (ov) renderOverlayInspector(st, ov); @@ -992,16 +856,6 @@ }, 60); }); - window.addEventListener("funpack-invalidate-global-prompt", () => { gpDraft = null; }); - window.addEventListener("funpack-global-prompt-updated", (e) => { - if (_editing && document.activeElement?.dataset?.k === "global-prompt") return; - gpDraft = null; - const ta = body.querySelector('[data-k="global-prompt"]'); - if (ta && e.detail?.text != null) ta.value = e.detail.text; - const hint = body.querySelector(".insp-global .insp-hint"); - if (hint) hint.textContent = "Edits apply automatically. Stays in sync with per-scene prompts. Per-seam split markers: Outgoing seam in the scene inspector."; - }); - if (window.ViewBus) window.ViewBus.subscribeInspector(render); else S.subscribe(render); })(); diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index ea9a0e66..e1977e29 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -1513,4 +1513,6 @@ body.tour-mode { overflow: hidden; } .composer-shell { display: flex; flex-direction: column; width: 100%; min-height: 0; } .composer-tabs { padding: 10px 12px 0; flex: 0 0 auto; } .composer-scroll { flex: 1; min-height: 0; overflow: auto; padding: 12px; } +.compose-bin { display: flex; flex-direction: column; gap: 8px; height: 100%; } +.compose-ta { flex: 1; min-height: 220px; resize: none; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; } diff --git a/movie_editor/frontend/tour.js b/movie_editor/frontend/tour.js index ca517308..d72ae9c7 100644 --- a/movie_editor/frontend/tour.js +++ b/movie_editor/frontend/tour.js @@ -448,7 +448,7 @@ id: "inspector-scene", title: "Scene inspector", target: "#inspector-body", - body: "Edit the prompt, source type (Empty, Image, Carry), transitions, and characters for the selected scene. Deselect all clips to edit project anchor, seed, and global timing.", + body: "Edit the prompt and source type (Empty, Image, Carry) for the selected scene. Deselect all clips to edit project anchor, seed, and global timing. The whole-montage global prompt lives in the Composer.", pad: 8, }, {