Mixed mode: skip frame overlap, keep one chain run

Mixed i2v anchors no longer blend the previous scene tail into the
starting chunk or the stitched output. Dragging an image onto a
continuation scene auto-sets mixed so montage stays one queue request.
Preview in-points skip overlap before mixed scenes.
This commit is contained in:
digital-garbage
2026-06-10 15:30:14 +03:00
parent a5dd5579c6
commit 682fe95bb3
3 changed files with 62 additions and 15 deletions
+19 -6
View File
@@ -665,9 +665,9 @@
state.sceneGhosts = (state.sceneGhosts || []).filter((g) => !ids.has(g.afterSceneId) && !ids.has(g.id));
}
// Pixel overlap between consecutive chain-sampler scenes (carry/i2v guide runs).
function _chainOverlapSec(runSceneCount) {
if (!runSceneCount || runSceneCount < 2) return 0;
// Pixel overlap between two consecutive chain scenes (0 when the next scene is mixed).
function _overlapBetweenScenes(prev, curr) {
if (!prev || !curr || (curr.source?.type) === "mixed") return 0;
const frames = +(state.project?.sampler_inputs?.frame_overlap ?? 16);
const fps = state.project?.frame_rate || 25;
return Math.max(0, frames / fps);
@@ -676,7 +676,6 @@
function _recordSegment(mediaList, targetSceneIds) {
if (!mediaList || !mediaList.length || !targetSceneIds || !targetSceneIds.length) return;
const primary = mediaList.find((m) => m.kind === "videos" || m.kind === "gifs") || mediaList[0];
const overlapSec = _chainOverlapSec(targetSceneIds.length);
let sourceEnd = 0;
let clearedRating = false;
_pruneGhostsAfterRegen(targetSceneIds);
@@ -685,7 +684,7 @@
const sc = scene(id); if (!sc) continue;
const prev = i > 0 ? scene(targetSceneIds[i - 1]) : null;
const sameUnit = prev && genUnitId(prev) === genUnitId(sc);
const inSec = i === 0 ? 0 : (sameUnit ? sourceEnd : Math.max(0, sourceEnd - overlapSec));
const inSec = i === 0 ? 0 : (sameUnit ? sourceEnd : Math.max(0, sourceEnd - _overlapBetweenScenes(prev, sc)));
state.sceneRenders[id] = { media: primary, inSec };
// New render invalidates the prior RLHF rating — UI shows "— rate —"; server
// uses continue-refinement (session memory, no new rating) on the next Studio run.
@@ -1015,9 +1014,23 @@
const t = (s.text || "").trim();
patchScene(s.id, { text: t ? `${t} ${trigger}` : trigger }); return true;
}
// True when this scene continues the current chain run (not a new run anchor).
function _continuesChainRun(s) {
const active = state.project.scenes.filter((sc) => !sc.excluded);
const idx = active.findIndex((sc) => sc.id === s.id);
if (idx <= 0) return false;
const t = (s.source && s.source.type) || "empty";
if (t === "empty") return false;
if (isGenSubclip(s)) return true;
if (t === "carry" || t === "mixed") return true;
if ((t === "image" || t === "generated_frame") && _anchorAvailable(s)) return false;
return true;
}
function assignMediaToScene(sceneId, mediaId) {
const s = scene(sceneId); if (!s) return;
const t = (s.source?.type === "mixed") ? "mixed" : "image";
const t = (s.source?.type === "mixed") ? "mixed"
: (_continuesChainRun(s) ? "mixed" : "image");
patchScene(sceneId, { source: { ...(s.source || {}), type: t, media_ref: mediaId } });
}
+5 -9
View File
@@ -3080,7 +3080,7 @@ class FunPackLTXAVSceneChainSampler:
return out if out is not None else chunk
def _build_mixed_anchor_chunk(self, vae, anchor_meta, latent_template, previous, video_overlap):
"""Mixed source: Img2Video anchor latent, then chain overlap from previous scene."""
"""Mixed source: Img2Video anchor latent only — prior scene overlap is not used."""
filename = (anchor_meta or {}).get("filename")
strength = float((anchor_meta or {}).get("strength", 1.0))
image = self._load_image_tensor(filename) if filename else None
@@ -3089,10 +3089,7 @@ class FunPackLTXAVSceneChainSampler:
return self._clone_latent(latent_template)
return self._build_continuation_chunk(latent_template, previous, video_overlap)
base = self._clone_latent(latent_template)
i2v_chunk = self._apply_img2video_to_video_latent(vae, image, base, strength)
if previous is None:
return i2v_chunk
return self._build_continuation_chunk(i2v_chunk, previous, video_overlap)
return self._apply_img2video_to_video_latent(vae, image, base, strength)
def _encode_image_guide_frame(self, filename, vae, ref_tensor):
import os
@@ -3389,10 +3386,8 @@ class FunPackLTXAVSceneChainSampler:
"pixel_frame": max(0, boundary_pixel),
"effect": effect if effect and transition_duration > 0 else None,
})
if video_overlap > 0:
run_mechanisms.append(f"latent_overlap({frame_overlap}px)")
chunk = self._build_mixed_anchor_chunk(
vae, anchor_meta, latent_template, output, video_overlap,
vae, anchor_meta, latent_template, output, 0,
)
else:
# Record boundary before blending
@@ -3480,7 +3475,8 @@ class FunPackLTXAVSceneChainSampler:
if guide_tail > 0:
sampled = self._crop_video_tail(sampled, guide_tail)
scene_outputs.append(self._clone_latent(sampled))
output = sampled if output is None else self._blend_latents(output, sampled, video_overlap)
blend_overlap = 0 if anchor_meta else video_overlap
output = sampled if output is None else self._blend_latents(output, sampled, blend_overlap)
cumulative_latent_frames = self._tensor_frames(self._latent_tensors(output)[0])
scene_meta = self._scene_meta(scene_cond, scene_index)
scene_runs.append({
+38
View File
@@ -476,3 +476,41 @@ def test_overlap_diagnostics_report_latent_blend_zone():
assert parsed["scene_count"] == 2
assert "boundaries" in parsed
assert "overlap_blend=2px" in status
def test_mixed_anchor_skips_frame_overlap():
import json
sample_calls.clear()
node = FunPackLTXAVSceneChainSampler()
node._load_image_tensor = lambda _fn: torch.ones(1, 8, 8, 3)
node._apply_img2video_to_video_latent = lambda _vae, _img, chunk, _strength: node._clone_latent(chunk)
positive = [scene_cond(0), scene_cond(1)]
latent_template = {"samples": torch.zeros(1, 2, 5, 3, 3)}
anchors = json.dumps({"1": {"filename": "anchor.png", "strength": 1.0}})
latent, _images, status, scene_count, _report, boundaries_json = node.sample(
model=object(),
vae=FakeVAE(),
positive=positive,
negative=[],
sampler=object(),
sigmas=torch.tensor([1.0, 0.0]),
seed=80,
latent_template=latent_template,
num_frames_per_scene=5,
frame_overlap=2,
cfg=1.0,
max_scenes=2,
funpack_scene_anchors=anchors,
prompt={"n1": {"inputs": {}}},
unique_id="test-node",
)
assert scene_count == 2
assert latent["samples"].shape[2] == 10
parsed = json.loads(boundaries_json)
mechs = parsed["scenes"][1]["mechanisms"]
assert "mixed_i2v_anchor" in mechs
assert not any("latent_overlap" in m for m in mechs)