diff --git a/conditioning.py b/conditioning.py index ae50cd0d..645260ae 100644 --- a/conditioning.py +++ b/conditioning.py @@ -92,6 +92,15 @@ RATING_PROFILES = { }, } +CATEGORY_FEEDBACK_MAP = { + 1: "general", + 2: "concept", + 3: "style", + 4: "quality", + 5: "character", + 6: "details", +} + def _clamp(value, low, high): return max(low, min(high, value)) @@ -772,6 +781,8 @@ class FunPackVideoRefiner: "label": " ".join((phrase_words or [])[:6]), "anchor_words": list(phrase_words or []), "category": category, + "category_source": "auto", + "category_confidence": 0.35 if category == "general" else 0.65, "word_importance": {}, "presence_target": 1.0, "priority_weight": 1.0, @@ -793,8 +804,13 @@ class FunPackVideoRefiner: anchor_words = list(cluster.get("anchor_words", [])) defaults = self._default_concept_cluster(anchor_words) defaults.update(cluster) - if not defaults.get("category") or defaults.get("category") == "general": + defaults.setdefault("category_source", "auto") + defaults.setdefault("category_confidence", 0.35 if defaults.get("category") == "general" else 0.65) + if defaults.get("category_source") != "user" and ( + not defaults.get("category") or defaults.get("category") == "general" + ): defaults["category"] = self._infer_concept_category(defaults.get("anchor_words", [])) + defaults["category_confidence"] = 0.35 if defaults["category"] == "general" else max(0.65, defaults.get("category_confidence", 0.0)) defaults["question_history"] = list(defaults.get("question_history", []))[-24:] return defaults @@ -827,6 +843,10 @@ class FunPackVideoRefiner: "prompt": "What is your preference for having '{label}' in future outputs?", "legend": "1=strongly less 2=less 3=slightly less 4=slightly more 5=more 6=much more", }, + "category": { + "prompt": "What kind of concept is '{label}'?", + "legend": "1=general 2=concept 3=style 4=quality 5=character 6=details", + }, } def _get_concept_mean_importance(self, cluster: dict): @@ -869,6 +889,10 @@ class FunPackVideoRefiner: score = 0.38 * (2.0 - cluster.get("semantic_fidelity", 1.0)) + 0.24 * rating_shift + 0.20 * uncertainty + 0.18 * max(0.0, 1.0 - similarity) elif question_type == "stability": score = 0.35 * rating_shift + 0.30 * abs(cluster.get("stability_weight", 1.0) - 1.0) + 0.20 * max(0.0, 1.0 - similarity) + 0.15 * uncertainty + elif question_type == "category": + category_unknown = 1.0 if cluster.get("category_source") != "user" else 0.0 + category_uncertainty = max(0.0, 1.0 - float(cluster.get("category_confidence", 0.35))) + score = 0.65 * category_unknown + 0.25 * category_uncertainty + 0.10 * rating_shift else: # preference score = 0.34 * abs(cluster.get("user_affinity", 1.0) - 1.0) + 0.28 * rating_shift + 0.20 * uncertainty + 0.18 * abs(cluster.get("priority_weight", 1.0) - 1.0) @@ -884,6 +908,7 @@ class FunPackVideoRefiner: question_specs = self._feedback_question_specs() rating_shift = min(1.0, abs(rating - last_rating) / 4.0) candidates = [] + category_candidates = [] for cid in ordered_concept_ids: cluster = concept_clusters.get(cid) @@ -912,7 +937,7 @@ class FunPackVideoRefiner: question_type, cluster, category, mean_imp, neighbor_mean, rating_shift, similarity, iter_num ) - candidates.append({ + candidate = { "concept_id": cid, "concept_label": current_concept_labels.get(cid, cluster.get("label", "")), "question_type": question_type, @@ -920,7 +945,16 @@ class FunPackVideoRefiner: "group_id": chosen_group_id, "category": category, "score": score, - }) + } + if question_type == "category": + category_candidates.append(candidate) + else: + candidates.append(candidate) + + if category_candidates: + category_candidate = max(category_candidates, key=lambda x: x["score"]) + if category_candidate["score"] >= 0.60: + return category_candidate if not candidates: return None @@ -1072,6 +1106,37 @@ class FunPackVideoRefiner: new_id = md5("|".join(sorted(phrase_words)).encode()).hexdigest()[:10] return new_id, True + def _apply_category_feedback(self, cluster: dict, feedback_rating: int, iter_num: int = 0): + category = CATEGORY_FEEDBACK_MAP.get(int(feedback_rating), "general") + cluster["category"] = category + cluster["category_source"] = "user" + cluster["category_confidence"] = 1.0 + + if category in {"concept", "character", "details"}: + cluster["presence_target"] = self._clip_profile_value(cluster.get("presence_target", 1.0) + 0.10) + cluster["priority_weight"] = self._clip_profile_value(cluster.get("priority_weight", 1.0) + 0.10) + cluster["semantic_fidelity"] = self._clip_profile_value( + cluster.get("semantic_fidelity", 1.0) + 0.08, + low=0.6, + high=1.8, + ) + elif category == "quality": + cluster["priority_weight"] = self._clip_profile_value(cluster.get("priority_weight", 1.0) + 0.06) + cluster["overrep_sensitivity"] = self._clip_profile_value(cluster.get("overrep_sensitivity", 1.0) + 0.08) + elif category == "style": + cluster["stability_weight"] = self._clip_profile_value(cluster.get("stability_weight", 1.0) + 0.08) + cluster["user_affinity"] = self._clip_profile_value(cluster.get("user_affinity", 1.0) + 0.05) + + cluster["question_history"] = list(cluster.get("question_history", []))[-23:] + cluster["question_history"].append({ + "iteration": iter_num, + "type": "category", + "rating": feedback_rating, + "category": category, + }) + cluster["last_question_type"] = "category" + cluster["last_question_iter"] = iter_num + def _build_word_concept_map(self, prompt: str, concept_clusters: dict): """ Parse the prompt into concept phrases (Level 3), match or create clusters, @@ -1099,10 +1164,16 @@ class FunPackVideoRefiner: for w in phrase_words: if w not in existing: concept_clusters[cid]["anchor_words"].append(w) - if concept_clusters[cid].get("category") == "general": + if ( + concept_clusters[cid].get("category_source") != "user" and + concept_clusters[cid].get("category") == "general" + ): concept_clusters[cid]["category"] = self._infer_concept_category( concept_clusters[cid]["anchor_words"] ) + concept_clusters[cid]["category_confidence"] = ( + 0.35 if concept_clusters[cid]["category"] == "general" else 0.65 + ) concept_clusters[cid]["last_prompt_label"] = phrase_label current_concept_labels[cid] = phrase_label for w in phrase_words: @@ -1249,6 +1320,10 @@ class FunPackVideoRefiner: concept_clusters[concept_id] = cluster question_type = question_type or "presence" + if question_type == "category": + self._apply_category_feedback(cluster, feedback_rating, iter_num) + return + direct_deltas = { "presence": {1: 0.90, 2: 0.50, 3: 0.20, 4: -0.15, 5: 0.04, 6: -0.55}, "priority": {1: -0.25, 2: -0.12, 3: -0.05, 4: 0.08, 5: 0.18, 6: 0.30}, @@ -1780,6 +1855,10 @@ class FunPackVideoRefiner: overlap = len(lora_words & prompt_words) / max(1, len(lora_words)) if lora_words else 0.0 category_score = 0.62 if lora_type == category else 0.0 + if lora_type == "concept" and category in {"concept", "details", "subject", "appearance", "action", "environment"}: + category_score = max(category_score, 0.58) + if lora_type == "character" and category in {"character", "subject", "appearance"}: + category_score = max(category_score, 0.72) if lora_type == "quality" and category == "quality": category_score = 0.78 if lora_type == "style" and category in {"style", "camera"}: @@ -2659,7 +2738,8 @@ class FunPackVideoRefiner: f"[{current_concept_labels.get(cid, c['label'])}/{c.get('category', 'general')}: " f"top={top_local} | p={c.get('presence_target', 1.0):.2f} " f"prio={c.get('priority_weight', 1.0):.2f} " - f"stab={c.get('stability_weight', 1.0):.2f}]" + f"stab={c.get('stability_weight', 1.0):.2f} " + f"cat={c.get('category_source', 'auto')}]" ) concept_line = "; ".join(active_concept_parts) if active_concept_parts else "none" diff --git a/docs/FunPackGemmaEmbeddingRefiner.md b/docs/FunPackGemmaEmbeddingRefiner.md index d398215c..37ded8ff 100644 --- a/docs/FunPackGemmaEmbeddingRefiner.md +++ b/docs/FunPackGemmaEmbeddingRefiner.md @@ -38,7 +38,7 @@ If you want the model to preserve speech or spoken lines, wrap that text in `"` **feedback_enabled**: If enabled, the node can return a concept-focused feedback question alongside the refined conditioning. -**feedback_rating**: Answer to the feedback question on its own 1 to 6 scale. This is only useful when concept feedback is enabled. +**feedback_rating**: Answer to the queued feedback question on its own 1 to 6 scale. The `feedback_question` output includes the exact legend to use. Most questions tune presence, priority, balance, fidelity, stability, or preference. The refiner can also ask a category-discovery question for uncertain word groups: `1=general`, `2=concept`, `3=style`, `4=quality`, `5=character`, `6=details`. **lora_stack**: Optional stack from `FunPack LoRA Loader`. The refiner uses it to save next-run model LoRA weight suggestions. @@ -50,7 +50,7 @@ If you want the model to preserve speech or spoken lines, wrap that text in `"` **status**: Short general node state summary. Use this to quickly see whether the refiner is learning well, what it is focused on, and which subsystems are active. -**feedback_question**: Optional concept feedback prompt generated by the node when feedback mode is active. +**feedback_question**: Optional concept feedback prompt generated by the node when feedback mode is active. The node prioritizes category-discovery questions for unknown or auto-classified concept phrases, then uses later 1 to 6 answers to adjust per-concept weights. **training_info**: Detailed training diagnostics for the current refinement step, including exact metrics, concept breakdown, LoRA suggestion details, and sigma/latent refinement details.