diff --git a/conditioning.py b/conditioning.py index 5236fb2a..ae50cd0d 100644 --- a/conditioning.py +++ b/conditioning.py @@ -26,11 +26,109 @@ LORA_REFINER_TYPE_PROFILES = { "character": {"step": 0.024, "max_offset": 0.20, "min_offset": -0.32, "bad_max_offset": 0.42, "bad_min_offset": -1.30, "culprit_bias": 0.10}, } +RATING_LABELS = [ + "I like it", + "Missing details", + "Missing concept", + "Missing quality", + "I don't like it", +] + +RATING_PROFILES = { + "I like it": { + "key": "like", + "level": 5, + "legacy_score": 10, + "legacy_range": "9-10", + "reward": 1.0, + "quality_signal": 1.0, + "concept_signal": 1.0, + "detail_signal": 1.0, + "prompt_emphasis": 0.45, + }, + "Missing details": { + "key": "missing_details", + "level": 4, + "legacy_score": 8, + "legacy_range": "7-8", + "reward": 0.45, + "quality_signal": 0.85, + "concept_signal": 0.70, + "detail_signal": -0.55, + "prompt_emphasis": 0.65, + }, + "Missing concept": { + "key": "missing_concept", + "level": 3, + "legacy_score": 6, + "legacy_range": "5-6", + "reward": -0.35, + "quality_signal": 0.65, + "concept_signal": -0.90, + "detail_signal": -0.70, + "prompt_emphasis": 0.95, + }, + "Missing quality": { + "key": "missing_quality", + "level": 2, + "legacy_score": 4, + "legacy_range": "3-4", + "reward": -0.75, + "quality_signal": -0.90, + "concept_signal": -0.75, + "detail_signal": -0.70, + "prompt_emphasis": 0.55, + }, + "I don't like it": { + "key": "dislike", + "level": 1, + "legacy_score": 2, + "legacy_range": "1-2", + "reward": -1.0, + "quality_signal": -1.0, + "concept_signal": -1.0, + "detail_signal": -1.0, + "prompt_emphasis": -0.65, + }, +} + def _clamp(value, low, high): return max(low, min(high, value)) +def normalize_refiner_rating(value): + if isinstance(value, str): + cleaned = value.strip() + if cleaned in RATING_PROFILES: + return dict(RATING_PROFILES[cleaned], label=cleaned) + try: + value = int(float(cleaned)) + except (TypeError, ValueError): + return dict(RATING_PROFILES["Missing concept"], label="Missing concept") + + try: + legacy_score = int(value) + except (TypeError, ValueError): + legacy_score = RATING_PROFILES["Missing concept"]["legacy_score"] + + legacy_score = int(_clamp(legacy_score, 1, 10)) + if legacy_score >= 9: + label = "I like it" + elif legacy_score >= 7: + label = "Missing details" + elif legacy_score >= 5: + label = "Missing concept" + elif legacy_score >= 3: + label = "Missing quality" + else: + label = "I don't like it" + + profile = dict(RATING_PROFILES[label], label=label) + profile["legacy_score"] = legacy_score + return profile + + PROTECTED_PHRASE_RE = re.compile(r'"([^"]+)"|“([^”]+)”|\\([^\\]+)\\') @@ -498,14 +596,7 @@ class FunPackVideoRefiner: "required": { "positive_conditioning": ("CONDITIONING",), "mode": (["ltx2", "wan"], {"default": "ltx2", "label": "Tokenizer Mode"}), - "rating": ("INT", { - "default": 5, - "min": 1, - "max": 10, - "step": 1, - "display": "slider", - "label": "Rating (1=horrific, 10=masterpiece)" - }), + "rating": (RATING_LABELS, {"default": "Missing concept", "label": "Rating"}), "refinement_key": ("STRING", {"default": "my_style_v1", "multiline": False}), "scheduler_mode": (["original", "accurate", "aggressive"], {"default": "original"}), }, @@ -1317,7 +1408,8 @@ class FunPackVideoRefiner: out[-1] = original[-1] return out - def _refine_sigma_schedule(self, sigmas, rating: int, global_adaptive: dict, strength_mode: str, seed: int): + def _refine_sigma_schedule(self, sigmas, rating: int, global_adaptive: dict, strength_mode: str, seed: int, + rating_profile: Optional[dict] = None): if not isinstance(sigmas, torch.Tensor): return torch.FloatTensor([]), "Sigma refinement inactive." @@ -1327,7 +1419,9 @@ class FunPackVideoRefiner: self._ensure_sigma_state_defaults(global_adaptive) - reward = (rating - 5.5) / 4.5 + rating_profile = rating_profile or normalize_refiner_rating(rating) + rating_key = rating_profile.get("key", "") + reward = float(rating_profile.get("reward", (rating - 5.5) / 4.5)) sigma_iterations = int(global_adaptive.get("sigma_iterations", 0)) profile = np.asarray(global_adaptive.get("sigma_profile", [0.0] * 32), dtype=np.float32) @@ -1374,12 +1468,13 @@ class FunPackVideoRefiner: global_adaptive["sigma_avg_reward_ema"] = sigma_avg_reward_ema global_adaptive["sigma_exploration_base"] = max( 0.012, - min(0.05, sigma_exploration_base * (0.97 if rating >= 8 else 1.02)) + min(0.05, sigma_exploration_base * (0.97 if rating_key == "like" else 1.02)) ) sigma_history = list(global_adaptive.get("sigma_history", []))[-119:] sigma_history.append({ "iteration": sigma_iterations + 1, "rating": int(rating), + "rating_label": rating_profile.get("label", str(rating)), "reward": round(float(reward), 6), "lr": round(float(sigma_lr), 6), }) @@ -1524,7 +1619,8 @@ class FunPackVideoRefiner: except OSError: return False - def _refine_latent(self, latent, refinement_key, mode, rating, reward, global_adaptive): + def _refine_latent(self, latent, refinement_key, mode, rating, reward, global_adaptive, + rating_profile: Optional[dict] = None): if isinstance(latent, dict) and not latent_is_plain_video_tensor(latent): self._raise_wrong_latent(latent) @@ -1584,7 +1680,16 @@ class FunPackVideoRefiner: latent_state["status"] = "Latent: only zero-valued positions available, passthrough." return refined_latent, latent_state["status"] - lr = 0.035 if rating >= 7 else 0.055 if rating <= 4 else 0.018 + rating_profile = rating_profile or normalize_refiner_rating(rating) + rating_key = rating_profile.get("key", "") + if rating_key == "like": + lr = 0.035 + elif rating_key in {"missing_quality", "dislike"}: + lr = 0.055 + elif rating_key == "missing_concept": + lr = 0.030 + else: + lr = 0.018 target_delta = (saved_samples - current) * nonzero_mask.to(dtype=current.dtype) update = target_delta * (reward * lr) momentum = 0.82 * momentum + 0.18 * update @@ -1601,6 +1706,7 @@ class FunPackVideoRefiner: latent_state["momentum"] = tensor_to_serializable(momentum.detach().cpu()) latent_state["last_rating"] = int(rating) + latent_state["last_rating_label"] = rating_profile.get("label", str(rating)) latent_state["last_reward"] = round(float(reward), 4) latent_state["nonzero_ratio"] = round(float(nonzero_mask.float().mean().item()), 6) latent_state["status"] = ( @@ -1690,30 +1796,45 @@ class FunPackVideoRefiner: return min(1.0, best_score), best_labels[:4], best_importance def _update_lora_weight_suggestions(self, lora_stack, active, global_adaptive, ordered_concept_ids, - concept_clusters, current_concept_labels, rating, reward): + concept_clusters, current_concept_labels, rating, reward, + rating_profile: Optional[dict] = None): if not isinstance(lora_stack, dict) or not lora_stack.get("loras"): return "LoRA suggestions: no FunPack LoRA stack connected." + rating_profile = rating_profile or normalize_refiner_rating(rating) + rating_key = rating_profile.get("key", "") memory = global_adaptive.setdefault("lora_weight_memory", {}) suggestions = {} status_parts = [] + relation_cache = {} + top_concept_relation = 0.0 + concept_lora_count = 0 for entry in lora_stack.get("loras", []): lora_type = entry.get("type", "general") - profile = LORA_REFINER_TYPE_PROFILES.get(lora_type, LORA_REFINER_TYPE_PROFILES["general"]) - lora_id, state = self._ensure_lora_memory(memory, entry) + lora_id = entry.get("id") or self._lora_state_id(entry.get("name", ""), lora_type) relation, matched_labels, concept_importance = self._score_lora_prompt_relation( entry, ordered_concept_ids, concept_clusters, current_concept_labels, ) + relation_cache[lora_id] = (relation, matched_labels, concept_importance) + if lora_type in {"concept", "character"}: + concept_lora_count += 1 + top_concept_relation = max(top_concept_relation, relation) + + for entry in lora_stack.get("loras", []): + lora_type = entry.get("type", "general") + profile = LORA_REFINER_TYPE_PROFILES.get(lora_type, LORA_REFINER_TYPE_PROFILES["general"]) + lora_id, state = self._ensure_lora_memory(memory, entry) + relation, matched_labels, concept_importance = relation_cache.get(lora_id, (0.0, [], 1.0)) state["reward_ema"] = 0.84 * float(state.get("reward_ema", 0.0)) + 0.16 * reward - if rating >= 8: + if rating_key == "like": state["good_streak"] = int(state.get("good_streak", 0)) + 1 state["bad_streak"] = 0 - elif rating <= 4: + elif rating_key in {"missing_quality", "dislike"}: state["bad_streak"] = int(state.get("bad_streak", 0)) + 1 state["good_streak"] = 0 else: @@ -1724,7 +1845,7 @@ class FunPackVideoRefiner: stable_offset = state.get("stable_offset_ratio") culprit_score = float(state.get("culprit_score", 0.0)) culprit_hits = int(state.get("culprit_hits", 0)) - if stable_offset is not None and rating >= 6: + if stable_offset is not None and rating_key in {"like", "missing_details"}: offset = 0.72 * offset + 0.28 * float(stable_offset) step = profile["step"] * max(0.15, relation) @@ -1739,7 +1860,15 @@ class FunPackVideoRefiner: concept_match_strength += 0.30 if matched_labels else 0.0 concept_match_strength += max(0.0, relation - 0.30) * 1.35 - if rating >= 8: + is_primary_concept_lora = ( + lora_type in {"concept", "character"} and + ( + concept_lora_count == 1 or + (top_concept_relation > 0.0 and relation >= max(0.30, top_concept_relation - 1e-6)) + ) + ) + + if rating_key == "like": value_mult = 0.75 + min(1.4, max(0.5, concept_importance)) * 0.25 offset += step * (0.45 + max(0.0, reward)) * value_mult culprit_score *= 0.72 @@ -1750,7 +1879,54 @@ class FunPackVideoRefiner: stable_offset = 0.78 * float(stable_offset) + 0.22 * offset state["stable_offset_ratio"] = _clamp(stable_offset, min_offset, max_offset) offset = state["stable_offset_ratio"] - elif rating <= 4: + elif rating_key == "missing_details": + culprit_score *= 0.86 + culprit_hits = max(0, culprit_hits - 1) + if relation >= 0.25 and lora_type in {"concept", "character", "general"}: + value_mult = 0.85 + min(1.2, max(0.5, concept_importance)) * 0.20 + offset += step * (0.30 + relation * 0.45) * value_mult + elif relation <= 0.05: + offset *= 0.96 + elif stable_offset is not None: + offset = 0.78 * offset + 0.22 * float(stable_offset) + elif rating_key == "missing_concept": + state["stable_offset_ratio"] = None + if is_primary_concept_lora: + max_offset = max(max_offset, profile["bad_max_offset"] * 0.82) + offset += step * (1.10 + abs(reward) * 1.25) * concept_match_strength + culprit_score *= 0.70 + culprit_hits = max(0, culprit_hits - 1) + elif lora_type in {"concept", "character"} and relation > 0.08: + culprit_score = _clamp(culprit_score * 0.75 + 0.35 * max(0.25, relation), 0.0, 2.5) + culprit_hits = culprit_hits + 1 if culprit_score >= 0.65 else culprit_hits + min_offset = min(min_offset, profile["bad_min_offset"] * 0.42) + offset -= step * (0.55 + abs(reward)) * max(0.25, relation) + elif relation <= 0.05: + offset *= 0.96 + else: + offset *= 0.92 + elif rating_key == "missing_quality": + state["stable_offset_ratio"] = None + if lora_type == "quality": + max_offset = max(max_offset, profile["bad_max_offset"] * 0.70) + offset += step * (1.00 + abs(reward) * 0.85) * max(0.45, effective_relation) + culprit_score *= 0.76 + elif is_primary_concept_lora: + max_offset = max(max_offset, profile["bad_max_offset"] * 0.55) + offset += step * (0.62 + abs(reward) * 0.55) * concept_match_strength + culprit_score *= 0.82 + elif lora_type in {"concept", "character"} and relation > 0.08: + culprit_score = _clamp(culprit_score * 0.78 + 0.42 * max(0.25, relation), 0.0, 2.5) + culprit_hits = culprit_hits + 1 if culprit_score >= 0.65 else culprit_hits + min_offset = min(min_offset, profile["bad_min_offset"] * 0.35) + offset -= step * (0.72 + abs(reward)) * max(0.25, relation) + else: + culprit_score = _clamp(culprit_score * 0.84 + 0.18 * max(0.0, relation), 0.0, 2.5) + if relation <= 0.05: + offset *= 0.94 + else: + offset -= step * 0.25 * max(0.20, relation) + elif rating_key == "dislike": severity = _clamp((5.0 - float(rating)) / 4.0, 0.0, 1.0) culprit_signal = max(0.20, effective_relation) * (0.70 + base_abs * 0.30) if is_concept_lora: @@ -1810,6 +1986,8 @@ class FunPackVideoRefiner: "relation": relation, "matched_concepts": matched_labels, "rating": int(rating), + "rating_label": rating_profile.get("label", str(rating)), + "rating_range": rating_profile.get("legacy_range", ""), "good_streak": state.get("good_streak", 0), "bad_streak": state.get("bad_streak", 0), "stable": state.get("stable_offset_ratio") is not None, @@ -1827,7 +2005,7 @@ class FunPackVideoRefiner: active["last_lora_stack"] = lora_stack if not status_parts: return "LoRA suggestions: stack empty." - return "LoRA suggestions: " + " | ".join(status_parts) + return f"LoRA suggestions ({rating_profile.get('label', str(rating))}): " + " | ".join(status_parts) # ========================================================================= # MAIN REFINE @@ -1843,6 +2021,10 @@ class FunPackVideoRefiner: mode = (mode or "ltx2").lower() if mode not in self._tokenizer_sources: mode = "ltx2" + rating_profile = normalize_refiner_rating(rating) + rating_label = rating_profile.get("label", str(rating)) + rating = int(rating_profile.get("legacy_score", 6)) + reward = float(rating_profile.get("reward", (rating - 5.5) / 4.5)) if seed != 0: torch.manual_seed(seed) @@ -1921,7 +2103,8 @@ class FunPackVideoRefiner: prompt_key: { "reference_embeds": tensor_to_serializable(raw_positive), "history": [], - "last_rating": rating + "last_rating": rating, + "last_rating_label": rating_label } }, "last_prompt_key": prompt_key, @@ -1938,8 +2121,9 @@ class FunPackVideoRefiner: refinement_key, mode, rating, - (rating - 5.5) / 4.5, + reward, {}, + rating_profile, ) else: fallback_latent, fallback_latent_status = self._latent_refinement_disabled(latent) @@ -1964,8 +2148,9 @@ class FunPackVideoRefiner: refinement_key, mode, rating, - (rating - 5.5) / 4.5, + reward, {}, + rating_profile, ) else: fallback_latent, fallback_latent_status = self._latent_refinement_disabled(latent) @@ -2027,7 +2212,8 @@ class FunPackVideoRefiner: active = { "reference_embeds": tensor_to_serializable(raw_positive), "history": [], - "last_rating": rating + "last_rating": rating, + "last_rating_label": rating_label } prompt_histories[prompt_key] = active @@ -2138,19 +2324,23 @@ class FunPackVideoRefiner: except Exception: similarity = 0.0 - reward = (rating - 5.5) / 4.5 last_rating = active.get("last_rating", 5) - last_reward = (last_rating - 5.5) / 4.5 + last_rating_profile = normalize_refiner_rating(active.get("last_rating_label", last_rating)) + if "last_rating_label" not in active: + last_rating_profile = normalize_refiner_rating(last_rating) + last_reward = float(last_rating_profile.get("reward", (last_rating - 5.5) / 4.5)) rating_shift = abs(rating - last_rating) feedback_memory["rating_change_events"] = list(feedback_memory.get("rating_change_events", []))[-15:] feedback_memory["rating_change_events"].append({ "iteration": iter_num, "rating": rating, + "rating_label": rating_label, "shift": rating_shift, "prompt_key": prompt_key, }) word_importance = global_adaptive["word_importance"] + prompt_emphasis = float(rating_profile.get("prompt_emphasis", reward)) lr_scale, confidence, exploration_mult, word_lr_mult = self._get_scheduler_factors( scheduler_mode, rating, reward, similarity, iter_num, total_iters, @@ -2166,7 +2356,7 @@ class FunPackVideoRefiner: wkey = full_word.lower() base_lr = 0.22 / (1 + 0.07 * (len(history) ** 0.5)) - group_delta = reward * base_lr * lr_scale * confidence + group_delta = prompt_emphasis * base_lr * lr_scale * confidence if wkey in word_lr_mult: group_delta *= word_lr_mult[wkey] @@ -2215,9 +2405,13 @@ class FunPackVideoRefiner: global_adaptive["avg_reward_ema"] = avg_reward_ema good_ratio = global_adaptive["good_ratio"] - if rating >= 8: + rating_key = rating_profile.get("key", "") + if rating_key == "like": good_ratio = 0.9 * good_ratio + 0.1 * 1.0 expl = max(0.015, expl * 0.96) + elif rating_key == "missing_details": + good_ratio = 0.9 * good_ratio + 0.1 * 0.55 + expl = min(0.12, expl * 1.02) else: good_ratio = 0.9 * good_ratio + 0.1 * 0.0 expl = min(0.12, expl * 1.08) @@ -2240,23 +2434,25 @@ class FunPackVideoRefiner: prev_modified = torch.zeros_like(old_reference) prev_delta = prev_modified - old_reference multiplier = max(0.05, 1.0 + reward * 1.45) - if rating == last_rating and rating >= 8: + if rating == last_rating and rating_key == "like": multiplier += 0.35 noise = torch.randn_like(old_reference) * (expl * (1.0 - avg_reward_ema * 0.7)) new_delta = (prev_delta * multiplier) + noise + (momentum * 0.6) else: good_deltas = [] for entry in history: - if entry.get("rating", 0) >= 7: - mod_data = entry.get("modified_embeds") - if mod_data is None: - continue - try: - mod = serializable_to_tensor(mod_data).to(device) - if list(mod.shape) == list(old_reference.shape): - good_deltas.append(mod - old_reference) - except Exception: - continue + entry_profile = normalize_refiner_rating(entry.get("rating_label", entry.get("rating", 0))) + if entry_profile.get("key") not in {"like", "missing_details"}: + continue + mod_data = entry.get("modified_embeds") + if mod_data is None: + continue + try: + mod = serializable_to_tensor(mod_data).to(device) + if list(mod.shape) == list(old_reference.shape): + good_deltas.append(mod - old_reference) + except Exception: + continue if good_deltas: new_delta = torch.stack(good_deltas).mean(dim=0) * (0.7 + reward * 0.4) else: @@ -2344,6 +2540,8 @@ class FunPackVideoRefiner: history_entry = { "iteration": iter_num, "rating": rating, + "rating_label": rating_label, + "rating_range": rating_profile.get("legacy_range", ""), "reward": round(reward, 3), "modified_embeds": tensor_to_serializable(new_positive), "similarity": round(similarity, 4), @@ -2360,6 +2558,7 @@ class FunPackVideoRefiner: active["history"] = history active["last_rating"] = rating + active["last_rating_label"] = rating_label active["last_positive_prompt"] = positive_prompt data["last_prompt_key"] = prompt_key data["prompt_histories"] = prompt_histories @@ -2371,6 +2570,7 @@ class FunPackVideoRefiner: global_adaptive, sigma_strength, seed, + rating_profile, ) if latent_output_connected: refined_latent, latent_status = self._refine_latent( @@ -2380,6 +2580,7 @@ class FunPackVideoRefiner: rating, reward, global_adaptive, + rating_profile, ) else: refined_latent, latent_status = self._latent_refinement_disabled(latent) @@ -2488,6 +2689,7 @@ class FunPackVideoRefiner: current_concept_labels, rating, reward, + rating_profile, ) normalized_reward = max(0.0, min(1.0, (avg_reward_ema + 1.0) / 2.0)) @@ -2497,8 +2699,9 @@ class FunPackVideoRefiner: training_info = ( f"Mode: {mode.upper()} | Scheduler: {scheduler_mode.upper()} | Step: {global_adaptive['current_step']} | " + f"Rating: {rating_label} ({rating_profile.get('legacy_range', rating)}) | " f"EMA Reward: {avg_reward_ema:+.3f} | Confidence: {confidence:.2f} | LR Scale: {lr_scale:.3f}\n" - f"Exploration: {expl:.3f} | Similarity: {similarity:.4f} | Good Ratio: {good_ratio:.1%}\n" + f"Exploration: {expl:.3f} | Similarity: {similarity:.4f} | Good Ratio: {good_ratio:.1%} | Prompt Emphasis: {prompt_emphasis:+.2f}\n" f"**Learning Loss: {learning_loss:.4f}** (lower is better)\n" f"Dominant concept: {dominant_line}\n" f"Concept phrases ({len(concept_clusters)} total): {concept_line}\n" @@ -2520,6 +2723,7 @@ class FunPackVideoRefiner: "total_iteration": global_total_iterations, "learning_loss": round(float(learning_loss), 6), "rating": int(rating), + "rating_label": rating_label, "similarity": round(float(similarity), 6), "scheduler_mode": scheduler_mode, "mode": mode, @@ -2572,7 +2776,7 @@ class FunPackVideoRefiner: ) status = ( - f"{health} | Mode {mode.upper()} | Rating {rating}/10 {trend} | Iter {iter_num}\n" + f"{health} | Mode {mode.upper()} | Rating {rating_label} ({rating_profile.get('legacy_range', rating)}) {trend} | Iter {iter_num}\n" f"Session: {len(prompt_histories)} prompt(s), {global_total_iterations} total update(s), " f"{len(history)} history item(s) on this prompt\n" f"Focus: {dominant_line} | {feedback_state}\n" diff --git a/docs/FunPackGemmaEmbeddingRefiner.md b/docs/FunPackGemmaEmbeddingRefiner.md index 1823411a..d398215c 100644 --- a/docs/FunPackGemmaEmbeddingRefiner.md +++ b/docs/FunPackGemmaEmbeddingRefiner.md @@ -8,7 +8,15 @@ This node analyzes the positive conditioning produced by your text encoder, trac **mode**: Tokenizer mode used for prompt analysis. Use `ltx2` for LTX-style workflows and `wan` for Wan workflows. -**rating**: Rating for the previously generated result, from 1 to 10, where 1 means "awful" and 10 means "masterpiece". +**rating**: Categorical feedback for the previously generated result: + +- `I like it`: The video fully matches the request, or is very close. +- `Missing details`: The video is visually solid and the main concept is present, but requested details were ignored or changed. +- `Missing concept`: The video is visually solid, but the requested concept and details are absent. +- `Missing quality`: The video may be visually pleasant, but anatomy, scene construction, concept, and details are not acceptable. +- `I don't like it`: The output is low quality, does not represent the requested concept, and ignores details. + +Older numeric workflows are still understood internally: `9-10` maps to `I like it`, `7-8` to `Missing details`, `5-6` to `Missing concept`, `3-4` to `Missing quality`, and `1-2` to `I don't like it`. **refinement_key**: Name of the refinement session. The node stores its learned refinement data under this key so you can continue training the same style later. diff --git a/docs/FunPackLoraWorkflow.md b/docs/FunPackLoraWorkflow.md index e25ad2eb..20adfcd9 100644 --- a/docs/FunPackLoraWorkflow.md +++ b/docs/FunPackLoraWorkflow.md @@ -38,11 +38,12 @@ When `per_block` is enabled on an `ltx2` stack, the loader keeps the user-facing Connect the `lora_stack` output from `FunPack LoRA Loader` into the optional `lora_stack` input on `FunPack Video Refiner`. -The refiner compares each LoRA's declared type and filename with the concepts it already extracted from the prompt. After processing the rating, it saves `lora_weight_suggestions` into the same prompt entry in the refiner JSON. +The refiner compares each LoRA's declared type and filename with the concepts it already extracted from the prompt. After processing the rating category, it saves `lora_weight_suggestions` into the same prompt entry in the refiner JSON. Examples: -- A `quality` LoRA matching a valuable quality concept can be boosted after good ratings. -- A `concept` LoRA matching a bad-rated prompt is treated more aggressively than broad LoRAs, so it can be muted or inverted faster if it keeps distorting that concept. -- Repeated bad ratings can mark a LoRA as a likely culprit, push its automatic weight down to `0.0`, and even invert it with a negative weight if it keeps ruining output. -- Consistently good ratings can stabilize around a saved offset instead of continually drifting. +- `I like it` boosts matching LoRAs gently and can stabilize repeated good offsets. +- `Missing details` keeps the main direction mostly positive while nudging related concept, character, and general LoRAs upward. +- `Missing concept` boosts the best-matching concept or character LoRA, while reducing weaker competing concept LoRAs that may be crowding it out. +- `Missing quality` can boost quality LoRAs and the best concept match, while reducing unrelated or competing concept LoRAs. +- `I don't like it` treats matching concept LoRAs more aggressively than broad LoRAs, so repeated failures can mark a LoRA as a likely culprit, push its automatic weight down to `0.0`, or invert it with a negative weight.