@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 = class {
1941
1955
  }
1942
1956
  };
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(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 {
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(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(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" && assets.get(c.asset)?.transcript?.length);
2024
+ return (project.tracks.find((t) => t.kind === "audio" && transcribed(t)) ?? project.tracks.find(transcribed))?.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 = assets.get(clip.asset)?.transcript;
2035
+ if (!words?.length) continue;
2036
+ const speed = videoClipSpeed(clip);
2037
+ const from = 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 = options.min ?? 700;
2074
+ const keep = 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);
@@ -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,
@@ -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",
@@ -3380,6 +3563,114 @@ function createVideoEditorToolProvider(controller, options = {}) {
3380
3563
  return apply(edits, "Copy grade", () => ({ grade: 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 > (from ?? 0) && w.start < (to ?? Infinity));
3616
+ const words = all.slice(0, limit ?? 1e3);
3617
+ return { track: words[0]?.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: options.id ?? "video",
3604
3895
  namespace: options.namespace,
@@ -3843,6 +4134,7 @@ export {
3843
4134
  VIDEO_CLIP_ANIMATION_EFFECTS,
3844
4135
  VIDEO_CLIP_ANIMATION_LABELS,
3845
4136
  VIDEO_CLIP_TYPES,
4137
+ VIDEO_DEFAULT_FILLERS,
3846
4138
  VIDEO_DEFAULT_STILL_DURATION,
3847
4139
  VIDEO_DEFAULT_TITLE_DURATION,
3848
4140
  VIDEO_EASE_NAMES,
@@ -3879,14 +4171,19 @@ export {
3879
4171
  ffmpegFilterValue,
3880
4172
  findVideoAudioSilences,
3881
4173
  findVideoClip,
4174
+ findVideoFillers,
3882
4175
  findVideoGaps,
4176
+ findVideoTranscriptPauses,
4177
+ findVideoTranscriptText,
3883
4178
  formatVideoSeconds,
3884
4179
  formatVideoTimecode,
3885
4180
  formatVideoTimecodeFrames,
3886
4181
  isVideoDropFrameRate,
3887
4182
  isVideoKeyframes,
4183
+ mergeVideoRanges,
3888
4184
  nextFreeVideoSlot,
3889
4185
  normalizeVideoGrade,
4186
+ normalizeVideoWord,
3890
4187
  parseVideoAnimatable,
3891
4188
  parseVideoAsset,
3892
4189
  parseVideoClip,
@@ -3897,6 +4194,8 @@ export {
3897
4194
  parseVideoMarker,
3898
4195
  parseVideoProject,
3899
4196
  parseVideoTrack,
4197
+ parseVideoTranscript,
4198
+ parseVideoTranscriptWords,
3900
4199
  parseVideoTransform,
3901
4200
  parseVideoTransition,
3902
4201
  placeVideoRect,
@@ -3971,18 +4270,24 @@ export {
3971
4270
  videoPeakDb,
3972
4271
  videoProjectDuration,
3973
4272
  videoProjectSvg,
4273
+ videoRemoveRangesEdits,
3974
4274
  videoRulerTicks,
3975
4275
  videoSnapPoints,
3976
4276
  videoTextStyle,
3977
4277
  videoTimecodeFrames,
4278
+ videoTimelineTranscript,
3978
4279
  videoTitleFrame,
3979
4280
  videoTrackSyncLocked,
4281
+ videoTranscriptEdit,
4282
+ videoTranscriptTrack,
3980
4283
  videoTransformAt,
3981
4284
  videoTransitionAudioCurve,
3982
4285
  videoTransitionEase,
3983
4286
  videoTransitionHasDirection,
3984
4287
  videoValueAt,
3985
4288
  videoWaveformPath,
3986
- videoWipeRect
4289
+ videoWipeRect,
4290
+ videoWordAt,
4291
+ videoWordsRange
3987
4292
  };
3988
4293
  //# sourceMappingURL=index.js.map