feat: cinematic highlight render — 20s procedural camera animation
New render type: 4-segment cinematic camera animation (480 frames @ 24fps) for professional product highlight videos. Camera sequence: 1. Establishing (5s): slow 45° orbit + push-in, 50mm lens 2. Detail sweep (5s): low-angle close arc, 85mm telephoto, shallow DOF 3. Crane up (5s): rising 30°→60°, 35mm wide reveal, pull-back 4. Hero close (5s): push-in to beauty angle, 65mm, smooth ease-out Technical: - cinematic_render.py: procedural camera from bounding sphere, cubic easing, per-frame keyframes (location, rotation, focal length, DOF) - render_cinematic_to_file(): service function (same pattern as turntable) - Pipeline routing: render_settings.cinematic flag → cinematic path - Depth of field enabled (f-stop scales with product size) - use_persistent_data for BVH caching between frames - Same material/template/USD pipeline as turntable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,122 +1,87 @@
|
||||
# Plan: Rich Product Metadata Extraction from STEP Files
|
||||
# Plan: Cinematic Highlight Render
|
||||
|
||||
## Context
|
||||
|
||||
The AI chat agent was asked "What is the biggest product from my order?" and couldn't answer because dimensional data wasn't available in tool results. While `cad_files.mesh_attributes` already stores bounding box dimensions, much more metadata is extractable from STEP files via OCC that would make the AI agent and the product library significantly more useful.
|
||||
Add a new render type: a 20-second cinematic product highlight video with procedural multi-shot camera animation. Unlike turntable (constant orbit), this has varying camera distances, focal lengths, elevations, and bezier-eased transitions between 4 segments.
|
||||
|
||||
**Currently extracted**: part names, bounding box (xyz), sharp edges, smooth angle
|
||||
**Available but not extracted**: per-part volume, surface area, assembly hierarchy, instance counts, embedded colors, triangle counts, geometric complexity
|
||||
## Tasks
|
||||
|
||||
**Goal**: Expand the STEP metadata extraction to compute richer product characteristics and store them in a structured `cad_metadata` JSONB field, accessible to the AI agent, product search, and frontend.
|
||||
### [ ] Task 1: Cinematic Blender script
|
||||
|
||||
## Affected Files
|
||||
- **File**: `render-worker/scripts/cinematic_render.py` (new)
|
||||
- **What**: Blender Python script that:
|
||||
1. Imports product geometry (GLB or USD, same as turntable_render.py)
|
||||
2. Applies materials (same pipeline as turntable)
|
||||
3. Computes bounding sphere from imported meshes
|
||||
4. Creates a procedural 4-segment camera animation (480 frames @ 24fps = 20s):
|
||||
- Segment 1 (0-120): Establishing — slow 45° orbit + push-in, 50mm lens
|
||||
- Segment 2 (121-240): Detail sweep — low-angle close arc, 85mm lens, shallow DOF
|
||||
- Segment 3 (241-360): Crane up — rising from 30° to 60° elevation, 35mm wide
|
||||
- Segment 4 (361-480): Hero close — slow push-in, 65mm, ease-out to still
|
||||
5. Each segment: camera position from spherical coords (azimuth, elevation, distance), bezier interpolation
|
||||
6. Depth of field enabled (f-stop scales with product size)
|
||||
7. Renders all frames as PNG to temp directory
|
||||
8. FFmpeg assembles frames → MP4 (H.264, yuv420p, 24fps)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `backend/app/services/step_processor.py` | Expand `extract_step_metadata()` with volume, surface area, hierarchy, complexity |
|
||||
| `backend/app/domains/products/models.py` | Add `cad_metadata` JSONB column to Product |
|
||||
| `backend/alembic/versions/XXX_add_cad_metadata.py` | Migration |
|
||||
| `backend/app/domains/pipeline/tasks/extract_metadata.py` | Populate `cad_metadata` after STEP processing |
|
||||
| `backend/app/domains/products/schemas.py` | Expose `cad_metadata` in ProductOut |
|
||||
| `backend/app/services/chat_service.py` | Include metadata in search_products and system prompt |
|
||||
| `frontend/src/pages/ProductDetail.tsx` | Display rich metadata (volume, part count, complexity) |
|
||||
|
||||
## Tasks (in order)
|
||||
|
||||
### [ ] Task 1: Expand STEP metadata extraction
|
||||
|
||||
- **File**: `backend/app/services/step_processor.py`
|
||||
- **What**: Expand `extract_step_metadata()` to compute additional properties after the existing bbox/edge extraction. Add a new function `extract_rich_metadata(doc, shape_tool)` that returns:
|
||||
```python
|
||||
{
|
||||
"part_count": 42, # Number of leaf parts
|
||||
"assembly_depth": 3, # Max nesting depth
|
||||
"total_volume_cm3": 1250.4, # Sum of all part volumes (cm³)
|
||||
"total_surface_area_cm2": 3400.2, # Sum of all surface areas (cm²)
|
||||
"total_triangle_count": 45000, # After tessellation
|
||||
"total_vertex_count": 23000, # After tessellation
|
||||
"largest_part": { # Part with largest volume
|
||||
"name": "OuterRing",
|
||||
"volume_cm3": 450.2,
|
||||
},
|
||||
"smallest_dimension_mm": 0.5, # Smallest bbox dimension across all parts
|
||||
"instance_count": 36, # Total instances (parts may repeat)
|
||||
"unique_part_count": 12, # Distinct shapes
|
||||
"complexity_score": "high", # low/medium/high based on triangle count
|
||||
}
|
||||
CLI args (same pattern as turntable_render.py):
|
||||
```
|
||||
Use OCC:
|
||||
- `GProp_GProps` + `BRepGProp.VolumeProperties()` for volume
|
||||
- `BRepGProp.SurfaceProperties()` for surface area
|
||||
- `Poly_Triangulation` for triangle/vertex counts (after tessellation)
|
||||
- Assembly tree walk (already done in `_collect_part_key_map`) for hierarchy depth + instance count
|
||||
- **Acceptance gate**: `extract_rich_metadata()` returns all fields for a test STEP file
|
||||
blender --background --python cinematic_render.py -- \
|
||||
<glb_path> <output_mp4> <width> <height> <engine> <samples> \
|
||||
<smooth_angle> <cycles_device> <transparent_bg> <template_path> \
|
||||
<target_collection> <material_library_path> <material_map_json> \
|
||||
<part_names_ordered_json> <lighting_only> <shadow_catcher> \
|
||||
<rotation_x> <rotation_y> <rotation_z> \
|
||||
[--usd-path ...] [--focal-length ...] [--material-override ...]
|
||||
```
|
||||
- **Acceptance gate**: Script renders a 20s MP4 from a STEP file
|
||||
- **Dependencies**: None
|
||||
|
||||
### [ ] Task 2: Add cad_metadata column to Product model
|
||||
### [ ] Task 2: Render service function
|
||||
|
||||
- **File**: `backend/app/domains/products/models.py`
|
||||
- **What**: Add `cad_metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)` to the Product model. This stores the rich metadata at the product level (not cad_file) because products are the user-facing entity.
|
||||
- **Migration**: `alembic revision --autogenerate -m "add cad_metadata to products"`
|
||||
- **Also**: Add to ProductOut schema in `backend/app/domains/products/schemas.py`
|
||||
- **Acceptance gate**: Column exists, schema includes it
|
||||
- **Dependencies**: None
|
||||
- **File**: `backend/app/services/render_blender.py`
|
||||
- **What**: Add `render_cinematic_to_file()` function with same signature as `render_turntable_to_file()` but:
|
||||
- Calls `cinematic_render.py` instead of `turntable_render.py`
|
||||
- Fixed 24fps, 480 frames (20s)
|
||||
- Output format: mp4
|
||||
- Passes all the same material/template/position args
|
||||
- **Acceptance gate**: Function callable, builds correct subprocess command
|
||||
- **Dependencies**: Task 1
|
||||
|
||||
### [ ] Task 3: Populate cad_metadata during STEP processing
|
||||
### [ ] Task 3: Pipeline integration
|
||||
|
||||
- **File**: `backend/app/domains/pipeline/tasks/extract_metadata.py`
|
||||
- **What**: After `process_step_file` extracts objects and queues thumbnail, call `extract_rich_metadata()` and store the result on the Product's `cad_metadata` field. Also store it on `cad_files.mesh_attributes` (merge with existing data).
|
||||
- **Also**: Add a "reextract metadata" admin action that re-runs this for all existing products
|
||||
- **Acceptance gate**: After STEP processing, product.cad_metadata is populated with volume, part_count, etc.
|
||||
- **Dependencies**: Tasks 1, 2
|
||||
|
||||
### [ ] Task 4: Expose metadata in AI agent tools
|
||||
|
||||
- **File**: `backend/app/services/chat_service.py`
|
||||
- **What**:
|
||||
1. Update `_tool_search_products()` to include `cad_metadata` fields (part_count, total_volume_cm3, complexity_score) in results
|
||||
2. Update `query_database` tool description to mention `products.cad_metadata` JSONB field
|
||||
3. Update system prompt to mention available metadata
|
||||
- **Acceptance gate**: AI agent can answer "What is the biggest product?" using volume data
|
||||
- **Dependencies**: Task 3
|
||||
|
||||
### [ ] Task 5: Display rich metadata on ProductDetail page
|
||||
|
||||
- **File**: `frontend/src/pages/ProductDetail.tsx`
|
||||
- **What**: Add a "CAD Metadata" section on the product detail page showing:
|
||||
- Part count + unique parts + instances
|
||||
- Total volume (cm³) + surface area (cm²)
|
||||
- Largest part name + volume
|
||||
- Complexity score badge (low/medium/high)
|
||||
- Triangle/vertex count
|
||||
- Assembly depth
|
||||
- **Acceptance gate**: Metadata displayed on product page; empty gracefully when not available
|
||||
- **File**: `backend/app/domains/pipeline/tasks/render_order_line.py`
|
||||
- **What**: In the render task, detect when output type has a cinematic flag. Add a check:
|
||||
- If `output_type.render_settings.get("cinematic")` is True → call `render_cinematic_to_file()` instead of `render_turntable_to_file()`
|
||||
- OR: if output_type name contains "Cinematic" → route to cinematic
|
||||
- **Acceptance gate**: Order line with cinematic output type renders via the new script
|
||||
- **Dependencies**: Task 2
|
||||
|
||||
### [ ] Task 6: Batch re-extract metadata for existing products
|
||||
### [ ] Task 4: Output type + test
|
||||
|
||||
- **File**: `backend/app/api/routers/admin.py`
|
||||
- **What**: Add a "Re-extract Rich Metadata" button in System Tools that queues a Celery task to re-process all completed STEP files and populate `cad_metadata` for all products.
|
||||
- **Acceptance gate**: Button triggers batch job; existing products get metadata populated
|
||||
- **Dependencies**: Tasks 1, 3
|
||||
- **What**: Create the "Cinematic Highlight" output type via API:
|
||||
```json
|
||||
{
|
||||
"name": "Cinematic Highlight",
|
||||
"renderer": "blender",
|
||||
"output_format": "mp4",
|
||||
"render_backend": "celery",
|
||||
"is_animation": true,
|
||||
"transparent_bg": false,
|
||||
"cycles_device": "gpu",
|
||||
"render_settings": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"engine": "cycles",
|
||||
"samples": 128,
|
||||
"cinematic": true,
|
||||
"fps": 24,
|
||||
"frame_count": 480
|
||||
}
|
||||
}
|
||||
```
|
||||
Link to BlenderStudio template. Test with a real product.
|
||||
- **Acceptance gate**: A cinematic MP4 renders successfully for a TRB product
|
||||
- **Dependencies**: Task 3
|
||||
|
||||
## Migration Check
|
||||
|
||||
**Yes** — one new JSONB column on `products` table.
|
||||
|
||||
## Order Recommendation
|
||||
|
||||
1. Task 1 (extraction logic) + Task 2 (model + migration) — parallel
|
||||
2. Task 3 (wire up in pipeline)
|
||||
3. Task 4 (AI agent) + Task 5 (frontend) — parallel
|
||||
4. Task 6 (batch re-extract)
|
||||
|
||||
## Risks / Open Questions
|
||||
|
||||
1. **Volume calculation accuracy**: OCC `BRepGProp` computes exact B-rep volume, not mesh-based. This is accurate but can be slow for very complex shapes. Cap at 5s per file.
|
||||
|
||||
2. **Performance**: Rich metadata extraction adds ~100-500ms per STEP file. This is acceptable since STEP processing already takes 1-5s.
|
||||
|
||||
3. **Existing products**: ~45 products with STEP files need backfill. Task 6 handles this.
|
||||
|
||||
4. **Triangle count varies**: Depends on tessellation settings (deflection angles). Store the count at the current tessellation quality for reference, with a note that it's approximate.
|
||||
**No** — no DB changes needed.
|
||||
|
||||
Reference in New Issue
Block a user