Movie Editor: fix multi-scene generation — always emit separator before each scene

build_combined_prompt had a bug: when no anchor text was set, need_sep was
False for scene 1 so its text landed in Studio's segments[0] (anchor slot)
and never generated. An N-scene project with no anchor only produced N-1 scenes.

Fix: always emit a separator before every scene. Studio splits on separators,
so segments[0] is always the anchor (empty if no anchor text is set) and
segments[1..N] are the generated scenes. Fallback label is now "scene {i+1}"
(correctly numbered) instead of the previous "scene {label_n+1}" off-by-one.

Also: pass max_scenes = active scene count from the generate route into the
builder so the chain sampler always knows the exact number of scenes to produce.

Note on per-scene images: only the source image on scene 1 (project anchor
image) is wired to studio.source_image. Scene 2+ anchoring works via the
carry_i2v_guides flag on the chain sampler, which carries each scene's last
frame as the i2v source for the next scene. Per-scene i2v requires carry_i2v_guides
to be enabled in the sampler inspector — it is off by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-07 00:30:59 +03:00
co-authored by Claude Sonnet 4.6
parent de52c789fc
commit 3c8dda0f5c
3 changed files with 14 additions and 11 deletions
+2
View File
@@ -218,6 +218,8 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
graph["fps"]["inputs"]["value"] = params["frame_rate"]
if params.get("seed") is not None:
graph["sampler"]["inputs"]["seed"] = params["seed"]
if params.get("max_scenes") is not None:
graph["sampler"]["inputs"]["max_scenes"] = params["max_scenes"]
# the result must be written to the output dir so the editor can fetch it back
# (the reference graph used preview-only save_output=False).
graph["vhs"]["inputs"]["save_output"] = True
+10 -11
View File
@@ -162,19 +162,18 @@ def build_combined_prompt(project: Project, include_excluded: bool = False) -> s
parts.append(anchor)
scenes = [s for s in project.scenes if include_excluded or not s.excluded]
label_n = 1
prev_marker = (project.intro_transition or "").strip() if anchor else ""
# Always emit a separator marker before every scene. Studio's split puts
# segments[0] as the character anchor and segments[1..N] as generated scenes.
# Without a separator before scene 1, scene 1's text would land in segments[0]
# (the anchor slot) and be skipped for generation — only N-1 scenes would be
# produced for an N-scene project.
prev_marker = (project.intro_transition or "").strip()
for i, scene in enumerate(scenes):
text = (scene.text or "").strip()
# A separator is needed before this scene if there is preceding content
# (the anchor, or an earlier scene) to split away from.
need_sep = bool(anchor) or i > 0
if need_sep:
marker = prev_marker
if not marker:
marker = f"scene {label_n + 1}"
parts.append(marker)
label_n += 1
marker = prev_marker
if not marker:
marker = f"scene {i + 1}"
parts.append(marker)
if text:
parts.append(text)
prev_marker = (scene.transition_to_next or "").strip()
+2
View File
@@ -369,12 +369,14 @@ if web is not None and PromptServer is not None:
oi = await bridge.object_info()
except Exception as e: # noqa: BLE001
return web.json_response({"detail": f"Node registry unavailable: {e}"}, status=502)
active_scene_count = len([s for s in target.scenes if not s.excluded])
graph, report = builder.build(oi, nodes.load_models(), {
"prompt": prompt, "seed": target.seed,
"num_frames_per_scene": target.num_frames_per_scene,
"frame_rate": target.frame_rate,
"width": target.width, "height": target.height,
"negative_prompt": target.negative_prompt or None,
"max_scenes": active_scene_count,
"studio_inputs": target.studio_inputs if target.conditioning_slot == "funpack" else {},
"sampler_inputs": target.sampler_inputs if target.sampler_slot == "funpack" else {},
}, media=_prepare_media(target))