@drakulavich/kesha-voice-kit 1.6.0 → 1.7.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 CHANGED
@@ -69,6 +69,22 @@ kesha say "Hello, world" > hello.wav
69
69
  kesha say "Привет, мир" > privet.wav # auto-routes (Milena on darwin, ru-vosk-m02 elsewhere)
70
70
  ```
71
71
 
72
+ **Russian abbreviations** (`ru-vosk-*` voices):
73
+
74
+ ```bash
75
+ # Auto-detect on by default — ФСБ reads as "эф-эс-бэ"
76
+ kesha say --voice ru-vosk-m02 'ФСБ объявила решение.'
77
+
78
+ # Force a literal reading (Vosk reads as "фсб")
79
+ kesha say --voice ru-vosk-m02 --no-expand-abbrev 'ФСБ.'
80
+
81
+ # Explicit SSML control (overrides the rule and the stop-list)
82
+ kesha say --voice ru-vosk-m02 --ssml \
83
+ '<speak><say-as interpret-as="characters">КОТ</say-as></speak>'
84
+ ```
85
+
86
+ Detection rule: auto-expand fires when the token cannot be pronounced as a natural Russian syllable — length ≤ 2 (ИП, ЕС), 0 vowels (ФСБ, СНГ), 2+ consecutive vowels (ОАЭ), or 2+ consecutive consonants (США, ЦСКА). Tokens with strict CVC/CVCV alternation pass through (ВОЗ → "воз", НАТО → "нато", ОПЕК → "опек"). Small stop-list for common short words (ОН, МЫ, ВЫ, КАК, ЧТО, …) and Ъ/Ь-containing tokens are always skipped. See [#232](https://github.com/drakulavich/kesha-voice-kit/issues/232).
87
+
72
88
  macOS system voices, SSML, voice listing, and the full voice catalogue: [docs/tts.md](docs/tts.md).
73
89
 
74
90
  ## Performance
package/SKILL.md CHANGED
@@ -17,12 +17,13 @@ install:
17
17
 
18
18
  Local voice toolkit: transcribe voice messages to text, synthesize speech, detect language of audio or text. Fully offline after `kesha install`. No API keys, no per-minute billing.
19
19
 
20
- **Trigger keywords for when to use this skill:** voice message, voice memo, .ogg, .wav, .mp3, audio file, transcribe, transcription, speech-to-text, STT, text-to-speech, TTS, synthesize speech, say, multilingual voice, multilingual ASR, language detection, offline voice, privacy, Apple Silicon, CoreML.
20
+ **Trigger keywords for when to use this skill:** voice message, voice memo, voice note, .ogg, .opus, .wav, .mp3, audio file, transcribe, transcription, speech-to-text, STT, text-to-speech, TTS, synthesize speech, say, telegram voice note, whatsapp voice note, ogg-opus, opus, multilingual voice, multilingual ASR, language detection, offline voice, privacy, Apple Silicon, CoreML.
21
21
 
22
22
  ## When to use
23
23
 
24
24
  - **Voice memo arrived** (Telegram, WhatsApp, Slack, Signal .ogg/.opus/.m4a): transcribe with `kesha --json <path>` and branch on the detected language.
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).
25
+ - **Need to reply with audio (file playback)**: 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).
26
+ - **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into the messenger-native format with `kesha say "<text>" --format ogg-opus --out reply.ogg`. Default is mono 24 kHz @ 32 kbps — what Telegram `sendVoice` expects. No `ffmpeg` round-trip needed.
26
27
  - **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.
27
28
 
28
29
  ## STT: transcribe audio
@@ -61,7 +62,19 @@ kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice —
61
62
  kesha say --list-voices # Kokoro + Vosk-TTS + ~180 macos-* voices
62
63
  ```
63
64
 
64
- Output: WAV mono float32. `--out <path>` writes to a file instead of stdout.
65
+ Output: WAV mono float32 by default. `--out <path>` writes to a file instead of stdout.
66
+
67
+ **Voice notes (Telegram / WhatsApp / Signal / Discord):** add `--format ogg-opus` to emit OGG/Opus directly — the format messenger APIs render as a native voice message:
68
+
69
+ ```bash
70
+ kesha say "Hello there" --format ogg-opus --out reply.ogg # 24 kHz @ 32 kbps mono — Telegram-grade
71
+ kesha say "Привет" --voice ru-vosk-m02 --format ogg-opus --out reply.ogg # Russian voice note
72
+ kesha say "Hi" --format ogg-opus --bitrate 16000 --out tiny.ogg # tinier file, intelligible but lossy
73
+ ```
74
+
75
+ Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OGG/Opus). `--bitrate` (6 000–510 000 bps) and `--sample-rate` (8 000 / 12 000 / 16 000 / 24 000 / 48 000 Hz) tune the encoder.
76
+
77
+ **Russian abbreviation handling** (`ru-vosk-*` only): all-uppercase Cyrillic tokens (length 2–5) auto-expand letter-by-letter when the token cannot be pronounced as a natural Russian syllable — `ФСБ объявила` → `эф-эс-бэ объявила`. Tokens with strict CVC/CVCV alternation pass through (ВОЗ → "воз", НАТО → "нато"). Disable per call with `--no-expand-abbrev`. Override per-token via SSML `<say-as interpret-as="characters">…</say-as>` (always wins, even with `--no-expand-abbrev`). Stop-list for common short words (ОН, МЫ, ВЫ, КАК, ЧТО) prevents false positives. Closes [#232](https://github.com/drakulavich/kesha-voice-kit/issues/232).
65
78
 
66
79
  ## Language detection standalone
67
80
 
@@ -5,9 +5,16 @@
5
5
  // through Kesha (Parakeet TDT via CoreML on Apple Silicon, ONNX elsewhere).
6
6
  // 25 languages, no cloud, ~19x faster than Whisper.
7
7
  //
8
+ // The same `kesha` CLI also produces messenger-ready voice notes via
9
+ // `kesha say --text "..." --format ogg-opus --out reply.ogg` (Telegram /
10
+ // Discord ingest OGG/Opus directly — no transcoding needed). Not wired into
11
+ // OpenClaw's media-understanding capability here; invoke it from a hook or
12
+ // agent tool when you want Kesha to speak.
13
+ //
8
14
  // Prerequisite: the `kesha` binary must be on PATH. Install it with:
9
15
  // bun add -g @drakulavich/kesha-voice-kit
10
- // kesha install
16
+ // kesha install # ASR models
17
+ // kesha install --tts # TTS models (Kokoro + Vosk-TTS), only if you use `kesha say`
11
18
  //
12
19
  // The module specifier below is split across `+` on purpose — see the
13
20
  // OpenClaw skill-scanner rule "dangerous-exec" in
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "kesha-voice-kit",
3
3
  "name": "Kesha Voice Kit",
4
- "description": "Local speech-to-text for voice messages. 25 languages, ~19x faster than Whisper on Apple Silicon.",
4
+ "description": "Local speech-to-text for voice messages — 25 languages, ~19x faster than Whisper on Apple Silicon. Bundled `kesha` CLI also synthesizes Telegram-ready OGG/Opus voice notes via `kesha say --format ogg-opus`.",
5
5
  "providers": ["kesha-voice-kit"],
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.6.0",
3
+ "version": "1.7.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.6.0"
33
+ "version": "1.7.0"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "bun test",
package/src/cli/say.ts CHANGED
@@ -82,6 +82,12 @@ export const sayCommand = defineCommand({
82
82
  description:
83
83
  "Opus encoder sample rate (8000/12000/16000/24000/48000). Only with --format ogg-opus.",
84
84
  },
85
+ "no-expand-abbrev": {
86
+ type: "boolean",
87
+ description:
88
+ "Disable Russian acronym auto-expansion (ВОЗ → 'вэ о зэ') for ru-vosk-* voices. " +
89
+ "<say-as interpret-as='characters'> still works. No effect for non-ru-vosk voices.",
90
+ },
85
91
  verbose: {
86
92
  type: "boolean",
87
93
  description: "Log TTS synthesis time to stderr",
@@ -148,6 +154,7 @@ export const sayCommand = defineCommand({
148
154
  format,
149
155
  bitrate: args.bitrate ? Number(args.bitrate) : undefined,
150
156
  sampleRate: args["sample-rate"] ? Number(args["sample-rate"]) : undefined,
157
+ noExpandAbbrev: Boolean(args["no-expand-abbrev"]),
151
158
  };
152
159
 
153
160
  try {
@@ -1,34 +1,20 @@
1
1
  import { dirname, join } from "path";
2
- import { existsSync, mkdirSync, chmodSync, readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, mkdirSync, chmodSync } from "fs";
3
3
  import { getEngineBinPath, getEngineCapabilities } from "./engine";
4
4
  import { log } from "./log";
5
5
  import { streamResponseToFile } from "./progress";
6
+ import {
7
+ readInstalledEngineVersion,
8
+ writeInstalledEngineVersion,
9
+ } from "./engine-version-marker";
6
10
 
7
- const GITHUB_REPO = "drakulavich/kesha-voice-kit";
8
-
9
- /**
10
- * Version-marker file written next to the engine binary on download (#151).
11
- * Lets `kesha install` detect a stale cached engine after a CLI upgrade and
12
- * re-download without requiring `--no-cache`.
13
- */
14
- export function getVersionMarkerPath(binPath: string): string {
15
- return `${binPath}.version`;
16
- }
11
+ export {
12
+ getVersionMarkerPath,
13
+ readInstalledEngineVersion,
14
+ writeInstalledEngineVersion,
15
+ } from "./engine-version-marker";
17
16
 
18
- /** Read the recorded version for the installed binary, or null if missing / empty. */
19
- export function readInstalledEngineVersion(binPath: string): string | null {
20
- try {
21
- const v = readFileSync(getVersionMarkerPath(binPath), "utf-8").trim();
22
- return v.length > 0 ? v : null;
23
- } catch {
24
- // Missing, unreadable, or permission-denied → treat as no marker so we re-download.
25
- return null;
26
- }
27
- }
28
-
29
- export function writeInstalledEngineVersion(binPath: string, version: string): void {
30
- writeFileSync(getVersionMarkerPath(binPath), `${version}\n`);
31
- }
17
+ const GITHUB_REPO = "drakulavich/kesha-voice-kit";
32
18
 
33
19
  function getEngineBinaryName(): string {
34
20
  const platform = process.platform;
@@ -0,0 +1,30 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+
3
+ /**
4
+ * Version-marker file written next to the engine binary on download (#151).
5
+ * Lets `kesha install` detect a stale cached engine after a CLI upgrade and
6
+ * re-download without requiring `--no-cache`.
7
+ *
8
+ * Lives in its own module so `engine-install.ts` does not co-locate
9
+ * `readFileSync` with `fetch()` — OpenClaw's plugin scanner pairs those into
10
+ * a `[potential-exfiltration]` warning at install time, which is a false
11
+ * positive for this engine-binary download path.
12
+ */
13
+ export function getVersionMarkerPath(binPath: string): string {
14
+ return `${binPath}.version`;
15
+ }
16
+
17
+ /** Read the recorded version for the installed binary, or null if missing / empty. */
18
+ export function readInstalledEngineVersion(binPath: string): string | null {
19
+ try {
20
+ const v = readFileSync(getVersionMarkerPath(binPath), "utf-8").trim();
21
+ return v.length > 0 ? v : null;
22
+ } catch {
23
+ // Missing, unreadable, or permission-denied → treat as no marker so we re-download.
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function writeInstalledEngineVersion(binPath: string, version: string): void {
29
+ writeFileSync(getVersionMarkerPath(binPath), `${version}\n`);
30
+ }
package/src/say.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getEngineBinPath, isEngineInstalled } from "./engine";
1
+ import { getEngineBinPath, isEngineInstalled, getEngineCapabilities, type EngineCapabilities } from "./engine";
2
2
  import { log } from "./log";
3
3
 
4
4
  /**
@@ -38,10 +38,17 @@ export interface SayOptions {
38
38
  * Must be one of 8000, 12000, 16000, 24000, 48000. Default 24000.
39
39
  */
40
40
  sampleRate?: number;
41
+ /**
42
+ * Disable Russian acronym auto-expansion for `ru-vosk-*` voices.
43
+ * When true, passes `--no-expand-abbrev` to the engine (requires engine
44
+ * capability `tts.ru_acronym_expansion`; silently dropped for older engines).
45
+ * `<say-as interpret-as="characters">` still works regardless of this flag.
46
+ */
47
+ noExpandAbbrev?: boolean;
41
48
  }
42
49
 
43
50
  /** Build the argv passed to `kesha-engine say` (pure, unit-testable). */
44
- export function buildSayArgs(o: SayOptions): string[] {
51
+ export function buildSayArgs(o: SayOptions, capabilities?: EngineCapabilities | null): string[] {
45
52
  const args: string[] = ["say"];
46
53
  if (o.voice) args.push("--voice", o.voice);
47
54
  if (o.lang) args.push("--lang", o.lang);
@@ -51,6 +58,16 @@ export function buildSayArgs(o: SayOptions): string[] {
51
58
  if (o.format) args.push("--format", o.format);
52
59
  if (o.bitrate !== undefined) args.push("--bitrate", String(o.bitrate));
53
60
  if (o.sampleRate !== undefined) args.push("--sample-rate", String(o.sampleRate));
61
+ if (o.noExpandAbbrev) {
62
+ const supportsRuExpand = capabilities?.features?.includes("tts.ru_acronym_expansion") ?? false;
63
+ if (supportsRuExpand) {
64
+ args.push("--no-expand-abbrev");
65
+ } else {
66
+ log.debug(
67
+ "kesha-engine does not advertise tts.ru_acronym_expansion; dropping --no-expand-abbrev",
68
+ );
69
+ }
70
+ }
54
71
  if (o.text !== undefined && o.text.length > 0) args.push(o.text);
55
72
  return args;
56
73
  }
@@ -78,7 +95,8 @@ export async function say(opts: SayOptions): Promise<Uint8Array> {
78
95
  "",
79
96
  );
80
97
  }
81
- const args = buildSayArgs({ ...opts, text: undefined });
98
+ const capabilities = opts.noExpandAbbrev ? await getEngineCapabilities() : null;
99
+ const args = buildSayArgs({ ...opts, text: undefined }, capabilities);
82
100
  const startedAt = performance.now();
83
101
  log.debug(`spawn ${getEngineBinPath()} ${args.join(" ")} (text: ${opts.text?.length ?? 0} chars)`);
84
102
  const proc = Bun.spawn([getEngineBinPath(), ...args], {