Compare commits

..
Author SHA1 Message Date
SolitaryThinkerandClaude Opus 4.6 300eab2b8d [feat] Migrate Hunyuan SamplingParam subclasses to profile-based defaults
Replace HunyuanSamplingParam and FastHunyuanSamplingParam subclasses
with profile-based defaults. This adds the profile infrastructure
(ConfigInfo.default_profile, SamplingParam._from_profile) and the
first profile definitions for Hunyuan models, enabling future model
families to migrate from subclass overrides to declarative profiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 16:33:37 -07:00
119 changed files with 1293 additions and 4494 deletions
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env bash
# Sync .agents/skills/ into .claude/skills/ via per-skill symlinks.
#
# Why: Claude Code only scans .claude/skills/ and ~/.claude/skills/ for
# user-invocable skills (no skillsPath config exists — see
# https://code.claude.com/docs/en/skills.md). This repo's skills live
# in .agents/skills/ so they travel with the repo and stay under git.
# Run this once after cloning (or after adding/removing a skill) to
# expose them to Claude Code without maintaining a parallel tree.
#
# Usage:
# .agents/scripts/sync-skills.sh
#
# Idempotent and safe to re-run. Prunes stale symlinks whose source
# has been removed from .agents/skills/. Leaves hand-written
# .claude/skills/<name>/ directories untouched (only symlinks are
# managed).
set -euo pipefail
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
SRC_DIR="$REPO_ROOT/.agents/skills"
DST_DIR="$REPO_ROOT/.claude/skills"
if [[ ! -d "$SRC_DIR" ]]; then
echo "Error: $SRC_DIR does not exist." >&2
exit 1
fi
mkdir -p "$DST_DIR"
linked=0
unchanged=0
skipped=0
pruned=0
link_skill() {
local name="$1"
local src="$SRC_DIR/$name"
local dst="$DST_DIR/$name"
# Relative target keeps symlinks portable across clones.
local rel="../../.agents/skills/$name"
if [[ -L "$dst" ]]; then
if [[ "$(readlink "$dst")" == "$rel" ]]; then
unchanged=$((unchanged + 1))
return
fi
rm "$dst"
elif [[ -e "$dst" ]]; then
echo "Skipped (not a symlink): .claude/skills/$name" >&2
skipped=$((skipped + 1))
return
fi
ln -s "$rel" "$dst"
echo "Linked: .claude/skills/$name -> $rel"
linked=$((linked + 1))
}
prune_stale() {
local link="$1"
local target
target="$(readlink "$link")"
case "$target" in
../../.agents/skills/*) ;;
*) return ;;
esac
local name="${target##*/}"
if [[ ! -d "$SRC_DIR/$name" ]]; then
rm "$link"
echo "Pruned stale: .claude/skills/$(basename "$link")"
pruned=$((pruned + 1))
fi
}
for src in "$SRC_DIR"/*/; do
[[ -d "$src" ]] || continue
name="$(basename "$src")"
# Only treat directories that actually contain a SKILL.md as skills.
[[ -f "$src/SKILL.md" ]] || continue
link_skill "$name"
done
shopt -s nullglob
for link in "$DST_DIR"/*; do
[[ -L "$link" ]] || continue
prune_stale "$link"
done
shopt -u nullglob
printf "\nSummary: %d linked, %d unchanged, %d pruned" "$linked" "$unchanged" "$pruned"
if [[ "$skipped" -gt 0 ]]; then
printf ", %d skipped (non-symlink collision)" "$skipped"
fi
printf "\n"
-1
View File
@@ -5,4 +5,3 @@
{"name": "evaluate-video-quality", "description": "Evaluate generated video quality using available metrics (SSIM, loss trajectory, caption consistency)", "path": "evaluate-video-quality/SKILL.md", "status": "draft", "trust": "low"}
{"name": "index-related-work", "description": "Ingest a paper or repository into the related work index", "path": "index-related-work/SKILL.md", "status": "draft", "trust": "low"}
{"name": "search-related-work", "description": "Query the related work index for relevant papers, repos, or comparisons", "path": "search-related-work/SKILL.md", "status": "draft", "trust": "low"}
{"name": "seed-ssim-references", "description": "Run a new or updated fastvideo/tests/ssim/ test on Modal, pull generated videos, and upload them to FastVideo/ssim-reference-videos so the test has a regression baseline", "path": "seed-ssim-references/SKILL.md", "status": "draft", "trust": "low"}
@@ -1,250 +0,0 @@
---
name: seed-ssim-references
description: Seed HF reference videos for a single newly-added SSIM test. Runs the test on Modal L40S, downloads the generated mp4s via `modal volume get`, pauses for the user to eyeball quality, then uploads only that test's files to `FastVideo/ssim-reference-videos`. Use when a new `fastvideo/tests/ssim/test_*_similarity.py` has just been added and has no references on HF yet.
---
# Seed SSIM Reference Videos
## Purpose
A brand-new SSIM test in `fastvideo/tests/ssim/` fails forever until its
reference videos exist on the HF dataset (`FastVideo/ssim-reference-videos`).
This skill:
1. Runs the test on Modal's L40S pool to generate the videos.
2. Downloads them to the local repo via `modal volume get`.
3. Pauses so the user can eyeball the mp4s and confirm quality.
4. Uploads only the new test's files to HF, with a guard that refuses to
overwrite anything already present.
The skill is run **manually**, once per new test. Before invoking it, the user
has already sanity-tested the new test locally — it launches `VideoGenerator`
and writes an mp4 without crashing. The skill does not re-test locally; it
goes straight to Modal L40S (which is what CI uses).
## When to use
- A new `test_*_similarity.py` file has been added in `fastvideo/tests/ssim/`
and the HF dataset has no `reference_videos/default/L40S_reference_videos/<model_id>/`
subtree for it yet.
## When not to use
- Regular CI runs — once refs exist, `pytest fastvideo/tests/ssim/` downloads
them automatically.
- Re-seeding an existing test. That requires `--force` on the upload step, and
is out of scope here; treat as a separate, deliberate operation.
## Inputs
The skill has **one required input**: the path to the new SSIM test file.
Prompt the user for it if they didn't supply it.
| Parameter | Required | Description |
|-----------|----------|-------------|
| `test_file` | Yes | e.g. `fastvideo/tests/ssim/test_ltx2_similarity.py`. The skill's first action is to ask for this if missing. |
Everything else is fixed:
- Modal runner GPU: **L40S** (hardcoded in `fastvideo/tests/modal/ssim_test.py`).
- Device folder: `L40S_reference_videos`.
- Quality tier: `default` (the tier CI runs). The `full_quality` tier is not
seeded by this skill.
- HF repo: `FastVideo/ssim-reference-videos` (dataset).
- Multi-model test files: all model ids in `*_MODEL_TO_PARAMS` are seeded
together; the Modal run produces one mp4 per (model, prompt, backend) and
the upload scopes by `--model-id`, looping if there is more than one.
## Prerequisites
The user has confirmed:
- `modal` CLI authenticated.
- `HF_API_KEY` (or `HUGGINGFACE_HUB_TOKEN` / `HF_TOKEN`) exported with write
access to `FastVideo/ssim-reference-videos`.
- The test file runs locally end-to-end (generates an mp4; SSIM assertion
failure due to missing reference is expected and fine).
Fail fast if the token env var is missing.
## Steps
### 1. Ask for the test file
If the user didn't name one, ask: *"Which SSIM test file do you want to seed
references for? (e.g. `fastvideo/tests/ssim/test_ltx2_similarity.py`)"*.
Validate:
- Path exists and matches `fastvideo/tests/ssim/test_*_similarity.py`.
- File defines a `*_MODEL_TO_PARAMS` dict — grep it to extract the set of
model ids. Those ids drive step 5.
If either check fails, stop and tell the user what's wrong.
### 2. Run the test on Modal L40S
Pick a subdir name so repeated runs don't collide:
```bash
SHORT_COMMIT=$(git rev-parse --short=12 HEAD)
TIMESTAMP=$(date -u +%Y%m%d_%H%M%S)
SUBDIR="${TIMESTAMP}_${SHORT_COMMIT}"
```
Then launch the Modal run:
```bash
modal run fastvideo/tests/modal/ssim_test.py \
--git-repo="$(git config --get remote.origin.url)" \
--git-commit="$(git rev-parse HEAD)" \
--hf-api-key="$HF_API_KEY" \
--test-files="<test_file>" \
--sync-generated-to-volume \
--generated-volume-subdir="$SUBDIR" \
--skip-reference-download \
--no-fail-fast
```
Flag rationale:
- `--skip-reference-download`: no refs exist yet, so conftest must not try to
pull them.
- `--no-fail-fast`: lets the test finish generation before `_assert_similarity`
raises `FileNotFoundError: Reference video folder does not exist`. The
expected failure is what we want — the mp4 has already been written.
- `--sync-generated-to-volume` + `--generated-volume-subdir`: copies the
generated mp4s to the `hf-model-weights` Modal volume under
`ssim_generated_videos/default/<SUBDIR>/generated_videos/` so we can pull
them locally.
The Modal run will end with a nonzero exit (expected) and print a
`modal volume get hf-model-weights ssim_generated_videos/default/<SUBDIR>/generated_videos ./generated_videos_modal/default`
command. Capture that `<SUBDIR>` — you need it for step 3.
### 3. Download generated videos locally
```bash
modal volume get --force hf-model-weights \
ssim_generated_videos/default/"$SUBDIR"/generated_videos \
./generated_videos_modal/default
```
`--force` is required when the parent `./generated_videos_modal/default`
already exists; without it, `modal volume get` errors with `[Errno 21] Is a
directory`. Safe to pass on the first run too.
After this, the mp4s live at
`./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4`.
The extra `generated_videos/` level comes from the volume layout in
`_sync_generated_videos_to_volume` (`ssim_test.py`) — the command copies
`<repo>/fastvideo/tests/ssim/generated_videos/<tier>` to
`ssim_generated_videos/<tier>/<SUBDIR>/generated_videos/`, and `modal volume
get` preserves that trailing `generated_videos/` segment.
### 4. PAUSE — user reviews quality
Print the list of downloaded mp4s and their paths, then stop. Tell the user:
> "Generated videos downloaded to `./generated_videos_modal/default/generated_videos/L40S_reference_videos/`. Please open them and confirm the quality looks correct. Reply **`upload`** to continue, or anything else to abort."
Do not proceed until the user explicitly says `upload`. If they abort, leave
everything on disk so they can inspect further — no cleanup.
### 5. Copy into the local reference layout
Scoped copy — only the new test's mp4s. Loop over each `<model_id>` extracted
in step 1:
```bash
python fastvideo/tests/ssim/reference_videos_cli.py copy-local \
--quality-tier default \
--device-folder L40S_reference_videos \
--generated-dir ./generated_videos_modal/default/generated_videos/L40S_reference_videos
```
(The `--generated-dir` points at the device-folder root inside the
downloaded tree; `copy-local` walks all `<model>/<backend>/*.mp4`
underneath it. Since the Modal run was scoped to a single test file via
`--test-files`, only that test's model(s) are present — so the copy is
implicitly per-test.)
Result: `fastvideo/tests/ssim/reference_videos/default/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4`.
### 6. Upload to HF — scoped per model_id, with overwrite guard
For each `<model_id>`:
```bash
python fastvideo/tests/ssim/reference_videos_cli.py upload \
--quality-tier default \
--device-folder L40S_reference_videos \
--model-id "<model_id>"
```
The upload command:
- Uploads **only** `reference_videos/default/L40S_reference_videos/<model_id>/`.
- **Refuses** if any file already exists at that path on HF (this is the
guard — seeding a new test should never clobber existing refs). To override,
the user must re-run with `--force`. If the guard fires, stop and report
exactly which files exist; do not silently `--force`.
Reads the HF token from `HF_API_KEY` / `HUGGINGFACE_HUB_TOKEN` / `HF_TOKEN`.
### 7. Report success
List what was uploaded (paths in repo) and remind the user to push any
related code changes. Do **not** auto-verify by re-running Modal — the user
can run `pytest fastvideo/tests/ssim/<test_file>` later to confirm end-to-end;
it will auto-download the refs they just uploaded.
## Failure modes and how to handle them
- **`HF_API_KEY` unset.** Stop before step 2. The Modal run needs it (passed
via `--hf-api-key`), and step 6 needs it for upload.
- **Modal run fails before generation.** No mp4s on the volume — nothing to
download. Fix the test locally (`pytest fastvideo/tests/ssim/<test_file>`)
and retry from step 2.
- **`./generated_videos_modal/default/L40S_reference_videos/` missing after
`modal volume get`.** The run didn't produce videos (most likely the test
crashed before writing, or `REQUIRED_GPUS` exceeded the partition capacity
— see Modal logs).
- **Upload guard fires (files already exist).** The test name / model id
collides with something already on HF. Verify the user actually wants to
replace existing refs; if so, re-run the upload with `--force`. If not,
rename the model id in `*_MODEL_TO_PARAMS` and re-seed.
- **Quality looks wrong in step 4.** Abort. The mp4s stay on disk for
inspection. The fix is usually in the test's params (resolution, steps,
seed) — edit the test, then re-run the skill.
## Design notes (for future skill maintainers)
- The skill deliberately runs on Modal, **not** locally, because the CI
runner is L40S. Seeding from a different GPU SKU produces refs that CI's
L40S runs can't match (SSIM drifts across SKUs).
- The skill is default-tier only. `full_quality` refs are seeded by a
separate, deliberate operation — they double runtime and aren't what CI
gates on.
- The overwrite guard in `reference_videos_cli.py upload` is default-on
specifically because this skill exists. Re-seeding is a distinct operation
that requires explicit `--force`.
## References
- `fastvideo/tests/modal/ssim_test.py` — Modal orchestrator; see
`--sync-generated-to-volume`, `--generated-volume-subdir`,
`--skip-reference-download`, `--no-fail-fast`.
- `fastvideo/tests/ssim/reference_videos_cli.py` — `copy-local`, `upload`
(with `--model-id`, `--force`), `download`, `ensure` subcommands.
- `fastvideo/tests/ssim/README.md` — reference layout, HF repo conventions.
- `fastvideo/tests/ssim/inference_similarity_utils.py` —
`run_text_to_video_similarity_test` + `_build_init_kwargs`: what each test
config passes to `VideoGenerator.from_pretrained`.
## Changelog
| Date | Change |
|------|--------|
| 2026-04-17 | Initial version (Modal sync-to-volume flow). |
| 2026-04-21 | Rewrite: single-test scope, explicit user-review pause, per-`model_id` upload, HF overwrite guard. Dropped `scripts/seed_ssim.sh`. |
| 2026-04-21 | Post-first-run fixes: `modal volume get` needs `--force` when parent exists; download tree has an extra `generated_videos/` level so `--generated-dir` must reflect it. |
+1 -1
View File
@@ -105,7 +105,7 @@ pull_request_rules:
- files~=^fastvideo/pipelines/samplers/
- files~=^fastvideo/entrypoints/
- files~=^fastvideo/worker/
- files~=^fastvideo/api/sampling_param
- files~=^fastvideo/configs/sample/
- files~=^fastvideo/configs/pipelines/
- files~=^examples/inference/
- -closed
+2 -2
View File
@@ -62,9 +62,9 @@ This page contains the complete API reference for the FastVideo library.
show_root_toc_entry: true
heading_level: 4
#### fastvideo.api.sampling_param
#### fastvideo.configs.sample
::: fastvideo.api.sampling_param
::: fastvideo.configs.sample
options:
show_source: true
show_root_heading: true
+1 -1
View File
@@ -173,7 +173,7 @@ Applied by Mergify based on which paths you modified. Multiple scope labels can
| Label | File paths that trigger it |
|-------|---------------------------|
| `scope: training` | `fastvideo/train/`, `fastvideo/training/`, `fastvideo/distillation/`, `examples/train/`, `examples/training/`, `examples/distill/` |
| `scope: inference` | `fastvideo/pipelines/basic/`, `fastvideo/pipelines/stages/`, `fastvideo/pipelines/samplers/`, `fastvideo/entrypoints/`, `fastvideo/worker/`, `fastvideo/api/sampling_param.py`, `fastvideo/configs/pipelines/`, `examples/inference/` |
| `scope: inference` | `fastvideo/pipelines/basic/`, `fastvideo/pipelines/stages/`, `fastvideo/pipelines/samplers/`, `fastvideo/entrypoints/`, `fastvideo/worker/`, `fastvideo/configs/sample/`, `fastvideo/configs/pipelines/`, `examples/inference/` |
| `scope: attention` | `fastvideo/attention/` |
| `scope: kernel` | `fastvideo-kernel/`, `csrc/` |
| `scope: data` | `fastvideo/dataset/`, `fastvideo/pipelines/preprocess/`, `examples/preprocessing/` |
+4 -5
View File
@@ -44,7 +44,7 @@ FastVideo maps a Diffusers-style repo into a pipeline like:
- `fastvideo/configs/models/*`: arch configs and `param_names_mapping` for
weight name translation.
- `fastvideo/configs/pipelines/*`: pipeline wiring (component classes + names).
- `fastvideo/api/sampling_param.py`: runtime sampling parameters.
- `fastvideo/configs/sample/*`: default runtime sampling parameters.
- `fastvideo/pipelines/basic/*`: end-to-end pipeline logic built from stages.
- `model_index.json`: the HF repo entrypoint that maps component names to
classes and weight files.
@@ -55,7 +55,7 @@ Minimal usage example (based on `examples/inference/basic/basic.py`):
```python
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
model_id = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" # or official_weights/<model_name>/
generator = VideoGenerator.from_pretrained(model_id, num_gpus=1)
@@ -319,8 +319,7 @@ Purpose:
- `fastvideo/configs/pipelines/` describes pipeline wiring and model module
names.
- `fastvideo/api/sampling_param.py` defines runtime sampling parameters.
Defaults come from profiles in `fastvideo/pipelines/basic/<family>/profiles.py`.
- `fastvideo/configs/sample/` defines default runtime parameters.
Action:
@@ -475,7 +474,7 @@ FastVideo integration.
3. Pipeline wiring.
- Pipeline: `fastvideo/pipelines/basic/wan/wan_pipeline.py`
- Pipeline config: `fastvideo/configs/pipelines/wan.py`
- Sampling defaults: `fastvideo/pipelines/basic/wan/profiles.py`
- Sampling defaults: `fastvideo/configs/sample/wan.py`
4. Minimal example.
- Script: `examples/inference/basic/basic.py`
@@ -1,7 +1,7 @@
status_definitions:
kept: "Public field remains on a public adapter surface with the same meaning."
moved: "Public field remains supported but normalizes into a different nested path."
preset_owned: "Public field remains supported only through a model/preset-specific surface."
profile_owned: "Public field remains supported only through a model/profile-specific surface."
compatibility_only: "Legacy public field remains adapter-only during migration and is not part of the canonical typed schema."
private_only: "Field should only be handled by private adapters and is not a public FastVideo compatibility promise."
internal_only: "Field is runtime/config plumbing and should not be part of the new public typed inference API."
@@ -29,7 +29,7 @@ surfaces:
vae_cpu_offload: generator.engine.offload.vae
pin_cpu_memory: generator.engine.offload.pin_cpu_memory
enable_torch_compile: generator.engine.compile.enabled
torch_compile_kwargs: generator.engine.compile.backend,fullgraph,mode,dynamic,extras
torch_compile_kwargs: generator.engine.compile.kwargs
disable_autocast: generator.engine.disable_autocast
enable_stage_verification: generator.engine.enable_stage_verification
prompt_txt: request.inputs.prompt_path
@@ -40,12 +40,12 @@ surfaces:
init_weights_from_safetensors_2: generator.pipeline.components.transformer_2_weights
override_pipeline_cls_name: generator.pipeline.components.override_pipeline_cls_name
boundary_ratio: request.sampling.boundary_ratio
ltx2_vae_tiling: generator.pipeline.vae_tiling
preset_owned:
ltx2_vae_spatial_tile_size_in_pixels: generator.pipeline.preset_overrides.ltx2.vae.spatial_tile_size_in_pixels
ltx2_vae_spatial_tile_overlap_in_pixels: generator.pipeline.preset_overrides.ltx2.vae.spatial_tile_overlap_in_pixels
ltx2_vae_temporal_tile_size_in_frames: generator.pipeline.preset_overrides.ltx2.vae.temporal_tile_size_in_frames
ltx2_vae_temporal_tile_overlap_in_frames: generator.pipeline.preset_overrides.ltx2.vae.temporal_tile_overlap_in_frames
profile_owned:
ltx2_vae_tiling: generator.pipeline.profile_overrides.ltx2.vae_tiling
ltx2_vae_spatial_tile_size_in_pixels: generator.pipeline.profile_overrides.ltx2.vae.spatial_tile_size_in_pixels
ltx2_vae_spatial_tile_overlap_in_pixels: generator.pipeline.profile_overrides.ltx2.vae.spatial_tile_overlap_in_pixels
ltx2_vae_temporal_tile_size_in_frames: generator.pipeline.profile_overrides.ltx2.vae.temporal_tile_size_in_frames
ltx2_vae_temporal_tile_overlap_in_frames: generator.pipeline.profile_overrides.ltx2.vae.temporal_tile_overlap_in_frames
ltx2_initial_latent_path: request.extensions.ltx2.initial_latent_path
compatibility_only:
mode: "Legacy multi-mode FastVideoArgs switch; typed inference config should not expose execution mode."
@@ -69,16 +69,16 @@ surfaces:
pipeline_config_base:
moved:
pipeline_config_path: generator.pipeline.components.pipeline_config_path
preset_owned:
embedded_cfg_scale: generator.pipeline.preset_overrides.embedded_cfg_scale
flow_shift: generator.pipeline.preset_overrides.flow_shift
flow_shift_sr: generator.pipeline.preset_overrides.flow_shift_sr
is_causal: generator.pipeline.preset_overrides.is_causal
vae_tiling: generator.pipeline.preset_overrides.vae_tiling
vae_sp: generator.pipeline.preset_overrides.vae_sp
dmd_denoising_steps: generator.pipeline.preset_overrides.dmd_denoising_steps
ti2v_task: generator.pipeline.preset_overrides.ti2v_task
boundary_ratio: generator.pipeline.preset_overrides.boundary_ratio
profile_owned:
embedded_cfg_scale: generator.pipeline.profile_overrides.embedded_cfg_scale
flow_shift: generator.pipeline.profile_overrides.flow_shift
flow_shift_sr: generator.pipeline.profile_overrides.flow_shift_sr
is_causal: generator.pipeline.profile_overrides.is_causal
vae_tiling: generator.pipeline.profile_overrides.vae_tiling
vae_sp: generator.pipeline.profile_overrides.vae_sp
dmd_denoising_steps: generator.pipeline.profile_overrides.dmd_denoising_steps
ti2v_task: generator.pipeline.profile_overrides.ti2v_task
boundary_ratio: generator.pipeline.profile_overrides.boundary_ratio
compatibility_only:
model_path: "Redundant with generator.model_path."
disable_autocast: "Duplicated by generator.engine.disable_autocast during migration."
@@ -97,7 +97,7 @@ surfaces:
postprocess_text_funcs: "Internal text postprocessing hooks."
pipeline_config_extensions:
preset_owned:
profile_owned:
conditioning_strategy:
sources:
- fastvideo.configs.pipelines.cosmos.CosmosConfig
@@ -309,8 +309,8 @@ surfaces:
compatibility_only:
batch_size: "Gen3C inference-only tuning field pending typed batching design."
gradient_checkpointing: "Gen3C inference-only compatibility field pending typed batching design."
guidance_scale: "Gen3C pipeline-level default pending preset/default-request cleanup."
num_inference_steps: "Gen3C pipeline-level default pending preset/default-request cleanup."
guidance_scale: "Gen3C pipeline-level default pending profile/default-request cleanup."
num_inference_steps: "Gen3C pipeline-level default pending profile/default-request cleanup."
internal_only:
audio_decoder_config: "Legacy internal component config object."
audio_decoder_precision: "Precision override pending dedicated component precision design."
@@ -345,7 +345,6 @@ surfaces:
num_inference_steps: request.sampling.num_inference_steps
num_inference_steps_sr: request.sampling.num_inference_steps_sr
guidance_scale: request.sampling.guidance_scale
guidance_scale_2: request.sampling.guidance_scale_2
guidance_rescale: request.sampling.guidance_rescale
boundary_ratio: request.sampling.boundary_ratio
sigmas: request.sampling.sigmas
@@ -354,34 +353,96 @@ surfaces:
return_frames: request.output.return_frames
return_trajectory_latents: request.runtime.return_trajectory_latents
return_trajectory_decoded: request.runtime.return_trajectory_decoded
preset_owned:
profile_owned:
t_thresh: request.stage_overrides.refine.t_thresh
spatial_refine_only: request.stage_overrides.refine.spatial_refine_only
num_cond_frames: request.stage_overrides.refine.num_cond_frames
trajectory_type: request.extensions.gen3c.trajectory_type
movement_distance: request.extensions.gen3c.movement_distance
camera_rotation: request.extensions.gen3c.camera_rotation
prompt_attention_mask: request.extensions.hyworld.prompt_attention_mask
negative_attention_mask: request.extensions.hyworld.negative_attention_mask
camera_states: request.extensions.hunyuangamecraft.camera_states
camera_trajectory: request.extensions.hunyuangamecraft.camera_trajectory
action_list: request.extensions.hunyuangamecraft.action_list
action_speed_list: request.extensions.hunyuangamecraft.action_speed_list
gt_latents: request.extensions.hunyuangamecraft.gt_latents
conditioning_mask: request.extensions.hunyuangamecraft.conditioning_mask
ltx2_cfg_scale_video: request.extensions.ltx2.cfg_scale_video
ltx2_cfg_scale_audio: request.extensions.ltx2.cfg_scale_audio
ltx2_modality_scale_video: request.extensions.ltx2.modality_scale_video
ltx2_modality_scale_audio: request.extensions.ltx2.modality_scale_audio
ltx2_rescale_scale: request.extensions.ltx2.rescale_scale
ltx2_stg_scale_video: request.extensions.ltx2.stg_scale_video
ltx2_stg_scale_audio: request.extensions.ltx2.stg_scale_audio
ltx2_stg_blocks_video: request.extensions.ltx2.stg_blocks_video
ltx2_stg_blocks_audio: request.extensions.ltx2.stg_blocks_audio
internal_only:
data_type: "Derived from the request shape and not a public input."
sampling_param_extensions: {}
sampling_param_extensions:
moved:
guidance_scale_2:
target: request.sampling.guidance_scale_2
sources:
- fastvideo.configs.sample.lingbotworld.LingBotWorld_SamplingParam
- fastvideo.configs.sample.lingbotworld.Wan2_2_I2V_A14B_SamplingParam
- fastvideo.configs.sample.wan.SelfForcingWan2_2_T2V_A14B_480P_SamplingParam
- fastvideo.configs.sample.wan.Wan2_2_I2V_A14B_SamplingParam
- fastvideo.configs.sample.wan.Wan2_2_T2V_A14B_SamplingParam
profile_owned:
action_list:
target: request.extensions.hunyuangamecraft.action_list
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
action_speed_list:
target: request.extensions.hunyuangamecraft.action_speed_list
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
camera_states:
target: request.extensions.hunyuangamecraft.camera_states
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
camera_trajectory:
target: request.extensions.hunyuangamecraft.camera_trajectory
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
conditioning_mask:
target: request.extensions.hunyuangamecraft.conditioning_mask
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
gt_latents:
target: request.extensions.hunyuangamecraft.gt_latents
sources:
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
prompt_attention_mask:
target: request.extensions.hyworld.prompt_attention_mask
sources: [fastvideo.configs.sample.hyworld.HYWorld_SamplingParam]
negative_attention_mask:
target: request.extensions.hyworld.negative_attention_mask
sources: [fastvideo.configs.sample.hyworld.HYWorld_SamplingParam]
ltx2_cfg_scale_audio:
target: request.extensions.ltx2.cfg_scale_audio
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_cfg_scale_video:
target: request.extensions.ltx2.cfg_scale_video
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_modality_scale_audio:
target: request.extensions.ltx2.modality_scale_audio
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_modality_scale_video:
target: request.extensions.ltx2.modality_scale_video
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_rescale_scale:
target: request.extensions.ltx2.rescale_scale
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_stg_blocks_audio:
target: request.extensions.ltx2.stg_blocks_audio
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_stg_blocks_video:
target: request.extensions.ltx2.stg_blocks_video
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_stg_scale_audio:
target: request.extensions.ltx2.stg_scale_audio
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
ltx2_stg_scale_video:
target: request.extensions.ltx2.stg_scale_video
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
openai_image_request:
kept:
+5 -6
View File
@@ -12,7 +12,7 @@ FastVideo maps a Diffusers-style repo into a pipeline like this:
- `fastvideo/configs/models/*`: arch configs and `param_names_mapping` for
weight name translation.
- `fastvideo/configs/pipelines/*`: pipeline wiring (component classes + names).
- `fastvideo/api/sampling_param.py`: runtime sampling parameters.
- `fastvideo/configs/sample/*`: default runtime sampling parameters.
- `fastvideo/pipelines/basic/*`: end-to-end pipelines.
- `fastvideo/pipelines/stages/*`: reusable pipeline stages.
- `fastvideo/models/loader/*`: component loaders for Diffusers-style repos.
@@ -26,7 +26,7 @@ Minimal usage (from `examples/inference/basic/basic.py`):
```python
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
model_id = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" # or official_weights/<model_name>/
generator = VideoGenerator.from_pretrained(model_id, num_gpus=1)
@@ -49,9 +49,8 @@ runtime parameters consistent:
- `fastvideo/configs/models/`: architecture definitions, layer shapes, and
`param_names_mapping` rules for key renaming.
- `fastvideo/configs/pipelines/`: pipeline wiring and required components.
- `fastvideo/api/sampling_param.py`: sampling parameters (steps, frames,
guidance scale, resolution, fps). Defaults come from profiles in
`fastvideo/pipelines/basic/<family>/profiles.py`.
- `fastvideo/configs/sample/`: default sampling parameters (steps, frames,
guidance scale, resolution, fps).
- `fastvideo/registry.py`: unified registry for pipeline config + sampling
defaults and model metadata resolution, defined via explicit
`register_configs(...)` blocks (no separate dict registries).
@@ -143,7 +142,7 @@ How this maps to FastVideo:
- `T5TokenizerFast` -> loaded via HF in `fastvideo/models/loader/`
- `UniPCMultistepScheduler` -> loaded via Diffusers scheduler utilities
- Pipeline defaults -> `fastvideo/configs/pipelines/wan.py`
- Sampling defaults -> `fastvideo/pipelines/basic/wan/profiles.py`
- Sampling defaults -> `fastvideo/configs/sample/wan.py`
## Pipeline system
+1 -1
View File
@@ -33,7 +33,7 @@ The following two classes `PipelineConfig` and `SamplingParam` are used to confi
### SamplingParam
::: fastvideo.api.sampling_param.SamplingParam
::: fastvideo.configs.sample.base.SamplingParam
options:
show_root_heading: true
show_source: false
+12 -7
View File
@@ -128,14 +128,19 @@ Concrete hierarchy: `DiTConfig` → `DiTArchConfig`, `VAEConfig` →
- `dump_to_json()` / `load_from_json()` — JSON persistence. Callable
fields and `arch_config` are excluded from dumps.
### SamplingParam (`fastvideo/api/sampling_param.py`)
### SamplingParam (`fastvideo/configs/sample/`)
Generation parameters separate from pipeline config. Each model family
provides defaults via a profile (see `fastvideo/pipelines/basic/<family>/profiles.py`):
provides defaults:
```python
sp = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
# sp.height == 480, sp.width == 832, sp.num_frames == 81, etc.
@dataclass
class WanT2V_1_3B_SamplingParam(SamplingParam):
height: int = 480
width: int = 832
num_frames: int = 81
guidance_scale: float = 3.0
num_inference_steps: int = 50
```
## Component Loading
@@ -425,9 +430,9 @@ User: generator.generate_video(prompt, ...)
`fastvideo/configs/pipelines/<model>.py`. Set DiT/VAE/encoder configs,
flow_shift, precision defaults.
2. **Sampling param profile** — Create a profile in
`fastvideo/pipelines/basic/<model>/profiles.py` with default height,
width, num_frames, guidance_scale, num_inference_steps.
2. **Sampling param** — Create a `SamplingParam` subclass in
`fastvideo/configs/sample/<model>.py`. Set default height, width,
num_frames, guidance_scale, num_inference_steps.
3. **Register configs** — In `fastvideo/registry.py`, add a
`register_configs()` call inside `_register_configs()` with
+1 -1
View File
@@ -89,7 +89,7 @@ GEN3C defaults in FastVideo:
These values are defined in:
- `fastvideo/pipelines/basic/gen3c/profiles.py`
- `fastvideo/configs/sample/gen3c.py`
- `fastvideo/configs/pipelines/gen3c.py`
and align with the official GEN3C inference defaults in:
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples"
def main():
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
def main():
@@ -1,5 +1,5 @@
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
def main():
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
def main():
+1 -1
View File
@@ -2,7 +2,7 @@ import os
import time
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_dmd2"
def main():
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
import json
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_hy15"
def main():
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
import json
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_hy15_1080p"
def main():
@@ -1,7 +1,7 @@
from fastvideo import VideoGenerator
from fastvideo.models.dits.lingbotworld.cam_utils import prepare_camera_embedding
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_lingbotworld"
def main():
# FastVideo will automatically use the optimal default arguments for the
+1 -1
View File
@@ -1,5 +1,5 @@
from fastvideo import VideoGenerator, PipelineConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
def main():
config = PipelineConfig.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples"
def main():
@@ -2,7 +2,7 @@
from fastvideo import VideoGenerator, SamplingParam
import json
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_i2v"
def main():
@@ -2,7 +2,7 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_t2v"
def main():
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_wan2_2_14B_t2v"
def main():
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_wan2_1_Fun"
OUTPUT_NAME = "wan2.1_test"
+1 -1
View File
@@ -1,6 +1,6 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
# from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "video_samples_wan2_2_14B_i2v"
def main():
@@ -5,7 +5,7 @@ import time
import gradio as gr
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample.base import SamplingParam
from copy import deepcopy
@@ -9,7 +9,7 @@ import tempfile
import gradio as gr
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample.base import SamplingParam
MODEL_PATH_MAPPING = {
@@ -185,7 +185,7 @@ class BaseModelDeployment:
def _initialize_generator(self, config: Dict[str, Any]) -> None:
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample.base import SamplingParam
print(f"Initializing model: {self.model_path}")
self.generator = VideoGenerator.from_pretrained(
@@ -1,5 +1,5 @@
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "./lora_out"
def main():
@@ -2,7 +2,7 @@
Inference using a LoRA checkpoint from FastVideo trainer.
"""
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
OUTPUT_PATH = "./lora_out"
def main():
+1 -10
View File
@@ -61,16 +61,7 @@ has_cmake_arg() {
}
detect_with_torch() {
# Prefer the active venv's python directly over `uv run --active --no-project`,
# which on some uv versions provisions its own interpreter and misses packages
# installed into VIRTUAL_ENV.
local py
if [[ -n "${VIRTUAL_ENV:-}" && -x "${VIRTUAL_ENV}/bin/python" ]]; then
py="${VIRTUAL_ENV}/bin/python"
else
py="$(command -v python3 || command -v python)"
fi
"${py}" -c "import torch
uv run --active --no-project python -c "import torch
if not torch.cuda.is_available():
raise RuntimeError('torch.cuda.is_available() is false')
mj, mn = torch.cuda.get_device_capability(0)
+1 -1
View File
@@ -1,5 +1,5 @@
from fastvideo.configs.pipelines import PipelineConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.version import __version__
-32
View File
@@ -7,37 +7,21 @@ from fastvideo.api.schema import (
GenerationPlan,
GenerationRequest,
GeneratorConfig,
GpuPoolConfig,
InputConfig,
OffloadConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
PlannedStage,
PromptEnhancerConfig,
PromptSafetyConfig,
QuantizationConfig,
RequestRuntimeConfig,
RunConfig,
SamplingConfig,
ServeConfig,
ServerConfig,
StreamingConfig,
WarmupConfig,
)
from fastvideo.api.errors import ConfigValidationError
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
from fastvideo.api.presets import (
InferencePreset,
PresetStageSpec,
get_all_preset_names,
get_preset,
get_presets_for_family,
register_preset,
validate_preset_selection,
validate_stage_names,
validate_stage_overrides,
)
from fastvideo.api.parser import (
config_to_dict,
load_config,
@@ -47,7 +31,6 @@ from fastvideo.api.parser import (
parse_config,
)
from fastvideo.api.results import GenerationResult
from fastvideo.api.sampling_param import SamplingParam
__all__ = [
"CompileConfig",
@@ -59,26 +42,18 @@ __all__ = [
"GenerationPlan",
"GenerationRequest",
"GeneratorConfig",
"GpuPoolConfig",
"InputConfig",
"OffloadConfig",
"OutputConfig",
"ParallelismConfig",
"PipelineSelection",
"PlannedStage",
"PromptEnhancerConfig",
"PromptSafetyConfig",
"QuantizationConfig",
"RequestRuntimeConfig",
"RunConfig",
"SamplingConfig",
"SamplingParam",
"ServeConfig",
"ServerConfig",
"StreamingConfig",
"WarmupConfig",
"InferencePreset",
"PresetStageSpec",
"apply_overrides",
"config_to_dict",
"load_config",
@@ -86,12 +61,5 @@ __all__ = [
"load_run_config",
"load_serve_config",
"parse_cli_overrides",
"get_all_preset_names",
"get_preset",
"get_presets_for_family",
"parse_config",
"register_preset",
"validate_preset_selection",
"validate_stage_names",
"validate_stage_overrides",
]
+93 -154
View File
@@ -7,16 +7,14 @@ from dataclasses import fields, is_dataclass
from pathlib import Path
from typing import Any
from fastvideo.api.overrides import apply_overrides, normalize_overrides
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
from fastvideo.api.parser import config_to_dict, load_raw_config, parse_config
from fastvideo.api.request_metadata import (
EXPLICIT_PATHS_ATTR,
EXPLICIT_REQUEST_ATTR,
bind_generation_request_raw,
get_explicit_paths,
reset_tracking_roots,
refresh_generation_request_raw,
)
from fastvideo.api.schema import (
CompileConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
@@ -24,9 +22,8 @@ from fastvideo.api.schema import (
RequestRuntimeConfig,
SamplingConfig,
)
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.pipelines.basic.ltx2.stage_overrides import REFINE_FLAT_KEYS
from fastvideo.utils import shallow_asdict
_INPUT_FIELD_NAMES = {field.name for field in fields(InputConfig)}
@@ -40,7 +37,6 @@ _LEGACY_REQUEST_ALIASES = {
_REQUEST_PIPELINE_OVERRIDE_FIELDS = frozenset({
"embedded_cfg_scale",
})
_COMPILE_TYPED_KEYS = ("backend", "fullgraph", "mode", "dynamic")
def normalize_generator_config(config: GeneratorConfig | Mapping[str, Any], ) -> GeneratorConfig:
@@ -54,7 +50,7 @@ def load_generator_config_from_file(
overrides: list[str] | Mapping[str, Any] | None = None,
) -> GeneratorConfig:
raw = load_raw_config(path)
normalized_overrides = normalize_overrides(overrides)
normalized_overrides = _normalize_overrides(overrides)
if _looks_like_run_or_serve_config(raw):
if normalized_overrides:
@@ -83,8 +79,6 @@ def legacy_from_pretrained_to_config(
components: dict[str, Any] = {}
quantization: dict[str, Any] = {}
experimental: dict[str, Any] = {}
preset_overrides: dict[str, Any] = {}
preset_refine: dict[str, Any] = {}
for key, value in kwargs.items():
if key == "revision":
@@ -112,30 +106,7 @@ def legacy_from_pretrained_to_config(
elif key == "enable_torch_compile":
compile_config["enabled"] = value
elif key == "torch_compile_kwargs":
remaining: dict[str, Any] = (dict(value) if isinstance(value, Mapping) else {})
for first_class in _COMPILE_TYPED_KEYS:
if first_class in remaining:
compile_config[first_class] = remaining.pop(first_class)
if remaining:
compile_config["extras"] = remaining
elif key == "ltx2_vae_tiling":
pipeline["vae_tiling"] = value
elif key == "config_model_path":
components["config_root"] = value
elif key == "ltx2_refine_enabled":
preset_refine["enabled"] = value
elif key == "ltx2_refine_upsampler_path":
# Empty string means "no upsampler"; keep typed None.
components["upsampler_weights"] = value or None
elif key == "ltx2_refine_lora_path":
# Empty string means "no refine LoRA"; keep typed None.
components["lora_path"] = value or None
elif key == "ltx2_refine_add_noise":
preset_refine["add_noise"] = value
elif key == "ltx2_refine_num_inference_steps":
preset_refine["num_inference_steps"] = value
elif key == "ltx2_refine_guidance_scale":
preset_refine["guidance_scale"] = value
compile_config["kwargs"] = deepcopy(value)
elif key in {"enable_stage_verification", "use_fsdp_inference", "disable_autocast"}:
engine[key] = value
elif key == "override_text_encoder_quant":
@@ -175,10 +146,6 @@ def legacy_from_pretrained_to_config(
if components:
pipeline["components"] = components
if preset_refine:
preset_overrides["refine"] = preset_refine
if preset_overrides:
pipeline["preset_overrides"] = preset_overrides
if experimental:
pipeline["experimental"] = experimental
if pipeline:
@@ -190,12 +157,16 @@ def legacy_from_pretrained_to_config(
def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, Any], ) -> FastVideoArgs:
normalized = normalize_generator_config(config)
unsupported = []
if normalized.pipeline.preset is not None:
unsupported.append("pipeline.preset")
if normalized.pipeline.preset_version is not None:
unsupported.append("pipeline.preset_version")
if normalized.pipeline.profile is not None:
unsupported.append("pipeline.profile")
if normalized.pipeline.profile_version is not None:
unsupported.append("pipeline.profile_version")
if normalized.pipeline.components.config_root is not None:
unsupported.append("pipeline.components.config_root")
if normalized.pipeline.components.vae_weights is not None:
unsupported.append("pipeline.components.vae_weights")
if normalized.pipeline.components.upsampler_weights is not None:
unsupported.append("pipeline.components.upsampler_weights")
if unsupported:
joined = ", ".join(unsupported)
raise NotImplementedError(f"VideoGenerator compatibility adapter does not support {joined} yet")
@@ -219,15 +190,13 @@ def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, An
"vae_cpu_offload": engine.offload.vae,
"pin_cpu_memory": engine.offload.pin_cpu_memory,
"enable_torch_compile": engine.compile.enabled,
"torch_compile_kwargs": _compile_config_to_torch_kwargs(engine.compile),
"torch_compile_kwargs": deepcopy(engine.compile.kwargs),
"enable_stage_verification": engine.enable_stage_verification,
"use_fsdp_inference": engine.use_fsdp_inference,
"disable_autocast": engine.disable_autocast,
}
if normalized.pipeline.workload_type is not None:
kwargs["workload_type"] = normalized.pipeline.workload_type
if normalized.pipeline.vae_tiling is not None:
kwargs["ltx2_vae_tiling"] = normalized.pipeline.vae_tiling
quantization = engine.quantization
if quantization is not None and quantization.text_encoder_quant is not None:
@@ -250,18 +219,8 @@ def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, An
kwargs["init_weights_from_safetensors"] = components.transformer_weights
if components.transformer_2_weights is not None:
kwargs["init_weights_from_safetensors_2"] = components.transformer_2_weights
if components.config_root is not None:
kwargs["config_model_path"] = components.config_root
if components.upsampler_weights is not None:
kwargs["ltx2_refine_upsampler_path"] = components.upsampler_weights
preset_overrides = deepcopy(normalized.pipeline.preset_overrides)
refine = preset_overrides.pop("refine", None)
if isinstance(refine, Mapping):
for key in REFINE_FLAT_KEYS:
if key in refine:
kwargs[f"ltx2_refine_{key}"] = refine[key]
kwargs.update(preset_overrides)
kwargs.update(deepcopy(normalized.pipeline.profile_overrides))
kwargs.update(deepcopy(normalized.pipeline.experimental))
return FastVideoArgs.from_kwargs(**kwargs)
@@ -269,9 +228,9 @@ def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, An
def normalize_generation_request(request: GenerationRequest | Mapping[str, Any], ) -> GenerationRequest:
normalized = (request if isinstance(request, GenerationRequest) else parse_config(GenerationRequest, request))
if not hasattr(normalized, EXPLICIT_PATHS_ATTR):
# Request wasn't bound through the parser (e.g. constructed
# directly). Treat every currently-set field as explicit.
if hasattr(normalized, EXPLICIT_REQUEST_ATTR):
refresh_generation_request_raw(normalized)
else:
bind_generation_request_raw(normalized, _serialize_generation_request(normalized))
return normalized
@@ -315,17 +274,12 @@ def request_to_sampling_param(
raise NotImplementedError("GenerationRequest.state is not wired into VideoGenerator yet")
sampling_param = SamplingParam.from_pretrained(model_path)
updates = explicit_request_updates(request)
updates = _explicit_request_updates(request)
for key, value in updates.items():
if hasattr(sampling_param, key):
setattr(sampling_param, key, deepcopy(value))
elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS:
continue
elif value == _SCHEMA_DEFAULT_UPDATES.get(key, _MISSING):
# Schema-default field that isn't on SamplingParam; tolerated
# because direct GenerationRequest(...) construction has no
# way to distinguish "user set" from "schema default".
elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS or _is_supported_as_default_only(key, value):
continue
else:
raise ValueError(f"Request field {key!r} is not supported by sampling params for {model_path}")
@@ -342,12 +296,10 @@ def expand_request_prompt_batch(request: GenerationRequest, ) -> list[Generation
requests: list[GenerationRequest] = []
for index, prompt in enumerate(request.prompt):
single_request = deepcopy(request)
# deepcopy preserves the tracking-root cycle, but re-pin roots
# defensively so that subsequent setattrs record on the copy.
reset_tracking_roots(single_request)
single_request.prompt = prompt
_fan_out_batched_input_value(request, single_request, "image_path", index)
_fan_out_batched_input_value(request, single_request, "video_path", index)
_fan_out_explicit_request_metadata(request, single_request, index, prompt)
requests.append(single_request)
return requests
@@ -356,18 +308,12 @@ def _looks_like_run_or_serve_config(raw: Mapping[str, Any]) -> bool:
return isinstance(raw.get("generator"), Mapping)
def _compile_config_to_torch_kwargs(compile_config: CompileConfig, ) -> dict[str, Any]:
"""Flatten typed ``CompileConfig`` back to the legacy
``torch_compile_kwargs`` dict, emitting only explicitly-set typed
fields and merging ``extras`` on top."""
out: dict[str, Any] = {}
for key in _COMPILE_TYPED_KEYS:
value = getattr(compile_config, key)
if value is not None:
out[key] = value
if compile_config.extras:
out.update(deepcopy(compile_config.extras))
return out
def _normalize_overrides(overrides: list[str] | Mapping[str, Any] | None, ) -> dict[str, Any] | None:
if not overrides:
return None
if isinstance(overrides, list):
return parse_cli_overrides(overrides)
return dict(overrides)
def _sampling_param_to_request_raw(sampling_param: SamplingParam | None, ) -> dict[str, Any]:
@@ -408,83 +354,20 @@ def _apply_request_field(
def request_to_pipeline_overrides(request: GenerationRequest) -> dict[str, Any]:
overrides: dict[str, Any] = {}
for key, value in explicit_request_updates(request).items():
for key, value in _explicit_request_updates(request).items():
if key in _REQUEST_PIPELINE_OVERRIDE_FIELDS:
overrides[key] = deepcopy(value)
return overrides
def explicit_request_updates(request: GenerationRequest) -> dict[str, Any]:
"""Project a ``GenerationRequest`` down to *explicitly set* fields only.
def _explicit_request_updates(request: GenerationRequest) -> dict[str, Any]:
raw = getattr(request, EXPLICIT_REQUEST_ATTR, None)
if raw is None:
raw = _serialize_generation_request(request)
Returns a flat kwargs dict suitable for merging into a generator call.
The projection uses ``_fastvideo_explicit_paths`` (populated during
``parse_config`` / raw binding) so schema defaults on the dataclass
are **not** emitted — only paths the caller/operator actually wrote.
This is what makes ``ServeConfig.default_request`` work as an
operator-pinned baseline rather than a full override: a YAML with just
``sampling.seed: 42`` yields ``{"seed": 42}``, not the full sampling
config with its 15 schema defaults.
Precondition: the request must carry ``_fastvideo_explicit_paths`` —
populated by :func:`fastvideo.api.parser.parse_config` or
:func:`fastvideo.api.compat.normalize_generation_request`. Calling on
a raw ``GenerationRequest()`` asserts.
"""
assert hasattr(request,
EXPLICIT_PATHS_ATTR), ("GenerationRequest reached explicit_request_updates without tracking; "
"every entry point must route through normalize_generation_request "
"or parse_config first")
paths = get_explicit_paths(request)
raw = _build_sparse_raw_from_paths(request, paths)
return _extract_request_updates(raw)
def _build_sparse_raw_from_paths(
request: GenerationRequest,
paths: frozenset[str],
) -> dict[str, Any]:
result: dict[str, Any] = {}
for path in paths:
parts = path.split(".")
value = _read_dotted_path(request, parts)
if value is _MISSING:
continue
_set_dotted_path(result, parts, deepcopy(value))
return result
def _read_dotted_path(obj: Any, parts: list[str]) -> Any:
for part in parts:
if is_dataclass(obj) and not isinstance(obj, type):
if not hasattr(obj, part):
return _MISSING
obj = getattr(obj, part)
elif isinstance(obj, Mapping):
if part not in obj:
return _MISSING
obj = obj[part]
else:
return _MISSING
return obj
def _set_dotted_path(
target: dict[str, Any],
parts: list[str],
value: Any,
) -> None:
cursor = target
for part in parts[:-1]:
nxt = cursor.get(part)
if not isinstance(nxt, dict):
nxt = {}
cursor[part] = nxt
cursor = nxt
cursor[parts[-1]] = value
def _extract_request_updates(raw: Mapping[str, Any]) -> dict[str, Any]:
updates: dict[str, Any] = {}
if "negative_prompt" in raw:
@@ -528,9 +411,6 @@ def _serialize_generation_request(request: GenerationRequest) -> dict[str, Any]:
return deepcopy(config_to_dict(request))
_SCHEMA_DEFAULT_UPDATES = _extract_request_updates(config_to_dict(GenerationRequest()))
def _fan_out_batched_input_value(
source_request: GenerationRequest,
target_request: GenerationRequest,
@@ -544,6 +424,29 @@ def _fan_out_batched_input_value(
setattr(target_request.inputs, field_name, deepcopy(value[index]))
def _fan_out_explicit_request_metadata(
source_request: GenerationRequest,
target_request: GenerationRequest,
index: int,
prompt: str,
) -> None:
raw = getattr(source_request, EXPLICIT_REQUEST_ATTR, None)
if raw is None:
return
raw = deepcopy(raw)
raw["prompt"] = prompt
inputs = raw.get("inputs")
if isinstance(inputs, dict):
for field_name in ("image_path", "video_path"):
value = inputs.get(field_name)
if isinstance(value, list):
_validate_batched_input_length(source_request.prompt, value, field_name)
inputs[field_name] = deepcopy(value[index])
setattr(target_request, EXPLICIT_REQUEST_ATTR, raw)
def _validate_batched_input_length(
prompts: str | list[str] | None,
values: list[Any],
@@ -555,8 +458,44 @@ def _validate_batched_input_length(
raise ValueError(f"GenerationRequest.inputs.{field_name} must have the same length as request.prompt")
def _is_supported_as_default_only(key: str, value: Any) -> bool:
default_value = _DEFAULT_REQUEST_UPDATES.get(key, _MISSING)
return default_value is not _MISSING and _values_equal(value, default_value)
def _collect_non_default_fields(
value: Any,
default: Any,
) -> dict[str, Any]:
if not (is_dataclass(value) and is_dataclass(default)):
return {}
result: dict[str, Any] = {}
for field in fields(value):
current = getattr(value, field.name)
default_value = getattr(default, field.name)
if is_dataclass(current) and is_dataclass(default_value):
nested = _collect_non_default_fields(current, default_value)
if nested:
result[field.name] = nested
continue
if not _values_equal(current, default_value):
result[field.name] = deepcopy(current)
return result
def _values_equal(left: Any, right: Any) -> bool:
if left is right:
return True
try:
return bool(left == right)
except Exception:
return False
_DEFAULT_REQUEST_UPDATES = _extract_request_updates(config_to_dict(GenerationRequest()))
__all__ = [
"explicit_request_updates",
"generator_config_to_fastvideo_args",
"legacy_from_pretrained_to_config",
"legacy_generate_call_to_request",
+1 -10
View File
@@ -45,15 +45,6 @@ def apply_overrides(config: Mapping[str, Any], overrides: Mapping[str, Any]) ->
return merged
def normalize_overrides(overrides: list[str] | Mapping[str, Any] | None, ) -> dict[str, Any] | None:
"""Normalize a CLI list or mapping of overrides into a flat dict."""
if not overrides:
return None
if isinstance(overrides, list):
return parse_cli_overrides(overrides)
return dict(overrides)
def _apply_single_override(config: dict[str, Any], dotted_key: str, value: Any) -> None:
parts = dotted_key.split(".")
if not all(parts):
@@ -107,4 +98,4 @@ def _normalize_override_key(key: str) -> str:
return key.replace("-", "_")
__all__ = ["apply_overrides", "normalize_overrides", "parse_cli_overrides"]
__all__ = ["apply_overrides", "parse_cli_overrides"]
+10 -2
View File
@@ -11,7 +11,7 @@ from typing import Any, Literal, TypeVar, Union, get_args, get_origin, get_type_
import yaml
from fastvideo.api.errors import ConfigValidationError
from fastvideo.api.overrides import apply_overrides, normalize_overrides
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
from fastvideo.api.request_metadata import (
bind_generation_request_raw,
bind_run_config_raw,
@@ -64,7 +64,7 @@ def load_config(
) -> T:
"""Load a typed config object from YAML or JSON."""
raw = load_raw_config(path)
normalized_overrides = normalize_overrides(overrides)
normalized_overrides = _normalize_overrides(overrides)
if normalized_overrides:
raw = apply_overrides(raw, normalized_overrides)
return parse_config(config_type, raw)
@@ -108,6 +108,14 @@ def _load_raw_mapping(handle: Any, config_path: Path) -> Any:
raise ValueError(f"Unsupported config file format: {config_path}")
def _normalize_overrides(overrides: list[str] | Mapping[str, Any] | None, ) -> dict[str, Any] | None:
if not overrides:
return None
if isinstance(overrides, list):
return parse_cli_overrides(overrides)
return dict(overrides)
class _SchemaParser:
def parse_dataclass(
-261
View File
@@ -1,261 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Pipeline preset registry.
A *preset* is a named inference preset for a model family. It bundles:
* ``defaults`` — sampling values applied when the user does not
override them (consumed at runtime via ``SamplingParam.from_pretrained``);
* ``stage_schemas`` — **validation-only** metadata describing which
user-facing stage names (``"denoise"``, ``"sr"``) the preset recognises
and which ``stage_overrides`` keys each stage accepts.
The ``stage_schemas`` tuple does **not** drive pipeline execution. The
concrete execution DAG (text encoding, denoising, VAE decoding, …) is
hard-coded per-pipeline in ``create_pipeline_stages()``. Schemas exist
purely so that ``PipelineSelection.preset`` and
``GenerationRequest.stage_overrides`` can be type-checked up front
without touching the pipeline.
Preset base types and the registry API live here (public API surface).
Preset *instances* are defined in pipeline-local ``presets.py`` files
(e.g. ``fastvideo/pipelines/basic/wan/presets.py``) and registered
explicitly from :func:`_register_presets` in ``fastvideo/registry.py``.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any
from fastvideo.api.errors import ConfigValidationError
# -------------------------------------------------------------------
# Types
# -------------------------------------------------------------------
@dataclass(frozen=True)
class PresetStageSpec:
"""A user-facing stage name within a preset, used only to validate
``stage_overrides`` keys. Not read by pipeline execution — the real
execution DAG lives in each pipeline's ``create_pipeline_stages()``.
"""
name: str
"""Short user-facing name, e.g. ``"denoise"``, ``"sr"``."""
kind: str
"""Semantic kind, e.g. ``"denoising"``, ``"super_resolution"``."""
description: str = ""
allowed_overrides: frozenset[str] = field(default_factory=frozenset)
"""Keys that may appear in ``stage_overrides[name]``."""
@dataclass(frozen=True)
class InferencePreset:
"""A named inference preset for a model family."""
name: str
"""Preset name, e.g. ``"wan_t2v_1_3b"``."""
version: int
"""Preset schema version; bump on breaking schema changes."""
model_family: str
"""Model family key, e.g. ``"wan"``, ``"ltx2"``."""
description: str = ""
workload_type: str | None = None
"""Optional workload hint: ``"t2v"``, ``"i2v"``, etc."""
stage_schemas: tuple[PresetStageSpec, ...] = ()
"""User-facing stage names for ``stage_overrides`` validation.
Validation-only: this tuple is consumed by
:func:`validate_stage_overrides` and is **not** used to drive
pipeline execution. Omit or leave empty if the preset exposes no
per-stage override surface.
"""
defaults: dict[str, Any] = field(default_factory=dict)
"""Preset-level default sampling/runtime values."""
stage_defaults: dict[str, dict[str, Any]] = field(default_factory=dict)
"""Per-stage default overrides, keyed by stage name."""
# -------------------------------------------------------------------
# Registry
# -------------------------------------------------------------------
# Keyed by (model_family, name, version).
_PRESET_REGISTRY: dict[tuple[str, str, int], InferencePreset] = {}
def register_preset(preset: InferencePreset) -> None:
"""Register a preset definition.
Raises :class:`ValueError` on duplicate
``(model_family, name, version)`` keys.
"""
key = (preset.model_family, preset.name, preset.version)
if key in _PRESET_REGISTRY:
raise ValueError(f"Duplicate preset registration: "
f"model_family={key[0]!r}, name={key[1]!r}, "
f"version={key[2]!r}")
_PRESET_REGISTRY[key] = preset
def get_preset(
name: str,
model_family: str,
version: int | None = None,
) -> InferencePreset:
"""Look up a registered preset.
When *version* is ``None`` the highest registered version for the
given *(model_family, name)* pair is returned.
Raises :class:`~fastvideo.api.errors.ConfigValidationError` when the
preset cannot be found.
"""
if version is not None:
key = (model_family, name, version)
preset = _PRESET_REGISTRY.get(key)
if preset is not None:
return preset
raise ConfigValidationError(
"pipeline.preset",
f"unknown preset {name!r} version {version!r} "
f"for model family {model_family!r}; "
f"registered: {_format_registered(model_family)}",
)
# Find the highest version for (model_family, name).
candidates = [prof for (fam, n, _v), prof in _PRESET_REGISTRY.items() if fam == model_family and n == name]
if not candidates:
raise ConfigValidationError(
"pipeline.preset",
f"unknown preset {name!r} for model family "
f"{model_family!r}; "
f"registered: {_format_registered(model_family)}",
)
return max(candidates, key=lambda p: p.version)
def get_presets_for_family(model_family: str, ) -> list[InferencePreset]:
"""Return all presets registered for *model_family*."""
return [prof for (fam, _n, _v), prof in _PRESET_REGISTRY.items() if fam == model_family]
def get_all_preset_names() -> list[str]:
"""Return the sorted list of all registered preset names."""
return sorted({prof.name for prof in _PRESET_REGISTRY.values()})
# -------------------------------------------------------------------
# Validation helpers
# -------------------------------------------------------------------
def validate_stage_names(
preset: InferencePreset,
stage_overrides: Mapping[str, Any],
) -> None:
"""Check that *stage_overrides* keys are valid stage names.
Raises :class:`~fastvideo.api.errors.ConfigValidationError` with a
path-qualified message for unknown stage names.
"""
valid_names = {stage.name for stage in preset.stage_schemas}
for stage_name in stage_overrides:
if stage_name not in valid_names:
raise ConfigValidationError(
f"stage_overrides.{stage_name}",
f"unknown stage for preset {preset.name!r}; "
f"valid stages: {sorted(valid_names)}",
)
def validate_stage_overrides(
preset: InferencePreset,
stage_overrides: Mapping[str, Any],
) -> None:
"""Validate stage override keys against the preset.
Calls :func:`validate_stage_names` first, then checks that each
override key is in the stage's ``allowed_overrides``.
"""
validate_stage_names(preset, stage_overrides)
stages_by_name = {stage.name: stage for stage in preset.stage_schemas}
for stage_name, overrides in stage_overrides.items():
if not isinstance(overrides, Mapping):
raise ConfigValidationError(
f"stage_overrides.{stage_name}",
"must be a mapping",
)
stage_spec = stages_by_name[stage_name]
if not stage_spec.allowed_overrides:
if overrides:
raise ConfigValidationError(
f"stage_overrides.{stage_name}",
f"stage {stage_name!r} does not accept "
f"overrides",
)
continue
for key in overrides:
if key not in stage_spec.allowed_overrides:
raise ConfigValidationError(
f"stage_overrides.{stage_name}.{key}",
f"not an allowed override for stage "
f"{stage_name!r}; allowed: "
f"{sorted(stage_spec.allowed_overrides)}",
)
def validate_preset_selection(
preset_name: str | None,
model_family: str,
*,
preset_version: int | None = None,
stage_overrides: Mapping[str, Any] | None = None,
) -> InferencePreset | None:
"""Resolve and validate a preset selection end-to-end.
Returns the resolved :class:`InferencePreset`, or ``None`` if
*preset_name* is ``None`` (no preset requested).
"""
if preset_name is None:
return None
preset = get_preset(preset_name, model_family, version=preset_version)
if stage_overrides:
validate_stage_overrides(preset, stage_overrides)
return preset
# -------------------------------------------------------------------
# Internal helpers
# -------------------------------------------------------------------
def _format_registered(model_family: str) -> str:
names = sorted({prof.name for (fam, _n, _v), prof in _PRESET_REGISTRY.items() if fam == model_family})
if not names:
return "(none)"
return ", ".join(repr(n) for n in names)
__all__ = [
"InferencePreset",
"PresetStageSpec",
"get_all_preset_names",
"get_preset",
"get_presets_for_family",
"register_preset",
"validate_preset_selection",
"validate_stage_names",
"validate_stage_overrides",
]
+110 -96
View File
@@ -1,25 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
"""Track which GenerationRequest fields the user explicitly provided.
When translating a GenerationRequest into a legacy SamplingParam we must
distinguish user-provided values (which should override model defaults)
from schema defaults (which should NOT override model defaults).
This module solves a specific problem: when translating a GenerationRequest into
a legacy SamplingParam, we need to distinguish user-provided values (which
should override model defaults) from schema defaults (which should NOT override
model defaults).
The mechanism: a single ``_fastvideo_explicit_paths`` set stored on the
root ``GenerationRequest``. It holds dotted leaf paths (e.g.
``"sampling.guidance_scale"``) the user has touched, either via raw
config at bind time or via attribute assignment at runtime. A patched
``__setattr__`` on the request dataclass types records assignments into
this set.
The set holds leaf paths only. Nested dataclass or mapping assignments
are flattened to their leaves at record time.
The approach:
1. At bind time, store the original raw dict and a baseline snapshot.
2. Patch __setattr__ on tracked dataclass types to record dirty field paths.
3. At access time, do a lazy 3-way merge: raw + baseline + current state,
with dirty paths forcing inclusion even when current == baseline.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping
from collections.abc import Mapping
from copy import deepcopy
import dataclasses
from typing import Any, cast
from collections.abc import Callable
from fastvideo.api.schema import (
ContinuationState,
@@ -34,12 +33,12 @@ from fastvideo.api.schema import (
ServeConfig,
)
EXPLICIT_PATHS_ATTR = "_fastvideo_explicit_paths"
EXPLICIT_REQUEST_ATTR = "_fastvideo_explicit_request"
ORIGINAL_REQUEST_STATE_ATTR = "_fastvideo_original_request_state"
_TRACKING_ROOT_ATTR = "_fastvideo_request_tracking_root"
_TRACKING_PATH_ATTR = "_fastvideo_request_tracking_path"
_TRACKING_PATCHED_ATTR = "_fastvideo_request_tracking_patched"
_DIRTY_PATHS_ATTR = "_fastvideo_dirty_paths"
_TRACKED_REQUEST_TYPES = (
GenerationRequest,
InputConfig,
@@ -56,20 +55,14 @@ def bind_generation_request_raw(
request: GenerationRequest,
raw: Mapping[str, Any] | None,
) -> GenerationRequest:
"""Install explicit-path tracking on *request*.
*raw* is the parsed config dict (YAML/JSON/kwargs); every leaf key
in it becomes an explicit path. Subsequent attribute assignments on
*request* or its nested dataclasses are recorded automatically via a
patched ``__setattr__``.
"""
_ensure_request_tracking()
# Disable recording while we walk the tree to install roots.
object.__setattr__(request, EXPLICIT_PATHS_ATTR, None)
# Disable dirty tracking during bind so tree walk doesn't record paths.
object.__setattr__(request, _DIRTY_PATHS_ATTR, None)
object.__setattr__(request, EXPLICIT_REQUEST_ATTR, deepcopy(dict(raw or {})))
object.__setattr__(request, ORIGINAL_REQUEST_STATE_ATTR, _serialize_config(request))
_set_tracking_roots(request, request, "")
paths: set[str] = set()
_record_value_paths(raw or {}, "", paths)
object.__setattr__(request, EXPLICIT_PATHS_ATTR, paths)
# Enable dirty tracking.
object.__setattr__(request, _DIRTY_PATHS_ATTR, set())
return request
@@ -80,8 +73,6 @@ def bind_run_config_raw(
request_raw = raw.get("request")
if isinstance(request_raw, Mapping):
bind_generation_request_raw(config.request, request_raw)
else:
bind_generation_request_raw(config.request, {})
return config
@@ -92,72 +83,87 @@ def bind_serve_config_raw(
default_request_raw = raw.get("default_request")
if isinstance(default_request_raw, Mapping):
bind_generation_request_raw(config.default_request, default_request_raw)
else:
elif "default_request" not in raw:
bind_generation_request_raw(config.default_request, {})
return config
def get_explicit_paths(request: GenerationRequest) -> frozenset[str]:
"""Return a snapshot of the explicit paths set on *request*."""
paths = getattr(request, EXPLICIT_PATHS_ATTR, None)
if isinstance(paths, set | frozenset):
return frozenset(paths)
return frozenset()
def refresh_generation_request_raw(request: GenerationRequest, ) -> dict[str, Any] | None:
raw = getattr(request, EXPLICIT_REQUEST_ATTR, None)
baseline = getattr(request, ORIGINAL_REQUEST_STATE_ATTR, None)
if not isinstance(raw, Mapping) or not isinstance(baseline, Mapping):
return None
dirty = getattr(request, _DIRTY_PATHS_ATTR, None) or frozenset()
current = _serialize_config(request)
merged = deepcopy(dict(raw))
_merge_request_mutations(merged, dict(baseline), current, dirty)
def reset_tracking_roots(request: GenerationRequest) -> None:
"""Re-install tracking roots after a deepcopy or manual clone.
The paths set itself deepcopies correctly; we only need to repoint
the tracking root on nested dataclasses at the new root.
"""
_ensure_request_tracking()
_set_tracking_roots(request, request, "")
object.__setattr__(request, EXPLICIT_REQUEST_ATTR, merged)
object.__setattr__(request, ORIGINAL_REQUEST_STATE_ATTR, current)
object.__setattr__(request, _DIRTY_PATHS_ATTR, set())
return merged
# ---------------------------------------------------------------------------
# Path recording
# 3-way merge: raw + baseline + current, with dirty-path forcing
# ---------------------------------------------------------------------------
_MISSING = object()
def _record_value_paths(
value: Any,
prefix: str,
out: set[str],
def _merge_request_mutations(
merged: dict[str, Any],
baseline: Mapping[str, Any],
current: Mapping[str, Any],
dirty: frozenset[str] | set[str],
path_prefix: str = "",
force_dirty: bool = False,
) -> None:
"""Add every leaf path under *value* to *out*.
# Remove keys that were deleted from the current state.
for key in set(merged) | set(baseline):
if key not in current:
merged.pop(key, None)
A leaf is any terminal value (non-dataclass, non-mapping, or empty
mapping/dataclass). ``prefix`` is the dotted path at which *value*
sits. When called with an empty ``prefix`` (the root), leaves are
recorded at their own key.
"""
if dataclasses.is_dataclass(value) and not isinstance(value, type):
dc_fields = dataclasses.fields(value)
if not dc_fields:
if prefix:
out.add(prefix)
return
for field in dc_fields:
child = getattr(value, field.name)
path = f"{prefix}.{field.name}" if prefix else field.name
_record_value_paths(child, path, out)
return
if isinstance(value, Mapping):
if not value:
if prefix:
out.add(prefix)
return
for key, child in value.items():
path = f"{prefix}.{key}" if prefix else key
_record_value_paths(child, path, out)
return
if prefix:
out.add(prefix)
for key in current:
current_path = f"{path_prefix}.{key}" if path_prefix else key
current_value = current[key]
baseline_value = baseline.get(key, _MISSING)
merged_value = merged.get(key, _MISSING)
# If this exact path was dirtied (e.g. whole section replaced),
# propagate to all children.
child_force = force_dirty or current_path in dirty
# Recurse into nested mappings.
if isinstance(current_value, Mapping) and isinstance(baseline_value, Mapping):
nested = (deepcopy(dict(merged_value)) if isinstance(merged_value, Mapping) else {})
_merge_request_mutations(
nested,
baseline_value,
current_value,
dirty,
current_path,
child_force,
)
if nested:
merged[key] = nested
else:
merged.pop(key, None)
continue
# A field is explicitly set if:
# - it's new (not in baseline),
# - it changed from baseline,
# - its path was touched by __setattr__ (dirty), or
# - an ancestor path was dirty (whole section replaced).
is_dirty = child_force or current_path in dirty
if baseline_value is _MISSING or current_value != baseline_value or is_dirty:
merged[key] = deepcopy(current_value)
# ---------------------------------------------------------------------------
# __setattr__ patching
# __setattr__ patching for dirty-path recording
# ---------------------------------------------------------------------------
@@ -181,22 +187,15 @@ def _patch_tracking_setattr(config_type: type[Any]) -> None:
original_setattr(self, name, value)
return
original_setattr(self, name, value)
root = getattr(self, _TRACKING_ROOT_ATTR, None)
if root is None:
return
paths = getattr(root, EXPLICIT_PATHS_ATTR, None)
if not isinstance(paths, set):
return
if root is not None:
dirty = getattr(root, _DIRTY_PATHS_ATTR, None)
if isinstance(dirty, set):
prefix = getattr(self, _TRACKING_PATH_ATTR, "")
path = f"{prefix}.{name}" if prefix else name
dirty.add(path)
prefix = getattr(self, _TRACKING_PATH_ATTR, "")
path = f"{prefix}.{name}" if prefix else name
# Wholesale dataclass replacement: install roots on the new
# instance so its future mutations are tracked too.
if dataclasses.is_dataclass(value) and not isinstance(value, type):
_set_tracking_roots(root, value, path)
_record_value_paths(value, path, paths)
original_setattr(self, name, value)
type.__setattr__(config_type, "__setattr__", _tracking_setattr)
setattr(config_type, _TRACKING_PATCHED_ATTR, True)
@@ -223,11 +222,26 @@ def _set_tracking_roots(
_set_tracking_roots(root, child, child_path)
# ---------------------------------------------------------------------------
# Serialization helper
# ---------------------------------------------------------------------------
def _serialize_config(config: Any) -> Any:
if dataclasses.is_dataclass(config) and not isinstance(config, type):
return {field.name: _serialize_config(getattr(config, field.name)) for field in dataclasses.fields(config)}
if isinstance(config, list):
return [_serialize_config(item) for item in config]
if isinstance(config, dict):
return {key: _serialize_config(value) for key, value in config.items()}
return deepcopy(config)
__all__ = [
"EXPLICIT_PATHS_ATTR",
"EXPLICIT_REQUEST_ATTR",
"ORIGINAL_REQUEST_STATE_ATTR",
"bind_generation_request_raw",
"bind_run_config_raw",
"bind_serve_config_raw",
"get_explicit_paths",
"reset_tracking_roots",
"refresh_generation_request_raw",
]
+4 -84
View File
@@ -33,19 +33,8 @@ class OffloadConfig:
@dataclass
class CompileConfig:
"""Typed ``torch.compile`` configuration.
``backend``/``fullgraph``/``mode``/``dynamic`` are the four most
common ``torch.compile`` knobs. ``extras`` holds any remaining
``torch.compile`` kwargs (e.g. ``options``, ``disable``).
"""
enabled: bool = False
backend: str | None = None
fullgraph: bool | None = None
mode: str | None = None
dynamic: bool | None = None
extras: dict[str, Any] = field(default_factory=dict)
kwargs: dict[str, Any] = field(default_factory=dict)
@dataclass
@@ -84,12 +73,10 @@ class ComponentConfig:
@dataclass
class PipelineSelection:
workload_type: Literal["t2v", "i2v", "t2i", "i2i"] | None = None
preset: str | None = None
preset_version: int | None = None
profile: str | None = None
profile_version: str | None = None
components: ComponentConfig = field(default_factory=ComponentConfig)
vae_tiling: bool | None = None
"""Tile-based VAE decode. ``None`` keeps the model's default."""
preset_overrides: dict[str, Any] = field(default_factory=dict)
profile_overrides: dict[str, Any] = field(default_factory=dict)
experimental: dict[str, Any] = field(default_factory=dict)
@@ -193,73 +180,11 @@ class RunConfig:
request: GenerationRequest
@dataclass
class WarmupConfig:
enabled: bool = True
prompt: str = ("A cinematic drone shot over coastal cliffs at sunrise, "
"golden light, gentle ocean waves, ultra detailed")
timeout_seconds: int = 2400
@dataclass
class GpuPoolConfig:
num_workers: int | None = None
enable_audio_reencode: bool = True
conditioning_num_frames: int = 9
conditioning_end_offset: int = 0
@dataclass
class PromptEnhancerConfig:
enabled: bool = False
provider: Literal["cerebras", "groq"] = "cerebras"
model: str = "gpt-oss-120b"
timeout_ms: int = 20000
system_prompt_dir: str | None = None
@dataclass
class PromptSafetyConfig:
enabled: bool = False
classifier_path: str | None = None
@dataclass
class StreamingConfig:
session_timeout_seconds: int = 300
generation_segment_cap: int = 6
stream_mode: Literal["av_fmp4", "legacy_jpeg"] = "av_fmp4"
warmup: WarmupConfig = field(default_factory=WarmupConfig)
pool: GpuPoolConfig = field(default_factory=GpuPoolConfig)
prompt: PromptEnhancerConfig = field(default_factory=PromptEnhancerConfig)
safety: PromptSafetyConfig = field(default_factory=PromptSafetyConfig)
@dataclass
class ServeConfig:
"""Typed serve config loaded from ``fastvideo serve --config``.
``default_request`` is a full :class:`GenerationRequest` — the same type
clients POST to ``/v1/videos``. At request time the server merges it into
the incoming body as the operator-pinned baseline.
Important nuance: only fields the operator **explicitly wrote** in the
serve YAML/JSON count as defaults. Although the in-memory object is
fully populated (schema defaults fill every unset field), the merge
walks ``_fastvideo_explicit_paths`` — populated during parse — so
unset fields are *not* forced onto requests. Per-request precedence:
body (client-explicit) > default_request (operator-explicit)
> hardcoded fallback (e.g. ``fps=24``)
See :func:`fastvideo.api.compat.explicit_request_updates` for the
projection and ``entrypoints/openai/video_api.py::_build_generation_kwargs``
for the merge.
"""
generator: GeneratorConfig
server: ServerConfig = field(default_factory=ServerConfig)
default_request: GenerationRequest = field(default_factory=GenerationRequest)
streaming: StreamingConfig | None = None
__all__ = [
@@ -270,21 +195,16 @@ __all__ = [
"GenerationPlan",
"GenerationRequest",
"GeneratorConfig",
"GpuPoolConfig",
"InputConfig",
"OffloadConfig",
"OutputConfig",
"ParallelismConfig",
"PipelineSelection",
"PlannedStage",
"PromptEnhancerConfig",
"PromptSafetyConfig",
"QuantizationConfig",
"RequestRuntimeConfig",
"RunConfig",
"SamplingConfig",
"ServeConfig",
"ServerConfig",
"StreamingConfig",
"WarmupConfig",
]
+1 -1
View File
@@ -5,7 +5,7 @@ from fastvideo.configs.pipelines.hunyuan import FastHunyuanConfig, HunyuanConfig
from fastvideo.configs.pipelines.hunyuan15 import Hunyuan15T2V480PConfig, Hunyuan15T2V720PConfig
from fastvideo.configs.pipelines.hunyuangamecraft import HunyuanGameCraftPipelineConfig
from fastvideo.configs.pipelines.hyworld import HYWorldConfig
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
from fastvideo.configs.pipelines.ltx2 import LTX2T2VConfig
from fastvideo.registry import get_pipeline_config_cls_from_name
from fastvideo.configs.pipelines.wan import (SelfForcingWanT2V480PConfig, WanI2V480PConfig, WanI2V720PConfig,
WanT2V480PConfig, WanT2V720PConfig)
+13
View File
@@ -0,0 +1,13 @@
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.configs.sample.hunyuangamecraft import (
HunyuanGameCraftSamplingParam,
HunyuanGameCraft65FrameSamplingParam,
HunyuanGameCraft129FrameSamplingParam,
)
__all__ = [
"SamplingParam",
"HunyuanGameCraftSamplingParam",
"HunyuanGameCraft65FrameSamplingParam",
"HunyuanGameCraft129FrameSamplingParam",
]
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
import copy
from dataclasses import dataclass, field, fields
from dataclasses import dataclass
from typing import Any
from fastvideo.logger import init_logger
@@ -31,16 +30,6 @@ class SamplingParam:
# Camera control inputs (HYWorld)
pose: str | None = None # Camera trajectory: pose string (e.g., 'w-31') or JSON file path
prompt_attention_mask: list = field(default_factory=list)
negative_attention_mask: list = field(default_factory=list)
# Camera/action control inputs (GameCraft)
camera_states: Any | None = None # Plücker coordinates [B, T_video, 6, H, W]
camera_trajectory: str | None = None
action_list: list[str] | None = None
action_speed_list: list[float] | None = None
gt_latents: Any | None = None # Ground truth latents [B, 16, T, H, W]
conditioning_mask: Any | None = None # Mask [B, 1, T, H, W]
# Camera control inputs (LingBotWorld)
c2ws_plucker_emb: Any | None = None # Plucker embedding: [B, C, F_lat, H_lat, W_lat]
@@ -79,7 +68,6 @@ class SamplingParam:
num_inference_steps: int = 50
num_inference_steps_sr: int = 50
guidance_scale: float = 1.0
guidance_scale_2: float | None = None
guidance_rescale: float = 0.0
boundary_ratio: float | None = None
sigmas: list[float] | None = None
@@ -92,17 +80,6 @@ class SamplingParam:
movement_distance: float | None = None
camera_rotation: str | None = None
# LTX2 multi-modal CFG and STG
ltx2_cfg_scale_video: float = 3.0
ltx2_cfg_scale_audio: float = 7.0
ltx2_modality_scale_video: float = 3.0
ltx2_modality_scale_audio: float = 3.0
ltx2_rescale_scale: float = 0.7
ltx2_stg_scale_video: float = 1.0
ltx2_stg_scale_audio: float = 1.0
ltx2_stg_blocks_video: list[int] = field(default_factory=lambda: [29])
ltx2_stg_blocks_audio: list[int] = field(default_factory=lambda: [29])
# Misc
save_video: bool = True
return_frames: bool = True
@@ -117,58 +94,64 @@ class SamplingParam:
raise ValueError("prompt_path must be a txt file")
def update(self, source_dict: dict[str, Any]) -> None:
valid_fields = {f.name for f in fields(self)}
for key, value in source_dict.items():
if key in valid_fields:
if hasattr(self, key):
setattr(self, key, value)
else:
logger.error("%s has no field %s", type(self).__name__, key)
logger.exception("%s has no attribute %s", type(self).__name__, key)
self.__post_init__()
@classmethod
def from_pretrained(cls, model_path: str) -> "SamplingParam":
sampling_param = cls._from_preset(model_path)
if sampling_param is not None:
return sampling_param
def _from_profile(
cls,
profile_name: str,
) -> "SamplingParam":
"""Create a ``SamplingParam`` with profile defaults applied.
logger.warning(
"Couldn't find a preset for %s."
" Using the default sampling param.",
model_path,
)
return cls()
Looks up *profile_name* in the registered profile tables,
creates a base ``SamplingParam()``, then delegates to
:meth:`update` so that ``__post_init__`` is called and
derived fields stay consistent.
"""
from fastvideo.pipelines.basic.hunyuan.profiles import (
PROFILES as _hunyuan_profiles, )
# Merged lazily; extend this dict as more model families
# migrate to profile-based defaults.
all_profiles = _hunyuan_profiles
if profile_name not in all_profiles:
raise ValueError(f"Unknown profile '{profile_name}'. "
f"Available: {sorted(all_profiles)}")
instance = cls()
instance.update(all_profiles[profile_name].defaults)
return instance
@classmethod
def _from_preset(
cls,
model_path: str,
) -> "SamplingParam | None":
"""Build a SamplingParam from preset defaults.
def from_pretrained(cls, model_path: str) -> "SamplingParam":
from fastvideo.registry import _get_config_info
Returns ``None`` when no preset is configured for
*model_path*, letting the caller fall back to the legacy
subclass lookup.
"""
from fastvideo.registry import get_preset_selection
config_info = _get_config_info(model_path, raise_on_missing=False)
try:
preset_name, model_family = get_preset_selection(model_path)
except (ValueError, RuntimeError):
return None
if preset_name is None or model_family is None:
return None
if config_info is None:
logger.warning(
"Couldn't find an optimal sampling param "
"for %s. Using the default sampling param.",
model_path,
)
return cls()
from fastvideo.api.presets import get_preset
# Profile-based path (preferred for new migrations).
if config_info.default_profile is not None:
return cls._from_profile(config_info.default_profile)
preset = get_preset(preset_name, model_family)
sp = cls()
valid_fields = {f.name for f in fields(cls)}
for key, value in preset.defaults.items():
if key in valid_fields:
setattr(sp, key, copy.deepcopy(value))
sp.__post_init__()
return sp
# Legacy path: use the registered subclass directly.
if config_info.sampling_param_cls is not None:
return config_info.sampling_param_cls()
return cls()
@staticmethod
def add_cli_args(parser: Any) -> Any:
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class Cosmos_Predict2_2B_Video2World_SamplingParam(SamplingParam):
# Video parameters
height: int = 704
width: int = 1280
num_frames: int = 93
fps: int = 16
# Denoising stage
guidance_scale: float = 7.0
negative_prompt: str = "The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality."
num_inference_steps: int = 35
+23
View File
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class Cosmos25SamplingParamBase(SamplingParam):
height: int = 704
width: int = 1280
num_frames: int = 77
fps: int = 24
seed: int = 0
guidance_scale: float = 7.0
negative_prompt: str = (
"The video captures a series of frames showing ugly scenes, static with no motion, motion blur, "
"over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, "
"underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, "
"low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, "
"unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. "
"Overall, the video is of poor quality.")
num_inference_steps: int = 35
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class Gen3C_Cosmos_7B_SamplingParam(SamplingParam):
"""Defaults for GEN3C (Cosmos-7B) camera-controlled video generation."""
# Video parameters (matching official GEN3C defaults)
height: int = 704
width: int = 1280
num_frames: int = 121
fps: int = 24
# Denoising stage
guidance_scale: float = 1.0
num_inference_steps: int = 35
# GEN3C camera control defaults
trajectory_type: str = "left"
movement_distance: float = 0.3
camera_rotation: str = "center_facing"
+7
View File
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
"""Hunyuan sampling parameter classes.
Hunyuan model-specific SamplingParam subclasses have been removed.
Defaults are now provided by pipeline profiles in
``fastvideo/pipelines/basic/hunyuan/profiles.py``.
"""
+55
View File
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
import numpy as np
from dataclasses import dataclass, field
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class Hunyuan15_480P_SamplingParam(SamplingParam):
num_inference_steps: int = 50
num_frames: int = 121
height: int = 480
width: int = 848
fps: int = 24
guidance_scale: float = 6.0
sigmas: list[float] | None = field(default_factory=lambda: list(np.linspace(1.0, 0.0, 50 + 1)[:-1]))
negative_prompt: str = ""
def __post_init__(self):
super().__post_init__()
self.sigmas = list(np.linspace(1.0, 0.0, self.num_inference_steps + 1)[:-1])
@dataclass
class Hunyuan15_480P_StepDistilled_I2V_SamplingParam(Hunyuan15_480P_SamplingParam):
num_inference_steps: int = 12
height: int = 720
width: int = 1280
guidance_scale: float = 1.0
@dataclass
class Hunyuan15_720P_SamplingParam(Hunyuan15_480P_SamplingParam):
height: int = 720
width: int = 1280
@dataclass
class Hunyuan15_720P_Distilled_I2V_SamplingParam(Hunyuan15_720P_SamplingParam):
guidance_scale: float = 1.0
@dataclass
class Hunyuan15_SR_1080P_SamplingParam(Hunyuan15_480P_SamplingParam):
height_sr: int = 1072
width_sr: int = 1920
num_inference_steps: int = 12
num_inference_steps_sr: int = 8
guidance_scale: float = 1.0
@@ -0,0 +1,92 @@
# SPDX-License-Identifier: Apache-2.0
"""
Sampling parameters for HunyuanGameCraft video generation.
GameCraft generates game-like videos with camera/action control.
Default parameters are based on the official implementation.
"""
from dataclasses import dataclass
from typing import Any
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class HunyuanGameCraftSamplingParam(SamplingParam):
"""Sampling parameters for HunyuanGameCraft video generation.
Supports camera/action conditioning via:
- camera_trajectory: Plücker coordinates for camera motion
- action_list: List of actions (e.g., ["forward", "left", "right"])
- action_speed_list: Speed multipliers for each action
Default resolution is 704x1280 (same as HunyuanVideo).
Default frame count is 33 video frames -> 9 latent frames.
"""
# Number of denoising steps
num_inference_steps: int = 50
# Video dimensions
# 33 video frames -> 9 latent frames (4x temporal compression)
num_frames: int = 33
height: int = 704
width: int = 1280
fps: int = 24
# Guidance scale - official GameCraft uses CFG with guidance_scale=6.0
guidance_scale: float = 6.0
# Negative prompt for CFG (empty string = unconditional)
negative_prompt: str = ""
# Camera/Action conditioning
# Camera states as Plücker coordinates [B, T_video, 6, H, W]
camera_states: Any | None = None
# Camera trajectory file/identifier (alternative to camera_states)
camera_trajectory: str | None = None
# Action list for camera motion (e.g., ["forward", "left"])
action_list: list[str] | None = None
# Speed multipliers for each action
action_speed_list: list[float] | None = None
# History frame conditioning (for autoregressive generation)
# Ground truth latents for conditioning [B, 16, T, H, W]
gt_latents: Any | None = None
# Mask for conditioning (1=use gt, 0=generate) [B, 1, T, H, W]
conditioning_mask: Any | None = None
# Number of conditioning frames (for autoregressive) - maps to num_cond_frames
num_cond_frames: int = 0
def __post_init__(self) -> None:
super().__post_init__()
# Validate action lists
if (self.action_list is not None and self.action_speed_list is not None
and len(self.action_list) != len(self.action_speed_list)):
raise ValueError(f"action_list length ({len(self.action_list)}) must match "
f"action_speed_list length ({len(self.action_speed_list)})")
@dataclass
class HunyuanGameCraft65FrameSamplingParam(HunyuanGameCraftSamplingParam):
"""Sampling parameters for 65-frame GameCraft generation.
65 video frames -> 17 latent frames (with first frame as key frame).
This is useful for longer video generation.
"""
num_frames: int = 65
@dataclass
class HunyuanGameCraft129FrameSamplingParam(HunyuanGameCraftSamplingParam):
"""Sampling parameters for 129-frame GameCraft generation.
129 video frames -> 33 latent frames.
This is the maximum supported by the official implementation.
"""
num_frames: int = 129
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.configs.sample.base import SamplingParam
import numpy as np
@dataclass
class HYWorld_SamplingParam(SamplingParam):
num_inference_steps: int = 50
num_frames: int = 125
height: int = 480
width: int = 832
fps: int = 24
# Camera trajectory: pose string (e.g., 'w-31' means generating [1 + 31] latents) or JSON file path
pose: str = 'w-31'
guidance_scale: float = 6.0
prompt_attention_mask: list = field(default_factory=list)
negative_attention_mask: list = field(default_factory=list)
sigmas: list[float] | None = field(default_factory=lambda: list(np.linspace(1.0, 0.0, 50 + 1)[:-1]))
negative_prompt: str = ""
+20
View File
@@ -0,0 +1,20 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from fastvideo.configs.sample.wan import Wan2_2_I2V_A14B_SamplingParam
@dataclass
class LingBotWorld_SamplingParam(Wan2_2_I2V_A14B_SamplingParam):
guidance_scale: float = 5.0 # high_noise
guidance_scale_2: float = 5.0 # low_noise
num_inference_steps: int = 70
boundary_ratio: float | None = 0.947
negative_prompt: str | None = ("画面突变,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,"
"最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,"
"畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,"
"镜头晃动,画面闪烁,模糊,噪点,水印,签名,文字,变形,扭曲,液化,不合逻辑的结构,卡顿,"
"PPT幻灯片感,过暗,欠曝,低对比度,霓虹灯光感,过度锐化,3D渲染感,人物,行人,游客,身体,"
"皮肤,肢体,面部特征,汽车,电线")
fps: int = 16
# NOTE(will): default boundary timestep is tracked by PipelineConfig, but
# can be overridden during sampling
+69
View File
@@ -0,0 +1,69 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class LTX2BaseSamplingParam(SamplingParam):
"""Default sampling parameters for LTX-2 base one-stage T2V.
Values follow the official LTX-2 one-stage defaults.
Multi-modal CFG params are read by ``LTX2DenoisingStage``.
"""
seed: int = 10
num_frames: int = 121
height: int = 512
width: int = 768
fps: int = 24
num_inference_steps: int = 40
guidance_scale: float = 3.0
# Copied/following official LTX-2 DEFAULT_NEGATIVE_PROMPT.
negative_prompt: str = ("blurry, out of focus, overexposed, underexposed, low contrast, "
"washed out colors, excessive noise, grainy texture, poor lighting, "
"flickering, motion blur, distorted proportions, unnatural skin "
"tones, deformed facial features, asymmetrical face, missing facial "
"features, extra limbs, disfigured hands, wrong hand count, "
"artifacts around text, inconsistent perspective, camera shake, "
"incorrect depth of field, background too sharp, background clutter, "
"distracting reflections, harsh shadows, inconsistent lighting "
"direction, color banding, cartoonish rendering, 3D CGI look, "
"unrealistic materials, uncanny valley effect, incorrect ethnicity, "
"wrong gender, exaggerated expressions, wrong gaze direction, "
"mismatched lip sync, silent or muted audio, distorted voice, "
"robotic voice, echo, background noise, off-sync audio, incorrect "
"dialogue, added dialogue, repetitive speech, jittery movement, "
"awkward pauses, incorrect timing, unnatural transitions, "
"inconsistent framing, tilted camera, flat lighting, inconsistent "
"tone, cinematic oversaturation, stylized filters, or AI artifacts.")
# Official LTX-2 multi-modal CFG defaults.
ltx2_cfg_scale_video: float = 3.0
ltx2_cfg_scale_audio: float = 7.0
ltx2_modality_scale_video: float = 3.0
ltx2_modality_scale_audio: float = 3.0
ltx2_rescale_scale: float = 0.7
# STG (Spatio-Temporal Guidance) defaults from official LTX-2.
ltx2_stg_scale_video: float = 1.0
ltx2_stg_scale_audio: float = 1.0
ltx2_stg_blocks_video: list[int] = field(default_factory=lambda: [29])
ltx2_stg_blocks_audio: list[int] = field(default_factory=lambda: [29])
@dataclass
class LTX2DistilledSamplingParam(SamplingParam):
"""Default sampling parameters for LTX-2 distilled one-stage T2V."""
seed: int = 10
num_frames: int = 121
height: int = 1024
width: int = 1536
fps: int = 24
num_inference_steps: int = 8
guidance_scale: float = 1.0
# No default negative_prompt for distilled models
negative_prompt: str = ""
# Backward compatibility alias.
LTX2SamplingParam = LTX2DistilledSamplingParam
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class SD35SamplingParam(SamplingParam):
prompt: str | None = "a photo of a cat"
negative_prompt: str = ""
num_videos_per_prompt: int = 1
seed: int = 0
num_frames: int = 1
height: int = 512
width: int = 512
fps: int = 1
num_inference_steps: int = 28
guidance_scale: float = 6.0
@@ -0,0 +1,73 @@
# SPDX-License-Identifier: Apache-2.0
"""
TurboDiffusion sampling parameters.
TurboDiffusion uses RCM (recurrent Consistency Model) scheduler for
1-4 step video generation with no classifier-free guidance.
"""
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class TurboDiffusionT2V_1_3B_SamplingParam(SamplingParam):
"""Sampling parameters for TurboDiffusion T2V 1.3B model.
Uses 4-step RCM sampling with guidance_scale=1.0 (no CFG).
"""
# Video parameters
height: int = 480
width: int = 832
num_frames: int = 81
fps: int = 16
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
guidance_scale: float = 1.0
num_inference_steps: int = 4
# No negative prompt needed for TurboDiffusion (no CFG)
negative_prompt: str | None = None
@dataclass
class TurboDiffusionT2V_14B_SamplingParam(SamplingParam):
"""Sampling parameters for TurboDiffusion T2V 14B model.
Uses 4-step RCM sampling with guidance_scale=1.0 (no CFG).
"""
# Video parameters (720p for 14B)
height: int = 720
width: int = 1280
num_frames: int = 81
fps: int = 16
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
guidance_scale: float = 1.0
num_inference_steps: int = 4
# No negative prompt needed for TurboDiffusion (no CFG)
negative_prompt: str | None = None
@dataclass
class TurboDiffusionI2V_A14B_SamplingParam(SamplingParam):
"""Sampling parameters for TurboDiffusion I2V A14B model.
Uses 4-step RCM sampling with dual-model switching (high/low noise).
"""
# Video parameters (720p for A14B I2V)
height: int = 720
width: int = 1280
num_frames: int = 81
fps: int = 16
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
guidance_scale: float = 1.0
num_inference_steps: int = 4
# Note: boundary_ratio is set in the pipeline config (TurboDiffusionI2VConfig),
# not here. This keeps sampling params and pipeline config separate.
# No negative prompt needed for TurboDiffusion (no CFG)
negative_prompt: str | None = None
+154
View File
@@ -0,0 +1,154 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from fastvideo.configs.sample.base import SamplingParam
@dataclass
class WanT2V_1_3B_SamplingParam(SamplingParam):
# Video parameters
height: int = 480
width: int = 832
num_frames: int = 81
fps: int = 16
# Denoising stage
guidance_scale: float = 3.0
negative_prompt: str = "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards"
num_inference_steps: int = 50
@dataclass
class WanT2V_14B_SamplingParam(SamplingParam):
# Video parameters
height: int = 720
width: int = 1280
num_frames: int = 81
fps: int = 16
# Denoising stage
guidance_scale: float = 5.0
negative_prompt: str = "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards"
num_inference_steps: int = 50
@dataclass
class WanI2V_14B_480P_SamplingParam(WanT2V_1_3B_SamplingParam):
# Denoising stage
guidance_scale: float = 5.0
num_inference_steps: int = 40
@dataclass
class WanI2V_14B_720P_SamplingParam(WanT2V_14B_SamplingParam):
# Denoising stage
guidance_scale: float = 5.0
num_inference_steps: int = 40
@dataclass
class FastWanT2V480P_SamplingParam(WanT2V_1_3B_SamplingParam):
# DMD parameters
# dmd_denoising_steps: list[int] | None = field(default_factory=lambda: [1000, 757, 522])
num_inference_steps: int = 3
num_frames: int = 61
height: int = 448
width: int = 832
fps: int = 16
# =============================================
# ============= Wan2.1 Fun Models =============
# =============================================
@dataclass
class Wan2_1_Fun_1_3B_InP_SamplingParam(SamplingParam):
"""Sampling parameters for Wan2.1 Fun 1.3B InP model."""
height: int = 480
width: int = 832
num_frames: int = 81
fps: int = 16
negative_prompt: str | None = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
guidance_scale: float = 6.0
num_inference_steps: int = 50
@dataclass
class Wan2_1_Fun_1_3B_Control_SamplingParam(SamplingParam):
fps: int = 16
num_frames: int = 49
height: int = 832
width: int = 480
guidance_scale: float = 6.0
# =============================================
# ============= Wan2.2 TI2V Models =============
# =============================================
@dataclass
class Wan2_2_Base_SamplingParam(SamplingParam):
"""Sampling parameters for Wan2.2 TI2V 5B model."""
negative_prompt: str | None = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
@dataclass
class Wan2_2_TI2V_5B_SamplingParam(Wan2_2_Base_SamplingParam):
"""Sampling parameters for Wan2.2 TI2V 5B model."""
height: int = 704
width: int = 1280
num_frames: int = 121
fps: int = 24
guidance_scale: float = 5.0
num_inference_steps: int = 50
@dataclass
class Wan2_2_T2V_A14B_SamplingParam(Wan2_2_Base_SamplingParam):
guidance_scale: float = 4.0 # high_noise
guidance_scale_2: float = 3.0 # low_noise
num_inference_steps: int = 40
fps: int = 16
# NOTE(will): default boundary timestep is tracked by PipelineConfig, but
# can be overridden during sampling
@dataclass
class Wan2_2_I2V_A14B_SamplingParam(Wan2_2_Base_SamplingParam):
guidance_scale: float = 3.5 # high_noise
guidance_scale_2: float = 3.5 # low_noise
num_inference_steps: int = 40
fps: int = 16
# NOTE(will): default boundary timestep is tracked by PipelineConfig, but
# can be overridden during sampling
@dataclass
class Wan2_2_Fun_A14B_Control_SamplingParam(Wan2_1_Fun_1_3B_Control_SamplingParam):
num_frames: int = 81
# =============================================
# ============= Causal Self-Forcing =============
# =============================================
@dataclass
class SelfForcingWan2_1_T2V_1_3B_480P_SamplingParam(Wan2_1_Fun_1_3B_InP_SamplingParam):
pass
@dataclass
class SelfForcingWan2_2_T2V_A14B_480P_SamplingParam(Wan2_2_T2V_A14B_SamplingParam):
num_inference_steps: int = 8
num_frames: int = 81
height: int = 448
width: int = 832
fps: int = 16
@dataclass
class MatrixGame2_SamplingParam(SamplingParam):
height: int = 352
width: int = 640
num_frames: int = 57
fps: int = 25
guidance_scale: float = 1.0
num_inference_steps: int = 3
negative_prompt: str | None = None
+10 -11
View File
@@ -6,6 +6,7 @@ import os
from typing import cast
from fastvideo.api.compat import generator_config_to_fastvideo_args
from fastvideo.api.request_metadata import EXPLICIT_REQUEST_ATTR
from fastvideo.entrypoints.cli.cli_types import CLISubcommand
from fastvideo.entrypoints.cli.inference_config import build_serve_config
from fastvideo.logger import init_logger
@@ -29,20 +30,19 @@ class ServeSubcommand(CLISubcommand):
args,
overrides=getattr(args, "_unknown", None),
)
logger.info("CLI serve config: %s", serve_config)
# A `streaming:` block selects the WebSocket/Dynamo runtime;
# its deps stay out of REST-only deployments via lazy import.
if serve_config.streaming is not None:
from fastvideo.entrypoints.streaming.server import (
run_server as run_streaming_server, )
run_streaming_server(serve_config)
return
explicit_raw = getattr(
serve_config.default_request,
EXPLICIT_REQUEST_ATTR,
None,
)
if explicit_raw:
raise NotImplementedError("ServeConfig.default_request is not wired into the OpenAI "
"server yet")
from fastvideo.entrypoints.openai.api_server import (
run_server, )
logger.info("CLI serve config: %s", serve_config)
logger.info(
"Server will listen on %s:%d",
serve_config.server.host,
@@ -55,7 +55,6 @@ class ServeSubcommand(CLISubcommand):
host=serve_config.server.host,
port=serve_config.server.port,
output_dir=serve_config.server.output_dir,
default_request=serve_config.default_request,
)
def validate(self, args: argparse.Namespace) -> None:
+2 -38
View File
@@ -8,8 +8,6 @@ import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastvideo.api.presets import validate_preset_selection
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.state import (
DEFAULT_OUTPUT_DIR,
clear_state,
@@ -18,7 +16,6 @@ from fastvideo.entrypoints.openai.state import (
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
from fastvideo.registry import get_preset_selection
logger = init_logger(__name__)
@@ -26,40 +23,17 @@ DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8000
def _validate_default_request_against_preset(
default_request: GenerationRequest,
model_path: str,
) -> None:
"""Validate ``default_request.stage_overrides`` against the model's preset.
Called once at server startup from :func:`run_server`. The
``default_request`` is static server config, so validation results are
invariant across requests — there's no reason to re-run per request.
"""
if not default_request.stage_overrides:
return
preset_name, model_family = get_preset_selection(model_path)
if preset_name is None or model_family is None:
return
validate_preset_selection(
preset_name,
model_family,
stage_overrides=default_request.stage_overrides,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Load model on startup, clean up on shutdown"""
args: FastVideoArgs = app.state.fastvideo_args
output_dir: str = app.state.output_dir
default_request: GenerationRequest | None = getattr(app.state, "default_request", None)
logger.info("Loading model from %s ...", args.model_path)
generator = VideoGenerator.from_fastvideo_args(args)
logger.info("Model loaded successfully.")
set_state(generator, args, output_dir, default_request=default_request)
set_state(generator, args, output_dir)
yield # server is running
@@ -72,7 +46,6 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
def create_app(
fastvideo_args: FastVideoArgs,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
) -> FastAPI:
"""Build the FastAPI application with all routers mounted"""
@@ -83,7 +56,6 @@ def create_app(
)
app.state.fastvideo_args = fastvideo_args
app.state.output_dir = output_dir
app.state.default_request = default_request
app.add_middleware(
CORSMiddleware,
@@ -136,17 +108,9 @@ def run_server(
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
):
"""Create the app and run it with uvicorn"""
if default_request is not None:
_validate_default_request_against_preset(default_request, fastvideo_args.model_path)
app = create_app(
fastvideo_args,
output_dir=output_dir,
default_request=default_request,
)
app = create_app(fastvideo_args, output_dir=output_dir)
logger.info("Starting FastVideo server on %s:%d", host, port)
logger.info("Model: %s", fastvideo_args.model_path)
+2 -12
View File
@@ -10,7 +10,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import FastVideoArgs
@@ -19,7 +18,6 @@ DEFAULT_OUTPUT_DIR = "outputs"
_generator: VideoGenerator | None = None
_fastvideo_args: FastVideoArgs | None = None
_output_dir: str = DEFAULT_OUTPUT_DIR
_default_request: GenerationRequest | None = None
def get_generator() -> VideoGenerator:
@@ -39,28 +37,20 @@ def get_output_dir() -> str:
return _output_dir
def get_default_request() -> GenerationRequest | None:
"""Return the ServeConfig.default_request set at startup, if any."""
return _default_request
def set_state(
generator: VideoGenerator,
fastvideo_args: FastVideoArgs,
output_dir: str,
default_request: GenerationRequest | None = None,
) -> None:
"""Set all server state at once (called from lifespan)."""
global _generator, _fastvideo_args, _output_dir, _default_request
global _generator, _fastvideo_args, _output_dir
_generator = generator
_fastvideo_args = fastvideo_args
_output_dir = output_dir
_default_request = default_request
def clear_state() -> None:
"""Clear server state on shutdown."""
global _generator, _fastvideo_args, _default_request
global _generator, _fastvideo_args
_generator = None
_fastvideo_args = None
_default_request = None
+21 -53
View File
@@ -19,10 +19,7 @@ from fastapi import (
)
from fastapi.responses import FileResponse
from fastvideo.api.compat import explicit_request_updates
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.state import (
get_default_request,
get_generator,
get_output_dir,
get_server_args,
@@ -45,73 +42,49 @@ logger = init_logger(__name__)
router = APIRouter(prefix="/v1/videos", tags=["videos"])
def _build_generation_kwargs(
request_id: str,
req: VideoGenerationsRequest,
default_request: GenerationRequest | None = None,
) -> dict[str, Any]:
"""Build a flat kwargs dict for ``generator.generate_video``.
def _build_generation_kwargs(request_id: str, req: VideoGenerationsRequest) -> dict[str, Any]:
Precedence (highest to lowest):
1. Request body — only fields the client explicitly sent
(``req.model_fields_set``, Pydantic v2).
2. ``default_request`` — only fields the operator explicitly set in
the serve YAML, projected via ``explicit_request_updates``. Schema
defaults on the dataclass are *not* treated as defaults here.
3. Hardcoded fallback (e.g. ``fps=24`` when neither side set it).
Why gate on ``model_fields_set`` / explicit paths? Both the request
Pydantic model and the ``GenerationRequest`` dataclass carry schema
defaults (e.g. ``seed=1024``, ``num_frames=125``). Without the gate
those would masquerade as intent and shadow the other side — the
gate preserves "operator pinned it" vs. "dataclass happened to have
that default."
"""
kwargs: dict[str, Any] = {}
if default_request is not None:
kwargs.update(explicit_request_updates(default_request))
body_set = req.model_fields_set
kwargs["prompt"] = req.prompt
if "size" in body_set and req.size:
# Resolution
if req.size:
w, h = parse_size(req.size)
if w is not None and h is not None:
kwargs["width"] = w
kwargs["height"] = h
if "fps" in body_set and req.fps is not None:
kwargs["fps"] = req.fps
# Frame count / duration
fps = req.fps if req.fps is not None else 24
kwargs["fps"] = fps
if "num_frames" in body_set and req.num_frames is not None:
if req.num_frames is not None:
kwargs["num_frames"] = req.num_frames
elif "seconds" in body_set and req.seconds is not None:
fps = kwargs.get("fps", 24)
elif req.seconds is not None:
kwargs["num_frames"] = fps * req.seconds
if "seed" in body_set and req.seed is not None:
# Sampling parameters
if req.seed is not None:
kwargs["seed"] = req.seed
if ("num_inference_steps" in body_set and req.num_inference_steps is not None):
if req.num_inference_steps is not None:
kwargs["num_inference_steps"] = req.num_inference_steps
if "guidance_scale" in body_set and req.guidance_scale is not None:
if req.guidance_scale is not None:
kwargs["guidance_scale"] = req.guidance_scale
if "guidance_scale_2" in body_set and req.guidance_scale_2 is not None:
if req.guidance_scale_2 is not None:
kwargs["guidance_scale_2"] = req.guidance_scale_2
if "negative_prompt" in body_set and req.negative_prompt is not None:
if req.negative_prompt is not None:
kwargs["negative_prompt"] = req.negative_prompt
if "enable_teacache" in body_set and req.enable_teacache:
if req.enable_teacache:
kwargs["enable_teacache"] = True
if "true_cfg_scale" in body_set and req.true_cfg_scale is not None:
if req.true_cfg_scale is not None:
kwargs["true_cfg_scale"] = req.true_cfg_scale
if "input_reference" in body_set and req.input_reference is not None:
# Image-to-video input
if req.input_reference is not None:
kwargs["image_path"] = req.input_reference
kwargs.setdefault("fps", 24)
default_output_path = kwargs.pop("output_path", None)
body_output_dir = req.output_path if "output_path" in body_set else None
output_dir = body_output_dir or default_output_path or os.path.join(get_output_dir(), "videos")
# Output path
output_dir = req.output_path or os.path.join(get_output_dir(), "videos")
os.makedirs(output_dir, exist_ok=True)
kwargs["output_path"] = os.path.join(output_dir, f"{request_id}.mp4")
kwargs["save_video"] = True
@@ -299,12 +272,7 @@ async def create_video(
logger.info("Video generation request %s: prompt=%s", request_id, req.prompt[:100])
# default_request was validated at server startup (run_server) and is
# read-only on the request hot path — _build_generation_kwargs and
# explicit_request_updates only read, so no per-request deepcopy needed.
default_request = get_default_request()
gen_kwargs = _build_generation_kwargs(request_id, req, default_request=default_request)
gen_kwargs = _build_generation_kwargs(request_id, req)
job = _make_video_job(request_id, req, gen_kwargs)
await VIDEO_STORE.upsert(request_id, job)
@@ -1,4 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo.entrypoints.streaming.server import run_server
__all__ = ["run_server"]
-15
View File
@@ -1,15 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from fastvideo.api.schema import ServeConfig
from fastvideo.logger import init_logger
logger = init_logger(__name__)
def run_server(serve_config: ServeConfig) -> None:
"""Launch the streaming (WebSocket / Dynamo) server."""
if serve_config.streaming is None:
raise ValueError("ServeConfig.streaming must be set to launch the streaming server; "
"got None. Add a `streaming:` block to your serve config.")
raise NotImplementedError("streaming server is not implemented yet")
+1 -1
View File
@@ -8,7 +8,7 @@ import torch
import torchvision
from einops import rearrange
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
+1 -1
View File
@@ -35,7 +35,7 @@ from fastvideo.api.compat import (
)
from fastvideo.api.results import GenerationResult
from fastvideo.api.schema import GenerationRequest, GeneratorConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
from fastvideo.pipelines import ForwardBatch
@@ -1,84 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos model family pipeline presets.
Covers both Cosmos Predict2 and Cosmos Predict2.5, which share the
same pipeline directory but have distinct model families.
"""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
# -------------------------------------------------------------------
# Cosmos Predict2
# -------------------------------------------------------------------
_COSMOS_NEGATIVE_PROMPT = ("The video captures a series of frames showing ugly scenes, "
"static with no motion, motion blur, over-saturation, shaky "
"footage, low resolution, grainy texture, pixelated images, "
"poorly lit areas, underexposed and overexposed scenes, poor "
"color balance, washed out colors, choppy sequences, jerky "
"movements, low frame rate, artifacting, color banding, "
"unnatural transitions, outdated special effects, fake elements, "
"unconvincing visuals, poorly edited content, jump cuts, visual "
"noise, and flickering. Overall, the video is of poor quality.")
COSMOS_PREDICT2_2B = InferencePreset(
name="cosmos_predict2_2b",
version=1,
model_family="cosmos",
description="Cosmos Predict2 2B Video2World",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 93,
"fps": 16,
"guidance_scale": 7.0,
"num_inference_steps": 35,
"negative_prompt": _COSMOS_NEGATIVE_PROMPT,
},
)
# -------------------------------------------------------------------
# Cosmos Predict2.5
# -------------------------------------------------------------------
_COSMOS25_NEGATIVE_PROMPT = ("The video captures a series of frames showing ugly scenes, "
"static with no motion, motion blur, over-saturation, shaky "
"footage, low resolution, grainy texture, pixelated images, "
"poorly lit areas, underexposed and overexposed scenes, poor "
"color balance, washed out colors, choppy sequences, jerky "
"movements, low frame rate, artifacting, color banding, "
"unnatural transitions, outdated special effects, fake elements, "
"unconvincing visuals, poorly edited content, jump cuts, visual "
"noise, and flickering. Overall, the video is of poor quality.")
COSMOS25_PREDICT2_2B = InferencePreset(
name="cosmos25_predict2_2b",
version=1,
model_family="cosmos25",
description="Cosmos Predict2.5 2B",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"seed": 0,
"height": 704,
"width": 1280,
"num_frames": 77,
"fps": 24,
"guidance_scale": 7.0,
"num_inference_steps": 35,
"negative_prompt": _COSMOS25_NEGATIVE_PROMPT,
},
)
ALL_PRESETS = (COSMOS_PREDICT2_2B, COSMOS25_PREDICT2_2B)
@@ -1,33 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""HunyuanGameCraft model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Action-controlled denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
GAMECRAFT_I2V = InferencePreset(
name="gamecraft_i2v",
version=1,
model_family="gamecraft",
description="HunyuanGameCraft I2V at 704x1280",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 33,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": "",
},
)
ALL_PRESETS = (GAMECRAFT_I2V, )
@@ -1,35 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""GEN3C model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Camera-controlled denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
GEN3C_COSMOS_7B = InferencePreset(
name="gen3c_cosmos_7b",
version=1,
model_family="gen3c",
description="GEN3C Cosmos 7B",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 35,
"trajectory_type": "left",
"movement_distance": 0.3,
"camera_rotation": "center_facing",
},
)
ALL_PRESETS = (GEN3C_COSMOS_7B, )
@@ -1,49 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Hunyuan model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
HUNYUAN_T2V = InferencePreset(
name="hunyuan_t2v",
version=1,
model_family="hunyuan",
description="HunyuanVideo T2V at 720p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 125,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 50,
},
)
FAST_HUNYUAN_T2V = InferencePreset(
name="fast_hunyuan_t2v",
version=1,
model_family="hunyuan",
description="FastHunyuan T2V at 720p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 125,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 6,
},
)
ALL_PRESETS = (HUNYUAN_T2V, FAST_HUNYUAN_T2V)
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: Apache-2.0
"""Pipeline profiles for Hunyuan model family.
Each profile defines default sampling parameters that differ from the
base ``SamplingParam`` defaults. The registry points a model to its
``default_profile`` name, and ``SamplingParam._from_profile`` applies
the profile's ``defaults`` dict onto a freshly-constructed base
instance.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class ProfileEntry:
"""Immutable description of a pipeline profile."""
defaults: dict[str, Any]
# Hunyuan base: all fields match SamplingParam defaults, so the
# defaults dict is empty. The profile still exists so that the
# registry can reference it.
HUNYUAN_T2V = ProfileEntry(defaults={})
# FastHunyuan: only num_inference_steps differs from base.
FAST_HUNYUAN_T2V = ProfileEntry(defaults={
"num_inference_steps": 6,
})
# Name -> ProfileEntry lookup used by SamplingParam._from_profile.
PROFILES: dict[str, ProfileEntry] = {
"hunyuan_t2v": HUNYUAN_T2V,
"fast_hunyuan_t2v": FAST_HUNYUAN_T2V,
}
@@ -1,152 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Hunyuan 1.5 model family pipeline presets."""
import numpy as np
from fastvideo.api.presets import InferencePreset, PresetStageSpec
def _sigmas(n: int) -> list[float]:
"""Precompute sigmas schedule for *n* inference steps."""
return np.linspace(1.0, 0.0, n + 1).tolist()[:-1]
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
_SR_STAGE = PresetStageSpec(
name="sr",
kind="super_resolution",
description="Super-resolution upscaling pass",
allowed_overrides=frozenset({
"num_inference_steps",
}),
)
# -------------------------------------------------------------------
# Hunyuan 1.5 T2V presets
# -------------------------------------------------------------------
HUNYUAN15_T2V_480P = InferencePreset(
name="hunyuan15_t2v_480p",
version=1,
model_family="hunyuan15",
description="HunyuanVideo 1.5 T2V at 480p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 848,
"num_frames": 121,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": "",
"sigmas": _sigmas(50),
},
)
HUNYUAN15_T2V_720P = InferencePreset(
name="hunyuan15_t2v_720p",
version=1,
model_family="hunyuan15",
description="HunyuanVideo 1.5 T2V at 720p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": "",
"sigmas": _sigmas(50),
},
)
# -------------------------------------------------------------------
# Hunyuan 1.5 I2V presets
# -------------------------------------------------------------------
HUNYUAN15_I2V_480P_DISTILLED = InferencePreset(
name="hunyuan15_i2v_480p_distilled",
version=1,
model_family="hunyuan15",
description="HunyuanVideo 1.5 I2V 480p step-distilled",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 12,
"negative_prompt": "",
"sigmas": _sigmas(12),
},
)
HUNYUAN15_I2V_720P_DISTILLED = InferencePreset(
name="hunyuan15_i2v_720p_distilled",
version=1,
model_family="hunyuan15",
description="HunyuanVideo 1.5 I2V 720p distilled",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 50,
"negative_prompt": "",
"sigmas": _sigmas(50),
},
)
# -------------------------------------------------------------------
# Hunyuan 1.5 SR preset (two-stage)
# -------------------------------------------------------------------
HUNYUAN15_SR_1080P = InferencePreset(
name="hunyuan15_sr_1080p",
version=1,
model_family="hunyuan15",
description="HunyuanVideo 1.5 SR to 1080p (two-stage)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, _SR_STAGE),
defaults={
"height": 480,
"width": 848,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 12,
"negative_prompt": "",
"sigmas": _sigmas(12),
},
stage_defaults={
"sr": {
"height_sr": 1072,
"width_sr": 1920,
"num_inference_steps": 8,
},
},
)
ALL_PRESETS = (
HUNYUAN15_T2V_480P,
HUNYUAN15_T2V_720P,
HUNYUAN15_I2V_480P_DISTILLED,
HUNYUAN15_I2V_720P_DISTILLED,
HUNYUAN15_SR_1080P,
)
@@ -1,39 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""HYWorld model family pipeline presets."""
import numpy as np
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_HYWORLD_SIGMAS = list(np.linspace(1.0, 0.0, 51).tolist()[:-1])
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Camera-controlled denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
HYWORLD_T2V = InferencePreset(
name="hyworld_t2v",
version=1,
model_family="hyworld",
description="HY-WorldPlay bidirectional at 480p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 125,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": "",
"pose": "w-31",
"sigmas": _HYWORLD_SIGMAS,
},
)
ALL_PRESETS = (HYWORLD_T2V, )
@@ -1,44 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBotWorld model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Dual-guidance denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
"guidance_scale_2",
"boundary_ratio",
}),
)
LINGBOTWORLD_I2V = InferencePreset(
name="lingbotworld_i2v",
version=1,
model_family="lingbotworld",
description="LingBot-World I2V with dual guidance",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"guidance_scale":
5.0,
"guidance_scale_2":
5.0,
"num_inference_steps":
70,
"fps":
16,
"boundary_ratio":
0.947,
"negative_prompt": ("画面突变,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,"
"最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,"
"畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,"
"镜头晃动,画面闪烁,模糊,噪点,水印,签名,文字,变形,扭曲,液化,不合逻辑的结构,卡顿,"
"PPT幻灯片感,过暗,欠曝,低对比度,霓虹灯光感,过度锐化,3D渲染感,人物,行人,游客,身体,"
"皮肤,肢体,面部特征,汽车,电线"),
},
)
ALL_PRESETS = (LINGBOTWORLD_I2V, )
@@ -1,65 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""LongCat model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
LONGCAT_T2V = InferencePreset(
name="longcat_t2v",
version=1,
model_family="longcat",
description="LongCat-Video T2V at 480p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 848,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 50,
},
)
LONGCAT_I2V = InferencePreset(
name="longcat_i2v",
version=1,
model_family="longcat",
description="LongCat-Video I2V at 480p",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 848,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 50,
},
)
LONGCAT_VC = InferencePreset(
name="longcat_vc",
version=1,
model_family="longcat",
description="LongCat-Video continuation at 480p",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 848,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 50,
},
)
ALL_PRESETS = (LONGCAT_T2V, LONGCAT_I2V, LONGCAT_VC)
-114
View File
@@ -1,114 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""LTX2 model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
from fastvideo.pipelines.basic.ltx2.stage_overrides import (
REFINE_STAGE_OVERRIDE_FIELDS, )
_LTX2_NEGATIVE_PROMPT = ("blurry, out of focus, overexposed, underexposed, low contrast, "
"washed out colors, excessive noise, grainy texture, poor lighting, "
"flickering, motion blur, distorted proportions, unnatural skin "
"tones, deformed facial features, asymmetrical face, missing facial "
"features, extra limbs, disfigured hands, wrong hand count, "
"artifacts around text, inconsistent perspective, camera shake, "
"incorrect depth of field, background too sharp, background clutter, "
"distracting reflections, harsh shadows, inconsistent lighting "
"direction, color banding, cartoonish rendering, 3D CGI look, "
"unrealistic materials, uncanny valley effect, incorrect ethnicity, "
"wrong gender, exaggerated expressions, wrong gaze direction, "
"mismatched lip sync, silent or muted audio, distorted voice, "
"robotic voice, echo, background noise, off-sync audio, incorrect "
"dialogue, added dialogue, repetitive speech, jittery movement, "
"awkward pauses, incorrect timing, unnatural transitions, "
"inconsistent framing, tilted camera, flat lighting, inconsistent "
"tone, cinematic oversaturation, stylized filters, or AI artifacts.")
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
_REFINE_STAGE = PresetStageSpec(
name="refine",
kind="refinement",
description="Latent-upsample + second-pass refine",
allowed_overrides=REFINE_STAGE_OVERRIDE_FIELDS,
)
LTX2_BASE = InferencePreset(
name="ltx2_base",
version=1,
model_family="ltx2",
description="LTX-2 base at 512x768",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"seed": 10,
"height": 512,
"width": 768,
"num_frames": 121,
"fps": 24,
"guidance_scale": 3.0,
"num_inference_steps": 40,
"negative_prompt": _LTX2_NEGATIVE_PROMPT,
"ltx2_cfg_scale_video": 3.0,
"ltx2_cfg_scale_audio": 7.0,
"ltx2_modality_scale_video": 3.0,
"ltx2_modality_scale_audio": 3.0,
"ltx2_rescale_scale": 0.7,
"ltx2_stg_scale_video": 1.0,
"ltx2_stg_scale_audio": 1.0,
"ltx2_stg_blocks_video": [29],
"ltx2_stg_blocks_audio": [29],
},
)
LTX2_DISTILLED = InferencePreset(
name="ltx2_distilled",
version=1,
model_family="ltx2",
description="LTX-2 distilled at 1024x1536",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"seed": 10,
"height": 1024,
"width": 1536,
"num_frames": 121,
"fps": 24,
"guidance_scale": 1.0,
"num_inference_steps": 8,
"negative_prompt": "",
},
)
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)
@@ -1,54 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Typed override surfaces for the LTX-2 two-stage refine flow.
* ``preset_overrides.refine`` — init-time knobs (see
:class:`LTX2RefinePresetOverride`).
* ``stage_overrides.refine`` — per-request knobs (see
:class:`LTX2RefineStageOverride`).
Asset paths live on :class:`~fastvideo.api.schema.ComponentConfig`
(``upsampler_weights`` and ``lora_path``).
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, fields
from typing import Any
@dataclass
class LTX2RefinePresetOverride:
"""Init-time refine wiring under ``preset_overrides.refine``."""
enabled: bool | None = None
add_noise: bool | None = None
@dataclass
class LTX2RefineStageOverride:
"""Per-request refine tuning under ``stage_overrides.refine``."""
num_inference_steps: int | None = None
guidance_scale: float | None = None
image_crf: int | None = None
video_position_offset_sec: float | None = None
def refine_override_to_dict(override: LTX2RefinePresetOverride | LTX2RefineStageOverride, ) -> dict[str, Any]:
"""Serialise a refine override, dropping ``None`` entries so only
user-set fields reach ``preset_overrides.refine`` or
``stage_overrides.refine``."""
return {k: v for k, v in asdict(override).items() if v is not None}
REFINE_PRESET_OVERRIDE_FIELDS: frozenset[str] = frozenset(f.name for f in fields(LTX2RefinePresetOverride))
REFINE_STAGE_OVERRIDE_FIELDS: frozenset[str] = frozenset(f.name for f in fields(LTX2RefineStageOverride))
REFINE_FLAT_KEYS: frozenset[str] = (REFINE_PRESET_OVERRIDE_FIELDS | REFINE_STAGE_OVERRIDE_FIELDS)
__all__ = [
"LTX2RefinePresetOverride",
"LTX2RefineStageOverride",
"REFINE_FLAT_KEYS",
"REFINE_PRESET_OVERRIDE_FIELDS",
"REFINE_STAGE_OVERRIDE_FIELDS",
"refine_override_to_dict",
]
@@ -1,17 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""LTX-2 family pipeline stages."""
from fastvideo.pipelines.basic.ltx2.stages.ltx2_audio_decoding import (
LTX2AudioDecodingStage, )
from fastvideo.pipelines.basic.ltx2.stages.ltx2_denoising import (
LTX2DenoisingStage, )
from fastvideo.pipelines.basic.ltx2.stages.ltx2_latent_preparation import (
LTX2LatentPreparationStage, )
from fastvideo.pipelines.basic.ltx2.stages.ltx2_text_encoding import (
LTX2TextEncodingStage, )
__all__ = [
"LTX2AudioDecodingStage",
"LTX2DenoisingStage",
"LTX2LatentPreparationStage",
"LTX2TextEncodingStage",
]
@@ -1,33 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""MatrixGame model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Causal denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
MATRIXGAME_I2V = InferencePreset(
name="matrixgame_i2v",
version=1,
model_family="matrixgame",
description="Matrix-Game 2.0 I2V",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 352,
"width": 640,
"num_frames": 57,
"fps": 25,
"guidance_scale": 1.0,
"num_inference_steps": 3,
"negative_prompt": None,
},
)
ALL_PRESETS = (MATRIXGAME_I2V, )
-34
View File
@@ -1,34 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Stable Diffusion 3.5 model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
SD35_MEDIUM = InferencePreset(
name="sd35_medium",
version=1,
model_family="sd35",
description="Stable Diffusion 3.5 Medium (text-to-image)",
workload_type="t2i",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 512,
"width": 512,
"num_frames": 1,
"fps": 1,
"seed": 0,
"guidance_scale": 6.0,
"num_inference_steps": 28,
"negative_prompt": "",
},
)
ALL_PRESETS = (SD35_MEDIUM, )
@@ -1,73 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""TurboDiffusion model family pipeline presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Fast few-step denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
TURBO_T2V_1_3B = InferencePreset(
name="turbo_t2v_1_3b",
version=1,
model_family="turbodiffusion",
description="TurboWan 2.1 T2V 1.3B (4-step)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 1.0,
"num_inference_steps": 4,
"negative_prompt": None,
},
)
TURBO_T2V_14B = InferencePreset(
name="turbo_t2v_14b",
version=1,
model_family="turbodiffusion",
description="TurboWan 2.1 T2V 14B (4-step)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 81,
"fps": 16,
"guidance_scale": 1.0,
"num_inference_steps": 4,
"negative_prompt": None,
},
)
TURBO_I2V_A14B = InferencePreset(
name="turbo_i2v_a14b",
version=1,
model_family="turbodiffusion",
description="TurboWan 2.2 I2V A14B (4-step)",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 81,
"fps": 16,
"guidance_scale": 1.0,
"num_inference_steps": 4,
"negative_prompt": None,
},
)
ALL_PRESETS = (
TURBO_T2V_1_3B,
TURBO_T2V_14B,
TURBO_I2V_A14B,
)
-347
View File
@@ -1,347 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Wan model family pipeline presets.
Each preset is a named inference preset that declares the user-facing
stage topology, default sampling values, and which per-stage overrides
are allowed. Presets are registered explicitly from
:func:`fastvideo.registry._register_presets`.
"""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
# -------------------------------------------------------------------
# Shared negative prompts
# -------------------------------------------------------------------
_NEGATIVE_PROMPT_EN = ("Bright tones, overexposed, static, blurred details, subtitles,"
" style, works, paintings, images, static, overall gray, worst"
" quality, low quality, JPEG compression residue, ugly,"
" incomplete, extra fingers, poorly drawn hands, poorly drawn"
" faces, deformed, disfigured, misshapen limbs, fused fingers,"
" still picture, messy background, three legs, many people in"
" the background, walking backwards")
_NEGATIVE_PROMPT_CN = ("色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,"
"静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,"
"多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,"
"形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,"
"背景人很多,倒着走")
# -------------------------------------------------------------------
# Shared stage specs
# -------------------------------------------------------------------
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Main denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
# -------------------------------------------------------------------
# Wan 2.1 T2V presets
# -------------------------------------------------------------------
WAN_T2V_1_3B = InferencePreset(
name="wan_t2v_1_3b",
version=1,
model_family="wan",
description="Wan 2.1 T2V 1.3B at 480p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 3.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
WAN_T2V_14B = InferencePreset(
name="wan_t2v_14b",
version=1,
model_family="wan",
description="Wan 2.1 T2V 14B at 720p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 81,
"fps": 16,
"guidance_scale": 5.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
# -------------------------------------------------------------------
# Wan 2.1 I2V presets
# -------------------------------------------------------------------
WAN_I2V_14B_480P = InferencePreset(
name="wan_i2v_14b_480p",
version=1,
model_family="wan",
description="Wan 2.1 I2V 14B at 480p",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 5.0,
"num_inference_steps": 40,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
WAN_I2V_14B_720P = InferencePreset(
name="wan_i2v_14b_720p",
version=1,
model_family="wan",
description="Wan 2.1 I2V 14B at 720p",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 720,
"width": 1280,
"num_frames": 81,
"fps": 16,
"guidance_scale": 5.0,
"num_inference_steps": 40,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
# -------------------------------------------------------------------
# Wan 2.2 presets
# -------------------------------------------------------------------
_DENOISE_STAGE_WAN22 = PresetStageSpec(
name="denoise",
kind="denoising",
description="Wan 2.2 two-guidance-scale denoising",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
"guidance_scale_2",
"boundary_ratio",
}),
)
WAN_2_2_T2V_A14B = InferencePreset(
name="wan_2_2_t2v_a14b",
version=1,
model_family="wan",
description="Wan 2.2 T2V A14B with dual guidance scales",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE_WAN22, ),
defaults={
"guidance_scale": 4.0,
"guidance_scale_2": 3.0,
"num_inference_steps": 40,
"fps": 16,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
WAN_2_2_I2V_A14B = InferencePreset(
name="wan_2_2_i2v_a14b",
version=1,
model_family="wan",
description="Wan 2.2 I2V A14B with dual guidance scales",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE_WAN22, ),
defaults={
"guidance_scale": 3.5,
"guidance_scale_2": 3.5,
"num_inference_steps": 40,
"fps": 16,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
# -------------------------------------------------------------------
# Wan 2.1 Fun / Control presets
# -------------------------------------------------------------------
WAN_FUN_1_3B_INP = InferencePreset(
name="wan_fun_1_3b_inp",
version=1,
model_family="wan",
description="Wan 2.1 Fun 1.3B InP (image-to-video inpainting)",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
WAN_FUN_1_3B_CONTROL = InferencePreset(
name="wan_fun_1_3b_control",
version=1,
model_family="wan",
description="Wan 2.1 Fun 1.3B Control (V2V)",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 832,
"width": 480,
"num_frames": 49,
"fps": 16,
"guidance_scale": 6.0,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
# -------------------------------------------------------------------
# FastWan (DMD) presets
# -------------------------------------------------------------------
FAST_WAN_T2V_480P = InferencePreset(
name="fast_wan_t2v_480p",
version=1,
model_family="wan",
description="FastWan 2.1 T2V DMD at 480p (3-step)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 448,
"width": 832,
"num_frames": 61,
"fps": 16,
"guidance_scale": 3.0,
"num_inference_steps": 3,
"negative_prompt": _NEGATIVE_PROMPT_EN,
},
)
# -------------------------------------------------------------------
# Wan 2.2 TI2V 5B presets
# -------------------------------------------------------------------
WAN_2_2_TI2V_5B = InferencePreset(
name="wan_2_2_ti2v_5b",
version=1,
model_family="wan",
description="Wan 2.2 TI2V 5B",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 5.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
FAST_WAN_2_2_TI2V_5B = InferencePreset(
name="fast_wan_2_2_ti2v_5b",
version=1,
model_family="wan",
description="FastWan 2.2 TI2V 5B DMD",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 121,
"fps": 24,
"guidance_scale": 5.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
# -------------------------------------------------------------------
# Self-Forcing (causal) presets
# -------------------------------------------------------------------
SF_WAN_T2V_1_3B = InferencePreset(
name="sf_wan_t2v_1_3b",
version=1,
model_family="wan",
description="Self-Forcing Wan 2.1 T2V 1.3B (causal)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
SF_WAN_2_2_T2V_A14B = InferencePreset(
name="sf_wan_2_2_t2v_a14b",
version=1,
model_family="wan",
description="Self-Forcing Wan 2.2 T2V A14B (causal)",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE_WAN22, ),
defaults={
"height": 448,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 4.0,
"guidance_scale_2": 3.0,
"num_inference_steps": 8,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
SF_WAN_2_2_I2V_A14B = InferencePreset(
name="sf_wan_2_2_i2v_a14b",
version=1,
model_family="wan",
description="Self-Forcing Wan 2.2 I2V A14B (causal)",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE_WAN22, ),
defaults={
"height": 448,
"width": 832,
"num_frames": 81,
"fps": 16,
"guidance_scale": 4.0,
"guidance_scale_2": 3.0,
"num_inference_steps": 8,
"negative_prompt": _NEGATIVE_PROMPT_CN,
},
)
# Collect all presets for bulk registration.
ALL_PRESETS = (
WAN_T2V_1_3B,
WAN_T2V_14B,
WAN_I2V_14B_480P,
WAN_I2V_14B_720P,
WAN_2_2_T2V_A14B,
WAN_2_2_I2V_A14B,
WAN_FUN_1_3B_INP,
WAN_FUN_1_3B_CONTROL,
FAST_WAN_T2V_480P,
WAN_2_2_TI2V_5B,
FAST_WAN_2_2_TI2V_5B,
SF_WAN_T2V_1_3B,
SF_WAN_2_2_T2V_A14B,
SF_WAN_2_2_I2V_A14B,
)
@@ -18,7 +18,7 @@ from torch.utils.data import DataLoader
from torchdata.stateful_dataloader import StatefulDataLoader
from tqdm import tqdm
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.dataset import gettextdataset
from fastvideo.dataset.dataloader.parquet_io import (ParquetDatasetWriter, records_to_table)
from fastvideo.dataset.dataloader.record_schema import (ode_text_only_record_creator)
+4 -6
View File
@@ -25,12 +25,10 @@ from fastvideo.pipelines.stages.latent_preparation import (Cosmos25LatentPrepara
Cosmos25AutoLatentPreparationStage,
Cosmos25T2WLatentPreparationStage,
Cosmos25V2WLatentPreparationStage, LatentPreparationStage)
from fastvideo.pipelines.basic.ltx2.stages import (
LTX2AudioDecodingStage,
LTX2DenoisingStage,
LTX2LatentPreparationStage,
LTX2TextEncodingStage,
)
from fastvideo.pipelines.stages.ltx2_audio_decoding import LTX2AudioDecodingStage
from fastvideo.pipelines.stages.ltx2_denoising import LTX2DenoisingStage
from fastvideo.pipelines.stages.ltx2_latent_preparation import (LTX2LatentPreparationStage)
from fastvideo.pipelines.stages.ltx2_text_encoding import LTX2TextEncodingStage
from fastvideo.pipelines.stages.matrixgame_denoising import (MatrixGameCausalDenoisingStage)
from fastvideo.pipelines.stages.hyworld_denoising import HYWorldDenoisingStage
from fastvideo.pipelines.stages.gamecraft_denoising import GameCraftDenoisingStage
+74 -199
View File
@@ -26,7 +26,7 @@ from fastvideo.configs.pipelines.hunyuan15 import (Hunyuan15T2V480PConfig, Hunyu
from fastvideo.configs.pipelines.hyworld import HYWorldConfig
from fastvideo.configs.pipelines.lingbotworld import LingBotWorldI2V480PConfig
from fastvideo.configs.pipelines.longcat import LongCatT2V480PConfig
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
from fastvideo.configs.pipelines.ltx2 import LTX2T2VConfig
from fastvideo.configs.pipelines.turbodiffusion import (
TurboDiffusionI2V_A14B_Config,
TurboDiffusionT2V_14B_Config,
@@ -48,7 +48,41 @@ from fastvideo.configs.pipelines.wan import (
WanT2V720PConfig,
)
from fastvideo.configs.pipelines.sd35 import SD35Config
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.configs.sample.cosmos import (
Cosmos_Predict2_2B_Video2World_SamplingParam, )
from fastvideo.configs.sample.cosmos2_5 import Cosmos25SamplingParamBase
from fastvideo.configs.sample.gen3c import Gen3C_Cosmos_7B_SamplingParam
from fastvideo.configs.sample.hunyuan15 import (Hunyuan15_480P_SamplingParam,
Hunyuan15_480P_StepDistilled_I2V_SamplingParam,
Hunyuan15_720P_SamplingParam,
Hunyuan15_720P_Distilled_I2V_SamplingParam,
Hunyuan15_SR_1080P_SamplingParam)
from fastvideo.configs.sample.hyworld import HYWorld_SamplingParam
from fastvideo.configs.sample.hunyuangamecraft import HunyuanGameCraftSamplingParam
from fastvideo.configs.sample.lingbotworld import LingBotWorld_SamplingParam
from fastvideo.configs.sample.ltx2 import (LTX2BaseSamplingParam, LTX2DistilledSamplingParam)
from fastvideo.configs.sample.turbodiffusion import (
TurboDiffusionI2V_A14B_SamplingParam,
TurboDiffusionT2V_14B_SamplingParam,
TurboDiffusionT2V_1_3B_SamplingParam,
)
from fastvideo.configs.sample.wan import (
FastWanT2V480P_SamplingParam,
MatrixGame2_SamplingParam,
SelfForcingWan2_1_T2V_1_3B_480P_SamplingParam,
SelfForcingWan2_2_T2V_A14B_480P_SamplingParam,
Wan2_1_Fun_1_3B_Control_SamplingParam,
Wan2_1_Fun_1_3B_InP_SamplingParam,
Wan2_2_I2V_A14B_SamplingParam,
Wan2_2_T2V_A14B_SamplingParam,
Wan2_2_TI2V_5B_SamplingParam,
WanI2V_14B_480P_SamplingParam,
WanI2V_14B_720P_SamplingParam,
WanT2V_14B_SamplingParam,
WanT2V_1_3B_SamplingParam,
)
from fastvideo.configs.sample.sd35 import SD35SamplingParam
from fastvideo.fastvideo_args import WorkloadType
from fastvideo.logger import init_logger
@@ -103,8 +137,7 @@ class ConfigInfo:
sampling_param_cls: type[SamplingParam] | None
pipeline_config_cls: type[PipelineConfig]
workload_types: tuple[WorkloadType, ...]
model_family: str | None = None
default_preset: str | None = None
default_profile: str | None = None
# The central registry mapping a model name to its configuration information
@@ -123,8 +156,7 @@ def register_configs(
workload_types: tuple[WorkloadType, ...],
hf_model_paths: list[str] | None = None,
model_detectors: list[Callable[[str], bool]] | None = None,
model_family: str | None = None,
default_preset: str | None = None,
default_profile: str | None = None,
) -> None:
"""Register config classes for a model family.
@@ -137,8 +169,7 @@ def register_configs(
sampling_param_cls=sampling_param_cls,
pipeline_config_cls=pipeline_config_cls,
workload_types=workload_types,
model_family=model_family,
default_preset=default_preset,
default_profile=default_profile,
)
if hf_model_paths:
@@ -213,7 +244,7 @@ def _get_config_info(
def _register_configs() -> None:
# LTX-2 (base)
register_configs(
sampling_param_cls=None,
sampling_param_cls=LTX2BaseSamplingParam,
pipeline_config_cls=LTX2T2VConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -224,12 +255,10 @@ def _register_configs() -> None:
model_detectors=[
lambda path: ("ltx2" in path.lower() or "ltx-2" in path.lower()) and "distilled" not in path.lower(),
],
model_family="ltx2",
default_preset="ltx2_base",
)
# LTX-2 (distilled)
register_configs(
sampling_param_cls=None,
sampling_param_cls=LTX2DistilledSamplingParam,
pipeline_config_cls=LTX2T2VConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -238,13 +267,11 @@ def _register_configs() -> None:
model_detectors=[
lambda path: ("ltx2" in path.lower() or "ltx-2" in path.lower()) and "distilled" in path.lower(),
],
model_family="ltx2",
default_preset="ltx2_distilled",
)
# Hunyuan 1.5 (specific)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Hunyuan15_480P_SamplingParam,
pipeline_config_cls=Hunyuan15T2V480PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -258,46 +285,36 @@ def _register_configs() -> None:
"hunyuanvideo_1.5",
)),
],
model_family="hunyuan15",
default_preset="hunyuan15_t2v_480p",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Hunyuan15_480P_StepDistilled_I2V_SamplingParam,
pipeline_config_cls=Hunyuan15I2V480PStepDistilledConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v_step_distilled",
],
model_family="hunyuan15",
default_preset="hunyuan15_i2v_480p_distilled",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Hunyuan15_720P_SamplingParam,
pipeline_config_cls=Hunyuan15T2V720PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v",
],
model_family="hunyuan15",
default_preset="hunyuan15_t2v_720p",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Hunyuan15_720P_Distilled_I2V_SamplingParam,
pipeline_config_cls=Hunyuan15I2V720PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v_distilled",
],
model_family="hunyuan15",
default_preset="hunyuan15_i2v_720p_distilled",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Hunyuan15_SR_1080P_SamplingParam,
pipeline_config_cls=Hunyuan15SR1080PConfig,
workload_types=(),
hf_model_paths=["weizhou03/HunyuanVideo-1.5-Diffusers-1080p", "weizhou03/HunyuanVideo-1.5-Diffusers-1080p-2SR"],
model_family="hunyuan15",
default_preset="hunyuan15_sr_1080p",
)
# Hunyuan (excludes gamecraft, hyworld, and versioned models)
@@ -312,8 +329,7 @@ def _register_configs() -> None:
lambda path: "hunyuan" in path.lower() and "gamecraft" not in path.lower() and "hyworld" not in path.lower(
) and "1.5" not in path.lower() and "1-5" not in path.lower()
],
model_family="hunyuan",
default_preset="hunyuan_t2v",
default_profile="hunyuan_t2v",
)
register_configs(
sampling_param_cls=None,
@@ -322,46 +338,39 @@ def _register_configs() -> None:
hf_model_paths=[
"FastVideo/FastHunyuan-diffusers",
],
model_family="hunyuan",
default_preset="fast_hunyuan_t2v",
default_profile="fast_hunyuan_t2v",
)
# HYWorld
register_configs(
sampling_param_cls=None,
sampling_param_cls=HYWorld_SamplingParam,
pipeline_config_cls=HYWorldConfig,
workload_types=(),
hf_model_paths=[
"FastVideo/HY-WorldPlay-Bidirectional-Diffusers",
],
model_detectors=[lambda path: "hyworld" in path.lower()],
model_family="hyworld",
default_preset="hyworld_t2v",
)
# HunyuanGameCraft
register_configs(
sampling_param_cls=None,
sampling_param_cls=HunyuanGameCraftSamplingParam,
pipeline_config_cls=HunyuanGameCraftPipelineConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"FastVideo/HunyuanGameCraft-Diffusers",
],
model_detectors=[lambda path: "gamecraft" in path.lower()],
model_family="gamecraft",
default_preset="gamecraft_i2v",
)
# LingBotWorld
register_configs(
sampling_param_cls=None,
sampling_param_cls=LingBotWorld_SamplingParam,
pipeline_config_cls=LingBotWorldI2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"FastVideo/LingBot-World-Base-Cam-Diffusers",
],
model_detectors=[lambda path: ("lingbotworld" in path.lower() or "lingbot-world" in path.lower())],
model_family="lingbotworld",
default_preset="lingbotworld_i2v",
)
# Kandinsky5 Lite T2V
@@ -375,7 +384,6 @@ def _register_configs() -> None:
model_detectors=[
lambda path: any(token in path.lower() for token in ("kandinsky5", "kandinsky-5")),
],
model_family="kandinsky5",
)
# LongCat (T2V, I2V, VC use same config; workload varies by path)
@@ -388,8 +396,6 @@ def _register_configs() -> None:
lambda path: "longcat" in path.lower() and "i2v" not in path.lower() and "imagetovideo" not in path.lower()
and "vc" not in path.lower() and "videocontinuation" not in path.lower(),
],
model_family="longcat",
default_preset="longcat_t2v",
)
register_configs(
sampling_param_cls=None,
@@ -399,8 +405,6 @@ def _register_configs() -> None:
model_detectors=[
lambda path: "longcatimagetovideo" in path.lower() or ("longcat" in path.lower() and "i2v" in path.lower()),
],
model_family="longcat",
default_preset="longcat_i2v",
)
register_configs(
sampling_param_cls=None,
@@ -411,13 +415,11 @@ def _register_configs() -> None:
lambda path: "longcatvideocontinuation" in path.lower() or
("longcat" in path.lower() and "vc" in path.lower()),
],
model_family="longcat",
default_preset="longcat_vc",
)
# MatrixGame
register_configs(
sampling_param_cls=None,
sampling_param_cls=MatrixGame2_SamplingParam,
pipeline_config_cls=MatrixGameI2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
@@ -428,13 +430,11 @@ def _register_configs() -> None:
model_detectors=[
lambda path: "matrix-game" in path.lower() or "matrixgame" in path.lower(),
],
model_family="matrixgame",
default_preset="matrixgame_i2v",
)
# GEN3C (must register before generic Cosmos detector)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Gen3C_Cosmos_7B_SamplingParam,
pipeline_config_cls=Gen3CConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -443,13 +443,11 @@ def _register_configs() -> None:
model_detectors=[
lambda path: "gen3c" in path.lower(),
],
model_family="gen3c",
default_preset="gen3c_cosmos_7b",
)
# Cosmos 2.5
register_configs(
sampling_param_cls=None,
sampling_param_cls=Cosmos25SamplingParamBase,
pipeline_config_cls=Cosmos25Config,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -462,13 +460,11 @@ def _register_configs() -> None:
"cosmos2.5",
)),
],
model_family="cosmos25",
default_preset="cosmos25_predict2_2b",
)
# Cosmos 2
register_configs(
sampling_param_cls=None,
sampling_param_cls=Cosmos_Predict2_2B_Video2World_SamplingParam,
pipeline_config_cls=CosmosConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -478,109 +474,89 @@ def _register_configs() -> None:
lambda path: "cosmos" in path.lower() and ("2.5" not in path.lower() and "2_5" not in path.lower() and "25"
not in path.lower() and "gen3c" not in path.lower()),
],
model_family="cosmos",
default_preset="cosmos_predict2_2b",
)
# TurboDiffusion
register_configs(
sampling_param_cls=None,
sampling_param_cls=TurboDiffusionT2V_1_3B_SamplingParam,
pipeline_config_cls=TurboDiffusionT2V_1_3B_Config,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers",
],
model_detectors=[lambda path: "turbodiffusion" in path.lower() or "turbowan" in path.lower()],
model_family="turbodiffusion",
default_preset="turbo_t2v_1_3b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=TurboDiffusionT2V_14B_SamplingParam,
pipeline_config_cls=TurboDiffusionT2V_14B_Config,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"loayrashid/TurboWan2.1-T2V-14B-Diffusers",
],
model_family="turbodiffusion",
default_preset="turbo_t2v_14b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=TurboDiffusionI2V_A14B_SamplingParam,
pipeline_config_cls=TurboDiffusionI2V_A14B_Config,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"loayrashid/TurboWan2.2-I2V-A14B-Diffusers",
],
model_family="turbodiffusion",
default_preset="turbo_i2v_a14b",
)
# Wan — defaults provided by presets (no sampling_param_cls needed)
# Wan
register_configs(
sampling_param_cls=None,
sampling_param_cls=WanT2V_1_3B_SamplingParam,
pipeline_config_cls=WanT2V480PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
],
model_detectors=[lambda path: "wanpipeline" in path.lower()],
model_family="wan",
default_preset="wan_t2v_1_3b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=WanT2V_14B_SamplingParam,
pipeline_config_cls=WanT2V720PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"Wan-AI/Wan2.1-T2V-14B-Diffusers",
"FastVideo/Wan2.1-VSA-T2V-14B-720P-Diffusers",
],
model_family="wan",
default_preset="wan_t2v_14b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=WanI2V_14B_480P_SamplingParam,
pipeline_config_cls=WanI2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
],
model_detectors=[lambda path: "wanimagetovideo" in path.lower()],
model_family="wan",
default_preset="wan_i2v_14b_480p",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=WanI2V_14B_720P_SamplingParam,
pipeline_config_cls=WanI2V720PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers",
],
model_family="wan",
default_preset="wan_i2v_14b_720p",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_1_Fun_1_3B_InP_SamplingParam,
pipeline_config_cls=WanI2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers",
],
model_family="wan",
default_preset="wan_fun_1_3b_inp",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_1_Fun_1_3B_Control_SamplingParam,
pipeline_config_cls=WANV2VConfig,
workload_types=(),
hf_model_paths=[
"IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers",
],
model_family="wan",
default_preset="wan_fun_1_3b_control",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=FastWanT2V480P_SamplingParam,
pipeline_config_cls=FastWan2_1_T2V_480P_Config,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
@@ -588,88 +564,72 @@ def _register_configs() -> None:
"FastVideo/FastWan2.1-T2V-14B-480P-Diffusers",
],
model_detectors=[lambda path: "wandmdpipeline" in path.lower()],
model_family="wan",
default_preset="fast_wan_t2v_480p",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_2_TI2V_5B_SamplingParam,
pipeline_config_cls=Wan2_2_TI2V_5B_Config,
workload_types=(WorkloadType.T2V, WorkloadType.I2V),
hf_model_paths=[
"Wan-AI/Wan2.2-TI2V-5B-Diffusers",
],
model_family="wan",
default_preset="wan_2_2_ti2v_5b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_2_TI2V_5B_SamplingParam,
pipeline_config_cls=FastWan2_2_TI2V_5B_Config,
workload_types=(WorkloadType.T2V, WorkloadType.I2V),
hf_model_paths=[
"FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
"FastVideo/FastWan2.2-TI2V-5B-Diffusers",
],
model_family="wan",
default_preset="fast_wan_2_2_ti2v_5b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_2_T2V_A14B_SamplingParam,
pipeline_config_cls=Wan2_2_T2V_A14B_Config,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"Wan-AI/Wan2.2-T2V-A14B-Diffusers",
],
model_family="wan",
default_preset="wan_2_2_t2v_a14b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=Wan2_2_I2V_A14B_SamplingParam,
pipeline_config_cls=Wan2_2_I2V_A14B_Config,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"Wan-AI/Wan2.2-I2V-A14B-Diffusers",
],
model_family="wan",
default_preset="wan_2_2_i2v_a14b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=SelfForcingWan2_1_T2V_1_3B_480P_SamplingParam,
pipeline_config_cls=SelfForcingWanT2V480PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=[
"wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers",
],
model_detectors=[lambda path: "wancausaldmdpipeline" in path.lower()],
model_family="wan",
default_preset="sf_wan_t2v_1_3b",
)
# SFWan2.2: T2V and I2V variants by path
register_configs(
sampling_param_cls=None,
sampling_param_cls=SelfForcingWan2_2_T2V_A14B_480P_SamplingParam,
pipeline_config_cls=SelfForcingWan2_2_T2V480PConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=["rand0nmr/SFWan2.2-T2V-A14B-Diffusers"],
model_detectors=[
lambda path: ("sfwan2.2" in path.lower() or "sfwan2_2" in path.lower()) and "i2v" not in path.lower(),
],
model_family="wan",
default_preset="sf_wan_2_2_t2v_a14b",
)
register_configs(
sampling_param_cls=None,
sampling_param_cls=SelfForcingWan2_2_T2V_A14B_480P_SamplingParam,
pipeline_config_cls=SelfForcingWan2_2_T2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=["FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers"],
model_detectors=[
lambda path: ("sfwan2.2" in path.lower() or "sfwan2_2" in path.lower()) and "i2v" in path.lower(),
],
model_family="wan",
default_preset="sf_wan_2_2_i2v_a14b",
)
# SD3.5
register_configs(
sampling_param_cls=None,
sampling_param_cls=SD35SamplingParam,
pipeline_config_cls=SD35Config,
workload_types=(WorkloadType.T2I, ),
hf_model_paths=[
@@ -682,8 +642,6 @@ def _register_configs() -> None:
"stabilityai__stable-diffusion-3.5-medium",
)),
],
model_family="sd35",
default_preset="sd35_medium",
)
@@ -762,87 +720,6 @@ def get_sampling_param_cls_for_name(pipeline_name_or_path: str) -> Any | None:
_register_configs()
def _register_presets() -> None:
from fastvideo.api.presets import register_preset
from fastvideo.pipelines.basic.cosmos.presets import (
ALL_PRESETS as COSMOS_PRESETS, )
from fastvideo.pipelines.basic.gamecraft.presets import (
ALL_PRESETS as GAMECRAFT_PRESETS, )
from fastvideo.pipelines.basic.gen3c.presets import (
ALL_PRESETS as GEN3C_PRESETS, )
from fastvideo.pipelines.basic.hunyuan.presets import (
ALL_PRESETS as HUNYUAN_PRESETS, )
from fastvideo.pipelines.basic.hunyuan15.presets import (
ALL_PRESETS as HUNYUAN15_PRESETS, )
from fastvideo.pipelines.basic.hyworld.presets import (
ALL_PRESETS as HYWORLD_PRESETS, )
from fastvideo.pipelines.basic.lingbotworld.presets import (
ALL_PRESETS as LINGBOTWORLD_PRESETS, )
from fastvideo.pipelines.basic.longcat.presets import (
ALL_PRESETS as LONGCAT_PRESETS, )
from fastvideo.pipelines.basic.ltx2.presets import (
ALL_PRESETS as LTX2_PRESETS, )
from fastvideo.pipelines.basic.matrixgame.presets import (
ALL_PRESETS as MATRIXGAME_PRESETS, )
from fastvideo.pipelines.basic.sd35.presets import (
ALL_PRESETS as SD35_PRESETS, )
from fastvideo.pipelines.basic.turbodiffusion.presets import (
ALL_PRESETS as TURBODIFFUSION_PRESETS, )
from fastvideo.pipelines.basic.wan.presets import (
ALL_PRESETS as WAN_PRESETS, )
all_preset_groups = (
COSMOS_PRESETS,
GAMECRAFT_PRESETS,
GEN3C_PRESETS,
HUNYUAN_PRESETS,
HUNYUAN15_PRESETS,
HYWORLD_PRESETS,
LINGBOTWORLD_PRESETS,
LONGCAT_PRESETS,
LTX2_PRESETS,
MATRIXGAME_PRESETS,
SD35_PRESETS,
TURBODIFFUSION_PRESETS,
WAN_PRESETS,
)
for group in all_preset_groups:
for preset in group:
register_preset(preset)
_register_presets()
def get_model_family(model_path: str) -> str | None:
"""Return the ``model_family`` string for a model path, or ``None``."""
config_info = _get_config_info(model_path, raise_on_missing=False)
if config_info is None:
return None
return config_info.model_family
def get_default_preset(model_path: str) -> str | None:
"""Return the ``default_preset`` name for a model path."""
config_info = _get_config_info(model_path, raise_on_missing=False)
if config_info is None:
return None
return config_info.default_preset
def get_preset_selection(model_path: str) -> tuple[str | None, str | None]:
"""Return ``(default_preset, model_family)`` for a model path.
Single-lookup variant of :func:`get_default_preset` +
:func:`get_model_family`; callers that need both should prefer this
to avoid walking the registry twice.
"""
config_info = _get_config_info(model_path, raise_on_missing=False)
if config_info is None:
return None, None
return config_info.default_preset, config_info.model_family
def get_registered_model_paths() -> list[str]:
"""Return all registered HuggingFace model paths.
@@ -882,8 +759,6 @@ def get_registered_models_with_workloads(workload_type: str | None = None, ) ->
__all__ = [
"ConfigInfo",
"ModelInfo",
"get_default_preset",
"get_model_family",
"get_model_info",
"get_pipeline_config_cls_from_name",
"get_registered_model_paths",
+9 -56
View File
@@ -12,10 +12,9 @@ from fastvideo.entrypoints.cli.inference_config import (
build_generate_run_config,
build_serve_config,
)
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.entrypoints.cli.serve import ServeSubcommand
from fastvideo.entrypoints.openai import api_server
from fastvideo.entrypoints.streaming import server as streaming_server
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.utils import FlexibleArgumentParser
@@ -445,12 +444,11 @@ def test_serve_subcommand_dispatches_via_typed_config(tmp_path, monkeypatch):
captured["config"] = config
return SimpleNamespace(model_path=config.model_path)
def fake_run_server(fastvideo_args, host, port, output_dir, default_request):
def fake_run_server(fastvideo_args, host, port, output_dir):
captured["fastvideo_args"] = fastvideo_args
captured["host"] = host
captured["port"] = port
captured["output_dir"] = output_dir
captured["default_request"] = default_request
monkeypatch.setattr(
"fastvideo.entrypoints.cli.serve.generator_config_to_fastvideo_args",
@@ -467,37 +465,22 @@ def test_serve_subcommand_dispatches_via_typed_config(tmp_path, monkeypatch):
assert captured["output_dir"] == "serve-outputs/"
def test_serve_subcommand_forwards_default_request(tmp_path, monkeypatch):
def test_serve_subcommand_rejects_non_default_default_request(tmp_path):
config_path = tmp_path / "serve-default-request.yaml"
config_path.write_text(
"generator:\n"
" model_path: serve-model\n"
"default_request:\n"
" prompt: hello\n"
" sampling:\n"
" seed: 42\n",
" prompt: hello\n",
encoding="utf-8",
)
args, _ = _parse_serve_args(["--config", str(config_path)])
captured: dict[str, object] = {}
def fake_generator_config_to_fastvideo_args(config):
return SimpleNamespace(model_path=config.model_path)
def fake_run_server(fastvideo_args, host, port, output_dir, default_request):
captured["default_request"] = default_request
monkeypatch.setattr(
"fastvideo.entrypoints.cli.serve.generator_config_to_fastvideo_args",
fake_generator_config_to_fastvideo_args,
)
monkeypatch.setattr(api_server, "run_server", fake_run_server)
ServeSubcommand().cmd(args)
default_request = captured["default_request"]
assert default_request.prompt == "hello"
assert default_request.sampling.seed == 42
with pytest.raises(
NotImplementedError,
match="ServeConfig.default_request is not wired",
):
ServeSubcommand().cmd(args)
def test_main_rejects_top_level_config_without_subcommand(tmp_path, monkeypatch):
@@ -517,33 +500,3 @@ def test_main_rejects_top_level_config_without_subcommand(tmp_path, monkeypatch)
with pytest.raises(SystemExit):
cli_main.main()
def test_serve_cmd_dispatches_to_streaming_when_streaming_block_set(tmp_path):
config_path = tmp_path / "serve-streaming.yaml"
config_path.write_text(
"generator:\n"
" model_path: stream-model\n"
"streaming:\n"
" stream_mode: av_fmp4\n",
encoding="utf-8",
)
args, _ = _parse_serve_args(["--config", str(config_path)])
with pytest.raises(NotImplementedError,
match="streaming server is not implemented"):
ServeSubcommand().cmd(args)
def test_streaming_run_server_rejects_missing_streaming_block():
from fastvideo.api.schema import GeneratorConfig, ServeConfig
config = ServeConfig(
generator=GeneratorConfig(model_path="x"),
streaming=None,
)
with pytest.raises(
ValueError,
match="ServeConfig.streaming must be set",
):
streaming_server.run_server(config)
@@ -1,181 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Tests for ``fastvideo.api.compat`` translation helpers covering the
typed CompileConfig + PipelineSelection.vae_tiling surfaces promoted in
PR 6.
"""
from __future__ import annotations
from fastvideo.api.compat import (
generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config,
)
from fastvideo.api.schema import CompileConfig, GeneratorConfig
class TestLegacyTorchCompileKwargsTranslation:
"""Legacy ``torch_compile_kwargs={...}`` gets split across the four
first-class :class:`CompileConfig` fields and anything unknown falls
into ``extras``."""
def test_all_typed_keys_promoted(self) -> None:
config = legacy_from_pretrained_to_config(
"/models/ltx2",
{
"enable_torch_compile": True,
"torch_compile_kwargs": {
"backend": "inductor",
"fullgraph": True,
"mode": "max-autotune-no-cudagraphs",
"dynamic": False,
},
},
)
compile_config = config.engine.compile
assert compile_config.enabled is True
assert compile_config.backend == "inductor"
assert compile_config.fullgraph is True
assert compile_config.mode == "max-autotune-no-cudagraphs"
assert compile_config.dynamic is False
assert compile_config.extras == {}
def test_unknown_keys_land_in_extras(self) -> None:
config = legacy_from_pretrained_to_config(
"/models/ltx2",
{
"enable_torch_compile": True,
"torch_compile_kwargs": {
"backend": "inductor",
"options": {"triton.cudagraphs": False},
"disable": False,
},
},
)
compile_config = config.engine.compile
assert compile_config.backend == "inductor"
assert compile_config.extras == {
"options": {"triton.cudagraphs": False},
"disable": False,
}
def test_empty_kwargs_produces_empty_extras(self) -> None:
config = legacy_from_pretrained_to_config(
"/models/ltx2",
{"torch_compile_kwargs": {}},
)
compile_config = config.engine.compile
assert compile_config.extras == {}
assert compile_config.backend is None
class TestCompileConfigRoundTrip:
"""typed CompileConfig -> FastVideoArgs.torch_compile_kwargs
reconstruction drops ``None`` typed fields and merges ``extras``."""
def test_only_typed_fields_emitted(self, monkeypatch) -> None:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(
CompileConfig(enabled=True, backend="inductor", fullgraph=True)),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["enable_torch_compile"] is True
assert args.kwargs["torch_compile_kwargs"] == {
"backend": "inductor",
"fullgraph": True,
}
def test_extras_merged_into_torch_compile_kwargs(self, monkeypatch) -> None:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(
CompileConfig(
enabled=True,
mode="reduce-overhead",
extras={"options": {"triton.cudagraphs": False}},
)),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["torch_compile_kwargs"] == {
"mode": "reduce-overhead",
"options": {"triton.cudagraphs": False},
}
def test_none_fields_suppressed(self, monkeypatch) -> None:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(CompileConfig()),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["torch_compile_kwargs"] == {}
class TestLegacyLtx2VaeTilingTranslation:
"""``ltx2_vae_tiling`` flat kwarg promotes to
``generator.pipeline.vae_tiling``; reverse direction emits the
legacy name back to FastVideoArgs."""
def test_forward_routes_to_pipeline_vae_tiling(self) -> None:
config = legacy_from_pretrained_to_config(
"/models/ltx2",
{"ltx2_vae_tiling": False},
)
assert config.pipeline.vae_tiling is False
def test_true_round_trips(self) -> None:
config = legacy_from_pretrained_to_config(
"/models/ltx2",
{"ltx2_vae_tiling": True},
)
assert config.pipeline.vae_tiling is True
def test_unset_stays_none(self) -> None:
config = legacy_from_pretrained_to_config("/models/ltx2", {})
assert config.pipeline.vae_tiling is None
def test_reverse_emits_legacy_name(self, monkeypatch) -> None:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(CompileConfig()),
)
config.pipeline.vae_tiling = False
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["ltx2_vae_tiling"] is False
def test_reverse_unset_skips_key(self, monkeypatch) -> None:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(CompileConfig()),
)
args = generator_config_to_fastvideo_args(config)
assert "ltx2_vae_tiling" not in args.kwargs
# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------
def _engine_with_compile(compile_config):
"""Build an ``EngineConfig`` that carries the supplied compile block."""
from fastvideo.api.schema import EngineConfig
engine = EngineConfig()
engine.compile = compile_config
return engine
def _stub_fastvideo_args_from_kwargs(monkeypatch):
"""Swap ``FastVideoArgs.from_kwargs`` for a capture-only stub so
translation tests don't need to construct a valid FastVideoArgs."""
from fastvideo import fastvideo_args as fva
class _Captured:
def __init__(self, **kw):
self.kwargs = kw
monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _Captured)
@@ -1,269 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""gpu_pool-style flat-kwarg integration tests.
Mirrors the ``load_kwargs`` dict that the FastVideo-internal
``ui/ltx2-streaming/server/gpu_pool.py`` passes to
``VideoGenerator.from_pretrained(**load_kwargs)`` and asserts that the
public typed ``GeneratorConfig`` surface can represent it end-to-end,
with no fields silently falling through to ``pipeline.experimental``.
"""
from __future__ import annotations
from copy import deepcopy
import pytest
from fastvideo.api.compat import (
generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config,
)
# Mirrors FastVideo-internal/ui/ltx2-streaming/server/gpu_pool.py
# :lines 233-260 (load_kwargs constructed for VideoGenerator.from_pretrained).
# Skipped here because they are opaque Python objects that legitimately
# belong in experimental:
# - pipeline_config=<PipelineConfig instance>
# - enable_torch_compile_text_encoder (not in public FastVideoArgs)
GPU_POOL_LOAD_KWARGS = {
"config_model_path": "/models/ltx2-distilled/config",
"num_gpus": 1,
"dit_layerwise_offload": False,
"use_fsdp_inference": False,
"dit_cpu_offload": False,
"vae_cpu_offload": False,
"text_encoder_cpu_offload": False,
"pin_cpu_memory": True,
"ltx2_vae_tiling": False,
"ltx2_refine_enabled": True,
"ltx2_refine_upsampler_path": "/models/ltx2-distilled/spatial_upsampler",
"ltx2_refine_lora_path": "",
"ltx2_refine_num_inference_steps": 2,
"ltx2_refine_guidance_scale": 1.0,
"ltx2_refine_add_noise": True,
"enable_torch_compile": True,
"torch_compile_kwargs": {
"backend": "inductor",
"fullgraph": True,
"mode": "max-autotune-no-cudagraphs",
"dynamic": False,
},
}
class TestGpuPoolForwardTranslation:
"""gpu_pool flat kwargs -> typed GeneratorConfig."""
@pytest.fixture(scope="class")
def config(self):
return legacy_from_pretrained_to_config(
"FastVideo/LTX2-Distilled-Diffusers",
GPU_POOL_LOAD_KWARGS,
)
def test_model_path_set(self, config) -> None:
assert config.model_path == "FastVideo/LTX2-Distilled-Diffusers"
def test_engine_basics(self, config) -> None:
assert config.engine.num_gpus == 1
assert config.engine.use_fsdp_inference is False
def test_offload_config(self, config) -> None:
assert config.engine.offload.dit is False
assert config.engine.offload.dit_layerwise is False
assert config.engine.offload.vae is False
assert config.engine.offload.text_encoder is False
assert config.engine.offload.pin_cpu_memory is True
def test_compile_config_typed_fields_extracted(self, config) -> None:
compile_config = config.engine.compile
assert compile_config.enabled is True
assert compile_config.backend == "inductor"
assert compile_config.fullgraph is True
assert compile_config.mode == "max-autotune-no-cudagraphs"
assert compile_config.dynamic is False
assert compile_config.extras == {}
def test_vae_tiling_routed_to_pipeline(self, config) -> None:
assert config.pipeline.vae_tiling is False
def test_config_model_path_routed_to_components(self, config) -> None:
assert config.pipeline.components.config_root == "/models/ltx2-distilled/config"
def test_refine_upsampler_routed_to_components(self, config) -> None:
assert config.pipeline.components.upsampler_weights == (
"/models/ltx2-distilled/spatial_upsampler")
def test_empty_refine_lora_becomes_none(self, config) -> None:
# gpu_pool passes "" to keep refine LoRA disabled; typed schema
# treats that as "no LoRA" rather than an empty-string path.
assert config.pipeline.components.lora_path is None
def test_refine_preset_overrides(self, config) -> None:
refine = config.pipeline.preset_overrides.get("refine", {})
assert refine == {
"enabled": True,
"num_inference_steps": 2,
"guidance_scale": 1.0,
"add_noise": True,
}
def test_no_experimental_leakage(self, config) -> None:
"""Every gpu_pool kwarg should have a typed home — nothing should
silently fall through to ``pipeline.experimental``."""
assert config.pipeline.experimental == {}
class TestGpuPoolReverseTranslation:
"""typed GeneratorConfig -> FastVideoArgs kwargs reproduces the
original gpu_pool flat-kwarg shape, so callers can wire a public
``gpu_pool`` through ``generator_config_to_fastvideo_args`` without
the runtime noticing."""
@pytest.fixture
def args_kwargs(self, monkeypatch):
from fastvideo import fastvideo_args as fva
captured: dict[str, object] = {}
def _capture(**kw):
captured.update(kw)
return _Captured(**kw)
class _Captured:
def __init__(self, **kw):
self.kwargs = kw
monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _capture)
config = legacy_from_pretrained_to_config(
"FastVideo/LTX2-Distilled-Diffusers",
GPU_POOL_LOAD_KWARGS,
)
generator_config_to_fastvideo_args(config)
return captured
def test_ltx2_refine_flags_reemitted(self, args_kwargs) -> None:
assert args_kwargs["ltx2_refine_enabled"] is True
assert args_kwargs["ltx2_refine_add_noise"] is True
assert args_kwargs["ltx2_refine_num_inference_steps"] == 2
assert args_kwargs["ltx2_refine_guidance_scale"] == 1.0
def test_refine_upsampler_path_reemitted(self, args_kwargs) -> None:
assert args_kwargs["ltx2_refine_upsampler_path"] == (
"/models/ltx2-distilled/spatial_upsampler")
def test_config_model_path_reemitted(self, args_kwargs) -> None:
assert args_kwargs["config_model_path"] == "/models/ltx2-distilled/config"
def test_torch_compile_kwargs_reassembled(self, args_kwargs) -> None:
assert args_kwargs["torch_compile_kwargs"] == {
"backend": "inductor",
"fullgraph": True,
"mode": "max-autotune-no-cudagraphs",
"dynamic": False,
}
def test_vae_tiling_reemitted_with_legacy_name(self, args_kwargs) -> None:
assert args_kwargs["ltx2_vae_tiling"] is False
def test_no_stray_refine_dict(self, args_kwargs) -> None:
"""preset_overrides.refine must flatten to ltx2_refine_* kwargs
rather than landing as a nested ``refine`` kwarg that
FastVideoArgs doesn't understand."""
assert "refine" not in args_kwargs
class TestRefineFlattenCoversAllTypedFields:
"""Every field on LTX2Refine{Preset,Stage}Override must survive the
round-trip through preset_overrides.refine back to ltx2_refine_*
kwargs. Guards against the hardcoded-key-tuple regression where
image_crf / video_position_offset_sec silently dropped."""
def test_all_fields_reemitted(self, monkeypatch) -> None:
from fastvideo import fastvideo_args as fva
from fastvideo.api.compat import (
generator_config_to_fastvideo_args,
)
from fastvideo.api.schema import GeneratorConfig, PipelineSelection
from fastvideo.pipelines.basic.ltx2.stage_overrides import (
REFINE_FLAT_KEYS, )
captured: dict[str, object] = {}
class _Captured:
def __init__(self, **kw):
self.kwargs = kw
def _capture(**kw):
captured.update(kw)
return _Captured(**kw)
monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _capture)
refine_payload = {
# Preset-override fields.
"enabled": True,
"add_noise": False,
# Stage-override fields.
"num_inference_steps": 3,
"guidance_scale": 1.5,
"image_crf": 18,
"video_position_offset_sec": 2.5,
}
assert set(refine_payload) == REFINE_FLAT_KEYS, (
"payload must cover every typed field to exercise the flatten loop")
config = GeneratorConfig(
model_path="/models/ltx2",
pipeline=PipelineSelection(preset_overrides={"refine": refine_payload}),
)
generator_config_to_fastvideo_args(config)
for key, value in refine_payload.items():
assert captured[f"ltx2_refine_{key}"] == value
class TestCompileExtrasPreserved:
"""Additional torch.compile kwargs beyond the four typed fields
round-trip through ``CompileConfig.extras``."""
def test_extras_preserved(self, monkeypatch) -> None:
from fastvideo import fastvideo_args as fva
captured: dict[str, object] = {}
def _capture(**kw):
captured.update(kw)
class _Captured:
def __init__(self, **kw):
self.kwargs = kw
return _Captured(**kw)
monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _capture)
kwargs = deepcopy(GPU_POOL_LOAD_KWARGS)
kwargs["torch_compile_kwargs"] = {
"backend": "inductor",
"options": {"triton.cudagraphs": False},
"disable": False,
}
config = legacy_from_pretrained_to_config(
"FastVideo/LTX2-Distilled-Diffusers", kwargs)
assert config.engine.compile.backend == "inductor"
assert config.engine.compile.extras == {
"options": {"triton.cudagraphs": False},
"disable": False,
}
generator_config_to_fastvideo_args(config)
assert captured["torch_compile_kwargs"] == {
"backend": "inductor",
"options": {"triton.cudagraphs": False},
"disable": False,
}
@@ -1,121 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Tests for typed LTX-2 stage override dataclasses."""
from __future__ import annotations
import pytest
from fastvideo.api.errors import ConfigValidationError
from fastvideo.api.presets import get_preset, validate_stage_overrides
from fastvideo.pipelines.basic.ltx2.stage_overrides import (
LTX2RefinePresetOverride,
LTX2RefineStageOverride,
REFINE_PRESET_OVERRIDE_FIELDS,
REFINE_STAGE_OVERRIDE_FIELDS,
refine_override_to_dict,
)
class TestRefineStageOverrideDataclass:
def test_all_fields_default_to_none(self) -> None:
override = LTX2RefineStageOverride()
assert override.num_inference_steps is None
assert override.guidance_scale is None
assert override.image_crf is None
assert override.video_position_offset_sec is None
def test_explicit_construction(self) -> None:
override = LTX2RefineStageOverride(
num_inference_steps=2,
guidance_scale=1.0,
image_crf=18,
video_position_offset_sec=2.5,
)
assert override.num_inference_steps == 2
assert override.guidance_scale == 1.0
assert override.image_crf == 18
assert override.video_position_offset_sec == 2.5
def test_to_dict_drops_none(self) -> None:
override = LTX2RefineStageOverride(num_inference_steps=3)
assert refine_override_to_dict(override) == {
"num_inference_steps": 3,
}
def test_to_dict_with_all_fields(self) -> None:
override = LTX2RefineStageOverride(
num_inference_steps=2,
guidance_scale=1.0,
image_crf=18,
video_position_offset_sec=0.0,
)
assert refine_override_to_dict(override) == {
"num_inference_steps": 2,
"guidance_scale": 1.0,
"image_crf": 18,
"video_position_offset_sec": 0.0,
}
def test_fields_accessor_matches_dataclass(self) -> None:
assert REFINE_STAGE_OVERRIDE_FIELDS == frozenset({
"num_inference_steps",
"guidance_scale",
"image_crf",
"video_position_offset_sec",
})
class TestRefinePresetOverrideDataclass:
def test_all_fields_default_to_none(self) -> None:
override = LTX2RefinePresetOverride()
assert override.enabled is None
assert override.add_noise is None
def test_to_dict_drops_none(self) -> None:
override = LTX2RefinePresetOverride(enabled=True)
assert refine_override_to_dict(override) == {
"enabled": True,
}
def test_to_dict_with_all_fields(self) -> None:
override = LTX2RefinePresetOverride(enabled=True, add_noise=False)
assert refine_override_to_dict(override) == {
"enabled": True,
"add_noise": False,
}
def test_fields_accessor_matches_dataclass(self) -> None:
assert REFINE_PRESET_OVERRIDE_FIELDS == frozenset({
"enabled",
"add_noise",
})
class TestStageOverridesMirrorPresetSchema:
"""The ltx2_two_stage preset's refine stage schema must list
exactly the :class:`LTX2RefineStageOverride` field names."""
def test_allowed_overrides_mirror_dataclass(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("ltx2_two_stage", "ltx2")
refine_schema = next(
s for s in preset.stage_schemas if s.name == "refine")
assert refine_schema.allowed_overrides == REFINE_STAGE_OVERRIDE_FIELDS
def test_roundtrip_through_validate_stage_overrides(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("ltx2_two_stage", "ltx2")
override = LTX2RefineStageOverride(
num_inference_steps=3,
guidance_scale=1.0,
)
validate_stage_overrides(
preset, {"refine": refine_override_to_dict(override)})
def test_unknown_field_rejected(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("ltx2_two_stage", "ltx2")
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
preset, {"refine": {"unknown_key": 1}})
+6 -112
View File
@@ -8,17 +8,12 @@ from fastvideo.api import (
ContinuationState,
GenerationRequest,
GeneratorConfig,
GpuPoolConfig,
load_run_config,
load_serve_config,
parse_config,
PlannedStage,
PromptEnhancerConfig,
PromptSafetyConfig,
RunConfig,
ServeConfig,
StreamingConfig,
WarmupConfig,
)
@@ -28,7 +23,7 @@ def test_parse_config_builds_nested_typed_config() -> None:
"model_path": "/models/ltx2",
"pipeline": {
"workload_type": "t2v",
"preset": "ltx2_two_stage",
"profile": "ltx2_two_stage",
},
},
"request": {
@@ -56,7 +51,7 @@ def test_parse_config_builds_nested_typed_config() -> None:
config = parse_config(RunConfig, raw)
assert config.generator.pipeline.preset == "ltx2_two_stage"
assert config.generator.pipeline.profile == "ltx2_two_stage"
assert config.request.prompt == ["a fox", "a wolf"]
assert config.request.state == ContinuationState(
kind="ltx2_continuation",
@@ -111,14 +106,7 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
"vae": True,
"pin_cpu_memory": True,
},
"compile": {
"enabled": False,
"backend": None,
"fullgraph": None,
"mode": None,
"dynamic": None,
"extras": {},
},
"compile": {"enabled": False, "kwargs": {}},
"enable_stage_verification": True,
"use_fsdp_inference": False,
"disable_autocast": False,
@@ -126,8 +114,8 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
},
"pipeline": {
"workload_type": None,
"preset": None,
"preset_version": None,
"profile": None,
"profile_version": None,
"components": {
"config_root": None,
"pipeline_config_path": None,
@@ -140,8 +128,7 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
"override_pipeline_cls_name": None,
"override_transformer_cls_name": None,
},
"vae_tiling": None,
"preset_overrides": {},
"profile_overrides": {},
"experimental": {},
},
},
@@ -213,96 +200,3 @@ def test_load_serve_config_supports_json_roundtrip(tmp_path) -> None:
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
-587
View File
@@ -1,587 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
import pytest
from fastvideo.api.errors import ConfigValidationError
from fastvideo.api.presets import (
InferencePreset,
PresetStageSpec,
get_all_preset_names,
get_preset,
get_presets_for_family,
register_preset,
validate_preset_selection,
validate_stage_names,
validate_stage_overrides,
)
# -------------------------------------------------------------------
# Fixtures
# -------------------------------------------------------------------
@pytest.fixture()
def _isolated_registry():
"""Run each test with an empty preset registry, restoring after."""
from fastvideo.api.presets import _PRESET_REGISTRY
saved = dict(_PRESET_REGISTRY)
_PRESET_REGISTRY.clear()
yield
_PRESET_REGISTRY.clear()
_PRESET_REGISTRY.update(saved)
_SIMPLE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
allowed_overrides=frozenset({"num_inference_steps", "guidance_scale"}),
)
_SR_STAGE = PresetStageSpec(
name="sr",
kind="super_resolution",
allowed_overrides=frozenset({"height_sr", "width_sr"}),
)
_NO_OVERRIDES_STAGE = PresetStageSpec(
name="encode",
kind="text_encoding",
)
def _make_preset(
name: str = "test_preset",
version: int = 1,
model_family: str = "test",
stage_schemas: tuple[PresetStageSpec, ...] = (_SIMPLE_STAGE, ),
**kwargs,
) -> InferencePreset:
return InferencePreset(
name=name,
version=version,
model_family=model_family,
stage_schemas=stage_schemas,
**kwargs,
)
# -------------------------------------------------------------------
# Registration and lookup
# -------------------------------------------------------------------
class TestRegistration:
@pytest.mark.usefixtures("_isolated_registry")
def test_register_and_get(self) -> None:
p = _make_preset()
register_preset(p)
assert get_preset("test_preset", "test") is p
@pytest.mark.usefixtures("_isolated_registry")
def test_get_with_explicit_version(self) -> None:
p = _make_preset(version=2)
register_preset(p)
assert get_preset("test_preset", "test", version=2) is p
@pytest.mark.usefixtures("_isolated_registry")
def test_get_latest_version(self) -> None:
p1 = _make_preset(version=1)
p2 = _make_preset(version=2)
register_preset(p1)
register_preset(p2)
assert get_preset("test_preset", "test") is p2
@pytest.mark.usefixtures("_isolated_registry")
def test_get_missing_raises(self) -> None:
with pytest.raises(ConfigValidationError, match="unknown preset"):
get_preset("nope", "test")
@pytest.mark.usefixtures("_isolated_registry")
def test_get_wrong_version_raises(self) -> None:
register_preset(_make_preset(version=1))
with pytest.raises(ConfigValidationError, match="version"):
get_preset("test_preset", "test", version=99)
@pytest.mark.usefixtures("_isolated_registry")
def test_duplicate_raises(self) -> None:
register_preset(_make_preset())
with pytest.raises(ValueError, match="Duplicate"):
register_preset(_make_preset())
@pytest.mark.usefixtures("_isolated_registry")
def test_get_presets_for_family(self) -> None:
register_preset(_make_preset(name="a"))
register_preset(_make_preset(name="b"))
register_preset(_make_preset(
name="c", model_family="other"))
result = get_presets_for_family("test")
assert {p.name for p in result} == {"a", "b"}
@pytest.mark.usefixtures("_isolated_registry")
def test_get_all_preset_names(self) -> None:
register_preset(_make_preset(name="beta"))
register_preset(_make_preset(name="alpha"))
assert get_all_preset_names() == ["alpha", "beta"]
# -------------------------------------------------------------------
# Stage-name validation
# -------------------------------------------------------------------
class TestStageNameValidation:
def test_valid_stage_name_passes(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, _SR_STAGE))
validate_stage_names(
preset, {"denoise": {}, "sr": {}})
def test_unknown_stage_name_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError, match="stage_overrides.bogus"
):
validate_stage_names(preset, {"bogus": {}})
def test_empty_overrides_passes(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
validate_stage_names(preset, {})
def test_error_lists_valid_stages(self) -> None:
preset = _make_preset(
stage_schemas=(_SIMPLE_STAGE, _SR_STAGE))
with pytest.raises(
ConfigValidationError, match="'denoise'"
):
validate_stage_names(preset, {"nope": {}})
# -------------------------------------------------------------------
# Stage-override validation
# -------------------------------------------------------------------
class TestStageOverrideValidation:
def test_allowed_override_passes(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
validate_stage_overrides(
preset,
{"denoise": {"num_inference_steps": 25}},
)
def test_disallowed_override_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError,
match="stage_overrides.denoise.height",
):
validate_stage_overrides(
preset,
{"denoise": {"height": 720}},
)
def test_override_on_stage_with_no_allowed_raises(self) -> None:
preset = _make_preset(stage_schemas=(_NO_OVERRIDES_STAGE, ))
with pytest.raises(
ConfigValidationError,
match="does not accept overrides",
):
validate_stage_overrides(
preset,
{"encode": {"some_key": 1}},
)
def test_empty_override_on_no_allowed_passes(self) -> None:
preset = _make_preset(stage_schemas=(_NO_OVERRIDES_STAGE, ))
validate_stage_overrides(preset, {"encode": {}})
def test_non_mapping_override_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(ConfigValidationError, match="mapping"):
validate_stage_overrides(
preset, {"denoise": "not a dict"})
def test_unknown_stage_still_caught(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(ConfigValidationError, match="unknown"):
validate_stage_overrides(
preset, {"missing_stage": {"a": 1}})
def test_error_lists_allowed_overrides(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError, match="guidance_scale"
):
validate_stage_overrides(
preset,
{"denoise": {"bad_key": 1}},
)
# -------------------------------------------------------------------
# validate_preset_selection end-to-end
# -------------------------------------------------------------------
class TestValidatePresetSelection:
@pytest.mark.usefixtures("_isolated_registry")
def test_none_preset_returns_none(self) -> None:
assert validate_preset_selection(
None, "test") is None
@pytest.mark.usefixtures("_isolated_registry")
def test_valid_preset_resolves(self) -> None:
p = _make_preset()
register_preset(p)
result = validate_preset_selection(
"test_preset", "test")
assert result is p
@pytest.mark.usefixtures("_isolated_registry")
def test_valid_preset_with_overrides(self) -> None:
p = _make_preset()
register_preset(p)
result = validate_preset_selection(
"test_preset",
"test",
stage_overrides={"denoise": {"guidance_scale": 2.0}},
)
assert result is p
@pytest.mark.usefixtures("_isolated_registry")
def test_invalid_preset_raises(self) -> None:
with pytest.raises(ConfigValidationError, match="unknown"):
validate_preset_selection("nope", "test")
@pytest.mark.usefixtures("_isolated_registry")
def test_bad_stage_override_raises(self) -> None:
register_preset(_make_preset())
with pytest.raises(ConfigValidationError):
validate_preset_selection(
"test_preset",
"test",
stage_overrides={"denoise": {"bad": 1}},
)
# -------------------------------------------------------------------
# Wan preset integration (uses real registry)
# -------------------------------------------------------------------
class TestWanPresets:
"""Verify the Wan presets registered from registry.py."""
def test_wan_presets_are_registered(self) -> None:
# Force registration by importing registry.
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("wan")
names = {p.name for p in presets}
assert "wan_t2v_1_3b" in names
assert "wan_t2v_14b" in names
assert "wan_i2v_14b_480p" in names
assert "wan_2_2_t2v_a14b" in names
def test_wan_t2v_1_3b_lookup(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("wan_t2v_1_3b", "wan")
assert preset.model_family == "wan"
assert preset.workload_type == "t2v"
assert len(preset.stage_schemas) == 1
assert preset.stage_schemas[0].name == "denoise"
assert preset.defaults["height"] == 480
assert preset.defaults["width"] == 832
def test_wan_2_2_allows_dual_guidance(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("wan_2_2_t2v_a14b", "wan")
stage = preset.stage_schemas[0]
assert "guidance_scale_2" in stage.allowed_overrides
assert "boundary_ratio" in stage.allowed_overrides
def test_wan_stage_override_validation(self) -> None:
import fastvideo.registry # noqa: F401
preset = get_preset("wan_t2v_14b", "wan")
# Valid override.
validate_stage_overrides(
preset,
{"denoise": {"num_inference_steps": 25}},
)
# Invalid override key.
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
preset,
{"denoise": {"height": 1080}},
)
def test_wan_model_family_in_registry(self) -> None:
from fastvideo.registry import get_model_family
family = get_model_family(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
assert family == "wan"
# -------------------------------------------------------------------
# LTX2 preset integration
# -------------------------------------------------------------------
class TestLtx2Presets:
def test_ltx2_presets_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("ltx2")
names = {p.name for p in presets}
assert names == {"ltx2_base", "ltx2_distilled", "ltx2_two_stage"}
def test_ltx2_base_lookup(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("ltx2_base", "ltx2")
assert p.workload_type == "t2v"
assert p.defaults["height"] == 512
assert p.defaults["width"] == 768
def test_ltx2_distilled_fewer_steps(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("ltx2_distilled", "ltx2")
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
# -------------------------------------------------------------------
class TestHunyuanPresets:
def test_hunyuan_presets_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("hunyuan")
names = {p.name for p in presets}
assert names == {"hunyuan_t2v", "fast_hunyuan_t2v"}
def test_fast_hunyuan_fewer_steps(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("fast_hunyuan_t2v", "hunyuan")
assert p.defaults["num_inference_steps"] == 6
# -------------------------------------------------------------------
# Hunyuan15 preset integration (includes two-stage SR)
# -------------------------------------------------------------------
class TestHunyuan15Presets:
def test_hunyuan15_presets_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("hunyuan15")
assert len(presets) == 5
def test_hunyuan15_sr_is_two_stage(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("hunyuan15_sr_1080p", "hunyuan15")
assert len(p.stage_schemas) == 2
assert p.stage_schemas[0].name == "denoise"
assert p.stage_schemas[1].name == "sr"
assert p.stage_schemas[1].kind == "super_resolution"
def test_hunyuan15_sr_stage_defaults(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("hunyuan15_sr_1080p", "hunyuan15")
sr = p.stage_defaults["sr"]
assert sr["height_sr"] == 1072
assert sr["width_sr"] == 1920
def test_hunyuan15_sr_stage_override_validation(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("hunyuan15_sr_1080p", "hunyuan15")
# Valid: override sr num_inference_steps.
validate_stage_overrides(
p, {"sr": {"num_inference_steps": 12}})
# Invalid: height not in sr allowed_overrides.
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
p, {"sr": {"height": 1080}})
# -------------------------------------------------------------------
# Cosmos / Cosmos25 preset integration
# -------------------------------------------------------------------
class TestCosmosPresets:
def test_cosmos_preset_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("cosmos")
assert len(presets) == 1
assert presets[0].name == "cosmos_predict2_2b"
def test_cosmos25_separate_family(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("cosmos25")
assert len(presets) == 1
assert presets[0].name == "cosmos25_predict2_2b"
def test_cosmos_and_cosmos25_different_fps(self) -> None:
import fastvideo.registry # noqa: F401
c = get_preset("cosmos_predict2_2b", "cosmos")
c25 = get_preset("cosmos25_predict2_2b", "cosmos25")
assert c.defaults["fps"] == 16
assert c25.defaults["fps"] == 24
# -------------------------------------------------------------------
# TurboDiffusion preset integration
# -------------------------------------------------------------------
class TestTurboDiffusionPresets:
def test_turbo_presets_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("turbodiffusion")
names = {p.name for p in presets}
assert names == {
"turbo_t2v_1_3b",
"turbo_t2v_14b",
"turbo_i2v_a14b",
}
def test_turbo_4_step(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("turbo_t2v_14b", "turbodiffusion")
assert p.defaults["num_inference_steps"] == 4
assert p.defaults["guidance_scale"] == 1.0
# -------------------------------------------------------------------
# SD35 preset integration
# -------------------------------------------------------------------
class TestSD35Presets:
def test_sd35_preset_registered(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("sd35_medium", "sd35")
assert p.workload_type == "t2i"
assert p.defaults["height"] == 512
assert p.defaults["num_frames"] == 1
# -------------------------------------------------------------------
# LingBotWorld preset integration (dual guidance)
# -------------------------------------------------------------------
class TestLingBotWorldPresets:
def test_lingbotworld_dual_guidance(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("lingbotworld_i2v", "lingbotworld")
stage = p.stage_schemas[0]
assert "guidance_scale_2" in stage.allowed_overrides
assert "boundary_ratio" in stage.allowed_overrides
def test_lingbotworld_override_validation(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("lingbotworld_i2v", "lingbotworld")
validate_stage_overrides(
p, {"denoise": {"boundary_ratio": 0.95}})
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
p, {"denoise": {"height": 720}})
# -------------------------------------------------------------------
# Remaining single-preset families
# -------------------------------------------------------------------
class TestSinglePresetFamilies:
def test_hyworld_registered(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("hyworld_t2v", "hyworld")
assert p.workload_type == "t2v"
def test_gamecraft_registered(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("gamecraft_i2v", "gamecraft")
assert p.workload_type == "i2v"
assert p.defaults["num_frames"] == 33
def test_gen3c_registered(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("gen3c_cosmos_7b", "gen3c")
assert p.defaults["num_inference_steps"] == 35
def test_matrixgame_registered(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("matrixgame_i2v", "matrixgame")
assert p.defaults["num_inference_steps"] == 3
assert p.defaults["fps"] == 25
def test_longcat_presets_registered(self) -> None:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("longcat")
names = {p.name for p in presets}
assert names == {
"longcat_t2v", "longcat_i2v", "longcat_vc"
}
# -------------------------------------------------------------------
# Cross-family: total preset count
# -------------------------------------------------------------------
class TestPresetCountIntegrity:
def test_total_preset_count(self) -> None:
"""At least the baseline 37 presets from 13 families are registered."""
import fastvideo.registry # noqa: F401
names = get_all_preset_names()
assert len(names) >= 37
@@ -12,7 +12,7 @@ import yaml
from fastvideo.api import RunConfig, ServeConfig
from fastvideo.configs.pipelines.base import PipelineConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.entrypoints.cli.generate import GenerateSubcommand
from fastvideo.entrypoints.cli.serve import ServeSubcommand
from fastvideo.entrypoints.openai import image_api, video_api
@@ -46,48 +46,22 @@ def _flatten_status_section(section: dict, valid_statuses: set[str]) -> set[str]
return names
def _get_extra_dataclass_fields(
package_names: str | tuple[str, ...],
base_cls: type,
) -> set[str]:
"""Collect dataclass fields declared on ``base_cls`` subclasses found
under any of the given package roots.
Accepts either a single package name (string) or a tuple of package
roots — the latter supports the PR 6 colocation where each model
family's ``PipelineConfig`` subclass moves from
``fastvideo.configs.pipelines.<family>`` to
``fastvideo.pipelines.basic.<family>.pipeline_configs``.
"""
if isinstance(package_names, str):
package_names = (package_names, )
def _get_extra_dataclass_fields(package_name: str, base_cls: type) -> set[str]:
package = importlib.import_module(package_name)
base_fields = {f.name for f in dataclasses.fields(base_cls)}
extras: set[str] = set()
for package_name in package_names:
package = importlib.import_module(package_name)
if not hasattr(package, "__path__"):
for _, modname, _ in pkgutil.iter_modules(package.__path__):
if modname == "__pycache__":
continue
for _, modname, is_pkg in pkgutil.walk_packages(
package.__path__, prefix=f"{package_name}."):
# Flat ``configs.pipelines.<family>`` modules carry the config
# directly; colocated ``basic.<family>.pipeline_configs``
# submodules do too. Everything else under ``basic`` is heavy
# model code we don't need to import for a schema check.
basename = modname.rsplit(".", 1)[-1]
is_flat = modname.startswith("fastvideo.configs.pipelines.")
if not is_flat and basename != "pipeline_configs":
continue
module = importlib.import_module(modname)
for obj in vars(module).values():
if (isinstance(obj, type)
and dataclasses.is_dataclass(obj)
and issubclass(obj, base_cls)
and obj is not base_cls):
extras.update(
f.name for f in dataclasses.fields(obj)
if f.name not in base_fields)
module = importlib.import_module(f"{package_name}.{modname}")
for obj in vars(module).values():
if (
isinstance(obj, type)
and dataclasses.is_dataclass(obj)
and issubclass(obj, base_cls)
and obj is not base_cls
):
extras.update(f.name for f in dataclasses.fields(obj) if f.name not in base_fields)
return extras
@@ -201,10 +175,7 @@ def test_pipeline_config_base_fields_are_classified() -> None:
def test_pipeline_config_extension_fields_are_classified() -> None:
inventory = _load_inventory()
expected = _get_extra_dataclass_fields(
("fastvideo.configs.pipelines", "fastvideo.pipelines.basic"),
PipelineConfig,
)
expected = _get_extra_dataclass_fields("fastvideo.configs.pipelines", PipelineConfig)
actual = _flatten_status_section(
inventory["surfaces"]["pipeline_config_extensions"],
set(inventory["status_definitions"]),
@@ -224,7 +195,7 @@ def test_sampling_param_base_fields_are_classified() -> None:
def test_sampling_param_extension_fields_are_classified() -> None:
inventory = _load_inventory()
expected = _get_extra_dataclass_fields("fastvideo.api.sampling_param", SamplingParam)
expected = _get_extra_dataclass_fields("fastvideo.configs.sample", SamplingParam)
actual = _flatten_status_section(
inventory["surfaces"]["sampling_param_extensions"],
set(inventory["status_definitions"]),
@@ -264,8 +235,8 @@ def test_cli_dest_inventory_matches_live_parsers() -> None:
def test_review_gap_fields_are_explicitly_inventory_tracked() -> None:
inventory = _load_inventory()
sampling_base = inventory["surfaces"]["sampling_param_base"]
assert "guidance_scale_2" in sampling_base["moved"]
sampling_extensions = inventory["surfaces"]["sampling_param_extensions"]
assert "guidance_scale_2" in sampling_extensions["moved"]
image_request = inventory["surfaces"]["openai_image_request"]
video_request = inventory["surfaces"]["openai_video_request"]
@@ -276,7 +247,7 @@ def test_review_gap_fields_are_explicitly_inventory_tracked() -> None:
def test_inventory_targets_exist_in_typed_schema() -> None:
inventory = _load_inventory()
target_statuses = {"moved", "preset_owned"}
target_statuses = {"moved", "profile_owned"}
for surface in inventory["surfaces"].values():
for status, entries in surface.items():
@@ -5,8 +5,6 @@ from unittest.mock import patch
import pytest
from fastvideo.api.parser import parse_config
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.protocol import (
ImageGenerationsRequest,
ImageResponseData,
@@ -215,236 +213,6 @@ class TestVideoBuildGenerationKwargs:
assert kw["output_path"].startswith(custom)
# ---------------------------------------------------------------------------
# video_api._build_generation_kwargs with ServeConfig.default_request
# ---------------------------------------------------------------------------
def _make_default_request(raw: dict) -> GenerationRequest:
"""Parse a raw config dict into a tracked GenerationRequest."""
return parse_config(GenerationRequest, raw)
class TestVideoDefaultRequestMerge:
@pytest.fixture(autouse=True)
def _patch_output_dir(self, tmp_path):
with patch(
"fastvideo.entrypoints.openai.video_api.get_output_dir",
return_value=str(tmp_path),
):
yield tmp_path
def _build(self, default_raw=None, **body_overrides):
from fastvideo.entrypoints.openai.video_api import (
_build_generation_kwargs, )
body_defaults = dict(prompt="a running dog", seconds=4)
body_defaults.update(body_overrides)
req = VideoGenerationsRequest(**body_defaults)
default_request = _make_default_request(
default_raw) if default_raw else None
return _build_generation_kwargs(
"req-v1", req, default_request=default_request)
def test_default_seed_flows_through_when_body_omits(self):
kw = self._build(default_raw={"sampling": {"seed": 42}})
assert kw["seed"] == 42
def test_body_seed_overrides_default(self):
kw = self._build(
default_raw={"sampling": {
"seed": 42
}},
seed=7,
)
assert kw["seed"] == 7
def test_default_fps_used_for_num_frames_from_seconds(self):
# Default fps=30, body only provides seconds=2 -> num_frames=60.
kw = self._build(default_raw={"sampling": {"fps": 30}}, seconds=2)
assert kw["fps"] == 30
assert kw["num_frames"] == 60
def test_default_guidance_scale_preserved_when_body_omits(self):
kw = self._build(default_raw={"sampling": {"guidance_scale": 5.5}})
assert kw["guidance_scale"] == 5.5
def test_body_guidance_scale_overrides_default(self):
kw = self._build(
default_raw={"sampling": {
"guidance_scale": 5.5
}},
guidance_scale=9.0,
)
assert kw["guidance_scale"] == 9.0
def test_body_size_overrides_default_sampling_dims(self):
kw = self._build(
default_raw={
"sampling": {
"width": 640,
"height": 360
}
},
size="1024x576",
)
assert kw["width"] == 1024
assert kw["height"] == 576
def test_default_width_height_preserved_when_body_omits_size(self):
kw = self._build(default_raw={
"sampling": {
"width": 640,
"height": 360
}
})
assert kw["width"] == 640
assert kw["height"] == 360
def test_default_output_path_used_as_output_dir(self, tmp_path):
custom = str(tmp_path / "from_default")
kw = self._build(default_raw={"output": {"output_path": custom}})
assert kw["output_path"].startswith(custom)
def test_body_output_path_overrides_default(self, tmp_path):
body_dir = str(tmp_path / "body")
default_dir = str(tmp_path / "default")
kw = self._build(
default_raw={"output": {
"output_path": default_dir
}},
output_path=body_dir,
)
assert kw["output_path"].startswith(body_dir)
assert default_dir not in kw["output_path"]
def test_default_negative_prompt_flows_through(self):
kw = self._build(
default_raw={"negative_prompt": "low quality, blur"})
assert kw["negative_prompt"] == "low quality, blur"
def test_body_negative_prompt_overrides_default(self):
kw = self._build(
default_raw={"negative_prompt": "low quality"},
negative_prompt="watermark",
)
assert kw["negative_prompt"] == "watermark"
def test_no_default_request_behaves_like_before(self):
kw = self._build(seed=123, fps=24)
assert kw["seed"] == 123
assert kw["fps"] == 24
def test_default_request_not_mutated_by_build(self):
# Merge should operate on a fresh copy (caller supplies a clone);
# the helper itself must not mutate the passed-in default.
default_request = _make_default_request(
{"sampling": {
"seed": 42,
"fps": 30
}})
from fastvideo.entrypoints.openai.video_api import (
_build_generation_kwargs, )
req = VideoGenerationsRequest(prompt="p", seconds=1)
_ = _build_generation_kwargs(
"req-1", req, default_request=default_request)
assert default_request.sampling.seed == 42
assert default_request.sampling.fps == 30
# ---------------------------------------------------------------------------
# Preset stage-override validation
# ---------------------------------------------------------------------------
class TestValidateDefaultRequestAgainstPreset:
"""Startup-time validation lives in api_server._validate_default_request_against_preset.
Called once by ``run_server`` before the FastAPI app is created — the
default_request is static server config, so per-request re-validation
would be pure overhead.
"""
def test_empty_stage_overrides_is_noop(self):
from fastvideo.entrypoints.openai.api_server import (
_validate_default_request_against_preset, )
default_request = _make_default_request({
"sampling": {
"seed": 42
}
})
_validate_default_request_against_preset(default_request, "any/model")
def test_unknown_model_path_is_noop(self):
from fastvideo.entrypoints.openai.api_server import (
_validate_default_request_against_preset, )
default_request = _make_default_request({
"stage_overrides": {
"denoise": {
"num_inference_steps": 10
}
}
})
with patch(
"fastvideo.entrypoints.openai.api_server.get_preset_selection",
return_value=(None, None),
):
_validate_default_request_against_preset(
default_request, "unknown/model")
def test_unknown_stage_name_raises(self):
from fastvideo.api.errors import ConfigValidationError
from fastvideo.entrypoints.openai.api_server import (
_validate_default_request_against_preset, )
default_request = _make_default_request({
"stage_overrides": {
"not_a_real_stage": {
"num_inference_steps": 10
}
}
})
with patch(
"fastvideo.entrypoints.openai.api_server.get_preset_selection",
return_value=("wan_t2v_1_3b", "wan"),
):
with pytest.raises(ConfigValidationError):
_validate_default_request_against_preset(
default_request, "Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
# ---------------------------------------------------------------------------
# Server state accessors
# ---------------------------------------------------------------------------
class TestDefaultRequestState:
def test_set_and_get_default_request(self):
from fastvideo.entrypoints.openai import state as state_mod
saved = state_mod._default_request
try:
dr = _make_default_request({"sampling": {"seed": 7}})
state_mod.set_state.__wrapped__ if False else None # keep lint happy
state_mod._default_request = dr
assert state_mod.get_default_request() is dr
finally:
state_mod._default_request = saved
def test_clear_state_resets_default_request(self):
from fastvideo.entrypoints.openai import state as state_mod
saved = state_mod._default_request
try:
state_mod._default_request = _make_default_request(
{"sampling": {
"seed": 7
}})
state_mod.clear_state()
assert state_mod.get_default_request() is None
finally:
state_mod._default_request = saved
# ---------------------------------------------------------------------------
# Protocol Pydantic models
# ---------------------------------------------------------------------------
@@ -12,7 +12,7 @@ from fastvideo.api import (
SamplingConfig,
load_run_config,
)
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.configs.sample import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import WorkloadType
+1 -1
View File
@@ -480,10 +480,10 @@ def _prepare_ssim_workspace(
{checkout_command}
rm -rf fastvideo/tests/ssim/reference_videos
git_retry git submodule update --init --recursive
uv pip install -e .[test]
cd fastvideo-kernel
./build.sh
cd ..
uv pip install -e .[test]
uv pip install git+https://github.com/microsoft/MoGe.git
export HF_HOME='/root/data/.cache'
hf auth login --token "$HF_API_KEY"
@@ -23,7 +23,6 @@ DEVICE_MAPPINGS = (
("L40S", "L40S"),
("H100", "H100"),
("H200", "H200"),
("B200", "B200"),
)
+3 -52
View File
@@ -251,8 +251,6 @@ def upload_reference_videos(
reference_dirs_by_tier: Sequence[tuple[str, Path]],
token: str,
private: bool,
model_id: str | None = None,
force: bool = False,
) -> None:
HfApi, _ = _load_hf_sdk()
api = HfApi(token=token)
@@ -263,44 +261,15 @@ def upload_reference_videos(
exist_ok=True,
)
try:
existing_repo_files = set(
api.list_repo_files(repo_id=repo_id, repo_type=repo_type))
except Exception:
# Fresh repo or list failure — treat as empty so upload can proceed.
existing_repo_files = set()
for quality_tier, reference_dir in reference_dirs_by_tier:
if not reference_dir.exists():
raise FileNotFoundError(f"Reference directory not found: {reference_dir}")
base_in_repo = f"{REFERENCE_VIDEOS_DIRNAME}/{quality_tier}/{reference_dir.name}"
if model_id:
folder_path = reference_dir / model_id
if not folder_path.exists():
raise FileNotFoundError(
f"Model subfolder not found for upload: {folder_path}")
path_in_repo = f"{base_in_repo}/{model_id}"
else:
folder_path = reference_dir
path_in_repo = base_in_repo
conflicts = sorted(
f for f in existing_repo_files
if f.startswith(f"{path_in_repo}/") or f == path_in_repo)
if conflicts and not force:
preview = "\n".join(f" - {c}" for c in conflicts[:10])
more = f"\n ... and {len(conflicts) - 10} more" if len(conflicts) > 10 else ""
raise RuntimeError(
f"Refusing to overwrite existing HF files under {path_in_repo} "
f"({len(conflicts)} file(s) already present):\n{preview}{more}\n"
f"Re-run with --force to overwrite.")
target_desc = f"{reference_dir.name}/{model_id}" if model_id else reference_dir.name
print(f"Uploading {target_desc} ({quality_tier}) to {repo_id}/{path_in_repo} ...")
path_in_repo = f"{REFERENCE_VIDEOS_DIRNAME}/{quality_tier}/{reference_dir.name}"
print(f"Uploading {reference_dir.name} ({quality_tier}) to {repo_id} ...")
api.upload_folder(
repo_id=repo_id,
repo_type=repo_type,
folder_path=str(folder_path),
folder_path=str(reference_dir),
path_in_repo=path_in_repo,
token=token,
)
@@ -531,22 +500,6 @@ def _build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Create/use a private repo instead of public.",
)
upload_parser.add_argument(
"--model-id",
default=None,
help=(
"Restrict upload to a single model subfolder "
"(reference_videos/<tier>/<device>/<model_id>). "
"Use when seeding references for a single new test."),
)
upload_parser.add_argument(
"--force",
action="store_true",
help=(
"Allow overwriting files that already exist at the target path on "
"Hugging Face. Off by default so seeding a new test cannot "
"clobber existing references."),
)
ensure_parser = subparsers.add_parser(
"ensure",
@@ -654,8 +607,6 @@ def main(argv: Sequence[str] | None = None) -> int:
reference_dirs_by_tier=reference_dirs_by_tier,
token=token,
private=args.private,
model_id=args.model_id,
force=args.force,
)
print("Upload complete.")
return 0

Some files were not shown because too many files have changed in this diff Show More