Setting the file. One moment. Cutlist · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
function mergeRanges
— line 149
This file
- Number
- 27.16
- Position
- 16 of 78
- Type
- JavaScript
- Size
- 6 KB
- Lines
- 184
scripts/lib/cutlist.mjs
JavaScript·184 lines·6 KB
!=
null
&&
hasRemovalSource
(opts)) {
9 throw new Error("--keep is mutually exclusive with removal options");
10 }
11
12 if (opts.keep != null) {
13 const duration = durationFrom(words, opts);
14 const ranges = parseTimeRanges(opts.keep);
15 return finalizeKept(duration != null ? clampRanges(ranges, duration) : ranges);
16 }
17
18 const duration = durationFrom(words, opts);
19 if (!duration) return [];
20
21 const removals = [
22 ...parseTimeRanges(opts.remove),
23 ...wordIndexRanges(words, opts.removeWords),
24 ...fillerRanges(words, opts.removeFillers),
25 ...silenceRanges(words, opts.cutSilence),
26 ];
27 const mergedRemovals = mergeRanges(clampRanges(removals, duration));
28 return finalizeKept(invertRanges(mergedRemovals, duration));
29}
30
31function hasRemovalSource(opts) {
32 return (
33 opts.remove != null ||
34 opts.removeWords != null ||
35 opts.removeFillers != null ||
36 opts.cutSilence != null
37 );
38}
39
40function durationFrom(words, opts) {
41 const explicit = Number(opts.duration ?? opts.totalDuration);
42 if (Number.isFinite(explicit) && explicit > 0) return explicit;
43 const last = words.at(-1);
44 return last && Number.isFinite(last.end) && last.end > 0 ? last.end : null;
45}
46
47function parseTimeRanges(value) {
48 if (value == null || value === false || value === "") return [];
49 if (typeof value === "string") {
50 return value
51 .split(",")
52 .map((part) => part.trim())
53 .filter(Boolean)
54 .map(parseRangeString);
55 }
56 if (!Array.isArray(value)) throw new Error("range list must be a string or array");
57 return value.map((range) => {
58 if (Array.isArray(range)) return cleanRange(Number(range[0]), Number(range[1]));
59 return cleanRange(Number(range?.start), Number(range?.end));
60 });
61}
62
63function parseRangeString(value) {
64 const match = value.match(/^([0-9]*\.?[0-9]+)\s*-\s*([0-9]*\.?[0-9]+)$/);
65 if (!match) throw new Error(`invalid range: ${value}`);
66 return cleanRange(Number(match[1]), Number(match[2]));
67}
68
69function cleanRange(start, end) {
70 if (!Number.isFinite(start) || !Number.isFinite(end)) {
71 throw new Error("range start/end must be finite numbers");
72 }
73 if (end < start) throw new Error(`range end ${end} is before start ${start}`);
74 return { start, end };
75}
76
77function wordIndexRanges(words, value) {
78 if (value == null || value === false || value === "") return [];
79 const ranges = typeof value === "string" ? value.split(",") : value;
80 if (!Array.isArray(ranges)) throw new Error("--remove-words must be a string or array");
81 return ranges
82 .map((range) => (typeof range === "string" ? range.trim() : range))
83 .filter(Boolean)
84 .map((range) => {
85 const [first, last = first] =
86 typeof range === "string" ? range.split("-").map((n) => n.trim()) : range;
87 const startIndex = Number(first);
88 const endIndex = Number(last);
89 if (!Number.isInteger(startIndex) || !Number.isInteger(endIndex)) {
90 throw new Error(`invalid word range: ${range}`);
91 }
92 if (startIndex < 0 || endIndex < startIndex || endIndex >= words.length) {
93 throw new Error(`word range out of bounds: ${range}`);
94 }
95 return { start: words[startIndex].start, end: words[endIndex].end };
96 });
97}
98
99function fillerRanges(words, value) {
100 if (value == null || value === false || value === "") return [];
101 const fillers = Array.isArray(value)
102 ? value
103 : String(value)
104 .split(",")
105 .map((s) => s.trim());
106 const set = new Set(fillers.filter(Boolean).map(bareToken));
107 if (set.size === 0) return [];
108 // Whisper emits words with attached punctuation and arbitrary case
109 // ("UM," / "Um."), so compare bare tokens.
110 return words
111 .filter((word) => set.has(bareToken(word.text)))
112 .map((word) => ({ start: word.start, end: word.end }));
113}
114
115function bareToken(text) {
116 return String(text)
117 .toLowerCase()
118 .replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
119}
120
121function silenceRanges(words, value) {
122 if (value == null || value === false || value === "") return [];
123 const threshold = Number(value);
124 if (!Number.isFinite(threshold) || threshold <= 0) {
125 throw new Error("--cut-silence must be a positive number");
126 }
127 const ranges = [];
128 for (let i = 0; i < words.length - 1; i++) {
129 const current = words[i];
130 const next = words[i + 1];
131 const gap = next.start - current.end;
132 if (gap <= threshold) continue;
133 const start = current.end + SILENCE_PAD_SECONDS;
134 const end = next.start - SILENCE_PAD_SECONDS;
135 if (end > start) ranges.push({ start, end });
136 }
137 return ranges;
138}
139
140function clampRanges(ranges, duration) {
141 return ranges
142 .map((range) => ({
143 start: Math.max(0, Math.min(duration, range.start)),
144 end: Math.max(0, Math.min(duration, range.end)),
145 }))
146 .filter((range) => range.end > range.start);
147}
148
149function mergeRanges(ranges) {
150 const sorted = ranges
151 .map((range) => ({ start: round3(range.start), end: round3(range.end) }))
152 .sort((a, b) => a.start - b.start || a.end - b.end);
153 const merged = [];
154 for (const range of sorted) {
155 const prev = merged.at(-1);
156 if (prev && range.start <= prev.end) {
157 prev.end = Math.max(prev.end, range.end);
158 } else {
159 merged.push({ ...range });
160 }
161 }
162 return merged;
163}
164
165function invertRanges(removals, duration) {
166 const kept = [];
167 let cursor = 0;
168 for (const range of removals) {
169 if (range.start > cursor) kept.push({ start: cursor, end: range.start });
170 cursor = Math.max(cursor, range.end);
171 }
172 if (cursor < duration) kept.push({ start: cursor, end: duration });
173 return kept;
174}
175
176function finalizeKept(ranges) {
177 return mergeRanges(ranges)
178 .map((range) => ({ start: round3(range.start), end: round3(range.end) }))
179 .filter((range) => round3(range.end - range.start) >= MIN_SEGMENT_SECONDS);
180}
181
182function round3(n) {
183 return Math.round(Number(n) * 1000) / 1000;
184}