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