@pixerate/editor 0.84.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -435,6 +435,18 @@ function parseVideoTransition(v, path) {
435
435
  audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`)
436
436
  });
437
437
  }
438
+ function parseVideoTranscriptWords(v, path) {
439
+ if (v === void 0) return void 0;
440
+ if (!Array.isArray(v)) return fail(path, "must be an array of { word, start, end } (ms in the source)");
441
+ const words = v.map((w, i) => {
442
+ if (!isObject(w)) return fail(`${path}[${i}]`, "must be an object { word, start, end }");
443
+ const word = str(w.word, `${path}[${i}].word`);
444
+ const start = num(w.start, `${path}[${i}].start`, { min: 0 });
445
+ const end = num(w.end, `${path}[${i}].end`, { min: start });
446
+ return { word, start, end };
447
+ });
448
+ return words.sort((a, b) => a.start - b.start);
449
+ }
438
450
  function parseVideoAsset(v, path = "asset") {
439
451
  if (!isObject(v)) return fail(path, "must be an object");
440
452
  const kind = oneOf(v.kind, ["video", "audio", "image"], `${path}.kind`, false);
@@ -447,8 +459,10 @@ function parseVideoAsset(v, path = "asset") {
447
459
  width: num(v.width, `${path}.width`, { optional: true, positive: true }),
448
460
  height: num(v.height, `${path}.height`, { optional: true, positive: true }),
449
461
  hasAudio: bool(v.hasAudio, `${path}.hasAudio`),
450
- poster: str(v.poster, `${path}.poster`, true)
462
+ poster: str(v.poster, `${path}.poster`, true),
463
+ transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`)
451
464
  });
465
+ if (kind === "image" && asset.transcript) fail(`${path}.transcript`, "is only for video and audio assets");
452
466
  if (kind === "image" && asset.duration !== void 0) fail(`${path}.duration`, "is only for video and audio assets");
453
467
  return asset;
454
468
  }
@@ -1941,6 +1955,171 @@ var VideoAudioPeaksCache = (_class2 = class {
1941
1955
  }
1942
1956
  }, _class2);
1943
1957
 
1958
+ // src/video-editor/transcript.ts
1959
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1960
+ function cueTime(s) {
1961
+ const m = s.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
1962
+ if (!m) throw new Error(`"${s.trim()}" is not a cue time (HH:MM:SS,mmm).`);
1963
+ return ((Number(_nullishCoalesce(m[1], () => ( 0))) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1e3 + Number(m[4].padEnd(3, "0"));
1964
+ }
1965
+ function cueWords(text, start, end) {
1966
+ const words = text.replace(/<[^>]+>/g, "").split(/\s+/).filter(Boolean);
1967
+ const step = (end - start) / Math.max(1, words.length);
1968
+ return words.map((word, i) => ({ word, start: Math.round(start + i * step), end: Math.round(start + (i + 1) * step) }));
1969
+ }
1970
+ function parseVideoTranscript(input) {
1971
+ let data = input;
1972
+ if (typeof input === "string") {
1973
+ const text = input.replace(/^/, "").trim();
1974
+ if (text.startsWith("{") || text.startsWith("[")) {
1975
+ try {
1976
+ data = JSON.parse(text);
1977
+ } catch (e3) {
1978
+ throw new Error("The transcript looks like JSON but is not valid JSON.");
1979
+ }
1980
+ } else {
1981
+ const words2 = [];
1982
+ for (const block of text.replace(/\r\n?/g, "\n").split(/\n{2,}/)) {
1983
+ const lines = block.split("\n");
1984
+ const at = lines.findIndex((l) => l.includes("-->"));
1985
+ if (at < 0) continue;
1986
+ const [a, b] = lines[at].split("-->");
1987
+ words2.push(...cueWords(lines.slice(at + 1).join(" "), cueTime(a), cueTime(b.trim().split(/\s+/)[0])));
1988
+ }
1989
+ if (!words2.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
1990
+ return { words: words2, approximate: true };
1991
+ }
1992
+ }
1993
+ const seconds = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e3) : NaN;
1994
+ const fromList = (list, toMs) => list.map((w, i) => {
1995
+ if (!isRecord(w)) throw new Error(`Word ${i + 1} is not an object { word, start, end }.`);
1996
+ const word = String(_nullishCoalesce(_nullishCoalesce(w.word, () => ( w.text)), () => ( ""))).trim();
1997
+ const start = toMs(w.start);
1998
+ const end = toMs(w.end);
1999
+ if (!word || !Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`Word ${i + 1} needs a word and numeric start and end.`);
2000
+ return { word, start, end: Math.max(start, end) };
2001
+ });
2002
+ let words = [];
2003
+ if (Array.isArray(data)) words = fromList(data, (v) => typeof v === "number" ? v : NaN);
2004
+ else if (isRecord(data) && Array.isArray(data.words)) words = fromList(data.words, seconds);
2005
+ else if (isRecord(data) && Array.isArray(data.segments)) {
2006
+ const segments = data.segments.filter(isRecord);
2007
+ if (segments.some((s) => Array.isArray(s.words))) words = fromList(segments.flatMap((s) => Array.isArray(s.words) ? s.words : []), seconds);
2008
+ else {
2009
+ for (const s of segments) words.push(...cueWords(String(_nullishCoalesce(s.text, () => ( ""))), seconds(s.start), seconds(s.end)));
2010
+ if (!words.length) throw new Error("No words found in the segments.");
2011
+ return { words, approximate: true };
2012
+ }
2013
+ }
2014
+ if (!words.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
2015
+ return { words: words.sort((a, b) => a.start - b.start), approximate: false };
2016
+ }
2017
+ function videoTranscriptTrack(project, track) {
2018
+ if (track !== void 0) {
2019
+ if (!project.tracks.some((t) => t.id === track)) throw new Error(`There is no track "${track}"; the tracks are ${project.tracks.map((t) => `"${t.id}"`).join(", ")}.`);
2020
+ return track;
2021
+ }
2022
+ const assets = videoAssetMap(project);
2023
+ const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _39 => _39.get, 'call', _40 => _40(c.asset), 'optionalAccess', _41 => _41.transcript, 'optionalAccess', _42 => _42.length]));
2024
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _43 => _43.id]), () => ( null));
2025
+ }
2026
+ function videoTimelineTranscript(project, options = {}) {
2027
+ const id = videoTranscriptTrack(project, options.track);
2028
+ if (!id) return [];
2029
+ const track = project.tracks.find((t) => t.id === id);
2030
+ const assets = videoAssetMap(project);
2031
+ const out = [];
2032
+ for (const clip of sortedVideoClips(track)) {
2033
+ if (clip.type !== "media") continue;
2034
+ const words = _optionalChain([assets, 'access', _44 => _44.get, 'call', _45 => _45(clip.asset), 'optionalAccess', _46 => _46.transcript]);
2035
+ if (!_optionalChain([words, 'optionalAccess', _47 => _47.length])) continue;
2036
+ const speed = videoClipSpeed(clip);
2037
+ const from = _nullishCoalesce(clip.in, () => ( 0));
2038
+ const to = from + clip.duration * speed;
2039
+ const toTimeline = (s) => Math.min(videoClipEnd(clip), Math.max(clip.start, clip.start + (s - from) / speed));
2040
+ words.forEach((w, index) => {
2041
+ const mid = (w.start + w.end) / 2;
2042
+ if (mid < from || mid >= to) return;
2043
+ out.push({ word: w.word, start: toTimeline(w.start), end: toTimeline(w.end), clip: clip.id, track: track.id, index });
2044
+ });
2045
+ }
2046
+ return out;
2047
+ }
2048
+ var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
2049
+ var rangeOf = (words, i, n2) => ({ from: words[i].start, to: words[i + n2 - 1].end, text: words.slice(i, i + n2).map((w) => w.word).join(" ") });
2050
+ function findVideoTranscriptText(words, query) {
2051
+ const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
2052
+ if (!q.length) throw new Error("Search for at least one word.");
2053
+ const norm = words.map((w) => normalizeVideoWord(w.word));
2054
+ const out = [];
2055
+ for (let i = 0; i + q.length <= norm.length; i++) if (q.every((x, k) => norm[i + k] === x)) out.push(rangeOf(words, i, q.length));
2056
+ return out;
2057
+ }
2058
+ var VIDEO_DEFAULT_FILLERS = ["um", "umm", "uh", "uhh", "uhm", "erm", "er", "ah", "hmm", "mm", "you know", "i mean"];
2059
+ function findVideoFillers(words, fillers = VIDEO_DEFAULT_FILLERS) {
2060
+ const phrases = fillers.map((f) => f.split(/\s+/).map(normalizeVideoWord).filter(Boolean)).filter((p) => p.length).sort((a, b) => b.length - a.length);
2061
+ const norm = words.map((w) => normalizeVideoWord(w.word));
2062
+ const out = [];
2063
+ for (let i = 0; i < norm.length; ) {
2064
+ const hit = phrases.find((p) => i + p.length <= norm.length && p.every((x, k) => norm[i + k] === x));
2065
+ if (hit) {
2066
+ out.push(rangeOf(words, i, hit.length));
2067
+ i += hit.length;
2068
+ } else i++;
2069
+ }
2070
+ return out;
2071
+ }
2072
+ function findVideoTranscriptPauses(words, options = {}) {
2073
+ const min = _nullishCoalesce(options.min, () => ( 700));
2074
+ const keep = _nullishCoalesce(options.keep, () => ( 150));
2075
+ const out = [];
2076
+ for (let i = 1; i < words.length; i++) {
2077
+ const a = words[i - 1];
2078
+ const b = words[i];
2079
+ if (a.clip !== b.clip || b.start - a.end < min) continue;
2080
+ let from = a.end + keep;
2081
+ let to = b.start - keep;
2082
+ if (options.fps) {
2083
+ const f = 1e3 / options.fps;
2084
+ from = snapToVideoFrame(Math.ceil(from / f - 1e-9) * f, options.fps);
2085
+ to = snapToVideoFrame(Math.floor(to / f + 1e-9) * f, options.fps);
2086
+ }
2087
+ if (to - from > VIDEO_TIME_EPSILON) out.push({ from, to, text: `(pause after "${a.word}")` });
2088
+ }
2089
+ return out;
2090
+ }
2091
+ function mergeVideoRanges(ranges) {
2092
+ const sorted = ranges.filter((r) => r.to - r.from > VIDEO_TIME_EPSILON).map((r) => ({ from: r.from, to: r.to })).sort((a, b) => a.from - b.from);
2093
+ const out = [];
2094
+ for (const r of sorted) {
2095
+ const last = out[out.length - 1];
2096
+ if (last && r.from <= last.to + VIDEO_TIME_EPSILON) last.to = Math.max(last.to, r.to);
2097
+ else out.push(r);
2098
+ }
2099
+ return out;
2100
+ }
2101
+ function videoRemoveRangesEdits(ranges, tracks) {
2102
+ return mergeVideoRanges(ranges).reverse().map((r) => ({ op: "extract_range", from: r.from, to: r.to, ...tracks ? { tracks } : {} }));
2103
+ }
2104
+ function videoWordsRange(words, a, b) {
2105
+ const [i, j] = a <= b ? [a, b] : [b, a];
2106
+ return { from: words[i].start, to: words[j].end };
2107
+ }
2108
+ function videoTranscriptEdit(assetId, text) {
2109
+ return { op: "update_asset", asset: assetId, set: { transcript: parseVideoTranscript(text).words } };
2110
+ }
2111
+ function videoWordAt(words, time) {
2112
+ let lo = 0;
2113
+ let hi = words.length - 1;
2114
+ while (lo <= hi) {
2115
+ const mid = lo + hi >> 1;
2116
+ if (words[mid].end <= time) lo = mid + 1;
2117
+ else if (words[mid].start > time) hi = mid - 1;
2118
+ else return mid;
2119
+ }
2120
+ return -1;
2121
+ }
2122
+
1944
2123
  // src/video-editor/render.ts
1945
2124
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1946
2125
  var n = (v) => String(Math.round(v * 100) / 100);
@@ -2097,7 +2276,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
2097
2276
  }
2098
2277
 
2099
2278
  // src/video-editor/export.ts
2100
- var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _39 => _39.name]), () => ( _optionalChain([asset, 'optionalAccess', _40 => _40.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
2279
+ var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _48 => _48.name]), () => ( _optionalChain([asset, 'optionalAccess', _49 => _49.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
2101
2280
  function toVideoEdl(project, options = {}) {
2102
2281
  const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
2103
2282
  if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
@@ -2132,7 +2311,7 @@ function toVideoEdl(project, options = {}) {
2132
2311
  lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2133
2312
  } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2134
2313
  if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
2135
- if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _41 => _41.name]), () => ( _optionalChain([asset, 'optionalAccess', _42 => _42.src]))), () => ( clip.asset))}`);
2314
+ if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _50 => _50.name]), () => ( _optionalChain([asset, 'optionalAccess', _51 => _51.src]))), () => ( clip.asset))}`);
2136
2315
  if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
2137
2316
  if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
2138
2317
  lines.push("");
@@ -2266,7 +2445,7 @@ function buildFfmpeg(project, options, measure) {
2266
2445
  if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
2267
2446
  if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
2268
2447
  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', _43 => _43.transition, 'access', _44 => _44.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2448
+ if (_optionalChain([next2, 'optionalAccess', _52 => _52.transition, 'access', _53 => _53.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2270
2449
  if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
2271
2450
  if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
2272
2451
  chain2.push(`adelay=${Math.round(t0)}:all=1`);
@@ -2279,7 +2458,7 @@ function buildFfmpeg(project, options, measure) {
2279
2458
  const onTimeline = `(t*1000-${Math.round(clip.start)})`;
2280
2459
  const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
2281
2460
  const tf = clip.transform;
2282
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _45 => _45[p]])));
2461
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _54 => _54[p]])));
2283
2462
  if (clip.type === "text" && options.titleImage) {
2284
2463
  inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
2285
2464
  index = inputs.length - 1;
@@ -2288,7 +2467,7 @@ function buildFfmpeg(project, options, measure) {
2288
2467
  const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
2289
2468
  const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
2290
2469
  const coord = (axis) => {
2291
- const v = _optionalChain([tf, 'optionalAccess', _46 => _46[axis]]);
2470
+ const v = _optionalChain([tf, 'optionalAccess', _55 => _55[axis]]);
2292
2471
  const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
2293
2472
  return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
2294
2473
  };
@@ -2296,7 +2475,7 @@ function buildFfmpeg(project, options, measure) {
2296
2475
  const opts = [
2297
2476
  `text=${ffmpegFilterValue(clip.text)}`,
2298
2477
  options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
2299
- isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _47 => _47.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _48 => _48.scale]), () => ( 1))))}`,
2478
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _56 => _56.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _57 => _57.scale]), () => ( 1))))}`,
2300
2479
  `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
2301
2480
  coord("x"),
2302
2481
  coord("y"),
@@ -2304,7 +2483,7 @@ function buildFfmpeg(project, options, measure) {
2304
2483
  ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
2305
2484
  `enable='between(t,${sec(clip.start)},${sec(end)})'`
2306
2485
  ];
2307
- if (_optionalChain([tf, 'optionalAccess', _49 => _49.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2486
+ if (_optionalChain([tf, 'optionalAccess', _58 => _58.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2308
2487
  if (videoGradeFfmpegFilters(clip.grade).length) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
2309
2488
  if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
2310
2489
  graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
@@ -2341,10 +2520,10 @@ function buildFfmpeg(project, options, measure) {
2341
2520
  const r = videoIrisRadius({ width: W, height: H }, 1);
2342
2521
  chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
2343
2522
  }
2344
- const rotation = _optionalChain([placed, 'optionalAccess', _50 => _50.rotation]);
2523
+ const rotation = _optionalChain([placed, 'optionalAccess', _59 => _59.rotation]);
2345
2524
  if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
2346
2525
  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', _51 => _51.scale]))) {
2526
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _60 => _60.scale]))) {
2348
2527
  const s = expr(placed.scale, onChain("t"), 1, "scale");
2349
2528
  chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
2350
2529
  }
@@ -2363,9 +2542,9 @@ function buildFfmpeg(project, options, measure) {
2363
2542
  const into = lead ? clip.transition.type : void 0;
2364
2543
  if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
2365
2544
  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', _52 => _52.transition, 'access', _53 => _53.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2545
+ if (_optionalChain([after, 'optionalAccess', _61 => _61.transition, 'access', _62 => _62.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2367
2546
  const at = (axis, c) => {
2368
- const v = _optionalChain([placed, 'optionalAccess', _54 => _54[axis]]);
2547
+ const v = _optionalChain([placed, 'optionalAccess', _63 => _63[axis]]);
2369
2548
  const extra = shifts[axis].map((s) => `+${s}`).join("");
2370
2549
  return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
2371
2550
  };
@@ -2399,7 +2578,7 @@ function buildFfmpeg(project, options, measure) {
2399
2578
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
2400
2579
  else if (options.loudness && !options.loudness.measured)
2401
2580
  notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
2402
- if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _55 => _55.get, 'call', _56 => _56(c.asset), 'optionalAccess', _57 => _57.kind]) === "video" && _optionalChain([assets, 'access', _58 => _58.get, 'call', _59 => _59(c.asset), 'optionalAccess', _60 => _60.hasAudio]) === void 0)))
2581
+ if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _64 => _64.get, 'call', _65 => _65(c.asset), 'optionalAccess', _66 => _66.kind]) === "video" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.hasAudio]) === void 0)))
2403
2582
  notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
2404
2583
  if (animated.size)
2405
2584
  notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
@@ -2438,7 +2617,7 @@ var VideoPreviewEngine = (_class3 = class {
2438
2617
  this.source = source;
2439
2618
  this.options = options;
2440
2619
  this.canvas = canvas;
2441
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _61 => _61.getContext, 'optionalCall', _62 => _62("2d")]), () => ( null));
2620
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _70 => _70.getContext, 'optionalCall', _71 => _71("2d")]), () => ( null));
2442
2621
  this.unsubscribe = source.subscribe(() => this.update());
2443
2622
  this.update();
2444
2623
  }
@@ -2488,8 +2667,8 @@ var VideoPreviewEngine = (_class3 = class {
2488
2667
  entry.media.pause();
2489
2668
  entry.media.removeEventListener("seeked", entry.onReady);
2490
2669
  entry.media.removeEventListener("loadeddata", entry.onReady);
2491
- _optionalChain([entry, 'access', _63 => _63.media, 'access', _64 => _64.removeAttribute, 'optionalCall', _65 => _65("src")]);
2492
- _optionalChain([entry, 'access', _66 => _66.media, 'access', _67 => _67.load, 'optionalCall', _68 => _68()]);
2670
+ _optionalChain([entry, 'access', _72 => _72.media, 'access', _73 => _73.removeAttribute, 'optionalCall', _74 => _74("src")]);
2671
+ _optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.load, 'optionalCall', _77 => _77()]);
2493
2672
  this.media.delete(entry.clip);
2494
2673
  }
2495
2674
  /** Brings the media elements in step with the editor and draws the frame. */
@@ -2522,7 +2701,7 @@ var VideoPreviewEngine = (_class3 = class {
2522
2701
  for (const layer of frame.layers) {
2523
2702
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
2524
2703
  const sound = sounding.get(layer.clip);
2525
- sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _69 => _69.speed]), () => ( 1)), sound ? sound.gain : null);
2704
+ sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _78 => _78.speed]), () => ( 1)), sound ? sound.gain : null);
2526
2705
  sounding.delete(layer.clip);
2527
2706
  }
2528
2707
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -2555,7 +2734,7 @@ var VideoPreviewEngine = (_class3 = class {
2555
2734
  if (this.canvas.width !== w) this.canvas.width = w;
2556
2735
  if (this.canvas.height !== h) this.canvas.height = h;
2557
2736
  drawVideoFrame(this.ctx, frame, this.sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
2558
- _optionalChain([this, 'access', _70 => _70.options, 'access', _71 => _71.onFrame, 'optionalCall', _72 => _72(frame)]);
2737
+ _optionalChain([this, 'access', _79 => _79.options, 'access', _80 => _80.onFrame, 'optionalCall', _81 => _81(frame)]);
2559
2738
  }
2560
2739
  __init13() {this.defineSvgFilter = (id, markup) => {
2561
2740
  if (this.options.defineSvgFilter === null) return;
@@ -2576,7 +2755,7 @@ var VideoPreviewEngine = (_class3 = class {
2576
2755
  /** Stops following the editor and releases every media element. */
2577
2756
  destroy() {
2578
2757
  this.destroyed = true;
2579
- _optionalChain([this, 'access', _73 => _73.filterHost, 'optionalAccess', _74 => _74.remove, 'call', _75 => _75()]);
2758
+ _optionalChain([this, 'access', _82 => _82.filterHost, 'optionalAccess', _83 => _83.remove, 'call', _84 => _84()]);
2580
2759
  this.filterHost = null;
2581
2760
  this.filters.clear();
2582
2761
  this.unsubscribe();
@@ -2684,7 +2863,7 @@ var VideoEditorController = (_class4 = class {
2684
2863
  this.applying = false;
2685
2864
  }
2686
2865
  this.notify();
2687
- _optionalChain([this, 'access', _76 => _76.onChange, 'optionalCall', _77 => _77(project)]);
2866
+ _optionalChain([this, 'access', _85 => _85.onChange, 'optionalCall', _86 => _86(project)]);
2688
2867
  }
2689
2868
  commit(label, next) {
2690
2869
  const before = this.project;
@@ -2748,7 +2927,7 @@ var VideoEditorController = (_class4 = class {
2748
2927
  }
2749
2928
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
2750
2929
  select(clips, options = {}) {
2751
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _78 => _78.filter, 'call', _79 => _79((id) => findVideoClip(this.project, id))]), () => ( []));
2930
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _87 => _87.filter, 'call', _88 => _88((id) => findVideoClip(this.project, id))]), () => ( []));
2752
2931
  let next;
2753
2932
  if (options.add) {
2754
2933
  next = [...this.selection];
@@ -2991,6 +3170,10 @@ var issueSummary = (issues) => ({
2991
3170
  notes: issues.filter((i) => i.severity === "note").length,
2992
3171
  lines: issues.map((i) => `${i.severity}: ${i.message}`)
2993
3172
  });
3173
+ var videoAssetOut = (a) => {
3174
+ const { transcript, ...rest } = a;
3175
+ return transcript ? { ...rest, transcriptWords: transcript.length } : rest;
3176
+ };
2994
3177
  function describeVideoProject(project) {
2995
3178
  return {
2996
3179
  ...project.name ? { name: project.name } : {},
@@ -2999,7 +3182,7 @@ function describeVideoProject(project) {
2999
3182
  fps: project.fps,
3000
3183
  duration: videoProjectDuration(project),
3001
3184
  ...project.background ? { background: project.background } : {},
3002
- assets: project.assets,
3185
+ assets: project.assets.map(videoAssetOut),
3003
3186
  tracks: project.tracks.map((t, index) => ({
3004
3187
  id: t.id,
3005
3188
  kind: t.kind,
@@ -3038,14 +3221,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
3038
3221
  return null;
3039
3222
  };
3040
3223
  const placed = (o) => ({
3041
- ..._optionalChain([o, 'access', _80 => _80.created, 'optionalAccess', _81 => _81.length]) ? { created: o.created } : {},
3042
- ..._optionalChain([o, 'access', _82 => _82.trimmed, 'optionalAccess', _83 => _83.length]) ? { trimmed: o.trimmed } : {},
3043
- ..._optionalChain([o, 'access', _84 => _84.removed, 'optionalAccess', _85 => _85.length]) ? { removed: o.removed } : {},
3224
+ ..._optionalChain([o, 'access', _89 => _89.created, 'optionalAccess', _90 => _90.length]) ? { created: o.created } : {},
3225
+ ..._optionalChain([o, 'access', _91 => _91.trimmed, 'optionalAccess', _92 => _92.length]) ? { trimmed: o.trimmed } : {},
3226
+ ..._optionalChain([o, 'access', _93 => _93.removed, 'optionalAccess', _94 => _94.length]) ? { removed: o.removed } : {},
3044
3227
  ...o.rippled ? { rippled: o.rippled } : {}
3045
3228
  });
3046
3229
  const keysOut = (p, clip, property) => {
3047
3230
  const c = clipOut(p, clip);
3048
- const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _86 => _86.transform, 'optionalAccess', _87 => _87[property]]) : _optionalChain([c, 'optionalAccess', _88 => _88[property]]);
3231
+ const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _95 => _95.transform, 'optionalAccess', _96 => _96[property]]) : _optionalChain([c, 'optionalAccess', _97 => _97[property]]);
3049
3232
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
3050
3233
  };
3051
3234
  const getProject = {
@@ -3221,9 +3404,9 @@ function createVideoEditorToolProvider(controller, options = {}) {
3221
3404
  name: "update_video_asset",
3222
3405
  title: "Change media details",
3223
3406
  description: "Changes an asset's src, name, duration, size, sound or poster (e.g. relinking moved media). Clips that now run past its end are refused. Returns the asset.",
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"]),
3407
+ inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript"] }, description: "Fields to clear (transcript removes its words)." } }, ["asset"]),
3225
3408
  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) }))
3409
+ handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: videoAssetOut(r.project.assets.find((a) => a.id === asset)) }))
3227
3410
  };
3228
3411
  const removeAsset = {
3229
3412
  name: "remove_video_asset",
@@ -3343,7 +3526,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3343
3526
  const at = findVideoClip(p, id);
3344
3527
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
3345
3528
  const { clip } = at;
3346
- if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _89 => _89.assets, 'access', _90 => _90.find, 'call', _91 => _91((a) => a.id === clip.asset), 'optionalAccess', _92 => _92.kind]) === "audio")
3529
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _98 => _98.assets, 'access', _99 => _99.find, 'call', _100 => _100((a) => a.id === clip.asset), 'optionalAccess', _101 => _101.kind]) === "audio")
3347
3530
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
3348
3531
  }
3349
3532
  };
@@ -3364,7 +3547,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3364
3547
  gradeTargets(clips);
3365
3548
  const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
3366
3549
  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', _93 => _93(r.project, id), 'optionalAccess', _94 => _94.clip, 'access', _95 => _95.grade]), () => ( null)) })) }));
3550
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _102 => _102(r.project, id), 'optionalAccess', _103 => _103.clip, 'access', _104 => _104.grade]), () => ( null)) })) }));
3368
3551
  }
3369
3552
  };
3370
3553
  const copyGrade = {
@@ -3380,6 +3563,114 @@ function createVideoEditorToolProvider(controller, options = {}) {
3380
3563
  return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
3381
3564
  }
3382
3565
  };
3566
+ const TRANSCRIPT_TRACK = { ...TRACK_ID, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." };
3567
+ const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
3568
+ const timelineWords = (track) => {
3569
+ const words = videoTimelineTranscript(project(), { track });
3570
+ if (!words.length) throw new Error(track ? `No clip on track "${track}" plays a transcribed asset; attach one with set_video_transcript.` : "No clip plays a transcribed asset; attach a transcript to the media with set_video_transcript first.");
3571
+ return words;
3572
+ };
3573
+ const setTranscript = {
3574
+ name: "set_video_transcript",
3575
+ title: "Attach a transcript",
3576
+ description: 'Attaches what is said in a video or audio asset, word by word, so the edit can be cut by text (one undo step). Pass "text" (Whisper-style JSON with word timings, SubRip or WebVTT, as the file says it) or "words" ([{ word, start, end }] in ms of the source). Captions have no word timings, so their words are spread across each cue (approximate). Replaces any transcript the asset had. Returns the word count, whether timings are approximate, and the first and last words.',
3577
+ inputSchema: object(
3578
+ {
3579
+ asset: { ...ASSET_ID, description: "A video or audio asset." },
3580
+ text: { type: "string", minLength: 1, description: "The transcript file's contents: Whisper-style JSON (times in seconds), .srt or .vtt. Pass this or words." },
3581
+ words: { type: "array", minItems: 1, items: object({ word: { type: "string", minLength: 1, description: "The word as said." }, start: { type: "number", minimum: 0, description: "Start in ms of the source." }, end: { type: "number", minimum: 0, description: "End in ms of the source." } }, ["word", "start", "end"], "One word."), description: "Words with source times in ms. Pass this or text." }
3582
+ },
3583
+ ["asset"]
3584
+ ),
3585
+ annotations: { title: "Attach a transcript", ...WRITE_IDEMPOTENT },
3586
+ handler: ({ asset, text, words }) => {
3587
+ if (text === void 0 === (words === void 0)) throw new Error(`Pass either "text" (a transcript file's contents) or "words", not both.`);
3588
+ const a = project().assets.find((x) => x.id === asset);
3589
+ if (!a) throw new Error(`There is no asset "${asset}"; the assets are ${project().assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
3590
+ if (a.kind === "image") throw new Error(`Asset "${asset}" is an image and has no sound to transcribe.`);
3591
+ const parsed = text !== void 0 ? parseVideoTranscript(text) : { words, approximate: false };
3592
+ const list = parsed.words;
3593
+ return apply({ op: "update_asset", asset, set: { transcript: list } }, "Attach transcript", () => ({
3594
+ asset,
3595
+ words: list.length,
3596
+ approximate: parsed.approximate,
3597
+ first: list[0],
3598
+ last: list[list.length - 1],
3599
+ ...a.duration !== void 0 && list[list.length - 1].end > a.duration + VIDEO_TIME_EPSILON ? { warning: `The transcript runs to ${list[list.length - 1].end} ms, past the asset's ${a.duration} ms: is it for this asset?` } : {}
3600
+ }));
3601
+ }
3602
+ };
3603
+ const getTranscript = {
3604
+ name: "get_video_transcript",
3605
+ title: "Read the transcript",
3606
+ description: "Returns what the timeline says, in order, from the transcripts of the media its clips play on one track: the text, and each word with its timeline start and end (ms) and the clip that plays it, so ranges can go straight to remove_video_ranges or split_video_at. Words cut out of the edit are not included.",
3607
+ inputSchema: object({
3608
+ track: TRANSCRIPT_TRACK,
3609
+ from: MS("Only words from this timeline time (default 0)"),
3610
+ to: MS("Only words up to this timeline time (default the end)"),
3611
+ limit: { type: "integer", minimum: 1, maximum: 5e3, description: 'Most words to return (default 1000); "more" says if there are others.' }
3612
+ }),
3613
+ annotations: { title: "Read the transcript", ...READ },
3614
+ handler: ({ track, from, to, limit }) => {
3615
+ const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
3616
+ const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
3617
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _105 => _105[0], 'optionalAccess', _106 => _106.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
3618
+ }
3619
+ };
3620
+ const findText = {
3621
+ name: "find_video_transcript_text",
3622
+ title: "Find words in the transcript",
3623
+ description: "Lists every place a word or phrase is said on the timeline (case and punctuation ignored) as { from, to, text } ranges in timeline ms, ready for remove_video_ranges, split_video_at or seek_video.",
3624
+ inputSchema: object({ query: { type: "string", minLength: 1, description: "The word or phrase." }, track: TRANSCRIPT_TRACK }, ["query"]),
3625
+ annotations: { title: "Find words in the transcript", ...READ },
3626
+ handler: ({ query, track }) => ({ matches: findVideoTranscriptText(timelineWords(track), query).map(RANGE_OUT) })
3627
+ };
3628
+ const findFillers = {
3629
+ name: "find_video_fillers",
3630
+ title: "Find filler words",
3631
+ description: `Lists filler words and phrases on the timeline as { from, to, text } ranges in timeline ms, longest phrase first so "you know" is found whole. Default fillers: ${VIDEO_DEFAULT_FILLERS.join(", ")} ("like" and "so" are left out as too often meant). Check them, then pass the ones to cut to remove_video_ranges.`,
3632
+ inputSchema: object({ track: TRANSCRIPT_TRACK, fillers: { type: "array", minItems: 1, items: { type: "string", minLength: 1 }, description: "Words or phrases to treat as fillers instead of the defaults." } }),
3633
+ annotations: { title: "Find filler words", ...READ },
3634
+ handler: ({ track, fillers }) => {
3635
+ const found = findVideoFillers(timelineWords(track), fillers);
3636
+ return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
3637
+ }
3638
+ };
3639
+ const findPauses = {
3640
+ name: "find_video_pauses",
3641
+ title: "Find pauses between words",
3642
+ description: 'Lists the gaps between consecutive words on the timeline (within one clip) longer than "min", less "keep" next to each word so speech is not clipped, snapped inward to frames, as { from, to } ranges in timeline ms ready for remove_video_ranges. Uses word timings; find_video_silences measures the sound instead.',
3643
+ inputSchema: object({
3644
+ track: TRANSCRIPT_TRACK,
3645
+ min: { type: "number", exclusiveMinimum: 0, description: "Shortest gap that counts, in ms (default 700)." },
3646
+ keep: { type: "number", minimum: 0, description: "Silence to keep after the word before and before the word after, in ms (default 150)." }
3647
+ }),
3648
+ annotations: { title: "Find pauses between words", ...READ },
3649
+ handler: ({ track, min, keep }) => {
3650
+ const found = findVideoTranscriptPauses(timelineWords(track), { min, keep, fps: project().fps });
3651
+ return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
3652
+ }
3653
+ };
3654
+ const removeRanges = {
3655
+ name: "remove_video_ranges",
3656
+ title: "Cut out stretches",
3657
+ description: "Cuts stretches of the timeline out and closes them up, as one undo step: the ranges are merged where they touch and extracted from the last to the first, so the times from find_video_fillers, find_video_pauses, find_video_transcript_text or find_video_silences can be passed as they are. Like extract_video_range, every sync-locked track moves with the cut. Returns how many stretches and how much time were removed and the new length.",
3658
+ inputSchema: object(
3659
+ {
3660
+ ranges: { type: "array", minItems: 1, maxItems: 500, items: object({ from: MS("Start, on the timeline"), to: MS("End, on the timeline"), text: { type: "string", description: "Ignored: the words in the stretch, as the find tools return them." } }, ["from", "to"], "A stretch to remove."), description: "The stretches, in any order (timeline ms)." },
3661
+ tracks: { type: "array", items: TRACK_ID, minItems: 1, description: "Tracks to cut (default every unlocked track)." }
3662
+ },
3663
+ ["ranges"]
3664
+ ),
3665
+ annotations: { title: "Cut out stretches", ...DESTRUCTIVE },
3666
+ handler: ({ ranges, tracks }) => {
3667
+ const bad = ranges.findIndex((r) => !(r.to > r.from));
3668
+ if (bad >= 0) throw new Error(`Range ${bad + 1} is empty or backwards (${ranges[bad].from}\u2013${ranges[bad].to} ms); "to" must be after "from".`);
3669
+ const merged = mergeVideoRanges(ranges);
3670
+ const before = videoProjectDuration(project());
3671
+ return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - videoProjectDuration(r.project)), duration: videoProjectDuration(r.project) }));
3672
+ }
3673
+ };
3383
3674
  const setTransition = {
3384
3675
  name: "set_video_transition",
3385
3676
  title: "Set a transition",
@@ -3597,8 +3888,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
3597
3888
  return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
3598
3889
  }
3599
3890
  };
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];
3891
+ const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
3892
+ const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
3602
3893
  return {
3603
3894
  id: _nullishCoalesce(options.id, () => ( "video")),
3604
3895
  namespace: options.namespace,
@@ -3684,7 +3975,7 @@ function videoClipLabel(clip, asset) {
3684
3975
  if (clip.name) return clip.name;
3685
3976
  if (clip.type === "text") return clip.text.split("\n")[0];
3686
3977
  if (clip.type === "color") return clip.color;
3687
- const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _96 => _96.name]), () => ( _optionalChain([asset, 'optionalAccess', _97 => _97.src, 'access', _98 => _98.replace, 'call', _99 => _99(/^.*[\\/]/, "")]))), () => ( clip.asset));
3978
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _107 => _107.name]), () => ( _optionalChain([asset, 'optionalAccess', _108 => _108.src, 'access', _109 => _109.replace, 'call', _110 => _110(/^.*[\\/]/, "")]))), () => ( clip.asset));
3688
3979
  const speed = videoClipSpeed(clip);
3689
3980
  return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
3690
3981
  }
@@ -3728,7 +4019,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
3728
4019
  const asset = videoAssetMap(project).get(assetId);
3729
4020
  if (!asset) return null;
3730
4021
  const kind = asset.kind === "audio" ? "audio" : "video";
3731
- const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _100 => _100.tracks, 'access', _101 => _101.find, 'call', _102 => _102((t) => t.kind === kind && !t.locked), 'optionalAccess', _103 => _103.id])));
4022
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _111 => _111.tracks, 'access', _112 => _112.find, 'call', _113 => _113((t) => t.kind === kind && !t.locked), 'optionalAccess', _114 => _114.id])));
3732
4023
  if (!track) return null;
3733
4024
  return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
3734
4025
  }
@@ -3777,14 +4068,14 @@ var KEY_LABELS = {
3777
4068
  scale: { label: "Scale", step: 0.05 },
3778
4069
  rotation: { label: "Rotation", step: 1 }
3779
4070
  };
3780
- var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _104 => _104.transform, 'optionalAccess', _105 => _105[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4071
+ var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _115 => _115.transform, 'optionalAccess', _116 => _116[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
3781
4072
  var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
3782
4073
  function videoKeyframeRows(project, clipId, time) {
3783
4074
  const at = findVideoClip(project, clipId);
3784
4075
  if (!at) return [];
3785
4076
  const { clip } = at;
3786
4077
  const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
3787
- const visual = !(_optionalChain([asset, 'optionalAccess', _106 => _106.kind]) === "audio" || at.track.kind === "audio");
4078
+ const visual = !(_optionalChain([asset, 'optionalAccess', _117 => _117.kind]) === "audio" || at.track.kind === "audio");
3788
4079
  const audible = clip.type === "media" && videoAssetHasSound(asset);
3789
4080
  const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
3790
4081
  const clipTime = playheadInClip(clip, time);
@@ -3984,5 +4275,19 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
3984
4275
 
3985
4276
 
3986
4277
 
3987
- 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_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect;
4278
+
4279
+
4280
+
4281
+
4282
+
4283
+
4284
+
4285
+
4286
+
4287
+
4288
+
4289
+
4290
+
4291
+
4292
+ exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
3988
4293
  //# sourceMappingURL=index.cjs.map