Files
naxci1-ComfyUI-FlashVSR_Stable/cli_main.py

576 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FlashVSR Command-Line Interface
===============================
A mirror-grade CLI that maps 1:1 with the ComfyUI node inputs.
All parameters from FlashVSRNode, FlashVSRNodeAdv, and FlashVSRNodeInitPipe
are exposed as command-line arguments.
Usage:
python cli_main.py --input video.mp4 --output upscaled.mp4 --scale 2
For full help:
python cli_main.py --help
"""
import argparse
import os
import sys
import gc
# =============================================================================
# CLI argument parsing - EXHAUSTIVE mapping from ComfyUI node INPUT_TYPES
# =============================================================================
def parse_args():
"""
Parse command-line arguments.
Every argument corresponds directly to a parameter in the ComfyUI node
INPUT_TYPES (FlashVSRNode, FlashVSRNodeAdv, FlashVSRNodeInitPipe).
"""
parser = argparse.ArgumentParser(
description="FlashVSR CLI - Video Super Resolution",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic 2x upscale with defaults
python cli_main.py --input video.mp4 --output upscaled.mp4 --scale 2
# 4x upscale with tiling enabled for lower VRAM
python cli_main.py --input video.mp4 --output upscaled.mp4 --scale 4 \\
--tiled_vae --tiled_dit --tile_size 256 --tile_overlap 24
# Long video with chunking to prevent OOM
python cli_main.py --input long_video.mp4 --output upscaled.mp4 \\
--frame_chunk_size 50 --mode tiny-long
# Low VRAM mode (8GB GPUs)
python cli_main.py --input video.mp4 --output upscaled.mp4 --scale 2 \\
--vae_model LightVAE_W2.1 --tiled_vae --tiled_dit \\
--frame_chunk_size 20 --resize_factor 0.5
For more information, visit: https://github.com/naxci1/ComfyUI-FlashVSR_Stable
"""
)
# ==========================================================================
# Required arguments
# ==========================================================================
parser.add_argument(
'--input', '-i',
type=str,
required=True,
help='Input video file path (e.g., video.mp4)'
)
parser.add_argument(
'--output', '-o',
type=str,
required=True,
help='Output video file path (e.g., upscaled.mp4)'
)
# ==========================================================================
# FlashVSRNodeInitPipe parameters (Pipeline Initialization)
# ==========================================================================
parser.add_argument(
'--model',
type=str,
choices=['FlashVSR', 'FlashVSR-v1.1'],
default='FlashVSR-v1.1',
help='FlashVSR model version. V1.1 is recommended for better stability. (default: FlashVSR-v1.1)'
)
parser.add_argument(
'--mode',
type=str,
choices=['tiny', 'tiny-long', 'full'],
default='tiny',
help='Operation mode. "tiny": faster, standard memory. "tiny-long": optimized for long videos (lower VRAM). "full": higher quality but max VRAM. (default: tiny)'
)
parser.add_argument(
'--vae_model',
type=str,
choices=['Wan2.1', 'Wan2.2', 'LightVAE_W2.1', 'TAE_W2.2', 'LightTAE_HY1.5'],
default='Wan2.1',
help='VAE model: Wan2.1 (default), Wan2.2, LightVAE_W2.1 (50%% less VRAM), TAE_W2.2, LightTAE_HY1.5. Auto-downloads if missing. (default: Wan2.1)'
)
parser.add_argument(
'--force_offload',
action='store_true',
default=True,
help='Force offloading of models to CPU RAM after execution to free up VRAM. (default: True)'
)
parser.add_argument(
'--no_force_offload',
action='store_true',
help='Disable force offloading (keeps models in VRAM).'
)
parser.add_argument(
'--precision',
type=str,
choices=['fp16', 'bf16', 'auto'],
default='auto',
help="Inference precision. 'auto' selects bf16 if supported (RTX 30/40/50 series), otherwise fp16. (default: auto)"
)
parser.add_argument(
'--device',
type=str,
default='auto',
help='Computation device (e.g., "cuda:0", "cuda:1", "cpu", "auto"). (default: auto)'
)
parser.add_argument(
'--attention_mode',
type=str,
choices=['sparse_sage_attention', 'block_sparse_attention', 'flash_attention_2', 'sdpa'],
default='sparse_sage_attention',
help='Attention mechanism backend. "sparse_sage"/"block_sparse" use efficient sparse attention. "flash_attention_2"/"sdpa" use dense attention. (default: sparse_sage_attention)'
)
# ==========================================================================
# FlashVSRNodeAdv parameters (Processing)
# ==========================================================================
parser.add_argument(
'--scale',
type=int,
choices=[2, 4],
default=2,
help='Upscaling factor. 2x or 4x. Higher scale requires more VRAM and compute. (default: 2)'
)
parser.add_argument(
'--color_fix',
action='store_true',
default=True,
help='Apply wavelet-based color correction to match output colors with input. (default: True)'
)
parser.add_argument(
'--no_color_fix',
action='store_true',
help='Disable color correction.'
)
parser.add_argument(
'--tiled_vae',
action='store_true',
default=False,
help='Enable spatial tiling for the VAE decoder. Reduces VRAM usage significantly but is slower.'
)
parser.add_argument(
'--tiled_dit',
action='store_true',
default=False,
help='Enable spatial tiling for the Diffusion Transformer (DiT). Crucial for saving VRAM on large inputs.'
)
parser.add_argument(
'--tile_size',
type=int,
default=256,
help='Size of the tiles for DiT processing (32-1024). Smaller = less VRAM, more tiles, slower. (default: 256)'
)
parser.add_argument(
'--tile_overlap',
type=int,
default=24,
help='Overlap pixels between tiles to blend seams (8-512). Higher = smoother transitions. (default: 24)'
)
parser.add_argument(
'--unload_dit',
action='store_true',
default=False,
help='Unload the DiT model from VRAM before VAE decoding starts. Use if VAE decode runs out of memory.'
)
parser.add_argument(
'--sparse_ratio',
type=float,
default=2.0,
help='Control for sparse attention (1.5-2.0). 1.5 is faster, 2.0 is more stable/quality. (default: 2.0)'
)
parser.add_argument(
'--kv_ratio',
type=float,
default=3.0,
help='Key/Value cache ratio (1.0-3.0). 1.0 uses less VRAM; 3.0 provides highest quality retention. (default: 3.0)'
)
parser.add_argument(
'--local_range',
type=int,
choices=[9, 11],
default=11,
help='Local attention range window. 9 = sharper details; 11 = more stable/consistent results. (default: 11)'
)
parser.add_argument(
'--seed',
type=int,
default=0,
help='Random seed for noise generation. Same seed + same settings = reproducible results. (default: 0)'
)
parser.add_argument(
'--frame_chunk_size',
type=int,
default=0,
help='Process video in chunks of N frames to prevent VRAM OOM. 0 = Process all frames at once. (default: 0)'
)
parser.add_argument(
'--enable_debug',
action='store_true',
default=False,
help='Enable verbose logging to console. Shows VRAM usage, step times, tile info, and detailed progress.'
)
parser.add_argument(
'--keep_models_on_cpu',
action='store_true',
default=True,
help='Move models to CPU RAM instead of keeping them in VRAM when not in use. (default: True)'
)
parser.add_argument(
'--no_keep_models_on_cpu',
action='store_true',
help='Keep models in VRAM (faster but uses more VRAM).'
)
parser.add_argument(
'--resize_factor',
type=float,
default=1.0,
help='Resize input frames before processing (0.1-1.0). Set to 0.5 for large 1080p+ videos. (default: 1.0)'
)
# ==========================================================================
# Video I/O parameters
# ==========================================================================
parser.add_argument(
'--fps',
type=float,
default=None,
help='Output video FPS. If not specified, uses input video FPS.'
)
parser.add_argument(
'--codec',
type=str,
default='libx264',
help='Video codec for output (e.g., libx264, libx265, h264_nvenc). (default: libx264)'
)
parser.add_argument(
'--crf',
type=int,
default=18,
help='Constant Rate Factor for quality (0-51, lower = better quality). (default: 18)'
)
parser.add_argument(
'--start_frame',
type=int,
default=0,
help='Start processing from this frame index (0-indexed). (default: 0)'
)
parser.add_argument(
'--end_frame',
type=int,
default=-1,
help='Stop processing at this frame index (-1 = process all). (default: -1)'
)
# ==========================================================================
# Model paths (optional, for custom model locations)
# ==========================================================================
parser.add_argument(
'--models_dir',
type=str,
default=None,
help='Custom path to FlashVSR models directory. If not set, uses ComfyUI default or ./models'
)
return parser.parse_args()
# =============================================================================
# Video I/O utilities
# =============================================================================
def load_video_frames(video_path, start_frame=0, end_frame=-1):
"""
Load video frames from a file.
Returns:
frames: torch.Tensor of shape (N, H, W, C) with values in [0, 1]
fps: float, original video FPS
"""
import torch
import numpy as np
try:
import cv2
except ImportError:
raise ImportError("OpenCV is required for video loading. Install with: pip install opencv-python")
if not os.path.exists(video_path):
raise FileNotFoundError(f"Input video not found: {video_path}")
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise RuntimeError(f"Failed to open video: {video_path}")
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if end_frame < 0 or end_frame > total_frames:
end_frame = total_frames
frames = []
frame_idx = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_idx >= start_frame and frame_idx < end_frame:
# Convert BGR to RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Normalize to [0, 1]
frame_normalized = frame_rgb.astype(np.float32) / 255.0
frames.append(frame_normalized)
frame_idx += 1
if frame_idx >= end_frame:
break
cap.release()
if len(frames) == 0:
raise RuntimeError(f"No frames loaded from video: {video_path}")
# Stack frames into tensor: (N, H, W, C)
frames_tensor = torch.from_numpy(np.stack(frames, axis=0))
print(f"Loaded {len(frames)} frames from {video_path} ({fps:.2f} FPS)")
print(f"Frame dimensions: {frames_tensor.shape[1]}x{frames_tensor.shape[2]}")
return frames_tensor, fps
def save_video_frames(frames_tensor, output_path, fps, codec='libx264', crf=18):
"""
Save video frames to a file.
Args:
frames_tensor: torch.Tensor of shape (N, H, W, C) with values in [0, 1]
output_path: Output video file path
fps: Output FPS
codec: Video codec
crf: Constant Rate Factor
"""
import torch
import numpy as np
try:
import cv2
except ImportError:
raise ImportError("OpenCV is required for video saving. Install with: pip install opencv-python")
# Convert tensor to numpy
if isinstance(frames_tensor, torch.Tensor):
frames_np = frames_tensor.cpu().numpy()
else:
frames_np = frames_tensor
# Ensure values are in [0, 1] and convert to uint8
frames_np = np.clip(frames_np, 0.0, 1.0)
frames_np = (frames_np * 255).astype(np.uint8)
n_frames, height, width, channels = frames_np.shape
# Determine codec fourcc
if codec in ['libx264', 'h264']:
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
elif codec in ['libx265', 'hevc']:
fourcc = cv2.VideoWriter_fourcc(*'hvc1')
else:
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
# Ensure output directory exists
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
if not out.isOpened():
raise RuntimeError(f"Failed to create output video: {output_path}")
for i in range(n_frames):
# Convert RGB to BGR for OpenCV
frame_bgr = cv2.cvtColor(frames_np[i], cv2.COLOR_RGB2BGR)
out.write(frame_bgr)
out.release()
print(f"Saved {n_frames} frames to {output_path} ({fps:.2f} FPS)")
print(f"Output dimensions: {width}x{height}")
# =============================================================================
# Main CLI entry point
# =============================================================================
def main():
args = parse_args()
# Handle boolean flag pairs
force_offload = args.force_offload and not args.no_force_offload
color_fix = args.color_fix and not args.no_color_fix
keep_models_on_cpu = args.keep_models_on_cpu and not args.no_keep_models_on_cpu
print("=" * 60)
print("FlashVSR CLI - Video Super Resolution")
print("=" * 60)
print(f"Input: {args.input}")
print(f"Output: {args.output}")
print(f"Model: {args.model}, Mode: {args.mode}")
print(f"VAE: {args.vae_model}, Scale: {args.scale}x")
print("=" * 60)
# ==========================================================================
# Setup environment and imports
# ==========================================================================
# Mock ComfyUI modules for standalone CLI operation
import sys
from unittest.mock import MagicMock
# Create mock folder_paths module
folder_paths_mock = MagicMock()
if args.models_dir:
folder_paths_mock.models_dir = args.models_dir
else:
# Default to ./models or ComfyUI default
folder_paths_mock.models_dir = os.path.join(os.path.dirname(__file__), "models")
folder_paths_mock.get_filename_list = MagicMock(return_value=[])
sys.modules['folder_paths'] = folder_paths_mock
# Create mock comfy modules
comfy_mock = MagicMock()
comfy_utils_mock = MagicMock()
comfy_utils_mock.ProgressBar = MagicMock()
sys.modules['comfy'] = comfy_mock
sys.modules['comfy.utils'] = comfy_utils_mock
# Now import FlashVSR modules
import torch
# Set device
device = args.device
if device == "auto":
if torch.cuda.is_available():
device = "cuda:0"
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
device = "mps"
else:
device = "cpu"
print(f"Device: {device}")
# Import FlashVSR modules after mocking
from nodes import (
init_pipeline, flashvsr, log,
VAE_MODEL_OPTIONS, VAE_MODEL_MAP
)
from src.models import wan_video_dit
# ==========================================================================
# Load input video
# ==========================================================================
print("\nLoading input video...")
frames, input_fps = load_video_frames(
args.input,
start_frame=args.start_frame,
end_frame=args.end_frame
)
# Use output FPS if specified, otherwise use input FPS
output_fps = args.fps if args.fps is not None else input_fps
# ==========================================================================
# Initialize pipeline
# ==========================================================================
print("\nInitializing FlashVSR pipeline...")
# Set attention mode
wan_video_dit.ATTENTION_MODE = args.attention_mode
# Determine dtype
if args.precision == "auto":
if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
dtype = torch.bfloat16
print("Auto-detected bf16 support.")
else:
dtype = torch.float16
print("Defaulting to fp16.")
elif args.precision == "bf16":
dtype = torch.bfloat16
else:
dtype = torch.float16
# Set CUDA device if using CUDA
if device.startswith("cuda"):
torch.cuda.set_device(device)
# Initialize the pipeline
pipe = init_pipeline(
model=args.model,
mode=args.mode,
device=device,
dtype=dtype,
vae_model=args.vae_model
)
# ==========================================================================
# Process video with FlashVSR
# ==========================================================================
print("\nProcessing video with FlashVSR...")
output_frames = flashvsr(
pipe=pipe,
frames=frames,
scale=args.scale,
color_fix=color_fix,
tiled_vae=args.tiled_vae,
tiled_dit=args.tiled_dit,
tile_size=args.tile_size,
tile_overlap=args.tile_overlap,
unload_dit=args.unload_dit,
sparse_ratio=args.sparse_ratio,
kv_ratio=args.kv_ratio,
local_range=args.local_range,
seed=args.seed,
force_offload=keep_models_on_cpu, # flashvsr() uses force_offload param for CPU offloading
enable_debug=args.enable_debug,
chunk_size=args.frame_chunk_size,
resize_factor=args.resize_factor,
mode=args.mode
)
# ==========================================================================
# Save output video
# ==========================================================================
print("\nSaving output video...")
save_video_frames(
frames_tensor=output_frames,
output_path=args.output,
fps=output_fps,
codec=args.codec,
crf=args.crf
)
# ==========================================================================
# Cleanup
# ==========================================================================
del pipe, frames, output_frames
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
print("\n" + "=" * 60)
print("FlashVSR processing complete!")
print("=" * 60)
if __name__ == "__main__":
main()