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
This commit is contained in:
Mario Lavoie
2026-01-27 20:18:28 +00:00
parent 148180c12e
commit ca51b10c45
7 changed files with 1010 additions and 191 deletions

190
README.md
View File

@@ -2,61 +2,132 @@
**One video → Complete engineering documentation.** **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 ## The Problem
- Documentation is tedious — Engineers spend hours documenting CAD models manually - **Documentation is tedious** — Engineers spend hours documenting CAD models manually
- Knowledge lives in heads — Verbal explanations during reviews aren't captured - **Knowledge lives in heads** — Verbal explanations during reviews aren't captured
- CDR prep is painful — Gathering images, writing descriptions, creating BOMs - **CDR prep is painful** — Gathering images, writing descriptions, creating BOMs
- FEA setup requires context — Atomizer needs model understanding that's often verbal - **FEA setup requires context** — Atomizer needs model understanding that's often verbal
## The Solution ## The Solution
### Input Record yourself explaining your CAD model. CAD-Documenter:
- 📹 Video of engineer explaining a CAD model
- Optional: CAD file references, existing P/N databases 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 ### Output
- 📄 **Markdown documentation** — Structured, version-controlled - 📄 **Markdown documentation** — Structured, version-controlled
- 📊 **Bill of Materials**With standardized P/N - 📊 **Bill of Materials**CSV with components, materials, functions
- 🔧 **Component registry**Parts, functions, materials, specs - 🔧 **Component registry**Detailed specs per component
- 🎯 **Atomizer hints**Parameters, constraints, objectives for FEA - 🎯 **Atomizer hints**FEA objectives, constraints, parameters
- 📑 **CDR-ready PDF**Via Atomaste Report Standard - 📑 **PDF**Professional output via Atomaste Report Standard
## Installation ## Installation
```bash ```bash
# Clone the repo # Clone
git clone http://100.80.199.40:3000/Antoine/CAD-Documenter.git git clone http://100.80.199.40:3000/Antoine/CAD-Documenter.git
cd CAD-Documenter cd CAD-Documenter
# Install dependencies (using uv) # Install with uv
uv sync uv sync
# Or with pip
pip install -e .
``` ```
### Requirements ### Requirements
- Python 3.12+ - Python 3.12+
- ffmpeg (for video/audio processing) - 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 ## Usage
```bash ```bash
# Basic documentation # Basic
cad-doc video.mp4 cad-doc video.mp4
# Full pipeline with all integrations # Custom output directory
cad-doc video.mp4 \ cad-doc video.mp4 --output ./my_docs/
--output docs/my_assembly/ \
--atomizer-hints \ # Full pipeline
--bom \ cad-doc video.mp4 --bom --atomizer-hints --pdf
--pdf
# Just extract frames # 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 ## Architecture
``` ```
@@ -76,18 +147,77 @@ cad-doc video.mp4 --frames-only --output frames/
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
``` ```
## Integrations ## Atomizer Integration
- **Atomizer** → FEA setup instructions from verbal explanations CAD-Documenter generates FEA optimization hints for Atomizer:
- **Part Manager** → Standardized P/N lookup
- **Atomaste Report Standard** → Professional PDF generation
## 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 ## License
MIT MIT
## Credits
Built by [Atomaste](https://atomaste.ca) for the engineering community.

View File

@@ -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

220
docs/USAGE.md Normal file
View File

@@ -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

View File

@@ -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"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "cad-documenter" name = "cad-documenter"
version = "0.1.0" version = "0.2.0"
description = "Video walkthrough → Complete engineering documentation" description = "Video walkthrough → Complete engineering documentation"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
@@ -8,26 +8,48 @@ license = {text = "MIT"}
authors = [ authors = [
{name = "Antoine Letarte", email = "antoine.letarte@gmail.com"}, {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 = [ dependencies = [
"click>=8.1.0", "click>=8.1.0",
"rich>=13.0.0", "rich>=13.0.0",
"pydantic>=2.0.0",
"jinja2>=3.1.0", "jinja2>=3.1.0",
"openai-whisper>=20231117", "openai-whisper>=20231117",
"pillow>=10.0.0", "pillow>=10.0.0",
"httpx>=0.27.0", # Vision API clients
"tomli>=2.0.0;python_version<'3.11'", "anthropic>=0.40.0",
"openai>=1.50.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"pytest>=8.0.0", "pytest>=8.0.0",
"pytest-cov>=4.0.0",
"ruff>=0.1.0", "ruff>=0.1.0",
"mypy>=1.0.0",
]
pdf = [
"pandoc", # For PDF generation fallback
] ]
[project.scripts] [project.scripts]
cad-doc = "cad_documenter.cli:main" 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] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
@@ -38,3 +60,17 @@ packages = ["src/cad_documenter"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
target-version = "py312" 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

View File

@@ -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",
]

View File

@@ -5,208 +5,379 @@ from pathlib import Path
import click import click
from rich.console import Console 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.panel import Panel
from rich.table import Table
from .config import load_config, create_default_config from .pipeline import DocumentationPipeline, PipelineProgress, PipelineStage, create_pipeline
from .pipeline import DocumentationPipeline from .config import Config, load_config
console = Console() console = Console()
def print_banner(): def print_banner():
"""Print welcome banner.""" """Print the CAD-Documenter banner."""
console.print(Panel.fit( 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]", "[dim]Video walkthrough → Engineering documentation[/dim]",
border_style="blue" 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.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("-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("--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("--atomizer-hints", is_flag=True, help="Generate Atomizer FEA hints")
@click.option("--bom", is_flag=True, help="Generate Bill of Materials") @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("--pdf", is_flag=True, help="Generate PDF via Atomaste Report Standard")
@click.option("--frame-interval", type=float, help="Seconds between frame extractions") @click.option("--frame-mode", type=click.Choice(["interval", "scene", "hybrid"]),
@click.option("--whisper-model", type=click.Choice(["tiny", "base", "small", "medium", "large"]), help="Whisper model size") default="hybrid", help="Frame extraction mode")
@click.option("--api-provider", type=click.Choice(["openai", "anthropic"]), help="Vision API provider") @click.option("--frame-interval", default=2.0, help="Seconds between frames (interval mode)")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Config file path") @click.option("--whisper-model", default="base",
@click.option("--init-config", is_flag=True, help="Create default config file and exit") help="Whisper model size (tiny/base/small/medium/large)")
@click.option("-v", "--verbose", is_flag=True, help="Verbose output") @click.option("--vision-provider", type=click.Choice(["anthropic", "openai"]),
@click.version_option() default="anthropic", help="Vision API provider")
def main( @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, video: Path,
output: Path | None, output: Path | None,
frames_only: bool, frames_only: bool,
skip_transcription: bool,
atomizer_hints: bool, atomizer_hints: bool,
bom: bool, bom: bool,
pdf: bool, pdf: bool,
frame_interval: float | None, frame_mode: str,
whisper_model: str | None, frame_interval: float,
api_provider: str | None, whisper_model: str,
config_path: Path | None, vision_provider: str,
init_config: bool, vision_model: str | None,
config: Path | None,
verbose: bool, 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.) VIDEO: Path to the video file (.mp4, .mov, .avi, etc.)
Examples: Examples:
cad-doc walkthrough.mp4 cad-doc process video.mp4
cad-doc video.mp4 --output ./docs --bom --atomizer-hints cad-doc process video.mp4 --atomizer-hints --bom --pdf
cad-doc video.mp4 --pdf --whisper-model medium cad-doc process video.mp4 --frame-mode scene --vision-provider openai
""" """
print_banner() print_banner()
console.print(f"\n📹 Processing: [cyan]{video}[/cyan]")
# Handle --init-config # Load or create config
if init_config: cfg = load_config(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 # Override config with CLI options
if frame_interval is not None: cfg.frame_extraction.mode = frame_mode
config.processing.frame_interval = frame_interval cfg.frame_extraction.interval_seconds = frame_interval
if whisper_model is not None: cfg.transcription.model = whisper_model
config.processing.whisper_model = whisper_model cfg.vision.provider = vision_provider
if api_provider is not None: if vision_model:
config.api.provider = api_provider cfg.vision.model = vision_model
# 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 # Default output directory
if output is None: if output is None:
output = video.parent / f"{video.stem}_docs" output = video.parent / f"{video.stem}_docs"
output.mkdir(parents=True, exist_ok=True) console.print(f"📁 Output: [cyan]{output}[/cyan]\n")
console.print(f"Output: [cyan]{output}[/cyan]")
# Initialize pipeline # Create pipeline
try:
pipeline = DocumentationPipeline( pipeline = DocumentationPipeline(
video_path=video, video_path=video,
output_dir=output, output_dir=output,
config=config, config=cfg,
progress_callback=progress_handler if verbose else None,
) )
except ValueError as e:
console.print(f"[red]Configuration error:[/red] {e}")
sys.exit(1)
# Frames only mode # Show video info
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: try:
pdf_path = pipeline.generate_pdf(doc_path) metadata = pipeline.get_video_metadata()
console.print(f"[green]✓[/green] PDF: {pdf_path}") console.print(f" Duration: {metadata.duration:.1f}s | "
except Exception as e: f"Resolution: {metadata.width}x{metadata.height} | "
console.print(f"[yellow]Warning:[/yellow] PDF generation failed: {e}") f"Audio: {'' if metadata.has_audio else ''}")
except Exception:
pass
# Summary
console.print() 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:
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,
)
# Print results
console.print()
if result.success:
console.print(Panel.fit( console.print(Panel.fit(
f"[bold green]Documentation complete![/bold green]\n\n" f"[bold green]Documentation generated successfully![/bold green]\n\n"
f"📄 [cyan]{doc_path}[/cyan]\n" f"📊 Frames extracted: {result.frames_extracted}\n"
f"📊 {len(analysis.components)} components documented\n" f"🔧 Components found: {result.components_found}\n"
f"🖼️ {len(frames)} frames extracted", f"🎤 Audio duration: {result.transcript_duration:.1f}s",
title="Summary", title="Results",
border_style="green" border_style="green"
)) ))
# Show atomizer hints summary if generated # Show output files
if (atomizer_hints or config.output.include_atomizer_hints) and analysis.atomizer_hints: table = Table(title="Output Files", show_header=True)
hints = analysis.atomizer_hints table.add_column("Type", style="cyan")
if hints.objectives or hints.constraints: table.add_column("Path")
console.print("\n[bold]Atomizer Hints:[/bold]")
for obj in hints.objectives[:3]: if result.documentation_path:
console.print(f" 🎯 {obj['direction'].capitalize()} {obj['name']}") table.add_row("Documentation", str(result.documentation_path))
for constraint in hints.constraints[:3]: if result.atomizer_hints_path:
console.print(f" 📏 {constraint['type']}: {constraint['value']}") 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__": if __name__ == "__main__":