diff --git a/h3_repr_steering.py b/h3_repr_steering.py index 836867d6..0b528b30 100644 --- a/h3_repr_steering.py +++ b/h3_repr_steering.py @@ -43,7 +43,12 @@ import torch # of guessed from someone else's percentage. Only DEFAULT_BLOCK ever STEERS; the rest of # CANDIDATE_BLOCKS are captured read-only alongside it so block_sweep() has something to rank. DEFAULT_BLOCK = 25 -CANDIDATE_BLOCKS = sorted({1, 5, 10, 15, 20, 25, 30, 35, 40, 45, DEFAULT_BLOCK}) +# 41-49 individually, not on the coarse grid: the block-influence probe measured the stack as +# heavily BACK-LOADED (blocks 48+49 alone carrying ~a third of all residual movement), so the +# tail is where a steering push has the most to act on and is worth resolving one block at a +# time. Everything below 40 stays coarse -- it is the quiet end. +CANDIDATE_BLOCKS = sorted({1, 5, 10, 15, 20, 25, 30, 35, DEFAULT_BLOCK, + *range(40, 50)}) MIN_PER_GROUP = 2 # need 2+ POSITIVE-weight and 2+ NEGATIVE-weight rows. Was 3 (parity with # absolute/taste steering's floor, not derived from anything specific to this mechanism). diff --git a/movie_editor/frontend/engine_settings.js b/movie_editor/frontend/engine_settings.js index d1be6073..a6d59c37 100644 --- a/movie_editor/frontend/engine_settings.js +++ b/movie_editor/frontend/engine_settings.js @@ -322,8 +322,8 @@ hint: "EXPERIMENTAL, unvalidated, H3 only. Reaches inside the model instead of around it: captures one block's video-row hidden state each generation, and once you have 3+ liked and 3+ disliked runs on this refinement key, adds their mean difference back into that block's output on every later run. No architectural ceiling like the attention-bias mechanisms have — push the strength too far and coherence can break with no warning." }, { name: "h3_repr_steering_strength", label: "Representation steering strength", kind: "float", default: 0.05, min: 0.0, max: 2.0, step: 0.01, dependsOn: "h3_repr_steering", hint: "Fraction of the block's own hidden-state norm added along the learned direction. Start low and push it deliberately high on a same-seed test to confirm it moves the video at all before trusting a small value." }, - { name: "h3_repr_steering_block", label: "Steer at block", kind: "combo", choices: ["1", "5", "10", "15", "20", "25", "30", "35", "40"], default: "25", dependsOn: "h3_repr_steering", - hint: "TEST-ONLY: which of the block-sweep's candidate blocks actually gets steered, instead of always block 25. Blocks 1-15 are deep-end territory (the anchor/reference rows haven't been read by the network yet at that depth) -- expect this to behave differently, not just 'weaker', than 20-40. The other candidate blocks are still captured read-only either way." }, + { name: "h3_repr_steering_block", label: "Steer at block", kind: "combo", choices: ["1", "5", "10", "15", "20", "25", "30", "35", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49"], default: "25", dependsOn: "h3_repr_steering", + hint: "TEST-ONLY: which of the block-sweep's candidate blocks actually gets steered, instead of always block 25. Blocks 40-49 are listed one at a time because that is where the stack does most of its work (48 and 49 alone carry about a third of all movement). Blocks 1-15 are deep-end territory (the anchor/reference rows haven't been read by the network yet at that depth) -- expect this to behave differently, not just 'weaker', than 20-40. The other candidate blocks are still captured read-only either way." }, { name: "identity_transfer_enabled", label: "Best-FaceID compatibility", kind: "bool", default: false, hint: "Feeds the identity pin image the way Best-FaceID identity LoRAs expect it. Needs an Identity pin set.", detail: "Replaces Continuity's identity-pin guide with separate, non-rendered reference tokens plus an optional ArcFace projector below. Load the LoRA itself in Models. No effect without an identity pin." }, diff --git a/samplers.py b/samplers.py index 7a0229e5..535f2d30 100644 --- a/samplers.py +++ b/samplers.py @@ -2966,7 +2966,7 @@ class FunPackLTXAVSceneChainSampler: # workflow (builder.py), silently corrupting anything the user hadn't # explicitly overridden in sampler_inputs. Fixed 2026-09-03; do not move this # back up next to its sibling widgets. - "h3_repr_steering_block": (["1", "5", "10", "15", "20", "25", "30", "35", "40"], { + "h3_repr_steering_block": (["1", "5", "10", "15", "20", "25", "30", "35", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49"], { "default": "25", "tooltip": "TEST-ONLY: which block actually gets steered (the rest of the sweep's candidate blocks are still captured read-only, same as always). direction() is recomputed for whichever block you pick here -- this does not change what block_sweep ranks, only which block's learned direction gets injected into this run.", }), diff --git a/tests/test_h3_repr_steering.py b/tests/test_h3_repr_steering.py index 1db8fae0..d0b4c2ae 100644 --- a/tests/test_h3_repr_steering.py +++ b/tests/test_h3_repr_steering.py @@ -223,3 +223,21 @@ def test_block_sweep_drops_neutral_weight_rows(): rs.commit("k", 0.0) # exactly neutral -- must not count toward either side sweep = rs.block_sweep("k", trials=200) assert sweep[rs.DEFAULT_BLOCK]["n"] == 2 * rs.MIN_PER_GROUP + + +def test_every_steerable_block_is_also_captured(): + """A block can only be STEERED if it is also in CANDIDATE_BLOCKS -- direction() reads + descriptors captured at that exact block, so a picker entry outside the capture set would + report "not enough data yet" forever no matter how many runs were rated.""" + import re + src = open("samplers.py").read() + choices = re.search(r'"h3_repr_steering_block": \(\[([^]]*)\]', src).group(1) + offered = {int(c.strip().strip('"')) for c in choices.split(",")} + assert offered <= set(rs.CANDIDATE_BLOCKS), offered - set(rs.CANDIDATE_BLOCKS) + + +def test_the_back_loaded_tail_is_resolved_one_block_at_a_time(): + """The influence probe measured blocks 48-49 carrying ~a third of all residual movement, + so the tail is where a push has the most to act on -- it is covered individually rather + than on the coarse grid the quiet early half uses.""" + assert set(range(40, 50)) <= set(rs.CANDIDATE_BLOCKS)