@mcptoolshop/voice-soundboard-mcp 0.0.0 → 1.0.3

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -3
  4. package/backend-python/soundboard_bridge/__init__.py +0 -0
  5. package/backend-python/soundboard_bridge/__main__.py +200 -0
  6. package/dist/backend.d.ts +67 -0
  7. package/dist/backend.d.ts.map +1 -0
  8. package/dist/backend.js +183 -0
  9. package/dist/backend.js.map +1 -0
  10. package/dist/backends/httpBackend.d.ts +31 -0
  11. package/dist/backends/httpBackend.d.ts.map +1 -0
  12. package/dist/backends/httpBackend.js +222 -0
  13. package/dist/backends/httpBackend.js.map +1 -0
  14. package/dist/backends/pythonBackend.d.ts +39 -0
  15. package/dist/backends/pythonBackend.d.ts.map +1 -0
  16. package/dist/backends/pythonBackend.js +228 -0
  17. package/dist/backends/pythonBackend.js.map +1 -0
  18. package/dist/cli.d.ts +10 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +200 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/concurrency.d.ts +21 -0
  23. package/dist/concurrency.d.ts.map +1 -0
  24. package/dist/concurrency.js +50 -0
  25. package/dist/concurrency.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/rateLimit.d.ts +24 -0
  31. package/dist/rateLimit.d.ts.map +1 -0
  32. package/dist/rateLimit.js +45 -0
  33. package/dist/rateLimit.js.map +1 -0
  34. package/dist/redact.d.ts +7 -0
  35. package/dist/redact.d.ts.map +1 -0
  36. package/dist/redact.js +27 -0
  37. package/dist/redact.js.map +1 -0
  38. package/dist/retention.d.ts +18 -0
  39. package/dist/retention.d.ts.map +1 -0
  40. package/dist/retention.js +63 -0
  41. package/dist/retention.js.map +1 -0
  42. package/dist/server.d.ts +21 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +152 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/timeout.d.ts +11 -0
  47. package/dist/timeout.d.ts.map +1 -0
  48. package/dist/timeout.js +29 -0
  49. package/dist/timeout.js.map +1 -0
  50. package/dist/tools/voiceDialogue.d.ts +39 -0
  51. package/dist/tools/voiceDialogue.d.ts.map +1 -0
  52. package/dist/tools/voiceDialogue.js +137 -0
  53. package/dist/tools/voiceDialogue.js.map +1 -0
  54. package/dist/tools/voiceInnerMonologue.d.ts +8 -0
  55. package/dist/tools/voiceInnerMonologue.d.ts.map +1 -0
  56. package/dist/tools/voiceInnerMonologue.js +6 -0
  57. package/dist/tools/voiceInnerMonologue.js.map +1 -0
  58. package/dist/tools/voiceInterrupt.d.ts +8 -0
  59. package/dist/tools/voiceInterrupt.d.ts.map +1 -0
  60. package/dist/tools/voiceInterrupt.js +10 -0
  61. package/dist/tools/voiceInterrupt.js.map +1 -0
  62. package/dist/tools/voiceSpeak.d.ts +17 -0
  63. package/dist/tools/voiceSpeak.d.ts.map +1 -0
  64. package/dist/tools/voiceSpeak.js +107 -0
  65. package/dist/tools/voiceSpeak.js.map +1 -0
  66. package/dist/tools/voiceStatus.d.ts +5 -0
  67. package/dist/tools/voiceStatus.d.ts.map +1 -0
  68. package/dist/tools/voiceStatus.js +18 -0
  69. package/dist/tools/voiceStatus.js.map +1 -0
  70. package/dist/validation.d.ts +15 -0
  71. package/dist/validation.d.ts.map +1 -0
  72. package/dist/validation.js +75 -0
  73. package/dist/validation.js.map +1 -0
  74. package/package.json +56 -7
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog: @mcptoolshop/voice-soundboard-mcp
2
+
3
+ The full history of the repository, including the Python package, is in the [repository CHANGELOG](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/blob/main/CHANGELOG.md).
4
+
5
+ ## [1.0.3] - 2026-10-15
6
+
7
+ The first npm release of both packages (`1.0.2` was never published to npm).
8
+
9
+ - `@mcptoolshop/voice-soundboard-core` and `@mcptoolshop/voice-soundboard-mcp` publish from CI through npm Trusted Publishing, with provenance.
10
+ - Each package has its own README. The core package's README described the server; both now match the code: 46 voices (not 48), the eight emotions and their `{emotion}` syntax, the six sound-effect tags, the presets, and how the server picks its speech backend.
11
+ - `voice-soundboard-mcp --help` lists the environment variables the server reads (`VOICE_SOUNDBOARD_TTS_URL`, not `_HTTP_URL`) and its real defaults (backend chosen automatically, 1 concurrent synthesis, a 20,000 ms timeout).
12
+ - `LICENSE` and `CHANGELOG.md` are in each package, as `files` already listed.
13
+ - Includes the 2.5.5 security fixes in `@mcptoolshop/voice-soundboard-core`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mcp-tool-shop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,263 @@
1
- # @mcptoolshop/voice-soundboard-mcp
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mcp-tool-shop-org/mcp-voice-soundboard/main/assets/logo-dark.jpg" alt="MCP Voice Soundboard" width="420" />
3
+ </p>
2
4
 
3
- Placeholder release. The real package is published from CI.
5
+ <h3 align="center">Text-to-speech for AI agents, as an MCP server.</h3>
4
6
 
5
- Source: <https://github.com/mcp-tool-shop-org/mcp-voice-soundboard>
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@mcptoolshop/voice-soundboard-mcp"><img src="https://img.shields.io/npm/v/@mcptoolshop/voice-soundboard-mcp?style=flat-square&color=cb3837&logo=npm" alt="npm"></a>
9
+ <a href="https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mcp-tool-shop-org/mcp-voice-soundboard/ci.yml?branch=main&style=flat-square&label=CI" alt="CI"></a>
10
+ <img src="https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js 20+">
11
+ <a href="https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ 46 voices &bull; 9 languages &bull; 5 presets &bull; 8 emotions &bull; SSML-lite &bull; sound effects &bull; multi-speaker dialogue
16
+ </p>
17
+
18
+ Give an agent a voice. Claude Desktop, Cursor or any MCP client can speak a line, perform a scripted dialogue between several speakers, and drop a chime where it belongs. Speech comes from **Kokoro-82M** (Apache-2.0) through **kokoro-onnx**, so it runs on your own machine with no account and no API key. Every request passes the same guardrails: rate limits, one synthesis at a time, timeouts, a sandboxed output folder and redaction of secrets.
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ npx -y @mcptoolshop/voice-soundboard-mcp
24
+ ```
25
+
26
+ Add it to your MCP client, for example `claude_desktop_config.json`:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "voice-soundboard": {
32
+ "command": "npx",
33
+ "args": ["-y", "@mcptoolshop/voice-soundboard-mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ The server speaks MCP over stdio. Set `PORT` to serve it over HTTP instead.
40
+
41
+ ## Where the speech comes from
42
+
43
+ The server picks a backend when it starts and logs which one:
44
+
45
+ 1. **HTTP:** when `VOICE_SOUNDBOARD_TTS_URL` is set, it sends synthesis to that endpoint (with `VOICE_SOUNDBOARD_TTS_TOKEN` as a bearer token if you set one).
46
+ 2. **Python bridge (Kokoro):** otherwise, when `python` is on your `PATH` (or `VOICE_SOUNDBOARD_PYTHON` names one), it starts the bundled bridge.
47
+ - On first use the bridge installs `kokoro-onnx` with `pip install --user` and downloads the model (`kokoro-v1.0.onnx`, about 310 MB, and `voices-v1.0.bin`, about 26 MB) into `~/.cache/voice-soundboard`.
48
+ - After that it runs offline.
49
+ 3. **Mock:** with neither, it answers with silent WAV files, so the tools still work end to end while you wire things up.
50
+
51
+ Choose one explicitly with `--backend=http|python|mock|none`.
52
+
53
+ ## Tools
54
+
55
+ ### `voice_speak`
56
+
57
+ Speak text with one voice.
58
+
59
+ ```
60
+ text: "Hello world!"
61
+ voice?: "am_fenrir" # a voice ID or a preset name
62
+ speed?: 1.0 # 0.5 to 2.0
63
+ format?: "wav" # wav | mp3 | ogg | raw
64
+ artifactMode?: "path" # path | base64
65
+ outputDir?: "clips" # a subfolder of the output root, in path mode
66
+ sfx?: true # turn on [ding], [chime] and the other sound-effect tags
67
+ ```
68
+
69
+ ### `voice_dialogue`
70
+
71
+ Perform a script with several speakers.
72
+
73
+ ```
74
+ script: "Alice: Hello!\n[pause 300ms]\nBob: Hey there!"
75
+ cast?: { "Alice": "af_sky", "Bob": "narrator" } # anyone not cast is assigned a voice
76
+ speed?: 1.0
77
+ concat?: true # one audio file for the whole scene
78
+ debug?: true # include the cue sheet and parse warnings
79
+ artifactMode?: "path"
80
+ outputDir?: "scenes"
81
+ ```
82
+
83
+ ### `voice_status`
84
+
85
+ The backend and its health, the voices, the presets and the default voice. No arguments.
86
+
87
+ ### `voice_interrupt`
88
+
89
+ Stop speech in progress.
90
+
91
+ ```
92
+ streamId?: "stream-123"
93
+ reason?: "user_spoke" # user_spoke | context_change | timeout | manual
94
+ ```
95
+
96
+ ### `voice_inner_monologue`
97
+
98
+ Short, throwaway utterances for ambient narration: up to 500 characters, rate-limited, with secrets redacted. Off unless the server runs with `--ambient` or `VOICE_SOUNDBOARD_AMBIENT_ENABLED=1`.
99
+
100
+ ```
101
+ text: "Interesting..."
102
+ category?: "thinking" # general | thinking | observation | debug
103
+ ```
104
+
105
+ ## Markup
106
+
107
+ | Markup | Syntax | Values |
108
+ |---|---|---|
109
+ | Emotion | `{joy}Great news!{/joy}` | `neutral`, `serious`, `friendly`, `professional`, `calm`, `joy`, `urgent`, `whisper` |
110
+ | Sound effect | `[ding]` (with `sfx: true`) | `ding`, `chime`, `whoosh`, `click`, `pop`, `tada` |
111
+ | SSML-lite | `<break time="300ms"/>` | `<speak>`, `<break>`, `<prosody>`, `<emphasis>` |
112
+ | Dialogue | `Alice: Hello!` | one line per cue, `[pause 350ms]` between cues |
113
+
114
+ Each emotion maps to a voice and speed. For example, `joy` uses `am_eric` at 1.03 and `urgent` uses `am_fenrir` at 1.08.
115
+
116
+ ## Voices
117
+
118
+ 46 voices in 9 languages. The language follows from the voice ID's prefix, so there's nothing to configure. The default voice is `bm_george`.
119
+
120
+ ### Presets
121
+
122
+ | Preset | Voice | Speed | Character |
123
+ |---|---|---|---|
124
+ | `assistant` | `af_jessica` | 1.0 | Friendly, helpful, conversational |
125
+ | `narrator` | `bm_george` | 0.95 | Calm, clear, documentary style |
126
+ | `announcer` | `am_eric` | 1.1 | Bold, energetic, broadcast style |
127
+ | `storyteller` | `bf_emma` | 0.9 | Expressive, varied pacing |
128
+ | `whisper` | `af_sky` | 0.85 | Soft, intimate, gentle |
129
+
130
+ #### English (American) (14)
131
+
132
+ | Voice | Name | Gender | Style |
133
+ |---|---|---|---|
134
+ | `af_aoede` | Aoede | female | musical |
135
+ | `af_bella` | Bella | female | warm |
136
+ | `af_heart` | Heart | female | caring |
137
+ | `af_jessica` | Jessica | female | professional |
138
+ | `af_kore` | Kore | female | youthful |
139
+ | `af_nicole` | Nicole | female | soft |
140
+ | `af_sarah` | Sarah | female | clear |
141
+ | `af_sky` | Sky | female | airy |
142
+ | `am_eric` | Eric | male | confident |
143
+ | `am_fenrir` | Fenrir | male | powerful |
144
+ | `am_liam` | Liam | male | friendly |
145
+ | `am_michael` | Michael | male | deep |
146
+ | `am_onyx` | Onyx | male | smooth |
147
+ | `am_puck` | Puck | male | playful |
148
+
149
+ #### English (British) (6)
150
+
151
+ | Voice | Name | Gender | Style |
152
+ |---|---|---|---|
153
+ | `bf_alice` | Alice | female | proper |
154
+ | `bf_emma` | Emma | female | refined |
155
+ | `bf_isabella` | Isabella | female | warm |
156
+ | `bm_fable` | Fable | male | storytelling |
157
+ | `bm_george` | George | male | authoritative |
158
+ | `bm_lewis` | Lewis | male | friendly |
159
+
160
+ #### Japanese (5)
161
+
162
+ | Voice | Name | Gender | Style |
163
+ |---|---|---|---|
164
+ | `jf_alpha` | Alpha | female | clear |
165
+ | `jf_gongitsune` | Gongitsune | female | storytelling |
166
+ | `jf_nezuko` | Nezuko | female | gentle |
167
+ | `jf_tebukuro` | Tebukuro | female | warm |
168
+ | `jm_kumo` | Kumo | male | calm |
169
+
170
+ #### Mandarin Chinese (8)
171
+
172
+ | Voice | Name | Gender | Style |
173
+ |---|---|---|---|
174
+ | `zf_xiaobei` | Xiaobei | female | bright |
175
+ | `zf_xiaoni` | Xiaoni | female | gentle |
176
+ | `zf_xiaoxiao` | Xiaoxiao | female | clear |
177
+ | `zf_xiaoyi` | Xiaoyi | female | warm |
178
+ | `zm_yunjian` | Yunjian | male | authoritative |
179
+ | `zm_yunxi` | Yunxi | male | friendly |
180
+ | `zm_yunxia` | Yunxia | male | calm |
181
+ | `zm_yunyang` | Yunyang | male | confident |
182
+
183
+ #### Spanish (3)
184
+
185
+ | Voice | Name | Gender | Style |
186
+ |---|---|---|---|
187
+ | `ef_dora` | Dora | female | warm |
188
+ | `em_alex` | Alex | male | confident |
189
+ | `em_santa` | Santa | male | jolly |
190
+
191
+ #### French (1)
192
+
193
+ | Voice | Name | Gender | Style |
194
+ |---|---|---|---|
195
+ | `ff_siwis` | Siwis | female | refined |
196
+
197
+ #### Hindi (4)
198
+
199
+ | Voice | Name | Gender | Style |
200
+ |---|---|---|---|
201
+ | `hf_alpha` | Alpha | female | clear |
202
+ | `hf_beta` | Beta | female | warm |
203
+ | `hm_omega` | Omega | male | deep |
204
+ | `hm_psi` | Psi | male | calm |
205
+
206
+ #### Italian (2)
207
+
208
+ | Voice | Name | Gender | Style |
209
+ |---|---|---|---|
210
+ | `if_sara` | Sara | female | warm |
211
+ | `im_nicola` | Nicola | male | confident |
212
+
213
+ #### Brazilian Portuguese (3)
214
+
215
+ | Voice | Name | Gender | Style |
216
+ |---|---|---|---|
217
+ | `pf_dora` | Dora | female | warm |
218
+ | `pm_alex` | Alex | male | confident |
219
+ | `pm_santa` | Santa | male | jolly |
220
+
221
+ ## Options
222
+
223
+ | Flag | Default | What it does |
224
+ |---|---|---|
225
+ | `--backend=<type>` | chosen automatically (see above) | `http`, `python`, `mock` or `none` |
226
+ | `--artifact=path\|base64` | `path` | Return audio as a file path or as base64 |
227
+ | `--output-dir=<path>` | `<tmpdir>/voice-soundboard` | Where audio files are written |
228
+ | `--max-concurrent=<n>` | `1` | Syntheses at the same time |
229
+ | `--timeout=<ms>` | `20000` | Timeout per request |
230
+ | `--retention-minutes=<n>` | `240` | Delete audio older than this (`0` keeps it) |
231
+ | `--ambient` | off | Enable `voice_inner_monologue` |
232
+ | `--version`, `--help` | | Print the version or the usage |
233
+
234
+ | Environment variable | What it does |
235
+ |---|---|
236
+ | `PORT` | Serve over HTTP on this port instead of stdio |
237
+ | `VOICE_SOUNDBOARD_TTS_URL` | HTTP backend endpoint (selects the HTTP backend) |
238
+ | `VOICE_SOUNDBOARD_TTS_TOKEN` | Bearer token for the HTTP backend |
239
+ | `VOICE_SOUNDBOARD_HTTP_TIMEOUT` | HTTP backend timeout in milliseconds |
240
+ | `VOICE_SOUNDBOARD_PYTHON` | Python command for the bridge |
241
+ | `VOICE_SOUNDBOARD_PYTHON_MODULE` | Bridge module to run |
242
+ | `VOICE_SOUNDBOARD_OUTPUT_DIR` | Output folder for the Python bridge |
243
+ | `VOICE_SOUNDBOARD_AMBIENT_ENABLED` | `1` enables `voice_inner_monologue` |
244
+
245
+ ## Limits and safety
246
+
247
+ - **Request size:** 12,000 characters of text, 50 chunks, 100 emotion spans, 400 SSML nodes, 30 sound effects and breaks of 2 seconds or less.
248
+ - **Output:** audio is written only inside the output root. Paths that climb out of it, and symlinks, are refused, and file names are generated by the server.
249
+ - **Errors:** each one comes back with a code, a hint and whether it's safe to retry, never a stack trace.
250
+ - **Privacy:** no telemetry, and nothing leaves your machine except calls to the HTTP backend you configure, and the bridge's one-time install and model download.
251
+
252
+ See the [threat model](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/blob/main/THREAT_MODEL.md) and the [security policy](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/blob/main/SECURITY.md).
253
+
254
+ ## Related
255
+
256
+ - [`@mcptoolshop/voice-soundboard-core`](https://www.npmjs.com/package/@mcptoolshop/voice-soundboard-core): the engine-agnostic library this server is built on, for your own integrations.
257
+ - [Source](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard), [discussions](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/discussions) and [issues](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/issues).
258
+
259
+ ## Credits and license
260
+
261
+ Speech is generated by [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) (Apache-2.0) through [kokoro-onnx](https://github.com/thewh1teagle/kokoro-onnx) (MIT).
262
+
263
+ This package is [MIT](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard/blob/main/LICENSE). Part of [MCP Tool Shop](https://mcptoolshop.com/).
File without changes
@@ -0,0 +1,200 @@
1
+ """
2
+ Voice Soundboard Python Bridge — NDJSON stdio protocol.
3
+
4
+ Auto-downloads Kokoro ONNX model + voices on first run.
5
+ Communicates with the Node.js MCP server via newline-delimited JSON on stdin/stdout.
6
+
7
+ Protocol:
8
+ IN: {"id": "...", "op": "health"}
9
+ OUT: {"id": "...", "ok": true, "model": "kokoro-v0.19"}
10
+
11
+ IN: {"id": "...", "op": "synthesize", "text": "Hello", "voice": "af_bella", "speed": 1.0,
12
+ "format": "wav", "artifact_mode": "base64"}
13
+ OUT: {"id": "...", "ok": true, "audio_bytes_base64": "...", "duration_ms": 1234,
14
+ "sample_rate": 24000, "format": "wav"}
15
+
16
+ IN: {"id": "...", "op": "interrupt"}
17
+ OUT: {"id": "...", "ok": true}
18
+ """
19
+
20
+ import json
21
+ import sys
22
+ import io
23
+ import base64
24
+ import struct
25
+ import os
26
+
27
+
28
+ GITHUB_RELEASE = "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0"
29
+ MODEL_FILE = "kokoro-v1.0.onnx"
30
+ VOICES_FILE = "voices-v1.0.bin"
31
+
32
+
33
+ def ensure_deps():
34
+ """Install kokoro-onnx if missing."""
35
+ try:
36
+ import kokoro_onnx # noqa: F401
37
+ except ImportError:
38
+ import subprocess
39
+ print("[bridge] Installing kokoro-onnx...", file=sys.stderr)
40
+ subprocess.check_call(
41
+ [sys.executable, "-m", "pip", "install", "--user", "-q", "kokoro-onnx"],
42
+ stdout=sys.stderr,
43
+ stderr=sys.stderr,
44
+ )
45
+
46
+
47
+ def download_file(url: str, dest: str):
48
+ """Download a file with progress."""
49
+ import urllib.request
50
+ filename = os.path.basename(dest)
51
+ print(f"[bridge] Downloading {filename}...", file=sys.stderr)
52
+ urllib.request.urlretrieve(url, dest)
53
+ size_mb = os.path.getsize(dest) / (1024 * 1024)
54
+ print(f"[bridge] Downloaded {filename} ({size_mb:.0f} MB)", file=sys.stderr)
55
+
56
+
57
+ def download_model():
58
+ """Download Kokoro ONNX model + voices from GitHub releases. Returns (model_path, voices_path)."""
59
+ cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "voice-soundboard")
60
+ os.makedirs(cache_dir, exist_ok=True)
61
+
62
+ model_path = os.path.join(cache_dir, MODEL_FILE)
63
+ voices_path = os.path.join(cache_dir, VOICES_FILE)
64
+
65
+ if not os.path.exists(model_path):
66
+ download_file(f"{GITHUB_RELEASE}/{MODEL_FILE}", model_path)
67
+
68
+ if not os.path.exists(voices_path):
69
+ download_file(f"{GITHUB_RELEASE}/{VOICES_FILE}", voices_path)
70
+
71
+ print(f"[bridge] Model ready: {model_path}", file=sys.stderr)
72
+ return model_path, voices_path
73
+
74
+
75
+ def pcm_to_wav(samples, sample_rate: int) -> bytes:
76
+ """Convert float32 numpy array to WAV bytes."""
77
+ import numpy as np
78
+
79
+ # Clamp and convert to 16-bit PCM
80
+ pcm = np.clip(samples, -1.0, 1.0)
81
+ pcm = (pcm * 32767).astype(np.int16)
82
+ raw = pcm.tobytes()
83
+
84
+ # Build WAV header
85
+ buf = io.BytesIO()
86
+ num_channels = 1
87
+ bits_per_sample = 16
88
+ byte_rate = sample_rate * num_channels * bits_per_sample // 8
89
+ block_align = num_channels * bits_per_sample // 8
90
+ data_size = len(raw)
91
+
92
+ buf.write(b"RIFF")
93
+ buf.write(struct.pack("<I", 36 + data_size))
94
+ buf.write(b"WAVE")
95
+ buf.write(b"fmt ")
96
+ buf.write(struct.pack("<I", 16))
97
+ buf.write(struct.pack("<H", 1)) # PCM
98
+ buf.write(struct.pack("<H", num_channels))
99
+ buf.write(struct.pack("<I", sample_rate))
100
+ buf.write(struct.pack("<I", byte_rate))
101
+ buf.write(struct.pack("<H", block_align))
102
+ buf.write(struct.pack("<H", bits_per_sample))
103
+ buf.write(b"data")
104
+ buf.write(struct.pack("<I", data_size))
105
+ buf.write(raw)
106
+
107
+ return buf.getvalue()
108
+
109
+
110
+ def main():
111
+ ensure_deps()
112
+
113
+ from kokoro_onnx import Kokoro
114
+
115
+ model_path, voices_path = download_model()
116
+ kokoro = Kokoro(model_path, voices_path)
117
+ available_voices = kokoro.get_voices()
118
+
119
+ print(f"[bridge] Ready — {len(available_voices)} voices available", file=sys.stderr)
120
+
121
+ for line in sys.stdin:
122
+ line = line.strip()
123
+ if not line:
124
+ continue
125
+
126
+ try:
127
+ msg = json.loads(line)
128
+ except json.JSONDecodeError:
129
+ continue
130
+
131
+ req_id = msg.get("id")
132
+ op = msg.get("op")
133
+
134
+ if op == "health":
135
+ resp = {"id": req_id, "ok": True, "model": "kokoro-v1.1-onnx", "voices": len(available_voices)}
136
+ sys.stdout.write(json.dumps(resp) + "\n")
137
+ sys.stdout.flush()
138
+
139
+ elif op == "synthesize":
140
+ try:
141
+ text = msg.get("text", "")
142
+ voice = msg.get("voice", "af_bella")
143
+ speed = float(msg.get("speed", 1.0))
144
+ artifact_mode = msg.get("artifact_mode", "base64")
145
+
146
+ # Validate voice
147
+ if voice not in available_voices:
148
+ # Try partial match
149
+ matches = [v for v in available_voices if v.startswith(voice)]
150
+ voice = matches[0] if matches else available_voices[0]
151
+
152
+ samples, sample_rate = kokoro.create(text, voice=voice, speed=speed)
153
+ wav_bytes = pcm_to_wav(samples, sample_rate)
154
+ duration_ms = int(len(samples) / sample_rate * 1000)
155
+
156
+ if artifact_mode == "base64":
157
+ resp = {
158
+ "id": req_id,
159
+ "ok": True,
160
+ "audio_bytes_base64": base64.b64encode(wav_bytes).decode(),
161
+ "duration_ms": duration_ms,
162
+ "sample_rate": sample_rate,
163
+ "format": "wav",
164
+ }
165
+ else:
166
+ output_dir = msg.get("output_dir", os.path.join(os.path.expanduser("~"), ".voice-soundboard"))
167
+ os.makedirs(output_dir, exist_ok=True)
168
+ import uuid
169
+ audio_path = os.path.join(output_dir, f"vsmcp_{uuid.uuid4().hex[:8]}.wav")
170
+ with open(audio_path, "wb") as f:
171
+ f.write(wav_bytes)
172
+ resp = {
173
+ "id": req_id,
174
+ "ok": True,
175
+ "audio_path": audio_path,
176
+ "duration_ms": duration_ms,
177
+ "sample_rate": sample_rate,
178
+ "format": "wav",
179
+ }
180
+
181
+ sys.stdout.write(json.dumps(resp) + "\n")
182
+ sys.stdout.flush()
183
+
184
+ except Exception as e:
185
+ resp = {
186
+ "id": req_id,
187
+ "ok": False,
188
+ "error": {"code": "SYNTHESIS_FAILED", "message": str(e)},
189
+ }
190
+ sys.stdout.write(json.dumps(resp) + "\n")
191
+ sys.stdout.flush()
192
+
193
+ elif op == "interrupt":
194
+ resp = {"id": req_id, "ok": True}
195
+ sys.stdout.write(json.dumps(resp) + "\n")
196
+ sys.stdout.flush()
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
@@ -0,0 +1,67 @@
1
+ /** Backend interface, types, and mock implementation. */
2
+ import type { SynthesisRequest } from "@mcptoolshop/voice-soundboard-core";
3
+ export type BackendType = "mock" | "python" | "http" | "none";
4
+ export interface SynthesisResult {
5
+ readonly audioPath?: string;
6
+ readonly audioBytesBase64?: string;
7
+ readonly durationMs: number;
8
+ readonly sampleRate: number;
9
+ readonly format: string;
10
+ }
11
+ export interface BackendHealth {
12
+ readonly ready: boolean;
13
+ readonly details?: string;
14
+ }
15
+ export interface Backend {
16
+ readonly type: BackendType;
17
+ readonly ready: boolean;
18
+ readonly model?: string;
19
+ readonly sampleRate?: number;
20
+ health(): Promise<BackendHealth>;
21
+ synthesize(request: SynthesisRequest): Promise<SynthesisResult>;
22
+ interrupt?(): Promise<void>;
23
+ }
24
+ export interface BackendConfig {
25
+ /** Explicit backend choice (--backend flag). */
26
+ backend?: BackendType;
27
+ /** HTTP backend URL (env: VOICE_SOUNDBOARD_TTS_URL). */
28
+ ttsUrl?: string;
29
+ /** HTTP backend auth token (env: VOICE_SOUNDBOARD_TTS_TOKEN). */
30
+ ttsToken?: string;
31
+ /** HTTP backend timeout in ms. */
32
+ httpTimeout?: number;
33
+ /** Python command (env: VOICE_SOUNDBOARD_PYTHON). */
34
+ pythonCommand?: string;
35
+ /** Python bridge module (env: VOICE_SOUNDBOARD_PYTHON_MODULE). */
36
+ pythonModule?: string;
37
+ }
38
+ /**
39
+ * Read backend config from CLI args and environment.
40
+ * CLI flags take precedence over env vars.
41
+ */
42
+ export declare function readBackendConfig(argv: string[]): BackendConfig;
43
+ /**
44
+ * Select and instantiate the appropriate backend.
45
+ *
46
+ * Priority:
47
+ * 1. Explicit --backend flag
48
+ * 2. VOICE_SOUNDBOARD_TTS_URL set → http
49
+ * 3. Python available → python
50
+ * 4. Fallback → none (or mock for dev)
51
+ */
52
+ export declare function selectBackend(config: BackendConfig): Promise<Backend>;
53
+ export declare class NoneBackend implements Backend {
54
+ readonly type: "none";
55
+ readonly ready = false;
56
+ health(): Promise<BackendHealth>;
57
+ synthesize(): Promise<SynthesisResult>;
58
+ }
59
+ export declare class MockBackend implements Backend {
60
+ readonly type: "mock";
61
+ readonly ready = true;
62
+ readonly model = "mock-silence";
63
+ readonly sampleRate = 24000;
64
+ health(): Promise<BackendHealth>;
65
+ synthesize(request: SynthesisRequest): Promise<SynthesisResult>;
66
+ }
67
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAOzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACjC,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAChE,SAAS,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAID,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAqB/D;AAWD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAoD3E;AAID,qBAAa,WAAY,YAAW,OAAO;IACzC,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;IAChC,QAAQ,CAAC,KAAK,SAAS;IAEjB,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IAIhC,UAAU,IAAI,OAAO,CAAC,eAAe,CAAC;CAG7C;AAyCD,qBAAa,WAAY,YAAW,OAAO;IACzC,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;IAChC,QAAQ,CAAC,KAAK,QAAQ;IACtB,QAAQ,CAAC,KAAK,kBAAkB;IAChC,QAAQ,CAAC,UAAU,SAAS;IAEtB,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IAIhC,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;CAwBtE"}