aivee 0.1.0-alpha.0 → 0.1.0-dev.59872e1

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 (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -21
  3. package/README.md +120 -118
  4. package/docs/architecture.md +401 -401
  5. package/docs/aspect-ratio-presets.md +72 -72
  6. package/docs/releasing.md +19 -19
  7. package/docs/usage.md +1193 -1193
  8. package/package.json +52 -52
  9. package/requirements-otio.txt +1 -1
  10. package/src/capabilities.mjs +307 -307
  11. package/src/cli.mjs +646 -646
  12. package/src/doctor.mjs +133 -133
  13. package/src/edits/analysis-to-edl.mjs +43 -43
  14. package/src/edits/detect-silence.mjs +51 -51
  15. package/src/edits/find-mistake-candidates.mjs +42 -42
  16. package/src/edits/plan-shot-placements.mjs +105 -105
  17. package/src/edits/plan-silence-cuts.mjs +49 -49
  18. package/src/edits/retime-transcript.mjs +105 -105
  19. package/src/files.mjs +80 -80
  20. package/src/index.mjs +44 -44
  21. package/src/interchange/otio.mjs +204 -204
  22. package/src/interchange/otio_bridge.py +521 -521
  23. package/src/media/analysis-provider.mjs +75 -75
  24. package/src/media/compatibility.mjs +45 -45
  25. package/src/media/ffmpeg-capabilities.mjs +129 -129
  26. package/src/media/frame-rate.mjs +58 -58
  27. package/src/media/frame-time.mjs +22 -22
  28. package/src/media/preview-assets.mjs +350 -350
  29. package/src/media/probe.mjs +149 -149
  30. package/src/media/render-cuts.mjs +90 -90
  31. package/src/media/scene-detection.mjs +107 -107
  32. package/src/media/scene-review.mjs +205 -205
  33. package/src/project/assets.mjs +1194 -1194
  34. package/src/project/create.mjs +100 -100
  35. package/src/project/presets.mjs +44 -44
  36. package/src/render/checkpoints.mjs +192 -192
  37. package/src/render/formats.mjs +163 -163
  38. package/src/render/hyperframes.mjs +590 -590
  39. package/src/render/process.mjs +111 -111
  40. package/src/render/progress.mjs +46 -46
  41. package/src/render/timeline-control.mjs +53 -53
  42. package/src/timeline/assemble.mjs +119 -119
  43. package/src/timeline/audio-effects.mjs +75 -75
  44. package/src/timeline/audio-mastering.mjs +236 -236
  45. package/src/timeline/audio-mix.mjs +105 -105
  46. package/src/timeline/audio-track-effects.mjs +139 -139
  47. package/src/timeline/audio-volume.mjs +63 -63
  48. package/src/timeline/background.mjs +8 -8
  49. package/src/timeline/blend-mode.mjs +34 -34
  50. package/src/timeline/caption-layouts.mjs +62 -61
  51. package/src/timeline/captions.mjs +80 -80
  52. package/src/timeline/edit.mjs +965 -965
  53. package/src/timeline/fingerprint.mjs +6 -6
  54. package/src/timeline/from-edl.mjs +31 -31
  55. package/src/timeline/history-store.mjs +281 -281
  56. package/src/timeline/history.mjs +69 -69
  57. package/src/timeline/inspect.mjs +117 -117
  58. package/src/timeline/keyframes.mjs +158 -158
  59. package/src/timeline/layers.mjs +18 -18
  60. package/src/timeline/mask.mjs +53 -53
  61. package/src/timeline/project.mjs +52 -52
  62. package/src/timeline/render.mjs +759 -759
  63. package/src/timeline/sidechain-render.mjs +209 -209
  64. package/src/timeline/source-fit.mjs +15 -15
  65. package/src/timeline/speech-bubble.mjs +26 -26
  66. package/src/timeline/speed-ramp.mjs +188 -188
  67. package/src/timeline/track-state.mjs +22 -22
  68. package/src/timeline/transition.mjs +34 -34
  69. package/src/timeline/validate.mjs +322 -322
  70. package/src/transcript/corrections.mjs +21 -21
  71. package/src/transcript/normalize.mjs +17 -17
  72. package/src/transcript/provider.mjs +42 -42
  73. package/src/transcript/subtitles.mjs +57 -57
  74. package/src/transcript/validate.mjs +39 -39
@@ -1,188 +1,188 @@
1
- export function validateSpeedRamp(clip) {
2
- const points = clip.speedRamp;
3
- if (!Array.isArray(points) || points.length < 2) throw new TypeError(`Clip ${clip.id} speedRamp must contain at least two points.`);
4
- if (clip.playbackRate !== undefined) throw new TypeError(`Clip ${clip.id} cannot combine speedRamp with playbackRate.`);
5
- const sourceSpan = clip.sourceOut - clip.sourceIn;
6
- if (points[0]?.sourceOffset !== 0 || Math.abs(points.at(-1)?.sourceOffset - sourceSpan) > 1e-6) {
7
- throw new TypeError(`Clip ${clip.id} speedRamp must start at sourceOffset 0 and end at the clip source span.`);
8
- }
9
- let previousOffset = -1;
10
- for (const [index, point] of points.entries()) {
11
- if (!point || typeof point !== 'object' || Array.isArray(point)
12
- || !Number.isFinite(point.sourceOffset) || point.sourceOffset <= previousOffset
13
- || !Number.isFinite(point.playbackRate) || point.playbackRate < 0.1 || point.playbackRate > 5
14
- || Object.keys(point).some((key) => !['sourceOffset', 'playbackRate', 'ease'].includes(key))
15
- || (point.ease !== undefined && !SPEED_RAMP_EASES.includes(point.ease))
16
- || (index === points.length - 1 && point.ease !== undefined)) {
17
- throw new TypeError(`Clip ${clip.id} speedRamp point ${index + 1} has invalid sourceOffset, playbackRate, or outgoing ease.`);
18
- }
19
- previousOffset = point.sourceOffset;
20
- }
21
- const expectedDuration = speedRampDuration(points);
22
- if (Math.abs(expectedDuration - clip.durationSeconds) > 1e-6) {
23
- throw new TypeError(`Clip ${clip.id} durationSeconds must match its integrated speedRamp duration (${expectedDuration}).`);
24
- }
25
- return points;
26
- }
27
-
28
- export function speedRampDuration(points, start = 0, end = points.at(-1).sourceOffset) {
29
- if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end > points.at(-1).sourceOffset || end < start) {
30
- throw new TypeError('Speed ramp duration bounds must be a positive range within the curve.');
31
- }
32
- if (end === start) return 0;
33
- let duration = 0;
34
- for (const segment of rampSegments(points)) {
35
- const from = Math.max(start, segment.start.sourceOffset);
36
- const to = Math.min(end, segment.end.sourceOffset);
37
- if (to <= from) continue;
38
- const fromRate = speedRampRateAt(segment, from);
39
- const toRate = speedRampRateAt(segment, to);
40
- duration += integrateReciprocalRate(to - from, fromRate, toRate);
41
- }
42
- return duration;
43
- }
44
-
45
- export function speedRampSourceAtTime(points, timelineTime) {
46
- if (!Number.isFinite(timelineTime) || timelineTime < 0) throw new TypeError('Speed ramp timeline time must be non-negative.');
47
- let elapsed = 0;
48
- for (const segment of rampSegments(points)) {
49
- const sourceSpan = segment.end.sourceOffset - segment.start.sourceOffset;
50
- const segmentDuration = integrateReciprocalRate(sourceSpan, segment.start.playbackRate, segment.end.playbackRate);
51
- if (timelineTime <= elapsed + segmentDuration) {
52
- const deltaTime = timelineTime - elapsed;
53
- const slope = (segment.end.playbackRate - segment.start.playbackRate) / sourceSpan;
54
- const deltaSource = Math.abs(slope) < 1e-10
55
- ? deltaTime * segment.start.playbackRate
56
- : segment.start.playbackRate * Math.expm1(slope * deltaTime) / slope;
57
- return Math.min(segment.end.sourceOffset, segment.start.sourceOffset + Math.max(0, deltaSource));
58
- }
59
- elapsed += segmentDuration;
60
- }
61
- if (Math.abs(timelineTime - elapsed) <= 1e-6) return points.at(-1).sourceOffset;
62
- throw new TypeError('Speed ramp timeline time exceeds the curve duration.');
63
- }
64
-
65
- export function sliceSpeedRamp(points, start, end) {
66
- if (!Array.isArray(points) || points.length < 2 || !Number.isFinite(start) || !Number.isFinite(end)
67
- || start < 0 || end > points.at(-1).sourceOffset || end <= start) {
68
- throw new TypeError('Speed ramp slice must be a positive range within the curve.');
69
- }
70
- const expanded = expandSpeedRampPoints(points);
71
- const sliced = [{ sourceOffset: 0, playbackRate: speedRampRateAtOffset(expanded, start) }];
72
- for (const point of expanded) {
73
- if (point.sourceOffset > start && point.sourceOffset < end) {
74
- sliced.push({ sourceOffset: point.sourceOffset - start, playbackRate: point.playbackRate });
75
- }
76
- }
77
- sliced.push({ sourceOffset: end - start, playbackRate: speedRampRateAtOffset(expanded, end) });
78
- return sliced;
79
- }
80
-
81
- export function speedRampPtsExpression(points) {
82
- const segments = rampSegments(points);
83
- let expression = `${formatNumber(speedRampDuration(points))}+(T-${formatNumber(points.at(-1).sourceOffset)})/${formatNumber(points.at(-1).playbackRate)}`;
84
- for (let index = segments.length - 1; index >= 0; index--) {
85
- const segment = segments[index];
86
- const sourceSpan = segment.end.sourceOffset - segment.start.sourceOffset;
87
- const slope = (segment.end.playbackRate - segment.start.playbackRate) / sourceSpan;
88
- const offset = `T-${formatNumber(segment.start.sourceOffset)}`;
89
- const elapsed = Math.abs(slope) < 1e-10
90
- ? `(${offset})/${formatNumber(segment.start.playbackRate)}`
91
- : `log((${formatNumber(segment.start.playbackRate)}+${formatNumber(slope)}*(${offset}))/${formatNumber(segment.start.playbackRate)})/${formatNumber(slope)}`;
92
- const value = `${formatNumber(speedRampDuration(points, 0, segment.start.sourceOffset))}+(${elapsed})`;
93
- expression = `if(lt(T\\,${formatNumber(segment.end.sourceOffset)})\\,${value}\\,${expression})`;
94
- }
95
- return `setpts=${expression}/TB`;
96
- }
97
-
98
- export function buildSpeedRampAudioAutomation(points, commandPath, maxSourceStep = 0.05) {
99
- if (!Number.isFinite(maxSourceStep) || maxSourceStep <= 0) throw new TypeError('Speed ramp audio command step must be positive.');
100
- const intervals = [];
101
- for (const segment of rampSegments(points)) {
102
- const span = segment.end.sourceOffset - segment.start.sourceOffset;
103
- const count = Math.ceil(span / maxSourceStep);
104
- for (let index = 0; index < count; index++) {
105
- const start = segment.start.sourceOffset + span * index / count;
106
- const end = segment.start.sourceOffset + span * (index + 1) / count;
107
- intervals.push({
108
- start,
109
- end,
110
- rate: speedRampRateAt(segment, (start + end) / 2),
111
- });
112
- }
113
- }
114
- const filter = 'rubberband@engine_speed_0';
115
- const filterInstance = `${filter}=${formatNumber(intervals[0].rate)}`;
116
- const commands = intervals.slice(1).map((interval) => {
117
- const factor = formatNumber(interval.rate);
118
- return `${formatNumber(interval.start)} ${filter} tempo ${factor};`;
119
- }).join('\n');
120
- const commandFilter = `asendcmd=f=${commandPath}`;
121
- return {
122
- initialRate: intervals[0].rate,
123
- commandContents: commands,
124
- filters: [commandFilter, filterInstance].join(','),
125
- };
126
- }
127
-
128
- function rampSegments(points) {
129
- const curve = expandSpeedRampPoints(points);
130
- return curve.slice(1).map((end, index) => ({ start: curve[index], end }));
131
- }
132
-
133
- export const SPEED_RAMP_EASES = Object.freeze(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic']);
134
- const EASE_STEPS = 64;
135
-
136
- function expandSpeedRampPoints(points) {
137
- if (!points.some((point) => point.ease !== undefined && point.ease !== 'linear')) return points;
138
- const expanded = [{ sourceOffset: points[0].sourceOffset, playbackRate: points[0].playbackRate }];
139
- for (let index = 0; index < points.length - 1; index++) {
140
- const start = points[index];
141
- const end = points[index + 1];
142
- const ease = start.ease ?? 'linear';
143
- const steps = ease === 'linear' ? 1 : EASE_STEPS;
144
- const sourceSpan = end.sourceOffset - start.sourceOffset;
145
- const rateSpan = end.playbackRate - start.playbackRate;
146
- for (let step = 1; step <= steps; step++) {
147
- const progress = step / steps;
148
- const eased = evaluateRampEase(ease, progress);
149
- expanded.push({
150
- sourceOffset: start.sourceOffset + sourceSpan * progress,
151
- playbackRate: start.playbackRate + rateSpan * eased,
152
- });
153
- }
154
- }
155
- return expanded;
156
- }
157
-
158
- function evaluateRampEase(ease, progress) {
159
- switch (ease) {
160
- case 'easeIn': return progress ** 2;
161
- case 'easeOut': return 1 - (1 - progress) ** 2;
162
- case 'easeInOut': return progress < 0.5 ? 2 * progress ** 2 : 1 - ((-2 * progress + 2) ** 2) / 2;
163
- case 'easeInCubic': return progress ** 3;
164
- case 'easeOutCubic': return 1 - (1 - progress) ** 3;
165
- case 'easeInOutCubic': return progress < 0.5 ? 4 * progress ** 3 : 1 - ((-2 * progress + 2) ** 3) / 2;
166
- default: return progress;
167
- }
168
- }
169
-
170
- function speedRampRateAtOffset(points, offset) {
171
- const segment = rampSegments(points).find(({ start, end }) => offset <= end.sourceOffset)
172
- ?? rampSegments(points).at(-1);
173
- return speedRampRateAt(segment, offset);
174
- }
175
-
176
- function speedRampRateAt(segment, offset) {
177
- const progress = (offset - segment.start.sourceOffset) / (segment.end.sourceOffset - segment.start.sourceOffset);
178
- return segment.start.playbackRate + (segment.end.playbackRate - segment.start.playbackRate) * progress;
179
- }
180
-
181
- function integrateReciprocalRate(sourceSpan, startRate, endRate) {
182
- const slope = (endRate - startRate) / sourceSpan;
183
- return Math.abs(slope) < 1e-10 ? sourceSpan / startRate : Math.log(endRate / startRate) / slope;
184
- }
185
-
186
- function formatNumber(value) {
187
- return Number(value.toPrecision(12)).toString();
188
- }
1
+ export function validateSpeedRamp(clip) {
2
+ const points = clip.speedRamp;
3
+ if (!Array.isArray(points) || points.length < 2) throw new TypeError(`Clip ${clip.id} speedRamp must contain at least two points.`);
4
+ if (clip.playbackRate !== undefined) throw new TypeError(`Clip ${clip.id} cannot combine speedRamp with playbackRate.`);
5
+ const sourceSpan = clip.sourceOut - clip.sourceIn;
6
+ if (points[0]?.sourceOffset !== 0 || Math.abs(points.at(-1)?.sourceOffset - sourceSpan) > 1e-6) {
7
+ throw new TypeError(`Clip ${clip.id} speedRamp must start at sourceOffset 0 and end at the clip source span.`);
8
+ }
9
+ let previousOffset = -1;
10
+ for (const [index, point] of points.entries()) {
11
+ if (!point || typeof point !== 'object' || Array.isArray(point)
12
+ || !Number.isFinite(point.sourceOffset) || point.sourceOffset <= previousOffset
13
+ || !Number.isFinite(point.playbackRate) || point.playbackRate < 0.1 || point.playbackRate > 5
14
+ || Object.keys(point).some((key) => !['sourceOffset', 'playbackRate', 'ease'].includes(key))
15
+ || (point.ease !== undefined && !SPEED_RAMP_EASES.includes(point.ease))
16
+ || (index === points.length - 1 && point.ease !== undefined)) {
17
+ throw new TypeError(`Clip ${clip.id} speedRamp point ${index + 1} has invalid sourceOffset, playbackRate, or outgoing ease.`);
18
+ }
19
+ previousOffset = point.sourceOffset;
20
+ }
21
+ const expectedDuration = speedRampDuration(points);
22
+ if (Math.abs(expectedDuration - clip.durationSeconds) > 1e-6) {
23
+ throw new TypeError(`Clip ${clip.id} durationSeconds must match its integrated speedRamp duration (${expectedDuration}).`);
24
+ }
25
+ return points;
26
+ }
27
+
28
+ export function speedRampDuration(points, start = 0, end = points.at(-1).sourceOffset) {
29
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end > points.at(-1).sourceOffset || end < start) {
30
+ throw new TypeError('Speed ramp duration bounds must be a positive range within the curve.');
31
+ }
32
+ if (end === start) return 0;
33
+ let duration = 0;
34
+ for (const segment of rampSegments(points)) {
35
+ const from = Math.max(start, segment.start.sourceOffset);
36
+ const to = Math.min(end, segment.end.sourceOffset);
37
+ if (to <= from) continue;
38
+ const fromRate = speedRampRateAt(segment, from);
39
+ const toRate = speedRampRateAt(segment, to);
40
+ duration += integrateReciprocalRate(to - from, fromRate, toRate);
41
+ }
42
+ return duration;
43
+ }
44
+
45
+ export function speedRampSourceAtTime(points, timelineTime) {
46
+ if (!Number.isFinite(timelineTime) || timelineTime < 0) throw new TypeError('Speed ramp timeline time must be non-negative.');
47
+ let elapsed = 0;
48
+ for (const segment of rampSegments(points)) {
49
+ const sourceSpan = segment.end.sourceOffset - segment.start.sourceOffset;
50
+ const segmentDuration = integrateReciprocalRate(sourceSpan, segment.start.playbackRate, segment.end.playbackRate);
51
+ if (timelineTime <= elapsed + segmentDuration) {
52
+ const deltaTime = timelineTime - elapsed;
53
+ const slope = (segment.end.playbackRate - segment.start.playbackRate) / sourceSpan;
54
+ const deltaSource = Math.abs(slope) < 1e-10
55
+ ? deltaTime * segment.start.playbackRate
56
+ : segment.start.playbackRate * Math.expm1(slope * deltaTime) / slope;
57
+ return Math.min(segment.end.sourceOffset, segment.start.sourceOffset + Math.max(0, deltaSource));
58
+ }
59
+ elapsed += segmentDuration;
60
+ }
61
+ if (Math.abs(timelineTime - elapsed) <= 1e-6) return points.at(-1).sourceOffset;
62
+ throw new TypeError('Speed ramp timeline time exceeds the curve duration.');
63
+ }
64
+
65
+ export function sliceSpeedRamp(points, start, end) {
66
+ if (!Array.isArray(points) || points.length < 2 || !Number.isFinite(start) || !Number.isFinite(end)
67
+ || start < 0 || end > points.at(-1).sourceOffset || end <= start) {
68
+ throw new TypeError('Speed ramp slice must be a positive range within the curve.');
69
+ }
70
+ const expanded = expandSpeedRampPoints(points);
71
+ const sliced = [{ sourceOffset: 0, playbackRate: speedRampRateAtOffset(expanded, start) }];
72
+ for (const point of expanded) {
73
+ if (point.sourceOffset > start && point.sourceOffset < end) {
74
+ sliced.push({ sourceOffset: point.sourceOffset - start, playbackRate: point.playbackRate });
75
+ }
76
+ }
77
+ sliced.push({ sourceOffset: end - start, playbackRate: speedRampRateAtOffset(expanded, end) });
78
+ return sliced;
79
+ }
80
+
81
+ export function speedRampPtsExpression(points) {
82
+ const segments = rampSegments(points);
83
+ let expression = `${formatNumber(speedRampDuration(points))}+(T-${formatNumber(points.at(-1).sourceOffset)})/${formatNumber(points.at(-1).playbackRate)}`;
84
+ for (let index = segments.length - 1; index >= 0; index--) {
85
+ const segment = segments[index];
86
+ const sourceSpan = segment.end.sourceOffset - segment.start.sourceOffset;
87
+ const slope = (segment.end.playbackRate - segment.start.playbackRate) / sourceSpan;
88
+ const offset = `T-${formatNumber(segment.start.sourceOffset)}`;
89
+ const elapsed = Math.abs(slope) < 1e-10
90
+ ? `(${offset})/${formatNumber(segment.start.playbackRate)}`
91
+ : `log((${formatNumber(segment.start.playbackRate)}+${formatNumber(slope)}*(${offset}))/${formatNumber(segment.start.playbackRate)})/${formatNumber(slope)}`;
92
+ const value = `${formatNumber(speedRampDuration(points, 0, segment.start.sourceOffset))}+(${elapsed})`;
93
+ expression = `if(lt(T\\,${formatNumber(segment.end.sourceOffset)})\\,${value}\\,${expression})`;
94
+ }
95
+ return `setpts=${expression}/TB`;
96
+ }
97
+
98
+ export function buildSpeedRampAudioAutomation(points, commandPath, maxSourceStep = 0.05) {
99
+ if (!Number.isFinite(maxSourceStep) || maxSourceStep <= 0) throw new TypeError('Speed ramp audio command step must be positive.');
100
+ const intervals = [];
101
+ for (const segment of rampSegments(points)) {
102
+ const span = segment.end.sourceOffset - segment.start.sourceOffset;
103
+ const count = Math.ceil(span / maxSourceStep);
104
+ for (let index = 0; index < count; index++) {
105
+ const start = segment.start.sourceOffset + span * index / count;
106
+ const end = segment.start.sourceOffset + span * (index + 1) / count;
107
+ intervals.push({
108
+ start,
109
+ end,
110
+ rate: speedRampRateAt(segment, (start + end) / 2),
111
+ });
112
+ }
113
+ }
114
+ const filter = 'rubberband@engine_speed_0';
115
+ const filterInstance = `${filter}=${formatNumber(intervals[0].rate)}`;
116
+ const commands = intervals.slice(1).map((interval) => {
117
+ const factor = formatNumber(interval.rate);
118
+ return `${formatNumber(interval.start)} ${filter} tempo ${factor};`;
119
+ }).join('\n');
120
+ const commandFilter = `asendcmd=f=${commandPath}`;
121
+ return {
122
+ initialRate: intervals[0].rate,
123
+ commandContents: commands,
124
+ filters: [commandFilter, filterInstance].join(','),
125
+ };
126
+ }
127
+
128
+ function rampSegments(points) {
129
+ const curve = expandSpeedRampPoints(points);
130
+ return curve.slice(1).map((end, index) => ({ start: curve[index], end }));
131
+ }
132
+
133
+ export const SPEED_RAMP_EASES = Object.freeze(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic']);
134
+ const EASE_STEPS = 64;
135
+
136
+ function expandSpeedRampPoints(points) {
137
+ if (!points.some((point) => point.ease !== undefined && point.ease !== 'linear')) return points;
138
+ const expanded = [{ sourceOffset: points[0].sourceOffset, playbackRate: points[0].playbackRate }];
139
+ for (let index = 0; index < points.length - 1; index++) {
140
+ const start = points[index];
141
+ const end = points[index + 1];
142
+ const ease = start.ease ?? 'linear';
143
+ const steps = ease === 'linear' ? 1 : EASE_STEPS;
144
+ const sourceSpan = end.sourceOffset - start.sourceOffset;
145
+ const rateSpan = end.playbackRate - start.playbackRate;
146
+ for (let step = 1; step <= steps; step++) {
147
+ const progress = step / steps;
148
+ const eased = evaluateRampEase(ease, progress);
149
+ expanded.push({
150
+ sourceOffset: start.sourceOffset + sourceSpan * progress,
151
+ playbackRate: start.playbackRate + rateSpan * eased,
152
+ });
153
+ }
154
+ }
155
+ return expanded;
156
+ }
157
+
158
+ function evaluateRampEase(ease, progress) {
159
+ switch (ease) {
160
+ case 'easeIn': return progress ** 2;
161
+ case 'easeOut': return 1 - (1 - progress) ** 2;
162
+ case 'easeInOut': return progress < 0.5 ? 2 * progress ** 2 : 1 - ((-2 * progress + 2) ** 2) / 2;
163
+ case 'easeInCubic': return progress ** 3;
164
+ case 'easeOutCubic': return 1 - (1 - progress) ** 3;
165
+ case 'easeInOutCubic': return progress < 0.5 ? 4 * progress ** 3 : 1 - ((-2 * progress + 2) ** 3) / 2;
166
+ default: return progress;
167
+ }
168
+ }
169
+
170
+ function speedRampRateAtOffset(points, offset) {
171
+ const segment = rampSegments(points).find(({ start, end }) => offset <= end.sourceOffset)
172
+ ?? rampSegments(points).at(-1);
173
+ return speedRampRateAt(segment, offset);
174
+ }
175
+
176
+ function speedRampRateAt(segment, offset) {
177
+ const progress = (offset - segment.start.sourceOffset) / (segment.end.sourceOffset - segment.start.sourceOffset);
178
+ return segment.start.playbackRate + (segment.end.playbackRate - segment.start.playbackRate) * progress;
179
+ }
180
+
181
+ function integrateReciprocalRate(sourceSpan, startRate, endRate) {
182
+ const slope = (endRate - startRate) / sourceSpan;
183
+ return Math.abs(slope) < 1e-10 ? sourceSpan / startRate : Math.log(endRate / startRate) / slope;
184
+ }
185
+
186
+ function formatNumber(value) {
187
+ return Number(value.toPrecision(12)).toString();
188
+ }
@@ -1,22 +1,22 @@
1
- /** Return a render-only timeline with hidden tracks and disabled clips removed. */
2
- export function timelineForRender(timeline) {
3
- const soloedAudio = new Set(timeline.tracks
4
- .filter((track) => track.kind === 'audio' && track.solo === true)
5
- .map((track) => track.id));
6
- const tracks = timeline.tracks
7
- .filter((track) => (track.kind === 'audio'
8
- ? track.muted !== true && (soloedAudio.size === 0 || soloedAudio.has(track.id))
9
- : track.visible !== false))
10
- .map((track) => ({ ...track, clips: track.clips.filter((clip) => clip.enabled !== false) }));
11
- const activeTrackIds = new Set(tracks.filter((track) => track.clips.length > 0).map((track) => track.id));
12
- const audioMix = timeline.audioMix === undefined ? undefined : {
13
- ...timeline.audioMix,
14
- ...(timeline.audioMix.ducking !== undefined
15
- ? { ducking: timeline.audioMix.ducking.filter((rule) => activeTrackIds.has(rule.sourceTrackId) && activeTrackIds.has(rule.targetTrackId)) }
16
- : {}),
17
- ...(timeline.audioMix.sidechain !== undefined
18
- ? { sidechain: timeline.audioMix.sidechain.filter((rule) => activeTrackIds.has(rule.sourceTrackId) && activeTrackIds.has(rule.targetTrackId)) }
19
- : {}),
20
- };
21
- return { ...timeline, tracks, ...(audioMix === undefined ? {} : { audioMix }) };
22
- }
1
+ /** Return a render-only timeline with hidden tracks and disabled clips removed. */
2
+ export function timelineForRender(timeline) {
3
+ const soloedAudio = new Set(timeline.tracks
4
+ .filter((track) => track.kind === 'audio' && track.solo === true)
5
+ .map((track) => track.id));
6
+ const tracks = timeline.tracks
7
+ .filter((track) => (track.kind === 'audio'
8
+ ? track.muted !== true && (soloedAudio.size === 0 || soloedAudio.has(track.id))
9
+ : track.visible !== false))
10
+ .map((track) => ({ ...track, clips: track.clips.filter((clip) => clip.enabled !== false) }));
11
+ const activeTrackIds = new Set(tracks.filter((track) => track.clips.length > 0).map((track) => track.id));
12
+ const audioMix = timeline.audioMix === undefined ? undefined : {
13
+ ...timeline.audioMix,
14
+ ...(timeline.audioMix.ducking !== undefined
15
+ ? { ducking: timeline.audioMix.ducking.filter((rule) => activeTrackIds.has(rule.sourceTrackId) && activeTrackIds.has(rule.targetTrackId)) }
16
+ : {}),
17
+ ...(timeline.audioMix.sidechain !== undefined
18
+ ? { sidechain: timeline.audioMix.sidechain.filter((rule) => activeTrackIds.has(rule.sourceTrackId) && activeTrackIds.has(rule.targetTrackId)) }
19
+ : {}),
20
+ };
21
+ return { ...timeline, tracks, ...(audioMix === undefined ? {} : { audioMix }) };
22
+ }
@@ -1,34 +1,34 @@
1
- const TYPES = new Set(['fade', 'wipe']);
2
- const DIRECTIONS = new Set(['left', 'right', 'up', 'down']);
3
-
4
- export function validateClipTransition(transition, clip, label = 'Transition') {
5
- if (!transition || typeof transition !== 'object' || Array.isArray(transition)) {
6
- throw new TypeError(`${label} must be an object.`);
7
- }
8
- const fields = transition.type === 'wipe'
9
- ? ['schemaVersion', 'type', 'direction', 'durationSeconds']
10
- : ['schemaVersion', 'type', 'durationSeconds'];
11
- if (Object.keys(transition).some((field) => !fields.includes(field))) {
12
- throw new TypeError(`${label} contains unsupported fields.`);
13
- }
14
- if (transition.schemaVersion !== 1) throw new TypeError(`${label} schemaVersion must be 1.`);
15
- if (!TYPES.has(transition.type)) throw new TypeError(`${label} type must be fade or wipe.`);
16
- if (!Number.isFinite(transition.durationSeconds) || transition.durationSeconds <= 0 || transition.durationSeconds > clip.durationSeconds) {
17
- throw new TypeError(`${label} durationSeconds must be positive and cannot exceed the clip duration.`);
18
- }
19
- if (transition.type === 'wipe' && !DIRECTIONS.has(transition.direction)) {
20
- throw new TypeError(`${label} direction must be left, right, up, or down.`);
21
- }
22
- if (transition.type === 'fade' && transition.direction !== undefined) {
23
- throw new TypeError(`${label} fade cannot set a direction.`);
24
- }
25
- return transition;
26
- }
27
-
28
- export function transitionClipPath(direction) {
29
- const progress = 'var(--engine-transition-progress,1)';
30
- if (direction === 'left') return `polygon(0 0,calc(100% * ${progress}) 0,calc(100% * ${progress}) 100%,0 100%)`;
31
- if (direction === 'right') return `polygon(calc(100% * (1 - ${progress})) 0,100% 0,100% 100%,calc(100% * (1 - ${progress})) 100%)`;
32
- if (direction === 'up') return `polygon(0 0,100% 0,100% calc(100% * ${progress}),0 calc(100% * ${progress}))`;
33
- return `polygon(0 calc(100% * (1 - ${progress})),100% calc(100% * (1 - ${progress})),100% 100%,0 100%)`;
34
- }
1
+ const TYPES = new Set(['fade', 'wipe']);
2
+ const DIRECTIONS = new Set(['left', 'right', 'up', 'down']);
3
+
4
+ export function validateClipTransition(transition, clip, label = 'Transition') {
5
+ if (!transition || typeof transition !== 'object' || Array.isArray(transition)) {
6
+ throw new TypeError(`${label} must be an object.`);
7
+ }
8
+ const fields = transition.type === 'wipe'
9
+ ? ['schemaVersion', 'type', 'direction', 'durationSeconds']
10
+ : ['schemaVersion', 'type', 'durationSeconds'];
11
+ if (Object.keys(transition).some((field) => !fields.includes(field))) {
12
+ throw new TypeError(`${label} contains unsupported fields.`);
13
+ }
14
+ if (transition.schemaVersion !== 1) throw new TypeError(`${label} schemaVersion must be 1.`);
15
+ if (!TYPES.has(transition.type)) throw new TypeError(`${label} type must be fade or wipe.`);
16
+ if (!Number.isFinite(transition.durationSeconds) || transition.durationSeconds <= 0 || transition.durationSeconds > clip.durationSeconds) {
17
+ throw new TypeError(`${label} durationSeconds must be positive and cannot exceed the clip duration.`);
18
+ }
19
+ if (transition.type === 'wipe' && !DIRECTIONS.has(transition.direction)) {
20
+ throw new TypeError(`${label} direction must be left, right, up, or down.`);
21
+ }
22
+ if (transition.type === 'fade' && transition.direction !== undefined) {
23
+ throw new TypeError(`${label} fade cannot set a direction.`);
24
+ }
25
+ return transition;
26
+ }
27
+
28
+ export function transitionClipPath(direction) {
29
+ const progress = 'var(--engine-transition-progress,1)';
30
+ if (direction === 'left') return `polygon(0 0,calc(100% * ${progress}) 0,calc(100% * ${progress}) 100%,0 100%)`;
31
+ if (direction === 'right') return `polygon(calc(100% * (1 - ${progress})) 0,100% 0,100% 100%,calc(100% * (1 - ${progress})) 100%)`;
32
+ if (direction === 'up') return `polygon(0 0,100% 0,100% calc(100% * ${progress}),0 calc(100% * ${progress}))`;
33
+ return `polygon(0 calc(100% * (1 - ${progress})),100% calc(100% * (1 - ${progress})),100% 100%,0 100%)`;
34
+ }