dsh-live-voice 0.7.0 → 0.7.2

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/lib/client.js CHANGED
@@ -152,7 +152,7 @@ var SessionSpeechMemory = class {
152
152
  };
153
153
 
154
154
  // src/modules/recognition/vad/assets.ts
155
- var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.0" : "test") + "/";
155
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
156
156
  async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
157
157
  try {
158
158
  const response = await fetchImpl(sileroVadAssetBase() + "availability", {
@@ -458,7 +458,7 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
458
458
  let loading = captureLoads.get(context);
459
459
  if (!loading) {
460
460
  loading = context.audioWorklet.addModule(
461
- sileroVadAssetBase() + "vad.capture.js?revision=finish-v1"
461
+ sileroVadAssetBase() + "vad.capture.js?revision=pause-v2"
462
462
  );
463
463
  captureLoads.set(context, loading);
464
464
  void loading.catch(() => {
@@ -477,6 +477,10 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
477
477
  let released = false;
478
478
  let acknowledged = false;
479
479
  let finishing;
480
+ let flushing;
481
+ let resolveFlush;
482
+ let rejectFlush;
483
+ let flushTimer;
480
484
  let resolveFinish;
481
485
  let rejectFinish;
482
486
  let failure2;
@@ -487,6 +491,8 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
487
491
  if (released) return;
488
492
  released = true;
489
493
  clearTimeout(timer);
494
+ clearTimeout(flushTimer);
495
+ rejectFlush?.(cancelled());
490
496
  unsubscribe();
491
497
  node.port.onmessage = null;
492
498
  node.onprocessorerror = null;
@@ -504,6 +510,8 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
504
510
  if (released) return;
505
511
  failure2 = error;
506
512
  rejectFinish?.(error);
513
+ rejectFlush?.(error);
514
+ rejectFlush = void 0;
507
515
  cleanup();
508
516
  };
509
517
  unsubscribe = stream.onInvalidated((error) => {
@@ -515,7 +523,15 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
515
523
  }
516
524
  node.port.onmessage = (event) => {
517
525
  if (released || stream.released) return;
518
- if (event.data?.type === "stopped") {
526
+ if (event.data?.type === "flushed") {
527
+ clearTimeout(flushTimer);
528
+ void stream.flush().then(() => {
529
+ const done = resolveFlush;
530
+ resolveFlush = rejectFlush = void 0;
531
+ flushing = void 0;
532
+ done?.();
533
+ }, fail);
534
+ } else if (event.data?.type === "stopped") {
519
535
  if (!finishing || acknowledged) return;
520
536
  acknowledged = true;
521
537
  clearTimeout(timer);
@@ -556,6 +572,25 @@ async function attachSileroVadCapture(context, source, stream, onPcm) {
556
572
  throw error;
557
573
  }
558
574
  return {
575
+ resume() {
576
+ if (released || finishing) throw failure2 || cancelled();
577
+ node.port.postMessage({ type: "resume" });
578
+ },
579
+ flush(pause = false) {
580
+ if (flushing) return flushing;
581
+ if (released || stream.released || finishing) return Promise.reject(failure2 || cancelled());
582
+ flushing = new Promise((resolve, reject) => {
583
+ resolveFlush = resolve;
584
+ rejectFlush = reject;
585
+ });
586
+ flushTimer = setTimeout(() => fail(new Error("VAD capture flush timed out.")), 3e3);
587
+ try {
588
+ node.port.postMessage({ type: "flush", pause });
589
+ } catch (error) {
590
+ fail(error instanceof Error ? error : new Error("VAD capture flush failed."));
591
+ }
592
+ return flushing;
593
+ },
559
594
  finish() {
560
595
  if (finishing) return finishing;
561
596
  if (released || stream.released) return finishing = Promise.reject(failure2 || cancelled());
@@ -2388,6 +2423,7 @@ async function startRecordingRecognition(engine, options, capture) {
2388
2423
  await writer.finish("interrupted");
2389
2424
  throw Object.assign(new Error("Cancelled"), { name: "AbortError" });
2390
2425
  }
2426
+ let suspension = null;
2391
2427
  const session = {
2392
2428
  writer,
2393
2429
  detector: capture.silero ? "silero" : "energy",
@@ -2408,6 +2444,31 @@ async function startRecordingRecognition(engine, options, capture) {
2408
2444
  lang,
2409
2445
  operation: uid(),
2410
2446
  capturedSamples: 0,
2447
+ captureSuspended: false,
2448
+ suspendCapture: () => {
2449
+ if (session.captureSuspended) return Promise.resolve(true);
2450
+ if (suspension) return suspension;
2451
+ if (session.finished || !valid()) return Promise.resolve(false);
2452
+ suspension = (async () => {
2453
+ await engine.meter.context.suspend();
2454
+ if (!valid()) return false;
2455
+ session.captureSuspended = true;
2456
+ return true;
2457
+ })().finally(() => {
2458
+ suspension = null;
2459
+ });
2460
+ return suspension;
2461
+ },
2462
+ resumeCapture: async () => {
2463
+ if (!session.captureSuspended) return true;
2464
+ if (session.finished || !valid()) return false;
2465
+ session.capture?.resume?.();
2466
+ await engine.meter.context.resume();
2467
+ if (!valid()) return false;
2468
+ session.captureSuspended = false;
2469
+ return true;
2470
+ },
2471
+ pauseForNavigation: async () => false,
2411
2472
  finishRecording: async () => {
2412
2473
  },
2413
2474
  stopRecording: async () => {
@@ -2502,10 +2563,10 @@ async function startRecordingRecognition(engine, options, capture) {
2502
2563
  queuedThrough = Math.max(queuedThrough, end);
2503
2564
  await releaseCapturedRanges();
2504
2565
  };
2505
- async function releaseCapturedRanges() {
2566
+ async function releaseCapturedRanges(clampToCaptured = false) {
2506
2567
  if (!valid()) return;
2507
2568
  const captured = writer.session.capturedSamples;
2508
- while (awaitingTail.length && (session.finished || awaitingTail[0].endSample <= captured)) {
2569
+ while (awaitingTail.length && (session.finished || clampToCaptured || awaitingTail[0].endSample <= captured)) {
2509
2570
  const pending = awaitingTail[0];
2510
2571
  const attempt = { ...pending, endSample: Math.min(pending.endSample, captured) };
2511
2572
  await writer.attempt(attempt);
@@ -2560,6 +2621,15 @@ async function startRecordingRecognition(engine, options, capture) {
2560
2621
  }
2561
2622
  });
2562
2623
  session.pinDetector = detector;
2624
+ session.pauseForNavigation = async () => {
2625
+ if (suspension) await suspension;
2626
+ if (!session.captureSuspended) await session.capture?.flush?.(true);
2627
+ if (!await session.suspendCapture() || !valid()) return false;
2628
+ detector.finish();
2629
+ await session.tail;
2630
+ await serialize(() => releaseCapturedRanges(true), false, true);
2631
+ return valid();
2632
+ };
2563
2633
  async function drain() {
2564
2634
  if (session.draining || !valid()) return;
2565
2635
  session.draining = true;
@@ -2829,7 +2899,7 @@ async function startRecordingRecognition(engine, options, capture) {
2829
2899
  awaitingTail.length = 0;
2830
2900
  await recovering;
2831
2901
  session.activeRequest?.abort();
2832
- if (session.finishing) session.capture?.release();
2902
+ if (session.finishing || session.captureSuspended) session.capture?.release();
2833
2903
  else {
2834
2904
  try {
2835
2905
  await session.capture?.finish();
@@ -3065,6 +3135,18 @@ var WhisperHttpRecognitionEngine = class {
3065
3135
  finish() {
3066
3136
  return this.session?.finishRecording() ?? Promise.resolve();
3067
3137
  }
3138
+ get captureSuspended() {
3139
+ return this.session?.captureSuspended === true;
3140
+ }
3141
+ suspendCapture() {
3142
+ return this.session?.suspendCapture() ?? Promise.resolve(false);
3143
+ }
3144
+ resumeCapture() {
3145
+ return this.session?.resumeCapture() ?? Promise.resolve(false);
3146
+ }
3147
+ pauseForNavigation() {
3148
+ return this.session?.pauseForNavigation() ?? Promise.resolve(false);
3149
+ }
3068
3150
  async stop() {
3069
3151
  this.startGeneration = (this.startGeneration || 0) + 1;
3070
3152
  const session = this.session;
@@ -3425,7 +3507,7 @@ async function removeBrowserModel(key, storage = caches) {
3425
3507
  }
3426
3508
 
3427
3509
  // src/modules/recognition/local/assets.ts
3428
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.0" : "test") + "/";
3510
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
3429
3511
 
3430
3512
  // src/modules/recognition/local/runtimeIdentity.ts
3431
3513
  var workerProtocol = "asr-worker-v3-ort-1.31.0-dev.20260914-8d85527a0";
@@ -10258,6 +10340,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
10258
10340
  this.unfinished = /* @__PURE__ */ new Set();
10259
10341
  this.suppressed = /* @__PURE__ */ new Set();
10260
10342
  this.inputTail = Promise.resolve();
10343
+ this.navigationPaused = false;
10344
+ this.navigationEpoch = 0;
10345
+ this.navigationResults = [];
10261
10346
  this.speechBarrier = Promise.resolve();
10262
10347
  this.disposed = false;
10263
10348
  this.inputReleaseError = null;
@@ -10502,6 +10587,10 @@ var VoiceCoordinator = class _VoiceCoordinator {
10502
10587
  });
10503
10588
  }
10504
10589
  _recoverInput(error) {
10590
+ if (this.navigationPaused) {
10591
+ this.patch({ inputError: message(error), error: message(error) });
10592
+ return;
10593
+ }
10505
10594
  const attempt = this.inputRecoveryAttempt || 0;
10506
10595
  const retry = !this.inputReleaseError && isRetryableVoiceError(error) && attempt < MAX_RECOVERY_ATTEMPTS;
10507
10596
  const generation = (this.inputRecoveryGeneration || 0) + 1;
@@ -10566,6 +10655,23 @@ var VoiceCoordinator = class _VoiceCoordinator {
10566
10655
  });
10567
10656
  return result;
10568
10657
  }
10658
+ async _suspendInputCapture() {
10659
+ return this._input(async () => {
10660
+ if (!this.snapshot.listening || typeof this.recognition.suspendCapture !== "function")
10661
+ return false;
10662
+ return await this.recognition.suspendCapture() !== false;
10663
+ });
10664
+ }
10665
+ async _resumeInputCapture(valid = () => true) {
10666
+ return this._input(async () => {
10667
+ if (!valid() || !this.snapshot.listening || typeof this.recognition.resumeCapture !== "function")
10668
+ return false;
10669
+ return await this.recognition.resumeCapture() !== false;
10670
+ });
10671
+ }
10672
+ _hasSuspendedInputCapture() {
10673
+ return this.snapshot.listening && this.recognition.captureSuspended === true;
10674
+ }
10569
10675
  async _releaseInput() {
10570
10676
  const recognitionResult = await Promise.resolve().then(() => this.recognition.stop()).then(
10571
10677
  () => ({ status: "fulfilled" }),
@@ -10874,8 +10980,19 @@ var VoiceCoordinator = class _VoiceCoordinator {
10874
10980
  void this.resumeSpeech();
10875
10981
  }, this.snapshot.settings.assistantSpeechDelaySeconds * 1e3);
10876
10982
  }
10877
- onResult({ final = "", interim = "", startedAt, paragraphBefore = false, onCommandRecognized }) {
10983
+ onResult(result) {
10984
+ const {
10985
+ final = "",
10986
+ interim = "",
10987
+ startedAt,
10988
+ paragraphBefore = false,
10989
+ onCommandRecognized
10990
+ } = result;
10878
10991
  if (this.disposed || !this.snapshot.listening && !this.snapshot.starting) return;
10992
+ if (this.navigationPaused) {
10993
+ if (final) this.navigationResults.push(result);
10994
+ return;
10995
+ }
10879
10996
  const recognizing = this.recognition.activityManaged ? this.snapshot.recognizing : !!interim;
10880
10997
  if (this.snapshot.speaking && !this.snapshot.paused && this.snapshot.settings.mode === "speaker")
10881
10998
  return;
@@ -10960,7 +11077,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
10960
11077
  return this.snapshot.pendingDeliveryTranscriptions ?? this.snapshot.pendingTranscriptions;
10961
11078
  }
10962
11079
  maybeScheduleAutoSend(draft = this.autoSendIntent) {
10963
- if (draft == null || draft !== this.composer.getDraft()) return;
11080
+ if (this.navigationPaused || draft == null || draft !== this.composer.getDraft()) return;
10964
11081
  if (this.autoSendTimer !== null) return;
10965
11082
  if (this.snapshot.settings.sendingMode === "manual" || this.snapshot.muted || this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.deliveryPending() > 0)
10966
11083
  return;
@@ -11015,6 +11132,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
11015
11132
  if (this.autoSendIntent != null && draft !== this.autoSendIntent) this.cancelAutoSend();
11016
11133
  }
11017
11134
  stopListening() {
11135
+ ++this.navigationEpoch;
11136
+ this.navigationPaused = false;
11137
+ this.navigationResults = [];
11018
11138
  this._cancelRecovery("input");
11019
11139
  this.holdToTalkRelease = false;
11020
11140
  this.cancelAutoSend();
@@ -11160,11 +11280,64 @@ var VoiceCoordinator = class _VoiceCoordinator {
11160
11280
  this.patch({ paused: checkpoint.paused && this.queue.length > 0 });
11161
11281
  this._syncSpeechSegments();
11162
11282
  }
11163
- async suspendSpeech() {
11283
+ async pauseInputForNavigation() {
11284
+ if (!this.snapshot.listening || typeof this.recognition.pauseForNavigation !== "function")
11285
+ return false;
11286
+ const generation = ++this.navigationEpoch;
11287
+ this.navigationPaused = true;
11288
+ this.cancelAutoSend();
11289
+ try {
11290
+ const paused = await this._input(() => this.recognition.pauseForNavigation());
11291
+ if (!paused) throw new Error("Microphone capture could not be paused safely.");
11292
+ return true;
11293
+ } catch (error) {
11294
+ if (generation !== this.navigationEpoch || this.disposed) return false;
11295
+ try {
11296
+ await this._input(() => this._releaseInput());
11297
+ this.inputReleaseError = null;
11298
+ } catch (cleanup) {
11299
+ this.inputReleaseError = cleanup;
11300
+ this.patch({ inputError: message(cleanup), error: message(cleanup) });
11301
+ throw cleanup;
11302
+ }
11303
+ if (generation !== this.navigationEpoch || this.disposed) return false;
11304
+ this.patch({
11305
+ listening: false,
11306
+ starting: false,
11307
+ recognizing: false,
11308
+ inputError: message(error),
11309
+ error: message(error)
11310
+ });
11311
+ return false;
11312
+ }
11313
+ }
11314
+ async resumeInputFromNavigation() {
11315
+ if (!this.navigationPaused || this.disposed) return false;
11316
+ const generation = this.navigationEpoch;
11317
+ if (!this.snapshot.listening) {
11318
+ const queued = this.navigationResults.splice(0);
11319
+ this.navigationPaused = false;
11320
+ await this._startInput(this.snapshot.conversation);
11321
+ if (this.disposed || generation !== this.navigationEpoch) return false;
11322
+ if (!this.snapshot.listening) {
11323
+ this.navigationPaused = true;
11324
+ this.navigationResults.unshift(...queued);
11325
+ return false;
11326
+ }
11327
+ for (const result of queued) this.onResult(result);
11328
+ return true;
11329
+ }
11330
+ const resumed = await this._input(() => this.recognition.resumeCapture());
11331
+ if (!resumed || this.disposed || generation !== this.navigationEpoch) return false;
11332
+ this.navigationPaused = false;
11333
+ for (const result of this.navigationResults.splice(0)) this.onResult(result);
11334
+ return true;
11335
+ }
11336
+ async suspendSpeech(preserveInput = false) {
11164
11337
  this.speechSuspended = true;
11165
11338
  await Promise.all([
11166
11339
  _VoiceCoordinator.prototype.stopSpeech.call(this, false),
11167
- this.stopListening()
11340
+ preserveInput ? this.pauseInputForNavigation() : this.stopListening()
11168
11341
  ]);
11169
11342
  }
11170
11343
  discardSpeechMessage(id) {
@@ -11232,6 +11405,13 @@ var VoiceCoordinator = class _VoiceCoordinator {
11232
11405
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
11233
11406
  return;
11234
11407
  }
11408
+ if (epoch === this.speechEpoch && !this.disposed && resumeListening && !this.navigationPaused && this.snapshot.settings.mode === "speaker" && this._hasSuspendedInputCapture()) {
11409
+ try {
11410
+ await this._resumeInputCapture();
11411
+ } catch (error) {
11412
+ this.patch({ inputError: message(error), error: message(error) });
11413
+ }
11414
+ }
11235
11415
  if (epoch === this.speechEpoch && !this.disposed && resumeListening && !this.snapshot.inputRecovery && this.snapshot.conversation && !this.snapshot.listening && !this.snapshot.starting)
11236
11416
  await this._startInput(true);
11237
11417
  }
@@ -11278,22 +11458,53 @@ var VoiceCoordinator = class _VoiceCoordinator {
11278
11458
  getSpeechProgress() {
11279
11459
  return this.engines[this.snapshot.speechEngine]?.getPlaybackProgress?.() ?? null;
11280
11460
  }
11461
+ async _restorePausedSpeechInput(epoch, control) {
11462
+ const valid = () => !this.disposed && !this.navigationPaused && epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.paused && this.snapshot.settings.mode === "speaker";
11463
+ if (!valid()) return;
11464
+ try {
11465
+ if (this._hasSuspendedInputCapture()) {
11466
+ await this._resumeInputCapture(valid);
11467
+ } else if (this.snapshot.conversation && !this.snapshot.listening && !this.snapshot.starting && !this.snapshot.inputRecovery) {
11468
+ await this._startInput(true);
11469
+ }
11470
+ } catch (error) {
11471
+ if (valid()) this.patch({ inputError: message(error), error: message(error) });
11472
+ }
11473
+ }
11474
+ async _gateResumedSpeechInput() {
11475
+ if (this.snapshot.settings.mode !== "speaker") return;
11476
+ const suspended = await this._suspendInputCapture();
11477
+ if (!suspended) {
11478
+ await this.stopListening();
11479
+ if (this.inputReleaseError) throw this.inputReleaseError;
11480
+ }
11481
+ }
11281
11482
  async pauseSpeech(automatic = false) {
11282
11483
  if (this.snapshot.speechRecovery?.retrying) this._cancelRecovery("speech", true);
11283
11484
  if (!automatic) {
11284
11485
  this._cancelInterruptionResume();
11285
11486
  this.interruptionPausedSpeech = false;
11286
11487
  }
11287
- if (this.disposed || this.snapshot.paused) return;
11488
+ if (this.disposed) return;
11489
+ if (this.snapshot.paused) {
11490
+ const epoch2 = this.speechEpoch;
11491
+ const control2 = ++this.controlEpoch;
11492
+ await this.inputTail;
11493
+ await this._restorePausedSpeechInput(epoch2, control2);
11494
+ return;
11495
+ }
11288
11496
  if (!this.snapshot.speaking && this.snapshot.speechRunActive) {
11289
11497
  this._cancelAssistantSpeechTimer();
11498
+ const control2 = ++this.controlEpoch;
11290
11499
  this.patch({ paused: true });
11500
+ await this._restorePausedSpeechInput(this.speechEpoch, control2);
11291
11501
  return;
11292
11502
  }
11293
11503
  if (!this.snapshot.speaking) return;
11294
11504
  if (this.speechLaunchPending) {
11295
- ++this.controlEpoch;
11505
+ const control2 = ++this.controlEpoch;
11296
11506
  this.patch({ paused: true });
11507
+ await this._restorePausedSpeechInput(this.speechEpoch, control2);
11297
11508
  return;
11298
11509
  }
11299
11510
  const epoch = this.speechEpoch;
@@ -11304,6 +11515,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
11304
11515
  if (epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.speaking && result !== false) {
11305
11516
  this.patch({ paused: true });
11306
11517
  this.activeRecordingTimeline?.("assistant-pause");
11518
+ await this._restorePausedSpeechInput(epoch, control);
11307
11519
  }
11308
11520
  } catch (error) {
11309
11521
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
@@ -11323,12 +11535,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
11323
11535
  if (this.speechLaunchPending) {
11324
11536
  const epoch2 = this.speechEpoch;
11325
11537
  const control2 = ++this.controlEpoch;
11326
- if (this.snapshot.settings.mode === "speaker") {
11327
- await this.stopListening();
11328
- if (this.inputReleaseError) {
11329
- this.patch({ error: message(this.inputReleaseError) });
11330
- return;
11331
- }
11538
+ try {
11539
+ await this._gateResumedSpeechInput();
11540
+ } catch (error) {
11541
+ if (epoch2 === this.speechEpoch) this.patch({ error: message(error) });
11542
+ return;
11332
11543
  }
11333
11544
  if (epoch2 === this.speechEpoch && control2 === this.controlEpoch)
11334
11545
  this.patch({ paused: false });
@@ -11337,19 +11548,19 @@ var VoiceCoordinator = class _VoiceCoordinator {
11337
11548
  const epoch = this.speechEpoch;
11338
11549
  const control = ++this.controlEpoch;
11339
11550
  try {
11340
- if (this.snapshot.settings.mode === "speaker") {
11341
- await this.stopListening();
11342
- if (this.inputReleaseError) throw this.inputReleaseError;
11343
- }
11551
+ await this._gateResumedSpeechInput();
11344
11552
  if (epoch !== this.speechEpoch || control !== this.controlEpoch) return;
11345
11553
  const result = await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
11346
11554
  if (epoch === this.speechEpoch && control === this.controlEpoch && result !== false) {
11347
11555
  this.patch({ paused: false });
11348
11556
  this.activeRecordingTimeline?.("assistant-resume");
11349
11557
  this._prefetchSpeech();
11558
+ } else {
11559
+ await this._restorePausedSpeechInput(epoch, control);
11350
11560
  }
11351
11561
  } catch (error) {
11352
11562
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
11563
+ await this._restorePausedSpeechInput(epoch, control);
11353
11564
  }
11354
11565
  }
11355
11566
  async speak(text, messageId = null) {
@@ -11429,15 +11640,22 @@ var VoiceCoordinator = class _VoiceCoordinator {
11429
11640
  if (epoch !== this.speechEpoch || this.disposed) return;
11430
11641
  if (this.snapshot.settings.mode === "speaker") {
11431
11642
  const gating = this.snapshot.listening;
11643
+ const persistentCapture = typeof this.recognition.suspendCapture === "function";
11432
11644
  if (gating) timeline?.("capture-gated");
11433
- await this.stopListening();
11434
- if (this.inputReleaseError) {
11645
+ try {
11646
+ const suspended = persistentCapture ? await this._suspendInputCapture() : false;
11647
+ if (gating && !suspended) {
11648
+ await this.stopListening();
11649
+ if (this.inputReleaseError) throw this.inputReleaseError;
11650
+ }
11651
+ } catch (error) {
11435
11652
  if (gating) timeline?.("capture-gate-failed");
11436
- throw this.inputReleaseError;
11653
+ throw error;
11437
11654
  }
11438
11655
  if (gating) timeline?.("input-stop-complete");
11439
11656
  }
11440
11657
  if (epoch !== this.speechEpoch || this.disposed) return;
11658
+ if (this.snapshot.paused) await this._restorePausedSpeechInput(epoch, this.controlEpoch);
11441
11659
  const queued = item;
11442
11660
  if (queued?.preparing) await queued.preparing;
11443
11661
  if (epoch !== this.speechEpoch || this.disposed) return;
@@ -11552,6 +11770,12 @@ var VoiceCoordinator = class _VoiceCoordinator {
11552
11770
  this.assistantSpeechDelayReason = "segment-gap";
11553
11771
  this.assistantSpeechNotBefore = Date.now() + this.snapshot.settings.segmentGapMs;
11554
11772
  this._drain();
11773
+ } else if (this.snapshot.settings.mode === "speaker" && this.snapshot.listening) {
11774
+ try {
11775
+ await this._resumeInputCapture();
11776
+ } catch (error) {
11777
+ this.patch({ inputError: message(error), error: message(error) });
11778
+ }
11555
11779
  } else if (this.snapshot.conversation && !this.snapshot.inputRecovery && !this.snapshot.listening && !this.snapshot.starting)
11556
11780
  await this._startInput(true);
11557
11781
  }
@@ -11664,16 +11888,16 @@ var VoiceCoordinator = class _VoiceCoordinator {
11664
11888
  if (boundary <= 0) return;
11665
11889
  if (!complete && hasIncompleteSpeechMarkdown(remaining.slice(0, boundary))) return;
11666
11890
  if (hasUnclosedCodeFence(remaining.slice(0, boundary)) && !complete) return;
11667
- const chunk = filterSpeechOutput(remaining.slice(0, boundary).trim(), {
11891
+ const segments = splitSpeechOutput(remaining.slice(0, boundary).trim(), {
11668
11892
  filterCodeBlocks: this.snapshot.settings.outputCodeFilterEnabled,
11669
11893
  codeBlockMaxLines: this.snapshot.settings.outputCodeMaxLines,
11670
11894
  codeBlockNotice: this.snapshot.settings.outputCodeNotice,
11671
11895
  lang: this.snapshot.settings.lang
11672
- }).trim();
11896
+ });
11673
11897
  this.consumed.set(id, offset + boundary);
11674
- if (chunk) {
11898
+ if (segments.length) {
11675
11899
  const initial = !this.snapshot.speaking && !this.queue.some((item) => item.prepared);
11676
- const parts = initial ? splitInitialSpeechItem(chunk) : [chunk];
11900
+ const parts = initial ? [...splitInitialSpeechItem(segments[0]), ...segments.slice(1)] : segments;
11677
11901
  this.queue.push(...parts.map((text2) => ({ text: text2, id })));
11678
11902
  this._prefetchSpeech();
11679
11903
  this._syncSpeechSegments();
@@ -11834,13 +12058,11 @@ var MeetingController = class {
11834
12058
  for (const input of this.inputs.values()) input.cancelAutoSend();
11835
12059
  }
11836
12060
  composerReplaced() {
12061
+ for (const [source, job] of this.jobs) {
12062
+ const state = this.snapshot[source];
12063
+ job.discardResults = (Number.isSafeInteger(state?.pendingTranscriptionRequests) ? state.pendingTranscriptionRequests : state?.pending || 0) || (state?.recognizing ? 1 : 0);
12064
+ }
11837
12065
  this.cancelDelivery();
11838
- const job = this.jobs.get("shared");
11839
- if (!job || !job.recognitionAbort) return;
11840
- job.recognitionAbort.abort();
11841
- if (this.sharedTrackLive(job)) this.queueRecognitionRecovery("shared", job);
11842
- else void this.stop("shared").catch(() => {
11843
- });
11844
12066
  }
11845
12067
  toggleTimestamps() {
11846
12068
  const enabled = !this.snapshot.timestamps;
@@ -11864,7 +12086,7 @@ var MeetingController = class {
11864
12086
  }
11865
12087
  this.patch(null, { active: true });
11866
12088
  const abort = new AbortController();
11867
- const job = { abort };
12089
+ const job = { abort, discardResults: 0 };
11868
12090
  this.jobs.set(source, job);
11869
12091
  const valid = () => this.jobs.get(source) === job && !abort.signal.aborted;
11870
12092
  this.patch(source, { starting: true, error: null });
@@ -11934,6 +12156,10 @@ var MeetingController = class {
11934
12156
  signal: recognitionAbort.signal,
11935
12157
  onResult: ({ final, interim, startedAt, paragraphBefore }) => {
11936
12158
  if (!valid()) return;
12159
+ if (job.discardResults > 0) {
12160
+ job.discardResults--;
12161
+ return;
12162
+ }
11937
12163
  if (final?.trim()) job.retries = 0;
11938
12164
  const accepted = this.onSpeech?.(source, final || interim) !== false;
11939
12165
  if (accepted)
@@ -12770,7 +12996,6 @@ var AttendeeClient = class {
12770
12996
  this.queues.set(source, work);
12771
12997
  }
12772
12998
  async audio(event, generation) {
12773
- if (this.canDeliver && !this.canDeliver()) throw new Error("no-composer");
12774
12999
  const source = "attendee:" + event.botId + ":" + event.participantId;
12775
13000
  let row = this.rows.get(source);
12776
13001
  if (!row) {
@@ -14576,8 +14801,8 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
14576
14801
  var import_react39 = __toESM(require("react"), 1);
14577
14802
 
14578
14803
  // src/modules/settings/services/releases.ts
14579
- var CURRENT_VERSION = "0.7.0";
14580
- var TESTED_DSH_VERSION = "0.2.2-alpha.2";
14804
+ var CURRENT_VERSION = "0.7.2";
14805
+ var TESTED_DSH_VERSION = "0.2.2-rc.1";
14581
14806
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
14582
14807
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
14583
14808
  var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
@@ -17674,7 +17899,9 @@ function apply(ctx) {
17674
17899
  entry.speechState.checkpoint = entry.controller.checkpointSpeech();
17675
17900
  entry.speechAttached = false;
17676
17901
  ++entry.speechRequest;
17677
- const stopped = entry.controller.suspendSpeech();
17902
+ const preserveInput = entry.controller.snapshot.conversation && entry.controller.snapshot.listening && typeof entry.controller.recognition.pauseForNavigation === "function";
17903
+ entry.parkedInput = preserveInput;
17904
+ const stopped = entry.controller.suspendSpeech(preserveInput);
17678
17905
  entry.speechRelease = Promise.resolve(stopped).then(() => {
17679
17906
  if (entry.controller.speechStopError) throw entry.controller.speechStopError;
17680
17907
  if (entry.controller.inputReleaseError) throw entry.controller.inputReleaseError;
@@ -17694,6 +17921,13 @@ function apply(ctx) {
17694
17921
  await entry.speechRelease;
17695
17922
  if (disposed || entry.closed || !entry.speechAttached || request !== entry.speechRequest)
17696
17923
  return;
17924
+ if (entry.parkedInput) {
17925
+ const resumed = await entry.controller.resumeInputFromNavigation();
17926
+ if (disposed || entry.closed || !entry.speechAttached || request !== entry.speechRequest)
17927
+ return;
17928
+ if (!resumed) throw new Error("The paused microphone recording could not resume.");
17929
+ entry.parkedInput = false;
17930
+ }
17697
17931
  entry.controller.restoreSpeech(entry.speechState.checkpoint);
17698
17932
  entry.speechState.checkpoint = null;
17699
17933
  entry.refreshSpeech(!entry.speechState.attached);
@@ -17704,6 +17938,7 @@ function apply(ctx) {
17704
17938
  function retire(entry) {
17705
17939
  if (entry.closed) return;
17706
17940
  suspendSessionSpeech(entry);
17941
+ if (entry.parkedInput && !disposed) return;
17707
17942
  entry.closed = true;
17708
17943
  if (meetingEntry === entry) {
17709
17944
  void meeting.attendee.stop();
@@ -17878,7 +18113,7 @@ function apply(ctx) {
17878
18113
  entry.chatListeners.add(listener);
17879
18114
  return () => entry.chatListeners.delete(listener);
17880
18115
  };
17881
- entry.readChat = entry.chat.getSnapshot.bind(entry.chat);
18116
+ entry.readChat = () => entry.chat.getSnapshot();
17882
18117
  const refresh = (reset = false) => {
17883
18118
  if (disposed || entry.closed) return;
17884
18119
  const snapshot = entry.readChat();
@@ -17906,10 +18141,20 @@ function apply(ctx) {
17906
18141
  };
17907
18142
  entry.refreshSpeech = refresh;
17908
18143
  refresh();
17909
- entry.unsubscribe = entry.chat.subscribe(() => {
18144
+ const publishChat = () => {
17910
18145
  refresh();
17911
18146
  for (const listener of entry.chatListeners) listener();
17912
- });
18147
+ };
18148
+ entry.unsubscribe = entry.chat.subscribe(publishChat);
18149
+ entry.reconnectChat = () => {
18150
+ if (disposed || entry.closed) return;
18151
+ const chat = ctx.uiConversation.binding(sessionId).target("chat");
18152
+ if (chat === entry.chat) return;
18153
+ entry.unsubscribe?.();
18154
+ entry.chat = chat;
18155
+ entry.unsubscribe = chat.subscribe(publishChat);
18156
+ publishChat();
18157
+ };
17913
18158
  const pendingInteractions = ctx.uiSession.pendingInteractions;
17914
18159
  const refreshPendingQuestion = (baseline = false) => {
17915
18160
  if (disposed || entry.closed || !pendingInteractions) return;
@@ -17941,7 +18186,7 @@ function apply(ctx) {
17941
18186
  const before = controller.getSnapshot();
17942
18187
  update(next);
17943
18188
  const settings2 = controller.getSnapshot().settings;
17944
- if ([...recognitionSettingKeys].some((key2) => before.settings[key2] !== settings2[key2])) {
18189
+ if (!entry.parkedInput && [...recognitionSettingKeys].some((key2) => before.settings[key2] !== settings2[key2])) {
17945
18190
  if (!entry.restoreInput) entry.inputRestoreRequest = entry.request;
17946
18191
  entry.restoreInput ||= Boolean(before.listening || before.starting);
17947
18192
  const revision = entry.inputSettingsRevision = (entry.inputSettingsRevision || 0) + 1;
@@ -17983,6 +18228,7 @@ function apply(ctx) {
17983
18228
  const original = controller[method].bind(controller);
17984
18229
  controller[method] = (...args) => {
17985
18230
  if (method === "endConversation" && args[0] === true && !entry.speechAttached) {
18231
+ if (entry.parkedInput) return entry.speechRelease ?? Promise.resolve();
17986
18232
  controller.patch({ conversation: false });
17987
18233
  return Promise.all([
17988
18234
  controller.stopListening(),
@@ -18004,6 +18250,7 @@ function apply(ctx) {
18004
18250
  }
18005
18251
  }
18006
18252
  entry.request++;
18253
+ if (method !== "stopSpeech") entry.parkedInput = false;
18007
18254
  if (method === "endConversation" && args[0] !== true || method === "stopConversationInput")
18008
18255
  voiceModeActive = false;
18009
18256
  const result = original(...args);
@@ -18084,6 +18331,7 @@ function apply(ctx) {
18084
18331
  import_react75.default.useLayoutEffect(() => {
18085
18332
  if (disposed) return;
18086
18333
  const current = get(sessionId);
18334
+ current.reconnectChat();
18087
18335
  current.refs++;
18088
18336
  if (kind === "buttons") current.buttons++;
18089
18337
  setEntry(current);
@@ -18108,7 +18356,6 @@ function apply(ctx) {
18108
18356
  entry.composers.delete(token.current);
18109
18357
  if (!entry.composers.size) suspendSessionSpeech(entry);
18110
18358
  if (meetingEntry === entry && !entry.composers.size) {
18111
- void meeting.attendee.stop();
18112
18359
  meeting.composerReplaced();
18113
18360
  meetingEntry = null;
18114
18361
  meeting.transcript.setActive("microphone", false);
@@ -18119,10 +18366,7 @@ function apply(ctx) {
18119
18366
  if (!entry || entry.closed || disposed) return;
18120
18367
  if (!input || typeof props.inputActions?.setDraft !== "function") return;
18121
18368
  if (meetingEntry !== entry) {
18122
- if (meetingEntry) {
18123
- void meeting.attendee.stop();
18124
- suspendSessionSpeech(meetingEntry);
18125
- }
18369
+ if (meetingEntry) suspendSessionSpeech(meetingEntry);
18126
18370
  meeting.composerReplaced();
18127
18371
  meetingEntry = entry;
18128
18372
  entry.controller.setExternalSpeechActivity(sharedSpeechActive);
@@ -18154,7 +18398,7 @@ function apply(ctx) {
18154
18398
  });
18155
18399
  const attachSpeech = !entry.speechAttached;
18156
18400
  if (attachSpeech) entry.speechAttached = true;
18157
- const inputReady = voiceModeActive && !entry.controller.getSnapshot().conversation ? entry.controller.startConversation() : Promise.resolve();
18401
+ const inputReady = !entry.parkedInput && voiceModeActive && !entry.controller.getSnapshot().conversation ? entry.controller.startConversation() : Promise.resolve();
18158
18402
  run(
18159
18403
  entry.controller,
18160
18404
  Promise.resolve(inputReady).then(() => {