Backend-B2V / backend /templates /DESIGN_DOC.md
Muhammad-Mehdi's picture
Backend and remotion-video for HF Space
cfe45d5
|
Raw
History Blame Contribute Delete
22.3 kB
# Blog2Video β€” Template System Design Document
> **Audience:** Engineer implementing the template system.
> **Status:** Specification β€” no code has been written yet.
> **Estimated effort:** 11–12 engineer-days (6 days if parallelized across 2–3 people).
---
## 1. Goal
Add a pluggable template system so users can choose a visual style for their video (or let AI pick one). Each template is a self-contained package of Remotion layout components, DSPy prompt files, and metadata. Adding a new template requires **zero changes** to core pipeline code.
---
## 2. Architecture
```
User picks template (or "Auto")
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ backend/templates/{template_id}/ β”‚
β”‚ β”œβ”€β”€ meta.json ← colors, composition_id, layout IDs β”‚
β”‚ └── prompt.md ← DSPy-readable design rules + layouts β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ read by
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TemplateService (backend/app/services/template_service.py) β”‚
β”‚ β”‚
β”‚ β€’ list_templates() β†’ list of all templates β”‚
β”‚ β€’ get_meta(id) β†’ one template's meta.json β”‚
β”‚ β€’ get_prompt(id) β†’ one template's prompt.md content β”‚
β”‚ β€’ get_valid_layouts(id) β†’ set of valid layout IDs β”‚
β”‚ β€’ validate_template_id(id) β†’ returns id or "default" β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ used by
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TemplateSceneGenerator (ONE class for ALL templates) β”‚
β”‚ (backend/app/dspy_modules/template_scene_gen.py) β”‚
β”‚ β”‚
β”‚ __init__(template_id) β”‚
β”‚ β†’ loads prompt.md as a DSPy input field β”‚
β”‚ β†’ loads valid_layouts, hero_layout, fallback_layout from meta β”‚
β”‚ β”‚
β”‚ generate_scene_descriptor(...) β”‚
β”‚ β†’ scene 0 always forced to hero_layout β”‚
β”‚ β†’ other scenes: calls DSPy with prompt.md content β”‚
β”‚ β†’ validates output layout against valid_layouts β”‚
β”‚ β†’ invalid layout β†’ uses fallback_layout β”‚
β”‚ β†’ returns {layout, layoutProps} β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ output consumed by
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ remotion.py β”‚
β”‚ β€’ write_remotion_data() β†’ uses hero_layout and fallback_layout β”‚
β”‚ β€’ provision_workspace() β†’ copies template's .tsx files β”‚
β”‚ β€’ render_video() β†’ uses composition_id from meta.json β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ renders
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ remotion-video/src/templates/{template_id}/ β”‚
β”‚ β”œβ”€β”€ {Name}Video.tsx ← Remotion composition β”‚
β”‚ └── layouts/ β”‚
β”‚ β”œβ”€β”€ index.ts ← LAYOUT_REGISTRY β”‚
β”‚ β”œβ”€β”€ types.ts ← TypeScript interfaces β”‚
β”‚ └── *.tsx ← UI components β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
---
## 3. Directory Structure
```
blog2video/
β”œβ”€β”€ backend/
β”‚ β”œβ”€β”€ templates/
β”‚ β”‚ β”œβ”€β”€ registry.json ← ["default","nightfall","gridcraft","spotlight"]
β”‚ β”‚ β”œβ”€β”€ HOW_TO_ADD_TEMPLATE.md ← instructions for adding a 5th template
β”‚ β”‚ β”œβ”€β”€ default/
β”‚ β”‚ β”‚ β”œβ”€β”€ meta.json
β”‚ β”‚ β”‚ └── prompt.md
β”‚ β”‚ β”œβ”€β”€ nightfall/
β”‚ β”‚ β”‚ β”œβ”€β”€ meta.json
β”‚ β”‚ β”‚ └── prompt.md
β”‚ β”‚ β”œβ”€β”€ gridcraft/
β”‚ β”‚ β”‚ β”œβ”€β”€ meta.json
β”‚ β”‚ β”‚ └── prompt.md
β”‚ β”‚ └── spotlight/
β”‚ β”‚ β”œβ”€β”€ meta.json
β”‚ β”‚ └── prompt.md
β”‚ └── app/
β”‚ β”œβ”€β”€ services/
β”‚ β”‚ └── template_service.py ← NEW
β”‚ β”œβ”€β”€ dspy_modules/
β”‚ β”‚ β”œβ”€β”€ template_scene_gen.py ← NEW (unified generator)
β”‚ β”‚ β”œβ”€β”€ template_suggest.py ← NEW (AI auto-select)
β”‚ β”‚ β”œβ”€β”€ scene_gen.py ← KEEP as fallback
β”‚ β”‚ └── script_gen.py ← UNCHANGED
β”‚ β”œβ”€β”€ models/project.py ← EDIT: add template column
β”‚ β”œβ”€β”€ schemas/schemas.py ← EDIT: add template field
β”‚ β”œβ”€β”€ routers/pipeline.py ← EDIT: use TemplateSceneGenerator
β”‚ β”œβ”€β”€ routers/projects.py ← EDIT: validate template + colors
β”‚ β”œβ”€β”€ database.py ← EDIT: add migration
β”‚ └── main.py ← EDIT: add GET /api/templates
β”‚
β”œβ”€β”€ remotion-video/src/
β”‚ β”œβ”€β”€ components/ ← SHARED across all templates
β”‚ β”‚ β”œβ”€β”€ LogoOverlay.tsx
β”‚ β”‚ └── Transitions.tsx
β”‚ β”œβ”€β”€ templates/
β”‚ β”‚ β”œβ”€β”€ default/
β”‚ β”‚ β”‚ β”œβ”€β”€ DefaultVideo.tsx ← renamed from ExplainerVideo.tsx
β”‚ β”‚ β”‚ └── layouts/
β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ types.ts
β”‚ β”‚ β”‚ └── (10 existing layout files β€” moved here)
β”‚ β”‚ β”œβ”€β”€ nightfall/
β”‚ β”‚ β”‚ β”œβ”€β”€ NightfallVideo.tsx
β”‚ β”‚ β”‚ └── layouts/
β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts, types.ts
β”‚ β”‚ β”‚ └── (9 layout files)
β”‚ β”‚ β”œβ”€β”€ gridcraft/
β”‚ β”‚ β”‚ β”œβ”€β”€ GridcraftVideo.tsx
β”‚ β”‚ β”‚ └── layouts/
β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts, types.ts
β”‚ β”‚ β”‚ └── (9 layout files)
β”‚ β”‚ └── spotlight/
β”‚ β”‚ β”œβ”€β”€ SpotlightVideo.tsx
β”‚ β”‚ └── layouts/
β”‚ β”‚ β”œβ”€β”€ index.ts, types.ts
β”‚ β”‚ └── (9 layout files)
β”‚ β”œβ”€β”€ Root.tsx ← EDIT: register all 4 compositions
β”‚ └── index.ts
β”‚
└── frontend/src/
β”œβ”€β”€ api/client.ts ← EDIT: template types + API calls
β”œβ”€β”€ components/
β”‚ β”œβ”€β”€ TemplatePicker.tsx ← NEW
β”‚ β”œβ”€β”€ BlogUrlForm.tsx ← EDIT: integrate TemplatePicker
β”‚ β”œβ”€β”€ VideoPreview.tsx ← EDIT: composition routing map
β”‚ └── remotion/templates/ ← NEW: preview mirrors
β”‚ β”œβ”€β”€ default/ (same layout structure)
β”‚ β”œβ”€β”€ nightfall/ (same layout structure)
β”‚ β”œβ”€β”€ gridcraft/ (same layout structure)
β”‚ └── spotlight/ (same layout structure)
└── pages/
β”œβ”€β”€ Dashboard.tsx ← EDIT: pass template param
└── ProjectView.tsx ← EDIT: show template badge
```
---
## 4. What Changes Per Template vs What Stays the Same
### NEVER modify for a new template (template-agnostic core):
| File | Why |
|------|-----|
| `template_scene_gen.py` | Reads prompt.md β€” no template logic |
| `template_service.py` | Reads registry.json β€” auto-discovers |
| `script_gen.py` | Script generation has no template dependency |
| `pipeline.py` | Just reads `project.template` and passes it |
| `remotion.py` | Reads meta.json for composition_id, hero/fallback |
| `TemplatePicker.tsx` | Fetches from API β€” auto-discovers |
| `BlogUrlForm.tsx` | Receives template from picker β€” template-agnostic |
| `Dashboard.tsx` | Passes template through β€” template-agnostic |
| `client.ts` | Generic API calls β€” template-agnostic |
### Files created PER template:
| File | Purpose |
|------|---------|
| `backend/templates/{id}/meta.json` | Identity, colors, layout IDs, composition ID |
| `backend/templates/{id}/prompt.md` | Full DSPy prompt with design rules + layout catalog |
| `remotion-video/src/templates/{id}/{Name}Video.tsx` | Main Remotion composition |
| `remotion-video/src/templates/{id}/layouts/*.tsx` | Layout UI components |
| `remotion-video/src/templates/{id}/layouts/index.ts` | Layout registry map |
| `remotion-video/src/templates/{id}/layouts/types.ts` | TypeScript interfaces |
| `frontend/src/components/remotion/templates/{id}/` | Preview mirror of all above |
### Files edited ONCE (not per template):
| File | What to add |
|------|-------------|
| `backend/templates/registry.json` | Template ID to the array |
| `remotion-video/src/Root.tsx` | New `<Composition>` entry |
| `frontend/src/components/VideoPreview.tsx` | Entry in COMPOSITIONS map |
---
## 5. The `meta.json` Schema
Every template must have a `meta.json` with exactly these fields:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Template ID (lowercase, no spaces/hyphens, matches directory name) |
| `name` | string | Display name for the UI (e.g. "Nightfall") |
| `description` | string | 1–2 sentence description for the template picker card |
| `preview_colors` | object | `{ accent, bg, text }` β€” hex color strings, applied as defaults |
| `composition_id` | string | PascalCase Remotion composition ID (e.g. `"NightfallVideo"`) β€” must match Root.tsx |
| `hero_layout` | string | Layout ID forced for scene 0 β€” must be in valid_layouts |
| `fallback_layout` | string | Layout ID used when DSPy output is invalid β€” must be in valid_layouts |
| `valid_layouts` | string[] | Complete list of every layout ID in this template |
---
## 6. The `prompt.md` Structure
Every `prompt.md` must have these five sections. DSPy reads the entire file as one input field, so it must be self-contained.
### Section 1: Design Philosophy
3–5 sentences describing the visual language. Tell the LLM what this template LOOKS LIKE β€” the UI components, the mood, the color treatment, the spatial approach. Be specific and visual.
### Section 2: Layout Catalog
For every layout, include:
- **Layout ID** (must match meta.json and the Remotion component registry)
- **Visual description** β€” what the viewer literally sees on screen. Describe the UI elements, sizes, colors, spatial arrangement. DSPy needs to understand what it is choosing between.
- **Best for** β€” what types of content suit this layout
- **Props** β€” exact prop names and types that the Remotion component expects. If no extra props, say "(none)". If the layout takes structured data, show the exact shape (array of objects with named fields). The prop names here must EXACTLY match the TypeScript interface in the component.
### Section 3: Scene Flow Rules
Which layout for scene 0, recommended ordering/patterns, maximum frequency for special layouts, what to use for opening/middle/closing.
### Section 4: Content Extraction Rules
For each layout that takes structured props: how to pull REAL data from the narration. Never fabricate. Give examples.
### Section 5: Variety Rules
Never repeat same layout more than 2 consecutive scenes. Minimum distinct layouts per video. Which layout is the baseline/fallback and its maximum frequency.
---
## 7. The DSPy Signature Design
There is ONE signature used for ALL templates. It has these fields:
**Inputs:**
- `template_prompt` β€” the full content of prompt.md (this is what changes between templates)
- `scene_title` β€” title of this scene
- `narration` β€” narration text to analyze and extract data from
- `visual_description` β€” visual hints from the script generator
- `scene_index` β€” 0-based index (scene 0 always gets the hero layout)
- `total_scenes` β€” used by the LLM to plan variety across the video
**Outputs:**
- `layout` β€” a layout ID from the template's catalog
- `layout_props_json` β€” JSON object with layout-specific props
The signature's docstring should be SHORT and GENERIC. It should say: "Read the template_prompt carefully and follow its design rules, layout catalog, and content extraction rules precisely." All the real design direction comes from prompt.md, not the Python code.
The generator class constructor takes `template_id`, loads prompt.md and meta.json via TemplateService, and stores hero_layout, fallback_layout, and valid_layouts. The generate method forces scene 0 to hero_layout, calls DSPy for other scenes, validates the output layout against the valid set, and falls back if invalid.
---
## 8. How `remotion.py` Adapts
Three functions read from TemplateService (no hardcoded template logic):
1. **`write_remotion_data()`** β€” Reads `hero_layout` and `fallback_layout` from meta.json instead of hardcoding `"hero_image"` and `"text_narration"`
2. **`provision_workspace()`** β€” Instead of a hardcoded file list, scans `remotion-video/src/templates/{template_id}/` and copies all `.tsx`/`.ts` files into the workspace. Always also copies shared components from `remotion-video/src/components/`
3. **Render command** β€” Reads `composition_id` from meta.json and passes it to the Remotion render CLI instead of hardcoding `"ExplainerVideo"`
---
## 9. 1:1 Alignment Rule
There is a strict 1:1:1 mapping that must stay in sync:
```
Layout ID in prompt.md ←→ Layout ID in meta.json valid_layouts
↕ ↕
Key in Remotion LAYOUT_REGISTRY ←→ Key in data.json scene output
↕
Component .tsx file
```
If any of these are misaligned, the system breaks silently (props don't reach the component, or invalid layouts fall back every time). When creating a template, write the layout IDs in meta.json FIRST, then use those exact strings everywhere else.
Props alignment: the prop names in `prompt.md`'s layout catalog must exactly match the TypeScript interface in the corresponding `.tsx` component. If prompt.md says `metrics` (array of `{value, label, suffix}`), the component must destructure `metrics` with that exact shape.
---
## 10. Implementation Steps (in dependency order)
### Step 1: Create Template Registry Files
Create `backend/templates/` with `registry.json` and all four template subdirectories, each containing `meta.json` and `prompt.md`. For default, extract existing content from `scene_gen.py`. For the other three, write fresh prompt.md files following the TEMPLATES.md spec.
**Acceptance:** Every meta.json passes schema validation. Every prompt.md has all five sections. Every layout ID in prompt.md exists in meta.json's valid_layouts.
### Step 2: Create TemplateService
Create `backend/app/services/template_service.py`. Reads registry on init, loads meta.json into memory, reads prompt.md fresh on each call. Module-level singleton. Unknown IDs fall back to "default".
**Acceptance:** Import the service, list_templates returns 4, get_prompt returns non-empty strings, validate_template_id("bogus") returns "default".
### Step 3: Add Templates API Endpoint
Add `GET /api/templates` in main.py. Calls TemplateService.list_templates(). No auth required.
**Acceptance:** API returns all 4 templates. Adding a 5th directory + registry entry makes it appear on restart.
### Step 4: Create Unified TemplateSceneGenerator
Create `backend/app/dspy_modules/template_scene_gen.py`. One class, one DSPy signature. Constructor takes template_id, loads prompt and meta. Scene 0 forced to hero_layout. Invalid outputs use fallback_layout. Keep existing scene_gen.py as fallback.
**Acceptance:** Generator("default") matches current quality. Generator("nightfall") produces nightfall layout IDs. Scene 0 is always the hero layout.
### Step 5: Create TemplateSuggester (AI Auto-Select)
Create `backend/app/dspy_modules/template_suggest.py`. DSPy signature takes blog summary + available templates list, outputs template_id + reasoning. Add `POST /api/suggest-template` endpoint.
**Acceptance:** Technical blog β†’ default. Design/opinion piece β†’ nightfall. Business/product content β†’ gridcraft. Marketing/listicle β†’ spotlight. At least 3/4 correct.
### Step 6: Database & Project Creation Updates
Add `template` column to Project model (string, default "default"). Add to schemas. Add migration. Validate template in create_project, apply template's default colors when user doesn't override.
**Acceptance:** Create project with template "nightfall" β†’ stored. Template "bogus" β†’ stores "default". Template "gridcraft" without colors β†’ gets orange/neutral.
### Step 7: Wire Template into Pipeline & Rendering
Replace SceneCodeGenerator with TemplateSceneGenerator(project.template) in pipeline.py. If template is "auto", call TemplateSuggester first. Update remotion.py to read composition_id, hero_layout, fallback_layout from TemplateService. Dynamic file copying in provision_workspace.
**Acceptance:** Each template produces correct layouts and renders with correct composition ID.
### Step 8: Build Remotion Components
**Do the default restructure first** β€” move existing files from `src/components/layouts/` into `src/templates/default/layouts/`, rename ExplainerVideo to DefaultVideo, verify everything still works. Then build three new templates (nightfall, gridcraft, spotlight) following the TEMPLATES.md spec. Mirror all components in the frontend for preview. Register compositions in Root.tsx. Add to COMPOSITIONS map in VideoPreview.tsx.
**Acceptance:** For each template β€” preview Player renders correctly, final video renders correctly, all layouts display with the template's visual style.
### Step 9: Frontend Template Picker
Create TemplatePicker component. Shows "Auto (AI picks)" as default + 4 template cards with color dots, name, description, checkmark. Selecting a template applies its colors. Wire through BlogUrlForm β†’ Dashboard β†’ createProject. Show template badge on ProjectView.
**Acceptance:** All templates appear. Selecting one changes colors. "Auto" triggers AI selection. Badge shows result.
### Step 10: Write HOW_TO_ADD_TEMPLATE.md
Step-by-step checklist for adding a 5th template. List files to create, files to edit (registry.json, Root.tsx, VideoPreview.tsx), files that DON'T change. Include testing checklist.
**Acceptance:** A new engineer can follow the doc and add a template without asking questions.
---
## 11. Parallelization Strategy
```
Engineer A: Steps 1, 2, 3, 4, 5, 6, 7, 10 (backend + DSPy + wiring)
Engineer B: Step 8 β€” nightfall components (9 layouts Γ— 2)
Engineer C: Step 8 β€” gridcraft components (9 layouts Γ— 2)
Engineer D: Step 8 β€” spotlight components (9 layouts Γ— 2)
Engineer A: Step 9 (frontend picker, after 1–7 done)
```
Engineer A must complete Step 1 (registry files) before B/C/D can start, and must complete "restructure default" in Step 8 before new templates are built.
Wall-clock time with 2–3 engineers: ~6 days.
---
## 12. Common Mistakes to Avoid
| Mistake | Impact | Prevention |
|---------|--------|-----------|
| Layout ID mismatch between prompt.md, meta.json, and index.ts | DSPy outputs get rejected as invalid, falls back every time | Write IDs in meta.json first, copy-paste everywhere else |
| Prop names in prompt.md don't match .tsx interface | DSPy extracts data that the component ignores | Copy-paste prop names from TypeScript interface into prompt.md |
| composition_id in meta.json doesn't match Root.tsx | Render command targets nonexistent composition, exits with error | String-compare before testing |
| Forgot to add template to registry.json | TemplateService doesn't know it exists | Always update registry first |
| hero_layout not included in valid_layouts | Scene 0 uses a layout that isn't in the Remotion registry | Include hero_layout in valid_layouts array |
| Hardcoding template-specific logic in pipeline.py or remotion.py | Defeats the pluggable architecture β€” every new template needs code changes | All template differences must live in meta.json and prompt.md |
| Writing vague prompt.md layout descriptions ("show content nicely") | DSPy makes poor layout choices because it doesn't know what layouts look like | Describe what the viewer literally sees: sizes, positions, colors, animations |