--- phase: 44-video-presentations plan: 01 type: execute wave: 1 depends_on: [] files_modified: - packages/content-renderer/package.json - packages/content-renderer/tsconfig.json - packages/content-renderer/src/index.ts - packages/content-renderer/src/Root.tsx - packages/content-renderer/src/compositions/index.ts - packages/content-renderer/src/compositions/PitchDeck.tsx - packages/content-renderer/src/compositions/DemoVideo.tsx - packages/content-renderer/src/components/SlideFrame.tsx - packages/content-renderer/src/components/TitleSlide.tsx - packages/shared/src/constants.ts - server/src/services/renderers/types.ts - server/src/services/content-job-runner.ts autonomous: true requirements: - PRES-01 - PRES-02 - PRES-04 must_haves: truths: - "packages/content-renderer is a valid pnpm workspace package with remotion deps installed" - "content_job.progress exists in LIVE_EVENT_TYPES and compiles" - "PresentationBundle type is exported from renderer types" - "content-job-runner dispatches presentation jobs to the new renderer" - "Remotion Root registers PitchDeck and DemoVideo compositions" artifacts: - path: "packages/content-renderer/package.json" provides: "Workspace package with remotion, @remotion/bundler, @remotion/renderer deps" contains: "@remotion/renderer" - path: "packages/content-renderer/src/Root.tsx" provides: "registerRoot entry for Remotion bundler" contains: "registerRoot" - path: "packages/content-renderer/src/compositions/PitchDeck.tsx" provides: "Pitch deck composition using Series" contains: "Series.Sequence" - path: "packages/content-renderer/src/compositions/DemoVideo.tsx" provides: "Demo video composition" contains: "Series.Sequence" - path: "packages/content-renderer/src/index.ts" provides: "getBundlePath and renderPresentationComposition exports" exports: ["getBundlePath", "renderPresentationComposition"] - path: "packages/shared/src/constants.ts" provides: "content_job.progress event type" contains: "content_job.progress" - path: "server/src/services/renderers/types.ts" provides: "PresentationBundle in ContentBundle union" contains: "PresentationBundle" - path: "server/src/services/content-job-runner.ts" provides: "presentation case in renderContent switch" contains: 'case "presentation"' key_links: - from: "packages/content-renderer/src/index.ts" to: "packages/content-renderer/src/Root.tsx" via: "bundle() entryPoint" pattern: "Root\\.tsx" - from: "server/src/services/content-job-runner.ts" to: "server/src/services/renderers/presentation-renderer.ts" via: "dynamic import" pattern: "presentation-renderer" --- Create the Remotion workspace package (packages/content-renderer/) with compositions for pitch decks and demo videos, extend shared constants with content_job.progress event type, add PresentationBundle to renderer types, and wire content-job-runner to dispatch presentation jobs. Purpose: Establish the isolated Remotion webpack context and all type contracts before the server renderer and UI can be built. Output: A compilable workspace package with Remotion compositions, updated shared constants, updated renderer types, updated job runner. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/44-video-presentations/44-RESEARCH.md @packages/shared/src/constants.ts @server/src/services/renderers/types.ts @server/src/services/content-job-runner.ts @server/src/services/renderers/diagram-renderer.ts (resolveBrowserPath pattern) From packages/shared/src/constants.ts: ```typescript export const LIVE_EVENT_TYPES = [ "content_job.queued", "content_job.running", "content_job.done", "content_job.failed", ] as const; export type LiveEventType = (typeof LIVE_EVENT_TYPES)[number]; ``` From server/src/services/renderers/types.ts: ```typescript export interface RenderResult { filename: string; contentType: string; buffer: Buffer; } export type ContentBundle = DiagramBundle | IconSetBundle | ... | BrandKitBundle; ``` From server/src/services/content-job-runner.ts: ```typescript export async function renderContent(jobType: string, input: Record): Promise ``` From server/src/services/renderers/diagram-renderer.ts: ```typescript export function resolveBrowserPath(): string // Resolves Playwright Chromium path ``` Task 1: Create packages/content-renderer workspace package with Remotion compositions packages/content-renderer/package.json, packages/content-renderer/tsconfig.json, packages/content-renderer/src/index.ts, packages/content-renderer/src/Root.tsx, packages/content-renderer/src/compositions/index.ts, packages/content-renderer/src/compositions/PitchDeck.tsx, packages/content-renderer/src/compositions/DemoVideo.tsx, packages/content-renderer/src/components/SlideFrame.tsx, packages/content-renderer/src/components/TitleSlide.tsx - packages/shared/package.json (package.json pattern for workspace packages) - server/src/services/renderers/diagram-renderer.ts (resolveBrowserPath export — line 90-107) - pnpm-workspace.yaml (confirm packages/* is included) Create `packages/content-renderer/` as a NEW workspace package. This package isolates Remotion's webpack/rspack from the server's tsc/Vite context. **package.json:** name `@paperclipai/content-renderer`, NO `"type": "module"` (omit the field entirely — Remotion's rspack bundler needs CommonJS resolution internally). Dependencies: `remotion@4.0.445`, `@remotion/bundler@4.0.445`, `@remotion/renderer@4.0.445`, `react`, `react-dom`. The `react` and `react-dom` should be peerDependencies listed as deps for the workspace to resolve. Add an `"exports"` field with `"."` pointing to `"./src/index.ts"` and `"./compositions"` pointing to `"./src/compositions/index.ts"` (the UI-safe sub-export that does NOT import bundler/renderer). **tsconfig.json:** Extend `../../tsconfig.base.json`. Override `"jsx": "react-jsx"`, `"module": "CommonJS"`, `"moduleResolution": "Node"`, `"esModuleInterop": true`. rootDir `src`, outDir `dist`. Include `src`. **src/Root.tsx:** Import `registerRoot` from `remotion`. Import `Composition` from `remotion`. Import `PitchDeck` from `./compositions/PitchDeck` and `DemoVideo` from `./compositions/DemoVideo`. Define `FRAMES_PER_SLIDE = 90` (3 seconds at 30fps). Register a `RemotionRoot` component containing two `` elements: - id `"PitchDeck"`, component `PitchDeck`, 1920x1080, 30fps, `durationInFrames: FRAMES_PER_SLIDE * 10` (default), `defaultProps: { slides: [], companyName: "" }` - id `"DemoVideo"`, component `DemoVideo`, 1920x1080, 30fps, `durationInFrames: FRAMES_PER_SLIDE * 8` (default), `defaultProps: { slides: [], title: "" }` Call `registerRoot(RemotionRoot)` at module level. **src/compositions/PitchDeck.tsx:** Export `Slide` interface: `{ title: string; body: string; accent: string }`. Export `PitchDeckProps`: `{ slides: Slide[]; companyName: string }`. Component renders an `` with dark background (#0f0f0f), wrapping a ``. Each slide is a `` with `durationInFrames={90}` containing a ``. First slide should be a `` (if `companyName` is provided and slides.length > 0, render TitleSlide for index 0). Use `calculateMetadata` static function to compute total durationInFrames as `slides.length * 90` (exported on the component for Remotion to pick up). **src/compositions/DemoVideo.tsx:** Export `DemoSlide` interface: `{ title: string; content: string; narration: string }`. Export `DemoVideoProps`: `{ slides: DemoSlide[]; title: string }`. Similar Series-based layout but with a different visual style — use a gradient background (#1a1a2e to #16213e), larger title text, and a narration text overlay at the bottom of each slide. Use `calculateMetadata` for dynamic duration. **src/compositions/index.ts:** Re-export `PitchDeck`, `PitchDeckProps`, `Slide` from `./PitchDeck` and `DemoVideo`, `DemoVideoProps`, `DemoSlide` from `./DemoVideo`. This is the UI-safe entrypoint — does NOT import from `@remotion/bundler` or `@remotion/renderer`. **src/components/SlideFrame.tsx:** Props: `{ slide: Slide; slideIndex: number; total: number }`. Renders a full-frame slide with the slide title in bold (white, 48px), body text below (light gray, 28px), a colored accent bar at the bottom using `slide.accent`, and a slide counter `slideIndex+1 / total` in the bottom-right corner. Use `useCurrentFrame` and `interpolate` from `remotion` for a fade-in animation on enter (first 15 frames). **src/components/TitleSlide.tsx:** Props: `{ companyName: string; subtitle?: string }`. Renders a centered title card with company name in large text (72px, white), optional subtitle below. Use `useCurrentFrame` + `spring` from `remotion` for a scale-in animation. **src/index.ts:** This is the server-side entrypoint. - Export `getBundlePath()`: Uses `bundle()` from `@remotion/bundler` with entryPoint pointing to `path.resolve(__dirname, "Root.tsx")`. Caches the result in a module-level `let cachedBundlePath: string | null = null`. Returns the cached path on subsequent calls. - Export `renderPresentationComposition(opts)`: Takes `{ serveUrl: string; input: Record; onProgress: (progress: number) => void; browserExecutable?: string }`. Uses `selectComposition()` from `@remotion/renderer` to select `input.videoType === "demo" ? "DemoVideo" : "PitchDeck"`. Then calls `renderMedia()` with `codec: "h264"`, `concurrency: 1` (to avoid competing with LLM inference), the `onProgress` callback mapping `({ progress }) => onProgress(Math.round(progress * 100))`, and `browserExecutable` if provided. Returns a `{ buffer: Buffer; durationInFrames: number; fps: number; inputProps: Record }` object. - Re-export composition types from `./compositions/index` for convenience. After creating all files, run `cd /opt/nexus && pnpm install` to link the workspace package. Verify with `pnpm ls --filter @paperclipai/content-renderer`. cd /opt/nexus && pnpm ls --filter @paperclipai/content-renderer remotion @remotion/bundler @remotion/renderer && node -e "const p = require('./packages/content-renderer/package.json'); if (!p.name.includes('content-renderer')) throw new Error('bad name')" - grep -q "content-renderer" packages/content-renderer/package.json - grep -q "remotion" packages/content-renderer/package.json - grep -q "@remotion/bundler" packages/content-renderer/package.json - grep -q "@remotion/renderer" packages/content-renderer/package.json - grep -q "registerRoot" packages/content-renderer/src/Root.tsx - grep -q "Series.Sequence" packages/content-renderer/src/compositions/PitchDeck.tsx - grep -q "Series.Sequence" packages/content-renderer/src/compositions/DemoVideo.tsx - grep -q "getBundlePath" packages/content-renderer/src/index.ts - grep -q "renderPresentationComposition" packages/content-renderer/src/index.ts - grep -q "concurrency" packages/content-renderer/src/index.ts packages/content-renderer/ is a linked workspace package with Remotion deps, Root.tsx registers PitchDeck and DemoVideo compositions, index.ts exports getBundlePath and renderPresentationComposition, compositions sub-export is UI-safe Task 2: Extend shared constants, renderer types, and content-job-runner for presentations packages/shared/src/constants.ts, server/src/services/renderers/types.ts, server/src/services/content-job-runner.ts - packages/shared/src/constants.ts (full file — find exact LIVE_EVENT_TYPES array) - server/src/services/renderers/types.ts (full file — find ContentBundle union) - server/src/services/content-job-runner.ts (full file — find renderContent switch) **packages/shared/src/constants.ts:** Add `"content_job.progress"` to the `LIVE_EVENT_TYPES` array, placed between `"content_job.running"` and `"content_job.done"`. This unblocks TypeScript compilation for any code that publishes progress events (PRES-04). **server/src/services/renderers/types.ts:** Add `PresentationBundle` interface: ```typescript export interface PresentationBundle { type: "presentation-bundle"; presentationType: "pitch-deck" | "demo-video"; title: string; slideCount: number; durationInFrames: number; fps: number; mp4Base64: string; inputProps: Record; } ``` Add `PresentationBundle` to the `ContentBundle` union type. **server/src/services/content-job-runner.ts:** Add a `"presentation"` case to the `renderContent()` switch statement: ```typescript case "presentation": { const { renderPresentation } = await import("./renderers/presentation-renderer.js"); return renderPresentation(input, job.companyId, job.id); } ``` IMPORTANT: The `renderPresentation` function signature differs from other renderers — it takes `(input, companyId, jobId)` because it needs companyId and jobId to publish progress SSE events. Update the `renderContent` function signature to accept `companyId` and `jobId` as optional parameters: ```typescript export async function renderContent( jobType: string, input: Record, companyId?: string, jobId?: string, ): Promise ``` Pass these through in the `"presentation"` case. Update the `runJob` call site to pass `job.companyId` and `job.id` to `renderContent`. cd /opt/nexus && pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit 2>&1 | head -5 && grep -c "content_job.progress" packages/shared/src/constants.ts - grep -q "content_job.progress" packages/shared/src/constants.ts - grep -q "PresentationBundle" server/src/services/renderers/types.ts - grep -q "presentation-bundle" server/src/services/renderers/types.ts - grep -q "mp4Base64" server/src/services/renderers/types.ts - grep -q 'case "presentation"' server/src/services/content-job-runner.ts - grep -q "presentation-renderer" server/src/services/content-job-runner.ts content_job.progress is a valid LiveEventType, PresentationBundle is in the ContentBundle union, content-job-runner dispatches "presentation" jobs to presentation-renderer - `pnpm ls --filter @paperclipai/content-renderer remotion` confirms Remotion deps installed - `grep -q "content_job.progress" packages/shared/src/constants.ts` confirms progress event type - `grep -q "PresentationBundle" server/src/services/renderers/types.ts` confirms bundle type - `grep -q 'case "presentation"' server/src/services/content-job-runner.ts` confirms runner wiring - `grep -q "registerRoot" packages/content-renderer/src/Root.tsx` confirms Remotion entry - packages/content-renderer/ is a linked workspace package with remotion, @remotion/bundler, @remotion/renderer - Root.tsx registers PitchDeck and DemoVideo compositions - compositions/index.ts is a UI-safe export (no bundler/renderer imports) - index.ts exports getBundlePath (cached) and renderPresentationComposition (concurrency:1) - content_job.progress is in LIVE_EVENT_TYPES - PresentationBundle type exists with mp4Base64, inputProps, presentationType - content-job-runner has "presentation" case pointing to presentation-renderer After completion, create `.planning/phases/44-video-presentations/44-01-SUMMARY.md`