Spaces:
Sleeping
Sleeping
| """Command-line interface for Audio Video Generator.""" | |
| import logging | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from typing import List, Optional | |
| import click | |
| from tqdm import tqdm | |
| from audio_video_generator import __version__ | |
| from audio_video_generator.config import ( | |
| ANIMATION_OPTIONS, | |
| ANIMATION_RANDOM_POOL, | |
| DEFAULT_OUTPUT_NAME, | |
| RESOLUTION_MAP, | |
| TRANSITION_OPTIONS, | |
| TRANSITION_RANDOM_POOL, | |
| ) | |
| from audio_video_generator.core.pipeline import ( | |
| VideoPipeline, | |
| VideoPipelineConfig, | |
| ) | |
| def setup_logging(verbose: bool = False) -> None: | |
| """Configure logging.""" | |
| level = logging.DEBUG if verbose else logging.INFO | |
| logging.basicConfig( | |
| level=level, | |
| format="%(levelname)s: %(message)s", | |
| handlers=[logging.StreamHandler(sys.stdout)] | |
| ) | |
| def validate_file(ctx, param, value): | |
| """Validate file exists.""" | |
| if value is None: | |
| return None | |
| if not os.path.exists(value): | |
| raise click.BadParameter(f"File not found: {value}") | |
| return value | |
| def cli(ctx: click.Context, verbose: bool) -> None: | |
| """Audio Video Generator - Synchronize images to audio. | |
| Generate videos by synchronizing images to audio using Whisper | |
| transcription and CSV mapping files. | |
| Example: | |
| avg generate -a audio.mp3 -c mapping.csv -i images.zip -o output.mp4 | |
| """ | |
| ctx.ensure_object(dict) | |
| ctx.obj["verbose"] = verbose | |
| setup_logging(verbose) | |
| def generate( | |
| ctx: click.Context, | |
| audio: str, | |
| csv: str, | |
| images: Optional[str], | |
| output: str, | |
| resolution: str, | |
| animation_mode: str, | |
| animation: Optional[str], | |
| animations: tuple, | |
| transition_mode: str, | |
| transition: Optional[str], | |
| transitions: tuple, | |
| txt_overlay: Optional[str], | |
| font_size: int, | |
| text_color: str, | |
| text_pos_x: float, | |
| text_pos_y: float, | |
| whisper_model: str, | |
| work_dir: str, | |
| fps: int, | |
| save_checkpoints: bool, | |
| keep_work_dir: bool | |
| ) -> None: | |
| """Generate video from audio, images, and CSV mapping.""" | |
| verbose = ctx.obj.get("verbose", False) | |
| # Validate images input | |
| if images is None: | |
| click.echo("Error: --images is required (ZIP file or directory)", err=True) | |
| sys.exit(1) | |
| # Determine image input mode | |
| if images.endswith(".zip"): | |
| input_mode = "ZIP" | |
| elif os.path.isdir(images): | |
| input_mode = "MANUAL" | |
| else: | |
| click.echo(f"Error: Images must be a ZIP file or directory: {images}", err=True) | |
| sys.exit(1) | |
| # Build text style config | |
| text_style = { | |
| "font_size": font_size, | |
| "text_color": text_color, | |
| "pos_x": text_pos_x, | |
| "pos_y": text_pos_y, | |
| } | |
| # Build pipeline config | |
| config = VideoPipelineConfig( | |
| audio_path=audio, | |
| csv_path=csv, | |
| input_mode=input_mode, | |
| zip_path=images if input_mode == "ZIP" else None, | |
| manual_images_dir=images if input_mode == "MANUAL" else None, | |
| output_filename=output, | |
| resolution=resolution, | |
| animation_mode=animation_mode, | |
| single_animation=animation, | |
| custom_animations=list(animations) if animations else None, | |
| transition_mode=transition_mode, | |
| single_transition=transition, | |
| custom_transitions=list(transitions) if transitions else None, | |
| txt_path=txt_overlay, | |
| enable_text_overlay=txt_overlay is not None, | |
| text_style=text_style if txt_overlay else None, | |
| whisper_model=whisper_model, | |
| work_root=work_dir, | |
| fps=fps, | |
| save_checkpoints=save_checkpoints, | |
| keep_work_dir=keep_work_dir, | |
| ) | |
| # Run pipeline | |
| pipeline = VideoPipeline(config) | |
| try: | |
| click.echo(f"Starting video generation...") | |
| click.echo(f" Audio: {audio}") | |
| click.echo(f" CSV: {csv}") | |
| click.echo(f" Images: {images} ({input_mode} mode)") | |
| click.echo(f" Output: {output}") | |
| click.echo(f" Resolution: {resolution} ({RESOLUTION_MAP[resolution][0]}x{RESOLUTION_MAP[resolution][1]})") | |
| result = pipeline.run(progress_callback=lambda msg, pct: click.echo(f" [{pct*100:5.1f}%] {msg}")) | |
| click.echo(f"\nSuccess! Video saved to: {result['output_path']}") | |
| if result.get("drive_path"): | |
| click.echo(f"Also saved to Drive: {result['drive_path']}") | |
| if verbose and result.get("report"): | |
| click.echo("\n--- Processing Report ---") | |
| click.echo(result["report"]) | |
| except Exception as e: | |
| click.echo(f"\nError: {e}", err=True) | |
| if verbose: | |
| import traceback | |
| click.echo(traceback.format_exc(), err=True) | |
| sys.exit(1) | |
| def web(port: int, host: str, share: bool) -> None: | |
| """Launch Gradio web interface.""" | |
| try: | |
| from audio_video_generator.web.gradio_ui import launch_ui | |
| click.echo(f"Starting web UI on http://{host}:{port}") | |
| launch_ui(host=host, port=port, share=share) | |
| except ImportError as e: | |
| click.echo(f"Error: Could not load web UI - {e}", err=True) | |
| sys.exit(1) | |
| def models() -> None: | |
| """List available Whisper models.""" | |
| models_info = [ | |
| ("tiny", "39 MB", "Fastest, lowest accuracy"), | |
| ("base", "74 MB", "Good balance (default)"), | |
| ("small", "244 MB", "Better accuracy"), | |
| ("medium", "769 MB", "High accuracy"), | |
| ("large", "1550 MB", "Best accuracy, slowest"), | |
| ] | |
| click.echo("Available Whisper models:") | |
| click.echo() | |
| for name, size, desc in models_info: | |
| marker = " -> " if name == "base" else " " | |
| click.echo(f"{marker}{name:8} {size:10} {desc}") | |
| click.echo() | |
| click.echo("Use with: avg generate --whisper-model <model>") | |
| def animations() -> None: | |
| """List available animations and transitions.""" | |
| click.echo("Image Animations:") | |
| for anim in ANIMATION_OPTIONS: | |
| click.echo(f" - {anim}") | |
| click.echo() | |
| click.echo("Image Transitions:") | |
| for trans in TRANSITION_OPTIONS: | |
| click.echo(f" - {trans}") | |
| def main() -> None: | |
| """Entry point for the CLI.""" | |
| cli() | |
| if __name__ == "__main__": | |
| main() | |