Skip to content

Commit d2c1944

Browse files
committed
some fix
1 parent 971e230 commit d2c1944

4 files changed

Lines changed: 150 additions & 8 deletions

File tree

fastvideo/pipelines/basic/wan/wan_svi_i2v_pipeline.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ def _tensor_to_pil_list(frames: torch.Tensor) -> list[PIL.Image.Image]:
3333
return [PIL.Image.fromarray(a) for a in arr]
3434

3535

36+
def _validate_multiclip_frames(num_motion: int, num_frames: int) -> None:
37+
"""num_motion must be < num_frames, else follow-up clips stitch to empty."""
38+
if num_motion >= num_frames:
39+
raise ValueError(f"svi_num_motion_frames ({num_motion}) must be smaller than num_frames ({num_frames}) "
40+
"for multi-clip generation; otherwise stitched follow-up clips would be empty.")
41+
42+
43+
def _stitch_clip_outputs(clip_outputs: list[torch.Tensor], num_motion: int) -> torch.Tensor:
44+
"""Concat per-clip (B,C,T,H,W) videos on time, dropping each follow-up's leading num_motion frames."""
45+
concatenated = [clip_outputs[0]]
46+
for video in clip_outputs[1:]:
47+
concatenated.append(video[:, :, num_motion:, :, :])
48+
return torch.cat(concatenated, dim=2)
49+
50+
3651
class WanSVIImageToVideoPipeline(WanImageToVideoPipeline):
3752
"""
3853
Pipeline for Stable-Video-Infinity multi-clip I2V generation on Wan 2.1.
@@ -110,6 +125,9 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
110125
batch = stage(batch, fastvideo_args)
111126
return batch
112127

128+
num_frames = int(batch.num_frames) if batch.num_frames is not None else 0
129+
_validate_multiclip_frames(num_motion, num_frames)
130+
113131
# Multi-clip needs the reference image up front to construct motion frames
114132
# for clip 0. Run InputValidationStage now to resolve image_path -> pil_image.
115133
self.input_validation_stage(batch, fastvideo_args)
@@ -156,12 +174,7 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
156174
tail = clip_batch.output[0, :, -num_motion:, :, :]
157175
motion_frames = _tensor_to_pil_list(tail)
158176

159-
# Drop the first num_motion frames of each follow-up clip to avoid duplicating
160-
# the previous clip's tail in the stitched output.
161-
concatenated = [clip_outputs[0]]
162-
for video in clip_outputs[1:]:
163-
concatenated.append(video[:, :, num_motion:, :, :])
164-
batch.output = torch.cat(concatenated, dim=2)
177+
batch.output = _stitch_clip_outputs(clip_outputs, num_motion)
165178
return batch
166179

167180

fastvideo/pipelines/lora_pipeline.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,12 @@ def set_lora_adapter(self,
325325
for name, weight in lora_state_dict.items():
326326
# Extract weights (lora_A, lora_B, and lora_alpha)
327327
name = name.replace("diffusion_model.", "")
328-
name = name.replace("pipe.dit.", "")
328+
# Guarded so non-SVI / non-PEFT adapters are provably untouched.
329+
if "pipe.dit." in name:
330+
name = name.replace("pipe.dit.", "")
329331
name = name.replace(".weight", "")
330-
name = name.replace(".default", "")
332+
if ".default" in name:
333+
name = name.replace(".default", "")
331334

332335
if "lora_alpha" in name:
333336
# Store alpha with minimal mapping - same processing as lora_A/lora_B
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Path-routing tests for fastvideo.utils.maybe_download_lora."""
3+
from __future__ import annotations
4+
5+
import huggingface_hub
6+
7+
import fastvideo.utils as fv_utils
8+
from fastvideo.utils import maybe_download_lora
9+
10+
11+
def test_triple_slash_downloads_single_file(monkeypatch):
12+
"""org/repo/sub/file.safetensors -> hf_hub_download(repo_id=org/repo, filename=sub/file)."""
13+
calls: list[dict] = []
14+
15+
def fake_hf_hub_download(*, repo_id, filename, local_dir=None, **kwargs):
16+
calls.append({"repo_id": repo_id, "filename": filename, "local_dir": local_dir})
17+
return f"/cache/{repo_id}/{filename}"
18+
19+
monkeypatch.setattr(huggingface_hub, "hf_hub_download", fake_hf_hub_download)
20+
21+
result = maybe_download_lora("vita-video-gen/svi-model/version-1.0/svi-shot.safetensors")
22+
23+
assert len(calls) == 1
24+
assert calls[0]["repo_id"] == "vita-video-gen/svi-model"
25+
assert calls[0]["filename"] == "version-1.0/svi-shot.safetensors"
26+
assert result == "/cache/vita-video-gen/svi-model/version-1.0/svi-shot.safetensors"
27+
28+
29+
def test_triple_slash_keeps_only_first_two_segments_as_repo(monkeypatch):
30+
"""A deeper nesting still maps repo_id to the first two segments."""
31+
captured: dict = {}
32+
33+
def fake_hf_hub_download(*, repo_id, filename, local_dir=None, **kwargs):
34+
captured["repo_id"] = repo_id
35+
captured["filename"] = filename
36+
return "ok"
37+
38+
monkeypatch.setattr(huggingface_hub, "hf_hub_download", fake_hf_hub_download)
39+
40+
maybe_download_lora("org/repo/a/b/c/weights.safetensors")
41+
42+
assert captured["repo_id"] == "org/repo"
43+
assert captured["filename"] == "a/b/c/weights.safetensors"
44+
45+
46+
def test_local_file_short_circuits(tmp_path, monkeypatch):
47+
"""An existing local .safetensors file is returned verbatim, no download."""
48+
49+
def boom(*args, **kwargs):
50+
raise AssertionError("hf_hub_download must not be called for a local file")
51+
52+
monkeypatch.setattr(huggingface_hub, "hf_hub_download", boom)
53+
54+
f = tmp_path / "version-1.0" / "svi-shot.safetensors"
55+
f.parent.mkdir(parents=True)
56+
f.write_bytes(b"\x00")
57+
58+
assert maybe_download_lora(str(f)) == str(f)
59+
60+
61+
def test_plain_repo_id_falls_through_to_repo_download(monkeypatch):
62+
"""A two-segment HF id (no .safetensors suffix) does NOT hit the single-file branch."""
63+
64+
def boom(*args, **kwargs):
65+
raise AssertionError("two-segment repo id must not take the single-file branch")
66+
67+
monkeypatch.setattr(huggingface_hub, "hf_hub_download", boom)
68+
monkeypatch.setattr(fv_utils, "maybe_download_model", lambda *a, **k: "/cache/org/repo")
69+
monkeypatch.setattr(fv_utils, "_best_guess_weight_name", lambda *a, **k: "adapter.safetensors")
70+
71+
result = maybe_download_lora("org/repo")
72+
73+
assert result == "/cache/org/repo/adapter.safetensors"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Tests for SVI multi-clip stitch index math and the frame-budget guard."""
3+
from __future__ import annotations
4+
5+
import pytest
6+
import torch
7+
8+
from fastvideo.pipelines.basic.wan.wan_svi_i2v_pipeline import (_stitch_clip_outputs, _validate_multiclip_frames)
9+
10+
11+
def _clip(num_frames: int, fill: float) -> torch.Tensor:
12+
# (B=1, C=3, T=num_frames, H=2, W=2)
13+
return torch.full((1, 3, num_frames, 2, 2), fill, dtype=torch.float32)
14+
15+
16+
def test_stitch_drops_motion_overlap_from_followups():
17+
num_motion = 2
18+
clips = [_clip(9, 0.0), _clip(9, 1.0), _clip(9, 2.0)]
19+
20+
out = _stitch_clip_outputs(clips, num_motion)
21+
22+
expected_t = 9 + (9 - num_motion) + (9 - num_motion)
23+
assert out.shape == (1, 3, expected_t, 2, 2)
24+
25+
# Provenance: 9 from clip0, then 7 from clip1, then 7 from clip2.
26+
assert torch.all(out[:, :, :9] == 0.0)
27+
assert torch.all(out[:, :, 9:9 + 7] == 1.0)
28+
assert torch.all(out[:, :, 9 + 7:] == 2.0)
29+
30+
31+
def test_stitch_single_clip_is_identity():
32+
clip = _clip(5, 3.0)
33+
out = _stitch_clip_outputs([clip], num_motion=2)
34+
assert torch.equal(out, clip)
35+
36+
37+
def test_stitch_num_motion_one_drops_single_frame():
38+
clips = [_clip(4, 0.0), _clip(4, 1.0)]
39+
out = _stitch_clip_outputs(clips, num_motion=1)
40+
assert out.shape[2] == 4 + 3
41+
42+
43+
def test_validate_rejects_motion_ge_frames():
44+
with pytest.raises(ValueError, match="must be smaller than num_frames"):
45+
_validate_multiclip_frames(num_motion=5, num_frames=5)
46+
with pytest.raises(ValueError, match="must be smaller than num_frames"):
47+
_validate_multiclip_frames(num_motion=8, num_frames=5)
48+
49+
50+
def test_validate_accepts_motion_lt_frames():
51+
# Should not raise.
52+
_validate_multiclip_frames(num_motion=1, num_frames=81)
53+
_validate_multiclip_frames(num_motion=5, num_frames=6)

0 commit comments

Comments
 (0)