opencode-voice-dictation 0.6.1 → 0.7.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 +144 -22
- package/dist/gui-experimental.cjs +8 -8
- package/docs/whispercpp.md +143 -0
- package/docs/whisperx.md +183 -0
- package/package.json +6 -3
- package/src/gui/definition.ts +69 -11
- package/src/gui/settings.tsx +186 -17
- package/src/gui/window.tsx +59 -20
- package/src/index.ts +34 -4
- package/src/rpc.ts +80 -0
- package/src/whispercpp/server.ts +277 -0
package/src/gui/settings.tsx
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 && !status.installed) {
|
|
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
|
-
<
|
|
443
|
-
<ui.
|
|
444
|
-
<ui.
|
|
445
|
-
{
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
}
|
package/src/gui/window.tsx
CHANGED
|
@@ -56,9 +56,28 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
56
56
|
const prefs = ctx.stores.prefs
|
|
57
57
|
const engine = ctx.stores.engine
|
|
58
58
|
const whisper = ctx.stores.whisper
|
|
59
|
+
const whisperCpp = ctx.stores.whispercpp
|
|
59
60
|
const [windowsActive, setWindowsActive] = createSignal(false)
|
|
60
61
|
const [whisperState, setWhisperState] = createSignal<"idle" | "recording" | "transcribing">("idle")
|
|
61
62
|
|
|
63
|
+
// The server owns the OS: it decides which engines exist here. On Linux this
|
|
64
|
+
// switches the default to whisper.cpp and drops the Windows-only engines.
|
|
65
|
+
void ctx.client
|
|
66
|
+
.rpc(VoiceRpc)
|
|
67
|
+
.capabilities({})
|
|
68
|
+
.then((value) => {
|
|
69
|
+
const caps = value as { platform: string; engines: string[] }
|
|
70
|
+
if (!caps.engines.includes(engine.value.mode) && caps.engines.length > 0) {
|
|
71
|
+
engine.update((draft) => {
|
|
72
|
+
draft.mode = caps.engines[0] ?? "windows"
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {})
|
|
77
|
+
|
|
78
|
+
const localMode = () =>
|
|
79
|
+
engine.value.mode === "whisper" || engine.value.mode === "whispercpp" ? engine.value.mode : undefined
|
|
80
|
+
|
|
62
81
|
let targetSession: string | undefined
|
|
63
82
|
let recording: Recording | undefined
|
|
64
83
|
|
|
@@ -122,7 +141,9 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
122
141
|
})
|
|
123
142
|
}
|
|
124
143
|
|
|
125
|
-
const
|
|
144
|
+
const localError = () => ctx.t(localMode() === "whispercpp" ? "error.whispercpp" : "error.whisper")
|
|
145
|
+
|
|
146
|
+
const startLocal = async () => {
|
|
126
147
|
if (whisperState() !== "idle") return
|
|
127
148
|
speaker.cancel()
|
|
128
149
|
|
|
@@ -133,19 +154,19 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
133
154
|
ctx.toast.show({
|
|
134
155
|
variant: "error",
|
|
135
156
|
title: ctx.t("pill.title"),
|
|
136
|
-
description: cause instanceof Error ? cause.message :
|
|
157
|
+
description: cause instanceof Error ? cause.message : localError(),
|
|
137
158
|
})
|
|
138
159
|
}
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
const
|
|
162
|
+
const cancelLocal = () => {
|
|
142
163
|
const current = recording
|
|
143
164
|
recording = undefined
|
|
144
165
|
if (current) void current.stop().catch(() => {})
|
|
145
166
|
setWhisperState("idle")
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
const
|
|
169
|
+
const stopLocal = async () => {
|
|
149
170
|
const current = recording
|
|
150
171
|
recording = undefined
|
|
151
172
|
|
|
@@ -155,22 +176,34 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
setWhisperState("transcribing")
|
|
179
|
+
const mode = localMode()
|
|
158
180
|
|
|
159
181
|
try {
|
|
160
182
|
const blob = await current.stop()
|
|
161
183
|
const audio = await blobToBase64(blob)
|
|
162
|
-
const result = (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
const result = (mode === "whispercpp"
|
|
185
|
+
? await ctx.client.rpc(VoiceRpc).whisperCppTranscribe({
|
|
186
|
+
audio,
|
|
187
|
+
mime: blob.type,
|
|
188
|
+
language: whisperCpp.value.language,
|
|
189
|
+
model: whisperCpp.value.model,
|
|
190
|
+
})
|
|
191
|
+
: await ctx.client.rpc(VoiceRpc).whisperTranscribe({
|
|
192
|
+
audio,
|
|
193
|
+
mime: blob.type,
|
|
194
|
+
language: whisper.value.language,
|
|
195
|
+
python: whisper.value.python,
|
|
196
|
+
model: whisper.value.model,
|
|
197
|
+
})) as { text: string; language: string }
|
|
169
198
|
|
|
170
199
|
const text = result.text.trim()
|
|
171
200
|
|
|
172
201
|
if (!text) {
|
|
173
|
-
ctx.toast.show({
|
|
202
|
+
ctx.toast.show({
|
|
203
|
+
variant: "default",
|
|
204
|
+
title: ctx.t("pill.title"),
|
|
205
|
+
description: ctx.t(mode === "whispercpp" ? "whispercpp.empty" : "whisper.empty"),
|
|
206
|
+
})
|
|
174
207
|
return
|
|
175
208
|
}
|
|
176
209
|
|
|
@@ -185,7 +218,7 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
185
218
|
ctx.toast.show({
|
|
186
219
|
variant: "error",
|
|
187
220
|
title: ctx.t("pill.title"),
|
|
188
|
-
description: cause instanceof Error ? cause.message :
|
|
221
|
+
description: cause instanceof Error ? cause.message : localError(),
|
|
189
222
|
})
|
|
190
223
|
} finally {
|
|
191
224
|
setWhisperState("idle")
|
|
@@ -193,9 +226,9 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
const toggle = () => {
|
|
196
|
-
if (
|
|
197
|
-
if (whisperState() === "recording") void
|
|
198
|
-
else if (whisperState() === "idle") void
|
|
229
|
+
if (localMode()) {
|
|
230
|
+
if (whisperState() === "recording") void stopLocal()
|
|
231
|
+
else if (whisperState() === "idle") void startLocal()
|
|
199
232
|
return
|
|
200
233
|
}
|
|
201
234
|
|
|
@@ -210,7 +243,7 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
210
243
|
}
|
|
211
244
|
|
|
212
245
|
const read = () => {
|
|
213
|
-
if (whisperState() === "recording")
|
|
246
|
+
if (whisperState() === "recording") cancelLocal()
|
|
214
247
|
|
|
215
248
|
if (speaker.speaking()) {
|
|
216
249
|
speaker.cancel()
|
|
@@ -318,7 +351,9 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
|
|
321
|
-
if (engine.value.mode === "whisper") {
|
|
354
|
+
if (engine.value.mode === "whisper" || engine.value.mode === "whispercpp") {
|
|
355
|
+
const title = ctx.t(engine.value.mode === "whispercpp" ? "pill.whispercpp" : "pill.whisper")
|
|
356
|
+
|
|
322
357
|
if (whisperState() === "recording") {
|
|
323
358
|
return {
|
|
324
359
|
label: ctx.t("pill.recording"),
|
|
@@ -333,14 +368,14 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
333
368
|
return {
|
|
334
369
|
label: ctx.t("pill.transcribing"),
|
|
335
370
|
icon: "outline-audio",
|
|
336
|
-
title
|
|
371
|
+
title,
|
|
337
372
|
}
|
|
338
373
|
}
|
|
339
374
|
|
|
340
375
|
return {
|
|
341
376
|
label: ctx.t("pill.idle"),
|
|
342
377
|
icon: "outline-wave-audio",
|
|
343
|
-
title
|
|
378
|
+
title,
|
|
344
379
|
run: toggle,
|
|
345
380
|
}
|
|
346
381
|
}
|
|
@@ -464,6 +499,10 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
|
|
|
464
499
|
{ id: "whisper-model", title: ctx.t("whisper.model"), keywords: "whisper model modelo" },
|
|
465
500
|
{ id: "whisper-language", title: ctx.t("whisper.language"), keywords: "whisper language idioma" },
|
|
466
501
|
{ id: "whisper-test", title: ctx.t("whisper.test"), keywords: "whisper check comprobar" },
|
|
502
|
+
{ id: "whispercpp-model", title: ctx.t("whispercpp.model"), keywords: "whisper.cpp model modelo" },
|
|
503
|
+
{ id: "whispercpp-language", title: ctx.t("whispercpp.language"), keywords: "whisper.cpp language idioma" },
|
|
504
|
+
{ id: "whispercpp-install", title: ctx.t("whispercpp.install"), keywords: "whisper.cpp download descargar instalar" },
|
|
505
|
+
{ id: "whispercpp-test", title: ctx.t("whispercpp.test"), keywords: "whisper.cpp check comprobar" },
|
|
467
506
|
{ id: "rate", title: ctx.t("speech.rate"), keywords: "read aloud speed velocidad lectura" },
|
|
468
507
|
{ id: "skip-code", title: ctx.t("speech.skipCode"), keywords: "read aloud code codigo lectura" },
|
|
469
508
|
{ id: "preview", title: ctx.t("speech.preview"), keywords: "read aloud test voice probar voz" },
|
package/src/index.ts
CHANGED
|
@@ -2,20 +2,31 @@ 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
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Server part of the plugin.
|
|
8
9
|
*
|
|
9
|
-
* It registers the Edge TTS and
|
|
10
|
-
* in the Electron main process) lets their dependencies resolve
|
|
11
|
-
* plugin's own node_modules.
|
|
10
|
+
* It registers the Edge TTS, WhisperX and whisper.cpp RPCs. Running them here
|
|
11
|
+
* (instead of in the Electron main process) lets their dependencies resolve
|
|
12
|
+
* from the plugin's own node_modules.
|
|
12
13
|
*/
|
|
13
14
|
export default Plugin.define({
|
|
14
15
|
id: "voice-dictation",
|
|
15
16
|
async setup(ctx) {
|
|
16
17
|
const whisper = new WhisperEngine()
|
|
18
|
+
const whisperCpp = new WhisperCppEngine()
|
|
17
19
|
|
|
18
20
|
await ctx.rpc.register(VoiceRpc, {
|
|
21
|
+
capabilities: async () => ({
|
|
22
|
+
platform: process.platform,
|
|
23
|
+
engines:
|
|
24
|
+
process.platform === "linux"
|
|
25
|
+
? ["whispercpp"]
|
|
26
|
+
: process.platform === "win32"
|
|
27
|
+
? ["windows", "system", "whisper"]
|
|
28
|
+
: [],
|
|
29
|
+
}),
|
|
19
30
|
edgeVoices: async () => listEdgeVoices(),
|
|
20
31
|
edgeSpeak: async (input) => {
|
|
21
32
|
const { text, voice, rate } = input as { text: string; voice: string; rate: number }
|
|
@@ -42,9 +53,28 @@ export default Plugin.define({
|
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
},
|
|
56
|
+
whisperCppInstall: async (input) => {
|
|
57
|
+
const { model } = input as { model: string }
|
|
58
|
+
return installWhisperCpp(model)
|
|
59
|
+
},
|
|
60
|
+
whisperCppStatus: async (input) => {
|
|
61
|
+
const { model } = input as { model: string }
|
|
62
|
+
return whisperCpp.status(model)
|
|
63
|
+
},
|
|
64
|
+
whisperCppTranscribe: async (input) => {
|
|
65
|
+
return whisperCpp.transcribe(input as {
|
|
66
|
+
audio: string
|
|
67
|
+
mime: string
|
|
68
|
+
language: string
|
|
69
|
+
model: string
|
|
70
|
+
})
|
|
71
|
+
},
|
|
45
72
|
})
|
|
46
73
|
|
|
47
|
-
return () =>
|
|
74
|
+
return () => {
|
|
75
|
+
whisper.stop()
|
|
76
|
+
whisperCpp.stop()
|
|
77
|
+
}
|
|
48
78
|
},
|
|
49
79
|
})
|
|
50
80
|
|
package/src/rpc.ts
CHANGED
|
@@ -85,6 +85,86 @@ export const VoiceRpc = {
|
|
|
85
85
|
additionalProperties: false,
|
|
86
86
|
},
|
|
87
87
|
},
|
|
88
|
+
/**
|
|
89
|
+
* Tells the window which OS the server runs on and which dictation engines
|
|
90
|
+
* exist there. The window uses it to pick the default engine and to hide
|
|
91
|
+
* engines that do not apply (Win+H on Linux, whisper.cpp on Windows).
|
|
92
|
+
*/
|
|
93
|
+
capabilities: {
|
|
94
|
+
input: { type: "object", additionalProperties: false },
|
|
95
|
+
output: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
platform: { type: "string" },
|
|
99
|
+
engines: { type: "array", items: { type: "string" } },
|
|
100
|
+
},
|
|
101
|
+
required: ["platform", "engines"],
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
whisperCppInstall: {
|
|
106
|
+
input: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
model: { type: "string" },
|
|
110
|
+
},
|
|
111
|
+
required: ["model"],
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
},
|
|
114
|
+
output: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
ok: { type: "boolean" },
|
|
118
|
+
model: { type: "string" },
|
|
119
|
+
error: { type: "string" },
|
|
120
|
+
},
|
|
121
|
+
required: ["ok", "model", "error"],
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
whisperCppStatus: {
|
|
126
|
+
input: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
model: { type: "string" },
|
|
130
|
+
},
|
|
131
|
+
required: ["model"],
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
},
|
|
134
|
+
output: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
installed: { type: "boolean" },
|
|
138
|
+
ready: { type: "boolean" },
|
|
139
|
+
model: { type: "string" },
|
|
140
|
+
error: { type: "string" },
|
|
141
|
+
},
|
|
142
|
+
required: ["installed", "ready", "model", "error"],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
whisperCppTranscribe: {
|
|
147
|
+
input: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
audio: { type: "string" },
|
|
151
|
+
mime: { type: "string" },
|
|
152
|
+
language: { type: "string" },
|
|
153
|
+
model: { type: "string" },
|
|
154
|
+
},
|
|
155
|
+
required: ["audio", "mime", "language", "model"],
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
158
|
+
output: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
text: { type: "string" },
|
|
162
|
+
language: { type: "string" },
|
|
163
|
+
},
|
|
164
|
+
required: ["text", "language"],
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
88
168
|
},
|
|
89
169
|
events: {},
|
|
90
170
|
} as const satisfies Rpc.PortableDefinition
|