diff --git a/movie_editor/backend/timeline.py b/movie_editor/backend/timeline.py index 49117532..28b8022e 100644 --- a/movie_editor/backend/timeline.py +++ b/movie_editor/backend/timeline.py @@ -325,24 +325,38 @@ def build_auto_continuity_guides(full: Project, target: Project) -> Optional[dic def continuity_media_refs(full: Project, target: Project) -> list[str]: - """Extra media-bin ids to copy for auto continuity (pin + prior anchors).""" + """Media-bin ids that must exist for the active continuity/guide path.""" cs = continuity_settings_for_run(full) char_map = _load_character_map() - refs: list[str] = list(character_media_refs_for_project(full, char_map)) - if cs["auto_enabled"] and cs["identity_pin_ref"]: - refs.append(cs["identity_pin_ref"]) - for sc in full.scenes: - if sc.excluded: - continue - pin = resolve_scene_identity_pin(sc, char_map, cs) - if pin: - refs.append(pin) - payload = build_auto_continuity_guides(full, target) - if payload: - for scene_list in payload.get("scenes") or []: - for g in scene_list or []: - if g.get("source") == "image" and g.get("media_ref"): - refs.append(g["media_ref"]) + refs: list[str] = [] + + if cs["auto_enabled"]: + refs.extend(character_media_refs_for_project(full, char_map)) + if cs["identity_pin_ref"]: + refs.append(cs["identity_pin_ref"]) + for sc in full.scenes: + if sc.excluded: + continue + pin = resolve_scene_identity_pin(sc, char_map, cs) + if pin: + refs.append(pin) + payload = build_auto_continuity_guides(full, target) + if payload: + for scene_list in payload.get("scenes") or []: + for g in scene_list or []: + if g.get("source") == "image" and g.get("media_ref"): + refs.append(g["media_ref"]) + + gs = normalize_guide_settings(full.guide_settings) + if gs["stack_enabled"]: + for sc in full.scenes: + if sc.excluded: + continue + for raw in (sc.guides or []): + g = GuideEntry.from_dict(raw if isinstance(raw, dict) else {}) + if g.enabled and g.source == "image" and g.media_ref: + refs.append(g.media_ref) + return list(dict.fromkeys(refs)) diff --git a/movie_editor/server.py b/movie_editor/server.py index 0b46b64a..d087eba8 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -424,7 +424,7 @@ def _scene_media_ref_map(media_pack: Optional[dict]) -> dict[str, str]: def _missing_continuity_media(full: Project, target: Project) -> list[str]: - """Media-bin ids referenced by continuity guides/pins that are missing on disk.""" + """Media-bin ids referenced by active continuity or manual guide stacks.""" from .backend.timeline import continuity_media_refs missing: list[str] = [] @@ -1171,7 +1171,7 @@ if web is not None and PromptServer is not None: if missing_media: ids = ", ".join(missing_media) return web.json_response( - {"detail": f"Missing media for identity pin or continuity guides ({ids}). Re-upload or pick another image."}, + {"detail": f"Missing media for continuity or guide stack ({ids}). Re-upload or clear the reference in Engine settings / Scene inspector."}, status=400, ) # V1 uniform chain: if trimmed scenes all agree on a frame count, use it. diff --git a/movie_editor/tests/test_timeline.py b/movie_editor/tests/test_timeline.py index dc9747cb..45ed0b48 100644 --- a/movie_editor/tests/test_timeline.py +++ b/movie_editor/tests/test_timeline.py @@ -368,6 +368,16 @@ def test_character_bible_refs_in_continuity_media(): ) refs = continuity_media_refs(p, p) assert refs == ["f1", "b1", "d1"] + p.continuity_settings = {"auto_enabled": False} + assert continuity_media_refs(p, p) == [] + + +def test_stale_identity_pin_ignored_when_auto_off(): + p = _project( + continuity_settings={"auto_enabled": False, "identity_pin_ref": "stale_pin"}, + scenes=[{"text": "a"}, {"text": "b", "source": {"type": "carry"}}], + ) + assert continuity_media_refs(p, p) == [] def test_scene_character_media_refs(monkeypatch):