@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +261 -3
- package/backend-python/soundboard_bridge/__init__.py +0 -0
- package/backend-python/soundboard_bridge/__main__.py +200 -0
- package/dist/backend.d.ts +67 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +183 -0
- package/dist/backend.js.map +1 -0
- package/dist/backends/httpBackend.d.ts +31 -0
- package/dist/backends/httpBackend.d.ts.map +1 -0
- package/dist/backends/httpBackend.js +222 -0
- package/dist/backends/httpBackend.js.map +1 -0
- package/dist/backends/pythonBackend.d.ts +39 -0
- package/dist/backends/pythonBackend.d.ts.map +1 -0
- package/dist/backends/pythonBackend.js +228 -0
- package/dist/backends/pythonBackend.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +200 -0
- package/dist/cli.js.map +1 -0
- package/dist/concurrency.d.ts +21 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +50 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/rateLimit.d.ts +24 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +45 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/redact.d.ts +7 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +27 -0
- package/dist/redact.js.map +1 -0
- package/dist/retention.d.ts +18 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +63 -0
- package/dist/retention.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/timeout.d.ts +11 -0
- package/dist/timeout.d.ts.map +1 -0
- package/dist/timeout.js +29 -0
- package/dist/timeout.js.map +1 -0
- package/dist/tools/voiceDialogue.d.ts +39 -0
- package/dist/tools/voiceDialogue.d.ts.map +1 -0
- package/dist/tools/voiceDialogue.js +137 -0
- package/dist/tools/voiceDialogue.js.map +1 -0
- package/dist/tools/voiceInnerMonologue.d.ts +8 -0
- package/dist/tools/voiceInnerMonologue.d.ts.map +1 -0
- package/dist/tools/voiceInnerMonologue.js +6 -0
- package/dist/tools/voiceInnerMonologue.js.map +1 -0
- package/dist/tools/voiceInterrupt.d.ts +8 -0
- package/dist/tools/voiceInterrupt.d.ts.map +1 -0
- package/dist/tools/voiceInterrupt.js +10 -0
- package/dist/tools/voiceInterrupt.js.map +1 -0
- package/dist/tools/voiceSpeak.d.ts +17 -0
- package/dist/tools/voiceSpeak.d.ts.map +1 -0
- package/dist/tools/voiceSpeak.js +107 -0
- package/dist/tools/voiceSpeak.js.map +1 -0
- package/dist/tools/voiceStatus.d.ts +5 -0
- package/dist/tools/voiceStatus.d.ts.map +1 -0
- package/dist/tools/voiceStatus.js +18 -0
- package/dist/tools/voiceStatus.js.map +1 -0
- package/dist/validation.d.ts +15 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +75 -0
- package/dist/validation.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
5
|
+
<h3 align="center">Text-to-speech for AI agents, as an MCP server.</h3>
|
|
4
6
|
|
|
5
|
-
|
|
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 • 9 languages • 5 presets • 8 emotions • SSML-lite • sound effects • 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"}
|