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
+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