Skip to content

Commit c1aa195

Browse files
charliewwdevclaude
andcommitted
V3: unified multi-backend video generation framework
Add support for 5 video generation backends on consumer GPUs (6-24GB): - Wan 2.1 (1.3B/14B) — best quality/VRAM ratio - HunyuanVideo (8.3B) — high quality with NF4 quantization - CogVideoX (2B/5B) — lightweight entry point - LTX-Video — real-time 8-step inference - AnimateDiff legacy — full backward compatibility Core infrastructure: - Smart VRAM manager: auto-detects GPU, recommends optimal config - Unified base pipeline: load/generate/save interface for all backends - Quantization layer: BitsAndBytes NF4/INT8, torchao FP8 - torch.compile support for Ampere+ GPUs CLI: `animatediff --backend wan --prompt "..." --quality high` --backend {auto|wan|hunyuan|cogvideo|ltx|animatediff} --quality {draft|standard|high|max} presets --auto mode picks best backend for available hardware All existing --pipeline flags preserved for backward compat WebUI: new V3 Multi-Backend tab alongside legacy AnimateDiff tab Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bdcae88 commit c1aa195

15 files changed

Lines changed: 1709 additions & 383 deletions

animatediff/backends/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Video generation backends — unified access to multiple model families.
3+
4+
Available backends:
5+
- wan: Wan 2.1 (Alibaba, 1.3B-14B, best quality-to-VRAM ratio)
6+
- hunyuan: HunyuanVideo (Tencent, 8.3B, high quality)
7+
- cogvideo: CogVideoX (THU, 2B-5B, lightest)
8+
- ltx: LTX-Video (Lightricks, real-time capable)
9+
- animatediff: AnimateDiff legacy (SD1.5/SDXL/Lightning)
10+
"""
11+
12+
from typing import Dict, Type
13+
14+
BACKEND_REGISTRY: Dict[str, str] = {
15+
"wan": "animatediff.backends.wan.WanBackend",
16+
"hunyuan": "animatediff.backends.hunyuan.HunyuanBackend",
17+
"cogvideo": "animatediff.backends.cogvideo.CogVideoBackend",
18+
"ltx": "animatediff.backends.ltx.LTXBackend",
19+
"animatediff": "animatediff.backends.animatediff_legacy.AnimateDiffBackend",
20+
}
21+
22+
23+
def get_backend(name: str):
24+
"""Lazily import and return a backend class by name."""
25+
if name not in BACKEND_REGISTRY:
26+
raise ValueError(f"Unknown backend: {name}. Available: {list(BACKEND_REGISTRY.keys())}")
27+
28+
module_path, class_name = BACKEND_REGISTRY[name].rsplit(".", 1)
29+
import importlib
30+
module = importlib.import_module(module_path)
31+
return getattr(module, class_name)
32+
33+
34+
def list_backends():
35+
return list(BACKEND_REGISTRY.keys())
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
AnimateDiff Legacy Backend — wraps existing V2/SDXL/Lightning/Legacy pipelines.
3+
4+
Provides backward compatibility with all existing AnimateDiff features:
5+
- SD1.5 text-to-video with motion modules
6+
- SDXL text-to-video
7+
- Lightning ultra-fast inference (1-8 steps)
8+
- SparseCtrl (legacy pipeline only)
9+
- FreeInit, FreeNoise, IP-Adapter, Prompt Travel (V2)
10+
"""
11+
12+
import logging
13+
from typing import Optional
14+
15+
import torch
16+
from PIL import Image
17+
18+
from animatediff.core.base_pipeline import BasePipeline, VideoOutput
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class AnimateDiffBackend(BasePipeline):
24+
"""Wraps existing AnimateDiff pipelines (V2/SDXL/Lightning/Legacy)."""
25+
26+
backend_name = "animatediff"
27+
28+
def __init__(self, pipe, pipeline_type: str = "v2"):
29+
self.pipe = pipe
30+
self.pipeline_type = pipeline_type
31+
32+
@classmethod
33+
def load(
34+
cls,
35+
model_path: Optional[str] = None,
36+
torch_dtype: torch.dtype = torch.float16,
37+
device: str = "cuda",
38+
quantization: str = "none",
39+
offload_strategy: str = "none",
40+
enable_vae_slicing: bool = True,
41+
enable_vae_tiling: bool = False,
42+
pipeline_type: str = "v2",
43+
scheduler: str = "ddim",
44+
motion_adapter: Optional[str] = None,
45+
lightning_steps: int = 4,
46+
**kwargs,
47+
) -> "AnimateDiffBackend":
48+
if model_path is None:
49+
model_path = "runwayml/stable-diffusion-v1-5"
50+
51+
if pipeline_type == "sdxl":
52+
from animatediff.pipelines.pipeline_sdxl import AnimateDiffSDXL
53+
pipe = AnimateDiffSDXL.from_pretrained(
54+
model_path=model_path or "stabilityai/stable-diffusion-xl-base-1.0",
55+
motion_adapter_path=motion_adapter or "guoyww/animatediff-motion-adapter-sdxl-beta",
56+
torch_dtype=torch_dtype,
57+
device=device,
58+
scheduler=scheduler,
59+
)
60+
elif pipeline_type == "lightning":
61+
from animatediff.pipelines.pipeline_lightning import AnimateDiffLightning
62+
pipe = AnimateDiffLightning.from_pretrained(
63+
model_path=model_path or "emilianJR/epiCRealism",
64+
num_steps=lightning_steps,
65+
torch_dtype=torch_dtype,
66+
device=device,
67+
)
68+
else:
69+
from animatediff.pipelines.pipeline_v2 import AnimateDiffV2Pipeline
70+
pipe = AnimateDiffV2Pipeline.from_pretrained(
71+
model_path=model_path,
72+
motion_adapter_path=motion_adapter or "guoyww/animatediff-motion-adapter-v1-5-3",
73+
torch_dtype=torch_dtype,
74+
device=device,
75+
scheduler=scheduler,
76+
enable_vae_slicing=enable_vae_slicing,
77+
enable_vae_tiling=enable_vae_tiling,
78+
)
79+
80+
return cls(pipe, pipeline_type=pipeline_type)
81+
82+
@torch.no_grad()
83+
def generate(
84+
self,
85+
prompt: str,
86+
negative_prompt: str = "bad quality, worst quality",
87+
width: int = 512,
88+
height: int = 512,
89+
num_frames: int = 16,
90+
num_inference_steps: int = 25,
91+
guidance_scale: float = 7.5,
92+
seed: int = -1,
93+
image: Optional[Image.Image] = None,
94+
**kwargs,
95+
) -> VideoOutput:
96+
output = self.pipe.generate(
97+
prompt=prompt,
98+
negative_prompt=negative_prompt,
99+
num_frames=num_frames,
100+
height=height,
101+
width=width,
102+
num_inference_steps=num_inference_steps,
103+
guidance_scale=guidance_scale,
104+
seed=seed,
105+
**kwargs,
106+
)
107+
frames = output.frames[0]
108+
109+
return VideoOutput(
110+
frames=frames,
111+
seed=seed,
112+
backend=self.backend_name,
113+
metadata={"pipeline_type": self.pipeline_type},
114+
)
115+
116+
def save(self, output: VideoOutput, path: str, fps: int = 8):
117+
"""Use existing pipeline save or default."""
118+
if hasattr(self.pipe, "save"):
119+
# The V2/SDXL/Lightning pipelines have their own save method
120+
# but they expect their native output format, not VideoOutput.
121+
# Use base class save instead.
122+
pass
123+
super().save(output, path, fps=fps)

animatediff/backends/cogvideo.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
CogVideoX Backend — wraps diffusers CogVideoXPipeline.
3+
4+
Model variants:
5+
- THUDM/CogVideoX-2b (6 GB VRAM with FP8, lightest option)
6+
- THUDM/CogVideoX-5b (12+ GB VRAM, better quality)
7+
8+
Features:
9+
- Text-to-video with 3D causal VAE
10+
- Adaptive LayerNorm for text-video alignment
11+
- 3D full attention for motion capture
12+
- LoRA support built-in
13+
"""
14+
15+
import logging
16+
from typing import Optional
17+
18+
import torch
19+
from PIL import Image
20+
21+
from animatediff.core.base_pipeline import BasePipeline, VideoOutput
22+
from animatediff.core.quantization import get_quantization_config
23+
24+
logger = logging.getLogger(__name__)
25+
26+
COGVIDEO_MODELS = {
27+
"2B": "THUDM/CogVideoX-2b",
28+
"5B": "THUDM/CogVideoX-5b",
29+
}
30+
31+
32+
class CogVideoBackend(BasePipeline):
33+
backend_name = "cogvideo"
34+
35+
def __init__(self, pipe, model_variant: str = "2B"):
36+
self.pipe = pipe
37+
self.model_variant = model_variant
38+
39+
@classmethod
40+
def load(
41+
cls,
42+
model_path: Optional[str] = None,
43+
torch_dtype: torch.dtype = torch.float16,
44+
device: str = "cuda",
45+
quantization: str = "none",
46+
offload_strategy: str = "none",
47+
enable_vae_slicing: bool = True,
48+
enable_vae_tiling: bool = False,
49+
model_variant: str = "2B",
50+
**kwargs,
51+
) -> "CogVideoBackend":
52+
from diffusers import CogVideoXPipeline
53+
54+
if model_path is None:
55+
model_path = COGVIDEO_MODELS.get(model_variant, COGVIDEO_MODELS["2B"])
56+
57+
logger.info(f"Loading CogVideoX-{model_variant} from {model_path} (dtype={torch_dtype}, quant={quantization})")
58+
59+
quant_config = get_quantization_config(quantization, components=["transformer"])
60+
61+
load_kwargs = dict(torch_dtype=torch_dtype)
62+
if quant_config is not None:
63+
load_kwargs["quantization_config"] = quant_config
64+
65+
pipe = CogVideoXPipeline.from_pretrained(model_path, **load_kwargs)
66+
67+
instance = cls(pipe, model_variant=model_variant)
68+
69+
if offload_strategy != "none":
70+
instance._apply_offloading(pipe, offload_strategy)
71+
else:
72+
pipe.to(device)
73+
74+
instance._apply_vae_opts(pipe, slicing=enable_vae_slicing, tiling=enable_vae_tiling)
75+
76+
return instance
77+
78+
@torch.no_grad()
79+
def generate(
80+
self,
81+
prompt: str,
82+
negative_prompt: str = "",
83+
width: int = 720,
84+
height: int = 480,
85+
num_frames: int = 49,
86+
num_inference_steps: int = 50,
87+
guidance_scale: float = 6.0,
88+
seed: int = -1,
89+
image: Optional[Image.Image] = None,
90+
**kwargs,
91+
) -> VideoOutput:
92+
gen_device = "cpu" if self.pipe.device.type == "cpu" else self.pipe.device
93+
generator = self._make_generator(seed, gen_device)
94+
95+
output = self.pipe(
96+
prompt=prompt,
97+
negative_prompt=negative_prompt or None,
98+
width=width,
99+
height=height,
100+
num_frames=num_frames,
101+
num_inference_steps=num_inference_steps,
102+
guidance_scale=guidance_scale,
103+
generator=generator,
104+
output_type="pil",
105+
)
106+
frames = output.frames[0]
107+
108+
return VideoOutput(
109+
frames=frames,
110+
seed=seed,
111+
backend=self.backend_name,
112+
metadata={"model_variant": self.model_variant},
113+
)

animatediff/backends/hunyuan.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
HunyuanVideo Backend — wraps diffusers HunyuanVideoPipeline.
3+
4+
Model: Tencent-Hunyuan/HunyuanVideo (8.3B parameters)
5+
6+
Features:
7+
- High-quality text-to-video generation
8+
- Dual-stream to single-stream transformer architecture
9+
- 3D causal VAE for temporal coherence
10+
- BitsAndBytes NF4 quantization (runs on 8GB VRAM)
11+
"""
12+
13+
import logging
14+
from typing import Optional
15+
16+
import torch
17+
from PIL import Image
18+
19+
from animatediff.core.base_pipeline import BasePipeline, VideoOutput
20+
from animatediff.core.quantization import get_quantization_config
21+
22+
logger = logging.getLogger(__name__)
23+
24+
HUNYUAN_MODELS = {
25+
"default": "tencent/HunyuanVideo",
26+
}
27+
28+
29+
class HunyuanBackend(BasePipeline):
30+
backend_name = "hunyuan"
31+
32+
def __init__(self, pipe, model_variant: str = "default"):
33+
self.pipe = pipe
34+
self.model_variant = model_variant
35+
36+
@classmethod
37+
def load(
38+
cls,
39+
model_path: Optional[str] = None,
40+
torch_dtype: torch.dtype = torch.float16,
41+
device: str = "cuda",
42+
quantization: str = "none",
43+
offload_strategy: str = "model_cpu",
44+
enable_vae_slicing: bool = True,
45+
enable_vae_tiling: bool = True,
46+
model_variant: str = "default",
47+
**kwargs,
48+
) -> "HunyuanBackend":
49+
from diffusers import HunyuanVideoPipeline
50+
51+
if model_path is None:
52+
model_path = HUNYUAN_MODELS.get(model_variant, HUNYUAN_MODELS["default"])
53+
54+
logger.info(f"Loading HunyuanVideo from {model_path} (dtype={torch_dtype}, quant={quantization})")
55+
56+
quant_config = get_quantization_config(quantization, components=["transformer"])
57+
58+
load_kwargs = dict(torch_dtype=torch_dtype)
59+
if quant_config is not None:
60+
load_kwargs["quantization_config"] = quant_config
61+
62+
pipe = HunyuanVideoPipeline.from_pretrained(model_path, **load_kwargs)
63+
64+
instance = cls(pipe, model_variant=model_variant)
65+
66+
if offload_strategy != "none":
67+
instance._apply_offloading(pipe, offload_strategy)
68+
else:
69+
pipe.to(device)
70+
71+
instance._apply_vae_opts(pipe, slicing=enable_vae_slicing, tiling=enable_vae_tiling)
72+
73+
return instance
74+
75+
@torch.no_grad()
76+
def generate(
77+
self,
78+
prompt: str,
79+
negative_prompt: str = "",
80+
width: int = 720,
81+
height: int = 480,
82+
num_frames: int = 33,
83+
num_inference_steps: int = 30,
84+
guidance_scale: float = 6.0,
85+
seed: int = -1,
86+
image: Optional[Image.Image] = None,
87+
**kwargs,
88+
) -> VideoOutput:
89+
gen_device = "cpu" if self.pipe.device.type == "cpu" else self.pipe.device
90+
generator = self._make_generator(seed, gen_device)
91+
92+
output = self.pipe(
93+
prompt=prompt,
94+
width=width,
95+
height=height,
96+
num_frames=num_frames,
97+
num_inference_steps=num_inference_steps,
98+
guidance_scale=guidance_scale,
99+
generator=generator,
100+
output_type="pil",
101+
)
102+
frames = output.frames[0]
103+
104+
return VideoOutput(
105+
frames=frames,
106+
seed=seed,
107+
backend=self.backend_name,
108+
)

0 commit comments

Comments
 (0)