Remove standalone scene transition nodes
This commit is contained in:
-10
@@ -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",
|
||||
|
||||
+19
-82
@@ -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,
|
||||
|
||||
@@ -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},)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user