On compute capability > (9, 0) GPUs (Blackwell sm_120, e.g. RTX PRO 6000),
xformers' memory_efficient_attention has no kernel that accepts a tensor
attn_bias — fa3F/cutlassF require cap <= (9,0), fa2F/fa3F reject a tensor
bias. So unmasked attention works (i2v ANCHOR scenes generate) but MASKED
attention crashes: scenes generated from GUIDES hit the LTX guide path
(comfy/ldm/lightricks/model.py _attention_with_guide_mask) which passes a
tensor mask → "No operator found for memory_efficient_attention_forward".
This is the ComfyUI-wide issue normally worked around with
--disable-xformers --use-pytorch-cross-attention; when that flag isn't in
effect on an instance, the guide path still reaches xformers.
Fix: _funpack_install_mask_safe_attention(model), called at the top of the
chain sampler's sample(). When the active backend is xformers AND the device
capability is > (9,0), install ComfyUI's per-call optimized_attention_override
(the wrap_attn hook) so MASKED attention falls back to attention_pytorch
(SDPA handles masks on every GPU) while UNMASKED attention stays on the fast
xformers path. Threaded via transformer_options so it reaches every scene's
forward. No global monkeypatch; no recursion (wrap_attn stamps
_inside_attn_wrapper before calling the override). No-op on supported GPUs,
non-xformers backends, or when another override is already installed;
idempotent.
Tests: tests/test_mask_safe_attention.py — 9 cases covering override routing
(masked→pytorch via kwarg or positional, unmasked→original) and install
gating (xformers+Blackwell installs; supported cap / non-xformers / no-CUDA
skip; existing override respected; idempotent). Run with the ComfyUI venv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extracts the pure global-prompt scene-matching kernel into scene_align.js
(dual browser/CommonJS export) and adds a node test suite so the "stale
scene anchor after reorder/insert/delete" class can't silently regress
again (it has recurred twice).
- scene_align.js: matchParsedToRoots(parsedTexts, rootTexts) — the two-pass
content-first matcher (exact text + nearest-position tiebreak, then
leftover roots in order), index-based and dependency-free. Exposed as
window.SceneAlign in the browser (same pattern as window.PipelineCaps)
and module.exports under node.
- store.js _alignScenesFromParsed now delegates the matching to the kernel
and maps indices back to roots — single source of truth, no duplicated
logic. Behaviour is identical to the previous inline two passes.
- index.html loads scene_align.js before store.js.
- scene_align.test.js: 11 cases via node's built-in runner — reorder,
edit-in-place, insert-at-front, delete-middle, reorder+edit-one,
edit+insert+keep, irreducible, duplicate-text tiebreak, empty-text,
no-roots, used-once. Run with `npm test` (node --test) in this dir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the "scene generates from a stale anchor/guide, survives
restart + key-clear" report. _alignScenesFromParsed matched parsed global-
prompt slots onto old scene roots BY POSITION first (oldRoots[i]), then
reused that root keeping its id + source/anchor + mapped render and
overwriting only .text. A reorder/insert/delete in the global prompt shifts
indices, so slot i's NEW text got glued onto a DIFFERENT scene's old
anchor+render. The wrong mapping is then SAVED into the project file, so it
survived ComfyUI restart and refinement-key/embed-guidance clears — it was
never a cache, it was persisted-wrong state.
Rewrite the matching priority to be CONTENT-first:
- Pass 1: exact normalized-text match (nearest original position breaks
ties) pins unchanged scenes to their own root through any reorder/insert/
delete.
- Pass 2: leftover slots inherit leftover roots in order (in-place text
edits keep their render/anchor; a slot with no root left becomes a fresh
scene with no inherited anchor).
The render lives in state.sceneRenders[id], so it follows the id
automatically — fixing which root a slot matches is the entire fix. The
only irreducible case (a scene both reordered AND fully rewritten with no
unchanged neighbour to anchor it) still falls back to a positional guess.
Validated with a standalone node test across reorder / edit-in-place /
insert-at-front / delete-middle / reorder+edit-one / edit+insert+keep /
irreducible — all pass; the old positional-first algorithm fails the first
five. Preserves ghost materialization, unused-root ghosting, timeline_order
tracking, and keep-render-on-minor-edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
splitScene deep-clones the scene into the second half, then clears the
first half's transition_to_next and transition_frames so the internal
split seam reads as a hard cut (store.js). But it never cleared
video_transition (the NLE video blend). Two consequences:
- videoTransitionState (timeline.js:484) treats ANY non-empty
video_transition as active and falls back to 16 frames when
transition_frames is null, so a phantom transition renders on the
internal A1|A2 seam that should be a clean cut.
- the transition is duplicated: the deep clone kept it on the second
half (correct) AND it survived on the first half (wrong).
- on export _renderClips emits transition="<type>" with tdur=0 for the
first half (store.js:3434-3435), a malformed transition to the stitcher.
Clear video_transition on the first half alongside the other two
transition fields. The clone happens before the clear, so the second
half still correctly inherits the original transition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
splitScene correctly splices a fresh scene right after the original
(store.js:1826) and deletes the original's durationSec so both halves
fall back to plan-based (frames/fps) length (store.js:1831). But
_probeRenderDuration's async <video> metadata probe only guards against
the scene's media filename changing — a split keeps the same filename,
so a probe already in flight from page load can resolve AFTER the
split and silently rewrite durationSec back to the full original file
length. Since buildPreviewSegments lays out segments using durationSec,
the first half snapping back to full length pushes the second half
later — looks exactly like "a full-length scene got appended to the
end" even though the scene array itself was spliced correctly.
Fixed with a per-render edit token: _probeRenderDuration snapshots
r._editTok before starting the probe and bails if it no longer matches
at resolve time; splitScene bumps the token when it deletes durationSec.
Dragging an audio clip's right trim handle had no upper clamp in the
live preview (only resizeAudioTrack's commit path clamped to
audioTrackMaxDurSec), so the handle would visually drag past the real
end of the source and then silently snap back on mouseup with no
feedback — reads as "can't get audio to fit". Preview now clamps to
the same max and labels it when clamped.
Separately, the inline "Start (s)" number input on audio clips can
keep keyboard focus after a stray click (the controls bar can cover
most of a short clip's clickable area). Since i/o/s/Delete are inert
inside a number field anyway, the global keydown handler now blurs
that input and lets the clip-level shortcut through instead of
treating it as "user is typing".
autoMontage required source.type === "video" (explicit "Convert to
video" scenes only) — but most generations are plain generative scenes
with a render in sceneRenders, never converted. That's why "2+ scenes
selected" still failed: none of them satisfied isVideoClip.
Added _renderRef()/hasPlayableRender(), the same "has a render" check
used elsewhere in this file (removeFromPlan), covering both an
explicit video clip (source_in/source_dur) and a generative scene's
own render (sceneRenders[id].media + inSec, cloned per segment).
Dialog now also pre-fills lead/pool from the current timeline
selection instead of ignoring it.
Custom keys (e.g. a 'cs'-tagged cumshot shortcut) only ever trained
through the Movie Editor's per-scene multi-rating path, which requires
BOTH a multi-scene run AND explicit per-scene ratings from the editor.
Most real generations are single-scene with one overall rating, so
those keys were never created at all -- confirmed on a live instance:
zero custom key files existed despite weeks of use.
Single-scene runs already carry their fired keys in
previous_run["scene_refinement_keys"] (Studio always computes this,
the Movie Editor always forces split_by_transitions=True). Train any
non-default keys found there too, additively -- the default key keeps
learning exactly as before; this doesn't replicate the multi-scene
path's exclusivity (custom key trains instead of default), since that
would mean restructuring the heavily-relied-on single-scene rating
path without separately testing the exclusive behavior there.
It was silently installing or no-op'ing with zero console output,
unlike ALG and Momentum Guidance which both confirm activation. Added
the same style confirmation line on successful hook install, plus a
distinct skip reason for each no-op path (no sentence split detected,
bad latent shape, narrow frame, missing transformer blocks) so it's
possible to tell whether it's running at all without reading code.
Structured Diffusion Guidance (arXiv:2212.05032)-style upgrade: instead
of tokenizing each sentence-half just to COUNT tokens (an approximate
boundary within one shared tokenization, vulnerable to BPE merge drift
at the seam), encode each half SEPARATELY through the real text encoder
and concatenate the two conditioning tensors. The split point is now
exact by construction, and the two spans can never share a token even
before any masking is applied. Same toggle, same metadata field
(funpack_bound_split_tokens) the sampler already reads — no sampler
changes needed.
MultiDiffusion (the other planned "same family" technique) needs
multiple forward passes per region (~2x+ cost) and is skipped per the
project's low/no-overhead bar, pending explicit user sign-off.
_V2_PERSISTENT_ENCODE_CACHE survived for the entire ComfyUI process
lifetime, across every generation request, keyed partly by id(clip) —
a CPython object identity that can in principle be reused for an
unrelated object after garbage collection. Conditioning must always
trace back to what's live in the current request; nothing should be
served from a cache that outlives it. The per-call encode_cache dict
(created fresh per sample() invocation, never escapes it) already
covers legitimate within-request de-duplication and is unaffected.
Live testing found decay=0.9 produces garbage at any strength on our
8-step schedule: the EMA barely forgets, so it stays anchored to the
very first (near-pure-noise, wrong-scale) step's direction for nearly
the whole run. decay=0.5 forgets that stale step within 2-3 iterations
and tested clean even at strength=1.0. Paper's longer-schedule decay
assumptions don't transfer to a few-step distilled run.
Masks text cross-attention (attn2 only, never audio or self-attention)
so the left half of the frame can only attend to subject-1's prompt
tokens and the right half only to subject-2's, fighting attribute/
anatomy bleed between two figures in one frame. Pure attention masking
via PyTorch forward-pre-hooks, no extra forward pass, model-level so it
works on any sampler.
Studio always tags each scene with an approximate token-split boundary
(sentence-count heuristic + real tokenization via clip.tokenize) as
conditioning metadata — cheap, no-op unless the new
bounded_attention_enabled toggle on the Scene Chain Sampler is on.
Opt-in, off by default.
Keeps a running EMA of the per-step ODE direction and blends the current
step toward it once sigma drops below mg_sigma_threshold — the
complementary window to ALG's anchor blur, which only acts above its own
threshold. EMA accumulates every step (free) so there's no cold-start
when application kicks in; video-only, audio untouched. Opt-in, off by
default.
Splices an already-rendered lead clip into shrinking-length segments
(faster cuts toward the end) interleaved with randomly-drawn cutaway
clips from a pool. Cutaway selection is random per-clip but always
sequential within a clip, so later content in a clip can never surface
before its own earlier content. Pure timeline construction, reuses the
splitScene source_in/source_dur mechanism — no new generation involved.
Paper default strength=2.5 visibly changed character appearance, even
without JoyAI-Echo compensating. 2.0 holds i2v consistency noticeably
better while still breaking the static shortcut, confirmed independently
twice. 0.975 threshold (narrower blur window) paired with it in the
same tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "bypass" toggle per node in the Models modal -- skips the node's
effect (pass its inputs straight through) without losing its saved
config, so testing without a LoRA/node no longer means deleting it or
zeroing a strength widget every time.
Backend (builder.py): _apply_bypass runs after auto-wire, drops a
bypassed slot's node and rewires its consumers to whatever already
feeds its matching-type input (model in -> MODEL out, clip in -> CLIP
out), same idea as ComfyUI's native node bypass. Only acts when the
output->input type mapping is unambiguous; otherwise leaves the node
active and reports why (e.g. a pure source node with no input to pass
through, like a checkpoint loader).
Frontend: bypass is a slot-level flag (slot.bypassed), kept out of
slot.inputs so it's never sent to ComfyUI as a fake widget value.
Exposable to the main editor via the same eye-button pattern as any
other exposed widget value (store.setModelBypass, inspector.js routes
the boolean control to slot.bypassed instead of slot.inputs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
min=1.01 offset the whole step grid (1.01, 1.11, 1.21, ... 2.51, ...)
so every value the spinner snapped to ended in .01. Min is now a clean
1.0 -- kappa=1.0 is already just a harmless no-op blur, never needed
the 1.01 floor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generalizes the i2v-anchor blur (_alg_blur_video_frame0 -> _alg_blur_frames)
to optionally also blur newly-appended guide-attention frames each scene
(mid_scene_guide / carry_i2v_guides-as-guide / configured guides / JoyAI
memory), reusing the existing guide_tail bookkeeping rather than a mask-value
heuristic (frame_overlap continuation also uses mask=0 and must stay
untouched). New Scene Chain Sampler toggle "Blur i2v guides and JoyAI memory"
mutates the shared KSAMPLER's extra_options per-chunk since guide_tail varies
per scene; always resets so nothing leaks across chunks/samplers. Surfaced in
Engine Settings via the existing Experimental section, no backend/builder
changes needed beyond the new INPUT_TYPES field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adaptive Low-Pass Guidance (arXiv:2506.08456): blurs the i2v anchor frame
while sigma is in the near-pure-noise region, then swaps back to the sharp
anchor, to stop the model shortcutting to a near-static video that just
matches the reference image. Opt-in, off by default, no-op without an i2v
anchor. Swaps model.latent_image per step (the same attribute ComfyUI's
inpaint compositing already re-pastes every step) instead of monkeypatching
comfy internals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project-scoped $name variables resolved DEAD LAST in refine_v2 — after
shortcut-expand and the transition split — so a value containing a comma or
trigger word can never move a scene cut (order: shortcuts -> splits -> vars).
Recursive (a value may reference other vars), cycle-safe (self-reference left
literal, no infinite loop), undefined -> literal $name.
- templates.resolve_variables: recursive, depth-guarded, cycle-safe resolver
- refine_v2: resolve base prompt (single-scene) + per-scene texts (post-split);
variables threaded via studio_settings.refiner (same channel as scenes)
- builder/server/timeline: carry project.variables into the build; persist
variables + prompt_templates on the project
- Composer: templates dropdown + Save above the prompt; collapsible Variables
editor (Add/Remove rows) below it, with live undeclared/cycle hint
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deleting a refinement key was non-atomic. A key's state spans <key>.json plus
sidecars (<key>.value_fn.pt, <key>.sampler_ctx.json, attn_maps/blessed maps +
K/V banks + creativity latent + velocity store), and there was no delete route —
users removed the visible <key>.json by hand, orphaning the rest. The orphans are
still applied: _v2_ascend_conditioning loads <key>.value_fn.pt purely by name
(not gated on the json), and blessed K/V inject at a 0.04 floor even at reward 0.
So a "cleared" key kept steering future generations, surviving both a ComfyUI
restart and the manual delete — the persistent half of the long-run quality drift
(the per-session half was the hook leak in the previous commit).
- templates.py: delete_refinement_key() sweeps the canonical json + every <key>.*
sidecar + clear_refinement_data (attn_maps banks, velocity disk+memory).
absolute_store_info()/clear_absolute_store() for the keyless Absolute taste store
(__funpack_absolute_global__): it learns from every rated gen across all prompts,
is invisible in the key list, and was otherwise only wiped by Session Reset.
Routes: POST refinement_keys/delete, GET/POST refinement_keys/absolute|clear_absolute.
- Refinement Key Loader node: "Delete Key" + "Clear Global Taste" buttons.
- Movie Editor: api deleteRefinementKey/absoluteStoreInfo/clearAbsoluteStore +
FunPack menu "Delete refinement key…" / "Clear global taste store…".
- tests: atomic-delete sweep + empty-key guard (real store, unique key, cleanup).
Pre-existing 16 failures unchanged (stale mocks); +2 new passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_enhancements registered forward/forward-pre hooks directly on the
shared diffusion_model blocks but only removed them on scene-transition
sigma jumps, never at end-of-sampling. Single-scene runs (and the final
scene of multi-scene runs) thus left hooks installed; because ModelPatcher
clones share the underlying blocks, every subsequent generation stacked
another set, each lerp-ing block output toward a previous run's stale
reference tensor -> progressive corruption that survived Session Reset and
deleting refinements/ (the leak is in-process, not on disk) and only
cleared on a full restart.
Tag every installed hook (_FUNPACK_HOOK_TAG) and add strip_funpack_block_hooks
to pop only tagged entries from each block's hook stores (idempotent, leaves
foreign hooks intact). Strip at the top of build_enhancements and defensively
at FunPackLTXAVSceneChainSampler.sample() so accumulation is bounded to zero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the JoyAI cross-shot memory bank from video-only to paired audio-video,
the part that distinguishes JoyAI-Echo from a generic video-consistency trick.
- _JoyAIMemoryBank now stores (video, audio) pairs under the same pin/roll policy;
frames()/audio() stay 1:1. Audio half is None until joyai_audio_memory is on, so
the phase-1 video path is byte-identical.
- _append_joyai_audio_memory pins each banked prior-shot audio latent into the
chunk's audio stream as a fully-protected (mask=0) tail, cropped back off after
sampling (_crop_audio_tail). The audio analogue of the video guide bank; rides
native audio self-attention since LTX has no audio guide-attention API.
- _harvest_joyai_audio harvests the paired audio frame, position-matched to video.
- v2a_grad_scale scales the model's trained video->audio cross-attention via
reversible forward hooks on each AV block's video_to_audio_attn submodule
(JoyAI uses 2.0). 1.0 installs no hook -> native behavior, zero overhead.
- joyai_audio_memory + v2a_grad_scale wired through sample(), batch training,
refinement context, run_mechanisms, INPUT_TYPES (append-only), and editor knobs.
Deliberately breaks the audio pass-through invariant, so both are opt-in and
off/native by default. 3 new bank tests; zero regressions (same 12 pre-existing
stale-mock failures before/after).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inputs were inserted mid-list (after mid_scene_guide_strength). The builder seeds
sampler widget values from the reference workflow's POSITIONAL widgets_values array
(extract_widgets maps it onto INPUT_TYPES order), so inserting widgets shifted every
following widget — landing a numeric on the joyai_frame_select combo ("0 not in
['center','first','random']") and corrupting the seeded defaults for embed_guidance /
score_slider / decode_*. Same class of bug the existing forceInput-ordering comment warns about.
Append-only fix: joyai widgets now sit at the END of the widget sequence (after
embed_guidance_source, before the forceInput sockets). They fall beyond the reference
array so extract_widgets leaves the object_info default ('center') in place, and all
pre-existing widgets realign. No logic change; behavior identical once on.
Requires a ComfyUI restart to reload object_info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The phase-1 sampler inputs existed only on the raw node; the editor's sampler panel is a
hardcoded knob list, not object_info-driven, so they weren't reachable in the normal flow.
Add joyai_memory + its size/anchors/frame-select/strength knobs to SAMPLER_KNOBS and the
Experimental group (beside mid_scene_guide). They flow to the sampler node automatically via
builder.py's generic sampler_inputs pass-through — no backend change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnostic conclusion from the JoyAI-Echo engine: conditioning is fed to the sampler
unchanged (gemma+connector, which Studio already produces), so the ONLY missing sampler
feature is generate_BidirectionalMemory's cross-shot paired AV memory bank. This lands the
VIDEO half: a managed set of clean prior-shot frames injected into each scene so identity
carries across the whole chain — JoyAI's story-level consistency.
Generalizes the single-anchor mid_scene_guide to N frames, reusing the same LTX
guide-attention primitive (_append_guide_latent). _JoyAIMemoryBank holds the policy
(num_fix opening shots pinned as a global anchor + rolling most-recent window capped at
max_size, matching configs/inference.yaml: max_size 7, num_fix_frames 3, center select).
Harvest one clean frame per finished scene; inject the bank before each continuation scene;
supersedes mid_scene_guide when on. Honors the 0.25 audio-safe strength floor.
Off by default, so the audio protect/pass-through invariant is unchanged. Paired audio
memory + v2a_grad_scale coupling is the next phase (needs the ltx_core v2a term).
Inputs: joyai_memory, joyai_memory_size, joyai_fix_frames, joyai_frame_select,
joyai_memory_strength. Threaded through batch-training; logged in run_mechanisms + the
refinement sampler-context. New test_joyai_memory.py covers the bank policy (6 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A scene's frozen render.durationSec was _planDurationSec (snapped frames/fps), but the
actual encoded file can be longer (LTXAV muxes audio; container fps rounding), so the
timeline drew the clip SHORTER than the real 10s media. sceneDurationSec already routes
render.durationSec for project-mode scenes, so the fix is to store the real length there.
Probe the decoded file's duration (mirrors _probeVideoClipDuration) and adopt it for
STANDALONE renders only — multi-scene chains share one file and partition it via
inSec/durationSec slices, which must keep their plan-based lengths. Runs on generation
(immediate) and on project load via _validateSceneRenders (retroactively fixes existing
clips once their file is confirmed on disk). Idempotent.
A separated-audio track that was auto-pinned to the old plan length is carried along to
the new duration; a hand-trimmed track (length no longer matching) is left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The score_slider synthesized a symmetric +/-liked pair, so its axis was
"liked vs anti-liked mirror" — meaningless as an away-from-bad signal. When
3+ disliked/awful gens have populated the existing bad_dir slot in the same
taste store, build the slider's minus pole from that real disliked direction
instead. The (eps_+ - eps_-) axis becomes good-vs-bad, actively steering the
noise prediction away from the conditioning region that produced rated-bad
gens. Falls back to the symmetric mirror when no bad_dir exists.
Reuses existing plumbing: bad_dir is already accumulated identically to
liked_dir (running-averaged unit-norm delta from session mean). Generalized
the direction loader to _load_taste_direction(slot); load + pass bad_dir at
the call site; log/run-mechanism note which pole is active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scene.eff_frames / eff_fps returned the per-scene value whenever frames/fps was
non-None, ignoring frames_mode. So a scene in "project" (inherit) mode with a
leftover per-scene frames value kept that stale length and ignored project length
changes — exactly the "scene ignores project length" symptom. Now only
timeline/custom modes use the per-scene value; project mode always inherits.
The frontend already respected the mode (sceneEffFrames/sceneEffFps); this aligns
the backend (per-scene duration, chain layout, merged-cut total) with it.
Tests: project-mode scene ignores stale frames/fps and follows project length
changes; timeline/custom keep their per-scene length. Updated the subclip-collapse
fixture to mark split clips timeline-mode (how the split UI actually creates them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full control let you wire freely, but build() still auto-wired and honored role-
default wires with no cycle-safety — so inserting an upscale node (decoded frames
-> upscale -> export) could close a loop through Studio·source_image and hand
ComfyUI a graph it rejects with the opaque "Dependency cycle detected".
- auto-wire is now cycle-aware: never picks a source that already depends on the
target (reports it for explicit wiring instead of looping).
- _break_cycles runs before validation: a looping auto/role-default wire is
dropped with a clear note; an all-explicit loop is reported as blocking with the
exact node path. Core-internal links and explicit user/override/input-source
wires are protected and never dropped.
- Decoded frames (sampler output 1) feed a custom node via core:sampler:1, and a
loader with no explicit output wire still auto-feeds a custom node's input — so
UpscaleModelLoader -> ImageUpscaleWithModel -> VHS images wires cleanly.
Tests: upscale-before-export wires acyclically + loader auto-feeds; looping
role-default source_image wire is dropped; all-explicit cycle is reported.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-scene prompt preview mirrors what Studio encodes, and the postfix is
appended to every scene at generation — so it belongs there. Surface it once
after the scenes as a 'postfix' badge, symmetric to the 'anchor' badge shown
once above them.
- bridge.expand_prompt_fragment: expands a standalone fragment (anchor/postfix)
the same way generation does, so the previewed text matches exactly.
- server preview: result["postfix"] = expanded effective_postfix(p).
- inspector: postfix badge line; _promptPreviewKey includes postfix so editing
it refreshes the preview live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cut order (timeline_order) is a list of scene ids. Editing the global
prompt rebuilds the plan and reassigns scene content to ids positionally, but
_distributeGlobalPrompt left timeline_order untouched — so a stale id sequence
scrambled the live clips against their plan badges (e.g. 01,02,04,03,...),
even though ghost re-layout (plan-anchored) stayed correct.
Fix: the cut order tracks the plan until the user explicitly pins a custom
order via the ◀ ▶ arrows. New Project.timeline_manually_ordered flag, set by
moveTimelineClip; _ensureTimelineOrder follows plan order when it's unset and
heals (survivors keep their manual order, new appended) when set. Reconcile on
global-prompt apply too. Existing drifted projects self-heal on load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A shortcut in the postfix is expanded at the generation hit (same split_scenes
path as the anchor) and its refinement key folds into every scene. The raw
postfix is preserved on the project for round-trip; expansion is generation-time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Symmetric to the anchor (prepended): a project-level postfix appended to
each scene's conditioning. Lives next to the negative prompt in the project
inspector with its own enable toggle; never part of the verbatim global prompt.
- timeline: Project.postfix + postfix_enabled, effective_postfix(), carried in
build_generation_scene_segments and folded into the run fingerprint.
- conditioning: split_scenes_from_segments expands the postfix (keys fold into
every scene); the structural fold appends it after anchor+scene text.
- inspector: Postfix field with inline enable toggle after Negative prompt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reliability sweep finding. list_projects() lists a project as long as its JSON
parses, but _project_or_404 -> projects.get() did Project.from_dict(json.loads(...))
with no guard. A corrupt or schema-incompatible project file therefore appeared in
the list but 500'd with no detail when opened (an un-openable dead end).
- server._project_or_404: catch the load failure and raise HTTPUnprocessableEntity
with a single-line, header-safe reason instead of a bare 500.
- frontend loadProject: wrap API.getProject so a failed open keeps the current view
and shows a notice with the reason, instead of silently half-loading.
- Tests: corrupt project -> clean 4xx (reason single-line); missing project -> 404.
Sweep also confirmed the rest is solid: generation poll gives up after 3 pending
checks with a clear message, queue_prompt raises on node_errors, project/media/nle
JSON parses are guarded, saves are atomic, commit() prunes selection by id, failed
saves flip the 'unsaved' chip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause the user pushed on: the editor flattened all scenes into ONE prompt
string and Studio re-derived boundaries by parsing text, so a carry scene with no
transition needed an injected 'scene N' delimiter. The editor already KNOWS every
boundary (each generative unit is a scene) — it never needed to smuggle markers
into the prompt.
- timeline.build_combined_prompt: no longer injects anything (verbatim anchor +
scene texts). New build_generation_scene_segments(project) lists the boundaries
the editor already has (each unit's raw text, prefixed with its REAL transition
so effects survive; carry = plain text).
- conditioning.split_scenes_from_segments: canonical split from an explicit scene
list (reuses split_scenes per item for expansion/keys/effect, folds the anchor),
same output shape as split_scenes. refine_v2 gains scene_segments and uses it
when present (count-preserving: a text-less i2v scene keeps its slot, no anchor
shift); falls back to text-splitting only for raw node-pack use.
- server/builder: editor sends scene_segments -> studio_settings.refiner.scenes.
Net: the generation prompt Studio encodes is now exactly what the user typed —
no 'scene N' anywhere, in the prompt or the conditioning. Tests updated to assert
the verbatim prompt + structural segments; new tests for split_scenes_from_segments
(boundaries, empty-scene preservation, anchor/key fold). Fallback path unchanged
(stash-verified zero regression). NOTE: montage generation needs GPU verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-scene bug: the Movie Editor injects a generic 'scene N' separator before
any scene that lacks a leading transition trigger (build_combined_prompt), purely
to force a boundary. split_scenes was splitting on those markers with placement
'start', which KEEPS the marker word in the new scene's text -> 'scene 1',
'scene 2', ... got encoded into every scene's conditioning. With a text-less
leading i2v scene this also produced a label-only first scene (renders as the
anchor with no prompt) and shifted each scene's content down by one.
Fix in the one canonical splitter: a generic 'scene <N>' label (not a real
custom trigger in the transition DB) now cuts SILENTLY -> boundary only, dropped
from both expanded (generation) and raw (editing) text. Real user/DB triggers
keep their configured placement, so 'cut'/'qcut' etc. are unaffected.
Regression test asserts the markers split but never appear in any scene's
expanded/raw text, and that a real trigger still leads its scene.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Chain Sampler's Guidance section already exposes embed_guidance via the
sampler-knob path; add its score-space sibling the same way. Two new knobs in
SAMPLER_KNOBS (score_slider bool, score_slider_strength eta 0-3, dependsOn the
toggle) listed in the chain_guid section beside embed_guidance. Flows through the
existing sampler_inputs path (setSamplerInput -> _run_sampler_inputs -> builder ->
Chain Sampler node); refinement_key_input is already wired, so no backend change.
BachVid + KV-Lock need no UI (automatic, ride the refinement key).
Test: score_slider sampler_inputs pass through _run_sampler_inputs untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Training-free concept sliders (arxiv 2511.00103) in noise-prediction space,
sourced from the learned taste (user's choice). New score_slider + strength(eta)
inputs on the Scene Chain sampler, additive/non-disruptive, reusing the same
learned liked direction + embed_guidance_source + refinement_key_input.
Mechanism: a per-scene model_function_wrapper synthesizes a +/- concept pair from
the base conditioning and the liked direction (no concept prompts), runs 3 forward
passes on quality-phase steps and combines eps_mod = eps_base + eta*(eps_+ - eps_-).
High-sigma steps stay single-pass = the paper's k-step base-only warmup, keeping the
2x cost off early steps. Video-only: +/- conds are audio-protected and the eps blend
is masked to the packed video stream. Composes with embed_guidance (its nudge cancels
in the +/- difference). Installed/torn down per scene in reverse order; threaded
through batch-training re-dispatch. Fully guarded -> falls back to base prediction.
Tests: tests/test_score_slider.py (5) — combine, warmup, eta=0 no-op, eta linearity.
Pre-existing 16 suite failures unchanged (stale-mock/splitter set); zero new.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inversion-free adaptation of 'When to Lock Attention' (arxiv 2603.09657).
Track x0 prediction instability per step (relative step-change of the predicted
clean latent, video-masked = a cheap proxy for the paper's variance-of-x0
hallucination metric over a sliding window). High instability => the model is
still drifting => raise the shared kvlock_scale so BachVid holds the blessed
identity K/V harder; as x0 settles the multiplier relaxes toward base strength.
No DDIM inversion (LTXAV inverts poorly) — locks against the blessed K/V bank.
No-op unless a BachVid bank installed the shared scale list, so the two compose
cleanly. Wired into all three FunPack sampler loops after each model() call;
fully guarded, cannot break sampling. Tests cover capture/inject/scale + the
schedule's stable-vs-jumpy behavior and the no-bank no-op path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Training-free character carry across scenes/sessions (arxiv 2510.21696),
built on the existing LTXAV attn-hook infra. Capture raw K/V at IDENTITY_BLOCKS
during the mid-sigma window into a per-refinement-key temp bank; promote to a
blessed bank on liked ratings (EMA-merge, alongside bless_attn_weights). On
later runs with a blessed bank, lerp the running gen's K/V toward the blessed
identity at matched token positions (packed-/RoPE-safe analogue of the paper's
concat-attend). Wholesale inject at identity blocks (no spatial mask yet).
Exposes a shared kvlock_scale multiplier in transformer_options for the Phase 2
KV-Lock variance scheduler to drive injection strength per step (1.0 = off now).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route the conditioning variant from per-config outcome history, via the one
charter-safe surface: the INTERNAL value-function ascend+search (not user knobs,
so no WYSIWYG / hidden-settings violation). Sampler-owned visual ops stay on the
sampler (boundary law kept).
Directly serves the original complaint (Perfect not locking, Awful reproducing):
- _v2_path_conditioning_plan: matching-arm Beta posteriors -> liked-rate mean ->
"lock" (>=0.6: hold the liked conditioning, skip the stochastic search),
"explore" (<=0.4: widen the search to flee a disliked region), else "normal"
- value_function.search gains eps_scale to widen/tighten the search radius
- threaded conditioning_plan through ascend -> apply_scene_refinement_keys ->
finalize; computed once per run from path_outcomes + prompt concepts; logged
Only active once the per-key VF is ready, since it rides the existing ascend path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrade the seed explore/exploit gate from a flat reuse chance to a proper
bandit over the path-outcome arms:
- _v2_path_arm_beta: Beta posterior over an arm's liked-rate (liked>=0.3 success,
<0.0 failure, neutral ignored; Laplace prior)
- _v2_path_explore_decision: Thompson-sample theta per concept-matching arm vs a
fresh Beta(1,1) explore arm; good arms exploit, dislike-dominated arms route to
a fresh seed ("do something else")
- wired into _v2_choose_successful_seed (falls back to the flat gate with no data)
- shared _v2_path_matching_arms helper; _V2_KEY_REWARD + liked/disliked consts
5 tests incl. a statistical one: a strongly-liked arm rarely explores, a hated
arm almost always does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make path_outcomes ACT via the one Studio->sampler lever wired today: the seed
(FunPackStudio.run -> _v2_choose_successful_seed -> seed output -> sampler). No
new data paths invented.
The existing seed chooser only exploited liked seeds with zero avoidance — the
headline gap. Now:
- recorder tags each seed sample avoid = reward<0 and not skip_value_function
(Wrong-* excluded: the seed was fine, the words/identity were off)
- _v2_path_disliked_seeds: seeds that gave a low-quality result for matching concepts
- _v2_choose_successful_seed redraws a disliked fallback, drops liked-then-later-
disliked seeds from reuse, and reports how many it avoided
Broader config levers (guidance/lora) need new wiring and are deferred pending
graph-topology confirmation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited every learning sink in the V2 rating flow. Rating-learning and the
Absolute store already honored skip_learning, but two feedback sinks ran gated
only by feedback_prompt — so a 'Just ignore it' run with typed feedback still
wrote advisor_feedback_history + intent_expansion_memory.
Add _v2_rating_is_discard (forget rating or auto-discarded refusal) and gate
both feedback sinks on it, without touching the active continue/fresh_prompt
modes that legitimately keep feedback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reframe ratings from local steering into global routing: every rating —
not just successful ones — is recorded as a labeled outcome on the per-scene
config "arm" that produced it, so a later planner can route away from arms
that keep generating disliked results.
- _v2_path_signature: arm = family + concepts + guidance config, seed EXCLUDED
(seed is the random draw within an arm); md5 arm_id collapses identical configs
- _v2_update_path_outcomes: learns from EVERY rating, decaying counts so recent
behavior dominates; records seed-level samples for later seed routing
- hooked at the shared per-scene learning hub + single-run path; stored in
per-key state under "path_outcomes"
Pure instrumentation — no generation behavior change yet (Phase 2 = Studio planner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drives the editor's built-in tour (?mode=tour, fully mocked — no backend/models/
GPU) with Playwright: serves the static frontend, walks window.TourGuide, and
screenshots each step (annotated with its spotlight + caption) while recording a
walkthrough video. Emits NN-*.png stills, an index.md gallery, and
walkthrough.webm/.mp4. Self-maintaining: new tour steps appear automatically.
No more screenshotting each button by hand. node_modules/ and out/ are gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fake routes object installed into sys.modules as `server` only defined
get/post/delete; server.py also registers put (models) and patch (media), so
importing the real server.py during collection (e.g. test_server.py) raised
AttributeError and aborted the whole run. Add put/patch to the mock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>