From e8f0b0f83f8b3877990ffa8a49eece0cc401a8f5 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Wed, 20 May 2026 19:29:33 +0300 Subject: [PATCH] Remove standalone scene transition nodes --- __init__.py | 10 - conditioning.py | 101 ++------ context_transition.py | 220 ------------------ docs/FunPackContextTransitionWindows.md | 45 ---- docs/FunPackHybridEuler2SSampler.md | 2 - ...ck_LTX23_SceneBuilder_Advisor_Updated.json | 73 +----- example_workflows/LTXFunPackRefinerV2.json | 72 +----- funpack.py | 5 - tests/test_v2_category_alignment.py | 18 +- web/funpack_studio.js | 5 +- 10 files changed, 31 insertions(+), 520 deletions(-) delete mode 100644 context_transition.py delete mode 100644 docs/FunPackContextTransitionWindows.md diff --git a/__init__.py b/__init__.py index 9d10a42a..af2e12b0 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,6 @@ if __package__: ) from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader from .samplers import FunPackHybridEuler2SSampler, FunPackDistilledFlowSampler - from .context_transition import FunPackSceneCutWindows, FunPackSceneNoise from .templates import FunPackRefinementKeyLoader, FunPackSceneBuilder else: # Standalone tests may not have the full ComfyUI/CUDA runtime loaded. @@ -60,11 +59,6 @@ else: except Exception: FunPackHybridEuler2SSampler = None FunPackDistilledFlowSampler = None - try: - from context_transition import FunPackSceneCutWindows, FunPackSceneNoise - except Exception: - FunPackSceneCutWindows = None - FunPackSceneNoise = None try: from templates import FunPackRefinementKeyLoader, FunPackSceneBuilder except Exception: @@ -90,8 +84,6 @@ NODE_CLASS_MAPPINGS = { "FunPackSaveRefinementLatent": FunPackSaveRefinementLatent, "FunPackHybridEuler2SSampler": FunPackHybridEuler2SSampler, "FunPackDistilledFlowSampler": FunPackDistilledFlowSampler, - "FunPackSceneCutWindows": FunPackSceneCutWindows, - "FunPackSceneNoise": FunPackSceneNoise, "FunPackApplyLoraWeights": FunPackApplyLoraWeights, "FunPackLoraLoader": FunPackLoraLoader, "FunPackRefinementKeyLoader": FunPackRefinementKeyLoader, @@ -116,8 +108,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FunPackSaveRefinementLatent": "FunPack Save Refinement Latent", "FunPackHybridEuler2SSampler": "FunPack Hybrid Euler 2S Sampler", "FunPackDistilledFlowSampler": "FunPack Distilled Flow Sampler", - "FunPackSceneCutWindows": "FunPack Scene Cut Windows", - "FunPackSceneNoise": "FunPack Scene Noise", "FunPackApplyLoraWeights": "FunPack Apply LoRA Weights", "FunPackLoraLoader": "FunPack LoRA Loader", "FunPackRefinementKeyLoader": "FunPack Refinement Key Loader", diff --git a/conditioning.py b/conditioning.py index b78620c9..8be0245c 100644 --- a/conditioning.py +++ b/conditioning.py @@ -5841,7 +5841,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): "split_by_transitions": ("BOOLEAN", { "default": False, "label": "Split by Transitions", - "tooltip": "Detect transition words in the prompt (e.g. 'then', 'suddenly', 'cut to') and encode each scene segment as a separate conditioning entry. The character description (text before the first comma) is prepended to every segment. Designed to work with FunPack Context Transition Windows using split_conds_to_windows.", + "tooltip": "Detect transition words in the prompt and preview scene segments in the encoded prompts output. The refiner still returns one full-prompt conditioning entry.", }), "latent": ("LATENT", { "tooltip": "Optional video latent for creativity masking. Takes priority over any saved latent for this key. Connect your i2v or previous KSampler output here.", @@ -6108,82 +6108,21 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): return segments if len(segments) > 1 else [text] - def _v2_contrastive_cond(self, scene_cond, general_cond, contrast): - """Push scene_cond further away from general_cond by the contrast factor. - - Formula: general + contrast × (scene − general) - At contrast=1.0 → plain scene_cond (no change). - At contrast=2.0 → 2×scene − general (classic contrastive). - - The character description is shared between scene_cond and general_cond, - so it cancels in the delta. The push focuses on what is unique to the scene. - Returns scene_cond unchanged if shapes mismatch or contrast is 1.0. - """ - if contrast == 1.0 or not isinstance(general_cond, torch.Tensor): - return scene_cond - try: - if scene_cond.shape != general_cond.shape: - return scene_cond - gen = general_cond.to(dtype=scene_cond.dtype, device=scene_cond.device) - return gen + contrast * (scene_cond - gen) - except Exception: - return scene_cond - - def _v2_encode_per_window_conditionings(self, clip, segments, general_cond=None, - contrast=1.0, encode_cache=None): - """Encode one conditioning entry per scene window. - - segments[0] is split into character description + first-scene context. - Each subsequent segment is combined with the character description to form - that window's conditioning. - - When general_cond (the full-prompt conditioning) and contrast > 1.0 are - provided, each window's conditioning is pushed further toward its unique - scene content and away from the shared baseline via: - window_i = general + contrast × (scene_i − general) - - Returns (conditionings, window_texts) where conditionings is a list of - (tensor, meta) pairs and window_texts is the prompt strings per window. - Returns (None, []) on failure. - """ - if not segments or clip is None: - return None, [] + def _v2_transition_scene_texts(self, segments): + if not segments: + return [] if len(segments) == 1: - cond, meta, _ = self._v2_encode_prompt(clip, segments[0], encode_cache=encode_cache) - if not isinstance(cond, torch.Tensor): - return None, [] - return [(cond, meta)], [segments[0]] + return [segments[0]] - # The entire first segment (everything before the first transition word) is the anchor. - # Splitting at the first comma within it breaks prompts like - # "In this anime video, Character Name is a..." where the comma is inside the description. + # Use the whole first segment as the anchor. Splitting at the first comma + # breaks prompts like "In this anime video, Character Name is a...". anchor = segments[0].strip() - conditionings = [] - window_texts = [] - - cond0_raw, meta0, _ = self._v2_encode_prompt(clip, anchor, encode_cache=encode_cache) - if not isinstance(cond0_raw, torch.Tensor): - return None, [] - cond0 = self._v2_contrastive_cond(cond0_raw, general_cond, contrast) - conditionings.append((cond0, meta0)) - window_texts.append(anchor) - + scene_texts = [anchor] for seg in segments[1:]: seg = seg.strip() - if not seg: - continue - combined = anchor + ", " + seg - cond_i_raw, meta_i, _ = self._v2_encode_prompt(clip, combined, encode_cache=encode_cache) - if isinstance(cond_i_raw, torch.Tensor): - cond_i = self._v2_contrastive_cond(cond_i_raw, general_cond, contrast) - conditionings.append((cond_i, meta_i)) - else: - conditionings.append((cond0, meta0)) - window_texts.append(combined) - - if len(conditionings) <= 1: - return [(cond0, meta0)], [anchor] - return conditionings, window_texts + if seg: + scene_texts.append(anchor + ", " + seg) + return scene_texts def _v2_conditioning_vector(self, conditioning): if not isinstance(conditioning, torch.Tensor) or conditioning.dim() <= 1: @@ -11221,22 +11160,20 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # When split_by_transitions is on, detect scene segments and show them in # encoded_prompts for transparency, but output a SINGLE full-prompt conditioning. - # Multi-entry conditioning without a working per-window routing mechanism just - # accumulates all scene conditionings onto every frame, which confuses the model. + # Multi-entry conditioning accumulates all scene conditionings onto every frame, + # which confuses the model. # The transition words in the full prompt already guide the model's natural scene cuts. output_conditioning = [(refined, meta)] - if split_by_transitions and clip is not None: + if split_by_transitions: try: segments = self._v2_split_prompt_by_transitions(prompt_to_encode) if len(segments) > 1: - _, window_texts = self._v2_encode_per_window_conditionings( - clip, segments, encode_cache=encode_cache, - ) - if window_texts: + scene_texts = self._v2_transition_scene_texts(segments) + if scene_texts: status_suffix = f"\nTransition split: {len(segments)} scenes detected" status = status + status_suffix + enhancement_status - window_lines = "\n".join( - f" Scene {i + 1}: {t}" for i, t in enumerate(window_texts) + scene_lines = "\n".join( + f" Scene {i + 1}: {t}" for i, t in enumerate(scene_texts) ) split_encoded_prompts = ( self._v2_encoded_prompts_output( @@ -11245,7 +11182,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): pre_advisor_prompt=pre_advisor_prompt, advisor_suggested=advisor_suggested, ) - + f"\n\nDetected scenes (for FunPack Scene Noise):\n{window_lines}" + + f"\n\nDetected scenes:\n{scene_lines}" ) return ( output_conditioning, diff --git a/context_transition.py b/context_transition.py deleted file mode 100644 index 8ee15cc0..00000000 --- a/context_transition.py +++ /dev/null @@ -1,220 +0,0 @@ -import logging - -import torch -import comfy.ldm.modules.attention as _attn_mod - - -# --------------------------------------------------------------------------- -# Scene-cut attention masking -# --------------------------------------------------------------------------- - -def _scene_split_self_attn(orig_fn, q, k, v, heads, num_scenes, cache, *args, **kwargs): - """Run self-attention in per-scene chunks so tokens in different scenes never attend each other. - - Scene 0's tokens are also available as key/value to all other scenes. - This preserves i2v guide-frame attention: the reference image lives in scene 0's - temporal region, and later scenes need to see it for visual consistency. - """ - seq_len = q.shape[1] - cache_key = (seq_len, num_scenes, q.device.type) - - if cache_key not in cache: - tokens_per_scene = max(1, seq_len // num_scenes) - ids = torch.arange(seq_len, device=q.device) - cache[cache_key] = (ids // tokens_per_scene).clamp(max=num_scenes - 1) - - scene_ids = cache[cache_key] - scene0_mask = (scene_ids == 0) - out = torch.zeros_like(q) - - for scene_id in range(num_scenes): - query_mask = (scene_ids == scene_id) - if not query_mask.any(): - continue - # Each scene queries its own tokens; keys/values come from own tokens + scene 0 - kv_mask = query_mask | scene0_mask if scene_id > 0 else query_mask - out[:, query_mask, :] = orig_fn( - q[:, query_mask, :], k[:, kv_mask, :], v[:, kv_mask, :], heads, *args, **kwargs - ) - - return out - - -class FunPackSceneCutHandler: - """Reads num_scenes from conditioning on each step; never activates context windows.""" - - def __init__(self): - self.num_scenes = 1 - - def should_use_context(self, model, conds, x_in, timestep, model_options): - try: - self.num_scenes = max((len(c) for c in conds if c), default=1) - except Exception: - self.num_scenes = 1 - logging.info( - "FunPack scene cut: %s scene(s) detected from conditioning.", - self.num_scenes, - ) - return False # full-pass denoising; masking applied inside each forward - - -class FunPackSceneCutWindows: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model": ("MODEL", { - "tooltip": "The model to apply scene-boundary attention masking to.", - }), - }, - } - - RETURN_TYPES = ("MODEL",) - RETURN_NAMES = ("model",) - FUNCTION = "apply" - CATEGORY = "FunPack/Sampling" - DESCRIPTION = ( - "Enforces scene cuts by blocking temporal self-attention across scene boundaries. " - "All frames are denoised in one forward pass (full temporal attention preserved " - "within each scene). Works for both LTXV and LTXAV. " - "Connect after Refiner V2 or Studio with 'Split by Transitions' enabled. " - "Scene count is auto-detected from the number of conditioning entries." - ) - - def apply(self, model): - model = model.clone() - attn_mask_cache = {} - - handler = FunPackSceneCutHandler() - model.model_options["context_handler"] = handler - - old_wrapper = model.model_options.get("model_function_wrapper") - - def _scene_wrapper(apply_fn, args, _old=old_wrapper): - num_scenes = handler.num_scenes - - if num_scenes <= 1: - if _old is not None: - return _old(apply_fn, args) - return apply_fn(args["input"], args["timestep"], **args.get("c", {})) - - orig_attn = _attn_mod.optimized_attention - orig_attn_masked = _attn_mod.optimized_attention_masked - - def _patched(q, k, v, heads, *a, **kw): - if q.shape[1] == k.shape[1]: - return _scene_split_self_attn(orig_attn, q, k, v, heads, num_scenes, attn_mask_cache, *a, **kw) - return orig_attn(q, k, v, heads, *a, **kw) - - def _patched_masked(q, k, v, heads, mask, *a, **kw): - if q.shape[1] == k.shape[1]: - return _scene_split_self_attn(orig_attn_masked, q, k, v, heads, num_scenes, attn_mask_cache, *a, **kw) - return orig_attn_masked(q, k, v, heads, mask, *a, **kw) - - _attn_mod.optimized_attention = _patched - _attn_mod.optimized_attention_masked = _patched_masked - try: - if _old is not None: - result = _old(apply_fn, args) - else: - result = apply_fn(args["input"], args["timestep"], **args.get("c", {})) - finally: - _attn_mod.optimized_attention = orig_attn - _attn_mod.optimized_attention_masked = orig_attn_masked - - return result - - model.model_options["model_function_wrapper"] = _scene_wrapper - return (model,) - - -# --------------------------------------------------------------------------- -# Seeded temporal noise: reinforce scene cuts via distinct noise per region -# --------------------------------------------------------------------------- - -class FunPackSceneNoise: - """Generate initial noise with different seeds per scene temporal region. - - For t2v: connect an empty latent to 'latent', output goes to - KSamplerAdvanced latent_image with add_noise=disable. - - For i2v: connect your image latent to BOTH 'latent' AND 'image_latent'. - The scene noise is added on top of the image latent so the reference - image is preserved. Use with KSamplerAdvanced add_noise=disable. - - Scene count is auto-detected from the 'positive' conditioning entries. - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "latent": ("LATENT", {"tooltip": "Defines shape/device/dtype. For i2v also connect your image latent to image_latent."}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - }, - "optional": { - "positive": ("CONDITIONING", {"tooltip": "Auto-detects scene count from conditioning entries."}), - "image_latent": ("LATENT", {"tooltip": "i2v only: image latent to blend scene noise onto. Scene noise is added on top so the reference image is preserved."}), - }, - } - - RETURN_TYPES = ("LATENT",) - RETURN_NAMES = ("noise",) - FUNCTION = "generate" - CATEGORY = "FunPack/Sampling" - - def generate(self, latent, seed, positive=None, image_latent=None): - samples = latent["samples"] - num_scenes = max(1, len(positive)) if positive else 1 - - def _make_noise(shape, s): - rng = torch.Generator(device=samples.device) - rng.manual_seed(s) - return torch.randn(shape, generator=rng, dtype=samples.dtype, device=samples.device) - - if num_scenes <= 1: - base_noise = _make_noise(samples.shape, seed) - if image_latent is not None: - img = image_latent["samples"].to(dtype=samples.dtype, device=samples.device) - if img.shape == base_noise.shape: - base_noise = img + base_noise - return ({"samples": base_noise},) - - temporal_dim = None - for d in [2, 1, 0] + list(range(3, samples.dim())): - if 1 < int(samples.size(d)) <= 4096: - temporal_dim = d - break - - if temporal_dim is None: - base_noise = _make_noise(samples.shape, seed) - if image_latent is not None: - img = image_latent["samples"].to(dtype=samples.dtype, device=samples.device) - if img.shape == base_noise.shape: - base_noise = img + base_noise - return ({"samples": base_noise},) - - total_frames = int(samples.size(temporal_dim)) - noise = torch.zeros_like(samples) - scene_size = total_frames // num_scenes - - for i in range(num_scenes): - scene_seed = (seed + i * 31337) & 0xffffffffffffffff - start = i * scene_size - end = (i + 1) * scene_size if i < num_scenes - 1 else total_frames - slices = [slice(None)] * samples.dim() - slices[temporal_dim] = slice(start, end) - scene_shape = list(samples.shape) - scene_shape[temporal_dim] = end - start - noise[tuple(slices)] = _make_noise(scene_shape, scene_seed) - - if image_latent is not None: - img = image_latent["samples"].to(dtype=samples.dtype, device=samples.device) - if img.shape == noise.shape: - noise = img + noise - - logging.info( - "FunPack scene noise: %s scenes, temporal_dim=%s, total_frames=%s, i2v=%s.", - num_scenes, temporal_dim, total_frames, image_latent is not None, - ) - return ({"samples": noise},) diff --git a/docs/FunPackContextTransitionWindows.md b/docs/FunPackContextTransitionWindows.md deleted file mode 100644 index 0e085df6..00000000 --- a/docs/FunPackContextTransitionWindows.md +++ /dev/null @@ -1,45 +0,0 @@ -# FunPack Context Transition Windows - -`FunPack Context Transition Windows` is a model wrapper for forcing video context resets without changing sampler sigmas or adding latent noise. It borrows Comfy's manual context-window mechanism, then splits the window schedule at transition boundaries so the model stops seeing the entire previous segment when denoising the next one. - -Use it before sampling: - -```text -MODEL -> FunPack Context Transition Windows -> sampler/model input path -``` - -## Why This Exists - -The sampler-level motion pulses are intentionally conservative: they add monotonic noise kicks, but they do not make the model forget earlier frames. This node targets the stronger behavior: “same scene, viewed differently” by reducing temporal context carryover across chosen parts of the clip. - -It does not resample audio and does not add upward sigma jumps. Audio/video errors from Restart-style sigma replay should not be reintroduced by this node. - -## Recommended LTX2.3 Starting Point - -- `transition_mode`: `aggressive` -- `context_length`: smaller than the latent clip length, commonly `8` to `16` -- `context_overlap`: `2` to `4` -- `context_schedule`: `standard_static` -- `fuse_method`: `pyramid` -- `dim`: `2` -- `transition_start_pct`: `0.30` -- `transition_count`: `2` -- `transition_spacing_pct`: `0.22` -- `transition_strength`: `0.85` to `1.0` -- `cond_retain_index_list`: leave empty for transition testing - -If `transition_start_pct` is `0.0`, the first boundary is placed immediately after latent frame `0`. That keeps the upstream image-to-video frame-1 anchor intact, but prevents later windows from treating frame `0` as normal continuous context. - -## Behavior - -- `off`: uses ordinary manual context windows with no transition boundaries. -- `balanced`: creates one context boundary and allows a small overlap bleed. -- `aggressive`: creates at least two context boundaries and blocks most cross-boundary context. -- `custom`: uses the values exactly as provided. - -`transition_strength` controls isolation: - -- `1.0`: hard segment split; windows do not cross transition boundaries. -- Lower values: allow a small number of overlap frames around the boundary, based on `context_overlap`. - -For strong scene/viewpoint changes, use explicit prompt/refiner language such as orbit, dolly, push-in, pull-back, low angle, high angle, profile view, over-shoulder, or reveal. This node gives those concepts temporal room to form; the conditioning still needs to ask for them. diff --git a/docs/FunPackHybridEuler2SSampler.md b/docs/FunPackHybridEuler2SSampler.md index c74bcec7..00f71522 100644 --- a/docs/FunPackHybridEuler2SSampler.md +++ b/docs/FunPackHybridEuler2SSampler.md @@ -96,5 +96,3 @@ The early phase uses a second-order denoised extrapolation (Adams-Bashforth 2-st Motion pulse state resets after any pulse fires because the pulse modifies the latent and invalidates the previous denoised estimate. The outgoing `SIGMAS` remain monotonic. Motion pulses happen inside the sampler at selected denoise steps rather than by expanding the schedule. - -If you need the model to stop carrying the previous segment's temporal context forward, add `FunPack Context Transition Windows`. It is the stronger transition tool and should be preferred for multi-view or "scene reset" testing. diff --git a/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json b/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json index 0b4b921f..8898907c 100644 --- a/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json +++ b/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json @@ -3300,7 +3300,6 @@ "name": "VAE", "type": "VAE", "linkIds": [ - 13426, 13426 ], "localized_name": "VAE", @@ -3342,7 +3341,6 @@ "name": "VAE_2", "type": "VAE", "linkIds": [ - 13428, 13428 ], "localized_name": "VAE_2", @@ -3371,7 +3369,6 @@ "name": "CLIP", "type": "CLIP", "linkIds": [ - 13435, 13435 ], "localized_name": "CLIP", @@ -3530,62 +3527,6 @@ false ] }, - { - "id": 5225, - "type": "FunPackContextTransitionWindows", - "pos": [ - -3499.400882804922, - 4203.790940060165 - ], - "size": [ - 346.26666870117185, - 394 - ], - "flags": {}, - "order": 8, - "mode": 4, - "inputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 14498 - } - ], - "outputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "links": [ - 14418 - ] - } - ], - "properties": { - "cnr_id": "ComfyUI-FunPack", - "ver": "ace940fed3f7f2724f784b8d3165871a4e027281", - "Node name for S&R": "FunPackContextTransitionWindows", - "aux_id": "digital-garbage/ComfyUI-FunPack" - }, - "widgets_values": [ - "aggressive", - 121, - 73, - "standard_uniform", - "overlap-linear", - 0, - 0, - 3, - 0.33, - 0, - 1, - false, - true, - "", - false - ] - }, { "id": 5004, "type": "VAELoaderKJ", @@ -3853,7 +3794,7 @@ "name": "model", "type": "MODEL", "links": [ - 14498 + 14418 ] }, { @@ -3970,19 +3911,11 @@ }, { "id": 14418, - "origin_id": 5225, + "origin_id": 5204, "origin_slot": 0, "target_id": -20, "target_slot": 4, "type": "MODEL" - }, - { - "id": 14498, - "origin_id": 5204, - "origin_slot": 0, - "target_id": 5225, - "target_slot": 0, - "type": "MODEL" } ], "extra": {} @@ -5475,4 +5408,4 @@ "linkExtensions": [] }, "version": 0.4 -} \ No newline at end of file +} diff --git a/example_workflows/LTXFunPackRefinerV2.json b/example_workflows/LTXFunPackRefinerV2.json index 52401337..268afa4c 100644 --- a/example_workflows/LTXFunPackRefinerV2.json +++ b/example_workflows/LTXFunPackRefinerV2.json @@ -2938,7 +2938,6 @@ "name": "VAE", "type": "VAE", "linkIds": [ - 13426, 13426 ], "localized_name": "VAE", @@ -2980,7 +2979,6 @@ "name": "VAE_2", "type": "VAE", "linkIds": [ - 13428, 13428 ], "localized_name": "VAE_2", @@ -3009,7 +3007,6 @@ "name": "CLIP", "type": "CLIP", "linkIds": [ - 13435, 13435 ], "localized_name": "CLIP", @@ -3361,7 +3358,7 @@ "name": "model", "type": "MODEL", "links": [ - 14498 + 14418 ] }, { @@ -3464,61 +3461,6 @@ 0, 1 ] - }, - { - "id": 5225, - "type": "FunPackContextTransitionWindows", - "pos": [ - -3499.400882804922, - 4203.790940060165 - ], - "size": [ - 346.26666870117185, - 394 - ], - "flags": {}, - "order": 8, - "mode": 4, - "inputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 14498 - } - ], - "outputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "links": [ - 14418 - ] - } - ], - "properties": { - "cnr_id": "ComfyUI-FunPack", - "ver": "ace940fed3f7f2724f784b8d3165871a4e027281", - "Node name for S&R": "FunPackContextTransitionWindows" - }, - "widgets_values": [ - "balanced", - 241, - 73, - "standard_uniform", - "overlap-linear", - 0, - 0, - 4, - 0.2, - 1, - 1, - false, - true, - "", - false - ] } ], "groups": [], @@ -3605,19 +3547,11 @@ }, { "id": 14418, - "origin_id": 5225, + "origin_id": 5204, "origin_slot": 0, "target_id": -20, "target_slot": 4, "type": "MODEL" - }, - { - "id": 14498, - "origin_id": 5204, - "origin_slot": 0, - "target_id": 5225, - "target_slot": 0, - "type": "MODEL" } ], "extra": {} @@ -5514,4 +5448,4 @@ ] }, "version": 0.4 -} \ No newline at end of file +} diff --git a/funpack.py b/funpack.py index defd1688..4f173de6 100644 --- a/funpack.py +++ b/funpack.py @@ -18,7 +18,6 @@ try: ) from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader from .samplers import FunPackHybridEuler2SSampler, sample_funpack_hybrid_euler_2s - from .context_transition import FunPackContextTransitionWindows from .templates import FunPackRefinementKeyLoader, FunPackSceneBuilder except ImportError: from conditioning import ( @@ -37,7 +36,6 @@ except ImportError: ) from model_management import FunPackApplyLoraWeights, FunPackLoraLoader from samplers import FunPackHybridEuler2SSampler, sample_funpack_hybrid_euler_2s - from context_transition import FunPackContextTransitionWindows from templates import FunPackRefinementKeyLoader, FunPackSceneBuilder NODE_CLASS_MAPPINGS = { @@ -52,7 +50,6 @@ except ImportError: "FunPackLorebookEnhancer": FunPackLorebookEnhancer, "FunPackVideoRefinerV2": FunPackVideoRefinerV2, "FunPackHybridEuler2SSampler": FunPackHybridEuler2SSampler, - "FunPackContextTransitionWindows": FunPackContextTransitionWindows, "FunPackApplyLoraWeights": FunPackApplyLoraWeights, "FunPackLoraLoader": FunPackLoraLoader, "FunPackRefinementKeyLoader": FunPackRefinementKeyLoader, @@ -71,7 +68,6 @@ except ImportError: "FunPackLorebookEnhancer": "FunPack Lorebook Enhancer", "FunPackVideoRefinerV2": "FunPack Video Refiner V2", "FunPackHybridEuler2SSampler": "FunPack Hybrid Euler 2S Sampler", - "FunPackContextTransitionWindows": "FunPack Context Transition Windows", "FunPackApplyLoraWeights": "FunPack Apply LoRA Weights", "FunPackLoraLoader": "FunPack LoRA Loader", "FunPackRefinementKeyLoader": "FunPack Refinement Key Loader", @@ -83,7 +79,6 @@ __all__ = [ "NODE_DISPLAY_NAME_MAPPINGS", "FunPackVideoRefinerV2", "FunPackHybridEuler2SSampler", - "FunPackContextTransitionWindows", "FunPackPromptCombiner", "FunPackLorebookEnhancer", "FunPackPromptEnhancer", diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py index 4da44c6c..0f265397 100644 --- a/tests/test_v2_category_alignment.py +++ b/tests/test_v2_category_alignment.py @@ -2276,7 +2276,7 @@ def test_split_by_transitions_shows_scenes_in_encoded_prompts(tmp_path): split_by_transitions=True, ) - # Single conditioning entry (full prompt) - no per-window routing without context windows + # Single conditioning entry - split mode is a prompt preview only. assert len(cond) == 1 assert "Transition split" in status assert "Detected scenes" in encoded_prompts @@ -2312,25 +2312,15 @@ def test_split_prompt_by_transitions_detects_known_phrases(): assert len(segments_none) == 1 -def test_per_window_anchor_uses_full_first_segment(): - """Window 0 = full segment[0]; windows 1+ = segment[0] + scene content. - No splitting at the first comma - prevents breaking prompts like - 'In this anime video, Character Name is ...'.""" +def test_transition_scene_preview_uses_full_first_segment(): refiner = FunPackVideoRefinerV2() - class FakeClip: - def tokenize(self, text): return text - def encode_from_tokens_scheduled(self, t): - import torch - return [(torch.ones(1, 4, 3), {"pooled_output": torch.ones(1, 3)})] - segs = refiner._v2_split_prompt_by_transitions( "In this anime video, Hiyuki is a mature woman, then she walks, suddenly she sits" ) assert len(segs) == 3 - conds, texts = refiner._v2_encode_per_window_conditionings(FakeClip(), segs) - assert len(conds) == 3 - # anchor = full first segment, not just "In this anime video" + texts = refiner._v2_transition_scene_texts(segs) + assert len(texts) == 3 assert texts[0] == "In this anime video, Hiyuki is a mature woman" assert texts[1].startswith("In this anime video, Hiyuki is a mature woman") assert "then she walks" in texts[1] diff --git a/web/funpack_studio.js b/web/funpack_studio.js index 61d9ebdd..48b91173 100644 --- a/web/funpack_studio.js +++ b/web/funpack_studio.js @@ -493,9 +493,8 @@ function openPanel(node) { const splitToggle = toggleEl(!!settings.refiner.split_by_transitions, "Split prompt by transitions"); splitToggle.inp.addEventListener("change", () => { settings.refiner.split_by_transitions = splitToggle.inp.checked; }); body.append(el("div", "funpack-studio-hint", - "Detect transition words in the prompt (then, suddenly, cut to...) and encode each scene segment as a separate conditioning entry. " + - "The character description (text before the first comma) is prepended to every segment to keep the subject consistent. " + - "Use together with FunPack Context Transition Windows.")); + "Detect transition words in the prompt and preview scene segments in the encoded prompts output. " + + "The refiner still returns one full-prompt conditioning entry.")); body.append(row("Split by transitions", splitToggle.wrap)); body.append(sectionTitle("Negative prompt"));