opencode-voice-dictation 0.1.0 → 0.2.0

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
@@ -3,11 +3,15 @@
3
3
  Plugin de [OpenCode](https://opencode.ai) V2 que añade voz a la app de
4
4
  escritorio:
5
5
 
6
- - **Dictado**: pulsas la píldora, hablas y el texto aparece en el prompt.
6
+ - **Dictado**: pulsas la píldora y se abre el dictado de Windows 11 (Win+H),
7
+ que escribe en el prompt con su motor neuronal. También puedes usar el motor
8
+ offline `System.Speech` desde Ajustes.
7
9
  - **Lectura**: pulsa «Leer» y reproduce en voz alta la última respuesta.
8
10
 
9
- - **Dictado offline**: usa `System.Speech` de Windows, sin API keys ni internet.
10
- - **Sin portapapeles**: escribe con `composer.insert()` en la posición del
11
+ - **Dictado preciso**: por defecto delega en Windows Voice Typing (Win+H).
12
+ - **Modo offline**: `System.Speech` sigue disponible como alternativa sin
13
+ conexión y sin depender del dictado de Windows.
14
+ - **Sin portapapeles**: la lectura usa `composer.insert()` en la posición del
11
15
  cursor, sin robar el foco ni simular `Ctrl+V`.
12
16
  - **Lectura local**: voces del sistema (`speechSynthesis`), con respaldo a
13
17
  `System.Speech.Synthesis` si Electron no expone voces.
@@ -60,6 +64,11 @@ experimental (`gui-experimental`) y puede cambiar entre versiones de OpenCode.
60
64
  El motor vive en el proceso `main` de Electron porque necesita
61
65
  `node:child_process`. La ventana solo pinta el estado y escribe el texto.
62
66
 
67
+ Con el motor por defecto (`Windows Voice Typing`) el flujo es más corto: la
68
+ píldora enfoca el composer, el proceso `main` envía `Win+H` con `keybd_event`, y
69
+ Windows escribe directamente en el campo. El plugin no transcribe ni toca el
70
+ texto.
71
+
63
72
  ## Requisitos
64
73
 
65
74
  - OpenCode Desktop en **Windows 10/11**.
@@ -131,7 +140,8 @@ en cada cambio y OpenCode Desktop recarga el plugin solo.
131
140
  | Ajustes | Ajustes → Dictado por voz, o paleta: "Ajustes de dictado por voz" |
132
141
 
133
142
  La píldora del micrófono muestra el estado: `Dictar`, `Iniciando…`,
134
- `Escuchando…` (icono en rojo) y `Reintentar dictado` si algo falla.
143
+ `Escuchando…` (System.Speech), `Dictando…` (Win+H) y `Reintentar dictado` si
144
+ algo falla.
135
145
 
136
146
  La píldora «Leer» cambia a `Leyendo…`; un segundo clic detiene la lectura.
137
147
  Dictar detiene la lectura y leer detiene el dictado, para que el micrófono no
@@ -144,11 +154,13 @@ espacio, se añade uno de separación (configurable).
144
154
 
145
155
  | Ajuste | Por defecto | Qué hace |
146
156
  |---|---|---|
147
- | Idioma | `es-ES` | Reconocedor instalado en Windows. Si no existe el exacto, usa el mismo idioma base. |
148
- | Confianza mínima | 0,4 | Descarta resultados con menor confianza para evitar ruido. |
149
- | Espacio antes del texto | Sí | Separa el dictado de lo que ya hay escrito. |
150
- | Enfocar el prompt al terminar | Sí | Mueve el cursor al prompt tras cada frase. |
151
- | Probar micrófono | — | Lista los idiomas instalados y comprueba el micrófono. |
157
+ | Motor de dictado | Windows Voice Typing (Win+H) | Usa el dictado de Windows 11, más preciso. `System.Speech` es la alternativa offline. |
158
+ | Probar Win+H | — | Envía el atajo y comprueba que Windows abre su barra de dictado. |
159
+ | Idioma | `es-ES` | Solo para `System.Speech`. Reconocedor instalado en Windows. Si no existe el exacto, usa el mismo idioma base. |
160
+ | Confianza mínima | 0,4 | Solo para `System.Speech`. Descarta resultados con menor confianza. |
161
+ | Espacio antes del texto | Sí | Solo para `System.Speech`. Separa el dictado de lo que ya hay escrito. |
162
+ | Enfocar el prompt al terminar | Sí | Solo para `System.Speech`. Mueve el cursor al prompt tras cada frase. |
163
+ | Probar micrófono | — | Solo para `System.Speech`. Lista los idiomas instalados y comprueba el micrófono. |
152
164
 
153
165
  ### Lectura en voz alta
154
166
 
@@ -180,7 +192,10 @@ bloqueas el micrófono, verás el aviso «No hay ningún micrófono disponible»
180
192
 
181
193
  | Síntoma | Causa / solución |
182
194
  |---|---|
183
- | «El idioma de reconocimiento es-ES no está instalado» | Instala el paquete de **reconocimiento** de voz, no solo el de visualización. El aviso muestra los idiomas disponibles. |
195
+ | Al pulsar «Dictar» no aparece la barra de Windows | Abre Ajustes → Dictado por voz → **Probar Win+H**. Si tampoco aparece, revisa Configuración → Accesibilidad → Dictado por voz, y que Kaspersky no esté bloqueando la combinación sintética. |
196
+ | Win+H se abre pero no escribe en el prompt | Haz clic en el cuadro de texto antes de dictar; el plugin enfoca el composer, pero otra ventana pudo robar el foco. |
197
+ | Quiero dictado sin Win+H | Cambia **Motor de dictado** a `System.Speech (offline)`. |
198
+ | «El idioma de reconocimiento es-ES no está instalado» | Solo afecta a `System.Speech`. Instala el paquete de **reconocimiento** de voz, no solo el de visualización. |
184
199
  | «Windows no tiene ningún reconocedor de voz instalado» | Mismo caso: no hay ningún idioma de voz instalado. |
185
200
  | «No hay ningún micrófono disponible» | Revisa Configuración → Sistema → Sonido → Entrada y el micrófono predeterminado. |
186
201
  | El motor se detiene inesperadamente | Otro proceso puede estar usando el micrófono en modo exclusivo. Ejecuta la prueba de micrófono en Ajustes. |
@@ -199,7 +214,12 @@ de la app muestran los más importantes.
199
214
  cambiar entre versiones de OpenCode.
200
215
  - `System.Speech` no añade puntuación ni mayúsculas automáticamente y su
201
216
  precisión es inferior a Whisper o al dictado nativo de Windows 11.
202
- - No hay resultados parciales: el texto aparece al terminar cada frase.
217
+ - El modo Win+H envía la combinación con `keybd_event`. Kaspersky o la
218
+ protección UIPI de Windows pueden bloquearla; en ese caso usa `System.Speech`.
219
+ - Con Win+H el texto lo escribe Windows en el campo enfocado: el plugin no
220
+ puede descartarlo si cambias de sesión ni limpiarlo.
221
+ - No hay resultados parciales con `System.Speech`: el texto aparece al terminar
222
+ cada frase.
203
223
  - La lectura usa las voces del sistema; si Electron no las expone, el respaldo
204
224
  por PowerShell usa las voces SAPI, con una latencia de arranque de ~0,5 s.
205
225
  - La lectura empieza con el mensaje tal como esté en ese momento: si el agente
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const b=require("@opencode/plugin/gui"),d=require("effect"),A=require("node:child_process"),z=require("node:crypto"),R=require("node:fs"),D=require("node:os"),T=require("node:path"),m=require("solid-js/web"),N=require("solid-js"),X=b.Plugin.define({os:["windows"],stores:{prefs:b.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:b.Store.global(d.Schema.Struct({voiceURI:d.Schema.String,rate:d.Schema.Number,skipCode:d.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0})},i18n:{en:{"pill.idle":"Dictate","pill.starting":"Starting…","pill.listening":"Listening…","pill.error":"Retry dictation","pill.title":"Voice dictation","pill.stop":"Stop dictation","command.toggle":"Toggle voice dictation","command.toggle.description":"Transcribe what you say into the prompt","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.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.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.error":"Reintentar dictado","pill.title":"Dictado por voz","pill.stop":"Detener dictado","command.toggle":"Alternar dictado por voz","command.toggle.description":"Transcribe lo que dices en el prompt","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.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.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."}}}),O=b.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)})}}),V=b.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})}}),F=String.raw`param(
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const w=require("@opencode/plugin/gui"),c=require("effect"),R=require("node:child_process"),_=require("node:crypto"),k=require("node:fs"),W=require("node:os"),I=require("node:path"),g=require("solid-js/web"),C=require("solid-js"),X=w.Plugin.define({os:["windows"],stores:{prefs:w.Store.global(c.Schema.Struct({locale:c.Schema.String,minConfidence:c.Schema.Number,trailingSpace:c.Schema.Boolean,autoFocus:c.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:w.Store.global(c.Schema.Struct({voiceURI:c.Schema.String,rate:c.Schema.Number,skipCode:c.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:w.Store.global(c.Schema.Struct({mode:c.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.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.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."}}}),z=w.Ipc.define({id:"voice-dictation.dictation",state:c.Schema.Struct({status:c.Schema.Literals(["idle","starting","listening","error"]),error:c.Schema.String,locale:c.Schema.String}),methods:{start:{input:c.Schema.Struct({locale:c.Schema.String,minConfidence:c.Schema.Number})},stop:{},diagnose:{output:c.Schema.Struct({locales:c.Schema.Array(c.Schema.String),microphone:c.Schema.Boolean,selected:c.Schema.String,error:c.Schema.String})}},events:{final:c.Schema.Struct({text:c.Schema.String,confidence:c.Schema.Number}),error:c.Schema.Struct({code:c.Schema.String,detail:c.Schema.String,available:c.Schema.Array(c.Schema.String)})}}),U=w.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:c.Schema.Struct({text:c.Schema.String,rate:c.Schema.Number,voice:c.Schema.String})},stop:{},voices:{output:c.Schema.Array(c.Schema.String)}},events:{done:c.Schema.Struct({ok:c.Schema.Boolean})}}),O=w.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),F=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
- `,J="powershell.exe";function U(){const e=T.join(D.tmpdir(),"opencode-voice-dictation");R.mkdirSync(e,{recursive:!0});const t=z.createHash("sha1").update(F).digest("hex").slice(0,12),i=T.join(e,`dictation-${t}.ps1`);return R.writeFileSync(i,F,"utf8"),i}function j(e){return A.spawn(J,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function B(e){const t=e.pid;if(t===void 0){e.kill();return}A.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function K(e,t){const i=j(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",U(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let c=!1,r="",s="";const g=o=>{switch(o.type){case"ready":t.onReady(o.locale??e.locale);break;case"final":o.text&&t.onFinal(o.text,o.confidence??0);break;case"error":t.onError(o.code??"UNKNOWN",o.detail??"",o.available??[]);break}};return i.stdout?.setEncoding("utf8"),i.stdout?.on("data",o=>{s+=o;let a=s.indexOf(`
109
- `);for(;a>=0;){const l=s.slice(0,a).replace(/\r$/,"").trim();if(s=s.slice(a+1),l)try{g(JSON.parse(l))}catch{}a=s.indexOf(`
110
- `)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",o=>{r=(r+o).slice(-2e3)}),i.on("error",o=>{c||t.onError("ENGINE_START_FAILED",o.message,[])}),i.on("close",o=>{c||t.onExit(o,r.trim().slice(-500))}),{stop(){c=!0,B(i)}}}function Z(){return new Promise((e,t)=>{const i=j(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",U(),"-Diagnose"]);let c="",r="",s=!1;const g=(a,l)=>{s||(s=!0,clearTimeout(o),a?t(a):e(l))},o=setTimeout(()=>{B(i),g(new Error("diagnose timed out"))},2e4);i.stdout?.setEncoding("utf8"),i.stdout?.on("data",a=>{c+=a}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",a=>{r+=a}),i.on("error",a=>g(a)),i.on("close",()=>{const a=c.split(/\r?\n/).map(l=>l.trim()).filter(l=>l.startsWith("{")).at(-1);if(!a){g(new Error(r.trim()||"no output from PowerShell"));return}try{const l=JSON.parse(a);g(void 0,{locales:l.locales??[],microphone:l.microphone===!0,selected:l.selected??"",error:l.error??""})}catch(l){g(l instanceof Error?l:new Error(String(l)))}})})}const P=String.raw`param(
108
+ `,Y="powershell.exe";function B(){const e=I.join(W.tmpdir(),"opencode-voice-dictation");k.mkdirSync(e,{recursive:!0});const t=_.createHash("sha1").update(F).digest("hex").slice(0,12),i=I.join(e,`dictation-${t}.ps1`);return k.writeFileSync(i,F,"utf8"),i}function j(e){return R.spawn(Y,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function M(e){const t=e.pid;if(t===void 0){e.kill();return}R.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function J(e,t){const i=j(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",B(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let l=!1,o="",s="";const p=r=>{switch(r.type){case"ready":t.onReady(r.locale??e.locale);break;case"final":r.text&&t.onFinal(r.text,r.confidence??0);break;case"error":t.onError(r.code??"UNKNOWN",r.detail??"",r.available??[]);break}};return i.stdout?.setEncoding("utf8"),i.stdout?.on("data",r=>{s+=r;let a=s.indexOf(`
109
+ `);for(;a>=0;){const d=s.slice(0,a).replace(/\r$/,"").trim();if(s=s.slice(a+1),d)try{p(JSON.parse(d))}catch{}a=s.indexOf(`
110
+ `)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",r=>{o=(o+r).slice(-2e3)}),i.on("error",r=>{l||t.onError("ENGINE_START_FAILED",r.message,[])}),i.on("close",r=>{l||t.onExit(r,o.trim().slice(-500))}),{stop(){l=!0,M(i)}}}function Q(){return new Promise((e,t)=>{const i=j(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",B(),"-Diagnose"]);let l="",o="",s=!1;const p=(a,d)=>{s||(s=!0,clearTimeout(r),a?t(a):e(d))},r=setTimeout(()=>{M(i),p(new Error("diagnose timed out"))},2e4);i.stdout?.setEncoding("utf8"),i.stdout?.on("data",a=>{l+=a}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",a=>{o+=a}),i.on("error",a=>p(a)),i.on("close",()=>{const a=l.split(/\r?\n/).map(d=>d.trim()).filter(d=>d.startsWith("{")).at(-1);if(!a){p(new Error(o.trim()||"no output from PowerShell"));return}try{const d=JSON.parse(a);p(void 0,{locales:d.locales??[],microphone:d.microphone===!0,selected:d.selected??"",error:d.error??""})}catch(d){p(d instanceof Error?d:new Error(String(d)))}})})}const D=String.raw`param(
111
111
  [string]$TextFile = "",
112
112
  [int]$Rate = 0,
113
113
  [string]$Voice = "",
@@ -160,6 +160,33 @@ try {
160
160
  } catch { }
161
161
 
162
162
  $synth.Dispose()
163
- `,Y="powershell.exe";function G(){const e=T.join(D.tmpdir(),"opencode-voice-dictation");return R.mkdirSync(e,{recursive:!0}),e}function M(){const e=z.createHash("sha1").update(P).digest("hex").slice(0,12),t=T.join(G(),`speak-${e}.ps1`);return R.writeFileSync(t,P,"utf8"),t}function Q(e){const t=T.join(G(),`speech-${z.randomUUID()}.txt`);return R.writeFileSync(t,e,"utf8"),t}function q(e){return A.spawn(Y,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function H(e){const t=e.pid;if(t===void 0){e.kill();return}A.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function x(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function ee(e,t){const i=Q(e.text),c=q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",M(),"-TextFile",i,"-Rate",String(x(e.rate)),"-Voice",e.voice]);let r=!1;const s=()=>{try{R.rmSync(i,{force:!0})}catch{}};return c.on("error",()=>{s(),r||t(!1)}),c.on("close",g=>{s(),r||t(g===0)}),{stop(){r=!0,H(c),s()}}}function te(){return new Promise(e=>{const t=q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",M(),"-ListVoices"]);let i="",c=!1;const r=g=>{c||(c=!0,clearTimeout(s),e(g))},s=setTimeout(()=>{H(t),r([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",g=>{i+=g}),t.on("error",()=>r([])),t.on("close",()=>{const g=i.split(/\r?\n/).map(o=>o.trim()).filter(o=>o.length>0);r(g)})})}const ie=e=>{let t="idle",i="",c="",r,s=!1;const g=()=>{r=void 0,s=!1},o=e.provide(O,{state:()=>({status:t,error:i,locale:c}),start(S,w){r&&(s=!0,r.stop(),g()),t="starting",i="",c=S.locale,o.changed();const p=w.window;try{r=K({locale:S.locale,minConfidence:S.minConfidence},{onReady(u){t="listening",c=u,o.changed()},onFinal(u,h){o.emit("final",{text:u,confidence:h},p)},onError(u,h,C){t="error",i=u,o.changed(),o.emit("error",{code:u,detail:h,available:C},p)},onExit(u,h){if(s){g(),t="idle",i="",c="",o.changed();return}g(),t!=="error"&&(t="error",i=u===null?"ENGINE_EXIT":`ENGINE_EXIT_${u}`,o.changed(),o.emit("error",{code:"ENGINE_EXIT",detail:h||(u===null?"":String(u)),available:[]},p))}})}catch(u){throw t="error",i="ENGINE_START_FAILED",o.changed(),u}},stop(){r&&(s=!0,r.stop(),g()),t="idle",i="",c="",o.changed()},diagnose(){return Z()}});let a;const l=e.provide(V,{speak(S){a?.stop(),a=ee(S,w=>{a=void 0,l.emit("done",{ok:w})})},stop(){a&&(a.stop(),a=void 0),l.emit("done",{ok:!0})},voices(){return te()}});e.onCleanup(()=>{r&&(s=!0,r.stop(),g()),a?.stop(),a=void 0})},ne=250;function oe(e,t){let i=e.replace(/\r\n/g,`
164
- `);return t?i=i.replace(/```[\s\S]*?```/g," "):i=i.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),i=i.replace(/`([^`]+)`/g,"$1"),i=i.replace(/!\[[^\]]*\]\([^)]*\)/g," "),i=i.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),i=i.replace(/^\s*#{1,6}\s+/gm,""),i=i.replace(/^\s*>\s?/gm,""),i=i.replace(/^\s*[-*+]\s+/gm,""),i=i.replace(/^\s*\d+\.\s+/gm,""),i=i.replace(/(\*\*|__)(.*?)\1/g,"$2"),i=i.replace(/(\*|_)(.*?)\1/g,"$2"),i=i.replace(/^\s*\|.*\|\s*$/gm," "),i=i.replace(/\n{2,}/g,". "),i=i.replace(/\n/g," "),i=i.replace(/\s+/g," "),i=i.replace(/\s+([.,;:!?])/g,"$1"),i=i.replace(/([.,;:!?]){2,}/g,"$1"),i.trim()}function re(e,t=ne){const i=e.trim();if(!i)return[];const c=i.split(new RegExp("(?<=[.!?…])\\s+")),r=[];let s="";const g=o=>{let a="";for(const l of o.split(/\s+/))l&&(a&&a.length+1+l.length>t?(r.push(a),a=l):a=a?`${a} ${l}`:l);return a};for(const o of c){if(o.length>t){s&&(r.push(s),s=""),s=g(o);continue}s&&s.length+1+o.length>t?(r.push(s),s=o):s=s?`${s} ${o}`:o}return s&&r.push(s),r}function se(e,t){const i=e.stores.speech,[c,r]=N.createSignal(!1),[s,g]=N.createSignal([]),[o,a]=N.createSignal("none");let l=0,S=[];const w=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,p=()=>{const v=w()?.getVoices()??[];if(v.length===0)return;const f=i.value.voiceURI;if(f){const $=v.find(I=>I.voiceURI===f);if($)return $}const y=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return v.find($=>$.lang.toLowerCase().startsWith(y))??v[0]},u=async()=>{const v=w()?.getVoices()??[];if(v.length>0){g(v.map(f=>({id:f.voiceURI,label:`${f.name} — ${f.lang}`}))),a("web");return}try{const f=await t.voices();if(f.length>0){g(f.map(y=>({id:y,label:y}))),a("sapi");return}}catch{}a("none")},h=()=>{l++,S=[],w()?.cancel(),r(!1),t.stop().catch(()=>{})},C=async v=>{const f=oe(v,i.value.skipCode);if(!f)return!1;if(h(),await u(),o()==="web"){const y=w();if(!y)return!1;const $=p(),I=i.value.rate,W=l;S=re(f),r(!0);const L=()=>{if(W!==l)return;const _=S.shift();if(!_){r(!1);return}const k=new SpeechSynthesisUtterance(_);$&&(k.voice=$),$?.lang&&(k.lang=$.lang),k.rate=I,k.onend=L,k.onerror=L,y.speak(k)};return L(),!0}if(o()==="sapi"){r(!0);try{await t.speak({text:f,rate:i.value.rate,voice:i.value.voiceURI})}catch(y){throw r(!1),y}return!0}throw r(!1),new Error(e.t("read.noVoices"))},n=async()=>{const v=e.sessions.current();if(!v)return"";const y=(await v.server.client.message.list({sessionID:v.id,type:"assistant",order:"desc",limit:1})).data[0];return!y||y.type!=="assistant"?"":y.content.filter($=>$.type==="text").map($=>$.text).join(`
165
- `)};return t.on("done",()=>r(!1)),w()?.addEventListener?.("voiceschanged",()=>{u()}),u(),{speaking:c,backend:o,voices:s,refreshVoices:u,lastAssistantText:n,speak:C,cancel:h,preview:()=>C(e.t("speech.preview.text"))}}const ae=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function ce(e){const t=b.usePlugin(),i=t.ui,c=t.stores.prefs,r=t.stores.speech,s=t.main(O),[g,o]=N.createSignal([]),[a,l]=N.createSignal(!1);N.onMount(()=>{s.diagnose().then(n=>{n.locales.length>0&&o(n.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const S=()=>{const n=g(),E=c.value.locale;return(n.includes(E)?n:[E,...n]).map(f=>({value:f,label:f}))},w=()=>[{value:"",label:t.t("speech.voice.auto")},...e.speaker.voices().map(n=>({value:n.id,label:n.label}))],p=n=>{c.update(E=>{n.locale!==void 0&&(E.locale=n.locale),n.minConfidence!==void 0&&(E.minConfidence=n.minConfidence),n.trailingSpace!==void 0&&(E.trailingSpace=n.trailingSpace),n.autoFocus!==void 0&&(E.autoFocus=n.autoFocus)})},u=n=>{r.update(E=>{n.voiceURI!==void 0&&(E.voiceURI=n.voiceURI),n.rate!==void 0&&(E.rate=n.rate),n.skipCode!==void 0&&(E.skipCode=n.skipCode)})},h=async()=>{try{await e.speaker.preview()}catch(n){t.toast.show({variant:"error",title:t.t("read.title"),description:n instanceof Error?n.message:t.t("read.error")})}},C=async()=>{l(!0);try{const n=await s.diagnose();if(n.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(!n.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}n.locales.length>0&&!g().length&&o(n.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:n.selected||n.locales[0]||c.value.locale})})}catch(n){t.toast.show({variant:"error",title:t.t("error.title"),description:n instanceof Error?n.message:String(n)})}finally{l(!1)}};return m.createComponent(i.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[m.createComponent(i.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[m.createComponent(i.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return m.createComponent(i.Select,{get label(){return t.t("settings.language")},get value(){return c.value.locale},get options(){return m.memo(()=>S().length>0)()?S():ae.map(n=>({value:n,label:n}))},onChange:n=>p({locale:n})})}}),m.createComponent(i.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return m.createComponent(i.Select,{get label(){return t.t("settings.confidence")},get value(){return String(c.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:n=>p({minConfidence:Number(n)||.4})})}}),m.createComponent(i.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return m.createComponent(i.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return c.value.trailingSpace},onChange:n=>p({trailingSpace:n})})}}),m.createComponent(i.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return m.createComponent(i.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return c.value.autoFocus},onChange:n=>p({autoFocus:n})})}})]}}),m.createComponent(i.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[m.createComponent(i.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return m.createComponent(i.Select,{get label(){return t.t("speech.voice")},get value(){return r.value.voiceURI},get options(){return w()},onChange:n=>u({voiceURI:n})})}}),m.createComponent(i.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return m.createComponent(i.Select,{get label(){return t.t("speech.rate")},get value(){return String(r.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:n=>u({rate:Number(n)||1})})}}),m.createComponent(i.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return m.createComponent(i.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return r.value.skipCode},onChange:n=>u({skipCode:n})})}}),m.createComponent(i.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return m.createComponent(i.Button,{onClick:()=>{h()},get children(){return t.t("read.idle")}})}})]}}),m.createComponent(i.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return m.createComponent(i.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return m.createComponent(i.Button,{get busy(){return a()},onClick:()=>{C()},get children(){return t.t("settings.test.run")}})}})}})]}})}const le=e=>{const t=e.main(O),i=e.main(V),c=se(e,i),r=e.stores.prefs;let s;const g=(p,u,h)=>{switch(p){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:u||r.value.locale,available:h.length>0?h.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:p})}},o=()=>{c.cancel(),s=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(p=>{e.toast.show({variant:"error",title:e.t("error.title"),description:p instanceof Error?p.message:String(p)})})},a=()=>{t.stop().catch(()=>{})},l=()=>{const p=t.state()?.status??"idle";p==="starting"||p==="listening"?a():o()},S=()=>{if(c.speaking()){c.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const p=t.state()?.status??"idle";(p==="starting"||p==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const u=await c.lastAssistantText();if(!u.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await c.speak(u)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(u){e.toast.show({variant:"error",title:e.t("read.title"),description:u instanceof Error?u.message:e.t("read.error")})}})()};t.on("final",p=>{const u=e.sessions.current()?.key;if(s&&u&&u!==s){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const h=e.screen.current()?.composer;if(!h)return;const C=h.text(),n=r.value.trailingSpace&&C.length>0&&!/\s$/.test(C)?" ":"";h.insert(n+p.text),r.value.autoFocus&&h.focus()}),t.on("error",p=>{e.toast.show({variant:"error",title:e.t("error.title"),description:g(p.code,p.detail,p.available),persistent:p.code==="LANGUAGE_NOT_AVAILABLE"||p.code==="NO_RECOGNIZERS",duration:8e3})});const w=()=>{if(e.app.platform!=="desktop")return;const p=t.state()?.status??"idle";return p==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:l}:p==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:l}:p==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:l}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:l}};e.add(b.ComposerPill,()=>({id:"dictate",show:()=>w()})),e.add(b.ComposerPill,()=>({id:"read",order:1,show:()=>{if(e.app.platform==="desktop")return c.speaking()?{label:e.t("read.speaking"),icon:"outline-audio",tone:"success",title:e.t("read.stop"),run:S}:{label:e.t("read.idle"),icon:"outline-audio",title:e.t("read.title"),run:S}}})),e.add(b.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:l})),e.add(b.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(b.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:S})),e.add(b.SettingsPage,{id:e.id,icon:"outline-audio",get title(){return e.t("settings.title")},get entries(){return[{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:()=>m.createComponent(ce,{speaker:c})})},de=b.Plugin.define({...X,setup:le,main:ie});exports.default=de;
163
+ `,x="powershell.exe";function G(){const e=I.join(W.tmpdir(),"opencode-voice-dictation");return k.mkdirSync(e,{recursive:!0}),e}function q(){const e=_.createHash("sha1").update(D).digest("hex").slice(0,12),t=I.join(G(),`speak-${e}.ps1`);return k.writeFileSync(t,D,"utf8"),t}function ee(e){const t=I.join(G(),`speech-${_.randomUUID()}.txt`);return k.writeFileSync(t,e,"utf8"),t}function K(e){return R.spawn(x,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function Z(e){const t=e.pid;if(t===void 0){e.kill();return}R.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function te(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function ie(e,t){const i=ee(e.text),l=K(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",q(),"-TextFile",i,"-Rate",String(te(e.rate)),"-Voice",e.voice]);let o=!1;const s=()=>{try{k.rmSync(i,{force:!0})}catch{}};return l.on("error",()=>{s(),o||t(!1)}),l.on("close",p=>{s(),o||t(p===0)}),{stop(){o=!0,Z(l),s()}}}function ne(){return new Promise(e=>{const t=K(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",q(),"-ListVoices"]);let i="",l=!1;const o=p=>{l||(l=!0,clearTimeout(s),e(p))},s=setTimeout(()=>{Z(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",p=>{i+=p}),t.on("error",()=>o([])),t.on("close",()=>{const p=i.split(/\r?\n/).map(r=>r.trim()).filter(r=>r.length>0);o(p)})})}const H=String.raw`$ErrorActionPreference = "Stop"
164
+
165
+ Add-Type @"
166
+ using System;
167
+ using System.Runtime.InteropServices;
168
+
169
+ public static class WinH {
170
+ [DllImport("user32.dll", SetLastError = true)]
171
+ public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
172
+
173
+ private const byte VK_LWIN = 0x5B;
174
+ private const byte VK_H = 0x48;
175
+ private const uint KEYUP = 0x0002;
176
+
177
+ public static void Send() {
178
+ keybd_event(VK_LWIN, 0, 0, UIntPtr.Zero);
179
+ System.Threading.Thread.Sleep(60);
180
+ keybd_event(VK_H, 0, 0, UIntPtr.Zero);
181
+ System.Threading.Thread.Sleep(60);
182
+ keybd_event(VK_H, 0, KEYUP, UIntPtr.Zero);
183
+ System.Threading.Thread.Sleep(60);
184
+ keybd_event(VK_LWIN, 0, KEYUP, UIntPtr.Zero);
185
+ }
186
+ }
187
+ "@
188
+
189
+ [WinH]::Send()
190
+ `,oe="powershell.exe";function re(){const e=I.join(W.tmpdir(),"opencode-voice-dictation");k.mkdirSync(e,{recursive:!0});const t=_.createHash("sha1").update(H).digest("hex").slice(0,12),i=I.join(e,`win-h-${t}.ps1`);return k.writeFileSync(i,H,"utf8"),i}function se(e){return R.spawn(oe,e,{windowsHide:!0,stdio:"ignore"})}function ae(){return new Promise((e,t)=>{const i=se(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",re()]);i.on("error",l=>t(l)),i.on("close",l=>{l===0?e():t(new Error(`Win+H helper exited with code ${l??"unknown"}`))})})}const ce=e=>{let t="idle",i="",l="",o,s=!1;const p=()=>{o=void 0,s=!1},r=e.provide(z,{state:()=>({status:t,error:i,locale:l}),start(b,y){o&&(s=!0,o.stop(),p()),t="starting",i="",l=b.locale,r.changed();const E=y.window;try{o=J({locale:b.locale,minConfidence:b.minConfidence},{onReady(S){t="listening",l=S,r.changed()},onFinal(S,v){r.emit("final",{text:S,confidence:v},E)},onError(S,v,$){t="error",i=S,r.changed(),r.emit("error",{code:S,detail:v,available:$},E)},onExit(S,v){if(s){p(),t="idle",i="",l="",r.changed();return}p(),t!=="error"&&(t="error",i=S===null?"ENGINE_EXIT":`ENGINE_EXIT_${S}`,r.changed(),r.emit("error",{code:"ENGINE_EXIT",detail:v||(S===null?"":String(S)),available:[]},E))}})}catch(S){throw t="error",i="ENGINE_START_FAILED",r.changed(),S}},stop(){o&&(s=!0,o.stop(),p()),t="idle",i="",l="",r.changed()},diagnose(){return Q()}});let a;const d=e.provide(U,{speak(b){a?.stop(),a=ie(b,y=>{a=void 0,d.emit("done",{ok:y})})},stop(){a&&(a.stop(),a=void 0),d.emit("done",{ok:!0})},voices(){return ne()}});e.provide(O,{toggle(){return ae()}}),e.onCleanup(()=>{o&&(s=!0,o.stop(),p()),a?.stop(),a=void 0})},le=250;function de(e,t){let i=e.replace(/\r\n/g,`
191
+ `);return t?i=i.replace(/```[\s\S]*?```/g," "):i=i.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),i=i.replace(/`([^`]+)`/g,"$1"),i=i.replace(/!\[[^\]]*\]\([^)]*\)/g," "),i=i.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),i=i.replace(/^\s*#{1,6}\s+/gm,""),i=i.replace(/^\s*>\s?/gm,""),i=i.replace(/^\s*[-*+]\s+/gm,""),i=i.replace(/^\s*\d+\.\s+/gm,""),i=i.replace(/(\*\*|__)(.*?)\1/g,"$2"),i=i.replace(/(\*|_)(.*?)\1/g,"$2"),i=i.replace(/^\s*\|.*\|\s*$/gm," "),i=i.replace(/\n{2,}/g,". "),i=i.replace(/\n/g," "),i=i.replace(/\s+/g," "),i=i.replace(/\s+([.,;:!?])/g,"$1"),i=i.replace(/([.,;:!?]){2,}/g,"$1"),i.trim()}function pe(e,t=le){const i=e.trim();if(!i)return[];const l=i.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let s="";const p=r=>{let a="";for(const d of r.split(/\s+/))d&&(a&&a.length+1+d.length>t?(o.push(a),a=d):a=a?`${a} ${d}`:d);return a};for(const r of l){if(r.length>t){s&&(o.push(s),s=""),s=p(r);continue}s&&s.length+1+r.length>t?(o.push(s),s=r):s=s?`${s} ${r}`:r}return s&&o.push(s),o}function ue(e,t){const i=e.stores.speech,[l,o]=C.createSignal(!1),[s,p]=C.createSignal([]),[r,a]=C.createSignal("none");let d=0,b=[];const y=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,E=()=>{const m=y()?.getVoices()??[];if(m.length===0)return;const f=i.value.voiceURI;if(f){const h=m.find(A=>A.voiceURI===f);if(h)return h}const n=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return m.find(h=>h.lang.toLowerCase().startsWith(n))??m[0]},S=async()=>{const m=y()?.getVoices()??[];if(m.length>0){p(m.map(f=>({id:f.voiceURI,label:`${f.name} — ${f.lang}`}))),a("web");return}try{const f=await t.voices();if(f.length>0){p(f.map(n=>({id:n,label:n}))),a("sapi");return}}catch{}a("none")},v=()=>{d++,b=[],y()?.cancel(),o(!1),t.stop().catch(()=>{})},$=async m=>{const f=de(m,i.value.skipCode);if(!f)return!1;if(v(),await S(),r()==="web"){const n=y();if(!n)return!1;const h=E(),A=i.value.rate,L=d;b=pe(f),o(!0);const P=()=>{if(L!==d)return;const V=b.shift();if(!V){o(!1);return}const T=new SpeechSynthesisUtterance(V);h&&(T.voice=h),h?.lang&&(T.lang=h.lang),T.rate=A,T.onend=P,T.onerror=P,n.speak(T)};return P(),!0}if(r()==="sapi"){o(!0);try{await t.speak({text:f,rate:i.value.rate,voice:i.value.voiceURI})}catch(n){throw o(!1),n}return!0}throw o(!1),new Error(e.t("read.noVoices"))},N=async()=>{const m=e.sessions.current();if(!m)return"";const n=(await m.server.client.message.list({sessionID:m.id,type:"assistant",order:"desc",limit:1})).data[0];return!n||n.type!=="assistant"?"":n.content.filter(h=>h.type==="text").map(h=>h.text).join(`
192
+ `)};return t.on("done",()=>o(!1)),y()?.addEventListener?.("voiceschanged",()=>{S()}),S(),{speaking:l,backend:r,voices:s,refreshVoices:S,lastAssistantText:N,speak:$,cancel:v,preview:()=>$(e.t("speech.preview.text"))}}const ge=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function he(e){const t=w.usePlugin(),i=t.ui,l=t.stores.prefs,o=t.stores.speech,s=t.stores.engine,p=t.main(z),r=t.main(O),[a,d]=C.createSignal([]),[b,y]=C.createSignal(!1);C.onMount(()=>{p.diagnose().then(n=>{n.locales.length>0&&d(n.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const E=()=>{const n=a(),h=l.value.locale;return(n.includes(h)?n:[h,...n]).map(L=>({value:L,label:L}))},S=()=>[{value:"",label:t.t("speech.voice.auto")},...e.speaker.voices().map(n=>({value:n.id,label:n.label}))],v=n=>{l.update(h=>{n.locale!==void 0&&(h.locale=n.locale),n.minConfidence!==void 0&&(h.minConfidence=n.minConfidence),n.trailingSpace!==void 0&&(h.trailingSpace=n.trailingSpace),n.autoFocus!==void 0&&(h.autoFocus=n.autoFocus)})},$=n=>{o.update(h=>{n.voiceURI!==void 0&&(h.voiceURI=n.voiceURI),n.rate!==void 0&&(h.rate=n.rate),n.skipCode!==void 0&&(h.skipCode=n.skipCode)})},N=n=>{s.update(h=>{h.mode=n})},u=async()=>{try{await r.toggle(),t.toast.show({variant:"default",title:t.t("settings.winh"),description:t.t("settings.winh.sent")})}catch(n){t.toast.show({variant:"error",title:t.t("pill.title"),description:n instanceof Error?n.message:t.t("error.winh")})}},m=async()=>{try{await e.speaker.preview()}catch(n){t.toast.show({variant:"error",title:t.t("read.title"),description:n instanceof Error?n.message:t.t("read.error")})}},f=async()=>{y(!0);try{const n=await p.diagnose();if(n.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(!n.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}n.locales.length>0&&!a().length&&d(n.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:n.selected||n.locales[0]||l.value.locale})})}catch(n){t.toast.show({variant:"error",title:t.t("error.title"),description:n instanceof Error?n.message:String(n)})}finally{y(!1)}};return g.createComponent(i.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[g.createComponent(i.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[g.createComponent(i.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("settings.engine")},get value(){return s.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:n=>N(n)})}}),g.createComponent(C.Show,{get when(){return s.value.mode==="windows"},get children(){return g.createComponent(i.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return g.createComponent(i.Button,{onClick:()=>{u()},get children(){return t.t("settings.winh.run")}})}})}}),g.createComponent(C.Show,{get when(){return s.value.mode==="system"},get children(){return[g.createComponent(i.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("settings.language")},get value(){return l.value.locale},get options(){return g.memo(()=>E().length>0)()?E():ge.map(n=>({value:n,label:n}))},onChange:n=>v({locale:n})})}}),g.createComponent(i.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return g.createComponent(i.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:n=>v({minConfidence:Number(n)||.4})})}}),g.createComponent(i.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return l.value.trailingSpace},onChange:n=>v({trailingSpace:n})})}}),g.createComponent(i.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return l.value.autoFocus},onChange:n=>v({autoFocus:n})})}})]}})]}}),g.createComponent(i.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[g.createComponent(i.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("speech.voice")},get value(){return o.value.voiceURI},get options(){return S()},onChange:n=>$({voiceURI:n})})}}),g.createComponent(i.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return g.createComponent(i.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:n=>$({rate:Number(n)||1})})}}),g.createComponent(i.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:n=>$({skipCode:n})})}}),g.createComponent(i.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return g.createComponent(i.Button,{onClick:()=>{m()},get children(){return t.t("read.idle")}})}})]}}),g.createComponent(i.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return g.createComponent(i.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return g.createComponent(i.Button,{get busy(){return b()},onClick:()=>{f()},get children(){return t.t("settings.test.run")}})}})}})]}})}const me=e=>{const t=e.main(z),i=e.main(U),l=e.main(O),o=ue(e,i),s=e.stores.prefs,p=e.stores.engine,[r,a]=C.createSignal(!1);let d;const b=(u,m,f)=>{switch(u){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:m||s.value.locale,available:f.length>0?f.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:u})}},y=()=>{r()&&(a(!1),l.toggle().catch(()=>{})),o.cancel(),d=e.sessions.current()?.key,t.start({locale:s.value.locale,minConfidence:s.value.minConfidence}).catch(u=>{e.toast.show({variant:"error",title:e.t("error.title"),description:u instanceof Error?u.message:String(u)})})},E=()=>{t.stop().catch(()=>{})},S=()=>{o.cancel(),e.screen.current()?.composer.focus(),l.toggle().then(()=>a(u=>!u)).catch(u=>{a(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:u instanceof Error?`${e.t("error.winh")} ${u.message}`:e.t("error.winh")})})},v=()=>{if(p.value.mode==="windows"){S();return}const u=t.state()?.status??"idle";u==="starting"||u==="listening"?E():y()},$=()=>{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 u=t.state()?.status??"idle";(u==="starting"||u==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const m=await o.lastAssistantText();if(!m.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(m)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(m){e.toast.show({variant:"error",title:e.t("read.title"),description:m instanceof Error?m.message:e.t("read.error")})}})()};t.on("final",u=>{const m=e.sessions.current()?.key;if(d&&m&&m!==d){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const f=e.screen.current()?.composer;if(!f)return;const n=f.text(),h=s.value.trailingSpace&&n.length>0&&!/\s$/.test(n)?" ":"";f.insert(h+u.text),s.value.autoFocus&&f.focus()}),t.on("error",u=>{e.toast.show({variant:"error",title:e.t("error.title"),description:b(u.code,u.detail,u.available),persistent:u.code==="LANGUAGE_NOT_AVAILABLE"||u.code==="NO_RECOGNIZERS",duration:8e3})});const N=()=>{if(e.app.platform!=="desktop")return;if(p.value.mode==="windows")return r()?{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 u=t.state()?.status??"idle";return u==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:v}:u==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:v}:u==="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}};e.add(w.ComposerPill,()=>({id:"dictate",show:()=>N()})),e.add(w.ComposerPill,()=>({id:"read",order:1,show:()=>{if(e.app.platform==="desktop")return o.speaking()?{label:e.t("read.speaking"),icon:"outline-audio",tone:"success",title:e.t("read.stop"),run:$}:{label:e.t("read.idle"),icon:"outline-audio",title:e.t("read.title"),run:$}}})),e.add(w.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(w.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(w.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(w.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(he,{speaker:o})})},fe=w.Plugin.define({...X,setup:me,main:ce});exports.default=fe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-voice-dictation",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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>",
@@ -55,8 +55,7 @@ export interface DiagnoseResult {
55
55
  * Text-to-speech bridge. The window speaks with `speechSynthesis` when the
56
56
  * Electron build exposes voices, and falls back to this main-process engine
57
57
  * (`System.Speech.Synthesis`) when it does not.
58
- */
59
- export const Speech = Ipc.define({
58
+ */export const Speech = Ipc.define({
60
59
  id: "voice-dictation.speech",
61
60
  methods: {
62
61
  speak: {
@@ -75,3 +74,14 @@ export const Speech = Ipc.define({
75
74
  done: Schema.Struct({ ok: Schema.Boolean }),
76
75
  },
77
76
  })
77
+
78
+ /**
79
+ * Windows 11 voice typing. The window focuses the composer and asks main to
80
+ * press Win+H; Windows writes the text straight into the focused field.
81
+ */
82
+ export const VoiceTyping = Ipc.define({
83
+ id: "voice-dictation.voice-typing",
84
+ methods: {
85
+ toggle: {},
86
+ },
87
+ })
@@ -37,17 +37,27 @@ export const definition = Plugin.define({
37
37
  skipCode: true,
38
38
  },
39
39
  ),
40
+ engine: Store.global(
41
+ Schema.Struct({
42
+ mode: Schema.String,
43
+ }),
44
+ {
45
+ mode: "windows",
46
+ },
47
+ ),
40
48
  },
41
49
  i18n: {
42
50
  en: {
43
51
  "pill.idle": "Dictate",
44
52
  "pill.starting": "Starting…",
45
53
  "pill.listening": "Listening…",
54
+ "pill.dictating": "Dictating…",
46
55
  "pill.error": "Retry dictation",
47
56
  "pill.title": "Voice dictation",
57
+ "pill.winH": "Open Windows voice typing (Win+H)",
48
58
  "pill.stop": "Stop dictation",
49
59
  "command.toggle": "Toggle voice dictation",
50
- "command.toggle.description": "Transcribe what you say into the prompt",
60
+ "command.toggle.description": "Dictate into the prompt with the selected engine",
51
61
  "command.configure": "Voice dictation settings",
52
62
  "error.title": "Voice dictation",
53
63
  "error.language": "The speech language {locale} is not installed. Available: {available}.",
@@ -60,6 +70,15 @@ export const definition = Plugin.define({
60
70
  "settings.title": "Voice dictation",
61
71
  "settings.description": "Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.",
62
72
  "settings.section": "Dictation",
73
+ "settings.engine": "Dictation engine",
74
+ "settings.engine.description": "Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.",
75
+ "engine.windows": "Windows Voice Typing (Win+H)",
76
+ "engine.system": "System.Speech (offline)",
77
+ "settings.winh": "Test Win+H",
78
+ "settings.winh.description": "Sends the Windows voice typing shortcut. If the floating bar appears, it works.",
79
+ "settings.winh.run": "Test Win+H",
80
+ "settings.winh.sent": "Win+H sent. If the Windows dictation bar did not appear, check Settings › Accessibility › Voice typing.",
81
+ "error.winh": "Windows voice typing could not be triggered.",
63
82
  "settings.language": "Language",
64
83
  "settings.language.description": "A speech recognizer installed in Windows. Accents and punctuation depend on it.",
65
84
  "settings.confidence": "Minimum confidence",
@@ -109,11 +128,13 @@ export const definition = Plugin.define({
109
128
  "pill.idle": "Dictar",
110
129
  "pill.starting": "Iniciando…",
111
130
  "pill.listening": "Escuchando…",
131
+ "pill.dictating": "Dictando…",
112
132
  "pill.error": "Reintentar dictado",
113
133
  "pill.title": "Dictado por voz",
134
+ "pill.winH": "Abrir el dictado de Windows (Win+H)",
114
135
  "pill.stop": "Detener dictado",
115
136
  "command.toggle": "Alternar dictado por voz",
116
- "command.toggle.description": "Transcribe lo que dices en el prompt",
137
+ "command.toggle.description": "Dicta en el prompt con el motor elegido",
117
138
  "command.configure": "Ajustes de dictado por voz",
118
139
  "error.title": "Dictado por voz",
119
140
  "error.language": "El idioma de reconocimiento {locale} no está instalado. Disponibles: {available}.",
@@ -126,6 +147,14 @@ export const definition = Plugin.define({
126
147
  "settings.title": "Dictado por voz",
127
148
  "settings.description": "Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.",
128
149
  "settings.section": "Dictado",
150
+ "settings.engine": "Motor de dictado",
151
+ "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.",
152
+ "engine.windows": "Windows Voice Typing (Win+H)",
153
+ "engine.system": "System.Speech (offline)",
154
+ "settings.winh": "Probar Win+H",
155
+ "settings.winh.description": "Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.",
156
+ "settings.winh.run": "Probar Win+H",
157
+ "settings.winh.sent": "Win+H enviado. Si no apareció la barra, revisa Configuración › Accesibilidad › Dictado por voz.",
129
158
  "settings.language": "Idioma",
130
159
  "settings.language.description": "Un reconocedor instalado en Windows. Los acentos y la puntuación dependen de él.",
131
160
  "settings.confidence": "Confianza mínima",
@@ -0,0 +1,80 @@
1
+ import { spawn, type ChildProcess } from "node:child_process"
2
+ import { createHash } from "node:crypto"
3
+ import { mkdirSync, writeFileSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { join } from "node:path"
6
+
7
+ /**
8
+ * Sends the Windows voice typing shortcut (Win+H).
9
+ *
10
+ * `SendKeys` cannot press the Windows key, so this uses `keybd_event` from
11
+ * user32.dll through an inline C# type compiled by `Add-Type`.
12
+ *
13
+ * Keep the script ASCII-only: Windows PowerShell 5.1 reads a BOM-less script
14
+ * as ANSI.
15
+ */
16
+ export const WIN_H_SCRIPT = String.raw`$ErrorActionPreference = "Stop"
17
+
18
+ Add-Type @"
19
+ using System;
20
+ using System.Runtime.InteropServices;
21
+
22
+ public static class WinH {
23
+ [DllImport("user32.dll", SetLastError = true)]
24
+ public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
25
+
26
+ private const byte VK_LWIN = 0x5B;
27
+ private const byte VK_H = 0x48;
28
+ private const uint KEYUP = 0x0002;
29
+
30
+ public static void Send() {
31
+ keybd_event(VK_LWIN, 0, 0, UIntPtr.Zero);
32
+ System.Threading.Thread.Sleep(60);
33
+ keybd_event(VK_H, 0, 0, UIntPtr.Zero);
34
+ System.Threading.Thread.Sleep(60);
35
+ keybd_event(VK_H, 0, KEYUP, UIntPtr.Zero);
36
+ System.Threading.Thread.Sleep(60);
37
+ keybd_event(VK_LWIN, 0, KEYUP, UIntPtr.Zero);
38
+ }
39
+ }
40
+ "@
41
+
42
+ [WinH]::Send()
43
+ `
44
+
45
+ const POWERSHELL = "powershell.exe"
46
+
47
+ function scriptFile(): string {
48
+ const dir = join(tmpdir(), "opencode-voice-dictation")
49
+ mkdirSync(dir, { recursive: true })
50
+ const hash = createHash("sha1").update(WIN_H_SCRIPT).digest("hex").slice(0, 12)
51
+ const file = join(dir, `win-h-${hash}.ps1`)
52
+ writeFileSync(file, WIN_H_SCRIPT, "utf8")
53
+ return file
54
+ }
55
+
56
+ function spawnPowershell(args: string[]): ChildProcess {
57
+ return spawn(POWERSHELL, args, {
58
+ windowsHide: true,
59
+ stdio: "ignore",
60
+ })
61
+ }
62
+
63
+ export function sendVoiceTypingShortcut(): Promise<void> {
64
+ return new Promise((resolve, reject) => {
65
+ const child = spawnPowershell([
66
+ "-NoProfile",
67
+ "-NonInteractive",
68
+ "-ExecutionPolicy",
69
+ "Bypass",
70
+ "-File",
71
+ scriptFile(),
72
+ ])
73
+
74
+ child.on("error", (cause) => reject(cause))
75
+ child.on("close", (code) => {
76
+ if (code === 0) resolve()
77
+ else reject(new Error(`Win+H helper exited with code ${code ?? "unknown"}`))
78
+ })
79
+ })
80
+ }
package/src/gui/main.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Plugin } from "@opencode/plugin/gui"
2
- import { Dictation, Speech } from "./contract"
2
+ 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
+ import { sendVoiceTypingShortcut } from "./engines/win-h"
5
6
 
6
7
  type Status = "idle" | "starting" | "listening" | "error"
7
8
 
@@ -131,6 +132,12 @@ export const main: Plugin.MainSetup = (ctx) => {
131
132
  },
132
133
  })
133
134
 
135
+ ctx.provide(VoiceTyping, {
136
+ toggle() {
137
+ return sendVoiceTypingShortcut()
138
+ },
139
+ })
140
+
134
141
  ctx.onCleanup(() => {
135
142
  if (handle) {
136
143
  stopping = true
@@ -1,6 +1,6 @@
1
- import { createSignal, onMount } from "solid-js"
1
+ import { createSignal, onMount, Show } from "solid-js"
2
2
  import { usePlugin } from "@opencode/plugin/gui"
3
- import { Dictation } from "./contract"
3
+ import { Dictation, VoiceTyping } from "./contract"
4
4
  import type { definition } from "./definition"
5
5
  import type { Speaker } from "./speak"
6
6
 
@@ -27,7 +27,9 @@ 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 enginePrefs = ctx.stores.engine
30
31
  const dictation = ctx.main(Dictation)
32
+ const voiceTyping = ctx.main(VoiceTyping)
31
33
 
32
34
  const [locales, setLocales] = createSignal<readonly string[]>([])
33
35
  const [busy, setBusy] = createSignal(false)
@@ -71,6 +73,29 @@ export function VoiceSettings(props: { speaker: Speaker }) {
71
73
  })
72
74
  }
73
75
 
76
+ const patchEngine = (mode: string) => {
77
+ enginePrefs.update((draft) => {
78
+ draft.mode = mode
79
+ })
80
+ }
81
+
82
+ const testWinH = async () => {
83
+ try {
84
+ await voiceTyping.toggle()
85
+ ctx.toast.show({
86
+ variant: "default",
87
+ title: ctx.t("settings.winh"),
88
+ description: ctx.t("settings.winh.sent"),
89
+ })
90
+ } catch (cause) {
91
+ ctx.toast.show({
92
+ variant: "error",
93
+ title: ctx.t("pill.title"),
94
+ description: cause instanceof Error ? cause.message : ctx.t("error.winh"),
95
+ })
96
+ }
97
+ }
98
+
74
99
  const preview = async () => {
75
100
  try {
76
101
  await props.speaker.preview()
@@ -130,52 +155,80 @@ export function VoiceSettings(props: { speaker: Speaker }) {
130
155
  <ui.SettingsLayout title={ctx.t("settings.title")} description={ctx.t("settings.description")}>
131
156
  <ui.SettingsSection title={ctx.t("settings.section")}>
132
157
  <ui.SettingsRow
133
- id="language"
134
- title={ctx.t("settings.language")}
135
- description={ctx.t("settings.language.description")}
136
- >
137
- <ui.Select
138
- label={ctx.t("settings.language")}
139
- value={prefs.value.locale}
140
- options={localeOptions().length > 0 ? localeOptions() : FALLBACK_LOCALES.map((value) => ({ value, label: value }))}
141
- onChange={(value) => patch({ locale: value })}
142
- />
143
- </ui.SettingsRow>
144
-
145
- <ui.SettingsRow
146
- id="confidence"
147
- title={ctx.t("settings.confidence")}
148
- description={ctx.t("settings.confidence.description")}
158
+ id="engine"
159
+ title={ctx.t("settings.engine")}
160
+ description={ctx.t("settings.engine.description")}
149
161
  >
150
162
  <ui.Select
151
- label={ctx.t("settings.confidence")}
152
- value={String(prefs.value.minConfidence)}
163
+ label={ctx.t("settings.engine")}
164
+ value={enginePrefs.value.mode}
153
165
  options={[
154
- { value: "0.2", label: ctx.t("confidence.low") },
155
- { value: "0.4", label: ctx.t("confidence.normal") },
156
- { value: "0.6", label: ctx.t("confidence.high") },
166
+ { value: "windows", label: ctx.t("engine.windows") },
167
+ { value: "system", label: ctx.t("engine.system") },
157
168
  ]}
158
- onChange={(value) => patch({ minConfidence: Number(value) || 0.4 })}
169
+ onChange={(value) => patchEngine(value)}
159
170
  />
160
171
  </ui.SettingsRow>
161
172
 
162
- <ui.SettingsRow id="space" title={ctx.t("settings.space")} description={ctx.t("settings.space.description")}>
163
- <ui.Switch
164
- label={ctx.t("settings.space")}
165
- hideLabel
166
- checked={prefs.value.trailingSpace}
167
- onChange={(checked) => patch({ trailingSpace: checked })}
168
- />
169
- </ui.SettingsRow>
173
+ <Show when={enginePrefs.value.mode === "windows"}>
174
+ <ui.SettingsRow
175
+ id="winh"
176
+ title={ctx.t("settings.winh")}
177
+ description={ctx.t("settings.winh.description")}
178
+ >
179
+ <ui.Button onClick={() => void testWinH()}>{ctx.t("settings.winh.run")}</ui.Button>
180
+ </ui.SettingsRow>
181
+ </Show>
170
182
 
171
- <ui.SettingsRow id="focus" title={ctx.t("settings.focus")} description={ctx.t("settings.focus.description")}>
172
- <ui.Switch
173
- label={ctx.t("settings.focus")}
174
- hideLabel
175
- checked={prefs.value.autoFocus}
176
- onChange={(checked) => patch({ autoFocus: checked })}
177
- />
178
- </ui.SettingsRow>
183
+ <Show when={enginePrefs.value.mode === "system"}>
184
+ <ui.SettingsRow
185
+ id="language"
186
+ title={ctx.t("settings.language")}
187
+ description={ctx.t("settings.language.description")}
188
+ >
189
+ <ui.Select
190
+ label={ctx.t("settings.language")}
191
+ value={prefs.value.locale}
192
+ options={localeOptions().length > 0 ? localeOptions() : FALLBACK_LOCALES.map((value) => ({ value, label: value }))}
193
+ onChange={(value) => patch({ locale: value })}
194
+ />
195
+ </ui.SettingsRow>
196
+
197
+ <ui.SettingsRow
198
+ id="confidence"
199
+ title={ctx.t("settings.confidence")}
200
+ description={ctx.t("settings.confidence.description")}
201
+ >
202
+ <ui.Select
203
+ label={ctx.t("settings.confidence")}
204
+ value={String(prefs.value.minConfidence)}
205
+ options={[
206
+ { value: "0.2", label: ctx.t("confidence.low") },
207
+ { value: "0.4", label: ctx.t("confidence.normal") },
208
+ { value: "0.6", label: ctx.t("confidence.high") },
209
+ ]}
210
+ onChange={(value) => patch({ minConfidence: Number(value) || 0.4 })}
211
+ />
212
+ </ui.SettingsRow>
213
+
214
+ <ui.SettingsRow id="space" title={ctx.t("settings.space")} description={ctx.t("settings.space.description")}>
215
+ <ui.Switch
216
+ label={ctx.t("settings.space")}
217
+ hideLabel
218
+ checked={prefs.value.trailingSpace}
219
+ onChange={(checked) => patch({ trailingSpace: checked })}
220
+ />
221
+ </ui.SettingsRow>
222
+
223
+ <ui.SettingsRow id="focus" title={ctx.t("settings.focus")} description={ctx.t("settings.focus.description")}>
224
+ <ui.Switch
225
+ label={ctx.t("settings.focus")}
226
+ hideLabel
227
+ checked={prefs.value.autoFocus}
228
+ onChange={(checked) => patch({ autoFocus: checked })}
229
+ />
230
+ </ui.SettingsRow>
231
+ </Show>
179
232
  </ui.SettingsSection>
180
233
 
181
234
  <ui.SettingsSection title={ctx.t("speech.section")}>
@@ -1,5 +1,6 @@
1
+ import { createSignal } from "solid-js"
1
2
  import { Command, ComposerPill, SettingsPage, type ComposerPillView, type Plugin } from "@opencode/plugin/gui"
2
- import { Dictation, Speech } from "./contract"
3
+ import { Dictation, Speech, VoiceTyping } from "./contract"
3
4
  import type { definition } from "./definition"
4
5
  import { createSpeaker } from "./speak"
5
6
  import { VoiceSettings } from "./settings"
@@ -11,8 +12,11 @@ import { VoiceSettings } from "./settings"
11
12
  export const setup: Plugin.Setup<typeof definition> = (ctx) => {
12
13
  const dictation = ctx.main(Dictation)
13
14
  const speech = ctx.main(Speech)
15
+ const voiceTyping = ctx.main(VoiceTyping)
14
16
  const speaker = createSpeaker(ctx, speech)
15
17
  const prefs = ctx.stores.prefs
18
+ const engine = ctx.stores.engine
19
+ const [windowsActive, setWindowsActive] = createSignal(false)
16
20
 
17
21
  let targetSession: string | undefined
18
22
 
@@ -37,6 +41,10 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
37
41
  }
38
42
 
39
43
  const start = () => {
44
+ if (windowsActive()) {
45
+ setWindowsActive(false)
46
+ void voiceTyping.toggle().catch(() => {})
47
+ }
40
48
  speaker.cancel()
41
49
  targetSession = ctx.sessions.current()?.key
42
50
  void dictation
@@ -54,7 +62,30 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
54
62
  void dictation.stop().catch(() => {})
55
63
  }
56
64
 
65
+ const toggleWindows = () => {
66
+ speaker.cancel()
67
+ ctx.screen.current()?.composer.focus()
68
+
69
+ void voiceTyping
70
+ .toggle()
71
+ .then(() => setWindowsActive((value) => !value))
72
+ .catch((cause: unknown) => {
73
+ setWindowsActive(false)
74
+ ctx.toast.show({
75
+ variant: "error",
76
+ title: ctx.t("pill.title"),
77
+ description:
78
+ cause instanceof Error ? `${ctx.t("error.winh")} ${cause.message}` : ctx.t("error.winh"),
79
+ })
80
+ })
81
+ }
82
+
57
83
  const toggle = () => {
84
+ if (engine.value.mode === "windows") {
85
+ toggleWindows()
86
+ return
87
+ }
88
+
58
89
  const status = dictation.state()?.status ?? "idle"
59
90
  if (status === "starting" || status === "listening") stop()
60
91
  else start()
@@ -126,6 +157,23 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
126
157
  const view = (): ComposerPillView | undefined => {
127
158
  if (ctx.app.platform !== "desktop") return undefined
128
159
 
160
+ if (engine.value.mode === "windows") {
161
+ return windowsActive()
162
+ ? {
163
+ label: ctx.t("pill.dictating"),
164
+ icon: "outline-wave-audio",
165
+ tone: "danger",
166
+ title: ctx.t("pill.stop"),
167
+ run: toggle,
168
+ }
169
+ : {
170
+ label: ctx.t("pill.idle"),
171
+ icon: "outline-wave-audio",
172
+ title: ctx.t("pill.winH"),
173
+ run: toggle,
174
+ }
175
+ }
176
+
129
177
  const status = dictation.state()?.status ?? "idle"
130
178
 
131
179
  if (status === "starting") {
@@ -225,6 +273,8 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
225
273
  },
226
274
  get entries() {
227
275
  return [
276
+ { id: "engine", title: ctx.t("settings.engine"), keywords: "engine motor dictation dictado winh voz" },
277
+ { id: "winh", title: ctx.t("settings.winh"), keywords: "winh windows voice typing dictado" },
228
278
  { id: "language", title: ctx.t("settings.language"), keywords: "voice language idioma voz" },
229
279
  { id: "confidence", title: ctx.t("settings.confidence"), keywords: "voice confidence confianza" },
230
280
  { id: "test", title: ctx.t("settings.test"), keywords: "voice microphone microphone test microfono" },