返回 html-video
index.ts
根目录 / packages / core / src / types / index.ts
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
440 lines TYPESCRIPT