@open-take/compositor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["mb: MotionBlurConfig | undefined","DEFAULT_FRAMING: FramingConfig","DEFAULT_MOTION_BLUR: MotionBlurConfig","DEFAULT_CURSOR: CursorConfig","scale: number","best: ZoomLevelName | null","MOTION: Record<\"calm\" | \"natural\" | \"brisk\", MotionPreset>","cursor: CursorConfig","DARK_SHADOW: LookPreset[\"shadow\"]","LOOKS: Record<string, LookPreset>","framing: FramingConfig","FINISH: Record<\"smooth\" | \"crisp\" | \"heavy\", MotionBlurConfig | undefined>","mb: MotionBlurConfig | undefined","cachedFfmpeg: string | undefined","cachedFfprobe: string | undefined","bin: string","pkg: string","t: number","x1: number","y1: number","x2: number","y2: number","s: number","x: number","bounce: number","p: number","cursor: TakeComposition[\"cursor\"]","angleDeg: number | undefined","oW: number","oH: number","kfs: KF<number>[]","ease: (u: number) => number","easeDown?: (u: number) => number","kfs: KF<Pt>[]","bbox: BBox","outW: number","outH: number","fillFrac: number","maxScale: number","restScale: number","videoW: number","videoH: number","insetFrac: number","center: Pt","scale: number","comp: TakeComposition","restC: Pt","anchors: Anchor[]","zf: { t: number; s: number }[]","cf: { t: number; c: Pt }[]","c: Pt","target: number","legs: Leg[]","cur: Pt","a: Pt","b: Pt","path: Pt[]","u: number","seg: number[]","log: CaptureLog","opts: PlanOpts","framing: FramingConfig","cursor: CursorConfig","p: Pt","b: BBox","pts: Pt[]","events: CompEvent[]","durationMs","reason: string","reqField: Record<string, string>","comp: TakeComposition","opts: ValidateOpts","issues: CompositionIssue[]","path: string","message: string","fix?: string","s: string","cmd: string","args: string[]","videoPath: string","outMp4: string","fps: number","inMp4: string","baseFps: number","samples: number","shutter: number","opts: RenderTakeOpts","composition: TakeComposition","issues: CompositionIssue[]","produced: string","_worker: number","progress: number"],"sources":["../src/types.ts","../src/presets.ts","../src/ffmpeg.ts","../src/math.ts","../src/plan.ts","../src/validate.ts","../src/render.ts"],"sourcesContent":["// The editable take composition — the source of truth. The agent (or a\n// human) edits THIS, and the revideo scene renders it deterministically.\n// All spatial fields are in VIDEO-pixel space (capture coords mapped\n// through viewport→video scaling), so the scene works in one coordinate\n// system.\n\nexport type Pt = { x: number; y: number };\nexport type BBox = { x: number; y: number; w: number; h: number };\n\n// --- capture input (the ground-truth event log) -----------------------\n\n/** Editorial zoom intent for an action (set by the planner/agent). */\nexport type ZoomIntent = \"auto\" | \"never\" | \"always\";\n\n/** Fields common to every captured action. `x,y` is the anchor / start\n * point (cursor target), viewport CSS px. */\nexport type CaptureEventBase = {\n /** anchor point (click target / field / drag start), viewport CSS px */\n x: number;\n y: number;\n /** element bounding box, viewport CSS px — the ground-truth edge */\n box?: BBox;\n /** ms from recording start (when the cursor arrives / action begins) */\n tMs: number;\n /** selector / note, kept for editability */\n sel?: string;\n note?: string;\n /** selective-zoom intent from the plan (default auto = heuristic) */\n zoom?: ZoomIntent;\n};\n\n/** A click (or a type's focus-click): an instantaneous action at a point. */\nexport type CaptureClick = CaptureEventBase & { kind?: \"click\" };\n\n/** Typing into a focused field: the cursor parks and the zoom holds for\n * `durationMs` while the text appears in the recording. */\nexport type CaptureType = CaptureEventBase & {\n kind: \"type\";\n /** what was typed (editability) */\n text: string;\n /** ms the typing occupies on screen (ground-truth wall time) */\n durationMs: number;\n};\n\n/** A drag: a path from the anchor (`x,y`) to `to`, optionally via `path`,\n * with the button held for `durationMs`. */\nexport type CaptureDrag = CaptureEventBase & {\n kind: \"drag\";\n /** drag end point, viewport CSS px */\n to: { x: number; y: number };\n /** full polyline incl. ends, viewport CSS px (freehand strokes) */\n path?: { x: number; y: number }[];\n /** ms the drag occupies on screen (ground-truth wall time) */\n durationMs: number;\n /** how the stroke was paced and BAKED into the ink: \"smooth\" (accel-in /\n * decel-out — a natural hand-draw) or \"linear\" (constant speed). The\n * compositor cursor must replay the SAME easing to stay locked to the ink.\n * Absent ⇒ linear (legacy captures). */\n ease?: \"linear\" | \"smooth\";\n};\n\n/** A scroll: the content pans for `durationMs`. The cursor holds (no travel),\n * full-view (no zoom). `dy` is the signed pixels scrolled (editability). */\nexport type CaptureScroll = CaptureEventBase & {\n kind: \"scroll\";\n /** signed pixels scrolled (positive = down) */\n dy: number;\n /** ms the scroll occupies on screen (ground-truth wall time) */\n durationMs: number;\n};\n\n/** A hover: the cursor travels to `x,y` and dwells for `durationMs` so a\n * tooltip / menu / hover-state reveals. Like a click that doesn't click. */\nexport type CaptureHover = CaptureEventBase & {\n kind: \"hover\";\n /** ms the dwell occupies on screen (ground-truth wall time) */\n durationMs: number;\n};\n\n/** A key press / shortcut: keyboard-driven, so the cursor holds (no travel).\n * Holds for `durationMs` while the effect plays out; if a reveal element was\n * located, `box` carries its bbox so the zoom can frame it. */\nexport type CapturePress = CaptureEventBase & {\n kind: \"press\";\n /** the chord pressed, e.g. \"Enter\" / \"Meta+k\" (editability) */\n keys: string;\n /** ms the hold occupies on screen (ground-truth wall time) */\n durationMs: number;\n};\n\nexport type CaptureEvent =\n | CaptureClick\n | CaptureType\n | CaptureDrag\n | CaptureScroll\n | CaptureHover\n | CapturePress;\n\nexport type CaptureLog = {\n video: { width: number; height: number; fps?: number | string; durationS?: number };\n viewport: { w: number; h: number };\n start?: { x: number; y: number };\n /** the ordered ground-truth actions (click / type / drag) */\n events: CaptureEvent[];\n tEndMs?: number;\n};\n\n// --- the composition (editable) ----------------------------------------\n\nexport type ZoomDecision = {\n /** selective: not every action zooms. Edit this to tune/remove. */\n enabled: boolean;\n /** absolute stage scale to reach (bbox-fit, capped) */\n scale: number;\n /** video-px point to frame (bbox center), pre-clamp */\n center: Pt;\n /** when the zoom-in begins (ms) */\n inAtMs: number;\n /** Optional \"glide\": a slow camera drift WHILE the zoom is held, as a velocity\n * in video-px per second {x,y}. The held centre pans from `center` by\n * `glide · holdSeconds` across the hold window (then the next beat ramps from\n * there / it zooms out from there). Adds life vs a dead-static hold (Screen\n * Studio's glide). Absent/0 ⇒ a still hold. Clamped to the video at read time,\n * so a drift just stops at the edge. Keep it gentle (tens of px/s). */\n glide?: Pt;\n /** why this decision (for the human/agent reading the composition) */\n reason: string;\n};\n\nexport type CompEvent = {\n kind: \"click\" | \"type\" | \"drag\" | \"scroll\" | \"hover\" | \"press\";\n tMs: number;\n /** anchor point (click / focus / drag start / hover) in video-px. For a\n * scroll/press the cursor does not move; this is its resting point. */\n point: Pt;\n /** element bbox in video-px (if known) */\n bbox?: BBox;\n label?: string;\n zoom: ZoomDecision;\n /** how long the action plays out after `tMs` (type/drag/scroll/hover/press);\n * 0 for a click. The cursor parks and the zoom holds for this long. */\n durationMs?: number;\n /** typed text (kind=type), for editability */\n text?: string;\n /** chord pressed (kind=press), for editability */\n keys?: string;\n /** drag end point, video-px (kind=drag) */\n to?: Pt;\n /** drag polyline incl. ends, video-px (kind=drag) — the cursor path */\n path?: Pt[];\n /** drag stroke easing baked into the ink (kind=drag): \"smooth\" or \"linear\".\n * The cursor replays it so it stays locked to the ink. Absent ⇒ linear. */\n ease?: \"linear\" | \"smooth\";\n};\n\nexport type FramingConfig = {\n /** video occupies this fraction of the frame at rest (inset for the backdrop) */\n insetFrac: number;\n cornerRadius: number;\n shadow: { color: string; blur: number; offset: Pt };\n /** backdrop behind the framed video. `type` defaults to \"gradient\" (from→to);\n * \"solid\" fills `from` only. `angle` (deg, CSS-like: 0 = upward) rotates the\n * gradient; absent ⇒ the legacy top-left→bottom-right diagonal (pixel-identical). */\n background: { from: string; to: string; type?: \"gradient\" | \"solid\"; angle?: number };\n};\n\nexport type CursorConfig = {\n /** Fallback travel duration (ms) when `travelWidthsPerSec` is unset/0. A FIXED\n * duration makes cursor speed scale with distance (short=slow, long=fast),\n * which reads inconsistent; prefer the distance-aware speed below. */\n travelMs: number;\n /** Distance-aware travel speed, as a fraction of the source video WIDTH per\n * second (resolution-independent). A travel leg's duration is\n * `clamp(distance / (widthsPerSec·videoWidth), travelMinMs, travelMaxMs)`, so\n * the cursor holds a roughly CONSTANT on-screen speed regardless of distance —\n * the premium-recorder feel (measured ~0.30 widths/s on the reference). Set 0\n * to fall back to the fixed `travelMs`. */\n travelWidthsPerSec: number;\n /** Floor for a distance-aware travel (ms) so short hops aren't an instant snap. */\n travelMinMs: number;\n /** Ceiling for a distance-aware travel (ms) so a full-width jump stays a glide,\n * not a multi-second crawl. */\n travelMaxMs: number;\n scale: number;\n arcFrac: number;\n arcMax: number;\n rippleMs: number;\n /** ms to hold a zoom after the action settles, before zooming back out */\n holdMs: number;\n /** ms for the zoom-OUT ramp (back to rest) */\n zoomOutMs: number;\n /** ms for the zoom-IN ramp (into a target). Decoupled from travelMs so the\n * zoom can be slower/gentler than the cursor (a cinematic ~1s zoom). */\n zoomInMs: number;\n /** Easing for the zoom/pan stage ramps (scale + center together), as\n * cubic-bezier control points. Absent ⇒ symmetric smootherstep — whose broad\n * near-constant-velocity middle reads a bit linear, esp. on the zoom-OUT\n * settle. A decel-biased curve gives a softer landing into rest. */\n zoomEase?: [number, number, number, number];\n /** Spring easing for the zoom/pan stage ramps, as a `bounce` amount ∈ [0,~0.6):\n * 0 = critically damped (a soft physical ease-out), higher = more overshoot/\n * snap (the \"silky\" settle a premium screen-recorder has; ~0.06 for zoom). When\n * set, this WINS over `zoomEase` (see math.ts stageEasing). Absent ⇒ use\n * `zoomEase`/smootherstep. The segment duration stays zoomInMs/zoomOutMs;\n * bounce only shapes the curve. NB: large bounce can undershoot rest on the\n * zoom-OUT (momentary backdrop dead-space) — keep it small for zoom. */\n zoomSpring?: number;\n /** ms to delay the synthetic cursor along a DRAG stroke, compensating for the\n * capture pipeline latency: the captured ink appears ~this long after the pen\n * actually moved, so without the delay the (exact-time) cursor leads the ink.\n * Tune so the cursor tip sits on the ink front mid-stroke. */\n dragLagMs: number;\n /** easing for a travel move, as cubic-bezier control points [x1,y1,x2,y2].\n * Default is a symmetric ease-in-out — measured from a reference recording, whose\n * cursor accelerates and decelerates evenly (a slow start, fast middle, soft\n * landing). Drag strokes ignore this (they ease the stroke in lockstep with\n * the captured ink — see math.ts). */\n travelEase: [number, number, number, number];\n};\n\n/** Camera motion blur (temporal supersampling — what a real shutter does). The\n * renderer samples the composition camera at `samples` sub-times within each\n * output frame and averages them, so a fast zoom/pan/cursor smears in the\n * motion direction (a camera/shutter motion blur). It smears the\n * backdrop-reveal on a zoom-OUT into a soft gradient instead of a hard\n * single-frame pop. The captured video's frames repeat across sub-samples, so\n * the recording's own content is NOT blurred — only the camera move + cursor. */\nexport type MotionBlurConfig = {\n /** sub-frames sampled per output frame. 1 ⇒ OFF (no supersampling, no cost). */\n samples: number;\n /** fraction of the frame interval the shutter is open (0..1). Blur strength;\n * 0 ⇒ OFF. ~0.5 = a 180° shutter, 1 = 360°. */\n shutter: number;\n};\n\n/** One caption window on a review copy: the pill reads `text` from `fromMs`\n * (inclusive) to `toMs` (exclusive) on the composition timeline. */\nexport type ReviewBadge = { fromMs: number; toMs: number; text: string };\n\n/** Render-time decoration for review copies and A/B variant reels: burned-in\n * beat badges, a REVIEW watermark, and a per-variant corner label. Drawn by the\n * scene in SCREEN space (fixed, outside the composition camera) so the text is\n * legible at any zoom. This is a render input only — it is never written into\n * the editable `*.composition.json` artifact (render strips it) and the\n * validator ignores it. Text renders in the headless Chrome, so any script\n * (incl. CJK) works without ffmpeg font filters. */\nexport type ReviewDecor = {\n /** top-right watermark, e.g. \"REVIEW\" — marks a copy as not-for-posting */\n watermark?: string;\n /** bottom-left caption pill, swapping per timeline window */\n badges?: ReviewBadge[];\n /** constant bottom-left variant label for A/B reels, e.g. \"B · tight ×1.8\" */\n label?: string;\n};\n\nexport type TakeComposition = {\n output: { width: number; height: number; fps: number };\n source: {\n videoUrl: string;\n videoWidth: number;\n videoHeight: number;\n viewport: { w: number; h: number };\n };\n framing: FramingConfig;\n cursor: CursorConfig;\n /** camera motion blur; absent ⇒ off (renders exactly as before). */\n motionBlur?: MotionBlurConfig;\n /** cursor start, video-px */\n start: Pt;\n events: CompEvent[];\n durationMs: number;\n /** render-time review decoration (badges/watermark/label); never persisted */\n review?: ReviewDecor;\n};\n\n/** True when motion blur is configured to actually do something (so the OFF\n * path stays byte-identical to the pre-motion-blur renderer). */\nexport function motionBlurActive(mb: MotionBlurConfig | undefined): mb is MotionBlurConfig {\n return !!mb && mb.samples > 1 && mb.shutter > 0;\n}\n\nexport const DEFAULT_FRAMING: FramingConfig = {\n insetFrac: 0.92,\n cornerRadius: 28,\n shadow: { color: \"rgba(0,0,0,0.55)\", blur: 60, offset: { x: 0, y: 28 } },\n background: { from: \"#1e1b3a\", to: \"#0a0e1c\" },\n};\n\n// Camera motion blur, ON by default — it smooths the zoom\n// motion and softens the zoom-OUT backdrop reveal (the model-C hitch). 6\n// sub-frames is a good smoothness/speed balance; ~0.7 shutter is a strong-ish\n// blur without ghosting. EXPORT render cost scales with `samples` (renders at\n// fps·samples then averages), so `samples` is the quality⇄speed knob — drop it\n// to render faster, raise (≤~12) for silkier fast pans.\nexport const DEFAULT_MOTION_BLUR: MotionBlurConfig = {\n samples: 6,\n shutter: 0.7,\n};\n\nexport const DEFAULT_CURSOR: CursorConfig = {\n travelMs: 560,\n // Distance-aware cursor speed — the dominant \"silky\" lever. A fixed duration\n // makes velocity scale with distance (short=slow, long=fast); holding a roughly\n // constant on-screen speed reads consistent/premium. ~0.35 widths/s with the\n // big-move cap (travelMaxMs 850) was tuned by eye to a notch slower than a\n // premium reference recording (svgl.mp4) on this app's long toolbar→canvas\n // moves. Tune up for snappier, down for slower/grander.\n // NOTE on the cap (subtle): travel ENDS at the action's instant, so a LONGER\n // duration starts EARLIER, not later. With the front-loaded ease (below) a\n // longer move therefore reaches the target SOONER — i.e. raising travelMaxMs\n // can read as *faster*. Judge speed by how fast the cursor SWEEPS, not by when\n // it arrives; lower the cap for a more deliberate sweep.\n travelWidthsPerSec: 0.35,\n travelMinMs: 300,\n travelMaxMs: 850,\n scale: 2.0,\n // Near-straight glide. The reference cursor barely bows (measured: a\n // full-width move deviated <2% off the straight line), so the arc is small —\n // just enough to avoid a robotic ruler-straight path, not a visible curve.\n arcFrac: 0.05,\n arcMax: 24,\n rippleMs: 450,\n holdMs: 1100,\n // Gentle, cinematic zoom (a ~1s ramp reads as premium); our\n // old 600ms tied-to-travel zoom felt snappy/mechanical by comparison.\n zoomOutMs: 800,\n zoomInMs: 760,\n // Zoom/pan stage easing. Same decel-biased curve as the cursor — symmetric\n // smootherstep (the fallback) has a broad near-constant-velocity middle that\n // reads a touch linear, especially as the zoom-OUT settles back to rest; this\n // gives a soft landing into rest. Applied to scale + center together.\n zoomEase: [0.3, 0.0, 0.2, 1.0],\n // The captured ink trails the pen by the screencast/encode pipeline latency τ;\n // delay the cursor by τ so its tip rides the ink front. Set to τ EXACTLY and\n // the cursor locks to the ink at ALL stroke speeds (both are the same time-\n // delay of the same path), which is what makes a SMOOTH (eased) drag work —\n // its fast mid-section amplifies any τ mismatch into a visible lead. Measured\n // τ≈190ms on this pipeline (swept dragLagMs, picked where the tip sits on the\n // ink front mid-stroke); the old 110ms left even linear strokes ~40px ahead.\n // Re-tune if a different machine's encode latency differs.\n dragLagMs: 190,\n // Soft launch + soft landing (gentle accel from rest, peak ~t0.24, decelerate\n // into the target). Chosen over a symmetric ease-in-out (which felt less silky)\n // and over a pure ease-out [.33,1,.68,1] (instant-max launch + a 46%-of-duration\n // creep tail that makes the speed knob lie — see travelMaxMs). This keeps the\n // soft landing (the silk) while trimming the tail to 38% and removing the abrupt\n // launch. Tune toward [0.42,0,0.58,1] for a more uniform sweep.\n travelEase: [0.3, 0.0, 0.2, 1.0],\n};\n","// Curated preset vocabulary — the Screen-Studio move (named levels, not raw\n// numbers) applied to the conversational refine loop. The composition schema\n// stays raw numbers; presets are a dictionary the CLI/agent speak: the agent\n// writes numbers into composition.json, and `beats`/`ab` reverse-map numbers\n// back to names for display. A value matching no preset displays as \"custom\"\n// and must never be silently rounded to a named level (bbox-derived precision\n// is ground truth).\n\nimport { DEFAULT_MOTION_BLUR } from \"./types\";\nimport type { CursorConfig, FramingConfig, MotionBlurConfig } from \"./types\";\n\n// --- zoom levels (absolute stage scale; rest ≈ 0.92 at same-size output) ----\n// medium matches the planner's default cap (1.5), so agent-planned zooms land\n// on a named level; close stays under the validator's 2.5 soft cap.\nexport const ZOOM_LEVELS = {\n light: 1.25,\n medium: 1.5,\n tight: 1.8,\n close: 2.2,\n} as const;\nexport type ZoomLevelName = keyof typeof ZOOM_LEVELS;\n\n/** Name a scale if it sits within tolerance of a preset; else null (custom). */\nexport function zoomLevelName(scale: number, tol = 0.07): ZoomLevelName | null {\n let best: ZoomLevelName | null = null;\n let bestD = tol;\n for (const [name, v] of Object.entries(ZOOM_LEVELS) as [ZoomLevelName, number][]) {\n const d = Math.abs(scale - v);\n if (d <= bestD) {\n best = name;\n bestD = d;\n }\n }\n return best;\n}\n\n// --- motion (pace) bundles ---------------------------------------------------\n// One name moves the whole feel together: cursor speed + hold + zoom ramps.\n// \"natural\" IS the shipped default (DEFAULT_CURSOR) — naming it makes the\n// default state speakable.\nexport type MotionPreset = Pick<\n CursorConfig,\n \"travelWidthsPerSec\" | \"holdMs\" | \"zoomInMs\" | \"zoomOutMs\"\n>;\nexport const MOTION: Record<\"calm\" | \"natural\" | \"brisk\", MotionPreset> = {\n calm: { travelWidthsPerSec: 0.28, holdMs: 1500, zoomInMs: 950, zoomOutMs: 950 },\n natural: { travelWidthsPerSec: 0.35, holdMs: 1100, zoomInMs: 760, zoomOutMs: 800 },\n brisk: { travelWidthsPerSec: 0.45, holdMs: 750, zoomInMs: 560, zoomOutMs: 600 },\n};\nexport type MotionName = keyof typeof MOTION;\n\nexport function motionName(cursor: CursorConfig): MotionName | null {\n for (const [name, m] of Object.entries(MOTION) as [MotionName, MotionPreset][]) {\n if (\n Math.abs(cursor.travelWidthsPerSec - m.travelWidthsPerSec) < 0.015 &&\n Math.abs(cursor.holdMs - m.holdMs) < 60 &&\n Math.abs(cursor.zoomInMs - m.zoomInMs) < 60 &&\n Math.abs(cursor.zoomOutMs - m.zoomOutMs) < 60\n )\n return name;\n }\n return null;\n}\n\n// --- looks (backdrop bundles) ------------------------------------------------\n// A look is the WHOLE backdrop treatment as one coherent bundle — background\n// gradient + corner radius + shadow — so \"paper\" doesn't produce a light\n// backdrop with dark-tuned corners/shadow.\nexport type LookPreset = Pick<FramingConfig, \"background\" | \"cornerRadius\" | \"shadow\">;\nconst DARK_SHADOW: LookPreset[\"shadow\"] = {\n color: \"rgba(0,0,0,0.55)\",\n blur: 60,\n offset: { x: 0, y: 28 },\n};\nexport const LOOKS: Record<string, LookPreset> = {\n midnight: {\n background: { from: \"#1e1b3a\", to: \"#0a0e1c\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n ink: {\n background: { from: \"#16181d\", to: \"#07080a\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n slate: {\n background: { from: \"#232733\", to: \"#0e1116\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n ocean: {\n background: { from: \"#0c2b36\", to: \"#071019\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n plum: {\n background: { from: \"#2a1e3a\", to: \"#140a1c\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n ember: {\n background: { from: \"#38231d\", to: \"#1a0d09\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n paper: {\n background: { from: \"#f6f4ef\", to: \"#ddd8cd\" },\n cornerRadius: 22,\n shadow: { color: \"rgba(31,27,20,0.30)\", blur: 45, offset: { x: 0, y: 20 } },\n },\n plain: {\n background: { from: \"#101014\", to: \"#101014\", type: \"solid\" },\n cornerRadius: 28,\n shadow: DARK_SHADOW,\n },\n};\nexport type LookName = keyof typeof LOOKS;\n\nexport function lookName(framing: FramingConfig): string | null {\n const bg = framing.background;\n for (const [name, l] of Object.entries(LOOKS)) {\n if (\n l.background.from.toLowerCase() === bg.from.toLowerCase() &&\n l.background.to.toLowerCase() === bg.to.toLowerCase() &&\n (l.background.type ?? \"gradient\") === (bg.type ?? \"gradient\")\n )\n return name;\n }\n return null;\n}\n\n// --- finish (motion blur) ----------------------------------------------------\nexport const FINISH: Record<\"smooth\" | \"crisp\" | \"heavy\", MotionBlurConfig | undefined> = {\n smooth: DEFAULT_MOTION_BLUR, // {samples: 6, shutter: 0.7}\n crisp: undefined, // blur off — exports ~6× faster\n heavy: { samples: 8, shutter: 0.85 },\n};\nexport type FinishName = keyof typeof FINISH;\n\nexport function finishName(mb: MotionBlurConfig | undefined): FinishName | null {\n const active = !!mb && mb.samples > 1 && mb.shutter > 0;\n if (!active) return \"crisp\";\n for (const [name, f] of Object.entries(FINISH) as [FinishName, MotionBlurConfig | undefined][]) {\n if (f && mb && f.samples === mb.samples && Math.abs(f.shutter - mb.shutter) < 0.03) return name;\n }\n return null;\n}\n","// ffmpeg/ffprobe resolution — zero-config for npm consumers. Prefer the\n// system binaries on PATH (often newer/faster, and what the repo has always\n// used); fall back to the platform binaries from @ffmpeg-installer/ffmpeg and\n// @ffprobe-installer/ffprobe (direct deps here, and already in the tree via\n// @revideo/ffmpeg — so the fallback costs consumers no extra download). Throw\n// with an install hint only when neither exists.\n\nimport { spawnSync } from \"node:child_process\";\n\nlet cachedFfmpeg: string | undefined;\nlet cachedFfprobe: string | undefined;\n\nfunction runsOk(bin: string): boolean {\n try {\n return spawnSync(bin, [\"-version\"], { stdio: \"ignore\" }).status === 0;\n } catch {\n return false;\n }\n}\n\nasync function installerPath(pkg: string): Promise<string | null> {\n try {\n const m = (await import(pkg)) as { default?: { path?: string }; path?: string };\n return m.default?.path ?? m.path ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function resolveFfmpeg(): Promise<string> {\n if (cachedFfmpeg) return cachedFfmpeg;\n if (runsOk(\"ffmpeg\")) {\n cachedFfmpeg = \"ffmpeg\";\n return cachedFfmpeg;\n }\n const p = await installerPath(\"@ffmpeg-installer/ffmpeg\");\n if (p && runsOk(p)) {\n cachedFfmpeg = p;\n return p;\n }\n throw new Error(\n \"ffmpeg not found — install it (e.g. `brew install ffmpeg`) or `npm install` so the bundled @ffmpeg-installer binary resolves for this platform\",\n );\n}\n\nexport async function resolveFfprobe(): Promise<string> {\n if (cachedFfprobe) return cachedFfprobe;\n if (runsOk(\"ffprobe\")) {\n cachedFfprobe = \"ffprobe\";\n return cachedFfprobe;\n }\n const p = await installerPath(\"@ffprobe-installer/ffprobe\");\n if (p && runsOk(p)) {\n cachedFfprobe = p;\n return p;\n }\n throw new Error(\n \"ffprobe not found — install ffmpeg (e.g. `brew install ffmpeg`) or `npm install` so the bundled @ffprobe-installer binary resolves for this platform\",\n );\n}\n","// Pure, isomorphic math — imported by both the node-side planner and the\n// revideo scene (compiled fresh by vite at render time). No node/browser\n// APIs in here.\n\nimport type { BBox, Pt, TakeComposition } from \"./types\";\n\n// smootherstep 6t^5-15t^4+10t^3\nexport function smoother(t: number): number {\n t = Math.max(0, Math.min(1, t));\n return t * t * t * (t * (t * 6 - 15) + 10);\n}\n\n// Cubic-bezier easing y(x) with endpoints (0,0),(1,1) and control points\n// (x1,y1),(x2,y2) — the same model CSS easing uses. Solves x(s)=x by\n// bisection (cheap, monotone) then returns y(s). Lets the travel cursor use a\n// decelerate-biased curve (long, gentle settle) instead of symmetric easing.\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number {\n const bx = (s: number) => {\n const u = 1 - s;\n return 3 * u * u * s * x1 + 3 * u * s * s * x2 + s * s * s;\n };\n const by = (s: number) => {\n const u = 1 - s;\n return 3 * u * u * s * y1 + 3 * u * s * s * y2 + s * s * s;\n };\n return (x: number) => {\n if (x <= 0) return 0;\n if (x >= 1) return 1;\n let lo = 0,\n hi = 1,\n s = x;\n for (let i = 0; i < 24; i++) {\n const xt = bx(s);\n if (Math.abs(xt - x) < 1e-4) break;\n if (xt < x) lo = s;\n else hi = s;\n s = (lo + hi) / 2;\n }\n return by(s);\n };\n}\n\n// Spring-shaped easing y(p) over [0,1]: the unit step response of a damped\n// spring, time-normalised so it rises (and, for bounce>0, slightly overshoots)\n// then settles within the segment. `bounce` ∈ [0, ~0.6): 0 = critically damped\n// (no overshoot — a soft, physical ease-out); higher = more overshoot/snap. The\n// \"silky\" landing comes from a touch of overshoot (a near-critically-damped\n// zoom spring, bounce ~0.06; a snappier cursor ~0.13).\n//\n// Under time-normalisation the curve depends ONLY on the damping ratio ζ=1−bounce\n// (the natural frequency cancels), so a single `bounce` knob is the whole shape —\n// the segment DURATION stays whatever the keyframes say (zoomInMs/zoomOutMs).\nexport function springEase(bounce: number): (p: number) => number {\n const zeta = Math.max(0.4, Math.min(1, 1 - bounce));\n const Ts = -Math.log(1e-3) / zeta; // settling time to the 0.1% band (ω0 = 1)\n return (p: number) => {\n if (p <= 0) return 0;\n if (p >= 1) return 1;\n const t = p * Ts;\n if (zeta >= 1) return 1 - Math.exp(-t) * (1 + t); // critically damped\n const wd = Math.sqrt(1 - zeta * zeta); // damped natural frequency (ω0 = 1)\n return 1 - Math.exp(-zeta * t) * (Math.cos(wd * t) + (zeta / wd) * Math.sin(wd * t));\n };\n}\n\n// The stage (zoom + pan) easing, selected from the cursor config. The single\n// source the revideo scene (scene.tsx) consumes — any other renderer must use\n// it identically so renderers can never drift. Precedence:\n// spring (zoomSpring) → cubic-bezier (zoomEase) → smootherstep.\nexport function stageEasing(cursor: TakeComposition[\"cursor\"]): (u: number) => number {\n if (cursor.zoomSpring != null) return springEase(cursor.zoomSpring);\n if (cursor.zoomEase) return cubicBezier(...cursor.zoomEase);\n return smoother;\n}\n\n// Easing for the CENTRE pan. Deliberately NEVER the spring: an overshooting\n// spring on a pan makes the camera wobble past its target and back (and re-\n// accelerates the zoom-out recenter = a stutter). Overshoot is only wanted on\n// the scale zoom-IN, so centre stays on the monotone bezier/smoother. For a\n// non-spring composition this equals stageEasing, so the default is unchanged.\nexport function panEasing(cursor: TakeComposition[\"cursor\"]): (u: number) => number {\n if (cursor.zoomEase) return cubicBezier(...cursor.zoomEase);\n return smoother;\n}\n\n// Backdrop gradient endpoints in TOP-LEFT origin (0..oW, 0..oH), shared by the\n// preview canvas and the revideo scene so the backdrop can't drift. `angle` is\n// CSS-like degrees (0 = upward); absent ⇒ the legacy (0,0)→(oW,oH) diagonal, so\n// existing compositions render pixel-identically. (Scene maps to centred coords\n// by subtracting oW/2, oH/2.)\nexport function gradientEndpoints(\n angleDeg: number | undefined,\n oW: number,\n oH: number,\n): { x0: number; y0: number; x1: number; y1: number } {\n if (angleDeg == null) return { x0: 0, y0: 0, x1: oW, y1: oH };\n const th = (angleDeg * Math.PI) / 180;\n const dx = Math.sin(th);\n const dy = -Math.cos(th);\n const L = Math.abs(oW * dx) + Math.abs(oH * dy); // CSS gradient line length\n const cx = oW / 2;\n const cy = oH / 2;\n return {\n x0: cx - (dx * L) / 2,\n y0: cy - (dy * L) / 2,\n x1: cx + (dx * L) / 2,\n y1: cy + (dy * L) / 2,\n };\n}\n\ntype KF<T> = [number, T];\n\n// `easeDown` (optional) eases segments where the value FALLS (v1<v0) differently\n// from rising ones — used so the scale springs on zoom-IN but settles smoothly\n// (bezier, no overshoot/abruptness) on zoom-OUT. Omit it ⇒ one easing for all\n// (the default behaviour, unchanged).\nexport function keyvalN(\n t: number,\n kfs: KF<number>[],\n ease: (u: number) => number = smoother,\n easeDown?: (u: number) => number,\n): number {\n if (t <= kfs[0]![0]) return kfs[0]![1];\n if (t >= kfs[kfs.length - 1]![0]) return kfs[kfs.length - 1]![1];\n for (let i = 0; i < kfs.length - 1; i++) {\n const [t0, v0] = kfs[i]!;\n const [t1, v1] = kfs[i + 1]!;\n if (t0 <= t && t <= t1) {\n const e = easeDown && v1 < v0 ? easeDown : ease;\n return v0 + (v1 - v0) * e((t - t0) / (t1 - t0));\n }\n }\n return kfs[kfs.length - 1]![1];\n}\n\nexport function keyvalP(t: number, kfs: KF<Pt>[], ease: (u: number) => number = smoother): Pt {\n if (t <= kfs[0]![0]) return kfs[0]![1];\n if (t >= kfs[kfs.length - 1]![0]) return kfs[kfs.length - 1]![1];\n for (let i = 0; i < kfs.length - 1; i++) {\n const [t0, v0] = kfs[i]!;\n const [t1, v1] = kfs[i + 1]!;\n if (t0 <= t && t <= t1) {\n const p = ease((t - t0) / (t1 - t0));\n return { x: v0.x + (v1.x - v0.x) * p, y: v0.y + (v1.y - v0.y) * p };\n }\n }\n return kfs[kfs.length - 1]![1];\n}\n\n// --- bbox-fit zoom (Finding 1) -----------------------------------------\n\n/**\n * Scale (absolute, video-px → output-px) that fits `bbox` into `fillFrac`\n * of the output frame, capped at `maxScale` and floored at `restScale`.\n * Wide/long elements naturally get a gentler scale because the limiting\n * dimension dominates min().\n */\nexport function bboxFitScale(\n bbox: BBox,\n outW: number,\n outH: number,\n fillFrac: number,\n maxScale: number,\n restScale: number,\n): number {\n const fitW = (outW * fillFrac) / Math.max(1, bbox.w);\n const fitH = (outH * fillFrac) / Math.max(1, bbox.h);\n return Math.min(maxScale, Math.max(restScale, Math.min(fitW, fitH)));\n}\n\n/** Stage scale at rest: video inset into the frame (leaves backdrop margin). */\nexport function restStageScale(\n videoW: number,\n videoH: number,\n outW: number,\n outH: number,\n insetFrac: number,\n): number {\n return insetFrac * Math.min(outW / videoW, outH / videoH);\n}\n\n/**\n * Clamp a desired video-px center so the scaled video still covers the output\n * frame (the viewport crop stays inside the source video) when zoomed in. When\n * the content does not cover an axis (zoomed out / inset), centre that axis.\n *\n * In the composition-camera model the whole composition\n * (backdrop + inset screen) is zoomed by one camera; this keeps the camera crop\n * within the screen so a zoom fills the frame (no backdrop margin) without ever\n * panning past the recording's edge.\n */\nexport function clampCenter(\n center: Pt,\n scale: number,\n videoW: number,\n videoH: number,\n outW: number,\n outH: number,\n): Pt {\n const halfW = outW / (2 * scale);\n const halfH = outH / (2 * scale);\n const cx =\n videoW * scale >= outW ? Math.min(Math.max(center.x, halfW), videoW - halfW) : videoW / 2;\n const cy =\n videoH * scale >= outH ? Math.min(Math.max(center.y, halfH), videoH - halfH) : videoH / 2;\n return { x: cx, y: cy };\n}\n\n// --- stage (zoom/pan) keyframes from the composition -------------------\n\nexport type StageKeyframes = {\n z: KF<number>[]; // absolute stage scale over time (seconds)\n c: KF<Pt>[]; // raw video-px center over time (clamp at eval)\n T: number; // total duration (s)\n};\n\nexport function buildStageKeyframes(comp: TakeComposition): StageKeyframes {\n const { videoWidth: vW, videoHeight: vH } = comp.source;\n const { width: oW, height: oH } = comp.output;\n const rest = restStageScale(vW, vH, oW, oH, comp.framing.insetFrac);\n const restC: Pt = { x: vW / 2, y: vH / 2 };\n const HOLD = comp.cursor.holdMs / 1000;\n const ZOUT = comp.cursor.zoomOutMs / 1000;\n\n // Framing anchors over time. A zoom-enabled beat ramps to its target; a\n // scroll (content pans) and a zoom-less press (Escape/Enter whose effect is\n // global) ramp back to REST so the frame is full-view through them — without\n // these, a prior zoom would persist across the scroll/keypress. Other\n // disabled beats (a \"never\" click) keep holding the prior framing, unchanged.\n type Anchor = {\n tMs: number;\n durationMs: number;\n inAtMs: number;\n scale: number;\n center: Pt;\n glide?: Pt;\n };\n const anchors: Anchor[] = [];\n for (const e of comp.events) {\n const base = { tMs: e.tMs, durationMs: e.durationMs ?? 0, inAtMs: e.zoom.inAtMs };\n if (e.kind === \"scroll\" || (e.kind === \"press\" && !e.zoom.enabled)) {\n anchors.push({ ...base, scale: rest, center: restC });\n } else if (e.zoom.enabled) {\n anchors.push({ ...base, scale: e.zoom.scale, center: e.zoom.center, glide: e.zoom.glide });\n }\n }\n\n // Scale and centre are evaluated independently (keyvalN / keyvalP), so their\n // keyframe TIMES need not line up — `pushC` adds a centre-only keyframe.\n const zf: { t: number; s: number }[] = [{ t: 0, s: rest }];\n const cf: { t: number; c: Pt }[] = [{ t: 0, c: restC }];\n const push = (t: number, s: number, c: Pt) => {\n zf.push({ t: Math.max(t, zf[zf.length - 1]!.t + 1e-3), s });\n cf.push({ t: Math.max(t, cf[cf.length - 1]!.t + 1e-3), c });\n };\n const pushC = (t: number, c: Pt) => {\n cf.push({ t: Math.max(t, cf[cf.length - 1]!.t + 1e-3), c });\n };\n // Invert the zoom-OUT easing to time the centre recenter (the centre reaches\n // rest exactly when the scale re-covers the frame — else the tightening centre-\n // clamp catches the still-panning centre and re-accelerates it = the \"two-stage\"\n // zoom-out stutter, commit b005cbe). The zoom-OUT scale uses panEasing (the\n // monotone bezier — scale springs only on zoom-IN, settles smoothly on the way\n // out), so invert THAT, by bisection.\n const ze = panEasing(comp.cursor);\n const invEase = (target: number) => {\n let lo = 0;\n let hi = 1;\n for (let i = 0; i < 30; i++) {\n const m = (lo + hi) / 2;\n if (ze(m) < target) lo = m;\n else hi = m;\n }\n return (lo + hi) / 2;\n };\n // Scale at which the video re-covers the frame on both axes; below it the\n // centre is forced to video-centre (clampCenter), so a recenter must FINISH by\n // here or the tightening clamp \"catches\" the still-panning centre.\n const fillThreshold = Math.max(oW / vW, oH / vH);\n\n let cur = { s: rest, c: restC };\n anchors.forEach((e, i) => {\n const rampStart = e.inAtMs / 1000;\n const clickT = e.tMs / 1000;\n // the action plays out (typing/drawing/scrolling) for durationMs after tMs\n // — hold the target framing through it (a point click has duration 0).\n const actionEnd = (e.tMs + e.durationMs) / 1000;\n const next = anchors[i + 1];\n const holdEndT = next ? next.inAtMs / 1000 : actionEnd + HOLD;\n // glide: drift the held centre across the hold window (velocity px/s ·\n // holdSeconds), so a held zoom slowly pans instead of sitting dead-static.\n // (Eased with the stage easing like any centre segment; clampCenter keeps it\n // in-bounds at read time. invEase below stays on the monotone bezier.)\n let holdC = e.center;\n if (e.glide && (e.glide.x !== 0 || e.glide.y !== 0)) {\n const holdDur = Math.max(0, holdEndT - clickT);\n holdC = { x: e.center.x + e.glide.x * holdDur, y: e.center.y + e.glide.y * holdDur };\n }\n push(rampStart, cur.s, cur.c); // hold current until ramp begins\n // Zoom-OUT across the fill-threshold INTO a beat (e.g. a rest/full-view beat):\n // land the centre on its target AT the crossing so the tightening centre-clamp\n // doesn't catch the still-panning centre and re-accelerate it. This is the same\n // two-stage stutter b005cbe killed for the FINAL zoom-out — here for the\n // between-beats ones (which went through this ramp and never got the fix).\n if (\n cur.s > fillThreshold &&\n e.scale < fillThreshold &&\n fillThreshold > rest &&\n clickT > rampStart &&\n Math.hypot(e.center.x - cur.c.x, e.center.y - cur.c.y) > 1\n ) {\n const uCross = invEase((fillThreshold - cur.s) / (e.scale - cur.s));\n pushC(rampStart + uCross * (clickT - rampStart), e.center);\n }\n push(clickT, e.scale, e.center); // ramp to target by the action\n cur = { s: e.scale, c: holdC };\n if (next) {\n // hold (or glide) the target until the next anchor begins ramping\n push(holdEndT, cur.s, cur.c);\n } else {\n const holdEnd = holdEndT;\n push(holdEnd, cur.s, cur.c);\n // Final zoom-out. Land the CENTRE on rest at the fill-threshold crossing\n // (the scale keeps its single smooth segment) so the centre clamp — which\n // tightens to a point as the video re-covers the frame — never catches the\n // still-panning centre and re-accelerates it (a two-stage stutter). Only\n // when the zoom actually overfills AND sits off-centre.\n const offset = Math.hypot(cur.c.x - restC.x, cur.c.y - restC.y) > 1;\n if (offset && cur.s > fillThreshold && fillThreshold > rest) {\n const uCross = invEase((fillThreshold - cur.s) / (rest - cur.s));\n pushC(holdEnd + uCross * ZOUT, restC);\n }\n push(holdEnd + ZOUT, rest, restC); // zoom back out\n cur = { s: rest, c: restC };\n }\n });\n\n const lastT = Math.max(zf[zf.length - 1]!.t, cf[cf.length - 1]!.t);\n const T = Math.max(comp.durationMs / 1000, lastT) + 0.3;\n push(T, rest, restC);\n\n return { z: zf.map((f) => [f.t, f.s]), c: cf.map((f) => [f.t, f.c]), T };\n}\n\n// --- cursor path (eased, with gentle arc) ------------------------------\n\n// A `drag` leg carries the polyline the cursor follows with the button held\n// (no perpendicular arc — it traces the real stroke). A normal travel leg has\n// no path and gets the gentle arc.\ntype Leg = {\n t0: number;\n t1: number;\n a: Pt;\n b: Pt;\n drag?: boolean;\n path?: Pt[];\n ease?: \"linear\" | \"smooth\";\n};\n\nexport function buildLegs(comp: TakeComposition): Leg[] {\n const legs: Leg[] = [];\n let cur: Pt = comp.start;\n // Distance-aware travel: hold a roughly constant on-screen speed (premium\n // feel) instead of a fixed duration (which makes short moves slow + long\n // moves fast). Falls back to the fixed travelMs when speed is unset/0.\n const { travelWidthsPerSec, travelMinMs, travelMaxMs, travelMs } = comp.cursor;\n const speedPxPerMs = ((travelWidthsPerSec || 0) * comp.source.videoWidth) / 1000;\n const travelDur = (a: Pt, b: Pt): number => {\n if (speedPxPerMs <= 0) return travelMs / 1000;\n const dist = Math.hypot(b.x - a.x, b.y - a.y);\n return Math.min(travelMaxMs, Math.max(travelMinMs, dist / speedPxPerMs)) / 1000;\n };\n for (const e of comp.events) {\n // scroll/press are not pointer-driven — the cursor holds where it was\n // (the content pans / the keyboard acts). No travel leg; `cur` is untouched,\n // so the between-legs parking logic keeps the cursor at its last anchor.\n if (e.kind === \"scroll\" || e.kind === \"press\") continue;\n const arrive = e.tMs / 1000;\n // Start travelDur before arrival, but never before the previous leg ended\n // (a long glide into a quick succession would otherwise overlap it — then\n // the move just runs in the available window, a touch faster than target).\n const prevEnd = legs.length ? legs[legs.length - 1]!.t1 : 0;\n const t0 = Math.max(arrive - travelDur(cur, e.point), prevEnd, 0);\n legs.push({ t0, t1: arrive, a: cur, b: e.point }); // travel to anchor\n cur = e.point;\n if (e.kind === \"drag\" && e.to) {\n // Delay the stroke by dragLagMs so the cursor rides the captured ink front\n // (the ink trails the pen by the capture-pipeline latency). The cursor\n // holds at the start point during the gap [arrive, arrive+lag], then traces\n // — matching when the ink actually appears on screen.\n const lag = comp.cursor.dragLagMs / 1000;\n const start = arrive + lag;\n const dEnd = (e.tMs + (e.durationMs ?? 0)) / 1000 + lag;\n const path = e.path && e.path.length >= 2 ? e.path : [e.point, e.to];\n if (dEnd > start)\n legs.push({ t0: start, t1: dEnd, a: e.point, b: e.to, drag: true, path, ease: e.ease });\n cur = e.to;\n }\n }\n return legs;\n}\n\n/** Point a fraction `u` (0..1) along a polyline, parameterised by arc length. */\nfunction alongPath(path: Pt[], u: number): Pt {\n if (path.length === 1) return path[0]!;\n const seg: number[] = [];\n let total = 0;\n for (let i = 0; i < path.length - 1; i++) {\n const d = Math.hypot(path[i + 1]!.x - path[i]!.x, path[i + 1]!.y - path[i]!.y);\n seg.push(d);\n total += d;\n }\n if (total === 0) return path[0]!;\n let target = u * total;\n for (let i = 0; i < seg.length; i++) {\n if (target <= seg[i]! || i === seg.length - 1) {\n const f = seg[i]! > 0 ? target / seg[i]! : 0;\n return {\n x: path[i]!.x + (path[i + 1]!.x - path[i]!.x) * f,\n y: path[i]!.y + (path[i + 1]!.y - path[i]!.y) * f,\n };\n }\n target -= seg[i]!;\n }\n return path[path.length - 1]!;\n}\n\nexport function cursorPos(t: number, legs: Leg[], comp: TakeComposition): Pt {\n for (const lg of legs) {\n if (lg.t0 <= t && t <= lg.t1) {\n const raw = Math.max(0, Math.min(1, (t - lg.t0) / (lg.t1 - lg.t0)));\n // A drag replays the captured stroke's pacing so the cursor stays locked\n // to the ink: \"smooth\" (accel-in / decel-out — a natural hand-draw) or\n // \"linear\" (constant speed). The capture bakes the SAME curve into the ink\n // (cdp-capture.ts) and records it on the event; absent ⇒ linear (legacy).\n // (Held-button moves are fire-and-forget there, so the slow eased ends —\n // sub-pixel, no paint — no longer stall the stroke; cursor and ink are\n // both ~stationary at the ends, so they stay locked.)\n if (lg.drag && lg.path) return alongPath(lg.path, lg.ease === \"smooth\" ? smoother(raw) : raw);\n const e = comp.cursor.travelEase;\n const p = e ? cubicBezier(e[0], e[1], e[2], e[3])(raw) : smoother(raw);\n const base = { x: lg.a.x + (lg.b.x - lg.a.x) * p, y: lg.a.y + (lg.b.y - lg.a.y) * p };\n const dx = lg.b.x - lg.a.x,\n dy = lg.b.y - lg.a.y;\n const L = Math.hypot(dx, dy) || 1;\n const arc = Math.min(comp.cursor.arcFrac * L, comp.cursor.arcMax) * Math.sin(Math.PI * p);\n return { x: base.x + (-dy / L) * arc, y: base.y + (dx / L) * arc };\n }\n }\n if (legs.length === 0 || t < legs[0]!.t0) return comp.start;\n for (let i = 0; i < legs.length - 1; i++)\n if (legs[i]!.t1 < t && t < legs[i + 1]!.t0) return legs[i]!.b;\n return legs[legs.length - 1]!.b;\n}\n\n/** True while a drag is mid-stroke (button held) — for the pressed cursor. */\nexport function isDragging(t: number, legs: Leg[]): boolean {\n return legs.some((lg) => lg.drag === true && lg.t0 <= t && t <= lg.t1);\n}\n","// planComposition: capture event log -> default editable TakeComposition.\n// The selective bbox-fit zoom heuristic lives here; every decision it\n// makes is written into the composition so a human/agent can tune it.\n\nimport { bboxFitScale, restStageScale } from \"./math\";\nimport {\n type BBox,\n type CaptureLog,\n type CompEvent,\n type CursorConfig,\n DEFAULT_CURSOR,\n DEFAULT_FRAMING,\n DEFAULT_MOTION_BLUR,\n type FramingConfig,\n type Pt,\n type TakeComposition,\n} from \"./types\";\n\nexport type PlanOpts = {\n output?: { width?: number; height?: number; fps?: number };\n framing?: Partial<FramingConfig>;\n cursor?: Partial<CursorConfig>;\n /** element should fill this fraction of the frame when zoomed (default 0.55) */\n fillFrac?: number;\n /** hard cap on zoom (default 1.5 — a gentle workhorse; lower than\n * the old 2.0 reads more premium. Small targets still zoom, just not as hard;\n * raise per-beat in the composition when a tiny element needs it.) */\n maxScale?: number;\n /** require fit-scale to exceed rest*this to bother zooming (default 1.3) */\n zoomRatio?: number;\n /** never zoom the first (orienting) action by default (Finding 1) */\n zoomFirst?: boolean;\n};\n\nexport function planComposition(log: CaptureLog, opts: PlanOpts = {}): TakeComposition {\n const vW = log.video.width,\n vH = log.video.height;\n const oW = opts.output?.width ?? vW;\n const oH = opts.output?.height ?? vH;\n const fps = opts.output?.fps ?? 30;\n const fillFrac = opts.fillFrac ?? 0.55;\n const maxScale = opts.maxScale ?? 1.5;\n const zoomRatio = opts.zoomRatio ?? 1.3;\n const zoomFirst = opts.zoomFirst ?? false;\n\n const framing: FramingConfig = { ...DEFAULT_FRAMING, ...opts.framing };\n const cursor: CursorConfig = { ...DEFAULT_CURSOR, ...opts.cursor };\n\n // viewport CSS px -> video px\n const sx = vW / log.viewport.w;\n const sy = vH / log.viewport.h;\n const mapPt = (p: Pt): Pt => ({ x: p.x * sx, y: p.y * sy });\n const mapBox = (b: BBox): BBox => ({ x: b.x * sx, y: b.y * sy, w: b.w * sx, h: b.h * sy });\n\n const rest = restStageScale(vW, vH, oW, oH, framing.insetFrac);\n\n // Axis-aligned bbox of a polyline (video-px). Used so a drag zooms to fit\n // the WHOLE stroke (a path, not a point): big cross-canvas drags fit ≈ rest\n // → no zoom (correct, global); small localised drags zoom in.\n const pathBBox = (pts: Pt[]): BBox => {\n const xs = pts.map((p) => p.x),\n ys = pts.map((p) => p.y);\n const x = Math.min(...xs),\n y = Math.min(...ys);\n return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };\n };\n\n const events: CompEvent[] = log.events.map((c, i) => {\n const kind = c.kind ?? \"click\";\n const point = mapPt({ x: c.x, y: c.y });\n const isFirst = i === 0;\n const intent = c.zoom ?? \"auto\";\n const durationMs = \"durationMs\" in c ? c.durationMs : 0;\n\n // drag: cursor path + the bbox we fit-zoom is the path's bbox\n const to = kind === \"drag\" ? mapPt((c as { to: Pt }).to) : undefined;\n const rawPath =\n kind === \"drag\"\n ? ((c as { path?: Pt[] }).path ?? [{ x: c.x, y: c.y }, (c as { to: Pt }).to])\n : undefined;\n const path = rawPath?.map(mapPt);\n const ease = kind === \"drag\" ? (c as { ease?: \"linear\" | \"smooth\" }).ease : undefined;\n\n // The region this action is \"about\" — what zoom should frame.\n const bbox = kind === \"drag\" && path ? pathBBox(path) : c.box ? mapBox(c.box) : undefined;\n\n let enabled = false;\n let scale = rest;\n let reason: string;\n const center = bbox ? { x: bbox.x + bbox.w / 2, y: bbox.y + bbox.h / 2 } : point;\n const fit = bbox ? bboxFitScale(bbox, oW, oH, fillFrac, maxScale, rest) : maxScale;\n\n if (kind === \"scroll\") {\n // A scroll is a pan beat: the content moves, the frame stays full-view.\n // Zooming would fight the motion, so a scroll never zooms.\n enabled = false;\n reason = \"scroll — full view (content pans, no zoom)\";\n } else if (intent === \"never\") {\n enabled = false;\n reason = \"plan: zoom=never (global/navigation payoff — keep full view)\";\n } else if (intent === \"always\") {\n enabled = true;\n scale = fit;\n reason = `plan: zoom=always → ${fit.toFixed(2)}x (capped ${maxScale}x)`;\n } else if (!bbox) {\n reason = \"no bbox in event log — cannot bbox-fit, so no zoom (avoids framing dead space)\";\n } else {\n scale = fit;\n const meaningful = fit > rest * zoomRatio;\n const region = kind === \"drag\" ? \"drag path\" : \"element\";\n if (isFirst && !zoomFirst) {\n enabled = false;\n reason = `first/orienting action — skipped by default (fit ${fit.toFixed(2)}x available)`;\n } else if (!meaningful) {\n enabled = false;\n reason = `${region} fills the frame already (fit ${fit.toFixed(2)}x ≈ rest ${rest.toFixed(2)}x) — gentle/no zoom`;\n } else {\n enabled = true;\n reason = `bbox-fit ${fit.toFixed(2)}x (capped ${maxScale}x), ${region} framed with ${Math.round(fillFrac * 100)}% fill`;\n }\n }\n\n return {\n kind,\n tMs: c.tMs,\n point,\n bbox,\n label: c.sel ?? c.note,\n // zoom-in ramp starts zoomInMs before the action (decoupled from cursor\n // travelMs so the zoom can be slower/gentler — a more cinematic feel).\n zoom: { enabled, scale, center, inAtMs: Math.max(0, c.tMs - cursor.zoomInMs), reason },\n ...(durationMs ? { durationMs } : {}),\n ...(kind === \"type\" ? { text: (c as { text: string }).text } : {}),\n ...(kind === \"press\" ? { keys: (c as { keys: string }).keys } : {}),\n ...(to ? { to } : {}),\n ...(path ? { path } : {}),\n ...(ease ? { ease } : {}),\n };\n });\n\n const start = log.start ? mapPt(log.start) : { x: vW * 0.25, y: vH * 0.9 };\n const last = log.events.length ? log.events[log.events.length - 1]! : undefined;\n // the last action ends durationMs after its tMs (typing/drag plays out)\n const lastEnd = last ? last.tMs + (\"durationMs\" in last ? last.durationMs : 0) : 0;\n const durationMs = Math.max(log.tEndMs ?? 0, lastEnd + cursor.holdMs + cursor.zoomOutMs + 400);\n\n return {\n output: { width: oW, height: oH, fps },\n source: { videoUrl: \"/capture.mp4\", videoWidth: vW, videoHeight: vH, viewport: log.viewport },\n framing,\n cursor,\n motionBlur: DEFAULT_MOTION_BLUR,\n start,\n events,\n durationMs,\n };\n}\n","// validateComposition: a NON-MUTATING structural check on an edited\n// composition. The refine loop is \"edit the JSON, re-render\" — so the agent\n// (or a human) hand-edits `*.composition.json`, and this gives a\n// millisecond, field-specific verdict BEFORE paying for a render. It never\n// changes the composition; it only reports.\n//\n// Severity:\n// \"error\" — the render would be wrong/broken (or silently mis-framed).\n// renderTake refuses to render until these are fixed.\n// \"warn\" — suspect, but may be intentional. Rendered as-is.\n//\n// The single most important check is the capture-lock (see CAPTURE-LOCKED\n// below): an action beat's `tMs` is bound to the recording — the video frame\n// at time t shows the page AS IT WAS at capture-time t — so re-rendering with\n// a moved `tMs` desyncs the overlay from the on-screen action. Retiming an\n// action needs a re-capture, not a JSON edit. Pass the capture log to enforce\n// this; without it the check is skipped (we can't know the ground truth).\n\nimport { restStageScale } from \"./math\";\nimport type { CaptureLog, TakeComposition } from \"./types\";\n\nexport type CompositionIssue = {\n severity: \"error\" | \"warn\";\n /** dotted/indexed field path, e.g. \"events[3].zoom.scale\" */\n path: string;\n message: string;\n /** a concrete suggested correction the agent can apply */\n fix?: string;\n};\n\nexport type ValidateOpts = {\n /** soft ceiling on zoom scale — above it the pixels visibly soften (warn).\n * Default 2.5 (the planner caps auto-zoom at 1.5; manual edits get headroom). */\n maxScale?: number;\n /** the ground-truth capture log. When given, action `tMs` is checked against\n * it (capture-lock). Omit to skip that check. */\n captureLog?: CaptureLog;\n};\n\nconst reqField: Record<string, string> = {\n type: \"text\",\n press: \"keys\",\n drag: \"to\",\n};\n\nexport function validateComposition(\n comp: TakeComposition,\n opts: ValidateOpts = {},\n): CompositionIssue[] {\n const issues: CompositionIssue[] = [];\n const err = (path: string, message: string, fix?: string) =>\n issues.push({ severity: \"error\", path, message, fix });\n const warn = (path: string, message: string, fix?: string) =>\n issues.push({ severity: \"warn\", path, message, fix });\n\n const maxScale = opts.maxScale ?? 2.5;\n\n // --- output / source sanity ---\n const { width: oW, height: oH, fps } = comp.output;\n if (!(oW > 0 && oH > 0))\n err(\"output\", `non-positive output size ${oW}x${oH}`, \"set positive width/height\");\n if (!(fps > 0)) err(\"output.fps\", `fps must be > 0 (got ${fps})`, \"set output.fps to 30 or 60\");\n const { videoWidth: vW, videoHeight: vH } = comp.source;\n if (!(vW > 0 && vH > 0)) err(\"source\", `non-positive source video size ${vW}x${vH}`);\n\n const rest = restStageScale(vW, vH, oW, oH, comp.framing.insetFrac);\n\n // --- motion blur (render cost scales with samples: frames = fps × samples) ---\n const mb = comp.motionBlur;\n if (mb) {\n if (!Number.isInteger(mb.samples) || mb.samples < 1 || mb.samples > 16)\n err(\n \"motionBlur.samples\",\n `samples must be an integer 1-16 (got ${mb.samples})`,\n \"6 is the balanced default; 1 turns blur off\",\n );\n else if (mb.samples > 9)\n warn(\n \"motionBlur.samples\",\n `samples ${mb.samples} renders ${mb.samples}× the frames for little extra smoothness`,\n \"9 is the practical ceiling\",\n );\n if (!(mb.shutter >= 0 && mb.shutter <= 1))\n err(\n \"motionBlur.shutter\",\n `shutter must be 0..1 (got ${mb.shutter})`,\n \"0.7 is the default; 0 turns blur off\",\n );\n }\n\n // --- duration / ordering ---\n const events = comp.events ?? [];\n let lastEnd = 0;\n let prevT = -1;\n for (let i = 0; i < events.length; i++) {\n const e = events[i]!;\n const p = `events[${i}]`;\n const dur = e.durationMs ?? 0;\n const endMs = e.tMs + dur;\n lastEnd = Math.max(lastEnd, endMs);\n\n // ordering: events must stay in temporal order (the video is temporal)\n if (e.tMs < prevT) {\n err(\n `${p}.tMs`,\n `tMs ${e.tMs} is before the previous beat (${prevT}) — events out of order`,\n \"events follow the recording; reordering needs a re-capture, not a swap here\",\n );\n }\n prevT = e.tMs;\n\n if (e.tMs < 0) err(`${p}.tMs`, `negative tMs ${e.tMs}`);\n if (e.tMs > comp.durationMs)\n err(\n `${p}.tMs`,\n `tMs ${e.tMs} exceeds composition durationMs ${comp.durationMs}`,\n \"raise durationMs or remove the beat\",\n );\n\n // kind-specific required fields (editability invariants)\n const need = reqField[e.kind];\n if (need && (e as Record<string, unknown>)[need] == null)\n err(\n `${p}.${need}`,\n `${e.kind} beat is missing \"${need}\"`,\n `restore ${need} from the capture`,\n );\n\n // --- zoom decision ---\n const z = e.zoom;\n if (!z) {\n err(\n `${p}.zoom`,\n \"missing zoom decision\",\n \"every beat needs a zoom { enabled, scale, center, inAtMs }\",\n );\n continue;\n }\n // inAtMs must precede the action (the zoom-in ramps in before the beat lands)\n if (z.inAtMs < 0) err(`${p}.zoom.inAtMs`, `negative inAtMs ${z.inAtMs}`, \"clamp to 0\");\n if (z.inAtMs > e.tMs)\n err(\n `${p}.zoom.inAtMs`,\n `inAtMs ${z.inAtMs} is AFTER the action at tMs ${e.tMs} — the zoom would arrive late`,\n `set inAtMs = tMs − cursor.zoomInMs (= ${Math.max(0, e.tMs - comp.cursor.zoomInMs)})`,\n );\n\n if (z.enabled) {\n // scale must zoom IN (≥ rest). Below rest shows MORE than the framed\n // video → backdrop dead-space; that's never what an enabled zoom wants.\n if (z.scale < rest - 1e-6)\n err(\n `${p}.zoom.scale`,\n `scale ${z.scale.toFixed(3)} is below rest ${rest.toFixed(3)} — that zooms OUT past the frame (dead space)`,\n `raise to ≥ ${rest.toFixed(2)}, or set zoom.enabled=false for a full-view beat`,\n );\n else if (z.scale <= rest + 1e-6)\n warn(\n `${p}.zoom.scale`,\n `scale ${z.scale.toFixed(3)} ≈ rest — zoom is enabled but does nothing visible`,\n \"raise scale to actually zoom, or set enabled=false\",\n );\n if (z.scale > maxScale)\n warn(\n `${p}.zoom.scale`,\n `scale ${z.scale.toFixed(2)} exceeds the soft cap ${maxScale} — pixels will soften`,\n `clamp toward ${maxScale.toFixed(1)} unless the detail truly needs it`,\n );\n // center inside the video (clampCenter will pull it in, but an off-video\n // center usually means a stale/hand-miscomputed value)\n if (z.center.x < 0 || z.center.x > vW || z.center.y < 0 || z.center.y > vH)\n warn(\n `${p}.zoom.center`,\n `center (${z.center.x.toFixed(0)},${z.center.y.toFixed(0)}) is outside the video ${vW}x${vH}`,\n e.bbox\n ? \"use the bbox center: { x: bbox.x+bbox.w/2, y: bbox.y+bbox.h/2 }\"\n : \"point at the on-screen target (video-px)\",\n );\n // enabling zoom on a no-bbox beat means an invented center/scale — fine,\n // but flag it so the refine loop knows it's not bbox-derived.\n if (!e.bbox)\n warn(\n `${p}.zoom`,\n \"zoom enabled on a beat with no bbox — center/scale are hand-set, not bbox-fit\",\n \"double-check the center frames the intended region\",\n );\n }\n }\n\n // tail: the composition must outlast the last action (+ a little settle)\n if (comp.durationMs < lastEnd)\n err(\n \"durationMs\",\n `durationMs ${comp.durationMs} ends before the last action (${lastEnd})`,\n `raise to ≥ ${Math.round(lastEnd + comp.cursor.holdMs + comp.cursor.zoomOutMs)}`,\n );\n else if (comp.durationMs < lastEnd + comp.cursor.zoomOutMs)\n warn(\n \"durationMs\",\n `only ${comp.durationMs - lastEnd}ms after the last action — the final zoom-out may be cut`,\n `allow ≥ ${comp.cursor.zoomOutMs}ms (cursor.zoomOutMs) of tail`,\n );\n\n // --- CAPTURE-LOCKED: action tMs must match the recording ---\n // The video is temporal: a beat's tMs is WHEN it is visible in the capture.\n // A render-only edit cannot move that, so a drifted tMs would fire the\n // overlay off the on-screen action. (Cinematic timing — inAtMs, holdMs,\n // zoom ramps, the intro travel and the tail — IS freely editable; only the\n // action anchor tMs is locked.)\n if (opts.captureLog) {\n const log = opts.captureLog;\n if (log.events.length !== events.length)\n warn(\n \"events\",\n `composition has ${events.length} beats but the capture log has ${log.events.length} — added/removed actions need a re-capture`,\n \"re-capture to change the choreography; edit only renders the existing recording\",\n );\n const n = Math.min(log.events.length, events.length);\n for (let i = 0; i < n; i++) {\n const lt = log.events[i]!.tMs;\n const ct = events[i]!.tMs;\n if (Math.abs(lt - ct) > 1)\n err(\n `events[${i}].tMs`,\n `tMs ${ct} drifted from the captured ${lt} — an action's tMs is capture-locked (the video shows it at ${lt}ms)`,\n `restore tMs to ${lt}; to retime the action itself, re-capture`,\n );\n }\n }\n\n return issues;\n}\n\n/** Format issues for a human/agent log. Errors first. */\nexport function formatIssues(issues: CompositionIssue[]): string {\n if (!issues.length) return \"composition OK (0 issues)\";\n const order = (s: string) => (s === \"error\" ? 0 : 1);\n return issues\n .slice()\n .sort((a, b) => order(a.severity) - order(b.severity))\n .map(\n (i) => ` [${i.severity}] ${i.path}: ${i.message}${i.fix ? `\\n fix: ${i.fix}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","// renderTake: composition (or capture log) + captured video -> polished\n// mp4 + the editable composition written alongside it.\n//\n// Runs revideo headless (vite + chromium + ffmpeg). The renderer resolves\n// everything relative to process.cwd(), and injects `projectFile` verbatim\n// as an import specifier — so we chdir to the package root and use the\n// vite-root-absolute \"/src/scene/project.ts\" (a bare specifier hangs the\n// renderer forever; see spike-revideo/VERDICT.md).\n\nimport { spawn } from \"node:child_process\";\nimport { copyFile, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { renderVideo } from \"@revideo/renderer\";\nimport { resolveFfmpeg } from \"./ffmpeg\";\nimport { type PlanOpts, planComposition } from \"./plan\";\nimport { type CaptureLog, type TakeComposition, motionBlurActive } from \"./types\";\nimport { type CompositionIssue, formatIssues, validateComposition } from \"./validate\";\n\n// dist/index.js -> package root\nconst PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst PUBLIC_MP4 = resolve(PKG_ROOT, \"public/capture.mp4\");\nconst COMP_JSON = resolve(PKG_ROOT, \"src/scene/.composition.json\");\nconst RENDER_OUT = \"out-render\"; // relative to cwd (= PKG_ROOT at render time)\n\nexport type RenderTakeOpts = {\n /** input capture video (webm or mp4) */\n videoPath: string;\n /** output polished mp4 path */\n outPath: string;\n /** provide a capture log (auto-planned) ... */\n log?: CaptureLog;\n /** ... or a ready-made composition (editable artifact) */\n composition?: TakeComposition;\n planOpts?: PlanOpts;\n logProgress?: boolean;\n /** Chrome binary for the headless render. Pass the same Chrome-for-Testing\n * the capture path uses so a single browser serves both stages (no second\n * download). revideo forwards this to puppeteer.launch's executablePath;\n * if unset, revideo's bundled puppeteer resolves its own. */\n chromePath?: string;\n /** the capture log, for cross-checking that an edited composition didn't\n * drift an action's capture-locked tMs (see validateComposition). Optional —\n * the structural checks run regardless. */\n captureLog?: CaptureLog;\n /** skip the pre-render structural validation. Default false — we validate and\n * refuse to render an errored composition (a render is expensive; catch a bad\n * hand-edit in milliseconds instead). */\n skipValidate?: boolean;\n /** progress callback (0..1) forwarded from revideo's renderer. */\n onProgress?: (progress: number) => void;\n /** render only this window of the composition timeline, in SECONDS — the\n * windowed-render path behind A/B variant reels (a 4s window instead of the\n * whole take). With motion blur OFF, frames are identical to the same span\n * of a full render (the timeline is deterministic). With blur active the\n * content matches but not bit-exactly: the tmix shutter windows are phased\n * from the CLIP start, and the first frame's trailing window is truncated.\n * Forwarded to revideo's projectSettings.range. */\n rangeSec?: [number, number];\n /** write the editable `<out>.composition.json` sibling (default true). Review\n * copies and A/B reels are disposable — they skip the sibling. */\n writeCompositionSibling?: boolean;\n};\n\nfunction run(cmd: string, args: string[]): Promise<void> {\n return new Promise((res, rej) => {\n const c = spawn(cmd, args, { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\n let err = \"\";\n c.stderr.on(\"data\", (d) => (err += d));\n c.on(\"error\", rej);\n c.on(\"close\", (code) => (code === 0 ? res() : rej(new Error(`${cmd} exited ${code}: ${err}`))));\n });\n}\n\n/** Normalise the capture to a constant-fps mp4 the web decoder can read.\n * fps follows the composition so a hi-fps capture can render at 60 (the\n * render grid must match — a 30-grid would throw away the extra frames). */\nasync function toMp4(videoPath: string, outMp4: string, fps: number): Promise<void> {\n await mkdir(dirname(outMp4), { recursive: true });\n await run(await resolveFfmpeg(), [\n \"-y\",\n \"-loglevel\",\n \"error\",\n \"-i\",\n resolve(videoPath),\n \"-c:v\",\n \"libx264\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-crf\",\n \"18\",\n \"-r\",\n String(fps),\n \"-an\",\n outMp4,\n ]);\n}\n\n/** Temporal-supersampling motion blur: the scene was rendered at fps·samples\n * (project.ts); average a trailing shutter window of sub-frames back down to the\n * output fps. `tmix=frames=M` averages M consecutive sub-frames; `fps=baseFps`\n * then decimates ≈every `samples`-th, so each output frame = the mean of the last\n * M sub-frames of its interval (a trailing shutter). Re-tags bt709/tv to match\n * the capture pipeline (the input is already bt709, but tmix→encode must keep it). */\nasync function motionBlurMp4(\n inMp4: string,\n outMp4: string,\n baseFps: number,\n samples: number,\n shutter: number,\n): Promise<void> {\n const M = Math.max(1, Math.min(samples, Math.round(shutter * samples)));\n const vf =\n `tmix=frames=${M},fps=${baseFps},format=yuv420p,` +\n \"setparams=range=tv:colorspace=bt709:color_primaries=bt709:color_trc=bt709\";\n await run(await resolveFfmpeg(), [\n \"-y\",\n \"-loglevel\",\n \"error\",\n \"-i\",\n resolve(inMp4),\n \"-vf\",\n vf,\n \"-c:v\",\n \"libx264\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-crf\",\n \"18\",\n \"-r\",\n String(baseFps),\n \"-an\",\n outMp4,\n ]);\n}\n\nexport async function renderTake(\n opts: RenderTakeOpts,\n): Promise<{ mp4Path: string; compositionPath: string }> {\n const composition: TakeComposition =\n opts.composition ??\n planComposition(\n opts.log ??\n (() => {\n throw new Error(\"renderTake: provide `log` or `composition`\");\n })(),\n opts.planOpts,\n );\n\n // 0. validate BEFORE the expensive render. A hand-edited composition (the\n // refine loop) can carry a malformed zoom or a capture-locked tMs drift;\n // catch it in milliseconds rather than after a multi-second render.\n if (!opts.skipValidate) {\n const issues: CompositionIssue[] = validateComposition(composition, {\n captureLog: opts.captureLog ?? opts.log,\n });\n const errors = issues.filter((i) => i.severity === \"error\");\n const warns = issues.filter((i) => i.severity === \"warn\");\n if (opts.logProgress && warns.length)\n process.stderr.write(`composition warnings:\\n${formatIssues(warns)}\\n`);\n if (errors.length)\n throw new Error(\n `composition has ${errors.length} error(s) — refusing to render:\\n${formatIssues(errors)}`,\n );\n }\n\n // 1. serve the capture as /capture.mp4 (vite public dir under PKG_ROOT)\n await toMp4(opts.videoPath, PUBLIC_MP4, composition.output.fps);\n\n // 2. hand the composition to the scene (static import, rewritten per render)\n await mkdir(dirname(COMP_JSON), { recursive: true });\n await writeFile(COMP_JSON, JSON.stringify(composition, null, 2));\n\n // 3. render headless, with cwd pinned to the package root.\n // revideo's @revideo/telemetry phones home to PostHog by default; this is an\n // all-local tool, so default it OFF (an explicit user-set value still wins).\n if (process.env.DISABLE_TELEMETRY === undefined) process.env.DISABLE_TELEMETRY = \"true\";\n const prevCwd = process.cwd();\n process.chdir(PKG_ROOT);\n let produced: string;\n try {\n produced = await renderVideo({\n projectFile: \"/src/scene/project.ts\",\n settings: {\n outFile: \"take.mp4\",\n outDir: RENDER_OUT,\n workers: 1,\n ...(opts.rangeSec ? { projectSettings: { range: opts.rangeSec } } : {}),\n logProgress: opts.logProgress ?? false,\n ...(opts.onProgress\n ? { progressCallback: (_worker: number, progress: number) => opts.onProgress!(progress) }\n : {}),\n // Reuse the capture-managed Chrome-for-Testing when given (one browser\n // for both stages); else let revideo's puppeteer resolve its own.\n puppeteer: {\n // --password-store/--use-mock-keychain: never touch the OS keychain, so\n // macOS doesn't pop a \"Chrome wants to use Chromium Safe Storage\" prompt\n // mid-render (matches the capture launch in runtime/cdp.ts).\n args: [\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--password-store=basic\",\n \"--use-mock-keychain\",\n ],\n ...(opts.chromePath ? { executablePath: opts.chromePath } : {}),\n },\n },\n });\n } finally {\n process.chdir(prevCwd);\n }\n\n // 4. deliver mp4 (motion-blur down from fps·samples if configured) + the\n // editable composition. OFF ⇒ a plain copy (byte-identical to before).\n await mkdir(dirname(resolve(opts.outPath)), { recursive: true });\n const producedAbs = resolve(PKG_ROOT, produced);\n if (motionBlurActive(composition.motionBlur)) {\n await motionBlurMp4(\n producedAbs,\n resolve(opts.outPath),\n composition.output.fps,\n composition.motionBlur.samples,\n composition.motionBlur.shutter,\n );\n } else {\n await copyFile(producedAbs, resolve(opts.outPath));\n }\n const compositionPath = resolve(opts.outPath).replace(/\\.mp4$/i, \"\") + \".composition.json\";\n if (opts.writeCompositionSibling !== false) {\n // strip the render-time review decoration — the editable artifact is the\n // clean composition, never the badged/watermarked variant of it.\n const { review: _review, ...persisted } = composition;\n await writeFile(compositionPath, JSON.stringify(persisted, null, 2));\n }\n\n return { mp4Path: resolve(opts.outPath), compositionPath };\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@open-take/compositor",
3
+ "version": "0.1.0",
4
+ "description": "Polish compositor (D3) — event log + captured frames -> polished mp4 + editable revideo composition.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src/**/*.ts",
15
+ "src/**/*.tsx"
16
+ ],
17
+ "dependencies": {
18
+ "@revideo/2d": "0.10.4",
19
+ "@revideo/core": "0.10.4",
20
+ "@revideo/renderer": "0.10.4",
21
+ "@revideo/ui": "0.10.4",
22
+ "@revideo/vite-plugin": "0.10.4",
23
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
24
+ "@ffprobe-installer/ffprobe": "^2.1.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.10.1",
28
+ "tsdown": "^0.9.0",
29
+ "tsx": "^4.22.3",
30
+ "typescript": "^5.6.3"
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "keywords": [
39
+ "demo",
40
+ "screen-recording",
41
+ "video",
42
+ "agent",
43
+ "cdp",
44
+ "screencast"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/pascal910107/open-take.git",
49
+ "directory": "packages/compositor"
50
+ },
51
+ "homepage": "https://github.com/pascal910107/open-take#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/pascal910107/open-take/issues"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "node --test --import tsx/esm test/*.test.ts",
59
+ "clean": "rm -rf dist out-render public/capture.mp4 src/scene/.composition.json"
60
+ }
61
+ }
package/src/ffmpeg.ts ADDED
@@ -0,0 +1,60 @@
1
+ // ffmpeg/ffprobe resolution — zero-config for npm consumers. Prefer the
2
+ // system binaries on PATH (often newer/faster, and what the repo has always
3
+ // used); fall back to the platform binaries from @ffmpeg-installer/ffmpeg and
4
+ // @ffprobe-installer/ffprobe (direct deps here, and already in the tree via
5
+ // @revideo/ffmpeg — so the fallback costs consumers no extra download). Throw
6
+ // with an install hint only when neither exists.
7
+
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ let cachedFfmpeg: string | undefined;
11
+ let cachedFfprobe: string | undefined;
12
+
13
+ function runsOk(bin: string): boolean {
14
+ try {
15
+ return spawnSync(bin, ["-version"], { stdio: "ignore" }).status === 0;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ async function installerPath(pkg: string): Promise<string | null> {
22
+ try {
23
+ const m = (await import(pkg)) as { default?: { path?: string }; path?: string };
24
+ return m.default?.path ?? m.path ?? null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export async function resolveFfmpeg(): Promise<string> {
31
+ if (cachedFfmpeg) return cachedFfmpeg;
32
+ if (runsOk("ffmpeg")) {
33
+ cachedFfmpeg = "ffmpeg";
34
+ return cachedFfmpeg;
35
+ }
36
+ const p = await installerPath("@ffmpeg-installer/ffmpeg");
37
+ if (p && runsOk(p)) {
38
+ cachedFfmpeg = p;
39
+ return p;
40
+ }
41
+ throw new Error(
42
+ "ffmpeg not found — install it (e.g. `brew install ffmpeg`) or `npm install` so the bundled @ffmpeg-installer binary resolves for this platform",
43
+ );
44
+ }
45
+
46
+ export async function resolveFfprobe(): Promise<string> {
47
+ if (cachedFfprobe) return cachedFfprobe;
48
+ if (runsOk("ffprobe")) {
49
+ cachedFfprobe = "ffprobe";
50
+ return cachedFfprobe;
51
+ }
52
+ const p = await installerPath("@ffprobe-installer/ffprobe");
53
+ if (p && runsOk(p)) {
54
+ cachedFfprobe = p;
55
+ return p;
56
+ }
57
+ throw new Error(
58
+ "ffprobe not found — install ffmpeg (e.g. `brew install ffmpeg`) or `npm install` so the bundled @ffprobe-installer binary resolves for this platform",
59
+ );
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // @open-take/compositor — the polish engine (D3). Event log + captured
2
+ // frames -> polished mp4 + an editable revideo composition.
3
+ //
4
+ // const comp = planComposition(captureLog) // default editable plan
5
+ // await renderTake({ composition: comp, videoPath, outPath })
6
+ //
7
+ // Edit `comp` (zoom decisions, framing, cursor) and re-render — the
8
+ // composition is the editable source of truth.
9
+
10
+ export * from "./types";
11
+ export * from "./presets";
12
+ export { resolveFfmpeg, resolveFfprobe } from "./ffmpeg";
13
+ export { planComposition, type PlanOpts } from "./plan";
14
+ export { renderTake, type RenderTakeOpts } from "./render";
15
+ export {
16
+ validateComposition,
17
+ formatIssues,
18
+ type CompositionIssue,
19
+ type ValidateOpts,
20
+ } from "./validate";
21
+ export * as math from "./math";