@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.
- package/dist/video-editor/index.cjs +342 -37
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +118 -2
- package/dist/video-editor/index.d.ts +118 -2
- package/dist/video-editor/index.js +312 -7
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +20 -3
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
2492
|
-
_optionalChain([entry, 'access',
|
|
2670
|
+
_optionalChain([entry, 'access', _72 => _72.media, 'access', _73 => _73.removeAttribute, 'optionalCall', _74 => _74("src")]);
|
|
2671
|
+
_optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.load, 'optionalCall', _77 => _77()]);
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
3042
|
-
..._optionalChain([o, 'access',
|
|
3043
|
-
..._optionalChain([o, 'access',
|
|
3224
|
+
..._optionalChain([o, 'access', _89 => _89.created, 'optionalAccess', _90 => _90.length]) ? { created: o.created } : {},
|
|
3225
|
+
..._optionalChain([o, 'access', _91 => _91.trimmed, 'optionalAccess', _92 => _92.length]) ? { trimmed: o.trimmed } : {},
|
|
3226
|
+
..._optionalChain([o, 'access', _93 => _93.removed, 'optionalAccess', _94 => _94.length]) ? { removed: o.removed } : {},
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|