@drakulavich/kesha-voice-kit 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +84 -1
- package/src/cli/init.ts +66 -12
- package/src/cli/install.ts +70 -7
- package/src/cli/main.ts +29 -2
- package/src/cli/say.ts +4 -4
- package/src/engine-install.ts +29 -10
- package/src/engine.ts +6 -0
- package/src/install-plan.ts +78 -59
- package/src/lib.ts +7 -3
- package/src/log.ts +40 -5
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.0",
|
|
4
4
|
"description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -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,90 @@ 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
|
+
* Decide whether ANSI colors should be disabled (#531) and return rawArgs with
|
|
57
|
+
* any `--no-color[=value]` token stripped so citty never sees it.
|
|
58
|
+
*
|
|
59
|
+
* Colors are disabled when `--no-color` (bare) or `--no-color=<truthy>` is
|
|
60
|
+
* passed, or when the environment looks like CI (`CI` set to a non-falsey value
|
|
61
|
+
* — GitHub Actions, GitLab, CircleCI, … export `CI=true`). `--no-color=false`
|
|
62
|
+
* and `CI=false`/`CI=0` explicitly opt back in.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveColorMode(
|
|
65
|
+
rawArgs: string[],
|
|
66
|
+
env: { CI?: string } = process.env as { CI?: string },
|
|
67
|
+
): { disableColor: boolean; rawArgs: string[] } {
|
|
68
|
+
let flag = false;
|
|
69
|
+
const cleaned: string[] = [];
|
|
70
|
+
for (const arg of rawArgs) {
|
|
71
|
+
if (arg === "--no-color") {
|
|
72
|
+
flag = true;
|
|
73
|
+
} else if (arg.startsWith("--no-color=")) {
|
|
74
|
+
flag = !isFalsey(arg.slice("--no-color=".length));
|
|
75
|
+
} else {
|
|
76
|
+
cleaned.push(arg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const ci = env.CI !== undefined && !isFalsey(env.CI);
|
|
80
|
+
return { disableColor: flag || ci, rawArgs: cleaned };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect `--quiet`/`-q` (and `--quiet=<value>`) and return rawArgs with the
|
|
85
|
+
* token stripped (#526). Resolved before citty — like `--no-color` — so quiet
|
|
86
|
+
* is global: it works for every command, not just the transcribe path, and a
|
|
87
|
+
* subcommand that doesn't declare it never sees the flag.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveQuietMode(rawArgs: string[]): { quiet: boolean; rawArgs: string[] } {
|
|
90
|
+
let flag = false;
|
|
91
|
+
const cleaned: string[] = [];
|
|
92
|
+
for (const arg of rawArgs) {
|
|
93
|
+
if (arg === "--quiet" || arg === "-q") {
|
|
94
|
+
flag = true;
|
|
95
|
+
} else if (arg.startsWith("--quiet=")) {
|
|
96
|
+
flag = !isFalsey(arg.slice("--quiet=".length));
|
|
97
|
+
} else {
|
|
98
|
+
cleaned.push(arg);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { quiet: flag, rawArgs: cleaned };
|
|
102
|
+
}
|
|
103
|
+
|
|
42
104
|
export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
105
|
+
// Global flags resolved before citty so they apply to every command (and help
|
|
106
|
+
// output) and never reach a subcommand's arg schema.
|
|
107
|
+
const color = resolveColorMode(rawArgs);
|
|
108
|
+
// Reset on EVERY invocation (not just the disable path): an earlier
|
|
109
|
+
// --no-color / CI call must not leave colors permanently off for later
|
|
110
|
+
// in-process calls (unit tests, `kesha mcp`). picocolors already honors
|
|
111
|
+
// NO_COLOR at startup; setting the env var also propagates to the engine.
|
|
112
|
+
setColorEnabled(!color.disableColor);
|
|
113
|
+
// Keep NO_COLOR symmetric so it doesn't leak to engine subprocesses spawned
|
|
114
|
+
// by a later in-process call: set it when WE disable, clear it on re-enable —
|
|
115
|
+
// but never clear a NO_COLOR the user exported themselves.
|
|
116
|
+
if (color.disableColor) {
|
|
117
|
+
process.env.NO_COLOR = "1";
|
|
118
|
+
} else if (!USER_FORCED_NO_COLOR) {
|
|
119
|
+
delete process.env.NO_COLOR;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const quiet = resolveQuietMode(color.rawArgs);
|
|
123
|
+
log.quietEnabled = quiet.quiet;
|
|
124
|
+
|
|
125
|
+
rawArgs = quiet.rawArgs;
|
|
43
126
|
const [firstArg, ...restArgs] = rawArgs;
|
|
44
127
|
|
|
45
128
|
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,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { downloadEngine } from "../engine-install";
|
|
3
|
-
import { getEngineBinPath } from "../engine";
|
|
3
|
+
import { getEngineBinPath, getEngineCapabilities } from "../engine";
|
|
4
4
|
import { renderInstallPlan } from "../install-plan";
|
|
5
5
|
import { maybeAskForStar } from "../star";
|
|
6
6
|
import { log } from "../log";
|
|
@@ -8,6 +8,8 @@ import { packageVersion } from "../package-info";
|
|
|
8
8
|
import { createDiagnosticLogSession, type DiagnosticLogSession } from "../diagnostic-log";
|
|
9
9
|
|
|
10
10
|
export interface InstallCommandArgs {
|
|
11
|
+
/** Positional args after `install` — candidate TTS language codes. */
|
|
12
|
+
_?: string[];
|
|
11
13
|
coreml: boolean;
|
|
12
14
|
onnx: boolean;
|
|
13
15
|
"no-cache": boolean;
|
|
@@ -19,6 +21,50 @@ export interface InstallCommandArgs {
|
|
|
19
21
|
plan: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* TTS languages installable on EVERY build, used when the engine binary isn't
|
|
26
|
+
* installed yet (capabilities unavailable). hi/ja/zh are darwin-arm64-only and
|
|
27
|
+
* can't be assumed without capabilities, so they're excluded here.
|
|
28
|
+
*/
|
|
29
|
+
export const TTS_LANG_FALLBACK = ["en", "es", "fr", "it", "pt", "ru"];
|
|
30
|
+
|
|
31
|
+
export interface TtsArgInput {
|
|
32
|
+
/** Whether --tts was passed. */
|
|
33
|
+
tts: boolean;
|
|
34
|
+
/** Positional args after the install command (candidate language codes). */
|
|
35
|
+
positionals: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the requested TTS language list. Bare `--tts` defaults to English.
|
|
40
|
+
* Positionals without `--tts` are an error. When `supported` is provided,
|
|
41
|
+
* unsupported codes are a hard error (nothing downloads); when it's undefined
|
|
42
|
+
* (engine not yet installed, capabilities unavailable) the check is skipped and
|
|
43
|
+
* the engine validates authoritatively at download time.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveTtsLangs(input: TtsArgInput, supported: string[] | undefined): string[] {
|
|
46
|
+
if (!input.tts) {
|
|
47
|
+
if (input.positionals.length > 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Language codes (${input.positionals.join(", ")}) require the --tts flag, ` +
|
|
50
|
+
`e.g. \`kesha install --tts ${input.positionals.join(" ")}\`.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const langs = input.positionals.length > 0 ? input.positionals : ["en"];
|
|
56
|
+
if (supported) {
|
|
57
|
+
const bad = langs.filter((l) => !supported.includes(l));
|
|
58
|
+
if (bad.length > 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unsupported TTS language(s): ${bad.join(", ")}. ` +
|
|
61
|
+
`Supported on this platform: ${supported.join(", ")}.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return langs;
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
export function resolveNoCacheFlag(
|
|
23
69
|
args: Pick<InstallCommandArgs, "no-cache" | "noCache" | "no_cache">,
|
|
24
70
|
rawArgs: string[] = [],
|
|
@@ -71,14 +117,14 @@ function finishInstallDiagnostic(
|
|
|
71
117
|
|
|
72
118
|
export async function performInstall(
|
|
73
119
|
noCache: boolean,
|
|
74
|
-
backend
|
|
75
|
-
|
|
120
|
+
backend: string | undefined,
|
|
121
|
+
ttsLangs: string[],
|
|
76
122
|
vad = false,
|
|
77
123
|
diarize = false,
|
|
78
124
|
plan = false,
|
|
79
125
|
) {
|
|
80
126
|
if (plan) {
|
|
81
|
-
log.info(await renderInstallPlan({ noCache, backend,
|
|
127
|
+
log.info(await renderInstallPlan({ noCache, backend, ttsLangs, vad, diarize }));
|
|
82
128
|
return;
|
|
83
129
|
}
|
|
84
130
|
|
|
@@ -91,7 +137,7 @@ export async function performInstall(
|
|
|
91
137
|
command: "install",
|
|
92
138
|
backend: backend ?? "auto",
|
|
93
139
|
noCache,
|
|
94
|
-
tts,
|
|
140
|
+
tts: ttsLangs.length > 0,
|
|
95
141
|
vad,
|
|
96
142
|
diarize,
|
|
97
143
|
});
|
|
@@ -111,7 +157,7 @@ export async function performInstall(
|
|
|
111
157
|
`the release engine uses "${platformBackend}".`,
|
|
112
158
|
);
|
|
113
159
|
}
|
|
114
|
-
await downloadEngine(noCache, backend, {
|
|
160
|
+
await downloadEngine(noCache, backend, { ttsLangs, vad, diarize });
|
|
115
161
|
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
116
162
|
finishInstallDiagnostic(diagnosticLog, startedAt, "success");
|
|
117
163
|
} catch (err: unknown) {
|
|
@@ -166,6 +212,23 @@ export const installCommand = defineCommand({
|
|
|
166
212
|
},
|
|
167
213
|
async run({ args, rawArgs }: { args: InstallCommandArgs; rawArgs: string[] }) {
|
|
168
214
|
const backend = resolveBackendFlag(args.coreml, args.onnx);
|
|
169
|
-
|
|
215
|
+
const positionals = (args._ ?? []).map(String);
|
|
216
|
+
const caps = await getEngineCapabilities();
|
|
217
|
+
const supported = caps?.tts?.languages.map((l) => l.code);
|
|
218
|
+
let ttsLangs: string[];
|
|
219
|
+
try {
|
|
220
|
+
ttsLangs = resolveTtsLangs({ tts: args.tts === true, positionals }, supported);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
await performInstall(
|
|
226
|
+
resolveNoCacheFlag(args, rawArgs),
|
|
227
|
+
backend,
|
|
228
|
+
ttsLangs,
|
|
229
|
+
args.vad,
|
|
230
|
+
args.diarize,
|
|
231
|
+
args.plan,
|
|
232
|
+
);
|
|
170
233
|
},
|
|
171
234
|
});
|
package/src/cli/main.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface MainCommandArgs {
|
|
|
37
37
|
"include-errors": boolean;
|
|
38
38
|
format?: string;
|
|
39
39
|
lang?: string;
|
|
40
|
+
quiet: boolean;
|
|
41
|
+
"no-color": boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function detectLanguage(text: string): string {
|
|
@@ -116,7 +118,9 @@ export function shouldReportTranscribeProgress(input: {
|
|
|
116
118
|
stderrIsTty: boolean;
|
|
117
119
|
stdoutIsTty: boolean;
|
|
118
120
|
debugEnabled: boolean;
|
|
121
|
+
quiet?: boolean;
|
|
119
122
|
}): boolean {
|
|
123
|
+
if (input.quiet) return false;
|
|
120
124
|
if (input.debugEnabled) return false;
|
|
121
125
|
return input.stderrIsTty || !input.stdoutIsTty;
|
|
122
126
|
}
|
|
@@ -147,6 +151,12 @@ export const mainCommand = defineCommand({
|
|
|
147
151
|
description:
|
|
148
152
|
"Kesha Voice Kit — open-source voice toolkit for Apple Silicon.\n" +
|
|
149
153
|
"\n" +
|
|
154
|
+
"Examples:\n" +
|
|
155
|
+
" kesha audio.ogg Transcribe an audio file.\n" +
|
|
156
|
+
" kesha --json audio.ogg Transcribe with machine-readable output.\n" +
|
|
157
|
+
" kesha say \"hello\" Speak text (text-to-speech).\n" +
|
|
158
|
+
" kesha init Guided first-time setup.\n" +
|
|
159
|
+
"\n" +
|
|
150
160
|
"Commands:\n" +
|
|
151
161
|
" completions Print shell completion script.\n" +
|
|
152
162
|
" doctor Collect support diagnostics.\n" +
|
|
@@ -178,7 +188,7 @@ export const mainCommand = defineCommand({
|
|
|
178
188
|
},
|
|
179
189
|
speakers: {
|
|
180
190
|
type: "boolean",
|
|
181
|
-
description: "Include speaker labels in
|
|
191
|
+
description: "Include speaker labels in segments. Needs --json/--toon and darwin-arm64; run `kesha install --diarize` first. Implies --timestamps.",
|
|
182
192
|
default: false,
|
|
183
193
|
},
|
|
184
194
|
"include-errors": {
|
|
@@ -197,7 +207,7 @@ export const mainCommand = defineCommand({
|
|
|
197
207
|
},
|
|
198
208
|
lang: {
|
|
199
209
|
type: "string",
|
|
200
|
-
description: "Expected language code (
|
|
210
|
+
description: "Expected language code, e.g. en or en-us (see docs/languages.md); warn if mismatch",
|
|
201
211
|
},
|
|
202
212
|
debug: {
|
|
203
213
|
type: "boolean",
|
|
@@ -214,9 +224,25 @@ export const mainCommand = defineCommand({
|
|
|
214
224
|
description: "Force full-file ASR for short/medium files; long audio fails early",
|
|
215
225
|
default: false,
|
|
216
226
|
},
|
|
227
|
+
// quiet and no-color are resolved before citty in dispatch.ts (so they
|
|
228
|
+
// apply to every command, not just transcribe); declared here only so they
|
|
229
|
+
// appear in `kesha --help`.
|
|
230
|
+
quiet: {
|
|
231
|
+
type: "boolean",
|
|
232
|
+
alias: "q",
|
|
233
|
+
description: "Suppress progress output; print only results and errors",
|
|
234
|
+
default: false,
|
|
235
|
+
},
|
|
236
|
+
"no-color": {
|
|
237
|
+
type: "boolean",
|
|
238
|
+
description: "Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)",
|
|
239
|
+
default: false,
|
|
240
|
+
},
|
|
217
241
|
},
|
|
218
242
|
async run({ args, rawArgs }: { args: MainCommandArgs; rawArgs: string[] }) {
|
|
219
243
|
if (args.debug) log.debugEnabled = true;
|
|
244
|
+
// `log.quietEnabled` is set globally in dispatch.ts (where --quiet/-q is
|
|
245
|
+
// resolved and stripped before citty), so it already reflects --quiet here.
|
|
220
246
|
const files = args._;
|
|
221
247
|
|
|
222
248
|
// Validate `--format <value>` and normalize into the boolean flags
|
|
@@ -297,6 +323,7 @@ export const mainCommand = defineCommand({
|
|
|
297
323
|
stderrIsTty: process.stderr.isTTY === true,
|
|
298
324
|
stdoutIsTty: process.stdout.isTTY === true,
|
|
299
325
|
debugEnabled: log.isDebugEnabled(),
|
|
326
|
+
quiet: log.quietEnabled,
|
|
300
327
|
});
|
|
301
328
|
|
|
302
329
|
for (const file of files) {
|
package/src/cli/say.ts
CHANGED
|
@@ -111,7 +111,7 @@ export const sayCommand = defineCommand({
|
|
|
111
111
|
lang: {
|
|
112
112
|
type: "string",
|
|
113
113
|
description:
|
|
114
|
-
"
|
|
114
|
+
"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.",
|
|
115
115
|
},
|
|
116
116
|
out: { type: "string", description: "Write audio to file instead of stdout" },
|
|
117
117
|
rate: { type: "string", description: "Speaking rate 0.5–2.0", default: "1.0" },
|
|
@@ -239,16 +239,16 @@ export const sayCommand = defineCommand({
|
|
|
239
239
|
const startedAt = performance.now();
|
|
240
240
|
if (opts.out) {
|
|
241
241
|
const voiceLabel = opts.voice ?? "default voice";
|
|
242
|
-
|
|
242
|
+
log.status(`Synthesizing ${voiceLabel} -> ${opts.out}...`);
|
|
243
243
|
}
|
|
244
244
|
const audio = await stats.timeStage("tts", () => say(opts));
|
|
245
245
|
const ttsTimeMs = Math.round(performance.now() - startedAt);
|
|
246
246
|
if (opts.out) {
|
|
247
|
-
|
|
247
|
+
log.status(`Saved ${opts.out} (${ttsTimeMs}ms)`);
|
|
248
248
|
}
|
|
249
249
|
if (args.verbose && !opts.out) {
|
|
250
250
|
// stderr — stdout may carry raw audio bytes when --out is omitted.
|
|
251
|
-
|
|
251
|
+
log.status(`TTS time: ${ttsTimeMs}ms`);
|
|
252
252
|
}
|
|
253
253
|
let outputFormat: string = opts.format ?? "wav";
|
|
254
254
|
let outputSizeBytes: number | null | undefined = audio.byteLength;
|
package/src/engine-install.ts
CHANGED
|
@@ -325,9 +325,25 @@ async function warmDarwinKokoro(binPath: string): Promise<void> {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/** Build the `kesha-engine install` argv from options. Exported for testing. */
|
|
329
|
+
export function buildEngineInstallArgs(opts: {
|
|
330
|
+
noCache: boolean;
|
|
331
|
+
ttsLangs?: string[];
|
|
332
|
+
vad?: boolean;
|
|
333
|
+
diarize?: boolean;
|
|
334
|
+
}): string[] {
|
|
335
|
+
return [
|
|
336
|
+
"install",
|
|
337
|
+
...(opts.noCache ? ["--no-cache"] : []),
|
|
338
|
+
...(opts.ttsLangs && opts.ttsLangs.length > 0 ? ["--tts", ...opts.ttsLangs] : []),
|
|
339
|
+
...(opts.vad ? ["--vad"] : []),
|
|
340
|
+
...(opts.diarize ? ["--diarize"] : []),
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
|
|
328
344
|
export interface InstallOptions {
|
|
329
|
-
/**
|
|
330
|
-
|
|
345
|
+
/** TTS languages to install (empty/undefined = no TTS). */
|
|
346
|
+
ttsLangs?: string[];
|
|
331
347
|
/** Also install Silero VAD model for long-audio preprocessing. */
|
|
332
348
|
vad?: boolean;
|
|
333
349
|
/** Also install the Sortformer streaming-diarization model (~245MB,
|
|
@@ -498,13 +514,12 @@ export async function downloadEngine(
|
|
|
498
514
|
}
|
|
499
515
|
|
|
500
516
|
log.progress("Installing models...");
|
|
501
|
-
const installArgs =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
];
|
|
517
|
+
const installArgs = buildEngineInstallArgs({
|
|
518
|
+
noCache,
|
|
519
|
+
ttsLangs: options.ttsLangs,
|
|
520
|
+
vad: options.vad,
|
|
521
|
+
diarize: options.diarize,
|
|
522
|
+
});
|
|
508
523
|
const proc = Bun.spawnSync([binPath, ...installArgs], {
|
|
509
524
|
stdout: "pipe",
|
|
510
525
|
stderr: "pipe",
|
|
@@ -520,7 +535,11 @@ export async function downloadEngine(
|
|
|
520
535
|
throw new Error(detail ? `Failed to install models: ${detail}` : "Failed to install models");
|
|
521
536
|
}
|
|
522
537
|
|
|
523
|
-
|
|
538
|
+
// Warm the FluidAudio Kokoro CoreML cache only when a Kokoro language is
|
|
539
|
+
// requested. Russian (`ru`) routes through Vosk-TTS, not Kokoro, so a
|
|
540
|
+
// Russian-only install has nothing to warm.
|
|
541
|
+
const wantsKokoro = (options.ttsLangs ?? []).some((l) => l !== "ru");
|
|
542
|
+
if (wantsKokoro) {
|
|
524
543
|
await warmDarwinKokoro(binPath);
|
|
525
544
|
}
|
|
526
545
|
|
package/src/engine.ts
CHANGED
|
@@ -340,10 +340,16 @@ export async function detectTextLanguageEngine(
|
|
|
340
340
|
return parseLangResult(stdout);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
export interface TtsLanguageCapability {
|
|
344
|
+
code: string;
|
|
345
|
+
engines: string[];
|
|
346
|
+
}
|
|
347
|
+
|
|
343
348
|
export interface EngineCapabilities {
|
|
344
349
|
protocolVersion: number;
|
|
345
350
|
backend: string;
|
|
346
351
|
features: string[];
|
|
352
|
+
tts?: { languages: TtsLanguageCapability[] };
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
let cachedEngineCapabilities:
|
package/src/install-plan.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
export interface InstallPlanOptions {
|
|
14
14
|
noCache?: boolean;
|
|
15
15
|
backend?: string;
|
|
16
|
-
|
|
16
|
+
/** Resolved TTS language codes, e.g. ["en", "ru"]. */
|
|
17
|
+
ttsLangs?: string[];
|
|
17
18
|
vad?: boolean;
|
|
18
19
|
diarize?: boolean;
|
|
19
20
|
}
|
|
@@ -70,17 +71,29 @@ const G2P_CHARSIU_FILES: PlanFile[] = [
|
|
|
70
71
|
{ relPath: "models/g2p/byt5-tiny/decoder_with_past_model.onnx", sizeBytes: 5_427_260 },
|
|
71
72
|
];
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{ relPath: "models/kokoro-82m/voices/
|
|
82
|
-
{ relPath: "models/kokoro-82m/voices/
|
|
83
|
-
|
|
74
|
+
// Kokoro ONNX graph (shared by all Kokoro languages on non-darwin builds).
|
|
75
|
+
const KOKORO_GRAPH_FILE: PlanFile = { relPath: "models/kokoro-82m/model.onnx", sizeBytes: 325_532_387 };
|
|
76
|
+
|
|
77
|
+
// Per-language default voice files.
|
|
78
|
+
// Multilingual voice packs for es/fr/it/pt (#212).
|
|
79
|
+
// em_alex (es, male), ff_siwis (fr, female — only French voice in Kokoro v1.0),
|
|
80
|
+
// im_nicola (it, male), pm_alex (pt, male).
|
|
81
|
+
const KOKORO_VOICE_FILES: Record<string, PlanFile> = {
|
|
82
|
+
en: { relPath: "models/kokoro-82m/voices/am_michael.bin", sizeBytes: 522_240 },
|
|
83
|
+
es: { relPath: "models/kokoro-82m/voices/em_alex.bin", sizeBytes: 522_240 },
|
|
84
|
+
fr: { relPath: "models/kokoro-82m/voices/ff_siwis.bin", sizeBytes: 522_240 },
|
|
85
|
+
it: { relPath: "models/kokoro-82m/voices/im_nicola.bin", sizeBytes: 522_240 },
|
|
86
|
+
pt: { relPath: "models/kokoro-82m/voices/pm_alex.bin", sizeBytes: 522_240 },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function kokoroPlanFiles(langs: string[]): PlanFile[] {
|
|
90
|
+
const files: PlanFile[] = [KOKORO_GRAPH_FILE];
|
|
91
|
+
for (const l of langs) {
|
|
92
|
+
const v = KOKORO_VOICE_FILES[l];
|
|
93
|
+
if (v) files.push(v);
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
84
97
|
|
|
85
98
|
const VOSK_RU_FILES: PlanFile[] = [
|
|
86
99
|
{ relPath: "models/vosk-ru/model.onnx", sizeBytes: 179_314_533 },
|
|
@@ -240,53 +253,59 @@ export async function renderInstallPlan(options: InstallPlanOptions = {}): Promi
|
|
|
240
253
|
),
|
|
241
254
|
);
|
|
242
255
|
|
|
243
|
-
|
|
256
|
+
const ttsLangs = options.ttsLangs ?? [];
|
|
257
|
+
// Darwin ANE serves every non-ru language through Kokoro (en/es/fr/it/pt + the
|
|
258
|
+
// ANE-only hi/ja/zh), so the warm-up gate mirrors engine-install.ts's
|
|
259
|
+
// `some((l) => l !== "ru")`. The ONNX component, by contrast, only covers the
|
|
260
|
+
// graph-backed en/es/fr/it/pt set (hi/ja/zh are darwin-only and rejected on ONNX).
|
|
261
|
+
const wantsAnyKokoro = ttsLangs.some((l) => l !== "ru");
|
|
262
|
+
const wantsOnnxKokoro = ttsLangs.some((l) => ["en", "es", "fr", "it", "pt"].includes(l));
|
|
263
|
+
const wantsG2p = ttsLangs.some((l) => ["es", "fr", "it", "pt"].includes(l));
|
|
264
|
+
const wantsRu = ttsLangs.includes("ru");
|
|
265
|
+
|
|
266
|
+
if (ttsLangs.length > 0) {
|
|
244
267
|
if (isDarwinArm64()) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
name: "TTS Kokoro EN",
|
|
257
|
-
note: `${FLUID_KOKORO_CACHE_NOTE} (${fluidKokoroCachePath()})`,
|
|
258
|
-
});
|
|
268
|
+
if (wantsAnyKokoro) {
|
|
269
|
+
warmups.push({
|
|
270
|
+
name: "TTS Kokoro (ANE)",
|
|
271
|
+
note: `${FLUID_KOKORO_CACHE_NOTE} (${fluidKokoroCachePath()})`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (wantsRu) {
|
|
275
|
+
components.push(
|
|
276
|
+
bundleComponent(cacheRoot, "TTS Vosk RU", "model cache", VOSK_RU_FILES, noCache, "Russian ru-vosk-* voices"),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
259
279
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
noCache,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
);
|
|
280
|
+
if (wantsOnnxKokoro) {
|
|
281
|
+
components.push(
|
|
282
|
+
bundleComponent(
|
|
283
|
+
cacheRoot,
|
|
284
|
+
"TTS Kokoro graph + voices",
|
|
285
|
+
"model cache",
|
|
286
|
+
kokoroPlanFiles(ttsLangs),
|
|
287
|
+
noCache,
|
|
288
|
+
`voices for ${ttsLangs.filter((l) => l !== "ru").join(", ")}`,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (wantsG2p) {
|
|
293
|
+
components.push(
|
|
294
|
+
bundleComponent(
|
|
295
|
+
cacheRoot,
|
|
296
|
+
"G2P CharsiuG2P byt5-tiny",
|
|
297
|
+
"model cache",
|
|
298
|
+
G2P_CHARSIU_FILES,
|
|
299
|
+
noCache,
|
|
300
|
+
"multilingual G2P for es/fr/it/pt (CC-BY 4.0)",
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (wantsRu) {
|
|
305
|
+
components.push(
|
|
306
|
+
bundleComponent(cacheRoot, "TTS Vosk RU", "model cache", VOSK_RU_FILES, noCache, "Russian ru-vosk-* voices"),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
290
309
|
}
|
|
291
310
|
}
|
|
292
311
|
|
|
@@ -370,7 +389,7 @@ export async function renderInstallPlan(options: InstallPlanOptions = {}): Promi
|
|
|
370
389
|
" - install warms the ASR backend after downloads; CoreML warm-up is typically 20-30 s, ONNX warm-up is about 500 ms.",
|
|
371
390
|
);
|
|
372
391
|
|
|
373
|
-
if (
|
|
392
|
+
if (ttsLangs.length > 0 && wantsAnyKokoro && isDarwinArm64()) {
|
|
374
393
|
lines.push(
|
|
375
394
|
" - --tts also warms FluidAudio Kokoro CoreML; FluidAudio may download/compile its own Kokoro cache on first use.",
|
|
376
395
|
);
|
|
@@ -382,7 +401,7 @@ export async function renderInstallPlan(options: InstallPlanOptions = {}): Promi
|
|
|
382
401
|
options.noCache ? "--no-cache" : "",
|
|
383
402
|
options.backend === "coreml" ? "--coreml" : "",
|
|
384
403
|
options.backend === "onnx" ? "--onnx" : "",
|
|
385
|
-
|
|
404
|
+
...(ttsLangs.length > 0 ? ["--tts", ...ttsLangs] : []),
|
|
386
405
|
options.vad ? "--vad" : "",
|
|
387
406
|
options.diarize ? "--diarize" : "",
|
|
388
407
|
].filter(Boolean);
|
package/src/lib.ts
CHANGED
|
@@ -29,9 +29,13 @@ export type {
|
|
|
29
29
|
TranscribeResult,
|
|
30
30
|
} from "./types";
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Install TTS models for the given languages (default: English only, matching
|
|
34
|
+
* `kesha install --tts`). Pass e.g. `["en", "ru"]` for more. Unsupported-on-platform
|
|
35
|
+
* codes are rejected by the engine.
|
|
36
|
+
*/
|
|
37
|
+
export async function downloadTts(noCache = false, langs: string[] = ["en"]): Promise<void> {
|
|
38
|
+
await downloadEngine(noCache, undefined, { ttsLangs: langs });
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** @deprecated Use `downloadModel` instead. */
|
package/src/log.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Colorizer indirection (#531). `picocolors` decides color support once at
|
|
5
|
+
* import time (honoring `NO_COLOR`, TTY, `FORCE_COLOR`). To support a runtime
|
|
6
|
+
* `--no-color` flag we keep a swappable reference: `setColorEnabled(false)`
|
|
7
|
+
* replaces it with a no-op colorizer. Methods below read `colors.*`, never the
|
|
8
|
+
* frozen default binding, so the toggle takes effect immediately.
|
|
9
|
+
*/
|
|
10
|
+
// Narrower type than the default export (which also carries `createColors`),
|
|
11
|
+
// so the no-op colorizer from `createColors(false)` is assignable back.
|
|
12
|
+
let colors: ReturnType<typeof pc.createColors> = pc;
|
|
13
|
+
|
|
14
|
+
/** Force colors on/off at runtime (used by `--no-color`). */
|
|
15
|
+
export function setColorEnabled(enabled: boolean): void {
|
|
16
|
+
colors = enabled ? pc : pc.createColors(false);
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Debug mode (#148): when `KESHA_DEBUG` is truthy OR the caller has flipped
|
|
5
21
|
* `log.debugEnabled = true` (via `--debug`), `log.debug()` writes structured
|
|
@@ -28,11 +44,30 @@ const PROCESS_T0_MS = performance.now();
|
|
|
28
44
|
|
|
29
45
|
export const log = {
|
|
30
46
|
info: (msg: string) => console.log(msg),
|
|
31
|
-
success: (msg: string) => console.log(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
success: (msg: string) => console.log(colors.green(msg)),
|
|
48
|
+
// `--quiet` (#526) silences status/progress chatter; warnings and errors
|
|
49
|
+
// always print, and results are written straight to stdout (not via log.*).
|
|
50
|
+
progress: (msg: string) => {
|
|
51
|
+
if (log.quietEnabled) return;
|
|
52
|
+
console.log(colors.cyan(msg));
|
|
53
|
+
},
|
|
54
|
+
// stderr via process.stderr.write (not console.error): Bun auto-colors
|
|
55
|
+
// console.error red in a TTY, and that decision is frozen at startup — it
|
|
56
|
+
// would survive `--no-color`. Writing the stream directly leaves picocolors
|
|
57
|
+
// (which `setColorEnabled` controls) as the only colorizer, so `--no-color`
|
|
58
|
+
// and `CI=true` produce genuinely plain output. (#531)
|
|
59
|
+
warn: (msg: string) => void process.stderr.write(colors.yellow(msg) + "\n"),
|
|
60
|
+
error: (msg: string) => void process.stderr.write(colors.red(msg) + "\n"),
|
|
61
|
+
// Status/progress line on STDERR (e.g. `say --out`, where stdout may carry
|
|
62
|
+
// raw audio bytes). Suppressed under `--quiet` and colored via the swappable
|
|
63
|
+
// colorizer so `--no-color` applies; process.stderr.write (not console.error)
|
|
64
|
+
// avoids Bun's startup-frozen TTY auto-red. (#526/#531)
|
|
65
|
+
status: (msg: string) => {
|
|
66
|
+
if (log.quietEnabled) return;
|
|
67
|
+
process.stderr.write(colors.cyan(msg) + "\n");
|
|
68
|
+
},
|
|
35
69
|
|
|
70
|
+
quietEnabled: false,
|
|
36
71
|
debugEnabled: false,
|
|
37
72
|
isDebugEnabled(): boolean {
|
|
38
73
|
return this.debugEnabled || envDebug();
|
|
@@ -47,7 +82,7 @@ export const log = {
|
|
|
47
82
|
// process", read the prefix difference; for cross-process spans,
|
|
48
83
|
// the spawn→exit `dt=Nms` inside the message remains authoritative.
|
|
49
84
|
const t = Math.round(performance.now() - PROCESS_T0_MS);
|
|
50
|
-
|
|
85
|
+
process.stderr.write(colors.dim(`[debug +${t}ms] ${msg}`) + "\n");
|
|
51
86
|
}
|
|
52
87
|
},
|
|
53
88
|
};
|