Files
hao-ai-lab-FastVideo/fastvideo/tests/api/test_parser.py
T
Boyang ZHONG dc72a35f4e [bugfix] Return individual images for batched image API requests (#1917)
For n > 1, the image endpoints returned one preview grid instead of n images.
The endpoints now run one model batch, save each decoded sample as its own file,
and return one URL or base64 item for each image. This applies to generations
and edits.

A keyword-only return_samples field (default False) is added to OutputConfig,
SamplingParam and ForwardBatch. Batched image requests set it and do not build
preview frames. The worker keeps the sample tensor for these requests, and
non-output ranks clear the field. Defaults and positional arguments do not
change.
2026-10-07 15:06:02 -07:00

394 lines
12 KiB
Python

# SPDX-License-Identifier: Apache-2.0
import json
from pathlib import Path
import yaml
from fastvideo.api import (
config_to_dict,
ContinuationState,
GenerationRequest,
GeneratorConfig,
GpuPoolConfig,
load_run_config,
load_serve_config,
parse_config,
PlannedStage,
PromptEnhancerConfig,
PromptSafetyConfig,
RunConfig,
ServeConfig,
StreamingConfig,
WarmupConfig,
)
def test_parse_config_builds_nested_typed_config() -> None:
raw = {
"generator": {
"model_path": "/models/ltx2",
"pipeline": {
"workload_type": "t2v",
"preset": "ltx2_two_stage",
},
},
"request": {
"prompt": ["a fox", "a wolf"],
"sampling": {
"num_frames": 121,
"height": 1024,
"width": 1536,
"guidance_scale": 1.5,
},
"state": {
"kind": "ltx2_continuation",
"payload": {
"segment_index": 1
},
},
"plan": {
"stages": [{
"name": "base",
"kind": "sample",
}]
},
},
}
config = parse_config(RunConfig, raw)
assert config.generator.pipeline.preset == "ltx2_two_stage"
assert config.request.prompt == ["a fox", "a wolf"]
assert config.request.state == ContinuationState(
kind="ltx2_continuation",
payload={"segment_index": 1},
)
assert config.request.plan is not None
assert config.request.plan.stages == [PlannedStage(name="base", kind="sample")]
def test_parse_config_accepts_existing_typed_instance() -> None:
typed = RunConfig(
generator=GeneratorConfig(model_path="/models/base"),
request=GenerationRequest(prompt="hello"),
)
assert parse_config(RunConfig, typed) is typed
def test_parse_config_accepts_external_launcher_backend() -> None:
config = parse_config(
GeneratorConfig,
{
"model_path": "/models/minimax-h3",
"engine": {
"num_gpus": 8,
"execution_backend": "external_launcher",
"parallelism": {
"sp_size": 8
},
},
},
)
assert config.engine.execution_backend == "external_launcher"
assert config.engine.num_gpus == 8
assert config.engine.parallelism.sp_size == 8
def test_external_launcher_example_is_a_valid_run_config() -> None:
repo_root = Path(__file__).resolve().parents[3]
config = load_run_config(repo_root / "examples/inference/basic/minimax_h3_external_launcher.yaml")
assert config.generator.engine.execution_backend == "external_launcher"
assert config.generator.engine.num_gpus == 4
assert config.generator.engine.parallelism.sp_size == 4
assert config.generator.engine.parallelism.dist_timeout == 600
assert config.request.output.return_frames is False
def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
raw = {
"generator": {
"model_path": "/models/wan"
},
"request": {
"prompt": "hello",
"sampling": {
"num_frames": 16
},
},
}
path = tmp_path / "run.yaml"
path.write_text(yaml.safe_dump(raw), encoding="utf-8")
loaded = load_run_config(path)
assert config_to_dict(loaded) == {
"generator": {
"model_path": "/models/wan",
"revision": None,
"trust_remote_code": False,
"engine": {
"num_gpus": 1,
"execution_backend": "mp",
"parallelism": {
"tp_size": -1,
"sp_size": -1,
"hsdp_replicate_dim": 1,
"hsdp_shard_dim": -1,
"dist_timeout": None,
},
"offload": {
"dit": True,
"dit_layerwise": True,
"text_encoder": True,
"image_encoder": True,
"vae": True,
"pin_cpu_memory": True,
"lazy_module_load": None,
},
"compile": {
"enabled": False,
"backend": None,
"fullgraph": None,
"mode": None,
"dynamic": None,
"extras": {},
"text_encoder_enabled": None,
"vae_enabled": None,
"audio_vae_enabled": None,
"dit_kwargs": {},
"text_encoder_kwargs": {},
"vae_kwargs": {},
"audio_vae_kwargs": {},
},
"enable_stage_verification": True,
"use_fsdp_inference": False,
"disable_autocast": False,
"quantization": None,
},
"pipeline": {
"workload_type": None,
"preset": None,
"preset_version": None,
"components": {
"config_root": None,
"pipeline_config_path": None,
"text_encoder_weights": None,
"transformer_weights": None,
"transformer_2_weights": None,
"vae_weights": None,
"upsampler_weights": None,
"lora_path": None,
"lora_nickname": "default",
"lora_strength": 1.0,
"override_pipeline_cls_name": None,
"override_transformer_cls_name": None,
},
"vae_tiling": None,
"preset_overrides": {},
"experimental": {},
},
},
"request": {
"prompt": "hello",
"negative_prompt": None,
"inputs": {
"prompt_path": None,
"image_path": None,
"video_path": None,
"pose_video_path": None,
"face_video_path": None,
"background_video_path": None,
"mask_video_path": None,
"animate_mode": None,
"audio_path": None,
"pil_image": None,
"last_image": None,
"references": None,
"latents": None,
"audio_latents": None,
"pose": None,
"mouse_cond": None,
"keyboard_cond": None,
"grid_sizes": None,
"c2ws_plucker_emb": None,
"action_path": None,
"refine_from": None,
"stage1_video": None,
},
"sampling": {
"num_videos_per_prompt": 1,
"seed": 1024,
"max_sequence_length": None,
"num_frames": 16,
"height": 720,
"width": 1280,
"height_sr": 1072,
"width_sr": 1920,
"fps": 24,
"num_inference_steps": 50,
"num_inference_steps_sr": 50,
"guidance_scale": 1.0,
"batch_cfg": False,
"guidance_scale_2": None,
"cfg_normalization": False,
"cfg_truncation": 1.0,
"guidance_rescale": 0.0,
"true_cfg_scale": None,
"use_embedded_guidance": None,
"boundary_ratio": None,
"sigmas": None,
"pyramid_num_inference_steps_list": None,
"history_sizes": None,
"num_latent_frames_per_chunk": 9,
"keep_first_frame": True,
"is_skip_first_chunk": False,
"use_zero_init": True,
"zero_steps": 1,
"is_amplify_first_chunk": False,
},
"runtime": {
"enable_teacache": False,
"return_trajectory_latents": False,
"return_trajectory_decoded": False,
},
"output": {
"output_path": "outputs/",
"output_video_name": None,
"save_video": True,
"return_frames": True,
"return_state": False,
"return_samples": False,
},
"stage_overrides": {},
"state": None,
"plan": None,
"extensions": {},
},
}
def test_load_serve_config_supports_json_roundtrip(tmp_path) -> None:
raw = {
"generator": {
"model_path": "/models/server"
},
"server": {
"port": 9000
},
"default_request": {
"prompt": "serve default"
},
}
path = tmp_path / "serve.json"
path.write_text(json.dumps(raw), encoding="utf-8")
loaded = load_serve_config(path)
assert isinstance(loaded, ServeConfig)
assert loaded.server.port == 9000
assert loaded.default_request.prompt == "serve default"
def test_serve_config_streaming_defaults_to_none() -> None:
raw = {"generator": {"model_path": "/models/server"}}
loaded = parse_config(ServeConfig, raw)
assert loaded.streaming is None
def test_serve_config_parses_streaming_block() -> None:
raw = {
"generator": {
"model_path": "/models/server"
},
"streaming": {
"session_timeout_seconds": 120,
"generation_segment_cap": 4,
"stream_mode": "legacy_jpeg",
"warmup": {
"enabled": False,
"prompt": "warmup prompt",
"timeout_seconds": 600,
},
"pool": {
"num_workers": 2,
"enable_audio_reencode": False,
"conditioning_num_frames": 5,
"conditioning_end_offset": 1,
},
"prompt": {
"provider": "groq",
"model": "llama-3-70b",
"timeout_ms": 10000,
"system_prompt_dir": "/opt/prompts",
},
"safety": {
"enabled": True,
"classifier_path": "/opt/safety.pt",
},
},
}
loaded = parse_config(ServeConfig, raw)
assert isinstance(loaded.streaming, StreamingConfig)
assert loaded.streaming.session_timeout_seconds == 120
assert loaded.streaming.generation_segment_cap == 4
assert loaded.streaming.stream_mode == "legacy_jpeg"
assert loaded.streaming.warmup == WarmupConfig(enabled=False, prompt="warmup prompt", timeout_seconds=600)
assert loaded.streaming.pool == GpuPoolConfig(
num_workers=2,
enable_audio_reencode=False,
conditioning_num_frames=5,
conditioning_end_offset=1,
)
assert loaded.streaming.prompt == PromptEnhancerConfig(
provider="groq",
model="llama-3-70b",
timeout_ms=10000,
system_prompt_dir="/opt/prompts",
)
assert loaded.streaming.safety == PromptSafetyConfig(enabled=True, classifier_path="/opt/safety.pt")
def test_serve_config_streaming_round_trip_through_config_to_dict() -> None:
raw = {
"generator": {
"model_path": "/models/server"
},
"streaming": {
"session_timeout_seconds": 600
},
}
loaded = parse_config(ServeConfig, raw)
dumped = config_to_dict(loaded)
assert dumped["streaming"]["session_timeout_seconds"] == 600
assert dumped["streaming"]["warmup"]["enabled"] is True
assert dumped["streaming"]["prompt"]["enabled"] is False
assert dumped["streaming"]["prompt"]["provider"] == "cerebras"
assert dumped["streaming"]["safety"]["enabled"] is False
def test_load_serve_config_with_streaming_from_yaml(tmp_path) -> None:
raw = {
"generator": {
"model_path": "/models/server"
},
"streaming": {
"stream_mode": "av_fmp4",
"pool": {
"num_workers": 4
},
},
}
path = tmp_path / "serve.yaml"
path.write_text(yaml.safe_dump(raw), encoding="utf-8")
loaded = load_serve_config(path)
assert loaded.streaming is not None
assert loaded.streaming.stream_mode == "av_fmp4"
assert loaded.streaming.pool.num_workers == 4