Spaces:
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 codeintro_code(Text, nullable) β intro scene variantoutro_code(Text, nullable) β outro scene variantbrand_kit_id(Integer FK β brand_kits.id, nullable, indexed)brand_kitrelationship
backend/app/models/scene.pyβ Added:scene_type(String(20), nullable) β "intro"/"content"/"outro"; NULL = "content"
backend/app/models/user.pyβ Added:brand_kitsrelationship (cascade delete)
backend/app/models/__init__.pyβ Added BrandKit import and exportbackend/app/database.pyβ Updated:_migrate_sqlite(): added component_code, intro_code, outro_code, brand_kit_id to ct_migrations; added scene_type to scene_migrationsinit_db(): added BrandKit to model imports
backend/alembic/env.pyβ Added BrandKit importbackend/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:ScrapedThemeDatanow haslogo_urls,og_image,screenshot_urlfields- 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
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000 - Confirm no startup errors
- Check brand_kits table exists in SQLite
- Check new columns on custom_templates and scenes
- 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-codeendpoint (calls code_generator, validates, stores in DB)GET /{template_id}/codeendpoint (lightweight code-only response)_serialize_template()now includescomponent_code,intro_code,outro_codeCustomTemplateOutschema updated with code fields
frontend/src/components/templatePreviews/CustomPreview.tsxβ Added:- Optional
componentCodeprop - When
componentCodeexists, rendersRemotionPreviewPlayer(lazy-loaded) instead of carousel - Existing carousel unchanged as fallback for templates without generated code
- Optional
frontend/src/api/client.tsβ Added:component_code,intro_code,outro_codefields toCustomTemplateItemgenerateTemplateCode(templateId)API methodgetTemplateCode(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
componentCodeto CustomPreview for live Remotion previews
- Template grid passes
frontend/package.jsonβ Added:@babel/standalonedependency@types/babel__standalonedev 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
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000 - Create a custom template via existing flow (paste URL β extract β save)
- Step 3 should auto-trigger code generation with spinner
- After generation, live Remotion preview should play in the modal
- On Custom Templates page, templates with generated code show live previews
npm run buildpasses β
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 responseedit_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/UndoEditRequestPydantic schemasVARIANT_FIELD_MAPhelper dictPOST /{template_id}/edit-codeβ sends existing code + instruction to Claude, saves edit history, updates template codePOST /{template_id}/undo-editβ reverts to previous code version from history, deletes history rowGET /{template_id}/edit-historyβ returns list of edits (instruction + summary, no code) for chat reconstruction
backend/app/models/custom_template.pyβ Added:edit_historyrelationship (cascade delete-orphan)
backend/app/models/__init__.pyβ Added CodeEditHistory import and exportbackend/app/database.pyβ Added CodeEditHistory to init_db imports (SQLite auto-creates via create_all)backend/alembic/env.pyβ Added CodeEditHistory importfrontend/src/api/client.tsβ Added:CodeVarianttype ("component" | "intro" | "outro")CodeEditHistoryIteminterfaceeditTemplateCode(templateId, instruction, variant)API methodundoTemplateEdit(templateId, variant)API methodgetTemplateEditHistory(templateId)API method
frontend/src/pages/CustomTemplates.tsxβ Added:codeEditTargetstate for full-screen code editor- "Customize" button on template cards (only shown when component_code exists, purple filled)
TemplateCodeEditorrendering when codeEditTarget is setpreloadBabel()call on page mounthandleCodeEditorClose()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
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000β no errors, code_edit_history table created - Create a template with generated code (Phase 2 flow)
- On Custom Templates page, "Customize" button appears on templates with code
- Click "Customize" β full-screen editor opens with preview + chat
- Type "make the title bigger" β Claude edits code β preview updates live
- Click "Undo" β code reverts to previous version β preview updates
- Close editor β template card on grid shows updated preview
npm run buildpasses β
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 includeshas_generated_code,component_code,intro_code,outro_codein cached result_get_custom_meta()passeshas_generated_codetobuild_custom_meta
backend/app/services/custom_prompt_builder.pyβ Updated:build_custom_meta()acceptshas_generated_codeparameter- 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 workspacesrc/templates/generated/directory_wrap_generated_code()(NEW) β wraps raw AI-generated code (const SceneComponent = ...) in a proper ESM module with Remotion imports + default exportwrite_remotion_data()β addsbrandColorsandsceneTypeper 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 β
GeneratedVideovsCustomVideois selected viabuild_custom_metabased on whether the template hascomponent_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.brandColorsdirectly. - sceneType tagging β each scene gets
sceneType("intro"/"content"/"outro") from the DBscene_typefield, falling back to positional inference (first=intro, last=outro). - Code wrapping β raw AI code (
const SceneComponent = (props) => { ... }) is wrapped with Remotion imports andexport default SceneComponent. This makes the generated files proper ES modules that Vite can bundle.
Verification Steps
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000β no errors - Create a template with generated code (Phase 2 flow)
- Trigger a video render for a project using the generated template
- Check logs: should see
[REMOTION] GeneratedVideo: brandColors and sceneTypes set for N scenes - Check workspace:
src/templates/generated/SceneIntro.tsxetc. should contain the AI-generated code wrapped in proper imports - Render should produce an MP4 using the AI-generated components
- Templates without generated code should still render via CustomVideo + UniversalScene
- 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 versionversionsrelationship (cascade delete-orphan, ordered by created_at desc)
backend/app/models/__init__.pyβ Added TemplateVersion import and exportbackend/app/database.pyβ Added:- TemplateVersion to init_db imports
current_version_idto SQLite migrations for custom_templates
backend/alembic/env.pyβ Added TemplateVersion importbackend/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" versionGET /{template_id}/versionsβ lists all saved versions (newest first) with current_version_idPOST /{template_id}/versions/{version_id}/rollbackβ restores code from a previous version; snapshots current state as "Before rollback" firstCustomTemplateOutand_serialize_template()now includecurrent_version_id
frontend/src/api/client.tsβ Added:current_version_idtoCustomTemplateIteminterfaceTemplateVersionItemandTemplateVersionsResponseinterfacesregenerateTemplateCode(templateId)API methodgetTemplateVersions(templateId)API methodrollbackTemplateVersion(templateId, versionId)API method
frontend/src/pages/CustomTemplates.tsxβ Added:regeneratingIdstate for tracking which template is regeneratinghandleRegenerate()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 mounthandleRegenerate()β regenerates code from within editor, clears chat, reloads versionshandleRollback(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
- Version history state:
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
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000β no errors, template_versions table created - Create a template with generated code (Phase 2 flow) β a version is saved automatically
- On Custom Templates page, hover a template card β Regenerate button (refresh icon) appears next to Customize
- Click Regenerate β spinner shows β new code generated β preview updates β old version preserved
- Click Customize β full-screen editor opens β "Versions (N)" button in chat header
- Click "Versions (N)" β version history panel opens with all versions listed
- Click "Restore" on an older version β code rolls back β preview updates
- 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 UniversalScenetypes.ts(3KB) β old CustomTheme, SceneLayoutConfig typesutils/styleEngine.ts(11KB) β theme style β CSS decoration mapperutils/normalize.ts(7KB) β layout config normalization helpers
frontend/src/components/remotion/custom/β entire directory removed:UniversalScene.tsxβ frontend copy of server UniversalSceneCustomVideoComposition.tsxβ frontend copy of CustomVideotypes.tsβ frontend copy of custom typesutils/β frontend copy of utils
Files modified:
remotion-video/src/Root.tsxβ Removed:CustomVideo+calculateCustomMetadataimport<Composition id="CustomVideo" .../>registration- Only
GeneratedVideocomposition remains for custom templates
backend/app/services/remotion.pyβ Updated:- Removed
has_generated_codeconditional inwrite_remotion_data()β brandColors and sceneType are now always set for custom templates - Replaced
print()calls withlogger.info()/logger.warning()
- Removed
backend/app/services/custom_prompt_builder.pyβ Updated:- Removed
has_generated_codeparameter frombuild_custom_meta() composition_idis now always"GeneratedVideo"β no fallback to"CustomVideo"
- Removed
backend/app/services/template_service.pyβ Updated:- Removed
has_generated_codekwarg frombuild_custom_meta()call
- Removed
frontend/src/components/remotion/remotionAdapters.tsxβ Removed:UniversalSceneimport from@remotion-video/templates/custom/UniversalSceneCustomTheme,SceneLayoutConfigtype imports from@remotion-video/templates/custom/typesRemotionCustomSceneInputinterfaceRemotionCustomVideoCompositionPropsinterfaceRemotionCustomVideoCompositioncomponent (entire ~100 line composition)
frontend/src/components/remotion/templateConfig.tsxβ Removed:CustomVideoCompositionimportCUSTOM_ARRANGEMENTSsetcustomentry fromTEMPLATE_REGISTRYcustom_routing ingetTemplateConfig()(no longer routes to old system)RemotionCustomVideoCompositionfrom the remotion source override chain
frontend/src/types/remotion-video.d.tsβ Removed:@remotion-video/templates/custom/UniversalScenemodule declaration@remotion-video/templates/custom/typesmodule 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
RemotionPreviewPlayerwhencomponentCodeexists - Shows simple placeholder when
componentCodeis 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
GeneratedVideocomposition exclusively. No fallback toCustomVideo+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 returnscomposition_id: "GeneratedVideo".write_remotion_data()always writes brandColors + sceneType for custom templates.
Verification Steps
cd remotion-video && npx tsc --noEmitβ passes βcd frontend && npx tsc --noEmitβ passes (only pre-existing @babel/standalone type issue from missing npm install) β- Backend imports:
python -c "from app.services.remotion import provision_workspace; ..."β passes β - Custom templates page shows live Remotion previews (no carousel fallback)
- 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_countsin-memory dict:user_id β (date_string, count)AI_DAILY_LIMIT = 5constant_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(), andedit_code()endpoints
Design Decisions
- In-memory counter β matches existing
_render_progresspattern. 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_LIMITcan 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_REGEXlist β 18 compiled regex patterns with word-boundary matching foreval,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
- Added
Design Decisions
- Regex word boundaries β
\beval\bcatchesevalbut notevaluation, 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]todict[str, (data, timestamp)] - Added
_CACHE_TTL = 30.0seconds constant _load_custom_template_data()now checkstime.monotonic()age on cache hit β refetches from DB if expired- Explicit invalidation (
invalidate_custom_template_cache()) still works for same-process freshness
- Cache type changed from
backend/app/services/remotion.pyβ Updated:provision_workspace()callsinvalidate_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 projectprovision_workspace()body wrapped inwith _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), restrictedstyle-src,img-src,media-src,font-src
- CSP meta tag:
frontend/src/components/RemotionPreviewPlayer.tsxβ Added:PlayerErrorBoundaryclass 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-evalrequired β @babel/standalone needsevalfor 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 iftpl.current_version_idexists 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:ExtractThemeResponsenow includeslogo_urls,og_image,screenshot_urlfrom ScrapedThemeDataCreateCustomTemplateRequestnow accepts optionallogo_urls,og_image,screenshot_urlextract_theme()passes scraped logo/image data through to the responsecreate_custom_template()now creates aBrandKitrow from theme data (colors, fonts, design_language, logos, images) and links it viabrand_kit_id
frontend/src/api/client.tsβ Updated:ExtractThemeResponseinterface includeslogo_urls,og_image,screenshot_urlcreateCustomTemplate()accepts optionallogo_urls,og_image,screenshot_url
frontend/src/components/CustomTemplateCreator.tsxβ Updated:- Stores scraped
logo_urls,og_image,screenshot_urlfrom extract response in state - Passes them to
createCustomTemplate()call so BrandKit is populated
- Stores scraped
Design Decisions
- BrandKit auto-created on template creation β every new template gets a linked BrandKit, populating data that
code_generator.pyalready 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-logoendpoint β validates file type (PNG/JPEG/WebP/SVG) and size (2MB), auto-creates BrandKit if missing, uploads to R2, updatesbk.logosJSON with primary logo entry- FastAPI
UploadFile,Fileimports
backend/app/services/template_service.pyβ Updated:_load_custom_template_data()now includesbrand_kitdict (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, setsdata["brandLogo"]for GeneratedVideo access viastaticFile()
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.pngforstaticFile()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: removedvalid_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:ScrapedThemeDatahas newbranding: dict | Nonefieldscrape_for_theme()triesformats=["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 onThemeExtractor- 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 whenscraped.brandingis 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, runsnpx remotion still GeneratedVideoat 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 intpl.preview_image_urlgenerate_code()andregenerate_code()now acceptBackgroundTasksand kick off thumbnail render after code gen_serialize_template()now includespreview_image_url
frontend/src/api/client.tsβ Updated:CustomTemplateItemincludespreview_image_url: string | null
frontend/src/components/templatePreviews/CustomPreview.tsxβ Rewritten:- If
previewImageUrlexists 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
- If
frontend/src/pages/CustomTemplates.tsxβ Updated:- Passes
previewImageUrl={tpl.preview_image_url}to CustomPreview in template grid
- Passes
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β Addedcontent_codes(Text, nullable) β JSON array of content variant code stringsbackend/app/models/template_version.pyβ Addedcontent_codes(Text, nullable) β version snapshots include all content variantsbackend/app/database.pyβ Addedcontent_codesto SQLite migrations for both custom_templates and template_versionsbackend/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 singlecomponent_code max_tokensincreased from 8000 to 16000 for multiple variants
- New SYSTEM_PROMPT: asks Claude to generate 5-8 scene variants using
backend/app/routers/custom_templates.pyβ Updated:EditCodeRequest.variantnow acceptscontent_Nformat (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 indexgenerate_code()andregenerate_code()storecontent_codesas JSON_save_version()snapshotscontent_codes- Rollback restores
content_codes _serialize_template()includescontent_codes(parsed from JSON)
backend/app/services/template_service.pyβ Updated:_load_custom_template_data()parses and cachescontent_codeshas_generated_codechecks bothcomponent_codeandcontent_codes
backend/app/services/remotion.pyβ Updated:_write_generated_scene_files()now writes SceneContent0.tsx, SceneContent1.tsx, ..., SceneContentN.tsx- Generates
contentRegistry.tsβ exportsCONTENT_VARIANTSarray with all content components - Generates SceneContent.tsx (re-exports Content0 for backward compat)
write_remotion_data()addscontentVariantIndexandcontentVariantCountto 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 componentsremotion-video/src/templates/generated/SceneContent0.tsxβ Placeholder; re-exports SceneContent
Files rewritten:
remotion-video/src/templates/generated/GeneratedVideo.tsxβ Now imports fromcontentRegistry.tsinstead of single ContentScene.getSceneComponent()picks variant bycontentVariantIndexfrom data.json.remotion-video/src/templates/generated/types.tsβ AddedcontentVariantIndexandcontentVariantCountfieldsfrontend/src/components/TemplateCodeEditor.tsxβ Dynamic variant tabs built fromtemplate.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βCodeVarianttype changed from"component" | "intro" | "outro"to"intro" | "outro" | \content_${number}`.CustomTemplateItemincludescontent_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_codeis always set to the first content variant. Old code that readscomponent_codestill 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
- Start backend:
cd backend && uvicorn app.main:app --reload --port 8000β no errors, content_codes column auto-created - Create a template and generate code β should see "Generated N scene variants (1 intro + X content + 1 outro)" in backend logs
- On Custom Templates page, click "Customize" β editor should show tabs: "Intro | Scene 1 | Scene 2 | ... | Outro"
- Click each tab β different preview for each scene variant
- Edit a specific variant via chat β only that variant changes
- Trigger a video render β check workspace: should have SceneContent0.tsx through SceneContentN.tsx
- Rendered video should have visually different layouts for each content scene
- 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_CHARSreduced: 40,000 β 15,000 (AI only needs enough HTML for colors/fonts/layout patterns)_MAX_MARKDOWN_CHARSreduced: 5,000 β 2,000
backend/app/dspy_modules/__init__.pyget_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__.pyget_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.pyPOST /{template_id}/generate-codenow returns 202 immediately, runs generation in background thread viaasyncio.get_event_loop().run_in_executor()- In-memory progress dict:
_codegen_progress[template_id]with status/step/running/error (same pattern as_pipeline_progressin pipeline.py) - New
GET /{template_id}/generation-statusendpoint for polling
frontend/src/components/CustomTemplateCreator.tsx- After 202 response, polls
generation-statusevery 2s - Shows progress steps, allows closing modal during generation
- After 202 response, polls
frontend/src/api/client.ts- Added
getCodeGenerationStatus(templateId)API function
- Added
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_SPECIALIZATIONSdict (~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
GenerateSceneCodesignature:- 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
- Deleted
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.pyGenerateDesignSystemsignature 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 ofget_custom_lm()(Sonnet) β structured CSS extraction doesn't need creative reasoning GenerateSceneCodeprompt 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 β
contentVariantIndexwritten 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_fortags 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 datamatch_scenes_to_archetypes()β deterministic matching: contentType β best archetype based onbest_fortags, 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 callsbackend/app/services/remotion.pyβ content-aware matching replaces blind cycling, persistscontentVariantIndexto DBfrontend/src/components/VideoPreview.tsxβ readscontentVariantIndexfrom DB instead ofCONTENT_TYPE_TO_VARIANT
3. Alembic Migration for content_archetype_ids
New file: backend/alembic/versions/phase8_content_archetype_ids.py
- Adds
content_archetype_ids TEXTcolumn tocustom_templatestable - 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-awarebuildArchetypeSampleData()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
CustomPreviewwhen generated scene code exists - Falls back to
FallbackSlidescomponent 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.2if code doesn't referencelogoUrl-0.2if code doesn't referenceimageUrl-0.3for non-monotonicinterpolate()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. |