@drakulavich/kesha-voice-kit 1.5.0 → 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 +3 -1
- package/README.md +1 -2
- package/package.json +3 -3
- package/src/cli/say.ts +53 -6
- package/src/say.ts +23 -0
package/NOTICES.md
CHANGED
|
@@ -29,7 +29,9 @@ Text-to-speech (English). Apache 2.0. ONNX export by [onnx-community/Kokoro-82M-
|
|
|
29
29
|
|
|
30
30
|
### Vosk-TTS (Russian, multi-speaker)
|
|
31
31
|
|
|
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`).
|
|
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
|
+
|
|
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.
|
|
33
35
|
|
|
34
36
|
### misaki-rs (English G2P)
|
|
35
37
|
|
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/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
|
}
|