Files
CAD-Documenter/src/cad_documenter/cli.py

214 lines
8.1 KiB
Python
Raw Normal View History

"""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()