From 279e52ad8dce9fae73fcb992befda1242ea5344d Mon Sep 17 00:00:00 2001 From: SolitaryThinker Date: Fri, 17 Apr 2026 15:43:05 -0700 Subject: [PATCH] [feat] [6/n] Improve API: add ltx2_two_stage preset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Land the ltx2_two_stage inference preset (PR 6 commit 1/5) that exposes the public LTX-2 two-stage distilled flow: half-resolution denoise followed by 2x spatial upsample + stage-2 refine denoise (3 steps with the official distilled sigma schedule, or 2 steps with the reduced variant). Schema: - new _REFINE_STAGE PresetStageSpec with kind="refinement" and allowed per-request overrides {num_inference_steps, guidance_scale, image_crf, video_position_offset_sec} - new LTX2_TWO_STAGE preset with stage_schemas=(denoise, refine) and stage_defaults.refine={num_inference_steps: 2, guidance_scale: 1.0} matching the load_kwargs the internal ltx2-streaming gpu_pool uses - LTX2_TWO_STAGE registered via ALL_PRESETS -> _register_presets() Init-time refine wiring (enabled, add_noise, upsampler/LoRA/transformer paths) flows through generator.pipeline.preset_overrides.refine.* and generator.pipeline.components.{upsampler_weights, lora_path} in a follow-up commit — the refine stage's internal implementation (fastvideo/pipelines/stages/ltx2_refine.py in FastVideo-internal) already treats those as FastVideoArgs fields; this commit only ships the public preset surface. 5 new tests under fastvideo/tests/api/test_presets.py::TestLtx2Presets: registration count (3 presets now), two-stage topology, stage defaults, valid per-request refine override keys, and unknown-override rejection. --- fastvideo/pipelines/basic/ltx2/presets.py | 46 ++++++++++++++++++++++- fastvideo/tests/api/test_presets.py | 39 ++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/fastvideo/pipelines/basic/ltx2/presets.py b/fastvideo/pipelines/basic/ltx2/presets.py index 2ea8ffe6d..e7af7dc0f 100644 --- a/fastvideo/pipelines/basic/ltx2/presets.py +++ b/fastvideo/pipelines/basic/ltx2/presets.py @@ -30,6 +30,18 @@ _DENOISE_STAGE = PresetStageSpec( }), ) +_REFINE_STAGE = PresetStageSpec( + name="refine", + kind="refinement", + description="Latent-upsample + second-pass refine", + allowed_overrides=frozenset({ + "num_inference_steps", + "guidance_scale", + "image_crf", + "video_position_offset_sec", + }), +) + LTX2_BASE = InferencePreset( name="ltx2_base", version=1, @@ -77,4 +89,36 @@ LTX2_DISTILLED = InferencePreset( }, ) -ALL_PRESETS = (LTX2_BASE, LTX2_DISTILLED) +# Two-stage distilled flow: half-res denoise (stage 1) followed by +# spatial-upsample + second denoise (stage 2 "refine"). Stage-2 +# sigmas are fixed in fastvideo.pipelines.stages.ltx2_refine — only +# num_inference_steps (2 or 3) and guidance_scale are user-tunable. +# Refine wiring (upsampler weights, optional LoRA, enable/add-noise) +# lives on generator.pipeline.preset_overrides.refine at init-time; +# per-request tuning goes through request.stage_overrides.refine. +LTX2_TWO_STAGE = InferencePreset( + name="ltx2_two_stage", + version=1, + model_family="ltx2", + description="LTX-2 distilled with 2x spatial refine (stage 1 half-res + stage 2 upsample+denoise)", + workload_type="t2v", + stage_schemas=(_DENOISE_STAGE, _REFINE_STAGE), + defaults={ + "seed": 10, + "height": 1024, + "width": 1536, + "num_frames": 121, + "fps": 24, + "guidance_scale": 1.0, + "num_inference_steps": 8, + "negative_prompt": "", + }, + stage_defaults={ + "refine": { + "num_inference_steps": 2, + "guidance_scale": 1.0, + }, + }, +) + +ALL_PRESETS = (LTX2_BASE, LTX2_DISTILLED, LTX2_TWO_STAGE) diff --git a/fastvideo/tests/api/test_presets.py b/fastvideo/tests/api/test_presets.py index cfe76063c..dbb8eb8b6 100644 --- a/fastvideo/tests/api/test_presets.py +++ b/fastvideo/tests/api/test_presets.py @@ -334,7 +334,7 @@ class TestLtx2Presets: import fastvideo.registry # noqa: F401 presets = get_presets_for_family("ltx2") names = {p.name for p in presets} - assert names == {"ltx2_base", "ltx2_distilled"} + assert names == {"ltx2_base", "ltx2_distilled", "ltx2_two_stage"} def test_ltx2_base_lookup(self) -> None: import fastvideo.registry # noqa: F401 @@ -349,6 +349,43 @@ class TestLtx2Presets: assert p.defaults["num_inference_steps"] == 8 assert p.defaults["guidance_scale"] == 1.0 + def test_ltx2_two_stage_is_two_stage(self) -> None: + import fastvideo.registry # noqa: F401 + p = get_preset("ltx2_two_stage", "ltx2") + assert len(p.stage_schemas) == 2 + assert p.stage_schemas[0].name == "denoise" + assert p.stage_schemas[1].name == "refine" + assert p.stage_schemas[1].kind == "refinement" + + def test_ltx2_two_stage_stage_defaults(self) -> None: + import fastvideo.registry # noqa: F401 + p = get_preset("ltx2_two_stage", "ltx2") + refine = p.stage_defaults["refine"] + # stage-2 refine only supports 2 or 3 denoising steps; preset + # defaults to 2 (matches gpu_pool.py load_kwargs). + assert refine["num_inference_steps"] == 2 + assert refine["guidance_scale"] == 1.0 + + def test_ltx2_two_stage_refine_overrides_valid(self) -> None: + import fastvideo.registry # noqa: F401 + p = get_preset("ltx2_two_stage", "ltx2") + validate_stage_overrides( + p, {"refine": {"num_inference_steps": 3}}) + validate_stage_overrides( + p, {"refine": {"guidance_scale": 1.0}}) + validate_stage_overrides( + p, {"refine": {"image_crf": 18}}) + validate_stage_overrides( + p, {"refine": {"video_position_offset_sec": 2.5}}) + + def test_ltx2_two_stage_rejects_unknown_refine_override(self) -> None: + import fastvideo.registry # noqa: F401 + from fastvideo.api.errors import ConfigValidationError + p = get_preset("ltx2_two_stage", "ltx2") + with pytest.raises(ConfigValidationError): + validate_stage_overrides( + p, {"refine": {"bogus_field": 1}}) + # ------------------------------------------------------------------- # Hunyuan preset integration