opencode-voice-dictation 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const k=require("@opencode/plugin/gui"),c=require("effect"),B=require("node:child_process"),q=require("node:crypto"),z=require("node:fs"),Z=require("node:os"),F=require("node:path"),u=require("solid-js/web"),I=require("solid-js"),$e=k.Plugin.define({os:["windows"],stores:{prefs:k.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:k.Store.global(c.Schema.Struct({voiceURI:c.Schema.String,rate:c.Schema.Number,skipCode:c.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:k.Store.global(c.Schema.Struct({mode:c.Schema.String}),{mode:"windows"}),tts:k.Store.global(c.Schema.Struct({engine:c.Schema.String,voice:c.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. Check the connection or switch to the system voice.","speech.edge.error":"Edge voices could not be loaded","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. Revisa la conexión o cambia a la voz del sistema.","speech.edge.error":"No se pudieron cargar las voces de Edge","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."}}}),Y=k.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)})}}),ce=k.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})}}),J=k.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),ie=String.raw`param(
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const $=require("@opencode/plugin/gui"),c=require("effect"),K=require("node:child_process"),Q=require("node:crypto"),j=require("node:fs"),te=require("node:os"),M=require("node:path"),d=require("solid-js/web"),R=require("solid-js"),Ce=$.Plugin.define({os:["windows"],stores:{prefs:$.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:$.Store.global(c.Schema.Struct({voiceURI:c.Schema.String,rate:c.Schema.Number,skipCode:c.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:$.Store.global(c.Schema.Struct({mode:c.Schema.String}),{mode:"windows"}),tts:$.Store.global(c.Schema.Struct({engine:c.Schema.String,voice:c.Schema.String}),{engine:"edge",voice:"es-ES-XimenaNeural"}),whisper:$.Store.global(c.Schema.Struct({python:c.Schema.String,model:c.Schema.String,language:c.Schema.String}),{python:"~/peopleby/voice_to_text/api_voice/demo/whisperx-env/bin/python",model:"large-v3-turbo",language:""})},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)","engine.whisper":"WhisperX local (WSL)","pill.recording":"Recording…","pill.transcribing":"Transcribing…","pill.whisper":"Record and transcribe with WhisperX","error.whisper":"WhisperX could not transcribe the audio.","whisper.section":"WhisperX (local)","whisper.python":"Environment Python","whisper.python.description":"Python of the venv that has whisperx installed.","whisper.model":"Model","whisper.model.description":"large-v3-turbo is the fastest with good accuracy on a GPU.","whisper.language":"Language","whisper.language.description":"Automatic detects the language of each recording.","whisper.language.auto":"Automatic","whisper.test":"Check WhisperX","whisper.test.description":"Starts the worker and reports whether the model loads.","whisper.test.run":"Check","whisper.test.ready":"WhisperX ready: {model} ({device})","whisper.test.error":"WhisperX is not ready: {error}","whisper.empty":"WhisperX returned no text.","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. Check the connection or switch to the system voice.","speech.edge.error":"Edge voices could not be loaded","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)","engine.whisper":"WhisperX local (WSL)","pill.recording":"Grabando…","pill.transcribing":"Transcribiendo…","pill.whisper":"Grabar y transcribir con WhisperX","error.whisper":"WhisperX no pudo transcribir el audio.","whisper.section":"WhisperX (local)","whisper.python":"Python del entorno","whisper.python.description":"Python del venv que tiene whisperx instalado.","whisper.model":"Modelo","whisper.model.description":"large-v3-turbo es el más rápido con buena precisión en GPU.","whisper.language":"Idioma","whisper.language.description":"Automático detecta el idioma de cada grabación.","whisper.language.auto":"Automático","whisper.test":"Comprobar WhisperX","whisper.test.description":"Arranca el worker e indica si el modelo carga.","whisper.test.run":"Comprobar","whisper.test.ready":"WhisperX listo: {model} ({device})","whisper.test.error":"WhisperX no está listo: {error}","whisper.empty":"WhisperX no devolvió texto.","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. Revisa la conexión o cambia a la voz del sistema.","speech.edge.error":"No se pudieron cargar las voces de Edge","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."}}}),ie=$.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)})}}),le=$.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})}}),ne=$.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),oe=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
- `,Ee="powershell.exe";function le(){const e=F.join(Z.tmpdir(),"opencode-voice-dictation");z.mkdirSync(e,{recursive:!0});const t=q.createHash("sha1").update(ie).digest("hex").slice(0,12),n=F.join(e,`dictation-${t}.ps1`);return z.writeFileSync(n,ie,"utf8"),n}function de(e){return B.spawn(Ee,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function ue(e){const t=e.pid;if(t===void 0){e.kill();return}B.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function Ce(e,t){const n=de(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",le(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let d=!1,o="",r="";const g=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 l=r.indexOf(`
109
- `);for(;l>=0;){const p=r.slice(0,l).replace(/\r$/,"").trim();if(r=r.slice(l+1),p)try{g(JSON.parse(p))}catch{}l=r.indexOf(`
110
- `)}}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",a=>{o=(o+a).slice(-2e3)}),n.on("error",a=>{d||t.onError("ENGINE_START_FAILED",a.message,[])}),n.on("close",a=>{d||t.onExit(a,o.trim().slice(-500))}),{stop(){d=!0,ue(n)}}}function ke(){return new Promise((e,t)=>{const n=de(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",le(),"-Diagnose"]);let d="",o="",r=!1;const g=(l,p)=>{r||(r=!0,clearTimeout(a),l?t(l):e(p))},a=setTimeout(()=>{ue(n),g(new Error("diagnose timed out"))},2e4);n.stdout?.setEncoding("utf8"),n.stdout?.on("data",l=>{d+=l}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",l=>{o+=l}),n.on("error",l=>g(l)),n.on("close",()=>{const l=d.split(/\r?\n/).map(p=>p.trim()).filter(p=>p.startsWith("{")).at(-1);if(!l){g(new Error(o.trim()||"no output from PowerShell"));return}try{const p=JSON.parse(l);g(void 0,{locales:p.locales??[],microphone:p.microphone===!0,selected:p.selected??"",error:p.error??""})}catch(p){g(p instanceof Error?p:new Error(String(p)))}})})}const oe=String.raw`param(
108
+ `,$e="powershell.exe";function de(){const e=M.join(te.tmpdir(),"opencode-voice-dictation");j.mkdirSync(e,{recursive:!0});const t=Q.createHash("sha1").update(oe).digest("hex").slice(0,12),i=M.join(e,`dictation-${t}.ps1`);return j.writeFileSync(i,oe,"utf8"),i}function pe(e){return K.spawn($e,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function ue(e){const t=e.pid;if(t===void 0){e.kill();return}K.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function Ee(e,t){const i=pe(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",de(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let l=!1,r="",o="";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 i.stdout?.setEncoding("utf8"),i.stdout?.on("data",s=>{o+=s;let p=o.indexOf(`
109
+ `);for(;p>=0;){const g=o.slice(0,p).replace(/\r$/,"").trim();if(o=o.slice(p+1),g)try{u(JSON.parse(g))}catch{}p=o.indexOf(`
110
+ `)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",s=>{r=(r+s).slice(-2e3)}),i.on("error",s=>{l||t.onError("ENGINE_START_FAILED",s.message,[])}),i.on("close",s=>{l||t.onExit(s,r.trim().slice(-500))}),{stop(){l=!0,ue(i)}}}function ke(){return new Promise((e,t)=>{const i=pe(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",de(),"-Diagnose"]);let l="",r="",o=!1;const u=(p,g)=>{o||(o=!0,clearTimeout(s),p?t(p):e(g))},s=setTimeout(()=>{ue(i),u(new Error("diagnose timed out"))},2e4);i.stdout?.setEncoding("utf8"),i.stdout?.on("data",p=>{l+=p}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",p=>{r+=p}),i.on("error",p=>u(p)),i.on("close",()=>{const p=l.split(/\r?\n/).map(g=>g.trim()).filter(g=>g.startsWith("{")).at(-1);if(!p){u(new Error(r.trim()||"no output from PowerShell"));return}try{const g=JSON.parse(p);u(void 0,{locales:g.locales??[],microphone:g.microphone===!0,selected:g.selected??"",error:g.error??""})}catch(g){u(g instanceof Error?g:new Error(String(g)))}})})}const se=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
- `,Re="powershell.exe";function pe(){const e=F.join(Z.tmpdir(),"opencode-voice-dictation");return z.mkdirSync(e,{recursive:!0}),e}function ge(){const e=q.createHash("sha1").update(oe).digest("hex").slice(0,12),t=F.join(pe(),`speak-${e}.ps1`);return z.writeFileSync(t,oe,"utf8"),t}function Te(e){const t=F.join(pe(),`speech-${q.randomUUID()}.txt`);return z.writeFileSync(t,e,"utf8"),t}function he(e){return B.spawn(Re,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function me(e){const t=e.pid;if(t===void 0){e.kill();return}B.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function Ie(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function Ne(e,t){const n=Te(e.text),d=he(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",ge(),"-TextFile",n,"-Rate",String(Ie(e.rate)),"-Voice",e.voice]);let o=!1;const r=()=>{try{z.rmSync(n,{force:!0})}catch{}};return d.on("error",()=>{r(),o||t(!1)}),d.on("close",g=>{r(),o||t(g===0)}),{stop(){o=!0,me(d),r()}}}function Ae(){return new Promise(e=>{const t=he(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",ge(),"-ListVoices"]);let n="",d=!1;const o=g=>{d||(d=!0,clearTimeout(r),e(g))},r=setTimeout(()=>{me(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",g=>{n+=g}),t.on("error",()=>o([])),t.on("close",()=>{const g=n.split(/\r?\n/).map(a=>a.trim()).filter(a=>a.length>0);o(g)})})}const re=String.raw`$ErrorActionPreference = "Stop"
163
+ `,Re="powershell.exe";function ge(){const e=M.join(te.tmpdir(),"opencode-voice-dictation");return j.mkdirSync(e,{recursive:!0}),e}function he(){const e=Q.createHash("sha1").update(se).digest("hex").slice(0,12),t=M.join(ge(),`speak-${e}.ps1`);return j.writeFileSync(t,se,"utf8"),t}function Te(e){const t=M.join(ge(),`speech-${Q.randomUUID()}.txt`);return j.writeFileSync(t,e,"utf8"),t}function me(e){return K.spawn(Re,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function fe(e){const t=e.pid;if(t===void 0){e.kill();return}K.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function Ie(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function Ae(e,t){const i=Te(e.text),l=me(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",he(),"-TextFile",i,"-Rate",String(Ie(e.rate)),"-Voice",e.voice]);let r=!1;const o=()=>{try{j.rmSync(i,{force:!0})}catch{}};return l.on("error",()=>{o(),r||t(!1)}),l.on("close",u=>{o(),r||t(u===0)}),{stop(){r=!0,fe(l),o()}}}function Ne(){return new Promise(e=>{const t=me(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",he(),"-ListVoices"]);let i="",l=!1;const r=u=>{l||(l=!0,clearTimeout(o),e(u))},o=setTimeout(()=>{fe(t),r([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",u=>{i+=u}),t.on("error",()=>r([])),t.on("close",()=>{const u=i.split(/\r?\n/).map(s=>s.trim()).filter(s=>s.length>0);r(u)})})}const ae=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 Le(){const e=F.join(Z.tmpdir(),"opencode-voice-dictation");z.mkdirSync(e,{recursive:!0});const t=q.createHash("sha1").update(re).digest("hex").slice(0,12),n=F.join(e,`win-h-${t}.ps1`);return z.writeFileSync(n,re,"utf8"),n}function _e(e){return B.spawn(Pe,e,{windowsHide:!0,stdio:"ignore"})}function Ve(){return new Promise((e,t)=>{const n=_e(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",Le()]);n.on("error",d=>t(d)),n.on("close",d=>{d===0?e():t(new Error(`Win+H helper exited with code ${d??"unknown"}`))})})}const We=e=>{let t="idle",n="",d="",o,r=!1;const g=()=>{o=void 0,r=!1},a=e.provide(Y,{state:()=>({status:t,error:n,locale:d}),start(v,N){o&&(r=!0,o.stop(),g()),t="starting",n="",d=v.locale,a.changed();const S=N.window;try{o=Ce({locale:v.locale,minConfidence:v.minConfidence},{onReady(y){t="listening",d=y,a.changed()},onFinal(y,b){a.emit("final",{text:y,confidence:b},S)},onError(y,b,_){t="error",n=y,a.changed(),a.emit("error",{code:y,detail:b,available:_},S)},onExit(y,b){if(r){g(),t="idle",n="",d="",a.changed();return}g(),t!=="error"&&(t="error",n=y===null?"ENGINE_EXIT":`ENGINE_EXIT_${y}`,a.changed(),a.emit("error",{code:"ENGINE_EXIT",detail:b||(y===null?"":String(y)),available:[]},S))}})}catch(y){throw t="error",n="ENGINE_START_FAILED",a.changed(),y}},stop(){o&&(r=!0,o.stop(),g()),t="idle",n="",d="",a.changed()},diagnose(){return ke()}});let l;const p=e.provide(ce,{speak(v){l?.stop(),l=Ne(v,N=>{l=void 0,p.emit("done",{ok:N})})},stop(){l&&(l.stop(),l=void 0)},voices(){return Ae()}});e.provide(J,{toggle(){return Ve()}}),e.onCleanup(()=>{o&&(r=!0,o.stop(),g()),l?.stop(),l=void 0})},se={id:"voice-dictation.voice",methods:{edgeVoices:{input:{type:"object",additionalProperties:!1},output:{type:"array",items:{type:"object",properties:{id:{type:"string"},label:{type:"string"},locale:{type:"string"},gender:{type:"string"}},required:["id","label","locale","gender"],additionalProperties:!1}}},edgeSpeak:{input:{type:"object",properties:{text:{type:"string"},voice:{type:"string"},rate:{type:"number"}},required:["text","voice","rate"],additionalProperties:!1},output:{type:"string"}}},events:{}},ze=250;function Oe(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 ae(e,t=ze){const n=e.trim();if(!n)return[];const d=n.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let r="";const g=a=>{let l="";for(const p of a.split(/\s+/))p&&(l&&l.length+1+p.length>t?(o.push(l),l=p):l=l?`${l} ${p}`:p);return l};for(const a of d){if(a.length>t){r&&(o.push(r),r=""),r=g(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 Fe(e,t){const n=e.stores.speech,d=e.stores.tts.value,[o,r]=I.createSignal(d.engine==="edge"?"edge":"system"),[g,a]=I.createSignal(d.voice),l=f=>{(f.engine==="edge"||f.engine==="system")&&r(f.engine),f.voice!==void 0&&a(f.voice)},[p,v]=I.createSignal(!1),[N,S]=I.createSignal(!1),[y,b]=I.createSignal([]),[_,V]=I.createSignal(""),[R,P]=I.createSignal("none");let s=0,w=[],C="",$,W;const i=new Map,h=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,Q=()=>{const f=h()?.getVoices()??[];if(f.length===0)return;const m=n.value.voiceURI;if(m){const T=f.find(D=>D.voiceURI===m);if(T)return T}const E=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return f.find(T=>T.lang.toLowerCase().startsWith(E))??f[0]},O=async()=>{if(o()==="edge"){try{const m=await e.client.rpc(se).edgeVoices({});m.length>0?(b(m.map(E=>({id:E.id,label:E.label}))),V("")):V(e.t("speech.edge.error"))}catch(m){V(m instanceof Error?m.message:String(m))}P("edge");return}V("");const f=h()?.getVoices()??[];if(f.length>0){b(f.map(m=>({id:m.voiceURI,label:`${m.name} — ${m.lang}`}))),P("web");return}try{const m=await t.voices();if(m.length>0){b(m.map(E=>({id:E,label:E}))),P("sapi");return}}catch{}P("none")},H=()=>{$&&($.onended=null,$.onerror=null,$.pause(),$=void 0),W&&(URL.revokeObjectURL(W),W=void 0)},x=()=>{s++,w=[],h()?.cancel(),H(),S(!1),v(!1),R()==="sapi"&&t.stop().catch(()=>{})},K=()=>R()==="web"||R()==="edge",ee=()=>{if(!(!p()||N()||!K())){if(R()==="edge"){$?.pause(),S(!0);return}h()?.pause(),S(!0)}},te=()=>{if(!(!p()||!N()||!K())){if(R()==="edge"){$?.play().catch(()=>{}),S(!1);return}h()?.resume(),S(!1)}},fe=()=>{N()?te():ee()},ve=()=>C?X(C):Promise.resolve(!1),Se=f=>{const m=g()||"es-ES-XimenaNeural",E=n.value.rate,T=ae(f,4e3),D=`${m}|${E}|${f}`,U=i.get(D)??[];i.clear(),i.set(D,U);const L=s;v(!0),S(!1);const j=A=>{if(L===s){if(A>=T.length){S(!1),v(!1);return}(async()=>{let M=U[A];try{M||(M=await e.client.rpc(se).edgeSpeak({text:T[A]??"",voice:m,rate:E}),U[A]=M)}catch{L===s&&(S(!1),v(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}));return}if(L!==s)return;const ye=Uint8Array.from(atob(M),be=>be.charCodeAt(0)),ne=URL.createObjectURL(new Blob([ye],{type:"audio/mpeg"}));H(),W=ne;const G=new Audio(ne);$=G,G.onended=()=>{L===s&&(H(),j(A+1))},G.onerror=()=>{L===s&&(H(),S(!1),v(!1))},await G.play().catch(()=>{L===s&&(H(),S(!1),v(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}))})})()}};return j(0),!0},X=async f=>{C=f;const m=Oe(f,n.value.skipCode);if(!m)return!1;if(x(),await O(),o()==="edge")return Se(m);if(R()==="web"){const E=h();if(!E)return!1;const T=Q(),D=n.value.rate,U=s;w=ae(m),v(!0),S(!1);const L=()=>{if(U!==s)return;const j=w.shift();if(!j){S(!1),v(!1);return}const A=new SpeechSynthesisUtterance(j);T&&(A.voice=T),T?.lang&&(A.lang=T.lang),A.rate=D,A.onend=L,A.onerror=L,E.speak(A)};return L(),!0}if(R()==="sapi"){v(!0),S(!1);try{await t.speak({text:m,rate:n.value.rate,voice:n.value.voiceURI})}catch(E){throw v(!1),E}return!0}throw v(!1),new Error(e.t("read.noVoices"))},we=async()=>{const f=e.sessions.current();if(!f)return"";const E=(await f.server.client.message.list({sessionID:f.id,type:"assistant",order:"desc",limit:1})).data[0];return!E||E.type!=="assistant"?"":E.content.filter(T=>T.type==="text").map(T=>T.text).join(`
192
- `)};return t.on("done",()=>{S(!1),v(!1)}),h()?.addEventListener?.("voiceschanged",()=>{O()}),O(),{speaking:p,paused:N,backend:R,engine:o,voice:g,error:_,canPause:K,setConfig:l,voices:y,refreshVoices:O,lastAssistantText:we,speak:X,cancel:x,pause:ee,resume:te,togglePause:fe,restart:ve,preview:async()=>{const f=await X(e.t("speech.preview.text"));if(f){const m=o()==="edge"?`Edge · ${g()}`:`${R()} · ${n.value.voiceURI||"auto"}`;e.toast.show({variant:"default",title:e.t("read.title"),description:m})}return f}}}const De=["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=k.usePlugin(),n=t.ui,d=t.stores.prefs,o=t.stores.speech,r=t.stores.tts,g=t.stores.engine,a=t.main(Y),l=t.main(J),[p,v]=I.createSignal([]),[N,S]=I.createSignal(!1);I.onMount(()=>{e.speaker.setConfig({engine:r.value.engine,voice:r.value.voice}),a.diagnose().then(i=>{i.locales.length>0&&v(i.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const y=()=>{const i=p(),h=d.value.locale;return(i.includes(h)?i:[h,...i]).map(O=>({value:O,label:O}))},b=()=>{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,V=i=>{if(r.value.engine==="edge"){r.update(h=>{h.voice=i}),e.speaker.setConfig({voice:i});return}P({voiceURI:i})},R=i=>{d.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)})},P=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)})},s=i=>{g.update(h=>{h.mode=i})},w=i=>{r.update(h=>{h.engine=i}),e.speaker.setConfig({engine:i}),e.speaker.refreshVoices()},C=async()=>{try{await l.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")})}},$=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")})}},W=async()=>{S(!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&&!p().length&&v(i.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:i.selected||i.locales[0]||d.value.locale})})}catch(i){t.toast.show({variant:"error",title:t.t("error.title"),description:i instanceof Error?i.message:String(i)})}finally{S(!1)}};return u.createComponent(n.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[u.createComponent(n.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[u.createComponent(n.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return u.createComponent(n.Select,{get label(){return t.t("settings.engine")},get value(){return g.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:i=>s(i)})}}),u.createComponent(I.Show,{get when(){return g.value.mode==="windows"},get children(){return u.createComponent(n.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return u.createComponent(n.Button,{onClick:()=>{C()},get children(){return t.t("settings.winh.run")}})}})}}),u.createComponent(I.Show,{get when(){return g.value.mode==="system"},get children(){return[u.createComponent(n.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return u.createComponent(n.Select,{get label(){return t.t("settings.language")},get value(){return d.value.locale},get options(){return u.memo(()=>y().length>0)()?y():De.map(i=>({value:i,label:i}))},onChange:i=>R({locale:i})})}}),u.createComponent(n.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return u.createComponent(n.Select,{get label(){return t.t("settings.confidence")},get value(){return String(d.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=>R({minConfidence:Number(i)||.4})})}}),u.createComponent(n.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return u.createComponent(n.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return d.value.trailingSpace},onChange:i=>R({trailingSpace:i})})}}),u.createComponent(n.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return u.createComponent(n.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return d.value.autoFocus},onChange:i=>R({autoFocus:i})})}})]}})]}}),u.createComponent(n.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[u.createComponent(n.SettingsRow,{id:"tts-engine",get title(){return t.t("speech.engine")},get description(){return t.t("speech.engine.description")},get children(){return u.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=>w(i)})}}),u.createComponent(n.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return u.createComponent(n.Select,{get label(){return t.t("speech.voice")},get value(){return _()},get options(){return b()},onChange:i=>V(i)})}}),u.createComponent(I.Show,{get when(){return e.speaker.error()},get children(){return u.createComponent(n.SettingsRow,{id:"tts-error",get title(){return t.t("speech.edge.error")},get description(){return e.speaker.error()}})}}),u.createComponent(n.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return u.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=>P({rate:Number(i)||1})})}}),u.createComponent(n.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return u.createComponent(n.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:i=>P({skipCode:i})})}}),u.createComponent(n.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return u.createComponent(n.Button,{onClick:()=>{$()},get children(){return t.t("read.idle")}})}})]}}),u.createComponent(n.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return u.createComponent(n.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return u.createComponent(n.Button,{get busy(){return N()},onClick:()=>{W()},get children(){return t.t("settings.test.run")}})}})}})]}})}function Ue(e){const t=k.usePlugin();return u.memo(()=>e.actions().map(n=>u.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 je=e=>{const t=e.main(Y),n=e.main(ce),d=e.main(J),o=Fe(e,n),r=e.stores.prefs,g=e.stores.engine,[a,l]=I.createSignal(!1);let p;const v=(s,w,C)=>{switch(s){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:w||r.value.locale,available:C.length>0?C.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:s})}},N=()=>{a()&&(l(!1),d.toggle().catch(()=>{})),o.cancel(),p=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(s=>{e.toast.show({variant:"error",title:e.t("error.title"),description:s instanceof Error?s.message:String(s)})})},S=()=>{t.stop().catch(()=>{})},y=()=>{o.cancel(),e.screen.current()?.composer.focus(),d.toggle().then(()=>l(s=>!s)).catch(s=>{l(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:s instanceof Error?`${e.t("error.winh")} ${s.message}`:e.t("error.winh")})})},b=()=>{if(g.value.mode==="windows"){y();return}const s=t.state()?.status??"idle";s==="starting"||s==="listening"?S():N()},_=()=>{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 s=t.state()?.status??"idle";(s==="starting"||s==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const w=await o.lastAssistantText();if(!w.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(w)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(w){e.toast.show({variant:"error",title:e.t("read.title"),description:w instanceof Error?w.message:e.t("read.error")})}})()},V=()=>{if(!o.speaking())return[{id:"start",label:e.t("read.idle"),icon:"outline-audio",run:_}];const s=o.paused(),w=[{id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{o.restart()}}];return o.canPause()&&w.push({id:"pause",label:s?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>o.togglePause()}),w.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",run:()=>o.cancel()}),w};t.on("final",s=>{const w=e.sessions.current()?.key;if(p&&w&&w!==p){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const C=e.screen.current()?.composer;if(!C)return;const $=C.text(),W=r.value.trailingSpace&&$.length>0&&!/\s$/.test($)?" ":"";C.insert(W+s.text),r.value.autoFocus&&C.focus()}),t.on("error",s=>{e.toast.show({variant:"error",title:e.t("error.title"),description:v(s.code,s.detail,s.available),persistent:s.code==="LANGUAGE_NOT_AVAILABLE"||s.code==="NO_RECOGNIZERS",duration:8e3})});const R=()=>{if(e.app.platform!=="desktop")return;if(g.value.mode==="windows")return a()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:b}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:b};const s=t.state()?.status??"idle";return s==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:b}:s==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:b}:s==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:b}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:b}},P=()=>{if(e.app.platform!=="desktop")return;const s=o.speaking(),w=o.paused(),C=s?V().map($=>({id:$.id,label:$.label,icon:$.icon,tone:$.id==="stop"?"danger":void 0,run:$.run})):[];return{label:s?w?e.t("read.paused"):e.t("read.speaking"):e.t("read.idle"),icon:"outline-audio",tone:s?"success":void 0,title:e.t("read.title"),rows:C.length>0?C:void 0,run:s?void 0:_}};e.add(k.ComposerPill,()=>({id:"dictate",show:()=>R()})),e.add(k.ComposerPill,()=>({id:"read",order:1,show:()=>P(),menu:s=>u.createComponent(Ue,{actions:V,get close(){return s.close}})})),e.add(k.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:b})),e.add(k.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(k.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(k.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:()=>u.createComponent(He,{speaker:o})})},Be=k.Plugin.define({...$e,setup:je,main:We});exports.default=Be;
190
+ `,Pe="powershell.exe";function We(){const e=M.join(te.tmpdir(),"opencode-voice-dictation");j.mkdirSync(e,{recursive:!0});const t=Q.createHash("sha1").update(ae).digest("hex").slice(0,12),i=M.join(e,`win-h-${t}.ps1`);return j.writeFileSync(i,ae,"utf8"),i}function Le(e){return K.spawn(Pe,e,{windowsHide:!0,stdio:"ignore"})}function _e(){return new Promise((e,t)=>{const i=Le(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",We()]);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 Ve=e=>{let t="idle",i="",l="",r,o=!1;const u=()=>{r=void 0,o=!1},s=e.provide(ie,{state:()=>({status:t,error:i,locale:l}),start(f,E){r&&(o=!0,r.stop(),u()),t="starting",i="",l=f.locale,s.changed();const S=E.window;try{r=Ee({locale:f.locale,minConfidence:f.minConfidence},{onReady(y){t="listening",l=y,s.changed()},onFinal(y,N){s.emit("final",{text:y,confidence:N},S)},onError(y,N,F){t="error",i=y,s.changed(),s.emit("error",{code:y,detail:N,available:F},S)},onExit(y,N){if(o){u(),t="idle",i="",l="",s.changed();return}u(),t!=="error"&&(t="error",i=y===null?"ENGINE_EXIT":`ENGINE_EXIT_${y}`,s.changed(),s.emit("error",{code:"ENGINE_EXIT",detail:N||(y===null?"":String(y)),available:[]},S))}})}catch(y){throw t="error",i="ENGINE_START_FAILED",s.changed(),y}},stop(){r&&(o=!0,r.stop(),u()),t="idle",i="",l="",s.changed()},diagnose(){return ke()}});let p;const g=e.provide(le,{speak(f){p?.stop(),p=Ae(f,E=>{p=void 0,g.emit("done",{ok:E})})},stop(){p&&(p.stop(),p=void 0)},voices(){return Ne()}});e.provide(ne,{toggle(){return _e()}}),e.onCleanup(()=>{r&&(o=!0,r.stop(),u()),p?.stop(),p=void 0})},J={id:"voice-dictation.voice",methods:{edgeVoices:{input:{type:"object",additionalProperties:!1},output:{type:"array",items:{type:"object",properties:{id:{type:"string"},label:{type:"string"},locale:{type:"string"},gender:{type:"string"}},required:["id","label","locale","gender"],additionalProperties:!1}}},edgeSpeak:{input:{type:"object",properties:{text:{type:"string"},voice:{type:"string"},rate:{type:"number"}},required:["text","voice","rate"],additionalProperties:!1},output:{type:"string"}},whisperTranscribe:{input:{type:"object",properties:{audio:{type:"string"},mime:{type:"string"},language:{type:"string"},python:{type:"string"},model:{type:"string"}},required:["audio","mime","language","python","model"],additionalProperties:!1},output:{type:"object",properties:{text:{type:"string"},language:{type:"string"}},required:["text","language"],additionalProperties:!1}},whisperStatus:{input:{type:"object",properties:{python:{type:"string"},model:{type:"string"}},required:["python","model"],additionalProperties:!1},output:{type:"object",properties:{ready:{type:"boolean"},model:{type:"string"},device:{type:"string"},error:{type:"string"}},required:["ready","model","device","error"],additionalProperties:!1}}},events:{}},ze=250;function Oe(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 ce(e,t=ze){const i=e.trim();if(!i)return[];const l=i.split(new RegExp("(?<=[.!?…])\\s+")),r=[];let o="";const u=s=>{let p="";for(const g of s.split(/\s+/))g&&(p&&p.length+1+g.length>t?(r.push(p),p=g):p=p?`${p} ${g}`:g);return p};for(const s of l){if(s.length>t){o&&(r.push(o),o=""),o=u(s);continue}o&&o.length+1+s.length>t?(r.push(o),o=s):o=o?`${o} ${s}`:s}return o&&r.push(o),r}function Fe(e,t){const i=e.stores.speech,l=e.stores.tts.value,[r,o]=R.createSignal(l.engine==="edge"?"edge":"system"),[u,s]=R.createSignal(l.voice),p=v=>{(v.engine==="edge"||v.engine==="system")&&o(v.engine),v.voice!==void 0&&s(v.voice)},[g,f]=R.createSignal(!1),[E,S]=R.createSignal(!1),[y,N]=R.createSignal([]),[F,O]=R.createSignal(""),[P,U]=R.createSignal("none");let I=0,V=[],k="",A,L;const B=new Map,_=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,a=()=>{const v=_()?.getVoices()??[];if(v.length===0)return;const w=i.value.voiceURI;if(w){const T=v.find(q=>q.voiceURI===w);if(T)return T}const C=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return v.find(T=>T.lang.toLowerCase().startsWith(C))??v[0]},m=async()=>{if(r()==="edge"){try{const w=await e.client.rpc(J).edgeVoices({});w.length>0?(N(w.map(C=>({id:C.id,label:C.label}))),O("")):O(e.t("speech.edge.error"))}catch(w){O(w instanceof Error?w.message:String(w))}U("edge");return}O("");const v=_()?.getVoices()??[];if(v.length>0){N(v.map(w=>({id:w.voiceURI,label:`${w.name} — ${w.lang}`}))),U("web");return}try{const w=await t.voices();if(w.length>0){N(w.map(C=>({id:C,label:C}))),U("sapi");return}}catch{}U("none")},b=()=>{A&&(A.onended=null,A.onerror=null,A.pause(),A=void 0),L&&(URL.revokeObjectURL(L),L=void 0)},n=()=>{I++,V=[],_()?.cancel(),b(),S(!1),f(!1),P()==="sapi"&&t.stop().catch(()=>{})},h=()=>P()==="web"||P()==="edge",D=()=>{if(!(!g()||E()||!h())){if(P()==="edge"){A?.pause(),S(!0);return}_()?.pause(),S(!0)}},H=()=>{if(!(!g()||!E()||!h())){if(P()==="edge"){A?.play().catch(()=>{}),S(!1);return}_()?.resume(),S(!1)}},x=()=>{E()?H():D()},we=()=>k?ee(k):Promise.resolve(!1),ve=v=>{const w=u()||"es-ES-XimenaNeural",C=i.value.rate,T=ce(v,4e3),q=`${w}|${C}|${v}`,G=B.get(q)??[];B.clear(),B.set(q,G);const z=I;f(!0),S(!1);const X=W=>{if(z===I){if(W>=T.length){S(!1),f(!1);return}(async()=>{let Z=G[W];try{Z||(Z=await e.client.rpc(J).edgeSpeak({text:T[W]??"",voice:w,rate:C}),G[W]=Z)}catch{z===I&&(S(!1),f(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}));return}if(z!==I)return;const ye=Uint8Array.from(atob(Z),be=>be.charCodeAt(0)),re=URL.createObjectURL(new Blob([ye],{type:"audio/mpeg"}));b(),L=re;const Y=new Audio(re);A=Y,Y.onended=()=>{z===I&&(b(),X(W+1))},Y.onerror=()=>{z===I&&(b(),S(!1),f(!1))},await Y.play().catch(()=>{z===I&&(b(),S(!1),f(!1),e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("error.edge")}))})})()}};return X(0),!0},ee=async v=>{k=v;const w=Oe(v,i.value.skipCode);if(!w)return!1;if(n(),await m(),r()==="edge")return ve(w);if(P()==="web"){const C=_();if(!C)return!1;const T=a(),q=i.value.rate,G=I;V=ce(w),f(!0),S(!1);const z=()=>{if(G!==I)return;const X=V.shift();if(!X){S(!1),f(!1);return}const W=new SpeechSynthesisUtterance(X);T&&(W.voice=T),T?.lang&&(W.lang=T.lang),W.rate=q,W.onend=z,W.onerror=z,C.speak(W)};return z(),!0}if(P()==="sapi"){f(!0),S(!1);try{await t.speak({text:w,rate:i.value.rate,voice:i.value.voiceURI})}catch(C){throw f(!1),C}return!0}throw f(!1),new Error(e.t("read.noVoices"))},Se=async()=>{const v=e.sessions.current();if(!v)return"";const C=(await v.server.client.message.list({sessionID:v.id,type:"assistant",order:"desc",limit:1})).data[0];return!C||C.type!=="assistant"?"":C.content.filter(T=>T.type==="text").map(T=>T.text).join(`
192
+ `)};return t.on("done",()=>{S(!1),f(!1)}),_()?.addEventListener?.("voiceschanged",()=>{m()}),m(),{speaking:g,paused:E,backend:P,engine:r,voice:u,error:F,canPause:h,setConfig:p,voices:y,refreshVoices:m,lastAssistantText:Se,speak:ee,cancel:n,pause:D,resume:H,togglePause:x,restart:we,preview:async()=>{const v=await ee(e.t("speech.preview.text"));if(v){const w=r()==="edge"?`Edge · ${u()}`:`${P()} · ${i.value.voiceURI||"auto"}`;e.toast.show({variant:"default",title:e.t("read.title"),description:w})}return v}}}const Ue=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function De(e){const t=$.usePlugin(),i=t.ui,l=t.stores.prefs,r=t.stores.speech,o=t.stores.tts,u=t.stores.engine,s=t.stores.whisper,p=t.main(ie),g=t.main(ne),[f,E]=R.createSignal([]),[S,y]=R.createSignal(!1),[N,F]=R.createSignal(!1);R.onMount(()=>{e.speaker.setConfig({engine:o.value.engine,voice:o.value.voice}),p.diagnose().then(n=>{n.locales.length>0&&E(n.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const O=()=>{const n=f(),h=l.value.locale;return(n.includes(h)?n:[h,...n]).map(H=>({value:H,label:H}))},P=()=>{const n=e.speaker.voices().map(h=>({value:h.id,label:h.label}));return o.value.engine==="edge"?n:[{value:"",label:t.t("speech.voice.auto")},...n]},U=()=>o.value.engine==="edge"?o.value.voice:r.value.voiceURI,I=n=>{if(o.value.engine==="edge"){o.update(h=>{h.voice=n}),e.speaker.setConfig({voice:n});return}k({voiceURI:n})},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)})},k=n=>{r.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)})},A=n=>{u.update(h=>{h.mode=n})},L=n=>{s.update(h=>{n.python!==void 0&&(h.python=n.python),n.model!==void 0&&(h.model=n.model),n.language!==void 0&&(h.language=n.language)})},B=async()=>{F(!0);try{const n=await t.client.rpc(J).whisperStatus({python:s.value.python,model:s.value.model});n.ready?t.toast.show({variant:"success",title:t.t("pill.title"),description:t.t("whisper.test.ready",{model:n.model,device:n.device})}):t.toast.show({variant:"error",title:t.t("pill.title"),description:t.t("whisper.test.error",{error:n.error||"?"})})}catch(n){t.toast.show({variant:"error",title:t.t("pill.title"),description:t.t("whisper.test.error",{error:n instanceof Error?n.message:String(n)})})}finally{F(!1)}},_=n=>{o.update(h=>{h.engine=n}),e.speaker.setConfig({engine:n}),e.speaker.refreshVoices()},a=async()=>{try{await g.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")})}},b=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&&!f().length&&E(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 d.createComponent(i.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[d.createComponent(i.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[d.createComponent(i.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("settings.engine")},get value(){return u.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")},{value:"whisper",label:t.t("engine.whisper")}]},onChange:n=>A(n)})}}),d.createComponent(R.Show,{get when(){return u.value.mode==="windows"},get children(){return d.createComponent(i.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return d.createComponent(i.Button,{onClick:()=>{a()},get children(){return t.t("settings.winh.run")}})}})}}),d.createComponent(R.Show,{get when(){return u.value.mode==="whisper"},get children(){return[d.createComponent(i.SettingsRow,{id:"whisper-python",get title(){return t.t("whisper.python")},get description(){return t.t("whisper.python.description")},get children(){return d.createComponent(i.TextInput,{get label(){return t.t("whisper.python")},get value(){return s.value.python},onInput:n=>L({python:n})})}}),d.createComponent(i.SettingsRow,{id:"whisper-model",get title(){return t.t("whisper.model")},get description(){return t.t("whisper.model.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("whisper.model")},get value(){return s.value.model},options:[{value:"large-v3-turbo",label:"large-v3-turbo"},{value:"large-v3",label:"large-v3"},{value:"medium",label:"medium"}],onChange:n=>L({model:n})})}}),d.createComponent(i.SettingsRow,{id:"whisper-language",get title(){return t.t("whisper.language")},get description(){return t.t("whisper.language.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("whisper.language")},get value(){return s.value.language},get options(){return[{value:"",label:t.t("whisper.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:n=>L({language:n})})}}),d.createComponent(i.SettingsRow,{id:"whisper-test",get title(){return t.t("whisper.test")},get description(){return t.t("whisper.test.description")},get children(){return d.createComponent(i.Button,{get busy(){return N()},onClick:()=>{B()},get children(){return t.t("whisper.test.run")}})}})]}}),d.createComponent(R.Show,{get when(){return u.value.mode==="system"},get children(){return[d.createComponent(i.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("settings.language")},get value(){return l.value.locale},get options(){return d.memo(()=>O().length>0)()?O():Ue.map(n=>({value:n,label:n}))},onChange:n=>V({locale:n})})}}),d.createComponent(i.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return d.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})})}}),d.createComponent(i.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return d.createComponent(i.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return l.value.trailingSpace},onChange:n=>V({trailingSpace:n})})}}),d.createComponent(i.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return d.createComponent(i.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return l.value.autoFocus},onChange:n=>V({autoFocus:n})})}})]}})]}}),d.createComponent(i.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[d.createComponent(i.SettingsRow,{id:"tts-engine",get title(){return t.t("speech.engine")},get description(){return t.t("speech.engine.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("speech.engine")},get value(){return o.value.engine},get options(){return[{value:"edge",label:t.t("speech.engine.edge")},{value:"system",label:t.t("speech.engine.system")}]},onChange:n=>_(n)})}}),d.createComponent(i.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return d.createComponent(i.Select,{get label(){return t.t("speech.voice")},get value(){return U()},get options(){return P()},onChange:n=>I(n)})}}),d.createComponent(R.Show,{get when(){return e.speaker.error()},get children(){return d.createComponent(i.SettingsRow,{id:"tts-error",get title(){return t.t("speech.edge.error")},get description(){return e.speaker.error()}})}}),d.createComponent(i.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return d.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=>k({rate:Number(n)||1})})}}),d.createComponent(i.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return d.createComponent(i.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return r.value.skipCode},onChange:n=>k({skipCode:n})})}}),d.createComponent(i.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return d.createComponent(i.Button,{onClick:()=>{m()},get children(){return t.t("read.idle")}})}})]}}),d.createComponent(i.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return d.createComponent(i.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return d.createComponent(i.Button,{get busy(){return S()},onClick:()=>{b()},get children(){return t.t("settings.test.run")}})}})}})]}})}async function He(){const e=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,noiseSuppression:!0}}),t=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm",i=new MediaRecorder(e,{mimeType:t}),l=[];return i.ondataavailable=r=>{r.data.size>0&&l.push(r.data)},i.start(),{stop:()=>new Promise(r=>{i.onstop=()=>{e.getTracks().forEach(o=>o.stop()),r(new Blob(l,{type:t}))},i.stop()})}}async function je(e){const t=new Uint8Array(await e.arrayBuffer());let i="";const l=32768;for(let r=0;r<t.length;r+=l)i+=String.fromCharCode(...t.subarray(r,r+l));return btoa(i)}function Be(e){const t=$.usePlugin();return d.memo(()=>e.actions().map(i=>d.createComponent(t.ui.Button,{get variant(){return i.id==="stop"?"danger":"neutral"},get icon(){return i.icon},onClick:()=>{i.run(),e.close()},get children(){return i.label}})))}const Me=e=>{const t=e.main(ie),i=e.main(le),l=e.main(ne),r=Fe(e,i),o=e.stores.prefs,u=e.stores.engine,s=e.stores.whisper,[p,g]=R.createSignal(!1),[f,E]=R.createSignal("idle");let S,y;const N=(a,m,b)=>{switch(a){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:m||o.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:a})}},F=()=>{p()&&(g(!1),l.toggle().catch(()=>{})),r.cancel(),S=e.sessions.current()?.key,t.start({locale:o.value.locale,minConfidence:o.value.minConfidence}).catch(a=>{e.toast.show({variant:"error",title:e.t("error.title"),description:a instanceof Error?a.message:String(a)})})},O=()=>{t.stop().catch(()=>{})},P=()=>{r.cancel(),e.screen.current()?.composer.focus(),l.toggle().then(()=>g(a=>!a)).catch(a=>{g(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:a instanceof Error?`${e.t("error.winh")} ${a.message}`:e.t("error.winh")})})},U=async()=>{if(f()==="idle"){r.cancel();try{y=await He(),E("recording")}catch(a){e.toast.show({variant:"error",title:e.t("pill.title"),description:a instanceof Error?a.message:e.t("error.whisper")})}}},I=()=>{const a=y;y=void 0,a&&a.stop().catch(()=>{}),E("idle")},V=async()=>{const a=y;if(y=void 0,!a){E("idle");return}E("transcribing");try{const m=await a.stop(),b=await je(m),h=(await e.client.rpc(J).whisperTranscribe({audio:b,mime:m.type,language:s.value.language,python:s.value.python,model:s.value.model})).text.trim();if(!h){e.toast.show({variant:"default",title:e.t("pill.title"),description:e.t("whisper.empty")});return}const D=e.screen.current()?.composer;if(D){const H=D.text(),x=H.length>0&&!/\s$/.test(H)?" ":"";D.insert(x+h),D.focus()}}catch(m){e.toast.show({variant:"error",title:e.t("pill.title"),description:m instanceof Error?m.message:e.t("error.whisper")})}finally{E("idle")}},k=()=>{if(u.value.mode==="whisper"){f()==="recording"?V():f()==="idle"&&U();return}if(u.value.mode==="windows"){P();return}const a=t.state()?.status??"idle";a==="starting"||a==="listening"?O():F()},A=()=>{if(f()==="recording"&&I(),r.speaking()){r.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const a=t.state()?.status??"idle";(a==="starting"||a==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const m=await r.lastAssistantText();if(!m.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await r.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")})}})()},L=()=>{if(!r.speaking())return[{id:"start",label:e.t("read.idle"),icon:"outline-audio",run:A}];const a=r.paused(),m=[{id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{r.restart()}}];return r.canPause()&&m.push({id:"pause",label:a?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>r.togglePause()}),m.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",run:()=>r.cancel()}),m};t.on("final",a=>{const m=e.sessions.current()?.key;if(S&&m&&m!==S){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 n=b.text(),h=o.value.trailingSpace&&n.length>0&&!/\s$/.test(n)?" ":"";b.insert(h+a.text),o.value.autoFocus&&b.focus()}),t.on("error",a=>{e.toast.show({variant:"error",title:e.t("error.title"),description:N(a.code,a.detail,a.available),persistent:a.code==="LANGUAGE_NOT_AVAILABLE"||a.code==="NO_RECOGNIZERS",duration:8e3})});const B=()=>{if(e.app.platform!=="desktop")return;if(u.value.mode==="windows")return p()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:k}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:k};if(u.value.mode==="whisper")return f()==="recording"?{label:e.t("pill.recording"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:k}:f()==="transcribing"?{label:e.t("pill.transcribing"),icon:"outline-audio",title:e.t("pill.whisper")}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.whisper"),run:k};const a=t.state()?.status??"idle";return a==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:k}:a==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:k}:a==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:k}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:k}},_=()=>{if(e.app.platform!=="desktop")return;const a=r.speaking(),m=r.paused(),b=a?L().map(n=>({id:n.id,label:n.label,icon:n.icon,tone:n.id==="stop"?"danger":void 0,run:n.run})):[];return{label:a?m?e.t("read.paused"):e.t("read.speaking"):e.t("read.idle"),icon:"outline-audio",tone:a?"success":void 0,title:e.t("read.title"),rows:b.length>0?b:void 0,run:a?void 0:A}};e.add($.ComposerPill,()=>({id:"dictate",show:()=>B()})),e.add($.ComposerPill,()=>({id:"read",order:1,show:()=>_(),menu:a=>d.createComponent(Be,{actions:L,get close(){return a.close}})})),e.add($.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:k})),e.add($.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($.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:A})),e.add($.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:"whisper-python",title:e.t("whisper.python"),keywords:"whisper python venv entorno"},{id:"whisper-model",title:e.t("whisper.model"),keywords:"whisper model modelo"},{id:"whisper-language",title:e.t("whisper.language"),keywords:"whisper language idioma"},{id:"whisper-test",title:e.t("whisper.test"),keywords:"whisper check comprobar"},{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:()=>d.createComponent(De,{speaker:r})})},qe=$.Plugin.define({...Ce,setup:Me,main:Ve});exports.default=qe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-voice-dictation",
3
- "version": "0.5.0",
3
+ "version": "0.6.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>",
package/src/edge.ts CHANGED
@@ -1,5 +1,11 @@
1
- import { MsEdgeTTS, OUTPUT_FORMAT } from "msedge-tts"
1
+ type EdgeModule = typeof import("msedge-tts")
2
2
 
3
+ let modulePromise: Promise<EdgeModule> | undefined
4
+
5
+ function load(): Promise<EdgeModule> {
6
+ modulePromise ??= import("msedge-tts")
7
+ return modulePromise
8
+ }
3
9
  export interface EdgeVoiceInfo {
4
10
  id: string
5
11
  label: string
@@ -26,6 +32,7 @@ function shortName(voice: RawVoice): string {
26
32
  }
27
33
 
28
34
  export async function listEdgeVoices(): Promise<EdgeVoiceInfo[]> {
35
+ const { MsEdgeTTS } = await load()
29
36
  const tts = new MsEdgeTTS()
30
37
 
31
38
  try {
@@ -50,6 +57,7 @@ export async function synthesizeEdge(input: {
50
57
  voice: string
51
58
  rate: number
52
59
  }): Promise<string> {
60
+ const { MsEdgeTTS, OUTPUT_FORMAT } = await load()
53
61
  const tts = new MsEdgeTTS()
54
62
 
55
63
  try {
@@ -55,6 +55,18 @@ export const definition = Plugin.define({
55
55
  voice: "es-ES-XimenaNeural",
56
56
  },
57
57
  ),
58
+ whisper: Store.global(
59
+ Schema.Struct({
60
+ python: Schema.String,
61
+ model: Schema.String,
62
+ language: Schema.String,
63
+ }),
64
+ {
65
+ python: "~/peopleby/voice_to_text/api_voice/demo/whisperx-env/bin/python",
66
+ model: "large-v3-turbo",
67
+ language: "",
68
+ },
69
+ ),
58
70
  },
59
71
  i18n: {
60
72
  en: {
@@ -84,6 +96,25 @@ export const definition = Plugin.define({
84
96
  "settings.engine.description": "Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.",
85
97
  "engine.windows": "Windows Voice Typing (Win+H)",
86
98
  "engine.system": "System.Speech (offline)",
99
+ "engine.whisper": "WhisperX local (WSL)",
100
+ "pill.recording": "Recording…",
101
+ "pill.transcribing": "Transcribing…",
102
+ "pill.whisper": "Record and transcribe with WhisperX",
103
+ "error.whisper": "WhisperX could not transcribe the audio.",
104
+ "whisper.section": "WhisperX (local)",
105
+ "whisper.python": "Environment Python",
106
+ "whisper.python.description": "Python of the venv that has whisperx installed.",
107
+ "whisper.model": "Model",
108
+ "whisper.model.description": "large-v3-turbo is the fastest with good accuracy on a GPU.",
109
+ "whisper.language": "Language",
110
+ "whisper.language.description": "Automatic detects the language of each recording.",
111
+ "whisper.language.auto": "Automatic",
112
+ "whisper.test": "Check WhisperX",
113
+ "whisper.test.description": "Starts the worker and reports whether the model loads.",
114
+ "whisper.test.run": "Check",
115
+ "whisper.test.ready": "WhisperX ready: {model} ({device})",
116
+ "whisper.test.error": "WhisperX is not ready: {error}",
117
+ "whisper.empty": "WhisperX returned no text.",
87
118
  "settings.winh": "Test Win+H",
88
119
  "settings.winh.description": "Sends the Windows voice typing shortcut. If the floating bar appears, it works.",
89
120
  "settings.winh.run": "Test Win+H",
@@ -171,6 +202,25 @@ export const definition = Plugin.define({
171
202
  "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.",
172
203
  "engine.windows": "Windows Voice Typing (Win+H)",
173
204
  "engine.system": "System.Speech (offline)",
205
+ "engine.whisper": "WhisperX local (WSL)",
206
+ "pill.recording": "Grabando…",
207
+ "pill.transcribing": "Transcribiendo…",
208
+ "pill.whisper": "Grabar y transcribir con WhisperX",
209
+ "error.whisper": "WhisperX no pudo transcribir el audio.",
210
+ "whisper.section": "WhisperX (local)",
211
+ "whisper.python": "Python del entorno",
212
+ "whisper.python.description": "Python del venv que tiene whisperx instalado.",
213
+ "whisper.model": "Modelo",
214
+ "whisper.model.description": "large-v3-turbo es el más rápido con buena precisión en GPU.",
215
+ "whisper.language": "Idioma",
216
+ "whisper.language.description": "Automático detecta el idioma de cada grabación.",
217
+ "whisper.language.auto": "Automático",
218
+ "whisper.test": "Comprobar WhisperX",
219
+ "whisper.test.description": "Arranca el worker e indica si el modelo carga.",
220
+ "whisper.test.run": "Comprobar",
221
+ "whisper.test.ready": "WhisperX listo: {model} ({device})",
222
+ "whisper.test.error": "WhisperX no está listo: {error}",
223
+ "whisper.empty": "WhisperX no devolvió texto.",
174
224
  "settings.winh": "Probar Win+H",
175
225
  "settings.winh.description": "Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.",
176
226
  "settings.winh.run": "Probar Win+H",
@@ -0,0 +1,51 @@
1
+ export interface Recording {
2
+ stop(): Promise<Blob>
3
+ }
4
+
5
+ /** Records the microphone to WebM/Opus until `stop()` is called. */
6
+ export async function startRecording(): Promise<Recording> {
7
+ const stream = await navigator.mediaDevices.getUserMedia({
8
+ audio: {
9
+ channelCount: 1,
10
+ echoCancellation: true,
11
+ noiseSuppression: true,
12
+ },
13
+ })
14
+
15
+ const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
16
+ ? "audio/webm;codecs=opus"
17
+ : "audio/webm"
18
+
19
+ const recorder = new MediaRecorder(stream, { mimeType })
20
+ const chunks: BlobPart[] = []
21
+
22
+ recorder.ondataavailable = (event) => {
23
+ if (event.data.size > 0) chunks.push(event.data)
24
+ }
25
+
26
+ recorder.start()
27
+
28
+ return {
29
+ stop: () =>
30
+ new Promise<Blob>((resolve) => {
31
+ recorder.onstop = () => {
32
+ stream.getTracks().forEach((track) => track.stop())
33
+ resolve(new Blob(chunks, { type: mimeType }))
34
+ }
35
+ recorder.stop()
36
+ }),
37
+ }
38
+ }
39
+
40
+ /** Base64 for the RPC bridge. */
41
+ export async function blobToBase64(blob: Blob): Promise<string> {
42
+ const bytes = new Uint8Array(await blob.arrayBuffer())
43
+ let binary = ""
44
+ const chunk = 0x8000
45
+
46
+ for (let index = 0; index < bytes.length; index += chunk) {
47
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk))
48
+ }
49
+
50
+ return btoa(binary)
51
+ }
@@ -3,6 +3,7 @@ import { usePlugin } from "@opencode/plugin/gui"
3
3
  import { Dictation, VoiceTyping } from "./contract"
4
4
  import type { definition } from "./definition"
5
5
  import type { Speaker } from "./speak"
6
+ import { VoiceRpc } from "../rpc"
6
7
 
7
8
  const FALLBACK_LOCALES = [
8
9
  "es-ES",
@@ -29,11 +30,13 @@ export function VoiceSettings(props: { speaker: Speaker }) {
29
30
  const speechPrefs = ctx.stores.speech
30
31
  const ttsPrefs = ctx.stores.tts
31
32
  const enginePrefs = ctx.stores.engine
33
+ const whisperPrefs = ctx.stores.whisper
32
34
  const dictation = ctx.main(Dictation)
33
35
  const voiceTyping = ctx.main(VoiceTyping)
34
36
 
35
37
  const [locales, setLocales] = createSignal<readonly string[]>([])
36
38
  const [busy, setBusy] = createSignal(false)
39
+ const [whisperBusy, setWhisperBusy] = createSignal(false)
37
40
 
38
41
  onMount(() => {
39
42
  props.speaker.setConfig({ engine: ttsPrefs.value.engine, voice: ttsPrefs.value.voice })
@@ -96,6 +99,48 @@ export function VoiceSettings(props: { speaker: Speaker }) {
96
99
  })
97
100
  }
98
101
 
102
+ const patchWhisper = (change: { python?: string; model?: string; language?: string }) => {
103
+ whisperPrefs.update((draft) => {
104
+ if (change.python !== undefined) draft.python = change.python
105
+ if (change.model !== undefined) draft.model = change.model
106
+ if (change.language !== undefined) draft.language = change.language
107
+ })
108
+ }
109
+
110
+ const testWhisper = async () => {
111
+ setWhisperBusy(true)
112
+ try {
113
+ const status = (await ctx.client.rpc(VoiceRpc).whisperStatus({
114
+ python: whisperPrefs.value.python,
115
+ model: whisperPrefs.value.model,
116
+ })) as { ready: boolean; model: string; device: string; error: string }
117
+
118
+ if (status.ready) {
119
+ ctx.toast.show({
120
+ variant: "success",
121
+ title: ctx.t("pill.title"),
122
+ description: ctx.t("whisper.test.ready", { model: status.model, device: status.device }),
123
+ })
124
+ } else {
125
+ ctx.toast.show({
126
+ variant: "error",
127
+ title: ctx.t("pill.title"),
128
+ description: ctx.t("whisper.test.error", { error: status.error || "?" }),
129
+ })
130
+ }
131
+ } catch (cause) {
132
+ ctx.toast.show({
133
+ variant: "error",
134
+ title: ctx.t("pill.title"),
135
+ description: ctx.t("whisper.test.error", {
136
+ error: cause instanceof Error ? cause.message : String(cause),
137
+ }),
138
+ })
139
+ } finally {
140
+ setWhisperBusy(false)
141
+ }
142
+ }
143
+
99
144
  const patchTts = (engine: string) => {
100
145
  ttsPrefs.update((draft) => {
101
146
  draft.engine = engine
@@ -190,6 +235,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
190
235
  options={[
191
236
  { value: "windows", label: ctx.t("engine.windows") },
192
237
  { value: "system", label: ctx.t("engine.system") },
238
+ { value: "whisper", label: ctx.t("engine.whisper") },
193
239
  ]}
194
240
  onChange={(value) => patchEngine(value)}
195
241
  />
@@ -205,6 +251,65 @@ export function VoiceSettings(props: { speaker: Speaker }) {
205
251
  </ui.SettingsRow>
206
252
  </Show>
207
253
 
254
+ <Show when={enginePrefs.value.mode === "whisper"}>
255
+ <ui.SettingsRow
256
+ id="whisper-python"
257
+ title={ctx.t("whisper.python")}
258
+ description={ctx.t("whisper.python.description")}
259
+ >
260
+ <ui.TextInput
261
+ label={ctx.t("whisper.python")}
262
+ value={whisperPrefs.value.python}
263
+ onInput={(value) => patchWhisper({ python: value })}
264
+ />
265
+ </ui.SettingsRow>
266
+
267
+ <ui.SettingsRow
268
+ id="whisper-model"
269
+ title={ctx.t("whisper.model")}
270
+ description={ctx.t("whisper.model.description")}
271
+ >
272
+ <ui.Select
273
+ label={ctx.t("whisper.model")}
274
+ value={whisperPrefs.value.model}
275
+ options={[
276
+ { value: "large-v3-turbo", label: "large-v3-turbo" },
277
+ { value: "large-v3", label: "large-v3" },
278
+ { value: "medium", label: "medium" },
279
+ ]}
280
+ onChange={(value) => patchWhisper({ model: value })}
281
+ />
282
+ </ui.SettingsRow>
283
+
284
+ <ui.SettingsRow
285
+ id="whisper-language"
286
+ title={ctx.t("whisper.language")}
287
+ description={ctx.t("whisper.language.description")}
288
+ >
289
+ <ui.Select
290
+ label={ctx.t("whisper.language")}
291
+ value={whisperPrefs.value.language}
292
+ options={[
293
+ { value: "", label: ctx.t("whisper.language.auto") },
294
+ { value: "es", label: "Español" },
295
+ { value: "en", label: "English" },
296
+ { value: "pt", label: "Português" },
297
+ ]}
298
+ onChange={(value) => patchWhisper({ language: value })}
299
+ />
300
+ </ui.SettingsRow>
301
+
302
+ <ui.SettingsRow
303
+ id="whisper-test"
304
+ title={ctx.t("whisper.test")}
305
+ description={ctx.t("whisper.test.description")}
306
+ >
307
+ <ui.Button busy={whisperBusy()} onClick={() => void testWhisper()}>
308
+ {ctx.t("whisper.test.run")}
309
+ </ui.Button>
310
+ </ui.SettingsRow>
311
+ </Show>
312
+
208
313
  <Show when={enginePrefs.value.mode === "system"}>
209
314
  <ui.SettingsRow
210
315
  id="language"
@@ -13,6 +13,8 @@ import { Dictation, Speech, VoiceTyping } from "./contract"
13
13
  import type { definition } from "./definition"
14
14
  import { createSpeaker, type Speaker } from "./speak"
15
15
  import { VoiceSettings } from "./settings"
16
+ import { blobToBase64, startRecording, type Recording } from "./recorder"
17
+ import { VoiceRpc } from "../rpc"
16
18
 
17
19
  interface ReadAction {
18
20
  id: string
@@ -53,9 +55,12 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
53
55
  const speaker = createSpeaker(ctx, speech)
54
56
  const prefs = ctx.stores.prefs
55
57
  const engine = ctx.stores.engine
58
+ const whisper = ctx.stores.whisper
56
59
  const [windowsActive, setWindowsActive] = createSignal(false)
60
+ const [whisperState, setWhisperState] = createSignal<"idle" | "recording" | "transcribing">("idle")
57
61
 
58
62
  let targetSession: string | undefined
63
+ let recording: Recording | undefined
59
64
 
60
65
  const reason = (code: string, detail: string, available: readonly string[]) => {
61
66
  switch (code) {
@@ -117,7 +122,83 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
117
122
  })
118
123
  }
119
124
 
125
+ const startWhisper = async () => {
126
+ if (whisperState() !== "idle") return
127
+ speaker.cancel()
128
+
129
+ try {
130
+ recording = await startRecording()
131
+ setWhisperState("recording")
132
+ } catch (cause) {
133
+ ctx.toast.show({
134
+ variant: "error",
135
+ title: ctx.t("pill.title"),
136
+ description: cause instanceof Error ? cause.message : ctx.t("error.whisper"),
137
+ })
138
+ }
139
+ }
140
+
141
+ const cancelWhisper = () => {
142
+ const current = recording
143
+ recording = undefined
144
+ if (current) void current.stop().catch(() => {})
145
+ setWhisperState("idle")
146
+ }
147
+
148
+ const stopWhisper = async () => {
149
+ const current = recording
150
+ recording = undefined
151
+
152
+ if (!current) {
153
+ setWhisperState("idle")
154
+ return
155
+ }
156
+
157
+ setWhisperState("transcribing")
158
+
159
+ try {
160
+ const blob = await current.stop()
161
+ const audio = await blobToBase64(blob)
162
+ const result = (await ctx.client.rpc(VoiceRpc).whisperTranscribe({
163
+ audio,
164
+ mime: blob.type,
165
+ language: whisper.value.language,
166
+ python: whisper.value.python,
167
+ model: whisper.value.model,
168
+ })) as { text: string; language: string }
169
+
170
+ const text = result.text.trim()
171
+
172
+ if (!text) {
173
+ ctx.toast.show({ variant: "default", title: ctx.t("pill.title"), description: ctx.t("whisper.empty") })
174
+ return
175
+ }
176
+
177
+ const composer = ctx.screen.current()?.composer
178
+ if (composer) {
179
+ const existing = composer.text()
180
+ const separator = existing.length > 0 && !/\s$/.test(existing) ? " " : ""
181
+ composer.insert(separator + text)
182
+ composer.focus()
183
+ }
184
+ } catch (cause) {
185
+ ctx.toast.show({
186
+ variant: "error",
187
+ title: ctx.t("pill.title"),
188
+ description: cause instanceof Error ? cause.message : ctx.t("error.whisper"),
189
+ })
190
+ } finally {
191
+ setWhisperState("idle")
192
+ }
193
+ }
194
+
120
195
  const toggle = () => {
196
+ if (engine.value.mode === "whisper") {
197
+ if (whisperState() === "recording") void stopWhisper()
198
+ else if (whisperState() === "idle") void startWhisper()
199
+ return
200
+ }
201
+
121
202
  if (engine.value.mode === "windows") {
122
203
  toggleWindows()
123
204
  return
@@ -129,6 +210,8 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
129
210
  }
130
211
 
131
212
  const read = () => {
213
+ if (whisperState() === "recording") cancelWhisper()
214
+
132
215
  if (speaker.speaking()) {
133
216
  speaker.cancel()
134
217
  return
@@ -235,6 +318,33 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
235
318
  }
236
319
  }
237
320
 
321
+ if (engine.value.mode === "whisper") {
322
+ if (whisperState() === "recording") {
323
+ return {
324
+ label: ctx.t("pill.recording"),
325
+ icon: "outline-wave-audio",
326
+ tone: "danger",
327
+ title: ctx.t("pill.stop"),
328
+ run: toggle,
329
+ }
330
+ }
331
+
332
+ if (whisperState() === "transcribing") {
333
+ return {
334
+ label: ctx.t("pill.transcribing"),
335
+ icon: "outline-audio",
336
+ title: ctx.t("pill.whisper"),
337
+ }
338
+ }
339
+
340
+ return {
341
+ label: ctx.t("pill.idle"),
342
+ icon: "outline-wave-audio",
343
+ title: ctx.t("pill.whisper"),
344
+ run: toggle,
345
+ }
346
+ }
347
+
238
348
  const status = dictation.state()?.status ?? "idle"
239
349
 
240
350
  if (status === "starting") {
@@ -350,6 +460,10 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
350
460
  { id: "test", title: ctx.t("settings.test"), keywords: "voice microphone microphone test microfono" },
351
461
  { id: "voice", title: ctx.t("speech.voice"), keywords: "read aloud voice voz lectura altavoz" },
352
462
  { id: "tts-engine", title: ctx.t("speech.engine"), keywords: "voice engine motor edge neural sistema" },
463
+ { id: "whisper-python", title: ctx.t("whisper.python"), keywords: "whisper python venv entorno" },
464
+ { id: "whisper-model", title: ctx.t("whisper.model"), keywords: "whisper model modelo" },
465
+ { id: "whisper-language", title: ctx.t("whisper.language"), keywords: "whisper language idioma" },
466
+ { id: "whisper-test", title: ctx.t("whisper.test"), keywords: "whisper check comprobar" },
353
467
  { id: "rate", title: ctx.t("speech.rate"), keywords: "read aloud speed velocidad lectura" },
354
468
  { id: "skip-code", title: ctx.t("speech.skipCode"), keywords: "read aloud code codigo lectura" },
355
469
  { id: "preview", title: ctx.t("speech.preview"), keywords: "read aloud test voice probar voz" },
package/src/index.ts CHANGED
@@ -1,22 +1,50 @@
1
1
  import { Plugin } from "@opencode/plugin"
2
2
  import { listEdgeVoices, synthesizeEdge } from "./edge.js"
3
3
  import { VoiceRpc } from "./rpc.js"
4
+ import { WhisperEngine } from "./whisper/server.js"
4
5
 
5
6
  /**
6
7
  * Server part of the plugin.
7
8
  *
8
- * It registers the Edge TTS RPC. Running it here (instead of in the Electron
9
- * main process) lets `msedge-tts` resolve from the plugin's own node_modules.
9
+ * It registers the Edge TTS and Whisper RPCs. Running them here (instead of
10
+ * in the Electron main process) lets their dependencies resolve from the
11
+ * plugin's own node_modules.
10
12
  */
11
13
  export default Plugin.define({
12
14
  id: "voice-dictation",
13
15
  async setup(ctx) {
16
+ const whisper = new WhisperEngine()
17
+
14
18
  await ctx.rpc.register(VoiceRpc, {
15
19
  edgeVoices: async () => listEdgeVoices(),
16
20
  edgeSpeak: async (input) => {
17
21
  const { text, voice, rate } = input as { text: string; voice: string; rate: number }
18
22
  return synthesizeEdge({ text, voice, rate })
19
23
  },
24
+ whisperTranscribe: async (input) => {
25
+ return whisper.transcribe(input as {
26
+ audio: string
27
+ mime: string
28
+ language: string
29
+ python: string
30
+ model: string
31
+ })
32
+ },
33
+ whisperStatus: async (input) => {
34
+ const { python, model } = input as { python: string; model: string }
35
+ try {
36
+ return await whisper.warm(python, model)
37
+ } catch (cause) {
38
+ return {
39
+ ...whisper.statusOf(),
40
+ ready: false,
41
+ error: cause instanceof Error ? cause.message : String(cause),
42
+ }
43
+ }
44
+ },
20
45
  })
46
+
47
+ return () => whisper.stop()
21
48
  },
22
49
  })
50
+
package/src/rpc.ts CHANGED
@@ -40,6 +40,51 @@ export const VoiceRpc = {
40
40
  },
41
41
  output: { type: "string" },
42
42
  },
43
+ whisperTranscribe: {
44
+ input: {
45
+ type: "object",
46
+ properties: {
47
+ audio: { type: "string" },
48
+ mime: { type: "string" },
49
+ language: { type: "string" },
50
+ python: { type: "string" },
51
+ model: { type: "string" },
52
+ },
53
+ required: ["audio", "mime", "language", "python", "model"],
54
+ additionalProperties: false,
55
+ },
56
+ output: {
57
+ type: "object",
58
+ properties: {
59
+ text: { type: "string" },
60
+ language: { type: "string" },
61
+ },
62
+ required: ["text", "language"],
63
+ additionalProperties: false,
64
+ },
65
+ },
66
+ whisperStatus: {
67
+ input: {
68
+ type: "object",
69
+ properties: {
70
+ python: { type: "string" },
71
+ model: { type: "string" },
72
+ },
73
+ required: ["python", "model"],
74
+ additionalProperties: false,
75
+ },
76
+ output: {
77
+ type: "object",
78
+ properties: {
79
+ ready: { type: "boolean" },
80
+ model: { type: "string" },
81
+ device: { type: "string" },
82
+ error: { type: "string" },
83
+ },
84
+ required: ["ready", "model", "device", "error"],
85
+ additionalProperties: false,
86
+ },
87
+ },
43
88
  },
44
89
  events: {},
45
90
  } as const satisfies Rpc.PortableDefinition
@@ -0,0 +1,275 @@
1
+ import { spawn, type ChildProcess } from "node:child_process"
2
+ import { randomUUID } from "node:crypto"
3
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { dirname, join } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const DEFAULT_PYTHON = "~/peopleby/voice_to_text/api_voice/demo/whisperx-env/bin/python"
9
+
10
+ export interface WhisperInput {
11
+ audio: string
12
+ mime: string
13
+ language: string
14
+ python: string
15
+ model: string
16
+ }
17
+
18
+ export interface WhisperResult {
19
+ text: string
20
+ language: string
21
+ }
22
+
23
+ export interface WhisperStatus {
24
+ ready: boolean
25
+ model: string
26
+ device: string
27
+ error: string
28
+ }
29
+
30
+ function workerPath(): string {
31
+ return join(dirname(fileURLToPath(import.meta.url)), "worker.py")
32
+ }
33
+
34
+ /** Converts a Windows path to the WSL mount path, e.g. C:\a\b -> /mnt/c/a/b. */
35
+ function toWslPath(path: string): string {
36
+ if (process.platform !== "win32") return path
37
+
38
+ return path
39
+ .replace(/^([A-Za-z]):[\\/]/, (_, drive: string) => `/mnt/${drive.toLowerCase()}/`)
40
+ .replace(/\\/g, "/")
41
+ }
42
+
43
+ function spawnWorker(python: string, model: string): ChildProcess {
44
+ const env = { ...process.env, WHISPER_MODEL: model, WHISPER_DEVICE: "cuda" }
45
+
46
+ // Inside `bash -lc`, a quoted `~` does not expand. Use $HOME instead.
47
+ const pythonArg = python.startsWith("~/") ? `"$HOME/${python.slice(2)}"` : `"${python}"`
48
+ const command = `${pythonArg} "${toWslPath(workerPath())}"`
49
+
50
+ if (process.platform === "win32") {
51
+ return spawn("wsl.exe", ["-d", "Ubuntu-22.04", "-e", "bash", "-lc", command], {
52
+ env,
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ windowsHide: true,
55
+ })
56
+ }
57
+
58
+ return spawn("bash", ["-lc", command], {
59
+ env,
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ })
62
+ }
63
+
64
+ interface Waiter {
65
+ resolve: (value: WhisperResult) => void
66
+ reject: (error: Error) => void
67
+ timer: NodeJS.Timeout
68
+ }
69
+
70
+ export class WhisperEngine {
71
+ private child: ChildProcess | undefined
72
+ private readyPromise: Promise<void> | undefined
73
+ private readyResolve: (() => void) | undefined
74
+ private readyReject: ((error: Error) => void) | undefined
75
+ private buffer = ""
76
+ private stderr = ""
77
+ private waiters = new Map<string, Waiter>()
78
+ private status: WhisperStatus = {
79
+ ready: false,
80
+ model: "",
81
+ device: "cuda",
82
+ error: "",
83
+ }
84
+ private python = ""
85
+ private model = ""
86
+
87
+ private start(python: string, model: string): Promise<void> {
88
+ this.stop()
89
+ this.python = python || DEFAULT_PYTHON
90
+ this.model = model || "large-v3-turbo"
91
+ this.status = { ready: false, model: this.model, device: "cuda", error: "" }
92
+ this.stderr = ""
93
+
94
+ const child = spawnWorker(this.python, this.model)
95
+ this.child = child
96
+
97
+ this.readyPromise = new Promise<void>((resolve, reject) => {
98
+ this.readyResolve = resolve
99
+ this.readyReject = reject
100
+ })
101
+
102
+ const timer = setTimeout(() => {
103
+ this.status.error = "Whisper took too long to load the model"
104
+ this.readyReject?.(new Error(this.status.error))
105
+ this.stop()
106
+ }, 180000)
107
+
108
+ child.stdout?.setEncoding("utf8")
109
+ child.stdout?.on("data", (chunk: string) => this.onStdout(chunk, timer))
110
+
111
+ child.stderr?.setEncoding("utf8")
112
+ child.stderr?.on("data", (chunk: string) => {
113
+ this.stderr = (this.stderr + chunk).slice(-3000)
114
+ })
115
+
116
+ child.on("error", (error) => {
117
+ clearTimeout(timer)
118
+ this.status.error = error.message
119
+ this.readyReject?.(error)
120
+ this.failWaiters(error)
121
+ })
122
+
123
+ child.on("close", (code) => {
124
+ clearTimeout(timer)
125
+ const detail = this.stderr.trim()
126
+
127
+ if (!this.status.ready) {
128
+ this.status = {
129
+ ready: false,
130
+ model: this.model,
131
+ device: "cuda",
132
+ error: detail || `Whisper worker exited with code ${code ?? "unknown"}`,
133
+ }
134
+ this.readyReject?.(new Error(this.status.error))
135
+ } else {
136
+ this.status = {
137
+ ready: false,
138
+ model: this.model,
139
+ device: "cuda",
140
+ error: detail || `Whisper worker exited with code ${code ?? "unknown"}`,
141
+ }
142
+ }
143
+
144
+ this.failWaiters(new Error(this.status.error || "Whisper worker stopped"))
145
+ this.child = undefined
146
+ this.readyPromise = undefined
147
+ })
148
+
149
+ return this.readyPromise
150
+ }
151
+
152
+ private onStdout(chunk: string, timer: NodeJS.Timeout) {
153
+ this.buffer += chunk
154
+
155
+ let index = this.buffer.indexOf("\n")
156
+ while (index >= 0) {
157
+ const line = this.buffer.slice(0, index).trim()
158
+ this.buffer = this.buffer.slice(index + 1)
159
+
160
+ if (line) {
161
+ try {
162
+ const event = JSON.parse(line) as {
163
+ type?: string
164
+ id?: string
165
+ text?: string
166
+ language?: string
167
+ model?: string
168
+ detail?: string
169
+ }
170
+
171
+ if (event.type === "ready") {
172
+ clearTimeout(timer)
173
+ this.status = {
174
+ ready: true,
175
+ model: event.model ?? this.model,
176
+ device: "cuda",
177
+ error: "",
178
+ }
179
+ this.readyResolve?.()
180
+ } else if (event.type === "result" && event.id) {
181
+ const waiter = this.waiters.get(event.id)
182
+ if (waiter) {
183
+ clearTimeout(waiter.timer)
184
+ this.waiters.delete(event.id)
185
+ waiter.resolve({ text: event.text ?? "", language: event.language ?? "" })
186
+ }
187
+ } else if (event.type === "error") {
188
+ const error = new Error(event.detail || "Whisper failed")
189
+ if (event.id) {
190
+ const waiter = this.waiters.get(event.id)
191
+ if (waiter) {
192
+ clearTimeout(waiter.timer)
193
+ this.waiters.delete(event.id)
194
+ waiter.reject(error)
195
+ }
196
+ } else {
197
+ this.status.error = error.message
198
+ this.readyReject?.(error)
199
+ }
200
+ }
201
+ } catch {
202
+ // Ignore non-JSON output such as warnings.
203
+ }
204
+ }
205
+
206
+ index = this.buffer.indexOf("\n")
207
+ }
208
+ }
209
+
210
+ private failWaiters(error: Error) {
211
+ for (const waiter of this.waiters.values()) {
212
+ clearTimeout(waiter.timer)
213
+ waiter.reject(error)
214
+ }
215
+ this.waiters.clear()
216
+ }
217
+
218
+ async transcribe(input: WhisperInput): Promise<WhisperResult> {
219
+ if (!this.child || !this.readyPromise || !this.status.ready) {
220
+ await this.start(input.python || DEFAULT_PYTHON, input.model || "large-v3-turbo")
221
+ }
222
+ await this.readyPromise
223
+
224
+ const dir = join(tmpdir(), "opencode-voice-dictation")
225
+ mkdirSync(dir, { recursive: true })
226
+ const extension = input.mime.includes("webm") ? "webm" : "wav"
227
+ const file = join(dir, `whisper-${randomUUID()}.${extension}`)
228
+ writeFileSync(file, Buffer.from(input.audio, "base64"))
229
+
230
+ const id = randomUUID()
231
+ const path = toWslPath(file)
232
+
233
+ try {
234
+ return await new Promise<WhisperResult>((resolve, reject) => {
235
+ const timer = setTimeout(() => {
236
+ this.waiters.delete(id)
237
+ reject(new Error("Whisper transcription timed out"))
238
+ }, 120000)
239
+
240
+ this.waiters.set(id, { resolve, reject, timer })
241
+ this.child?.stdin?.write(`${JSON.stringify({ id, path, language: input.language })}\n`)
242
+ })
243
+ } finally {
244
+ try {
245
+ rmSync(file, { force: true })
246
+ } catch {
247
+ // Best effort.
248
+ }
249
+ }
250
+ }
251
+
252
+ async warm(python: string, model: string): Promise<WhisperStatus> {
253
+ if (!this.status.ready) {
254
+ await this.start(python || DEFAULT_PYTHON, model || "large-v3-turbo")
255
+ }
256
+ return this.statusOf()
257
+ }
258
+
259
+ statusOf(): WhisperStatus {
260
+ return { ...this.status }
261
+ }
262
+
263
+ stop() {
264
+ this.failWaiters(new Error("Whisper worker stopped"))
265
+ if (this.child) {
266
+ const child = this.child
267
+ this.child = undefined
268
+ this.readyPromise = undefined
269
+ child.kill()
270
+ if (process.platform === "win32" && child.pid) {
271
+ spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" })
272
+ }
273
+ }
274
+ }
275
+ }
@@ -0,0 +1,53 @@
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ import whisperx
6
+
7
+ MODEL = os.environ.get("WHISPER_MODEL", "large-v3-turbo")
8
+ DEVICE = os.environ.get("WHISPER_DEVICE", "cuda")
9
+
10
+
11
+ def emit(payload):
12
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
13
+ sys.stdout.flush()
14
+
15
+
16
+ try:
17
+ model = whisperx.load_model(
18
+ MODEL,
19
+ DEVICE,
20
+ compute_type="float16",
21
+ vad_options={"vad_onset": 0.4, "vad_offset": 0.3},
22
+ )
23
+ except Exception as exc: # noqa: BLE001
24
+ emit({"type": "error", "code": "MODEL_LOAD", "detail": str(exc)})
25
+ sys.exit(1)
26
+
27
+ emit({"type": "ready", "model": MODEL, "device": DEVICE})
28
+
29
+ for raw in sys.stdin:
30
+ line = raw.strip()
31
+ if not line:
32
+ continue
33
+
34
+ request_id = None
35
+ try:
36
+ request = json.loads(line)
37
+ request_id = request.get("id")
38
+ language = request.get("language") or None
39
+ audio = whisperx.load_audio(request["path"])
40
+ result = model.transcribe(audio, language=language)
41
+ text = " ".join(
42
+ segment.get("text", "").strip() for segment in result.get("segments", [])
43
+ ).strip()
44
+ emit(
45
+ {
46
+ "id": request_id,
47
+ "type": "result",
48
+ "text": text,
49
+ "language": result.get("language", ""),
50
+ }
51
+ )
52
+ except Exception as exc: # noqa: BLE001
53
+ emit({"id": request_id, "type": "error", "detail": str(exc)})