From ca51b10c453553c9fb1f4f65f6304ed2dec538c5 Mon Sep 17 00:00:00 2001 From: Mario Lavoie Date: Tue, 27 Jan 2026 20:18:28 +0000 Subject: [PATCH] Add comprehensive documentation and examples - docs/USAGE.md: Full usage guide with CLI options, Python API, troubleshooting - docs/ATOMIZER_INTEGRATION.md: Guide for FEA/Atomizer integration - examples/sample_config.toml: Annotated configuration example - README.md: Expanded with installation, usage, architecture --- README.md | 190 +++++++++++-- docs/ATOMIZER_INTEGRATION.md | 182 +++++++++++++ docs/USAGE.md | 220 +++++++++++++++ examples/sample_config.toml | 49 ++++ pyproject.toml | 44 ++- src/cad_documenter/__init__.py | 35 ++- src/cad_documenter/cli.py | 481 ++++++++++++++++++++++----------- 7 files changed, 1010 insertions(+), 191 deletions(-) create mode 100644 docs/ATOMIZER_INTEGRATION.md create mode 100644 docs/USAGE.md create mode 100644 examples/sample_config.toml diff --git a/README.md b/README.md index e21271e..d9ca864 100644 --- a/README.md +++ b/README.md @@ -2,61 +2,132 @@ **One video → Complete engineering documentation.** -Transform video walkthroughs of CAD models into comprehensive, structured documentation — ready for CDRs, FEA setups, and integration with the Atomaste engineering ecosystem. +Transform video walkthroughs of CAD models into comprehensive, structured documentation — ready for CDRs, FEA setups, and client deliverables. + +[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## The Problem -- Documentation is tedious — Engineers spend hours documenting CAD models manually -- Knowledge lives in heads — Verbal explanations during reviews aren't captured -- CDR prep is painful — Gathering images, writing descriptions, creating BOMs -- FEA setup requires context — Atomizer needs model understanding that's often verbal +- **Documentation is tedious** — Engineers spend hours documenting CAD models manually +- **Knowledge lives in heads** — Verbal explanations during reviews aren't captured +- **CDR prep is painful** — Gathering images, writing descriptions, creating BOMs +- **FEA setup requires context** — Atomizer needs model understanding that's often verbal ## The Solution -### Input -- 📹 Video of engineer explaining a CAD model -- Optional: CAD file references, existing P/N databases +Record yourself explaining your CAD model. CAD-Documenter: + +1. **Extracts key frames** at scene changes (smart, not fixed intervals) +2. **Transcribes your explanation** via Whisper +3. **Analyzes visually** using GPT-4o or Claude vision +4. **Correlates visual + verbal** to identify components +5. **Generates documentation** with images, BOM, and Atomizer hints ### Output + - 📄 **Markdown documentation** — Structured, version-controlled -- 📊 **Bill of Materials** — With standardized P/N -- 🔧 **Component registry** — Parts, functions, materials, specs -- 🎯 **Atomizer hints** — Parameters, constraints, objectives for FEA -- 📑 **CDR-ready PDF** — Via Atomaste Report Standard +- 📊 **Bill of Materials** — CSV with components, materials, functions +- 🔧 **Component registry** — Detailed specs per component +- 🎯 **Atomizer hints** — FEA objectives, constraints, parameters +- 📑 **PDF** — Professional output via Atomaste Report Standard ## Installation ```bash -# Clone the repo +# Clone git clone http://100.80.199.40:3000/Antoine/CAD-Documenter.git cd CAD-Documenter -# Install dependencies (using uv) +# Install with uv uv sync + +# Or with pip +pip install -e . ``` ### Requirements + - Python 3.12+ - ffmpeg (for video/audio processing) -- Whisper (for transcription) +- OpenAI or Anthropic API key (for vision analysis) + +```bash +# macOS +brew install ffmpeg + +# Ubuntu/Debian +sudo apt install ffmpeg + +# Windows (with chocolatey) +choco install ffmpeg +``` + +## Quick Start + +```bash +# Set API key +export OPENAI_API_KEY="sk-your-key" + +# Run +uv run cad-doc walkthrough.mp4 + +# With all features +uv run cad-doc walkthrough.mp4 --bom --atomizer-hints --pdf +``` ## Usage ```bash -# Basic documentation +# Basic cad-doc video.mp4 -# Full pipeline with all integrations -cad-doc video.mp4 \ - --output docs/my_assembly/ \ - --atomizer-hints \ - --bom \ - --pdf +# Custom output directory +cad-doc video.mp4 --output ./my_docs/ + +# Full pipeline +cad-doc video.mp4 --bom --atomizer-hints --pdf # Just extract frames -cad-doc video.mp4 --frames-only --output frames/ +cad-doc video.mp4 --frames-only + +# Use Anthropic instead of OpenAI +cad-doc video.mp4 --api-provider anthropic + +# Better transcription (slower) +cad-doc video.mp4 --whisper-model medium ``` +## Configuration + +Create a config file: + +```bash +cad-doc --init-config +# Creates ~/.cad-documenter.toml +``` + +Or set environment variables: + +```bash +export OPENAI_API_KEY="sk-..." # Required for OpenAI +export ANTHROPIC_API_KEY="sk-..." # Required for Anthropic +export CAD_DOC_PROVIDER="anthropic" # Override default provider +``` + +See [docs/USAGE.md](docs/USAGE.md) for full configuration options. + +## Recording Tips + +For best results when recording: + +1. **Spin slowly** — Give the AI time to see each angle +2. **Name components** — "This is the main bracket..." +3. **Mention materials** — "Made of 6061 aluminum" +4. **Describe functions** — "This holds the motor" +5. **Note constraints** — "Must fit within 200mm" +6. **Point out features** — "These fillets reduce stress" + ## Architecture ``` @@ -76,18 +147,77 @@ cad-doc video.mp4 --frames-only --output frames/ └─────────────────────────────────────────────────────────────────────┘ ``` -## Integrations +## Atomizer Integration -- **Atomizer** → FEA setup instructions from verbal explanations -- **Part Manager** → Standardized P/N lookup -- **Atomaste Report Standard** → Professional PDF generation +CAD-Documenter generates FEA optimization hints for Atomizer: -## Project Status +```bash +cad-doc walkthrough.mp4 --atomizer-hints +``` -🚧 **Phase 1: Core Pipeline (MVP)** — In Progress +Output `atomizer_hints.json`: +```json +{ + "objectives": [{"name": "mass", "direction": "minimize"}], + "constraints": [{"type": "frequency", "value": ">100 Hz"}], + "parameters": ["thickness", "fillet_radius"], + "critical_regions": [{"feature": "fillet", "concern": "stress_concentration"}] +} +``` -See [ROADMAP.md](ROADMAP.md) for full implementation plan. +See [docs/ATOMIZER_INTEGRATION.md](docs/ATOMIZER_INTEGRATION.md) for details. + +## Python API + +```python +from cad_documenter.pipeline import DocumentationPipeline + +pipeline = DocumentationPipeline( + video_path="walkthrough.mp4", + output_dir="./docs" +) + +results = pipeline.run_full_pipeline( + atomizer_hints=True, + bom=True, + pdf=True +) +``` + +## Project Structure + +``` +CAD-Documenter/ +├── src/cad_documenter/ +│ ├── cli.py # Command-line interface +│ ├── pipeline.py # Main orchestrator +│ ├── config.py # Configuration management +│ ├── video_processor.py # Frame extraction +│ ├── audio_analyzer.py # Whisper transcription +│ ├── vision_analyzer.py # AI vision analysis +│ └── doc_generator.py # Output generation +├── prompts/ # AI prompts +├── templates/ # Jinja2 templates +├── tests/ # Test suite +├── docs/ # Documentation +└── examples/ # Example configs +``` + +## Roadmap + +- [x] Core pipeline (frames, transcription, vision) +- [x] Configuration system +- [x] Atomizer hints extraction +- [x] BOM generation +- [ ] Part Manager integration (P/N lookup) +- [ ] Interactive review mode +- [ ] Gitea auto-publish +- [ ] SolidWorks add-in ## License MIT + +## Credits + +Built by [Atomaste](https://atomaste.ca) for the engineering community. diff --git a/docs/ATOMIZER_INTEGRATION.md b/docs/ATOMIZER_INTEGRATION.md new file mode 100644 index 0000000..40ecae3 --- /dev/null +++ b/docs/ATOMIZER_INTEGRATION.md @@ -0,0 +1,182 @@ +# Atomizer Integration Guide + +CAD-Documenter generates FEA optimization hints that integrate with Atomizer, the Atomaste optimization framework. + +## Overview + +When you explain your CAD model in a video walkthrough, CAD-Documenter extracts: + +1. **Optimization objectives** - What you want to minimize/maximize +2. **Design constraints** - Limits and requirements +3. **Design parameters** - Variables that could be optimized +4. **Critical regions** - Areas requiring careful FEA attention + +## Verbal Cues → Atomizer Hints + +### Objectives + +| What You Say | Generated Objective | +|--------------|---------------------| +| "minimize the weight" | `{"name": "mass", "direction": "minimize"}` | +| "maximize stiffness" | `{"name": "stiffness", "direction": "maximize"}` | +| "reduce stress" | `{"name": "stress", "direction": "minimize"}` | +| "keep the frequency high" | `{"name": "frequency", "direction": "maximize"}` | +| "minimize deflection" | `{"name": "displacement", "direction": "minimize"}` | + +### Constraints + +| What You Say | Generated Constraint | +|--------------|---------------------| +| "must fit in 200mm" | `{"type": "envelope", "value": "200mm"}` | +| "under 500 grams" | `{"type": "limit", "value": "500g"}` | +| "frequency above 100 Hz" | `{"type": "minimum", "value": "100 Hz"}` | +| "stress cannot exceed 150 MPa" | `{"type": "maximum", "value": "150 MPa"}` | + +### Parameters + +Mentioning these terms adds them to potential design parameters: + +- thickness, wall thickness +- radius, fillet radius +- diameter, hole size +- length, width, height +- angle, spacing, count +- rib dimensions + +### Critical Regions + +| What You Mention | FEA Concern | +|-----------------|-------------| +| "this fillet is important" | Stress concentration | +| "corner stress" | Stress concentration | +| "bearing load here" | Contact stress | +| "weld joint" | Fatigue concern | +| "interface between parts" | Contact analysis | + +## Output Format + +### atomizer_hints.json + +```json +{ + "assembly_name": "Motor Bracket Assembly", + "generated": "2026-01-27T20:15:00", + + "model_understanding": { + "components": [ + { + "name": "Main Bracket", + "material": "Aluminum 6061-T6", + "function": "Motor mounting", + "features": ["M6 holes", "fillet radii"] + } + ], + "materials_mentioned": ["Aluminum 6061-T6", "Steel"] + }, + + "optimization_hints": { + "objectives": [ + {"name": "mass", "direction": "minimize", "source": "Mentioned 'lightweight' in transcript"}, + {"name": "stiffness", "direction": "maximize", "source": "Mentioned 'stiff' in transcript"} + ], + "constraints": [ + {"type": "envelope", "value": "200mm", "raw_match": "must fit in (\\d+)"}, + {"type": "minimum", "value": "100 Hz", "raw_match": "greater than (\\d+)"} + ], + "parameters": ["thickness", "fillet_radius", "rib_count"] + }, + + "fea_hints": { + "critical_regions": [ + {"feature": "fillet", "concern": "stress_concentration"}, + {"feature": "interface", "concern": "contact_analysis"} + ], + "study_suggestions": [ + "Modal analysis recommended - frequency/vibration mentioned", + "Topology optimization could reduce mass while maintaining performance" + ] + }, + + "transcript_summary": "This assembly provides structural support for a NEMA 23 motor..." +} +``` + +## Using with Atomizer + +### 1. Generate Hints + +```bash +cad-doc walkthrough.mp4 --atomizer-hints +``` + +### 2. Load in Atomizer + +```python +# In Atomizer study setup +from atomizer import Study +import json + +with open("walkthrough_docs/atomizer_hints.json") as f: + hints = json.load(f) + +study = Study.from_hints(hints) + +# Hints pre-populate: +# - Objective functions +# - Constraints +# - Suggested parameters +# - Critical regions for mesh refinement +``` + +### 3. Review and Refine + +The hints are suggestions, not final configurations: + +- Verify objectives match your actual goals +- Add specific constraint values +- Map parameters to CAD model variables +- Adjust mesh refinement regions + +## Best Practices + +### During Recording + +1. **State objectives clearly** + - "The goal is to minimize weight while maintaining stiffness" + - "We need to keep the first natural frequency above 100 Hz" + +2. **Quantify constraints** + - "Maximum envelope is 200mm by 150mm by 100mm" + - "Weight budget is 500 grams" + - "Stress must stay below 150 MPa" + +3. **Point out critical features** + - "This fillet is critical for stress concentration" + - "The bearing surface here sees high contact stress" + +4. **Mention what could be optimized** + - "The wall thickness could potentially be reduced" + - "These rib dimensions are candidates for optimization" + +### After Generation + +1. Review `atomizer_hints.json` for accuracy +2. Add missing numerical values +3. Map parameters to your CAD model +4. Use critical regions to guide mesh refinement +5. Run initial FEA to validate hints + +## Example Workflow + +1. **Record walkthrough** explaining bracket design +2. **Run CAD-Documenter** with `--atomizer-hints` +3. **Review hints** in generated JSON +4. **Import into Atomizer** for optimization study +5. **Iterate** based on FEA results + +## Limitations + +- Hints are extracted from verbal/visual cues only +- Numerical values may need refinement +- Complex multi-objective problems need manual setup +- Material properties should be verified against database diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..a6183a3 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,220 @@ +# CAD-Documenter Usage Guide + +## Quick Start + +```bash +# Install +cd /path/to/CAD-Documenter +uv sync + +# Set API key +export OPENAI_API_KEY="sk-your-key" + +# Run +uv run cad-doc your_video.mp4 +``` + +## Basic Commands + +### Generate Documentation +```bash +cad-doc video.mp4 +``` + +Creates `video_docs/` with: +- `documentation.md` - Main documentation +- `frames/` - Extracted keyframes + +### With All Features +```bash +cad-doc video.mp4 --bom --atomizer-hints --pdf +``` + +Creates additional: +- `bom.csv` - Bill of Materials +- `atomizer_hints.json` - FEA setup hints +- `documentation.pdf` - PDF version + +### Custom Output Directory +```bash +cad-doc video.mp4 --output ./my_docs/ +``` + +## Recording Tips + +For best results when recording your CAD walkthrough: + +### Video +- Use 1080p or higher resolution +- 30 FPS is sufficient +- Record the CAD viewport directly (not your whole screen) +- Spin the model slowly and steadily +- Pause briefly when showing details + +### Audio +- Use a decent microphone +- Speak clearly and at normal pace +- Name each component explicitly: "This is the main bracket..." +- Mention materials: "Made of 6061 aluminum" +- Describe functions: "This holds the motor in place" +- Note constraints: "Must fit within 200mm envelope" +- Call out features: "These fillets reduce stress concentration" + +### Structure +1. Start with an overview (assembly name, purpose) +2. Go through major components +3. Show details and features +4. Discuss assembly relationships +5. Mention any constraints or requirements + +## Configuration + +### Create Config File +```bash +cad-doc --init-config +``` + +Creates `~/.cad-documenter.toml` with defaults. + +### Config Options + +```toml +[api] +provider = "openai" # or "anthropic" +# api_key = "sk-..." # Or use env var + +[processing] +whisper_model = "base" # tiny/base/small/medium/large +use_scene_detection = true +max_frames = 15 + +[output] +include_bom = true +include_atomizer_hints = true +``` + +### Environment Variables + +```bash +export OPENAI_API_KEY="sk-..." # For OpenAI +export ANTHROPIC_API_KEY="sk-..." # For Anthropic +export CAD_DOC_PROVIDER="anthropic" # Override provider +export CAD_DOC_WHISPER_MODEL="medium" # Override Whisper model +``` + +## CLI Options + +``` +Usage: cad-doc [OPTIONS] VIDEO + +Options: + -o, --output PATH Output directory + --frames-only Only extract frames, skip analysis + --atomizer-hints Generate Atomizer FEA hints + --bom Generate Bill of Materials + --pdf Generate PDF output + --frame-interval FLOAT Seconds between frames + --whisper-model [tiny|base|small|medium|large] + Whisper model size + --api-provider [openai|anthropic] + Vision API provider + --config PATH Config file path + --init-config Create default config file + -v, --verbose Verbose output + --version Show version + --help Show this message +``` + +## Python API + +```python +from cad_documenter.pipeline import DocumentationPipeline + +# Simple usage +pipeline = DocumentationPipeline( + video_path="walkthrough.mp4", + output_dir="./docs" +) + +results = pipeline.run_full_pipeline( + atomizer_hints=True, + bom=True, + pdf=True +) + +print(f"Documentation: {results['documentation']}") +print(f"Components: {results['components']}") +``` + +### With Progress Callback + +```python +def on_progress(progress): + print(f"[{progress.stage.value}] {progress.message} ({progress.progress:.0%})") + +pipeline = DocumentationPipeline( + video_path="walkthrough.mp4", + output_dir="./docs", + progress_callback=on_progress +) + +results = pipeline.run() +``` + +## Atomizer Integration + +The `atomizer_hints.json` output is designed for use with Atomizer FEA optimization: + +```json +{ + "assembly_name": "Motor Bracket Assembly", + "optimization_hints": { + "objectives": [ + {"name": "mass", "direction": "minimize"}, + {"name": "stiffness", "direction": "maximize"} + ], + "constraints": [ + {"type": "envelope", "value": "200mm"}, + {"type": "frequency", "value": ">100 Hz"} + ], + "parameters": ["thickness", "fillet_radius", "rib_count"] + }, + "fea_hints": { + "critical_regions": [ + {"feature": "fillet", "concern": "stress_concentration"} + ], + "study_suggestions": [ + "Modal analysis recommended - frequency/vibration mentioned" + ] + } +} +``` + +## Troubleshooting + +### "No API key found" +Set your API key: +```bash +export OPENAI_API_KEY="sk-..." +``` +Or add to config file. + +### "No frames extracted" +- Check video file is valid +- Try different frame_interval +- Ensure ffmpeg is installed + +### "No components detected" +- Ensure video clearly shows the model +- Speak component names clearly +- Try larger Whisper model: `--whisper-model medium` +- Check API key has sufficient credits + +### "Transcription failed" +- Video may have no audio track +- Use `--frames-only` to skip transcription +- Check ffmpeg can extract audio: `ffmpeg -i video.mp4 -vn audio.wav` + +### PDF generation fails +- Install pandoc: `apt install pandoc texlive-xetex` +- Or use Typst with Atomaste Report Standard diff --git a/examples/sample_config.toml b/examples/sample_config.toml new file mode 100644 index 0000000..e871677 --- /dev/null +++ b/examples/sample_config.toml @@ -0,0 +1,49 @@ +# CAD-Documenter Sample Configuration +# Copy to ~/.cad-documenter.toml and customize + +[api] +# Vision API provider: "openai" or "anthropic" +provider = "openai" + +# API key - REQUIRED for vision analysis +# Can also be set via OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable +# api_key = "sk-your-key-here" + +# Model overrides (optional - uses provider defaults if not set) +# For OpenAI: gpt-4o (default), gpt-4o-mini +# For Anthropic: claude-sonnet-4-20250514 (default), claude-3-haiku-20240307 +# vision_model = "gpt-4o" +# text_model = "gpt-4o-mini" + +[processing] +# Whisper model for transcription +# Options: tiny (fastest), base (default), small, medium, large (most accurate) +whisper_model = "base" + +# Seconds between frame extractions (used if scene detection disabled) +frame_interval = 2.0 + +# Use AI-powered scene change detection for smarter frame selection +use_scene_detection = true + +# Maximum frames to analyze (limits API costs) +max_frames = 15 + +# Scene detection sensitivity (0.0-1.0, lower = more sensitive) +scene_threshold = 0.3 + +[output] +# Include Bill of Materials table in documentation +include_bom = true + +# Include Atomizer FEA hints section +include_atomizer_hints = true + +# Include raw transcript at end of documentation +include_raw_transcript = true + +# Keep extracted frames in output directory +include_frames = true + +# PDF template name (for future template support) +pdf_template = "default" diff --git a/pyproject.toml b/pyproject.toml index 63c9cd1..e70db6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cad-documenter" -version = "0.1.0" +version = "0.2.0" description = "Video walkthrough → Complete engineering documentation" readme = "README.md" requires-python = ">=3.12" @@ -8,26 +8,48 @@ license = {text = "MIT"} authors = [ {name = "Antoine Letarte", email = "antoine.letarte@gmail.com"}, ] +keywords = ["cad", "documentation", "engineering", "video", "ai", "vision", "whisper"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Manufacturing", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Multimedia :: Video", +] + dependencies = [ "click>=8.1.0", "rich>=13.0.0", - "pydantic>=2.0.0", "jinja2>=3.1.0", "openai-whisper>=20231117", "pillow>=10.0.0", - "httpx>=0.27.0", - "tomli>=2.0.0;python_version<'3.11'", + # Vision API clients + "anthropic>=0.40.0", + "openai>=1.50.0", ] [project.optional-dependencies] dev = [ "pytest>=8.0.0", + "pytest-cov>=4.0.0", "ruff>=0.1.0", + "mypy>=1.0.0", +] +pdf = [ + "pandoc", # For PDF generation fallback ] [project.scripts] cad-doc = "cad_documenter.cli:main" +[project.urls] +Homepage = "http://100.80.199.40:3000/Antoine/CAD-Documenter" +Documentation = "http://100.80.199.40:3000/Antoine/CAD-Documenter" +Repository = "http://100.80.199.40:3000/Antoine/CAD-Documenter" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -38,3 +60,17 @@ packages = ["src/cad_documenter"] [tool.ruff] line-length = 100 target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] +ignore = ["E501"] # Line length handled separately + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] + +[tool.mypy] +python_version = "3.12" +strict = false +warn_return_any = true +warn_unused_ignores = true diff --git a/src/cad_documenter/__init__.py b/src/cad_documenter/__init__.py index ff61d9d..a991372 100644 --- a/src/cad_documenter/__init__.py +++ b/src/cad_documenter/__init__.py @@ -1,3 +1,34 @@ -"""CAD-Documenter: Video walkthrough → Complete engineering documentation.""" +"""CAD-Documenter: Video walkthrough → Engineering documentation.""" -__version__ = "0.1.0" +__version__ = "0.2.0" + +from .pipeline import DocumentationPipeline, create_pipeline, PipelineResult +from .config import Config, load_config +from .video_processor import VideoProcessor, FrameInfo, VideoMetadata +from .audio_analyzer import AudioAnalyzer, Transcript, TranscriptSegment +from .vision_analyzer import VisionAnalyzer, Component, ComponentAnalysis +from .doc_generator import DocGenerator + +__all__ = [ + # Main entry points + "DocumentationPipeline", + "create_pipeline", + "PipelineResult", + # Configuration + "Config", + "load_config", + # Video processing + "VideoProcessor", + "FrameInfo", + "VideoMetadata", + # Audio processing + "AudioAnalyzer", + "Transcript", + "TranscriptSegment", + # Vision analysis + "VisionAnalyzer", + "Component", + "ComponentAnalysis", + # Documentation + "DocGenerator", +] diff --git a/src/cad_documenter/cli.py b/src/cad_documenter/cli.py index e93e8b1..3866de7 100644 --- a/src/cad_documenter/cli.py +++ b/src/cad_documenter/cli.py @@ -5,208 +5,379 @@ from pathlib import Path import click from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn from rich.panel import Panel +from rich.table import Table -from .config import load_config, create_default_config -from .pipeline import DocumentationPipeline +from .pipeline import DocumentationPipeline, PipelineProgress, PipelineStage, create_pipeline +from .config import Config, load_config console = Console() def print_banner(): - """Print welcome banner.""" + """Print the CAD-Documenter banner.""" console.print(Panel.fit( - "[bold blue]CAD-Documenter[/bold blue] v0.1.0\n" + "[bold blue]CAD-Documenter[/bold blue] v0.2.0\n" "[dim]Video walkthrough → Engineering documentation[/dim]", border_style="blue" )) -@click.command() +def progress_handler(progress: PipelineProgress): + """Handle progress updates from pipeline.""" + stage_icons = { + PipelineStage.INIT: "🔧", + PipelineStage.FRAMES: "🎬", + PipelineStage.TRANSCRIPTION: "🎤", + PipelineStage.ANALYSIS: "🔍", + PipelineStage.DOCUMENTATION: "📝", + PipelineStage.PDF: "📄", + PipelineStage.COMPLETE: "✅", + } + icon = stage_icons.get(progress.stage, "⏳") + + if progress.error: + console.print(f" [red]✗[/red] {progress.message}") + else: + console.print(f" {icon} {progress.message}") + + +@click.group(invoke_without_command=True) +@click.pass_context +def cli(ctx): + """CAD-Documenter: Generate engineering documentation from video walkthroughs.""" + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +@cli.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("--skip-transcription", is_flag=True, help="Skip audio transcription") @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( +@click.option("--frame-mode", type=click.Choice(["interval", "scene", "hybrid"]), + default="hybrid", help="Frame extraction mode") +@click.option("--frame-interval", default=2.0, help="Seconds between frames (interval mode)") +@click.option("--whisper-model", default="base", + help="Whisper model size (tiny/base/small/medium/large)") +@click.option("--vision-provider", type=click.Choice(["anthropic", "openai"]), + default="anthropic", help="Vision API provider") +@click.option("--vision-model", default=None, help="Vision model name (provider-specific)") +@click.option("--config", type=click.Path(path_type=Path), help="Config file path") +@click.option("--verbose", "-v", is_flag=True, help="Verbose output") +def process( video: Path, output: Path | None, frames_only: bool, + skip_transcription: 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, + frame_mode: str, + frame_interval: float, + whisper_model: str, + vision_provider: str, + vision_model: str | None, + config: Path | None, verbose: bool, ): """ - Generate engineering documentation from a CAD walkthrough video. + Process a video walkthrough and generate documentation. 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 + + cad-doc process video.mp4 + + cad-doc process video.mp4 --atomizer-hints --bom --pdf + + cad-doc process video.mp4 --frame-mode scene --vision-provider openai """ 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) + console.print(f"\n📹 Processing: [cyan]{video}[/cyan]") + + # Load or create config + cfg = load_config(config) # 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}") - + cfg.frame_extraction.mode = frame_mode + cfg.frame_extraction.interval_seconds = frame_interval + cfg.transcription.model = whisper_model + cfg.vision.provider = vision_provider + if vision_model: + cfg.vision.model = vision_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 + + console.print(f"📁 Output: [cyan]{output}[/cyan]\n") + + # Create pipeline + pipeline = DocumentationPipeline( + video_path=video, + output_dir=output, + config=cfg, + progress_callback=progress_handler if verbose else None, + ) + + # Show video info 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 + metadata = pipeline.get_video_metadata() + console.print(f" Duration: {metadata.duration:.1f}s | " + f"Resolution: {metadata.width}x{metadata.height} | " + f"Audio: {'✓' if metadata.has_audio else '✗'}") + except Exception: + pass + + console.print() + + # Run pipeline with progress with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), console=console, + disable=verbose, # Disable progress bar if verbose (use callback instead) ) 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, + task = progress.add_task("Processing...", total=100) + + def update_progress(p: PipelineProgress): + progress.update(task, completed=int(p.progress * 100), description=p.message) + if verbose: + progress_handler(p) + + pipeline.progress_callback = update_progress + + result = pipeline.run( + frames_only=frames_only, + skip_transcription=skip_transcription, + atomizer_hints=atomizer_hints, + bom=bom, + pdf=pdf, ) - 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 + + # Print results 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 result.success: + console.print(Panel.fit( + f"[bold green]✓ Documentation generated successfully![/bold green]\n\n" + f"📊 Frames extracted: {result.frames_extracted}\n" + f"🔧 Components found: {result.components_found}\n" + f"🎤 Audio duration: {result.transcript_duration:.1f}s", + title="Results", + border_style="green" + )) + + # Show output files + table = Table(title="Output Files", show_header=True) + table.add_column("Type", style="cyan") + table.add_column("Path") + + if result.documentation_path: + table.add_row("Documentation", str(result.documentation_path)) + if result.atomizer_hints_path: + table.add_row("Atomizer Hints", str(result.atomizer_hints_path)) + if result.bom_path: + table.add_row("BOM", str(result.bom_path)) + if result.pdf_path: + table.add_row("PDF", str(result.pdf_path)) + + console.print(table) + + # Show warnings + if result.warnings: + console.print("\n[yellow]Warnings:[/yellow]") + for warning in result.warnings: + console.print(f" ⚠️ {warning}") + else: + console.print(Panel.fit( + f"[bold red]✗ Pipeline failed[/bold red]\n\n" + + "\n".join(result.errors), + title="Error", + border_style="red" + )) + sys.exit(1) + + +@cli.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("--mode", type=click.Choice(["interval", "scene", "hybrid"]), + default="hybrid", help="Extraction mode") +@click.option("--interval", default=2.0, help="Seconds between frames") +@click.option("--threshold", default=0.3, help="Scene change threshold") +def frames(video: Path, output: Path | None, mode: str, interval: float, threshold: float): + """ + Extract frames from a video without full processing. + + Useful for previewing what frames will be analyzed. + """ + from .video_processor import VideoProcessor + from .config import FrameExtractionConfig + + print_banner() + console.print(f"\n📹 Extracting frames from: [cyan]{video}[/cyan]") + + if output is None: + output = video.parent / f"{video.stem}_frames" + + config = FrameExtractionConfig( + mode=mode, + interval_seconds=interval, + scene_threshold=threshold, + ) + + processor = VideoProcessor(video, output, config) + + with console.status("Extracting frames..."): + frames_list = processor.extract_frames() + + console.print(f"\n[green]✓[/green] Extracted {len(frames_list)} frames to {output}") + + # Show frame timestamps + table = Table(title="Extracted Frames") + table.add_column("#", style="dim") + table.add_column("Timestamp") + table.add_column("File") + table.add_column("Scene Score") + + for i, frame in enumerate(frames_list[:20]): # Show first 20 + table.add_row( + str(i + 1), + f"{frame.timestamp:.2f}s", + frame.path.name, + f"{frame.scene_score:.2f}" if frame.scene_score else "-" + ) + + if len(frames_list) > 20: + table.add_row("...", f"({len(frames_list) - 20} more)", "", "") + + console.print(table) + + +@cli.command() +@click.argument("video", type=click.Path(exists=True, path_type=Path)) +@click.option("--model", default="base", help="Whisper model size") +@click.option("--output", "-o", type=click.Path(path_type=Path), help="Output file") +def transcribe(video: Path, model: str, output: Path | None): + """ + Transcribe audio from a video file. + + Outputs transcript with timestamps. + """ + from .audio_analyzer import AudioAnalyzer + from .config import TranscriptionConfig + + print_banner() + console.print(f"\n🎤 Transcribing: [cyan]{video}[/cyan]") + + config = TranscriptionConfig(model=model) + analyzer = AudioAnalyzer(video, config) + + with console.status(f"Transcribing with Whisper ({model})..."): + transcript = analyzer.transcribe() + + console.print(f"\n[green]✓[/green] Transcribed {len(transcript.segments)} segments") + console.print(f" Duration: {transcript.duration:.1f}s") + console.print(f" Language: {transcript.language}\n") + + # Save or display transcript + if output: + lines = [] + for seg in transcript.segments: + lines.append(f"[{seg.start:.2f} - {seg.end:.2f}] {seg.text}") + output.write_text("\n".join(lines)) + console.print(f"Saved to: {output}") + else: + for seg in transcript.segments[:10]: + console.print(f"[dim][{seg.start:.1f}s][/dim] {seg.text}") + if len(transcript.segments) > 10: + console.print(f"[dim]... ({len(transcript.segments) - 10} more segments)[/dim]") + + +@cli.command() +@click.option("--output", "-o", type=click.Path(path_type=Path), help="Output config file") +def init(output: Path | None): + """ + Create a default configuration file. + """ + from .config import Config + + if output is None: + output = Path(".cad-documenter.json") + + config = Config() + config.to_file(output) + + console.print(f"[green]✓[/green] Created config file: {output}") + console.print("\nEdit this file to customize:") + console.print(" - Vision model and provider") + console.print(" - Whisper transcription settings") + console.print(" - Frame extraction parameters") + + +@cli.command() +@click.argument("video", type=click.Path(exists=True, path_type=Path)) +def info(video: Path): + """ + Show information about a video file. + """ + from .video_processor import VideoProcessor + + processor = VideoProcessor(video, Path("/tmp")) + metadata = processor.get_metadata() + + table = Table(title=f"Video Info: {video.name}") + table.add_column("Property", style="cyan") + table.add_column("Value") + + table.add_row("Duration", f"{metadata.duration:.2f}s ({metadata.duration/60:.1f} min)") + table.add_row("Resolution", f"{metadata.width}x{metadata.height}") + table.add_row("FPS", f"{metadata.fps:.2f}") + table.add_row("Codec", metadata.codec) + table.add_row("Has Audio", "✓" if metadata.has_audio else "✗") + table.add_row("File Size", f"{video.stat().st_size / 1024 / 1024:.1f} MB") + + console.print(table) + + +# Legacy command for backwards compatibility +@cli.command(name="main", hidden=True) +@click.argument("video", type=click.Path(exists=True, path_type=Path)) +@click.option("-o", "--output", type=click.Path(path_type=Path)) +@click.option("--frames-only", is_flag=True) +@click.option("--atomizer-hints", is_flag=True) +@click.option("--bom", is_flag=True) +@click.option("--pdf", is_flag=True) +@click.option("--frame-interval", default=2.0) +@click.option("--whisper-model", default="base") +@click.pass_context +def main_legacy(ctx, video, output, frames_only, atomizer_hints, bom, pdf, frame_interval, whisper_model): + """Legacy entry point - redirects to process command.""" + ctx.invoke( + process, + video=video, + output=output, + frames_only=frames_only, + atomizer_hints=atomizer_hints, + bom=bom, + pdf=pdf, + frame_interval=frame_interval, + whisper_model=whisper_model, + ) + + +def main(): + """Main entry point.""" + cli() if __name__ == "__main__":