dsh-live-voice 0.5.0 → 0.5.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 (64) hide show
  1. package/README.md +20 -112
  2. package/lib/client.js +1608 -559
  3. package/lib/server.js +41 -5
  4. package/lib/vad/manifest.json +2 -2
  5. package/lib/vad/vad.worker.js +1 -1
  6. package/package.json +2 -2
  7. package/src/app/client/apply.tsx +176 -50
  8. package/src/app/client/i18n/catalogs/base.ts +22 -1
  9. package/src/app/client/i18n/catalogs/en.ts +30 -8
  10. package/src/app/client/i18n/catalogs/es.ts +29 -6
  11. package/src/app/client/i18n/catalogs/fr.ts +29 -6
  12. package/src/app/client/i18n/catalogs/hi.ts +30 -8
  13. package/src/app/client/i18n/catalogs/pt-BR.ts +30 -8
  14. package/src/app/client/i18n/catalogs/zh.ts +30 -8
  15. package/src/app/server/registerRoutes.ts +2 -1
  16. package/src/modules/conversation/components/ConversationControls.tsx +3 -17
  17. package/src/modules/conversation/components/createConversationComponents.tsx +2 -16
  18. package/src/modules/conversation/models/meeting.ts +76 -20
  19. package/src/modules/conversation/models/meetingTranscript.ts +10 -2
  20. package/src/modules/core/ARCHITECTURE.md +1 -1
  21. package/src/modules/core/coordinator.ts +144 -15
  22. package/src/modules/core/engineSelection.ts +10 -0
  23. package/src/modules/core/qwen/QwenSettings.tsx +0 -1
  24. package/src/modules/core/settings.ts +38 -1
  25. package/src/modules/recognition/ARCHITECTURE.md +2 -0
  26. package/src/modules/recognition/engines/browser/BrowserRecognitionEngine.ts +57 -5
  27. package/src/modules/recognition/engines/browser/externalAudioSupport.ts +71 -0
  28. package/src/modules/recognition/engines/whisper/WhisperSettings.tsx +0 -1
  29. package/src/modules/settings/ARCHITECTURE.md +4 -0
  30. package/src/modules/settings/components/LiveVoiceSettings.tsx +62 -24
  31. package/src/modules/settings/models/browserVoiceSelection.ts +76 -0
  32. package/src/modules/settings/models/settingsHost.ts +6 -2
  33. package/src/modules/settings/models/settingsStorage.ts +3 -0
  34. package/src/modules/settings/sections/conversation/ConversationDelaySettings.tsx +1 -1
  35. package/src/modules/settings/sections/conversation/ConversationSettingsSection.tsx +12 -10
  36. package/src/modules/settings/sections/conversation/DeliverySettings.tsx +2 -6
  37. package/src/modules/settings/sections/conversation/HoldToTalkSettings.tsx +1 -1
  38. package/src/modules/settings/sections/conversation/MeetingSourceSettings.tsx +30 -0
  39. package/src/modules/settings/sections/conversation/VoiceModeSettings.tsx +5 -5
  40. package/src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx +12 -18
  41. package/src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx +4 -4
  42. package/src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx +6 -4
  43. package/src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx +9 -4
  44. package/src/modules/settings/sections/recognition/VoiceCommandSettings.tsx +13 -12
  45. package/src/modules/settings/sections/speak/OutputFilterSettings.tsx +7 -3
  46. package/src/modules/settings/sections/speak/PlaybackPolicySettings.tsx +6 -6
  47. package/src/modules/settings/sections/speak/SpeakSettingsSection.tsx +5 -3
  48. package/src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx +1 -1
  49. package/src/modules/settings/sections/speak/SpeechEngineSettings.tsx +76 -45
  50. package/src/modules/speak/ARCHITECTURE.md +4 -0
  51. package/src/modules/speak/engines/audio/HostAudioEngine.ts +26 -8
  52. package/src/modules/speak/engines/browser/BrowserSpeakingEngine.ts +75 -13
  53. package/src/modules/speak/services/sessionSpeechMemory.ts +55 -0
  54. package/src/shared/ARCHITECTURE.md +1 -1
  55. package/src/shared/design-system/feedback/Hint.tsx +109 -0
  56. package/src/shared/design-system/forms/CheckboxField.tsx +9 -3
  57. package/src/shared/design-system/forms/PhraseField.tsx +107 -0
  58. package/src/shared/design-system/forms/SelectField.tsx +32 -11
  59. package/src/shared/design-system/icons/icons.ts +5 -0
  60. package/src/shared/design-system/index.ts +1 -0
  61. package/src/shared/design-system/layout/SettingsSection.tsx +45 -5
  62. package/src/shared/design-system/layout/SettingsSectionBody.tsx +6 -0
  63. package/src/shared/design-system/layout/index.ts +1 -0
  64. package/src/styles/index.ts +41 -1
package/lib/client.js CHANGED
@@ -34,10 +34,62 @@ __export(apply_exports, {
34
34
  inject: () => inject
35
35
  });
36
36
  module.exports = __toCommonJS(apply_exports);
37
- var import_react60 = __toESM(require("react"), 1);
37
+ var import_react64 = __toESM(require("react"), 1);
38
+
39
+ // src/modules/speak/services/sessionSpeechMemory.ts
40
+ var SessionSpeechMemory = class {
41
+ constructor() {
42
+ this.sessions = /* @__PURE__ */ new Map();
43
+ }
44
+ session(key, startedAt = Date.now()) {
45
+ if (!this.sessions.has(key))
46
+ this.sessions.set(key, {
47
+ startedAt,
48
+ observed: /* @__PURE__ */ new Set(),
49
+ textById: /* @__PURE__ */ new Map(),
50
+ frontier: null,
51
+ baselined: false,
52
+ checkpoint: null
53
+ });
54
+ return this.sessions.get(key);
55
+ }
56
+ baseline(state, message2, initial = false) {
57
+ const known = state.observed.has(message2.id);
58
+ const previous = state.frontier;
59
+ const position = [message2.turn, message2.step];
60
+ if (!position.every(Number.isFinite)) return true;
61
+ const newer = !previous || position[0] > previous[0] || position[0] === previous[0] && position[1] > previous[1];
62
+ state.observed.add(message2.id);
63
+ if (newer) state.frontier = position;
64
+ if (known) return false;
65
+ if (Number.isFinite(message2.time)) return message2.time < state.startedAt;
66
+ return initial || !state.baselined || !newer;
67
+ }
68
+ reset(startedAt = Date.now()) {
69
+ for (const state of this.sessions.values()) {
70
+ const old = state.checkpoint;
71
+ const suppressed = /* @__PURE__ */ new Set([
72
+ ...old?.suppressed || [],
73
+ ...old?.unfinished || [],
74
+ ...(old?.pending || []).map((item) => item.id)
75
+ ]);
76
+ state.startedAt = startedAt;
77
+ state.checkpoint = {
78
+ consumed: old?.consumed || [],
79
+ unfinished: [],
80
+ suppressed: [...suppressed],
81
+ pending: [],
82
+ paused: false
83
+ };
84
+ }
85
+ }
86
+ clear() {
87
+ this.sessions.clear();
88
+ }
89
+ };
38
90
 
39
91
  // src/modules/recognition/vad/assets.ts
40
- var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.5.0" : "test") + "/";
92
+ var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.5.2" : "test") + "/";
41
93
  async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
42
94
  try {
43
95
  const response = await fetchImpl(sileroVadAssetBase() + "availability", {
@@ -369,7 +421,7 @@ function createVadAssetDiagnostics(base, fetchImpl = globalThis.fetch) {
369
421
  }
370
422
 
371
423
  // src/modules/conversation/components/SpeechStatusBar.tsx
372
- var import_react24 = __toESM(require("react"), 1);
424
+ var import_react26 = __toESM(require("react"), 1);
373
425
 
374
426
  // src/app/client/i18n/catalogs/en.ts
375
427
  var en = {
@@ -430,9 +482,10 @@ var en = {
430
482
  "dsh-live-voice.commons.version.label": "DSH Live Voice v{version}",
431
483
  "dsh-live-voice.commons.version.link": "DSH Live Voice v{version}. Open releases",
432
484
  "dsh-live-voice.commons.version.title": "Version information",
485
+ "dsh-live-voice.meeting.browserLocalUnavailable": "Local browser recognition is unavailable for this language. Install its language pack or explicitly turn off local-only processing in Settings to permit browser-service recognition.",
486
+ "dsh-live-voice.meeting.browserSharedUnsupported": "This browser cannot safely recognize shared audio. Choose Qwen or Whisper HTTP for shared audio.",
433
487
  "dsh-live-voice.meeting.failed": "Recognition failed.",
434
488
  "dsh-live-voice.meeting.help": "Independent sources; transcripts stay in the composer until you send them manually.",
435
- "dsh-live-voice.meeting.httpRequired": "Use Qwen or Whisper HTTP for meeting mode.",
436
489
  "dsh-live-voice.meeting.inactive": "Inactive",
437
490
  "dsh-live-voice.meeting.listening": "Listening",
438
491
  "dsh-live-voice.meeting.microphone": "Microphone",
@@ -455,10 +508,13 @@ var en = {
455
508
  "dsh-live-voice.recognition.browser.localProcessing": "Process recognition locally on this device",
456
509
  "dsh-live-voice.recognition.browser.microphoneHelp": "Browser SpeechRecognition may use the browser or system default microphone instead of this selection.",
457
510
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "Browser-service recognition is enabled. The browser may send microphone audio to its recognition service.",
511
+ "dsh-live-voice.recognition.commands.addPhrase": "Add phrase\u2026",
458
512
  "dsh-live-voice.recognition.commands.clear": "Clear composer",
459
513
  "dsh-live-voice.recognition.commands.enabled": "Enable exact voice commands",
514
+ "dsh-live-voice.recognition.commands.end": "End conversation",
460
515
  "dsh-live-voice.recognition.commands.mute": "Mute composer input",
461
516
  "dsh-live-voice.recognition.commands.queue": "Put in queue",
517
+ "dsh-live-voice.recognition.commands.removePhrase": "Remove phrase",
462
518
  "dsh-live-voice.recognition.commands.resume": "Resume composer input",
463
519
  "dsh-live-voice.recognition.commands.send": "Send to running agent",
464
520
  "dsh-live-voice.recognition.commands.stopSpeech": "Stop assistant speech",
@@ -471,6 +527,9 @@ var en = {
471
527
  "dsh-live-voice.recognition.holdToTalk.help": "While a composer is open, hold Control anywhere on the page to capture speech. Release it to flush queued transcription, wait the configured send delay, queue the message, and close voice capture. Press Escape while holding to cancel.",
472
528
  "dsh-live-voice.recognition.language.automatic": "Automatic \u2014 detect language",
473
529
  "dsh-live-voice.recognition.language.label": "Recognition language",
530
+ "dsh-live-voice.recognition.language.options.chinese": "Chinese (Mandarin)",
531
+ "dsh-live-voice.recognition.language.options.french": "French",
532
+ "dsh-live-voice.recognition.language.options.spanish": "Spanish",
474
533
  "dsh-live-voice.recognition.manualSend.help": "Recognized text stays in the composer until you use the normal DSH Send control.",
475
534
  "dsh-live-voice.recognition.maxUtterance.help": "If speech never pauses, start a new transcription chunk after this duration. Default: 60 seconds.",
476
535
  "dsh-live-voice.recognition.maxUtterance.label": "Maximum continuous speech (seconds)",
@@ -556,8 +615,19 @@ var en = {
556
615
  "dsh-live-voice.settings.engine.refresh": "Refresh available engines",
557
616
  "dsh-live-voice.settings.filters.title": "Filtering",
558
617
  "dsh-live-voice.settings.general.title": "General",
618
+ "dsh-live-voice.settings.layout.delivery": "Message delivery",
619
+ "dsh-live-voice.settings.layout.diagnostics": "Diagnostics",
620
+ "dsh-live-voice.settings.layout.listening": "Listening & interruptions",
621
+ "dsh-live-voice.settings.layout.playback": "Playback & output",
622
+ "dsh-live-voice.settings.layout.shortcut": "Shortcuts",
623
+ "dsh-live-voice.settings.layout.transcription": "Transcription",
624
+ "dsh-live-voice.settings.layout.voice": "Voice & engine",
559
625
  "dsh-live-voice.settings.persistence.loadError": "Could not load Live Voice settings from the server. Reload to try again.",
560
626
  "dsh-live-voice.settings.persistence.saveError": "Could not save Live Voice settings on the server. Please try again.",
627
+ "dsh-live-voice.settings.sourceNames.help": "When both sources capture, these names label new transcript blocks, not individual participants. Saved on blur; blank restores Me / Them. Maximum 80 characters. Existing text is unchanged.",
628
+ "dsh-live-voice.settings.sourceNames.microphone": "Microphone source name",
629
+ "dsh-live-voice.settings.sourceNames.shared": "Shared-audio source name",
630
+ "dsh-live-voice.settings.sourceNames.title": "Meeting source names",
561
631
  "dsh-live-voice.settings.tabs.conversation": "Conversation",
562
632
  "dsh-live-voice.settings.tabs.recognition": "Speech recognition",
563
633
  "dsh-live-voice.settings.tabs.speak": "Speech",
@@ -571,12 +641,13 @@ var en = {
571
641
  "dsh-live-voice.speak.autoPlayback.disable": "Disable automatic assistant speech",
572
642
  "dsh-live-voice.speak.autoPlayback.enable": "Enable automatic assistant speech",
573
643
  "dsh-live-voice.speak.autoPlayback.enabled": "Automatically speak new assistant messages",
574
- "dsh-live-voice.speak.autoPlayback.help": "During a voice conversation, assistant phrases are announced automatically. Playback waits while you are speaking.",
644
+ "dsh-live-voice.speak.autoPlayback.help": "New assistant messages are spoken automatically, even with the microphone off. Playback waits while you are speaking.",
575
645
  "dsh-live-voice.speak.autoPlayback.label": "Automatic assistant speech",
576
646
  "dsh-live-voice.speak.autoPlayback.remainingOne": "Automatic assistant speech: {state} \u2014 {count} speech segment remaining",
577
647
  "dsh-live-voice.speak.autoPlayback.remainingOther": "Automatic assistant speech: {state} \u2014 {count} speech segments remaining",
578
648
  "dsh-live-voice.speak.autoPlayback.status": "Automatic assistant speech: {state}",
579
649
  "dsh-live-voice.speak.browser.automaticVoice": "Automatic local voice",
650
+ "dsh-live-voice.speak.browser.defaultVoice": "default",
580
651
  "dsh-live-voice.speak.browser.label": "Browser speech \u2014 device audio",
581
652
  "dsh-live-voice.speak.browser.name": "Browser speech",
582
653
  "dsh-live-voice.speak.browser.outputHelp": "Browser speech synthesis may ignore the selected output device; this browser API normally follows the system default.",
@@ -644,7 +715,9 @@ var en = {
644
715
  "dsh-live-voice.speak.segmentGap.help": "Wait this many milliseconds between consecutive spoken segments. 400 ms is the default.",
645
716
  "dsh-live-voice.speak.segmentGap.label": "Pause between speech segments",
646
717
  "dsh-live-voice.speak.status.paused": "Speech paused",
647
- "dsh-live-voice.speak.status.playing": "Speaking"
718
+ "dsh-live-voice.speak.status.playing": "Speaking",
719
+ "dsh-live-voice.speak.volume.help": "Speech playback only: 0% is silent; 100% is full volume (default). Microphone input is unchanged. Browser speech changes apply from the next segment.",
720
+ "dsh-live-voice.speak.volume.label": "Speech volume"
648
721
  };
649
722
  var en_default = Object.freeze(en);
650
723
 
@@ -707,9 +780,10 @@ var es = {
707
780
  "dsh-live-voice.commons.version.label": "DSH Voz en vivo v{version}",
708
781
  "dsh-live-voice.commons.version.link": "DSH Voz en vivo v{version}. Abrir la lista de versiones",
709
782
  "dsh-live-voice.commons.version.title": "Informaci\xF3n de versi\xF3n",
783
+ "dsh-live-voice.meeting.browserLocalUnavailable": "El reconocimiento local del navegador no est\xE1 disponible para este idioma. Instala su paquete de idioma o desactiva expl\xEDcitamente el procesamiento solo local en Ajustes para permitir el servicio del navegador.",
784
+ "dsh-live-voice.meeting.browserSharedUnsupported": "Este navegador no puede reconocer audio compartido de forma segura. Elige Qwen o Whisper HTTP para el audio compartido.",
710
785
  "dsh-live-voice.meeting.failed": "Error de reconocimiento.",
711
786
  "dsh-live-voice.meeting.help": "Fuentes independientes; env\xEDa las transcripciones manualmente desde el editor.",
712
- "dsh-live-voice.meeting.httpRequired": "Usa Qwen o Whisper HTTP para el modo reuni\xF3n.",
713
787
  "dsh-live-voice.meeting.inactive": "Inactivo",
714
788
  "dsh-live-voice.meeting.listening": "Escuchando",
715
789
  "dsh-live-voice.meeting.microphone": "Micr\xF3fono",
@@ -732,10 +806,13 @@ var es = {
732
806
  "dsh-live-voice.recognition.browser.localProcessing": "Procesar el reconocimiento localmente en este dispositivo",
733
807
  "dsh-live-voice.recognition.browser.microphoneHelp": "SpeechRecognition puede utilizar el micr\xF3fono predeterminado del navegador o del sistema en lugar del seleccionado aqu\xED.",
734
808
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "El reconocimiento mediante el servicio del navegador est\xE1 activado. El navegador puede enviar el audio del micr\xF3fono a su servicio de reconocimiento.",
809
+ "dsh-live-voice.recognition.commands.addPhrase": "A\xF1adir frase\u2026",
735
810
  "dsh-live-voice.recognition.commands.clear": "Vaciar el cuadro de mensaje",
736
811
  "dsh-live-voice.recognition.commands.enabled": "Activar comandos de voz con coincidencia exacta",
812
+ "dsh-live-voice.recognition.commands.end": "Finalizar conversaci\xF3n",
737
813
  "dsh-live-voice.recognition.commands.mute": "Suspender la entrada de voz en el cuadro de mensaje",
738
814
  "dsh-live-voice.recognition.commands.queue": "A\xF1adir a la cola",
815
+ "dsh-live-voice.recognition.commands.removePhrase": "Eliminar frase",
739
816
  "dsh-live-voice.recognition.commands.resume": "Reanudar la entrada de voz en el cuadro de mensaje",
740
817
  "dsh-live-voice.recognition.commands.send": "Enviar al agente en ejecuci\xF3n",
741
818
  "dsh-live-voice.recognition.commands.stopSpeech": "Detener la lectura en voz alta del asistente",
@@ -748,6 +825,9 @@ var es = {
748
825
  "dsh-live-voice.recognition.holdToTalk.help": "Con un cuadro de mensaje abierto, mant\xE9n Control pulsado en cualquier parte de la p\xE1gina para capturar tu voz. Al soltarlo, se procesa la transcripci\xF3n pendiente, se espera el retraso de env\xEDo configurado, se a\xF1ade el mensaje a la cola y se cierra la captura de voz. Pulsa Escape mientras mantienes Control para cancelar.",
749
826
  "dsh-live-voice.recognition.language.automatic": "Autom\xE1tico \u2014 detectar idioma",
750
827
  "dsh-live-voice.recognition.language.label": "Idioma de reconocimiento",
828
+ "dsh-live-voice.recognition.language.options.chinese": "Chino (mandar\xEDn)",
829
+ "dsh-live-voice.recognition.language.options.french": "Franc\xE9s",
830
+ "dsh-live-voice.recognition.language.options.spanish": "Espa\xF1ol",
751
831
  "dsh-live-voice.recognition.manualSend.help": "El texto reconocido permanece en el cuadro de mensaje hasta que utilices el bot\xF3n de env\xEDo habitual de DSH.",
752
832
  "dsh-live-voice.recognition.maxUtterance.help": "Si hablas sin hacer pausas, se inicia un nuevo fragmento de transcripci\xF3n tras este intervalo. Valor predeterminado: 60 segundos.",
753
833
  "dsh-live-voice.recognition.maxUtterance.label": "Duraci\xF3n m\xE1xima del habla continua (segundos)",
@@ -833,8 +913,19 @@ var es = {
833
913
  "dsh-live-voice.settings.engine.refresh": "Actualizar los motores disponibles",
834
914
  "dsh-live-voice.settings.filters.title": "Filtrado",
835
915
  "dsh-live-voice.settings.general.title": "General",
916
+ "dsh-live-voice.settings.layout.delivery": "Env\xEDo de mensajes",
917
+ "dsh-live-voice.settings.layout.diagnostics": "Diagn\xF3stico",
918
+ "dsh-live-voice.settings.layout.listening": "Escucha e interrupciones",
919
+ "dsh-live-voice.settings.layout.playback": "Reproducci\xF3n y salida",
920
+ "dsh-live-voice.settings.layout.shortcut": "Atajos",
921
+ "dsh-live-voice.settings.layout.transcription": "Transcripci\xF3n",
922
+ "dsh-live-voice.settings.layout.voice": "Voz y motor",
836
923
  "dsh-live-voice.settings.persistence.loadError": "No se pudieron cargar los ajustes de Voz en vivo del servidor. Recarga para volver a intentarlo.",
837
924
  "dsh-live-voice.settings.persistence.saveError": "No se pudieron guardar los ajustes de Voz en vivo en el servidor. Int\xE9ntalo de nuevo.",
925
+ "dsh-live-voice.settings.sourceNames.help": "Cuando ambas fuentes capturan, estos nombres etiquetan nuevos bloques de transcripci\xF3n, no participantes individuales. Se guardan al salir del campo; vac\xEDo restaura Me / Them. M\xE1ximo 80 caracteres. El texto existente no cambia.",
926
+ "dsh-live-voice.settings.sourceNames.microphone": "Nombre de la fuente del micr\xF3fono",
927
+ "dsh-live-voice.settings.sourceNames.shared": "Nombre de la fuente del audio compartido",
928
+ "dsh-live-voice.settings.sourceNames.title": "Nombres de fuentes de la reuni\xF3n",
838
929
  "dsh-live-voice.settings.tabs.conversation": "Conversaci\xF3n",
839
930
  "dsh-live-voice.settings.tabs.recognition": "Reconocimiento de voz",
840
931
  "dsh-live-voice.settings.tabs.speak": "S\xEDntesis de voz",
@@ -848,12 +939,13 @@ var es = {
848
939
  "dsh-live-voice.speak.autoPlayback.disable": "Desactivar voz autom\xE1tica del asistente",
849
940
  "dsh-live-voice.speak.autoPlayback.enable": "Activar voz autom\xE1tica del asistente",
850
941
  "dsh-live-voice.speak.autoPlayback.enabled": "Leer autom\xE1ticamente los nuevos mensajes del asistente en voz alta",
851
- "dsh-live-voice.speak.autoPlayback.help": "Durante una conversaci\xF3n por voz, las frases del asistente se leen autom\xE1ticamente. La reproducci\xF3n espera mientras hablas.",
942
+ "dsh-live-voice.speak.autoPlayback.help": "Los nuevos mensajes del asistente se leen autom\xE1ticamente, incluso con el micr\xF3fono apagado. La reproducci\xF3n espera mientras hablas.",
852
943
  "dsh-live-voice.speak.autoPlayback.label": "Lectura autom\xE1tica del asistente",
853
944
  "dsh-live-voice.speak.autoPlayback.remainingOne": "Lectura autom\xE1tica del asistente: {state} \u2014 queda {count} segmento de voz",
854
945
  "dsh-live-voice.speak.autoPlayback.remainingOther": "Lectura autom\xE1tica del asistente: {state} \u2014 quedan {count} segmentos de voz",
855
946
  "dsh-live-voice.speak.autoPlayback.status": "Lectura autom\xE1tica del asistente: {state}",
856
947
  "dsh-live-voice.speak.browser.automaticVoice": "Voz local autom\xE1tica",
948
+ "dsh-live-voice.speak.browser.defaultVoice": "predeterminada",
857
949
  "dsh-live-voice.speak.browser.label": "S\xEDntesis de voz del navegador \u2014 audio en este dispositivo",
858
950
  "dsh-live-voice.speak.browser.name": "S\xEDntesis de voz del navegador",
859
951
  "dsh-live-voice.speak.browser.outputHelp": "La s\xEDntesis de voz del navegador puede ignorar el dispositivo de salida seleccionado; esta API del navegador normalmente utiliza el predeterminado del sistema.",
@@ -921,7 +1013,9 @@ var es = {
921
1013
  "dsh-live-voice.speak.segmentGap.help": "Espera esta cantidad de milisegundos entre segmentos hablados consecutivos. El valor predeterminado es 400 ms.",
922
1014
  "dsh-live-voice.speak.segmentGap.label": "Pausa entre segmentos de voz",
923
1015
  "dsh-live-voice.speak.status.paused": "Lectura en pausa",
924
- "dsh-live-voice.speak.status.playing": "Hablando"
1016
+ "dsh-live-voice.speak.status.playing": "Hablando",
1017
+ "dsh-live-voice.speak.volume.help": "Solo reproducci\xF3n de voz: 0% es silencio; 100% es el volumen m\xE1ximo (predeterminado). El micr\xF3fono no cambia. En Browser, los cambios se aplican desde el siguiente segmento.",
1018
+ "dsh-live-voice.speak.volume.label": "Volumen de voz"
925
1019
  };
926
1020
  var es_default = Object.freeze(es);
927
1021
 
@@ -984,9 +1078,10 @@ var fr = {
984
1078
  "dsh-live-voice.commons.version.label": "DSH Voix en direct v{version}",
985
1079
  "dsh-live-voice.commons.version.link": "DSH Voix en direct v{version}. Ouvrir la liste des versions",
986
1080
  "dsh-live-voice.commons.version.title": "Informations de version",
1081
+ "dsh-live-voice.meeting.browserLocalUnavailable": "La reconnaissance locale du navigateur est indisponible pour cette langue. Installez son pack linguistique ou d\xE9sactivez explicitement le traitement uniquement local dans les param\xE8tres pour autoriser le service du navigateur.",
1082
+ "dsh-live-voice.meeting.browserSharedUnsupported": "Ce navigateur ne peut pas reconna\xEEtre l\u2019audio partag\xE9 en toute s\xE9curit\xE9. Choisissez Qwen ou Whisper HTTP pour l\u2019audio partag\xE9.",
987
1083
  "dsh-live-voice.meeting.failed": "\xC9chec de reconnaissance.",
988
1084
  "dsh-live-voice.meeting.help": "Sources ind\xE9pendantes ; envoyez les transcriptions manuellement depuis l\u2019\xE9diteur.",
989
- "dsh-live-voice.meeting.httpRequired": "Utilisez Qwen ou Whisper HTTP en mode r\xE9union.",
990
1085
  "dsh-live-voice.meeting.inactive": "Inactif",
991
1086
  "dsh-live-voice.meeting.listening": "\xC9coute",
992
1087
  "dsh-live-voice.meeting.microphone": "Microphone",
@@ -1009,10 +1104,13 @@ var fr = {
1009
1104
  "dsh-live-voice.recognition.browser.localProcessing": "Effectuer la reconnaissance localement sur cet appareil",
1010
1105
  "dsh-live-voice.recognition.browser.microphoneHelp": "SpeechRecognition peut utiliser le microphone par d\xE9faut du navigateur ou du syst\xE8me plut\xF4t que celui s\xE9lectionn\xE9 ici.",
1011
1106
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "La reconnaissance via le service du navigateur est activ\xE9e. Le navigateur peut envoyer le son du microphone \xE0 son service de reconnaissance.",
1107
+ "dsh-live-voice.recognition.commands.addPhrase": "Ajouter une phrase\u2026",
1012
1108
  "dsh-live-voice.recognition.commands.clear": "Vider la zone de r\xE9daction",
1013
1109
  "dsh-live-voice.recognition.commands.enabled": "Activer les commandes vocales par correspondance exacte",
1110
+ "dsh-live-voice.recognition.commands.end": "Terminer la conversation",
1014
1111
  "dsh-live-voice.recognition.commands.mute": "Suspendre la saisie vocale dans la zone de r\xE9daction",
1015
1112
  "dsh-live-voice.recognition.commands.queue": "Mettre en file d\u2019attente",
1113
+ "dsh-live-voice.recognition.commands.removePhrase": "Supprimer la phrase",
1016
1114
  "dsh-live-voice.recognition.commands.resume": "Reprendre la saisie vocale dans la zone de r\xE9daction",
1017
1115
  "dsh-live-voice.recognition.commands.send": "Envoyer \xE0 l\u2019agent en cours d\u2019ex\xE9cution",
1018
1116
  "dsh-live-voice.recognition.commands.stopSpeech": "Arr\xEAter la lecture vocale de l\u2019assistant",
@@ -1025,6 +1123,9 @@ var fr = {
1025
1123
  "dsh-live-voice.recognition.holdToTalk.help": "Lorsqu\u2019une zone de r\xE9daction est ouverte, maintenez Ctrl n\u2019importe o\xF9 sur la page pour capturer votre voix. Rel\xE2chez la touche pour traiter la transcription en attente, attendre le d\xE9lai d\u2019envoi configur\xE9, mettre le message en file d\u2019attente et arr\xEAter la capture vocale. Appuyez sur \xC9chap tout en maintenant Ctrl pour annuler.",
1026
1124
  "dsh-live-voice.recognition.language.automatic": "Automatique \u2014 d\xE9tecter la langue",
1027
1125
  "dsh-live-voice.recognition.language.label": "Langue de reconnaissance",
1126
+ "dsh-live-voice.recognition.language.options.chinese": "Chinois (mandarin)",
1127
+ "dsh-live-voice.recognition.language.options.french": "Fran\xE7ais",
1128
+ "dsh-live-voice.recognition.language.options.spanish": "Espagnol",
1028
1129
  "dsh-live-voice.recognition.manualSend.help": "Le texte reconnu reste dans la zone de r\xE9daction jusqu\u2019\xE0 ce que vous utilisiez le bouton d\u2019envoi habituel de DSH.",
1029
1130
  "dsh-live-voice.recognition.maxUtterance.help": "Si vous parlez sans pause, un nouveau segment de transcription commence apr\xE8s cette dur\xE9e. Valeur par d\xE9faut : 60 secondes.",
1030
1131
  "dsh-live-voice.recognition.maxUtterance.label": "Dur\xE9e maximale de parole continue (secondes)",
@@ -1110,8 +1211,19 @@ var fr = {
1110
1211
  "dsh-live-voice.settings.engine.refresh": "Actualiser les moteurs disponibles",
1111
1212
  "dsh-live-voice.settings.filters.title": "Filtrage",
1112
1213
  "dsh-live-voice.settings.general.title": "G\xE9n\xE9ral",
1214
+ "dsh-live-voice.settings.layout.delivery": "Envoi des messages",
1215
+ "dsh-live-voice.settings.layout.diagnostics": "Diagnostic",
1216
+ "dsh-live-voice.settings.layout.listening": "\xC9coute et interruptions",
1217
+ "dsh-live-voice.settings.layout.playback": "Lecture et sortie",
1218
+ "dsh-live-voice.settings.layout.shortcut": "Raccourcis",
1219
+ "dsh-live-voice.settings.layout.transcription": "Transcription vocale",
1220
+ "dsh-live-voice.settings.layout.voice": "Voix et moteur",
1113
1221
  "dsh-live-voice.settings.persistence.loadError": "Impossible de charger les param\xE8tres Voix en direct depuis le serveur. Rechargez pour r\xE9essayer.",
1114
1222
  "dsh-live-voice.settings.persistence.saveError": "Impossible de sauvegarder les param\xE8tres Voix en direct sur le serveur. R\xE9essayez.",
1223
+ "dsh-live-voice.settings.sourceNames.help": "Lorsque les deux sources capturent, ces noms identifient les nouveaux blocs de transcription, pas les participants individuels. Enregistr\xE9s en quittant le champ ; vide r\xE9tablit Me / Them. Maximum 80 caract\xE8res. Le texte existant reste inchang\xE9.",
1224
+ "dsh-live-voice.settings.sourceNames.microphone": "Nom de la source microphone",
1225
+ "dsh-live-voice.settings.sourceNames.shared": "Nom de la source audio partag\xE9",
1226
+ "dsh-live-voice.settings.sourceNames.title": "Noms des sources de r\xE9union",
1115
1227
  "dsh-live-voice.settings.tabs.conversation": "Conversation",
1116
1228
  "dsh-live-voice.settings.tabs.recognition": "Reconnaissance vocale",
1117
1229
  "dsh-live-voice.settings.tabs.speak": "Synth\xE8se vocale",
@@ -1125,12 +1237,13 @@ var fr = {
1125
1237
  "dsh-live-voice.speak.autoPlayback.disable": "D\xE9sactiver la lecture automatique de l\u2019assistant",
1126
1238
  "dsh-live-voice.speak.autoPlayback.enable": "Activer la lecture automatique de l\u2019assistant",
1127
1239
  "dsh-live-voice.speak.autoPlayback.enabled": "Lire automatiquement les nouveaux messages de l\u2019assistant",
1128
- "dsh-live-voice.speak.autoPlayback.help": "Pendant une conversation vocale, les phrases de l\u2019assistant sont lues automatiquement. La lecture attend pendant que vous parlez.",
1240
+ "dsh-live-voice.speak.autoPlayback.help": "Les nouveaux messages de l\u2019assistant sont lus automatiquement, m\xEAme avec le microphone d\xE9sactiv\xE9. La lecture attend pendant que vous parlez.",
1129
1241
  "dsh-live-voice.speak.autoPlayback.label": "Lecture automatique de l\u2019assistant",
1130
1242
  "dsh-live-voice.speak.autoPlayback.remainingOne": "Lecture automatique de l\u2019assistant : {state} \u2014 {count} segment vocal restant",
1131
1243
  "dsh-live-voice.speak.autoPlayback.remainingOther": "Lecture automatique de l\u2019assistant : {state} \u2014 {count} segments vocaux restants",
1132
1244
  "dsh-live-voice.speak.autoPlayback.status": "Lecture automatique de l\u2019assistant : {state}",
1133
1245
  "dsh-live-voice.speak.browser.automaticVoice": "Voix locale automatique",
1246
+ "dsh-live-voice.speak.browser.defaultVoice": "par d\xE9faut",
1134
1247
  "dsh-live-voice.speak.browser.label": "Synth\xE8se vocale du navigateur \u2014 son sur cet appareil",
1135
1248
  "dsh-live-voice.speak.browser.name": "Synth\xE8se vocale du navigateur",
1136
1249
  "dsh-live-voice.speak.browser.outputHelp": "La synth\xE8se vocale du navigateur peut ignorer le p\xE9riph\xE9rique de sortie s\xE9lectionn\xE9 ; cette API du navigateur utilise normalement celui d\xE9fini par d\xE9faut sur le syst\xE8me.",
@@ -1198,7 +1311,9 @@ var fr = {
1198
1311
  "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.",
1199
1312
  "dsh-live-voice.speak.segmentGap.label": "Pause entre les segments vocaux",
1200
1313
  "dsh-live-voice.speak.status.paused": "Lecture en pause",
1201
- "dsh-live-voice.speak.status.playing": "Lecture en cours"
1314
+ "dsh-live-voice.speak.status.playing": "Lecture en cours",
1315
+ "dsh-live-voice.speak.volume.help": "Lecture vocale uniquement : 0% est silencieux ; 100% est le volume maximal (par d\xE9faut). Le microphone reste inchang\xE9. Dans Browser, les changements prennent effet au segment suivant.",
1316
+ "dsh-live-voice.speak.volume.label": "Volume de la voix"
1202
1317
  };
1203
1318
  var fr_default = Object.freeze(fr);
1204
1319
 
@@ -1261,9 +1376,10 @@ var hi = {
1261
1376
  "dsh-live-voice.commons.version.label": "DSH \u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 v{version}",
1262
1377
  "dsh-live-voice.commons.version.link": "DSH \u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 v{version}\u0964 \u0930\u093F\u0932\u0940\u091C\u093C \u0938\u0942\u091A\u0940 \u0916\u094B\u0932\u0947\u0902",
1263
1378
  "dsh-live-voice.commons.version.title": "\u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u091C\u093E\u0928\u0915\u093E\u0930\u0940",
1379
+ "dsh-live-voice.meeting.browserLocalUnavailable": "\u0907\u0938 \u092D\u093E\u0937\u093E \u0915\u0947 \u0932\u093F\u090F \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0915\u0940 \u0938\u094D\u0925\u093E\u0928\u0940\u092F \u092A\u0939\u091A\u093E\u0928 \u0909\u092A\u0932\u092C\u094D\u0927 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964 \u092D\u093E\u0937\u093E \u092A\u0948\u0915 \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0915\u0930\u0947\u0902 \u092F\u093E \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0938\u0947\u0935\u093E \u0915\u0940 \u0905\u0928\u0941\u092E\u0924\u093F \u0926\u0947\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u0938\u0947\u091F\u093F\u0902\u0917\u094D\u0938 \u092E\u0947\u0902 \u0915\u0947\u0935\u0932 \u0938\u094D\u0925\u093E\u0928\u0940\u092F \u092A\u094D\u0930\u094B\u0938\u0947\u0938\u093F\u0902\u0917 \u0938\u094D\u092A\u0937\u094D\u091F \u0930\u0942\u092A \u0938\u0947 \u092C\u0902\u0926 \u0915\u0930\u0947\u0902\u0964",
1380
+ "dsh-live-voice.meeting.browserSharedUnsupported": "\u092F\u0939 \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0938\u093E\u091D\u093E \u0911\u0921\u093F\u092F\u094B \u0915\u094B \u0938\u0941\u0930\u0915\u094D\u0937\u093F\u0924 \u0930\u0942\u092A \u0938\u0947 \u0928\u0939\u0940\u0902 \u092A\u0939\u091A\u093E\u0928 \u0938\u0915\u0924\u093E\u0964 \u0938\u093E\u091D\u093E \u0911\u0921\u093F\u092F\u094B \u0915\u0947 \u0932\u093F\u090F Qwen \u092F\u093E Whisper HTTP \u091A\u0941\u0928\u0947\u0902\u0964",
1264
1381
  "dsh-live-voice.meeting.failed": "\u092A\u0939\u091A\u093E\u0928 \u0935\u093F\u092B\u0932 \u0939\u0941\u0908\u0964",
1265
1382
  "dsh-live-voice.meeting.help": "\u0938\u094D\u0935\u0924\u0902\u0924\u094D\u0930 \u0938\u094D\u0930\u094B\u0924; \u0938\u0902\u092A\u093E\u0926\u0915 \u0938\u0947 \u0932\u093F\u092A\u094D\u092F\u0902\u0924\u0930\u0923 \u0938\u094D\u0935\u092F\u0902 \u092D\u0947\u091C\u0947\u0902\u0964",
1266
- "dsh-live-voice.meeting.httpRequired": "\u092C\u0948\u0920\u0915 \u092E\u094B\u0921 \u0915\u0947 \u0932\u093F\u090F Qwen \u092F\u093E Whisper HTTP \u0909\u092A\u092F\u094B\u0917 \u0915\u0930\u0947\u0902\u0964",
1267
1383
  "dsh-live-voice.meeting.inactive": "\u0928\u093F\u0937\u094D\u0915\u094D\u0930\u093F\u092F",
1268
1384
  "dsh-live-voice.meeting.listening": "\u0938\u0941\u0928 \u0930\u0939\u093E \u0939\u0948",
1269
1385
  "dsh-live-voice.meeting.microphone": "\u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928",
@@ -1286,10 +1402,13 @@ var hi = {
1286
1402
  "dsh-live-voice.recognition.browser.localProcessing": "\u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928 \u0915\u0940 \u092A\u094D\u0930\u094B\u0938\u0947\u0938\u093F\u0902\u0917 \u0907\u0938\u0940 \u0921\u093F\u0935\u093E\u0907\u0938 \u092A\u0930 \u0938\u094D\u0925\u093E\u0928\u0940\u092F \u0930\u0942\u092A \u0938\u0947 \u0915\u0930\u0947\u0902",
1287
1403
  "dsh-live-voice.recognition.browser.microphoneHelp": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0915\u093E SpeechRecognition \u092F\u0939\u093E\u0901 \u091A\u0941\u0928\u0947 \u0917\u090F \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0915\u0947 \u092C\u091C\u093E\u092F \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u092F\u093E \u0938\u093F\u0938\u094D\u091F\u092E \u0915\u0947 \u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0915\u093E \u0909\u092A\u092F\u094B\u0917 \u0915\u0930 \u0938\u0915\u0924\u093E \u0939\u0948\u0964",
1288
1404
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0938\u0947\u0935\u093E \u0915\u0947 \u091C\u093C\u0930\u093F\u090F \u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928 \u091A\u093E\u0932\u0942 \u0939\u0948\u0964 \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0915\u093E \u0911\u0921\u093F\u092F\u094B \u0905\u092A\u0928\u0940 \u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928 \u0938\u0947\u0935\u093E \u0915\u094B \u092D\u0947\u091C \u0938\u0915\u0924\u093E \u0939\u0948\u0964",
1405
+ "dsh-live-voice.recognition.commands.addPhrase": "\u0935\u093E\u0915\u094D\u092F\u093E\u0902\u0936 \u091C\u094B\u0921\u093C\u0947\u0902\u2026",
1289
1406
  "dsh-live-voice.recognition.commands.clear": "\u0938\u0902\u0926\u0947\u0936 \u0932\u093F\u0916\u0928\u0947 \u0915\u093E \u092C\u0949\u0915\u094D\u0938 \u0916\u093E\u0932\u0940 \u0915\u0930\u0947\u0902",
1290
1407
  "dsh-live-voice.recognition.commands.enabled": "\u0939\u0942\u092C\u0939\u0942 \u092E\u093F\u0932\u093E\u0928 \u0935\u093E\u0932\u0947 \u0935\u0949\u0907\u0938 \u0915\u092E\u093E\u0902\u0921 \u091A\u093E\u0932\u0942 \u0915\u0930\u0947\u0902",
1408
+ "dsh-live-voice.recognition.commands.end": "\u092C\u093E\u0924\u091A\u0940\u0924 \u0938\u092E\u093E\u092A\u094D\u0924 \u0915\u0930\u0947\u0902",
1291
1409
  "dsh-live-voice.recognition.commands.mute": "\u0938\u0902\u0926\u0947\u0936 \u092C\u0949\u0915\u094D\u0938 \u092E\u0947\u0902 \u0935\u0949\u0907\u0938 \u0907\u0928\u092A\u0941\u091F \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
1292
1410
  "dsh-live-voice.recognition.commands.queue": "\u0915\u0924\u093E\u0930 \u092E\u0947\u0902 \u091C\u094B\u0921\u093C\u0947\u0902",
1411
+ "dsh-live-voice.recognition.commands.removePhrase": "\u0935\u093E\u0915\u094D\u092F\u093E\u0902\u0936 \u0939\u091F\u093E\u090F\u0901",
1293
1412
  "dsh-live-voice.recognition.commands.resume": "\u0938\u0902\u0926\u0947\u0936 \u092C\u0949\u0915\u094D\u0938 \u092E\u0947\u0902 \u0935\u0949\u0907\u0938 \u0907\u0928\u092A\u0941\u091F \u092B\u093F\u0930 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902",
1294
1413
  "dsh-live-voice.recognition.commands.send": "\u091A\u0932 \u0930\u0939\u0947 \u090F\u091C\u0947\u0902\u091F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
1295
1414
  "dsh-live-voice.recognition.commands.stopSpeech": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0935\u093E\u091A\u0928 \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
@@ -1302,6 +1421,9 @@ var hi = {
1302
1421
  "dsh-live-voice.recognition.holdToTalk.help": "\u0938\u0902\u0926\u0947\u0936 \u092C\u0949\u0915\u094D\u0938 \u0916\u0941\u0932\u093E \u0939\u094B\u0928\u0947 \u092A\u0930, \u092A\u0947\u091C \u092A\u0930 \u0915\u0939\u0940\u0902 \u092D\u0940 Control \u0926\u092C\u093E\u090F \u0930\u0916\u0915\u0930 \u0906\u0935\u093E\u091C\u093C \u0930\u093F\u0915\u0949\u0930\u094D\u0921 \u0915\u0930\u0947\u0902\u0964 \u091B\u094B\u0921\u093C\u0928\u0947 \u092A\u0930 \u0915\u0924\u093E\u0930 \u092E\u0947\u0902 \u092E\u094C\u091C\u0942\u0926 \u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u0936\u0928 \u092A\u0942\u0930\u093E \u0915\u093F\u092F\u093E \u091C\u093E\u0924\u093E \u0939\u0948, \u0924\u092F \u092D\u0947\u091C\u0928\u0947 \u0915\u0947 \u0935\u093F\u0932\u0902\u092C \u0924\u0915 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0939\u094B\u0924\u0940 \u0939\u0948, \u0938\u0902\u0926\u0947\u0936 \u0915\u0924\u093E\u0930 \u092E\u0947\u0902 \u091C\u0941\u0921\u093C\u0924\u093E \u0939\u0948 \u0914\u0930 \u0906\u0935\u093E\u091C\u093C \u0930\u093F\u0915\u0949\u0930\u094D\u0921 \u0915\u0930\u0928\u093E \u092C\u0902\u0926 \u0939\u094B \u091C\u093E\u0924\u093E \u0939\u0948\u0964 \u0930\u0926\u094D\u0926 \u0915\u0930\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F Control \u0926\u092C\u093E\u090F \u0930\u0916\u0924\u0947 \u0939\u0941\u090F Escape \u0926\u092C\u093E\u090F\u0901\u0964",
1303
1422
  "dsh-live-voice.recognition.language.automatic": "\u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u2014 \u092D\u093E\u0937\u093E \u092A\u0939\u091A\u093E\u0928\u0947\u0902",
1304
1423
  "dsh-live-voice.recognition.language.label": "\u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928 \u0915\u0940 \u092D\u093E\u0937\u093E",
1424
+ "dsh-live-voice.recognition.language.options.chinese": "\u091A\u0940\u0928\u0940 (\u092E\u0902\u0926\u093E\u0930\u093F\u0928)",
1425
+ "dsh-live-voice.recognition.language.options.french": "\u092B\u093C\u094D\u0930\u0947\u0902\u091A",
1426
+ "dsh-live-voice.recognition.language.options.spanish": "\u0938\u094D\u092A\u0947\u0928\u093F\u0936",
1305
1427
  "dsh-live-voice.recognition.manualSend.help": "\u092A\u0939\u091A\u093E\u0928\u093E \u0917\u092F\u093E \u092A\u093E\u0920 \u0938\u0902\u0926\u0947\u0936 \u092C\u0949\u0915\u094D\u0938 \u092E\u0947\u0902 \u0924\u092C \u0924\u0915 \u0930\u0939\u0924\u093E \u0939\u0948 \u091C\u092C \u0924\u0915 \u0906\u092A DSH \u0915\u0947 \u0938\u093E\u092E\u093E\u0928\u094D\u092F \u092D\u0947\u091C\u0947\u0902 \u092C\u091F\u0928 \u0915\u093E \u0909\u092A\u092F\u094B\u0917 \u0928\u0939\u0940\u0902 \u0915\u0930\u0924\u0947\u0964",
1306
1428
  "dsh-live-voice.recognition.maxUtterance.help": "\u092F\u0926\u093F \u092C\u094B\u0932\u0928\u093E \u092C\u093F\u0928\u093E \u0930\u0941\u0915\u0947 \u091C\u093E\u0930\u0940 \u0930\u0939\u0947, \u0924\u094B \u0907\u0938 \u0905\u0935\u0927\u093F \u0915\u0947 \u092C\u093E\u0926 \u0928\u092F\u093E \u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u0936\u0928 \u0916\u0902\u0921 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902\u0964 \u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F: 60 \u0938\u0947\u0915\u0902\u0921\u0964",
1307
1429
  "dsh-live-voice.recognition.maxUtterance.label": "\u0932\u0917\u093E\u0924\u093E\u0930 \u092C\u094B\u0932\u0928\u0947 \u0915\u0940 \u0905\u0927\u093F\u0915\u0924\u092E \u0905\u0935\u0927\u093F (\u0938\u0947\u0915\u0902\u0921)",
@@ -1387,8 +1509,19 @@ var hi = {
1387
1509
  "dsh-live-voice.settings.engine.refresh": "\u0909\u092A\u0932\u092C\u094D\u0927 \u0907\u0902\u091C\u0928 \u0930\u0940\u092B\u093C\u094D\u0930\u0947\u0936 \u0915\u0930\u0947\u0902",
1388
1510
  "dsh-live-voice.settings.filters.title": "\u092B\u093C\u093F\u0932\u094D\u091F\u0930\u093F\u0902\u0917",
1389
1511
  "dsh-live-voice.settings.general.title": "\u0938\u093E\u092E\u093E\u0928\u094D\u092F",
1512
+ "dsh-live-voice.settings.layout.delivery": "\u0938\u0902\u0926\u0947\u0936 \u092D\u0947\u091C\u0928\u093E",
1513
+ "dsh-live-voice.settings.layout.diagnostics": "\u0928\u093F\u0926\u093E\u0928",
1514
+ "dsh-live-voice.settings.layout.listening": "\u0938\u0941\u0928\u0928\u093E \u0914\u0930 \u0935\u094D\u092F\u0935\u0927\u093E\u0928",
1515
+ "dsh-live-voice.settings.layout.playback": "\u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u0914\u0930 \u0906\u0909\u091F\u092A\u0941\u091F",
1516
+ "dsh-live-voice.settings.layout.shortcut": "\u0936\u0949\u0930\u094D\u091F\u0915\u091F",
1517
+ "dsh-live-voice.settings.layout.transcription": "\u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u0936\u0928",
1518
+ "dsh-live-voice.settings.layout.voice": "\u0906\u0935\u093E\u091C\u093C \u0914\u0930 \u0907\u0902\u091C\u0928",
1390
1519
  "dsh-live-voice.settings.persistence.loadError": "\u0938\u0930\u094D\u0935\u0930 \u0938\u0947 \u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 \u0938\u0947\u091F\u093F\u0902\u0917 \u0932\u094B\u0921 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940\u0902\u0964 \u092B\u093F\u0930 \u092A\u094D\u0930\u092F\u093E\u0938 \u0915\u0930\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u092A\u0947\u091C \u0930\u0940\u0932\u094B\u0921 \u0915\u0930\u0947\u0902\u0964",
1391
1520
  "dsh-live-voice.settings.persistence.saveError": "\u0938\u0930\u094D\u0935\u0930 \u092A\u0930 \u0932\u093E\u0907\u0935 \u0935\u0949\u0907\u0938 \u0938\u0947\u091F\u093F\u0902\u0917 \u0938\u0947\u0935 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940\u0902\u0964 \u092B\u093F\u0930 \u092A\u094D\u0930\u092F\u093E\u0938 \u0915\u0930\u0947\u0902\u0964",
1521
+ "dsh-live-voice.settings.sourceNames.help": "\u091C\u092C \u0926\u094B\u0928\u094B\u0902 \u0938\u094D\u0930\u094B\u0924 \u0930\u093F\u0915\u0949\u0930\u094D\u0921 \u0915\u0930\u0924\u0947 \u0939\u0948\u0902, \u092F\u0947 \u0928\u093E\u092E \u0928\u090F \u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u091F \u092C\u094D\u0932\u0949\u0915 \u0915\u094B \u091A\u093F\u0939\u094D\u0928\u093F\u0924 \u0915\u0930\u0924\u0947 \u0939\u0948\u0902, \u0905\u0932\u0917-\u0905\u0932\u0917 \u092A\u094D\u0930\u0924\u093F\u092D\u093E\u0917\u093F\u092F\u094B\u0902 \u0915\u094B \u0928\u0939\u0940\u0902\u0964 \u092B\u093C\u0940\u0932\u094D\u0921 \u091B\u094B\u0921\u093C\u0928\u0947 \u092A\u0930 \u0938\u0939\u0947\u091C\u0947 \u091C\u093E\u0924\u0947 \u0939\u0948\u0902; \u0916\u093E\u0932\u0940 \u0930\u0916\u0928\u0947 \u092A\u0930 Me / Them \u0935\u093E\u092A\u0938 \u0906\u0924\u0947 \u0939\u0948\u0902\u0964 \u0905\u0927\u093F\u0915\u0924\u092E 80 \u0905\u0915\u094D\u0937\u0930\u0964 \u092E\u094C\u091C\u0942\u0926\u093E \u092A\u093E\u0920 \u0928\u0939\u0940\u0902 \u092C\u0926\u0932\u0924\u093E\u0964",
1522
+ "dsh-live-voice.settings.sourceNames.microphone": "\u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0938\u094D\u0930\u094B\u0924 \u0915\u093E \u0928\u093E\u092E",
1523
+ "dsh-live-voice.settings.sourceNames.shared": "\u0938\u093E\u091D\u093E \u0911\u0921\u093F\u092F\u094B \u0938\u094D\u0930\u094B\u0924 \u0915\u093E \u0928\u093E\u092E",
1524
+ "dsh-live-voice.settings.sourceNames.title": "\u092C\u0948\u0920\u0915 \u0938\u094D\u0930\u094B\u0924\u094B\u0902 \u0915\u0947 \u0928\u093E\u092E",
1392
1525
  "dsh-live-voice.settings.tabs.conversation": "\u092C\u093E\u0924\u091A\u0940\u0924",
1393
1526
  "dsh-live-voice.settings.tabs.recognition": "\u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928",
1394
1527
  "dsh-live-voice.settings.tabs.speak": "\u0935\u093E\u091A\u0928",
@@ -1402,12 +1535,13 @@ var hi = {
1402
1535
  "dsh-live-voice.speak.autoPlayback.disable": "\u0938\u0939\u093E\u092F\u0915 \u0915\u0940 \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0906\u0935\u093E\u091C\u093C \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
1403
1536
  "dsh-live-voice.speak.autoPlayback.enable": "\u0938\u0939\u093E\u092F\u0915 \u0915\u0940 \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0906\u0935\u093E\u091C\u093C \u091A\u093E\u0932\u0942 \u0915\u0930\u0947\u0902",
1404
1537
  "dsh-live-voice.speak.autoPlayback.enabled": "\u0938\u0939\u093E\u092F\u0915 \u0915\u0947 \u0928\u090F \u0938\u0902\u0926\u0947\u0936 \u0905\u092A\u0928\u0947 \u0906\u092A \u092A\u0922\u093C\u0915\u0930 \u0938\u0941\u0928\u093E\u090F\u0901",
1405
- "dsh-live-voice.speak.autoPlayback.help": "\u0935\u0949\u0907\u0938 \u092C\u093E\u0924\u091A\u0940\u0924 \u0915\u0947 \u0926\u094C\u0930\u093E\u0928 \u0938\u0939\u093E\u092F\u0915 \u0915\u0947 \u0935\u093E\u0915\u094D\u092F\u093E\u0902\u0936 \u0905\u092A\u0928\u0947 \u0906\u092A \u092A\u0922\u093C\u0915\u0930 \u0938\u0941\u0928\u093E\u090F \u091C\u093E\u0924\u0947 \u0939\u0948\u0902\u0964 \u091C\u092C \u0906\u092A \u092C\u094B\u0932 \u0930\u0939\u0947 \u0939\u094B\u0902, \u0924\u094B \u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0915\u0930\u0924\u093E \u0939\u0948\u0964",
1538
+ "dsh-live-voice.speak.autoPlayback.help": "\u0938\u0939\u093E\u092F\u0915 \u0915\u0947 \u0928\u090F \u0938\u0902\u0926\u0947\u0936 \u0905\u092A\u0928\u0947 \u0906\u092A \u092A\u0922\u093C\u0915\u0930 \u0938\u0941\u0928\u093E\u090F \u091C\u093E\u0924\u0947 \u0939\u0948\u0902, \u092D\u0932\u0947 \u0939\u0940 \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u092C\u0902\u0926 \u0939\u094B\u0964 \u091C\u092C \u0906\u092A \u092C\u094B\u0932 \u0930\u0939\u0947 \u0939\u094B\u0902, \u0924\u094B \u092A\u094D\u0932\u0947\u092C\u0948\u0915 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0915\u0930\u0924\u093E \u0939\u0948\u0964",
1406
1539
  "dsh-live-voice.speak.autoPlayback.label": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0935\u093E\u091A\u0928",
1407
1540
  "dsh-live-voice.speak.autoPlayback.remainingOne": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0935\u093E\u091A\u0928: {state} \u2014 {count} \u0935\u093E\u0915\u094D \u0916\u0902\u0921 \u0936\u0947\u0937 \u0939\u0948",
1408
1541
  "dsh-live-voice.speak.autoPlayback.remainingOther": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0935\u093E\u091A\u0928: {state} \u2014 {count} \u0935\u093E\u0915\u094D \u0916\u0902\u0921 \u0936\u0947\u0937 \u0939\u0948\u0902",
1409
1542
  "dsh-live-voice.speak.autoPlayback.status": "\u0938\u0939\u093E\u092F\u0915 \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u0935\u093E\u091A\u0928: {state}",
1410
1543
  "dsh-live-voice.speak.browser.automaticVoice": "\u0938\u094D\u0925\u093E\u0928\u0940\u092F \u0906\u0935\u093E\u091C\u093C \u0915\u093E \u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u091A\u092F\u0928",
1544
+ "dsh-live-voice.speak.browser.defaultVoice": "\u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F",
1411
1545
  "dsh-live-voice.speak.browser.label": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0935\u093E\u091A\u0928 \u2014 \u0907\u0938 \u0921\u093F\u0935\u093E\u0907\u0938 \u092A\u0930 \u0911\u0921\u093F\u092F\u094B",
1412
1546
  "dsh-live-voice.speak.browser.name": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0935\u093E\u091A\u0928",
1413
1547
  "dsh-live-voice.speak.browser.outputHelp": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u0915\u093E \u0935\u093E\u0915\u094D \u0938\u0902\u0936\u094D\u0932\u0947\u0937\u0923 \u091A\u0941\u0928\u0947 \u0917\u090F \u0906\u0909\u091F\u092A\u0941\u091F \u0921\u093F\u0935\u093E\u0907\u0938 \u0915\u094B \u0905\u0928\u0926\u0947\u0916\u093E \u0915\u0930 \u0938\u0915\u0924\u093E \u0939\u0948; \u092F\u0939 \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 API \u0938\u093E\u092E\u093E\u0928\u094D\u092F\u0924\u0903 \u0938\u093F\u0938\u094D\u091F\u092E \u0915\u0947 \u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F \u0915\u093E \u0909\u092A\u092F\u094B\u0917 \u0915\u0930\u0924\u093E \u0939\u0948\u0964",
@@ -1475,7 +1609,9 @@ var hi = {
1475
1609
  "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",
1476
1610
  "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",
1477
1611
  "dsh-live-voice.speak.status.paused": "\u0935\u093E\u091A\u0928 \u0920\u0939\u0930\u093E \u0939\u0941\u0906 \u0939\u0948",
1478
- "dsh-live-voice.speak.status.playing": "\u092A\u0922\u093C\u0915\u0930 \u0938\u0941\u0928\u093E\u092F\u093E \u091C\u093E \u0930\u0939\u093E \u0939\u0948"
1612
+ "dsh-live-voice.speak.status.playing": "\u092A\u0922\u093C\u0915\u0930 \u0938\u0941\u0928\u093E\u092F\u093E \u091C\u093E \u0930\u0939\u093E \u0939\u0948",
1613
+ "dsh-live-voice.speak.volume.help": "\u0915\u0947\u0935\u0932 \u0935\u093E\u0923\u0940 \u092A\u094D\u0932\u0947\u092C\u0948\u0915: 0% \u092E\u094C\u0928 \u0939\u0948; 100% \u092A\u0942\u0930\u094D\u0923 \u0906\u0935\u093E\u091C\u093C \u0939\u0948 (\u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F)\u0964 \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0928\u0939\u0940\u0902 \u092C\u0926\u0932\u0924\u093E\u0964 Browser \u092E\u0947\u0902 \u092C\u0926\u0932\u093E\u0935 \u0905\u0917\u0932\u0947 \u0916\u0902\u0921 \u0938\u0947 \u0932\u093E\u0917\u0942 \u0939\u094B\u0924\u0947 \u0939\u0948\u0902\u0964",
1614
+ "dsh-live-voice.speak.volume.label": "\u0935\u093E\u0923\u0940 \u0915\u0940 \u0906\u0935\u093E\u091C\u093C \u0915\u093E \u0938\u094D\u0924\u0930"
1479
1615
  };
1480
1616
  var hi_default = Object.freeze(hi);
1481
1617
 
@@ -1538,9 +1674,10 @@ var ptBR = {
1538
1674
  "dsh-live-voice.commons.version.label": "DSH Voz ao Vivo v{version}",
1539
1675
  "dsh-live-voice.commons.version.link": "DSH Voz ao Vivo v{version}. Abrir vers\xF5es",
1540
1676
  "dsh-live-voice.commons.version.title": "Informa\xE7\xF5es da vers\xE3o",
1677
+ "dsh-live-voice.meeting.browserLocalUnavailable": "O reconhecimento local do navegador est\xE1 indispon\xEDvel para este idioma. Instale o pacote de idioma ou desative explicitamente o processamento somente local nas Configura\xE7\xF5es para permitir o servi\xE7o do navegador.",
1678
+ "dsh-live-voice.meeting.browserSharedUnsupported": "Este navegador n\xE3o pode reconhecer \xE1udio compartilhado com seguran\xE7a. Escolha Qwen ou Whisper HTTP para \xE1udio compartilhado.",
1541
1679
  "dsh-live-voice.meeting.failed": "Falha no reconhecimento.",
1542
1680
  "dsh-live-voice.meeting.help": "Fontes independentes; as transcri\xE7\xF5es ficam no composer at\xE9 voc\xEA envi\xE1-las manualmente.",
1543
- "dsh-live-voice.meeting.httpRequired": "Use Qwen ou Whisper HTTP no modo reuni\xE3o.",
1544
1681
  "dsh-live-voice.meeting.inactive": "Inativo",
1545
1682
  "dsh-live-voice.meeting.listening": "Ouvindo",
1546
1683
  "dsh-live-voice.meeting.microphone": "Microfone",
@@ -1563,10 +1700,13 @@ var ptBR = {
1563
1700
  "dsh-live-voice.recognition.browser.localProcessing": "Processar reconhecimento localmente neste dispositivo",
1564
1701
  "dsh-live-voice.recognition.browser.microphoneHelp": "O SpeechRecognition do navegador pode usar o microfone padr\xE3o do sistema ou do navegador em vez desta sele\xE7\xE3o.",
1565
1702
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "O reconhecimento pelo servi\xE7o do navegador est\xE1 ativado. O navegador pode enviar o \xE1udio do microfone para o servi\xE7o remoto.",
1703
+ "dsh-live-voice.recognition.commands.addPhrase": "Adicionar frase\u2026",
1566
1704
  "dsh-live-voice.recognition.commands.clear": "Limpar editor de mensagens",
1567
1705
  "dsh-live-voice.recognition.commands.enabled": "Ativar comandos de voz exatos",
1706
+ "dsh-live-voice.recognition.commands.end": "Encerrar conversa",
1568
1707
  "dsh-live-voice.recognition.commands.mute": "Silenciar entrada do editor de mensagens",
1569
1708
  "dsh-live-voice.recognition.commands.queue": "Adicionar \xE0 fila",
1709
+ "dsh-live-voice.recognition.commands.removePhrase": "Remover frase",
1570
1710
  "dsh-live-voice.recognition.commands.resume": "Retomar entrada do editor de mensagens",
1571
1711
  "dsh-live-voice.recognition.commands.send": "Enviar ao agente em execu\xE7\xE3o",
1572
1712
  "dsh-live-voice.recognition.commands.stopSpeech": "Parar fala do assistente",
@@ -1579,6 +1719,9 @@ var ptBR = {
1579
1719
  "dsh-live-voice.recognition.holdToTalk.help": "Com o editor aberto, segure Control em qualquer lugar da p\xE1gina para capturar a fala. Solte para concluir as transcri\xE7\xF5es pendentes, aguardar o atraso configurado, enfileirar a mensagem e encerrar a captura. Pressione Escape enquanto segura para cancelar.",
1580
1720
  "dsh-live-voice.recognition.language.automatic": "Autom\xE1tico \u2014 detectar idioma",
1581
1721
  "dsh-live-voice.recognition.language.label": "Idioma de reconhecimento",
1722
+ "dsh-live-voice.recognition.language.options.chinese": "Chin\xEAs (mandarim)",
1723
+ "dsh-live-voice.recognition.language.options.french": "Franc\xEAs",
1724
+ "dsh-live-voice.recognition.language.options.spanish": "Espanhol",
1582
1725
  "dsh-live-voice.recognition.manualSend.help": "O texto reconhecido permanece no editor at\xE9 que voc\xEA use o bot\xE3o normal de envio do DSH.",
1583
1726
  "dsh-live-voice.recognition.maxUtterance.help": "Se a fala nunca pausar, inicia um novo trecho de transcri\xE7\xE3o ap\xF3s esta dura\xE7\xE3o. Padr\xE3o: 60 segundos.",
1584
1727
  "dsh-live-voice.recognition.maxUtterance.label": "Fala cont\xEDnua m\xE1xima (segundos)",
@@ -1664,8 +1807,19 @@ var ptBR = {
1664
1807
  "dsh-live-voice.settings.engine.refresh": "Atualizar mecanismos dispon\xEDveis",
1665
1808
  "dsh-live-voice.settings.filters.title": "Filtragem",
1666
1809
  "dsh-live-voice.settings.general.title": "Gerais",
1810
+ "dsh-live-voice.settings.layout.delivery": "Envio de mensagens",
1811
+ "dsh-live-voice.settings.layout.diagnostics": "Diagn\xF3stico",
1812
+ "dsh-live-voice.settings.layout.listening": "Escuta e interrup\xE7\xF5es",
1813
+ "dsh-live-voice.settings.layout.playback": "Reprodu\xE7\xE3o e sa\xEDda",
1814
+ "dsh-live-voice.settings.layout.shortcut": "Atalhos",
1815
+ "dsh-live-voice.settings.layout.transcription": "Transcri\xE7\xE3o",
1816
+ "dsh-live-voice.settings.layout.voice": "Voz e mecanismo",
1667
1817
  "dsh-live-voice.settings.persistence.loadError": "N\xE3o foi poss\xEDvel carregar as configura\xE7\xF5es da Voz ao Vivo do servidor. Recarregue para tentar novamente.",
1668
1818
  "dsh-live-voice.settings.persistence.saveError": "N\xE3o foi poss\xEDvel salvar as configura\xE7\xF5es da Voz ao Vivo no servidor. Tente novamente.",
1819
+ "dsh-live-voice.settings.sourceNames.help": "Quando as duas fontes capturam, estes nomes identificam novos blocos da transcri\xE7\xE3o, n\xE3o participantes individuais. Salvos ao sair do campo; vazio restaura Me / Them. M\xE1ximo de 80 caracteres. O texto existente n\xE3o muda.",
1820
+ "dsh-live-voice.settings.sourceNames.microphone": "Nome da fonte do microfone",
1821
+ "dsh-live-voice.settings.sourceNames.shared": "Nome da fonte do \xE1udio compartilhado",
1822
+ "dsh-live-voice.settings.sourceNames.title": "Nomes das fontes da reuni\xE3o",
1669
1823
  "dsh-live-voice.settings.tabs.conversation": "Conversa",
1670
1824
  "dsh-live-voice.settings.tabs.recognition": "Reconhecimento de voz",
1671
1825
  "dsh-live-voice.settings.tabs.speak": "Fala",
@@ -1679,12 +1833,13 @@ var ptBR = {
1679
1833
  "dsh-live-voice.speak.autoPlayback.disable": "Desativar fala autom\xE1tica da IA",
1680
1834
  "dsh-live-voice.speak.autoPlayback.enable": "Ativar fala autom\xE1tica da IA",
1681
1835
  "dsh-live-voice.speak.autoPlayback.enabled": "Falar automaticamente novas mensagens do assistente",
1682
- "dsh-live-voice.speak.autoPlayback.help": "Durante uma conversa por voz, as frases do assistente s\xE3o anunciadas automaticamente. A reprodu\xE7\xE3o aguarda enquanto voc\xEA fala.",
1836
+ "dsh-live-voice.speak.autoPlayback.help": "Novas mensagens do assistente s\xE3o faladas automaticamente, mesmo com o microfone desligado. A reprodu\xE7\xE3o aguarda enquanto voc\xEA fala.",
1683
1837
  "dsh-live-voice.speak.autoPlayback.label": "Fala autom\xE1tica do assistente",
1684
1838
  "dsh-live-voice.speak.autoPlayback.remainingOne": "Fala autom\xE1tica do assistente: {state} \u2014 resta {count} segmento de fala",
1685
1839
  "dsh-live-voice.speak.autoPlayback.remainingOther": "Fala autom\xE1tica do assistente: {state} \u2014 restam {count} segmentos de fala",
1686
1840
  "dsh-live-voice.speak.autoPlayback.status": "Fala autom\xE1tica do assistente: {state}",
1687
1841
  "dsh-live-voice.speak.browser.automaticVoice": "Voz local autom\xE1tica",
1842
+ "dsh-live-voice.speak.browser.defaultVoice": "padr\xE3o",
1688
1843
  "dsh-live-voice.speak.browser.label": "Fala do navegador \u2014 \xE1udio neste dispositivo",
1689
1844
  "dsh-live-voice.speak.browser.name": "Fala do navegador",
1690
1845
  "dsh-live-voice.speak.browser.outputHelp": "A s\xEDntese de fala do navegador pode ignorar o dispositivo de sa\xEDda selecionado; esta API normalmente segue o padr\xE3o do sistema.",
@@ -1752,7 +1907,9 @@ var ptBR = {
1752
1907
  "dsh-live-voice.speak.segmentGap.help": "Aguarde esta quantidade de milissegundos entre trechos falados consecutivos. O padr\xE3o \xE9 400 ms.",
1753
1908
  "dsh-live-voice.speak.segmentGap.label": "Pausa entre trechos de fala",
1754
1909
  "dsh-live-voice.speak.status.paused": "Fala pausada",
1755
- "dsh-live-voice.speak.status.playing": "Falando"
1910
+ "dsh-live-voice.speak.status.playing": "Falando",
1911
+ "dsh-live-voice.speak.volume.help": "Somente reprodu\xE7\xE3o da fala: 0% \xE9 silencioso; 100% \xE9 o volume m\xE1ximo (padr\xE3o). O microfone n\xE3o \xE9 alterado. No Browser, mudan\xE7as valem a partir do pr\xF3ximo trecho.",
1912
+ "dsh-live-voice.speak.volume.label": "Volume da fala"
1756
1913
  };
1757
1914
  var pt_BR_default = Object.freeze(ptBR);
1758
1915
 
@@ -1815,9 +1972,10 @@ var zh = {
1815
1972
  "dsh-live-voice.commons.version.label": "DSH \u5B9E\u65F6\u8BED\u97F3 v{version}",
1816
1973
  "dsh-live-voice.commons.version.link": "DSH \u5B9E\u65F6\u8BED\u97F3 v{version}\u3002\u6253\u5F00\u7248\u672C\u53D1\u5E03\u5217\u8868",
1817
1974
  "dsh-live-voice.commons.version.title": "\u7248\u672C\u4FE1\u606F",
1975
+ "dsh-live-voice.meeting.browserLocalUnavailable": "\u6B64\u8BED\u8A00\u7684\u6D4F\u89C8\u5668\u672C\u5730\u8BC6\u522B\u4E0D\u53EF\u7528\u3002\u8BF7\u5B89\u88C5\u8BED\u8A00\u5305\uFF0C\u6216\u5728\u8BBE\u7F6E\u4E2D\u660E\u786E\u5173\u95ED\u4EC5\u672C\u5730\u5904\u7406\uFF0C\u4EE5\u5141\u8BB8\u6D4F\u89C8\u5668\u670D\u52A1\u8BC6\u522B\u3002",
1976
+ "dsh-live-voice.meeting.browserSharedUnsupported": "\u6B64\u6D4F\u89C8\u5668\u65E0\u6CD5\u5B89\u5168\u5730\u8BC6\u522B\u5171\u4EAB\u97F3\u9891\u3002\u8BF7\u4E3A\u5171\u4EAB\u97F3\u9891\u9009\u62E9 Qwen \u6216 Whisper HTTP\u3002",
1818
1977
  "dsh-live-voice.meeting.failed": "\u8BED\u97F3\u8BC6\u522B\u5931\u8D25\u3002",
1819
1978
  "dsh-live-voice.meeting.help": "\u72EC\u7ACB\u6765\u6E90\uFF1B\u8BF7\u4ECE\u7F16\u8F91\u5668\u624B\u52A8\u53D1\u9001\u8F6C\u5199\u5185\u5BB9\u3002",
1820
- "dsh-live-voice.meeting.httpRequired": "\u4F1A\u8BAE\u6A21\u5F0F\u9700\u8981 Qwen \u6216 Whisper HTTP\u3002",
1821
1979
  "dsh-live-voice.meeting.inactive": "\u672A\u542F\u7528",
1822
1980
  "dsh-live-voice.meeting.listening": "\u6B63\u5728\u8046\u542C",
1823
1981
  "dsh-live-voice.meeting.microphone": "\u9EA6\u514B\u98CE",
@@ -1840,10 +1998,13 @@ var zh = {
1840
1998
  "dsh-live-voice.recognition.browser.localProcessing": "\u5728\u6B64\u8BBE\u5907\u4E0A\u672C\u5730\u5904\u7406\u8BED\u97F3\u8BC6\u522B",
1841
1999
  "dsh-live-voice.recognition.browser.microphoneHelp": "\u6D4F\u89C8\u5668 SpeechRecognition \u53EF\u80FD\u4F7F\u7528\u6D4F\u89C8\u5668\u6216\u7CFB\u7EDF\u9ED8\u8BA4\u9EA6\u514B\u98CE\uFF0C\u800C\u975E\u6B64\u5904\u9009\u62E9\u7684\u8BBE\u5907\u3002",
1842
2000
  "dsh-live-voice.recognition.browser.remoteServiceWarning": "\u5DF2\u542F\u7528\u6D4F\u89C8\u5668\u670D\u52A1\u8BED\u97F3\u8BC6\u522B\u3002\u6D4F\u89C8\u5668\u53EF\u80FD\u4F1A\u5C06\u9EA6\u514B\u98CE\u97F3\u9891\u53D1\u9001\u81F3\u5176\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\u3002",
2001
+ "dsh-live-voice.recognition.commands.addPhrase": "\u6DFB\u52A0\u77ED\u8BED\u2026",
1843
2002
  "dsh-live-voice.recognition.commands.clear": "\u6E05\u7A7A\u6D88\u606F\u8F93\u5165\u6846",
1844
2003
  "dsh-live-voice.recognition.commands.enabled": "\u542F\u7528\u7CBE\u786E\u5339\u914D\u7684\u8BED\u97F3\u547D\u4EE4",
2004
+ "dsh-live-voice.recognition.commands.end": "\u7ED3\u675F\u5BF9\u8BDD",
1845
2005
  "dsh-live-voice.recognition.commands.mute": "\u505C\u6B62\u5411\u6D88\u606F\u8F93\u5165\u6846\u8F93\u5165\u8BED\u97F3",
1846
2006
  "dsh-live-voice.recognition.commands.queue": "\u52A0\u5165\u961F\u5217",
2007
+ "dsh-live-voice.recognition.commands.removePhrase": "\u5220\u9664\u77ED\u8BED",
1847
2008
  "dsh-live-voice.recognition.commands.resume": "\u6062\u590D\u5411\u6D88\u606F\u8F93\u5165\u6846\u8F93\u5165\u8BED\u97F3",
1848
2009
  "dsh-live-voice.recognition.commands.send": "\u53D1\u9001\u7ED9\u8FD0\u884C\u4E2D\u7684\u667A\u80FD\u4F53",
1849
2010
  "dsh-live-voice.recognition.commands.stopSpeech": "\u505C\u6B62\u52A9\u624B\u6717\u8BFB",
@@ -1856,6 +2017,9 @@ var zh = {
1856
2017
  "dsh-live-voice.recognition.holdToTalk.help": "\u6D88\u606F\u8F93\u5165\u6846\u6253\u5F00\u65F6\uFF0C\u5728\u9875\u9762\u4EFB\u610F\u4F4D\u7F6E\u6309\u4F4F Control \u952E\u5373\u53EF\u91C7\u96C6\u8BED\u97F3\u3002\u677E\u5F00\u540E\u4F1A\u5904\u7406\u5B8C\u961F\u5217\u4E2D\u7684\u8F6C\u5199\uFF0C\u7B49\u5F85\u8BBE\u5B9A\u7684\u53D1\u9001\u5EF6\u8FDF\uFF0C\u5C06\u6D88\u606F\u52A0\u5165\u961F\u5217\uFF0C\u7136\u540E\u5173\u95ED\u8BED\u97F3\u91C7\u96C6\u3002\u6309\u4F4F\u671F\u95F4\u6309 Escape \u952E\u53EF\u53D6\u6D88\u3002",
1857
2018
  "dsh-live-voice.recognition.language.automatic": "\u81EA\u52A8 \u2014 \u68C0\u6D4B\u8BED\u8A00",
1858
2019
  "dsh-live-voice.recognition.language.label": "\u8BC6\u522B\u8BED\u8A00",
2020
+ "dsh-live-voice.recognition.language.options.chinese": "\u4E2D\u6587\uFF08\u666E\u901A\u8BDD\uFF09",
2021
+ "dsh-live-voice.recognition.language.options.french": "\u6CD5\u8BED",
2022
+ "dsh-live-voice.recognition.language.options.spanish": "\u897F\u73ED\u7259\u8BED",
1859
2023
  "dsh-live-voice.recognition.manualSend.help": "\u8BC6\u522B\u51FA\u7684\u6587\u672C\u4F1A\u4FDD\u7559\u5728\u6D88\u606F\u8F93\u5165\u6846\u4E2D\uFF0C\u76F4\u5230\u4F60\u4F7F\u7528 DSH \u7684\u5E38\u89C4\u53D1\u9001\u6309\u94AE\u3002",
1860
2024
  "dsh-live-voice.recognition.maxUtterance.help": "\u5982\u679C\u8BF4\u8BDD\u4E00\u76F4\u6CA1\u6709\u505C\u987F\uFF0C\u5219\u5728\u6B64\u65F6\u957F\u540E\u5F00\u59CB\u65B0\u7684\u8F6C\u5199\u7247\u6BB5\u3002\u9ED8\u8BA4\u503C\uFF1A60 \u79D2\u3002",
1861
2025
  "dsh-live-voice.recognition.maxUtterance.label": "\u6700\u957F\u8FDE\u7EED\u8BED\u97F3\uFF08\u79D2\uFF09",
@@ -1941,8 +2105,19 @@ var zh = {
1941
2105
  "dsh-live-voice.settings.engine.refresh": "\u5237\u65B0\u53EF\u7528\u5F15\u64CE",
1942
2106
  "dsh-live-voice.settings.filters.title": "\u8FC7\u6EE4",
1943
2107
  "dsh-live-voice.settings.general.title": "\u5E38\u89C4",
2108
+ "dsh-live-voice.settings.layout.delivery": "\u6D88\u606F\u53D1\u9001",
2109
+ "dsh-live-voice.settings.layout.diagnostics": "\u8BCA\u65AD",
2110
+ "dsh-live-voice.settings.layout.listening": "\u8046\u542C\u4E0E\u6253\u65AD",
2111
+ "dsh-live-voice.settings.layout.playback": "\u64AD\u653E\u4E0E\u8F93\u51FA",
2112
+ "dsh-live-voice.settings.layout.shortcut": "\u5FEB\u6377\u952E",
2113
+ "dsh-live-voice.settings.layout.transcription": "\u8BED\u97F3\u8F6C\u5199",
2114
+ "dsh-live-voice.settings.layout.voice": "\u58F0\u97F3\u4E0E\u5F15\u64CE",
1944
2115
  "dsh-live-voice.settings.persistence.loadError": "\u65E0\u6CD5\u4ECE\u670D\u52A1\u5668\u52A0\u8F7D \u5B9E\u65F6\u8BED\u97F3 \u8BBE\u7F6E\u3002\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
1945
2116
  "dsh-live-voice.settings.persistence.saveError": "\u65E0\u6CD5\u5728\u670D\u52A1\u5668\u4E0A\u4FDD\u5B58 \u5B9E\u65F6\u8BED\u97F3 \u8BBE\u7F6E\u3002\u8BF7\u91CD\u8BD5\u3002",
2117
+ "dsh-live-voice.settings.sourceNames.help": "\u540C\u65F6\u91C7\u96C6\u4E24\u4E2A\u97F3\u6E90\u65F6\uFF0C\u8FD9\u4E9B\u540D\u79F0\u6807\u8BB0\u65B0\u7684\u8F6C\u5F55\u6BB5\u843D\uFF0C\u800C\u975E\u5404\u4E2A\u53C2\u4F1A\u8005\u3002\u79BB\u5F00\u8F93\u5165\u6846\u65F6\u4FDD\u5B58\uFF1B\u7559\u7A7A\u6062\u590D Me / Them\u3002\u6700\u591A 80 \u4E2A\u5B57\u7B26\u3002\u73B0\u6709\u6587\u672C\u4E0D\u4F1A\u6539\u53D8\u3002",
2118
+ "dsh-live-voice.settings.sourceNames.microphone": "\u9EA6\u514B\u98CE\u97F3\u6E90\u540D\u79F0",
2119
+ "dsh-live-voice.settings.sourceNames.shared": "\u5171\u4EAB\u97F3\u9891\u97F3\u6E90\u540D\u79F0",
2120
+ "dsh-live-voice.settings.sourceNames.title": "\u4F1A\u8BAE\u97F3\u6E90\u540D\u79F0",
1946
2121
  "dsh-live-voice.settings.tabs.conversation": "\u5BF9\u8BDD",
1947
2122
  "dsh-live-voice.settings.tabs.recognition": "\u8BED\u97F3\u8BC6\u522B",
1948
2123
  "dsh-live-voice.settings.tabs.speak": "\u8BED\u97F3\u5408\u6210",
@@ -1956,12 +2131,13 @@ var zh = {
1956
2131
  "dsh-live-voice.speak.autoPlayback.disable": "\u5173\u95ED\u52A9\u624B\u81EA\u52A8\u6717\u8BFB",
1957
2132
  "dsh-live-voice.speak.autoPlayback.enable": "\u5F00\u542F\u52A9\u624B\u81EA\u52A8\u6717\u8BFB",
1958
2133
  "dsh-live-voice.speak.autoPlayback.enabled": "\u81EA\u52A8\u6717\u8BFB\u65B0\u7684\u52A9\u624B\u6D88\u606F",
1959
- "dsh-live-voice.speak.autoPlayback.help": "\u8BED\u97F3\u5BF9\u8BDD\u671F\u95F4\uFF0C\u52A9\u624B\u7684\u8BED\u53E5\u4F1A\u81EA\u52A8\u6717\u8BFB\u3002\u4F60\u8BF4\u8BDD\u65F6\u4F1A\u7B49\u5F85\uFF0C\u4E0D\u4F1A\u64AD\u653E\u3002",
2134
+ "dsh-live-voice.speak.autoPlayback.help": "\u5373\u4F7F\u9EA6\u514B\u98CE\u5173\u95ED\uFF0C\u52A9\u624B\u7684\u65B0\u6D88\u606F\u4E5F\u4F1A\u81EA\u52A8\u6717\u8BFB\u3002\u4F60\u8BF4\u8BDD\u65F6\u4F1A\u7B49\u5F85\uFF0C\u4E0D\u4F1A\u64AD\u653E\u3002",
1960
2135
  "dsh-live-voice.speak.autoPlayback.label": "\u52A9\u624B\u81EA\u52A8\u6717\u8BFB",
1961
2136
  "dsh-live-voice.speak.autoPlayback.remainingOne": "\u52A9\u624B\u81EA\u52A8\u6717\u8BFB\uFF1A{state} \u2014 \u5269\u4F59 {count} \u4E2A\u8BED\u97F3\u7247\u6BB5",
1962
2137
  "dsh-live-voice.speak.autoPlayback.remainingOther": "\u52A9\u624B\u81EA\u52A8\u6717\u8BFB\uFF1A{state} \u2014 \u5269\u4F59 {count} \u4E2A\u8BED\u97F3\u7247\u6BB5",
1963
2138
  "dsh-live-voice.speak.autoPlayback.status": "\u52A9\u624B\u81EA\u52A8\u6717\u8BFB\uFF1A{state}",
1964
2139
  "dsh-live-voice.speak.browser.automaticVoice": "\u81EA\u52A8\u9009\u62E9\u672C\u5730\u8BED\u97F3",
2140
+ "dsh-live-voice.speak.browser.defaultVoice": "\u9ED8\u8BA4",
1965
2141
  "dsh-live-voice.speak.browser.label": "\u6D4F\u89C8\u5668\u8BED\u97F3\u5408\u6210 \u2014 \u5728\u6B64\u8BBE\u5907\u64AD\u653E",
1966
2142
  "dsh-live-voice.speak.browser.name": "\u6D4F\u89C8\u5668\u8BED\u97F3\u5408\u6210",
1967
2143
  "dsh-live-voice.speak.browser.outputHelp": "\u6D4F\u89C8\u5668\u8BED\u97F3\u5408\u6210\u53EF\u80FD\u5FFD\u7565\u6240\u9009\u8F93\u51FA\u8BBE\u5907\uFF1B\u6B64\u6D4F\u89C8\u5668 API \u901A\u5E38\u4F7F\u7528\u7CFB\u7EDF\u9ED8\u8BA4\u8BBE\u5907\u3002",
@@ -2029,7 +2205,9 @@ var zh = {
2029
2205
  "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",
2030
2206
  "dsh-live-voice.speak.segmentGap.label": "\u8BED\u97F3\u7247\u6BB5\u4E4B\u95F4\u7684\u505C\u987F",
2031
2207
  "dsh-live-voice.speak.status.paused": "\u8BED\u97F3\u5DF2\u6682\u505C",
2032
- "dsh-live-voice.speak.status.playing": "\u6B63\u5728\u6717\u8BFB"
2208
+ "dsh-live-voice.speak.status.playing": "\u6B63\u5728\u6717\u8BFB",
2209
+ "dsh-live-voice.speak.volume.help": "\u4EC5\u8C03\u6574\u8BED\u97F3\u64AD\u653E\uFF1A0% \u4E3A\u9759\u97F3\uFF0C100% \u4E3A\u6700\u5927\u97F3\u91CF\uFF08\u9ED8\u8BA4\uFF09\u3002\u4E0D\u6539\u53D8\u9EA6\u514B\u98CE\u8F93\u5165\u3002Browser \u7684\u66F4\u6539\u4ECE\u4E0B\u4E00\u8BED\u97F3\u7247\u6BB5\u751F\u6548\u3002",
2210
+ "dsh-live-voice.speak.volume.label": "\u8BED\u97F3\u97F3\u91CF"
2033
2211
  };
2034
2212
  var zh_default = Object.freeze(zh);
2035
2213
 
@@ -2233,6 +2411,11 @@ var import_react3 = __toESM(require("react"), 1);
2233
2411
 
2234
2412
  // src/shared/design-system/icons/icons.ts
2235
2413
  var iconPaths = {
2414
+ conversation: "M4 3h16v13H8l-4 4V3M8 7h8M8 11h5",
2415
+ keyboard: "M3 6h18v12H3V6M7 10h.01M11 10h.01M15 10h.01M18 10h.01M7 14h10",
2416
+ headphones: "M3 14v-3a9 9 0 0 1 18 0v3M3 12h4v8H3v-8M17 12h4v8h-4v-8",
2417
+ commands: "M3 6l2 2 3-4M11 6h10M3 14l2 2 3-4M11 14h10M11 20h10",
2418
+ bug: "M8 8h8v9a4 4 0 0 1-8 0V8M9 8V6a3 3 0 0 1 6 0v2M3 9l5 2M16 11l5-2M3 16h5M16 16h5M5 22l3-3M16 19l3 3",
2236
2419
  meeting: "M8 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6M16 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6M2 20v-3a6 6 0 0 1 12 0v3M16 13a5 5 0 0 1 6 5v2",
2237
2420
  meetingOff: "M8 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6M16 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6M2 20v-3a6 6 0 0 1 12 0v3M17 16l5 5M22 16l-5 5",
2238
2421
  mic: "M9 5a3 3 0 0 1 6 0v7a3 3 0 0 1-6 0V5M6 10v2a6 6 0 0 0 12 0v-2M12 18v4M8 22h8",
@@ -2330,22 +2513,134 @@ var import_react8 = __toESM(require("react"), 1);
2330
2513
  // src/shared/design-system/feedback/StatusMessage.tsx
2331
2514
  var import_react9 = __toESM(require("react"), 1);
2332
2515
 
2333
- // src/shared/design-system/forms/CheckboxField.tsx
2516
+ // src/shared/design-system/feedback/Hint.tsx
2334
2517
  var import_react10 = __toESM(require("react"), 1);
2518
+ function Hint({
2519
+ label,
2520
+ children,
2521
+ id: suppliedId
2522
+ }) {
2523
+ const generatedId = import_react10.default.useId();
2524
+ const id2 = suppliedId || generatedId;
2525
+ const [open, setOpen] = import_react10.default.useState(false);
2526
+ const trigger = import_react10.default.useRef(null);
2527
+ const bubble = import_react10.default.useRef(null);
2528
+ const hovered = import_react10.default.useRef(false);
2529
+ const focused = import_react10.default.useRef(false);
2530
+ const timer = import_react10.default.useRef(void 0);
2531
+ const cancel = () => clearTimeout(timer.current);
2532
+ const closeLater = () => {
2533
+ cancel();
2534
+ timer.current = setTimeout(() => {
2535
+ if (!hovered.current && !focused.current) setOpen(false);
2536
+ }, 120);
2537
+ };
2538
+ import_react10.default.useEffect(() => () => clearTimeout(timer.current), []);
2539
+ import_react10.default.useLayoutEffect(() => {
2540
+ const node = bubble.current;
2541
+ if (!open || !node || !trigger.current) return;
2542
+ node.showPopover?.();
2543
+ const position = () => {
2544
+ const anchor = trigger.current.getBoundingClientRect();
2545
+ const rect = node.getBoundingClientRect();
2546
+ node.style.left = Math.max(12, Math.min(anchor.left, window.innerWidth - rect.width - 12)) + "px";
2547
+ const below = anchor.bottom + 8;
2548
+ node.style.top = Math.max(
2549
+ 12,
2550
+ below + rect.height <= window.innerHeight - 12 ? below : anchor.top - rect.height - 8
2551
+ ) + "px";
2552
+ };
2553
+ position();
2554
+ const dismiss = (e) => {
2555
+ if (e.key === "Escape") {
2556
+ e.stopPropagation();
2557
+ setOpen(false);
2558
+ }
2559
+ };
2560
+ const outside = (e) => {
2561
+ if (!node.contains(e.target) && !trigger.current?.contains(e.target))
2562
+ setOpen(false);
2563
+ };
2564
+ document.addEventListener("keydown", dismiss, true);
2565
+ document.addEventListener("pointerdown", outside, true);
2566
+ window.addEventListener("resize", position);
2567
+ window.addEventListener("scroll", position, true);
2568
+ return () => {
2569
+ node.hidePopover?.();
2570
+ document.removeEventListener("keydown", dismiss, true);
2571
+ document.removeEventListener("pointerdown", outside, true);
2572
+ window.removeEventListener("resize", position);
2573
+ window.removeEventListener("scroll", position, true);
2574
+ };
2575
+ }, [open]);
2576
+ return /* @__PURE__ */ import_react10.default.createElement(
2577
+ "span",
2578
+ {
2579
+ className: "dlv-hint",
2580
+ onMouseEnter: () => {
2581
+ hovered.current = true;
2582
+ cancel();
2583
+ setOpen(true);
2584
+ },
2585
+ onMouseLeave: () => {
2586
+ hovered.current = false;
2587
+ closeLater();
2588
+ }
2589
+ },
2590
+ /* @__PURE__ */ import_react10.default.createElement(
2591
+ "button",
2592
+ {
2593
+ ref: trigger,
2594
+ type: "button",
2595
+ className: "dlv-hint-trigger",
2596
+ "aria-label": label,
2597
+ "aria-describedby": open ? id2 : void 0,
2598
+ "aria-expanded": open,
2599
+ onFocus: () => {
2600
+ focused.current = true;
2601
+ cancel();
2602
+ setOpen(true);
2603
+ },
2604
+ onBlur: () => {
2605
+ focused.current = false;
2606
+ closeLater();
2607
+ },
2608
+ onClick: () => setOpen(true)
2609
+ },
2610
+ "?"
2611
+ ),
2612
+ /* @__PURE__ */ import_react10.default.createElement(
2613
+ "span",
2614
+ {
2615
+ ref: bubble,
2616
+ id: id2,
2617
+ role: "tooltip",
2618
+ popover: "manual",
2619
+ hidden: !open,
2620
+ className: "dlv-hint-bubble"
2621
+ },
2622
+ children
2623
+ )
2624
+ );
2625
+ }
2626
+
2627
+ // src/shared/design-system/forms/CheckboxField.tsx
2628
+ var import_react11 = __toESM(require("react"), 1);
2335
2629
  function CheckboxField({
2336
2630
  label,
2337
2631
  description,
2632
+ hint,
2338
2633
  className = "dlv-check",
2339
2634
  ...props
2340
2635
  }) {
2341
- return /* @__PURE__ */ import_react10.default.createElement("div", null, /* @__PURE__ */ import_react10.default.createElement("label", { className }, /* @__PURE__ */ import_react10.default.createElement("input", { ...props, type: "checkbox" }), " ", label), description ? /* @__PURE__ */ import_react10.default.createElement("p", { className: "dlv-setting-description" }, description) : null);
2636
+ return /* @__PURE__ */ import_react11.default.createElement("div", null, /* @__PURE__ */ import_react11.default.createElement("div", { className: "dlv-field-label" }, /* @__PURE__ */ import_react11.default.createElement("label", { className }, /* @__PURE__ */ import_react11.default.createElement("input", { ...props, type: "checkbox" }), " ", label), hint && /* @__PURE__ */ import_react11.default.createElement(Hint, { label }, hint)), description ? /* @__PURE__ */ import_react11.default.createElement("p", { className: "dlv-setting-description" }, description) : null);
2342
2637
  }
2343
2638
 
2344
2639
  // src/shared/design-system/forms/NumberField.tsx
2345
- var import_react12 = __toESM(require("react"), 1);
2640
+ var import_react13 = __toESM(require("react"), 1);
2346
2641
 
2347
2642
  // src/shared/design-system/forms/DraftField.tsx
2348
- var import_react11 = __toESM(require("react"), 1);
2643
+ var import_react12 = __toESM(require("react"), 1);
2349
2644
  function DraftField({
2350
2645
  label,
2351
2646
  value,
@@ -2359,10 +2654,10 @@ function DraftField({
2359
2654
  ...props
2360
2655
  }) {
2361
2656
  const saved = String(value ?? defaultValue ?? "");
2362
- const [draft, setDraft] = import_react11.default.useState(saved);
2363
- const editing = import_react11.default.useRef(false);
2364
- const dirty = import_react11.default.useRef(false);
2365
- import_react11.default.useEffect(() => {
2657
+ const [draft, setDraft] = import_react12.default.useState(saved);
2658
+ const editing = import_react12.default.useRef(false);
2659
+ const dirty = import_react12.default.useRef(false);
2660
+ import_react12.default.useEffect(() => {
2366
2661
  if (!editing.current) setDraft(saved);
2367
2662
  }, [saved]);
2368
2663
  const inputProps = {
@@ -2395,46 +2690,73 @@ function DraftField({
2395
2690
  onBlur?.(event);
2396
2691
  }
2397
2692
  };
2398
- return /* @__PURE__ */ import_react11.default.createElement("label", null, label, multiline ? /* @__PURE__ */ import_react11.default.createElement("textarea", { ...inputProps }) : /* @__PURE__ */ import_react11.default.createElement("input", { ...inputProps }));
2693
+ return /* @__PURE__ */ import_react12.default.createElement("label", null, label, multiline ? /* @__PURE__ */ import_react12.default.createElement("textarea", { ...inputProps }) : /* @__PURE__ */ import_react12.default.createElement("input", { ...inputProps }));
2399
2694
  }
2400
2695
 
2401
2696
  // src/shared/design-system/forms/NumberField.tsx
2402
2697
  function NumberField(props) {
2403
- return /* @__PURE__ */ import_react12.default.createElement(DraftField, { ...props, type: "number" });
2698
+ return /* @__PURE__ */ import_react13.default.createElement(DraftField, { ...props, type: "number" });
2404
2699
  }
2405
2700
 
2406
2701
  // src/shared/design-system/forms/SelectField.tsx
2407
- var import_react13 = __toESM(require("react"), 1);
2408
- function SelectField({ label, options, ...props }) {
2409
- return /* @__PURE__ */ import_react13.default.createElement("label", null, label, /* @__PURE__ */ import_react13.default.createElement("select", { ...props }, options.map((option) => /* @__PURE__ */ import_react13.default.createElement("option", { key: option.value, value: option.value, disabled: option.disabled }, option.label))));
2702
+ var import_react14 = __toESM(require("react"), 1);
2703
+ function SelectField({ label, options, hint, ...props }) {
2704
+ const generatedId = import_react14.default.useId();
2705
+ const id2 = props.id || generatedId;
2706
+ return /* @__PURE__ */ import_react14.default.createElement("div", { className: "dlv-form-field" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "dlv-field-label" }, /* @__PURE__ */ import_react14.default.createElement("label", { htmlFor: id2 }, label), hint && /* @__PURE__ */ import_react14.default.createElement(Hint, { label, id: props["aria-describedby"] }, hint)), /* @__PURE__ */ import_react14.default.createElement("select", { ...props, id: id2 }, options.map(
2707
+ (option) => "options" in option ? /* @__PURE__ */ import_react14.default.createElement("optgroup", { key: option.label, label: option.label }, option.options.map((item) => /* @__PURE__ */ import_react14.default.createElement("option", { key: item.value, value: item.value, disabled: item.disabled }, item.label))) : /* @__PURE__ */ import_react14.default.createElement("option", { key: option.value, value: option.value, disabled: option.disabled }, option.label)
2708
+ )));
2410
2709
  }
2411
2710
 
2412
2711
  // src/shared/design-system/forms/TextAreaField.tsx
2413
- var import_react14 = __toESM(require("react"), 1);
2712
+ var import_react15 = __toESM(require("react"), 1);
2414
2713
  function TextAreaField(props) {
2415
- return /* @__PURE__ */ import_react14.default.createElement(DraftField, { ...props, multiline: true });
2714
+ return /* @__PURE__ */ import_react15.default.createElement(DraftField, { ...props, multiline: true });
2416
2715
  }
2417
2716
 
2418
2717
  // src/shared/design-system/forms/TextField.tsx
2419
- var import_react15 = __toESM(require("react"), 1);
2718
+ var import_react16 = __toESM(require("react"), 1);
2420
2719
  function TextField(props) {
2421
- return /* @__PURE__ */ import_react15.default.createElement(DraftField, { ...props, type: "text" });
2720
+ return /* @__PURE__ */ import_react16.default.createElement(DraftField, { ...props, type: "text" });
2422
2721
  }
2423
2722
 
2424
2723
  // src/shared/design-system/layout/SettingsCard.tsx
2425
- var import_react16 = __toESM(require("react"), 1);
2724
+ var import_react17 = __toESM(require("react"), 1);
2426
2725
  function SettingsCard({
2427
2726
  children,
2428
2727
  className = ""
2429
2728
  }) {
2430
- return /* @__PURE__ */ import_react16.default.createElement("div", { className: ["dlv-settings-card", className].filter(Boolean).join(" ") }, children);
2729
+ return /* @__PURE__ */ import_react17.default.createElement("div", { className: ["dlv-settings-card", className].filter(Boolean).join(" ") }, children);
2431
2730
  }
2432
2731
 
2433
2732
  // src/shared/design-system/layout/SettingsSection.tsx
2434
- var import_react17 = __toESM(require("react"), 1);
2733
+ var import_react19 = __toESM(require("react"), 1);
2435
2734
 
2436
- // src/shared/design-system/layout/SettingsSubcard.tsx
2735
+ // src/shared/design-system/layout/SettingsSectionBody.tsx
2437
2736
  var import_react18 = __toESM(require("react"), 1);
2737
+ function SettingsSectionBody({ children }) {
2738
+ return /* @__PURE__ */ import_react18.default.createElement("div", { className: "dlv-section-content" }, children);
2739
+ }
2740
+
2741
+ // src/shared/design-system/layout/SettingsSection.tsx
2742
+ function SettingsSection({
2743
+ title,
2744
+ children,
2745
+ icon,
2746
+ description,
2747
+ collapsible = true,
2748
+ defaultOpen = true,
2749
+ ariaLabel
2750
+ }) {
2751
+ const heading = /* @__PURE__ */ import_react19.default.createElement("h4", null, icon && /* @__PURE__ */ import_react19.default.createElement(Icon, { name: icon }), title);
2752
+ const body = /* @__PURE__ */ import_react19.default.createElement(SettingsSectionBody, null, description && /* @__PURE__ */ import_react19.default.createElement("p", null, description), children);
2753
+ if (!collapsible)
2754
+ return /* @__PURE__ */ import_react19.default.createElement("section", { className: "dlv-settings-section" }, /* @__PURE__ */ import_react19.default.createElement("header", { className: "dlv-section-heading" }, heading), body);
2755
+ return /* @__PURE__ */ import_react19.default.createElement("details", { className: "dlv-settings-section", open: defaultOpen }, /* @__PURE__ */ import_react19.default.createElement("summary", { className: "dlv-section-heading", "aria-label": ariaLabel }, heading, /* @__PURE__ */ import_react19.default.createElement(Icon, { name: "chevron", className: "dlv-section-chevron" })), body);
2756
+ }
2757
+
2758
+ // src/shared/design-system/layout/SettingsSubcard.tsx
2759
+ var import_react20 = __toESM(require("react"), 1);
2438
2760
  function SettingsSubcard({
2439
2761
  title,
2440
2762
  children,
@@ -2442,13 +2764,13 @@ function SettingsSubcard({
2442
2764
  ariaLabel,
2443
2765
  icon = "settings"
2444
2766
  }) {
2445
- return /* @__PURE__ */ import_react18.default.createElement("details", { className: "dlv-settings-subcard", open }, /* @__PURE__ */ import_react18.default.createElement("summary", { "aria-label": ariaLabel }, /* @__PURE__ */ import_react18.default.createElement(Icon, { name: icon, className: "dlv-settings-subcard-icon" }), /* @__PURE__ */ import_react18.default.createElement("span", null, title), /* @__PURE__ */ import_react18.default.createElement(Icon, { name: "chevron", className: "dlv-settings-subcard-chevron" })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "dlv-settings-subcard-body" }, children));
2767
+ return /* @__PURE__ */ import_react20.default.createElement("details", { className: "dlv-settings-subcard", open }, /* @__PURE__ */ import_react20.default.createElement("summary", { "aria-label": ariaLabel }, /* @__PURE__ */ import_react20.default.createElement(Icon, { name: icon, className: "dlv-settings-subcard-icon" }), /* @__PURE__ */ import_react20.default.createElement("span", null, title), /* @__PURE__ */ import_react20.default.createElement(Icon, { name: "chevron", className: "dlv-settings-subcard-chevron" })), /* @__PURE__ */ import_react20.default.createElement("div", { className: "dlv-settings-subcard-body" }, children));
2446
2768
  }
2447
2769
 
2448
2770
  // src/shared/design-system/layout/SettingsTabs.tsx
2449
- var import_react19 = __toESM(require("react"), 1);
2771
+ var import_react21 = __toESM(require("react"), 1);
2450
2772
  function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2451
- const refs = import_react19.default.useRef([]);
2773
+ const refs = import_react21.default.useRef([]);
2452
2774
  const activate = (index) => {
2453
2775
  const tab = tabs[index];
2454
2776
  if (!tab) return;
@@ -2465,7 +2787,7 @@ function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2465
2787
  event.preventDefault();
2466
2788
  activate(next);
2467
2789
  };
2468
- return /* @__PURE__ */ import_react19.default.createElement("div", { className: "dlv-settings-tabs", role: "tablist", "aria-label": label }, tabs.map((tab, index) => /* @__PURE__ */ import_react19.default.createElement(
2790
+ return /* @__PURE__ */ import_react21.default.createElement("div", { className: "dlv-settings-tabs", role: "tablist", "aria-label": label }, tabs.map((tab, index) => /* @__PURE__ */ import_react21.default.createElement(
2469
2791
  "button",
2470
2792
  {
2471
2793
  key: tab.id,
@@ -2483,23 +2805,23 @@ function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2483
2805
  onClick: () => onChange(tab.id),
2484
2806
  onKeyDown: (event) => onKeyDown(event, index)
2485
2807
  },
2486
- tab.icon ? /* @__PURE__ */ import_react19.default.createElement(Icon, { name: tab.icon, className: "dlv-settings-tab-icon" }) : null,
2487
- /* @__PURE__ */ import_react19.default.createElement("span", null, tab.label)
2808
+ tab.icon ? /* @__PURE__ */ import_react21.default.createElement(Icon, { name: tab.icon, className: "dlv-settings-tab-icon" }) : null,
2809
+ /* @__PURE__ */ import_react21.default.createElement("span", null, tab.label)
2488
2810
  )));
2489
2811
  }
2490
2812
 
2491
2813
  // src/modules/conversation/hooks/useConversationActions.ts
2492
- var import_react20 = __toESM(require("react"), 1);
2814
+ var import_react22 = __toESM(require("react"), 1);
2493
2815
  function useConversationActions(controller) {
2494
- const [error, setError] = import_react20.default.useState("");
2495
- const alive = import_react20.default.useRef(true);
2496
- import_react20.default.useEffect(
2816
+ const [error, setError] = import_react22.default.useState("");
2817
+ const alive = import_react22.default.useRef(true);
2818
+ import_react22.default.useEffect(
2497
2819
  () => () => {
2498
2820
  alive.current = false;
2499
2821
  },
2500
2822
  []
2501
2823
  );
2502
- const invoke = import_react20.default.useCallback(
2824
+ const invoke = import_react22.default.useCallback(
2503
2825
  (name, ...args) => {
2504
2826
  setError("");
2505
2827
  try {
@@ -2512,23 +2834,23 @@ function useConversationActions(controller) {
2512
2834
  },
2513
2835
  [controller]
2514
2836
  );
2515
- const clearError = import_react20.default.useCallback(() => setError(""), []);
2837
+ const clearError = import_react22.default.useCallback(() => setError(""), []);
2516
2838
  return { invoke, error, clearError };
2517
2839
  }
2518
2840
 
2519
2841
  // src/modules/conversation/hooks/useConversationController.ts
2520
- var import_react21 = __toESM(require("react"), 1);
2842
+ var import_react23 = __toESM(require("react"), 1);
2521
2843
  function useConversationController(controller) {
2522
- const subscribe = import_react21.default.useCallback(
2844
+ const subscribe = import_react23.default.useCallback(
2523
2845
  (listener) => controller.subscribe(listener),
2524
2846
  [controller]
2525
2847
  );
2526
- const read = import_react21.default.useCallback(() => controller.getSnapshot(), [controller]);
2527
- return import_react21.default.useSyncExternalStore(subscribe, read, read);
2848
+ const read = import_react23.default.useCallback(() => controller.getSnapshot(), [controller]);
2849
+ return import_react23.default.useSyncExternalStore(subscribe, read, read);
2528
2850
  }
2529
2851
 
2530
2852
  // src/modules/conversation/components/PlaybackControls.tsx
2531
- var import_react22 = __toESM(require("react"), 1);
2853
+ var import_react24 = __toESM(require("react"), 1);
2532
2854
  function PlaybackControls({
2533
2855
  state,
2534
2856
  invoke,
@@ -2536,21 +2858,21 @@ function PlaybackControls({
2536
2858
  }) {
2537
2859
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
2538
2860
  const capabilities = state.capabilities?.[state.speechEngine || state.settings?.engine] ?? {};
2539
- return /* @__PURE__ */ import_react22.default.createElement(import_react22.default.Fragment, null, navigation && state.speechHasPrevious ? /* @__PURE__ */ import_react22.default.createElement(
2861
+ return /* @__PURE__ */ import_react24.default.createElement(import_react24.default.Fragment, null, navigation && state.speechHasPrevious ? /* @__PURE__ */ import_react24.default.createElement(
2540
2862
  IconButton,
2541
2863
  {
2542
2864
  label: speak.playback.previous(),
2543
2865
  icon: "skipPrevious",
2544
2866
  onClick: () => invoke("previousSpeechSegment")
2545
2867
  }
2546
- ) : null, navigation && state.speechSegmentsRemaining > 1 ? /* @__PURE__ */ import_react22.default.createElement(
2868
+ ) : null, navigation && state.speechSegmentsRemaining > 1 ? /* @__PURE__ */ import_react24.default.createElement(
2547
2869
  IconButton,
2548
2870
  {
2549
2871
  label: speak.playback.next(),
2550
2872
  icon: "skipNext",
2551
2873
  onClick: () => invoke("skipSpeechSegment")
2552
2874
  }
2553
- ) : null, state.speechSegmentsRemaining > 0 && capabilities.pause && !state.paused ? /* @__PURE__ */ import_react22.default.createElement(
2875
+ ) : null, state.speechSegmentsRemaining > 0 && capabilities.pause && !state.paused ? /* @__PURE__ */ import_react24.default.createElement(
2554
2876
  IconButton,
2555
2877
  {
2556
2878
  label: speak.playback.pause(),
@@ -2558,14 +2880,14 @@ function PlaybackControls({
2558
2880
  disabled: !state.speaking || state.paused,
2559
2881
  onClick: () => invoke("pauseSpeech")
2560
2882
  }
2561
- ) : null, state.paused && capabilities.resume ? /* @__PURE__ */ import_react22.default.createElement(
2883
+ ) : null, state.paused && capabilities.resume ? /* @__PURE__ */ import_react24.default.createElement(
2562
2884
  IconButton,
2563
2885
  {
2564
2886
  label: speak.playback.resume(),
2565
2887
  icon: "play",
2566
2888
  onClick: () => invoke("resumeSpeech")
2567
2889
  }
2568
- ) : null, state.speechSegmentsRemaining > 0 || state.speechRunActive ? /* @__PURE__ */ import_react22.default.createElement(
2890
+ ) : null, state.speechSegmentsRemaining > 0 || state.speechRunActive ? /* @__PURE__ */ import_react24.default.createElement(
2569
2891
  IconButton,
2570
2892
  {
2571
2893
  label: speak.playback.stopAll(),
@@ -2576,7 +2898,7 @@ function PlaybackControls({
2576
2898
  }
2577
2899
 
2578
2900
  // src/modules/conversation/components/ScrollingSpeechCaption.tsx
2579
- var import_react23 = __toESM(require("react"), 1);
2901
+ var import_react25 = __toESM(require("react"), 1);
2580
2902
  function ScrollingSpeechCaption({
2581
2903
  text,
2582
2904
  label,
@@ -2585,12 +2907,12 @@ function ScrollingSpeechCaption({
2585
2907
  paused,
2586
2908
  loading
2587
2909
  }) {
2588
- const viewport = import_react23.default.useRef(null);
2589
- const line = import_react23.default.useRef(null);
2590
- const marker = import_react23.default.useRef(null);
2591
- const current = import_react23.default.useRef({ paused, loading });
2910
+ const viewport = import_react25.default.useRef(null);
2911
+ const line = import_react25.default.useRef(null);
2912
+ const marker = import_react25.default.useRef(null);
2913
+ const current = import_react25.default.useRef({ paused, loading });
2592
2914
  current.current = { paused, loading };
2593
- import_react23.default.useEffect(() => {
2915
+ import_react25.default.useEffect(() => {
2594
2916
  const box = viewport.current;
2595
2917
  const content = line.current;
2596
2918
  const band = marker.current;
@@ -2643,7 +2965,7 @@ function ScrollingSpeechCaption({
2643
2965
  observer?.disconnect();
2644
2966
  };
2645
2967
  }, [controller, text, segment]);
2646
- return /* @__PURE__ */ import_react23.default.createElement("div", { className: "dlv-caption-stage" }, /* @__PURE__ */ import_react23.default.createElement(
2968
+ return /* @__PURE__ */ import_react25.default.createElement("div", { className: "dlv-caption-stage" }, /* @__PURE__ */ import_react25.default.createElement(
2647
2969
  "div",
2648
2970
  {
2649
2971
  ref: viewport,
@@ -2651,8 +2973,8 @@ function ScrollingSpeechCaption({
2651
2973
  title: text,
2652
2974
  "aria-label": label
2653
2975
  },
2654
- /* @__PURE__ */ import_react23.default.createElement("span", { ref: line, className: "dlv-caption-line" }, text)
2655
- ), /* @__PURE__ */ import_react23.default.createElement("span", { ref: marker, className: "dlv-caption-marker", "aria-hidden": "true" }));
2976
+ /* @__PURE__ */ import_react25.default.createElement("span", { ref: line, className: "dlv-caption-line" }, text)
2977
+ ), /* @__PURE__ */ import_react25.default.createElement("span", { ref: marker, className: "dlv-caption-marker", "aria-hidden": "true" }));
2656
2978
  }
2657
2979
 
2658
2980
  // src/modules/conversation/components/SpeechStatusBar.tsx
@@ -2661,8 +2983,8 @@ function SpeechStatusBar({ controller }) {
2661
2983
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
2662
2984
  const state = useConversationController(controller);
2663
2985
  const { invoke, error, clearError } = useConversationActions(controller);
2664
- const [progress, setProgress] = import_react24.default.useState(null);
2665
- import_react24.default.useEffect(() => {
2986
+ const [progress, setProgress] = import_react26.default.useState(null);
2987
+ import_react26.default.useEffect(() => {
2666
2988
  setProgress(null);
2667
2989
  if (!state.speechText || !state.speaking) return;
2668
2990
  const update = () => setProgress(controller.getSpeechProgress?.() ?? null);
@@ -2678,14 +3000,14 @@ function SpeechStatusBar({ controller }) {
2678
3000
  const loading = state.speechLoading === true && state.speaking && !state.paused;
2679
3001
  const index = state.speechSegmentIndex ?? 0;
2680
3002
  const total = state.speechSegmentsTotal ?? state.speechSegmentsRemaining ?? 0;
2681
- return /* @__PURE__ */ import_react24.default.createElement("div", { className: "dlv-bar-wrap dlv-speech-bar" }, /* @__PURE__ */ import_react24.default.createElement(
3003
+ return /* @__PURE__ */ import_react26.default.createElement("div", { className: "dlv-bar-wrap dlv-speech-bar" }, /* @__PURE__ */ import_react26.default.createElement(
2682
3004
  "div",
2683
3005
  {
2684
3006
  className: "dlv-pill dlv-speech-pill",
2685
3007
  role: "group",
2686
3008
  "aria-label": speak.captions.title()
2687
3009
  },
2688
- /* @__PURE__ */ import_react24.default.createElement(
3010
+ /* @__PURE__ */ import_react26.default.createElement(
2689
3011
  IconButton,
2690
3012
  {
2691
3013
  label: speak.playback.previous(),
@@ -2694,7 +3016,7 @@ function SpeechStatusBar({ controller }) {
2694
3016
  onClick: () => invoke("previousSpeechSegment")
2695
3017
  }
2696
3018
  ),
2697
- /* @__PURE__ */ import_react24.default.createElement("div", { className: "dlv-caption-stack" }, /* @__PURE__ */ import_react24.default.createElement(
3019
+ /* @__PURE__ */ import_react26.default.createElement("div", { className: "dlv-caption-stack" }, /* @__PURE__ */ import_react26.default.createElement(
2698
3020
  ScrollingSpeechCaption,
2699
3021
  {
2700
3022
  text,
@@ -2704,7 +3026,7 @@ function SpeechStatusBar({ controller }) {
2704
3026
  paused: state.paused || !state.speaking,
2705
3027
  loading
2706
3028
  }
2707
- ), /* @__PURE__ */ import_react24.default.createElement(
3029
+ ), /* @__PURE__ */ import_react26.default.createElement(
2708
3030
  "div",
2709
3031
  {
2710
3032
  className: "dlv-caption-progress",
@@ -2716,9 +3038,9 @@ function SpeechStatusBar({ controller }) {
2716
3038
  "aria-valuemax": 100,
2717
3039
  "aria-valuenow": loading || ratio === null ? void 0 : Math.round(ratio * 100)
2718
3040
  },
2719
- /* @__PURE__ */ import_react24.default.createElement("span", { style: loading ? void 0 : { width: (ratio ?? 0) * 100 + "%" } })
3041
+ /* @__PURE__ */ import_react26.default.createElement("span", { style: loading ? void 0 : { width: (ratio ?? 0) * 100 + "%" } })
2720
3042
  )),
2721
- /* @__PURE__ */ import_react24.default.createElement(
3043
+ /* @__PURE__ */ import_react26.default.createElement(
2722
3044
  IconButton,
2723
3045
  {
2724
3046
  label: speak.playback.next(),
@@ -2727,7 +3049,7 @@ function SpeechStatusBar({ controller }) {
2727
3049
  onClick: () => invoke("skipSpeechSegment")
2728
3050
  }
2729
3051
  ),
2730
- /* @__PURE__ */ import_react24.default.createElement(
3052
+ /* @__PURE__ */ import_react26.default.createElement(
2731
3053
  "span",
2732
3054
  {
2733
3055
  className: "dlv-speech-count",
@@ -2737,8 +3059,8 @@ function SpeechStatusBar({ controller }) {
2737
3059
  "/",
2738
3060
  total
2739
3061
  ),
2740
- /* @__PURE__ */ import_react24.default.createElement(PlaybackControls, { state, invoke, navigation: false })
2741
- ), /* @__PURE__ */ import_react24.default.createElement(
3062
+ /* @__PURE__ */ import_react26.default.createElement(PlaybackControls, { state, invoke, navigation: false })
3063
+ ), /* @__PURE__ */ import_react26.default.createElement(
2742
3064
  ErrorMessage,
2743
3065
  {
2744
3066
  error,
@@ -2749,67 +3071,6 @@ function SpeechStatusBar({ controller }) {
2749
3071
  ));
2750
3072
  }
2751
3073
 
2752
- // src/modules/conversation/models/meetingTranscript.ts
2753
- var MeetingTranscript = class {
2754
- constructor(composer) {
2755
- this.composer = composer;
2756
- }
2757
- composer;
2758
- active = /* @__PURE__ */ new Set();
2759
- last = null;
2760
- expected = null;
2761
- timestamps = false;
2762
- sourceTimestamps = /* @__PURE__ */ new Map();
2763
- setSourceTimestamps(source, enabled) {
2764
- if (this.sourceTimestamps.get(source) === enabled) return;
2765
- this.sourceTimestamps.set(source, enabled);
2766
- this.last = null;
2767
- }
2768
- setTimestamps(enabled) {
2769
- this.timestamps = enabled;
2770
- this.last = null;
2771
- }
2772
- setActive(source, enabled) {
2773
- if (enabled) this.active.add(source);
2774
- else this.active.delete(source);
2775
- this.last = null;
2776
- }
2777
- append(source, text, startedAt) {
2778
- const chunk = text.trim();
2779
- if (!chunk || !this.active.has(source)) return;
2780
- const draft = this.composer.getDraft();
2781
- if (draft !== this.expected) this.last = null;
2782
- const labelled = this.active.size > 1;
2783
- const timestamps = this.sourceTimestamps.get(source) ?? this.timestamps;
2784
- const changed = (labelled || timestamps) && this.last !== source;
2785
- const date = new Date(Number.isFinite(startedAt) ? startedAt : Date.now());
2786
- const pad = (value) => String(value).padStart(2, "0");
2787
- const stamp = timestamps && changed ? "[" + date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()) + "] " : "";
2788
- const prefix = changed ? stamp + (labelled ? source === "microphone" ? "Me: " : "Them: " : "") : "";
2789
- const separator = draft ? changed ? "\n\n" : "\n" : "";
2790
- const next = draft + separator + prefix + chunk;
2791
- this.composer.setDraft(next);
2792
- this.expected = next;
2793
- this.last = labelled || timestamps ? source : null;
2794
- }
2795
- reset() {
2796
- this.active.clear();
2797
- this.last = null;
2798
- this.expected = null;
2799
- }
2800
- };
2801
-
2802
- // src/modules/core/transcript.ts
2803
- var TranscriptDraft = class {
2804
- reset() {
2805
- }
2806
- update(current, hypothesis, final = false) {
2807
- if (!final || !hypothesis) return current;
2808
- const separator = current && !/\s$/.test(current) ? " " : "";
2809
- return current + separator + hypothesis;
2810
- }
2811
- };
2812
-
2813
3074
  // src/modules/core/speechDefaults.ts
2814
3075
  var notices = {
2815
3076
  en: "You can see the code in our conversation history.",
@@ -2893,6 +3154,11 @@ var defaultSettings = Object.freeze({
2893
3154
  recognitionFilterEnabled: true,
2894
3155
  recognitionMinimumWords: 2,
2895
3156
  voiceCommandsEnabled: true,
3157
+ sharedVoiceCommandsEnabled: true,
3158
+ timestampsEnabled: false,
3159
+ sharedTimestampsEnabled: false,
3160
+ microphoneSourceName: "Me",
3161
+ sharedSourceName: "Them",
2896
3162
  voiceCommandSend: "send, send message",
2897
3163
  voiceCommandQueue: "queue, queue message",
2898
3164
  voiceCommandEnd: "end, end conversation",
@@ -2904,8 +3170,15 @@ var defaultSettings = Object.freeze({
2904
3170
  outputCodeMaxLines: 5,
2905
3171
  outputCodeNotice: resolveCodeNotice(void 0, "pt-BR"),
2906
3172
  rate: 1,
3173
+ volume: 1,
2907
3174
  segmentGapMs: 400
2908
3175
  });
3176
+ var normalizeSourceName = (value, fallback) => {
3177
+ if (typeof value !== "string" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value))
3178
+ return fallback;
3179
+ const name = value.trim();
3180
+ return name && name.length <= 80 ? name : fallback;
3181
+ };
2909
3182
  var normalizeCommandPhrases = (value, fallback) => typeof value === "string" && value.length <= 1e3 && !value.includes("\0") ? value.split(/[,\r\n]+/).map((phrase) => phrase.trim()).filter(Boolean).slice(0, 20).join(", ") : fallback;
2910
3183
  var customSilenceMinMs = 100;
2911
3184
  var customSilenceMaxMs = 1e4;
@@ -2941,6 +3214,17 @@ function normalizeSettings(value) {
2941
3214
  recognitionFilterEnabled: typeof source.recognitionFilterEnabled === "boolean" ? source.recognitionFilterEnabled : defaultSettings.recognitionFilterEnabled,
2942
3215
  recognitionMinimumWords: Number.isInteger(source.recognitionMinimumWords) && source.recognitionMinimumWords >= 1 && source.recognitionMinimumWords <= 20 ? source.recognitionMinimumWords : defaultSettings.recognitionMinimumWords,
2943
3216
  voiceCommandsEnabled: typeof source.voiceCommandsEnabled === "boolean" ? source.voiceCommandsEnabled : defaultSettings.voiceCommandsEnabled,
3217
+ microphoneSourceName: normalizeSourceName(
3218
+ source.microphoneSourceName,
3219
+ defaultSettings.microphoneSourceName
3220
+ ),
3221
+ sharedSourceName: normalizeSourceName(
3222
+ source.sharedSourceName,
3223
+ defaultSettings.sharedSourceName
3224
+ ),
3225
+ timestampsEnabled: typeof source.timestampsEnabled === "boolean" ? source.timestampsEnabled : false,
3226
+ sharedTimestampsEnabled: typeof source.sharedTimestampsEnabled === "boolean" ? source.sharedTimestampsEnabled : false,
3227
+ sharedVoiceCommandsEnabled: typeof source.sharedVoiceCommandsEnabled === "boolean" ? source.sharedVoiceCommandsEnabled : typeof source.voiceCommandsEnabled === "boolean" ? source.voiceCommandsEnabled : defaultSettings.sharedVoiceCommandsEnabled,
2944
3228
  voiceCommandSend: normalizeCommandPhrases(
2945
3229
  source.voiceCommandSend,
2946
3230
  defaultSettings.voiceCommandSend
@@ -2973,10 +3257,79 @@ function normalizeSettings(value) {
2973
3257
  outputCodeMaxLines: Number.isInteger(source.outputCodeMaxLines) && source.outputCodeMaxLines >= 0 && source.outputCodeMaxLines <= 100 ? source.outputCodeMaxLines : defaultSettings.outputCodeMaxLines,
2974
3258
  outputCodeNotice: typeof source.outputCodeNotice === "string" && source.outputCodeNotice.trim() && source.outputCodeNotice.length <= 300 && !source.outputCodeNotice.includes("\0") ? resolveCodeNotice(source.outputCodeNotice, source.lang || defaultSettings.lang) : resolveCodeNotice(defaultSettings.outputCodeNotice, source.lang || defaultSettings.lang),
2975
3259
  rate: Number.isFinite(source.rate) && source.rate >= 0.1 && source.rate <= 3 ? source.rate : 1,
3260
+ volume: Number.isFinite(source.volume) && source.volume >= 0 && source.volume <= 1 ? source.volume : 1,
2976
3261
  segmentGapMs: Number.isFinite(source.segmentGapMs) && source.segmentGapMs >= 0 && source.segmentGapMs <= 2e3 ? Math.round(source.segmentGapMs) : 400
2977
3262
  };
2978
3263
  }
2979
3264
 
3265
+ // src/modules/conversation/models/meetingTranscript.ts
3266
+ var MeetingTranscript = class {
3267
+ constructor(composer, settings = () => defaultSettings) {
3268
+ this.composer = composer;
3269
+ this.settings = settings;
3270
+ }
3271
+ composer;
3272
+ settings;
3273
+ active = /* @__PURE__ */ new Set();
3274
+ last = null;
3275
+ expected = null;
3276
+ timestamps = false;
3277
+ sourceTimestamps = /* @__PURE__ */ new Map();
3278
+ setSourceTimestamps(source, enabled) {
3279
+ if (this.sourceTimestamps.get(source) === enabled) return;
3280
+ this.sourceTimestamps.set(source, enabled);
3281
+ this.last = null;
3282
+ }
3283
+ setTimestamps(enabled) {
3284
+ this.timestamps = enabled;
3285
+ this.last = null;
3286
+ }
3287
+ setActive(source, enabled) {
3288
+ if (enabled) this.active.add(source);
3289
+ else this.active.delete(source);
3290
+ this.last = null;
3291
+ }
3292
+ append(source, text, startedAt) {
3293
+ const chunk = text.trim();
3294
+ if (!chunk || !this.active.has(source)) return;
3295
+ const draft = this.composer.getDraft();
3296
+ if (draft !== this.expected) this.last = null;
3297
+ const labelled = this.active.size > 1;
3298
+ const timestamps = this.sourceTimestamps.get(source) ?? this.timestamps;
3299
+ const changed = (labelled || timestamps) && this.last !== source;
3300
+ const date = new Date(Number.isFinite(startedAt) ? startedAt : Date.now());
3301
+ const pad = (value) => String(value).padStart(2, "0");
3302
+ const stamp = timestamps && changed ? "[" + date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()) + "] " : "";
3303
+ const key = source === "microphone" ? "microphoneSourceName" : "sharedSourceName";
3304
+ const name = normalizeSourceName(this.settings()[key], defaultSettings[key]);
3305
+ const prefix = changed ? stamp + (labelled ? name + ": " : "") : "";
3306
+ const separator = draft ? changed ? "\n\n" : "\n" : "";
3307
+ const next = draft + separator + prefix + chunk;
3308
+ this.composer.setDraft(next);
3309
+ this.expected = next;
3310
+ this.last = labelled || timestamps ? source : null;
3311
+ }
3312
+ reset() {
3313
+ this.active.clear();
3314
+ this.last = null;
3315
+ this.expected = null;
3316
+ }
3317
+ };
3318
+
3319
+ // src/modules/core/transcript.ts
3320
+ var TranscriptDraft = class {
3321
+ reset() {
3322
+ }
3323
+ update(current, hypothesis, final = false) {
3324
+ if (!final || !hypothesis) return current;
3325
+ const separator = current && !/\s$/.test(current) ? " " : "";
3326
+ return current + separator + hypothesis;
3327
+ }
3328
+ };
3329
+
3330
+ // src/modules/speak/services/speechQueue.ts
3331
+ var SPEECH_QUEUE_PREFETCH_LIMIT = 3;
3332
+
2980
3333
  // node_modules/.pnpm/marked@18.1.0/node_modules/marked/lib/marked.esm.js
2981
3334
  function I() {
2982
3335
  return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
@@ -4523,7 +4876,8 @@ var VoiceCoordinator = class _VoiceCoordinator {
4523
4876
  speechHasPrevious: false,
4524
4877
  speechRunActive: false,
4525
4878
  capabilities: {},
4526
- settings: normalizeSettings(settings)
4879
+ settings: normalizeSettings(settings),
4880
+ timestamps: normalizeSettings(settings).timestampsEnabled
4527
4881
  };
4528
4882
  this.autoSendTimer = null;
4529
4883
  this.autoSendDraft = null;
@@ -4582,6 +4936,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
4582
4936
  (key) => Object.hasOwn(next, key) && settings[key] !== previousSettings[key]
4583
4937
  );
4584
4938
  if (speechPreparationChanged) this._cancelSpeechPrefetch();
4939
+ if (settings.volume !== previousSettings.volume) {
4940
+ this.engines[this.snapshot.speechEngine || settings.engine]?.setVolume?.(settings.volume);
4941
+ }
4585
4942
  this.meter.deviceId = settings.inputDeviceId;
4586
4943
  Object.assign(this.recognition, {
4587
4944
  lang: settings.recognitionLang,
@@ -4593,16 +4950,14 @@ var VoiceCoordinator = class _VoiceCoordinator {
4593
4950
  });
4594
4951
  this.patch({
4595
4952
  settings,
4953
+ timestamps: settings.timestampsEnabled,
4596
4954
  muted: settings.microphoneEnabled === false,
4597
4955
  recognizing: this.snapshot.recognizing,
4598
4956
  error: null
4599
4957
  });
4600
4958
  if (settings.sendingMode === "manual") this.cancelAutoSend();
4601
- if (Object.hasOwn(next, "announceAssistantMessages") && !settings.announceAssistantMessages) {
4602
- this._cancelSpeechPrefetch();
4603
- this.queue = [];
4604
- this.assistantSpeechNotBefore = 0;
4605
- this._cancelAssistantSpeechTimer();
4959
+ if (previousSettings.announceAssistantMessages && !settings.announceAssistantMessages) {
4960
+ void this.stopSpeech();
4606
4961
  }
4607
4962
  if (Object.hasOwn(next, "assistantSpeechDelaySeconds") && this.assistantSpeechNotBefore > 0) {
4608
4963
  this.assistantSpeechNotBefore = Date.now() + settings.assistantSpeechDelaySeconds * 1e3;
@@ -4610,7 +4965,35 @@ var VoiceCoordinator = class _VoiceCoordinator {
4610
4965
  this._drain();
4611
4966
  }
4612
4967
  }
4613
- async refreshCapabilities() {
4968
+ async refreshCapabilities({ localOnly = false } = {}) {
4969
+ if (localOnly) {
4970
+ const playback = Object.fromEntries(
4971
+ Object.entries(this.engines).map(([id2, engine]) => [
4972
+ id2,
4973
+ {
4974
+ ...this.snapshot.capabilities[id2],
4975
+ ...engine.getPlaybackCapabilities?.()
4976
+ }
4977
+ ])
4978
+ );
4979
+ const browser = this.engines.browser?.voices?.();
4980
+ const capture = await this.meter.capability?.();
4981
+ if (this.disposed) return;
4982
+ this.patch({
4983
+ capabilities: {
4984
+ ...this.snapshot.capabilities,
4985
+ ...playback,
4986
+ ...browser ? {
4987
+ browser: {
4988
+ ...playback.browser,
4989
+ voices: browser.map(({ name, voiceURI, lang: lang2 }) => ({ name, voiceURI, lang: lang2 }))
4990
+ }
4991
+ } : {},
4992
+ ...capture ? { capture } : {}
4993
+ }
4994
+ });
4995
+ return;
4996
+ }
4614
4997
  const request = this.capabilityRequest = (this.capabilityRequest || 0) + 1;
4615
4998
  const caps = {};
4616
4999
  const lang = this.snapshot.settings.recognitionLang;
@@ -4696,7 +5079,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
4696
5079
  this._drain();
4697
5080
  }
4698
5081
  toggleTimestamps() {
4699
- this.patch({ timestamps: !this.snapshot.timestamps });
5082
+ return this.updateSettings({ timestampsEnabled: !this.snapshot.timestamps });
4700
5083
  }
4701
5084
  muteListening() {
4702
5085
  this.cancelAutoSend();
@@ -4948,8 +5331,8 @@ var VoiceCoordinator = class _VoiceCoordinator {
4948
5331
  else if (command === "mute") this.muteListening();
4949
5332
  else if (command === "resume") this.resumeListeningInput();
4950
5333
  else if (command === "stopSpeaking") {
5334
+ if (!this.snapshot.settings.announceAssistantMessages) void this.stopSpeech();
4951
5335
  this.updateSettings({ announceAssistantMessages: false });
4952
- void this.stopSpeech();
4953
5336
  } else if (command === "clear") {
4954
5337
  this.cancelAutoSend();
4955
5338
  this.transcript.reset();
@@ -5101,16 +5484,18 @@ var VoiceCoordinator = class _VoiceCoordinator {
5101
5484
  item.preparationKey = key;
5102
5485
  }
5103
5486
  _prefetchSpeech() {
5487
+ if (this.speechSuspended && !this.queue[0]?.manual) return;
5104
5488
  const engine = this.engines[this.snapshot.settings.engine];
5105
5489
  if (typeof engine?.prepare !== "function") return;
5106
5490
  const options = {
5107
5491
  voice: this.snapshot.settings.voice || void 0,
5108
5492
  rate: this.snapshot.settings.rate,
5493
+ volume: this.snapshot.settings.volume,
5109
5494
  lang: this.snapshot.settings.lang,
5110
5495
  outputDeviceId: this.snapshot.settings.outputDeviceId
5111
5496
  };
5112
5497
  const key = this._speechPreparationKey();
5113
- for (const item of this.queue.slice(0, 3)) {
5498
+ for (const item of this.queue.slice(0, SPEECH_QUEUE_PREFETCH_LIMIT)) {
5114
5499
  this._resetStaleSpeechItem(item, key);
5115
5500
  if (item.prepared || item.preparing) continue;
5116
5501
  this.prefetchItems.add(item);
@@ -5163,6 +5548,48 @@ var VoiceCoordinator = class _VoiceCoordinator {
5163
5548
  speechSegmentIndex
5164
5549
  });
5165
5550
  }
5551
+ /** Copy only logical automatic-speech state before lifecycle teardown destroys resources. */
5552
+ checkpointSpeech() {
5553
+ const active = this.snapshot.speaking && this.activeSpeechItem && !this.activeSpeechItem.manual ? [this.activeSpeechItem] : [];
5554
+ return {
5555
+ consumed: [...this.consumed],
5556
+ unfinished: [...this.unfinished],
5557
+ suppressed: [...this.suppressed],
5558
+ pending: [...active, ...this.queue.filter((item) => !item.manual)].map(({ text, id: id2 }) => ({
5559
+ text,
5560
+ id: id2
5561
+ })),
5562
+ paused: this.snapshot.paused && !this.interruptionPausedSpeech
5563
+ };
5564
+ }
5565
+ restoreSpeech(checkpoint) {
5566
+ if (!checkpoint || this.disposed) return;
5567
+ this.consumed = new Map(checkpoint.consumed);
5568
+ this.unfinished = new Set(checkpoint.unfinished);
5569
+ this.suppressed = new Set(checkpoint.suppressed);
5570
+ this.queue = checkpoint.pending.map(({ text, id: id2 }) => ({ text, id: id2 }));
5571
+ this.patch({ paused: checkpoint.paused && this.queue.length > 0 });
5572
+ this._syncSpeechSegments();
5573
+ }
5574
+ async suspendSpeech() {
5575
+ this.speechSuspended = true;
5576
+ await Promise.all([
5577
+ _VoiceCoordinator.prototype.stopSpeech.call(this, false),
5578
+ this.stopListening()
5579
+ ]);
5580
+ }
5581
+ discardSpeechMessage(id2) {
5582
+ this.suppressed.add(id2);
5583
+ this.queue = this.queue.filter((item) => item.id !== id2);
5584
+ if (this.snapshot.activeMessageId === id2) void this._navigateSpeech();
5585
+ this._syncSpeechSegments();
5586
+ }
5587
+ activateSpeech() {
5588
+ if (this.disposed) return;
5589
+ this.speechSuspended = false;
5590
+ this._prefetchSpeech();
5591
+ this._drain();
5592
+ }
5166
5593
  _suppressPending() {
5167
5594
  for (const id2 of this.unfinished) this.suppressed.add(id2);
5168
5595
  if (this.snapshot.activeMessageId !== null) this.suppressed.add(this.snapshot.activeMessageId);
@@ -5170,6 +5597,8 @@ var VoiceCoordinator = class _VoiceCoordinator {
5170
5597
  this.queue = [];
5171
5598
  }
5172
5599
  async stopSpeech(resumeListening = true) {
5600
+ this._cancelAssistantSpeechTimer();
5601
+ this.assistantSpeechNotBefore = 0;
5173
5602
  this._cancelInterruptionResume();
5174
5603
  if (this.interruptionTimer !== null) clearTimeout(this.interruptionTimer);
5175
5604
  this.interruptionTimer = null;
@@ -5178,6 +5607,8 @@ var VoiceCoordinator = class _VoiceCoordinator {
5178
5607
  this.interruptionPausedSpeech = false;
5179
5608
  const epoch = ++this.speechEpoch;
5180
5609
  ++this.controlEpoch;
5610
+ this.speechController?.abort();
5611
+ this.speechLaunchPending = false;
5181
5612
  this._cancelSpeechPrefetch();
5182
5613
  this._suppressPending();
5183
5614
  this._releaseSpeechHistory();
@@ -5230,6 +5661,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
5230
5661
  const engine = this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine];
5231
5662
  const epoch = ++this.speechEpoch;
5232
5663
  ++this.controlEpoch;
5664
+ this.speechController?.abort();
5233
5665
  this._cancelAssistantSpeechTimer();
5234
5666
  this.assistantSpeechNotBefore = 0;
5235
5667
  this.patch({ speaking: false, paused: false, speechLoading: false });
@@ -5262,6 +5694,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
5262
5694
  return;
5263
5695
  }
5264
5696
  if (!this.snapshot.speaking) return;
5697
+ if (this.speechLaunchPending) {
5698
+ ++this.controlEpoch;
5699
+ this.patch({ paused: true });
5700
+ return;
5701
+ }
5265
5702
  const epoch = this.speechEpoch;
5266
5703
  const control = ++this.controlEpoch;
5267
5704
  try {
@@ -5283,6 +5720,20 @@ var VoiceCoordinator = class _VoiceCoordinator {
5283
5720
  this._drain();
5284
5721
  return;
5285
5722
  }
5723
+ if (this.speechLaunchPending) {
5724
+ const epoch2 = this.speechEpoch;
5725
+ const control2 = ++this.controlEpoch;
5726
+ if (this.snapshot.settings.mode === "speaker") {
5727
+ await this.stopListening();
5728
+ if (this.inputReleaseError) {
5729
+ this.patch({ error: message(this.inputReleaseError) });
5730
+ return;
5731
+ }
5732
+ }
5733
+ if (epoch2 === this.speechEpoch && control2 === this.controlEpoch)
5734
+ this.patch({ paused: false });
5735
+ return;
5736
+ }
5286
5737
  const epoch = this.speechEpoch;
5287
5738
  const control = ++this.controlEpoch;
5288
5739
  try {
@@ -5334,6 +5785,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
5334
5785
  if (!this.speechHistory.includes(item)) this.speechHistory.push(item);
5335
5786
  this.speechCursor = this.speechHistory.indexOf(item);
5336
5787
  const epoch = ++this.speechEpoch;
5788
+ const controller = new AbortController();
5789
+ this.speechController = controller;
5790
+ this.speechLaunchPending = true;
5337
5791
  const engine = this.engines[this.snapshot.settings.engine];
5338
5792
  this._resetStaleSpeechItem(item, this._speechPreparationKey());
5339
5793
  item.engine = engine;
@@ -5372,7 +5826,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
5372
5826
  },
5373
5827
  voice: this.snapshot.settings.voice || void 0,
5374
5828
  rate: this.snapshot.settings.rate,
5375
- outputDeviceId: this.snapshot.settings.outputDeviceId
5829
+ volume: this.snapshot.settings.volume,
5830
+ outputDeviceId: this.snapshot.settings.outputDeviceId,
5831
+ signal: controller.signal
5376
5832
  };
5377
5833
  if (!queued.prepared && typeof engine.prepare === "function") {
5378
5834
  const prepared = await engine.prepare(text, options);
@@ -5382,6 +5838,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
5382
5838
  }
5383
5839
  queued.prepared = prepared;
5384
5840
  }
5841
+ while (this.snapshot.paused && epoch === this.speechEpoch && !this.disposed)
5842
+ await this._waitSpeechLaunch(controller.signal);
5843
+ if (epoch !== this.speechEpoch || this.disposed) return;
5844
+ this.speechLaunchPending = false;
5845
+ options.volume = this.snapshot.settings.volume;
5385
5846
  if (queued?.prepared && typeof engine.playPrepared === "function") {
5386
5847
  const prepared = queued.prepared;
5387
5848
  this.prefetchItems.delete(queued);
@@ -5414,12 +5875,27 @@ var VoiceCoordinator = class _VoiceCoordinator {
5414
5875
  }
5415
5876
  }
5416
5877
  }
5417
- _cancelAssistantSpeechTimer() {
5418
- if (this.assistantSpeechTimer !== null) clearTimeout(this.assistantSpeechTimer);
5419
- this.assistantSpeechTimer = null;
5420
- }
5421
- _drain() {
5422
- if (this.disposed || !this.snapshot.settings.announceAssistantMessages && !this.queue[0]?.manual || this.snapshot.speaking || this.snapshot.paused || this._speechActivity() || this.snapshot.starting || !this.queue[0]?.manual && (this.snapshot.pendingTranscriptions > 0 || this.snapshot.autoSendAt !== null) || !this.queue.length)
5878
+ _waitSpeechLaunch(signal) {
5879
+ if (!this.snapshot.paused) return Promise.resolve();
5880
+ return new Promise((resolve) => {
5881
+ const done = () => {
5882
+ unsubscribe();
5883
+ signal.removeEventListener("abort", done);
5884
+ resolve();
5885
+ };
5886
+ const unsubscribe = this.subscribe(() => {
5887
+ if (!this.snapshot.paused || signal.aborted) done();
5888
+ });
5889
+ signal.addEventListener("abort", done, { once: true });
5890
+ if (signal.aborted) done();
5891
+ });
5892
+ }
5893
+ _cancelAssistantSpeechTimer() {
5894
+ if (this.assistantSpeechTimer !== null) clearTimeout(this.assistantSpeechTimer);
5895
+ this.assistantSpeechTimer = null;
5896
+ }
5897
+ _drain() {
5898
+ if (this.disposed || this.speechSuspended && !this.queue[0]?.manual || !this.snapshot.settings.announceAssistantMessages && !this.queue[0]?.manual || this.snapshot.speaking || this.snapshot.paused || this._speechActivity() || this.snapshot.starting || !this.queue[0]?.manual && (this.snapshot.pendingTranscriptions > 0 || this.snapshot.autoSendAt !== null) || !this.queue.length)
5423
5899
  return;
5424
5900
  const wait = this.assistantSpeechNotBefore - Date.now();
5425
5901
  if (wait > 0) {
@@ -5450,7 +5926,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
5450
5926
  this.patch({ speechText: null, speechEngine: null });
5451
5927
  this._syncSpeechSegments();
5452
5928
  }
5453
- if (baseline || !this.snapshot.conversation || !this.snapshot.settings.announceAssistantMessages || this.suppressed.has(id2)) {
5929
+ if (baseline || !this.snapshot.settings.announceAssistantMessages || this.suppressed.has(id2)) {
5454
5930
  this.consumed.set(id2, text.length);
5455
5931
  return;
5456
5932
  }
@@ -5492,17 +5968,27 @@ var VoiceCoordinator = class _VoiceCoordinator {
5492
5968
 
5493
5969
  // src/modules/conversation/models/meeting.ts
5494
5970
  var MeetingController = class {
5495
- constructor({ composer, settings, createSource, translate, onActivity, onSpeech, stopSpeech }) {
5971
+ constructor({
5972
+ composer,
5973
+ settings,
5974
+ createSource,
5975
+ translate,
5976
+ onActivity,
5977
+ onSpeech,
5978
+ stopSpeech,
5979
+ savePreferences
5980
+ }) {
5496
5981
  this.composer = composer;
5497
5982
  this.stopSpeech = stopSpeech;
5498
5983
  this.inputs = /* @__PURE__ */ new Map();
5499
5984
  this.inputOverrides = /* @__PURE__ */ new Map();
5500
5985
  this.settings = settings;
5986
+ this.savePreferences = savePreferences;
5501
5987
  this.createSource = createSource;
5502
5988
  this.t = translate;
5503
5989
  this.onActivity = onActivity;
5504
5990
  this.onSpeech = onSpeech;
5505
- this.transcript = new MeetingTranscript(composer);
5991
+ this.transcript = new MeetingTranscript(composer, settings);
5506
5992
  this.jobs = /* @__PURE__ */ new Map();
5507
5993
  this.generations = /* @__PURE__ */ new Map();
5508
5994
  this.listeners = /* @__PURE__ */ new Set();
@@ -5541,7 +6027,14 @@ var MeetingController = class {
5541
6027
  engines: {},
5542
6028
  meter: { stop: async () => {
5543
6029
  } },
5544
- settings: { ...this.settings(), microphoneEnabled: true },
6030
+ settings: {
6031
+ ...this.settings(),
6032
+ microphoneEnabled: true,
6033
+ timestampsEnabled: source === "shared" ? this.settings().sharedTimestampsEnabled : this.settings().timestampsEnabled,
6034
+ ...source === "shared" ? {
6035
+ voiceCommandsEnabled: this.settings().sharedVoiceCommandsEnabled ?? this.settings().voiceCommandsEnabled
6036
+ } : {}
6037
+ },
5545
6038
  composer: {
5546
6039
  getDraft: () => this.composer.getDraft(),
5547
6040
  setDraft: (text) => this.composer.setDraft(text),
@@ -5562,6 +6055,14 @@ var MeetingController = class {
5562
6055
  input.updateSettings = (next) => {
5563
6056
  this.inputOverrides.set(source, { ...this.inputOverrides.get(source), ...next });
5564
6057
  update(next);
6058
+ const patch = {};
6059
+ if (source === "shared" && Object.hasOwn(next, "voiceCommandsEnabled"))
6060
+ patch.sharedVoiceCommandsEnabled = next.voiceCommandsEnabled;
6061
+ if (Object.hasOwn(next, "timestampsEnabled")) {
6062
+ this.transcript.setSourceTimestamps(source, next.timestampsEnabled);
6063
+ patch[source === "shared" ? "sharedTimestampsEnabled" : "timestampsEnabled"] = next.timestampsEnabled;
6064
+ }
6065
+ if (Object.keys(patch).length) return this.savePreferences?.(input, patch);
5565
6066
  };
5566
6067
  input.clearError = () => this.patch(source, { error: null });
5567
6068
  input.endConversation = () => this.stop(source);
@@ -5569,24 +6070,27 @@ var MeetingController = class {
5569
6070
  const updatePolicy = input.updateSettings;
5570
6071
  input.updateSettings = (next) => {
5571
6072
  if (Object.hasOwn(next, "announceAssistantMessages"))
5572
- this.composer.updatePlaybackSettings?.({ announceAssistantMessages: next.announceAssistantMessages });
6073
+ this.composer.updatePlaybackSettings?.({
6074
+ announceAssistantMessages: next.announceAssistantMessages
6075
+ });
5573
6076
  return updatePolicy(next);
5574
6077
  };
5575
- input.toggleTimestamps = () => {
5576
- const timestamps = !input.getSnapshot().timestamps;
5577
- this.transcript.setSourceTimestamps(source, timestamps);
5578
- input.patch({ timestamps });
5579
- };
6078
+ this.transcript.setSourceTimestamps(source, input.getSnapshot().timestamps);
5580
6079
  this.inputs.set(source, input);
5581
6080
  return input;
5582
6081
  }
5583
6082
  applySettings(settings) {
5584
- for (const [source, input] of this.inputs)
6083
+ for (const [source, input] of this.inputs) {
6084
+ const timestamps = source === "shared" ? settings.sharedTimestampsEnabled : settings.timestampsEnabled;
6085
+ this.transcript.setSourceTimestamps(source, Boolean(timestamps));
5585
6086
  VoiceCoordinator.prototype.updateSettings.call(input, {
5586
6087
  ...settings,
5587
6088
  ...this.inputOverrides.get(source),
6089
+ ...source === "shared" && typeof settings.sharedVoiceCommandsEnabled === "boolean" ? { voiceCommandsEnabled: settings.sharedVoiceCommandsEnabled } : {},
6090
+ timestampsEnabled: timestamps ?? input.getSnapshot().timestamps,
5588
6091
  microphoneEnabled: !input.getSnapshot().muted
5589
6092
  });
6093
+ }
5590
6094
  }
5591
6095
  composerChanged(draft) {
5592
6096
  for (const input of this.inputs.values()) input.composerChanged(draft);
@@ -5625,7 +6129,6 @@ var MeetingController = class {
5625
6129
  this.patch(source, { starting: true, error: null });
5626
6130
  try {
5627
6131
  const settings = this.settings();
5628
- if (settings.recognitionEngine === "browser") throw new Error(this.t("httpRequired"));
5629
6132
  const { meter, engine } = this.createSource(source, settings);
5630
6133
  const input = this.input(source);
5631
6134
  input.meter = meter;
@@ -5635,9 +6138,15 @@ var MeetingController = class {
5635
6138
  Object.assign(job, { meter, engine });
5636
6139
  if (source === "shared") meter.provide(request);
5637
6140
  meter.deviceId = settings.inputDeviceId;
5638
- const capability = await engine.capability();
6141
+ const capability = await engine.capability({
6142
+ lang: settings.recognitionLang,
6143
+ signal: abort.signal
6144
+ });
5639
6145
  if (!valid()) return;
5640
- if (!capability.supported) throw new Error(capability.reason || this.t("unavailable"));
6146
+ if (!capability.supported)
6147
+ throw Object.assign(new Error(capability.reason || this.t("unavailable")), {
6148
+ code: capability.code
6149
+ });
5641
6150
  if (!await meter.start({ signal: abort.signal })) {
5642
6151
  if (valid()) await this.stop(source);
5643
6152
  return;
@@ -5693,7 +6202,7 @@ var MeetingController = class {
5693
6202
  } catch (error) {
5694
6203
  if (valid()) {
5695
6204
  this.patch(source, {
5696
- error: source === "shared" && error.message === "No shared audio track was returned." ? this.t("noAudio") : error.message === this.t("httpRequired") ? error.message : this.t("failed")
6205
+ error: source === "shared" && error.message === "No shared audio track was returned." ? this.t("noAudio") : error.code === "external-audio-unsupported" ? this.t("browserSharedUnsupported") : error.code === "browser-local-unavailable" ? this.t("browserLocalUnavailable") : this.t("failed")
5697
6206
  });
5698
6207
  await this.stop(source);
5699
6208
  }
@@ -5713,7 +6222,13 @@ var MeetingController = class {
5713
6222
  this.jobs.delete(source);
5714
6223
  const input = this.inputs.get(source);
5715
6224
  input?.cancelAutoSend();
5716
- input?.patch({ conversation: false, starting: false, listening: false, recognizing: false, pendingTranscriptions: 0 });
6225
+ input?.patch({
6226
+ conversation: false,
6227
+ starting: false,
6228
+ listening: false,
6229
+ recognizing: false,
6230
+ pendingTranscriptions: 0
6231
+ });
5717
6232
  this.onActivity?.(source, false);
5718
6233
  job?.abort.abort();
5719
6234
  this.transcript.setActive(source, false);
@@ -5979,13 +6494,13 @@ var SharedAudioMeter = class extends MicrophoneMeter {
5979
6494
  };
5980
6495
 
5981
6496
  // src/modules/conversation/components/MeetingControls.tsx
5982
- var import_react29 = __toESM(require("react"), 1);
6497
+ var import_react31 = __toESM(require("react"), 1);
5983
6498
 
5984
6499
  // src/modules/conversation/components/ConversationStatusBar.tsx
5985
- var import_react28 = __toESM(require("react"), 1);
6500
+ var import_react30 = __toESM(require("react"), 1);
5986
6501
 
5987
6502
  // src/modules/conversation/components/DeliveryModeButton.tsx
5988
- var import_react25 = __toESM(require("react"), 1);
6503
+ var import_react27 = __toESM(require("react"), 1);
5989
6504
  function nextDeliveryMode(mode) {
5990
6505
  return mode === "queue" ? "steer" : mode === "steer" ? "manual" : "queue";
5991
6506
  }
@@ -5998,7 +6513,7 @@ function DeliveryModeButton({
5998
6513
  const description = mode === "steer" ? settings.delivery.steerDescription() : mode === "queue" ? commons.queue() : commons.manual();
5999
6514
  const status = settings.delivery.status({ mode: description });
6000
6515
  const visible = mode === "steer" ? commons.send() : mode === "queue" ? commons.delivery.queueBadge() : commons.toggle.offBadge();
6001
- return /* @__PURE__ */ import_react25.default.createElement(
6516
+ return /* @__PURE__ */ import_react27.default.createElement(
6002
6517
  ToggleButton,
6003
6518
  {
6004
6519
  className: "dlv-live-toggle",
@@ -6014,15 +6529,15 @@ function DeliveryModeButton({
6014
6529
  }
6015
6530
 
6016
6531
  // src/modules/conversation/components/RecognitionBar.tsx
6017
- var import_react27 = __toESM(require("react"), 1);
6532
+ var import_react29 = __toESM(require("react"), 1);
6018
6533
 
6019
6534
  // src/modules/conversation/components/Waveform.tsx
6020
- var import_react26 = __toESM(require("react"), 1);
6535
+ var import_react28 = __toESM(require("react"), 1);
6021
6536
  function Waveform({ controller, enabled }) {
6022
- const ref = import_react26.default.useRef(null);
6023
- const current = import_react26.default.useRef({ controller, enabled });
6537
+ const ref = import_react28.default.useRef(null);
6538
+ const current = import_react28.default.useRef({ controller, enabled });
6024
6539
  current.current = { controller, enabled };
6025
- import_react26.default.useEffect(() => {
6540
+ import_react28.default.useEffect(() => {
6026
6541
  const canvas = ref.current;
6027
6542
  const context = canvas?.getContext("2d");
6028
6543
  if (!canvas || !context) return;
@@ -6077,12 +6592,12 @@ function Waveform({ controller, enabled }) {
6077
6592
  window.removeEventListener("resize", resize);
6078
6593
  };
6079
6594
  }, []);
6080
- return /* @__PURE__ */ import_react26.default.createElement("canvas", { ref, className: "dlv-wave", "aria-hidden": true });
6595
+ return /* @__PURE__ */ import_react28.default.createElement("canvas", { ref, className: "dlv-wave", "aria-hidden": true });
6081
6596
  }
6082
6597
 
6083
6598
  // src/modules/conversation/components/RecognitionBar.tsx
6084
6599
  function RecognitionBar({ controller, listening, label, status, leading, children }) {
6085
- return /* @__PURE__ */ import_react27.default.createElement("div", { className: "dlv-pill", role: "group", "aria-label": label }, leading, /* @__PURE__ */ import_react27.default.createElement(Waveform, { controller, enabled: Boolean(listening) }), /* @__PURE__ */ import_react27.default.createElement("span", { className: "dlv-status", role: "status", "aria-live": "polite" }, status), children);
6600
+ return /* @__PURE__ */ import_react29.default.createElement("div", { className: "dlv-pill", role: "group", "aria-label": label }, leading, /* @__PURE__ */ import_react29.default.createElement(Waveform, { controller, enabled: Boolean(listening) }), /* @__PURE__ */ import_react29.default.createElement("span", { className: "dlv-status", role: "status", "aria-live": "polite" }, status), children);
6086
6601
  }
6087
6602
 
6088
6603
  // src/modules/conversation/components/conversationStatus.ts
@@ -6115,8 +6630,8 @@ function ConversationStatusBar({
6115
6630
  const { scoped: meeting } = useLanguage((ctx) => ctx.meeting);
6116
6631
  const state = useConversationController(controller);
6117
6632
  const { invoke, error, clearError } = useConversationActions(controller);
6118
- const [now, setNow] = import_react28.default.useState(Date.now());
6119
- import_react28.default.useEffect(() => {
6633
+ const [now, setNow] = import_react30.default.useState(Date.now());
6634
+ import_react30.default.useEffect(() => {
6120
6635
  if (!state.autoSendAt) return;
6121
6636
  setNow(Date.now());
6122
6637
  const timer = setInterval(() => setNow(Date.now()), 200);
@@ -6133,28 +6648,28 @@ function ConversationStatusBar({
6133
6648
  settings,
6134
6649
  speak
6135
6650
  });
6136
- return /* @__PURE__ */ import_react28.default.createElement(
6651
+ return /* @__PURE__ */ import_react30.default.createElement(
6137
6652
  "div",
6138
6653
  {
6139
6654
  className: overlay ? "dlv-bar-wrap dlv-bar-stack dlv-question-overlay" : "dlv-bar-wrap dlv-bar-stack",
6140
6655
  style: overlay ? overlayStyle : void 0
6141
6656
  },
6142
- includeSpeech && /* @__PURE__ */ import_react28.default.createElement(SpeechStatusBar, { controller }),
6143
- (state.conversation || capture || state.error || error) && /* @__PURE__ */ import_react28.default.createElement(
6657
+ includeSpeech && /* @__PURE__ */ import_react30.default.createElement(SpeechStatusBar, { controller }),
6658
+ (state.conversation || capture || state.error || error) && /* @__PURE__ */ import_react30.default.createElement(
6144
6659
  RecognitionBar,
6145
6660
  {
6146
6661
  controller,
6147
6662
  listening: state.listening,
6148
6663
  label: sourceLabel || commons.controls.title(),
6149
6664
  status,
6150
- leading: /* @__PURE__ */ import_react28.default.createElement(import_react28.default.Fragment, null, state.conversation && !capture ? /* @__PURE__ */ import_react28.default.createElement(
6665
+ leading: /* @__PURE__ */ import_react30.default.createElement(import_react30.default.Fragment, null, state.conversation && !capture ? /* @__PURE__ */ import_react30.default.createElement(
6151
6666
  IconButton,
6152
6667
  {
6153
6668
  label: recognition.microphone.takeControl(),
6154
6669
  icon: sourceIcon,
6155
6670
  onClick: () => invoke("takeMicrophone")
6156
6671
  }
6157
- ) : null, capture ? /* @__PURE__ */ import_react28.default.createElement(
6672
+ ) : null, capture ? /* @__PURE__ */ import_react30.default.createElement(
6158
6673
  ToggleButton,
6159
6674
  {
6160
6675
  className: "dlv-mic-state",
@@ -6170,7 +6685,7 @@ function ConversationStatusBar({
6170
6685
  }
6171
6686
  ) : null)
6172
6687
  },
6173
- /* @__PURE__ */ import_react28.default.createElement(
6688
+ /* @__PURE__ */ import_react30.default.createElement(
6174
6689
  ToggleButton,
6175
6690
  {
6176
6691
  icon: "filter",
@@ -6180,7 +6695,7 @@ function ConversationStatusBar({
6180
6695
  onClick: () => invoke("updateSettings", { voiceCommandsEnabled: !state.settings.voiceCommandsEnabled })
6181
6696
  }
6182
6697
  ),
6183
- /* @__PURE__ */ import_react28.default.createElement(
6698
+ /* @__PURE__ */ import_react30.default.createElement(
6184
6699
  ToggleButton,
6185
6700
  {
6186
6701
  className: "dlv-timestamp-toggle",
@@ -6191,14 +6706,14 @@ function ConversationStatusBar({
6191
6706
  onClick: () => invoke("toggleTimestamps")
6192
6707
  }
6193
6708
  ),
6194
- /* @__PURE__ */ import_react28.default.createElement(
6709
+ /* @__PURE__ */ import_react30.default.createElement(
6195
6710
  DeliveryModeButton,
6196
6711
  {
6197
6712
  mode: state.settings.sendingMode || "manual",
6198
6713
  onChange: (sendingMode) => invoke("updateSettings", { sendingMode })
6199
6714
  }
6200
6715
  ),
6201
- remaining ? /* @__PURE__ */ import_react28.default.createElement(
6716
+ remaining ? /* @__PURE__ */ import_react30.default.createElement(
6202
6717
  IconButton,
6203
6718
  {
6204
6719
  label: settings.autoSend.cancel(),
@@ -6207,7 +6722,7 @@ function ConversationStatusBar({
6207
6722
  }
6208
6723
  ) : null
6209
6724
  ),
6210
- /* @__PURE__ */ import_react28.default.createElement(
6725
+ /* @__PURE__ */ import_react30.default.createElement(
6211
6726
  ErrorMessage,
6212
6727
  {
6213
6728
  error: error || state.error,
@@ -6225,9 +6740,9 @@ function ConversationStatusBar({
6225
6740
  // src/modules/conversation/components/MeetingControls.tsx
6226
6741
  function MeetingToggle({ meeting, onToggle }) {
6227
6742
  const { scoped: copy } = useLanguage((ctx) => ctx.meeting);
6228
- const state = import_react29.default.useSyncExternalStore(meeting.subscribe, meeting.getSnapshot);
6743
+ const state = import_react31.default.useSyncExternalStore(meeting.subscribe, meeting.getSnapshot);
6229
6744
  const active = Boolean(state.shared.listening || state.shared.starting);
6230
- return /* @__PURE__ */ import_react29.default.createElement(
6745
+ return /* @__PURE__ */ import_react31.default.createElement(
6231
6746
  IconButton,
6232
6747
  {
6233
6748
  icon: active ? "meeting" : "meetingOff",
@@ -6242,12 +6757,12 @@ function MeetingToggle({ meeting, onToggle }) {
6242
6757
  }
6243
6758
  function MeetingBars({ meeting }) {
6244
6759
  const { scoped: copy } = useLanguage((ctx) => ctx.meeting);
6245
- const state = import_react29.default.useSyncExternalStore(meeting.subscribe, meeting.getSnapshot);
6760
+ const state = import_react31.default.useSyncExternalStore(meeting.subscribe, meeting.getSnapshot);
6246
6761
  const value = state.shared;
6247
6762
  if (!value.listening && !value.starting && !value.error) return null;
6248
6763
  const label = copy.shared();
6249
6764
  const input = meeting.input("shared");
6250
- return /* @__PURE__ */ import_react29.default.createElement("div", { className: "dlv-meeting-bars" }, /* @__PURE__ */ import_react29.default.createElement(
6765
+ return /* @__PURE__ */ import_react31.default.createElement("div", { className: "dlv-meeting-bars" }, /* @__PURE__ */ import_react31.default.createElement(
6251
6766
  ConversationStatusBar,
6252
6767
  {
6253
6768
  controller: input,
@@ -6366,9 +6881,72 @@ var VoiceOwnership = class {
6366
6881
  };
6367
6882
 
6368
6883
  // src/modules/recognition/components/RecognitionCapabilityStatus.tsx
6369
- var import_react30 = __toESM(require("react"), 1);
6884
+ var import_react32 = __toESM(require("react"), 1);
6885
+
6886
+ // src/modules/recognition/engines/browser/externalAudioSupport.ts
6887
+ async function detectExternalAudioSupport(globals = globalThis, { signal } = {}) {
6888
+ let frame, stream, recognition, timer, onAbort;
6889
+ const clearTimer = globals.clearTimeout ?? globalThis.clearTimeout;
6890
+ try {
6891
+ if (signal?.aborted) return false;
6892
+ const document2 = globals.document;
6893
+ if (!document2?.body) return false;
6894
+ frame = document2.createElement("iframe");
6895
+ frame.hidden = true;
6896
+ frame.allow = "microphone 'none'";
6897
+ frame.srcdoc = "<!doctype html><html></html>";
6898
+ const loaded = await new Promise((resolve) => {
6899
+ frame.onload = () => resolve(true);
6900
+ frame.onerror = () => resolve(false);
6901
+ onAbort = () => resolve(false);
6902
+ signal?.addEventListener("abort", onAbort, { once: true });
6903
+ timer = (globals.setTimeout ?? globalThis.setTimeout)(() => resolve(false), 3e3);
6904
+ document2.body.append(frame);
6905
+ });
6906
+ if (!loaded || signal?.aborted || !frame.isConnected) return false;
6907
+ const doc = frame.contentDocument;
6908
+ const window2 = frame.contentWindow;
6909
+ const policy = doc?.permissionsPolicy ?? doc?.featurePolicy;
6910
+ if (!policy?.features?.().includes("microphone") || policy.allowsFeature("microphone") !== false)
6911
+ return false;
6912
+ const Recognition = typeof globals.SpeechRecognition === "function" ? window2?.SpeechRecognition : window2?.webkitSpeechRecognition;
6913
+ if (typeof Recognition !== "function") return false;
6914
+ stream = doc.createElement("canvas").captureStream();
6915
+ const track = stream.getVideoTracks()[0];
6916
+ if (!track || track.kind !== "video") return false;
6917
+ track.stop();
6918
+ if (track.readyState !== "ended") return false;
6919
+ recognition = new Recognition();
6920
+ if (typeof recognition.start !== "function" || typeof recognition.abort !== "function")
6921
+ return false;
6922
+ try {
6923
+ recognition.start(track);
6924
+ return false;
6925
+ } catch (error) {
6926
+ return error?.name === "InvalidStateError" && frame.isConnected && !signal?.aborted;
6927
+ } finally {
6928
+ recognition.abort();
6929
+ }
6930
+ } catch {
6931
+ return false;
6932
+ } finally {
6933
+ if (timer !== void 0) clearTimer(timer);
6934
+ signal?.removeEventListener("abort", onAbort);
6935
+ if (frame) {
6936
+ frame.onload = null;
6937
+ frame.onerror = null;
6938
+ }
6939
+ for (const track of stream?.getTracks() ?? []) track.stop();
6940
+ frame?.remove();
6941
+ }
6942
+ }
6370
6943
 
6371
6944
  // src/modules/recognition/engines/browser/BrowserRecognitionEngine.ts
6945
+ var externalAudioReason = "This browser cannot safely recognize shared audio. Choose Qwen or Whisper HTTP for shared audio.";
6946
+ var invalidAudioTrack = () => Object.assign(new Error("Shared recognition requires a live audio track."), {
6947
+ name: "InvalidStateError",
6948
+ code: "invalid-audio-track"
6949
+ });
6372
6950
  var abortError = () => Object.assign(new Error("Speech recognition was cancelled."), { name: "AbortError" });
6373
6951
  var localReason = "Local browser speech recognition is unavailable for this language. Install the local language pack, or explicitly turn off local-only processing in Settings if you permit the browser recognition service to process audio.";
6374
6952
  var notify = (callback, value) => {
@@ -6380,6 +6958,8 @@ var notify = (callback, value) => {
6380
6958
  var BrowserRecognitionEngine = class {
6381
6959
  constructor({
6382
6960
  globals = globalThis,
6961
+ meter,
6962
+ inputSource = "microphone",
6383
6963
  lang = "pt-BR",
6384
6964
  processLocally = true,
6385
6965
  autoInstallLocalPack = true,
@@ -6395,6 +6975,8 @@ var BrowserRecognitionEngine = class {
6395
6975
  throw new RangeError("restartDelayMs must be nonnegative.");
6396
6976
  Object.assign(this, {
6397
6977
  globals,
6978
+ meter,
6979
+ inputSource,
6398
6980
  lang,
6399
6981
  processLocally,
6400
6982
  autoInstallLocalPack,
@@ -6417,7 +6999,27 @@ var BrowserRecognitionEngine = class {
6417
6999
  return void 0;
6418
7000
  }
6419
7001
  }
6420
- async capability({ lang = this.lang, processLocally = this.processLocally } = {}) {
7002
+ async capability({ lang = this.lang, processLocally = this.processLocally, signal } = {}) {
7003
+ if (this.inputSource !== "shared") return this._processingCapability({ lang, processLocally });
7004
+ const Recognition = this.Recognition;
7005
+ const externalAudio = Recognition && this.externalAudioRecognition === Recognition || await detectExternalAudioSupport(this.globals, { signal });
7006
+ if (!externalAudio)
7007
+ return {
7008
+ supported: false,
7009
+ local: processLocally,
7010
+ externalAudio: false,
7011
+ code: "external-audio-unsupported",
7012
+ reason: externalAudioReason
7013
+ };
7014
+ this.externalAudioRecognition = Recognition;
7015
+ const capability = await this._processingCapability({ lang, processLocally });
7016
+ return {
7017
+ ...capability,
7018
+ externalAudio: true,
7019
+ ...!capability.supported && processLocally ? { code: "browser-local-unavailable" } : {}
7020
+ };
7021
+ }
7022
+ async _processingCapability({ lang, processLocally }) {
6421
7023
  try {
6422
7024
  const Recognition = this.Recognition;
6423
7025
  if (typeof Recognition !== "function") throw new Error();
@@ -6482,6 +7084,8 @@ var BrowserRecognitionEngine = class {
6482
7084
  const session = {
6483
7085
  lang,
6484
7086
  signal,
7087
+ probeAbort: new AbortController(),
7088
+ audioTrack: null,
6485
7089
  onResult,
6486
7090
  onActivity,
6487
7091
  onError,
@@ -6502,7 +7106,11 @@ var BrowserRecognitionEngine = class {
6502
7106
  };
6503
7107
  signal?.addEventListener("abort", session.cancel, { once: true });
6504
7108
  const capability = await Promise.race([
6505
- this.capability({ lang, processLocally: this.processLocally }),
7109
+ this.capability({
7110
+ lang,
7111
+ processLocally: this.processLocally,
7112
+ signal: session.probeAbort.signal
7113
+ }),
6506
7114
  cancelled3
6507
7115
  ]);
6508
7116
  if (this.session !== session || signal?.aborted) {
@@ -6511,9 +7119,11 @@ var BrowserRecognitionEngine = class {
6511
7119
  }
6512
7120
  if (!capability.supported) {
6513
7121
  this.stop();
6514
- throw new Error(capability.reason);
7122
+ throw Object.assign(new Error(capability.reason), { code: capability.code });
6515
7123
  }
6516
7124
  try {
7125
+ if (this.inputSource === "shared")
7126
+ session.audioTrack = this.meter?.stream?.getAudioTracks?.()[0];
6517
7127
  this._begin(session);
6518
7128
  if (this.session !== session) throw abortError();
6519
7129
  } catch (error) {
@@ -6538,6 +7148,8 @@ var BrowserRecognitionEngine = class {
6538
7148
  }
6539
7149
  _begin(session) {
6540
7150
  if (this.session !== session) return;
7151
+ if (this.inputSource === "shared" && (session.audioTrack?.kind !== "audio" || session.audioTrack.readyState !== "live"))
7152
+ throw invalidAudioTrack();
6541
7153
  const recognition = new this.Recognition();
6542
7154
  if (this.processLocally) {
6543
7155
  if (!("processLocally" in recognition)) throw new Error(localReason);
@@ -6628,7 +7240,8 @@ var BrowserRecognitionEngine = class {
6628
7240
  Math.min(3e4, this.restartDelayMs * 2 ** Math.min(session.restarts - 1, 20))
6629
7241
  );
6630
7242
  };
6631
- recognition.start();
7243
+ if (this.inputSource === "shared") recognition.start(session.audioTrack);
7244
+ else recognition.start();
6632
7245
  }
6633
7246
  finish() {
6634
7247
  const session = this.session;
@@ -6682,6 +7295,7 @@ var BrowserRecognitionEngine = class {
6682
7295
  if (!session) return;
6683
7296
  this.session = null;
6684
7297
  session.rejectCancelled(abortError());
7298
+ session.probeAbort.abort();
6685
7299
  session.signal?.removeEventListener("abort", session.cancel);
6686
7300
  if (session.timer !== null)
6687
7301
  (this.globals.clearTimeout ?? globalThis.clearTimeout)(session.timer);
@@ -7084,6 +7698,9 @@ var HostAudioSpeakingEngine = class {
7084
7698
  this.playbackRate = playbackRate || ((rate) => rate);
7085
7699
  this.current = null;
7086
7700
  }
7701
+ getPlaybackCapabilities() {
7702
+ return { pause: true, resume: true };
7703
+ }
7087
7704
  async capability() {
7088
7705
  try {
7089
7706
  const url = this.capabilityEndpoint || this.endpoint.replace(/\/speech$/, "/capabilities");
@@ -7158,18 +7775,26 @@ var HostAudioSpeakingEngine = class {
7158
7775
  }
7159
7776
  }
7160
7777
  async playPrepared(prepared, options = {}) {
7161
- const { signal, outputDeviceId = "", rate = 1, retainPrepared = false } = options;
7778
+ const { signal, outputDeviceId = "", rate = 1, volume = 1, retainPrepared = false } = options;
7162
7779
  const playbackRate = this.playbackRate(rate);
7163
7780
  if (!prepared?.audio) throw new TypeError("Prepared speech audio is required.");
7164
7781
  await this.stop();
7165
7782
  if (signal?.aborted) throw cancelled2();
7166
- const operation = { prepared, audio: prepared.audio };
7783
+ const operation = { prepared, audio: prepared.audio, paused: false, ready: false };
7167
7784
  this.current = operation;
7168
7785
  const audio = operation.audio;
7169
7786
  if (retainPrepared) audio.currentTime = 0;
7170
7787
  audio.playbackRate = playbackRate;
7171
- if (outputDeviceId && typeof audio.setSinkId === "function")
7172
- await audio.setSinkId(outputDeviceId);
7788
+ audio.volume = volume;
7789
+ try {
7790
+ if (outputDeviceId && typeof audio.setSinkId === "function")
7791
+ await audio.setSinkId(outputDeviceId);
7792
+ if (signal?.aborted || this.current !== operation) throw cancelled2();
7793
+ } catch (error) {
7794
+ if (this.current === operation) this.current = null;
7795
+ if (!retainPrepared) prepared.dispose();
7796
+ throw error;
7797
+ }
7173
7798
  return new Promise((resolve, reject) => {
7174
7799
  const cleanup = () => {
7175
7800
  audio.removeEventListener("playing", started);
@@ -7200,7 +7825,8 @@ var HostAudioSpeakingEngine = class {
7200
7825
  audio.addEventListener("ended", done, { once: true });
7201
7826
  audio.addEventListener("error", failed, { once: true });
7202
7827
  signal?.addEventListener("abort", aborted, { once: true });
7203
- Promise.resolve(audio.play()).catch(failed);
7828
+ operation.ready = true;
7829
+ if (!operation.paused) Promise.resolve(audio.play()).catch(failed);
7204
7830
  });
7205
7831
  }
7206
7832
  async speak(text, options = {}) {
@@ -7212,6 +7838,9 @@ var HostAudioSpeakingEngine = class {
7212
7838
  throw error;
7213
7839
  }
7214
7840
  }
7841
+ setVolume(volume) {
7842
+ if (this.current?.audio) this.current.audio.volume = volume;
7843
+ }
7215
7844
  async stop() {
7216
7845
  const operation = this.current;
7217
7846
  if (!operation) return;
@@ -7229,13 +7858,15 @@ var HostAudioSpeakingEngine = class {
7229
7858
  return { positionSeconds: audio.currentTime, durationSeconds: audio.duration };
7230
7859
  }
7231
7860
  pause() {
7232
- if (!this.current?.audio || this.current.audio.paused) return false;
7861
+ if (!this.current?.audio || this.current.paused) return false;
7862
+ this.current.paused = true;
7233
7863
  this.current.audio.pause();
7234
7864
  return true;
7235
7865
  }
7236
7866
  async resume() {
7237
- if (!this.current?.audio || !this.current.audio.paused) return false;
7238
- await this.current.audio.play();
7867
+ if (!this.current?.audio || !this.current.paused && !this.current.audio.paused) return false;
7868
+ this.current.paused = false;
7869
+ if (this.current.ready) await this.current.audio.play();
7239
7870
  return true;
7240
7871
  }
7241
7872
  };
@@ -7257,6 +7888,12 @@ var BrowserSpeakingEngine = class {
7257
7888
  return [];
7258
7889
  }
7259
7890
  }
7891
+ getPlaybackCapabilities() {
7892
+ return {
7893
+ pause: typeof this.globals.speechSynthesis?.pause === "function",
7894
+ resume: typeof this.globals.speechSynthesis?.resume === "function"
7895
+ };
7896
+ }
7260
7897
  capability() {
7261
7898
  const supported = typeof this.globals.SpeechSynthesisUtterance === "function" && typeof this.globals.speechSynthesis?.speak === "function" && typeof this.globals.speechSynthesis?.cancel === "function" && this.voices().length > 0;
7262
7899
  return {
@@ -7267,15 +7904,17 @@ var BrowserSpeakingEngine = class {
7267
7904
  voiceURI: voice.voiceURI,
7268
7905
  lang: voice.lang
7269
7906
  })),
7270
- pause: typeof this.globals.speechSynthesis?.pause === "function",
7271
- resume: typeof this.globals.speechSynthesis?.resume === "function",
7907
+ ...this.getPlaybackCapabilities(),
7272
7908
  ...supported ? {} : {
7273
7909
  reason: "Local browser speech synthesis is unavailable. Enable an installed local system voice, refresh the voice list, or choose another local speaking engine. Remote voices are not allowed."
7274
7910
  }
7275
7911
  };
7276
7912
  }
7277
- async speak(text, { voice, rate = 1, signal, onPlaybackStart } = {}) {
7278
- this.stop();
7913
+ /** Inaudible preparation only: Web Speech does not expose pre-synthesized audio. */
7914
+ async prepare(text, options = {}) {
7915
+ return this._prepare(text, options);
7916
+ }
7917
+ _prepare(text, { voice, lang = this.lang, rate = 1, volume = 1, signal } = {}) {
7279
7918
  if (signal?.aborted) throw abortError3();
7280
7919
  if (typeof text !== "string") throw new TypeError("Speech text must be a string.");
7281
7920
  if (!Number.isFinite(rate) || rate < 0.1 || rate > 10)
@@ -7283,18 +7922,61 @@ var BrowserSpeakingEngine = class {
7283
7922
  const capability = this.capability();
7284
7923
  if (!capability.supported) throw new Error(capability.reason);
7285
7924
  const voices = this.voices();
7286
- const chosen = voice == null ? voices.find((v) => v.lang?.toLowerCase() === this.lang.toLowerCase()) ?? voices[0] : voices.find(
7925
+ const chosen = voice == null ? voices.find((v) => v.lang?.toLowerCase() === lang.toLowerCase()) ?? voices[0] : voices.find(
7287
7926
  (v) => typeof voice === "string" ? v.voiceURI === voice || v.name === voice : v === voice
7288
7927
  );
7289
7928
  if (!chosen)
7290
7929
  throw new Error(
7291
7930
  "The selected voice is not an available local browser voice. Choose a voice from voices()."
7292
7931
  );
7293
- if (!text.trim()) return;
7294
- const utterance = new this.globals.SpeechSynthesisUtterance(text);
7295
- utterance.voice = chosen;
7296
- utterance.lang = chosen.lang || this.lang;
7297
- utterance.rate = rate;
7932
+ let disposed = false;
7933
+ const createUtterance = () => {
7934
+ if (disposed) throw abortError3();
7935
+ const available = this.voices().find((value) => value.voiceURI === chosen.voiceURI);
7936
+ if (!available) throw new Error("The prepared local browser voice is no longer available.");
7937
+ const utterance2 = new this.globals.SpeechSynthesisUtterance(text);
7938
+ utterance2.voice = available;
7939
+ utterance2.lang = available.lang || lang;
7940
+ utterance2.rate = rate;
7941
+ utterance2.volume = volume;
7942
+ return utterance2;
7943
+ };
7944
+ let utterance = createUtterance();
7945
+ return {
7946
+ text,
7947
+ rate,
7948
+ takeUtterance: () => {
7949
+ if (disposed) throw abortError3();
7950
+ if (!this.voices().some((value) => value.voiceURI === chosen.voiceURI))
7951
+ throw new Error("The prepared local browser voice is no longer available.");
7952
+ const next = utterance || createUtterance();
7953
+ utterance = null;
7954
+ return next;
7955
+ },
7956
+ dispose() {
7957
+ disposed = true;
7958
+ utterance = null;
7959
+ }
7960
+ };
7961
+ }
7962
+ async speak(text, options = {}) {
7963
+ this.stop();
7964
+ return this.playPrepared(this._prepare(text, options), options);
7965
+ }
7966
+ async playPrepared(prepared, { signal, volume, onPlaybackStart, retainPrepared = false } = {}) {
7967
+ this.stop();
7968
+ if (signal?.aborted) {
7969
+ if (!retainPrepared) prepared?.dispose?.();
7970
+ throw abortError3();
7971
+ }
7972
+ if (!prepared || typeof prepared.takeUtterance !== "function")
7973
+ throw new TypeError("Prepared browser speech is required.");
7974
+ if (!prepared.text.trim()) {
7975
+ if (!retainPrepared) prepared.dispose();
7976
+ return;
7977
+ }
7978
+ const utterance = prepared.takeUtterance();
7979
+ if (volume !== void 0) utterance.volume = volume;
7298
7980
  return new Promise((resolve, reject) => {
7299
7981
  const finish = (error) => {
7300
7982
  if (this.current !== job) return;
@@ -7303,6 +7985,7 @@ var BrowserSpeakingEngine = class {
7303
7985
  utterance.onend = null;
7304
7986
  utterance.onerror = null;
7305
7987
  signal?.removeEventListener("abort", cancel);
7988
+ if (!retainPrepared) prepared.dispose();
7306
7989
  error ? reject(error) : resolve();
7307
7990
  };
7308
7991
  const cancel = () => {
@@ -7314,7 +7997,10 @@ var BrowserSpeakingEngine = class {
7314
7997
  startedAt: null,
7315
7998
  elapsedMs: 0,
7316
7999
  hasStarted: false,
7317
- estimatedDurationSeconds: Math.max(1, Array.from(text).length / (14 * rate))
8000
+ estimatedDurationSeconds: Math.max(
8001
+ 1,
8002
+ Array.from(prepared.text).length / (14 * prepared.rate)
8003
+ )
7318
8004
  };
7319
8005
  this.current = job;
7320
8006
  utterance.onstart = () => {
@@ -7337,7 +8023,7 @@ var BrowserSpeakingEngine = class {
7337
8023
  return;
7338
8024
  }
7339
8025
  try {
7340
- this.globals.speechSynthesis.resume?.();
8026
+ if (this.globals.speechSynthesis.paused) this.globals.speechSynthesis.resume?.();
7341
8027
  this.globals.speechSynthesis.speak(utterance);
7342
8028
  } catch (error) {
7343
8029
  finish(error);
@@ -7392,16 +8078,16 @@ var QwenHttpSpeakingEngine = class extends HostAudioSpeakingEngine {
7392
8078
  };
7393
8079
 
7394
8080
  // src/modules/settings/components/createLiveVoiceSettings.tsx
7395
- var import_react55 = __toESM(require("react"), 1);
8081
+ var import_react59 = __toESM(require("react"), 1);
7396
8082
 
7397
8083
  // src/modules/settings/components/LiveVoiceSettings.tsx
7398
- var import_react54 = __toESM(require("react"), 1);
8084
+ var import_react58 = __toESM(require("react"), 1);
7399
8085
 
7400
8086
  // src/modules/settings/hooks/useAudioDevices.ts
7401
- var import_react31 = __toESM(require("react"), 1);
8087
+ var import_react33 = __toESM(require("react"), 1);
7402
8088
  function useAudioDevices() {
7403
- const [audioDevices, setAudioDevices] = import_react31.default.useState([]);
7404
- import_react31.default.useEffect(() => {
8089
+ const [audioDevices, setAudioDevices] = import_react33.default.useState([]);
8090
+ import_react33.default.useEffect(() => {
7405
8091
  let active = true;
7406
8092
  const refresh = async () => {
7407
8093
  try {
@@ -7431,10 +8117,10 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
7431
8117
  }
7432
8118
 
7433
8119
  // src/modules/settings/hooks/useReleaseStatus.ts
7434
- var import_react32 = __toESM(require("react"), 1);
8120
+ var import_react34 = __toESM(require("react"), 1);
7435
8121
 
7436
8122
  // src/modules/settings/services/releases.ts
7437
- var CURRENT_VERSION = "0.5.0";
8123
+ var CURRENT_VERSION = "0.5.2";
7438
8124
  var TESTED_DSH_VERSION = "0.2.1-alpha.2";
7439
8125
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
7440
8126
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
@@ -7536,10 +8222,10 @@ function hasNewerRelease(release, currentVersion = CURRENT_VERSION) {
7536
8222
 
7537
8223
  // src/modules/settings/hooks/useReleaseStatus.ts
7538
8224
  function useReleaseStatus() {
7539
- const [latestRelease, setLatestRelease] = import_react32.default.useState(
8225
+ const [latestRelease, setLatestRelease] = import_react34.default.useState(
7540
8226
  null
7541
8227
  );
7542
- import_react32.default.useEffect(() => {
8228
+ import_react34.default.useEffect(() => {
7543
8229
  let active = true;
7544
8230
  void checkLatestRelease().then((result) => {
7545
8231
  if (active) setLatestRelease(result.release);
@@ -7552,10 +8238,10 @@ function useReleaseStatus() {
7552
8238
  }
7553
8239
 
7554
8240
  // src/modules/settings/sections/conversation/ConversationSettingsSection.tsx
7555
- var import_react38 = __toESM(require("react"), 1);
8241
+ var import_react40 = __toESM(require("react"), 1);
7556
8242
 
7557
8243
  // src/modules/settings/sections/conversation/DeliverySettings.tsx
7558
- var import_react33 = __toESM(require("react"), 1);
8244
+ var import_react35 = __toESM(require("react"), 1);
7559
8245
  function DeliverySettings({
7560
8246
  settings,
7561
8247
  updateSettings
@@ -7563,8 +8249,8 @@ function DeliverySettings({
7563
8249
  const { scoped } = useLanguage((ctx) => ctx.settings);
7564
8250
  const mode = settings.sendingMode || "manual";
7565
8251
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
7566
- const busyEnterHelpId = import_react33.default.useId();
7567
- return /* @__PURE__ */ import_react33.default.createElement(import_react33.default.Fragment, null, /* @__PURE__ */ import_react33.default.createElement(
8252
+ const busyEnterHelpId = import_react35.default.useId();
8253
+ return /* @__PURE__ */ import_react35.default.createElement(import_react35.default.Fragment, null, /* @__PURE__ */ import_react35.default.createElement(
7568
8254
  SelectField,
7569
8255
  {
7570
8256
  label: scoped.delivery.label(),
@@ -7576,11 +8262,12 @@ function DeliverySettings({
7576
8262
  ],
7577
8263
  onChange: (event) => updateSettings({ sendingMode: event.target.value })
7578
8264
  }
7579
- ), /* @__PURE__ */ import_react33.default.createElement(
8265
+ ), /* @__PURE__ */ import_react35.default.createElement(
7580
8266
  SelectField,
7581
8267
  {
7582
8268
  label: scoped.delivery.dshEnterQuestion(),
7583
8269
  "aria-describedby": busyEnterHelpId,
8270
+ hint: scoped.delivery.dshEnterHelp(),
7584
8271
  value: settings.dshBusyEnterBehavior || "queue",
7585
8272
  options: [
7586
8273
  { value: "steer", label: scoped.delivery.dshEnterSteer() },
@@ -7588,7 +8275,7 @@ function DeliverySettings({
7588
8275
  ],
7589
8276
  onChange: (event) => updateSettings({ dshBusyEnterBehavior: event.target.value })
7590
8277
  }
7591
- ), /* @__PURE__ */ import_react33.default.createElement("p", { id: busyEnterHelpId, className: "dlv-setting-description" }, scoped.delivery.dshEnterHelp()), mode !== "manual" ? /* @__PURE__ */ import_react33.default.createElement(
8278
+ ), mode !== "manual" ? /* @__PURE__ */ import_react35.default.createElement(
7592
8279
  SelectField,
7593
8280
  {
7594
8281
  label: scoped.autoSend.delay(),
@@ -7599,21 +8286,21 @@ function DeliverySettings({
7599
8286
  })),
7600
8287
  onChange: (event) => updateSettings({ autoSendDelaySeconds: Number(event.target.value) })
7601
8288
  }
7602
- ) : /* @__PURE__ */ import_react33.default.createElement("p", { className: "dlv-setting-description" }, scoped.delivery.manualLabel()));
8289
+ ) : null);
7603
8290
  }
7604
8291
 
7605
8292
  // src/modules/settings/sections/conversation/HoldToTalkSettings.tsx
7606
- var import_react34 = __toESM(require("react"), 1);
8293
+ var import_react36 = __toESM(require("react"), 1);
7607
8294
  function HoldToTalkSettings({
7608
8295
  settings,
7609
8296
  updateSettings
7610
8297
  }) {
7611
8298
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
7612
- return /* @__PURE__ */ import_react34.default.createElement(
8299
+ return /* @__PURE__ */ import_react36.default.createElement(
7613
8300
  CheckboxField,
7614
8301
  {
7615
8302
  label: recognition.holdToTalk.enabled(),
7616
- description: recognition.holdToTalk.help(),
8303
+ hint: recognition.holdToTalk.help(),
7617
8304
  checked: settings.holdToTalkEnabled !== false,
7618
8305
  onChange: (event) => updateSettings({ holdToTalkEnabled: event.target.checked })
7619
8306
  }
@@ -7621,17 +8308,18 @@ function HoldToTalkSettings({
7621
8308
  }
7622
8309
 
7623
8310
  // src/modules/settings/sections/conversation/VoiceModeSettings.tsx
7624
- var import_react35 = __toESM(require("react"), 1);
8311
+ var import_react37 = __toESM(require("react"), 1);
7625
8312
  function VoiceModeSettings({
7626
8313
  settings,
7627
8314
  updateSettings
7628
8315
  }) {
7629
8316
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
7630
8317
  const mode = settings.mode || "speaker";
7631
- return /* @__PURE__ */ import_react35.default.createElement(import_react35.default.Fragment, null, /* @__PURE__ */ import_react35.default.createElement(
8318
+ return /* @__PURE__ */ import_react37.default.createElement(import_react37.default.Fragment, null, /* @__PURE__ */ import_react37.default.createElement(
7632
8319
  SelectField,
7633
8320
  {
7634
8321
  label: recognition.mode.label(),
8322
+ hint: mode === "headphones" ? recognition.headphoneMode.help() : recognition.speakerMode.help(),
7635
8323
  value: mode,
7636
8324
  options: [
7637
8325
  { value: "speaker", label: recognition.speakerMode.label() },
@@ -7639,19 +8327,20 @@ function VoiceModeSettings({
7639
8327
  ],
7640
8328
  onChange: (event) => updateSettings({ mode: event.target.value })
7641
8329
  }
7642
- ), /* @__PURE__ */ import_react35.default.createElement("p", { className: "dlv-setting-description" }, mode === "headphones" ? recognition.headphoneMode.help() : recognition.speakerMode.help()));
8330
+ ));
7643
8331
  }
7644
8332
 
7645
8333
  // src/modules/settings/sections/conversation/ConversationDelaySettings.tsx
7646
- var import_react36 = __toESM(require("react"), 1);
8334
+ var import_react38 = __toESM(require("react"), 1);
7647
8335
  var DELAYS = [0, 1, 2, 3, 4];
7648
8336
  function ConversationDelaySettings({ settings, updateSettings }) {
7649
8337
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
7650
8338
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
7651
- return /* @__PURE__ */ import_react36.default.createElement(import_react36.default.Fragment, null, /* @__PURE__ */ import_react36.default.createElement(
8339
+ return /* @__PURE__ */ import_react38.default.createElement(import_react38.default.Fragment, null, /* @__PURE__ */ import_react38.default.createElement(
7652
8340
  SelectField,
7653
8341
  {
7654
8342
  label: speak.responseDelay.label(),
8343
+ hint: speak.responseDelay.help(),
7655
8344
  value: String(settings.assistantSpeechDelaySeconds ?? 0),
7656
8345
  options: DELAYS.map((seconds) => ({
7657
8346
  value: String(seconds),
@@ -7659,41 +8348,43 @@ function ConversationDelaySettings({ settings, updateSettings }) {
7659
8348
  })),
7660
8349
  onChange: (event) => updateSettings({ assistantSpeechDelaySeconds: Number(event.target.value) })
7661
8350
  }
7662
- ), /* @__PURE__ */ import_react36.default.createElement("p", { className: "dlv-setting-description" }, speak.responseDelay.help()));
8351
+ ));
7663
8352
  }
7664
8353
 
7665
8354
  // src/modules/settings/sections/speak/PlaybackPolicySettings.tsx
7666
- var import_react37 = __toESM(require("react"), 1);
8355
+ var import_react39 = __toESM(require("react"), 1);
7667
8356
  function PlaybackPolicySettings({ settings, updateSettings }) {
7668
8357
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
7669
- return /* @__PURE__ */ import_react37.default.createElement(import_react37.default.Fragment, null, /* @__PURE__ */ import_react37.default.createElement(
8358
+ return /* @__PURE__ */ import_react39.default.createElement(import_react39.default.Fragment, null, /* @__PURE__ */ import_react39.default.createElement(
7670
8359
  CheckboxField,
7671
8360
  {
7672
8361
  label: speak.autoPlayback.label(),
8362
+ hint: speak.autoPlayback.help(),
7673
8363
  checked: settings.announceAssistantMessages !== false,
7674
8364
  onChange: (event) => updateSettings({ announceAssistantMessages: event.target.checked })
7675
8365
  }
7676
- ), /* @__PURE__ */ import_react37.default.createElement("p", null, speak.autoPlayback.help()), /* @__PURE__ */ import_react37.default.createElement(
8366
+ ), /* @__PURE__ */ import_react39.default.createElement(
7677
8367
  CheckboxField,
7678
8368
  {
7679
8369
  label: speak.interruption.enabled(),
8370
+ hint: settings.interruptSpeechOnUserMessage ? speak.interruption.enabledHelp() : speak.interruption.disabledHelp(),
7680
8371
  checked: Boolean(settings.interruptSpeechOnUserMessage),
7681
8372
  onChange: (event) => updateSettings({ interruptSpeechOnUserMessage: event.target.checked })
7682
8373
  }
7683
- ), /* @__PURE__ */ import_react37.default.createElement("p", null, settings.interruptSpeechOnUserMessage ? speak.interruption.enabledHelp() : speak.interruption.disabledHelp()));
8374
+ ));
7684
8375
  }
7685
8376
 
7686
8377
  // src/modules/settings/sections/conversation/ConversationSettingsSection.tsx
7687
8378
  function ConversationSettingsSection(props) {
7688
8379
  const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
7689
- return /* @__PURE__ */ import_react38.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react38.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react38.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "send" }, /* @__PURE__ */ import_react38.default.createElement("div", { className: "dlv-conversation-fields" }, /* @__PURE__ */ import_react38.default.createElement(DeliverySettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(HoldToTalkSettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(VoiceModeSettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(PlaybackPolicySettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(ConversationDelaySettings, { ...props })))));
8380
+ return /* @__PURE__ */ import_react40.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react40.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react40.default.createElement(SettingsSection, { title: settingsLanguage.layout.delivery(), icon: "send" }, /* @__PURE__ */ import_react40.default.createElement(DeliverySettings, { ...props })), /* @__PURE__ */ import_react40.default.createElement(SettingsSection, { title: settingsLanguage.layout.listening(), icon: "headphones" }, /* @__PURE__ */ import_react40.default.createElement(VoiceModeSettings, { ...props }), /* @__PURE__ */ import_react40.default.createElement(PlaybackPolicySettings, { ...props }), /* @__PURE__ */ import_react40.default.createElement(ConversationDelaySettings, { ...props })), /* @__PURE__ */ import_react40.default.createElement(SettingsSection, { title: settingsLanguage.layout.shortcut(), icon: "keyboard" }, /* @__PURE__ */ import_react40.default.createElement(HoldToTalkSettings, { ...props }))));
7690
8381
  }
7691
8382
 
7692
8383
  // src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx
7693
- var import_react41 = __toESM(require("react"), 1);
8384
+ var import_react43 = __toESM(require("react"), 1);
7694
8385
 
7695
8386
  // src/modules/core/qwen/QwenSettings.tsx
7696
- var import_react39 = __toESM(require("react"), 1);
8387
+ var import_react41 = __toESM(require("react"), 1);
7697
8388
  var BASE = "/api/dsh-live-voice/qwen";
7698
8389
  var UNLOADED = "dsh-live-voice.speak.qwen.restartRequired";
7699
8390
  async function qwenSettingsRequest(path, { method = "GET", config, signal } = {}, fetchImpl = globalThis.fetch) {
@@ -7722,15 +8413,15 @@ function QwenSettings({ controller }) {
7722
8413
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
7723
8414
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
7724
8415
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
7725
- const [draft, setDraft] = import_react39.default.useState({
8416
+ const [draft, setDraft] = import_react41.default.useState({
7726
8417
  baseUrl: "http://127.0.0.1:8080/",
7727
8418
  timeoutMs: 3e5
7728
8419
  });
7729
- const [busy, setBusy] = import_react39.default.useState(true);
7730
- const [loaded, setLoaded] = import_react39.default.useState(false);
7731
- const [error, setError] = import_react39.default.useState("");
7732
- const [message2, setMessage] = import_react39.default.useState("");
7733
- const active = import_react39.default.useRef(null);
8420
+ const [busy, setBusy] = import_react41.default.useState(true);
8421
+ const [loaded, setLoaded] = import_react41.default.useState(false);
8422
+ const [error, setError] = import_react41.default.useState("");
8423
+ const [message2, setMessage] = import_react41.default.useState("");
8424
+ const active = import_react41.default.useRef(null);
7734
8425
  const translateStatus = (value) => ({
7735
8426
  "dsh-live-voice.commons.connection.unsaved": commons.connection.unsaved(),
7736
8427
  "dsh-live-voice.speak.qwen.saved": speak.qwen.saved(),
@@ -7764,7 +8455,6 @@ function QwenSettings({ controller }) {
7764
8455
  if (!abort.signal.aborted) {
7765
8456
  setDraft(value);
7766
8457
  setMessage("dsh-live-voice.speak.qwen.saved");
7767
- await controller.refreshCapabilities?.();
7768
8458
  }
7769
8459
  } else {
7770
8460
  const value = await qwenSettingsRequest("/test", {
@@ -7784,13 +8474,13 @@ function QwenSettings({ controller }) {
7784
8474
  if (!abort.signal.aborted) setBusy(false);
7785
8475
  }
7786
8476
  }
7787
- import_react39.default.useEffect(() => {
8477
+ import_react41.default.useEffect(() => {
7788
8478
  void run("load");
7789
8479
  return () => active.current?.abort();
7790
8480
  }, []);
7791
8481
  const field = (label, key, type = "text") => {
7792
8482
  const Field = type === "number" ? NumberField : TextField;
7793
- return /* @__PURE__ */ import_react39.default.createElement(
8483
+ return /* @__PURE__ */ import_react41.default.createElement(
7794
8484
  Field,
7795
8485
  {
7796
8486
  label,
@@ -7806,11 +8496,17 @@ function QwenSettings({ controller }) {
7806
8496
  }
7807
8497
  );
7808
8498
  };
7809
- return /* @__PURE__ */ import_react39.default.createElement(import_react39.default.Fragment, null, /* @__PURE__ */ import_react39.default.createElement("p", null, recognition.qwen.hostHelp()), field(speak.qwen.endpoint(), "baseUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react39.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react39.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, speak.qwen.save()), /* @__PURE__ */ import_react39.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, speak.qwen.test()), /* @__PURE__ */ import_react39.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react39.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react39.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react39.default.createElement("p", { role: "alert" }, translateStatus(error)));
8499
+ return /* @__PURE__ */ import_react41.default.createElement(import_react41.default.Fragment, null, /* @__PURE__ */ import_react41.default.createElement("p", null, recognition.qwen.hostHelp()), field(speak.qwen.endpoint(), "baseUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react41.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, speak.qwen.save()), /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, speak.qwen.test()), /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react41.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react41.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react41.default.createElement("p", { role: "alert" }, translateStatus(error)));
8500
+ }
8501
+
8502
+ // src/modules/core/engineSelection.ts
8503
+ var recognitionEngineOrder = Object.freeze(["browser", "qwen-http", "whisper-http"]);
8504
+ function speakingEngineOrder(hostPlatform) {
8505
+ return hostPlatform === "darwin" ? ["say", "browser", "qwen-http"] : ["browser", "qwen-http", "say"];
7810
8506
  }
7811
8507
 
7812
8508
  // src/modules/recognition/engines/whisper/WhisperSettings.tsx
7813
- var import_react40 = __toESM(require("react"), 1);
8509
+ var import_react42 = __toESM(require("react"), 1);
7814
8510
  var BASE2 = "/api/dsh-live-voice/whisper";
7815
8511
  var UNLOADED2 = "dsh-live-voice.recognition.whisper.restartRequired";
7816
8512
  async function whisperSettingsRequest(path, { method = "GET", config, signal } = {}, fetchImpl = globalThis.fetch) {
@@ -7839,16 +8535,16 @@ function WhisperSettings({ controller }) {
7839
8535
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
7840
8536
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
7841
8537
  const { scoped: settings } = useLanguage((ctx) => ctx.settings);
7842
- const [draft, setDraft] = import_react40.default.useState({
8538
+ const [draft, setDraft] = import_react42.default.useState({
7843
8539
  url: "http://127.0.0.1:8080/inference",
7844
8540
  healthUrl: "/health",
7845
8541
  timeoutMs: 3e4
7846
8542
  });
7847
- const [busy, setBusy] = import_react40.default.useState(true);
7848
- const [loaded, setLoaded] = import_react40.default.useState(false);
7849
- const [error, setError] = import_react40.default.useState("");
7850
- const [message2, setMessage] = import_react40.default.useState("");
7851
- const active = import_react40.default.useRef(null);
8543
+ const [busy, setBusy] = import_react42.default.useState(true);
8544
+ const [loaded, setLoaded] = import_react42.default.useState(false);
8545
+ const [error, setError] = import_react42.default.useState("");
8546
+ const [message2, setMessage] = import_react42.default.useState("");
8547
+ const active = import_react42.default.useRef(null);
7852
8548
  const translateStatus = (value) => ({
7853
8549
  "dsh-live-voice.commons.connection.unsaved": commons.connection.unsaved(),
7854
8550
  "dsh-live-voice.recognition.whisper.saved": recognition.whisper.saved(),
@@ -7882,7 +8578,6 @@ function WhisperSettings({ controller }) {
7882
8578
  if (!abort.signal.aborted) {
7883
8579
  setDraft(value);
7884
8580
  setMessage("dsh-live-voice.recognition.whisper.saved");
7885
- await controller.refreshCapabilities?.();
7886
8581
  }
7887
8582
  } else {
7888
8583
  const value = await whisperSettingsRequest("/test", {
@@ -7902,13 +8597,13 @@ function WhisperSettings({ controller }) {
7902
8597
  if (!abort.signal.aborted) setBusy(false);
7903
8598
  }
7904
8599
  }
7905
- import_react40.default.useEffect(() => {
8600
+ import_react42.default.useEffect(() => {
7906
8601
  void run("load");
7907
8602
  return () => active.current?.abort();
7908
8603
  }, []);
7909
8604
  const field = (label, key, type = "text") => {
7910
8605
  const Field = type === "number" ? NumberField : TextField;
7911
- return /* @__PURE__ */ import_react40.default.createElement(
8606
+ return /* @__PURE__ */ import_react42.default.createElement(
7912
8607
  Field,
7913
8608
  {
7914
8609
  label,
@@ -7924,7 +8619,7 @@ function WhisperSettings({ controller }) {
7924
8619
  }
7925
8620
  );
7926
8621
  };
7927
- return /* @__PURE__ */ import_react40.default.createElement(import_react40.default.Fragment, null, /* @__PURE__ */ import_react40.default.createElement("p", null, settings.whisper.hostHelp()), field(commons.connection.endpoint(), "url"), field(commons.connection.healthEndpoint(), "healthUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react40.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, recognition.whisper.save()), /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, commons.connection.test()), /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react40.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react40.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react40.default.createElement("p", { role: "alert" }, translateStatus(error)));
8622
+ return /* @__PURE__ */ import_react42.default.createElement(import_react42.default.Fragment, null, /* @__PURE__ */ import_react42.default.createElement("p", null, settings.whisper.hostHelp()), field(commons.connection.endpoint(), "url"), field(commons.connection.healthEndpoint(), "healthUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react42.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react42.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, recognition.whisper.save()), /* @__PURE__ */ import_react42.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, commons.connection.test()), /* @__PURE__ */ import_react42.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react42.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react42.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react42.default.createElement("p", { role: "alert" }, translateStatus(error)));
7928
8623
  }
7929
8624
 
7930
8625
  // src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx
@@ -7941,32 +8636,22 @@ function RecognitionEngineSettings({
7941
8636
  const setEngine = (next) => updateSettings(
7942
8637
  next === "browser" && settings.recognitionLang === "auto" ? { recognitionEngine: next, recognitionLang: "pt-BR" } : { recognitionEngine: next }
7943
8638
  );
7944
- return /* @__PURE__ */ import_react41.default.createElement(import_react41.default.Fragment, null, /* @__PURE__ */ import_react41.default.createElement(
8639
+ return /* @__PURE__ */ import_react43.default.createElement(import_react43.default.Fragment, null, /* @__PURE__ */ import_react43.default.createElement(
7945
8640
  SelectField,
7946
8641
  {
7947
8642
  label: recognition.engine.label(),
7948
8643
  value: engine,
7949
- options: [
7950
- {
7951
- value: "browser",
7952
- label: recognition.browser.label()
7953
- // Keep Browser selectable so unavailable local processing can be reconfigured.
7954
- // capabilities.recognition describes the current engine, not Browser support.
7955
- },
7956
- {
7957
- value: "qwen-http",
7958
- label: recognition.qwen.label(),
7959
- disabled: capabilities["qwen-http-recognition"]?.supported === false
7960
- },
7961
- {
7962
- value: "whisper-http",
7963
- label: recognition.whisper.label(),
7964
- disabled: capabilities["whisper-http"]?.supported === false
7965
- }
7966
- ],
8644
+ options: recognitionEngineOrder.map((value) => ({
8645
+ value,
8646
+ label: {
8647
+ browser: recognition.browser.label(),
8648
+ "qwen-http": recognition.qwen.label(),
8649
+ "whisper-http": recognition.whisper.label()
8650
+ }[value]
8651
+ })),
7967
8652
  onChange: (event) => setEngine(event.target.value)
7968
8653
  }
7969
- ), /* @__PURE__ */ import_react41.default.createElement(
8654
+ ), /* @__PURE__ */ import_react43.default.createElement(
7970
8655
  SelectField,
7971
8656
  {
7972
8657
  label: recognition.microphone.device(),
@@ -7979,7 +8664,7 @@ function RecognitionEngineSettings({
7979
8664
  ),
7980
8665
  onChange: (event) => updateSettings({ inputDeviceId: event.target.value })
7981
8666
  }
7982
- ), /* @__PURE__ */ import_react41.default.createElement(
8667
+ ), /* @__PURE__ */ import_react43.default.createElement(
7983
8668
  SelectField,
7984
8669
  {
7985
8670
  label: recognition.language.label(),
@@ -7987,41 +8672,44 @@ function RecognitionEngineSettings({
7987
8672
  options: [
7988
8673
  ...engine === "browser" ? [] : [{ value: "auto", label: recognition.language.automatic() }],
7989
8674
  { value: "pt-BR", label: "Portugu\xEAs (Brasil)" },
7990
- { value: "en-US", label: "English (US)" }
8675
+ { value: "en-US", label: "English (US)" },
8676
+ { value: "zh-CN", label: recognition.language.options.chinese() },
8677
+ { value: "es-ES", label: recognition.language.options.spanish() },
8678
+ { value: "fr-FR", label: recognition.language.options.french() }
7991
8679
  ],
7992
8680
  onChange: (event) => updateSettings({ recognitionLang: event.target.value })
7993
8681
  }
7994
- ), engine === "browser" && /* @__PURE__ */ import_react41.default.createElement(import_react41.default.Fragment, null, /* @__PURE__ */ import_react41.default.createElement("p", null, recognition.browser.help()), /* @__PURE__ */ import_react41.default.createElement(
8682
+ ), engine === "browser" && /* @__PURE__ */ import_react43.default.createElement(import_react43.default.Fragment, null, /* @__PURE__ */ import_react43.default.createElement("p", null, recognition.browser.help()), /* @__PURE__ */ import_react43.default.createElement(
7995
8683
  CheckboxField,
7996
8684
  {
7997
8685
  label: recognition.browser.localProcessing(),
7998
8686
  checked: settings.recognitionProcessLocally !== false,
7999
8687
  onChange: (event) => updateSettings({ recognitionProcessLocally: event.target.checked })
8000
8688
  }
8001
- ), settings.recognitionProcessLocally !== false ? /* @__PURE__ */ import_react41.default.createElement(
8689
+ ), settings.recognitionProcessLocally !== false ? /* @__PURE__ */ import_react43.default.createElement(
8002
8690
  CheckboxField,
8003
8691
  {
8004
8692
  label: recognition.browser.autoInstallPack(),
8005
8693
  checked: settings.recognitionAutoInstall !== false,
8006
8694
  onChange: (event) => updateSettings({ recognitionAutoInstall: event.target.checked })
8007
8695
  }
8008
- ) : /* @__PURE__ */ import_react41.default.createElement("p", { role: "status" }, recognition.browser.remoteServiceWarning())), engine === "whisper-http" && /* @__PURE__ */ import_react41.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react41.default.createElement("p", null, recognition.whisper.captureHelp()), /* @__PURE__ */ import_react41.default.createElement(WhisperSettings, { controller })), engine === "qwen-http" && /* @__PURE__ */ import_react41.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react41.default.createElement("p", null, recognition.qwen.captureHelp()), settings.engine !== "qwen-http" && /* @__PURE__ */ import_react41.default.createElement(QwenSettings, { controller })));
8696
+ ) : /* @__PURE__ */ import_react43.default.createElement("p", { role: "status" }, recognition.browser.remoteServiceWarning())), engine === "whisper-http" && /* @__PURE__ */ import_react43.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react43.default.createElement("p", null, recognition.whisper.captureHelp()), /* @__PURE__ */ import_react43.default.createElement(WhisperSettings, { controller })), engine === "qwen-http" && /* @__PURE__ */ import_react43.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react43.default.createElement("p", null, recognition.qwen.captureHelp()), settings.engine !== "qwen-http" && /* @__PURE__ */ import_react43.default.createElement(QwenSettings, { controller })));
8009
8697
  }
8010
8698
 
8011
8699
  // src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx
8012
- var import_react42 = __toESM(require("react"), 1);
8700
+ var import_react44 = __toESM(require("react"), 1);
8013
8701
  function RecognitionFilterSettings({ settings, updateSettings }) {
8014
8702
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
8015
8703
  const enabled = settings.recognitionFilterEnabled !== false;
8016
- return /* @__PURE__ */ import_react42.default.createElement(SettingsSubcard, { title: recognition.minimumWords.label(), icon: "filter" }, /* @__PURE__ */ import_react42.default.createElement(
8704
+ return /* @__PURE__ */ import_react44.default.createElement(SettingsSection, { title: recognition.minimumWords.label(), icon: "filter" }, /* @__PURE__ */ import_react44.default.createElement(
8017
8705
  CheckboxField,
8018
8706
  {
8019
8707
  label: recognition.minimumWords.enabled(),
8020
- description: recognition.minimumWords.help(),
8708
+ hint: recognition.minimumWords.help(),
8021
8709
  checked: enabled,
8022
8710
  onChange: (event) => updateSettings({ recognitionFilterEnabled: event.target.checked })
8023
8711
  }
8024
- ), /* @__PURE__ */ import_react42.default.createElement(
8712
+ ), /* @__PURE__ */ import_react44.default.createElement(
8025
8713
  NumberField,
8026
8714
  {
8027
8715
  label: recognition.minimumWords.label(),
@@ -8039,14 +8727,14 @@ function RecognitionFilterSettings({ settings, updateSettings }) {
8039
8727
  }
8040
8728
 
8041
8729
  // src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx
8042
- var import_react46 = __toESM(require("react"), 1);
8730
+ var import_react50 = __toESM(require("react"), 1);
8043
8731
 
8044
8732
  // src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx
8045
- var import_react43 = __toESM(require("react"), 1);
8733
+ var import_react45 = __toESM(require("react"), 1);
8046
8734
  function SilenceDetectionSettings({ settings, updateSettings }) {
8047
8735
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
8048
- const [sileroAvailable, setSileroAvailable] = import_react43.default.useState(false);
8049
- import_react43.default.useEffect(() => {
8736
+ const [sileroAvailable, setSileroAvailable] = import_react45.default.useState(false);
8737
+ import_react45.default.useEffect(() => {
8050
8738
  let current = true;
8051
8739
  void sileroVadAvailable().then((available) => {
8052
8740
  if (current) setSileroAvailable(available);
@@ -8058,15 +8746,16 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
8058
8746
  const savedCustomMs = normalizeCustomSilenceMs(settings.voiceDetectionCustomSilenceMs);
8059
8747
  if (!usesPluginVoiceDetection(settings.recognitionEngine)) return null;
8060
8748
  const selected = settings.voiceDetectionPreset || defaultSettings.voiceDetectionPreset;
8061
- return /* @__PURE__ */ import_react43.default.createElement(
8062
- SettingsSubcard,
8749
+ return /* @__PURE__ */ import_react45.default.createElement(
8750
+ SettingsSection,
8063
8751
  {
8064
8752
  title: recognition.silenceDetection.label(),
8065
8753
  ariaLabel: recognition.silenceDetection.title(),
8066
- icon: "pause"
8754
+ icon: "pause",
8755
+ defaultOpen: false
8067
8756
  },
8068
- /* @__PURE__ */ import_react43.default.createElement("p", null, recognition.silenceDetection.help()),
8069
- /* @__PURE__ */ import_react43.default.createElement(
8757
+ /* @__PURE__ */ import_react45.default.createElement("p", null, recognition.silenceDetection.help()),
8758
+ /* @__PURE__ */ import_react45.default.createElement(
8070
8759
  SelectField,
8071
8760
  {
8072
8761
  label: recognition.silenceDetection.detector.label(),
@@ -8082,7 +8771,7 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
8082
8771
  onChange: (event) => updateSettings({ voiceDetectionEngine: event.target.value })
8083
8772
  }
8084
8773
  ),
8085
- /* @__PURE__ */ import_react43.default.createElement(
8774
+ /* @__PURE__ */ import_react45.default.createElement(
8086
8775
  NumberField,
8087
8776
  {
8088
8777
  label: recognition.maxUtterance.label(),
@@ -8097,15 +8786,15 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
8097
8786
  }
8098
8787
  }
8099
8788
  ),
8100
- /* @__PURE__ */ import_react43.default.createElement("small", null, recognition.maxUtterance.help()),
8101
- /* @__PURE__ */ import_react43.default.createElement(
8789
+ /* @__PURE__ */ import_react45.default.createElement("small", null, recognition.maxUtterance.help()),
8790
+ /* @__PURE__ */ import_react45.default.createElement(
8102
8791
  "div",
8103
8792
  {
8104
8793
  className: "dlv-preset-group",
8105
8794
  role: "radiogroup",
8106
8795
  "aria-label": recognition.silenceDetection.pauseLabel()
8107
8796
  },
8108
- [...Object.keys(voiceDetectionPresets), "custom"].map((value) => /* @__PURE__ */ import_react43.default.createElement("label", { key: value, className: "dlv-preset" }, /* @__PURE__ */ import_react43.default.createElement(
8797
+ [...Object.keys(voiceDetectionPresets), "custom"].map((value) => /* @__PURE__ */ import_react45.default.createElement("label", { key: value, className: "dlv-preset" }, /* @__PURE__ */ import_react45.default.createElement(
8109
8798
  "input",
8110
8799
  {
8111
8800
  type: "radio",
@@ -8114,9 +8803,9 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
8114
8803
  checked: selected === value,
8115
8804
  onChange: () => updateSettings({ voiceDetectionPreset: value })
8116
8805
  }
8117
- ), /* @__PURE__ */ import_react43.default.createElement("span", null, /* @__PURE__ */ import_react43.default.createElement("strong", null, recognition.presets[value].label()), /* @__PURE__ */ import_react43.default.createElement("small", null, recognition.presets[value].description()))))
8806
+ ), /* @__PURE__ */ import_react45.default.createElement("span", null, /* @__PURE__ */ import_react45.default.createElement("strong", null, recognition.presets[value].label()), /* @__PURE__ */ import_react45.default.createElement("small", null, recognition.presets[value].description()))))
8118
8807
  ),
8119
- selected === "custom" && /* @__PURE__ */ import_react43.default.createElement(import_react43.default.Fragment, null, /* @__PURE__ */ import_react43.default.createElement(
8808
+ selected === "custom" && /* @__PURE__ */ import_react45.default.createElement(import_react45.default.Fragment, null, /* @__PURE__ */ import_react45.default.createElement(
8120
8809
  NumberField,
8121
8810
  {
8122
8811
  label: recognition.silenceDetection.customLabel(),
@@ -8130,19 +8819,102 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
8130
8819
  updateSettings({ voiceDetectionCustomSilenceMs: value });
8131
8820
  }
8132
8821
  }
8133
- ), /* @__PURE__ */ import_react43.default.createElement("small", null, recognition.silenceDetection.customHelp())),
8134
- /* @__PURE__ */ import_react43.default.createElement("p", { className: "dlv-vad-summary" }, recognition.silenceDetection.duration({
8822
+ ), /* @__PURE__ */ import_react45.default.createElement("small", null, recognition.silenceDetection.customHelp())),
8823
+ /* @__PURE__ */ import_react45.default.createElement("p", { className: "dlv-vad-summary" }, recognition.silenceDetection.duration({
8135
8824
  milliseconds: voiceDetectionSilenceMs(settings)
8136
8825
  }))
8137
8826
  );
8138
8827
  }
8139
8828
 
8140
8829
  // src/modules/settings/sections/recognition/VoiceCommandSettings.tsx
8141
- var import_react44 = __toESM(require("react"), 1);
8830
+ var import_react47 = __toESM(require("react"), 1);
8831
+
8832
+ // src/shared/design-system/forms/PhraseField.tsx
8833
+ var import_react46 = __toESM(require("react"), 1);
8834
+ function PhraseField({
8835
+ label,
8836
+ value,
8837
+ disabled,
8838
+ onCommit,
8839
+ addLabel,
8840
+ removeLabel
8841
+ }) {
8842
+ const id2 = import_react46.default.useId();
8843
+ const [draft, setDraft] = import_react46.default.useState("");
8844
+ const [phrases2, setPhrases] = import_react46.default.useState(
8845
+ () => value.split(/[,\r\n]+/).map((s) => s.trim()).filter(Boolean)
8846
+ );
8847
+ const editing = import_react46.default.useRef(false);
8848
+ import_react46.default.useEffect(() => {
8849
+ if (!editing.current)
8850
+ setPhrases(
8851
+ value.split(/[,\r\n]+/).map((s) => s.trim()).filter(Boolean)
8852
+ );
8853
+ }, [value]);
8854
+ const commit = (raw = draft) => {
8855
+ const additions = raw.split(/[,\r\n]+/).map((s) => s.trim()).filter(Boolean);
8856
+ if (!additions.length) {
8857
+ setDraft("");
8858
+ return;
8859
+ }
8860
+ const next = [...phrases2, ...additions];
8861
+ if (next.join(", ").length > 1e3) return;
8862
+ setPhrases(next);
8863
+ setDraft("");
8864
+ onCommit(next.join(", "));
8865
+ };
8866
+ return /* @__PURE__ */ import_react46.default.createElement("div", { className: "dlv-phrase-field" }, /* @__PURE__ */ import_react46.default.createElement("label", { id: id2, htmlFor: id2 + "-input" }, label), /* @__PURE__ */ import_react46.default.createElement("div", { className: "dlv-phrase-box", "aria-disabled": disabled }, phrases2.map((phrase, index) => /* @__PURE__ */ import_react46.default.createElement("span", { className: "dlv-phrase", key: index }, /* @__PURE__ */ import_react46.default.createElement("span", null, phrase), /* @__PURE__ */ import_react46.default.createElement(
8867
+ "button",
8868
+ {
8869
+ type: "button",
8870
+ disabled,
8871
+ "aria-label": removeLabel + ": " + phrase,
8872
+ onClick: () => {
8873
+ const next = phrases2.filter((_2, i) => i !== index);
8874
+ setPhrases(next);
8875
+ onCommit(next.join(", "));
8876
+ }
8877
+ },
8878
+ "\xD7"
8879
+ ))), /* @__PURE__ */ import_react46.default.createElement(
8880
+ "input",
8881
+ {
8882
+ id: id2 + "-input",
8883
+ "aria-labelledby": id2,
8884
+ placeholder: addLabel,
8885
+ value: draft,
8886
+ disabled,
8887
+ maxLength: 1e3,
8888
+ onFocus: () => {
8889
+ editing.current = true;
8890
+ },
8891
+ onInput: (e) => setDraft(e.currentTarget.value),
8892
+ onChange: (e) => setDraft(e.target.value),
8893
+ onBlur: () => {
8894
+ editing.current = false;
8895
+ commit();
8896
+ },
8897
+ onKeyDown: (e) => {
8898
+ if (e.nativeEvent.isComposing) return;
8899
+ if (e.key === "Enter" || e.key === ",") {
8900
+ e.preventDefault();
8901
+ commit();
8902
+ }
8903
+ if (e.key === "Escape") {
8904
+ e.preventDefault();
8905
+ e.stopPropagation();
8906
+ setDraft("");
8907
+ }
8908
+ }
8909
+ }
8910
+ )));
8911
+ }
8912
+
8913
+ // src/modules/settings/sections/recognition/VoiceCommandSettings.tsx
8142
8914
  var COMMANDS = [
8143
8915
  ["send", "voiceCommandSend", "send message, send it"],
8144
8916
  ["queue", "voiceCommandQueue", "queue message, queue it"],
8145
- ["stopSpeech", "voiceCommandEnd", "end conversation, stop conversation"],
8917
+ ["end", "voiceCommandEnd", "end conversation, stop conversation"],
8146
8918
  ["mute", "voiceCommandMute", "mute microphone, stop listening"],
8147
8919
  ["resume", "voiceCommandResume", "resume listening, unmute microphone"],
8148
8920
  ["stopSpeech", "voiceCommandStopSpeaking", "stop talking, stop speaking, shut up"],
@@ -8151,126 +8923,216 @@ var COMMANDS = [
8151
8923
  function VoiceCommandSettings({ settings, updateSettings }) {
8152
8924
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
8153
8925
  const enabled = settings.voiceCommandsEnabled !== false;
8154
- return /* @__PURE__ */ import_react44.default.createElement(SettingsSubcard, { title: recognition.commands.title(), icon: "send" }, /* @__PURE__ */ import_react44.default.createElement(
8155
- CheckboxField,
8926
+ return /* @__PURE__ */ import_react47.default.createElement(
8927
+ SettingsSection,
8156
8928
  {
8157
- label: recognition.commands.enabled(),
8158
- checked: enabled,
8159
- onChange: (event) => updateSettings({ voiceCommandsEnabled: event.target.checked })
8160
- }
8161
- ), /* @__PURE__ */ import_react44.default.createElement("p", { className: "dlv-setting-description" }, recognition.voiceCommands.help()), COMMANDS.map(([label, key, fallback]) => /* @__PURE__ */ import_react44.default.createElement(
8162
- TextAreaField,
8163
- {
8164
- key,
8165
- rows: 2,
8166
- maxLength: 1e3,
8167
- label: recognition.commands[label](),
8168
- value: settings[key] ?? fallback,
8169
- disabled: !enabled,
8170
- onCommit: (value) => updateSettings({ [key]: value })
8171
- }
8172
- )));
8929
+ title: recognition.commands.title(),
8930
+ icon: "commands",
8931
+ defaultOpen: false
8932
+ },
8933
+ /* @__PURE__ */ import_react47.default.createElement(
8934
+ CheckboxField,
8935
+ {
8936
+ label: recognition.commands.enabled(),
8937
+ hint: recognition.voiceCommands.help(),
8938
+ checked: enabled,
8939
+ onChange: (event) => updateSettings({ voiceCommandsEnabled: event.target.checked })
8940
+ }
8941
+ ),
8942
+ COMMANDS.map(([label, key, fallback]) => /* @__PURE__ */ import_react47.default.createElement(
8943
+ PhraseField,
8944
+ {
8945
+ key,
8946
+ addLabel: recognition.commands.addPhrase(),
8947
+ removeLabel: recognition.commands.removePhrase(),
8948
+ label: recognition.commands[label](),
8949
+ value: settings[key] ?? fallback,
8950
+ disabled: !enabled,
8951
+ onCommit: (value) => updateSettings({ [key]: value })
8952
+ }
8953
+ ))
8954
+ );
8173
8955
  }
8174
8956
 
8175
8957
  // src/modules/settings/sections/recognition/RecognitionStatus.tsx
8176
- var import_react45 = __toESM(require("react"), 1);
8958
+ var import_react48 = __toESM(require("react"), 1);
8177
8959
  function RecognitionStatus({ capabilities = {} }) {
8178
8960
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
8179
- return /* @__PURE__ */ import_react45.default.createElement(import_react45.default.Fragment, null, capabilities.capture?.supported === false ? /* @__PURE__ */ import_react45.default.createElement("p", { role: "status" }, recognition.microphone.failure({ reason: capabilities.capture.reason || "" })) : capabilities.capture?.permission === "prompt" ? /* @__PURE__ */ import_react45.default.createElement("p", { role: "status" }, recognition.microphone.permissionHelp()) : null, capabilities.recognition?.supported === false && /* @__PURE__ */ import_react45.default.createElement("p", { role: "status" }, capabilities.recognition.reason));
8961
+ return /* @__PURE__ */ import_react48.default.createElement(import_react48.default.Fragment, null, capabilities.capture?.supported === false ? /* @__PURE__ */ import_react48.default.createElement("p", { role: "status" }, recognition.microphone.failure({ reason: capabilities.capture.reason || "" })) : capabilities.capture?.permission === "prompt" ? /* @__PURE__ */ import_react48.default.createElement("p", { role: "status" }, recognition.microphone.permissionHelp()) : null, capabilities.recognition?.supported === false && /* @__PURE__ */ import_react48.default.createElement("p", { role: "status" }, capabilities.recognition.reason));
8962
+ }
8963
+
8964
+ // src/modules/settings/sections/conversation/MeetingSourceSettings.tsx
8965
+ var import_react49 = __toESM(require("react"), 1);
8966
+ function MeetingSourceSettings({ settings, updateSettings }) {
8967
+ const { scoped: language } = useLanguage((ctx) => ctx.settings);
8968
+ const { scoped: meeting } = useLanguage((ctx) => ctx.meeting);
8969
+ const names = language.sourceNames;
8970
+ return /* @__PURE__ */ import_react49.default.createElement(SettingsSection, { title: meeting.title(), icon: "meeting", defaultOpen: false }, /* @__PURE__ */ import_react49.default.createElement("p", null, names.help()), /* @__PURE__ */ import_react49.default.createElement(
8971
+ DraftField,
8972
+ {
8973
+ label: names.microphone(),
8974
+ value: settings.microphoneSourceName,
8975
+ maxLength: 80,
8976
+ onCommit: (microphoneSourceName) => updateSettings({ microphoneSourceName })
8977
+ }
8978
+ ), /* @__PURE__ */ import_react49.default.createElement(
8979
+ DraftField,
8980
+ {
8981
+ label: names.shared(),
8982
+ value: settings.sharedSourceName,
8983
+ maxLength: 80,
8984
+ onCommit: (sharedSourceName) => updateSettings({ sharedSourceName })
8985
+ }
8986
+ ));
8180
8987
  }
8181
8988
 
8182
8989
  // src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx
8183
8990
  function RecognitionSettingsSection(props) {
8184
8991
  const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
8185
- return /* @__PURE__ */ import_react46.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react46.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react46.default.createElement(RecognitionStatus, { ...props }), /* @__PURE__ */ import_react46.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "mic" }, /* @__PURE__ */ import_react46.default.createElement(RecognitionEngineSettings, { ...props })), /* @__PURE__ */ import_react46.default.createElement(VoiceCommandSettings, { ...props }), /* @__PURE__ */ import_react46.default.createElement(RecognitionFilterSettings, { ...props }), /* @__PURE__ */ import_react46.default.createElement(SilenceDetectionSettings, { ...props })));
8992
+ return /* @__PURE__ */ import_react50.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react50.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react50.default.createElement(RecognitionStatus, { ...props }), /* @__PURE__ */ import_react50.default.createElement(SettingsSection, { title: settingsLanguage.layout.transcription(), icon: "mic" }, /* @__PURE__ */ import_react50.default.createElement(RecognitionEngineSettings, { ...props })), /* @__PURE__ */ import_react50.default.createElement(MeetingSourceSettings, { ...props }), /* @__PURE__ */ import_react50.default.createElement(VoiceCommandSettings, { ...props }), /* @__PURE__ */ import_react50.default.createElement(SilenceDetectionSettings, { ...props }), /* @__PURE__ */ import_react50.default.createElement(RecognitionFilterSettings, { ...props })));
8186
8993
  }
8187
8994
 
8188
8995
  // src/modules/settings/sections/speak/OutputFilterSettings.tsx
8189
- var import_react47 = __toESM(require("react"), 1);
8996
+ var import_react51 = __toESM(require("react"), 1);
8190
8997
  function OutputFilterSettings({ settings, updateSettings }) {
8191
8998
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
8192
8999
  const enabled = settings.outputCodeFilterEnabled !== false;
8193
- return /* @__PURE__ */ import_react47.default.createElement(SettingsSubcard, { title: speak.filters.code.enabled(), icon: "filter" }, /* @__PURE__ */ import_react47.default.createElement(
8194
- CheckboxField,
8195
- {
8196
- label: speak.filters.code.enabled(),
8197
- checked: enabled,
8198
- onChange: (event) => updateSettings({ outputCodeFilterEnabled: event.target.checked })
8199
- }
8200
- ), /* @__PURE__ */ import_react47.default.createElement(
8201
- NumberField,
9000
+ return /* @__PURE__ */ import_react51.default.createElement(
9001
+ SettingsSection,
8202
9002
  {
8203
- label: speak.filters.code.maxLines(),
8204
- min: 0,
8205
- max: 100,
8206
- value: settings.outputCodeMaxLines ?? 5,
8207
- disabled: !enabled,
8208
- onCommit: (raw) => {
8209
- const value = Number(raw);
8210
- if (Number.isInteger(value) && value >= 0 && value <= 100)
8211
- updateSettings({ outputCodeMaxLines: value });
9003
+ title: speak.filters.code.enabled(),
9004
+ icon: "filter",
9005
+ defaultOpen: false
9006
+ },
9007
+ /* @__PURE__ */ import_react51.default.createElement(
9008
+ CheckboxField,
9009
+ {
9010
+ label: speak.filters.code.enabled(),
9011
+ checked: enabled,
9012
+ onChange: (event) => updateSettings({ outputCodeFilterEnabled: event.target.checked })
8212
9013
  }
8213
- }
8214
- ), /* @__PURE__ */ import_react47.default.createElement(
8215
- TextField,
8216
- {
8217
- label: speak.filters.code.replacement(),
8218
- value: settings.outputCodeNotice ?? speak.filters.code.notice(),
8219
- disabled: !enabled,
8220
- onCommit: (value) => updateSettings({ outputCodeNotice: value })
8221
- }
8222
- ));
9014
+ ),
9015
+ /* @__PURE__ */ import_react51.default.createElement(
9016
+ NumberField,
9017
+ {
9018
+ label: speak.filters.code.maxLines(),
9019
+ min: 0,
9020
+ max: 100,
9021
+ value: settings.outputCodeMaxLines ?? 5,
9022
+ disabled: !enabled,
9023
+ onCommit: (raw) => {
9024
+ const value = Number(raw);
9025
+ if (Number.isInteger(value) && value >= 0 && value <= 100)
9026
+ updateSettings({ outputCodeMaxLines: value });
9027
+ }
9028
+ }
9029
+ ),
9030
+ /* @__PURE__ */ import_react51.default.createElement(
9031
+ TextField,
9032
+ {
9033
+ label: speak.filters.code.replacement(),
9034
+ value: settings.outputCodeNotice ?? speak.filters.code.notice(),
9035
+ disabled: !enabled,
9036
+ onCommit: (value) => updateSettings({ outputCodeNotice: value })
9037
+ }
9038
+ )
9039
+ );
8223
9040
  }
8224
9041
 
8225
9042
  // src/modules/settings/sections/speak/SpeakSettingsSection.tsx
8226
- var import_react50 = __toESM(require("react"), 1);
9043
+ var import_react54 = __toESM(require("react"), 1);
8227
9044
 
8228
9045
  // src/modules/settings/sections/speak/SpeechEngineSettings.tsx
8229
- var import_react48 = __toESM(require("react"), 1);
8230
- function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
9046
+ var import_react52 = __toESM(require("react"), 1);
9047
+
9048
+ // src/modules/settings/models/browserVoiceSelection.ts
9049
+ function baseLanguage(language) {
9050
+ return (language ?? "").trim().replace(/_/g, "-").split("-")[0].toLowerCase();
9051
+ }
9052
+ function isNaturalVoice(voice) {
9053
+ return /\bnatural\b/i.test(voice.name);
9054
+ }
9055
+ function rankBrowserVoices(voices, browserLanguage) {
9056
+ const language = baseLanguage(browserLanguage);
9057
+ const matches = (voice) => Boolean(language) && baseLanguage(voice.lang) === language;
9058
+ return [...voices].sort(
9059
+ (a, b) => Number(isNaturalVoice(b)) - Number(isNaturalVoice(a)) || Number(matches(b)) - Number(matches(a)) || a.name.localeCompare(b.name)
9060
+ );
9061
+ }
9062
+ function groupBrowserVoices(voices, browserLanguage) {
9063
+ const groups = /* @__PURE__ */ new Map();
9064
+ for (const voice of rankBrowserVoices(voices, browserLanguage)) {
9065
+ const language = baseLanguage(voice.lang);
9066
+ const group = groups.get(language) ?? [];
9067
+ group.push(voice);
9068
+ groups.set(language, group);
9069
+ }
9070
+ const preferredLanguage = baseLanguage(browserLanguage);
9071
+ return [...groups].map(([language, voices2]) => ({ language, voices: voices2 })).sort(
9072
+ (a, b) => Number(Boolean(preferredLanguage) && b.language === preferredLanguage) - Number(Boolean(preferredLanguage) && a.language === preferredLanguage) || Number(!a.language) - Number(!b.language) || a.language.localeCompare(b.language)
9073
+ );
9074
+ }
9075
+ function preferredBrowserVoice(voices, browserLanguage) {
9076
+ const voice = rankBrowserVoices(voices, browserLanguage).find(isNaturalVoice);
9077
+ return voice ? voice.voiceURI || voice.name : void 0;
9078
+ }
9079
+ function speechEngineSelectionPatch(currentEngine, nextEngine, voices, browserLanguage) {
9080
+ const voice = nextEngine === "browser" && currentEngine !== "browser" ? preferredBrowserVoice(voices, browserLanguage) : void 0;
9081
+ return { engine: nextEngine, ...voice === void 0 ? {} : { voice } };
9082
+ }
9083
+
9084
+ // src/modules/settings/sections/speak/SpeechEngineSettings.tsx
9085
+ function SpeechEngineSettings({
9086
+ settings,
9087
+ capabilities,
9088
+ updateSettings,
9089
+ hostPlatform
9090
+ }) {
9091
+ const locale = typeof window === "undefined" ? "" : window.navigator.language;
8231
9092
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8232
9093
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
8233
9094
  const engine = settings.engine || "browser";
8234
9095
  const browserVoices = capabilities?.browser?.voices || [];
8235
- return /* @__PURE__ */ import_react48.default.createElement(import_react48.default.Fragment, null, /* @__PURE__ */ import_react48.default.createElement(
9096
+ const capability = capabilities?.[engine];
9097
+ const defaultBrowserVoice = preferredBrowserVoice(browserVoices, locale);
9098
+ const sortedBrowserOptions = groupBrowserVoices(browserVoices, locale).map((group) => ({
9099
+ label: group.language || commons.unknownLanguage(),
9100
+ options: group.voices.map((voice) => ({
9101
+ value: voice.voiceURI || voice.name,
9102
+ label: (voice.voiceURI || voice.name) === defaultBrowserVoice ? voice.name + " (" + speak.browser.defaultVoice() + ")" : voice.name
9103
+ }))
9104
+ }));
9105
+ return /* @__PURE__ */ import_react52.default.createElement(import_react52.default.Fragment, null, /* @__PURE__ */ import_react52.default.createElement(
8236
9106
  SelectField,
8237
9107
  {
8238
9108
  label: speak.engine.label(),
8239
9109
  value: engine,
8240
- options: [
8241
- {
8242
- value: "qwen-http",
8243
- label: speak.qwen.label(),
8244
- disabled: capabilities?.["qwen-http"]?.supported === false
8245
- },
8246
- {
8247
- value: "say",
8248
- label: speak.macos.label(),
8249
- disabled: capabilities?.say?.supported === false
8250
- },
8251
- {
8252
- value: "browser",
8253
- label: speak.browser.label(),
8254
- disabled: capabilities?.browser?.supported === false
8255
- }
8256
- ],
8257
- onChange: (event) => updateSettings({ engine: event.target.value })
9110
+ options: speakingEngineOrder(hostPlatform).map((value) => ({
9111
+ value,
9112
+ label: {
9113
+ say: speak.macos.label(),
9114
+ browser: speak.browser.label(),
9115
+ "qwen-http": speak.qwen.label()
9116
+ }[value]
9117
+ })),
9118
+ onChange: (event) => {
9119
+ updateSettings(
9120
+ speechEngineSelectionPatch(engine, event.target.value, browserVoices, locale)
9121
+ );
9122
+ }
8258
9123
  }
8259
- ), engine === "browser" && /* @__PURE__ */ import_react48.default.createElement(
9124
+ ), engine === "browser" && /* @__PURE__ */ import_react52.default.createElement(
8260
9125
  SelectField,
8261
9126
  {
8262
9127
  label: speak.browser.voice(),
8263
9128
  value: settings.voice || "",
8264
9129
  options: [
8265
9130
  { value: "", label: commons.systemDefault() },
8266
- ...browserVoices.map((voice) => ({
8267
- value: voice.voiceURI || voice.name,
8268
- label: voice.name
8269
- }))
9131
+ ...sortedBrowserOptions
8270
9132
  ],
8271
9133
  onChange: (event) => updateSettings({ voice: event.target.value })
8272
9134
  }
8273
- ), engine === "say" && /* @__PURE__ */ import_react48.default.createElement(import_react48.default.Fragment, null, /* @__PURE__ */ import_react48.default.createElement(
9135
+ ), engine === "say" && /* @__PURE__ */ import_react52.default.createElement(import_react52.default.Fragment, null, /* @__PURE__ */ import_react52.default.createElement(
8274
9136
  TextField,
8275
9137
  {
8276
9138
  label: speak.macos.voice(),
@@ -8278,7 +9140,7 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
8278
9140
  placeholder: commons.systemDefault(),
8279
9141
  onCommit: (value) => updateSettings({ voice: value.trim() })
8280
9142
  }
8281
- ), /* @__PURE__ */ import_react48.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.voiceHelp())), engine === "qwen-http" && /* @__PURE__ */ import_react48.default.createElement(
9143
+ ), /* @__PURE__ */ import_react52.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.voiceHelp())), engine === "qwen-http" && /* @__PURE__ */ import_react52.default.createElement(
8282
9144
  SelectField,
8283
9145
  {
8284
9146
  label: speak.qwen.voice(),
@@ -8286,31 +9148,42 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
8286
9148
  options: qwenVoices,
8287
9149
  onChange: (event) => updateSettings({ voice: event.target.value })
8288
9150
  }
8289
- ), /* @__PURE__ */ import_react48.default.createElement(
9151
+ ), /* @__PURE__ */ import_react52.default.createElement("label", null, speak.volume.label(), " (", Math.round((settings.volume ?? 1) * 100), "%)", /* @__PURE__ */ import_react52.default.createElement(
9152
+ "input",
9153
+ {
9154
+ type: "range",
9155
+ min: 0,
9156
+ max: 1,
9157
+ step: 0.01,
9158
+ value: settings.volume ?? 1,
9159
+ "aria-label": speak.volume.label(),
9160
+ onInput: (event) => updateSettings({ volume: Number(event.currentTarget.value) })
9161
+ }
9162
+ )), /* @__PURE__ */ import_react52.default.createElement("p", { className: "dlv-setting-description" }, speak.volume.help()), /* @__PURE__ */ import_react52.default.createElement("div", { className: "dlv-field-pair" }, /* @__PURE__ */ import_react52.default.createElement(
8290
9163
  NumberField,
8291
9164
  {
8292
- label: speak.rate.label(),
9165
+ label: speak.rate.label() + " (\xD7)",
8293
9166
  min: 0.1,
8294
9167
  max: 3,
8295
9168
  step: 0.1,
8296
9169
  value: settings.rate ?? 1,
8297
9170
  onCommit: (value) => updateSettings({ rate: Number(value) })
8298
9171
  }
8299
- ), /* @__PURE__ */ import_react48.default.createElement(
9172
+ ), /* @__PURE__ */ import_react52.default.createElement(
8300
9173
  NumberField,
8301
9174
  {
8302
- label: speak.segmentGap.label(),
9175
+ label: speak.segmentGap.label() + " (ms)",
8303
9176
  min: 0,
8304
9177
  max: 2e3,
8305
9178
  step: 50,
8306
9179
  value: settings.segmentGapMs ?? 400,
8307
9180
  onCommit: (value) => updateSettings({ segmentGapMs: Number(value) })
8308
9181
  }
8309
- ), capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.supported === false && /* @__PURE__ */ import_react48.default.createElement("p", { role: "status" }, capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.reason || commons.engine.failure()));
9182
+ )), capability?.supported === false && /* @__PURE__ */ import_react52.default.createElement("p", { role: "status" }, capability.reason || commons.engine.failure()));
8310
9183
  }
8311
9184
 
8312
9185
  // src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx
8313
- var import_react49 = __toESM(require("react"), 1);
9186
+ var import_react53 = __toESM(require("react"), 1);
8314
9187
  function SpeechAdvancedSettings({
8315
9188
  controller,
8316
9189
  settings,
@@ -8328,7 +9201,7 @@ function SpeechAdvancedSettings({
8328
9201
  commons.systemDefault(),
8329
9202
  speak.output.fallbackName()
8330
9203
  );
8331
- return /* @__PURE__ */ import_react49.default.createElement(import_react49.default.Fragment, null, settings.engine !== "say" ? /* @__PURE__ */ import_react49.default.createElement(
9204
+ return /* @__PURE__ */ import_react53.default.createElement(import_react53.default.Fragment, null, settings.engine !== "say" ? /* @__PURE__ */ import_react53.default.createElement(
8332
9205
  SelectField,
8333
9206
  {
8334
9207
  label: speak.output.device(),
@@ -8336,14 +9209,14 @@ function SpeechAdvancedSettings({
8336
9209
  options: outputOptions,
8337
9210
  onChange: (event) => updateSettings({ outputDeviceId: event.target.value })
8338
9211
  }
8339
- ) : /* @__PURE__ */ import_react49.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.outputHelp()), settings.engine === "qwen-http" && /* @__PURE__ */ import_react49.default.createElement(SettingsSubcard, { title: speak.qwen.connection(), icon: "link" }, /* @__PURE__ */ import_react49.default.createElement(QwenSettings, { controller })), /* @__PURE__ */ import_react49.default.createElement(SettingsSubcard, { title: speak.agentContext.label(), icon: "settings" }, /* @__PURE__ */ import_react49.default.createElement(
9212
+ ) : /* @__PURE__ */ import_react53.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.outputHelp()), settings.engine === "qwen-http" && /* @__PURE__ */ import_react53.default.createElement(SettingsSubcard, { title: speak.qwen.connection(), icon: "link" }, /* @__PURE__ */ import_react53.default.createElement(QwenSettings, { controller })), /* @__PURE__ */ import_react53.default.createElement(SettingsSubcard, { title: speak.agentContext.label(), icon: "settings" }, /* @__PURE__ */ import_react53.default.createElement(
8340
9213
  CheckboxField,
8341
9214
  {
8342
9215
  label: speak.agentContext.enabled(),
8343
9216
  checked: settings.agentVoiceContextEnabled !== false,
8344
9217
  onChange: (event) => updateSettings({ agentVoiceContextEnabled: event.target.checked })
8345
9218
  }
8346
- ), /* @__PURE__ */ import_react49.default.createElement(
9219
+ ), /* @__PURE__ */ import_react53.default.createElement(
8347
9220
  TextAreaField,
8348
9221
  {
8349
9222
  label: speak.agentContext.label(),
@@ -8351,30 +9224,30 @@ function SpeechAdvancedSettings({
8351
9224
  disabled: settings.agentVoiceContextEnabled === false,
8352
9225
  onCommit: (value) => updateSettings({ agentVoiceContext: value })
8353
9226
  }
8354
- ), /* @__PURE__ */ import_react49.default.createElement(
9227
+ ), /* @__PURE__ */ import_react53.default.createElement(
8355
9228
  "button",
8356
9229
  {
8357
9230
  type: "button",
8358
9231
  onClick: () => updateSettings({ agentVoiceContext: defaultAgentVoiceContext })
8359
9232
  },
8360
9233
  speak.agentContext.restore()
8361
- )), /* @__PURE__ */ import_react49.default.createElement("p", null, speak.engine.playbackHelp()), /* @__PURE__ */ import_react49.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react49.default.createElement("button", { type: "button", onClick: () => invoke("speak", speak.output.testPhrase()) }, speak.output.test()), speaking && /* @__PURE__ */ import_react49.default.createElement("button", { type: "button", onClick: () => invoke("stopSpeech") }, speak.output.stopTest()), /* @__PURE__ */ import_react49.default.createElement("button", { type: "button", onClick: () => invoke("refreshCapabilities") }, settingsLanguage.engine.refresh())));
9234
+ )), /* @__PURE__ */ import_react53.default.createElement("p", null, speak.engine.playbackHelp()), /* @__PURE__ */ import_react53.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react53.default.createElement("button", { type: "button", onClick: () => invoke("speak", speak.output.testPhrase()) }, speak.output.test()), speaking && /* @__PURE__ */ import_react53.default.createElement("button", { type: "button", onClick: () => invoke("stopSpeech") }, speak.output.stopTest()), /* @__PURE__ */ import_react53.default.createElement("button", { type: "button", onClick: () => invoke("refreshCapabilities", { localOnly: true }) }, settingsLanguage.engine.refresh())));
8362
9235
  }
8363
9236
 
8364
9237
  // src/modules/settings/sections/speak/SpeakSettingsSection.tsx
8365
9238
  function SpeakSettingsSection(props) {
8366
9239
  const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
8367
- return /* @__PURE__ */ import_react50.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react50.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react50.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "speaker" }, /* @__PURE__ */ import_react50.default.createElement(SpeechEngineSettings, { ...props }), /* @__PURE__ */ import_react50.default.createElement(SpeechAdvancedSettings, { ...props })), /* @__PURE__ */ import_react50.default.createElement(OutputFilterSettings, { ...props })));
9240
+ return /* @__PURE__ */ import_react54.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react54.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react54.default.createElement(SettingsSection, { title: settingsLanguage.layout.voice(), icon: "speaker" }, /* @__PURE__ */ import_react54.default.createElement(SpeechEngineSettings, { ...props })), /* @__PURE__ */ import_react54.default.createElement(SettingsSection, { title: settingsLanguage.layout.playback(), icon: "play" }, /* @__PURE__ */ import_react54.default.createElement(SpeechAdvancedSettings, { ...props })), /* @__PURE__ */ import_react54.default.createElement(OutputFilterSettings, { ...props })));
8368
9241
  }
8369
9242
 
8370
9243
  // src/modules/settings/components/SettingsHeader.tsx
8371
- var import_react53 = __toESM(require("react"), 1);
9244
+ var import_react57 = __toESM(require("react"), 1);
8372
9245
 
8373
9246
  // src/modules/settings/components/VersionBadges.tsx
8374
- var import_react51 = __toESM(require("react"), 1);
9247
+ var import_react55 = __toESM(require("react"), 1);
8375
9248
  function VersionBadges() {
8376
9249
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8377
- return /* @__PURE__ */ import_react51.default.createElement("div", { className: "dlv-version-badges", "aria-label": commons.version.title() }, /* @__PURE__ */ import_react51.default.createElement(
9250
+ return /* @__PURE__ */ import_react55.default.createElement("div", { className: "dlv-version-badges", "aria-label": commons.version.title() }, /* @__PURE__ */ import_react55.default.createElement(
8378
9251
  "a",
8379
9252
  {
8380
9253
  className: "dlv-shields-badge",
@@ -8384,9 +9257,9 @@ function VersionBadges() {
8384
9257
  "aria-label": commons.version.link({ version: CURRENT_VERSION }),
8385
9258
  title: commons.version.label({ version: CURRENT_VERSION })
8386
9259
  },
8387
- /* @__PURE__ */ import_react51.default.createElement("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
8388
- /* @__PURE__ */ import_react51.default.createElement("span", null, `v${CURRENT_VERSION}`)
8389
- ), /* @__PURE__ */ import_react51.default.createElement(
9260
+ /* @__PURE__ */ import_react55.default.createElement("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
9261
+ /* @__PURE__ */ import_react55.default.createElement("span", null, `v${CURRENT_VERSION}`)
9262
+ ), /* @__PURE__ */ import_react55.default.createElement(
8390
9263
  "a",
8391
9264
  {
8392
9265
  className: "dlv-shields-badge",
@@ -8396,32 +9269,32 @@ function VersionBadges() {
8396
9269
  "aria-label": commons.version.compatibilityLink({ version: TESTED_DSH_VERSION }),
8397
9270
  title: commons.version.compatibility({ version: TESTED_DSH_VERSION })
8398
9271
  },
8399
- /* @__PURE__ */ import_react51.default.createElement("img", { src: DSH_BADGE_URL, alt: "" }),
8400
- /* @__PURE__ */ import_react51.default.createElement("span", null, `v${TESTED_DSH_VERSION}`)
9272
+ /* @__PURE__ */ import_react55.default.createElement("img", { src: DSH_BADGE_URL, alt: "" }),
9273
+ /* @__PURE__ */ import_react55.default.createElement("span", null, `v${TESTED_DSH_VERSION}`)
8401
9274
  ));
8402
9275
  }
8403
9276
 
8404
9277
  // src/modules/settings/components/PluginUpdateAction.tsx
8405
- var import_react52 = __toESM(require("react"), 1);
9278
+ var import_react56 = __toESM(require("react"), 1);
8406
9279
  function PluginUpdateAction({ updater, tag }) {
8407
9280
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8408
9281
  const text = commons.update;
8409
- const state = import_react52.default.useSyncExternalStore(
9282
+ const state = import_react56.default.useSyncExternalStore(
8410
9283
  updater.subscribe,
8411
9284
  updater.getSnapshot,
8412
9285
  updater.getSnapshot
8413
9286
  );
8414
- const [open, setOpen] = import_react52.default.useState(false);
8415
- const panelId = import_react52.default.useId();
8416
- const trigger = import_react52.default.useRef(null);
8417
- const confirmButton = import_react52.default.useRef(null);
8418
- import_react52.default.useEffect(() => {
9287
+ const [open, setOpen] = import_react56.default.useState(false);
9288
+ const panelId = import_react56.default.useId();
9289
+ const trigger = import_react56.default.useRef(null);
9290
+ const confirmButton = import_react56.default.useRef(null);
9291
+ import_react56.default.useEffect(() => {
8419
9292
  if (state.phase === "confirm") confirmButton.current?.focus();
8420
9293
  }, [state.phase]);
8421
9294
  const busy = state.phase === "checking" || state.phase === "installing";
8422
9295
  const settled = ["restart", "done", "uncertain"].includes(state.phase);
8423
9296
  const expanded = open || busy || settled || state.phase === "confirm";
8424
- return /* @__PURE__ */ import_react52.default.createElement("span", { className: "dlv-plugin-update" }, /* @__PURE__ */ import_react52.default.createElement(
9297
+ return /* @__PURE__ */ import_react56.default.createElement("span", { className: "dlv-plugin-update" }, /* @__PURE__ */ import_react56.default.createElement(
8425
9298
  "button",
8426
9299
  {
8427
9300
  type: "button",
@@ -8435,9 +9308,9 @@ function PluginUpdateAction({ updater, tag }) {
8435
9308
  void updater.prepare(tag);
8436
9309
  }
8437
9310
  },
8438
- /* @__PURE__ */ import_react52.default.createElement("span", { "aria-hidden": "true" }, "\u21BB "),
9311
+ /* @__PURE__ */ import_react56.default.createElement("span", { "aria-hidden": "true" }, "\u21BB "),
8439
9312
  busy ? text.installing() : text.now()
8440
- ), expanded && /* @__PURE__ */ import_react52.default.createElement("span", { id: panelId, className: "dlv-plugin-update-panel" }, state.phase === "confirm" ? /* @__PURE__ */ import_react52.default.createElement(import_react52.default.Fragment, null, /* @__PURE__ */ import_react52.default.createElement("span", { id: panelId + "-confirmation" }, text.confirm({ version: state.version })), /* @__PURE__ */ import_react52.default.createElement(
9313
+ ), expanded && /* @__PURE__ */ import_react56.default.createElement("span", { id: panelId, className: "dlv-plugin-update-panel" }, state.phase === "confirm" ? /* @__PURE__ */ import_react56.default.createElement(import_react56.default.Fragment, null, /* @__PURE__ */ import_react56.default.createElement("span", { id: panelId + "-confirmation" }, text.confirm({ version: state.version })), /* @__PURE__ */ import_react56.default.createElement(
8441
9314
  "button",
8442
9315
  {
8443
9316
  ref: confirmButton,
@@ -8446,7 +9319,7 @@ function PluginUpdateAction({ updater, tag }) {
8446
9319
  onClick: () => void updater.confirm()
8447
9320
  },
8448
9321
  text.now()
8449
- ), /* @__PURE__ */ import_react52.default.createElement(
9322
+ ), /* @__PURE__ */ import_react56.default.createElement(
8450
9323
  "button",
8451
9324
  {
8452
9325
  type: "button",
@@ -8457,13 +9330,13 @@ function PluginUpdateAction({ updater, tag }) {
8457
9330
  }
8458
9331
  },
8459
9332
  text.cancel()
8460
- )) : /* @__PURE__ */ import_react52.default.createElement(
9333
+ )) : /* @__PURE__ */ import_react56.default.createElement(
8461
9334
  "span",
8462
9335
  {
8463
9336
  role: state.phase === "failed" || state.phase === "uncertain" ? "alert" : "status"
8464
9337
  },
8465
9338
  state.phase === "restart" ? text.restart({ version: state.version }) : state.phase === "done" ? text.done({ version: state.version }) : state.problem ? text[state.problem]() : busy ? text.installing() : null
8466
- ), state.phase === "failed" && /* @__PURE__ */ import_react52.default.createElement("button", { type: "button", onClick: () => setOpen(false) }, text.cancel())));
9339
+ ), state.phase === "failed" && /* @__PURE__ */ import_react56.default.createElement("button", { type: "button", onClick: () => setOpen(false) }, text.cancel())));
8467
9340
  }
8468
9341
 
8469
9342
  // src/modules/settings/components/SettingsHeader.tsx
@@ -8474,7 +9347,7 @@ function SettingsHeader({
8474
9347
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8475
9348
  const { scoped: settings } = useLanguage((ctx) => ctx.settings);
8476
9349
  const { latestRelease, updateAvailable } = useReleaseStatus();
8477
- return /* @__PURE__ */ import_react53.default.createElement("div", { className: "dlv-settings-heading" }, /* @__PURE__ */ import_react53.default.createElement("h3", null, commons.pluginName()), /* @__PURE__ */ import_react53.default.createElement(VersionBadges, null), /* @__PURE__ */ import_react53.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true }), updateAvailable && latestRelease ? /* @__PURE__ */ import_react53.default.createElement(import_react53.default.Fragment, null, pluginUpdate && /* @__PURE__ */ import_react53.default.createElement(PluginUpdateAction, { updater: pluginUpdate, tag: latestRelease.tag }), /* @__PURE__ */ import_react53.default.createElement(
9350
+ return /* @__PURE__ */ import_react57.default.createElement("div", { className: "dlv-settings-heading" }, /* @__PURE__ */ import_react57.default.createElement("h3", null, commons.pluginName()), /* @__PURE__ */ import_react57.default.createElement(VersionBadges, null), /* @__PURE__ */ import_react57.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true }), updateAvailable && latestRelease ? /* @__PURE__ */ import_react57.default.createElement(import_react57.default.Fragment, null, pluginUpdate && /* @__PURE__ */ import_react57.default.createElement(PluginUpdateAction, { updater: pluginUpdate, tag: latestRelease.tag }), /* @__PURE__ */ import_react57.default.createElement(
8478
9351
  "a",
8479
9352
  {
8480
9353
  className: "dlv-version-badge dlv-update-badge",
@@ -8484,9 +9357,9 @@ function SettingsHeader({
8484
9357
  "aria-label": commons.update.link({ version: latestRelease.tag }),
8485
9358
  title: commons.update.version({ version: latestRelease.tag })
8486
9359
  },
8487
- /* @__PURE__ */ import_react53.default.createElement("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
9360
+ /* @__PURE__ */ import_react57.default.createElement("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
8488
9361
  commons.update.label()
8489
- ), /* @__PURE__ */ import_react53.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true })) : null, /* @__PURE__ */ import_react53.default.createElement(
9362
+ ), /* @__PURE__ */ import_react57.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true })) : null, /* @__PURE__ */ import_react57.default.createElement(
8490
9363
  "a",
8491
9364
  {
8492
9365
  className: "dlv-version-badge dlv-star-badge",
@@ -8498,7 +9371,7 @@ function SettingsHeader({
8498
9371
  },
8499
9372
  "\u2605 ",
8500
9373
  commons.repository.starLabel()
8501
- ), onClose ? /* @__PURE__ */ import_react53.default.createElement(IconButton, { icon: "close", label: settings.close(), onClick: onClose }) : null);
9374
+ ), onClose ? /* @__PURE__ */ import_react57.default.createElement(IconButton, { icon: "close", label: settings.close(), onClick: onClose }) : null);
8502
9375
  }
8503
9376
 
8504
9377
  // src/modules/core/developerExtension.ts
@@ -8523,15 +9396,40 @@ function LiveVoiceSettings({
8523
9396
  const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
8524
9397
  const state = useConversationController(controller);
8525
9398
  const { invoke, error, clearError } = useConversationActions(controller);
8526
- const [activeTab, setActiveTab] = import_react54.default.useState("conversation");
8527
- const tabsId = import_react54.default.useId();
8528
- const developer = import_react54.default.useSyncExternalStore(
9399
+ const [activeTab, setActiveTab] = import_react58.default.useState("conversation");
9400
+ const tabsId = import_react58.default.useId();
9401
+ const container = import_react58.default.useRef(null);
9402
+ import_react58.default.useLayoutEffect(() => {
9403
+ const element = container.current;
9404
+ if (!element) return;
9405
+ let viewport = element.parentElement;
9406
+ while (viewport && !/(auto|scroll)/.test(window.getComputedStyle(viewport).overflowY))
9407
+ viewport = viewport.parentElement;
9408
+ if (!viewport) return;
9409
+ const host = viewport;
9410
+ const resize = () => {
9411
+ const style = window.getComputedStyle(host);
9412
+ element.style.height = Math.max(
9413
+ 0,
9414
+ host.clientHeight - parseFloat(style.paddingTop || "0") - parseFloat(style.paddingBottom || "0")
9415
+ ) + "px";
9416
+ };
9417
+ resize();
9418
+ const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;
9419
+ observer?.observe(host);
9420
+ window.addEventListener("resize", resize);
9421
+ return () => {
9422
+ observer?.disconnect();
9423
+ window.removeEventListener("resize", resize);
9424
+ };
9425
+ }, []);
9426
+ const developer = import_react58.default.useSyncExternalStore(
8529
9427
  subscribeDeveloperExtension,
8530
9428
  readDeveloperExtension,
8531
9429
  () => null
8532
9430
  );
8533
9431
  const Developer = developer?.component;
8534
- const selectedTab = activeTab === "developer" && !developer ? "conversation" : activeTab;
9432
+ const selectedTab = activeTab;
8535
9433
  const tabs = [
8536
9434
  { id: "speech", label: settingsLanguage.tabs.speak(), icon: "speaker" },
8537
9435
  {
@@ -8542,50 +9440,62 @@ function LiveVoiceSettings({
8542
9440
  {
8543
9441
  id: "conversation",
8544
9442
  label: settingsLanguage.tabs.conversation(),
8545
- icon: "send"
9443
+ icon: "conversation"
8546
9444
  }
8547
9445
  ];
8548
- if (developer) tabs.push({ id: "developer", label: developer.label(), icon: "send" });
8549
9446
  const sectionProps = {
8550
9447
  controller,
8551
9448
  settings: state.settings,
8552
9449
  capabilities: state.capabilities,
9450
+ hostPlatform: state.hostPlatform,
8553
9451
  speaking: state.speaking,
8554
9452
  invoke,
8555
9453
  updateSettings: (next) => invoke("updateSettings", next)
8556
9454
  };
8557
- return /* @__PURE__ */ import_react54.default.createElement("section", { className: "dlv-settings", "aria-label": settingsLanguage.title() }, /* @__PURE__ */ import_react54.default.createElement(SettingsHeader, { onClose, pluginUpdate }), /* @__PURE__ */ import_react54.default.createElement(
8558
- SettingsTabs,
8559
- {
8560
- label: settingsLanguage.title(),
8561
- tabs,
8562
- active: selectedTab,
8563
- idPrefix: tabsId,
8564
- onChange: setActiveTab
8565
- }
8566
- ), tabs.map((tab) => /* @__PURE__ */ import_react54.default.createElement(
8567
- "div",
9455
+ return /* @__PURE__ */ import_react58.default.createElement(
9456
+ "section",
8568
9457
  {
8569
- key: tab.id,
8570
- id: `${tabsId}-panel-${tab.id}`,
8571
- className: "dlv-settings-panel",
8572
- role: "tabpanel",
8573
- "aria-labelledby": `${tabsId}-tab-${tab.id}`,
8574
- hidden: selectedTab !== tab.id
9458
+ ref: container,
9459
+ className: "dlv-settings",
9460
+ "aria-label": settingsLanguage.title()
8575
9461
  },
8576
- tab.id === "developer" && Developer ? /* @__PURE__ */ import_react54.default.createElement(Developer, null) : tab.id === "speech" ? /* @__PURE__ */ import_react54.default.createElement(SpeakSettingsSection, { ...sectionProps }) : tab.id === "recognition" ? /* @__PURE__ */ import_react54.default.createElement(RecognitionSettingsSection, { ...sectionProps }) : /* @__PURE__ */ import_react54.default.createElement(ConversationSettingsSection, { ...sectionProps })
8577
- )), /* @__PURE__ */ import_react54.default.createElement(
8578
- ErrorMessage,
8579
- {
8580
- error: error || state.error,
8581
- dismissLabel: commons.dismissError(),
8582
- dismissText: commons.dismiss(),
8583
- onDismiss: () => {
8584
- clearError();
8585
- controller.clearError();
9462
+ /* @__PURE__ */ import_react58.default.createElement(SettingsHeader, { onClose, pluginUpdate }),
9463
+ /* @__PURE__ */ import_react58.default.createElement(
9464
+ SettingsTabs,
9465
+ {
9466
+ label: settingsLanguage.title(),
9467
+ tabs,
9468
+ active: selectedTab,
9469
+ idPrefix: tabsId,
9470
+ onChange: setActiveTab
8586
9471
  }
8587
- }
8588
- ));
9472
+ ),
9473
+ /* @__PURE__ */ import_react58.default.createElement("div", { className: "dlv-settings-scroll" }, tabs.map((tab) => /* @__PURE__ */ import_react58.default.createElement(
9474
+ "div",
9475
+ {
9476
+ key: tab.id,
9477
+ id: `${tabsId}-panel-${tab.id}`,
9478
+ className: "dlv-settings-panel",
9479
+ role: "tabpanel",
9480
+ "aria-labelledby": `${tabsId}-tab-${tab.id}`,
9481
+ hidden: selectedTab !== tab.id
9482
+ },
9483
+ tab.id === "speech" ? /* @__PURE__ */ import_react58.default.createElement(SpeakSettingsSection, { ...sectionProps }) : tab.id === "recognition" ? /* @__PURE__ */ import_react58.default.createElement(RecognitionSettingsSection, { ...sectionProps }) : /* @__PURE__ */ import_react58.default.createElement(ConversationSettingsSection, { ...sectionProps })
9484
+ ))),
9485
+ Developer && /* @__PURE__ */ import_react58.default.createElement("footer", { className: "dlv-settings-footer" }, /* @__PURE__ */ import_react58.default.createElement(Developer, null)),
9486
+ /* @__PURE__ */ import_react58.default.createElement(
9487
+ ErrorMessage,
9488
+ {
9489
+ error: error || state.error,
9490
+ dismissLabel: commons.dismissError(),
9491
+ dismissText: commons.dismiss(),
9492
+ onDismiss: () => {
9493
+ clearError();
9494
+ controller.clearError();
9495
+ }
9496
+ }
9497
+ )
9498
+ );
8589
9499
  }
8590
9500
 
8591
9501
  // src/modules/settings/components/createLiveVoiceSettings.tsx
@@ -8595,7 +9505,7 @@ function createLiveVoiceSettings(..._legacyArguments) {
8595
9505
  onClose,
8596
9506
  pluginUpdate
8597
9507
  }) {
8598
- return /* @__PURE__ */ import_react55.default.createElement(LiveVoiceSettings, { controller, onClose, pluginUpdate });
9508
+ return /* @__PURE__ */ import_react59.default.createElement(LiveVoiceSettings, { controller, onClose, pluginUpdate });
8599
9509
  };
8600
9510
  }
8601
9511
 
@@ -8603,6 +9513,7 @@ function createLiveVoiceSettings(..._legacyArguments) {
8603
9513
  var SETTINGS_PATH = "/api/dsh-live-voice/settings";
8604
9514
  function createSettingsClient(fetchImpl = globalThis.fetch) {
8605
9515
  let settings = normalizeSettings({});
9516
+ let hostPlatform;
8606
9517
  let queue = Promise.resolve();
8607
9518
  const pendingPatches = [];
8608
9519
  const listeners = /* @__PURE__ */ new Set();
@@ -8615,6 +9526,7 @@ function createSettingsClient(fetchImpl = globalThis.fetch) {
8615
9526
  });
8616
9527
  const body = await response.json();
8617
9528
  if (!response.ok || body?.ok !== true) throw new Error("settings-request-failed");
9529
+ hostPlatform = typeof body.hostPlatform === "string" ? body.hostPlatform : void 0;
8618
9530
  if (method === "PUT") pendingPatches.shift();
8619
9531
  settings = normalizeSettings(Object.assign({}, body.value, ...pendingPatches));
8620
9532
  for (const listener of listeners) listener(settings);
@@ -8626,6 +9538,7 @@ function createSettingsClient(fetchImpl = globalThis.fetch) {
8626
9538
  return {
8627
9539
  ready,
8628
9540
  getSnapshot: () => settings,
9541
+ getHostPlatform: () => hostPlatform,
8629
9542
  subscribe(listener) {
8630
9543
  listeners.add(listener);
8631
9544
  return () => {
@@ -8655,13 +9568,13 @@ function createSettingsClient(fetchImpl = globalThis.fetch) {
8655
9568
  }
8656
9569
 
8657
9570
  // src/modules/conversation/components/ConversationControls.tsx
8658
- var import_react58 = __toESM(require("react"), 1);
9571
+ var import_react62 = __toESM(require("react"), 1);
8659
9572
 
8660
9573
  // src/modules/conversation/components/MicrophoneButton.tsx
8661
- var import_react56 = __toESM(require("react"), 1);
9574
+ var import_react60 = __toESM(require("react"), 1);
8662
9575
  function MicrophoneButton(props) {
8663
9576
  const active = Boolean(props["aria-pressed"]);
8664
- return /* @__PURE__ */ import_react56.default.createElement(
9577
+ return /* @__PURE__ */ import_react60.default.createElement(
8665
9578
  IconButton,
8666
9579
  {
8667
9580
  ...props,
@@ -8673,7 +9586,7 @@ function MicrophoneButton(props) {
8673
9586
  }
8674
9587
 
8675
9588
  // src/modules/conversation/components/AutoPlaybackToggle.tsx
8676
- var import_react57 = __toESM(require("react"), 1);
9589
+ var import_react61 = __toESM(require("react"), 1);
8677
9590
  function AutoPlaybackToggle({ state, invoke, error }) {
8678
9591
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8679
9592
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
@@ -8685,7 +9598,7 @@ function AutoPlaybackToggle({ state, invoke, error }) {
8685
9598
  }) : speak.autoPlayback.status({
8686
9599
  state: active ? commons.on() : commons.off()
8687
9600
  });
8688
- return /* @__PURE__ */ import_react57.default.createElement(
9601
+ return /* @__PURE__ */ import_react61.default.createElement(
8689
9602
  IconButton,
8690
9603
  {
8691
9604
  className: "dlv-composer-toggle",
@@ -8704,27 +9617,20 @@ function AutoPlaybackToggle({ state, invoke, error }) {
8704
9617
  // src/modules/conversation/components/ConversationControls.tsx
8705
9618
  function ConversationControls({ controller }) {
8706
9619
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
8707
- const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
8708
9620
  const state = useConversationController(controller);
8709
9621
  const { invoke, error, clearError } = useConversationActions(controller);
8710
9622
  const busy = state.conversation || state.listening || state.starting || state.recognizing;
8711
- const capability = state.capabilities?.recognition;
8712
- const capture = state.capabilities?.capture;
8713
- const pending = !capability || !capture;
8714
- const unavailable = capability?.supported === false || capture?.supported === false;
8715
- const reason = capture?.supported === false ? capture.reason : capability?.reason;
8716
- const label = busy ? commons.conversation.end() : pending ? recognition.microphone.checking() : unavailable ? reason || recognition.status.unavailable() : commons.conversation.start();
8717
- return /* @__PURE__ */ import_react58.default.createElement(import_react58.default.Fragment, null, /* @__PURE__ */ import_react58.default.createElement(AutoPlaybackToggle, { state, invoke, error }), /* @__PURE__ */ import_react58.default.createElement(
9623
+ const label = busy ? commons.conversation.end() : commons.conversation.start();
9624
+ return /* @__PURE__ */ import_react62.default.createElement(import_react62.default.Fragment, null, /* @__PURE__ */ import_react62.default.createElement(AutoPlaybackToggle, { state, invoke, error }), /* @__PURE__ */ import_react62.default.createElement(
8718
9625
  MicrophoneButton,
8719
9626
  {
8720
9627
  label,
8721
- disabled: !busy && pending,
8722
9628
  "aria-pressed": Boolean(busy),
8723
9629
  onClick: () => invoke(
8724
- busy ? state.conversation ? "stopConversationInput" : "cancelDictation" : unavailable ? "explainRecognition" : "startConversation"
9630
+ busy ? state.conversation ? "stopConversationInput" : "cancelDictation" : "startConversation"
8725
9631
  )
8726
9632
  }
8727
- ), /* @__PURE__ */ import_react58.default.createElement(
9633
+ ), /* @__PURE__ */ import_react62.default.createElement(
8728
9634
  ErrorMessage,
8729
9635
  {
8730
9636
  error,
@@ -8736,7 +9642,7 @@ function ConversationControls({ controller }) {
8736
9642
  }
8737
9643
 
8738
9644
  // src/modules/conversation/components/SpeakButton.tsx
8739
- var import_react59 = __toESM(require("react"), 1);
9645
+ var import_react63 = __toESM(require("react"), 1);
8740
9646
  function SpeakButton({
8741
9647
  active = false,
8742
9648
  disabled = false,
@@ -8745,7 +9651,7 @@ function SpeakButton({
8745
9651
  }) {
8746
9652
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
8747
9653
  const resolvedLabel = label ?? (active ? speak.playback.stop() : speak.playback.message());
8748
- return /* @__PURE__ */ import_react59.default.createElement(
9654
+ return /* @__PURE__ */ import_react63.default.createElement(
8749
9655
  IconButton,
8750
9656
  {
8751
9657
  className: "dlv-speaker",
@@ -8894,7 +9800,7 @@ var styles = `
8894
9800
  .dlv-wave{display:block;flex:1 1 180px;min-width:30px;width:0;height:40px;color:var(--dsw-alias-label-primary)}
8895
9801
  .dlv-status{flex:1 1 120px;min-width:0;font-size:13px;line-height:1.4;color:var(--dsw-alias-label-secondary)}
8896
9802
  .dlv-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;max-width:720px;margin:8px auto}
8897
- .dlv-settings{box-sizing:border-box;padding:18px;width:100%;display:grid;gap:16px;max-width:640px;color:var(--dsw-alias-label-primary)}
9803
+ .dlv-settings{box-sizing:border-box;padding:8px 2px 20px;width:100%;display:flex;flex-direction:column;gap:20px;min-width:0;max-width:820px;min-height:100%;color:var(--dsw-alias-label-primary);container-type:inline-size}
8898
9804
  .dlv-settings h3,.dlv-settings p{margin:0}
8899
9805
  .dlv-settings-heading{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}
8900
9806
  .dlv-plugin-update{display:inline-flex;flex-wrap:wrap;gap:6px;max-width:100%}.dlv-plugin-update-panel{display:flex;flex-wrap:wrap;align-items:center;gap:8px;width:100%;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;font-size:12px;overflow-wrap:anywhere}.dlv-plugin-update button{cursor:pointer;font:inherit}.dlv-plugin-update button:disabled{opacity:.6;cursor:default}.dlv-plugin-update button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-settings-heading h3{white-space:nowrap}.dlv-heading-divider{height:1px;min-width:12px;flex:1;background:var(--dsw-alias-border-l1)}.dlv-settings-heading .dlv-star-badge{flex:none}
@@ -8906,6 +9812,46 @@ var styles = `
8906
9812
  .dlv-preset-group{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dlv-settings label.dlv-preset{display:flex;align-items:flex-start;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;cursor:pointer}.dlv-settings label.dlv-preset:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.dlv-settings label.dlv-preset input{width:auto;margin-top:3px}.dlv-preset span{display:grid;gap:3px}.dlv-vad-summary{font-weight:500}
8907
9813
  .dlv-settings-actions{display:flex;flex-wrap:wrap;gap:8px}.dlv-settings-actions button{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}.dlv-settings-actions button:disabled{opacity:.45;cursor:default}
8908
9814
  @media(max-width:480px){.dlv-settings-heading{gap:6px}.dlv-heading-divider{min-width:4px}.dlv-settings-heading .dlv-star-badge{padding:3px 6px;font-size:10px}.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
9815
+
9816
+ .dlv-phrase-field{display:grid;gap:7px}.dlv-phrase-box{display:flex;flex-wrap:wrap;gap:6px;padding:9px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;min-width:0}.dlv-phrase-box:focus-within{outline:2px solid var(--dsw-alias-label-primary);outline-offset:2px}.dlv-phrase{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:3px 7px 3px 10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:14px;font-size:12px}.dlv-phrase>span{overflow-wrap:anywhere;min-width:0}.dlv-phrase button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:17px;padding:0 3px}.dlv-phrase-box input{border:0!important;outline:0!important;width:120px!important;flex:1;min-width:100px!important;padding:4px!important;background:transparent!important}.dlv-phrase-box[aria-disabled=true]{opacity:.5}
9817
+ .dlv-form-field{display:grid;gap:7px;min-width:0}.dlv-field-label{display:flex;align-items:center;gap:7px;min-width:0}.dlv-hint{display:inline-flex;flex:none}.dlv-hint-trigger{display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:help}.dlv-hint-trigger:hover,.dlv-hint-trigger:focus-visible{color:var(--dsw-alias-label-primary);border-color:currentColor}.dlv-hint-bubble{position:fixed;inset:auto;margin:0;z-index:2147483647;box-sizing:border-box;width:300px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);overflow:auto;padding:13px 15px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;font-weight:400;line-height:1.65;box-shadow:0 8px 30px #0004;text-align:left}.dlv-hint-bubble[hidden]{display:none}
9818
+ .dlv-settings{min-height:0!important;overflow:hidden;gap:16px!important;padding-bottom:0!important}.dlv-settings-heading,.dlv-settings-tabs{flex-shrink:0}.dlv-settings-scroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:2px 4px 12px 0;scrollbar-gutter:stable}.dlv-settings-footer{flex:none!important;margin-top:0!important;padding:8px 0 0!important}.dlv-settings-footer .dlv-settings-subcard-body{max-height:240px;overflow-y:auto}.dlv-settings>.dlv-error{flex:none}
9819
+ .dlv-field-pair{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;align-items:start}.dlv-field-pair label{min-width:0}@container(max-width:360px){.dlv-field-pair{grid-template-columns:1fr}}
9820
+ .dlv-diagnostics-launcher{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dlv-diagnostics-button{display:inline-flex;align-items:center;gap:7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);padding:6px 2px;font:inherit;font-size:12px;cursor:pointer}.dlv-diagnostics-button:hover{color:var(--dsw-alias-label-primary)}.dlv-diagnostics-button svg{width:15px;height:15px}.dlv-diagnostics-launcher [role=alert]{flex-basis:100%;margin:0}
9821
+ @container(max-width:250px){.dlv-settings-tabs{flex-direction:column}.dlv-settings-tab{flex:0 1 auto;white-space:normal;overflow-wrap:anywhere;padding:8px 4px}.dlv-section-content{padding-inline-start:12px}.dlv-version-badges{flex-wrap:wrap}.dlv-settings-heading{overflow-wrap:anywhere}.dlv-settings-heading .dlv-heading-divider{display:none}.dlv-settings-heading .dlv-star-badge{white-space:normal}.dlv-preset-group{grid-template-columns:1fr}.dlv-settings-actions button{max-width:100%;white-space:normal}}
9822
+ /* Primary sections stay visible; secondary settings use quieter disclosures. */
9823
+ .dlv-settings-tabs{display:flex;flex-wrap:wrap;align-items:stretch;gap:4px;padding:4px;overflow:visible;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}
9824
+ .dlv-settings-tab{flex:1 0 auto;justify-content:center;min-width:0;padding:10px 14px;font-size:13px;line-height:18px;border-radius:8px;white-space:nowrap;text-align:center}
9825
+ .dlv-settings-tab[data-active=true]{background:var(--dsw-alias-bg-layer-1);box-shadow:0 1px 4px #0001}
9826
+ .dlv-settings-tab[data-active=true]:after,.dlv-settings-tab:focus-visible:after{display:none}
9827
+ .dlv-settings-card-body{gap:22px;padding:0}
9828
+ .dlv-settings-section{min-width:0;display:grid;gap:14px;padding:0 0 22px;border-bottom:1px solid var(--dsw-alias-border-l1)}
9829
+ .dlv-settings-section:last-child{border-bottom:0;padding-bottom:0}
9830
+ .dlv-section-heading h4{display:flex;align-items:center;gap:9px;margin:0;font-size:14px;line-height:22px;font-weight:650}
9831
+ .dlv-section-heading svg{width:18px;height:18px;color:var(--dsw-alias-label-secondary);flex:none}
9832
+ .dlv-section-content{display:grid;gap:14px;min-width:0;padding-inline-start:27px}
9833
+ .dlv-settings :is(input,select,textarea){min-width:0;font-size:13px;padding:10px 12px}
9834
+ .dlv-settings label{font-size:13px;font-weight:500;gap:7px;min-width:0}
9835
+ .dlv-settings label.dlv-check{align-items:center;line-height:1.5}
9836
+ .dlv-settings label.dlv-check input{flex:none;width:16px;height:16px;padding:0;margin:0;accent-color:var(--dsw-alias-label-primary)}
9837
+ .dlv-settings .dlv-setting-description{margin:-5px 0 0;font-size:12px;line-height:1.6}
9838
+ .dlv-settings .dlv-check + .dlv-setting-description{margin:6px 0 0 24px}
9839
+ .dlv-settings-subcard{margin:0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:clip}
9840
+ .dlv-settings-subcard>summary{border:0;background:transparent;padding:12px;font-size:13px;line-height:20px}
9841
+ .dlv-settings-subcard>summary:hover{background:var(--dsw-alias-interactive-bg-hover)}
9842
+ .dlv-settings-subcard[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary)}
9843
+ .dlv-settings-subcard-body{display:grid;gap:14px;padding:16px!important;min-width:0}
9844
+ .dlv-section-content>.dlv-settings-subcard{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 45%,transparent);border-left:3px solid var(--dsw-alias-border-l2)}
9845
+ .dlv-settings-footer{margin-top:auto;padding-top:18px;border-top:1px solid var(--dsw-alias-border-l1)}
9846
+ .dlv-settings-footer>.dlv-settings-subcard{border:0}
9847
+ .dlv-settings-footer summary{padding:8px 0;color:var(--dsw-alias-label-tertiary);font-size:12px}
9848
+ .dlv-settings .dlv-star-badge{background:transparent;border:0;color:var(--dsw-alias-label-tertiary);font-weight:400;font-size:11px;padding:0}
9849
+ .dlv-settings-heading h3{font-size:18px;letter-spacing:-.02em}
9850
+ .dlv-settings :is(button,a,textarea,summary):focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}
9851
+ @container(max-width:360px){.dlv-settings-tabs{display:flex;flex-wrap:wrap}.dlv-settings-tab{flex:1 0 auto;font-size:12px;padding:8px;gap:5px}.dlv-settings-heading h3{flex-basis:100%}.dlv-settings-subcard-body{padding:12px!important}}
9852
+ .dlv-settings-subcard-body>button{justify-self:start;padding:7px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit;font-size:12px;cursor:pointer}
9853
+ @container(max-width:250px){.dlv-settings-tabs{flex-direction:column}.dlv-settings-tab{flex:0 1 auto;white-space:normal;overflow-wrap:anywhere;max-width:100%;padding:8px 4px}.dlv-section-content{padding-inline-start:12px}.dlv-settings-heading .dlv-version-badges{flex-wrap:wrap}.dlv-settings-heading .dlv-star-badge{white-space:normal;max-width:100%}}
9854
+ details.dlv-settings-section{display:block}.dlv-settings-section>summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none}.dlv-settings-section>summary::-webkit-details-marker{display:none}.dlv-settings-section>summary h4{flex:1;min-width:0}.dlv-settings-section>summary>.dlv-section-chevron{width:16px;height:16px;flex:none;transition:transform .18s ease;color:var(--dsw-alias-label-secondary)}.dlv-settings-section[open]>summary>.dlv-section-chevron{transform:rotate(180deg)}.dlv-settings-section>summary+.dlv-section-content{margin-top:14px}details.dlv-settings-section:not([open])>.dlv-section-content{display:none}@media(prefers-reduced-motion:reduce){.dlv-settings-section>summary>.dlv-section-chevron{transition:none}}
8909
9855
  `;
8910
9856
 
8911
9857
  // src/modules/settings/services/pluginUpdate.ts
@@ -9137,8 +10083,11 @@ function apply(ctx) {
9137
10083
  const t = registerLiveVoiceLocales(ctx);
9138
10084
  const SettingsPanel = createLiveVoiceSettings(t, ctx.locale);
9139
10085
  const controllers = /* @__PURE__ */ new Map();
10086
+ const speechMemory = new SessionSpeechMemory();
10087
+ let automaticSpeechEnabled;
9140
10088
  const retiring = /* @__PURE__ */ new Set();
9141
10089
  const preferences = createSettingsClient();
10090
+ let preferencesLoaded = false;
9142
10091
  const settingsControllers = /* @__PURE__ */ new Set();
9143
10092
  let disposed = false;
9144
10093
  let voiceModeActive = false;
@@ -9169,24 +10118,30 @@ function apply(ctx) {
9169
10118
  canAutoSend: () => !!meetingEntry && !meetingEntry.controller.getSnapshot().recognizing && !meetingEntry.controller.getSnapshot().pendingTranscriptions
9170
10119
  },
9171
10120
  settings: () => preferences.getSnapshot(),
10121
+ savePreferences: (controller, next) => savePreferences(controller, next),
9172
10122
  translate: (key) => t("dsh-live-voice.meeting." + key),
9173
10123
  createSource: (source, settings) => {
9174
10124
  const meter = source === "shared" ? new SharedAudioMeter() : new MicrophoneMeter();
9175
- return { meter, engine: recognitionFor(settings, meter) };
10125
+ return { meter, engine: recognitionFor(settings, meter, source) };
9176
10126
  }
9177
10127
  });
9178
10128
  let updateManager;
9179
- if (typeof ctx.inject === "function") ctx.inject(["remote", "remote.pluginManager"], (remoteCtx) => {
9180
- updateManager = remoteCtx.remote.pluginManager;
9181
- remoteCtx.effect(() => () => {
9182
- updateManager = void 0;
9183
- }, "dsh-live-voice: release update manager");
9184
- });
10129
+ if (typeof ctx.inject === "function")
10130
+ ctx.inject(["remote", "remote.pluginManager"], (remoteCtx) => {
10131
+ updateManager = remoteCtx.remote.pluginManager;
10132
+ remoteCtx.effect(
10133
+ () => () => {
10134
+ updateManager = void 0;
10135
+ },
10136
+ "dsh-live-voice: release update manager"
10137
+ );
10138
+ });
9185
10139
  const pluginUpdate = createPluginUpdate({
9186
10140
  manager: () => updateManager,
9187
10141
  isBusy: () => {
9188
10142
  const m = meeting.getSnapshot();
9189
- if (disposed || voiceModeActive || retiring.size || m.active || m.shared.starting || m.shared.pending || m.microphone.starting || m.microphone.pending) return true;
10143
+ if (disposed || voiceModeActive || retiring.size || m.active || m.shared.starting || m.shared.pending || m.microphone.starting || m.microphone.pending)
10144
+ return true;
9190
10145
  return [...controllers.values()].some((entry) => {
9191
10146
  const s = entry.controller.getSnapshot();
9192
10147
  return s.starting || s.listening || s.recognizing || s.pendingTranscriptions || s.speaking || s.autoSendPending || s.speechBarVisible;
@@ -9286,7 +10241,7 @@ function apply(ctx) {
9286
10241
  "recognitionMaxUtteranceSeconds"
9287
10242
  ]);
9288
10243
  const changesRecognition = (next) => Object.keys(next).some((key) => recognitionSettingKeys.has(key));
9289
- const recognitionFor = (settings, meter) => settings.recognitionEngine === "qwen-http" ? new QwenHttpRecognitionEngine({
10244
+ const recognitionFor = (settings, meter, inputSource = "microphone") => settings.recognitionEngine === "qwen-http" ? new QwenHttpRecognitionEngine({
9290
10245
  meter,
9291
10246
  vadErrorMessage: (error) => t(
9292
10247
  "dsh-live-voice.recognition.silero.error." + (error?.code === "overloaded" ? "overloaded" : "failed")
@@ -9305,6 +10260,7 @@ function apply(ctx) {
9305
10260
  voiceDetectionCustomSilenceMs: settings.voiceDetectionCustomSilenceMs,
9306
10261
  maxUtteranceSeconds: settings.recognitionMaxUtteranceSeconds
9307
10262
  }) : new BrowserRecognitionEngine({
10263
+ ...inputSource === "shared" ? { meter, inputSource } : {},
9308
10264
  processLocally: settings.recognitionProcessLocally,
9309
10265
  autoInstallLocalPack: settings.recognitionAutoInstall
9310
10266
  });
@@ -9323,10 +10279,18 @@ function apply(ctx) {
9323
10279
  }
9324
10280
  const unsubscribePreferences = preferences.subscribe((settings) => {
9325
10281
  if (disposed) return;
10282
+ preferencesLoaded = true;
10283
+ const speechChanged = automaticSpeechEnabled !== void 0 && automaticSpeechEnabled !== settings.announceAssistantMessages;
10284
+ automaticSpeechEnabled = settings.announceAssistantMessages;
10285
+ if (speechChanged) speechMemory.reset();
9326
10286
  meeting.applySettings(settings);
9327
10287
  for (const entry of controllers.values()) {
9328
10288
  if (entry.closed) continue;
9329
10289
  entry.applySettings(settings);
10290
+ if (speechChanged) {
10291
+ entry.speechState = speechMemory.session(entry.key);
10292
+ entry.refreshSpeech(true);
10293
+ } else if (entry.speechAttached) entry.refreshSpeech();
9330
10294
  }
9331
10295
  for (const c of settingsControllers) {
9332
10296
  if (c.disposed) continue;
@@ -9334,16 +10298,51 @@ function apply(ctx) {
9334
10298
  if ([...recognitionSettingKeys].some((key) => previous[key] !== settings[key]))
9335
10299
  c.replaceRecognition(recognitionFor(settings, c.meter));
9336
10300
  c.applySavedSettings(settings);
10301
+ c.patch({ hostPlatform: preferences.getHostPlatform() });
9337
10302
  for (const engine of Object.values(c.engines)) engine.lang = settings.lang;
9338
- run(c, c.refreshCapabilities());
10303
+ run(c, c.refreshCapabilities({ localOnly: true }));
9339
10304
  }
9340
10305
  });
9341
10306
  ctx.effect(
9342
10307
  () => () => unsubscribePreferences(),
9343
10308
  "dsh-live-voice: remove preferences subscription"
9344
10309
  );
10310
+ function suspendSessionSpeech(entry) {
10311
+ if (!entry.speechAttached) return;
10312
+ if (!entry.controller.speechSuspended || !entry.speechState.checkpoint)
10313
+ entry.speechState.checkpoint = entry.controller.checkpointSpeech();
10314
+ entry.speechAttached = false;
10315
+ ++entry.speechRequest;
10316
+ const stopped = entry.controller.suspendSpeech();
10317
+ entry.speechRelease = Promise.resolve(stopped).then(() => {
10318
+ if (entry.controller.speechStopError) throw entry.controller.speechStopError;
10319
+ if (entry.controller.inputReleaseError) throw entry.controller.inputReleaseError;
10320
+ });
10321
+ entry.speechRelease.catch(() => {
10322
+ });
10323
+ }
10324
+ function activateSessionSpeech(entry) {
10325
+ const request = ++entry.speechRequest;
10326
+ const releases = [
10327
+ ...retiring,
10328
+ ...settingsControllers,
10329
+ ...[...controllers.values()].filter((other) => other !== entry).map((other) => ({ endConversation: () => other.speechRelease }))
10330
+ ];
10331
+ return ownership.run(entry.controller, releases, async () => {
10332
+ await preferences.ready;
10333
+ await entry.speechRelease;
10334
+ if (disposed || entry.closed || !entry.speechAttached || request !== entry.speechRequest)
10335
+ return;
10336
+ entry.controller.restoreSpeech(entry.speechState.checkpoint);
10337
+ entry.speechState.checkpoint = null;
10338
+ entry.refreshSpeech(!entry.speechState.attached);
10339
+ entry.speechState.attached = true;
10340
+ entry.controller.activateSpeech();
10341
+ });
10342
+ }
9345
10343
  function retire(entry) {
9346
10344
  if (entry.closed) return;
10345
+ suspendSessionSpeech(entry);
9347
10346
  entry.closed = true;
9348
10347
  if (meetingEntry === entry) {
9349
10348
  meeting.cancelDelivery();
@@ -9366,15 +10365,27 @@ function apply(ctx) {
9366
10365
  if (controllers.get(entry.key) === entry) controllers.delete(entry.key);
9367
10366
  notifyDiagnostics();
9368
10367
  const done = run(entry.controller, entry.controller.dispose());
9369
- const barrier = { endConversation: () => done };
10368
+ const barrier = {
10369
+ endConversation: async () => {
10370
+ await done;
10371
+ if (entry.controller.speechStopError) throw entry.controller.speechStopError;
10372
+ if (entry.controller.inputReleaseError) throw entry.controller.inputReleaseError;
10373
+ }
10374
+ };
9370
10375
  retiring.add(barrier);
9371
- void done.finally(() => retiring.delete(barrier));
10376
+ void done.finally(() => {
10377
+ if (!entry.controller.speechStopError && !entry.controller.inputReleaseError)
10378
+ retiring.delete(barrier);
10379
+ });
9372
10380
  }
9373
10381
  function get(sessionId) {
9374
10382
  const key = String(sessionId);
9375
10383
  if (controllers.has(key)) return controllers.get(key);
9376
10384
  const entry = {
9377
10385
  key,
10386
+ speechState: speechMemory.session(key),
10387
+ speechAttached: false,
10388
+ speechRequest: 0,
9378
10389
  draft: "",
9379
10390
  pendingDraft: void 0,
9380
10391
  publishedDraft: void 0,
@@ -9474,6 +10485,8 @@ function apply(ctx) {
9474
10485
  settings
9475
10486
  });
9476
10487
  const controller = entry.controller;
10488
+ controller.speechSuspended = true;
10489
+ controller.restoreSpeech(entry.speechState.checkpoint);
9477
10490
  entry.unsubscribeVoiceContext = controller.subscribe(() => {
9478
10491
  publishVoiceContext(entry);
9479
10492
  notifyDiagnostics();
@@ -9489,28 +10502,33 @@ function apply(ctx) {
9489
10502
  return () => entry.chatListeners.delete(listener);
9490
10503
  };
9491
10504
  entry.readChat = entry.chat.getSnapshot.bind(entry.chat);
9492
- const attachedAt = Date.now();
9493
- const observed = /* @__PURE__ */ new Set();
9494
- let baselined = false;
9495
- const refresh = (forceBaseline = false) => {
10505
+ const refresh = (reset = false) => {
9496
10506
  if (disposed || entry.closed) return;
9497
10507
  const snapshot = entry.readChat();
9498
- const baseline = forceBaseline || !baselined;
9499
- if ([...snapshot?.nodes?.values?.() || []].length) baselined = true;
10508
+ const messages = assistantMessages(snapshot);
10509
+ const memory = entry.speechState;
10510
+ if (!preferencesLoaded && memory.attached) return;
10511
+ const initial = !memory.baselined;
9500
10512
  const userSeq = latestUserSequence(snapshot);
9501
- if (!baseline && userSeq > entry.userSeq && controller.getSnapshot().settings.interruptSpeechOnUserMessage && (controller.getSnapshot().speaking || controller.getSnapshot().paused))
10513
+ if (!initial && entry.speechAttached && userSeq > entry.userSeq && controller.getSnapshot().settings.interruptSpeechOnUserMessage && (controller.getSnapshot().speaking || controller.getSnapshot().paused))
9502
10514
  run(controller, controller.stopSpeech());
9503
10515
  entry.userSeq = Math.max(entry.userSeq ?? -1, userSeq);
9504
- for (const message2 of assistantMessages(snapshot)) {
9505
- const historical = !observed.has(message2.id) && message2.complete && Number.isFinite(message2.time) && message2.time < attachedAt;
10516
+ if (!entry.speechAttached && memory.baselined && !reset) return;
10517
+ for (const message2 of messages) {
10518
+ const previousText = memory.textById.get(message2.id);
10519
+ const rewritten = !reset && memory.attached && previousText !== void 0 && !message2.text.startsWith(previousText);
10520
+ if (message2.interrupted || rewritten) controller.discardSpeechMessage(message2.id);
10521
+ memory.textById.set(message2.id, message2.text);
10522
+ const baseline = speechMemory.baseline(memory, message2, initial);
9506
10523
  controller.observeMessage(message2.id, message2.text, {
9507
10524
  complete: message2.complete,
9508
- baseline: forceBaseline || baseline && (!Number.isFinite(message2.time) || message2.time < attachedAt) || historical || message2.interrupted
10525
+ baseline: !preferencesLoaded || reset || baseline || message2.interrupted
9509
10526
  });
9510
- observed.add(message2.id);
9511
10527
  }
10528
+ if (messages.length) memory.baselined = true;
9512
10529
  };
9513
- refresh(true);
10530
+ entry.refreshSpeech = refresh;
10531
+ refresh();
9514
10532
  entry.unsubscribe = entry.chat.subscribe(() => {
9515
10533
  refresh();
9516
10534
  for (const listener of entry.chatListeners) listener();
@@ -9570,7 +10588,7 @@ function apply(ctx) {
9570
10588
  publishVoiceContext(entry);
9571
10589
  engineBrowser.lang = controller.getSnapshot().settings.lang;
9572
10590
  engineQwen.lang = controller.getSnapshot().settings.lang;
9573
- run(controller, controller.refreshCapabilities());
10591
+ run(controller, controller.refreshCapabilities({ localOnly: true }));
9574
10592
  };
9575
10593
  controller.updateSettings = (next) => {
9576
10594
  if (disposed || entry.closed) return;
@@ -9586,6 +10604,27 @@ function apply(ctx) {
9586
10604
  ]) {
9587
10605
  const original = controller[method].bind(controller);
9588
10606
  controller[method] = (...args) => {
10607
+ if (method === "endConversation" && args[0] === true && !entry.speechAttached) {
10608
+ controller.patch({ conversation: false });
10609
+ return Promise.all([
10610
+ controller.stopListening(),
10611
+ VoiceCoordinator.prototype.stopSpeech.call(controller, false)
10612
+ ]);
10613
+ }
10614
+ if (!entry.closed && (method === "stopSpeech" || method === "endConversation")) {
10615
+ const parked = entry.speechState.checkpoint;
10616
+ if (parked) {
10617
+ parked.suppressed = [
10618
+ .../* @__PURE__ */ new Set([
10619
+ ...parked.suppressed,
10620
+ ...parked.unfinished,
10621
+ ...parked.pending.map((item) => item.id)
10622
+ ])
10623
+ ];
10624
+ parked.pending = [];
10625
+ parked.paused = false;
10626
+ }
10627
+ }
9589
10628
  entry.request++;
9590
10629
  if (method === "endConversation" && args[0] !== true || method === "stopConversationInput")
9591
10630
  voiceModeActive = false;
@@ -9606,7 +10645,6 @@ function apply(ctx) {
9606
10645
  if (disposed || entry.closed || !entry.refs) return Promise.resolve();
9607
10646
  if (method !== "speak" && !entry.composers.size) return Promise.resolve();
9608
10647
  if (method === "startConversation" || method === "takeMicrophone") {
9609
- refresh(true);
9610
10648
  voiceModeActive = true;
9611
10649
  }
9612
10650
  const request = ++entry.request;
@@ -9655,12 +10693,12 @@ function apply(ctx) {
9655
10693
  if (!entry.closed)
9656
10694
  controller.patch({ error: t("dsh-live-voice.settings.persistence.loadError") });
9657
10695
  });
9658
- run(controller, controller.refreshCapabilities());
10696
+ run(controller, controller.refreshCapabilities({ localOnly: true }));
9659
10697
  return entry;
9660
10698
  }
9661
10699
  function useEntry(sessionId, kind) {
9662
- const [entry, setEntry] = import_react60.default.useState(null);
9663
- import_react60.default.useLayoutEffect(() => {
10700
+ const [entry, setEntry] = import_react64.default.useState(null);
10701
+ import_react64.default.useLayoutEffect(() => {
9664
10702
  if (disposed) return;
9665
10703
  const current = get(sessionId);
9666
10704
  current.refs++;
@@ -9680,11 +10718,12 @@ function apply(ctx) {
9680
10718
  function useComposer(entry, props) {
9681
10719
  const subscribedInput = props.useInput?.((value) => value);
9682
10720
  const input = subscribedInput ?? props.input;
9683
- const token = import_react60.default.useRef({});
9684
- import_react60.default.useLayoutEffect(() => {
10721
+ const token = import_react64.default.useRef({});
10722
+ import_react64.default.useLayoutEffect(() => {
9685
10723
  if (!entry || entry.closed || disposed) return;
9686
10724
  return () => {
9687
10725
  entry.composers.delete(token.current);
10726
+ if (!entry.composers.size) suspendSessionSpeech(entry);
9688
10727
  if (meetingEntry === entry && !entry.composers.size) {
9689
10728
  meeting.cancelDelivery();
9690
10729
  meetingEntry = null;
@@ -9692,10 +10731,11 @@ function apply(ctx) {
9692
10731
  }
9693
10732
  };
9694
10733
  }, [entry]);
9695
- import_react60.default.useLayoutEffect(() => {
10734
+ import_react64.default.useLayoutEffect(() => {
9696
10735
  if (!entry || entry.closed || disposed) return;
9697
10736
  if (!input || typeof props.inputActions?.setDraft !== "function") return;
9698
10737
  if (meetingEntry !== entry) {
10738
+ if (meetingEntry) suspendSessionSpeech(meetingEntry);
9699
10739
  meeting.cancelDelivery();
9700
10740
  meetingEntry = entry;
9701
10741
  entry.controller.setExternalSpeechActivity(sharedSpeechActive);
@@ -9725,10 +10765,18 @@ function apply(ctx) {
9725
10765
  );
9726
10766
  }
9727
10767
  });
9728
- if (voiceModeActive && !entry.controller.getSnapshot().conversation)
9729
- run(entry.controller, entry.controller.startConversation());
10768
+ const attachSpeech = !entry.speechAttached;
10769
+ if (attachSpeech) entry.speechAttached = true;
10770
+ const inputReady = voiceModeActive && !entry.controller.getSnapshot().conversation ? entry.controller.startConversation() : Promise.resolve();
10771
+ run(
10772
+ entry.controller,
10773
+ Promise.resolve(inputReady).then(() => {
10774
+ if (attachSpeech && entry.speechAttached && !entry.closed)
10775
+ return activateSessionSpeech(entry);
10776
+ })
10777
+ );
9730
10778
  }, [entry, input, props.inputActions]);
9731
- import_react60.default.useLayoutEffect(() => {
10779
+ import_react64.default.useLayoutEffect(() => {
9732
10780
  if (!entry || entry.closed || disposed || !input) return;
9733
10781
  const published = typeof input.draft === "string" ? input.draft : "";
9734
10782
  entry.publishedDraft = published;
@@ -9747,7 +10795,7 @@ function apply(ctx) {
9747
10795
  function Buttons(props) {
9748
10796
  const entry = useEntry(props.sessionId, "buttons");
9749
10797
  useComposer(entry, props);
9750
- return entry ? /* @__PURE__ */ import_react60.default.createElement("div", { className: "dlv-composer-toggles" }, /* @__PURE__ */ import_react60.default.createElement(
10798
+ return entry ? /* @__PURE__ */ import_react64.default.createElement("div", { className: "dlv-composer-toggles" }, /* @__PURE__ */ import_react64.default.createElement(
9751
10799
  MeetingToggle,
9752
10800
  {
9753
10801
  meeting: entry.meeting,
@@ -9757,11 +10805,11 @@ function apply(ctx) {
9757
10805
  else entry.startMeetingSource("shared");
9758
10806
  }
9759
10807
  }
9760
- ), /* @__PURE__ */ import_react60.default.createElement(ConversationControls, { controller: entry.controller })) : null;
10808
+ ), /* @__PURE__ */ import_react64.default.createElement(ConversationControls, { controller: entry.controller })) : null;
9761
10809
  }
9762
10810
  function Settings() {
9763
- const [controller, setController] = import_react60.default.useState(null);
9764
- import_react60.default.useEffect(() => {
10811
+ const [controller, setController] = import_react64.default.useState(null);
10812
+ import_react64.default.useEffect(() => {
9765
10813
  const settings = preferences.getSnapshot();
9766
10814
  const browser = new BrowserSpeakingEngine({ lang: settings.lang });
9767
10815
  const qwen = new QwenHttpSpeakingEngine({ lang: settings.lang });
@@ -9815,12 +10863,13 @@ function apply(ctx) {
9815
10863
  const settings2 = c.getSnapshot().settings;
9816
10864
  if (changesRecognition(next)) c.replaceRecognition(recognitionFor(settings2, c.meter));
9817
10865
  const saved = savePreferences(c, next);
9818
- run(c, c.refreshCapabilities());
10866
+ run(c, c.refreshCapabilities({ localOnly: true }));
9819
10867
  await saved;
9820
10868
  if (revision !== settingsRevision || disposed || c.disposed) return;
9821
10869
  };
9822
- const refresh = () => run(c, c.refreshCapabilities());
10870
+ const refresh = () => run(c, c.refreshCapabilities({ localOnly: true }));
9823
10871
  document.addEventListener("dsh-live-voice:capabilitieschanged", refresh);
10872
+ c.patch({ hostPlatform: preferences.getHostPlatform() });
9824
10873
  setController(c);
9825
10874
  refresh();
9826
10875
  return () => {
@@ -9831,21 +10880,21 @@ function apply(ctx) {
9831
10880
  void c.dispose();
9832
10881
  };
9833
10882
  }, []);
9834
- return controller ? /* @__PURE__ */ import_react60.default.createElement(SettingsPanel, { controller, pluginUpdate }) : null;
10883
+ return controller ? /* @__PURE__ */ import_react64.default.createElement(SettingsPanel, { controller, pluginUpdate }) : null;
9835
10884
  }
9836
10885
  registerSettingsSlot(ctx, Settings, () => t("dsh-live-voice.commons.pluginName"));
9837
10886
  function Dock(props) {
9838
10887
  const entry = useEntry(props.sessionId, "dock");
9839
10888
  useComposer(entry, props);
9840
- return entry ? /* @__PURE__ */ import_react60.default.createElement("div", { className: "dlv-bar-stack" }, /* @__PURE__ */ import_react60.default.createElement(SpeechStatusBar, { controller: entry.controller }), /* @__PURE__ */ import_react60.default.createElement(MeetingBars, { meeting: entry.meeting }), /* @__PURE__ */ import_react60.default.createElement(ConversationStatusBar, { controller: entry.controller, includeSpeech: false })) : null;
10889
+ return entry ? /* @__PURE__ */ import_react64.default.createElement("div", { className: "dlv-bar-stack" }, /* @__PURE__ */ import_react64.default.createElement(SpeechStatusBar, { controller: entry.controller }), /* @__PURE__ */ import_react64.default.createElement(MeetingBars, { meeting: entry.meeting }), /* @__PURE__ */ import_react64.default.createElement(ConversationStatusBar, { controller: entry.controller, includeSpeech: false })) : null;
9841
10890
  }
9842
10891
  function QuestionStatusView({ entry }) {
9843
- const snapshot = import_react60.default.useSyncExternalStore(
10892
+ const snapshot = import_react64.default.useSyncExternalStore(
9844
10893
  entry.controller.subscribe,
9845
10894
  entry.controller.getSnapshot
9846
10895
  );
9847
- const [overlayStyle, setOverlayStyle] = import_react60.default.useState();
9848
- import_react60.default.useLayoutEffect(() => {
10896
+ const [overlayStyle, setOverlayStyle] = import_react64.default.useState();
10897
+ import_react64.default.useLayoutEffect(() => {
9849
10898
  if (!snapshot.answeringQuestion) return;
9850
10899
  const seat = document.querySelector("[data-composer-seat]");
9851
10900
  if (!seat) return;
@@ -9867,7 +10916,7 @@ function apply(ctx) {
9867
10916
  }, [snapshot.answeringQuestion]);
9868
10917
  const target = typeof document === "undefined" ? null : document.body;
9869
10918
  return snapshot.answeringQuestion && target && overlayStyle ? (0, import_react_dom.createPortal)(
9870
- /* @__PURE__ */ import_react60.default.createElement(
10919
+ /* @__PURE__ */ import_react64.default.createElement(
9871
10920
  ConversationStatusBar,
9872
10921
  {
9873
10922
  controller: entry.controller,
@@ -9881,24 +10930,21 @@ function apply(ctx) {
9881
10930
  }
9882
10931
  function QuestionStatus(props) {
9883
10932
  const entry = useEntry(props.sessionId, "question-status");
9884
- return entry ? /* @__PURE__ */ import_react60.default.createElement(QuestionStatusView, { entry }) : null;
10933
+ return entry ? /* @__PURE__ */ import_react64.default.createElement(QuestionStatusView, { entry }) : null;
9885
10934
  }
9886
10935
  function ActionView({ entry, messageId }) {
9887
- const snapshot = import_react60.default.useSyncExternalStore(
10936
+ const snapshot = import_react64.default.useSyncExternalStore(
9888
10937
  entry.controller.subscribe,
9889
10938
  entry.controller.getSnapshot
9890
10939
  );
9891
- const chat = import_react60.default.useSyncExternalStore(entry.subscribeChat, entry.readChat);
10940
+ const chat = import_react64.default.useSyncExternalStore(entry.subscribeChat, entry.readChat);
9892
10941
  const message2 = addressedTurn(assistantMessages(chat), messageId);
9893
- const capability = snapshot.capabilities[snapshot.settings.engine];
9894
10942
  const active = snapshot.speaking && message2.id === snapshot.activeMessageId;
9895
- const unavailable = capability?.supported !== true;
9896
- return /* @__PURE__ */ import_react60.default.createElement(
10943
+ return /* @__PURE__ */ import_react64.default.createElement(
9897
10944
  SpeakButton,
9898
10945
  {
9899
10946
  active,
9900
- disabled: !message2.text.trim() || !active && unavailable,
9901
- label: unavailable ? capability?.reason || t("dsh-live-voice.speak.output.checking") : void 0,
10947
+ disabled: !message2.text.trim(),
9902
10948
  onClick: () => run(
9903
10949
  entry.controller,
9904
10950
  active ? entry.controller.stopSpeech() : entry.controller.speak(message2.text, message2.id)
@@ -9908,7 +10954,7 @@ function apply(ctx) {
9908
10954
  }
9909
10955
  function Action(props) {
9910
10956
  const entry = useEntry(props.sessionId, "action");
9911
- return entry ? /* @__PURE__ */ import_react60.default.createElement(ActionView, { entry, messageId: props.messageId }) : null;
10957
+ return entry ? /* @__PURE__ */ import_react64.default.createElement(ActionView, { entry, messageId: props.messageId }) : null;
9912
10958
  }
9913
10959
  ctx.effect(() => {
9914
10960
  const style = document.createElement("style");
@@ -9924,6 +10970,8 @@ function apply(ctx) {
9924
10970
  );
9925
10971
  ctx.effect(() => {
9926
10972
  const stop = () => {
10973
+ speechMemory.clear();
10974
+ for (const entry of controllers.values()) entry.speechState.checkpoint = null;
9927
10975
  ownership.cancel();
9928
10976
  void meeting.end();
9929
10977
  for (const entry of controllers.values()) {
@@ -9974,7 +11022,7 @@ function apply(ctx) {
9974
11022
  };
9975
11023
  const refreshCapabilities = () => {
9976
11024
  for (const entry of controllers.values())
9977
- run(entry.controller, entry.controller.refreshCapabilities());
11025
+ run(entry.controller, entry.controller.refreshCapabilities({ localOnly: true }));
9978
11026
  document.dispatchEvent(new Event("dsh-live-voice:capabilitieschanged"));
9979
11027
  };
9980
11028
  const visibilityChanged = () => {
@@ -9990,6 +11038,7 @@ function apply(ctx) {
9990
11038
  return () => {
9991
11039
  disposed = true;
9992
11040
  ownership.close();
11041
+ speechMemory.clear();
9993
11042
  window.speechSynthesis?.removeEventListener?.("voiceschanged", refreshCapabilities);
9994
11043
  navigator.mediaDevices?.removeEventListener?.("devicechange", refreshCapabilities);
9995
11044
  document.removeEventListener("visibilitychange", visibilityChanged);