diff --git a/.github/scripts/plan_merge_ci.py b/.github/scripts/plan_merge_ci.py
index e00c4199d..a1e8b237c 100644
--- a/.github/scripts/plan_merge_ci.py
+++ b/.github/scripts/plan_merge_ci.py
@@ -12,6 +12,7 @@ from __future__ import annotations
import argparse
import fnmatch
import re
+from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TextIO
@@ -289,16 +290,22 @@ def _select_output_coverage(plan: MergePlan, path: str) -> None:
plan.add_ssim(SSIM_SMOKE_TESTS, reason=f"shared output SSIM smoke coverage: {path}")
-def classify_paths(paths: list[str]) -> MergePlan:
+def _normalize_path(raw_path: str) -> str:
+ path = raw_path.strip()
+ while path.startswith("./"):
+ path = path[2:]
+ return path
+
+
+def classify_paths(paths: list[str], removed_paths: Iterable[str] = ()) -> MergePlan:
+ """Plan merge lanes for ``paths``.
+
+ ``removed_paths`` lists changed paths that no longer exist at the PR head
+ (deleted files and rename sources); removed golden/SSIM tests are not run.
+ """
plan = MergePlan()
- normalized_paths: list[str] = []
- for raw_path in paths:
- path = raw_path.strip()
- while path.startswith("./"):
- path = path[2:]
- if path:
- normalized_paths.append(path)
- normalized_paths = sorted(set(normalized_paths))
+ normalized_paths = sorted({path for path in map(_normalize_path, paths) if path})
+ removed = {path for path in map(_normalize_path, removed_paths) if path}
if not normalized_paths:
plan.require_all("changed-file list was empty; failing closed")
return plan
@@ -338,7 +345,10 @@ def classify_paths(paths: list[str]) -> MergePlan:
if path.startswith("fastvideo/tests/golden_gate/"):
name = Path(path).name
if name.startswith("test_") and name.endswith(".py"):
- plan.add_golden((name, ), reason=f"changed golden test: {path}")
+ if path in removed:
+ plan.reasons.append(f"removed golden test has nothing to run: {path}")
+ else:
+ plan.add_golden((name, ), reason=f"changed golden test: {path}")
elif name in {"AGENTS.md", "README.md"}:
plan.reasons.append(f"golden documentation only: {path}")
else:
@@ -349,7 +359,10 @@ def classify_paths(paths: list[str]) -> MergePlan:
if path.startswith("fastvideo/tests/ssim/"):
name = Path(path).name
if name.startswith("test_") and name.endswith(".py"):
- plan.add_ssim((name, ), reason=f"changed SSIM test: {path}")
+ if path in removed:
+ plan.reasons.append(f"removed SSIM test has nothing to run: {path}")
+ else:
+ plan.add_ssim((name, ), reason=f"changed SSIM test: {path}")
elif path.endswith((".py", ".json", ".pt", ".png", ".mp4")):
plan.ssim_all = True
plan.add_lanes("ssim", reason=f"shared SSIM harness/reference: {path}")
@@ -525,6 +538,7 @@ def _write_summary(output: TextIO, plan: MergePlan) -> None:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--paths-file", type=Path, required=True)
+ parser.add_argument("--removed-paths-file", type=Path)
parser.add_argument("--github-output", type=Path)
parser.add_argument("--summary-file", type=Path)
return parser.parse_args()
@@ -533,7 +547,9 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
paths = args.paths_file.read_text(encoding="utf-8").splitlines()
- plan = classify_paths(paths)
+ removed_paths = (args.removed_paths_file.read_text(encoding="utf-8").splitlines()
+ if args.removed_paths_file else [])
+ plan = classify_paths(paths, removed_paths)
print(f"MERGE_TEST_PLAN={plan.encoded_lanes()}")
print(f"MERGE_GOLDEN_TESTS={plan.encoded_golden_tests()}")
print(f"MERGE_SSIM_TESTS={plan.encoded_ssim_tests()}")
diff --git a/.github/workflows/ci-trigger-full-suite.yml b/.github/workflows/ci-trigger-full-suite.yml
index e96ad9c81..f70f267c6 100644
--- a/.github/workflows/ci-trigger-full-suite.yml
+++ b/.github/workflows/ci-trigger-full-suite.yml
@@ -76,6 +76,8 @@ jobs:
set -euo pipefail
changed_json="$RUNNER_TEMP/merge-changed-files.json"
changed_paths="$RUNNER_TEMP/merge-changed-paths.txt"
+ removed_paths="$RUNNER_TEMP/merge-removed-paths.txt"
+ : > "$removed_paths"
if gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \
> "$changed_json"; then
@@ -83,6 +85,10 @@ jobs:
if [ "$observed" = "$EXPECTED_CHANGED_FILES" ]; then
jq -r '.[][] | .filename, (.previous_filename // empty)' "$changed_json" \
| sort -u > "$changed_paths"
+ # Paths absent from the PR head, so the planner never selects a deleted test.
+ jq -r '.[][] | if .status == "removed" then .filename
+ elif .status == "renamed" then (.previous_filename // empty) else empty end' \
+ "$changed_json" | sort -u > "$removed_paths"
else
echo "::warning::Changed-file API returned $observed of $EXPECTED_CHANGED_FILES paths; selecting all merge lanes."
echo '__FASTVIDEO_CI_PLAN_ALL__' > "$changed_paths"
@@ -98,6 +104,7 @@ jobs:
run: |
python3 .github/scripts/plan_merge_ci.py \
--paths-file "$RUNNER_TEMP/merge-changed-paths.txt" \
+ --removed-paths-file "$RUNNER_TEMP/merge-removed-paths.txt" \
--github-output "$GITHUB_OUTPUT" \
--summary-file "$GITHUB_STEP_SUMMARY"
diff --git a/docs/inference/support_matrix.md b/docs/inference/support_matrix.md
index 6a713a01c..e6a49f4ca 100644
--- a/docs/inference/support_matrix.md
+++ b/docs/inference/support_matrix.md
@@ -152,25 +152,25 @@ optimizations: absence means **untested**, not incompatible.
}
-| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn (Legacy Branch) | Sage Attn | VSA | BSA |
-|------------|---------------------|-------------|----------|-------------------|-----------|-----|-----|
-| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ |
-| FastWan2.2 TI2V 5B Full Attn | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ |
-| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ | ⭕ |
-| Lucy Edit Dev 5B*** | `decart-ai/Lucy-Edit-Dev` | 480P | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
-| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | ⭕ |
-| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | ⭕ |
-| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ |
-| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ |
-| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | ⭕ |
-| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅ | ✅ | ⭕ | ⭕ |
-| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | ⭕ |
-| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ | ⭕ |
-| Matrix Game 2.0 Base Distilled | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
-| Matrix Game 2.0 GTA Distilled | `FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
-| Matrix Game 2.0 TempleRun Distilled | `FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
-| Matrix Game 3.0 Base Distilled | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | 720x1280 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
-| GEN3C Cosmos 7B | `FastVideo/GEN3C-Cosmos-7B-Diffusers` | 704px1280p | ❌ | ❌ | ❌ | ⭕ | ⭕ |
+| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn (Legacy Branch) | Sage Attn | VSA |
+|------------|---------------------|-------------|----------|-------------------|-----------|-----|
+| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ |
+| FastWan2.2 TI2V 5B Full Attn | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ |
+| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ |
+| Lucy Edit Dev 5B*** | `decart-ai/Lucy-Edit-Dev` | 480P | ⭕ | ⭕ | ⭕ | ⭕ |
+| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ |
+| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ |
+| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ |
+| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ |
+| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ |
+| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅ | ✅ | ⭕ |
+| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ |
+| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ |
+| Matrix Game 2.0 Base Distilled | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ |
+| Matrix Game 2.0 GTA Distilled | `FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ |
+| Matrix Game 2.0 TempleRun Distilled | `FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ |
+| Matrix Game 3.0 Base Distilled | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | 720x1280 | ⭕ | ⭕ | ⭕ | ⭕ |
+| GEN3C Cosmos 7B | `FastVideo/GEN3C-Cosmos-7B-Diffusers` | 704px1280p | ❌ | ❌ | ❌ | ⭕ |
## Apple Silicon native runtime
diff --git a/fastvideo/models/AGENTS.md b/fastvideo/models/AGENTS.md
index 635f41eee..ab7483cd0 100644
--- a/fastvideo/models/AGENTS.md
+++ b/fastvideo/models/AGENTS.md
@@ -21,7 +21,6 @@ models/
├── schedulers/ # FlowMatch / EulerDiscrete / DPM custom schedulers
├── upsamplers/ # Hunyuan15 super-resolution
├── audio/ # Audio-VAE/decoder modules (LTX-2 audio, Stable Audio)
-├── camera/ # Camera-conditioning modules (Gen3C)
└── loader/ # component_loader.py, fsdp_load.py, weight_utils.py
```
diff --git a/fastvideo/models/registry.py b/fastvideo/models/registry.py
index f3baf6e28..66e1bef39 100644
--- a/fastvideo/models/registry.py
+++ b/fastvideo/models/registry.py
@@ -62,10 +62,6 @@ _IMAGE_TO_VIDEO_DIT_MODELS = {
"MatrixGame3WanModel": ("dits", "matrixgame3", "MatrixGame3WanModel"),
}
-# Text-to-image DiT models (2D image generation)
-_TEXT_TO_IMAGE_DIT_MODELS = {
-}
-
_TEXT_ENCODER_MODELS = {
"MMAudioDFNCLIPTextEncoder": ("encoders", "mmaudio_clip", "MMAudioDFNCLIPTextEncoder"),
"CLIPTextModel": ("encoders", "clip", "CLIPTextModel"),
@@ -104,7 +100,6 @@ _VAE_MODELS = {
"LingBotWorld2WanVAE": ("vaes", "lingbotworld2_wanvae", "LingBotWorld2WanVAE"),
"AutoencoderKL": ("vaes", "autoencoder_kl", "AutoencoderKL"),
"AutoencoderKLGen3CTokenizer": ("vaes", "gen3c_tokenizer_vae", "AutoencoderKLGen3CTokenizer"),
- "AutoencoderKLStepvideo": ("vaes", "stepvideovae", "AutoencoderKLStepvideo"),
"CausalVideoAutoencoder": ("vaes", "ltx2vae", "LTX2CausalVideoAutoencoder"),
"AutoencoderKLFlux2": ("vaes", "flux2vae", "AutoencoderKLFlux2"),
# `stable-audio-open-1.0/vae/config.json` ships `_class_name="AutoencoderOobleck"`
@@ -139,7 +134,6 @@ _UPSAMPLERS = {
_LEGACY_FAST_VIDEO_MODELS = {
**_TEXT_TO_VIDEO_DIT_MODELS,
**_IMAGE_TO_VIDEO_DIT_MODELS,
- **_TEXT_TO_IMAGE_DIT_MODELS,
**_TEXT_ENCODER_MODELS,
**_IMAGE_ENCODER_MODELS,
**_VAE_MODELS,
diff --git a/fastvideo/pipelines/AGENTS.md b/fastvideo/pipelines/AGENTS.md
index e7b7b4639..018452338 100644
--- a/fastvideo/pipelines/AGENTS.md
+++ b/fastvideo/pipelines/AGENTS.md
@@ -23,8 +23,8 @@ pipelines/
│ ├── gen3c_stages.py # Gen3C-specific stages
│ └── matrixgame2_denoising.py # Matrix-Game 2.0-specific
├── basic/ # Per-model end-to-end pipelines
-│ ├── cosmos/, flux/, flux_2/, gen3c/
-│ ├── hunyuan/, hunyuan15/, hyworld/, kandinsky5/, lingbot_video/, lingbotworld/
+│ ├── cosmos/, flux/, flux_2/, gen3c/, hunyuan/, hunyuan15/, hyworld/
+│ ├── kandinsky5/, kandinsky6/, kandinsky6_sr/, lingbot_video/, lingbotworld/
│ ├── lingbotworld2/, ltx2/, magi_human/, matrixgame2/, matrixgame3/
│ ├── minimax_h3/, mmaudio/, sd35/, stable_audio/, wan/
│ └── /{_pipeline.py, presets.py, __init__.py}
diff --git a/fastvideo/registry.py b/fastvideo/registry.py
index b01150093..fa9917efd 100644
--- a/fastvideo/registry.py
+++ b/fastvideo/registry.py
@@ -464,7 +464,7 @@ def _register_configs() -> None:
default_preset="hunyuan15_sr_1080p",
)
- # Hunyuan (excludes hyworld and versioned models)
+ # Hunyuan (excludes hyworld, versioned models, and removed GameCraft checkpoints)
register_configs(
sampling_param_cls=None,
pipeline_config_cls=HunyuanConfig,
@@ -473,8 +473,8 @@ def _register_configs() -> None:
"hunyuanvideo-community/HunyuanVideo",
],
model_detectors=[
- lambda path: "hunyuan" in path.lower() and "hyworld" not in path.lower() and "1.5" not in path.lower() and
- "1-5" not in path.lower()
+ 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",
diff --git a/fastvideo/tests/api/test_presets.py b/fastvideo/tests/api/test_presets.py
index 846f15ad7..1dcc64d58 100644
--- a/fastvideo/tests/api/test_presets.py
+++ b/fastvideo/tests/api/test_presets.py
@@ -404,6 +404,22 @@ class TestHunyuanPresets:
p = get_preset("fast_hunyuan_t2v", "hunyuan")
assert p.defaults["num_inference_steps"] == 6
+ def test_removed_gamecraft_checkpoint_is_not_detected_as_hunyuan(self, tmp_path) -> None:
+ from fastvideo.registry import get_model_family
+
+ def make_checkpoint(name: str, class_name: str) -> str:
+ model_dir = tmp_path / name
+ model_dir.mkdir()
+ (model_dir / "model_index.json").write_text(
+ json.dumps({
+ "_class_name": class_name,
+ "_diffusers_version": "0.33.1"
+ }))
+ return str(model_dir)
+
+ assert get_model_family(make_checkpoint("HunyuanVideo-Custom", "HunyuanVideoPipeline")) == "hunyuan"
+ assert get_model_family(make_checkpoint("Hunyuan-GameCraft-Diffusers", "HunyuanGameCraftPipeline")) is None
+
# -------------------------------------------------------------------
# Hunyuan15 preset integration (includes two-stage SR)
diff --git a/fastvideo/tests/contract/test_merge_ci_plan.py b/fastvideo/tests/contract/test_merge_ci_plan.py
index 8cfce78ee..af5774cd8 100644
--- a/fastvideo/tests/contract/test_merge_ci_plan.py
+++ b/fastvideo/tests/contract/test_merge_ci_plan.py
@@ -111,6 +111,28 @@ def test_changed_ssim_test_selects_only_that_file():
assert plan.encoded_ssim_tests() == "test_flux_t2i_similarity.py"
+def test_removed_golden_and_ssim_tests_are_not_selected():
+ removed = [
+ "fastvideo/tests/golden_gate/test_longcat.py",
+ "fastvideo/tests/ssim/test_longcat_similarity.py",
+ ]
+ plan = PLAN_MERGE_CI.classify_paths(removed, removed_paths=removed)
+
+ assert plan.encoded_lanes() == ",none,"
+ assert plan.encoded_golden_tests() == "none"
+ assert plan.encoded_ssim_tests() == "none"
+
+
+def test_renamed_golden_test_selects_only_the_new_name():
+ plan = PLAN_MERGE_CI.classify_paths(
+ ["fastvideo/tests/golden_gate/test_old_name.py", "fastvideo/tests/golden_gate/test_new_name.py"],
+ removed_paths=["fastvideo/tests/golden_gate/test_old_name.py"],
+ )
+
+ assert plan.encoded_lanes() == ",golden-gate,"
+ assert plan.encoded_golden_tests() == "test_new_name.py"
+
+
def test_shared_golden_harness_or_reference_requires_full_golden_lane():
plan = PLAN_MERGE_CI.classify_paths(["fastvideo/tests/golden_gate/_harness.py"])
diff --git a/fastvideo/tests/train/utils/test_finetune_config_construction.py b/fastvideo/tests/train/utils/test_finetune_config_construction.py
index cb726daa4..a73225d93 100644
--- a/fastvideo/tests/train/utils/test_finetune_config_construction.py
+++ b/fastvideo/tests/train/utils/test_finetune_config_construction.py
@@ -40,4 +40,4 @@ def test_fine_tuning_recipe_constructs(recipe: Path) -> None:
def test_recipe_glob_found_recipes() -> None:
# Guard against the glob silently matching nothing after a move.
- assert len(_RECIPES) >= 15
+ assert len(_RECIPES) >= 18
diff --git a/pyproject_other.toml b/pyproject_other.toml
index cdf5db5db..4af8eeda2 100644
--- a/pyproject_other.toml
+++ b/pyproject_other.toml
@@ -21,7 +21,9 @@ dependencies = [
"requests>=2.32.2",
# Machine Learning & Transformers
- # Floor bumped from >=4.57.3 to >=5.0.0 (stable, not rc).
+ # Floor bumped from >=4.57.3 to >=5.0.0 (stable, not rc) for GLM-Image (#1030).
+ # GLM-Image was removed in #1926, but nothing merged since has been tested
+ # against transformers 4.x, so keep the floor until that is verified.
"transformers>=5.0.0",
"tokenizers>=0.20.1",
"sentencepiece>=0.2.0",