@drakulavich/kesha-voice-kit 1.4.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICES.md +5 -9
- package/README.md +1 -2
- package/SKILL.md +10 -11
- package/package.json +3 -3
- package/src/cli/say.ts +53 -6
- package/src/say.ts +23 -0
package/NOTICES.md
CHANGED
|
@@ -27,19 +27,15 @@ Pinned to the `v6.2.1` tag.
|
|
|
27
27
|
|
|
28
28
|
Text-to-speech (English). Apache 2.0. ONNX export by [onnx-community/Kokoro-82M-v1.0-ONNX](https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX).
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### Vosk-TTS (Russian, multi-speaker)
|
|
31
31
|
|
|
32
|
-
Text-to-speech (
|
|
32
|
+
Text-to-speech (Russian, 5 baked-in speakers). Apache 2.0. Model: [alphacep/vosk-tts](https://huggingface.co/alphacep/vosk-tts) (`vosk-model-tts-ru-0.9-multi`).
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
**Runtime code:** Originally pulled from `crates.io` as `vosk-tts-rs 0.1.0`. As of #216 the runtime subset is **vendored** under `rust/vendor/vosk-tts/` (Apache 2.0; copied from [`andreytkachenko/vosk-tts-rs`](https://github.com/andreytkachenko/vosk-tts-rs) — see `rust/vendor/vosk-tts/LICENSE` and `rust/vendor/vosk-tts/NOTICE`). The vendored copy drops the upstream gRPC server/CLI/HTTP-model-fetch modules and replaces the `tokenizers` crate with a small inline BERT WordPiece tokenizer, eliminating the `prost`, `tonic`, `tokio`, `axum`, `hyper`, `reqwest`, `tokenizers`, `onig_sys`, `esaxx-rs`, `bzip2-sys`, `lzma-sys`, and `zstd-sys` transitive dependencies that broke Windows MSVC linkage.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
### misaki-rs (English G2P)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- ONNX export: [klebster/g2p_multilingual_byT5_tiny_onnx](https://huggingface.co/klebster/g2p_multilingual_byT5_tiny_onnx) by Kleber Noel — CC-BY 4.0.
|
|
41
|
-
- Base checkpoint: [charsiu/g2p_multilingual_byT5_tiny_16_layers_100](https://huggingface.co/charsiu/g2p_multilingual_byT5_tiny_16_layers_100) — **no explicit license declared on HuggingFace**; usage here follows the CC-BY 4.0 declared by the downstream export and the paper's terms.
|
|
42
|
-
- Paper: Zhu, J., Zhang, C., & Jurgens, D. (2022). "ByT5 model for massively multilingual grapheme-to-phoneme conversion." [arXiv:2204.03067](https://arxiv.org/abs/2204.03067) · Interspeech 2022.
|
|
38
|
+
Embedded grapheme-to-phoneme for English (Kokoro pipeline, #207). MIT. Source: [`misaki-rs`](https://github.com/MicheleYin/misaki-rs).
|
|
43
39
|
|
|
44
40
|
## System libraries linked by the engine binary
|
|
45
41
|
|
package/README.md
CHANGED
|
@@ -117,8 +117,7 @@ const text = await transcribe("audio.ogg"); // transcribe
|
|
|
117
117
|
## Requirements
|
|
118
118
|
|
|
119
119
|
- [Bun](https://bun.sh) >= 1.3
|
|
120
|
-
- macOS arm64 or
|
|
121
|
-
- Windows x64 temporarily unsupported in v1.5.0 — Vosk-TTS native deps trigger MSVC link conflicts. Tracked in [#216](https://github.com/drakulavich/kesha-voice-kit/issues/216).
|
|
120
|
+
- macOS arm64, Linux x64, or Windows x64
|
|
122
121
|
|
|
123
122
|
## Contributing
|
|
124
123
|
|
package/SKILL.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kesha-voice-kit
|
|
3
|
-
description: Local multilingual voice toolkit — speech-to-text (STT), text-to-speech (TTS), and language detection. Runs entirely offline on Apple Silicon, Linux, and Windows. No API keys, no cloud. NVIDIA Parakeet TDT for STT across 25 European languages, Kokoro-82M +
|
|
3
|
+
description: Local multilingual voice toolkit — speech-to-text (STT), text-to-speech (TTS), and language detection. Runs entirely offline on Apple Silicon, Linux, and Windows. No API keys, no cloud. NVIDIA Parakeet TDT for STT across 25 European languages, Kokoro-82M + Vosk-TTS for TTS, plus macOS AVSpeechSynthesizer for ~180 system voices with zero install.
|
|
4
4
|
emoji: 🎙️
|
|
5
5
|
|
|
6
6
|
requires:
|
|
7
7
|
bins: [kesha]
|
|
8
8
|
|
|
9
9
|
install:
|
|
10
|
-
- kind:
|
|
11
|
-
|
|
12
|
-
flags: [--global]
|
|
10
|
+
- kind: bash
|
|
11
|
+
cmd: bun add -g "@drakulavich/kesha-voice-kit"
|
|
13
12
|
- kind: bash
|
|
14
13
|
cmd: kesha install
|
|
15
14
|
---
|
|
@@ -23,7 +22,7 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
|
|
|
23
22
|
## When to use
|
|
24
23
|
|
|
25
24
|
- **Voice memo arrived** (Telegram, WhatsApp, Slack, Signal .ogg/.opus/.m4a): transcribe with `kesha --json <path>` and branch on the detected language.
|
|
26
|
-
- **Need to reply with audio**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English,
|
|
25
|
+
- **Need to reply with audio**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
|
|
27
26
|
- **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
27
|
|
|
29
28
|
## STT: transcribe audio
|
|
@@ -57,9 +56,9 @@ Use `lang` (or the more detailed `audioLanguage`/`textLanguage`) to decide how t
|
|
|
57
56
|
|
|
58
57
|
```bash
|
|
59
58
|
kesha say "Hello, world" > hello.wav # auto-routes en → Kokoro-82M
|
|
60
|
-
kesha say "Привет, мир" > privet.wav # auto-routes ru →
|
|
59
|
+
kesha say "Привет, мир" > privet.wav # auto-routes ru → Vosk-TTS
|
|
61
60
|
kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice — German, French, Italian, ...
|
|
62
|
-
kesha say --list-voices # Kokoro +
|
|
61
|
+
kesha say --list-voices # Kokoro + Vosk-TTS + ~180 macos-* voices
|
|
63
62
|
```
|
|
64
63
|
|
|
65
64
|
Output: WAV mono float32. `--out <path>` writes to a file instead of stdout.
|
|
@@ -71,18 +70,18 @@ Output: WAV mono float32. `--out <path>` writes to a file instead of stdout.
|
|
|
71
70
|
## Install
|
|
72
71
|
|
|
73
72
|
```bash
|
|
74
|
-
bun add
|
|
73
|
+
bun add -g @drakulavich/kesha-voice-kit # global CLI install
|
|
75
74
|
kesha install # downloads engine (~350 MB)
|
|
76
|
-
kesha install --tts # adds Kokoro +
|
|
75
|
+
kesha install --tts # adds Kokoro + Vosk-TTS RU (~990 MB more, for TTS)
|
|
77
76
|
```
|
|
78
77
|
|
|
79
|
-
No system deps — G2P
|
|
78
|
+
No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled inside Vosk-TTS. `macos-*` voices need no install either — they use voices already on the Mac.
|
|
80
79
|
|
|
81
80
|
## Supported languages
|
|
82
81
|
|
|
83
82
|
**Speech-to-text (25):** Bulgarian, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Spanish, Swedish, Ukrainian.
|
|
84
83
|
|
|
85
|
-
**Text-to-speech:** English (Kokoro-82M, ~70 voices), Russian (
|
|
84
|
+
**Text-to-speech:** English (Kokoro-82M, ~70 voices), Russian (Vosk-TTS, 5 baked-in speakers — default `ru-vosk-m02`), plus any macOS system voice via `--voice macos-*`.
|
|
86
85
|
|
|
87
86
|
## Performance
|
|
88
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Open-source voice toolkit. Speech-to-text (25 langs, ~19x faster than Whisper on Apple Silicon, ONNX fallback on CPU), text-to-speech (Kokoro + Vosk-TTS + ~180 macOS system voices, SSML), voice-activity detection, language detection (107 langs). Rust engine, OpenClaw skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
32
|
"keshaEngine": {
|
|
33
|
-
"version": "1.
|
|
33
|
+
"version": "1.6.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "bun test",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"type": "git",
|
|
59
59
|
"url": "git+https://github.com/drakulavich/kesha-voice-kit.git"
|
|
60
60
|
},
|
|
61
|
-
"homepage": "https://github.
|
|
61
|
+
"homepage": "https://drakulavich.github.io/kesha-voice-kit/",
|
|
62
62
|
"bugs": {
|
|
63
63
|
"url": "https://github.com/drakulavich/kesha-voice-kit/issues"
|
|
64
64
|
},
|
package/src/cli/say.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { detectTextLanguageEngine, getEngineBinPath } from "../engine";
|
|
3
3
|
import { log } from "../log";
|
|
4
|
-
import { say, SayError } from "../say";
|
|
4
|
+
import { say, SayError, type SayFormat } from "../say";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Darwin defaults to AVSpeech Milena — zero install, no model download required.
|
|
@@ -54,19 +54,34 @@ async function resolveText(inline: string | undefined): Promise<string> {
|
|
|
54
54
|
export const sayCommand = defineCommand({
|
|
55
55
|
meta: {
|
|
56
56
|
name: "say",
|
|
57
|
-
description:
|
|
57
|
+
description:
|
|
58
|
+
"Synthesize speech from text (TTS). Writes audio to stdout (or --out file). Defaults to WAV; use --format ogg-opus for messenger-ready voice notes.",
|
|
58
59
|
},
|
|
59
60
|
args: {
|
|
60
61
|
text: { type: "positional", required: false, description: "Text to speak (stdin if omitted)" },
|
|
61
62
|
voice: { type: "string", description: "Voice id, e.g. en-am_michael" },
|
|
62
63
|
lang: { type: "string", description: "BCP 47 language code (default en-us)" },
|
|
63
|
-
out: { type: "string", description: "Write
|
|
64
|
+
out: { type: "string", description: "Write audio to file instead of stdout" },
|
|
64
65
|
rate: { type: "string", description: "Speaking rate 0.5–2.0", default: "1.0" },
|
|
65
66
|
"list-voices": { type: "boolean", description: "List installed voices and exit" },
|
|
66
67
|
ssml: {
|
|
67
68
|
type: "boolean",
|
|
68
69
|
description: "Parse input as SSML (supports <speak>, <break>; strips unknown tags)",
|
|
69
70
|
},
|
|
71
|
+
format: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description:
|
|
74
|
+
"Output format: wav (default) or ogg-opus (Telegram-ready voice note). Inferred from --out extension when omitted.",
|
|
75
|
+
},
|
|
76
|
+
bitrate: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Opus bitrate in bits/sec (e.g. 32000). Only with --format ogg-opus.",
|
|
79
|
+
},
|
|
80
|
+
"sample-rate": {
|
|
81
|
+
type: "string",
|
|
82
|
+
description:
|
|
83
|
+
"Opus encoder sample rate (8000/12000/16000/24000/48000). Only with --format ogg-opus.",
|
|
84
|
+
},
|
|
70
85
|
verbose: {
|
|
71
86
|
type: "boolean",
|
|
72
87
|
description: "Log TTS synthesis time to stderr",
|
|
@@ -94,6 +109,35 @@ export const sayCommand = defineCommand({
|
|
|
94
109
|
const explicitVoice = typeof args.voice === "string" ? args.voice : undefined;
|
|
95
110
|
const voice = explicitVoice ?? (await autoRouteVoice(text));
|
|
96
111
|
|
|
112
|
+
// Validate --format up front so we surface a clear error before spawning
|
|
113
|
+
// the engine subprocess. The engine repeats the check authoritatively, but
|
|
114
|
+
// catching it here gives the user a faster failure mode in scripts.
|
|
115
|
+
const fmtArg = typeof args.format === "string" ? args.format.toLowerCase() : undefined;
|
|
116
|
+
let format: SayFormat | undefined;
|
|
117
|
+
if (fmtArg) {
|
|
118
|
+
if (fmtArg === "wav" || fmtArg === "ogg-opus") {
|
|
119
|
+
format = fmtArg;
|
|
120
|
+
} else if (fmtArg === "opus" || fmtArg === "ogg") {
|
|
121
|
+
format = "ogg-opus";
|
|
122
|
+
} else {
|
|
123
|
+
log.error(`unknown --format '${args.format}'. supported: wav, ogg-opus`);
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reject --bitrate / --sample-rate with WAV up front to surface the error fast.
|
|
129
|
+
const hasOpusOnlyFlag = Boolean(args.bitrate) || Boolean(args["sample-rate"]);
|
|
130
|
+
if (hasOpusOnlyFlag) {
|
|
131
|
+
const outExt = typeof args.out === "string"
|
|
132
|
+
? args.out.split(".").pop()?.toLowerCase()
|
|
133
|
+
: undefined;
|
|
134
|
+
const impliesOpus = outExt && ["ogg", "opus", "oga"].includes(outExt);
|
|
135
|
+
if (format === "wav" || (format === undefined && !impliesOpus)) {
|
|
136
|
+
log.error("--bitrate and --sample-rate are only valid with --format ogg-opus");
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
97
141
|
const opts = {
|
|
98
142
|
text,
|
|
99
143
|
voice,
|
|
@@ -101,18 +145,21 @@ export const sayCommand = defineCommand({
|
|
|
101
145
|
out: typeof args.out === "string" ? args.out : undefined,
|
|
102
146
|
rate: args.rate ? Number(args.rate) : undefined,
|
|
103
147
|
ssml: Boolean(args.ssml),
|
|
148
|
+
format,
|
|
149
|
+
bitrate: args.bitrate ? Number(args.bitrate) : undefined,
|
|
150
|
+
sampleRate: args["sample-rate"] ? Number(args["sample-rate"]) : undefined,
|
|
104
151
|
};
|
|
105
152
|
|
|
106
153
|
try {
|
|
107
154
|
const startedAt = performance.now();
|
|
108
|
-
const
|
|
155
|
+
const audio = await say(opts);
|
|
109
156
|
const ttsTimeMs = Math.round(performance.now() - startedAt);
|
|
110
157
|
if (args.verbose) {
|
|
111
|
-
// stderr — stdout may carry raw
|
|
158
|
+
// stderr — stdout may carry raw audio bytes when --out is omitted.
|
|
112
159
|
console.error(`TTS time: ${ttsTimeMs}ms`);
|
|
113
160
|
}
|
|
114
161
|
if (!opts.out) {
|
|
115
|
-
process.stdout.write(
|
|
162
|
+
process.stdout.write(audio);
|
|
116
163
|
}
|
|
117
164
|
} catch (err) {
|
|
118
165
|
if (err instanceof SayError) {
|
package/src/say.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { getEngineBinPath, isEngineInstalled } from "./engine";
|
|
2
2
|
import { log } from "./log";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Wire format for the synthesized audio. Matches the engine's `--format` flag.
|
|
6
|
+
* - `wav` (default): RIFF WAV at the engine's native sample rate.
|
|
7
|
+
* - `ogg-opus`: OGG-encapsulated Opus, mono. The format Telegram, WhatsApp,
|
|
8
|
+
* Signal, and Discord render as native voice messages. See #223.
|
|
9
|
+
*/
|
|
10
|
+
export type SayFormat = "wav" | "ogg-opus";
|
|
11
|
+
|
|
4
12
|
export interface SayOptions {
|
|
5
13
|
/**
|
|
6
14
|
* Text to synthesize. Required for programmatic callers — `say()` does not
|
|
@@ -18,6 +26,18 @@ export interface SayOptions {
|
|
|
18
26
|
rate?: number;
|
|
19
27
|
/** Parse `text` as SSML (`<speak>…<break time="500ms"/>…</speak>`). See issue #122. */
|
|
20
28
|
ssml?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Output audio format. Defaults to `wav` (or inferred from the `out`
|
|
31
|
+
* extension when omitted: `.wav` → wav, `.ogg`/`.opus` → ogg-opus).
|
|
32
|
+
*/
|
|
33
|
+
format?: SayFormat;
|
|
34
|
+
/** Opus bitrate in bits/second. Only valid with `format: "ogg-opus"`. Default 32000. */
|
|
35
|
+
bitrate?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Encoder sample rate in Hz. Only valid with `format: "ogg-opus"`.
|
|
38
|
+
* Must be one of 8000, 12000, 16000, 24000, 48000. Default 24000.
|
|
39
|
+
*/
|
|
40
|
+
sampleRate?: number;
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
/** Build the argv passed to `kesha-engine say` (pure, unit-testable). */
|
|
@@ -28,6 +48,9 @@ export function buildSayArgs(o: SayOptions): string[] {
|
|
|
28
48
|
if (o.out) args.push("--out", o.out);
|
|
29
49
|
if (o.rate !== undefined && o.rate !== 1.0) args.push("--rate", String(o.rate));
|
|
30
50
|
if (o.ssml) args.push("--ssml");
|
|
51
|
+
if (o.format) args.push("--format", o.format);
|
|
52
|
+
if (o.bitrate !== undefined) args.push("--bitrate", String(o.bitrate));
|
|
53
|
+
if (o.sampleRate !== undefined) args.push("--sample-rate", String(o.sampleRate));
|
|
31
54
|
if (o.text !== undefined && o.text.length > 0) args.push(o.text);
|
|
32
55
|
return args;
|
|
33
56
|
}
|