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:
190
README.md
190
README.md
@@ -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.
|
||||||
|
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](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.
|
||||||
|
|||||||
182
docs/ATOMIZER_INTEGRATION.md
Normal file
182
docs/ATOMIZER_INTEGRATION.md
Normal 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
220
docs/USAGE.md
Normal 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
|
||||||
49
examples/sample_config.toml
Normal file
49
examples/sample_config.toml
Normal 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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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
|
|
||||||
if init_config:
|
# Load or create config
|
||||||
default_path = Path.home() / ".cad-documenter.toml"
|
cfg = load_config(config)
|
||||||
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]")
|
|
||||||
|
# Create pipeline
|
||||||
# Initialize pipeline
|
pipeline = DocumentationPipeline(
|
||||||
|
video_path=video,
|
||||||
|
output_dir=output,
|
||||||
|
config=cfg,
|
||||||
|
progress_callback=progress_handler if verbose else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show video info
|
||||||
try:
|
try:
|
||||||
pipeline = DocumentationPipeline(
|
metadata = pipeline.get_video_metadata()
|
||||||
video_path=video,
|
console.print(f" Duration: {metadata.duration:.1f}s | "
|
||||||
output_dir=output,
|
f"Resolution: {metadata.width}x{metadata.height} | "
|
||||||
config=config,
|
f"Audio: {'✓' if metadata.has_audio else '✗'}")
|
||||||
)
|
except Exception:
|
||||||
except ValueError as e:
|
pass
|
||||||
console.print(f"[red]Configuration error:[/red] {e}")
|
|
||||||
sys.exit(1)
|
console.print()
|
||||||
|
|
||||||
# Frames only mode
|
# Run pipeline with progress
|
||||||
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(
|
with Progress(
|
||||||
SpinnerColumn(),
|
SpinnerColumn(),
|
||||||
TextColumn("[progress.description]{task.description}"),
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||||
console=console,
|
console=console,
|
||||||
|
disable=verbose, # Disable progress bar if verbose (use callback instead)
|
||||||
) as progress:
|
) as progress:
|
||||||
|
task = progress.add_task("Processing...", total=100)
|
||||||
# Step 1: Extract frames
|
|
||||||
task1 = progress.add_task("[cyan]Step 1/4:[/cyan] Extracting frames...", total=None)
|
def update_progress(p: PipelineProgress):
|
||||||
frames = pipeline.extract_frames()
|
progress.update(task, completed=int(p.progress * 100), description=p.message)
|
||||||
progress.update(task1, description=f"[green]✓[/green] Extracted {len(frames)} frames")
|
if verbose:
|
||||||
progress.remove_task(task1)
|
progress_handler(p)
|
||||||
|
|
||||||
# Step 2: Transcribe
|
pipeline.progress_callback = update_progress
|
||||||
task2 = progress.add_task("[cyan]Step 2/4:[/cyan] Transcribing audio...", total=None)
|
|
||||||
transcript = pipeline.transcribe_audio()
|
result = pipeline.run(
|
||||||
seg_count = len(transcript.segments) if transcript.segments else 0
|
frames_only=frames_only,
|
||||||
progress.update(task2, description=f"[green]✓[/green] Transcribed {seg_count} segments")
|
skip_transcription=skip_transcription,
|
||||||
progress.remove_task(task2)
|
atomizer_hints=atomizer_hints,
|
||||||
|
bom=bom,
|
||||||
if verbose and transcript.full_text:
|
pdf=pdf,
|
||||||
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)
|
# Print results
|
||||||
|
|
||||||
# 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()
|
||||||
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 result.success:
|
||||||
if (atomizer_hints or config.output.include_atomizer_hints) and analysis.atomizer_hints:
|
console.print(Panel.fit(
|
||||||
hints = analysis.atomizer_hints
|
f"[bold green]✓ Documentation generated successfully![/bold green]\n\n"
|
||||||
if hints.objectives or hints.constraints:
|
f"📊 Frames extracted: {result.frames_extracted}\n"
|
||||||
console.print("\n[bold]Atomizer Hints:[/bold]")
|
f"🔧 Components found: {result.components_found}\n"
|
||||||
for obj in hints.objectives[:3]:
|
f"🎤 Audio duration: {result.transcript_duration:.1f}s",
|
||||||
console.print(f" 🎯 {obj['direction'].capitalize()} {obj['name']}")
|
title="Results",
|
||||||
for constraint in hints.constraints[:3]:
|
border_style="green"
|
||||||
console.print(f" 📏 {constraint['type']}: {constraint['value']}")
|
))
|
||||||
|
|
||||||
|
# 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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user