@tekyzinc/stt-component 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +95 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -5
- package/dist/index.d.ts +33 -5
- package/dist/index.js +93 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -26,8 +26,10 @@ __export(index_exports, {
|
|
|
26
26
|
SpeechStreamingManager: () => SpeechStreamingManager,
|
|
27
27
|
TypedEventEmitter: () => TypedEventEmitter,
|
|
28
28
|
WorkerManager: () => WorkerManager,
|
|
29
|
+
pauseCapture: () => pauseCapture,
|
|
29
30
|
resampleAudio: () => resampleAudio,
|
|
30
31
|
resolveConfig: () => resolveConfig,
|
|
32
|
+
resumeCapture: () => resumeCapture,
|
|
31
33
|
snapshotAudio: () => snapshotAudio,
|
|
32
34
|
startCapture: () => startCapture,
|
|
33
35
|
stopCapture: () => stopCapture
|
|
@@ -43,8 +45,8 @@ var DEFAULT_STT_CONFIG = {
|
|
|
43
45
|
correction: {
|
|
44
46
|
enabled: true,
|
|
45
47
|
provider: "whisper",
|
|
46
|
-
pauseThreshold:
|
|
47
|
-
forcedInterval:
|
|
48
|
+
pauseThreshold: 1e3,
|
|
49
|
+
forcedInterval: 3e3
|
|
48
50
|
},
|
|
49
51
|
chunking: {
|
|
50
52
|
chunkLengthS: 30,
|
|
@@ -133,7 +135,19 @@ async function startCapture() {
|
|
|
133
135
|
source.connect(processor);
|
|
134
136
|
processor.connect(silencer);
|
|
135
137
|
silencer.connect(audioCtx.destination);
|
|
136
|
-
return { audioCtx, stream, samples, _processor: processor };
|
|
138
|
+
return { audioCtx, stream, samples, _processor: processor, _source: source, _silencer: silencer };
|
|
139
|
+
}
|
|
140
|
+
async function pauseCapture(capture) {
|
|
141
|
+
capture._source.disconnect();
|
|
142
|
+
const currentSamples = [...capture.samples];
|
|
143
|
+
capture.samples.length = 0;
|
|
144
|
+
return resampleAudio(currentSamples, capture.audioCtx.sampleRate);
|
|
145
|
+
}
|
|
146
|
+
async function resumeCapture(capture) {
|
|
147
|
+
if (capture.audioCtx.state === "suspended") {
|
|
148
|
+
await capture.audioCtx.resume();
|
|
149
|
+
}
|
|
150
|
+
capture._source.connect(capture._processor);
|
|
137
151
|
}
|
|
138
152
|
function snapshotAudio(capture) {
|
|
139
153
|
return [...capture.samples];
|
|
@@ -179,8 +193,17 @@ var import_meta = {};
|
|
|
179
193
|
var WorkerManager = class extends TypedEventEmitter {
|
|
180
194
|
worker = null;
|
|
181
195
|
transcribeResolve = null;
|
|
196
|
+
currentTranscribePromise = null;
|
|
182
197
|
modelReadyResolve = null;
|
|
183
198
|
modelReadyReject = null;
|
|
199
|
+
/** True while a transcription job is running in the worker. */
|
|
200
|
+
get isTranscribing() {
|
|
201
|
+
return this.transcribeResolve !== null;
|
|
202
|
+
}
|
|
203
|
+
/** Await the current in-flight transcription without starting a new one. */
|
|
204
|
+
awaitCurrentTranscription() {
|
|
205
|
+
return this.currentTranscribePromise ?? Promise.resolve("");
|
|
206
|
+
}
|
|
184
207
|
/** Spawn the Web Worker. Must be called before loadModel/transcribe. */
|
|
185
208
|
spawn(workerUrl) {
|
|
186
209
|
if (this.worker) return;
|
|
@@ -216,10 +239,11 @@ var WorkerManager = class extends TypedEventEmitter {
|
|
|
216
239
|
async transcribe(audio) {
|
|
217
240
|
if (!this.worker) throw new Error("Worker not spawned");
|
|
218
241
|
if (audio.length === 0) return "";
|
|
219
|
-
|
|
242
|
+
this.currentTranscribePromise = new Promise((resolve) => {
|
|
220
243
|
this.transcribeResolve = resolve;
|
|
221
244
|
this.worker.postMessage({ type: "transcribe", audio }, [audio.buffer]);
|
|
222
245
|
});
|
|
246
|
+
return this.currentTranscribePromise;
|
|
223
247
|
}
|
|
224
248
|
/** Cancel any in-flight transcription. */
|
|
225
249
|
cancel() {
|
|
@@ -439,8 +463,12 @@ var SpeechStreamingManager = class {
|
|
|
439
463
|
* SpeechRecognition has claimed the microphone (onaudiostart) or after
|
|
440
464
|
* a 300ms fallback — whichever comes first. The engine should await
|
|
441
465
|
* this before calling getUserMedia to avoid dual-mic conflicts.
|
|
466
|
+
*
|
|
467
|
+
* When skipMicWait is true (warm restart — mic already active), returns
|
|
468
|
+
* immediately after calling recognition.start() without waiting for
|
|
469
|
+
* onaudiostart.
|
|
442
470
|
*/
|
|
443
|
-
start(language) {
|
|
471
|
+
start(language, skipMicWait = false) {
|
|
444
472
|
const SR = getSpeechRecognition();
|
|
445
473
|
if (!SR) {
|
|
446
474
|
this.log("[SSM] SpeechRecognition not available in this environment");
|
|
@@ -550,6 +578,10 @@ var SpeechStreamingManager = class {
|
|
|
550
578
|
);
|
|
551
579
|
return Promise.resolve();
|
|
552
580
|
}
|
|
581
|
+
if (skipMicWait) {
|
|
582
|
+
this.log("[SSM] skipMicWait \u2014 warm restart, returning immediately");
|
|
583
|
+
return Promise.resolve();
|
|
584
|
+
}
|
|
553
585
|
return micClaimPromise;
|
|
554
586
|
}
|
|
555
587
|
clearNoResultTimer() {
|
|
@@ -597,6 +629,8 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
597
629
|
capture = null;
|
|
598
630
|
state;
|
|
599
631
|
workerUrl;
|
|
632
|
+
/** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
|
|
633
|
+
_stopping = false;
|
|
600
634
|
/**
|
|
601
635
|
* Create a new STT engine instance.
|
|
602
636
|
* @param config - Optional configuration overrides (model, backend, language, etc.).
|
|
@@ -642,14 +676,22 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
642
676
|
throw new Error(`Cannot start: engine is "${this.state.status}", expected "ready"`);
|
|
643
677
|
}
|
|
644
678
|
try {
|
|
679
|
+
const warmCapture = this.capture && this.capture.stream.getTracks().every((t) => t.readyState === "live");
|
|
645
680
|
this.emitDebug(
|
|
646
|
-
`[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}"`
|
|
681
|
+
`[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}", warm: ${!!warmCapture}`
|
|
647
682
|
);
|
|
648
683
|
if (this.config.streaming.enabled) {
|
|
649
|
-
await this.speechStreaming.start(this.config.language);
|
|
650
|
-
|
|
684
|
+
await this.speechStreaming.start(this.config.language, !!warmCapture);
|
|
685
|
+
if (!warmCapture) {
|
|
686
|
+
this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (warmCapture) {
|
|
690
|
+
await resumeCapture(this.capture);
|
|
691
|
+
this.emitDebug("[STT] warm mic resumed \u2014 skipped getUserMedia");
|
|
692
|
+
} else {
|
|
693
|
+
this.capture = await startCapture();
|
|
651
694
|
}
|
|
652
|
-
this.capture = await startCapture();
|
|
653
695
|
this.updateStatus("recording");
|
|
654
696
|
this.correctionOrchestrator.start();
|
|
655
697
|
} catch (err) {
|
|
@@ -659,16 +701,49 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
659
701
|
);
|
|
660
702
|
}
|
|
661
703
|
}
|
|
662
|
-
/** Stop recording, run final transcription, return text.
|
|
704
|
+
/** Stop recording, run final transcription, return text.
|
|
705
|
+
* Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
|
|
663
706
|
async stop() {
|
|
664
707
|
if (!this.capture) return "";
|
|
708
|
+
this._stopping = true;
|
|
665
709
|
this.correctionOrchestrator.stop();
|
|
666
710
|
this.speechStreaming.stop();
|
|
667
|
-
this.workerManager.cancel();
|
|
668
711
|
this.updateStatus("processing");
|
|
712
|
+
if (this.workerManager.isTranscribing) {
|
|
713
|
+
try {
|
|
714
|
+
const [audio, inFlightText] = await Promise.all([
|
|
715
|
+
pauseCapture(this.capture),
|
|
716
|
+
this.workerManager.awaitCurrentTranscription()
|
|
717
|
+
]);
|
|
718
|
+
this._stopping = false;
|
|
719
|
+
const text = inFlightText.trim();
|
|
720
|
+
if (text) {
|
|
721
|
+
this.emit("correction", text);
|
|
722
|
+
this.updateStatus("ready");
|
|
723
|
+
return text;
|
|
724
|
+
}
|
|
725
|
+
if (audio.length > 0) {
|
|
726
|
+
const freshText = await this.workerManager.transcribe(audio);
|
|
727
|
+
this.emit("correction", freshText);
|
|
728
|
+
this.updateStatus("ready");
|
|
729
|
+
return freshText;
|
|
730
|
+
}
|
|
731
|
+
this.updateStatus("ready");
|
|
732
|
+
return "";
|
|
733
|
+
} catch (err) {
|
|
734
|
+
this._stopping = false;
|
|
735
|
+
this.emitError(
|
|
736
|
+
"TRANSCRIPTION_FAILED",
|
|
737
|
+
err instanceof Error ? err.message : "Final transcription failed."
|
|
738
|
+
);
|
|
739
|
+
this.updateStatus("ready");
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
this.workerManager.cancel();
|
|
744
|
+
this._stopping = false;
|
|
669
745
|
try {
|
|
670
|
-
const audio = await
|
|
671
|
-
this.capture = null;
|
|
746
|
+
const audio = await pauseCapture(this.capture);
|
|
672
747
|
if (audio.length === 0) {
|
|
673
748
|
this.updateStatus("ready");
|
|
674
749
|
return "";
|
|
@@ -691,6 +766,10 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
691
766
|
this.correctionOrchestrator.stop();
|
|
692
767
|
this.speechStreaming.destroy();
|
|
693
768
|
if (this.capture) {
|
|
769
|
+
try {
|
|
770
|
+
this.capture._processor.disconnect();
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
694
773
|
for (const track of this.capture.stream.getTracks()) {
|
|
695
774
|
track.stop();
|
|
696
775
|
}
|
|
@@ -719,7 +798,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
719
798
|
const audio = await resampleAudio(samples, nativeSr);
|
|
720
799
|
if (audio.length === 0) return;
|
|
721
800
|
const text = await this.workerManager.transcribe(audio);
|
|
722
|
-
if (text.trim() && this.capture) {
|
|
801
|
+
if (text.trim() && this.capture && !this._stopping) {
|
|
723
802
|
this.emit("correction", text);
|
|
724
803
|
}
|
|
725
804
|
} catch (err) {
|
|
@@ -777,8 +856,10 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
777
856
|
SpeechStreamingManager,
|
|
778
857
|
TypedEventEmitter,
|
|
779
858
|
WorkerManager,
|
|
859
|
+
pauseCapture,
|
|
780
860
|
resampleAudio,
|
|
781
861
|
resolveConfig,
|
|
862
|
+
resumeCapture,
|
|
782
863
|
snapshotAudio,
|
|
783
864
|
startCapture,
|
|
784
865
|
stopCapture
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\r\nexport type {\r\n STTModelSize,\r\n STTBackend,\r\n STTStatus,\r\n STTCorrectionProvider,\r\n STTStreamingProvider,\r\n STTCorrectionConfig,\r\n STTStreamingConfig,\r\n STTChunkingConfig,\r\n STTConfig,\r\n ResolvedSTTConfig,\r\n STTState,\r\n STTError,\r\n STTEvents,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\n\r\n// Public values\r\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\r\n\r\n// Event emitter\r\nexport { TypedEventEmitter } from './event-emitter.js';\r\n\r\n// Audio capture\r\nexport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\r\n\r\n// Worker manager\r\nexport { WorkerManager } from './worker-manager.js';\r\nexport type { WorkerManagerEvents } from './worker-manager.js';\r\n\r\n// Correction orchestrator\r\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\n\r\n// Speech streaming\r\nexport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n// STT Engine (main public API)\r\nexport { STTEngine } from './stt-engine.js';\r\n","/** Supported Whisper model sizes. */\r\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\r\n\r\n/** Supported compute backends. */\r\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\r\n\r\n/** Engine lifecycle states. */\r\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\r\n\r\n/** Supported correction engine providers. */\r\nexport type STTCorrectionProvider = 'whisper';\r\n\r\n/** Supported real-time streaming providers. */\r\nexport type STTStreamingProvider = 'web-speech-api';\r\n\r\n/** Correction engine configuration. */\r\nexport interface STTCorrectionConfig {\r\n /** Enable mid-recording correction. Default: true */\r\n enabled?: boolean;\r\n /** Correction engine provider. Default: 'whisper' */\r\n provider?: STTCorrectionProvider;\r\n /** Silence duration (ms) before triggering correction. Default: 3000 */\r\n pauseThreshold?: number;\r\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\r\n forcedInterval?: number;\r\n}\r\n\r\n/** Real-time streaming preview configuration. */\r\nexport interface STTStreamingConfig {\r\n /** Enable real-time streaming transcript. Default: true */\r\n enabled?: boolean;\r\n /** Streaming provider. Default: 'web-speech-api' */\r\n provider?: STTStreamingProvider;\r\n}\r\n\r\n/** Audio chunking configuration for long-form audio. */\r\nexport interface STTChunkingConfig {\r\n /** Chunk length in seconds for Whisper processing. Default: 30 */\r\n chunkLengthS?: number;\r\n /** Stride length in seconds for overlapping chunks. Default: 5 */\r\n strideLengthS?: number;\r\n}\r\n\r\n/** Full engine configuration. All fields optional — sensible defaults applied. */\r\nexport interface STTConfig {\r\n /** Whisper model size. Default: 'tiny' */\r\n model?: STTModelSize;\r\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\r\n backend?: STTBackend;\r\n /** Transcription language. Default: 'en' */\r\n language?: string;\r\n /** Model quantization dtype. Default: 'q4' */\r\n dtype?: string;\r\n /** Mid-recording correction settings. */\r\n correction?: STTCorrectionConfig;\r\n /** Audio chunking settings for long-form audio. */\r\n chunking?: STTChunkingConfig;\r\n /** Web Speech API streaming preview settings. */\r\n streaming?: STTStreamingConfig;\r\n}\r\n\r\n/** Resolved configuration with all defaults applied. */\r\nexport interface ResolvedSTTConfig {\r\n model: STTModelSize;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n correction: Required<STTCorrectionConfig>;\r\n chunking: Required<STTChunkingConfig>;\r\n streaming: Required<STTStreamingConfig>;\r\n}\r\n\r\n/** Engine state exposed to consumers via status events. */\r\nexport interface STTState {\r\n status: STTStatus;\r\n isModelLoaded: boolean;\r\n /** Model download progress (0–100). */\r\n loadProgress: number;\r\n /** Active compute backend, or null if not yet determined. */\r\n backend: 'webgpu' | 'wasm' | null;\r\n error: string | null;\r\n}\r\n\r\n/** Structured error emitted via the 'error' event. */\r\nexport interface STTError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\n/** Event map for the typed event emitter. */\r\nexport type STTEvents = {\r\n /** Streaming interim text during recording. */\r\n transcript: (text: string) => void;\r\n /** Whisper-corrected text replacing interim text. */\r\n correction: (text: string) => void;\r\n /** Actionable error (mic denied, model fail, transcription fail). */\r\n error: (error: STTError) => void;\r\n /** Engine state change. */\r\n status: (state: STTState) => void;\r\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\r\n debug: (message: string) => void;\r\n};\r\n\r\n/** Handle returned by audio capture — used internally. */\r\nexport interface AudioCaptureHandle {\r\n audioCtx: AudioContext;\r\n stream: MediaStream;\r\n samples: Float32Array[];\r\n /** Retain reference to prevent GC from stopping audio processing. */\r\n _processor: ScriptProcessorNode;\r\n}\r\n\r\n/** Message sent from main thread to Whisper worker. */\r\nexport interface WorkerMessage {\r\n type: 'load' | 'transcribe' | 'cancel';\r\n audio?: Float32Array;\r\n config?: {\r\n model: string;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n chunkLengthS: number;\r\n strideLengthS: number;\r\n };\r\n}\r\n\r\n/** Response sent from Whisper worker to main thread. */\r\nexport interface WorkerResponse {\r\n type: 'progress' | 'ready' | 'result' | 'error';\r\n data?: unknown;\r\n}\r\n\r\n/** Default configuration values. */\r\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\r\n model: 'tiny',\r\n backend: 'auto',\r\n language: 'en',\r\n dtype: 'q4',\r\n correction: {\r\n enabled: true,\r\n provider: 'whisper',\r\n pauseThreshold: 3_000,\r\n forcedInterval: 5_000,\r\n },\r\n chunking: {\r\n chunkLengthS: 30,\r\n strideLengthS: 5,\r\n },\r\n streaming: {\r\n enabled: true,\r\n provider: 'web-speech-api',\r\n },\r\n};\r\n\r\n/** Merge user config with defaults to produce resolved config. */\r\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\r\n return {\r\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\r\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\r\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\r\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\r\n correction: {\r\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\r\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\r\n pauseThreshold:\r\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\r\n forcedInterval:\r\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\r\n },\r\n chunking: {\r\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\r\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\r\n },\r\n streaming: {\r\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\r\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\r\n },\r\n };\r\n}\r\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';\r\n\r\nconst TARGET_SAMPLE_RATE = 16_000;\r\n\r\n/**\r\n * Start capturing raw PCM audio from the microphone.\r\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\r\n */\r\nexport async function startCapture(): Promise<AudioCaptureHandle> {\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: { channelCount: 1 },\r\n });\r\n const audioCtx = new AudioContext();\r\n\r\n // Chrome may suspend AudioContext — must resume within user gesture\r\n if (audioCtx.state === 'suspended') {\r\n await audioCtx.resume();\r\n }\r\n\r\n const source = audioCtx.createMediaStreamSource(stream);\r\n const samples: Float32Array[] = [];\r\n\r\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\r\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\r\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\r\n };\r\n\r\n // Connect through a silent gain node so mic audio doesn't play back\r\n const silencer = audioCtx.createGain();\r\n silencer.gain.value = 0;\r\n source.connect(processor);\r\n processor.connect(silencer);\r\n silencer.connect(audioCtx.destination);\r\n\r\n return { audioCtx, stream, samples, _processor: processor };\r\n}\r\n\r\n/**\r\n * Copy current audio buffer without stopping capture.\r\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\r\n */\r\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\r\n return [...capture.samples];\r\n}\r\n\r\n/**\r\n * Concatenate sample chunks and resample to 16kHz for Whisper.\r\n */\r\nexport async function resampleAudio(\r\n samples: Float32Array[],\r\n nativeSr: number,\r\n): Promise<Float32Array> {\r\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\r\n if (totalLength === 0) return new Float32Array(0);\r\n\r\n const fullAudio = new Float32Array(totalLength);\r\n let offset = 0;\r\n for (const s of samples) {\r\n fullAudio.set(s, offset);\r\n offset += s.length;\r\n }\r\n\r\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\r\n\r\n const duration = fullAudio.length / nativeSr;\r\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\r\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\r\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\r\n buffer.getChannelData(0).set(fullAudio);\r\n const src = offline.createBufferSource();\r\n src.buffer = buffer;\r\n src.connect(offline.destination);\r\n src.start(0);\r\n const resampled = await offline.startRendering();\r\n return resampled.getChannelData(0);\r\n}\r\n\r\n/**\r\n * Stop capturing and return resampled audio at 16kHz.\r\n */\r\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n const { audioCtx, stream, samples, _processor } = capture;\r\n\r\n // Disconnect processor to stop capturing\r\n try {\r\n _processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n\r\n // Stop microphone tracks\r\n for (const track of stream.getTracks()) {\r\n track.stop();\r\n }\r\n\r\n const nativeSr = audioCtx.sampleRate;\r\n await audioCtx.close();\r\n\r\n return resampleAudio(samples, nativeSr);\r\n}\r\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 ──────────────────────────────── */\r\n\r\ninterface SpeechRecognitionEvent {\r\n results: SpeechRecognitionResultList;\r\n resultIndex: number;\r\n}\r\n\r\ninterface SpeechRecognitionErrorEvent {\r\n error: string;\r\n}\r\n\r\ninterface SpeechRecognitionInstance {\r\n continuous: boolean;\r\n interimResults: boolean;\r\n lang: string;\r\n onaudiostart: (() => void) | null;\r\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\r\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\r\n onend: (() => void) | null;\r\n start: () => void;\r\n stop: () => void;\r\n abort: () => void;\r\n}\r\n\r\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\r\n\r\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\r\n if (typeof globalThis === 'undefined') return null;\r\n const w = globalThis as unknown as Record<string, unknown>;\r\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\r\n}\r\n\r\n/* ─── Language mapping ──────────────────────────────────── */\r\n\r\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\r\nconst WHISPER_TO_BCP47: Record<string, string> = {\r\n en: 'en-US',\r\n english: 'en-US',\r\n zh: 'zh-CN',\r\n chinese: 'zh-CN',\r\n de: 'de-DE',\r\n german: 'de-DE',\r\n es: 'es-ES',\r\n spanish: 'es-ES',\r\n ru: 'ru-RU',\r\n russian: 'ru-RU',\r\n ko: 'ko-KR',\r\n korean: 'ko-KR',\r\n fr: 'fr-FR',\r\n french: 'fr-FR',\r\n ja: 'ja-JP',\r\n japanese: 'ja-JP',\r\n pt: 'pt-BR',\r\n portuguese: 'pt-BR',\r\n tr: 'tr-TR',\r\n turkish: 'tr-TR',\r\n pl: 'pl-PL',\r\n polish: 'pl-PL',\r\n nl: 'nl-NL',\r\n dutch: 'nl-NL',\r\n ar: 'ar-SA',\r\n arabic: 'ar-SA',\r\n sv: 'sv-SE',\r\n swedish: 'sv-SE',\r\n it: 'it-IT',\r\n italian: 'it-IT',\r\n id: 'id-ID',\r\n indonesian: 'id-ID',\r\n hi: 'hi-IN',\r\n hindi: 'hi-IN',\r\n fi: 'fi-FI',\r\n finnish: 'fi-FI',\r\n vi: 'vi-VN',\r\n vietnamese: 'vi-VN',\r\n he: 'he-IL',\r\n hebrew: 'he-IL',\r\n uk: 'uk-UA',\r\n ukrainian: 'uk-UA',\r\n el: 'el-GR',\r\n greek: 'el-GR',\r\n ms: 'ms-MY',\r\n malay: 'ms-MY',\r\n cs: 'cs-CZ',\r\n czech: 'cs-CZ',\r\n ro: 'ro-RO',\r\n romanian: 'ro-RO',\r\n da: 'da-DK',\r\n danish: 'da-DK',\r\n hu: 'hu-HU',\r\n hungarian: 'hu-HU',\r\n no: 'nb-NO',\r\n norwegian: 'nb-NO',\r\n th: 'th-TH',\r\n thai: 'th-TH',\r\n};\r\n\r\n/**\r\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\r\n * Already-BCP-47 codes (containing '-') pass through unchanged.\r\n */\r\nfunction toBCP47(language: string): string {\r\n if (language.includes('-')) return language;\r\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\r\n}\r\n\r\n/* ─── SpeechStreamingManager ────────────────────────────── */\r\n\r\n/**\r\n * Manages Web Speech API for real-time streaming transcript preview.\r\n * Provides word-by-word interim text while Whisper handles corrections.\r\n */\r\nconst NO_RESULT_TIMEOUT_MS = 5_000;\r\n\r\nexport class SpeechStreamingManager {\r\n private recognition: SpeechRecognitionInstance | null = null;\r\n private accumulated = '';\r\n private active = false;\r\n private receivedResult = false;\r\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\r\n private onTranscript: ((text: string) => void) | null = null;\r\n private onPause: (() => void) | null = null;\r\n private onError: ((message: string) => void) | null = null;\r\n private onDebug: ((message: string) => void) | null = null;\r\n\r\n /** Check if the Web Speech API is available in this environment. */\r\n static isSupported(): boolean {\r\n return getSpeechRecognition() !== null;\r\n }\r\n\r\n /** Set callback for streaming transcript updates (interim + final text). */\r\n setOnTranscript(fn: (text: string) => void): void {\r\n this.onTranscript = fn;\r\n }\r\n\r\n /** Set callback for speech pause detection (Speech API onend). */\r\n setOnPause(fn: () => void): void {\r\n this.onPause = fn;\r\n }\r\n\r\n /** Set callback for errors. */\r\n setOnError(fn: (message: string) => void): void {\r\n this.onError = fn;\r\n }\r\n\r\n /** Set callback for diagnostic debug messages. */\r\n setOnDebug(fn: (message: string) => void): void {\r\n this.onDebug = fn;\r\n }\r\n\r\n private log(message: string): void {\r\n this.onDebug?.(message);\r\n console.warn(message);\r\n }\r\n\r\n /**\r\n * Start streaming recognition. Returns a Promise that resolves once\r\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\r\n * a 300ms fallback — whichever comes first. The engine should await\r\n * this before calling getUserMedia to avoid dual-mic conflicts.\r\n */\r\n start(language: string): Promise<void> {\r\n const SR = getSpeechRecognition();\r\n if (!SR) {\r\n this.log('[SSM] SpeechRecognition not available in this environment');\r\n return Promise.resolve();\r\n }\r\n\r\n const bcp47 = toBCP47(language);\r\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\r\n\r\n this.accumulated = '';\r\n this.active = true;\r\n this.receivedResult = false;\r\n\r\n const recognition = new SR();\r\n recognition.continuous = true;\r\n recognition.interimResults = true;\r\n recognition.lang = bcp47;\r\n\r\n let lastFinalIndex = -1;\r\n let lastFinalText = '';\r\n\r\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\r\n // This ensures getUserMedia doesn't compete for the mic.\r\n let micReady = false;\r\n const micClaimPromise = new Promise<void>((resolve) => {\r\n recognition.onaudiostart = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\r\n if (!micReady) {\r\n micReady = true;\r\n resolve();\r\n }\r\n };\r\n // Fallback: resolve after 300ms even if onaudiostart never fires\r\n setTimeout(() => {\r\n if (!micReady) {\r\n micReady = true;\r\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\r\n resolve();\r\n }\r\n }, 300);\r\n });\r\n\r\n // Detect silent failure: if no onresult fires within timeout, emit error\r\n this.clearNoResultTimer();\r\n this.noResultTimer = setTimeout(() => {\r\n if (this.active && !this.receivedResult) {\r\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\r\n this.onError?.(\r\n 'Speech streaming started but received no results. ' +\r\n 'Mic may be blocked by another audio capture.',\r\n );\r\n }\r\n }, NO_RESULT_TIMEOUT_MS);\r\n\r\n recognition.onresult = (e: SpeechRecognitionEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.receivedResult = true;\r\n this.clearNoResultTimer();\r\n\r\n let final_ = '';\r\n let interim = '';\r\n for (let i = e.resultIndex; i < e.results.length; i++) {\r\n const t = e.results[i][0].transcript;\r\n if (e.results[i].isFinal) {\r\n if (i > lastFinalIndex) {\r\n final_ += t;\r\n lastFinalIndex = i;\r\n }\r\n } else {\r\n interim += t;\r\n }\r\n }\r\n\r\n this.log(\r\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\r\n );\r\n\r\n if (final_ && final_.trim() !== lastFinalText) {\r\n lastFinalText = final_.trim();\r\n this.accumulated = this.accumulated\r\n ? this.accumulated + ' ' + final_.trim()\r\n : final_.trim();\r\n this.onTranscript?.(this.accumulated);\r\n } else if (interim) {\r\n const trimmed = interim.trimStart();\r\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\r\n this.onTranscript?.(full);\r\n }\r\n };\r\n\r\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onerror — ${e.error}`);\r\n this.onError?.(e.error);\r\n };\r\n\r\n recognition.onend = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\r\n\r\n if (this.active) {\r\n // Speech API paused — trigger correction\r\n this.onPause?.();\r\n // Restart for continued streaming\r\n try {\r\n recognition.start();\r\n this.log('[SSM] restarted after pause');\r\n } catch (err) {\r\n this.log(`[SSM] restart THREW: ${err}`);\r\n this.recognition = null;\r\n this.onError?.('Speech recognition failed to restart after pause.');\r\n }\r\n } else {\r\n this.recognition = null;\r\n }\r\n };\r\n\r\n this.recognition = recognition;\r\n try {\r\n recognition.start();\r\n this.log('[SSM] recognition.start() succeeded');\r\n } catch (err) {\r\n this.log(`[SSM] recognition.start() THREW: ${err}`);\r\n this.recognition = null;\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n this.onError?.(\r\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\r\n }\r\n\r\n return micClaimPromise;\r\n }\r\n\r\n private clearNoResultTimer(): void {\r\n if (this.noResultTimer) {\r\n clearTimeout(this.noResultTimer);\r\n this.noResultTimer = null;\r\n }\r\n }\r\n\r\n /** Stop streaming recognition and return accumulated text. */\r\n stop(): string {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.stop();\r\n }\r\n const result = this.accumulated;\r\n this.accumulated = '';\r\n return result;\r\n }\r\n\r\n /** Abort immediately without returning text. */\r\n destroy(): void {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.abort();\r\n }\r\n this.accumulated = '';\r\n this.onTranscript = null;\r\n this.onPause = null;\r\n this.onError = null;\r\n this.onDebug = null;\r\n }\r\n}\r\n","import type {\r\n STTConfig,\r\n STTState,\r\n STTEvents,\r\n STTStatus,\r\n ResolvedSTTConfig,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\nimport { resolveConfig } from './types.js';\r\nimport { TypedEventEmitter } from './event-emitter.js';\r\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\r\nimport { WorkerManager } from './worker-manager.js';\r\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\nimport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n/**\r\n * Main STT engine — the public API for speech-to-text with Whisper correction.\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const engine = new STTEngine({ model: 'tiny' });\r\n * engine.on('transcript', (text) => console.log(text));\r\n * engine.on('correction', (text) => console.log('corrected:', text));\r\n * await engine.init();\r\n * await engine.start();\r\n * const finalText = await engine.stop();\r\n * ```\r\n */\r\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\r\n private config: ResolvedSTTConfig;\r\n private workerManager: WorkerManager;\r\n private correctionOrchestrator: CorrectionOrchestrator;\r\n private speechStreaming: SpeechStreamingManager;\r\n private capture: AudioCaptureHandle | null = null;\r\n private state: STTState;\r\n private workerUrl?: URL;\r\n\r\n /**\r\n * Create a new STT engine instance.\r\n * @param config - Optional configuration overrides (model, backend, language, etc.).\r\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\r\n */\r\n constructor(config?: STTConfig, workerUrl?: URL) {\r\n super();\r\n this.config = resolveConfig(config);\r\n this.workerManager = new WorkerManager();\r\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\r\n this.speechStreaming = new SpeechStreamingManager();\r\n this.workerUrl = workerUrl;\r\n\r\n this.state = {\r\n status: 'idle',\r\n isModelLoaded: false,\r\n loadProgress: 0,\r\n backend: null,\r\n error: null,\r\n };\r\n\r\n this.correctionOrchestrator.setCorrectionFn(() => {\r\n this.performCorrection();\r\n });\r\n\r\n this.setupWorkerListeners();\r\n this.setupStreamingCallbacks();\r\n }\r\n\r\n /** Initialize the engine: spawn worker and load model. */\r\n async init(): Promise<void> {\r\n this.updateStatus('loading');\r\n this.workerManager.spawn(this.workerUrl);\r\n\r\n try {\r\n await this.workerManager.loadModel(this.config);\r\n this.state.isModelLoaded = true;\r\n this.updateStatus('ready');\r\n } catch (err) {\r\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\r\n this.updateStatus('idle');\r\n throw err;\r\n }\r\n }\r\n\r\n /** Start recording audio and enable correction cycles. */\r\n async start(): Promise<void> {\r\n if (this.state.status !== 'ready') {\r\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\r\n }\r\n\r\n try {\r\n // Start Speech API BEFORE getUserMedia and wait for it to claim\r\n // the mic (onaudiostart or 300ms fallback). Without this wait,\r\n // getUserMedia opens a competing audio capture and Chrome's\r\n // SpeechRecognition receives no audio (silent failure).\r\n this.emitDebug(\r\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\"`,\r\n );\r\n if (this.config.streaming.enabled) {\r\n await this.speechStreaming.start(this.config.language);\r\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\r\n }\r\n this.capture = await startCapture();\r\n this.updateStatus('recording');\r\n this.correctionOrchestrator.start();\r\n } catch (err) {\r\n this.emitError(\r\n 'MIC_DENIED',\r\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\r\n );\r\n }\r\n }\r\n\r\n /** Stop recording, run final transcription, return text. */\r\n async stop(): Promise<string> {\r\n if (!this.capture) return '';\r\n\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.stop();\r\n this.workerManager.cancel();\r\n\r\n this.updateStatus('processing');\r\n\r\n try {\r\n const audio = await stopCapture(this.capture);\r\n this.capture = null;\r\n\r\n if (audio.length === 0) {\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n /** Destroy the engine: terminate worker, release all resources. */\r\n destroy(): void {\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.destroy();\r\n\r\n if (this.capture) {\r\n for (const track of this.capture.stream.getTracks()) {\r\n track.stop();\r\n }\r\n this.capture.audioCtx.close().catch(() => {});\r\n this.capture = null;\r\n }\r\n\r\n this.workerManager.destroy();\r\n this.updateStatus('idle');\r\n this.removeAllListeners();\r\n }\r\n\r\n /** Get current engine state. */\r\n getState(): Readonly<STTState> {\r\n return { ...this.state };\r\n }\r\n\r\n /** Notify the correction orchestrator of a speech pause. */\r\n notifyPause(): void {\r\n this.correctionOrchestrator.onPauseDetected();\r\n }\r\n\r\n private async performCorrection(): Promise<void> {\r\n if (!this.capture || !this.state.isModelLoaded) return;\r\n\r\n this.workerManager.cancel();\r\n\r\n try {\r\n const samples = snapshotAudio(this.capture);\r\n const nativeSr = this.capture.audioCtx.sampleRate;\r\n const audio = await resampleAudio(samples, nativeSr);\r\n\r\n if (audio.length === 0) return;\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n if (text.trim() && this.capture) {\r\n this.emit('correction', text);\r\n }\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Correction transcription failed.',\r\n );\r\n // Recording continues — error is non-fatal\r\n }\r\n }\r\n\r\n private setupStreamingCallbacks(): void {\r\n this.speechStreaming.setOnDebug((message) => {\r\n this.emit('debug', message);\r\n });\r\n\r\n this.speechStreaming.setOnTranscript((text) => {\r\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\r\n this.emit('transcript', text);\r\n });\r\n\r\n this.speechStreaming.setOnPause(() => {\r\n this.emitDebug('[STT] pause callback — triggering correction');\r\n this.correctionOrchestrator.onPauseDetected();\r\n });\r\n\r\n this.speechStreaming.setOnError((message) => {\r\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\r\n this.emitError('STREAMING_ERROR', message);\r\n });\r\n }\r\n\r\n private setupWorkerListeners(): void {\r\n this.workerManager.on('progress', (percent) => {\r\n this.state.loadProgress = percent;\r\n this.emit('status', { ...this.state });\r\n });\r\n\r\n this.workerManager.on('error', (message) => {\r\n this.emitError('WORKER_ERROR', message);\r\n });\r\n }\r\n\r\n private updateStatus(status: STTStatus): void {\r\n this.state.status = status;\r\n this.state.error = null;\r\n this.emit('status', { ...this.state });\r\n }\r\n\r\n private emitError(code: string, message: string): void {\r\n this.state.error = message;\r\n this.emit('error', { code, message });\r\n }\r\n\r\n private emitDebug(message: string): void {\r\n console.warn(message);\r\n this.emit('debug', message);\r\n }\r\n}\r\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\r\nexport type {\r\n STTModelSize,\r\n STTBackend,\r\n STTStatus,\r\n STTCorrectionProvider,\r\n STTStreamingProvider,\r\n STTCorrectionConfig,\r\n STTStreamingConfig,\r\n STTChunkingConfig,\r\n STTConfig,\r\n ResolvedSTTConfig,\r\n STTState,\r\n STTError,\r\n STTEvents,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\n\r\n// Public values\r\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\r\n\r\n// Event emitter\r\nexport { TypedEventEmitter } from './event-emitter.js';\r\n\r\n// Audio capture\r\nexport {\r\n startCapture,\r\n pauseCapture,\r\n resumeCapture,\r\n snapshotAudio,\r\n resampleAudio,\r\n stopCapture,\r\n} from './audio-capture.js';\r\n\r\n// Worker manager\r\nexport { WorkerManager } from './worker-manager.js';\r\nexport type { WorkerManagerEvents } from './worker-manager.js';\r\n\r\n// Correction orchestrator\r\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\n\r\n// Speech streaming\r\nexport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n// STT Engine (main public API)\r\nexport { STTEngine } from './stt-engine.js';\r\n","/** Supported Whisper model sizes. */\r\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\r\n\r\n/** Supported compute backends. */\r\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\r\n\r\n/** Engine lifecycle states. */\r\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\r\n\r\n/** Supported correction engine providers. */\r\nexport type STTCorrectionProvider = 'whisper';\r\n\r\n/** Supported real-time streaming providers. */\r\nexport type STTStreamingProvider = 'web-speech-api';\r\n\r\n/** Correction engine configuration. */\r\nexport interface STTCorrectionConfig {\r\n /** Enable mid-recording correction. Default: true */\r\n enabled?: boolean;\r\n /** Correction engine provider. Default: 'whisper' */\r\n provider?: STTCorrectionProvider;\r\n /** Silence duration (ms) before triggering correction. Default: 1000 */\r\n pauseThreshold?: number;\r\n /** Maximum interval (ms) between forced corrections. Default: 3000 */\r\n forcedInterval?: number;\r\n}\r\n\r\n/** Real-time streaming preview configuration. */\r\nexport interface STTStreamingConfig {\r\n /** Enable real-time streaming transcript. Default: true */\r\n enabled?: boolean;\r\n /** Streaming provider. Default: 'web-speech-api' */\r\n provider?: STTStreamingProvider;\r\n}\r\n\r\n/** Audio chunking configuration for long-form audio. */\r\nexport interface STTChunkingConfig {\r\n /** Chunk length in seconds for Whisper processing. Default: 30 */\r\n chunkLengthS?: number;\r\n /** Stride length in seconds for overlapping chunks. Default: 5 */\r\n strideLengthS?: number;\r\n}\r\n\r\n/** Full engine configuration. All fields optional — sensible defaults applied. */\r\nexport interface STTConfig {\r\n /** Whisper model size. Default: 'tiny' */\r\n model?: STTModelSize;\r\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\r\n backend?: STTBackend;\r\n /** Transcription language. Default: 'en' */\r\n language?: string;\r\n /** Model quantization dtype. Default: 'q4' */\r\n dtype?: string;\r\n /** Mid-recording correction settings. */\r\n correction?: STTCorrectionConfig;\r\n /** Audio chunking settings for long-form audio. */\r\n chunking?: STTChunkingConfig;\r\n /** Web Speech API streaming preview settings. */\r\n streaming?: STTStreamingConfig;\r\n}\r\n\r\n/** Resolved configuration with all defaults applied. */\r\nexport interface ResolvedSTTConfig {\r\n model: STTModelSize;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n correction: Required<STTCorrectionConfig>;\r\n chunking: Required<STTChunkingConfig>;\r\n streaming: Required<STTStreamingConfig>;\r\n}\r\n\r\n/** Engine state exposed to consumers via status events. */\r\nexport interface STTState {\r\n status: STTStatus;\r\n isModelLoaded: boolean;\r\n /** Model download progress (0–100). */\r\n loadProgress: number;\r\n /** Active compute backend, or null if not yet determined. */\r\n backend: 'webgpu' | 'wasm' | null;\r\n error: string | null;\r\n}\r\n\r\n/** Structured error emitted via the 'error' event. */\r\nexport interface STTError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\n/** Event map for the typed event emitter. */\r\nexport type STTEvents = {\r\n /** Streaming interim text during recording. */\r\n transcript: (text: string) => void;\r\n /** Whisper-corrected text replacing interim text. */\r\n correction: (text: string) => void;\r\n /** Actionable error (mic denied, model fail, transcription fail). */\r\n error: (error: STTError) => void;\r\n /** Engine state change. */\r\n status: (state: STTState) => void;\r\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\r\n debug: (message: string) => void;\r\n};\r\n\r\n/** Handle returned by audio capture — used internally. */\r\nexport interface AudioCaptureHandle {\r\n audioCtx: AudioContext;\r\n stream: MediaStream;\r\n samples: Float32Array[];\r\n /** Retain reference to prevent GC from stopping audio processing. */\r\n _processor: ScriptProcessorNode;\r\n /** Source node for disconnect/reconnect on pause/resume. */\r\n _source: MediaStreamAudioSourceNode;\r\n /** Gain node (silent) to prevent mic playback. */\r\n _silencer: GainNode;\r\n}\r\n\r\n/** Message sent from main thread to Whisper worker. */\r\nexport interface WorkerMessage {\r\n type: 'load' | 'transcribe' | 'cancel';\r\n audio?: Float32Array;\r\n config?: {\r\n model: string;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n chunkLengthS: number;\r\n strideLengthS: number;\r\n };\r\n}\r\n\r\n/** Response sent from Whisper worker to main thread. */\r\nexport interface WorkerResponse {\r\n type: 'progress' | 'ready' | 'result' | 'error';\r\n data?: unknown;\r\n}\r\n\r\n/** Default configuration values. */\r\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\r\n model: 'tiny',\r\n backend: 'auto',\r\n language: 'en',\r\n dtype: 'q4',\r\n correction: {\r\n enabled: true,\r\n provider: 'whisper',\r\n pauseThreshold: 1_000,\r\n forcedInterval: 3_000,\r\n },\r\n chunking: {\r\n chunkLengthS: 30,\r\n strideLengthS: 5,\r\n },\r\n streaming: {\r\n enabled: true,\r\n provider: 'web-speech-api',\r\n },\r\n};\r\n\r\n/** Merge user config with defaults to produce resolved config. */\r\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\r\n return {\r\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\r\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\r\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\r\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\r\n correction: {\r\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\r\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\r\n pauseThreshold:\r\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\r\n forcedInterval:\r\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\r\n },\r\n chunking: {\r\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\r\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\r\n },\r\n streaming: {\r\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\r\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\r\n },\r\n };\r\n}\r\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';\r\n\r\nconst TARGET_SAMPLE_RATE = 16_000;\r\n\r\n/**\r\n * Start capturing raw PCM audio from the microphone.\r\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\r\n */\r\nexport async function startCapture(): Promise<AudioCaptureHandle> {\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: { channelCount: 1 },\r\n });\r\n const audioCtx = new AudioContext();\r\n\r\n // Chrome may suspend AudioContext — must resume within user gesture\r\n if (audioCtx.state === 'suspended') {\r\n await audioCtx.resume();\r\n }\r\n\r\n const source = audioCtx.createMediaStreamSource(stream);\r\n const samples: Float32Array[] = [];\r\n\r\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\r\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\r\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\r\n };\r\n\r\n // Connect through a silent gain node so mic audio doesn't play back\r\n const silencer = audioCtx.createGain();\r\n silencer.gain.value = 0;\r\n source.connect(processor);\r\n processor.connect(silencer);\r\n silencer.connect(audioCtx.destination);\r\n\r\n return { audioCtx, stream, samples, _processor: processor, _source: source, _silencer: silencer };\r\n}\r\n\r\n/**\r\n * Pause capture without releasing mic or AudioContext.\r\n * Disconnects the audio source so no new samples are collected.\r\n * Returns resampled audio from the recording period.\r\n * Call resumeCapture() to start collecting again.\r\n */\r\nexport async function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n capture._source.disconnect();\r\n const currentSamples = [...capture.samples];\r\n capture.samples.length = 0;\r\n return resampleAudio(currentSamples, capture.audioCtx.sampleRate);\r\n}\r\n\r\n/**\r\n * Resume a paused capture. Reconnects the audio source to the processor.\r\n * AudioContext is resumed if suspended.\r\n */\r\nexport async function resumeCapture(capture: AudioCaptureHandle): Promise<void> {\r\n if (capture.audioCtx.state === 'suspended') {\r\n await capture.audioCtx.resume();\r\n }\r\n capture._source.connect(capture._processor);\r\n}\r\n\r\n/**\r\n * Copy current audio buffer without stopping capture.\r\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\r\n */\r\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\r\n return [...capture.samples];\r\n}\r\n\r\n/**\r\n * Concatenate sample chunks and resample to 16kHz for Whisper.\r\n */\r\nexport async function resampleAudio(\r\n samples: Float32Array[],\r\n nativeSr: number,\r\n): Promise<Float32Array> {\r\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\r\n if (totalLength === 0) return new Float32Array(0);\r\n\r\n const fullAudio = new Float32Array(totalLength);\r\n let offset = 0;\r\n for (const s of samples) {\r\n fullAudio.set(s, offset);\r\n offset += s.length;\r\n }\r\n\r\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\r\n\r\n const duration = fullAudio.length / nativeSr;\r\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\r\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\r\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\r\n buffer.getChannelData(0).set(fullAudio);\r\n const src = offline.createBufferSource();\r\n src.buffer = buffer;\r\n src.connect(offline.destination);\r\n src.start(0);\r\n const resampled = await offline.startRendering();\r\n return resampled.getChannelData(0);\r\n}\r\n\r\n/**\r\n * Stop capturing and return resampled audio at 16kHz.\r\n */\r\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n const { audioCtx, stream, samples, _processor } = capture;\r\n\r\n // Disconnect processor to stop capturing\r\n try {\r\n _processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n\r\n // Stop microphone tracks\r\n for (const track of stream.getTracks()) {\r\n track.stop();\r\n }\r\n\r\n const nativeSr = audioCtx.sampleRate;\r\n await audioCtx.close();\r\n\r\n return resampleAudio(samples, nativeSr);\r\n}\r\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 currentTranscribePromise: Promise<string> | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** True while a transcription job is running in the worker. */\n get isTranscribing(): boolean {\n return this.transcribeResolve !== null;\n }\n\n /** Await the current in-flight transcription without starting a new one. */\n awaitCurrentTranscription(): Promise<string> {\n return this.currentTranscribePromise ?? Promise.resolve('');\n }\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 this.currentTranscribePromise = new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n return this.currentTranscribePromise;\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 ──────────────────────────────── */\r\n\r\ninterface SpeechRecognitionEvent {\r\n results: SpeechRecognitionResultList;\r\n resultIndex: number;\r\n}\r\n\r\ninterface SpeechRecognitionErrorEvent {\r\n error: string;\r\n}\r\n\r\ninterface SpeechRecognitionInstance {\r\n continuous: boolean;\r\n interimResults: boolean;\r\n lang: string;\r\n onaudiostart: (() => void) | null;\r\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\r\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\r\n onend: (() => void) | null;\r\n start: () => void;\r\n stop: () => void;\r\n abort: () => void;\r\n}\r\n\r\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\r\n\r\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\r\n if (typeof globalThis === 'undefined') return null;\r\n const w = globalThis as unknown as Record<string, unknown>;\r\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\r\n}\r\n\r\n/* ─── Language mapping ──────────────────────────────────── */\r\n\r\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\r\nconst WHISPER_TO_BCP47: Record<string, string> = {\r\n en: 'en-US',\r\n english: 'en-US',\r\n zh: 'zh-CN',\r\n chinese: 'zh-CN',\r\n de: 'de-DE',\r\n german: 'de-DE',\r\n es: 'es-ES',\r\n spanish: 'es-ES',\r\n ru: 'ru-RU',\r\n russian: 'ru-RU',\r\n ko: 'ko-KR',\r\n korean: 'ko-KR',\r\n fr: 'fr-FR',\r\n french: 'fr-FR',\r\n ja: 'ja-JP',\r\n japanese: 'ja-JP',\r\n pt: 'pt-BR',\r\n portuguese: 'pt-BR',\r\n tr: 'tr-TR',\r\n turkish: 'tr-TR',\r\n pl: 'pl-PL',\r\n polish: 'pl-PL',\r\n nl: 'nl-NL',\r\n dutch: 'nl-NL',\r\n ar: 'ar-SA',\r\n arabic: 'ar-SA',\r\n sv: 'sv-SE',\r\n swedish: 'sv-SE',\r\n it: 'it-IT',\r\n italian: 'it-IT',\r\n id: 'id-ID',\r\n indonesian: 'id-ID',\r\n hi: 'hi-IN',\r\n hindi: 'hi-IN',\r\n fi: 'fi-FI',\r\n finnish: 'fi-FI',\r\n vi: 'vi-VN',\r\n vietnamese: 'vi-VN',\r\n he: 'he-IL',\r\n hebrew: 'he-IL',\r\n uk: 'uk-UA',\r\n ukrainian: 'uk-UA',\r\n el: 'el-GR',\r\n greek: 'el-GR',\r\n ms: 'ms-MY',\r\n malay: 'ms-MY',\r\n cs: 'cs-CZ',\r\n czech: 'cs-CZ',\r\n ro: 'ro-RO',\r\n romanian: 'ro-RO',\r\n da: 'da-DK',\r\n danish: 'da-DK',\r\n hu: 'hu-HU',\r\n hungarian: 'hu-HU',\r\n no: 'nb-NO',\r\n norwegian: 'nb-NO',\r\n th: 'th-TH',\r\n thai: 'th-TH',\r\n};\r\n\r\n/**\r\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\r\n * Already-BCP-47 codes (containing '-') pass through unchanged.\r\n */\r\nfunction toBCP47(language: string): string {\r\n if (language.includes('-')) return language;\r\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\r\n}\r\n\r\n/* ─── SpeechStreamingManager ────────────────────────────── */\r\n\r\n/**\r\n * Manages Web Speech API for real-time streaming transcript preview.\r\n * Provides word-by-word interim text while Whisper handles corrections.\r\n */\r\nconst NO_RESULT_TIMEOUT_MS = 5_000;\r\n\r\nexport class SpeechStreamingManager {\r\n private recognition: SpeechRecognitionInstance | null = null;\r\n private accumulated = '';\r\n private active = false;\r\n private receivedResult = false;\r\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\r\n private onTranscript: ((text: string) => void) | null = null;\r\n private onPause: (() => void) | null = null;\r\n private onError: ((message: string) => void) | null = null;\r\n private onDebug: ((message: string) => void) | null = null;\r\n\r\n /** Check if the Web Speech API is available in this environment. */\r\n static isSupported(): boolean {\r\n return getSpeechRecognition() !== null;\r\n }\r\n\r\n /** Set callback for streaming transcript updates (interim + final text). */\r\n setOnTranscript(fn: (text: string) => void): void {\r\n this.onTranscript = fn;\r\n }\r\n\r\n /** Set callback for speech pause detection (Speech API onend). */\r\n setOnPause(fn: () => void): void {\r\n this.onPause = fn;\r\n }\r\n\r\n /** Set callback for errors. */\r\n setOnError(fn: (message: string) => void): void {\r\n this.onError = fn;\r\n }\r\n\r\n /** Set callback for diagnostic debug messages. */\r\n setOnDebug(fn: (message: string) => void): void {\r\n this.onDebug = fn;\r\n }\r\n\r\n private log(message: string): void {\r\n this.onDebug?.(message);\r\n console.warn(message);\r\n }\r\n\r\n /**\r\n * Start streaming recognition. Returns a Promise that resolves once\r\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\r\n * a 300ms fallback — whichever comes first. The engine should await\r\n * this before calling getUserMedia to avoid dual-mic conflicts.\r\n *\r\n * When skipMicWait is true (warm restart — mic already active), returns\r\n * immediately after calling recognition.start() without waiting for\r\n * onaudiostart.\r\n */\r\n start(language: string, skipMicWait = false): Promise<void> {\r\n const SR = getSpeechRecognition();\r\n if (!SR) {\r\n this.log('[SSM] SpeechRecognition not available in this environment');\r\n return Promise.resolve();\r\n }\r\n\r\n const bcp47 = toBCP47(language);\r\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\r\n\r\n this.accumulated = '';\r\n this.active = true;\r\n this.receivedResult = false;\r\n\r\n const recognition = new SR();\r\n recognition.continuous = true;\r\n recognition.interimResults = true;\r\n recognition.lang = bcp47;\r\n\r\n let lastFinalIndex = -1;\r\n let lastFinalText = '';\r\n\r\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\r\n // This ensures getUserMedia doesn't compete for the mic.\r\n let micReady = false;\r\n const micClaimPromise = new Promise<void>((resolve) => {\r\n recognition.onaudiostart = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\r\n if (!micReady) {\r\n micReady = true;\r\n resolve();\r\n }\r\n };\r\n // Fallback: resolve after 300ms even if onaudiostart never fires\r\n setTimeout(() => {\r\n if (!micReady) {\r\n micReady = true;\r\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\r\n resolve();\r\n }\r\n }, 300);\r\n });\r\n\r\n // Detect silent failure: if no onresult fires within timeout, emit error\r\n this.clearNoResultTimer();\r\n this.noResultTimer = setTimeout(() => {\r\n if (this.active && !this.receivedResult) {\r\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\r\n this.onError?.(\r\n 'Speech streaming started but received no results. ' +\r\n 'Mic may be blocked by another audio capture.',\r\n );\r\n }\r\n }, NO_RESULT_TIMEOUT_MS);\r\n\r\n recognition.onresult = (e: SpeechRecognitionEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.receivedResult = true;\r\n this.clearNoResultTimer();\r\n\r\n let final_ = '';\r\n let interim = '';\r\n for (let i = e.resultIndex; i < e.results.length; i++) {\r\n const t = e.results[i][0].transcript;\r\n if (e.results[i].isFinal) {\r\n if (i > lastFinalIndex) {\r\n final_ += t;\r\n lastFinalIndex = i;\r\n }\r\n } else {\r\n interim += t;\r\n }\r\n }\r\n\r\n this.log(\r\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\r\n );\r\n\r\n if (final_ && final_.trim() !== lastFinalText) {\r\n lastFinalText = final_.trim();\r\n this.accumulated = this.accumulated\r\n ? this.accumulated + ' ' + final_.trim()\r\n : final_.trim();\r\n this.onTranscript?.(this.accumulated);\r\n } else if (interim) {\r\n const trimmed = interim.trimStart();\r\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\r\n this.onTranscript?.(full);\r\n }\r\n };\r\n\r\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onerror — ${e.error}`);\r\n this.onError?.(e.error);\r\n };\r\n\r\n recognition.onend = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\r\n\r\n if (this.active) {\r\n // Speech API paused — trigger correction\r\n this.onPause?.();\r\n // Restart for continued streaming\r\n try {\r\n recognition.start();\r\n this.log('[SSM] restarted after pause');\r\n } catch (err) {\r\n this.log(`[SSM] restart THREW: ${err}`);\r\n this.recognition = null;\r\n this.onError?.('Speech recognition failed to restart after pause.');\r\n }\r\n } else {\r\n this.recognition = null;\r\n }\r\n };\r\n\r\n this.recognition = recognition;\r\n try {\r\n recognition.start();\r\n this.log('[SSM] recognition.start() succeeded');\r\n } catch (err) {\r\n this.log(`[SSM] recognition.start() THREW: ${err}`);\r\n this.recognition = null;\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n this.onError?.(\r\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\r\n }\r\n\r\n if (skipMicWait) {\r\n this.log('[SSM] skipMicWait — warm restart, returning immediately');\r\n return Promise.resolve();\r\n }\r\n\r\n return micClaimPromise;\r\n }\r\n\r\n private clearNoResultTimer(): void {\r\n if (this.noResultTimer) {\r\n clearTimeout(this.noResultTimer);\r\n this.noResultTimer = null;\r\n }\r\n }\r\n\r\n /** Stop streaming recognition and return accumulated text. */\r\n stop(): string {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.stop();\r\n }\r\n const result = this.accumulated;\r\n this.accumulated = '';\r\n return result;\r\n }\r\n\r\n /** Abort immediately without returning text. */\r\n destroy(): void {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.abort();\r\n }\r\n this.accumulated = '';\r\n this.onTranscript = null;\r\n this.onPause = null;\r\n this.onError = null;\r\n this.onDebug = null;\r\n }\r\n}\r\n","import type {\r\n STTConfig,\r\n STTState,\r\n STTEvents,\r\n STTStatus,\r\n ResolvedSTTConfig,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\nimport { resolveConfig } from './types.js';\r\nimport { TypedEventEmitter } from './event-emitter.js';\r\nimport { startCapture, pauseCapture, resumeCapture, snapshotAudio, resampleAudio } from './audio-capture.js';\r\nimport { WorkerManager } from './worker-manager.js';\r\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\nimport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n/**\r\n * Main STT engine — the public API for speech-to-text with Whisper correction.\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const engine = new STTEngine({ model: 'tiny' });\r\n * engine.on('transcript', (text) => console.log(text));\r\n * engine.on('correction', (text) => console.log('corrected:', text));\r\n * await engine.init();\r\n * await engine.start();\r\n * const finalText = await engine.stop();\r\n * ```\r\n */\r\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\r\n private config: ResolvedSTTConfig;\r\n private workerManager: WorkerManager;\r\n private correctionOrchestrator: CorrectionOrchestrator;\r\n private speechStreaming: SpeechStreamingManager;\r\n private capture: AudioCaptureHandle | null = null;\r\n private state: STTState;\r\n private workerUrl?: URL;\r\n /** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */\r\n private _stopping = false;\r\n\r\n /**\r\n * Create a new STT engine instance.\r\n * @param config - Optional configuration overrides (model, backend, language, etc.).\r\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\r\n */\r\n constructor(config?: STTConfig, workerUrl?: URL) {\r\n super();\r\n this.config = resolveConfig(config);\r\n this.workerManager = new WorkerManager();\r\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\r\n this.speechStreaming = new SpeechStreamingManager();\r\n this.workerUrl = workerUrl;\r\n\r\n this.state = {\r\n status: 'idle',\r\n isModelLoaded: false,\r\n loadProgress: 0,\r\n backend: null,\r\n error: null,\r\n };\r\n\r\n this.correctionOrchestrator.setCorrectionFn(() => {\r\n this.performCorrection();\r\n });\r\n\r\n this.setupWorkerListeners();\r\n this.setupStreamingCallbacks();\r\n }\r\n\r\n /** Initialize the engine: spawn worker and load model. */\r\n async init(): Promise<void> {\r\n this.updateStatus('loading');\r\n this.workerManager.spawn(this.workerUrl);\r\n\r\n try {\r\n await this.workerManager.loadModel(this.config);\r\n this.state.isModelLoaded = true;\r\n this.updateStatus('ready');\r\n } catch (err) {\r\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\r\n this.updateStatus('idle');\r\n throw err;\r\n }\r\n }\r\n\r\n /** Start recording audio and enable correction cycles. */\r\n async start(): Promise<void> {\r\n if (this.state.status !== 'ready') {\r\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\r\n }\r\n\r\n try {\r\n // Check if mic is already warm from a previous recording session.\r\n // When warm, skip getUserMedia and the 300ms SR mic-claim wait.\r\n const warmCapture =\r\n this.capture &&\r\n this.capture.stream.getTracks().every((t) => t.readyState === 'live');\r\n\r\n this.emitDebug(\r\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\", warm: ${!!warmCapture}`,\r\n );\r\n\r\n if (this.config.streaming.enabled) {\r\n // On warm restart, skip the mic-claim wait — no getUserMedia race.\r\n await this.speechStreaming.start(this.config.language, !!warmCapture);\r\n if (!warmCapture) {\r\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\r\n }\r\n }\r\n\r\n if (warmCapture) {\r\n await resumeCapture(this.capture!);\r\n this.emitDebug('[STT] warm mic resumed — skipped getUserMedia');\r\n } else {\r\n // First start or stale capture: full mic init with getUserMedia.\r\n this.capture = await startCapture();\r\n }\r\n\r\n this.updateStatus('recording');\r\n this.correctionOrchestrator.start();\r\n } catch (err) {\r\n this.emitError(\r\n 'MIC_DENIED',\r\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\r\n );\r\n }\r\n }\r\n\r\n /** Stop recording, run final transcription, return text.\r\n * Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */\r\n async stop(): Promise<string> {\r\n if (!this.capture) return '';\r\n\r\n // Prevent any in-flight performCorrection from emitting — stop() will own the final emit.\r\n this._stopping = true;\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.stop();\r\n\r\n this.updateStatus('processing');\r\n\r\n // If a correction job is already running, reuse it: Whisper can't be interrupted mid-inference\r\n // anyway, so cancelling just wastes the work and causes a second full run.\r\n if (this.workerManager.isTranscribing) {\r\n try {\r\n // Resample audio and await the in-flight result in parallel.\r\n const [audio, inFlightText] = await Promise.all([\r\n pauseCapture(this.capture),\r\n this.workerManager.awaitCurrentTranscription(),\r\n ]);\r\n this._stopping = false;\r\n\r\n const text = inFlightText.trim();\r\n if (text) {\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n }\r\n\r\n // In-flight returned empty (cancelled or error) — fall through to fresh transcription.\r\n if (audio.length > 0) {\r\n const freshText = await this.workerManager.transcribe(audio);\r\n this.emit('correction', freshText);\r\n this.updateStatus('ready');\r\n return freshText;\r\n }\r\n\r\n this.updateStatus('ready');\r\n return '';\r\n } catch (err) {\r\n this._stopping = false;\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n // No job in-flight — cancel is a no-op, run fresh transcription.\r\n this.workerManager.cancel();\r\n this._stopping = false;\r\n\r\n try {\r\n // Soft pause — keeps stream and AudioContext alive for fast restart.\r\n const audio = await pauseCapture(this.capture);\r\n\r\n if (audio.length === 0) {\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n /** Destroy the engine: terminate worker, release all resources. */\r\n destroy(): void {\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.destroy();\r\n\r\n if (this.capture) {\r\n try {\r\n this.capture._processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n for (const track of this.capture.stream.getTracks()) {\r\n track.stop();\r\n }\r\n this.capture.audioCtx.close().catch(() => {});\r\n this.capture = null;\r\n }\r\n\r\n this.workerManager.destroy();\r\n this.updateStatus('idle');\r\n this.removeAllListeners();\r\n }\r\n\r\n /** Get current engine state. */\r\n getState(): Readonly<STTState> {\r\n return { ...this.state };\r\n }\r\n\r\n /** Notify the correction orchestrator of a speech pause. */\r\n notifyPause(): void {\r\n this.correctionOrchestrator.onPauseDetected();\r\n }\r\n\r\n private async performCorrection(): Promise<void> {\r\n if (!this.capture || !this.state.isModelLoaded) return;\r\n\r\n this.workerManager.cancel();\r\n\r\n try {\r\n const samples = snapshotAudio(this.capture);\r\n const nativeSr = this.capture.audioCtx.sampleRate;\r\n const audio = await resampleAudio(samples, nativeSr);\r\n\r\n if (audio.length === 0) return;\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n if (text.trim() && this.capture && !this._stopping) {\r\n this.emit('correction', text);\r\n }\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Correction transcription failed.',\r\n );\r\n // Recording continues — error is non-fatal\r\n }\r\n }\r\n\r\n private setupStreamingCallbacks(): void {\r\n this.speechStreaming.setOnDebug((message) => {\r\n this.emit('debug', message);\r\n });\r\n\r\n this.speechStreaming.setOnTranscript((text) => {\r\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\r\n this.emit('transcript', text);\r\n });\r\n\r\n this.speechStreaming.setOnPause(() => {\r\n this.emitDebug('[STT] pause callback — triggering correction');\r\n this.correctionOrchestrator.onPauseDetected();\r\n });\r\n\r\n this.speechStreaming.setOnError((message) => {\r\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\r\n this.emitError('STREAMING_ERROR', message);\r\n });\r\n }\r\n\r\n private setupWorkerListeners(): void {\r\n this.workerManager.on('progress', (percent) => {\r\n this.state.loadProgress = percent;\r\n this.emit('status', { ...this.state });\r\n });\r\n\r\n this.workerManager.on('error', (message) => {\r\n this.emitError('WORKER_ERROR', message);\r\n });\r\n }\r\n\r\n private updateStatus(status: STTStatus): void {\r\n this.state.status = status;\r\n this.state.error = null;\r\n this.emit('status', { ...this.state });\r\n }\r\n\r\n private emitError(code: string, message: string): void {\r\n this.state.error = message;\r\n this.emit('error', { code, message });\r\n }\r\n\r\n private emitDebug(message: string): void {\r\n console.warn(message);\r\n this.emit('debug', message);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -14,9 +14,9 @@ interface STTCorrectionConfig {
|
|
|
14
14
|
enabled?: boolean;
|
|
15
15
|
/** Correction engine provider. Default: 'whisper' */
|
|
16
16
|
provider?: STTCorrectionProvider;
|
|
17
|
-
/** Silence duration (ms) before triggering correction. Default:
|
|
17
|
+
/** Silence duration (ms) before triggering correction. Default: 1000 */
|
|
18
18
|
pauseThreshold?: number;
|
|
19
|
-
/** Maximum interval (ms) between forced corrections. Default:
|
|
19
|
+
/** Maximum interval (ms) between forced corrections. Default: 3000 */
|
|
20
20
|
forcedInterval?: number;
|
|
21
21
|
}
|
|
22
22
|
/** Real-time streaming preview configuration. */
|
|
@@ -95,6 +95,10 @@ interface AudioCaptureHandle {
|
|
|
95
95
|
samples: Float32Array[];
|
|
96
96
|
/** Retain reference to prevent GC from stopping audio processing. */
|
|
97
97
|
_processor: ScriptProcessorNode;
|
|
98
|
+
/** Source node for disconnect/reconnect on pause/resume. */
|
|
99
|
+
_source: MediaStreamAudioSourceNode;
|
|
100
|
+
/** Gain node (silent) to prevent mic playback. */
|
|
101
|
+
_silencer: GainNode;
|
|
98
102
|
}
|
|
99
103
|
/** Default configuration values. */
|
|
100
104
|
declare const DEFAULT_STT_CONFIG: ResolvedSTTConfig;
|
|
@@ -124,6 +128,18 @@ declare class TypedEventEmitter<T extends Record<string, (...args: any[]) => voi
|
|
|
124
128
|
* Uses ScriptProcessorNode to collect Float32Array samples directly.
|
|
125
129
|
*/
|
|
126
130
|
declare function startCapture(): Promise<AudioCaptureHandle>;
|
|
131
|
+
/**
|
|
132
|
+
* Pause capture without releasing mic or AudioContext.
|
|
133
|
+
* Disconnects the audio source so no new samples are collected.
|
|
134
|
+
* Returns resampled audio from the recording period.
|
|
135
|
+
* Call resumeCapture() to start collecting again.
|
|
136
|
+
*/
|
|
137
|
+
declare function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array>;
|
|
138
|
+
/**
|
|
139
|
+
* Resume a paused capture. Reconnects the audio source to the processor.
|
|
140
|
+
* AudioContext is resumed if suspended.
|
|
141
|
+
*/
|
|
142
|
+
declare function resumeCapture(capture: AudioCaptureHandle): Promise<void>;
|
|
127
143
|
/**
|
|
128
144
|
* Copy current audio buffer without stopping capture.
|
|
129
145
|
* Returns a shallow copy of the samples array (each chunk is shared, not cloned).
|
|
@@ -152,8 +168,13 @@ type WorkerManagerEvents = {
|
|
|
152
168
|
declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
|
|
153
169
|
private worker;
|
|
154
170
|
private transcribeResolve;
|
|
171
|
+
private currentTranscribePromise;
|
|
155
172
|
private modelReadyResolve;
|
|
156
173
|
private modelReadyReject;
|
|
174
|
+
/** True while a transcription job is running in the worker. */
|
|
175
|
+
get isTranscribing(): boolean;
|
|
176
|
+
/** Await the current in-flight transcription without starting a new one. */
|
|
177
|
+
awaitCurrentTranscription(): Promise<string>;
|
|
157
178
|
/** Spawn the Web Worker. Must be called before loadModel/transcribe. */
|
|
158
179
|
spawn(workerUrl?: URL): void;
|
|
159
180
|
/** Load the Whisper model in the worker. Resolves when ready. */
|
|
@@ -220,8 +241,12 @@ declare class SpeechStreamingManager {
|
|
|
220
241
|
* SpeechRecognition has claimed the microphone (onaudiostart) or after
|
|
221
242
|
* a 300ms fallback — whichever comes first. The engine should await
|
|
222
243
|
* this before calling getUserMedia to avoid dual-mic conflicts.
|
|
244
|
+
*
|
|
245
|
+
* When skipMicWait is true (warm restart — mic already active), returns
|
|
246
|
+
* immediately after calling recognition.start() without waiting for
|
|
247
|
+
* onaudiostart.
|
|
223
248
|
*/
|
|
224
|
-
start(language: string): Promise<void>;
|
|
249
|
+
start(language: string, skipMicWait?: boolean): Promise<void>;
|
|
225
250
|
private clearNoResultTimer;
|
|
226
251
|
/** Stop streaming recognition and return accumulated text. */
|
|
227
252
|
stop(): string;
|
|
@@ -250,6 +275,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
250
275
|
private capture;
|
|
251
276
|
private state;
|
|
252
277
|
private workerUrl?;
|
|
278
|
+
/** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
|
|
279
|
+
private _stopping;
|
|
253
280
|
/**
|
|
254
281
|
* Create a new STT engine instance.
|
|
255
282
|
* @param config - Optional configuration overrides (model, backend, language, etc.).
|
|
@@ -260,7 +287,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
260
287
|
init(): Promise<void>;
|
|
261
288
|
/** Start recording audio and enable correction cycles. */
|
|
262
289
|
start(): Promise<void>;
|
|
263
|
-
/** Stop recording, run final transcription, return text.
|
|
290
|
+
/** Stop recording, run final transcription, return text.
|
|
291
|
+
* Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
|
|
264
292
|
stop(): Promise<string>;
|
|
265
293
|
/** Destroy the engine: terminate worker, release all resources. */
|
|
266
294
|
destroy(): void;
|
|
@@ -276,4 +304,4 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
276
304
|
private emitDebug;
|
|
277
305
|
}
|
|
278
306
|
|
|
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 };
|
|
307
|
+
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, pauseCapture, resampleAudio, resolveConfig, resumeCapture, snapshotAudio, startCapture, stopCapture };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ interface STTCorrectionConfig {
|
|
|
14
14
|
enabled?: boolean;
|
|
15
15
|
/** Correction engine provider. Default: 'whisper' */
|
|
16
16
|
provider?: STTCorrectionProvider;
|
|
17
|
-
/** Silence duration (ms) before triggering correction. Default:
|
|
17
|
+
/** Silence duration (ms) before triggering correction. Default: 1000 */
|
|
18
18
|
pauseThreshold?: number;
|
|
19
|
-
/** Maximum interval (ms) between forced corrections. Default:
|
|
19
|
+
/** Maximum interval (ms) between forced corrections. Default: 3000 */
|
|
20
20
|
forcedInterval?: number;
|
|
21
21
|
}
|
|
22
22
|
/** Real-time streaming preview configuration. */
|
|
@@ -95,6 +95,10 @@ interface AudioCaptureHandle {
|
|
|
95
95
|
samples: Float32Array[];
|
|
96
96
|
/** Retain reference to prevent GC from stopping audio processing. */
|
|
97
97
|
_processor: ScriptProcessorNode;
|
|
98
|
+
/** Source node for disconnect/reconnect on pause/resume. */
|
|
99
|
+
_source: MediaStreamAudioSourceNode;
|
|
100
|
+
/** Gain node (silent) to prevent mic playback. */
|
|
101
|
+
_silencer: GainNode;
|
|
98
102
|
}
|
|
99
103
|
/** Default configuration values. */
|
|
100
104
|
declare const DEFAULT_STT_CONFIG: ResolvedSTTConfig;
|
|
@@ -124,6 +128,18 @@ declare class TypedEventEmitter<T extends Record<string, (...args: any[]) => voi
|
|
|
124
128
|
* Uses ScriptProcessorNode to collect Float32Array samples directly.
|
|
125
129
|
*/
|
|
126
130
|
declare function startCapture(): Promise<AudioCaptureHandle>;
|
|
131
|
+
/**
|
|
132
|
+
* Pause capture without releasing mic or AudioContext.
|
|
133
|
+
* Disconnects the audio source so no new samples are collected.
|
|
134
|
+
* Returns resampled audio from the recording period.
|
|
135
|
+
* Call resumeCapture() to start collecting again.
|
|
136
|
+
*/
|
|
137
|
+
declare function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array>;
|
|
138
|
+
/**
|
|
139
|
+
* Resume a paused capture. Reconnects the audio source to the processor.
|
|
140
|
+
* AudioContext is resumed if suspended.
|
|
141
|
+
*/
|
|
142
|
+
declare function resumeCapture(capture: AudioCaptureHandle): Promise<void>;
|
|
127
143
|
/**
|
|
128
144
|
* Copy current audio buffer without stopping capture.
|
|
129
145
|
* Returns a shallow copy of the samples array (each chunk is shared, not cloned).
|
|
@@ -152,8 +168,13 @@ type WorkerManagerEvents = {
|
|
|
152
168
|
declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
|
|
153
169
|
private worker;
|
|
154
170
|
private transcribeResolve;
|
|
171
|
+
private currentTranscribePromise;
|
|
155
172
|
private modelReadyResolve;
|
|
156
173
|
private modelReadyReject;
|
|
174
|
+
/** True while a transcription job is running in the worker. */
|
|
175
|
+
get isTranscribing(): boolean;
|
|
176
|
+
/** Await the current in-flight transcription without starting a new one. */
|
|
177
|
+
awaitCurrentTranscription(): Promise<string>;
|
|
157
178
|
/** Spawn the Web Worker. Must be called before loadModel/transcribe. */
|
|
158
179
|
spawn(workerUrl?: URL): void;
|
|
159
180
|
/** Load the Whisper model in the worker. Resolves when ready. */
|
|
@@ -220,8 +241,12 @@ declare class SpeechStreamingManager {
|
|
|
220
241
|
* SpeechRecognition has claimed the microphone (onaudiostart) or after
|
|
221
242
|
* a 300ms fallback — whichever comes first. The engine should await
|
|
222
243
|
* this before calling getUserMedia to avoid dual-mic conflicts.
|
|
244
|
+
*
|
|
245
|
+
* When skipMicWait is true (warm restart — mic already active), returns
|
|
246
|
+
* immediately after calling recognition.start() without waiting for
|
|
247
|
+
* onaudiostart.
|
|
223
248
|
*/
|
|
224
|
-
start(language: string): Promise<void>;
|
|
249
|
+
start(language: string, skipMicWait?: boolean): Promise<void>;
|
|
225
250
|
private clearNoResultTimer;
|
|
226
251
|
/** Stop streaming recognition and return accumulated text. */
|
|
227
252
|
stop(): string;
|
|
@@ -250,6 +275,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
250
275
|
private capture;
|
|
251
276
|
private state;
|
|
252
277
|
private workerUrl?;
|
|
278
|
+
/** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
|
|
279
|
+
private _stopping;
|
|
253
280
|
/**
|
|
254
281
|
* Create a new STT engine instance.
|
|
255
282
|
* @param config - Optional configuration overrides (model, backend, language, etc.).
|
|
@@ -260,7 +287,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
260
287
|
init(): Promise<void>;
|
|
261
288
|
/** Start recording audio and enable correction cycles. */
|
|
262
289
|
start(): Promise<void>;
|
|
263
|
-
/** Stop recording, run final transcription, return text.
|
|
290
|
+
/** Stop recording, run final transcription, return text.
|
|
291
|
+
* Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
|
|
264
292
|
stop(): Promise<string>;
|
|
265
293
|
/** Destroy the engine: terminate worker, release all resources. */
|
|
266
294
|
destroy(): void;
|
|
@@ -276,4 +304,4 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
276
304
|
private emitDebug;
|
|
277
305
|
}
|
|
278
306
|
|
|
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 };
|
|
307
|
+
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, pauseCapture, resampleAudio, resolveConfig, resumeCapture, snapshotAudio, startCapture, stopCapture };
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ var DEFAULT_STT_CONFIG = {
|
|
|
7
7
|
correction: {
|
|
8
8
|
enabled: true,
|
|
9
9
|
provider: "whisper",
|
|
10
|
-
pauseThreshold:
|
|
11
|
-
forcedInterval:
|
|
10
|
+
pauseThreshold: 1e3,
|
|
11
|
+
forcedInterval: 3e3
|
|
12
12
|
},
|
|
13
13
|
chunking: {
|
|
14
14
|
chunkLengthS: 30,
|
|
@@ -97,7 +97,19 @@ async function startCapture() {
|
|
|
97
97
|
source.connect(processor);
|
|
98
98
|
processor.connect(silencer);
|
|
99
99
|
silencer.connect(audioCtx.destination);
|
|
100
|
-
return { audioCtx, stream, samples, _processor: processor };
|
|
100
|
+
return { audioCtx, stream, samples, _processor: processor, _source: source, _silencer: silencer };
|
|
101
|
+
}
|
|
102
|
+
async function pauseCapture(capture) {
|
|
103
|
+
capture._source.disconnect();
|
|
104
|
+
const currentSamples = [...capture.samples];
|
|
105
|
+
capture.samples.length = 0;
|
|
106
|
+
return resampleAudio(currentSamples, capture.audioCtx.sampleRate);
|
|
107
|
+
}
|
|
108
|
+
async function resumeCapture(capture) {
|
|
109
|
+
if (capture.audioCtx.state === "suspended") {
|
|
110
|
+
await capture.audioCtx.resume();
|
|
111
|
+
}
|
|
112
|
+
capture._source.connect(capture._processor);
|
|
101
113
|
}
|
|
102
114
|
function snapshotAudio(capture) {
|
|
103
115
|
return [...capture.samples];
|
|
@@ -142,8 +154,17 @@ async function stopCapture(capture) {
|
|
|
142
154
|
var WorkerManager = class extends TypedEventEmitter {
|
|
143
155
|
worker = null;
|
|
144
156
|
transcribeResolve = null;
|
|
157
|
+
currentTranscribePromise = null;
|
|
145
158
|
modelReadyResolve = null;
|
|
146
159
|
modelReadyReject = null;
|
|
160
|
+
/** True while a transcription job is running in the worker. */
|
|
161
|
+
get isTranscribing() {
|
|
162
|
+
return this.transcribeResolve !== null;
|
|
163
|
+
}
|
|
164
|
+
/** Await the current in-flight transcription without starting a new one. */
|
|
165
|
+
awaitCurrentTranscription() {
|
|
166
|
+
return this.currentTranscribePromise ?? Promise.resolve("");
|
|
167
|
+
}
|
|
147
168
|
/** Spawn the Web Worker. Must be called before loadModel/transcribe. */
|
|
148
169
|
spawn(workerUrl) {
|
|
149
170
|
if (this.worker) return;
|
|
@@ -179,10 +200,11 @@ var WorkerManager = class extends TypedEventEmitter {
|
|
|
179
200
|
async transcribe(audio) {
|
|
180
201
|
if (!this.worker) throw new Error("Worker not spawned");
|
|
181
202
|
if (audio.length === 0) return "";
|
|
182
|
-
|
|
203
|
+
this.currentTranscribePromise = new Promise((resolve) => {
|
|
183
204
|
this.transcribeResolve = resolve;
|
|
184
205
|
this.worker.postMessage({ type: "transcribe", audio }, [audio.buffer]);
|
|
185
206
|
});
|
|
207
|
+
return this.currentTranscribePromise;
|
|
186
208
|
}
|
|
187
209
|
/** Cancel any in-flight transcription. */
|
|
188
210
|
cancel() {
|
|
@@ -402,8 +424,12 @@ var SpeechStreamingManager = class {
|
|
|
402
424
|
* SpeechRecognition has claimed the microphone (onaudiostart) or after
|
|
403
425
|
* a 300ms fallback — whichever comes first. The engine should await
|
|
404
426
|
* this before calling getUserMedia to avoid dual-mic conflicts.
|
|
427
|
+
*
|
|
428
|
+
* When skipMicWait is true (warm restart — mic already active), returns
|
|
429
|
+
* immediately after calling recognition.start() without waiting for
|
|
430
|
+
* onaudiostart.
|
|
405
431
|
*/
|
|
406
|
-
start(language) {
|
|
432
|
+
start(language, skipMicWait = false) {
|
|
407
433
|
const SR = getSpeechRecognition();
|
|
408
434
|
if (!SR) {
|
|
409
435
|
this.log("[SSM] SpeechRecognition not available in this environment");
|
|
@@ -513,6 +539,10 @@ var SpeechStreamingManager = class {
|
|
|
513
539
|
);
|
|
514
540
|
return Promise.resolve();
|
|
515
541
|
}
|
|
542
|
+
if (skipMicWait) {
|
|
543
|
+
this.log("[SSM] skipMicWait \u2014 warm restart, returning immediately");
|
|
544
|
+
return Promise.resolve();
|
|
545
|
+
}
|
|
516
546
|
return micClaimPromise;
|
|
517
547
|
}
|
|
518
548
|
clearNoResultTimer() {
|
|
@@ -560,6 +590,8 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
560
590
|
capture = null;
|
|
561
591
|
state;
|
|
562
592
|
workerUrl;
|
|
593
|
+
/** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
|
|
594
|
+
_stopping = false;
|
|
563
595
|
/**
|
|
564
596
|
* Create a new STT engine instance.
|
|
565
597
|
* @param config - Optional configuration overrides (model, backend, language, etc.).
|
|
@@ -605,14 +637,22 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
605
637
|
throw new Error(`Cannot start: engine is "${this.state.status}", expected "ready"`);
|
|
606
638
|
}
|
|
607
639
|
try {
|
|
640
|
+
const warmCapture = this.capture && this.capture.stream.getTracks().every((t) => t.readyState === "live");
|
|
608
641
|
this.emitDebug(
|
|
609
|
-
`[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}"`
|
|
642
|
+
`[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}", warm: ${!!warmCapture}`
|
|
610
643
|
);
|
|
611
644
|
if (this.config.streaming.enabled) {
|
|
612
|
-
await this.speechStreaming.start(this.config.language);
|
|
613
|
-
|
|
645
|
+
await this.speechStreaming.start(this.config.language, !!warmCapture);
|
|
646
|
+
if (!warmCapture) {
|
|
647
|
+
this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (warmCapture) {
|
|
651
|
+
await resumeCapture(this.capture);
|
|
652
|
+
this.emitDebug("[STT] warm mic resumed \u2014 skipped getUserMedia");
|
|
653
|
+
} else {
|
|
654
|
+
this.capture = await startCapture();
|
|
614
655
|
}
|
|
615
|
-
this.capture = await startCapture();
|
|
616
656
|
this.updateStatus("recording");
|
|
617
657
|
this.correctionOrchestrator.start();
|
|
618
658
|
} catch (err) {
|
|
@@ -622,16 +662,49 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
622
662
|
);
|
|
623
663
|
}
|
|
624
664
|
}
|
|
625
|
-
/** Stop recording, run final transcription, return text.
|
|
665
|
+
/** Stop recording, run final transcription, return text.
|
|
666
|
+
* Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
|
|
626
667
|
async stop() {
|
|
627
668
|
if (!this.capture) return "";
|
|
669
|
+
this._stopping = true;
|
|
628
670
|
this.correctionOrchestrator.stop();
|
|
629
671
|
this.speechStreaming.stop();
|
|
630
|
-
this.workerManager.cancel();
|
|
631
672
|
this.updateStatus("processing");
|
|
673
|
+
if (this.workerManager.isTranscribing) {
|
|
674
|
+
try {
|
|
675
|
+
const [audio, inFlightText] = await Promise.all([
|
|
676
|
+
pauseCapture(this.capture),
|
|
677
|
+
this.workerManager.awaitCurrentTranscription()
|
|
678
|
+
]);
|
|
679
|
+
this._stopping = false;
|
|
680
|
+
const text = inFlightText.trim();
|
|
681
|
+
if (text) {
|
|
682
|
+
this.emit("correction", text);
|
|
683
|
+
this.updateStatus("ready");
|
|
684
|
+
return text;
|
|
685
|
+
}
|
|
686
|
+
if (audio.length > 0) {
|
|
687
|
+
const freshText = await this.workerManager.transcribe(audio);
|
|
688
|
+
this.emit("correction", freshText);
|
|
689
|
+
this.updateStatus("ready");
|
|
690
|
+
return freshText;
|
|
691
|
+
}
|
|
692
|
+
this.updateStatus("ready");
|
|
693
|
+
return "";
|
|
694
|
+
} catch (err) {
|
|
695
|
+
this._stopping = false;
|
|
696
|
+
this.emitError(
|
|
697
|
+
"TRANSCRIPTION_FAILED",
|
|
698
|
+
err instanceof Error ? err.message : "Final transcription failed."
|
|
699
|
+
);
|
|
700
|
+
this.updateStatus("ready");
|
|
701
|
+
return "";
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
this.workerManager.cancel();
|
|
705
|
+
this._stopping = false;
|
|
632
706
|
try {
|
|
633
|
-
const audio = await
|
|
634
|
-
this.capture = null;
|
|
707
|
+
const audio = await pauseCapture(this.capture);
|
|
635
708
|
if (audio.length === 0) {
|
|
636
709
|
this.updateStatus("ready");
|
|
637
710
|
return "";
|
|
@@ -654,6 +727,10 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
654
727
|
this.correctionOrchestrator.stop();
|
|
655
728
|
this.speechStreaming.destroy();
|
|
656
729
|
if (this.capture) {
|
|
730
|
+
try {
|
|
731
|
+
this.capture._processor.disconnect();
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
657
734
|
for (const track of this.capture.stream.getTracks()) {
|
|
658
735
|
track.stop();
|
|
659
736
|
}
|
|
@@ -682,7 +759,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
682
759
|
const audio = await resampleAudio(samples, nativeSr);
|
|
683
760
|
if (audio.length === 0) return;
|
|
684
761
|
const text = await this.workerManager.transcribe(audio);
|
|
685
|
-
if (text.trim() && this.capture) {
|
|
762
|
+
if (text.trim() && this.capture && !this._stopping) {
|
|
686
763
|
this.emit("correction", text);
|
|
687
764
|
}
|
|
688
765
|
} catch (err) {
|
|
@@ -739,8 +816,10 @@ export {
|
|
|
739
816
|
SpeechStreamingManager,
|
|
740
817
|
TypedEventEmitter,
|
|
741
818
|
WorkerManager,
|
|
819
|
+
pauseCapture,
|
|
742
820
|
resampleAudio,
|
|
743
821
|
resolveConfig,
|
|
822
|
+
resumeCapture,
|
|
744
823
|
snapshotAudio,
|
|
745
824
|
startCapture,
|
|
746
825
|
stopCapture
|
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. */\r\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\r\n\r\n/** Supported compute backends. */\r\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\r\n\r\n/** Engine lifecycle states. */\r\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\r\n\r\n/** Supported correction engine providers. */\r\nexport type STTCorrectionProvider = 'whisper';\r\n\r\n/** Supported real-time streaming providers. */\r\nexport type STTStreamingProvider = 'web-speech-api';\r\n\r\n/** Correction engine configuration. */\r\nexport interface STTCorrectionConfig {\r\n /** Enable mid-recording correction. Default: true */\r\n enabled?: boolean;\r\n /** Correction engine provider. Default: 'whisper' */\r\n provider?: STTCorrectionProvider;\r\n /** Silence duration (ms) before triggering correction. Default: 3000 */\r\n pauseThreshold?: number;\r\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\r\n forcedInterval?: number;\r\n}\r\n\r\n/** Real-time streaming preview configuration. */\r\nexport interface STTStreamingConfig {\r\n /** Enable real-time streaming transcript. Default: true */\r\n enabled?: boolean;\r\n /** Streaming provider. Default: 'web-speech-api' */\r\n provider?: STTStreamingProvider;\r\n}\r\n\r\n/** Audio chunking configuration for long-form audio. */\r\nexport interface STTChunkingConfig {\r\n /** Chunk length in seconds for Whisper processing. Default: 30 */\r\n chunkLengthS?: number;\r\n /** Stride length in seconds for overlapping chunks. Default: 5 */\r\n strideLengthS?: number;\r\n}\r\n\r\n/** Full engine configuration. All fields optional — sensible defaults applied. */\r\nexport interface STTConfig {\r\n /** Whisper model size. Default: 'tiny' */\r\n model?: STTModelSize;\r\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\r\n backend?: STTBackend;\r\n /** Transcription language. Default: 'en' */\r\n language?: string;\r\n /** Model quantization dtype. Default: 'q4' */\r\n dtype?: string;\r\n /** Mid-recording correction settings. */\r\n correction?: STTCorrectionConfig;\r\n /** Audio chunking settings for long-form audio. */\r\n chunking?: STTChunkingConfig;\r\n /** Web Speech API streaming preview settings. */\r\n streaming?: STTStreamingConfig;\r\n}\r\n\r\n/** Resolved configuration with all defaults applied. */\r\nexport interface ResolvedSTTConfig {\r\n model: STTModelSize;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n correction: Required<STTCorrectionConfig>;\r\n chunking: Required<STTChunkingConfig>;\r\n streaming: Required<STTStreamingConfig>;\r\n}\r\n\r\n/** Engine state exposed to consumers via status events. */\r\nexport interface STTState {\r\n status: STTStatus;\r\n isModelLoaded: boolean;\r\n /** Model download progress (0–100). */\r\n loadProgress: number;\r\n /** Active compute backend, or null if not yet determined. */\r\n backend: 'webgpu' | 'wasm' | null;\r\n error: string | null;\r\n}\r\n\r\n/** Structured error emitted via the 'error' event. */\r\nexport interface STTError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\n/** Event map for the typed event emitter. */\r\nexport type STTEvents = {\r\n /** Streaming interim text during recording. */\r\n transcript: (text: string) => void;\r\n /** Whisper-corrected text replacing interim text. */\r\n correction: (text: string) => void;\r\n /** Actionable error (mic denied, model fail, transcription fail). */\r\n error: (error: STTError) => void;\r\n /** Engine state change. */\r\n status: (state: STTState) => void;\r\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\r\n debug: (message: string) => void;\r\n};\r\n\r\n/** Handle returned by audio capture — used internally. */\r\nexport interface AudioCaptureHandle {\r\n audioCtx: AudioContext;\r\n stream: MediaStream;\r\n samples: Float32Array[];\r\n /** Retain reference to prevent GC from stopping audio processing. */\r\n _processor: ScriptProcessorNode;\r\n}\r\n\r\n/** Message sent from main thread to Whisper worker. */\r\nexport interface WorkerMessage {\r\n type: 'load' | 'transcribe' | 'cancel';\r\n audio?: Float32Array;\r\n config?: {\r\n model: string;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n chunkLengthS: number;\r\n strideLengthS: number;\r\n };\r\n}\r\n\r\n/** Response sent from Whisper worker to main thread. */\r\nexport interface WorkerResponse {\r\n type: 'progress' | 'ready' | 'result' | 'error';\r\n data?: unknown;\r\n}\r\n\r\n/** Default configuration values. */\r\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\r\n model: 'tiny',\r\n backend: 'auto',\r\n language: 'en',\r\n dtype: 'q4',\r\n correction: {\r\n enabled: true,\r\n provider: 'whisper',\r\n pauseThreshold: 3_000,\r\n forcedInterval: 5_000,\r\n },\r\n chunking: {\r\n chunkLengthS: 30,\r\n strideLengthS: 5,\r\n },\r\n streaming: {\r\n enabled: true,\r\n provider: 'web-speech-api',\r\n },\r\n};\r\n\r\n/** Merge user config with defaults to produce resolved config. */\r\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\r\n return {\r\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\r\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\r\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\r\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\r\n correction: {\r\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\r\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\r\n pauseThreshold:\r\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\r\n forcedInterval:\r\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\r\n },\r\n chunking: {\r\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\r\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\r\n },\r\n streaming: {\r\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\r\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\r\n },\r\n };\r\n}\r\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';\r\n\r\nconst TARGET_SAMPLE_RATE = 16_000;\r\n\r\n/**\r\n * Start capturing raw PCM audio from the microphone.\r\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\r\n */\r\nexport async function startCapture(): Promise<AudioCaptureHandle> {\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: { channelCount: 1 },\r\n });\r\n const audioCtx = new AudioContext();\r\n\r\n // Chrome may suspend AudioContext — must resume within user gesture\r\n if (audioCtx.state === 'suspended') {\r\n await audioCtx.resume();\r\n }\r\n\r\n const source = audioCtx.createMediaStreamSource(stream);\r\n const samples: Float32Array[] = [];\r\n\r\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\r\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\r\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\r\n };\r\n\r\n // Connect through a silent gain node so mic audio doesn't play back\r\n const silencer = audioCtx.createGain();\r\n silencer.gain.value = 0;\r\n source.connect(processor);\r\n processor.connect(silencer);\r\n silencer.connect(audioCtx.destination);\r\n\r\n return { audioCtx, stream, samples, _processor: processor };\r\n}\r\n\r\n/**\r\n * Copy current audio buffer without stopping capture.\r\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\r\n */\r\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\r\n return [...capture.samples];\r\n}\r\n\r\n/**\r\n * Concatenate sample chunks and resample to 16kHz for Whisper.\r\n */\r\nexport async function resampleAudio(\r\n samples: Float32Array[],\r\n nativeSr: number,\r\n): Promise<Float32Array> {\r\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\r\n if (totalLength === 0) return new Float32Array(0);\r\n\r\n const fullAudio = new Float32Array(totalLength);\r\n let offset = 0;\r\n for (const s of samples) {\r\n fullAudio.set(s, offset);\r\n offset += s.length;\r\n }\r\n\r\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\r\n\r\n const duration = fullAudio.length / nativeSr;\r\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\r\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\r\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\r\n buffer.getChannelData(0).set(fullAudio);\r\n const src = offline.createBufferSource();\r\n src.buffer = buffer;\r\n src.connect(offline.destination);\r\n src.start(0);\r\n const resampled = await offline.startRendering();\r\n return resampled.getChannelData(0);\r\n}\r\n\r\n/**\r\n * Stop capturing and return resampled audio at 16kHz.\r\n */\r\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n const { audioCtx, stream, samples, _processor } = capture;\r\n\r\n // Disconnect processor to stop capturing\r\n try {\r\n _processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n\r\n // Stop microphone tracks\r\n for (const track of stream.getTracks()) {\r\n track.stop();\r\n }\r\n\r\n const nativeSr = audioCtx.sampleRate;\r\n await audioCtx.close();\r\n\r\n return resampleAudio(samples, nativeSr);\r\n}\r\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 ──────────────────────────────── */\r\n\r\ninterface SpeechRecognitionEvent {\r\n results: SpeechRecognitionResultList;\r\n resultIndex: number;\r\n}\r\n\r\ninterface SpeechRecognitionErrorEvent {\r\n error: string;\r\n}\r\n\r\ninterface SpeechRecognitionInstance {\r\n continuous: boolean;\r\n interimResults: boolean;\r\n lang: string;\r\n onaudiostart: (() => void) | null;\r\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\r\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\r\n onend: (() => void) | null;\r\n start: () => void;\r\n stop: () => void;\r\n abort: () => void;\r\n}\r\n\r\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\r\n\r\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\r\n if (typeof globalThis === 'undefined') return null;\r\n const w = globalThis as unknown as Record<string, unknown>;\r\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\r\n}\r\n\r\n/* ─── Language mapping ──────────────────────────────────── */\r\n\r\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\r\nconst WHISPER_TO_BCP47: Record<string, string> = {\r\n en: 'en-US',\r\n english: 'en-US',\r\n zh: 'zh-CN',\r\n chinese: 'zh-CN',\r\n de: 'de-DE',\r\n german: 'de-DE',\r\n es: 'es-ES',\r\n spanish: 'es-ES',\r\n ru: 'ru-RU',\r\n russian: 'ru-RU',\r\n ko: 'ko-KR',\r\n korean: 'ko-KR',\r\n fr: 'fr-FR',\r\n french: 'fr-FR',\r\n ja: 'ja-JP',\r\n japanese: 'ja-JP',\r\n pt: 'pt-BR',\r\n portuguese: 'pt-BR',\r\n tr: 'tr-TR',\r\n turkish: 'tr-TR',\r\n pl: 'pl-PL',\r\n polish: 'pl-PL',\r\n nl: 'nl-NL',\r\n dutch: 'nl-NL',\r\n ar: 'ar-SA',\r\n arabic: 'ar-SA',\r\n sv: 'sv-SE',\r\n swedish: 'sv-SE',\r\n it: 'it-IT',\r\n italian: 'it-IT',\r\n id: 'id-ID',\r\n indonesian: 'id-ID',\r\n hi: 'hi-IN',\r\n hindi: 'hi-IN',\r\n fi: 'fi-FI',\r\n finnish: 'fi-FI',\r\n vi: 'vi-VN',\r\n vietnamese: 'vi-VN',\r\n he: 'he-IL',\r\n hebrew: 'he-IL',\r\n uk: 'uk-UA',\r\n ukrainian: 'uk-UA',\r\n el: 'el-GR',\r\n greek: 'el-GR',\r\n ms: 'ms-MY',\r\n malay: 'ms-MY',\r\n cs: 'cs-CZ',\r\n czech: 'cs-CZ',\r\n ro: 'ro-RO',\r\n romanian: 'ro-RO',\r\n da: 'da-DK',\r\n danish: 'da-DK',\r\n hu: 'hu-HU',\r\n hungarian: 'hu-HU',\r\n no: 'nb-NO',\r\n norwegian: 'nb-NO',\r\n th: 'th-TH',\r\n thai: 'th-TH',\r\n};\r\n\r\n/**\r\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\r\n * Already-BCP-47 codes (containing '-') pass through unchanged.\r\n */\r\nfunction toBCP47(language: string): string {\r\n if (language.includes('-')) return language;\r\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\r\n}\r\n\r\n/* ─── SpeechStreamingManager ────────────────────────────── */\r\n\r\n/**\r\n * Manages Web Speech API for real-time streaming transcript preview.\r\n * Provides word-by-word interim text while Whisper handles corrections.\r\n */\r\nconst NO_RESULT_TIMEOUT_MS = 5_000;\r\n\r\nexport class SpeechStreamingManager {\r\n private recognition: SpeechRecognitionInstance | null = null;\r\n private accumulated = '';\r\n private active = false;\r\n private receivedResult = false;\r\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\r\n private onTranscript: ((text: string) => void) | null = null;\r\n private onPause: (() => void) | null = null;\r\n private onError: ((message: string) => void) | null = null;\r\n private onDebug: ((message: string) => void) | null = null;\r\n\r\n /** Check if the Web Speech API is available in this environment. */\r\n static isSupported(): boolean {\r\n return getSpeechRecognition() !== null;\r\n }\r\n\r\n /** Set callback for streaming transcript updates (interim + final text). */\r\n setOnTranscript(fn: (text: string) => void): void {\r\n this.onTranscript = fn;\r\n }\r\n\r\n /** Set callback for speech pause detection (Speech API onend). */\r\n setOnPause(fn: () => void): void {\r\n this.onPause = fn;\r\n }\r\n\r\n /** Set callback for errors. */\r\n setOnError(fn: (message: string) => void): void {\r\n this.onError = fn;\r\n }\r\n\r\n /** Set callback for diagnostic debug messages. */\r\n setOnDebug(fn: (message: string) => void): void {\r\n this.onDebug = fn;\r\n }\r\n\r\n private log(message: string): void {\r\n this.onDebug?.(message);\r\n console.warn(message);\r\n }\r\n\r\n /**\r\n * Start streaming recognition. Returns a Promise that resolves once\r\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\r\n * a 300ms fallback — whichever comes first. The engine should await\r\n * this before calling getUserMedia to avoid dual-mic conflicts.\r\n */\r\n start(language: string): Promise<void> {\r\n const SR = getSpeechRecognition();\r\n if (!SR) {\r\n this.log('[SSM] SpeechRecognition not available in this environment');\r\n return Promise.resolve();\r\n }\r\n\r\n const bcp47 = toBCP47(language);\r\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\r\n\r\n this.accumulated = '';\r\n this.active = true;\r\n this.receivedResult = false;\r\n\r\n const recognition = new SR();\r\n recognition.continuous = true;\r\n recognition.interimResults = true;\r\n recognition.lang = bcp47;\r\n\r\n let lastFinalIndex = -1;\r\n let lastFinalText = '';\r\n\r\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\r\n // This ensures getUserMedia doesn't compete for the mic.\r\n let micReady = false;\r\n const micClaimPromise = new Promise<void>((resolve) => {\r\n recognition.onaudiostart = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\r\n if (!micReady) {\r\n micReady = true;\r\n resolve();\r\n }\r\n };\r\n // Fallback: resolve after 300ms even if onaudiostart never fires\r\n setTimeout(() => {\r\n if (!micReady) {\r\n micReady = true;\r\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\r\n resolve();\r\n }\r\n }, 300);\r\n });\r\n\r\n // Detect silent failure: if no onresult fires within timeout, emit error\r\n this.clearNoResultTimer();\r\n this.noResultTimer = setTimeout(() => {\r\n if (this.active && !this.receivedResult) {\r\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\r\n this.onError?.(\r\n 'Speech streaming started but received no results. ' +\r\n 'Mic may be blocked by another audio capture.',\r\n );\r\n }\r\n }, NO_RESULT_TIMEOUT_MS);\r\n\r\n recognition.onresult = (e: SpeechRecognitionEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.receivedResult = true;\r\n this.clearNoResultTimer();\r\n\r\n let final_ = '';\r\n let interim = '';\r\n for (let i = e.resultIndex; i < e.results.length; i++) {\r\n const t = e.results[i][0].transcript;\r\n if (e.results[i].isFinal) {\r\n if (i > lastFinalIndex) {\r\n final_ += t;\r\n lastFinalIndex = i;\r\n }\r\n } else {\r\n interim += t;\r\n }\r\n }\r\n\r\n this.log(\r\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\r\n );\r\n\r\n if (final_ && final_.trim() !== lastFinalText) {\r\n lastFinalText = final_.trim();\r\n this.accumulated = this.accumulated\r\n ? this.accumulated + ' ' + final_.trim()\r\n : final_.trim();\r\n this.onTranscript?.(this.accumulated);\r\n } else if (interim) {\r\n const trimmed = interim.trimStart();\r\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\r\n this.onTranscript?.(full);\r\n }\r\n };\r\n\r\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onerror — ${e.error}`);\r\n this.onError?.(e.error);\r\n };\r\n\r\n recognition.onend = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\r\n\r\n if (this.active) {\r\n // Speech API paused — trigger correction\r\n this.onPause?.();\r\n // Restart for continued streaming\r\n try {\r\n recognition.start();\r\n this.log('[SSM] restarted after pause');\r\n } catch (err) {\r\n this.log(`[SSM] restart THREW: ${err}`);\r\n this.recognition = null;\r\n this.onError?.('Speech recognition failed to restart after pause.');\r\n }\r\n } else {\r\n this.recognition = null;\r\n }\r\n };\r\n\r\n this.recognition = recognition;\r\n try {\r\n recognition.start();\r\n this.log('[SSM] recognition.start() succeeded');\r\n } catch (err) {\r\n this.log(`[SSM] recognition.start() THREW: ${err}`);\r\n this.recognition = null;\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n this.onError?.(\r\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\r\n }\r\n\r\n return micClaimPromise;\r\n }\r\n\r\n private clearNoResultTimer(): void {\r\n if (this.noResultTimer) {\r\n clearTimeout(this.noResultTimer);\r\n this.noResultTimer = null;\r\n }\r\n }\r\n\r\n /** Stop streaming recognition and return accumulated text. */\r\n stop(): string {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.stop();\r\n }\r\n const result = this.accumulated;\r\n this.accumulated = '';\r\n return result;\r\n }\r\n\r\n /** Abort immediately without returning text. */\r\n destroy(): void {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.abort();\r\n }\r\n this.accumulated = '';\r\n this.onTranscript = null;\r\n this.onPause = null;\r\n this.onError = null;\r\n this.onDebug = null;\r\n }\r\n}\r\n","import type {\r\n STTConfig,\r\n STTState,\r\n STTEvents,\r\n STTStatus,\r\n ResolvedSTTConfig,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\nimport { resolveConfig } from './types.js';\r\nimport { TypedEventEmitter } from './event-emitter.js';\r\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\r\nimport { WorkerManager } from './worker-manager.js';\r\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\nimport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n/**\r\n * Main STT engine — the public API for speech-to-text with Whisper correction.\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const engine = new STTEngine({ model: 'tiny' });\r\n * engine.on('transcript', (text) => console.log(text));\r\n * engine.on('correction', (text) => console.log('corrected:', text));\r\n * await engine.init();\r\n * await engine.start();\r\n * const finalText = await engine.stop();\r\n * ```\r\n */\r\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\r\n private config: ResolvedSTTConfig;\r\n private workerManager: WorkerManager;\r\n private correctionOrchestrator: CorrectionOrchestrator;\r\n private speechStreaming: SpeechStreamingManager;\r\n private capture: AudioCaptureHandle | null = null;\r\n private state: STTState;\r\n private workerUrl?: URL;\r\n\r\n /**\r\n * Create a new STT engine instance.\r\n * @param config - Optional configuration overrides (model, backend, language, etc.).\r\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\r\n */\r\n constructor(config?: STTConfig, workerUrl?: URL) {\r\n super();\r\n this.config = resolveConfig(config);\r\n this.workerManager = new WorkerManager();\r\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\r\n this.speechStreaming = new SpeechStreamingManager();\r\n this.workerUrl = workerUrl;\r\n\r\n this.state = {\r\n status: 'idle',\r\n isModelLoaded: false,\r\n loadProgress: 0,\r\n backend: null,\r\n error: null,\r\n };\r\n\r\n this.correctionOrchestrator.setCorrectionFn(() => {\r\n this.performCorrection();\r\n });\r\n\r\n this.setupWorkerListeners();\r\n this.setupStreamingCallbacks();\r\n }\r\n\r\n /** Initialize the engine: spawn worker and load model. */\r\n async init(): Promise<void> {\r\n this.updateStatus('loading');\r\n this.workerManager.spawn(this.workerUrl);\r\n\r\n try {\r\n await this.workerManager.loadModel(this.config);\r\n this.state.isModelLoaded = true;\r\n this.updateStatus('ready');\r\n } catch (err) {\r\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\r\n this.updateStatus('idle');\r\n throw err;\r\n }\r\n }\r\n\r\n /** Start recording audio and enable correction cycles. */\r\n async start(): Promise<void> {\r\n if (this.state.status !== 'ready') {\r\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\r\n }\r\n\r\n try {\r\n // Start Speech API BEFORE getUserMedia and wait for it to claim\r\n // the mic (onaudiostart or 300ms fallback). Without this wait,\r\n // getUserMedia opens a competing audio capture and Chrome's\r\n // SpeechRecognition receives no audio (silent failure).\r\n this.emitDebug(\r\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\"`,\r\n );\r\n if (this.config.streaming.enabled) {\r\n await this.speechStreaming.start(this.config.language);\r\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\r\n }\r\n this.capture = await startCapture();\r\n this.updateStatus('recording');\r\n this.correctionOrchestrator.start();\r\n } catch (err) {\r\n this.emitError(\r\n 'MIC_DENIED',\r\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\r\n );\r\n }\r\n }\r\n\r\n /** Stop recording, run final transcription, return text. */\r\n async stop(): Promise<string> {\r\n if (!this.capture) return '';\r\n\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.stop();\r\n this.workerManager.cancel();\r\n\r\n this.updateStatus('processing');\r\n\r\n try {\r\n const audio = await stopCapture(this.capture);\r\n this.capture = null;\r\n\r\n if (audio.length === 0) {\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n /** Destroy the engine: terminate worker, release all resources. */\r\n destroy(): void {\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.destroy();\r\n\r\n if (this.capture) {\r\n for (const track of this.capture.stream.getTracks()) {\r\n track.stop();\r\n }\r\n this.capture.audioCtx.close().catch(() => {});\r\n this.capture = null;\r\n }\r\n\r\n this.workerManager.destroy();\r\n this.updateStatus('idle');\r\n this.removeAllListeners();\r\n }\r\n\r\n /** Get current engine state. */\r\n getState(): Readonly<STTState> {\r\n return { ...this.state };\r\n }\r\n\r\n /** Notify the correction orchestrator of a speech pause. */\r\n notifyPause(): void {\r\n this.correctionOrchestrator.onPauseDetected();\r\n }\r\n\r\n private async performCorrection(): Promise<void> {\r\n if (!this.capture || !this.state.isModelLoaded) return;\r\n\r\n this.workerManager.cancel();\r\n\r\n try {\r\n const samples = snapshotAudio(this.capture);\r\n const nativeSr = this.capture.audioCtx.sampleRate;\r\n const audio = await resampleAudio(samples, nativeSr);\r\n\r\n if (audio.length === 0) return;\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n if (text.trim() && this.capture) {\r\n this.emit('correction', text);\r\n }\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Correction transcription failed.',\r\n );\r\n // Recording continues — error is non-fatal\r\n }\r\n }\r\n\r\n private setupStreamingCallbacks(): void {\r\n this.speechStreaming.setOnDebug((message) => {\r\n this.emit('debug', message);\r\n });\r\n\r\n this.speechStreaming.setOnTranscript((text) => {\r\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\r\n this.emit('transcript', text);\r\n });\r\n\r\n this.speechStreaming.setOnPause(() => {\r\n this.emitDebug('[STT] pause callback — triggering correction');\r\n this.correctionOrchestrator.onPauseDetected();\r\n });\r\n\r\n this.speechStreaming.setOnError((message) => {\r\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\r\n this.emitError('STREAMING_ERROR', message);\r\n });\r\n }\r\n\r\n private setupWorkerListeners(): void {\r\n this.workerManager.on('progress', (percent) => {\r\n this.state.loadProgress = percent;\r\n this.emit('status', { ...this.state });\r\n });\r\n\r\n this.workerManager.on('error', (message) => {\r\n this.emitError('WORKER_ERROR', message);\r\n });\r\n }\r\n\r\n private updateStatus(status: STTStatus): void {\r\n this.state.status = status;\r\n this.state.error = null;\r\n this.emit('status', { ...this.state });\r\n }\r\n\r\n private emitError(code: string, message: string): void {\r\n this.state.error = message;\r\n this.emit('error', { code, message });\r\n }\r\n\r\n private emitDebug(message: string): void {\r\n console.warn(message);\r\n this.emit('debug', message);\r\n }\r\n}\r\n"],"mappings":";AAqIO,IAAM,qBAAwC;AAAA,EACnD,OAAO;AAAA,EACP,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA,EACP,YAAY;AAAA,IACV,SAAS;AAAA,IACT,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,EAClB;AAAA,EACA,UAAU;AAAA,IACR,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,WAAW;AAAA,IACT,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AACF;AAGO,SAAS,cAAc,QAAuC;AACnE,SAAO;AAAA,IACL,OAAO,QAAQ,SAAS,mBAAmB;AAAA,IAC3C,SAAS,QAAQ,WAAW,mBAAmB;AAAA,IAC/C,UAAU,QAAQ,YAAY,mBAAmB;AAAA,IACjD,OAAO,QAAQ,SAAS,mBAAmB;AAAA,IAC3C,YAAY;AAAA,MACV,SAAS,QAAQ,YAAY,WAAW,mBAAmB,WAAW;AAAA,MACtE,UAAU,QAAQ,YAAY,YAAY,mBAAmB,WAAW;AAAA,MACxE,gBACE,QAAQ,YAAY,kBAAkB,mBAAmB,WAAW;AAAA,MACtE,gBACE,QAAQ,YAAY,kBAAkB,mBAAmB,WAAW;AAAA,IACxE;AAAA,IACA,UAAU;AAAA,MACR,cAAc,QAAQ,UAAU,gBAAgB,mBAAmB,SAAS;AAAA,MAC5E,eAAe,QAAQ,UAAU,iBAAiB,mBAAmB,SAAS;AAAA,IAChF;AAAA,IACA,WAAW;AAAA,MACT,SAAS,QAAQ,WAAW,WAAW,mBAAmB,UAAU;AAAA,MACpE,UAAU,QAAQ,WAAW,YAAY,mBAAmB,UAAU;AAAA,IACxE;AAAA,EACF;AACF;;;AC3KO,IAAM,oBAAN,MAA4E;AAAA,EACzE,YAAY,oBAAI,IAA8B;AAAA;AAAA,EAGtD,GAAsB,OAAU,UAAsB;AACpD,QAAI,MAAM,KAAK,UAAU,IAAI,KAAK;AAClC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,UAAU,IAAI,OAAO,GAAG;AAAA,IAC/B;AACA,QAAI,IAAI,QAAsB;AAAA,EAChC;AAAA;AAAA,EAGA,IAAuB,OAAU,UAAsB;AACrD,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,QAAsB;AAAA,EAC1D;AAAA;AAAA,EAGA,KAAwB,UAAa,MAA8B;AACjE,UAAM,MAAM,KAAK,UAAU,IAAI,KAAK;AACpC,QAAI,CAAC,IAAK;AACV,eAAW,YAAY,KAAK;AAC1B,MAAC,SAA8C,GAAG,IAAI;AAAA,IACxD;AAAA,EACF;AAAA;AAAA,EAGA,mBAAmB,OAAuB;AACxC,QAAI,UAAU,QAAW;AACvB,WAAK,UAAU,OAAO,KAAK;AAAA,IAC7B,OAAO;AACL,WAAK,UAAU,MAAM;AAAA,IACvB;AAAA,EACF;AACF;;;ACxCA,IAAM,qBAAqB;AAM3B,eAAsB,eAA4C;AAChE,QAAM,SAAS,MAAM,UAAU,aAAa,aAAa;AAAA,IACvD,OAAO,EAAE,cAAc,EAAE;AAAA,EAC3B,CAAC;AACD,QAAM,WAAW,IAAI,aAAa;AAGlC,MAAI,SAAS,UAAU,aAAa;AAClC,UAAM,SAAS,OAAO;AAAA,EACxB;AAEA,QAAM,SAAS,SAAS,wBAAwB,MAAM;AACtD,QAAM,UAA0B,CAAC;AAEjC,QAAM,YAAY,SAAS,sBAAsB,MAAM,GAAG,CAAC;AAC3D,YAAU,iBAAiB,CAAC,MAA4B;AACtD,YAAQ,KAAK,IAAI,aAAa,EAAE,YAAY,eAAe,CAAC,CAAC,CAAC;AAAA,EAChE;AAGA,QAAM,WAAW,SAAS,WAAW;AACrC,WAAS,KAAK,QAAQ;AACtB,SAAO,QAAQ,SAAS;AACxB,YAAU,QAAQ,QAAQ;AAC1B,WAAS,QAAQ,SAAS,WAAW;AAErC,SAAO,EAAE,UAAU,QAAQ,SAAS,YAAY,UAAU;AAC5D;AAMO,SAAS,cAAc,SAA6C;AACzE,SAAO,CAAC,GAAG,QAAQ,OAAO;AAC5B;AAKA,eAAsB,cACpB,SACA,UACuB;AACvB,QAAM,cAAc,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AAChE,MAAI,gBAAgB,EAAG,QAAO,IAAI,aAAa,CAAC;AAEhD,QAAM,YAAY,IAAI,aAAa,WAAW;AAC9C,MAAI,SAAS;AACb,aAAW,KAAK,SAAS;AACvB,cAAU,IAAI,GAAG,MAAM;AACvB,cAAU,EAAE;AAAA,EACd;AAEA,MAAI,aAAa,mBAAoB,QAAO;AAE5C,QAAM,WAAW,UAAU,SAAS;AACpC,QAAM,YAAY,KAAK,MAAM,WAAW,kBAAkB;AAC1D,QAAM,UAAU,IAAI,oBAAoB,GAAG,WAAW,kBAAkB;AACxE,QAAM,SAAS,QAAQ,aAAa,GAAG,UAAU,QAAQ,QAAQ;AACjE,SAAO,eAAe,CAAC,EAAE,IAAI,SAAS;AACtC,QAAM,MAAM,QAAQ,mBAAmB;AACvC,MAAI,SAAS;AACb,MAAI,QAAQ,QAAQ,WAAW;AAC/B,MAAI,MAAM,CAAC;AACX,QAAM,YAAY,MAAM,QAAQ,eAAe;AAC/C,SAAO,UAAU,eAAe,CAAC;AACnC;AAKA,eAAsB,YAAY,SAAoD;AACpF,QAAM,EAAE,UAAU,QAAQ,SAAS,WAAW,IAAI;AAGlD,MAAI;AACF,eAAW,WAAW;AAAA,EACxB,QAAQ;AAAA,EAER;AAGA,aAAW,SAAS,OAAO,UAAU,GAAG;AACtC,UAAM,KAAK;AAAA,EACb;AAEA,QAAM,WAAW,SAAS;AAC1B,QAAM,SAAS,MAAM;AAErB,SAAO,cAAc,SAAS,QAAQ;AACxC;;;ACpFO,IAAM,gBAAN,cAA4B,kBAAuC;AAAA,EAChE,SAAwB;AAAA,EACxB,oBAAqD;AAAA,EACrD,oBAAyC;AAAA,EACzC,mBAAkD;AAAA;AAAA,EAG1D,MAAM,WAAuB;AAC3B,QAAI,KAAK,OAAQ;AAEjB,UAAM,MAAM,aAAa,IAAI,IAAI,uBAAuB,YAAY,GAAG;AAEvE,SAAK,SAAS,IAAI,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAChD,SAAK,OAAO,YAAY,CAAC,MAAoC;AAC3D,WAAK,cAAc,EAAE,IAAI;AAAA,IAC3B;AACA,SAAK,OAAO,UAAU,CAAC,MAAkB;AACvC,WAAK,KAAK,SAAS,EAAE,WAAW,cAAc;AAAA,IAChD;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,UAAU,QAA0C;AACxD,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AAEtD,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,oBAAoB;AACzB,WAAK,mBAAmB;AACxB,WAAK,OAAQ,YAAY;AAAA,QACvB,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,OAAO,OAAO;AAAA,UACd,SAAS,OAAO;AAAA,UAChB,UAAU,OAAO;AAAA,UACjB,OAAO,OAAO;AAAA,UACd,cAAc,OAAO,SAAS;AAAA,UAC9B,eAAe,OAAO,SAAS;AAAA,QACjC;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,WAAW,OAAsC;AACrD,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACtD,QAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,WAAO,IAAI,QAAgB,CAAC,YAAY;AACtC,WAAK,oBAAoB;AACzB,WAAK,OAAQ,YAAY,EAAE,MAAM,cAAc,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC;AAAA,IACxE,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,SAAe;AACb,SAAK,QAAQ,YAAY,EAAE,MAAM,SAAS,CAAC;AAC3C,QAAI,KAAK,mBAAmB;AAC1B,WAAK,kBAAkB,EAAE;AACzB,WAAK,oBAAoB;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,OAAO;AACZ,SAAK,QAAQ,UAAU;AACvB,SAAK,SAAS;AACd,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEQ,cAAc,KAA2B;AAC/C,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,aAAK,KAAK,YAAY,IAAI,IAAc;AACxC;AAAA,MACF,KAAK;AACH,aAAK,KAAK,OAAO;AACjB,aAAK,oBAAoB;AACzB,aAAK,oBAAoB;AACzB,aAAK,mBAAmB;AACxB;AAAA,MACF,KAAK;AACH,aAAK,KAAK,UAAU,IAAI,IAAc;AACtC,aAAK,oBAAoB,IAAI,IAAc;AAC3C,aAAK,oBAAoB;AACzB;AAAA,MACF,KAAK,SAAS;AACZ,cAAM,SAAS,IAAI;AACnB,aAAK,KAAK,SAAS,MAAM;AAEzB,YAAI,KAAK,kBAAkB;AACzB,eAAK,iBAAiB,IAAI,MAAM,MAAM,CAAC;AACvC,eAAK,oBAAoB;AACzB,eAAK,mBAAmB;AAAA,QAC1B;AAEA,YAAI,KAAK,mBAAmB;AAC1B,eAAK,kBAAkB,EAAE;AACzB,eAAK,oBAAoB;AAAA,QAC3B;AACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACjHO,IAAM,yBAAN,MAA6B;AAAA,EAC1B,cAAqD;AAAA,EACrD,qBAAqB;AAAA,EACrB,eAAoC;AAAA,EACpC;AAAA;AAAA,EAGR,YAAY,QAAyC;AACnD,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,gBAAgB,IAAsB;AACpC,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,CAAC,KAAK,OAAO,QAAS;AAE1B,SAAK,qBAAqB,KAAK,IAAI;AACnC,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA,EAGA,kBAAwB;AACtB,QAAI,CAAC,KAAK,OAAO,QAAS;AAE1B,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,KAAK,qBAAqB,KAAK,OAAO,eAAgB;AAEhE,SAAK,kBAAkB;AAAA,EACzB;AAAA;AAAA,EAGA,kBAAwB;AACtB,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEQ,oBAA0B;AAChC,SAAK,qBAAqB,KAAK,IAAI;AACnC,SAAK,eAAe;AAEpB,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEQ,mBAAyB;AAC/B,SAAK,gBAAgB;AACrB,SAAK,cAAc,YAAY,MAAM;AACnC,WAAK,kBAAkB;AAAA,IACzB,GAAG,KAAK,OAAO,cAAc;AAAA,EAC/B;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,KAAK,aAAa;AACpB,oBAAc,KAAK,WAAW;AAC9B,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,qBAA2B;AACjC,QAAI,KAAK,aAAa;AACpB,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AACF;;;AClDA,SAAS,uBAAqD;AAC5D,MAAI,OAAO,eAAe,YAAa,QAAO;AAC9C,QAAM,IAAI;AACV,SAAQ,EAAE,qBAAqB,EAAE,2BAA2B;AAC9D;AAKA,IAAM,mBAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,MAAM;AACR;AAMA,SAAS,QAAQ,UAA0B;AACzC,MAAI,SAAS,SAAS,GAAG,EAAG,QAAO;AACnC,SAAO,iBAAiB,SAAS,YAAY,CAAC,KAAK;AACrD;AAQA,IAAM,uBAAuB;AAEtB,IAAM,yBAAN,MAA6B;AAAA,EAC1B,cAAgD;AAAA,EAChD,cAAc;AAAA,EACd,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,gBAAsD;AAAA,EACtD,eAAgD;AAAA,EAChD,UAA+B;AAAA,EAC/B,UAA8C;AAAA,EAC9C,UAA8C;AAAA;AAAA,EAGtD,OAAO,cAAuB;AAC5B,WAAO,qBAAqB,MAAM;AAAA,EACpC;AAAA;AAAA,EAGA,gBAAgB,IAAkC;AAChD,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA,EAGA,WAAW,IAAsB;AAC/B,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,WAAW,IAAqC;AAC9C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,WAAW,IAAqC;AAC9C,SAAK,UAAU;AAAA,EACjB;AAAA,EAEQ,IAAI,SAAuB;AACjC,SAAK,UAAU,OAAO;AACtB,YAAQ,KAAK,OAAO;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAiC;AACrC,UAAM,KAAK,qBAAqB;AAChC,QAAI,CAAC,IAAI;AACP,WAAK,IAAI,2DAA2D;AACpE,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,UAAM,QAAQ,QAAQ,QAAQ;AAC9B,SAAK,IAAI,+BAA0B,QAAQ,aAAQ,KAAK,GAAG;AAE3D,SAAK,cAAc;AACnB,SAAK,SAAS;AACd,SAAK,iBAAiB;AAEtB,UAAM,cAAc,IAAI,GAAG;AAC3B,gBAAY,aAAa;AACzB,gBAAY,iBAAiB;AAC7B,gBAAY,OAAO;AAEnB,QAAI,iBAAiB;AACrB,QAAI,gBAAgB;AAIpB,QAAI,WAAW;AACf,UAAM,kBAAkB,IAAI,QAAc,CAAC,YAAY;AACrD,kBAAY,eAAe,MAAM;AAC/B,YAAI,KAAK,gBAAgB,YAAa;AACtC,aAAK,IAAI,sDAAiD;AAC1D,YAAI,CAAC,UAAU;AACb,qBAAW;AACX,kBAAQ;AAAA,QACV;AAAA,MACF;AAEA,iBAAW,MAAM;AACf,YAAI,CAAC,UAAU;AACb,qBAAW;AACX,eAAK,IAAI,wDAAmD;AAC5D,kBAAQ;AAAA,QACV;AAAA,MACF,GAAG,GAAG;AAAA,IACR,CAAC;AAGD,SAAK,mBAAmB;AACxB,SAAK,gBAAgB,WAAW,MAAM;AACpC,UAAI,KAAK,UAAU,CAAC,KAAK,gBAAgB;AACvC,aAAK,IAAI,wDAAmD;AAC5D,aAAK;AAAA,UACH;AAAA,QAEF;AAAA,MACF;AAAA,IACF,GAAG,oBAAoB;AAEvB,gBAAY,WAAW,CAAC,MAA8B;AACpD,UAAI,KAAK,gBAAgB,YAAa;AACtC,WAAK,iBAAiB;AACtB,WAAK,mBAAmB;AAExB,UAAI,SAAS;AACb,UAAI,UAAU;AACd,eAAS,IAAI,EAAE,aAAa,IAAI,EAAE,QAAQ,QAAQ,KAAK;AACrD,cAAM,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE;AAC1B,YAAI,EAAE,QAAQ,CAAC,EAAE,SAAS;AACxB,cAAI,IAAI,gBAAgB;AACtB,sBAAU;AACV,6BAAiB;AAAA,UACnB;AAAA,QACF,OAAO;AACL,qBAAW;AAAA,QACb;AAAA,MACF;AAEA,WAAK;AAAA,QACH,kCAA6B,MAAM,gBAAgB,OAAO,oBAAoB,KAAK,WAAW;AAAA,MAChG;AAEA,UAAI,UAAU,OAAO,KAAK,MAAM,eAAe;AAC7C,wBAAgB,OAAO,KAAK;AAC5B,aAAK,cAAc,KAAK,cACpB,KAAK,cAAc,MAAM,OAAO,KAAK,IACrC,OAAO,KAAK;AAChB,aAAK,eAAe,KAAK,WAAW;AAAA,MACtC,WAAW,SAAS;AAClB,cAAM,UAAU,QAAQ,UAAU;AAClC,cAAM,OAAO,KAAK,cAAc,KAAK,cAAc,MAAM,UAAU;AACnE,aAAK,eAAe,IAAI;AAAA,MAC1B;AAAA,IACF;AAEA,gBAAY,UAAU,CAAC,MAAmC;AACxD,UAAI,KAAK,gBAAgB,YAAa;AACtC,WAAK,IAAI,wBAAmB,EAAE,KAAK,EAAE;AACrC,WAAK,UAAU,EAAE,KAAK;AAAA,IACxB;AAEA,gBAAY,QAAQ,MAAM;AACxB,UAAI,KAAK,gBAAgB,YAAa;AACtC,WAAK,IAAI,8BAAyB,KAAK,MAAM,qBAAqB,KAAK,cAAc,EAAE;AAEvF,UAAI,KAAK,QAAQ;AAEf,aAAK,UAAU;AAEf,YAAI;AACF,sBAAY,MAAM;AAClB,eAAK,IAAI,6BAA6B;AAAA,QACxC,SAAS,KAAK;AACZ,eAAK,IAAI,wBAAwB,GAAG,EAAE;AACtC,eAAK,cAAc;AACnB,eAAK,UAAU,mDAAmD;AAAA,QACpE;AAAA,MACF,OAAO;AACL,aAAK,cAAc;AAAA,MACrB;AAAA,IACF;AAEA,SAAK,cAAc;AACnB,QAAI;AACF,kBAAY,MAAM;AAClB,WAAK,IAAI,qCAAqC;AAAA,IAChD,SAAS,KAAK;AACZ,WAAK,IAAI,oCAAoC,GAAG,EAAE;AAClD,WAAK,cAAc;AACnB,WAAK,SAAS;AACd,WAAK,mBAAmB;AACxB,WAAK;AAAA,QACH,uCAAuC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACzF;AACA,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,qBAA2B;AACjC,QAAI,KAAK,eAAe;AACtB,mBAAa,KAAK,aAAa;AAC/B,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA;AAAA,EAGA,OAAe;AACb,SAAK,SAAS;AACd,SAAK,mBAAmB;AACxB,QAAI,KAAK,aAAa;AACpB,YAAM,MAAM,KAAK;AACjB,WAAK,cAAc;AACnB,UAAI,KAAK;AAAA,IACX;AACA,UAAM,SAAS,KAAK;AACpB,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,SAAS;AACd,SAAK,mBAAmB;AACxB,QAAI,KAAK,aAAa;AACpB,YAAM,MAAM,KAAK;AACjB,WAAK,cAAc;AACnB,UAAI,MAAM;AAAA,IACZ;AACA,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,UAAU;AAAA,EACjB;AACF;;;ACjTO,IAAM,YAAN,cAAwB,kBAA6B;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAqC;AAAA,EACrC;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOR,YAAY,QAAoB,WAAiB;AAC/C,UAAM;AACN,SAAK,SAAS,cAAc,MAAM;AAClC,SAAK,gBAAgB,IAAI,cAAc;AACvC,SAAK,yBAAyB,IAAI,uBAAuB,KAAK,OAAO,UAAU;AAC/E,SAAK,kBAAkB,IAAI,uBAAuB;AAClD,SAAK,YAAY;AAEjB,SAAK,QAAQ;AAAA,MACX,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,cAAc;AAAA,MACd,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAEA,SAAK,uBAAuB,gBAAgB,MAAM;AAChD,WAAK,kBAAkB;AAAA,IACzB,CAAC;AAED,SAAK,qBAAqB;AAC1B,SAAK,wBAAwB;AAAA,EAC/B;AAAA;AAAA,EAGA,MAAM,OAAsB;AAC1B,SAAK,aAAa,SAAS;AAC3B,SAAK,cAAc,MAAM,KAAK,SAAS;AAEvC,QAAI;AACF,YAAM,KAAK,cAAc,UAAU,KAAK,MAAM;AAC9C,WAAK,MAAM,gBAAgB;AAC3B,WAAK,aAAa,OAAO;AAAA,IAC3B,SAAS,KAAK;AACZ,WAAK,UAAU,qBAAqB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AACpF,WAAK,aAAa,MAAM;AACxB,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,MAAM,WAAW,SAAS;AACjC,YAAM,IAAI,MAAM,4BAA4B,KAAK,MAAM,MAAM,qBAAqB;AAAA,IACpF;AAEA,QAAI;AAKF,WAAK;AAAA,QACH,mCAA8B,KAAK,OAAO,UAAU,OAAO,YAAY,KAAK,OAAO,QAAQ;AAAA,MAC7F;AACA,UAAI,KAAK,OAAO,UAAU,SAAS;AACjC,cAAM,KAAK,gBAAgB,MAAM,KAAK,OAAO,QAAQ;AACrD,aAAK,UAAU,kEAA6D;AAAA,MAC9E;AACA,WAAK,UAAU,MAAM,aAAa;AAClC,WAAK,aAAa,WAAW;AAC7B,WAAK,uBAAuB,MAAM;AAAA,IACpC,SAAS,KAAK;AACZ,WAAK;AAAA,QACH;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAwB;AAC5B,QAAI,CAAC,KAAK,QAAS,QAAO;AAE1B,SAAK,uBAAuB,KAAK;AACjC,SAAK,gBAAgB,KAAK;AAC1B,SAAK,cAAc,OAAO;AAE1B,SAAK,aAAa,YAAY;AAE9B,QAAI;AACF,YAAM,QAAQ,MAAM,YAAY,KAAK,OAAO;AAC5C,WAAK,UAAU;AAEf,UAAI,MAAM,WAAW,GAAG;AACtB,aAAK,aAAa,OAAO;AACzB,eAAO;AAAA,MACT;AAEA,YAAM,OAAO,MAAM,KAAK,cAAc,WAAW,KAAK;AACtD,WAAK,KAAK,cAAc,IAAI;AAC5B,WAAK,aAAa,OAAO;AACzB,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK;AAAA,QACH;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACvC;AACA,WAAK,aAAa,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,uBAAuB,KAAK;AACjC,SAAK,gBAAgB,QAAQ;AAE7B,QAAI,KAAK,SAAS;AAChB,iBAAW,SAAS,KAAK,QAAQ,OAAO,UAAU,GAAG;AACnD,cAAM,KAAK;AAAA,MACb;AACA,WAAK,QAAQ,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC5C,WAAK,UAAU;AAAA,IACjB;AAEA,SAAK,cAAc,QAAQ;AAC3B,SAAK,aAAa,MAAM;AACxB,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA,EAGA,WAA+B;AAC7B,WAAO,EAAE,GAAG,KAAK,MAAM;AAAA,EACzB;AAAA;AAAA,EAGA,cAAoB;AAClB,SAAK,uBAAuB,gBAAgB;AAAA,EAC9C;AAAA,EAEA,MAAc,oBAAmC;AAC/C,QAAI,CAAC,KAAK,WAAW,CAAC,KAAK,MAAM,cAAe;AAEhD,SAAK,cAAc,OAAO;AAE1B,QAAI;AACF,YAAM,UAAU,cAAc,KAAK,OAAO;AAC1C,YAAM,WAAW,KAAK,QAAQ,SAAS;AACvC,YAAM,QAAQ,MAAM,cAAc,SAAS,QAAQ;AAEnD,UAAI,MAAM,WAAW,EAAG;AAExB,YAAM,OAAO,MAAM,KAAK,cAAc,WAAW,KAAK;AACtD,UAAI,KAAK,KAAK,KAAK,KAAK,SAAS;AAC/B,aAAK,KAAK,cAAc,IAAI;AAAA,MAC9B;AAAA,IACF,SAAS,KAAK;AACZ,WAAK;AAAA,QACH;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACvC;AAAA,IAEF;AAAA,EACF;AAAA,EAEQ,0BAAgC;AACtC,SAAK,gBAAgB,WAAW,CAAC,YAAY;AAC3C,WAAK,KAAK,SAAS,OAAO;AAAA,IAC5B,CAAC;AAED,SAAK,gBAAgB,gBAAgB,CAAC,SAAS;AAC7C,WAAK,UAAU,qCAAgC,IAAI,GAAG;AACtD,WAAK,KAAK,cAAc,IAAI;AAAA,IAC9B,CAAC;AAED,SAAK,gBAAgB,WAAW,MAAM;AACpC,WAAK,UAAU,mDAA8C;AAC7D,WAAK,uBAAuB,gBAAgB;AAAA,IAC9C,CAAC;AAED,SAAK,gBAAgB,WAAW,CAAC,YAAY;AAC3C,WAAK,UAAU,iCAA4B,OAAO,GAAG;AACrD,WAAK,UAAU,mBAAmB,OAAO;AAAA,IAC3C,CAAC;AAAA,EACH;AAAA,EAEQ,uBAA6B;AACnC,SAAK,cAAc,GAAG,YAAY,CAAC,YAAY;AAC7C,WAAK,MAAM,eAAe;AAC1B,WAAK,KAAK,UAAU,EAAE,GAAG,KAAK,MAAM,CAAC;AAAA,IACvC,CAAC;AAED,SAAK,cAAc,GAAG,SAAS,CAAC,YAAY;AAC1C,WAAK,UAAU,gBAAgB,OAAO;AAAA,IACxC,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,QAAyB;AAC5C,SAAK,MAAM,SAAS;AACpB,SAAK,MAAM,QAAQ;AACnB,SAAK,KAAK,UAAU,EAAE,GAAG,KAAK,MAAM,CAAC;AAAA,EACvC;AAAA,EAEQ,UAAU,MAAc,SAAuB;AACrD,SAAK,MAAM,QAAQ;AACnB,SAAK,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,EACtC;AAAA,EAEQ,UAAU,SAAuB;AACvC,YAAQ,KAAK,OAAO;AACpB,SAAK,KAAK,SAAS,OAAO;AAAA,EAC5B;AACF;","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. */\r\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\r\n\r\n/** Supported compute backends. */\r\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\r\n\r\n/** Engine lifecycle states. */\r\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\r\n\r\n/** Supported correction engine providers. */\r\nexport type STTCorrectionProvider = 'whisper';\r\n\r\n/** Supported real-time streaming providers. */\r\nexport type STTStreamingProvider = 'web-speech-api';\r\n\r\n/** Correction engine configuration. */\r\nexport interface STTCorrectionConfig {\r\n /** Enable mid-recording correction. Default: true */\r\n enabled?: boolean;\r\n /** Correction engine provider. Default: 'whisper' */\r\n provider?: STTCorrectionProvider;\r\n /** Silence duration (ms) before triggering correction. Default: 1000 */\r\n pauseThreshold?: number;\r\n /** Maximum interval (ms) between forced corrections. Default: 3000 */\r\n forcedInterval?: number;\r\n}\r\n\r\n/** Real-time streaming preview configuration. */\r\nexport interface STTStreamingConfig {\r\n /** Enable real-time streaming transcript. Default: true */\r\n enabled?: boolean;\r\n /** Streaming provider. Default: 'web-speech-api' */\r\n provider?: STTStreamingProvider;\r\n}\r\n\r\n/** Audio chunking configuration for long-form audio. */\r\nexport interface STTChunkingConfig {\r\n /** Chunk length in seconds for Whisper processing. Default: 30 */\r\n chunkLengthS?: number;\r\n /** Stride length in seconds for overlapping chunks. Default: 5 */\r\n strideLengthS?: number;\r\n}\r\n\r\n/** Full engine configuration. All fields optional — sensible defaults applied. */\r\nexport interface STTConfig {\r\n /** Whisper model size. Default: 'tiny' */\r\n model?: STTModelSize;\r\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\r\n backend?: STTBackend;\r\n /** Transcription language. Default: 'en' */\r\n language?: string;\r\n /** Model quantization dtype. Default: 'q4' */\r\n dtype?: string;\r\n /** Mid-recording correction settings. */\r\n correction?: STTCorrectionConfig;\r\n /** Audio chunking settings for long-form audio. */\r\n chunking?: STTChunkingConfig;\r\n /** Web Speech API streaming preview settings. */\r\n streaming?: STTStreamingConfig;\r\n}\r\n\r\n/** Resolved configuration with all defaults applied. */\r\nexport interface ResolvedSTTConfig {\r\n model: STTModelSize;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n correction: Required<STTCorrectionConfig>;\r\n chunking: Required<STTChunkingConfig>;\r\n streaming: Required<STTStreamingConfig>;\r\n}\r\n\r\n/** Engine state exposed to consumers via status events. */\r\nexport interface STTState {\r\n status: STTStatus;\r\n isModelLoaded: boolean;\r\n /** Model download progress (0–100). */\r\n loadProgress: number;\r\n /** Active compute backend, or null if not yet determined. */\r\n backend: 'webgpu' | 'wasm' | null;\r\n error: string | null;\r\n}\r\n\r\n/** Structured error emitted via the 'error' event. */\r\nexport interface STTError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\n/** Event map for the typed event emitter. */\r\nexport type STTEvents = {\r\n /** Streaming interim text during recording. */\r\n transcript: (text: string) => void;\r\n /** Whisper-corrected text replacing interim text. */\r\n correction: (text: string) => void;\r\n /** Actionable error (mic denied, model fail, transcription fail). */\r\n error: (error: STTError) => void;\r\n /** Engine state change. */\r\n status: (state: STTState) => void;\r\n /** Diagnostic log for debugging (subscribe to capture all internal events). */\r\n debug: (message: string) => void;\r\n};\r\n\r\n/** Handle returned by audio capture — used internally. */\r\nexport interface AudioCaptureHandle {\r\n audioCtx: AudioContext;\r\n stream: MediaStream;\r\n samples: Float32Array[];\r\n /** Retain reference to prevent GC from stopping audio processing. */\r\n _processor: ScriptProcessorNode;\r\n /** Source node for disconnect/reconnect on pause/resume. */\r\n _source: MediaStreamAudioSourceNode;\r\n /** Gain node (silent) to prevent mic playback. */\r\n _silencer: GainNode;\r\n}\r\n\r\n/** Message sent from main thread to Whisper worker. */\r\nexport interface WorkerMessage {\r\n type: 'load' | 'transcribe' | 'cancel';\r\n audio?: Float32Array;\r\n config?: {\r\n model: string;\r\n backend: STTBackend;\r\n language: string;\r\n dtype: string;\r\n chunkLengthS: number;\r\n strideLengthS: number;\r\n };\r\n}\r\n\r\n/** Response sent from Whisper worker to main thread. */\r\nexport interface WorkerResponse {\r\n type: 'progress' | 'ready' | 'result' | 'error';\r\n data?: unknown;\r\n}\r\n\r\n/** Default configuration values. */\r\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\r\n model: 'tiny',\r\n backend: 'auto',\r\n language: 'en',\r\n dtype: 'q4',\r\n correction: {\r\n enabled: true,\r\n provider: 'whisper',\r\n pauseThreshold: 1_000,\r\n forcedInterval: 3_000,\r\n },\r\n chunking: {\r\n chunkLengthS: 30,\r\n strideLengthS: 5,\r\n },\r\n streaming: {\r\n enabled: true,\r\n provider: 'web-speech-api',\r\n },\r\n};\r\n\r\n/** Merge user config with defaults to produce resolved config. */\r\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\r\n return {\r\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\r\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\r\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\r\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\r\n correction: {\r\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\r\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\r\n pauseThreshold:\r\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\r\n forcedInterval:\r\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\r\n },\r\n chunking: {\r\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\r\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\r\n },\r\n streaming: {\r\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\r\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\r\n },\r\n };\r\n}\r\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';\r\n\r\nconst TARGET_SAMPLE_RATE = 16_000;\r\n\r\n/**\r\n * Start capturing raw PCM audio from the microphone.\r\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\r\n */\r\nexport async function startCapture(): Promise<AudioCaptureHandle> {\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: { channelCount: 1 },\r\n });\r\n const audioCtx = new AudioContext();\r\n\r\n // Chrome may suspend AudioContext — must resume within user gesture\r\n if (audioCtx.state === 'suspended') {\r\n await audioCtx.resume();\r\n }\r\n\r\n const source = audioCtx.createMediaStreamSource(stream);\r\n const samples: Float32Array[] = [];\r\n\r\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\r\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\r\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\r\n };\r\n\r\n // Connect through a silent gain node so mic audio doesn't play back\r\n const silencer = audioCtx.createGain();\r\n silencer.gain.value = 0;\r\n source.connect(processor);\r\n processor.connect(silencer);\r\n silencer.connect(audioCtx.destination);\r\n\r\n return { audioCtx, stream, samples, _processor: processor, _source: source, _silencer: silencer };\r\n}\r\n\r\n/**\r\n * Pause capture without releasing mic or AudioContext.\r\n * Disconnects the audio source so no new samples are collected.\r\n * Returns resampled audio from the recording period.\r\n * Call resumeCapture() to start collecting again.\r\n */\r\nexport async function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n capture._source.disconnect();\r\n const currentSamples = [...capture.samples];\r\n capture.samples.length = 0;\r\n return resampleAudio(currentSamples, capture.audioCtx.sampleRate);\r\n}\r\n\r\n/**\r\n * Resume a paused capture. Reconnects the audio source to the processor.\r\n * AudioContext is resumed if suspended.\r\n */\r\nexport async function resumeCapture(capture: AudioCaptureHandle): Promise<void> {\r\n if (capture.audioCtx.state === 'suspended') {\r\n await capture.audioCtx.resume();\r\n }\r\n capture._source.connect(capture._processor);\r\n}\r\n\r\n/**\r\n * Copy current audio buffer without stopping capture.\r\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\r\n */\r\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\r\n return [...capture.samples];\r\n}\r\n\r\n/**\r\n * Concatenate sample chunks and resample to 16kHz for Whisper.\r\n */\r\nexport async function resampleAudio(\r\n samples: Float32Array[],\r\n nativeSr: number,\r\n): Promise<Float32Array> {\r\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\r\n if (totalLength === 0) return new Float32Array(0);\r\n\r\n const fullAudio = new Float32Array(totalLength);\r\n let offset = 0;\r\n for (const s of samples) {\r\n fullAudio.set(s, offset);\r\n offset += s.length;\r\n }\r\n\r\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\r\n\r\n const duration = fullAudio.length / nativeSr;\r\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\r\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\r\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\r\n buffer.getChannelData(0).set(fullAudio);\r\n const src = offline.createBufferSource();\r\n src.buffer = buffer;\r\n src.connect(offline.destination);\r\n src.start(0);\r\n const resampled = await offline.startRendering();\r\n return resampled.getChannelData(0);\r\n}\r\n\r\n/**\r\n * Stop capturing and return resampled audio at 16kHz.\r\n */\r\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\r\n const { audioCtx, stream, samples, _processor } = capture;\r\n\r\n // Disconnect processor to stop capturing\r\n try {\r\n _processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n\r\n // Stop microphone tracks\r\n for (const track of stream.getTracks()) {\r\n track.stop();\r\n }\r\n\r\n const nativeSr = audioCtx.sampleRate;\r\n await audioCtx.close();\r\n\r\n return resampleAudio(samples, nativeSr);\r\n}\r\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 currentTranscribePromise: Promise<string> | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** True while a transcription job is running in the worker. */\n get isTranscribing(): boolean {\n return this.transcribeResolve !== null;\n }\n\n /** Await the current in-flight transcription without starting a new one. */\n awaitCurrentTranscription(): Promise<string> {\n return this.currentTranscribePromise ?? Promise.resolve('');\n }\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 this.currentTranscribePromise = new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n return this.currentTranscribePromise;\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 ──────────────────────────────── */\r\n\r\ninterface SpeechRecognitionEvent {\r\n results: SpeechRecognitionResultList;\r\n resultIndex: number;\r\n}\r\n\r\ninterface SpeechRecognitionErrorEvent {\r\n error: string;\r\n}\r\n\r\ninterface SpeechRecognitionInstance {\r\n continuous: boolean;\r\n interimResults: boolean;\r\n lang: string;\r\n onaudiostart: (() => void) | null;\r\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\r\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\r\n onend: (() => void) | null;\r\n start: () => void;\r\n stop: () => void;\r\n abort: () => void;\r\n}\r\n\r\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\r\n\r\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\r\n if (typeof globalThis === 'undefined') return null;\r\n const w = globalThis as unknown as Record<string, unknown>;\r\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\r\n}\r\n\r\n/* ─── Language mapping ──────────────────────────────────── */\r\n\r\n/** Map Whisper language codes to BCP-47 locale tags for the Speech API. */\r\nconst WHISPER_TO_BCP47: Record<string, string> = {\r\n en: 'en-US',\r\n english: 'en-US',\r\n zh: 'zh-CN',\r\n chinese: 'zh-CN',\r\n de: 'de-DE',\r\n german: 'de-DE',\r\n es: 'es-ES',\r\n spanish: 'es-ES',\r\n ru: 'ru-RU',\r\n russian: 'ru-RU',\r\n ko: 'ko-KR',\r\n korean: 'ko-KR',\r\n fr: 'fr-FR',\r\n french: 'fr-FR',\r\n ja: 'ja-JP',\r\n japanese: 'ja-JP',\r\n pt: 'pt-BR',\r\n portuguese: 'pt-BR',\r\n tr: 'tr-TR',\r\n turkish: 'tr-TR',\r\n pl: 'pl-PL',\r\n polish: 'pl-PL',\r\n nl: 'nl-NL',\r\n dutch: 'nl-NL',\r\n ar: 'ar-SA',\r\n arabic: 'ar-SA',\r\n sv: 'sv-SE',\r\n swedish: 'sv-SE',\r\n it: 'it-IT',\r\n italian: 'it-IT',\r\n id: 'id-ID',\r\n indonesian: 'id-ID',\r\n hi: 'hi-IN',\r\n hindi: 'hi-IN',\r\n fi: 'fi-FI',\r\n finnish: 'fi-FI',\r\n vi: 'vi-VN',\r\n vietnamese: 'vi-VN',\r\n he: 'he-IL',\r\n hebrew: 'he-IL',\r\n uk: 'uk-UA',\r\n ukrainian: 'uk-UA',\r\n el: 'el-GR',\r\n greek: 'el-GR',\r\n ms: 'ms-MY',\r\n malay: 'ms-MY',\r\n cs: 'cs-CZ',\r\n czech: 'cs-CZ',\r\n ro: 'ro-RO',\r\n romanian: 'ro-RO',\r\n da: 'da-DK',\r\n danish: 'da-DK',\r\n hu: 'hu-HU',\r\n hungarian: 'hu-HU',\r\n no: 'nb-NO',\r\n norwegian: 'nb-NO',\r\n th: 'th-TH',\r\n thai: 'th-TH',\r\n};\r\n\r\n/**\r\n * Convert a Whisper language code to a BCP-47 locale tag for the Speech API.\r\n * Already-BCP-47 codes (containing '-') pass through unchanged.\r\n */\r\nfunction toBCP47(language: string): string {\r\n if (language.includes('-')) return language;\r\n return WHISPER_TO_BCP47[language.toLowerCase()] ?? language;\r\n}\r\n\r\n/* ─── SpeechStreamingManager ────────────────────────────── */\r\n\r\n/**\r\n * Manages Web Speech API for real-time streaming transcript preview.\r\n * Provides word-by-word interim text while Whisper handles corrections.\r\n */\r\nconst NO_RESULT_TIMEOUT_MS = 5_000;\r\n\r\nexport class SpeechStreamingManager {\r\n private recognition: SpeechRecognitionInstance | null = null;\r\n private accumulated = '';\r\n private active = false;\r\n private receivedResult = false;\r\n private noResultTimer: ReturnType<typeof setTimeout> | null = null;\r\n private onTranscript: ((text: string) => void) | null = null;\r\n private onPause: (() => void) | null = null;\r\n private onError: ((message: string) => void) | null = null;\r\n private onDebug: ((message: string) => void) | null = null;\r\n\r\n /** Check if the Web Speech API is available in this environment. */\r\n static isSupported(): boolean {\r\n return getSpeechRecognition() !== null;\r\n }\r\n\r\n /** Set callback for streaming transcript updates (interim + final text). */\r\n setOnTranscript(fn: (text: string) => void): void {\r\n this.onTranscript = fn;\r\n }\r\n\r\n /** Set callback for speech pause detection (Speech API onend). */\r\n setOnPause(fn: () => void): void {\r\n this.onPause = fn;\r\n }\r\n\r\n /** Set callback for errors. */\r\n setOnError(fn: (message: string) => void): void {\r\n this.onError = fn;\r\n }\r\n\r\n /** Set callback for diagnostic debug messages. */\r\n setOnDebug(fn: (message: string) => void): void {\r\n this.onDebug = fn;\r\n }\r\n\r\n private log(message: string): void {\r\n this.onDebug?.(message);\r\n console.warn(message);\r\n }\r\n\r\n /**\r\n * Start streaming recognition. Returns a Promise that resolves once\r\n * SpeechRecognition has claimed the microphone (onaudiostart) or after\r\n * a 300ms fallback — whichever comes first. The engine should await\r\n * this before calling getUserMedia to avoid dual-mic conflicts.\r\n *\r\n * When skipMicWait is true (warm restart — mic already active), returns\r\n * immediately after calling recognition.start() without waiting for\r\n * onaudiostart.\r\n */\r\n start(language: string, skipMicWait = false): Promise<void> {\r\n const SR = getSpeechRecognition();\r\n if (!SR) {\r\n this.log('[SSM] SpeechRecognition not available in this environment');\r\n return Promise.resolve();\r\n }\r\n\r\n const bcp47 = toBCP47(language);\r\n this.log(`[SSM] start() — lang: \"${language}\" → \"${bcp47}\"`);\r\n\r\n this.accumulated = '';\r\n this.active = true;\r\n this.receivedResult = false;\r\n\r\n const recognition = new SR();\r\n recognition.continuous = true;\r\n recognition.interimResults = true;\r\n recognition.lang = bcp47;\r\n\r\n let lastFinalIndex = -1;\r\n let lastFinalText = '';\r\n\r\n // Promise resolves when SR claims mic (onaudiostart) or after fallback.\r\n // This ensures getUserMedia doesn't compete for the mic.\r\n let micReady = false;\r\n const micClaimPromise = new Promise<void>((resolve) => {\r\n recognition.onaudiostart = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log('[SSM] onaudiostart — mic acquired by Speech API');\r\n if (!micReady) {\r\n micReady = true;\r\n resolve();\r\n }\r\n };\r\n // Fallback: resolve after 300ms even if onaudiostart never fires\r\n setTimeout(() => {\r\n if (!micReady) {\r\n micReady = true;\r\n this.log('[SSM] mic-claim fallback — proceeding after 300ms');\r\n resolve();\r\n }\r\n }, 300);\r\n });\r\n\r\n // Detect silent failure: if no onresult fires within timeout, emit error\r\n this.clearNoResultTimer();\r\n this.noResultTimer = setTimeout(() => {\r\n if (this.active && !this.receivedResult) {\r\n this.log('[SSM] no-result timeout fired — no onresult in 5s');\r\n this.onError?.(\r\n 'Speech streaming started but received no results. ' +\r\n 'Mic may be blocked by another audio capture.',\r\n );\r\n }\r\n }, NO_RESULT_TIMEOUT_MS);\r\n\r\n recognition.onresult = (e: SpeechRecognitionEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.receivedResult = true;\r\n this.clearNoResultTimer();\r\n\r\n let final_ = '';\r\n let interim = '';\r\n for (let i = e.resultIndex; i < e.results.length; i++) {\r\n const t = e.results[i][0].transcript;\r\n if (e.results[i].isFinal) {\r\n if (i > lastFinalIndex) {\r\n final_ += t;\r\n lastFinalIndex = i;\r\n }\r\n } else {\r\n interim += t;\r\n }\r\n }\r\n\r\n this.log(\r\n `[SSM] onresult — finals: \"${final_}\", interim: \"${interim}\", accumulated: \"${this.accumulated}\"`,\r\n );\r\n\r\n if (final_ && final_.trim() !== lastFinalText) {\r\n lastFinalText = final_.trim();\r\n this.accumulated = this.accumulated\r\n ? this.accumulated + ' ' + final_.trim()\r\n : final_.trim();\r\n this.onTranscript?.(this.accumulated);\r\n } else if (interim) {\r\n const trimmed = interim.trimStart();\r\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\r\n this.onTranscript?.(full);\r\n }\r\n };\r\n\r\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onerror — ${e.error}`);\r\n this.onError?.(e.error);\r\n };\r\n\r\n recognition.onend = () => {\r\n if (this.recognition !== recognition) return;\r\n this.log(`[SSM] onend — active: ${this.active}, receivedResult: ${this.receivedResult}`);\r\n\r\n if (this.active) {\r\n // Speech API paused — trigger correction\r\n this.onPause?.();\r\n // Restart for continued streaming\r\n try {\r\n recognition.start();\r\n this.log('[SSM] restarted after pause');\r\n } catch (err) {\r\n this.log(`[SSM] restart THREW: ${err}`);\r\n this.recognition = null;\r\n this.onError?.('Speech recognition failed to restart after pause.');\r\n }\r\n } else {\r\n this.recognition = null;\r\n }\r\n };\r\n\r\n this.recognition = recognition;\r\n try {\r\n recognition.start();\r\n this.log('[SSM] recognition.start() succeeded');\r\n } catch (err) {\r\n this.log(`[SSM] recognition.start() THREW: ${err}`);\r\n this.recognition = null;\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n this.onError?.(\r\n `Speech recognition failed to start: ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n return Promise.resolve(); // Resolve so engine can proceed to getUserMedia\r\n }\r\n\r\n if (skipMicWait) {\r\n this.log('[SSM] skipMicWait — warm restart, returning immediately');\r\n return Promise.resolve();\r\n }\r\n\r\n return micClaimPromise;\r\n }\r\n\r\n private clearNoResultTimer(): void {\r\n if (this.noResultTimer) {\r\n clearTimeout(this.noResultTimer);\r\n this.noResultTimer = null;\r\n }\r\n }\r\n\r\n /** Stop streaming recognition and return accumulated text. */\r\n stop(): string {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.stop();\r\n }\r\n const result = this.accumulated;\r\n this.accumulated = '';\r\n return result;\r\n }\r\n\r\n /** Abort immediately without returning text. */\r\n destroy(): void {\r\n this.active = false;\r\n this.clearNoResultTimer();\r\n if (this.recognition) {\r\n const rec = this.recognition;\r\n this.recognition = null;\r\n rec.abort();\r\n }\r\n this.accumulated = '';\r\n this.onTranscript = null;\r\n this.onPause = null;\r\n this.onError = null;\r\n this.onDebug = null;\r\n }\r\n}\r\n","import type {\r\n STTConfig,\r\n STTState,\r\n STTEvents,\r\n STTStatus,\r\n ResolvedSTTConfig,\r\n AudioCaptureHandle,\r\n} from './types.js';\r\nimport { resolveConfig } from './types.js';\r\nimport { TypedEventEmitter } from './event-emitter.js';\r\nimport { startCapture, pauseCapture, resumeCapture, snapshotAudio, resampleAudio } from './audio-capture.js';\r\nimport { WorkerManager } from './worker-manager.js';\r\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\r\nimport { SpeechStreamingManager } from './speech-streaming.js';\r\n\r\n/**\r\n * Main STT engine — the public API for speech-to-text with Whisper correction.\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const engine = new STTEngine({ model: 'tiny' });\r\n * engine.on('transcript', (text) => console.log(text));\r\n * engine.on('correction', (text) => console.log('corrected:', text));\r\n * await engine.init();\r\n * await engine.start();\r\n * const finalText = await engine.stop();\r\n * ```\r\n */\r\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\r\n private config: ResolvedSTTConfig;\r\n private workerManager: WorkerManager;\r\n private correctionOrchestrator: CorrectionOrchestrator;\r\n private speechStreaming: SpeechStreamingManager;\r\n private capture: AudioCaptureHandle | null = null;\r\n private state: STTState;\r\n private workerUrl?: URL;\r\n /** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */\r\n private _stopping = false;\r\n\r\n /**\r\n * Create a new STT engine instance.\r\n * @param config - Optional configuration overrides (model, backend, language, etc.).\r\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\r\n */\r\n constructor(config?: STTConfig, workerUrl?: URL) {\r\n super();\r\n this.config = resolveConfig(config);\r\n this.workerManager = new WorkerManager();\r\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\r\n this.speechStreaming = new SpeechStreamingManager();\r\n this.workerUrl = workerUrl;\r\n\r\n this.state = {\r\n status: 'idle',\r\n isModelLoaded: false,\r\n loadProgress: 0,\r\n backend: null,\r\n error: null,\r\n };\r\n\r\n this.correctionOrchestrator.setCorrectionFn(() => {\r\n this.performCorrection();\r\n });\r\n\r\n this.setupWorkerListeners();\r\n this.setupStreamingCallbacks();\r\n }\r\n\r\n /** Initialize the engine: spawn worker and load model. */\r\n async init(): Promise<void> {\r\n this.updateStatus('loading');\r\n this.workerManager.spawn(this.workerUrl);\r\n\r\n try {\r\n await this.workerManager.loadModel(this.config);\r\n this.state.isModelLoaded = true;\r\n this.updateStatus('ready');\r\n } catch (err) {\r\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\r\n this.updateStatus('idle');\r\n throw err;\r\n }\r\n }\r\n\r\n /** Start recording audio and enable correction cycles. */\r\n async start(): Promise<void> {\r\n if (this.state.status !== 'ready') {\r\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\r\n }\r\n\r\n try {\r\n // Check if mic is already warm from a previous recording session.\r\n // When warm, skip getUserMedia and the 300ms SR mic-claim wait.\r\n const warmCapture =\r\n this.capture &&\r\n this.capture.stream.getTracks().every((t) => t.readyState === 'live');\r\n\r\n this.emitDebug(\r\n `[STT] start() — streaming: ${this.config.streaming.enabled}, lang: \"${this.config.language}\", warm: ${!!warmCapture}`,\r\n );\r\n\r\n if (this.config.streaming.enabled) {\r\n // On warm restart, skip the mic-claim wait — no getUserMedia race.\r\n await this.speechStreaming.start(this.config.language, !!warmCapture);\r\n if (!warmCapture) {\r\n this.emitDebug('[STT] Speech API mic claim complete — starting getUserMedia');\r\n }\r\n }\r\n\r\n if (warmCapture) {\r\n await resumeCapture(this.capture!);\r\n this.emitDebug('[STT] warm mic resumed — skipped getUserMedia');\r\n } else {\r\n // First start or stale capture: full mic init with getUserMedia.\r\n this.capture = await startCapture();\r\n }\r\n\r\n this.updateStatus('recording');\r\n this.correctionOrchestrator.start();\r\n } catch (err) {\r\n this.emitError(\r\n 'MIC_DENIED',\r\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\r\n );\r\n }\r\n }\r\n\r\n /** Stop recording, run final transcription, return text.\r\n * Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */\r\n async stop(): Promise<string> {\r\n if (!this.capture) return '';\r\n\r\n // Prevent any in-flight performCorrection from emitting — stop() will own the final emit.\r\n this._stopping = true;\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.stop();\r\n\r\n this.updateStatus('processing');\r\n\r\n // If a correction job is already running, reuse it: Whisper can't be interrupted mid-inference\r\n // anyway, so cancelling just wastes the work and causes a second full run.\r\n if (this.workerManager.isTranscribing) {\r\n try {\r\n // Resample audio and await the in-flight result in parallel.\r\n const [audio, inFlightText] = await Promise.all([\r\n pauseCapture(this.capture),\r\n this.workerManager.awaitCurrentTranscription(),\r\n ]);\r\n this._stopping = false;\r\n\r\n const text = inFlightText.trim();\r\n if (text) {\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n }\r\n\r\n // In-flight returned empty (cancelled or error) — fall through to fresh transcription.\r\n if (audio.length > 0) {\r\n const freshText = await this.workerManager.transcribe(audio);\r\n this.emit('correction', freshText);\r\n this.updateStatus('ready');\r\n return freshText;\r\n }\r\n\r\n this.updateStatus('ready');\r\n return '';\r\n } catch (err) {\r\n this._stopping = false;\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n // No job in-flight — cancel is a no-op, run fresh transcription.\r\n this.workerManager.cancel();\r\n this._stopping = false;\r\n\r\n try {\r\n // Soft pause — keeps stream and AudioContext alive for fast restart.\r\n const audio = await pauseCapture(this.capture);\r\n\r\n if (audio.length === 0) {\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n this.emit('correction', text);\r\n this.updateStatus('ready');\r\n return text;\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Final transcription failed.',\r\n );\r\n this.updateStatus('ready');\r\n return '';\r\n }\r\n }\r\n\r\n /** Destroy the engine: terminate worker, release all resources. */\r\n destroy(): void {\r\n this.correctionOrchestrator.stop();\r\n this.speechStreaming.destroy();\r\n\r\n if (this.capture) {\r\n try {\r\n this.capture._processor.disconnect();\r\n } catch {\r\n /* already disconnected */\r\n }\r\n for (const track of this.capture.stream.getTracks()) {\r\n track.stop();\r\n }\r\n this.capture.audioCtx.close().catch(() => {});\r\n this.capture = null;\r\n }\r\n\r\n this.workerManager.destroy();\r\n this.updateStatus('idle');\r\n this.removeAllListeners();\r\n }\r\n\r\n /** Get current engine state. */\r\n getState(): Readonly<STTState> {\r\n return { ...this.state };\r\n }\r\n\r\n /** Notify the correction orchestrator of a speech pause. */\r\n notifyPause(): void {\r\n this.correctionOrchestrator.onPauseDetected();\r\n }\r\n\r\n private async performCorrection(): Promise<void> {\r\n if (!this.capture || !this.state.isModelLoaded) return;\r\n\r\n this.workerManager.cancel();\r\n\r\n try {\r\n const samples = snapshotAudio(this.capture);\r\n const nativeSr = this.capture.audioCtx.sampleRate;\r\n const audio = await resampleAudio(samples, nativeSr);\r\n\r\n if (audio.length === 0) return;\r\n\r\n const text = await this.workerManager.transcribe(audio);\r\n if (text.trim() && this.capture && !this._stopping) {\r\n this.emit('correction', text);\r\n }\r\n } catch (err) {\r\n this.emitError(\r\n 'TRANSCRIPTION_FAILED',\r\n err instanceof Error ? err.message : 'Correction transcription failed.',\r\n );\r\n // Recording continues — error is non-fatal\r\n }\r\n }\r\n\r\n private setupStreamingCallbacks(): void {\r\n this.speechStreaming.setOnDebug((message) => {\r\n this.emit('debug', message);\r\n });\r\n\r\n this.speechStreaming.setOnTranscript((text) => {\r\n this.emitDebug(`[STT] transcript callback — \"${text}\"`);\r\n this.emit('transcript', text);\r\n });\r\n\r\n this.speechStreaming.setOnPause(() => {\r\n this.emitDebug('[STT] pause callback — triggering correction');\r\n this.correctionOrchestrator.onPauseDetected();\r\n });\r\n\r\n this.speechStreaming.setOnError((message) => {\r\n this.emitDebug(`[STT] streaming error — \"${message}\"`);\r\n this.emitError('STREAMING_ERROR', message);\r\n });\r\n }\r\n\r\n private setupWorkerListeners(): void {\r\n this.workerManager.on('progress', (percent) => {\r\n this.state.loadProgress = percent;\r\n this.emit('status', { ...this.state });\r\n });\r\n\r\n this.workerManager.on('error', (message) => {\r\n this.emitError('WORKER_ERROR', message);\r\n });\r\n }\r\n\r\n private updateStatus(status: STTStatus): void {\r\n this.state.status = status;\r\n this.state.error = null;\r\n this.emit('status', { ...this.state });\r\n }\r\n\r\n private emitError(code: string, message: string): void {\r\n this.state.error = message;\r\n this.emit('error', { code, message });\r\n }\r\n\r\n private emitDebug(message: string): void {\r\n console.warn(message);\r\n this.emit('debug', message);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED