feat(webv2): Ref2VA reference-extend — append the generation to an initial video - #191
Merged
Conversation
…itial video The Ref2VA panel gains an Initial Video section: the generated clip is appended to the uploaded video at its End Frame cutpoint (same extract + crossfade video_concat join as FL2VA extend; video_concat rebuilds the soundtrack from every input, so both clips' audio survives). Continuity comes from a linked video reference the panel derives from the clip — its trim defaults to the last 141 frames (~5s at 24 fps, exactly on the 17n+5 grid) before the cutpoint and re-derives whenever the cutpoints change, while remaining an ordinary reference card: trimmable, reorderable, conditioning-selectable, counted against the 3-video cap, and badged as linked. No frame conditioning and no backend changes; generation_mode stays minimax_h3_ref2v with the extend-style source_video extras riding along. State-wise, references and sourceVideo now coexist (validation still rejects the pair on models without reference-extend, and references remain exclusive with first/last frames). Switching FL2VA -> Ref2VA carries an initial video over and derives its tail reference; switching back clears references and keeps the clip for FL2VA extend. Recall restores both the references and the source video with its recorded trim, and the linkage re-establishes by clip identity when the recalled trims are next edited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
- Drop the sourceVideo <-> references rival rows from the gallery optimistic-rollback rules: the pair is legal on a reference-extend panel, and treating them as rivals made a FAILED gallery deletion permanently drop whichever slot the sweep had cleared. - applyReferenceExtendSourceVideo now prefers the flagged linked entry over an earlier same-name reference: a flag-or-name findIndex rewrote a user's own hand-trimmed reference (and left a second flagged entry dangling) when it sat above the linked one during a source swap. - The model-selection transition derives the linked tail reference only when none exists yet — it also runs on task-neutral edits (same-model re-selection, component changes), which reset hand-tuned trims the help text only promises to reset on cutpoint changes. - Setting an Initial Video with all 3 video-reference slots occupied now raises a warning toast instead of silently generating an extension with no continuity reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
lstein
requested review from
JPPhoto,
Pfannkuchensack and
blessedcoolant
as code owners
August 31, 2026 01:16
…me rate The 141-frame default was source-native, so a 16 fps clip sampled ~8.8 s instead of the intended ~5 s. Treat 141-at-24fps as a duration and convert it to source frames (16 fps -> 94, 30 fps -> 176); 24 fps sources keep exactly 141, which stays grid-exact through the backend's 24 fps resample and 17n+5 snap-down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
… count The parent scaled the tail window to the source frame rate, fixing the ~8.8s a 16 fps clip sampled. The window is still bounded by a second rule it does not account for, and that one costs the frames continuity actually depends on. Three backend rules bound a video reference, and every one of them discards from the END -- the frames adjacent to the cutpoint. normalize_reference_video_frames resamples onto H3's fixed 24 fps and truncates to the GENERATED frame count keeping the FRONT (frames[:num_frames]); encode_reference_video then snaps what survives DOWN to the 17n+5 grid. So a 141-frame window against the 124-frame default keeps 124 frames -- the right count, shifted 17 frames (0.7s) off the seam. The window is now min(TAIL, numFrames), the smaller bound winning. The fps conversion rounds UP rather than to nearest. Overshooting is free: the truncation cuts the window back to exactly numFrames and the snap -- both already on the 17n+5 grid -- then keeps it whole. Undershooting lands OFF the grid and the snap eats up to 16 frames. Rounding to nearest lost 17 frames at 23.976 fps (NTSC film) at every frame count on offer, and 17 at 16 fps at the 124-frame default. A test runs all three backend rules over 12 source rates x all 16 frame choices and asserts the kept count equals the budget exactly. Two defects found by adversarial review of the first cut, both created by making the window depend on the frame count: - The frame count is snapped BEFORE the window is derived. Deriving first let a Wan panel's count (as low as 5) size the window for a 90-frame H3 generation, under the 13 frames sample_text_conditioning_frames needs -- Generate failed outright with a ValueError. - The frame-count application re-derives instead of shrinking to fit. The Frames number input emits a value per keystroke, unclamped, so typing "345" arrives as 3 -- and a shrink-only rule pinned the window at a 3-frame budget for the rest of the session, silently. Re-deriving is idempotent, so keystrokes and slider drags leave no trace. Separately, the linked tail reference is APPENDED rather than prepended. Request order is rotary order: build_ref2va_packed_sequence lays the reference blocks out in order, each advancing a shared clock, and the generated rows start at the position the LAST block left behind. The reference the model continues from is the final one, so the continuity anchor belongs at the end -- with a single reference this changed nothing, which is why it went unnoticed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
`toTailAwareIndex` sends a near-the-end bound out as a NEGATIVE index the backend resolves against the clip's REAL frame count, and leaves anything further back POSITIVE, taken from the panel's ESTIMATE (duration x fps). That split is right for a hand-picked trim, whose start is an absolute position the estimate must not drift. The linked tail window straddles it: the cutpoint end goes negative while the start, a tail-length back, stays positive -- so what the backend extracts is `tail + (real - estimate)` frames, not `tail`. The length is a budget the backend enforces by discarding the overrun at the SEAM, so an inexact estimate put back exactly the frames the budget exists to protect. It also meant an estimate that overshot by more than `tail - 1` walked the start past the end and raised, a margin that shrank with the frame-rate-scaled window (140 frames flat before, as low as 29 for an 8 fps source). The linked entry's start is not an absolute pick -- it is defined as `tail - 1` frames before the cutpoint -- so it now rides the same negative anchor and the window keeps its exact length at any real count. Two cases stay absolute: a start the clip's own beginning already clamped to 0, and a cutpoint far enough from the end that both bounds already share the estimate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Request order is rotary order: build_ref2va_packed_sequence lays the reference blocks out in order, each advancing a shared clock, and the generated rows start at the position the LAST block left behind. The linked tail reference only anchors anything if it IS that block -- an image dropped after the Initial Video wedges a whole rotary slot between the initial video's tail and the first generated frame, and the model continues from the image instead. Two ordinary actions in the wrong order were enough, with nothing in the UI to say so. The anchor's position is now derived like its trim: last, always. `pinReferenceExtendAnchor` runs in the reference field's single setter, so it covers add, remove, retrim and drag alike, and on the adopt path where a recalled entry is claimed in place. The video cap is enforced at both doors rather than only one. The add-video button was already gated; the Initial Video field now disables itself when the video slots are full and no anchor exists yet, since setting a clip there has to place one. With an anchor already present the drop only re-derives it in place, so it stays available -- swapping the initial video keeps working. And `sanitizeVideoReferences` overflows videos from the FRONT. It keeps the first N of each kind, which for videos is now exactly backwards: with the anchor pinned last, tail-dropping discards the one entry the extension depends on. An over-cap list should no longer reach it from the UI at all, so this is the guard for a stale or hand-edited project record. Images have no ordering role and keep the front. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Clicking the randomize-seed toggle in the Video panel moved focus to the Seed
text field and never toggled anything. The Upscale panel had the same bug.
Chakra's `Field.Root` hands its single `ids.control` to EVERY control nested
inside it, and both panels' seed rows put three controls in one `Field` -- the
seed NumberInput, the shuffle button and the switch. The switch's hidden input
therefore claimed the same id as the seed input:
seedId=_r_0_ swId=_r_0_ collision=true
switchLabelFor=_r_0_ focusedSeed=true fired=0
`Switch.Root` renders a `<label>`, and `label[for]` resolves to the FIRST
element carrying that id -- the seed NumberInput, earlier in the DOM. So the
click activated the seed field and the switch never saw it. Giving the switch
its own `ids.hiddenInput`, derived from `useId` so it stays unique across
widget instances, breaks the collision.
The Video panel's label text was also an inert `<Text>` sibling outside
`Switch.Root`, so even a working switch would not have responded to a click on
the words. It is now a `Switch.Label`, matching Upscale.
Covered by a browser test asserting the switch and the seed input hold
different ids, that the switch's own label points at the switch, and that
clicking either the control or the label toggles without focusing the seed
field. Verified failing before the fix (`fired=0`).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… slop
The relative start index resolves to `startFrame + (real - estimate)`, which
goes NEGATIVE once the estimate overshoots by more than `startFrame` --
and `_ResolvedVideoRange.resolve` rejects an out-of-range index outright
rather than clamping, so Generate failed with
start_frame=-142 is out of range for a 138-frame reference video
The guard was `startFrame === 0`, which misses by exactly the margin that
matters: a start of 1 is the common case for a clip barely longer than the
window, and `duration x fps` overshooting by a frame or two is the error
`toTailAwareIndex` was written to absorb in the first place. Before the
relative start existed the same input emitted a positive 1 and generated fine,
so this turned a small quality loss into a hard failure.
The guard is now `startFrame <= TAIL_INDEX_SLOP`, reusing (and naming) the
3-frame allowance `toTailAwareIndex` already applies at the other end of the
clip. Below the slop the window fills nearly the whole clip, where its length
cannot be honoured anyway, and the absolute form is always in range; the drift
it costs there is the estimate error itself. Above it the window keeps its
exact length at any real count, which is what the relative form is for.
Verified against the review's failing cases: est=143/real=141, est=145/real=138,
est=92/real=90 and est=1002/real=1000 all resolve, while est=402 keeps a
141-frame window at real counts of both 400 and 396.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…e cap trim Two of this branch's own changes combined into data loss. `sanitizeVideoReferences` was changed to drop videos from the FRONT, on the reasoning that the anchor is pinned last so a tail-drop would discard it. That holds only for panels this build wrote. The commit immediately before this branch PREPENDED the anchor, so every project saved before it loads with the anchor at index 0 -- and the front-drop then deletes exactly the entry it was added to protect. The second half is quieter and hits every already-saved panel, over the cap or not: normalization never pinned, so a prepended anchor stayed first. The generated frames continue from the LAST reference block, so those panels keep generating from whatever reference follows the anchor. Nothing repaired it -- `setNumFrames` does not pin, and the model-selection transition skips the derive entirely when a flagged entry already exists -- so it persisted until the user happened to touch the reference list. Pinning inside `sanitizeVideoReferences`, ahead of the cap trim, closes both: the anchor is last by the time the overflow rule runs, so the front-drop protects it, and normalization runs on every read, so old panels heal on load. The pins in `setReferences` and `applyReferenceExtendSourceVideo` stay. They are redundant with this one on any path that normalizes, but they keep the written value correct for any that does not, and a violated ordering invariant is silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
`addImageReference` / `addVideoReference` closed over `references`, awaited a gallery resolve, then wrote `[...references, item]` -- the array as it was at drop time. `isLoading` gates the reference cards and the drop zone, but not the Initial Video field or the Frames slider, and this branch made both of those writers of `references`. Two losses, neither of them warned about: - Drop a video into References; while it resolves, set an Initial Video. The resolve lands and writes the pre-anchor array. You get `sourceVideo` set with NO anchor, the cap gate reading false and validation passing, so the extension generates with no continuity reference at all. - Drop an image into References; while it resolves, drag Frames 345 -> 90. `setNumFrames` re-derives the anchor to a 90-frame window; the resolve then restores the stale 141-frame one. `frames[:90]` keeps the FRONT, so 51 frames at the cutpoint are discarded -- the exact bug this branch exists to fix -- and nothing re-derives afterwards, so it is permanent. `onChange` now takes an updater instead of a snapshot, and `setReferences` applies it against a ref holding the newest normalized list, so a write that landed during the await is merged rather than clobbered. Every handler converted, not just the async ones: a delete or reorder during an in-flight resolve is the same race. The ref syncs in an effect rather than during render -- the react-compiler rule forbids touching a ref while rendering, and a gallery resolve lands whole frames after the commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
The gate was `no flagged entry && videoCount >= max`, but the setter refuses
only when `no flagged && no same-name entry && videoCount >= max`. Adopting an
existing reference for the clip consumes no slot, and the gate ignored that
term -- so it disabled the field in a state the setter handles fine.
Metadata recall lands exactly there. `graph.ts` records the references without
`fromSourceVideo` (it is panel state, deliberately not metadata) and records
`source_video` separately, so recalling a reference-extend generation that used
three video references restores three UNFLAGGED videos beside the clip: no
flagged entry, three videos, gate fires.
`disabled` reaches the clip's trim sliders as well as the drop target, so the
cutpoint could not be moved at all on a clip that was legitimately set,
re-dropping the same clip to re-establish the linkage was unreachable, and the
message ("Remove a video reference first") was wrong -- there was room.
Both sides now defer to `canPlaceReferenceExtendAnchor`, and a test drives the
predicate and the setter over the same seven reference states and asserts they
agree, so the gate cannot drift from the behaviour it describes.
The gate stays conservative for a clip not yet dropped: its name is unknowable
before the drop, so the answer is whether a NEW anchor would fit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… grid exactly Two seam losses in the same function. A clip shorter than the tail window fell through to `Math.max(0, ...)` and the window became the whole clip -- an arbitrary length, so generally OFF the 17n+5 grid, and `encode_reference_video` snaps DOWN and drops the difference from the END. No estimate error needed: at 24 fps a 120-frame clip with numFrames 345 kept 107 of its 119 frames and stopped half a second short of the cutpoint. It was the common case for any source under ~5.9s at 24 fps, and the grid test could not see it because it only ran a 402-frame clip. The clamped branch now falls back to the largest ON-GRID budget the clip does support, so the same frames land ON the seam instead of before it. And `ceil(budget * fps / 24)` was never exact. The resample is a step function, so rounding up overshoots by a frame or two, and the overshoot is discarded at the seam. Walking down to the smallest source count that still reaches the budget hits it exactly wherever the source's frame boundaries allow, and within one frame where they cannot (12 fps cannot reach an odd 141 at all). Over 15 rates x all 16 frame choices the total seam offset halves, 72 frames to 36, with no budget lost either way. The grid test now sweeps short clips across 6 rates x 7 clip lengths x 4 frame counts, asserting at most one frame is trimmed and the window still ends on the cutpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…bilise setNumFrames Two leftovers from the review. The continuity anchor is pinned last, so the arrows that would displace it fired a patch and got the list straight back: the anchor's own up arrow, and the down arrow of the card above it. Both looked enabled and did nothing (worse, the patch remounted the list, since the keys embed the index). The card now takes `canMoveUp` / `canMoveDown` instead of `isFirst` / `isLast`, and the list excludes the moves the pin reverts. `setNumFrames` listed `values.references` as a dependency, which is a fresh array on every render, so the callback identity churned and the Frames slider re-rendered on every patch anywhere in the panel -- against this file's stated "every prop identity is stable by construction" contract. It now reads the list through the ref added for the functional-update fix, which is safe precisely because the re-derive is idempotent in `numFrames`. The ref moves above both consumers: declared after them, react-compiler flagged the effect's assignment as modifying a value already passed to a hook. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… flag on load
Two identities that did not survive a boundary.
The latest-references ref crossed PROJECTS. The video widget is reconciled,
never remounted, across a project switch (the <Activity> key is the panel
instance, not the project), while `patch` stays bound to the project its render
belonged to. So the bare ref tracked whichever project was ACTIVE: drop a video
into project A's references, switch to B before the gallery resolve lands, and
the captured updater ran against B's list with A's patch -- replacing A's
reference list with B's entries plus the new one, invisibly until the user
switched back. The snapshot array this branch removed was at least
per-project-correct. The ref now carries `{ projectId, references }` and a
write whose project has moved on is dropped: losing one pending add because
the user switched projects mid-resolve beats overwriting a list they can see.
The anchor flag did not survive RECALL. `fromSourceVideo` is panel state and
never reaches metadata, so a recalled reference-extend panel arrives with its
anchor unflagged beside the source video -- and every invariant this branch
built keys on the flag. The pin skipped it (the model continued from whatever
reference followed), the frame-count re-budget skipped it (Frames 345 -> 90
left a 141-frame window and the backend cut 2.125s off the seam, the exact
failure this branch exists to prevent), and the seam-anchored start index fell
back to the absolute form. The cap-gate commit even taught its predicate to
tolerate the unflagged shape without noticing the other three invariants do
not.
The flag is derivable, not just storable: the video reference naming the
Initial Video's clip IS the anchor, the same identity the setter adopts by.
`sanitizeVideoReferences` now re-derives it -- before the pin, which is before
the cap trim, so a freshly flagged anchor is also immediately last and
protected from overflow. An already-flagged entry stays authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…d say why
`setSourceVideo` detected the refusal -- `applyReferenceExtendSourceVideo`
returning its input untouched -- toasted about it, and then patched the clip in
anyway. That left the panel in the worst available state: an Initial Video set
with NO continuity anchor, generating an extension with nothing sampling the
clip, and the cap gate then closing over it so the trim sliders froze. The cap
gate cannot pre-empt this: it can only ask about the clip currently set, and
the refused drop is by definition a different clip. The drop is now refused
whole -- no partial application -- and the toast says what actually happened
("Initial video not set") instead of describing a reference that was never
created. Clearing is unaffected: removing the linked entry cannot overflow
anything.
And when the gate does close with a clip already set (reachable by deleting the
anchor card and filling the slots), the only symptom was frozen trim sliders:
`disabledReason` rendered exclusively in the empty-state slot, which a set clip
replaces with its preview. The trim block now shows the same reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… newest
The render-time cap gates (`canAddVideo` / `canAddImage`) are evaluated before
the add handlers' await, and the Initial Video field is an independent writer
that can fill the slots during it. The over-cap write then reached
normalization, whose front-drop overflow deleted the user's OLDEST reference --
where the pre-branch keep-first rule had at least deleted the surplus add.
So a race turned into silent loss of the user's earliest work.
Two layers:
- The add updaters re-check the cap against the LIVE list at apply time. A
declined add surfaces through the field's existing error line ("All 3 video
reference slots filled while this one was loading, so it was not added")
rather than vanishing. The updater form makes this exact: it runs against
the same list the write would land on.
- Sanitization's overflow now drops the NEWEST non-anchor videos instead of
the front. The surplus in any over-cap record is whatever arrived last; the
anchor stays exempt wherever it sits, which was the one thing the front-drop
got right. This also restores the pre-branch behaviour for anchor-less
lists, which the front-drop had changed for no reason the rationale covered.
The race shape is pinned in a test: [B, C, anchor, D-raced-in] normalizes to
[B, C, anchor] -- the racing add is the casualty, not B.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…ndings setSourceVideo was the one reference writer still resolving against a captured `values.references` -- and it is called from an await (the clip field's adopt and upload paths), so a reference added while an Initial Video drop resolved was clobbered by the stale list. It now reads through the project-scoped ref like its siblings, with the same drop-the-write guard for a project that moved on, and its refusal identity check compares against the list it actually used. tailSourceFrames gets its closed form: for an integer budget, `floor(t*24/fps + 0.5) >= budget` is exactly `t >= (budget - 0.5)*fps/24`, so the smallest count is a ceil away, with two at-most-a-step loops absorbing last-ulp float disagreements. The previous walk down from `ceil(budget*fps/24)` ran ~fps/24 iterations -- its own comment claimed the inverse bound -- which an absurd-but-probeable container frame rate turned into 124 ms per Frames keystroke. Verified value-identical across 3,952 (budget x rate) pairs including NTSC rationals; the up-loop never fired. The browser seed-toggle test now sets IS_REACT_ACT_ENVIRONMENT like every other browser test, so its act() calls actually gate on effect flushing instead of warning six times and passing on userEvent's own awaits. Also DECIDED, recorded in toReferenceStartIndex's docstring: TAIL_INDEX_SLOP stays at 3. An estimate error past the slop can still hard-fail the relative start, but only on a clip barely longer than the window (it needs error > startFrame), and 3 is this file's declared bound on estimate error. A wider guard would move the cliff, not remove it, and pay with silent length drift on every clip inside the wider margin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Four fixes, two of them sharing a root. The derived flag adopts the LAST same-name entry, not the first. The pin invariant records the anchor last, and metadata can hold a user's own reference to the source clip ahead of it -- a first-match flagged the user's entry: their trim was re-budgeted, the list reordered against the recording, and the true tail window sat unprotected at the seam, the exact bug the derivation exists to fix, displaced onto the wrong reference. The flag is canonicalized to at most one entry (keeping the last) before the pin and the trim. Two flagged entries -- a corrupt or hand-merged record -- were a poisoned state: the pin swaps the pair on alternate normalization passes, and the overflow trim exempts every flagged entry, so an over-cap list of them could never come back under the cap. With one flag, both invariants hold unconditionally. The drop set is also keyed by index rather than object identity, so an aliased entry can no longer take its twin down with it. The fps guard gains an upper bound (1000). Past ~2^53 source frames the tailSourceFrames adjustment loops cannot even step -- `tail + 1 === tail` in floats -- and spin forever: a hand-edited record with fps 1e17 froze the tab on the first Frames keystroke, verified with a real 60s hang. The closed-form commit fixed the ~1e6-range loop cost and left this range hanging; absurd rates now take the same fallback as broken ones. The reference ref carries a LIVENESS bit, set in its sync effect and cleared in that effect's cleanup. A hidden <Activity> destroys effects while promise continuations keep running, so the ref froze with the projectId still matching -- and a same-project guard replayed the frozen list over whatever the store had done meanwhile, resurrecting references a gallery deletion sweep had removed. A write now needs the ref both live and same-project. setReferences also skips the patch when the updater returns its input identity: a declined add no longer fires the spread's first/last-frame clearing or dirties the project with a no-op write. Comments corrected to say effect-fresh rather than live -- two resolves in one microtask drain still share a window, which is inherent to reading through React state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…gh-rate clips The float plateau the bound guards (`tail + 1 === tail`) starts around fps 1.5e15; the bound was 1000 -- twelve orders tighter than the failure. Every rate in between terminated in the closed form and produced a correct window before the guard existed, and the clamp broke it: a probe-reported 1200 fps clip took the 24 fallback, the frontend sized a 141-SOURCE-frame window, and the backend -- which resamples at the rate it probes itself, with no fps field in the graph to override it -- collapsed that window to 3 frames, under text conditioning's 13-frame minimum. A working input became a hard invoke-time ValueError with nothing pointing at the clamp. 1e6 kills the hang with nine orders of margin and no reachable regression. The boundary test now actually pins the behaviour on both sides: 1200 fps on a 50001-frame clip derives its real 7025-source-frame window (previously asserted through a fixture too short to distinguish the clamp from its fallback), and 1e17 takes the fallback and terminates. Found by the fourth adversarial pass, scoped to the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
lstein
enabled auto-merge
September 1, 2026 18:26
lstein
added a commit
that referenced
this pull request
Sep 1, 2026
…cord baselines post-merge Merging main brought in the Ref2VA reference-extend UI (#191), whose VideoReferenceListField carried two new upload call sites hard-coded to board 'none' — the same bug this PR fixes in the other video-panel fields. They now use the video port's getUploadBoardId(). The reference list's video upload input also accepts audio/* — the server wraps audio files into waveform videos, which is how audio-only reference clips enter the pipeline. Performance baselines re-recorded on the merged tree (both sides of the merge had re-recorded them independently) and formatted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #190. Adds the Ref2VA counterpart of FL2VA's extend mode: an Initial Video section on the reference panel whose clip the generated video is appended to, with continuity supplied by a linked tail reference instead of frame conditioning.
The flow
[end − 140, end]— and re-derives live whenever the cutpoints move. It is otherwise an ordinary card: re-trimmable (the "frames to sample" knob), reorderable, removable, conditioning-selectable.141 sits exactly on H3's 17n+5 frame grid, so the backend's reference snap-down keeps the whole window.
Mechanics
video_concatjoin as FL2VA extend (addSourceJoin, factored out of the extend scaffolding; no last-frame extraction, no frame conditioning).video_concatrebuilds the soundtrack from every input, so both clips' audio crosses the seam. No backend changes.generation_modestaysminimax_h3_ref2v, with the extend-stylesource_video+ trim extras riding along for recall.referencesandsourceVideonow coexist; validation rejects the pair on models without reference-extend, and references stay exclusive with first/last frames. The linked reference carries a panel-onlyfromSourceVideoflag (never recorded in metadata).Adversarial review
A fresh-context review ran before push; confirmed findings fixed in the second commit with regression tests:
sourceVideo ⊕ referencesas rivals, so a failed gallery deletion permanently dropped one of the pair;Testing
6765 webv2 tests pass, including new coverage: trim derivation/clamping, linkage add/update/adopt/remove/cap, flag-priority matching, mixed validation, FL2VA↔Ref2VA transitions, the reference-extend graph shape (concat output, no frame conditioning, metadata payload), recall of the combined recording, and the rollback-restore regression.
🤖 Generated with Claude Code
https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A