feat(LTX25-RETAKE): regenerate a time window of an existing clip, on upstream's frame-folder arm (#924, #987) - #992
Merged
Merged
Conversation
…s the audio question (#924) `RetakePipeline` regenerates a chosen time window of an existing clip. This spec commits before any implementation, as `## Spec before code` requires. The clip enters as a directory of `frame_%06d.ppm` rather than a container, because no demuxer is vendored here (`video_api.cpp:115-121`). That is not a local workaround: upstream carries a second ingestion arm for exactly the no-container case, and every consequence this row inherits from it is upstream's own -- the frame rate must be supplied (`media_io/decode.py:213-215`), the folder has no audio stream (`utils/helpers.py:261-262`), and both of retake's audio predicates are conjunctions with `initial_audio_latent is not None` (`retake.py:279,282`), so `regenerate_audio` has no observable effect on this arm. Mirrored rather than repaired, and pinned by a test so the next reader finds it asserted. Two record corrections ride in the spec because deriving them a third time is the cost this protocol exists to avoid. `ref_video_dir` is NOT read by nothing -- MiniMax-H3 consumes it fully at `minimax_h3_video.cpp:647,650`; what is true is that the LTX-2.5 engine never reads its contents. And #924's cited recipe-refusal anchor has drifted from `ltx2_pipeline.cpp:1131-1136` to `:1175-1179`. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…upstream's frame-folder arm (#924, #987) `RetakePipeline` keeps the source clip outside `[start_time, end_time)` and regenerates what is inside it from the prompt (`retake.py:53,151-329` @ `fd4ded7f`). It is reachable from `vllm_video_generate` and from `ltx2-gen` through `--pipeline-kind retake` plus `--ref-video` and the three window knobs. THE SOURCE IS A FRAME DIRECTORY, AND THAT IS UPSTREAM'S SECOND INGESTION ARM rather than a local substitute. Upstream opens a container with PyAV (`media_io/decode.py:226`) and no demuxer is vendored here, so a `.mp4` is refused by name. Upstream also carries a folder arm for exactly that case, and three consequences follow from its own lines and are mirrored rather than invented: the frame rate must be supplied because a folder has none (`decode.py:213-215`), a folder carries no audio stream (`utils/helpers.py:261-262`), and both of retake's audio predicates are therefore false whatever `regenerate_audio` says (`retake.py:279,282`). The last one is surprising, it is upstream's, and `test_ltx2_retake` pins it so the next reader finds it asserted instead of re-deriving it. `TemporalRegionMask` (`noise_mask_cond.py:9-45`) lands as a REPLACEMENT of the all-ones denoise mask `create_initial_state` writes, which is what `state.denoise_mask.copy_` does at `:44`, and nothing else in the phase loop changes. Every consumer of that mask already broadcasts it, so seeding `video_initial` with the encoded clip and zeroing the mask outside the window is the whole mechanism. Four things in it fail silently and each has a case: `causal_fix` defaults TRUE at the call site (`:33`) and FALSE in `get_pixel_coords` (`patchifiers.py:140`), so reading the function's default moves every boundary by seven pixel frames; the video bounds are latent frames and the audio bounds are already seconds (`:27-35`); the test is OVERLAP, not containment (`:39`); and the write replaces rather than conjoins. `Ltx2ConformLatentLength` truncates OR ZERO-PADS (`utils/helpers.py:149-162`) and is deliberately a second function rather than a flag on the audio-to-video one, which truncates only (`a2vid_two_stage.py:202`) and where a short take is an error because `create_initial_state` asserts the shape (`ltx-core/tools.py:146-148`). One shared helper would have to pick a polarity and be wrong for one caller. The `retake` recipe is its own row rather than `distilled_two_stage` with a phase removed: that recipe renders stage 1 at `spatial_downscale = 2` and a full-resolution source latent does not fit that grid, so a retake on it is refused by name. TWO STALE REFUSAL SENTENCES REPAIRED IN THE SAME FLOW (#987, filed and closed here). `ltx2_video.cpp` claimed "nothing reads `ref_video_dir` at all" -- false about the tree when written, because MiniMax-H3 consumes the directory in full at `minimax_h3_video.cpp:650`, and doubly false now -- and claimed "there is no AUDIO_VAE_ENCODER key filter", which `c2019b0e3` landed. Both are rewritten in the `WHAT IS *NOT* THE REASON` shape, and the guarding case asserts the LOCAL fact that the LTX side now reads the directory, because a case asserting only upstream symbol names is how the first sentence survived. The reference conditioning arm itself stays refused and #975 stays open: it consumes the same directory through a downscale resize, a temporal subsample and an appended stage-1 item, none of which retake performs. Owed and named: a container demuxer, without which retake's audio half cannot exist at all; and the real-checkpoint render, since the GPU was out of bounds for this row. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
The row branch was cut at 0e1bee4 and main has moved. Merged rather than rebased so the spec-before-code order stays visible in the history. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…ing observed (#924) The row's mutation pass found two guarantees asserted only in prose. M2 deleted the seeding of the phase's initial video latent with the encoded source clip -- `video_initial = retake_video_volume;` -- and every case stayed green. `retake_latent_absmax` observes the ENCODE and says nothing about whether the latent ever reached the phase, so a build that read the clip, encoded it, recorded the digest and then started from zeros passed the whole suite and rendered a clip of the right length with the right mask. The repair renders two DIFFERENT source clips at the same seed and the same window and requires the pixels to differ; on the mutated build both start from zeros and are identical. M3 flipped the production call site to `causal_fix=false`, taking `get_pixel_coords`' own default (`patchifiers.py:140`) instead of the call site's (`noise_mask_cond.py:33`), and stayed green too. At 24 fps the window [0.05, 0.10) selects one latent frame under BOTH polarities, so the case could not see the difference it was written to pin. The repair adds a window, [0.30, 0.40), where the fix selects one latent frame and its absence selects both, so the count separates them. M7 additionally failed to BUILD -- dropping `has_audio_latent` from the audio predicates left the parameter unused under -Werror -- and a mutation that fails to build establishes nothing, so it is re-run with the parameter still referenced. It is DETECTED. All ten mutations are now detected. Each is recorded with three facts: the `git diff --stat` after applying, whether it BUILT with the compile-error count, and the exit code, captured directly rather than through a pipe. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…nged, and the mutation table (#924) Section 3.3 planned to lift MiniMax-H3's `ReadReferenceClipChw` into a shared surface, on the ground that writing a second frame walker by hand is what `AGENTS.md` `## Shared seams` forbids. Reading both sides made that the wrong seam, and the reason is recorded rather than the section quietly rewritten: H3's walker returns raw pixels at whatever size the files are, while upstream's video ingestion runs every frame through `video_preprocess` -> `resize_and_center_crop` to the target size (`utils/helpers.py:228`, and `:202` says the folder arm's centre-crop is chosen to match it). This tree already has that chain on the LTX side as `Ltx2LoadImageAndPreprocess`. Lifting H3's walker would have shared the six-line file loop and NOT the preprocessing, which is the half with an upstream anchor and the half that fails silently. Section 8 adds the mutation table, three facts per row, and the reachability answer in the two parts the guide asks for. Two mutations survived first time and both are written up rather than summarized away. `## Now` records that `ROAD-V1-LTX25` does NOT change lifecycle state here, so `docs/STATUS.md` has nothing to record and this branch does not touch it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…hich surfaces reach it (#924) The retake table listed the per-generation extra keys and left a reader to guess what to type. It now gives the `ltx2-gen` flag beside each one, says the extras ride the existing `extra_keys` / `extra_values` array so the C ABI needs no new field, and states plainly that `/v1/videos` forwards no engine extras today (#928) -- so the CLI and the C ABI are the reachable surfaces and the HTTP one is not claimed. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
… resolves (#924) The recipe-table case enumerates supported and refused `(kind, version)` pairs by hand, so the two new `retake` rows needed adding on both sides: `retake`/2 and `retake`/2.5 resolve, `retake`/2.3 is refused by name. Resolving is the weaker half. Every value on a `retake` recipe is read off `retake.py` rather than adapted from a neighbour, so each is asserted: ONE phase at `spatial_downscale = 1` with no input transform (`:313-324`, `:317-318`), `DISTILLED_SIGMAS` (`:287`, because `distilled` defaults True at `:85` and the CLI hard-codes it at `:359`), an empty negative prompt and no guidance override (`SimpleDenoiser` at `:290-294`, `[prompt]` alone at `:259`). The stepper assertion is made against the SIBLING recipe rather than against a literal. `DiffusionStage.__call__` defaults `stepper` to `EulerDiffusionStep()` (`utils/blocks.py:526-527`) and retake overrides neither `stepper` nor `loop`, so the ancestral sampler `distilled.py` selects for generation 2.5 reaches retake through nothing. Checking `kEuler` alone would pass on a build where both recipes were ancestral or both plain; checking it beside `distilled_two_stage`/2.5 being `kEulerAncestral` cannot. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
localai-bot
pushed a commit
that referenced
this pull request
Aug 16, 2026
…ine kinds `LTX25-RETAKE` (`3ce1cf7c7`, #992) landed while this row was in review, and the two rows overlap on ten files. Four conflicted as source, and all four conflicts are the same shape: both rows add a pipeline kind, so both extend the recipe table, the per-generation extras list and the refusal set. Every one is resolved as a UNION with nothing dropped from either side. - `ltx2_pipeline.h` — the `ResolveLtx2PipelineRecipe` table comment. The `dfr` row and the two `retake` rows now sit together, and retake's paragraphs on why it is not `distilled_two_stage` with a phase removed are kept in full. - `ltx2_pipeline.cpp` — `DfrRecipe` and `RetakeRecipe`. Git's conflict swallowed the shared `return recipe; }` tail, so both functions had to be closed by hand; each keeps its own body byte-for-byte. The dispatch arms below auto-merged and both kinds resolve. - `ltx2_video.h` — the extras key constants. `temporal_upsample_rounds` and the five retake keys are all declared, each with its own comment block unchanged. - `ltx2_video.cpp` — the known-extras predicate, the "this family defines" list and the `Generate` preamble. The preamble conflict is the only semantic one: DFR gave `CheckGeneratedKeyframes` a `pipeline_kind` parameter, so the merged call is DFR's two-argument form, and both refusal blocks run. `READER ANCHORS` was re-derived rather than assumed. Both rows insert above it, so neither side's list was right: every anchor moved by one, `780 …` became `781 …`, and the list here is the one `test_ltx2_video`'s own walk produced. Both rows recorded this hazard in advance and both were correct to. `docs/FEATURES.md` was reconciled BY KEY against `origin/main` rather than accepted from the automatic merge: 194 keys, 192 byte-identical to main, one added (the DFR base row) and one changed. The changed row's only difference is the six-character `GENkf/` removal in its fourth cell; its third cell, which carries `Temporal x2 ups gated, UNDRIVEN` and which `.agents/specs/ltx25-resolution-envelope.md:436` asserts is untouched, is byte-identical to main at 218 characters. `docs/USAGE.md` is exactly main plus this row's own hunks — the diff against `origin/main` and the diff across this row's base are equal line for line. `.agents/issue-index.md` union-merged to 253 rows: two added, zero removed, zero duplicate issue ids. `check-env-doc` and `test_check_env_doc` are red, and they are #995, not this merge. The only `VT_MOE_EXPERT` line this branch adds anywhere is the #995 index row itself, and the branch touches none of the checker's four inputs. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
localai-bot
pushed a commit
that referenced
this pull request
Aug 16, 2026
Pinned SHA, per #841. main moved four commits while this row was measuring and re-landing: #993, #982, #988 and #992. One of them is directly relevant -- #988 split #810's A2-Q into A2-Q1 and A2-Q2, and A2-Q1 is the unit this row exists to unblock -- and none of them touch the fp8 quant registration, the CUDA source list, or either checker this branch edits. `.agents/issue-index.md` is the only keyed record both sides wrote. Verified afterwards that origin/main's copy is a strict PREFIX of the merged file and that only this branch's own two rows follow it, which is what append-only means and what a clean-but-wrong interleave would break. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
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.
RetakePipelinekeeps the source clip outside[start_time, end_time)and regenerates what is inside it from the prompt (Lightricks/LTX-2packages/ltx-pipelines/src/ltx_pipelines/retake.py:53,151-329@fd4ded7f2d88d3da713abcdd4ad41ecc4a9314ca, verified withgit rev-parse HEADon a clean local checkout before any anchor here was taken). It is reachable fromvllm_video_generateand fromltx2-genthrough--pipeline-kind retake,--ref-videoand three window flags.Issue #924. Spec
.agents/specs/ltx25-retake.md, committed before any implementation. Also closes #987, filed and fixed in this flow.The source is a frame directory, and that is upstream's own second arm
Upstream opens a container with PyAV (
media_io/decode.py:226) and no demuxer is vendored here —src/vllm/entrypoints/openai/video_api.cpp:115-121says so where it defines the reference-video field — so a.mp4is refused by name. Upstream also carries a folder arm for exactly the no-container case, and three consequences follow from its own lines rather than from a local simplification:decode.py:213-215);utils/helpers.py:261-262returnsNonebefore opening anything);initial_audio_latent is not None(retake.py:279,282), soregenerate_audiohas no observable effect on this arm and the soundtrack is generated fresh.The last one is surprising. It is upstream's, it is mirrored rather than repaired, and
test_ltx2_retakepins it so the next reader finds it asserted instead of re-deriving it.What the port had to get right
TemporalRegionMask(ltx-core/conditioning/types/noise_mask_cond.py:9-45) lands as a replacement of the all-ones denoise maskcreate_initial_statewrites (ltx-core/tools.py:158-161), which is whatstate.denoise_mask.copy_does at:44. Nothing else in the phase loop changes, and that is the design rather than a coincidence: every consumer of that mask already broadcasts it, so seedingvideo_initialwith the encoded clip and zeroing the mask outside the window is the whole mechanism.video.clean = video.latentis what makes the blend restore the source rather than zeros (tools.py:156).Four things in it fail silently, and each has a case:
causal_fixdefaults the other way at the call site.get_pixel_coordsdeclaresFalse(patchifiers.py:140);TemporalRegionMaskcalls it withgetattr(latent_tools, "causal_fix", True)(noise_mask_cond.py:33). Reading the function's default moves every video boundary bytime - 1= seven pixel frames and still renders.get_pixel_coordsand divided by fps (:31-35); audio bounds are already seconds (:27-29, viapatchifiers.py:216-249) and must not be scaled again.:39), so a token straddling either edge is inside. Containment drops the boundary tokens and leaves a seam at each end.:44).Ltx2ConformLatentLengthtruncates or zero-pads (utils/helpers.py:149-162) and is deliberately a second function rather than a flag on the audio-to-video one, which truncates only (a2vid_two_stage.py:202) and where a short take is an error becausecreate_initial_stateasserts the shape (ltx-core/tools.py:146-148). Both polarities are upstream's, on two callers; one shared helper would have to pick one and be wrong for the other.The
retakerecipe is its own row inResolveLtx2PipelineReciperather thandistilled_two_stagewith a phase removed: that recipe renders stage 1 atspatial_downscale = 2, and a full-resolution source latent does not fit that grid, so a retake on it is refused by name. One phase,DISTILLED_SIGMAS(retake.py:287,distilleddefaultsTrueat:85and the CLI hard-codes it at:359), plain Euler (DiffusionStage.__call__defaults toEulerDiffusionStep(),utils/blocks.py:526-527, and retake overrides neither) and no negative prompt (SimpleDenoiserat:290-294,[prompt]alone at:259).Arms
Ltx2VideoEngine::Loadalready resolves serves it unchangedassert_never-closed atltx-pipelines/utils/quantization_factory.py:23-26(assert_neverat:50) —fp8-cast,fp8-scaled-mm,nvfp4-cast,nvfp4-prequant. There is no GGUF arm to port.mp4, …).agents/specs/ltx25-retire-dead-arms.md:167retakeon a(kind, version)pair the table does not carryaudio_pathretake.py:250-256)8k+1grid or off the 32-pixel gridretake.py:347-353)Two stale refusal sentences repaired in the same flow (#987)
ltx2_video.cppclaimed "nothing readsref_video_dirat all". That was false about the tree when it was written: MiniMax-H3 consumes the directory in full (ReadReferenceClipChw,src/vllm/multimodal/minimax_h3_video.cpp:135, called at:650), and #975 inherited the wider claim from this message. It is doubly false now. It also claimed "there is no AUDIO_VAE_ENCODER key filter", whichc2019b0e3landed.Both are rewritten in the
WHAT IS *NOT* THE REASONshape so the ruled-out cause stays visible, and the guarding case asserts the local fact that the LTX side now reads the directory — a case asserting only upstream symbol names is exactly how the first sentence survived.The reference-conditioning arm itself stays refused and #975 stays open. The
!wants_retakeguard narrows the refusal rather than weakening it: retake encodes the clip at its own resolution and seeds the initial latent, while the reference item is downscaled by the adapter's factor, temporally subsampled and appended as extra tokens to a stage-1-only adapter (iclora_utils.py:112-117,87-89,144-148). Serving the first says nothing about the second, and mutation M8 (dropping the guard) is red.Upstream ships no tests
find /home/mudler/_git/LTX-2 -name 'test_*.py'returns 0 lines at this pin, and so doesfind . -iname '*test*' -not -path './.git/*'. "Port the upstream tests in the same change" therefore has nothing to port, and the obligation became pinning upstream's behaviours againstfile:lineanchors, plus one assertion tied to a local fact so the suite can see a refusal going stale. This is stated rather than quietly skipped, because a reader who sees no ported test file should be able to tell "upstream had none" from "the porter did not look".Reachability
Production entry point.
vllm_video_generate(include/vllm.h:969) →src/capi/vllm_c.cpp→Ltx2VideoEngine::Generate, where the knobs are parsed, the clip read throughLtx2ReadFrameDirectory, encoded, conformed, and the mask assigned intovideo.maskin the phase loop. The clip ridesvllm_video_params::ref_video(include/vllm.h:912), an ABI field that already existed and that this engine previously only tested for emptiness — no struct grows a field, so sibling rows editing that header concurrently are unaffected.ltx2-genreaches the same path on its default configuration.Test entry. The five
test_ltx2_videocases start atLoadVideoEngineandGenerate, not atLtx2TemporalRegionMaskVideo. M1 deletes the production call site: that suite goes RED (exit 1) whiletest_ltx2_retakestays green (exit 0), which is the split.agents/reachability.mdpredicts and the reason the unit suite is not the proof./v1/videosforwards no engine extras today (#928), so the CLI and the C ABI are what this row claims and the HTTP surface is not.Mutations — three facts each
Restores verified by
sha256sumof both product files, not assumed. Exit codes captured directly, never through a pipe. Filters are comma-free and the case count is printed beside every exit code, so a filter that matched nothing cannot read as a pass.git diff --statltx2_video.cpp | 2 --compile_err=0video_initialwith the encoded clipltx2_video.cpp | 2 +-compile_err=0get_pixel_coords' owncausal_fix=falseltx2_video.cpp | 2 +-compile_err=0ltx2_video.cpp | 2 +-compile_err=0ltx2_retake.cpp | 1 +compile_err=0ltx2_retake.cpp | 2 +-compile_err=0initial_audio_latent is not Noneltx2_retake.cpp | 4 ++--compile_err=1, then YEScompile_err=0!wants_retakeguardltx2_video.cpp | 2 +-compile_err=08k+1refusal rounds the snapped value UPltx2_retake.cpp | 2 +-compile_err=0ltx2_retake.cpp | 4 ++++compile_err=0M2 and M3 survived on the first run. Both were claims made in a comment and observed by nothing. M2 is the sharper: the suite recorded
retake_latent_absmax, which observes the encode, and inferred the latent reached the phase — a build that reads the clip, encodes it, records the digest and starts from zeros passed everything and rendered a clip of the right length with the right mask. The repair renders two different sources at the same seed and window and requires the pixels to differ. M3's unit case was already red under the flip, but it calls the function directly and says nothing about what the production call site passes; the end-to-end window [0.05, 0.10) selects the same latent frame under both polarities, so a second window [0.30, 0.40) was added where they disagree. M7 failed to build (has_audio_latentunused under-Werror) and is recorded as establishing nothing, then re-run with the parameter referenced.Merge hazard
ltx2_video.cpp:363-364carries theREADER ANCHORSlist, derived and string-compared bytest_ltx2_video. This branch shifted it by one (779 …→780 …) and re-derived it at the final tree. A cleangit mergewill not warn if another branch shifts it again — re-derive after any merge.FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]