opencode-voice-dictation 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@ escritorio:
13
13
  conexión y sin depender del dictado de Windows.
14
14
  - **Sin portapapeles**: la lectura usa `composer.insert()` en la posición del
15
15
  cursor, sin robar el foco ni simular `Ctrl+V`.
16
- - **Lectura local**: voces del sistema (`speechSynthesis`), con respaldo a
17
- `System.Speech.Synthesis` si Electron no expone voces.
16
+ - **Lectura natural**: voces neuronales de Edge (45 en español), con respaldo
17
+ a las voces del sistema si no hay internet.
18
18
  - **Solo Windows**: la app declara `os: ["windows"]`; en otros sistemas aparece
19
19
  como no disponible en vez de fallar.
20
20
 
@@ -167,11 +167,16 @@ espacio, se añade uno de separación (configurable).
167
167
 
168
168
  | Ajuste | Por defecto | Qué hace |
169
169
  |---|---|---|
170
- | Voz | Automática | Voz del sistema; automática elige una en el idioma de la app. |
170
+ | Motor de voz | Edge neural (online) | Voces neuronales de Edge, mucho más naturales. `Sistema` usa las voces SAPI instaladas, sin conexión. |
171
+ | Voz | Ximena (es-ES) | 45 voces en español, agrupadas por acento (España, México, Argentina, Colombia, Chile…). |
171
172
  | Velocidad | 1× | 0,8×, 1×, 1,2× o 1,5×. |
172
173
  | Ignorar bloques de código | Sí | Lee solo la prosa; omite código, rutas y comandos. |
173
174
  | Probar voz | — | Lee una frase corta con la configuración actual. |
174
175
 
176
+ Edge neural usa la API de lectura de Microsoft Edge: es gratis y no necesita
177
+ clave, pero **requiere internet** y el texto se envía a Microsoft. Si falla, el
178
+ plugin usa automáticamente la voz del sistema.
179
+
175
180
  La lectura limpia el Markdown antes de hablar: quita fences, backticks,
176
181
  negritas, encabezados, viñetas y convierte `[texto](url)` en `texto`. Trocea el
177
182
  texto por frases para poder detenerse rápido.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const S=require("@opencode/plugin/gui"),d=require("effect"),z=require("node:child_process"),V=require("node:crypto"),A=require("node:fs"),D=require("node:os"),L=require("node:path"),g=require("solid-js/web"),T=require("solid-js"),ne=S.Plugin.define({os:["windows"],stores:{prefs:S.Store.global(d.Schema.Struct({locale:d.Schema.String,minConfidence:d.Schema.Number,trailingSpace:d.Schema.Boolean,autoFocus:d.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:S.Store.global(d.Schema.Struct({voiceURI:d.Schema.String,rate:d.Schema.Number,skipCode:d.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:S.Store.global(d.Schema.Struct({mode:d.Schema.String}),{mode:"windows"})},i18n:{en:{"pill.idle":"Dictate","pill.starting":"Starting…","pill.listening":"Listening…","pill.dictating":"Dictating…","pill.error":"Retry dictation","pill.title":"Voice dictation","pill.winH":"Open Windows voice typing (Win+H)","pill.stop":"Stop dictation","command.toggle":"Toggle voice dictation","command.toggle.description":"Dictate into the prompt with the selected engine","command.configure":"Voice dictation settings","error.title":"Voice dictation","error.language":"The speech language {locale} is not installed. Available: {available}.","error.none":"Windows has no speech recognizer installed. Settings › Time & language › Language › Speech.","error.mic":"No microphone is available for speech recognition.","error.system":"System.Speech is not available in this Windows installation.","error.exit":"The dictation engine stopped unexpectedly.","error.unknown":"Dictation failed ({code}).","toast.discarded":"Dictation discarded: the active session changed.","settings.title":"Voice dictation","settings.description":"Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Test Win+H","settings.winh.description":"Sends the Windows voice typing shortcut. If the floating bar appears, it works.","settings.winh.run":"Test Win+H","settings.winh.sent":"Win+H sent. If the Windows dictation bar did not appear, check Settings › Accessibility › Voice typing.","error.winh":"Windows voice typing could not be triggered.","settings.language":"Language","settings.language.description":"A speech recognizer installed in Windows. Accents and punctuation depend on it.","settings.confidence":"Minimum confidence","settings.confidence.description":"Results below this threshold are ignored to avoid noise.","confidence.low":"Low (0.2)","confidence.normal":"Normal (0.4)","confidence.high":"High (0.6)","settings.space":"Add a space before the text","settings.space.description":"Separates the dictation from what is already in the prompt.","settings.focus":"Focus the prompt after dictating","settings.focus.description":"Moves the cursor to the prompt so you can review or send it.","settings.test.section":"Diagnostics","settings.test":"Test microphone","settings.test.description":"Checks installed recognizers and the default microphone.","settings.test.run":"Run test","settings.test.success":"Speech recognition ready: {locale}","settings.test.failed":"Not ready: {reason}","common.reason.no-recognizers":"no speech language installed","common.reason.no-microphone":"no microphone available","read.idle":"Read","read.speaking":"Reading…","read.paused":"Paused","read.pause":"Pause","read.resume":"Resume","read.restart":"Read again","read.stop":"Stop reading","read.title":"Read the last message aloud","command.read":"Read the last message aloud","command.read.description":"Speaks the last reply with a system voice","read.noSession":"Open a conversation to read its last message.","read.empty":"The last message has no text to read.","read.error":"The last message could not be read.","read.noVoices":"Windows has no voices installed for reading.","speech.section":"Read aloud","speech.voice":"Voice","speech.voice.description":"A system voice. Automatic picks one in the app language.","speech.voice.auto":"Automatic","speech.rate":"Speed","speech.rate.description":"How fast the text is spoken.","speech.rate.slow":"0.8×","speech.rate.normal":"1×","speech.rate.fast":"1.2×","speech.rate.faster":"1.5×","speech.skipCode":"Skip code blocks","speech.skipCode.description":"Read only the prose; code, paths and commands are left out.","speech.preview":"Test voice","speech.preview.description":"Speaks a short sample with the current voice and speed.","speech.preview.text":"Hello, this is a reading test."},es:{"pill.idle":"Dictar","pill.starting":"Iniciando…","pill.listening":"Escuchando…","pill.dictating":"Dictando…","pill.error":"Reintentar dictado","pill.title":"Dictado por voz","pill.winH":"Abrir el dictado de Windows (Win+H)","pill.stop":"Detener dictado","command.toggle":"Alternar dictado por voz","command.toggle.description":"Dicta en el prompt con el motor elegido","command.configure":"Ajustes de dictado por voz","error.title":"Dictado por voz","error.language":"El idioma de reconocimiento {locale} no está instalado. Disponibles: {available}.","error.none":"Windows no tiene ningún reconocedor de voz instalado. Configuración › Hora e idioma › Idioma › Voz.","error.mic":"No hay ningún micrófono disponible para el reconocimiento de voz.","error.system":"System.Speech no está disponible en esta instalación de Windows.","error.exit":"El motor de dictado se detuvo inesperadamente.","error.unknown":"El dictado falló ({code}).","toast.discarded":"Dictado descartado: la sesión activa cambió.","settings.title":"Dictado por voz","settings.description":"Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows usa el dictado de Windows 11 (Win+H), más preciso. System.Speech funciona sin Windows 11 pero es menos exacto.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Probar Win+H","settings.winh.description":"Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.","settings.winh.run":"Probar Win+H","settings.winh.sent":"Win+H enviado. Si no apareció la barra, revisa Configuración › Accesibilidad › Dictado por voz.","settings.language":"Idioma","settings.language.description":"Un reconocedor instalado en Windows. Los acentos y la puntuación dependen de él.","settings.confidence":"Confianza mínima","settings.confidence.description":"Los resultados por debajo de este umbral se ignoran para evitar ruido.","confidence.low":"Baja (0,2)","confidence.normal":"Normal (0,4)","confidence.high":"Alta (0,6)","settings.space":"Añadir un espacio antes del texto","settings.space.description":"Separa el dictado de lo que ya hay en el prompt.","settings.focus":"Enfocar el prompt al terminar","settings.focus.description":"Mueve el cursor al prompt para revisar o enviar el texto.","settings.test.section":"Diagnóstico","settings.test":"Probar micrófono","settings.test.description":"Comprueba los reconocedores instalados y el micrófono predeterminado.","settings.test.run":"Ejecutar prueba","settings.test.success":"Reconocimiento de voz listo: {locale}","settings.test.failed":"No está listo: {reason}","common.reason.no-recognizers":"no hay ningún idioma de voz instalado","common.reason.no-microphone":"no hay micrófono disponible","read.idle":"Leer","read.speaking":"Leyendo…","read.paused":"Pausado","read.pause":"Pausar","read.resume":"Reanudar","read.restart":"Leer de nuevo","read.stop":"Detener lectura","read.title":"Leer en voz alta el último mensaje","command.read":"Leer en voz alta el último mensaje","command.read.description":"Reproduce la última respuesta con una voz del sistema","read.noSession":"Abre una conversación para leer su último mensaje.","read.empty":"El último mensaje no tiene texto para leer.","read.error":"No se pudo leer el último mensaje.","read.noVoices":"Windows no tiene voces instaladas para la lectura.","speech.section":"Lectura en voz alta","speech.voice":"Voz","speech.voice.description":"Una voz del sistema. Automática elige una en el idioma de la app.","speech.voice.auto":"Automática","speech.rate":"Velocidad","speech.rate.description":"A qué ritmo se lee el texto.","speech.rate.slow":"0,8×","speech.rate.normal":"1×","speech.rate.fast":"1,2×","speech.rate.faster":"1,5×","speech.skipCode":"Ignorar bloques de código","speech.skipCode.description":"Lee solo la prosa; omite código, rutas y comandos.","speech.preview":"Probar voz","speech.preview.description":"Lee una frase corta con la voz y velocidad actuales.","speech.preview.text":"Hola, esta es una prueba de lectura."}}}),H=S.Ipc.define({id:"voice-dictation.dictation",state:d.Schema.Struct({status:d.Schema.Literals(["idle","starting","listening","error"]),error:d.Schema.String,locale:d.Schema.String}),methods:{start:{input:d.Schema.Struct({locale:d.Schema.String,minConfidence:d.Schema.Number})},stop:{},diagnose:{output:d.Schema.Struct({locales:d.Schema.Array(d.Schema.String),microphone:d.Schema.Boolean,selected:d.Schema.String,error:d.Schema.String})}},events:{final:d.Schema.Struct({text:d.Schema.String,confidence:d.Schema.Number}),error:d.Schema.Struct({code:d.Schema.String,detail:d.Schema.String,available:d.Schema.Array(d.Schema.String)})}}),q=S.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:d.Schema.Struct({text:d.Schema.String,rate:d.Schema.Number,voice:d.Schema.String})},stop:{},voices:{output:d.Schema.Array(d.Schema.String)}},events:{done:d.Schema.Struct({ok:d.Schema.Boolean})}}),U=S.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),j=String.raw`param(
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const gui$1=require("@opencode/plugin/gui"),effect=require("effect"),node_child_process=require("node:child_process"),node_crypto=require("node:crypto"),node_fs=require("node:fs"),node_os=require("node:os"),node_path=require("node:path"),web=require("solid-js/web"),solidJs=require("solid-js"),definition=gui$1.Plugin.define({os:["windows"],stores:{prefs:gui$1.Store.global(effect.Schema.Struct({locale:effect.Schema.String,minConfidence:effect.Schema.Number,trailingSpace:effect.Schema.Boolean,autoFocus:effect.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:gui$1.Store.global(effect.Schema.Struct({voiceURI:effect.Schema.String,rate:effect.Schema.Number,skipCode:effect.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:gui$1.Store.global(effect.Schema.Struct({mode:effect.Schema.String}),{mode:"windows"}),tts:gui$1.Store.global(effect.Schema.Struct({engine:effect.Schema.String,voice:effect.Schema.String}),{engine:"edge",voice:"es-ES-XimenaNeural"})},i18n:{en:{"pill.idle":"Dictate","pill.starting":"Starting…","pill.listening":"Listening…","pill.dictating":"Dictating…","pill.error":"Retry dictation","pill.title":"Voice dictation","pill.winH":"Open Windows voice typing (Win+H)","pill.stop":"Stop dictation","command.toggle":"Toggle voice dictation","command.toggle.description":"Dictate into the prompt with the selected engine","command.configure":"Voice dictation settings","error.title":"Voice dictation","error.language":"The speech language {locale} is not installed. Available: {available}.","error.none":"Windows has no speech recognizer installed. Settings › Time & language › Language › Speech.","error.mic":"No microphone is available for speech recognition.","error.system":"System.Speech is not available in this Windows installation.","error.exit":"The dictation engine stopped unexpectedly.","error.unknown":"Dictation failed ({code}).","toast.discarded":"Dictation discarded: the active session changed.","settings.title":"Voice dictation","settings.description":"Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Test Win+H","settings.winh.description":"Sends the Windows voice typing shortcut. If the floating bar appears, it works.","settings.winh.run":"Test Win+H","settings.winh.sent":"Win+H sent. If the Windows dictation bar did not appear, check Settings › Accessibility › Voice typing.","error.winh":"Windows voice typing could not be triggered.","settings.language":"Language","settings.language.description":"A speech recognizer installed in Windows. Accents and punctuation depend on it.","settings.confidence":"Minimum confidence","settings.confidence.description":"Results below this threshold are ignored to avoid noise.","confidence.low":"Low (0.2)","confidence.normal":"Normal (0.4)","confidence.high":"High (0.6)","settings.space":"Add a space before the text","settings.space.description":"Separates the dictation from what is already in the prompt.","settings.focus":"Focus the prompt after dictating","settings.focus.description":"Moves the cursor to the prompt so you can review or send it.","settings.test.section":"Diagnostics","settings.test":"Test microphone","settings.test.description":"Checks installed recognizers and the default microphone.","settings.test.run":"Run test","settings.test.success":"Speech recognition ready: {locale}","settings.test.failed":"Not ready: {reason}","common.reason.no-recognizers":"no speech language installed","common.reason.no-microphone":"no microphone available","read.idle":"Read","read.speaking":"Reading…","read.paused":"Paused","read.pause":"Pause","read.resume":"Resume","read.restart":"Read again","read.stop":"Stop reading","read.title":"Read the last message aloud","command.read":"Read the last message aloud","command.read.description":"Speaks the last reply with a system voice","read.noSession":"Open a conversation to read its last message.","read.empty":"The last message has no text to read.","read.error":"The last message could not be read.","read.noVoices":"Windows has no voices installed for reading.","speech.section":"Read aloud","speech.engine":"Voice engine","speech.engine.description":"Edge neural sounds much more natural but needs internet. System uses the installed Windows voices, offline.","speech.engine.edge":"Edge neural (online)","speech.engine.system":"System (offline)","error.edge":"The Edge voice could not be used. Falling back to the system voice.","speech.voice":"Voice","speech.voice.description":"The voice used for reading.","speech.voice.auto":"Automatic","speech.rate":"Speed","speech.rate.description":"How fast the text is spoken.","speech.rate.slow":"0.8×","speech.rate.normal":"1×","speech.rate.fast":"1.2×","speech.rate.faster":"1.5×","speech.skipCode":"Skip code blocks","speech.skipCode.description":"Read only the prose; code, paths and commands are left out.","speech.preview":"Test voice","speech.preview.description":"Speaks a short sample with the current voice and speed.","speech.preview.text":"Hello, this is a reading test."},es:{"pill.idle":"Dictar","pill.starting":"Iniciando…","pill.listening":"Escuchando…","pill.dictating":"Dictando…","pill.error":"Reintentar dictado","pill.title":"Dictado por voz","pill.winH":"Abrir el dictado de Windows (Win+H)","pill.stop":"Detener dictado","command.toggle":"Alternar dictado por voz","command.toggle.description":"Dicta en el prompt con el motor elegido","command.configure":"Ajustes de dictado por voz","error.title":"Dictado por voz","error.language":"El idioma de reconocimiento {locale} no está instalado. Disponibles: {available}.","error.none":"Windows no tiene ningún reconocedor de voz instalado. Configuración › Hora e idioma › Idioma › Voz.","error.mic":"No hay ningún micrófono disponible para el reconocimiento de voz.","error.system":"System.Speech no está disponible en esta instalación de Windows.","error.exit":"El motor de dictado se detuvo inesperadamente.","error.unknown":"El dictado falló ({code}).","toast.discarded":"Dictado descartado: la sesión activa cambió.","settings.title":"Dictado por voz","settings.description":"Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows usa el dictado de Windows 11 (Win+H), más preciso. System.Speech funciona sin Windows 11 pero es menos exacto.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Probar Win+H","settings.winh.description":"Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.","settings.winh.run":"Probar Win+H","settings.winh.sent":"Win+H enviado. Si no apareció la barra, revisa Configuración › Accesibilidad › Dictado por voz.","settings.language":"Idioma","settings.language.description":"Un reconocedor instalado en Windows. Los acentos y la puntuación dependen de él.","settings.confidence":"Confianza mínima","settings.confidence.description":"Los resultados por debajo de este umbral se ignoran para evitar ruido.","confidence.low":"Baja (0,2)","confidence.normal":"Normal (0,4)","confidence.high":"Alta (0,6)","settings.space":"Añadir un espacio antes del texto","settings.space.description":"Separa el dictado de lo que ya hay en el prompt.","settings.focus":"Enfocar el prompt al terminar","settings.focus.description":"Mueve el cursor al prompt para revisar o enviar el texto.","settings.test.section":"Diagnóstico","settings.test":"Probar micrófono","settings.test.description":"Comprueba los reconocedores instalados y el micrófono predeterminado.","settings.test.run":"Ejecutar prueba","settings.test.success":"Reconocimiento de voz listo: {locale}","settings.test.failed":"No está listo: {reason}","common.reason.no-recognizers":"no hay ningún idioma de voz instalado","common.reason.no-microphone":"no hay micrófono disponible","read.idle":"Leer","read.speaking":"Leyendo…","read.paused":"Pausado","read.pause":"Pausar","read.resume":"Reanudar","read.restart":"Leer de nuevo","read.stop":"Detener lectura","read.title":"Leer en voz alta el último mensaje","command.read":"Leer en voz alta el último mensaje","command.read.description":"Reproduce la última respuesta con una voz del sistema","read.noSession":"Abre una conversación para leer su último mensaje.","read.empty":"El último mensaje no tiene texto para leer.","read.error":"No se pudo leer el último mensaje.","read.noVoices":"Windows no tiene voces instaladas para la lectura.","speech.section":"Lectura en voz alta","speech.engine":"Motor de voz","speech.engine.description":"Edge neuronal suena mucho más natural pero necesita internet. Sistema usa las voces instaladas de Windows, sin conexión.","speech.engine.edge":"Edge neuronal (online)","speech.engine.system":"Sistema (offline)","error.edge":"No se pudo usar la voz de Edge. Se usará la voz del sistema.","speech.voice":"Voz","speech.voice.description":"La voz que se usa para leer.","speech.voice.auto":"Automática","speech.rate":"Velocidad","speech.rate.description":"A qué ritmo se lee el texto.","speech.rate.slow":"0,8×","speech.rate.normal":"1×","speech.rate.fast":"1,2×","speech.rate.faster":"1,5×","speech.skipCode":"Ignorar bloques de código","speech.skipCode.description":"Lee solo la prosa; omite código, rutas y comandos.","speech.preview":"Probar voz","speech.preview.description":"Lee una frase corta con la voz y velocidad actuales.","speech.preview.text":"Hola, esta es una prueba de lectura."}}}),Dictation=gui$1.Ipc.define({id:"voice-dictation.dictation",state:effect.Schema.Struct({status:effect.Schema.Literals(["idle","starting","listening","error"]),error:effect.Schema.String,locale:effect.Schema.String}),methods:{start:{input:effect.Schema.Struct({locale:effect.Schema.String,minConfidence:effect.Schema.Number})},stop:{},diagnose:{output:effect.Schema.Struct({locales:effect.Schema.Array(effect.Schema.String),microphone:effect.Schema.Boolean,selected:effect.Schema.String,error:effect.Schema.String})}},events:{final:effect.Schema.Struct({text:effect.Schema.String,confidence:effect.Schema.Number}),error:effect.Schema.Struct({code:effect.Schema.String,detail:effect.Schema.String,available:effect.Schema.Array(effect.Schema.String)})}}),Speech=gui$1.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:effect.Schema.Struct({text:effect.Schema.String,rate:effect.Schema.Number,voice:effect.Schema.String})},stop:{},voices:{output:effect.Schema.Array(effect.Schema.String)},edgeVoices:{output:effect.Schema.Array(effect.Schema.Struct({id:effect.Schema.String,label:effect.Schema.String,locale:effect.Schema.String,gender:effect.Schema.String}))},edgeSpeak:{input:effect.Schema.Struct({text:effect.Schema.String,voice:effect.Schema.String,rate:effect.Schema.Number}),output:effect.Schema.String}},events:{done:effect.Schema.Struct({ok:effect.Schema.Boolean})}}),VoiceTyping=gui$1.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),DICTATION_SCRIPT=String.raw`param(
2
2
  [string]$Culture = "es-ES",
3
3
  [double]$MinConfidence = 0.4,
4
4
  [switch]$Diagnose
@@ -105,9 +105,9 @@ while ($true) {
105
105
  Start-Sleep -Milliseconds 250
106
106
  }
107
107
  }
108
- `,ie="powershell.exe";function K(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=V.createHash("sha1").update(j).digest("hex").slice(0,12),n=L.join(e,`dictation-${t}.ps1`);return A.writeFileSync(n,j,"utf8"),n}function Z(e){return z.spawn(ie,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function X(e){const t=e.pid;if(t===void 0){e.kill();return}z.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function oe(e,t){const n=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let l=!1,o="",r="";const u=s=>{switch(s.type){case"ready":t.onReady(s.locale??e.locale);break;case"final":s.text&&t.onFinal(s.text,s.confidence??0);break;case"error":t.onError(s.code??"UNKNOWN",s.detail??"",s.available??[]);break}};return n.stdout?.setEncoding("utf8"),n.stdout?.on("data",s=>{r+=s;let a=r.indexOf(`
109
- `);for(;a>=0;){const p=r.slice(0,a).replace(/\r$/,"").trim();if(r=r.slice(a+1),p)try{u(JSON.parse(p))}catch{}a=r.indexOf(`
110
- `)}}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",s=>{o=(o+s).slice(-2e3)}),n.on("error",s=>{l||t.onError("ENGINE_START_FAILED",s.message,[])}),n.on("close",s=>{l||t.onExit(s,o.trim().slice(-500))}),{stop(){l=!0,X(n)}}}function re(){return new Promise((e,t)=>{const n=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Diagnose"]);let l="",o="",r=!1;const u=(a,p)=>{r||(r=!0,clearTimeout(s),a?t(a):e(p))},s=setTimeout(()=>{X(n),u(new Error("diagnose timed out"))},2e4);n.stdout?.setEncoding("utf8"),n.stdout?.on("data",a=>{l+=a}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",a=>{o+=a}),n.on("error",a=>u(a)),n.on("close",()=>{const a=l.split(/\r?\n/).map(p=>p.trim()).filter(p=>p.startsWith("{")).at(-1);if(!a){u(new Error(o.trim()||"no output from PowerShell"));return}try{const p=JSON.parse(a);u(void 0,{locales:p.locales??[],microphone:p.microphone===!0,selected:p.selected??"",error:p.error??""})}catch(p){u(p instanceof Error?p:new Error(String(p)))}})})}const M=String.raw`param(
108
+ `,POWERSHELL$2="powershell.exe";function scriptFile$2(){const e=node_path.join(node_os.tmpdir(),"opencode-voice-dictation");node_fs.mkdirSync(e,{recursive:!0});const t=node_crypto.createHash("sha1").update(DICTATION_SCRIPT).digest("hex").slice(0,12),n=node_path.join(e,`dictation-${t}.ps1`);return node_fs.writeFileSync(n,DICTATION_SCRIPT,"utf8"),n}function spawnPowershell$2(e){return node_child_process.spawn(POWERSHELL$2,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function killTree$1(e){const t=e.pid;if(t===void 0){e.kill();return}node_child_process.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function startEngine(e,t){const n=spawnPowershell$2(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",scriptFile$2(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let s=!1,o="",r="";const d=a=>{switch(a.type){case"ready":t.onReady(a.locale??e.locale);break;case"final":a.text&&t.onFinal(a.text,a.confidence??0);break;case"error":t.onError(a.code??"UNKNOWN",a.detail??"",a.available??[]);break}};return n.stdout?.setEncoding("utf8"),n.stdout?.on("data",a=>{r+=a;let c=r.indexOf(`
109
+ `);for(;c>=0;){const u=r.slice(0,c).replace(/\r$/,"").trim();if(r=r.slice(c+1),u)try{d(JSON.parse(u))}catch{}c=r.indexOf(`
110
+ `)}}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",a=>{o=(o+a).slice(-2e3)}),n.on("error",a=>{s||t.onError("ENGINE_START_FAILED",a.message,[])}),n.on("close",a=>{s||t.onExit(a,o.trim().slice(-500))}),{stop(){s=!0,killTree$1(n)}}}function diagnose(){return new Promise((e,t)=>{const n=spawnPowershell$2(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",scriptFile$2(),"-Diagnose"]);let s="",o="",r=!1;const d=(c,u)=>{r||(r=!0,clearTimeout(a),c?t(c):e(u))},a=setTimeout(()=>{killTree$1(n),d(new Error("diagnose timed out"))},2e4);n.stdout?.setEncoding("utf8"),n.stdout?.on("data",c=>{s+=c}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",c=>{o+=c}),n.on("error",c=>d(c)),n.on("close",()=>{const c=s.split(/\r?\n/).map(u=>u.trim()).filter(u=>u.startsWith("{")).at(-1);if(!c){d(new Error(o.trim()||"no output from PowerShell"));return}try{const u=JSON.parse(c);d(void 0,{locales:u.locales??[],microphone:u.microphone===!0,selected:u.selected??"",error:u.error??""})}catch(u){d(u instanceof Error?u:new Error(String(u)))}})})}const SPEAK_SCRIPT=String.raw`param(
111
111
  [string]$TextFile = "",
112
112
  [int]$Rate = 0,
113
113
  [string]$Voice = "",
@@ -160,7 +160,7 @@ try {
160
160
  } catch { }
161
161
 
162
162
  $synth.Dispose()
163
- `,se="powershell.exe";function Y(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");return A.mkdirSync(e,{recursive:!0}),e}function J(){const e=V.createHash("sha1").update(M).digest("hex").slice(0,12),t=L.join(Y(),`speak-${e}.ps1`);return A.writeFileSync(t,M,"utf8"),t}function ae(e){const t=L.join(Y(),`speech-${V.randomUUID()}.txt`);return A.writeFileSync(t,e,"utf8"),t}function Q(e){return z.spawn(se,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function x(e){const t=e.pid;if(t===void 0){e.kill();return}z.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function ce(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function le(e,t){const n=ae(e.text),l=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-TextFile",n,"-Rate",String(ce(e.rate)),"-Voice",e.voice]);let o=!1;const r=()=>{try{A.rmSync(n,{force:!0})}catch{}};return l.on("error",()=>{r(),o||t(!1)}),l.on("close",u=>{r(),o||t(u===0)}),{stop(){o=!0,x(l),r()}}}function de(){return new Promise(e=>{const t=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-ListVoices"]);let n="",l=!1;const o=u=>{l||(l=!0,clearTimeout(r),e(u))},r=setTimeout(()=>{x(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",u=>{n+=u}),t.on("error",()=>o([])),t.on("close",()=>{const u=n.split(/\r?\n/).map(s=>s.trim()).filter(s=>s.length>0);o(u)})})}const G=String.raw`$ErrorActionPreference = "Stop"
163
+ `,POWERSHELL$1="powershell.exe";function workDir(){const e=node_path.join(node_os.tmpdir(),"opencode-voice-dictation");return node_fs.mkdirSync(e,{recursive:!0}),e}function scriptFile$1(){const e=node_crypto.createHash("sha1").update(SPEAK_SCRIPT).digest("hex").slice(0,12),t=node_path.join(workDir(),`speak-${e}.ps1`);return node_fs.writeFileSync(t,SPEAK_SCRIPT,"utf8"),t}function textFile(e){const t=node_path.join(workDir(),`speech-${node_crypto.randomUUID()}.txt`);return node_fs.writeFileSync(t,e,"utf8"),t}function spawnPowershell$1(e){return node_child_process.spawn(POWERSHELL$1,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function killTree(e){const t=e.pid;if(t===void 0){e.kill();return}node_child_process.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function sapiRate(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function speakSapi(e,t){const n=textFile(e.text),s=spawnPowershell$1(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",scriptFile$1(),"-TextFile",n,"-Rate",String(sapiRate(e.rate)),"-Voice",e.voice]);let o=!1;const r=()=>{try{node_fs.rmSync(n,{force:!0})}catch{}};return s.on("error",()=>{r(),o||t(!1)}),s.on("close",d=>{r(),o||t(d===0)}),{stop(){o=!0,killTree(s),r()}}}function listSapiVoices(){return new Promise(e=>{const t=spawnPowershell$1(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",scriptFile$1(),"-ListVoices"]);let n="",s=!1;const o=d=>{s||(s=!0,clearTimeout(r),e(d))},r=setTimeout(()=>{killTree(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",d=>{n+=d}),t.on("error",()=>o([])),t.on("close",()=>{const d=n.split(/\r?\n/).map(a=>a.trim()).filter(a=>a.length>0);o(d)})})}const WIN_H_SCRIPT=String.raw`$ErrorActionPreference = "Stop"
164
164
 
165
165
  Add-Type @"
166
166
  using System;
@@ -187,6 +187,6 @@ public static class WinH {
187
187
  "@
188
188
 
189
189
  [WinH]::Send()
190
- `,pe="powershell.exe";function ue(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=V.createHash("sha1").update(G).digest("hex").slice(0,12),n=L.join(e,`win-h-${t}.ps1`);return A.writeFileSync(n,G,"utf8"),n}function ge(e){return z.spawn(pe,e,{windowsHide:!0,stdio:"ignore"})}function me(){return new Promise((e,t)=>{const n=ge(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",ue()]);n.on("error",l=>t(l)),n.on("close",l=>{l===0?e():t(new Error(`Win+H helper exited with code ${l??"unknown"}`))})})}const he=e=>{let t="idle",n="",l="",o,r=!1;const u=()=>{o=void 0,r=!1},s=e.provide(H,{state:()=>({status:t,error:n,locale:l}),start(y,E){o&&(r=!0,o.stop(),u()),t="starting",n="",l=y.locale,s.changed();const C=E.window;try{o=oe({locale:y.locale,minConfidence:y.minConfidence},{onReady(f){t="listening",l=f,s.changed()},onFinal(f,h){s.emit("final",{text:f,confidence:h},C)},onError(f,h,k){t="error",n=f,s.changed(),s.emit("error",{code:f,detail:h,available:k},C)},onExit(f,h){if(r){u(),t="idle",n="",l="",s.changed();return}u(),t!=="error"&&(t="error",n=f===null?"ENGINE_EXIT":`ENGINE_EXIT_${f}`,s.changed(),s.emit("error",{code:"ENGINE_EXIT",detail:h||(f===null?"":String(f)),available:[]},C))}})}catch(f){throw t="error",n="ENGINE_START_FAILED",s.changed(),f}},stop(){o&&(r=!0,o.stop(),u()),t="idle",n="",l="",s.changed()},diagnose(){return re()}});let a;const p=e.provide(q,{speak(y){a?.stop(),a=le(y,E=>{a=void 0,p.emit("done",{ok:E})})},stop(){a&&(a.stop(),a=void 0)},voices(){return de()}});e.provide(U,{toggle(){return me()}}),e.onCleanup(()=>{o&&(r=!0,o.stop(),u()),a?.stop(),a=void 0})},fe=250;function Se(e,t){let n=e.replace(/\r\n/g,`
191
- `);return t?n=n.replace(/```[\s\S]*?```/g," "):n=n.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),n=n.replace(/`([^`]+)`/g,"$1"),n=n.replace(/!\[[^\]]*\]\([^)]*\)/g," "),n=n.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),n=n.replace(/^\s*#{1,6}\s+/gm,""),n=n.replace(/^\s*>\s?/gm,""),n=n.replace(/^\s*[-*+]\s+/gm,""),n=n.replace(/^\s*\d+\.\s+/gm,""),n=n.replace(/(\*\*|__)(.*?)\1/g,"$2"),n=n.replace(/(\*|_)(.*?)\1/g,"$2"),n=n.replace(/^\s*\|.*\|\s*$/gm," "),n=n.replace(/\n{2,}/g,". "),n=n.replace(/\n/g," "),n=n.replace(/\s+/g," "),n=n.replace(/\s+([.,;:!?])/g,"$1"),n=n.replace(/([.,;:!?]){2,}/g,"$1"),n.trim()}function ve(e,t=fe){const n=e.trim();if(!n)return[];const l=n.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let r="";const u=s=>{let a="";for(const p of s.split(/\s+/))p&&(a&&a.length+1+p.length>t?(o.push(a),a=p):a=a?`${a} ${p}`:p);return a};for(const s of l){if(s.length>t){r&&(o.push(r),r=""),r=u(s);continue}r&&r.length+1+s.length>t?(o.push(r),r=s):r=r?`${r} ${s}`:s}return r&&o.push(r),o}function we(e,t){const n=e.stores.speech,[l,o]=T.createSignal(!1),[r,u]=T.createSignal(!1),[s,a]=T.createSignal([]),[p,y]=T.createSignal("none");let E=0,C=[],f="";const h=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,k=()=>{const v=h()?.getVoices()??[];if(v.length===0)return;const w=n.value.voiceURI;if(w){const $=v.find(O=>O.voiceURI===w);if($)return $}const b=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return v.find($=>$.lang.toLowerCase().startsWith(b))??v[0]},R=async()=>{const v=h()?.getVoices()??[];if(v.length>0){a(v.map(w=>({id:w.voiceURI,label:`${w.name} — ${w.lang}`}))),y("web");return}try{const w=await t.voices();if(w.length>0){a(w.map(b=>({id:b,label:b}))),y("sapi");return}}catch{}y("none")},_=()=>{E++,C=[],h()?.cancel(),u(!1),o(!1),p()==="sapi"&&t.stop().catch(()=>{})},P=()=>p()==="web",c=()=>{!l()||r()||!P()||(h()?.pause(),u(!0))},i=()=>{!l()||!r()||!P()||(h()?.resume(),u(!1))},m=()=>{r()?i():c()},I=()=>f?N(f):Promise.resolve(!1),N=async v=>{f=v;const w=Se(v,n.value.skipCode);if(!w)return!1;if(_(),await R(),p()==="web"){const b=h();if(!b)return!1;const $=k(),O=n.value.rate,te=E;C=ve(w),o(!0),u(!1);const F=()=>{if(te!==E)return;const B=C.shift();if(!B){u(!1),o(!1);return}const W=new SpeechSynthesisUtterance(B);$&&(W.voice=$),$?.lang&&(W.lang=$.lang),W.rate=O,W.onend=F,W.onerror=F,b.speak(W)};return F(),!0}if(p()==="sapi"){o(!0),u(!1);try{await t.speak({text:w,rate:n.value.rate,voice:n.value.voiceURI})}catch(b){throw o(!1),b}return!0}throw o(!1),new Error(e.t("read.noVoices"))},ee=async()=>{const v=e.sessions.current();if(!v)return"";const b=(await v.server.client.message.list({sessionID:v.id,type:"assistant",order:"desc",limit:1})).data[0];return!b||b.type!=="assistant"?"":b.content.filter($=>$.type==="text").map($=>$.text).join(`
192
- `)};return t.on("done",()=>{u(!1),o(!1)}),h()?.addEventListener?.("voiceschanged",()=>{R()}),R(),{speaking:l,paused:r,backend:p,canPause:P,voices:s,refreshVoices:R,lastAssistantText:ee,speak:N,cancel:_,pause:c,resume:i,togglePause:m,restart:I,preview:()=>N(e.t("speech.preview.text"))}}const ye=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function be(e){const t=S.usePlugin(),n=t.ui,l=t.stores.prefs,o=t.stores.speech,r=t.stores.engine,u=t.main(H),s=t.main(U),[a,p]=T.createSignal([]),[y,E]=T.createSignal(!1);T.onMount(()=>{u.diagnose().then(i=>{i.locales.length>0&&p(i.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const C=()=>{const i=a(),m=l.value.locale;return(i.includes(m)?i:[m,...i]).map(N=>({value:N,label:N}))},f=()=>[{value:"",label:t.t("speech.voice.auto")},...e.speaker.voices().map(i=>({value:i.id,label:i.label}))],h=i=>{l.update(m=>{i.locale!==void 0&&(m.locale=i.locale),i.minConfidence!==void 0&&(m.minConfidence=i.minConfidence),i.trailingSpace!==void 0&&(m.trailingSpace=i.trailingSpace),i.autoFocus!==void 0&&(m.autoFocus=i.autoFocus)})},k=i=>{o.update(m=>{i.voiceURI!==void 0&&(m.voiceURI=i.voiceURI),i.rate!==void 0&&(m.rate=i.rate),i.skipCode!==void 0&&(m.skipCode=i.skipCode)})},R=i=>{r.update(m=>{m.mode=i})},_=async()=>{try{await s.toggle(),t.toast.show({variant:"default",title:t.t("settings.winh"),description:t.t("settings.winh.sent")})}catch(i){t.toast.show({variant:"error",title:t.t("pill.title"),description:i instanceof Error?i.message:t.t("error.winh")})}},P=async()=>{try{await e.speaker.preview()}catch(i){t.toast.show({variant:"error",title:t.t("read.title"),description:i instanceof Error?i.message:t.t("read.error")})}},c=async()=>{E(!0);try{const i=await u.diagnose();if(i.locales.length===0){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-recognizers")}),persistent:!0});return}if(!i.microphone){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-microphone")}),persistent:!0});return}i.locales.length>0&&!a().length&&p(i.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:i.selected||i.locales[0]||l.value.locale})})}catch(i){t.toast.show({variant:"error",title:t.t("error.title"),description:i instanceof Error?i.message:String(i)})}finally{E(!1)}};return g.createComponent(n.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[g.createComponent(n.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[g.createComponent(n.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.engine")},get value(){return r.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:i=>R(i)})}}),g.createComponent(T.Show,{get when(){return r.value.mode==="windows"},get children(){return g.createComponent(n.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return g.createComponent(n.Button,{onClick:()=>{_()},get children(){return t.t("settings.winh.run")}})}})}}),g.createComponent(T.Show,{get when(){return r.value.mode==="system"},get children(){return[g.createComponent(n.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.language")},get value(){return l.value.locale},get options(){return g.memo(()=>C().length>0)()?C():ye.map(i=>({value:i,label:i}))},onChange:i=>h({locale:i})})}}),g.createComponent(n.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.confidence")},get value(){return String(l.value.minConfidence)},get options(){return[{value:"0.2",label:t.t("confidence.low")},{value:"0.4",label:t.t("confidence.normal")},{value:"0.6",label:t.t("confidence.high")}]},onChange:i=>h({minConfidence:Number(i)||.4})})}}),g.createComponent(n.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return l.value.trailingSpace},onChange:i=>h({trailingSpace:i})})}}),g.createComponent(n.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return l.value.autoFocus},onChange:i=>h({autoFocus:i})})}})]}})]}}),g.createComponent(n.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[g.createComponent(n.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("speech.voice")},get value(){return o.value.voiceURI},get options(){return f()},onChange:i=>k({voiceURI:i})})}}),g.createComponent(n.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("speech.rate")},get value(){return String(o.value.rate)},get options(){return[{value:"0.8",label:t.t("speech.rate.slow")},{value:"1",label:t.t("speech.rate.normal")},{value:"1.2",label:t.t("speech.rate.fast")},{value:"1.5",label:t.t("speech.rate.faster")}]},onChange:i=>k({rate:Number(i)||1})})}}),g.createComponent(n.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:i=>k({skipCode:i})})}}),g.createComponent(n.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return g.createComponent(n.Button,{onClick:()=>{P()},get children(){return t.t("read.idle")}})}})]}}),g.createComponent(n.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return g.createComponent(n.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return g.createComponent(n.Button,{get busy(){return y()},onClick:()=>{c()},get children(){return t.t("settings.test.run")}})}})}})]}})}function $e(e){const t=S.usePlugin();return g.memo(()=>e.actions().map(n=>g.createComponent(t.ui.Button,{get variant(){return n.id==="stop"?"danger":"neutral"},get icon(){return n.icon},onClick:()=>{n.run(),e.close()},get children(){return n.label}})))}const Ee=e=>{const t=e.main(H),n=e.main(q),l=e.main(U),o=we(e,n),r=e.stores.prefs,u=e.stores.engine,[s,a]=T.createSignal(!1);let p;const y=(c,i,m)=>{switch(c){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:i||r.value.locale,available:m.length>0?m.join(", "):"—"});case"NO_RECOGNIZERS":return e.t("error.none");case"MICROPHONE_UNAVAILABLE":return e.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return e.t("error.system");case"ENGINE_EXIT":return e.t("error.exit");default:return e.t("error.unknown",{code:c})}},E=()=>{s()&&(a(!1),l.toggle().catch(()=>{})),o.cancel(),p=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(c=>{e.toast.show({variant:"error",title:e.t("error.title"),description:c instanceof Error?c.message:String(c)})})},C=()=>{t.stop().catch(()=>{})},f=()=>{o.cancel(),e.screen.current()?.composer.focus(),l.toggle().then(()=>a(c=>!c)).catch(c=>{a(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:c instanceof Error?`${e.t("error.winh")} ${c.message}`:e.t("error.winh")})})},h=()=>{if(u.value.mode==="windows"){f();return}const c=t.state()?.status??"idle";c==="starting"||c==="listening"?C():E()},k=()=>{if(o.speaking()){o.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const c=t.state()?.status??"idle";(c==="starting"||c==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const i=await o.lastAssistantText();if(!i.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(i)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(i){e.toast.show({variant:"error",title:e.t("read.title"),description:i instanceof Error?i.message:e.t("read.error")})}})()},R=()=>{if(!o.speaking())return[{id:"start",label:e.t("read.idle"),icon:"outline-audio",run:k}];const c=o.paused(),i=[{id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{o.restart()}}];return o.canPause()&&i.push({id:"pause",label:c?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>o.togglePause()}),i.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",run:()=>o.cancel()}),i};t.on("final",c=>{const i=e.sessions.current()?.key;if(p&&i&&i!==p){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const m=e.screen.current()?.composer;if(!m)return;const I=m.text(),N=r.value.trailingSpace&&I.length>0&&!/\s$/.test(I)?" ":"";m.insert(N+c.text),r.value.autoFocus&&m.focus()}),t.on("error",c=>{e.toast.show({variant:"error",title:e.t("error.title"),description:y(c.code,c.detail,c.available),persistent:c.code==="LANGUAGE_NOT_AVAILABLE"||c.code==="NO_RECOGNIZERS",duration:8e3})});const _=()=>{if(e.app.platform!=="desktop")return;if(u.value.mode==="windows")return s()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:h}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:h};const c=t.state()?.status??"idle";return c==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:h}:c==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:h}:c==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:h}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:h}},P=()=>{if(e.app.platform!=="desktop")return;const c=o.speaking(),i=o.paused(),m=c?R().map(I=>({id:I.id,label:I.label,icon:I.icon,tone:I.id==="stop"?"danger":void 0,run:I.run})):[];return{label:c?i?e.t("read.paused"):e.t("read.speaking"):e.t("read.idle"),icon:"outline-audio",tone:c?"success":void 0,title:e.t("read.title"),rows:m.length>0?m:void 0,run:c?void 0:k}};e.add(S.ComposerPill,()=>({id:"dictate",show:()=>_()})),e.add(S.ComposerPill,()=>({id:"read",order:1,show:()=>P(),menu:c=>g.createComponent($e,{actions:R,get close(){return c.close}})})),e.add(S.Command,()=>({id:"toggle",title:e.t("command.toggle"),description:e.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:e.app.platform==="desktop",run:h})),e.add(S.Command,()=>({id:"configure",title:e.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),enabled:e.app.platform==="desktop",run:()=>e.layout.settings(e.id)})),e.add(S.Command,()=>({id:"read",title:e.t("command.read"),description:e.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:e.app.platform==="desktop",run:k})),e.add(S.SettingsPage,{id:e.id,icon:"outline-audio",get title(){return e.t("settings.title")},get entries(){return[{id:"engine",title:e.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:e.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:e.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:e.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:e.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:e.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"rate",title:e.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:e.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:e.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>g.createComponent(be,{speaker:o})})},Ce=S.Plugin.define({...ne,setup:Ee,main:he});exports.default=Ce;
190
+ `,POWERSHELL="powershell.exe";function scriptFile(){const e=node_path.join(node_os.tmpdir(),"opencode-voice-dictation");node_fs.mkdirSync(e,{recursive:!0});const t=node_crypto.createHash("sha1").update(WIN_H_SCRIPT).digest("hex").slice(0,12),n=node_path.join(e,`win-h-${t}.ps1`);return node_fs.writeFileSync(n,WIN_H_SCRIPT,"utf8"),n}function spawnPowershell(e){return node_child_process.spawn(POWERSHELL,e,{windowsHide:!0,stdio:"ignore"})}function sendVoiceTypingShortcut(){return new Promise((e,t)=>{const n=spawnPowershell(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",scriptFile()]);n.on("error",s=>t(s)),n.on("close",s=>{s===0?e():t(new Error(`Win+H helper exited with code ${s??"unknown"}`))})})}let cached;function load(){if(cached)return cached;const runtimeRequire=eval("require");return cached=runtimeRequire("msedge-tts"),cached}function shortName(e){const t=e.FriendlyName.replace(/^Microsoft\s+/i,"").replace(/\s+Online.*$/i,"").replace(/\s*-\s*Spanish.*$/i,"").trim();return t&&t!==e.ShortName?t:e.ShortName.replace(/^[a-z]{2}-[A-Z]{2}-/,"").replace(/Neural$/,"")}async function listEdgeVoices(){const{MsEdgeTTS:e}=load(),t=new e;try{return(await t.getVoices()).filter(s=>s.Locale.toLowerCase().startsWith("es-")).map(s=>({id:s.ShortName,label:`${s.Locale} · ${shortName(s)}`,locale:s.Locale,gender:s.Gender})).sort((s,o)=>s.label.localeCompare(o.label))}finally{t.close()}}async function synthesizeEdge(e){const{MsEdgeTTS:t,OUTPUT_FORMAT:n}=load(),s=new t;try{await s.setMetadata(e.voice,n.AUDIO_24KHZ_48KBITRATE_MONO_MP3);const{audioStream:o}=s.toStream(e.text,{rate:e.rate}),r=[];return await new Promise((d,a)=>{o.on("data",c=>r.push(Buffer.from(c))),o.on("end",()=>d(Buffer.concat(r).toString("base64"))),o.on("error",a)})}finally{s.close()}}const main=e=>{let t="idle",n="",s="",o,r=!1;const d=()=>{o=void 0,r=!1},a=e.provide(Dictation,{state:()=>({status:t,error:n,locale:s}),start(f,T){o&&(r=!0,o.stop(),d()),t="starting",n="",s=f.locale,a.changed();const m=T.window;try{o=startEngine({locale:f.locale,minConfidence:f.minConfidence},{onReady(w){t="listening",s=w,a.changed()},onFinal(w,v){a.emit("final",{text:w,confidence:v},m)},onError(w,v,$){t="error",n=w,a.changed(),a.emit("error",{code:w,detail:v,available:$},m)},onExit(w,v){if(r){d(),t="idle",n="",s="",a.changed();return}d(),t!=="error"&&(t="error",n=w===null?"ENGINE_EXIT":`ENGINE_EXIT_${w}`,a.changed(),a.emit("error",{code:"ENGINE_EXIT",detail:v||(w===null?"":String(w)),available:[]},m))}})}catch(w){throw t="error",n="ENGINE_START_FAILED",a.changed(),w}},stop(){o&&(r=!0,o.stop(),d()),t="idle",n="",s="",a.changed()},diagnose(){return diagnose()}});let c;const u=e.provide(Speech,{speak(f){c?.stop(),c=speakSapi(f,T=>{c=void 0,u.emit("done",{ok:T})})},stop(){c&&(c.stop(),c=void 0)},voices(){return listSapiVoices()},edgeVoices(){return listEdgeVoices()},edgeSpeak(f){return synthesizeEdge(f)}});e.provide(VoiceTyping,{toggle(){return sendVoiceTypingShortcut()}}),e.onCleanup(()=>{o&&(r=!0,o.stop(),d()),c?.stop(),c=void 0})},MAX_CHUNK=250;function cleanForSpeech(e,t){let n=e.replace(/\r\n/g,`
191
+ `);return t?n=n.replace(/```[\s\S]*?```/g," "):n=n.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),n=n.replace(/`([^`]+)`/g,"$1"),n=n.replace(/!\[[^\]]*\]\([^)]*\)/g," "),n=n.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),n=n.replace(/^\s*#{1,6}\s+/gm,""),n=n.replace(/^\s*>\s?/gm,""),n=n.replace(/^\s*[-*+]\s+/gm,""),n=n.replace(/^\s*\d+\.\s+/gm,""),n=n.replace(/(\*\*|__)(.*?)\1/g,"$2"),n=n.replace(/(\*|_)(.*?)\1/g,"$2"),n=n.replace(/^\s*\|.*\|\s*$/gm," "),n=n.replace(/\n{2,}/g,". "),n=n.replace(/\n/g," "),n=n.replace(/\s+/g," "),n=n.replace(/\s+([.,;:!?])/g,"$1"),n=n.replace(/([.,;:!?]){2,}/g,"$1"),n.trim()}function chunkForSpeech(e,t=MAX_CHUNK){const n=e.trim();if(!n)return[];const s=n.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let r="";const d=a=>{let c="";for(const u of a.split(/\s+/))u&&(c&&c.length+1+u.length>t?(o.push(c),c=u):c=c?`${c} ${u}`:u);return c};for(const a of s){if(a.length>t){r&&(o.push(r),r=""),r=d(a);continue}r&&r.length+1+a.length>t?(o.push(r),r=a):r=r?`${r} ${a}`:a}return r&&o.push(r),o}function createSpeaker(e,t){const n=e.stores.speech,s=e.stores.tts.value,[o,r]=solidJs.createSignal(s.engine==="edge"?"edge":"system"),[d,a]=solidJs.createSignal(s.voice),c=g=>{(g.engine==="edge"||g.engine==="system")&&r(g.engine),g.voice!==void 0&&a(g.voice)},[u,f]=solidJs.createSignal(!1),[T,m]=solidJs.createSignal(!1),[w,v]=solidJs.createSignal([]),[$,N]=solidJs.createSignal("none");let E=0,A=[],l="",p,b;const k=new Map,R=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,i=()=>{const g=R()?.getVoices()??[];if(g.length===0)return;const S=n.value.voiceURI;if(S){const C=g.find(L=>L.voiceURI===S);if(C)return C}const y=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return g.find(C=>C.lang.toLowerCase().startsWith(y))??g[0]},h=async()=>{if(o()==="edge"){try{const S=await t.edgeVoices();S.length>0&&v(S.map(y=>({id:y.id,label:y.label})))}catch{}N("edge");return}const g=R()?.getVoices()??[];if(g.length>0){v(g.map(S=>({id:S.voiceURI,label:`${S.name} — ${S.lang}`}))),N("web");return}try{const S=await t.voices();if(S.length>0){v(S.map(y=>({id:y,label:y}))),N("sapi");return}}catch{}N("none")},P=()=>{p&&(p.onended=null,p.onerror=null,p.pause(),p=void 0),b&&(URL.revokeObjectURL(b),b=void 0)},V=()=>{E++,A=[],R()?.cancel(),P(),m(!1),f(!1),$()==="sapi"&&t.stop().catch(()=>{})},D=()=>$()==="web"||$()==="edge",U=()=>{if(!(!u()||T()||!D())){if($()==="edge"){p?.pause(),m(!0);return}R()?.pause(),m(!0)}},M=()=>{if(!(!u()||!T()||!D())){if($()==="edge"){p?.play().catch(()=>{}),m(!1);return}R()?.resume(),m(!1)}},j=()=>{T()?M():U()},G=()=>l?H(l):Promise.resolve(!1),q=g=>{const S=d()||"es-ES-XimenaNeural",y=n.value.rate,C=chunkForSpeech(g,4e3),L=`${S}|${y}|${g}`,O=k.get(L)??[];k.clear(),k.set(L,O);const I=E;f(!0),m(!1);const W=_=>{if(I===E){if(_>=C.length){m(!1),f(!1);return}(async()=>{let F=O[_];try{F||(F=await t.edgeSpeak({text:C[_]??"",voice:S,rate:y}),O[_]=F)}catch{I===E&&(m(!1),f(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}));return}if(I!==E)return;const J=Uint8Array.from(atob(F),Z=>Z.charCodeAt(0)),B=URL.createObjectURL(new Blob([J],{type:"audio/mpeg"}));P(),b=B;const z=new Audio(B);p=z,z.onended=()=>{I===E&&(P(),W(_+1))},z.onerror=()=>{I===E&&(P(),m(!1),f(!1))},await z.play().catch(()=>{I===E&&(P(),m(!1),f(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}))})})()}};return W(0),!0},H=async g=>{l=g;const S=cleanForSpeech(g,n.value.skipCode);if(!S)return!1;if(V(),await h(),o()==="edge")return q(S);if($()==="web"){const y=R();if(!y)return!1;const C=i(),L=n.value.rate,O=E;A=chunkForSpeech(S),f(!0),m(!1);const I=()=>{if(O!==E)return;const W=A.shift();if(!W){m(!1),f(!1);return}const _=new SpeechSynthesisUtterance(W);C&&(_.voice=C),C?.lang&&(_.lang=C.lang),_.rate=L,_.onend=I,_.onerror=I,y.speak(_)};return I(),!0}if($()==="sapi"){f(!0),m(!1);try{await t.speak({text:S,rate:n.value.rate,voice:n.value.voiceURI})}catch(y){throw f(!1),y}return!0}throw f(!1),new Error(e.t("read.noVoices"))},K=async()=>{const g=e.sessions.current();if(!g)return"";const y=(await g.server.client.message.list({sessionID:g.id,type:"assistant",order:"desc",limit:1})).data[0];return!y||y.type!=="assistant"?"":y.content.filter(C=>C.type==="text").map(C=>C.text).join(`
192
+ `)};return t.on("done",()=>{m(!1),f(!1)}),R()?.addEventListener?.("voiceschanged",()=>{h()}),h(),{speaking:u,paused:T,backend:$,engine:o,voice:d,canPause:D,setConfig:c,voices:w,refreshVoices:h,lastAssistantText:K,speak:H,cancel:V,pause:U,resume:M,togglePause:j,restart:G,preview:async()=>{const g=await H(e.t("speech.preview.text"));if(g){const S=o()==="edge"?`Edge · ${d()}`:`${$()} · ${n.value.voiceURI||"auto"}`;e.toast.show({variant:"default",title:e.t("read.title"),description:S})}return g}}}const FALLBACK_LOCALES=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function VoiceSettings(e){const t=gui$1.usePlugin(),n=t.ui,s=t.stores.prefs,o=t.stores.speech,r=t.stores.tts,d=t.stores.engine,a=t.main(Dictation),c=t.main(VoiceTyping),[u,f]=solidJs.createSignal([]),[T,m]=solidJs.createSignal(!1);solidJs.onMount(()=>{e.speaker.setConfig({engine:r.value.engine,voice:r.value.voice}),a.diagnose().then(i=>{i.locales.length>0&&f(i.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const w=()=>{const i=u(),h=s.value.locale;return(i.includes(h)?i:[h,...i]).map(V=>({value:V,label:V}))},v=()=>{const i=e.speaker.voices().map(h=>({value:h.id,label:h.label}));return r.value.engine==="edge"?i:[{value:"",label:t.t("speech.voice.auto")},...i]},$=()=>r.value.engine==="edge"?r.value.voice:o.value.voiceURI,N=i=>{if(r.value.engine==="edge"){r.update(h=>{h.voice=i}),e.speaker.setConfig({voice:i});return}A({voiceURI:i})},E=i=>{s.update(h=>{i.locale!==void 0&&(h.locale=i.locale),i.minConfidence!==void 0&&(h.minConfidence=i.minConfidence),i.trailingSpace!==void 0&&(h.trailingSpace=i.trailingSpace),i.autoFocus!==void 0&&(h.autoFocus=i.autoFocus)})},A=i=>{o.update(h=>{i.voiceURI!==void 0&&(h.voiceURI=i.voiceURI),i.rate!==void 0&&(h.rate=i.rate),i.skipCode!==void 0&&(h.skipCode=i.skipCode)})},l=i=>{d.update(h=>{h.mode=i})},p=i=>{r.update(h=>{h.engine=i}),e.speaker.setConfig({engine:i}),e.speaker.refreshVoices()},b=async()=>{try{await c.toggle(),t.toast.show({variant:"default",title:t.t("settings.winh"),description:t.t("settings.winh.sent")})}catch(i){t.toast.show({variant:"error",title:t.t("pill.title"),description:i instanceof Error?i.message:t.t("error.winh")})}},k=async()=>{try{await e.speaker.preview()}catch(i){t.toast.show({variant:"error",title:t.t("read.title"),description:i instanceof Error?i.message:t.t("read.error")})}},R=async()=>{m(!0);try{const i=await a.diagnose();if(i.locales.length===0){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-recognizers")}),persistent:!0});return}if(!i.microphone){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-microphone")}),persistent:!0});return}i.locales.length>0&&!u().length&&f(i.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:i.selected||i.locales[0]||s.value.locale})})}catch(i){t.toast.show({variant:"error",title:t.t("error.title"),description:i instanceof Error?i.message:String(i)})}finally{m(!1)}};return web.createComponent(n.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[web.createComponent(n.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[web.createComponent(n.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("settings.engine")},get value(){return d.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:i=>l(i)})}}),web.createComponent(solidJs.Show,{get when(){return d.value.mode==="windows"},get children(){return web.createComponent(n.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return web.createComponent(n.Button,{onClick:()=>{b()},get children(){return t.t("settings.winh.run")}})}})}}),web.createComponent(solidJs.Show,{get when(){return d.value.mode==="system"},get children(){return[web.createComponent(n.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("settings.language")},get value(){return s.value.locale},get options(){return web.memo(()=>w().length>0)()?w():FALLBACK_LOCALES.map(i=>({value:i,label:i}))},onChange:i=>E({locale:i})})}}),web.createComponent(n.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("settings.confidence")},get value(){return String(s.value.minConfidence)},get options(){return[{value:"0.2",label:t.t("confidence.low")},{value:"0.4",label:t.t("confidence.normal")},{value:"0.6",label:t.t("confidence.high")}]},onChange:i=>E({minConfidence:Number(i)||.4})})}}),web.createComponent(n.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return web.createComponent(n.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return s.value.trailingSpace},onChange:i=>E({trailingSpace:i})})}}),web.createComponent(n.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return web.createComponent(n.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return s.value.autoFocus},onChange:i=>E({autoFocus:i})})}})]}})]}}),web.createComponent(n.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[web.createComponent(n.SettingsRow,{id:"tts-engine",get title(){return t.t("speech.engine")},get description(){return t.t("speech.engine.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("speech.engine")},get value(){return r.value.engine},get options(){return[{value:"edge",label:t.t("speech.engine.edge")},{value:"system",label:t.t("speech.engine.system")}]},onChange:i=>p(i)})}}),web.createComponent(n.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("speech.voice")},get value(){return $()},get options(){return v()},onChange:i=>N(i)})}}),web.createComponent(n.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return web.createComponent(n.Select,{get label(){return t.t("speech.rate")},get value(){return String(o.value.rate)},get options(){return[{value:"0.8",label:t.t("speech.rate.slow")},{value:"1",label:t.t("speech.rate.normal")},{value:"1.2",label:t.t("speech.rate.fast")},{value:"1.5",label:t.t("speech.rate.faster")}]},onChange:i=>A({rate:Number(i)||1})})}}),web.createComponent(n.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return web.createComponent(n.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:i=>A({skipCode:i})})}}),web.createComponent(n.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return web.createComponent(n.Button,{onClick:()=>{k()},get children(){return t.t("read.idle")}})}})]}}),web.createComponent(n.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return web.createComponent(n.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return web.createComponent(n.Button,{get busy(){return T()},onClick:()=>{R()},get children(){return t.t("settings.test.run")}})}})}})]}})}function ReadMenu(e){const t=gui$1.usePlugin();return web.memo(()=>e.actions().map(n=>web.createComponent(t.ui.Button,{get variant(){return n.id==="stop"?"danger":"neutral"},get icon(){return n.icon},onClick:()=>{n.run(),e.close()},get children(){return n.label}})))}const setup=e=>{const t=e.main(Dictation),n=e.main(Speech),s=e.main(VoiceTyping),o=createSpeaker(e,n),r=e.stores.prefs,d=e.stores.engine,[a,c]=solidJs.createSignal(!1);let u;const f=(l,p,b)=>{switch(l){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:p||r.value.locale,available:b.length>0?b.join(", "):"—"});case"NO_RECOGNIZERS":return e.t("error.none");case"MICROPHONE_UNAVAILABLE":return e.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return e.t("error.system");case"ENGINE_EXIT":return e.t("error.exit");default:return e.t("error.unknown",{code:l})}},T=()=>{a()&&(c(!1),s.toggle().catch(()=>{})),o.cancel(),u=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(l=>{e.toast.show({variant:"error",title:e.t("error.title"),description:l instanceof Error?l.message:String(l)})})},m=()=>{t.stop().catch(()=>{})},w=()=>{o.cancel(),e.screen.current()?.composer.focus(),s.toggle().then(()=>c(l=>!l)).catch(l=>{c(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:l instanceof Error?`${e.t("error.winh")} ${l.message}`:e.t("error.winh")})})},v=()=>{if(d.value.mode==="windows"){w();return}const l=t.state()?.status??"idle";l==="starting"||l==="listening"?m():T()},$=()=>{if(o.speaking()){o.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const l=t.state()?.status??"idle";(l==="starting"||l==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const p=await o.lastAssistantText();if(!p.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(p)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(p){e.toast.show({variant:"error",title:e.t("read.title"),description:p instanceof Error?p.message:e.t("read.error")})}})()},N=()=>{if(!o.speaking())return[{id:"start",label:e.t("read.idle"),icon:"outline-audio",run:$}];const l=o.paused(),p=[{id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{o.restart()}}];return o.canPause()&&p.push({id:"pause",label:l?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>o.togglePause()}),p.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",run:()=>o.cancel()}),p};t.on("final",l=>{const p=e.sessions.current()?.key;if(u&&p&&p!==u){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const b=e.screen.current()?.composer;if(!b)return;const k=b.text(),R=r.value.trailingSpace&&k.length>0&&!/\s$/.test(k)?" ":"";b.insert(R+l.text),r.value.autoFocus&&b.focus()}),t.on("error",l=>{e.toast.show({variant:"error",title:e.t("error.title"),description:f(l.code,l.detail,l.available),persistent:l.code==="LANGUAGE_NOT_AVAILABLE"||l.code==="NO_RECOGNIZERS",duration:8e3})});const E=()=>{if(e.app.platform!=="desktop")return;if(d.value.mode==="windows")return a()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:v}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:v};const l=t.state()?.status??"idle";return l==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:v}:l==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:v}:l==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:v}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:v}},A=()=>{if(e.app.platform!=="desktop")return;const l=o.speaking(),p=o.paused(),b=l?N().map(k=>({id:k.id,label:k.label,icon:k.icon,tone:k.id==="stop"?"danger":void 0,run:k.run})):[];return{label:l?p?e.t("read.paused"):e.t("read.speaking"):e.t("read.idle"),icon:"outline-audio",tone:l?"success":void 0,title:e.t("read.title"),rows:b.length>0?b:void 0,run:l?void 0:$}};e.add(gui$1.ComposerPill,()=>({id:"dictate",show:()=>E()})),e.add(gui$1.ComposerPill,()=>({id:"read",order:1,show:()=>A(),menu:l=>web.createComponent(ReadMenu,{actions:N,get close(){return l.close}})})),e.add(gui$1.Command,()=>({id:"toggle",title:e.t("command.toggle"),description:e.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:e.app.platform==="desktop",run:v})),e.add(gui$1.Command,()=>({id:"configure",title:e.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),enabled:e.app.platform==="desktop",run:()=>e.layout.settings(e.id)})),e.add(gui$1.Command,()=>({id:"read",title:e.t("command.read"),description:e.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:e.app.platform==="desktop",run:$})),e.add(gui$1.SettingsPage,{id:e.id,icon:"outline-audio",get title(){return e.t("settings.title")},get entries(){return[{id:"engine",title:e.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:e.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:e.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:e.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:e.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:e.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"tts-engine",title:e.t("speech.engine"),keywords:"voice engine motor edge neural sistema"},{id:"rate",title:e.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:e.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:e.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>web.createComponent(VoiceSettings,{speaker:o})})},gui=gui$1.Plugin.define({...definition,setup,main});exports.default=gui;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-voice-dictation",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Offline voice dictation and read-aloud for OpenCode Desktop on Windows",
5
5
  "license": "MIT",
6
6
  "author": "Fran Carlos <francarlos803@gmail.com>",
@@ -33,6 +33,10 @@
33
33
  "README.md",
34
34
  "LICENSE"
35
35
  ],
36
+ "dependencies": {
37
+ "msedge-tts": "^2.1.1",
38
+ "opencode-voice-dictation": "^0.2.0"
39
+ },
36
40
  "publishConfig": {
37
41
  "access": "public"
38
42
  },
@@ -51,8 +55,5 @@
51
55
  "typescript": "^5.9.0",
52
56
  "vite": "^7.3.0",
53
57
  "vite-plugin-solid": "^2.11.0"
54
- },
55
- "dependencies": {
56
- "opencode-voice-dictation": "^0.2.0"
57
58
  }
58
59
  }
@@ -69,6 +69,24 @@ export interface DiagnoseResult {
69
69
  voices: {
70
70
  output: Schema.Array(Schema.String),
71
71
  },
72
+ edgeVoices: {
73
+ output: Schema.Array(
74
+ Schema.Struct({
75
+ id: Schema.String,
76
+ label: Schema.String,
77
+ locale: Schema.String,
78
+ gender: Schema.String,
79
+ }),
80
+ ),
81
+ },
82
+ edgeSpeak: {
83
+ input: Schema.Struct({
84
+ text: Schema.String,
85
+ voice: Schema.String,
86
+ rate: Schema.Number,
87
+ }),
88
+ output: Schema.String,
89
+ },
72
90
  },
73
91
  events: {
74
92
  done: Schema.Struct({ ok: Schema.Boolean }),
@@ -45,6 +45,16 @@ export const definition = Plugin.define({
45
45
  mode: "windows",
46
46
  },
47
47
  ),
48
+ tts: Store.global(
49
+ Schema.Struct({
50
+ engine: Schema.String,
51
+ voice: Schema.String,
52
+ }),
53
+ {
54
+ engine: "edge",
55
+ voice: "es-ES-XimenaNeural",
56
+ },
57
+ ),
48
58
  },
49
59
  i18n: {
50
60
  en: {
@@ -113,8 +123,13 @@ export const definition = Plugin.define({
113
123
  "read.error": "The last message could not be read.",
114
124
  "read.noVoices": "Windows has no voices installed for reading.",
115
125
  "speech.section": "Read aloud",
126
+ "speech.engine": "Voice engine",
127
+ "speech.engine.description": "Edge neural sounds much more natural but needs internet. System uses the installed Windows voices, offline.",
128
+ "speech.engine.edge": "Edge neural (online)",
129
+ "speech.engine.system": "System (offline)",
130
+ "error.edge": "The Edge voice could not be used. Falling back to the system voice.",
116
131
  "speech.voice": "Voice",
117
- "speech.voice.description": "A system voice. Automatic picks one in the app language.",
132
+ "speech.voice.description": "The voice used for reading.",
118
133
  "speech.voice.auto": "Automatic",
119
134
  "speech.rate": "Speed",
120
135
  "speech.rate.description": "How fast the text is spoken.",
@@ -193,8 +208,13 @@ export const definition = Plugin.define({
193
208
  "read.error": "No se pudo leer el último mensaje.",
194
209
  "read.noVoices": "Windows no tiene voces instaladas para la lectura.",
195
210
  "speech.section": "Lectura en voz alta",
211
+ "speech.engine": "Motor de voz",
212
+ "speech.engine.description": "Edge neuronal suena mucho más natural pero necesita internet. Sistema usa las voces instaladas de Windows, sin conexión.",
213
+ "speech.engine.edge": "Edge neuronal (online)",
214
+ "speech.engine.system": "Sistema (offline)",
215
+ "error.edge": "No se pudo usar la voz de Edge. Se usará la voz del sistema.",
196
216
  "speech.voice": "Voz",
197
- "speech.voice.description": "Una voz del sistema. Automática elige una en el idioma de la app.",
217
+ "speech.voice.description": "La voz que se usa para leer.",
198
218
  "speech.voice.auto": "Automática",
199
219
  "speech.rate": "Velocidad",
200
220
  "speech.rate.description": "A qué ritmo se lee el texto.",
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Edge neural text-to-speech.
3
+ *
4
+ * `msedge-tts` is loaded dynamically so the renderer process never evaluates
5
+ * Node-only modules: only the Electron main process imports it.
6
+ */
7
+ type EdgeModule = typeof import("msedge-tts")
8
+
9
+ export interface EdgeVoiceInfo {
10
+ id: string
11
+ label: string
12
+ locale: string
13
+ gender: string
14
+ }
15
+
16
+ interface RawVoice {
17
+ ShortName: string
18
+ FriendlyName: string
19
+ Locale: string
20
+ Gender: string
21
+ }
22
+
23
+ let cached: EdgeModule | undefined
24
+
25
+ function load(): EdgeModule {
26
+ if (cached) return cached
27
+
28
+ // Node-only package: Vite must not bundle it. Resolve it at runtime in the
29
+ // Electron main process, where `require` is real.
30
+ const runtimeRequire = eval("require") as NodeRequire
31
+ cached = runtimeRequire("msedge-tts") as EdgeModule
32
+ return cached
33
+ }
34
+
35
+ function shortName(voice: RawVoice): string {
36
+ const friendly = voice.FriendlyName.replace(/^Microsoft\s+/i, "")
37
+ .replace(/\s+Online.*$/i, "")
38
+ .replace(/\s*-\s*Spanish.*$/i, "")
39
+ .trim()
40
+
41
+ if (friendly && friendly !== voice.ShortName) return friendly
42
+
43
+ return voice.ShortName.replace(/^[a-z]{2}-[A-Z]{2}-/, "").replace(/Neural$/, "")
44
+ }
45
+
46
+ export async function listEdgeVoices(): Promise<EdgeVoiceInfo[]> {
47
+ const { MsEdgeTTS } = load()
48
+ const tts = new MsEdgeTTS()
49
+
50
+ try {
51
+ const voices = (await tts.getVoices()) as RawVoice[]
52
+
53
+ return voices
54
+ .filter((voice) => voice.Locale.toLowerCase().startsWith("es-"))
55
+ .map((voice) => ({
56
+ id: voice.ShortName,
57
+ label: `${voice.Locale} · ${shortName(voice)}`,
58
+ locale: voice.Locale,
59
+ gender: voice.Gender,
60
+ }))
61
+ .sort((a, b) => a.label.localeCompare(b.label))
62
+ } finally {
63
+ tts.close()
64
+ }
65
+ }
66
+
67
+ export async function synthesizeEdge(input: {
68
+ text: string
69
+ voice: string
70
+ rate: number
71
+ }): Promise<string> {
72
+ const { MsEdgeTTS, OUTPUT_FORMAT } = load()
73
+ const tts = new MsEdgeTTS()
74
+
75
+ try {
76
+ await tts.setMetadata(input.voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3)
77
+
78
+ const { audioStream } = tts.toStream(input.text, { rate: input.rate })
79
+ const chunks: Buffer[] = []
80
+
81
+ return await new Promise<string>((resolve, reject) => {
82
+ audioStream.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
83
+ audioStream.on("end", () => resolve(Buffer.concat(chunks).toString("base64")))
84
+ audioStream.on("error", reject)
85
+ })
86
+ } finally {
87
+ tts.close()
88
+ }
89
+ }
package/src/gui/main.ts CHANGED
@@ -3,6 +3,7 @@ import { Dictation, Speech, VoiceTyping } from "./contract"
3
3
  import { diagnose, startEngine, type EngineHandle } from "./engines/system-speech"
4
4
  import { listSapiVoices, speakSapi, type SpeakHandle } from "./engines/sapi-speech"
5
5
  import { sendVoiceTypingShortcut } from "./engines/win-h"
6
+ import { listEdgeVoices, synthesizeEdge } from "./engines/edge-tts"
6
7
 
7
8
  type Status = "idle" | "starting" | "listening" | "error"
8
9
 
@@ -129,6 +130,14 @@ export const main: Plugin.MainSetup = (ctx) => {
129
130
  voices() {
130
131
  return listSapiVoices()
131
132
  },
133
+
134
+ edgeVoices() {
135
+ return listEdgeVoices()
136
+ },
137
+
138
+ edgeSpeak(input) {
139
+ return synthesizeEdge(input)
140
+ },
132
141
  })
133
142
 
134
143
  ctx.provide(VoiceTyping, {
@@ -27,6 +27,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
27
27
  const ui = ctx.ui
28
28
  const prefs = ctx.stores.prefs
29
29
  const speechPrefs = ctx.stores.speech
30
+ const ttsPrefs = ctx.stores.tts
30
31
  const enginePrefs = ctx.stores.engine
31
32
  const dictation = ctx.main(Dictation)
32
33
  const voiceTyping = ctx.main(VoiceTyping)
@@ -35,6 +36,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
35
36
  const [busy, setBusy] = createSignal(false)
36
37
 
37
38
  onMount(() => {
39
+ props.speaker.setConfig({ engine: ttsPrefs.value.engine, voice: ttsPrefs.value.voice })
38
40
  void dictation
39
41
  .diagnose()
40
42
  .then((result) => {
@@ -51,10 +53,25 @@ export function VoiceSettings(props: { speaker: Speaker }) {
51
53
  return merged.map((value) => ({ value, label: value }))
52
54
  }
53
55
 
54
- const voiceOptions = () => [
55
- { value: "", label: ctx.t("speech.voice.auto") },
56
- ...props.speaker.voices().map((voice) => ({ value: voice.id, label: voice.label })),
57
- ]
56
+ const voiceOptions = () => {
57
+ const list = props.speaker.voices().map((voice) => ({ value: voice.id, label: voice.label }))
58
+ if (ttsPrefs.value.engine === "edge") return list
59
+ return [{ value: "", label: ctx.t("speech.voice.auto") }, ...list]
60
+ }
61
+
62
+ const voiceValue = () =>
63
+ ttsPrefs.value.engine === "edge" ? ttsPrefs.value.voice : speechPrefs.value.voiceURI
64
+
65
+ const selectVoice = (value: string) => {
66
+ if (ttsPrefs.value.engine === "edge") {
67
+ ttsPrefs.update((draft) => {
68
+ draft.voice = value
69
+ })
70
+ props.speaker.setConfig({ voice: value })
71
+ return
72
+ }
73
+ patchSpeech({ voiceURI: value })
74
+ }
58
75
 
59
76
  const patch = (change: { locale?: string; minConfidence?: number; trailingSpace?: boolean; autoFocus?: boolean }) => {
60
77
  prefs.update((draft) => {
@@ -79,6 +96,14 @@ export function VoiceSettings(props: { speaker: Speaker }) {
79
96
  })
80
97
  }
81
98
 
99
+ const patchTts = (engine: string) => {
100
+ ttsPrefs.update((draft) => {
101
+ draft.engine = engine
102
+ })
103
+ props.speaker.setConfig({ engine })
104
+ void props.speaker.refreshVoices()
105
+ }
106
+
82
107
  const testWinH = async () => {
83
108
  try {
84
109
  await voiceTyping.toggle()
@@ -232,6 +257,22 @@ export function VoiceSettings(props: { speaker: Speaker }) {
232
257
  </ui.SettingsSection>
233
258
 
234
259
  <ui.SettingsSection title={ctx.t("speech.section")}>
260
+ <ui.SettingsRow
261
+ id="tts-engine"
262
+ title={ctx.t("speech.engine")}
263
+ description={ctx.t("speech.engine.description")}
264
+ >
265
+ <ui.Select
266
+ label={ctx.t("speech.engine")}
267
+ value={ttsPrefs.value.engine}
268
+ options={[
269
+ { value: "edge", label: ctx.t("speech.engine.edge") },
270
+ { value: "system", label: ctx.t("speech.engine.system") },
271
+ ]}
272
+ onChange={(value) => patchTts(value)}
273
+ />
274
+ </ui.SettingsRow>
275
+
235
276
  <ui.SettingsRow
236
277
  id="voice"
237
278
  title={ctx.t("speech.voice")}
@@ -239,9 +280,9 @@ export function VoiceSettings(props: { speaker: Speaker }) {
239
280
  >
240
281
  <ui.Select
241
282
  label={ctx.t("speech.voice")}
242
- value={speechPrefs.value.voiceURI}
283
+ value={voiceValue()}
243
284
  options={voiceOptions()}
244
- onChange={(value) => patchSpeech({ voiceURI: value })}
285
+ onChange={(value) => selectVoice(value)}
245
286
  />
246
287
  </ui.SettingsRow>
247
288
 
package/src/gui/speak.ts CHANGED
@@ -11,8 +11,11 @@ export interface VoiceOption {
11
11
  export interface Speaker {
12
12
  speaking(): boolean
13
13
  paused(): boolean
14
- backend(): "web" | "sapi" | "none"
14
+ backend(): "web" | "sapi" | "edge" | "none"
15
+ engine(): "edge" | "system"
16
+ voice(): string
15
17
  canPause(): boolean
18
+ setConfig(input: { engine?: string; voice?: string }): void
16
19
  voices(): VoiceOption[]
17
20
  refreshVoices(): Promise<void>
18
21
  lastAssistantText(): Promise<string>
@@ -106,15 +109,27 @@ export function createSpeaker(
106
109
  speech: IpcClient<typeof Speech.spec>,
107
110
  ): Speaker {
108
111
  const prefs = ctx.stores.speech
112
+ const initial = ctx.stores.tts.value
113
+
114
+ const [engine, setEngine] = createSignal<"edge" | "system">(initial.engine === "edge" ? "edge" : "system")
115
+ const [voice, setVoice] = createSignal(initial.voice)
116
+
117
+ const setConfig = (input: { engine?: string; voice?: string }) => {
118
+ if (input.engine === "edge" || input.engine === "system") setEngine(input.engine)
119
+ if (input.voice !== undefined) setVoice(input.voice)
120
+ }
109
121
 
110
122
  const [speaking, setSpeaking] = createSignal(false)
111
123
  const [paused, setPaused] = createSignal(false)
112
124
  const [voices, setVoices] = createSignal<VoiceOption[]>([])
113
- const [backend, setBackend] = createSignal<"web" | "sapi" | "none">("none")
125
+ const [backend, setBackend] = createSignal<"web" | "sapi" | "edge" | "none">("none")
114
126
 
115
127
  let generation = 0
116
128
  let queue: string[] = []
117
129
  let lastText = ""
130
+ let currentAudio: HTMLAudioElement | undefined
131
+ let currentUrl: string | undefined
132
+ const edgeCache = new Map<string, string[]>()
118
133
 
119
134
  const synth = () =>
120
135
  typeof globalThis.speechSynthesis !== "undefined" ? globalThis.speechSynthesis : undefined
@@ -134,6 +149,20 @@ export function createSpeaker(
134
149
  }
135
150
 
136
151
  const refreshVoices = async () => {
152
+ if (engine() === "edge") {
153
+ try {
154
+ const list = await speech.edgeVoices()
155
+ if (list.length > 0) {
156
+ setVoices(list.map((voice) => ({ id: voice.id, label: voice.label })))
157
+ }
158
+ } catch {
159
+ // Keep the current list. The error surfaces when speaking, never as a
160
+ // silent switch to the system voice.
161
+ }
162
+ setBackend("edge")
163
+ return
164
+ }
165
+
137
166
  const available = synth()?.getVoices() ?? []
138
167
 
139
168
  if (available.length > 0) {
@@ -161,25 +190,49 @@ export function createSpeaker(
161
190
  setBackend("none")
162
191
  }
163
192
 
193
+ const stopAudio = () => {
194
+ if (currentAudio) {
195
+ currentAudio.onended = null
196
+ currentAudio.onerror = null
197
+ currentAudio.pause()
198
+ currentAudio = undefined
199
+ }
200
+ if (currentUrl) {
201
+ URL.revokeObjectURL(currentUrl)
202
+ currentUrl = undefined
203
+ }
204
+ }
205
+
164
206
  const cancel = () => {
165
207
  generation++
166
208
  queue = []
167
209
  synth()?.cancel()
210
+ stopAudio()
168
211
  setPaused(false)
169
212
  setSpeaking(false)
170
213
  if (backend() === "sapi") void speech.stop().catch(() => {})
171
214
  }
172
215
 
173
- const canPause = () => backend() === "web"
216
+ const canPause = () => backend() === "web" || backend() === "edge"
174
217
 
175
218
  const pause = () => {
176
219
  if (!speaking() || paused() || !canPause()) return
220
+ if (backend() === "edge") {
221
+ currentAudio?.pause()
222
+ setPaused(true)
223
+ return
224
+ }
177
225
  synth()?.pause()
178
226
  setPaused(true)
179
227
  }
180
228
 
181
229
  const resume = () => {
182
230
  if (!speaking() || !paused() || !canPause()) return
231
+ if (backend() === "edge") {
232
+ void currentAudio?.play().catch(() => {})
233
+ setPaused(false)
234
+ return
235
+ }
183
236
  synth()?.resume()
184
237
  setPaused(false)
185
238
  }
@@ -194,6 +247,89 @@ export function createSpeaker(
194
247
  return speak(lastText)
195
248
  }
196
249
 
250
+ const speakEdge = (cleaned: string) => {
251
+ const selected = voice() || "es-ES-XimenaNeural"
252
+ const rate = prefs.value.rate
253
+ const blocks = chunkForSpeech(cleaned, 4000)
254
+ const key = `${selected}|${rate}|${cleaned}`
255
+ const cached = edgeCache.get(key) ?? []
256
+
257
+ edgeCache.clear()
258
+ edgeCache.set(key, cached)
259
+
260
+ const gen = generation
261
+ setSpeaking(true)
262
+ setPaused(false)
263
+
264
+ const play = (index: number) => {
265
+ if (gen !== generation) return
266
+ if (index >= blocks.length) {
267
+ setPaused(false)
268
+ setSpeaking(false)
269
+ return
270
+ }
271
+
272
+ void (async () => {
273
+ let encoded = cached[index]
274
+
275
+ try {
276
+ if (!encoded) {
277
+ encoded = await speech.edgeSpeak({ text: blocks[index] ?? "", voice: selected, rate })
278
+ cached[index] = encoded
279
+ }
280
+ } catch {
281
+ if (gen === generation) {
282
+ setPaused(false)
283
+ setSpeaking(false)
284
+ ctx.toast.show({
285
+ variant: "error",
286
+ title: ctx.t("read.title"),
287
+ description: ctx.t("error.edge"),
288
+ })
289
+ }
290
+ return
291
+ }
292
+
293
+ if (gen !== generation) return
294
+
295
+ const bytes = Uint8Array.from(atob(encoded), (char) => char.charCodeAt(0))
296
+ const url = URL.createObjectURL(new Blob([bytes], { type: "audio/mpeg" }))
297
+
298
+ stopAudio()
299
+ currentUrl = url
300
+
301
+ const audio = new Audio(url)
302
+ currentAudio = audio
303
+ audio.onended = () => {
304
+ if (gen !== generation) return
305
+ stopAudio()
306
+ play(index + 1)
307
+ }
308
+ audio.onerror = () => {
309
+ if (gen !== generation) return
310
+ stopAudio()
311
+ setPaused(false)
312
+ setSpeaking(false)
313
+ }
314
+
315
+ await audio.play().catch(() => {
316
+ if (gen !== generation) return
317
+ stopAudio()
318
+ setPaused(false)
319
+ setSpeaking(false)
320
+ ctx.toast.show({
321
+ variant: "error",
322
+ title: ctx.t("read.title"),
323
+ description: ctx.t("error.edge"),
324
+ })
325
+ })
326
+ })()
327
+ }
328
+
329
+ play(0)
330
+ return true
331
+ }
332
+
197
333
  const speak = async (text: string) => {
198
334
  lastText = text
199
335
  const cleaned = cleanForSpeech(text, prefs.value.skipCode)
@@ -202,6 +338,9 @@ export function createSpeaker(
202
338
  cancel()
203
339
  await refreshVoices()
204
340
 
341
+ if (engine() === "edge") {
342
+ return speakEdge(cleaned)
343
+ }
205
344
  if (backend() === "web") {
206
345
  const engine = synth()
207
346
  if (!engine) return false
@@ -283,15 +422,30 @@ export function createSpeaker(
283
422
  setSpeaking(false)
284
423
  })
285
424
 
286
- const engine = synth()
287
- engine?.addEventListener?.("voiceschanged", () => void refreshVoices())
425
+ const synthEngine = synth()
426
+ synthEngine?.addEventListener?.("voiceschanged", () => void refreshVoices())
288
427
  void refreshVoices()
289
428
 
429
+ const preview = async () => {
430
+ const started = await speak(ctx.t("speech.preview.text"))
431
+ if (started) {
432
+ const detail =
433
+ engine() === "edge"
434
+ ? `Edge · ${voice()}`
435
+ : `${backend()} · ${prefs.value.voiceURI || "auto"}`
436
+ ctx.toast.show({ variant: "default", title: ctx.t("read.title"), description: detail })
437
+ }
438
+ return started
439
+ }
440
+
290
441
  return {
291
442
  speaking,
292
443
  paused,
293
444
  backend,
445
+ engine,
446
+ voice,
294
447
  canPause,
448
+ setConfig,
295
449
  voices,
296
450
  refreshVoices,
297
451
  lastAssistantText,
@@ -301,6 +455,6 @@ export function createSpeaker(
301
455
  resume,
302
456
  togglePause,
303
457
  restart,
304
- preview: () => speak(ctx.t("speech.preview.text")),
458
+ preview,
305
459
  }
306
460
  }
@@ -349,6 +349,7 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
349
349
  { id: "confidence", title: ctx.t("settings.confidence"), keywords: "voice confidence confianza" },
350
350
  { id: "test", title: ctx.t("settings.test"), keywords: "voice microphone microphone test microfono" },
351
351
  { id: "voice", title: ctx.t("speech.voice"), keywords: "read aloud voice voz lectura altavoz" },
352
+ { id: "tts-engine", title: ctx.t("speech.engine"), keywords: "voice engine motor edge neural sistema" },
352
353
  { id: "rate", title: ctx.t("speech.rate"), keywords: "read aloud speed velocidad lectura" },
353
354
  { id: "skip-code", title: ctx.t("speech.skipCode"), keywords: "read aloud code codigo lectura" },
354
355
  { id: "preview", title: ctx.t("speech.preview"), keywords: "read aloud test voice probar voz" },