nexus/.planning/phases/44-video-presentations/44-01-PLAN.md

16 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
44-video-presentations 01 execute 1
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
true
PRES-01
PRES-02
PRES-04
truths artifacts key_links
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
path provides contains
packages/content-renderer/package.json Workspace package with remotion, @remotion/bundler, @remotion/renderer deps @remotion/renderer
path provides contains
packages/content-renderer/src/Root.tsx registerRoot entry for Remotion bundler registerRoot
path provides contains
packages/content-renderer/src/compositions/PitchDeck.tsx Pitch deck composition using Series Series.Sequence
path provides contains
packages/content-renderer/src/compositions/DemoVideo.tsx Demo video composition Series.Sequence
path provides exports
packages/content-renderer/src/index.ts getBundlePath and renderPresentationComposition exports
getBundlePath
renderPresentationComposition
path provides contains
packages/shared/src/constants.ts content_job.progress event type content_job.progress
path provides contains
server/src/services/renderers/types.ts PresentationBundle in ContentBundle union PresentationBundle
path provides contains
server/src/services/content-job-runner.ts presentation case in renderContent switch case "presentation"
from to via pattern
packages/content-renderer/src/index.ts packages/content-renderer/src/Root.tsx bundle() entryPoint Root.tsx
from to via pattern
server/src/services/content-job-runner.ts server/src/services/renderers/presentation-renderer.ts dynamic import 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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) 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:

export interface RenderResult {
  filename: string;
  contentType: string;
  buffer: Buffer;
}
export type ContentBundle = DiagramBundle | IconSetBundle | ... | BrandKitBundle;

From server/src/services/content-job-runner.ts:

export async function renderContent(jobType: string, input: Record<string, unknown>): Promise<RenderResult>

From server/src/services/renderers/diagram-renderer.ts:

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

<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>
After completion, create `.planning/phases/44-video-presentations/44-01-SUMMARY.md`