opencode-voice-dictation 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ # Dictado con whisper.cpp nativo (Linux)
2
+
3
+ Guía para configurar el motor **whisper.cpp** del plugin en Ubuntu y otras
4
+ distros Linux. Transcribe **audio a texto** en la CPU, sin WSL, sin Python y sin
5
+ GPU.
6
+
7
+ > Es el motor por defecto en Linux. En Windows no está disponible: usa
8
+ > `Windows Voice Typing (Win+H)`, `System.Speech` o
9
+ > [WhisperX](whisperx.md).
10
+
11
+ ## Requisitos
12
+
13
+ - **OpenCode Desktop** en Linux (el plugin GUI no se carga en la consola/TUI).
14
+ - **ffmpeg** en el `PATH`:
15
+ ```bash
16
+ sudo apt update && sudo apt install -y ffmpeg
17
+ ffmpeg -version
18
+ ```
19
+ - **glibc** compatible con el binario oficial: Ubuntu 22.04 o posterior. En
20
+ distros muy antiguas habría que compilar `whisper.cpp` a mano.
21
+ - **Internet la primera vez**: ~10 MB de binario y 57–180 MB de modelo.
22
+ - Micrófono accesible por la app.
23
+
24
+ No hace falta Python, `cmake` ni GPU.
25
+
26
+ ## Instalación desde el plugin
27
+
28
+ 1. Instala el plugin (`opencode plugin add opencode-voice-dictation`) y reinicia
29
+ OpenCode Desktop.
30
+ 2. Abre **Ajustes → Dictado por voz**. En Linux el motor es
31
+ `whisper.cpp (local, nativo)`.
32
+ 3. Elige el modelo:
33
+ - `base-q5_1` (57 MB): el más ligero; recomendado para portátiles modestos.
34
+ - `small-q5_1` (~180 MB): más preciso, consume más CPU y memoria.
35
+ 4. Pulsa **Descargar motor**. Se bajan el binario oficial y el modelo.
36
+ 5. Pulsa **Comprobar** para verificar que están instalados.
37
+ 6. Pulsa la píldora del micrófono, habla y vuelve a pulsarla: el texto se
38
+ inserta en el prompt.
39
+
40
+ La descarga se guarda en:
41
+
42
+ ```
43
+ ~/.local/share/opencode-voice-dictation/whisper.cpp/
44
+ ├── whisper-bin-ubuntu-x64/ # o arm64
45
+ │ ├── whisper-server
46
+ │ └── lib*.so
47
+ ├── ggml-base-q5_1.bin
48
+ └── ggml-small-q5_1.bin # si lo descargaste
49
+ ```
50
+
51
+ ## Instalación manual (opcional)
52
+
53
+ Si prefieres preparar los archivos a mano:
54
+
55
+ ```bash
56
+ mkdir -p ~/.local/share/opencode-voice-dictation/whisper.cpp
57
+ cd ~/.local/share/opencode-voice-dictation/whisper.cpp
58
+
59
+ curl -L -o whisper-bin.tar.gz \
60
+ https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.2/whisper-bin-ubuntu-x64.tar.gz
61
+ tar xzf whisper-bin.tar.gz
62
+ rm whisper-bin.tar.gz
63
+
64
+ curl -L -O \
65
+ https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base-q5_1.bin
66
+ ```
67
+
68
+ El plugin detecta el binario y el modelo en esa ruta; no necesitas configurar
69
+ nada más.
70
+
71
+ ## Cómo funciona
72
+
73
+ ```
74
+ Ventana (OpenCode Desktop)
75
+ getUserMedia + MediaRecorder → WebM/Opus base64
76
+ │ RPC whisperCppTranscribe
77
+ ▼
78
+ Servidor del plugin
79
+ escribe /tmp/opencode-voice-dictation/whispercpp-*.webm
80
+ ffmpeg → WAV 16 kHz mono
81
+ POST http://127.0.0.1:<puerto>/inference
82
+ ▼
83
+ whisper-server (proceso persistente)
84
+ modelo cargado una vez; responde {"text": "..."}
85
+ ```
86
+
87
+ - `whisper-server` se arranca la primera vez y se mantiene vivo. Cargar el
88
+ modelo es lo lento; las frases siguientes son rápidas.
89
+ - Si cambias de modelo o de idioma, el servidor se reinicia para aplicar el
90
+ cambio.
91
+ - El puerto se elige al azar entre 18081 y 18580 y solo escucha en
92
+ `127.0.0.1`.
93
+ - El audio temporal se borra después de cada transcripción.
94
+
95
+ ## Ajustes
96
+
97
+ | Ajuste | Por defecto | Qué hace |
98
+ |---|---|---|
99
+ | Motor de dictado | `whisper.cpp` en Linux | Único motor disponible en Linux. |
100
+ | Modelo | `base-q5_1` | `base-q5_1` (57 MB) o `small-q5_1` (~180 MB). |
101
+ | Idioma | Automático | Autodetección o `es`, `en`, `pt`. |
102
+ | Descargar motor | — | Baja el binario y el modelo seleccionado. |
103
+ | Comprobar whisper.cpp | — | Indica si el servidor y el modelo están instalados. |
104
+
105
+ ## Rendimiento orientativo
106
+
107
+ Depende de la CPU. Como referencia con `base-q5_1` y 4 hilos:
108
+
109
+ | Equipo | Una frase de 5–10 s |
110
+ |---|---|
111
+ | Portátil moderno (4–8 núcleos) | ~1–3 s |
112
+ | Portátil modesto (2 núcleos) | ~4–10 s |
113
+
114
+ `small-q5_1` mejora la precisión y multiplica el tiempo por ~2–3. Si el equipo
115
+ va justo, quédate en `base-q5_1`.
116
+
117
+ ## Solución de problemas
118
+
119
+ | Síntoma | Causa / solución |
120
+ |---|---|
121
+ | «Whisper.cpp is not installed» | Pulsa **Descargar motor** en Ajustes. |
122
+ | La descarga falla | Revisa internet, proxy o firewall hacia GitHub y Hugging Face. |
123
+ | `ffmpeg` no encontrado | `sudo apt install ffmpeg` y comprueba `ffmpeg -version`. |
124
+ | El binario no arranca | `glibc` antigua o arquitectura distinta; compila `whisper.cpp` para tu sistema o usa Ubuntu 22.04+. |
125
+ | Se queda en «Transcribiendo…» | Mira el toast y el log del servidor de OpenCode; suele ser `ffmpeg` o el binario. |
126
+ | El RPC devuelve 400 | El plugin server no cargó: Ajustes → Extensiones → Plugins → **Reload**. |
127
+ | Tarda mucho | Usa `base-q5_1`; el primer dictado carga el modelo. |
128
+ | No hay píldora | La API GUI solo existe en OpenCode Desktop, no en la consola/TUI. |
129
+
130
+ ## Privacidad
131
+
132
+ El audio se escribe en `/tmp`, se convierte en local y se transcribe en tu CPU.
133
+ No se envía a ningún servicio. Solo la primera descarga usa la red
134
+ (GitHub para el binario y Hugging Face para el modelo).
135
+
136
+ ## Desktop y consola
137
+
138
+ - **OpenCode Desktop**: carga `./gui-experimental`, así que tiene píldora,
139
+ atajo `Ctrl+Shift+M`, `/dictado` y ajustes. whisper.cpp funciona completo.
140
+ - **OpenCode consola/TUI**: no carga la parte GUI. La parte servidor (RPC)
141
+ existe, pero no hay micrófono ni píldora. Para dictar en la consola haría
142
+ falta una parte `./tui` que grabara con `arecord`/`ffmpeg`; es un desarrollo
143
+ aparte y todavía no está incluido.
package/docs/whisperx.md CHANGED
@@ -6,7 +6,8 @@ de lectura.
6
6
 
7
7
  > Este motor es **avanzado**. Para un usuario sin WSL, Python y GPU NVIDIA, usa
8
8
  > `Windows Voice Typing (Win+H)` o un motor en la nube. La integración con
9
- > `whisper.cpp` nativo (sin WSL) está en el roadmap.
9
+ > `whisper.cpp` nativo (sin WSL) ya está disponible para Linux; ver
10
+ > [la guía de whisper.cpp](whispercpp.md).
10
11
 
11
12
  ## Requisitos
12
13
 
@@ -164,7 +165,7 @@ La idea es añadir más motores de dictado con la misma interfaz:
164
165
  | Windows Voice Typing (Win+H) | Disponible | Cero configuración, online |
165
166
  | System.Speech | Disponible | Offline, calidad antigua |
166
167
  | WhisperX local (WSL) | Disponible | Avanzado, NVIDIA + WSL |
167
- | `whisper.cpp` nativo | Planeado | Sin WSL/Python; binario + modelo |
168
+ | `whisper.cpp` nativo | Disponible | Linux, sin WSL ni Python; ver [la guía](whispercpp.md) |
168
169
  | Google Cloud Speech-to-Text | Planeado | API key, online |
169
170
  | OpenAI (`gpt-4o-transcribe` / `whisper-1`) | Planeado | API key, online |
170
171
  | Deepgram / AssemblyAI | Planeado | API key, online |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-voice-dictation",
3
- "version": "0.6.2",
4
- "description": "Offline voice dictation and read-aloud for OpenCode Desktop on Windows",
3
+ "version": "0.7.1",
4
+ "description": "Offline voice dictation and read-aloud for OpenCode Desktop on Windows and Linux",
5
5
  "license": "MIT",
6
6
  "author": "Fran Carlos <francarlos803@gmail.com>",
7
7
  "homepage": "https://github.com/evious/opencode-voice-dictation#readme",
@@ -20,6 +20,8 @@
20
20
  "speech-to-text",
21
21
  "text-to-speech",
22
22
  "windows",
23
+ "linux",
24
+ "whisper.cpp",
23
25
  "offline"
24
26
  ],
25
27
  "type": "module",
@@ -4,12 +4,12 @@ import { Plugin, Store } from "@opencode/plugin/gui"
4
4
  /**
5
5
  * Declarations read by the desktop app before either setup runs.
6
6
  *
7
- * `os: ["windows"]` keeps the plugin from loading on macOS/Linux, where
8
- * System.Speech does not exist. Settings shows it as not available instead of
9
- * failing.
7
+ * Windows uses Win+H, System.Speech and WhisperX. Linux uses whisper.cpp
8
+ * natively; the window reads the server capabilities and offers only the
9
+ * engines that exist on each OS.
10
10
  */
11
11
  export const definition = Plugin.define({
12
- os: ["windows"],
12
+ os: ["windows", "linux"],
13
13
  stores: {
14
14
  prefs: Store.global(
15
15
  Schema.Struct({
@@ -67,6 +67,16 @@ export const definition = Plugin.define({
67
67
  language: "",
68
68
  },
69
69
  ),
70
+ whispercpp: Store.global(
71
+ Schema.Struct({
72
+ model: Schema.String,
73
+ language: Schema.String,
74
+ }),
75
+ {
76
+ model: "base-q5_1",
77
+ language: "auto",
78
+ },
79
+ ),
70
80
  },
71
81
  i18n: {
72
82
  en: {
@@ -90,13 +100,14 @@ export const definition = Plugin.define({
90
100
  "error.unknown": "Dictation failed ({code}).",
91
101
  "toast.discarded": "Dictation discarded: the active session changed.",
92
102
  "settings.title": "Voice dictation",
93
- "settings.description": "Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.",
103
+ "settings.description": "Offline dictation on Windows and Linux. The text is inserted into the prompt; it is never sent automatically.",
94
104
  "settings.section": "Dictation",
95
105
  "settings.engine": "Dictation engine",
96
- "settings.engine.description": "Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.",
106
+ "settings.engine.description": "Windows offers Win+H and System.Speech; Linux uses whisper.cpp locally.",
97
107
  "engine.windows": "Windows Voice Typing (Win+H)",
98
108
  "engine.system": "System.Speech (offline)",
99
109
  "engine.whisper": "WhisperX local (WSL)",
110
+ "engine.whispercpp": "whisper.cpp (local, native)",
100
111
  "pill.recording": "Recording…",
101
112
  "pill.transcribing": "Transcribing…",
102
113
  "pill.whisper": "Record and transcribe with WhisperX",
@@ -115,6 +126,29 @@ export const definition = Plugin.define({
115
126
  "whisper.test.ready": "WhisperX ready: {model} ({device})",
116
127
  "whisper.test.error": "WhisperX is not ready: {error}",
117
128
  "whisper.empty": "WhisperX returned no text.",
129
+ "pill.whispercpp": "Record and transcribe with whisper.cpp",
130
+ "error.whispercpp": "whisper.cpp could not transcribe the audio.",
131
+ "whispercpp.section": "whisper.cpp (local, native)",
132
+ "whispercpp.description": "Runs on the CPU without WSL or Python. The first time you need to download the server (~10 MB) and the model.",
133
+ "whispercpp.model": "Model",
134
+ "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.",
135
+ "whispercpp.language": "Language",
136
+ "whispercpp.language.description": "Automatic detects the language of each recording.",
137
+ "whispercpp.language.auto": "Automatic",
138
+ "whispercpp.install": "Download engine",
139
+ "whispercpp.install.description": "Downloads the official whisper.cpp server and the selected model to ~/.local/share/opencode-voice-dictation. Needs internet once.",
140
+ "whispercpp.install.run": "Download",
141
+ "whispercpp.install.busy": "Downloading…",
142
+ "whispercpp.install.ok": "whisper.cpp installed: {model}",
143
+ "whispercpp.install.error": "Could not install whisper.cpp: {error}",
144
+ "whispercpp.test": "Check whisper.cpp",
145
+ "whispercpp.test.description": "Reports whether the server and the selected model are installed.",
146
+ "whispercpp.test.run": "Check",
147
+ "whispercpp.test.ready": "whisper.cpp ready: {model}",
148
+ "whispercpp.test.missing": "whisper.cpp is not installed. Download the engine first.",
149
+ "whispercpp.test.error": "whisper.cpp is not ready: {error}",
150
+ "whispercpp.empty": "whisper.cpp returned no text.",
151
+ "whispercpp.linux": "whisper.cpp is only available on Linux. On Windows use Win+H, System.Speech or WhisperX.",
118
152
  "settings.winh": "Test Win+H",
119
153
  "settings.winh.description": "Sends the Windows voice typing shortcut. If the floating bar appears, it works.",
120
154
  "settings.winh.run": "Test Win+H",
@@ -152,7 +186,7 @@ export const definition = Plugin.define({
152
186
  "read.noSession": "Open a conversation to read its last message.",
153
187
  "read.empty": "The last message has no text to read.",
154
188
  "read.error": "The last message could not be read.",
155
- "read.noVoices": "Windows has no voices installed for reading.",
189
+ "read.noVoices": "The system has no voices installed for reading.",
156
190
  "speech.section": "Read aloud",
157
191
  "speech.engine": "Voice engine",
158
192
  "speech.engine.description": "Edge neural sounds much more natural but needs internet. System uses the installed Windows voices, offline.",
@@ -196,13 +230,14 @@ export const definition = Plugin.define({
196
230
  "error.unknown": "El dictado falló ({code}).",
197
231
  "toast.discarded": "Dictado descartado: la sesión activa cambió.",
198
232
  "settings.title": "Dictado por voz",
199
- "settings.description": "Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.",
233
+ "settings.description": "Dictado offline en Windows y Linux. El texto se inserta en el prompt; nunca se envía solo.",
200
234
  "settings.section": "Dictado",
201
235
  "settings.engine": "Motor de dictado",
202
- "settings.engine.description": "Windows usa el dictado de Windows 11 (Win+H), más preciso. System.Speech funciona sin Windows 11 pero es menos exacto.",
236
+ "settings.engine.description": "Windows ofrece Win+H y System.Speech; Linux usa whisper.cpp en local.",
203
237
  "engine.windows": "Windows Voice Typing (Win+H)",
204
238
  "engine.system": "System.Speech (offline)",
205
239
  "engine.whisper": "WhisperX local (WSL)",
240
+ "engine.whispercpp": "whisper.cpp (local, nativo)",
206
241
  "pill.recording": "Grabando…",
207
242
  "pill.transcribing": "Transcribiendo…",
208
243
  "pill.whisper": "Grabar y transcribir con WhisperX",
@@ -221,6 +256,29 @@ export const definition = Plugin.define({
221
256
  "whisper.test.ready": "WhisperX listo: {model} ({device})",
222
257
  "whisper.test.error": "WhisperX no está listo: {error}",
223
258
  "whisper.empty": "WhisperX no devolvió texto.",
259
+ "pill.whispercpp": "Grabar y transcribir con whisper.cpp",
260
+ "error.whispercpp": "whisper.cpp no pudo transcribir el audio.",
261
+ "whispercpp.section": "whisper.cpp (local, nativo)",
262
+ "whispercpp.description": "Funciona en CPU sin WSL ni Python. La primera vez hay que descargar el servidor (~10 MB) y el modelo.",
263
+ "whispercpp.model": "Modelo",
264
+ "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.",
265
+ "whispercpp.language": "Idioma",
266
+ "whispercpp.language.description": "Automático detecta el idioma de cada grabación.",
267
+ "whispercpp.language.auto": "Automático",
268
+ "whispercpp.install": "Descargar motor",
269
+ "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.",
270
+ "whispercpp.install.run": "Descargar",
271
+ "whispercpp.install.busy": "Descargando…",
272
+ "whispercpp.install.ok": "whisper.cpp instalado: {model}",
273
+ "whispercpp.install.error": "No se pudo instalar whisper.cpp: {error}",
274
+ "whispercpp.test": "Comprobar whisper.cpp",
275
+ "whispercpp.test.description": "Indica si el servidor y el modelo elegido están instalados.",
276
+ "whispercpp.test.run": "Comprobar",
277
+ "whispercpp.test.ready": "whisper.cpp listo: {model}",
278
+ "whispercpp.test.missing": "whisper.cpp no está instalado. Descarga el motor primero.",
279
+ "whispercpp.test.error": "whisper.cpp no está listo: {error}",
280
+ "whispercpp.empty": "whisper.cpp no devolvió texto.",
281
+ "whispercpp.linux": "whisper.cpp solo está disponible en Linux. En Windows usa Win+H, System.Speech o WhisperX.",
224
282
  "settings.winh": "Probar Win+H",
225
283
  "settings.winh.description": "Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.",
226
284
  "settings.winh.run": "Probar Win+H",
@@ -257,10 +315,10 @@ export const definition = Plugin.define({
257
315
  "read.noSession": "Abre una conversación para leer su último mensaje.",
258
316
  "read.empty": "El último mensaje no tiene texto para leer.",
259
317
  "read.error": "No se pudo leer el último mensaje.",
260
- "read.noVoices": "Windows no tiene voces instaladas para la lectura.",
318
+ "read.noVoices": "El sistema no tiene voces instaladas para la lectura.",
261
319
  "speech.section": "Lectura en voz alta",
262
320
  "speech.engine": "Motor de voz",
263
- "speech.engine.description": "Edge neuronal suena mucho más natural pero necesita internet. Sistema usa las voces instaladas de Windows, sin conexión.",
321
+ "speech.engine.description": "Edge neuronal suena mucho más natural pero necesita internet. Sistema usa las voces instaladas del equipo, sin conexión.",
264
322
  "speech.engine.edge": "Edge neuronal (online)",
265
323
  "speech.engine.system": "Sistema (offline)",
266
324
  "error.edge": "No se pudo usar la voz de Edge. Revisa la conexión o cambia a la voz del sistema.",
@@ -31,24 +31,57 @@ export function VoiceSettings(props: { speaker: Speaker }) {
31
31
  const ttsPrefs = ctx.stores.tts
32
32
  const enginePrefs = ctx.stores.engine
33
33
  const whisperPrefs = ctx.stores.whisper
34
+ const whisperCppPrefs = ctx.stores.whispercpp
34
35
  const dictation = ctx.main(Dictation)
35
36
  const voiceTyping = ctx.main(VoiceTyping)
36
37
 
37
38
  const [locales, setLocales] = createSignal<readonly string[]>([])
38
39
  const [busy, setBusy] = createSignal(false)
39
40
  const [whisperBusy, setWhisperBusy] = createSignal(false)
41
+ const [whisperCppBusy, setWhisperCppBusy] = createSignal(false)
42
+ const [whisperCppCheckBusy, setWhisperCppCheckBusy] = createSignal(false)
43
+ const [platform, setPlatform] = createSignal("")
40
44
 
41
45
  onMount(() => {
42
46
  props.speaker.setConfig({ engine: ttsPrefs.value.engine, voice: ttsPrefs.value.voice })
43
- void dictation
44
- .diagnose()
45
- .then((result) => {
46
- if (result.locales.length > 0) setLocales(result.locales)
47
+
48
+ const loadLocales = () => {
49
+ void dictation
50
+ .diagnose()
51
+ .then((result) => {
52
+ if (result.locales.length > 0) setLocales(result.locales)
53
+ })
54
+ .catch(() => {})
55
+ }
56
+
57
+ void ctx.client
58
+ .rpc(VoiceRpc)
59
+ .capabilities({})
60
+ .then((value) => {
61
+ const caps = value as { platform: string; engines: string[] }
62
+ setPlatform(caps.platform)
63
+ if (!caps.engines.includes(enginePrefs.value.mode) && caps.engines.length > 0) {
64
+ patchEngine(caps.engines[0] ?? "windows")
65
+ }
66
+ if (caps.platform !== "linux") loadLocales()
47
67
  })
48
- .catch(() => {})
68
+ .catch(() => loadLocales())
69
+
49
70
  void props.speaker.refreshVoices()
50
71
  })
51
72
 
73
+ const engineOptions = () => {
74
+ if (platform() === "linux") {
75
+ return [{ value: "whispercpp", label: ctx.t("engine.whispercpp") }]
76
+ }
77
+
78
+ return [
79
+ { value: "windows", label: ctx.t("engine.windows") },
80
+ { value: "system", label: ctx.t("engine.system") },
81
+ { value: "whisper", label: ctx.t("engine.whisper") },
82
+ ]
83
+ }
84
+
52
85
  const localeOptions = () => {
53
86
  const list = locales()
54
87
  const current = prefs.value.locale
@@ -107,6 +140,88 @@ export function VoiceSettings(props: { speaker: Speaker }) {
107
140
  })
108
141
  }
109
142
 
143
+ const patchWhisperCpp = (change: { model?: string; language?: string }) => {
144
+ whisperCppPrefs.update((draft) => {
145
+ if (change.model !== undefined) draft.model = change.model
146
+ if (change.language !== undefined) draft.language = change.language
147
+ })
148
+ }
149
+
150
+ const installWhisperCpp = async () => {
151
+ setWhisperCppBusy(true)
152
+ try {
153
+ const result = (await ctx.client.rpc(VoiceRpc).whisperCppInstall({
154
+ model: whisperCppPrefs.value.model,
155
+ })) as { ok: boolean; model: string; error: string }
156
+
157
+ if (result.ok) {
158
+ ctx.toast.show({
159
+ variant: "success",
160
+ title: ctx.t("pill.title"),
161
+ description: ctx.t("whispercpp.install.ok", { model: result.model }),
162
+ })
163
+ } else {
164
+ ctx.toast.show({
165
+ variant: "error",
166
+ title: ctx.t("pill.title"),
167
+ description: ctx.t("whispercpp.install.error", { error: result.error || "?" }),
168
+ persistent: true,
169
+ })
170
+ }
171
+ } catch (cause) {
172
+ ctx.toast.show({
173
+ variant: "error",
174
+ title: ctx.t("pill.title"),
175
+ description: ctx.t("whispercpp.install.error", {
176
+ error: cause instanceof Error ? cause.message : String(cause),
177
+ }),
178
+ persistent: true,
179
+ })
180
+ } finally {
181
+ setWhisperCppBusy(false)
182
+ }
183
+ }
184
+
185
+ const checkWhisperCpp = async () => {
186
+ setWhisperCppCheckBusy(true)
187
+ try {
188
+ const status = (await ctx.client.rpc(VoiceRpc).whisperCppStatus({
189
+ model: whisperCppPrefs.value.model,
190
+ })) as { installed: boolean; ready: boolean; model: string; error: string }
191
+
192
+ if (status.error) {
193
+ ctx.toast.show({
194
+ variant: "error",
195
+ title: ctx.t("pill.title"),
196
+ description: ctx.t("whispercpp.test.error", { error: status.error }),
197
+ persistent: true,
198
+ })
199
+ } else if (!status.installed) {
200
+ ctx.toast.show({
201
+ variant: "default",
202
+ title: ctx.t("pill.title"),
203
+ description: ctx.t("whispercpp.test.missing"),
204
+ })
205
+ } else {
206
+ ctx.toast.show({
207
+ variant: "success",
208
+ title: ctx.t("pill.title"),
209
+ description: ctx.t("whispercpp.test.ready", { model: status.model }),
210
+ })
211
+ }
212
+ } catch (cause) {
213
+ ctx.toast.show({
214
+ variant: "error",
215
+ title: ctx.t("pill.title"),
216
+ description: ctx.t("whispercpp.test.error", {
217
+ error: cause instanceof Error ? cause.message : String(cause),
218
+ }),
219
+ })
220
+ } finally {
221
+ setWhisperCppCheckBusy(false)
222
+ }
223
+ }
224
+
110
225
  const testWhisper = async () => {
111
226
  setWhisperBusy(true)
112
227
  try {
@@ -232,11 +347,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
232
347
  <ui.Select
233
348
  label={ctx.t("settings.engine")}
234
349
  value={enginePrefs.value.mode}
235
- options={[
236
- { value: "windows", label: ctx.t("engine.windows") },
237
- { value: "system", label: ctx.t("engine.system") },
238
- { value: "whisper", label: ctx.t("engine.whisper") },
239
- ]}
350
+ options={engineOptions()}
240
351
  onChange={(value) => patchEngine(value)}
241
352
  />
242
353
  </ui.SettingsRow>
@@ -310,6 +421,62 @@ export function VoiceSettings(props: { speaker: Speaker }) {
310
421
  </ui.SettingsRow>
311
422
  </Show>
312
423
 
424
+ <Show when={enginePrefs.value.mode === "whispercpp"}>
425
+ <ui.SettingsRow
426
+ id="whispercpp-model"
427
+ title={ctx.t("whispercpp.model")}
428
+ description={ctx.t("whispercpp.model.description")}
429
+ >
430
+ <ui.Select
431
+ label={ctx.t("whispercpp.model")}
432
+ value={whisperCppPrefs.value.model}
433
+ options={[
434
+ { value: "base-q5_1", label: "base-q5_1 · 57 MB" },
435
+ { value: "small-q5_1", label: "small-q5_1 · 180 MB" },
436
+ ]}
437
+ onChange={(value) => patchWhisperCpp({ model: value })}
438
+ />
439
+ </ui.SettingsRow>
440
+
441
+ <ui.SettingsRow
442
+ id="whispercpp-language"
443
+ title={ctx.t("whispercpp.language")}
444
+ description={ctx.t("whispercpp.language.description")}
445
+ >
446
+ <ui.Select
447
+ label={ctx.t("whispercpp.language")}
448
+ value={whisperCppPrefs.value.language}
449
+ options={[
450
+ { value: "auto", label: ctx.t("whispercpp.language.auto") },
451
+ { value: "es", label: "Español" },
452
+ { value: "en", label: "English" },
453
+ { value: "pt", label: "Português" },
454
+ ]}
455
+ onChange={(value) => patchWhisperCpp({ language: value })}
456
+ />
457
+ </ui.SettingsRow>
458
+
459
+ <ui.SettingsRow
460
+ id="whispercpp-install"
461
+ title={ctx.t("whispercpp.install")}
462
+ description={ctx.t("whispercpp.install.description")}
463
+ >
464
+ <ui.Button busy={whisperCppBusy()} onClick={() => void installWhisperCpp()}>
465
+ {ctx.t("whispercpp.install.run")}
466
+ </ui.Button>
467
+ </ui.SettingsRow>
468
+
469
+ <ui.SettingsRow
470
+ id="whispercpp-test"
471
+ title={ctx.t("whispercpp.test")}
472
+ description={ctx.t("whispercpp.test.description")}
473
+ >
474
+ <ui.Button busy={whisperCppCheckBusy()} onClick={() => void checkWhisperCpp()}>
475
+ {ctx.t("whispercpp.test.run")}
476
+ </ui.Button>
477
+ </ui.SettingsRow>
478
+ </Show>
479
+
313
480
  <Show when={enginePrefs.value.mode === "system"}>
314
481
  <ui.SettingsRow
315
482
  id="language"
@@ -439,13 +606,15 @@ export function VoiceSettings(props: { speaker: Speaker }) {
439
606
  </ui.SettingsRow>
440
607
  </ui.SettingsSection>
441
608
 
442
- <ui.SettingsSection title={ctx.t("settings.test.section")}>
443
- <ui.SettingsRow id="test" title={ctx.t("settings.test")} description={ctx.t("settings.test.description")}>
444
- <ui.Button busy={busy()} onClick={() => void test()}>
445
- {ctx.t("settings.test.run")}
446
- </ui.Button>
447
- </ui.SettingsRow>
448
- </ui.SettingsSection>
609
+ <Show when={platform() !== "linux"}>
610
+ <ui.SettingsSection title={ctx.t("settings.test.section")}>
611
+ <ui.SettingsRow id="test" title={ctx.t("settings.test")} description={ctx.t("settings.test.description")}>
612
+ <ui.Button busy={busy()} onClick={() => void test()}>
613
+ {ctx.t("settings.test.run")}
614
+ </ui.Button>
615
+ </ui.SettingsRow>
616
+ </ui.SettingsSection>
617
+ </Show>
449
618
  </ui.SettingsLayout>
450
619
  )
451
620
  }