Fix identity pin breaking generation on carry-only runs.
Wire funpack_scene_media_refs even without mixed anchors, keep guide-only pin images off the run anchor LoadImage, and return 400 when pin media is missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a434ed0d15
commit
5d540d43bc
+38
-11
@@ -200,7 +200,7 @@ def _prepare_media(proj: Project, extra_refs: Optional[list] = None) -> Optional
|
||||
by_scene: dict = {}
|
||||
seen_refs: set = set()
|
||||
|
||||
def _add_ref(ref: str, tgt=None, sid: Optional[str] = None) -> None:
|
||||
def _add_ref(ref: str, tgt=None, sid: Optional[str] = None, *, set_primary: bool = True) -> None:
|
||||
nonlocal primary
|
||||
if not ref or ref in seen_refs:
|
||||
return
|
||||
@@ -213,7 +213,7 @@ def _prepare_media(proj: Project, extra_refs: Optional[list] = None) -> Optional
|
||||
by_scene[sid] = entry
|
||||
else:
|
||||
by_scene[f"_ref_{ref}"] = entry
|
||||
if primary is None:
|
||||
if set_primary and primary is None:
|
||||
primary = {"filename": fn, "target": tgt}
|
||||
|
||||
for sc in proj.scenes:
|
||||
@@ -225,9 +225,10 @@ def _prepare_media(proj: Project, extra_refs: Optional[list] = None) -> Optional
|
||||
stype = getattr(src, "type", "") if src else ""
|
||||
if stype not in ("image", "generated_frame", "mixed") or not ref:
|
||||
continue
|
||||
_add_ref(ref, tgt, sc.id)
|
||||
_add_ref(ref, tgt, sc.id, set_primary=True)
|
||||
for ref in (extra_refs or []):
|
||||
_add_ref(str(ref))
|
||||
# Guide-only assets (identity pin, prior-scene borrow) must not become the run anchor.
|
||||
_add_ref(str(ref), set_primary=False)
|
||||
if not by_scene:
|
||||
return None
|
||||
return {"primary": primary, "by_scene": by_scene}
|
||||
@@ -410,11 +411,37 @@ def _run_sampler_inputs(target: Project, scene_count: int, full: Optional[Projec
|
||||
return samp
|
||||
|
||||
|
||||
def _scene_media_ref_map(media_pack: Optional[dict]) -> dict[str, str]:
|
||||
if not media_pack:
|
||||
return {}
|
||||
by_scene = media_pack.get("by_scene") or {}
|
||||
return {
|
||||
entry["media_ref"]: entry["filename"]
|
||||
for entry in by_scene.values()
|
||||
if isinstance(entry, dict) and entry.get("media_ref") and entry.get("filename")
|
||||
}
|
||||
|
||||
|
||||
def _missing_continuity_media(full: Project, target: Project) -> list[str]:
|
||||
"""Media-bin ids referenced by continuity guides/pins that are missing on disk."""
|
||||
from .backend.timeline import continuity_media_refs
|
||||
|
||||
missing: list[str] = []
|
||||
for ref in continuity_media_refs(full, target):
|
||||
if not media.path_for(ref):
|
||||
missing.append(ref)
|
||||
return missing
|
||||
|
||||
|
||||
def _attach_scene_anchors(samp: dict, media_pack: Optional[dict], target: Project) -> dict:
|
||||
"""Mixed-source i2v anchors (Img2Video starting latents), separate from guides."""
|
||||
import json
|
||||
from .backend.timeline import build_scene_anchors_payload
|
||||
|
||||
by_ref = _scene_media_ref_map(media_pack)
|
||||
if by_ref:
|
||||
samp["funpack_scene_media_refs"] = json.dumps(by_ref)
|
||||
|
||||
anchors = build_scene_anchors_payload(target)
|
||||
if not anchors or not media_pack:
|
||||
return samp
|
||||
@@ -428,13 +455,6 @@ def _attach_scene_anchors(samp: dict, media_pack: Optional[dict], target: Projec
|
||||
enriched[idx] = {**meta, "filename": fn}
|
||||
if enriched:
|
||||
samp["funpack_scene_anchors"] = json.dumps(enriched)
|
||||
by_ref = {
|
||||
entry["media_ref"]: entry["filename"]
|
||||
for entry in by_scene.values()
|
||||
if isinstance(entry, dict) and entry.get("media_ref") and entry.get("filename")
|
||||
}
|
||||
if by_ref:
|
||||
samp["funpack_scene_media_refs"] = json.dumps(by_ref)
|
||||
return samp
|
||||
|
||||
|
||||
@@ -950,6 +970,13 @@ if web is not None and PromptServer is not None:
|
||||
{"detail": "Nothing to generate — all clips are video-only or excluded."},
|
||||
status=400,
|
||||
)
|
||||
missing_media = _missing_continuity_media(p, target)
|
||||
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."},
|
||||
status=400,
|
||||
)
|
||||
# V1 uniform chain: if trimmed scenes all agree on a frame count, use it.
|
||||
# This makes the timeline trim handle actually affect generation length,
|
||||
# provided the user has linked EmptyLTXVLatent.num_frames → Project Frames
|
||||
|
||||
@@ -126,6 +126,53 @@ def test_carry_chain_auto_mid_scene_guide():
|
||||
assert guides["scenes"][1][0]["source"] == "template"
|
||||
|
||||
|
||||
def test_identity_pin_media_refs_without_mixed_anchors():
|
||||
import json
|
||||
from movie_editor.server import _attach_scene_anchors
|
||||
|
||||
full = _project(
|
||||
continuity_settings={"identity_pin_ref": "pin1"},
|
||||
scenes=[{"id": "s1", "text": "a"}, {"id": "s2", "text": "b", "source": {"type": "carry"}}],
|
||||
)
|
||||
samp = _run_sampler_inputs(full, 2, full=full)
|
||||
assert "funpack_scene_guides" in samp
|
||||
media_pack = {
|
||||
"primary": None,
|
||||
"by_scene": {"_ref_pin1": {"filename": "funpack_movie_pin1.png", "media_ref": "pin1"}},
|
||||
}
|
||||
_attach_scene_anchors(samp, media_pack, full)
|
||||
refs = json.loads(samp["funpack_scene_media_refs"])
|
||||
assert refs["pin1"] == "funpack_movie_pin1.png"
|
||||
assert "funpack_scene_anchors" not in samp
|
||||
|
||||
|
||||
def test_prepare_media_extra_refs_do_not_become_primary(monkeypatch, tmp_path):
|
||||
from movie_editor import server
|
||||
from movie_editor.backend import config, media
|
||||
|
||||
indir = tmp_path / "input"
|
||||
indir.mkdir()
|
||||
mediadir = tmp_path / "media"
|
||||
mediadir.mkdir()
|
||||
(mediadir / "pin1.png").write_bytes(b"fake")
|
||||
|
||||
monkeypatch.setattr(config, "MEDIA_DIR", mediadir)
|
||||
monkeypatch.setattr(media, "path_for", lambda mid: mediadir / "pin1.png" if mid == "pin1" else None)
|
||||
|
||||
class FP:
|
||||
@staticmethod
|
||||
def get_input_directory():
|
||||
return str(indir)
|
||||
|
||||
monkeypatch.setitem(__import__("sys").modules, "folder_paths", FP)
|
||||
|
||||
p = _project(scenes=[{"id": "s1", "text": "a", "source": {"type": "carry"}}])
|
||||
pack = server._prepare_media(p, ["pin1"])
|
||||
assert pack is not None
|
||||
assert pack["primary"] is None
|
||||
assert pack["by_scene"]["_ref_pin1"]["filename"].startswith("funpack_movie_pin1")
|
||||
|
||||
|
||||
def test_studio_inputs_skips_custom_conditioning():
|
||||
p = _project([])
|
||||
p.conditioning_slot = "custom"
|
||||
|
||||
Reference in New Issue
Block a user