Fix Refiner V2 conditioning fallback 2.3.3

This commit is contained in:
digital-garbage
2026-05-08 22:27:34 +03:00
parent 10d6a49603
commit 82ceea6c9a
5 changed files with 143 additions and 9 deletions
+6
View File
@@ -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
+4
View File
@@ -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.
+56 -8
View File
@@ -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"
+1 -1
View File
@@ -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
+76
View File
@@ -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"