| 1 | /** |
| 2 | * @html-video/core type definitions |
| 3 | * Implements RFC-01 (engine adapter) + RFC-02 (template metadata) + RFC-05 (project-centric workflow). |
| 4 | * See research/2026-05-{26,27}-spec-{01,02,05}-*.md. |
| 5 | * |
| 6 | * NOTE: Storyboard / Scene types from RFC-04 were removed in v0.1 |
| 7 | * after Joey's product clarification — see RFC-05. |
| 8 | */ |
| 9 | |
| 10 | // ============================================================================ |
| 11 | // RFC-01: Engine Adapter |
| 12 | // ============================================================================ |
| 13 | |
| 14 | export type EngineId = string; |
| 15 | |
| 16 | export type Paradigm = |
| 17 | | 'html-css-gsap' |
| 18 | | 'react-tsx' |
| 19 | | 'ts-generator' |
| 20 | | 'json-scene' |
| 21 | | 'imperative-canvas'; |
| 22 | |
| 23 | export type OutputFormat = 'mp4' | 'webm' | 'webm-alpha' | 'gif' | 'png-sequence' | 'apng'; |
| 24 | |
| 25 | export type RenderTarget = 'local-chromium' | 'local-canvas' | 'lambda' | 'cloud-run'; |
| 26 | |
| 27 | export type LicensingTier = 'free-osi' | 'commercial-restricted' | 'unknown'; |
| 28 | |
| 29 | export interface RenderSpeedHint { |
| 30 | resolution: string; |
| 31 | durationSec: number; |
| 32 | fps: number; |
| 33 | estimatedRenderSec: number; |
| 34 | } |
| 35 | |
| 36 | export interface EngineCapabilities { |
| 37 | paradigms: Paradigm[]; |
| 38 | outputFormats: OutputFormat[]; |
| 39 | maxResolution: { width: number; height: number }; |
| 40 | alpha: boolean; |
| 41 | audio: 'none' | 'single' | 'multi'; |
| 42 | subtitles: ('none' | 'burn-in' | 'sidecar')[]; |
| 43 | renderTarget: RenderTarget[]; |
| 44 | licensing: LicensingTier; |
| 45 | renderSpeedHint?: RenderSpeedHint; |
| 46 | bestFor: string[]; |
| 47 | weaknesses: string[]; |
| 48 | } |
| 49 | |
| 50 | export interface ValidationError { |
| 51 | code: string; |
| 52 | message: string; |
| 53 | fix?: string; |
| 54 | } |
| 55 | |
| 56 | export interface ValidationResult { |
| 57 | ok: boolean; |
| 58 | errors: ValidationError[]; |
| 59 | warnings: ValidationError[]; |
| 60 | } |
| 61 | |
| 62 | export interface RenderConfig { |
| 63 | format: OutputFormat; |
| 64 | resolution: { width: number; height: number }; |
| 65 | fps: number; |
| 66 | duration: number | 'auto'; |
| 67 | /** |
| 68 | * How to treat `duration`. 'explicit' = the user set a per-frame length; it is |
| 69 | * a hard cap, do NOT extend the recording to fit a longer animation. 'auto' |
| 70 | * (default) = a fallback the renderer may extend so an opening animation isn't |
| 71 | * cut mid-play. Multi-frame export sets 'explicit' (the format card collected a |
| 72 | * real per-frame value); single-frame fast preview leaves it 'auto'. |
| 73 | */ |
| 74 | durationMode?: 'explicit' | 'auto'; |
| 75 | outputPath: string; |
| 76 | alpha?: boolean; |
| 77 | quality?: number | 'low' | 'medium' | 'high' | 'lossless'; |
| 78 | audio?: { path: string; volumeDb?: number }[]; |
| 79 | } |
| 80 | |
| 81 | export interface RenderInput { |
| 82 | template: TemplateRef; |
| 83 | variables: Record<string, unknown>; |
| 84 | config: RenderConfig; |
| 85 | } |
| 86 | |
| 87 | export interface RenderContext { |
| 88 | workDir: string; |
| 89 | onProgress?: (pct: number, stage: string) => void; |
| 90 | signal?: AbortSignal; |
| 91 | env?: Record<string, string>; |
| 92 | } |
| 93 | |
| 94 | export interface RenderOutput { |
| 95 | outputPath: string; |
| 96 | meta: { |
| 97 | durationSec: number; |
| 98 | fileSizeBytes: number; |
| 99 | actualResolution: { width: number; height: number }; |
| 100 | fps: number; |
| 101 | renderedFrames: number; |
| 102 | renderWallClockSec: number; |
| 103 | engineVersion: string; |
| 104 | }; |
| 105 | diagnostics: string[]; |
| 106 | } |
| 107 | |
| 108 | export interface PreviewContext { |
| 109 | workDir: string; |
| 110 | hostname?: string; |
| 111 | port?: number; |
| 112 | } |
| 113 | |
| 114 | export interface PreviewHandle { |
| 115 | url: string; |
| 116 | port: number; |
| 117 | close(): Promise<void>; |
| 118 | } |
| 119 | |
| 120 | export interface NativeTemplateRef { |
| 121 | nativeId: string; |
| 122 | path: string; |
| 123 | hints?: { name?: string; description?: string; bestFor?: string[] }; |
| 124 | } |
| 125 | |
| 126 | export interface HtmlSceneOutput { |
| 127 | htmlPath: string; |
| 128 | referencedAssets: { assetId: string; usagePath: string }[]; |
| 129 | posterPath: string; |
| 130 | durationSec: number; |
| 131 | } |
| 132 | |
| 133 | export interface EngineAdapter { |
| 134 | id: EngineId; |
| 135 | name: string; |
| 136 | upstreamVersion: string; |
| 137 | capabilities: EngineCapabilities; |
| 138 | |
| 139 | validate(template: TemplateRef): ValidationResult; |
| 140 | render(input: RenderInput, ctx: RenderContext): Promise<RenderOutput>; |
| 141 | preview?(template: TemplateRef, ctx: PreviewContext): Promise<PreviewHandle>; |
| 142 | renderToHtml?(input: RenderInput, ctx: RenderContext): Promise<HtmlSceneOutput>; |
| 143 | listNativeTemplates?(): Promise<NativeTemplateRef[]>; |
| 144 | } |
| 145 | |
| 146 | // ============================================================================ |
| 147 | // RFC-02: Template Metadata |
| 148 | // ============================================================================ |
| 149 | |
| 150 | export type TemplateCategory = |
| 151 | | 'data-viz' |
| 152 | | 'social-shorts' |
| 153 | | 'product-demo' |
| 154 | | 'explainer' |
| 155 | | 'marketing' |
| 156 | | 'intro-outro' |
| 157 | | 'ambient' |
| 158 | | 'documentary' |
| 159 | | 'presentation' |
| 160 | | 'transition'; |
| 161 | |
| 162 | export interface OutputCapabilities { |
| 163 | formats: OutputFormat[]; |
| 164 | default_format: OutputFormat; |
| 165 | resolution: { |
| 166 | default: { width: number; height: number }; |
| 167 | supported_aspects: string[]; |
| 168 | }; |
| 169 | fps: { default: number; supported: number[] }; |
| 170 | duration: { |
| 171 | type: 'variable' | 'fixed'; |
| 172 | min_sec: number; |
| 173 | max_sec: number; |
| 174 | }; |
| 175 | alpha: boolean; |
| 176 | audio: { supported: boolean; expected_inputs?: string[] }; |
| 177 | } |
| 178 | |
| 179 | export interface LicenseInfo { |
| 180 | spdx: string; |
| 181 | attribution_required: boolean; |
| 182 | redistribution_allowed: boolean; |
| 183 | commercial_use: boolean; |
| 184 | notes?: string | null; |
| 185 | } |
| 186 | |
| 187 | export interface AssetAttribution { |
| 188 | name: string; |
| 189 | license: string; |
| 190 | author?: string; |
| 191 | url?: string; |
| 192 | } |
| 193 | |
| 194 | export interface ChangelogEntry { |
| 195 | version: string; |
| 196 | date: string; |
| 197 | notes: string; |
| 198 | } |
| 199 | |
| 200 | export interface PerformanceRef { |
| 201 | duration_sec: number; |
| 202 | render_wall_clock_sec: number; |
| 203 | machine: string; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Three-layer provenance (RFC-07). Records where a template's design actually |
| 208 | * came from so the studio can surface honest attribution: |
| 209 | * - origin — L1: the real-world design inspiration (a studio, person, or |
| 210 | * movement). `name: 'none'` / `kind: 'none'` when there is no |
| 211 | * specific upstream source (e.g. an original skill preset). |
| 212 | * - via_skill — L2: the open-source skill we actually transformed from; its |
| 213 | * license governs redistribution. author = the real copyright |
| 214 | * holder verified against the upstream LICENSE. |
| 215 | * - transformation — L3: what html-video changed (free-text). |
| 216 | */ |
| 217 | export interface ProvenanceOrigin { |
| 218 | name: string; |
| 219 | kind?: 'studio' | 'person' | 'movement' | 'none'; |
| 220 | reference?: string; |
| 221 | } |
| 222 | export interface ProvenanceViaSkill { |
| 223 | name: string; |
| 224 | author?: string; |
| 225 | url?: string; |
| 226 | license?: string; |
| 227 | source_file?: string; |
| 228 | } |
| 229 | export interface Provenance { |
| 230 | origin?: ProvenanceOrigin; |
| 231 | via_skill?: ProvenanceViaSkill; |
| 232 | transformation?: string; |
| 233 | } |
| 234 | |
| 235 | export interface TemplateMetadata { |
| 236 | spec_version: 1; |
| 237 | id: string; |
| 238 | name: string; |
| 239 | description: string; |
| 240 | engine: EngineId; |
| 241 | engine_version: string; |
| 242 | source_entry: string; |
| 243 | /** |
| 244 | * Native-engine templates (RFC-08 Phase 2): `source_entry` is a bundle entry |
| 245 | * (e.g. a Remotion `entry.ts` calling `registerRoot`) rather than an HTML |
| 246 | * file. `compositionId` is the `<Composition id>` the adapter selects. Absent |
| 247 | * for the classic HTML+CSS+GSAP templates (the 27 hyperframes ones). |
| 248 | */ |
| 249 | native?: { compositionId: string }; |
| 250 | category: TemplateCategory; |
| 251 | subcategory?: string; |
| 252 | tags: string[]; |
| 253 | best_for: string[]; |
| 254 | not_for?: string[]; |
| 255 | output: OutputCapabilities; |
| 256 | inputs: { schema: object; examples: object[] }; |
| 257 | license: LicenseInfo; |
| 258 | /** Three-layer design attribution (RFC-07). See {@link Provenance}. */ |
| 259 | provenance?: Provenance; |
| 260 | assets_attribution?: AssetAttribution[]; |
| 261 | author: { name: string; url?: string; contact?: string }; |
| 262 | maintainers?: { github: string }[]; |
| 263 | contributing?: { url: string }; |
| 264 | version: string; |
| 265 | changelog?: ChangelogEntry[]; |
| 266 | preview: { poster: string; loop?: string; thumbnail?: string }; |
| 267 | performance?: { reference_render: PerformanceRef }; |
| 268 | share_optimized_for?: string[]; |
| 269 | /** Internal: filesystem location of the template directory (set by registry) */ |
| 270 | __dir?: string; |
| 271 | } |
| 272 | |
| 273 | export interface TemplateRef { |
| 274 | id: string; |
| 275 | engine: EngineId; |
| 276 | /** Bridge mode: an HTML file. Native mode: a Remotion .tsx bundle entry. */ |
| 277 | sourcePath: string; |
| 278 | variables?: Record<string, unknown>; |
| 279 | /** |
| 280 | * How the engine should consume `sourcePath`. Absent ⇒ 'bridge' (the legacy |
| 281 | * path: render the given HTML). 'native' ⇒ the engine bundles `sourcePath` as |
| 282 | * its own component entry and renders `nativeCompositionId` directly (e.g. a |
| 283 | * Remotion React-tsx data-animation template fed real data via `variables`). |
| 284 | * Only the Remotion adapter honors 'native' today (RFC-08 Phase 2). |
| 285 | */ |
| 286 | mode?: 'bridge' | 'native'; |
| 287 | /** Native mode only: the Remotion `<Composition id>` to select after bundling. */ |
| 288 | nativeCompositionId?: string; |
| 289 | } |
| 290 | |
| 291 | // ============================================================================ |
| 292 | // RFC-05: Project-centric workflow |
| 293 | // ============================================================================ |
| 294 | |
| 295 | export type AssetType = 'image' | 'text' | 'data' | 'audio' | 'video' | 'reference-link'; |
| 296 | |
| 297 | export interface Asset { |
| 298 | id: string; |
| 299 | type: AssetType; |
| 300 | path?: string; |
| 301 | content?: string; |
| 302 | metadata: { |
| 303 | filename?: string; |
| 304 | mimeType?: string; |
| 305 | sizeBytes?: number; |
| 306 | width?: number; |
| 307 | height?: number; |
| 308 | durationSec?: number; |
| 309 | userCaption?: string; |
| 310 | }; |
| 311 | userTags: string[]; |
| 312 | } |
| 313 | |
| 314 | export interface UserPreferences { |
| 315 | aspect?: string; |
| 316 | durationTargetSec?: number; |
| 317 | format?: 'mp4' | 'webm'; |
| 318 | resolution?: { width: number; height: number }; |
| 319 | fps?: number; |
| 320 | mood?: string; |
| 321 | brandColors?: string[]; |
| 322 | fontFamilies?: string[]; |
| 323 | language?: string; |
| 324 | commercial?: boolean; |
| 325 | } |
| 326 | |
| 327 | export type ProjectStatus = 'draft' | 'previewed' | 'rendered'; |
| 328 | |
| 329 | /** |
| 330 | * v0.8: a single rendered HTML frame in a multi-frame project. |
| 331 | * Maps 1:1 to a node in the project's contentGraph (graphNodeId). |
| 332 | */ |
| 333 | export interface FrameRecord { |
| 334 | /** Stable id, mirrors the graph node id */ |
| 335 | graphNodeId: string; |
| 336 | /** Absolute path to the rendered HTML file (e.g. .../frames/01-intro.html) */ |
| 337 | htmlPath: string; |
| 338 | /** Playback duration for this frame, seconds */ |
| 339 | durationSec: number; |
| 340 | /** Optional poster image (first-frame thumbnail) */ |
| 341 | posterPath?: string; |
| 342 | /** 0-based index in topo-sorted play order */ |
| 343 | order: number; |
| 344 | /** |
| 345 | * Per-frame engine override (RFC-08/09). Absent ⇒ inherit the project's |
| 346 | * template engine (hyperframes). Set to 'remotion' when the user explicitly |
| 347 | * opts this frame into a native motion-enhancement. The base render at |
| 348 | * `htmlPath` is always retained so toggling enhancement off is non-destructive. |
| 349 | */ |
| 350 | engine?: EngineId; |
| 351 | /** |
| 352 | * When enhanced: which native template renders this frame (e.g. |
| 353 | * 'frame-data-rollup'). Resolved against the TemplateRegistry at export time. |
| 354 | */ |
| 355 | nativeTemplateId?: string; |
| 356 | /** |
| 357 | * When enhanced: snapshot of the source content-graph DataNode's `data`, |
| 358 | * bound at enhance time and fed to the native template as inputProps. Kept on |
| 359 | * the frame so export is self-contained and doesn't re-read the graph. |
| 360 | */ |
| 361 | data?: unknown; |
| 362 | /** |
| 363 | * When enhanced: path to a short single-frame MP4 the studio renders so the |
| 364 | * user can preview the native animation before a full export (a native frame |
| 365 | * has no HTML to load in an iframe). Cleared on unenhance. Separate from the |
| 366 | * export loop's `frames/NN.mp4` so the two lifecycles don't collide. |
| 367 | */ |
| 368 | previewMp4Path?: string; |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * v0.9: project-level soundtrack — one background music track + one narration |
| 373 | * track mixed into the exported MP4. Both reference an entry in `assets[]` |
| 374 | * (type 'audio'); this struct only holds the ids + mix preferences, so the |
| 375 | * audio bytes live in the normal asset store. Per-frame audio is a v2 concern. |
| 376 | */ |
| 377 | export interface ProjectSoundtrack { |
| 378 | /** asset.id of the background-music track (type 'audio'), if generated */ |
| 379 | musicAssetId?: string; |
| 380 | /** asset.id of the narration / voiceover track, if generated */ |
| 381 | narrationAssetId?: string; |
| 382 | /** Background-music gain in dB applied at mux time (default -18, pushed under voice) */ |
| 383 | musicVolumeDb?: number; |
| 384 | /** Narration gain in dB (default 0) */ |
| 385 | narrationVolumeDb?: number; |
| 386 | /** Last music style prompt used — kept so the UI can show / re-run it */ |
| 387 | musicPrompt?: string; |
| 388 | /** Last narration text used (the stitched full script) */ |
| 389 | narrationText?: string; |
| 390 | /** Per-frame narration: { [graphNodeId]: line }. The UI edits/shows narration |
| 391 | * per selected frame; narrationText is these stitched in frame order. */ |
| 392 | narrationByFrame?: Record<string, string>; |
| 393 | /** Optional music fade-in seconds at the start of the video */ |
| 394 | fadeInSec?: number; |
| 395 | /** Optional music fade-out seconds at the end of the video */ |
| 396 | fadeOutSec?: number; |
| 397 | } |
| 398 | |
| 399 | export interface Project { |
| 400 | id: string; |
| 401 | name: string; |
| 402 | intent?: string; |
| 403 | assets: Asset[]; |
| 404 | templateId: string | null; |
| 405 | /** Agent runtime to use (detected agent id, e.g. "claude" / "cursor-agent"). null = default first available */ |
| 406 | agentId?: string | null; |
| 407 | /** Model id for agents that support model selection (e.g. AMR: deepseek-v4-flash, |
| 408 | * claude-opus-4.8…). null = agent's default. */ |
| 409 | agentModel?: string | null; |
| 410 | /** |
| 411 | * Free-form variables (RFC-02 inputs.schema compatible). |
| 412 | * v0.3+: deprecated as the user-facing primary surface — agents now produce HTML directly. |
| 413 | * Kept for adapter render() backward compatibility (engine still expects vars). |
| 414 | */ |
| 415 | variables: Record<string, unknown>; |
| 416 | preferences: UserPreferences; |
| 417 | status: ProjectStatus; |
| 418 | /** Path to the latest agent-generated HTML (v0.3 chat-to-HTML pipeline; single-frame fast path) */ |
| 419 | lastPreviewHtmlPath?: string; |
| 420 | lastPreviewPosterPath?: string; |
| 421 | lastOutputMp4Path?: string; |
| 422 | /** Export history — every MP4 exported for this project, newest last. Each |
| 423 | * export writes a uniquely-named file so older ones aren't overwritten. */ |
| 424 | exports?: Array<{ path: string; createdAt: string; filename: string }>; |
| 425 | /** |
| 426 | * v0.8: path to content-graph.json for multi-frame projects. |
| 427 | * Absent for single-frame fast-path projects. |
| 428 | */ |
| 429 | contentGraphPath?: string; |
| 430 | /** |
| 431 | * v0.8: rendered frame sequence in topo-sorted play order. |
| 432 | * Empty for single-frame fast-path projects. |
| 433 | */ |
| 434 | frames?: FrameRecord[]; |
| 435 | /** v0.9: optional background music + narration mixed into the export. */ |
| 436 | soundtrack?: ProjectSoundtrack; |
| 437 | createdAt: string; |
| 438 | updatedAt: string; |
| 439 | } |
| 440 |