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

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 +120 -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 +62 -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,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
+ }