@drakulavich/kesha-voice-kit 1.23.0 → 1.24.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 +2 -1
- package/completions/kesha.bash +2 -2
- package/completions/kesha.fish +5 -3
- package/completions/kesha.zsh +11 -7
- package/man/kesha.1 +9 -3
- package/package.json +3 -1
- package/src/cli/dispatch.ts +88 -1
- package/src/cli/init.ts +66 -12
- package/src/cli/install.ts +73 -9
- package/src/cli/main.ts +31 -3
- package/src/cli/say.ts +6 -5
- package/src/cli/support-bundle.ts +2 -1
- package/src/diagnostic-log.ts +2 -1
- package/src/doctor.ts +5 -15
- package/src/engine-install.ts +156 -114
- package/src/engine.ts +8 -1
- package/src/error-utils.ts +4 -0
- package/src/format.ts +13 -0
- package/src/install-plan.ts +100 -84
- package/src/lib.ts +7 -3
- package/src/log.ts +40 -5
- package/src/mcp/tools.ts +5 -8
- package/src/stats.ts +71 -98
package/README.md
CHANGED
|
@@ -125,7 +125,8 @@ All of these install the Bun CLI wrapper; engine + models still download explici
|
|
|
125
125
|
- [Architecture](docs/architecture.md) — runtime data flow, the models that ship, the CLI ↔ Rust engine boundary, model pinning, and where tests live.
|
|
126
126
|
- [Use cases](docs/use-cases.md) — copy-paste recipes (transcribe a meeting, speak from OpenClaw, run offline, move the cache).
|
|
127
127
|
- [Product positioning](docs/product-positioning.md) — supported workflows, non-goals, maturity labels, platform matrix.
|
|
128
|
-
- **Diagnostics:** `kesha doctor`, `kesha support-bundle` (redacted `.tar.gz` for issues), and `kesha logs` produce local, content-free diagnostics — see [docs/diagnostic-logs.md](docs/diagnostic-logs.md). Every failure prints a stable `error [CODE]: …` line
|
|
128
|
+
- **Diagnostics:** `kesha doctor`, `kesha support-bundle` (redacted `.tar.gz` for issues), and `kesha logs` produce local, content-free diagnostics — see [docs/diagnostic-logs.md](docs/diagnostic-logs.md). Every failure prints a stable `error [CODE]: …` line and a documented [process exit code](docs/errors.md#process-exit-codes).
|
|
129
|
+
- **Scripting & CI:** `--json` (or `--toon`) for machine-readable output, `--quiet`/`-q` to silence progress, and `--no-color` (or `NO_COLOR=1`) for plain logs. Colors switch off automatically when `CI=true`.
|
|
129
130
|
- **Privacy / Local Stats:** Stats are **off by default** and fully local. Opt in with `kesha stats enable` to record content-free operational metrics in a local SQLite database — never networked, never storing audio, transcripts, text, or paths. Full commands & lifecycle: [docs/local-stats.md](docs/local-stats.md).
|
|
130
131
|
|
|
131
132
|
## Contributing
|
package/completions/kesha.bash
CHANGED
|
@@ -10,7 +10,7 @@ _kesha_completion() {
|
|
|
10
10
|
|
|
11
11
|
if [[ "$COMP_CWORD" -eq 1 ]]; then
|
|
12
12
|
if [[ "$cur" == -* ]]; then
|
|
13
|
-
COMPREPLY=( $(compgen -W "--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad" -- "$cur") )
|
|
13
|
+
COMPREPLY=( $(compgen -W "--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad --quiet --no-color" -- "$cur") )
|
|
14
14
|
else
|
|
15
15
|
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
16
16
|
fi
|
|
@@ -30,7 +30,7 @@ _kesha_completion() {
|
|
|
30
30
|
stats) opts="--help -h --format" ;;
|
|
31
31
|
status) opts="--help -h --disk" ;;
|
|
32
32
|
support-bundle) opts="--help -h --output --include-logs" ;;
|
|
33
|
-
*) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad" ;;
|
|
33
|
+
*) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad --quiet --no-color" ;;
|
|
34
34
|
esac
|
|
35
35
|
|
|
36
36
|
if [[ "$cur" == -* ]]; then
|
package/completions/kesha.fish
CHANGED
|
@@ -19,14 +19,16 @@ complete -c kesha -n '__fish_use_subcommand' -s v -d 'Show version'
|
|
|
19
19
|
complete -c kesha -n '__fish_use_subcommand' -l json -d 'Output results as JSON'
|
|
20
20
|
complete -c kesha -n '__fish_use_subcommand' -l toon -d 'Output results as TOON (compact, LLM-friendly encoding of the same data as --json)'
|
|
21
21
|
complete -c kesha -n '__fish_use_subcommand' -l timestamps -d 'Include timestamped transcript segments in JSON/TOON output'
|
|
22
|
-
complete -c kesha -n '__fish_use_subcommand' -l speakers -d 'Include speaker labels in
|
|
22
|
+
complete -c kesha -n '__fish_use_subcommand' -l speakers -d 'Include speaker labels in segments. Needs --json/--toon and darwin-arm64; run `kesha install --diarize` first. Implies --timestamps.'
|
|
23
23
|
complete -c kesha -n '__fish_use_subcommand' -l include-errors -d 'With --json, output { results, errors } so scripts can read per-file failures without parsing stderr'
|
|
24
24
|
complete -c kesha -n '__fish_use_subcommand' -l verbose -d 'Show language detection details'
|
|
25
25
|
complete -c kesha -n '__fish_use_subcommand' -l format -r -d 'Output format: transcript | json | toon (long-form alias for --json / --toon)'
|
|
26
|
-
complete -c kesha -n '__fish_use_subcommand' -l lang -r -d 'Expected language code (
|
|
26
|
+
complete -c kesha -n '__fish_use_subcommand' -l lang -r -d 'Expected language code, e.g. en or en-us (see docs/languages.md); warn if mismatch'
|
|
27
27
|
complete -c kesha -n '__fish_use_subcommand' -l debug -d 'Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)'
|
|
28
28
|
complete -c kesha -n '__fish_use_subcommand' -l vad -d 'Force Silero VAD preprocessing (kesha install --vad first). Without this, VAD auto-engages on audio ≥ 120s.'
|
|
29
29
|
complete -c kesha -n '__fish_use_subcommand' -l no-vad -d 'Force full-file ASR for short/medium files; long audio fails early'
|
|
30
|
+
complete -c kesha -n '__fish_use_subcommand' -l quiet -d 'Suppress progress output; print only results and errors'
|
|
31
|
+
complete -c kesha -n '__fish_use_subcommand' -l no-color -d 'Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)'
|
|
30
32
|
complete -c kesha -n '__fish_seen_subcommand_from completions' -l help -d 'Show help'
|
|
31
33
|
complete -c kesha -n '__fish_seen_subcommand_from completions' -s h -d 'Show help'
|
|
32
34
|
complete -c kesha -n '__fish_seen_subcommand_from doctor' -l help -d 'Show help'
|
|
@@ -67,7 +69,7 @@ complete -c kesha -n '__fish_seen_subcommand_from record' -l debug -d 'Trace eng
|
|
|
67
69
|
complete -c kesha -n '__fish_seen_subcommand_from say' -l help -d 'Show help'
|
|
68
70
|
complete -c kesha -n '__fish_seen_subcommand_from say' -s h -d 'Show help'
|
|
69
71
|
complete -c kesha -n '__fish_seen_subcommand_from say' -l voice -r -d 'Voice id, e.g. en-am_michael'
|
|
70
|
-
complete -c kesha -n '__fish_seen_subcommand_from say' -l lang -r -d '
|
|
72
|
+
complete -c kesha -n '__fish_seen_subcommand_from say' -l lang -r -d 'Language code, e.g. en-us (default en-us; see docs/languages.md). With no --voice, routes to that language\'s default voice and skips text-language detection.'
|
|
71
73
|
complete -c kesha -n '__fish_seen_subcommand_from say' -l out -r -d 'Write audio to file instead of stdout'
|
|
72
74
|
complete -c kesha -n '__fish_seen_subcommand_from say' -l rate -r -d 'Speaking rate 0.5–2.0'
|
|
73
75
|
complete -c kesha -n '__fish_seen_subcommand_from say' -l list-voices -d 'List installed voices and exit'
|
package/completions/kesha.zsh
CHANGED
|
@@ -27,14 +27,16 @@ _kesha() {
|
|
|
27
27
|
'--json[Output results as JSON]' \
|
|
28
28
|
'--toon[Output results as TOON (compact, LLM-friendly encoding of the same data as --json)]' \
|
|
29
29
|
'--timestamps[Include timestamped transcript segments in JSON/TOON output]' \
|
|
30
|
-
'--speakers[Include speaker labels in
|
|
30
|
+
'--speakers[Include speaker labels in segments. Needs --json/--toon and darwin-arm64; run `kesha install --diarize` first. Implies --timestamps.]' \
|
|
31
31
|
'--include-errors[With --json, output { results, errors } so scripts can read per-file failures without parsing stderr]' \
|
|
32
32
|
'--verbose[Show language detection details]' \
|
|
33
33
|
'--format=[Output format: transcript | json | toon (long-form alias for --json / --toon)]:format:' \
|
|
34
|
-
'--lang=[Expected language code (
|
|
34
|
+
'--lang=[Expected language code, e.g. en or en-us (see docs/languages.md); warn if mismatch]:lang:' \
|
|
35
35
|
'--debug[Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)]' \
|
|
36
36
|
'--vad[Force Silero VAD preprocessing (kesha install --vad first). Without this, VAD auto-engages on audio ≥ 120s.]' \
|
|
37
|
-
'--no-vad[Force full-file ASR for short/medium files; long audio fails early]'
|
|
37
|
+
'--no-vad[Force full-file ASR for short/medium files; long audio fails early]' \
|
|
38
|
+
'--quiet[Suppress progress output; print only results and errors]' \
|
|
39
|
+
'--no-color[Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)]'
|
|
38
40
|
else
|
|
39
41
|
_describe -t commands 'kesha command' commands
|
|
40
42
|
fi
|
|
@@ -99,7 +101,7 @@ _kesha() {
|
|
|
99
101
|
_arguments '--help[Show help]' \
|
|
100
102
|
'-h[Show help]' \
|
|
101
103
|
'--voice=[Voice id, e.g. en-am_michael]:voice:' \
|
|
102
|
-
'--lang=[
|
|
104
|
+
'--lang=[Language code, e.g. en-us (default en-us; see docs/languages.md). With no --voice, routes to that language'\''s default voice and skips text-language detection.]:lang:' \
|
|
103
105
|
'--out=[Write audio to file instead of stdout]:out:' \
|
|
104
106
|
'--rate=[Speaking rate 0.5–2.0]:rate:' \
|
|
105
107
|
'--list-voices[List installed voices and exit]' \
|
|
@@ -135,14 +137,16 @@ _kesha() {
|
|
|
135
137
|
'--json[Output results as JSON]' \
|
|
136
138
|
'--toon[Output results as TOON (compact, LLM-friendly encoding of the same data as --json)]' \
|
|
137
139
|
'--timestamps[Include timestamped transcript segments in JSON/TOON output]' \
|
|
138
|
-
'--speakers[Include speaker labels in
|
|
140
|
+
'--speakers[Include speaker labels in segments. Needs --json/--toon and darwin-arm64; run `kesha install --diarize` first. Implies --timestamps.]' \
|
|
139
141
|
'--include-errors[With --json, output { results, errors } so scripts can read per-file failures without parsing stderr]' \
|
|
140
142
|
'--verbose[Show language detection details]' \
|
|
141
143
|
'--format=[Output format: transcript | json | toon (long-form alias for --json / --toon)]:format:' \
|
|
142
|
-
'--lang=[Expected language code (
|
|
144
|
+
'--lang=[Expected language code, e.g. en or en-us (see docs/languages.md); warn if mismatch]:lang:' \
|
|
143
145
|
'--debug[Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)]' \
|
|
144
146
|
'--vad[Force Silero VAD preprocessing (kesha install --vad first). Without this, VAD auto-engages on audio ≥ 120s.]' \
|
|
145
|
-
'--no-vad[Force full-file ASR for short/medium files; long audio fails early]'
|
|
147
|
+
'--no-vad[Force full-file ASR for short/medium files; long audio fails early]' \
|
|
148
|
+
'--quiet[Suppress progress output; print only results and errors]' \
|
|
149
|
+
'--no-color[Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)]'
|
|
146
150
|
;;
|
|
147
151
|
esac
|
|
148
152
|
}
|
package/man/kesha.1
CHANGED
|
@@ -58,7 +58,7 @@ Output results as TOON (compact, LLM\-friendly encoding of the same data as \-\-
|
|
|
58
58
|
Include timestamped transcript segments in JSON/TOON output
|
|
59
59
|
.TP
|
|
60
60
|
.B \-\-speakers
|
|
61
|
-
Include speaker labels in
|
|
61
|
+
Include speaker labels in segments. Needs \-\-json/\-\-toon and darwin\-arm64; run `kesha install \-\-diarize` first. Implies \-\-timestamps.
|
|
62
62
|
.TP
|
|
63
63
|
.B \-\-include\-errors
|
|
64
64
|
With \-\-json, output { results, errors } so scripts can read per\-file failures without parsing stderr
|
|
@@ -70,7 +70,7 @@ Show language detection details
|
|
|
70
70
|
Output format: transcript | json | toon (long\-form alias for \-\-json / \-\-toon)
|
|
71
71
|
.TP
|
|
72
72
|
.B \-\-lang
|
|
73
|
-
Expected language code (
|
|
73
|
+
Expected language code, e.g. en or en\-us (see docs/languages.md); warn if mismatch
|
|
74
74
|
.TP
|
|
75
75
|
.B \-\-debug
|
|
76
76
|
Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)
|
|
@@ -80,6 +80,12 @@ Force Silero VAD preprocessing (kesha install \-\-vad first). Without this, VAD
|
|
|
80
80
|
.TP
|
|
81
81
|
.B \-\-no\-vad
|
|
82
82
|
Force full\-file ASR for short/medium files; long audio fails early
|
|
83
|
+
.TP
|
|
84
|
+
.B \-\-quiet
|
|
85
|
+
Suppress progress output; print only results and errors
|
|
86
|
+
.TP
|
|
87
|
+
.B \-\-no\-color
|
|
88
|
+
Disable ANSI colors (also via NO_COLOR=1; auto\-off when CI=true)
|
|
83
89
|
.SH SUBCOMMAND OPTIONS
|
|
84
90
|
.SS doctor
|
|
85
91
|
.TP
|
|
@@ -155,7 +161,7 @@ Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)
|
|
|
155
161
|
Voice id, e.g. en\-am_michael
|
|
156
162
|
.TP
|
|
157
163
|
.B \-\-lang
|
|
158
|
-
|
|
164
|
+
Language code, e.g. en\-us (default en\-us; see docs/languages.md). With no \-\-voice, routes to that language's default voice and skips text\-language detection.
|
|
159
165
|
.TP
|
|
160
166
|
.B \-\-out
|
|
161
167
|
Write audio to file instead of stdout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"test": "bun test",
|
|
45
|
+
"test:watch": "bun test --watch",
|
|
45
46
|
"test:unit": "bun test tests/unit/",
|
|
46
47
|
"test:cli-fast": "bun test tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/e2e-cli.test.ts tests/integration/say.test.ts",
|
|
47
48
|
"test:integration": "bun test tests/integration/",
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"yaml": "^2.9.0"
|
|
91
92
|
},
|
|
92
93
|
"dependencies": {
|
|
94
|
+
"@clack/prompts": "^1.5.0",
|
|
93
95
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
94
96
|
"@toon-format/toon": "^2.1.0",
|
|
95
97
|
"citty": "^0.2.2",
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runMain, type CommandDef } from "citty";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
-
import { log } from "../log";
|
|
3
|
+
import { log, setColorEnabled } from "../log";
|
|
4
4
|
import { suggestCommand } from "../suggest-command";
|
|
5
5
|
import { completionsCommand } from "./completions";
|
|
6
6
|
import { doctorCommand } from "./doctor";
|
|
@@ -39,7 +39,94 @@ function isPathLike(arg: string): boolean {
|
|
|
39
39
|
return arg.includes(".") || arg.includes("/") || existsSync(arg);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Falsey grammar shared by --no-color and CI. Mirrors KESHA_DEBUG so
|
|
43
|
+
// `CI=false`/`CI=0` and `--no-color=false` opt back in to colors.
|
|
44
|
+
const FALSEY_VALUES = new Set(["", "0", "false", "no", "off"]);
|
|
45
|
+
function isFalsey(v: string): boolean {
|
|
46
|
+
return FALSEY_VALUES.has(v.trim().toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Whether the process was launched with NO_COLOR already forced on. Captured
|
|
50
|
+
// once at import (before any runCli) so re-enabling colors never clobbers an
|
|
51
|
+
// externally-set NO_COLOR — we only clear the var on re-enable when WE set it.
|
|
52
|
+
const USER_FORCED_NO_COLOR =
|
|
53
|
+
process.env.NO_COLOR !== undefined && !isFalsey(process.env.NO_COLOR);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Strip a boolean global flag out of rawArgs so citty never sees it. Matches
|
|
57
|
+
* any of `names` bare (sets the flag) and the `--name=<value>` form for the
|
|
58
|
+
* long names (`<falsey>` explicitly turns the flag back off).
|
|
59
|
+
*/
|
|
60
|
+
function stripBooleanFlag(
|
|
61
|
+
rawArgs: string[],
|
|
62
|
+
names: string[],
|
|
63
|
+
): { value: boolean; rawArgs: string[] } {
|
|
64
|
+
const valuedPrefixes = names.filter((n) => n.startsWith("--")).map((n) => `${n}=`);
|
|
65
|
+
let value = false;
|
|
66
|
+
const cleaned: string[] = [];
|
|
67
|
+
for (const arg of rawArgs) {
|
|
68
|
+
if (names.includes(arg)) {
|
|
69
|
+
value = true;
|
|
70
|
+
} else if (valuedPrefixes.some((prefix) => arg.startsWith(prefix))) {
|
|
71
|
+
value = !isFalsey(arg.slice(arg.indexOf("=") + 1));
|
|
72
|
+
} else {
|
|
73
|
+
cleaned.push(arg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { value, rawArgs: cleaned };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decide whether ANSI colors should be disabled (#531) and return rawArgs with
|
|
81
|
+
* any `--no-color[=value]` token stripped so citty never sees it.
|
|
82
|
+
*
|
|
83
|
+
* Colors are disabled when `--no-color` (bare) or `--no-color=<truthy>` is
|
|
84
|
+
* passed, or when the environment looks like CI (`CI` set to a non-falsey value
|
|
85
|
+
* — GitHub Actions, GitLab, CircleCI, … export `CI=true`). `--no-color=false`
|
|
86
|
+
* and `CI=false`/`CI=0` explicitly opt back in.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveColorMode(
|
|
89
|
+
rawArgs: string[],
|
|
90
|
+
env: { CI?: string } = process.env as { CI?: string },
|
|
91
|
+
): { disableColor: boolean; rawArgs: string[] } {
|
|
92
|
+
const { value, rawArgs: cleaned } = stripBooleanFlag(rawArgs, ["--no-color"]);
|
|
93
|
+
const ci = env.CI !== undefined && !isFalsey(env.CI);
|
|
94
|
+
return { disableColor: value || ci, rawArgs: cleaned };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect `--quiet`/`-q` (and `--quiet=<value>`) and return rawArgs with the
|
|
99
|
+
* token stripped (#526). Resolved before citty — like `--no-color` — so quiet
|
|
100
|
+
* is global: it works for every command, not just the transcribe path, and a
|
|
101
|
+
* subcommand that doesn't declare it never sees the flag.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveQuietMode(rawArgs: string[]): { quiet: boolean; rawArgs: string[] } {
|
|
104
|
+
const { value, rawArgs: cleaned } = stripBooleanFlag(rawArgs, ["--quiet", "-q"]);
|
|
105
|
+
return { quiet: value, rawArgs: cleaned };
|
|
106
|
+
}
|
|
107
|
+
|
|
42
108
|
export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
109
|
+
// Global flags resolved before citty so they apply to every command (and help
|
|
110
|
+
// output) and never reach a subcommand's arg schema.
|
|
111
|
+
const color = resolveColorMode(rawArgs);
|
|
112
|
+
// Reset on EVERY invocation (not just the disable path): an earlier
|
|
113
|
+
// --no-color / CI call must not leave colors permanently off for later
|
|
114
|
+
// in-process calls (unit tests, `kesha mcp`). picocolors already honors
|
|
115
|
+
// NO_COLOR at startup; setting the env var also propagates to the engine.
|
|
116
|
+
setColorEnabled(!color.disableColor);
|
|
117
|
+
// Keep NO_COLOR symmetric so it doesn't leak to engine subprocesses spawned
|
|
118
|
+
// by a later in-process call: set it when WE disable, clear it on re-enable —
|
|
119
|
+
// but never clear a NO_COLOR the user exported themselves.
|
|
120
|
+
if (color.disableColor) {
|
|
121
|
+
process.env.NO_COLOR = "1";
|
|
122
|
+
} else if (!USER_FORCED_NO_COLOR) {
|
|
123
|
+
delete process.env.NO_COLOR;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const quiet = resolveQuietMode(color.rawArgs);
|
|
127
|
+
log.quietEnabled = quiet.quiet;
|
|
128
|
+
|
|
129
|
+
rawArgs = quiet.rawArgs;
|
|
43
130
|
const [firstArg, ...restArgs] = rawArgs;
|
|
44
131
|
|
|
45
132
|
if (firstArg && Object.hasOwn(SUBCOMMANDS, firstArg)) {
|
package/src/cli/init.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { multiselect, isCancel, cancel } from "@clack/prompts";
|
|
4
5
|
import { renderInstallPlan } from "../install-plan";
|
|
5
6
|
import { log } from "../log";
|
|
6
|
-
import {
|
|
7
|
+
import { getEngineCapabilities } from "../engine";
|
|
8
|
+
import {
|
|
9
|
+
performInstall,
|
|
10
|
+
resolveBackendFlag,
|
|
11
|
+
resolveNoCacheFlag,
|
|
12
|
+
TTS_LANG_FALLBACK,
|
|
13
|
+
} from "./install";
|
|
14
|
+
|
|
15
|
+
const TTS_LANG_LABELS: Record<string, string> = {
|
|
16
|
+
en: "English (Kokoro)",
|
|
17
|
+
es: "Spanish (Kokoro)",
|
|
18
|
+
fr: "French (Kokoro)",
|
|
19
|
+
hi: "Hindi (Kokoro ANE)",
|
|
20
|
+
it: "Italian (Kokoro)",
|
|
21
|
+
ja: "Japanese (Kokoro ANE)",
|
|
22
|
+
pt: "Portuguese (Kokoro)",
|
|
23
|
+
zh: "Chinese (Kokoro ANE)",
|
|
24
|
+
ru: "Russian (Vosk-TTS)",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Signature of the interactive TTS-language picker (injectable for tests). */
|
|
28
|
+
export type TtsLangPrompt = (preselect: string[]) => Promise<string[]>;
|
|
29
|
+
|
|
30
|
+
async function promptTtsLangs(preselect: string[]): Promise<string[]> {
|
|
31
|
+
const caps = await getEngineCapabilities();
|
|
32
|
+
const supported = caps?.tts?.languages.map((l) => l.code) ?? TTS_LANG_FALLBACK;
|
|
33
|
+
const selected = await multiselect({
|
|
34
|
+
message:
|
|
35
|
+
"Select TTS languages to install (space to toggle, enter to confirm; none = skip TTS):",
|
|
36
|
+
options: supported.map((code) => ({ value: code, label: TTS_LANG_LABELS[code] ?? code })),
|
|
37
|
+
initialValues: preselect.filter((l) => supported.includes(l)),
|
|
38
|
+
required: false,
|
|
39
|
+
});
|
|
40
|
+
if (isCancel(selected)) {
|
|
41
|
+
cancel("Init cancelled.");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
return selected as string[];
|
|
45
|
+
}
|
|
7
46
|
|
|
8
47
|
export interface InitCommandArgs {
|
|
9
48
|
coreml: boolean;
|
|
@@ -21,7 +60,7 @@ export interface InitCommandArgs {
|
|
|
21
60
|
export interface InitSelection {
|
|
22
61
|
noCache: boolean;
|
|
23
62
|
backend?: string;
|
|
24
|
-
|
|
63
|
+
ttsLangs: string[];
|
|
25
64
|
vad: boolean;
|
|
26
65
|
diarize: boolean;
|
|
27
66
|
}
|
|
@@ -45,7 +84,7 @@ export function resolveInitSelection(
|
|
|
45
84
|
return {
|
|
46
85
|
noCache,
|
|
47
86
|
backend,
|
|
48
|
-
|
|
87
|
+
ttsLangs: args.tts ? ["en"] : [],
|
|
49
88
|
vad: args.vad,
|
|
50
89
|
diarize: args.diarize,
|
|
51
90
|
};
|
|
@@ -58,7 +97,7 @@ export function initInstallArgs(selection: InitSelection): string[] {
|
|
|
58
97
|
selection.noCache ? "--no-cache" : "",
|
|
59
98
|
selection.backend === "coreml" ? "--coreml" : "",
|
|
60
99
|
selection.backend === "onnx" ? "--onnx" : "",
|
|
61
|
-
selection.
|
|
100
|
+
...(selection.ttsLangs.length > 0 ? ["--tts", ...selection.ttsLangs] : []),
|
|
62
101
|
selection.vad ? "--vad" : "",
|
|
63
102
|
selection.diarize ? "--diarize" : "",
|
|
64
103
|
].filter(Boolean);
|
|
@@ -70,12 +109,12 @@ export function initSuggestionCommands(
|
|
|
70
109
|
): string[][] {
|
|
71
110
|
const variants: InitSelection[] = [
|
|
72
111
|
selection,
|
|
73
|
-
{ ...selection,
|
|
74
|
-
{ ...selection,
|
|
112
|
+
{ ...selection, ttsLangs: [], vad: true, diarize: false },
|
|
113
|
+
{ ...selection, ttsLangs: ["en"], vad: true, diarize: false },
|
|
75
114
|
];
|
|
76
115
|
|
|
77
116
|
if (canDiarize) {
|
|
78
|
-
variants.push({ ...selection,
|
|
117
|
+
variants.push({ ...selection, ttsLangs: [], vad: true, diarize: true });
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
const seen = new Set<string>();
|
|
@@ -118,8 +157,9 @@ export async function promptInitSelection(
|
|
|
118
157
|
backend = resolveBackendFlag(args.coreml, args.onnx),
|
|
119
158
|
canDiarize = canInstallDiarizeOnPlatform(),
|
|
120
159
|
noCache = resolveNoCacheFlag(args),
|
|
160
|
+
promptTts: TtsLangPrompt = promptTtsLangs,
|
|
121
161
|
): Promise<InitSelection> {
|
|
122
|
-
const
|
|
162
|
+
const ttsLangs = await promptTts(args.tts ? ["en"] : []);
|
|
123
163
|
const vad = await askYesNo(prompt, "Install VAD for long or silence-heavy audio?", args.vad);
|
|
124
164
|
let diarize = false;
|
|
125
165
|
if (canDiarize) {
|
|
@@ -131,7 +171,7 @@ export async function promptInitSelection(
|
|
|
131
171
|
return {
|
|
132
172
|
noCache,
|
|
133
173
|
backend,
|
|
134
|
-
|
|
174
|
+
ttsLangs,
|
|
135
175
|
vad,
|
|
136
176
|
diarize,
|
|
137
177
|
};
|
|
@@ -153,7 +193,7 @@ async function printPlan(selection: InitSelection): Promise<void> {
|
|
|
153
193
|
await renderInstallPlan({
|
|
154
194
|
noCache: selection.noCache,
|
|
155
195
|
backend: selection.backend,
|
|
156
|
-
|
|
196
|
+
ttsLangs: selection.ttsLangs,
|
|
157
197
|
vad: selection.vad,
|
|
158
198
|
diarize: selection.diarize,
|
|
159
199
|
}),
|
|
@@ -240,7 +280,7 @@ export const initCommand = defineCommand({
|
|
|
240
280
|
await performInstall(
|
|
241
281
|
installSelection.noCache,
|
|
242
282
|
installSelection.backend,
|
|
243
|
-
installSelection.
|
|
283
|
+
installSelection.ttsLangs,
|
|
244
284
|
installSelection.vad,
|
|
245
285
|
installSelection.diarize,
|
|
246
286
|
);
|
|
@@ -265,7 +305,21 @@ export const initCommand = defineCommand({
|
|
|
265
305
|
log.info(`Skipped install. Run later: ${initInstallArgs(prompted).join(" ")}`);
|
|
266
306
|
return;
|
|
267
307
|
}
|
|
268
|
-
await performInstall(
|
|
308
|
+
await performInstall(
|
|
309
|
+
prompted.noCache,
|
|
310
|
+
prompted.backend,
|
|
311
|
+
prompted.ttsLangs,
|
|
312
|
+
prompted.vad,
|
|
313
|
+
prompted.diarize,
|
|
314
|
+
);
|
|
315
|
+
// Next-steps hint (#523): leave the user with something runnable rather
|
|
316
|
+
// than ending on the install log.
|
|
317
|
+
log.info("");
|
|
318
|
+
log.success("Kesha is ready. Try:");
|
|
319
|
+
log.info(" kesha path/to/audio.ogg Transcribe a file.");
|
|
320
|
+
if (prompted.ttsLangs.length > 0) {
|
|
321
|
+
log.info(' kesha say "hello" Speak text (TTS).');
|
|
322
|
+
}
|
|
269
323
|
} finally {
|
|
270
324
|
rl.close();
|
|
271
325
|
}
|
package/src/cli/install.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
+
import { errorMessage } from "../error-utils";
|
|
2
3
|
import { downloadEngine } from "../engine-install";
|
|
3
|
-
import { getEngineBinPath } from "../engine";
|
|
4
|
+
import { getEngineBinPath, getEngineCapabilities } from "../engine";
|
|
4
5
|
import { renderInstallPlan } from "../install-plan";
|
|
5
6
|
import { maybeAskForStar } from "../star";
|
|
6
7
|
import { log } from "../log";
|
|
@@ -8,6 +9,8 @@ import { packageVersion } from "../package-info";
|
|
|
8
9
|
import { createDiagnosticLogSession, type DiagnosticLogSession } from "../diagnostic-log";
|
|
9
10
|
|
|
10
11
|
export interface InstallCommandArgs {
|
|
12
|
+
/** Positional args after `install` — candidate TTS language codes. */
|
|
13
|
+
_?: string[];
|
|
11
14
|
coreml: boolean;
|
|
12
15
|
onnx: boolean;
|
|
13
16
|
"no-cache": boolean;
|
|
@@ -19,6 +22,50 @@ export interface InstallCommandArgs {
|
|
|
19
22
|
plan: boolean;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* TTS languages installable on EVERY build, used when the engine binary isn't
|
|
27
|
+
* installed yet (capabilities unavailable). hi/ja/zh are darwin-arm64-only and
|
|
28
|
+
* can't be assumed without capabilities, so they're excluded here.
|
|
29
|
+
*/
|
|
30
|
+
export const TTS_LANG_FALLBACK = ["en", "es", "fr", "it", "pt", "ru"];
|
|
31
|
+
|
|
32
|
+
export interface TtsArgInput {
|
|
33
|
+
/** Whether --tts was passed. */
|
|
34
|
+
tts: boolean;
|
|
35
|
+
/** Positional args after the install command (candidate language codes). */
|
|
36
|
+
positionals: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the requested TTS language list. Bare `--tts` defaults to English.
|
|
41
|
+
* Positionals without `--tts` are an error. When `supported` is provided,
|
|
42
|
+
* unsupported codes are a hard error (nothing downloads); when it's undefined
|
|
43
|
+
* (engine not yet installed, capabilities unavailable) the check is skipped and
|
|
44
|
+
* the engine validates authoritatively at download time.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveTtsLangs(input: TtsArgInput, supported: string[] | undefined): string[] {
|
|
47
|
+
if (!input.tts) {
|
|
48
|
+
if (input.positionals.length > 0) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Language codes (${input.positionals.join(", ")}) require the --tts flag, ` +
|
|
51
|
+
`e.g. \`kesha install --tts ${input.positionals.join(" ")}\`.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const langs = input.positionals.length > 0 ? input.positionals : ["en"];
|
|
57
|
+
if (supported) {
|
|
58
|
+
const bad = langs.filter((l) => !supported.includes(l));
|
|
59
|
+
if (bad.length > 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unsupported TTS language(s): ${bad.join(", ")}. ` +
|
|
62
|
+
`Supported on this platform: ${supported.join(", ")}.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return langs;
|
|
67
|
+
}
|
|
68
|
+
|
|
22
69
|
export function resolveNoCacheFlag(
|
|
23
70
|
args: Pick<InstallCommandArgs, "no-cache" | "noCache" | "no_cache">,
|
|
24
71
|
rawArgs: string[] = [],
|
|
@@ -65,20 +112,20 @@ function finishInstallDiagnostic(
|
|
|
65
112
|
});
|
|
66
113
|
diagnosticLog.finish(status);
|
|
67
114
|
} catch (err) {
|
|
68
|
-
log.debug(`install diagnostic log finish dropped: ${
|
|
115
|
+
log.debug(`install diagnostic log finish dropped: ${errorMessage(err)}`);
|
|
69
116
|
}
|
|
70
117
|
}
|
|
71
118
|
|
|
72
119
|
export async function performInstall(
|
|
73
120
|
noCache: boolean,
|
|
74
|
-
backend
|
|
75
|
-
|
|
121
|
+
backend: string | undefined,
|
|
122
|
+
ttsLangs: string[],
|
|
76
123
|
vad = false,
|
|
77
124
|
diarize = false,
|
|
78
125
|
plan = false,
|
|
79
126
|
) {
|
|
80
127
|
if (plan) {
|
|
81
|
-
log.info(await renderInstallPlan({ noCache, backend,
|
|
128
|
+
log.info(await renderInstallPlan({ noCache, backend, ttsLangs, vad, diarize }));
|
|
82
129
|
return;
|
|
83
130
|
}
|
|
84
131
|
|
|
@@ -91,7 +138,7 @@ export async function performInstall(
|
|
|
91
138
|
command: "install",
|
|
92
139
|
backend: backend ?? "auto",
|
|
93
140
|
noCache,
|
|
94
|
-
tts,
|
|
141
|
+
tts: ttsLangs.length > 0,
|
|
95
142
|
vad,
|
|
96
143
|
diarize,
|
|
97
144
|
});
|
|
@@ -111,11 +158,11 @@ export async function performInstall(
|
|
|
111
158
|
`the release engine uses "${platformBackend}".`,
|
|
112
159
|
);
|
|
113
160
|
}
|
|
114
|
-
await downloadEngine(noCache, backend, {
|
|
161
|
+
await downloadEngine(noCache, backend, { ttsLangs, vad, diarize });
|
|
115
162
|
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
116
163
|
finishInstallDiagnostic(diagnosticLog, startedAt, "success");
|
|
117
164
|
} catch (err: unknown) {
|
|
118
|
-
const message =
|
|
165
|
+
const message = errorMessage(err);
|
|
119
166
|
finishInstallDiagnostic(diagnosticLog, startedAt, "failed", errorKind);
|
|
120
167
|
log.error(message);
|
|
121
168
|
process.exit(1);
|
|
@@ -166,6 +213,23 @@ export const installCommand = defineCommand({
|
|
|
166
213
|
},
|
|
167
214
|
async run({ args, rawArgs }: { args: InstallCommandArgs; rawArgs: string[] }) {
|
|
168
215
|
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
169
|
-
|
|
216
|
+
const positionals = (args._ ?? []).map(String);
|
|
217
|
+
const caps = await getEngineCapabilities();
|
|
218
|
+
const supported = caps?.tts?.languages.map((l) => l.code);
|
|
219
|
+
let ttsLangs: string[];
|
|
220
|
+
try {
|
|
221
|
+
ttsLangs = resolveTtsLangs({ tts: args.tts === true, positionals }, supported);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
log.error(errorMessage(err));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
await performInstall(
|
|
227
|
+
resolveNoCacheFlag(args, rawArgs),
|
|
228
|
+
backend,
|
|
229
|
+
ttsLangs,
|
|
230
|
+
args.vad,
|
|
231
|
+
args.diarize,
|
|
232
|
+
args.plan,
|
|
233
|
+
);
|
|
170
234
|
},
|
|
171
235
|
});
|