Files
hao-ai-lab-FastVideo/examples/inference/batch_generate.py
T

356 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Batch video generation script for FastVideo.
Takes a text file with one prompt per line and generates videos for each prompt.
Usage:
python batch_generate.py --prompts prompts.txt --output_dir outputs/
python batch_generate.py --prompts prompts.txt --model Wan-AI/Wan2.1-T2V-14B-Diffusers --num_gpus 2
"""
import argparse
import os
import re
import time
from pathlib import Path
from fastvideo import VideoGenerator
from fastvideo.configs.sample import SamplingParam
def sanitize_filename(prompt: str, max_length: int = 50) -> str:
"""
Create a safe filename from a prompt.
Args:
prompt: The prompt text
max_length: Maximum length of the filename (excluding extension)
Returns:
A sanitized filename string
"""
# Take first part of prompt and clean it
sanitized = re.sub(r'[^\w\s-]', '', prompt.lower())
sanitized = re.sub(r'[-\s]+', '_', sanitized)
sanitized = sanitized.strip('_')
# Truncate to max length
if len(sanitized) > max_length:
sanitized = sanitized[:max_length].rsplit('_', 1)[0]
return sanitized or "video"
def load_prompts(prompt_file: str) -> list[str]:
"""
Load prompts from a text file.
Args:
prompt_file: Path to the text file with prompts (one per line)
Returns:
List of prompts
"""
with open(prompt_file, 'r', encoding='utf-8') as f:
prompts = [line.strip() for line in f if line.strip()]
return prompts
def main():
parser = argparse.ArgumentParser(
description="Batch video generation with FastVideo",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic usage with default model (Wan2.1-T2V-1.3B)
python batch_generate.py --prompts prompts.txt
# Use 14B model with 2 GPUs
python batch_generate.py --prompts prompts.txt --model Wan-AI/Wan2.1-T2V-14B-Diffusers --num_gpus 2
# Custom resolution and frame count
python batch_generate.py --prompts prompts.txt --height 720 --width 1280 --num_frames 125
# Use CPU offloading for low VRAM
python batch_generate.py --prompts prompts.txt --dit_cpu_offload --vae_cpu_offload
# Use custom fine-tuned weights
python batch_generate.py --prompts prompts.txt --init_weights_from_safetensors checkpoints/my_model/transformer
"""
)
# Input/Output arguments
parser.add_argument(
"--prompts", "-p",
type=str,
required=True,
help="Path to text file with prompts (one per line)"
)
parser.add_argument(
"--output_dir", "-o",
type=str,
default="batch_outputs",
help="Directory to save generated videos (default: batch_outputs)"
)
# Model arguments
parser.add_argument(
"--model", "-m",
type=str,
default="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
help="Model path or HuggingFace model ID (default: Wan-AI/Wan2.1-T2V-1.3B-Diffusers)"
)
parser.add_argument(
"--num_gpus",
type=int,
default=1,
help="Number of GPUs to use (default: 1)"
)
# Video parameters
parser.add_argument(
"--height",
type=int,
default=None,
help="Video height (default: model default)"
)
parser.add_argument(
"--width",
type=int,
default=None,
help="Video width (default: model default)"
)
parser.add_argument(
"--num_frames",
type=int,
default=None,
help="Number of frames to generate (default: model default)"
)
parser.add_argument(
"--fps",
type=int,
default=None,
help="Frames per second (default: model default)"
)
# Sampling parameters
parser.add_argument(
"--num_inference_steps",
type=int,
default=None,
help="Number of inference steps (default: model default)"
)
parser.add_argument(
"--guidance_scale",
type=float,
default=None,
help="Guidance scale (default: model default)"
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="Random seed (default: None, uses different seed per video)"
)
parser.add_argument(
"--negative_prompt",
type=str,
default=None,
help="Negative prompt (default: model default)"
)
# Memory optimization arguments
parser.add_argument(
"--use_fsdp_inference",
action="store_true",
help="Use FSDP for inference (set if GPU OOM)"
)
parser.add_argument(
"--dit_cpu_offload",
action="store_true",
help="Offload DiT to CPU when not in use"
)
parser.add_argument(
"--vae_cpu_offload",
action="store_true",
help="Offload VAE to CPU when not in use"
)
parser.add_argument(
"--text_encoder_cpu_offload",
action="store_true",
default=True,
help="Offload text encoder to CPU when not in use (default: True)"
)
parser.add_argument(
"--no_text_encoder_cpu_offload",
action="store_true",
help="Disable text encoder CPU offloading"
)
parser.add_argument(
"--pin_cpu_memory",
action="store_true",
default=True,
help="Pin CPU memory for faster transfers (default: True)"
)
parser.add_argument(
"--no_pin_cpu_memory",
action="store_true",
help="Disable CPU memory pinning"
)
parser.add_argument(
"--init_weights_from_safetensors",
type=str,
default=None,
help="Path to custom safetensors weights to load (e.g., checkpoint/transformer directory)"
)
# Processing arguments
parser.add_argument(
"--start_idx",
type=int,
default=0,
help="Start processing from this prompt index (0-based)"
)
parser.add_argument(
"--end_idx",
type=int,
default=None,
help="Stop processing at this prompt index (exclusive)"
)
parser.add_argument(
"--filename_prefix",
type=str,
default="",
help="Prefix for output filenames"
)
parser.add_argument(
"--use_index_only",
action="store_true",
help="Use only index numbers for filenames instead of prompt text"
)
args = parser.parse_args()
# Validate inputs
if not os.path.exists(args.prompts):
raise FileNotFoundError(f"Prompts file not found: {args.prompts}")
# Load prompts
prompts = load_prompts(args.prompts)
print(f"Loaded {len(prompts)} prompts from {args.prompts}")
# Apply start/end indices
end_idx = args.end_idx if args.end_idx is not None else len(prompts)
prompts_to_process = prompts[args.start_idx:end_idx]
print(f"Processing prompts {args.start_idx} to {end_idx - 1} ({len(prompts_to_process)} videos)")
# Create output directory
os.makedirs(args.output_dir, exist_ok=True)
# Handle boolean flag inversions
text_encoder_cpu_offload = not args.no_text_encoder_cpu_offload
pin_cpu_memory = not args.no_pin_cpu_memory
# Initialize generator
print(f"\nInitializing VideoGenerator with model: {args.model}")
if args.init_weights_from_safetensors:
print(f"Loading custom weights from: {args.init_weights_from_safetensors}")
generator = VideoGenerator.from_pretrained(
args.model,
num_gpus=args.num_gpus,
use_fsdp_inference=args.use_fsdp_inference,
dit_cpu_offload=args.dit_cpu_offload,
vae_cpu_offload=args.vae_cpu_offload,
text_encoder_cpu_offload=text_encoder_cpu_offload,
pin_cpu_memory=pin_cpu_memory,
init_weights_from_safetensors=args.init_weights_from_safetensors,
)
print("Generator initialized successfully!\n")
# Build optional kwargs for generate_video
generate_kwargs = {}
if args.height is not None:
generate_kwargs["height"] = args.height
if args.width is not None:
generate_kwargs["width"] = args.width
if args.num_frames is not None:
generate_kwargs["num_frames"] = args.num_frames
if args.fps is not None:
generate_kwargs["fps"] = args.fps
if args.num_inference_steps is not None:
generate_kwargs["num_inference_steps"] = args.num_inference_steps
if args.guidance_scale is not None:
generate_kwargs["guidance_scale"] = args.guidance_scale
if args.negative_prompt is not None:
generate_kwargs["negative_prompt"] = args.negative_prompt
# Process each prompt
total_start_time = time.time()
successful = 0
failed = 0
for i, prompt in enumerate(prompts_to_process):
global_idx = args.start_idx + i
# Generate filename
if args.use_index_only:
filename = f"{args.filename_prefix}{global_idx:04d}"
else:
prompt_part = sanitize_filename(prompt)
filename = f"{args.filename_prefix}{global_idx:04d}_{prompt_part}"
output_path = os.path.join(args.output_dir, f"{filename}.mp4")
print(f"\n{'='*60}")
print(f"[{i+1}/{len(prompts_to_process)}] Generating video {global_idx}")
print(f"Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}")
print(f"Output: {output_path}")
print(f"{'='*60}")
try:
video_start_time = time.time()
# Set seed for this video if specified
kwargs = generate_kwargs.copy()
if args.seed is not None:
kwargs["seed"] = args.seed + i # Different seed per video but reproducible
# Generate video
result = generator.generate_video(
prompt=prompt,
output_path=output_path,
save_video=True,
**kwargs
)
video_time = time.time() - video_start_time
print(f"✓ Video generated in {video_time:.2f}s")
successful += 1
except Exception as e:
print(f"✗ Failed to generate video: {e}")
failed += 1
continue
# Summary
total_time = time.time() - total_start_time
print(f"\n{'='*60}")
print("BATCH GENERATION COMPLETE")
print(f"{'='*60}")
print(f"Total videos: {len(prompts_to_process)}")
print(f"Successful: {successful}")
print(f"Failed: {failed}")
print(f"Total time: {total_time:.2f}s")
if successful > 0:
print(f"Average time per video: {total_time/successful:.2f}s")
print(f"Output directory: {args.output_dir}")
# Shutdown generator
generator.shutdown()
if __name__ == "__main__":
main()