opencode-voice-dictation 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/gui-experimental.cjs +8 -8
- package/docs/whispercpp.md +32 -7
- package/package.json +1 -1
- package/src/gui/definition.ts +22 -2
- package/src/gui/errors.ts +33 -0
- package/src/gui/settings.tsx +101 -5
- package/src/gui/window.tsx +5 -2
- package/src/index.ts +2 -1
- package/src/rpc.ts +17 -1
- package/src/whispercpp/server.ts +189 -3
package/README.md
CHANGED
|
@@ -91,7 +91,13 @@ En Linux el flujo es distinto: la ventana graba con `getUserMedia` +
|
|
|
91
91
|
|
|
92
92
|
**Linux (Ubuntu)**
|
|
93
93
|
|
|
94
|
-
- OpenCode Desktop
|
|
94
|
+
- OpenCode Desktop.
|
|
95
|
+
- **ffmpeg obligatorio**: convierte la grabación WebM a WAV. El plugin lo
|
|
96
|
+
detecta y ofrece **Instalar ffmpeg** en Ajustes; si no puede, muestra el
|
|
97
|
+
comando. Instalación manual:
|
|
98
|
+
```bash
|
|
99
|
+
sudo apt install -y ffmpeg
|
|
100
|
+
```
|
|
95
101
|
- `glibc` compatible con el binario oficial de `whisper.cpp` (Ubuntu 22.04+).
|
|
96
102
|
- Micrófono accesible por la app. El primer dictado descarga el binario
|
|
97
103
|
(~10 MB) y el modelo (`base-q5_1`, 57 MB) desde Ajustes.
|
|
@@ -205,6 +211,7 @@ los propios ajustes. Los motores en la nube del roadmap solo pedirán una API ke
|
|
|
205
211
|
| Comprobar WhisperX | — | Arranca el worker y valida GPU/modelo. |
|
|
206
212
|
| Modelo (whisper.cpp) | `base-q5_1` | Solo Linux. `base-q5_1` (57 MB) o `small-q5_1` (180 MB, más preciso). |
|
|
207
213
|
| Idioma (whisper.cpp) | Automático | Solo Linux. Autodetección o `es`, `en`, `pt`. |
|
|
214
|
+
| ffmpeg | — | Solo Linux. Indica si está instalado y ofrece **Instalar ffmpeg** con el diálogo de contraseña del sistema. |
|
|
208
215
|
| Descargar motor | — | Solo Linux. Baja el binario oficial y el modelo a `~/.local/share/opencode-voice-dictation`. |
|
|
209
216
|
| Comprobar whisper.cpp | — | Solo Linux. Indica si el servidor y el modelo están instalados. |
|
|
210
217
|
|
|
@@ -255,7 +262,9 @@ bloqueas el micrófono, verás el aviso «No hay ningún micrófono disponible»
|
|
|
255
262
|
|
|
256
263
|
En **Linux**, la app abre el micrófono con `getUserMedia`; el navegador/Electron
|
|
257
264
|
puede pedir permiso la primera vez. El servidor solo ejecuta `ffmpeg` y el
|
|
258
|
-
binario descargado.
|
|
265
|
+
binario descargado. Si falta `ffmpeg` y pulsas **Instalar ffmpeg**, se ejecuta
|
|
266
|
+
`pkexec`/`apt-get` con el diálogo de contraseña del sistema; el plugin nunca
|
|
267
|
+
guarda esa contraseña.
|
|
259
268
|
|
|
260
269
|
## Solución de problemas
|
|
261
270
|
|
|
@@ -275,6 +284,7 @@ binario descargado.
|
|
|
275
284
|
| «Abre una conversación para leer su último mensaje» | En Home o en un chat nuevo sin mensajes no hay respuesta que leer. |
|
|
276
285
|
| WhisperX se queda en «Transcribiendo…» | El worker no arrancó. Mira el toast: ruta del Python, CUDA o token de Hugging Face. Guía: <https://github.com/evious/opencode-voice-dictation/blob/main/docs/whisperx.md>. |
|
|
277
286
|
| whisper.cpp dice «not installed» | Entra en Ajustes → Dictado por voz → **Descargar motor**. Requiere internet la primera vez. |
|
|
287
|
+
| Falta ffmpeg o falla la conversión del audio | Ajustes → Dictado por voz → whisper.cpp → **Instalar ffmpeg**, o ejecuta `sudo apt install -y ffmpeg`. El motor no puede transcribir sin ffmpeg. |
|
|
278
288
|
| whisper.cpp falla al arrancar | Comprueba que `ffmpeg` está en el `PATH` (`ffmpeg -version`) y que el binario es compatible con tu `glibc` (Ubuntu 22.04+). |
|
|
279
289
|
| whisper.cpp tarda mucho | Usa `base-q5_1` en portátiles modestos; `small-q5_1` necesita más CPU y memoria. La primera transcripción carga el modelo. |
|
|
280
290
|
| El RPC devuelve 400 o no cargan las voces de Edge | El plugin server no cargó: Ajustes → Extensiones → Plugins → **Reload**, o reinicia el servicio. |
|
|
@@ -368,6 +378,7 @@ src/
|
|
|
368
378
|
├── definition.ts os, stores, i18n es/en
|
|
369
379
|
├── contract.ts Ipc: dictado y lectura
|
|
370
380
|
├── window.tsx Píldoras, comandos e inserción de texto
|
|
381
|
+
├── errors.ts Extrae el mensaje real de los errores RPC
|
|
371
382
|
├── recorder.ts getUserMedia + MediaRecorder para WhisperX/whisper.cpp
|
|
372
383
|
├── main.ts Dictado Win+H / System.Speech y voz SAPI
|
|
373
384
|
├── speak.ts Limpieza de Markdown, troceado, Edge y speechSynthesis
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const E=require("@opencode/plugin/gui"),p=require("effect"),ee=require("node:child_process"),re=require("node:crypto"),M=require("node:fs"),ne=require("node:os"),G=require("node:path"),a=require("solid-js/web"),S=require("solid-js"),ke=E.Plugin.define({os:["windows","linux"],stores:{prefs:E.Store.global(p.Schema.Struct({locale:p.Schema.String,minConfidence:p.Schema.Number,trailingSpace:p.Schema.Boolean,autoFocus:p.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:E.Store.global(p.Schema.Struct({voiceURI:p.Schema.String,rate:p.Schema.Number,skipCode:p.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:E.Store.global(p.Schema.Struct({mode:p.Schema.String}),{mode:"windows"}),tts:E.Store.global(p.Schema.Struct({engine:p.Schema.String,voice:p.Schema.String}),{engine:"edge",voice:"es-ES-XimenaNeural"}),whisper:E.Store.global(p.Schema.Struct({python:p.Schema.String,model:p.Schema.String,language:p.Schema.String}),{python:"~/peopleby/voice_to_text/api_voice/demo/whisperx-env/bin/python",model:"large-v3-turbo",language:""}),whispercpp:E.Store.global(p.Schema.Struct({model:p.Schema.String,language:p.Schema.String}),{model:"base-q5_1",language:"auto"})},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 on Windows and Linux. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows offers Win+H and System.Speech; Linux uses whisper.cpp locally.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","engine.whisper":"WhisperX local (WSL)","engine.whispercpp":"whisper.cpp (local, native)","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.","pill.whispercpp":"Record and transcribe with whisper.cpp","error.whispercpp":"whisper.cpp could not transcribe the audio.","whispercpp.section":"whisper.cpp (local, native)","whispercpp.description":"Runs on the CPU without WSL or Python. The first time you need to download the server (~10 MB) and the model.","whispercpp.model":"Model","whispercpp.model.description":"base-q5_1 is the lightest model (57 MB) and works on modest laptops. small-q5_1 is more accurate but uses more CPU and memory.","whispercpp.language":"Language","whispercpp.language.description":"Automatic detects the language of each recording.","whispercpp.language.auto":"Automatic","whispercpp.install":"Download engine","whispercpp.install.description":"Downloads the official whisper.cpp server and the selected model to ~/.local/share/opencode-voice-dictation. Needs internet once.","whispercpp.install.run":"Download","whispercpp.install.busy":"Downloading…","whispercpp.install.ok":"whisper.cpp installed: {model}","whispercpp.install.error":"Could not install whisper.cpp: {error}","whispercpp.test":"Check whisper.cpp","whispercpp.test.description":"Reports whether the server and the selected model are installed.","whispercpp.test.run":"Check","whispercpp.test.ready":"whisper.cpp ready: {model}","whispercpp.test.missing":"whisper.cpp is not installed. Download the engine first.","whispercpp.test.error":"whisper.cpp is not ready: {error}","whispercpp.empty":"whisper.cpp returned no text.","whispercpp.linux":"whisper.cpp is only available on Linux. On Windows use Win+H, System.Speech or WhisperX.","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":"The system 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 en Windows y Linux. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows ofrece Win+H y System.Speech; Linux usa whisper.cpp en local.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","engine.whisper":"WhisperX local (WSL)","engine.whispercpp":"whisper.cpp (local, nativo)","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.","pill.whispercpp":"Grabar y transcribir con whisper.cpp","error.whispercpp":"whisper.cpp no pudo transcribir el audio.","whispercpp.section":"whisper.cpp (local, nativo)","whispercpp.description":"Funciona en CPU sin WSL ni Python. La primera vez hay que descargar el servidor (~10 MB) y el modelo.","whispercpp.model":"Modelo","whispercpp.model.description":"base-q5_1 es el más ligero (57 MB) y va bien en portátiles modestos. small-q5_1 es más preciso pero consume más CPU y memoria.","whispercpp.language":"Idioma","whispercpp.language.description":"Automático detecta el idioma de cada grabación.","whispercpp.language.auto":"Automático","whispercpp.install":"Descargar motor","whispercpp.install.description":"Descarga el servidor oficial de whisper.cpp y el modelo elegido en ~/.local/share/opencode-voice-dictation. Solo necesita internet una vez.","whispercpp.install.run":"Descargar","whispercpp.install.busy":"Descargando…","whispercpp.install.ok":"whisper.cpp instalado: {model}","whispercpp.install.error":"No se pudo instalar whisper.cpp: {error}","whispercpp.test":"Comprobar whisper.cpp","whispercpp.test.description":"Indica si el servidor y el modelo elegido están instalados.","whispercpp.test.run":"Comprobar","whispercpp.test.ready":"whisper.cpp listo: {model}","whispercpp.test.missing":"whisper.cpp no está instalado. Descarga el motor primero.","whispercpp.test.error":"whisper.cpp no está listo: {error}","whispercpp.empty":"whisper.cpp no devolvió texto.","whispercpp.linux":"whisper.cpp solo está disponible en Linux. En Windows usa Win+H, System.Speech o WhisperX.","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":"El sistema 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 del equipo, 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."}}}),oe=E.Ipc.define({id:"voice-dictation.dictation",state:p.Schema.Struct({status:p.Schema.Literals(["idle","starting","listening","error"]),error:p.Schema.String,locale:p.Schema.String}),methods:{start:{input:p.Schema.Struct({locale:p.Schema.String,minConfidence:p.Schema.Number})},stop:{},diagnose:{output:p.Schema.Struct({locales:p.Schema.Array(p.Schema.String),microphone:p.Schema.Boolean,selected:p.Schema.String,error:p.Schema.String})}},events:{final:p.Schema.Struct({text:p.Schema.String,confidence:p.Schema.Number}),error:p.Schema.Struct({code:p.Schema.String,detail:p.Schema.String,available:p.Schema.Array(p.Schema.String)})}}),he=E.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:p.Schema.Struct({text:p.Schema.String,rate:p.Schema.Number,voice:p.Schema.String})},stop:{},voices:{output:p.Schema.Array(p.Schema.String)}},events:{done:p.Schema.Struct({ok:p.Schema.Boolean})}}),se=E.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),pe=String.raw`param(
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const $=require("@opencode/plugin/gui"),p=require("effect"),ie=require("node:child_process"),oe=require("node:crypto"),K=require("node:fs"),ae=require("node:os"),Q=require("node:path"),o=require("solid-js/web"),y=require("solid-js"),$e=$.Plugin.define({os:["windows","linux"],stores:{prefs:$.Store.global(p.Schema.Struct({locale:p.Schema.String,minConfidence:p.Schema.Number,trailingSpace:p.Schema.Boolean,autoFocus:p.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:$.Store.global(p.Schema.Struct({voiceURI:p.Schema.String,rate:p.Schema.Number,skipCode:p.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:$.Store.global(p.Schema.Struct({mode:p.Schema.String}),{mode:"windows"}),tts:$.Store.global(p.Schema.Struct({engine:p.Schema.String,voice:p.Schema.String}),{engine:"edge",voice:"es-ES-XimenaNeural"}),whisper:$.Store.global(p.Schema.Struct({python:p.Schema.String,model:p.Schema.String,language:p.Schema.String}),{python:"~/peopleby/voice_to_text/api_voice/demo/whisperx-env/bin/python",model:"large-v3-turbo",language:""}),whispercpp:$.Store.global(p.Schema.Struct({model:p.Schema.String,language:p.Schema.String}),{model:"base-q5_1",language:"auto"})},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 on Windows and Linux. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows offers Win+H and System.Speech; Linux uses whisper.cpp locally.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","engine.whisper":"WhisperX local (WSL)","engine.whispercpp":"whisper.cpp (local, native)","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.","pill.whispercpp":"Record and transcribe with whisper.cpp","error.whispercpp":"whisper.cpp could not transcribe the audio.","whispercpp.section":"whisper.cpp (local, native)","whispercpp.description":"Runs on the CPU without WSL or Python. The first time you need to download the server (~10 MB) and the model.","whispercpp.model":"Model","whispercpp.model.description":"base-q5_1 is the lightest model (57 MB) and works on modest laptops. small-q5_1 is more accurate but uses more CPU and memory.","whispercpp.language":"Language","whispercpp.language.description":"Automatic detects the language of each recording.","whispercpp.language.auto":"Automatic","whispercpp.install":"Download engine","whispercpp.install.description":"Downloads the official whisper.cpp server and the selected model to ~/.local/share/opencode-voice-dictation. Needs internet once.","whispercpp.install.run":"Download","whispercpp.install.busy":"Downloading…","whispercpp.install.ok":"whisper.cpp installed: {model}","whispercpp.install.error":"Could not install whisper.cpp: {error}","whispercpp.test":"Check whisper.cpp","whispercpp.test.description":"Reports whether the server, the selected model and ffmpeg are ready.","whispercpp.test.run":"Check","whispercpp.test.ready":"whisper.cpp ready: {model}","whispercpp.test.missing":"whisper.cpp is not installed. Download the engine first.","whispercpp.test.error":"whisper.cpp is not ready: {error}","whispercpp.empty":"whisper.cpp returned no text.","whispercpp.ffmpeg":"ffmpeg","whispercpp.ffmpeg.description":"Required to convert the WebM recording to 16 kHz WAV. The plugin can install it with your system password.","whispercpp.ffmpeg.ready":"ffmpeg ready: {version}","whispercpp.ffmpeg.missing":"ffmpeg is not installed. Without it the audio cannot be transcribed.","whispercpp.ffmpeg.install":"Install ffmpeg","whispercpp.ffmpeg.install.busy":"Installing…","whispercpp.ffmpeg.install.ok":"ffmpeg installed.","whispercpp.ffmpeg.install.error":"Could not install ffmpeg: {error}","whispercpp.ffmpeg.install.manual":"Run it manually: {command}","error.ffmpeg":"ffmpeg is required to convert the audio but is not installed. Open Settings → Voice dictation → whisper.cpp and install it, or run: sudo apt install -y ffmpeg","whispercpp.linux":"whisper.cpp is only available on Linux. On Windows use Win+H, System.Speech or WhisperX.","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":"The system 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 en Windows y Linux. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows ofrece Win+H y System.Speech; Linux usa whisper.cpp en local.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","engine.whisper":"WhisperX local (WSL)","engine.whispercpp":"whisper.cpp (local, nativo)","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.","pill.whispercpp":"Grabar y transcribir con whisper.cpp","error.whispercpp":"whisper.cpp no pudo transcribir el audio.","whispercpp.section":"whisper.cpp (local, nativo)","whispercpp.description":"Funciona en CPU sin WSL ni Python. La primera vez hay que descargar el servidor (~10 MB) y el modelo.","whispercpp.model":"Modelo","whispercpp.model.description":"base-q5_1 es el más ligero (57 MB) y va bien en portátiles modestos. small-q5_1 es más preciso pero consume más CPU y memoria.","whispercpp.language":"Idioma","whispercpp.language.description":"Automático detecta el idioma de cada grabación.","whispercpp.language.auto":"Automático","whispercpp.install":"Descargar motor","whispercpp.install.description":"Descarga el servidor oficial de whisper.cpp y el modelo elegido en ~/.local/share/opencode-voice-dictation. Solo necesita internet una vez.","whispercpp.install.run":"Descargar","whispercpp.install.busy":"Descargando…","whispercpp.install.ok":"whisper.cpp instalado: {model}","whispercpp.install.error":"No se pudo instalar whisper.cpp: {error}","whispercpp.test":"Comprobar whisper.cpp","whispercpp.test.description":"Indica si el servidor, el modelo elegido y ffmpeg están listos.","whispercpp.test.run":"Comprobar","whispercpp.test.ready":"whisper.cpp listo: {model}","whispercpp.test.missing":"whisper.cpp no está instalado. Descarga el motor primero.","whispercpp.test.error":"whisper.cpp no está listo: {error}","whispercpp.empty":"whisper.cpp no devolvió texto.","whispercpp.ffmpeg":"ffmpeg","whispercpp.ffmpeg.description":"Necesario para convertir la grabación WebM a WAV de 16 kHz. El plugin puede instalarlo con tu contraseña del sistema.","whispercpp.ffmpeg.ready":"ffmpeg listo: {version}","whispercpp.ffmpeg.missing":"ffmpeg no está instalado. Sin él no se puede transcribir el audio.","whispercpp.ffmpeg.install":"Instalar ffmpeg","whispercpp.ffmpeg.install.busy":"Instalando…","whispercpp.ffmpeg.install.ok":"ffmpeg instalado correctamente.","whispercpp.ffmpeg.install.error":"No se pudo instalar ffmpeg: {error}","whispercpp.ffmpeg.install.manual":"Ejecútalo manualmente: {command}","error.ffmpeg":"Falta ffmpeg, necesario para convertir el audio. Ve a Ajustes → Dictado por voz → whisper.cpp e instálalo, o ejecuta: sudo apt install -y ffmpeg","whispercpp.linux":"whisper.cpp solo está disponible en Linux. En Windows usa Win+H, System.Speech o WhisperX.","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":"El sistema 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 del equipo, 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."}}}),le=$.Ipc.define({id:"voice-dictation.dictation",state:p.Schema.Struct({status:p.Schema.Literals(["idle","starting","listening","error"]),error:p.Schema.String,locale:p.Schema.String}),methods:{start:{input:p.Schema.Struct({locale:p.Schema.String,minConfidence:p.Schema.Number})},stop:{},diagnose:{output:p.Schema.Struct({locales:p.Schema.Array(p.Schema.String),microphone:p.Schema.Boolean,selected:p.Schema.String,error:p.Schema.String})}},events:{final:p.Schema.Struct({text:p.Schema.String,confidence:p.Schema.Number}),error:p.Schema.Struct({code:p.Schema.String,detail:p.Schema.String,available:p.Schema.Array(p.Schema.String)})}}),fe=$.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:p.Schema.Struct({text:p.Schema.String,rate:p.Schema.Number,voice:p.Schema.String})},stop:{},voices:{output:p.Schema.Array(p.Schema.String)}},events:{done:p.Schema.Struct({ok:p.Schema.Boolean})}}),ce=$.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),ue=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
|
-
|
|
109
|
-
`);for(;g>=0;){const h=
|
|
110
|
-
`)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",
|
|
108
|
+
`,Re="powershell.exe";function we(){const t=Q.join(ae.tmpdir(),"opencode-voice-dictation");K.mkdirSync(t,{recursive:!0});const e=oe.createHash("sha1").update(ue).digest("hex").slice(0,12),i=Q.join(t,`dictation-${e}.ps1`);return K.writeFileSync(i,ue,"utf8"),i}function ve(t){return ie.spawn(Re,t,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function ye(t){const e=t.pid;if(e===void 0){t.kill();return}ie.spawn("taskkill",["/PID",String(e),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>t.kill())}function Pe(t,e){const i=ve(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",we(),"-Culture",t.locale,"-MinConfidence",String(t.minConfidence)]);let c=!1,n="",s="";const u=a=>{switch(a.type){case"ready":e.onReady(a.locale??t.locale);break;case"final":a.text&&e.onFinal(a.text,a.confidence??0);break;case"error":e.onError(a.code??"UNKNOWN",a.detail??"",a.available??[]);break}};return i.stdout?.setEncoding("utf8"),i.stdout?.on("data",a=>{s+=a;let g=s.indexOf(`
|
|
109
|
+
`);for(;g>=0;){const h=s.slice(0,g).replace(/\r$/,"").trim();if(s=s.slice(g+1),h)try{u(JSON.parse(h))}catch{}g=s.indexOf(`
|
|
110
|
+
`)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",a=>{n=(n+a).slice(-2e3)}),i.on("error",a=>{c||e.onError("ENGINE_START_FAILED",a.message,[])}),i.on("close",a=>{c||e.onExit(a,n.trim().slice(-500))}),{stop(){c=!0,ye(i)}}}function Ie(){return new Promise((t,e)=>{const i=ve(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",we(),"-Diagnose"]);let c="",n="",s=!1;const u=(g,h)=>{s||(s=!0,clearTimeout(a),g?e(g):t(h))},a=setTimeout(()=>{ye(i),u(new Error("diagnose timed out"))},2e4);i.stdout?.setEncoding("utf8"),i.stdout?.on("data",g=>{c+=g}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",g=>{n+=g}),i.on("error",g=>u(g)),i.on("close",()=>{const g=c.split(/\r?\n/).map(h=>h.trim()).filter(h=>h.startsWith("{")).at(-1);if(!g){u(new Error(n.trim()||"no output from PowerShell"));return}try{const h=JSON.parse(g);u(void 0,{locales:h.locales??[],microphone:h.microphone===!0,selected:h.selected??"",error:h.error??""})}catch(h){u(h instanceof Error?h:new Error(String(h)))}})})}const ge=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
|
-
`,Te="powershell.exe";function
|
|
163
|
+
`,Te="powershell.exe";function Se(){const t=Q.join(ae.tmpdir(),"opencode-voice-dictation");return K.mkdirSync(t,{recursive:!0}),t}function be(){const t=oe.createHash("sha1").update(ge).digest("hex").slice(0,12),e=Q.join(Se(),`speak-${t}.ps1`);return K.writeFileSync(e,ge,"utf8"),e}function Ae(t){const e=Q.join(Se(),`speech-${oe.randomUUID()}.txt`);return K.writeFileSync(e,t,"utf8"),e}function Ce(t){return ie.spawn(Te,t,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function ke(t){const e=t.pid;if(e===void 0){t.kill();return}ie.spawn("taskkill",["/PID",String(e),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>t.kill())}function We(t){return Math.max(-10,Math.min(10,Math.round((t-1)*5)))}function Ne(t,e){const i=Ae(t.text),c=Ce(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",be(),"-TextFile",i,"-Rate",String(We(t.rate)),"-Voice",t.voice]);let n=!1;const s=()=>{try{K.rmSync(i,{force:!0})}catch{}};return c.on("error",()=>{s(),n||e(!1)}),c.on("close",u=>{s(),n||e(u===0)}),{stop(){n=!0,ke(c),s()}}}function Le(){return new Promise(t=>{const e=Ce(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",be(),"-ListVoices"]);let i="",c=!1;const n=u=>{c||(c=!0,clearTimeout(s),t(u))},s=setTimeout(()=>{ke(e),n([])},1e4);e.stdout?.setEncoding("utf8"),e.stdout?.on("data",u=>{i+=u}),e.on("error",()=>n([])),e.on("close",()=>{const u=i.split(/\r?\n/).map(a=>a.trim()).filter(a=>a.length>0);n(u)})})}const he=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
|
-
`,
|
|
191
|
-
`);return e?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
|
|
192
|
-
`)};return e.on("done",()=>{w(!1),y(!1)}),A()?.addEventListener?.("voiceschanged",()=>{H()}),H(),{speaking:h,paused:k,backend:I,engine:n,voice:u,error:_,canPause:m,setConfig:g,voices:b,refreshVoices:H,lastAssistantText:J,speak:j,cancel:s,pause:f,resume:$,togglePause:K,restart:Z,preview:async()=>{const r=await j(t.t("speech.preview.text"));if(r){const o=n()==="edge"?`Edge · ${u()}`:`${I()} · ${i.value.voiceURI||"auto"}`;t.toast.show({variant:"default",title:t.t("read.title"),description:o})}return r}}}const Fe=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function He(t){const e=E.usePlugin(),i=e.ui,d=e.stores.prefs,n=e.stores.speech,l=e.stores.tts,u=e.stores.engine,c=e.stores.whisper,g=e.stores.whispercpp,h=e.main(oe),y=e.main(se),[k,w]=S.createSignal([]),[b,R]=S.createSignal(!1),[_,z]=S.createSignal(!1),[I,D]=S.createSignal(!1),[T,O]=S.createSignal(!1),[B,N]=S.createSignal("");S.onMount(()=>{t.speaker.setConfig({engine:l.value.engine,voice:l.value.voice});const r=()=>{h.diagnose().then(o=>{o.locales.length>0&&w(o.locales)}).catch(()=>{})};e.client.rpc(U).capabilities({}).then(o=>{const v=o;N(v.platform),!v.engines.includes(u.value.mode)&&v.engines.length>0&&m(v.engines[0]??"windows"),v.platform!=="linux"&&r()}).catch(()=>r()),t.speaker.refreshVoices()});const F=()=>B()==="linux"?[{value:"whispercpp",label:e.t("engine.whispercpp")}]:[{value:"windows",label:e.t("engine.windows")},{value:"system",label:e.t("engine.system")},{value:"whisper",label:e.t("engine.whisper")}],P=()=>{const r=k(),o=d.value.locale;return(r.includes(o)?r:[o,...r]).map(C=>({value:C,label:C}))},A=()=>{const r=t.speaker.voices().map(o=>({value:o.id,label:o.label}));return l.value.engine==="edge"?r:[{value:"",label:e.t("speech.voice.auto")},...r]},X=()=>l.value.engine==="edge"?l.value.voice:n.value.voiceURI,H=r=>{if(l.value.engine==="edge"){l.update(o=>{o.voice=r}),t.speaker.setConfig({voice:r});return}s({voiceURI:r})},L=r=>{d.update(o=>{r.locale!==void 0&&(o.locale=r.locale),r.minConfidence!==void 0&&(o.minConfidence=r.minConfidence),r.trailingSpace!==void 0&&(o.trailingSpace=r.trailingSpace),r.autoFocus!==void 0&&(o.autoFocus=r.autoFocus)})},s=r=>{n.update(o=>{r.voiceURI!==void 0&&(o.voiceURI=r.voiceURI),r.rate!==void 0&&(o.rate=r.rate),r.skipCode!==void 0&&(o.skipCode=r.skipCode)})},m=r=>{u.update(o=>{o.mode=r})},f=r=>{c.update(o=>{r.python!==void 0&&(o.python=r.python),r.model!==void 0&&(o.model=r.model),r.language!==void 0&&(o.language=r.language)})},$=r=>{g.update(o=>{r.model!==void 0&&(o.model=r.model),r.language!==void 0&&(o.language=r.language)})},K=async()=>{D(!0);try{const r=await e.client.rpc(U).whisperCppInstall({model:g.value.model});r.ok?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.install.ok",{model:r.model})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:r.error||"?"}),persistent:!0})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:r instanceof Error?r.message:String(r)}),persistent:!0})}finally{D(!1)}},Z=async()=>{O(!0);try{const r=await e.client.rpc(U).whisperCppStatus({model:g.value.model});r.error?e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:r.error}),persistent:!0}):r.installed?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.test.ready",{model:r.model})}):e.toast.show({variant:"default",title:e.t("pill.title"),description:e.t("whispercpp.test.missing")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:r instanceof Error?r.message:String(r)})})}finally{O(!1)}},q=async()=>{z(!0);try{const r=await e.client.rpc(U).whisperStatus({python:c.value.python,model:c.value.model});r.ready?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whisper.test.ready",{model:r.model,device:r.device})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r.error||"?"})})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r instanceof Error?r.message:String(r)})})}finally{z(!1)}},j=r=>{l.update(o=>{o.engine=r}),t.speaker.setConfig({engine:r}),t.speaker.refreshVoices()},J=async()=>{try{await y.toggle(),e.toast.show({variant:"default",title:e.t("settings.winh"),description:e.t("settings.winh.sent")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:r instanceof Error?r.message:e.t("error.winh")})}},ae=async()=>{try{await t.speaker.preview()}catch(r){e.toast.show({variant:"error",title:e.t("read.title"),description:r instanceof Error?r.message:e.t("read.error")})}},le=async()=>{R(!0);try{const r=await h.diagnose();if(r.locales.length===0){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-recognizers")}),persistent:!0});return}if(!r.microphone){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-microphone")}),persistent:!0});return}r.locales.length>0&&!k().length&&w(r.locales),e.toast.show({variant:"success",title:e.t("error.title"),description:e.t("settings.test.success",{locale:r.selected||r.locales[0]||d.value.locale})})}catch(r){e.toast.show({variant:"error",title:e.t("error.title"),description:r instanceof Error?r.message:String(r)})}finally{R(!1)}};return a.createComponent(i.SettingsLayout,{get title(){return e.t("settings.title")},get description(){return e.t("settings.description")},get children(){return[a.createComponent(i.SettingsSection,{get title(){return e.t("settings.section")},get children(){return[a.createComponent(i.SettingsRow,{id:"engine",get title(){return e.t("settings.engine")},get description(){return e.t("settings.engine.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("settings.engine")},get value(){return u.value.mode},get options(){return F()},onChange:r=>m(r)})}}),a.createComponent(S.Show,{get when(){return u.value.mode==="windows"},get children(){return a.createComponent(i.SettingsRow,{id:"winh",get title(){return e.t("settings.winh")},get description(){return e.t("settings.winh.description")},get children(){return a.createComponent(i.Button,{onClick:()=>{J()},get children(){return e.t("settings.winh.run")}})}})}}),a.createComponent(S.Show,{get when(){return u.value.mode==="whisper"},get children(){return[a.createComponent(i.SettingsRow,{id:"whisper-python",get title(){return e.t("whisper.python")},get description(){return e.t("whisper.python.description")},get children(){return a.createComponent(i.TextInput,{get label(){return e.t("whisper.python")},get value(){return c.value.python},onInput:r=>f({python:r})})}}),a.createComponent(i.SettingsRow,{id:"whisper-model",get title(){return e.t("whisper.model")},get description(){return e.t("whisper.model.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("whisper.model")},get value(){return c.value.model},options:[{value:"large-v3-turbo",label:"large-v3-turbo"},{value:"large-v3",label:"large-v3"},{value:"medium",label:"medium"}],onChange:r=>f({model:r})})}}),a.createComponent(i.SettingsRow,{id:"whisper-language",get title(){return e.t("whisper.language")},get description(){return e.t("whisper.language.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("whisper.language")},get value(){return c.value.language},get options(){return[{value:"",label:e.t("whisper.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>f({language:r})})}}),a.createComponent(i.SettingsRow,{id:"whisper-test",get title(){return e.t("whisper.test")},get description(){return e.t("whisper.test.description")},get children(){return a.createComponent(i.Button,{get busy(){return _()},onClick:()=>{q()},get children(){return e.t("whisper.test.run")}})}})]}}),a.createComponent(S.Show,{get when(){return u.value.mode==="whispercpp"},get children(){return[a.createComponent(i.SettingsRow,{id:"whispercpp-model",get title(){return e.t("whispercpp.model")},get description(){return e.t("whispercpp.model.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("whispercpp.model")},get value(){return g.value.model},options:[{value:"base-q5_1",label:"base-q5_1 · 57 MB"},{value:"small-q5_1",label:"small-q5_1 · 180 MB"}],onChange:r=>$({model:r})})}}),a.createComponent(i.SettingsRow,{id:"whispercpp-language",get title(){return e.t("whispercpp.language")},get description(){return e.t("whispercpp.language.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("whispercpp.language")},get value(){return g.value.language},get options(){return[{value:"auto",label:e.t("whispercpp.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>$({language:r})})}}),a.createComponent(i.SettingsRow,{id:"whispercpp-install",get title(){return e.t("whispercpp.install")},get description(){return e.t("whispercpp.install.description")},get children(){return a.createComponent(i.Button,{get busy(){return I()},onClick:()=>{K()},get children(){return e.t("whispercpp.install.run")}})}}),a.createComponent(i.SettingsRow,{id:"whispercpp-test",get title(){return e.t("whispercpp.test")},get description(){return e.t("whispercpp.test.description")},get children(){return a.createComponent(i.Button,{get busy(){return T()},onClick:()=>{Z()},get children(){return e.t("whispercpp.test.run")}})}})]}}),a.createComponent(S.Show,{get when(){return u.value.mode==="system"},get children(){return[a.createComponent(i.SettingsRow,{id:"language",get title(){return e.t("settings.language")},get description(){return e.t("settings.language.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("settings.language")},get value(){return d.value.locale},get options(){return a.memo(()=>P().length>0)()?P():Fe.map(r=>({value:r,label:r}))},onChange:r=>L({locale:r})})}}),a.createComponent(i.SettingsRow,{id:"confidence",get title(){return e.t("settings.confidence")},get description(){return e.t("settings.confidence.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("settings.confidence")},get value(){return String(d.value.minConfidence)},get options(){return[{value:"0.2",label:e.t("confidence.low")},{value:"0.4",label:e.t("confidence.normal")},{value:"0.6",label:e.t("confidence.high")}]},onChange:r=>L({minConfidence:Number(r)||.4})})}}),a.createComponent(i.SettingsRow,{id:"space",get title(){return e.t("settings.space")},get description(){return e.t("settings.space.description")},get children(){return a.createComponent(i.Switch,{get label(){return e.t("settings.space")},hideLabel:!0,get checked(){return d.value.trailingSpace},onChange:r=>L({trailingSpace:r})})}}),a.createComponent(i.SettingsRow,{id:"focus",get title(){return e.t("settings.focus")},get description(){return e.t("settings.focus.description")},get children(){return a.createComponent(i.Switch,{get label(){return e.t("settings.focus")},hideLabel:!0,get checked(){return d.value.autoFocus},onChange:r=>L({autoFocus:r})})}})]}})]}}),a.createComponent(i.SettingsSection,{get title(){return e.t("speech.section")},get children(){return[a.createComponent(i.SettingsRow,{id:"tts-engine",get title(){return e.t("speech.engine")},get description(){return e.t("speech.engine.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("speech.engine")},get value(){return l.value.engine},get options(){return[{value:"edge",label:e.t("speech.engine.edge")},{value:"system",label:e.t("speech.engine.system")}]},onChange:r=>j(r)})}}),a.createComponent(i.SettingsRow,{id:"voice",get title(){return e.t("speech.voice")},get description(){return e.t("speech.voice.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("speech.voice")},get value(){return X()},get options(){return A()},onChange:r=>H(r)})}}),a.createComponent(S.Show,{get when(){return t.speaker.error()},get children(){return a.createComponent(i.SettingsRow,{id:"tts-error",get title(){return e.t("speech.edge.error")},get description(){return t.speaker.error()}})}}),a.createComponent(i.SettingsRow,{id:"rate",get title(){return e.t("speech.rate")},get description(){return e.t("speech.rate.description")},get children(){return a.createComponent(i.Select,{get label(){return e.t("speech.rate")},get value(){return String(n.value.rate)},get options(){return[{value:"0.8",label:e.t("speech.rate.slow")},{value:"1",label:e.t("speech.rate.normal")},{value:"1.2",label:e.t("speech.rate.fast")},{value:"1.5",label:e.t("speech.rate.faster")}]},onChange:r=>s({rate:Number(r)||1})})}}),a.createComponent(i.SettingsRow,{id:"skip-code",get title(){return e.t("speech.skipCode")},get description(){return e.t("speech.skipCode.description")},get children(){return a.createComponent(i.Switch,{get label(){return e.t("speech.skipCode")},hideLabel:!0,get checked(){return n.value.skipCode},onChange:r=>s({skipCode:r})})}}),a.createComponent(i.SettingsRow,{id:"preview",get title(){return e.t("speech.preview")},get description(){return e.t("speech.preview.description")},get children(){return a.createComponent(i.Button,{onClick:()=>{ae()},get children(){return e.t("read.idle")}})}})]}}),a.createComponent(S.Show,{get when(){return B()!=="linux"},get children(){return a.createComponent(i.SettingsSection,{get title(){return e.t("settings.test.section")},get children(){return a.createComponent(i.SettingsRow,{id:"test",get title(){return e.t("settings.test")},get description(){return e.t("settings.test.description")},get children(){return a.createComponent(i.Button,{get busy(){return b()},onClick:()=>{le()},get children(){return e.t("settings.test.run")}})}})}})}})]}})}async function qe(){const t=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,noiseSuppression:!0}}),e=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm",i=new MediaRecorder(t,{mimeType:e}),d=[];return i.ondataavailable=n=>{n.data.size>0&&d.push(n.data)},i.start(),{stop:()=>new Promise(n=>{i.onstop=()=>{t.getTracks().forEach(l=>l.stop()),n(new Blob(d,{type:e}))},i.stop()})}}async function je(t){const e=new Uint8Array(await t.arrayBuffer());let i="";const d=32768;for(let n=0;n<e.length;n+=d)i+=String.fromCharCode(...e.subarray(n,n+d));return btoa(i)}function Me(t){const e=E.usePlugin();return a.memo(()=>t.actions().map(i=>a.createComponent(e.ui.Button,{get variant(){return i.id==="stop"?"danger":"neutral"},get icon(){return i.icon},onClick:()=>{i.run(),t.close()},get children(){return i.label}})))}const Ge=t=>{const e=t.main(oe),i=t.main(he),d=t.main(se),n=Be(t,i),l=t.stores.prefs,u=t.stores.engine,c=t.stores.whisper,g=t.stores.whispercpp,[h,y]=S.createSignal(!1),[k,w]=S.createSignal("idle");t.client.rpc(U).capabilities({}).then(s=>{const m=s;!m.engines.includes(u.value.mode)&&m.engines.length>0&&u.update(f=>{f.mode=m.engines[0]??"windows"})}).catch(()=>{});const b=()=>u.value.mode==="whisper"||u.value.mode==="whispercpp"?u.value.mode:void 0;let R,_;const z=(s,m,f)=>{switch(s){case"LANGUAGE_NOT_AVAILABLE":return t.t("error.language",{locale:m||l.value.locale,available:f.length>0?f.join(", "):"—"});case"NO_RECOGNIZERS":return t.t("error.none");case"MICROPHONE_UNAVAILABLE":return t.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return t.t("error.system");case"ENGINE_EXIT":return t.t("error.exit");default:return t.t("error.unknown",{code:s})}},I=()=>{h()&&(y(!1),d.toggle().catch(()=>{})),n.cancel(),R=t.sessions.current()?.key,e.start({locale:l.value.locale,minConfidence:l.value.minConfidence}).catch(s=>{t.toast.show({variant:"error",title:t.t("error.title"),description:s instanceof Error?s.message:String(s)})})},D=()=>{e.stop().catch(()=>{})},T=()=>{n.cancel(),t.screen.current()?.composer.focus(),d.toggle().then(()=>y(s=>!s)).catch(s=>{y(!1),t.toast.show({variant:"error",title:t.t("pill.title"),description:s instanceof Error?`${t.t("error.winh")} ${s.message}`:t.t("error.winh")})})},O=()=>t.t(b()==="whispercpp"?"error.whispercpp":"error.whisper"),B=async()=>{if(k()==="idle"){n.cancel();try{_=await qe(),w("recording")}catch(s){t.toast.show({variant:"error",title:t.t("pill.title"),description:s instanceof Error?s.message:O()})}}},N=()=>{const s=_;_=void 0,s&&s.stop().catch(()=>{}),w("idle")},F=async()=>{const s=_;if(_=void 0,!s){w("idle");return}w("transcribing");const m=b();try{const f=await s.stop(),$=await je(f),Z=(m==="whispercpp"?await t.client.rpc(U).whisperCppTranscribe({audio:$,mime:f.type,language:g.value.language,model:g.value.model}):await t.client.rpc(U).whisperTranscribe({audio:$,mime:f.type,language:c.value.language,python:c.value.python,model:c.value.model})).text.trim();if(!Z){t.toast.show({variant:"default",title:t.t("pill.title"),description:t.t(m==="whispercpp"?"whispercpp.empty":"whisper.empty")});return}const q=t.screen.current()?.composer;if(q){const j=q.text(),J=j.length>0&&!/\s$/.test(j)?" ":"";q.insert(J+Z),q.focus()}}catch(f){t.toast.show({variant:"error",title:t.t("pill.title"),description:f instanceof Error?f.message:O()})}finally{w("idle")}},P=()=>{if(b()){k()==="recording"?F():k()==="idle"&&B();return}if(u.value.mode==="windows"){T();return}const s=e.state()?.status??"idle";s==="starting"||s==="listening"?D():I()},A=()=>{if(k()==="recording"&&N(),n.speaking()){n.cancel();return}if(!t.sessions.current()){t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("read.noSession")});return}const s=e.state()?.status??"idle";(s==="starting"||s==="listening")&&e.stop().catch(()=>{}),(async()=>{try{const m=await n.lastAssistantText();if(!m.trim()){t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")});return}await n.speak(m)||t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")})}catch(m){t.toast.show({variant:"error",title:t.t("read.title"),description:m instanceof Error?m.message:t.t("read.error")})}})()},X=()=>{if(!n.speaking())return[{id:"start",label:t.t("read.idle"),icon:"outline-audio",run:A}];const s=n.paused(),m=[{id:"restart",label:t.t("read.restart"),icon:"outline-reset",run:()=>{n.restart()}}];return n.canPause()&&m.push({id:"pause",label:s?t.t("read.resume"):t.t("read.pause"),icon:"outline-audio",run:()=>n.togglePause()}),m.push({id:"stop",label:t.t("read.stop"),icon:"xmark-small",run:()=>n.cancel()}),m};e.on("final",s=>{const m=t.sessions.current()?.key;if(R&&m&&m!==R){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("toast.discarded")});return}const f=t.screen.current()?.composer;if(!f)return;const $=f.text(),K=l.value.trailingSpace&&$.length>0&&!/\s$/.test($)?" ":"";f.insert(K+s.text),l.value.autoFocus&&f.focus()}),e.on("error",s=>{t.toast.show({variant:"error",title:t.t("error.title"),description:z(s.code,s.detail,s.available),persistent:s.code==="LANGUAGE_NOT_AVAILABLE"||s.code==="NO_RECOGNIZERS",duration:8e3})});const H=()=>{if(t.app.platform!=="desktop")return;if(u.value.mode==="windows")return h()?{label:t.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:P}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.winH"),run:P};if(u.value.mode==="whisper"||u.value.mode==="whispercpp"){const m=t.t(u.value.mode==="whispercpp"?"pill.whispercpp":"pill.whisper");return k()==="recording"?{label:t.t("pill.recording"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:P}:k()==="transcribing"?{label:t.t("pill.transcribing"),icon:"outline-audio",title:m}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:m,run:P}}const s=e.state()?.status??"idle";return s==="starting"?{label:t.t("pill.starting"),icon:"outline-wave-audio",title:t.t("pill.title"),run:P}:s==="listening"?{label:t.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:P}:s==="error"?{label:t.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:t.t("pill.title"),run:P}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.title"),run:P}},L=()=>{if(t.app.platform!=="desktop")return;const s=n.speaking(),m=n.paused(),f=s?X().map($=>({id:$.id,label:$.label,icon:$.icon,tone:$.id==="stop"?"danger":void 0,run:$.run})):[];return{label:s?m?t.t("read.paused"):t.t("read.speaking"):t.t("read.idle"),icon:"outline-audio",tone:s?"success":void 0,title:t.t("read.title"),rows:f.length>0?f:void 0,run:s?void 0:A}};t.add(E.ComposerPill,()=>({id:"dictate",show:()=>H()})),t.add(E.ComposerPill,()=>({id:"read",order:1,show:()=>L(),menu:s=>a.createComponent(Me,{actions:X,get close(){return s.close}})})),t.add(E.Command,()=>({id:"toggle",title:t.t("command.toggle"),description:t.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:t.app.platform==="desktop",run:P})),t.add(E.Command,()=>({id:"configure",title:t.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),enabled:t.app.platform==="desktop",run:()=>t.layout.settings(t.id)})),t.add(E.Command,()=>({id:"read",title:t.t("command.read"),description:t.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:t.app.platform==="desktop",run:A})),t.add(E.SettingsPage,{id:t.id,icon:"outline-audio",get title(){return t.t("settings.title")},get entries(){return[{id:"engine",title:t.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:t.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:t.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:t.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:t.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:t.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"tts-engine",title:t.t("speech.engine"),keywords:"voice engine motor edge neural sistema"},{id:"whisper-python",title:t.t("whisper.python"),keywords:"whisper python venv entorno"},{id:"whisper-model",title:t.t("whisper.model"),keywords:"whisper model modelo"},{id:"whisper-language",title:t.t("whisper.language"),keywords:"whisper language idioma"},{id:"whisper-test",title:t.t("whisper.test"),keywords:"whisper check comprobar"},{id:"whispercpp-model",title:t.t("whispercpp.model"),keywords:"whisper.cpp model modelo"},{id:"whispercpp-language",title:t.t("whispercpp.language"),keywords:"whisper.cpp language idioma"},{id:"whispercpp-install",title:t.t("whispercpp.install"),keywords:"whisper.cpp download descargar instalar"},{id:"whispercpp-test",title:t.t("whispercpp.test"),keywords:"whisper.cpp check comprobar"},{id:"rate",title:t.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:t.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:t.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>a.createComponent(He,{speaker:n})})},Xe=E.Plugin.define({...ke,setup:Ge,main:De});exports.default=Xe;
|
|
190
|
+
`,_e="powershell.exe";function Ve(){const t=Q.join(ae.tmpdir(),"opencode-voice-dictation");K.mkdirSync(t,{recursive:!0});const e=oe.createHash("sha1").update(he).digest("hex").slice(0,12),i=Q.join(t,`win-h-${e}.ps1`);return K.writeFileSync(i,he,"utf8"),i}function ze(t){return ie.spawn(_e,t,{windowsHide:!0,stdio:"ignore"})}function De(){return new Promise((t,e)=>{const i=ze(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",Ve()]);i.on("error",c=>e(c)),i.on("close",c=>{c===0?t():e(new Error(`Win+H helper exited with code ${c??"unknown"}`))})})}const Fe=t=>{let e="idle",i="",c="",n,s=!1;const u=()=>{n=void 0,s=!1},a=t.provide(le,{state:()=>({status:e,error:i,locale:c}),start(S,R){n&&(s=!0,n.stop(),u()),e="starting",i="",c=S.locale,a.changed();const v=R.window;try{n=Pe({locale:S.locale,minConfidence:S.minConfidence},{onReady(C){e="listening",c=C,a.changed()},onFinal(C,I){a.emit("final",{text:C,confidence:I},v)},onError(C,I,z){e="error",i=C,a.changed(),a.emit("error",{code:C,detail:I,available:z},v)},onExit(C,I){if(s){u(),e="idle",i="",c="",a.changed();return}u(),e!=="error"&&(e="error",i=C===null?"ENGINE_EXIT":`ENGINE_EXIT_${C}`,a.changed(),a.emit("error",{code:"ENGINE_EXIT",detail:I||(C===null?"":String(C)),available:[]},v))}})}catch(C){throw e="error",i="ENGINE_START_FAILED",a.changed(),C}},stop(){n&&(s=!0,n.stop(),u()),e="idle",i="",c="",a.changed()},diagnose(){return Ie()}});let g;const h=t.provide(fe,{speak(S){g?.stop(),g=Ne(S,R=>{g=void 0,h.emit("done",{ok:R})})},stop(){g&&(g.stop(),g=void 0)},voices(){return Le()}});t.provide(ce,{toggle(){return De()}}),t.onCleanup(()=>{n&&(s=!0,n.stop(),u()),g?.stop(),g=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}},capabilities:{input:{type:"object",additionalProperties:!1},output:{type:"object",properties:{platform:{type:"string"},engines:{type:"array",items:{type:"string"}}},required:["platform","engines"],additionalProperties:!1}},whisperCppInstall:{input:{type:"object",properties:{model:{type:"string"}},required:["model"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},model:{type:"string"},error:{type:"string"}},required:["ok","model","error"],additionalProperties:!1}},whisperCppStatus:{input:{type:"object",properties:{model:{type:"string"}},required:["model"],additionalProperties:!1},output:{type:"object",properties:{installed:{type:"boolean"},ready:{type:"boolean"},model:{type:"string"},error:{type:"string"},ffmpeg:{type:"boolean"},ffmpegVersion:{type:"string"},ffmpegPath:{type:"string"}},required:["installed","ready","model","error","ffmpeg","ffmpegVersion","ffmpegPath"],additionalProperties:!1}},whisperCppInstallFfmpeg:{input:{type:"object",additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},error:{type:"string"},command:{type:"string"}},required:["ok","error","command"],additionalProperties:!1}},whisperCppTranscribe:{input:{type:"object",properties:{audio:{type:"string"},mime:{type:"string"},language:{type:"string"},model:{type:"string"}},required:["audio","mime","language","model"],additionalProperties:!1},output:{type:"object",properties:{text:{type:"string"},language:{type:"string"}},required:["text","language"],additionalProperties:!1}}},events:{}},Oe=250;function je(t,e){let i=t.replace(/\r\n/g,`
|
|
191
|
+
`);return e?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 me(t,e=Oe){const i=t.trim();if(!i)return[];const c=i.split(new RegExp("(?<=[.!?…])\\s+")),n=[];let s="";const u=a=>{let g="";for(const h of a.split(/\s+/))h&&(g&&g.length+1+h.length>e?(n.push(g),g=h):g=g?`${g} ${h}`:h);return g};for(const a of c){if(a.length>e){s&&(n.push(s),s=""),s=u(a);continue}s&&s.length+1+a.length>e?(n.push(s),s=a):s=s?`${s} ${a}`:a}return s&&n.push(s),n}function Be(t,e){const i=t.stores.speech,c=t.stores.tts.value,[n,s]=y.createSignal(c.engine==="edge"?"edge":"system"),[u,a]=y.createSignal(c.voice),g=w=>{(w.engine==="edge"||w.engine==="system")&&s(w.engine),w.voice!==void 0&&a(w.voice)},[h,S]=y.createSignal(!1),[R,v]=y.createSignal(!1),[C,I]=y.createSignal([]),[z,B]=y.createSignal(""),[N,U]=y.createSignal("none");let A=0,q=[],Z="",L,F;const T=new Map,W=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,x=()=>{const w=W()?.getVoices()??[];if(w.length===0)return;const f=i.value.voiceURI;if(f){const P=w.find(X=>X.voiceURI===f);if(P)return P}const k=t.locale.locale().split("-")[0]?.toLowerCase()??"es";return w.find(P=>P.lang.toLowerCase().startsWith(k))??w[0]},_=async()=>{if(n()==="edge"){try{const f=await t.client.rpc(j).edgeVoices({});f.length>0?(I(f.map(k=>({id:k.id,label:k.label}))),B("")):B(t.t("speech.edge.error"))}catch(f){B(f instanceof Error?f.message:String(f))}U("edge");return}B("");const w=W()?.getVoices()??[];if(w.length>0){I(w.map(f=>({id:f.voiceURI,label:`${f.name} — ${f.lang}`}))),U("web");return}try{const f=await e.voices();if(f.length>0){I(f.map(k=>({id:k,label:k}))),U("sapi");return}}catch{}U("none")},H=()=>{L&&(L.onended=null,L.onerror=null,L.pause(),L=void 0),F&&(URL.revokeObjectURL(F),F=void 0)},l=()=>{A++,q=[],W()?.cancel(),H(),v(!1),S(!1),N()==="sapi"&&e.stop().catch(()=>{})},m=()=>N()==="web"||N()==="edge",b=()=>{if(!(!h()||R()||!m())){if(N()==="edge"){L?.pause(),v(!0);return}W()?.pause(),v(!0)}},E=()=>{if(!(!h()||!R()||!m())){if(N()==="edge"){L?.play().catch(()=>{}),v(!1);return}W()?.resume(),v(!1)}},ee=()=>{R()?E():b()},M=()=>Z?G(Z):Promise.resolve(!1),O=w=>{const f=u()||"es-ES-XimenaNeural",k=i.value.rate,P=me(w,4e3),X=`${f}|${k}|${w}`,Y=T.get(X)??[];T.clear(),T.set(X,Y);const V=A;S(!0),v(!1);const r=d=>{if(V===A){if(d>=P.length){v(!1),S(!1);return}(async()=>{let D=Y[d];try{D||(D=await t.client.rpc(j).edgeSpeak({text:P[d]??"",voice:f,rate:k}),Y[d]=D)}catch{V===A&&(v(!1),S(!1),t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("error.edge")}));return}if(V!==A)return;const re=Uint8Array.from(atob(D),Ee=>Ee.charCodeAt(0)),de=URL.createObjectURL(new Blob([re],{type:"audio/mpeg"}));H(),F=de;const ne=new Audio(de);L=ne,ne.onended=()=>{V===A&&(H(),r(d+1))},ne.onerror=()=>{V===A&&(H(),v(!1),S(!1))},await ne.play().catch(()=>{V===A&&(H(),v(!1),S(!1),t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("error.edge")}))})})()}};return r(0),!0},G=async w=>{Z=w;const f=je(w,i.value.skipCode);if(!f)return!1;if(l(),await _(),n()==="edge")return O(f);if(N()==="web"){const k=W();if(!k)return!1;const P=x(),X=i.value.rate,Y=A;q=me(f),S(!0),v(!1);const V=()=>{if(Y!==A)return;const r=q.shift();if(!r){v(!1),S(!1);return}const d=new SpeechSynthesisUtterance(r);P&&(d.voice=P),P?.lang&&(d.lang=P.lang),d.rate=X,d.onend=V,d.onerror=V,k.speak(d)};return V(),!0}if(N()==="sapi"){S(!0),v(!1);try{await e.speak({text:f,rate:i.value.rate,voice:i.value.voiceURI})}catch(k){throw S(!1),k}return!0}throw S(!1),new Error(t.t("read.noVoices"))},J=async()=>{const w=t.sessions.current();if(!w)return"";const k=(await w.server.client.message.list({sessionID:w.id,type:"assistant",order:"desc",limit:1})).data[0];return!k||k.type!=="assistant"?"":k.content.filter(P=>P.type==="text").map(P=>P.text).join(`
|
|
192
|
+
`)};return e.on("done",()=>{v(!1),S(!1)}),W()?.addEventListener?.("voiceschanged",()=>{_()}),_(),{speaking:h,paused:R,backend:N,engine:n,voice:u,error:z,canPause:m,setConfig:g,voices:C,refreshVoices:_,lastAssistantText:J,speak:G,cancel:l,pause:b,resume:E,togglePause:ee,restart:M,preview:async()=>{const w=await G(t.t("speech.preview.text"));if(w){const f=n()==="edge"?`Edge · ${u()}`:`${N()} · ${i.value.voiceURI||"auto"}`;t.toast.show({variant:"default",title:t.t("read.title"),description:f})}return w}}}function te(t,e){if(t instanceof Error&&t.message)return t.message;if(typeof t=="string"&&t)return t;if(t&&typeof t=="object"){const i=t;for(const n of["message","error","detail","defect"]){const s=i[n];if(typeof s=="string"&&s)return s}const c=i.data;if(c&&typeof c=="object"){const n=c.message;if(typeof n=="string"&&n)return n}try{const n=JSON.stringify(t);if(n&&n!=="{}")return n}catch{return e}}return e}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 qe(t){const e=$.usePlugin(),i=e.ui,c=e.stores.prefs,n=e.stores.speech,s=e.stores.tts,u=e.stores.engine,a=e.stores.whisper,g=e.stores.whispercpp,h=e.main(le),S=e.main(ce),[R,v]=y.createSignal([]),[C,I]=y.createSignal(!1),[z,B]=y.createSignal(!1),[N,U]=y.createSignal(!1),[A,q]=y.createSignal(!1),[Z,L]=y.createSignal(!1),[F,T]=y.createSignal(void 0),[W,x]=y.createSignal(""),_=async()=>{const r=await e.client.rpc(j).whisperCppStatus({model:g.value.model});return T({available:r.ffmpeg,version:r.ffmpegVersion}),r},H=()=>{const r=F();return r?r.available?e.t("whispercpp.ffmpeg.ready",{version:r.version}):e.t("whispercpp.ffmpeg.missing"):e.t("whispercpp.ffmpeg.description")};y.onMount(()=>{t.speaker.setConfig({engine:s.value.engine,voice:s.value.voice});const r=()=>{h.diagnose().then(d=>{d.locales.length>0&&v(d.locales)}).catch(()=>{})};e.client.rpc(j).capabilities({}).then(d=>{const D=d;x(D.platform),!D.engines.includes(u.value.mode)&&D.engines.length>0&&G(D.engines[0]??"windows"),D.platform!=="linux"?r():_().catch(()=>{})}).catch(()=>r()),t.speaker.refreshVoices()});const l=()=>W()==="linux"?[{value:"whispercpp",label:e.t("engine.whispercpp")}]:[{value:"windows",label:e.t("engine.windows")},{value:"system",label:e.t("engine.system")},{value:"whisper",label:e.t("engine.whisper")}],m=()=>{const r=R(),d=c.value.locale;return(r.includes(d)?r:[d,...r]).map(re=>({value:re,label:re}))},b=()=>{const r=t.speaker.voices().map(d=>({value:d.id,label:d.label}));return s.value.engine==="edge"?r:[{value:"",label:e.t("speech.voice.auto")},...r]},E=()=>s.value.engine==="edge"?s.value.voice:n.value.voiceURI,ee=r=>{if(s.value.engine==="edge"){s.update(d=>{d.voice=r}),t.speaker.setConfig({voice:r});return}O({voiceURI:r})},M=r=>{c.update(d=>{r.locale!==void 0&&(d.locale=r.locale),r.minConfidence!==void 0&&(d.minConfidence=r.minConfidence),r.trailingSpace!==void 0&&(d.trailingSpace=r.trailingSpace),r.autoFocus!==void 0&&(d.autoFocus=r.autoFocus)})},O=r=>{n.update(d=>{r.voiceURI!==void 0&&(d.voiceURI=r.voiceURI),r.rate!==void 0&&(d.rate=r.rate),r.skipCode!==void 0&&(d.skipCode=r.skipCode)})},G=r=>{u.update(d=>{d.mode=r})},J=r=>{a.update(d=>{r.python!==void 0&&(d.python=r.python),r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},se=r=>{g.update(d=>{r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},pe=async()=>{U(!0);try{const r=await e.client.rpc(j).whisperCppInstall({model:g.value.model});r.ok?(e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.install.ok",{model:r.model})}),await _().catch(()=>{})):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:r.error||"?"}),persistent:!0})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:te(r,"?")}),persistent:!0})}finally{U(!1)}},w=async()=>{q(!0);try{const r=await _();r.error?e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:r.error}),persistent:!0}):r.installed?r.ffmpeg?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.test.ready",{model:r.model})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.missing"),persistent:!0}):e.toast.show({variant:"default",title:e.t("pill.title"),description:e.t("whispercpp.test.missing")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:te(r,"?")})})}finally{q(!1)}},f=async()=>{L(!0);try{const r=await e.client.rpc(j).whisperCppInstallFfmpeg({});if(r.ok)e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.ok")}),await _();else{const d=r.command?` ${e.t("whispercpp.ffmpeg.install.manual",{command:r.command})}`:"";e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:r.error||"?"})+d,persistent:!0})}}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:te(r,"?")}),persistent:!0})}finally{L(!1)}},k=async()=>{B(!0);try{const r=await e.client.rpc(j).whisperStatus({python:a.value.python,model:a.value.model});r.ready?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whisper.test.ready",{model:r.model,device:r.device})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r.error||"?"})})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r instanceof Error?r.message:String(r)})})}finally{B(!1)}},P=r=>{s.update(d=>{d.engine=r}),t.speaker.setConfig({engine:r}),t.speaker.refreshVoices()},X=async()=>{try{await S.toggle(),e.toast.show({variant:"default",title:e.t("settings.winh"),description:e.t("settings.winh.sent")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:r instanceof Error?r.message:e.t("error.winh")})}},Y=async()=>{try{await t.speaker.preview()}catch(r){e.toast.show({variant:"error",title:e.t("read.title"),description:r instanceof Error?r.message:e.t("read.error")})}},V=async()=>{I(!0);try{const r=await h.diagnose();if(r.locales.length===0){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-recognizers")}),persistent:!0});return}if(!r.microphone){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-microphone")}),persistent:!0});return}r.locales.length>0&&!R().length&&v(r.locales),e.toast.show({variant:"success",title:e.t("error.title"),description:e.t("settings.test.success",{locale:r.selected||r.locales[0]||c.value.locale})})}catch(r){e.toast.show({variant:"error",title:e.t("error.title"),description:r instanceof Error?r.message:String(r)})}finally{I(!1)}};return o.createComponent(i.SettingsLayout,{get title(){return e.t("settings.title")},get description(){return e.t("settings.description")},get children(){return[o.createComponent(i.SettingsSection,{get title(){return e.t("settings.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"engine",get title(){return e.t("settings.engine")},get description(){return e.t("settings.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.engine")},get value(){return u.value.mode},get options(){return l()},onChange:r=>G(r)})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="windows"},get children(){return o.createComponent(i.SettingsRow,{id:"winh",get title(){return e.t("settings.winh")},get description(){return e.t("settings.winh.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{X()},get children(){return e.t("settings.winh.run")}})}})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whisper"},get children(){return[o.createComponent(i.SettingsRow,{id:"whisper-python",get title(){return e.t("whisper.python")},get description(){return e.t("whisper.python.description")},get children(){return o.createComponent(i.TextInput,{get label(){return e.t("whisper.python")},get value(){return a.value.python},onInput:r=>J({python:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-model",get title(){return e.t("whisper.model")},get description(){return e.t("whisper.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.model")},get value(){return a.value.model},options:[{value:"large-v3-turbo",label:"large-v3-turbo"},{value:"large-v3",label:"large-v3"},{value:"medium",label:"medium"}],onChange:r=>J({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-language",get title(){return e.t("whisper.language")},get description(){return e.t("whisper.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.language")},get value(){return a.value.language},get options(){return[{value:"",label:e.t("whisper.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>J({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-test",get title(){return e.t("whisper.test")},get description(){return e.t("whisper.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return z()},onClick:()=>{k()},get children(){return e.t("whisper.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whispercpp"},get children(){return[o.createComponent(i.SettingsRow,{id:"whispercpp-model",get title(){return e.t("whispercpp.model")},get description(){return e.t("whispercpp.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.model")},get value(){return g.value.model},options:[{value:"base-q5_1",label:"base-q5_1 · 57 MB"},{value:"small-q5_1",label:"small-q5_1 · 180 MB"}],onChange:r=>se({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-language",get title(){return e.t("whispercpp.language")},get description(){return e.t("whispercpp.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.language")},get value(){return g.value.language},get options(){return[{value:"auto",label:e.t("whispercpp.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>se({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-ffmpeg",get title(){return e.t("whispercpp.ffmpeg")},get description(){return H()},get children(){return[o.createComponent(y.Show,{get when(){return F()?.available===!1},get children(){return o.createComponent(i.Button,{get busy(){return Z()},onClick:()=>{f()},get children(){return e.t("whispercpp.ffmpeg.install")}})}}),o.createComponent(y.Show,{get when(){return F()?.available!==!1},get children(){return o.createComponent(i.Button,{onClick:()=>{_().catch(()=>{})},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(i.SettingsRow,{id:"whispercpp-install",get title(){return e.t("whispercpp.install")},get description(){return e.t("whispercpp.install.description")},get children(){return o.createComponent(i.Button,{get busy(){return N()},onClick:()=>{pe()},get children(){return e.t("whispercpp.install.run")}})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-test",get title(){return e.t("whispercpp.test")},get description(){return e.t("whispercpp.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return A()},onClick:()=>{w()},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="system"},get children(){return[o.createComponent(i.SettingsRow,{id:"language",get title(){return e.t("settings.language")},get description(){return e.t("settings.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.language")},get value(){return c.value.locale},get options(){return o.memo(()=>m().length>0)()?m():Ue.map(r=>({value:r,label:r}))},onChange:r=>M({locale:r})})}}),o.createComponent(i.SettingsRow,{id:"confidence",get title(){return e.t("settings.confidence")},get description(){return e.t("settings.confidence.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.confidence")},get value(){return String(c.value.minConfidence)},get options(){return[{value:"0.2",label:e.t("confidence.low")},{value:"0.4",label:e.t("confidence.normal")},{value:"0.6",label:e.t("confidence.high")}]},onChange:r=>M({minConfidence:Number(r)||.4})})}}),o.createComponent(i.SettingsRow,{id:"space",get title(){return e.t("settings.space")},get description(){return e.t("settings.space.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.space")},hideLabel:!0,get checked(){return c.value.trailingSpace},onChange:r=>M({trailingSpace:r})})}}),o.createComponent(i.SettingsRow,{id:"focus",get title(){return e.t("settings.focus")},get description(){return e.t("settings.focus.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.focus")},hideLabel:!0,get checked(){return c.value.autoFocus},onChange:r=>M({autoFocus:r})})}})]}})]}}),o.createComponent(i.SettingsSection,{get title(){return e.t("speech.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"tts-engine",get title(){return e.t("speech.engine")},get description(){return e.t("speech.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.engine")},get value(){return s.value.engine},get options(){return[{value:"edge",label:e.t("speech.engine.edge")},{value:"system",label:e.t("speech.engine.system")}]},onChange:r=>P(r)})}}),o.createComponent(i.SettingsRow,{id:"voice",get title(){return e.t("speech.voice")},get description(){return e.t("speech.voice.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.voice")},get value(){return E()},get options(){return b()},onChange:r=>ee(r)})}}),o.createComponent(y.Show,{get when(){return t.speaker.error()},get children(){return o.createComponent(i.SettingsRow,{id:"tts-error",get title(){return e.t("speech.edge.error")},get description(){return t.speaker.error()}})}}),o.createComponent(i.SettingsRow,{id:"rate",get title(){return e.t("speech.rate")},get description(){return e.t("speech.rate.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.rate")},get value(){return String(n.value.rate)},get options(){return[{value:"0.8",label:e.t("speech.rate.slow")},{value:"1",label:e.t("speech.rate.normal")},{value:"1.2",label:e.t("speech.rate.fast")},{value:"1.5",label:e.t("speech.rate.faster")}]},onChange:r=>O({rate:Number(r)||1})})}}),o.createComponent(i.SettingsRow,{id:"skip-code",get title(){return e.t("speech.skipCode")},get description(){return e.t("speech.skipCode.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("speech.skipCode")},hideLabel:!0,get checked(){return n.value.skipCode},onChange:r=>O({skipCode:r})})}}),o.createComponent(i.SettingsRow,{id:"preview",get title(){return e.t("speech.preview")},get description(){return e.t("speech.preview.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{Y()},get children(){return e.t("read.idle")}})}})]}}),o.createComponent(y.Show,{get when(){return W()!=="linux"},get children(){return o.createComponent(i.SettingsSection,{get title(){return e.t("settings.test.section")},get children(){return o.createComponent(i.SettingsRow,{id:"test",get title(){return e.t("settings.test")},get description(){return e.t("settings.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return C()},onClick:()=>{V()},get children(){return e.t("settings.test.run")}})}})}})}})]}})}async function He(){const t=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,noiseSuppression:!0}}),e=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm",i=new MediaRecorder(t,{mimeType:e}),c=[];return i.ondataavailable=n=>{n.data.size>0&&c.push(n.data)},i.start(),{stop:()=>new Promise(n=>{i.onstop=()=>{t.getTracks().forEach(s=>s.stop()),n(new Blob(c,{type:e}))},i.stop()})}}async function Me(t){const e=new Uint8Array(await t.arrayBuffer());let i="";const c=32768;for(let n=0;n<e.length;n+=c)i+=String.fromCharCode(...e.subarray(n,n+c));return btoa(i)}function Ge(t){const e=$.usePlugin();return o.memo(()=>t.actions().map(i=>o.createComponent(e.ui.Button,{get variant(){return i.id==="stop"?"danger":"neutral"},get icon(){return i.icon},onClick:()=>{i.run(),t.close()},get children(){return i.label}})))}const Xe=t=>{const e=t.main(le),i=t.main(fe),c=t.main(ce),n=Be(t,i),s=t.stores.prefs,u=t.stores.engine,a=t.stores.whisper,g=t.stores.whispercpp,[h,S]=y.createSignal(!1),[R,v]=y.createSignal("idle");t.client.rpc(j).capabilities({}).then(l=>{const m=l;!m.engines.includes(u.value.mode)&&m.engines.length>0&&u.update(b=>{b.mode=m.engines[0]??"windows"})}).catch(()=>{});const C=()=>u.value.mode==="whisper"||u.value.mode==="whispercpp"?u.value.mode:void 0;let I,z;const B=(l,m,b)=>{switch(l){case"LANGUAGE_NOT_AVAILABLE":return t.t("error.language",{locale:m||s.value.locale,available:b.length>0?b.join(", "):"—"});case"NO_RECOGNIZERS":return t.t("error.none");case"MICROPHONE_UNAVAILABLE":return t.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return t.t("error.system");case"ENGINE_EXIT":return t.t("error.exit");default:return t.t("error.unknown",{code:l})}},N=()=>{h()&&(S(!1),c.toggle().catch(()=>{})),n.cancel(),I=t.sessions.current()?.key,e.start({locale:s.value.locale,minConfidence:s.value.minConfidence}).catch(l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:l instanceof Error?l.message:String(l)})})},U=()=>{e.stop().catch(()=>{})},A=()=>{n.cancel(),t.screen.current()?.composer.focus(),c.toggle().then(()=>S(l=>!l)).catch(l=>{S(!1),t.toast.show({variant:"error",title:t.t("pill.title"),description:l instanceof Error?`${t.t("error.winh")} ${l.message}`:t.t("error.winh")})})},q=()=>t.t(C()==="whispercpp"?"error.whispercpp":"error.whisper"),Z=async()=>{if(R()==="idle"){n.cancel();try{z=await He(),v("recording")}catch(l){t.toast.show({variant:"error",title:t.t("pill.title"),description:te(l,q())})}}},L=()=>{const l=z;z=void 0,l&&l.stop().catch(()=>{}),v("idle")},F=async()=>{const l=z;if(z=void 0,!l){v("idle");return}v("transcribing");const m=C();try{const b=await l.stop(),E=await Me(b),M=(m==="whispercpp"?await t.client.rpc(j).whisperCppTranscribe({audio:E,mime:b.type,language:g.value.language,model:g.value.model}):await t.client.rpc(j).whisperTranscribe({audio:E,mime:b.type,language:a.value.language,python:a.value.python,model:a.value.model})).text.trim();if(!M){t.toast.show({variant:"default",title:t.t("pill.title"),description:t.t(m==="whispercpp"?"whispercpp.empty":"whisper.empty")});return}const O=t.screen.current()?.composer;if(O){const G=O.text(),J=G.length>0&&!/\s$/.test(G)?" ":"";O.insert(J+M),O.focus()}}catch(b){const E=te(b,q());t.toast.show({variant:"error",title:t.t("pill.title"),description:E.includes("FFMPEG_MISSING")?t.t("error.ffmpeg"):E})}finally{v("idle")}},T=()=>{if(C()){R()==="recording"?F():R()==="idle"&&Z();return}if(u.value.mode==="windows"){A();return}const l=e.state()?.status??"idle";l==="starting"||l==="listening"?U():N()},W=()=>{if(R()==="recording"&&L(),n.speaking()){n.cancel();return}if(!t.sessions.current()){t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("read.noSession")});return}const l=e.state()?.status??"idle";(l==="starting"||l==="listening")&&e.stop().catch(()=>{}),(async()=>{try{const m=await n.lastAssistantText();if(!m.trim()){t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")});return}await n.speak(m)||t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")})}catch(m){t.toast.show({variant:"error",title:t.t("read.title"),description:m instanceof Error?m.message:t.t("read.error")})}})()},x=()=>{if(!n.speaking())return[{id:"start",label:t.t("read.idle"),icon:"outline-audio",run:W}];const l=n.paused(),m=[{id:"restart",label:t.t("read.restart"),icon:"outline-reset",run:()=>{n.restart()}}];return n.canPause()&&m.push({id:"pause",label:l?t.t("read.resume"):t.t("read.pause"),icon:"outline-audio",run:()=>n.togglePause()}),m.push({id:"stop",label:t.t("read.stop"),icon:"xmark-small",run:()=>n.cancel()}),m};e.on("final",l=>{const m=t.sessions.current()?.key;if(I&&m&&m!==I){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("toast.discarded")});return}const b=t.screen.current()?.composer;if(!b)return;const E=b.text(),ee=s.value.trailingSpace&&E.length>0&&!/\s$/.test(E)?" ":"";b.insert(ee+l.text),s.value.autoFocus&&b.focus()}),e.on("error",l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:B(l.code,l.detail,l.available),persistent:l.code==="LANGUAGE_NOT_AVAILABLE"||l.code==="NO_RECOGNIZERS",duration:8e3})});const _=()=>{if(t.app.platform!=="desktop")return;if(u.value.mode==="windows")return h()?{label:t.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.winH"),run:T};if(u.value.mode==="whisper"||u.value.mode==="whispercpp"){const m=t.t(u.value.mode==="whispercpp"?"pill.whispercpp":"pill.whisper");return R()==="recording"?{label:t.t("pill.recording"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:R()==="transcribing"?{label:t.t("pill.transcribing"),icon:"outline-audio",title:m}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:m,run:T}}const l=e.state()?.status??"idle";return l==="starting"?{label:t.t("pill.starting"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}:l==="listening"?{label:t.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:l==="error"?{label:t.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:t.t("pill.title"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}},H=()=>{if(t.app.platform!=="desktop")return;const l=n.speaking(),m=n.paused(),b=l?x().map(E=>({id:E.id,label:E.label,icon:E.icon,tone:E.id==="stop"?"danger":void 0,run:E.run})):[];return{label:l?m?t.t("read.paused"):t.t("read.speaking"):t.t("read.idle"),icon:"outline-audio",tone:l?"success":void 0,title:t.t("read.title"),rows:b.length>0?b:void 0,run:l?void 0:W}};t.add($.ComposerPill,()=>({id:"dictate",show:()=>_()})),t.add($.ComposerPill,()=>({id:"read",order:1,show:()=>H(),menu:l=>o.createComponent(Ge,{actions:x,get close(){return l.close}})})),t.add($.Command,()=>({id:"toggle",title:t.t("command.toggle"),description:t.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:t.app.platform==="desktop",run:T})),t.add($.Command,()=>({id:"configure",title:t.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),enabled:t.app.platform==="desktop",run:()=>t.layout.settings(t.id)})),t.add($.Command,()=>({id:"read",title:t.t("command.read"),description:t.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:t.app.platform==="desktop",run:W})),t.add($.SettingsPage,{id:t.id,icon:"outline-audio",get title(){return t.t("settings.title")},get entries(){return[{id:"engine",title:t.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:t.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:t.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:t.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:t.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:t.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"tts-engine",title:t.t("speech.engine"),keywords:"voice engine motor edge neural sistema"},{id:"whisper-python",title:t.t("whisper.python"),keywords:"whisper python venv entorno"},{id:"whisper-model",title:t.t("whisper.model"),keywords:"whisper model modelo"},{id:"whisper-language",title:t.t("whisper.language"),keywords:"whisper language idioma"},{id:"whisper-test",title:t.t("whisper.test"),keywords:"whisper check comprobar"},{id:"whispercpp-model",title:t.t("whispercpp.model"),keywords:"whisper.cpp model modelo"},{id:"whispercpp-language",title:t.t("whispercpp.language"),keywords:"whisper.cpp language idioma"},{id:"whispercpp-ffmpeg",title:t.t("whispercpp.ffmpeg"),keywords:"whisper.cpp ffmpeg audio convertir"},{id:"whispercpp-install",title:t.t("whispercpp.install"),keywords:"whisper.cpp download descargar instalar"},{id:"whispercpp-test",title:t.t("whispercpp.test"),keywords:"whisper.cpp check comprobar"},{id:"rate",title:t.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:t.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:t.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>o.createComponent(qe,{speaker:n})})},Ke=$.Plugin.define({...$e,setup:Xe,main:Fe});exports.default=Ke;
|
package/docs/whispercpp.md
CHANGED
|
@@ -11,7 +11,10 @@ GPU.
|
|
|
11
11
|
## Requisitos
|
|
12
12
|
|
|
13
13
|
- **OpenCode Desktop** en Linux (el plugin GUI no se carga en la consola/TUI).
|
|
14
|
-
- **ffmpeg
|
|
14
|
+
- **ffmpeg obligatorio**. Sin ffmpeg no se puede convertir la grabación WebM a
|
|
15
|
+
WAV de 16 kHz y la transcripción falla. El plugin lo detecta y ofrece
|
|
16
|
+
**Instalar ffmpeg** en Ajustes; si no puede, muestra el comando. Instalación
|
|
17
|
+
manual:
|
|
15
18
|
```bash
|
|
16
19
|
sudo apt update && sudo apt install -y ffmpeg
|
|
17
20
|
ffmpeg -version
|
|
@@ -23,18 +26,38 @@ GPU.
|
|
|
23
26
|
|
|
24
27
|
No hace falta Python, `cmake` ni GPU.
|
|
25
28
|
|
|
29
|
+
## ffmpeg (obligatorio)
|
|
30
|
+
|
|
31
|
+
`whisper-server` solo entiende WAV mono a 16 kHz. La ventana graba WebM/Opus,
|
|
32
|
+
así que el plugin usa `ffmpeg` para convertir el audio antes de transcribir.
|
|
33
|
+
|
|
34
|
+
- **Comprobación**: en Ajustes → Dictado por voz → whisper.cpp verás una fila
|
|
35
|
+
**ffmpeg** con la versión detectada o un aviso de que falta.
|
|
36
|
+
- **Instalación desde el plugin**: pulsa **Instalar ffmpeg**. Se usa `pkexec`
|
|
37
|
+
para mostrar el diálogo de contraseña del sistema (no se guarda). Si `pkexec`
|
|
38
|
+
no está disponible, se intenta `sudo -n`; si tampoco, se muestra el comando
|
|
39
|
+
manual.
|
|
40
|
+
- **Manual**:
|
|
41
|
+
```bash
|
|
42
|
+
sudo apt update && sudo apt install -y ffmpeg
|
|
43
|
+
```
|
|
44
|
+
- **Otras distros**: `sudo dnf install -y ffmpeg`, `sudo pacman -S --noconfirm ffmpeg`
|
|
45
|
+
o `sudo zypper install -y ffmpeg`.
|
|
46
|
+
|
|
26
47
|
## Instalación desde el plugin
|
|
27
48
|
|
|
28
49
|
1. Instala el plugin (`opencode plugin add opencode-voice-dictation`) y reinicia
|
|
29
50
|
OpenCode Desktop.
|
|
30
51
|
2. Abre **Ajustes → Dictado por voz**. En Linux el motor es
|
|
31
52
|
`whisper.cpp (local, nativo)`.
|
|
32
|
-
3.
|
|
53
|
+
3. Comprueba la fila **ffmpeg**. Si falta, pulsa **Instalar ffmpeg** (o instálalo
|
|
54
|
+
a mano con `sudo apt install -y ffmpeg`).
|
|
55
|
+
4. Elige el modelo:
|
|
33
56
|
- `base-q5_1` (57 MB): el más ligero; recomendado para portátiles modestos.
|
|
34
57
|
- `small-q5_1` (~180 MB): más preciso, consume más CPU y memoria.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
5. Pulsa **Descargar motor**. Se bajan el binario oficial y el modelo.
|
|
59
|
+
6. Pulsa **Comprobar** para verificar que están instalados.
|
|
60
|
+
7. Pulsa la píldora del micrófono, habla y vuelve a pulsarla: el texto se
|
|
38
61
|
inserta en el prompt.
|
|
39
62
|
|
|
40
63
|
La descarga se guarda en:
|
|
@@ -99,8 +122,9 @@ whisper-server (proceso persistente)
|
|
|
99
122
|
| Motor de dictado | `whisper.cpp` en Linux | Único motor disponible en Linux. |
|
|
100
123
|
| Modelo | `base-q5_1` | `base-q5_1` (57 MB) o `small-q5_1` (~180 MB). |
|
|
101
124
|
| Idioma | Automático | Autodetección o `es`, `en`, `pt`. |
|
|
125
|
+
| ffmpeg | — | Comprueba si está instalado y ofrece **Instalar ffmpeg**. Es obligatorio. |
|
|
102
126
|
| Descargar motor | — | Baja el binario y el modelo seleccionado. |
|
|
103
|
-
| Comprobar whisper.cpp | — | Indica si el servidor
|
|
127
|
+
| Comprobar whisper.cpp | — | Indica si el servidor, el modelo y ffmpeg están listos. |
|
|
104
128
|
|
|
105
129
|
## Rendimiento orientativo
|
|
106
130
|
|
|
@@ -120,7 +144,8 @@ va justo, quédate en `base-q5_1`.
|
|
|
120
144
|
|---|---|
|
|
121
145
|
| «Whisper.cpp is not installed» | Pulsa **Descargar motor** en Ajustes. |
|
|
122
146
|
| La descarga falla | Revisa internet, proxy o firewall hacia GitHub y Hugging Face. |
|
|
123
|
-
| `ffmpeg` no encontrado | `sudo apt install ffmpeg
|
|
147
|
+
| `ffmpeg` no encontrado | Pulsa **Instalar ffmpeg** en Ajustes o ejecuta `sudo apt update && sudo apt install -y ffmpeg`. Comprueba con `ffmpeg -version`. |
|
|
148
|
+
| «Falta ffmpeg» al dictar | Es obligatorio: sin él no se convierte el WebM a WAV. Instálalo desde Ajustes o con `sudo apt install -y ffmpeg`. |
|
|
124
149
|
| El binario no arranca | `glibc` antigua o arquitectura distinta; compila `whisper.cpp` para tu sistema o usa Ubuntu 22.04+. |
|
|
125
150
|
| Se queda en «Transcribiendo…» | Mira el toast y el log del servidor de OpenCode; suele ser `ffmpeg` o el binario. |
|
|
126
151
|
| El RPC devuelve 400 | El plugin server no cargó: Ajustes → Extensiones → Plugins → **Reload**. |
|
package/package.json
CHANGED
package/src/gui/definition.ts
CHANGED
|
@@ -142,12 +142,22 @@ export const definition = Plugin.define({
|
|
|
142
142
|
"whispercpp.install.ok": "whisper.cpp installed: {model}",
|
|
143
143
|
"whispercpp.install.error": "Could not install whisper.cpp: {error}",
|
|
144
144
|
"whispercpp.test": "Check whisper.cpp",
|
|
145
|
-
"whispercpp.test.description": "Reports whether the server
|
|
145
|
+
"whispercpp.test.description": "Reports whether the server, the selected model and ffmpeg are ready.",
|
|
146
146
|
"whispercpp.test.run": "Check",
|
|
147
147
|
"whispercpp.test.ready": "whisper.cpp ready: {model}",
|
|
148
148
|
"whispercpp.test.missing": "whisper.cpp is not installed. Download the engine first.",
|
|
149
149
|
"whispercpp.test.error": "whisper.cpp is not ready: {error}",
|
|
150
150
|
"whispercpp.empty": "whisper.cpp returned no text.",
|
|
151
|
+
"whispercpp.ffmpeg": "ffmpeg",
|
|
152
|
+
"whispercpp.ffmpeg.description": "Required to convert the WebM recording to 16 kHz WAV. The plugin can install it with your system password.",
|
|
153
|
+
"whispercpp.ffmpeg.ready": "ffmpeg ready: {version}",
|
|
154
|
+
"whispercpp.ffmpeg.missing": "ffmpeg is not installed. Without it the audio cannot be transcribed.",
|
|
155
|
+
"whispercpp.ffmpeg.install": "Install ffmpeg",
|
|
156
|
+
"whispercpp.ffmpeg.install.busy": "Installing…",
|
|
157
|
+
"whispercpp.ffmpeg.install.ok": "ffmpeg installed.",
|
|
158
|
+
"whispercpp.ffmpeg.install.error": "Could not install ffmpeg: {error}",
|
|
159
|
+
"whispercpp.ffmpeg.install.manual": "Run it manually: {command}",
|
|
160
|
+
"error.ffmpeg": "ffmpeg is required to convert the audio but is not installed. Open Settings → Voice dictation → whisper.cpp and install it, or run: sudo apt install -y ffmpeg",
|
|
151
161
|
"whispercpp.linux": "whisper.cpp is only available on Linux. On Windows use Win+H, System.Speech or WhisperX.",
|
|
152
162
|
"settings.winh": "Test Win+H",
|
|
153
163
|
"settings.winh.description": "Sends the Windows voice typing shortcut. If the floating bar appears, it works.",
|
|
@@ -272,12 +282,22 @@ export const definition = Plugin.define({
|
|
|
272
282
|
"whispercpp.install.ok": "whisper.cpp instalado: {model}",
|
|
273
283
|
"whispercpp.install.error": "No se pudo instalar whisper.cpp: {error}",
|
|
274
284
|
"whispercpp.test": "Comprobar whisper.cpp",
|
|
275
|
-
"whispercpp.test.description": "Indica si el servidor
|
|
285
|
+
"whispercpp.test.description": "Indica si el servidor, el modelo elegido y ffmpeg están listos.",
|
|
276
286
|
"whispercpp.test.run": "Comprobar",
|
|
277
287
|
"whispercpp.test.ready": "whisper.cpp listo: {model}",
|
|
278
288
|
"whispercpp.test.missing": "whisper.cpp no está instalado. Descarga el motor primero.",
|
|
279
289
|
"whispercpp.test.error": "whisper.cpp no está listo: {error}",
|
|
280
290
|
"whispercpp.empty": "whisper.cpp no devolvió texto.",
|
|
291
|
+
"whispercpp.ffmpeg": "ffmpeg",
|
|
292
|
+
"whispercpp.ffmpeg.description": "Necesario para convertir la grabación WebM a WAV de 16 kHz. El plugin puede instalarlo con tu contraseña del sistema.",
|
|
293
|
+
"whispercpp.ffmpeg.ready": "ffmpeg listo: {version}",
|
|
294
|
+
"whispercpp.ffmpeg.missing": "ffmpeg no está instalado. Sin él no se puede transcribir el audio.",
|
|
295
|
+
"whispercpp.ffmpeg.install": "Instalar ffmpeg",
|
|
296
|
+
"whispercpp.ffmpeg.install.busy": "Instalando…",
|
|
297
|
+
"whispercpp.ffmpeg.install.ok": "ffmpeg instalado correctamente.",
|
|
298
|
+
"whispercpp.ffmpeg.install.error": "No se pudo instalar ffmpeg: {error}",
|
|
299
|
+
"whispercpp.ffmpeg.install.manual": "Ejecútalo manualmente: {command}",
|
|
300
|
+
"error.ffmpeg": "Falta ffmpeg, necesario para convertir el audio. Ve a Ajustes → Dictado por voz → whisper.cpp e instálalo, o ejecuta: sudo apt install -y ffmpeg",
|
|
281
301
|
"whispercpp.linux": "whisper.cpp solo está disponible en Linux. En Windows usa Win+H, System.Speech o WhisperX.",
|
|
282
302
|
"settings.winh": "Probar Win+H",
|
|
283
303
|
"settings.winh.description": "Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC errors do not always arrive as `Error` instances: the bridge may deliver
|
|
3
|
+
* a plain object. Extract the useful text so the UI shows the real cause (for
|
|
4
|
+
* example, a missing ffmpeg) instead of a generic message.
|
|
5
|
+
*/
|
|
6
|
+
export function errorMessage(cause: unknown, fallback: string): string {
|
|
7
|
+
if (cause instanceof Error && cause.message) return cause.message
|
|
8
|
+
if (typeof cause === "string" && cause) return cause
|
|
9
|
+
|
|
10
|
+
if (cause && typeof cause === "object") {
|
|
11
|
+
const value = cause as Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
for (const key of ["message", "error", "detail", "defect"]) {
|
|
14
|
+
const found = value[key]
|
|
15
|
+
if (typeof found === "string" && found) return found
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data = value.data
|
|
19
|
+
if (data && typeof data === "object") {
|
|
20
|
+
const message = (data as Record<string, unknown>).message
|
|
21
|
+
if (typeof message === "string" && message) return message
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const json = JSON.stringify(cause)
|
|
26
|
+
if (json && json !== "{}") return json
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fallback
|
|
33
|
+
}
|
package/src/gui/settings.tsx
CHANGED
|
@@ -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 { errorMessage } from "./errors"
|
|
6
7
|
import { VoiceRpc } from "../rpc"
|
|
7
8
|
|
|
8
9
|
const FALLBACK_LOCALES = [
|
|
@@ -40,8 +41,37 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
40
41
|
const [whisperBusy, setWhisperBusy] = createSignal(false)
|
|
41
42
|
const [whisperCppBusy, setWhisperCppBusy] = createSignal(false)
|
|
42
43
|
const [whisperCppCheckBusy, setWhisperCppCheckBusy] = createSignal(false)
|
|
44
|
+
const [whisperCppFfmpegBusy, setWhisperCppFfmpegBusy] = createSignal(false)
|
|
45
|
+
const [ffmpeg, setFfmpeg] = createSignal<{ available: boolean; version: string } | undefined>(undefined)
|
|
43
46
|
const [platform, setPlatform] = createSignal("")
|
|
44
47
|
|
|
48
|
+
type WhisperCppStatus = {
|
|
49
|
+
installed: boolean
|
|
50
|
+
ready: boolean
|
|
51
|
+
model: string
|
|
52
|
+
error: string
|
|
53
|
+
ffmpeg: boolean
|
|
54
|
+
ffmpegVersion: string
|
|
55
|
+
ffmpegPath: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const readWhisperCppStatus = async (): Promise<WhisperCppStatus> => {
|
|
59
|
+
const status = (await ctx.client.rpc(VoiceRpc).whisperCppStatus({
|
|
60
|
+
model: whisperCppPrefs.value.model,
|
|
61
|
+
})) as WhisperCppStatus
|
|
62
|
+
|
|
63
|
+
setFfmpeg({ available: status.ffmpeg, version: status.ffmpegVersion })
|
|
64
|
+
return status
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ffmpegDescription = () => {
|
|
68
|
+
const status = ffmpeg()
|
|
69
|
+
if (!status) return ctx.t("whispercpp.ffmpeg.description")
|
|
70
|
+
return status.available
|
|
71
|
+
? ctx.t("whispercpp.ffmpeg.ready", { version: status.version })
|
|
72
|
+
: ctx.t("whispercpp.ffmpeg.missing")
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
onMount(() => {
|
|
46
76
|
props.speaker.setConfig({ engine: ttsPrefs.value.engine, voice: ttsPrefs.value.voice })
|
|
47
77
|
|
|
@@ -64,6 +94,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
64
94
|
patchEngine(caps.engines[0] ?? "windows")
|
|
65
95
|
}
|
|
66
96
|
if (caps.platform !== "linux") loadLocales()
|
|
97
|
+
else void readWhisperCppStatus().catch(() => {})
|
|
67
98
|
})
|
|
68
99
|
.catch(() => loadLocales())
|
|
69
100
|
|
|
@@ -160,6 +191,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
160
191
|
title: ctx.t("pill.title"),
|
|
161
192
|
description: ctx.t("whispercpp.install.ok", { model: result.model }),
|
|
162
193
|
})
|
|
194
|
+
await readWhisperCppStatus().catch(() => {})
|
|
163
195
|
} else {
|
|
164
196
|
ctx.toast.show({
|
|
165
197
|
variant: "error",
|
|
@@ -173,7 +205,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
173
205
|
variant: "error",
|
|
174
206
|
title: ctx.t("pill.title"),
|
|
175
207
|
description: ctx.t("whispercpp.install.error", {
|
|
176
|
-
error: cause
|
|
208
|
+
error: errorMessage(cause, "?"),
|
|
177
209
|
}),
|
|
178
210
|
persistent: true,
|
|
179
211
|
})
|
|
@@ -185,9 +217,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
185
217
|
const checkWhisperCpp = async () => {
|
|
186
218
|
setWhisperCppCheckBusy(true)
|
|
187
219
|
try {
|
|
188
|
-
const status =
|
|
189
|
-
model: whisperCppPrefs.value.model,
|
|
190
|
-
})) as { installed: boolean; ready: boolean; model: string; error: string }
|
|
220
|
+
const status = await readWhisperCppStatus()
|
|
191
221
|
|
|
192
222
|
if (status.error) {
|
|
193
223
|
ctx.toast.show({
|
|
@@ -202,6 +232,13 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
202
232
|
title: ctx.t("pill.title"),
|
|
203
233
|
description: ctx.t("whispercpp.test.missing"),
|
|
204
234
|
})
|
|
235
|
+
} else if (!status.ffmpeg) {
|
|
236
|
+
ctx.toast.show({
|
|
237
|
+
variant: "error",
|
|
238
|
+
title: ctx.t("pill.title"),
|
|
239
|
+
description: ctx.t("whispercpp.ffmpeg.missing"),
|
|
240
|
+
persistent: true,
|
|
241
|
+
})
|
|
205
242
|
} else {
|
|
206
243
|
ctx.toast.show({
|
|
207
244
|
variant: "success",
|
|
@@ -214,7 +251,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
214
251
|
variant: "error",
|
|
215
252
|
title: ctx.t("pill.title"),
|
|
216
253
|
description: ctx.t("whispercpp.test.error", {
|
|
217
|
-
error: cause
|
|
254
|
+
error: errorMessage(cause, "?"),
|
|
218
255
|
}),
|
|
219
256
|
})
|
|
220
257
|
} finally {
|
|
@@ -222,6 +259,48 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
222
259
|
}
|
|
223
260
|
}
|
|
224
261
|
|
|
262
|
+
const installFfmpeg = async () => {
|
|
263
|
+
setWhisperCppFfmpegBusy(true)
|
|
264
|
+
try {
|
|
265
|
+
const result = (await ctx.client.rpc(VoiceRpc).whisperCppInstallFfmpeg({})) as {
|
|
266
|
+
ok: boolean
|
|
267
|
+
error: string
|
|
268
|
+
command: string
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (result.ok) {
|
|
272
|
+
ctx.toast.show({
|
|
273
|
+
variant: "success",
|
|
274
|
+
title: ctx.t("pill.title"),
|
|
275
|
+
description: ctx.t("whispercpp.ffmpeg.install.ok"),
|
|
276
|
+
})
|
|
277
|
+
await readWhisperCppStatus()
|
|
278
|
+
} else {
|
|
279
|
+
const manual = result.command
|
|
280
|
+
? ` ${ctx.t("whispercpp.ffmpeg.install.manual", { command: result.command })}`
|
|
281
|
+
: ""
|
|
282
|
+
|
|
283
|
+
ctx.toast.show({
|
|
284
|
+
variant: "error",
|
|
285
|
+
title: ctx.t("pill.title"),
|
|
286
|
+
description: ctx.t("whispercpp.ffmpeg.install.error", { error: result.error || "?" }) + manual,
|
|
287
|
+
persistent: true,
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
ctx.toast.show({
|
|
292
|
+
variant: "error",
|
|
293
|
+
title: ctx.t("pill.title"),
|
|
294
|
+
description: ctx.t("whispercpp.ffmpeg.install.error", {
|
|
295
|
+
error: errorMessage(cause, "?"),
|
|
296
|
+
}),
|
|
297
|
+
persistent: true,
|
|
298
|
+
})
|
|
299
|
+
} finally {
|
|
300
|
+
setWhisperCppFfmpegBusy(false)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
225
304
|
const testWhisper = async () => {
|
|
226
305
|
setWhisperBusy(true)
|
|
227
306
|
try {
|
|
@@ -456,6 +535,23 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
456
535
|
/>
|
|
457
536
|
</ui.SettingsRow>
|
|
458
537
|
|
|
538
|
+
<ui.SettingsRow
|
|
539
|
+
id="whispercpp-ffmpeg"
|
|
540
|
+
title={ctx.t("whispercpp.ffmpeg")}
|
|
541
|
+
description={ffmpegDescription()}
|
|
542
|
+
>
|
|
543
|
+
<Show when={ffmpeg()?.available === false}>
|
|
544
|
+
<ui.Button busy={whisperCppFfmpegBusy()} onClick={() => void installFfmpeg()}>
|
|
545
|
+
{ctx.t("whispercpp.ffmpeg.install")}
|
|
546
|
+
</ui.Button>
|
|
547
|
+
</Show>
|
|
548
|
+
<Show when={ffmpeg()?.available !== false}>
|
|
549
|
+
<ui.Button onClick={() => void readWhisperCppStatus().catch(() => {})}>
|
|
550
|
+
{ctx.t("whispercpp.test.run")}
|
|
551
|
+
</ui.Button>
|
|
552
|
+
</Show>
|
|
553
|
+
</ui.SettingsRow>
|
|
554
|
+
|
|
459
555
|
<ui.SettingsRow
|
|
460
556
|
id="whispercpp-install"
|
|
461
557
|
title={ctx.t("whispercpp.install")}
|
package/src/gui/window.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import type { definition } from "./definition"
|
|
|
14
14
|
import { createSpeaker, type Speaker } from "./speak"
|
|
15
15
|
import { VoiceSettings } from "./settings"
|
|
16
16
|
import { blobToBase64, startRecording, type Recording } from "./recorder"
|
|
17
|
+
import { errorMessage } from "./errors"
|
|
17
18
|
import { VoiceRpc } from "../rpc"
|
|
18
19
|
|
|
19
20
|
interface ReadAction {
|
|
@@ -154,7 +155,7 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
154
155
|
ctx.toast.show({
|
|
155
156
|
variant: "error",
|
|
156
157
|
title: ctx.t("pill.title"),
|
|
157
|
-
description: cause
|
|
158
|
+
description: errorMessage(cause, localError()),
|
|
158
159
|
})
|
|
159
160
|
}
|
|
160
161
|
}
|
|
@@ -215,10 +216,11 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
215
216
|
composer.focus()
|
|
216
217
|
}
|
|
217
218
|
} catch (cause) {
|
|
219
|
+
const message = errorMessage(cause, localError())
|
|
218
220
|
ctx.toast.show({
|
|
219
221
|
variant: "error",
|
|
220
222
|
title: ctx.t("pill.title"),
|
|
221
|
-
description:
|
|
223
|
+
description: message.includes("FFMPEG_MISSING") ? ctx.t("error.ffmpeg") : message,
|
|
222
224
|
})
|
|
223
225
|
} finally {
|
|
224
226
|
setWhisperState("idle")
|
|
@@ -501,6 +503,7 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
501
503
|
{ id: "whisper-test", title: ctx.t("whisper.test"), keywords: "whisper check comprobar" },
|
|
502
504
|
{ id: "whispercpp-model", title: ctx.t("whispercpp.model"), keywords: "whisper.cpp model modelo" },
|
|
503
505
|
{ id: "whispercpp-language", title: ctx.t("whispercpp.language"), keywords: "whisper.cpp language idioma" },
|
|
506
|
+
{ id: "whispercpp-ffmpeg", title: ctx.t("whispercpp.ffmpeg"), keywords: "whisper.cpp ffmpeg audio convertir" },
|
|
504
507
|
{ id: "whispercpp-install", title: ctx.t("whispercpp.install"), keywords: "whisper.cpp download descargar instalar" },
|
|
505
508
|
{ id: "whispercpp-test", title: ctx.t("whispercpp.test"), keywords: "whisper.cpp check comprobar" },
|
|
506
509
|
{ id: "rate", title: ctx.t("speech.rate"), keywords: "read aloud speed velocidad lectura" },
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Plugin } from "@opencode/plugin"
|
|
|
2
2
|
import { listEdgeVoices, synthesizeEdge } from "./edge.js"
|
|
3
3
|
import { VoiceRpc } from "./rpc.js"
|
|
4
4
|
import { WhisperEngine } from "./whisper/server.js"
|
|
5
|
-
import { installWhisperCpp, WhisperCppEngine } from "./whispercpp/server.js"
|
|
5
|
+
import { installWhisperCpp, WhisperCppEngine, installFfmpeg } from "./whispercpp/server.js"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Server part of the plugin.
|
|
@@ -61,6 +61,7 @@ export default Plugin.define({
|
|
|
61
61
|
const { model } = input as { model: string }
|
|
62
62
|
return whisperCpp.status(model)
|
|
63
63
|
},
|
|
64
|
+
whisperCppInstallFfmpeg: async () => installFfmpeg(),
|
|
64
65
|
whisperCppTranscribe: async (input) => {
|
|
65
66
|
return whisperCpp.transcribe(input as {
|
|
66
67
|
audio: string
|
package/src/rpc.ts
CHANGED
|
@@ -138,8 +138,24 @@ export const VoiceRpc = {
|
|
|
138
138
|
ready: { type: "boolean" },
|
|
139
139
|
model: { type: "string" },
|
|
140
140
|
error: { type: "string" },
|
|
141
|
+
ffmpeg: { type: "boolean" },
|
|
142
|
+
ffmpegVersion: { type: "string" },
|
|
143
|
+
ffmpegPath: { type: "string" },
|
|
141
144
|
},
|
|
142
|
-
required: ["installed", "ready", "model", "error"],
|
|
145
|
+
required: ["installed", "ready", "model", "error", "ffmpeg", "ffmpegVersion", "ffmpegPath"],
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
whisperCppInstallFfmpeg: {
|
|
150
|
+
input: { type: "object", additionalProperties: false },
|
|
151
|
+
output: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
ok: { type: "boolean" },
|
|
155
|
+
error: { type: "string" },
|
|
156
|
+
command: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
required: ["ok", "error", "command"],
|
|
143
159
|
additionalProperties: false,
|
|
144
160
|
},
|
|
145
161
|
},
|
package/src/whispercpp/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process"
|
|
2
2
|
import { randomUUID } from "node:crypto"
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
4
4
|
import { connect } from "node:net"
|
|
5
5
|
import { homedir, tmpdir } from "node:os"
|
|
6
6
|
import { join } from "node:path"
|
|
@@ -22,6 +22,9 @@ export interface WhisperCppStatus {
|
|
|
22
22
|
ready: boolean
|
|
23
23
|
model: string
|
|
24
24
|
error: string
|
|
25
|
+
ffmpeg: boolean
|
|
26
|
+
ffmpegVersion: string
|
|
27
|
+
ffmpegPath: string
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface WhisperCppResult {
|
|
@@ -29,6 +32,25 @@ export interface WhisperCppResult {
|
|
|
29
32
|
language: string
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
export interface FfmpegStatus {
|
|
36
|
+
available: boolean
|
|
37
|
+
version: string
|
|
38
|
+
path: string
|
|
39
|
+
error: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface InstallFfmpegResult {
|
|
43
|
+
ok: boolean
|
|
44
|
+
error: string
|
|
45
|
+
command: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface RunResult {
|
|
49
|
+
code: number | null
|
|
50
|
+
stdout: string
|
|
51
|
+
stderr: string
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
function rootDir(): string {
|
|
33
55
|
return join(homedir(), ".local", "share", "opencode-voice-dictation", "whisper.cpp")
|
|
34
56
|
}
|
|
@@ -78,6 +100,141 @@ function run(command: string, args: string[]): Promise<string> {
|
|
|
78
100
|
})
|
|
79
101
|
}
|
|
80
102
|
|
|
103
|
+
/** Runs a command and returns its output instead of throwing on a non-zero exit. */
|
|
104
|
+
function runCapture(command: string, args: string[]): Promise<RunResult> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const child = spawn(command, args, { windowsHide: true })
|
|
107
|
+
let stdout = ""
|
|
108
|
+
let stderr = ""
|
|
109
|
+
|
|
110
|
+
child.stdout?.setEncoding("utf8")
|
|
111
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
112
|
+
stdout = (stdout + chunk).slice(-8000)
|
|
113
|
+
})
|
|
114
|
+
child.stderr?.setEncoding("utf8")
|
|
115
|
+
child.stderr?.on("data", (chunk: string) => {
|
|
116
|
+
stderr = (stderr + chunk).slice(-8000)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
child.on("error", (cause) => {
|
|
120
|
+
resolve({ code: null, stdout, stderr: (stderr + errorText(cause)).slice(-8000) })
|
|
121
|
+
})
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
resolve({ code, stdout, stderr: stderr.slice(-8000) })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function errorText(cause: unknown): string {
|
|
129
|
+
return cause instanceof Error ? cause.message : String(cause)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function commandExists(command: string): boolean {
|
|
133
|
+
const path = process.env.PATH ?? ""
|
|
134
|
+
|
|
135
|
+
return path.split(":").some((directory) => {
|
|
136
|
+
try {
|
|
137
|
+
accessSync(join(directory || ".", command), constants.X_OK)
|
|
138
|
+
return true
|
|
139
|
+
} catch {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Checks that ffmpeg can run. ffmpeg converts the browser WebM to 16 kHz WAV. */
|
|
146
|
+
export async function detectFfmpeg(): Promise<FfmpegStatus> {
|
|
147
|
+
const result = await runCapture("ffmpeg", ["-version"])
|
|
148
|
+
|
|
149
|
+
if (result.code === 0) {
|
|
150
|
+
const version =
|
|
151
|
+
result.stdout
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.find((line) => line.startsWith("ffmpeg version")) ?? "ffmpeg"
|
|
155
|
+
|
|
156
|
+
return { available: true, version, path: "ffmpeg", error: "" }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
available: false,
|
|
161
|
+
version: "",
|
|
162
|
+
path: "",
|
|
163
|
+
error: result.stderr.trim() || "ffmpeg not found in PATH",
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Installs ffmpeg with the system package manager.
|
|
169
|
+
*
|
|
170
|
+
* Uses `pkexec` (the desktop's authentication dialog) so the user's password is
|
|
171
|
+
* never handled by the plugin. Falls back to passwordless `sudo -n` and, if
|
|
172
|
+
* neither works, returns the command the user can run manually.
|
|
173
|
+
*/
|
|
174
|
+
export async function installFfmpeg(): Promise<InstallFfmpegResult> {
|
|
175
|
+
const manual = "sudo apt update && sudo apt install -y ffmpeg"
|
|
176
|
+
|
|
177
|
+
if (process.platform !== "linux") {
|
|
178
|
+
return { ok: false, error: "Automatic ffmpeg installation is only available on Linux", command: manual }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const current = await detectFfmpeg()
|
|
182
|
+
if (current.available) {
|
|
183
|
+
return { ok: true, error: "", command: "" }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!commandExists("apt-get")) {
|
|
187
|
+
const command = commandExists("dnf")
|
|
188
|
+
? "sudo dnf install -y ffmpeg"
|
|
189
|
+
: commandExists("pacman")
|
|
190
|
+
? "sudo pacman -S --noconfirm ffmpeg"
|
|
191
|
+
: commandExists("zypper")
|
|
192
|
+
? "sudo zypper install -y ffmpeg"
|
|
193
|
+
: manual
|
|
194
|
+
|
|
195
|
+
return { ok: false, error: "No supported package manager was found", command }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const install = "DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg"
|
|
199
|
+
const updateAndInstall = `DEBIAN_FRONTEND=noninteractive apt-get update && ${install}`
|
|
200
|
+
const attempts: Array<{ command: string; args: string[] }> = []
|
|
201
|
+
|
|
202
|
+
if (commandExists("pkexec")) {
|
|
203
|
+
attempts.push({ command: "pkexec", args: ["sh", "-c", install] })
|
|
204
|
+
attempts.push({ command: "pkexec", args: ["sh", "-c", updateAndInstall] })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (commandExists("sudo")) {
|
|
208
|
+
attempts.push({ command: "sudo", args: ["-n", "sh", "-c", install] })
|
|
209
|
+
attempts.push({ command: "sudo", args: ["-n", "sh", "-c", updateAndInstall] })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let lastError = ""
|
|
213
|
+
|
|
214
|
+
for (const attempt of attempts) {
|
|
215
|
+
const result = await runCapture(attempt.command, attempt.args)
|
|
216
|
+
|
|
217
|
+
if (result.code === 0) {
|
|
218
|
+
const after = await detectFfmpeg()
|
|
219
|
+
if (after.available) return { ok: true, error: "", command: "" }
|
|
220
|
+
lastError = after.error || "ffmpeg is still not available"
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
lastError = result.stderr.trim() || `${attempt.command} exited with code ${result.code ?? "unknown"}`
|
|
225
|
+
|
|
226
|
+
// 126 is "dismissed/not authorized" in pkexec: do not ask for the password
|
|
227
|
+
// again just because the update-and-install fallback exists.
|
|
228
|
+
if (attempt.command === "pkexec" && (result.code === 126 || result.code === 127)) break
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
error: lastError || "ffmpeg installation failed",
|
|
234
|
+
command: manual,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
81
238
|
export async function installWhisperCpp(model: string): Promise<{ ok: boolean; model: string; error: string }> {
|
|
82
239
|
if (process.platform !== "linux") {
|
|
83
240
|
return { ok: false, model: modelFile(model), error: "Whisper.cpp native is only implemented for Linux" }
|
|
@@ -216,6 +373,14 @@ export class WhisperCppEngine {
|
|
|
216
373
|
language: string
|
|
217
374
|
model: string
|
|
218
375
|
}): Promise<WhisperCppResult> {
|
|
376
|
+
const ffmpeg = await detectFfmpeg()
|
|
377
|
+
|
|
378
|
+
if (!ffmpeg.available) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`FFMPEG_MISSING: ffmpeg is required to convert the audio. Install it with: sudo apt install -y ffmpeg (${ffmpeg.error})`,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
219
384
|
const port = await this.ensure(input.model, input.language)
|
|
220
385
|
|
|
221
386
|
const dir = join(tmpdir(), "opencode-voice-dictation")
|
|
@@ -228,7 +393,24 @@ export class WhisperCppEngine {
|
|
|
228
393
|
writeFileSync(source, Buffer.from(input.audio, "base64"))
|
|
229
394
|
|
|
230
395
|
try {
|
|
231
|
-
|
|
396
|
+
try {
|
|
397
|
+
await run("ffmpeg", [
|
|
398
|
+
"-y",
|
|
399
|
+
"-loglevel",
|
|
400
|
+
"error",
|
|
401
|
+
"-i",
|
|
402
|
+
source,
|
|
403
|
+
"-ar",
|
|
404
|
+
"16000",
|
|
405
|
+
"-ac",
|
|
406
|
+
"1",
|
|
407
|
+
"-c:a",
|
|
408
|
+
"pcm_s16le",
|
|
409
|
+
wav,
|
|
410
|
+
])
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
throw new Error(`ffmpeg failed: ${errorText(cause)}`)
|
|
413
|
+
}
|
|
232
414
|
|
|
233
415
|
const form = new FormData()
|
|
234
416
|
form.append("file", new Blob([new Uint8Array(readFileSync(wav))], { type: "audio/wav" }), "audio.wav")
|
|
@@ -251,8 +433,9 @@ export class WhisperCppEngine {
|
|
|
251
433
|
}
|
|
252
434
|
}
|
|
253
435
|
|
|
254
|
-
status(model: string): WhisperCppStatus {
|
|
436
|
+
async status(model: string): Promise<WhisperCppStatus> {
|
|
255
437
|
const file = modelFile(model)
|
|
438
|
+
const ffmpeg = await detectFfmpeg()
|
|
256
439
|
const installed = existsSync(binaryPath()) && existsSync(modelPath(file))
|
|
257
440
|
|
|
258
441
|
return {
|
|
@@ -260,6 +443,9 @@ export class WhisperCppEngine {
|
|
|
260
443
|
ready: this.state !== undefined && this.state.model === file,
|
|
261
444
|
model: file,
|
|
262
445
|
error: this.error,
|
|
446
|
+
ffmpeg: ffmpeg.available,
|
|
447
|
+
ffmpegVersion: ffmpeg.version,
|
|
448
|
+
ffmpegPath: ffmpeg.path,
|
|
263
449
|
}
|
|
264
450
|
}
|
|
265
451
|
|