@drakulavich/kesha-voice-kit 1.17.1 → 1.18.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.
- package/README.md +69 -3
- package/SKILL.md +2 -2
- package/docs/product-positioning.md +76 -0
- package/package.json +6 -2
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/doctor.ts +35 -0
- package/src/cli/install.ts +15 -5
- package/src/cli/main.ts +33 -10
- package/src/cli/say.ts +67 -9
- package/src/cli/status.ts +13 -2
- package/src/cli/support-bundle.ts +31 -0
- package/src/cli.ts +17 -1
- package/src/doctor.ts +375 -0
- package/src/engine-install.ts +84 -12
- package/src/engine.ts +3 -6
- package/src/format.ts +8 -3
- package/src/install-plan.ts +359 -0
- package/src/lib.ts +7 -3
- package/src/package-info.ts +6 -0
- package/src/paths.ts +10 -0
- package/src/stats.ts +307 -37
- package/src/status.ts +14 -13
- package/src/support-bundle.ts +148 -0
- package/src/types.ts +13 -0
- package/tsconfig.json +1 -0
package/README.md
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
<a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun" alt="Bun"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
<p align="center"><b>Open-source voice toolkit.</b> Optimized for Apple Silicon (CoreML),
|
|
14
|
+
<p align="center"><b>Open-source voice toolkit.</b> Optimized for Apple Silicon (CoreML), with ONNX fallback on supported Linux/Windows builds.<br>A collection of small, fast, open-source audio models — packaged as CLI tools and an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill for LLM agents.</p>
|
|
15
15
|
|
|
16
16
|
- **Speech-to-text** — 25 languages, ~15x faster than Whisper on Apple Silicon, ~2.5x on CPU
|
|
17
17
|
- **Text-to-speech** — Kokoro (EN) + Vosk-TTS (RU) + macOS system voices, SSML preview
|
|
18
18
|
- **Rust engine** — single 20MB binary, no ffmpeg, no Python, no native Node addons
|
|
19
19
|
- **OpenClaw-ready** — plug into your LLM agent as a voice processing skill
|
|
20
20
|
|
|
21
|
+
See [Product positioning](docs/product-positioning.md) for supported workflows, non-goals, maturity labels, and the platform matrix.
|
|
22
|
+
|
|
21
23
|
<p align="center">
|
|
22
24
|
<img src="./demo.gif" alt="kesha demo — English + Russian transcription with automatic language detection" width="800">
|
|
23
25
|
</p>
|
|
@@ -49,6 +51,32 @@ kesha audio.ogg # transcript to stdout
|
|
|
49
51
|
|
|
50
52
|
Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
|
|
51
53
|
|
|
54
|
+
## Homebrew Install
|
|
55
|
+
|
|
56
|
+
Homebrew installs the Bun-based CLI wrapper. Engine and model downloads remain
|
|
57
|
+
explicit:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
brew tap oven-sh/bun
|
|
61
|
+
brew install drakulavich/tap/kesha-voice-kit
|
|
62
|
+
kesha install
|
|
63
|
+
kesha audio.ogg
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [Homebrew install](docs/homebrew.md) for package scope and maintainer
|
|
67
|
+
validation.
|
|
68
|
+
|
|
69
|
+
## Support diagnostics
|
|
70
|
+
|
|
71
|
+
Kesha can collect local diagnostics without downloading models or mutating cache state:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
kesha doctor --json --redact
|
|
75
|
+
kesha support-bundle --output kesha-support.tar.gz
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`support-bundle` creates a redacted `.tar.gz` archive for GitHub issues. It includes runtime, engine, cache, optional-component, Stats status, and known Kesha environment settings. It does not include audio, transcripts, model files, or the Stats database.
|
|
79
|
+
|
|
52
80
|
## Nix Install
|
|
53
81
|
|
|
54
82
|
Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
|
|
@@ -60,6 +88,44 @@ nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
|
|
|
60
88
|
|
|
61
89
|
Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
|
|
62
90
|
|
|
91
|
+
## Docker
|
|
92
|
+
|
|
93
|
+
Linux x64 CLI image, published to GHCR:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
docker run --rm \
|
|
97
|
+
-v kesha-cache:/cache/kesha \
|
|
98
|
+
-v "$PWD:/work" -w /work \
|
|
99
|
+
ghcr.io/drakulavich/kesha-voice-kit:latest install
|
|
100
|
+
|
|
101
|
+
docker run --rm \
|
|
102
|
+
-v kesha-cache:/cache/kesha \
|
|
103
|
+
-v "$PWD:/work" -w /work \
|
|
104
|
+
ghcr.io/drakulavich/kesha-voice-kit:latest audio.ogg
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The image keeps model downloads and the engine cache under `/cache/kesha`.
|
|
108
|
+
Mount that path as a named volume so `kesha install`, TTS models, VAD, and future
|
|
109
|
+
runs reuse the same cache. `compose.yml` provides the same layout:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
docker compose run --rm kesha install
|
|
113
|
+
docker compose run --rm kesha audio.ogg
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Linux Packages
|
|
117
|
+
|
|
118
|
+
Stable engine releases also publish `.deb` and `.rpm` packages for Linux x64.
|
|
119
|
+
They install the standalone CLI wrapper; engine and model downloads remain explicit:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
kesha install
|
|
123
|
+
kesha audio.ogg
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See [Linux packages](docs/linux-packages.md) for install commands and package
|
|
127
|
+
scope.
|
|
128
|
+
|
|
63
129
|
## Speech-to-text
|
|
64
130
|
|
|
65
131
|
```bash
|
|
@@ -100,7 +166,7 @@ Each segment gets a `speaker` integer (cluster ID, stable within one file). Linu
|
|
|
100
166
|
|
|
101
167
|
## Text-to-speech
|
|
102
168
|
|
|
103
|
-
Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language:
|
|
169
|
+
Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language. On darwin-arm64, Kokoro uses FluidAudio CoreML instead of ONNX:
|
|
104
170
|
|
|
105
171
|
```bash
|
|
106
172
|
kesha install --tts # ~990MB (Kokoro + Vosk-TTS RU, opt-in)
|
|
@@ -158,7 +224,7 @@ See [BENCHMARK.md](BENCHMARK.md) for the full per-file breakdown (Russian + Engl
|
|
|
158
224
|
| SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
|
|
159
225
|
| Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
|
|
160
226
|
| Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
|
|
161
|
-
| Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [Kokoro](https://
|
|
227
|
+
| Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [FluidAudio Kokoro](https://github.com/FluidInference/FluidAudio) on darwin-arm64; ONNX Kokoro elsewhere · [Vosk-TTS](https://github.com/alphacep/vosk-tts) |
|
|
162
228
|
|
|
163
229
|
All models run through `kesha-engine` — a Rust binary using [FluidAudio](https://github.com/FluidInference/FluidAudio) (CoreML) on Apple Silicon and [ort](https://github.com/pykeio/ort) (ONNX Runtime) on other platforms.
|
|
164
230
|
|
package/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
|
|
|
22
22
|
## When to use
|
|
23
23
|
|
|
24
24
|
- **Voice memo arrived** (Telegram, WhatsApp, Slack, Signal .ogg/.opus/.m4a): transcribe with `kesha --json <path>` and branch on the detected language.
|
|
25
|
-
- **Need to reply with audio (file playback)**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
|
|
25
|
+
- **Need to reply with audio (file playback)**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). On darwin-arm64, English Kokoro uses FluidAudio CoreML instead of ONNX. For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
|
|
26
26
|
- **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into the messenger-native format with `kesha say "<text>" --format ogg-opus --out reply.ogg`. Default is mono 24 kHz @ 32 kbps — what Telegram `sendVoice` expects. No `ffmpeg` round-trip needed.
|
|
27
27
|
- **Need to detect what language a file is in** before choosing a pipeline: `kesha --json audio.ogg` returns both audio-based and text-based language detection with confidence scores.
|
|
28
28
|
|
|
@@ -153,7 +153,7 @@ No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled
|
|
|
153
153
|
|
|
154
154
|
**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.
|
|
155
155
|
|
|
156
|
-
**Text-to-speech:** English (Kokoro-82M,
|
|
156
|
+
**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 any macOS system voice via `--voice macos-*`.
|
|
157
157
|
|
|
158
158
|
## Performance
|
|
159
159
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Product positioning
|
|
2
|
+
|
|
3
|
+
Kesha Voice Kit is a local-first voice toolkit for developers and agent workflows. It is built for small, scriptable jobs where a CLI can turn audio into text, text into audio, or voice messages into structured data without sending content to a hosted API.
|
|
4
|
+
|
|
5
|
+
The product promise is not "one model for every audio problem." The promise is a boring, automatable local voice stack with clear platform limits, predictable install paths, and machine-readable output.
|
|
6
|
+
|
|
7
|
+
## Who this is for
|
|
8
|
+
|
|
9
|
+
- Developers and agent builders who need local speech-to-text for voice messages, meetings, support clips, or batch audio files.
|
|
10
|
+
- OpenClaw users who want private, low-latency voice-message transcription through a CLI model route.
|
|
11
|
+
- macOS users who want the fastest path on Apple Silicon through CoreML, with a CPU/ONNX fallback on Linux and Windows.
|
|
12
|
+
- Automation-heavy users who prefer stdout/stderr contracts, JSON/TOON output, and shell-friendly commands over a hosted dashboard.
|
|
13
|
+
- Projects that can accept explicit model downloads and local cache management in exchange for no API keys and no cloud audio upload.
|
|
14
|
+
|
|
15
|
+
## Primary workflows
|
|
16
|
+
|
|
17
|
+
| Workflow | Status | Command surface |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Transcribe one or more audio files to plain text | Stable | `kesha audio.ogg` |
|
|
20
|
+
| Transcribe to structured output for scripts or agents | Stable | `kesha --json audio.ogg`, `kesha --toon audio.ogg`, `kesha --format transcript audio.ogg` |
|
|
21
|
+
| Detect likely audio/text language during transcription | Stable | `--lang`, `--verbose`, JSON/TOON language fields |
|
|
22
|
+
| Skip silence in long audio | Beta | `kesha --vad meeting.m4a`, auto-on for long audio when VAD is installed |
|
|
23
|
+
| Synthesize English or Russian speech locally | Beta | `kesha say "text"`, `kesha install --tts` |
|
|
24
|
+
| Use macOS system voices without model downloads | Stable on macOS | `kesha say --voice macos-*` |
|
|
25
|
+
| Label speakers in meeting transcripts | Preview, darwin-arm64 only | `kesha --json --vad --speakers meeting.m4a` |
|
|
26
|
+
| Integrate with OpenClaw as a local voice model | Stable integration surface, user-configured route | `docs/openclaw.md` |
|
|
27
|
+
| Use Raycast actions for selected-file transcription or clipboard speech | Beta, macOS only | `raycast/` extension |
|
|
28
|
+
| Install through Nix instead of Bun/npm | Beta, selected systems | `docs/nix-install.md` |
|
|
29
|
+
|
|
30
|
+
## Non-goals
|
|
31
|
+
|
|
32
|
+
- Hosted SaaS transcription, dashboards, accounts, billing, or team management.
|
|
33
|
+
- Streaming phone-call transcription or real-time conversation infrastructure.
|
|
34
|
+
- Speaker identity across files. Diarization emits per-file cluster IDs, not names or persistent voice profiles.
|
|
35
|
+
- Full SSML coverage. Kesha supports a practical subset for current TTS engines; unsupported tags should fail or warn clearly.
|
|
36
|
+
- Universal TTS across all 25 STT languages. TTS is intentionally scoped to English, Russian, and macOS system voices.
|
|
37
|
+
- Model training, fine-tuning, or benchmark leadership on every dataset.
|
|
38
|
+
- Browser/mobile SDKs. The supported product surface is CLI-first, with thin integrations around it.
|
|
39
|
+
|
|
40
|
+
## Feature maturity
|
|
41
|
+
|
|
42
|
+
| Feature | Maturity | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Core STT CLI | Stable | Main product path; stdout transcript, stderr progress/errors. |
|
|
45
|
+
| JSON / TOON / transcript output formats | Stable | Intended for scripts, agents, and downstream tooling. |
|
|
46
|
+
| Audio language detection | Stable | Used for warnings and structured metadata. |
|
|
47
|
+
| VAD preprocessing | Beta | Useful for long or silence-heavy audio; short voice messages stay on the fast path. |
|
|
48
|
+
| English/Russian TTS | Beta | Requires `kesha install --tts`; model cache and first-run cost are expected. |
|
|
49
|
+
| macOS AVSpeech voices | Stable on macOS | Zero-install system voices; quality is OS voice quality, not neural TTS. |
|
|
50
|
+
| SSML subset | Preview | `<prosody rate>`, Russian stress markers, and IPA support are engine-specific. |
|
|
51
|
+
| Speaker diarization | Preview | darwin-arm64 only; cluster IDs are stable within one file only. |
|
|
52
|
+
| Raycast extension | Beta | Convenience UI over the CLI; macOS-only. |
|
|
53
|
+
| OpenClaw plugin | Stable integration surface | Discovery is packaged; actual audio route remains explicit user config. |
|
|
54
|
+
| Nix flake | Beta | Reproducible alternate install path for selected systems; Bun/npm remains canonical. |
|
|
55
|
+
|
|
56
|
+
## Platform matrix
|
|
57
|
+
|
|
58
|
+
| Capability | macOS arm64 | macOS x64 | Linux x64 | Windows x64 | Nix path |
|
|
59
|
+
|---|---|---|---|---|---|
|
|
60
|
+
| STT | Supported, CoreML path | Not shipped | Supported, ONNX CPU path | Supported, ONNX CPU path | `aarch64-darwin`, `x86_64-linux` |
|
|
61
|
+
| Audio language detection | Supported | Not shipped | Supported | Supported | Supported where the flake builds |
|
|
62
|
+
| 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 |
|
|
63
|
+
| TTS: English Kokoro | Supported, FluidAudio/CoreML in release builds | Not shipped | Supported, ONNX path | Supported, ONNX path | Supported except where noted in `docs/nix-install.md` |
|
|
64
|
+
| TTS: Russian Vosk-TTS | Supported | Not shipped | Supported | Supported | Supported except where noted in `docs/nix-install.md` |
|
|
65
|
+
| macOS system voices | Supported | Not shipped | Not applicable | Not applicable | Supported on `aarch64-darwin` |
|
|
66
|
+
| Speaker diarization | Preview, darwin-arm64 only | Not supported | Not supported | Not supported | Not wired into the Nix build yet |
|
|
67
|
+
| Raycast extension | Supported | Not shipped | Not applicable | Not applicable | Not applicable |
|
|
68
|
+
| OpenClaw integration | Supported through CLI route | Not shipped | Supported through CLI route | Supported through CLI route | Use the installed `kesha` command from the chosen path |
|
|
69
|
+
|
|
70
|
+
`macOS x64` means Intel Macs. Kesha does not currently publish a `darwin-x64` engine binary, so Intel Macs are intentionally marked as not shipped rather than implied to use the ONNX fallback.
|
|
71
|
+
|
|
72
|
+
## When to choose something else
|
|
73
|
+
|
|
74
|
+
Use a hosted transcription provider when you need managed uptime, centralized audit logs, team permissions, or no local model cache. Use a streaming ASR stack when partial transcripts during a live call matter more than local-first batch reliability. Use a domain-specific ASR model when medical, legal, or noisy industrial audio quality is the primary requirement.
|
|
75
|
+
|
|
76
|
+
Kesha is strongest when the input is already a file, the caller is an engineer or agent, and the desired output is a transcript, JSON object, TOON payload, or WAV file that can continue through a local pipeline.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"description": "Open-source voice toolkit. Speech-to-text (25 langs, ~19x faster than Whisper on Apple Silicon, ONNX fallback on CPU), text-to-speech (Kokoro + Vosk-TTS + ~180 macOS system voices, SSML), voice-activity detection, language detection (107 langs). Rust engine, OpenClaw skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"tsconfig.json",
|
|
19
19
|
"openclaw.plugin.json",
|
|
20
20
|
"openclaw-plugin.cjs",
|
|
21
|
+
"docs/product-positioning.md",
|
|
21
22
|
"SKILL.md",
|
|
22
23
|
"scripts/postinstall.cjs",
|
|
23
24
|
"LICENSE",
|
|
@@ -30,15 +31,18 @@
|
|
|
30
31
|
]
|
|
31
32
|
},
|
|
32
33
|
"keshaEngine": {
|
|
33
|
-
"version": "1.
|
|
34
|
+
"version": "1.18.0"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
37
|
"test": "bun test",
|
|
37
38
|
"test:unit": "bun test tests/unit/",
|
|
39
|
+
"test:cli-fast": "bun test tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/e2e-cli.test.ts tests/integration/say.test.ts",
|
|
38
40
|
"test:integration": "bun test tests/integration/",
|
|
39
41
|
"lint": "bunx tsc --noEmit",
|
|
40
42
|
"lint:tsgo": "bunx tsgo --noEmit",
|
|
43
|
+
"check": "bun run lint && bun run check:versions && bun run test:cli-fast",
|
|
41
44
|
"check:workflows": "bun .github/scripts/check-workflows.ts",
|
|
45
|
+
"check:release-manifest": "bun .github/scripts/release-manifest.mjs --check",
|
|
42
46
|
"check:versions": "bun .github/scripts/check-versions.ts",
|
|
43
47
|
"postinstall": "node scripts/postinstall.cjs"
|
|
44
48
|
},
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -2,13 +2,15 @@ import { runMain } from "citty";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { log } from "../log";
|
|
4
4
|
import { suggestCommand } from "../suggest-command";
|
|
5
|
+
import { doctorCommand } from "./doctor";
|
|
5
6
|
import { installCommand } from "./install";
|
|
6
7
|
import { sayCommand } from "./say";
|
|
7
8
|
import { statsCommand } from "./stats";
|
|
8
9
|
import { statusCommand } from "./status";
|
|
10
|
+
import { supportBundleCommand } from "./support-bundle";
|
|
9
11
|
import { mainCommand } from "./main";
|
|
10
12
|
|
|
11
|
-
const SUBCOMMANDS = ["install", "status", "say", "stats"];
|
|
13
|
+
const SUBCOMMANDS = ["doctor", "install", "status", "say", "stats", "support-bundle"];
|
|
12
14
|
|
|
13
15
|
function isPathLike(arg: string): boolean {
|
|
14
16
|
return arg.includes(".") || arg.includes("/") || existsSync(arg);
|
|
@@ -17,6 +19,11 @@ function isPathLike(arg: string): boolean {
|
|
|
17
19
|
export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
18
20
|
const [firstArg, ...restArgs] = rawArgs;
|
|
19
21
|
|
|
22
|
+
if (firstArg === "doctor") {
|
|
23
|
+
await runMain(doctorCommand, { rawArgs: restArgs });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
if (firstArg === "install") {
|
|
21
28
|
await runMain(installCommand, { rawArgs: restArgs });
|
|
22
29
|
return;
|
|
@@ -37,6 +44,11 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
|
37
44
|
return;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
if (firstArg === "support-bundle") {
|
|
48
|
+
await runMain(supportBundleCommand, { rawArgs: restArgs });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
// Check for unknown subcommands (non-flag, non-file-path args).
|
|
41
53
|
// Extensionless existing files remain valid transcription inputs; missing
|
|
42
54
|
// bare tokens are more likely command typos and should not start the engine.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { collectDoctorReport, formatDoctorReport } from "../doctor";
|
|
3
|
+
import { log } from "../log";
|
|
4
|
+
|
|
5
|
+
interface DoctorCommandArgs {
|
|
6
|
+
json: boolean;
|
|
7
|
+
redact: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const doctorCommand = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "doctor",
|
|
13
|
+
description: "Collect support diagnostics without changing local state",
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
json: {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
description: "Output diagnostics as JSON",
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
redact: {
|
|
22
|
+
type: "boolean",
|
|
23
|
+
description: "Redact secrets and user-home paths from diagnostic output",
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
async run({ args }: { args: DoctorCommandArgs }) {
|
|
28
|
+
const report = await collectDoctorReport({ redact: args.redact });
|
|
29
|
+
if (args.json) {
|
|
30
|
+
console.log(JSON.stringify(report, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
log.info(formatDoctorReport(report));
|
|
34
|
+
},
|
|
35
|
+
});
|
package/src/cli/install.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { downloadEngine } from "../engine-install";
|
|
3
3
|
import { getEngineBinPath } from "../engine";
|
|
4
|
+
import { renderInstallPlan } from "../install-plan";
|
|
4
5
|
import { maybeAskForStar } from "../star";
|
|
5
6
|
import { log } from "../log";
|
|
7
|
+
import { packageVersion } from "../package-info";
|
|
6
8
|
|
|
7
9
|
interface InstallCommandArgs {
|
|
8
10
|
coreml: boolean;
|
|
@@ -11,10 +13,9 @@ interface InstallCommandArgs {
|
|
|
11
13
|
tts: boolean;
|
|
12
14
|
vad: boolean;
|
|
13
15
|
diarize: boolean;
|
|
16
|
+
plan: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
|
|
17
|
-
|
|
18
19
|
function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
|
|
19
20
|
if (coreml && onnx) {
|
|
20
21
|
log.error('Choose only one backend: "--coreml" or "--onnx".');
|
|
@@ -31,7 +32,12 @@ async function performInstall(
|
|
|
31
32
|
tts = false,
|
|
32
33
|
vad = false,
|
|
33
34
|
diarize = false,
|
|
35
|
+
plan = false,
|
|
34
36
|
) {
|
|
37
|
+
if (plan) {
|
|
38
|
+
log.info(await renderInstallPlan({ noCache, backend, tts, vad, diarize }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
|
|
36
42
|
log.error(
|
|
37
43
|
"--diarize is currently darwin-arm64 only " +
|
|
@@ -41,8 +47,7 @@ async function performInstall(
|
|
|
41
47
|
}
|
|
42
48
|
try {
|
|
43
49
|
await downloadEngine(noCache, backend, { tts, vad, diarize });
|
|
44
|
-
|
|
45
|
-
await maybeAskForStar(getEngineBinPath(), currentVersion, log);
|
|
50
|
+
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
46
51
|
} catch (err: unknown) {
|
|
47
52
|
const message = err instanceof Error ? err.message : String(err);
|
|
48
53
|
log.error(message);
|
|
@@ -71,6 +76,11 @@ export const installCommand = defineCommand({
|
|
|
71
76
|
description: "Re-download even if cached",
|
|
72
77
|
default: false,
|
|
73
78
|
},
|
|
79
|
+
plan: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: "Show download, disk, and warm-up plan without changing local state",
|
|
82
|
+
default: false,
|
|
83
|
+
},
|
|
74
84
|
tts: {
|
|
75
85
|
type: "boolean",
|
|
76
86
|
description: "Also install TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)",
|
|
@@ -89,6 +99,6 @@ export const installCommand = defineCommand({
|
|
|
89
99
|
},
|
|
90
100
|
async run({ args }: { args: InstallCommandArgs }) {
|
|
91
101
|
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
92
|
-
await performInstall(args["no-cache"], backend, args.tts, args.vad, args.diarize);
|
|
102
|
+
await performInstall(args["no-cache"], backend, args.tts, args.vad, args.diarize, args.plan);
|
|
93
103
|
},
|
|
94
104
|
});
|
package/src/cli/main.ts
CHANGED
|
@@ -5,18 +5,17 @@ import { transcribeWithSegments } from "../transcribe";
|
|
|
5
5
|
import { detectAudioLanguageEngine, detectTextLanguageEngine } from "../engine";
|
|
6
6
|
import type { LangDetectResult } from "../engine";
|
|
7
7
|
import { log } from "../log";
|
|
8
|
-
import type { TranscribeResult } from "../types";
|
|
8
|
+
import type { TranscribeErrorRecord, TranscribeResult } from "../types";
|
|
9
9
|
import {
|
|
10
10
|
formatJsonOutput,
|
|
11
11
|
formatTextOutput,
|
|
12
12
|
formatTranscriptOutput,
|
|
13
13
|
formatVerboseOutput,
|
|
14
14
|
} from "../format";
|
|
15
|
+
import { packageVersion } from "../package-info";
|
|
15
16
|
import { formatToonOutput } from "../toon";
|
|
16
17
|
import { artifactFromFile, createStatsRecorder } from "../stats";
|
|
17
18
|
|
|
18
|
-
const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
|
|
19
|
-
|
|
20
19
|
interface MainCommandArgs {
|
|
21
20
|
_: string[];
|
|
22
21
|
json: boolean;
|
|
@@ -25,8 +24,11 @@ interface MainCommandArgs {
|
|
|
25
24
|
debug: boolean;
|
|
26
25
|
vad: boolean;
|
|
27
26
|
"no-vad": boolean;
|
|
27
|
+
noVad?: boolean;
|
|
28
|
+
no_vad?: boolean;
|
|
28
29
|
timestamps: boolean;
|
|
29
30
|
speakers: boolean;
|
|
31
|
+
"include-errors": boolean;
|
|
30
32
|
format?: string;
|
|
31
33
|
lang?: string;
|
|
32
34
|
}
|
|
@@ -107,15 +109,17 @@ export function checkLanguageMismatch(expected: string | undefined, detected: st
|
|
|
107
109
|
export const mainCommand = defineCommand({
|
|
108
110
|
meta: {
|
|
109
111
|
name: "kesha",
|
|
110
|
-
version:
|
|
112
|
+
version: packageVersion,
|
|
111
113
|
description:
|
|
112
114
|
"Kesha Voice Kit — open-source voice toolkit for Apple Silicon.\n" +
|
|
113
115
|
"\n" +
|
|
114
116
|
"Commands:\n" +
|
|
117
|
+
" doctor Collect support diagnostics.\n" +
|
|
115
118
|
" install Download engine and models.\n" +
|
|
116
119
|
" status Inspect installed backend.\n" +
|
|
117
120
|
" say Synthesize speech from text.\n" +
|
|
118
|
-
" stats Manage local anonymous performance stats
|
|
121
|
+
" stats Manage local anonymous performance stats.\n" +
|
|
122
|
+
" support-bundle Create a redacted diagnostics archive.",
|
|
119
123
|
},
|
|
120
124
|
args: {
|
|
121
125
|
json: {
|
|
@@ -138,6 +142,11 @@ export const mainCommand = defineCommand({
|
|
|
138
142
|
description: "Include speaker labels in transcript segments. Requires --json / --toon / --format json. Implies --timestamps. Currently darwin-arm64 only (#199).",
|
|
139
143
|
default: false,
|
|
140
144
|
},
|
|
145
|
+
"include-errors": {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "With --json, output { results, errors } so scripts can read per-file failures without parsing stderr",
|
|
148
|
+
default: false,
|
|
149
|
+
},
|
|
141
150
|
verbose: {
|
|
142
151
|
type: "boolean",
|
|
143
152
|
description: "Show language detection details",
|
|
@@ -167,7 +176,7 @@ export const mainCommand = defineCommand({
|
|
|
167
176
|
default: false,
|
|
168
177
|
},
|
|
169
178
|
},
|
|
170
|
-
async run({ args }: { args: MainCommandArgs }) {
|
|
179
|
+
async run({ args, rawArgs }: { args: MainCommandArgs; rawArgs: string[] }) {
|
|
171
180
|
if (args.debug) log.debugEnabled = true;
|
|
172
181
|
const files = args._;
|
|
173
182
|
|
|
@@ -187,7 +196,12 @@ export const mainCommand = defineCommand({
|
|
|
187
196
|
}
|
|
188
197
|
const { wantsJson, wantsToon, wantsTranscript } = fmt;
|
|
189
198
|
|
|
190
|
-
|
|
199
|
+
// citty treats --no-vad as the negated form of --vad, so read rawArgs
|
|
200
|
+
// to distinguish "off" from the default auto mode and to catch both flags.
|
|
201
|
+
const vad = rawArgs.includes("--vad") || Boolean(args.vad);
|
|
202
|
+
const noVad = rawArgs.includes("--no-vad") || Boolean(args["no-vad"] ?? args.noVad ?? args.no_vad);
|
|
203
|
+
|
|
204
|
+
if (vad && noVad) {
|
|
191
205
|
log.error("--vad and --no-vad are mutually exclusive.");
|
|
192
206
|
process.exit(2);
|
|
193
207
|
}
|
|
@@ -199,21 +213,28 @@ export const mainCommand = defineCommand({
|
|
|
199
213
|
log.error("--speakers requires --json, --toon, or --format {json,toon}.");
|
|
200
214
|
process.exit(2);
|
|
201
215
|
}
|
|
202
|
-
|
|
216
|
+
if (args["include-errors"] && !wantsJson) {
|
|
217
|
+
log.error("--include-errors requires --json or --format json.");
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
const vadMode = vad ? "on" : noVad ? "off" : "auto";
|
|
203
221
|
|
|
204
222
|
if (files.length === 0) {
|
|
205
223
|
log.info(
|
|
206
224
|
"Usage: kesha <audio_file> [audio_file ...]\n" +
|
|
225
|
+
" kesha doctor [--json] [--redact]\n" +
|
|
207
226
|
" kesha install [--no-cache]\n" +
|
|
208
227
|
" kesha status\n" +
|
|
209
228
|
" kesha say <text>\n" +
|
|
210
|
-
" kesha stats [enable|disable|status|week|errors]"
|
|
229
|
+
" kesha stats [enable|disable|status|week|errors]\n" +
|
|
230
|
+
" kesha support-bundle [--output path.tar.gz]",
|
|
211
231
|
);
|
|
212
232
|
process.exit(1);
|
|
213
233
|
}
|
|
214
234
|
|
|
215
235
|
let hasError = false;
|
|
216
236
|
const results: TranscribeResult[] = [];
|
|
237
|
+
const errors: TranscribeErrorRecord[] = [];
|
|
217
238
|
const stats = createStatsRecorder("transcribe");
|
|
218
239
|
|
|
219
240
|
const wantsLangId = !!(args.lang || args.verbose || wantsJson || wantsToon || wantsTranscript);
|
|
@@ -223,6 +244,7 @@ export const mainCommand = defineCommand({
|
|
|
223
244
|
if (!existsSync(file)) {
|
|
224
245
|
hasError = true;
|
|
225
246
|
stats.recordError("input", new Error("File not found"), "file_not_found");
|
|
247
|
+
errors.push({ file, code: "file_not_found", message: "File not found" });
|
|
226
248
|
log.error(`${file}: File not found`);
|
|
227
249
|
continue;
|
|
228
250
|
}
|
|
@@ -290,12 +312,13 @@ export const mainCommand = defineCommand({
|
|
|
290
312
|
hasError = true;
|
|
291
313
|
stats.recordError("transcribe", err);
|
|
292
314
|
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
+
errors.push({ file, code: "transcribe_failed", message });
|
|
293
316
|
log.error(`${file}: ${message}`);
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
319
|
|
|
297
320
|
if (wantsJson) {
|
|
298
|
-
process.stdout.write(formatJsonOutput(results));
|
|
321
|
+
process.stdout.write(formatJsonOutput(results, args["include-errors"] ? errors : undefined));
|
|
299
322
|
} else if (wantsToon) {
|
|
300
323
|
process.stdout.write(formatToonOutput(results));
|
|
301
324
|
} else if (wantsTranscript) {
|
package/src/cli/say.ts
CHANGED
|
@@ -29,6 +29,55 @@ async function resolveText(inline: string | undefined): Promise<string> {
|
|
|
29
29
|
return new TextDecoder().decode(merged).trim();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function shouldRejectMissingSayText(
|
|
33
|
+
inlineText: string | undefined,
|
|
34
|
+
stdinIsTty: boolean | undefined,
|
|
35
|
+
): boolean {
|
|
36
|
+
return (inlineText === undefined || inlineText.length === 0) && stdinIsTty === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseFiniteNumberFlag(name: string, value: unknown): number | undefined {
|
|
40
|
+
if (value === undefined || value === null || value === false) return undefined;
|
|
41
|
+
const raw = String(value).trim();
|
|
42
|
+
const parsed = Number(raw);
|
|
43
|
+
if (raw === "" || !Number.isFinite(parsed)) {
|
|
44
|
+
log.error(`${name} must be a finite number.`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseRateFlag(value: unknown): number | undefined {
|
|
51
|
+
const rate = parseFiniteNumberFlag("--rate", value);
|
|
52
|
+
if (rate === undefined) return undefined;
|
|
53
|
+
if (rate < 0.5 || rate > 2.0) {
|
|
54
|
+
log.error("--rate must be between 0.5 and 2.0.");
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
return rate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseBitrateFlag(value: unknown): number | undefined {
|
|
61
|
+
const bitrate = parseFiniteNumberFlag("--bitrate", value);
|
|
62
|
+
if (bitrate === undefined) return undefined;
|
|
63
|
+
if (!Number.isInteger(bitrate) || bitrate <= 0) {
|
|
64
|
+
log.error("--bitrate must be a positive integer.");
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
return bitrate;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseSampleRateFlag(value: unknown): number | undefined {
|
|
71
|
+
const sampleRate = parseFiniteNumberFlag("--sample-rate", value);
|
|
72
|
+
if (sampleRate === undefined) return undefined;
|
|
73
|
+
const supported = [8000, 12000, 16000, 24000, 48000];
|
|
74
|
+
if (!Number.isInteger(sampleRate) || !supported.includes(sampleRate)) {
|
|
75
|
+
log.error("--sample-rate must be one of 8000, 12000, 16000, 24000, 48000.");
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
return sampleRate;
|
|
79
|
+
}
|
|
80
|
+
|
|
32
81
|
export const sayCommand = defineCommand({
|
|
33
82
|
meta: {
|
|
34
83
|
name: "say",
|
|
@@ -88,11 +137,6 @@ export const sayCommand = defineCommand({
|
|
|
88
137
|
process.exit(await proc.exited);
|
|
89
138
|
}
|
|
90
139
|
|
|
91
|
-
const inlineText = typeof args.text === "string" ? args.text : undefined;
|
|
92
|
-
const text = await resolveText(inlineText);
|
|
93
|
-
const explicitVoice = typeof args.voice === "string" ? args.voice : undefined;
|
|
94
|
-
const voice = explicitVoice ?? (await autoRouteVoice(text));
|
|
95
|
-
|
|
96
140
|
// Validate --format up front so we surface a clear error before spawning
|
|
97
141
|
// the engine subprocess. The engine repeats the check authoritatively, but
|
|
98
142
|
// catching it here gives the user a faster failure mode in scripts.
|
|
@@ -109,8 +153,12 @@ export const sayCommand = defineCommand({
|
|
|
109
153
|
}
|
|
110
154
|
}
|
|
111
155
|
|
|
156
|
+
const rate = parseRateFlag(args.rate);
|
|
157
|
+
const bitrate = parseBitrateFlag(args.bitrate);
|
|
158
|
+
const sampleRate = parseSampleRateFlag(args["sample-rate"]);
|
|
159
|
+
|
|
112
160
|
// Reject --bitrate / --sample-rate with WAV up front to surface the error fast.
|
|
113
|
-
const hasOpusOnlyFlag =
|
|
161
|
+
const hasOpusOnlyFlag = bitrate !== undefined || sampleRate !== undefined;
|
|
114
162
|
if (hasOpusOnlyFlag) {
|
|
115
163
|
const outExt = typeof args.out === "string"
|
|
116
164
|
? args.out.split(".").pop()?.toLowerCase()
|
|
@@ -122,16 +170,26 @@ export const sayCommand = defineCommand({
|
|
|
122
170
|
}
|
|
123
171
|
}
|
|
124
172
|
|
|
173
|
+
const inlineText = typeof args.text === "string" ? args.text : undefined;
|
|
174
|
+
const stdinIsTty = (process.stdin as { isTTY?: boolean }).isTTY;
|
|
175
|
+
if (shouldRejectMissingSayText(inlineText, stdinIsTty)) {
|
|
176
|
+
log.error("kesha say requires text or piped stdin. Usage: kesha say <text>");
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
const text = await resolveText(inlineText);
|
|
180
|
+
const explicitVoice = typeof args.voice === "string" ? args.voice : undefined;
|
|
181
|
+
const voice = explicitVoice ?? (await autoRouteVoice(text));
|
|
182
|
+
|
|
125
183
|
const opts = {
|
|
126
184
|
text,
|
|
127
185
|
voice,
|
|
128
186
|
lang: typeof args.lang === "string" ? args.lang : undefined,
|
|
129
187
|
out: typeof args.out === "string" ? args.out : undefined,
|
|
130
|
-
rate
|
|
188
|
+
rate,
|
|
131
189
|
ssml: Boolean(args.ssml),
|
|
132
190
|
format,
|
|
133
|
-
bitrate
|
|
134
|
-
sampleRate
|
|
191
|
+
bitrate,
|
|
192
|
+
sampleRate,
|
|
135
193
|
noExpandAbbrev: Boolean(args["no-expand-abbrev"]),
|
|
136
194
|
};
|
|
137
195
|
const stats = createStatsRecorder("say");
|
package/src/cli/status.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { showStatus } from "../status";
|
|
3
3
|
|
|
4
|
+
interface StatusCommandArgs {
|
|
5
|
+
disk: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export const statusCommand = defineCommand({
|
|
5
9
|
meta: {
|
|
6
10
|
name: "status",
|
|
7
11
|
description: "Show backend installation status",
|
|
8
12
|
},
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
args: {
|
|
14
|
+
disk: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "Include recursive cache disk usage",
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async run({ args }: { args: StatusCommandArgs }) {
|
|
21
|
+
await showStatus({ disk: args.disk });
|
|
11
22
|
},
|
|
12
23
|
});
|