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>
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>
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>
[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.
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).
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.
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).
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).
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.
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.
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.
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>
"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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
"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.
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.
"<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.
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.
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.
"placed from the conditioning tail" means the tags were not usable and
the bias fell back to cond_len minus the tokens measured here. That holds
only when Studio encoded the text. On a wired conditioning another node
did, from a string this one never saw — the editor hands a linked node
the pre-expanded prompt while Studio keeps the raw one — so the
arithmetic lands the bias on a window of the wrong words with nothing
saying so.
The modality tags are the only trustworthy placement there. Without them
the emphasis is skipped and says why. Studio's own encodes still use the
tail, where the measurement is its own.
512 -> 197 positions inside finalize. _v2_apply_bounded_attention is
always-on: it re-encodes the prompt through CLIP, concatenates the two
sentence halves and REPLACES the tensor with the result. That result has
no reference image in it, because this node never received one.
So on a reference run the picture and the prompt were never both in the
conditioning the sampler got — which half survived depended on which
step ran last, and the only visible symptom was a tag-count mismatch
three functions away that read like housekeeping.
Entries that arrived on a wire now carry funpack_conditioning_owner, and
no step that rebuilds a conditioning by encoding text will touch one.
Same rule the scene split got. Bounded Attention still runs normally on
Studio's own encodes.
Every stage of _v2_finalize_conditioning is length-checked and names
itself in the log if it changes one, so the next such bug reports itself
instead of surfacing as a trimmed tag count.
An r2v node emits the reference block and the encoded prompt as separate
entries. _v2_extract_conditioning reads entry 0 and refine_v2 rebuilt a
single-entry list around it, so everything after the first entry was
dropped: the tensor kept the vision block and lost the prompt. 197
positions of a 512-position tag map, of which 191 are image marks — the
right character, speaking invented syllables.
The extra entries now pass through unchanged. Steering is fitted to the
entry Studio measured; applying it blind to a companion entry of another
length and purpose would be a guess, and discarding them is the bug.
The tag-surplus message said the reference was missing. It is the
opposite — a leading run of image marks IS the reference, and what gets
trimmed is the prompt.
197 positions against 512 modality tags is not the one spare tag a vision
block leaves, and "trimmed 315 spare tags" reads like housekeeping. The
ownership line now reports the conditioning's size, its tag count and its
entry count as RECEIVED, so it is possible to tell whether the prompt
arrived short or was cut inside this node. An outsized surplus also
reports the surviving text/image split, and any length change between
finalize's input and output is called a bug in the log.
Also resolves $variables in the phrase list before the status line, not
only before matching — the log printed raw phrases, which made it look
like `$style` was reaching the encoder.
The split re-encodes each scene from text and its entries replace
output_conditioning WHOLESALE. It re-establishes H3's visual conditioning
from source_image / h3_references — and nothing in the editor pipeline
sets either, because the reference lives on the R2V node, not on Studio.
So a wired reference conditioning was replaced by text-only encodes: not
appearance drift, a different person.
_v2_transition_scene_conditionings returns None when clip is None, so the
wired conditioning survived with CLIP disconnected. That is why
disconnecting CLIP "fixed" it, and why every fix upstream of the split
changed nothing.
Studio cannot split a conditioning it did not build. When one is wired it
keeps it, for every scene, and says so.
H3 tokenizes a reference INTO the conditioning, and comfy's
token_tags_from_embeds_info marks the whole vision block (plus its
flanking vision_start/vision_end) as 0 with text at 1. Those rows are
Qwen's encoding of the picture.
Every steering path is a lerp or add over the whole tensor — learned
directions, taste pull, concept deltas, the absolute store,
negative_erase — so the encoded picture was being pulled toward a
direction learned from TEXT. That is the character appearance drift, and
it only happens with CLIP connected because that is what builds the
directions.
The tag map existed and nothing read it. The vision rows are now stashed
before any steering and put back after all of it. Text rows still steer.
The spans were measured on funpack_scene_text — the RAW prompt, still
holding `$style` and unrolled shortcuts — while the conditioning was
built from the resolved string. Different string, so every span lands on
the wrong words or the placement guard rejects the run. funpack_encode_text
is the one that was encoded; use it.
Phrase memory also stores raw text, so a phrase could carry `$style`
while the prompt had it resolved and match nothing. Variables are a plain
substitution, so both sides are put in the same form. Shortcuts are not
peeked: a /trigger rolls per seed and peeking would give a different roll
from the encoded one, so an unrolled phrase failing to match is correct.
A wired positive CONDITIONING means Studio does not encode the prompt.
That is not a preference to configure; it is what wiring the input says,
and it is what a wired negative CONDITIONING has always done. Wanting
CLIP to own the prompt means not wiring one.
Removes clip_owns_prompt from the node, both settings panels and the
settings JSON.
A new scale was invented where two calibrated ones existed.
The ceiling is now 1.25 — the exact boost _v2_build_attn2_patch has
applied to an emphasised phrase's K/V on LTX since long before this, on
a path that works. A different mechanism for the same intent should not
push harder.
The ramp toward it is _v2_auto_strength, which already answers "how much
evidence is there": base 0.030, scaled by bad_streak and reward EMA,
clamped to [0.008, 0.085]. So one Awful reaches about x1.13 and a
sustained bad streak x1.19, instead of the top phrase jumping straight to
the maximum off a single rating.
Two defects behind a reported x1.9.
Spans overlap — a phrase and one of its own words match the same tokens
and their biases ADD. Intended, so the word reinforces the phrase, but
unbounded three overlapping x1.5 spans reach an effective x3.4 on one
token. Bounded against the strongest SINGLE span, so an explicit
(word:2.0) still means exactly 2.0.
And it shipped on by default. It changes what the model sees on every
rated run, and it is gated on is_h3_clip — which is why disconnecting
CLIP made generations normal again. Now a toggle, off.
Also replaces prefer_wired_conditioning with clip_owns_prompt: a wired
conditioning owns the prompt by default, and the old order is the opt-in.
CLIP won whenever both were connected, so Studio replaced an R2V/I2V
node's conditioning with its own text-only encode. On H3 that discards
the reference outright: Qwen3-VL is causal and the vision block precedes
the prompt, so every prompt row is a hidden state that already read the
image. Studio cannot reproduce that — nothing wires h3_references or
source_image in the editor pipeline, so its encode never sees a picture.
Detected from minimax_token_tags (0 marks a vision block) rather than
guessed, so a text-only or non-H3 graph keeps the old precedence.
Studio reads studio_settings.refiner.negative_prompt whenever the wired
negative_prompt is blank. The editor writes the wired one and has no
field for the stored one, so clearing the visible box handed control to
a negative saved in an earlier session, invisibly — a 353-character
negative encoding on a run whose Negative prompt field was empty.
The editor's box now writes both, empty included. The log says which
source supplied the text instead of only how long it was.
Three problems, all visible in the first live run.
x1.03 on every phrase: category scores are 0..1 confidences and a phrase
rarely carries much of one axis, so `1 + strength * score` was applied
and inert. Weights are now relative to the strongest candidate, making
strength mean "how much the best phrase gets".
94 candidates capped to 8 by LENGTH kept an arbitrary eight. Ranking is
by weight; longest-first is now only the application order, so a phrase
still lands before one of its own words adds to it.
The prompt was assumed to be the tail of the conditioning. The same run
reported 367 conditioning rows against 368 modality tags, which is
exactly how that assumption breaks. minimax_token_tags marks text as 1
and vision blocks as 0, and references precede the prompt, so the prompt
is the trailing run of 1s — read it instead of inferring it. The status
line says which of the two placed it.
Studio has known which words to weight since 2.0 —
_v2_concept_units_for_run splits every run into phrases, single words and
n-grams; _v2_update_phrase_memory trains each against the rating's
missing/wrong axes. The only consumer was _v2_build_attn2_patch, and H3
skips it: one packed self-attention stream, no cross-attention. So the
learning ran on every generation and reached nothing.
It now becomes an attention-logit bias on the prompt's token positions in
the packed stream, which is the same statement the K/V x1.25 emphasis was
making, at a place H3 has. Boost only by default.
Located with the tokenizer's offset mapping (verified against ComfyUI's
H3 vocabulary), not by re-encoding each phrase through Qwen3-VL-32B the
way _v2_find_phrase_token_ranges does.
The bias is a mask and SLA routes masked calls to dense, so a weighted
run is a dense run. Said once, at install.
MiniMax H3 encodes with Qwen3-VL; the stand-in path fetched a 12B Gemma
tokenizer regardless. Local HF cache is now tried before any download,
and a download announces itself — it blocks the run with no timeout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On H3 it is a Qwen3-VL-32B forward whose output CFG 1.0 never reads.
It still has to happen, but it must be visible when accounting for
what a run loaded and when.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erase the negative's pooled direction from the positive conditioning
(projection by default, never the vision span). Plus an option to let a
wired positive CONDITIONING own the prompt while CLIP keeps its jobs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>