Fix refiner phrase category alignment

This commit is contained in:
digital-garbage
2026-05-07 21:22:17 +03:00
parent 95dff4b3c9
commit faec254cd9
6 changed files with 247 additions and 33 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## [2.2.1] - 2026-05-07
### Fixed
Fixed `FunPack Video Refiner V2` prompt phrase categorization so environment and appearance descriptions are not pulled into action learning by generic `-ing` or `-ed` words.
Updated Refiner V2 category similarity blending so CLIP category comparisons only help uncertain phrases instead of overriding strong local action, camera, appearance, environment, quality, or detail anchors.
## [2.2.0] - 2026-05-07
### 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.2.1
Fixed Refiner V2 prompt phrase categorization so background, appearance, quality, camera, and action phrases stay aligned before the node updates Lucky memory or LoRA suggestions.
## Updates in 2.2.0
Added `FunPack Video Refiner V2`, a simpler prompt-owned refiner that takes `positive_prompt` and a connected `CLIP`, encodes the prompt inside the node, learns from ratings, and returns refined positive conditioning plus diagnostics.
+60 -18
View File
@@ -1,23 +1,64 @@
# ComfyUI-FunPack/__init__.py
from .conditioning import (
FunPackLorebookEnhancer,
FunPackPromptCombiner,
FunPackPromptEnhancer,
FunPackStoryWriter,
FunPackVideoRefinerV2,
)
from .image_processing import (
FunPackClipVisionOutputCombine,
FunPackContinueVideo,
FunPackStoryMemKeyframeExtractor,
FunPackStoryMemLastFrameExtractor,
FunPackVideoStitch,
)
from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader
from .samplers import FunPackHybridEuler2SSampler
from .context_transition import FunPackContextTransitionWindows
from .templates import FunPackTemplateManager
if __package__:
from .conditioning import (
FunPackLorebookEnhancer,
FunPackPromptCombiner,
FunPackPromptEnhancer,
FunPackStoryWriter,
FunPackVideoRefinerV2,
)
from .image_processing import (
FunPackClipVisionOutputCombine,
FunPackContinueVideo,
FunPackStoryMemKeyframeExtractor,
FunPackStoryMemLastFrameExtractor,
FunPackVideoStitch,
)
from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader
from .samplers import FunPackHybridEuler2SSampler
from .context_transition import FunPackContextTransitionWindows
from .templates import FunPackTemplateManager
else:
# Standalone tests may not have the full ComfyUI/CUDA runtime loaded.
from conditioning import (
FunPackLorebookEnhancer,
FunPackPromptCombiner,
FunPackPromptEnhancer,
FunPackStoryWriter,
FunPackVideoRefinerV2,
)
try:
from image_processing import (
FunPackClipVisionOutputCombine,
FunPackContinueVideo,
FunPackStoryMemKeyframeExtractor,
FunPackStoryMemLastFrameExtractor,
FunPackVideoStitch,
)
except Exception:
FunPackClipVisionOutputCombine = None
FunPackContinueVideo = None
FunPackStoryMemKeyframeExtractor = None
FunPackStoryMemLastFrameExtractor = None
FunPackVideoStitch = None
try:
from model_management import FunPackApplyLoraWeights, FunPackLoraLoader
except Exception:
FunPackApplyLoraWeights = None
FunPackLoraLoader = None
try:
from samplers import FunPackHybridEuler2SSampler
except Exception:
FunPackHybridEuler2SSampler = None
try:
from context_transition import FunPackContextTransitionWindows
except Exception:
FunPackContextTransitionWindows = None
try:
from templates import FunPackTemplateManager
except Exception:
FunPackTemplateManager = None
WEB_DIRECTORY = "./web"
@@ -38,6 +79,7 @@ NODE_CLASS_MAPPINGS = {
"FunPackLoraLoader": FunPackLoraLoader,
"FunPackTemplateManager": FunPackTemplateManager,
}
NODE_CLASS_MAPPINGS = {name: cls for name, cls in NODE_CLASS_MAPPINGS.items() if cls is not None}
NODE_DISPLAY_NAME_MAPPINGS = {
"FunPackPromptCombiner": "FunPack Prompt Combiner",
+121 -14
View File
@@ -5546,37 +5546,85 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
"jumping", "fly", "flying", "move", "moving", "motion", "gesture", "gesturing",
"hold", "holding", "reach", "reaching", "look", "looking", "blink", "blinking",
"smile", "smiling", "sit", "sitting", "stand", "standing", "kneel", "kneeling",
"flow", "flowing", "sway", "swaying", "spin", "spinning", "fall", "falling",
"sway", "swaying", "spin", "spinning", "fall", "falling", "grab", "grabbing",
"throw", "throwing", "catch", "catching", "climb", "climbing", "fight", "fighting",
"talk", "talking", "speak", "speaking", "wave", "waving", "crawl", "crawling",
"drive", "driving", "ride", "riding", "open", "opening", "close", "closing",
},
"camera": {
"camera", "shot", "closeup", "close-up", "wide", "angle", "zoom", "pan", "panning",
"dolly", "tracking", "handheld", "focus", "framing", "push", "pull", "tilt",
"dolly", "tracking", "handheld", "focus", "framing", "push", "push-in", "pull",
"pull-back", "tilt", "orbit", "orbiting", "crane", "rack", "lens", "viewpoint",
"perspective", "overhead", "low-angle", "high-angle", "close-up", "macro",
},
"subject": {
"woman", "man", "girl", "boy", "person", "character", "robot", "creature", "dragon",
"animal", "dog", "cat", "car", "vehicle", "object", "sculpture", "glass",
"animal", "dog", "cat", "car", "vehicle", "object", "sculpture", "monster",
"child", "baby", "adult", "crowd", "dancer", "runner", "warrior", "knight",
},
"appearance": {
"hair", "eyes", "face", "skin", "dress", "jacket", "armor", "outfit", "clothing",
"hands", "pose", "expression", "beard", "makeup", "body", "anatomy",
"hands", "pose", "expression", "beard", "makeup", "body", "anatomy", "wearing",
"wears", "dressed", "clothed", "costume", "shirt", "coat", "robe", "boots",
"hat", "helmet", "gloves", "mask", "tattoo", "freckles", "scar", "silhouette",
"flowing", "curly", "straight", "long", "short", "blonde", "brunette",
},
"environment": {
"forest", "city", "street", "room", "kitchen", "beach", "mountain", "temple",
"sunset", "night", "rain", "snow", "sky", "background", "studio", "tabletop",
"setting", "environment", "landscape", "interior", "exterior", "alley", "road",
"field", "desert", "ocean", "sea", "lake", "river", "waterfall", "clouds",
"weather", "fog", "mist", "storm", "snowfall", "backdrop", "horizon",
},
"style": {
"anime", "cinematic", "photorealistic", "painterly", "illustration", "stylized",
"realistic", "film", "noir", "vintage", "dramatic", "soft", "lighting",
"moody", "surreal", "documentary", "editorial", "watercolor", "sketch",
"render", "rendered", "monochrome", "pastel", "neon", "gothic",
},
"quality": {
"masterpiece", "best", "quality", "detailed", "sharp", "highres", "high-res",
"ultra", "perfect", "clean", "realism", "realistic", "smooth",
"ultra", "perfect", "clean", "realism", "realistic", "smooth", "crisp",
"high-detail", "highly-detailed", "ultra-detailed", "noise-free", "polished",
"refined", "clear", "high-resolution", "8k", "4k",
},
"details": {
"reflection", "reflections", "texture", "textures", "shadow", "shadows", "smoke",
"dust", "particles", "small", "tiny", "prop", "props", "fabric", "glass",
"sparkles", "embers", "debris", "scratches", "cracks", "drops", "droplets",
"pattern", "patterns", "grain", "details", "ornament", "ornaments",
},
}
CATEGORY_PRIORITY = {
"action": 0.018,
"camera": 0.014,
"subject": 0.008,
"appearance": 0.012,
"environment": 0.012,
"style": 0.006,
"quality": 0.004,
"details": 0.002,
}
ACTION_SUFFIX_STEMS = {
"walk", "run", "turn", "danc", "jump", "fly", "mov", "gestur", "hold", "reach",
"look", "blink", "smil", "sit", "stand", "kneel", "sway", "spin", "fall",
"grab", "throw", "catch", "climb", "fight", "talk", "speak", "wav", "crawl",
"driv", "rid", "open", "clos",
}
APPEARANCE_CONTEXT_WORDS = {
"hair", "eyes", "face", "skin", "dress", "jacket", "armor", "outfit", "clothing",
"shirt", "coat", "robe", "boots", "hat", "helmet", "gloves", "mask", "body",
"anatomy", "beard", "makeup", "costume",
}
ENVIRONMENT_CONTEXT_WORDS = {
"background", "setting", "environment", "landscape", "room", "street", "forest",
"city", "beach", "mountain", "temple", "sky", "studio", "interior", "exterior",
"backdrop", "horizon", "weather",
}
QUALITY_CONTEXT_WORDS = {
"quality", "detail", "details", "detailed", "sharp", "clean", "crisp", "realism",
"resolution", "highres", "high-resolution", "smooth", "polished",
}
@classmethod
def INPUT_TYPES(cls):
@@ -5692,28 +5740,67 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
if self._is_valuable_token(word.strip())
]
def _v2_has_action_suffix(self, words):
for word in words:
if word.endswith("ing"):
stem = word[:-3]
elif word.endswith("ed"):
stem = word[:-2]
else:
continue
if stem in self.ACTION_SUFFIX_STEMS:
return True
if stem.endswith("n") and stem[:-1] in self.ACTION_SUFFIX_STEMS:
return True
if stem.endswith("p") and stem[:-1] in self.ACTION_SUFFIX_STEMS:
return True
return False
def _v2_rebalance_anchored_scores(self, scores, words):
words = set(words or [])
has_action = bool(words & self.CATEGORY_KEYWORDS["action"]) or self._v2_has_action_suffix(words)
if words & self.APPEARANCE_CONTEXT_WORDS:
scores["appearance"] = max(scores["appearance"], 0.66)
if not has_action:
scores["action"] = min(scores["action"], 0.40)
if words & self.ENVIRONMENT_CONTEXT_WORDS:
scores["environment"] = max(scores["environment"], 0.66)
if not has_action:
scores["action"] = min(scores["action"], 0.36)
if words & self.QUALITY_CONTEXT_WORDS:
scores["quality"] = max(scores["quality"], 0.64)
if not has_action:
scores["action"] = min(scores["action"], 0.34)
if has_action:
scores["action"] = max(scores["action"], 0.66)
return scores
def _v2_heuristic_scores(self, phrase):
words = set(self._v2_phrase_words(phrase))
scores = {category: 0.0 for category in self.CATEGORY_DESCRIPTIONS}
for category, keywords in self.CATEGORY_KEYWORDS.items():
hits = len(words & keywords)
if hits:
scores[category] += min(0.85, 0.38 + hits * 0.17)
scores[category] += min(0.92, 0.48 + hits * 0.18)
text = str(phrase or "").lower()
if re.search(r"\b\w+(ing|ed)\b", text):
if self._v2_has_action_suffix(words):
scores["action"] = max(scores["action"], 0.52)
if "camera" in text and re.search(r"\b(move|moves|moving|push|pull|pan|zoom|track|dolly|tilt)", text):
scores["camera"] = max(scores["camera"], 0.78)
scores["action"] = max(scores["action"], 0.48)
if re.search(r"\b(slowly|quickly|gentle|smooth|fast)\b", text) and scores["action"] > 0.0:
scores["action"] = min(0.92, scores["action"] + 0.12)
scores = self._v2_rebalance_anchored_scores(scores, words)
return scores
def _v2_scores_primary(self, scores):
if not scores:
return "details", 0.0
primary, confidence = max(scores.items(), key=lambda item: item[1])
primary, confidence = max(
scores.items(),
key=lambda item: (item[1], self.CATEGORY_PRIORITY.get(item[0], 0.0)),
)
return primary, float(confidence)
def _v2_clip_similarity_scores(self, clip, phrase, category_vectors):
@@ -5738,6 +5825,31 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
vectors[category] = vector.cpu()
return vectors
def _v2_merge_clip_category_scores(self, heuristic_scores, clip_scores):
if not clip_scores:
return heuristic_scores
merged = dict(heuristic_scores)
heuristic_primary, heuristic_confidence = self._v2_scores_primary(heuristic_scores)
ranked_clip = sorted(clip_scores.items(), key=lambda item: item[1], reverse=True)
if not ranked_clip:
return merged
clip_primary, clip_confidence = ranked_clip[0]
clip_runner_up = ranked_clip[1][1] if len(ranked_clip) > 1 else 0.0
clip_margin = float(clip_confidence) - float(clip_runner_up)
if heuristic_confidence >= 0.60:
return merged
if heuristic_confidence >= 0.44 and clip_primary != heuristic_primary and clip_margin < 0.075:
return merged
mean_clip = sum(float(value) for value in clip_scores.values()) / max(1, len(clip_scores))
for category, score in clip_scores.items():
relative = max(0.0, float(score) - mean_clip)
merged[category] = max(float(merged.get(category, 0.0)), min(0.56, relative * 2.40))
if clip_margin >= 0.045:
merged[clip_primary] = max(float(merged.get(clip_primary, 0.0)), min(0.68, 0.48 + clip_margin * 2.20))
return merged
def _v2_classify_phrases(self, clip, phrases):
phrase_items = []
uncertain = []
@@ -5766,12 +5878,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
clip_scores = self._v2_clip_similarity_scores(clip, item["text"], category_vectors)
if not clip_scores:
continue
merged = {}
for category in self.CATEGORY_DESCRIPTIONS:
merged[category] = max(
float(item["category_scores"].get(category, 0.0)) * 0.58,
float(clip_scores.get(category, 0.0)) * 0.82,
)
merged = self._v2_merge_clip_category_scores(item["category_scores"], clip_scores)
primary, confidence = self._v2_scores_primary(merged)
item["category_scores"] = merged
item["primary"] = primary
+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.2.0" # Custom Node version. Must be semantically versioned.
version = "2.2.1" # Custom Node version. Must be semantically versioned.
license = { text = "GNU GPL v3" }
dependencies = [] # Filled in from requirements.txt
+53
View File
@@ -0,0 +1,53 @@
import sys
import types
sys.modules.setdefault("folder_paths", types.SimpleNamespace(models_dir=""))
from conditioning import FunPackVideoRefinerV2
def primary_category(phrase):
refiner = FunPackVideoRefinerV2()
scores = refiner._v2_heuristic_scores(phrase)
primary, confidence = refiner._v2_scores_primary(scores)
return primary, confidence, scores
def test_environment_descriptions_do_not_become_actions():
primary, confidence, scores = primary_category("detailed background")
assert primary == "environment"
assert confidence >= 0.60
assert scores["action"] == 0.0
def test_weathered_environment_descriptions_do_not_become_actions():
primary, confidence, scores = primary_category("weathered stone room")
assert primary == "environment"
assert confidence >= 0.60
assert scores["action"] == 0.0
def test_appearance_descriptions_stay_appearance():
primary, confidence, scores = primary_category("flowing hair and blue eyes")
assert primary == "appearance"
assert confidence >= 0.60
assert scores["action"] == 0.0
def test_clothing_descriptions_stay_appearance():
primary, confidence, scores = primary_category("wearing red dress")
assert primary == "appearance"
assert confidence >= 0.60
assert scores["action"] == 0.0
def test_motion_descriptions_stay_action():
primary, confidence, scores = primary_category("running through the street")
assert primary == "action"
assert confidence >= 0.60
assert scores["action"] >= scores["environment"]