dsh-live-voice 0.7.0 → 0.7.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/README.md CHANGED
@@ -4,7 +4,7 @@ Local-first speech-to-text (STT), text-to-speech (TTS), and meeting audio transc
4
4
 
5
5
  ## Installation
6
6
 
7
- Tested with **DeepSeek Harness v0.2.1-rc.2**. The package manifest's `dshTestedVersion` field records the tested version.
7
+ Tested with **DeepSeek Harness v0.2.2-rc.1**. The package manifest's `dshTestedVersion` field records the tested version.
8
8
 
9
9
  Install the prebuilt package from npm; no source build is required. The Git repository contains source only.
10
10
 
@@ -36682,7 +36682,7 @@ async function createQwenRuntime(options) {
36682
36682
  }
36683
36683
 
36684
36684
  // src/modules/recognition/local/assets.ts
36685
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.0" : "test") + "/";
36685
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
36686
36686
 
36687
36687
  // src/modules/recognition/local/modelCatalog.ts
36688
36688
  var browserAsrModels = [
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.7.1",
3
3
  "assets": [
4
4
  "asr.worker.js",
5
5
  "manifest.json",
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.1" : "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.1" : "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() {
10666
+ return this._input(async () => {
10667
+ if (!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
  }
@@ -11337,7 +11517,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
11337
11517
  const epoch = this.speechEpoch;
11338
11518
  const control = ++this.controlEpoch;
11339
11519
  try {
11340
- if (this.snapshot.settings.mode === "speaker") {
11520
+ if (this.snapshot.settings.mode === "speaker" && !this._hasSuspendedInputCapture()) {
11341
11521
  await this.stopListening();
11342
11522
  if (this.inputReleaseError) throw this.inputReleaseError;
11343
11523
  }
@@ -11429,11 +11609,17 @@ var VoiceCoordinator = class _VoiceCoordinator {
11429
11609
  if (epoch !== this.speechEpoch || this.disposed) return;
11430
11610
  if (this.snapshot.settings.mode === "speaker") {
11431
11611
  const gating = this.snapshot.listening;
11612
+ const persistentCapture = typeof this.recognition.suspendCapture === "function";
11432
11613
  if (gating) timeline?.("capture-gated");
11433
- await this.stopListening();
11434
- if (this.inputReleaseError) {
11614
+ try {
11615
+ const suspended = persistentCapture ? await this._suspendInputCapture() : false;
11616
+ if (gating && !suspended) {
11617
+ await this.stopListening();
11618
+ if (this.inputReleaseError) throw this.inputReleaseError;
11619
+ }
11620
+ } catch (error) {
11435
11621
  if (gating) timeline?.("capture-gate-failed");
11436
- throw this.inputReleaseError;
11622
+ throw error;
11437
11623
  }
11438
11624
  if (gating) timeline?.("input-stop-complete");
11439
11625
  }
@@ -11552,6 +11738,12 @@ var VoiceCoordinator = class _VoiceCoordinator {
11552
11738
  this.assistantSpeechDelayReason = "segment-gap";
11553
11739
  this.assistantSpeechNotBefore = Date.now() + this.snapshot.settings.segmentGapMs;
11554
11740
  this._drain();
11741
+ } else if (this.snapshot.settings.mode === "speaker" && this.snapshot.listening) {
11742
+ try {
11743
+ await this._resumeInputCapture();
11744
+ } catch (error) {
11745
+ this.patch({ inputError: message(error), error: message(error) });
11746
+ }
11555
11747
  } else if (this.snapshot.conversation && !this.snapshot.inputRecovery && !this.snapshot.listening && !this.snapshot.starting)
11556
11748
  await this._startInput(true);
11557
11749
  }
@@ -11664,16 +11856,16 @@ var VoiceCoordinator = class _VoiceCoordinator {
11664
11856
  if (boundary <= 0) return;
11665
11857
  if (!complete && hasIncompleteSpeechMarkdown(remaining.slice(0, boundary))) return;
11666
11858
  if (hasUnclosedCodeFence(remaining.slice(0, boundary)) && !complete) return;
11667
- const chunk = filterSpeechOutput(remaining.slice(0, boundary).trim(), {
11859
+ const segments = splitSpeechOutput(remaining.slice(0, boundary).trim(), {
11668
11860
  filterCodeBlocks: this.snapshot.settings.outputCodeFilterEnabled,
11669
11861
  codeBlockMaxLines: this.snapshot.settings.outputCodeMaxLines,
11670
11862
  codeBlockNotice: this.snapshot.settings.outputCodeNotice,
11671
11863
  lang: this.snapshot.settings.lang
11672
- }).trim();
11864
+ });
11673
11865
  this.consumed.set(id, offset + boundary);
11674
- if (chunk) {
11866
+ if (segments.length) {
11675
11867
  const initial = !this.snapshot.speaking && !this.queue.some((item) => item.prepared);
11676
- const parts = initial ? splitInitialSpeechItem(chunk) : [chunk];
11868
+ const parts = initial ? [...splitInitialSpeechItem(segments[0]), ...segments.slice(1)] : segments;
11677
11869
  this.queue.push(...parts.map((text2) => ({ text: text2, id })));
11678
11870
  this._prefetchSpeech();
11679
11871
  this._syncSpeechSegments();
@@ -11834,13 +12026,11 @@ var MeetingController = class {
11834
12026
  for (const input of this.inputs.values()) input.cancelAutoSend();
11835
12027
  }
11836
12028
  composerReplaced() {
12029
+ for (const [source, job] of this.jobs) {
12030
+ const state = this.snapshot[source];
12031
+ job.discardResults = (Number.isSafeInteger(state?.pendingTranscriptionRequests) ? state.pendingTranscriptionRequests : state?.pending || 0) || (state?.recognizing ? 1 : 0);
12032
+ }
11837
12033
  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
12034
  }
11845
12035
  toggleTimestamps() {
11846
12036
  const enabled = !this.snapshot.timestamps;
@@ -11864,7 +12054,7 @@ var MeetingController = class {
11864
12054
  }
11865
12055
  this.patch(null, { active: true });
11866
12056
  const abort = new AbortController();
11867
- const job = { abort };
12057
+ const job = { abort, discardResults: 0 };
11868
12058
  this.jobs.set(source, job);
11869
12059
  const valid = () => this.jobs.get(source) === job && !abort.signal.aborted;
11870
12060
  this.patch(source, { starting: true, error: null });
@@ -11934,6 +12124,10 @@ var MeetingController = class {
11934
12124
  signal: recognitionAbort.signal,
11935
12125
  onResult: ({ final, interim, startedAt, paragraphBefore }) => {
11936
12126
  if (!valid()) return;
12127
+ if (job.discardResults > 0) {
12128
+ job.discardResults--;
12129
+ return;
12130
+ }
11937
12131
  if (final?.trim()) job.retries = 0;
11938
12132
  const accepted = this.onSpeech?.(source, final || interim) !== false;
11939
12133
  if (accepted)
@@ -12770,7 +12964,6 @@ var AttendeeClient = class {
12770
12964
  this.queues.set(source, work);
12771
12965
  }
12772
12966
  async audio(event, generation) {
12773
- if (this.canDeliver && !this.canDeliver()) throw new Error("no-composer");
12774
12967
  const source = "attendee:" + event.botId + ":" + event.participantId;
12775
12968
  let row = this.rows.get(source);
12776
12969
  if (!row) {
@@ -14576,8 +14769,8 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
14576
14769
  var import_react39 = __toESM(require("react"), 1);
14577
14770
 
14578
14771
  // src/modules/settings/services/releases.ts
14579
- var CURRENT_VERSION = "0.7.0";
14580
- var TESTED_DSH_VERSION = "0.2.2-alpha.2";
14772
+ var CURRENT_VERSION = "0.7.1";
14773
+ var TESTED_DSH_VERSION = "0.2.2-rc.1";
14581
14774
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
14582
14775
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
14583
14776
  var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
@@ -17674,7 +17867,9 @@ function apply(ctx) {
17674
17867
  entry.speechState.checkpoint = entry.controller.checkpointSpeech();
17675
17868
  entry.speechAttached = false;
17676
17869
  ++entry.speechRequest;
17677
- const stopped = entry.controller.suspendSpeech();
17870
+ const preserveInput = entry.controller.snapshot.conversation && entry.controller.snapshot.listening && typeof entry.controller.recognition.pauseForNavigation === "function";
17871
+ entry.parkedInput = preserveInput;
17872
+ const stopped = entry.controller.suspendSpeech(preserveInput);
17678
17873
  entry.speechRelease = Promise.resolve(stopped).then(() => {
17679
17874
  if (entry.controller.speechStopError) throw entry.controller.speechStopError;
17680
17875
  if (entry.controller.inputReleaseError) throw entry.controller.inputReleaseError;
@@ -17694,6 +17889,13 @@ function apply(ctx) {
17694
17889
  await entry.speechRelease;
17695
17890
  if (disposed || entry.closed || !entry.speechAttached || request !== entry.speechRequest)
17696
17891
  return;
17892
+ if (entry.parkedInput) {
17893
+ const resumed = await entry.controller.resumeInputFromNavigation();
17894
+ if (disposed || entry.closed || !entry.speechAttached || request !== entry.speechRequest)
17895
+ return;
17896
+ if (!resumed) throw new Error("The paused microphone recording could not resume.");
17897
+ entry.parkedInput = false;
17898
+ }
17697
17899
  entry.controller.restoreSpeech(entry.speechState.checkpoint);
17698
17900
  entry.speechState.checkpoint = null;
17699
17901
  entry.refreshSpeech(!entry.speechState.attached);
@@ -17704,6 +17906,7 @@ function apply(ctx) {
17704
17906
  function retire(entry) {
17705
17907
  if (entry.closed) return;
17706
17908
  suspendSessionSpeech(entry);
17909
+ if (entry.parkedInput && !disposed) return;
17707
17910
  entry.closed = true;
17708
17911
  if (meetingEntry === entry) {
17709
17912
  void meeting.attendee.stop();
@@ -17941,7 +18144,7 @@ function apply(ctx) {
17941
18144
  const before = controller.getSnapshot();
17942
18145
  update(next);
17943
18146
  const settings2 = controller.getSnapshot().settings;
17944
- if ([...recognitionSettingKeys].some((key2) => before.settings[key2] !== settings2[key2])) {
18147
+ if (!entry.parkedInput && [...recognitionSettingKeys].some((key2) => before.settings[key2] !== settings2[key2])) {
17945
18148
  if (!entry.restoreInput) entry.inputRestoreRequest = entry.request;
17946
18149
  entry.restoreInput ||= Boolean(before.listening || before.starting);
17947
18150
  const revision = entry.inputSettingsRevision = (entry.inputSettingsRevision || 0) + 1;
@@ -17983,6 +18186,7 @@ function apply(ctx) {
17983
18186
  const original = controller[method].bind(controller);
17984
18187
  controller[method] = (...args) => {
17985
18188
  if (method === "endConversation" && args[0] === true && !entry.speechAttached) {
18189
+ if (entry.parkedInput) return entry.speechRelease ?? Promise.resolve();
17986
18190
  controller.patch({ conversation: false });
17987
18191
  return Promise.all([
17988
18192
  controller.stopListening(),
@@ -18004,6 +18208,7 @@ function apply(ctx) {
18004
18208
  }
18005
18209
  }
18006
18210
  entry.request++;
18211
+ if (method !== "stopSpeech") entry.parkedInput = false;
18007
18212
  if (method === "endConversation" && args[0] !== true || method === "stopConversationInput")
18008
18213
  voiceModeActive = false;
18009
18214
  const result = original(...args);
@@ -18108,7 +18313,6 @@ function apply(ctx) {
18108
18313
  entry.composers.delete(token.current);
18109
18314
  if (!entry.composers.size) suspendSessionSpeech(entry);
18110
18315
  if (meetingEntry === entry && !entry.composers.size) {
18111
- void meeting.attendee.stop();
18112
18316
  meeting.composerReplaced();
18113
18317
  meetingEntry = null;
18114
18318
  meeting.transcript.setActive("microphone", false);
@@ -18119,10 +18323,7 @@ function apply(ctx) {
18119
18323
  if (!entry || entry.closed || disposed) return;
18120
18324
  if (!input || typeof props.inputActions?.setDraft !== "function") return;
18121
18325
  if (meetingEntry !== entry) {
18122
- if (meetingEntry) {
18123
- void meeting.attendee.stop();
18124
- suspendSessionSpeech(meetingEntry);
18125
- }
18326
+ if (meetingEntry) suspendSessionSpeech(meetingEntry);
18126
18327
  meeting.composerReplaced();
18127
18328
  meetingEntry = entry;
18128
18329
  entry.controller.setExternalSpeechActivity(sharedSpeechActive);
@@ -18154,7 +18355,7 @@ function apply(ctx) {
18154
18355
  });
18155
18356
  const attachSpeech = !entry.speechAttached;
18156
18357
  if (attachSpeech) entry.speechAttached = true;
18157
- const inputReady = voiceModeActive && !entry.controller.getSnapshot().conversation ? entry.controller.startConversation() : Promise.resolve();
18358
+ const inputReady = !entry.parkedInput && voiceModeActive && !entry.controller.getSnapshot().conversation ? entry.controller.startConversation() : Promise.resolve();
18158
18359
  run(
18159
18360
  entry.controller,
18160
18361
  Promise.resolve(inputReady).then(() => {
package/lib/server.js CHANGED
@@ -2052,7 +2052,7 @@ var VAD_ASSET_TYPES = {
2052
2052
  };
2053
2053
  var MAX_VAD_ASSET_BYTES = 32 * 1024 * 1024;
2054
2054
  function registerVadAssetRoutes(ctx, options = {}) {
2055
- const version = options.version ?? (true ? "0.7.0" : "test");
2055
+ const version = options.version ?? (true ? "0.7.1" : "test");
2056
2056
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version))
2057
2057
  throw new Error("Invalid VAD asset version");
2058
2058
  const directory = options.directory ?? fileURLToPath(new URL("./vad/", import.meta.url));
@@ -2146,7 +2146,7 @@ import { lstat as lstat2, readFile as readFile5 } from "node:fs/promises";
2146
2146
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2147
2147
  import { join as join8 } from "node:path";
2148
2148
  function registerBrowserAsrAssets(ctx, options = {}) {
2149
- const version = options.version ?? (true ? "0.7.0" : "test");
2149
+ const version = options.version ?? (true ? "0.7.1" : "test");
2150
2150
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version)) throw new Error("invalid-version");
2151
2151
  const base = "/api/dsh-live-voice/asr/" + encodeURIComponent(version) + "/";
2152
2152
  const directory = options.directory ?? fileURLToPath2(new URL("./asr/", import.meta.url));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "pluginVersion": "0.7.0",
3
+ "pluginVersion": "0.7.1",
4
4
  "dependencies": {
5
5
  "@ricky0123/vad-web": "0.0.31",
6
6
  "onnxruntime-web": "1.22.0"
@@ -36,12 +36,12 @@
36
36
  "sha256": "2623a2953f6ff3d2c1e61740c6cdb7168133479b267dfef114a4a3cc5bdd788f"
37
37
  },
38
38
  "vad.capture.js": {
39
- "bytes": 1254,
40
- "sha256": "da75b304c7a67d2538b96c0310cb9d4d991a46ca495f246b19ab70ce71b9f26b"
39
+ "bytes": 1694,
40
+ "sha256": "d24df69dc6326d8c1b37437b0bef459a3a67405237108a3c32868a87c3fa13c4"
41
41
  },
42
42
  "vad.worker.js": {
43
43
  "bytes": 79337,
44
- "sha256": "16ae4a3a3d4fa13ad7e65e8a7ce220e816448c4b9899ebaa53a2ad4c287eecbb"
44
+ "sha256": "36a94fd4b60d3717d25628a033574d1b6c2fb576a8be732c87255ba567c33bd0"
45
45
  }
46
46
  }
47
47
  }
@@ -1,11 +1,22 @@
1
1
  // src/modules/recognition/vad/vad.capture.ts
2
2
  var VadCaptureProcessor = class extends AudioWorkletProcessor {
3
3
  stopped = false;
4
+ paused = false;
4
5
  pcm = new Float32Array(2048);
5
6
  at = 0;
6
7
  constructor() {
7
8
  super();
8
9
  this.port.onmessage = (event) => {
10
+ if (event.data?.type === "resume") this.paused = false;
11
+ if (event.data?.type === "flush") {
12
+ if (event.data.pause) this.paused = true;
13
+ if (this.at) {
14
+ const pcm = this.pcm.slice(0, this.at);
15
+ this.port.postMessage({ pcm, sampleRate }, [pcm.buffer]);
16
+ this.at = 0;
17
+ }
18
+ this.port.postMessage({ type: "flushed" });
19
+ }
9
20
  if (event.data?.type === "stop") {
10
21
  this.stopped = true;
11
22
  if (event.data.drain && this.at) {
@@ -20,6 +31,7 @@ var VadCaptureProcessor = class extends AudioWorkletProcessor {
20
31
  process(inputs, outputs) {
21
32
  for (const channels of outputs) for (const channel of channels) channel.fill(0);
22
33
  if (this.stopped) return false;
34
+ if (this.paused) return true;
23
35
  const input = inputs[0];
24
36
  if (!input?.length) return true;
25
37
  for (let i = 0; i < input[0].length; i++) {
@@ -1632,7 +1632,7 @@ var vo = Ke;
1632
1632
  Object.defineProperty(I.versions, "web", { value: $t, enumerable: true });
1633
1633
 
1634
1634
  // src/modules/recognition/vad/assets.ts
1635
- var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.0" : "test") + "/";
1635
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.1" : "test") + "/";
1636
1636
 
1637
1637
  // src/modules/recognition/vad/StreamingResampler.ts
1638
1638
  var StreamingResampler = class {