249 lines
12 KiB
Markdown
249 lines
12 KiB
Markdown
---
|
|
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>
|