@drakulavich/kesha-voice-kit 1.26.0 → 1.27.0-alpha.2
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 +2 -1
- package/SKILL.md +16 -5
- package/completions/kesha.bash +1 -1
- package/completions/kesha.fish +1 -0
- package/completions/kesha.zsh +2 -1
- package/man/kesha.1 +3 -0
- package/package.json +3 -2
- package/src/cli/init.ts +20 -18
- package/src/cli/install.ts +80 -23
- package/src/doctor.ts +62 -5
- package/src/engine-install.ts +67 -24
- package/src/fluid-asr-cache.ts +91 -0
- package/src/install-plan.ts +121 -19
- package/src/semver.d.mts +16 -0
- package/src/semver.mjs +87 -0
- package/src/star.ts +3 -6
- package/src/status.ts +36 -9
- package/src/__tests__/error-codes-drift.test.ts +0 -29
- package/src/__tests__/error-codes.test.ts +0 -25
- package/src/__tests__/stats-error-codes.test.ts +0 -14
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ curl -fsSL https://bun.sh/install | bash # or: brew install oven-sh/bun/b
|
|
|
36
36
|
bun add -g @drakulavich/kesha-voice-kit
|
|
37
37
|
kesha --version # confirms `kesha` resolved on PATH
|
|
38
38
|
kesha install --plan # preview exact download/disk sizes first — downloads nothing
|
|
39
|
-
kesha install # ~2.5 GB
|
|
39
|
+
kesha install # ~2.5 GB on Linux/Windows; ~0.6 GB on Apple Silicon, whose CoreML
|
|
40
|
+
# engine uses a different, smaller model set. Explicit — never automatic.
|
|
40
41
|
# No progress bar during the model step; can take several minutes.
|
|
41
42
|
# Prefer a guided wizard? `kesha init` walks through the same choices interactively.
|
|
42
43
|
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kesha-voice-kit
|
|
3
|
-
description: Local multilingual voice toolkit — speech-to-text (STT), text-to-speech (TTS), and language detection. Runs entirely offline on Apple Silicon, Linux, and Windows. No API keys, no cloud. NVIDIA Parakeet TDT for STT across 25 European languages, Kokoro-82M + Vosk-TTS for TTS, plus macOS AVSpeechSynthesizer for ~180 system voices with zero install.
|
|
3
|
+
description: Local multilingual voice toolkit — speech-to-text (STT), text-to-speech (TTS), speaker diarization, and language detection, over a CLI or an MCP server. Runs entirely offline on Apple Silicon, Linux, and Windows. No API keys, no cloud. NVIDIA Parakeet TDT for STT across 25 European languages, Kokoro-82M + Vosk-TTS for TTS in 9 languages, plus macOS AVSpeechSynthesizer for ~180 system voices with zero install.
|
|
4
4
|
emoji: 🎙️
|
|
5
5
|
|
|
6
6
|
requires:
|
|
@@ -17,7 +17,7 @@ install:
|
|
|
17
17
|
|
|
18
18
|
Local voice toolkit: transcribe voice messages to text, synthesize speech, detect language of audio or text. Fully offline after `kesha install`. No API keys, no per-minute billing.
|
|
19
19
|
|
|
20
|
-
**Trigger keywords for when to use this skill:** voice message, voice memo, voice note, .ogg, .opus, .wav, .mp3, audio file, transcribe, transcription, speech-to-text, STT, text-to-speech, TTS, synthesize speech, say, telegram voice note, whatsapp voice note, ogg-opus, opus, multilingual voice, multilingual ASR, language detection, offline voice, privacy, Apple Silicon, CoreML.
|
|
20
|
+
**Trigger keywords for when to use this skill:** voice message, voice memo, voice note, .ogg, .opus, .wav, .mp3, audio file, transcribe, transcription, speech-to-text, STT, text-to-speech, TTS, synthesize speech, say, telegram voice note, whatsapp voice note, ogg-opus, opus, multilingual voice, multilingual ASR, language detection, speaker diarization, who said what, meeting transcript, MCP server, offline voice, privacy, Apple Silicon, CoreML.
|
|
21
21
|
|
|
22
22
|
## When to use
|
|
23
23
|
|
|
@@ -107,6 +107,16 @@ When invoking Kesha manually from an OpenClaw flow, write OGG/Opus into an OpenC
|
|
|
107
107
|
|
|
108
108
|
Do not configure OpenClaw Telegram TTS as `kesha say "<text>" > reply.wav`; that creates a WAV file and will not render as a native Telegram voice note.
|
|
109
109
|
|
|
110
|
+
## MCP server
|
|
111
|
+
|
|
112
|
+
`kesha mcp` serves the same capabilities over Model Context Protocol (stdio), exposing `transcribe_audio`, `synthesize_speech`, `list_voices`, and `list_languages` to any MCP client:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "mcpServers": { "kesha": { "command": "kesha", "args": ["mcp"] } } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Models are never auto-downloaded — tools fail with a `kesha install` / `kesha install --tts` hint when missing. Full client setup (Claude Code, Claude Desktop, Codex, Cursor): [docs/mcp.md](docs/mcp.md).
|
|
119
|
+
|
|
110
120
|
## STT: transcribe audio
|
|
111
121
|
|
|
112
122
|
```bash
|
|
@@ -207,8 +217,9 @@ bun add -g @drakulavich/kesha-voice-kit # global CLI install (always fi
|
|
|
207
217
|
kesha init
|
|
208
218
|
|
|
209
219
|
# For agents and CI: explicit, scriptable install commands
|
|
210
|
-
kesha install # engine only
|
|
211
|
-
kesha install --
|
|
220
|
+
kesha install # engine only — ~0.6 GB on Apple Silicon, ~2.5 GB on Linux/Windows
|
|
221
|
+
kesha install --plan # preview exact download/disk sizes first — downloads nothing
|
|
222
|
+
kesha install --tts # + English Kokoro; add langs additively: --tts en ru es
|
|
212
223
|
kesha install --tts --vad # + Silero VAD (long-audio chunking)
|
|
213
224
|
kesha install --tts --vad --diarize # + speaker diarization (darwin-arm64 only)
|
|
214
225
|
```
|
|
@@ -223,7 +234,7 @@ No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled
|
|
|
223
234
|
|
|
224
235
|
**Speech-to-text (25):** Bulgarian, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Spanish, Swedish, Ukrainian.
|
|
225
236
|
|
|
226
|
-
**Text-to-speech:** English (Kokoro-82M; FluidAudio CoreML on darwin-arm64, ONNX elsewhere), Russian (Vosk-TTS, 5 baked-in speakers — default `ru-vosk-m02`), plus
|
|
237
|
+
**Text-to-speech (9):** English (Kokoro-82M; FluidAudio CoreML on darwin-arm64, ONNX elsewhere), Russian (Vosk-TTS, 5 baked-in speakers — default `ru-vosk-m02`), Spanish, French, Italian, Portuguese (Kokoro — CharsiuG2P on ONNX builds, FluidAudio G2P on darwin-arm64), plus Hindi, Japanese, Chinese (Kokoro, darwin-arm64 only). `hi`/`ja` accept romanized Latin input only; native Devanagari and kana/kanji are rejected with `E_SCRIPT_UNSUPPORTED` — see [#492](https://github.com/drakulavich/kesha-voice-kit/issues/492). Any macOS system voice is also available via `--voice macos-*`.
|
|
227
238
|
|
|
228
239
|
## Performance
|
|
229
240
|
|
package/completions/kesha.bash
CHANGED
|
@@ -21,7 +21,7 @@ _kesha_completion() {
|
|
|
21
21
|
completions) opts="--help -h" ;;
|
|
22
22
|
doctor) opts="--help -h --json --redact" ;;
|
|
23
23
|
init) opts="--help -h --coreml --onnx --no-cache --plan --yes --tts --vad --diarize" ;;
|
|
24
|
-
install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize" ;;
|
|
24
|
+
install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize --engine-version" ;;
|
|
25
25
|
logs) opts="--help -h --json" ;;
|
|
26
26
|
manpage) opts="--help -h" ;;
|
|
27
27
|
mcp) opts="--help -h" ;;
|
package/completions/kesha.fish
CHANGED
|
@@ -54,6 +54,7 @@ complete -c kesha -n '__fish_seen_subcommand_from install' -l plan -d 'Show down
|
|
|
54
54
|
complete -c kesha -n '__fish_seen_subcommand_from install' -l tts -d 'Also install TTS models (English only; add codes for more, e.g. --tts en ru (preview sizes: kesha install --plan))'
|
|
55
55
|
complete -c kesha -n '__fish_seen_subcommand_from install' -l vad -d 'Also install Silero VAD (~2.3MB) for long-audio preprocessing'
|
|
56
56
|
complete -c kesha -n '__fish_seen_subcommand_from install' -l diarize -d 'Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)'
|
|
57
|
+
complete -c kesha -n '__fish_seen_subcommand_from install' -l engine-version -r -d 'Install this exact engine release instead of the pinned one (expert; the next install returns to the pin)'
|
|
57
58
|
complete -c kesha -n '__fish_seen_subcommand_from logs' -l help -d 'Show help'
|
|
58
59
|
complete -c kesha -n '__fish_seen_subcommand_from logs' -s h -d 'Show help'
|
|
59
60
|
complete -c kesha -n '__fish_seen_subcommand_from logs' -l json -d 'Output status as JSON'
|
package/completions/kesha.zsh
CHANGED
|
@@ -75,7 +75,8 @@ _kesha() {
|
|
|
75
75
|
'--plan[Show download, disk, and warm-up plan without changing local state]' \
|
|
76
76
|
'--tts[Also install TTS models (English only; add codes for more, e.g. --tts en ru (preview sizes: kesha install --plan))]' \
|
|
77
77
|
'--vad[Also install Silero VAD (~2.3MB) for long-audio preprocessing]' \
|
|
78
|
-
'--diarize[Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)]'
|
|
78
|
+
'--diarize[Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)]' \
|
|
79
|
+
'--engine-version=[Install this exact engine release instead of the pinned one (expert; the next install returns to the pin)]:engine version:'
|
|
79
80
|
;;
|
|
80
81
|
logs)
|
|
81
82
|
_arguments '--help[Show help]' \
|
package/man/kesha.1
CHANGED
|
@@ -141,6 +141,9 @@ Also install Silero VAD (~2.3MB) for long\-audio preprocessing
|
|
|
141
141
|
.TP
|
|
142
142
|
.B \-\-diarize
|
|
143
143
|
Also install the Sortformer streaming\-diarization model (~245MB, darwin\-arm64 only — #199)
|
|
144
|
+
.TP
|
|
145
|
+
.B \-\-engine\-version
|
|
146
|
+
Install this exact engine release instead of the pinned one (expert; the next install returns to the pin)
|
|
144
147
|
.SS logs
|
|
145
148
|
.TP
|
|
146
149
|
.B \-\-json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0-alpha.2",
|
|
4
4
|
"description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"SKILL.md",
|
|
26
26
|
"LICENSE",
|
|
27
27
|
"NOTICES.md",
|
|
28
|
-
"README.md"
|
|
28
|
+
"README.md",
|
|
29
|
+
"!src/__tests__"
|
|
29
30
|
],
|
|
30
31
|
"openclaw": {
|
|
31
32
|
"extensions": [
|
package/src/cli/init.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { confirm, multiselect, isCancel, cancel } from "@clack/prompts";
|
|
|
3
3
|
import { renderInstallPlan } from "../install-plan";
|
|
4
4
|
import { log } from "../log";
|
|
5
5
|
import { getEngineCapabilities } from "../engine";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
performInstall,
|
|
8
|
+
resolveBackendFlag,
|
|
9
|
+
resolveNoCacheFlag,
|
|
10
|
+
TTS_LANG_FALLBACK,
|
|
11
|
+
unavailableBackendError,
|
|
12
|
+
} from "./install";
|
|
7
13
|
import type { SharedInstallArgs } from "./types";
|
|
8
14
|
|
|
9
15
|
const TTS_LANG_LABELS: Record<string, string> = {
|
|
@@ -171,7 +177,14 @@ export async function promptInitSelection(
|
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
|
|
180
|
+
/** Mirrors the guard in `performInstall`: a preview must not describe an install this platform rejects (#684). */
|
|
181
|
+
async function printPlan(selection: InitSelection): Promise<boolean> {
|
|
182
|
+
const backendError = unavailableBackendError(selection.backend);
|
|
183
|
+
if (backendError) {
|
|
184
|
+
log.error(backendError);
|
|
185
|
+
process.exitCode = 2;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
175
188
|
log.info(
|
|
176
189
|
await renderInstallPlan({
|
|
177
190
|
noCache: selection.noCache,
|
|
@@ -181,6 +194,7 @@ async function printPlan(selection: InitSelection): Promise<void> {
|
|
|
181
194
|
diarize: selection.diarize,
|
|
182
195
|
}),
|
|
183
196
|
);
|
|
197
|
+
return true;
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
async function runNonInteractive(selection: InitSelection): Promise<void> {
|
|
@@ -190,7 +204,7 @@ async function runNonInteractive(selection: InitSelection): Promise<void> {
|
|
|
190
204
|
log.warn("--diarize is currently darwin-arm64 only; omitting it from non-interactive examples.");
|
|
191
205
|
}
|
|
192
206
|
log.info(renderInitOverview(canDiarize));
|
|
193
|
-
await printPlan(printableSelection);
|
|
207
|
+
if (!(await printPlan(printableSelection))) return;
|
|
194
208
|
log.info("Run one of these commands from an interactive terminal:");
|
|
195
209
|
for (const command of initSuggestionCommands(printableSelection, canDiarize)) {
|
|
196
210
|
log.info(` ${command.join(" ")}`);
|
|
@@ -260,13 +274,7 @@ export const initCommand = defineCommand({
|
|
|
260
274
|
if (selection.diarize && !installSelection.diarize) {
|
|
261
275
|
log.warn("--diarize is currently darwin-arm64 only; omitting it from the --yes install.");
|
|
262
276
|
}
|
|
263
|
-
await performInstall(
|
|
264
|
-
installSelection.noCache,
|
|
265
|
-
installSelection.backend,
|
|
266
|
-
installSelection.ttsLangs,
|
|
267
|
-
installSelection.vad,
|
|
268
|
-
installSelection.diarize,
|
|
269
|
-
);
|
|
277
|
+
await performInstall(installSelection);
|
|
270
278
|
return;
|
|
271
279
|
}
|
|
272
280
|
|
|
@@ -286,7 +294,7 @@ export const initCommand = defineCommand({
|
|
|
286
294
|
noCache,
|
|
287
295
|
);
|
|
288
296
|
log.info("");
|
|
289
|
-
await printPlan(prompted);
|
|
297
|
+
if (!(await printPlan(prompted))) return;
|
|
290
298
|
const confirmed = await promptConfirm(
|
|
291
299
|
`Run \`${initInstallArgs(prompted).join(" ")}\` now?`,
|
|
292
300
|
true,
|
|
@@ -295,13 +303,7 @@ export const initCommand = defineCommand({
|
|
|
295
303
|
log.info(`Skipped install. Run later: ${initInstallArgs(prompted).join(" ")}`);
|
|
296
304
|
return;
|
|
297
305
|
}
|
|
298
|
-
await performInstall(
|
|
299
|
-
prompted.noCache,
|
|
300
|
-
prompted.backend,
|
|
301
|
-
prompted.ttsLangs,
|
|
302
|
-
prompted.vad,
|
|
303
|
-
prompted.diarize,
|
|
304
|
-
);
|
|
306
|
+
await performInstall(prompted);
|
|
305
307
|
// #523: end on something runnable, not on the install log.
|
|
306
308
|
log.info("");
|
|
307
309
|
log.success("Kesha is ready. Try:");
|
package/src/cli/install.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
|
-
import {
|
|
3
|
+
import { installEngine } from "../engine-install";
|
|
4
4
|
import { engineTarget } from "../engine-targets";
|
|
5
5
|
import { getEngineBinPath, getEngineCapabilities } from "../engine";
|
|
6
6
|
import { renderInstallPlan } from "../install-plan";
|
|
7
7
|
import { maybeAskForStar } from "../star";
|
|
8
8
|
import { log } from "../log";
|
|
9
9
|
import { packageVersion } from "../package-info";
|
|
10
|
+
import { isSemver } from "../semver.mjs";
|
|
10
11
|
import { createDiagnosticLogSession, type DiagnosticLogSession } from "../diagnostic-log";
|
|
11
12
|
import type { SharedInstallArgs } from "./types";
|
|
12
13
|
|
|
13
14
|
export interface InstallCommandArgs extends SharedInstallArgs {
|
|
14
15
|
/** Positional args after `install` — candidate TTS language codes. */
|
|
15
16
|
_?: string[];
|
|
17
|
+
/** `init` deliberately has no such flag: an override is an expert action, not a guided one. */
|
|
18
|
+
"engine-version"?: string;
|
|
19
|
+
engineVersion?: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -57,6 +61,23 @@ export function resolveTtsLangs(input: TtsArgInput, supported: string[] | undefi
|
|
|
57
61
|
return langs;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Validates `--engine-version` before any network call. Returns undefined when the flag is
|
|
66
|
+
* absent, so the Pinned Engine version stays the default; throws on anything the version
|
|
67
|
+
* drift gate would not accept either — one SemVer 2.0 definition across the repo.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveEngineVersionFlag(raw: unknown): string | undefined {
|
|
70
|
+
if (raw === undefined || raw === null || raw === false) return undefined;
|
|
71
|
+
const value = String(raw).trim();
|
|
72
|
+
if (isSemver(value)) return value;
|
|
73
|
+
const hint = /^v\d/.test(value)
|
|
74
|
+
? ' Drop the leading "v" — that belongs to the release tag, not the version.'
|
|
75
|
+
: "";
|
|
76
|
+
throw new Error(
|
|
77
|
+
`--engine-version needs an exact SemVer 2.0 version like 1.24.8 or 1.24.8-alpha.1, got "${value}".${hint}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
export function resolveNoCacheFlag(
|
|
61
82
|
args: Pick<InstallCommandArgs, "no-cache" | "noCache" | "no_cache">,
|
|
62
83
|
rawArgs: string[] = [],
|
|
@@ -120,16 +141,42 @@ function finishInstallDiagnostic(
|
|
|
120
141
|
}
|
|
121
142
|
}
|
|
122
143
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
/** Null when the requested backend is installable here. `KESHA_ENGINE_BIN` opts out — the user supplied their own engine. */
|
|
145
|
+
export function unavailableBackendError(backend: string | undefined): string | null {
|
|
146
|
+
const platformBackend = defaultBackendForPlatform();
|
|
147
|
+
if (!backend || process.env.KESHA_ENGINE_BIN || !platformBackend) return null;
|
|
148
|
+
if (backend === platformBackend) return null;
|
|
149
|
+
return (
|
|
150
|
+
`Requested backend "${backend}" is not available on this platform; ` +
|
|
151
|
+
`the release engine uses "${platformBackend}".`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PerformInstallOptions {
|
|
156
|
+
noCache: boolean;
|
|
157
|
+
backend?: string;
|
|
158
|
+
ttsLangs: string[];
|
|
159
|
+
vad?: boolean;
|
|
160
|
+
diarize?: boolean;
|
|
161
|
+
plan?: boolean;
|
|
162
|
+
/** Engine release to install instead of the pin, for this invocation only. */
|
|
163
|
+
engineVersion?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function performInstall(options: PerformInstallOptions) {
|
|
167
|
+
const { noCache, backend, ttsLangs, vad = false, diarize = false, plan = false, engineVersion } =
|
|
168
|
+
options;
|
|
169
|
+
// A plan for an unavailable backend previews what its own printed command rejects (#684).
|
|
170
|
+
const backendError = unavailableBackendError(backend);
|
|
131
171
|
if (plan) {
|
|
132
|
-
|
|
172
|
+
if (backendError) {
|
|
173
|
+
log.error(backendError);
|
|
174
|
+
process.exitCode = 2;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
log.info(
|
|
178
|
+
await renderInstallPlan({ noCache, backend, ttsLangs, vad, diarize, engineVersion }),
|
|
179
|
+
);
|
|
133
180
|
return;
|
|
134
181
|
}
|
|
135
182
|
|
|
@@ -145,6 +192,7 @@ export async function performInstall(
|
|
|
145
192
|
tts: ttsLangs.length > 0,
|
|
146
193
|
vad,
|
|
147
194
|
diarize,
|
|
195
|
+
engineVersionOverride: engineVersion !== undefined,
|
|
148
196
|
});
|
|
149
197
|
|
|
150
198
|
if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
|
|
@@ -154,15 +202,11 @@ export async function performInstall(
|
|
|
154
202
|
"(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
|
|
155
203
|
);
|
|
156
204
|
}
|
|
157
|
-
|
|
158
|
-
if (backend && !process.env.KESHA_ENGINE_BIN && platformBackend && backend !== platformBackend) {
|
|
205
|
+
if (backendError) {
|
|
159
206
|
errorKind = "validation_failed";
|
|
160
|
-
throw new Error(
|
|
161
|
-
`Requested backend "${backend}" is not available on this platform; ` +
|
|
162
|
-
`the release engine uses "${platformBackend}".`,
|
|
163
|
-
);
|
|
207
|
+
throw new Error(backendError);
|
|
164
208
|
}
|
|
165
|
-
await
|
|
209
|
+
await installEngine({ noCache, backend, ttsLangs, vad, diarize, version: engineVersion });
|
|
166
210
|
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
167
211
|
finishInstallDiagnostic(diagnosticLog, startedAt, "success");
|
|
168
212
|
} catch (err: unknown) {
|
|
@@ -214,8 +258,20 @@ export const installCommand = defineCommand({
|
|
|
214
258
|
description: "Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)",
|
|
215
259
|
default: false,
|
|
216
260
|
},
|
|
261
|
+
"engine-version": {
|
|
262
|
+
type: "string",
|
|
263
|
+
description:
|
|
264
|
+
"Install this exact engine release instead of the pinned one (expert; the next install returns to the pin)",
|
|
265
|
+
},
|
|
217
266
|
},
|
|
218
267
|
async run({ args, rawArgs }: { args: InstallCommandArgs; rawArgs: string[] }) {
|
|
268
|
+
let engineVersion: string | undefined;
|
|
269
|
+
try {
|
|
270
|
+
engineVersion = resolveEngineVersionFlag(args["engine-version"] ?? args.engineVersion);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
log.error(errorMessage(err));
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
219
275
|
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
220
276
|
const positionals = (args._ ?? []).map(String);
|
|
221
277
|
const caps = await getEngineCapabilities();
|
|
@@ -227,13 +283,14 @@ export const installCommand = defineCommand({
|
|
|
227
283
|
log.error(errorMessage(err));
|
|
228
284
|
process.exit(1);
|
|
229
285
|
}
|
|
230
|
-
await performInstall(
|
|
231
|
-
resolveNoCacheFlag(args, rawArgs),
|
|
286
|
+
await performInstall({
|
|
287
|
+
noCache: resolveNoCacheFlag(args, rawArgs),
|
|
232
288
|
backend,
|
|
233
289
|
ttsLangs,
|
|
234
|
-
args.vad,
|
|
235
|
-
args.diarize,
|
|
236
|
-
args.plan,
|
|
237
|
-
|
|
290
|
+
vad: args.vad,
|
|
291
|
+
diarize: args.diarize,
|
|
292
|
+
plan: args.plan,
|
|
293
|
+
engineVersion,
|
|
294
|
+
});
|
|
238
295
|
},
|
|
239
296
|
});
|
package/src/doctor.ts
CHANGED
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
} from "./engine";
|
|
11
11
|
import { readInstalledEngineVersion } from "./engine-version-marker";
|
|
12
12
|
import { keshaCacheDir } from "./paths";
|
|
13
|
-
import { packageName, packageVersion } from "./package-info";
|
|
13
|
+
import { engineVersion, packageName, packageVersion } from "./package-info";
|
|
14
14
|
import { getStatsStatus, type StatsStatus } from "./stats";
|
|
15
15
|
import {
|
|
16
16
|
fluidKokoroCacheInfo,
|
|
17
17
|
type FluidKokoroCacheInfo,
|
|
18
18
|
} from "./fluid-kokoro-cache";
|
|
19
|
+
import { fluidAsrCacheInfo, isCoremlBackend } from "./fluid-asr-cache";
|
|
19
20
|
import { diagnosticHomeDir, dirSizeBytes } from "./diagnostic-paths";
|
|
20
21
|
import {
|
|
21
22
|
getDiagnosticLogStatus,
|
|
@@ -30,6 +31,7 @@ const KNOWN_ENV_KEYS = [
|
|
|
30
31
|
"KESHA_STATS_DB",
|
|
31
32
|
"KESHA_DEBUG",
|
|
32
33
|
"KESHA_DEBUG_FD",
|
|
34
|
+
"KESHA_KOKORO_COMPUTE_UNITS",
|
|
33
35
|
] as const;
|
|
34
36
|
|
|
35
37
|
interface DoctorOptions {
|
|
@@ -53,6 +55,21 @@ interface OptionalComponent extends PathSummary {
|
|
|
53
55
|
|
|
54
56
|
type DoctorDiagnosticLogStatus = DiagnosticLogStatus & { error?: string };
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* How the Recorded Engine version relates to the Pinned Engine version. `differs-from-pin`
|
|
60
|
+
* is a supported state (`kesha install --engine-version`), not a fault — `unrecorded` is
|
|
61
|
+
* separate so a missing marker is never reported as a difference.
|
|
62
|
+
*/
|
|
63
|
+
export type EngineVersionState = "matches-pin" | "differs-from-pin" | "unrecorded";
|
|
64
|
+
|
|
65
|
+
export function engineVersionState(
|
|
66
|
+
versionMarker: string | null,
|
|
67
|
+
pinnedVersion: string,
|
|
68
|
+
): EngineVersionState {
|
|
69
|
+
if (versionMarker === null) return "unrecorded";
|
|
70
|
+
return versionMarker === pinnedVersion ? "matches-pin" : "differs-from-pin";
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
export interface DoctorReport {
|
|
57
74
|
generatedAt: string;
|
|
58
75
|
redacted: boolean;
|
|
@@ -69,6 +86,8 @@ export interface DoctorReport {
|
|
|
69
86
|
path: string;
|
|
70
87
|
installed: boolean;
|
|
71
88
|
versionMarker: string | null;
|
|
89
|
+
pinnedVersion: string;
|
|
90
|
+
versionState: EngineVersionState;
|
|
72
91
|
capabilities: EngineCapabilities | null;
|
|
73
92
|
probeError: string | null;
|
|
74
93
|
};
|
|
@@ -188,10 +207,14 @@ async function collectEngine(redact: boolean): Promise<DoctorReport["engine"]> {
|
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
209
|
|
|
210
|
+
const versionMarker = readInstalledEngineVersion(binPath);
|
|
211
|
+
|
|
191
212
|
return {
|
|
192
213
|
path: redactPath(binPath, redact),
|
|
193
214
|
installed,
|
|
194
|
-
versionMarker
|
|
215
|
+
versionMarker,
|
|
216
|
+
pinnedVersion: engineVersion,
|
|
217
|
+
versionState: engineVersionState(versionMarker, engineVersion),
|
|
195
218
|
capabilities,
|
|
196
219
|
probeError: redactString("probeError", probeError, redact),
|
|
197
220
|
};
|
|
@@ -200,18 +223,33 @@ async function collectEngine(redact: boolean): Promise<DoctorReport["engine"]> {
|
|
|
200
223
|
function collectCache(
|
|
201
224
|
redact: boolean,
|
|
202
225
|
fluidKokoro: FluidKokoroCacheInfo,
|
|
226
|
+
backend?: string,
|
|
203
227
|
): DoctorReport["cache"] {
|
|
204
228
|
const cache = keshaCacheDir();
|
|
205
229
|
const binPath = getEngineBinPath();
|
|
206
230
|
const engineDir = dirname(dirname(binPath));
|
|
231
|
+
const coreml = isCoremlBackend(backend);
|
|
207
232
|
const components: CacheComponent[] = [
|
|
208
233
|
{ label: "Engine", ...pathSummary(engineDir) },
|
|
209
|
-
|
|
234
|
+
// A CoreML engine never populates the ONNX dir; pointing at it would render a
|
|
235
|
+
// healthy darwin install as "ASR missing" (#684).
|
|
236
|
+
...(coreml
|
|
237
|
+
? []
|
|
238
|
+
: [{ label: "ASR (Parakeet)", ...pathSummary(join(cache, "models/parakeet-tdt-v3")) }]),
|
|
210
239
|
{ label: "Language ID", ...pathSummary(join(cache, "models/lang-id-ecapa")) },
|
|
211
240
|
{ label: "VAD (Silero)", ...pathSummary(join(cache, "models/silero-vad")) },
|
|
212
241
|
{ label: "TTS (Kokoro)", ...pathSummary(join(cache, "models/kokoro-82m")) },
|
|
213
242
|
{ label: "TTS (Vosk)", ...pathSummary(join(cache, "models/vosk-ru")) },
|
|
214
243
|
];
|
|
244
|
+
if (coreml) {
|
|
245
|
+
const fluidAsr = fluidAsrCacheInfo();
|
|
246
|
+
components.push({
|
|
247
|
+
label: "ASR (Parakeet, FluidAudio) (external)",
|
|
248
|
+
path: fluidAsr.path,
|
|
249
|
+
exists: fluidAsr.exists,
|
|
250
|
+
sizeBytes: fluidAsr.sizeBytes,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
215
253
|
if (fluidKokoro.supported) {
|
|
216
254
|
components.push({
|
|
217
255
|
label: "FluidAudio Kokoro cache (external)",
|
|
@@ -346,6 +384,7 @@ export async function collectDoctorReport(
|
|
|
346
384
|
): Promise<DoctorReport> {
|
|
347
385
|
const redact = options.redact === true;
|
|
348
386
|
const fluidKokoro = fluidKokoroCacheInfo();
|
|
387
|
+
const engine = await collectEngine(redact);
|
|
349
388
|
return {
|
|
350
389
|
generatedAt: new Date().toISOString(),
|
|
351
390
|
redacted: redact,
|
|
@@ -355,8 +394,8 @@ export async function collectDoctorReport(
|
|
|
355
394
|
platform: process.platform,
|
|
356
395
|
arch: process.arch,
|
|
357
396
|
},
|
|
358
|
-
engine
|
|
359
|
-
cache: collectCache(redact, fluidKokoro),
|
|
397
|
+
engine,
|
|
398
|
+
cache: collectCache(redact, fluidKokoro, engine.capabilities?.backend),
|
|
360
399
|
optionalComponents: collectOptionalComponents(redact, fluidKokoro),
|
|
361
400
|
stats: collectStats(redact),
|
|
362
401
|
diagnosticLogs: collectDiagnosticLogs(redact),
|
|
@@ -368,6 +407,22 @@ function formatInstalled(installed: boolean): string {
|
|
|
368
407
|
return installed ? "installed" : "missing";
|
|
369
408
|
}
|
|
370
409
|
|
|
410
|
+
function formatVersionState(engine: DoctorReport["engine"]): string {
|
|
411
|
+
switch (engine.versionState) {
|
|
412
|
+
case "differs-from-pin":
|
|
413
|
+
return (
|
|
414
|
+
`installed v${engine.versionMarker} differs from the pinned v${engine.pinnedVersion} — ` +
|
|
415
|
+
"supported; `kesha install` without --engine-version restores the pin"
|
|
416
|
+
);
|
|
417
|
+
case "unrecorded":
|
|
418
|
+
return engine.installed
|
|
419
|
+
? "no recorded version beside the binary; `kesha install` records one"
|
|
420
|
+
: "no engine installed";
|
|
421
|
+
default:
|
|
422
|
+
return "matches the pin";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
371
426
|
function formatCapabilities(engine: DoctorReport["engine"]): string {
|
|
372
427
|
if (!engine.capabilities) return engine.probeError ?? "not available";
|
|
373
428
|
const { backend, protocolVersion, features } = engine.capabilities;
|
|
@@ -437,6 +492,8 @@ export function formatDoctorReport(report: DoctorReport): string {
|
|
|
437
492
|
"Engine:",
|
|
438
493
|
` Binary: ${report.engine.path} (${formatInstalled(report.engine.installed)})`,
|
|
439
494
|
` Version marker: ${report.engine.versionMarker ?? "missing"}`,
|
|
495
|
+
` Pinned version: ${report.engine.pinnedVersion}`,
|
|
496
|
+
` Version state: ${formatVersionState(report.engine)}`,
|
|
440
497
|
` Capabilities: ${formatCapabilities(report.engine)}`,
|
|
441
498
|
"",
|
|
442
499
|
...formatCacheSection(report.cache),
|