frameyard 0.6.0 → 0.8.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +64 -55
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +255 -59
  7. package/dist/cli.js.map +1 -1
  8. package/dist/composition/attributes.d.ts.map +1 -1
  9. package/dist/composition/descriptor.d.ts +17 -3
  10. package/dist/composition/descriptor.d.ts.map +1 -1
  11. package/dist/composition/descriptor.js +51 -28
  12. package/dist/composition/descriptor.js.map +1 -1
  13. package/dist/composition/diagnostics.js.map +1 -1
  14. package/dist/composition/format.d.ts +12 -0
  15. package/dist/composition/format.d.ts.map +1 -0
  16. package/dist/composition/format.js +28 -0
  17. package/dist/composition/format.js.map +1 -0
  18. package/dist/composition/index.d.ts +2 -1
  19. package/dist/composition/index.d.ts.map +1 -1
  20. package/dist/composition/index.js +1 -0
  21. package/dist/composition/index.js.map +1 -1
  22. package/dist/contracts.d.ts +55 -6
  23. package/dist/contracts.d.ts.map +1 -1
  24. package/dist/contracts.js +3 -0
  25. package/dist/contracts.js.map +1 -1
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/doctor.js +79 -0
  28. package/dist/doctor.js.map +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/html/document-adapter.d.ts +3 -3
  33. package/dist/html/document-adapter.d.ts.map +1 -1
  34. package/dist/html/document-adapter.js +1 -1
  35. package/dist/html/document-adapter.js.map +1 -1
  36. package/dist/html/index.d.ts +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/parse.d.ts +10 -2
  39. package/dist/html/parse.d.ts.map +1 -1
  40. package/dist/html/parse.js +14 -5
  41. package/dist/html/parse.js.map +1 -1
  42. package/dist/html/validate.d.ts +12 -1
  43. package/dist/html/validate.d.ts.map +1 -1
  44. package/dist/html/validate.js +233 -61
  45. package/dist/html/validate.js.map +1 -1
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/preview/main.d.ts +2 -2
  51. package/dist/preview/main.d.ts.map +1 -1
  52. package/dist/preview/main.js +47 -19
  53. package/dist/preview/main.js.map +1 -1
  54. package/dist/preview/timeline-data.d.ts +5 -1
  55. package/dist/preview/timeline-data.d.ts.map +1 -1
  56. package/dist/preview/timeline-host.d.ts +2 -0
  57. package/dist/preview/timeline-host.d.ts.map +1 -1
  58. package/dist/preview/timeline-host.js +31 -10
  59. package/dist/preview/timeline-host.js.map +1 -1
  60. package/dist/preview/timeline-main.js +12 -7
  61. package/dist/preview/timeline-main.js.map +1 -1
  62. package/dist/project.d.ts +3 -5
  63. package/dist/project.d.ts.map +1 -1
  64. package/dist/project.js.map +1 -1
  65. package/dist/renderer/browser.d.ts +2 -2
  66. package/dist/renderer/browser.d.ts.map +1 -1
  67. package/dist/renderer/browser.js +10 -4
  68. package/dist/renderer/browser.js.map +1 -1
  69. package/dist/renderer/ffmpeg.d.ts +69 -2
  70. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  71. package/dist/renderer/ffmpeg.js +381 -69
  72. package/dist/renderer/ffmpeg.js.map +1 -1
  73. package/dist/renderer/index.d.ts +66 -5
  74. package/dist/renderer/index.d.ts.map +1 -1
  75. package/dist/renderer/index.js +226 -51
  76. package/dist/renderer/index.js.map +1 -1
  77. package/dist/renderer/timeline.d.ts +41 -4
  78. package/dist/renderer/timeline.d.ts.map +1 -1
  79. package/dist/renderer/timeline.js +242 -39
  80. package/dist/renderer/timeline.js.map +1 -1
  81. package/dist/runtime/host.d.ts.map +1 -1
  82. package/dist/runtime/host.js +155 -31
  83. package/dist/runtime/host.js.map +1 -1
  84. package/dist/runtime/media.d.ts.map +1 -1
  85. package/dist/runtime/media.js +3 -6
  86. package/dist/runtime/media.js.map +1 -1
  87. package/dist/runtime/video.d.ts +13 -0
  88. package/dist/runtime/video.d.ts.map +1 -0
  89. package/dist/runtime/video.js +41 -0
  90. package/dist/runtime/video.js.map +1 -0
  91. package/dist/scaffold.d.ts.map +1 -1
  92. package/dist/scaffold.js +107 -103
  93. package/dist/scaffold.js.map +1 -1
  94. package/dist/server/composition-html.d.ts +13 -7
  95. package/dist/server/composition-html.d.ts.map +1 -1
  96. package/dist/server/composition-html.js +65 -16
  97. package/dist/server/composition-html.js.map +1 -1
  98. package/dist/server/preview-html.js +1 -1
  99. package/dist/server/server.d.ts.map +1 -1
  100. package/dist/server/server.js +26 -6
  101. package/dist/server/server.js.map +1 -1
  102. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  103. package/dist/server/timeline-preview-data.js +11 -0
  104. package/dist/server/timeline-preview-data.js.map +1 -1
  105. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  106. package/dist/server/timeline-preview-html.js +2 -1
  107. package/dist/server/timeline-preview-html.js.map +1 -1
  108. package/dist/timeline/plan.js.map +1 -1
  109. package/dist/timeline/resolve.d.ts +4 -3
  110. package/dist/timeline/resolve.d.ts.map +1 -1
  111. package/dist/timeline/resolve.js +14 -4
  112. package/dist/timeline/resolve.js.map +1 -1
  113. package/dist/timeline/schema.d.ts +7 -0
  114. package/dist/timeline/schema.d.ts.map +1 -1
  115. package/dist/timeline/schema.js +50 -0
  116. package/dist/timeline/schema.js.map +1 -1
  117. package/dist/transcript.js.map +1 -1
  118. package/dist/version.d.ts +1 -1
  119. package/dist/version.js +1 -1
  120. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  121. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  122. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  123. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  124. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  125. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  126. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  127. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  128. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  129. package/docs/migration-0.4.md +3 -1
  130. package/docs/migration-0.8.md +163 -0
  131. package/docs/preview-testing.md +60 -0
  132. package/docs/v0.1-design.md +52 -42
  133. package/examples/blend/README.md +106 -0
  134. package/examples/blend/assets/ink.mp4 +0 -0
  135. package/examples/blend/compositions/aurora.html +55 -0
  136. package/examples/blend/compositions/backdrop.html +74 -0
  137. package/examples/blend/compositions/labels.html +149 -0
  138. package/examples/blend/scripts/make-ink.sh +26 -0
  139. package/examples/blend/timelines/blend.timeline.json +155 -0
  140. package/examples/blend/video.config.js +11 -0
  141. package/examples/demo/compositions/demo.html +243 -251
  142. package/examples/static/README.md +17 -0
  143. package/examples/static/compositions/still.html +37 -0
  144. package/examples/static/timelines/still.timeline.json +13 -0
  145. package/examples/static/video.config.js +9 -0
  146. package/examples/video/compositions/video-mp4.html +349 -357
  147. package/examples/video/compositions/video-overlay.html +337 -345
  148. package/examples/video/compositions/video.html +349 -357
  149. package/examples/webgl/compositions/webgl.html +205 -213
  150. package/package.json +6 -3
  151. package/skills/frameyard/README.md +7 -5
  152. package/skills/frameyard/SKILL.md +17 -13
  153. package/skills/frameyard/references/common-mistakes.md +30 -6
  154. package/skills/frameyard/references/controllers.md +5 -4
  155. package/skills/frameyard/references/deterministic-authoring.md +42 -36
  156. package/skills/frameyard/references/diagnostics.md +11 -3
  157. package/skills/frameyard/references/failure-recovery.md +28 -7
  158. package/skills/frameyard/references/json-results.md +58 -7
  159. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  160. package/skills/frameyard/references/timelines.md +21 -15
  161. package/skills/frameyard/scripts/frameyard.mjs +20 -3
@@ -1,6 +1,7 @@
1
- import type { CompositionDiagnostic, CompositionMetadata } from "../contracts.js";
1
+ import { type CompositionDiagnostic, type CompositionMetadata, type StaticCompositionMetadata } from "../contracts.js";
2
2
  import type { Project } from "../project.js";
3
3
  import { type AudioInspection } from "./audio.js";
4
+ import { type VideoCodec, type VideoContainer, type VideoEncoderChoice, type VideoEncoderName } from "./ffmpeg.js";
4
5
  import { type TranscriptInspection } from "./transcripts.js";
5
6
  export interface ProgressEvent {
6
7
  frame: number;
@@ -14,7 +15,8 @@ interface SessionOptions {
14
15
  signal?: AbortSignal;
15
16
  onProgress?: (event: ProgressEvent) => void;
16
17
  }
17
- export interface CheckResult {
18
+ export interface AnimatedCheckResult {
19
+ kind: "animated";
18
20
  composition: string;
19
21
  entry: string;
20
22
  controller: {
@@ -36,7 +38,8 @@ export interface CheckResult {
36
38
  diagnostics: CompositionDiagnostic[];
37
39
  elapsedMs: number;
38
40
  }
39
- export interface StillResult {
41
+ export interface StaticCheckResult {
42
+ kind: "static";
40
43
  composition: string;
41
44
  entry: string;
42
45
  controller: {
@@ -45,14 +48,37 @@ export interface StillResult {
45
48
  kind: "external";
46
49
  src: string;
47
50
  } | null;
51
+ metadata: StaticCompositionMetadata;
52
+ framesChecked: 1;
53
+ diagnostics: CompositionDiagnostic[];
54
+ elapsedMs: number;
55
+ }
56
+ export type CheckResult = AnimatedCheckResult | StaticCheckResult;
57
+ export interface StillFrameResult {
48
58
  frame: number;
49
59
  path: string;
60
+ }
61
+ export interface StillResult {
62
+ composition: string;
63
+ entry: string;
64
+ controller: {
65
+ kind: "inline";
66
+ } | {
67
+ kind: "external";
68
+ src: string;
69
+ } | null;
70
+ /** Present only when exactly one frame was captured. */
71
+ frame?: number;
72
+ /** Present only when exactly one frame was captured. */
73
+ path?: string;
74
+ frames: StillFrameResult[];
50
75
  width: number;
51
76
  height: number;
52
77
  diagnostics: CompositionDiagnostic[];
53
78
  elapsedMs: number;
54
79
  }
55
- export interface RenderResult {
80
+ export interface VideoRenderResult {
81
+ kind: "video";
56
82
  composition: string;
57
83
  entry: string;
58
84
  controller: {
@@ -69,6 +95,12 @@ export interface RenderResult {
69
95
  durationSeconds: number;
70
96
  framesRendered: number;
71
97
  concurrency: number;
98
+ /** Resolved delivery codec. */
99
+ codec: VideoCodec;
100
+ /** Resolved delivery container. */
101
+ container: VideoContainer;
102
+ /** Resolved encoder used for the output. */
103
+ encoder: VideoEncoderName;
72
104
  bytes: number;
73
105
  audio: {
74
106
  clips: number;
@@ -79,19 +111,48 @@ export interface RenderResult {
79
111
  diagnostics: CompositionDiagnostic[];
80
112
  elapsedMs: number;
81
113
  }
114
+ export interface ImageRenderResult {
115
+ kind: "image";
116
+ composition: string;
117
+ entry: string;
118
+ controller: {
119
+ kind: "inline";
120
+ } | {
121
+ kind: "external";
122
+ src: string;
123
+ } | null;
124
+ path: string;
125
+ width: number;
126
+ height: number;
127
+ bytes: number;
128
+ diagnostics: CompositionDiagnostic[];
129
+ elapsedMs: number;
130
+ }
131
+ export type RenderResult = VideoRenderResult | ImageRenderResult;
82
132
  export declare const MAX_RENDER_CONCURRENCY = 16;
83
133
  /** Validates the render concurrency flag and returns its normalized value. */
84
134
  export declare function validateRenderConcurrency(value: number | undefined): number;
85
135
  export declare function checkComposition(options: SessionOptions): Promise<CheckResult>;
136
+ export declare function captureStills(options: SessionOptions & {
137
+ frames: number[];
138
+ outPaths: string[];
139
+ overwrite?: boolean;
140
+ }): Promise<StillResult>;
86
141
  export declare function captureStill(options: SessionOptions & {
87
142
  frame: number;
88
143
  outPath: string;
89
144
  overwrite?: boolean;
90
145
  }): Promise<StillResult>;
91
146
  export declare function renderComposition(options: SessionOptions & {
92
- outPath: string;
147
+ outPath?: string;
93
148
  overwrite?: boolean;
94
149
  concurrency?: number;
150
+ /** Encoder choice; defaults to the codec's software encoder. */
151
+ encoder?: VideoEncoderChoice;
152
+ /** Delivery codec; defaults to `h264`. */
153
+ codec?: VideoCodec;
154
+ /** Delivery container; defaults to `mp4`. */
155
+ container?: VideoContainer;
95
156
  }): Promise<RenderResult>;
96
157
  export {};
97
158
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"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"}
@@ -2,12 +2,14 @@ import { link, mkdir, mkdtemp, rename, rm, stat, writeFile } from "node:fs/promi
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
+ import { COMPOSITION_FORMAT_UNRESOLVED_MESSAGE } from "../composition/index.js";
6
+ import { isPlainObject, } from "../contracts.js";
5
7
  import { FrameyardError, isFrameyardError } from "../errors.js";
6
8
  import { validateHtmlComposition } from "../html/validate.js";
7
9
  import { startPreviewServer } from "../server/server.js";
8
10
  import { inspectTimelineAudio } from "./audio.js";
9
11
  import { openCompositionSession } from "./browser.js";
10
- import { assertFfmpegCapabilities, mixAudioTimeline, startVideoEncoder } from "./ffmpeg.js";
12
+ import { assertContainerCodec, assertFfmpegCapabilities, CONTAINER_EXTENSION, defaultCodecForContainer, mixAudioTimeline, resolveVideoEncoder, startVideoEncoder, } from "./ffmpeg.js";
11
13
  import { inspectTranscriptClips } from "./transcripts.js";
12
14
  export const MAX_RENDER_CONCURRENCY = 16;
13
15
  /** Validates the render concurrency flag and returns its normalized value. */
@@ -55,6 +57,21 @@ function mergeDiagnostics(...groups) {
55
57
  }
56
58
  return result;
57
59
  }
60
+ function withBatchContext(error, frames, failedFrame) {
61
+ if (!isFrameyardError(error)) {
62
+ return error;
63
+ }
64
+ const details = error.details === undefined
65
+ ? {}
66
+ : isPlainObject(error.details)
67
+ ? { ...error.details }
68
+ : { originalDetails: error.details };
69
+ return new FrameyardError(error.code, error.message, {
70
+ cause: error,
71
+ details: { ...details, frames: [...frames], failedFrame },
72
+ diagnostics: error.diagnostics,
73
+ });
74
+ }
58
75
  async function readSessionDiagnostics(sessions) {
59
76
  const groups = await Promise.all(sessions.map((session) => session.readDiagnostics()));
60
77
  return mergeDiagnostics(...groups);
@@ -79,6 +96,25 @@ async function ensureOutputAvailable(outPath, overwrite) {
79
96
  }
80
97
  await mkdir(path.dirname(outPath), { recursive: true });
81
98
  }
99
+ async function assertOutputsAvailable(outPaths, overwrite) {
100
+ if (overwrite) {
101
+ return;
102
+ }
103
+ const conflicts = [];
104
+ for (const outPath of outPaths) {
105
+ if (await pathExists(outPath)) {
106
+ conflicts.push(outPath);
107
+ }
108
+ }
109
+ if (conflicts.length === 0) {
110
+ return;
111
+ }
112
+ const first = conflicts[0];
113
+ const message = conflicts.length === 1
114
+ ? `Output already exists: ${first}. Pass --overwrite to replace it.`
115
+ : `Output already exists: ${first} (and ${conflicts.length - 1} more). Pass --overwrite to replace them.`;
116
+ throw new FrameyardError("OUTPUT_EXISTS", message, { details: { conflicts } });
117
+ }
82
118
  async function publishFile(tempPath, outPath, overwrite) {
83
119
  if (overwrite) {
84
120
  await rename(tempPath, outPath);
@@ -100,21 +136,28 @@ async function publishFile(tempPath, outPath, overwrite) {
100
136
  await rm(tempPath, { force: true });
101
137
  }
102
138
  }
103
- async function withSessions(options, resolveSessionCount, callback) {
139
+ async function prepareComposition(options) {
104
140
  throwIfAborted(options.signal);
105
141
  const composition = compositionFor(options.project, options.compositionId);
106
142
  const validation = await validateHtmlComposition({
107
143
  projectRoot: options.project.root,
108
144
  entry: composition.entry,
109
145
  props: options.props,
146
+ ...(options.project.format ? { format: options.project.format } : {}),
110
147
  });
111
148
  const staticDiagnostics = validation.diagnostics;
112
149
  const staticErrors = staticDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
113
150
  if (!validation.info || staticErrors.length > 0) {
114
151
  const first = staticErrors[0];
152
+ if (!first && validation.formatUnresolved) {
153
+ throw new FrameyardError("COMPOSITION_FORMAT_UNRESOLVED", `Composition "${options.compositionId}": ${COMPOSITION_FORMAT_UNRESOLVED_MESSAGE}`, { diagnostics: validation.diagnostics });
154
+ }
115
155
  throw new FrameyardError(first?.code ?? "COMPOSITION_INVALID", `Composition "${options.compositionId}": ${first?.message ?? "static validation failed."}`, { diagnostics: validation.diagnostics });
116
156
  }
117
- const staticInfo = validation.info;
157
+ return { composition, staticInfo: validation.info, staticDiagnostics };
158
+ }
159
+ async function withPreparedSessions(options, prepared, resolveSessionCount, callback) {
160
+ const { composition, staticInfo, staticDiagnostics } = prepared;
118
161
  const server = await startPreviewServer({ projectRoot: options.project.root, port: 0 });
119
162
  const sessions = [];
120
163
  try {
@@ -176,9 +219,6 @@ async function withSessions(options, resolveSessionCount, callback) {
176
219
  }
177
220
  }
178
221
  }
179
- async function withSession(options, callback) {
180
- return withSessions(options, () => 1, async (sessions) => callback(sessions[0]));
181
- }
182
222
  function reportProgress(options, frame, total, startedAt) {
183
223
  options.onProgress?.({ frame, total, elapsedMs: performance.now() - startedAt });
184
224
  }
@@ -264,12 +304,31 @@ async function captureFramesInOrder(options) {
264
304
  }
265
305
  export async function checkComposition(options) {
266
306
  const startedAt = performance.now();
267
- return withSession(options, async (session) => {
268
- const total = session.metadata.durationInFrames;
307
+ const prepared = await prepareComposition(options);
308
+ return withPreparedSessions(options, prepared, () => 1, async (sessions) => {
309
+ const session = sessions[0];
310
+ const metadata = session.metadata;
311
+ if (metadata.static === true) {
312
+ throwIfAborted(options.signal);
313
+ await session.seek(0);
314
+ reportProgress(options, 1, 1, startedAt);
315
+ const diagnostics = mergeDiagnostics(prepared.staticDiagnostics, await session.readDiagnostics());
316
+ return {
317
+ kind: "static",
318
+ composition: options.compositionId,
319
+ entry: session.entry,
320
+ controller: session.controller,
321
+ metadata,
322
+ framesChecked: 1,
323
+ diagnostics,
324
+ elapsedMs: performance.now() - startedAt,
325
+ };
326
+ }
327
+ const total = metadata.durationInFrames;
269
328
  const audio = await inspectTimelineAudio({
270
329
  projectRoot: options.project.root,
271
330
  clips: timelineAudioClips(session),
272
- fps: session.metadata.fps,
331
+ fps: metadata.fps,
273
332
  durationInFrames: total,
274
333
  signal: options.signal,
275
334
  });
@@ -285,10 +344,11 @@ export async function checkComposition(options) {
285
344
  }
286
345
  const diagnostics = mergeDiagnostics(await session.readDiagnostics(), audioDiagnostics(audio.warnings), transcriptDiagnostics(transcripts.warnings));
287
346
  return {
347
+ kind: "animated",
288
348
  composition: options.compositionId,
289
349
  entry: session.entry,
290
350
  controller: session.controller,
291
- metadata: session.metadata,
351
+ metadata,
292
352
  framesChecked: total,
293
353
  audio: { clips: audio.clips.length, details: audio.clips },
294
354
  transcripts: {
@@ -300,63 +360,171 @@ export async function checkComposition(options) {
300
360
  };
301
361
  });
302
362
  }
303
- export async function captureStill(options) {
304
- if (!Number.isInteger(options.frame) || options.frame < 0) {
305
- throw new FrameyardError("FRAME_OUT_OF_RANGE", `--frame must be a non-negative integer; received ${options.frame}.`);
363
+ export async function captureStills(options) {
364
+ if (options.frames.length === 0) {
365
+ throw new FrameyardError("USAGE", "Still capture requires at least one frame.");
366
+ }
367
+ if (options.frames.length !== options.outPaths.length) {
368
+ throw new FrameyardError("USAGE", "Still frames and output paths must have the same length.");
306
369
  }
307
- if (path.extname(options.outPath).toLowerCase() !== ".png") {
308
- throw new FrameyardError("USAGE", `Still output must use a .png extension: ${options.outPath}`);
370
+ for (const frame of options.frames) {
371
+ if (!Number.isInteger(frame) || frame < 0) {
372
+ throw new FrameyardError("FRAME_OUT_OF_RANGE", `--frame must be a non-negative integer; received ${frame}.`);
373
+ }
374
+ }
375
+ if (new Set(options.frames).size !== options.frames.length) {
376
+ throw new FrameyardError("USAGE", "Still frames must be unique.");
377
+ }
378
+ if (new Set(options.outPaths).size !== options.outPaths.length) {
379
+ throw new FrameyardError("USAGE", "Still output paths must be unique.");
380
+ }
381
+ for (const outPath of options.outPaths) {
382
+ if (path.extname(outPath).toLowerCase() !== ".png") {
383
+ throw new FrameyardError("USAGE", `Still output must use a .png extension: ${outPath}`);
384
+ }
309
385
  }
310
386
  const overwrite = options.overwrite ?? false;
311
- await ensureOutputAvailable(options.outPath, overwrite);
312
387
  const startedAt = performance.now();
313
- return withSession(options, async (session) => {
314
- const total = session.metadata.durationInFrames;
315
- if (options.frame >= total) {
316
- throw new FrameyardError("FRAME_OUT_OF_RANGE", `Frame ${options.frame} is outside the valid range 0..${total - 1}.`);
317
- }
388
+ const prepared = await prepareComposition(options);
389
+ const metadata = prepared.staticInfo.metadata;
390
+ const total = metadata.static === true ? 1 : metadata.durationInFrames;
391
+ const invalidFrames = options.frames.filter((frame) => frame >= total);
392
+ if (invalidFrames.length > 0) {
393
+ const first = invalidFrames[0];
394
+ throw new FrameyardError("FRAME_OUT_OF_RANGE", `Frame ${first} is outside the valid range 0..${total - 1}.`, {
395
+ details: { invalidFrames, validRange: { min: 0, max: total - 1 } },
396
+ });
397
+ }
398
+ await assertOutputsAvailable(options.outPaths, overwrite);
399
+ for (const outPath of options.outPaths) {
400
+ await mkdir(path.dirname(outPath), { recursive: true });
401
+ }
402
+ return withPreparedSessions(options, prepared, () => 1, async (sessions) => {
403
+ const session = sessions[0];
318
404
  const transcripts = await inspectTranscriptClips({
319
405
  projectRoot: options.project.root,
320
406
  clips: session.transcriptClips,
321
407
  signal: options.signal,
322
408
  });
323
- throwIfAborted(options.signal);
324
- await session.seek(options.frame);
325
- const png = await session.capture();
326
- const diagnostics = mergeDiagnostics(await session.readDiagnostics(), transcriptDiagnostics(transcripts.warnings));
327
- const tempPath = path.join(path.dirname(options.outPath), `.${path.basename(options.outPath)}.frameyard-${process.pid}-${randomUUID()}.tmp`);
409
+ const tempPaths = options.outPaths.map((outPath) => path.join(path.dirname(outPath), `.${path.basename(outPath)}.frameyard-${process.pid}-${randomUUID()}.tmp`));
410
+ try {
411
+ for (const [index, frame] of options.frames.entries()) {
412
+ throwIfAborted(options.signal);
413
+ try {
414
+ await session.seek(frame);
415
+ const png = await session.capture();
416
+ await writeFile(tempPaths[index], png);
417
+ }
418
+ catch (error) {
419
+ throw withBatchContext(error, options.frames, frame);
420
+ }
421
+ }
422
+ await assertOutputsAvailable(options.outPaths, overwrite);
423
+ const diagnostics = mergeDiagnostics(metadata.static === true ? prepared.staticDiagnostics : [], await session.readDiagnostics(), transcriptDiagnostics(transcripts.warnings));
424
+ for (const [index, outPath] of options.outPaths.entries()) {
425
+ await publishFile(tempPaths[index], outPath, overwrite);
426
+ }
427
+ const frames = options.frames.map((frame, index) => ({
428
+ frame,
429
+ path: options.outPaths[index],
430
+ }));
431
+ const first = frames[0];
432
+ return {
433
+ composition: options.compositionId,
434
+ entry: session.entry,
435
+ controller: session.controller,
436
+ ...(frames.length === 1 && first !== undefined ? { frame: first.frame, path: first.path } : {}),
437
+ frames,
438
+ width: session.metadata.width,
439
+ height: session.metadata.height,
440
+ diagnostics,
441
+ elapsedMs: performance.now() - startedAt,
442
+ };
443
+ }
444
+ finally {
445
+ await Promise.all(tempPaths.map((tempPath) => rm(tempPath, { force: true })));
446
+ }
447
+ });
448
+ }
449
+ export async function captureStill(options) {
450
+ const { frame, outPath, ...rest } = options;
451
+ return captureStills({
452
+ ...rest,
453
+ frames: [frame],
454
+ outPaths: [outPath],
455
+ });
456
+ }
457
+ async function renderStaticComposition(options, prepared, startedAt) {
458
+ if (options.container !== undefined ||
459
+ options.codec !== undefined ||
460
+ options.encoder !== undefined ||
461
+ options.concurrency !== undefined) {
462
+ throw new FrameyardError("USAGE", "Static compositions render to PNG; --container, --codec, --encoder, and --concurrency are not valid.");
463
+ }
464
+ const metadata = prepared.staticInfo.metadata;
465
+ if (metadata.static !== true) {
466
+ throw new FrameyardError("INTERNAL", "renderStaticComposition received an animated composition.");
467
+ }
468
+ const outPath = options.outPath ?? path.join(options.project.root, "out", `${options.compositionId}.png`);
469
+ if (path.extname(outPath).toLowerCase() !== ".png") {
470
+ throw new FrameyardError("USAGE", `Static compositions render to a .png file: ${outPath}`);
471
+ }
472
+ const overwrite = options.overwrite ?? false;
473
+ await ensureOutputAvailable(outPath, overwrite);
474
+ return withPreparedSessions(options, prepared, () => 1, async (sessions) => {
475
+ const session = sessions[0];
476
+ const tempPath = path.join(path.dirname(outPath), `.${path.basename(outPath)}.frameyard-${process.pid}-${randomUUID()}.tmp`);
328
477
  try {
478
+ throwIfAborted(options.signal);
479
+ await session.seek(0);
480
+ const png = await session.capture();
329
481
  await writeFile(tempPath, png);
330
- await ensureOutputAvailable(options.outPath, overwrite);
331
- await publishFile(tempPath, options.outPath, overwrite);
482
+ await assertOutputsAvailable([outPath], overwrite);
483
+ await publishFile(tempPath, outPath, overwrite);
484
+ reportProgress(options, 1, 1, startedAt);
485
+ const diagnostics = mergeDiagnostics(prepared.staticDiagnostics, await session.readDiagnostics());
486
+ return {
487
+ kind: "image",
488
+ composition: options.compositionId,
489
+ entry: session.entry,
490
+ controller: session.controller,
491
+ path: outPath,
492
+ width: metadata.width,
493
+ height: metadata.height,
494
+ bytes: (await stat(outPath)).size,
495
+ diagnostics,
496
+ elapsedMs: performance.now() - startedAt,
497
+ };
332
498
  }
333
499
  finally {
334
500
  await rm(tempPath, { force: true });
335
501
  }
336
- return {
337
- composition: options.compositionId,
338
- entry: session.entry,
339
- controller: session.controller,
340
- frame: options.frame,
341
- path: options.outPath,
342
- width: session.metadata.width,
343
- height: session.metadata.height,
344
- diagnostics,
345
- elapsedMs: performance.now() - startedAt,
346
- };
347
502
  });
348
503
  }
349
504
  export async function renderComposition(options) {
350
- if (path.extname(options.outPath).toLowerCase() !== ".mp4") {
351
- throw new FrameyardError("USAGE", `Render output must use an .mp4 extension: ${options.outPath}`);
505
+ const startedAt = performance.now();
506
+ const prepared = await prepareComposition(options);
507
+ if (prepared.staticInfo.metadata.static === true) {
508
+ return renderStaticComposition(options, prepared, startedAt);
509
+ }
510
+ const container = options.container ?? "mp4";
511
+ const codec = options.codec ?? defaultCodecForContainer(container);
512
+ assertContainerCodec(container, codec);
513
+ const extension = `.${CONTAINER_EXTENSION[container]}`;
514
+ const outPath = options.outPath ?? path.join(options.project.root, "out", `${options.compositionId}${extension}`);
515
+ if (path.extname(outPath).toLowerCase() !== extension) {
516
+ throw new FrameyardError("USAGE", `Render output for --container ${container} must use a ${extension} extension: ${outPath}`);
352
517
  }
353
518
  const requestedConcurrency = validateRenderConcurrency(options.concurrency);
354
519
  const overwrite = options.overwrite ?? false;
355
- await ensureOutputAvailable(options.outPath, overwrite);
356
- await assertFfmpegCapabilities(options.signal);
357
- const startedAt = performance.now();
358
- return withSessions(options, (first) => Math.min(requestedConcurrency, first.metadata.durationInFrames), async (sessions) => {
520
+ await ensureOutputAvailable(outPath, overwrite);
521
+ await assertFfmpegCapabilities(options.signal, codec);
522
+ const encoderName = await resolveVideoEncoder(codec, options.encoder, options.signal);
523
+ return withPreparedSessions(options, prepared, (first) => (first.metadata.static === true ? 1 : Math.min(requestedConcurrency, first.metadata.durationInFrames)), async (sessions) => {
359
524
  const session = sessions[0];
525
+ if (session.metadata.static === true) {
526
+ throw new FrameyardError("INTERNAL", "An animated render opened a static composition session.");
527
+ }
360
528
  const { width, height, fps, durationInFrames } = session.metadata;
361
529
  const concurrency = sessions.length;
362
530
  if (width % 2 !== 0 || height % 2 !== 0) {
@@ -377,7 +545,7 @@ export async function renderComposition(options) {
377
545
  signal: options.signal,
378
546
  });
379
547
  const scratchDir = await mkdtemp(path.join(tmpdir(), "frameyard-render-"));
380
- const tempOut = path.join(path.dirname(options.outPath), `.${path.basename(options.outPath)}.frameyard-${process.pid}-${randomUUID()}.mp4`);
548
+ const tempOut = path.join(path.dirname(outPath), `.${path.basename(outPath)}.frameyard-${process.pid}-${randomUUID()}.mp4`);
381
549
  let encoder;
382
550
  let completed = false;
383
551
  try {
@@ -403,6 +571,9 @@ export async function renderComposition(options) {
403
571
  fps,
404
572
  frames: total,
405
573
  durationSeconds,
574
+ encoder: encoderName,
575
+ codec,
576
+ container,
406
577
  ...(audioPath !== undefined ? { audioPath } : {}),
407
578
  signal: options.signal,
408
579
  });
@@ -428,17 +599,18 @@ export async function renderComposition(options) {
428
599
  }
429
600
  await encoder.finish();
430
601
  encoder = undefined;
431
- if (!overwrite && (await pathExists(options.outPath))) {
432
- throw new FrameyardError("OUTPUT_EXISTS", `Output already exists: ${options.outPath}. Pass --overwrite to replace it.`);
602
+ if (!overwrite && (await pathExists(outPath))) {
603
+ throw new FrameyardError("OUTPUT_EXISTS", `Output already exists: ${outPath}. Pass --overwrite to replace it.`);
433
604
  }
434
- await publishFile(tempOut, options.outPath, overwrite);
605
+ await publishFile(tempOut, outPath, overwrite);
435
606
  completed = true;
436
607
  const diagnostics = mergeDiagnostics(await readSessionDiagnostics(sessions), audioDiagnostics(audio.warnings), transcriptDiagnostics(transcripts.warnings));
437
608
  return {
609
+ kind: "video",
438
610
  composition: options.compositionId,
439
611
  entry: session.entry,
440
612
  controller: session.controller,
441
- path: options.outPath,
613
+ path: outPath,
442
614
  width,
443
615
  height,
444
616
  fps,
@@ -446,7 +618,10 @@ export async function renderComposition(options) {
446
618
  durationSeconds,
447
619
  framesRendered: total,
448
620
  concurrency,
449
- bytes: (await stat(options.outPath)).size,
621
+ codec,
622
+ container,
623
+ encoder: encoderName,
624
+ bytes: (await stat(outPath)).size,
450
625
  audio: { clips: audio.clips.length },
451
626
  transcripts: {
452
627
  clips: transcripts.clips.length,