@tekyzinc/stt-component 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # STT-Component
2
2
 
3
- ![Version](https://img.shields.io/badge/version-0.2.2-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.2.4-blue)
4
4
 
5
5
  A framework-agnostic, browser-first speech-to-text package with real-time streaming transcription and mid-recording Whisper correction, powered by [@huggingface/transformers](https://github.com/huggingface/transformers.js).
6
6
 
@@ -87,6 +87,7 @@ Unsubscribe a specific listener.
87
87
  | `correction` | `(text: string) => void` | Whisper-corrected text replacing interim text (display in normal style) |
88
88
  | `error` | `(error: STTError) => void` | Actionable error (`{ code: string, message: string }`) |
89
89
  | `status` | `(state: STTState) => void` | Engine state changes |
90
+ | `debug` | `(message: string) => void` | Internal diagnostic logs (Speech API lifecycle, errors, results) |
90
91
 
91
92
  #### Error Codes
92
93
 
package/dist/index.cjs CHANGED
@@ -409,6 +409,7 @@ var SpeechStreamingManager = class {
409
409
  onTranscript = null;
410
410
  onPause = null;
411
411
  onError = null;
412
+ onDebug = null;
412
413
  /** Check if the Web Speech API is available in this environment. */
413
414
  static isSupported() {
414
415
  return getSpeechRecognition() !== null;
@@ -425,22 +426,59 @@ var SpeechStreamingManager = class {
425
426
  setOnError(fn) {
426
427
  this.onError = fn;
427
428
  }
428
- /** Start streaming recognition. No-op if Speech API unavailable. */
429
+ /** Set callback for diagnostic debug messages. */
430
+ setOnDebug(fn) {
431
+ this.onDebug = fn;
432
+ }
433
+ log(message) {
434
+ this.onDebug?.(message);
435
+ console.warn(message);
436
+ }
437
+ /**
438
+ * Start streaming recognition. Returns a Promise that resolves once
439
+ * SpeechRecognition has claimed the microphone (onaudiostart) or after
440
+ * a 300ms fallback — whichever comes first. The engine should await
441
+ * this before calling getUserMedia to avoid dual-mic conflicts.
442
+ */
429
443
  start(language) {
430
444
  const SR = getSpeechRecognition();
431
- if (!SR) return;
445
+ if (!SR) {
446
+ this.log("[SSM] SpeechRecognition not available in this environment");
447
+ return Promise.resolve();
448
+ }
449
+ const bcp47 = toBCP47(language);
450
+ this.log(`[SSM] start() \u2014 lang: "${language}" \u2192 "${bcp47}"`);
432
451
  this.accumulated = "";
433
452
  this.active = true;
434
453
  this.receivedResult = false;
435
454
  const recognition = new SR();
436
455
  recognition.continuous = true;
437
456
  recognition.interimResults = true;
438
- recognition.lang = toBCP47(language);
457
+ recognition.lang = bcp47;
439
458
  let lastFinalIndex = -1;
440
459
  let lastFinalText = "";
460
+ let micReady = false;
461
+ const micClaimPromise = new Promise((resolve) => {
462
+ recognition.onaudiostart = () => {
463
+ if (this.recognition !== recognition) return;
464
+ this.log("[SSM] onaudiostart \u2014 mic acquired by Speech API");
465
+ if (!micReady) {
466
+ micReady = true;
467
+ resolve();
468
+ }
469
+ };
470
+ setTimeout(() => {
471
+ if (!micReady) {
472
+ micReady = true;
473
+ this.log("[SSM] mic-claim fallback \u2014 proceeding after 300ms");
474
+ resolve();
475
+ }
476
+ }, 300);
477
+ });
441
478
  this.clearNoResultTimer();
442
479
  this.noResultTimer = setTimeout(() => {
443
480
  if (this.active && !this.receivedResult) {
481
+ this.log("[SSM] no-result timeout fired \u2014 no onresult in 5s");
444
482
  this.onError?.(
445
483
  "Speech streaming started but received no results. Mic may be blocked by another audio capture."
446
484
  );
@@ -463,6 +501,9 @@ var SpeechStreamingManager = class {
463
501
  interim += t;
464
502
  }
465
503
  }
504
+ this.log(
505
+ `[SSM] onresult \u2014 finals: "${final_}", interim: "${interim}", accumulated: "${this.accumulated}"`
506
+ );
466
507
  if (final_ && final_.trim() !== lastFinalText) {
467
508
  lastFinalText = final_.trim();
468
509
  this.accumulated = this.accumulated ? this.accumulated + " " + final_.trim() : final_.trim();
@@ -475,15 +516,19 @@ var SpeechStreamingManager = class {
475
516
  };
476
517
  recognition.onerror = (e) => {
477
518
  if (this.recognition !== recognition) return;
519
+ this.log(`[SSM] onerror \u2014 ${e.error}`);
478
520
  this.onError?.(e.error);
479
521
  };
480
522
  recognition.onend = () => {
481
523
  if (this.recognition !== recognition) return;
524
+ this.log(`[SSM] onend \u2014 active: ${this.active}, receivedResult: ${this.receivedResult}`);
482
525
  if (this.active) {
483
526
  this.onPause?.();
484
527
  try {
485
528
  recognition.start();
486
- } catch {
529
+ this.log("[SSM] restarted after pause");
530
+ } catch (err) {
531
+ this.log(`[SSM] restart THREW: ${err}`);
487
532
  this.recognition = null;
488
533
  this.onError?.("Speech recognition failed to restart after pause.");
489
534
  }
@@ -494,14 +539,18 @@ var SpeechStreamingManager = class {
494
539
  this.recognition = recognition;
495
540
  try {
496
541
  recognition.start();
542
+ this.log("[SSM] recognition.start() succeeded");
497
543
  } catch (err) {
544
+ this.log(`[SSM] recognition.start() THREW: ${err}`);
498
545
  this.recognition = null;
499
546
  this.active = false;
500
547
  this.clearNoResultTimer();
501
548
  this.onError?.(
502
549
  `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`
503
550
  );
551
+ return Promise.resolve();
504
552
  }
553
+ return micClaimPromise;
505
554
  }
506
555
  clearNoResultTimer() {
507
556
  if (this.noResultTimer) {
@@ -535,6 +584,7 @@ var SpeechStreamingManager = class {
535
584
  this.onTranscript = null;
536
585
  this.onPause = null;
537
586
  this.onError = null;
587
+ this.onDebug = null;
538
588
  }
539
589
  };
540
590
 
@@ -592,8 +642,12 @@ var STTEngine = class extends TypedEventEmitter {
592
642
  throw new Error(`Cannot start: engine is "${this.state.status}", expected "ready"`);
593
643
  }
594
644
  try {
645
+ this.emitDebug(
646
+ `[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}"`
647
+ );
595
648
  if (this.config.streaming.enabled) {
596
- this.speechStreaming.start(this.config.language);
649
+ await this.speechStreaming.start(this.config.language);
650
+ this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
597
651
  }
598
652
  this.capture = await startCapture();
599
653
  this.updateStatus("recording");
@@ -676,13 +730,19 @@ var STTEngine = class extends TypedEventEmitter {
676
730
  }
677
731
  }
678
732
  setupStreamingCallbacks() {
733
+ this.speechStreaming.setOnDebug((message) => {
734
+ this.emit("debug", message);
735
+ });
679
736
  this.speechStreaming.setOnTranscript((text) => {
737
+ this.emitDebug(`[STT] transcript callback \u2014 "${text}"`);
680
738
  this.emit("transcript", text);
681
739
  });
682
740
  this.speechStreaming.setOnPause(() => {
741
+ this.emitDebug("[STT] pause callback \u2014 triggering correction");
683
742
  this.correctionOrchestrator.onPauseDetected();
684
743
  });
685
744
  this.speechStreaming.setOnError((message) => {
745
+ this.emitDebug(`[STT] streaming error \u2014 "${message}"`);
686
746
  this.emitError("STREAMING_ERROR", message);
687
747
  });
688
748
  }
@@ -704,6 +764,10 @@ var STTEngine = class extends TypedEventEmitter {
704
764
  this.state.error = message;
705
765
  this.emit("error", { code, message });
706
766
  }
767
+ emitDebug(message) {
768
+ console.warn(message);
769
+ this.emit("debug", message);
770
+ }
707
771
  };
708
772
  // Annotate the CommonJS export names for ESM import in node:
709
773
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["// Public types\nexport type {\n STTModelSize,\n STTBackend,\n STTStatus,\n STTCorrectionProvider,\n STTStreamingProvider,\n STTCorrectionConfig,\n STTStreamingConfig,\n STTChunkingConfig,\n STTConfig,\n ResolvedSTTConfig,\n STTState,\n STTError,\n STTEvents,\n AudioCaptureHandle,\n} from './types.js';\n\n// Public values\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\n\n// Event emitter\nexport { TypedEventEmitter } from './event-emitter.js';\n\n// Audio capture\nexport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\n\n// Worker manager\nexport { WorkerManager } from './worker-manager.js';\nexport type { WorkerManagerEvents } from './worker-manager.js';\n\n// Correction orchestrator\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n// Speech streaming\nexport { SpeechStreamingManager } from './speech-streaming.js';\n\n// STT Engine (main public API)\nexport { STTEngine } from './stt-engine.js';\n","/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/* ─── Language mapping ──────────────────────────────────── */\n\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\nconst WHISPER_TO_BCP47: Record<string, string> = {\n en: 'en-US', english: 'en-US',\n zh: 'zh-CN', chinese: 'zh-CN',\n de: 'de-DE', german: 'de-DE',\n es: 'es-ES', spanish: 'es-ES',\n ru: 'ru-RU', russian: 'ru-RU',\n ko: 'ko-KR', korean: 'ko-KR',\n fr: 'fr-FR', french: 'fr-FR',\n ja: 'ja-JP', japanese: 'ja-JP',\n pt: 'pt-BR', portuguese: 'pt-BR',\n tr: 'tr-TR', turkish: 'tr-TR',\n pl: 'pl-PL', polish: 'pl-PL',\n nl: 'nl-NL', dutch: 'nl-NL',\n ar: 'ar-SA', arabic: 'ar-SA',\n sv: 'sv-SE', swedish: 'sv-SE',\n it: 'it-IT', italian: 'it-IT',\n id: 'id-ID', indonesian: 'id-ID',\n hi: 'hi-IN', hindi: 'hi-IN',\n fi: 'fi-FI', finnish: 'fi-FI',\n vi: 'vi-VN', vietnamese: 'vi-VN',\n he: 'he-IL', hebrew: 'he-IL',\n uk: 'uk-UA', ukrainian: 'uk-UA',\n el: 'el-GR', greek: 'el-GR',\n ms: 'ms-MY', malay: 'ms-MY',\n cs: 'cs-CZ', czech: 'cs-CZ',\n ro: 'ro-RO', romanian: 'ro-RO',\n da: 'da-DK', danish: 'da-DK',\n hu: 'hu-HU', hungarian: 'hu-HU',\n no: 'nb-NO', norwegian: 'nb-NO',\n th: 'th-TH', thai: 'th-TH',\n};\n\n/**\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\n * Already-BCP-47 codes (containing '-') pass through unchanged.\n */\nfunction toBCP47(language: string): string {\n if (language.includes('-')) return language;\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\n}\n\n/* ─── SpeechStreamingManager ────────────────────────────── */\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nconst NO_RESULT_TIMEOUT_MS = 5_000;\n\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private receivedResult = false;\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Start streaming recognition. No-op if Speech API unavailable. */\n start(language: string): void {\n const SR = getSpeechRecognition();\n if (!SR) return;\n\n this.accumulated = '';\n this.active = true;\n this.receivedResult = false;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = toBCP47(language);\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n // Detect silent failure: if no onresult fires within timeout, emit error\n this.clearNoResultTimer();\n this.noResultTimer = setTimeout(() => {\n if (this.active && !this.receivedResult) {\n this.onError?.(\n 'Speech streaming started but received no results. ' +\n 'Mic may be blocked by another audio capture.',\n );\n }\n }, NO_RESULT_TIMEOUT_MS);\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n this.receivedResult = true;\n this.clearNoResultTimer();\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.onError?.('Speech recognition failed to restart after pause.');\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n } catch (err) {\n this.recognition = null;\n this.active = false;\n this.clearNoResultTimer();\n this.onError?.(\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n private clearNoResultTimer(): void {\n if (this.noResultTimer) {\n clearTimeout(this.noResultTimer);\n this.noResultTimer = null;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n // Start Speech API BEFORE getUserMedia — avoids mic conflict where\n // the second mic accessor silently fails (no errors, no events).\n // This matches the working ClaudeWebCLI order.\n if (this.config.streaming.enabled) {\n this.speechStreaming.start(this.config.language);\n }\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnTranscript((text) => {\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["// Public types\nexport type {\n STTModelSize,\n STTBackend,\n STTStatus,\n STTCorrectionProvider,\n STTStreamingProvider,\n STTCorrectionConfig,\n STTStreamingConfig,\n STTChunkingConfig,\n STTConfig,\n ResolvedSTTConfig,\n STTState,\n STTError,\n STTEvents,\n AudioCaptureHandle,\n} from './types.js';\n\n// Public values\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\n\n// Event emitter\nexport { TypedEventEmitter } from './event-emitter.js';\n\n// Audio capture\nexport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\n\n// Worker manager\nexport { WorkerManager } from './worker-manager.js';\nexport type { WorkerManagerEvents } from './worker-manager.js';\n\n// Correction orchestrator\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n// Speech streaming\nexport { SpeechStreamingManager } from './speech-streaming.js';\n\n// STT Engine (main public API)\nexport { STTEngine } from './stt-engine.js';\n","/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\n debug: (message: string) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onaudiostart: (() => void) | null;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/* ─── Language mapping ──────────────────────────────────── */\n\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\nconst WHISPER_TO_BCP47: Record<string, string> = {\n en: 'en-US',\n english: 'en-US',\n zh: 'zh-CN',\n chinese: 'zh-CN',\n de: 'de-DE',\n german: 'de-DE',\n es: 'es-ES',\n spanish: 'es-ES',\n ru: 'ru-RU',\n russian: 'ru-RU',\n ko: 'ko-KR',\n korean: 'ko-KR',\n fr: 'fr-FR',\n french: 'fr-FR',\n ja: 'ja-JP',\n japanese: 'ja-JP',\n pt: 'pt-BR',\n portuguese: 'pt-BR',\n tr: 'tr-TR',\n turkish: 'tr-TR',\n pl: 'pl-PL',\n polish: 'pl-PL',\n nl: 'nl-NL',\n dutch: 'nl-NL',\n ar: 'ar-SA',\n arabic: 'ar-SA',\n sv: 'sv-SE',\n swedish: 'sv-SE',\n it: 'it-IT',\n italian: 'it-IT',\n id: 'id-ID',\n indonesian: 'id-ID',\n hi: 'hi-IN',\n hindi: 'hi-IN',\n fi: 'fi-FI',\n finnish: 'fi-FI',\n vi: 'vi-VN',\n vietnamese: 'vi-VN',\n he: 'he-IL',\n hebrew: 'he-IL',\n uk: 'uk-UA',\n ukrainian: 'uk-UA',\n el: 'el-GR',\n greek: 'el-GR',\n ms: 'ms-MY',\n malay: 'ms-MY',\n cs: 'cs-CZ',\n czech: 'cs-CZ',\n ro: 'ro-RO',\n romanian: 'ro-RO',\n da: 'da-DK',\n danish: 'da-DK',\n hu: 'hu-HU',\n hungarian: 'hu-HU',\n no: 'nb-NO',\n norwegian: 'nb-NO',\n th: 'th-TH',\n thai: 'th-TH',\n};\n\n/**\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\n * Already-BCP-47 codes (containing '-') pass through unchanged.\n */\nfunction toBCP47(language: string): string {\n if (language.includes('-')) return language;\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\n}\n\n/* ─── SpeechStreamingManager ────────────────────────────── */\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nconst NO_RESULT_TIMEOUT_MS = 5_000;\n\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private receivedResult = false;\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n private onDebug: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Set callback for diagnostic debug messages. */\n setOnDebug(fn: (message: string) => void): void {\n this.onDebug = fn;\n }\n\n private log(message: string): void {\n this.onDebug?.(message);\n console.warn(message);\n }\n\n /**\n * Start streaming recognition. Returns a Promise that resolves once\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\n * a 300ms fallback — whichever comes first. The engine should await\n * this before calling getUserMedia to avoid dual-mic conflicts.\n */\n start(language: string): Promise<void> {\n const SR = getSpeechRecognition();\n if (!SR) {\n this.log('[SSM] SpeechRecognition not available in this environment');\n return Promise.resolve();\n }\n\n const bcp47 = toBCP47(language);\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\n\n this.accumulated = '';\n this.active = true;\n this.receivedResult = false;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = bcp47;\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\n // This ensures getUserMedia doesn't compete for the mic.\n let micReady = false;\n const micClaimPromise = new Promise<void>((resolve) => {\n recognition.onaudiostart = () => {\n if (this.recognition !== recognition) return;\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\n if (!micReady) {\n micReady = true;\n resolve();\n }\n };\n // Fallback: resolve after 300ms even if onaudiostart never fires\n setTimeout(() => {\n if (!micReady) {\n micReady = true;\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\n resolve();\n }\n }, 300);\n });\n\n // Detect silent failure: if no onresult fires within timeout, emit error\n this.clearNoResultTimer();\n this.noResultTimer = setTimeout(() => {\n if (this.active && !this.receivedResult) {\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\n this.onError?.(\n 'Speech streaming started but received no results. ' +\n 'Mic may be blocked by another audio capture.',\n );\n }\n }, NO_RESULT_TIMEOUT_MS);\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n this.receivedResult = true;\n this.clearNoResultTimer();\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n this.log(\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\n );\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.log(`[SSM] onerror — ${e.error}`);\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n this.log('[SSM] restarted after pause');\n } catch (err) {\n this.log(`[SSM] restart THREW: ${err}`);\n this.recognition = null;\n this.onError?.('Speech recognition failed to restart after pause.');\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n this.log('[SSM] recognition.start() succeeded');\n } catch (err) {\n this.log(`[SSM] recognition.start() THREW: ${err}`);\n this.recognition = null;\n this.active = false;\n this.clearNoResultTimer();\n this.onError?.(\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\n );\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\n }\n\n return micClaimPromise;\n }\n\n private clearNoResultTimer(): void {\n if (this.noResultTimer) {\n clearTimeout(this.noResultTimer);\n this.noResultTimer = null;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n this.onDebug = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n // Start Speech API BEFORE getUserMedia and wait for it to claim\n // the mic (onaudiostart or 300ms fallback). Without this wait,\n // getUserMedia opens a competing audio capture and Chrome's\n // SpeechRecognition receives no audio (silent failure).\n this.emitDebug(\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\"`,\n );\n if (this.config.streaming.enabled) {\n await this.speechStreaming.start(this.config.language);\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\n }\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnDebug((message) => {\n this.emit('debug', message);\n });\n\n this.speechStreaming.setOnTranscript((text) => {\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.emitDebug('[STT] pause callback — triggering correction');\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n\n private emitDebug(message: string): void {\n console.warn(message);\n this.emit('debug', message);\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -85,6 +85,8 @@ type STTEvents = {
85
85
  error: (error: STTError) => void;
86
86
  /** Engine state change. */
87
87
  status: (state: STTState) => void;
88
+ /** Diagnostic log for debugging (subscribe to capture all internal events). */
89
+ debug: (message: string) => void;
88
90
  };
89
91
  /** Handle returned by audio capture — used internally. */
90
92
  interface AudioCaptureHandle {
@@ -201,6 +203,7 @@ declare class SpeechStreamingManager {
201
203
  private onTranscript;
202
204
  private onPause;
203
205
  private onError;
206
+ private onDebug;
204
207
  /** Check if the Web Speech API is available in this environment. */
205
208
  static isSupported(): boolean;
206
209
  /** Set callback for streaming transcript updates (interim + final text). */
@@ -209,8 +212,16 @@ declare class SpeechStreamingManager {
209
212
  setOnPause(fn: () => void): void;
210
213
  /** Set callback for errors. */
211
214
  setOnError(fn: (message: string) => void): void;
212
- /** Start streaming recognition. No-op if Speech API unavailable. */
213
- start(language: string): void;
215
+ /** Set callback for diagnostic debug messages. */
216
+ setOnDebug(fn: (message: string) => void): void;
217
+ private log;
218
+ /**
219
+ * Start streaming recognition. Returns a Promise that resolves once
220
+ * SpeechRecognition has claimed the microphone (onaudiostart) or after
221
+ * a 300ms fallback — whichever comes first. The engine should await
222
+ * this before calling getUserMedia to avoid dual-mic conflicts.
223
+ */
224
+ start(language: string): Promise<void>;
214
225
  private clearNoResultTimer;
215
226
  /** Stop streaming recognition and return accumulated text. */
216
227
  stop(): string;
@@ -262,6 +273,7 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
262
273
  private setupWorkerListeners;
263
274
  private updateStatus;
264
275
  private emitError;
276
+ private emitDebug;
265
277
  }
266
278
 
267
279
  export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
package/dist/index.d.ts CHANGED
@@ -85,6 +85,8 @@ type STTEvents = {
85
85
  error: (error: STTError) => void;
86
86
  /** Engine state change. */
87
87
  status: (state: STTState) => void;
88
+ /** Diagnostic log for debugging (subscribe to capture all internal events). */
89
+ debug: (message: string) => void;
88
90
  };
89
91
  /** Handle returned by audio capture — used internally. */
90
92
  interface AudioCaptureHandle {
@@ -201,6 +203,7 @@ declare class SpeechStreamingManager {
201
203
  private onTranscript;
202
204
  private onPause;
203
205
  private onError;
206
+ private onDebug;
204
207
  /** Check if the Web Speech API is available in this environment. */
205
208
  static isSupported(): boolean;
206
209
  /** Set callback for streaming transcript updates (interim + final text). */
@@ -209,8 +212,16 @@ declare class SpeechStreamingManager {
209
212
  setOnPause(fn: () => void): void;
210
213
  /** Set callback for errors. */
211
214
  setOnError(fn: (message: string) => void): void;
212
- /** Start streaming recognition. No-op if Speech API unavailable. */
213
- start(language: string): void;
215
+ /** Set callback for diagnostic debug messages. */
216
+ setOnDebug(fn: (message: string) => void): void;
217
+ private log;
218
+ /**
219
+ * Start streaming recognition. Returns a Promise that resolves once
220
+ * SpeechRecognition has claimed the microphone (onaudiostart) or after
221
+ * a 300ms fallback — whichever comes first. The engine should await
222
+ * this before calling getUserMedia to avoid dual-mic conflicts.
223
+ */
224
+ start(language: string): Promise<void>;
214
225
  private clearNoResultTimer;
215
226
  /** Stop streaming recognition and return accumulated text. */
216
227
  stop(): string;
@@ -262,6 +273,7 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
262
273
  private setupWorkerListeners;
263
274
  private updateStatus;
264
275
  private emitError;
276
+ private emitDebug;
265
277
  }
266
278
 
267
279
  export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
package/dist/index.js CHANGED
@@ -372,6 +372,7 @@ var SpeechStreamingManager = class {
372
372
  onTranscript = null;
373
373
  onPause = null;
374
374
  onError = null;
375
+ onDebug = null;
375
376
  /** Check if the Web Speech API is available in this environment. */
376
377
  static isSupported() {
377
378
  return getSpeechRecognition() !== null;
@@ -388,22 +389,59 @@ var SpeechStreamingManager = class {
388
389
  setOnError(fn) {
389
390
  this.onError = fn;
390
391
  }
391
- /** Start streaming recognition. No-op if Speech API unavailable. */
392
+ /** Set callback for diagnostic debug messages. */
393
+ setOnDebug(fn) {
394
+ this.onDebug = fn;
395
+ }
396
+ log(message) {
397
+ this.onDebug?.(message);
398
+ console.warn(message);
399
+ }
400
+ /**
401
+ * Start streaming recognition. Returns a Promise that resolves once
402
+ * SpeechRecognition has claimed the microphone (onaudiostart) or after
403
+ * a 300ms fallback — whichever comes first. The engine should await
404
+ * this before calling getUserMedia to avoid dual-mic conflicts.
405
+ */
392
406
  start(language) {
393
407
  const SR = getSpeechRecognition();
394
- if (!SR) return;
408
+ if (!SR) {
409
+ this.log("[SSM] SpeechRecognition not available in this environment");
410
+ return Promise.resolve();
411
+ }
412
+ const bcp47 = toBCP47(language);
413
+ this.log(`[SSM] start() \u2014 lang: "${language}" \u2192 "${bcp47}"`);
395
414
  this.accumulated = "";
396
415
  this.active = true;
397
416
  this.receivedResult = false;
398
417
  const recognition = new SR();
399
418
  recognition.continuous = true;
400
419
  recognition.interimResults = true;
401
- recognition.lang = toBCP47(language);
420
+ recognition.lang = bcp47;
402
421
  let lastFinalIndex = -1;
403
422
  let lastFinalText = "";
423
+ let micReady = false;
424
+ const micClaimPromise = new Promise((resolve) => {
425
+ recognition.onaudiostart = () => {
426
+ if (this.recognition !== recognition) return;
427
+ this.log("[SSM] onaudiostart \u2014 mic acquired by Speech API");
428
+ if (!micReady) {
429
+ micReady = true;
430
+ resolve();
431
+ }
432
+ };
433
+ setTimeout(() => {
434
+ if (!micReady) {
435
+ micReady = true;
436
+ this.log("[SSM] mic-claim fallback \u2014 proceeding after 300ms");
437
+ resolve();
438
+ }
439
+ }, 300);
440
+ });
404
441
  this.clearNoResultTimer();
405
442
  this.noResultTimer = setTimeout(() => {
406
443
  if (this.active && !this.receivedResult) {
444
+ this.log("[SSM] no-result timeout fired \u2014 no onresult in 5s");
407
445
  this.onError?.(
408
446
  "Speech streaming started but received no results. Mic may be blocked by another audio capture."
409
447
  );
@@ -426,6 +464,9 @@ var SpeechStreamingManager = class {
426
464
  interim += t;
427
465
  }
428
466
  }
467
+ this.log(
468
+ `[SSM] onresult \u2014 finals: "${final_}", interim: "${interim}", accumulated: "${this.accumulated}"`
469
+ );
429
470
  if (final_ && final_.trim() !== lastFinalText) {
430
471
  lastFinalText = final_.trim();
431
472
  this.accumulated = this.accumulated ? this.accumulated + " " + final_.trim() : final_.trim();
@@ -438,15 +479,19 @@ var SpeechStreamingManager = class {
438
479
  };
439
480
  recognition.onerror = (e) => {
440
481
  if (this.recognition !== recognition) return;
482
+ this.log(`[SSM] onerror \u2014 ${e.error}`);
441
483
  this.onError?.(e.error);
442
484
  };
443
485
  recognition.onend = () => {
444
486
  if (this.recognition !== recognition) return;
487
+ this.log(`[SSM] onend \u2014 active: ${this.active}, receivedResult: ${this.receivedResult}`);
445
488
  if (this.active) {
446
489
  this.onPause?.();
447
490
  try {
448
491
  recognition.start();
449
- } catch {
492
+ this.log("[SSM] restarted after pause");
493
+ } catch (err) {
494
+ this.log(`[SSM] restart THREW: ${err}`);
450
495
  this.recognition = null;
451
496
  this.onError?.("Speech recognition failed to restart after pause.");
452
497
  }
@@ -457,14 +502,18 @@ var SpeechStreamingManager = class {
457
502
  this.recognition = recognition;
458
503
  try {
459
504
  recognition.start();
505
+ this.log("[SSM] recognition.start() succeeded");
460
506
  } catch (err) {
507
+ this.log(`[SSM] recognition.start() THREW: ${err}`);
461
508
  this.recognition = null;
462
509
  this.active = false;
463
510
  this.clearNoResultTimer();
464
511
  this.onError?.(
465
512
  `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`
466
513
  );
514
+ return Promise.resolve();
467
515
  }
516
+ return micClaimPromise;
468
517
  }
469
518
  clearNoResultTimer() {
470
519
  if (this.noResultTimer) {
@@ -498,6 +547,7 @@ var SpeechStreamingManager = class {
498
547
  this.onTranscript = null;
499
548
  this.onPause = null;
500
549
  this.onError = null;
550
+ this.onDebug = null;
501
551
  }
502
552
  };
503
553
 
@@ -555,8 +605,12 @@ var STTEngine = class extends TypedEventEmitter {
555
605
  throw new Error(`Cannot start: engine is "${this.state.status}", expected "ready"`);
556
606
  }
557
607
  try {
608
+ this.emitDebug(
609
+ `[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}"`
610
+ );
558
611
  if (this.config.streaming.enabled) {
559
- this.speechStreaming.start(this.config.language);
612
+ await this.speechStreaming.start(this.config.language);
613
+ this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
560
614
  }
561
615
  this.capture = await startCapture();
562
616
  this.updateStatus("recording");
@@ -639,13 +693,19 @@ var STTEngine = class extends TypedEventEmitter {
639
693
  }
640
694
  }
641
695
  setupStreamingCallbacks() {
696
+ this.speechStreaming.setOnDebug((message) => {
697
+ this.emit("debug", message);
698
+ });
642
699
  this.speechStreaming.setOnTranscript((text) => {
700
+ this.emitDebug(`[STT] transcript callback \u2014 "${text}"`);
643
701
  this.emit("transcript", text);
644
702
  });
645
703
  this.speechStreaming.setOnPause(() => {
704
+ this.emitDebug("[STT] pause callback \u2014 triggering correction");
646
705
  this.correctionOrchestrator.onPauseDetected();
647
706
  });
648
707
  this.speechStreaming.setOnError((message) => {
708
+ this.emitDebug(`[STT] streaming error \u2014 "${message}"`);
649
709
  this.emitError("STREAMING_ERROR", message);
650
710
  });
651
711
  }
@@ -667,6 +727,10 @@ var STTEngine = class extends TypedEventEmitter {
667
727
  this.state.error = message;
668
728
  this.emit("error", { code, message });
669
729
  }
730
+ emitDebug(message) {
731
+ console.warn(message);
732
+ this.emit("debug", message);
733
+ }
670
734
  };
671
735
  export {
672
736
  CorrectionOrchestrator,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/* ─── Language mapping ──────────────────────────────────── */\n\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\nconst WHISPER_TO_BCP47: Record<string, string> = {\n en: 'en-US', english: 'en-US',\n zh: 'zh-CN', chinese: 'zh-CN',\n de: 'de-DE', german: 'de-DE',\n es: 'es-ES', spanish: 'es-ES',\n ru: 'ru-RU', russian: 'ru-RU',\n ko: 'ko-KR', korean: 'ko-KR',\n fr: 'fr-FR', french: 'fr-FR',\n ja: 'ja-JP', japanese: 'ja-JP',\n pt: 'pt-BR', portuguese: 'pt-BR',\n tr: 'tr-TR', turkish: 'tr-TR',\n pl: 'pl-PL', polish: 'pl-PL',\n nl: 'nl-NL', dutch: 'nl-NL',\n ar: 'ar-SA', arabic: 'ar-SA',\n sv: 'sv-SE', swedish: 'sv-SE',\n it: 'it-IT', italian: 'it-IT',\n id: 'id-ID', indonesian: 'id-ID',\n hi: 'hi-IN', hindi: 'hi-IN',\n fi: 'fi-FI', finnish: 'fi-FI',\n vi: 'vi-VN', vietnamese: 'vi-VN',\n he: 'he-IL', hebrew: 'he-IL',\n uk: 'uk-UA', ukrainian: 'uk-UA',\n el: 'el-GR', greek: 'el-GR',\n ms: 'ms-MY', malay: 'ms-MY',\n cs: 'cs-CZ', czech: 'cs-CZ',\n ro: 'ro-RO', romanian: 'ro-RO',\n da: 'da-DK', danish: 'da-DK',\n hu: 'hu-HU', hungarian: 'hu-HU',\n no: 'nb-NO', norwegian: 'nb-NO',\n th: 'th-TH', thai: 'th-TH',\n};\n\n/**\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\n * Already-BCP-47 codes (containing '-') pass through unchanged.\n */\nfunction toBCP47(language: string): string {\n if (language.includes('-')) return language;\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\n}\n\n/* ─── SpeechStreamingManager ────────────────────────────── */\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nconst NO_RESULT_TIMEOUT_MS = 5_000;\n\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private receivedResult = false;\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Start streaming recognition. No-op if Speech API unavailable. */\n start(language: string): void {\n const SR = getSpeechRecognition();\n if (!SR) return;\n\n this.accumulated = '';\n this.active = true;\n this.receivedResult = false;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = toBCP47(language);\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n // Detect silent failure: if no onresult fires within timeout, emit error\n this.clearNoResultTimer();\n this.noResultTimer = setTimeout(() => {\n if (this.active && !this.receivedResult) {\n this.onError?.(\n 'Speech streaming started but received no results. ' +\n 'Mic may be blocked by another audio capture.',\n );\n }\n }, NO_RESULT_TIMEOUT_MS);\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n this.receivedResult = true;\n this.clearNoResultTimer();\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.onError?.('Speech recognition failed to restart after pause.');\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n } catch (err) {\n this.recognition = null;\n this.active = false;\n this.clearNoResultTimer();\n this.onError?.(\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n private clearNoResultTimer(): void {\n if (this.noResultTimer) {\n clearTimeout(this.noResultTimer);\n this.noResultTimer = null;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n // Start Speech API BEFORE getUserMedia — avoids mic conflict where\n // the second mic accessor silently fails (no errors, no events).\n // This matches the working ClaudeWebCLI order.\n if (this.config.streaming.enabled) {\n this.speechStreaming.start(this.config.language);\n }\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnTranscript((text) => {\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\n debug: (message: string) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onaudiostart: (() => void) | null;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/* ─── Language mapping ──────────────────────────────────── */\n\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\nconst WHISPER_TO_BCP47: Record<string, string> = {\n en: 'en-US',\n english: 'en-US',\n zh: 'zh-CN',\n chinese: 'zh-CN',\n de: 'de-DE',\n german: 'de-DE',\n es: 'es-ES',\n spanish: 'es-ES',\n ru: 'ru-RU',\n russian: 'ru-RU',\n ko: 'ko-KR',\n korean: 'ko-KR',\n fr: 'fr-FR',\n french: 'fr-FR',\n ja: 'ja-JP',\n japanese: 'ja-JP',\n pt: 'pt-BR',\n portuguese: 'pt-BR',\n tr: 'tr-TR',\n turkish: 'tr-TR',\n pl: 'pl-PL',\n polish: 'pl-PL',\n nl: 'nl-NL',\n dutch: 'nl-NL',\n ar: 'ar-SA',\n arabic: 'ar-SA',\n sv: 'sv-SE',\n swedish: 'sv-SE',\n it: 'it-IT',\n italian: 'it-IT',\n id: 'id-ID',\n indonesian: 'id-ID',\n hi: 'hi-IN',\n hindi: 'hi-IN',\n fi: 'fi-FI',\n finnish: 'fi-FI',\n vi: 'vi-VN',\n vietnamese: 'vi-VN',\n he: 'he-IL',\n hebrew: 'he-IL',\n uk: 'uk-UA',\n ukrainian: 'uk-UA',\n el: 'el-GR',\n greek: 'el-GR',\n ms: 'ms-MY',\n malay: 'ms-MY',\n cs: 'cs-CZ',\n czech: 'cs-CZ',\n ro: 'ro-RO',\n romanian: 'ro-RO',\n da: 'da-DK',\n danish: 'da-DK',\n hu: 'hu-HU',\n hungarian: 'hu-HU',\n no: 'nb-NO',\n norwegian: 'nb-NO',\n th: 'th-TH',\n thai: 'th-TH',\n};\n\n/**\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\n * Already-BCP-47 codes (containing '-') pass through unchanged.\n */\nfunction toBCP47(language: string): string {\n if (language.includes('-')) return language;\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\n}\n\n/* ─── SpeechStreamingManager ────────────────────────────── */\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nconst NO_RESULT_TIMEOUT_MS = 5_000;\n\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private receivedResult = false;\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n private onDebug: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Set callback for diagnostic debug messages. */\n setOnDebug(fn: (message: string) => void): void {\n this.onDebug = fn;\n }\n\n private log(message: string): void {\n this.onDebug?.(message);\n console.warn(message);\n }\n\n /**\n * Start streaming recognition. Returns a Promise that resolves once\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\n * a 300ms fallback — whichever comes first. The engine should await\n * this before calling getUserMedia to avoid dual-mic conflicts.\n */\n start(language: string): Promise<void> {\n const SR = getSpeechRecognition();\n if (!SR) {\n this.log('[SSM] SpeechRecognition not available in this environment');\n return Promise.resolve();\n }\n\n const bcp47 = toBCP47(language);\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\n\n this.accumulated = '';\n this.active = true;\n this.receivedResult = false;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = bcp47;\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\n // This ensures getUserMedia doesn't compete for the mic.\n let micReady = false;\n const micClaimPromise = new Promise<void>((resolve) => {\n recognition.onaudiostart = () => {\n if (this.recognition !== recognition) return;\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\n if (!micReady) {\n micReady = true;\n resolve();\n }\n };\n // Fallback: resolve after 300ms even if onaudiostart never fires\n setTimeout(() => {\n if (!micReady) {\n micReady = true;\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\n resolve();\n }\n }, 300);\n });\n\n // Detect silent failure: if no onresult fires within timeout, emit error\n this.clearNoResultTimer();\n this.noResultTimer = setTimeout(() => {\n if (this.active && !this.receivedResult) {\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\n this.onError?.(\n 'Speech streaming started but received no results. ' +\n 'Mic may be blocked by another audio capture.',\n );\n }\n }, NO_RESULT_TIMEOUT_MS);\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n this.receivedResult = true;\n this.clearNoResultTimer();\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n this.log(\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\n );\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.log(`[SSM] onerror — ${e.error}`);\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n this.log('[SSM] restarted after pause');\n } catch (err) {\n this.log(`[SSM] restart THREW: ${err}`);\n this.recognition = null;\n this.onError?.('Speech recognition failed to restart after pause.');\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n this.log('[SSM] recognition.start() succeeded');\n } catch (err) {\n this.log(`[SSM] recognition.start() THREW: ${err}`);\n this.recognition = null;\n this.active = false;\n this.clearNoResultTimer();\n this.onError?.(\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\n );\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\n }\n\n return micClaimPromise;\n }\n\n private clearNoResultTimer(): void {\n if (this.noResultTimer) {\n clearTimeout(this.noResultTimer);\n this.noResultTimer = null;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n this.clearNoResultTimer();\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n this.onDebug = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n // Start Speech API BEFORE getUserMedia and wait for it to claim\n // the mic (onaudiostart or 300ms fallback). Without this wait,\n // getUserMedia opens a competing audio capture and Chrome's\n // SpeechRecognition receives no audio (silent failure).\n this.emitDebug(\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\"`,\n );\n if (this.config.streaming.enabled) {\n await this.speechStreaming.start(this.config.language);\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\n }\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnDebug((message) => {\n this.emit('debug', message);\n });\n\n this.speechStreaming.setOnTranscript((text) => {\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.emitDebug('[STT] pause callback — triggering correction');\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n\n private emitDebug(message: string): void {\n console.warn(message);\n this.emit('debug', message);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/stt-component",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Framework-agnostic speech-to-text with real-time streaming transcription and mid-recording Whisper correction",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",