@pixerate/editor 0.84.0 → 0.86.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 +700 -74
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +188 -2
- package/dist/video-editor/index.d.ts +188 -2
- package/dist/video-editor/index.js +648 -22
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +32 -4
|
@@ -340,6 +340,105 @@ function videoGradeFfmpegFilters(grade) {
|
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
// src/video-editor/remap.ts
|
|
344
|
+
function videoClipSourceTime(clip, clipTime) {
|
|
345
|
+
const base = _nullishCoalesce(clip.in, () => ( 0));
|
|
346
|
+
return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (_nullishCoalesce(clip.speed, () => ( 1)));
|
|
347
|
+
}
|
|
348
|
+
function videoClipSpeedAt(clip, clipTime) {
|
|
349
|
+
if (clip.type !== "media") return 1;
|
|
350
|
+
if (!clip.timeRemap) return _nullishCoalesce(clip.speed, () => ( 1));
|
|
351
|
+
const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
|
|
352
|
+
return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
|
|
353
|
+
}
|
|
354
|
+
function videoSpeedRampKeys(segments) {
|
|
355
|
+
if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
|
|
356
|
+
const keys = [];
|
|
357
|
+
let t = 0;
|
|
358
|
+
let v = 0;
|
|
359
|
+
segments.forEach((s, i) => {
|
|
360
|
+
if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
|
|
361
|
+
for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
|
|
362
|
+
if (x !== void 0 && !(x >= 0 && x <= 20)) throw new Error(`Stretch ${i + 1}'s ${name} must be from 0 (a freeze) to 20; got ${x}.`);
|
|
363
|
+
const a = s.speed;
|
|
364
|
+
const b = _nullishCoalesce(s.toSpeed, () => ( s.speed));
|
|
365
|
+
const avg = (a + b) / 2;
|
|
366
|
+
let ease = "linear";
|
|
367
|
+
if (a !== b && avg > 0) {
|
|
368
|
+
ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
|
|
369
|
+
}
|
|
370
|
+
keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
|
|
371
|
+
t += s.duration;
|
|
372
|
+
v += avg * s.duration;
|
|
373
|
+
});
|
|
374
|
+
keys.push({ t, v });
|
|
375
|
+
return { keys: mergeLinear(keys), duration: t, source: v };
|
|
376
|
+
}
|
|
377
|
+
function mergeLinear(keys) {
|
|
378
|
+
const out = [keys[0]];
|
|
379
|
+
for (let i = 1; i < keys.length; i++) {
|
|
380
|
+
const prev = out[out.length - 1];
|
|
381
|
+
const k = keys[i];
|
|
382
|
+
const before = out.length > 1 ? out[out.length - 2] : null;
|
|
383
|
+
if (before && !before.ease && !prev.ease) {
|
|
384
|
+
const s1 = (prev.v - before.v) / (prev.t - before.t);
|
|
385
|
+
const s2 = (k.v - prev.v) / (k.t - prev.t);
|
|
386
|
+
if (Math.abs(s1 - s2) < 1e-9) {
|
|
387
|
+
out[out.length - 1] = k;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
out.push(k);
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
function checkVideoTimeRemap(remap, path = "timeRemap") {
|
|
396
|
+
const { keys } = remap;
|
|
397
|
+
if (!keys.length) throw new Error(`${path} needs at least one key.`);
|
|
398
|
+
keys.forEach((k, i) => {
|
|
399
|
+
if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
|
|
400
|
+
const next = keys[i + 1];
|
|
401
|
+
if (!next) return;
|
|
402
|
+
if (next.v < k.v - 1e-6) throw new Error(`${path}.keys[${i + 1}] goes back in the source (${next.v} ms after ${k.v} ms); a ramp only plays forwards, so make each value at least the one before.`);
|
|
403
|
+
if (!videoEaseBezier(k.ease)) return;
|
|
404
|
+
let prev = k.v;
|
|
405
|
+
for (let n2 = 1; n2 <= 32; n2++) {
|
|
406
|
+
const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
|
|
407
|
+
if (v < prev - 1e-3) throw new Error(`${path}.keys[${i}].ease overshoots, which would play backwards; use a curve that only moves forwards (or linear).`);
|
|
408
|
+
prev = v;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function videoTimeRemapSpan(remap) {
|
|
413
|
+
return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
|
|
414
|
+
}
|
|
415
|
+
function videoTimeRemapBreakpoints(remap, step) {
|
|
416
|
+
const { points, holds } = videoKeyframeBreakpoints(remap, step);
|
|
417
|
+
const out = [];
|
|
418
|
+
points.forEach(([t, v], i) => {
|
|
419
|
+
out.push({ t, v });
|
|
420
|
+
if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
|
|
421
|
+
});
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
function videoSpeedRampEdit(clip, segments, ripple = true) {
|
|
425
|
+
const ramp = videoSpeedRampKeys(segments);
|
|
426
|
+
const offset = videoClipSourceTime(clip, 0) - (_nullishCoalesce(clip.in, () => ( 0)));
|
|
427
|
+
return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
|
|
428
|
+
}
|
|
429
|
+
function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
|
|
430
|
+
if (at < 0 || at > clip.duration) throw new Error(`${Math.round(at)} ms is outside clip "${clip.id}" (0\u2013${clip.duration} ms of clip time).`);
|
|
431
|
+
const remap = _nullishCoalesce(clip.timeRemap, () => ( { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (_nullishCoalesce(clip.speed, () => ( 1))) }] }));
|
|
432
|
+
const [left, right] = splitVideoAnimatable(remap, at);
|
|
433
|
+
const keys = [...left.keys.slice(0, -1), { ...left.keys[left.keys.length - 1], ease: "linear" }, ...right.keys.map((k) => ({ ...k, t: k.t + at + duration }))];
|
|
434
|
+
return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
|
|
435
|
+
}
|
|
436
|
+
var videoSlowMotionRamp = (duration) => [
|
|
437
|
+
{ duration: Math.max(1, duration / 2), speed: 1 },
|
|
438
|
+
{ duration: 500, speed: 1, toSpeed: 0.3 },
|
|
439
|
+
{ duration: 1500, speed: 0.3 }
|
|
440
|
+
];
|
|
441
|
+
|
|
343
442
|
// src/video-editor/parse.ts
|
|
344
443
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
345
444
|
var fail = (path, what) => {
|
|
@@ -408,6 +507,18 @@ function parseVideoTransform(v, path) {
|
|
|
408
507
|
rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
|
|
409
508
|
});
|
|
410
509
|
}
|
|
510
|
+
function parseVideoTimeRemap(v, speed, path) {
|
|
511
|
+
if (v === void 0) return void 0;
|
|
512
|
+
const remap = parseVideoAnimatable(v, path, { optional: true, min: 0 });
|
|
513
|
+
if (typeof remap === "number" || remap === void 0) return fail(path, "must be keyframes { keys: [{ t, v, ease }] } (v: ms of source after the in point)");
|
|
514
|
+
if (speed !== void 0 && speed !== 1) fail(path, 'cannot be used with a speed; a ramp sets the speed itself (remove "speed")');
|
|
515
|
+
try {
|
|
516
|
+
checkVideoTimeRemap(remap, path);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
throw new Error(e.message.replace(/\.?$/, "."));
|
|
519
|
+
}
|
|
520
|
+
return remap;
|
|
521
|
+
}
|
|
411
522
|
function parseVideoClipAnimation(v, path) {
|
|
412
523
|
if (v === void 0) return void 0;
|
|
413
524
|
if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
|
|
@@ -435,6 +546,18 @@ function parseVideoTransition(v, path) {
|
|
|
435
546
|
audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`)
|
|
436
547
|
});
|
|
437
548
|
}
|
|
549
|
+
function parseVideoTranscriptWords(v, path) {
|
|
550
|
+
if (v === void 0) return void 0;
|
|
551
|
+
if (!Array.isArray(v)) return fail(path, "must be an array of { word, start, end } (ms in the source)");
|
|
552
|
+
const words = v.map((w, i) => {
|
|
553
|
+
if (!isObject(w)) return fail(`${path}[${i}]`, "must be an object { word, start, end }");
|
|
554
|
+
const word = str(w.word, `${path}[${i}].word`);
|
|
555
|
+
const start = num(w.start, `${path}[${i}].start`, { min: 0 });
|
|
556
|
+
const end = num(w.end, `${path}[${i}].end`, { min: start });
|
|
557
|
+
return { word, start, end };
|
|
558
|
+
});
|
|
559
|
+
return words.sort((a, b) => a.start - b.start);
|
|
560
|
+
}
|
|
438
561
|
function parseVideoAsset(v, path = "asset") {
|
|
439
562
|
if (!isObject(v)) return fail(path, "must be an object");
|
|
440
563
|
const kind = oneOf(v.kind, ["video", "audio", "image"], `${path}.kind`, false);
|
|
@@ -447,8 +570,10 @@ function parseVideoAsset(v, path = "asset") {
|
|
|
447
570
|
width: num(v.width, `${path}.width`, { optional: true, positive: true }),
|
|
448
571
|
height: num(v.height, `${path}.height`, { optional: true, positive: true }),
|
|
449
572
|
hasAudio: bool(v.hasAudio, `${path}.hasAudio`),
|
|
450
|
-
poster: str(v.poster, `${path}.poster`, true)
|
|
573
|
+
poster: str(v.poster, `${path}.poster`, true),
|
|
574
|
+
transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`)
|
|
451
575
|
});
|
|
576
|
+
if (kind === "image" && asset.transcript) fail(`${path}.transcript`, "is only for video and audio assets");
|
|
452
577
|
if (kind === "image" && asset.duration !== void 0) fail(`${path}.duration`, "is only for video and audio assets");
|
|
453
578
|
return asset;
|
|
454
579
|
}
|
|
@@ -480,6 +605,7 @@ function parseVideoClip(v, path = "clip") {
|
|
|
480
605
|
speed: num(v.speed, `${path}.speed`, { optional: true, min: 0.05, max: 20 }),
|
|
481
606
|
volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
482
607
|
muted: bool(v.muted, `${path}.muted`),
|
|
608
|
+
timeRemap: parseVideoTimeRemap(v.timeRemap, v.speed, `${path}.timeRemap`),
|
|
483
609
|
fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
|
|
484
610
|
fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
|
|
485
611
|
});
|
|
@@ -575,6 +701,7 @@ var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.spe
|
|
|
575
701
|
function videoClipSourceRange(clip) {
|
|
576
702
|
if (clip.type !== "media") return null;
|
|
577
703
|
const start = _nullishCoalesce(clip.in, () => ( 0));
|
|
704
|
+
if (_optionalChain([clip, 'access', _2 => _2.timeRemap, 'optionalAccess', _3 => _3.keys, 'access', _4 => _4.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
|
|
578
705
|
return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
|
|
579
706
|
}
|
|
580
707
|
var videoFrameDuration = (fps) => 1e3 / fps;
|
|
@@ -655,13 +782,13 @@ function videoClipHandles(clip, assets) {
|
|
|
655
782
|
const speed = videoClipSpeed(clip);
|
|
656
783
|
const range = videoClipSourceRange(clip);
|
|
657
784
|
const before = range.in / speed;
|
|
658
|
-
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess',
|
|
785
|
+
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _5 => _5.kind]) === "image" ? Infinity : before, after: Infinity };
|
|
659
786
|
return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
|
|
660
787
|
}
|
|
661
788
|
function videoClipOverhang(track, clip) {
|
|
662
789
|
const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
|
|
663
790
|
const next = videoClipAfter(track, clip);
|
|
664
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
791
|
+
const tail = _optionalChain([next, 'optionalAccess', _6 => _6.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
|
|
665
792
|
return { lead, tail };
|
|
666
793
|
}
|
|
667
794
|
function videoSnapPoints(project, options = {}) {
|
|
@@ -745,7 +872,7 @@ function videoClipAnimated(clip, project) {
|
|
|
745
872
|
const spans = videoClipAnimationSpans(clip);
|
|
746
873
|
const out = { ...clip, transform: { ...clip.transform } };
|
|
747
874
|
for (const prop of PROPS) {
|
|
748
|
-
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access',
|
|
875
|
+
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _7 => _7.transform, 'optionalAccess', _8 => _8[prop]]);
|
|
749
876
|
if (isVideoKeyframes(current)) continue;
|
|
750
877
|
const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
|
|
751
878
|
const away = (spec) => {
|
|
@@ -782,7 +909,7 @@ function videoClipKeyframes(clip) {
|
|
|
782
909
|
const out = [
|
|
783
910
|
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
784
911
|
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
785
|
-
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access',
|
|
912
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _9 => _9.transform, 'optionalAccess', _10 => _10[p]])) }))
|
|
786
913
|
];
|
|
787
914
|
return out.filter((o) => o.keys.length);
|
|
788
915
|
}
|
|
@@ -839,7 +966,7 @@ function checkVideoProject(project) {
|
|
|
839
966
|
for (const which of ["animateIn", "animateOut"]) {
|
|
840
967
|
const a = clip[which];
|
|
841
968
|
if (!a) continue;
|
|
842
|
-
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access',
|
|
969
|
+
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _11 => _11.transform, 'optionalAccess', _12 => _12[p]])).length);
|
|
843
970
|
if (keyed.length)
|
|
844
971
|
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.` });
|
|
845
972
|
}
|
|
@@ -914,6 +1041,7 @@ var VIDEO_EDIT_OPS = [
|
|
|
914
1041
|
"extract_range",
|
|
915
1042
|
"split_at",
|
|
916
1043
|
"set_keyframes",
|
|
1044
|
+
"set_time_remap",
|
|
917
1045
|
"close_gaps",
|
|
918
1046
|
"add_marker",
|
|
919
1047
|
"update_marker",
|
|
@@ -941,13 +1069,16 @@ function mergeSpans(spans) {
|
|
|
941
1069
|
}
|
|
942
1070
|
return out;
|
|
943
1071
|
}
|
|
1072
|
+
var isRamped = (clip) => clip.type === "media" && !!clip.timeRemap;
|
|
944
1073
|
function clipAnimatables(clip) {
|
|
945
1074
|
const c = clip;
|
|
946
1075
|
const out = [
|
|
947
1076
|
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
948
|
-
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
1077
|
+
{ get: () => c.volume, set: (v) => c.volume = v },
|
|
1078
|
+
// A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
|
|
1079
|
+
{ get: () => c.timeRemap, set: (v) => c.timeRemap = v }
|
|
949
1080
|
];
|
|
950
|
-
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access',
|
|
1081
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _13 => _13.transform, 'optionalAccess', _14 => _14[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
951
1082
|
return out;
|
|
952
1083
|
}
|
|
953
1084
|
function retimeClipKeys(clip, fn) {
|
|
@@ -956,7 +1087,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
956
1087
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
957
1088
|
}
|
|
958
1089
|
}
|
|
959
|
-
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"];
|
|
1090
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
960
1091
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
961
1092
|
var Editor = (_class = class {
|
|
962
1093
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
@@ -995,7 +1126,7 @@ var Editor = (_class = class {
|
|
|
995
1126
|
kindFits(track, clip) {
|
|
996
1127
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
997
1128
|
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.`);
|
|
998
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
1129
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _15 => _15.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
999
1130
|
}
|
|
1000
1131
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
1001
1132
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
@@ -1037,13 +1168,13 @@ var Editor = (_class = class {
|
|
|
1037
1168
|
}
|
|
1038
1169
|
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
1039
1170
|
moveIn(clip, delta) {
|
|
1040
|
-
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call',
|
|
1171
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _16 => _16(this.p), 'access', _17 => _17.get, 'call', _18 => _18(clip.asset), 'optionalAccess', _19 => _19.kind]) === "image") return;
|
|
1041
1172
|
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
1042
1173
|
if (clip.in < 1e-9) delete clip.in;
|
|
1043
1174
|
}
|
|
1044
1175
|
/** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
|
|
1045
1176
|
trimHead(clip, delta) {
|
|
1046
|
-
this.moveIn(clip, delta);
|
|
1177
|
+
if (!isRamped(clip)) this.moveIn(clip, delta);
|
|
1047
1178
|
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
1048
1179
|
}
|
|
1049
1180
|
/** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
|
|
@@ -1060,7 +1191,7 @@ var Editor = (_class = class {
|
|
|
1060
1191
|
if (isVideoKeyframes(l)) leftValues[i].set(l);
|
|
1061
1192
|
if (isVideoKeyframes(rr)) r.set(rr);
|
|
1062
1193
|
});
|
|
1063
|
-
this.moveIn(right, at - clip.start);
|
|
1194
|
+
if (!isRamped(right)) this.moveIn(right, at - clip.start);
|
|
1064
1195
|
clip.duration = at - clip.start;
|
|
1065
1196
|
delete clip.fadeOut;
|
|
1066
1197
|
delete clip.animateOut;
|
|
@@ -1272,7 +1403,7 @@ var Editor = (_class = class {
|
|
|
1272
1403
|
if (e.edge === "start") {
|
|
1273
1404
|
const delta = e.to - clip.start;
|
|
1274
1405
|
if (clip.duration - delta < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${formatVideoSeconds(end)}.`);
|
|
1275
|
-
if (clip.type === "media") {
|
|
1406
|
+
if (clip.type === "media" && !isRamped(clip)) {
|
|
1276
1407
|
const sourceIn = (_nullishCoalesce(clip.in, () => ( 0))) + delta * speed;
|
|
1277
1408
|
if (sourceIn < -VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${formatVideoSeconds(clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
|
|
1278
1409
|
clip.in = Math.max(0, sourceIn);
|
|
@@ -1300,10 +1431,10 @@ var Editor = (_class = class {
|
|
|
1300
1431
|
} else {
|
|
1301
1432
|
const duration = e.to - clip.start;
|
|
1302
1433
|
if (duration < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the end to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it starts at ${formatVideoSeconds(clip.start)}.`);
|
|
1303
|
-
if (clip.type === "media") {
|
|
1434
|
+
if (clip.type === "media" && !isRamped(clip)) {
|
|
1304
1435
|
const asset = assets.get(clip.asset);
|
|
1305
1436
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
1306
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1437
|
+
if (_optionalChain([asset, 'optionalAccess', _20 => _20.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
1307
1438
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
1308
1439
|
}
|
|
1309
1440
|
if (e.ripple) {
|
|
@@ -1356,10 +1487,12 @@ var Editor = (_class = class {
|
|
|
1356
1487
|
for (const k of ["id", "type"]) if (k in set) refuse(`A clip's ${k} cannot be changed; delete it and add a new one.`);
|
|
1357
1488
|
for (const k of _nullishCoalesce(e.remove, () => ( []))) if (!OPTIONAL_CLIP_FIELDS.includes(k)) refuse(`"${k}" cannot be removed; removable fields are ${OPTIONAL_CLIP_FIELDS.join(", ")}.`);
|
|
1358
1489
|
if ("speed" in set && (typeof set.speed !== "number" || !(set.speed > 0))) refuse('"speed" must be a number greater than 0 (1 = normal speed).');
|
|
1490
|
+
if ("speed" in set && isRamped(at.clip)) refuse(`Clip "${at.clip.id}" has a speed ramp, which sets its speed; change the ramp, or remove it first.`);
|
|
1491
|
+
if ("timeRemap" in set) refuse("Set a speed ramp with set_time_remap, which checks it against the media and moves later clips if asked.");
|
|
1359
1492
|
let stretch = 1;
|
|
1360
1493
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
1361
1494
|
const was = videoClipSpeed(at.clip);
|
|
1362
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1495
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _21 => _21.remove, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("speed")]) ? 1 : was;
|
|
1363
1496
|
if (now !== was) {
|
|
1364
1497
|
set.duration = at.clip.duration * was / now;
|
|
1365
1498
|
stretch = was / now;
|
|
@@ -1375,6 +1508,56 @@ var Editor = (_class = class {
|
|
|
1375
1508
|
at.track.clips.sort((a, b) => a.start - b.start);
|
|
1376
1509
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: at.track.id });
|
|
1377
1510
|
}
|
|
1511
|
+
case "set_time_remap": {
|
|
1512
|
+
const { track, clip } = this.clip(e.clip);
|
|
1513
|
+
this.unlocked(track);
|
|
1514
|
+
if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
|
|
1515
|
+
const asset = this.asset(clip.asset);
|
|
1516
|
+
if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
|
|
1517
|
+
const media = clip;
|
|
1518
|
+
const end = videoClipEnd(media);
|
|
1519
|
+
const duration = _nullishCoalesce(e.duration, () => ( media.duration));
|
|
1520
|
+
if (!(duration >= 1e3 / this.p.fps - VIDEO_TIME_EPSILON)) refuse(`"duration" must be at least a frame (${Math.ceil(1e3 / this.p.fps)} ms).`);
|
|
1521
|
+
if (e.keys === null) {
|
|
1522
|
+
if (!media.timeRemap) refuse(`Clip "${clip.id}" has no speed ramp to remove.`);
|
|
1523
|
+
const from = (_nullishCoalesce(media.in, () => ( 0))) + media.timeRemap.keys[0].v;
|
|
1524
|
+
if (asset.duration !== void 0 && from + duration > asset.duration + VIDEO_TIME_EPSILON)
|
|
1525
|
+
refuse(`At normal speed from ${Math.round(from)} ms, "${clip.id}" would run past the end of "${asset.id}" (${asset.duration} ms); give a duration of at most ${Math.floor(asset.duration - from)} ms.`);
|
|
1526
|
+
delete media.timeRemap;
|
|
1527
|
+
media.in = from;
|
|
1528
|
+
if (media.in < 1e-9) delete media.in;
|
|
1529
|
+
} else {
|
|
1530
|
+
if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must be a list of at least one { t, v, ease } (or null to remove the ramp).');
|
|
1531
|
+
const remap = parseVideoTimeRemap({ keys: e.keys }, void 0, "keys");
|
|
1532
|
+
remap.keys = sortVideoKeys(remap.keys);
|
|
1533
|
+
checkVideoTimeRemap(remap, "keys");
|
|
1534
|
+
const span = videoTimeRemapSpan(remap);
|
|
1535
|
+
if (asset.duration !== void 0 && (_nullishCoalesce(media.in, () => ( 0))) + span.to > asset.duration + VIDEO_TIME_EPSILON)
|
|
1536
|
+
refuse(`The ramp plays ${Math.round(span.to)} ms of source from the in point (${_nullishCoalesce(media.in, () => ( 0))} ms), past the end of "${asset.id}" (${asset.duration} ms); it can use at most ${Math.floor(asset.duration - (_nullishCoalesce(media.in, () => ( 0))))} ms.`);
|
|
1537
|
+
media.timeRemap = remap;
|
|
1538
|
+
delete media.speed;
|
|
1539
|
+
}
|
|
1540
|
+
const newEnd = media.start + duration;
|
|
1541
|
+
const outcome = { op: e.op, clip: clip.id, track: track.id };
|
|
1542
|
+
if (Math.abs(newEnd - end) > VIDEO_TIME_EPSILON) {
|
|
1543
|
+
if (e.ripple) {
|
|
1544
|
+
const tracks = this.rippleTracks([track]);
|
|
1545
|
+
if (newEnd < end) {
|
|
1546
|
+
media.duration = duration;
|
|
1547
|
+
this.close(tracks, newEnd, end);
|
|
1548
|
+
} else {
|
|
1549
|
+
const created = this.open(tracks, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]));
|
|
1550
|
+
media.duration = duration;
|
|
1551
|
+
if (created.length) outcome.created = created;
|
|
1552
|
+
}
|
|
1553
|
+
outcome.rippled = tracks.map((t) => t.id);
|
|
1554
|
+
} else {
|
|
1555
|
+
if (newEnd > end) this.fits(track, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]), `Lengthening "${clip.id}"`);
|
|
1556
|
+
media.duration = duration;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return this.outcomes.push(outcome);
|
|
1560
|
+
}
|
|
1378
1561
|
case "set_transition": {
|
|
1379
1562
|
const { track, clip } = this.clip(e.clip);
|
|
1380
1563
|
this.unlocked(track);
|
|
@@ -1455,7 +1638,7 @@ var Editor = (_class = class {
|
|
|
1455
1638
|
this.trimHead(after, delta);
|
|
1456
1639
|
}
|
|
1457
1640
|
track.clips.sort((a, b) => a.start - b.start);
|
|
1458
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess',
|
|
1641
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _24 => _24.id]), clip.id, _optionalChain([after, 'optionalAccess', _25 => _25.id])].filter((x) => !!x), track: track.id, delta });
|
|
1459
1642
|
}
|
|
1460
1643
|
case "lift_range":
|
|
1461
1644
|
case "extract_range": {
|
|
@@ -1486,7 +1669,7 @@ var Editor = (_class = class {
|
|
|
1486
1669
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1487
1670
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1488
1671
|
const c = clip;
|
|
1489
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1672
|
+
const current = transform ? _optionalChain([c, 'access', _26 => _26.transform, 'optionalAccess', _27 => _27[e.property]]) : c[e.property];
|
|
1490
1673
|
let value;
|
|
1491
1674
|
if (e.keys === null) {
|
|
1492
1675
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1568,7 +1751,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1568
1751
|
editor.run(edit);
|
|
1569
1752
|
} catch (error) {
|
|
1570
1753
|
const message = error.message;
|
|
1571
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1754
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _28 => _28.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1572
1755
|
}
|
|
1573
1756
|
}
|
|
1574
1757
|
let next;
|
|
@@ -1587,7 +1770,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1587
1770
|
|
|
1588
1771
|
// src/video-editor/evaluate.ts
|
|
1589
1772
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1590
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1773
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _30 => _30.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _31 => _31.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.rotation]), clipTime, 0) };
|
|
1591
1774
|
}
|
|
1592
1775
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1593
1776
|
let w = size.width;
|
|
@@ -1634,8 +1817,8 @@ function videoFadeCurveGain(curve, x) {
|
|
|
1634
1817
|
function fadeGain(clip, time, sound = false) {
|
|
1635
1818
|
let g = 1;
|
|
1636
1819
|
const shape = sound && clip.type === "media" ? clip : void 0;
|
|
1637
|
-
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1638
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1820
|
+
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _33 => _33.fadeInCurve]), (time - clip.start) / clip.fadeIn);
|
|
1821
|
+
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _34 => _34.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
1639
1822
|
return g;
|
|
1640
1823
|
}
|
|
1641
1824
|
function videoClipSoundGain(clip, clipTime) {
|
|
@@ -1648,7 +1831,7 @@ function activeOnTrack(track, time) {
|
|
|
1648
1831
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1649
1832
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1650
1833
|
const next = videoClipAfter(track, clip);
|
|
1651
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1834
|
+
const tail = _optionalChain([next, 'optionalAccess', _35 => _35.transition]) ? next.transition.duration / 2 : 0;
|
|
1652
1835
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1653
1836
|
const a = { clip };
|
|
1654
1837
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1657,7 +1840,7 @@ function activeOnTrack(track, time) {
|
|
|
1657
1840
|
}
|
|
1658
1841
|
return out;
|
|
1659
1842
|
}
|
|
1660
|
-
var sourceTimeAt = (clip, time) => clip.type === "media" ? (
|
|
1843
|
+
var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
|
|
1661
1844
|
function videoFrameAt(project, time) {
|
|
1662
1845
|
const assets = videoAssetMap(project);
|
|
1663
1846
|
const layers = [];
|
|
@@ -1665,8 +1848,8 @@ function videoFrameAt(project, time) {
|
|
|
1665
1848
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1666
1849
|
const track = project.tracks[ti];
|
|
1667
1850
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1668
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1669
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1851
|
+
if (_optionalChain([incoming, 'optionalAccess', _36 => _36.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1852
|
+
if (_optionalChain([outgoing, 'optionalAccess', _37 => _37.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1670
1853
|
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
1671
1854
|
const fade = fadeGain(clip, inside);
|
|
1672
1855
|
const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
|
|
@@ -1677,7 +1860,7 @@ function videoFrameAt(project, time) {
|
|
|
1677
1860
|
const audible = videoAssetHasSound(asset);
|
|
1678
1861
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
1679
1862
|
const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
|
|
1680
|
-
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed:
|
|
1863
|
+
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
|
|
1681
1864
|
}
|
|
1682
1865
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1683
1866
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
@@ -1697,8 +1880,8 @@ function videoFrameAt(project, time) {
|
|
|
1697
1880
|
shift = videoTransitionEase(incoming.progress) - 1;
|
|
1698
1881
|
} else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1699
1882
|
}
|
|
1700
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1701
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1883
|
+
if (_optionalChain([outgoing, 'optionalAccess', _38 => _38.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1884
|
+
if (_optionalChain([outgoing, 'optionalAccess', _39 => _39.type]) === "push") {
|
|
1702
1885
|
shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
|
|
1703
1886
|
shift = videoTransitionEase(outgoing.progress);
|
|
1704
1887
|
}
|
|
@@ -1708,7 +1891,7 @@ function videoFrameAt(project, time) {
|
|
|
1708
1891
|
const across = shiftDirection === "left" || shiftDirection === "right";
|
|
1709
1892
|
const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
|
|
1710
1893
|
const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
|
|
1711
|
-
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access',
|
|
1894
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _40 => _40.transform, 'optionalAccess', _41 => _41.rotation]), clipTime, 0) };
|
|
1712
1895
|
if (wipe) layer.wipe = wipe;
|
|
1713
1896
|
if (iris) layer.iris = iris;
|
|
1714
1897
|
if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
|
|
@@ -1941,6 +2124,171 @@ var VideoAudioPeaksCache = (_class2 = class {
|
|
|
1941
2124
|
}
|
|
1942
2125
|
}, _class2);
|
|
1943
2126
|
|
|
2127
|
+
// src/video-editor/transcript.ts
|
|
2128
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2129
|
+
function cueTime(s) {
|
|
2130
|
+
const m = s.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
|
|
2131
|
+
if (!m) throw new Error(`"${s.trim()}" is not a cue time (HH:MM:SS,mmm).`);
|
|
2132
|
+
return ((Number(_nullishCoalesce(m[1], () => ( 0))) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1e3 + Number(m[4].padEnd(3, "0"));
|
|
2133
|
+
}
|
|
2134
|
+
function cueWords(text, start, end) {
|
|
2135
|
+
const words = text.replace(/<[^>]+>/g, "").split(/\s+/).filter(Boolean);
|
|
2136
|
+
const step = (end - start) / Math.max(1, words.length);
|
|
2137
|
+
return words.map((word, i) => ({ word, start: Math.round(start + i * step), end: Math.round(start + (i + 1) * step) }));
|
|
2138
|
+
}
|
|
2139
|
+
function parseVideoTranscript(input) {
|
|
2140
|
+
let data = input;
|
|
2141
|
+
if (typeof input === "string") {
|
|
2142
|
+
const text = input.replace(/^/, "").trim();
|
|
2143
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
2144
|
+
try {
|
|
2145
|
+
data = JSON.parse(text);
|
|
2146
|
+
} catch (e3) {
|
|
2147
|
+
throw new Error("The transcript looks like JSON but is not valid JSON.");
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
const words2 = [];
|
|
2151
|
+
for (const block of text.replace(/\r\n?/g, "\n").split(/\n{2,}/)) {
|
|
2152
|
+
const lines = block.split("\n");
|
|
2153
|
+
const at = lines.findIndex((l) => l.includes("-->"));
|
|
2154
|
+
if (at < 0) continue;
|
|
2155
|
+
const [a, b] = lines[at].split("-->");
|
|
2156
|
+
words2.push(...cueWords(lines.slice(at + 1).join(" "), cueTime(a), cueTime(b.trim().split(/\s+/)[0])));
|
|
2157
|
+
}
|
|
2158
|
+
if (!words2.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
|
|
2159
|
+
return { words: words2, approximate: true };
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
const seconds = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e3) : NaN;
|
|
2163
|
+
const fromList = (list, toMs) => list.map((w, i) => {
|
|
2164
|
+
if (!isRecord(w)) throw new Error(`Word ${i + 1} is not an object { word, start, end }.`);
|
|
2165
|
+
const word = String(_nullishCoalesce(_nullishCoalesce(w.word, () => ( w.text)), () => ( ""))).trim();
|
|
2166
|
+
const start = toMs(w.start);
|
|
2167
|
+
const end = toMs(w.end);
|
|
2168
|
+
if (!word || !Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`Word ${i + 1} needs a word and numeric start and end.`);
|
|
2169
|
+
return { word, start, end: Math.max(start, end) };
|
|
2170
|
+
});
|
|
2171
|
+
let words = [];
|
|
2172
|
+
if (Array.isArray(data)) words = fromList(data, (v) => typeof v === "number" ? v : NaN);
|
|
2173
|
+
else if (isRecord(data) && Array.isArray(data.words)) words = fromList(data.words, seconds);
|
|
2174
|
+
else if (isRecord(data) && Array.isArray(data.segments)) {
|
|
2175
|
+
const segments = data.segments.filter(isRecord);
|
|
2176
|
+
if (segments.some((s) => Array.isArray(s.words))) words = fromList(segments.flatMap((s) => Array.isArray(s.words) ? s.words : []), seconds);
|
|
2177
|
+
else {
|
|
2178
|
+
for (const s of segments) words.push(...cueWords(String(_nullishCoalesce(s.text, () => ( ""))), seconds(s.start), seconds(s.end)));
|
|
2179
|
+
if (!words.length) throw new Error("No words found in the segments.");
|
|
2180
|
+
return { words, approximate: true };
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (!words.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
|
|
2184
|
+
return { words: words.sort((a, b) => a.start - b.start), approximate: false };
|
|
2185
|
+
}
|
|
2186
|
+
function videoTranscriptTrack(project, track) {
|
|
2187
|
+
if (track !== void 0) {
|
|
2188
|
+
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(", ")}.`);
|
|
2189
|
+
return track;
|
|
2190
|
+
}
|
|
2191
|
+
const assets = videoAssetMap(project);
|
|
2192
|
+
const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _42 => _42.get, 'call', _43 => _43(c.asset), 'optionalAccess', _44 => _44.transcript, 'optionalAccess', _45 => _45.length]));
|
|
2193
|
+
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _46 => _46.id]), () => ( null));
|
|
2194
|
+
}
|
|
2195
|
+
function videoTimelineTranscript(project, options = {}) {
|
|
2196
|
+
const id = videoTranscriptTrack(project, options.track);
|
|
2197
|
+
if (!id) return [];
|
|
2198
|
+
const track = project.tracks.find((t) => t.id === id);
|
|
2199
|
+
const assets = videoAssetMap(project);
|
|
2200
|
+
const out = [];
|
|
2201
|
+
for (const clip of sortedVideoClips(track)) {
|
|
2202
|
+
if (clip.type !== "media") continue;
|
|
2203
|
+
const words = _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(clip.asset), 'optionalAccess', _49 => _49.transcript]);
|
|
2204
|
+
if (!_optionalChain([words, 'optionalAccess', _50 => _50.length])) continue;
|
|
2205
|
+
const speed = videoClipSpeed(clip);
|
|
2206
|
+
const from = _nullishCoalesce(clip.in, () => ( 0));
|
|
2207
|
+
const to = from + clip.duration * speed;
|
|
2208
|
+
const toTimeline = (s) => Math.min(videoClipEnd(clip), Math.max(clip.start, clip.start + (s - from) / speed));
|
|
2209
|
+
words.forEach((w, index) => {
|
|
2210
|
+
const mid = (w.start + w.end) / 2;
|
|
2211
|
+
if (mid < from || mid >= to) return;
|
|
2212
|
+
out.push({ word: w.word, start: toTimeline(w.start), end: toTimeline(w.end), clip: clip.id, track: track.id, index });
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
return out;
|
|
2216
|
+
}
|
|
2217
|
+
var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
|
|
2218
|
+
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(" ") });
|
|
2219
|
+
function findVideoTranscriptText(words, query) {
|
|
2220
|
+
const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
|
|
2221
|
+
if (!q.length) throw new Error("Search for at least one word.");
|
|
2222
|
+
const norm = words.map((w) => normalizeVideoWord(w.word));
|
|
2223
|
+
const out = [];
|
|
2224
|
+
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));
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
var VIDEO_DEFAULT_FILLERS = ["um", "umm", "uh", "uhh", "uhm", "erm", "er", "ah", "hmm", "mm", "you know", "i mean"];
|
|
2228
|
+
function findVideoFillers(words, fillers = VIDEO_DEFAULT_FILLERS) {
|
|
2229
|
+
const phrases = fillers.map((f) => f.split(/\s+/).map(normalizeVideoWord).filter(Boolean)).filter((p) => p.length).sort((a, b) => b.length - a.length);
|
|
2230
|
+
const norm = words.map((w) => normalizeVideoWord(w.word));
|
|
2231
|
+
const out = [];
|
|
2232
|
+
for (let i = 0; i < norm.length; ) {
|
|
2233
|
+
const hit = phrases.find((p) => i + p.length <= norm.length && p.every((x, k) => norm[i + k] === x));
|
|
2234
|
+
if (hit) {
|
|
2235
|
+
out.push(rangeOf(words, i, hit.length));
|
|
2236
|
+
i += hit.length;
|
|
2237
|
+
} else i++;
|
|
2238
|
+
}
|
|
2239
|
+
return out;
|
|
2240
|
+
}
|
|
2241
|
+
function findVideoTranscriptPauses(words, options = {}) {
|
|
2242
|
+
const min = _nullishCoalesce(options.min, () => ( 700));
|
|
2243
|
+
const keep = _nullishCoalesce(options.keep, () => ( 150));
|
|
2244
|
+
const out = [];
|
|
2245
|
+
for (let i = 1; i < words.length; i++) {
|
|
2246
|
+
const a = words[i - 1];
|
|
2247
|
+
const b = words[i];
|
|
2248
|
+
if (a.clip !== b.clip || b.start - a.end < min) continue;
|
|
2249
|
+
let from = a.end + keep;
|
|
2250
|
+
let to = b.start - keep;
|
|
2251
|
+
if (options.fps) {
|
|
2252
|
+
const f = 1e3 / options.fps;
|
|
2253
|
+
from = snapToVideoFrame(Math.ceil(from / f - 1e-9) * f, options.fps);
|
|
2254
|
+
to = snapToVideoFrame(Math.floor(to / f + 1e-9) * f, options.fps);
|
|
2255
|
+
}
|
|
2256
|
+
if (to - from > VIDEO_TIME_EPSILON) out.push({ from, to, text: `(pause after "${a.word}")` });
|
|
2257
|
+
}
|
|
2258
|
+
return out;
|
|
2259
|
+
}
|
|
2260
|
+
function mergeVideoRanges(ranges) {
|
|
2261
|
+
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);
|
|
2262
|
+
const out = [];
|
|
2263
|
+
for (const r of sorted) {
|
|
2264
|
+
const last = out[out.length - 1];
|
|
2265
|
+
if (last && r.from <= last.to + VIDEO_TIME_EPSILON) last.to = Math.max(last.to, r.to);
|
|
2266
|
+
else out.push(r);
|
|
2267
|
+
}
|
|
2268
|
+
return out;
|
|
2269
|
+
}
|
|
2270
|
+
function videoRemoveRangesEdits(ranges, tracks) {
|
|
2271
|
+
return mergeVideoRanges(ranges).reverse().map((r) => ({ op: "extract_range", from: r.from, to: r.to, ...tracks ? { tracks } : {} }));
|
|
2272
|
+
}
|
|
2273
|
+
function videoWordsRange(words, a, b) {
|
|
2274
|
+
const [i, j] = a <= b ? [a, b] : [b, a];
|
|
2275
|
+
return { from: words[i].start, to: words[j].end };
|
|
2276
|
+
}
|
|
2277
|
+
function videoTranscriptEdit(assetId, text) {
|
|
2278
|
+
return { op: "update_asset", asset: assetId, set: { transcript: parseVideoTranscript(text).words } };
|
|
2279
|
+
}
|
|
2280
|
+
function videoWordAt(words, time) {
|
|
2281
|
+
let lo = 0;
|
|
2282
|
+
let hi = words.length - 1;
|
|
2283
|
+
while (lo <= hi) {
|
|
2284
|
+
const mid = lo + hi >> 1;
|
|
2285
|
+
if (words[mid].end <= time) lo = mid + 1;
|
|
2286
|
+
else if (words[mid].start > time) hi = mid - 1;
|
|
2287
|
+
else return mid;
|
|
2288
|
+
}
|
|
2289
|
+
return -1;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
1944
2292
|
// src/video-editor/render.ts
|
|
1945
2293
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1946
2294
|
var n = (v) => String(Math.round(v * 100) / 100);
|
|
@@ -2097,7 +2445,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
|
2097
2445
|
}
|
|
2098
2446
|
|
|
2099
2447
|
// src/video-editor/export.ts
|
|
2100
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2448
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _51 => _51.name]), () => ( _optionalChain([asset, 'optionalAccess', _52 => _52.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
2101
2449
|
function toVideoEdl(project, options = {}) {
|
|
2102
2450
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
2103
2451
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -2132,7 +2480,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
2132
2480
|
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2133
2481
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2134
2482
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
2135
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2483
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _53 => _53.name]), () => ( _optionalChain([asset, 'optionalAccess', _54 => _54.src]))), () => ( clip.asset))}`);
|
|
2136
2484
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
2137
2485
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
2138
2486
|
lines.push("");
|
|
@@ -2165,6 +2513,7 @@ function parseVideoLoudnessMeasurement(output) {
|
|
|
2165
2513
|
return m;
|
|
2166
2514
|
}
|
|
2167
2515
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
2516
|
+
var fmtMs = (v) => String(Math.round(v * 100) / 100);
|
|
2168
2517
|
var FFMPEG_BLEND = {
|
|
2169
2518
|
multiply: "multiply",
|
|
2170
2519
|
screen: "screen",
|
|
@@ -2224,6 +2573,58 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2224
2573
|
let cur = "base";
|
|
2225
2574
|
let usesDrawtext = false;
|
|
2226
2575
|
const animated = /* @__PURE__ */ new Set();
|
|
2576
|
+
const ramped = /* @__PURE__ */ new Set();
|
|
2577
|
+
const rampedPicture = (remap, lead, end) => {
|
|
2578
|
+
const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
|
|
2579
|
+
const first = pts[0];
|
|
2580
|
+
const last = pts[pts.length - 1];
|
|
2581
|
+
const s = `(T*1000+${fmtMs(first.v)})`;
|
|
2582
|
+
let e = fmtMs(last.t - first.t);
|
|
2583
|
+
for (let i = pts.length - 2; i >= 0; i--) {
|
|
2584
|
+
const a = pts[i];
|
|
2585
|
+
const b = pts[i + 1];
|
|
2586
|
+
if (b.v - a.v < 1e-6) continue;
|
|
2587
|
+
e = `if(lt(${s},${fmtMs(b.v)}),${fmtMs(a.t - first.t)}+(${s}-${fmtMs(a.v)})*${fmtMs((b.t - a.t) / (b.v - a.v))},${e})`;
|
|
2588
|
+
}
|
|
2589
|
+
const before = lead + first.t;
|
|
2590
|
+
const after = Math.max(0, end - last.t);
|
|
2591
|
+
return ["setpts=PTS-STARTPTS", `setpts='(${e})/1000/TB'`, `fps=${fps}`, ...before > 0.5 || after > 0.5 ? [`tpad=start_mode=clone:start_duration=${sec(before)}:stop_mode=clone:stop_duration=${sec(after + 1e3 / fps)}`] : [`tpad=stop_mode=clone:stop_duration=${sec(1e3 / fps)}`], `trim=duration=${sec(lead + end)}`];
|
|
2592
|
+
};
|
|
2593
|
+
const rampedSound = (remap, input2, output, lead, end) => {
|
|
2594
|
+
const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
|
|
2595
|
+
const first = pts[0];
|
|
2596
|
+
const last = pts[pts.length - 1];
|
|
2597
|
+
const n2 = audio.length;
|
|
2598
|
+
const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
|
|
2599
|
+
const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
|
|
2600
|
+
const pieces = [];
|
|
2601
|
+
const out = [];
|
|
2602
|
+
const parts = [];
|
|
2603
|
+
if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
|
|
2604
|
+
const played = [];
|
|
2605
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
2606
|
+
const dt = pts[i + 1].t - pts[i].t;
|
|
2607
|
+
if (dt < 0.5) continue;
|
|
2608
|
+
const speed = (pts[i + 1].v - pts[i].v) / dt;
|
|
2609
|
+
if (speed < 0.1) parts.push(silence(dt));
|
|
2610
|
+
else {
|
|
2611
|
+
played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
|
|
2612
|
+
parts.push(`#${played.length - 1}`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (end - last.t > 0.5) parts.push(silence(end - last.t));
|
|
2616
|
+
if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
|
|
2617
|
+
parts.forEach((p, k2) => {
|
|
2618
|
+
const label = `[r${n2}p${k2}]`;
|
|
2619
|
+
if (p.startsWith("#")) {
|
|
2620
|
+
const piece = played[Number(p.slice(1))];
|
|
2621
|
+
out.push(`[r${n2}s${p.slice(1)}]atrim=start=${sec(piece.from)}:end=${sec(piece.to)},asetpts=PTS-STARTPTS,${atempo(piece.speed).join(",") || "anull"},${fmtA}${label}`);
|
|
2622
|
+
} else out.push(`${p}${label}`);
|
|
2623
|
+
pieces.push(label);
|
|
2624
|
+
});
|
|
2625
|
+
out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
|
|
2626
|
+
return out;
|
|
2627
|
+
};
|
|
2227
2628
|
const blended = /* @__PURE__ */ new Set();
|
|
2228
2629
|
let k = 0;
|
|
2229
2630
|
const step = 2e3 / fps;
|
|
@@ -2236,7 +2637,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2236
2637
|
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
2237
2638
|
const span = clip.duration + lead + tail;
|
|
2238
2639
|
if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
|
|
2239
|
-
else
|
|
2640
|
+
else if (clip.timeRemap) {
|
|
2641
|
+
const { from, to } = videoTimeRemapSpan(clip.timeRemap);
|
|
2642
|
+
inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
|
|
2643
|
+
} else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
|
|
2240
2644
|
return inputs.length - 1;
|
|
2241
2645
|
};
|
|
2242
2646
|
for (const track of [...project.tracks].reverse()) {
|
|
@@ -2258,7 +2662,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2258
2662
|
const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
|
|
2259
2663
|
const trackGain = _nullishCoalesce(track.volume, () => ( 1));
|
|
2260
2664
|
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}`;
|
|
2261
|
-
|
|
2665
|
+
let source2 = `[${ai}:a]`;
|
|
2666
|
+
if (clip.timeRemap) {
|
|
2667
|
+
source2 = `[r${audio.length}]`;
|
|
2668
|
+
graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
|
|
2669
|
+
ramped.add("sound");
|
|
2670
|
+
}
|
|
2671
|
+
const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
|
|
2262
2672
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2263
2673
|
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
2264
2674
|
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
@@ -2266,11 +2676,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2266
2676
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2267
2677
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2268
2678
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2269
|
-
if (_optionalChain([next2, 'optionalAccess',
|
|
2679
|
+
if (_optionalChain([next2, 'optionalAccess', _55 => _55.transition, 'access', _56 => _56.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
2270
2680
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
2271
2681
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
2272
2682
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
2273
|
-
graph.push(
|
|
2683
|
+
graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
|
|
2274
2684
|
audio.push(`[a${audio.length}]`);
|
|
2275
2685
|
}
|
|
2276
2686
|
if (measure || track.kind !== "video" || track.hidden) continue;
|
|
@@ -2279,7 +2689,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2279
2689
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
2280
2690
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
2281
2691
|
const tf = clip.transform;
|
|
2282
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2692
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _57 => _57[p]])));
|
|
2283
2693
|
if (clip.type === "text" && options.titleImage) {
|
|
2284
2694
|
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 })]);
|
|
2285
2695
|
index = inputs.length - 1;
|
|
@@ -2288,7 +2698,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2288
2698
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2289
2699
|
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("*");
|
|
2290
2700
|
const coord = (axis) => {
|
|
2291
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
2701
|
+
const v = _optionalChain([tf, 'optionalAccess', _58 => _58[axis]]);
|
|
2292
2702
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2293
2703
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2294
2704
|
};
|
|
@@ -2296,7 +2706,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2296
2706
|
const opts = [
|
|
2297
2707
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2298
2708
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2299
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2709
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _59 => _59.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _60 => _60.scale]), () => ( 1))))}`,
|
|
2300
2710
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2301
2711
|
coord("x"),
|
|
2302
2712
|
coord("y"),
|
|
@@ -2304,7 +2714,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2304
2714
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2305
2715
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2306
2716
|
];
|
|
2307
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2717
|
+
if (_optionalChain([tf, 'optionalAccess', _61 => _61.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
2308
2718
|
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.`);
|
|
2309
2719
|
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.`);
|
|
2310
2720
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
@@ -2320,7 +2730,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2320
2730
|
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
2321
2731
|
else {
|
|
2322
2732
|
source = `[${index}:v]`;
|
|
2323
|
-
|
|
2733
|
+
if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2734
|
+
else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2324
2735
|
}
|
|
2325
2736
|
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2326
2737
|
if (grade.length) chain.push("format=rgba", ...grade);
|
|
@@ -2341,10 +2752,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2341
2752
|
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2342
2753
|
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)'`);
|
|
2343
2754
|
}
|
|
2344
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
2755
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _62 => _62.rotation]);
|
|
2345
2756
|
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
2346
2757
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2347
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
2758
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _63 => _63.scale]))) {
|
|
2348
2759
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2349
2760
|
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2350
2761
|
}
|
|
@@ -2363,9 +2774,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2363
2774
|
const into = lead ? clip.transition.type : void 0;
|
|
2364
2775
|
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2365
2776
|
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2366
|
-
if (_optionalChain([after, 'optionalAccess',
|
|
2777
|
+
if (_optionalChain([after, 'optionalAccess', _64 => _64.transition, 'access', _65 => _65.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2367
2778
|
const at = (axis, c) => {
|
|
2368
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
2779
|
+
const v = _optionalChain([placed, 'optionalAccess', _66 => _66[axis]]);
|
|
2369
2780
|
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2370
2781
|
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}'`;
|
|
2371
2782
|
};
|
|
@@ -2399,11 +2810,12 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2399
2810
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2400
2811
|
else if (options.loudness && !options.loudness.measured)
|
|
2401
2812
|
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.");
|
|
2402
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2813
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.kind]) === "video" && _optionalChain([assets, 'access', _70 => _70.get, 'call', _71 => _71(c.asset), 'optionalAccess', _72 => _72.hasAudio]) === void 0)))
|
|
2403
2814
|
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.');
|
|
2404
2815
|
if (animated.size)
|
|
2405
2816
|
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)" : ""}.`);
|
|
2406
2817
|
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.");
|
|
2818
|
+
if (ramped.has("sound")) notes.push("Sound through a speed ramp plays each stretch at its own speed, so a gliding change is heard in small steps (about every 80 ms), and freezes are silent.");
|
|
2407
2819
|
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.");
|
|
2408
2820
|
const filterComplex = graph.join(";");
|
|
2409
2821
|
if (measure) {
|
|
@@ -2438,7 +2850,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2438
2850
|
this.source = source;
|
|
2439
2851
|
this.options = options;
|
|
2440
2852
|
this.canvas = canvas;
|
|
2441
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
2853
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _73 => _73.getContext, 'optionalCall', _74 => _74("2d")]), () => ( null));
|
|
2442
2854
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
2443
2855
|
this.update();
|
|
2444
2856
|
}
|
|
@@ -2488,8 +2900,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2488
2900
|
entry.media.pause();
|
|
2489
2901
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
2490
2902
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
2491
|
-
_optionalChain([entry, 'access',
|
|
2492
|
-
_optionalChain([entry, 'access',
|
|
2903
|
+
_optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.removeAttribute, 'optionalCall', _77 => _77("src")]);
|
|
2904
|
+
_optionalChain([entry, 'access', _78 => _78.media, 'access', _79 => _79.load, 'optionalCall', _80 => _80()]);
|
|
2493
2905
|
this.media.delete(entry.clip);
|
|
2494
2906
|
}
|
|
2495
2907
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -2503,13 +2915,14 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2503
2915
|
const { media } = this.entry(clip, src, kind);
|
|
2504
2916
|
active.add(clip);
|
|
2505
2917
|
const target = sourceTime / 1e3;
|
|
2506
|
-
|
|
2918
|
+
const frozen = speed < 0.0625;
|
|
2919
|
+
if (!frozen && Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
|
|
2507
2920
|
if (gain === null) media.muted = true;
|
|
2508
2921
|
else {
|
|
2509
2922
|
media.muted = gain <= 0;
|
|
2510
2923
|
media.volume = Math.min(1, Math.max(0, gain));
|
|
2511
2924
|
}
|
|
2512
|
-
if (playing) {
|
|
2925
|
+
if (playing && !frozen) {
|
|
2513
2926
|
if (media.paused || Math.abs(media.currentTime - target) > tolerance) media.currentTime = target;
|
|
2514
2927
|
if (media.paused) void Promise.resolve(media.play()).catch(() => {
|
|
2515
2928
|
});
|
|
@@ -2522,7 +2935,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2522
2935
|
for (const layer of frame.layers) {
|
|
2523
2936
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
2524
2937
|
const sound = sounding.get(layer.clip);
|
|
2525
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
2938
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _81 => _81.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
2526
2939
|
sounding.delete(layer.clip);
|
|
2527
2940
|
}
|
|
2528
2941
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -2555,7 +2968,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2555
2968
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
2556
2969
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2557
2970
|
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
|
|
2558
|
-
_optionalChain([this, 'access',
|
|
2971
|
+
_optionalChain([this, 'access', _82 => _82.options, 'access', _83 => _83.onFrame, 'optionalCall', _84 => _84(frame)]);
|
|
2559
2972
|
}
|
|
2560
2973
|
__init13() {this.defineSvgFilter = (id, markup) => {
|
|
2561
2974
|
if (this.options.defineSvgFilter === null) return;
|
|
@@ -2576,7 +2989,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2576
2989
|
/** Stops following the editor and releases every media element. */
|
|
2577
2990
|
destroy() {
|
|
2578
2991
|
this.destroyed = true;
|
|
2579
|
-
_optionalChain([this, 'access',
|
|
2992
|
+
_optionalChain([this, 'access', _85 => _85.filterHost, 'optionalAccess', _86 => _86.remove, 'call', _87 => _87()]);
|
|
2580
2993
|
this.filterHost = null;
|
|
2581
2994
|
this.filters.clear();
|
|
2582
2995
|
this.unsubscribe();
|
|
@@ -2684,7 +3097,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2684
3097
|
this.applying = false;
|
|
2685
3098
|
}
|
|
2686
3099
|
this.notify();
|
|
2687
|
-
_optionalChain([this, 'access',
|
|
3100
|
+
_optionalChain([this, 'access', _88 => _88.onChange, 'optionalCall', _89 => _89(project)]);
|
|
2688
3101
|
}
|
|
2689
3102
|
commit(label, next) {
|
|
2690
3103
|
const before = this.project;
|
|
@@ -2748,7 +3161,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2748
3161
|
}
|
|
2749
3162
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
2750
3163
|
select(clips, options = {}) {
|
|
2751
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
3164
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _90 => _90.filter, 'call', _91 => _91((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
2752
3165
|
let next;
|
|
2753
3166
|
if (options.add) {
|
|
2754
3167
|
next = [...this.selection];
|
|
@@ -2991,6 +3404,10 @@ var issueSummary = (issues) => ({
|
|
|
2991
3404
|
notes: issues.filter((i) => i.severity === "note").length,
|
|
2992
3405
|
lines: issues.map((i) => `${i.severity}: ${i.message}`)
|
|
2993
3406
|
});
|
|
3407
|
+
var videoAssetOut = (a) => {
|
|
3408
|
+
const { transcript, ...rest } = a;
|
|
3409
|
+
return transcript ? { ...rest, transcriptWords: transcript.length } : rest;
|
|
3410
|
+
};
|
|
2994
3411
|
function describeVideoProject(project) {
|
|
2995
3412
|
return {
|
|
2996
3413
|
...project.name ? { name: project.name } : {},
|
|
@@ -2999,7 +3416,7 @@ function describeVideoProject(project) {
|
|
|
2999
3416
|
fps: project.fps,
|
|
3000
3417
|
duration: videoProjectDuration(project),
|
|
3001
3418
|
...project.background ? { background: project.background } : {},
|
|
3002
|
-
assets: project.assets,
|
|
3419
|
+
assets: project.assets.map(videoAssetOut),
|
|
3003
3420
|
tracks: project.tracks.map((t, index) => ({
|
|
3004
3421
|
id: t.id,
|
|
3005
3422
|
kind: t.kind,
|
|
@@ -3038,14 +3455,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3038
3455
|
return null;
|
|
3039
3456
|
};
|
|
3040
3457
|
const placed = (o) => ({
|
|
3041
|
-
..._optionalChain([o, 'access',
|
|
3042
|
-
..._optionalChain([o, 'access',
|
|
3043
|
-
..._optionalChain([o, 'access',
|
|
3458
|
+
..._optionalChain([o, 'access', _92 => _92.created, 'optionalAccess', _93 => _93.length]) ? { created: o.created } : {},
|
|
3459
|
+
..._optionalChain([o, 'access', _94 => _94.trimmed, 'optionalAccess', _95 => _95.length]) ? { trimmed: o.trimmed } : {},
|
|
3460
|
+
..._optionalChain([o, 'access', _96 => _96.removed, 'optionalAccess', _97 => _97.length]) ? { removed: o.removed } : {},
|
|
3044
3461
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
3045
3462
|
});
|
|
3046
3463
|
const keysOut = (p, clip, property) => {
|
|
3047
3464
|
const c = clipOut(p, clip);
|
|
3048
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
3465
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _98 => _98.transform, 'optionalAccess', _99 => _99[property]]) : _optionalChain([c, 'optionalAccess', _100 => _100[property]]);
|
|
3049
3466
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
3050
3467
|
};
|
|
3051
3468
|
const getProject = {
|
|
@@ -3221,9 +3638,9 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3221
3638
|
name: "update_video_asset",
|
|
3222
3639
|
title: "Change media details",
|
|
3223
3640
|
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.",
|
|
3224
|
-
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"]),
|
|
3641
|
+
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"]),
|
|
3225
3642
|
annotations: { title: "Change media details", ...WRITE_IDEMPOTENT },
|
|
3226
|
-
handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: r.project.assets.find((a) => a.id === asset) }))
|
|
3643
|
+
handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: videoAssetOut(r.project.assets.find((a) => a.id === asset)) }))
|
|
3227
3644
|
};
|
|
3228
3645
|
const removeAsset = {
|
|
3229
3646
|
name: "remove_video_asset",
|
|
@@ -3343,7 +3760,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3343
3760
|
const at = findVideoClip(p, id);
|
|
3344
3761
|
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3345
3762
|
const { clip } = at;
|
|
3346
|
-
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access',
|
|
3763
|
+
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _101 => _101.assets, 'access', _102 => _102.find, 'call', _103 => _103((a) => a.id === clip.asset), 'optionalAccess', _104 => _104.kind]) === "audio")
|
|
3347
3764
|
throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
|
|
3348
3765
|
}
|
|
3349
3766
|
};
|
|
@@ -3364,7 +3781,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3364
3781
|
gradeTargets(clips);
|
|
3365
3782
|
const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
|
|
3366
3783
|
const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3367
|
-
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call',
|
|
3784
|
+
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _105 => _105(r.project, id), 'optionalAccess', _106 => _106.clip, 'access', _107 => _107.grade]), () => ( null)) })) }));
|
|
3368
3785
|
}
|
|
3369
3786
|
};
|
|
3370
3787
|
const copyGrade = {
|
|
@@ -3380,6 +3797,191 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3380
3797
|
return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
|
|
3381
3798
|
}
|
|
3382
3799
|
};
|
|
3800
|
+
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)." };
|
|
3801
|
+
const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
|
|
3802
|
+
const timelineWords = (track) => {
|
|
3803
|
+
const words = videoTimelineTranscript(project(), { track });
|
|
3804
|
+
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.");
|
|
3805
|
+
return words;
|
|
3806
|
+
};
|
|
3807
|
+
const setTranscript = {
|
|
3808
|
+
name: "set_video_transcript",
|
|
3809
|
+
title: "Attach a transcript",
|
|
3810
|
+
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.',
|
|
3811
|
+
inputSchema: object(
|
|
3812
|
+
{
|
|
3813
|
+
asset: { ...ASSET_ID, description: "A video or audio asset." },
|
|
3814
|
+
text: { type: "string", minLength: 1, description: "The transcript file's contents: Whisper-style JSON (times in seconds), .srt or .vtt. Pass this or words." },
|
|
3815
|
+
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." }
|
|
3816
|
+
},
|
|
3817
|
+
["asset"]
|
|
3818
|
+
),
|
|
3819
|
+
annotations: { title: "Attach a transcript", ...WRITE_IDEMPOTENT },
|
|
3820
|
+
handler: ({ asset, text, words }) => {
|
|
3821
|
+
if (text === void 0 === (words === void 0)) throw new Error(`Pass either "text" (a transcript file's contents) or "words", not both.`);
|
|
3822
|
+
const a = project().assets.find((x) => x.id === asset);
|
|
3823
|
+
if (!a) throw new Error(`There is no asset "${asset}"; the assets are ${project().assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
|
|
3824
|
+
if (a.kind === "image") throw new Error(`Asset "${asset}" is an image and has no sound to transcribe.`);
|
|
3825
|
+
const parsed = text !== void 0 ? parseVideoTranscript(text) : { words, approximate: false };
|
|
3826
|
+
const list = parsed.words;
|
|
3827
|
+
return apply({ op: "update_asset", asset, set: { transcript: list } }, "Attach transcript", () => ({
|
|
3828
|
+
asset,
|
|
3829
|
+
words: list.length,
|
|
3830
|
+
approximate: parsed.approximate,
|
|
3831
|
+
first: list[0],
|
|
3832
|
+
last: list[list.length - 1],
|
|
3833
|
+
...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?` } : {}
|
|
3834
|
+
}));
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
const getTranscript = {
|
|
3838
|
+
name: "get_video_transcript",
|
|
3839
|
+
title: "Read the transcript",
|
|
3840
|
+
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.",
|
|
3841
|
+
inputSchema: object({
|
|
3842
|
+
track: TRANSCRIPT_TRACK,
|
|
3843
|
+
from: MS("Only words from this timeline time (default 0)"),
|
|
3844
|
+
to: MS("Only words up to this timeline time (default the end)"),
|
|
3845
|
+
limit: { type: "integer", minimum: 1, maximum: 5e3, description: 'Most words to return (default 1000); "more" says if there are others.' }
|
|
3846
|
+
}),
|
|
3847
|
+
annotations: { title: "Read the transcript", ...READ },
|
|
3848
|
+
handler: ({ track, from, to, limit }) => {
|
|
3849
|
+
const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
|
|
3850
|
+
const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
|
|
3851
|
+
return { track: _nullishCoalesce(_optionalChain([words, 'access', _108 => _108[0], 'optionalAccess', _109 => _109.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 };
|
|
3852
|
+
}
|
|
3853
|
+
};
|
|
3854
|
+
const findText = {
|
|
3855
|
+
name: "find_video_transcript_text",
|
|
3856
|
+
title: "Find words in the transcript",
|
|
3857
|
+
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.",
|
|
3858
|
+
inputSchema: object({ query: { type: "string", minLength: 1, description: "The word or phrase." }, track: TRANSCRIPT_TRACK }, ["query"]),
|
|
3859
|
+
annotations: { title: "Find words in the transcript", ...READ },
|
|
3860
|
+
handler: ({ query, track }) => ({ matches: findVideoTranscriptText(timelineWords(track), query).map(RANGE_OUT) })
|
|
3861
|
+
};
|
|
3862
|
+
const findFillers = {
|
|
3863
|
+
name: "find_video_fillers",
|
|
3864
|
+
title: "Find filler words",
|
|
3865
|
+
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.`,
|
|
3866
|
+
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." } }),
|
|
3867
|
+
annotations: { title: "Find filler words", ...READ },
|
|
3868
|
+
handler: ({ track, fillers }) => {
|
|
3869
|
+
const found = findVideoFillers(timelineWords(track), fillers);
|
|
3870
|
+
return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
const findPauses = {
|
|
3874
|
+
name: "find_video_pauses",
|
|
3875
|
+
title: "Find pauses between words",
|
|
3876
|
+
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.',
|
|
3877
|
+
inputSchema: object({
|
|
3878
|
+
track: TRANSCRIPT_TRACK,
|
|
3879
|
+
min: { type: "number", exclusiveMinimum: 0, description: "Shortest gap that counts, in ms (default 700)." },
|
|
3880
|
+
keep: { type: "number", minimum: 0, description: "Silence to keep after the word before and before the word after, in ms (default 150)." }
|
|
3881
|
+
}),
|
|
3882
|
+
annotations: { title: "Find pauses between words", ...READ },
|
|
3883
|
+
handler: ({ track, min, keep }) => {
|
|
3884
|
+
const found = findVideoTranscriptPauses(timelineWords(track), { min, keep, fps: project().fps });
|
|
3885
|
+
return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
|
|
3886
|
+
}
|
|
3887
|
+
};
|
|
3888
|
+
const removeRanges = {
|
|
3889
|
+
name: "remove_video_ranges",
|
|
3890
|
+
title: "Cut out stretches",
|
|
3891
|
+
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.",
|
|
3892
|
+
inputSchema: object(
|
|
3893
|
+
{
|
|
3894
|
+
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)." },
|
|
3895
|
+
tracks: { type: "array", items: TRACK_ID, minItems: 1, description: "Tracks to cut (default every unlocked track)." }
|
|
3896
|
+
},
|
|
3897
|
+
["ranges"]
|
|
3898
|
+
),
|
|
3899
|
+
annotations: { title: "Cut out stretches", ...DESTRUCTIVE },
|
|
3900
|
+
handler: ({ ranges, tracks }) => {
|
|
3901
|
+
const bad = ranges.findIndex((r) => !(r.to > r.from));
|
|
3902
|
+
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".`);
|
|
3903
|
+
const merged = mergeVideoRanges(ranges);
|
|
3904
|
+
const before = videoProjectDuration(project());
|
|
3905
|
+
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) }));
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
const rampTarget = (id) => {
|
|
3909
|
+
const at = findVideoClip(project(), id);
|
|
3910
|
+
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3911
|
+
const { clip } = at;
|
|
3912
|
+
if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
|
|
3913
|
+
if (_optionalChain([project, 'call', _110 => _110(), 'access', _111 => _111.assets, 'access', _112 => _112.find, 'call', _113 => _113((a) => a.id === clip.asset), 'optionalAccess', _114 => _114.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
|
|
3914
|
+
return clip;
|
|
3915
|
+
};
|
|
3916
|
+
const rampOut = (r, id) => {
|
|
3917
|
+
const outcome2 = r.outcomes[r.outcomes.length - 1];
|
|
3918
|
+
return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: videoProjectDuration(r.project) };
|
|
3919
|
+
};
|
|
3920
|
+
const RIPPLE_RAMP = { type: "boolean", description: "Move later material on every sync-locked track by the change in length (default true); false refuses if the clip would overlap the next one." };
|
|
3921
|
+
const setSpeedRamp = {
|
|
3922
|
+
name: "set_video_speed_ramp",
|
|
3923
|
+
title: "Ramp the speed",
|
|
3924
|
+
description: `Plays a video or audio clip through stretches of speed, from its first frame: e.g. [{ duration: 1500, speed: 1 }, { duration: 500, speed: 1, toSpeed: 0.25 }, { duration: 2000, speed: 0.25 }] for a slow-motion ramp. "toSpeed" glides evenly to a new speed over the stretch; speed 0 is a freeze frame. The clip's length becomes the stretches' total (one undo step). Replaces any ramp or speed it had. Returns the clip (with its timeRemap keys and source range).`,
|
|
3925
|
+
inputSchema: object(
|
|
3926
|
+
{
|
|
3927
|
+
clip: CLIP_ID,
|
|
3928
|
+
segments: {
|
|
3929
|
+
type: "array",
|
|
3930
|
+
minItems: 1,
|
|
3931
|
+
maxItems: 100,
|
|
3932
|
+
description: "The stretches in order, from the clip's start.",
|
|
3933
|
+
items: object(
|
|
3934
|
+
{
|
|
3935
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length of the stretch on the timeline, in ms." },
|
|
3936
|
+
speed: { type: "number", minimum: 0, maximum: 20, description: "Speed at its start: 1 normal, 0.5 half speed (slow motion), 2 double, 0 a freeze frame." },
|
|
3937
|
+
toSpeed: { type: "number", minimum: 0, maximum: 20, description: "Glide evenly to this speed by the stretch's end (default: stay at speed). A ramp between two speeds." }
|
|
3938
|
+
},
|
|
3939
|
+
["duration", "speed"],
|
|
3940
|
+
"One stretch."
|
|
3941
|
+
)
|
|
3942
|
+
},
|
|
3943
|
+
ripple: RIPPLE_RAMP
|
|
3944
|
+
},
|
|
3945
|
+
["clip", "segments"]
|
|
3946
|
+
),
|
|
3947
|
+
annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
|
|
3948
|
+
handler: ({ clip, segments, ripple }) => {
|
|
3949
|
+
const edit = videoSpeedRampEdit(rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
|
|
3950
|
+
return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(videoSpeedRampKeys(segments).source) }));
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
const addFreeze = {
|
|
3954
|
+
name: "add_video_freeze_frame",
|
|
3955
|
+
title: "Add a freeze frame",
|
|
3956
|
+
description: "Holds the frame a video clip shows at a timeline time for a while, then plays on (one undo step): the clip gets longer by the hold, and with ripple (default) everything after it on sync-locked tracks moves along. Works on clips with or without a speed ramp. Returns the clip.",
|
|
3957
|
+
inputSchema: object(
|
|
3958
|
+
{
|
|
3959
|
+
clip: CLIP_ID,
|
|
3960
|
+
at: MS("Timeline time of the frame to hold (inside the clip)"),
|
|
3961
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "How long to hold it, in ms." },
|
|
3962
|
+
ripple: RIPPLE_RAMP
|
|
3963
|
+
},
|
|
3964
|
+
["clip", "at", "duration"]
|
|
3965
|
+
),
|
|
3966
|
+
annotations: { title: "Add a freeze frame", ...WRITE },
|
|
3967
|
+
handler: ({ clip, at, duration, ripple }) => {
|
|
3968
|
+
const c = rampTarget(clip);
|
|
3969
|
+
const t = at - c.start;
|
|
3970
|
+
if (t < 0 || t > c.duration) throw new Error(`${formatVideoSeconds(at)} is outside clip "${clip}" (${formatVideoSeconds(c.start)}\u2013${formatVideoSeconds(videoClipEnd(c))}); pick a time inside it.`);
|
|
3971
|
+
return apply(videoFreezeFrameEdit(c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
|
|
3972
|
+
}
|
|
3973
|
+
};
|
|
3974
|
+
const removeRamp = {
|
|
3975
|
+
name: "remove_video_speed_ramp",
|
|
3976
|
+
title: "Remove a speed ramp",
|
|
3977
|
+
description: 'Puts a ramped clip back to normal speed from the moment its ramp started (one undo step), keeping its length unless "duration" is given. Returns the clip.',
|
|
3978
|
+
inputSchema: object({ clip: CLIP_ID, duration: { type: "number", exclusiveMinimum: 0, description: "New length in ms (default: unchanged)." }, ripple: RIPPLE_RAMP }, ["clip"]),
|
|
3979
|
+
annotations: { title: "Remove a speed ramp", ...WRITE },
|
|
3980
|
+
handler: ({ clip, duration, ripple }) => {
|
|
3981
|
+
rampTarget(clip);
|
|
3982
|
+
return apply({ op: "set_time_remap", clip, keys: null, ...duration !== void 0 ? { duration } : {}, ripple: _nullishCoalesce(ripple, () => ( true)) }, "Remove speed ramp", (r) => rampOut(r, clip));
|
|
3983
|
+
}
|
|
3984
|
+
};
|
|
3383
3985
|
const setTransition = {
|
|
3384
3986
|
name: "set_video_transition",
|
|
3385
3987
|
title: "Set a transition",
|
|
@@ -3490,7 +4092,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3490
4092
|
};
|
|
3491
4093
|
const EDIT_OP = {
|
|
3492
4094
|
type: "object",
|
|
3493
|
-
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
4095
|
+
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, set_time_remap { clip, keys (clip ms \u2192 source ms after the in point; null removes), duration, ripple }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
3494
4096
|
properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
|
|
3495
4097
|
required: ["op"]
|
|
3496
4098
|
};
|
|
@@ -3597,8 +4199,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3597
4199
|
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
|
|
3598
4200
|
}
|
|
3599
4201
|
};
|
|
3600
|
-
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
3601
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
4202
|
+
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
4203
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
3602
4204
|
return {
|
|
3603
4205
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
3604
4206
|
namespace: options.namespace,
|
|
@@ -3684,7 +4286,7 @@ function videoClipLabel(clip, asset) {
|
|
|
3684
4286
|
if (clip.name) return clip.name;
|
|
3685
4287
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
3686
4288
|
if (clip.type === "color") return clip.color;
|
|
3687
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
4289
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _115 => _115.name]), () => ( _optionalChain([asset, 'optionalAccess', _116 => _116.src, 'access', _117 => _117.replace, 'call', _118 => _118(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
3688
4290
|
const speed = videoClipSpeed(clip);
|
|
3689
4291
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
3690
4292
|
}
|
|
@@ -3728,7 +4330,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
|
|
|
3728
4330
|
const asset = videoAssetMap(project).get(assetId);
|
|
3729
4331
|
if (!asset) return null;
|
|
3730
4332
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
3731
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
4333
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _119 => _119.tracks, 'access', _120 => _120.find, 'call', _121 => _121((t) => t.kind === kind && !t.locked), 'optionalAccess', _122 => _122.id])));
|
|
3732
4334
|
if (!track) return null;
|
|
3733
4335
|
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 } : {} };
|
|
3734
4336
|
}
|
|
@@ -3777,14 +4379,14 @@ var KEY_LABELS = {
|
|
|
3777
4379
|
scale: { label: "Scale", step: 0.05 },
|
|
3778
4380
|
rotation: { label: "Rotation", step: 1 }
|
|
3779
4381
|
};
|
|
3780
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
4382
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _123 => _123.transform, 'optionalAccess', _124 => _124[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
3781
4383
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
3782
4384
|
function videoKeyframeRows(project, clipId, time) {
|
|
3783
4385
|
const at = findVideoClip(project, clipId);
|
|
3784
4386
|
if (!at) return [];
|
|
3785
4387
|
const { clip } = at;
|
|
3786
4388
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
3787
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
4389
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _125 => _125.kind]) === "audio" || at.track.kind === "audio");
|
|
3788
4390
|
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
3789
4391
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
3790
4392
|
const clipTime = playheadInClip(clip, time);
|
|
@@ -3984,5 +4586,29 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
3984
4586
|
|
|
3985
4587
|
|
|
3986
4588
|
|
|
3987
|
-
|
|
4589
|
+
|
|
4590
|
+
|
|
4591
|
+
|
|
4592
|
+
|
|
4593
|
+
|
|
4594
|
+
|
|
4595
|
+
|
|
4596
|
+
|
|
4597
|
+
|
|
4598
|
+
|
|
4599
|
+
|
|
4600
|
+
|
|
4601
|
+
|
|
4602
|
+
|
|
4603
|
+
|
|
4604
|
+
|
|
4605
|
+
|
|
4606
|
+
|
|
4607
|
+
|
|
4608
|
+
|
|
4609
|
+
|
|
4610
|
+
|
|
4611
|
+
|
|
4612
|
+
|
|
4613
|
+
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.checkVideoTimeRemap = checkVideoTimeRemap; 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.parseVideoTimeRemap = parseVideoTimeRemap; 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.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; 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.videoFreezeFrameEdit = videoFreezeFrameEdit; 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.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; 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;
|
|
3988
4614
|
//# sourceMappingURL=index.cjs.map
|