diff --git a/movie_editor/frontend/overlay_ui.js b/movie_editor/frontend/overlay_ui.js index 73f040e2..1845b5cb 100644 --- a/movie_editor/frontend/overlay_ui.js +++ b/movie_editor/frontend/overlay_ui.js @@ -79,7 +79,7 @@ return sel; } - function openModal({ title, subtitle, widthClass, onClose }) { + function openModal({ title, subtitle, widthClass, onClose, sticky }) { const overlay = document.createElement("div"); overlay.className = "modal-overlay ov-modal-overlay"; const box = document.createElement("div"); @@ -115,7 +115,7 @@ if (onClose) onClose(); }; closeBtn.onclick = close; - overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); + if (!sticky) overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); document.body.append(overlay); return { overlay, body, foot, close }; } diff --git a/movie_editor/frontend/reactive_focus.js b/movie_editor/frontend/reactive_focus.js index d277eda7..42ea3d15 100644 --- a/movie_editor/frontend/reactive_focus.js +++ b/movie_editor/frontend/reactive_focus.js @@ -103,6 +103,7 @@ title: "Reactive focus", subtitle: "", widthClass: "ov-modal-wide", + sticky: true, // a stray click must not cancel the run; only × / Cancel / a step does onClose: () => { if (!done) { done = true; resolve(null); } }, }); const show = () => { @@ -138,9 +139,10 @@ const wantMoves = !!(rf.camera_moves && rf.reactive_focus && chance(rf.camera_moves_chance, 0.7) > 0); const wantViews = !!(rf.shot_views && rf.reactive_focus && chance(rf.shot_view_chance, 0.4) > 0); if (!wantMoves && !wantViews) return true; - let scenes; + let scenes, seen = {}; try { - scenes = (await window.MovieEditorAPI.focusOptions(project.id, sceneIds)).scenes || []; + seen = await window.MovieEditorAPI.focusOptions(project.id, sceneIds); + scenes = seen.scenes || []; } catch (e) { console.warn("[FunPack] reactive focus: could not list targets —", e.message); window.Store.get().notice = `Reactive focus was skipped: ${e.message}`; @@ -157,7 +159,15 @@ const todo = pending(scenes, savedViews, "views", (s) => !s.already && s.candidates.length); if (todo.length) steps.push({ kind: "views", title: "Views", page: viewsPage(todo) }); } - if (!steps.length) return true; + if (!steps.length) { + // Say why nothing was asked, or "no modal" is indistinguishable from "broken". + const why = !seen.with_shot_marks + ? `no [Shot N] found in the ${seen.texts == null ? "" : seen.texts + " "}scene text(s) sent to generation` + : scenes.length ? "every shot is already decided or already has a camera move/view" + : "no shot has a target or view to offer"; + window.Store.get().notice = `Reactive focus: nothing to ask — ${why}.`; + return true; + } const res = await ask(steps); if (!res) return false; const patch = {}, learn = { decisions: [], views: [] }; diff --git a/movie_editor/server.py b/movie_editor/server.py index e25d5390..6b75b26a 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -3129,13 +3129,14 @@ if web is not None and PromptServer is not None: if shots or views: out.append({"index": i, "preview": " ".join(text.split())[:80], "shots": shots, "views": views}) - return out + marked = sum(1 for t in texts if "[shot" in t.lower()) + return out, {"texts": len(texts), "with_shot_marks": marked} try: - scenes = await asyncio.to_thread(_build) + scenes, seen = await asyncio.to_thread(_build) except Exception as e: # noqa: BLE001 return web.json_response({"detail": f"Could not list camera targets: {e}"}, status=500) - return web.json_response({"scenes": scenes}) + return web.json_response({"scenes": scenes, **seen}) @routes.post(UI_PREFIX + "/api/focus/learn") async def _focus_learn(req):