16 KiB
16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 44-video-presentations | 01 | execute | 1 |
|
true |
|
|
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>