@clubnet/seedclub 0.2.33 → 0.2.35
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/assets/extensions/seedclub/commands/transcript-export-helpers.js +354 -0
- package/assets/extensions/seedclub/commands/transcript-export.ts +656 -0
- package/assets/extensions/seedclub/commands/transcript-intent.ts +6 -986
- package/assets/extensions/seedclub/commands/transcripts.ts +16 -241
- package/assets/extensions/seedclub/tool-utils.ts +45 -11
- package/assets/extensions/seedclub/tools/media.ts +569 -0
- package/assets/extensions/seedclub/ui-copy.ts +4 -0
- package/package.json +6 -2
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
function normalizeWhitespace(value) {
|
|
2
|
+
return value.replace(/[\u2018\u2019]/g, "'").replace(/\s+/g, " ").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const NON_PERSON_QUERIES = new Set([
|
|
6
|
+
"both",
|
|
7
|
+
"all",
|
|
8
|
+
"everyone",
|
|
9
|
+
"everybody",
|
|
10
|
+
"anyone",
|
|
11
|
+
"anybody",
|
|
12
|
+
"each",
|
|
13
|
+
"each of these",
|
|
14
|
+
"all of these",
|
|
15
|
+
"these",
|
|
16
|
+
"those",
|
|
17
|
+
"this",
|
|
18
|
+
"them",
|
|
19
|
+
"the above",
|
|
20
|
+
"above",
|
|
21
|
+
"interviews",
|
|
22
|
+
"calls",
|
|
23
|
+
"meetings",
|
|
24
|
+
"conversations",
|
|
25
|
+
"episodes",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const NON_PERSON_PREFIXES = new Set([
|
|
29
|
+
"i",
|
|
30
|
+
"we",
|
|
31
|
+
"download",
|
|
32
|
+
"get",
|
|
33
|
+
"grab",
|
|
34
|
+
"pull",
|
|
35
|
+
"save",
|
|
36
|
+
"export",
|
|
37
|
+
"need",
|
|
38
|
+
"want",
|
|
39
|
+
"send",
|
|
40
|
+
"give",
|
|
41
|
+
"retrieve",
|
|
42
|
+
"latest",
|
|
43
|
+
"last",
|
|
44
|
+
"most",
|
|
45
|
+
"recent",
|
|
46
|
+
"the",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const NON_PERSON_TOKENS = new Set([
|
|
50
|
+
"i",
|
|
51
|
+
"we",
|
|
52
|
+
"me",
|
|
53
|
+
"us",
|
|
54
|
+
"need",
|
|
55
|
+
"want",
|
|
56
|
+
"download",
|
|
57
|
+
"export",
|
|
58
|
+
"get",
|
|
59
|
+
"last",
|
|
60
|
+
"latest",
|
|
61
|
+
"recent",
|
|
62
|
+
"stream",
|
|
63
|
+
"show",
|
|
64
|
+
"11am",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function trimQuotes(value) {
|
|
68
|
+
return value.replace(/^['\"]|['\"]$/g, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizePersonQuery(raw) {
|
|
72
|
+
if (!raw) return undefined;
|
|
73
|
+
const cleaned = normalizeWhitespace(raw)
|
|
74
|
+
.replace(/\b(?:interviews?|calls?|meetings?|conversations?|episodes?|transcripts?|vtts?|stream|show)\b/gi, "")
|
|
75
|
+
.replace(/\b(?:each|all)\s+of\s+(?:these|those|them)\b/gi, "")
|
|
76
|
+
.replace(/\b(?:on|at|in|from)\b\s*$/i, "")
|
|
77
|
+
.trim();
|
|
78
|
+
if (!cleaned) return undefined;
|
|
79
|
+
|
|
80
|
+
const lower = cleaned.toLowerCase();
|
|
81
|
+
if (NON_PERSON_QUERIES.has(lower)) return undefined;
|
|
82
|
+
if (
|
|
83
|
+
/\b(today(?:'s|s)?|yesterday(?:'s|s)?|tomorrow(?:'s|s)?|tonight|morning|afternoon|evening|monday|mon|tuesday|tues|tue|toesday|wednesday|wed|thursday|thurs|thu|friday|fri|saturday|sat|sunday|sun|latest|recent|last)\b/.test(
|
|
84
|
+
lower,
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (/^\d{1,2}(?::\d{2})?\s*(?:am|pm)$/.test(lower)) return undefined;
|
|
90
|
+
if (/^(?:these|those|this|them|each|the\s+above|above)$/.test(lower)) return undefined;
|
|
91
|
+
|
|
92
|
+
const parts = lower.split(/\s+/).filter(Boolean);
|
|
93
|
+
if (!parts.length) return undefined;
|
|
94
|
+
|
|
95
|
+
const firstWord = parts[0];
|
|
96
|
+
if (NON_PERSON_PREFIXES.has(firstWord)) return undefined;
|
|
97
|
+
if (parts.some((part) => NON_PERSON_TOKENS.has(part))) return undefined;
|
|
98
|
+
|
|
99
|
+
return cleaned;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatLocalDate(date) {
|
|
103
|
+
const year = date.getFullYear();
|
|
104
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
105
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
106
|
+
return `${year}-${month}-${day}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function addLocalDays(date, days) {
|
|
110
|
+
const next = new Date(date);
|
|
111
|
+
next.setDate(next.getDate() + days);
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseRelativeDateHint(text) {
|
|
116
|
+
const lower = normalizeWhitespace(text).toLowerCase();
|
|
117
|
+
const today = new Date();
|
|
118
|
+
|
|
119
|
+
if (/\b(today(?:'s|s)?|tonight)\b/.test(lower)) return formatLocalDate(today);
|
|
120
|
+
if (/\byesterday(?:'s|s)?\b/.test(lower)) return formatLocalDate(addLocalDays(today, -1));
|
|
121
|
+
if (/\btomorrow(?:'s|s)?\b/.test(lower)) return formatLocalDate(addLocalDays(today, 1));
|
|
122
|
+
|
|
123
|
+
const weekdayMatch = lower.match(
|
|
124
|
+
/\b(?:(last|next)\s+)?(monday|mon|tuesday|tues|tue|toesday|wednesday|wed|thursday|thurs|thu|friday|fri|saturday|sat|sunday|sun)(?:'s|s)?\b/,
|
|
125
|
+
);
|
|
126
|
+
if (!weekdayMatch) return undefined;
|
|
127
|
+
|
|
128
|
+
const weekdayByName = {
|
|
129
|
+
sunday: 0,
|
|
130
|
+
sun: 0,
|
|
131
|
+
monday: 1,
|
|
132
|
+
mon: 1,
|
|
133
|
+
tuesday: 2,
|
|
134
|
+
tues: 2,
|
|
135
|
+
tue: 2,
|
|
136
|
+
toesday: 2,
|
|
137
|
+
wednesday: 3,
|
|
138
|
+
wed: 3,
|
|
139
|
+
thursday: 4,
|
|
140
|
+
thurs: 4,
|
|
141
|
+
thu: 4,
|
|
142
|
+
friday: 5,
|
|
143
|
+
fri: 5,
|
|
144
|
+
saturday: 6,
|
|
145
|
+
sat: 6,
|
|
146
|
+
};
|
|
147
|
+
const modifier = weekdayMatch[1];
|
|
148
|
+
const targetDay = weekdayByName[weekdayMatch[2]];
|
|
149
|
+
const currentDay = today.getDay();
|
|
150
|
+
let offset = targetDay - currentDay;
|
|
151
|
+
|
|
152
|
+
if (modifier === "next") {
|
|
153
|
+
offset = offset <= 0 ? offset + 7 : offset;
|
|
154
|
+
} else if (modifier === "last") {
|
|
155
|
+
offset = offset >= 0 ? offset - 7 : offset;
|
|
156
|
+
} else if (offset > 0) {
|
|
157
|
+
offset -= 7;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return formatLocalDate(addLocalDays(today, offset));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseDateHint(text) {
|
|
164
|
+
const iso = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
165
|
+
if (iso?.[1]) return iso[1];
|
|
166
|
+
|
|
167
|
+
const relative = parseRelativeDateHint(text);
|
|
168
|
+
if (relative) return relative;
|
|
169
|
+
|
|
170
|
+
const monthDay = text.match(/\b(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(\d{4}))?\b/i);
|
|
171
|
+
if (!monthDay) return undefined;
|
|
172
|
+
|
|
173
|
+
const monthNames = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
|
|
174
|
+
const monthIndex = monthNames.indexOf(monthDay[1].toLowerCase());
|
|
175
|
+
if (monthIndex < 0) return undefined;
|
|
176
|
+
|
|
177
|
+
const day = Number.parseInt(monthDay[2], 10);
|
|
178
|
+
if (!Number.isFinite(day) || day < 1 || day > 31) return undefined;
|
|
179
|
+
|
|
180
|
+
const year = monthDay[3] ? Number.parseInt(monthDay[3], 10) : new Date().getFullYear();
|
|
181
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
182
|
+
if (Number.isNaN(date.getTime())) return undefined;
|
|
183
|
+
return date.toISOString().slice(0, 10);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseIntent(input) {
|
|
187
|
+
const text = normalizeWhitespace(input);
|
|
188
|
+
const intent = {};
|
|
189
|
+
|
|
190
|
+
const betweenPair = text.match(/\bbetween\s+([\w'. -]{2,80}?)\s*(?:and|&)\s*([\w'. -]{2,80}?)(?=\s+(?:from|on|in|at|for|transcripts?|vtts?|interviews?|calls?|meetings?|date|to)\b|[?.!,]|$)/i);
|
|
191
|
+
if (betweenPair?.[1] && betweenPair?.[2]) {
|
|
192
|
+
const first = normalizePersonQuery(betweenPair[1]);
|
|
193
|
+
const second = normalizePersonQuery(betweenPair[2]);
|
|
194
|
+
const people = [first, second].filter(Boolean);
|
|
195
|
+
if (people.length) intent.people = Array.from(new Set(people));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const personWith = text.match(/\bwith\s+([\w'. -]{2,80}?)(?=\s+(?:from|on|in|at|for|transcripts?|vtts?|interviews?|calls?|meetings?|date|to)\b|$)/i);
|
|
199
|
+
if (personWith?.[1]) intent.person = normalizePersonQuery(personWith[1]);
|
|
200
|
+
|
|
201
|
+
if (!intent.person) {
|
|
202
|
+
const personFor = text.match(/\bfor\s+([\w'. -]{2,80}?)(?=\s+(?:from|on|in|at|transcripts?|vtts?|interviews?|calls?|meetings?|date|to)\b|$)/i);
|
|
203
|
+
if (personFor?.[1]) intent.person = normalizePersonQuery(personFor[1]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!intent.person) {
|
|
207
|
+
const verbBeforeTranscript = text.match(
|
|
208
|
+
/\b(?:download|get|export|save|pull|grab|retrieve|fetch|send|give)\s+([a-z][\w'.-]*(?:\s+[a-z][\w'.-]*){0,3})\s+transcripts?\b/i,
|
|
209
|
+
);
|
|
210
|
+
if (verbBeforeTranscript?.[1]) {
|
|
211
|
+
intent.person = normalizePersonQuery(verbBeforeTranscript[1]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!intent.person) {
|
|
216
|
+
const beforeTranscript = text.match(/\b([a-z][\w'.-]*(?:\s+[a-z][\w'.-]*){0,3})\s+transcripts?\b/i);
|
|
217
|
+
if (beforeTranscript?.[1]) {
|
|
218
|
+
const candidate = normalizePersonQuery(beforeTranscript[1]);
|
|
219
|
+
if (candidate && !/\b(?:need|want|download|get|last|latest)\b/i.test(candidate)) {
|
|
220
|
+
intent.person = candidate;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if ((!intent.people || !intent.people.length) && intent.person) {
|
|
226
|
+
intent.people = [intent.person];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const latestN = text.match(/\b(?:last|latest|most recent)\s+(\d{1,2})\b/i);
|
|
230
|
+
if (latestN?.[1]) {
|
|
231
|
+
intent.latestCount = Math.max(1, Math.min(20, Number.parseInt(latestN[1], 10)));
|
|
232
|
+
} else if (/\b(?:last|latest|most recent)\b/i.test(text)) {
|
|
233
|
+
intent.latestCount = 1;
|
|
234
|
+
} else if (/\brecent\b/i.test(text)) {
|
|
235
|
+
intent.latestCount = 5;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
intent.date = parseDateHint(text);
|
|
239
|
+
if ((intent.people?.length ?? 0) > 1 && /\b(?:both|between|across|and|&)\b/i.test(text)) {
|
|
240
|
+
intent.date = undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const explicitTimeMatch = text.match(/\b(?:at|time)\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm)|(?:[01]?\d|2[0-3]):[0-5]\d)\b/i);
|
|
244
|
+
const bareTimeMatch = text.match(/\b((?:[1-9]|1[0-2])(?::\d{2})?\s*(?:am|pm)|(?:[01]?\d|2[0-3]):[0-5]\d)\b/i);
|
|
245
|
+
const timeMatch = explicitTimeMatch ?? bareTimeMatch;
|
|
246
|
+
if (timeMatch?.[1]) {
|
|
247
|
+
const normalizedTime = timeMatch[1].replace(/\s+/g, "").toLowerCase();
|
|
248
|
+
if (explicitTimeMatch || normalizedTime !== "11am") intent.time = normalizedTime;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const outMatch = text.match(/\b(?:to|into|in)\s+(~\/\S+|\/\S+)/i);
|
|
252
|
+
if (outMatch?.[1]) intent.outDir = trimQuotes(outMatch[1]);
|
|
253
|
+
|
|
254
|
+
return intent;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseTimeFilter(input) {
|
|
258
|
+
if (!input) return null;
|
|
259
|
+
const value = input.trim().toLowerCase();
|
|
260
|
+
const twentyFourHour = value.match(/^([01]?\d|2[0-3]):([0-5]\d)$/);
|
|
261
|
+
if (twentyFourHour) {
|
|
262
|
+
return {
|
|
263
|
+
hour: Number.parseInt(twentyFourHour[1], 10),
|
|
264
|
+
minute: Number.parseInt(twentyFourHour[2], 10),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const match = value.match(/^(\d{1,2})(?::(\d{2}))?(am|pm)$/);
|
|
269
|
+
if (!match) return null;
|
|
270
|
+
let hour = Number.parseInt(match[1], 10);
|
|
271
|
+
const minute = match[2] ? Number.parseInt(match[2], 10) : undefined;
|
|
272
|
+
const period = match[3];
|
|
273
|
+
if (hour < 1 || hour > 12) return null;
|
|
274
|
+
if (minute !== undefined && (minute < 0 || minute > 59)) return null;
|
|
275
|
+
if (period === "am") {
|
|
276
|
+
hour = hour === 12 ? 0 : hour;
|
|
277
|
+
} else {
|
|
278
|
+
hour = hour === 12 ? 12 : hour + 12;
|
|
279
|
+
}
|
|
280
|
+
return { hour, minute };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeFuzzyText(value) {
|
|
284
|
+
return value
|
|
285
|
+
.toLowerCase()
|
|
286
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
287
|
+
.replace(/\s+/g, " ")
|
|
288
|
+
.trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function exactNormalizedTextMatch(query, target) {
|
|
292
|
+
const q = normalizeFuzzyText(query);
|
|
293
|
+
const t = normalizeFuzzyText(target);
|
|
294
|
+
if (!q || !t) return false;
|
|
295
|
+
if (q === t) return true;
|
|
296
|
+
return new RegExp(`(^| )${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}( |$)`).test(t);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeTranscriptText(value) {
|
|
300
|
+
return value.replace(/\r\n/g, "\n").trim();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isLikelyVtt(value) {
|
|
304
|
+
const text = value.trim();
|
|
305
|
+
if (!text) return false;
|
|
306
|
+
return /^WEBVTT\b/i.test(text) || /\d{2}:\d{2}:\d{2}[\.,]\d{3}\s+-->\s+\d{2}:\d{2}:\d{2}[\.,]\d{3}/.test(text);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toTimestamp(totalSeconds) {
|
|
310
|
+
const ms = Math.max(0, Math.floor(totalSeconds * 1000));
|
|
311
|
+
const h = Math.floor(ms / 3600000);
|
|
312
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
313
|
+
const s = Math.floor((ms % 60000) / 1000);
|
|
314
|
+
const milli = ms % 1000;
|
|
315
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function textToSyntheticVtt(text) {
|
|
319
|
+
const normalized = normalizeTranscriptText(text);
|
|
320
|
+
if (!normalized) return null;
|
|
321
|
+
|
|
322
|
+
const sections = normalized
|
|
323
|
+
.split(/\n\s*\n+/)
|
|
324
|
+
.map((value) => value.trim())
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
.slice(0, 500);
|
|
327
|
+
if (!sections.length) return null;
|
|
328
|
+
|
|
329
|
+
let cursor = 0;
|
|
330
|
+
const cues = [];
|
|
331
|
+
for (const section of sections) {
|
|
332
|
+
const charCount = section.length;
|
|
333
|
+
const duration = Math.max(2, Math.min(10, Math.ceil(charCount / 28)));
|
|
334
|
+
const start = toTimestamp(cursor);
|
|
335
|
+
const end = toTimestamp(cursor + duration);
|
|
336
|
+
cues.push(`${start} --> ${end}\n${section}`);
|
|
337
|
+
cursor += duration;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return `WEBVTT\n\n${cues.join("\n\n")}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export {
|
|
344
|
+
exactNormalizedTextMatch,
|
|
345
|
+
isLikelyVtt,
|
|
346
|
+
normalizeFuzzyText,
|
|
347
|
+
normalizePersonQuery,
|
|
348
|
+
normalizeWhitespace,
|
|
349
|
+
parseDateHint,
|
|
350
|
+
parseIntent,
|
|
351
|
+
parseRelativeDateHint,
|
|
352
|
+
parseTimeFilter,
|
|
353
|
+
textToSyntheticVtt,
|
|
354
|
+
};
|