blog2video-test / progress.md
faisalnazeer67's picture
chore: update fixes
92e8f98
|
Raw
History Blame Contribute Delete
61.7 kB
# AI Custom Templates β€” Progress Tracker
## Master Plan
Replace the fixed `UniversalScene.tsx` renderer (9 layouts, 10 element types) with AI-generated unique React components per brand. Each template becomes a code string stored in DB, compiled at runtime for preview, written to temp files for rendering.
**No fallback to old system.** `UniversalScene.tsx`, `styleEngine.ts`, and the old custom template types will be **deleted** as a final cleanup step after all phases are implemented, tested, and stable. No dual-path code long-term β€” the new system must stand on its own so bugs are visible, not hidden behind a fallback.
**Full plan:** `/Users/faisalnazir/Desktop/Blog2Video - AI Generated Custom Templates Plan.md`
## Phases Overview
| Phase | Description | Status |
|-------|-------------|--------|
| 1 | DB Models + Enhanced Scraping | DONE |
| 2 | AI Code Generation + Preview | DONE |
| 3 | Chat-Based Editing | DEFERRED β€” removed from codebase, will revisit in a future phase |
| 4 | Render Pipeline Update | DONE |
| 5 | Polish & Preview System | DONE |
| 6 | Cleanup β€” Remove old UniversalScene/styleEngine/fallback code | DONE |
| H1 | Rate Limiting on Claude API Endpoints | DONE |
| H2 | Code Validator Hardening | DONE |
| H3 | Template Cache TTL | DONE |
| H4 | Render Workspace Locking | DONE |
| H5 | Frontend Code Sandbox (CSP + Error Boundary) | DONE |
| H6 | Self-healing current_version_id | DONE |
| F1 | BrandKit Population During Theme Extraction | DONE |
| F2 | Brand Asset Uploads (Logo) | DONE |
| F5 | Version Snapshots on Chat Edits | DEFERRED β€” depends on Phase 3 |
| F6 | custom_prompt_builder.py Cleanup | DONE |
| F3 | Enhanced Scraping with Firecrawl Branding Format | DONE |
| F4 | Preview Thumbnails for Template Gallery | DONE |
| F7 | Multiple Unique Content Scene Variants | DONE |
| OPT | Performance & Quality Optimization (5 fixes) | DONE |
| V2 | Content-Aware Architecture (feat/customv2) | DONE |
---
## Phase 1: DB Models + Enhanced Scraping β€” DONE
### What Was Done
**New file created:**
- `backend/app/models/brand_kit.py` β€” BrandKit model (brand_kits table)
- Fields: id, user_id, source_url, brand_name, colors (JSON), fonts (JSON), design_language (JSON), logos (JSON), images (JSON), timestamps
- Relationships: user, custom_templates
**Files modified:**
- `backend/app/models/custom_template.py` β€” Added:
- `component_code` (Text, nullable) β€” will hold AI-generated React code
- `intro_code` (Text, nullable) β€” intro scene variant
- `outro_code` (Text, nullable) β€” outro scene variant
- `brand_kit_id` (Integer FK β†’ brand_kits.id, nullable, indexed)
- `brand_kit` relationship
- `backend/app/models/scene.py` β€” Added:
- `scene_type` (String(20), nullable) β€” "intro"/"content"/"outro"; NULL = "content"
- `backend/app/models/user.py` β€” Added:
- `brand_kits` relationship (cascade delete)
- `backend/app/models/__init__.py` β€” Added BrandKit import and export
- `backend/app/database.py` β€” Updated:
- `_migrate_sqlite()`: added component_code, intro_code, outro_code, brand_kit_id to ct_migrations; added scene_type to scene_migrations
- `init_db()`: added BrandKit to model imports
- `backend/alembic/env.py` β€” Added BrandKit import
- `backend/alembic/versions/phase1_brand_kit_and_schema_fields.py` β€” New Alembic migration:
- Creates brand_kits table
- Adds 4 columns to custom_templates
- Adds scene_type to scenes
- Down revision: 7ed594338d45
- `backend/app/services/theme_scraper.py` β€” Enhanced:
- `ScrapedThemeData` now has `logo_urls`, `og_image`, `screenshot_url` fields
- New `_extract_logo_urls()` helper extracts logos from HTML (favicon links + img tags with "logo" in attributes)
- Tries Firecrawl screenshot format with graceful fallback
- Extracts OG image from metadata
### Backward Compatibility
- All new columns nullable β€” existing data unaffected
- No API endpoint changes β€” responses identical
- No frontend changes
- ScrapedThemeData new fields are additive β€” ThemeExtractor ignores them
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000`
2. Confirm no startup errors
3. Check brand_kits table exists in SQLite
4. Check new columns on custom_templates and scenes
5. Test existing custom template flow (paste URL β†’ extract β†’ save)
---
## Phase 2: AI Code Generation + Preview β€” DONE
### What Was Done
**New files created:**
- `backend/app/services/code_validator.py` β€” Validates generated code (blocklist for dangerous APIs, SceneComponent declaration check)
- `backend/app/services/code_generator.py` β€” Claude API-based code generation (claude-sonnet-4-5-20250929). Generates 3 variants (intro/content/outro) in one call using XML delimiters. Retry logic on validation failure.
- `frontend/src/utils/compileComponent.ts` β€” @babel/standalone JIT compiler. Lazy-loads Babel (~3MB), transpiles JSX, creates component via Function() factory with injected Remotion APIs.
- `frontend/src/components/RemotionPreviewPlayer.tsx` β€” Remotion Player wrapper. Compiles code string, renders live preview with sample props. Error state with retry button.
**Files modified:**
- `backend/app/routers/custom_templates.py` β€” Added:
- `POST /{template_id}/generate-code` endpoint (calls code_generator, validates, stores in DB)
- `GET /{template_id}/code` endpoint (lightweight code-only response)
- `_serialize_template()` now includes `component_code`, `intro_code`, `outro_code`
- `CustomTemplateOut` schema updated with code fields
- `frontend/src/components/templatePreviews/CustomPreview.tsx` β€” Added:
- Optional `componentCode` prop
- When `componentCode` exists, renders `RemotionPreviewPlayer` (lazy-loaded) instead of carousel
- Existing carousel unchanged as fallback for templates without generated code
- `frontend/src/api/client.ts` β€” Added:
- `component_code`, `intro_code`, `outro_code` fields to `CustomTemplateItem`
- `generateTemplateCode(templateId)` API method
- `getTemplateCode(templateId)` API method
- `frontend/src/components/CustomTemplateCreator.tsx` β€” Added:
- Step 3: "Generating Template" β€” shows spinner during code generation, then live Remotion preview
- After saving template (Step 2), automatically triggers code generation
- Error handling with retry button
- "Done" button to close modal
- `frontend/src/pages/CustomTemplates.tsx` β€” Updated:
- Template grid passes `componentCode` to CustomPreview for live Remotion previews
- `frontend/package.json` β€” Added:
- `@babel/standalone` dependency
- `@types/babel__standalone` dev dependency
### Architecture Decisions
- **Claude API for code gen** (not DSPy) β€” raw code output doesn't fit DSPy's structured fields
- **Separate endpoint** β€” code gen is slow (10-30s), doesn't block template creation
- **One LLM call for 3 variants** β€” intro/content/outro in one call for visual coherence
- **Lazy-loaded Babel** β€” ~3MB chunk only loaded when viewing templates with generated code
- **No fallback** β€” compilation errors surface inline with retry option
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000`
2. Create a custom template via existing flow (paste URL β†’ extract β†’ save)
3. Step 3 should auto-trigger code generation with spinner
4. After generation, live Remotion preview should play in the modal
5. On Custom Templates page, templates with generated code show live previews
6. `npm run build` passes βœ“
---
## Phase 3: Chat-Based Editing β€” DONE
### What Was Done
**New files created:**
- `backend/app/models/code_edit_history.py` β€” CodeEditHistory model (code_edit_history table)
- Fields: id, template_id (FK, CASCADE), code_type ("component"/"intro"/"outro"), previous_code, instruction, summary, created_at
- Composite index on (template_id, code_type)
- Relationship: template (back to CustomTemplate)
- `backend/alembic/versions/phase3_code_edit_history.py` β€” Alembic migration
- Creates code_edit_history table
- Down revision: phase1_brand_kit
- `frontend/src/components/TemplateCodeEditor.tsx` β€” Full-screen code editor overlay
- Two-panel layout: Remotion preview (left) + chat panel (right)
- Variant tabs (Intro/Content/Outro) to switch which code is previewed/edited
- Chat messages reconstructed from edit history on mount
- Typing indicator dots during Claude processing
- Undo button per variant (reverts to previous code version)
- Suggestion chips for common edit requests
- Error display for failed edits
**Files modified:**
- `backend/app/services/code_generator.py` β€” Added:
- `EDIT_SYSTEM_PROMPT` β€” system prompt for code editing (modify existing code, keep everything else intact)
- `_parse_edit_response()` β€” parses `<CODE>` and `<SUMMARY>` XML tags from response
- `edit_component_code(existing_code, instruction)` β€” calls Claude to edit code, validates, retries on failure. Returns `{ code, summary }`
- `backend/app/routers/custom_templates.py` β€” Added:
- `EditCodeRequest` / `UndoEditRequest` Pydantic schemas
- `VARIANT_FIELD_MAP` helper dict
- `POST /{template_id}/edit-code` β€” sends existing code + instruction to Claude, saves edit history, updates template code
- `POST /{template_id}/undo-edit` β€” reverts to previous code version from history, deletes history row
- `GET /{template_id}/edit-history` β€” returns list of edits (instruction + summary, no code) for chat reconstruction
- `backend/app/models/custom_template.py` β€” Added:
- `edit_history` relationship (cascade delete-orphan)
- `backend/app/models/__init__.py` β€” Added CodeEditHistory import and export
- `backend/app/database.py` β€” Added CodeEditHistory to init_db imports (SQLite auto-creates via create_all)
- `backend/alembic/env.py` β€” Added CodeEditHistory import
- `frontend/src/api/client.ts` β€” Added:
- `CodeVariant` type ("component" | "intro" | "outro")
- `CodeEditHistoryItem` interface
- `editTemplateCode(templateId, instruction, variant)` API method
- `undoTemplateEdit(templateId, variant)` API method
- `getTemplateEditHistory(templateId)` API method
- `frontend/src/pages/CustomTemplates.tsx` β€” Added:
- `codeEditTarget` state for full-screen code editor
- "Customize" button on template cards (only shown when component_code exists, purple filled)
- `TemplateCodeEditor` rendering when codeEditTarget is set
- `preloadBabel()` call on page mount
- `handleCodeEditorClose()` to update template in grid after editing
### Architecture Decisions
- **Separate edit function** β€” `edit_component_code()` uses a different system prompt than generation (focused on modifying existing code, not creating from scratch)
- **Per-variant editing** β€” user selects which variant (Intro/Content/Outro) to edit; edits apply to one variant at a time (saves cost, more predictable)
- **Edit history table** β€” follows SceneEditHistory pattern; stores previous_code for multi-level undo
- **Chat reconstruction** β€” edit history reconstructed into chat messages on mount (no separate chat persistence)
- **No new npm dependencies** β€” reuses existing @babel/standalone, @remotion/player, RemotionPreviewPlayer
- **Separate from CustomTemplateEditor** β€” existing editor kept for name/style changes; new TemplateCodeEditor is full-screen for code editing
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000` β€” no errors, code_edit_history table created
2. Create a template with generated code (Phase 2 flow)
3. On Custom Templates page, "Customize" button appears on templates with code
4. Click "Customize" β†’ full-screen editor opens with preview + chat
5. Type "make the title bigger" β†’ Claude edits code β†’ preview updates live
6. Click "Undo" β†’ code reverts to previous version β†’ preview updates
7. Close editor β†’ template card on grid shows updated preview
8. `npm run build` passes βœ“
---
## Phase 4: Render Pipeline Update β€” DONE
### What Was Done
**New files created:**
- `remotion-video/src/templates/generated/types.ts` β€” TypeScript types for generated video data (GeneratedSceneProps, GeneratedVideoData, GeneratedSceneData)
- `remotion-video/src/templates/generated/SceneIntro.tsx` β€” Placeholder intro scene component (overwritten with AI code at render time)
- `remotion-video/src/templates/generated/SceneContent.tsx` β€” Placeholder content scene component (overwritten with AI code at render time)
- `remotion-video/src/templates/generated/SceneOutro.tsx` β€” Placeholder outro scene component (overwritten with AI code at render time)
- `remotion-video/src/templates/generated/GeneratedVideo.tsx` β€” New Remotion composition:
- Reads data.json (including brandColors and sceneType per scene)
- Statically imports SceneIntro, SceneContent, SceneOutro components
- Maps each scene to the right component based on sceneType (explicit from DB, or inferred: first=intro, last=outro, middle=content)
- Handles font loading via delayRender/continueRender
- Sequences scenes with Audio and LogoOverlay (same pattern as CustomVideo)
- calculateGeneratedMetadata for dynamic duration/dimensions
**Files modified:**
- `remotion-video/src/Root.tsx` β€” Added:
- Import of GeneratedVideo + calculateGeneratedMetadata
- New `<Composition id="GeneratedVideo" .../>` registration
- `backend/app/services/template_service.py` β€” Updated:
- `_load_custom_template_data()` now includes `has_generated_code`, `component_code`, `intro_code`, `outro_code` in cached result
- `_get_custom_meta()` passes `has_generated_code` to `build_custom_meta`
- `backend/app/services/custom_prompt_builder.py` β€” Updated:
- `build_custom_meta()` accepts `has_generated_code` parameter
- Returns `composition_id: "GeneratedVideo"` when template has AI code, `"CustomVideo"` otherwise
- `backend/app/services/remotion.py` β€” Updated:
- `provision_workspace()` calls `_write_generated_scene_files()` for custom templates with AI code
- `_write_generated_scene_files()` (NEW) β€” loads component code from DB, wraps in proper .tsx module with imports, writes to workspace `src/templates/generated/` directory
- `_wrap_generated_code()` (NEW) β€” wraps raw AI-generated code (`const SceneComponent = ...`) in a proper ESM module with Remotion imports + default export
- `write_remotion_data()` β€” adds `brandColors` and `sceneType` per scene to data.json when template has generated code
### Architecture Decisions
- **Static file overwrite** β€” generated code is written as actual .tsx files in the workspace, overwriting placeholders. Vite bundles them normally β€” no runtime compilation, no @babel/standalone needed server-side.
- **Placeholder pattern** β€” the repo contains stub scene components so Root.tsx always compiles. At render time, these stubs are replaced with the real AI-generated code.
- **composition_id routing** β€” `GeneratedVideo` vs `CustomVideo` is selected via `build_custom_meta` based on whether the template has `component_code`. Old templates without generated code continue to use UniversalScene.
- **brandColors in data.json** β€” derived from theme colors, passed to GeneratedSceneProps. Generated components use `props.brandColors` directly.
- **sceneType tagging** β€” each scene gets `sceneType` ("intro"/"content"/"outro") from the DB `scene_type` field, falling back to positional inference (first=intro, last=outro).
- **Code wrapping** β€” raw AI code (`const SceneComponent = (props) => { ... }`) is wrapped with Remotion imports and `export default SceneComponent`. This makes the generated files proper ES modules that Vite can bundle.
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000` β€” no errors
2. Create a template with generated code (Phase 2 flow)
3. Trigger a video render for a project using the generated template
4. Check logs: should see `[REMOTION] GeneratedVideo: brandColors and sceneTypes set for N scenes`
5. Check workspace: `src/templates/generated/SceneIntro.tsx` etc. should contain the AI-generated code wrapped in proper imports
6. Render should produce an MP4 using the AI-generated components
7. Templates without generated code should still render via CustomVideo + UniversalScene
8. TypeScript check passes: `cd remotion-video && npx tsc --noEmit` βœ“
---
## Phase 5: Polish & Preview System β€” DONE
### What Was Done
**New file created:**
- `backend/app/models/template_version.py` β€” TemplateVersion model (template_versions table)
- Fields: id, template_id (FK, CASCADE), component_code, intro_code, outro_code, label, created_at
- Relationship: template (back to CustomTemplate)
- `backend/alembic/versions/phase5_template_versions.py` β€” Alembic migration
- Creates template_versions table
- Adds current_version_id column to custom_templates
- Down revision: phase3_code_edit_history
**Files modified:**
- `backend/app/models/custom_template.py` β€” Added:
- `current_version_id` (Integer, nullable) β€” pointer to active version
- `versions` relationship (cascade delete-orphan, ordered by created_at desc)
- `backend/app/models/__init__.py` β€” Added TemplateVersion import and export
- `backend/app/database.py` β€” Added:
- TemplateVersion to init_db imports
- `current_version_id` to SQLite migrations for custom_templates
- `backend/alembic/env.py` β€” Added TemplateVersion import
- `backend/app/routers/custom_templates.py` β€” Added:
- `_save_version()` helper β€” snapshots code fields into a new TemplateVersion row
- Updated `generate_code()` β€” saves "Initial generation" version after first code gen
- `POST /{template_id}/regenerate-code` β€” regenerates all code variants from scratch; snapshots current code before overwriting, saves new "Regenerated" version
- `GET /{template_id}/versions` β€” lists all saved versions (newest first) with current_version_id
- `POST /{template_id}/versions/{version_id}/rollback` β€” restores code from a previous version; snapshots current state as "Before rollback" first
- `CustomTemplateOut` and `_serialize_template()` now include `current_version_id`
- `frontend/src/api/client.ts` β€” Added:
- `current_version_id` to `CustomTemplateItem` interface
- `TemplateVersionItem` and `TemplateVersionsResponse` interfaces
- `regenerateTemplateCode(templateId)` API method
- `getTemplateVersions(templateId)` API method
- `rollbackTemplateVersion(templateId, versionId)` API method
- `frontend/src/pages/CustomTemplates.tsx` β€” Added:
- `regeneratingId` state for tracking which template is regenerating
- `handleRegenerate()` function β€” calls regenerate API, updates template in grid
- Regenerate icon button on each template card (next to Customize), with spinning SVG animation during regeneration
- `frontend/src/components/TemplateCodeEditor.tsx` β€” Added:
- Version history state: `versions`, `currentVersionId`, `showVersions`, `isRegenerating`, `isRollingBack`
- `loadVersions()` β€” fetches version list from API on mount
- `handleRegenerate()` β€” regenerates code from within editor, clears chat, reloads versions
- `handleRollback(versionId)` β€” restores a previous version, reloads version list
- Regenerate button in preview panel header (purple outline, with spinner)
- "Versions (N)" toggle button in chat header β€” opens collapsible version history panel
- Version history panel: lists all versions with labels, timestamps, "(current)" badge, and "Restore" button per version
### Architecture Decisions
- **Version snapshots** β€” each version stores a full copy of all 3 code variants (intro/component/outro). Not incremental diffs β€” simpler, no reconstruction needed.
- **Automatic versioning** β€” versions are created automatically on generate, regenerate, and rollback. No manual "save version" step needed.
- **Pre-rollback snapshots** β€” before a rollback overwrites code, the current state is saved as a "Before rollback" version, so users can always undo a rollback.
- **current_version_id** β€” pointer on the template to its active version. Used by frontend to highlight "(current)" in the version list. Not a FK constraint (flexible).
- **No preview thumbnails yet** β€” this phase focused on regeneration + versioning. Server-side mini-renders for gallery thumbnails can be added later as an optimization.
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000` β€” no errors, template_versions table created
2. Create a template with generated code (Phase 2 flow) β€” a version is saved automatically
3. On Custom Templates page, hover a template card β†’ Regenerate button (refresh icon) appears next to Customize
4. Click Regenerate β†’ spinner shows β†’ new code generated β†’ preview updates β†’ old version preserved
5. Click Customize β†’ full-screen editor opens β†’ "Versions (N)" button in chat header
6. Click "Versions (N)" β†’ version history panel opens with all versions listed
7. Click "Restore" on an older version β†’ code rolls back β†’ preview updates
8. TypeScript check passes (no new errors) βœ“
---
## Phase 6: Cleanup β€” Remove Old UniversalScene/StyleEngine/Fallback Code β€” DONE
### What Was Done
**Files deleted:**
- `remotion-video/src/templates/custom/` β€” entire directory removed:
- `UniversalScene.tsx` (36KB) β€” old fixed layout engine (9 arrangements, 10 element types)
- `CustomVideo.tsx` (10KB) β€” old Remotion composition using UniversalScene
- `types.ts` (3KB) β€” old CustomTheme, SceneLayoutConfig types
- `utils/styleEngine.ts` (11KB) β€” theme style β†’ CSS decoration mapper
- `utils/normalize.ts` (7KB) β€” layout config normalization helpers
- `frontend/src/components/remotion/custom/` β€” entire directory removed:
- `UniversalScene.tsx` β€” frontend copy of server UniversalScene
- `CustomVideoComposition.tsx` β€” frontend copy of CustomVideo
- `types.ts` β€” frontend copy of custom types
- `utils/` β€” frontend copy of utils
**Files modified:**
- `remotion-video/src/Root.tsx` β€” Removed:
- `CustomVideo` + `calculateCustomMetadata` import
- `<Composition id="CustomVideo" .../>` registration
- Only `GeneratedVideo` composition remains for custom templates
- `backend/app/services/remotion.py` β€” Updated:
- Removed `has_generated_code` conditional in `write_remotion_data()` β€” brandColors and sceneType are now always set for custom templates
- Replaced `print()` calls with `logger.info()`/`logger.warning()`
- `backend/app/services/custom_prompt_builder.py` β€” Updated:
- Removed `has_generated_code` parameter from `build_custom_meta()`
- `composition_id` is now always `"GeneratedVideo"` β€” no fallback to `"CustomVideo"`
- `backend/app/services/template_service.py` β€” Updated:
- Removed `has_generated_code` kwarg from `build_custom_meta()` call
- `frontend/src/components/remotion/remotionAdapters.tsx` β€” Removed:
- `UniversalScene` import from `@remotion-video/templates/custom/UniversalScene`
- `CustomTheme`, `SceneLayoutConfig` type imports from `@remotion-video/templates/custom/types`
- `RemotionCustomSceneInput` interface
- `RemotionCustomVideoCompositionProps` interface
- `RemotionCustomVideoComposition` component (entire ~100 line composition)
- `frontend/src/components/remotion/templateConfig.tsx` β€” Removed:
- `CustomVideoComposition` import
- `CUSTOM_ARRANGEMENTS` set
- `custom` entry from `TEMPLATE_REGISTRY`
- `custom_` routing in `getTemplateConfig()` (no longer routes to old system)
- `RemotionCustomVideoComposition` from the remotion source override chain
- `frontend/src/types/remotion-video.d.ts` β€” Removed:
- `@remotion-video/templates/custom/UniversalScene` module declaration
- `@remotion-video/templates/custom/types` module declaration
- `frontend/src/components/templatePreviews/CustomPreview.tsx` β€” Rewritten:
- Removed 3-slide carousel fallback (SlideHero, SlideContent, SlideTypo, SlideDots, ScaledCanvas)
- Removed all old style helpers (getStyleBg, getCardStyle, getDecorations)
- Now only renders `RemotionPreviewPlayer` when `componentCode` exists
- Shows simple placeholder when `componentCode` is missing
- `frontend/src/components/SceneEditModal.tsx` β€” Updated:
- Changed comment from "UniversalScene handles sizing" to "Custom template arrangements"
### Architecture Changes
- **Single rendering path** β€” All custom templates now use `GeneratedVideo` composition exclusively. No fallback to `CustomVideo` + `UniversalScene`.
- **~107KB of dead code removed** β€” UniversalScene (36KB), styleEngine (11KB), normalize (7KB), CustomVideo (10KB Γ— 2 copies), types (3KB Γ— 2 copies), carousel preview (~400 lines).
- **No dual-path conditionals** β€” `build_custom_meta()` always returns `composition_id: "GeneratedVideo"`. `write_remotion_data()` always writes brandColors + sceneType for custom templates.
### Verification Steps
1. `cd remotion-video && npx tsc --noEmit` β€” passes βœ“
2. `cd frontend && npx tsc --noEmit` β€” passes (only pre-existing @babel/standalone type issue from missing npm install) βœ“
3. Backend imports: `python -c "from app.services.remotion import provision_workspace; ..."` β€” passes βœ“
4. Custom templates page shows live Remotion previews (no carousel fallback)
5. Video rendering for custom templates uses GeneratedVideo composition exclusively
---
## Phase H1: Rate Limiting on Claude API Endpoints β€” DONE
### What Was Done
**File modified:**
- `backend/app/routers/custom_templates.py` β€” Added:
- `_ai_call_counts` in-memory dict: `user_id β†’ (date_string, count)`
- `AI_DAILY_LIMIT = 5` constant
- `_check_ai_rate_limit(user_id)` helper β€” checks daily count, resets on new day, raises HTTP 429 if exceeded
- Rate limit check added at top of `generate_code()`, `regenerate_code()`, and `edit_code()` endpoints
### Design Decisions
- **In-memory counter** β€” matches existing `_render_progress` pattern. Single Cloud Run instance, so in-memory is fine.
- **Combined limit** β€” 5 calls/day total across generate + regenerate + edit (not 5 per endpoint).
- **Daily reset** β€” counter resets on date change, no cron needed.
- **Future extensibility** β€” `AI_DAILY_LIMIT` can be made per-plan (Free: 5, Standard: 20, Pro: 50).
---
## Phase H2: Code Validator Hardening β€” DONE
### What Was Done
**File modified:**
- `backend/app/services/code_validator.py` β€” Enhanced:
- Added `DANGEROUS_REGEX` list β€” 18 compiled regex patterns with word-boundary matching for `eval`, `Function`, `fetch`, `document`, `window`, `process`, `globalThis`, `require`, `import`, `__proto__`, `constructor[]`, `Proxy`, `Reflect`, `XMLHttpRequest`, `WebSocket`, `localStorage`, `sessionStorage`, `cookie`
- Added `MAX_CODE_LENGTH = 15,000` β€” rejects code exceeding this limit
- Added `MAX_NESTING_DEPTH = 20` β€” rejects excessively nested brace structures
- Added balanced braces check β€” ensures `{` and `}` counts match
- Two-pass validation: fast substring check first, then thorough regex pass
### Design Decisions
- **Regex word boundaries** β€” `\beval\b` catches `eval` but not `evaluation`, and is harder to bypass with string concatenation tricks
- **New blocked APIs** β€” `Proxy`, `Reflect` (metaprogramming), `WebSocket` (network), `localStorage`/`sessionStorage`/`cookie` (storage)
- **Structural checks** β€” max length prevents abuse via massive code payloads; nesting depth catches obfuscated code; balanced braces catch malformed output
---
## Phase H3: Template Cache TTL β€” DONE
### What Was Done
**Files modified:**
- `backend/app/services/template_service.py` β€” Updated:
- Cache type changed from `dict[str, data]` to `dict[str, (data, timestamp)]`
- Added `_CACHE_TTL = 30.0` seconds constant
- `_load_custom_template_data()` now checks `time.monotonic()` age on cache hit β€” refetches from DB if expired
- Explicit invalidation (`invalidate_custom_template_cache()`) still works for same-process freshness
- `backend/app/services/remotion.py` β€” Updated:
- `provision_workspace()` calls `invalidate_custom_template_cache(template_id)` before writing scene files, ensuring fresh DB data
### Design Decisions
- **30s TTL** β€” short enough for multi-worker consistency, long enough to avoid hammering DB during a single request lifecycle
- **`time.monotonic()`** β€” immune to system clock changes
- **Explicit invalidation preserved** β€” same-process updates still get instant cache busting
---
## Phase H4: Render Workspace Locking β€” DONE
### What Was Done
**File modified:**
- `backend/app/services/remotion.py` β€” Added:
- `_workspace_locks: dict[int, threading.Lock]` β€” per-project lock registry
- `_get_workspace_lock(project_id)` β€” returns or creates a lock for a project
- `provision_workspace()` body wrapped in `with _get_workspace_lock(project_id):`
### Design Decisions
- **Per-project locks** β€” different projects can provision concurrently; only same-project requests serialize
- **Same pattern as `_render_progress`** β€” uses module-level dict for lock storage
- **`threading.Lock`** β€” sufficient for single-process multi-thread scenarios (uvicorn workers)
---
## Phase H5: Frontend Code Sandbox β€” DONE
### What Was Done
**Files modified:**
- `frontend/index.html` β€” Added:
- CSP meta tag: `script-src 'self' 'unsafe-eval'` (needed for Babel), `connect-src 'self' https://*.blog2video.app` (blocks generated code from fetching external URLs), restricted `style-src`, `img-src`, `media-src`, `font-src`
- `frontend/src/components/RemotionPreviewPlayer.tsx` β€” Added:
- `PlayerErrorBoundary` class component β€” catches React render errors from AI-generated components
- Shows friendly error message with "Retry" button instead of crashing the app
- Player JSX wrapped in `<PlayerErrorBoundary>`
### Design Decisions
- **CSP as first line of defense** β€” even if generated code contains `fetch()`, the browser blocks it from reaching external URLs
- **`unsafe-eval` required** β€” @babel/standalone needs `eval` for JIT compilation; CSP restricts everything else
- **Error boundary on Player** β€” catches runtime errors from compiled AI components; shows error UI instead of white screen
---
## Phase H6: Self-healing current_version_id β€” DONE
### What Was Done
**File modified:**
- `backend/app/routers/custom_templates.py` β€” Updated:
- `list_versions()` now checks if `tpl.current_version_id` exists in the fetched versions set
- If not found (dangling pointer), auto-sets to the newest version's ID and commits
### Design Decisions
- **Lazy self-healing** β€” only checks when versions are listed (read path), not on every template access
- **Newest version wins** β€” if pointer is dangling, the most recent version is the safest default
---
## Phase F1: BrandKit Population During Theme Extraction β€” DONE
### What Was Done
**Files modified:**
- `backend/app/routers/custom_templates.py` β€” Updated:
- `ExtractThemeResponse` now includes `logo_urls`, `og_image`, `screenshot_url` from ScrapedThemeData
- `CreateCustomTemplateRequest` now accepts optional `logo_urls`, `og_image`, `screenshot_url`
- `extract_theme()` passes scraped logo/image data through to the response
- `create_custom_template()` now creates a `BrandKit` row from theme data (colors, fonts, design_language, logos, images) and links it via `brand_kit_id`
- `frontend/src/api/client.ts` β€” Updated:
- `ExtractThemeResponse` interface includes `logo_urls`, `og_image`, `screenshot_url`
- `createCustomTemplate()` accepts optional `logo_urls`, `og_image`, `screenshot_url`
- `frontend/src/components/CustomTemplateCreator.tsx` β€” Updated:
- Stores scraped `logo_urls`, `og_image`, `screenshot_url` from extract response in state
- Passes them to `createCustomTemplate()` call so BrandKit is populated
### Design Decisions
- **BrandKit auto-created on template creation** β€” every new template gets a linked BrandKit, populating data that `code_generator.py` already reads (lines 199-207)
- **Logo URLs stored as JSON list** β€” scraped logo URLs are raw strings; F2 adds structured upload with primary/secondary types
- **OG image stored in BrandKit.images** β€” serves as the initial brand image until user uploads custom ones
---
## Phase F2: Brand Asset Uploads (Logo) β€” DONE
### What Was Done
**Files modified:**
- `backend/app/services/r2_storage.py` β€” Added:
- `brand_asset_key(user_id, brand_kit_id, filename)` helper for R2 key generation
- `backend/app/routers/custom_templates.py` β€” Added:
- `POST /{template_id}/upload-logo` endpoint β€” validates file type (PNG/JPEG/WebP/SVG) and size (2MB), auto-creates BrandKit if missing, uploads to R2, updates `bk.logos` JSON with primary logo entry
- FastAPI `UploadFile`, `File` imports
- `backend/app/services/template_service.py` β€” Updated:
- `_load_custom_template_data()` now includes `brand_kit` dict (colors, fonts, logos, design_language, images) in cached result
- `backend/app/services/remotion.py` β€” Updated:
- `write_remotion_data()` reads brand_kit logos from custom_data, downloads primary logo to workspace public/ folder, sets `data["brandLogo"]` for GeneratedVideo access via `staticFile()`
- `frontend/src/api/client.ts` β€” Added:
- `uploadTemplateLogo(templateId, file)` API method
- `frontend/src/components/TemplateCodeEditor.tsx` β€” Added:
- Hidden file input + "Logo" button in preview panel header
- `handleLogoUpload()` β€” uploads file, updates template state, shows success/error message in chat
### Design Decisions
- **Auto-create BrandKit on upload** β€” if template was created before F1, uploading a logo still works by auto-creating a minimal BrandKit
- **Logo format migration** β€” handles old BrandKit.logos format (list of URL strings) and new format (list of dicts with type/url)
- **Primary logo replacement** β€” uploading a new logo replaces the previous primary, preserving scraped logos as type "scraped"
- **Brand logo in data.json** β€” downloaded to workspace `public/brand-logo.png` for `staticFile()` access in GeneratedVideo
---
## Phase F5: Version Snapshots on Chat Edits β€” DONE
### What Was Done
**File modified:**
- `backend/app/routers/custom_templates.py` β€” Updated:
- `edit_code()` now counts total edits for the template after each edit
- Every 3rd edit, auto-saves a version snapshot via `_save_version(tpl, f"After {count} edits", db)`
### Design Decisions
- **Every 3rd edit** β€” balances version granularity vs. list clutter; users get checkpoints at edits 3, 6, 9, etc.
- **Combined with existing versioning** β€” these auto-versions appear alongside generate/regenerate/rollback versions in the version panel
- **Non-blocking** β€” version save happens in the same DB transaction as the edit, no extra overhead
---
## Phase F6: custom_prompt_builder.py Cleanup β€” DONE
### What Was Done
**File modified:**
- `backend/app/services/custom_prompt_builder.py` β€” Updated:
- `build_custom_meta()` simplified: removed `valid_arrangements`, `preferred_arrangements`, `arrangements_without_image`, `hero_arrangement`, `fallback_arrangement`
- Kept `valid_layouts`, `layouts_without_image` (used by SceneEditModal), `hero_layout`, `fallback_layout` (used by scene generation)
- Added docstring explaining dual purpose: `build_custom_prompt()` is for DSPy AI script generation, `build_custom_meta()` is for pipeline routing
### Design Decisions
- **Prompt preserved** β€” `build_custom_prompt()` still generates the full 300-line prompt for DSPy scene generation (narration, visual hints) even though GeneratedVideo doesn't use arrangement/element layout data
- **Meta simplified** β€” removed arrangement-specific fields that GeneratedVideo ignores; kept layout fields that the API pipeline (SceneEditModal dropdown) still uses
---
## Phase F3: Enhanced Scraping with Firecrawl Branding Format β€” DONE
### What Was Done
**Files modified:**
- `backend/app/services/theme_scraper.py` β€” Updated:
- `ScrapedThemeData` has new `branding: dict | None` field
- `scrape_for_theme()` tries `formats=["branding", "html", "markdown", "screenshot"]` first
- Falls back to standard formats if branding not available on the Firecrawl plan
- Normalizes branding_data to dict (handles SDK object or raw dict)
- `backend/app/dspy_modules/theme_extractor.py` β€” Added:
- `_apply_branding_overrides()` static method on `ThemeExtractor`
- After DSPy theme extraction, merges Firecrawl branding data as high-confidence overrides
- Overrides: `colors.primary→accent`, `colors.background→bg`, `colors.text→text`, `colors.secondary→surface`
- Overrides: `typography.headingFont→fonts.heading`, `typography.bodyFont→fonts.body`
- Called in `extract_theme()` only when `scraped.branding` is present and is a dict
### Design Decisions
- **Graceful fallback** β€” branding format may not be available on all Firecrawl plans; try/except ensures zero regression
- **Override, not replace** β€” Firecrawl branding values are merged on top of DSPy-extracted theme; DSPy still provides style, animation, patterns, category
- **Only override when present** β€” each branding field is checked individually; missing fields are left as DSPy extracted them
---
## Phase F4: Preview Thumbnails for Template Gallery β€” DONE
### What Was Done
**New file created:**
- `backend/app/services/thumbnail_renderer.py` β€” Renders single-frame PNG thumbnail:
- `render_template_thumbnail(template_id, user_id)` β€” provisions temporary workspace, writes mock data.json with 1 intro scene, runs `npx remotion still GeneratedVideo` at 480p, uploads PNG to R2
- Uses negative project IDs for temp workspace to avoid collision
- Cleanup: removes temp workspace in finally block
**Files modified:**
- `backend/app/routers/custom_templates.py` β€” Updated:
- `_render_and_store_thumbnail()` background task helper β€” calls renderer, stores URL in `tpl.preview_image_url`
- `generate_code()` and `regenerate_code()` now accept `BackgroundTasks` and kick off thumbnail render after code gen
- `_serialize_template()` now includes `preview_image_url`
- `frontend/src/api/client.ts` β€” Updated:
- `CustomTemplateItem` includes `preview_image_url: string | null`
- `frontend/src/components/templatePreviews/CustomPreview.tsx` β€” Rewritten:
- If `previewImageUrl` exists and user hasn't hovered, shows static image with play icon overlay (instant load)
- On hover, lazy-loads the full `RemotionPreviewPlayer` (Babel JIT compilation)
- Suspense fallback uses thumbnail image if available
- `frontend/src/pages/CustomTemplates.tsx` β€” Updated:
- Passes `previewImageUrl={tpl.preview_image_url}` to CustomPreview in template grid
### Design Decisions
- **Background task** β€” thumbnail rendering is non-critical; failure doesn't affect code generation response
- **`remotion still`** β€” uses Remotion's built-in still image command (frame 0) instead of full video render; much faster
- **480p resolution** β€” sufficient for gallery thumbnails, keeps file size small
- **Hover-to-play** β€” template grid loads instantly with static images; @babel/standalone (~3MB) only loaded when user interacts
- **Thumbnail as Suspense fallback** β€” when Player is loading, the thumbnail serves as a smooth placeholder instead of a blank rectangle
---
## Phase F7: Multiple Unique Content Scene Variants β€” DONE
### What Was Done
Previously, Claude generated only 3 variants (1 intro, 1 content, 1 outro). All content scenes reused the same component β€” different text/image props but identical layout. This was the same fundamental limitation as the old UniversalScene system.
Now, Claude decides how many unique scene variants to create (typically 5-8 total). Each content scene gets a visually distinct layout, and scenes cycle through variants at render time.
**Before:** Intro β†’ Content (same layout Γ— N) β†’ Outro
**After:** Intro β†’ Scene1 β†’ Scene2 β†’ Scene3 β†’ Scene4 β†’ Scene5 β†’ Outro (all unique layouts)
**Files modified:**
- `backend/app/models/custom_template.py` β€” Added `content_codes` (Text, nullable) β€” JSON array of content variant code strings
- `backend/app/models/template_version.py` β€” Added `content_codes` (Text, nullable) β€” version snapshots include all content variants
- `backend/app/database.py` β€” Added `content_codes` to SQLite migrations for both custom_templates and template_versions
- `backend/app/services/code_generator.py` β€” Major rewrite:
- New SYSTEM_PROMPT: asks Claude to generate 5-8 scene variants using `<SCENE_0>`, `<SCENE_1>`, ..., `<SCENE_N>` XML tags
- Claude decides how many unique layouts the brand needs
- Scene 0 = intro, last scene = outro, all middle scenes = unique content variants
- `_parse_scene_variants()` replaces `_parse_variants()` β€” parses dynamic numbered XML tags
- Returns `content_codes: list[str]` instead of single `component_code`
- `max_tokens` increased from 8000 to 16000 for multiple variants
- `backend/app/routers/custom_templates.py` β€” Updated:
- `EditCodeRequest.variant` now accepts `content_N` format (e.g., `content_0`, `content_3`)
- `_parse_variant()` helper parses variant strings into (type, index) tuples
- `_get_variant_code()` / `_set_variant_code()` handle reading/writing content codes by index
- `generate_code()` and `regenerate_code()` store `content_codes` as JSON
- `_save_version()` snapshots `content_codes`
- Rollback restores `content_codes`
- `_serialize_template()` includes `content_codes` (parsed from JSON)
- `backend/app/services/template_service.py` β€” Updated:
- `_load_custom_template_data()` parses and caches `content_codes`
- `has_generated_code` checks both `component_code` and `content_codes`
- `backend/app/services/remotion.py` β€” Updated:
- `_write_generated_scene_files()` now writes SceneContent0.tsx, SceneContent1.tsx, ..., SceneContentN.tsx
- Generates `contentRegistry.ts` β€” exports `CONTENT_VARIANTS` array with all content components
- Generates SceneContent.tsx (re-exports Content0 for backward compat)
- `write_remotion_data()` adds `contentVariantIndex` and `contentVariantCount` to data.json
- Content scenes are assigned variant indices cyclically (scene[i] uses variant[i % numVariants])
**New files created:**
- `remotion-video/src/templates/generated/contentRegistry.ts` β€” Placeholder; at render time, overwritten with imports of all content variant components
- `remotion-video/src/templates/generated/SceneContent0.tsx` β€” Placeholder; re-exports SceneContent
**Files rewritten:**
- `remotion-video/src/templates/generated/GeneratedVideo.tsx` β€” Now imports from `contentRegistry.ts` instead of single ContentScene. `getSceneComponent()` picks variant by `contentVariantIndex` from data.json.
- `remotion-video/src/templates/generated/types.ts` β€” Added `contentVariantIndex` and `contentVariantCount` fields
- `frontend/src/components/TemplateCodeEditor.tsx` β€” Dynamic variant tabs built from `template.content_codes`. Shows "Intro | Scene 1 | Scene 2 | ... | Scene N | Outro" tabs. Each tab can be independently edited via chat.
- `frontend/src/api/client.ts` β€” `CodeVariant` type changed from `"component" | "intro" | "outro"` to `"intro" | "outro" | \`content_${number}\``. `CustomTemplateItem` includes `content_codes: string[] | null`.
### Architecture Decisions
- **Claude decides count** β€” The prompt asks for "at least 5 variants" but lets Claude decide the right number (5-8) based on the brand personality. Complex brands might get more variety.
- **Cyclic assignment** β€” If there are 5 content variants and 8 content scenes, scenes cycle: 0,1,2,3,4,0,1,2. Simple, predictable, no AI needed at render time.
- **Backward compatibility** β€” `component_code` is always set to the first content variant. Old code that reads `component_code` still works (preview, gallery, etc).
- **Per-variant editing** β€” User selects a specific scene tab (e.g., "Scene 3") and edits just that variant. Edits are scoped and precise.
- **contentRegistry.ts pattern** β€” Instead of dynamic imports (which Vite can't bundle), we generate a registry file at render time that statically imports all content components. Vite bundles them normally.
- **max_tokens increase** β€” 8000 β†’ 16000 to accommodate 5-8 full React components in one API call.
### Verification Steps
1. Start backend: `cd backend && uvicorn app.main:app --reload --port 8000` β€” no errors, content_codes column auto-created
2. Create a template and generate code β€” should see "Generated N scene variants (1 intro + X content + 1 outro)" in backend logs
3. On Custom Templates page, click "Customize" β€” editor should show tabs: "Intro | Scene 1 | Scene 2 | ... | Outro"
4. Click each tab β€” different preview for each scene variant
5. Edit a specific variant via chat β€” only that variant changes
6. Trigger a video render β€” check workspace: should have SceneContent0.tsx through SceneContentN.tsx
7. Rendered video should have visually different layouts for each content scene
8. TypeScript check passes: `cd remotion-video && npx tsc --noEmit` βœ“
---
## Phase OPT: Custom Template Performance & Quality Optimization β€” DONE
### Overview
5 fixes targeting custom template speed (246s β†’ ~140s), animation quality, and UX. Previously code generation was blocking, scenes were 500-800 lines with repetitive animations, and CSS/logo scraping was broken.
### Fix 1: CSS & Logo Scraping Improvements β€” DONE
**Files modified:**
- `backend/app/services/theme_scraper.py`
- `_extract_css_content()` now fetches external `<link rel="stylesheet">` CSS files (first 2-3, up to `_MAX_CSS_CHARS`) in addition to inline `<style>` blocks
- Logo fallbacks improved: og:image added to logos list, SVG logo detection via logo-like class names, favicon.ico fallback
- Note: SPA sites (e.g. noon.com) use CSS-in-JS, so CSS=0 is expected for them β€” the AI reads inline styles from the rendered HTML instead
### Fix 2: Theme Extraction Speed β€” DONE
**Files modified:**
- `backend/app/services/theme_scraper.py`
- `_MAX_HTML_CHARS` reduced: 40,000 β†’ 15,000 (AI only needs enough HTML for colors/fonts/layout patterns)
- `_MAX_MARKDOWN_CHARS` reduced: 5,000 β†’ 2,000
- `backend/app/dspy_modules/__init__.py`
- `get_theme_lm()` model changed: `anthropic/claude-sonnet-4-6` β†’ `anthropic/claude-haiku-4-5-20251001` (structured JSON extraction doesn't need Sonnet)
**Impact:** Theme extraction ~25s β†’ ~13.8s (tested on noon.com)
### Fix 3: Code Generation Speed β€” DONE
**Files modified:**
- `backend/app/services/code_generator.py`
- Line target in prompt: "There is NO line limit" β†’ "Target 200-350 lines. Spend tokens on animation logic, not verbose inline styles."
- Brevity reward in `_scene_reward()`: +0.05 for 200-350 lines, -0.10 for >500 lines
- `backend/app/dspy_modules/__init__.py`
- `get_custom_lm()` max_tokens: 8192 β†’ 5120 (~300-400 lines of JSX, matching built-in template complexity)
- All 5 content variants kept (NUM_CONTENT_VARIANTS=5)
**Impact:** Scene sizes reduced from 500-800 lines β†’ 242-354 lines. Fewer truncation retries.
### Fix 4: Background Code Generation β€” DONE
**Files modified:**
- `backend/app/routers/custom_templates.py`
- `POST /{template_id}/generate-code` now returns 202 immediately, runs generation in background thread via `asyncio.get_event_loop().run_in_executor()`
- In-memory progress dict: `_codegen_progress[template_id]` with status/step/running/error (same pattern as `_pipeline_progress` in pipeline.py)
- New `GET /{template_id}/generation-status` endpoint for polling
- `frontend/src/components/CustomTemplateCreator.tsx`
- After 202 response, polls `generation-status` every 2s
- Shows progress steps, allows closing modal during generation
- `frontend/src/api/client.ts`
- Added `getCodeGenerationStatus(templateId)` API function
**Impact:** Frontend no longer blocks during code generation. User can navigate away.
### Fix 5: Animation Quality & Creative Freedom β€” DONE
**Files modified:**
- `backend/app/services/code_generator.py`
- **Deleted** `_VARIANT_SPECIALIZATIONS` dict (~150 lines of hardcoded JSX recipes that killed creativity)
- **Added** `_CONTENT_VARIANT_ROLES` β€” lightweight content-type assignments (name, content_types, role description) giving Claude full creative freedom
- **Rewrote** `_build_creative_direction()` β€” assigns content specialization but lets Claude design layout/animation freely
- **Enhanced** `GenerateSceneCode` signature:
- Animation technique reference section (word-by-word spring, image glow entrance, decorative orb) β€” examples not recipes
- Quality rules: β‰₯3 different spring configs, staggered text reveal, decorative background element, proper Img handling
- Forbidden: defaulting to simple opacity+translateY for everything
- **Strengthened** `_scene_reward()`:
- +0.10 for diverse springs (β‰₯3 unique damping/stiffness configs)
- +0.10 for staggered text animation (.map + spring + stagger timing)
- +0.10 for Img + animation (spring/interpolate near Img usage)
- +0.05 for glow/blur effects
- -0.10 for opacity+translateY only (no other animation types)
- -0.15 for scenes that completely ignore `imageUrl` (images won't render)
- **Quality debug logging**: each scene logs techniques used (staggered-text, blur, radial-glow, scale, rotate, translateX, clip-path, easing) and spring configs
**Impact:** Generated scenes now have diverse animations (staggered-text, blur, radial-glow, scale, rotate, easing), 4 unique spring configs per scene, and scores of 1.00-1.30.
### Design System Optimization β€” DONE
**Problem:** Design system generation took 43.6s (Sonnet) producing 6323 chars including spring configs. All 7 scenes copied the same springs, producing identical motion feel.
**Files modified:**
- `backend/app/services/code_generator.py`
- `GenerateDesignSystem` signature slimmed: 7 categories β†’ 3 (background treatment, card/container CSS, text treatment)
- Explicitly excludes: "Do NOT include spring configs, animation physics, decorative elements, or entrance patterns. Those are creative choices each scene makes independently."
- Output constrained to ~1500 chars
- Uses `get_theme_lm()` (Haiku) instead of `get_custom_lm()` (Sonnet) β€” structured CSS extraction doesn't need creative reasoning
- `GenerateSceneCode` prompt updated: "Follow the design_system for visual styling (colors, cards, backgrounds, text treatment). Invent your OWN unique spring physics and animation timing β€” each scene should have distinctly different motion feel."
**Impact:** Design system ~43.6s β†’ ~8-12s (Haiku + shorter output). Springs now vary per scene instead of being identical across all 7 scenes.
### Content Variant Diversity β€” DONE
**Problem:** When all blog content is the same type (e.g. noon.com: all bullets), every scene got routed to the same variant (variant 0, lists specialist). All scenes looked identical.
**Files modified:**
- `backend/app/services/remotion.py`
- Variant routing updated: specialist variant used for the **first** occurrence of each content type only
- Subsequent scenes with the same content type cycle through other variants for visual diversity
- Example: 5 bullet scenes β†’ variant 0 (specialist), 1 (cycling), 2, 3, 4 β€” all different layouts
**Impact:** Even when all content is the same type, every scene gets a different visual treatment.
### Test Results (noon.com)
| Metric | Before | After | Target |
|--------|--------|-------|--------|
| CSS chars | 0 | 0 (expected for SPA) | > 0 for traditional sites |
| Logos | 0 | 1 | > 0 |
| Theme AI time | 25s | 13.8s | < 12s |
| Codegen total | 246s | 177.7s (pending design system fix) | < 140s |
| Scene lines | 500-800 | 242-354 | 200-350 |
| Spring configs | 1 identical | 4 unique per scene | β‰₯ 3 |
| Animation techniques | opacity+translateY | staggered-text, blur, radial-glow, scale, rotate, translateX, easing | diverse |
| Frontend blocking | yes (60-250s) | 202 + polling | non-blocking |
---
## Phase V2: Content-Aware Architecture (feat/customv2) β€” DONE
### Problem
Custom templates produced repetitive, content-blind videos. Built-in templates look great because each scene gets a layout matched to its content. Custom templates were generic because:
- 5 hardcoded layout families β€” every brand got the same structural layouts
- Blind cycling β€” scenes rotated through variants regardless of content (metrics scene might get kinetic_typography)
- 16 wasted DSPy calls per video generating layoutConfig that the renderer ignored
- Images broken β€” only one layout family rendered `<Img>`, others silently ignored images
- Preview broken β€” `contentVariantIndex` written to data.json but not DB
### What Was Done
#### 1. AI-Decided Brand Scene Types (DecideBrandSceneTypes DSPy Signature)
**Removed:** `CONTENT_ARCHETYPES` (10 hardcoded archetypes), `_LAYOUT_FAMILIES`, `NUM_CONTENT_VARIANTS=5`, `_build_creative_direction()`, `_VARIANT_SPECIALIZATIONS`, `_CONTENT_VARIANT_ROLES`
**Added:** `DecideBrandSceneTypes` DSPy signature β€” one call per brand that outputs 6-8 scene types tailored to the brand.
- Example: Metal (fundraising SaaS) β†’ `brand_hook_intro, feature_spotlight, how_it_works, metrics_proof, vs_traditional, fundraising_timeline, founder_voice, cta_outro`
- Example: Al Jazeera (news) β†’ completely different set of scene types
- Each scene type has `best_for` tags mapping to content types (bullets, metrics, quote, etc.)
**Files:** `backend/app/services/code_generator.py` β€” complete rewrite of generation flow
#### 2. Content Extraction & Archetype Matching
**New file:** `backend/app/services/content_classifier.py`
- `extract_structured_content_batch()` β€” ONE cheap Haiku call classifies ALL scene narrations into contentType (metrics/bullets/quote/comparison/timeline/steps/code/plain) and extracts structured data
- `match_scenes_to_archetypes()` β€” deterministic matching: contentType β†’ best archetype based on `best_for` tags, with anti-repeat logic so consecutive scenes never get the same archetype
**Files modified:**
- `backend/app/routers/pipeline.py` β€” custom template branch now uses batch extraction instead of 16 per-scene DSPy calls
- `backend/app/services/remotion.py` β€” content-aware matching replaces blind cycling, persists `contentVariantIndex` to DB
- `frontend/src/components/VideoPreview.tsx` β€” reads `contentVariantIndex` from DB instead of `CONTENT_TYPE_TO_VARIANT`
#### 3. Alembic Migration for content_archetype_ids
**New file:** `backend/alembic/versions/phase8_content_archetype_ids.py`
- Adds `content_archetype_ids TEXT` column to `custom_templates` table
- Merges 3 divergent alembic heads into one
#### 4. Preview System Enrichment
**File:** `frontend/src/components/templatePreviews/CustomPreview.tsx` β€” complete rewrite
- Replaced simple `buildSampleData()` with archetype-aware `buildArchetypeSampleData()` returning rich structured data per content type (metrics with values, bullets, quotes, comparison, timeline, steps, code)
- Scene crossfade transitions with dot navigation
- Pre-compiles ALL scene codes on mount (eliminates per-scene "Compiling preview..." flash)
**File:** `frontend/src/components/templatePreviews/CustomPreviewLandscape.tsx`
- Delegates to `CustomPreview` when generated scene code exists
- Falls back to `FallbackSlides` component when no code
**File:** `frontend/src/components/BlogUrlForm.tsx`
- Template picker passes full template data (introCode, outroCode, contentCodes, contentArchetypeIds, logoUrls, ogImage)
- Selected template preview shows real generated scenes instead of hardcoded slides
#### 5. Image & Logo Rendering (MANDATORY in AI-generated scenes)
**Problem:** AI-generated components ignored `props.imageUrl` and `props.logoUrl` β€” pipeline delivered images correctly but components didn't render them.
**Root cause:** Not the pipeline β€” `GeneratedVideo.tsx` correctly maps `scene.images[0]` β†’ `imageUrl`, and `VideoPreview.tsx` correctly passes `imageUrl` from `sceneImageMap`. The issue was that AI-generated React code never referenced these props.
**Fix β€” Prompt (code_generator.py `GenerateSceneCode`):**
- Images & Logo section marked MANDATORY with explicit JSX patterns
- Image techniques from built-in templates: Ken Burns zoom, radial vignette reveal, clipPath slit reveal, gradient overlays (3-layer: vignette + bottom gradient + accent wash)
- Adaptive layout: `const hasImage = !!props.imageUrl` β€” with image: split layout or full-bleed overlay; without image: text expands, larger fonts, particles/gradient as visual interest
- Both modes must look intentionally designed
**Fix β€” Text animations:**
- Word-by-word / line-by-line reveals with staggered springs
- Typewriter effect with blinking cursor
- Scale-punch for key words (bouncy overshoot spring)
- Bullet stagger (slide from right, delay=20+i*10)
- Exit animations 20-30 frames before durationInFrames
**Fix β€” Scene motion:**
- Multiple spring configs with different damping/stiffness/mass
- Metric count-ups, card fly-ins, decorative corner shapes, accent line grows
- Parallax depth, ambient gradient shifts, pulsing accent glows
- Reference configs: fast={damping:22,stiffness:140,mass:1.2}, bouncy={damping:14,stiffness:220,mass:1.1}, smooth={damping:20,stiffness:70}
**Fix β€” Reward function penalties:**
- `-0.2` if code doesn't reference `logoUrl`
- `-0.2` if code doesn't reference `imageUrl`
- `-0.3` for non-monotonic `interpolate()` inputRange (catches runtime crashes like `[0,60,120,90]`)
- Combined with existing checks (overflow:hidden, hardcoded data, visible contentType/sceneIndex)
#### 6. Code Generator Architecture (current state)
```
generate_component_code(template)
β”‚
β”œβ”€ _build_brand_context() β€” raw data only (colors, fonts, patterns, brand kit)
β”‚
β”œβ”€ _decide_brand_scene_types() β€” ONE DSPy call β†’ 6-8 brand-specific scene types
β”‚
β”œβ”€ _generate_design_system() β€” ONE Haiku call β†’ concise CSS design system (<2000 chars)
β”‚
└─ asyncio.gather(*tasks) β€” ALL scenes in PARALLEL via ThreadPoolExecutor(max_workers=8)
└─ _generate_single_scene() β†’ dspy.Refine(ChainOfThought(GenerateSceneCode), reward_fn=_scene_reward)
└─ Up to 3 attempts per scene (1 initial + 2 retries if score < 0.75)
```
**Output:** `{ intro_code, outro_code, content_codes: list[str], archetype_ids: list[dict] }`
### Files Changed Summary
| File | Change |
|------|--------|
| `backend/app/services/code_generator.py` | Complete rewrite: DecideBrandSceneTypes, stripped prescriptive code, enriched animation/image prompt, reward penalties for missing image/logo |
| `backend/app/services/content_classifier.py` | NEW: batch content extraction (Haiku) + deterministic archetype matching |
| `backend/app/routers/pipeline.py` | Custom template branch uses batch extraction |
| `backend/app/services/remotion.py` | Content-aware matching, passes structuredContent to scenes |
| `backend/app/routers/custom_templates.py` | content_archetype_ids serialization, regeneration endpoint |
| `backend/app/routers/projects.py` | Scene regeneration for custom templates |
| `backend/app/models/custom_template.py` | content_archetype_ids column |
| `backend/app/database.py` | SQLite migration for content_archetype_ids |
| `backend/app/services/template_service.py` | Loads and caches archetype metadata |
| `backend/alembic/versions/phase8_content_archetype_ids.py` | PostgreSQL migration |
| `frontend/src/components/templatePreviews/CustomPreview.tsx` | Archetype-aware previews with crossfade |
| `frontend/src/components/templatePreviews/CustomPreviewLandscape.tsx` | Delegates to CustomPreview for generated code |
| `frontend/src/components/BlogUrlForm.tsx` | Template picker passes full template data |
| `frontend/src/components/VideoPreview.tsx` | Reads contentVariantIndex from DB |
| `frontend/src/components/CustomTemplateCreator.tsx` | Centered step indicator |
| `frontend/src/pages/CustomTemplates.tsx` | Passes archetype data to previews |
| `frontend/src/pages/ProjectView.tsx` | Layout label from archetype name |
| `frontend/src/api/client.ts` | content_archetype_ids types |
### Test Results
Three brands tested end-to-end (template creation β†’ scene generation β†’ video creation):
| Brand | Scene Types Decided | Content Extraction | Archetype Matching | Generation Time |
|-------|--------------------|--------------------|-------------------|-----------------|
| Metal (fundraising SaaS) | 8 types: brand_hook_intro, feature_spotlight, how_it_works, metrics_proof, vs_traditional, fundraising_timeline, founder_voice, cta_outro | All scenes classified correctly | Anti-repeat working | ~137s |
| Al Jazeera (news) | Unique news-focused types | Classified correctly | Working | ~130s |
| NestlΓ© Pakistan (FMCG) | Unique consumer brand types | Classified correctly | Working | ~135s |
All regenerated scenes score 1.00 (image/logo handling verified by reward function).
---
## Next Improvements (TODO)
| # | Improvement | Priority | Description |
|---|------------|----------|-------------|
| N2 | brandImages Pass-through in frontend preview | Low | VideoPreview.tsx doesn't pass `brandImages` to scene props (Remotion pipeline does). Add it for preview parity. |
| N7 | Per-Plan AI Limits | Low | AI_DAILY_LIMIT per-plan: Free: 5, Standard: 20, Pro: 50. |