@drakulavich/kesha-voice-kit 1.28.0 → 1.30.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/NOTICES.md +1 -1
- package/README.md +5 -4
- package/docs/product-positioning.md +5 -4
- package/package.json +4 -4
- package/src/cli/command-session.ts +11 -0
- package/src/cli/init.ts +6 -4
- package/src/cli/install.ts +17 -11
- package/src/cli/main.ts +14 -15
- package/src/cli/record.ts +10 -3
- package/src/cli/say.ts +13 -31
- package/src/doctor.ts +2 -1
- package/src/engine/describe.ts +227 -0
- package/src/engine/events.ts +235 -0
- package/src/engine-health.ts +14 -14
- package/src/engine-install.ts +132 -96
- package/src/engine-targets.ts +3 -3
- package/src/engine.ts +244 -380
- package/src/error-utils.ts +4 -1
- package/src/fluid-asr-cache.ts +3 -3
- package/src/fluid-roots.ts +4 -4
- package/src/install-lock.ts +10 -9
- package/src/kokoro-ane.ts +4 -4
- package/src/lib.ts +4 -2
- package/src/mcp/voices.ts +3 -37
- package/src/progress.ts +34 -2
- package/src/status.ts +9 -7
- package/src/synth.ts +68 -91
- package/src/transcribe.ts +18 -25
- package/src/error-codes.ts +0 -45
package/NOTICES.md
CHANGED
|
@@ -19,7 +19,7 @@ see [speechbrain/lang-id-voxlingua107-ecapa](https://huggingface.co/speechbrain/
|
|
|
19
19
|
### Silero VAD v5 (opt-in: `kesha install --vad`)
|
|
20
20
|
|
|
21
21
|
Voice activity detection. MIT. Authored by [Silero Team](https://github.com/snakers4/silero-vad).
|
|
22
|
-
Pinned to the `v6.2.1` tag.
|
|
22
|
+
Pinned to a commit (the `v6.2.1` tag's target at the time of pinning).
|
|
23
23
|
|
|
24
24
|
## Models downloaded by `kesha install --tts`
|
|
25
25
|
|
package/README.md
CHANGED
|
@@ -36,14 +36,14 @@ bun add -g @drakulavich/kesha-voice-kit
|
|
|
36
36
|
kesha --version # confirms `kesha` resolved on PATH
|
|
37
37
|
|
|
38
38
|
# 3. Download the engine and models — pick one path
|
|
39
|
-
kesha init # guided:
|
|
39
|
+
kesha init # guided: TTS languages and optional VAD / diarization
|
|
40
40
|
kesha install --plan && kesha install # manual: preview the sizes, then download
|
|
41
41
|
|
|
42
42
|
# 4. Transcribe
|
|
43
43
|
kesha audio.ogg # transcript to stdout
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`kesha install` pulls ~2.5 GB on Linux/Windows and ~0.6 GB on Apple Silicon, whose CoreML engine reads a smaller model set. It is always explicit — nothing downloads behind your back — and
|
|
46
|
+
`kesha install` pulls ~2.5 GB on Linux/Windows and ~0.6 GB on Apple Silicon, whose CoreML engine reads a smaller model set. It is always explicit — nothing downloads behind your back — and reports download progress on stderr. If `bun --version` fails right after step 1, reload your PATH: `exec $SHELL -l`.
|
|
47
47
|
|
|
48
48
|
Prefer Homebrew or Docker? See [Other install methods](#other-install-methods).
|
|
49
49
|
Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
|
|
@@ -90,15 +90,16 @@ $ kesha freedom.ogg tahiti.ogg
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
- **Record from the mic (macOS):** `kesha record --out hello.wav` writes microphone audio to a WAV file (`kesha hello.wav` transcribes it). macOS prompts for microphone access on first use — grant it under System Settings → Privacy & Security → Microphone if it was denied. On Linux/Windows or headless boxes, pass any existing audio file straight to `kesha` instead.
|
|
93
|
-
- **Dictate straight to text (darwin-arm64):** `kesha record --live` transcribes the mic as it captures and prints the transcript to stdout — no WAV in between, so it pipes (`kesha record --live | pbcopy`). Progress goes to stderr.
|
|
93
|
+
- **Dictate straight to text (darwin-arm64):** `kesha record --live` transcribes the mic as it captures and prints the transcript to stdout — no WAV in between, so it pipes (`kesha record --live | pbcopy`). To end after trailing silence, explicitly install VAD then opt in: `kesha install --vad && kesha record --live --auto-stop`. The defaults are 1,000 ms of silence after 250 ms of speech; tune them with `--auto-stop-silence-ms`, `--auto-stop-min-speech-ms`, and `--auto-stop-threshold`. Progress goes to stderr. Linux and Windows do not capture the microphone; pass an existing audio file to `kesha` to transcribe it. An interruption is recoverable: Ctrl-C (or SIGTERM) stops the session, still prints what you dictated, and exits 130/143, and the audio is spilled to a recovery WAV under `~/.cache/kesha/recordings/` — named on stderr when the session starts, deleted once the transcript has actually been delivered, kept if anything — a signal, a crash, a closed terminal, a dead pipe — got in the way first ([#962](https://github.com/drakulavich/kesha-voice-kit/issues/962)).
|
|
94
94
|
- **Long / silence-heavy audio:** install VAD (`kesha install --vad`); Kesha auto-uses it past 120 s. Without VAD, long audio falls back to fixed ASR chunks. See [docs/vad.md](docs/vad.md).
|
|
95
95
|
- **Speaker diarization** (darwin-arm64): `kesha install --diarize` (which installs VAD too), then `kesha --json --speakers meeting.m4a` stamps each segment with a `speaker` id. `--speakers` engages VAD windowing itself at any duration, so it cannot be combined with `--no-vad`. Linux/Windows return a clear "darwin-arm64 only" error ([#199](https://github.com/drakulavich/kesha-voice-kit/issues/199)).
|
|
96
96
|
- **Word-level timestamps** (every platform): `kesha --json --timestamps audio.ogg` adds a `words` array to each segment — `{ "word": "email", "start": 0.72, "end": 1.12 }` — on the same file-relative clock as the segment, so a word always lies inside the segment carrying it. Read them off the decoder's own frame grid, so: times are quantised to 0.08 s, consecutive spans may overlap (each `end` is a per-word duration prediction, not the next word's `start`), `end >= start` rather than strictly greater, and punctuation stays attached to its word. The key is simply absent where a segment has none — any segment `--itn` rewrote, for one — so check the `transcribe.words` capability rather than expecting an empty array ([#720](https://github.com/drakulavich/kesha-voice-kit/issues/720)).
|
|
97
|
+
- **Text-language detection:** JSON and TOON results include `textLanguage` with a language code, confidence, and its `source`. On macOS Kesha uses Apple `NLLanguageRecognizer`; elsewhere it uses the bundled `tinyld` fallback, whose confidence scale is different. This is separate from `audioLanguage`, which identifies the spoken audio when available.
|
|
97
98
|
- **Written-form numbers:** `--itn` rewrites what the model spells out — `"two hundred thirty two"` → `"232"`, `"five dollars and fifty cents"` → `"$5.50"`. Opt-in, every platform, timestamps untouched. English-only in practice; Russian and the rest pass through unchanged. Spoken punctuation names stay words (`"dot"`, `"comma"`, `"the period of growth"`) because Kesha transcribes speech rather than dictation — so `"example dot com"` keeps its words too ([#822](https://github.com/drakulavich/kesha-voice-kit/issues/822)). A sentence `"and"` survives the number that follows it (`"cats and three dogs"` → `"cats and 3 dogs"`), while an `"and"` the number owns still joins it (`"three hundred and five"` → `"305"`) ([#1000](https://github.com/drakulavich/kesha-voice-kit/issues/1000)) — and no longer splits the number around it (`"two hundred and thirty two"` → `"232"`, not `"230 2"`) ([#1006](https://github.com/drakulavich/kesha-voice-kit/issues/1006)). A hyphenated number reads the same as the spaced form (`"twenty-five apples"` → `"25 apples"`), while a hyphen between ordinary words is left alone (`"well-known"`, `"state-of-the-art"`, `"twenty-something"`) ([#1004](https://github.com/drakulavich/kesha-voice-kit/issues/1004)).
|
|
98
99
|
|
|
99
100
|
## Text-to-speech
|
|
100
101
|
|
|
101
|
-
Kesha speaks back in [9 languages](docs/languages.md#text-to-speech). On macOS
|
|
102
|
+
Kesha speaks back in [9 languages](docs/languages.md#text-to-speech). Kokoro runs natively through FluidAudio CoreML/ANE on Apple Silicon and through ONNX on Linux and Windows; Russian uses Vosk-TTS, while `macos-*` system voices need no model download. On macOS Kesha picks the voice from the text's own language; on Linux and Windows, state the language with `--lang <code>` (or the voice with `--voice <id>`) — otherwise the engine default speaks.
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
105
|
kesha install --tts # English voices; sizes differ per platform — preview: kesha install --plan
|
|
@@ -21,7 +21,7 @@ The product promise is not "one model for every audio problem." The promise is a
|
|
|
21
21
|
| Transcribe to structured output for scripts or agents | Stable | `kesha --json audio.ogg`, `kesha --toon audio.ogg`, `kesha --format transcript audio.ogg` |
|
|
22
22
|
| Detect likely audio/text language during transcription | Stable | `--lang`, `--verbose`, JSON/TOON language fields |
|
|
23
23
|
| Skip silence in long audio | Beta | `kesha --vad meeting.m4a`, auto-on for long audio when VAD is installed |
|
|
24
|
-
| Synthesize
|
|
24
|
+
| Synthesize supported languages locally | Beta | `kesha say "text"`, `kesha install --tts` |
|
|
25
25
|
| Use macOS system voices without model downloads | Stable on macOS | `kesha say --voice macos-*` |
|
|
26
26
|
| Label speakers in meeting transcripts | Preview, darwin-arm64 only | `kesha --json --vad --speakers meeting.m4a` |
|
|
27
27
|
| Integrate with OpenClaw as a local voice model | Stable integration surface, user-configured route | `docs/openclaw.md` |
|
|
@@ -35,7 +35,7 @@ The product promise is not "one model for every audio problem." The promise is a
|
|
|
35
35
|
- Streaming phone-call transcription or real-time conversation infrastructure.
|
|
36
36
|
- Speaker identity across files. Diarization emits per-file cluster IDs, not names or persistent voice profiles.
|
|
37
37
|
- Full SSML coverage. Kesha supports a practical subset for current TTS engines; unsupported tags should fail or warn clearly.
|
|
38
|
-
- Universal TTS across all 25 STT languages. TTS is intentionally scoped to
|
|
38
|
+
- Universal TTS across all 25 STT languages. TTS is intentionally scoped to its documented nine-language set and macOS system voices.
|
|
39
39
|
- Model training, fine-tuning, or benchmark leadership on every dataset.
|
|
40
40
|
- Browser/mobile SDKs. The supported product surface is CLI-first, with thin integrations around it.
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ The product promise is not "one model for every audio problem." The promise is a
|
|
|
47
47
|
| JSON / TOON / transcript output formats | Stable | Intended for scripts, agents, and downstream tooling. |
|
|
48
48
|
| Audio language detection | Stable | Used for warnings and structured metadata. |
|
|
49
49
|
| VAD preprocessing | Beta | Useful for long or silence-heavy audio; short voice messages stay on the fast path. |
|
|
50
|
-
|
|
|
50
|
+
| Kokoro and Vosk TTS | Beta | Nine documented languages; requires `kesha install --tts`, so model cache and first-run cost are expected. |
|
|
51
51
|
| macOS AVSpeech voices | Stable on macOS | Zero-install system voices; quality is OS voice quality, not neural TTS. |
|
|
52
52
|
| SSML subset | Preview | `<prosody rate>`, Russian stress markers, and IPA support are engine-specific. |
|
|
53
53
|
| Speaker diarization | Preview | darwin-arm64 only; cluster IDs are stable within one file only. |
|
|
@@ -63,8 +63,9 @@ The product promise is not "one model for every audio problem." The promise is a
|
|
|
63
63
|
| STT | Supported, CoreML path | Not shipped | Supported, ONNX CPU path | Supported, ONNX CPU path | `aarch64-darwin`, `x86_64-linux` |
|
|
64
64
|
| Audio language detection | Supported | Not shipped | Supported | Supported | Supported where the flake builds |
|
|
65
65
|
| VAD | Supported with `kesha install --vad` | Not shipped | Supported with `kesha install --vad` | Supported with `kesha install --vad` | Supported where the engine path includes VAD assets |
|
|
66
|
-
| TTS:
|
|
66
|
+
| TTS: Kokoro (`en` `es` `fr` `it` `pt`) | Supported, FluidAudio/CoreML in release builds | Not shipped | Supported, ONNX path | Supported, ONNX path | Supported except where noted in `docs/nix-install.md` |
|
|
67
67
|
| TTS: Russian Vosk-TTS | Supported | Not shipped | Supported | Supported | Supported except where noted in `docs/nix-install.md` |
|
|
68
|
+
| TTS: Kokoro (`hi` `ja` `zh`) | Supported, darwin-arm64 only | Not shipped | Not supported | Not supported | `aarch64-darwin` only |
|
|
68
69
|
| macOS system voices | Supported | Not shipped | Not applicable | Not applicable | Supported on `aarch64-darwin` |
|
|
69
70
|
| Speaker diarization | Preview, darwin-arm64 only | Not supported | Not supported | Not supported | Not wired into the Nix build yet |
|
|
70
71
|
| Raycast extension | Supported | Not shipped | Not applicable | Not applicable | Not applicable |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
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": {
|
|
@@ -40,11 +40,10 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"keshaEngine": {
|
|
43
|
-
"version": "1.
|
|
43
|
+
"version": "1.25.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"test": "bun test --timeout 30000",
|
|
47
|
-
"conveyor": "bun scripts/backlog.ts",
|
|
48
47
|
"test:watch": "bun test --timeout 30000 --watch",
|
|
49
48
|
"test:unit": "bun test --timeout 30000 tests/unit/",
|
|
50
49
|
"test:cli-fast": "bun test --timeout 30000 tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/error-codes-cli.test.ts tests/integration/say.test.ts",
|
|
@@ -110,6 +109,7 @@
|
|
|
110
109
|
"overrides": {
|
|
111
110
|
"hono": "^4.12.25",
|
|
112
111
|
"fast-uri": "^3.1.5",
|
|
113
|
-
"ip-address": "^10.3.1"
|
|
112
|
+
"ip-address": "^10.3.1",
|
|
113
|
+
"smol-toml": "1.7.2"
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DiagnosticLogSession,
|
|
5
5
|
DiagnosticSessionStatus,
|
|
6
6
|
} from "../diagnostic-log";
|
|
7
|
+
import { setEngineDebugSink } from "../engine/events";
|
|
7
8
|
import { errorMessage } from "../error-utils";
|
|
8
9
|
import { log } from "../log";
|
|
9
10
|
import { createStatsRecorder } from "../stats";
|
|
@@ -46,12 +47,22 @@ export async function runCommandSession(
|
|
|
46
47
|
};
|
|
47
48
|
session.diagnosticLog.event("command.start", { command, ...startFields });
|
|
48
49
|
|
|
50
|
+
setEngineDebugSink((event) =>
|
|
51
|
+
session.diagnosticLog.event("engine.debug", {
|
|
52
|
+
t_ms: event.t_ms,
|
|
53
|
+
event: event.event ?? null,
|
|
54
|
+
message: event.message,
|
|
55
|
+
fields: event.fields === undefined ? null : JSON.stringify(event.fields),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
49
58
|
let outcome: CommandOutcome;
|
|
50
59
|
try {
|
|
51
60
|
outcome = await body(session);
|
|
52
61
|
} catch (err) {
|
|
53
62
|
closeSessionQuietly(session, command, { status: "failed", itemCount: 0 });
|
|
54
63
|
throw err;
|
|
64
|
+
} finally {
|
|
65
|
+
setEngineDebugSink(null);
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
// A flush error must not displace a failure the command already has to report (Greptile P1/P2 on #607).
|
package/src/cli/init.ts
CHANGED
|
@@ -2,13 +2,15 @@ import { defineCommand } from "citty";
|
|
|
2
2
|
import { confirm, multiselect, isCancel, cancel } from "@clack/prompts";
|
|
3
3
|
import { installCommandTokens, renderInstallPlan } from "../install-plan";
|
|
4
4
|
import { log } from "../log";
|
|
5
|
+
import { errorMessage } from "../error-utils";
|
|
6
|
+
import { exitCodeFor } from "../engine/events";
|
|
5
7
|
import { getEngineCapabilities } from "../engine";
|
|
6
8
|
import {
|
|
7
9
|
installableTtsLangs,
|
|
8
10
|
performInstall,
|
|
9
11
|
resolveBackendFlag,
|
|
10
12
|
resolveNoCacheFlag,
|
|
11
|
-
|
|
13
|
+
unavailableBackendRefusal,
|
|
12
14
|
} from "./install";
|
|
13
15
|
import type { SharedInstallArgs } from "./types";
|
|
14
16
|
|
|
@@ -176,10 +178,10 @@ export async function promptInitSelection(
|
|
|
176
178
|
|
|
177
179
|
/** Mirrors the guard in `performInstall`: a preview must not describe an install this platform rejects (#684). */
|
|
178
180
|
async function printPlan(selection: InitSelection): Promise<boolean> {
|
|
179
|
-
const backendError =
|
|
181
|
+
const backendError = unavailableBackendRefusal(selection.backend);
|
|
180
182
|
if (backendError) {
|
|
181
|
-
log.error(backendError);
|
|
182
|
-
process.exitCode =
|
|
183
|
+
log.error(errorMessage(backendError));
|
|
184
|
+
process.exitCode = exitCodeFor(backendError);
|
|
183
185
|
return false;
|
|
184
186
|
}
|
|
185
187
|
log.info(
|
package/src/cli/install.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
|
-
import { installEngine } from "../engine-install";
|
|
3
|
+
import { assertPlatformCanInstall, installEngine } from "../engine-install";
|
|
4
4
|
import { engineTarget, isDarwinArm64 } from "../engine-targets";
|
|
5
5
|
import { getEngineBinPath, getEngineCapabilities, type EngineCapabilities } from "../engine";
|
|
6
|
+
import { exitCodeFor, KeshaError } from "../engine/events";
|
|
6
7
|
import { renderInstallPlan } from "../install-plan";
|
|
7
8
|
import { maybeAskForStar } from "../star";
|
|
8
9
|
import { log } from "../log";
|
|
@@ -171,6 +172,12 @@ function finishInstallDiagnostic(
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/** The refusal `install`, `install --plan` and `init --plan` all render the same way (#684). */
|
|
176
|
+
export function unavailableBackendRefusal(backend: string | undefined): KeshaError | null {
|
|
177
|
+
const message = unavailableBackendError(backend);
|
|
178
|
+
return message ? new KeshaError("E_INVALID_ARG", message) : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
174
181
|
/** Null when the requested backend is installable here. `KESHA_ENGINE_BIN` opts out — the user supplied their own engine. */
|
|
175
182
|
export function unavailableBackendError(backend: string | undefined): string | null {
|
|
176
183
|
const platformBackend = defaultBackendForPlatform();
|
|
@@ -197,11 +204,11 @@ export async function performInstall(options: PerformInstallOptions) {
|
|
|
197
204
|
const { noCache, backend, ttsLangs, vad = false, diarize = false, plan = false, engineVersion } =
|
|
198
205
|
options;
|
|
199
206
|
// A plan for an unavailable backend previews what its own printed command rejects (#684).
|
|
200
|
-
const backendError =
|
|
207
|
+
const backendError = unavailableBackendRefusal(backend);
|
|
201
208
|
if (plan) {
|
|
202
209
|
if (backendError) {
|
|
203
|
-
log.error(backendError);
|
|
204
|
-
process.exitCode =
|
|
210
|
+
log.error(errorMessage(backendError));
|
|
211
|
+
process.exitCode = exitCodeFor(backendError);
|
|
205
212
|
return;
|
|
206
213
|
}
|
|
207
214
|
log.info(
|
|
@@ -225,16 +232,15 @@ export async function performInstall(options: PerformInstallOptions) {
|
|
|
225
232
|
engineVersionOverride: engineVersion !== undefined,
|
|
226
233
|
});
|
|
227
234
|
|
|
228
|
-
|
|
235
|
+
try {
|
|
236
|
+
assertPlatformCanInstall({ diarize });
|
|
237
|
+
} catch (err) {
|
|
229
238
|
errorKind = "validation_failed";
|
|
230
|
-
throw
|
|
231
|
-
"--diarize is currently darwin-arm64 only " +
|
|
232
|
-
"(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
|
|
233
|
-
);
|
|
239
|
+
throw err;
|
|
234
240
|
}
|
|
235
241
|
if (backendError) {
|
|
236
242
|
errorKind = "validation_failed";
|
|
237
|
-
throw
|
|
243
|
+
throw backendError;
|
|
238
244
|
}
|
|
239
245
|
await installEngine({ noCache, backend, ttsLangs, vad, diarize, version: engineVersion });
|
|
240
246
|
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
@@ -248,7 +254,7 @@ export async function performInstall(options: PerformInstallOptions) {
|
|
|
248
254
|
const message = errorMessage(err);
|
|
249
255
|
finishInstallDiagnostic(diagnosticLog, startedAt, "failed", errorKind);
|
|
250
256
|
log.error(message);
|
|
251
|
-
process.exit(1);
|
|
257
|
+
process.exit(err instanceof KeshaError ? exitCodeFor(err) : 1);
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
package/src/cli/main.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { defineCommand } from "citty";
|
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
3
|
import { existsSync, statSync } from "fs";
|
|
4
4
|
import { detectAll } from "tinyld";
|
|
5
|
-
import {
|
|
5
|
+
import { transcribeWithSegments, validateTranscribeRequest } from "../transcribe";
|
|
6
6
|
import { detectAudioLanguageEngine, detectTextLanguageEngine } from "../engine";
|
|
7
7
|
import type { LangDetectResult } from "../engine";
|
|
8
8
|
import { log } from "../log";
|
|
@@ -23,7 +23,7 @@ import { diagnosticSizeBucket } from "../diagnostic-events";
|
|
|
23
23
|
import { runCommandSession, type CommandSession } from "./command-session";
|
|
24
24
|
import { USAGE_MESSAGE } from "./dispatch";
|
|
25
25
|
import type { CliContext } from "./context";
|
|
26
|
-
import {
|
|
26
|
+
import { KeshaError } from "../engine/events";
|
|
27
27
|
|
|
28
28
|
interface MainCommandArgs {
|
|
29
29
|
_: string[];
|
|
@@ -301,26 +301,25 @@ async function processFile(
|
|
|
301
301
|
const { stats, diagnosticLog } = recorders;
|
|
302
302
|
|
|
303
303
|
if (!existsSync(file)) {
|
|
304
|
-
|
|
304
|
+
const err = new KeshaError("E_INPUT_NOT_FOUND", "File not found");
|
|
305
|
+
stats.recordError("input", err, err.code);
|
|
305
306
|
diagnosticLog.event("input.missing", {
|
|
306
307
|
command: "transcribe",
|
|
307
|
-
error_code:
|
|
308
|
+
error_code: err.code,
|
|
308
309
|
});
|
|
309
|
-
log.error(`${file}:
|
|
310
|
-
return { ok: false, error: { file, code:
|
|
310
|
+
log.error(`${file}: ${errorMessage(err)}`);
|
|
311
|
+
return { ok: false, error: { file, code: err.code, message: err.message } };
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
if (isDirectoryPath(file)) {
|
|
314
|
-
|
|
315
|
+
const err = new KeshaError("E_INVALID_ARG", "is a directory (expected an audio file)");
|
|
316
|
+
stats.recordError("input", err, err.code);
|
|
315
317
|
diagnosticLog.event("input.invalid", {
|
|
316
318
|
command: "transcribe",
|
|
317
|
-
error_code:
|
|
319
|
+
error_code: err.code,
|
|
318
320
|
});
|
|
319
|
-
log.error(`${file}:
|
|
320
|
-
return {
|
|
321
|
-
ok: false,
|
|
322
|
-
error: { file, code: TS_NATIVE_CODES.INVALID_ARG, message: "is a directory (expected an audio file)" },
|
|
323
|
-
};
|
|
321
|
+
log.error(`${file}: ${errorMessage(err)}`);
|
|
322
|
+
return { ok: false, error: { file, code: err.code, message: err.message } };
|
|
324
323
|
}
|
|
325
324
|
|
|
326
325
|
const inputArtifact = artifactFromFile(file, "input_audio");
|
|
@@ -343,7 +342,7 @@ async function processFile(
|
|
|
343
342
|
else write();
|
|
344
343
|
};
|
|
345
344
|
try {
|
|
346
|
-
await
|
|
345
|
+
await validateTranscribeRequest({ vad: vadMode, timestamps, speakers, itn });
|
|
347
346
|
progress = reportProgress
|
|
348
347
|
? createPercentProgress(`Transcribing ${file}`, {
|
|
349
348
|
estimatedTotalMs: speakers ? 60 * 60 * 1000 : 30 * 60 * 1000,
|
|
@@ -394,8 +393,8 @@ async function processFile(
|
|
|
394
393
|
return { ok: true, result };
|
|
395
394
|
} catch (err: unknown) {
|
|
396
395
|
progress?.stop();
|
|
396
|
+
const code = err instanceof KeshaError ? err.code : "E_TRANSCRIBE_FAILED";
|
|
397
397
|
const stderrText = errorMessage(err);
|
|
398
|
-
const code = extractEngineErrorCode(stderrText) ?? ENGINE_CODES.TRANSCRIBE_FAILED;
|
|
399
398
|
stats.recordError("transcribe", err, code);
|
|
400
399
|
diagnosticLog.event("engine.exit", {
|
|
401
400
|
command: "transcribe",
|
package/src/cli/record.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
|
+
import { exitCodeFor, KeshaError } from "../engine/events";
|
|
3
4
|
import {
|
|
4
5
|
isEngineInstalled,
|
|
5
|
-
preflightRecordLive,
|
|
6
6
|
recordEngine,
|
|
7
|
+
validateRecordRequest,
|
|
7
8
|
type LiveAutoStopOptions,
|
|
8
9
|
type RecordTarget,
|
|
9
10
|
} from "../engine";
|
|
10
11
|
import { installHint } from "../install-hint";
|
|
11
12
|
import { log } from "../log";
|
|
13
|
+
import { getPendingSignalExitCode, waitForPendingSignalCleanup } from "../process-tree";
|
|
12
14
|
|
|
13
15
|
export interface RecordArgs {
|
|
14
16
|
out?: string;
|
|
@@ -203,11 +205,16 @@ export const recordCommand = defineCommand({
|
|
|
203
205
|
process.exit(1);
|
|
204
206
|
}
|
|
205
207
|
try {
|
|
206
|
-
|
|
208
|
+
await validateRecordRequest(resolved.target, resolved.maxSeconds);
|
|
207
209
|
await recordEngine(resolved.target, resolved.maxSeconds);
|
|
208
210
|
} catch (err) {
|
|
211
|
+
const signalExitCode = getPendingSignalExitCode();
|
|
212
|
+
if (signalExitCode !== null) {
|
|
213
|
+
await waitForPendingSignalCleanup();
|
|
214
|
+
process.exit(signalExitCode);
|
|
215
|
+
}
|
|
209
216
|
log.error(errorMessage(err));
|
|
210
|
-
process.exit(1);
|
|
217
|
+
process.exit(err instanceof KeshaError ? exitCodeFor(err) : 1);
|
|
211
218
|
}
|
|
212
219
|
},
|
|
213
220
|
});
|
package/src/cli/say.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
|
-
import {
|
|
4
|
-
getEngineBinPath,
|
|
5
|
-
isEngineInstalled,
|
|
6
|
-
spawnEngineProcess,
|
|
7
|
-
spawnStdioWithDebugFd,
|
|
8
|
-
} from "../engine";
|
|
9
|
-
import { installHint } from "../install-hint";
|
|
10
|
-
import { registerProcessTree } from "../process-tree";
|
|
3
|
+
import { exitCodeFor, KeshaError } from "../engine/events";
|
|
11
4
|
import { log } from "../log";
|
|
12
5
|
import {
|
|
6
|
+
listVoiceIds,
|
|
13
7
|
say,
|
|
14
|
-
SayError,
|
|
15
8
|
SUPPORTED_SAMPLE_RATES,
|
|
16
9
|
type SayFormat,
|
|
17
10
|
type SayOptions,
|
|
@@ -216,16 +209,16 @@ async function synthesizeAndEmit(
|
|
|
216
209
|
},
|
|
217
210
|
};
|
|
218
211
|
} catch (err) {
|
|
219
|
-
const code = err instanceof
|
|
220
|
-
const exitCode = err
|
|
212
|
+
const code = err instanceof KeshaError ? err.code : "E_INTERNAL";
|
|
213
|
+
const exitCode = exitCodeFor(err);
|
|
221
214
|
stats.recordError("tts", err, code);
|
|
222
|
-
log.error(
|
|
215
|
+
log.error(errorMessage(err));
|
|
223
216
|
return {
|
|
224
217
|
status: "failed",
|
|
225
218
|
itemCount: 1,
|
|
226
219
|
exitCode,
|
|
227
220
|
finishFields: {
|
|
228
|
-
errorKind: err instanceof
|
|
221
|
+
errorKind: err instanceof KeshaError ? "say_error" : "error",
|
|
229
222
|
exitCode,
|
|
230
223
|
error_code: code,
|
|
231
224
|
},
|
|
@@ -290,26 +283,15 @@ export const sayCommand = defineCommand({
|
|
|
290
283
|
async run({ args }) {
|
|
291
284
|
if (args.debug) log.debugEnabled = true;
|
|
292
285
|
if (args["list-voices"]) {
|
|
293
|
-
|
|
294
|
-
log.error(`kesha-engine not installed. run: ${installHint()}`);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
// The engine prints the list directly — just relay its stdout + exit code.
|
|
298
|
-
const proc = spawnEngineProcess(
|
|
299
|
-
getEngineBinPath(),
|
|
300
|
-
["say", "--list-voices"],
|
|
301
|
-
spawnStdioWithDebugFd(["inherit", "inherit", "inherit"]),
|
|
302
|
-
);
|
|
303
|
-
// Register so a Ctrl-C during a cold Engine load terminates it and exits 130/143 (#939);
|
|
304
|
-
// dispose before the process.exit below so the registration never outlives the run.
|
|
305
|
-
const tree = registerProcessTree(proc);
|
|
306
|
-
let exitCode: number;
|
|
286
|
+
let ids: string[];
|
|
307
287
|
try {
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
288
|
+
ids = await listVoiceIds({ onProgress: (line) => log.status(line) });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.error(errorMessage(err));
|
|
291
|
+
process.exit(exitCodeFor(err));
|
|
311
292
|
}
|
|
312
|
-
|
|
293
|
+
await Bun.write(Bun.stdout, ids.map((id) => `${id}\n`).join(""));
|
|
294
|
+
process.exit(0);
|
|
313
295
|
}
|
|
314
296
|
|
|
315
297
|
const flags = resolveSayFlags(args);
|
package/src/doctor.ts
CHANGED
|
@@ -34,7 +34,6 @@ const KNOWN_ENV_KEYS = [
|
|
|
34
34
|
"KESHA_MODEL_MIRROR",
|
|
35
35
|
"KESHA_STATS_DB",
|
|
36
36
|
"KESHA_DEBUG",
|
|
37
|
-
"KESHA_DEBUG_FD",
|
|
38
37
|
"KESHA_KOKORO_COMPUTE_UNITS",
|
|
39
38
|
"KESHA_DIARIZE_COMPUTE_UNITS",
|
|
40
39
|
"KESHA_DIARIZE_TIMEOUT_SECS",
|
|
@@ -221,6 +220,8 @@ async function collectEngine(redact: boolean): Promise<DoctorReport["engine"]> {
|
|
|
221
220
|
probeError = `binary is present but does not run (${health.detail}); re-run \`kesha install\``;
|
|
222
221
|
} else if (health.status === "mute") {
|
|
223
222
|
probeError = `${health.detail}; re-run \`kesha install\``;
|
|
223
|
+
} else if (health.status === "protocol") {
|
|
224
|
+
probeError = health.detail;
|
|
224
225
|
} else if (health.status === "ok") {
|
|
225
226
|
capabilities = health.capabilities;
|
|
226
227
|
}
|