@ai-matrx/media 0.13.0 → 0.15.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.
@@ -0,0 +1,1689 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res, err) => function __init() {
7
+ if (err) throw err[0];
8
+ try {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ } catch (e) {
11
+ throw err = [e], e;
12
+ }
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+
28
+ // src/speech/configure.ts
29
+ function configureSpeech(next) {
30
+ ports = { ...ports, ...next, adapters: { ...ports.adapters, ...next.adapters } };
31
+ }
32
+ function speechPorts() {
33
+ return ports;
34
+ }
35
+ function requirePort(key) {
36
+ const value = ports[key];
37
+ if (!value) {
38
+ throw new Error(`Speech is not set up in this app: configureSpeech({ ${String(key)} }) was never called.`);
39
+ }
40
+ return value;
41
+ }
42
+ function resetSpeechPortsForTests() {
43
+ ports = {};
44
+ }
45
+ var ports;
46
+ var init_configure = __esm({
47
+ "src/speech/configure.ts"() {
48
+ "use strict";
49
+ ports = {};
50
+ }
51
+ });
52
+
53
+ // src/speech/unlock.ts
54
+ function declarePlaybackAudioSession() {
55
+ if (audioSessionDeclared) return;
56
+ const session = navigator.audioSession;
57
+ if (session) {
58
+ try {
59
+ session.type = "playback";
60
+ audioSessionDeclared = true;
61
+ } catch {
62
+ }
63
+ } else {
64
+ audioSessionDeclared = true;
65
+ }
66
+ }
67
+ function getUnlockedAudioContext() {
68
+ return sharedContext && sharedContext.state !== "closed" ? sharedContext : null;
69
+ }
70
+ function getPrimedMediaElement() {
71
+ return sharedElement;
72
+ }
73
+ function primeAudioOutput() {
74
+ if (typeof window === "undefined") return;
75
+ declarePlaybackAudioSession();
76
+ try {
77
+ if (!sharedContext || sharedContext.state === "closed") {
78
+ sharedContext = new AudioContext();
79
+ }
80
+ if (sharedContext.state === "suspended") {
81
+ void sharedContext.resume().catch(() => {
82
+ });
83
+ }
84
+ const buffer = sharedContext.createBuffer(1, 1, sharedContext.sampleRate);
85
+ const node = sharedContext.createBufferSource();
86
+ node.buffer = buffer;
87
+ node.connect(sharedContext.destination);
88
+ node.start(0);
89
+ } catch {
90
+ }
91
+ try {
92
+ if (!sharedElement) {
93
+ const el2 = new Audio();
94
+ el2.setAttribute("playsinline", "true");
95
+ sharedElement = el2;
96
+ }
97
+ const el = sharedElement;
98
+ if (el.paused && !el.src.startsWith("blob:") && !el.currentSrc.startsWith("http")) {
99
+ el.muted = true;
100
+ el.src = SILENT_WAV;
101
+ void el.play().then(() => {
102
+ el.pause();
103
+ el.muted = false;
104
+ }).catch(() => {
105
+ el.muted = false;
106
+ });
107
+ }
108
+ } catch {
109
+ }
110
+ }
111
+ function installAudioUnlockListeners() {
112
+ if (listenersInstalled || typeof window === "undefined") return;
113
+ listenersInstalled = true;
114
+ const onGesture = () => primeAudioOutput();
115
+ window.addEventListener("pointerdown", onGesture, {
116
+ capture: true,
117
+ passive: true
118
+ });
119
+ window.addEventListener("keydown", onGesture, {
120
+ capture: true,
121
+ passive: true
122
+ });
123
+ window.addEventListener("touchend", onGesture, {
124
+ capture: true,
125
+ passive: true
126
+ });
127
+ }
128
+ var sharedContext, sharedElement, listenersInstalled, audioSessionDeclared, SILENT_WAV;
129
+ var init_unlock = __esm({
130
+ "src/speech/unlock.ts"() {
131
+ "use strict";
132
+ sharedContext = null;
133
+ sharedElement = null;
134
+ listenersInstalled = false;
135
+ audioSessionDeclared = false;
136
+ SILENT_WAV = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=";
137
+ }
138
+ });
139
+
140
+ // src/voices/speech-text.ts
141
+ function speakTables(text) {
142
+ const lines = text.split("\n");
143
+ const out = [];
144
+ for (let i = 0; i < lines.length; i += 1) {
145
+ if (!(0, import_source.tableStartsAt)(lines, i)) {
146
+ out.push(lines[i] ?? "");
147
+ continue;
148
+ }
149
+ const end = (0, import_source.findTableEnd)(lines, i);
150
+ const headers = (0, import_source.rowCells)(lines[i] ?? "").map(import_source.unescapeCellPipes).filter((h) => h.length > 0);
151
+ const rowCount = lines.slice(i + 2, end).filter((line) => line.trim().length > 1).length;
152
+ const headerText = headers.length > 1 ? headers.slice(0, -1).join(", ") + ", and " + headers[headers.length - 1] : headers.length === 1 ? headers[0] : "unlabeled columns";
153
+ const rowText = rowCount === 1 ? "one row" : rowCount > 0 ? `${rowCount} rows` : "no rows";
154
+ out.push(`There is a table with ${rowText} of data provided for ${headerText}.`);
155
+ i = end - 1;
156
+ }
157
+ return out.join("\n");
158
+ }
159
+ function escapeRegExp(s) {
160
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+ function spokenFormForAbbreviation(match, sourceAbbreviation, suffix = "") {
163
+ const abbreviation = COMMON_ABBREVIATION_BY_UPPERCASE[sourceAbbreviation.toUpperCase()];
164
+ if (!abbreviation) return match;
165
+ if (CASE_SENSITIVE_WORD_ACRONYMS.has(abbreviation) && sourceAbbreviation !== abbreviation) {
166
+ return match;
167
+ }
168
+ if (SPOKEN_AS_WORD.has(abbreviation)) return `${abbreviation}${suffix}`;
169
+ const spoken = Array.from(abbreviation.toUpperCase()).join(" ");
170
+ return suffix ? `${spoken}'s` : spoken;
171
+ }
172
+ function normalizeSpeechBlanks(text) {
173
+ if (!text || !text.includes("_")) return text;
174
+ const blank = /(?<![\p{L}\p{N}])_(?:[ \t]*_)+(?![\p{L}\p{N}])/gu;
175
+ return text.split("\n").map(
176
+ (line) => /^[ \t]*_[ \t_]*$/.test(line) ? line : line.replace(blank, ` ${SPEECH_BLANK_WORD} `)
177
+ ).join("\n");
178
+ }
179
+ function applyPronunciations(text, pairs) {
180
+ let out = text;
181
+ for (const { from, to } of pairs) {
182
+ const term = from.trim();
183
+ if (!term || !to) continue;
184
+ const re = new RegExp(
185
+ `(?<![\\p{L}\\p{N}])${escapeRegExp(term)}(?![\\p{L}\\p{N}])`,
186
+ "giu"
187
+ );
188
+ out = out.replace(re, to);
189
+ }
190
+ return out;
191
+ }
192
+ function parseMarkdownToText(markdown, options) {
193
+ if (!markdown || typeof markdown !== "string") return "";
194
+ const numberToWords = (num) => {
195
+ const ones = [
196
+ "zero",
197
+ "one",
198
+ "two",
199
+ "three",
200
+ "four",
201
+ "five",
202
+ "six",
203
+ "seven",
204
+ "eight",
205
+ "nine",
206
+ "ten",
207
+ "eleven",
208
+ "twelve",
209
+ "thirteen",
210
+ "fourteen",
211
+ "fifteen",
212
+ "sixteen",
213
+ "seventeen",
214
+ "eighteen",
215
+ "nineteen"
216
+ ];
217
+ const tens = [
218
+ "",
219
+ "",
220
+ "twenty",
221
+ "thirty",
222
+ "forty",
223
+ "fifty",
224
+ "sixty",
225
+ "seventy",
226
+ "eighty",
227
+ "ninety"
228
+ ];
229
+ const numInt = parseInt(num, 10);
230
+ if (numInt < 20) return ones[numInt] ?? num;
231
+ if (numInt < 100) {
232
+ const t = Math.floor(numInt / 10);
233
+ const o = numInt % 10;
234
+ return o === 0 ? tens[t] ?? num : `${tens[t] ?? ""}-${ones[o] ?? ""}`;
235
+ }
236
+ if (numInt < 1e3) {
237
+ const h = Math.floor(numInt / 100);
238
+ const remainder = numInt % 100;
239
+ return remainder === 0 ? `${ones[h]} hundred` : `${ones[h]} hundred ${numberToWords(String(remainder))}`;
240
+ }
241
+ return String(numInt);
242
+ };
243
+ const emojiMap = {
244
+ "\u{1F60A}": "smiling face",
245
+ "\u{1F602}": "laughing face",
246
+ "\u2764\uFE0F": "heart",
247
+ "\u{1F44D}": "thumbs up",
248
+ "\u{1F44E}": "thumbs down",
249
+ "\u{1F525}": "fire",
250
+ "\u2B50": "star",
251
+ "\u2705": "check mark",
252
+ "\u274C": "cross mark",
253
+ "\u26A0\uFE0F": "warning",
254
+ "\u{1F680}": "rocket",
255
+ "\u{1F4A1}": "light bulb",
256
+ "\u{1F3AF}": "target",
257
+ "\u{1F4F1}": "mobile phone",
258
+ "\u{1F4BB}": "laptop",
259
+ "\u{1F31F}": "glowing star",
260
+ "\u{1F517}": "link",
261
+ "\u{1F4E7}": "email",
262
+ "\u{1F4C4}": "document",
263
+ "\u{1F4CA}": "chart",
264
+ "\u{1F6E0}\uFE0F": "tools",
265
+ "\u2699\uFE0F": "gear",
266
+ "\u{1F527}": "wrench",
267
+ "\u{1F4C8}": "chart increasing",
268
+ "\u{1F4C9}": "chart decreasing",
269
+ "\u{1F389}": "party popper",
270
+ "\u{1F4AA}": "flexed biceps",
271
+ "\u{1F914}": "thinking face",
272
+ "\u{1F4AD}": "thought bubble",
273
+ "\u{1F440}": "eyes",
274
+ "\u{1F44B}": "waving hand",
275
+ "\u2728": "sparkles",
276
+ "\u{1F50D}": "magnifying mx-glass",
277
+ "\u{1F4DD}": "memo",
278
+ "\u{1F4CB}": "clipboard",
279
+ "\u{1F4C5}": "calendar",
280
+ "\u23F0": "alarm clock",
281
+ "\u{1F512}": "locked",
282
+ "\u{1F513}": "unlocked",
283
+ "\u{1F3C6}": "trophy",
284
+ "\u{1F396}\uFE0F": "military medal",
285
+ "\u{1F397}\uFE0F": "reminder ribbon",
286
+ "\u{1F3C5}": "sports medal",
287
+ "\u{1F947}": "first place medal",
288
+ "\u{1F948}": "second place medal",
289
+ "\u{1F949}": "third place medal"
290
+ };
291
+ const measurementUnits = {
292
+ // Weight
293
+ lbs: "pound",
294
+ // Changed from 'pounds'
295
+ lb: "pound",
296
+ oz: "ounce",
297
+ // Changed from 'ounces'
298
+ kg: "kilogram",
299
+ // Changed from 'kilograms'
300
+ gm: "gram",
301
+ // Changed from 'grams'
302
+ mg: "milligram",
303
+ // Changed from 'milligrams'
304
+ // Distance/Length
305
+ km: "kilometer",
306
+ // Changed from 'kilometers'
307
+ cm: "centimeter",
308
+ // Changed from 'centimeters'
309
+ mm: "millimeter",
310
+ // Changed from 'millimeters'
311
+ ft: "foot",
312
+ // Changed from 'feet' (Irregular plural)
313
+ mi: "mile",
314
+ // Changed from 'miles'
315
+ // Speed
316
+ mph: "mile per hour",
317
+ // Changed from 'miles per hour'
318
+ kph: "kilometer per hour",
319
+ // Changed from 'kilometers per hour'
320
+ kmh: "kilometer per hour",
321
+ // Changed from 'kilometers per hour'
322
+ // Volume
323
+ ml: "milliliter",
324
+ // Changed from 'milliliters'
325
+ mL: "milliliter",
326
+ // Changed from 'milliliters'
327
+ gal: "gallon",
328
+ // Changed from 'gallons'
329
+ qt: "quart",
330
+ // Changed from 'quarts'
331
+ tbsp: "tablespoon",
332
+ // Changed from 'tablespoons'
333
+ tsp: "teaspoon",
334
+ // Changed from 'teaspoons'
335
+ // Area
336
+ sqft: "square foot",
337
+ // Changed from 'square feet' (Irregular plural)
338
+ sqm: "square meter",
339
+ // Changed from 'square meters'
340
+ // Time
341
+ sec: "second",
342
+ // Changed from 'seconds'
343
+ min: "minute",
344
+ // Changed from 'minutes'
345
+ hr: "hour",
346
+ // Changed from 'hours'
347
+ hrs: "hour",
348
+ // Changed from 'hours'
349
+ ms: "millisecond",
350
+ // Changed from 'milliseconds'
351
+ // Other common units
352
+ psi: "pound per square inch",
353
+ // Changed from 'pounds per square inch'
354
+ rpm: "revolution per minute",
355
+ // Changed from 'revolutions per minute'
356
+ bpm: "beat per minute"
357
+ // Changed from 'beats per minute'
358
+ };
359
+ const stripReasoningTags = (input) => {
360
+ return input.replace(/<(thinking|reasoning|think|reason)\b[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<\/?(?:thinking|reasoning|think|reason)\b[^>]*>/gi, "");
361
+ };
362
+ const stripLeadingMarkdown = (input) => {
363
+ let text = input.replace(/^\uFEFF/, "").replace(/^[ \t]*\r?\n/, "");
364
+ text = text.replace(
365
+ /^[ \t]*(?:#{1,6}[ \t]+|>[ \t]*|[-*+][ \t]+|\d+\.[ \t]+|-[ \t]*\[[ xX]\][ \t]+)/,
366
+ ""
367
+ );
368
+ return text;
369
+ };
370
+ const source = normalizeSpeechBlanks(
371
+ options?.pronunciations?.length ? applyPronunciations(markdown, options.pronunciations) : markdown
372
+ );
373
+ let result = speakTables((0, import_source.unwrapCodeSpans)((0, import_source.replaceFences)(stripLeadingMarkdown(stripReasoningTags(source)), ({ lang }) => {
374
+ const language = lang.toLowerCase();
375
+ if (language === "mermaid") return "Please see the diagram provided.";
376
+ return SPOKEN_CODE_LANGUAGES.has(language) ? `Please see the ${language} code provided.` : "Please see the code provided.";
377
+ }))).split("\n").map((line) => (0, import_source.isPipeLedRow)(line) && line.trimEnd().endsWith("|") ? "" : line).join("\n").replace(
378
+ /\b(\d+)\s*[-–—]\s*(\d+)\b/g,
379
+ (_match, a, b) => `${numberToWords(a)} to ${numberToWords(b)}`
380
+ ).replace(
381
+ /^#{1,6}\s+(.+)$/gm,
382
+ (_m, t) => /[.!?:]$/.test(t.trim()) ? t : `${t.trim()}.`
383
+ ).replace(/\/\/\s*/g, "").replace(/--\s*/g, "").replace(/#\s+/g, "").replace(/;\s*/g, "").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/~~(.*?)~~/g, "$1").replace(/==(.*?)==/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
384
+ if (url.startsWith("mailto:")) {
385
+ return `${text}. Email address provided.`;
386
+ }
387
+ const fileExtensions = [
388
+ ".pdf",
389
+ ".doc",
390
+ ".docx",
391
+ ".xls",
392
+ ".xlsx",
393
+ ".ppt",
394
+ ".pptx",
395
+ ".txt",
396
+ ".md",
397
+ ".json",
398
+ ".xml",
399
+ ".csv",
400
+ ".zip"
401
+ ];
402
+ const hasFileExtension = fileExtensions.some(
403
+ (ext) => url.toLowerCase().includes(ext)
404
+ );
405
+ if (hasFileExtension) {
406
+ return `${text}. Document link provided.`;
407
+ }
408
+ return `${text}. Link provided.`;
409
+ }).replace(
410
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
411
+ ($0, alt) => alt ? `${alt}. Image provided.` : "Image provided."
412
+ ).replace(/^>\s*(.+)$/gm, "Quote: $1").replace(/^-\s*\[x\]\s+(.+)$/gm, "Completed task: $1").replace(/^-\s*\[\s*\]\s+(.+)$/gm, "Pending task: $1").replace(/^([-*+])\s+(.+)$/gm, "$2").replace(
413
+ /^(\d+)\.\s+(.+)$/gm,
414
+ (_match, num, content) => `Number ${numberToWords(num)}: ${content}`
415
+ ).replace(/\[\^(\d+)\]/g, "Reference $1").replace(/^\[\d+\]:\s*(.+)$/gm, "Reference: $1").replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "").replace(/\$([^$]+)\$/g, "Mathematical expression: $1").replace(/\$\$([^$]+)\$\$/g, "Mathematical formula: $1").replace(
416
+ /(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
417
+ "Phone number provided."
418
+ ).replace(
419
+ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
420
+ "Email address: $1"
421
+ ).replace(/\$(\d+(?:,\d{3})*(?:\.\d{2})?)/g, "$1 dollars").replace(/€(\d+(?:[.,]\d{3})*(?:[.,]\d{2})?)/g, "$1 euros").replace(/£(\d+(?:,\d{3})*(?:\.\d{2})?)/g, "$1 pounds").replace(/¥(\d+(?:,\d{3})*(?:\.\d{2})?)/g, "$1 yen").replace(
422
+ /(\d{4})[-/](\d{1,2})[-/](\d{1,2})/g,
423
+ (_match, year, month, day) => {
424
+ const monthNames = [
425
+ "January",
426
+ "February",
427
+ "March",
428
+ "April",
429
+ "May",
430
+ "June",
431
+ "July",
432
+ "August",
433
+ "September",
434
+ "October",
435
+ "November",
436
+ "December"
437
+ ];
438
+ const monthName = monthNames[parseInt(month) - 1] || month;
439
+ return `${monthName} ${day}, ${year}`;
440
+ }
441
+ ).replace(
442
+ /(\d{1,2}):(\d{2})\s*(AM|PM|am|pm)?/g,
443
+ (_match, hour, minute, period) => {
444
+ const hourNum = parseInt(hour);
445
+ const periodText = period ? period.toUpperCase() === "AM" ? "A.M." : "P.M." : "";
446
+ return `${hourNum} ${minute} ${periodText}`.trim();
447
+ }
448
+ ).replace(
449
+ /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,
450
+ (emoji) => {
451
+ return emojiMap[emoji] || "emoji";
452
+ }
453
+ ).replace(COMMON_ABBREVIATION_PATTERN, spokenFormForAbbreviation).replace(
454
+ /(\d+(?:\.\d+)?)\s*(lbs|lb|oz|kg|gm|mg|km|cm|mm|ft|mi|mph|kph|kmh|ml|mL|gal|qt|tbsp|tsp|sqft|sqm|sec|min|hr|hrs|ms|psi|rpm|bpm)\b/gi,
455
+ (match, number, unit) => {
456
+ const unitLower = unit.toLowerCase();
457
+ const unitText = measurementUnits[unitLower] || measurementUnits[unit] || unit;
458
+ return `${number} ${unitText}`;
459
+ }
460
+ ).replace(
461
+ /\b(lbs|lb|oz|kg|gm|mg|km|cm|mm|mph|kph|kmh|ml|mL|gal|qt|tbsp|tsp|sqft|sqm|psi|rpm|bpm)(?=\s|$|[.,;!?])/gi,
462
+ (match) => {
463
+ const unitLower = match.toLowerCase();
464
+ return measurementUnits[unitLower] || measurementUnits[match] || match;
465
+ }
466
+ ).replace(/©/g, "copyright").replace(/®/g, "registered trademark").replace(/™/g, "trademark").replace(/°/g, "degrees").replace(/±/g, "plus or minus").replace(/≈/g, "approximately").replace(/≠/g, "not equal to").replace(/≤/g, "less than or equal to").replace(/≥/g, "greater than or equal to").replace(/=/g, " equals ").replace(/→/g, "arrow").replace(/←/g, "left arrow").replace(/↑/g, "up arrow").replace(/↓/g, "down arrow").replace(/<\/?[^>]+(>|$)/g, "").replace(/([^\s.!?,;:\-])(\n)/g, "$1.\n").replace(/\s+/g, " ").trim();
467
+ return result;
468
+ }
469
+ var import_source, SPOKEN_CODE_LANGUAGES, COMMON_ABBREVIATION_EXPANSIONS, COMMON_ABBREVIATION_BY_UPPERCASE, COMMON_ABBREVIATION_PATTERN, SPOKEN_AS_WORD, CASE_SENSITIVE_WORD_ACRONYMS, SPEECH_BLANK_WORD;
470
+ var init_speech_text = __esm({
471
+ "src/voices/speech-text.ts"() {
472
+ "use strict";
473
+ import_source = require("@ai-matrx/content-ir/source");
474
+ SPOKEN_CODE_LANGUAGES = /* @__PURE__ */ new Set([
475
+ "javascript",
476
+ "js",
477
+ "typescript",
478
+ "ts",
479
+ "python",
480
+ "py",
481
+ "java",
482
+ "csharp",
483
+ "cs",
484
+ "cpp",
485
+ "c++",
486
+ "c",
487
+ "go",
488
+ "rust",
489
+ "php",
490
+ "ruby",
491
+ "swift",
492
+ "kotlin",
493
+ "scala",
494
+ "sql",
495
+ "bash",
496
+ "shell",
497
+ "powershell",
498
+ "yaml",
499
+ "yml",
500
+ "json",
501
+ "xml",
502
+ "html",
503
+ "css",
504
+ "markdown",
505
+ "md"
506
+ ]);
507
+ COMMON_ABBREVIATION_EXPANSIONS = {
508
+ AI: "Artificial Intelligence",
509
+ API: "Application Programming Interface",
510
+ HTTP: "Hypertext Transfer Protocol",
511
+ HTTPS: "Hypertext Transfer Protocol Secure",
512
+ URL: "Uniform Resource Locator",
513
+ URI: "Uniform Resource Identifier",
514
+ JSON: "JavaScript Object Notation",
515
+ XML: "eXtensible Markup Language",
516
+ CSS: "Cascading Style Sheets",
517
+ HTML: "Hypertext Markup Language",
518
+ JS: "JavaScript",
519
+ TS: "TypeScript",
520
+ SQL: "Structured Query Language",
521
+ DB: "Database",
522
+ UI: "User Interface",
523
+ UX: "User Experience",
524
+ SEO: "Search Engine Optimization",
525
+ SDK: "Software Development Kit",
526
+ CLI: "Command Line Interface",
527
+ IDE: "Integrated Development Environment",
528
+ JWT: "JSON Web Token",
529
+ OAuth: "Open Authorization",
530
+ REST: "Representational State Transfer",
531
+ CRUD: "Create Read Update Delete",
532
+ MVC: "Model View Controller",
533
+ SPA: "Single Page Application",
534
+ SSR: "Server Side Rendering",
535
+ CSR: "Client Side Rendering",
536
+ PWA: "Progressive Web App",
537
+ DOM: "Document Object Model",
538
+ BOM: "Browser Object Model",
539
+ CDN: "Content Delivery Network",
540
+ CMS: "Content Management System",
541
+ ERP: "Enterprise Resource Planning",
542
+ CRM: "Customer Relationship Management",
543
+ SaaS: "Software as a Service",
544
+ PaaS: "Platform as a Service",
545
+ IaaS: "Infrastructure as a Service",
546
+ VPN: "Virtual Private Network",
547
+ LAN: "Local Area Network",
548
+ WAN: "Wide Area Network",
549
+ TCP: "Transmission Control Protocol",
550
+ UDP: "User Datagram Protocol",
551
+ IP: "Internet Protocol",
552
+ DNS: "Domain Name System",
553
+ FTP: "File Transfer Protocol",
554
+ SMTP: "Simple Mail Transfer Protocol",
555
+ POP3: "Post Office Protocol version 3",
556
+ IMAP: "Internet Message Access Protocol"
557
+ };
558
+ COMMON_ABBREVIATION_BY_UPPERCASE = Object.fromEntries(
559
+ Object.keys(COMMON_ABBREVIATION_EXPANSIONS).map(
560
+ (abbreviation) => [abbreviation.toUpperCase(), abbreviation]
561
+ )
562
+ );
563
+ COMMON_ABBREVIATION_PATTERN = new RegExp(
564
+ `\\b(${Object.keys(COMMON_ABBREVIATION_EXPANSIONS).sort((a, b) => b.length - a.length).map(escapeRegExp).join("|")})(['\u2019]s|s)?\\b`,
565
+ "gi"
566
+ );
567
+ SPOKEN_AS_WORD = /* @__PURE__ */ new Set([
568
+ "JSON",
569
+ "OAuth",
570
+ "REST",
571
+ "CRUD",
572
+ "DOM",
573
+ "BOM",
574
+ "SaaS",
575
+ "PaaS",
576
+ "IaaS",
577
+ "LAN",
578
+ "WAN"
579
+ ]);
580
+ CASE_SENSITIVE_WORD_ACRONYMS = /* @__PURE__ */ new Set([
581
+ "REST",
582
+ "CRUD",
583
+ "SPA",
584
+ "DOM",
585
+ "BOM",
586
+ "LAN",
587
+ "WAN"
588
+ ]);
589
+ SPEECH_BLANK_WORD = "blank";
590
+ }
591
+ });
592
+
593
+ // src/voices/tts-config.ts
594
+ function resolveVoiceId(userVoiceId, purpose) {
595
+ const v = (userVoiceId ?? "").trim();
596
+ if (v && v !== LEGACY_DEFAULT_VOICE_ID) return v;
597
+ return purpose === "reading" ? READING_VOICE_ID : ASSISTANT_VOICE_ID;
598
+ }
599
+ function resolveSpeed(userSpeed) {
600
+ if (typeof userSpeed === "number" && userSpeed >= TTS_SPEED_MIN && userSpeed <= TTS_SPEED_MAX) {
601
+ return userSpeed;
602
+ }
603
+ return TTS_DEFAULT_SPEED;
604
+ }
605
+ var TTS_MODEL_ID, CARTESIA_API_VERSION, READING_VOICE_ID, ASSISTANT_VOICE_ID, LEGACY_DEFAULT_VOICE_ID, TTS_SPEED_MIN, TTS_SPEED_MAX, TTS_DEFAULT_SPEED, TTS_DEFAULT_VOLUME, TTS_PLAYBACK_BUFFER_SEC;
606
+ var init_tts_config = __esm({
607
+ "src/voices/tts-config.ts"() {
608
+ "use strict";
609
+ TTS_MODEL_ID = "sonic-3.5";
610
+ CARTESIA_API_VERSION = "2026-08-14";
611
+ READING_VOICE_ID = "db6b0ed5-d5d3-463d-ae85-518a07d3c2b4";
612
+ ASSISTANT_VOICE_ID = "47c38ca4-5f35-497b-b1a3-415245fb35e1";
613
+ LEGACY_DEFAULT_VOICE_ID = "156fb8d2-335b-4950-9cb3-a2d33befec77";
614
+ TTS_SPEED_MIN = 0.6;
615
+ TTS_SPEED_MAX = 1.5;
616
+ TTS_DEFAULT_SPEED = 1.2;
617
+ TTS_DEFAULT_VOLUME = 1;
618
+ TTS_PLAYBACK_BUFFER_SEC = 0.7;
619
+ }
620
+ });
621
+
622
+ // src/speech/cartesia-socket.ts
623
+ function isCartesiaAuthError(error) {
624
+ const message = error instanceof Error ? error.message : String(error ?? "");
625
+ return /\b(401|403)\b|unauthorized|forbidden|invalid.*token|expired.*token|authentication/i.test(message);
626
+ }
627
+ async function reportFailure(failure) {
628
+ const report = speechPorts().reportProviderFailure;
629
+ if (!report) return null;
630
+ try {
631
+ return await report(failure);
632
+ } catch {
633
+ return null;
634
+ }
635
+ }
636
+ function decodeBase64(value) {
637
+ const decoded = atob(value);
638
+ const bytes = new Uint8Array(decoded.length);
639
+ for (let index = 0; index < decoded.length; index += 1) {
640
+ bytes[index] = decoded.charCodeAt(index);
641
+ }
642
+ return bytes;
643
+ }
644
+ function toNativeVoice(voice) {
645
+ return { id: voice.id };
646
+ }
647
+ async function buildConnectedSocket(token, options) {
648
+ const client = new import_client.Cartesia({ token });
649
+ const native = await client.tts.websocket({
650
+ headers: { "Cartesia-Version": CARTESIA_API_VERSION }
651
+ });
652
+ await native.connect();
653
+ const closeListeners = /* @__PURE__ */ new Set();
654
+ native.socket.addEventListener("close", () => {
655
+ for (const listener of closeListeners) listener();
656
+ });
657
+ return {
658
+ ws: new CartesiaV4Socket(native, options),
659
+ ctx: {
660
+ on: (_event, listener) => {
661
+ closeListeners.add(listener);
662
+ }
663
+ }
664
+ };
665
+ }
666
+ async function connectCartesiaTts(options) {
667
+ const requested = { ...DEFAULTS, ...options };
668
+ if (requested.container !== "raw") {
669
+ throw new Error("Cartesia WebSocket playback requires the raw audio container.");
670
+ }
671
+ if (requested.encoding !== "pcm_f32le") {
672
+ throw new Error("Cartesia browser playback requires pcm_f32le audio.");
673
+ }
674
+ const sampleRates = [8e3, 16e3, 22050, 24e3, 44100, 48e3];
675
+ if (!sampleRates.includes(requested.sampleRate)) {
676
+ throw new Error(`Unsupported Cartesia sample rate: ${requested.sampleRate}`);
677
+ }
678
+ const resolved = requested;
679
+ const getToken = requirePort("getCartesiaToken");
680
+ const token = await getToken();
681
+ try {
682
+ try {
683
+ return await buildConnectedSocket(token, resolved);
684
+ } catch (error) {
685
+ if (!isCartesiaAuthError(error)) throw error;
686
+ speechPorts().invalidateCartesiaToken?.(token);
687
+ const refreshed = await getToken({ forceRefresh: true }).catch(
688
+ (mintError) => {
689
+ throw new BrokerMintFailure(mintError);
690
+ }
691
+ );
692
+ return await buildConnectedSocket(refreshed, resolved);
693
+ }
694
+ } catch (error) {
695
+ if (error instanceof BrokerMintFailure) throw error.cause;
696
+ throw await reportCartesiaConnectFailure(error);
697
+ }
698
+ }
699
+ async function reportCartesiaConnectFailure(error) {
700
+ const raw = error instanceof Error ? error.message : typeof error === "object" && error && "message" in error ? String(error.message) : "";
701
+ const status = /\b([45]\d\d)\b/.exec(raw)?.[1];
702
+ const failure = {
703
+ provider: "cartesia",
704
+ status_code: status ? Number(status) : null,
705
+ message: raw || "Cartesia WebSocket connection failed"
706
+ };
707
+ const verdict = await reportFailure(failure);
708
+ return new ProviderSessionError(
709
+ failure,
710
+ verdict,
711
+ raw || "Could not connect to the speech service.",
712
+ { cause: error }
713
+ );
714
+ }
715
+ var import_client, ProviderSessionError, CartesiaAudioSource, DEFAULTS, CartesiaV4Socket, BrokerMintFailure;
716
+ var init_cartesia_socket = __esm({
717
+ "src/speech/cartesia-socket.ts"() {
718
+ "use strict";
719
+ import_client = require("@cartesia/cartesia-js/client");
720
+ init_tts_config();
721
+ init_configure();
722
+ ProviderSessionError = class extends Error {
723
+ name = "ProviderSessionError";
724
+ provider;
725
+ retryable;
726
+ errorType;
727
+ reported;
728
+ constructor(failure, verdict, fallbackMessage, options) {
729
+ super(verdict?.user_message || fallbackMessage, options);
730
+ this.provider = failure.provider;
731
+ this.retryable = verdict ? verdict.retryable : true;
732
+ this.errorType = verdict?.error_type ?? failure.error_type ?? null;
733
+ this.reported = verdict !== null;
734
+ }
735
+ };
736
+ CartesiaAudioSource = class {
737
+ sampleRate;
738
+ #chunks = [];
739
+ #done = false;
740
+ #waiter = null;
741
+ constructor(sampleRate) {
742
+ this.sampleRate = sampleRate;
743
+ }
744
+ durationToSampleCount(durationSecs) {
745
+ return Math.ceil(durationSecs * this.sampleRate);
746
+ }
747
+ push(bytes) {
748
+ const sampleCount = Math.floor(bytes.byteLength / Float32Array.BYTES_PER_ELEMENT);
749
+ const samples = new Float32Array(sampleCount);
750
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
751
+ for (let index = 0; index < sampleCount; index += 1) {
752
+ samples[index] = view.getFloat32(index * Float32Array.BYTES_PER_ELEMENT, true);
753
+ }
754
+ this.#chunks.push(samples);
755
+ this.#wake();
756
+ }
757
+ finish() {
758
+ this.#done = true;
759
+ this.#wake();
760
+ }
761
+ async read(destination) {
762
+ let written = 0;
763
+ while (written < destination.length) {
764
+ while (this.#chunks.length === 0 && !this.#done) {
765
+ await new Promise((resolve) => {
766
+ this.#waiter = resolve;
767
+ });
768
+ }
769
+ if (this.#chunks.length === 0) break;
770
+ const chunk = this.#chunks[0];
771
+ const count = Math.min(chunk.length, destination.length - written);
772
+ destination.set(chunk.subarray(0, count), written);
773
+ written += count;
774
+ if (count === chunk.length) {
775
+ this.#chunks.shift();
776
+ } else {
777
+ this.#chunks[0] = chunk.subarray(count);
778
+ }
779
+ }
780
+ return written;
781
+ }
782
+ #wake() {
783
+ this.#waiter?.();
784
+ this.#waiter = null;
785
+ }
786
+ };
787
+ DEFAULTS = {
788
+ container: "raw",
789
+ encoding: "pcm_f32le",
790
+ sampleRate: 44100
791
+ };
792
+ CartesiaV4Socket = class {
793
+ #native;
794
+ #options;
795
+ #sources = /* @__PURE__ */ new Map();
796
+ #listeners = /* @__PURE__ */ new Map();
797
+ /** Set when the provider refused this account and reconnecting cannot help. */
798
+ #refused = null;
799
+ constructor(native, options) {
800
+ this.#native = native;
801
+ this.#options = options;
802
+ native.on("error", () => {
803
+ });
804
+ native.on("event", (event) => {
805
+ const contextId = "context_id" in event ? event.context_id : void 0;
806
+ if (event.type === "error") {
807
+ const listeners3 = contextId ? this.#listeners.get(contextId) : void 0;
808
+ if (contextId) {
809
+ this.#sources.get(contextId)?.finish();
810
+ this.#sources.delete(contextId);
811
+ this.#listeners.delete(contextId);
812
+ } else {
813
+ for (const source2 of this.#sources.values()) source2.finish();
814
+ this.#sources.clear();
815
+ this.#listeners.clear();
816
+ }
817
+ void this.#reportProviderError(event, listeners3);
818
+ return;
819
+ }
820
+ if (!contextId) return;
821
+ const source = this.#sources.get(contextId);
822
+ if (event.type === "chunk" && source) {
823
+ source.push(event.audio ?? decodeBase64(event.data));
824
+ } else if (event.type === "done") {
825
+ source?.finish();
826
+ }
827
+ const serialized = JSON.stringify(event);
828
+ for (const listener of this.#listeners.get(contextId) ?? []) {
829
+ listener(serialized);
830
+ }
831
+ if (event.type === "done") {
832
+ this.#sources.delete(contextId);
833
+ this.#listeners.delete(contextId);
834
+ }
835
+ });
836
+ }
837
+ /**
838
+ * Browser-held session: the server never sees this provider error unless we
839
+ * report it. Listeners get the event with `message` replaced by the server's
840
+ * sentence, and the person sees it as a toast.
841
+ */
842
+ async #reportProviderError(event, listeners3) {
843
+ const failure = {
844
+ provider: "cartesia",
845
+ status_code: event.status_code ?? null,
846
+ error_type: event.error_code ?? event.title ?? null,
847
+ message: event.message || event.title || "Cartesia TTS error"
848
+ };
849
+ const verdict = await reportFailure(failure);
850
+ const error = new ProviderSessionError(failure, verdict, failure.message);
851
+ if (!error.retryable) this.#refused = error;
852
+ const delivered = JSON.stringify({
853
+ ...event,
854
+ message: error.message,
855
+ retryable: error.retryable
856
+ });
857
+ for (const listener of listeners3 ?? []) listener(delivered);
858
+ speechPorts().notifyError?.("Speech stopped", error.message);
859
+ }
860
+ async send(request) {
861
+ if (this.#refused) throw this.#refused;
862
+ const contextId = request.contextId ?? crypto.randomUUID();
863
+ let source = this.#sources.get(contextId);
864
+ if (!source) {
865
+ source = new CartesiaAudioSource(this.#options.sampleRate);
866
+ this.#sources.set(contextId, source);
867
+ }
868
+ await this.#native.send({
869
+ context_id: contextId,
870
+ model_id: request.modelId,
871
+ transcript: request.transcript,
872
+ voice: toNativeVoice(request.voice),
873
+ output_format: {
874
+ container: this.#options.container,
875
+ encoding: this.#options.encoding,
876
+ sample_rate: this.#options.sampleRate
877
+ },
878
+ language: request.language,
879
+ continue: request.continue,
880
+ add_timestamps: request.addTimestamps,
881
+ add_phoneme_timestamps: request.addPhonemeTimestamps,
882
+ max_buffer_delay_ms: request.maxBufferDelayMs,
883
+ generation_config: request.generationConfig
884
+ });
885
+ return {
886
+ source,
887
+ on: (_event, listener) => {
888
+ const listeners3 = this.#listeners.get(contextId) ?? /* @__PURE__ */ new Set();
889
+ listeners3.add(listener);
890
+ this.#listeners.set(contextId, listeners3);
891
+ }
892
+ };
893
+ }
894
+ continue(request) {
895
+ return this.send(request);
896
+ }
897
+ disconnect() {
898
+ for (const source of this.#sources.values()) source.finish();
899
+ this.#sources.clear();
900
+ this.#listeners.clear();
901
+ this.#native.close();
902
+ }
903
+ };
904
+ BrokerMintFailure = class extends Error {
905
+ constructor(cause) {
906
+ super("broker mint failed");
907
+ this.cause = cause;
908
+ }
909
+ cause;
910
+ };
911
+ }
912
+ });
913
+
914
+ // src/speech/output-sink.ts
915
+ function mediaElementSinkSupported() {
916
+ return typeof HTMLMediaElement !== "undefined" && "setSinkId" in HTMLMediaElement.prototype;
917
+ }
918
+ function audioContextSinkSupported() {
919
+ if (typeof window === "undefined") return false;
920
+ const Ctor = window.AudioContext || window.webkitAudioContext;
921
+ return !!Ctor && "setSinkId" in Ctor.prototype;
922
+ }
923
+ function outputSelectionSupported() {
924
+ return mediaElementSinkSupported() || audioContextSinkSupported();
925
+ }
926
+ function getPreferredOutputDeviceId() {
927
+ return state.deviceId;
928
+ }
929
+ function setPreferredOutputDeviceId(deviceId) {
930
+ if (state.deviceId === deviceId) return;
931
+ state.deviceId = deviceId;
932
+ for (const l of state.listeners) {
933
+ try {
934
+ l(deviceId);
935
+ } catch {
936
+ }
937
+ }
938
+ }
939
+ function subscribeOutputDevice(listener) {
940
+ state.listeners.add(listener);
941
+ return () => {
942
+ state.listeners.delete(listener);
943
+ };
944
+ }
945
+ async function applySinkToMediaElement(el) {
946
+ if (!el) return;
947
+ if (!mediaElementSinkSupported()) return;
948
+ const target = state.deviceId;
949
+ const elWithSink = el;
950
+ if (elWithSink.sinkId === target) return;
951
+ try {
952
+ await elWithSink.setSinkId?.(target);
953
+ } catch (err) {
954
+ console.error(
955
+ "[audioOutputSink] setSinkId on media element failed \u2014 falling back to the system default device. Requested device may be unavailable.",
956
+ { deviceId: target, error: err }
957
+ );
958
+ }
959
+ }
960
+ var state;
961
+ var init_output_sink = __esm({
962
+ "src/speech/output-sink.ts"() {
963
+ "use strict";
964
+ state = {
965
+ deviceId: "",
966
+ listeners: /* @__PURE__ */ new Set()
967
+ };
968
+ }
969
+ });
970
+
971
+ // src/speech/sink-aware-player.ts
972
+ function defaultCreateContext(sampleRate) {
973
+ return new AudioContext({ sampleRate });
974
+ }
975
+ var SinkAwarePlayer;
976
+ var init_sink_aware_player = __esm({
977
+ "src/speech/sink-aware-player.ts"() {
978
+ "use strict";
979
+ init_output_sink();
980
+ init_unlock();
981
+ SinkAwarePlayer = class {
982
+ #context = null;
983
+ #startNextPlaybackAt = 0;
984
+ #bufferDuration;
985
+ #createContext;
986
+ #unsubscribeSink = null;
987
+ /**
988
+ * True when `#context` is the app's SHARED gesture-unlocked context
989
+ * (features/audio/unlock.ts) rather than a private per-utterance one.
990
+ * Shared mode exists for iOS/WebKit, where a context created outside a
991
+ * user gesture starts `suspended` forever — our audio starts from
992
+ * websocket callbacks, so per-utterance contexts were silent on iPhone.
993
+ * In shared mode the utterance plays through a per-utterance GainNode and
994
+ * `stop()` tears that down instead of closing the context.
995
+ */
996
+ #sharedMode = false;
997
+ #utteranceGain = null;
998
+ #liveSources = /* @__PURE__ */ new Set();
999
+ #stopped = false;
1000
+ constructor({ bufferDuration, createContext }) {
1001
+ this.#bufferDuration = bufferDuration;
1002
+ this.#createContext = createContext ?? defaultCreateContext;
1003
+ }
1004
+ /**
1005
+ * Play audio from a source. Resolves when the audio has finished playing.
1006
+ *
1007
+ * Prefers the app's shared gesture-unlocked context (required for sound on
1008
+ * iOS/WebKit); otherwise builds a fresh AudioContext per call (matching the
1009
+ * SDK). Either way the output is routed to the user's preferred device and
1010
+ * re-routed live on device change. Web Audio resamples per-buffer, so the
1011
+ * shared context's hardware rate plays 44.1k sources correctly.
1012
+ */
1013
+ async play(source) {
1014
+ this.#startNextPlaybackAt = 0;
1015
+ this.#stopped = false;
1016
+ this.#detachSinkSubscription();
1017
+ const shared = this.#createContext === defaultCreateContext ? getUnlockedAudioContext() : null;
1018
+ const context = shared ?? this.#createContext(source.sampleRate);
1019
+ this.#sharedMode = shared !== null;
1020
+ this.#context = context;
1021
+ if (this.#sharedMode) {
1022
+ const ctx = shared;
1023
+ this.#utteranceGain = ctx.createGain();
1024
+ this.#utteranceGain.connect(ctx.destination);
1025
+ } else {
1026
+ this.#utteranceGain = null;
1027
+ }
1028
+ this.#attachSinkRouting(context);
1029
+ if (context.state !== "running") {
1030
+ try {
1031
+ await context.resume();
1032
+ } catch {
1033
+ }
1034
+ if (context.state !== "running") {
1035
+ throw new Error(
1036
+ "The browser blocked audio output \u2014 tap the play button to start sound."
1037
+ );
1038
+ }
1039
+ }
1040
+ try {
1041
+ const buffer = new Float32Array(
1042
+ source.durationToSampleCount(this.#bufferDuration)
1043
+ );
1044
+ const plays = [];
1045
+ while (true) {
1046
+ const read = await source.read(buffer);
1047
+ const playableAudio = buffer.subarray(0, read);
1048
+ plays.push(this.#playBuffer(playableAudio, source.sampleRate));
1049
+ if (read < buffer.length) break;
1050
+ }
1051
+ await Promise.all(plays);
1052
+ } finally {
1053
+ this.#detachSinkSubscription();
1054
+ }
1055
+ }
1056
+ /** Suspend playback. Throws before the first play (contract parity). */
1057
+ async pause() {
1058
+ const context = this.#requireContext();
1059
+ if (this.#sharedMode && this.#stopped) return;
1060
+ await context.suspend();
1061
+ }
1062
+ /** Resume suspended playback. Throws before the first play. */
1063
+ async resume() {
1064
+ const context = this.#requireContext();
1065
+ if (this.#sharedMode && this.#stopped) return;
1066
+ await context.resume();
1067
+ }
1068
+ /** Pause when running, resume otherwise. Throws before the first play. */
1069
+ async toggle() {
1070
+ if (this.#requireContext().state === "running") {
1071
+ await this.pause();
1072
+ } else {
1073
+ await this.resume();
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Stop playback. Throws before the first play (contract parity); a repeat
1078
+ * stop is a silent no-op.
1079
+ *
1080
+ * Owned-context mode closes the context (the SDK contract). Shared mode
1081
+ * NEVER closes the app-wide unlocked context — it stops this utterance's
1082
+ * scheduled sources and tears down its GainNode, and resumes the context
1083
+ * if this utterance had paused it (a suspended shared context would mute
1084
+ * the NEXT utterance too).
1085
+ */
1086
+ async stop() {
1087
+ const context = this.#requireContext();
1088
+ this.#detachSinkSubscription();
1089
+ if (this.#sharedMode) {
1090
+ if (this.#stopped) return;
1091
+ this.#stopped = true;
1092
+ for (const node of this.#liveSources) {
1093
+ try {
1094
+ node.stop();
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ this.#liveSources.clear();
1099
+ try {
1100
+ this.#utteranceGain?.disconnect();
1101
+ } catch {
1102
+ }
1103
+ this.#utteranceGain = null;
1104
+ if (context.state === "suspended") {
1105
+ await context.resume().catch(() => {
1106
+ });
1107
+ }
1108
+ return;
1109
+ }
1110
+ if (context.state === "closed") return;
1111
+ await context.close();
1112
+ }
1113
+ #requireContext() {
1114
+ if (!this.#context) {
1115
+ throw new Error("AudioContext not initialized.");
1116
+ }
1117
+ return this.#context;
1118
+ }
1119
+ /**
1120
+ * Route `context` to the current preferred output device and keep it
1121
+ * routed while it lives. Feature-detected: on Firefox/Safari (no
1122
+ * `AudioContext.setSinkId`) this is a clean no-op and audio plays on the
1123
+ * system default device.
1124
+ */
1125
+ #attachSinkRouting(context) {
1126
+ if (typeof context.setSinkId !== "function") return;
1127
+ const applySink = (deviceId) => {
1128
+ if (this.#context !== context || context.state === "closed") return;
1129
+ context.setSinkId?.(deviceId).catch((err) => {
1130
+ console.error(
1131
+ "[SinkAwarePlayer] setSinkId failed \u2014 playback continues on the system default device. The requested device may be unavailable.",
1132
+ { deviceId, error: err }
1133
+ );
1134
+ });
1135
+ };
1136
+ const initial = getPreferredOutputDeviceId();
1137
+ if (initial) applySink(initial);
1138
+ this.#unsubscribeSink = subscribeOutputDevice(applySink);
1139
+ }
1140
+ #detachSinkSubscription() {
1141
+ this.#unsubscribeSink?.();
1142
+ this.#unsubscribeSink = null;
1143
+ }
1144
+ async #playBuffer(buf, sampleRate) {
1145
+ const context = this.#requireContext();
1146
+ if (buf.length === 0) return;
1147
+ if (this.#sharedMode && this.#stopped) return;
1148
+ const startAt = this.#startNextPlaybackAt;
1149
+ const duration = buf.length / sampleRate;
1150
+ this.#startNextPlaybackAt = duration + Math.max(context.currentTime, this.#startNextPlaybackAt);
1151
+ const sourceNode = context.createBufferSource();
1152
+ const audioBuffer = context.createBuffer(1, buf.length, sampleRate);
1153
+ audioBuffer.getChannelData(0).set(buf);
1154
+ sourceNode.buffer = audioBuffer;
1155
+ sourceNode.connect(this.#utteranceGain ?? context.destination);
1156
+ sourceNode.start(startAt);
1157
+ if (this.#sharedMode) this.#liveSources.add(sourceNode);
1158
+ await new Promise((resolve) => {
1159
+ sourceNode.onended = () => {
1160
+ this.#liveSources.delete(sourceNode);
1161
+ resolve();
1162
+ };
1163
+ });
1164
+ }
1165
+ };
1166
+ }
1167
+ });
1168
+
1169
+ // src/speech/cartesia-adapter.ts
1170
+ var cartesia_adapter_exports = {};
1171
+ __export(cartesia_adapter_exports, {
1172
+ buildGenerationConfig: () => buildGenerationConfig,
1173
+ cartesiaAdapter: () => cartesiaAdapter
1174
+ });
1175
+ function buildGenerationConfig(opts) {
1176
+ const cfg = {
1177
+ speed: resolveSpeed(opts?.speed),
1178
+ volume: opts?.volume ?? TTS_DEFAULT_VOLUME
1179
+ };
1180
+ if (opts?.emotion) cfg.emotion = opts.emotion.toLowerCase();
1181
+ return cfg;
1182
+ }
1183
+ async function resolveText(item) {
1184
+ let pronunciations = [];
1185
+ try {
1186
+ pronunciations = await speechPorts().resolvePronunciations?.(item) ?? [];
1187
+ } catch {
1188
+ pronunciations = [];
1189
+ }
1190
+ const processMarkdown = item.processMarkdown ?? true;
1191
+ return processMarkdown ? parseMarkdownToText(item.text, pronunciations.length ? { pronunciations } : void 0) : item.text;
1192
+ }
1193
+ var cartesiaAdapter;
1194
+ var init_cartesia_adapter = __esm({
1195
+ "src/speech/cartesia-adapter.ts"() {
1196
+ "use strict";
1197
+ init_speech_text();
1198
+ init_tts_config();
1199
+ init_cartesia_socket();
1200
+ init_configure();
1201
+ init_sink_aware_player();
1202
+ cartesiaAdapter = {
1203
+ provider: "cartesia",
1204
+ async start(item, cb) {
1205
+ cb.onLoading();
1206
+ const overrides = item.cartesia ?? {};
1207
+ const settings = await requirePort("resolveVoiceSettings")();
1208
+ const voice = {
1209
+ voiceId: overrides.voiceId ?? resolveVoiceId(settings.voice, overrides.purpose ?? "assistant"),
1210
+ language: overrides.language ?? settings.language,
1211
+ speed: overrides.speed ?? settings.speed,
1212
+ emotion: settings.emotion
1213
+ };
1214
+ const processed = (await resolveText(item)).trim();
1215
+ if (!processed) {
1216
+ throw new Error("Nothing to speak");
1217
+ }
1218
+ const { ws } = await connectCartesiaTts();
1219
+ let stopped = false;
1220
+ const player = new SinkAwarePlayer({
1221
+ bufferDuration: TTS_PLAYBACK_BUFFER_SEC
1222
+ });
1223
+ const cleanup = () => {
1224
+ try {
1225
+ ws.disconnect();
1226
+ } catch {
1227
+ }
1228
+ };
1229
+ const response = await ws.send({
1230
+ modelId: TTS_MODEL_ID,
1231
+ voice: { mode: "id", id: voice.voiceId },
1232
+ language: voice.language || "en",
1233
+ transcript: processed,
1234
+ generationConfig: buildGenerationConfig({ speed: voice.speed, emotion: voice.emotion ?? null })
1235
+ });
1236
+ if (stopped) {
1237
+ cleanup();
1238
+ throw new Error("Playback cancelled before it started");
1239
+ }
1240
+ cb.onPlaying();
1241
+ player.play(response.source).then(() => {
1242
+ if (stopped) return;
1243
+ cleanup();
1244
+ cb.onEnded();
1245
+ }).catch((err) => {
1246
+ if (stopped) return;
1247
+ cleanup();
1248
+ cb.onError(err instanceof Error ? err.message : "Playback failed");
1249
+ });
1250
+ return {
1251
+ pause: () => player.pause().catch(() => {
1252
+ }),
1253
+ resume: () => player.resume().catch(() => {
1254
+ }),
1255
+ stop: async () => {
1256
+ stopped = true;
1257
+ try {
1258
+ await player.stop();
1259
+ } catch {
1260
+ }
1261
+ cleanup();
1262
+ }
1263
+ };
1264
+ }
1265
+ };
1266
+ }
1267
+ });
1268
+
1269
+ // src/speech.ts
1270
+ var speech_exports = {};
1271
+ __export(speech_exports, {
1272
+ CartesiaAudioSource: () => CartesiaAudioSource,
1273
+ ProviderSessionError: () => ProviderSessionError,
1274
+ SinkAwarePlayer: () => SinkAwarePlayer,
1275
+ applySinkToMediaElement: () => applySinkToMediaElement,
1276
+ audioContextSinkSupported: () => audioContextSinkSupported,
1277
+ buildGenerationConfig: () => buildGenerationConfig,
1278
+ cartesiaAdapter: () => cartesiaAdapter,
1279
+ claimPlayback: () => claimPlayback,
1280
+ clearPlayback: () => clearPlayback,
1281
+ configureSpeech: () => configureSpeech,
1282
+ connectCartesiaTts: () => connectCartesiaTts,
1283
+ enqueuePlayback: () => enqueuePlayback,
1284
+ getActivePlaybackHolderId: () => getActivePlaybackHolderId,
1285
+ getPlaybackSnapshot: () => getPlaybackSnapshot,
1286
+ getPreferredOutputDeviceId: () => getPreferredOutputDeviceId,
1287
+ getPrimedMediaElement: () => getPrimedMediaElement,
1288
+ getUnlockedAudioContext: () => getUnlockedAudioContext,
1289
+ installAudioUnlockListeners: () => installAudioUnlockListeners,
1290
+ isCartesiaAuthError: () => isCartesiaAuthError,
1291
+ isPlaybackHeld: () => isPlaybackHeld,
1292
+ mediaElementSinkSupported: () => mediaElementSinkSupported,
1293
+ outputSelectionSupported: () => outputSelectionSupported,
1294
+ pausePlayback: () => pausePlayback,
1295
+ playPlaybackItem: () => playPlaybackItem,
1296
+ primeAudioOutput: () => primeAudioOutput,
1297
+ releasePlayback: () => releasePlayback,
1298
+ removePlaybackItem: () => removePlaybackItem,
1299
+ resetPlaybackForTests: () => resetPlaybackForTests,
1300
+ resetSpeechPortsForTests: () => resetSpeechPortsForTests,
1301
+ resolveSpeakEngine: () => resolveSpeakEngine,
1302
+ resumePlayback: () => resumePlayback,
1303
+ setPlaybackRate: () => setPlaybackRate,
1304
+ setPreferredOutputDeviceId: () => setPreferredOutputDeviceId,
1305
+ skipPlayback: () => skipPlayback,
1306
+ speak: () => speak,
1307
+ subscribeOutputDevice: () => subscribeOutputDevice,
1308
+ subscribePlayback: () => subscribePlayback,
1309
+ subscribePlaybackLock: () => subscribePlaybackLock
1310
+ });
1311
+ module.exports = __toCommonJS(speech_exports);
1312
+ init_configure();
1313
+
1314
+ // src/speech/speak.ts
1315
+ init_configure();
1316
+
1317
+ // src/speech/playback-queue.ts
1318
+ init_configure();
1319
+ init_unlock();
1320
+
1321
+ // src/speech/playback-lock.ts
1322
+ var current = null;
1323
+ var listeners = /* @__PURE__ */ new Set();
1324
+ function notify(preempted) {
1325
+ const event = { current, preempted };
1326
+ for (const cb of listeners) {
1327
+ try {
1328
+ cb(event);
1329
+ } catch (err) {
1330
+ console.error("[playbackLock] listener threw:", err);
1331
+ }
1332
+ }
1333
+ }
1334
+ function claimPlayback(holder) {
1335
+ if (current && current.id === holder.id) {
1336
+ current = holder;
1337
+ notify(null);
1338
+ return;
1339
+ }
1340
+ let preempted = null;
1341
+ if (current) {
1342
+ preempted = current;
1343
+ current = null;
1344
+ try {
1345
+ preempted.stop();
1346
+ } catch (err) {
1347
+ console.error(
1348
+ `[playbackLock] previous holder "${preempted.id}" stop() threw during takeover:`,
1349
+ err
1350
+ );
1351
+ }
1352
+ }
1353
+ current = holder;
1354
+ notify(preempted);
1355
+ }
1356
+ function releasePlayback(id) {
1357
+ if (current?.id === id) {
1358
+ current = null;
1359
+ notify(null);
1360
+ }
1361
+ }
1362
+ function getActivePlaybackHolderId() {
1363
+ return current?.id ?? null;
1364
+ }
1365
+ function isPlaybackHeld() {
1366
+ return current !== null;
1367
+ }
1368
+ function subscribePlaybackLock(cb) {
1369
+ listeners.add(cb);
1370
+ return () => {
1371
+ listeners.delete(cb);
1372
+ };
1373
+ }
1374
+
1375
+ // src/speech/playback-queue.ts
1376
+ var QUEUE_HOLDER_ID = "playback-queue";
1377
+ var adapterCache = /* @__PURE__ */ new Map();
1378
+ async function getAdapter(provider) {
1379
+ const cached = adapterCache.get(provider);
1380
+ if (cached) return cached;
1381
+ let adapter;
1382
+ const registered = speechPorts().adapters?.[provider];
1383
+ if (registered) {
1384
+ adapter = await registered();
1385
+ } else if (provider === "cartesia") {
1386
+ adapter = (await Promise.resolve().then(() => (init_cartesia_adapter(), cartesia_adapter_exports))).cartesiaAdapter;
1387
+ } else {
1388
+ throw new Error(`No speech engine "${provider}" is set up in this app.`);
1389
+ }
1390
+ adapterCache.set(provider, adapter);
1391
+ return adapter;
1392
+ }
1393
+ var items = [];
1394
+ var currentId = null;
1395
+ var rate = 1;
1396
+ var active = null;
1397
+ var runToken = 0;
1398
+ var listeners2 = /* @__PURE__ */ new Set();
1399
+ var seq = 0;
1400
+ function nextId() {
1401
+ seq += 1;
1402
+ return `pb_${Date.now().toString(36)}_${seq}`;
1403
+ }
1404
+ function snapshot() {
1405
+ return { items: items.map((i) => ({ ...i })), currentId, rate };
1406
+ }
1407
+ function notify2() {
1408
+ const snap = snapshot();
1409
+ listeners2.forEach((l) => {
1410
+ try {
1411
+ l(snap);
1412
+ } catch (err) {
1413
+ console.error("[playbackQueue] listener error", err);
1414
+ }
1415
+ });
1416
+ }
1417
+ function patch(id, changes) {
1418
+ items = items.map((i) => i.id === id ? { ...i, ...changes } : i);
1419
+ }
1420
+ function isBusy() {
1421
+ return active !== null || currentId !== null;
1422
+ }
1423
+ async function startItem(id) {
1424
+ const item = items.find((i) => i.id === id);
1425
+ if (!item) return;
1426
+ claimPlayback({
1427
+ id: QUEUE_HOLDER_ID,
1428
+ label: "Playback queue",
1429
+ stop: forcePreemptFromLock
1430
+ });
1431
+ if (active) {
1432
+ const prev = active;
1433
+ active = null;
1434
+ try {
1435
+ await prev.stop();
1436
+ } catch {
1437
+ }
1438
+ }
1439
+ const token = ++runToken;
1440
+ currentId = id;
1441
+ patch(id, { status: "loading", error: void 0 });
1442
+ notify2();
1443
+ const stale = () => token !== runToken;
1444
+ const beforeStart = speechPorts().beforeStart;
1445
+ if (beforeStart) {
1446
+ try {
1447
+ await beforeStart(item);
1448
+ } catch (err) {
1449
+ if (stale()) return;
1450
+ patch(id, {
1451
+ status: "error",
1452
+ error: speechPorts().describeStartRefusal?.(err) ?? (err instanceof Error ? err.message : "Playback failed")
1453
+ });
1454
+ active = null;
1455
+ void advance();
1456
+ return;
1457
+ }
1458
+ if (stale()) return;
1459
+ }
1460
+ try {
1461
+ const adapter = await getAdapter(item.provider);
1462
+ if (stale()) return;
1463
+ const handle = await adapter.start(
1464
+ item,
1465
+ {
1466
+ onLoading: () => {
1467
+ if (stale()) return;
1468
+ patch(id, { status: "loading" });
1469
+ notify2();
1470
+ },
1471
+ onPlaying: () => {
1472
+ if (stale()) return;
1473
+ patch(id, { status: "playing" });
1474
+ notify2();
1475
+ },
1476
+ onEnded: () => {
1477
+ if (stale()) return;
1478
+ patch(id, { status: "done" });
1479
+ active = null;
1480
+ void advance();
1481
+ },
1482
+ onError: (message) => {
1483
+ if (stale()) return;
1484
+ patch(id, { status: "error", error: message });
1485
+ active = null;
1486
+ void advance();
1487
+ }
1488
+ },
1489
+ rate
1490
+ );
1491
+ if (stale()) {
1492
+ try {
1493
+ await handle.stop();
1494
+ } catch {
1495
+ }
1496
+ return;
1497
+ }
1498
+ active = handle;
1499
+ } catch (err) {
1500
+ if (stale()) return;
1501
+ patch(id, {
1502
+ status: "error",
1503
+ error: err instanceof Error ? err.message : "Playback failed"
1504
+ });
1505
+ active = null;
1506
+ void advance();
1507
+ }
1508
+ }
1509
+ async function advance() {
1510
+ const next = items.find((i) => i.status === "queued");
1511
+ if (next) {
1512
+ await startItem(next.id);
1513
+ } else {
1514
+ currentId = null;
1515
+ releasePlayback(QUEUE_HOLDER_ID);
1516
+ notify2();
1517
+ }
1518
+ }
1519
+ function forcePreemptFromLock() {
1520
+ runToken += 1;
1521
+ if (active) {
1522
+ const prev = active;
1523
+ active = null;
1524
+ try {
1525
+ void prev.stop();
1526
+ } catch {
1527
+ }
1528
+ }
1529
+ items = [];
1530
+ currentId = null;
1531
+ notify2();
1532
+ }
1533
+ function enqueuePlayback(request) {
1534
+ speechPorts().onEngage?.();
1535
+ primeAudioOutput();
1536
+ const id = nextId();
1537
+ const item = {
1538
+ ...request,
1539
+ id,
1540
+ status: "queued",
1541
+ enqueuedAtMs: Date.now()
1542
+ };
1543
+ items = [...items, item];
1544
+ notify2();
1545
+ if (!isBusy()) {
1546
+ void startItem(id);
1547
+ }
1548
+ return { id };
1549
+ }
1550
+ async function pausePlayback() {
1551
+ if (!active || !currentId) return;
1552
+ await active.pause();
1553
+ patch(currentId, { status: "paused" });
1554
+ notify2();
1555
+ }
1556
+ async function resumePlayback() {
1557
+ speechPorts().onEngage?.();
1558
+ primeAudioOutput();
1559
+ if (!active || !currentId) return;
1560
+ await active.resume();
1561
+ patch(currentId, { status: "playing" });
1562
+ notify2();
1563
+ }
1564
+ async function skipPlayback() {
1565
+ if (currentId) patch(currentId, { status: "done" });
1566
+ if (active) {
1567
+ const prev = active;
1568
+ active = null;
1569
+ try {
1570
+ await prev.stop();
1571
+ } catch {
1572
+ }
1573
+ }
1574
+ await advance();
1575
+ }
1576
+ async function playPlaybackItem(id) {
1577
+ speechPorts().onEngage?.();
1578
+ primeAudioOutput();
1579
+ const item = items.find((i) => i.id === id);
1580
+ if (!item) return;
1581
+ if (item.status === "done" || item.status === "error") {
1582
+ patch(id, { status: "queued", error: void 0 });
1583
+ }
1584
+ if (currentId && currentId !== id) {
1585
+ patch(currentId, { status: "done" });
1586
+ }
1587
+ await startItem(id);
1588
+ }
1589
+ async function removePlaybackItem(id) {
1590
+ if (id === currentId) {
1591
+ await skipPlayback();
1592
+ items = items.filter((i) => i.id !== id);
1593
+ notify2();
1594
+ return;
1595
+ }
1596
+ items = items.filter((i) => i.id !== id);
1597
+ notify2();
1598
+ }
1599
+ async function clearPlayback() {
1600
+ runToken += 1;
1601
+ if (active) {
1602
+ const prev = active;
1603
+ active = null;
1604
+ try {
1605
+ await prev.stop();
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ items = [];
1610
+ currentId = null;
1611
+ releasePlayback(QUEUE_HOLDER_ID);
1612
+ notify2();
1613
+ }
1614
+ function setPlaybackRate(value) {
1615
+ rate = Math.max(0.25, Math.min(4, value));
1616
+ active?.setRate?.(rate);
1617
+ notify2();
1618
+ }
1619
+ function getPlaybackSnapshot() {
1620
+ return snapshot();
1621
+ }
1622
+ function resetPlaybackForTests() {
1623
+ runToken += 1;
1624
+ active = null;
1625
+ items = [];
1626
+ currentId = null;
1627
+ rate = 1;
1628
+ adapterCache.clear();
1629
+ releasePlayback(QUEUE_HOLDER_ID);
1630
+ }
1631
+ function subscribePlayback(listener) {
1632
+ listeners2.add(listener);
1633
+ listener(snapshot());
1634
+ return () => {
1635
+ listeners2.delete(listener);
1636
+ };
1637
+ }
1638
+
1639
+ // src/speech/speak.ts
1640
+ function resolveSpeakEngine(requested) {
1641
+ return requested ?? speechPorts().defaultEngine ?? "cartesia";
1642
+ }
1643
+ function speak(request) {
1644
+ const complete = (r) => speechPorts().completeRequest?.(r) ?? r;
1645
+ if (request.sample) {
1646
+ const { id: id2 } = enqueuePlayback(
1647
+ complete({
1648
+ text: request.text,
1649
+ processMarkdown: false,
1650
+ label: request.label,
1651
+ provider: "catalog",
1652
+ catalog: { sample: request.sample }
1653
+ })
1654
+ );
1655
+ return { id: id2, engine: "catalog" };
1656
+ }
1657
+ const engine = resolveSpeakEngine(request.engine);
1658
+ const common = {
1659
+ text: request.text,
1660
+ processMarkdown: request.processMarkdown,
1661
+ label: request.label,
1662
+ dictionarySurfaceKey: request.dictionarySurfaceKey
1663
+ };
1664
+ if (engine === "cartesia") {
1665
+ const { id: id2 } = enqueuePlayback(
1666
+ complete({
1667
+ ...common,
1668
+ provider: "cartesia",
1669
+ cartesia: {
1670
+ voiceId: request.voice,
1671
+ language: request.language,
1672
+ speed: request.speed,
1673
+ purpose: request.purpose ?? "assistant"
1674
+ }
1675
+ })
1676
+ );
1677
+ return { id: id2, engine };
1678
+ }
1679
+ const { id } = enqueuePlayback(complete({ ...common, provider: engine, catalog: { voice: request.voice } }));
1680
+ return { id, engine };
1681
+ }
1682
+
1683
+ // src/speech.ts
1684
+ init_unlock();
1685
+ init_output_sink();
1686
+ init_sink_aware_player();
1687
+ init_cartesia_socket();
1688
+ init_cartesia_adapter();
1689
+ //# sourceMappingURL=speech.cjs.map