diff --git a/conditioning.py b/conditioning.py index 8e520e65..6622abf9 100644 --- a/conditioning.py +++ b/conditioning.py @@ -4791,10 +4791,14 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): return seed_memory_status def _v2_learn_scene_into_keys(self, scene_keys, project_key, scene_run, profile, iter_num, - axis_feedback, seed_output_connected): + axis_feedback, seed_output_connected, is_h3=False): """Train every non-default key that participated in a scene. Each key's own state file gets the full learning suite + its value-function update, then is saved. The project - key is skipped here (it already learns into the run's main global_state).""" + key is skipped here (it already learns into the run's main global_state). + + `is_h3` skips the value-function update: nothing on H3 reads it (see refine_v2's + is_h3 override) -- the rest of the learning suite still runs, it is only this + mechanism-specific training that has nowhere to go on H3.""" trained = [] for key in scene_keys or []: key = str(key or "").strip() @@ -4806,7 +4810,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): self._v2_learn_scene_into_state( key_global, scene_run, profile, iter_num, axis_feedback, seed_output_connected, ) - if not profile.get("skip_learning") and self._v2_reward_admissible(profile): + if not is_h3 and not profile.get("skip_learning") and self._v2_reward_admissible(profile): self._v2_train_value_function( refinement_state_path(key, "value_fn", prefix="refine_v2", extension="pt"), scene_run.get("conditioning"), @@ -4834,6 +4838,11 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): """Apply Refiner V2 learning once per rated scene (Movie Editor multi-scene chains).""" if not isinstance(previous_run, dict) or not scene_ratings: return "Movie Editor ratings: none.", None + try: + from . import minimax_h3 as _h3fam + except ImportError: + import minimax_h3 as _h3fam + is_h3 = _h3fam.is_h3_clip(clip) scene_count = int(previous_run.get("scene_count") or len(previous_run.get("scene_texts") or []) or 1) if scene_count <= 1: return "Movie Editor ratings: previous run was single-scene.", None @@ -4873,9 +4882,10 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # so a clip's key never trains the default (and vice versa). trained_keys = self._v2_learn_scene_into_keys( scene_keys, refinement_key, scene_run, profile, iter_num, - axis_feedback, seed_output_connected, + axis_feedback, seed_output_connected, is_h3=is_h3, ) - self._v2_learn_absolute(scene_run, profile) + if not is_h3: + self._v2_learn_absolute(scene_run, profile) profiles.append(profile) lines.append( f"Scene {scene_index + 1} ({label}): trained custom key(s) [{', '.join(trained_keys)}], default skipped" @@ -4886,7 +4896,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): seed_memory_status = self._v2_learn_scene_into_state( global_state, scene_run, profile, iter_num, axis_feedback, seed_output_connected, ) - if refinement_key and self._v2_reward_admissible(profile): + if not is_h3 and refinement_key and self._v2_reward_admissible(profile): with self._v2_stage("value function"): n = self._v2_train_value_function( refinement_state_path(refinement_key, "value_fn", prefix="refine_v2", extension="pt"), @@ -4895,7 +4905,8 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): ) if n is not None: print(f"[FunPackRefiner] Scene {scene_index + 1} value function updated — {n} samples") - self._v2_learn_absolute(scene_run, profile) + if not is_h3: + self._v2_learn_absolute(scene_run, profile) profiles.append(profile) lines.append( f"Scene {scene_index + 1} ({label}): {seed_memory_status.split(': ', 1)[-1]}" @@ -10207,7 +10218,9 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # Runs even with no refinement_key (Absolute is global), so standalone runs still build it. # Skipped for Wrong-* repair ratings (skip_value_function): Absolute reads reward as pure # quality, so a 0.0/low repair reward on a good gen would push it into the global bad_dir. - if has_previous_run: + # NOT on H3 -- nothing reads it (steer_mode/absolute_strength are Studio-side, gated by + # the same value_guidance override above), so training it is pure waste. + if has_previous_run and not _is_h3: with self._v2_stage("absolute taste"): self._v2_learn_absolute(previous_run, learning_profile) if has_previous_run and not learning_profile.get("skip_learning"): diff --git a/samplers.py b/samplers.py index d4e0a7b7..782e828f 100644 --- a/samplers.py +++ b/samplers.py @@ -6909,6 +6909,20 @@ class FunPackLTXAVSceneChainSampler: # happened last run is reported again rather than deduped away forever. _log.begin_run() self._is_h3 = self._set_stream_axes(model) + if self._is_h3: + # H3's rating-driven mechanisms are h3_phrase_emphasis (Studio) and + # h3_repr_steering (this file) ONLY -- see project_reward_model_rework.md and the + # 2026-09-03 dev session. Every other rating-driven latent/conditioning steerer + # was built and validated for LTXAV; none of them has been shown to do anything + # useful on H3 (output_guidance/trajectory_guidance/dynashift/score_slider ride + # the same last-half gate that was calibrated there, and embed_guidance/ + # taste_nearest_prompt read the same relative value function that phrase + # emphasis's own manipulation check showed produces near-invisible pushes at H3's + # real calibration). Forced off HERE, not just hidden in Engine Settings, so a + # project saved before this still behaves correctly instead of silently doing + # six things nobody asked for. + output_guidance = trajectory_guidance = dynashift = False + embed_guidance = score_slider = taste_nearest_prompt = False # Read once per run, consumed by _install_h3_final_layer at each scene's sample call. self._h3_video_detail = max(0.0, min(2.0, float(h3_video_detail))) # The gate every rating-driven wrapper shares. On H3 it is read off the schedule's diff --git a/tests/test_refinement_keys.py b/tests/test_refinement_keys.py index 9bc24517..dc83fb8a 100644 --- a/tests/test_refinement_keys.py +++ b/tests/test_refinement_keys.py @@ -457,6 +457,40 @@ def test_me_scene_ratings_custom_key_skips_default(monkeypatch): assert calls["default_vf"] == 1 # default VF trained ONLY scene 2 +def test_h3_scene_ratings_skip_value_function_and_absolute(monkeypatch): + """H3's only rating-driven mechanisms are h3_phrase_emphasis and h3_repr_steering -- + nothing reads the per-key value function or the Absolute taste store on H3, so training + them per rated scene is pure waste (see the 2026-09-03 dev session).""" + import minimax_h3 + monkeypatch.setattr(minimax_h3, "is_h3_clip", lambda c: True) + studio = conditioning.FunPackVideoRefinerV2() + calls = {"vf": 0, "absolute": 0, "custom_is_h3": []} + monkeypatch.setattr(studio, "_v2_build_scene_learning_run", + lambda run, idx, *a, **k: {"conditioning": {"x": 1}, "scene_index": idx}) + monkeypatch.setattr(studio, "_v2_axis_feedback", + lambda *a, **k: {"missing_axes": [], "satisfied_axes": []}) + monkeypatch.setattr(studio, "_v2_learn_scene_into_state", lambda *a, **k: "seed: ok") + monkeypatch.setattr(studio, "_v2_train_value_function", + lambda *a, **k: (calls.__setitem__("vf", calls["vf"] + 1), 1)[1]) + monkeypatch.setattr(studio, "_v2_learn_absolute", + lambda *a, **k: calls.__setitem__("absolute", calls["absolute"] + 1)) + + def _fake_learn_into_keys(keys, *a, is_h3=False, **k): + calls["custom_is_h3"].append(is_h3) + return list(keys) + monkeypatch.setattr(studio, "_v2_learn_scene_into_keys", _fake_learn_into_keys) + + prev = {"scene_count": 2, "scene_texts": ["s1", "s2"], + "scene_refinement_keys": [["mykey"], []]} # scene1 custom, scene2 default + ratings = [{"index": 0, "rating": "Perfect"}, {"index": 1, "rating": "Perfect"}] + studio._v2_apply_movie_editor_scene_ratings( + {}, prev, ratings, 1, refinement_key="default", clip=object()) + + assert calls["vf"] == 0 # scene 2 (default key) never trains its VF on H3 + assert calls["absolute"] == 0 # neither scene feeds Absolute on H3 + assert calls["custom_is_h3"] == [True] # scene 1's custom-key path was told it's H3 too + + def test_wrong_ratings_skip_value_function(): """Wrong-* are prompt-repair signals, not quality rewards: they must NOT train the value function (a 0.0/low reward on a visually-good gen poisons the learned quality landscape).