aivee 0.1.0-alpha.0 → 0.1.0-dev.7025438

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 (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -118
  3. package/docs/architecture.md +401 -401
  4. package/docs/aspect-ratio-presets.md +72 -72
  5. package/docs/releasing.md +19 -19
  6. package/docs/usage.md +1193 -1193
  7. package/package.json +52 -52
  8. package/requirements-otio.txt +1 -1
  9. package/src/capabilities.mjs +307 -307
  10. package/src/cli.mjs +646 -646
  11. package/src/doctor.mjs +133 -133
  12. package/src/edits/analysis-to-edl.mjs +43 -43
  13. package/src/edits/detect-silence.mjs +51 -51
  14. package/src/edits/find-mistake-candidates.mjs +42 -42
  15. package/src/edits/plan-shot-placements.mjs +105 -105
  16. package/src/edits/plan-silence-cuts.mjs +49 -49
  17. package/src/edits/retime-transcript.mjs +105 -105
  18. package/src/files.mjs +80 -80
  19. package/src/index.mjs +44 -44
  20. package/src/interchange/otio.mjs +204 -204
  21. package/src/interchange/otio_bridge.py +521 -521
  22. package/src/media/analysis-provider.mjs +75 -75
  23. package/src/media/compatibility.mjs +45 -45
  24. package/src/media/ffmpeg-capabilities.mjs +129 -129
  25. package/src/media/frame-rate.mjs +58 -58
  26. package/src/media/frame-time.mjs +22 -22
  27. package/src/media/preview-assets.mjs +350 -350
  28. package/src/media/probe.mjs +149 -149
  29. package/src/media/render-cuts.mjs +90 -90
  30. package/src/media/scene-detection.mjs +107 -107
  31. package/src/media/scene-review.mjs +205 -205
  32. package/src/project/assets.mjs +1194 -1194
  33. package/src/project/create.mjs +100 -100
  34. package/src/project/presets.mjs +44 -44
  35. package/src/render/checkpoints.mjs +192 -192
  36. package/src/render/formats.mjs +163 -163
  37. package/src/render/hyperframes.mjs +590 -590
  38. package/src/render/process.mjs +111 -111
  39. package/src/render/progress.mjs +46 -46
  40. package/src/render/timeline-control.mjs +53 -53
  41. package/src/timeline/assemble.mjs +119 -119
  42. package/src/timeline/audio-effects.mjs +75 -75
  43. package/src/timeline/audio-mastering.mjs +236 -236
  44. package/src/timeline/audio-mix.mjs +105 -105
  45. package/src/timeline/audio-track-effects.mjs +139 -139
  46. package/src/timeline/audio-volume.mjs +63 -63
  47. package/src/timeline/background.mjs +8 -8
  48. package/src/timeline/blend-mode.mjs +34 -34
  49. package/src/timeline/caption-layouts.mjs +61 -61
  50. package/src/timeline/captions.mjs +80 -80
  51. package/src/timeline/edit.mjs +965 -965
  52. package/src/timeline/fingerprint.mjs +6 -6
  53. package/src/timeline/from-edl.mjs +31 -31
  54. package/src/timeline/history-store.mjs +281 -281
  55. package/src/timeline/history.mjs +69 -69
  56. package/src/timeline/inspect.mjs +117 -117
  57. package/src/timeline/keyframes.mjs +158 -158
  58. package/src/timeline/layers.mjs +18 -18
  59. package/src/timeline/mask.mjs +53 -53
  60. package/src/timeline/project.mjs +52 -52
  61. package/src/timeline/render.mjs +759 -759
  62. package/src/timeline/sidechain-render.mjs +209 -209
  63. package/src/timeline/source-fit.mjs +15 -15
  64. package/src/timeline/speech-bubble.mjs +26 -26
  65. package/src/timeline/speed-ramp.mjs +188 -188
  66. package/src/timeline/track-state.mjs +22 -22
  67. package/src/timeline/transition.mjs +34 -34
  68. package/src/timeline/validate.mjs +322 -322
  69. package/src/transcript/corrections.mjs +21 -21
  70. package/src/transcript/normalize.mjs +17 -17
  71. package/src/transcript/provider.mjs +42 -42
  72. package/src/transcript/subtitles.mjs +57 -57
  73. package/src/transcript/validate.mjs +39 -39
@@ -1,34 +1,34 @@
1
- const BLEND_MODES = new Set([
2
- 'normal',
3
- 'multiply',
4
- 'screen',
5
- 'overlay',
6
- 'darken',
7
- 'lighten',
8
- 'color-dodge',
9
- 'color-burn',
10
- 'hard-light',
11
- 'soft-light',
12
- 'difference',
13
- 'exclusion',
14
- 'hue',
15
- 'saturation',
16
- 'color',
17
- 'luminosity',
18
- ]);
19
-
20
- export function validateBlendMode(clip, trackKind) {
21
- if (clip.blendMode === undefined) return;
22
- if (trackKind !== 'video' && trackKind !== 'overlay') {
23
- throw new TypeError(`Clip ${clip.id} blendMode is supported only on video and overlay tracks.`);
24
- }
25
- if (typeof clip.blendMode !== 'string' || !BLEND_MODES.has(clip.blendMode)) {
26
- throw new TypeError(`Clip ${clip.id} blendMode must be one of: ${[...BLEND_MODES].join(', ')}.`);
27
- }
28
- }
29
-
30
- export function blendModeStyle(clip) {
31
- return clip.blendMode && clip.blendMode !== 'normal'
32
- ? `mix-blend-mode:${clip.blendMode};`
33
- : '';
34
- }
1
+ const BLEND_MODES = new Set([
2
+ 'normal',
3
+ 'multiply',
4
+ 'screen',
5
+ 'overlay',
6
+ 'darken',
7
+ 'lighten',
8
+ 'color-dodge',
9
+ 'color-burn',
10
+ 'hard-light',
11
+ 'soft-light',
12
+ 'difference',
13
+ 'exclusion',
14
+ 'hue',
15
+ 'saturation',
16
+ 'color',
17
+ 'luminosity',
18
+ ]);
19
+
20
+ export function validateBlendMode(clip, trackKind) {
21
+ if (clip.blendMode === undefined) return;
22
+ if (trackKind !== 'video' && trackKind !== 'overlay') {
23
+ throw new TypeError(`Clip ${clip.id} blendMode is supported only on video and overlay tracks.`);
24
+ }
25
+ if (typeof clip.blendMode !== 'string' || !BLEND_MODES.has(clip.blendMode)) {
26
+ throw new TypeError(`Clip ${clip.id} blendMode must be one of: ${[...BLEND_MODES].join(', ')}.`);
27
+ }
28
+ }
29
+
30
+ export function blendModeStyle(clip) {
31
+ return clip.blendMode && clip.blendMode !== 'normal'
32
+ ? `mix-blend-mode:${clip.blendMode};`
33
+ : '';
34
+ }
@@ -1,61 +1,61 @@
1
- const SOCIAL_LAYOUT = {
2
- x: 0.08, y: 0.68, width: 0.84, height: 0.24, fontSize: 64,
3
- fontWeight: 800, lineHeight: 1.05, verticalAlign: 'bottom', paddingPixels: 14,
4
- wordWrap: true,
5
- textShadow: { color: '#000000cc', blurPixels: 6, offsetXPixels: 0, offsetYPixels: 3 },
6
- color: '#ffffff', backgroundColor: '#00000000', textAlign: 'center',
7
- };
8
-
9
- const LAYOUTS = {
10
- subtitle: {
11
- x: 0.08, y: 0.76, width: 0.84, height: 0.16, fontSize: 54,
12
- fontWeight: 600, verticalAlign: 'bottom', paddingPixels: 12, wordWrap: true,
13
- textShadow: { color: '#000000b8', blurPixels: 4, offsetXPixels: 0, offsetYPixels: 2 },
14
- color: '#ffffff', backgroundColor: '#000000b8', textAlign: 'center',
15
- },
16
- social: SOCIAL_LAYOUT,
17
- socialPop: {
18
- ...SOCIAL_LAYOUT,
19
- wordHighlight: true, wordHighlightColor: '#ffe45c', wordHighlightAnimation: 'pop',
20
- },
21
- socialStack: {
22
- ...SOCIAL_LAYOUT,
23
- y: 0.64, height: 0.3, fontSize: 58, lineHeight: 1.02,
24
- wordsPerCaption: 6, wordsPerLine: 3,
25
- wordHighlight: true, wordHighlightColor: '#ffe45c', wordHighlightAnimation: 'pop',
26
- },
27
- speechBubble: {
28
- x: 0.32, y: 0.66, width: 0.62, height: 0.1, fontSize: 44,
29
- fontWeight: 600, lineHeight: 1.12, verticalAlign: 'center', paddingPixels: 22,
30
- wordWrap: true, wordsPerCaption: 6, wordsPerLine: 3,
31
- wordHighlight: true, wordHighlightColor: '#426BEB', wordHighlightAnimation: 'scale',
32
- color: '#172236', backgroundColor: '#ffffff', textAlign: 'left',
33
- textShadow: { color: '#17223622', blurPixels: 10, offsetXPixels: 0, offsetYPixels: 4 },
34
- bubble: { tailSide: 'bottom', tailPosition: 0.16, tailSizePixels: 26, cornerRadiusPixels: 28 },
35
- },
36
- lowerThird: {
37
- x: 0.08, y: 0.72, width: 0.76, height: 0.18, fontSize: 48,
38
- fontWeight: 600, verticalAlign: 'bottom', paddingPixels: 12, wordWrap: true,
39
- textShadow: { color: '#000000b8', blurPixels: 3, offsetXPixels: 0, offsetYPixels: 2 },
40
- color: '#ffffff', backgroundColor: '#000000b8', textAlign: 'left',
41
- },
42
- };
43
-
44
- export const CAPTION_LAYOUT_PRESETS = Object.freeze(Object.fromEntries(
45
- Object.entries(LAYOUTS).map(([name, layout]) => [name, Object.freeze({
46
- ...layout,
47
- ...(layout.textOutline ? { textOutline: Object.freeze({ ...layout.textOutline }) } : {}),
48
- ...(layout.textShadow ? { textShadow: Object.freeze({ ...layout.textShadow }) } : {}),
49
- })]),
50
- ));
51
-
52
- export function listCaptionLayoutPresets() {
53
- return Object.keys(CAPTION_LAYOUT_PRESETS);
54
- }
55
-
56
- export function resolveCaptionLayoutPreset(name = 'subtitle') {
57
- if (typeof name !== 'string' || !Object.hasOwn(CAPTION_LAYOUT_PRESETS, name)) {
58
- throw new TypeError(`Unknown caption layout preset: ${name}`);
59
- }
60
- return structuredClone(CAPTION_LAYOUT_PRESETS[name]);
61
- }
1
+ const SOCIAL_LAYOUT = {
2
+ x: 0.08, y: 0.68, width: 0.84, height: 0.24, fontSize: 64,
3
+ fontWeight: 800, lineHeight: 1.05, verticalAlign: 'bottom', paddingPixels: 14,
4
+ wordWrap: true,
5
+ textShadow: { color: '#000000cc', blurPixels: 6, offsetXPixels: 0, offsetYPixels: 3 },
6
+ color: '#ffffff', backgroundColor: '#00000000', textAlign: 'center',
7
+ };
8
+
9
+ const LAYOUTS = {
10
+ subtitle: {
11
+ x: 0.08, y: 0.76, width: 0.84, height: 0.16, fontSize: 54,
12
+ fontWeight: 600, verticalAlign: 'bottom', paddingPixels: 12, wordWrap: true,
13
+ textShadow: { color: '#000000b8', blurPixels: 4, offsetXPixels: 0, offsetYPixels: 2 },
14
+ color: '#ffffff', backgroundColor: '#000000b8', textAlign: 'center',
15
+ },
16
+ social: SOCIAL_LAYOUT,
17
+ socialPop: {
18
+ ...SOCIAL_LAYOUT,
19
+ wordHighlight: true, wordHighlightColor: '#ffe45c', wordHighlightAnimation: 'pop',
20
+ },
21
+ socialStack: {
22
+ ...SOCIAL_LAYOUT,
23
+ y: 0.64, height: 0.3, fontSize: 58, lineHeight: 1.02,
24
+ wordsPerCaption: 6, wordsPerLine: 3,
25
+ wordHighlight: true, wordHighlightColor: '#ffe45c', wordHighlightAnimation: 'pop',
26
+ },
27
+ speechBubble: {
28
+ x: 0.32, y: 0.66, width: 0.62, height: 0.1, fontSize: 44,
29
+ fontWeight: 600, lineHeight: 1.12, verticalAlign: 'center', paddingPixels: 22,
30
+ wordWrap: true, wordsPerCaption: 6, wordsPerLine: 3,
31
+ wordHighlight: true, wordHighlightColor: '#426BEB', wordHighlightAnimation: 'scale',
32
+ color: '#172236', backgroundColor: '#ffffff', textAlign: 'left',
33
+ textShadow: { color: '#17223622', blurPixels: 10, offsetXPixels: 0, offsetYPixels: 4 },
34
+ bubble: { tailSide: 'bottom', tailPosition: 0.16, tailSizePixels: 26, cornerRadiusPixels: 28 },
35
+ },
36
+ lowerThird: {
37
+ x: 0.08, y: 0.72, width: 0.76, height: 0.18, fontSize: 48,
38
+ fontWeight: 600, verticalAlign: 'bottom', paddingPixels: 12, wordWrap: true,
39
+ textShadow: { color: '#000000b8', blurPixels: 3, offsetXPixels: 0, offsetYPixels: 2 },
40
+ color: '#ffffff', backgroundColor: '#000000b8', textAlign: 'left',
41
+ },
42
+ };
43
+
44
+ export const CAPTION_LAYOUT_PRESETS = Object.freeze(Object.fromEntries(
45
+ Object.entries(LAYOUTS).map(([name, layout]) => [name, Object.freeze({
46
+ ...layout,
47
+ ...(layout.textOutline ? { textOutline: Object.freeze({ ...layout.textOutline }) } : {}),
48
+ ...(layout.textShadow ? { textShadow: Object.freeze({ ...layout.textShadow }) } : {}),
49
+ })]),
50
+ ));
51
+
52
+ export function listCaptionLayoutPresets() {
53
+ return Object.keys(CAPTION_LAYOUT_PRESETS);
54
+ }
55
+
56
+ export function resolveCaptionLayoutPreset(name = 'subtitle') {
57
+ if (typeof name !== 'string' || !Object.hasOwn(CAPTION_LAYOUT_PRESETS, name)) {
58
+ throw new TypeError(`Unknown caption layout preset: ${name}`);
59
+ }
60
+ return structuredClone(CAPTION_LAYOUT_PRESETS[name]);
61
+ }
@@ -1,80 +1,80 @@
1
- import { validateTranscript } from '../transcript/validate.mjs';
2
- import { resolveCaptionLayoutPreset } from './caption-layouts.mjs';
3
- import { resolveSpeechBubble } from './speech-bubble.mjs';
4
-
5
- export function createCaptionTrack(transcript, options = {}) {
6
- validateTranscript(transcript);
7
- const layout = resolveCaptionLayoutPreset(options.layoutPreset);
8
- const wordHighlight = options.wordHighlight ?? layout.wordHighlight ?? false;
9
- if (typeof wordHighlight !== 'boolean') throw new TypeError('wordHighlight must be a boolean.');
10
- if (wordHighlight && transcript.granularity === 'cue') throw new TypeError('Word highlighting requires word-level transcript timing.');
11
- const wordHighlightColor = options.wordHighlightColor ?? layout.wordHighlightColor ?? '#ffe45c';
12
- if (typeof wordHighlightColor !== 'string' || !/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.test(wordHighlightColor)) {
13
- throw new TypeError('wordHighlightColor must be a hex color.');
14
- }
15
- if (!wordHighlight && options.wordHighlightColor !== undefined) throw new TypeError('wordHighlightColor requires wordHighlight: true.');
16
- const wordHighlightAnimation = options.wordHighlightAnimation ?? layout.wordHighlightAnimation ?? 'scale';
17
- if (!['scale', 'pop'].includes(wordHighlightAnimation)) throw new TypeError('wordHighlightAnimation must be scale or pop.');
18
- if (!wordHighlight && options.wordHighlightAnimation !== undefined) throw new TypeError('wordHighlightAnimation requires wordHighlight: true.');
19
- const wordsPerCaption = options.wordsPerCaption ?? layout.wordsPerCaption ?? 6;
20
- if (!Number.isSafeInteger(wordsPerCaption) || wordsPerCaption < 1 || wordsPerCaption > 20) throw new TypeError('wordsPerCaption must be an integer from 1 to 20.');
21
- const wordsPerLine = options.wordsPerLine ?? layout.wordsPerLine;
22
- if (wordsPerLine !== undefined && (!Number.isSafeInteger(wordsPerLine) || wordsPerLine < 1 || wordsPerLine > 20)) {
23
- throw new TypeError('wordsPerLine must be an integer from 1 to 20.');
24
- }
25
- const wordWrap = options.wordWrap ?? layout.wordWrap ?? false;
26
- if (typeof wordWrap !== 'boolean') throw new TypeError('wordWrap must be a boolean.');
27
- const maxGapSeconds = options.maxGapSeconds ?? 0.8;
28
- if (!Number.isFinite(maxGapSeconds) || maxGapSeconds < 0) throw new TypeError('maxGapSeconds must be a non-negative number.');
29
- const bubbleSource = options.bubble ?? layout.bubble;
30
- const bubble = bubbleSource === undefined ? undefined : resolveSpeechBubble(bubbleSource, layout.bubble);
31
-
32
- const groups = [];
33
- let group = [];
34
- for (const word of transcript.words) {
35
- if (group.length && (group.length >= wordsPerCaption || word.start - group.at(-1).end > maxGapSeconds || /[.!?]$/.test(group.at(-1).text))) {
36
- groups.push(group);
37
- group = [];
38
- }
39
- group.push(word);
40
- }
41
- if (group.length) groups.push(group);
42
-
43
- const clips = groups.map((words, index) => ({
44
- id: `caption-${String(index + 1).padStart(3, '0')}`,
45
- timelineStart: words[0].start,
46
- durationSeconds: words.at(-1).end - words[0].start,
47
- text: words.map(({ text }) => text).join(' '),
48
- ...(wordsPerLine !== undefined ? { wordsPerLine } : {}),
49
- ...(wordHighlight ? {
50
- wordTimings: words.map((word) => ({
51
- text: word.text,
52
- start: word.start - words[0].start,
53
- end: word.end - words[0].start,
54
- })),
55
- wordHighlightColor,
56
- wordHighlightAnimation,
57
- } : {}),
58
- x: options.x ?? layout.x,
59
- y: options.y ?? layout.y,
60
- width: options.width ?? layout.width,
61
- height: options.height ?? layout.height,
62
- fontSize: options.fontSize ?? layout.fontSize,
63
- ...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
64
- fontWeight: options.fontWeight ?? layout.fontWeight,
65
- ...(options.lineHeight !== undefined || layout.lineHeight !== undefined ? { lineHeight: options.lineHeight ?? layout.lineHeight } : {}),
66
- verticalAlign: options.verticalAlign ?? layout.verticalAlign,
67
- wordWrap,
68
- paddingPixels: options.paddingPixels ?? layout.paddingPixels,
69
- textOutline: options.textOutline ?? layout.textOutline,
70
- textShadow: options.textShadow ?? layout.textShadow,
71
- color: options.color ?? layout.color,
72
- backgroundColor: options.backgroundColor ?? layout.backgroundColor,
73
- textAlign: options.textAlign ?? layout.textAlign,
74
- ...(bubble === undefined ? {} : { bubble: structuredClone(bubble) }),
75
- ...(options.fadeInSeconds ? { fadeInSeconds: options.fadeInSeconds } : {}),
76
- ...(options.fadeOutSeconds ? { fadeOutSeconds: options.fadeOutSeconds } : {}),
77
- }));
78
-
79
- return { id: options.trackId ?? 'captions', kind: 'overlay', order: options.order ?? 100, clips };
80
- }
1
+ import { validateTranscript } from '../transcript/validate.mjs';
2
+ import { resolveCaptionLayoutPreset } from './caption-layouts.mjs';
3
+ import { resolveSpeechBubble } from './speech-bubble.mjs';
4
+
5
+ export function createCaptionTrack(transcript, options = {}) {
6
+ validateTranscript(transcript);
7
+ const layout = resolveCaptionLayoutPreset(options.layoutPreset);
8
+ const wordHighlight = options.wordHighlight ?? layout.wordHighlight ?? false;
9
+ if (typeof wordHighlight !== 'boolean') throw new TypeError('wordHighlight must be a boolean.');
10
+ if (wordHighlight && transcript.granularity === 'cue') throw new TypeError('Word highlighting requires word-level transcript timing.');
11
+ const wordHighlightColor = options.wordHighlightColor ?? layout.wordHighlightColor ?? '#ffe45c';
12
+ if (typeof wordHighlightColor !== 'string' || !/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.test(wordHighlightColor)) {
13
+ throw new TypeError('wordHighlightColor must be a hex color.');
14
+ }
15
+ if (!wordHighlight && options.wordHighlightColor !== undefined) throw new TypeError('wordHighlightColor requires wordHighlight: true.');
16
+ const wordHighlightAnimation = options.wordHighlightAnimation ?? layout.wordHighlightAnimation ?? 'scale';
17
+ if (!['scale', 'pop'].includes(wordHighlightAnimation)) throw new TypeError('wordHighlightAnimation must be scale or pop.');
18
+ if (!wordHighlight && options.wordHighlightAnimation !== undefined) throw new TypeError('wordHighlightAnimation requires wordHighlight: true.');
19
+ const wordsPerCaption = options.wordsPerCaption ?? layout.wordsPerCaption ?? 6;
20
+ if (!Number.isSafeInteger(wordsPerCaption) || wordsPerCaption < 1 || wordsPerCaption > 20) throw new TypeError('wordsPerCaption must be an integer from 1 to 20.');
21
+ const wordsPerLine = options.wordsPerLine ?? layout.wordsPerLine;
22
+ if (wordsPerLine !== undefined && (!Number.isSafeInteger(wordsPerLine) || wordsPerLine < 1 || wordsPerLine > 20)) {
23
+ throw new TypeError('wordsPerLine must be an integer from 1 to 20.');
24
+ }
25
+ const wordWrap = options.wordWrap ?? layout.wordWrap ?? false;
26
+ if (typeof wordWrap !== 'boolean') throw new TypeError('wordWrap must be a boolean.');
27
+ const maxGapSeconds = options.maxGapSeconds ?? 0.8;
28
+ if (!Number.isFinite(maxGapSeconds) || maxGapSeconds < 0) throw new TypeError('maxGapSeconds must be a non-negative number.');
29
+ const bubbleSource = options.bubble ?? layout.bubble;
30
+ const bubble = bubbleSource === undefined ? undefined : resolveSpeechBubble(bubbleSource, layout.bubble);
31
+
32
+ const groups = [];
33
+ let group = [];
34
+ for (const word of transcript.words) {
35
+ if (group.length && (group.length >= wordsPerCaption || word.start - group.at(-1).end > maxGapSeconds || /[.!?]$/.test(group.at(-1).text))) {
36
+ groups.push(group);
37
+ group = [];
38
+ }
39
+ group.push(word);
40
+ }
41
+ if (group.length) groups.push(group);
42
+
43
+ const clips = groups.map((words, index) => ({
44
+ id: `caption-${String(index + 1).padStart(3, '0')}`,
45
+ timelineStart: words[0].start,
46
+ durationSeconds: words.at(-1).end - words[0].start,
47
+ text: words.map(({ text }) => text).join(' '),
48
+ ...(wordsPerLine !== undefined ? { wordsPerLine } : {}),
49
+ ...(wordHighlight ? {
50
+ wordTimings: words.map((word) => ({
51
+ text: word.text,
52
+ start: word.start - words[0].start,
53
+ end: word.end - words[0].start,
54
+ })),
55
+ wordHighlightColor,
56
+ wordHighlightAnimation,
57
+ } : {}),
58
+ x: options.x ?? layout.x,
59
+ y: options.y ?? layout.y,
60
+ width: options.width ?? layout.width,
61
+ height: options.height ?? layout.height,
62
+ fontSize: options.fontSize ?? layout.fontSize,
63
+ ...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
64
+ fontWeight: options.fontWeight ?? layout.fontWeight,
65
+ ...(options.lineHeight !== undefined || layout.lineHeight !== undefined ? { lineHeight: options.lineHeight ?? layout.lineHeight } : {}),
66
+ verticalAlign: options.verticalAlign ?? layout.verticalAlign,
67
+ wordWrap,
68
+ paddingPixels: options.paddingPixels ?? layout.paddingPixels,
69
+ textOutline: options.textOutline ?? layout.textOutline,
70
+ textShadow: options.textShadow ?? layout.textShadow,
71
+ color: options.color ?? layout.color,
72
+ backgroundColor: options.backgroundColor ?? layout.backgroundColor,
73
+ textAlign: options.textAlign ?? layout.textAlign,
74
+ ...(bubble === undefined ? {} : { bubble: structuredClone(bubble) }),
75
+ ...(options.fadeInSeconds ? { fadeInSeconds: options.fadeInSeconds } : {}),
76
+ ...(options.fadeOutSeconds ? { fadeOutSeconds: options.fadeOutSeconds } : {}),
77
+ }));
78
+
79
+ return { id: options.trackId ?? 'captions', kind: 'overlay', order: options.order ?? 100, clips };
80
+ }