---
phase: 44-video-presentations
plan: 02
type: execute
wave: 2
depends_on: ["44-01"]
files_modified:
  - server/src/services/renderers/presentation-renderer.ts
  - server/src/services/content-job-runner.ts
autonomous: true
requirements:
  - PRES-01
  - PRES-02
  - PRES-03
  - PRES-04

must_haves:
  truths:
    - "LLM generates structured slide JSON from a user prompt for both pitch-deck and demo-video types"
    - "renderPresentation calls getBundlePath, selectComposition, and renderMedia to produce an MP4 buffer"
    - "onProgress callback publishes content_job.progress SSE events with 0-100 percentage"
    - "Remotion concurrency is capped at 1 to avoid starving LLM inference"
    - "browserExecutable reuses existing Playwright Chromium binary"
    - "Resulting MP4 buffer is wrapped in a PresentationBundle JSON with inputProps for Player replay"
  artifacts:
    - path: "server/src/services/renderers/presentation-renderer.ts"
      provides: "renderPresentation function — LLM prompt + Remotion render + SSE progress"
      exports: ["renderPresentation"]
      min_lines: 80
  key_links:
    - from: "server/src/services/renderers/presentation-renderer.ts"
      to: "packages/content-renderer/src/index.ts"
      via: "dynamic import @paperclipai/content-renderer"
      pattern: "content-renderer"
    - from: "server/src/services/renderers/presentation-renderer.ts"
      to: "server/src/services/live-events.ts"
      via: "publishLiveEvent content_job.progress"
      pattern: "content_job\\.progress"
    - from: "server/src/services/renderers/presentation-renderer.ts"
      to: "server/src/services/puter-inference.ts"
      via: "puterChatComplete for slide JSON generation"
      pattern: "puterChatComplete"
    - from: "server/src/services/renderers/presentation-renderer.ts"
      to: "server/src/services/renderers/diagram-renderer.ts"
      via: "resolveBrowserPath import"
      pattern: "resolveBrowserPath"
---

<objective>
Create the presentation renderer that generates slide JSON via LLM and renders it to MP4 via Remotion, publishing SSE progress events throughout the render.

Purpose: This is the core rendering pipeline for PRES-01 through PRES-04 — it connects the LLM (slide content generation), Remotion (video rendering), and SSE (progress reporting).
Output: server/src/services/renderers/presentation-renderer.ts
</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
@.planning/phases/44-video-presentations/44-01-SUMMARY.md
@server/src/services/renderers/diagram-renderer.ts (resolveBrowserPath pattern)
@server/src/services/renderers/brand-renderer.ts (LLM prompt pattern)
@server/src/services/puter-inference.ts (puterChatComplete interface)
@server/src/services/live-events.ts (publishLiveEvent interface)

<interfaces>
From packages/content-renderer/src/index.ts (created in Plan 01):
```typescript
export async function getBundlePath(): Promise<string>;
export async function renderPresentationComposition(opts: {
  serveUrl: string;
  input: Record<string, unknown>;
  onProgress: (progress: number) => void;
  browserExecutable?: string;
}): Promise<{ buffer: Buffer; durationInFrames: number; fps: number; inputProps: Record<string, unknown> }>;
```

From server/src/services/renderers/types.ts (updated in Plan 01):
```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>;
}
export interface RenderResult {
  filename: string;
  contentType: string;
  buffer: Buffer;
}
```

From server/src/services/puter-inference.ts:
```typescript
export async function puterChatComplete(messages: ChatMessage[], model?: string): Promise<string>;
```

From server/src/services/live-events.ts:
```typescript
export function publishLiveEvent(input: { companyId: string; type: LiveEventType; payload?: Record<string, unknown> }): LiveEvent;
```

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

<tasks>

<task type="auto">
  <name>Task 1: Create presentation-renderer with LLM slide generation and Remotion render pipeline</name>
  <files>server/src/services/renderers/presentation-renderer.ts</files>
  <read_first>
    - server/src/services/renderers/brand-renderer.ts (full — LLM system prompt + JSON parse pattern)
    - server/src/services/renderers/wallpaper-renderer.ts (first 60 lines — simpler LLM prompt pattern)
    - server/src/services/renderers/diagram-renderer.ts (lines 90-107 — resolveBrowserPath)
    - server/src/services/puter-inference.ts (first 30 lines — puterChatComplete signature)
    - packages/content-renderer/src/compositions/PitchDeck.tsx (Slide interface)
    - packages/content-renderer/src/compositions/DemoVideo.tsx (DemoSlide interface)
  </read_first>
  <action>
    Create `server/src/services/renderers/presentation-renderer.ts`.

    **Exports:** `renderPresentation(input: Record<string, unknown>, companyId: string, jobId: string): Promise<RenderResult>`

    **Flow:**
    1. Extract `input.prompt` (string), `input.videoType` ("pitch-deck" | "demo-video", default "pitch-deck"), `input.title` (optional string).
    2. Call `puterChatComplete` with a system prompt that instructs the LLM to generate slide content as a JSON array. Use two different system prompts depending on `videoType`:

       For "pitch-deck": System prompt asks the LLM to generate a JSON array of `Slide` objects: `[{ "title": "...", "body": "...", "accent": "#hex" }]`. The prompt should request 6-10 slides covering: title slide, problem, solution, market, business model, traction, team, ask/closing. Each slide should have a distinct accent color.

       For "demo-video": System prompt asks the LLM to generate a JSON array of `DemoSlide` objects: `[{ "title": "...", "content": "...", "narration": "..." }]`. Request 4-8 slides covering: intro, key features, demo walkthrough, conclusion. Each slide should have a narration script.

       Use a helper `stripMarkdownFences(text: string): string` (define locally, same pattern as pdf-renderer) to clean LLM output before JSON.parse.

    3. Parse LLM response as JSON. Wrap in try/catch — if parse fails, throw with a clear error including the raw response.

    4. Build `inputProps` for Remotion: For pitch-deck: `{ slides, companyName: input.title || "Presentation" }`. For demo-video: `{ slides, title: input.title || "Demo" }`.

    5. Call the content-renderer package via dynamic import:
       ```typescript
       const { getBundlePath, renderPresentationComposition } = await import("@paperclipai/content-renderer");
       ```

    6. Resolve browser path: import `resolveBrowserPath` from `./diagram-renderer.js`. Wrap in try/catch — if Playwright Chromium not found, pass `undefined` (Remotion will auto-download).

    7. Call `renderPresentationComposition({ serveUrl: await getBundlePath(), input: inputProps, onProgress, browserExecutable })` where `onProgress` publishes:
       ```typescript
       publishLiveEvent({
         companyId,
         type: "content_job.progress",
         payload: { jobId, progress },
       });
       ```

    8. Build `PresentationBundle` from the result:
       ```typescript
       const bundle: PresentationBundle = {
         type: "presentation-bundle",
         presentationType: videoType === "demo" ? "demo-video" : "pitch-deck",
         title: input.title as string || "Presentation",
         slideCount: slides.length,
         durationInFrames: result.durationInFrames,
         fps: result.fps,
         mp4Base64: result.buffer.toString("base64"),
         inputProps,
       };
       ```

    9. Return `RenderResult`: `{ filename: title.mp4, contentType: "application/json", buffer: Buffer.from(JSON.stringify(bundle)) }`. Use `application/json` as contentType because the bundle contains both the MP4 (base64) and metadata — same pattern as brand-renderer returns JSON bundles.
  </action>
  <verify>
    <automated>cd /opt/nexus && grep -c "renderPresentation" server/src/services/renderers/presentation-renderer.ts && grep -c "publishLiveEvent" server/src/services/renderers/presentation-renderer.ts && grep -c "content_job.progress" server/src/services/renderers/presentation-renderer.ts</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "export async function renderPresentation" server/src/services/renderers/presentation-renderer.ts
    - grep -q "puterChatComplete" server/src/services/renderers/presentation-renderer.ts
    - grep -q "content_job.progress" server/src/services/renderers/presentation-renderer.ts
    - grep -q "publishLiveEvent" server/src/services/renderers/presentation-renderer.ts
    - grep -q "getBundlePath" server/src/services/renderers/presentation-renderer.ts
    - grep -q "renderPresentationComposition" server/src/services/renderers/presentation-renderer.ts
    - grep -q "resolveBrowserPath" server/src/services/renderers/presentation-renderer.ts
    - grep -q "concurrency" server/src/services/renderers/presentation-renderer.ts || grep -q "concurrency" packages/content-renderer/src/index.ts
    - grep -q "stripMarkdownFences" server/src/services/renderers/presentation-renderer.ts
    - grep -q "pitch-deck" server/src/services/renderers/presentation-renderer.ts
    - grep -q "demo-video" server/src/services/renderers/presentation-renderer.ts
  </acceptance_criteria>
  <done>presentation-renderer.ts generates slide JSON via LLM, renders via Remotion with concurrency:1, publishes SSE progress events, reuses Playwright browser, returns PresentationBundle with mp4Base64 and inputProps</done>
</task>

<task type="auto">
  <name>Task 2: Verify server tsc compilation with presentation renderer wired</name>
  <files>server/src/services/content-job-runner.ts</files>
  <read_first>
    - server/src/services/content-job-runner.ts (verify the "presentation" case was added in Plan 01)
  </read_first>
  <action>
    Verify that the full server compiles with the new presentation renderer. Run `pnpm --filter @paperclipai/server exec -- npx tsc --noEmit`. If there are TypeScript errors:
    - If errors relate to the dynamic import of `@paperclipai/content-renderer`, ensure the workspace package is properly linked and its types are resolvable. May need to add `@paperclipai/content-renderer` as a dependency in `server/package.json` using `pnpm --filter @paperclipai/server add @paperclipai/content-renderer@workspace:*`.
    - If errors relate to `content_job.progress` not being in `LiveEventType`, verify Plan 01 Task 2 was completed (check `packages/shared/src/constants.ts`).
    - Fix any type mismatches between the renderer return type and `RenderResult`.

    Also run `pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit` to verify shared package compiles.

    Do NOT attempt to run the actual Remotion render (that requires the full bundle pipeline) — just verify TypeScript compilation.
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit 2>&1 | tail -3</automated>
  </verify>
  <acceptance_criteria>
    - pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit exits 0 or shows no errors
  </acceptance_criteria>
  <done>Server and shared packages compile without TypeScript errors, presentation renderer is type-correct</done>
</task>

</tasks>

<verification>
- `grep -q "renderPresentation" server/src/services/renderers/presentation-renderer.ts` confirms renderer exists
- `grep -q "content_job.progress" server/src/services/renderers/presentation-renderer.ts` confirms SSE progress
- `grep -q "puterChatComplete" server/src/services/renderers/presentation-renderer.ts` confirms LLM integration
- `pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit` passes
</verification>

<success_criteria>
- presentation-renderer.ts exists with renderPresentation export
- LLM generates slide JSON for both pitch-deck and demo-video types (PRES-01, PRES-03)
- Remotion renderMedia is called with concurrency:1 (PRES-02)
- onProgress publishes content_job.progress SSE events (PRES-04)
- browserExecutable reuses Playwright Chromium
- PresentationBundle includes mp4Base64 and inputProps for interactive Player replay
- TypeScript compilation passes for server and shared packages
</success_criteria>

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