@pixerate/editor 0.83.0 → 0.85.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.
- package/dist/video-editor/index.cjs +865 -94
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +364 -9
- package/dist/video-editor/index.d.ts +364 -9
- package/dist/video-editor/index.js +814 -43
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +44 -9
|
@@ -13,8 +13,11 @@ require('../chunk-2FHLHZAL.cjs');
|
|
|
13
13
|
|
|
14
14
|
// src/video-editor/types.ts
|
|
15
15
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
|
-
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe"];
|
|
16
|
+
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
|
|
17
17
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
+
var videoTransitionAudioCurve = (t) => _nullishCoalesce(t.audioCurve, () => ( (t.type === "dip" ? "linear" : "equal-power")));
|
|
19
|
+
var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
|
|
20
|
+
var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
|
|
18
21
|
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
19
22
|
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
20
23
|
var VIDEO_CLIP_TYPES = ["media", "text", "color"];
|
|
@@ -207,6 +210,136 @@ function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
|
|
|
207
210
|
return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
// src/video-editor/grade.ts
|
|
214
|
+
var VIDEO_GRADE_CONTROLS = [
|
|
215
|
+
{ key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
|
|
216
|
+
{ key: "contrast", label: "Contrast", min: -1, max: 1, step: 0.05, description: "Contrast around mid grey, -1 (flat grey) to 1 (twice as steep); default 0." },
|
|
217
|
+
{ key: "saturation", label: "Saturation", min: -1, max: 1, step: 0.05, description: "Saturation, -1 (black and white) to 1 (twice as saturated); default 0." },
|
|
218
|
+
{ key: "hue", label: "Hue", min: -180, max: 180, step: 1, description: "Hue rotation in degrees (default 0)." },
|
|
219
|
+
{ key: "temperature", label: "Temperature", min: -1, max: 1, step: 0.05, description: "White balance, -1 (cool, bluer) to 1 (warm, more orange); default 0." },
|
|
220
|
+
{ key: "tint", label: "Tint", min: -1, max: 1, step: 0.05, description: "White balance, -1 (greener) to 1 (more magenta); default 0." }
|
|
221
|
+
];
|
|
222
|
+
var VIDEO_GRADE_PRESETS = {
|
|
223
|
+
warm: { temperature: 0.4, saturation: 0.1 },
|
|
224
|
+
cool: { temperature: -0.4, tint: -0.05 },
|
|
225
|
+
punchy: { contrast: 0.25, saturation: 0.3 },
|
|
226
|
+
faded: { exposure: 0.15, contrast: -0.3, saturation: -0.3 },
|
|
227
|
+
"black-and-white": { saturation: -1, contrast: 0.15 },
|
|
228
|
+
"bleach-bypass": { saturation: -0.6, contrast: 0.4 },
|
|
229
|
+
"day-for-night": { exposure: -1.5, saturation: -0.4, temperature: -0.6 }
|
|
230
|
+
};
|
|
231
|
+
var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
|
|
232
|
+
var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
|
|
233
|
+
function normalizeVideoGrade(grade) {
|
|
234
|
+
if (!grade) return void 0;
|
|
235
|
+
const out = {};
|
|
236
|
+
for (const c of VIDEO_GRADE_CONTROLS) {
|
|
237
|
+
const v = grade[c.key];
|
|
238
|
+
if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
|
|
239
|
+
}
|
|
240
|
+
return Object.keys(out).length ? out : void 0;
|
|
241
|
+
}
|
|
242
|
+
function videoGradePreset(name, amount = 1) {
|
|
243
|
+
const preset = VIDEO_GRADE_PRESETS[name];
|
|
244
|
+
if (!preset) throw new Error(`There is no grade preset "${name}"; the presets are ${VIDEO_GRADE_PRESET_NAMES.join(", ")}.`);
|
|
245
|
+
const k = Math.min(1, Math.max(0, amount));
|
|
246
|
+
return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
|
|
247
|
+
}
|
|
248
|
+
function videoGradeSteps(grade) {
|
|
249
|
+
const g = normalizeVideoGrade(grade);
|
|
250
|
+
if (!g) return [];
|
|
251
|
+
const steps = [];
|
|
252
|
+
if (g.exposure) steps.push({ kind: "brightness", amount: 2 ** g.exposure });
|
|
253
|
+
if (g.contrast) steps.push({ kind: "contrast", amount: 1 + g.contrast });
|
|
254
|
+
if (g.saturation) steps.push({ kind: "saturate", amount: 1 + g.saturation });
|
|
255
|
+
if (g.hue) steps.push({ kind: "hue-rotate", degrees: g.hue });
|
|
256
|
+
if (g.temperature || g.tint) {
|
|
257
|
+
const t = _nullishCoalesce(g.temperature, () => ( 0));
|
|
258
|
+
steps.push({ kind: "gains", r: 1 + 0.25 * t, g: 1 - 0.25 * (_nullishCoalesce(g.tint, () => ( 0))), b: 1 - 0.25 * t });
|
|
259
|
+
}
|
|
260
|
+
return steps;
|
|
261
|
+
}
|
|
262
|
+
function videoGradeStepMatrix(step) {
|
|
263
|
+
switch (step.kind) {
|
|
264
|
+
case "brightness":
|
|
265
|
+
return [
|
|
266
|
+
[step.amount, 0, 0, 0],
|
|
267
|
+
[0, step.amount, 0, 0],
|
|
268
|
+
[0, 0, step.amount, 0]
|
|
269
|
+
];
|
|
270
|
+
case "contrast": {
|
|
271
|
+
const c = step.amount;
|
|
272
|
+
const o = 0.5 - 0.5 * c;
|
|
273
|
+
return [
|
|
274
|
+
[c, 0, 0, o],
|
|
275
|
+
[0, c, 0, o],
|
|
276
|
+
[0, 0, c, o]
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
case "gains":
|
|
280
|
+
return [
|
|
281
|
+
[step.r, 0, 0, 0],
|
|
282
|
+
[0, step.g, 0, 0],
|
|
283
|
+
[0, 0, step.b, 0]
|
|
284
|
+
];
|
|
285
|
+
case "saturate": {
|
|
286
|
+
const s = step.amount;
|
|
287
|
+
return [
|
|
288
|
+
[0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
|
|
289
|
+
[0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
|
|
290
|
+
[0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0]
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
case "hue-rotate": {
|
|
294
|
+
const a = step.degrees * Math.PI / 180;
|
|
295
|
+
const cos = Math.cos(a);
|
|
296
|
+
const sin = Math.sin(a);
|
|
297
|
+
return [
|
|
298
|
+
[0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, 0],
|
|
299
|
+
[0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.14, 0.072 - cos * 0.072 - sin * 0.283, 0],
|
|
300
|
+
[0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, 0]
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function applyVideoGrade(grade, rgb) {
|
|
306
|
+
let v = rgb.map((x) => x / 255);
|
|
307
|
+
for (const step of videoGradeSteps(grade)) {
|
|
308
|
+
const m = videoGradeStepMatrix(step);
|
|
309
|
+
v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
|
|
310
|
+
}
|
|
311
|
+
return v.map((x) => Math.round(x * 255));
|
|
312
|
+
}
|
|
313
|
+
var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
|
|
314
|
+
function videoGradeStepSvgValues(step) {
|
|
315
|
+
return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
|
|
316
|
+
}
|
|
317
|
+
function videoGradeSvgFilter(id, grade) {
|
|
318
|
+
const steps = videoGradeSteps(grade);
|
|
319
|
+
return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}</filter>`;
|
|
320
|
+
}
|
|
321
|
+
var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
|
|
322
|
+
function videoGradeCssFilter(grade) {
|
|
323
|
+
return videoGradeSteps(grade).map((s) => s.kind === "brightness" ? `brightness(${fmt2(s.amount)})` : s.kind === "contrast" ? `contrast(${fmt2(s.amount)})` : s.kind === "saturate" ? `saturate(${fmt2(s.amount)})` : s.kind === "hue-rotate" ? `hue-rotate(${fmt2(s.degrees)}deg)` : `url(#${videoGradeGainsFilterId(s)})`).join(" ");
|
|
324
|
+
}
|
|
325
|
+
function videoGradeSvgDefs(grade) {
|
|
326
|
+
return videoGradeSteps(grade).filter((s) => s.kind === "gains").map((s) => {
|
|
327
|
+
const id = videoGradeGainsFilterId(s);
|
|
328
|
+
return { id, markup: `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/></filter>` };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function videoGradeFfmpegFilters(grade) {
|
|
332
|
+
return videoGradeSteps(grade).map((step) => {
|
|
333
|
+
const m = videoGradeStepMatrix(step);
|
|
334
|
+
if (step.kind === "brightness" || step.kind === "contrast") {
|
|
335
|
+
const lut = (i) => `clip(val*${fmt2(m[i][i])}+${fmt2(m[i][3] * 255)},0,255)`;
|
|
336
|
+
return `lutrgb=r='${lut(0)}':g='${lut(1)}':b='${lut(2)}'`;
|
|
337
|
+
}
|
|
338
|
+
const k = ["r", "g", "b"];
|
|
339
|
+
return `colorchannelmixer=${m.flatMap((row, i) => row.slice(0, 3).map((v, j) => `${k[i]}${k[j]}=${fmt2(v)}`)).join(":")}`;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
210
343
|
// src/video-editor/parse.ts
|
|
211
344
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
212
345
|
var fail = (path, what) => {
|
|
@@ -275,6 +408,22 @@ function parseVideoTransform(v, path) {
|
|
|
275
408
|
rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
|
|
276
409
|
});
|
|
277
410
|
}
|
|
411
|
+
function parseVideoClipAnimation(v, path) {
|
|
412
|
+
if (v === void 0) return void 0;
|
|
413
|
+
if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
|
|
414
|
+
return compact({
|
|
415
|
+
effect: oneOf(v.effect, VIDEO_CLIP_ANIMATION_EFFECTS, `${path}.effect`, false),
|
|
416
|
+
duration: num(v.duration, `${path}.duration`, { optional: true, positive: true })
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function parseVideoGrade(v, path) {
|
|
420
|
+
if (v === void 0) return void 0;
|
|
421
|
+
if (!isObject(v)) return fail(path, "must be an object such as { exposure, contrast, saturation }");
|
|
422
|
+
const known = new Set(VIDEO_GRADE_CONTROLS.map((c) => c.key));
|
|
423
|
+
const unknown = Object.keys(v).find((k) => !known.has(k));
|
|
424
|
+
if (unknown) fail(`${path}.${unknown}`, `is not a grade setting; use ${[...known].join(", ")}`);
|
|
425
|
+
return compact(Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])));
|
|
426
|
+
}
|
|
278
427
|
function parseVideoTransition(v, path) {
|
|
279
428
|
if (v === void 0) return void 0;
|
|
280
429
|
if (!isObject(v)) return fail(path, "must be an object { type, duration }");
|
|
@@ -282,8 +431,21 @@ function parseVideoTransition(v, path) {
|
|
|
282
431
|
type: oneOf(v.type, VIDEO_TRANSITION_TYPES, `${path}.type`, false),
|
|
283
432
|
duration: num(v.duration, `${path}.duration`, { positive: true }),
|
|
284
433
|
color: str(v.color, `${path}.color`, true),
|
|
285
|
-
direction: oneOf(v.direction, VIDEO_WIPE_DIRECTIONS, `${path}.direction`)
|
|
434
|
+
direction: oneOf(v.direction, VIDEO_WIPE_DIRECTIONS, `${path}.direction`),
|
|
435
|
+
audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function parseVideoTranscriptWords(v, path) {
|
|
439
|
+
if (v === void 0) return void 0;
|
|
440
|
+
if (!Array.isArray(v)) return fail(path, "must be an array of { word, start, end } (ms in the source)");
|
|
441
|
+
const words = v.map((w, i) => {
|
|
442
|
+
if (!isObject(w)) return fail(`${path}[${i}]`, "must be an object { word, start, end }");
|
|
443
|
+
const word = str(w.word, `${path}[${i}].word`);
|
|
444
|
+
const start = num(w.start, `${path}[${i}].start`, { min: 0 });
|
|
445
|
+
const end = num(w.end, `${path}[${i}].end`, { min: start });
|
|
446
|
+
return { word, start, end };
|
|
286
447
|
});
|
|
448
|
+
return words.sort((a, b) => a.start - b.start);
|
|
287
449
|
}
|
|
288
450
|
function parseVideoAsset(v, path = "asset") {
|
|
289
451
|
if (!isObject(v)) return fail(path, "must be an object");
|
|
@@ -297,8 +459,10 @@ function parseVideoAsset(v, path = "asset") {
|
|
|
297
459
|
width: num(v.width, `${path}.width`, { optional: true, positive: true }),
|
|
298
460
|
height: num(v.height, `${path}.height`, { optional: true, positive: true }),
|
|
299
461
|
hasAudio: bool(v.hasAudio, `${path}.hasAudio`),
|
|
300
|
-
poster: str(v.poster, `${path}.poster`, true)
|
|
462
|
+
poster: str(v.poster, `${path}.poster`, true),
|
|
463
|
+
transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`)
|
|
301
464
|
});
|
|
465
|
+
if (kind === "image" && asset.transcript) fail(`${path}.transcript`, "is only for video and audio assets");
|
|
302
466
|
if (kind === "image" && asset.duration !== void 0) fail(`${path}.duration`, "is only for video and audio assets");
|
|
303
467
|
return asset;
|
|
304
468
|
}
|
|
@@ -313,6 +477,10 @@ function parseVideoClip(v, path = "clip") {
|
|
|
313
477
|
opacity: parseVideoAnimatable(v.opacity, `${path}.opacity`, { optional: true, min: 0, max: 1 }),
|
|
314
478
|
transform: parseVideoTransform(v.transform, `${path}.transform`),
|
|
315
479
|
fit: oneOf(v.fit, VIDEO_FITS, `${path}.fit`),
|
|
480
|
+
blend: oneOf(v.blend, VIDEO_BLEND_MODES, `${path}.blend`),
|
|
481
|
+
grade: parseVideoGrade(v.grade, `${path}.grade`),
|
|
482
|
+
animateIn: parseVideoClipAnimation(v.animateIn, `${path}.animateIn`),
|
|
483
|
+
animateOut: parseVideoClipAnimation(v.animateOut, `${path}.animateOut`),
|
|
316
484
|
fadeIn: num(v.fadeIn, `${path}.fadeIn`, { optional: true, min: 0 }),
|
|
317
485
|
fadeOut: num(v.fadeOut, `${path}.fadeOut`, { optional: true, min: 0 }),
|
|
318
486
|
transition: parseVideoTransition(v.transition, `${path}.transition`)
|
|
@@ -563,12 +731,72 @@ function videoClipsAt(project, time) {
|
|
|
563
731
|
return out;
|
|
564
732
|
}
|
|
565
733
|
|
|
734
|
+
// src/video-editor/clip-animation.ts
|
|
735
|
+
var VIDEO_CLIP_ANIMATION_DURATION = 500;
|
|
736
|
+
var EFFECTS = {
|
|
737
|
+
fade: { easeIn: "linear", easeOut: "linear" },
|
|
738
|
+
rise: { y: 0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
739
|
+
drop: { y: -0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
740
|
+
"slide-left": { x: 0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
741
|
+
"slide-right": { x: -0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
742
|
+
zoom: { scale: 0.6, easeIn: "ease-out", easeOut: "ease-in" },
|
|
743
|
+
// Overshoots past full size, then settles (a "back" curve).
|
|
744
|
+
pop: { scale: 0.3, easeIn: [0.34, 1.56, 0.64, 1], easeOut: [0.36, 0, 0.66, -0.56] }
|
|
745
|
+
};
|
|
746
|
+
var PROPS = ["opacity", "x", "y", "scale"];
|
|
747
|
+
function videoClipAnimationProperties(animation) {
|
|
748
|
+
const spec = EFFECTS[animation.effect];
|
|
749
|
+
return PROPS.filter((p) => p === "opacity" || spec[p] !== void 0);
|
|
750
|
+
}
|
|
751
|
+
function videoClipAnimationSpans(clip) {
|
|
752
|
+
const a = clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
|
|
753
|
+
const b = clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
|
|
754
|
+
const k = a + b > clip.duration ? clip.duration / (a + b) : 1;
|
|
755
|
+
return { in: a * k, out: b * k };
|
|
756
|
+
}
|
|
757
|
+
function videoClipAnimated(clip, project) {
|
|
758
|
+
if (!clip.animateIn && !clip.animateOut) return clip;
|
|
759
|
+
const spans = videoClipAnimationSpans(clip);
|
|
760
|
+
const out = { ...clip, transform: { ...clip.transform } };
|
|
761
|
+
for (const prop of PROPS) {
|
|
762
|
+
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _4 => _4.transform, 'optionalAccess', _5 => _5[prop]]);
|
|
763
|
+
if (isVideoKeyframes(current)) continue;
|
|
764
|
+
const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
|
|
765
|
+
const away = (spec) => {
|
|
766
|
+
if (prop === "opacity") return 0;
|
|
767
|
+
const v = spec[prop];
|
|
768
|
+
if (v === void 0) return void 0;
|
|
769
|
+
return prop === "scale" ? base * v : base + v * (prop === "x" ? project.width : project.height);
|
|
770
|
+
};
|
|
771
|
+
const keys = [];
|
|
772
|
+
if (clip.animateIn) {
|
|
773
|
+
const spec = EFFECTS[clip.animateIn.effect];
|
|
774
|
+
const from = away(spec);
|
|
775
|
+
if (from !== void 0) keys.push({ t: 0, v: from, ease: spec.easeIn }, { t: spans.in, v: base });
|
|
776
|
+
}
|
|
777
|
+
if (clip.animateOut) {
|
|
778
|
+
const spec = EFFECTS[clip.animateOut.effect];
|
|
779
|
+
const to = away(spec);
|
|
780
|
+
if (to !== void 0) {
|
|
781
|
+
const start = clip.duration - spans.out;
|
|
782
|
+
if (!keys.length || keys[keys.length - 1].t < start) keys.push({ t: start, v: base, ease: spec.easeOut });
|
|
783
|
+
else keys[keys.length - 1] = { ...keys[keys.length - 1], ease: spec.easeOut };
|
|
784
|
+
keys.push({ t: clip.duration, v: to });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (!keys.length) continue;
|
|
788
|
+
if (prop === "opacity") out.opacity = { keys };
|
|
789
|
+
else out.transform[prop] = { keys };
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
|
|
566
794
|
// src/video-editor/check.ts
|
|
567
795
|
function videoClipKeyframes(clip) {
|
|
568
796
|
const out = [
|
|
569
797
|
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
570
798
|
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
571
|
-
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access',
|
|
799
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _6 => _6.transform, 'optionalAccess', _7 => _7[p]])) }))
|
|
572
800
|
];
|
|
573
801
|
return out.filter((o) => o.keys.length);
|
|
574
802
|
}
|
|
@@ -581,6 +809,8 @@ var VIDEO_ISSUE_CODES = [
|
|
|
581
809
|
"transition-too-long",
|
|
582
810
|
"transition-handles",
|
|
583
811
|
"fades-too-long",
|
|
812
|
+
"animations-too-long",
|
|
813
|
+
"animation-keyed",
|
|
584
814
|
"keyframes-outside-clip",
|
|
585
815
|
"unknown-duration",
|
|
586
816
|
"unused-asset",
|
|
@@ -617,6 +847,16 @@ function checkVideoProject(project) {
|
|
|
617
847
|
issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}" from ${formatVideoSeconds(next.start)}; move one of them.` });
|
|
618
848
|
if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + VIDEO_TIME_EPSILON)
|
|
619
849
|
issues.push({ code: "fades-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s fades (${_nullishCoalesce(clip.fadeIn, () => ( 0))} + ${_nullishCoalesce(clip.fadeOut, () => ( 0))} ms) are longer than the clip (${clip.duration} ms); shorten them.` });
|
|
850
|
+
const anim = (clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0) + (clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0);
|
|
851
|
+
if (anim > clip.duration + VIDEO_TIME_EPSILON)
|
|
852
|
+
issues.push({ code: "animations-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s entrance and exit (${anim} ms together) are longer than the clip (${clip.duration} ms), so both are shortened; give them shorter durations.` });
|
|
853
|
+
for (const which of ["animateIn", "animateOut"]) {
|
|
854
|
+
const a = clip[which];
|
|
855
|
+
if (!a) continue;
|
|
856
|
+
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _8 => _8.transform, 'optionalAccess', _9 => _9[p]])).length);
|
|
857
|
+
if (keyed.length)
|
|
858
|
+
issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
|
|
859
|
+
}
|
|
620
860
|
for (const { property, keys } of videoClipKeyframes(clip)) {
|
|
621
861
|
const outside = keys.filter((k) => k.t < -VIDEO_TIME_EPSILON || k.t > clip.duration + VIDEO_TIME_EPSILON);
|
|
622
862
|
if (outside.length)
|
|
@@ -721,7 +961,7 @@ function clipAnimatables(clip) {
|
|
|
721
961
|
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
722
962
|
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
723
963
|
];
|
|
724
|
-
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access',
|
|
964
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _10 => _10.transform, 'optionalAccess', _11 => _11[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
725
965
|
return out;
|
|
726
966
|
}
|
|
727
967
|
function retimeClipKeys(clip, fn) {
|
|
@@ -730,7 +970,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
730
970
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
731
971
|
}
|
|
732
972
|
}
|
|
733
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
973
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
734
974
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
735
975
|
var Editor = (_class = class {
|
|
736
976
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
@@ -769,7 +1009,7 @@ var Editor = (_class = class {
|
|
|
769
1009
|
kindFits(track, clip) {
|
|
770
1010
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
771
1011
|
if (track.kind === "audio" && (clip.type !== "media" || asset.kind === "image")) refuse(`Track "${track.id}" is an audio track; ${clip.type === "media" ? "images" : `${clip.type} clips`} go on a video track.`);
|
|
772
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
1012
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _12 => _12.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
773
1013
|
}
|
|
774
1014
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
775
1015
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
@@ -811,7 +1051,7 @@ var Editor = (_class = class {
|
|
|
811
1051
|
}
|
|
812
1052
|
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
813
1053
|
moveIn(clip, delta) {
|
|
814
|
-
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call',
|
|
1054
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _13 => _13(this.p), 'access', _14 => _14.get, 'call', _15 => _15(clip.asset), 'optionalAccess', _16 => _16.kind]) === "image") return;
|
|
815
1055
|
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
816
1056
|
if (clip.in < 1e-9) delete clip.in;
|
|
817
1057
|
}
|
|
@@ -827,6 +1067,7 @@ var Editor = (_class = class {
|
|
|
827
1067
|
delete right.transition;
|
|
828
1068
|
delete right.fadeIn;
|
|
829
1069
|
delete right.fadeInCurve;
|
|
1070
|
+
delete right.animateIn;
|
|
830
1071
|
const leftValues = clipAnimatables(clip);
|
|
831
1072
|
clipAnimatables(right).forEach((r, i) => {
|
|
832
1073
|
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), at - clip.start);
|
|
@@ -836,6 +1077,7 @@ var Editor = (_class = class {
|
|
|
836
1077
|
this.moveIn(right, at - clip.start);
|
|
837
1078
|
clip.duration = at - clip.start;
|
|
838
1079
|
delete clip.fadeOut;
|
|
1080
|
+
delete clip.animateOut;
|
|
839
1081
|
if (clip.type === "media") delete clip.fadeOutCurve;
|
|
840
1082
|
track.clips.push(right);
|
|
841
1083
|
track.clips.sort((a, b) => a.start - b.start);
|
|
@@ -1075,7 +1317,7 @@ var Editor = (_class = class {
|
|
|
1075
1317
|
if (clip.type === "media") {
|
|
1076
1318
|
const asset = assets.get(clip.asset);
|
|
1077
1319
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
1078
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1320
|
+
if (_optionalChain([asset, 'optionalAccess', _17 => _17.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
1079
1321
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
1080
1322
|
}
|
|
1081
1323
|
if (e.ripple) {
|
|
@@ -1131,7 +1373,7 @@ var Editor = (_class = class {
|
|
|
1131
1373
|
let stretch = 1;
|
|
1132
1374
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
1133
1375
|
const was = videoClipSpeed(at.clip);
|
|
1134
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1376
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _18 => _18.remove, 'optionalAccess', _19 => _19.includes, 'call', _20 => _20("speed")]) ? 1 : was;
|
|
1135
1377
|
if (now !== was) {
|
|
1136
1378
|
set.duration = at.clip.duration * was / now;
|
|
1137
1379
|
stretch = was / now;
|
|
@@ -1227,7 +1469,7 @@ var Editor = (_class = class {
|
|
|
1227
1469
|
this.trimHead(after, delta);
|
|
1228
1470
|
}
|
|
1229
1471
|
track.clips.sort((a, b) => a.start - b.start);
|
|
1230
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess',
|
|
1472
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _21 => _21.id]), clip.id, _optionalChain([after, 'optionalAccess', _22 => _22.id])].filter((x) => !!x), track: track.id, delta });
|
|
1231
1473
|
}
|
|
1232
1474
|
case "lift_range":
|
|
1233
1475
|
case "extract_range": {
|
|
@@ -1258,7 +1500,7 @@ var Editor = (_class = class {
|
|
|
1258
1500
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1259
1501
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1260
1502
|
const c = clip;
|
|
1261
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1503
|
+
const current = transform ? _optionalChain([c, 'access', _23 => _23.transform, 'optionalAccess', _24 => _24[e.property]]) : c[e.property];
|
|
1262
1504
|
let value;
|
|
1263
1505
|
if (e.keys === null) {
|
|
1264
1506
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1340,7 +1582,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1340
1582
|
editor.run(edit);
|
|
1341
1583
|
} catch (error) {
|
|
1342
1584
|
const message = error.message;
|
|
1343
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1585
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _25 => _25.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1344
1586
|
}
|
|
1345
1587
|
}
|
|
1346
1588
|
let next;
|
|
@@ -1359,7 +1601,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1359
1601
|
|
|
1360
1602
|
// src/video-editor/evaluate.ts
|
|
1361
1603
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1362
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1604
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _26 => _26.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _27 => _27.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _28 => _28.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.rotation]), clipTime, 0) };
|
|
1363
1605
|
}
|
|
1364
1606
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1365
1607
|
let w = size.width;
|
|
@@ -1387,6 +1629,7 @@ function videoTitleFrame(project, clip, clipTime = 0) {
|
|
|
1387
1629
|
return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
|
|
1388
1630
|
}
|
|
1389
1631
|
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
1632
|
+
var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp01(progress))) / 2;
|
|
1390
1633
|
function videoFadeCurveGain(curve, x) {
|
|
1391
1634
|
const t = clamp01(x);
|
|
1392
1635
|
switch (curve) {
|
|
@@ -1405,8 +1648,8 @@ function videoFadeCurveGain(curve, x) {
|
|
|
1405
1648
|
function fadeGain(clip, time, sound = false) {
|
|
1406
1649
|
let g = 1;
|
|
1407
1650
|
const shape = sound && clip.type === "media" ? clip : void 0;
|
|
1408
|
-
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1409
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1651
|
+
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _30 => _30.fadeInCurve]), (time - clip.start) / clip.fadeIn);
|
|
1652
|
+
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _31 => _31.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
1410
1653
|
return g;
|
|
1411
1654
|
}
|
|
1412
1655
|
function videoClipSoundGain(clip, clipTime) {
|
|
@@ -1419,7 +1662,7 @@ function activeOnTrack(track, time) {
|
|
|
1419
1662
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1420
1663
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1421
1664
|
const next = videoClipAfter(track, clip);
|
|
1422
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1665
|
+
const tail = _optionalChain([next, 'optionalAccess', _32 => _32.transition]) ? next.transition.duration / 2 : 0;
|
|
1423
1666
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1424
1667
|
const a = { clip };
|
|
1425
1668
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1436,12 +1679,12 @@ function videoFrameAt(project, time) {
|
|
|
1436
1679
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1437
1680
|
const track = project.tracks[ti];
|
|
1438
1681
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1439
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1440
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1682
|
+
if (_optionalChain([incoming, 'optionalAccess', _33 => _33.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1683
|
+
if (_optionalChain([outgoing, 'optionalAccess', _34 => _34.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1441
1684
|
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
1442
1685
|
const fade = fadeGain(clip, inside);
|
|
1443
|
-
const level = (t, into) => t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress;
|
|
1444
|
-
const mix = (incoming ? level(incoming, true) : 1) * (outgoing ? level(outgoing, false) : 1);
|
|
1686
|
+
const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
|
|
1687
|
+
const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
|
|
1445
1688
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
1446
1689
|
const sourceTime = sourceTimeAt(clip, time);
|
|
1447
1690
|
const clipTime = time - clip.start;
|
|
@@ -1452,20 +1695,39 @@ function videoFrameAt(project, time) {
|
|
|
1452
1695
|
}
|
|
1453
1696
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1454
1697
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
1455
|
-
|
|
1698
|
+
const shown = videoClipAnimated(clip, project);
|
|
1699
|
+
let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
|
|
1456
1700
|
let wipe;
|
|
1457
1701
|
let dip = null;
|
|
1702
|
+
let iris;
|
|
1703
|
+
let shift = 0;
|
|
1704
|
+
let shiftDirection = "right";
|
|
1458
1705
|
if (incoming) {
|
|
1459
1706
|
if (incoming.type === "crossfade") opacity *= incoming.progress;
|
|
1460
1707
|
else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
|
|
1461
|
-
else
|
|
1708
|
+
else if (incoming.type === "iris") iris = { progress: incoming.progress };
|
|
1709
|
+
else if (incoming.type === "push" || incoming.type === "slide") {
|
|
1710
|
+
shiftDirection = _nullishCoalesce(incoming.transition.direction, () => ( "right"));
|
|
1711
|
+
shift = videoTransitionEase(incoming.progress) - 1;
|
|
1712
|
+
} else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1713
|
+
}
|
|
1714
|
+
if (_optionalChain([outgoing, 'optionalAccess', _35 => _35.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1715
|
+
if (_optionalChain([outgoing, 'optionalAccess', _36 => _36.type]) === "push") {
|
|
1716
|
+
shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
|
|
1717
|
+
shift = videoTransitionEase(outgoing.progress);
|
|
1462
1718
|
}
|
|
1463
|
-
if (_optionalChain([outgoing, 'optionalAccess', _31 => _31.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1464
1719
|
const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
1465
1720
|
const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1721
|
+
const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
|
|
1722
|
+
const across = shiftDirection === "left" || shiftDirection === "right";
|
|
1723
|
+
const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
|
|
1724
|
+
const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
|
|
1725
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _37 => _37.transform, 'optionalAccess', _38 => _38.rotation]), clipTime, 0) };
|
|
1468
1726
|
if (wipe) layer.wipe = wipe;
|
|
1727
|
+
if (iris) layer.iris = iris;
|
|
1728
|
+
if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
|
|
1729
|
+
const grade = normalizeVideoGrade(clip.grade);
|
|
1730
|
+
if (grade) layer.grade = grade;
|
|
1469
1731
|
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
1470
1732
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
1471
1733
|
if (clip.type === "color") layer.color = clip.color;
|
|
@@ -1693,9 +1955,175 @@ var VideoAudioPeaksCache = (_class2 = class {
|
|
|
1693
1955
|
}
|
|
1694
1956
|
}, _class2);
|
|
1695
1957
|
|
|
1958
|
+
// src/video-editor/transcript.ts
|
|
1959
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1960
|
+
function cueTime(s) {
|
|
1961
|
+
const m = s.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
|
|
1962
|
+
if (!m) throw new Error(`"${s.trim()}" is not a cue time (HH:MM:SS,mmm).`);
|
|
1963
|
+
return ((Number(_nullishCoalesce(m[1], () => ( 0))) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1e3 + Number(m[4].padEnd(3, "0"));
|
|
1964
|
+
}
|
|
1965
|
+
function cueWords(text, start, end) {
|
|
1966
|
+
const words = text.replace(/<[^>]+>/g, "").split(/\s+/).filter(Boolean);
|
|
1967
|
+
const step = (end - start) / Math.max(1, words.length);
|
|
1968
|
+
return words.map((word, i) => ({ word, start: Math.round(start + i * step), end: Math.round(start + (i + 1) * step) }));
|
|
1969
|
+
}
|
|
1970
|
+
function parseVideoTranscript(input) {
|
|
1971
|
+
let data = input;
|
|
1972
|
+
if (typeof input === "string") {
|
|
1973
|
+
const text = input.replace(/^/, "").trim();
|
|
1974
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
1975
|
+
try {
|
|
1976
|
+
data = JSON.parse(text);
|
|
1977
|
+
} catch (e3) {
|
|
1978
|
+
throw new Error("The transcript looks like JSON but is not valid JSON.");
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
const words2 = [];
|
|
1982
|
+
for (const block of text.replace(/\r\n?/g, "\n").split(/\n{2,}/)) {
|
|
1983
|
+
const lines = block.split("\n");
|
|
1984
|
+
const at = lines.findIndex((l) => l.includes("-->"));
|
|
1985
|
+
if (at < 0) continue;
|
|
1986
|
+
const [a, b] = lines[at].split("-->");
|
|
1987
|
+
words2.push(...cueWords(lines.slice(at + 1).join(" "), cueTime(a), cueTime(b.trim().split(/\s+/)[0])));
|
|
1988
|
+
}
|
|
1989
|
+
if (!words2.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
|
|
1990
|
+
return { words: words2, approximate: true };
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const seconds = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e3) : NaN;
|
|
1994
|
+
const fromList = (list, toMs) => list.map((w, i) => {
|
|
1995
|
+
if (!isRecord(w)) throw new Error(`Word ${i + 1} is not an object { word, start, end }.`);
|
|
1996
|
+
const word = String(_nullishCoalesce(_nullishCoalesce(w.word, () => ( w.text)), () => ( ""))).trim();
|
|
1997
|
+
const start = toMs(w.start);
|
|
1998
|
+
const end = toMs(w.end);
|
|
1999
|
+
if (!word || !Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`Word ${i + 1} needs a word and numeric start and end.`);
|
|
2000
|
+
return { word, start, end: Math.max(start, end) };
|
|
2001
|
+
});
|
|
2002
|
+
let words = [];
|
|
2003
|
+
if (Array.isArray(data)) words = fromList(data, (v) => typeof v === "number" ? v : NaN);
|
|
2004
|
+
else if (isRecord(data) && Array.isArray(data.words)) words = fromList(data.words, seconds);
|
|
2005
|
+
else if (isRecord(data) && Array.isArray(data.segments)) {
|
|
2006
|
+
const segments = data.segments.filter(isRecord);
|
|
2007
|
+
if (segments.some((s) => Array.isArray(s.words))) words = fromList(segments.flatMap((s) => Array.isArray(s.words) ? s.words : []), seconds);
|
|
2008
|
+
else {
|
|
2009
|
+
for (const s of segments) words.push(...cueWords(String(_nullishCoalesce(s.text, () => ( ""))), seconds(s.start), seconds(s.end)));
|
|
2010
|
+
if (!words.length) throw new Error("No words found in the segments.");
|
|
2011
|
+
return { words, approximate: true };
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (!words.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
|
|
2015
|
+
return { words: words.sort((a, b) => a.start - b.start), approximate: false };
|
|
2016
|
+
}
|
|
2017
|
+
function videoTranscriptTrack(project, track) {
|
|
2018
|
+
if (track !== void 0) {
|
|
2019
|
+
if (!project.tracks.some((t) => t.id === track)) throw new Error(`There is no track "${track}"; the tracks are ${project.tracks.map((t) => `"${t.id}"`).join(", ")}.`);
|
|
2020
|
+
return track;
|
|
2021
|
+
}
|
|
2022
|
+
const assets = videoAssetMap(project);
|
|
2023
|
+
const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _39 => _39.get, 'call', _40 => _40(c.asset), 'optionalAccess', _41 => _41.transcript, 'optionalAccess', _42 => _42.length]));
|
|
2024
|
+
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _43 => _43.id]), () => ( null));
|
|
2025
|
+
}
|
|
2026
|
+
function videoTimelineTranscript(project, options = {}) {
|
|
2027
|
+
const id = videoTranscriptTrack(project, options.track);
|
|
2028
|
+
if (!id) return [];
|
|
2029
|
+
const track = project.tracks.find((t) => t.id === id);
|
|
2030
|
+
const assets = videoAssetMap(project);
|
|
2031
|
+
const out = [];
|
|
2032
|
+
for (const clip of sortedVideoClips(track)) {
|
|
2033
|
+
if (clip.type !== "media") continue;
|
|
2034
|
+
const words = _optionalChain([assets, 'access', _44 => _44.get, 'call', _45 => _45(clip.asset), 'optionalAccess', _46 => _46.transcript]);
|
|
2035
|
+
if (!_optionalChain([words, 'optionalAccess', _47 => _47.length])) continue;
|
|
2036
|
+
const speed = videoClipSpeed(clip);
|
|
2037
|
+
const from = _nullishCoalesce(clip.in, () => ( 0));
|
|
2038
|
+
const to = from + clip.duration * speed;
|
|
2039
|
+
const toTimeline = (s) => Math.min(videoClipEnd(clip), Math.max(clip.start, clip.start + (s - from) / speed));
|
|
2040
|
+
words.forEach((w, index) => {
|
|
2041
|
+
const mid = (w.start + w.end) / 2;
|
|
2042
|
+
if (mid < from || mid >= to) return;
|
|
2043
|
+
out.push({ word: w.word, start: toTimeline(w.start), end: toTimeline(w.end), clip: clip.id, track: track.id, index });
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
return out;
|
|
2047
|
+
}
|
|
2048
|
+
var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
|
|
2049
|
+
var rangeOf = (words, i, n2) => ({ from: words[i].start, to: words[i + n2 - 1].end, text: words.slice(i, i + n2).map((w) => w.word).join(" ") });
|
|
2050
|
+
function findVideoTranscriptText(words, query) {
|
|
2051
|
+
const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
|
|
2052
|
+
if (!q.length) throw new Error("Search for at least one word.");
|
|
2053
|
+
const norm = words.map((w) => normalizeVideoWord(w.word));
|
|
2054
|
+
const out = [];
|
|
2055
|
+
for (let i = 0; i + q.length <= norm.length; i++) if (q.every((x, k) => norm[i + k] === x)) out.push(rangeOf(words, i, q.length));
|
|
2056
|
+
return out;
|
|
2057
|
+
}
|
|
2058
|
+
var VIDEO_DEFAULT_FILLERS = ["um", "umm", "uh", "uhh", "uhm", "erm", "er", "ah", "hmm", "mm", "you know", "i mean"];
|
|
2059
|
+
function findVideoFillers(words, fillers = VIDEO_DEFAULT_FILLERS) {
|
|
2060
|
+
const phrases = fillers.map((f) => f.split(/\s+/).map(normalizeVideoWord).filter(Boolean)).filter((p) => p.length).sort((a, b) => b.length - a.length);
|
|
2061
|
+
const norm = words.map((w) => normalizeVideoWord(w.word));
|
|
2062
|
+
const out = [];
|
|
2063
|
+
for (let i = 0; i < norm.length; ) {
|
|
2064
|
+
const hit = phrases.find((p) => i + p.length <= norm.length && p.every((x, k) => norm[i + k] === x));
|
|
2065
|
+
if (hit) {
|
|
2066
|
+
out.push(rangeOf(words, i, hit.length));
|
|
2067
|
+
i += hit.length;
|
|
2068
|
+
} else i++;
|
|
2069
|
+
}
|
|
2070
|
+
return out;
|
|
2071
|
+
}
|
|
2072
|
+
function findVideoTranscriptPauses(words, options = {}) {
|
|
2073
|
+
const min = _nullishCoalesce(options.min, () => ( 700));
|
|
2074
|
+
const keep = _nullishCoalesce(options.keep, () => ( 150));
|
|
2075
|
+
const out = [];
|
|
2076
|
+
for (let i = 1; i < words.length; i++) {
|
|
2077
|
+
const a = words[i - 1];
|
|
2078
|
+
const b = words[i];
|
|
2079
|
+
if (a.clip !== b.clip || b.start - a.end < min) continue;
|
|
2080
|
+
let from = a.end + keep;
|
|
2081
|
+
let to = b.start - keep;
|
|
2082
|
+
if (options.fps) {
|
|
2083
|
+
const f = 1e3 / options.fps;
|
|
2084
|
+
from = snapToVideoFrame(Math.ceil(from / f - 1e-9) * f, options.fps);
|
|
2085
|
+
to = snapToVideoFrame(Math.floor(to / f + 1e-9) * f, options.fps);
|
|
2086
|
+
}
|
|
2087
|
+
if (to - from > VIDEO_TIME_EPSILON) out.push({ from, to, text: `(pause after "${a.word}")` });
|
|
2088
|
+
}
|
|
2089
|
+
return out;
|
|
2090
|
+
}
|
|
2091
|
+
function mergeVideoRanges(ranges) {
|
|
2092
|
+
const sorted = ranges.filter((r) => r.to - r.from > VIDEO_TIME_EPSILON).map((r) => ({ from: r.from, to: r.to })).sort((a, b) => a.from - b.from);
|
|
2093
|
+
const out = [];
|
|
2094
|
+
for (const r of sorted) {
|
|
2095
|
+
const last = out[out.length - 1];
|
|
2096
|
+
if (last && r.from <= last.to + VIDEO_TIME_EPSILON) last.to = Math.max(last.to, r.to);
|
|
2097
|
+
else out.push(r);
|
|
2098
|
+
}
|
|
2099
|
+
return out;
|
|
2100
|
+
}
|
|
2101
|
+
function videoRemoveRangesEdits(ranges, tracks) {
|
|
2102
|
+
return mergeVideoRanges(ranges).reverse().map((r) => ({ op: "extract_range", from: r.from, to: r.to, ...tracks ? { tracks } : {} }));
|
|
2103
|
+
}
|
|
2104
|
+
function videoWordsRange(words, a, b) {
|
|
2105
|
+
const [i, j] = a <= b ? [a, b] : [b, a];
|
|
2106
|
+
return { from: words[i].start, to: words[j].end };
|
|
2107
|
+
}
|
|
2108
|
+
function videoTranscriptEdit(assetId, text) {
|
|
2109
|
+
return { op: "update_asset", asset: assetId, set: { transcript: parseVideoTranscript(text).words } };
|
|
2110
|
+
}
|
|
2111
|
+
function videoWordAt(words, time) {
|
|
2112
|
+
let lo = 0;
|
|
2113
|
+
let hi = words.length - 1;
|
|
2114
|
+
while (lo <= hi) {
|
|
2115
|
+
const mid = lo + hi >> 1;
|
|
2116
|
+
if (words[mid].end <= time) lo = mid + 1;
|
|
2117
|
+
else if (words[mid].start > time) hi = mid - 1;
|
|
2118
|
+
else return mid;
|
|
2119
|
+
}
|
|
2120
|
+
return -1;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1696
2123
|
// src/video-editor/render.ts
|
|
1697
2124
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1698
2125
|
var n = (v) => String(Math.round(v * 100) / 100);
|
|
2126
|
+
var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
|
|
1699
2127
|
function videoWipeRect(rect, wipe) {
|
|
1700
2128
|
const { direction, progress } = wipe;
|
|
1701
2129
|
const w = rect.width * progress;
|
|
@@ -1713,6 +2141,11 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1713
2141
|
const { rect } = layer;
|
|
1714
2142
|
const attrs = [];
|
|
1715
2143
|
if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
|
|
2144
|
+
if (layer.blend) attrs.push(`style="mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}"`);
|
|
2145
|
+
if (layer.grade) {
|
|
2146
|
+
defs.push(videoGradeSvgFilter(`g${i}`, layer.grade));
|
|
2147
|
+
attrs.push(`filter="url(#g${i})"`);
|
|
2148
|
+
}
|
|
1716
2149
|
if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
|
|
1717
2150
|
if (layer.wipe) {
|
|
1718
2151
|
const r = videoWipeRect(rect, layer.wipe);
|
|
@@ -1744,7 +2177,12 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1744
2177
|
inner = `<rect ${box} fill="#334155" stroke="#94a3b8" stroke-width="2"/><text x="${n(rect.x + rect.width / 2)}" y="${n(rect.y + rect.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="${n(size)}" fill="#e2e8f0">${esc(label)}</text>`;
|
|
1745
2178
|
}
|
|
1746
2179
|
}
|
|
1747
|
-
|
|
2180
|
+
let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
|
|
2181
|
+
if (layer.iris) {
|
|
2182
|
+
defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
|
|
2183
|
+
item = `<g clip-path="url(#i${i})">${item}</g>`;
|
|
2184
|
+
}
|
|
2185
|
+
body.push(item);
|
|
1748
2186
|
});
|
|
1749
2187
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body.join("")}</svg>`;
|
|
1750
2188
|
}
|
|
@@ -1768,7 +2206,7 @@ function videoContactSheetSvg(project, times, options = {}) {
|
|
|
1768
2206
|
});
|
|
1769
2207
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(W)} ${n(H)}" width="${n(W)}" height="${n(H)}"><rect width="100%" height="100%" fill="#f8fafc"/>${cells.join("")}</svg>`;
|
|
1770
2208
|
}
|
|
1771
|
-
function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
2209
|
+
function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
1772
2210
|
ctx.save();
|
|
1773
2211
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
1774
2212
|
ctx.globalAlpha = 1;
|
|
@@ -1778,6 +2216,18 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1778
2216
|
const { rect } = layer;
|
|
1779
2217
|
ctx.save();
|
|
1780
2218
|
ctx.globalAlpha = layer.opacity;
|
|
2219
|
+
if (layer.iris) {
|
|
2220
|
+
ctx.beginPath();
|
|
2221
|
+
ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
|
|
2222
|
+
ctx.clip();
|
|
2223
|
+
}
|
|
2224
|
+
if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
|
|
2225
|
+
if (layer.grade) {
|
|
2226
|
+
const defs = videoGradeSvgDefs(layer.grade);
|
|
2227
|
+
if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
|
|
2228
|
+
const css = videoGradeCssFilter(layer.grade);
|
|
2229
|
+
ctx.filter = options.defineSvgFilter ? css : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
|
|
2230
|
+
}
|
|
1781
2231
|
if (layer.rotation) {
|
|
1782
2232
|
ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1783
2233
|
ctx.rotate(layer.rotation * Math.PI / 180);
|
|
@@ -1826,7 +2276,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1826
2276
|
}
|
|
1827
2277
|
|
|
1828
2278
|
// src/video-editor/export.ts
|
|
1829
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2279
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _48 => _48.name]), () => ( _optionalChain([asset, 'optionalAccess', _49 => _49.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
1830
2280
|
function toVideoEdl(project, options = {}) {
|
|
1831
2281
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
1832
2282
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -1858,10 +2308,10 @@ function toVideoEdl(project, options = {}) {
|
|
|
1858
2308
|
const outReel = before.type === "media" ? reelName(assets.get(before.asset)) : "BL";
|
|
1859
2309
|
const at = sourceAt(before, recIn);
|
|
1860
2310
|
lines.push(row(n2, outReel, "C", at, at, recIn, recIn));
|
|
1861
|
-
lines.push(row(n2, reel, `${t.type === "
|
|
2311
|
+
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1862
2312
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1863
2313
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
1864
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2314
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _50 => _50.name]), () => ( _optionalChain([asset, 'optionalAccess', _51 => _51.src]))), () => ( clip.asset))}`);
|
|
1865
2315
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1866
2316
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1867
2317
|
lines.push("");
|
|
@@ -1894,6 +2344,20 @@ function parseVideoLoudnessMeasurement(output) {
|
|
|
1894
2344
|
return m;
|
|
1895
2345
|
}
|
|
1896
2346
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
2347
|
+
var FFMPEG_BLEND = {
|
|
2348
|
+
multiply: "multiply",
|
|
2349
|
+
screen: "screen",
|
|
2350
|
+
overlay: "overlay",
|
|
2351
|
+
darken: "darken",
|
|
2352
|
+
lighten: "lighten",
|
|
2353
|
+
"color-dodge": "dodge",
|
|
2354
|
+
"color-burn": "burn",
|
|
2355
|
+
"hard-light": "hardlight",
|
|
2356
|
+
"soft-light": "softlight",
|
|
2357
|
+
difference: "difference",
|
|
2358
|
+
exclusion: "exclusion",
|
|
2359
|
+
add: "addition"
|
|
2360
|
+
};
|
|
1897
2361
|
var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
|
|
1898
2362
|
var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
|
|
1899
2363
|
var even = (v) => Math.max(2, 2 * Math.round(v / 2));
|
|
@@ -1939,6 +2403,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1939
2403
|
let cur = "base";
|
|
1940
2404
|
let usesDrawtext = false;
|
|
1941
2405
|
const animated = /* @__PURE__ */ new Set();
|
|
2406
|
+
const blended = /* @__PURE__ */ new Set();
|
|
1942
2407
|
let k = 0;
|
|
1943
2408
|
const step = 2e3 / fps;
|
|
1944
2409
|
const expr = (prop, clipTime, fallback, what) => {
|
|
@@ -1954,7 +2419,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1954
2419
|
return inputs.length - 1;
|
|
1955
2420
|
};
|
|
1956
2421
|
for (const track of [...project.tracks].reverse()) {
|
|
1957
|
-
for (const
|
|
2422
|
+
for (const raw of sortedVideoClips(track)) {
|
|
2423
|
+
const clip = videoClipAnimated(raw, project);
|
|
1958
2424
|
const { lead, tail } = videoClipOverhang(track, clip);
|
|
1959
2425
|
const t0 = clip.start - lead;
|
|
1960
2426
|
const span = clip.duration + lead + tail;
|
|
@@ -1973,11 +2439,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1973
2439
|
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
|
|
1974
2440
|
const chain2 = ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip)), volume];
|
|
1975
2441
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
2442
|
+
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
2443
|
+
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
2444
|
+
const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
|
|
2445
|
+
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2446
|
+
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2447
|
+
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2448
|
+
if (_optionalChain([next2, 'optionalAccess', _52 => _52.transition, 'access', _53 => _53.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
1981
2449
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
1982
2450
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
1983
2451
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
@@ -1990,7 +2458,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1990
2458
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
1991
2459
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
1992
2460
|
const tf = clip.transform;
|
|
1993
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2461
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _54 => _54[p]])));
|
|
1994
2462
|
if (clip.type === "text" && options.titleImage) {
|
|
1995
2463
|
inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
|
|
1996
2464
|
index = inputs.length - 1;
|
|
@@ -1999,7 +2467,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1999
2467
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2000
2468
|
const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
|
|
2001
2469
|
const coord = (axis) => {
|
|
2002
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
2470
|
+
const v = _optionalChain([tf, 'optionalAccess', _55 => _55[axis]]);
|
|
2003
2471
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2004
2472
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2005
2473
|
};
|
|
@@ -2007,7 +2475,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2007
2475
|
const opts = [
|
|
2008
2476
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2009
2477
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2010
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2478
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _56 => _56.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _57 => _57.scale]), () => ( 1))))}`,
|
|
2011
2479
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2012
2480
|
coord("x"),
|
|
2013
2481
|
coord("y"),
|
|
@@ -2015,7 +2483,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2015
2483
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2016
2484
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2017
2485
|
];
|
|
2018
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2486
|
+
if (_optionalChain([tf, 'optionalAccess', _58 => _58.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
2487
|
+
if (videoGradeFfmpegFilters(clip.grade).length) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
|
|
2488
|
+
if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
|
|
2019
2489
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
2020
2490
|
cur = next;
|
|
2021
2491
|
continue;
|
|
@@ -2031,10 +2501,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2031
2501
|
source = `[${index}:v]`;
|
|
2032
2502
|
chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2033
2503
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
chain.push(`scale=w='max(2,trunc(${even(rect.width)}*(${s})/2)*2)':h='max(2,trunc(${even(rect.height)}*(${s})/2)*2)':eval=frame`);
|
|
2037
|
-
}
|
|
2504
|
+
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2505
|
+
if (grade.length) chain.push("format=rgba", ...grade);
|
|
2038
2506
|
chain.push("format=yuva420p");
|
|
2039
2507
|
if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
|
|
2040
2508
|
else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
|
|
@@ -2048,18 +2516,50 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2048
2516
|
const test = dir === "right" ? `lt(X,W*${p})` : dir === "left" ? `gt(X,W*(1-${p}))` : dir === "down" ? `lt(Y,H*${p})` : `gt(Y,H*(1-${p}))`;
|
|
2049
2517
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
2050
2518
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2519
|
+
if (lead && clip.transition.type === "iris") {
|
|
2520
|
+
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2521
|
+
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
|
|
2522
|
+
}
|
|
2523
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _59 => _59.rotation]);
|
|
2524
|
+
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
2525
|
+
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2526
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _60 => _60.scale]))) {
|
|
2527
|
+
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2528
|
+
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2529
|
+
}
|
|
2054
2530
|
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
2055
2531
|
const cx = rect.x + rect.width / 2;
|
|
2056
2532
|
const cy = rect.y + rect.height / 2;
|
|
2533
|
+
const shifts = { x: [], y: [] };
|
|
2534
|
+
const shiftBy = (direction, from, d, entering) => {
|
|
2535
|
+
const dir = _nullishCoalesce(direction, () => ( "right"));
|
|
2536
|
+
const axis = dir === "left" || dir === "right" ? "x" : "y";
|
|
2537
|
+
const size2 = axis === "x" ? W : H;
|
|
2538
|
+
const sign = dir === "left" || dir === "up" ? -1 : 1;
|
|
2539
|
+
const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
|
|
2540
|
+
shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
|
|
2541
|
+
};
|
|
2542
|
+
const into = lead ? clip.transition.type : void 0;
|
|
2543
|
+
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2544
|
+
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2545
|
+
if (_optionalChain([after, 'optionalAccess', _61 => _61.transition, 'access', _62 => _62.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2057
2546
|
const at = (axis, c) => {
|
|
2058
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
2059
|
-
|
|
2547
|
+
const v = _optionalChain([placed, 'optionalAccess', _63 => _63[axis]]);
|
|
2548
|
+
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2549
|
+
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
|
|
2060
2550
|
};
|
|
2061
2551
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
2062
|
-
|
|
2552
|
+
if (clip.blend && clip.blend !== "normal") {
|
|
2553
|
+
graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[t${next}]`);
|
|
2554
|
+
graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split[f${next}][g${next}]`);
|
|
2555
|
+
graph.push(`[${cur}]format=gbrap,split[c${next}][d${next}]`);
|
|
2556
|
+
const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
|
|
2557
|
+
graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
|
|
2558
|
+
graph.push(`[g${next}]alphaextract[m${next}]`);
|
|
2559
|
+
graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
|
|
2560
|
+
graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
|
|
2561
|
+
blended.add(clip.blend);
|
|
2562
|
+
} else graph.push(`[${cur}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
|
|
2063
2563
|
cur = next;
|
|
2064
2564
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2065
2565
|
if (before && clip.transition.type === "dip") {
|
|
@@ -2078,10 +2578,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2078
2578
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2079
2579
|
else if (options.loudness && !options.loudness.measured)
|
|
2080
2580
|
notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
|
|
2081
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2581
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _64 => _64.get, 'call', _65 => _65(c.asset), 'optionalAccess', _66 => _66.kind]) === "video" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.hasAudio]) === void 0)))
|
|
2082
2582
|
notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
|
|
2083
2583
|
if (animated.size)
|
|
2084
2584
|
notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
|
|
2585
|
+
if (blended.has("soft-light")) notes.push("ffmpeg's soft-light blend uses a slightly different formula from the browser's, so the render can differ a little from the preview.");
|
|
2085
2586
|
if (usesDrawtext) notes.push("Titles use drawtext, which needs an ffmpeg built with libfreetype (Homebrew's is not: check `ffmpeg -filters | grep drawtext`); pass titleImage to render them as images instead.");
|
|
2086
2587
|
const filterComplex = graph.join(";");
|
|
2087
2588
|
if (measure) {
|
|
@@ -2112,11 +2613,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2112
2613
|
|
|
2113
2614
|
// src/video-editor/preview.ts
|
|
2114
2615
|
var VideoPreviewEngine = (_class3 = class {
|
|
2115
|
-
constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);
|
|
2616
|
+
constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);
|
|
2116
2617
|
this.source = source;
|
|
2117
2618
|
this.options = options;
|
|
2118
2619
|
this.canvas = canvas;
|
|
2119
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
2620
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _70 => _70.getContext, 'optionalCall', _71 => _71("2d")]), () => ( null));
|
|
2120
2621
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
2121
2622
|
this.update();
|
|
2122
2623
|
}
|
|
@@ -2129,6 +2630,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2129
2630
|
|
|
2130
2631
|
__init8() {this.scheduled = false}
|
|
2131
2632
|
__init9() {this.destroyed = false}
|
|
2633
|
+
__init10() {this.filterHost = null}
|
|
2634
|
+
__init11() {this.filters = /* @__PURE__ */ new Set()}
|
|
2132
2635
|
createMedia(kind) {
|
|
2133
2636
|
if (this.options.createMedia) return this.options.createMedia(kind);
|
|
2134
2637
|
const el = document.createElement(kind);
|
|
@@ -2164,8 +2667,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2164
2667
|
entry.media.pause();
|
|
2165
2668
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
2166
2669
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
2167
|
-
_optionalChain([entry, 'access',
|
|
2168
|
-
_optionalChain([entry, 'access',
|
|
2670
|
+
_optionalChain([entry, 'access', _72 => _72.media, 'access', _73 => _73.removeAttribute, 'optionalCall', _74 => _74("src")]);
|
|
2671
|
+
_optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.load, 'optionalCall', _77 => _77()]);
|
|
2169
2672
|
this.media.delete(entry.clip);
|
|
2170
2673
|
}
|
|
2171
2674
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -2198,7 +2701,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2198
2701
|
for (const layer of frame.layers) {
|
|
2199
2702
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
2200
2703
|
const sound = sounding.get(layer.clip);
|
|
2201
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
2704
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _78 => _78.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
2202
2705
|
sounding.delete(layer.clip);
|
|
2203
2706
|
}
|
|
2204
2707
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -2213,7 +2716,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2213
2716
|
if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
|
|
2214
2717
|
});
|
|
2215
2718
|
}
|
|
2216
|
-
|
|
2719
|
+
__init12() {this.sourceFor = (layer) => {
|
|
2217
2720
|
if (layer.assetKind === "image" && layer.src) {
|
|
2218
2721
|
const img = this.image(layer.src);
|
|
2219
2722
|
return img.complete ? img : null;
|
|
@@ -2230,12 +2733,31 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2230
2733
|
const h = Math.max(1, Math.round(frame.height * resolution));
|
|
2231
2734
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
2232
2735
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2233
|
-
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution);
|
|
2234
|
-
_optionalChain([this, 'access',
|
|
2736
|
+
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
|
|
2737
|
+
_optionalChain([this, 'access', _79 => _79.options, 'access', _80 => _80.onFrame, 'optionalCall', _81 => _81(frame)]);
|
|
2235
2738
|
}
|
|
2739
|
+
__init13() {this.defineSvgFilter = (id, markup) => {
|
|
2740
|
+
if (this.options.defineSvgFilter === null) return;
|
|
2741
|
+
if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
|
|
2742
|
+
if (this.filters.has(id) || typeof document === "undefined") return;
|
|
2743
|
+
if (!this.filterHost) {
|
|
2744
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2745
|
+
svg.setAttribute("width", "0");
|
|
2746
|
+
svg.setAttribute("height", "0");
|
|
2747
|
+
svg.setAttribute("aria-hidden", "true");
|
|
2748
|
+
svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden");
|
|
2749
|
+
document.body.appendChild(svg);
|
|
2750
|
+
this.filterHost = svg;
|
|
2751
|
+
}
|
|
2752
|
+
this.filterHost.insertAdjacentHTML("beforeend", markup);
|
|
2753
|
+
this.filters.add(id);
|
|
2754
|
+
}}
|
|
2236
2755
|
/** Stops following the editor and releases every media element. */
|
|
2237
2756
|
destroy() {
|
|
2238
2757
|
this.destroyed = true;
|
|
2758
|
+
_optionalChain([this, 'access', _82 => _82.filterHost, 'optionalAccess', _83 => _83.remove, 'call', _84 => _84()]);
|
|
2759
|
+
this.filterHost = null;
|
|
2760
|
+
this.filters.clear();
|
|
2239
2761
|
this.unsubscribe();
|
|
2240
2762
|
for (const entry of [...this.media.values()]) this.release(entry);
|
|
2241
2763
|
this.images.clear();
|
|
@@ -2250,16 +2772,16 @@ var VideoEditorController = (_class4 = class {
|
|
|
2250
2772
|
|
|
2251
2773
|
|
|
2252
2774
|
|
|
2253
|
-
|
|
2775
|
+
__init14() {this.selection = []}
|
|
2254
2776
|
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2777
|
+
__init15() {this.error = null}
|
|
2778
|
+
__init16() {this.revision = 0}
|
|
2779
|
+
__init17() {this.applying = false}
|
|
2258
2780
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2781
|
+
__init18() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2782
|
+
__init19() {this.unsubscribers = []}
|
|
2261
2783
|
|
|
2262
|
-
constructor(options = {}) {;_class4.prototype.
|
|
2784
|
+
constructor(options = {}) {;_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);_class4.prototype.__init21.call(this);
|
|
2263
2785
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
2264
2786
|
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
2265
2787
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
@@ -2284,8 +2806,8 @@ var VideoEditorController = (_class4 = class {
|
|
|
2284
2806
|
this.player.pause();
|
|
2285
2807
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
2286
2808
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2809
|
+
__init20() {this.getState = () => this.state}
|
|
2810
|
+
__init21() {this.subscribe = (listener) => {
|
|
2289
2811
|
this.connect();
|
|
2290
2812
|
this.listeners.add(listener);
|
|
2291
2813
|
return () => {
|
|
@@ -2341,7 +2863,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2341
2863
|
this.applying = false;
|
|
2342
2864
|
}
|
|
2343
2865
|
this.notify();
|
|
2344
|
-
_optionalChain([this, 'access',
|
|
2866
|
+
_optionalChain([this, 'access', _85 => _85.onChange, 'optionalCall', _86 => _86(project)]);
|
|
2345
2867
|
}
|
|
2346
2868
|
commit(label, next) {
|
|
2347
2869
|
const before = this.project;
|
|
@@ -2405,7 +2927,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2405
2927
|
}
|
|
2406
2928
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
2407
2929
|
select(clips, options = {}) {
|
|
2408
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
2930
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _87 => _87.filter, 'call', _88 => _88((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
2409
2931
|
let next;
|
|
2410
2932
|
if (options.add) {
|
|
2411
2933
|
next = [...this.selection];
|
|
@@ -2529,19 +3051,37 @@ var TRANSFORM = object(
|
|
|
2529
3051
|
);
|
|
2530
3052
|
var TRANSITION = object(
|
|
2531
3053
|
{
|
|
2532
|
-
type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it)
|
|
2533
|
-
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut.
|
|
3054
|
+
type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre)." },
|
|
3055
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
|
|
2534
3056
|
color: COLOR("For dip: the colour passed through (default black)"),
|
|
2535
|
-
direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe: the way the edge travels (default right)." }
|
|
3057
|
+
direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
|
|
3058
|
+
audioCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
|
|
2536
3059
|
},
|
|
2537
3060
|
["type", "duration"],
|
|
2538
3061
|
"A transition from the clip that ends where this one starts on the same track."
|
|
2539
3062
|
);
|
|
3063
|
+
var GRADE = object(
|
|
3064
|
+
Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
|
|
3065
|
+
[],
|
|
3066
|
+
"Colour correction of the picture, applied in order: exposure, contrast, saturation, hue, white balance (temperature, tint). Leave a setting out for no change; the object replaces the clip's whole grade."
|
|
3067
|
+
);
|
|
3068
|
+
var ANIMATION = (when) => object(
|
|
3069
|
+
{
|
|
3070
|
+
effect: { type: "string", enum: [...VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
|
|
3071
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default 500)." }
|
|
3072
|
+
},
|
|
3073
|
+
["effect"],
|
|
3074
|
+
when
|
|
3075
|
+
);
|
|
2540
3076
|
var CLIP_COMMON = {
|
|
2541
3077
|
name: { type: "string", description: "A label for the clip." },
|
|
2542
3078
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
|
|
2543
3079
|
transform: TRANSFORM,
|
|
2544
3080
|
fit: { type: "string", enum: [...VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
|
|
3081
|
+
grade: GRADE,
|
|
3082
|
+
animateIn: ANIMATION("How the picture enters, from the clip's start (on top of any fade in). Titles usually rise, pop or fade in."),
|
|
3083
|
+
animateOut: ANIMATION("How the picture leaves, ending at the clip's end."),
|
|
3084
|
+
blend: { type: "string", enum: [...VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
|
|
2545
3085
|
fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
|
|
2546
3086
|
fadeOut: MS("Fade down to nothing over this long")
|
|
2547
3087
|
};
|
|
@@ -2630,6 +3170,10 @@ var issueSummary = (issues) => ({
|
|
|
2630
3170
|
notes: issues.filter((i) => i.severity === "note").length,
|
|
2631
3171
|
lines: issues.map((i) => `${i.severity}: ${i.message}`)
|
|
2632
3172
|
});
|
|
3173
|
+
var videoAssetOut = (a) => {
|
|
3174
|
+
const { transcript, ...rest } = a;
|
|
3175
|
+
return transcript ? { ...rest, transcriptWords: transcript.length } : rest;
|
|
3176
|
+
};
|
|
2633
3177
|
function describeVideoProject(project) {
|
|
2634
3178
|
return {
|
|
2635
3179
|
...project.name ? { name: project.name } : {},
|
|
@@ -2638,7 +3182,7 @@ function describeVideoProject(project) {
|
|
|
2638
3182
|
fps: project.fps,
|
|
2639
3183
|
duration: videoProjectDuration(project),
|
|
2640
3184
|
...project.background ? { background: project.background } : {},
|
|
2641
|
-
assets: project.assets,
|
|
3185
|
+
assets: project.assets.map(videoAssetOut),
|
|
2642
3186
|
tracks: project.tracks.map((t, index) => ({
|
|
2643
3187
|
id: t.id,
|
|
2644
3188
|
kind: t.kind,
|
|
@@ -2677,14 +3221,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2677
3221
|
return null;
|
|
2678
3222
|
};
|
|
2679
3223
|
const placed = (o) => ({
|
|
2680
|
-
..._optionalChain([o, 'access',
|
|
2681
|
-
..._optionalChain([o, 'access',
|
|
2682
|
-
..._optionalChain([o, 'access',
|
|
3224
|
+
..._optionalChain([o, 'access', _89 => _89.created, 'optionalAccess', _90 => _90.length]) ? { created: o.created } : {},
|
|
3225
|
+
..._optionalChain([o, 'access', _91 => _91.trimmed, 'optionalAccess', _92 => _92.length]) ? { trimmed: o.trimmed } : {},
|
|
3226
|
+
..._optionalChain([o, 'access', _93 => _93.removed, 'optionalAccess', _94 => _94.length]) ? { removed: o.removed } : {},
|
|
2683
3227
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
2684
3228
|
});
|
|
2685
3229
|
const keysOut = (p, clip, property) => {
|
|
2686
3230
|
const c = clipOut(p, clip);
|
|
2687
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
3231
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _95 => _95.transform, 'optionalAccess', _96 => _96[property]]) : _optionalChain([c, 'optionalAccess', _97 => _97[property]]);
|
|
2688
3232
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
2689
3233
|
};
|
|
2690
3234
|
const getProject = {
|
|
@@ -2702,7 +3246,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2702
3246
|
const getFrame = {
|
|
2703
3247
|
name: "get_video_frame",
|
|
2704
3248
|
title: "What plays at a moment",
|
|
2705
|
-
description: "Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe) and the sounds (clip, source time, gain after volumes, fades and transitions).",
|
|
3249
|
+
description: "Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe or iris) and the sounds (clip, source time, gain after volumes, fades and transitions).",
|
|
2706
3250
|
inputSchema: object({ time: MS("Time from the start (default: the playhead)") }),
|
|
2707
3251
|
annotations: { title: "What plays at a moment", ...READ },
|
|
2708
3252
|
handler: ({ time }) => {
|
|
@@ -2724,7 +3268,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2724
3268
|
const checkProject = {
|
|
2725
3269
|
name: "check_video_project",
|
|
2726
3270
|
title: "Check the video project",
|
|
2727
|
-
description: "Lists the project's problems, errors first, each with a code, the clip / track / asset it is about and how to fix it: missing assets, overlaps, clips past the end of their source, transitions without a cut or handles, fades longer than the clip, keyframes outside their clip, assets with no duration, unused assets.",
|
|
3271
|
+
description: "Lists the project's problems, errors first, each with a code, the clip / track / asset it is about and how to fix it: missing assets, overlaps, clips past the end of their source, transitions without a cut or handles, fades or animations longer than the clip, animations a clip's own keyframes override, keyframes outside their clip, assets with no duration, unused assets.",
|
|
2728
3272
|
inputSchema: object({}),
|
|
2729
3273
|
annotations: { title: "Check the video project", ...READ },
|
|
2730
3274
|
handler: () => {
|
|
@@ -2860,9 +3404,9 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2860
3404
|
name: "update_video_asset",
|
|
2861
3405
|
title: "Change media details",
|
|
2862
3406
|
description: "Changes an asset's src, name, duration, size, sound or poster (e.g. relinking moved media). Clips that now run past its end are refused. Returns the asset.",
|
|
2863
|
-
inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster"] }, description: "Fields to clear." } }, ["asset"]),
|
|
3407
|
+
inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript"] }, description: "Fields to clear (transcript removes its words)." } }, ["asset"]),
|
|
2864
3408
|
annotations: { title: "Change media details", ...WRITE_IDEMPOTENT },
|
|
2865
|
-
handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: r.project.assets.find((a) => a.id === asset) }))
|
|
3409
|
+
handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: videoAssetOut(r.project.assets.find((a) => a.id === asset)) }))
|
|
2866
3410
|
};
|
|
2867
3411
|
const removeAsset = {
|
|
2868
3412
|
name: "remove_video_asset",
|
|
@@ -2956,12 +3500,12 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2956
3500
|
const updateClip = {
|
|
2957
3501
|
name: "update_video_clip",
|
|
2958
3502
|
title: "Change a clip",
|
|
2959
|
-
description: `Changes a clip's properties (one undo step): opacity, transform, fit, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
3503
|
+
description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
2960
3504
|
inputSchema: object(
|
|
2961
3505
|
{
|
|
2962
3506
|
clip: CLIP_ID,
|
|
2963
3507
|
set: object({ ...CLIP_COMMON, ...MEDIA_FIELDS, asset: { ...ASSET_ID, description: "For media: play another asset (same in point)." }, ...TEXT_FIELDS, color: COLOR("Text colour, or a colour clip's fill") }, [], "Fields to replace (only those that fit the clip's type)."),
|
|
2964
|
-
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
3508
|
+
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
2965
3509
|
},
|
|
2966
3510
|
["clip"]
|
|
2967
3511
|
),
|
|
@@ -2969,17 +3513,168 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2969
3513
|
handler: ({ clip, set, remove }) => {
|
|
2970
3514
|
const current = clipOut(project(), clip);
|
|
2971
3515
|
if (current && set) {
|
|
2972
|
-
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
3516
|
+
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
2973
3517
|
const wrong = Object.keys(set).filter((k) => !allowed.has(k));
|
|
2974
3518
|
if (wrong.length) throw new Error(`${wrong.map((k) => `"${k}"`).join(", ")} cannot be set on a ${current.type} clip; it takes ${[...allowed].join(", ")}.`);
|
|
2975
3519
|
}
|
|
2976
3520
|
return apply({ op: "update_clip", clip, set, remove }, "Change clip", (r) => ({ clip: clipOut(r.project, clip) }));
|
|
2977
3521
|
}
|
|
2978
3522
|
};
|
|
3523
|
+
const gradeTargets = (ids) => {
|
|
3524
|
+
const p = project();
|
|
3525
|
+
for (const id of ids) {
|
|
3526
|
+
const at = findVideoClip(p, id);
|
|
3527
|
+
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3528
|
+
const { clip } = at;
|
|
3529
|
+
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _98 => _98.assets, 'access', _99 => _99.find, 'call', _100 => _100((a) => a.id === clip.asset), 'optionalAccess', _101 => _101.kind]) === "audio")
|
|
3530
|
+
throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
const applyGradePreset = {
|
|
3534
|
+
name: "apply_video_grade_preset",
|
|
3535
|
+
title: "Apply a colour look",
|
|
3536
|
+
description: "Gives clips a named colour look at a strength (one undo step), replacing their grades: warm, cool, punchy (more contrast and colour), faded (lifted and muted), black-and-white, bleach-bypass (desaturated, contrasty), day-for-night (dark and blue). Returns each clip's resulting grade; tweak it with update_video_clip set.grade.",
|
|
3537
|
+
inputSchema: object(
|
|
3538
|
+
{
|
|
3539
|
+
clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to grade." },
|
|
3540
|
+
preset: { type: "string", enum: [...VIDEO_GRADE_PRESET_NAMES], description: "The look." },
|
|
3541
|
+
amount: { type: "number", minimum: 0, maximum: 1, description: "Strength, 0 (none) to 1 (full, the default); 0.5 is a subtler version." }
|
|
3542
|
+
},
|
|
3543
|
+
["clips", "preset"]
|
|
3544
|
+
),
|
|
3545
|
+
annotations: { title: "Apply a colour look", ...WRITE_IDEMPOTENT },
|
|
3546
|
+
handler: ({ clips, preset, amount }) => {
|
|
3547
|
+
gradeTargets(clips);
|
|
3548
|
+
const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
|
|
3549
|
+
const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3550
|
+
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _102 => _102(r.project, id), 'optionalAccess', _103 => _103.clip, 'access', _104 => _104.grade]), () => ( null)) })) }));
|
|
3551
|
+
}
|
|
3552
|
+
};
|
|
3553
|
+
const copyGrade = {
|
|
3554
|
+
name: "copy_video_grade",
|
|
3555
|
+
title: "Copy a colour grade",
|
|
3556
|
+
description: "Gives clips the same colour grade as another clip (one undo step), to match shots in a scene; a source with no grade clears theirs. Returns the grade copied and the clips changed.",
|
|
3557
|
+
inputSchema: object({ from: { ...CLIP_ID, description: "The clip whose grade to copy." }, to: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to give it to." } }, ["from", "to"]),
|
|
3558
|
+
annotations: { title: "Copy a colour grade", ...WRITE_IDEMPOTENT },
|
|
3559
|
+
handler: ({ from, to }) => {
|
|
3560
|
+
gradeTargets([from, ...to]);
|
|
3561
|
+
const grade = findVideoClip(project(), from).clip.grade;
|
|
3562
|
+
const edits = to.map((clip) => grade ? { op: "update_clip", clip, set: { grade: { ...grade } } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3563
|
+
return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
const TRANSCRIPT_TRACK = { ...TRACK_ID, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." };
|
|
3567
|
+
const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
|
|
3568
|
+
const timelineWords = (track) => {
|
|
3569
|
+
const words = videoTimelineTranscript(project(), { track });
|
|
3570
|
+
if (!words.length) throw new Error(track ? `No clip on track "${track}" plays a transcribed asset; attach one with set_video_transcript.` : "No clip plays a transcribed asset; attach a transcript to the media with set_video_transcript first.");
|
|
3571
|
+
return words;
|
|
3572
|
+
};
|
|
3573
|
+
const setTranscript = {
|
|
3574
|
+
name: "set_video_transcript",
|
|
3575
|
+
title: "Attach a transcript",
|
|
3576
|
+
description: 'Attaches what is said in a video or audio asset, word by word, so the edit can be cut by text (one undo step). Pass "text" (Whisper-style JSON with word timings, SubRip or WebVTT, as the file says it) or "words" ([{ word, start, end }] in ms of the source). Captions have no word timings, so their words are spread across each cue (approximate). Replaces any transcript the asset had. Returns the word count, whether timings are approximate, and the first and last words.',
|
|
3577
|
+
inputSchema: object(
|
|
3578
|
+
{
|
|
3579
|
+
asset: { ...ASSET_ID, description: "A video or audio asset." },
|
|
3580
|
+
text: { type: "string", minLength: 1, description: "The transcript file's contents: Whisper-style JSON (times in seconds), .srt or .vtt. Pass this or words." },
|
|
3581
|
+
words: { type: "array", minItems: 1, items: object({ word: { type: "string", minLength: 1, description: "The word as said." }, start: { type: "number", minimum: 0, description: "Start in ms of the source." }, end: { type: "number", minimum: 0, description: "End in ms of the source." } }, ["word", "start", "end"], "One word."), description: "Words with source times in ms. Pass this or text." }
|
|
3582
|
+
},
|
|
3583
|
+
["asset"]
|
|
3584
|
+
),
|
|
3585
|
+
annotations: { title: "Attach a transcript", ...WRITE_IDEMPOTENT },
|
|
3586
|
+
handler: ({ asset, text, words }) => {
|
|
3587
|
+
if (text === void 0 === (words === void 0)) throw new Error(`Pass either "text" (a transcript file's contents) or "words", not both.`);
|
|
3588
|
+
const a = project().assets.find((x) => x.id === asset);
|
|
3589
|
+
if (!a) throw new Error(`There is no asset "${asset}"; the assets are ${project().assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
|
|
3590
|
+
if (a.kind === "image") throw new Error(`Asset "${asset}" is an image and has no sound to transcribe.`);
|
|
3591
|
+
const parsed = text !== void 0 ? parseVideoTranscript(text) : { words, approximate: false };
|
|
3592
|
+
const list = parsed.words;
|
|
3593
|
+
return apply({ op: "update_asset", asset, set: { transcript: list } }, "Attach transcript", () => ({
|
|
3594
|
+
asset,
|
|
3595
|
+
words: list.length,
|
|
3596
|
+
approximate: parsed.approximate,
|
|
3597
|
+
first: list[0],
|
|
3598
|
+
last: list[list.length - 1],
|
|
3599
|
+
...a.duration !== void 0 && list[list.length - 1].end > a.duration + VIDEO_TIME_EPSILON ? { warning: `The transcript runs to ${list[list.length - 1].end} ms, past the asset's ${a.duration} ms: is it for this asset?` } : {}
|
|
3600
|
+
}));
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
const getTranscript = {
|
|
3604
|
+
name: "get_video_transcript",
|
|
3605
|
+
title: "Read the transcript",
|
|
3606
|
+
description: "Returns what the timeline says, in order, from the transcripts of the media its clips play on one track: the text, and each word with its timeline start and end (ms) and the clip that plays it, so ranges can go straight to remove_video_ranges or split_video_at. Words cut out of the edit are not included.",
|
|
3607
|
+
inputSchema: object({
|
|
3608
|
+
track: TRANSCRIPT_TRACK,
|
|
3609
|
+
from: MS("Only words from this timeline time (default 0)"),
|
|
3610
|
+
to: MS("Only words up to this timeline time (default the end)"),
|
|
3611
|
+
limit: { type: "integer", minimum: 1, maximum: 5e3, description: 'Most words to return (default 1000); "more" says if there are others.' }
|
|
3612
|
+
}),
|
|
3613
|
+
annotations: { title: "Read the transcript", ...READ },
|
|
3614
|
+
handler: ({ track, from, to, limit }) => {
|
|
3615
|
+
const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
|
|
3616
|
+
const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
|
|
3617
|
+
return { track: _nullishCoalesce(_optionalChain([words, 'access', _105 => _105[0], 'optionalAccess', _106 => _106.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
|
|
3618
|
+
}
|
|
3619
|
+
};
|
|
3620
|
+
const findText = {
|
|
3621
|
+
name: "find_video_transcript_text",
|
|
3622
|
+
title: "Find words in the transcript",
|
|
3623
|
+
description: "Lists every place a word or phrase is said on the timeline (case and punctuation ignored) as { from, to, text } ranges in timeline ms, ready for remove_video_ranges, split_video_at or seek_video.",
|
|
3624
|
+
inputSchema: object({ query: { type: "string", minLength: 1, description: "The word or phrase." }, track: TRANSCRIPT_TRACK }, ["query"]),
|
|
3625
|
+
annotations: { title: "Find words in the transcript", ...READ },
|
|
3626
|
+
handler: ({ query, track }) => ({ matches: findVideoTranscriptText(timelineWords(track), query).map(RANGE_OUT) })
|
|
3627
|
+
};
|
|
3628
|
+
const findFillers = {
|
|
3629
|
+
name: "find_video_fillers",
|
|
3630
|
+
title: "Find filler words",
|
|
3631
|
+
description: `Lists filler words and phrases on the timeline as { from, to, text } ranges in timeline ms, longest phrase first so "you know" is found whole. Default fillers: ${VIDEO_DEFAULT_FILLERS.join(", ")} ("like" and "so" are left out as too often meant). Check them, then pass the ones to cut to remove_video_ranges.`,
|
|
3632
|
+
inputSchema: object({ track: TRANSCRIPT_TRACK, fillers: { type: "array", minItems: 1, items: { type: "string", minLength: 1 }, description: "Words or phrases to treat as fillers instead of the defaults." } }),
|
|
3633
|
+
annotations: { title: "Find filler words", ...READ },
|
|
3634
|
+
handler: ({ track, fillers }) => {
|
|
3635
|
+
const found = findVideoFillers(timelineWords(track), fillers);
|
|
3636
|
+
return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
const findPauses = {
|
|
3640
|
+
name: "find_video_pauses",
|
|
3641
|
+
title: "Find pauses between words",
|
|
3642
|
+
description: 'Lists the gaps between consecutive words on the timeline (within one clip) longer than "min", less "keep" next to each word so speech is not clipped, snapped inward to frames, as { from, to } ranges in timeline ms ready for remove_video_ranges. Uses word timings; find_video_silences measures the sound instead.',
|
|
3643
|
+
inputSchema: object({
|
|
3644
|
+
track: TRANSCRIPT_TRACK,
|
|
3645
|
+
min: { type: "number", exclusiveMinimum: 0, description: "Shortest gap that counts, in ms (default 700)." },
|
|
3646
|
+
keep: { type: "number", minimum: 0, description: "Silence to keep after the word before and before the word after, in ms (default 150)." }
|
|
3647
|
+
}),
|
|
3648
|
+
annotations: { title: "Find pauses between words", ...READ },
|
|
3649
|
+
handler: ({ track, min, keep }) => {
|
|
3650
|
+
const found = findVideoTranscriptPauses(timelineWords(track), { min, keep, fps: project().fps });
|
|
3651
|
+
return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
const removeRanges = {
|
|
3655
|
+
name: "remove_video_ranges",
|
|
3656
|
+
title: "Cut out stretches",
|
|
3657
|
+
description: "Cuts stretches of the timeline out and closes them up, as one undo step: the ranges are merged where they touch and extracted from the last to the first, so the times from find_video_fillers, find_video_pauses, find_video_transcript_text or find_video_silences can be passed as they are. Like extract_video_range, every sync-locked track moves with the cut. Returns how many stretches and how much time were removed and the new length.",
|
|
3658
|
+
inputSchema: object(
|
|
3659
|
+
{
|
|
3660
|
+
ranges: { type: "array", minItems: 1, maxItems: 500, items: object({ from: MS("Start, on the timeline"), to: MS("End, on the timeline"), text: { type: "string", description: "Ignored: the words in the stretch, as the find tools return them." } }, ["from", "to"], "A stretch to remove."), description: "The stretches, in any order (timeline ms)." },
|
|
3661
|
+
tracks: { type: "array", items: TRACK_ID, minItems: 1, description: "Tracks to cut (default every unlocked track)." }
|
|
3662
|
+
},
|
|
3663
|
+
["ranges"]
|
|
3664
|
+
),
|
|
3665
|
+
annotations: { title: "Cut out stretches", ...DESTRUCTIVE },
|
|
3666
|
+
handler: ({ ranges, tracks }) => {
|
|
3667
|
+
const bad = ranges.findIndex((r) => !(r.to > r.from));
|
|
3668
|
+
if (bad >= 0) throw new Error(`Range ${bad + 1} is empty or backwards (${ranges[bad].from}\u2013${ranges[bad].to} ms); "to" must be after "from".`);
|
|
3669
|
+
const merged = mergeVideoRanges(ranges);
|
|
3670
|
+
const before = videoProjectDuration(project());
|
|
3671
|
+
return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - videoProjectDuration(r.project)), duration: videoProjectDuration(r.project) }));
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
2979
3674
|
const setTransition = {
|
|
2980
3675
|
name: "set_video_transition",
|
|
2981
3676
|
title: "Set a transition",
|
|
2982
|
-
description: "Sets (or with null removes) the transition into a clip from the clip that ends where it starts on the same track (one undo step).
|
|
3677
|
+
description: "Sets (or with null removes) the transition into a clip from the clip that ends where it starts on the same track (one undo step). Every type but dip plays half before and half after the cut, so both clips need that much media past the cut; when they do not, the error says the longest that fits. Returns the clip.",
|
|
2983
3678
|
inputSchema: object({ clip: { ...CLIP_ID, description: "The incoming clip (the one after the cut)." }, transition: { anyOf: [TRANSITION, { type: "null" }], description: "The transition, or null to remove it." } }, ["clip", "transition"]),
|
|
2984
3679
|
annotations: { title: "Set a transition", ...WRITE_IDEMPOTENT },
|
|
2985
3680
|
handler: ({ clip, transition }) => apply({ op: "set_transition", clip, transition }, transition ? "Set transition" : "Remove transition", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
@@ -3193,8 +3888,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3193
3888
|
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
|
|
3194
3889
|
}
|
|
3195
3890
|
};
|
|
3196
|
-
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
3197
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
3891
|
+
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
3892
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
3198
3893
|
return {
|
|
3199
3894
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
3200
3895
|
namespace: options.namespace,
|
|
@@ -3230,6 +3925,39 @@ function videoClipShowsWaveform(clip, asset, mode = "audio") {
|
|
|
3230
3925
|
if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
|
|
3231
3926
|
return mode === "all" || asset.kind === "audio";
|
|
3232
3927
|
}
|
|
3928
|
+
var VIDEO_CLIP_ANIMATION_LABELS = {
|
|
3929
|
+
fade: "Fade",
|
|
3930
|
+
rise: "Rise",
|
|
3931
|
+
drop: "Drop",
|
|
3932
|
+
"slide-left": "Slide left",
|
|
3933
|
+
"slide-right": "Slide right",
|
|
3934
|
+
zoom: "Zoom",
|
|
3935
|
+
pop: "Pop"
|
|
3936
|
+
};
|
|
3937
|
+
var VIDEO_TRANSITION_LABELS = {
|
|
3938
|
+
crossfade: "Crossfade",
|
|
3939
|
+
dip: "Dip to colour",
|
|
3940
|
+
wipe: "Wipe",
|
|
3941
|
+
push: "Push",
|
|
3942
|
+
slide: "Slide",
|
|
3943
|
+
iris: "Iris"
|
|
3944
|
+
};
|
|
3945
|
+
var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
|
|
3946
|
+
var VIDEO_BLEND_MODE_LABELS = {
|
|
3947
|
+
normal: "Normal",
|
|
3948
|
+
multiply: "Multiply",
|
|
3949
|
+
screen: "Screen",
|
|
3950
|
+
overlay: "Overlay",
|
|
3951
|
+
darken: "Darken",
|
|
3952
|
+
lighten: "Lighten",
|
|
3953
|
+
"color-dodge": "Colour dodge",
|
|
3954
|
+
"color-burn": "Colour burn",
|
|
3955
|
+
"hard-light": "Hard light",
|
|
3956
|
+
"soft-light": "Soft light",
|
|
3957
|
+
difference: "Difference",
|
|
3958
|
+
exclusion: "Exclusion",
|
|
3959
|
+
add: "Add"
|
|
3960
|
+
};
|
|
3233
3961
|
var VIDEO_FADE_CURVE_LABELS = {
|
|
3234
3962
|
linear: "Linear",
|
|
3235
3963
|
"equal-power": "Equal power",
|
|
@@ -3247,7 +3975,7 @@ function videoClipLabel(clip, asset) {
|
|
|
3247
3975
|
if (clip.name) return clip.name;
|
|
3248
3976
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
3249
3977
|
if (clip.type === "color") return clip.color;
|
|
3250
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
3978
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _107 => _107.name]), () => ( _optionalChain([asset, 'optionalAccess', _108 => _108.src, 'access', _109 => _109.replace, 'call', _110 => _110(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
3251
3979
|
const speed = videoClipSpeed(clip);
|
|
3252
3980
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
3253
3981
|
}
|
|
@@ -3291,7 +4019,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
|
|
|
3291
4019
|
const asset = videoAssetMap(project).get(assetId);
|
|
3292
4020
|
if (!asset) return null;
|
|
3293
4021
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
3294
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
4022
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _111 => _111.tracks, 'access', _112 => _112.find, 'call', _113 => _113((t) => t.kind === kind && !t.locked), 'optionalAccess', _114 => _114.id])));
|
|
3295
4023
|
if (!track) return null;
|
|
3296
4024
|
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
|
|
3297
4025
|
}
|
|
@@ -3340,14 +4068,14 @@ var KEY_LABELS = {
|
|
|
3340
4068
|
scale: { label: "Scale", step: 0.05 },
|
|
3341
4069
|
rotation: { label: "Rotation", step: 1 }
|
|
3342
4070
|
};
|
|
3343
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
4071
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _115 => _115.transform, 'optionalAccess', _116 => _116[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
3344
4072
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
3345
4073
|
function videoKeyframeRows(project, clipId, time) {
|
|
3346
4074
|
const at = findVideoClip(project, clipId);
|
|
3347
4075
|
if (!at) return [];
|
|
3348
4076
|
const { clip } = at;
|
|
3349
4077
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
3350
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
4078
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _117 => _117.kind]) === "audio" || at.track.kind === "audio");
|
|
3351
4079
|
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
3352
4080
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
3353
4081
|
const clipTime = playheadInClip(clip, time);
|
|
@@ -3518,5 +4246,48 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
3518
4246
|
|
|
3519
4247
|
|
|
3520
4248
|
|
|
3521
|
-
|
|
4249
|
+
|
|
4250
|
+
|
|
4251
|
+
|
|
4252
|
+
|
|
4253
|
+
|
|
4254
|
+
|
|
4255
|
+
|
|
4256
|
+
|
|
4257
|
+
|
|
4258
|
+
|
|
4259
|
+
|
|
4260
|
+
|
|
4261
|
+
|
|
4262
|
+
|
|
4263
|
+
|
|
4264
|
+
|
|
4265
|
+
|
|
4266
|
+
|
|
4267
|
+
|
|
4268
|
+
|
|
4269
|
+
|
|
4270
|
+
|
|
4271
|
+
|
|
4272
|
+
|
|
4273
|
+
|
|
4274
|
+
|
|
4275
|
+
|
|
4276
|
+
|
|
4277
|
+
|
|
4278
|
+
|
|
4279
|
+
|
|
4280
|
+
|
|
4281
|
+
|
|
4282
|
+
|
|
4283
|
+
|
|
4284
|
+
|
|
4285
|
+
|
|
4286
|
+
|
|
4287
|
+
|
|
4288
|
+
|
|
4289
|
+
|
|
4290
|
+
|
|
4291
|
+
|
|
4292
|
+
exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
|
|
3522
4293
|
//# sourceMappingURL=index.cjs.map
|