Update video refiner rating categories

This commit is contained in:
digital-garbage
2026-04-24 02:43:49 +03:00
parent 7438cfb125
commit 009e1edf8a
3 changed files with 262 additions and 49 deletions
+247 -43
View File
@@ -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"
+9 -1
View File
@@ -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.
+6 -5
View File
@@ -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.