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:
co-authored by
Claude Opus 4.8
parent
61e7097416
commit
df011b927f
+74
-6
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user