Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| # 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. | | |