@360labs/live-transcribe 0.1.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/index.mjs ADDED
@@ -0,0 +1,4682 @@
1
+ /* @360labs/live-transcribe - MIT License - Built by 360labs */
2
+
3
+ // src/core/EventEmitter.ts
4
+ var EventEmitter = class {
5
+ constructor() {
6
+ this.events = /* @__PURE__ */ new Map();
7
+ }
8
+ /**
9
+ * Subscribe to an event
10
+ * @param event - Event name to subscribe to
11
+ * @param listener - Callback function
12
+ */
13
+ on(event, listener) {
14
+ if (!this.events.has(event)) {
15
+ this.events.set(event, /* @__PURE__ */ new Set());
16
+ }
17
+ this.events.get(event).add(listener);
18
+ return this;
19
+ }
20
+ /**
21
+ * Unsubscribe from an event
22
+ * @param event - Event name to unsubscribe from
23
+ * @param listener - Callback function to remove
24
+ */
25
+ off(event, listener) {
26
+ const listeners = this.events.get(event);
27
+ if (listeners) {
28
+ listeners.delete(listener);
29
+ if (listeners.size === 0) {
30
+ this.events.delete(event);
31
+ }
32
+ }
33
+ return this;
34
+ }
35
+ /**
36
+ * Subscribe to an event for one-time notification
37
+ * @param event - Event name to subscribe to
38
+ * @param listener - Callback function
39
+ */
40
+ once(event, listener) {
41
+ const onceWrapper = ((...args) => {
42
+ this.off(event, onceWrapper);
43
+ listener.apply(this, args);
44
+ });
45
+ return this.on(event, onceWrapper);
46
+ }
47
+ /**
48
+ * Emit an event to all subscribers
49
+ * @param event - Event name to emit
50
+ * @param args - Arguments to pass to listeners
51
+ */
52
+ emit(event, ...args) {
53
+ const listeners = this.events.get(event);
54
+ if (!listeners || listeners.size === 0) {
55
+ return false;
56
+ }
57
+ listeners.forEach((listener) => {
58
+ try {
59
+ listener.apply(this, args);
60
+ } catch (error) {
61
+ console.error(`Error in event listener for "${String(event)}":`, error);
62
+ }
63
+ });
64
+ return true;
65
+ }
66
+ /**
67
+ * Remove all listeners for an event, or all listeners if no event specified
68
+ * @param event - Optional event name
69
+ */
70
+ removeAllListeners(event) {
71
+ if (event !== void 0) {
72
+ this.events.delete(event);
73
+ } else {
74
+ this.events.clear();
75
+ }
76
+ return this;
77
+ }
78
+ /**
79
+ * Get the number of listeners for an event
80
+ * @param event - Event name
81
+ */
82
+ listenerCount(event) {
83
+ const listeners = this.events.get(event);
84
+ return listeners ? listeners.size : 0;
85
+ }
86
+ /**
87
+ * Get all event names that have listeners
88
+ */
89
+ eventNames() {
90
+ return Array.from(this.events.keys());
91
+ }
92
+ };
93
+
94
+ // src/types/config.ts
95
+ var TranscriptionProvider = /* @__PURE__ */ ((TranscriptionProvider2) => {
96
+ TranscriptionProvider2["WebSpeechAPI"] = "web-speech";
97
+ TranscriptionProvider2["Deepgram"] = "deepgram";
98
+ TranscriptionProvider2["AssemblyAI"] = "assemblyai";
99
+ TranscriptionProvider2["Custom"] = "custom";
100
+ return TranscriptionProvider2;
101
+ })(TranscriptionProvider || {});
102
+ var AudioEncoding = /* @__PURE__ */ ((AudioEncoding2) => {
103
+ AudioEncoding2["LINEAR16"] = "linear16";
104
+ AudioEncoding2["MULAW"] = "mulaw";
105
+ AudioEncoding2["ALAW"] = "alaw";
106
+ AudioEncoding2["OPUS"] = "opus";
107
+ return AudioEncoding2;
108
+ })(AudioEncoding || {});
109
+ var DEFAULT_AUDIO_CONFIG = {
110
+ sampleRate: 16e3,
111
+ channels: 1,
112
+ bitDepth: 16,
113
+ encoding: "linear16" /* LINEAR16 */
114
+ };
115
+ var DEFAULT_TRANSCRIPTION_CONFIG = {
116
+ language: "en-US",
117
+ interimResults: true,
118
+ profanityFilter: false,
119
+ punctuation: true
120
+ };
121
+
122
+ // src/types/session.ts
123
+ var SessionState = /* @__PURE__ */ ((SessionState2) => {
124
+ SessionState2["IDLE"] = "idle";
125
+ SessionState2["INITIALIZING"] = "initializing";
126
+ SessionState2["ACTIVE"] = "active";
127
+ SessionState2["PAUSED"] = "paused";
128
+ SessionState2["STOPPING"] = "stopping";
129
+ SessionState2["STOPPED"] = "stopped";
130
+ SessionState2["ERROR"] = "error";
131
+ return SessionState2;
132
+ })(SessionState || {});
133
+ var DEFAULT_SESSION_CONFIG = {
134
+ recordAudio: false,
135
+ maxDuration: 0,
136
+ // 0 means no limit
137
+ silenceTimeout: 0,
138
+ // 0 means no auto-stop
139
+ enableVAD: false,
140
+ vadThreshold: 0.5
141
+ };
142
+
143
+ // src/types/events.ts
144
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
145
+ ErrorCode2["INITIALIZATION_FAILED"] = "initialization_failed";
146
+ ErrorCode2["CONNECTION_FAILED"] = "connection_failed";
147
+ ErrorCode2["AUTHENTICATION_FAILED"] = "authentication_failed";
148
+ ErrorCode2["MICROPHONE_ACCESS_DENIED"] = "microphone_access_denied";
149
+ ErrorCode2["UNSUPPORTED_BROWSER"] = "unsupported_browser";
150
+ ErrorCode2["NETWORK_ERROR"] = "network_error";
151
+ ErrorCode2["PROVIDER_ERROR"] = "provider_error";
152
+ ErrorCode2["INVALID_CONFIG"] = "invalid_config";
153
+ ErrorCode2["SESSION_EXPIRED"] = "session_expired";
154
+ ErrorCode2["UNKNOWN_ERROR"] = "unknown_error";
155
+ return ErrorCode2;
156
+ })(ErrorCode || {});
157
+ var TranscriptionError = class _TranscriptionError extends Error {
158
+ constructor(message, code, provider, details) {
159
+ super(message);
160
+ this.name = "TranscriptionError";
161
+ this.code = code;
162
+ this.provider = provider;
163
+ this.details = details;
164
+ if (Error.captureStackTrace) {
165
+ Error.captureStackTrace(this, _TranscriptionError);
166
+ }
167
+ }
168
+ };
169
+
170
+ // src/core/BaseTranscriber.ts
171
+ var BaseTranscriber = class extends EventEmitter {
172
+ /**
173
+ * Create a new BaseTranscriber instance
174
+ * @param config - Transcription configuration
175
+ */
176
+ constructor(config) {
177
+ super();
178
+ /** Current session state */
179
+ this.state = "idle" /* IDLE */;
180
+ /** Recorded audio chunks */
181
+ this.audioRecording = [];
182
+ /** Word count in current session */
183
+ this.wordCount = 0;
184
+ this.config = { ...DEFAULT_TRANSCRIPTION_CONFIG, ...config };
185
+ this.sessionMetadata = this.initializeMetadata();
186
+ this.validateConfig();
187
+ }
188
+ // ==================== Public Helper Methods ====================
189
+ /**
190
+ * Get the current session state
191
+ */
192
+ getState() {
193
+ return this.state;
194
+ }
195
+ /**
196
+ * Get session metadata
197
+ */
198
+ getMetadata() {
199
+ return {
200
+ ...this.sessionMetadata,
201
+ duration: this.calculateDuration(),
202
+ wordCount: this.wordCount
203
+ };
204
+ }
205
+ /**
206
+ * Get recorded audio data
207
+ * @returns Combined audio data or null if not recording
208
+ */
209
+ getRecording() {
210
+ if (this.audioRecording.length === 0) {
211
+ return null;
212
+ }
213
+ const totalLength = this.audioRecording.reduce((acc, chunk) => acc + chunk.byteLength, 0);
214
+ const combined = new ArrayBuffer(totalLength);
215
+ const view = new Uint8Array(combined);
216
+ let offset = 0;
217
+ for (const chunk of this.audioRecording) {
218
+ view.set(new Uint8Array(chunk), offset);
219
+ offset += chunk.byteLength;
220
+ }
221
+ return combined;
222
+ }
223
+ // ==================== Protected Helper Methods ====================
224
+ /**
225
+ * Update session state and emit state change event
226
+ * @param newState - New session state
227
+ */
228
+ setState(newState) {
229
+ const previousState = this.state;
230
+ this.state = newState;
231
+ if (newState === "active" /* ACTIVE */ && !this.startTime) {
232
+ this.startTime = Date.now();
233
+ this.sessionMetadata.startTime = this.startTime;
234
+ }
235
+ if (newState === "stopped" /* STOPPED */ || newState === "error" /* ERROR */) {
236
+ this.sessionMetadata.endTime = Date.now();
237
+ this.sessionMetadata.duration = this.calculateDuration();
238
+ }
239
+ if (previousState !== newState) {
240
+ this.emit("stateChange", newState);
241
+ }
242
+ }
243
+ /**
244
+ * Handle incoming transcription result
245
+ * @param result - Transcription result from provider
246
+ */
247
+ handleTranscript(result) {
248
+ if (result.isFinal && result.text) {
249
+ const words = result.text.trim().split(/\s+/).filter((w) => w.length > 0);
250
+ this.wordCount += words.length;
251
+ }
252
+ this.emit("transcript", result);
253
+ if (result.isFinal) {
254
+ this.emit("final", result);
255
+ } else {
256
+ this.emit("interim", result);
257
+ }
258
+ }
259
+ /**
260
+ * Handle errors and emit error event
261
+ * @param error - Error to handle
262
+ */
263
+ handleError(error) {
264
+ let transcriptionError;
265
+ if (error instanceof TranscriptionError) {
266
+ transcriptionError = error;
267
+ } else {
268
+ transcriptionError = new TranscriptionError(
269
+ error.message,
270
+ "unknown_error" /* UNKNOWN_ERROR */,
271
+ this.config.provider,
272
+ error
273
+ );
274
+ }
275
+ this.setState("error" /* ERROR */);
276
+ this.emit("error", transcriptionError);
277
+ }
278
+ /**
279
+ * Validate configuration
280
+ * @throws TranscriptionError if configuration is invalid
281
+ */
282
+ validateConfig() {
283
+ if (!this.config.provider) {
284
+ throw new TranscriptionError(
285
+ "Provider must be specified in configuration",
286
+ "invalid_config" /* INVALID_CONFIG */
287
+ );
288
+ }
289
+ const cloudProviders = ["deepgram", "assemblyai"];
290
+ if (cloudProviders.includes(this.config.provider) && !this.config.apiKey) {
291
+ throw new TranscriptionError(
292
+ `API key is required for ${this.config.provider} provider`,
293
+ "invalid_config" /* INVALID_CONFIG */,
294
+ this.config.provider
295
+ );
296
+ }
297
+ }
298
+ /**
299
+ * Record audio data if recording is enabled
300
+ * @param data - Audio data to record
301
+ */
302
+ recordAudioData(data) {
303
+ this.audioRecording.push(data.slice(0));
304
+ }
305
+ /**
306
+ * Calculate session duration
307
+ * @returns Duration in milliseconds
308
+ */
309
+ calculateDuration() {
310
+ if (!this.startTime) {
311
+ return 0;
312
+ }
313
+ const endTime = this.sessionMetadata.endTime || Date.now();
314
+ return endTime - this.startTime;
315
+ }
316
+ /**
317
+ * Clear recording data
318
+ */
319
+ clearRecording() {
320
+ this.audioRecording = [];
321
+ }
322
+ /**
323
+ * Reset session state for new session
324
+ */
325
+ resetSession() {
326
+ this.state = "idle" /* IDLE */;
327
+ this.startTime = void 0;
328
+ this.wordCount = 0;
329
+ this.audioRecording = [];
330
+ this.sessionMetadata = this.initializeMetadata();
331
+ }
332
+ // ==================== Private Helper Methods ====================
333
+ /**
334
+ * Generate a unique session ID
335
+ */
336
+ generateSessionId() {
337
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
338
+ const r = Math.random() * 16 | 0;
339
+ const v = c === "x" ? r : r & 3 | 8;
340
+ return v.toString(16);
341
+ });
342
+ }
343
+ /**
344
+ * Initialize session metadata
345
+ */
346
+ initializeMetadata() {
347
+ return {
348
+ id: this.generateSessionId(),
349
+ startTime: 0,
350
+ wordCount: 0,
351
+ provider: this.config.provider
352
+ };
353
+ }
354
+ };
355
+
356
+ // src/utils/exporters/JSONExporter.ts
357
+ var JSONExporter = class {
358
+ /**
359
+ * Export session to minified JSON string
360
+ * @param session - Session data to export
361
+ * @returns JSON string
362
+ */
363
+ static export(session) {
364
+ const exportData = {
365
+ version: "1.0.0",
366
+ session: {
367
+ metadata: session.metadata,
368
+ transcripts: session.transcripts,
369
+ config: {}
370
+ },
371
+ exportedAt: Date.now()
372
+ };
373
+ return JSON.stringify(exportData);
374
+ }
375
+ /**
376
+ * Export session to formatted/pretty JSON string
377
+ * @param session - Session data to export
378
+ * @param indent - Indentation spaces (default: 2)
379
+ * @returns Formatted JSON string
380
+ */
381
+ static exportPretty(session, indent = 2) {
382
+ const exportData = {
383
+ version: "1.0.0",
384
+ session: {
385
+ metadata: session.metadata,
386
+ transcripts: session.transcripts,
387
+ config: {}
388
+ },
389
+ exportedAt: Date.now()
390
+ };
391
+ return JSON.stringify(exportData, null, indent);
392
+ }
393
+ /**
394
+ * Parse JSON and validate structure
395
+ * @param json - JSON string to parse
396
+ * @returns Parsed session export data
397
+ */
398
+ static parse(json) {
399
+ const data = JSON.parse(json);
400
+ if (!data.version || !data.session) {
401
+ throw new Error("Invalid session export format");
402
+ }
403
+ return data;
404
+ }
405
+ };
406
+
407
+ // src/utils/exporters/TextExporter.ts
408
+ var TextExporter = class {
409
+ /**
410
+ * Export session transcripts to plain text
411
+ * @param session - Session data to export
412
+ * @param options - Export options
413
+ * @returns Plain text string
414
+ */
415
+ static export(session, options = {}) {
416
+ const {
417
+ includeTimestamps = false,
418
+ includeSpeakers = false,
419
+ includeConfidence = false,
420
+ paragraphBreaks = false
421
+ } = options;
422
+ const lines = [];
423
+ const finalTranscripts = session.transcripts.filter((t) => t.isFinal);
424
+ for (const transcript of finalTranscripts) {
425
+ let line = "";
426
+ if (includeTimestamps && transcript.timestamp) {
427
+ const time = new Date(transcript.timestamp).toISOString().substr(11, 8);
428
+ line += `[${time}] `;
429
+ }
430
+ if (includeSpeakers && transcript.speaker) {
431
+ line += `${transcript.speaker}: `;
432
+ }
433
+ line += transcript.text;
434
+ if (includeConfidence && transcript.confidence !== void 0) {
435
+ line += ` (${Math.round(transcript.confidence * 100)}%)`;
436
+ }
437
+ lines.push(line);
438
+ }
439
+ const separator = paragraphBreaks ? "\n\n" : " ";
440
+ return lines.join(separator).trim();
441
+ }
442
+ /**
443
+ * Export as continuous text without any formatting
444
+ * @param session - Session data to export
445
+ * @returns Plain text string
446
+ */
447
+ static exportPlain(session) {
448
+ return session.transcripts.filter((t) => t.isFinal).map((t) => t.text).join(" ").trim();
449
+ }
450
+ };
451
+
452
+ // src/utils/exporters/SRTExporter.ts
453
+ var SRTExporter = class {
454
+ /**
455
+ * Export session transcripts to SRT format
456
+ * @param session - Session data to export
457
+ * @returns SRT formatted string
458
+ */
459
+ static export(session) {
460
+ const finalTranscripts = session.transcripts.filter((t) => t.isFinal);
461
+ const lines = [];
462
+ let sequenceNumber = 1;
463
+ let currentTime = 0;
464
+ for (const transcript of finalTranscripts) {
465
+ if (!transcript.text.trim()) continue;
466
+ let startTime = currentTime;
467
+ let endTime;
468
+ if (transcript.words && transcript.words.length > 0) {
469
+ startTime = transcript.words[0].start;
470
+ endTime = transcript.words[transcript.words.length - 1].end;
471
+ } else {
472
+ const wordCount = transcript.text.split(/\s+/).length;
473
+ const durationMs = wordCount / 150 * 60 * 1e3;
474
+ endTime = startTime + Math.max(durationMs, 1e3);
475
+ }
476
+ lines.push(String(sequenceNumber));
477
+ lines.push(`${this.formatTime(startTime)} --> ${this.formatTime(endTime)}`);
478
+ lines.push(transcript.text);
479
+ lines.push("");
480
+ sequenceNumber++;
481
+ currentTime = endTime + 100;
482
+ }
483
+ return lines.join("\n");
484
+ }
485
+ /**
486
+ * Format milliseconds to SRT timestamp format (HH:MM:SS,mmm)
487
+ * @param ms - Time in milliseconds
488
+ * @returns Formatted timestamp
489
+ */
490
+ static formatTime(ms) {
491
+ const totalSeconds = Math.floor(ms / 1e3);
492
+ const hours = Math.floor(totalSeconds / 3600);
493
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
494
+ const seconds = totalSeconds % 60;
495
+ const milliseconds = Math.floor(ms % 1e3);
496
+ return `${this.pad(hours, 2)}:${this.pad(minutes, 2)}:${this.pad(seconds, 2)},${this.pad(milliseconds, 3)}`;
497
+ }
498
+ /**
499
+ * Pad number with leading zeros
500
+ * @param num - Number to pad
501
+ * @param length - Target length
502
+ * @returns Padded string
503
+ */
504
+ static pad(num, length) {
505
+ return String(num).padStart(length, "0");
506
+ }
507
+ };
508
+
509
+ // src/utils/exporters/VTTExporter.ts
510
+ var VTTExporter = class {
511
+ /**
512
+ * Export session transcripts to WebVTT format
513
+ * @param session - Session data to export
514
+ * @returns WebVTT formatted string
515
+ */
516
+ static export(session) {
517
+ const finalTranscripts = session.transcripts.filter((t) => t.isFinal);
518
+ const lines = ["WEBVTT", ""];
519
+ let currentTime = 0;
520
+ for (const transcript of finalTranscripts) {
521
+ if (!transcript.text.trim()) continue;
522
+ let startTime = currentTime;
523
+ let endTime;
524
+ if (transcript.words && transcript.words.length > 0) {
525
+ startTime = transcript.words[0].start;
526
+ endTime = transcript.words[transcript.words.length - 1].end;
527
+ } else {
528
+ const wordCount = transcript.text.split(/\s+/).length;
529
+ const durationMs = wordCount / 150 * 60 * 1e3;
530
+ endTime = startTime + Math.max(durationMs, 1e3);
531
+ }
532
+ lines.push(`${this.formatTime(startTime)} --> ${this.formatTime(endTime)}`);
533
+ if (transcript.speaker) {
534
+ lines.push(`<v ${transcript.speaker}>${transcript.text}`);
535
+ } else {
536
+ lines.push(transcript.text);
537
+ }
538
+ lines.push("");
539
+ currentTime = endTime + 100;
540
+ }
541
+ return lines.join("\n");
542
+ }
543
+ /**
544
+ * Export with cue identifiers
545
+ * @param session - Session data to export
546
+ * @param cuePrefix - Prefix for cue identifiers
547
+ * @returns WebVTT formatted string with cue IDs
548
+ */
549
+ static exportWithCues(session, cuePrefix = "cue") {
550
+ const finalTranscripts = session.transcripts.filter((t) => t.isFinal);
551
+ const lines = ["WEBVTT", ""];
552
+ let cueNumber = 1;
553
+ let currentTime = 0;
554
+ for (const transcript of finalTranscripts) {
555
+ if (!transcript.text.trim()) continue;
556
+ let startTime = currentTime;
557
+ let endTime;
558
+ if (transcript.words && transcript.words.length > 0) {
559
+ startTime = transcript.words[0].start;
560
+ endTime = transcript.words[transcript.words.length - 1].end;
561
+ } else {
562
+ const wordCount = transcript.text.split(/\s+/).length;
563
+ const durationMs = wordCount / 150 * 60 * 1e3;
564
+ endTime = startTime + Math.max(durationMs, 1e3);
565
+ }
566
+ lines.push(`${cuePrefix}-${cueNumber}`);
567
+ lines.push(`${this.formatTime(startTime)} --> ${this.formatTime(endTime)}`);
568
+ lines.push(transcript.text);
569
+ lines.push("");
570
+ cueNumber++;
571
+ currentTime = endTime + 100;
572
+ }
573
+ return lines.join("\n");
574
+ }
575
+ /**
576
+ * Format milliseconds to WebVTT timestamp format (HH:MM:SS.mmm)
577
+ * @param ms - Time in milliseconds
578
+ * @returns Formatted timestamp
579
+ */
580
+ static formatTime(ms) {
581
+ const totalSeconds = Math.floor(ms / 1e3);
582
+ const hours = Math.floor(totalSeconds / 3600);
583
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
584
+ const seconds = totalSeconds % 60;
585
+ const milliseconds = Math.floor(ms % 1e3);
586
+ return `${this.pad(hours, 2)}:${this.pad(minutes, 2)}:${this.pad(seconds, 2)}.${this.pad(milliseconds, 3)}`;
587
+ }
588
+ /**
589
+ * Pad number with leading zeros
590
+ */
591
+ static pad(num, length) {
592
+ return String(num).padStart(length, "0");
593
+ }
594
+ };
595
+
596
+ // src/utils/exporters/CSVExporter.ts
597
+ var CSVExporter = class {
598
+ /**
599
+ * Export session transcripts to CSV format
600
+ * @param session - Session data to export
601
+ * @param options - Export options
602
+ * @returns CSV formatted string
603
+ */
604
+ static export(session, options = {}) {
605
+ const {
606
+ delimiter = ",",
607
+ includeHeaders = true,
608
+ columns = ["timestamp", "text", "isFinal", "confidence", "speaker"]
609
+ } = options;
610
+ const rows = [];
611
+ if (includeHeaders) {
612
+ rows.push(columns.map((col) => this.escapeField(col, delimiter)).join(delimiter));
613
+ }
614
+ for (const transcript of session.transcripts) {
615
+ const values = columns.map((col) => {
616
+ const value = this.getFieldValue(transcript, col);
617
+ return this.escapeField(String(value), delimiter);
618
+ });
619
+ rows.push(values.join(delimiter));
620
+ }
621
+ return rows.join("\n");
622
+ }
623
+ /**
624
+ * Export only final transcripts
625
+ * @param session - Session data to export
626
+ * @param options - Export options
627
+ * @returns CSV formatted string
628
+ */
629
+ static exportFinalOnly(session, options = {}) {
630
+ const filteredSession = {
631
+ transcripts: session.transcripts.filter((t) => t.isFinal)
632
+ };
633
+ return this.export(filteredSession, options);
634
+ }
635
+ /**
636
+ * Get field value from transcript
637
+ * @param transcript - Transcription result
638
+ * @param field - Field name
639
+ * @returns Field value
640
+ */
641
+ static getFieldValue(transcript, field) {
642
+ switch (field) {
643
+ case "timestamp":
644
+ return transcript.timestamp || "";
645
+ case "text":
646
+ return transcript.text || "";
647
+ case "isFinal":
648
+ return transcript.isFinal;
649
+ case "confidence":
650
+ return transcript.confidence !== void 0 ? transcript.confidence : "";
651
+ case "speaker":
652
+ return transcript.speaker || "";
653
+ case "language":
654
+ return transcript.language || "";
655
+ case "wordCount":
656
+ return transcript.text ? transcript.text.split(/\s+/).filter((w) => w).length : 0;
657
+ case "startTime":
658
+ return transcript.words?.[0]?.start || "";
659
+ case "endTime":
660
+ return transcript.words?.[transcript.words.length - 1]?.end || "";
661
+ default:
662
+ return "";
663
+ }
664
+ }
665
+ /**
666
+ * Escape field for CSV format
667
+ * @param field - Field value
668
+ * @param delimiter - CSV delimiter
669
+ * @returns Escaped field
670
+ */
671
+ static escapeField(field, delimiter) {
672
+ if (field.includes(delimiter) || field.includes('"') || field.includes("\n") || field.includes("\r")) {
673
+ const escaped = field.replace(/"/g, '""');
674
+ return `"${escaped}"`;
675
+ }
676
+ return field;
677
+ }
678
+ };
679
+
680
+ // src/core/TranscriptionSession.ts
681
+ var TranscriptionSession = class {
682
+ /**
683
+ * Create a new TranscriptionSession
684
+ * @param provider - Transcription provider to use
685
+ * @param sessionConfig - Session configuration options
686
+ */
687
+ constructor(provider, sessionConfig = {}) {
688
+ /** Collected transcription results */
689
+ this.transcripts = [];
690
+ /** Current session state */
691
+ this.state = "idle" /* IDLE */;
692
+ this.provider = provider;
693
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...sessionConfig };
694
+ this.id = this.generateSessionId();
695
+ }
696
+ /**
697
+ * Start the transcription session
698
+ */
699
+ async start() {
700
+ if (this.state !== "idle" /* IDLE */ && this.state !== "stopped" /* STOPPED */) {
701
+ throw new Error(`Cannot start session in state: ${this.state}`);
702
+ }
703
+ this.state = "initializing" /* INITIALIZING */;
704
+ this.startTime = Date.now();
705
+ try {
706
+ await this.provider.start();
707
+ this.state = "active" /* ACTIVE */;
708
+ this.setupTimers();
709
+ } catch (error) {
710
+ this.state = "error" /* ERROR */;
711
+ throw error;
712
+ }
713
+ }
714
+ /**
715
+ * Stop the transcription session
716
+ */
717
+ async stop() {
718
+ if (this.state === "stopped" /* STOPPED */ || this.state === "idle" /* IDLE */) {
719
+ return;
720
+ }
721
+ this.state = "stopping" /* STOPPING */;
722
+ this.clearTimers();
723
+ try {
724
+ await this.provider.stop();
725
+ } finally {
726
+ this.state = "stopped" /* STOPPED */;
727
+ }
728
+ }
729
+ /**
730
+ * Pause the transcription session
731
+ */
732
+ pause() {
733
+ if (this.state !== "active" /* ACTIVE */) {
734
+ return;
735
+ }
736
+ this.provider.pause();
737
+ this.state = "paused" /* PAUSED */;
738
+ this.clearTimers();
739
+ }
740
+ /**
741
+ * Resume the transcription session
742
+ */
743
+ resume() {
744
+ if (this.state !== "paused" /* PAUSED */) {
745
+ return;
746
+ }
747
+ this.provider.resume();
748
+ this.state = "active" /* ACTIVE */;
749
+ this.setupTimers();
750
+ }
751
+ /**
752
+ * Add a transcription result to the session
753
+ * @param result - Transcription result to add
754
+ */
755
+ addTranscript(result) {
756
+ this.transcripts.push(result);
757
+ this.resetSilenceTimer();
758
+ }
759
+ /**
760
+ * Get transcription results
761
+ * @param finalOnly - If true, return only final results
762
+ */
763
+ getTranscripts(finalOnly = false) {
764
+ if (finalOnly) {
765
+ return this.transcripts.filter((t) => t.isFinal);
766
+ }
767
+ return [...this.transcripts];
768
+ }
769
+ /**
770
+ * Get concatenated text from all final transcripts
771
+ */
772
+ getFullText() {
773
+ return this.transcripts.filter((t) => t.isFinal).map((t) => t.text).join(" ").trim();
774
+ }
775
+ /**
776
+ * Get the current session state
777
+ */
778
+ getState() {
779
+ return this.state;
780
+ }
781
+ /**
782
+ * Export session data in raw format
783
+ */
784
+ exportRaw() {
785
+ const metadata = {
786
+ id: this.id,
787
+ startTime: this.startTime || 0,
788
+ endTime: this.state === "stopped" /* STOPPED */ ? Date.now() : void 0,
789
+ duration: this.startTime ? Date.now() - this.startTime : 0,
790
+ wordCount: this.getWordCount(),
791
+ provider: this.provider.getState()
792
+ };
793
+ return {
794
+ metadata,
795
+ transcripts: this.getTranscripts(),
796
+ fullText: this.getFullText()
797
+ };
798
+ }
799
+ /**
800
+ * Export session data in specified format
801
+ * @param format - Export format (json, text, srt, vtt, csv)
802
+ */
803
+ export(format = "json") {
804
+ const transcripts = this.getTranscripts(true);
805
+ const sessionData = { transcripts };
806
+ const rawExport = this.exportRaw();
807
+ let data;
808
+ let mimeType;
809
+ let extension;
810
+ switch (format) {
811
+ case "json":
812
+ data = JSONExporter.export({ metadata: rawExport.metadata, transcripts });
813
+ mimeType = "application/json";
814
+ extension = "json";
815
+ break;
816
+ case "text":
817
+ data = TextExporter.export(sessionData, {});
818
+ mimeType = "text/plain";
819
+ extension = "txt";
820
+ break;
821
+ case "srt":
822
+ data = SRTExporter.export(sessionData);
823
+ mimeType = "text/plain";
824
+ extension = "srt";
825
+ break;
826
+ case "vtt":
827
+ data = VTTExporter.export(sessionData);
828
+ mimeType = "text/vtt";
829
+ extension = "vtt";
830
+ break;
831
+ case "csv":
832
+ data = CSVExporter.export(sessionData, {});
833
+ mimeType = "text/csv";
834
+ extension = "csv";
835
+ break;
836
+ default:
837
+ data = JSONExporter.export({ metadata: rawExport.metadata, transcripts });
838
+ mimeType = "application/json";
839
+ extension = "json";
840
+ }
841
+ return {
842
+ format,
843
+ data,
844
+ filename: `transcript-${this.id}.${extension}`,
845
+ mimeType
846
+ };
847
+ }
848
+ /**
849
+ * Get session statistics
850
+ */
851
+ getStatistics() {
852
+ const transcripts = this.getTranscripts(true);
853
+ const wordCount = this.getWordCount();
854
+ const durationMs = this.startTime ? Date.now() - this.startTime : 0;
855
+ let totalConfidence = 0;
856
+ let confCount = 0;
857
+ for (const t of transcripts) {
858
+ if (t.confidence !== void 0) {
859
+ totalConfidence += t.confidence;
860
+ confCount++;
861
+ }
862
+ }
863
+ const averageConfidence = confCount > 0 ? totalConfidence / confCount : 0;
864
+ const durationMinutes = durationMs / 6e4;
865
+ const speakingRate = durationMinutes > 0 ? wordCount / durationMinutes : 0;
866
+ return {
867
+ wordCount,
868
+ averageConfidence,
869
+ speakingRate,
870
+ silencePeriods: 0,
871
+ // Would require VAD tracking
872
+ durationMs,
873
+ transcriptCount: transcripts.length
874
+ };
875
+ }
876
+ /**
877
+ * Clear all transcripts
878
+ */
879
+ clear() {
880
+ this.transcripts = [];
881
+ }
882
+ /**
883
+ * Get the total word count from final transcripts
884
+ */
885
+ getWordCount() {
886
+ return this.transcripts.filter((t) => t.isFinal).reduce((count, t) => {
887
+ const words = t.text.trim().split(/\s+/).filter((w) => w.length > 0);
888
+ return count + words.length;
889
+ }, 0);
890
+ }
891
+ /**
892
+ * Set up session timers (max duration, silence timeout)
893
+ */
894
+ setupTimers() {
895
+ if (this.config.maxDuration > 0) {
896
+ this.maxDurationTimer = setTimeout(() => {
897
+ void this.stop();
898
+ }, this.config.maxDuration);
899
+ }
900
+ this.resetSilenceTimer();
901
+ }
902
+ /**
903
+ * Reset the silence timeout timer
904
+ */
905
+ resetSilenceTimer() {
906
+ if (this.silenceTimer) {
907
+ clearTimeout(this.silenceTimer);
908
+ }
909
+ if (this.config.silenceTimeout > 0 && this.state === "active" /* ACTIVE */) {
910
+ this.silenceTimer = setTimeout(() => {
911
+ void this.stop();
912
+ }, this.config.silenceTimeout);
913
+ }
914
+ }
915
+ /**
916
+ * Clear all timers
917
+ */
918
+ clearTimers() {
919
+ if (this.maxDurationTimer) {
920
+ clearTimeout(this.maxDurationTimer);
921
+ this.maxDurationTimer = void 0;
922
+ }
923
+ if (this.silenceTimer) {
924
+ clearTimeout(this.silenceTimer);
925
+ this.silenceTimer = void 0;
926
+ }
927
+ }
928
+ /**
929
+ * Generate a unique session ID
930
+ */
931
+ generateSessionId() {
932
+ return "session-" + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
933
+ const r = Math.random() * 16 | 0;
934
+ const v = c === "x" ? r : r & 3 | 8;
935
+ return v.toString(16);
936
+ });
937
+ }
938
+ };
939
+
940
+ // src/core/SessionManager.ts
941
+ var SessionManager = class {
942
+ /**
943
+ * Create a new SessionManager
944
+ * @param options - Manager configuration
945
+ */
946
+ constructor(options = {}) {
947
+ /** Active sessions map */
948
+ this.sessions = /* @__PURE__ */ new Map();
949
+ /** Currently active session ID */
950
+ this.activeSessionId = null;
951
+ this.defaultConfig = options.defaultConfig || {};
952
+ this.maxSessions = options.maxSessions || 10;
953
+ }
954
+ /**
955
+ * Create a new transcription session
956
+ * @param provider - Transcription provider instance
957
+ * @param config - Session configuration (merged with defaults)
958
+ * @returns New TranscriptionSession instance
959
+ */
960
+ createSession(provider, config) {
961
+ if (this.sessions.size >= this.maxSessions) {
962
+ throw new Error(`Maximum number of sessions (${this.maxSessions}) reached`);
963
+ }
964
+ const mergedConfig = { ...this.defaultConfig, ...config };
965
+ const session = new TranscriptionSession(provider, mergedConfig);
966
+ this.sessions.set(session.id, session);
967
+ if (!this.activeSessionId) {
968
+ this.activeSessionId = session.id;
969
+ }
970
+ return session;
971
+ }
972
+ /**
973
+ * Get session by ID
974
+ * @param sessionId - Session ID
975
+ * @returns TranscriptionSession or null
976
+ */
977
+ getSession(sessionId) {
978
+ return this.sessions.get(sessionId) || null;
979
+ }
980
+ /**
981
+ * Get the currently active session
982
+ * @returns Active TranscriptionSession or null
983
+ */
984
+ getActiveSession() {
985
+ if (!this.activeSessionId) {
986
+ return null;
987
+ }
988
+ return this.sessions.get(this.activeSessionId) || null;
989
+ }
990
+ /**
991
+ * Set the active session
992
+ * @param sessionId - Session ID to make active
993
+ */
994
+ setActiveSession(sessionId) {
995
+ if (!this.sessions.has(sessionId)) {
996
+ throw new Error(`Session ${sessionId} not found`);
997
+ }
998
+ this.activeSessionId = sessionId;
999
+ }
1000
+ /**
1001
+ * Get all sessions
1002
+ * @returns Array of all sessions
1003
+ */
1004
+ getAllSessions() {
1005
+ return Array.from(this.sessions.values());
1006
+ }
1007
+ /**
1008
+ * Delete a session
1009
+ * @param sessionId - Session ID to delete
1010
+ */
1011
+ async deleteSession(sessionId) {
1012
+ const session = this.sessions.get(sessionId);
1013
+ if (!session) {
1014
+ return;
1015
+ }
1016
+ if (session.getState() === "active" /* ACTIVE */ || session.getState() === "paused" /* PAUSED */) {
1017
+ await session.stop();
1018
+ }
1019
+ this.sessions.delete(sessionId);
1020
+ if (this.activeSessionId === sessionId) {
1021
+ this.activeSessionId = null;
1022
+ const remaining = this.sessions.keys().next();
1023
+ if (!remaining.done) {
1024
+ this.activeSessionId = remaining.value;
1025
+ }
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Clear all sessions
1030
+ */
1031
+ async clearAllSessions() {
1032
+ const stopPromises = [];
1033
+ for (const session of this.sessions.values()) {
1034
+ const state = session.getState();
1035
+ if (state === "active" /* ACTIVE */ || state === "paused" /* PAUSED */) {
1036
+ stopPromises.push(session.stop());
1037
+ }
1038
+ }
1039
+ await Promise.all(stopPromises);
1040
+ this.sessions.clear();
1041
+ this.activeSessionId = null;
1042
+ }
1043
+ /**
1044
+ * Get statistics for all sessions
1045
+ * @returns Session statistics
1046
+ */
1047
+ getSessionStats() {
1048
+ let totalTranscripts = 0;
1049
+ let totalDuration = 0;
1050
+ let totalConfidence = 0;
1051
+ let confidenceCount = 0;
1052
+ let activeSessions = 0;
1053
+ for (const session of this.sessions.values()) {
1054
+ const state = session.getState();
1055
+ if (state === "active" /* ACTIVE */) {
1056
+ activeSessions++;
1057
+ }
1058
+ const stats = session.getStatistics();
1059
+ totalTranscripts += stats.transcriptCount;
1060
+ totalDuration += stats.durationMs;
1061
+ if (stats.averageConfidence > 0) {
1062
+ totalConfidence += stats.averageConfidence;
1063
+ confidenceCount++;
1064
+ }
1065
+ }
1066
+ return {
1067
+ totalSessions: this.sessions.size,
1068
+ activeSessions,
1069
+ totalTranscripts,
1070
+ totalDuration,
1071
+ averageConfidence: confidenceCount > 0 ? totalConfidence / confidenceCount : 0
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Export a session to specified format
1076
+ * @param sessionId - Session ID
1077
+ * @param format - Export format
1078
+ * @returns Export result
1079
+ */
1080
+ exportSession(sessionId, format) {
1081
+ const session = this.sessions.get(sessionId);
1082
+ if (!session) {
1083
+ throw new Error(`Session ${sessionId} not found`);
1084
+ }
1085
+ const exportData = session.exportRaw();
1086
+ const sessionData = {
1087
+ metadata: exportData.metadata,
1088
+ transcripts: exportData.transcripts
1089
+ };
1090
+ let data;
1091
+ let mimeType;
1092
+ let extension;
1093
+ switch (format) {
1094
+ case "json":
1095
+ data = JSONExporter.exportPretty(sessionData);
1096
+ mimeType = "application/json";
1097
+ extension = "json";
1098
+ break;
1099
+ case "text":
1100
+ data = TextExporter.export(sessionData);
1101
+ mimeType = "text/plain";
1102
+ extension = "txt";
1103
+ break;
1104
+ case "srt":
1105
+ data = SRTExporter.export(sessionData);
1106
+ mimeType = "application/x-subrip";
1107
+ extension = "srt";
1108
+ break;
1109
+ case "vtt":
1110
+ data = VTTExporter.export(sessionData);
1111
+ mimeType = "text/vtt";
1112
+ extension = "vtt";
1113
+ break;
1114
+ case "csv":
1115
+ data = CSVExporter.export(sessionData);
1116
+ mimeType = "text/csv";
1117
+ extension = "csv";
1118
+ break;
1119
+ default:
1120
+ throw new Error(`Unsupported export format: ${format}`);
1121
+ }
1122
+ return {
1123
+ format,
1124
+ data,
1125
+ filename: `transcript-${sessionId}.${extension}`,
1126
+ mimeType
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Import a session from data
1131
+ * @param data - Session import data
1132
+ * @param provider - Provider instance for the session
1133
+ * @returns Imported session
1134
+ */
1135
+ importSession(data, provider) {
1136
+ if (this.sessions.size >= this.maxSessions) {
1137
+ throw new Error(`Maximum number of sessions (${this.maxSessions}) reached`);
1138
+ }
1139
+ const session = new TranscriptionSession(provider, data.config || {});
1140
+ for (const transcript of data.transcripts) {
1141
+ session.addTranscript(transcript);
1142
+ }
1143
+ this.sessions.set(session.id, session);
1144
+ return session;
1145
+ }
1146
+ /**
1147
+ * Check if a session exists
1148
+ * @param sessionId - Session ID
1149
+ * @returns True if session exists
1150
+ */
1151
+ hasSession(sessionId) {
1152
+ return this.sessions.has(sessionId);
1153
+ }
1154
+ /**
1155
+ * Get session count
1156
+ * @returns Number of sessions
1157
+ */
1158
+ getSessionCount() {
1159
+ return this.sessions.size;
1160
+ }
1161
+ /**
1162
+ * Get sessions by state
1163
+ * @param state - Session state to filter by
1164
+ * @returns Array of sessions with matching state
1165
+ */
1166
+ getSessionsByState(state) {
1167
+ return this.getAllSessions().filter((session) => session.getState() === state);
1168
+ }
1169
+ };
1170
+
1171
+ // src/providers/WebSpeechProvider.ts
1172
+ var _WebSpeechProvider = class _WebSpeechProvider extends BaseTranscriber {
1173
+ /**
1174
+ * Create a new WebSpeechProvider
1175
+ * @param config - Transcription configuration
1176
+ */
1177
+ constructor(config) {
1178
+ super({ ...config, provider: "web-speech" /* WebSpeechAPI */ });
1179
+ /** Speech recognition instance */
1180
+ this.recognition = null;
1181
+ /** Media stream from microphone */
1182
+ this.mediaStream = null;
1183
+ /** Audio context for analysis */
1184
+ this.audioContext = null;
1185
+ /** Audio analyser for VAD */
1186
+ this.analyser = null;
1187
+ /** Script processor for audio level monitoring */
1188
+ this.audioLevelInterval = null;
1189
+ /** Whether recognition is being restarted automatically */
1190
+ this.isRestarting = false;
1191
+ /** Retry count for auto-restart */
1192
+ this.retryCount = 0;
1193
+ /** Maximum retry attempts */
1194
+ this.maxRetries = 3;
1195
+ }
1196
+ /**
1197
+ * Check if Web Speech API is supported in the current environment
1198
+ */
1199
+ isSupported() {
1200
+ if (typeof window === "undefined") {
1201
+ return false;
1202
+ }
1203
+ return !!(window.SpeechRecognition || window.webkitSpeechRecognition);
1204
+ }
1205
+ /**
1206
+ * Initialize the Web Speech API provider
1207
+ */
1208
+ async initialize() {
1209
+ if (!this.isSupported()) {
1210
+ throw new TranscriptionError(
1211
+ "Web Speech API is not supported in this browser",
1212
+ "unsupported_browser" /* UNSUPPORTED_BROWSER */,
1213
+ "web-speech" /* WebSpeechAPI */
1214
+ );
1215
+ }
1216
+ this.setState("initializing" /* INITIALIZING */);
1217
+ try {
1218
+ await this.getMicrophoneAccess();
1219
+ const SpeechRecognitionClass = window.SpeechRecognition || window.webkitSpeechRecognition;
1220
+ this.recognition = new SpeechRecognitionClass();
1221
+ this.recognition.continuous = true;
1222
+ this.recognition.interimResults = this.config.interimResults ?? true;
1223
+ this.recognition.lang = this.config.language || "en-US";
1224
+ this.recognition.maxAlternatives = 1;
1225
+ this.setupEventHandlers();
1226
+ if (this.config.providerOptions?.enableVAD) {
1227
+ this.setupAudioLevelMonitoring();
1228
+ }
1229
+ } catch (error) {
1230
+ this.handleError(error);
1231
+ throw error;
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Start transcription
1236
+ */
1237
+ async start() {
1238
+ if (!this.recognition) {
1239
+ await this.initialize();
1240
+ }
1241
+ if (this.state === "active" /* ACTIVE */) {
1242
+ return;
1243
+ }
1244
+ try {
1245
+ this.recognition.start();
1246
+ this.retryCount = 0;
1247
+ } catch (error) {
1248
+ if (error.message?.includes("already started")) {
1249
+ return;
1250
+ }
1251
+ this.handleError(error);
1252
+ throw error;
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Stop transcription
1257
+ */
1258
+ async stop() {
1259
+ if (this.state === "stopped" /* STOPPED */ || this.state === "idle" /* IDLE */) {
1260
+ return;
1261
+ }
1262
+ this.setState("stopping" /* STOPPING */);
1263
+ this.isRestarting = false;
1264
+ try {
1265
+ if (this.recognition) {
1266
+ this.recognition.stop();
1267
+ }
1268
+ this.stopAudioLevelMonitoring();
1269
+ this.stopMediaStream();
1270
+ this.setState("stopped" /* STOPPED */);
1271
+ this.emit("stop");
1272
+ } catch (error) {
1273
+ this.handleError(error);
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Pause transcription
1278
+ */
1279
+ pause() {
1280
+ if (this.state !== "active" /* ACTIVE */) {
1281
+ return;
1282
+ }
1283
+ this.isRestarting = false;
1284
+ try {
1285
+ if (this.recognition) {
1286
+ this.recognition.stop();
1287
+ }
1288
+ this.setState("paused" /* PAUSED */);
1289
+ this.emit("pause");
1290
+ } catch (error) {
1291
+ this.handleError(error);
1292
+ }
1293
+ }
1294
+ /**
1295
+ * Resume transcription
1296
+ */
1297
+ resume() {
1298
+ if (this.state !== "paused" /* PAUSED */) {
1299
+ return;
1300
+ }
1301
+ try {
1302
+ if (this.recognition) {
1303
+ this.recognition.start();
1304
+ }
1305
+ this.setState("active" /* ACTIVE */);
1306
+ this.emit("resume");
1307
+ } catch (error) {
1308
+ this.handleError(error);
1309
+ }
1310
+ }
1311
+ /**
1312
+ * Send audio data - not supported by Web Speech API
1313
+ * @param _audioData - Audio data (unused)
1314
+ */
1315
+ sendAudio(_audioData) {
1316
+ console.warn("WebSpeechProvider does not support external audio input. Audio data is captured directly from the microphone.");
1317
+ }
1318
+ /**
1319
+ * Clean up all resources
1320
+ */
1321
+ async cleanup() {
1322
+ this.isRestarting = false;
1323
+ if (this.recognition) {
1324
+ try {
1325
+ this.recognition.stop();
1326
+ } catch {
1327
+ }
1328
+ this.recognition.onstart = null;
1329
+ this.recognition.onend = null;
1330
+ this.recognition.onerror = null;
1331
+ this.recognition.onresult = null;
1332
+ this.recognition.onspeechstart = null;
1333
+ this.recognition.onspeechend = null;
1334
+ this.recognition = null;
1335
+ }
1336
+ this.stopAudioLevelMonitoring();
1337
+ if (this.audioContext) {
1338
+ try {
1339
+ await this.audioContext.close();
1340
+ } catch {
1341
+ }
1342
+ this.audioContext = null;
1343
+ this.analyser = null;
1344
+ }
1345
+ this.stopMediaStream();
1346
+ this.removeAllListeners();
1347
+ }
1348
+ /**
1349
+ * Get provider capabilities
1350
+ */
1351
+ getCapabilities() {
1352
+ return _WebSpeechProvider.capabilities;
1353
+ }
1354
+ // ==================== Private Methods ====================
1355
+ /**
1356
+ * Set up event handlers for speech recognition
1357
+ */
1358
+ setupEventHandlers() {
1359
+ if (!this.recognition) return;
1360
+ this.recognition.onstart = () => {
1361
+ this.setState("active" /* ACTIVE */);
1362
+ this.emit("start");
1363
+ };
1364
+ this.recognition.onend = () => {
1365
+ this.handleRecognitionEnd();
1366
+ };
1367
+ this.recognition.onerror = (event) => {
1368
+ this.handleRecognitionError(event);
1369
+ };
1370
+ this.recognition.onresult = (event) => {
1371
+ this.processRecognitionResult(event);
1372
+ };
1373
+ this.recognition.onspeechstart = () => {
1374
+ this.emit("speech");
1375
+ };
1376
+ this.recognition.onspeechend = () => {
1377
+ this.emit("silence");
1378
+ };
1379
+ }
1380
+ /**
1381
+ * Process speech recognition results
1382
+ */
1383
+ processRecognitionResult(event) {
1384
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1385
+ const result = event.results[i];
1386
+ const alternative = result[0];
1387
+ if (!alternative) continue;
1388
+ const transcriptionResult = {
1389
+ text: alternative.transcript,
1390
+ isFinal: result.isFinal,
1391
+ confidence: alternative.confidence,
1392
+ timestamp: Date.now(),
1393
+ language: this.config.language
1394
+ };
1395
+ this.handleTranscript(transcriptionResult);
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Handle recognition end event
1400
+ */
1401
+ handleRecognitionEnd() {
1402
+ if (this.state === "stopping" /* STOPPING */ || this.state === "stopped" /* STOPPED */ || this.state === "paused" /* PAUSED */ || this.state === "error" /* ERROR */) {
1403
+ return;
1404
+ }
1405
+ if (this.state === "active" /* ACTIVE */ && !this.isRestarting) {
1406
+ this.isRestarting = true;
1407
+ if (this.retryCount < this.maxRetries) {
1408
+ this.retryCount++;
1409
+ setTimeout(() => {
1410
+ if (this.recognition && this.state === "active" /* ACTIVE */) {
1411
+ try {
1412
+ this.recognition.start();
1413
+ } catch (error) {
1414
+ this.handleError(error);
1415
+ }
1416
+ }
1417
+ this.isRestarting = false;
1418
+ }, 100);
1419
+ } else {
1420
+ this.setState("stopped" /* STOPPED */);
1421
+ this.emit("stop");
1422
+ }
1423
+ }
1424
+ }
1425
+ /**
1426
+ * Handle recognition errors
1427
+ */
1428
+ handleRecognitionError(event) {
1429
+ let errorCode;
1430
+ let shouldStop = false;
1431
+ switch (event.error) {
1432
+ case "no-speech":
1433
+ console.warn("No speech detected");
1434
+ return;
1435
+ case "audio-capture":
1436
+ case "not-allowed":
1437
+ errorCode = "microphone_access_denied" /* MICROPHONE_ACCESS_DENIED */;
1438
+ shouldStop = true;
1439
+ break;
1440
+ case "network":
1441
+ errorCode = "network_error" /* NETWORK_ERROR */;
1442
+ break;
1443
+ case "aborted":
1444
+ return;
1445
+ case "service-not-allowed":
1446
+ errorCode = "unsupported_browser" /* UNSUPPORTED_BROWSER */;
1447
+ shouldStop = true;
1448
+ break;
1449
+ default:
1450
+ errorCode = "provider_error" /* PROVIDER_ERROR */;
1451
+ }
1452
+ const error = new TranscriptionError(
1453
+ event.message || `Speech recognition error: ${event.error}`,
1454
+ errorCode,
1455
+ "web-speech" /* WebSpeechAPI */,
1456
+ { originalError: event.error }
1457
+ );
1458
+ if (shouldStop) {
1459
+ this.handleError(error);
1460
+ } else {
1461
+ this.emit("error", error);
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Request microphone access
1466
+ */
1467
+ async getMicrophoneAccess() {
1468
+ try {
1469
+ const constraints = {
1470
+ audio: {
1471
+ echoCancellation: true,
1472
+ noiseSuppression: true,
1473
+ autoGainControl: true,
1474
+ sampleRate: this.config.audioConfig?.sampleRate || 16e3
1475
+ }
1476
+ };
1477
+ this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
1478
+ } catch (error) {
1479
+ const err = error;
1480
+ throw new TranscriptionError(
1481
+ err.message || "Microphone access denied",
1482
+ "microphone_access_denied" /* MICROPHONE_ACCESS_DENIED */,
1483
+ "web-speech" /* WebSpeechAPI */,
1484
+ error
1485
+ );
1486
+ }
1487
+ }
1488
+ /**
1489
+ * Stop media stream tracks
1490
+ */
1491
+ stopMediaStream() {
1492
+ if (this.mediaStream) {
1493
+ this.mediaStream.getTracks().forEach((track) => track.stop());
1494
+ this.mediaStream = null;
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Set up audio level monitoring for VAD
1499
+ */
1500
+ setupAudioLevelMonitoring() {
1501
+ if (!this.mediaStream) return;
1502
+ try {
1503
+ this.audioContext = new AudioContext();
1504
+ const source = this.audioContext.createMediaStreamSource(this.mediaStream);
1505
+ this.analyser = this.audioContext.createAnalyser();
1506
+ this.analyser.fftSize = 256;
1507
+ source.connect(this.analyser);
1508
+ const dataArray = new Uint8Array(this.analyser.frequencyBinCount);
1509
+ this.audioLevelInterval = setInterval(() => {
1510
+ if (this.analyser && this.state === "active" /* ACTIVE */) {
1511
+ this.analyser.getByteFrequencyData(dataArray);
1512
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
1513
+ const normalizedLevel = average / 255;
1514
+ this.emit("audioLevel", normalizedLevel);
1515
+ }
1516
+ }, 100);
1517
+ } catch (error) {
1518
+ console.warn("Failed to set up audio level monitoring:", error);
1519
+ }
1520
+ }
1521
+ /**
1522
+ * Stop audio level monitoring
1523
+ */
1524
+ stopAudioLevelMonitoring() {
1525
+ if (this.audioLevelInterval) {
1526
+ clearInterval(this.audioLevelInterval);
1527
+ this.audioLevelInterval = null;
1528
+ }
1529
+ }
1530
+ };
1531
+ /** Provider capabilities */
1532
+ _WebSpeechProvider.capabilities = {
1533
+ supportsInterim: true,
1534
+ supportsWordTimestamps: false,
1535
+ supportsSpeakerDiarization: false,
1536
+ supportsPunctuation: false,
1537
+ supportsLanguageDetection: false,
1538
+ supportedLanguages: [
1539
+ "en-US",
1540
+ "en-GB",
1541
+ "en-AU",
1542
+ "en-CA",
1543
+ "en-IN",
1544
+ "es-ES",
1545
+ "es-MX",
1546
+ "fr-FR",
1547
+ "de-DE",
1548
+ "it-IT",
1549
+ "pt-BR",
1550
+ "pt-PT",
1551
+ "ja-JP",
1552
+ "ko-KR",
1553
+ "zh-CN",
1554
+ "zh-TW",
1555
+ "ru-RU",
1556
+ "ar-SA",
1557
+ "hi-IN",
1558
+ "nl-NL"
1559
+ ]
1560
+ };
1561
+ var WebSpeechProvider = _WebSpeechProvider;
1562
+
1563
+ // src/providers/DeepgramProvider.ts
1564
+ var DEEPGRAM_WS_URL = "wss://api.deepgram.com/v1/listen";
1565
+ var KEEP_ALIVE_INTERVAL = 5e3;
1566
+ var RECONNECT_DELAY = 1e3;
1567
+ var _DeepgramProvider = class _DeepgramProvider extends BaseTranscriber {
1568
+ /**
1569
+ * Create a new DeepgramProvider
1570
+ * @param config - Transcription configuration with API key
1571
+ */
1572
+ constructor(config) {
1573
+ super({ ...config, provider: "deepgram" /* Deepgram */ });
1574
+ /** WebSocket connection */
1575
+ this.socket = null;
1576
+ /** Media stream from microphone */
1577
+ this.mediaStream = null;
1578
+ /** Audio context for processing */
1579
+ this.audioContext = null;
1580
+ /** Audio processor node */
1581
+ this.processor = null;
1582
+ /** Connection attempt counter */
1583
+ this.connectionAttempts = 0;
1584
+ /** Maximum reconnection attempts */
1585
+ this.maxRetries = 3;
1586
+ /** Reconnection timeout */
1587
+ this.reconnectTimeout = null;
1588
+ /** Keep-alive interval */
1589
+ this.keepAliveInterval = null;
1590
+ /** Flag indicating if connection is ready */
1591
+ this.isConnectionReady = false;
1592
+ /** Flag for intentional close */
1593
+ this.isIntentionalClose = false;
1594
+ }
1595
+ /**
1596
+ * Check if Deepgram provider is supported
1597
+ */
1598
+ isSupported() {
1599
+ if (typeof window === "undefined") {
1600
+ return typeof WebSocket !== "undefined";
1601
+ }
1602
+ return !!(typeof WebSocket !== "undefined" && navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && (window.AudioContext || window.webkitAudioContext));
1603
+ }
1604
+ /**
1605
+ * Initialize the Deepgram provider
1606
+ */
1607
+ async initialize() {
1608
+ if (!this.config.apiKey) {
1609
+ throw new TranscriptionError(
1610
+ "Deepgram API key is required",
1611
+ "invalid_config" /* INVALID_CONFIG */,
1612
+ "deepgram" /* Deepgram */
1613
+ );
1614
+ }
1615
+ if (!this.isSupported()) {
1616
+ throw new TranscriptionError(
1617
+ "Deepgram provider is not supported in this environment",
1618
+ "unsupported_browser" /* UNSUPPORTED_BROWSER */,
1619
+ "deepgram" /* Deepgram */
1620
+ );
1621
+ }
1622
+ this.setState("initializing" /* INITIALIZING */);
1623
+ this.isConnectionReady = false;
1624
+ this.isIntentionalClose = false;
1625
+ try {
1626
+ await this.setupWebSocket();
1627
+ } catch (error) {
1628
+ this.handleError(error);
1629
+ throw error;
1630
+ }
1631
+ }
1632
+ /**
1633
+ * Start transcription
1634
+ */
1635
+ async start() {
1636
+ if (!this.socket || !this.isConnectionReady) {
1637
+ await this.initialize();
1638
+ }
1639
+ if (this.state === "active" /* ACTIVE */) {
1640
+ return;
1641
+ }
1642
+ try {
1643
+ await this.getMicrophoneAccess();
1644
+ this.setupAudioProcessing();
1645
+ this.setState("active" /* ACTIVE */);
1646
+ this.emit("start");
1647
+ } catch (error) {
1648
+ this.handleError(error);
1649
+ throw error;
1650
+ }
1651
+ }
1652
+ /**
1653
+ * Stop transcription
1654
+ */
1655
+ async stop() {
1656
+ if (this.state === "stopped" /* STOPPED */ || this.state === "idle" /* IDLE */) {
1657
+ return;
1658
+ }
1659
+ this.setState("stopping" /* STOPPING */);
1660
+ this.isIntentionalClose = true;
1661
+ this.stopKeepAlive();
1662
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1663
+ try {
1664
+ this.socket.send(JSON.stringify({ type: "CloseStream" }));
1665
+ await new Promise((resolve) => setTimeout(resolve, 500));
1666
+ } catch {
1667
+ }
1668
+ }
1669
+ this.closeWebSocket();
1670
+ this.stopAudioProcessing();
1671
+ this.stopMediaStream();
1672
+ this.setState("stopped" /* STOPPED */);
1673
+ this.emit("stop");
1674
+ }
1675
+ /**
1676
+ * Pause transcription
1677
+ */
1678
+ pause() {
1679
+ if (this.state !== "active" /* ACTIVE */) {
1680
+ return;
1681
+ }
1682
+ this.stopAudioProcessing();
1683
+ this.setState("paused" /* PAUSED */);
1684
+ this.emit("pause");
1685
+ }
1686
+ /**
1687
+ * Resume transcription
1688
+ */
1689
+ resume() {
1690
+ if (this.state !== "paused" /* PAUSED */) {
1691
+ return;
1692
+ }
1693
+ if (this.mediaStream && this.audioContext) {
1694
+ this.setupAudioProcessing();
1695
+ }
1696
+ this.setState("active" /* ACTIVE */);
1697
+ this.emit("resume");
1698
+ }
1699
+ /**
1700
+ * Send audio data through WebSocket
1701
+ * @param audioData - Raw audio data as ArrayBuffer
1702
+ */
1703
+ sendAudio(audioData) {
1704
+ if (this.socket && this.socket.readyState === WebSocket.OPEN && this.state === "active" /* ACTIVE */) {
1705
+ this.socket.send(audioData);
1706
+ this.recordAudioData(audioData);
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Clean up all resources
1711
+ */
1712
+ async cleanup() {
1713
+ this.isIntentionalClose = true;
1714
+ this.stopKeepAlive();
1715
+ if (this.reconnectTimeout) {
1716
+ clearTimeout(this.reconnectTimeout);
1717
+ this.reconnectTimeout = null;
1718
+ }
1719
+ this.closeWebSocket();
1720
+ this.stopAudioProcessing();
1721
+ if (this.audioContext) {
1722
+ try {
1723
+ await this.audioContext.close();
1724
+ } catch {
1725
+ }
1726
+ this.audioContext = null;
1727
+ }
1728
+ this.stopMediaStream();
1729
+ this.connectionAttempts = 0;
1730
+ this.isConnectionReady = false;
1731
+ this.removeAllListeners();
1732
+ }
1733
+ /**
1734
+ * Get provider capabilities
1735
+ */
1736
+ getCapabilities() {
1737
+ return _DeepgramProvider.capabilities;
1738
+ }
1739
+ // ==================== Private Methods ====================
1740
+ /**
1741
+ * Build WebSocket URL with query parameters
1742
+ */
1743
+ buildWebSocketUrl() {
1744
+ const params = new URLSearchParams();
1745
+ const options = this.config.providerOptions || {};
1746
+ params.set("model", options.model || "nova-2");
1747
+ if (options.version) params.set("version", options.version);
1748
+ if (options.tier) params.set("tier", options.tier);
1749
+ if (this.config.language) {
1750
+ params.set("language", this.config.language);
1751
+ }
1752
+ params.set("punctuate", String(this.config.punctuation ?? options.punctuate ?? true));
1753
+ params.set("interim_results", String(this.config.interimResults ?? true));
1754
+ if (options.diarize) params.set("diarize", "true");
1755
+ if (options.multichannel) params.set("multichannel", "true");
1756
+ if (options.alternatives) params.set("alternatives", String(options.alternatives));
1757
+ if (options.numerals) params.set("numerals", "true");
1758
+ if (options.smartFormat) params.set("smart_format", "true");
1759
+ if (options.endpointing !== void 0) params.set("endpointing", String(options.endpointing));
1760
+ if (options.keywords?.length) {
1761
+ options.keywords.forEach((k) => params.append("keywords", k));
1762
+ }
1763
+ if (options.search?.length) {
1764
+ options.search.forEach((s) => params.append("search", s));
1765
+ }
1766
+ params.set("encoding", "linear16");
1767
+ params.set("sample_rate", String(this.config.audioConfig?.sampleRate || 16e3));
1768
+ params.set("channels", String(this.config.audioConfig?.channels || 1));
1769
+ return `${DEEPGRAM_WS_URL}?${params.toString()}`;
1770
+ }
1771
+ /**
1772
+ * Set up WebSocket connection
1773
+ */
1774
+ async setupWebSocket() {
1775
+ return new Promise((resolve, reject) => {
1776
+ const url = this.buildWebSocketUrl();
1777
+ this.socket = new WebSocket(url, ["token", this.config.apiKey]);
1778
+ this.socket.binaryType = "arraybuffer";
1779
+ const connectionTimeout = setTimeout(() => {
1780
+ reject(new TranscriptionError(
1781
+ "WebSocket connection timeout",
1782
+ "connection_failed" /* CONNECTION_FAILED */,
1783
+ "deepgram" /* Deepgram */
1784
+ ));
1785
+ }, 1e4);
1786
+ this.socket.onopen = () => {
1787
+ clearTimeout(connectionTimeout);
1788
+ this.handleWebSocketOpen();
1789
+ resolve();
1790
+ };
1791
+ this.socket.onmessage = (event) => {
1792
+ this.handleWebSocketMessage(event);
1793
+ };
1794
+ this.socket.onerror = (event) => {
1795
+ clearTimeout(connectionTimeout);
1796
+ this.handleWebSocketError(event);
1797
+ reject(new TranscriptionError(
1798
+ "WebSocket connection error",
1799
+ "connection_failed" /* CONNECTION_FAILED */,
1800
+ "deepgram" /* Deepgram */
1801
+ ));
1802
+ };
1803
+ this.socket.onclose = (event) => {
1804
+ clearTimeout(connectionTimeout);
1805
+ this.handleWebSocketClose(event);
1806
+ };
1807
+ });
1808
+ }
1809
+ /**
1810
+ * Handle WebSocket open event
1811
+ */
1812
+ handleWebSocketOpen() {
1813
+ this.isConnectionReady = true;
1814
+ this.connectionAttempts = 0;
1815
+ this.startKeepAlive();
1816
+ }
1817
+ /**
1818
+ * Handle incoming WebSocket messages
1819
+ */
1820
+ handleWebSocketMessage(event) {
1821
+ try {
1822
+ const message = JSON.parse(event.data);
1823
+ switch (message.type) {
1824
+ case "Results":
1825
+ this.processTranscriptionResult(message);
1826
+ break;
1827
+ case "Metadata":
1828
+ break;
1829
+ case "SpeechStarted":
1830
+ this.emit("speech");
1831
+ break;
1832
+ case "UtteranceEnd":
1833
+ this.emit("silence");
1834
+ break;
1835
+ case "Error":
1836
+ this.handleDeepgramError(message);
1837
+ break;
1838
+ }
1839
+ } catch (error) {
1840
+ console.error("Failed to parse Deepgram message:", error);
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Process transcription result from Deepgram
1845
+ */
1846
+ processTranscriptionResult(message) {
1847
+ if (!message.channel?.alternatives?.length) {
1848
+ return;
1849
+ }
1850
+ const alternative = message.channel.alternatives[0];
1851
+ const isFinal = message.is_final ?? message.speech_final ?? false;
1852
+ if (!alternative.transcript && !isFinal) {
1853
+ return;
1854
+ }
1855
+ const words = alternative.words?.map((w) => ({
1856
+ text: w.word,
1857
+ start: Math.round(w.start * 1e3),
1858
+ // Convert to milliseconds
1859
+ end: Math.round(w.end * 1e3),
1860
+ confidence: w.confidence
1861
+ }));
1862
+ const result = {
1863
+ text: alternative.transcript,
1864
+ isFinal,
1865
+ confidence: alternative.confidence,
1866
+ timestamp: Date.now(),
1867
+ words,
1868
+ language: this.config.language
1869
+ };
1870
+ this.handleTranscript(result);
1871
+ }
1872
+ /**
1873
+ * Handle Deepgram-specific errors
1874
+ */
1875
+ handleDeepgramError(message) {
1876
+ let errorCode = "provider_error" /* PROVIDER_ERROR */;
1877
+ const errorMessage = message.error?.message || "Unknown Deepgram error";
1878
+ if (errorMessage.toLowerCase().includes("unauthorized") || errorMessage.toLowerCase().includes("invalid api key")) {
1879
+ errorCode = "authentication_failed" /* AUTHENTICATION_FAILED */;
1880
+ } else if (errorMessage.toLowerCase().includes("rate limit")) {
1881
+ errorCode = "provider_error" /* PROVIDER_ERROR */;
1882
+ }
1883
+ const error = new TranscriptionError(
1884
+ errorMessage,
1885
+ errorCode,
1886
+ "deepgram" /* Deepgram */,
1887
+ message.error
1888
+ );
1889
+ this.emit("error", error);
1890
+ }
1891
+ /**
1892
+ * Handle WebSocket error
1893
+ */
1894
+ handleWebSocketError(_event) {
1895
+ if (!this.isIntentionalClose && this.connectionAttempts < this.maxRetries) {
1896
+ this.reconnect();
1897
+ } else {
1898
+ const error = new TranscriptionError(
1899
+ "WebSocket connection error",
1900
+ "connection_failed" /* CONNECTION_FAILED */,
1901
+ "deepgram" /* Deepgram */
1902
+ );
1903
+ this.handleError(error);
1904
+ }
1905
+ }
1906
+ /**
1907
+ * Handle WebSocket close
1908
+ */
1909
+ handleWebSocketClose(event) {
1910
+ this.isConnectionReady = false;
1911
+ if (!this.isIntentionalClose && this.state === "active" /* ACTIVE */) {
1912
+ if (this.connectionAttempts < this.maxRetries) {
1913
+ this.reconnect();
1914
+ } else {
1915
+ const error = new TranscriptionError(
1916
+ `WebSocket closed unexpectedly: ${event.code} ${event.reason}`,
1917
+ "connection_failed" /* CONNECTION_FAILED */,
1918
+ "deepgram" /* Deepgram */
1919
+ );
1920
+ this.handleError(error);
1921
+ }
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Attempt to reconnect
1926
+ */
1927
+ reconnect() {
1928
+ this.connectionAttempts++;
1929
+ const delay = RECONNECT_DELAY * Math.pow(2, this.connectionAttempts - 1);
1930
+ this.reconnectTimeout = setTimeout(async () => {
1931
+ try {
1932
+ await this.setupWebSocket();
1933
+ if (this.mediaStream) {
1934
+ this.setupAudioProcessing();
1935
+ }
1936
+ } catch (error) {
1937
+ if (this.connectionAttempts < this.maxRetries) {
1938
+ this.reconnect();
1939
+ } else {
1940
+ this.handleError(error);
1941
+ }
1942
+ }
1943
+ }, delay);
1944
+ }
1945
+ /**
1946
+ * Start keep-alive interval
1947
+ */
1948
+ startKeepAlive() {
1949
+ this.keepAliveInterval = setInterval(() => {
1950
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1951
+ this.socket.send(JSON.stringify({ type: "KeepAlive" }));
1952
+ }
1953
+ }, KEEP_ALIVE_INTERVAL);
1954
+ }
1955
+ /**
1956
+ * Stop keep-alive interval
1957
+ */
1958
+ stopKeepAlive() {
1959
+ if (this.keepAliveInterval) {
1960
+ clearInterval(this.keepAliveInterval);
1961
+ this.keepAliveInterval = null;
1962
+ }
1963
+ }
1964
+ /**
1965
+ * Request microphone access
1966
+ */
1967
+ async getMicrophoneAccess() {
1968
+ try {
1969
+ const sampleRate = this.config.audioConfig?.sampleRate || 16e3;
1970
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({
1971
+ audio: {
1972
+ sampleRate,
1973
+ channelCount: this.config.audioConfig?.channels || 1,
1974
+ echoCancellation: true,
1975
+ noiseSuppression: true,
1976
+ autoGainControl: true
1977
+ }
1978
+ });
1979
+ } catch (error) {
1980
+ throw new TranscriptionError(
1981
+ "Microphone access denied",
1982
+ "microphone_access_denied" /* MICROPHONE_ACCESS_DENIED */,
1983
+ "deepgram" /* Deepgram */,
1984
+ error
1985
+ );
1986
+ }
1987
+ }
1988
+ /**
1989
+ * Set up audio processing pipeline
1990
+ */
1991
+ setupAudioProcessing() {
1992
+ if (!this.mediaStream) return;
1993
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1994
+ this.audioContext = new AudioContextClass({
1995
+ sampleRate: this.config.audioConfig?.sampleRate || 16e3
1996
+ });
1997
+ const source = this.audioContext.createMediaStreamSource(this.mediaStream);
1998
+ this.processor = this.audioContext.createScriptProcessor(4096, 1, 1);
1999
+ this.processor.onaudioprocess = (event) => {
2000
+ if (this.state !== "active" /* ACTIVE */) return;
2001
+ const inputData = event.inputBuffer.getChannelData(0);
2002
+ const int16Data = this.convertFloat32ToInt16(inputData);
2003
+ this.sendAudio(int16Data.buffer);
2004
+ };
2005
+ source.connect(this.processor);
2006
+ this.processor.connect(this.audioContext.destination);
2007
+ }
2008
+ /**
2009
+ * Stop audio processing
2010
+ */
2011
+ stopAudioProcessing() {
2012
+ if (this.processor) {
2013
+ this.processor.disconnect();
2014
+ this.processor.onaudioprocess = null;
2015
+ this.processor = null;
2016
+ }
2017
+ }
2018
+ /**
2019
+ * Convert Float32 audio samples to Int16
2020
+ */
2021
+ convertFloat32ToInt16(buffer) {
2022
+ const int16 = new Int16Array(buffer.length);
2023
+ for (let i = 0; i < buffer.length; i++) {
2024
+ const s = Math.max(-1, Math.min(1, buffer[i]));
2025
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
2026
+ }
2027
+ return int16;
2028
+ }
2029
+ /**
2030
+ * Close WebSocket connection
2031
+ */
2032
+ closeWebSocket() {
2033
+ if (this.socket) {
2034
+ this.socket.onopen = null;
2035
+ this.socket.onmessage = null;
2036
+ this.socket.onerror = null;
2037
+ this.socket.onclose = null;
2038
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
2039
+ this.socket.close();
2040
+ }
2041
+ this.socket = null;
2042
+ }
2043
+ }
2044
+ /**
2045
+ * Stop media stream tracks
2046
+ */
2047
+ stopMediaStream() {
2048
+ if (this.mediaStream) {
2049
+ this.mediaStream.getTracks().forEach((track) => track.stop());
2050
+ this.mediaStream = null;
2051
+ }
2052
+ }
2053
+ };
2054
+ /** Provider capabilities */
2055
+ _DeepgramProvider.capabilities = {
2056
+ supportsInterim: true,
2057
+ supportsWordTimestamps: true,
2058
+ supportsSpeakerDiarization: true,
2059
+ supportsPunctuation: true,
2060
+ supportsLanguageDetection: true,
2061
+ supportedLanguages: [
2062
+ "en",
2063
+ "en-US",
2064
+ "en-GB",
2065
+ "en-AU",
2066
+ "en-IN",
2067
+ "es",
2068
+ "es-ES",
2069
+ "es-419",
2070
+ "fr",
2071
+ "fr-FR",
2072
+ "fr-CA",
2073
+ "de",
2074
+ "de-DE",
2075
+ "it",
2076
+ "it-IT",
2077
+ "pt",
2078
+ "pt-BR",
2079
+ "pt-PT",
2080
+ "nl",
2081
+ "nl-NL",
2082
+ "ja",
2083
+ "ja-JP",
2084
+ "ko",
2085
+ "ko-KR",
2086
+ "zh",
2087
+ "zh-CN",
2088
+ "zh-TW",
2089
+ "ru",
2090
+ "ru-RU",
2091
+ "uk",
2092
+ "uk-UA",
2093
+ "hi",
2094
+ "hi-IN",
2095
+ "tr",
2096
+ "tr-TR",
2097
+ "pl",
2098
+ "pl-PL",
2099
+ "sv",
2100
+ "sv-SE",
2101
+ "da",
2102
+ "da-DK",
2103
+ "no",
2104
+ "no-NO",
2105
+ "fi",
2106
+ "fi-FI"
2107
+ ]
2108
+ };
2109
+ var DeepgramProvider = _DeepgramProvider;
2110
+
2111
+ // src/providers/AssemblyAIProvider.ts
2112
+ var ASSEMBLYAI_AUTH_URL = "https://api.assemblyai.com/v2/realtime/token";
2113
+ var ASSEMBLYAI_WS_URL = "wss://api.assemblyai.com/v2/realtime/ws";
2114
+ var SAMPLE_RATE = 16e3;
2115
+ var RECONNECT_DELAY2 = 1e3;
2116
+ var _AssemblyAIProvider = class _AssemblyAIProvider extends BaseTranscriber {
2117
+ /**
2118
+ * Create a new AssemblyAIProvider
2119
+ * @param config - Transcription configuration with API key
2120
+ */
2121
+ constructor(config) {
2122
+ super({ ...config, provider: "assemblyai" /* AssemblyAI */ });
2123
+ /** WebSocket connection */
2124
+ this.socket = null;
2125
+ /** Media stream from microphone */
2126
+ this.mediaStream = null;
2127
+ /** Audio context for processing */
2128
+ this.audioContext = null;
2129
+ /** Audio processor node */
2130
+ this.processor = null;
2131
+ /** Session token for WebSocket authentication */
2132
+ this.sessionToken = null;
2133
+ /** Connection attempt counter */
2134
+ this.connectionAttempts = 0;
2135
+ /** Maximum reconnection attempts */
2136
+ this.maxRetries = 3;
2137
+ /** Reconnection timeout */
2138
+ this.reconnectTimeout = null;
2139
+ /** Flag indicating if connection is ready */
2140
+ this.isConnectionReady = false;
2141
+ /** Flag for intentional close */
2142
+ this.isIntentionalClose = false;
2143
+ /** Session ID from AssemblyAI */
2144
+ this.sessionId = null;
2145
+ }
2146
+ /**
2147
+ * Check if AssemblyAI provider is supported
2148
+ */
2149
+ isSupported() {
2150
+ if (typeof window === "undefined") {
2151
+ return typeof WebSocket !== "undefined" && typeof fetch !== "undefined";
2152
+ }
2153
+ return !!(typeof WebSocket !== "undefined" && typeof fetch !== "undefined" && navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && (window.AudioContext || window.webkitAudioContext));
2154
+ }
2155
+ /**
2156
+ * Initialize the AssemblyAI provider
2157
+ */
2158
+ async initialize() {
2159
+ if (!this.config.apiKey) {
2160
+ throw new TranscriptionError(
2161
+ "AssemblyAI API key is required",
2162
+ "invalid_config" /* INVALID_CONFIG */,
2163
+ "assemblyai" /* AssemblyAI */
2164
+ );
2165
+ }
2166
+ if (!this.isSupported()) {
2167
+ throw new TranscriptionError(
2168
+ "AssemblyAI provider is not supported in this environment",
2169
+ "unsupported_browser" /* UNSUPPORTED_BROWSER */,
2170
+ "assemblyai" /* AssemblyAI */
2171
+ );
2172
+ }
2173
+ this.setState("initializing" /* INITIALIZING */);
2174
+ this.isConnectionReady = false;
2175
+ this.isIntentionalClose = false;
2176
+ try {
2177
+ this.sessionToken = await this.getSessionToken();
2178
+ await this.setupWebSocket();
2179
+ } catch (error) {
2180
+ this.handleError(error);
2181
+ throw error;
2182
+ }
2183
+ }
2184
+ /**
2185
+ * Start transcription
2186
+ */
2187
+ async start() {
2188
+ if (!this.socket || !this.isConnectionReady) {
2189
+ await this.initialize();
2190
+ }
2191
+ if (this.state === "active" /* ACTIVE */) {
2192
+ return;
2193
+ }
2194
+ try {
2195
+ await this.getMicrophoneAccess();
2196
+ this.setupAudioProcessing();
2197
+ this.setState("active" /* ACTIVE */);
2198
+ this.emit("start");
2199
+ } catch (error) {
2200
+ this.handleError(error);
2201
+ throw error;
2202
+ }
2203
+ }
2204
+ /**
2205
+ * Stop transcription
2206
+ */
2207
+ async stop() {
2208
+ if (this.state === "stopped" /* STOPPED */ || this.state === "idle" /* IDLE */) {
2209
+ return;
2210
+ }
2211
+ this.setState("stopping" /* STOPPING */);
2212
+ this.isIntentionalClose = true;
2213
+ this.sendTerminateMessage();
2214
+ await new Promise((resolve) => setTimeout(resolve, 500));
2215
+ this.closeWebSocket();
2216
+ this.stopAudioProcessing();
2217
+ this.stopMediaStream();
2218
+ this.setState("stopped" /* STOPPED */);
2219
+ this.emit("stop");
2220
+ }
2221
+ /**
2222
+ * Pause transcription
2223
+ */
2224
+ pause() {
2225
+ if (this.state !== "active" /* ACTIVE */) {
2226
+ return;
2227
+ }
2228
+ this.stopAudioProcessing();
2229
+ this.setState("paused" /* PAUSED */);
2230
+ this.emit("pause");
2231
+ }
2232
+ /**
2233
+ * Resume transcription
2234
+ */
2235
+ resume() {
2236
+ if (this.state !== "paused" /* PAUSED */) {
2237
+ return;
2238
+ }
2239
+ if (this.mediaStream && this.audioContext) {
2240
+ this.setupAudioProcessing();
2241
+ }
2242
+ this.setState("active" /* ACTIVE */);
2243
+ this.emit("resume");
2244
+ }
2245
+ /**
2246
+ * Send audio data through WebSocket
2247
+ * @param audioData - Raw audio data as ArrayBuffer
2248
+ */
2249
+ sendAudio(audioData) {
2250
+ if (this.socket && this.socket.readyState === WebSocket.OPEN && this.state === "active" /* ACTIVE */) {
2251
+ const base64Audio = this.encodeAudioToBase64(audioData);
2252
+ this.sendAudioMessage(base64Audio);
2253
+ this.recordAudioData(audioData);
2254
+ }
2255
+ }
2256
+ /**
2257
+ * Clean up all resources
2258
+ */
2259
+ async cleanup() {
2260
+ this.isIntentionalClose = true;
2261
+ if (this.reconnectTimeout) {
2262
+ clearTimeout(this.reconnectTimeout);
2263
+ this.reconnectTimeout = null;
2264
+ }
2265
+ this.closeWebSocket();
2266
+ this.stopAudioProcessing();
2267
+ if (this.audioContext) {
2268
+ try {
2269
+ await this.audioContext.close();
2270
+ } catch {
2271
+ }
2272
+ this.audioContext = null;
2273
+ }
2274
+ this.stopMediaStream();
2275
+ this.sessionToken = null;
2276
+ this.sessionId = null;
2277
+ this.connectionAttempts = 0;
2278
+ this.isConnectionReady = false;
2279
+ this.removeAllListeners();
2280
+ }
2281
+ /**
2282
+ * Get provider capabilities
2283
+ */
2284
+ getCapabilities() {
2285
+ return _AssemblyAIProvider.capabilities;
2286
+ }
2287
+ // ==================== Private Methods ====================
2288
+ /**
2289
+ * Get temporary session token from AssemblyAI
2290
+ */
2291
+ async getSessionToken() {
2292
+ try {
2293
+ const response = await fetch(ASSEMBLYAI_AUTH_URL, {
2294
+ method: "POST",
2295
+ headers: {
2296
+ "authorization": this.config.apiKey,
2297
+ "Content-Type": "application/json"
2298
+ },
2299
+ body: JSON.stringify({ expires_in: 3600 })
2300
+ });
2301
+ if (!response.ok) {
2302
+ if (response.status === 401) {
2303
+ throw new TranscriptionError(
2304
+ "Invalid AssemblyAI API key",
2305
+ "authentication_failed" /* AUTHENTICATION_FAILED */,
2306
+ "assemblyai" /* AssemblyAI */
2307
+ );
2308
+ }
2309
+ throw new TranscriptionError(
2310
+ `Failed to get session token: ${response.statusText}`,
2311
+ "provider_error" /* PROVIDER_ERROR */,
2312
+ "assemblyai" /* AssemblyAI */
2313
+ );
2314
+ }
2315
+ const data = await response.json();
2316
+ return data.token;
2317
+ } catch (error) {
2318
+ if (error instanceof TranscriptionError) {
2319
+ throw error;
2320
+ }
2321
+ throw new TranscriptionError(
2322
+ "Failed to authenticate with AssemblyAI",
2323
+ "network_error" /* NETWORK_ERROR */,
2324
+ "assemblyai" /* AssemblyAI */,
2325
+ error
2326
+ );
2327
+ }
2328
+ }
2329
+ /**
2330
+ * Set up WebSocket connection
2331
+ */
2332
+ async setupWebSocket() {
2333
+ return new Promise((resolve, reject) => {
2334
+ const url = `${ASSEMBLYAI_WS_URL}?sample_rate=${SAMPLE_RATE}&token=${this.sessionToken}`;
2335
+ this.socket = new WebSocket(url);
2336
+ const connectionTimeout = setTimeout(() => {
2337
+ reject(new TranscriptionError(
2338
+ "WebSocket connection timeout",
2339
+ "connection_failed" /* CONNECTION_FAILED */,
2340
+ "assemblyai" /* AssemblyAI */
2341
+ ));
2342
+ }, 1e4);
2343
+ this.socket.onopen = () => {
2344
+ clearTimeout(connectionTimeout);
2345
+ this.handleWebSocketOpen();
2346
+ resolve();
2347
+ };
2348
+ this.socket.onmessage = (event) => {
2349
+ this.handleWebSocketMessage(event);
2350
+ };
2351
+ this.socket.onerror = (event) => {
2352
+ clearTimeout(connectionTimeout);
2353
+ this.handleWebSocketError(event);
2354
+ reject(new TranscriptionError(
2355
+ "WebSocket connection error",
2356
+ "connection_failed" /* CONNECTION_FAILED */,
2357
+ "assemblyai" /* AssemblyAI */
2358
+ ));
2359
+ };
2360
+ this.socket.onclose = (event) => {
2361
+ clearTimeout(connectionTimeout);
2362
+ this.handleWebSocketClose(event);
2363
+ };
2364
+ });
2365
+ }
2366
+ /**
2367
+ * Handle WebSocket open event
2368
+ */
2369
+ handleWebSocketOpen() {
2370
+ this.connectionAttempts = 0;
2371
+ }
2372
+ /**
2373
+ * Handle incoming WebSocket messages
2374
+ */
2375
+ handleWebSocketMessage(event) {
2376
+ try {
2377
+ const message = JSON.parse(event.data);
2378
+ switch (message.message_type) {
2379
+ case "SessionBegins":
2380
+ this.handleSessionBegins(message);
2381
+ break;
2382
+ case "PartialTranscript":
2383
+ this.handlePartialTranscript(message);
2384
+ break;
2385
+ case "FinalTranscript":
2386
+ this.handleFinalTranscript(message);
2387
+ break;
2388
+ case "SessionTerminated":
2389
+ this.handleSessionTerminated();
2390
+ break;
2391
+ case "error":
2392
+ this.handleAssemblyAIError(message);
2393
+ break;
2394
+ }
2395
+ } catch (error) {
2396
+ console.error("Failed to parse AssemblyAI message:", error);
2397
+ }
2398
+ }
2399
+ /**
2400
+ * Handle SessionBegins message
2401
+ */
2402
+ handleSessionBegins(message) {
2403
+ this.sessionId = message.session_id || null;
2404
+ this.isConnectionReady = true;
2405
+ }
2406
+ /**
2407
+ * Handle partial (interim) transcript
2408
+ */
2409
+ handlePartialTranscript(message) {
2410
+ if (!message.text) return;
2411
+ const result = {
2412
+ text: message.text,
2413
+ isFinal: false,
2414
+ confidence: message.confidence,
2415
+ timestamp: Date.now(),
2416
+ language: this.config.language
2417
+ };
2418
+ this.handleTranscript(result);
2419
+ }
2420
+ /**
2421
+ * Handle final transcript
2422
+ */
2423
+ handleFinalTranscript(message) {
2424
+ if (!message.text) return;
2425
+ const words = message.words?.map((w) => ({
2426
+ text: w.text,
2427
+ start: w.start,
2428
+ end: w.end,
2429
+ confidence: w.confidence
2430
+ }));
2431
+ const result = {
2432
+ text: message.text,
2433
+ isFinal: true,
2434
+ confidence: message.confidence,
2435
+ timestamp: Date.now(),
2436
+ words,
2437
+ language: this.config.language
2438
+ };
2439
+ this.handleTranscript(result);
2440
+ }
2441
+ /**
2442
+ * Handle session terminated message
2443
+ */
2444
+ handleSessionTerminated() {
2445
+ this.isConnectionReady = false;
2446
+ if (!this.isIntentionalClose) {
2447
+ this.setState("stopped" /* STOPPED */);
2448
+ this.emit("stop");
2449
+ }
2450
+ }
2451
+ /**
2452
+ * Handle AssemblyAI-specific errors
2453
+ */
2454
+ handleAssemblyAIError(message) {
2455
+ let errorCode = "provider_error" /* PROVIDER_ERROR */;
2456
+ const errorMessage = message.error || "Unknown AssemblyAI error";
2457
+ if (errorMessage.toLowerCase().includes("unauthorized") || errorMessage.toLowerCase().includes("invalid token")) {
2458
+ errorCode = "authentication_failed" /* AUTHENTICATION_FAILED */;
2459
+ } else if (errorMessage.toLowerCase().includes("insufficient credits")) {
2460
+ errorCode = "provider_error" /* PROVIDER_ERROR */;
2461
+ } else if (errorMessage.toLowerCase().includes("session expired")) {
2462
+ errorCode = "session_expired" /* SESSION_EXPIRED */;
2463
+ }
2464
+ const error = new TranscriptionError(
2465
+ errorMessage,
2466
+ errorCode,
2467
+ "assemblyai" /* AssemblyAI */,
2468
+ message
2469
+ );
2470
+ this.emit("error", error);
2471
+ }
2472
+ /**
2473
+ * Handle WebSocket error
2474
+ */
2475
+ handleWebSocketError(_event) {
2476
+ if (!this.isIntentionalClose && this.connectionAttempts < this.maxRetries) {
2477
+ this.reconnect();
2478
+ } else {
2479
+ const error = new TranscriptionError(
2480
+ "WebSocket connection error",
2481
+ "connection_failed" /* CONNECTION_FAILED */,
2482
+ "assemblyai" /* AssemblyAI */
2483
+ );
2484
+ this.handleError(error);
2485
+ }
2486
+ }
2487
+ /**
2488
+ * Handle WebSocket close
2489
+ */
2490
+ handleWebSocketClose(event) {
2491
+ this.isConnectionReady = false;
2492
+ if (!this.isIntentionalClose && this.state === "active" /* ACTIVE */) {
2493
+ if (this.connectionAttempts < this.maxRetries) {
2494
+ this.reconnect();
2495
+ } else {
2496
+ const error = new TranscriptionError(
2497
+ `WebSocket closed unexpectedly: ${event.code} ${event.reason}`,
2498
+ "connection_failed" /* CONNECTION_FAILED */,
2499
+ "assemblyai" /* AssemblyAI */
2500
+ );
2501
+ this.handleError(error);
2502
+ }
2503
+ }
2504
+ }
2505
+ /**
2506
+ * Attempt to reconnect
2507
+ */
2508
+ reconnect() {
2509
+ this.connectionAttempts++;
2510
+ const delay = RECONNECT_DELAY2 * Math.pow(2, this.connectionAttempts - 1);
2511
+ this.reconnectTimeout = setTimeout(async () => {
2512
+ try {
2513
+ this.sessionToken = await this.getSessionToken();
2514
+ await this.setupWebSocket();
2515
+ if (this.mediaStream) {
2516
+ this.setupAudioProcessing();
2517
+ }
2518
+ } catch (error) {
2519
+ if (this.connectionAttempts < this.maxRetries) {
2520
+ this.reconnect();
2521
+ } else {
2522
+ this.handleError(error);
2523
+ }
2524
+ }
2525
+ }, delay);
2526
+ }
2527
+ /**
2528
+ * Request microphone access
2529
+ */
2530
+ async getMicrophoneAccess() {
2531
+ try {
2532
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({
2533
+ audio: {
2534
+ sampleRate: SAMPLE_RATE,
2535
+ channelCount: 1,
2536
+ echoCancellation: true,
2537
+ noiseSuppression: true,
2538
+ autoGainControl: true
2539
+ }
2540
+ });
2541
+ } catch (error) {
2542
+ throw new TranscriptionError(
2543
+ "Microphone access denied",
2544
+ "microphone_access_denied" /* MICROPHONE_ACCESS_DENIED */,
2545
+ "assemblyai" /* AssemblyAI */,
2546
+ error
2547
+ );
2548
+ }
2549
+ }
2550
+ /**
2551
+ * Set up audio processing pipeline
2552
+ */
2553
+ setupAudioProcessing() {
2554
+ if (!this.mediaStream) return;
2555
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
2556
+ this.audioContext = new AudioContextClass({ sampleRate: SAMPLE_RATE });
2557
+ const source = this.audioContext.createMediaStreamSource(this.mediaStream);
2558
+ this.processor = this.audioContext.createScriptProcessor(4096, 1, 1);
2559
+ this.processor.onaudioprocess = (event) => {
2560
+ if (this.state !== "active" /* ACTIVE */) return;
2561
+ const inputData = event.inputBuffer.getChannelData(0);
2562
+ const pcmData = this.convertFloat32ToPCM16(inputData);
2563
+ this.sendAudio(pcmData);
2564
+ };
2565
+ source.connect(this.processor);
2566
+ this.processor.connect(this.audioContext.destination);
2567
+ }
2568
+ /**
2569
+ * Stop audio processing
2570
+ */
2571
+ stopAudioProcessing() {
2572
+ if (this.processor) {
2573
+ this.processor.disconnect();
2574
+ this.processor.onaudioprocess = null;
2575
+ this.processor = null;
2576
+ }
2577
+ }
2578
+ /**
2579
+ * Convert Float32 audio samples to PCM16 (Int16)
2580
+ */
2581
+ convertFloat32ToPCM16(float32Array) {
2582
+ const int16 = new Int16Array(float32Array.length);
2583
+ for (let i = 0; i < float32Array.length; i++) {
2584
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
2585
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
2586
+ }
2587
+ return int16.buffer;
2588
+ }
2589
+ /**
2590
+ * Encode ArrayBuffer to base64
2591
+ */
2592
+ encodeAudioToBase64(arrayBuffer) {
2593
+ const bytes = new Uint8Array(arrayBuffer);
2594
+ let binary = "";
2595
+ for (let i = 0; i < bytes.byteLength; i++) {
2596
+ binary += String.fromCharCode(bytes[i]);
2597
+ }
2598
+ return btoa(binary);
2599
+ }
2600
+ /**
2601
+ * Send audio data message
2602
+ */
2603
+ sendAudioMessage(base64Audio) {
2604
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
2605
+ this.socket.send(JSON.stringify({ audio_data: base64Audio }));
2606
+ }
2607
+ }
2608
+ /**
2609
+ * Send terminate session message
2610
+ */
2611
+ sendTerminateMessage() {
2612
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
2613
+ this.socket.send(JSON.stringify({ terminate_session: true }));
2614
+ }
2615
+ }
2616
+ /**
2617
+ * Close WebSocket connection
2618
+ */
2619
+ closeWebSocket() {
2620
+ if (this.socket) {
2621
+ this.socket.onopen = null;
2622
+ this.socket.onmessage = null;
2623
+ this.socket.onerror = null;
2624
+ this.socket.onclose = null;
2625
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
2626
+ this.socket.close();
2627
+ }
2628
+ this.socket = null;
2629
+ }
2630
+ }
2631
+ /**
2632
+ * Stop media stream tracks
2633
+ */
2634
+ stopMediaStream() {
2635
+ if (this.mediaStream) {
2636
+ this.mediaStream.getTracks().forEach((track) => track.stop());
2637
+ this.mediaStream = null;
2638
+ }
2639
+ }
2640
+ };
2641
+ /** Provider capabilities */
2642
+ _AssemblyAIProvider.capabilities = {
2643
+ supportsInterim: true,
2644
+ supportsWordTimestamps: true,
2645
+ supportsSpeakerDiarization: true,
2646
+ supportsPunctuation: true,
2647
+ supportsLanguageDetection: false,
2648
+ supportedLanguages: ["en", "en-US", "en-GB", "en-AU"]
2649
+ };
2650
+ var AssemblyAIProvider = _AssemblyAIProvider;
2651
+
2652
+ // src/utils/AudioProcessor.ts
2653
+ var AudioProcessor = class {
2654
+ /**
2655
+ * Convert Float32 audio samples to Int16
2656
+ * @param buffer - Input Float32Array
2657
+ * @returns Int16Array of converted samples
2658
+ */
2659
+ static convertFloat32ToInt16(buffer) {
2660
+ const int16 = new Int16Array(buffer.length);
2661
+ for (let i = 0; i < buffer.length; i++) {
2662
+ const s = Math.max(-1, Math.min(1, buffer[i]));
2663
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
2664
+ }
2665
+ return int16;
2666
+ }
2667
+ /**
2668
+ * Convert Int16 audio samples to Float32
2669
+ * @param buffer - Input Int16Array
2670
+ * @returns Float32Array of converted samples
2671
+ */
2672
+ static convertInt16ToFloat32(buffer) {
2673
+ const float32 = new Float32Array(buffer.length);
2674
+ for (let i = 0; i < buffer.length; i++) {
2675
+ float32[i] = buffer[i] / 32768;
2676
+ }
2677
+ return float32;
2678
+ }
2679
+ /**
2680
+ * Resample audio buffer to different sample rate
2681
+ * Uses linear interpolation
2682
+ * @param buffer - Input audio buffer
2683
+ * @param fromRate - Source sample rate
2684
+ * @param toRate - Target sample rate
2685
+ * @returns Resampled Float32Array
2686
+ */
2687
+ static resampleBuffer(buffer, fromRate, toRate) {
2688
+ if (fromRate === toRate) {
2689
+ return buffer;
2690
+ }
2691
+ const ratio = toRate / fromRate;
2692
+ const newLength = Math.round(buffer.length * ratio);
2693
+ const output = new Float32Array(newLength);
2694
+ for (let i = 0; i < newLength; i++) {
2695
+ const srcPosition = i / ratio;
2696
+ const srcIndex = Math.floor(srcPosition);
2697
+ const fraction = srcPosition - srcIndex;
2698
+ const sample1 = buffer[srcIndex] || 0;
2699
+ const sample2 = buffer[srcIndex + 1] || sample1;
2700
+ output[i] = sample1 + (sample2 - sample1) * fraction;
2701
+ }
2702
+ return output;
2703
+ }
2704
+ /**
2705
+ * Downsample audio buffer (optimized for reducing sample rate)
2706
+ * @param buffer - Input audio buffer
2707
+ * @param fromRate - Source sample rate
2708
+ * @param toRate - Target sample rate
2709
+ * @returns Downsampled Float32Array
2710
+ */
2711
+ static downsampleBuffer(buffer, fromRate, toRate) {
2712
+ if (fromRate <= toRate) {
2713
+ return buffer;
2714
+ }
2715
+ const ratio = fromRate / toRate;
2716
+ const newLength = Math.ceil(buffer.length / ratio);
2717
+ const output = new Float32Array(newLength);
2718
+ for (let i = 0; i < newLength; i++) {
2719
+ const startIdx = Math.floor(i * ratio);
2720
+ const endIdx = Math.min(Math.floor((i + 1) * ratio), buffer.length);
2721
+ let sum = 0;
2722
+ const count = endIdx - startIdx;
2723
+ for (let j = startIdx; j < endIdx; j++) {
2724
+ sum += buffer[j];
2725
+ }
2726
+ output[i] = count > 0 ? sum / count : 0;
2727
+ }
2728
+ return output;
2729
+ }
2730
+ /**
2731
+ * Upsample audio buffer (optimized for increasing sample rate)
2732
+ * @param buffer - Input audio buffer
2733
+ * @param fromRate - Source sample rate
2734
+ * @param toRate - Target sample rate
2735
+ * @returns Upsampled Float32Array
2736
+ */
2737
+ static upsampleBuffer(buffer, fromRate, toRate) {
2738
+ if (fromRate >= toRate) {
2739
+ return buffer;
2740
+ }
2741
+ return this.resampleBuffer(buffer, fromRate, toRate);
2742
+ }
2743
+ /**
2744
+ * Normalize audio buffer to peak amplitude of 1.0
2745
+ * @param buffer - Input audio buffer
2746
+ * @returns Normalized Float32Array
2747
+ */
2748
+ static normalizeBuffer(buffer) {
2749
+ let peak = 0;
2750
+ for (let i = 0; i < buffer.length; i++) {
2751
+ const abs = Math.abs(buffer[i]);
2752
+ if (abs > peak) peak = abs;
2753
+ }
2754
+ if (peak === 0) {
2755
+ return buffer;
2756
+ }
2757
+ const output = new Float32Array(buffer.length);
2758
+ for (let i = 0; i < buffer.length; i++) {
2759
+ output[i] = buffer[i] / peak;
2760
+ }
2761
+ return output;
2762
+ }
2763
+ /**
2764
+ * Apply gain to audio buffer
2765
+ * @param buffer - Input audio buffer
2766
+ * @param gain - Gain multiplier
2767
+ * @returns Processed Float32Array
2768
+ */
2769
+ static applyGain(buffer, gain) {
2770
+ const output = new Float32Array(buffer.length);
2771
+ for (let i = 0; i < buffer.length; i++) {
2772
+ output[i] = Math.max(-1, Math.min(1, buffer[i] * gain));
2773
+ }
2774
+ return output;
2775
+ }
2776
+ /**
2777
+ * Mix two audio buffers together
2778
+ * @param buffer1 - First audio buffer
2779
+ * @param buffer2 - Second audio buffer
2780
+ * @param ratio - Mix ratio (0-1, where 0.5 is equal mix)
2781
+ * @returns Mixed Float32Array
2782
+ */
2783
+ static mixBuffers(buffer1, buffer2, ratio = 0.5) {
2784
+ const length = Math.max(buffer1.length, buffer2.length);
2785
+ const output = new Float32Array(length);
2786
+ for (let i = 0; i < length; i++) {
2787
+ const s1 = i < buffer1.length ? buffer1[i] : 0;
2788
+ const s2 = i < buffer2.length ? buffer2[i] : 0;
2789
+ output[i] = Math.max(-1, Math.min(1, s1 * (1 - ratio) + s2 * ratio));
2790
+ }
2791
+ return output;
2792
+ }
2793
+ /**
2794
+ * Convert AudioBuffer to WAV format
2795
+ * @param audioBuffer - Web Audio API AudioBuffer
2796
+ * @param sampleRate - Output sample rate (defaults to buffer's sample rate)
2797
+ * @returns WAV file as ArrayBuffer
2798
+ */
2799
+ static bufferToWav(audioBuffer, sampleRate) {
2800
+ const outputRate = sampleRate || audioBuffer.sampleRate;
2801
+ const channels = audioBuffer.numberOfChannels;
2802
+ const bitDepth = 16;
2803
+ const sourceData = audioBuffer.getChannelData(0);
2804
+ const channelData = outputRate !== audioBuffer.sampleRate ? this.resampleBuffer(sourceData, audioBuffer.sampleRate, outputRate) : new Float32Array(sourceData);
2805
+ const int16Data = this.convertFloat32ToInt16(channelData);
2806
+ const dataLength = int16Data.length * (bitDepth / 8);
2807
+ const header = this.createWavHeader(dataLength, outputRate, channels, bitDepth);
2808
+ const wav = new ArrayBuffer(44 + dataLength);
2809
+ const view = new DataView(wav);
2810
+ const headerView = new DataView(header);
2811
+ for (let i = 0; i < 44; i++) {
2812
+ view.setUint8(i, headerView.getUint8(i));
2813
+ }
2814
+ for (let i = 0; i < int16Data.length; i++) {
2815
+ view.setInt16(44 + i * 2, int16Data[i], true);
2816
+ }
2817
+ return wav;
2818
+ }
2819
+ /**
2820
+ * Create WAV file header
2821
+ * @param dataLength - Length of audio data in bytes
2822
+ * @param sampleRate - Sample rate
2823
+ * @param channels - Number of channels
2824
+ * @param bitDepth - Bits per sample
2825
+ * @returns WAV header as ArrayBuffer
2826
+ */
2827
+ static createWavHeader(dataLength, sampleRate, channels, bitDepth) {
2828
+ const header = new ArrayBuffer(44);
2829
+ const view = new DataView(header);
2830
+ const byteRate = sampleRate * channels * (bitDepth / 8);
2831
+ const blockAlign = channels * (bitDepth / 8);
2832
+ this.writeString(view, 0, "RIFF");
2833
+ view.setUint32(4, 36 + dataLength, true);
2834
+ this.writeString(view, 8, "WAVE");
2835
+ this.writeString(view, 12, "fmt ");
2836
+ view.setUint32(16, 16, true);
2837
+ view.setUint16(20, 1, true);
2838
+ view.setUint16(22, channels, true);
2839
+ view.setUint32(24, sampleRate, true);
2840
+ view.setUint32(28, byteRate, true);
2841
+ view.setUint16(32, blockAlign, true);
2842
+ view.setUint16(34, bitDepth, true);
2843
+ this.writeString(view, 36, "data");
2844
+ view.setUint32(40, dataLength, true);
2845
+ return header;
2846
+ }
2847
+ /**
2848
+ * Write string to DataView
2849
+ * @param view - DataView to write to
2850
+ * @param offset - Byte offset
2851
+ * @param string - String to write
2852
+ */
2853
+ static writeString(view, offset, string) {
2854
+ for (let i = 0; i < string.length; i++) {
2855
+ view.setUint8(offset + i, string.charCodeAt(i));
2856
+ }
2857
+ }
2858
+ /**
2859
+ * Convert raw PCM Float32 array to WAV ArrayBuffer
2860
+ * @param samples - Float32Array of audio samples
2861
+ * @param sampleRate - Sample rate
2862
+ * @returns WAV file as ArrayBuffer
2863
+ */
2864
+ static float32ToWav(samples, sampleRate) {
2865
+ const int16Data = this.convertFloat32ToInt16(samples);
2866
+ const dataLength = int16Data.length * 2;
2867
+ const header = this.createWavHeader(dataLength, sampleRate, 1, 16);
2868
+ const wav = new ArrayBuffer(44 + dataLength);
2869
+ const view = new DataView(wav);
2870
+ const headerView = new DataView(header);
2871
+ for (let i = 0; i < 44; i++) {
2872
+ view.setUint8(i, headerView.getUint8(i));
2873
+ }
2874
+ for (let i = 0; i < int16Data.length; i++) {
2875
+ view.setInt16(44 + i * 2, int16Data[i], true);
2876
+ }
2877
+ return wav;
2878
+ }
2879
+ };
2880
+
2881
+ // src/utils/VoiceActivityDetector.ts
2882
+ var VoiceActivityDetector = class {
2883
+ /**
2884
+ * Create a new VoiceActivityDetector
2885
+ * @param options - VAD configuration options
2886
+ */
2887
+ constructor(options = {}) {
2888
+ /** Energy history buffer */
2889
+ this.energyHistory = [];
2890
+ /** Current speaking state */
2891
+ this.isSpeaking = false;
2892
+ /** Speech start time */
2893
+ this.speechStartTime = null;
2894
+ /** Silence start time */
2895
+ this.silenceStartTime = null;
2896
+ /** Last processed timestamp */
2897
+ this.lastProcessTime = 0;
2898
+ this.threshold = options.threshold ?? 0.01;
2899
+ this.minSpeechDuration = options.minSpeechDuration ?? 300;
2900
+ this.minSilenceDuration = options.minSilenceDuration ?? 500;
2901
+ this.historySize = options.historySize ?? 10;
2902
+ this.onSpeechStart = options.onSpeechStart;
2903
+ this.onSpeechEnd = options.onSpeechEnd;
2904
+ this.onVolumeChange = options.onVolumeChange;
2905
+ }
2906
+ /**
2907
+ * Process audio data and detect voice activity
2908
+ * @param audioData - Audio samples as Float32Array
2909
+ * @returns Current speaking state
2910
+ */
2911
+ processAudio(audioData) {
2912
+ const currentTime = Date.now();
2913
+ const energy = this.calculateRMSEnergy(audioData);
2914
+ this.energyHistory.push(energy);
2915
+ if (this.energyHistory.length > this.historySize) {
2916
+ this.energyHistory.shift();
2917
+ }
2918
+ if (this.onVolumeChange) {
2919
+ this.onVolumeChange(energy);
2920
+ }
2921
+ const isAboveThreshold = energy > this.threshold;
2922
+ if (isAboveThreshold) {
2923
+ this.silenceStartTime = null;
2924
+ if (!this.isSpeaking) {
2925
+ if (this.speechStartTime === null) {
2926
+ this.speechStartTime = currentTime;
2927
+ } else if (currentTime - this.speechStartTime >= this.minSpeechDuration) {
2928
+ this.isSpeaking = true;
2929
+ this.speechStartTime = null;
2930
+ if (this.onSpeechStart) {
2931
+ this.onSpeechStart();
2932
+ }
2933
+ }
2934
+ }
2935
+ } else {
2936
+ this.speechStartTime = null;
2937
+ if (this.isSpeaking) {
2938
+ if (this.silenceStartTime === null) {
2939
+ this.silenceStartTime = currentTime;
2940
+ } else if (currentTime - this.silenceStartTime >= this.minSilenceDuration) {
2941
+ this.isSpeaking = false;
2942
+ this.silenceStartTime = null;
2943
+ if (this.onSpeechEnd) {
2944
+ this.onSpeechEnd();
2945
+ }
2946
+ }
2947
+ }
2948
+ }
2949
+ this.lastProcessTime = currentTime;
2950
+ return this.isSpeaking;
2951
+ }
2952
+ /**
2953
+ * Calculate RMS (Root Mean Square) energy of audio buffer
2954
+ * @param buffer - Audio samples
2955
+ * @returns RMS energy value (0-1)
2956
+ */
2957
+ calculateRMSEnergy(buffer) {
2958
+ let sum = 0;
2959
+ for (let i = 0; i < buffer.length; i++) {
2960
+ sum += buffer[i] * buffer[i];
2961
+ }
2962
+ return Math.sqrt(sum / buffer.length);
2963
+ }
2964
+ /**
2965
+ * Calculate adaptive threshold based on energy history
2966
+ * @returns Adaptive threshold value
2967
+ */
2968
+ calculateAdaptiveThreshold() {
2969
+ if (this.energyHistory.length === 0) {
2970
+ return this.threshold;
2971
+ }
2972
+ const average = this.energyHistory.reduce((a, b) => a + b, 0) / this.energyHistory.length;
2973
+ return average * 1.5 + 5e-3;
2974
+ }
2975
+ /**
2976
+ * Reset detector state
2977
+ */
2978
+ reset() {
2979
+ this.energyHistory = [];
2980
+ this.isSpeaking = false;
2981
+ this.speechStartTime = null;
2982
+ this.silenceStartTime = null;
2983
+ this.lastProcessTime = 0;
2984
+ }
2985
+ /**
2986
+ * Update threshold value
2987
+ * @param threshold - New threshold (0-1)
2988
+ */
2989
+ setThreshold(threshold) {
2990
+ this.threshold = Math.max(0, Math.min(1, threshold));
2991
+ }
2992
+ /**
2993
+ * Get average energy from history
2994
+ * @returns Average energy value
2995
+ */
2996
+ getAverageEnergy() {
2997
+ if (this.energyHistory.length === 0) {
2998
+ return 0;
2999
+ }
3000
+ return this.energyHistory.reduce((a, b) => a + b, 0) / this.energyHistory.length;
3001
+ }
3002
+ /**
3003
+ * Check if speech is currently detected
3004
+ * @returns Speaking state
3005
+ */
3006
+ isSpeechDetected() {
3007
+ return this.isSpeaking;
3008
+ }
3009
+ /**
3010
+ * Get current threshold
3011
+ * @returns Threshold value
3012
+ */
3013
+ getThreshold() {
3014
+ return this.threshold;
3015
+ }
3016
+ /**
3017
+ * Update callbacks
3018
+ * @param callbacks - New callback functions
3019
+ */
3020
+ setCallbacks(callbacks) {
3021
+ if (callbacks.onSpeechStart !== void 0) {
3022
+ this.onSpeechStart = callbacks.onSpeechStart;
3023
+ }
3024
+ if (callbacks.onSpeechEnd !== void 0) {
3025
+ this.onSpeechEnd = callbacks.onSpeechEnd;
3026
+ }
3027
+ if (callbacks.onVolumeChange !== void 0) {
3028
+ this.onVolumeChange = callbacks.onVolumeChange;
3029
+ }
3030
+ }
3031
+ };
3032
+
3033
+ // src/utils/AudioLevelMonitor.ts
3034
+ var AudioLevelMonitor = class {
3035
+ /**
3036
+ * Create a new AudioLevelMonitor
3037
+ * @param options - Monitor configuration
3038
+ */
3039
+ constructor(options = {}) {
3040
+ /** Current smoothed level */
3041
+ this.currentLevel = 0;
3042
+ /** Peak level since last reset */
3043
+ this.peakLevel = 0;
3044
+ this.smoothingFactor = options.smoothingFactor ?? 0.8;
3045
+ this.onLevelChange = options.onLevelChange;
3046
+ }
3047
+ /**
3048
+ * Process audio data and update levels
3049
+ * @param audioData - Audio samples as Float32Array
3050
+ * @returns Current smoothed level
3051
+ */
3052
+ processAudio(audioData) {
3053
+ const instantLevel = this.calculateLevel(audioData);
3054
+ this.currentLevel = this.smoothingFactor * this.currentLevel + (1 - this.smoothingFactor) * instantLevel;
3055
+ if (this.currentLevel > this.peakLevel) {
3056
+ this.peakLevel = this.currentLevel;
3057
+ }
3058
+ if (this.onLevelChange) {
3059
+ this.onLevelChange(this.currentLevel);
3060
+ }
3061
+ return this.currentLevel;
3062
+ }
3063
+ /**
3064
+ * Calculate RMS level of audio buffer
3065
+ * @param buffer - Audio samples
3066
+ * @returns Level value (0-1)
3067
+ */
3068
+ calculateLevel(buffer) {
3069
+ let sum = 0;
3070
+ for (let i = 0; i < buffer.length; i++) {
3071
+ sum += buffer[i] * buffer[i];
3072
+ }
3073
+ return Math.sqrt(sum / buffer.length);
3074
+ }
3075
+ /**
3076
+ * Get current smoothed level
3077
+ * @returns Current level (0-1)
3078
+ */
3079
+ getCurrentLevel() {
3080
+ return this.currentLevel;
3081
+ }
3082
+ /**
3083
+ * Get peak level since last reset
3084
+ * @returns Peak level (0-1)
3085
+ */
3086
+ getPeakLevel() {
3087
+ return this.peakLevel;
3088
+ }
3089
+ /**
3090
+ * Reset current and peak levels
3091
+ */
3092
+ reset() {
3093
+ this.currentLevel = 0;
3094
+ this.peakLevel = 0;
3095
+ }
3096
+ /**
3097
+ * Reset only the peak level
3098
+ */
3099
+ resetPeak() {
3100
+ this.peakLevel = 0;
3101
+ }
3102
+ /**
3103
+ * Convert current level to decibels
3104
+ * @returns Level in dB (typically -60 to 0)
3105
+ */
3106
+ getDecibels() {
3107
+ if (this.currentLevel <= 0) {
3108
+ return -Infinity;
3109
+ }
3110
+ return 20 * Math.log10(this.currentLevel);
3111
+ }
3112
+ /**
3113
+ * Convert specific level to decibels
3114
+ * @param level - Level value (0-1)
3115
+ * @returns Level in dB
3116
+ */
3117
+ static toDecibels(level) {
3118
+ if (level <= 0) {
3119
+ return -Infinity;
3120
+ }
3121
+ return 20 * Math.log10(level);
3122
+ }
3123
+ /**
3124
+ * Convert decibels to linear level
3125
+ * @param db - Level in decibels
3126
+ * @returns Linear level (0-1)
3127
+ */
3128
+ static fromDecibels(db) {
3129
+ return Math.pow(10, db / 20);
3130
+ }
3131
+ /**
3132
+ * Set smoothing factor
3133
+ * @param factor - Smoothing factor (0-1)
3134
+ */
3135
+ setSmoothingFactor(factor) {
3136
+ this.smoothingFactor = Math.max(0, Math.min(1, factor));
3137
+ }
3138
+ /**
3139
+ * Get current smoothing factor
3140
+ * @returns Smoothing factor
3141
+ */
3142
+ getSmoothingFactor() {
3143
+ return this.smoothingFactor;
3144
+ }
3145
+ /**
3146
+ * Set level change callback
3147
+ * @param callback - Callback function
3148
+ */
3149
+ setOnLevelChange(callback) {
3150
+ this.onLevelChange = callback;
3151
+ }
3152
+ /**
3153
+ * Get level as percentage (0-100)
3154
+ * @returns Level percentage
3155
+ */
3156
+ getLevelPercentage() {
3157
+ return Math.min(100, this.currentLevel * 100);
3158
+ }
3159
+ };
3160
+
3161
+ // src/utils/AudioBufferManager.ts
3162
+ var AudioBufferManager = class {
3163
+ /**
3164
+ * Create a new AudioBufferManager
3165
+ * @param bufferSize - Maximum number of chunks to store
3166
+ */
3167
+ constructor(bufferSize = 100) {
3168
+ /** Buffer storage */
3169
+ this.buffer = [];
3170
+ /** Write position */
3171
+ this.writeIndex = 0;
3172
+ /** Read position */
3173
+ this.readIndex = 0;
3174
+ /** Number of available chunks */
3175
+ this.count = 0;
3176
+ this.bufferSize = bufferSize;
3177
+ this.buffer = new Array(bufferSize);
3178
+ }
3179
+ /**
3180
+ * Write a chunk to the buffer
3181
+ * @param chunk - Audio data chunk
3182
+ */
3183
+ write(chunk) {
3184
+ this.buffer[this.writeIndex] = new Float32Array(chunk);
3185
+ this.writeIndex = (this.writeIndex + 1) % this.bufferSize;
3186
+ if (this.count < this.bufferSize) {
3187
+ this.count++;
3188
+ } else {
3189
+ this.readIndex = (this.readIndex + 1) % this.bufferSize;
3190
+ }
3191
+ }
3192
+ /**
3193
+ * Read and remove chunks from the buffer
3194
+ * @param numChunks - Number of chunks to read (default: all available)
3195
+ * @returns Array of audio chunks
3196
+ */
3197
+ read(numChunks) {
3198
+ const toRead = numChunks !== void 0 ? Math.min(numChunks, this.count) : this.count;
3199
+ const chunks = [];
3200
+ for (let i = 0; i < toRead; i++) {
3201
+ const chunk = this.buffer[this.readIndex];
3202
+ if (chunk) {
3203
+ chunks.push(chunk);
3204
+ }
3205
+ this.readIndex = (this.readIndex + 1) % this.bufferSize;
3206
+ }
3207
+ this.count -= toRead;
3208
+ return chunks;
3209
+ }
3210
+ /**
3211
+ * Read chunks without removing them
3212
+ * @param numChunks - Number of chunks to peek (default: all available)
3213
+ * @returns Array of audio chunks
3214
+ */
3215
+ peek(numChunks) {
3216
+ const toPeek = numChunks !== void 0 ? Math.min(numChunks, this.count) : this.count;
3217
+ const chunks = [];
3218
+ let idx = this.readIndex;
3219
+ for (let i = 0; i < toPeek; i++) {
3220
+ const chunk = this.buffer[idx];
3221
+ if (chunk) {
3222
+ chunks.push(chunk);
3223
+ }
3224
+ idx = (idx + 1) % this.bufferSize;
3225
+ }
3226
+ return chunks;
3227
+ }
3228
+ /**
3229
+ * Clear all data from buffer
3230
+ */
3231
+ clear() {
3232
+ this.buffer = new Array(this.bufferSize);
3233
+ this.writeIndex = 0;
3234
+ this.readIndex = 0;
3235
+ this.count = 0;
3236
+ }
3237
+ /**
3238
+ * Get number of available chunks
3239
+ * @returns Number of chunks in buffer
3240
+ */
3241
+ getAvailableChunks() {
3242
+ return this.count;
3243
+ }
3244
+ /**
3245
+ * Check if buffer is full
3246
+ * @returns True if buffer is full
3247
+ */
3248
+ isFull() {
3249
+ return this.count >= this.bufferSize;
3250
+ }
3251
+ /**
3252
+ * Check if buffer is empty
3253
+ * @returns True if buffer is empty
3254
+ */
3255
+ isEmpty() {
3256
+ return this.count === 0;
3257
+ }
3258
+ /**
3259
+ * Concatenate multiple chunks into a single buffer
3260
+ * @param chunks - Array of audio chunks
3261
+ * @returns Single concatenated Float32Array
3262
+ */
3263
+ concatenateChunks(chunks) {
3264
+ if (chunks.length === 0) {
3265
+ return new Float32Array(0);
3266
+ }
3267
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
3268
+ const result = new Float32Array(totalLength);
3269
+ let offset = 0;
3270
+ for (const chunk of chunks) {
3271
+ result.set(chunk, offset);
3272
+ offset += chunk.length;
3273
+ }
3274
+ return result;
3275
+ }
3276
+ /**
3277
+ * Get all data as a single concatenated buffer
3278
+ * @returns Concatenated Float32Array
3279
+ */
3280
+ getAll() {
3281
+ const chunks = this.peek();
3282
+ return this.concatenateChunks(chunks);
3283
+ }
3284
+ /**
3285
+ * Get total number of samples across all chunks
3286
+ * @returns Total sample count
3287
+ */
3288
+ getTotalSamples() {
3289
+ let total = 0;
3290
+ let idx = this.readIndex;
3291
+ for (let i = 0; i < this.count; i++) {
3292
+ const chunk = this.buffer[idx];
3293
+ if (chunk) {
3294
+ total += chunk.length;
3295
+ }
3296
+ idx = (idx + 1) % this.bufferSize;
3297
+ }
3298
+ return total;
3299
+ }
3300
+ /**
3301
+ * Get buffer capacity
3302
+ * @returns Maximum number of chunks
3303
+ */
3304
+ getCapacity() {
3305
+ return this.bufferSize;
3306
+ }
3307
+ /**
3308
+ * Resize the buffer
3309
+ * @param newSize - New buffer size
3310
+ */
3311
+ resize(newSize) {
3312
+ if (newSize === this.bufferSize) return;
3313
+ const chunks = this.read();
3314
+ this.bufferSize = newSize;
3315
+ this.buffer = new Array(newSize);
3316
+ this.writeIndex = 0;
3317
+ this.readIndex = 0;
3318
+ this.count = 0;
3319
+ for (const chunk of chunks.slice(-newSize)) {
3320
+ this.write(chunk);
3321
+ }
3322
+ }
3323
+ };
3324
+
3325
+ // src/utils/AudioRecorder.ts
3326
+ var AudioRecorder = class {
3327
+ /**
3328
+ * Create a new AudioRecorder
3329
+ * @param sampleRate - Sample rate for recording
3330
+ */
3331
+ constructor(sampleRate = 16e3) {
3332
+ /** Recorded audio chunks */
3333
+ this.audioChunks = [];
3334
+ /** Recording state */
3335
+ this.isRecording = false;
3336
+ /** Recording start time */
3337
+ this.startTime = null;
3338
+ this.sampleRate = sampleRate;
3339
+ }
3340
+ /**
3341
+ * Start recording
3342
+ */
3343
+ start() {
3344
+ this.audioChunks = [];
3345
+ this.isRecording = true;
3346
+ this.startTime = Date.now();
3347
+ }
3348
+ /**
3349
+ * Record an audio chunk
3350
+ * @param audioData - Audio data to record
3351
+ */
3352
+ recordChunk(audioData) {
3353
+ if (this.isRecording) {
3354
+ this.audioChunks.push(new Float32Array(audioData));
3355
+ }
3356
+ }
3357
+ /**
3358
+ * Stop recording and return all recorded audio
3359
+ * @returns Complete audio as Float32Array
3360
+ */
3361
+ stop() {
3362
+ this.isRecording = false;
3363
+ return this.getCombinedAudio();
3364
+ }
3365
+ /**
3366
+ * Clear all recorded audio
3367
+ */
3368
+ clear() {
3369
+ this.audioChunks = [];
3370
+ this.startTime = null;
3371
+ }
3372
+ /**
3373
+ * Export recording to specified format
3374
+ * @param format - Output format ('raw' or 'wav')
3375
+ * @returns Audio data as ArrayBuffer
3376
+ */
3377
+ export(format = "wav") {
3378
+ const audioData = this.getCombinedAudio();
3379
+ if (format === "raw") {
3380
+ return audioData.buffer;
3381
+ }
3382
+ return AudioProcessor.float32ToWav(audioData, this.sampleRate);
3383
+ }
3384
+ /**
3385
+ * Get recording duration in seconds
3386
+ * @returns Duration in seconds
3387
+ */
3388
+ getDuration() {
3389
+ const totalSamples = this.audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
3390
+ return totalSamples / this.sampleRate;
3391
+ }
3392
+ /**
3393
+ * Get number of recorded chunks
3394
+ * @returns Chunk count
3395
+ */
3396
+ getChunkCount() {
3397
+ return this.audioChunks.length;
3398
+ }
3399
+ /**
3400
+ * Check if currently recording
3401
+ * @returns Recording state
3402
+ */
3403
+ getIsRecording() {
3404
+ return this.isRecording;
3405
+ }
3406
+ /**
3407
+ * Get sample rate
3408
+ * @returns Sample rate
3409
+ */
3410
+ getSampleRate() {
3411
+ return this.sampleRate;
3412
+ }
3413
+ /**
3414
+ * Set sample rate (only effective before recording starts)
3415
+ * @param sampleRate - New sample rate
3416
+ */
3417
+ setSampleRate(sampleRate) {
3418
+ if (!this.isRecording) {
3419
+ this.sampleRate = sampleRate;
3420
+ }
3421
+ }
3422
+ /**
3423
+ * Get total number of recorded samples
3424
+ * @returns Sample count
3425
+ */
3426
+ getTotalSamples() {
3427
+ return this.audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
3428
+ }
3429
+ /**
3430
+ * Get recording start time
3431
+ * @returns Start timestamp or null
3432
+ */
3433
+ getStartTime() {
3434
+ return this.startTime;
3435
+ }
3436
+ /**
3437
+ * Get elapsed recording time in milliseconds
3438
+ * @returns Elapsed time in ms
3439
+ */
3440
+ getElapsedTime() {
3441
+ if (!this.startTime) return 0;
3442
+ return Date.now() - this.startTime;
3443
+ }
3444
+ /**
3445
+ * Get combined audio data
3446
+ * @returns Concatenated Float32Array
3447
+ */
3448
+ getCombinedAudio() {
3449
+ if (this.audioChunks.length === 0) {
3450
+ return new Float32Array(0);
3451
+ }
3452
+ const totalLength = this.audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
3453
+ const combined = new Float32Array(totalLength);
3454
+ let offset = 0;
3455
+ for (const chunk of this.audioChunks) {
3456
+ combined.set(chunk, offset);
3457
+ offset += chunk.length;
3458
+ }
3459
+ return combined;
3460
+ }
3461
+ /**
3462
+ * Create a Blob from the recording
3463
+ * @param format - Output format
3464
+ * @returns Blob with audio data
3465
+ */
3466
+ toBlob(format = "wav") {
3467
+ const buffer = this.export(format);
3468
+ const mimeType = format === "wav" ? "audio/wav" : "application/octet-stream";
3469
+ return new Blob([buffer], { type: mimeType });
3470
+ }
3471
+ /**
3472
+ * Create a data URL from the recording
3473
+ * @param format - Output format
3474
+ * @returns Data URL string
3475
+ */
3476
+ toDataURL(format = "wav") {
3477
+ const blob = this.toBlob(format);
3478
+ return URL.createObjectURL(blob);
3479
+ }
3480
+ /**
3481
+ * Download the recording
3482
+ * @param filename - Output filename
3483
+ * @param format - Output format
3484
+ */
3485
+ download(filename = "recording", format = "wav") {
3486
+ const url = this.toDataURL(format);
3487
+ const extension = format === "wav" ? ".wav" : ".raw";
3488
+ const link = document.createElement("a");
3489
+ link.href = url;
3490
+ link.download = filename + extension;
3491
+ link.click();
3492
+ URL.revokeObjectURL(url);
3493
+ }
3494
+ };
3495
+
3496
+ // src/utils/StorageAdapter.ts
3497
+ var LocalStorageAdapter = class {
3498
+ /**
3499
+ * Create a new LocalStorageAdapter
3500
+ * @param prefix - Key prefix (default: 'live-transcribe')
3501
+ */
3502
+ constructor(prefix = "live-transcribe") {
3503
+ this.prefix = prefix;
3504
+ }
3505
+ /**
3506
+ * Get prefixed key
3507
+ */
3508
+ getKey(key) {
3509
+ return `${this.prefix}:${key}`;
3510
+ }
3511
+ async save(key, data) {
3512
+ if (typeof window === "undefined" || !window.localStorage) {
3513
+ throw new Error("localStorage is not available");
3514
+ }
3515
+ const serialized = JSON.stringify(data);
3516
+ localStorage.setItem(this.getKey(key), serialized);
3517
+ }
3518
+ async load(key) {
3519
+ if (typeof window === "undefined" || !window.localStorage) {
3520
+ throw new Error("localStorage is not available");
3521
+ }
3522
+ const data = localStorage.getItem(this.getKey(key));
3523
+ if (data === null) {
3524
+ return null;
3525
+ }
3526
+ return JSON.parse(data);
3527
+ }
3528
+ async delete(key) {
3529
+ if (typeof window === "undefined" || !window.localStorage) {
3530
+ throw new Error("localStorage is not available");
3531
+ }
3532
+ localStorage.removeItem(this.getKey(key));
3533
+ }
3534
+ async list() {
3535
+ if (typeof window === "undefined" || !window.localStorage) {
3536
+ throw new Error("localStorage is not available");
3537
+ }
3538
+ const keys = [];
3539
+ const prefixWithColon = `${this.prefix}:`;
3540
+ for (let i = 0; i < localStorage.length; i++) {
3541
+ const key = localStorage.key(i);
3542
+ if (key?.startsWith(prefixWithColon)) {
3543
+ keys.push(key.substring(prefixWithColon.length));
3544
+ }
3545
+ }
3546
+ return keys;
3547
+ }
3548
+ async exists(key) {
3549
+ if (typeof window === "undefined" || !window.localStorage) {
3550
+ throw new Error("localStorage is not available");
3551
+ }
3552
+ return localStorage.getItem(this.getKey(key)) !== null;
3553
+ }
3554
+ };
3555
+ var MemoryStorageAdapter = class {
3556
+ constructor() {
3557
+ this.storage = /* @__PURE__ */ new Map();
3558
+ }
3559
+ async save(key, data) {
3560
+ this.storage.set(key, JSON.parse(JSON.stringify(data)));
3561
+ }
3562
+ async load(key) {
3563
+ const data = this.storage.get(key);
3564
+ if (data === void 0) {
3565
+ return null;
3566
+ }
3567
+ return JSON.parse(JSON.stringify(data));
3568
+ }
3569
+ async delete(key) {
3570
+ this.storage.delete(key);
3571
+ }
3572
+ async list() {
3573
+ return Array.from(this.storage.keys());
3574
+ }
3575
+ async exists(key) {
3576
+ return this.storage.has(key);
3577
+ }
3578
+ /**
3579
+ * Clear all data
3580
+ */
3581
+ clear() {
3582
+ this.storage.clear();
3583
+ }
3584
+ /**
3585
+ * Get storage size
3586
+ */
3587
+ size() {
3588
+ return this.storage.size;
3589
+ }
3590
+ };
3591
+
3592
+ // src/utils/validators.ts
3593
+ function validateTranscriptionConfig(config) {
3594
+ const errors = [];
3595
+ const warnings = [];
3596
+ if (!config.provider) {
3597
+ errors.push({
3598
+ field: "provider",
3599
+ message: "Provider is required",
3600
+ code: "REQUIRED_FIELD"
3601
+ });
3602
+ } else if (!Object.values(TranscriptionProvider).includes(config.provider)) {
3603
+ errors.push({
3604
+ field: "provider",
3605
+ message: `Invalid provider: ${config.provider}`,
3606
+ code: "INVALID_PROVIDER"
3607
+ });
3608
+ }
3609
+ const cloudProviders = ["deepgram" /* Deepgram */, "assemblyai" /* AssemblyAI */];
3610
+ if (config.provider && cloudProviders.includes(config.provider)) {
3611
+ if (!config.apiKey) {
3612
+ errors.push({
3613
+ field: "apiKey",
3614
+ message: `API key is required for ${config.provider} provider`,
3615
+ code: "REQUIRED_API_KEY"
3616
+ });
3617
+ } else if (config.apiKey.length < 10) {
3618
+ warnings.push({
3619
+ field: "apiKey",
3620
+ message: "API key seems too short"
3621
+ });
3622
+ }
3623
+ }
3624
+ if (config.language && !validateLanguageCode(config.language)) {
3625
+ errors.push({
3626
+ field: "language",
3627
+ message: `Invalid language code: ${config.language}`,
3628
+ code: "INVALID_LANGUAGE"
3629
+ });
3630
+ }
3631
+ if (config.audioConfig) {
3632
+ const audioValidation = validateAudioConfig(config.audioConfig);
3633
+ errors.push(...audioValidation.errors);
3634
+ if (audioValidation.warnings) {
3635
+ warnings.push(...audioValidation.warnings);
3636
+ }
3637
+ }
3638
+ return {
3639
+ valid: errors.length === 0,
3640
+ errors,
3641
+ warnings: warnings.length > 0 ? warnings : void 0
3642
+ };
3643
+ }
3644
+ function validateAudioConfig(config) {
3645
+ const errors = [];
3646
+ const warnings = [];
3647
+ if (config.sampleRate !== void 0) {
3648
+ if (config.sampleRate < 8e3 || config.sampleRate > 48e3) {
3649
+ errors.push({
3650
+ field: "audioConfig.sampleRate",
3651
+ message: "Sample rate must be between 8000 and 48000",
3652
+ code: "INVALID_SAMPLE_RATE"
3653
+ });
3654
+ } else if (config.sampleRate !== 16e3 && config.sampleRate !== 44100 && config.sampleRate !== 48e3) {
3655
+ warnings.push({
3656
+ field: "audioConfig.sampleRate",
3657
+ message: "Non-standard sample rate may not be supported by all providers"
3658
+ });
3659
+ }
3660
+ }
3661
+ if (config.channels !== void 0 && (config.channels < 1 || config.channels > 2)) {
3662
+ errors.push({
3663
+ field: "audioConfig.channels",
3664
+ message: "Channels must be 1 or 2",
3665
+ code: "INVALID_CHANNELS"
3666
+ });
3667
+ }
3668
+ if (config.bitDepth !== void 0 && ![8, 16, 24].includes(config.bitDepth)) {
3669
+ errors.push({
3670
+ field: "audioConfig.bitDepth",
3671
+ message: "Bit depth must be 8, 16, or 24",
3672
+ code: "INVALID_BIT_DEPTH"
3673
+ });
3674
+ }
3675
+ if (config.encoding !== void 0 && !Object.values(AudioEncoding).includes(config.encoding)) {
3676
+ errors.push({
3677
+ field: "audioConfig.encoding",
3678
+ message: `Invalid encoding: ${config.encoding}`,
3679
+ code: "INVALID_ENCODING"
3680
+ });
3681
+ }
3682
+ return {
3683
+ valid: errors.length === 0,
3684
+ errors,
3685
+ warnings: warnings.length > 0 ? warnings : void 0
3686
+ };
3687
+ }
3688
+ function validateSessionConfig(config) {
3689
+ const errors = [];
3690
+ const warnings = [];
3691
+ if (config.maxDuration !== void 0 && config.maxDuration < 0) {
3692
+ errors.push({
3693
+ field: "maxDuration",
3694
+ message: "Max duration must be positive",
3695
+ code: "INVALID_MAX_DURATION"
3696
+ });
3697
+ }
3698
+ if (config.silenceTimeout !== void 0 && config.silenceTimeout < 0) {
3699
+ errors.push({
3700
+ field: "silenceTimeout",
3701
+ message: "Silence timeout must be positive",
3702
+ code: "INVALID_SILENCE_TIMEOUT"
3703
+ });
3704
+ }
3705
+ if (config.vadThreshold !== void 0 && (config.vadThreshold < 0 || config.vadThreshold > 1)) {
3706
+ errors.push({
3707
+ field: "vadThreshold",
3708
+ message: "VAD threshold must be between 0 and 1",
3709
+ code: "INVALID_VAD_THRESHOLD"
3710
+ });
3711
+ }
3712
+ return {
3713
+ valid: errors.length === 0,
3714
+ errors,
3715
+ warnings: warnings.length > 0 ? warnings : void 0
3716
+ };
3717
+ }
3718
+ function validateLanguageCode(code) {
3719
+ const pattern = /^[a-z]{2,3}(-[A-Z]{2})?(-[A-Za-z]{4})?(-[A-Z]{2}|-[0-9]{3})?$/i;
3720
+ return pattern.test(code);
3721
+ }
3722
+ function validateApiKey(provider, key) {
3723
+ const errors = [];
3724
+ if (provider === "web-speech" /* WebSpeechAPI */) {
3725
+ return { valid: true, errors: [] };
3726
+ }
3727
+ if (!key) {
3728
+ errors.push({
3729
+ field: "apiKey",
3730
+ message: `API key is required for ${provider}`,
3731
+ code: "MISSING_API_KEY"
3732
+ });
3733
+ return { valid: false, errors };
3734
+ }
3735
+ switch (provider) {
3736
+ case "deepgram" /* Deepgram */:
3737
+ if (key.length < 20) {
3738
+ errors.push({
3739
+ field: "apiKey",
3740
+ message: "Deepgram API key appears to be too short",
3741
+ code: "INVALID_API_KEY_FORMAT"
3742
+ });
3743
+ }
3744
+ break;
3745
+ case "assemblyai" /* AssemblyAI */:
3746
+ if (key.length < 20) {
3747
+ errors.push({
3748
+ field: "apiKey",
3749
+ message: "AssemblyAI API key appears to be too short",
3750
+ code: "INVALID_API_KEY_FORMAT"
3751
+ });
3752
+ }
3753
+ break;
3754
+ }
3755
+ return {
3756
+ valid: errors.length === 0,
3757
+ errors
3758
+ };
3759
+ }
3760
+
3761
+ // src/utils/browserCheck.ts
3762
+ function getBrowserInfo() {
3763
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
3764
+ return {
3765
+ name: "Node.js",
3766
+ version: typeof process !== "undefined" ? process.version : "unknown",
3767
+ os: typeof process !== "undefined" ? process.platform : "unknown",
3768
+ isMobile: false
3769
+ };
3770
+ }
3771
+ const ua = navigator.userAgent;
3772
+ let name = "Unknown";
3773
+ let version = "unknown";
3774
+ if (ua.includes("Firefox/")) {
3775
+ name = "Firefox";
3776
+ version = ua.match(/Firefox\/(\d+\.\d+)/)?.[1] || "unknown";
3777
+ } else if (ua.includes("Edg/")) {
3778
+ name = "Edge";
3779
+ version = ua.match(/Edg\/(\d+\.\d+)/)?.[1] || "unknown";
3780
+ } else if (ua.includes("Chrome/")) {
3781
+ name = "Chrome";
3782
+ version = ua.match(/Chrome\/(\d+\.\d+)/)?.[1] || "unknown";
3783
+ } else if (ua.includes("Safari/") && !ua.includes("Chrome")) {
3784
+ name = "Safari";
3785
+ version = ua.match(/Version\/(\d+\.\d+)/)?.[1] || "unknown";
3786
+ }
3787
+ let os = "Unknown";
3788
+ if (ua.includes("Windows")) os = "Windows";
3789
+ else if (ua.includes("Mac OS")) os = "macOS";
3790
+ else if (ua.includes("Linux")) os = "Linux";
3791
+ else if (ua.includes("Android")) os = "Android";
3792
+ else if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
3793
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
3794
+ return { name, version, os, isMobile };
3795
+ }
3796
+ function checkWebSpeechAPISupport() {
3797
+ if (typeof window === "undefined") {
3798
+ return {
3799
+ supported: false,
3800
+ details: "Web Speech API is only available in browser environments",
3801
+ fallback: "Use Deepgram or AssemblyAI provider in Node.js"
3802
+ };
3803
+ }
3804
+ const hasSupport = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
3805
+ if (hasSupport) {
3806
+ const isWebkit = !window.SpeechRecognition && !!window.webkitSpeechRecognition;
3807
+ return {
3808
+ supported: true,
3809
+ details: isWebkit ? "Supported via webkit prefix" : "Fully supported"
3810
+ };
3811
+ }
3812
+ return {
3813
+ supported: false,
3814
+ details: "Web Speech API is not supported in this browser",
3815
+ fallback: "Use Deepgram or AssemblyAI provider instead"
3816
+ };
3817
+ }
3818
+ function checkWebSocketSupport() {
3819
+ const hasSupport = typeof WebSocket !== "undefined";
3820
+ if (hasSupport) {
3821
+ return {
3822
+ supported: true,
3823
+ details: "WebSocket is fully supported"
3824
+ };
3825
+ }
3826
+ return {
3827
+ supported: false,
3828
+ details: "WebSocket is not supported",
3829
+ fallback: "Upgrade to a modern browser"
3830
+ };
3831
+ }
3832
+ function checkMediaDevicesSupport() {
3833
+ if (typeof navigator === "undefined") {
3834
+ return {
3835
+ supported: false,
3836
+ details: "Navigator API is not available",
3837
+ fallback: "Run in a browser environment"
3838
+ };
3839
+ }
3840
+ const hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
3841
+ const hasEnumerateDevices = !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices);
3842
+ if (hasGetUserMedia && hasEnumerateDevices) {
3843
+ return {
3844
+ supported: true,
3845
+ details: "Full media devices support"
3846
+ };
3847
+ }
3848
+ if (hasGetUserMedia) {
3849
+ return {
3850
+ supported: true,
3851
+ details: "getUserMedia supported, enumerateDevices not available"
3852
+ };
3853
+ }
3854
+ return {
3855
+ supported: false,
3856
+ details: "Media devices API is not supported",
3857
+ fallback: "Use HTTPS and a modern browser"
3858
+ };
3859
+ }
3860
+ function checkAudioContextSupport() {
3861
+ if (typeof window === "undefined") {
3862
+ return {
3863
+ supported: false,
3864
+ details: "AudioContext is only available in browser environments"
3865
+ };
3866
+ }
3867
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
3868
+ if (AudioContextClass) {
3869
+ const hasWorklet = "audioWorklet" in AudioContext.prototype;
3870
+ return {
3871
+ supported: true,
3872
+ details: hasWorklet ? "Full AudioContext support with AudioWorklet" : "AudioContext supported (no AudioWorklet)"
3873
+ };
3874
+ }
3875
+ return {
3876
+ supported: false,
3877
+ details: "AudioContext is not supported",
3878
+ fallback: "Use a modern browser"
3879
+ };
3880
+ }
3881
+ function getFullCompatibilityReport() {
3882
+ const browser = getBrowserInfo();
3883
+ const webSpeechAPI = checkWebSpeechAPISupport();
3884
+ const webSocket = checkWebSocketSupport();
3885
+ const mediaDevices = checkMediaDevicesSupport();
3886
+ const audioContext = checkAudioContextSupport();
3887
+ const recommendations = [];
3888
+ if (!webSpeechAPI.supported) {
3889
+ recommendations.push("Consider using Chrome, Edge, or Safari for Web Speech API support");
3890
+ }
3891
+ if (!mediaDevices.supported) {
3892
+ recommendations.push("Ensure HTTPS is enabled and grant microphone permissions");
3893
+ }
3894
+ if (!audioContext.supported) {
3895
+ recommendations.push("Update to a modern browser for audio processing support");
3896
+ }
3897
+ const overallCompatible = (webSpeechAPI.supported || webSocket.supported) && mediaDevices.supported;
3898
+ if (browser.isMobile) {
3899
+ recommendations.push("Mobile support may vary; consider testing on desktop for best results");
3900
+ }
3901
+ return {
3902
+ browser,
3903
+ webSpeechAPI,
3904
+ webSocket,
3905
+ mediaDevices,
3906
+ audioContext,
3907
+ overallCompatible,
3908
+ recommendations
3909
+ };
3910
+ }
3911
+
3912
+ // src/utils/timing.ts
3913
+ function debounce(func, wait) {
3914
+ let timeoutId = null;
3915
+ return function debounced(...args) {
3916
+ if (timeoutId) {
3917
+ clearTimeout(timeoutId);
3918
+ }
3919
+ timeoutId = setTimeout(() => {
3920
+ func.apply(null, args);
3921
+ timeoutId = null;
3922
+ }, wait);
3923
+ };
3924
+ }
3925
+ function throttle(func, limit) {
3926
+ let lastRun = 0;
3927
+ let timeoutId = null;
3928
+ return function throttled(...args) {
3929
+ const now = Date.now();
3930
+ if (now - lastRun >= limit) {
3931
+ lastRun = now;
3932
+ func.apply(null, args);
3933
+ } else if (!timeoutId) {
3934
+ const remaining = limit - (now - lastRun);
3935
+ timeoutId = setTimeout(() => {
3936
+ lastRun = Date.now();
3937
+ func.apply(null, args);
3938
+ timeoutId = null;
3939
+ }, remaining);
3940
+ }
3941
+ };
3942
+ }
3943
+ function sleep(ms) {
3944
+ return new Promise((resolve) => setTimeout(resolve, ms));
3945
+ }
3946
+ function timeout(promise, ms, message = "Operation timed out") {
3947
+ return new Promise((resolve, reject) => {
3948
+ const timeoutId = setTimeout(() => {
3949
+ reject(new Error(message));
3950
+ }, ms);
3951
+ promise.then((result) => {
3952
+ clearTimeout(timeoutId);
3953
+ resolve(result);
3954
+ }).catch((error) => {
3955
+ clearTimeout(timeoutId);
3956
+ reject(error);
3957
+ });
3958
+ });
3959
+ }
3960
+ async function retry(fn, options = {}) {
3961
+ const {
3962
+ maxAttempts = 3,
3963
+ delay = 1e3,
3964
+ backoff = "exponential",
3965
+ maxDelay = 3e4,
3966
+ shouldRetry = () => true
3967
+ } = options;
3968
+ let lastError;
3969
+ let currentDelay = delay;
3970
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3971
+ try {
3972
+ return await fn();
3973
+ } catch (error) {
3974
+ lastError = error;
3975
+ if (attempt === maxAttempts || !shouldRetry(error)) {
3976
+ break;
3977
+ }
3978
+ await sleep(currentDelay);
3979
+ if (backoff === "exponential") {
3980
+ currentDelay = Math.min(currentDelay * 2, maxDelay);
3981
+ } else {
3982
+ currentDelay = Math.min(currentDelay + delay, maxDelay);
3983
+ }
3984
+ }
3985
+ }
3986
+ throw lastError;
3987
+ }
3988
+ function cancellableTimeout(ms) {
3989
+ let timeoutId;
3990
+ let rejectFn;
3991
+ const promise = new Promise((resolve, reject) => {
3992
+ rejectFn = reject;
3993
+ timeoutId = setTimeout(resolve, ms);
3994
+ });
3995
+ const cancel = () => {
3996
+ clearTimeout(timeoutId);
3997
+ rejectFn(new Error("Timeout cancelled"));
3998
+ };
3999
+ return { promise, cancel };
4000
+ }
4001
+ function setIntervalAsync(fn, interval, immediate = false) {
4002
+ let stopped = false;
4003
+ let timeoutId;
4004
+ const execute = async () => {
4005
+ if (stopped) return;
4006
+ try {
4007
+ await fn();
4008
+ } catch (error) {
4009
+ console.error("Interval function error:", error);
4010
+ }
4011
+ if (!stopped) {
4012
+ timeoutId = setTimeout(execute, interval);
4013
+ }
4014
+ };
4015
+ if (immediate) {
4016
+ void execute();
4017
+ } else {
4018
+ timeoutId = setTimeout(execute, interval);
4019
+ }
4020
+ return () => {
4021
+ stopped = true;
4022
+ clearTimeout(timeoutId);
4023
+ };
4024
+ }
4025
+
4026
+ // src/utils/formatters.ts
4027
+ function formatDuration(ms) {
4028
+ if (ms < 0) return "0s";
4029
+ const seconds = Math.floor(ms / 1e3);
4030
+ const minutes = Math.floor(seconds / 60);
4031
+ const hours = Math.floor(minutes / 60);
4032
+ const remainingMinutes = minutes % 60;
4033
+ const remainingSeconds = seconds % 60;
4034
+ if (hours > 0) {
4035
+ return `${hours}h ${remainingMinutes}m`;
4036
+ }
4037
+ if (minutes > 0) {
4038
+ return `${minutes}m ${remainingSeconds}s`;
4039
+ }
4040
+ return `${seconds}s`;
4041
+ }
4042
+ function formatTimestamp(ms, format = "readable") {
4043
+ if (ms < 0) ms = 0;
4044
+ const totalSeconds = Math.floor(ms / 1e3);
4045
+ const hours = Math.floor(totalSeconds / 3600);
4046
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
4047
+ const seconds = totalSeconds % 60;
4048
+ const milliseconds = Math.floor(ms % 1e3);
4049
+ const pad = (n, length = 2) => String(n).padStart(length, "0");
4050
+ switch (format) {
4051
+ case "srt":
4052
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)},${pad(milliseconds, 3)}`;
4053
+ case "vtt":
4054
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}`;
4055
+ case "readable":
4056
+ if (hours > 0) {
4057
+ return `${hours}:${pad(minutes)}:${pad(seconds)}`;
4058
+ }
4059
+ return `${minutes}:${pad(seconds)}`;
4060
+ case "iso":
4061
+ return new Date(ms).toISOString();
4062
+ case "ms":
4063
+ return String(ms);
4064
+ default:
4065
+ return String(ms);
4066
+ }
4067
+ }
4068
+ function formatConfidence(confidence) {
4069
+ if (confidence < 0 || confidence > 1) {
4070
+ return "N/A";
4071
+ }
4072
+ return `${Math.round(confidence * 100)}%`;
4073
+ }
4074
+ function formatFileSize(bytes) {
4075
+ if (bytes < 0) return "0 B";
4076
+ const units = ["B", "KB", "MB", "GB", "TB"];
4077
+ let unitIndex = 0;
4078
+ let size = bytes;
4079
+ while (size >= 1024 && unitIndex < units.length - 1) {
4080
+ size /= 1024;
4081
+ unitIndex++;
4082
+ }
4083
+ return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
4084
+ }
4085
+ function formatTranscriptForDisplay(results, options = {}) {
4086
+ const {
4087
+ showTimestamps = false,
4088
+ showConfidence = false,
4089
+ highlightInterim = false,
4090
+ maxLength
4091
+ } = options;
4092
+ const parts = [];
4093
+ for (const result of results) {
4094
+ let text = result.text;
4095
+ const prefix = [];
4096
+ if (showTimestamps && result.timestamp) {
4097
+ const time = formatTimestamp(result.timestamp, "readable");
4098
+ prefix.push(`[${time}]`);
4099
+ }
4100
+ if (result.speaker) {
4101
+ prefix.push(`${result.speaker}:`);
4102
+ }
4103
+ if (showConfidence && result.confidence !== void 0) {
4104
+ prefix.push(`(${formatConfidence(result.confidence)})`);
4105
+ }
4106
+ if (highlightInterim && !result.isFinal) {
4107
+ text = `*${text}*`;
4108
+ }
4109
+ const line = prefix.length > 0 ? `${prefix.join(" ")} ${text}` : text;
4110
+ parts.push(line);
4111
+ }
4112
+ let output = parts.join("\n");
4113
+ if (maxLength && output.length > maxLength) {
4114
+ output = output.substring(0, maxLength - 3) + "...";
4115
+ }
4116
+ return output;
4117
+ }
4118
+ function formatAsPlainText(results, finalOnly = true) {
4119
+ const filtered = finalOnly ? results.filter((r) => r.isFinal) : results;
4120
+ return filtered.map((r) => r.text).join(" ").trim();
4121
+ }
4122
+ function formatNumber(num) {
4123
+ return num.toLocaleString();
4124
+ }
4125
+ function truncateText(text, maxLength) {
4126
+ if (text.length <= maxLength) return text;
4127
+ return text.substring(0, maxLength - 3) + "...";
4128
+ }
4129
+
4130
+ // src/utils/languageUtils.ts
4131
+ var LANGUAGE_MAP = {
4132
+ "en": { name: "English", nativeName: "English" },
4133
+ "en-US": { name: "English (US)", nativeName: "English (US)" },
4134
+ "en-GB": { name: "English (UK)", nativeName: "English (UK)" },
4135
+ "en-AU": { name: "English (Australia)", nativeName: "English (Australia)" },
4136
+ "en-CA": { name: "English (Canada)", nativeName: "English (Canada)" },
4137
+ "en-IN": { name: "English (India)", nativeName: "English (India)" },
4138
+ "es": { name: "Spanish", nativeName: "Espa\xF1ol" },
4139
+ "es-ES": { name: "Spanish (Spain)", nativeName: "Espa\xF1ol (Espa\xF1a)" },
4140
+ "es-MX": { name: "Spanish (Mexico)", nativeName: "Espa\xF1ol (M\xE9xico)" },
4141
+ "es-419": { name: "Spanish (Latin America)", nativeName: "Espa\xF1ol (Latinoam\xE9rica)" },
4142
+ "fr": { name: "French", nativeName: "Fran\xE7ais" },
4143
+ "fr-FR": { name: "French (France)", nativeName: "Fran\xE7ais (France)" },
4144
+ "fr-CA": { name: "French (Canada)", nativeName: "Fran\xE7ais (Canada)" },
4145
+ "de": { name: "German", nativeName: "Deutsch" },
4146
+ "de-DE": { name: "German (Germany)", nativeName: "Deutsch (Deutschland)" },
4147
+ "it": { name: "Italian", nativeName: "Italiano" },
4148
+ "it-IT": { name: "Italian (Italy)", nativeName: "Italiano (Italia)" },
4149
+ "pt": { name: "Portuguese", nativeName: "Portugu\xEAs" },
4150
+ "pt-BR": { name: "Portuguese (Brazil)", nativeName: "Portugu\xEAs (Brasil)" },
4151
+ "pt-PT": { name: "Portuguese (Portugal)", nativeName: "Portugu\xEAs (Portugal)" },
4152
+ "nl": { name: "Dutch", nativeName: "Nederlands" },
4153
+ "nl-NL": { name: "Dutch (Netherlands)", nativeName: "Nederlands (Nederland)" },
4154
+ "ja": { name: "Japanese", nativeName: "\u65E5\u672C\u8A9E" },
4155
+ "ja-JP": { name: "Japanese (Japan)", nativeName: "\u65E5\u672C\u8A9E (\u65E5\u672C)" },
4156
+ "ko": { name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4" },
4157
+ "ko-KR": { name: "Korean (Korea)", nativeName: "\uD55C\uAD6D\uC5B4 (\uB300\uD55C\uBBFC\uAD6D)" },
4158
+ "zh": { name: "Chinese", nativeName: "\u4E2D\u6587" },
4159
+ "zh-CN": { name: "Chinese (Simplified)", nativeName: "\u4E2D\u6587 (\u7B80\u4F53)" },
4160
+ "zh-TW": { name: "Chinese (Traditional)", nativeName: "\u4E2D\u6587 (\u7E41\u9AD4)" },
4161
+ "ru": { name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
4162
+ "ru-RU": { name: "Russian (Russia)", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439 (\u0420\u043E\u0441\u0441\u0438\u044F)" },
4163
+ "ar": { name: "Arabic", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" },
4164
+ "ar-SA": { name: "Arabic (Saudi Arabia)", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629 (\u0627\u0644\u0633\u0639\u0648\u062F\u064A\u0629)" },
4165
+ "hi": { name: "Hindi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940" },
4166
+ "hi-IN": { name: "Hindi (India)", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940 (\u092D\u093E\u0930\u0924)" },
4167
+ "tr": { name: "Turkish", nativeName: "T\xFCrk\xE7e" },
4168
+ "tr-TR": { name: "Turkish (Turkey)", nativeName: "T\xFCrk\xE7e (T\xFCrkiye)" },
4169
+ "pl": { name: "Polish", nativeName: "Polski" },
4170
+ "pl-PL": { name: "Polish (Poland)", nativeName: "Polski (Polska)" },
4171
+ "uk": { name: "Ukrainian", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
4172
+ "uk-UA": { name: "Ukrainian (Ukraine)", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430 (\u0423\u043A\u0440\u0430\u0457\u043D\u0430)" },
4173
+ "sv": { name: "Swedish", nativeName: "Svenska" },
4174
+ "sv-SE": { name: "Swedish (Sweden)", nativeName: "Svenska (Sverige)" },
4175
+ "da": { name: "Danish", nativeName: "Dansk" },
4176
+ "da-DK": { name: "Danish (Denmark)", nativeName: "Dansk (Danmark)" },
4177
+ "no": { name: "Norwegian", nativeName: "Norsk" },
4178
+ "no-NO": { name: "Norwegian (Norway)", nativeName: "Norsk (Norge)" },
4179
+ "fi": { name: "Finnish", nativeName: "Suomi" },
4180
+ "fi-FI": { name: "Finnish (Finland)", nativeName: "Suomi (Suomi)" }
4181
+ };
4182
+ var PROVIDER_LANGUAGES = {
4183
+ ["web-speech" /* WebSpeechAPI */]: [
4184
+ "en-US",
4185
+ "en-GB",
4186
+ "en-AU",
4187
+ "en-CA",
4188
+ "en-IN",
4189
+ "es-ES",
4190
+ "es-MX",
4191
+ "fr-FR",
4192
+ "de-DE",
4193
+ "it-IT",
4194
+ "pt-BR",
4195
+ "pt-PT",
4196
+ "ja-JP",
4197
+ "ko-KR",
4198
+ "zh-CN",
4199
+ "zh-TW",
4200
+ "ru-RU",
4201
+ "ar-SA",
4202
+ "hi-IN",
4203
+ "nl-NL"
4204
+ ],
4205
+ ["deepgram" /* Deepgram */]: [
4206
+ "en",
4207
+ "en-US",
4208
+ "en-GB",
4209
+ "en-AU",
4210
+ "en-IN",
4211
+ "es",
4212
+ "es-ES",
4213
+ "es-419",
4214
+ "fr",
4215
+ "fr-FR",
4216
+ "fr-CA",
4217
+ "de",
4218
+ "de-DE",
4219
+ "it",
4220
+ "it-IT",
4221
+ "pt",
4222
+ "pt-BR",
4223
+ "pt-PT",
4224
+ "nl",
4225
+ "nl-NL",
4226
+ "ja",
4227
+ "ja-JP",
4228
+ "ko",
4229
+ "ko-KR",
4230
+ "zh",
4231
+ "zh-CN",
4232
+ "zh-TW",
4233
+ "ru",
4234
+ "ru-RU",
4235
+ "uk",
4236
+ "uk-UA",
4237
+ "hi",
4238
+ "hi-IN",
4239
+ "tr",
4240
+ "tr-TR",
4241
+ "pl",
4242
+ "pl-PL",
4243
+ "sv",
4244
+ "sv-SE",
4245
+ "da",
4246
+ "da-DK",
4247
+ "no",
4248
+ "no-NO",
4249
+ "fi",
4250
+ "fi-FI"
4251
+ ],
4252
+ ["assemblyai" /* AssemblyAI */]: [
4253
+ "en",
4254
+ "en-US",
4255
+ "en-GB",
4256
+ "en-AU"
4257
+ ],
4258
+ ["custom" /* Custom */]: []
4259
+ };
4260
+ function getSupportedLanguages(provider) {
4261
+ const codes = PROVIDER_LANGUAGES[provider] || [];
4262
+ return codes.map((code) => {
4263
+ const info = LANGUAGE_MAP[code] || { name: code, nativeName: code };
4264
+ return {
4265
+ code,
4266
+ name: info.name,
4267
+ nativeName: info.nativeName,
4268
+ provider
4269
+ };
4270
+ });
4271
+ }
4272
+ function normalizeLanguageCode(code) {
4273
+ if (!code) return "en-US";
4274
+ const normalized = code.trim();
4275
+ const parts = normalized.split(/[-_]/);
4276
+ if (parts.length === 1) {
4277
+ return parts[0].toLowerCase();
4278
+ }
4279
+ const language = parts[0].toLowerCase();
4280
+ const region = parts[1].toUpperCase();
4281
+ return `${language}-${region}`;
4282
+ }
4283
+ function getLanguageName(code) {
4284
+ const normalized = normalizeLanguageCode(code);
4285
+ const info = LANGUAGE_MAP[normalized];
4286
+ return info?.name || code;
4287
+ }
4288
+ function getNativeLanguageName(code) {
4289
+ const normalized = normalizeLanguageCode(code);
4290
+ const info = LANGUAGE_MAP[normalized];
4291
+ return info?.nativeName || code;
4292
+ }
4293
+ function detectBrowserLanguage() {
4294
+ if (typeof navigator === "undefined") {
4295
+ return "en-US";
4296
+ }
4297
+ const browserLang = navigator.language || navigator.userLanguage;
4298
+ return normalizeLanguageCode(browserLang || "en-US");
4299
+ }
4300
+ function isLanguageSupported(code, provider) {
4301
+ const normalized = normalizeLanguageCode(code);
4302
+ const supported = PROVIDER_LANGUAGES[provider] || [];
4303
+ if (supported.includes(normalized)) {
4304
+ return true;
4305
+ }
4306
+ const baseLang = normalized.split("-")[0];
4307
+ return supported.includes(baseLang);
4308
+ }
4309
+ function getBestMatchingLanguage(code, provider) {
4310
+ const normalized = normalizeLanguageCode(code);
4311
+ const supported = PROVIDER_LANGUAGES[provider] || [];
4312
+ if (supported.includes(normalized)) {
4313
+ return normalized;
4314
+ }
4315
+ const baseLang = normalized.split("-")[0];
4316
+ if (supported.includes(baseLang)) {
4317
+ return baseLang;
4318
+ }
4319
+ const match = supported.find((s) => s.startsWith(baseLang + "-"));
4320
+ if (match) {
4321
+ return match;
4322
+ }
4323
+ return supported.includes("en-US") ? "en-US" : supported[0] || "en-US";
4324
+ }
4325
+
4326
+ // src/utils/helpers.ts
4327
+ function generateId(prefix) {
4328
+ const timestamp = Date.now().toString(36);
4329
+ const random = Math.random().toString(36).substring(2, 9);
4330
+ const id = `${timestamp}-${random}`;
4331
+ return prefix ? `${prefix}-${id}` : id;
4332
+ }
4333
+ function deepClone(obj) {
4334
+ if (obj === null || typeof obj !== "object") {
4335
+ return obj;
4336
+ }
4337
+ if (Array.isArray(obj)) {
4338
+ return obj.map((item) => deepClone(item));
4339
+ }
4340
+ if (obj instanceof Date) {
4341
+ return new Date(obj.getTime());
4342
+ }
4343
+ if (obj instanceof ArrayBuffer) {
4344
+ return obj.slice(0);
4345
+ }
4346
+ const cloned = {};
4347
+ for (const key in obj) {
4348
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
4349
+ cloned[key] = deepClone(obj[key]);
4350
+ }
4351
+ }
4352
+ return cloned;
4353
+ }
4354
+ function mergeDeep(...objects) {
4355
+ const result = {};
4356
+ for (const obj of objects) {
4357
+ if (!obj) continue;
4358
+ for (const key in obj) {
4359
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
4360
+ const value = obj[key];
4361
+ const existing = result[key];
4362
+ if (isObject(value) && isObject(existing)) {
4363
+ result[key] = mergeDeep(
4364
+ existing,
4365
+ value
4366
+ );
4367
+ } else {
4368
+ result[key] = value;
4369
+ }
4370
+ }
4371
+ }
4372
+ return result;
4373
+ }
4374
+ function isFunction(value) {
4375
+ return typeof value === "function";
4376
+ }
4377
+ function isObject(value) {
4378
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4379
+ }
4380
+ function isEmpty(value) {
4381
+ if (value === null || value === void 0) return true;
4382
+ if (typeof value === "string") return value.length === 0;
4383
+ if (Array.isArray(value)) return value.length === 0;
4384
+ if (isObject(value)) return Object.keys(value).length === 0;
4385
+ return false;
4386
+ }
4387
+ function pick(obj, keys) {
4388
+ const result = {};
4389
+ for (const key of keys) {
4390
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
4391
+ result[key] = obj[key];
4392
+ }
4393
+ }
4394
+ return result;
4395
+ }
4396
+ function omit(obj, keys) {
4397
+ const result = { ...obj };
4398
+ for (const key of keys) {
4399
+ delete result[key];
4400
+ }
4401
+ return result;
4402
+ }
4403
+ function waitFor(condition, interval = 100, timeout2 = 5e3) {
4404
+ return new Promise((resolve, reject) => {
4405
+ const startTime = Date.now();
4406
+ const check = () => {
4407
+ if (condition()) {
4408
+ resolve();
4409
+ return;
4410
+ }
4411
+ if (Date.now() - startTime > timeout2) {
4412
+ reject(new Error("Timeout waiting for condition"));
4413
+ return;
4414
+ }
4415
+ setTimeout(check, interval);
4416
+ };
4417
+ check();
4418
+ });
4419
+ }
4420
+ function groupBy(array, keyFn) {
4421
+ return array.reduce((result, item) => {
4422
+ const key = keyFn(item);
4423
+ if (!result[key]) {
4424
+ result[key] = [];
4425
+ }
4426
+ result[key].push(item);
4427
+ return result;
4428
+ }, {});
4429
+ }
4430
+ function clamp(value, min, max) {
4431
+ return Math.min(Math.max(value, min), max);
4432
+ }
4433
+ function round(value, decimals = 0) {
4434
+ const factor = Math.pow(10, decimals);
4435
+ return Math.round(value * factor) / factor;
4436
+ }
4437
+
4438
+ // src/utils/audioUtils.ts
4439
+ function calculateBitrate(sampleRate, bitDepth, channels) {
4440
+ return sampleRate * bitDepth * channels;
4441
+ }
4442
+ function estimateAudioSize(durationMs, config) {
4443
+ const sampleRate = config.sampleRate || 16e3;
4444
+ const bitDepth = config.bitDepth || 16;
4445
+ const channels = config.channels || 1;
4446
+ const durationSec = durationMs / 1e3;
4447
+ const bytesPerSecond = sampleRate * bitDepth * channels / 8;
4448
+ return Math.ceil(durationSec * bytesPerSecond);
4449
+ }
4450
+ function getOptimalBufferSize(sampleRate) {
4451
+ const targetSamples = sampleRate * 0.02;
4452
+ let bufferSize = 256;
4453
+ while (bufferSize < targetSamples && bufferSize < 16384) {
4454
+ bufferSize *= 2;
4455
+ }
4456
+ return Math.max(256, bufferSize);
4457
+ }
4458
+ function validateAudioFormat(data) {
4459
+ const view = new DataView(data);
4460
+ if (data.byteLength >= 44) {
4461
+ const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
4462
+ const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));
4463
+ if (riff === "RIFF" && wave === "WAVE") {
4464
+ return parseWavHeader(view);
4465
+ }
4466
+ }
4467
+ return {
4468
+ format: "raw"
4469
+ };
4470
+ }
4471
+ function parseWavHeader(view) {
4472
+ try {
4473
+ const channels = view.getUint16(22, true);
4474
+ const sampleRate = view.getUint32(24, true);
4475
+ const bitDepth = view.getUint16(34, true);
4476
+ const dataSize = view.getUint32(40, true);
4477
+ const duration = dataSize / (sampleRate * channels * (bitDepth / 8));
4478
+ return {
4479
+ format: "wav",
4480
+ sampleRate,
4481
+ channels,
4482
+ bitDepth,
4483
+ duration
4484
+ };
4485
+ } catch {
4486
+ return { format: "wav" };
4487
+ }
4488
+ }
4489
+ function calculateRMS(buffer) {
4490
+ let sum = 0;
4491
+ for (let i = 0; i < buffer.length; i++) {
4492
+ sum += buffer[i] * buffer[i];
4493
+ }
4494
+ return Math.sqrt(sum / buffer.length);
4495
+ }
4496
+ function calculatePeak(buffer) {
4497
+ let peak = 0;
4498
+ for (let i = 0; i < buffer.length; i++) {
4499
+ const abs = Math.abs(buffer[i]);
4500
+ if (abs > peak) peak = abs;
4501
+ }
4502
+ return peak;
4503
+ }
4504
+ function dbToLinear(db) {
4505
+ return Math.pow(10, db / 20);
4506
+ }
4507
+ function linearToDb(linear) {
4508
+ if (linear <= 0) return -Infinity;
4509
+ return 20 * Math.log10(linear);
4510
+ }
4511
+ function isSilence(buffer, threshold = 1e-3) {
4512
+ const rms = calculateRMS(buffer);
4513
+ return rms < threshold;
4514
+ }
4515
+ function getAudioConstraints(config = {}) {
4516
+ return {
4517
+ sampleRate: config.sampleRate || 16e3,
4518
+ channelCount: config.channels || 1,
4519
+ echoCancellation: true,
4520
+ noiseSuppression: true,
4521
+ autoGainControl: true
4522
+ };
4523
+ }
4524
+ function samplesToDuration(samples, sampleRate) {
4525
+ return samples / sampleRate * 1e3;
4526
+ }
4527
+ function durationToSamples(durationMs, sampleRate) {
4528
+ return Math.round(durationMs / 1e3 * sampleRate);
4529
+ }
4530
+
4531
+ // src/index.ts
4532
+ var VERSION = "0.1.0";
4533
+ var LIBRARY_NAME = "live-transcribe";
4534
+ function createTranscriber(config) {
4535
+ switch (config.provider) {
4536
+ case "web-speech" /* WebSpeechAPI */:
4537
+ return new WebSpeechProvider(config);
4538
+ case "deepgram" /* Deepgram */:
4539
+ if (!config.apiKey) {
4540
+ throw new TranscriptionError(
4541
+ "API key is required for Deepgram provider",
4542
+ "invalid_config" /* INVALID_CONFIG */,
4543
+ "deepgram" /* Deepgram */
4544
+ );
4545
+ }
4546
+ return new DeepgramProvider(config);
4547
+ case "assemblyai" /* AssemblyAI */:
4548
+ if (!config.apiKey) {
4549
+ throw new TranscriptionError(
4550
+ "API key is required for AssemblyAI provider",
4551
+ "invalid_config" /* INVALID_CONFIG */,
4552
+ "assemblyai" /* AssemblyAI */
4553
+ );
4554
+ }
4555
+ return new AssemblyAIProvider(config);
4556
+ case "custom" /* Custom */:
4557
+ throw new TranscriptionError(
4558
+ "Custom provider requires manual implementation",
4559
+ "invalid_config" /* INVALID_CONFIG */
4560
+ );
4561
+ default:
4562
+ throw new TranscriptionError(
4563
+ `Unsupported provider: ${config.provider}`,
4564
+ "invalid_config" /* INVALID_CONFIG */
4565
+ );
4566
+ }
4567
+ }
4568
+ function createSession(config, sessionConfig) {
4569
+ const provider = createTranscriber(config);
4570
+ return new TranscriptionSession(provider, sessionConfig);
4571
+ }
4572
+ async function quickStart(options = {}) {
4573
+ const {
4574
+ provider: requestedProvider,
4575
+ apiKey,
4576
+ language = "en-US",
4577
+ onTranscript,
4578
+ onError,
4579
+ onStart,
4580
+ onStop,
4581
+ interimResults = true,
4582
+ recordAudio = false
4583
+ } = options;
4584
+ let provider = requestedProvider;
4585
+ if (!provider) {
4586
+ if (apiKey) {
4587
+ provider = "deepgram" /* Deepgram */;
4588
+ } else if (typeof window !== "undefined") {
4589
+ const hasWebSpeech = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
4590
+ if (hasWebSpeech) {
4591
+ provider = "web-speech" /* WebSpeechAPI */;
4592
+ } else {
4593
+ throw new TranscriptionError(
4594
+ "No speech recognition available. Provide an API key for Deepgram or AssemblyAI.",
4595
+ "unsupported_browser" /* UNSUPPORTED_BROWSER */
4596
+ );
4597
+ }
4598
+ } else {
4599
+ throw new TranscriptionError(
4600
+ "Provider must be specified in Node.js environment",
4601
+ "invalid_config" /* INVALID_CONFIG */
4602
+ );
4603
+ }
4604
+ }
4605
+ const transcriptionConfig = {
4606
+ provider,
4607
+ apiKey,
4608
+ language,
4609
+ interimResults
4610
+ };
4611
+ const sessionConfig = {
4612
+ recordAudio
4613
+ };
4614
+ const transcriber = createTranscriber(transcriptionConfig);
4615
+ const session = new TranscriptionSession(transcriber, sessionConfig);
4616
+ if (onTranscript) {
4617
+ transcriber.on("transcript", onTranscript);
4618
+ }
4619
+ if (onError) {
4620
+ transcriber.on("error", onError);
4621
+ }
4622
+ if (onStart) {
4623
+ transcriber.on("start", onStart);
4624
+ }
4625
+ if (onStop) {
4626
+ transcriber.on("stop", onStop);
4627
+ }
4628
+ try {
4629
+ await transcriber.initialize();
4630
+ await session.start();
4631
+ } catch (error) {
4632
+ if (onError && error instanceof TranscriptionError) {
4633
+ onError(error);
4634
+ }
4635
+ throw error;
4636
+ }
4637
+ return session;
4638
+ }
4639
+ function isProviderSupported(provider) {
4640
+ switch (provider) {
4641
+ case "web-speech" /* WebSpeechAPI */:
4642
+ return new WebSpeechProvider({ provider }).isSupported();
4643
+ case "deepgram" /* Deepgram */:
4644
+ return new DeepgramProvider({ provider, apiKey: "test" }).isSupported();
4645
+ case "assemblyai" /* AssemblyAI */:
4646
+ return new AssemblyAIProvider({ provider, apiKey: "test" }).isSupported();
4647
+ default:
4648
+ return false;
4649
+ }
4650
+ }
4651
+ function getSupportedProviders() {
4652
+ const providers = [];
4653
+ if (isProviderSupported("web-speech" /* WebSpeechAPI */)) {
4654
+ providers.push("web-speech" /* WebSpeechAPI */);
4655
+ }
4656
+ if (isProviderSupported("deepgram" /* Deepgram */)) {
4657
+ providers.push("deepgram" /* Deepgram */);
4658
+ }
4659
+ if (isProviderSupported("assemblyai" /* AssemblyAI */)) {
4660
+ providers.push("assemblyai" /* AssemblyAI */);
4661
+ }
4662
+ return providers;
4663
+ }
4664
+ var index_default = {
4665
+ VERSION,
4666
+ LIBRARY_NAME,
4667
+ createTranscriber,
4668
+ createSession,
4669
+ quickStart,
4670
+ isProviderSupported,
4671
+ getSupportedProviders,
4672
+ TranscriptionProvider,
4673
+ WebSpeechProvider,
4674
+ DeepgramProvider,
4675
+ AssemblyAIProvider,
4676
+ TranscriptionSession,
4677
+ SessionManager
4678
+ };
4679
+
4680
+ export { AssemblyAIProvider, AudioBufferManager, AudioEncoding, AudioLevelMonitor, AudioProcessor, AudioRecorder, BaseTranscriber, CSVExporter, DEFAULT_AUDIO_CONFIG, DEFAULT_SESSION_CONFIG, DEFAULT_TRANSCRIPTION_CONFIG, DeepgramProvider, ErrorCode, EventEmitter, JSONExporter, LIBRARY_NAME, LocalStorageAdapter, MemoryStorageAdapter, SRTExporter, SessionManager, SessionState, TextExporter, TranscriptionError, TranscriptionProvider, TranscriptionSession, VERSION, VTTExporter, VoiceActivityDetector, WebSpeechProvider, calculateBitrate, calculatePeak, calculateRMS, cancellableTimeout, checkAudioContextSupport, checkMediaDevicesSupport, checkWebSocketSupport, checkWebSpeechAPISupport, clamp, createSession, createTranscriber, dbToLinear, debounce, deepClone, index_default as default, detectBrowserLanguage, durationToSamples, estimateAudioSize, formatAsPlainText, formatConfidence, formatDuration, formatFileSize, formatNumber, formatTimestamp, formatTranscriptForDisplay, generateId, getAudioConstraints, getBestMatchingLanguage, getBrowserInfo, getFullCompatibilityReport, getLanguageName, getNativeLanguageName, getOptimalBufferSize, getSupportedLanguages, getSupportedProviders, groupBy, isEmpty, isFunction, isLanguageSupported, isObject, isProviderSupported, isSilence, linearToDb, mergeDeep, normalizeLanguageCode, omit, pick, quickStart, retry, round, samplesToDuration, setIntervalAsync, sleep, throttle, timeout, truncateText, validateApiKey, validateAudioConfig, validateAudioFormat, validateLanguageCode, validateSessionConfig, validateTranscriptionConfig, waitFor };
4681
+ //# sourceMappingURL=index.mjs.map
4682
+ //# sourceMappingURL=index.mjs.map