---
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