@drakulavich/kesha-voice-kit 1.18.5 → 1.19.0-beta.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 +180 -136
- package/SKILL.md +57 -10
- package/completions/kesha.bash +2 -1
- package/completions/kesha.fish +11 -0
- package/completions/kesha.zsh +13 -0
- package/man/kesha.1 +29 -1
- package/package.json +2 -2
- package/src/cli/dispatch.ts +7 -0
- package/src/cli/init.ts +273 -0
- package/src/cli/install.ts +19 -5
- package/src/cli/main.ts +33 -19
- package/src/cli.ts +13 -0
- package/src/doctor.ts +7 -8
- package/src/engine-install.ts +45 -16
- package/src/install-plan.ts +5 -7
- package/src/shell-artifacts.ts +2 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- **Transcribe locally** — 25 languages, up to ~19x faster than Whisper on Apple Silicon, ~2.5x on CPU
|
|
17
17
|
- **Speak back** — Kokoro (EN), Vosk-TTS (RU), macOS system voices, and SSML preview
|
|
18
|
-
- **Plug into agents** — ship voice workflows as CLI commands or an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill
|
|
18
|
+
- **Plug into agents** — ship voice workflows as CLI commands or an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill; setup docs: [OpenClaw](docs/openclaw.md), [Hermes](docs/hermes.md)
|
|
19
19
|
- **Small Rust engine** — single ~20MB binary, no ffmpeg, no Python, no native Node addons
|
|
20
20
|
|
|
21
21
|
See [Product positioning](docs/product-positioning.md) for supported workflows, non-goals, maturity labels, and the platform matrix.
|
|
@@ -51,139 +51,10 @@ kesha audio.ogg # transcript to stdout
|
|
|
51
51
|
|
|
52
52
|
Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
|
|
53
53
|
|
|
54
|
-
##
|
|
55
|
-
|
|
56
|
-
The npm package includes bash, zsh, and fish completions plus `kesha(1)`.
|
|
57
|
-
The CLI can print the packaged files, so install paths do not depend on the
|
|
58
|
-
Bun global package layout:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
# bash
|
|
62
|
-
mkdir -p ~/.local/share/bash-completion/completions
|
|
63
|
-
kesha completions bash > ~/.local/share/bash-completion/completions/kesha
|
|
64
|
-
|
|
65
|
-
# zsh
|
|
66
|
-
mkdir -p ~/.zsh/completions
|
|
67
|
-
kesha completions zsh > ~/.zsh/completions/_kesha
|
|
68
|
-
# add to ~/.zshrc once: fpath=(~/.zsh/completions $fpath); autoload -Uz compinit; compinit
|
|
69
|
-
|
|
70
|
-
# fish
|
|
71
|
-
mkdir -p ~/.config/fish/completions
|
|
72
|
-
kesha completions fish > ~/.config/fish/completions/kesha.fish
|
|
73
|
-
|
|
74
|
-
# manpage
|
|
75
|
-
mkdir -p ~/.local/share/man/man1
|
|
76
|
-
kesha manpage > ~/.local/share/man/man1/kesha.1
|
|
77
|
-
mandb ~/.local/share/man 2>/dev/null || true
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Homebrew Install
|
|
81
|
-
|
|
82
|
-
Homebrew installs the Bun-based CLI wrapper. Engine and model downloads remain
|
|
83
|
-
explicit:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
brew tap oven-sh/bun
|
|
87
|
-
brew install drakulavich/tap/kesha-voice-kit
|
|
88
|
-
kesha install
|
|
89
|
-
kesha audio.ogg
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
See [Homebrew install](docs/homebrew.md) for package scope and maintainer
|
|
93
|
-
validation.
|
|
94
|
-
|
|
95
|
-
## Support diagnostics
|
|
96
|
-
|
|
97
|
-
Kesha can collect local diagnostics without downloading models or mutating cache state:
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
kesha doctor --json --redact
|
|
101
|
-
kesha support-bundle --output kesha-support.tar.gz
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
`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.
|
|
105
|
-
|
|
106
|
-
## Local Stats privacy and lifecycle
|
|
107
|
-
|
|
108
|
-
Kesha Stats is disabled by default. When you opt in with `kesha stats enable`,
|
|
109
|
-
Kesha writes a local SQLite database only on your machine:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
kesha stats status
|
|
113
|
-
kesha stats week
|
|
114
|
-
kesha stats errors
|
|
115
|
-
kesha stats export --format json # or csv
|
|
116
|
-
kesha stats retention 30 # default: 90 days
|
|
117
|
-
kesha stats retention off # keep until reset
|
|
118
|
-
kesha stats reset # delete recorded stats rows
|
|
119
|
-
kesha stats vacuum # compact the SQLite file
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
The database stores content-free operational records only: command name
|
|
123
|
-
(`transcribe` or `say`), timestamps, success/failure status, app version, item
|
|
124
|
-
count, anonymous stage timings, input/output artifact kind, file extension,
|
|
125
|
-
size, optional duration/sample-rate/channel counts, and sanitized error
|
|
126
|
-
class/code/message.
|
|
127
|
-
|
|
128
|
-
Stats never stores audio bytes, transcripts, input text, generated speech text,
|
|
129
|
-
file names, full file paths, raw stdout/stderr, environment variables, model
|
|
130
|
-
files, API tokens, or cloud identifiers. `support-bundle` reports Stats status
|
|
131
|
-
only; it never includes the Stats SQLite database.
|
|
132
|
-
|
|
133
|
-
By default, Stats prunes rows older than 90 days before writing or exporting
|
|
134
|
-
data. Use `kesha stats retention <days>` to change the TTL or `kesha stats
|
|
135
|
-
retention off` to disable TTL pruning. `kesha stats reset` deletes recorded
|
|
136
|
-
runs, artifacts, timings, and errors while preserving settings such as enabled
|
|
137
|
-
state and retention.
|
|
138
|
-
|
|
139
|
-
## Nix Install
|
|
140
|
-
|
|
141
|
-
Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
nix run github:drakulavich/kesha-voice-kit -- install # downloads models (engine is bundled)
|
|
145
|
-
nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
|
|
149
|
-
|
|
150
|
-
## Docker
|
|
151
|
-
|
|
152
|
-
Linux x64 CLI image, published to GHCR:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
docker run --rm \
|
|
156
|
-
-v kesha-cache:/cache/kesha \
|
|
157
|
-
-v "$PWD:/work" -w /work \
|
|
158
|
-
ghcr.io/drakulavich/kesha-voice-kit:latest install
|
|
159
|
-
|
|
160
|
-
docker run --rm \
|
|
161
|
-
-v kesha-cache:/cache/kesha \
|
|
162
|
-
-v "$PWD:/work" -w /work \
|
|
163
|
-
ghcr.io/drakulavich/kesha-voice-kit:latest audio.ogg
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
The image keeps model downloads and the engine cache under `/cache/kesha`.
|
|
167
|
-
Mount that path as a named volume so `kesha install`, TTS models, VAD, and future
|
|
168
|
-
runs reuse the same cache. `compose.yml` provides the same layout:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
docker compose run --rm kesha install
|
|
172
|
-
docker compose run --rm kesha audio.ogg
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Linux Packages
|
|
176
|
-
|
|
177
|
-
Stable engine releases also publish `.deb` and `.rpm` packages for Linux x64.
|
|
178
|
-
They install the standalone CLI wrapper; engine and model downloads remain explicit:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
kesha install
|
|
182
|
-
kesha audio.ogg
|
|
183
|
-
```
|
|
54
|
+
## Requirements
|
|
184
55
|
|
|
185
|
-
|
|
186
|
-
|
|
56
|
+
- [Bun](https://bun.sh) >= 1.3
|
|
57
|
+
- macOS arm64, Linux x64, or Windows x64
|
|
187
58
|
|
|
188
59
|
## Speech-to-text
|
|
189
60
|
|
|
@@ -265,6 +136,96 @@ Honored when `<prosody rate>` wraps the whole utterance. Mid-utterance prosody w
|
|
|
265
136
|
|
|
266
137
|
macOS system voices, SSML, voice listing, and the full voice catalogue: [docs/tts.md](docs/tts.md).
|
|
267
138
|
|
|
139
|
+
## Homebrew Install
|
|
140
|
+
|
|
141
|
+
Homebrew installs the Bun-based CLI wrapper. Engine and model downloads remain
|
|
142
|
+
explicit:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
brew tap oven-sh/bun
|
|
146
|
+
brew install drakulavich/tap/kesha-voice-kit
|
|
147
|
+
kesha install
|
|
148
|
+
kesha audio.ogg
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
See [Homebrew install](docs/homebrew.md) for package scope and maintainer
|
|
152
|
+
validation.
|
|
153
|
+
|
|
154
|
+
## Linux Packages
|
|
155
|
+
|
|
156
|
+
Stable engine releases also publish `.deb` and `.rpm` packages for Linux x64.
|
|
157
|
+
They install the standalone CLI wrapper; engine and model downloads remain explicit:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
kesha install
|
|
161
|
+
kesha audio.ogg
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
See [Linux packages](docs/linux-packages.md) for install commands and package
|
|
165
|
+
scope.
|
|
166
|
+
|
|
167
|
+
## Docker
|
|
168
|
+
|
|
169
|
+
Linux x64 CLI image, published to GHCR:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
docker run --rm \
|
|
173
|
+
-v kesha-cache:/cache/kesha \
|
|
174
|
+
-v "$PWD:/work" -w /work \
|
|
175
|
+
ghcr.io/drakulavich/kesha-voice-kit:latest install
|
|
176
|
+
|
|
177
|
+
docker run --rm \
|
|
178
|
+
-v kesha-cache:/cache/kesha \
|
|
179
|
+
-v "$PWD:/work" -w /work \
|
|
180
|
+
ghcr.io/drakulavich/kesha-voice-kit:latest audio.ogg
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The image keeps model downloads and the engine cache under `/cache/kesha`.
|
|
184
|
+
Mount that path as a named volume so `kesha install`, TTS models, VAD, and future
|
|
185
|
+
runs reuse the same cache. `compose.yml` provides the same layout:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
docker compose run --rm kesha install
|
|
189
|
+
docker compose run --rm kesha audio.ogg
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Nix Install
|
|
193
|
+
|
|
194
|
+
Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
nix run github:drakulavich/kesha-voice-kit -- install # downloads models (engine is bundled)
|
|
198
|
+
nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
|
|
202
|
+
|
|
203
|
+
## Shell Completions and Manpage
|
|
204
|
+
|
|
205
|
+
The npm package includes bash, zsh, and fish completions plus `kesha(1)`.
|
|
206
|
+
The CLI can print the packaged files, so install paths do not depend on the
|
|
207
|
+
Bun global package layout:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# bash
|
|
211
|
+
mkdir -p ~/.local/share/bash-completion/completions
|
|
212
|
+
kesha completions bash > ~/.local/share/bash-completion/completions/kesha
|
|
213
|
+
|
|
214
|
+
# zsh
|
|
215
|
+
mkdir -p ~/.zsh/completions
|
|
216
|
+
kesha completions zsh > ~/.zsh/completions/_kesha
|
|
217
|
+
# add to ~/.zshrc once: fpath=(~/.zsh/completions $fpath); autoload -Uz compinit; compinit
|
|
218
|
+
|
|
219
|
+
# fish
|
|
220
|
+
mkdir -p ~/.config/fish/completions
|
|
221
|
+
kesha completions fish > ~/.config/fish/completions/kesha.fish
|
|
222
|
+
|
|
223
|
+
# manpage
|
|
224
|
+
mkdir -p ~/.local/share/man/man1
|
|
225
|
+
kesha manpage > ~/.local/share/man/man1/kesha.1
|
|
226
|
+
mandb ~/.local/share/man 2>/dev/null || true
|
|
227
|
+
```
|
|
228
|
+
|
|
268
229
|
## Performance
|
|
269
230
|
|
|
270
231
|
> **Up to ~19x faster than Whisper** on Apple Silicon (M2), **~2.5x faster** on CPU
|
|
@@ -275,6 +236,50 @@ Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
|
|
|
275
236
|
|
|
276
237
|
See [BENCHMARK.md](BENCHMARK.md) for the full per-file breakdown (Russian + English).
|
|
277
238
|
|
|
239
|
+
## Architecture
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
Users / agents
|
|
243
|
+
shell | scripts | OpenClaw | Hermes | Raycast | @drakulavich/kesha-voice-kit/core
|
|
244
|
+
|
|
|
245
|
+
v
|
|
246
|
+
+------------------------------- Kesha CLI -------------------------------+
|
|
247
|
+
| Bun + TypeScript wrapper |
|
|
248
|
+
| - parses commands and formats stdout/stderr |
|
|
249
|
+
| - installs pinned engine/model assets only when explicitly requested |
|
|
250
|
+
| - keeps cache, support bundles, and local Stats in the CLI |
|
|
251
|
+
+-----------------------------------+-------------------------------------+
|
|
252
|
+
|
|
|
253
|
+
| spawns one local process
|
|
254
|
+
v
|
|
255
|
+
+----------------------------- kesha-engine ------------------------------+
|
|
256
|
+
| Rust binary, no cloud calls, no Python, no ffmpeg |
|
|
257
|
+
| |
|
|
258
|
+
| Audio input Text input Diagnostics |
|
|
259
|
+
| WAV/MP3/OGG/FLAC/AAC/M4A plain text / SSML status/support |
|
|
260
|
+
| | | | |
|
|
261
|
+
| v v v |
|
|
262
|
+
| Symphonia decode TTS preprocessing runtime probes |
|
|
263
|
+
| | | |
|
|
264
|
+
| +--> optional VAD +--> voice routing |
|
|
265
|
+
| | + diarization | Kokoro / Vosk / macOS voices |
|
|
266
|
+
| | | |
|
|
267
|
+
| +--> audio lang ID +--> speech synthesis |
|
|
268
|
+
| | SpeechBrain ONNX |
|
|
269
|
+
| | |
|
|
270
|
+
| +--> ASR backend |
|
|
271
|
+
| CoreML on Apple Silicon |
|
|
272
|
+
| ONNX Runtime on Linux/Windows/fallback |
|
|
273
|
+
+-----------------------------------+-------------------------------------+
|
|
274
|
+
|
|
|
275
|
+
v
|
|
276
|
+
transcript | JSON/TOON | WAV | local diagnostics
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Cache boundary: `kesha install` and opt-in feature installs populate the local
|
|
280
|
+
cache; ordinary transcription and speech commands fail fast if required assets
|
|
281
|
+
are missing.
|
|
282
|
+
|
|
278
283
|
## What's Inside
|
|
279
284
|
|
|
280
285
|
| Model | Task | Size | Source |
|
|
@@ -309,10 +314,49 @@ await downloadModel(); // install engine + models
|
|
|
309
314
|
const text = await transcribe("audio.ogg"); // transcribe
|
|
310
315
|
```
|
|
311
316
|
|
|
312
|
-
##
|
|
317
|
+
## Support diagnostics
|
|
313
318
|
|
|
314
|
-
|
|
315
|
-
|
|
319
|
+
Kesha can collect local diagnostics without downloading models or mutating cache state:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
kesha doctor --json --redact
|
|
323
|
+
kesha support-bundle --output kesha-support.tar.gz
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`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.
|
|
327
|
+
|
|
328
|
+
## Local Stats privacy and lifecycle
|
|
329
|
+
|
|
330
|
+
Kesha Stats is disabled by default. When you opt in with `kesha stats enable`,
|
|
331
|
+
Kesha writes a local SQLite database only on your machine:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
kesha stats status
|
|
335
|
+
kesha stats week
|
|
336
|
+
kesha stats errors
|
|
337
|
+
kesha stats export --format json # or csv
|
|
338
|
+
kesha stats retention 30 # default: 90 days
|
|
339
|
+
kesha stats retention off # keep until reset
|
|
340
|
+
kesha stats reset # delete recorded stats rows
|
|
341
|
+
kesha stats vacuum # compact the SQLite file
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The database stores content-free operational records only: command name
|
|
345
|
+
(`transcribe` or `say`), timestamps, success/failure status, app version, item
|
|
346
|
+
count, anonymous stage timings, input/output artifact kind, file extension,
|
|
347
|
+
size, optional duration/sample-rate/channel counts, and sanitized error
|
|
348
|
+
class/code/message.
|
|
349
|
+
|
|
350
|
+
Stats never stores audio bytes, transcripts, input text, generated speech text,
|
|
351
|
+
file names, full file paths, raw stdout/stderr, environment variables, model
|
|
352
|
+
files, API tokens, or cloud identifiers. `support-bundle` reports Stats status
|
|
353
|
+
only; it never includes the Stats SQLite database.
|
|
354
|
+
|
|
355
|
+
By default, Stats prunes rows older than 90 days before writing or exporting
|
|
356
|
+
data. Use `kesha stats retention <days>` to change the TTL or `kesha stats
|
|
357
|
+
retention off` to disable TTL pruning. `kesha stats reset` deletes recorded
|
|
358
|
+
runs, artifacts, timings, and errors while preserving settings such as enabled
|
|
359
|
+
state and retention.
|
|
316
360
|
|
|
317
361
|
## Contributing
|
|
318
362
|
|
package/SKILL.md
CHANGED
|
@@ -22,8 +22,8 @@ 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
|
|
26
|
-
- **Need
|
|
25
|
+
- **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into messenger-native OGG/Opus with `kesha say --format ogg-opus --out reply.ogg "<text>"`. Default is mono 24 kHz @ 32 kbps - what Telegram `sendVoice` expects. No WAV redirect and no `ffmpeg` round-trip.
|
|
26
|
+
- **Need local file playback/debug output**: WAV is still available with `kesha say --out reply.wav "<text>"`, but do not use WAV for Telegram voice replies. 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).
|
|
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
|
|
|
29
29
|
## OpenClaw plugin setup
|
|
@@ -34,12 +34,30 @@ Install the plugin, then explicitly route OpenClaw audio understanding through t
|
|
|
34
34
|
bun add -g @drakulavich/kesha-voice-kit
|
|
35
35
|
kesha install
|
|
36
36
|
openclaw plugins install @drakulavich/kesha-voice-kit
|
|
37
|
-
openclaw config
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
openclaw config patch --stdin <<'JSON5'
|
|
38
|
+
{
|
|
39
|
+
tools: {
|
|
40
|
+
media: {
|
|
41
|
+
audio: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
models: [
|
|
44
|
+
{
|
|
45
|
+
type: "cli",
|
|
46
|
+
command: "kesha",
|
|
47
|
+
args: ["{{MediaPath}}"],
|
|
48
|
+
timeoutSeconds: 15,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
echoTranscript: true,
|
|
52
|
+
echoFormat: '🦜 "{transcript}"',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
JSON5
|
|
40
58
|
```
|
|
41
59
|
|
|
42
|
-
Use
|
|
60
|
+
Use Kesha's default output for OpenClaw's normal voice-message path: stdout is the bare transcript text, while progress and errors stay off the transcript payload. The default setup echoes each transcript back to chat as `🦜 "{transcript}"` before the agent responds.
|
|
43
61
|
|
|
44
62
|
For agents that need timestamped segments, switch the model entry to JSON output and allow a longer timeout:
|
|
45
63
|
|
|
@@ -55,10 +73,39 @@ which kesha
|
|
|
55
73
|
kesha status
|
|
56
74
|
openclaw plugins list
|
|
57
75
|
openclaw config get tools.media.audio.models
|
|
76
|
+
openclaw config get tools.media.audio.echoTranscript
|
|
77
|
+
openclaw config get tools.media.audio.echoFormat
|
|
58
78
|
```
|
|
59
79
|
|
|
60
80
|
Do not rely on `openclaw.plugin.json` to patch `tools.media.audio.models`; OpenClaw ignores non-schema fields such as `configPatch`. Keep the CLI route in user config.
|
|
61
81
|
|
|
82
|
+
For OpenClaw TTS replies, route the local TTS provider to Kesha OGG/Opus output. This is the Telegram-safe path:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
openclaw config patch --stdin <<'JSON5'
|
|
86
|
+
{
|
|
87
|
+
messages: {
|
|
88
|
+
tts: {
|
|
89
|
+
auto: "always",
|
|
90
|
+
provider: "tts-local-cli",
|
|
91
|
+
providers: {
|
|
92
|
+
"tts-local-cli": {
|
|
93
|
+
command: "kesha",
|
|
94
|
+
args: ["say", "--format", "ogg-opus", "--out", "{{OutputPath}}", "{{Text}}"],
|
|
95
|
+
outputFormat: "opus",
|
|
96
|
+
timeoutMs: 120000,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
JSON5
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When invoking Kesha manually from an OpenClaw flow, write OGG/Opus into an OpenClaw-owned temp path, for example `kesha say --format ogg-opus --out /tmp/openclaw/reply.ogg "<text>"`, after ensuring the directory exists. The configured `tts-local-cli` provider should use OpenClaw's `{{OutputPath}}` placeholder instead of a hardcoded path.
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
62
109
|
## STT: transcribe audio
|
|
63
110
|
|
|
64
111
|
```bash
|
|
@@ -115,14 +162,14 @@ kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice —
|
|
|
115
162
|
kesha say --list-voices # Kokoro + Vosk-TTS + ~180 macos-* voices
|
|
116
163
|
```
|
|
117
164
|
|
|
118
|
-
Output: WAV mono float32 by default. `--out <path>` writes to a file instead of stdout.
|
|
165
|
+
Output: WAV mono float32 by default. `--out <path>` writes to a file instead of stdout. For Telegram/OpenClaw replies, prefer `--format ogg-opus --out reply.ogg` or the OpenClaw-provided `{{OutputPath}}`.
|
|
119
166
|
|
|
120
167
|
**Voice notes (Telegram / WhatsApp / Signal / Discord):** add `--format ogg-opus` to emit OGG/Opus directly — the format messenger APIs render as a native voice message:
|
|
121
168
|
|
|
122
169
|
```bash
|
|
123
|
-
kesha say
|
|
124
|
-
kesha say
|
|
125
|
-
kesha say
|
|
170
|
+
kesha say --format ogg-opus --out reply.ogg "Hello there" # 24 kHz @ 32 kbps mono - Telegram-grade
|
|
171
|
+
kesha say --voice ru-vosk-m02 --format ogg-opus --out reply.ogg "Привет" # Russian voice note
|
|
172
|
+
kesha say --format ogg-opus --bitrate 16000 --out tiny.ogg "Hi" # tinier file, intelligible but lossy
|
|
126
173
|
```
|
|
127
174
|
|
|
128
175
|
Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OGG/Opus). `--bitrate` (6 000–510 000 bps) and `--sample-rate` (8 000 / 12 000 / 16 000 / 24 000 / 48 000 Hz) tune the encoder.
|
package/completions/kesha.bash
CHANGED
|
@@ -6,7 +6,7 @@ _kesha_completion() {
|
|
|
6
6
|
COMPREPLY=()
|
|
7
7
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
8
|
command="${COMP_WORDS[1]}"
|
|
9
|
-
commands="completions doctor install manpage record say stats status support-bundle"
|
|
9
|
+
commands="completions doctor init install manpage record say stats status support-bundle"
|
|
10
10
|
|
|
11
11
|
if [[ "$COMP_CWORD" -eq 1 ]]; then
|
|
12
12
|
if [[ "$cur" == -* ]]; then
|
|
@@ -20,6 +20,7 @@ _kesha_completion() {
|
|
|
20
20
|
case "$command" in
|
|
21
21
|
completions) opts="--help -h" ;;
|
|
22
22
|
doctor) opts="--help -h --json --redact" ;;
|
|
23
|
+
init) opts="--help -h --coreml --onnx --no-cache --plan --yes --tts --vad --diarize" ;;
|
|
23
24
|
install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize" ;;
|
|
24
25
|
manpage) opts="--help -h" ;;
|
|
25
26
|
record) opts="--help -h --out --max-seconds --debug" ;;
|
package/completions/kesha.fish
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
complete -c kesha -f
|
|
3
3
|
complete -c kesha -n '__fish_use_subcommand' -a 'completions' -d 'Print shell completion script for bash, zsh, or fish'
|
|
4
4
|
complete -c kesha -n '__fish_use_subcommand' -a 'doctor' -d 'Collect support diagnostics without changing local state'
|
|
5
|
+
complete -c kesha -n '__fish_use_subcommand' -a 'init' -d 'Interactive setup guide for Kesha features'
|
|
5
6
|
complete -c kesha -n '__fish_use_subcommand' -a 'install' -d 'Download inference engine and models'
|
|
6
7
|
complete -c kesha -n '__fish_use_subcommand' -a 'manpage' -d 'Print the kesha(1) manpage'
|
|
7
8
|
complete -c kesha -n '__fish_use_subcommand' -a 'record' -d 'Record microphone audio to a WAV file'
|
|
@@ -30,6 +31,16 @@ complete -c kesha -n '__fish_seen_subcommand_from doctor' -l help -d 'Show help'
|
|
|
30
31
|
complete -c kesha -n '__fish_seen_subcommand_from doctor' -s h -d 'Show help'
|
|
31
32
|
complete -c kesha -n '__fish_seen_subcommand_from doctor' -l json -d 'Output diagnostics as JSON'
|
|
32
33
|
complete -c kesha -n '__fish_seen_subcommand_from doctor' -l redact -d 'Redact secrets and user-home paths from diagnostic output'
|
|
34
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l help -d 'Show help'
|
|
35
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -s h -d 'Show help'
|
|
36
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l coreml -d 'Preselect CoreML backend (macOS arm64)'
|
|
37
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l onnx -d 'Preselect ONNX backend'
|
|
38
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l no-cache -d 'Re-download even if cached'
|
|
39
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l plan -d 'Show the selected install plan without downloading'
|
|
40
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l yes -d 'Accept defaults and run without prompts'
|
|
41
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l tts -d 'Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
|
|
42
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l vad -d 'Preselect Silero VAD (~2.3MB) for long-audio preprocessing'
|
|
43
|
+
complete -c kesha -n '__fish_seen_subcommand_from init' -l diarize -d 'Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)'
|
|
33
44
|
complete -c kesha -n '__fish_seen_subcommand_from install' -l help -d 'Show help'
|
|
34
45
|
complete -c kesha -n '__fish_seen_subcommand_from install' -s h -d 'Show help'
|
|
35
46
|
complete -c kesha -n '__fish_seen_subcommand_from install' -l coreml -d 'Force CoreML backend (macOS arm64)'
|
package/completions/kesha.zsh
CHANGED
|
@@ -6,6 +6,7 @@ _kesha() {
|
|
|
6
6
|
commands=(
|
|
7
7
|
'completions:Print shell completion script for bash, zsh, or fish'
|
|
8
8
|
'doctor:Collect support diagnostics without changing local state'
|
|
9
|
+
'init:Interactive setup guide for Kesha features'
|
|
9
10
|
'install:Download inference engine and models'
|
|
10
11
|
'manpage:Print the kesha(1) manpage'
|
|
11
12
|
'record:Record microphone audio to a WAV file'
|
|
@@ -49,6 +50,18 @@ _kesha() {
|
|
|
49
50
|
'--json[Output diagnostics as JSON]' \
|
|
50
51
|
'--redact[Redact secrets and user-home paths from diagnostic output]'
|
|
51
52
|
;;
|
|
53
|
+
init)
|
|
54
|
+
_arguments '--help[Show help]' \
|
|
55
|
+
'-h[Show help]' \
|
|
56
|
+
'--coreml[Preselect CoreML backend (macOS arm64)]' \
|
|
57
|
+
'--onnx[Preselect ONNX backend]' \
|
|
58
|
+
'--no-cache[Re-download even if cached]' \
|
|
59
|
+
'--plan[Show the selected install plan without downloading]' \
|
|
60
|
+
'--yes[Accept defaults and run without prompts]' \
|
|
61
|
+
'--tts[Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)]' \
|
|
62
|
+
'--vad[Preselect Silero VAD (~2.3MB) for long-audio preprocessing]' \
|
|
63
|
+
'--diarize[Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)]'
|
|
64
|
+
;;
|
|
52
65
|
install)
|
|
53
66
|
_arguments '--help[Show help]' \
|
|
54
67
|
'-h[Show help]' \
|
package/man/kesha.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH KESHA 1 "May 2026" "kesha 1.
|
|
1
|
+
.TH KESHA 1 "May 2026" "kesha 1.19.0-beta.1" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
3
|
kesha \- open-source local voice toolkit
|
|
4
4
|
.SH SYNOPSIS
|
|
@@ -17,6 +17,9 @@ Print shell completion script for bash, zsh, or fish
|
|
|
17
17
|
.B doctor
|
|
18
18
|
Collect support diagnostics without changing local state
|
|
19
19
|
.TP
|
|
20
|
+
.B init
|
|
21
|
+
Interactive setup guide for Kesha features
|
|
22
|
+
.TP
|
|
20
23
|
.B install
|
|
21
24
|
Download inference engine and models
|
|
22
25
|
.TP
|
|
@@ -79,6 +82,31 @@ Output diagnostics as JSON
|
|
|
79
82
|
.TP
|
|
80
83
|
.B \-\-redact
|
|
81
84
|
Redact secrets and user\-home paths from diagnostic output
|
|
85
|
+
.SS init
|
|
86
|
+
.TP
|
|
87
|
+
.B \-\-coreml
|
|
88
|
+
Preselect CoreML backend (macOS arm64)
|
|
89
|
+
.TP
|
|
90
|
+
.B \-\-onnx
|
|
91
|
+
Preselect ONNX backend
|
|
92
|
+
.TP
|
|
93
|
+
.B \-\-no\-cache
|
|
94
|
+
Re\-download even if cached
|
|
95
|
+
.TP
|
|
96
|
+
.B \-\-plan
|
|
97
|
+
Show the selected install plan without downloading
|
|
98
|
+
.TP
|
|
99
|
+
.B \-\-yes
|
|
100
|
+
Accept defaults and run without prompts
|
|
101
|
+
.TP
|
|
102
|
+
.B \-\-tts
|
|
103
|
+
Preselect TTS models (Kokoro EN + Vosk\-TTS RU, ~990MB)
|
|
104
|
+
.TP
|
|
105
|
+
.B \-\-vad
|
|
106
|
+
Preselect Silero VAD (~2.3MB) for long\-audio preprocessing
|
|
107
|
+
.TP
|
|
108
|
+
.B \-\-diarize
|
|
109
|
+
Preselect Sortformer speaker diarization (~245MB, darwin\-arm64 only)
|
|
82
110
|
.SS install
|
|
83
111
|
.TP
|
|
84
112
|
.B \-\-coreml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0-beta.1",
|
|
4
4
|
"description": "Give your local tools a voice: fast STT in 25 languages (~19x faster than Whisper on Apple Silicon, ONNX CPU fallback), TTS via Kokoro/Vosk/macOS voices, VAD + 107-language detection. Rust engine, Bun CLI, OpenClaw skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
34
|
"keshaEngine": {
|
|
35
|
-
"version": "1.
|
|
35
|
+
"version": "1.19.0-beta.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "bun test",
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { log } from "../log";
|
|
|
4
4
|
import { suggestCommand } from "../suggest-command";
|
|
5
5
|
import { completionsCommand } from "./completions";
|
|
6
6
|
import { doctorCommand } from "./doctor";
|
|
7
|
+
import { initCommand } from "./init";
|
|
7
8
|
import { installCommand } from "./install";
|
|
8
9
|
import { manpageCommand } from "./manpage";
|
|
9
10
|
import { recordCommand } from "./record";
|
|
@@ -15,6 +16,7 @@ import { mainCommand } from "./main";
|
|
|
15
16
|
|
|
16
17
|
const SUBCOMMANDS = [
|
|
17
18
|
"doctor",
|
|
19
|
+
"init",
|
|
18
20
|
"install",
|
|
19
21
|
"status",
|
|
20
22
|
"record",
|
|
@@ -37,6 +39,11 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
if (firstArg === "init") {
|
|
43
|
+
await runMain(initCommand, { rawArgs: restArgs });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
if (firstArg === "completions") {
|
|
41
48
|
await runMain(completionsCommand, { rawArgs: restArgs });
|
|
42
49
|
return;
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { renderInstallPlan } from "../install-plan";
|
|
5
|
+
import { log } from "../log";
|
|
6
|
+
import { performInstall, resolveBackendFlag, resolveNoCacheFlag } from "./install";
|
|
7
|
+
|
|
8
|
+
export interface InitCommandArgs {
|
|
9
|
+
coreml: boolean;
|
|
10
|
+
onnx: boolean;
|
|
11
|
+
"no-cache": boolean;
|
|
12
|
+
noCache?: boolean;
|
|
13
|
+
no_cache?: boolean;
|
|
14
|
+
tts: boolean;
|
|
15
|
+
vad: boolean;
|
|
16
|
+
diarize: boolean;
|
|
17
|
+
plan: boolean;
|
|
18
|
+
yes: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InitSelection {
|
|
22
|
+
noCache: boolean;
|
|
23
|
+
backend?: string;
|
|
24
|
+
tts: boolean;
|
|
25
|
+
vad: boolean;
|
|
26
|
+
diarize: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PromptApi {
|
|
30
|
+
question(prompt: string): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function canInstallDiarizeOnPlatform(
|
|
34
|
+
platform = process.platform,
|
|
35
|
+
arch = process.arch,
|
|
36
|
+
): boolean {
|
|
37
|
+
return platform === "darwin" && arch === "arm64";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveInitSelection(
|
|
41
|
+
args: InitCommandArgs,
|
|
42
|
+
backend = resolveBackendFlag(args.coreml, args.onnx),
|
|
43
|
+
noCache = resolveNoCacheFlag(args),
|
|
44
|
+
): InitSelection {
|
|
45
|
+
return {
|
|
46
|
+
noCache,
|
|
47
|
+
backend,
|
|
48
|
+
tts: args.tts,
|
|
49
|
+
vad: args.vad,
|
|
50
|
+
diarize: args.diarize,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function initInstallArgs(selection: InitSelection): string[] {
|
|
55
|
+
return [
|
|
56
|
+
"kesha",
|
|
57
|
+
"install",
|
|
58
|
+
selection.noCache ? "--no-cache" : "",
|
|
59
|
+
selection.backend === "coreml" ? "--coreml" : "",
|
|
60
|
+
selection.backend === "onnx" ? "--onnx" : "",
|
|
61
|
+
selection.tts ? "--tts" : "",
|
|
62
|
+
selection.vad ? "--vad" : "",
|
|
63
|
+
selection.diarize ? "--diarize" : "",
|
|
64
|
+
].filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function initSuggestionCommands(
|
|
68
|
+
selection: InitSelection,
|
|
69
|
+
canDiarize = canInstallDiarizeOnPlatform(),
|
|
70
|
+
): string[][] {
|
|
71
|
+
const variants: InitSelection[] = [
|
|
72
|
+
selection,
|
|
73
|
+
{ ...selection, tts: false, vad: true, diarize: false },
|
|
74
|
+
{ ...selection, tts: true, vad: true, diarize: false },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
if (canDiarize) {
|
|
78
|
+
variants.push({ ...selection, tts: false, vad: true, diarize: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
return variants.map(initInstallArgs).filter((command) => {
|
|
83
|
+
const key = command.join("\0");
|
|
84
|
+
if (seen.has(key)) return false;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function omitUnsupportedDiarize(
|
|
91
|
+
selection: InitSelection,
|
|
92
|
+
canDiarize = canInstallDiarizeOnPlatform(),
|
|
93
|
+
): InitSelection {
|
|
94
|
+
return selection.diarize && !canDiarize ? { ...selection, diarize: false } : selection;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderInitOverview(canDiarize = canInstallDiarizeOnPlatform()): string {
|
|
98
|
+
const lines = [
|
|
99
|
+
"Kesha init",
|
|
100
|
+
"",
|
|
101
|
+
"Kesha is a local voice toolkit. The base install downloads the engine, speech-to-text models, and language detection models.",
|
|
102
|
+
"",
|
|
103
|
+
"Optional features:",
|
|
104
|
+
" - Text-to-speech: enables `kesha say` with Kokoro English and Vosk-TTS Russian voices (~990MB).",
|
|
105
|
+
" - VAD: skips silence in long audio and improves meeting, lecture, and podcast transcripts (~2.3MB).",
|
|
106
|
+
canDiarize
|
|
107
|
+
? " - Speaker diarization: labels speakers in JSON/TOON transcript segments (~245MB, darwin-arm64)."
|
|
108
|
+
: " - Speaker diarization: labels speakers, but the install is currently darwin-arm64 only.",
|
|
109
|
+
"",
|
|
110
|
+
"Nothing downloads until you confirm the final install plan.",
|
|
111
|
+
];
|
|
112
|
+
return `${lines.join("\n")}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function promptInitSelection(
|
|
116
|
+
args: InitCommandArgs,
|
|
117
|
+
prompt: PromptApi,
|
|
118
|
+
backend = resolveBackendFlag(args.coreml, args.onnx),
|
|
119
|
+
canDiarize = canInstallDiarizeOnPlatform(),
|
|
120
|
+
noCache = resolveNoCacheFlag(args),
|
|
121
|
+
): Promise<InitSelection> {
|
|
122
|
+
const tts = await askYesNo(prompt, "Install text-to-speech models for `kesha say`?", args.tts);
|
|
123
|
+
const vad = await askYesNo(prompt, "Install VAD for long or silence-heavy audio?", args.vad);
|
|
124
|
+
let diarize = false;
|
|
125
|
+
if (canDiarize) {
|
|
126
|
+
diarize = await askYesNo(prompt, "Install speaker diarization for `--speakers`?", args.diarize);
|
|
127
|
+
} else if (args.diarize) {
|
|
128
|
+
log.warn("--diarize is currently darwin-arm64 only; omitting it from the interactive install.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
noCache,
|
|
133
|
+
backend,
|
|
134
|
+
tts,
|
|
135
|
+
vad,
|
|
136
|
+
diarize,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function askYesNo(prompt: PromptApi, message: string, defaultValue: boolean): Promise<boolean> {
|
|
141
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
142
|
+
for (;;) {
|
|
143
|
+
const answer = (await prompt.question(`${message} [${suffix}] `)).trim().toLowerCase();
|
|
144
|
+
if (answer === "") return defaultValue;
|
|
145
|
+
if (answer === "y" || answer === "yes") return true;
|
|
146
|
+
if (answer === "n" || answer === "no") return false;
|
|
147
|
+
log.warn("Please answer yes or no.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function printPlan(selection: InitSelection): Promise<void> {
|
|
152
|
+
log.info(
|
|
153
|
+
await renderInstallPlan({
|
|
154
|
+
noCache: selection.noCache,
|
|
155
|
+
backend: selection.backend,
|
|
156
|
+
tts: selection.tts,
|
|
157
|
+
vad: selection.vad,
|
|
158
|
+
diarize: selection.diarize,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function runNonInteractive(selection: InitSelection): Promise<void> {
|
|
164
|
+
const canDiarize = canInstallDiarizeOnPlatform();
|
|
165
|
+
const printableSelection = omitUnsupportedDiarize(selection, canDiarize);
|
|
166
|
+
if (selection.diarize && !canDiarize) {
|
|
167
|
+
log.warn("--diarize is currently darwin-arm64 only; omitting it from non-interactive examples.");
|
|
168
|
+
}
|
|
169
|
+
log.info(renderInitOverview(canDiarize));
|
|
170
|
+
await printPlan(printableSelection);
|
|
171
|
+
log.info("Run one of these commands from an interactive terminal:");
|
|
172
|
+
for (const command of initSuggestionCommands(printableSelection, canDiarize)) {
|
|
173
|
+
log.info(` ${command.join(" ")}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const initCommand = defineCommand({
|
|
178
|
+
meta: {
|
|
179
|
+
name: "init",
|
|
180
|
+
description: "Interactive setup guide for Kesha features",
|
|
181
|
+
},
|
|
182
|
+
args: {
|
|
183
|
+
coreml: {
|
|
184
|
+
type: "boolean",
|
|
185
|
+
description: "Preselect CoreML backend (macOS arm64)",
|
|
186
|
+
default: false,
|
|
187
|
+
},
|
|
188
|
+
onnx: {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
description: "Preselect ONNX backend",
|
|
191
|
+
default: false,
|
|
192
|
+
},
|
|
193
|
+
"no-cache": {
|
|
194
|
+
type: "boolean",
|
|
195
|
+
description: "Re-download even if cached",
|
|
196
|
+
default: false,
|
|
197
|
+
},
|
|
198
|
+
plan: {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
description: "Show the selected install plan without downloading",
|
|
201
|
+
default: false,
|
|
202
|
+
},
|
|
203
|
+
yes: {
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "Accept defaults and run without prompts",
|
|
206
|
+
default: false,
|
|
207
|
+
},
|
|
208
|
+
tts: {
|
|
209
|
+
type: "boolean",
|
|
210
|
+
description: "Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)",
|
|
211
|
+
default: false,
|
|
212
|
+
},
|
|
213
|
+
vad: {
|
|
214
|
+
type: "boolean",
|
|
215
|
+
description: "Preselect Silero VAD (~2.3MB) for long-audio preprocessing",
|
|
216
|
+
default: false,
|
|
217
|
+
},
|
|
218
|
+
diarize: {
|
|
219
|
+
type: "boolean",
|
|
220
|
+
description: "Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)",
|
|
221
|
+
default: false,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
async run({ args, rawArgs }: { args: InitCommandArgs; rawArgs: string[] }) {
|
|
225
|
+
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
226
|
+
const noCache = resolveNoCacheFlag(args, rawArgs);
|
|
227
|
+
const selection = resolveInitSelection(args, backend, noCache);
|
|
228
|
+
|
|
229
|
+
if (args.plan) {
|
|
230
|
+
log.info(renderInitOverview());
|
|
231
|
+
await printPlan(selection);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (args.yes) {
|
|
236
|
+
const installSelection = omitUnsupportedDiarize(selection);
|
|
237
|
+
if (selection.diarize && !installSelection.diarize) {
|
|
238
|
+
log.warn("--diarize is currently darwin-arm64 only; omitting it from the --yes install.");
|
|
239
|
+
}
|
|
240
|
+
await performInstall(
|
|
241
|
+
installSelection.noCache,
|
|
242
|
+
installSelection.backend,
|
|
243
|
+
installSelection.tts,
|
|
244
|
+
installSelection.vad,
|
|
245
|
+
installSelection.diarize,
|
|
246
|
+
);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stdinIsTty = process.stdin.isTTY === true;
|
|
251
|
+
const stdoutIsTty = process.stdout.isTTY === true;
|
|
252
|
+
if (!stdinIsTty || !stdoutIsTty) {
|
|
253
|
+
await runNonInteractive(selection);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
log.info(renderInitOverview());
|
|
258
|
+
const rl = createInterface({ input, output });
|
|
259
|
+
try {
|
|
260
|
+
const prompted = await promptInitSelection(args, rl, backend, canInstallDiarizeOnPlatform(), noCache);
|
|
261
|
+
log.info("");
|
|
262
|
+
await printPlan(prompted);
|
|
263
|
+
const confirmed = await askYesNo(rl, `Run \`${initInstallArgs(prompted).join(" ")}\` now?`, true);
|
|
264
|
+
if (!confirmed) {
|
|
265
|
+
log.info(`Skipped install. Run later: ${initInstallArgs(prompted).join(" ")}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await performInstall(prompted.noCache, prompted.backend, prompted.tts, prompted.vad, prompted.diarize);
|
|
269
|
+
} finally {
|
|
270
|
+
rl.close();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
package/src/cli/install.ts
CHANGED
|
@@ -6,17 +6,31 @@ import { maybeAskForStar } from "../star";
|
|
|
6
6
|
import { log } from "../log";
|
|
7
7
|
import { packageVersion } from "../package-info";
|
|
8
8
|
|
|
9
|
-
interface InstallCommandArgs {
|
|
9
|
+
export interface InstallCommandArgs {
|
|
10
10
|
coreml: boolean;
|
|
11
11
|
onnx: boolean;
|
|
12
12
|
"no-cache": boolean;
|
|
13
|
+
noCache?: boolean;
|
|
14
|
+
no_cache?: boolean;
|
|
13
15
|
tts: boolean;
|
|
14
16
|
vad: boolean;
|
|
15
17
|
diarize: boolean;
|
|
16
18
|
plan: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function
|
|
21
|
+
export function resolveNoCacheFlag(
|
|
22
|
+
args: Pick<InstallCommandArgs, "no-cache" | "noCache" | "no_cache">,
|
|
23
|
+
rawArgs: string[] = [],
|
|
24
|
+
): boolean {
|
|
25
|
+
return (
|
|
26
|
+
rawArgs.includes("--no-cache") ||
|
|
27
|
+
args["no-cache"] === true ||
|
|
28
|
+
args.noCache === true ||
|
|
29
|
+
args.no_cache === true
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
|
|
20
34
|
if (coreml && onnx) {
|
|
21
35
|
log.error('Choose only one backend: "--coreml" or "--onnx".');
|
|
22
36
|
process.exit(1);
|
|
@@ -32,7 +46,7 @@ function defaultBackendForPlatform(): string | undefined {
|
|
|
32
46
|
return undefined;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
async function performInstall(
|
|
49
|
+
export async function performInstall(
|
|
36
50
|
noCache: boolean,
|
|
37
51
|
backend?: string,
|
|
38
52
|
tts = false,
|
|
@@ -111,8 +125,8 @@ export const installCommand = defineCommand({
|
|
|
111
125
|
default: false,
|
|
112
126
|
},
|
|
113
127
|
},
|
|
114
|
-
async run({ args }: { args: InstallCommandArgs }) {
|
|
128
|
+
async run({ args, rawArgs }: { args: InstallCommandArgs; rawArgs: string[] }) {
|
|
115
129
|
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
116
|
-
await performInstall(args
|
|
130
|
+
await performInstall(resolveNoCacheFlag(args, rawArgs), backend, args.tts, args.vad, args.diarize, args.plan);
|
|
117
131
|
},
|
|
118
132
|
});
|
package/src/cli/main.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { formatToonOutput } from "../toon";
|
|
|
17
17
|
import { artifactFromFile, createStatsRecorder } from "../stats";
|
|
18
18
|
import { createPercentProgress } from "../progress";
|
|
19
19
|
import { getPendingSignalExitCode, waitForPendingSignalCleanup } from "../process-tree";
|
|
20
|
+
import type { TranscriptionSegment } from "../types";
|
|
20
21
|
|
|
21
22
|
interface MainCommandArgs {
|
|
22
23
|
_: string[];
|
|
@@ -117,6 +118,25 @@ export function shouldReportTranscribeProgress(input: {
|
|
|
117
118
|
return input.stderrIsTty || !input.stdoutIsTty;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
const AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS = 10 * 60;
|
|
122
|
+
|
|
123
|
+
export function estimateTranscriptDurationSeconds(segments: TranscriptionSegment[]): number | null {
|
|
124
|
+
const duration = segments.reduce((max, segment) => {
|
|
125
|
+
const end = Number(segment.end);
|
|
126
|
+
return Number.isFinite(end) && end > max ? end : max;
|
|
127
|
+
}, 0);
|
|
128
|
+
return duration > 0 ? duration : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function shouldRunAudioLanguageDetection(input: {
|
|
132
|
+
wantsLangId: boolean;
|
|
133
|
+
transcriptDurationSeconds: number | null;
|
|
134
|
+
}): boolean {
|
|
135
|
+
if (!input.wantsLangId) return false;
|
|
136
|
+
if (input.transcriptDurationSeconds === null) return true;
|
|
137
|
+
return input.transcriptDurationSeconds <= AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS;
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
export const mainCommand = defineCommand({
|
|
121
141
|
meta: {
|
|
122
142
|
name: "kesha",
|
|
@@ -127,6 +147,7 @@ export const mainCommand = defineCommand({
|
|
|
127
147
|
"Commands:\n" +
|
|
128
148
|
" completions Print shell completion script.\n" +
|
|
129
149
|
" doctor Collect support diagnostics.\n" +
|
|
150
|
+
" init Interactive setup guide for Kesha features.\n" +
|
|
130
151
|
" install Download engine and models.\n" +
|
|
131
152
|
" manpage Print the kesha(1) manpage.\n" +
|
|
132
153
|
" record Record microphone audio to a WAV file.\n" +
|
|
@@ -285,32 +306,25 @@ export const mainCommand = defineCommand({
|
|
|
285
306
|
estimatedTotalMs: args.speakers ? 60 * 60 * 1000 : 30 * 60 * 1000,
|
|
286
307
|
})
|
|
287
308
|
: null;
|
|
288
|
-
|
|
289
|
-
// the sibling engine process alive if one side fails first.
|
|
290
|
-
const engineAbort = new AbortController();
|
|
291
|
-
const audioPromise = wantsLangId
|
|
292
|
-
? stats.timeStage("lang_id_audio", () =>
|
|
293
|
-
detectAudioLanguageEngine(file, { signal: engineAbort.signal })
|
|
294
|
-
)
|
|
295
|
-
: Promise.resolve(null);
|
|
296
|
-
const transcriptPromise = stats.timeStage("transcribe", () =>
|
|
309
|
+
const transcript = await stats.timeStage("transcribe", () =>
|
|
297
310
|
transcribeWithSegments(file, {
|
|
298
311
|
vad: vadMode,
|
|
299
|
-
signal: engineAbort.signal,
|
|
300
312
|
timestamps: args.timestamps,
|
|
301
313
|
speakers: args.speakers,
|
|
302
314
|
})
|
|
303
315
|
);
|
|
304
|
-
let audioResult: LangDetectResult | null;
|
|
305
|
-
let transcript: Awaited<ReturnType<typeof transcribeWithSegments>>;
|
|
306
|
-
try {
|
|
307
|
-
[audioResult, transcript] = await Promise.all([audioPromise, transcriptPromise]);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
engineAbort.abort();
|
|
310
|
-
await Promise.allSettled([audioPromise, transcriptPromise]);
|
|
311
|
-
throw err;
|
|
312
|
-
}
|
|
313
316
|
const { text, segments } = transcript;
|
|
317
|
+
const transcriptDurationSeconds = estimateTranscriptDurationSeconds(segments);
|
|
318
|
+
|
|
319
|
+
let audioResult: LangDetectResult | null = null;
|
|
320
|
+
if (shouldRunAudioLanguageDetection({ wantsLangId, transcriptDurationSeconds })) {
|
|
321
|
+
audioResult = await stats.timeStage("lang_id_audio", () => detectAudioLanguageEngine(file));
|
|
322
|
+
} else if (wantsLangId) {
|
|
323
|
+
log.debug(
|
|
324
|
+
`skip lang_id_audio for ${file}: transcript duration ${transcriptDurationSeconds?.toFixed(1)}s exceeds ` +
|
|
325
|
+
`${AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS}s`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
314
328
|
|
|
315
329
|
let audioLanguage: LangDetectResult | undefined;
|
|
316
330
|
if (audioResult && audioResult.code) {
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
// public surface is unchanged.
|
|
6
6
|
export { doctorCommand } from "./cli/doctor";
|
|
7
7
|
export { completionsCommand } from "./cli/completions";
|
|
8
|
+
export {
|
|
9
|
+
canInstallDiarizeOnPlatform,
|
|
10
|
+
initCommand,
|
|
11
|
+
initInstallArgs,
|
|
12
|
+
initSuggestionCommands,
|
|
13
|
+
omitUnsupportedDiarize,
|
|
14
|
+
promptInitSelection,
|
|
15
|
+
renderInitOverview,
|
|
16
|
+
resolveInitSelection,
|
|
17
|
+
} from "./cli/init";
|
|
18
|
+
export type { InitCommandArgs, InitSelection } from "./cli/init";
|
|
8
19
|
export { installCommand } from "./cli/install";
|
|
9
20
|
export { manpageCommand } from "./cli/manpage";
|
|
10
21
|
export { recordCommand, resolveRecordArgs } from "./cli/record";
|
|
@@ -18,7 +29,9 @@ export {
|
|
|
18
29
|
mainCommand,
|
|
19
30
|
detectLanguage,
|
|
20
31
|
checkLanguageMismatch,
|
|
32
|
+
estimateTranscriptDurationSeconds,
|
|
21
33
|
resolveOutputFormat,
|
|
34
|
+
shouldRunAudioLanguageDetection,
|
|
22
35
|
shouldReportTranscribeProgress,
|
|
23
36
|
} from "./cli/main";
|
|
24
37
|
export type { ResolvedOutputFormat } from "./cli/main";
|
package/src/doctor.ts
CHANGED
|
@@ -257,20 +257,19 @@ function collectOptionalComponents(redact: boolean): OptionalComponent[] {
|
|
|
257
257
|
...pathSummary(join(cache, "models/vosk-ru")),
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
// Diarization (#199) and Kokoro (#207) run in-engine now (native
|
|
261
|
+
// fluidaudio-rs) — no Swift sidecar binaries. The Sortformer model is
|
|
262
|
+
// the only installable diarization artifact; Kokoro is covered by the
|
|
263
|
+
// "TTS (Kokoro)" entry above.
|
|
264
|
+
name: "Diarization (Sortformer)",
|
|
265
|
+
note: "enabled with `kesha install --diarize` (darwin-arm64); runs in-engine",
|
|
266
|
+
...pathSummary(join(cache, "models/diarize")),
|
|
263
267
|
},
|
|
264
268
|
{
|
|
265
269
|
name: "AVSpeech sidecar",
|
|
266
270
|
note: "macOS voices",
|
|
267
271
|
...pathSummary(join(sidecarDir, "say-avspeech")),
|
|
268
272
|
},
|
|
269
|
-
{
|
|
270
|
-
name: "FluidAudio Kokoro sidecar",
|
|
271
|
-
note: "darwin-arm64 Kokoro",
|
|
272
|
-
...pathSummary(join(sidecarDir, "kesha-kokoro")),
|
|
273
|
-
},
|
|
274
273
|
{
|
|
275
274
|
name: "Text language sidecar",
|
|
276
275
|
note: "darwin text language fast path",
|
package/src/engine-install.ts
CHANGED
|
@@ -69,20 +69,9 @@ const SIDECARS: SidecarSpec[] = [
|
|
|
69
69
|
availableHint: "macOS voices available",
|
|
70
70
|
unavailableHint: "macos-* voices unavailable",
|
|
71
71
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
displayName: "Diarization sidecar",
|
|
76
|
-
availableHint: "--speakers available",
|
|
77
|
-
unavailableHint: "--speakers unavailable",
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
fileBasename: "kesha-kokoro",
|
|
81
|
-
assetName: "kesha-kokoro-darwin-arm64",
|
|
82
|
-
displayName: "FluidAudio Kokoro sidecar",
|
|
83
|
-
availableHint: "Kokoro uses FluidAudio CoreML",
|
|
84
|
-
unavailableHint: "en-* Kokoro voices unavailable on Darwin",
|
|
85
|
-
},
|
|
72
|
+
// Kokoro TTS (#207) and speaker diarization (#199) no longer ship as Swift
|
|
73
|
+
// sidecars — both run in-engine via the native `fluidaudio-rs` binding. Only
|
|
74
|
+
// the AVSpeech and text-lang sidecars remain.
|
|
86
75
|
{
|
|
87
76
|
// Runtime resolver looks for plain `kesha-textlang` next to the engine
|
|
88
77
|
// (see `rust/src/text_lang.rs::helper_path`), not the platform-suffixed
|
|
@@ -99,6 +88,40 @@ const SIDECARS: SidecarSpec[] = [
|
|
|
99
88
|
},
|
|
100
89
|
];
|
|
101
90
|
|
|
91
|
+
const RETIRED_SIDECAR_FILENAMES = [
|
|
92
|
+
// Historical installed filenames.
|
|
93
|
+
"kesha-kokoro",
|
|
94
|
+
"kesha-diarize",
|
|
95
|
+
// Historical release-asset filenames. Keep these explicit: AVSpeech and
|
|
96
|
+
// text-lang helpers are still active and must not be swept up by a glob.
|
|
97
|
+
"kesha-kokoro-darwin-arm64",
|
|
98
|
+
"kesha-diarize-darwin-arm64",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export function cleanupRetiredSidecars(engineDir: string): string[] {
|
|
102
|
+
const removed: string[] = [];
|
|
103
|
+
|
|
104
|
+
for (const filename of RETIRED_SIDECAR_FILENAMES) {
|
|
105
|
+
const path = join(engineDir, filename);
|
|
106
|
+
if (!existsSync(path)) continue;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
rmSync(path);
|
|
110
|
+
removed.push(filename);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
log.warn(
|
|
113
|
+
`Could not remove retired sidecar ${filename} (${e instanceof Error ? e.message : e}); continuing.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (removed.length > 0) {
|
|
119
|
+
log.success(`Removed retired sidecars: ${removed.join(", ")}.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return removed;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
/**
|
|
103
126
|
* Make a freshly-downloaded Mach-O runnable on macOS 15+ Sequoia.
|
|
104
127
|
*
|
|
@@ -232,8 +255,10 @@ async function downloadSidecar(
|
|
|
232
255
|
|
|
233
256
|
async function warmDarwinKokoro(binPath: string): Promise<void> {
|
|
234
257
|
if (process.platform !== "darwin" || process.arch !== "arm64") return;
|
|
235
|
-
|
|
236
|
-
|
|
258
|
+
// Kokoro now runs in-engine (FluidAudio CoreML, system_kokoro) — warm it by
|
|
259
|
+
// exercising the engine's own `say`, not a sidecar. The first synthesis
|
|
260
|
+
// compiles/fetches FluidAudio's CoreML Kokoro cache.
|
|
261
|
+
if (!existsSync(binPath)) return;
|
|
237
262
|
|
|
238
263
|
const outPath = join(tmpdir(), `kesha-kokoro-warmup-${process.pid}.wav`);
|
|
239
264
|
log.progress("Warming FluidAudio Kokoro CoreML cache...");
|
|
@@ -499,6 +524,10 @@ export async function downloadEngine(
|
|
|
499
524
|
await warmDarwinKokoro(binPath);
|
|
500
525
|
}
|
|
501
526
|
|
|
527
|
+
if (canWriteEngineDir) {
|
|
528
|
+
cleanupRetiredSidecars(engineDir);
|
|
529
|
+
}
|
|
530
|
+
|
|
502
531
|
log.success("Backend installed successfully.");
|
|
503
532
|
return binPath;
|
|
504
533
|
}
|
package/src/install-plan.ts
CHANGED
|
@@ -81,10 +81,10 @@ const DIARIZE_FILES: PlanFile[] = [
|
|
|
81
81
|
},
|
|
82
82
|
];
|
|
83
83
|
|
|
84
|
+
// Kokoro (#207) and diarization (#199) are in-engine now (native fluidaudio-rs),
|
|
85
|
+
// so only AVSpeech and text-lang ship as separate Swift-sidecar assets.
|
|
84
86
|
const DARWIN_SIDECARS: ReleaseAssetSpec[] = [
|
|
85
87
|
{ assetName: "say-avspeech-darwin-arm64", sizeBytes: 63_056 },
|
|
86
|
-
{ assetName: "kesha-diarize-darwin-arm64", sizeBytes: 7_228_000 },
|
|
87
|
-
{ assetName: "kesha-kokoro-darwin-arm64", sizeBytes: 7_189_648 },
|
|
88
88
|
{ assetName: "kesha-textlang-darwin-arm64", sizeBytes: 57_648 },
|
|
89
89
|
];
|
|
90
90
|
|
|
@@ -123,8 +123,6 @@ function engineAssetForPlatform(): ReleaseAssetSpec | null {
|
|
|
123
123
|
|
|
124
124
|
function sidecarFilename(assetName: string): string {
|
|
125
125
|
if (assetName === "say-avspeech-darwin-arm64") return "say-avspeech";
|
|
126
|
-
if (assetName === "kesha-diarize-darwin-arm64") return "kesha-diarize-darwin-arm64";
|
|
127
|
-
if (assetName === "kesha-kokoro-darwin-arm64") return "kesha-kokoro";
|
|
128
126
|
if (assetName === "kesha-textlang-darwin-arm64") return "kesha-textlang";
|
|
129
127
|
return assetName;
|
|
130
128
|
}
|
|
@@ -234,11 +232,11 @@ export async function renderInstallPlan(options: InstallPlanOptions = {}): Promi
|
|
|
234
232
|
);
|
|
235
233
|
components.push({
|
|
236
234
|
name: "TTS Kokoro EN",
|
|
237
|
-
source: "FluidAudio CoreML
|
|
235
|
+
source: "FluidAudio CoreML (in-engine)",
|
|
238
236
|
sizeBytes: 0,
|
|
239
|
-
cached: existsSync(
|
|
237
|
+
cached: existsSync(binPath),
|
|
240
238
|
refresh: false,
|
|
241
|
-
note: "no Kokoro ONNX model is downloaded by Kesha on darwin-arm64; warm-up may compile/update FluidAudio's CoreML cache",
|
|
239
|
+
note: "Kokoro runs in-engine via FluidAudio CoreML; no Kokoro ONNX model is downloaded by Kesha on darwin-arm64; warm-up may compile/update FluidAudio's CoreML cache",
|
|
242
240
|
});
|
|
243
241
|
} else {
|
|
244
242
|
components.push(
|
package/src/shell-artifacts.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ArgDef, ArgsDef, CommandDef, CommandMeta, Resolvable } from "citty";
|
|
2
2
|
import { completionsCommand } from "./cli/completions";
|
|
3
3
|
import { doctorCommand } from "./cli/doctor";
|
|
4
|
+
import { initCommand } from "./cli/init";
|
|
4
5
|
import { installCommand } from "./cli/install";
|
|
5
6
|
import { mainCommand } from "./cli/main";
|
|
6
7
|
import { manpageCommand } from "./cli/manpage";
|
|
@@ -44,6 +45,7 @@ export interface ShellArtifact {
|
|
|
44
45
|
const CLI_COMMANDS: CliCommand[] = [
|
|
45
46
|
{ name: "completions", command: completionsCommand },
|
|
46
47
|
{ name: "doctor", command: doctorCommand },
|
|
48
|
+
{ name: "init", command: initCommand },
|
|
47
49
|
{ name: "install", command: installCommand },
|
|
48
50
|
{ name: "manpage", command: manpageCommand },
|
|
49
51
|
{ name: "record", command: recordCommand },
|