@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 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
- ### Piper voices (Russian: `ru-denis`)
30
+ ### Vosk-TTS (Russian, multi-speaker)
31
31
 
32
- Text-to-speech (multilingual, Piper VITS). MIT. Voice packs from [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices).
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
- ### CharsiuG2P ByT5-tiny (ONNX export)
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
- Grapheme-to-phoneme conversion for Kokoro and Piper pipelines (#123).
36
+ ### misaki-rs (English G2P)
37
37
 
38
- **License: CC-BY 4.0** (declared on the ONNX-export repo). Attribution required.
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 Linux x64
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 + Piper VITS for TTS, plus macOS AVSpeechSynthesizer for ~180 system voices with zero install.
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: npm
11
- packages: [@drakulavich/kesha-voice-kit]
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, Piper for Russian). For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
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 → Piper
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 + Piper + ~180 macos-* voices
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 --global @drakulavich/kesha-voice-kit # or: npm i -g @drakulavich/kesha-voice-kit
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 + Piper RU + ONNX G2P (~490 MB more, for TTS)
75
+ kesha install --tts # adds Kokoro + Vosk-TTS RU (~990 MB more, for TTS)
77
76
  ```
78
77
 
79
- No system deps — G2P runs as ONNX alongside Kokoro/Piper. `macos-*` voices need no install either — they use voices already on the Mac.
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 (Piper `ru-denis`), plus any macOS system voice via `--voice macos-*`.
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.4.4",
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.4.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.com/drakulavich/kesha-voice-kit#readme",
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: "Synthesize speech from text (TTS). Writes WAV to stdout (or --out file).",
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 WAV to file instead of stdout" },
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 wav = await say(opts);
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 WAV bytes when --out is omitted.
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(wav);
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
  }