Skip to content

Commit cf4b92b

Browse files
charliewwdevclaude
andcommitted
fix video quality: use ffmpeg H.264 instead of OpenCV MPEG-4 for MP4 export
diffusers' export_to_video uses OpenCV with MPEG-4 Part 2 codec by default, producing severely degraded video quality. Replace with ffmpeg pipe encoding using H.264 CRF 18, with automatic fallback to OpenCV if ffmpeg is unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 40367b3 commit cf4b92b

1 file changed

Lines changed: 41 additions & 3 deletions

File tree

animatediff/core/base_pipeline.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,55 @@ def generate(
6666
...
6767

6868
def save(self, output: VideoOutput, path: str, fps: int = 8):
69-
"""Save VideoOutput to a GIF or MP4 file."""
70-
from diffusers.utils import export_to_gif, export_to_video
69+
"""Save VideoOutput to a GIF or MP4 file.
7170
71+
For MP4, uses ffmpeg with H.264 encoding for high quality.
72+
Falls back to diffusers export_to_video (OpenCV/MPEG-4) if ffmpeg
73+
is unavailable.
74+
"""
7275
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
7376

7477
if path.endswith(".mp4"):
75-
export_to_video(output.frames, path, fps=fps)
78+
self._save_mp4_ffmpeg(output.frames, path, fps)
7679
else:
80+
from diffusers.utils import export_to_gif
7781
export_to_gif(output.frames, path)
7882
logger.info(f"Saved video to {path}")
7983

84+
@staticmethod
85+
def _save_mp4_ffmpeg(frames: list, path: str, fps: int):
86+
"""Save frames to MP4 using ffmpeg pipe (H.264, CRF 18)."""
87+
import subprocess
88+
import shutil
89+
90+
if not shutil.which("ffmpeg"):
91+
from diffusers.utils import export_to_video
92+
logger.warning("ffmpeg not found, falling back to OpenCV export (lower quality)")
93+
export_to_video(frames, path, fps=fps)
94+
return
95+
96+
w, h = frames[0].size
97+
cmd = [
98+
"ffmpeg", "-y",
99+
"-f", "rawvideo", "-pix_fmt", "rgb24",
100+
"-s", f"{w}x{h}", "-r", str(fps),
101+
"-i", "pipe:0",
102+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
103+
"-pix_fmt", "yuv420p", "-an",
104+
path,
105+
]
106+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
107+
import numpy as np
108+
for frame in frames:
109+
proc.stdin.write(np.array(frame).tobytes())
110+
proc.stdin.close()
111+
proc.wait()
112+
if proc.returncode != 0:
113+
err = proc.stderr.read().decode()[-200:]
114+
logger.warning(f"ffmpeg encode failed: {err}")
115+
from diffusers.utils import export_to_video
116+
export_to_video(frames, path, fps=fps)
117+
80118
def _make_generator(self, seed: int, device: str) -> Optional[torch.Generator]:
81119
if seed >= 0:
82120
return torch.Generator(device=device).manual_seed(seed)

0 commit comments

Comments
 (0)