@tekyzinc/stt-component 0.1.0 → 0.2.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/README.md +11 -3
- package/dist/index.cjs +149 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# STT-Component
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
A framework-agnostic, browser-first speech-to-text package with real-time streaming transcription and mid-recording Whisper correction, powered by [@huggingface/transformers](https://github.com/huggingface/transformers.js).
|
|
6
6
|
|
|
@@ -83,8 +83,8 @@ Unsubscribe a specific listener.
|
|
|
83
83
|
|
|
84
84
|
| Event | Callback Signature | Description |
|
|
85
85
|
|-------|-------------------|-------------|
|
|
86
|
-
| `transcript` | `(text: string) => void` |
|
|
87
|
-
| `correction` | `(text: string) => void` | Whisper-corrected text replacing interim text |
|
|
86
|
+
| `transcript` | `(text: string) => void` | Real-time streaming text via Web Speech API (display in *italics*) |
|
|
87
|
+
| `correction` | `(text: string) => void` | Whisper-corrected text replacing interim text (display in normal style) |
|
|
88
88
|
| `error` | `(error: STTError) => void` | Actionable error (`{ code: string, message: string }`) |
|
|
89
89
|
| `status` | `(state: STTState) => void` | Engine state changes |
|
|
90
90
|
|
|
@@ -96,6 +96,7 @@ Unsubscribe a specific listener.
|
|
|
96
96
|
| `MODEL_LOAD_FAILED` | Whisper model download or initialization failed |
|
|
97
97
|
| `TRANSCRIPTION_FAILED` | Whisper inference failed (recording continues) |
|
|
98
98
|
| `WORKER_ERROR` | Web Worker encountered an error |
|
|
99
|
+
| `STREAMING_ERROR` | Web Speech API streaming error |
|
|
99
100
|
|
|
100
101
|
#### Engine States (`STTStatus`)
|
|
101
102
|
|
|
@@ -120,8 +121,11 @@ All fields are optional. Defaults shown in the table.
|
|
|
120
121
|
| `language` | `string` | `'en'` | Transcription language |
|
|
121
122
|
| `dtype` | `string` | `'q4'` | Model quantization dtype |
|
|
122
123
|
| `correction.enabled` | `boolean` | `true` | Enable mid-recording Whisper correction |
|
|
124
|
+
| `correction.provider` | `'whisper'` | `'whisper'` | Correction engine provider |
|
|
123
125
|
| `correction.pauseThreshold` | `number` (ms) | `3000` | Silence duration before triggering correction |
|
|
124
126
|
| `correction.forcedInterval` | `number` (ms) | `5000` | Maximum interval between forced corrections |
|
|
127
|
+
| `streaming.enabled` | `boolean` | `true` | Enable real-time streaming transcript via Web Speech API |
|
|
128
|
+
| `streaming.provider` | `'web-speech-api'` | `'web-speech-api'` | Streaming provider (Chrome/Edge) |
|
|
125
129
|
| `chunking.chunkLengthS` | `number` (seconds) | `30` | Chunk length for Whisper processing |
|
|
126
130
|
| `chunking.strideLengthS` | `number` (seconds) | `5` | Stride length for overlapping chunks |
|
|
127
131
|
|
|
@@ -262,6 +266,9 @@ import type {
|
|
|
262
266
|
STTModelSize,
|
|
263
267
|
STTBackend,
|
|
264
268
|
STTStatus,
|
|
269
|
+
STTCorrectionProvider,
|
|
270
|
+
STTStreamingProvider,
|
|
271
|
+
STTStreamingConfig,
|
|
265
272
|
} from '@tekyzinc/stt-component';
|
|
266
273
|
|
|
267
274
|
// Utilities (advanced usage)
|
|
@@ -271,6 +278,7 @@ import {
|
|
|
271
278
|
TypedEventEmitter,
|
|
272
279
|
WorkerManager,
|
|
273
280
|
CorrectionOrchestrator,
|
|
281
|
+
SpeechStreamingManager,
|
|
274
282
|
} from '@tekyzinc/stt-component';
|
|
275
283
|
```
|
|
276
284
|
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
CorrectionOrchestrator: () => CorrectionOrchestrator,
|
|
24
24
|
DEFAULT_STT_CONFIG: () => DEFAULT_STT_CONFIG,
|
|
25
25
|
STTEngine: () => STTEngine,
|
|
26
|
+
SpeechStreamingManager: () => SpeechStreamingManager,
|
|
26
27
|
TypedEventEmitter: () => TypedEventEmitter,
|
|
27
28
|
WorkerManager: () => WorkerManager,
|
|
28
29
|
resampleAudio: () => resampleAudio,
|
|
@@ -41,12 +42,17 @@ var DEFAULT_STT_CONFIG = {
|
|
|
41
42
|
dtype: "q4",
|
|
42
43
|
correction: {
|
|
43
44
|
enabled: true,
|
|
45
|
+
provider: "whisper",
|
|
44
46
|
pauseThreshold: 3e3,
|
|
45
47
|
forcedInterval: 5e3
|
|
46
48
|
},
|
|
47
49
|
chunking: {
|
|
48
50
|
chunkLengthS: 30,
|
|
49
51
|
strideLengthS: 5
|
|
52
|
+
},
|
|
53
|
+
streaming: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
provider: "web-speech-api"
|
|
50
56
|
}
|
|
51
57
|
};
|
|
52
58
|
function resolveConfig(config) {
|
|
@@ -57,12 +63,17 @@ function resolveConfig(config) {
|
|
|
57
63
|
dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,
|
|
58
64
|
correction: {
|
|
59
65
|
enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,
|
|
66
|
+
provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,
|
|
60
67
|
pauseThreshold: config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,
|
|
61
68
|
forcedInterval: config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval
|
|
62
69
|
},
|
|
63
70
|
chunking: {
|
|
64
71
|
chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,
|
|
65
72
|
strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS
|
|
73
|
+
},
|
|
74
|
+
streaming: {
|
|
75
|
+
enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,
|
|
76
|
+
provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider
|
|
66
77
|
}
|
|
67
78
|
};
|
|
68
79
|
}
|
|
@@ -318,11 +329,130 @@ var CorrectionOrchestrator = class {
|
|
|
318
329
|
}
|
|
319
330
|
};
|
|
320
331
|
|
|
332
|
+
// src/speech-streaming.ts
|
|
333
|
+
function getSpeechRecognition() {
|
|
334
|
+
if (typeof globalThis === "undefined") return null;
|
|
335
|
+
const w = globalThis;
|
|
336
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
337
|
+
}
|
|
338
|
+
var SpeechStreamingManager = class {
|
|
339
|
+
recognition = null;
|
|
340
|
+
accumulated = "";
|
|
341
|
+
active = false;
|
|
342
|
+
onTranscript = null;
|
|
343
|
+
onPause = null;
|
|
344
|
+
onError = null;
|
|
345
|
+
/** Check if the Web Speech API is available in this environment. */
|
|
346
|
+
static isSupported() {
|
|
347
|
+
return getSpeechRecognition() !== null;
|
|
348
|
+
}
|
|
349
|
+
/** Set callback for streaming transcript updates (interim + final text). */
|
|
350
|
+
setOnTranscript(fn) {
|
|
351
|
+
this.onTranscript = fn;
|
|
352
|
+
}
|
|
353
|
+
/** Set callback for speech pause detection (Speech API onend). */
|
|
354
|
+
setOnPause(fn) {
|
|
355
|
+
this.onPause = fn;
|
|
356
|
+
}
|
|
357
|
+
/** Set callback for errors. */
|
|
358
|
+
setOnError(fn) {
|
|
359
|
+
this.onError = fn;
|
|
360
|
+
}
|
|
361
|
+
/** Start streaming recognition. No-op if Speech API unavailable. */
|
|
362
|
+
start(language) {
|
|
363
|
+
const SR = getSpeechRecognition();
|
|
364
|
+
if (!SR) return;
|
|
365
|
+
this.accumulated = "";
|
|
366
|
+
this.active = true;
|
|
367
|
+
const recognition = new SR();
|
|
368
|
+
recognition.continuous = true;
|
|
369
|
+
recognition.interimResults = true;
|
|
370
|
+
recognition.lang = language;
|
|
371
|
+
let lastFinalIndex = -1;
|
|
372
|
+
let lastFinalText = "";
|
|
373
|
+
recognition.onresult = (e) => {
|
|
374
|
+
if (this.recognition !== recognition) return;
|
|
375
|
+
let final_ = "";
|
|
376
|
+
let interim = "";
|
|
377
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
378
|
+
const t = e.results[i][0].transcript;
|
|
379
|
+
if (e.results[i].isFinal) {
|
|
380
|
+
if (i > lastFinalIndex) {
|
|
381
|
+
final_ += t;
|
|
382
|
+
lastFinalIndex = i;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
interim += t;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (final_ && final_.trim() !== lastFinalText) {
|
|
389
|
+
lastFinalText = final_.trim();
|
|
390
|
+
this.accumulated = this.accumulated ? this.accumulated + " " + final_.trim() : final_.trim();
|
|
391
|
+
this.onTranscript?.(this.accumulated);
|
|
392
|
+
} else if (interim) {
|
|
393
|
+
const trimmed = interim.trimStart();
|
|
394
|
+
const full = this.accumulated ? this.accumulated + " " + trimmed : trimmed;
|
|
395
|
+
this.onTranscript?.(full);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
recognition.onerror = (e) => {
|
|
399
|
+
if (this.recognition !== recognition) return;
|
|
400
|
+
this.onError?.(e.error);
|
|
401
|
+
};
|
|
402
|
+
recognition.onend = () => {
|
|
403
|
+
if (this.recognition !== recognition) return;
|
|
404
|
+
if (this.active) {
|
|
405
|
+
this.onPause?.();
|
|
406
|
+
try {
|
|
407
|
+
recognition.start();
|
|
408
|
+
} catch {
|
|
409
|
+
this.recognition = null;
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
this.recognition = null;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
this.recognition = recognition;
|
|
416
|
+
try {
|
|
417
|
+
recognition.start();
|
|
418
|
+
} catch {
|
|
419
|
+
this.recognition = null;
|
|
420
|
+
this.active = false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** Stop streaming recognition and return accumulated text. */
|
|
424
|
+
stop() {
|
|
425
|
+
this.active = false;
|
|
426
|
+
if (this.recognition) {
|
|
427
|
+
const rec = this.recognition;
|
|
428
|
+
this.recognition = null;
|
|
429
|
+
rec.stop();
|
|
430
|
+
}
|
|
431
|
+
const result = this.accumulated;
|
|
432
|
+
this.accumulated = "";
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
/** Abort immediately without returning text. */
|
|
436
|
+
destroy() {
|
|
437
|
+
this.active = false;
|
|
438
|
+
if (this.recognition) {
|
|
439
|
+
const rec = this.recognition;
|
|
440
|
+
this.recognition = null;
|
|
441
|
+
rec.abort();
|
|
442
|
+
}
|
|
443
|
+
this.accumulated = "";
|
|
444
|
+
this.onTranscript = null;
|
|
445
|
+
this.onPause = null;
|
|
446
|
+
this.onError = null;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
321
450
|
// src/stt-engine.ts
|
|
322
451
|
var STTEngine = class extends TypedEventEmitter {
|
|
323
452
|
config;
|
|
324
453
|
workerManager;
|
|
325
454
|
correctionOrchestrator;
|
|
455
|
+
speechStreaming;
|
|
326
456
|
capture = null;
|
|
327
457
|
state;
|
|
328
458
|
workerUrl;
|
|
@@ -336,6 +466,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
336
466
|
this.config = resolveConfig(config);
|
|
337
467
|
this.workerManager = new WorkerManager();
|
|
338
468
|
this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);
|
|
469
|
+
this.speechStreaming = new SpeechStreamingManager();
|
|
339
470
|
this.workerUrl = workerUrl;
|
|
340
471
|
this.state = {
|
|
341
472
|
status: "idle",
|
|
@@ -348,6 +479,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
348
479
|
this.performCorrection();
|
|
349
480
|
});
|
|
350
481
|
this.setupWorkerListeners();
|
|
482
|
+
this.setupStreamingCallbacks();
|
|
351
483
|
}
|
|
352
484
|
/** Initialize the engine: spawn worker and load model. */
|
|
353
485
|
async init() {
|
|
@@ -372,6 +504,9 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
372
504
|
this.capture = await startCapture();
|
|
373
505
|
this.updateStatus("recording");
|
|
374
506
|
this.correctionOrchestrator.start();
|
|
507
|
+
if (this.config.streaming.enabled) {
|
|
508
|
+
this.speechStreaming.start(this.config.language);
|
|
509
|
+
}
|
|
375
510
|
} catch (err) {
|
|
376
511
|
this.emitError(
|
|
377
512
|
"MIC_DENIED",
|
|
@@ -383,6 +518,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
383
518
|
async stop() {
|
|
384
519
|
if (!this.capture) return "";
|
|
385
520
|
this.correctionOrchestrator.stop();
|
|
521
|
+
this.speechStreaming.stop();
|
|
386
522
|
this.workerManager.cancel();
|
|
387
523
|
this.updateStatus("processing");
|
|
388
524
|
try {
|
|
@@ -408,6 +544,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
408
544
|
/** Destroy the engine: terminate worker, release all resources. */
|
|
409
545
|
destroy() {
|
|
410
546
|
this.correctionOrchestrator.stop();
|
|
547
|
+
this.speechStreaming.destroy();
|
|
411
548
|
if (this.capture) {
|
|
412
549
|
for (const track of this.capture.stream.getTracks()) {
|
|
413
550
|
track.stop();
|
|
@@ -447,6 +584,17 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
447
584
|
);
|
|
448
585
|
}
|
|
449
586
|
}
|
|
587
|
+
setupStreamingCallbacks() {
|
|
588
|
+
this.speechStreaming.setOnTranscript((text) => {
|
|
589
|
+
this.emit("transcript", text);
|
|
590
|
+
});
|
|
591
|
+
this.speechStreaming.setOnPause(() => {
|
|
592
|
+
this.correctionOrchestrator.onPauseDetected();
|
|
593
|
+
});
|
|
594
|
+
this.speechStreaming.setOnError((message) => {
|
|
595
|
+
this.emitError("STREAMING_ERROR", message);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
450
598
|
setupWorkerListeners() {
|
|
451
599
|
this.workerManager.on("progress", (percent) => {
|
|
452
600
|
this.state.loadProgress = percent;
|
|
@@ -471,6 +619,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
471
619
|
CorrectionOrchestrator,
|
|
472
620
|
DEFAULT_STT_CONFIG,
|
|
473
621
|
STTEngine,
|
|
622
|
+
SpeechStreamingManager,
|
|
474
623
|
TypedEventEmitter,
|
|
475
624
|
WorkerManager,
|
|
476
625
|
resampleAudio,
|
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/stt-engine.ts"],"sourcesContent":["// Public types\nexport type {\n STTModelSize,\n STTBackend,\n STTStatus,\n STTCorrectionConfig,\n STTChunkingConfig,\n STTConfig,\n ResolvedSTTConfig,\n STTState,\n STTError,\n STTEvents,\n AudioCaptureHandle,\n} from './types.js';\n\n// Public values\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\n\n// Event emitter\nexport { TypedEventEmitter } from './event-emitter.js';\n\n// Audio capture\nexport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\n\n// Worker manager\nexport { WorkerManager } from './worker-manager.js';\nexport type { WorkerManagerEvents } from './worker-manager.js';\n\n// Correction orchestrator\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n// STT Engine (main public API)\nexport { STTEngine } from './stt-engine.js';\n","/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Correction timing configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording Whisper correction. Default: true */\n enabled?: boolean;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["// Public types\nexport type {\n STTModelSize,\n STTBackend,\n STTStatus,\n STTCorrectionProvider,\n STTStreamingProvider,\n STTCorrectionConfig,\n STTStreamingConfig,\n STTChunkingConfig,\n STTConfig,\n ResolvedSTTConfig,\n STTState,\n STTError,\n STTEvents,\n AudioCaptureHandle,\n} from './types.js';\n\n// Public values\nexport { DEFAULT_STT_CONFIG, resolveConfig } from './types.js';\n\n// Event emitter\nexport { TypedEventEmitter } from './event-emitter.js';\n\n// Audio capture\nexport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\n\n// Worker manager\nexport { WorkerManager } from './worker-manager.js';\nexport type { WorkerManagerEvents } from './worker-manager.js';\n\n// Correction orchestrator\nexport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n// Speech streaming\nexport { SpeechStreamingManager } from './speech-streaming.js';\n\n// STT Engine (main public API)\nexport { STTEngine } from './stt-engine.js';\n","/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Start streaming recognition. No-op if Speech API unavailable. */\n start(language: string): void {\n const SR = getSpeechRecognition();\n if (!SR) return;\n\n this.accumulated = '';\n this.active = true;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = language;\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.active = false;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n if (this.config.streaming.enabled) {\n this.speechStreaming.start(this.config.language);\n }\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnTranscript((text) => {\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -4,15 +4,28 @@ type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';
|
|
|
4
4
|
type STTBackend = 'webgpu' | 'wasm' | 'auto';
|
|
5
5
|
/** Engine lifecycle states. */
|
|
6
6
|
type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';
|
|
7
|
-
/**
|
|
7
|
+
/** Supported correction engine providers. */
|
|
8
|
+
type STTCorrectionProvider = 'whisper';
|
|
9
|
+
/** Supported real-time streaming providers. */
|
|
10
|
+
type STTStreamingProvider = 'web-speech-api';
|
|
11
|
+
/** Correction engine configuration. */
|
|
8
12
|
interface STTCorrectionConfig {
|
|
9
|
-
/** Enable mid-recording
|
|
13
|
+
/** Enable mid-recording correction. Default: true */
|
|
10
14
|
enabled?: boolean;
|
|
15
|
+
/** Correction engine provider. Default: 'whisper' */
|
|
16
|
+
provider?: STTCorrectionProvider;
|
|
11
17
|
/** Silence duration (ms) before triggering correction. Default: 3000 */
|
|
12
18
|
pauseThreshold?: number;
|
|
13
19
|
/** Maximum interval (ms) between forced corrections. Default: 5000 */
|
|
14
20
|
forcedInterval?: number;
|
|
15
21
|
}
|
|
22
|
+
/** Real-time streaming preview configuration. */
|
|
23
|
+
interface STTStreamingConfig {
|
|
24
|
+
/** Enable real-time streaming transcript. Default: true */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Streaming provider. Default: 'web-speech-api' */
|
|
27
|
+
provider?: STTStreamingProvider;
|
|
28
|
+
}
|
|
16
29
|
/** Audio chunking configuration for long-form audio. */
|
|
17
30
|
interface STTChunkingConfig {
|
|
18
31
|
/** Chunk length in seconds for Whisper processing. Default: 30 */
|
|
@@ -34,6 +47,8 @@ interface STTConfig {
|
|
|
34
47
|
correction?: STTCorrectionConfig;
|
|
35
48
|
/** Audio chunking settings for long-form audio. */
|
|
36
49
|
chunking?: STTChunkingConfig;
|
|
50
|
+
/** Web Speech API streaming preview settings. */
|
|
51
|
+
streaming?: STTStreamingConfig;
|
|
37
52
|
}
|
|
38
53
|
/** Resolved configuration with all defaults applied. */
|
|
39
54
|
interface ResolvedSTTConfig {
|
|
@@ -43,6 +58,7 @@ interface ResolvedSTTConfig {
|
|
|
43
58
|
dtype: string;
|
|
44
59
|
correction: Required<STTCorrectionConfig>;
|
|
45
60
|
chunking: Required<STTChunkingConfig>;
|
|
61
|
+
streaming: Required<STTStreamingConfig>;
|
|
46
62
|
}
|
|
47
63
|
/** Engine state exposed to consumers via status events. */
|
|
48
64
|
interface STTState {
|
|
@@ -176,6 +192,33 @@ declare class CorrectionOrchestrator {
|
|
|
176
192
|
private restartForcedTimer;
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Manages Web Speech API for real-time streaming transcript preview.
|
|
197
|
+
* Provides word-by-word interim text while Whisper handles corrections.
|
|
198
|
+
*/
|
|
199
|
+
declare class SpeechStreamingManager {
|
|
200
|
+
private recognition;
|
|
201
|
+
private accumulated;
|
|
202
|
+
private active;
|
|
203
|
+
private onTranscript;
|
|
204
|
+
private onPause;
|
|
205
|
+
private onError;
|
|
206
|
+
/** Check if the Web Speech API is available in this environment. */
|
|
207
|
+
static isSupported(): boolean;
|
|
208
|
+
/** Set callback for streaming transcript updates (interim + final text). */
|
|
209
|
+
setOnTranscript(fn: (text: string) => void): void;
|
|
210
|
+
/** Set callback for speech pause detection (Speech API onend). */
|
|
211
|
+
setOnPause(fn: () => void): void;
|
|
212
|
+
/** Set callback for errors. */
|
|
213
|
+
setOnError(fn: (message: string) => void): void;
|
|
214
|
+
/** Start streaming recognition. No-op if Speech API unavailable. */
|
|
215
|
+
start(language: string): void;
|
|
216
|
+
/** Stop streaming recognition and return accumulated text. */
|
|
217
|
+
stop(): string;
|
|
218
|
+
/** Abort immediately without returning text. */
|
|
219
|
+
destroy(): void;
|
|
220
|
+
}
|
|
221
|
+
|
|
179
222
|
/**
|
|
180
223
|
* Main STT engine — the public API for speech-to-text with Whisper correction.
|
|
181
224
|
*
|
|
@@ -193,6 +236,7 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
193
236
|
private config;
|
|
194
237
|
private workerManager;
|
|
195
238
|
private correctionOrchestrator;
|
|
239
|
+
private speechStreaming;
|
|
196
240
|
private capture;
|
|
197
241
|
private state;
|
|
198
242
|
private workerUrl?;
|
|
@@ -215,9 +259,10 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
215
259
|
/** Notify the correction orchestrator of a speech pause. */
|
|
216
260
|
notifyPause(): void;
|
|
217
261
|
private performCorrection;
|
|
262
|
+
private setupStreamingCallbacks;
|
|
218
263
|
private setupWorkerListeners;
|
|
219
264
|
private updateStatus;
|
|
220
265
|
private emitError;
|
|
221
266
|
}
|
|
222
267
|
|
|
223
|
-
export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
|
|
268
|
+
export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,15 +4,28 @@ type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';
|
|
|
4
4
|
type STTBackend = 'webgpu' | 'wasm' | 'auto';
|
|
5
5
|
/** Engine lifecycle states. */
|
|
6
6
|
type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';
|
|
7
|
-
/**
|
|
7
|
+
/** Supported correction engine providers. */
|
|
8
|
+
type STTCorrectionProvider = 'whisper';
|
|
9
|
+
/** Supported real-time streaming providers. */
|
|
10
|
+
type STTStreamingProvider = 'web-speech-api';
|
|
11
|
+
/** Correction engine configuration. */
|
|
8
12
|
interface STTCorrectionConfig {
|
|
9
|
-
/** Enable mid-recording
|
|
13
|
+
/** Enable mid-recording correction. Default: true */
|
|
10
14
|
enabled?: boolean;
|
|
15
|
+
/** Correction engine provider. Default: 'whisper' */
|
|
16
|
+
provider?: STTCorrectionProvider;
|
|
11
17
|
/** Silence duration (ms) before triggering correction. Default: 3000 */
|
|
12
18
|
pauseThreshold?: number;
|
|
13
19
|
/** Maximum interval (ms) between forced corrections. Default: 5000 */
|
|
14
20
|
forcedInterval?: number;
|
|
15
21
|
}
|
|
22
|
+
/** Real-time streaming preview configuration. */
|
|
23
|
+
interface STTStreamingConfig {
|
|
24
|
+
/** Enable real-time streaming transcript. Default: true */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Streaming provider. Default: 'web-speech-api' */
|
|
27
|
+
provider?: STTStreamingProvider;
|
|
28
|
+
}
|
|
16
29
|
/** Audio chunking configuration for long-form audio. */
|
|
17
30
|
interface STTChunkingConfig {
|
|
18
31
|
/** Chunk length in seconds for Whisper processing. Default: 30 */
|
|
@@ -34,6 +47,8 @@ interface STTConfig {
|
|
|
34
47
|
correction?: STTCorrectionConfig;
|
|
35
48
|
/** Audio chunking settings for long-form audio. */
|
|
36
49
|
chunking?: STTChunkingConfig;
|
|
50
|
+
/** Web Speech API streaming preview settings. */
|
|
51
|
+
streaming?: STTStreamingConfig;
|
|
37
52
|
}
|
|
38
53
|
/** Resolved configuration with all defaults applied. */
|
|
39
54
|
interface ResolvedSTTConfig {
|
|
@@ -43,6 +58,7 @@ interface ResolvedSTTConfig {
|
|
|
43
58
|
dtype: string;
|
|
44
59
|
correction: Required<STTCorrectionConfig>;
|
|
45
60
|
chunking: Required<STTChunkingConfig>;
|
|
61
|
+
streaming: Required<STTStreamingConfig>;
|
|
46
62
|
}
|
|
47
63
|
/** Engine state exposed to consumers via status events. */
|
|
48
64
|
interface STTState {
|
|
@@ -176,6 +192,33 @@ declare class CorrectionOrchestrator {
|
|
|
176
192
|
private restartForcedTimer;
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Manages Web Speech API for real-time streaming transcript preview.
|
|
197
|
+
* Provides word-by-word interim text while Whisper handles corrections.
|
|
198
|
+
*/
|
|
199
|
+
declare class SpeechStreamingManager {
|
|
200
|
+
private recognition;
|
|
201
|
+
private accumulated;
|
|
202
|
+
private active;
|
|
203
|
+
private onTranscript;
|
|
204
|
+
private onPause;
|
|
205
|
+
private onError;
|
|
206
|
+
/** Check if the Web Speech API is available in this environment. */
|
|
207
|
+
static isSupported(): boolean;
|
|
208
|
+
/** Set callback for streaming transcript updates (interim + final text). */
|
|
209
|
+
setOnTranscript(fn: (text: string) => void): void;
|
|
210
|
+
/** Set callback for speech pause detection (Speech API onend). */
|
|
211
|
+
setOnPause(fn: () => void): void;
|
|
212
|
+
/** Set callback for errors. */
|
|
213
|
+
setOnError(fn: (message: string) => void): void;
|
|
214
|
+
/** Start streaming recognition. No-op if Speech API unavailable. */
|
|
215
|
+
start(language: string): void;
|
|
216
|
+
/** Stop streaming recognition and return accumulated text. */
|
|
217
|
+
stop(): string;
|
|
218
|
+
/** Abort immediately without returning text. */
|
|
219
|
+
destroy(): void;
|
|
220
|
+
}
|
|
221
|
+
|
|
179
222
|
/**
|
|
180
223
|
* Main STT engine — the public API for speech-to-text with Whisper correction.
|
|
181
224
|
*
|
|
@@ -193,6 +236,7 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
193
236
|
private config;
|
|
194
237
|
private workerManager;
|
|
195
238
|
private correctionOrchestrator;
|
|
239
|
+
private speechStreaming;
|
|
196
240
|
private capture;
|
|
197
241
|
private state;
|
|
198
242
|
private workerUrl?;
|
|
@@ -215,9 +259,10 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
|
|
|
215
259
|
/** Notify the correction orchestrator of a speech pause. */
|
|
216
260
|
notifyPause(): void;
|
|
217
261
|
private performCorrection;
|
|
262
|
+
private setupStreamingCallbacks;
|
|
218
263
|
private setupWorkerListeners;
|
|
219
264
|
private updateStatus;
|
|
220
265
|
private emitError;
|
|
221
266
|
}
|
|
222
267
|
|
|
223
|
-
export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
|
|
268
|
+
export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,17 @@ var DEFAULT_STT_CONFIG = {
|
|
|
6
6
|
dtype: "q4",
|
|
7
7
|
correction: {
|
|
8
8
|
enabled: true,
|
|
9
|
+
provider: "whisper",
|
|
9
10
|
pauseThreshold: 3e3,
|
|
10
11
|
forcedInterval: 5e3
|
|
11
12
|
},
|
|
12
13
|
chunking: {
|
|
13
14
|
chunkLengthS: 30,
|
|
14
15
|
strideLengthS: 5
|
|
16
|
+
},
|
|
17
|
+
streaming: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
provider: "web-speech-api"
|
|
15
20
|
}
|
|
16
21
|
};
|
|
17
22
|
function resolveConfig(config) {
|
|
@@ -22,12 +27,17 @@ function resolveConfig(config) {
|
|
|
22
27
|
dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,
|
|
23
28
|
correction: {
|
|
24
29
|
enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,
|
|
30
|
+
provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,
|
|
25
31
|
pauseThreshold: config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,
|
|
26
32
|
forcedInterval: config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval
|
|
27
33
|
},
|
|
28
34
|
chunking: {
|
|
29
35
|
chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,
|
|
30
36
|
strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS
|
|
37
|
+
},
|
|
38
|
+
streaming: {
|
|
39
|
+
enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,
|
|
40
|
+
provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider
|
|
31
41
|
}
|
|
32
42
|
};
|
|
33
43
|
}
|
|
@@ -282,11 +292,130 @@ var CorrectionOrchestrator = class {
|
|
|
282
292
|
}
|
|
283
293
|
};
|
|
284
294
|
|
|
295
|
+
// src/speech-streaming.ts
|
|
296
|
+
function getSpeechRecognition() {
|
|
297
|
+
if (typeof globalThis === "undefined") return null;
|
|
298
|
+
const w = globalThis;
|
|
299
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
300
|
+
}
|
|
301
|
+
var SpeechStreamingManager = class {
|
|
302
|
+
recognition = null;
|
|
303
|
+
accumulated = "";
|
|
304
|
+
active = false;
|
|
305
|
+
onTranscript = null;
|
|
306
|
+
onPause = null;
|
|
307
|
+
onError = null;
|
|
308
|
+
/** Check if the Web Speech API is available in this environment. */
|
|
309
|
+
static isSupported() {
|
|
310
|
+
return getSpeechRecognition() !== null;
|
|
311
|
+
}
|
|
312
|
+
/** Set callback for streaming transcript updates (interim + final text). */
|
|
313
|
+
setOnTranscript(fn) {
|
|
314
|
+
this.onTranscript = fn;
|
|
315
|
+
}
|
|
316
|
+
/** Set callback for speech pause detection (Speech API onend). */
|
|
317
|
+
setOnPause(fn) {
|
|
318
|
+
this.onPause = fn;
|
|
319
|
+
}
|
|
320
|
+
/** Set callback for errors. */
|
|
321
|
+
setOnError(fn) {
|
|
322
|
+
this.onError = fn;
|
|
323
|
+
}
|
|
324
|
+
/** Start streaming recognition. No-op if Speech API unavailable. */
|
|
325
|
+
start(language) {
|
|
326
|
+
const SR = getSpeechRecognition();
|
|
327
|
+
if (!SR) return;
|
|
328
|
+
this.accumulated = "";
|
|
329
|
+
this.active = true;
|
|
330
|
+
const recognition = new SR();
|
|
331
|
+
recognition.continuous = true;
|
|
332
|
+
recognition.interimResults = true;
|
|
333
|
+
recognition.lang = language;
|
|
334
|
+
let lastFinalIndex = -1;
|
|
335
|
+
let lastFinalText = "";
|
|
336
|
+
recognition.onresult = (e) => {
|
|
337
|
+
if (this.recognition !== recognition) return;
|
|
338
|
+
let final_ = "";
|
|
339
|
+
let interim = "";
|
|
340
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
341
|
+
const t = e.results[i][0].transcript;
|
|
342
|
+
if (e.results[i].isFinal) {
|
|
343
|
+
if (i > lastFinalIndex) {
|
|
344
|
+
final_ += t;
|
|
345
|
+
lastFinalIndex = i;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
interim += t;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (final_ && final_.trim() !== lastFinalText) {
|
|
352
|
+
lastFinalText = final_.trim();
|
|
353
|
+
this.accumulated = this.accumulated ? this.accumulated + " " + final_.trim() : final_.trim();
|
|
354
|
+
this.onTranscript?.(this.accumulated);
|
|
355
|
+
} else if (interim) {
|
|
356
|
+
const trimmed = interim.trimStart();
|
|
357
|
+
const full = this.accumulated ? this.accumulated + " " + trimmed : trimmed;
|
|
358
|
+
this.onTranscript?.(full);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
recognition.onerror = (e) => {
|
|
362
|
+
if (this.recognition !== recognition) return;
|
|
363
|
+
this.onError?.(e.error);
|
|
364
|
+
};
|
|
365
|
+
recognition.onend = () => {
|
|
366
|
+
if (this.recognition !== recognition) return;
|
|
367
|
+
if (this.active) {
|
|
368
|
+
this.onPause?.();
|
|
369
|
+
try {
|
|
370
|
+
recognition.start();
|
|
371
|
+
} catch {
|
|
372
|
+
this.recognition = null;
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
this.recognition = null;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
this.recognition = recognition;
|
|
379
|
+
try {
|
|
380
|
+
recognition.start();
|
|
381
|
+
} catch {
|
|
382
|
+
this.recognition = null;
|
|
383
|
+
this.active = false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Stop streaming recognition and return accumulated text. */
|
|
387
|
+
stop() {
|
|
388
|
+
this.active = false;
|
|
389
|
+
if (this.recognition) {
|
|
390
|
+
const rec = this.recognition;
|
|
391
|
+
this.recognition = null;
|
|
392
|
+
rec.stop();
|
|
393
|
+
}
|
|
394
|
+
const result = this.accumulated;
|
|
395
|
+
this.accumulated = "";
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
/** Abort immediately without returning text. */
|
|
399
|
+
destroy() {
|
|
400
|
+
this.active = false;
|
|
401
|
+
if (this.recognition) {
|
|
402
|
+
const rec = this.recognition;
|
|
403
|
+
this.recognition = null;
|
|
404
|
+
rec.abort();
|
|
405
|
+
}
|
|
406
|
+
this.accumulated = "";
|
|
407
|
+
this.onTranscript = null;
|
|
408
|
+
this.onPause = null;
|
|
409
|
+
this.onError = null;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
285
413
|
// src/stt-engine.ts
|
|
286
414
|
var STTEngine = class extends TypedEventEmitter {
|
|
287
415
|
config;
|
|
288
416
|
workerManager;
|
|
289
417
|
correctionOrchestrator;
|
|
418
|
+
speechStreaming;
|
|
290
419
|
capture = null;
|
|
291
420
|
state;
|
|
292
421
|
workerUrl;
|
|
@@ -300,6 +429,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
300
429
|
this.config = resolveConfig(config);
|
|
301
430
|
this.workerManager = new WorkerManager();
|
|
302
431
|
this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);
|
|
432
|
+
this.speechStreaming = new SpeechStreamingManager();
|
|
303
433
|
this.workerUrl = workerUrl;
|
|
304
434
|
this.state = {
|
|
305
435
|
status: "idle",
|
|
@@ -312,6 +442,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
312
442
|
this.performCorrection();
|
|
313
443
|
});
|
|
314
444
|
this.setupWorkerListeners();
|
|
445
|
+
this.setupStreamingCallbacks();
|
|
315
446
|
}
|
|
316
447
|
/** Initialize the engine: spawn worker and load model. */
|
|
317
448
|
async init() {
|
|
@@ -336,6 +467,9 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
336
467
|
this.capture = await startCapture();
|
|
337
468
|
this.updateStatus("recording");
|
|
338
469
|
this.correctionOrchestrator.start();
|
|
470
|
+
if (this.config.streaming.enabled) {
|
|
471
|
+
this.speechStreaming.start(this.config.language);
|
|
472
|
+
}
|
|
339
473
|
} catch (err) {
|
|
340
474
|
this.emitError(
|
|
341
475
|
"MIC_DENIED",
|
|
@@ -347,6 +481,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
347
481
|
async stop() {
|
|
348
482
|
if (!this.capture) return "";
|
|
349
483
|
this.correctionOrchestrator.stop();
|
|
484
|
+
this.speechStreaming.stop();
|
|
350
485
|
this.workerManager.cancel();
|
|
351
486
|
this.updateStatus("processing");
|
|
352
487
|
try {
|
|
@@ -372,6 +507,7 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
372
507
|
/** Destroy the engine: terminate worker, release all resources. */
|
|
373
508
|
destroy() {
|
|
374
509
|
this.correctionOrchestrator.stop();
|
|
510
|
+
this.speechStreaming.destroy();
|
|
375
511
|
if (this.capture) {
|
|
376
512
|
for (const track of this.capture.stream.getTracks()) {
|
|
377
513
|
track.stop();
|
|
@@ -411,6 +547,17 @@ var STTEngine = class extends TypedEventEmitter {
|
|
|
411
547
|
);
|
|
412
548
|
}
|
|
413
549
|
}
|
|
550
|
+
setupStreamingCallbacks() {
|
|
551
|
+
this.speechStreaming.setOnTranscript((text) => {
|
|
552
|
+
this.emit("transcript", text);
|
|
553
|
+
});
|
|
554
|
+
this.speechStreaming.setOnPause(() => {
|
|
555
|
+
this.correctionOrchestrator.onPauseDetected();
|
|
556
|
+
});
|
|
557
|
+
this.speechStreaming.setOnError((message) => {
|
|
558
|
+
this.emitError("STREAMING_ERROR", message);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
414
561
|
setupWorkerListeners() {
|
|
415
562
|
this.workerManager.on("progress", (percent) => {
|
|
416
563
|
this.state.loadProgress = percent;
|
|
@@ -434,6 +581,7 @@ export {
|
|
|
434
581
|
CorrectionOrchestrator,
|
|
435
582
|
DEFAULT_STT_CONFIG,
|
|
436
583
|
STTEngine,
|
|
584
|
+
SpeechStreamingManager,
|
|
437
585
|
TypedEventEmitter,
|
|
438
586
|
WorkerManager,
|
|
439
587
|
resampleAudio,
|
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/stt-engine.ts"],"sourcesContent":["/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Correction timing configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording Whisper correction. Default: true */\n enabled?: boolean;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/event-emitter.ts","../src/audio-capture.ts","../src/worker-manager.ts","../src/correction-orchestrator.ts","../src/speech-streaming.ts","../src/stt-engine.ts"],"sourcesContent":["/** Supported Whisper model sizes. */\nexport type STTModelSize = 'tiny' | 'base' | 'small' | 'medium';\n\n/** Supported compute backends. */\nexport type STTBackend = 'webgpu' | 'wasm' | 'auto';\n\n/** Engine lifecycle states. */\nexport type STTStatus = 'idle' | 'loading' | 'ready' | 'recording' | 'processing';\n\n/** Supported correction engine providers. */\nexport type STTCorrectionProvider = 'whisper';\n\n/** Supported real-time streaming providers. */\nexport type STTStreamingProvider = 'web-speech-api';\n\n/** Correction engine configuration. */\nexport interface STTCorrectionConfig {\n /** Enable mid-recording correction. Default: true */\n enabled?: boolean;\n /** Correction engine provider. Default: 'whisper' */\n provider?: STTCorrectionProvider;\n /** Silence duration (ms) before triggering correction. Default: 3000 */\n pauseThreshold?: number;\n /** Maximum interval (ms) between forced corrections. Default: 5000 */\n forcedInterval?: number;\n}\n\n/** Real-time streaming preview configuration. */\nexport interface STTStreamingConfig {\n /** Enable real-time streaming transcript. Default: true */\n enabled?: boolean;\n /** Streaming provider. Default: 'web-speech-api' */\n provider?: STTStreamingProvider;\n}\n\n/** Audio chunking configuration for long-form audio. */\nexport interface STTChunkingConfig {\n /** Chunk length in seconds for Whisper processing. Default: 30 */\n chunkLengthS?: number;\n /** Stride length in seconds for overlapping chunks. Default: 5 */\n strideLengthS?: number;\n}\n\n/** Full engine configuration. All fields optional — sensible defaults applied. */\nexport interface STTConfig {\n /** Whisper model size. Default: 'tiny' */\n model?: STTModelSize;\n /** Compute backend preference. Default: 'auto' (WebGPU with WASM fallback) */\n backend?: STTBackend;\n /** Transcription language. Default: 'en' */\n language?: string;\n /** Model quantization dtype. Default: 'q4' */\n dtype?: string;\n /** Mid-recording correction settings. */\n correction?: STTCorrectionConfig;\n /** Audio chunking settings for long-form audio. */\n chunking?: STTChunkingConfig;\n /** Web Speech API streaming preview settings. */\n streaming?: STTStreamingConfig;\n}\n\n/** Resolved configuration with all defaults applied. */\nexport interface ResolvedSTTConfig {\n model: STTModelSize;\n backend: STTBackend;\n language: string;\n dtype: string;\n correction: Required<STTCorrectionConfig>;\n chunking: Required<STTChunkingConfig>;\n streaming: Required<STTStreamingConfig>;\n}\n\n/** Engine state exposed to consumers via status events. */\nexport interface STTState {\n status: STTStatus;\n isModelLoaded: boolean;\n /** Model download progress (0–100). */\n loadProgress: number;\n /** Active compute backend, or null if not yet determined. */\n backend: 'webgpu' | 'wasm' | null;\n error: string | null;\n}\n\n/** Structured error emitted via the 'error' event. */\nexport interface STTError {\n code: string;\n message: string;\n}\n\n/** Event map for the typed event emitter. */\nexport type STTEvents = {\n /** Streaming interim text during recording. */\n transcript: (text: string) => void;\n /** Whisper-corrected text replacing interim text. */\n correction: (text: string) => void;\n /** Actionable error (mic denied, model fail, transcription fail). */\n error: (error: STTError) => void;\n /** Engine state change. */\n status: (state: STTState) => void;\n};\n\n/** Handle returned by audio capture — used internally. */\nexport interface AudioCaptureHandle {\n audioCtx: AudioContext;\n stream: MediaStream;\n samples: Float32Array[];\n /** Retain reference to prevent GC from stopping audio processing. */\n _processor: ScriptProcessorNode;\n}\n\n/** Message sent from main thread to Whisper worker. */\nexport interface WorkerMessage {\n type: 'load' | 'transcribe' | 'cancel';\n audio?: Float32Array;\n config?: {\n model: string;\n backend: STTBackend;\n language: string;\n dtype: string;\n chunkLengthS: number;\n strideLengthS: number;\n };\n}\n\n/** Response sent from Whisper worker to main thread. */\nexport interface WorkerResponse {\n type: 'progress' | 'ready' | 'result' | 'error';\n data?: unknown;\n}\n\n/** Default configuration values. */\nexport const DEFAULT_STT_CONFIG: ResolvedSTTConfig = {\n model: 'tiny',\n backend: 'auto',\n language: 'en',\n dtype: 'q4',\n correction: {\n enabled: true,\n provider: 'whisper',\n pauseThreshold: 3_000,\n forcedInterval: 5_000,\n },\n chunking: {\n chunkLengthS: 30,\n strideLengthS: 5,\n },\n streaming: {\n enabled: true,\n provider: 'web-speech-api',\n },\n};\n\n/** Merge user config with defaults to produce resolved config. */\nexport function resolveConfig(config?: STTConfig): ResolvedSTTConfig {\n return {\n model: config?.model ?? DEFAULT_STT_CONFIG.model,\n backend: config?.backend ?? DEFAULT_STT_CONFIG.backend,\n language: config?.language ?? DEFAULT_STT_CONFIG.language,\n dtype: config?.dtype ?? DEFAULT_STT_CONFIG.dtype,\n correction: {\n enabled: config?.correction?.enabled ?? DEFAULT_STT_CONFIG.correction.enabled,\n provider: config?.correction?.provider ?? DEFAULT_STT_CONFIG.correction.provider,\n pauseThreshold:\n config?.correction?.pauseThreshold ?? DEFAULT_STT_CONFIG.correction.pauseThreshold,\n forcedInterval:\n config?.correction?.forcedInterval ?? DEFAULT_STT_CONFIG.correction.forcedInterval,\n },\n chunking: {\n chunkLengthS: config?.chunking?.chunkLengthS ?? DEFAULT_STT_CONFIG.chunking.chunkLengthS,\n strideLengthS: config?.chunking?.strideLengthS ?? DEFAULT_STT_CONFIG.chunking.strideLengthS,\n },\n streaming: {\n enabled: config?.streaming?.enabled ?? DEFAULT_STT_CONFIG.streaming.enabled,\n provider: config?.streaming?.provider ?? DEFAULT_STT_CONFIG.streaming.provider,\n },\n };\n}\n","/**\n * A generic, typed event emitter.\n *\n * Type parameter `T` is a map of event names to listener signatures,\n * giving consumers compile-time safety on event names and callback args.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<T extends Record<string, (...args: any[]) => void>> {\n private listeners = new Map<keyof T, Set<T[keyof T]>>();\n\n /** Subscribe to an event. */\n on<K extends keyof T>(event: K, listener: T[K]): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as T[keyof T]);\n }\n\n /** Unsubscribe a specific listener. No-op if not registered. */\n off<K extends keyof T>(event: K, listener: T[K]): void {\n this.listeners.get(event)?.delete(listener as T[keyof T]);\n }\n\n /** Emit an event, calling all registered listeners in insertion order. */\n emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) {\n (listener as (...a: Parameters<T[K]>) => void)(...args);\n }\n }\n\n /** Remove all listeners, optionally for a single event. */\n removeAllListeners(event?: keyof T): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n }\n}\n","import type { AudioCaptureHandle } from './types.js';\n\nconst TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Start capturing raw PCM audio from the microphone.\n * Uses ScriptProcessorNode to collect Float32Array samples directly.\n */\nexport async function startCapture(): Promise<AudioCaptureHandle> {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { channelCount: 1 },\n });\n const audioCtx = new AudioContext();\n\n // Chrome may suspend AudioContext — must resume within user gesture\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const source = audioCtx.createMediaStreamSource(stream);\n const samples: Float32Array[] = [];\n\n const processor = audioCtx.createScriptProcessor(4096, 1, 1);\n processor.onaudioprocess = (e: AudioProcessingEvent) => {\n samples.push(new Float32Array(e.inputBuffer.getChannelData(0)));\n };\n\n // Connect through a silent gain node so mic audio doesn't play back\n const silencer = audioCtx.createGain();\n silencer.gain.value = 0;\n source.connect(processor);\n processor.connect(silencer);\n silencer.connect(audioCtx.destination);\n\n return { audioCtx, stream, samples, _processor: processor };\n}\n\n/**\n * Copy current audio buffer without stopping capture.\n * Returns a shallow copy of the samples array (each chunk is shared, not cloned).\n */\nexport function snapshotAudio(capture: AudioCaptureHandle): Float32Array[] {\n return [...capture.samples];\n}\n\n/**\n * Concatenate sample chunks and resample to 16kHz for Whisper.\n */\nexport async function resampleAudio(\n samples: Float32Array[],\n nativeSr: number,\n): Promise<Float32Array> {\n const totalLength = samples.reduce((sum, s) => sum + s.length, 0);\n if (totalLength === 0) return new Float32Array(0);\n\n const fullAudio = new Float32Array(totalLength);\n let offset = 0;\n for (const s of samples) {\n fullAudio.set(s, offset);\n offset += s.length;\n }\n\n if (nativeSr === TARGET_SAMPLE_RATE) return fullAudio;\n\n const duration = fullAudio.length / nativeSr;\n const outLength = Math.round(duration * TARGET_SAMPLE_RATE);\n const offline = new OfflineAudioContext(1, outLength, TARGET_SAMPLE_RATE);\n const buffer = offline.createBuffer(1, fullAudio.length, nativeSr);\n buffer.getChannelData(0).set(fullAudio);\n const src = offline.createBufferSource();\n src.buffer = buffer;\n src.connect(offline.destination);\n src.start(0);\n const resampled = await offline.startRendering();\n return resampled.getChannelData(0);\n}\n\n/**\n * Stop capturing and return resampled audio at 16kHz.\n */\nexport async function stopCapture(capture: AudioCaptureHandle): Promise<Float32Array> {\n const { audioCtx, stream, samples, _processor } = capture;\n\n // Disconnect processor to stop capturing\n try {\n _processor.disconnect();\n } catch {\n /* already disconnected */\n }\n\n // Stop microphone tracks\n for (const track of stream.getTracks()) {\n track.stop();\n }\n\n const nativeSr = audioCtx.sampleRate;\n await audioCtx.close();\n\n return resampleAudio(samples, nativeSr);\n}\n","import type { ResolvedSTTConfig, WorkerResponse } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\n\n/** Events emitted by the WorkerManager. */\nexport type WorkerManagerEvents = {\n progress: (percent: number) => void;\n ready: () => void;\n result: (text: string) => void;\n error: (message: string) => void;\n};\n\n/**\n * Manages the Whisper Web Worker lifecycle.\n * Provides typed message passing and a promise-based transcription API.\n */\nexport class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {\n private worker: Worker | null = null;\n private transcribeResolve: ((text: string) => void) | null = null;\n private modelReadyResolve: (() => void) | null = null;\n private modelReadyReject: ((err: Error) => void) | null = null;\n\n /** Spawn the Web Worker. Must be called before loadModel/transcribe. */\n spawn(workerUrl?: URL): void {\n if (this.worker) return;\n\n const url = workerUrl ?? new URL('./whisper-worker.js', import.meta.url);\n\n this.worker = new Worker(url, { type: 'module' });\n this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => {\n this.handleMessage(e.data);\n };\n this.worker.onerror = (e: ErrorEvent) => {\n this.emit('error', e.message ?? 'Worker error');\n };\n }\n\n /** Load the Whisper model in the worker. Resolves when ready. */\n async loadModel(config: ResolvedSTTConfig): Promise<void> {\n if (!this.worker) throw new Error('Worker not spawned');\n\n return new Promise<void>((resolve, reject) => {\n this.modelReadyResolve = resolve;\n this.modelReadyReject = reject;\n this.worker!.postMessage({\n type: 'load',\n config: {\n model: config.model,\n backend: config.backend,\n language: config.language,\n dtype: config.dtype,\n chunkLengthS: config.chunking.chunkLengthS,\n strideLengthS: config.chunking.strideLengthS,\n },\n });\n });\n }\n\n /** Send audio to the worker for transcription. Resolves with text. */\n async transcribe(audio: Float32Array): Promise<string> {\n if (!this.worker) throw new Error('Worker not spawned');\n if (audio.length === 0) return '';\n\n return new Promise<string>((resolve) => {\n this.transcribeResolve = resolve;\n this.worker!.postMessage({ type: 'transcribe', audio }, [audio.buffer]);\n });\n }\n\n /** Cancel any in-flight transcription. */\n cancel(): void {\n this.worker?.postMessage({ type: 'cancel' });\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n }\n\n /** Terminate the worker and release resources. */\n destroy(): void {\n this.cancel();\n this.worker?.terminate();\n this.worker = null;\n this.removeAllListeners();\n }\n\n private handleMessage(msg: WorkerResponse): void {\n switch (msg.type) {\n case 'progress':\n this.emit('progress', msg.data as number);\n break;\n case 'ready':\n this.emit('ready');\n this.modelReadyResolve?.();\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n break;\n case 'result':\n this.emit('result', msg.data as string);\n this.transcribeResolve?.(msg.data as string);\n this.transcribeResolve = null;\n break;\n case 'error': {\n const errMsg = msg.data as string;\n this.emit('error', errMsg);\n // Reject model load if still pending\n if (this.modelReadyReject) {\n this.modelReadyReject(new Error(errMsg));\n this.modelReadyResolve = null;\n this.modelReadyReject = null;\n }\n // Resolve transcribe with empty string on error\n if (this.transcribeResolve) {\n this.transcribeResolve('');\n this.transcribeResolve = null;\n }\n break;\n }\n }\n }\n}\n","import type { ResolvedSTTConfig } from './types.js';\n\n/**\n * Manages mid-recording correction timing.\n * Two triggers: pause detection and forced interval.\n */\nexport class CorrectionOrchestrator {\n private forcedTimer: ReturnType<typeof setInterval> | null = null;\n private lastCorrectionTime = 0;\n private correctionFn: (() => void) | null = null;\n private config: ResolvedSTTConfig['correction'];\n\n /** Create a new correction orchestrator with the given timing config. */\n constructor(config: ResolvedSTTConfig['correction']) {\n this.config = config;\n }\n\n /** Set the function to call when a correction is triggered. */\n setCorrectionFn(fn: () => void): void {\n this.correctionFn = fn;\n }\n\n /** Start the correction orchestrator (begin forced interval timer). */\n start(): void {\n if (!this.config.enabled) return;\n\n this.lastCorrectionTime = Date.now();\n this.startForcedTimer();\n }\n\n /** Stop the orchestrator (clear all timers). */\n stop(): void {\n this.stopForcedTimer();\n }\n\n /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */\n onPauseDetected(): void {\n if (!this.config.enabled) return;\n\n const now = Date.now();\n if (now - this.lastCorrectionTime < this.config.pauseThreshold) return;\n\n this.triggerCorrection();\n }\n\n /** Force a correction now (resets timer). */\n forceCorrection(): void {\n this.triggerCorrection();\n }\n\n private triggerCorrection(): void {\n this.lastCorrectionTime = Date.now();\n this.correctionFn?.();\n // Reset forced timer after any correction\n this.restartForcedTimer();\n }\n\n private startForcedTimer(): void {\n this.stopForcedTimer();\n this.forcedTimer = setInterval(() => {\n this.triggerCorrection();\n }, this.config.forcedInterval);\n }\n\n private stopForcedTimer(): void {\n if (this.forcedTimer) {\n clearInterval(this.forcedTimer);\n this.forcedTimer = null;\n }\n }\n\n private restartForcedTimer(): void {\n if (this.forcedTimer) {\n this.startForcedTimer();\n }\n }\n}\n","/* ─── Web Speech API types ──────────────────────────────── */\n\ninterface SpeechRecognitionEvent {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionErrorEvent {\n error: string;\n}\n\ninterface SpeechRecognitionInstance {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onresult: ((e: SpeechRecognitionEvent) => void) | null;\n onerror: ((e: SpeechRecognitionErrorEvent) => void) | null;\n onend: (() => void) | null;\n start: () => void;\n stop: () => void;\n abort: () => void;\n}\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionInstance;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof globalThis === 'undefined') return null;\n const w = globalThis as unknown as Record<string, unknown>;\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as SpeechRecognitionCtor | null;\n}\n\n/**\n * Manages Web Speech API for real-time streaming transcript preview.\n * Provides word-by-word interim text while Whisper handles corrections.\n */\nexport class SpeechStreamingManager {\n private recognition: SpeechRecognitionInstance | null = null;\n private accumulated = '';\n private active = false;\n private onTranscript: ((text: string) => void) | null = null;\n private onPause: (() => void) | null = null;\n private onError: ((message: string) => void) | null = null;\n\n /** Check if the Web Speech API is available in this environment. */\n static isSupported(): boolean {\n return getSpeechRecognition() !== null;\n }\n\n /** Set callback for streaming transcript updates (interim + final text). */\n setOnTranscript(fn: (text: string) => void): void {\n this.onTranscript = fn;\n }\n\n /** Set callback for speech pause detection (Speech API onend). */\n setOnPause(fn: () => void): void {\n this.onPause = fn;\n }\n\n /** Set callback for errors. */\n setOnError(fn: (message: string) => void): void {\n this.onError = fn;\n }\n\n /** Start streaming recognition. No-op if Speech API unavailable. */\n start(language: string): void {\n const SR = getSpeechRecognition();\n if (!SR) return;\n\n this.accumulated = '';\n this.active = true;\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = language;\n\n let lastFinalIndex = -1;\n let lastFinalText = '';\n\n recognition.onresult = (e: SpeechRecognitionEvent) => {\n if (this.recognition !== recognition) return;\n\n let final_ = '';\n let interim = '';\n for (let i = e.resultIndex; i < e.results.length; i++) {\n const t = e.results[i][0].transcript;\n if (e.results[i].isFinal) {\n if (i > lastFinalIndex) {\n final_ += t;\n lastFinalIndex = i;\n }\n } else {\n interim += t;\n }\n }\n\n if (final_ && final_.trim() !== lastFinalText) {\n lastFinalText = final_.trim();\n this.accumulated = this.accumulated\n ? this.accumulated + ' ' + final_.trim()\n : final_.trim();\n this.onTranscript?.(this.accumulated);\n } else if (interim) {\n const trimmed = interim.trimStart();\n const full = this.accumulated ? this.accumulated + ' ' + trimmed : trimmed;\n this.onTranscript?.(full);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (this.recognition !== recognition) return;\n this.onError?.(e.error);\n };\n\n recognition.onend = () => {\n if (this.recognition !== recognition) return;\n\n if (this.active) {\n // Speech API paused — trigger correction\n this.onPause?.();\n // Restart for continued streaming\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n }\n } else {\n this.recognition = null;\n }\n };\n\n this.recognition = recognition;\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.active = false;\n }\n }\n\n /** Stop streaming recognition and return accumulated text. */\n stop(): string {\n this.active = false;\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.stop();\n }\n const result = this.accumulated;\n this.accumulated = '';\n return result;\n }\n\n /** Abort immediately without returning text. */\n destroy(): void {\n this.active = false;\n if (this.recognition) {\n const rec = this.recognition;\n this.recognition = null;\n rec.abort();\n }\n this.accumulated = '';\n this.onTranscript = null;\n this.onPause = null;\n this.onError = null;\n }\n}\n","import type {\n STTConfig,\n STTState,\n STTEvents,\n STTStatus,\n ResolvedSTTConfig,\n AudioCaptureHandle,\n} from './types.js';\nimport { resolveConfig } from './types.js';\nimport { TypedEventEmitter } from './event-emitter.js';\nimport { startCapture, snapshotAudio, resampleAudio, stopCapture } from './audio-capture.js';\nimport { WorkerManager } from './worker-manager.js';\nimport { CorrectionOrchestrator } from './correction-orchestrator.js';\nimport { SpeechStreamingManager } from './speech-streaming.js';\n\n/**\n * Main STT engine — the public API for speech-to-text with Whisper correction.\n *\n * Usage:\n * ```typescript\n * const engine = new STTEngine({ model: 'tiny' });\n * engine.on('transcript', (text) => console.log(text));\n * engine.on('correction', (text) => console.log('corrected:', text));\n * await engine.init();\n * await engine.start();\n * const finalText = await engine.stop();\n * ```\n */\nexport class STTEngine extends TypedEventEmitter<STTEvents> {\n private config: ResolvedSTTConfig;\n private workerManager: WorkerManager;\n private correctionOrchestrator: CorrectionOrchestrator;\n private speechStreaming: SpeechStreamingManager;\n private capture: AudioCaptureHandle | null = null;\n private state: STTState;\n private workerUrl?: URL;\n\n /**\n * Create a new STT engine instance.\n * @param config - Optional configuration overrides (model, backend, language, etc.).\n * @param workerUrl - Optional custom URL for the Whisper Web Worker script.\n */\n constructor(config?: STTConfig, workerUrl?: URL) {\n super();\n this.config = resolveConfig(config);\n this.workerManager = new WorkerManager();\n this.correctionOrchestrator = new CorrectionOrchestrator(this.config.correction);\n this.speechStreaming = new SpeechStreamingManager();\n this.workerUrl = workerUrl;\n\n this.state = {\n status: 'idle',\n isModelLoaded: false,\n loadProgress: 0,\n backend: null,\n error: null,\n };\n\n this.correctionOrchestrator.setCorrectionFn(() => {\n this.performCorrection();\n });\n\n this.setupWorkerListeners();\n this.setupStreamingCallbacks();\n }\n\n /** Initialize the engine: spawn worker and load model. */\n async init(): Promise<void> {\n this.updateStatus('loading');\n this.workerManager.spawn(this.workerUrl);\n\n try {\n await this.workerManager.loadModel(this.config);\n this.state.isModelLoaded = true;\n this.updateStatus('ready');\n } catch (err) {\n this.emitError('MODEL_LOAD_FAILED', err instanceof Error ? err.message : String(err));\n this.updateStatus('idle');\n throw err;\n }\n }\n\n /** Start recording audio and enable correction cycles. */\n async start(): Promise<void> {\n if (this.state.status !== 'ready') {\n throw new Error(`Cannot start: engine is \"${this.state.status}\", expected \"ready\"`);\n }\n\n try {\n this.capture = await startCapture();\n this.updateStatus('recording');\n this.correctionOrchestrator.start();\n if (this.config.streaming.enabled) {\n this.speechStreaming.start(this.config.language);\n }\n } catch (err) {\n this.emitError(\n 'MIC_DENIED',\n err instanceof Error ? err.message : 'Microphone access denied. Check browser permissions.',\n );\n }\n }\n\n /** Stop recording, run final transcription, return text. */\n async stop(): Promise<string> {\n if (!this.capture) return '';\n\n this.correctionOrchestrator.stop();\n this.speechStreaming.stop();\n this.workerManager.cancel();\n\n this.updateStatus('processing');\n\n try {\n const audio = await stopCapture(this.capture);\n this.capture = null;\n\n if (audio.length === 0) {\n this.updateStatus('ready');\n return '';\n }\n\n const text = await this.workerManager.transcribe(audio);\n this.emit('correction', text);\n this.updateStatus('ready');\n return text;\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Final transcription failed.',\n );\n this.updateStatus('ready');\n return '';\n }\n }\n\n /** Destroy the engine: terminate worker, release all resources. */\n destroy(): void {\n this.correctionOrchestrator.stop();\n this.speechStreaming.destroy();\n\n if (this.capture) {\n for (const track of this.capture.stream.getTracks()) {\n track.stop();\n }\n this.capture.audioCtx.close().catch(() => {});\n this.capture = null;\n }\n\n this.workerManager.destroy();\n this.updateStatus('idle');\n this.removeAllListeners();\n }\n\n /** Get current engine state. */\n getState(): Readonly<STTState> {\n return { ...this.state };\n }\n\n /** Notify the correction orchestrator of a speech pause. */\n notifyPause(): void {\n this.correctionOrchestrator.onPauseDetected();\n }\n\n private async performCorrection(): Promise<void> {\n if (!this.capture || !this.state.isModelLoaded) return;\n\n this.workerManager.cancel();\n\n try {\n const samples = snapshotAudio(this.capture);\n const nativeSr = this.capture.audioCtx.sampleRate;\n const audio = await resampleAudio(samples, nativeSr);\n\n if (audio.length === 0) return;\n\n const text = await this.workerManager.transcribe(audio);\n if (text.trim() && this.capture) {\n this.emit('correction', text);\n }\n } catch (err) {\n this.emitError(\n 'TRANSCRIPTION_FAILED',\n err instanceof Error ? err.message : 'Correction transcription failed.',\n );\n // Recording continues — error is non-fatal\n }\n }\n\n private setupStreamingCallbacks(): void {\n this.speechStreaming.setOnTranscript((text) => {\n this.emit('transcript', text);\n });\n\n this.speechStreaming.setOnPause(() => {\n this.correctionOrchestrator.onPauseDetected();\n });\n\n this.speechStreaming.setOnError((message) => {\n this.emitError('STREAMING_ERROR', message);\n });\n }\n\n private setupWorkerListeners(): void {\n this.workerManager.on('progress', (percent) => {\n this.state.loadProgress = percent;\n this.emit('status', { ...this.state });\n });\n\n this.workerManager.on('error', (message) => {\n this.emitError('WORKER_ERROR', message);\n });\n }\n\n private updateStatus(status: STTStatus): void {\n this.state.status = status;\n this.state.error = null;\n this.emit('status', { ...this.state });\n }\n\n private emitError(code: string, message: string): void {\n this.state.error = message;\n this.emit('error', { code, message });\n }\n}\n"],"mappings":";AAmIO,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;;;ACzKO,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;;;ACnDA,SAAS,uBAAqD;AAC5D,MAAI,OAAO,eAAe,YAAa,QAAO;AAC9C,QAAM,IAAI;AACV,SAAQ,EAAE,qBAAqB,EAAE,2BAA2B;AAC9D;AAMO,IAAM,yBAAN,MAA6B;AAAA,EAC1B,cAAgD;AAAA,EAChD,cAAc;AAAA,EACd,SAAS;AAAA,EACT,eAAgD;AAAA,EAChD,UAA+B;AAAA,EAC/B,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,MAAM,UAAwB;AAC5B,UAAM,KAAK,qBAAqB;AAChC,QAAI,CAAC,GAAI;AAET,SAAK,cAAc;AACnB,SAAK,SAAS;AAEd,UAAM,cAAc,IAAI,GAAG;AAC3B,gBAAY,aAAa;AACzB,gBAAY,iBAAiB;AAC7B,gBAAY,OAAO;AAEnB,QAAI,iBAAiB;AACrB,QAAI,gBAAgB;AAEpB,gBAAY,WAAW,CAAC,MAA8B;AACpD,UAAI,KAAK,gBAAgB,YAAa;AAEtC,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,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,UAAU,EAAE,KAAK;AAAA,IACxB;AAEA,gBAAY,QAAQ,MAAM;AACxB,UAAI,KAAK,gBAAgB,YAAa;AAEtC,UAAI,KAAK,QAAQ;AAEf,aAAK,UAAU;AAEf,YAAI;AACF,sBAAY,MAAM;AAAA,QACpB,QAAQ;AACN,eAAK,cAAc;AAAA,QACrB;AAAA,MACF,OAAO;AACL,aAAK,cAAc;AAAA,MACrB;AAAA,IACF;AAEA,SAAK,cAAc;AACnB,QAAI;AACF,kBAAY,MAAM;AAAA,IACpB,QAAQ;AACN,WAAK,cAAc;AACnB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,OAAe;AACb,SAAK,SAAS;AACd,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,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;AAAA,EACjB;AACF;;;AC1IO,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;AACF,WAAK,UAAU,MAAM,aAAa;AAClC,WAAK,aAAa,WAAW;AAC7B,WAAK,uBAAuB,MAAM;AAClC,UAAI,KAAK,OAAO,UAAU,SAAS;AACjC,aAAK,gBAAgB,MAAM,KAAK,OAAO,QAAQ;AAAA,MACjD;AAAA,IACF,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,gBAAgB,CAAC,SAAS;AAC7C,WAAK,KAAK,cAAc,IAAI;AAAA,IAC9B,CAAC;AAED,SAAK,gBAAgB,WAAW,MAAM;AACpC,WAAK,uBAAuB,gBAAgB;AAAA,IAC9C,CAAC;AAED,SAAK,gBAAgB,WAAW,CAAC,YAAY;AAC3C,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;AACF;","names":[]}
|
package/package.json
CHANGED