"""CAD-Documenter CLI - Main entry point.""" import sys from pathlib import Path import click from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn from rich.panel import Panel from .config import load_config, create_default_config from .pipeline import DocumentationPipeline console = Console() def print_banner(): """Print welcome banner.""" console.print(Panel.fit( "[bold blue]CAD-Documenter[/bold blue] v0.1.0\n" "[dim]Video walkthrough → Engineering documentation[/dim]", border_style="blue" )) @click.command() @click.argument("video", type=click.Path(exists=True, path_type=Path)) @click.option("-o", "--output", type=click.Path(path_type=Path), help="Output directory") @click.option("--frames-only", is_flag=True, help="Only extract frames, skip documentation") @click.option("--atomizer-hints", is_flag=True, help="Generate Atomizer FEA hints") @click.option("--bom", is_flag=True, help="Generate Bill of Materials") @click.option("--pdf", is_flag=True, help="Generate PDF via Atomaste Report Standard") @click.option("--frame-interval", type=float, help="Seconds between frame extractions") @click.option("--whisper-model", type=click.Choice(["tiny", "base", "small", "medium", "large"]), help="Whisper model size") @click.option("--api-provider", type=click.Choice(["openai", "anthropic"]), help="Vision API provider") @click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Config file path") @click.option("--init-config", is_flag=True, help="Create default config file and exit") @click.option("-v", "--verbose", is_flag=True, help="Verbose output") @click.version_option() def main( video: Path, output: Path | None, frames_only: bool, atomizer_hints: bool, bom: bool, pdf: bool, frame_interval: float | None, whisper_model: str | None, api_provider: str | None, config_path: Path | None, init_config: bool, verbose: bool, ): """ Generate engineering documentation from a CAD walkthrough video. VIDEO: Path to the video file (.mp4, .mov, .avi, etc.) Examples: cad-doc walkthrough.mp4 cad-doc video.mp4 --output ./docs --bom --atomizer-hints cad-doc video.mp4 --pdf --whisper-model medium """ print_banner() # Handle --init-config if init_config: default_path = Path.home() / ".cad-documenter.toml" create_default_config(default_path) console.print(f"[green]✓[/green] Created config file: {default_path}") console.print("[dim]Edit this file to configure API keys and defaults.[/dim]") return # Load configuration config = load_config(config_path) # Override config with CLI options if frame_interval is not None: config.processing.frame_interval = frame_interval if whisper_model is not None: config.processing.whisper_model = whisper_model if api_provider is not None: config.api.provider = api_provider # Check API key if not frames_only and not config.api.api_key: provider = config.api.provider.upper() console.print(f"[red]Error:[/red] No API key found for {config.api.provider}.") console.print(f"Set [cyan]{provider}_API_KEY[/cyan] environment variable or add to config file.") console.print(f"\nTo create a config file: [cyan]cad-doc --init-config[/cyan]") sys.exit(1) console.print(f"Processing: [cyan]{video.name}[/cyan]") if verbose: console.print(f" API: {config.api.provider} ({config.api.vision_model or 'default'})") console.print(f" Whisper: {config.processing.whisper_model}") # Default output directory if output is None: output = video.parent / f"{video.stem}_docs" output.mkdir(parents=True, exist_ok=True) console.print(f"Output: [cyan]{output}[/cyan]") # Initialize pipeline try: pipeline = DocumentationPipeline( video_path=video, output_dir=output, config=config, ) except ValueError as e: console.print(f"[red]Configuration error:[/red] {e}") sys.exit(1) # Frames only mode if frames_only: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: progress.add_task("Extracting frames...", total=None) frames = pipeline.extract_frames() console.print(f"[green]✓[/green] Extracted {len(frames)} frames to {output / 'frames'}") return # Full pipeline with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: # Step 1: Extract frames task1 = progress.add_task("[cyan]Step 1/4:[/cyan] Extracting frames...", total=None) frames = pipeline.extract_frames() progress.update(task1, description=f"[green]✓[/green] Extracted {len(frames)} frames") progress.remove_task(task1) # Step 2: Transcribe task2 = progress.add_task("[cyan]Step 2/4:[/cyan] Transcribing audio...", total=None) transcript = pipeline.transcribe_audio() seg_count = len(transcript.segments) if transcript.segments else 0 progress.update(task2, description=f"[green]✓[/green] Transcribed {seg_count} segments") progress.remove_task(task2) if verbose and transcript.full_text: console.print(Panel( transcript.full_text[:500] + ("..." if len(transcript.full_text) > 500 else ""), title="Transcript Preview", border_style="dim" )) # Step 3: Analyze task3 = progress.add_task("[cyan]Step 3/4:[/cyan] Analyzing components...", total=None) analysis = pipeline.analyze_components(frames, transcript) comp_count = len(analysis.components) progress.update(task3, description=f"[green]✓[/green] Identified {comp_count} components") progress.remove_task(task3) if verbose and analysis.components: console.print("\n[bold]Components found:[/bold]") for c in analysis.components: console.print(f" • {c.name} ({c.material or 'material unknown'})") # Step 4: Generate documentation task4 = progress.add_task("[cyan]Step 4/4:[/cyan] Generating documentation...", total=None) doc_path = pipeline.generate_documentation( analysis, atomizer_hints=atomizer_hints or config.output.include_atomizer_hints, bom=bom or config.output.include_bom, ) progress.update(task4, description=f"[green]✓[/green] Documentation generated") progress.remove_task(task4) # Generate PDF if requested if pdf: console.print("[cyan]Generating PDF...[/cyan]") try: pdf_path = pipeline.generate_pdf(doc_path) console.print(f"[green]✓[/green] PDF: {pdf_path}") except Exception as e: console.print(f"[yellow]Warning:[/yellow] PDF generation failed: {e}") # Summary console.print() console.print(Panel.fit( f"[bold green]Documentation complete![/bold green]\n\n" f"📄 [cyan]{doc_path}[/cyan]\n" f"📊 {len(analysis.components)} components documented\n" f"🖼️ {len(frames)} frames extracted", title="Summary", border_style="green" )) # Show atomizer hints summary if generated if (atomizer_hints or config.output.include_atomizer_hints) and analysis.atomizer_hints: hints = analysis.atomizer_hints if hints.objectives or hints.constraints: console.print("\n[bold]Atomizer Hints:[/bold]") for obj in hints.objectives[:3]: console.print(f" 🎯 {obj['direction'].capitalize()} {obj['name']}") for constraint in hints.constraints[:3]: console.print(f" 📏 {constraint['type']}: {constraint['value']}") if __name__ == "__main__": main()