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

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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)

<interfaces>
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<string, unknown>): Promise<RenderResult>
```

From server/src/services/renderers/diagram-renderer.ts:
```typescript
export function resolveBrowserPath(): string  // Resolves Playwright Chromium path
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create packages/content-renderer workspace package with Remotion compositions</name>
  <files>
    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
  </files>
  <read_first>
    - 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)
  </read_first>
  <action>
    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 `<Composition>` 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 `<AbsoluteFill>` with dark background (#0f0f0f), wrapping a `<Series>`. Each slide is a `<Series.Sequence>` with `durationInFrames={90}` containing a `<SlideFrame>`. First slide should be a `<TitleSlide>` (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<string, unknown>; 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<string, unknown> }` 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`.
  </action>
  <verify>
    <automated>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')"</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>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</done>
</task>

<task type="auto">
  <name>Task 2: Extend shared constants, renderer types, and content-job-runner for presentations</name>
  <files>
    packages/shared/src/constants.ts,
    server/src/services/renderers/types.ts,
    server/src/services/content-job-runner.ts
  </files>
  <read_first>
    - 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)
  </read_first>
  <action>
    **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<string, unknown>;
    }
    ```
    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<string, unknown>,
      companyId?: string,
      jobId?: string,
    ): Promise<RenderResult>
    ```
    Pass these through in the `"presentation"` case. Update the `runJob` call site to pass `job.companyId` and `job.id` to `renderContent`.
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>content_job.progress is a valid LiveEventType, PresentationBundle is in the ContentBundle union, content-job-runner dispatches "presentation" jobs to presentation-renderer</done>
</task>

</tasks>

<verification>
- `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
</verification>

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

<output>
After completion, create `.planning/phases/44-video-presentations/44-01-SUMMARY.md`
</output>
