From 82ceea6c9ab05f21bd2f5c78d8b34d3012c74ba8 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 8 May 2026 22:27:34 +0300 Subject: [PATCH] Fix Refiner V2 conditioning fallback 2.3.3 --- CHANGELOG.md | 6 +++ README.md | 4 ++ conditioning.py | 64 +++++++++++++++++++++--- pyproject.toml | 2 +- tests/test_v2_category_alignment.py | 76 +++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1eb1806..c4c23d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.3.3] - 2026-05-08 + +### Fixed + +Fixed Refiner V2 so `CLIP` and pre-encoded `positive_conditioning` can both be optional inputs. When `CLIP` is connected, V2 keeps owning prompt encoding as before. When `CLIP` is not connected but `positive_conditioning` is connected, V2 accepts the finished Gemma3/LTX2 conditioning, uses the prompt for analysis, and loads only the Gemma3 tokenizer. + ## [2.3.2] - 2026-05-08 ### Added diff --git a/README.md b/README.md index 6622a29f..9134e0dc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A set of ComfyUI nodes for experimenting with video generation workflows based on WAN, HunyuanVideo, LTX, and similar models. +## Updates in 2.3.3 + +Refiner V2 now supports the restored pre-encoded conditioning workflow: connect `positive_conditioning` without `CLIP` and it accepts the finished Gemma3/LTX2 conditioning while loading only the Gemma3 tokenizer. If `CLIP` is connected, V2 still encodes the prompt itself and ignores the fallback conditioning path. + ## Updates in 2.3.2 Refiner V2 now learns original-intent alignment when `user_intent_prompt` stays the same but an enhancer gives different `positive_prompt` variants. Ratings teach it which intent-enhance pairs represented the original request, which original phrases were missing, and which enhancer-only additions were rejected. diff --git a/conditioning.py b/conditioning.py index 1d2a09d7..35e8c1b9 100644 --- a/conditioning.py +++ b/conditioning.py @@ -5701,10 +5701,13 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): return { "required": { "positive_prompt": ("STRING", {"multiline": True, "default": "", "placeholder": "Positive prompt"}), - "clip": ("CLIP", {"tooltip": "Connected text encoder used for all V2 prompt encoding and similarity checks."}), "rating": (V2_RATING_LABELS, {"default": "Missing action", "label": "Rating"}), }, "optional": { + "clip": ("CLIP", {"tooltip": "Optional text encoder. When connected, V2 encodes the prompt itself."}), + "positive_conditioning": ("CONDITIONING", { + "tooltip": "Optional pre-encoded Gemma3/LTX2 conditioning. Used only when CLIP is not connected.", + }), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "label": "Exploration Seed"}), "reset_session": ("BOOLEAN", {"default": False, "label": "Reset V2 Session"}), "lora_stack": ("FUNPACK_LORA_STACK", {"tooltip": "Optional stack from FunPack LoRA Loader. V2 writes prompt-specific suggested weights."}), @@ -5824,6 +5827,44 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): return None, {"pooled_output": None}, "encode returned invalid conditioning" return cond, meta, f"encoded {self._get_conditioning_seq_len(cond)} positions" + def _v2_gemma3_tokenizer_status(self): + tokenizer = self._get_tokenizer("ltx2") + if tokenizer is None: + return "Gemma3 tokenizer unavailable" + source = str(getattr(tokenizer, "name_or_path", "") or "").strip() + if source: + return f"Gemma3 tokenizer loaded: {source}" + return "Gemma3 tokenizer loaded" + + def _v2_conditioning_source(self, clip, prompt_text, positive_conditioning): + if clip is not None: + cond, meta, encode_status = self._v2_encode_prompt(clip, prompt_text) + return cond, meta, encode_status, "CLIP-owned" + + if positive_conditioning is None: + return ( + None, + {"pooled_output": None}, + "CLIP missing and no positive CONDITIONING connected", + "CONDITIONING-owned", + ) + + cond, meta = self._v2_extract_conditioning(positive_conditioning) + if not isinstance(cond, torch.Tensor): + return ( + None, + {"pooled_output": None}, + "connected positive CONDITIONING is invalid", + "CONDITIONING-owned", + ) + + tokenizer_status = self._v2_gemma3_tokenizer_status() + encode_status = ( + f"accepted connected positive CONDITIONING " + f"({self._get_conditioning_seq_len(cond)} positions); {tokenizer_status}" + ) + return cond, meta, encode_status, "CONDITIONING-owned" + def _v2_conditioning_vector(self, conditioning): if not isinstance(conditioning, torch.Tensor) or conditioning.dim() <= 1: return None @@ -8071,9 +8112,9 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): prompt_history["lora_weight_suggestions"] = suggestions return "LoRA suggestions: " + " | ".join(parts) - def refine_v2(self, positive_prompt, clip, rating, refinement_key="", + def refine_v2(self, positive_prompt, clip=None, rating="Missing action", refinement_key="", seed=0, reset_session=False, lora_stack=None, im_feeling_lucky=False, user_intent_prompt="", - refinement_key_input=""): + refinement_key_input="", positive_conditioning=None): if seed != 0: torch.manual_seed(seed) random.seed(seed) @@ -8150,7 +8191,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): lucky_status = "Prompt refusal filter: enhancer refusal detected; current prompt will not be stored or learned." encoded_role = "refusal passthrough" refusal_status = "Current prompt refused by enhancer; storage skipped." - elif im_feeling_lucky: + elif im_feeling_lucky and clip is not None: prompt_to_encode, lucky_status = self._v2_compose_lucky_prompt(analysis_prompt, phrases, global_state) encoded_role = "lucky prompt" else: @@ -8170,13 +8211,20 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): axis_feedback, intent_phrases=intent_phrases, ) - lucky_status = f"Lucky: off | trained memory only. {emphasis_status}" + if im_feeling_lucky and clip is None: + lucky_status = f"Lucky: unavailable without CLIP | connected CONDITIONING accepted. {emphasis_status}" + else: + lucky_status = f"Lucky: off | trained memory only. {emphasis_status}" encoded_role = "current prompt" - cond, meta, encode_status = self._v2_encode_prompt(clip, prompt_to_encode) + cond, meta, encode_status, conditioning_owner = self._v2_conditioning_source( + clip, + prompt_to_encode, + positive_conditioning, + ) fallback_graph = render_refinement_loss_graph(refinement_key, "v2", "clip", 0, 0.0, []) if not isinstance(cond, torch.Tensor): - status = f"ERROR: V2 could not encode prompt | {encode_status}" + status = f"ERROR: V2 could not prepare conditioning | {encode_status}" training_info = f"Rating: {rating_label}\n{memory_status}\n{lucky_status}\n{encode_status}" return ([], status, training_info, fallback_graph) @@ -8300,7 +8348,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): ) refusal_status_line = f"\n{refusal_status}" if refusal_status else "" status = ( - f"V2 {state_status} | CLIP-owned | Rating {rating_label} | Iter {global_state['total_iterations']} | " + f"V2 {state_status} | {conditioning_owner} | Rating {rating_label} | Iter {global_state['total_iterations']} | " f"Learning {'trained previous run' if has_previous_run and not learning_profile.get('skip_learning') else 'not applied'}\n" f"{adaptation_status}\n" f"{training_guidance}\n" diff --git a/pyproject.toml b/pyproject.toml index 281314e9..17b7aac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "ComfyUI-FunPack" # Unique identifier for your node. Immutable after creation. description = "A set of nodes for experiments with video generation models, including prompt refinement, adaptive LoRA weight workflows, story tools, video stitching, lorebook enhancement and more." -version = "2.3.2" # Custom Node version. Must be semantically versioned. +version = "2.3.3" # Custom Node version. Must be semantically versioned. license = { text = "GNU GPL v3" } dependencies = [] # Filled in from requirements.txt diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py index 8f9d2fe0..6ac26816 100644 --- a/tests/test_v2_category_alignment.py +++ b/tests/test_v2_category_alignment.py @@ -737,6 +737,82 @@ def test_refiner_training_info_uses_readable_sections(tmp_path): assert "Category compact view:\n-" in training_info +def test_refiner_v2_exposes_clip_and_conditioning_as_optional_inputs(): + inputs = FunPackVideoRefinerV2.INPUT_TYPES() + + assert "clip" not in inputs["required"] + assert "positive_conditioning" not in inputs["required"] + assert "clip" in inputs["optional"] + assert "positive_conditioning" in inputs["optional"] + + +def test_refiner_v2_accepts_conditioning_without_clip_and_loads_gemma3_tokenizer(tmp_path, monkeypatch): + refiner = FunPackVideoRefinerV2() + state_path = tmp_path / "state.json" + refiner._v2_state_path = lambda refinement_key: str(state_path) + tokenizer_modes = [] + + class FakeTokenizer: + name_or_path = "DreamFast/gemma-3-12b-it-heretic-v2" + + def fake_get_tokenizer(mode="ltx2"): + tokenizer_modes.append(mode) + return FakeTokenizer() + + monkeypatch.setattr(refiner, "_get_tokenizer", fake_get_tokenizer) + positive_conditioning = [(torch.full((1, 4, 3), 2.0), {"pooled_output": torch.ones(1, 3)})] + + modified, status, training_info, _ = refiner.refine_v2( + "woman walking through neon rain", + None, + "Perfect", + "conditioning-input-test", + positive_conditioning=positive_conditioning, + ) + + assert tokenizer_modes == ["ltx2"] + assert "CONDITIONING-owned" in status + assert "Gemma3 tokenizer loaded: DreamFast/gemma-3-12b-it-heretic-v2" in training_info + assert modified[0][0].shape == positive_conditioning[0][0].shape + + +def test_refiner_v2_prefers_clip_when_both_clip_and_conditioning_are_connected(tmp_path, monkeypatch): + refiner = FunPackVideoRefinerV2() + state_path = tmp_path / "state.json" + refiner._v2_state_path = lambda refinement_key: str(state_path) + monkeypatch.setattr(refiner, "_get_tokenizer", lambda mode="ltx2": (_ for _ in ()).throw(AssertionError("unexpected tokenizer load"))) + positive_conditioning = [(torch.full((1, 4, 3), 9.0), {"pooled_output": torch.ones(1, 3)})] + + modified, status, training_info, _ = refiner.refine_v2( + "woman walking through neon rain", + FakeClip(), + "Perfect", + "clip-priority-test", + positive_conditioning=positive_conditioning, + ) + + assert "CLIP-owned" in status + assert "accepted connected positive CONDITIONING" not in training_info + assert torch.allclose(modified[0][0], torch.ones(1, 4, 3)) + + +def test_refiner_v2_errors_without_clip_or_conditioning(tmp_path): + refiner = FunPackVideoRefinerV2() + state_path = tmp_path / "state.json" + refiner._v2_state_path = lambda refinement_key: str(state_path) + + modified, status, training_info, _ = refiner.refine_v2( + "woman walking through neon rain", + None, + "Perfect", + "missing-conditioning-test", + ) + + assert modified == [] + assert "ERROR: V2 could not prepare conditioning" in status + assert "CLIP missing and no positive CONDITIONING connected" in training_info + + def test_prompt_enhancer_refusal_is_not_stored_as_last_run(tmp_path): refiner = FunPackVideoRefinerV2() state_path = tmp_path / "state.json"