--- 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" --- 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 @$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 @.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) From packages/content-renderer/src/index.ts (created in Plan 01): ```typescript export async function getBundlePath(): Promise; export async function renderPresentationComposition(opts: { serveUrl: string; input: Record; onProgress: (progress: number) => void; browserExecutable?: string; }): Promise<{ buffer: Buffer; durationInFrames: number; fps: number; inputProps: Record }>; ``` 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; } 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; ``` From server/src/services/live-events.ts: ```typescript export function publishLiveEvent(input: { companyId: string; type: LiveEventType; payload?: Record }): LiveEvent; ``` From server/src/services/renderers/diagram-renderer.ts: ```typescript export function resolveBrowserPath(): string; ``` Task 1: Create presentation-renderer with LLM slide generation and Remotion render pipeline server/src/services/renderers/presentation-renderer.ts - 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) Create `server/src/services/renderers/presentation-renderer.ts`. **Exports:** `renderPresentation(input: Record, companyId: string, jobId: string): Promise` **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. 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 - 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 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 Task 2: Verify server tsc compilation with presentation renderer wired server/src/services/content-job-runner.ts - server/src/services/content-job-runner.ts (verify the "presentation" case was added in Plan 01) 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. cd /opt/nexus && pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit 2>&1 | tail -3 - pnpm --filter @paperclipai/shared exec -- npx tsc --noEmit exits 0 or shows no errors Server and shared packages compile without TypeScript errors, presentation renderer is type-correct - `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 - 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 After completion, create `.planning/phases/44-video-presentations/44-02-SUMMARY.md`