Commit Graph
385 Commits
Author SHA1 Message Date
digital-garbage d6981036d0 feat: per-block influence probe for H3 (measurement only)
Records ||f_i(x_i)|| / ||x_i|| on video rows for every block, averaged
over each run's steps, and pairs the profile with that run's rating.
Answers the question that gates rating-driven block weighting: is H3's
depth profile flat (nothing to aim at) or structured? Reports a
scale-invariant flatness number, and once a key has 2+ liked and 2+
disliked runs, which blocks run hotter on the liked ones.

Steers nothing. Installed BEFORE any steering hook so the delta it
records is the block's own, not one already containing an injection.
Bounded cost: two norms per block per step on ~512 strided rows, no
extra forward pass, results accumulated as device tensors and synced
once per run rather than 50x per step. Says so explicitly when it
records nothing, since an empty capture and a flat profile are
otherwise indistinguishable downstream.

Adjacent to per-block residual steering, removed in b7ed4f9 for
finding no rating signal above the 4th decimal -- that pass had no
per-step resolution, which its own removal note named as the gap.
This either reproduces that null definitively or it doesn't.
2026-09-04 17:01:21 +03:00
digital-garbage b7f15a1a6b feat: re-enable DynaShift on H3, add a positive-pull bank alongside
the negative one

DynaShift was forced off for H3 in the 2026-09-03 disable pass on
theoretical grounds (LTXAV-calibrated, never actually tested on H3) --
re-enabled here to find out. Also gives it a second bank: negative_memory
now stores liked generations the same way it already stores disliked
ones (is_positive_profile, same sign-only convention h3_repr_steering
uses), and _build_dynashift_wrapper computes mean(liked) - mean(disliked)
over each bank's matching-resolution frames as a pull direction --
deliberately NOT nearest-neighbor matching toward one liked example,
which would risk reproducing that specific past generation. Needs
MIN_BANK_FOR_PULL (2) entries in each bank at the run's own latent
resolution before it contributes anything.

Reuses the existing dynashift/dynashift_strength/dynashift_threshold
controls for both directions -- no new Editor UI needed.
2026-09-03 23:22:35 +03:00
digital-garbage e079c01c09 revert: remove prompt_hash cross-prompt grouping from block_sweep
Never worked on this R2V-wired setup (prompt_hash always came back
None, silently disabling the guard on every sweep) and fixing it was
explicitly declined. Removed rather than left inert -- a guard that
never engages is worse than no guard, since it implies a protection
that was never actually there.
2026-09-03 21:21:35 +03:00
digital-garbage 9c0dac7076 revert: remove the REINS capture-pause toggle entirely
Misread "disable that .pt tracking" as "stop recording ratings" --
the actual ask was about console noise, already handled by removing
the block-sweep print. This toggle instead silently stopped every
rating from being recorded while it was on, confirmed live: a real
key sat at "0 liked / 0 disliked" through multiple ratings with no
visible error, and the "committing weight" log line looked correct
the whole time (fixed separately, but the toggle itself should never
have existed).

Removed cleanly rather than patched: capture_enabled/set_capture_enabled,
the enabled gate in save_pending/commit, the /enabled route, the API
client method, and the Settings toggle row. save_pending and commit
now behave exactly as they did before this was ever added -- every
rating is recorded, unconditionally.
2026-09-03 20:17:31 +03:00
digital-garbage a2da292f52 fix: commit() reports whether a row was actually recorded, not just the reward
"Committing weight +X" printed unconditionally, before checking
whether there was a pending capture to attach it to -- so a correctly
computed reward with nothing to pair it with (capture was off, or the
sampler's h3_repr_steering toggle was off, on the run being rated)
looked identical in the log to a real commit. That's the gap that
made "0 liked / 0 disliked" on a cleared key impossible to diagnose
from the console alone: the weight line looked right every time.

commit() now returns "recorded" | "no_pending" | "disabled" | "no_key"
and the caller prints which one actually happened.
2026-09-03 20:11:34 +03:00
digital-garbage 329e2ec057 fix: stop printing the block-sweep ranking to console every rating
Duplicated what Settings > Refinement & Taste already computes and
shows as a table on demand. Console still gets the compact one-line
commit confirmation; the full per-block permutation-test ranking now
lives only in the panel.
2026-09-03 19:30:55 +03:00
digital-garbage 4af9875d15 feat: H3 block-depth sweep + Settings export/clear UI for REINS
Captures every candidate block (5,10,...,45, plus DEFAULT_BLOCK=25)
read-only alongside the one that actually steers, so block_sweep()
can answer "which block really separates liked from disliked" from
data already being collected -- reusing trajectory_probe's own
permutation test (proven correct on this project's real data) instead
of trusting REINS' 50%-depth heuristic (their own paper picks the
best layer per model by sweep, not by applying one universal
percentage) or porting a diagnostic built for a different question
(LTXAV's identity-block search measured cross-scene consistency, not
liked-vs-disliked separation).

Grouped by prompt_hash (trajectory_probe's own, reused) so cross-
prompt content differences can't masquerade as a rating signal -- the
same confound that inverted the first cross-prompt read of this
project's output value function.

Settings > Learning > Refinement & Taste gets a per-key REINS panel
(status, block-sweep table, export/clear) mirroring the trajectory
probe UI, plus matching /api/h3_repr_steering routes -- so data can
be pulled off a rental and handed over directly instead of relayed
through pasted console logs.
2026-09-03 19:26:30 +03:00
digital-garbage 3e5c07249a fix: close a gap in the H3 disable pass — output_guidance snapshot and DynaShift capture
Both were captured "regardless of the toggle" by original design (so
turning guidance on later works immediately), which meant they kept
running on H3 even after output_guidance/dynashift were forced off —
exactly the kind of function the "disable everything unrelated to
REINS" pass was supposed to catch, missed because the capture point
predates any toggle check.

Confirmed live: the user's own log showed "DynaShift negative bank
updated — 1 entry" after the rest of the disable pass had already
landed.
2026-09-03 18:59:47 +03:00
digital-garbage 85add9ed84 fix: H3 scale ratings were silently discarded and replaced with the default
Root cause of "every rating counts as liked": the 'rating' node
input is a COMBO widget the graph builder validates against
V2_RATING_LABELS (builder.py's _live_value) -- the same guard that
falls back to a node's default when a saved LoRA/model choice isn't
installed on this machine. The H3 scale sends bare digit strings,
which were never added to that list, so EVERY H3 rating since the
scale shipped was silently rejected as "not a valid choice" and
replaced with the node's declared default, "Missing action" (+0.05)
-- explaining the diagnostic log exactly: legacy_score=None (never
reached the digit-parsing path at all) and reward +0.050 (the
literal default, not a mapped value).

Confirmed with the log line the user captured: the fix earlier this
session to normalize_refiner_v2_rating's digit math was correct and
irrelevant -- the real rating value never reached that function.

Fix: add "1".."10" to V2_RATING_LABELS so they pass validation
unchanged, same list both rating widget declarations already read.
2026-09-03 18:56:28 +03:00
digital-garbage 90250e9f6a debug: log the weight REINS actually commits, and which rating produced it 2026-09-03 18:52:24 +03:00
digital-garbage 6a942ae3a9 feat: disable every rating-driven mechanism on H3 except phrase emphasis and REINS
H3's only validated (or being-validated) rating consumers are
h3_phrase_emphasis and h3_repr_steering. Everything else --
value_guidance/absolute taste steering, output_guidance,
trajectory_guidance, dynashift, embed_guidance, score_slider,
taste_nearest_prompt, and the path planner -- was built and
calibrated for LTXAV or for a value function nothing on H3 reads.
None of it helps REINS; it only trains value functions that are
never consumed and prints status lines for mechanisms that can
never fire, which is what actually produced the "80% of the log is
useless" bug report this responds to.

Forced off at two chokepoints (top of sample() in samplers.py once
self._is_h3 is known; top of refine_v2 in conditioning.py once the
clip's family is known), not just hidden in Engine Settings, so a
project saved before this still behaves correctly. Value-function
and Absolute-taste TRAINING (not just application) is skipped too --
per-scene, per-key, and the main path -- since nothing on H3 will
ever read what they'd produce.
2026-09-03 18:46:30 +03:00
digital-garbage c8993891c6 fix: H3 scale's reward sign was backwards for 5 and 6 out of 10
normalize_refiner_v2_rating's legacy digit path snapped a bare number
to the nearest existing category label and used THAT label's reward
verbatim. Missing action's +0.05 was calibrated for its own meaning
("the frame was fine, just no motion") -- not "a mediocre score" --
but 5 and 6 both landed in that bucket, so rating something a 5/10
produced a POSITIVE reward. Every h3_repr_steering weight built from
those ratings was backwards.

Reward is now a straight linear map across the full 1-10 range
(1 = -0.9, 10 = 1.0), so the sign matches what the scale promises.
Level/missing_axes/skip flags still come from the nearest label --
harmless, since nothing needs them exact for a legacy score.
2026-09-03 18:42:59 +03:00
digital-garbage 3ba114708b feat: h3_repr_steering learns a weighted direction, not a hard liked/disliked split
Every rated run now pulls proportionally to its own reward instead of
being bucketed into two equal-weight groups -- a 9/10 counts for more
than a 6/10, instead of both counting as one full vote. Centring the
WEIGHTS (not the descriptors) before the weighted sum is what makes
shared content cancel exactly even when the weights are unbalanced;
see the module docstring for the identity. This is what actually
justifies the scale existing at all, rather than just picking one of
two buckets with extra steps.

Also: the H3 scale's heart/loved button did nothing --
normalize_refiner_v2_rating only applies the loved boost inside the
branch that matches an existing category label, and a bare number
never reaches it (confirmed: '7' and '7|loved' produced the identical
reward). Removed it from the scale rather than wire a boost with no
established meaning on a 1-10 input.
2026-09-03 18:30:17 +03:00
digital-garbage 33038ae700 feat: H3 rating picker becomes a 1-10 scale; REINS learns from rating identity, not reward sign
The category grid assumes axis-specific effects (phrase emphasis,
dynashift...) this project has not proven land differently on H3.
Replaces it with a single scale for H3 pipelines only -- the backend
already turns a bare number into the nearest existing rating profile
(normalize_refiner_v2_rating's legacy-score path), so every consumer
(value functions, phrase emphasis, dynashift, path planner) keeps
working unchanged; only what the picker sends changes.

Also fixes h3_repr_steering: it was splitting liked/disliked by the
sign of the blended scalar reward, which counts near-misses like
Missing action (+0.05) as "liked" even though that reward exists for
the value functions' quality-landscape ascent, not as a liked/disliked
verdict. Now filters by rating KEY (LIKED_KEYS/DISLIKED_KEYS) and
excludes everything ambiguous instead of guessing a side for it.
2026-09-03 18:22:11 +03:00
digital-garbage 6e28f56c92 feat: REINS-style representation steering for H3 (experimental)
Reaches inside the forward pass instead of around it: captures the
video rows of one block's hidden state per generation, and once a
refinement key has 3+ liked and 3+ disliked runs, adds their mean
difference back into that block's output on every later run. No
gradient, no MLP -- a mean-difference of vectors is the whole model.

Masked to video rows only via the block's own mod_segments tags,
never audio or text -- the one documented failure of this class of
intervention (project_self_consistency_failed.md) happened on a
different model (LTXAV) via joint audio-video attention.

Off by default, H3-only, unvalidated.
2026-09-03 17:50:37 +03:00
digital-garbage c86e8894f6 feat: variability dial on H3 phrase emphasis
0 = only cemented phrases get extra attention. 1 = that bias
collapses to nothing, letting untrained content compete evenly.
Same attention-bias channel as the existing toggle, scaled down.
2026-09-03 16:58:34 +03:00
digital-garbageandClaude Opus 5 d392578832 feat: steering that reaches the first half of a generation
Every rating-driven mechanism shares one gate that opens only over the last half
of the schedule — after motion and layout are decided — so a rating about
movement has never had a window to act in.

This learns one value function per schedule quarter, each trained on that
quarter's own predicted output, each steering inside its own window, with no
shared late ramp. It reads the probe's log, so it starts trained on whatever has
already been rated.

Basis: 51 rated runs across two independent sessions, different prompts and
balances. The early window measured 88% and 89% of the late window's effect,
direction unanimous across all 8 bucket-by-session results, sessions combining
to p 0.043. Cross-validated on the clean session the heads rank held-out runs at
AUC 0.73-0.82.

Beside output_guidance, not replacing it, and off by default: the honest test is
the same seed with it off and on. A window with under 10 samples is byte
identical to the feature being off, so "nothing changed" stays readable, and the
run says which windows can steer rather than only that it is on.

Not included: axis routing (quality late, action early). It is the next step and
it would muddy this A/B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:29:17 +03:00
digital-garbageandClaude Opus 5 39d0f312de feat: trajectory probe — does a rating show up before the halfway point?
Every rating-driven mechanism shares one gate (_make_steer_ramp) that opens
only over the last half of the schedule, so a "Missing action" rating has no
window in which it could act. Banking x0_hat per schedule position would give
it one — if the early half carries any signal. This measures that.

Off unless FUNPACK_TRAJECTORY_PROBE=1. Records, never steers. Built for H3:
buckets clamp to few-step runs, batched predictions are refused rather than
averaged with their unconditional row, and each pass binds its own schedule.
Read the result with tools/trajectory_separation.py.

fix: reference numbering counts within its own kind

A reference slot resolves per kind, so marking an image then a video labelled
the video R2 while the only slot reaching it was "Reference video 1" — there
was no way to have an R1 video and an R1 image at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:35:41 +03:00
digital-garbageandClaude Opus 5 b16d48b2db maint: finish prompt repair, and label bounded attention untested
Prompt repair lost most of itself in an earlier stage; what was left was a
fallback that appended missing phrases back onto the prompt, reachable only
through a list nothing fills any more. Gone. The axes stay — active_repair_axes
is the "what's missing" signal the advisor reads, and it never edits a prompt
by itself.

prompt_repair and prefer_wired_conditioning are read by nothing at all and only
survive in old saved settings, so the card stops printing them.

Bounded attention is built and has never been run, and its left/right split is
fixed. Both labels now say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:41:10 +03:00
digital-garbageandClaude Opus 5 aebc286042 maint: remove the H3 render gains
Six widgets (three write gains, prompt loudness, taste push, learned/manual
mode) and the whole rating loop behind them. Never proved out on the rental:
picture gain past +-0.1 adds distortion and latent noise, the rest changed
nothing legible, and the audio gain was never worth testing.

The final-layer picture detail SURVIVES and now reads its own widget — it was
the only one of the set that showed a real effect. Relabelled experimental.

Two things fall out with them. The refinement key no longer holds any sampler
value, so key scoping has nothing left to scope and goes too. And the card's
ownership table now reads dependsVals as well as dependsValue: the Studio rows
use the plural form, so absolute_strength was printing under steer_mode
"relative", where it does nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:30:19 +03:00
digital-garbageandClaude Opus 5 edcedd9062 feat(h3): two more punctures — prompt timing, and picture detail that spares the audio
h3_prompt_time: the prompt is told how finished it is independently of the
picture, so the model leans on it from the first step instead of treating it
as unreliable while the picture is noise. One extra row through each block's
AdaLN projection, dropped before the result returns. Manual dial only — its
off value sits at the end of its range, not the middle, so the rating loop
cannot explore around it safely.

h3_video_detail: crisper or softer picture with the audio bit-for-bit
unchanged. The final layer is past the last attention pass, so unlike every
other picture control it has no path to the soundtrack.

apply_adaln_gains -> apply_adaln_edits; both block edits share one wrapper.
video_detail joins the rating-learned gains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 00:05:42 +03:00
digital-garbage 0597603980 feat(log): one line per feature, state then reason
[FunPack] K/V conditioning patch: Inactive | Non-LTX model loaded
    [FunPack] Chunk cache: Active | 22 chunks, 4 steps each, consistency

`feature(tag, name, active, reason)` is the shape, and `skipped` renders
identically — a deliberate skip and an inactive feature are the same fact and
should not have two spellings. `failed` becomes `Name: Failed | <effect>`,
keeping the effect rather than the exception text: "that explains what I am
looking at" is the half worth the line, and an exception message rarely says it.

Everything funpack_log prints is now cut at the first sentence end, so the call
sites keep their reasoning without spending it on every run. SENTENCE ENDS ONLY
— an em-dash introduces the reason as often as the essay, and a renderer that
guesses wrong eats the one part of the line worth reading. A single long
sentence is hard-trimmed instead of left to run.

FUNPACK_LOG=verbose (or set_verbose) prints the full text, exception included.

Converted to the new shape: the chunk cache, region locks, interior pins, the
taste push, the token-refiner edit, AdaLN gains, rating-learned gains, phrase
emphasis, the K/V conditioning patch, the scene split, Bounded Attention,
per-block LoRA weights, LTX enhancements. The rest of the log gets the trim for
free and keeps its own phrasing until converted.

Tests: 1815 python (+9), 68 JS.
2026-08-26 23:10:27 +03:00
digital-garbage 0f05d81db8 fix(studio): the enhancer generated twice on every Editor run
The Movie Editor's builder forces split_by_transitions on for EVERY project, and
the per-scene conditionings replace the base entry — so enhancing the base
prompt as well was a second full generation whose result was then discarded. At
the ~59s per call measured on the rental that is a minute of pure waste per run,
and it looked like thinking mode being stuck on.

It was not thinking: that defaults off and is passed explicitly, so it can never
inherit the repair advisor's own default of True. It was two code paths both
doing the work.

Now the base is enhanced only when the run does NOT split, and the skip says so
on the console rather than being silent. One generation per prompt that is
actually encoded.

Tests: 1696 passed (+6).
2026-08-26 17:48:07 +03:00
digital-garbage 5e9de5223c fix(studio): trim the enhancer by repetition, not by an invented length cap
The 2000-character cap was wrong. H3 has no padding, so every prompt token is a
real sequence row and a long prompt is a legitimate thing to want — an enhanced
one especially. Cutting at a fixed length contradicted the token limit the user
had just set.

Length cannot tell "long and detailed" from "stuck". What distinguishes them is
that a model which has stopped writing starts repeating itself, so that is what
is detected now: three occurrences of the same sentence and the prompt is cut
at the first repeat. A long prompt that never repeats is never touched.

The only remaining length bound is the one the user's own token limit already
implies (~4 chars/token), so there is one control rather than two fighting.
Short recurring lines are exempt — "She smiles." legitimately recurs in a shot
list.

Tests: 1690 passed.
2026-08-25 23:03:22 +03:00
digital-garbage 599e6a7fa8 fix(studio): the enhancer ran to the cap and never said where it ran
Reported from a rental: with the length limit raised to 8192 the enhancer used
all of it. The H3 encoder has no trained decoder head — ComfyUI falls back to
tied word embeddings — so it may never emit a stop token, and max_length stops
being a ceiling and becomes the actual length. Nothing downstream noticed: it
just encoded thousands of tokens of rambling as the prompt.

Now capped at 2000 characters and cut back to the last COMPLETE sentence, never
mid-word, and the status says the model did not stop on its own.

Also reported: it seems to run on CPU. Every generation now reports the device
it executed on, how long it took and the cap that applied — a 32B encoder
sharing a card with the DiT can be placed on the CPU by ComfyUI's loader, which
is minutes per call and invisible from the returned text.

Tests: 1685 passed (+9 enhancer).
2026-08-25 22:58:55 +03:00
digital-garbage 01400b4a2b feat(studio): an LLM enhances the prompt before anything is generated from it
Runs after shortcuts and after $variables resolve, before encoding — the model
is handed exactly the text that would otherwise have been encoded, and the
result is what ComfyUI generates from, so nothing downstream knows it happened.

Not the repair advisor: that one is triggered by a rating and fixes a named
fault. This runs on every generation, needs no key, no rating and no previous
run, and only elaborates. Uses advisor_clip when one is wired (the Studio node
already has the socket), the generation CLIP otherwise.

Multi-scene enhances each scene separately. The editor's scene list is
authoritative on count, so rewriting the scenes as one paragraph and
re-splitting could come back with a different number and desync every clip from
its anchor. One generation per distinct text, cached within the run.

Every failure path returns the ORIGINAL prompt — an enhancer that empties the
prompt on a bad generation would render a blank video and blame the model.
Thinking traces, code fences, preambles and whole-answer quotes are stripped;
quotes inside the prompt survive, because speech is quoted on purpose.

Settings > Engine > Studio: toggle, editable system prompt (new textarea field
kind), length limit, temperature, top-p, thinking. Off by default.

Tests: 1637 passed (+24).
2026-08-25 22:44:26 +03:00
digital-garbage 54fa8e4b43 feat(h3): region-locked anchors, and the taste direction lands after the refiner
Region lock: a keyframe pin can cover part of a frame. Transparent areas of an
i2v anchor are dropped from the packed sequence entirely — not blanked, not
noised, absent — so the model invents them. Row deletion re-derives the layout
from upstream's own output; two patches share one keep-mask so they cannot
disagree. Surfaced through the anchor's alpha, which needs no new wire and works
in the Editor. Excluded pixels are filled with the kept area's mean before the
VAE so no hard edge bleeds in. EXPERIMENTAL: fl2va saw whole condition frames.

Taste bias: the liked direction Studio already learns is now also added AFTER
H3's token refiner, where it lands as sent instead of being mixed across tokens
and normalized away. Relative to the span's own row norm, so one strength means
the same thing on every prompt. How far to push is a fifth learned gain,
centred on 0 and signed; h3_taste_bias is the manual override. Needs 3 liked
runs.

Gain state generalized to per-key centres, bounds and exploration width.

Tests: 1608 passed (+46). Untested on a rental.
2026-08-25 21:48:28 +03:00
digital-garbage 551363d1b8 feat(h3): each rating credits the gain it is actually about
The gains read only the scalar reward, so "Missing concept" — reward +0.10
because quality was fine — nudged every gain slightly TOWARD the run that
missed the prompt, and taught the audio gain something about a complaint
that had nothing to do with audio.

Every rating already carries three per-axis signals in [-1, +1]. Route
them: concept -> prompt + prompt_scale, detail/quality -> video, and audio
keeps the overall reward because no rated axis covers it.

The combiner is min, not mean. These axes are conjunctive — the video gain
has to satisfy detail AND quality — so a satisfied axis must not mask a
failed one. Averaging let it: "Missing details" came out at -0.08 for the
gain that governs detail. It is -1.00 now.

Recovering four parameters from one scalar worked, but only by waiting for
random perturbations to decorrelate. Each rating now teaches the parameter
it names.
2026-08-25 21:27:18 +03:00
digital-garbage ffe2a49a9e feat(h3): render gains are learned from ratings, manual is the opt-in
The four H3 render strengths shipped as hand-set dials. They should not
have: four scalars is a SMALLER search than the sigma profile, which
already converges on ratings alone, so typing them is the user doing work
the loop can do.

Same ES update as the sigma profile, with one difference — that profile is
centred on 0, so its applied value IS its perturbation; these are centred
on 1.0, so the perturbation is (applied - value) and that is what the
reward is credited to. Exploration narrows on a like, widens otherwise,
and is seeded from the run seed so a repeat repeats.

Learned values reach the sampler through the conditioning meta, the bridge
h3 token weighting already uses: Studio owns the key, the sampler owns the
application. h3_gain_mode defaults to "learned" and ignores the widgets;
"manual" uses them as typed and still works with no Refiner in the graph.

Learning runs on every rating regardless of the toggle, the same rule
value_guidance follows.

Also fixes four note_on_change calls added earlier this session that were
missing the required tag argument — they would have raised on the non-H3
and unreadable-tags paths.
2026-08-25 18:48:50 +03:00
digital-garbageandClaude Opus 5 6e99b0204f fix(h3): the emphasis line reports counts, never the phrases themselves
It named the three strongest learned phrases as examples. Phrase memory holds
the user's prompt, so every paste of the log carried prompt text out with it —
stripped by hand each time, which is a thing that will eventually be forgotten.

The strongest weight replaces them: it answers the question the examples were
there for (is emphasis doing too much) without quoting anything. Counts stay.

Adds a test on the SHAPE, not on one message — joining phrase text into a log
line is how this would come back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:42:05 +03:00
digital-garbageandClaude Opus 5 33227b198d diag(vf): say where the value functions trained, and how big they are
"Training is slow" and "training is slow ON THE CPU with four million
parameters" are different problems with different fixes, and nothing in the log
told them apart — leaving it to be inferred from timings, which is how the
first guess at this went to the disk instead.

One line per run per value function, naming the device and the parameter count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:34:26 +03:00
digital-garbageandClaude Opus 5 8cdee7072f feat(editor): Settings > Appearance gets "Show timeline on hover"
Off by default. On, the timeline sits as a strip — its header, so the title,
transport and Composer stay reachable — and opens to full height while the
pointer is on it or something inside it has focus.

It also opens for the whole of a DRAG. During one the pointer is in drag mode
and a strip that stays shut is a drop target you cannot hit, so dragenter holds
it open and drop/dragend closes it. The tracker counts depth rather than
holding a boolean: dragleave for the element being left fires AFTER dragenter
for the one being entered, so a flag closes the strip mid-drag as the pointer
crosses from one clip to the next.

The strip height is MEASURED from the header rather than fixed: .zone-head
wraps when the zone is narrow (34px min-height, 138px on a normal window), so
any constant clips the very controls the strip exists to keep visible. It is
re-measured on resize. flex: 0 0 auto matters — .zone is a shrinkable column
child, and a height with min-height: 0 let the workspace squeeze the strip to
1px. The body collapses rather than being display:none so the lanes are not
torn down and rebuilt on every open.

A browser preference like the colour scheme, not a project one, and registered
in the same Settings section for that reason.

Also in this commit:
- vf: train wherever there is a GPU, not only when the sample sits on one. The
  x0 value function's snapshot loads with map_location="cpu", so keying on the
  sample left it training on the CPU — the run that named it measured
  "x0 vf: train 10.1s" against 46ms for the same net locally.
- The stage line counted nested sub-stages twice and reported 153% of a run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:32:11 +03:00
digital-garbageandClaude Opus 5 78fdad9b29 perf(studio): split the value-function stages into load, train and save
Both value functions cost about eight seconds a run, and one has ten times
fewer parameters than the other — so it is not the training math, and the whole
per-call cost is somewhere else. The key file writes at roughly 3 MB/s on the
rental, which makes the checkpoint write the first suspect, but that is a guess
until the three phases are timed apart.

Also times loading the x0 snapshot, which is a full packed latent read from the
same disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:13:54 +03:00
digital-garbageandClaude Opus 5 8e3c707c81 fix(sampler): a wired reference conditioning is not a second scene
The Chain Sampler reads one conditioning entry per SCENE
(scene_conditionings = positive[:max_scenes]). When Studio stopped discarding
the entries past the first of a wired CONDITIONING — the fix for a reference
surviving while the prompt did not — those entries reached the sampler
untagged, and the r2v reference block was counted as a scene of its own. The
run sampled the shot twice, which from outside is "the model ran a second pass
for no reason".

Studio tags every pass-through entry as a companion; the sampler excludes
companions when counting scenes and hands them to every scene instead, so they
still reach the model. Both halves of the original bug stay fixed: nothing is
dropped, and nothing is mistaken for a scene.

Also fixes the liked-conditioning blend, which was the last payload path still
doing a raw lerp: _v2_shape_compatible ignores sequence length by design, so a
direction learned at 586 positions met a 494-position prompt, threw, and took
the whole pull toward what the user rated well with it. Routed through
_v2_payload_like like every other payload.

Adds stage timers for the nine learning steps the first pass did not cover —
55% of a 26.9s run was accounted for, and the rest was unnamed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:06:52 +03:00
digital-garbageandClaude Opus 5 ce046fdfd8 perf(studio): time the learning stages, so a slow run names itself
The encoder tally answered "is it the encoder" — at 24.9s it is 2% — and the
next question is which learning stage the rest is. 2.4s at three rated samples
and 24.9s at five is not smooth growth, and the shape of that curve says
nothing about which stage draws it.

Times the six that can dominate a run: phrase memory, conditioning memory, the
three value functions, and applying the learned direction. One line, stages
over 0.05s only, costliest first.

Measurement only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:56:05 +03:00
digital-garbageandClaude Opus 5 3bb7b78557 perf(studio): keep the key's tensors in a binary sidecar, not as base64 in its JSON
One conditioning delta is 586x3584 floats — several megabytes of base64 inside
a document that is parsed and rewritten IN FULL on every generation. That is
the cost that grows with everything ever learned, and it is paid whether or not
any of those tensors are used.

Tensors now go to <key>.json.tensors.pt and the JSON keeps a named reference
whose id is the tensor's path in the tree ("state.global.liked_dir.direction"),
so the key stays readable and a reader can see what a reference points at.

No migration: a key written before this carries inline blobs, is read exactly
as it stands, and moves to the sidecar on its next save. If the sidecar cannot
be written the tensors stay inline — a slow key is fine, a JSON pointing at a
sidecar that does not exist is not. A reference the sidecar has lost reads as
no tensor, so a direction goes unapplied rather than breaking the run.

Deletion already sweeps it: delete_refinement_key globs every <key>.* sidecar
for exactly this reason. Session Reset removes it explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:54:01 +03:00
digital-garbageandClaude Opus 5 47b78e3c52 perf(studio): report what the refinement key costs, and what is making it big
The key is read and rewritten IN FULL on every run and holds base64
conditioning tensors, so its cost grows with everything ever learned — which
is what "2.0s, then 2.4s" looks like from outside. Timing the read and the
write, with the file size and the entry counts of the collections that can
grow without bound, says whether that is where the time goes and which
collection to bound first.

Measurement only. Nothing about what is stored or how changes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:48:35 +03:00
digital-garbageandClaude Opus 5 b0c22c85dd perf(studio): classify every uncertain phrase in one encode instead of one each
A phrase's vector came from encoding that phrase alone — a full pass of the
text encoder per phrase, and on H3 that encoder is Qwen3-VL-32B. The phrases
are encoded together now, as one joined text, and each vector is the mean of
the rows its own tokens occupy; the tokenizer's offset mapping identifies those
rows exactly, so this is a lookup rather than an approximation.

Each phrase is located at the position it was joined at rather than by
searching the text, because one phrase can be a substring of another and a
search would hand both the same rows.

Falls back to one encode per phrase, unchanged, when the tokenizer offers no
offsets or the spans do not line up with the conditioning that came back. The
cosine step is shared by both paths so a phrase scores the same either way, and
the batched pass is labelled separately in the per-run encoder tally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:36:23 +03:00
digital-garbageandClaude Opus 5 8ae739ad9c perf(studio): embed the category descriptions once per encoder, not once per run
CATEGORY_DESCRIPTIONS is a class attribute of eight fixed strings, so the
vectors depend only on the encoder. They were re-embedded every run — eight
full passes of Qwen3-VL-32B on H3 — to arrive at the same eight vectors.

Held for the process, keyed by the encoder, behind a WEAK reference: pinning a
text encoder in VRAM to save eight passes would be the wrong trade. An identity
check sits on top of the id() key because ids are reused once a model is freed,
and a stale hit would classify phrases against another encoder's geometry.

Not a cache of generated content — nothing user-typed goes into it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:34:08 +03:00
digital-garbageandClaude Opus 5 7cd063302a perf(studio): count text-encoder passes by purpose, per run
The encoder is the most expensive thing Studio touches — on H3 it is
Qwen3-VL-32B — and most calls to it are bookkeeping rather than the prompt:
embedding eight category descriptions to classify a phrase by cosine
similarity, embedding a memory to ask whether it is about this scene.

Every call now records its purpose, its cost, and whether the per-run cache
served it. One line per run says where the time went and what share of Studio
it was, so "26 seconds before generation starts" becomes a list of candidates
rather than a guess. The tally is per run and never persisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:32:44 +03:00
digital-garbageandClaude Opus 5 c454e3b2d9 perf(studio): don't locate phrase ranges for a patch that already declined
Locating a phrase's token range runs the text encoder once per phrase — on H3
that is the same multi-billion-parameter encoder the prompt goes through, up
to eight times. The ranges exist only to feed the attn2 direction patch, and
H3 has no cross-attention for it to hook, so the patch declined on the very
next line and the results were dropped.

The same question is now asked before the work instead of after it. LTX is
unaffected: it reads attn2, so the ranges are still computed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:26:43 +03:00
digital-garbageandClaude Opus 5 8739fa8d2e maint: remove the v1 LoRA relevance scorer and suggestion writer
291 lines: _lora_words, _ensure_lora_memory, _score_lora_prompt_relation and
_update_lora_weight_suggestions. The scorer matched prompt words against each
LoRA's filename; the writer was the v1 half of the suggestion pair, superseded
by _v2_update_lora_suggestions, and unreachable since the v1 refiner entry
point was removed on this branch.

_lora_state_id stays — the live v2 writer still uses it.

Checked: no refs anywhere (grep -a, all file types, node_modules excluded),
not a route handler, not in NODE_CLASS_MAPPINGS, not an INPUT_TYPES widget,
no getattr dispatch. Tests: 1379 passed, baseline plus this branch's two new
ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:05:37 +03:00
digital-garbageandClaude Opus 5 e9ba41f62a feat(lora): a LoRA's filename no longer decides how relevant it is
The suggester split each LoRA's FILE NAME into words and matched them against
the prompt; the overlap scaled how far that LoRA's weight moved. That guessed
what a LoRA does from what someone happened to call it, which is a different
question from how much weight it has earned.

Relevance now comes from the type set on the LoRA and the categories present
in the prompt. Weight tuning itself is unchanged — ratings, axis feedback and
the learned offset all behave as before. Behaviour change: a LoRA whose name
echoes the prompt will move less per rating than it used to, and one with an
opaque name will move more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:04:22 +03:00
digital-garbageandClaude Opus 5 3676a9fdb0 maint: remove the v2.4-era Lucky and Into the Void machinery
32 methods, 1317 lines: the co-occurrence memory, token/pair banks, phrase
placements, embedding canvas and prompt composer. Every entry point was
called only from the v1 refine() removed in the previous commit, or from
another method in the cluster — verified individually after that removal, all
clean. Lucky and Void are one commit because they share the token banks and
call into each other; splitting them would leave a commit referencing a
method that no longer exists.

Into the Void's toggle only ever existed on the unregistered v1 node, so no
UI could reach it. The Lucky TOGGLE stays: the Studio's Lucky is a separate
56-line composer over phrase_memory (_v2_compose_lucky_prompt), unrelated to
this code and still live.

The v2 state schema declares none of these keys, so refinement-key training
never touched them. Tests: 1377 passed, same as baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:53:29 +03:00
digital-garbageandClaude Opus 5 161891bc7d maint: remove the unreachable v1 refiner entry point
FunPackVideoRefiner is not in NODE_CLASS_MAPPINGS; only FunPackVideoRefinerV2
is, and it overrides FUNCTION, INPUT_TYPES and IS_CHANGED with no call to its
parent. Nothing in the repo calls refine(), so the v1 node contract and its
1263-line body could not execute.

The class itself stays — V2 inherits ~200 live helpers from it. What goes is
the node contract that ComfyUI never reads and the entry point nothing reaches.

Checked: no refs (grep -a, incl. the NUL-byte frontend files), not registered,
not a route handler, no getattr dispatch (the file's only two getattr sites
read literal flags), no JS or workflow-template reference, and the v2 state
schema declares none of its keys. Tests: 1377 passed, same as baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:52:22 +03:00
digital-garbage 1c500f0641 fix(studio): a learned direction survives a prompt whose length changed
"conditioning memory failed (size of tensor a (492) must match tensor b
(499))" — the compatibility gate checks the channel width and the batch
and deliberately not the sequence length, so the payload passed and the
lerp threw. On LTX the encoder pads to a fixed length and this never
happened; Qwen does not pad, so on H3 the length moves with every prompt
edit and the entire learned direction was dropped for that run.

_resize_conditioning_sequence_like was already in the file for exactly
this, with zero callers. Both the blend and the repel now use it, and say
once when a payload had to be resampled.

An exact-length payload is untouched, so the common case — same prompt,
re-rolled — stays bit-exact.
2026-08-23 10:24:19 +03:00
digital-garbage dba33fb4f6 fix(h3): weight every learned phrase that is in the prompt, uncapped
Two bugs, one line apart.

Phrase memory accumulates everything the session has rated, including
phrases from prompts since edited away. The cap to 8 was applied BEFORE
checking whether a phrase is in the current prompt, so those could take
all eight slots on rank alone, match nothing, and leave phrases actually
in the prompt unweighted — while appearing in the status line as
"weighted", which is where they were seen.

And there was no reason for a cap. Locating a phrase here is a substring
find and a scan of the tokenizer's offset map. The 8 was carried over
from _v2_find_phrase_token_ranges, which locates a phrase by RE-ENCODING
it through the text encoder — eight of those is eight Qwen3-VL-32B
forward passes, and the cap is the whole point there.

The status line now names what was APPLIED, and counts how many learned
phrases are in this prompt rather than how many exist.
2026-08-23 10:15:37 +03:00
digital-garbage 3148a5f62a fix(h3): the tokenizer's labels are not the user's prompt
"<Picture n>: " and "<Audio n>: " are emitted by the H3 tokenizer, not
written by anyone. An AUDIO reference is the awkward one: it contributes
its label and NO vision block, so it lands inside the same run of text
tags as the prompt.

Placement is now anchored at the END of that run rather than requiring
the run to nearly equal the prompt, so any number of label tokens in
front no longer shifts the bias — or refuses it outright, which is what
the slack cap did.

Protection follows the same boundary: the whole tensor is stashed and
everything before the verified prompt start is restored, so a label is no
more steerable than the picture is. Without a tokenizer it falls back to
the end of the vision block — a label may stay steerable, the reference
never does.
2026-08-23 09:55:21 +03:00
digital-garbage 7b5746fceb fix(h3): protect to the reference/prompt boundary, not the image tags
An r2v conditioning is laid out reference-first: the "<Picture n>: "
label, the vision block, then the encoded prompt. So the boundary is the
only thing Studio needs — rows before it are the picture and are not
Studio's to touch, rows from it on are the prompt and are.

Protecting by the image tags alone left the label steerable, since it is
text sitting between the two.

prompt_region() is now the single place that walk lives; prompt_base and
choose_encoded_text both read it instead of repeating it.
2026-08-23 09:49:44 +03:00
digital-garbage 409f9cf58d fix(h3): place the emphasis from the text that was actually encoded
Skipping was not the answer — Studio has the text. What it did not have
was a way to know WHICH text: it keeps the raw prompt on its own port and
expands per scene, while the editor hands a linked node the already
expanded string. So its copy and the encoded one can differ.

server._expanded_link_texts is exactly what a node outside Studio
received, and the builder now forwards it. And the choice is CHECKED, not
guessed: minimax_token_tags says how many text positions the tensor
holds, so each candidate is tokenized and the one that fits wins, which
also yields the exact base offset. No arithmetic fallback, no assumption
about whose string it was.

Emphasis is only skipped when no candidate tokenizes to the tensor's text
run at all, which means the encoded string genuinely is not here.
2026-08-23 09:47:21 +03:00