dsh-live-voice 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/lib/asr/asr.worker.js +1 -1
  3. package/lib/asr/manifest.json +1 -1
  4. package/lib/client.js +483 -95
  5. package/lib/server.js +47 -7
  6. package/lib/vad/manifest.json +2 -2
  7. package/lib/vad/vad.worker.js +1 -1
  8. package/package.json +7 -5
  9. package/src/app/client/apply.tsx +16 -2
  10. package/src/app/client/i18n/catalogs/base.ts +6 -1
  11. package/src/app/client/i18n/catalogs/en.ts +10 -1
  12. package/src/app/client/i18n/catalogs/es.ts +10 -1
  13. package/src/app/client/i18n/catalogs/fr.ts +10 -1
  14. package/src/app/client/i18n/catalogs/hi.ts +10 -1
  15. package/src/app/client/i18n/catalogs/pt-BR.ts +10 -1
  16. package/src/app/client/i18n/catalogs/zh.ts +10 -1
  17. package/src/app/server/apply.ts +19 -0
  18. package/src/modules/conversation/ARCHITECTURE.md +1 -1
  19. package/src/modules/conversation/models/meeting.ts +17 -4
  20. package/src/modules/core/coordinator.ts +66 -11
  21. package/src/modules/core/qwen/QwenHttpHost.ts +5 -1
  22. package/src/modules/core/settings.ts +17 -7
  23. package/src/modules/recognition/ARCHITECTURE.md +3 -1
  24. package/src/modules/recognition/engines/whisper/WhisperRecognitionEngine.ts +9 -1
  25. package/src/modules/recognition/engines/whisper/whisperRecognitionHost.ts +4 -1
  26. package/src/modules/recognition/local/BrowserLocalRecognitionEngine.ts +4 -1
  27. package/src/modules/recognition/transcriptionContext.ts +28 -0
  28. package/src/modules/recording/ARCHITECTURE.md +8 -2
  29. package/src/modules/recording/PinDetector.ts +6 -0
  30. package/src/modules/recording/RecordingRecognition.ts +198 -57
  31. package/src/modules/recording/codec.ts +4 -0
  32. package/src/modules/recording/repository.ts +137 -5
  33. package/src/modules/recording/storage.ts +21 -1
  34. package/src/modules/recording/types.ts +19 -0
  35. package/src/modules/settings/ARCHITECTURE.md +1 -1
  36. package/src/modules/settings/sections/conversation/ConversationDelaySettings.tsx +9 -7
  37. package/src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx +17 -9
  38. package/src/modules/settings/services/pluginUpdate.ts +10 -1
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.2" : "test") + "/";
156
156
  async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
157
157
  try {
158
158
  const response = await fetchImpl(sileroVadAssetBase() + "availability", {
@@ -615,6 +615,7 @@ var RecordingRepositoryError = class extends Error {
615
615
  }
616
616
  code;
617
617
  };
618
+ var audioPageBytes = (page) => (page.decoderConfig?.description?.byteLength ?? 0) + page.packets.reduce((total, packet) => total + packet.data.byteLength, 0);
618
619
  var globals = () => globalThis;
619
620
  function unsupported(cause) {
620
621
  return new RecordingRepositoryError(
@@ -919,6 +920,19 @@ async function openRecordingStore() {
919
920
  }
920
921
  };
921
922
  }),
923
+ audioBytes: (id) => transact("readonly", (tx, result) => {
924
+ let bytes = 0;
925
+ const request = tx.objectStore("pages").index("recordingId").openCursor(IDBKeyRange.only(id));
926
+ request.onsuccess = () => {
927
+ const cursor = request.result;
928
+ if (!cursor) {
929
+ result(bytes);
930
+ return;
931
+ }
932
+ bytes += audioPageBytes(cursor.value);
933
+ cursor.continue();
934
+ };
935
+ }),
922
936
  nextPage: (id, start, end) => transact("readonly", (tx, result) => {
923
937
  if (end <= start) {
924
938
  result(null);
@@ -956,6 +970,7 @@ function createMemoryStore(maxBytes = 32 * 1024 * 1024) {
956
970
  let used = 0;
957
971
  const size2 = (page) => 256 + (page.decoderConfig?.description?.byteLength ?? 0) + page.packets.reduce((total, packet) => total + packet.data.byteLength + 64, 0);
958
972
  return {
973
+ audioBytes: async (id) => (pages.get(id) ?? []).reduce((sum, page) => sum + audioPageBytes(page), 0),
959
974
  list: async () => structuredClone([...sessions.values()]),
960
975
  get: async (id) => structuredClone(sessions.get(id) ?? null),
961
976
  create: async (session) => {
@@ -1204,6 +1219,17 @@ function createRecordingRepository() {
1204
1219
  }
1205
1220
  return locate(id);
1206
1221
  }
1222
+ const measured = /* @__PURE__ */ new Map();
1223
+ async function withAudioBytes(session) {
1224
+ if (session.audioBytes !== void 0) return session;
1225
+ let cached = measured.get(session.id);
1226
+ if (!cached || cached.samples !== session.savedSamples) {
1227
+ const { store } = await locate(session.id);
1228
+ cached = { samples: session.savedSamples, bytes: await store.audioBytes(session.id) };
1229
+ measured.set(session.id, cached);
1230
+ }
1231
+ return { ...session, audioBytes: cached.bytes };
1232
+ }
1207
1233
  const repository = {
1208
1234
  version: 1,
1209
1235
  segments: recordingSegments,
@@ -1214,14 +1240,21 @@ function createRecordingRepository() {
1214
1240
  },
1215
1241
  async list() {
1216
1242
  check2();
1217
- return (await all()).map(
1218
- (session) => writers.get(session.id)?.writer.session ?? cloneSession(session)
1243
+ const sessions = await all();
1244
+ const ids = new Set(sessions.map((session) => session.id));
1245
+ for (const id of measured.keys()) if (!ids.has(id)) measured.delete(id);
1246
+ return Promise.all(
1247
+ sessions.map(
1248
+ (session) => withAudioBytes(writers.get(session.id)?.writer.session ?? cloneSession(session))
1249
+ )
1219
1250
  );
1220
1251
  },
1221
1252
  async get(id) {
1222
1253
  check2();
1223
1254
  try {
1224
- return writers.get(id)?.writer.session ?? cloneSession((await locate(id)).session);
1255
+ return await withAudioBytes(
1256
+ writers.get(id)?.writer.session ?? cloneSession((await locate(id)).session)
1257
+ );
1225
1258
  } catch (error) {
1226
1259
  if (error instanceof RecordingRepositoryError && error.code === "not-found") return null;
1227
1260
  throw error;
@@ -1326,6 +1359,66 @@ function createRecordingRepository() {
1326
1359
  emit();
1327
1360
  });
1328
1361
  },
1362
+ async setName(id, value) {
1363
+ check2();
1364
+ if (typeof value !== "string" || value.trim().length > 120 || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value))
1365
+ throw new RecordingRepositoryError(
1366
+ "invalid-range",
1367
+ "Recording names must be a single line of at most 120 characters."
1368
+ );
1369
+ const name = value.trim();
1370
+ const active = writers.get(id);
1371
+ if (active) return active.setName(name);
1372
+ await serial(async () => {
1373
+ const { store } = await locate(id);
1374
+ const release = store === memory ? () => {
1375
+ } : await holdLock(id);
1376
+ try {
1377
+ await store.update(id, (current) => {
1378
+ if (!current.starred)
1379
+ throw new RecordingRepositoryError(
1380
+ "invalid-range",
1381
+ "Star a recording before naming it."
1382
+ );
1383
+ return { ...current, name };
1384
+ });
1385
+ } finally {
1386
+ release();
1387
+ }
1388
+ emit();
1389
+ });
1390
+ },
1391
+ timeline() {
1392
+ const targets = [...writers.entries()];
1393
+ return async (kind) => {
1394
+ const occurredAt = now();
1395
+ await Promise.all(
1396
+ targets.map(async ([id, active]) => {
1397
+ const value = {
1398
+ id: crypto.randomUUID(),
1399
+ kind,
1400
+ occurredAt,
1401
+ atSample: active.writer.session.capturedSamples
1402
+ };
1403
+ if (writers.get(id) === active) return active.event(value);
1404
+ await serial(async () => {
1405
+ const { store } = await locate(id);
1406
+ const release = store === memory ? () => {
1407
+ } : await holdLock(id);
1408
+ try {
1409
+ await store.update(id, (current) => ({
1410
+ ...current,
1411
+ events: [...current.events ?? [], value]
1412
+ }));
1413
+ } finally {
1414
+ release();
1415
+ }
1416
+ emit();
1417
+ });
1418
+ })
1419
+ );
1420
+ };
1421
+ },
1329
1422
  async begin(input) {
1330
1423
  check2();
1331
1424
  return serial(async () => {
@@ -1363,12 +1456,16 @@ function createRecordingRepository() {
1363
1456
  state: "recording",
1364
1457
  capturedSamples: 0,
1365
1458
  savedSamples: 0,
1459
+ audioBytes: 0,
1366
1460
  persistent,
1367
1461
  codec: "opus",
1368
1462
  storage: policy.enabled ? "indexeddb" : "memory",
1369
1463
  configuration: input.configuration,
1370
1464
  pins: [],
1371
1465
  attempts: [],
1466
+ events: [
1467
+ { id: crypto.randomUUID(), kind: "capture-start", occurredAt: now(), atSample: 0 }
1468
+ ],
1372
1469
  keepPrevious: Math.max(1, Math.floor(policy.keepPrevious))
1373
1470
  };
1374
1471
  try {
@@ -1422,7 +1519,14 @@ function createRecordingRepository() {
1422
1519
  sampleRate,
1423
1520
  ...encoded
1424
1521
  };
1425
- await update((current) => ({ ...current, savedSamples: page.startSample + count }), page);
1522
+ await update(
1523
+ (current) => ({
1524
+ ...current,
1525
+ savedSamples: page.startSample + count,
1526
+ audioBytes: (current.audioBytes ?? 0) + audioPageBytes(page)
1527
+ }),
1528
+ page
1529
+ );
1426
1530
  pendingCount = 0;
1427
1531
  index++;
1428
1532
  };
@@ -1553,7 +1657,16 @@ function createRecordingRepository() {
1553
1657
  ...current,
1554
1658
  state: fatal ? "failed" : state,
1555
1659
  error: fatal?.code ?? error,
1556
- endedAt: now()
1660
+ endedAt: now(),
1661
+ events: [
1662
+ ...current.events ?? [],
1663
+ {
1664
+ id: crypto.randomUUID(),
1665
+ kind: "capture-end",
1666
+ occurredAt: now(),
1667
+ atSample: current.capturedSamples
1668
+ }
1669
+ ]
1557
1670
  }));
1558
1671
  } finally {
1559
1672
  pending = new Float32Array(0);
@@ -1572,6 +1685,25 @@ function createRecordingRepository() {
1572
1685
  setStarred: (value) => enqueue(async () => {
1573
1686
  await update((current) => ({ ...current, starred: value }));
1574
1687
  }),
1688
+ setName: (name) => enqueue(async () => {
1689
+ await update((current) => {
1690
+ if (!current.starred)
1691
+ throw new RecordingRepositoryError(
1692
+ "invalid-range",
1693
+ "Star a recording before naming it."
1694
+ );
1695
+ return { ...current, name };
1696
+ });
1697
+ }),
1698
+ event: (value) => enqueue(async () => {
1699
+ await update((current) => ({
1700
+ ...current,
1701
+ events: [
1702
+ ...current.events ?? [],
1703
+ { ...value, atSample: current.capturedSamples }
1704
+ ]
1705
+ }));
1706
+ }),
1575
1707
  idle: async () => {
1576
1708
  await tail;
1577
1709
  }
@@ -1734,8 +1866,10 @@ var en = {
1734
1866
  "dsh-live-voice.commons.update.cancel": "Cancel",
1735
1867
  "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
1868
  "dsh-live-voice.commons.update.current": "This version is already installed. Restart DSH to use it.",
1869
+ "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
1870
  "dsh-live-voice.commons.update.done": "Live Voice {version} installed. Reload the page when ready.",
1738
1871
  "dsh-live-voice.commons.update.failed": "Update failed. The release may not be published on npm yet. Check the DSH plugin manager.",
1872
+ "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
1873
  "dsh-live-voice.commons.update.installing": "Updating\u2026",
1740
1874
  "dsh-live-voice.commons.update.label": "Update available",
1741
1875
  "dsh-live-voice.commons.update.link": "Update available: {version}. Open release",
@@ -1805,6 +1939,9 @@ var en = {
1805
1939
  "dsh-live-voice.recognition.commands.send": "Send to running agent",
1806
1940
  "dsh-live-voice.recognition.commands.stopSpeech": "Stop assistant speech",
1807
1941
  "dsh-live-voice.recognition.commands.title": "Voice commands",
1942
+ "dsh-live-voice.recognition.context.hint": "Optional background and vocabulary sent as prompt to the selected HTTP speech service, together with each audio segment. Saved on blur. Support depends on the server; this does not restrict languages or guarantee instruction following. Leave empty to send no context.",
1943
+ "dsh-live-voice.recognition.context.label": "Transcription context",
1944
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
1808
1945
  "dsh-live-voice.recognition.dictation.cancel": "Cancel dictation",
1809
1946
  "dsh-live-voice.recognition.engine.label": "Recognition engine",
1810
1947
  "dsh-live-voice.recognition.headphoneMode.help": "The microphone stays open during playback. Recognized non-echo speech can pause playback after the configured delay; it resumes after microphone and shared meeting audio stay silent for that delay. Manual Pause requires Resume.",
@@ -2077,7 +2214,7 @@ var en = {
2077
2214
  "dsh-live-voice.speak.rate.label": "Speech rate",
2078
2215
  "dsh-live-voice.speak.responseDelay.help": "After you stop speaking, automatic assistant playback waits for this much continuous silence. Speaking again restarts the wait.",
2079
2216
  "dsh-live-voice.speak.responseDelay.label": "Assistant response delay",
2080
- "dsh-live-voice.speak.responseDelay.none": "No delay",
2217
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1.5 seconds",
2081
2218
  "dsh-live-voice.speak.segmentGap.help": "Wait this many milliseconds between consecutive spoken segments. 400 ms is the default.",
2082
2219
  "dsh-live-voice.speak.segmentGap.label": "Pause between speech segments",
2083
2220
  "dsh-live-voice.speak.status.paused": "Speech paused",
@@ -2129,8 +2266,10 @@ var es = {
2129
2266
  "dsh-live-voice.commons.update.cancel": "Cancelar",
2130
2267
  "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
2268
  "dsh-live-voice.commons.update.current": "Esta versi\xF3n ya est\xE1 instalada. Reinicia DSH para usarla.",
2269
+ "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
2270
  "dsh-live-voice.commons.update.done": "Voz en vivo {version} instalado. Recarga la p\xE1gina cuando quieras.",
2133
2271
  "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.",
2272
+ "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
2273
  "dsh-live-voice.commons.update.installing": "Actualizando\u2026",
2135
2274
  "dsh-live-voice.commons.update.label": "Actualizaci\xF3n disponible",
2136
2275
  "dsh-live-voice.commons.update.link": "Actualizaci\xF3n disponible: {version}. Abrir la p\xE1gina de esta versi\xF3n",
@@ -2200,6 +2339,9 @@ var es = {
2200
2339
  "dsh-live-voice.recognition.commands.send": "Enviar al agente en ejecuci\xF3n",
2201
2340
  "dsh-live-voice.recognition.commands.stopSpeech": "Detener la lectura en voz alta del asistente",
2202
2341
  "dsh-live-voice.recognition.commands.title": "Comandos de voz",
2342
+ "dsh-live-voice.recognition.context.hint": "Contexto y vocabulario opcionales enviados como prompt al servicio HTTP de voz seleccionado con cada segmento de audio. Se guarda al salir del campo. La compatibilidad depende del servidor; no restringe idiomas ni garantiza el seguimiento de instrucciones. D\xE9jalo vac\xEDo para no enviar contexto.",
2343
+ "dsh-live-voice.recognition.context.label": "Contexto de transcripci\xF3n",
2344
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
2203
2345
  "dsh-live-voice.recognition.dictation.cancel": "Cancelar dictado",
2204
2346
  "dsh-live-voice.recognition.engine.label": "Motor de reconocimiento",
2205
2347
  "dsh-live-voice.recognition.headphoneMode.help": "El micr\xF3fono sigue abierto durante la reproducci\xF3n. El habla reconocida sin eco puede pausarla tras el retraso configurado; se reanuda cuando el micr\xF3fono y el audio compartido guardan silencio durante ese intervalo. La pausa manual requiere Reanudar.",
@@ -2472,7 +2614,7 @@ var es = {
2472
2614
  "dsh-live-voice.speak.rate.label": "Velocidad de habla",
2473
2615
  "dsh-live-voice.speak.responseDelay.help": "Cuando dejas de hablar, la reproducci\xF3n autom\xE1tica del asistente espera este intervalo de silencio continuo. Si vuelves a hablar, la espera se reinicia.",
2474
2616
  "dsh-live-voice.speak.responseDelay.label": "Demora de respuesta del asistente",
2475
- "dsh-live-voice.speak.responseDelay.none": "Sin retraso",
2617
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1,5 segundos",
2476
2618
  "dsh-live-voice.speak.segmentGap.help": "Espera esta cantidad de milisegundos entre segmentos hablados consecutivos. El valor predeterminado es 400 ms.",
2477
2619
  "dsh-live-voice.speak.segmentGap.label": "Pausa entre segmentos de voz",
2478
2620
  "dsh-live-voice.speak.status.paused": "Lectura en pausa",
@@ -2524,8 +2666,10 @@ var fr = {
2524
2666
  "dsh-live-voice.commons.update.cancel": "Annuler",
2525
2667
  "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
2668
  "dsh-live-voice.commons.update.current": "Cette version est d\xE9j\xE0 install\xE9e. Red\xE9marrez DSH pour l\u2019utiliser.",
2669
+ "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
2670
  "dsh-live-voice.commons.update.done": "Voix en direct {version} install\xE9. Rechargez la page quand vous le souhaitez.",
2528
2671
  "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.",
2672
+ "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
2673
  "dsh-live-voice.commons.update.installing": "Mise \xE0 jour\u2026",
2530
2674
  "dsh-live-voice.commons.update.label": "Mise \xE0 jour disponible",
2531
2675
  "dsh-live-voice.commons.update.link": "Mise \xE0 jour disponible : {version}. Ouvrir la page de cette version",
@@ -2595,6 +2739,9 @@ var fr = {
2595
2739
  "dsh-live-voice.recognition.commands.send": "Envoyer \xE0 l\u2019agent en cours d\u2019ex\xE9cution",
2596
2740
  "dsh-live-voice.recognition.commands.stopSpeech": "Arr\xEAter la lecture vocale de l\u2019assistant",
2597
2741
  "dsh-live-voice.recognition.commands.title": "Commandes vocales",
2742
+ "dsh-live-voice.recognition.context.hint": "Contexte et vocabulaire facultatifs envoy\xE9s comme prompt au service vocal HTTP s\xE9lectionn\xE9 avec chaque segment audio. Enregistr\xE9 \xE0 la sortie du champ. La prise en charge d\xE9pend du serveur ; ne limite pas les langues et ne garantit pas le suivi des consignes. Laissez vide pour ne pas envoyer de contexte.",
2743
+ "dsh-live-voice.recognition.context.label": "Contexte de transcription",
2744
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
2598
2745
  "dsh-live-voice.recognition.dictation.cancel": "Annuler la dict\xE9e",
2599
2746
  "dsh-live-voice.recognition.engine.label": "Moteur de reconnaissance",
2600
2747
  "dsh-live-voice.recognition.headphoneMode.help": "Le microphone reste ouvert pendant la lecture. La parole reconnue sans \xE9cho peut la mettre en pause apr\xE8s le d\xE9lai configur\xE9 ; elle reprend lorsque le microphone et le son partag\xE9 restent silencieux pendant ce d\xE9lai. Une pause manuelle n\xE9cessite Reprendre.",
@@ -2867,7 +3014,7 @@ var fr = {
2867
3014
  "dsh-live-voice.speak.rate.label": "D\xE9bit de parole",
2868
3015
  "dsh-live-voice.speak.responseDelay.help": "Apr\xE8s que vous avez cess\xE9 de parler, la lecture automatique de l\u2019assistant attend cette dur\xE9e de silence continu. Si vous reparlez, l\u2019attente recommence.",
2869
3016
  "dsh-live-voice.speak.responseDelay.label": "D\xE9lai de r\xE9ponse de l\u2019assistant",
2870
- "dsh-live-voice.speak.responseDelay.none": "Sans d\xE9lai",
3017
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1,5 seconde",
2871
3018
  "dsh-live-voice.speak.segmentGap.help": "Attend ce nombre de millisecondes entre des segments vocaux cons\xE9cutifs. La valeur par d\xE9faut est 400 ms.",
2872
3019
  "dsh-live-voice.speak.segmentGap.label": "Pause entre les segments vocaux",
2873
3020
  "dsh-live-voice.speak.status.paused": "Lecture en pause",
@@ -2919,8 +3066,10 @@ var hi = {
2919
3066
  "dsh-live-voice.commons.update.cancel": "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
2920
3067
  "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
3068
  "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",
3069
+ "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
3070
  "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
3071
  "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",
3072
+ "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
3073
  "dsh-live-voice.commons.update.installing": "\u0905\u092A\u0921\u0947\u091F \u0939\u094B \u0930\u0939\u093E \u0939\u0948\u2026",
2925
3074
  "dsh-live-voice.commons.update.label": "\u0905\u092A\u0921\u0947\u091F \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948",
2926
3075
  "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",
@@ -2990,6 +3139,9 @@ var hi = {
2990
3139
  "dsh-live-voice.recognition.commands.send": "\u091A\u0932 \u0930\u0939\u0947 \u090F\u091C\u0947\u0902\u091F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
2991
3140
  "dsh-live-voice.recognition.commands.stopSpeech": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0935\u093E\u091A\u0928 \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
2992
3141
  "dsh-live-voice.recognition.commands.title": "\u0935\u0949\u0907\u0938 \u0915\u092E\u093E\u0902\u0921",
3142
+ "dsh-live-voice.recognition.context.hint": "\u0935\u0948\u0915\u0932\u094D\u092A\u093F\u0915 \u0938\u0902\u0926\u0930\u094D\u092D \u0914\u0930 \u0936\u092C\u094D\u0926\u093E\u0935\u0932\u0940 \u0939\u0930 \u0911\u0921\u093F\u092F\u094B \u0916\u0902\u0921 \u0915\u0947 \u0938\u093E\u0925 \u091A\u0941\u0928\u0940 \u0917\u0908 HTTP \u0935\u093E\u0915\u094D \u0938\u0947\u0935\u093E \u0915\u094B prompt \u0915\u0947 \u0930\u0942\u092A \u092E\u0947\u0902 \u092D\u0947\u091C\u0947 \u091C\u093E\u0924\u0947 \u0939\u0948\u0902\u0964 \u092B\u093C\u0940\u0932\u094D\u0921 \u0938\u0947 \u092C\u093E\u0939\u0930 \u0906\u0928\u0947 \u092A\u0930 \u0938\u0939\u0947\u091C\u093E \u091C\u093E\u0924\u093E \u0939\u0948\u0964 \u0938\u092E\u0930\u094D\u0925\u0928 \u0938\u0930\u094D\u0935\u0930 \u092A\u0930 \u0928\u093F\u0930\u094D\u092D\u0930 \u0939\u0948; \u092F\u0939 \u092D\u093E\u0937\u093E\u0913\u0902 \u0915\u094B \u0938\u0940\u092E\u093F\u0924 \u0928\u0939\u0940\u0902 \u0915\u0930\u0924\u093E \u0914\u0930 \u0928\u093F\u0930\u094D\u0926\u0947\u0936\u094B\u0902 \u0915\u0947 \u092A\u093E\u0932\u0928 \u0915\u0940 \u0917\u093E\u0930\u0902\u091F\u0940 \u0928\u0939\u0940\u0902 \u0926\u0947\u0924\u093E\u0964 \u0938\u0902\u0926\u0930\u094D\u092D \u0928 \u092D\u0947\u091C\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u0916\u093E\u0932\u0940 \u091B\u094B\u0921\u093C\u0947\u0902\u0964",
3143
+ "dsh-live-voice.recognition.context.label": "\u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u0936\u0928 \u0938\u0902\u0926\u0930\u094D\u092D",
3144
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
2993
3145
  "dsh-live-voice.recognition.dictation.cancel": "\u0921\u093F\u0915\u094D\u091F\u0947\u0936\u0928 \u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
2994
3146
  "dsh-live-voice.recognition.engine.label": "\u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928 \u0907\u0902\u091C\u0928",
2995
3147
  "dsh-live-voice.recognition.headphoneMode.help": "\u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u0915\u0947 \u0926\u094C\u0930\u093E\u0928 \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u091A\u093E\u0932\u0942 \u0930\u0939\u0924\u093E \u0939\u0948\u0964 \u092C\u093F\u0928\u093E \u092A\u094D\u0930\u0924\u093F\u0927\u094D\u0935\u0928\u093F \u0915\u0940 \u092A\u0939\u091A\u093E\u0928\u0940 \u0917\u0908 \u0906\u0935\u093E\u091C\u093C \u0928\u093F\u0930\u094D\u0927\u093E\u0930\u093F\u0924 \u0935\u093F\u0932\u0902\u092C \u0915\u0947 \u092C\u093E\u0926 \u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u0930\u094B\u0915 \u0938\u0915\u0924\u0940 \u0939\u0948; \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0914\u0930 \u0938\u093E\u091D\u093E \u092E\u0940\u091F\u093F\u0902\u0917 \u0911\u0921\u093F\u092F\u094B \u0909\u0924\u0928\u0940 \u0926\u0947\u0930 \u0936\u093E\u0902\u0924 \u0930\u0939\u0928\u0947 \u092A\u0930 \u092F\u0939 \u092B\u093F\u0930 \u091A\u0932\u0924\u093E \u0939\u0948\u0964 \u092E\u0948\u0928\u094D\u092F\u0941\u0905\u0932 \u0935\u093F\u0930\u093E\u092E \u0915\u0947 \u092C\u093E\u0926 \u092B\u093F\u0930 \u091A\u0932\u093E\u090F\u0901 \u0926\u092C\u093E\u090F\u0901\u0964",
@@ -3262,7 +3414,7 @@ var hi = {
3262
3414
  "dsh-live-voice.speak.rate.label": "\u092C\u094B\u0932\u0928\u0947 \u0915\u0940 \u0917\u0924\u093F",
3263
3415
  "dsh-live-voice.speak.responseDelay.help": "\u0906\u092A\u0915\u0947 \u092C\u094B\u0932\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0928\u0947 \u0915\u0947 \u092C\u093E\u0926 \u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u0907\u0924\u0928\u0940 \u0926\u0947\u0930 \u0924\u0915 \u0932\u0917\u093E\u0924\u093E\u0930 \u092E\u094C\u0928 \u0930\u0939\u0928\u0947 \u0915\u0940 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0915\u0930\u0924\u093E \u0939\u0948\u0964 \u0926\u094B\u092C\u093E\u0930\u093E \u092C\u094B\u0932\u0928\u0947 \u092A\u0930 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u092B\u093F\u0930 \u0938\u0947 \u0936\u0941\u0930\u0942 \u0939\u094B\u0924\u0940 \u0939\u0948\u0964",
3264
3416
  "dsh-live-voice.speak.responseDelay.label": "\u0938\u0939\u093E\u092F\u0915 \u0915\u0947 \u091C\u0935\u093E\u092C \u0915\u093E \u0935\u093F\u0932\u0902\u092C",
3265
- "dsh-live-voice.speak.responseDelay.none": "\u0915\u094B\u0908 \u0935\u093F\u0932\u0902\u092C \u0928\u0939\u0940\u0902",
3417
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1.5 \u0938\u0947\u0915\u0902\u0921",
3266
3418
  "dsh-live-voice.speak.segmentGap.help": "\u0932\u0917\u093E\u0924\u093E\u0930 \u092C\u094B\u0932\u0947 \u0917\u090F \u0939\u093F\u0938\u094D\u0938\u094B\u0902 \u0915\u0947 \u092C\u0940\u091A \u0907\u0924\u0928\u0947 \u092E\u093F\u0932\u0940\u0938\u0947\u0915\u0902\u0921 \u0930\u0941\u0915\u0947\u0902\u0964 \u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F 400 ms \u0939\u0948\u0964",
3267
3419
  "dsh-live-voice.speak.segmentGap.label": "\u092C\u094B\u0932\u0947 \u0917\u090F \u0939\u093F\u0938\u094D\u0938\u094B\u0902 \u0915\u0947 \u092C\u0940\u091A \u0935\u093F\u0930\u093E\u092E",
3268
3420
  "dsh-live-voice.speak.status.paused": "\u0935\u093E\u091A\u0928 \u0920\u0939\u0930\u093E \u0939\u0941\u0906 \u0939\u0948",
@@ -3314,8 +3466,10 @@ var ptBR = {
3314
3466
  "dsh-live-voice.commons.update.cancel": "Cancelar",
3315
3467
  "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
3468
  "dsh-live-voice.commons.update.current": "Esta vers\xE3o j\xE1 est\xE1 instalada. Reinicie o DSH para utiliz\xE1-la.",
3469
+ "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
3470
  "dsh-live-voice.commons.update.done": "Voz ao Vivo {version} instalado. Recarregue a p\xE1gina quando quiser.",
3318
3471
  "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.",
3472
+ "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
3473
  "dsh-live-voice.commons.update.installing": "Atualizando\u2026",
3320
3474
  "dsh-live-voice.commons.update.label": "Atualiza\xE7\xE3o dispon\xEDvel",
3321
3475
  "dsh-live-voice.commons.update.link": "Atualiza\xE7\xE3o dispon\xEDvel: {version}. Abrir lan\xE7amento",
@@ -3385,6 +3539,9 @@ var ptBR = {
3385
3539
  "dsh-live-voice.recognition.commands.send": "Enviar ao agente em execu\xE7\xE3o",
3386
3540
  "dsh-live-voice.recognition.commands.stopSpeech": "Parar fala do assistente",
3387
3541
  "dsh-live-voice.recognition.commands.title": "Comandos de voz",
3542
+ "dsh-live-voice.recognition.context.hint": "Contexto e vocabul\xE1rio opcionais enviados como prompt ao servi\xE7o HTTP de voz selecionado, junto com cada trecho de \xE1udio. Salvo ao sair do campo. O suporte depende do servidor; n\xE3o restringe idiomas nem garante que instru\xE7\xF5es sejam seguidas. Deixe vazio para n\xE3o enviar contexto.",
3543
+ "dsh-live-voice.recognition.context.label": "Contexto da transcri\xE7\xE3o",
3544
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
3388
3545
  "dsh-live-voice.recognition.dictation.cancel": "Cancelar ditado",
3389
3546
  "dsh-live-voice.recognition.engine.label": "Mecanismo de reconhecimento",
3390
3547
  "dsh-live-voice.recognition.headphoneMode.help": "O microfone fica aberto durante a reprodu\xE7\xE3o. Fala reconhecida sem eco pode pausar ap\xF3s o atraso configurado; a reprodu\xE7\xE3o retoma quando microfone e \xE1udio compartilhado da reuni\xE3o ficam em sil\xEAncio por esse intervalo. Pausa manual exige Retomar.",
@@ -3657,7 +3814,7 @@ var ptBR = {
3657
3814
  "dsh-live-voice.speak.rate.label": "Velocidade da fala",
3658
3815
  "dsh-live-voice.speak.responseDelay.help": "Depois que voc\xEA parar de falar, a reprodu\xE7\xE3o autom\xE1tica do assistente aguardar\xE1 esse tempo de sil\xEAncio cont\xEDnuo. Falar novamente reinicia a contagem.",
3659
3816
  "dsh-live-voice.speak.responseDelay.label": "Atraso da resposta do assistente",
3660
- "dsh-live-voice.speak.responseDelay.none": "Sem atraso",
3817
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1,5 segundo",
3661
3818
  "dsh-live-voice.speak.segmentGap.help": "Aguarde esta quantidade de milissegundos entre trechos falados consecutivos. O padr\xE3o \xE9 400 ms.",
3662
3819
  "dsh-live-voice.speak.segmentGap.label": "Pausa entre trechos de fala",
3663
3820
  "dsh-live-voice.speak.status.paused": "Fala pausada",
@@ -3709,8 +3866,10 @@ var zh = {
3709
3866
  "dsh-live-voice.commons.update.cancel": "\u53D6\u6D88",
3710
3867
  "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
3868
  "dsh-live-voice.commons.update.current": "\u6B64\u7248\u672C\u5DF2\u5B89\u88C5\u3002\u8BF7\u91CD\u542F DSH \u540E\u4F7F\u7528\u3002",
3869
+ "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
3870
  "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
3871
  "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",
3872
+ "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
3873
  "dsh-live-voice.commons.update.installing": "\u6B63\u5728\u66F4\u65B0\u2026",
3715
3874
  "dsh-live-voice.commons.update.label": "\u6709\u53EF\u7528\u66F4\u65B0",
3716
3875
  "dsh-live-voice.commons.update.link": "\u6709\u53EF\u7528\u66F4\u65B0\uFF1A{version}\u3002\u6253\u5F00\u53D1\u5E03\u9875\u9762",
@@ -3780,6 +3939,9 @@ var zh = {
3780
3939
  "dsh-live-voice.recognition.commands.send": "\u53D1\u9001\u7ED9\u8FD0\u884C\u4E2D\u7684\u667A\u80FD\u4F53",
3781
3940
  "dsh-live-voice.recognition.commands.stopSpeech": "\u505C\u6B62\u52A9\u624B\u6717\u8BFB",
3782
3941
  "dsh-live-voice.recognition.commands.title": "\u8BED\u97F3\u547D\u4EE4",
3942
+ "dsh-live-voice.recognition.context.hint": "\u53EF\u9009\u7684\u80CC\u666F\u548C\u8BCD\u6C47\u4F1A\u968F\u6BCF\u6BB5\u97F3\u9891\u4EE5 prompt \u5B57\u6BB5\u53D1\u9001\u5230\u6240\u9009 HTTP \u8BED\u97F3\u670D\u52A1\u3002\u79BB\u5F00\u8F93\u5165\u6846\u65F6\u4FDD\u5B58\u3002\u652F\u6301\u60C5\u51B5\u53D6\u51B3\u4E8E\u670D\u52A1\u5668\uFF1B\u4E0D\u4F1A\u9650\u5236\u8BED\u8A00\uFF0C\u4E5F\u4E0D\u4FDD\u8BC1\u9075\u5FAA\u6307\u4EE4\u3002\u7559\u7A7A\u5219\u4E0D\u53D1\u9001\u4E0A\u4E0B\u6587\u3002",
3943
+ "dsh-live-voice.recognition.context.label": "\u8F6C\u5199\u4E0A\u4E0B\u6587",
3944
+ "dsh-live-voice.recognition.context.placeholder": "Native Brazilian Portuguese speaker who frequently uses technical terms in English.",
3783
3945
  "dsh-live-voice.recognition.dictation.cancel": "\u53D6\u6D88\u542C\u5199",
3784
3946
  "dsh-live-voice.recognition.engine.label": "\u8BC6\u522B\u5F15\u64CE",
3785
3947
  "dsh-live-voice.recognition.headphoneMode.help": "\u64AD\u653E\u65F6\u9EA6\u514B\u98CE\u4FDD\u6301\u5F00\u542F\u3002\u8BC6\u522B\u5230\u975E\u56DE\u58F0\u8BED\u97F3\u540E\uFF0C\u53EF\u5728\u8BBE\u5B9A\u5EF6\u8FDF\u540E\u6682\u505C\u64AD\u653E\uFF1B\u9EA6\u514B\u98CE\u548C\u5171\u4EAB\u4F1A\u8BAE\u97F3\u9891\u6301\u7EED\u9759\u97F3\u8FBE\u5230\u8BE5\u65F6\u957F\u540E\u6062\u590D\u3002\u624B\u52A8\u6682\u505C\u540E\u9700\u70B9\u51FB\u6062\u590D\u3002",
@@ -4052,7 +4214,7 @@ var zh = {
4052
4214
  "dsh-live-voice.speak.rate.label": "\u8BED\u901F",
4053
4215
  "dsh-live-voice.speak.responseDelay.help": "\u4F60\u505C\u6B62\u8BF4\u8BDD\u540E\uFF0C\u52A9\u624B\u4F1A\u7B49\u5F85\u6B64\u65F6\u957F\u7684\u8FDE\u7EED\u9759\u97F3\uFF0C\u7136\u540E\u81EA\u52A8\u64AD\u653E\u8BED\u97F3\u3002\u518D\u6B21\u8BF4\u8BDD\u4F1A\u91CD\u65B0\u5F00\u59CB\u7B49\u5F85\u3002",
4054
4216
  "dsh-live-voice.speak.responseDelay.label": "\u52A9\u624B\u54CD\u5E94\u5EF6\u8FDF",
4055
- "dsh-live-voice.speak.responseDelay.none": "\u65E0\u5EF6\u8FDF",
4217
+ "dsh-live-voice.speak.responseDelay.oneAndHalf": "1.5 \u79D2",
4056
4218
  "dsh-live-voice.speak.segmentGap.help": "\u5728\u8FDE\u7EED\u8BED\u97F3\u7247\u6BB5\u4E4B\u95F4\u7B49\u5F85\u6B64\u6BEB\u79D2\u6570\u3002\u9ED8\u8BA4\u503C\u4E3A 400 ms\u3002",
4057
4219
  "dsh-live-voice.speak.segmentGap.label": "\u8BED\u97F3\u7247\u6BB5\u4E4B\u95F4\u7684\u505C\u987F",
4058
4220
  "dsh-live-voice.speak.status.paused": "\u8BED\u97F3\u5DF2\u6682\u505C",
@@ -5050,6 +5212,19 @@ function SpeechStatusBar({ controller }) {
5050
5212
  ));
5051
5213
  }
5052
5214
 
5215
+ // src/modules/recognition/transcriptionContext.ts
5216
+ var TRANSCRIPTION_CONTEXT_MAX_LENGTH = 1e3;
5217
+ function transcriptionContext(value) {
5218
+ if (value === void 0 || value === null) return "";
5219
+ if (typeof value !== "string" || value.length > TRANSCRIPTION_CONTEXT_MAX_LENGTH || value.includes("\0"))
5220
+ throw new Error("Transcription context must contain at most 1000 characters without NUL.");
5221
+ return value.trim();
5222
+ }
5223
+ function transcriptionContextHeader(value) {
5224
+ const text = transcriptionContext(value);
5225
+ return text ? { "x-dlv-transcription-context": encodeURIComponent(text) } : {};
5226
+ }
5227
+
5053
5228
  // src/modules/core/speechDefaults.ts
5054
5229
  var notices = {
5055
5230
  en: "You can see the code in our conversation history.",
@@ -5107,6 +5282,7 @@ var defaultQwenVoice = qwenVoices[0].value;
5107
5282
  var defaultAgentVoiceContext = `Live Voice output is active. Your entire user-facing response will be spoken aloud.
5108
5283
  Be concise and conversational. Lead with the answer or next action. Avoid unnecessary repetition, long preambles, dense lists, raw code, paths, and verbose status narration.
5109
5284
  Do not narrate routine tool activity by default. If the user explicitly asks you to keep them informed while working, provide brief spoken progress updates only at meaningful milestones.`;
5285
+ var assistantSpeechDelayOptions = Object.freeze([0.6, 1.5, 2, 3, 4, 5]);
5110
5286
  var autoSendDelayOptions = Object.freeze([0.6, 0.8, 1, 2, 3, 4, 5, 6]);
5111
5287
  var recordingBitrateOptions = Object.freeze([24e3, 32e3, 48e3, 64e3, 96e3]);
5112
5288
  var defaultSettings = Object.freeze({
@@ -5142,10 +5318,11 @@ var defaultSettings = Object.freeze({
5142
5318
  sendingMode: "manual",
5143
5319
  dshBusyEnterBehavior: "queue",
5144
5320
  autoSendDelaySeconds: 4,
5145
- assistantSpeechDelaySeconds: 0,
5321
+ assistantSpeechDelaySeconds: 1.5,
5146
5322
  mode: "speaker",
5147
5323
  lang: "pt-BR",
5148
5324
  recognitionLang: "pt-BR",
5325
+ recognitionContext: "",
5149
5326
  voice: "",
5150
5327
  inputDeviceId: "",
5151
5328
  outputDeviceId: "",
@@ -5207,6 +5384,7 @@ function normalizeSettings(value) {
5207
5384
  browserQwenModel: "qwen-0.6b",
5208
5385
  browserAsrDevice: source.browserAsrDevice === "wasm" ? "wasm" : "webgpu",
5209
5386
  recognitionProcessLocally: typeof source.recognitionProcessLocally === "boolean" ? source.recognitionProcessLocally : defaultSettings.recognitionProcessLocally,
5387
+ recognitionContext: typeof source.recognitionContext === "string" ? source.recognitionContext.replaceAll("\0", "").slice(0, TRANSCRIPTION_CONTEXT_MAX_LENGTH).trim() : defaultSettings.recognitionContext,
5210
5388
  recognitionAutoInstall: typeof source.recognitionAutoInstall === "boolean" ? source.recognitionAutoInstall : defaultSettings.recognitionAutoInstall,
5211
5389
  voiceDetectionEngine: ["energy", "silero"].includes(source.voiceDetectionEngine) ? source.voiceDetectionEngine : defaultSettings.voiceDetectionEngine,
5212
5390
  voiceDetectionPreset: source.voiceDetectionPreset === "custom" || Object.hasOwn(voiceDetectionPresets, source.voiceDetectionPreset) ? source.voiceDetectionPreset : defaultSettings.voiceDetectionPreset,
@@ -5244,7 +5422,9 @@ function normalizeSettings(value) {
5244
5422
  dshBusyEnterBehavior: ["queue", "steer"].includes(source.dshBusyEnterBehavior) ? source.dshBusyEnterBehavior : defaultSettings.dshBusyEnterBehavior,
5245
5423
  sendingMode: source.sendingMode === "automatic" ? "queue" : ["manual", "queue", "steer"].includes(source.sendingMode) ? source.sendingMode : defaultSettings.sendingMode,
5246
5424
  autoSendDelaySeconds: autoSendDelayOptions.includes(source.autoSendDelaySeconds) ? source.autoSendDelaySeconds : defaultSettings.autoSendDelaySeconds,
5247
- assistantSpeechDelaySeconds: Number.isInteger(source.assistantSpeechDelaySeconds) && source.assistantSpeechDelaySeconds >= 0 && source.assistantSpeechDelaySeconds <= 4 ? source.assistantSpeechDelaySeconds : defaultSettings.assistantSpeechDelaySeconds,
5425
+ assistantSpeechDelaySeconds: assistantSpeechDelayOptions.includes(
5426
+ source.assistantSpeechDelaySeconds
5427
+ ) ? source.assistantSpeechDelaySeconds : defaultSettings.assistantSpeechDelaySeconds,
5248
5428
  mode: ["speaker", "headphones"].includes(source.mode) ? source.mode : defaultSettings.mode,
5249
5429
  lang: typeof source.lang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.lang) ? source.lang : defaultSettings.lang,
5250
5430
  recognitionLang: source.recognitionLang === "auto" || typeof source.recognitionLang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.recognitionLang) ? source.recognitionLang : typeof source.lang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.lang) ? source.lang : defaultSettings.recognitionLang,
@@ -6869,8 +7049,23 @@ function cancellable(promise, signal) {
6869
7049
  });
6870
7050
  }
6871
7051
  var VoiceCoordinator = class _VoiceCoordinator {
6872
- constructor({ recognition, engines, meter, composer, settings = {}, persistSettings }) {
6873
- Object.assign(this, { recognition, engines, meter, composer, persistSettings });
7052
+ constructor({
7053
+ recognition,
7054
+ engines,
7055
+ meter,
7056
+ composer,
7057
+ settings = {},
7058
+ persistSettings,
7059
+ recordingTimeline
7060
+ }) {
7061
+ Object.assign(this, {
7062
+ recognition,
7063
+ engines,
7064
+ meter,
7065
+ composer,
7066
+ persistSettings,
7067
+ recordingTimeline
7068
+ });
6874
7069
  this.listeners = /* @__PURE__ */ new Set();
6875
7070
  this.transcript = new TranscriptDraft();
6876
7071
  this.epoch = 0;
@@ -6896,6 +7091,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
6896
7091
  recognizing: false,
6897
7092
  pendingTranscriptions: 0,
6898
7093
  pendingTranscriptionRequests: void 0,
7094
+ pendingDeliveryTranscriptions: void 0,
6899
7095
  muted: normalizeSettings(settings).microphoneEnabled === false,
6900
7096
  speaking: false,
6901
7097
  paused: false,
@@ -7240,14 +7436,20 @@ var VoiceCoordinator = class _VoiceCoordinator {
7240
7436
  onResult: (result) => {
7241
7437
  if (valid()) this.onResult(result);
7242
7438
  },
7243
- onProcessingChange: ({ pending = 0, transcribing } = {}) => {
7439
+ onProcessingChange: ({ pending = 0, transcribing, deliveryPending } = {}) => {
7244
7440
  if (!valid()) return;
7245
7441
  const count = Number.isSafeInteger(pending) && pending >= 0 ? pending : 0;
7246
7442
  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();
7443
+ const delivery = Number.isSafeInteger(deliveryPending) && deliveryPending >= 0 ? deliveryPending : count;
7444
+ this.patch({
7445
+ pendingTranscriptions: count,
7446
+ pendingTranscriptionRequests: requests,
7447
+ pendingDeliveryTranscriptions: delivery
7448
+ });
7449
+ if (delivery > 0) this.cancelAutoSend({ preserveIntent: true });
7450
+ else if (this.holdToTalkRelease) {
7451
+ if (count === 0) this._finishHoldToTalk();
7452
+ } else this.maybeScheduleAutoSend();
7251
7453
  if (count === 0) this._drain();
7252
7454
  },
7253
7455
  onActivity: (active) => {
@@ -7448,15 +7650,18 @@ var VoiceCoordinator = class _VoiceCoordinator {
7448
7650
  this._handleSpeechInterruption(this._speechActivity());
7449
7651
  this._drain();
7450
7652
  }
7653
+ deliveryPending() {
7654
+ return this.snapshot.pendingDeliveryTranscriptions ?? this.snapshot.pendingTranscriptions;
7655
+ }
7451
7656
  maybeScheduleAutoSend(draft = this.autoSendIntent) {
7452
7657
  if (draft == null || draft !== this.composer.getDraft()) return;
7453
7658
  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)
7659
+ if (this.snapshot.settings.sendingMode === "manual" || this.snapshot.muted || this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.deliveryPending() > 0)
7455
7660
  return;
7456
7661
  this.scheduleAutoSend(draft);
7457
7662
  }
7458
7663
  refreshDeliveryReadiness() {
7459
- if (this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.snapshot.pendingTranscriptions > 0)
7664
+ if (this.composer.canAutoSend?.() === false || this.snapshot.recognizing || this.deliveryPending() > 0)
7460
7665
  this.cancelAutoSend({ preserveIntent: true });
7461
7666
  else this.maybeScheduleAutoSend();
7462
7667
  }
@@ -7513,6 +7718,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
7513
7718
  recognizing: false,
7514
7719
  pendingTranscriptions: 0,
7515
7720
  pendingTranscriptionRequests: 0,
7721
+ pendingDeliveryTranscriptions: 0,
7516
7722
  starting: false
7517
7723
  });
7518
7724
  return this._input(async () => {
@@ -7673,6 +7879,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
7673
7879
  this.queue = [];
7674
7880
  }
7675
7881
  async stopSpeech(resumeListening = true) {
7882
+ this.activeRecordingTimeline = null;
7676
7883
  this._cancelAssistantSpeechTimer();
7677
7884
  this.assistantSpeechNotBefore = 0;
7678
7885
  this._cancelInterruptionResume();
@@ -7780,8 +7987,10 @@ var VoiceCoordinator = class _VoiceCoordinator {
7780
7987
  try {
7781
7988
  const result = await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].pause();
7782
7989
  if (result !== false) this._cancelSpeechPrefetch();
7783
- if (epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.speaking && result !== false)
7990
+ if (epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.speaking && result !== false) {
7784
7991
  this.patch({ paused: true });
7992
+ this.activeRecordingTimeline?.("assistant-pause");
7993
+ }
7785
7994
  } catch (error) {
7786
7995
  if (epoch === this.speechEpoch) this.patch({ error: message(error) });
7787
7996
  }
@@ -7821,6 +8030,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
7821
8030
  const result = await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
7822
8031
  if (epoch === this.speechEpoch && control === this.controlEpoch && result !== false) {
7823
8032
  this.patch({ paused: false });
8033
+ this.activeRecordingTimeline?.("assistant-resume");
7824
8034
  this._prefetchSpeech();
7825
8035
  }
7826
8036
  } catch (error) {
@@ -7864,6 +8074,10 @@ var VoiceCoordinator = class _VoiceCoordinator {
7864
8074
  const controller = new AbortController();
7865
8075
  this.speechController = controller;
7866
8076
  this.speechLaunchPending = true;
8077
+ const timeline = this.activeRecordingTimeline ?? this.recordingTimeline?.();
8078
+ this.activeRecordingTimeline = timeline;
8079
+ let playbackStarted = false;
8080
+ let playbackFinished = false;
7867
8081
  const engine = this.engines[this.snapshot.settings.engine];
7868
8082
  this._resetStaleSpeechItem(item, this._speechPreparationKey());
7869
8083
  item.engine = engine;
@@ -7888,6 +8102,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
7888
8102
  if (this.speechStopError) throw this.speechStopError;
7889
8103
  if (epoch !== this.speechEpoch || this.disposed) return;
7890
8104
  if (this.snapshot.settings.mode === "speaker") {
8105
+ if (this.snapshot.listening) timeline?.("capture-gated");
7891
8106
  await this.stopListening();
7892
8107
  if (this.inputReleaseError) throw this.inputReleaseError;
7893
8108
  }
@@ -7898,7 +8113,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
7898
8113
  if (queued?.prepareError) throw queued.prepareError;
7899
8114
  const options = {
7900
8115
  onPlaybackStart: () => {
7901
- if (epoch === this.speechEpoch && !this.disposed) this.patch({ speechLoading: false });
8116
+ if (epoch === this.speechEpoch && !this.disposed && !controller.signal.aborted && !playbackFinished) {
8117
+ if (!playbackStarted) timeline?.("assistant-start");
8118
+ playbackStarted = true;
8119
+ this.patch({ speechLoading: false });
8120
+ }
7902
8121
  },
7903
8122
  voice: this.snapshot.settings.voice || void 0,
7904
8123
  rate: this.snapshot.settings.rate,
@@ -7931,6 +8150,10 @@ var VoiceCoordinator = class _VoiceCoordinator {
7931
8150
  if (error.name !== "AbortError") this.patch({ error: message(error) });
7932
8151
  }
7933
8152
  } finally {
8153
+ playbackFinished = true;
8154
+ if (playbackStarted) timeline?.("assistant-end");
8155
+ if (this.activeRecordingTimeline === timeline && epoch === this.speechEpoch && !this.queue.length)
8156
+ this.activeRecordingTimeline = null;
7934
8157
  if (epoch === this.speechEpoch && !this.disposed) {
7935
8158
  this.patch({
7936
8159
  speaking: false,
@@ -8089,6 +8312,8 @@ var MeetingController = class {
8089
8312
  if (Object.hasOwn(next, "pending")) policy.pendingTranscriptions = next.pending;
8090
8313
  if (Object.hasOwn(next, "transcribing"))
8091
8314
  policy.pendingTranscriptionRequests = next.transcribing;
8315
+ if (Object.hasOwn(next, "deliveryPending"))
8316
+ policy.pendingDeliveryTranscriptions = next.deliveryPending;
8092
8317
  input?.patch(policy);
8093
8318
  }
8094
8319
  this.snapshot = {
@@ -8261,11 +8486,12 @@ var MeetingController = class {
8261
8486
  this.onActivity?.(source, active);
8262
8487
  input.refreshDeliveryReadiness();
8263
8488
  },
8264
- onProcessingChange: ({ pending, transcribing }) => {
8489
+ onProcessingChange: ({ pending, transcribing, deliveryPending }) => {
8265
8490
  if (!valid()) return;
8266
8491
  const requests = Number.isSafeInteger(transcribing) && transcribing >= 0 ? transcribing : pending;
8267
- this.patch(source, { pending, transcribing: requests });
8268
- if (pending) input.cancelAutoSend({ preserveIntent: true });
8492
+ const delivery = Number.isSafeInteger(deliveryPending) && deliveryPending >= 0 ? deliveryPending : pending;
8493
+ this.patch(source, { pending, transcribing: requests, deliveryPending: delivery });
8494
+ if (delivery) input.cancelAutoSend({ preserveIntent: true });
8269
8495
  else if (!input.getSnapshot().muted) input.maybeScheduleAutoSend();
8270
8496
  },
8271
8497
  onError: (error) => {
@@ -8308,12 +8534,19 @@ var MeetingController = class {
8308
8534
  listening: false,
8309
8535
  recognizing: false,
8310
8536
  pendingTranscriptions: 0,
8311
- pendingTranscriptionRequests: 0
8537
+ pendingTranscriptionRequests: 0,
8538
+ pendingDeliveryTranscriptions: 0
8312
8539
  });
8313
8540
  this.onActivity?.(source, false);
8314
8541
  job?.abort.abort();
8315
8542
  this.transcript.setActive(source, false);
8316
- this.patch(source, { starting: false, listening: false, pending: 0, transcribing: 0 });
8543
+ this.patch(source, {
8544
+ starting: false,
8545
+ listening: false,
8546
+ pending: 0,
8547
+ transcribing: 0,
8548
+ deliveryPending: 0
8549
+ });
8317
8550
  await job?.engine?.stop();
8318
8551
  await job?.meter?.stop();
8319
8552
  }
@@ -9494,6 +9727,12 @@ var PinDetector = class {
9494
9727
  }
9495
9728
  } else this.candidate = 0;
9496
9729
  }
9730
+ /** Advance past unclassified saved samples without inventing a speech/silence transition. */
9731
+ skipTo(position) {
9732
+ this.at = Math.max(this.at, position);
9733
+ this.candidate = 0;
9734
+ this.quiet = 0;
9735
+ }
9497
9736
  finish() {
9498
9737
  if (this.speaking) {
9499
9738
  this.speaking = false;
@@ -9505,6 +9744,7 @@ var PinDetector = class {
9505
9744
 
9506
9745
  // src/modules/recording/RecordingRecognition.ts
9507
9746
  var asError = (value) => value instanceof Error ? value : new Error(String(value));
9747
+ var TRANSCRIPTION_BOUNDARY_MARGIN_MS = 800;
9508
9748
  var uid = () => crypto.randomUUID();
9509
9749
  async function startRecordingRecognition(engine, options, capture) {
9510
9750
  const { signal, lang = engine.lang, onResult, onActivity, onError, onProcessingChange } = options;
@@ -9513,8 +9753,8 @@ async function startRecordingRecognition(engine, options, capture) {
9513
9753
  enabled: engine.recordingEnabled !== false,
9514
9754
  keepPrevious: engine.recordingKeepPrevious ?? 1,
9515
9755
  bitrate: engine.recordingBitrate ?? 48e3,
9516
- preRollMs: engine.recordingPreRollMs ?? 320,
9517
- postRollMs: engine.recordingPostRollMs ?? 320
9756
+ preRollMs: Math.max(TRANSCRIPTION_BOUNDARY_MARGIN_MS, engine.recordingPreRollMs ?? 320),
9757
+ postRollMs: Math.max(TRANSCRIPTION_BOUNDARY_MARGIN_MS, engine.recordingPostRollMs ?? 320)
9518
9758
  };
9519
9759
  const generation = engine.startGeneration;
9520
9760
  const writer = await engine.recordings.begin({
@@ -9562,6 +9802,7 @@ async function startRecordingRecognition(engine, options, capture) {
9562
9802
  onProcessingChange,
9563
9803
  tail: Promise.resolve(),
9564
9804
  pendingWrites: 0,
9805
+ pendingSpeechWrites: 0,
9565
9806
  lang,
9566
9807
  operation: uid(),
9567
9808
  capturedSamples: 0,
@@ -9574,18 +9815,22 @@ async function startRecordingRecognition(engine, options, capture) {
9574
9815
  };
9575
9816
  engine.session = session;
9576
9817
  const valid = () => engine.session === session && !signal?.aborted;
9818
+ const awaitingTail = [];
9577
9819
  let stopping = false;
9820
+ let recoveryPending = false;
9578
9821
  let captureFailure;
9579
9822
  const captureValid = () => engine.session === session || stopping;
9580
9823
  const notify2 = () => {
9581
9824
  if (valid())
9582
9825
  session.onProcessingChange?.({
9583
- queued: session.transcriptionQueue.length,
9826
+ queued: awaitingTail.length + session.transcriptionQueue.length,
9584
9827
  active: !!session.activeRequest,
9585
9828
  // Only queued/running speech attempts belong in the visible transcription status.
9586
- transcribing: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0),
9587
- // Audio writes and final drain still block delivery/release, even in silence.
9588
- pending: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0) + session.pendingWrites + (session.finishing ? 1 : 0)
9829
+ transcribing: awaitingTail.length + session.transcriptionQueue.length + (session.activeRequest ? 1 : 0),
9830
+ // Speech-boundary bookkeeping must settle before delivery, but silent PCM writes must not reset it.
9831
+ deliveryPending: awaitingTail.length + session.transcriptionQueue.length + (session.activeRequest ? 1 : 0) + session.pendingSpeechWrites + (session.finishing || recoveryPending ? 1 : 0),
9832
+ // All writes still protect release, playback admission and page leave.
9833
+ pending: awaitingTail.length + session.transcriptionQueue.length + (session.activeRequest ? 1 : 0) + session.pendingWrites + (session.finishing || recoveryPending ? 1 : 0)
9589
9834
  });
9590
9835
  };
9591
9836
  const failure = (error) => {
@@ -9594,14 +9839,16 @@ async function startRecordingRecognition(engine, options, capture) {
9594
9839
  onError?.(new Error(engine.recordingErrorMessage(error)));
9595
9840
  void engine.stop();
9596
9841
  };
9597
- const serialize = (task, preserve = false) => {
9842
+ const serialize = (task, preserve = false, speech = false) => {
9598
9843
  session.pendingWrites++;
9844
+ if (speech) session.pendingSpeechWrites++;
9599
9845
  notify2();
9600
9846
  const work = session.tail.then(async () => {
9601
9847
  if (preserve || valid()) await task();
9602
9848
  });
9603
9849
  session.tail = work.catch(failure).finally(() => {
9604
9850
  session.pendingWrites--;
9851
+ if (speech) session.pendingSpeechWrites--;
9605
9852
  notify2();
9606
9853
  });
9607
9854
  return work;
@@ -9621,19 +9868,20 @@ async function startRecordingRecognition(engine, options, capture) {
9621
9868
  if (through - Math.max(liveSpeechStart, lastSplitQueued) < max) return;
9622
9869
  const end = Math.max(liveSpeechStart, lastSplitQueued) + max;
9623
9870
  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(() => {
9871
+ void serialize(
9872
+ async () => {
9873
+ const speech = [...writer.session.pins].reverse().find((p) => p.kind === "speech-start");
9874
+ if (speech && valid()) await queueSpeech(speech, end);
9875
+ },
9876
+ false,
9877
+ true
9878
+ ).catch(() => {
9628
9879
  });
9629
9880
  };
9630
9881
  const queueSpeech = async (pin2, end) => {
9631
9882
  for (let at2 = Math.max(pin2.atSample, queuedThrough); at2 < end; at2 += max) {
9632
9883
  const requestedStart = Math.max(0, at2 - policy.preRollMs * rate / 1e3);
9633
- const requestedEnd = Math.min(
9634
- writer.session.capturedSamples,
9635
- Math.min(end, at2 + max) + policy.postRollMs * rate / 1e3
9636
- );
9884
+ const requestedEnd = Math.min(end, at2 + max) + policy.postRollMs * rate / 1e3;
9637
9885
  const attempt = {
9638
9886
  id: uid(),
9639
9887
  segmentId: pin2.id,
@@ -9645,13 +9893,26 @@ async function startRecordingRecognition(engine, options, capture) {
9645
9893
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9646
9894
  status: "pending"
9647
9895
  };
9896
+ awaitingTail.push(attempt);
9897
+ notify2();
9898
+ }
9899
+ queuedThrough = Math.max(queuedThrough, end);
9900
+ await releaseCapturedRanges();
9901
+ };
9902
+ async function releaseCapturedRanges() {
9903
+ if (!valid()) return;
9904
+ const captured = writer.session.capturedSamples;
9905
+ while (awaitingTail.length && (session.finished || awaitingTail[0].endSample <= captured)) {
9906
+ const pending = awaitingTail[0];
9907
+ const attempt = { ...pending, endSample: Math.min(pending.endSample, captured) };
9648
9908
  await writer.attempt(attempt);
9909
+ if (!valid()) return;
9910
+ awaitingTail.shift();
9649
9911
  session.transcriptionQueue.push(attempt);
9650
9912
  notify2();
9651
9913
  }
9652
- queuedThrough = Math.max(queuedThrough, end);
9653
9914
  void drain();
9654
- };
9915
+ }
9655
9916
  const pin = (kind, at2, confirmed) => {
9656
9917
  const startSample = Math.min(
9657
9918
  session.capturedSamples,
@@ -9665,14 +9926,18 @@ async function startRecordingRecognition(engine, options, capture) {
9665
9926
  liveSpeechStart = startSample;
9666
9927
  lastSplitQueued = startSample;
9667
9928
  } 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(() => {
9929
+ void serialize(
9930
+ async () => {
9931
+ const previous = writer.session.pins.at(-1);
9932
+ if (kind === "speech-start" && previous?.kind === "silence-start" && previous.atSample > 0 && startSample - previous.atSample >= paragraphSamples)
9933
+ paragraphStarts.add(startSample);
9934
+ await writer.pin(kind, startSample, confirmedSample);
9935
+ if (valid() && kind === "silence-start" && previous?.kind === "speech-start")
9936
+ await queueSpeech(previous, startSample);
9937
+ },
9938
+ true,
9939
+ true
9940
+ ).catch(() => {
9676
9941
  });
9677
9942
  };
9678
9943
  const detector = new PinDetector({
@@ -9714,7 +9979,8 @@ async function startRecordingRecognition(engine, options, capture) {
9714
9979
  "content-type": "audio/wav",
9715
9980
  "x-dlv-client-id": session.operation,
9716
9981
  "x-dlv-operation-id": attempt.id,
9717
- "x-dlv-language": lang
9982
+ "x-dlv-language": lang,
9983
+ ...transcriptionContextHeader(engine.recognitionContext)
9718
9984
  },
9719
9985
  credentials: "same-origin",
9720
9986
  body: capture.encode(pcm, rate),
@@ -9768,7 +10034,114 @@ async function startRecordingRecognition(engine, options, capture) {
9768
10034
  return;
9769
10035
  }
9770
10036
  const copy = pcm.slice();
9771
- void serialize(() => writer.append(copy), true).catch(() => {
10037
+ void serialize(async () => {
10038
+ await writer.append(copy);
10039
+ await releaseCapturedRanges();
10040
+ }, true).catch(() => {
10041
+ });
10042
+ };
10043
+ let vadGeneration = 0;
10044
+ let restartAttempts = 0;
10045
+ let recovering = null;
10046
+ let deferredFailure = null;
10047
+ const sileroValid = () => valid() && !stopping && !session.finished && !session.finishing;
10048
+ const releaseSilero = () => {
10049
+ ++vadGeneration;
10050
+ const previousCapture = session.capture;
10051
+ const previousVad = session.vad;
10052
+ session.capture = null;
10053
+ session.vad = null;
10054
+ try {
10055
+ previousCapture?.release();
10056
+ } finally {
10057
+ previousVad?.release();
10058
+ }
10059
+ };
10060
+ const terminalVadFailure = (error) => {
10061
+ if (!valid() || stopping || session.finished) return;
10062
+ captureFailure = asError(error).message;
10063
+ onError?.(new Error(engine.vadErrorMessage(error)));
10064
+ void engine.stop();
10065
+ };
10066
+ const installSilero = async () => {
10067
+ const token = ++vadGeneration;
10068
+ let installing = true;
10069
+ let installationFailure;
10070
+ const failed = (error) => {
10071
+ if (token !== vadGeneration || !sileroValid()) return;
10072
+ if (installing) installationFailure = error;
10073
+ else recoverSilero(error);
10074
+ };
10075
+ const vad = engine.vadFactory({
10076
+ onProbability: ({ probability, pcm }) => {
10077
+ if (token !== vadGeneration || !captureValid() || session.finished) return;
10078
+ restartAttempts = 0;
10079
+ detector.frame(probability, pcm.length);
10080
+ checkLiveSplit();
10081
+ },
10082
+ onError: failed,
10083
+ onReset: () => failed(new Error("VAD analysis interrupted."))
10084
+ });
10085
+ session.vad = vad;
10086
+ try {
10087
+ const attached = await engine.vadCapture(
10088
+ engine.meter.context,
10089
+ engine.meter.source,
10090
+ vad,
10091
+ (pcm, sampleRate) => {
10092
+ if (token === vadGeneration) ingest(pcm, sampleRate);
10093
+ }
10094
+ );
10095
+ if (token !== vadGeneration || !sileroValid()) {
10096
+ attached.release();
10097
+ vad.release();
10098
+ throw Object.assign(new Error("Cancelled"), { name: "AbortError" });
10099
+ }
10100
+ session.capture = attached;
10101
+ if (installationFailure) throw installationFailure;
10102
+ } finally {
10103
+ installing = false;
10104
+ }
10105
+ };
10106
+ const retrySilero = async (initialError) => {
10107
+ let error = initialError;
10108
+ while (sileroValid()) {
10109
+ if (error) {
10110
+ releaseSilero();
10111
+ if (restartAttempts >= 2) throw error;
10112
+ restartAttempts++;
10113
+ detector.skipTo(Math.round(session.capturedSamples * 16e3 / rate));
10114
+ await Promise.resolve();
10115
+ }
10116
+ if (!sileroValid()) return;
10117
+ try {
10118
+ await installSilero();
10119
+ return;
10120
+ } catch (failure2) {
10121
+ if (!sileroValid() || asError(failure2).name === "AbortError") return;
10122
+ error = failure2;
10123
+ }
10124
+ }
10125
+ };
10126
+ const recoverSilero = (error) => {
10127
+ if (!sileroValid()) return;
10128
+ if (recovering) {
10129
+ ++vadGeneration;
10130
+ deferredFailure = error;
10131
+ return;
10132
+ }
10133
+ ++vadGeneration;
10134
+ recoveryPending = true;
10135
+ notify2();
10136
+ recovering = Promise.resolve().then(() => retrySilero(error)).catch(terminalVadFailure).finally(() => {
10137
+ recovering = null;
10138
+ const pending = deferredFailure;
10139
+ deferredFailure = null;
10140
+ if (pending) recoverSilero(pending);
10141
+ else {
10142
+ recoveryPending = false;
10143
+ notify2();
10144
+ }
9772
10145
  });
9773
10146
  };
9774
10147
  session.finishRecording = () => {
@@ -9776,10 +10149,12 @@ async function startRecordingRecognition(engine, options, capture) {
9776
10149
  session.finishing = true;
9777
10150
  session.finishPromise = Promise.resolve().then(async () => {
9778
10151
  try {
10152
+ await recovering;
9779
10153
  await session.capture?.finish();
9780
10154
  if (!valid()) return;
9781
10155
  session.finished = true;
9782
10156
  detector.finish();
10157
+ await serialize(releaseCapturedRanges);
9783
10158
  await session.tail;
9784
10159
  await writer.finish();
9785
10160
  session.vad?.release();
@@ -9797,6 +10172,8 @@ async function startRecordingRecognition(engine, options, capture) {
9797
10172
  };
9798
10173
  session.stopRecording = async () => {
9799
10174
  stopping = true;
10175
+ awaitingTail.length = 0;
10176
+ await recovering;
9800
10177
  session.activeRequest?.abort();
9801
10178
  if (session.finishing) session.capture?.release();
9802
10179
  else {
@@ -9826,31 +10203,7 @@ async function startRecordingRecognition(engine, options, capture) {
9826
10203
  signal?.addEventListener("abort", session.abort, { once: true });
9827
10204
  try {
9828
10205
  if (capture.silero) {
9829
- session.vad = engine.vadFactory({
9830
- onProbability: ({ probability, pcm }) => {
9831
- if (captureValid() && !session.finished) {
9832
- detector.frame(probability, pcm.length);
9833
- checkLiveSplit();
9834
- }
9835
- },
9836
- onError: (error) => {
9837
- captureFailure = error.message;
9838
- if (valid()) onError?.(new Error(engine.vadErrorMessage(error)));
9839
- void engine.stop();
9840
- },
9841
- onReset: () => {
9842
- captureFailure = "VAD analysis interrupted.";
9843
- if (valid())
9844
- onError?.(new Error(engine.vadErrorMessage(new Error("VAD analysis interrupted."))));
9845
- void engine.stop();
9846
- }
9847
- });
9848
- session.capture = await engine.vadCapture(
9849
- engine.meter.context,
9850
- engine.meter.source,
9851
- session.vad,
9852
- ingest
9853
- );
10206
+ await retrySilero();
9854
10207
  } else {
9855
10208
  const processor = engine.meter.context.createScriptProcessor(4096, 1, 1);
9856
10209
  const gain = engine.meter.context.createGain();
@@ -9936,6 +10289,7 @@ var WhisperHttpRecognitionEngine = class {
9936
10289
  voiceDetectionPreset = defaultSettings.voiceDetectionPreset,
9937
10290
  voiceDetectionCustomSilenceMs = defaultSettings.voiceDetectionCustomSilenceMs,
9938
10291
  voiceDetectionCustomParagraphMs = defaultSettings.voiceDetectionCustomParagraphMs,
10292
+ recognitionContext = "",
9939
10293
  maxUtteranceSeconds = 60,
9940
10294
  recordings,
9941
10295
  inputSource = "microphone",
@@ -9962,6 +10316,7 @@ var WhisperHttpRecognitionEngine = class {
9962
10316
  this.voiceDetectionPreset = voiceDetectionPreset;
9963
10317
  this.voiceDetectionCustomSilenceMs = voiceDetectionCustomSilenceMs;
9964
10318
  this.voiceDetectionCustomParagraphMs = voiceDetectionCustomParagraphMs;
10319
+ this.recognitionContext = recognitionContext;
9965
10320
  this.maxUtteranceSeconds = maxUtteranceSeconds;
9966
10321
  Object.assign(this, {
9967
10322
  recordings: recordings ?? createRecordingRepository(),
@@ -10053,7 +10408,13 @@ var WhisperHttpRecognitionEngine = class {
10053
10408
  this.session = null;
10054
10409
  session.signal?.removeEventListener("abort", session.abort);
10055
10410
  await session.stopRecording();
10056
- session.onProcessingChange?.({ queued: 0, active: false, transcribing: 0, pending: 0 });
10411
+ session.onProcessingChange?.({
10412
+ queued: 0,
10413
+ active: false,
10414
+ transcribing: 0,
10415
+ deliveryPending: 0,
10416
+ pending: 0
10417
+ });
10057
10418
  }
10058
10419
  };
10059
10420
 
@@ -10532,8 +10893,8 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
10532
10893
  var import_react36 = __toESM(require("react"), 1);
10533
10894
 
10534
10895
  // src/modules/settings/services/releases.ts
10535
- var CURRENT_VERSION = "0.6.0";
10536
- var TESTED_DSH_VERSION = "0.2.1-alpha.2";
10896
+ var CURRENT_VERSION = "0.6.2";
10897
+ var TESTED_DSH_VERSION = "0.2.1-rc.2";
10537
10898
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
10538
10899
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
10539
10900
  var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
@@ -10761,7 +11122,6 @@ function VoiceModeSettings({
10761
11122
 
10762
11123
  // src/modules/settings/sections/conversation/ConversationDelaySettings.tsx
10763
11124
  var import_react40 = __toESM(require("react"), 1);
10764
- var DELAYS = [0, 1, 2, 3, 4];
10765
11125
  function ConversationDelaySettings({ settings, updateSettings }) {
10766
11126
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
10767
11127
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
@@ -10770,10 +11130,12 @@ function ConversationDelaySettings({ settings, updateSettings }) {
10770
11130
  {
10771
11131
  label: speak.responseDelay.label(),
10772
11132
  hint: speak.responseDelay.help(),
10773
- value: String(settings.assistantSpeechDelaySeconds ?? 0),
10774
- options: DELAYS.map((seconds) => ({
11133
+ value: String(
11134
+ settings.assistantSpeechDelaySeconds ?? defaultSettings.assistantSpeechDelaySeconds
11135
+ ),
11136
+ options: assistantSpeechDelayOptions.map((seconds) => ({
10775
11137
  value: String(seconds),
10776
- label: seconds === 0 ? speak.responseDelay.none() : seconds === 1 ? commons.second() : commons.seconds({ seconds })
11138
+ label: seconds < 1 ? commons.milliseconds({ milliseconds: seconds * 1e3 }) : seconds === 1.5 ? speak.responseDelay.oneAndHalf() : commons.seconds({ seconds })
10777
11139
  })),
10778
11140
  onChange: (event) => updateSettings({ assistantSpeechDelaySeconds: Number(event.target.value) })
10779
11141
  }
@@ -11518,7 +11880,18 @@ function RecognitionEngineSettings({
11518
11880
  settings,
11519
11881
  updateSettings
11520
11882
  }
11521
- ), engine === "whisper-http" && /* @__PURE__ */ import_react46.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react46.default.createElement("p", null, recognition.whisper.captureHelp()), /* @__PURE__ */ import_react46.default.createElement(WhisperSettings, { controller })), engine === "qwen-http" && /* @__PURE__ */ import_react46.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react46.default.createElement("p", null, recognition.qwen.captureHelp()), settings.engine !== "qwen-http" && /* @__PURE__ */ import_react46.default.createElement(QwenSettings, { controller })));
11883
+ ), ["whisper-http", "qwen-http"].includes(engine) && /* @__PURE__ */ import_react46.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, engine === "whisper-http" ? /* @__PURE__ */ import_react46.default.createElement(WhisperSettings, { controller }) : settings.engine !== "qwen-http" && /* @__PURE__ */ import_react46.default.createElement(QwenSettings, { controller }), /* @__PURE__ */ import_react46.default.createElement(
11884
+ TextAreaField,
11885
+ {
11886
+ label: recognition.context.label(),
11887
+ hint: recognition.context.hint(),
11888
+ placeholder: recognition.context.placeholder(),
11889
+ value: settings.recognitionContext || "",
11890
+ rows: 3,
11891
+ maxLength: TRANSCRIPTION_CONTEXT_MAX_LENGTH,
11892
+ onCommit: (recognitionContext) => updateSettings({ recognitionContext })
11893
+ }
11894
+ )));
11522
11895
  }
11523
11896
 
11524
11897
  // src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx
@@ -13057,7 +13430,9 @@ function createPluginUpdate({
13057
13430
  if (!result?.ok) return patch({ ...state, phase: "uncertain", problem: "uncertain" });
13058
13431
  const value = result.value;
13059
13432
  if (value.application === "failed" || value.application === "cancelled")
13060
- return fail(value.pendingBuilds?.length ? "scripts" : "failed");
13433
+ return fail(
13434
+ 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"
13435
+ );
13061
13436
  if (value.bundle !== "dsh-live-voice" || value.version !== state.version || !["applied", "restart-required"].includes(value.application))
13062
13437
  return patch({ ...state, phase: "uncertain", problem: "uncertain" });
13063
13438
  patch({ ...state, phase: value.application === "restart-required" ? "restart" : "done" });
@@ -13270,7 +13645,7 @@ async function removeBrowserModel(key, storage = caches) {
13270
13645
  }
13271
13646
 
13272
13647
  // src/modules/recognition/local/assets.ts
13273
- var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.6.0" : "test") + "/";
13648
+ var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.6.2" : "test") + "/";
13274
13649
 
13275
13650
  // src/modules/recognition/local/runtimeIdentity.ts
13276
13651
  var workerProtocol = "asr-worker-v3-ort-1.31.0-dev.20260914-8d85527a0";
@@ -13738,7 +14113,7 @@ var BrowserLocalRecognitionEngine = class extends WhisperHttpRecognitionEngine {
13738
14113
  message;
13739
14114
  release;
13740
14115
  constructor({ manager, modelKey, device = "webgpu", message: message2, ...options }) {
13741
- const margins = ((options.recordingPreRollMs ?? 320) + (options.recordingPostRollMs ?? 320)) / 1e3;
14116
+ const margins = (Math.max(TRANSCRIPTION_BOUNDARY_MARGIN_MS, options.recordingPreRollMs ?? 320) + Math.max(TRANSCRIPTION_BOUNDARY_MARGIN_MS, options.recordingPostRollMs ?? 320)) / 1e3;
13742
14117
  super({
13743
14118
  ...options,
13744
14119
  maxUtteranceSeconds: Math.max(
@@ -13980,6 +14355,15 @@ function apply(ctx) {
13980
14355
  const settingsControllers = /* @__PURE__ */ new Set();
13981
14356
  let disposed = false;
13982
14357
  const recordings = createRecordingRepository();
14358
+ const recordingTimeline = () => {
14359
+ const append = recordings.timeline();
14360
+ return (kind) => {
14361
+ void append(kind).catch(() => {
14362
+ for (const entry of controllers.values())
14363
+ entry.controller.patch({ error: t("dsh-live-voice.recording.error.failed") });
14364
+ });
14365
+ };
14366
+ };
13983
14367
  const localModels = new LocalModelManager();
13984
14368
  void localModels.refresh();
13985
14369
  const leaveGuard = createTranscriptionLeaveGuard(window);
@@ -14008,7 +14392,7 @@ function apply(ctx) {
14008
14392
  },
14009
14393
  updatePlaybackSettings: (next) => meetingEntry?.controller.updateSettings(next),
14010
14394
  handleQuestionResult: (result) => meetingEntry?.controller.composer.handleQuestionResult?.(result),
14011
- canAutoSend: () => !!meetingEntry && !meetingEntry.controller.getSnapshot().recognizing && !meetingEntry.controller.getSnapshot().pendingTranscriptions
14395
+ canAutoSend: () => !!meetingEntry && !meetingEntry.controller.getSnapshot().recognizing && !meetingEntry.controller.deliveryPending()
14012
14396
  },
14013
14397
  settings: () => preferences.getSnapshot(),
14014
14398
  savePreferences: (controller, next) => savePreferences(controller, next),
@@ -14142,6 +14526,7 @@ function apply(ctx) {
14142
14526
  "browserQwenModel",
14143
14527
  "browserAsrDevice",
14144
14528
  "recognitionLang",
14529
+ "recognitionContext",
14145
14530
  "inputDeviceId",
14146
14531
  "recognitionProcessLocally",
14147
14532
  "recognitionAutoInstall",
@@ -14161,6 +14546,7 @@ function apply(ctx) {
14161
14546
  const changesRecognition = (next) => Object.keys(next).some((key) => recognitionSettingKeys.has(key));
14162
14547
  const recordingOptions = (settings, inputSource) => ({
14163
14548
  recordings,
14549
+ recognitionContext: settings.recognitionContext,
14164
14550
  inputSource,
14165
14551
  sourceId: inputSource,
14166
14552
  sourceLabel: inputSource === "shared" ? settings.sharedSourceName : settings.micSourceName,
@@ -14361,13 +14747,14 @@ function apply(ctx) {
14361
14747
  const meter = new MicrophoneMeter();
14362
14748
  const recognition = recognitionFor(settings, meter);
14363
14749
  entry.controller = new VoiceCoordinator({
14750
+ recordingTimeline,
14364
14751
  persistSettings: (next) => savePreferences(entry.controller, next),
14365
14752
  recognition,
14366
14753
  engines: { browser: engineBrowser, say: engineSay, "qwen-http": engineQwen },
14367
14754
  meter,
14368
14755
  composer: {
14369
14756
  getDraft: () => entry.draft,
14370
- canAutoSend: () => !meeting.getSnapshot().shared.pending && !meeting.input().getSnapshot().recognizing,
14757
+ canAutoSend: () => !meeting.input().deliveryPending() && !meeting.input().getSnapshot().recognizing,
14371
14758
  appendFinal: (text, startedAt, paragraphBefore) => {
14372
14759
  meeting.cancelDelivery();
14373
14760
  if (meetingEntry === entry && (entry.meeting?.getSnapshot().shared.listening || entry.controller.getSnapshot().timestamps)) {
@@ -14764,6 +15151,7 @@ function apply(ctx) {
14764
15151
  const meter = new MicrophoneMeter();
14765
15152
  const recognition = recognitionFor(settings, meter);
14766
15153
  const c = new VoiceCoordinator({
15154
+ recordingTimeline,
14767
15155
  persistSettings: (next) => savePreferences(c, next),
14768
15156
  recognition,
14769
15157
  engines: {