From edac8d1cfc490f0b050bc898603a6c8de24672dd Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:20:59 +0300 Subject: [PATCH] Clear orphan scene ratings when renders are missing on reload. Drop stale ratings without playable media, hide rating badges in the UI, and avoid preview 500s when validation fails. --- movie_editor/frontend/store.js | 59 ++++++++++++++++++++++++++++--- movie_editor/frontend/timeline.js | 9 +++++ movie_editor/server.py | 11 +++++- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index a8deb646..f45fb97b 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -213,7 +213,9 @@ let _globalApplyTimer = null; function scheduleGlobalPromptApply(text) { clearTimeout(_globalApplyTimer); - _globalApplyTimer = setTimeout(() => { applyGlobalPromptQuiet(text); }, 500); + _globalApplyTimer = setTimeout(() => { + applyGlobalPromptQuiet(text).catch((e) => console.warn("Global prompt apply failed:", e)); + }, 500); } function _normalizeSceneText(t) { @@ -500,6 +502,40 @@ if (!quiet) notify(); } + function _genUnitHasRender(unitId) { + return (state.project?.scenes || []).some( + (s) => genUnitId(s) === unitId && !!(state.sceneRenders[s.id]?.media?.filename) + ); + } + + // Ratings apply to a specific render — drop stale ratings when nothing is playable. + function _clearOrphanRatings() { + if (!state.project) return false; + let cleared = false; + const seen = new Set(); + for (const sc of state.project.scenes || []) { + if (isVideoClip(sc)) { + if (sc.rating) { sc.rating = ""; cleared = true; } + continue; + } + const uid = genUnitId(sc); + if (seen.has(uid)) continue; + seen.add(uid); + const root = genUnitRoot(uid) || sc; + if (root.rating && !_genUnitHasRender(uid)) { + root.rating = ""; + cleared = true; + } + for (const s of state.project.scenes) { + if (genUnitId(s) === uid && s.id !== root.id && s.rating) { + s.rating = ""; + cleared = true; + } + } + } + return cleared; + } + let _validateRendersToken = 0; async function _validateSceneRenders() { @@ -507,7 +543,14 @@ const token = ++_validateRendersToken; const snapshot = JSON.parse(JSON.stringify(state.sceneRenders || {})); const ids = Object.keys(snapshot); - if (!ids.length) return; + if (!ids.length) { + if (_clearOrphanRatings()) { + _syncEditorStateToProject(); + scheduleSave(); + notify(); + } + return; + } let missing = 0; await Promise.all(ids.map(async (id) => { if (token !== _validateRendersToken) return; @@ -534,8 +577,11 @@ } })); if (token !== _validateRendersToken) return; - if (missing) { - state.notice = `${missing} saved render${missing > 1 ? "s" : ""} no longer on disk — regenerate those clips.`; + const clearedRatings = _clearOrphanRatings(); + if (missing || clearedRatings) { + if (missing) { + state.notice = `${missing} saved render${missing > 1 ? "s" : ""} no longer on disk — regenerate those clips.`; + } _syncEditorStateToProject(); scheduleSave(); notify(); @@ -558,6 +604,10 @@ state.gen = { state: "idle", promptId: null, media: [], msg: "" }; state.sceneRenders = JSON.parse(JSON.stringify(state.project.scene_renders || {})); state.sceneGhosts = JSON.parse(JSON.stringify(state.project.scene_ghosts || [])); + if (_clearOrphanRatings()) { + _syncEditorStateToProject(); + scheduleSaveSilent(); + } _genInFlightIds.clear(); _removedDuringGen.clear(); if (window.EditorHistory) window.EditorHistory.clear(); @@ -995,6 +1045,7 @@ source: { type: "video", media_ref: mediaRef }, gen_unit_id: root.id, cut_offset_frames: 0, + rating: "", }); if (state.selectedSceneId && !scene(state.selectedSceneId)) { state.selectedSceneId = root.id; diff --git a/movie_editor/frontend/timeline.js b/movie_editor/frontend/timeline.js index 4efb4f1f..a04b6e4d 100644 --- a/movie_editor/frontend/timeline.js +++ b/movie_editor/frontend/timeline.js @@ -191,7 +191,16 @@ }); } + function genUnitHasRender(st, scene) { + const uid = S.genUnitId(scene); + return (st.project?.scenes || []).some( + (s) => S.genUnitId(s) === uid && hasRender(st, s.id) + ); + } + function sceneRatingRaw(st, scene) { + if (S.isVideoClip && S.isVideoClip(scene)) return ""; + if (!genUnitHasRender(st, scene)) return ""; const root = S.genUnitRoot(S.genUnitId(scene)) || scene; return ((root && root.rating) || scene.rating || "").trim(); } diff --git a/movie_editor/server.py b/movie_editor/server.py index abf4dc92..127bddbd 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -613,7 +613,16 @@ if web is not None and PromptServer is not None: preview_target = Project.from_dict(p.to_dict()) preview_target.scenes = [s for s in preview_target.scenes if not s.excluded] if for_generation: - validation = bridge.validate_generation_prompt(p, preview_target) + try: + validation = bridge.validate_generation_prompt(p, preview_target) + except Exception as e: # noqa: BLE001 + return web.json_response({ + "combined_prompt": "", + "display_prompt": "", + "for_generation": True, + "parse_error": str(e), + "validation_error": str(e), + }) prompt = validation["generation_prompt"] result = { "combined_prompt": prompt,