dsh-live-voice 0.6.0 → 0.6.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-alpha.2**. The package manifest's `dshTestedVersion` field records the tested version.
7
+ Tested with **DeepSeek Harness v0.2.1-alpha.3**. 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.6.0" : "test") + "/";
36685
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.6.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.6.0",
2
+ "version": "0.6.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.6.0" : "test") + "/";
155
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.6.1" : "test") + "/";
156
156
  async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
157
157
  try {
158
158
  const response = await fetchImpl(sileroVadAssetBase() + "availability", {
@@ -1734,8 +1734,10 @@ var en = {
1734
1734
  "dsh-live-voice.commons.update.cancel": "Cancel",
1735
1735
  "dsh-live-voice.commons.update.confirm": "Install Live Voice {version} in this DSH profile? Download requires network access. Restart DSH afterward when requested; nothing restarts automatically.",
1736
1736
  "dsh-live-voice.commons.update.current": "This version is already installed. Restart DSH to use it.",
1737
+ "dsh-live-voice.commons.update.delayed": "The requested version is not available to this profile yet. It may still be publishing or be delayed by pnpm minimumReleaseAge. Wait, then check the DSH plugin manager; no security policy was bypassed.",
1737
1738
  "dsh-live-voice.commons.update.done": "Live Voice {version} installed. Reload the page when ready.",
1738
1739
  "dsh-live-voice.commons.update.failed": "Update failed. The release may not be published on npm yet. Check the DSH plugin manager.",
1740
+ "dsh-live-voice.commons.update.incompatible": "This release is incompatible with the running DSH version. Check the DSH plugin manager and use a compatible DSH/plugin version; no compatibility exemption was granted.",
1739
1741
  "dsh-live-voice.commons.update.installing": "Updating\u2026",
1740
1742
  "dsh-live-voice.commons.update.label": "Update available",
1741
1743
  "dsh-live-voice.commons.update.link": "Update available: {version}. Open release",
@@ -2129,8 +2131,10 @@ var es = {
2129
2131
  "dsh-live-voice.commons.update.cancel": "Cancelar",
2130
2132
  "dsh-live-voice.commons.update.confirm": "\xBFInstalar Voz en vivo {version} en este perfil de DSH? La descarga requiere red. Reinicia DSH despu\xE9s si se solicita; nada se reinicia autom\xE1ticamente.",
2131
2133
  "dsh-live-voice.commons.update.current": "Esta versi\xF3n ya est\xE1 instalada. Reinicia DSH para usarla.",
2134
+ "dsh-live-voice.commons.update.delayed": "La versi\xF3n solicitada a\xFAn no est\xE1 disponible para este perfil. Puede estar public\xE1ndose o esperando el minimumReleaseAge de pnpm. Espera y consulta el gestor de plugins de DSH; no se omiti\xF3 ninguna pol\xEDtica de seguridad.",
2132
2135
  "dsh-live-voice.commons.update.done": "Voz en vivo {version} instalado. Recarga la p\xE1gina cuando quieras.",
2133
2136
  "dsh-live-voice.commons.update.failed": "La actualizaci\xF3n fall\xF3. Puede que la versi\xF3n a\xFAn no est\xE9 en npm. Revisa el gestor de plugins de DSH.",
2137
+ "dsh-live-voice.commons.update.incompatible": "Esta versi\xF3n es incompatible con el DSH en ejecuci\xF3n. Consulta el gestor de plugins y usa versiones compatibles de DSH y del plugin; no se concedi\xF3 ninguna excepci\xF3n.",
2134
2138
  "dsh-live-voice.commons.update.installing": "Actualizando\u2026",
2135
2139
  "dsh-live-voice.commons.update.label": "Actualizaci\xF3n disponible",
2136
2140
  "dsh-live-voice.commons.update.link": "Actualizaci\xF3n disponible: {version}. Abrir la p\xE1gina de esta versi\xF3n",
@@ -2524,8 +2528,10 @@ var fr = {
2524
2528
  "dsh-live-voice.commons.update.cancel": "Annuler",
2525
2529
  "dsh-live-voice.commons.update.confirm": "Installer Voix en direct {version} dans ce profil DSH ? Le t\xE9l\xE9chargement n\xE9cessite le r\xE9seau. Red\xE9marrez DSH ensuite si demand\xE9 ; rien ne red\xE9marre automatiquement.",
2526
2530
  "dsh-live-voice.commons.update.current": "Cette version est d\xE9j\xE0 install\xE9e. Red\xE9marrez DSH pour l\u2019utiliser.",
2531
+ "dsh-live-voice.commons.update.delayed": "La version demand\xE9e n\u2019est pas encore disponible pour ce profil. La publication ou le minimumReleaseAge de pnpm peut la retarder. Patientez puis consultez le gestionnaire DSH ; aucune r\xE8gle de s\xE9curit\xE9 n\u2019a \xE9t\xE9 contourn\xE9e.",
2527
2532
  "dsh-live-voice.commons.update.done": "Voix en direct {version} install\xE9. Rechargez la page quand vous le souhaitez.",
2528
2533
  "dsh-live-voice.commons.update.failed": "\xC9chec de mise \xE0 jour. La version n\u2019est peut-\xEAtre pas encore sur npm. Consultez le gestionnaire de plugins DSH.",
2534
+ "dsh-live-voice.commons.update.incompatible": "Cette version est incompatible avec le DSH en cours. Consultez le gestionnaire de plugins et utilisez des versions compatibles de DSH et du plugin ; aucune d\xE9rogation n\u2019a \xE9t\xE9 accord\xE9e.",
2529
2535
  "dsh-live-voice.commons.update.installing": "Mise \xE0 jour\u2026",
2530
2536
  "dsh-live-voice.commons.update.label": "Mise \xE0 jour disponible",
2531
2537
  "dsh-live-voice.commons.update.link": "Mise \xE0 jour disponible : {version}. Ouvrir la page de cette version",
@@ -2919,8 +2925,10 @@ var hi = {
2919
2925
  "dsh-live-voice.commons.update.cancel": "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
2920
2926
  "dsh-live-voice.commons.update.confirm": "\u0907\u0938 DSH \u092A\u094D\u0930\u094B\u092B\u093C\u093E\u0907\u0932 \u092E\u0947\u0902 \u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 {version} \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0915\u0930\u0947\u0902? \u0921\u093E\u0909\u0928\u0932\u094B\u0921 \u0915\u0947 \u0932\u093F\u090F \u0928\u0947\u091F\u0935\u0930\u094D\u0915 \u091A\u093E\u0939\u093F\u090F\u0964 \u0905\u0928\u0941\u0930\u094B\u0927 \u0939\u094B\u0928\u0947 \u092A\u0930 \u092C\u093E\u0926 \u092E\u0947\u0902 DSH \u092A\u0941\u0928\u0903 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902; \u0915\u0941\u091B \u092D\u0940 \u0905\u092A\u0928\u0947 \u0906\u092A \u092A\u0941\u0928\u0903 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B\u0917\u093E\u0964",
2921
2927
  "dsh-live-voice.commons.update.current": "\u092F\u0939 \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u092A\u0939\u0932\u0947 \u0938\u0947 \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0939\u0948\u0964 \u0909\u092A\u092F\u094B\u0917 \u0915\u0947 \u0932\u093F\u090F DSH \u092A\u0941\u0928\u0903 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902\u0964",
2928
+ "dsh-live-voice.commons.update.delayed": "\u092E\u093E\u0901\u0917\u093E \u0917\u092F\u093E \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u0905\u092D\u0940 \u0907\u0938 \u092A\u094D\u0930\u094B\u092B\u093C\u093E\u0907\u0932 \u0915\u0947 \u0932\u093F\u090F \u0909\u092A\u0932\u092C\u094D\u0927 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964 \u092A\u094D\u0930\u0915\u093E\u0936\u0928 \u091C\u093E\u0930\u0940 \u0939\u094B \u0938\u0915\u0924\u093E \u0939\u0948 \u092F\u093E pnpm \u0915\u093E minimumReleaseAge \u0935\u093F\u0932\u0902\u092C \u0915\u0930 \u0938\u0915\u0924\u093E \u0939\u0948\u0964 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0915\u0930\u0947\u0902, \u092B\u093F\u0930 DSH \u092A\u094D\u0932\u0917\u0907\u0928 \u092A\u094D\u0930\u092C\u0902\u0927\u0915 \u0926\u0947\u0916\u0947\u0902; \u0915\u094B\u0908 \u0938\u0941\u0930\u0915\u094D\u0937\u093E \u0928\u0940\u0924\u093F \u0928\u0939\u0940\u0902 \u092C\u0926\u0932\u0940 \u0917\u0908\u0964",
2922
2929
  "dsh-live-voice.commons.update.done": "\u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 {version} \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0939\u094B \u0917\u092F\u093E\u0964 \u0924\u0948\u092F\u093E\u0930 \u0939\u094B\u0928\u0947 \u092A\u0930 \u092A\u0947\u091C \u0930\u0940\u0932\u094B\u0921 \u0915\u0930\u0947\u0902\u0964",
2923
2930
  "dsh-live-voice.commons.update.failed": "\u0905\u092A\u0921\u0947\u091F \u0935\u093F\u092B\u0932\u0964 \u0930\u093F\u0932\u0940\u091C\u093C \u0905\u092D\u0940 npm \u092A\u0930 \u0909\u092A\u0932\u092C\u094D\u0927 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0924\u0940\u0964 DSH \u092A\u094D\u0932\u0917\u0907\u0928 \u092A\u094D\u0930\u092C\u0902\u0927\u0915 \u0926\u0947\u0916\u0947\u0902\u0964",
2931
+ "dsh-live-voice.commons.update.incompatible": "\u092F\u0939 \u0930\u093F\u0932\u0940\u091C\u093C \u091A\u0932 \u0930\u0939\u0947 DSH \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u0915\u0947 \u0938\u093E\u0925 \u0938\u0902\u0917\u0924 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964 DSH \u092A\u094D\u0932\u0917\u0907\u0928 \u092A\u094D\u0930\u092C\u0902\u0927\u0915 \u0926\u0947\u0916\u0947\u0902 \u0914\u0930 \u0938\u0902\u0917\u0924 DSH \u0924\u0925\u093E \u092A\u094D\u0932\u0917\u0907\u0928 \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u0909\u092A\u092F\u094B\u0917 \u0915\u0930\u0947\u0902; \u0915\u094B\u0908 \u0938\u0902\u0917\u0924\u0924\u093E \u091B\u0942\u091F \u0928\u0939\u0940\u0902 \u0926\u0940 \u0917\u0908\u0964",
2924
2932
  "dsh-live-voice.commons.update.installing": "\u0905\u092A\u0921\u0947\u091F \u0939\u094B \u0930\u0939\u093E \u0939\u0948\u2026",
2925
2933
  "dsh-live-voice.commons.update.label": "\u0905\u092A\u0921\u0947\u091F \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948",
2926
2934
  "dsh-live-voice.commons.update.link": "\u0905\u092A\u0921\u0947\u091F \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948: {version}\u0964 \u0930\u093F\u0932\u0940\u091C\u093C \u0916\u094B\u0932\u0947\u0902",
@@ -3314,8 +3322,10 @@ var ptBR = {
3314
3322
  "dsh-live-voice.commons.update.cancel": "Cancelar",
3315
3323
  "dsh-live-voice.commons.update.confirm": "Instalar Voz ao Vivo {version} neste perfil do DSH? O download precisa de rede. Reinicie o DSH depois, se solicitado; nada reinicia automaticamente.",
3316
3324
  "dsh-live-voice.commons.update.current": "Esta vers\xE3o j\xE1 est\xE1 instalada. Reinicie o DSH para utiliz\xE1-la.",
3325
+ "dsh-live-voice.commons.update.delayed": "A vers\xE3o solicitada ainda n\xE3o est\xE1 dispon\xEDvel para este perfil. A publica\xE7\xE3o pode estar em andamento ou aguardando o minimumReleaseAge do pnpm. Aguarde e consulte o gerenciador de plugins do DSH; nenhuma pol\xEDtica de seguran\xE7a foi ignorada.",
3317
3326
  "dsh-live-voice.commons.update.done": "Voz ao Vivo {version} instalado. Recarregue a p\xE1gina quando quiser.",
3318
3327
  "dsh-live-voice.commons.update.failed": "Falha na atualiza\xE7\xE3o. A vers\xE3o pode ainda n\xE3o estar no npm. Verifique o gerenciador de plugins do DSH.",
3328
+ "dsh-live-voice.commons.update.incompatible": "Esta vers\xE3o \xE9 incompat\xEDvel com o DSH em execu\xE7\xE3o. Consulte o gerenciador de plugins e use vers\xF5es compat\xEDveis do DSH e do plugin; nenhuma exce\xE7\xE3o de compatibilidade foi concedida.",
3319
3329
  "dsh-live-voice.commons.update.installing": "Atualizando\u2026",
3320
3330
  "dsh-live-voice.commons.update.label": "Atualiza\xE7\xE3o dispon\xEDvel",
3321
3331
  "dsh-live-voice.commons.update.link": "Atualiza\xE7\xE3o dispon\xEDvel: {version}. Abrir lan\xE7amento",
@@ -3709,8 +3719,10 @@ var zh = {
3709
3719
  "dsh-live-voice.commons.update.cancel": "\u53D6\u6D88",
3710
3720
  "dsh-live-voice.commons.update.confirm": "\u5728\u6B64 DSH \u914D\u7F6E\u4E2D\u5B89\u88C5 \u5B9E\u65F6\u8BED\u97F3 {version}\uFF1F\u4E0B\u8F7D\u9700\u8981\u7F51\u7EDC\u3002\u82E5\u63D0\u793A\uFF0C\u8BF7\u4E4B\u540E\u91CD\u542F DSH\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u542F\u3002",
3711
3721
  "dsh-live-voice.commons.update.current": "\u6B64\u7248\u672C\u5DF2\u5B89\u88C5\u3002\u8BF7\u91CD\u542F DSH \u540E\u4F7F\u7528\u3002",
3722
+ "dsh-live-voice.commons.update.delayed": "\u6B64\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u6240\u8BF7\u6C42\u7684\u7248\u672C\u3002\u5B83\u53EF\u80FD\u4ECD\u5728\u53D1\u5E03\uFF0C\u6216\u53D7 pnpm minimumReleaseAge \u9650\u5236\u3002\u8BF7\u7A0D\u540E\u68C0\u67E5 DSH \u63D2\u4EF6\u7BA1\u7406\u5668\uFF1B\u672A\u7ED5\u8FC7\u4EFB\u4F55\u5B89\u5168\u7B56\u7565\u3002",
3712
3723
  "dsh-live-voice.commons.update.done": "\u5B9E\u65F6\u8BED\u97F3 {version} \u5DF2\u5B89\u88C5\u3002\u51C6\u5907\u597D\u540E\u8BF7\u5237\u65B0\u9875\u9762\u3002",
3713
3724
  "dsh-live-voice.commons.update.failed": "\u66F4\u65B0\u5931\u8D25\u3002\u8BE5\u7248\u672C\u53EF\u80FD\u5C1A\u672A\u53D1\u5E03\u5230 npm\u3002\u8BF7\u67E5\u770B DSH \u63D2\u4EF6\u7BA1\u7406\u5668\u3002",
3725
+ "dsh-live-voice.commons.update.incompatible": "\u6B64\u7248\u672C\u4E0E\u6B63\u5728\u8FD0\u884C\u7684 DSH \u4E0D\u517C\u5BB9\u3002\u8BF7\u68C0\u67E5 DSH \u63D2\u4EF6\u7BA1\u7406\u5668\u5E76\u4F7F\u7528\u517C\u5BB9\u7684 DSH \u548C\u63D2\u4EF6\u7248\u672C\uFF1B\u672A\u6388\u4E88\u517C\u5BB9\u6027\u8C41\u514D\u3002",
3714
3726
  "dsh-live-voice.commons.update.installing": "\u6B63\u5728\u66F4\u65B0\u2026",
3715
3727
  "dsh-live-voice.commons.update.label": "\u6709\u53EF\u7528\u66F4\u65B0",
3716
3728
  "dsh-live-voice.commons.update.link": "\u6709\u53EF\u7528\u66F4\u65B0\uFF1A{version}\u3002\u6253\u5F00\u53D1\u5E03\u9875\u9762",
@@ -6896,6 +6908,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
6896
6908
  recognizing: false,
6897
6909
  pendingTranscriptions: 0,
6898
6910
  pendingTranscriptionRequests: void 0,
6911
+ pendingDeliveryTranscriptions: void 0,
6899
6912
  muted: normalizeSettings(settings).microphoneEnabled === false,
6900
6913
  speaking: false,
6901
6914
  paused: false,
@@ -7240,14 +7253,20 @@ var VoiceCoordinator = class _VoiceCoordinator {
7240
7253
  onResult: (result) => {
7241
7254
  if (valid()) this.onResult(result);
7242
7255
  },
7243
- onProcessingChange: ({ pending = 0, transcribing } = {}) => {
7256
+ onProcessingChange: ({ pending = 0, transcribing, deliveryPending } = {}) => {
7244
7257
  if (!valid()) return;
7245
7258
  const count = Number.isSafeInteger(pending) && pending >= 0 ? pending : 0;
7246
7259
  const requests = Number.isSafeInteger(transcribing) && transcribing >= 0 ? transcribing : count;
7247
- this.patch({ pendingTranscriptions: count, pendingTranscriptionRequests: requests });
7248
- if (count > 0) this.cancelAutoSend({ preserveIntent: true });
7249
- else if (this.holdToTalkRelease) this._finishHoldToTalk();
7250
- else this.maybeScheduleAutoSend();
7260
+ const delivery = Number.isSafeInteger(deliveryPending) && deliveryPending >= 0 ? deliveryPending : count;
7261
+ this.patch({
7262
+ pendingTranscriptions: count,
7263
+ pendingTranscriptionRequests: requests,
7264
+ pendingDeliveryTranscriptions: delivery
7265
+ });
7266
+ if (delivery > 0) this.cancelAutoSend({ preserveIntent: true });
7267
+ else if (this.holdToTalkRelease) {
7268
+ if (count === 0) this._finishHoldToTalk();
7269
+ } else this.maybeScheduleAutoSend();
7251
7270
  if (count === 0) this._drain();
7252
7271
  },
7253
7272
  onActivity: (active) => {
@@ -7448,15 +7467,18 @@ var VoiceCoordinator = class _VoiceCoordinator {
7448
7467
  this._handleSpeechInterruption(this._speechActivity());
7449
7468
  this._drain();
7450
7469
  }
7470
+ deliveryPending() {
7471
+ return this.snapshot.pendingDeliveryTranscriptions ?? this.snapshot.pendingTranscriptions;
7472
+ }
7451
7473
  maybeScheduleAutoSend(draft = this.autoSendIntent) {
7452
7474
  if (draft == null || draft !== this.composer.getDraft()) return;
7453
7475
  if (this.autoSendTimer !== null) return;
7454
- if (this.snapshot.settings.sendingMode === "manual" || this.snapshot.muted || this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.snapshot.pendingTranscriptions > 0)
7476
+ if (this.snapshot.settings.sendingMode === "manual" || this.snapshot.muted || this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.deliveryPending() > 0)
7455
7477
  return;
7456
7478
  this.scheduleAutoSend(draft);
7457
7479
  }
7458
7480
  refreshDeliveryReadiness() {
7459
- if (this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.snapshot.pendingTranscriptions > 0)
7481
+ if (this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.deliveryPending() > 0)
7460
7482
  this.cancelAutoSend({ preserveIntent: true });
7461
7483
  else this.maybeScheduleAutoSend();
7462
7484
  }
@@ -7513,6 +7535,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
7513
7535
  recognizing: false,
7514
7536
  pendingTranscriptions: 0,
7515
7537
  pendingTranscriptionRequests: 0,
7538
+ pendingDeliveryTranscriptions: 0,
7516
7539
  starting: false
7517
7540
  });
7518
7541
  return this._input(async () => {
@@ -8089,6 +8112,8 @@ var MeetingController = class {
8089
8112
  if (Object.hasOwn(next, "pending")) policy.pendingTranscriptions = next.pending;
8090
8113
  if (Object.hasOwn(next, "transcribing"))
8091
8114
  policy.pendingTranscriptionRequests = next.transcribing;
8115
+ if (Object.hasOwn(next, "deliveryPending"))
8116
+ policy.pendingDeliveryTranscriptions = next.deliveryPending;
8092
8117
  input?.patch(policy);
8093
8118
  }
8094
8119
  this.snapshot = {
@@ -8261,11 +8286,12 @@ var MeetingController = class {
8261
8286
  this.onActivity?.(source, active);
8262
8287
  input.refreshDeliveryReadiness();
8263
8288
  },
8264
- onProcessingChange: ({ pending, transcribing }) => {
8289
+ onProcessingChange: ({ pending, transcribing, deliveryPending }) => {
8265
8290
  if (!valid()) return;
8266
8291
  const requests = Number.isSafeInteger(transcribing) && transcribing >= 0 ? transcribing : pending;
8267
- this.patch(source, { pending, transcribing: requests });
8268
- if (pending) input.cancelAutoSend({ preserveIntent: true });
8292
+ const delivery = Number.isSafeInteger(deliveryPending) && deliveryPending >= 0 ? deliveryPending : pending;
8293
+ this.patch(source, { pending, transcribing: requests, deliveryPending: delivery });
8294
+ if (delivery) input.cancelAutoSend({ preserveIntent: true });
8269
8295
  else if (!input.getSnapshot().muted) input.maybeScheduleAutoSend();
8270
8296
  },
8271
8297
  onError: (error) => {
@@ -8308,12 +8334,19 @@ var MeetingController = class {
8308
8334
  listening: false,
8309
8335
  recognizing: false,
8310
8336
  pendingTranscriptions: 0,
8311
- pendingTranscriptionRequests: 0
8337
+ pendingTranscriptionRequests: 0,
8338
+ pendingDeliveryTranscriptions: 0
8312
8339
  });
8313
8340
  this.onActivity?.(source, false);
8314
8341
  job?.abort.abort();
8315
8342
  this.transcript.setActive(source, false);
8316
- this.patch(source, { starting: false, listening: false, pending: 0, transcribing: 0 });
8343
+ this.patch(source, {
8344
+ starting: false,
8345
+ listening: false,
8346
+ pending: 0,
8347
+ transcribing: 0,
8348
+ deliveryPending: 0
8349
+ });
8317
8350
  await job?.engine?.stop();
8318
8351
  await job?.meter?.stop();
8319
8352
  }
@@ -9562,6 +9595,7 @@ async function startRecordingRecognition(engine, options, capture) {
9562
9595
  onProcessingChange,
9563
9596
  tail: Promise.resolve(),
9564
9597
  pendingWrites: 0,
9598
+ pendingSpeechWrites: 0,
9565
9599
  lang,
9566
9600
  operation: uid(),
9567
9601
  capturedSamples: 0,
@@ -9584,7 +9618,9 @@ async function startRecordingRecognition(engine, options, capture) {
9584
9618
  active: !!session.activeRequest,
9585
9619
  // Only queued/running speech attempts belong in the visible transcription status.
9586
9620
  transcribing: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0),
9587
- // Audio writes and final drain still block delivery/release, even in silence.
9621
+ // Speech-boundary bookkeeping must settle before delivery, but silent PCM writes must not reset it.
9622
+ deliveryPending: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0) + session.pendingSpeechWrites + (session.finishing ? 1 : 0),
9623
+ // All writes still protect release, playback admission and page leave.
9588
9624
  pending: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0) + session.pendingWrites + (session.finishing ? 1 : 0)
9589
9625
  });
9590
9626
  };
@@ -9594,14 +9630,16 @@ async function startRecordingRecognition(engine, options, capture) {
9594
9630
  onError?.(new Error(engine.recordingErrorMessage(error)));
9595
9631
  void engine.stop();
9596
9632
  };
9597
- const serialize = (task, preserve = false) => {
9633
+ const serialize = (task, preserve = false, speech = false) => {
9598
9634
  session.pendingWrites++;
9635
+ if (speech) session.pendingSpeechWrites++;
9599
9636
  notify2();
9600
9637
  const work = session.tail.then(async () => {
9601
9638
  if (preserve || valid()) await task();
9602
9639
  });
9603
9640
  session.tail = work.catch(failure).finally(() => {
9604
9641
  session.pendingWrites--;
9642
+ if (speech) session.pendingSpeechWrites--;
9605
9643
  notify2();
9606
9644
  });
9607
9645
  return work;
@@ -9621,10 +9659,14 @@ async function startRecordingRecognition(engine, options, capture) {
9621
9659
  if (through - Math.max(liveSpeechStart, lastSplitQueued) < max) return;
9622
9660
  const end = Math.max(liveSpeechStart, lastSplitQueued) + max;
9623
9661
  lastSplitQueued = end;
9624
- void serialize(async () => {
9625
- const speech = [...writer.session.pins].reverse().find((p) => p.kind === "speech-start");
9626
- if (speech && valid()) await queueSpeech(speech, end);
9627
- }).catch(() => {
9662
+ void serialize(
9663
+ async () => {
9664
+ const speech = [...writer.session.pins].reverse().find((p) => p.kind === "speech-start");
9665
+ if (speech && valid()) await queueSpeech(speech, end);
9666
+ },
9667
+ false,
9668
+ true
9669
+ ).catch(() => {
9628
9670
  });
9629
9671
  };
9630
9672
  const queueSpeech = async (pin2, end) => {
@@ -9665,14 +9707,18 @@ async function startRecordingRecognition(engine, options, capture) {
9665
9707
  liveSpeechStart = startSample;
9666
9708
  lastSplitQueued = startSample;
9667
9709
  } else liveSpeechStart = null;
9668
- void serialize(async () => {
9669
- const previous = writer.session.pins.at(-1);
9670
- if (kind === "speech-start" && previous?.kind === "silence-start" && previous.atSample > 0 && startSample - previous.atSample >= paragraphSamples)
9671
- paragraphStarts.add(startSample);
9672
- await writer.pin(kind, startSample, confirmedSample);
9673
- if (valid() && kind === "silence-start" && previous?.kind === "speech-start")
9674
- await queueSpeech(previous, startSample);
9675
- }, true).catch(() => {
9710
+ void serialize(
9711
+ async () => {
9712
+ const previous = writer.session.pins.at(-1);
9713
+ if (kind === "speech-start" && previous?.kind === "silence-start" && previous.atSample > 0 && startSample - previous.atSample >= paragraphSamples)
9714
+ paragraphStarts.add(startSample);
9715
+ await writer.pin(kind, startSample, confirmedSample);
9716
+ if (valid() && kind === "silence-start" && previous?.kind === "speech-start")
9717
+ await queueSpeech(previous, startSample);
9718
+ },
9719
+ true,
9720
+ true
9721
+ ).catch(() => {
9676
9722
  });
9677
9723
  };
9678
9724
  const detector = new PinDetector({
@@ -10053,7 +10099,13 @@ var WhisperHttpRecognitionEngine = class {
10053
10099
  this.session = null;
10054
10100
  session.signal?.removeEventListener("abort", session.abort);
10055
10101
  await session.stopRecording();
10056
- session.onProcessingChange?.({ queued: 0, active: false, transcribing: 0, pending: 0 });
10102
+ session.onProcessingChange?.({
10103
+ queued: 0,
10104
+ active: false,
10105
+ transcribing: 0,
10106
+ deliveryPending: 0,
10107
+ pending: 0
10108
+ });
10057
10109
  }
10058
10110
  };
10059
10111
 
@@ -10532,8 +10584,8 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
10532
10584
  var import_react36 = __toESM(require("react"), 1);
10533
10585
 
10534
10586
  // src/modules/settings/services/releases.ts
10535
- var CURRENT_VERSION = "0.6.0";
10536
- var TESTED_DSH_VERSION = "0.2.1-alpha.2";
10587
+ var CURRENT_VERSION = "0.6.1";
10588
+ var TESTED_DSH_VERSION = "0.2.1-alpha.3";
10537
10589
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
10538
10590
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
10539
10591
  var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
@@ -13057,7 +13109,9 @@ function createPluginUpdate({
13057
13109
  if (!result?.ok) return patch({ ...state, phase: "uncertain", problem: "uncertain" });
13058
13110
  const value = result.value;
13059
13111
  if (value.application === "failed" || value.application === "cancelled")
13060
- return fail(value.pendingBuilds?.length ? "scripts" : "failed");
13112
+ return fail(
13113
+ value.error?.code === "incompatible-version" || value.packageResult?.incompatible?.length ? "incompatible" : value.pendingBuilds?.length || value.packageResult?.kind === "build-blocked" ? "scripts" : ["not-found", "no-matching-version"].includes(value.packageResult?.kind) ? "delayed" : "failed"
13114
+ );
13061
13115
  if (value.bundle !== "dsh-live-voice" || value.version !== state.version || !["applied", "restart-required"].includes(value.application))
13062
13116
  return patch({ ...state, phase: "uncertain", problem: "uncertain" });
13063
13117
  patch({ ...state, phase: value.application === "restart-required" ? "restart" : "done" });
@@ -13270,7 +13324,7 @@ async function removeBrowserModel(key, storage = caches) {
13270
13324
  }
13271
13325
 
13272
13326
  // src/modules/recognition/local/assets.ts
13273
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.6.0" : "test") + "/";
13327
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.6.1" : "test") + "/";
13274
13328
 
13275
13329
  // src/modules/recognition/local/runtimeIdentity.ts
13276
13330
  var workerProtocol = "asr-worker-v3-ort-1.31.0-dev.20260914-8d85527a0";
@@ -14008,7 +14062,7 @@ function apply(ctx) {
14008
14062
  },
14009
14063
  updatePlaybackSettings: (next) => meetingEntry?.controller.updateSettings(next),
14010
14064
  handleQuestionResult: (result) => meetingEntry?.controller.composer.handleQuestionResult?.(result),
14011
- canAutoSend: () => !!meetingEntry && !meetingEntry.controller.getSnapshot().recognizing && !meetingEntry.controller.getSnapshot().pendingTranscriptions
14065
+ canAutoSend: () => !!meetingEntry && !meetingEntry.controller.getSnapshot().recognizing && !meetingEntry.controller.deliveryPending()
14012
14066
  },
14013
14067
  settings: () => preferences.getSnapshot(),
14014
14068
  savePreferences: (controller, next) => savePreferences(controller, next),
@@ -14367,7 +14421,7 @@ function apply(ctx) {
14367
14421
  meter,
14368
14422
  composer: {
14369
14423
  getDraft: () => entry.draft,
14370
- canAutoSend: () => !meeting.getSnapshot().shared.pending && !meeting.input().getSnapshot().recognizing,
14424
+ canAutoSend: () => !meeting.input().deliveryPending() && !meeting.input().getSnapshot().recognizing,
14371
14425
  appendFinal: (text, startedAt, paragraphBefore) => {
14372
14426
  meeting.cancelDelivery();
14373
14427
  if (meetingEntry === entry && (entry.meeting?.getSnapshot().shared.listening || entry.controller.getSnapshot().timestamps)) {
package/lib/server.js CHANGED
@@ -996,7 +996,7 @@ var VAD_ASSET_TYPES = {
996
996
  };
997
997
  var MAX_VAD_ASSET_BYTES = 32 * 1024 * 1024;
998
998
  function registerVadAssetRoutes(ctx, options = {}) {
999
- const version = options.version ?? (true ? "0.6.0" : "test");
999
+ const version = options.version ?? (true ? "0.6.1" : "test");
1000
1000
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version))
1001
1001
  throw new Error("Invalid VAD asset version");
1002
1002
  const directory = options.directory ?? fileURLToPath(new URL("./vad/", import.meta.url));
@@ -1090,7 +1090,7 @@ import { lstat as lstat2, readFile as readFile5 } from "node:fs/promises";
1090
1090
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1091
1091
  import { join as join7 } from "node:path";
1092
1092
  function registerBrowserAsrAssets(ctx, options = {}) {
1093
- const version = options.version ?? (true ? "0.6.0" : "test");
1093
+ const version = options.version ?? (true ? "0.6.1" : "test");
1094
1094
  if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version)) throw new Error("invalid-version");
1095
1095
  const base = "/api/dsh-live-voice/asr/" + encodeURIComponent(version) + "/";
1096
1096
  const directory = options.directory ?? fileURLToPath2(new URL("./asr/", import.meta.url));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "pluginVersion": "0.6.0",
3
+ "pluginVersion": "0.6.1",
4
4
  "dependencies": {
5
5
  "@ricky0123/vad-web": "0.0.31",
6
6
  "onnxruntime-web": "1.22.0"
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "vad.worker.js": {
43
43
  "bytes": 79337,
44
- "sha256": "10f6b6eab959345681c5f3014d4f4fdb87900d81e3308b12fd93066344778257"
44
+ "sha256": "8408341e3f7ea6773036bed3d002fcb0a037af3e6cd2a419477eb67a8e18fde4"
45
45
  }
46
46
  }
47
47
  }
@@ -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.6.0" : "test") + "/";
1635
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.6.1" : "test") + "/";
1636
1636
 
1637
1637
  // src/modules/recognition/vad/StreamingResampler.ts
1638
1638
  var StreamingResampler = class {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-live-voice",
3
- "version": "0.6.0",
4
- "dshTestedVersion": "0.2.1-alpha.2",
3
+ "version": "0.6.1",
4
+ "dshTestedVersion": "0.2.1-alpha.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "node --experimental-strip-types ../../scripts/build.ts --package live-voice",
@@ -92,12 +92,14 @@
92
92
  }
93
93
  },
94
94
  "dependencies": {
95
+ "@wads.dev/i18n-react": "^0.0.1-alpha.0",
96
+ "@wads.dev/i18n-ts": "0.0.1-alpha.0",
97
+ "marked": "^18.1.0"
98
+ },
99
+ "devDependencies": {
95
100
  "@dlv/ort-local": "npm:onnxruntime-web@1.31.0-dev.20260914-8d85527a0",
96
101
  "@huggingface/transformers": "4.3.1",
97
102
  "@ricky0123/vad-web": "0.0.31",
98
- "@wads.dev/i18n-react": "^0.0.1-alpha.0",
99
- "@wads.dev/i18n-ts": "0.0.1-alpha.0",
100
- "marked": "^18.1.0",
101
103
  "onnxruntime-web": "1.22.0"
102
104
  }
103
105
  }
@@ -106,7 +106,7 @@ export function apply(ctx) {
106
106
  canAutoSend: () =>
107
107
  !!meetingEntry &&
108
108
  !meetingEntry.controller.getSnapshot().recognizing &&
109
- !meetingEntry.controller.getSnapshot().pendingTranscriptions,
109
+ !meetingEntry.controller.deliveryPending(),
110
110
  },
111
111
  settings: () => preferences.getSnapshot(),
112
112
  savePreferences: (controller, next) => savePreferences(controller, next),
@@ -521,7 +521,7 @@ export function apply(ctx) {
521
521
  composer: {
522
522
  getDraft: () => entry.draft,
523
523
  canAutoSend: () =>
524
- !meeting.getSnapshot().shared.pending && !meeting.input().getSnapshot().recognizing,
524
+ !meeting.input().deliveryPending() && !meeting.input().getSnapshot().recognizing,
525
525
  appendFinal: (text, startedAt, paragraphBefore) => {
526
526
  meeting.cancelDelivery();
527
527
  if (
@@ -41,8 +41,10 @@ export interface LiveVoiceTranslation extends Translation {
41
41
  'dsh-live-voice.commons.update.cancel': string;
42
42
  'dsh-live-voice.commons.update.confirm': string;
43
43
  'dsh-live-voice.commons.update.current': string;
44
+ 'dsh-live-voice.commons.update.delayed': string;
44
45
  'dsh-live-voice.commons.update.done': string;
45
46
  'dsh-live-voice.commons.update.failed': string;
47
+ 'dsh-live-voice.commons.update.incompatible': string;
46
48
  'dsh-live-voice.commons.update.installing': string;
47
49
  'dsh-live-voice.commons.update.label': string;
48
50
  'dsh-live-voice.commons.update.link': string;
@@ -43,10 +43,14 @@ const en: LiveVoiceTranslation = {
43
43
  'Install Live Voice {version} in this DSH profile? Download requires network access. Restart DSH afterward when requested; nothing restarts automatically.',
44
44
  'dsh-live-voice.commons.update.current':
45
45
  'This version is already installed. Restart DSH to use it.',
46
+ 'dsh-live-voice.commons.update.delayed':
47
+ 'The requested version is not available to this profile yet. It may still be publishing or be delayed by pnpm minimumReleaseAge. Wait, then check the DSH plugin manager; no security policy was bypassed.',
46
48
  'dsh-live-voice.commons.update.done':
47
49
  'Live Voice {version} installed. Reload the page when ready.',
48
50
  'dsh-live-voice.commons.update.failed':
49
51
  'Update failed. The release may not be published on npm yet. Check the DSH plugin manager.',
52
+ 'dsh-live-voice.commons.update.incompatible':
53
+ 'This release is incompatible with the running DSH version. Check the DSH plugin manager and use a compatible DSH/plugin version; no compatibility exemption was granted.',
50
54
  'dsh-live-voice.commons.update.installing': 'Updating…',
51
55
  'dsh-live-voice.commons.update.label': 'Update available',
52
56
  'dsh-live-voice.commons.update.link': 'Update available: {version}. Open release',
@@ -44,10 +44,14 @@ const es: LiveVoiceTranslation = {
44
44
  '¿Instalar Voz en vivo {version} en este perfil de DSH? La descarga requiere red. Reinicia DSH después si se solicita; nada se reinicia automáticamente.',
45
45
  'dsh-live-voice.commons.update.current':
46
46
  'Esta versión ya está instalada. Reinicia DSH para usarla.',
47
+ 'dsh-live-voice.commons.update.delayed':
48
+ 'La versión solicitada aún no está disponible para este perfil. Puede estar publicándose o esperando el minimumReleaseAge de pnpm. Espera y consulta el gestor de plugins de DSH; no se omitió ninguna política de seguridad.',
47
49
  'dsh-live-voice.commons.update.done':
48
50
  'Voz en vivo {version} instalado. Recarga la página cuando quieras.',
49
51
  'dsh-live-voice.commons.update.failed':
50
52
  'La actualización falló. Puede que la versión aún no esté en npm. Revisa el gestor de plugins de DSH.',
53
+ 'dsh-live-voice.commons.update.incompatible':
54
+ 'Esta versión es incompatible con el DSH en ejecución. Consulta el gestor de plugins y usa versiones compatibles de DSH y del plugin; no se concedió ninguna excepción.',
51
55
  'dsh-live-voice.commons.update.installing': 'Actualizando…',
52
56
  'dsh-live-voice.commons.update.label': 'Actualización disponible',
53
57
  'dsh-live-voice.commons.update.link':
@@ -45,10 +45,14 @@ const fr: LiveVoiceTranslation = {
45
45
  'Installer Voix en direct {version} dans ce profil DSH ? Le téléchargement nécessite le réseau. Redémarrez DSH ensuite si demandé ; rien ne redémarre automatiquement.',
46
46
  'dsh-live-voice.commons.update.current':
47
47
  'Cette version est déjà installée. Redémarrez DSH pour l’utiliser.',
48
+ 'dsh-live-voice.commons.update.delayed':
49
+ 'La version demandée n’est pas encore disponible pour ce profil. La publication ou le minimumReleaseAge de pnpm peut la retarder. Patientez puis consultez le gestionnaire DSH ; aucune règle de sécurité n’a été contournée.',
48
50
  'dsh-live-voice.commons.update.done':
49
51
  'Voix en direct {version} installé. Rechargez la page quand vous le souhaitez.',
50
52
  'dsh-live-voice.commons.update.failed':
51
53
  'Échec de mise à jour. La version n’est peut-être pas encore sur npm. Consultez le gestionnaire de plugins DSH.',
54
+ 'dsh-live-voice.commons.update.incompatible':
55
+ 'Cette version est incompatible avec le DSH en cours. Consultez le gestionnaire de plugins et utilisez des versions compatibles de DSH et du plugin ; aucune dérogation n’a été accordée.',
52
56
  'dsh-live-voice.commons.update.installing': 'Mise à jour…',
53
57
  'dsh-live-voice.commons.update.label': 'Mise à jour disponible',
54
58
  'dsh-live-voice.commons.update.link':
@@ -43,10 +43,14 @@ const hi: LiveVoiceTranslation = {
43
43
  'इस DSH प्रोफ़ाइल में लाइव वॉइस {version} इंस्टॉल करें? डाउनलोड के लिए नेटवर्क चाहिए। अनुरोध होने पर बाद में DSH पुनः शुरू करें; कुछ भी अपने आप पुनः शुरू नहीं होगा।',
44
44
  'dsh-live-voice.commons.update.current':
45
45
  'यह संस्करण पहले से इंस्टॉल है। उपयोग के लिए DSH पुनः शुरू करें।',
46
+ 'dsh-live-voice.commons.update.delayed':
47
+ 'माँगा गया संस्करण अभी इस प्रोफ़ाइल के लिए उपलब्ध नहीं है। प्रकाशन जारी हो सकता है या pnpm का minimumReleaseAge विलंब कर सकता है। प्रतीक्षा करें, फिर DSH प्लगइन प्रबंधक देखें; कोई सुरक्षा नीति नहीं बदली गई।',
46
48
  'dsh-live-voice.commons.update.done':
47
49
  'लाइव वॉइस {version} इंस्टॉल हो गया। तैयार होने पर पेज रीलोड करें।',
48
50
  'dsh-live-voice.commons.update.failed':
49
51
  'अपडेट विफल। रिलीज़ अभी npm पर उपलब्ध नहीं हो सकती। DSH प्लगइन प्रबंधक देखें।',
52
+ 'dsh-live-voice.commons.update.incompatible':
53
+ 'यह रिलीज़ चल रहे DSH संस्करण के साथ संगत नहीं है। DSH प्लगइन प्रबंधक देखें और संगत DSH तथा प्लगइन संस्करण उपयोग करें; कोई संगतता छूट नहीं दी गई।',
50
54
  'dsh-live-voice.commons.update.installing': 'अपडेट हो रहा है…',
51
55
  'dsh-live-voice.commons.update.label': 'अपडेट उपलब्ध है',
52
56
  'dsh-live-voice.commons.update.link': 'अपडेट उपलब्ध है: {version}। रिलीज़ खोलें',
@@ -44,10 +44,14 @@ const ptBR: LiveVoiceTranslation = {
44
44
  'Instalar Voz ao Vivo {version} neste perfil do DSH? O download precisa de rede. Reinicie o DSH depois, se solicitado; nada reinicia automaticamente.',
45
45
  'dsh-live-voice.commons.update.current':
46
46
  'Esta versão já está instalada. Reinicie o DSH para utilizá-la.',
47
+ 'dsh-live-voice.commons.update.delayed':
48
+ 'A versão solicitada ainda não está disponível para este perfil. A publicação pode estar em andamento ou aguardando o minimumReleaseAge do pnpm. Aguarde e consulte o gerenciador de plugins do DSH; nenhuma política de segurança foi ignorada.',
47
49
  'dsh-live-voice.commons.update.done':
48
50
  'Voz ao Vivo {version} instalado. Recarregue a página quando quiser.',
49
51
  'dsh-live-voice.commons.update.failed':
50
52
  'Falha na atualização. A versão pode ainda não estar no npm. Verifique o gerenciador de plugins do DSH.',
53
+ 'dsh-live-voice.commons.update.incompatible':
54
+ 'Esta versão é incompatível com o DSH em execução. Consulte o gerenciador de plugins e use versões compatíveis do DSH e do plugin; nenhuma exceção de compatibilidade foi concedida.',
51
55
  'dsh-live-voice.commons.update.installing': 'Atualizando…',
52
56
  'dsh-live-voice.commons.update.label': 'Atualização disponível',
53
57
  'dsh-live-voice.commons.update.link': 'Atualização disponível: {version}. Abrir lançamento',
@@ -42,9 +42,13 @@ const zh: LiveVoiceTranslation = {
42
42
  'dsh-live-voice.commons.update.confirm':
43
43
  '在此 DSH 配置中安装 实时语音 {version}?下载需要网络。若提示,请之后重启 DSH;不会自动重启。',
44
44
  'dsh-live-voice.commons.update.current': '此版本已安装。请重启 DSH 后使用。',
45
+ 'dsh-live-voice.commons.update.delayed':
46
+ '此配置暂时无法获取所请求的版本。它可能仍在发布,或受 pnpm minimumReleaseAge 限制。请稍后检查 DSH 插件管理器;未绕过任何安全策略。',
45
47
  'dsh-live-voice.commons.update.done': '实时语音 {version} 已安装。准备好后请刷新页面。',
46
48
  'dsh-live-voice.commons.update.failed':
47
49
  '更新失败。该版本可能尚未发布到 npm。请查看 DSH 插件管理器。',
50
+ 'dsh-live-voice.commons.update.incompatible':
51
+ '此版本与正在运行的 DSH 不兼容。请检查 DSH 插件管理器并使用兼容的 DSH 和插件版本;未授予兼容性豁免。',
48
52
  'dsh-live-voice.commons.update.installing': '正在更新…',
49
53
  'dsh-live-voice.commons.update.label': '有可用更新',
50
54
  'dsh-live-voice.commons.update.link': '有可用更新:{version}。打开发布页面',
@@ -1,6 +1,6 @@
1
1
  # Conversation module
2
2
 
3
- Transcription status separates detected speech (`recognizing`) from unsettled speech requests (`pendingTranscriptionRequests`, with legacy fallback to `pendingTranscriptions`). Aggregate `pendingTranscriptions` also includes storage/final-drain work for safety barriers; it must not be mistaken for visible transcription activity. Statuses are listening, listening and transcribing, waiting without speech, transcribing queued work without speech, and waiting for a command in Ignore mode. Playback remains separate; question and delivery countdown states retain their policy. Status copy has no ellipses. The compact recording indicator precedes the waveform and shows the committed audio frontier as seconds/minutes/unbounded hours, never wall-clock elapsed time; its saved-audio explanation is available only in the localized accessible name/tooltip.
3
+ Transcription status separates detected speech (`recognizing`) from unsettled speech requests (`pendingTranscriptionRequests`, with legacy fallback to `pendingTranscriptions`). Aggregate `pendingTranscriptions` also includes storage/final-drain work for safety barriers; it must not be mistaken for visible transcription activity. `pendingDeliveryTranscriptions` isolates speech-boundary bookkeeping and queued/active speech work from silent recording writes, so automatic-send countdowns in either source cannot starve on periodic persistence. Cross-source delivery gates use the same distinction; missing fields retain conservative legacy blocking. Statuses are listening, listening and transcribing, waiting without speech, transcribing queued work without speech, and waiting for a command in Ignore mode. Playback remains separate; question and delivery countdown states retain their policy. Status copy has no ellipses. The compact recording indicator precedes the waveform and shows the committed audio frontier as seconds/minutes/unbounded hours, never wall-clock elapsed time; its saved-audio explanation is available only in the localized accessible name/tooltip.
4
4
 
5
5
  Final recognition chunks join with a space. A confirmed paragraph boundary adds exactly two newlines; source changes also start a new block. Timestamp/source prefixes appear only at the first block, source change or paragraph. Recognition completion time and normal source activity/settings updates do not create timestamp boundaries. Composer text is append-only; user edits remain untouched.
6
6
 
@@ -51,6 +51,8 @@ export class MeetingController {
51
51
  if (Object.hasOwn(next, 'pending')) policy.pendingTranscriptions = next.pending;
52
52
  if (Object.hasOwn(next, 'transcribing'))
53
53
  policy.pendingTranscriptionRequests = next.transcribing;
54
+ if (Object.hasOwn(next, 'deliveryPending'))
55
+ policy.pendingDeliveryTranscriptions = next.deliveryPending;
54
56
  input?.patch(policy);
55
57
  }
56
58
  this.snapshot = {
@@ -235,12 +237,16 @@ export class MeetingController {
235
237
  this.onActivity?.(source, active);
236
238
  input.refreshDeliveryReadiness();
237
239
  },
238
- onProcessingChange: ({ pending, transcribing }) => {
240
+ onProcessingChange: ({ pending, transcribing, deliveryPending }) => {
239
241
  if (!valid()) return;
240
242
  const requests =
241
243
  Number.isSafeInteger(transcribing) && transcribing >= 0 ? transcribing : pending;
242
- this.patch(source, { pending, transcribing: requests });
243
- if (pending) input.cancelAutoSend({ preserveIntent: true });
244
+ const delivery =
245
+ Number.isSafeInteger(deliveryPending) && deliveryPending >= 0
246
+ ? deliveryPending
247
+ : pending;
248
+ this.patch(source, { pending, transcribing: requests, deliveryPending: delivery });
249
+ if (delivery) input.cancelAutoSend({ preserveIntent: true });
244
250
  else if (!input.getSnapshot().muted) input.maybeScheduleAutoSend();
245
251
  },
246
252
  onError: (error) => {
@@ -292,11 +298,18 @@ export class MeetingController {
292
298
  recognizing: false,
293
299
  pendingTranscriptions: 0,
294
300
  pendingTranscriptionRequests: 0,
301
+ pendingDeliveryTranscriptions: 0,
295
302
  });
296
303
  this.onActivity?.(source, false);
297
304
  job?.abort.abort();
298
305
  this.transcript.setActive(source, false);
299
- this.patch(source, { starting: false, listening: false, pending: 0, transcribing: 0 });
306
+ this.patch(source, {
307
+ starting: false,
308
+ listening: false,
309
+ pending: 0,
310
+ transcribing: 0,
311
+ deliveryPending: 0,
312
+ });
300
313
  await job?.engine?.stop();
301
314
  await job?.meter?.stop();
302
315
  }
@@ -56,6 +56,7 @@ export class VoiceCoordinator {
56
56
  recognizing: false,
57
57
  pendingTranscriptions: 0,
58
58
  pendingTranscriptionRequests: undefined,
59
+ pendingDeliveryTranscriptions: undefined,
59
60
  muted: normalizeSettings(settings).microphoneEnabled === false,
60
61
  speaking: false,
61
62
  paused: false,
@@ -440,15 +441,24 @@ export class VoiceCoordinator {
440
441
  onResult: (result) => {
441
442
  if (valid()) this.onResult(result);
442
443
  },
443
- onProcessingChange: ({ pending = 0, transcribing } = {}) => {
444
+ onProcessingChange: ({ pending = 0, transcribing, deliveryPending } = {}) => {
444
445
  if (!valid()) return;
445
446
  const count = Number.isSafeInteger(pending) && pending >= 0 ? pending : 0;
446
447
  const requests =
447
448
  Number.isSafeInteger(transcribing) && transcribing >= 0 ? transcribing : count;
448
- this.patch({ pendingTranscriptions: count, pendingTranscriptionRequests: requests });
449
- if (count > 0) this.cancelAutoSend({ preserveIntent: true });
450
- else if (this.holdToTalkRelease) this._finishHoldToTalk();
451
- else this.maybeScheduleAutoSend();
449
+ const delivery =
450
+ Number.isSafeInteger(deliveryPending) && deliveryPending >= 0
451
+ ? deliveryPending
452
+ : count;
453
+ this.patch({
454
+ pendingTranscriptions: count,
455
+ pendingTranscriptionRequests: requests,
456
+ pendingDeliveryTranscriptions: delivery,
457
+ });
458
+ if (delivery > 0) this.cancelAutoSend({ preserveIntent: true });
459
+ else if (this.holdToTalkRelease) {
460
+ if (count === 0) this._finishHoldToTalk();
461
+ } else this.maybeScheduleAutoSend();
452
462
  if (count === 0) this._drain();
453
463
  },
454
464
  onActivity: (active) => {
@@ -713,6 +723,9 @@ export class VoiceCoordinator {
713
723
  this._handleSpeechInterruption(this._speechActivity());
714
724
  this._drain();
715
725
  }
726
+ deliveryPending() {
727
+ return this.snapshot.pendingDeliveryTranscriptions ?? this.snapshot.pendingTranscriptions;
728
+ }
716
729
  maybeScheduleAutoSend(draft = this.autoSendIntent) {
717
730
  if (draft == null || draft !== this.composer.getDraft()) return;
718
731
  if (this.autoSendTimer !== null) return;
@@ -721,7 +734,7 @@ export class VoiceCoordinator {
721
734
  this.snapshot.muted ||
722
735
  this.composer.canAutoSend?.() === false ||
723
736
  this.snapshot.recognizing ||
724
- this.snapshot.pendingTranscriptions > 0
737
+ this.deliveryPending() > 0
725
738
  )
726
739
  return;
727
740
  this.scheduleAutoSend(draft);
@@ -730,7 +743,7 @@ export class VoiceCoordinator {
730
743
  if (
731
744
  this.composer.canAutoSend?.() === false ||
732
745
  this.snapshot.recognizing ||
733
- this.snapshot.pendingTranscriptions > 0
746
+ this.deliveryPending() > 0
734
747
  )
735
748
  this.cancelAutoSend({ preserveIntent: true });
736
749
  else this.maybeScheduleAutoSend();
@@ -806,6 +819,7 @@ export class VoiceCoordinator {
806
819
  recognizing: false,
807
820
  pendingTranscriptions: 0,
808
821
  pendingTranscriptionRequests: 0,
822
+ pendingDeliveryTranscriptions: 0,
809
823
  starting: false,
810
824
  });
811
825
  return this._input(async () => {
@@ -175,6 +175,12 @@ export class WhisperHttpRecognitionEngine {
175
175
  this.session = null;
176
176
  session.signal?.removeEventListener('abort', session.abort);
177
177
  await session.stopRecording();
178
- session.onProcessingChange?.({ queued: 0, active: false, transcribing: 0, pending: 0 });
178
+ session.onProcessingChange?.({
179
+ queued: 0,
180
+ active: false,
181
+ transcribing: 0,
182
+ deliveryPending: 0,
183
+ pending: 0,
184
+ });
179
185
  }
180
186
  }
@@ -13,7 +13,7 @@ This browser-only module owns recording sessions, speech/silence pin markers, re
13
13
 
14
14
  ## Ownership and limits
15
15
 
16
- Processing notifications expose `transcribing` (queued plus active speech attempts) separately from aggregate `pending` (including serialized audio writes and capture finish). Only the former drives visible transcription status; the latter still gates delivery, Hold-to-Talk release, playback admission and page-leave protection. Continuous silent PCM persistence must not announce transcription.
16
+ Processing notifications separate `transcribing` (queued plus active speech attempts), `deliveryPending` (those attempts plus unsettled speech-boundary writes and capture finish), and aggregate `pending` (all serialized writes). Only speech attempts drive visible transcription status. Automatic delivery waits for `deliveryPending`, not recurring silent PCM writes; Hold-to-Talk release, playback admission and page-leave protection still wait for aggregate work. Legacy engines without the delivery field conservatively use aggregate pending. Continuous silent PCM persistence must neither announce transcription nor reset the send countdown.
17
17
 
18
18
  Append, pin, attempt, read and finish operations are serialized per writer. Append owns a copy of its input and rejects more than eight seconds of queued PCM as backpressure rather than buffering indefinitely. Finish immediately seals further audio and pins, drains accepted appends, flushes the partial page, then releases ownership. Attempt upserts may still settle after audio is sealed, for already-running recognition requests. Dispose interrupts and drains active writers before closing storage.
19
19
 
@@ -6,7 +6,13 @@ import type {
6
6
  } from './types.js';
7
7
  import type { SileroVadOptions, SileroVadStream } from '../recognition/vad/SileroVadRuntime.js';
8
8
  type CaptureHandle = { finish(): Promise<void>; release(): void };
9
- type Processing = { queued: number; active: boolean; transcribing: number; pending: number };
9
+ type Processing = {
10
+ queued: number;
11
+ active: boolean;
12
+ transcribing: number;
13
+ deliveryPending: number;
14
+ pending: number;
15
+ };
10
16
  interface RecognitionCallbacks {
11
17
  signal?: AbortSignal;
12
18
  lang?: string;
@@ -75,6 +81,7 @@ interface RecognitionSession {
75
81
  onProcessingChange?: RecognitionCallbacks['onProcessingChange'];
76
82
  tail: Promise<void>;
77
83
  pendingWrites: number;
84
+ pendingSpeechWrites: number;
78
85
  lang: string;
79
86
  operation: string;
80
87
  capturedSamples: number;
@@ -151,6 +158,7 @@ export async function startRecordingRecognition(
151
158
  onProcessingChange,
152
159
  tail: Promise.resolve(),
153
160
  pendingWrites: 0,
161
+ pendingSpeechWrites: 0,
154
162
  lang,
155
163
  operation: uid(),
156
164
  capturedSamples: 0,
@@ -170,7 +178,13 @@ export async function startRecordingRecognition(
170
178
  active: !!session.activeRequest,
171
179
  // Only queued/running speech attempts belong in the visible transcription status.
172
180
  transcribing: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0),
173
- // Audio writes and final drain still block delivery/release, even in silence.
181
+ // Speech-boundary bookkeeping must settle before delivery, but silent PCM writes must not reset it.
182
+ deliveryPending:
183
+ session.transcriptionQueue.length +
184
+ (session.activeRequest ? 1 : 0) +
185
+ session.pendingSpeechWrites +
186
+ (session.finishing ? 1 : 0),
187
+ // All writes still protect release, playback admission and page leave.
174
188
  pending:
175
189
  session.transcriptionQueue.length +
176
190
  (session.activeRequest ? 1 : 0) +
@@ -184,14 +198,16 @@ export async function startRecordingRecognition(
184
198
  onError?.(new Error(engine.recordingErrorMessage(error)));
185
199
  void engine.stop();
186
200
  };
187
- const serialize = (task: () => Promise<void>, preserve = false) => {
201
+ const serialize = (task: () => Promise<void>, preserve = false, speech = false) => {
188
202
  session.pendingWrites++;
203
+ if (speech) session.pendingSpeechWrites++;
189
204
  notify();
190
205
  const work = session.tail.then(async () => {
191
206
  if (preserve || valid()) await task();
192
207
  });
193
208
  session.tail = work.catch(failure).finally(() => {
194
209
  session.pendingWrites--;
210
+ if (speech) session.pendingSpeechWrites--;
195
211
  notify();
196
212
  });
197
213
  return work;
@@ -211,10 +227,14 @@ export async function startRecordingRecognition(
211
227
  if (through - Math.max(liveSpeechStart, lastSplitQueued) < max) return;
212
228
  const end = Math.max(liveSpeechStart, lastSplitQueued) + max;
213
229
  lastSplitQueued = end;
214
- void serialize(async () => {
215
- const speech = [...writer.session.pins].reverse().find((p) => p.kind === 'speech-start');
216
- if (speech && valid()) await queueSpeech(speech, end);
217
- }).catch(() => {});
230
+ void serialize(
231
+ async () => {
232
+ const speech = [...writer.session.pins].reverse().find((p) => p.kind === 'speech-start');
233
+ if (speech && valid()) await queueSpeech(speech, end);
234
+ },
235
+ false,
236
+ true,
237
+ ).catch(() => {});
218
238
  };
219
239
  const queueSpeech = async (pin: RecordingPin, end: number) => {
220
240
  for (let at = Math.max(pin.atSample, queuedThrough); at < end; at += max) {
@@ -260,19 +280,23 @@ export async function startRecordingRecognition(
260
280
  liveSpeechStart = startSample;
261
281
  lastSplitQueued = startSample;
262
282
  } else liveSpeechStart = null;
263
- void serialize(async () => {
264
- const previous = writer.session.pins.at(-1);
265
- if (
266
- kind === 'speech-start' &&
267
- previous?.kind === 'silence-start' &&
268
- previous.atSample > 0 &&
269
- startSample - previous.atSample >= paragraphSamples
270
- )
271
- paragraphStarts.add(startSample);
272
- await writer.pin(kind, startSample, confirmedSample);
273
- if (valid() && kind === 'silence-start' && previous?.kind === 'speech-start')
274
- await queueSpeech(previous, startSample);
275
- }, true).catch(() => {});
283
+ void serialize(
284
+ async () => {
285
+ const previous = writer.session.pins.at(-1);
286
+ if (
287
+ kind === 'speech-start' &&
288
+ previous?.kind === 'silence-start' &&
289
+ previous.atSample > 0 &&
290
+ startSample - previous.atSample >= paragraphSamples
291
+ )
292
+ paragraphStarts.add(startSample);
293
+ await writer.pin(kind, startSample, confirmedSample);
294
+ if (valid() && kind === 'silence-start' && previous?.kind === 'speech-start')
295
+ await queueSpeech(previous, startSample);
296
+ },
297
+ true,
298
+ true,
299
+ ).catch(() => {});
276
300
  };
277
301
  const detector = new PinDetector({
278
302
  sampleRate: capture.silero ? 16000 : rate,
@@ -1,6 +1,6 @@
1
1
  # Settings module
2
2
 
3
- `services/pluginUpdate.ts` owns an application-lifetime explicit update state machine, passed into Settings by the app composition root. Opening Settings only checks GitHub; Update now reads profile eligibility, then requires a separate exact-version confirmation before the official DSH manager installs. Development/custom sources are never replaced; uncertain outcomes lock retries until the person checks the manager. Closing Settings preserves operation state. Installation never grants build-script permissions or restarts/reloads automatically. The inspected DSH manager returns restart-required for existing profile dependencies, so the running plugin is not replaced during download. Voice activity is checked immediately before submission, not reserved across the download or across browser clients. The manager offers no expected-source/CAS option: do not change this dependency through another manager/CLI concurrently with confirmation. Source is rechecked at confirmation but cannot be atomically guarded by this client.
3
+ `services/pluginUpdate.ts` owns an application-lifetime explicit update state machine, passed into Settings by the app composition root. Opening Settings only checks GitHub; Update now reads profile eligibility, then requires a separate exact-version confirmation before the official DSH manager installs. Development/custom sources are never replaced; uncertain outcomes lock retries until the person checks the manager. Closing Settings preserves operation state. Installation never grants build-script permissions or restarts/reloads automatically. Classify structured manager failures for blocked scripts, unavailable versions (including possible publication/release-age delays), and incompatible DSH versions; do not infer a successful update from installed files alone. The three-hour GitHub detection cache is independent of installation and remains unchanged. The inspected DSH manager returns restart-required for existing profile dependencies, so the running plugin is not replaced during download. Voice activity is checked immediately before submission, not reserved across the download or across browser clients. The manager offers no expected-source/CAS option: do not change this dependency through another manager/CLI concurrently with confirmation. Source is rechecked at confirmation but cannot be atomically guarded by this client.
4
4
 
5
5
  `services/debuggerInstall.ts` owns the separate application-lifetime, explicitly confirmed installation of the matching-version optional debugger through the same remote DSH manager. It rechecks inventory and voice activity before submission and refuses every existing debugger dependency, including disabled registry installs and unreadable custom sources. Submitted failures or uncertain replies lock retries for manager review. The manager has no expected-source/CAS option: do not change the debugger through another manager or CLI during confirmation/installation. `components/DebuggerLauncher.tsx` keeps Diagnostics in the fixed footer without importing the debugger bundle, explains the dependency in a native accessible dialog, and observes the versioned developer-extension registration before reporting ready. An applied result alone is not proof of browser readiness; restart-required results are shown truthfully without restarting or reloading. Dialog styles are feature-scoped.
6
6
 
@@ -92,7 +92,16 @@ export function createPluginUpdate({
92
92
  if (!result?.ok) return patch({ ...state, phase: 'uncertain', problem: 'uncertain' });
93
93
  const value = result.value;
94
94
  if (value.application === 'failed' || value.application === 'cancelled')
95
- return fail(value.pendingBuilds?.length ? 'scripts' : 'failed');
95
+ return fail(
96
+ value.error?.code === 'incompatible-version' ||
97
+ value.packageResult?.incompatible?.length
98
+ ? 'incompatible'
99
+ : value.pendingBuilds?.length || value.packageResult?.kind === 'build-blocked'
100
+ ? 'scripts'
101
+ : ['not-found', 'no-matching-version'].includes(value.packageResult?.kind)
102
+ ? 'delayed'
103
+ : 'failed',
104
+ );
96
105
  if (
97
106
  value.bundle !== 'dsh-live-voice' ||
98
107
  value.version !== state.version ||