Stage 2 Part B: "Wrong appearance" → consistency anchor

Reworks the "Wrong appearance" rating from a (now-removed) prompt-repair
signal into a conditioning-level consistency anchor (option b). "Wrong
appearance" now means "character consistency wasn't preserved" and steers
the next generation back toward the established character.

Capture (_v2_update_appearance_anchor, wired into both the single-run
learning path and the shared per-scene suite _v2_learn_scene_into_state):
  - good rating (like/nailed_it/loved_it/loved) → store the rated run's
    conditioning as the per-key "blessed appearance" (overwrite = last good)
  - wrong_appearance → store the rejected run's conditioning as the "drift"
global_state is already per-refinement-key, so both live with that key.

Apply (_v2_apply_conditioning_memory_impl, after the intent-family delta):
when the user rated the previous gen wrong_appearance, gently pull this gen
toward the blessed appearance (lerp 0.05) and repel from the drift (0.04),
reusing _v2_apply_conditioning_payload / _v2_repel_conditioning_payload. No
ramping; the existing 7.5% delta cap bounds the net effect. No-op when
nothing is stored or shapes mismatch.

Drops the dead wrong_categories field (zero consumers after Part A) and
keeps skip_value_function (the anchor is a conditioning nudge, not a reward).

Tests: 7 new unit tests (storage + overwrite + skip + pull/repel + idle +
non-trigger); updated test_wrong_appearance_rating_is_available for the new
semantics. Zero new failures vs HEAD (the 10/31 remaining are pre-existing
env failures from missing server/comfy modules).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-15 18:44:55 +03:00
co-authored by Claude Opus 4.8
parent 61e7097416
commit df011b927f
2 changed files with 207 additions and 7 deletions
+74 -6
View File
@@ -6113,21 +6113,24 @@ V2_RATING_PROFILES = {
"Missing action": {"key": "missing_action", "reward": 0.05, "level": 5, "missing_axes": ["action"]},
"Missing quality": {"key": "missing_quality", "reward": -0.30, "level": 4, "missing_axes": ["quality"]},
"Missing details + action": {"key": "missing_details_action", "reward": -0.10, "level": 3, "missing_axes": ["details", "action"]},
# Wrong-* are prompt-REPAIR signals ("good gen, but the words/identity were off"), not quality
# rewards. `skip_value_function` keeps them out of value-function + Absolute-taste reward
# training (where a 0.0/low reward on a visually-good gen poisons the learned quality landscape
# and the ascent direction → drift/wrong-character). They STILL drive prompt repair and the
# relative direction/category memory via their missing_axes/wrong_axes/wrong_categories.
# Wrong-* are quality-neutral signals ("good gen, but the words/identity were off"), not
# quality rewards. `skip_value_function` keeps them out of value-function + Absolute-taste
# reward training (where a 0.0/low reward on a visually-good gen poisons the learned quality
# landscape and the ascent direction → drift/wrong-character). They STILL drive the relative
# direction/category memory via their missing_axes/wrong_axes.
"Wrong details": {"key": "wrong_details", "reward": 0.20, "level": 5, "missing_axes": ["details"], "wrong_axes": ["details"], "skip_value_function": True},
"Wrong action": {"key": "wrong_action", "reward": -0.10, "level": 4, "missing_axes": ["action"], "wrong_axes": ["action"], "skip_value_function": True},
"Wrong action + quality": {"key": "wrong_action_quality", "reward": -0.40, "level": 3, "missing_axes": ["quality"], "wrong_axes": ["action"], "skip_value_function": True},
"Wrong details + action": {"key": "wrong_details_action", "reward": 0.00, "level": 3, "missing_axes": ["details", "action"], "wrong_axes": ["details", "action"], "skip_value_function": True},
# "Wrong appearance" = character consistency wasn't preserved. Drives the consistency anchor
# (_v2_update_appearance_anchor / _v2_apply_conditioning_memory_impl): pull the next gen toward
# the last good-rated conditioning for this key and repel from the rejected drift. Not a reward
# (skip_value_function) — it's a conditioning nudge, not a quality score.
"Wrong appearance": {
"key": "wrong_appearance",
"reward": 0.0,
"level": 4,
"missing_axes": [],
"wrong_categories": ["appearance", "subject", "environment"],
"skip_value_function": True,
},
"Missing details + quality": {"key": "missing_details_quality", "reward": -0.40, "level": 2, "missing_axes": ["details", "quality"]},
@@ -6858,6 +6861,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
seed_output_connected=bool(seed_output_connected),
)
self._v2_update_intent_family_memory(target_global, scene_run, profile, iter_num, axis_feedback)
self._v2_update_appearance_anchor(target_global, scene_run, profile, iter_num)
self._v2_update_negative_prompt_memory(target_global, scene_run, profile, axis_feedback)
self._v2_update_intent_alignment_memory(target_global, scene_run, profile, iter_num, axis_feedback)
self._v2_update_conditioning_memory(target_global, scene_run, profile, axis_feedback)
@@ -8365,6 +8369,42 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
slot["count"] = 1
return True
def _v2_update_appearance_anchor(self, global_state, previous_run, rating_profile, iter_num):
"""Consistency anchor for the "Wrong appearance" rating. global_state is already
per-refinement-key, so these live with that key automatically.
On a good rating, remember the rated run's conditioning as the "blessed appearance"
(the last good character). On a Wrong-appearance rating, remember the rejected run's
conditioning as the "drift" to repel from. _v2_apply_conditioning_memory_impl pulls the
next gen toward the blessed appearance and away from the drift when the user rated the
previous gen Wrong appearance."""
if not isinstance(global_state, dict) or not isinstance(previous_run, dict):
return "Appearance anchor: no update."
if rating_profile.get("skip_learning"):
return "Appearance anchor: no update."
conditioning = previous_run.get("conditioning")
if not isinstance(conditioning, dict):
return "Appearance anchor: run carried no conditioning."
key = rating_profile.get("key", "")
is_good = key in {"like", "nailed_it", "loved_it"} or bool(rating_profile.get("loved_modifier"))
if is_good:
existing = global_state.get("appearance_anchor") or {}
global_state["appearance_anchor"] = {
"conditioning": conditioning,
"prompt": self._v2_prompt_key(previous_run.get("prompt", "")),
"count": int(existing.get("count", 0)) + 1,
"last_seen_iter": int(iter_num),
}
return "Appearance anchor: blessed appearance stored."
if key == "wrong_appearance":
global_state["appearance_drift"] = {
"conditioning": conditioning,
"prompt": self._v2_prompt_key(previous_run.get("prompt", "")),
"last_seen_iter": int(iter_num),
}
return "Appearance anchor: drift stored (repel on next gen)."
return "Appearance anchor: no update."
def _v2_update_intent_family_memory(self, global_state, last_run, rating_profile, iter_num, axis_feedback=None):
if not isinstance(global_state, dict) or not isinstance(last_run, dict) or rating_profile.get("skip_learning"):
return "Intent family: no learning update.", ""
@@ -9971,6 +10011,25 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
mixed, family_delta_status = self._v2_apply_intent_family_delta(mixed, intent_family_slot, strength)
# --- Consistency anchor (Wrong appearance) ---
# "Wrong appearance" means the established character drifted. Pull this run gently back
# toward the last good-rated conditioning for this key (the "blessed appearance") and
# repel from the just-rejected drifted look. Gentle fixed strengths; the global 7.5%
# delta cap below bounds the net effect. No-op when nothing is stored or shapes mismatch.
appearance_status = "appearance-anchor idle"
if rating_profile.get("key") == "wrong_appearance":
anchor = global_state.get("appearance_anchor") or {}
drift = global_state.get("appearance_drift") or {}
anchor_actions = []
if self._v2_shape_compatible(anchor.get("conditioning"), mixed):
mixed = self._v2_apply_conditioning_payload(mixed, anchor.get("conditioning"), 0.05)
anchor_actions.append("pull→blessed")
if self._v2_shape_compatible(drift.get("conditioning"), mixed):
mixed = self._v2_repel_conditioning_payload(mixed, drift.get("conditioning"), 0.04)
anchor_actions.append("repel←drift")
if anchor_actions:
appearance_status = "appearance-anchor: " + " + ".join(anchor_actions)
axis_memory = global_state.get("axis_conditioning_memory", {})
axis_actions = []
missing_axes = set(axis_feedback.get("missing_axes", []))
@@ -10096,6 +10155,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
f" Bad conditioning: {'direction' if bad_count >= 3 else f'lerp fallback ({bad_count}/3 runs)'}\n"
f" Axis adjustments: {axis_str}\n"
f" {family_delta_status}\n"
f" {appearance_status}\n"
f" Total delta clamped to 7.5% of conditioning norm"
)
@@ -11610,6 +11670,8 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
global_state.setdefault("intent_alignment_memory", {})
global_state.setdefault("intent_family_memory", {})
global_state.setdefault("perfect_anchors", {})
global_state.setdefault("appearance_anchor", {})
global_state.setdefault("appearance_drift", {})
global_state.setdefault("variant_evidence", {})
global_state.setdefault("intent_preference_phrases", {})
global_state.setdefault("conditioning_deltas", {})
@@ -11711,6 +11773,12 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
int(global_state.get("total_iterations", 0)) + 1,
axis_feedback,
)
self._v2_update_appearance_anchor(
global_state,
previous_run,
learning_profile,
int(global_state.get("total_iterations", 0)) + 1,
)
negative_memory_status = self._v2_update_negative_prompt_memory(
global_state,
previous_run,
+133 -1
View File
@@ -256,7 +256,10 @@ def test_wrong_appearance_rating_is_available():
assert "Wrong appearance" in V2_RATING_LABELS
assert profile["key"] == "wrong_appearance"
assert profile["wrong_categories"] == ["appearance", "subject", "environment"]
# Wrong appearance now drives the consistency anchor, not prompt repair (the dead
# wrong_categories field was dropped in Stage 2 Part B). It stays quality-neutral.
assert "wrong_categories" not in profile
assert profile["skip_value_function"] is True
def test_scene_builder_user_category_overrides_refiner_classification():
@@ -2067,3 +2070,132 @@ def test_split_by_transitions_uses_provided_scene_seeds(tmp_path):
assert [item[1]["funpack_scene_seed"] for item in cond] == [900, 901]
assert all(item[1]["funpack_seed_source"] == "successful seed memory" for item in cond)
# --- Stage 2 Part B: "Wrong appearance" consistency anchor ---
def _mean_cosine(a, b):
a = a.reshape(-1, a.shape[-1]).float()
b = b.reshape(-1, b.shape[-1]).float()
a = a / a.norm(dim=-1, keepdim=True).clamp_min(1e-8)
b = b / b.norm(dim=-1, keepdim=True).clamp_min(1e-8)
return float((a * b).sum(dim=-1).mean().item())
def test_appearance_anchor_stores_blessed_on_good_rating():
refiner = FunPackVideoRefinerV2()
global_state = {}
payload = tensor_to_serializable(torch.randn(1, 6, 8))
previous_run = {"conditioning": payload, "prompt": "woman in red dress"}
status = refiner._v2_update_appearance_anchor(
global_state, previous_run, normalize_refiner_v2_rating("Perfect"), 3
)
assert "blessed" in status.lower()
assert global_state["appearance_anchor"]["conditioning"] is payload
assert global_state["appearance_anchor"]["count"] == 1
# A good rating should NOT populate the drift slot.
assert "appearance_drift" not in global_state
def test_appearance_anchor_stores_drift_on_wrong_appearance():
refiner = FunPackVideoRefinerV2()
global_state = {}
payload = tensor_to_serializable(torch.randn(1, 6, 8))
previous_run = {"conditioning": payload, "prompt": "woman in red dress"}
refiner._v2_update_appearance_anchor(
global_state, previous_run, normalize_refiner_v2_rating("Wrong appearance"), 4
)
assert global_state["appearance_drift"]["conditioning"] is payload
# Wrong appearance must not overwrite/seed the blessed anchor.
assert "appearance_anchor" not in global_state
def test_good_rating_overwrites_blessed_anchor():
refiner = FunPackVideoRefinerV2()
global_state = {}
first = tensor_to_serializable(torch.randn(1, 6, 8))
second = tensor_to_serializable(torch.randn(1, 6, 8))
refiner._v2_update_appearance_anchor(
global_state, {"conditioning": first, "prompt": "a"}, normalize_refiner_v2_rating("Perfect"), 1
)
refiner._v2_update_appearance_anchor(
global_state, {"conditioning": second, "prompt": "b"}, normalize_refiner_v2_rating("Nailed it"), 2
)
assert global_state["appearance_anchor"]["conditioning"] is second
assert global_state["appearance_anchor"]["count"] == 2
def test_appearance_anchor_skips_when_no_conditioning_or_skip_learning():
refiner = FunPackVideoRefinerV2()
global_state = {}
# No conditioning payload on the run.
refiner._v2_update_appearance_anchor(
global_state, {"prompt": "x"}, normalize_refiner_v2_rating("Perfect"), 1
)
assert "appearance_anchor" not in global_state
# skip_learning rating (e.g. -Just forget it-) never stores.
payload = tensor_to_serializable(torch.randn(1, 6, 8))
refiner._v2_update_appearance_anchor(
global_state, {"conditioning": payload}, normalize_refiner_v2_rating("-Just forget it-"), 1
)
assert "appearance_anchor" not in global_state
def test_wrong_appearance_pulls_toward_blessed_and_repels_drift():
refiner = FunPackVideoRefinerV2()
torch.manual_seed(0)
original = torch.randn(1, 6, 8)
blessed = torch.randn(1, 6, 8)
drift = torch.randn(1, 6, 8)
global_state = {
"appearance_anchor": {"conditioning": tensor_to_serializable(blessed)},
"appearance_drift": {"conditioning": tensor_to_serializable(drift)},
}
refined, status = refiner._v2_apply_conditioning_memory(
original.clone(), global_state, normalize_refiner_v2_rating("Wrong appearance")
)
assert "appearance-anchor: pull→blessed + repel←drift" in status
# Output moved toward the blessed appearance and away from the drift (direction).
assert _mean_cosine(refined, blessed) > _mean_cosine(original, blessed)
assert _mean_cosine(refined, drift) < _mean_cosine(original, drift)
# Gentle: per-token norm is preserved and the net change is small.
assert torch.allclose(
refined.norm(dim=-1), original.norm(dim=-1), atol=1e-3
)
assert (refined - original).norm() / original.norm() < 0.08
def test_appearance_anchor_idle_without_blessed():
refiner = FunPackVideoRefinerV2()
torch.manual_seed(1)
original = torch.randn(1, 6, 8)
refined, status = refiner._v2_apply_conditioning_memory(
original.clone(), {}, normalize_refiner_v2_rating("Wrong appearance")
)
assert "appearance-anchor idle" in status
assert torch.allclose(refined, original, atol=1e-4)
def test_appearance_anchor_not_applied_on_non_wrong_appearance_rating():
refiner = FunPackVideoRefinerV2()
torch.manual_seed(2)
original = torch.randn(1, 6, 8)
blessed = torch.randn(1, 6, 8)
global_state = {"appearance_anchor": {"conditioning": tensor_to_serializable(blessed)}}
# A "Missing details" rating must not trigger the appearance anchor pull.
refined, status = refiner._v2_apply_conditioning_memory(
original.clone(), global_state, normalize_refiner_v2_rating("Missing details")
)
assert "appearance-anchor idle" in status