@tekyzinc/stt-component 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -14,9 +14,9 @@ interface STTCorrectionConfig {
14
14
  enabled?: boolean;
15
15
  /** Correction engine provider. Default: 'whisper' */
16
16
  provider?: STTCorrectionProvider;
17
- /** Silence duration (ms) before triggering correction. Default: 3000 */
17
+ /** Silence duration (ms) before triggering correction. Default: 1000 */
18
18
  pauseThreshold?: number;
19
- /** Maximum interval (ms) between forced corrections. Default: 5000 */
19
+ /** Maximum interval (ms) between forced corrections. Default: 3000 */
20
20
  forcedInterval?: number;
21
21
  }
22
22
  /** Real-time streaming preview configuration. */
@@ -95,6 +95,10 @@ interface AudioCaptureHandle {
95
95
  samples: Float32Array[];
96
96
  /** Retain reference to prevent GC from stopping audio processing. */
97
97
  _processor: ScriptProcessorNode;
98
+ /** Source node for disconnect/reconnect on pause/resume. */
99
+ _source: MediaStreamAudioSourceNode;
100
+ /** Gain node (silent) to prevent mic playback. */
101
+ _silencer: GainNode;
98
102
  }
99
103
  /** Default configuration values. */
100
104
  declare const DEFAULT_STT_CONFIG: ResolvedSTTConfig;
@@ -124,6 +128,18 @@ declare class TypedEventEmitter<T extends Record<string, (...args: any[]) => voi
124
128
  * Uses ScriptProcessorNode to collect Float32Array samples directly.
125
129
  */
126
130
  declare function startCapture(): Promise<AudioCaptureHandle>;
131
+ /**
132
+ * Pause capture without releasing mic or AudioContext.
133
+ * Disconnects the audio source so no new samples are collected.
134
+ * Returns resampled audio from the recording period.
135
+ * Call resumeCapture() to start collecting again.
136
+ */
137
+ declare function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array>;
138
+ /**
139
+ * Resume a paused capture. Reconnects the audio source to the processor.
140
+ * AudioContext is resumed if suspended.
141
+ */
142
+ declare function resumeCapture(capture: AudioCaptureHandle): Promise<void>;
127
143
  /**
128
144
  * Copy current audio buffer without stopping capture.
129
145
  * Returns a shallow copy of the samples array (each chunk is shared, not cloned).
@@ -152,8 +168,13 @@ type WorkerManagerEvents = {
152
168
  declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
153
169
  private worker;
154
170
  private transcribeResolve;
171
+ private currentTranscribePromise;
155
172
  private modelReadyResolve;
156
173
  private modelReadyReject;
174
+ /** True while a transcription job is running in the worker. */
175
+ get isTranscribing(): boolean;
176
+ /** Await the current in-flight transcription without starting a new one. */
177
+ awaitCurrentTranscription(): Promise<string>;
157
178
  /** Spawn the Web Worker. Must be called before loadModel/transcribe. */
158
179
  spawn(workerUrl?: URL): void;
159
180
  /** Load the Whisper model in the worker. Resolves when ready. */
@@ -167,12 +188,9 @@ declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
167
188
  private handleMessage;
168
189
  }
169
190
 
170
- /**
171
- * Manages mid-recording correction timing.
172
- * Two triggers: pause detection and forced interval.
173
- */
174
191
  declare class CorrectionOrchestrator {
175
192
  private forcedTimer;
193
+ private initialTimer;
176
194
  private lastCorrectionTime;
177
195
  private correctionFn;
178
196
  private config;
@@ -180,7 +198,9 @@ declare class CorrectionOrchestrator {
180
198
  constructor(config: ResolvedSTTConfig['correction']);
181
199
  /** Set the function to call when a correction is triggered. */
182
200
  setCorrectionFn(fn: () => void): void;
183
- /** Start the correction orchestrator (begin forced interval timer). */
201
+ /** Start the correction orchestrator.
202
+ * Fires a quick initial correction after 1s for early feedback, then
203
+ * switches to the regular forcedInterval cadence from that point. */
184
204
  start(): void;
185
205
  /** Stop the orchestrator (clear all timers). */
186
206
  stop(): void;
@@ -220,8 +240,12 @@ declare class SpeechStreamingManager {
220
240
  * SpeechRecognition has claimed the microphone (onaudiostart) or after
221
241
  * a 300ms fallback — whichever comes first. The engine should await
222
242
  * this before calling getUserMedia to avoid dual-mic conflicts.
243
+ *
244
+ * When skipMicWait is true (warm restart — mic already active), returns
245
+ * immediately after calling recognition.start() without waiting for
246
+ * onaudiostart.
223
247
  */
224
- start(language: string): Promise<void>;
248
+ start(language: string, skipMicWait?: boolean): Promise<void>;
225
249
  private clearNoResultTimer;
226
250
  /** Stop streaming recognition and return accumulated text. */
227
251
  stop(): string;
@@ -250,6 +274,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
250
274
  private capture;
251
275
  private state;
252
276
  private workerUrl?;
277
+ /** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
278
+ private _stopping;
253
279
  /**
254
280
  * Create a new STT engine instance.
255
281
  * @param config - Optional configuration overrides (model, backend, language, etc.).
@@ -260,7 +286,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
260
286
  init(): Promise<void>;
261
287
  /** Start recording audio and enable correction cycles. */
262
288
  start(): Promise<void>;
263
- /** Stop recording, run final transcription, return text. */
289
+ /** Stop recording, run final transcription, return text.
290
+ * Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
264
291
  stop(): Promise<string>;
265
292
  /** Destroy the engine: terminate worker, release all resources. */
266
293
  destroy(): void;
@@ -276,4 +303,4 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
276
303
  private emitDebug;
277
304
  }
278
305
 
279
- export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
306
+ export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, pauseCapture, resampleAudio, resolveConfig, resumeCapture, snapshotAudio, startCapture, stopCapture };
package/dist/index.d.ts CHANGED
@@ -14,9 +14,9 @@ interface STTCorrectionConfig {
14
14
  enabled?: boolean;
15
15
  /** Correction engine provider. Default: 'whisper' */
16
16
  provider?: STTCorrectionProvider;
17
- /** Silence duration (ms) before triggering correction. Default: 3000 */
17
+ /** Silence duration (ms) before triggering correction. Default: 1000 */
18
18
  pauseThreshold?: number;
19
- /** Maximum interval (ms) between forced corrections. Default: 5000 */
19
+ /** Maximum interval (ms) between forced corrections. Default: 3000 */
20
20
  forcedInterval?: number;
21
21
  }
22
22
  /** Real-time streaming preview configuration. */
@@ -95,6 +95,10 @@ interface AudioCaptureHandle {
95
95
  samples: Float32Array[];
96
96
  /** Retain reference to prevent GC from stopping audio processing. */
97
97
  _processor: ScriptProcessorNode;
98
+ /** Source node for disconnect/reconnect on pause/resume. */
99
+ _source: MediaStreamAudioSourceNode;
100
+ /** Gain node (silent) to prevent mic playback. */
101
+ _silencer: GainNode;
98
102
  }
99
103
  /** Default configuration values. */
100
104
  declare const DEFAULT_STT_CONFIG: ResolvedSTTConfig;
@@ -124,6 +128,18 @@ declare class TypedEventEmitter<T extends Record<string, (...args: any[]) => voi
124
128
  * Uses ScriptProcessorNode to collect Float32Array samples directly.
125
129
  */
126
130
  declare function startCapture(): Promise<AudioCaptureHandle>;
131
+ /**
132
+ * Pause capture without releasing mic or AudioContext.
133
+ * Disconnects the audio source so no new samples are collected.
134
+ * Returns resampled audio from the recording period.
135
+ * Call resumeCapture() to start collecting again.
136
+ */
137
+ declare function pauseCapture(capture: AudioCaptureHandle): Promise<Float32Array>;
138
+ /**
139
+ * Resume a paused capture. Reconnects the audio source to the processor.
140
+ * AudioContext is resumed if suspended.
141
+ */
142
+ declare function resumeCapture(capture: AudioCaptureHandle): Promise<void>;
127
143
  /**
128
144
  * Copy current audio buffer without stopping capture.
129
145
  * Returns a shallow copy of the samples array (each chunk is shared, not cloned).
@@ -152,8 +168,13 @@ type WorkerManagerEvents = {
152
168
  declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
153
169
  private worker;
154
170
  private transcribeResolve;
171
+ private currentTranscribePromise;
155
172
  private modelReadyResolve;
156
173
  private modelReadyReject;
174
+ /** True while a transcription job is running in the worker. */
175
+ get isTranscribing(): boolean;
176
+ /** Await the current in-flight transcription without starting a new one. */
177
+ awaitCurrentTranscription(): Promise<string>;
157
178
  /** Spawn the Web Worker. Must be called before loadModel/transcribe. */
158
179
  spawn(workerUrl?: URL): void;
159
180
  /** Load the Whisper model in the worker. Resolves when ready. */
@@ -167,12 +188,9 @@ declare class WorkerManager extends TypedEventEmitter<WorkerManagerEvents> {
167
188
  private handleMessage;
168
189
  }
169
190
 
170
- /**
171
- * Manages mid-recording correction timing.
172
- * Two triggers: pause detection and forced interval.
173
- */
174
191
  declare class CorrectionOrchestrator {
175
192
  private forcedTimer;
193
+ private initialTimer;
176
194
  private lastCorrectionTime;
177
195
  private correctionFn;
178
196
  private config;
@@ -180,7 +198,9 @@ declare class CorrectionOrchestrator {
180
198
  constructor(config: ResolvedSTTConfig['correction']);
181
199
  /** Set the function to call when a correction is triggered. */
182
200
  setCorrectionFn(fn: () => void): void;
183
- /** Start the correction orchestrator (begin forced interval timer). */
201
+ /** Start the correction orchestrator.
202
+ * Fires a quick initial correction after 1s for early feedback, then
203
+ * switches to the regular forcedInterval cadence from that point. */
184
204
  start(): void;
185
205
  /** Stop the orchestrator (clear all timers). */
186
206
  stop(): void;
@@ -220,8 +240,12 @@ declare class SpeechStreamingManager {
220
240
  * SpeechRecognition has claimed the microphone (onaudiostart) or after
221
241
  * a 300ms fallback — whichever comes first. The engine should await
222
242
  * this before calling getUserMedia to avoid dual-mic conflicts.
243
+ *
244
+ * When skipMicWait is true (warm restart — mic already active), returns
245
+ * immediately after calling recognition.start() without waiting for
246
+ * onaudiostart.
223
247
  */
224
- start(language: string): Promise<void>;
248
+ start(language: string, skipMicWait?: boolean): Promise<void>;
225
249
  private clearNoResultTimer;
226
250
  /** Stop streaming recognition and return accumulated text. */
227
251
  stop(): string;
@@ -250,6 +274,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
250
274
  private capture;
251
275
  private state;
252
276
  private workerUrl?;
277
+ /** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
278
+ private _stopping;
253
279
  /**
254
280
  * Create a new STT engine instance.
255
281
  * @param config - Optional configuration overrides (model, backend, language, etc.).
@@ -260,7 +286,8 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
260
286
  init(): Promise<void>;
261
287
  /** Start recording audio and enable correction cycles. */
262
288
  start(): Promise<void>;
263
- /** Stop recording, run final transcription, return text. */
289
+ /** Stop recording, run final transcription, return text.
290
+ * Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
264
291
  stop(): Promise<string>;
265
292
  /** Destroy the engine: terminate worker, release all resources. */
266
293
  destroy(): void;
@@ -276,4 +303,4 @@ declare class STTEngine extends TypedEventEmitter<STTEvents> {
276
303
  private emitDebug;
277
304
  }
278
305
 
279
- export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, resampleAudio, resolveConfig, snapshotAudio, startCapture, stopCapture };
306
+ export { type AudioCaptureHandle, CorrectionOrchestrator, DEFAULT_STT_CONFIG, type ResolvedSTTConfig, type STTBackend, type STTChunkingConfig, type STTConfig, type STTCorrectionConfig, type STTCorrectionProvider, STTEngine, type STTError, type STTEvents, type STTModelSize, type STTState, type STTStatus, type STTStreamingConfig, type STTStreamingProvider, SpeechStreamingManager, TypedEventEmitter, WorkerManager, type WorkerManagerEvents, pauseCapture, resampleAudio, resolveConfig, resumeCapture, snapshotAudio, startCapture, stopCapture };
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ var DEFAULT_STT_CONFIG = {
7
7
  correction: {
8
8
  enabled: true,
9
9
  provider: "whisper",
10
- pauseThreshold: 3e3,
11
- forcedInterval: 5e3
10
+ pauseThreshold: 1e3,
11
+ forcedInterval: 3e3
12
12
  },
13
13
  chunking: {
14
14
  chunkLengthS: 30,
@@ -97,7 +97,19 @@ async function startCapture() {
97
97
  source.connect(processor);
98
98
  processor.connect(silencer);
99
99
  silencer.connect(audioCtx.destination);
100
- return { audioCtx, stream, samples, _processor: processor };
100
+ return { audioCtx, stream, samples, _processor: processor, _source: source, _silencer: silencer };
101
+ }
102
+ async function pauseCapture(capture) {
103
+ capture._source.disconnect();
104
+ const currentSamples = [...capture.samples];
105
+ capture.samples.length = 0;
106
+ return resampleAudio(currentSamples, capture.audioCtx.sampleRate);
107
+ }
108
+ async function resumeCapture(capture) {
109
+ if (capture.audioCtx.state === "suspended") {
110
+ await capture.audioCtx.resume();
111
+ }
112
+ capture._source.connect(capture._processor);
101
113
  }
102
114
  function snapshotAudio(capture) {
103
115
  return [...capture.samples];
@@ -142,8 +154,17 @@ async function stopCapture(capture) {
142
154
  var WorkerManager = class extends TypedEventEmitter {
143
155
  worker = null;
144
156
  transcribeResolve = null;
157
+ currentTranscribePromise = null;
145
158
  modelReadyResolve = null;
146
159
  modelReadyReject = null;
160
+ /** True while a transcription job is running in the worker. */
161
+ get isTranscribing() {
162
+ return this.transcribeResolve !== null;
163
+ }
164
+ /** Await the current in-flight transcription without starting a new one. */
165
+ awaitCurrentTranscription() {
166
+ return this.currentTranscribePromise ?? Promise.resolve("");
167
+ }
147
168
  /** Spawn the Web Worker. Must be called before loadModel/transcribe. */
148
169
  spawn(workerUrl) {
149
170
  if (this.worker) return;
@@ -179,10 +200,11 @@ var WorkerManager = class extends TypedEventEmitter {
179
200
  async transcribe(audio) {
180
201
  if (!this.worker) throw new Error("Worker not spawned");
181
202
  if (audio.length === 0) return "";
182
- return new Promise((resolve) => {
203
+ this.currentTranscribePromise = new Promise((resolve) => {
183
204
  this.transcribeResolve = resolve;
184
205
  this.worker.postMessage({ type: "transcribe", audio }, [audio.buffer]);
185
206
  });
207
+ return this.currentTranscribePromise;
186
208
  }
187
209
  /** Cancel any in-flight transcription. */
188
210
  cancel() {
@@ -234,8 +256,10 @@ var WorkerManager = class extends TypedEventEmitter {
234
256
  };
235
257
 
236
258
  // src/correction-orchestrator.ts
259
+ var INITIAL_CORRECTION_DELAY_MS = 1e3;
237
260
  var CorrectionOrchestrator = class {
238
261
  forcedTimer = null;
262
+ initialTimer = null;
239
263
  lastCorrectionTime = 0;
240
264
  correctionFn = null;
241
265
  config;
@@ -247,14 +271,25 @@ var CorrectionOrchestrator = class {
247
271
  setCorrectionFn(fn) {
248
272
  this.correctionFn = fn;
249
273
  }
250
- /** Start the correction orchestrator (begin forced interval timer). */
274
+ /** Start the correction orchestrator.
275
+ * Fires a quick initial correction after 1s for early feedback, then
276
+ * switches to the regular forcedInterval cadence from that point. */
251
277
  start() {
252
278
  if (!this.config.enabled) return;
253
279
  this.lastCorrectionTime = Date.now();
254
- this.startForcedTimer();
280
+ this.initialTimer = setTimeout(() => {
281
+ this.initialTimer = null;
282
+ this.correctionFn?.();
283
+ this.lastCorrectionTime = Date.now();
284
+ this.startForcedTimer();
285
+ }, INITIAL_CORRECTION_DELAY_MS);
255
286
  }
256
287
  /** Stop the orchestrator (clear all timers). */
257
288
  stop() {
289
+ if (this.initialTimer) {
290
+ clearTimeout(this.initialTimer);
291
+ this.initialTimer = null;
292
+ }
258
293
  this.stopForcedTimer();
259
294
  }
260
295
  /** Called when a speech pause is detected. Triggers correction if cooldown elapsed. */
@@ -402,8 +437,12 @@ var SpeechStreamingManager = class {
402
437
  * SpeechRecognition has claimed the microphone (onaudiostart) or after
403
438
  * a 300ms fallback — whichever comes first. The engine should await
404
439
  * this before calling getUserMedia to avoid dual-mic conflicts.
440
+ *
441
+ * When skipMicWait is true (warm restart — mic already active), returns
442
+ * immediately after calling recognition.start() without waiting for
443
+ * onaudiostart.
405
444
  */
406
- start(language) {
445
+ start(language, skipMicWait = false) {
407
446
  const SR = getSpeechRecognition();
408
447
  if (!SR) {
409
448
  this.log("[SSM] SpeechRecognition not available in this environment");
@@ -513,6 +552,10 @@ var SpeechStreamingManager = class {
513
552
  );
514
553
  return Promise.resolve();
515
554
  }
555
+ if (skipMicWait) {
556
+ this.log("[SSM] skipMicWait \u2014 warm restart, returning immediately");
557
+ return Promise.resolve();
558
+ }
516
559
  return micClaimPromise;
517
560
  }
518
561
  clearNoResultTimer() {
@@ -560,6 +603,8 @@ var STTEngine = class extends TypedEventEmitter {
560
603
  capture = null;
561
604
  state;
562
605
  workerUrl;
606
+ /** Prevents performCorrection from emitting while stop() is consuming the in-flight result. */
607
+ _stopping = false;
563
608
  /**
564
609
  * Create a new STT engine instance.
565
610
  * @param config - Optional configuration overrides (model, backend, language, etc.).
@@ -605,14 +650,22 @@ var STTEngine = class extends TypedEventEmitter {
605
650
  throw new Error(`Cannot start: engine is "${this.state.status}", expected "ready"`);
606
651
  }
607
652
  try {
653
+ const warmCapture = this.capture && this.capture.stream.getTracks().every((t) => t.readyState === "live");
608
654
  this.emitDebug(
609
- `[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}"`
655
+ `[STT] start() \u2014 streaming: ${this.config.streaming.enabled}, lang: "${this.config.language}", warm: ${!!warmCapture}`
610
656
  );
611
657
  if (this.config.streaming.enabled) {
612
- await this.speechStreaming.start(this.config.language);
613
- this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
658
+ await this.speechStreaming.start(this.config.language, !!warmCapture);
659
+ if (!warmCapture) {
660
+ this.emitDebug("[STT] Speech API mic claim complete \u2014 starting getUserMedia");
661
+ }
662
+ }
663
+ if (warmCapture) {
664
+ await resumeCapture(this.capture);
665
+ this.emitDebug("[STT] warm mic resumed \u2014 skipped getUserMedia");
666
+ } else {
667
+ this.capture = await startCapture();
614
668
  }
615
- this.capture = await startCapture();
616
669
  this.updateStatus("recording");
617
670
  this.correctionOrchestrator.start();
618
671
  } catch (err) {
@@ -622,16 +675,49 @@ var STTEngine = class extends TypedEventEmitter {
622
675
  );
623
676
  }
624
677
  }
625
- /** Stop recording, run final transcription, return text. */
678
+ /** Stop recording, run final transcription, return text.
679
+ * Mic and AudioContext stay alive for fast restart — call destroy() to fully release. */
626
680
  async stop() {
627
681
  if (!this.capture) return "";
682
+ this._stopping = true;
628
683
  this.correctionOrchestrator.stop();
629
684
  this.speechStreaming.stop();
630
- this.workerManager.cancel();
631
685
  this.updateStatus("processing");
686
+ if (this.workerManager.isTranscribing) {
687
+ try {
688
+ const [audio, inFlightText] = await Promise.all([
689
+ pauseCapture(this.capture),
690
+ this.workerManager.awaitCurrentTranscription()
691
+ ]);
692
+ this._stopping = false;
693
+ const text = inFlightText.trim();
694
+ if (text) {
695
+ this.emit("correction", text);
696
+ this.updateStatus("ready");
697
+ return text;
698
+ }
699
+ if (audio.length > 0) {
700
+ const freshText = await this.workerManager.transcribe(audio);
701
+ this.emit("correction", freshText);
702
+ this.updateStatus("ready");
703
+ return freshText;
704
+ }
705
+ this.updateStatus("ready");
706
+ return "";
707
+ } catch (err) {
708
+ this._stopping = false;
709
+ this.emitError(
710
+ "TRANSCRIPTION_FAILED",
711
+ err instanceof Error ? err.message : "Final transcription failed."
712
+ );
713
+ this.updateStatus("ready");
714
+ return "";
715
+ }
716
+ }
717
+ this.workerManager.cancel();
718
+ this._stopping = false;
632
719
  try {
633
- const audio = await stopCapture(this.capture);
634
- this.capture = null;
720
+ const audio = await pauseCapture(this.capture);
635
721
  if (audio.length === 0) {
636
722
  this.updateStatus("ready");
637
723
  return "";
@@ -654,6 +740,10 @@ var STTEngine = class extends TypedEventEmitter {
654
740
  this.correctionOrchestrator.stop();
655
741
  this.speechStreaming.destroy();
656
742
  if (this.capture) {
743
+ try {
744
+ this.capture._processor.disconnect();
745
+ } catch {
746
+ }
657
747
  for (const track of this.capture.stream.getTracks()) {
658
748
  track.stop();
659
749
  }
@@ -682,7 +772,7 @@ var STTEngine = class extends TypedEventEmitter {
682
772
  const audio = await resampleAudio(samples, nativeSr);
683
773
  if (audio.length === 0) return;
684
774
  const text = await this.workerManager.transcribe(audio);
685
- if (text.trim() && this.capture) {
775
+ if (text.trim() && this.capture && !this._stopping) {
686
776
  this.emit("correction", text);
687
777
  }
688
778
  } catch (err) {
@@ -739,8 +829,10 @@ export {
739
829
  SpeechStreamingManager,
740
830
  TypedEventEmitter,
741
831
  WorkerManager,
832
+ pauseCapture,
742
833
  resampleAudio,
743
834
  resolveConfig,
835
+ resumeCapture,
744
836
  snapshotAudio,
745
837
  startCapture,
746
838
  stopCapture