agentvibes 5.12.0 → 5.13.1
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/.agentvibes/install-manifest.json +111 -91
- package/.claude/commands/agent-vibes/commands.json +0 -20
- package/.claude/commands/agent-vibes/unmute.md +6 -2
- package/.claude/config/audio-effects.cfg +6 -6
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/agentvibes-session-id.sh +69 -0
- package/.claude/hooks/bmad-party-speak.sh +20 -4
- package/.claude/hooks/bmad-speak.sh +60 -2
- package/.claude/hooks/bmad-tts-injector.sh +20 -1
- package/.claude/hooks/bmad-voice-manager.sh +25 -3
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
- package/.claude/hooks/clawdbot-receiver.sh +19 -1
- package/.claude/hooks/elevenlabs-voices.sh +62 -0
- package/.claude/hooks/kokoro-installer.sh +20 -10
- package/.claude/hooks/language-manager.sh +10 -3
- package/.claude/hooks/party-set-room.sh +71 -0
- package/.claude/hooks/party-stage-roster.py +328 -0
- package/.claude/hooks/personality-manager.sh +19 -2
- package/.claude/hooks/piper-voice-manager.sh +3 -2
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
- package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
- package/.claude/hooks/play-tts-kokoro.sh +33 -6
- package/.claude/hooks/play-tts-soprano.sh +3 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +37 -29
- package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
- package/.claude/hooks/play-tts.sh +66 -61
- package/.claude/hooks/provider-catalog.json +352 -0
- package/.claude/hooks/provider-catalog.sh +161 -0
- package/.claude/hooks/provider-commands.sh +2 -1
- package/.claude/hooks/provider-manager.sh +47 -9
- package/.claude/hooks/python-resolver.sh +117 -0
- package/.claude/hooks/session-id.sh +56 -0
- package/.claude/hooks/session-start-tts.sh +39 -0
- package/.claude/hooks/speed-manager.sh +1 -1
- package/.claude/hooks/translate-manager.sh +3 -2
- package/.claude/hooks/translator.py +1 -1
- package/.claude/hooks/voice-manager.sh +242 -10
- package/.claude/hooks-windows/language-manager.ps1 +7 -1
- package/.claude/hooks-windows/personality-manager.ps1 +16 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
- package/.claude/hooks-windows/play-tts.ps1 +32 -3
- package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
- package/.claude/hooks-windows/provider-manager.ps1 +63 -8
- package/.claude/hooks-windows/tts-watcher.ps1 +33 -12
- package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
- package/.mcp.json +0 -7
- package/README.md +12 -3
- package/RELEASE_NOTES.md +61 -0
- package/mcp-server/server.py +146 -49
- package/mcp-server/test_mcp_correctness.py +20 -2
- package/mcp-server/test_windows_script_parity.py +0 -2
- package/package.json +2 -2
- package/src/cli/list-voices.js +218 -114
- package/src/console/bling.js +71 -0
- package/src/console/music-preview.js +79 -0
- package/src/console/tabs/music-tab.js +16 -39
- package/src/console/tabs/settings-tab.js +195 -13
- package/src/console/tabs/setup-tab.js +9 -34
- package/src/console/tabs/voices-tab.js +83 -14
- package/src/console/widgets/track-picker.js +82 -0
- package/src/installer.js +136 -18
- package/src/services/provider-catalog.js +412 -0
- package/src/services/provider-voice-catalog.js +52 -73
- package/src/services/tts-engine-service.js +29 -0
- package/src/utils/provider-validator.js +62 -12
- package/.claude/commands/agent-vibes/language.md +0 -23
- package/.claude/commands/agent-vibes/learn.md +0 -67
- package/.claude/commands/agent-vibes/replay-target.md +0 -14
- package/.claude/commands/agent-vibes/target-voice.md +0 -26
- package/.claude/commands/agent-vibes/target.md +0 -30
- package/.claude/hooks/learn-manager.sh +0 -492
- package/.claude/hooks/replay-target-audio.sh +0 -95
- package/.claude/hooks-windows/learn-manager.ps1 +0 -241
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Catalog — Layer 2 of the SSOT program (companion to the Utterance
|
|
3
|
+
* Resolver, src/services/utterance-resolver.js). See
|
|
4
|
+
* docs/architecture/provider-catalog-design.md.
|
|
5
|
+
*
|
|
6
|
+
* ONE record per provider answers the inventory question — "WHAT exists to speak
|
|
7
|
+
* with?": voice lists, defaults, membership validation, per-platform runtime,
|
|
8
|
+
* and display names. The resolver answers the orthogonal question ("HOW is this
|
|
9
|
+
* utterance spoken?"). The two layers NEVER import each other — the seam is
|
|
10
|
+
* welded by conformance assertions only (see
|
|
11
|
+
* test/unit/provider-catalog-conformance.test.js). That keeps the resolver's
|
|
12
|
+
* 433-test contract untouched.
|
|
13
|
+
*
|
|
14
|
+
* DISCIPLINE: this module is PURE data + PURE functions. No `fs`, no `env`, no
|
|
15
|
+
* clock — exactly like the resolver. Discovered providers (piper, macos, SAPI)
|
|
16
|
+
* receive their installed-voice list via injection (`{ installed }`); the
|
|
17
|
+
* catalog owns the RULE, never the I/O.
|
|
18
|
+
*
|
|
19
|
+
* SECURITY NOTE: the receiver `ALLOWED_VOICES` allowlists
|
|
20
|
+
* (clawdbot-receiver-SECURE.sh) are a SECURITY control, NOT inventory — they are
|
|
21
|
+
* deliberately narrower than this catalog and must never be unified with it.
|
|
22
|
+
*
|
|
23
|
+
* @module services/provider-catalog
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
/* ------------------------------------------------------------------ *
|
|
29
|
+
* Static voice data (the canonical copies; provider-voice-catalog.js
|
|
30
|
+
* re-exports these for back-compat).
|
|
31
|
+
* ------------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
/** ElevenLabs DEFAULT-LIBRARY voices (voice_id + display metadata), available to
|
|
34
|
+
* every API key. Community/library voices that must be added to an account first
|
|
35
|
+
* are intentionally excluded so a switch can't fail at synth time. Mirrored on
|
|
36
|
+
* the shell side by .claude/hooks/elevenlabs-voices.sh (parity-guarded). */
|
|
37
|
+
const ELEVENLABS_VOICES = [
|
|
38
|
+
{ id: 'EXAVITQu4vr4xnSDxMaL', name: 'Sarah', gender: 'Female', lang: 'en-US', desc: 'Mature, reassuring' },
|
|
39
|
+
{ id: 'CwhRBWXzGAHq8TQ4Fs17', name: 'Roger', gender: 'Male', lang: 'en-US', desc: 'Laid-back, casual' },
|
|
40
|
+
{ id: 'FGY2WhTYpPnrIDTdsKH5', name: 'Laura', gender: 'Female', lang: 'en-US', desc: 'Enthusiast, quirky' },
|
|
41
|
+
{ id: 'IKne3meq5aSn9XLyUdCD', name: 'Charlie', gender: 'Male', lang: 'en-AU', desc: 'Deep, confident' },
|
|
42
|
+
{ id: 'JBFqnCBsd6RMkjVDRZzb', name: 'George', gender: 'Male', lang: 'en-GB', desc: 'Warm storyteller' },
|
|
43
|
+
{ id: 'N2lVS1w4EtoT3dr4eOWO', name: 'Callum', gender: 'Male', lang: 'en-US', desc: 'Husky trickster' },
|
|
44
|
+
{ id: 'SAz9YHcvj6GT2YYXdXww', name: 'River', gender: '', lang: 'en-US', desc: 'Relaxed, neutral' },
|
|
45
|
+
{ id: 'SOYHLrjzK2X1ezoPC6cr', name: 'Harry', gender: 'Male', lang: 'en-US', desc: 'Fierce warrior' },
|
|
46
|
+
{ id: 'TX3LPaxmHKxFdv7VOQHJ', name: 'Liam', gender: 'Male', lang: 'en-US', desc: 'Energetic creator' },
|
|
47
|
+
{ id: 'Xb7hH8MSUJpSbSDYk0k2', name: 'Alice', gender: 'Female', lang: 'en-GB', desc: 'Clear educator' },
|
|
48
|
+
{ id: 'XrExE9yKIg1WjnnlVkGX', name: 'Matilda', gender: 'Female', lang: 'en-US', desc: 'Knowledgable, pro' },
|
|
49
|
+
{ id: 'bIHbv24MWmeRgasZH58o', name: 'Will', gender: 'Male', lang: 'en-US', desc: 'Relaxed optimist' },
|
|
50
|
+
{ id: 'cgSgspJ2msm6clMCkdW9', name: 'Jessica', gender: 'Female', lang: 'en-US', desc: 'Playful, bright' },
|
|
51
|
+
{ id: 'cjVigY5qzO86Huf0OWal', name: 'Eric', gender: 'Male', lang: 'en-US', desc: 'Smooth, trustworthy' },
|
|
52
|
+
{ id: 'hpp4J3VqNfWAUOO0d1Us', name: 'Bella', gender: 'Female', lang: 'en-US', desc: 'Professional, warm' },
|
|
53
|
+
{ id: 'iP95p4xoKVk53GoZ742B', name: 'Chris', gender: 'Male', lang: 'en-US', desc: 'Charming, down-to-earth' },
|
|
54
|
+
{ id: 'nPczCjzI2devNBz1zQrb', name: 'Brian', gender: 'Male', lang: 'en-US', desc: 'Deep, comforting' },
|
|
55
|
+
{ id: 'onwK4e9ZLuTAKqWW03F9', name: 'Daniel', gender: 'Male', lang: 'en-GB', desc: 'Steady broadcaster' },
|
|
56
|
+
{ id: 'pFZP5JQG7iQjIQuC4Bku', name: 'Lily', gender: 'Female', lang: 'en-GB', desc: 'Velvety actress' },
|
|
57
|
+
{ id: 'pNInz6obpgDQGcFmaJgB', name: 'Adam', gender: 'Male', lang: 'en-US', desc: 'Dominant, firm' },
|
|
58
|
+
{ id: 'pqHfZKP75CvOlQylNhV4', name: 'Bill', gender: 'Male', lang: 'en-US', desc: 'Wise, mature' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** Kokoro voice ids. Gender is encoded in the id: the 2nd char is f|m. */
|
|
62
|
+
const KOKORO_VOICE_IDS = [
|
|
63
|
+
// American English
|
|
64
|
+
'af_heart','af_alloy','af_aoede','af_bella','af_jessica','af_kore','af_nicole','af_nova','af_river','af_sarah','af_sky',
|
|
65
|
+
'am_adam','am_echo','am_eric','am_fenrir','am_liam','am_michael','am_onyx','am_puck',
|
|
66
|
+
// British English
|
|
67
|
+
'bf_alice','bf_emma','bf_isabella','bf_lily',
|
|
68
|
+
'bm_daniel','bm_fable','bm_george','bm_lewis',
|
|
69
|
+
// Japanese
|
|
70
|
+
'jf_alpha','jf_gongitsune','jf_nezumi','jf_tebukuro','jm_kumo',
|
|
71
|
+
// Mandarin Chinese
|
|
72
|
+
'zf_xiaobei','zf_xiaoni','zf_xiaoxiao','zf_xiaoyi','zm_yunxi','zm_yunxia','zm_yunyang',
|
|
73
|
+
// Spanish
|
|
74
|
+
'ef_dora','em_alex','em_santa',
|
|
75
|
+
// French
|
|
76
|
+
'ff_siwis',
|
|
77
|
+
// Hindi
|
|
78
|
+
'hf_alpha','hm_omega',
|
|
79
|
+
// Italian
|
|
80
|
+
'if_sara','im_nicola',
|
|
81
|
+
// Brazilian Portuguese
|
|
82
|
+
'pf_dora','pm_alex','pm_santa',
|
|
83
|
+
// Korean
|
|
84
|
+
'kf_alpha','km_hyunsu',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Infer a Kokoro voice's gender from its id. Kokoro ids follow `<lang><sex>_name`
|
|
89
|
+
* where the 2nd character is `f` (female) or `m` (male).
|
|
90
|
+
* @param {string} id
|
|
91
|
+
* @returns {'Female'|'Male'|''}
|
|
92
|
+
*/
|
|
93
|
+
function kokoroGender(id) {
|
|
94
|
+
const c = typeof id === 'string' && id.length > 1 ? id[1].toLowerCase() : '';
|
|
95
|
+
if (c === 'f') return 'Female';
|
|
96
|
+
if (c === 'm') return 'Male';
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ------------------------------------------------------------------ *
|
|
101
|
+
* Voice-model validation rules (pure). Each returns { ok, canonical, reason }.
|
|
102
|
+
* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
/** Piper-family voice shape: `en_US-lessac-medium` or a `model::Speaker` form. */
|
|
105
|
+
const PIPER_VOICE_SHAPE = /^[a-z]{2,3}_[A-Z]{2}-/;
|
|
106
|
+
/** Raw ElevenLabs voice_id shape (preserves elevenlabs-voices.sh:66 semantics). */
|
|
107
|
+
const ELEVENLABS_ID_SHAPE = /^[A-Za-z0-9]{20}$/;
|
|
108
|
+
|
|
109
|
+
/* ------------------------------------------------------------------ *
|
|
110
|
+
* The seven canonical ProviderRecords.
|
|
111
|
+
*
|
|
112
|
+
* `aliases` are spellings that resolve to THIS record via getProvider()
|
|
113
|
+
* (e.g. windows-sapi ← 'sapi'). `engineId` is the resolver ENGINES value
|
|
114
|
+
* ('sapi' for windows-sapi, 'piper' for windows-piper); the derived
|
|
115
|
+
* engineAliasTable() mirrors the resolver's ENGINE_ALIASES.
|
|
116
|
+
* ------------------------------------------------------------------ */
|
|
117
|
+
|
|
118
|
+
const RECORDS = [
|
|
119
|
+
{
|
|
120
|
+
id: 'soprano',
|
|
121
|
+
engineId: 'soprano',
|
|
122
|
+
aliases: [],
|
|
123
|
+
displayName: 'Soprano TTS',
|
|
124
|
+
voiceModel: 'single',
|
|
125
|
+
voices: ['soprano-default'],
|
|
126
|
+
defaultVoice: 'soprano-default',
|
|
127
|
+
runtime: { unix: 'play-tts-soprano.sh', windows: 'play-tts-soprano.ps1', darwinOnly: false },
|
|
128
|
+
requires: 'pip',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'piper',
|
|
132
|
+
engineId: 'piper',
|
|
133
|
+
aliases: [],
|
|
134
|
+
displayName: 'Piper TTS',
|
|
135
|
+
voiceModel: 'discovered',
|
|
136
|
+
voices: null, // discovered on disk (*.onnx glob)
|
|
137
|
+
defaultVoice: 'en_US-lessac-medium', // preferred fallback, NOT a guaranteed install
|
|
138
|
+
runtime: { unix: 'play-tts-piper.sh', windows: null, darwinOnly: false }, // Windows uses windows-piper
|
|
139
|
+
requires: 'pip',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'kokoro',
|
|
143
|
+
engineId: 'kokoro',
|
|
144
|
+
aliases: [],
|
|
145
|
+
displayName: 'Kokoro TTS',
|
|
146
|
+
voiceModel: 'static',
|
|
147
|
+
voices: KOKORO_VOICE_IDS,
|
|
148
|
+
defaultVoice: 'af_heart',
|
|
149
|
+
runtime: { unix: 'play-tts-kokoro.sh', windows: 'play-tts-kokoro.ps1', darwinOnly: false },
|
|
150
|
+
requires: 'pip',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'elevenlabs',
|
|
154
|
+
engineId: 'elevenlabs',
|
|
155
|
+
aliases: [],
|
|
156
|
+
displayName: 'ElevenLabs',
|
|
157
|
+
voiceModel: 'name-to-id',
|
|
158
|
+
voices: ELEVENLABS_VOICES,
|
|
159
|
+
defaultVoice: 'Sarah',
|
|
160
|
+
runtime: { unix: 'play-tts-elevenlabs.sh', windows: null, darwinOnly: false }, // ← no ps1 runtime; the contradiction, now data
|
|
161
|
+
requires: 'api-key',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'macos',
|
|
165
|
+
engineId: 'macos',
|
|
166
|
+
aliases: ['macos-say', 'say'],
|
|
167
|
+
displayName: 'macOS Say',
|
|
168
|
+
voiceModel: 'discovered',
|
|
169
|
+
voices: null, // `say -v ?`
|
|
170
|
+
defaultVoice: 'Samantha',
|
|
171
|
+
runtime: { unix: 'play-tts-macos.sh', windows: null, darwinOnly: true },
|
|
172
|
+
requires: 'builtin',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'windows-sapi',
|
|
176
|
+
engineId: 'sapi',
|
|
177
|
+
aliases: ['sapi'],
|
|
178
|
+
displayName: 'Windows SAPI',
|
|
179
|
+
voiceModel: 'discovered',
|
|
180
|
+
voices: null, // System.Speech
|
|
181
|
+
defaultVoice: '', // system default
|
|
182
|
+
runtime: { unix: null, windows: 'play-tts-sapi.ps1', darwinOnly: false },
|
|
183
|
+
requires: 'builtin',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'windows-piper',
|
|
187
|
+
engineId: 'piper',
|
|
188
|
+
aliases: ['piper'], // on Windows, bare 'piper' forwards to windows-piper
|
|
189
|
+
displayName: 'Piper TTS',
|
|
190
|
+
voiceModel: 'discovered',
|
|
191
|
+
voices: null, // same *.onnx models as piper
|
|
192
|
+
defaultVoice: 'en_US-lessac-medium',
|
|
193
|
+
runtime: { unix: null, windows: 'play-tts-windows-piper.ps1', darwinOnly: false },
|
|
194
|
+
requires: 'exe',
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
// Lookup indexes (built once).
|
|
199
|
+
const _BY_ID = new Map();
|
|
200
|
+
const _BY_ALIAS = new Map();
|
|
201
|
+
for (const r of RECORDS) {
|
|
202
|
+
_BY_ID.set(r.id, r);
|
|
203
|
+
}
|
|
204
|
+
for (const r of RECORDS) {
|
|
205
|
+
for (const a of r.aliases) {
|
|
206
|
+
// Exact-id spellings always win, so never let an alias shadow a real id.
|
|
207
|
+
if (!_BY_ID.has(a) && !_BY_ALIAS.has(a)) _BY_ALIAS.set(a, r);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Normalize an id-or-alias to a lowercase key. */
|
|
212
|
+
function _norm(idOrAlias) {
|
|
213
|
+
return typeof idOrAlias === 'string' ? idOrAlias.trim().toLowerCase() : '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ------------------------------------------------------------------ *
|
|
217
|
+
* Public API (design §3.2). All entries are alias-normalizing.
|
|
218
|
+
* ------------------------------------------------------------------ */
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} idOrAlias
|
|
222
|
+
* @returns {object|null} the ProviderRecord, or null if unknown.
|
|
223
|
+
*/
|
|
224
|
+
function getProvider(idOrAlias) {
|
|
225
|
+
const key = _norm(idOrAlias);
|
|
226
|
+
if (!key) return null;
|
|
227
|
+
return _BY_ID.get(key) || _BY_ALIAS.get(key) || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @returns {object[]} all seven records (fresh array; records are shared). */
|
|
231
|
+
function listProviders() {
|
|
232
|
+
return RECORDS.slice();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Is a provider playable on `platform`?
|
|
237
|
+
* - 'windows' → has a Windows runtime script.
|
|
238
|
+
* - 'darwin' → has a Unix runtime script (darwin runs the .sh players).
|
|
239
|
+
* - 'unix' → has a Unix runtime AND is not darwin-only (i.e. Linux).
|
|
240
|
+
* @param {string} idOrAlias
|
|
241
|
+
* @param {'unix'|'windows'|'darwin'} platform
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
function isAvailable(idOrAlias, platform) {
|
|
245
|
+
const r = getProvider(idOrAlias);
|
|
246
|
+
if (!r) return false;
|
|
247
|
+
if (platform === 'windows') return r.runtime.windows !== null;
|
|
248
|
+
if (platform === 'darwin') return r.runtime.unix !== null;
|
|
249
|
+
return r.runtime.unix !== null && !r.runtime.darwinOnly; // 'unix' == generic Linux
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {'unix'|'windows'|'darwin'} platform
|
|
254
|
+
* @returns {object[]} records available on that platform.
|
|
255
|
+
*/
|
|
256
|
+
function providersFor(platform) {
|
|
257
|
+
return RECORDS.filter((r) => isAvailable(r.id, platform));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* List a provider's voices as `{ id, name?, gender, lang? }` entries. Discovered
|
|
262
|
+
* providers require an injected `installed` list (else an empty array).
|
|
263
|
+
* @param {string} idOrAlias
|
|
264
|
+
* @param {{installed?: string[]}} [opts]
|
|
265
|
+
* @returns {Array<{id: string, name?: string, gender: string, lang?: string}>}
|
|
266
|
+
*/
|
|
267
|
+
function listVoices(idOrAlias, { installed } = {}) {
|
|
268
|
+
const r = getProvider(idOrAlias);
|
|
269
|
+
if (!r) return [];
|
|
270
|
+
switch (r.voiceModel) {
|
|
271
|
+
case 'static': // kokoro
|
|
272
|
+
return r.voices.map((id) => ({ id, gender: kokoroGender(id) }));
|
|
273
|
+
case 'name-to-id': // elevenlabs
|
|
274
|
+
return r.voices.map((v) => ({ id: v.id, name: v.name, gender: v.gender || '', lang: v.lang }));
|
|
275
|
+
case 'single': // soprano
|
|
276
|
+
return r.voices.map((id) => ({ id, gender: '' }));
|
|
277
|
+
case 'discovered': // piper / macos / windows-sapi / windows-piper
|
|
278
|
+
default:
|
|
279
|
+
if (Array.isArray(installed)) return installed.map((id) => ({ id, gender: '' }));
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {string} idOrAlias
|
|
286
|
+
* @returns {string} the provider's default voice ('' for windows-sapi = system default).
|
|
287
|
+
*/
|
|
288
|
+
function defaultVoice(idOrAlias) {
|
|
289
|
+
const r = getProvider(idOrAlias);
|
|
290
|
+
return r ? r.defaultVoice : '';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validate a voice for a provider. STRICT membership at config-write time
|
|
295
|
+
* (typos never reach voice.txt). Returns `{ ok, canonical, reason }` — never a
|
|
296
|
+
* bare boolean — so every rejection carries a message and the accepted spelling
|
|
297
|
+
* is normalized ONCE.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} idOrAlias
|
|
300
|
+
* @param {string} voice
|
|
301
|
+
* @param {{installed?: string[], allowUnlisted?: boolean}} [opts]
|
|
302
|
+
* `allowUnlisted` expresses the AGENTVIBES_ALLOW_UNLISTED_VOICE escape hatch —
|
|
303
|
+
* the catalog never reads env; consumers pass the flag (design §3.3). It relaxes
|
|
304
|
+
* MEMBERSHIP, not shape.
|
|
305
|
+
* @returns {{ok: boolean, canonical: string|null, reason: string}}
|
|
306
|
+
*/
|
|
307
|
+
function validateVoice(idOrAlias, voice, { installed, allowUnlisted = false } = {}) {
|
|
308
|
+
const r = getProvider(idOrAlias);
|
|
309
|
+
if (!r) return { ok: false, canonical: null, reason: `Unknown provider: ${idOrAlias}` };
|
|
310
|
+
const raw = typeof voice === 'string' ? voice.trim() : '';
|
|
311
|
+
|
|
312
|
+
switch (r.voiceModel) {
|
|
313
|
+
case 'static': { // kokoro — case-fold, exact membership
|
|
314
|
+
const folded = raw.toLowerCase();
|
|
315
|
+
if (r.voices.includes(folded)) return { ok: true, canonical: folded, reason: '' };
|
|
316
|
+
if (allowUnlisted && folded) return { ok: true, canonical: folded, reason: '' };
|
|
317
|
+
return { ok: false, canonical: null, reason: `"${voice}" is not a known ${r.displayName} voice` };
|
|
318
|
+
}
|
|
319
|
+
case 'name-to-id': { // elevenlabs — case-insensitive name → id, OR raw 20-char id
|
|
320
|
+
const lc = raw.toLowerCase();
|
|
321
|
+
const byName = r.voices.find((v) => v.name.toLowerCase() === lc);
|
|
322
|
+
if (byName) return { ok: true, canonical: byName.id, reason: '' };
|
|
323
|
+
if (ELEVENLABS_ID_SHAPE.test(raw)) return { ok: true, canonical: raw, reason: '' };
|
|
324
|
+
return { ok: false, canonical: null, reason: `"${voice}" is not a known ElevenLabs voice name or id` };
|
|
325
|
+
}
|
|
326
|
+
case 'single': { // soprano — '', 'soprano', 'soprano-default' → canonical 'soprano-default'
|
|
327
|
+
const lc = raw.toLowerCase();
|
|
328
|
+
if (lc === '' || lc === r.id || lc === r.defaultVoice) {
|
|
329
|
+
return { ok: true, canonical: r.defaultVoice, reason: '' };
|
|
330
|
+
}
|
|
331
|
+
return { ok: false, canonical: null, reason: `${r.displayName} has a single voice (${r.defaultVoice})` };
|
|
332
|
+
}
|
|
333
|
+
case 'discovered': // piper / macos / windows-sapi / windows-piper
|
|
334
|
+
default: {
|
|
335
|
+
const isPiperFamily = r.engineId === 'piper';
|
|
336
|
+
if (Array.isArray(installed)) {
|
|
337
|
+
if (installed.includes(raw)) return { ok: true, canonical: raw, reason: '' };
|
|
338
|
+
if (!allowUnlisted) {
|
|
339
|
+
return { ok: false, canonical: null, reason: `"${voice}" is not an installed ${r.displayName} voice` };
|
|
340
|
+
}
|
|
341
|
+
// allowUnlisted: fall through to shape/permissive check
|
|
342
|
+
}
|
|
343
|
+
if (isPiperFamily) {
|
|
344
|
+
if (PIPER_VOICE_SHAPE.test(raw) || raw.includes('::')) return { ok: true, canonical: raw, reason: '' };
|
|
345
|
+
return { ok: false, canonical: null, reason: `"${voice}" is not a valid Piper voice id` };
|
|
346
|
+
}
|
|
347
|
+
// macos / windows-sapi: permissive when no list is available; '' = system default.
|
|
348
|
+
if (raw === '' && r.defaultVoice === '') return { ok: true, canonical: '', reason: '' };
|
|
349
|
+
if (raw) return { ok: true, canonical: raw, reason: '' };
|
|
350
|
+
return { ok: false, canonical: null, reason: `Empty voice for ${r.displayName}` };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {string} idOrAlias
|
|
357
|
+
* @returns {string} a human display name, or the input unchanged if unknown
|
|
358
|
+
* (mirrors provider-validator.getProviderDisplayName's passthrough).
|
|
359
|
+
*/
|
|
360
|
+
function displayName(idOrAlias) {
|
|
361
|
+
const r = getProvider(idOrAlias);
|
|
362
|
+
if (r) return r.displayName;
|
|
363
|
+
return typeof idOrAlias === 'string' ? idOrAlias : '';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Derived alias→engine table, built to MIRROR the resolver's ENGINE_ALIASES
|
|
368
|
+
* (utterance-resolver.js:52-57). A record whose id differs from its engineId
|
|
369
|
+
* contributes id→engineId; each non-identity alias contributes alias→engineId.
|
|
370
|
+
* Conformance group 6 asserts this deep-equals the resolver's table.
|
|
371
|
+
* @returns {Record<string,string>}
|
|
372
|
+
*/
|
|
373
|
+
function engineAliasTable() {
|
|
374
|
+
const table = {};
|
|
375
|
+
for (const r of RECORDS) {
|
|
376
|
+
if (r.id !== r.engineId) table[r.id] = r.engineId;
|
|
377
|
+
for (const a of r.aliases) {
|
|
378
|
+
if (a !== r.engineId) table[a] = r.engineId;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return table;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
getProvider,
|
|
386
|
+
listProviders,
|
|
387
|
+
providersFor,
|
|
388
|
+
isAvailable,
|
|
389
|
+
listVoices,
|
|
390
|
+
defaultVoice,
|
|
391
|
+
validateVoice,
|
|
392
|
+
displayName,
|
|
393
|
+
engineAliasTable,
|
|
394
|
+
kokoroGender,
|
|
395
|
+
ELEVENLABS_VOICES,
|
|
396
|
+
KOKORO_VOICE_IDS,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export default {
|
|
400
|
+
getProvider,
|
|
401
|
+
listProviders,
|
|
402
|
+
providersFor,
|
|
403
|
+
isAvailable,
|
|
404
|
+
listVoices,
|
|
405
|
+
defaultVoice,
|
|
406
|
+
validateVoice,
|
|
407
|
+
displayName,
|
|
408
|
+
engineAliasTable,
|
|
409
|
+
kokoroGender,
|
|
410
|
+
ELEVENLABS_VOICES,
|
|
411
|
+
KOKORO_VOICE_IDS,
|
|
412
|
+
};
|
|
@@ -1,87 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider voice catalogs —
|
|
3
|
-
* of the cloud/neural providers, plus a helper that returns the voice pool for
|
|
4
|
-
* whichever provider is currently active.
|
|
2
|
+
* Provider voice catalogs — BACK-COMPAT SHIM.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The canonical source of provider voice data is now
|
|
5
|
+
* `src/services/provider-catalog.js` (the Provider Catalog, SSOT Layer 2). This
|
|
6
|
+
* module re-exports the static voice lists (`ELEVENLABS_VOICES`,
|
|
7
|
+
* `KOKORO_VOICE_IDS`) and the gender helper from the catalog, and keeps the
|
|
8
|
+
* `voicesForProvider()` convenience used by BMAD gender-aware auto-assignment.
|
|
9
|
+
*
|
|
10
|
+
* Kept as a shim so existing importers (and test/unit/provider-voice-catalog.test.js)
|
|
11
|
+
* continue to work unmodified.
|
|
10
12
|
*
|
|
11
13
|
* @module services/provider-voice-catalog
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { isSingleVoiceProvider } from './agent-voice-store.js';
|
|
17
|
+
import {
|
|
18
|
+
ELEVENLABS_VOICES,
|
|
19
|
+
KOKORO_VOICE_IDS,
|
|
20
|
+
kokoroGender,
|
|
21
|
+
} from './provider-catalog.js';
|
|
22
|
+
|
|
23
|
+
export { ELEVENLABS_VOICES, KOKORO_VOICE_IDS, kokoroGender };
|
|
15
24
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ id: '
|
|
29
|
-
{ id: '
|
|
30
|
-
{ id: '
|
|
31
|
-
{ id: '
|
|
32
|
-
{ id: '
|
|
33
|
-
{ id: 'iP95p4xoKVk53GoZ742B', name: 'Chris', gender: 'Male', lang: 'en-US', desc: 'Charming, down-to-earth' },
|
|
34
|
-
{ id: 'nPczCjzI2devNBz1zQrb', name: 'Brian', gender: 'Male', lang: 'en-US', desc: 'Deep, comforting' },
|
|
35
|
-
{ id: 'onwK4e9ZLuTAKqWW03F9', name: 'Daniel', gender: 'Male', lang: 'en-GB', desc: 'Steady broadcaster' },
|
|
36
|
-
{ id: 'pFZP5JQG7iQjIQuC4Bku', name: 'Lily', gender: 'Female', lang: 'en-GB', desc: 'Velvety actress' },
|
|
37
|
-
{ id: 'pNInz6obpgDQGcFmaJgB', name: 'Adam', gender: 'Male', lang: 'en-US', desc: 'Dominant, firm' },
|
|
38
|
-
{ id: 'pqHfZKP75CvOlQylNhV4', name: 'Bill', gender: 'Male', lang: 'en-US', desc: 'Wise, mature' },
|
|
39
|
-
// ── Added from the ElevenLabs voice library (raw library voice_ids) ───────
|
|
40
|
-
{ id: 'HBDoL4wkcalemIO0nUAu', name: 'Emily', gender: 'Female', lang: 'en-GB', desc: 'RPG, immersive' },
|
|
41
|
-
{ id: 'Mtmp3KhFIjYpWYRycDe3', name: 'John', gender: 'Male', lang: 'en-US', desc: 'Raspy surfer' },
|
|
42
|
-
{ id: '8kgj5469z1URcH4MB2G4', name: 'Sakuya', gender: 'Female', lang: 'en-US', desc: 'Cheerful anime' },
|
|
43
|
-
{ id: '2ajXGJNYBR0iNHpS4VZb', name: 'Rob', gender: 'Male', lang: 'en-GB', desc: 'Tough, gritty' },
|
|
44
|
-
{ id: 'mMdTuD2nnFiCH88UdXSb', name: 'Jane', gender: 'Female', lang: 'en-US', desc: 'Cinematic villain' },
|
|
25
|
+
/**
|
|
26
|
+
* Curated built-in voices for the OS-native "discovered" providers (Windows SAPI,
|
|
27
|
+
* macOS `say`). Their real voices live on the target device — for a remote
|
|
28
|
+
* receiver we cannot enumerate them from here — so these standard voices (shipped
|
|
29
|
+
* by default on Windows 10/11 and macOS) give the picker an honest, selectable
|
|
30
|
+
* list. The `id` is the exact name the player passes to SelectVoice / `say -v`;
|
|
31
|
+
* if the receiver lacks one it falls back to the system default (benign).
|
|
32
|
+
*
|
|
33
|
+
* Verified against a real Windows 11 install (System.Speech GetInstalledVoices).
|
|
34
|
+
* The receiver remains authoritative — this is UI convenience, not inventory SSOT.
|
|
35
|
+
*/
|
|
36
|
+
const WINDOWS_SAPI_VOICES = [
|
|
37
|
+
{ id: 'Microsoft David Desktop', name: 'David (Desktop)', gender: 'Male', lang: 'en-US' },
|
|
38
|
+
{ id: 'Microsoft Zira Desktop', name: 'Zira (Desktop)', gender: 'Female', lang: 'en-US' },
|
|
39
|
+
{ id: 'Microsoft David', name: 'David', gender: 'Male', lang: 'en-US' },
|
|
40
|
+
{ id: 'Microsoft Mark', name: 'Mark', gender: 'Male', lang: 'en-US' },
|
|
41
|
+
{ id: 'Microsoft Zira', name: 'Zira', gender: 'Female', lang: 'en-US' },
|
|
45
42
|
];
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
'bf_alice','bf_emma','bf_isabella','bf_lily',
|
|
54
|
-
'bm_daniel','bm_fable','bm_george','bm_lewis',
|
|
55
|
-
// Japanese
|
|
56
|
-
'jf_alpha','jf_gongitsune','jf_nezumi','jf_tebukuro','jm_kumo',
|
|
57
|
-
// Mandarin Chinese
|
|
58
|
-
'zf_xiaobei','zf_xiaoni','zf_xiaoxiao','zf_xiaoyi','zm_yunxi','zm_yunxia','zm_yunyang',
|
|
59
|
-
// Spanish
|
|
60
|
-
'ef_dora','em_alex','em_santa',
|
|
61
|
-
// French
|
|
62
|
-
'ff_siwis',
|
|
63
|
-
// Hindi
|
|
64
|
-
'hf_alpha','hm_omega',
|
|
65
|
-
// Italian
|
|
66
|
-
'if_sara','im_nicola',
|
|
67
|
-
// Brazilian Portuguese
|
|
68
|
-
'pf_dora','pm_alex','pm_santa',
|
|
69
|
-
// Korean
|
|
70
|
-
'kf_alpha','km_hyunsu',
|
|
44
|
+
const MACOS_VOICES = [
|
|
45
|
+
{ id: 'Samantha', name: 'Samantha', gender: 'Female', lang: 'en-US' },
|
|
46
|
+
{ id: 'Alex', name: 'Alex', gender: 'Male', lang: 'en-US' },
|
|
47
|
+
{ id: 'Daniel', name: 'Daniel', gender: 'Male', lang: 'en-GB' },
|
|
48
|
+
{ id: 'Karen', name: 'Karen', gender: 'Female', lang: 'en-AU' },
|
|
49
|
+
{ id: 'Moira', name: 'Moira', gender: 'Female', lang: 'en-IE' },
|
|
71
50
|
];
|
|
72
51
|
|
|
73
|
-
|
|
74
|
-
* Infer a Kokoro voice's gender from its id. Kokoro ids follow `<lang><sex>_name`
|
|
75
|
-
* where the 2nd character is `f` (female) or `m` (male).
|
|
76
|
-
* @param {string} id
|
|
77
|
-
* @returns {'Female'|'Male'|''}
|
|
78
|
-
*/
|
|
79
|
-
export function kokoroGender(id) {
|
|
80
|
-
const c = typeof id === 'string' && id.length > 1 ? id[1].toLowerCase() : '';
|
|
81
|
-
if (c === 'f') return 'Female';
|
|
82
|
-
if (c === 'm') return 'Male';
|
|
83
|
-
return '';
|
|
84
|
-
}
|
|
52
|
+
export { WINDOWS_SAPI_VOICES, MACOS_VOICES };
|
|
85
53
|
|
|
86
54
|
/**
|
|
87
55
|
* Return the voice pool for the currently-active provider as `{ id, gender }`
|
|
@@ -108,6 +76,17 @@ export function voicesForProvider(provider, { scanInstalledVoices, getVoiceMeta
|
|
|
108
76
|
return ELEVENLABS_VOICES.map(v => ({ id: v.id, gender: v.gender || '' }));
|
|
109
77
|
}
|
|
110
78
|
|
|
79
|
+
// OS-native discovered providers: their voices live on the target device (the
|
|
80
|
+
// receiver in remote mode), so they can't be scanned here. Return the curated
|
|
81
|
+
// built-in list instead of falling through to the Piper disk scan (which would
|
|
82
|
+
// mislabel them as Piper voices).
|
|
83
|
+
if (p === 'sapi' || p === 'windows-sapi') {
|
|
84
|
+
return WINDOWS_SAPI_VOICES.map(v => ({ id: v.id, gender: v.gender || '' }));
|
|
85
|
+
}
|
|
86
|
+
if (p === 'macos' || p === 'macos-say' || p === 'say') {
|
|
87
|
+
return MACOS_VOICES.map(v => ({ id: v.id, gender: v.gender || '' }));
|
|
88
|
+
}
|
|
89
|
+
|
|
111
90
|
// Single-voice providers (e.g. soprano): one synthetic entry named after the
|
|
112
91
|
// provider — every agent ends up sharing it, which is correct for these.
|
|
113
92
|
if (isSingleVoiceProvider(p)) {
|
|
@@ -123,4 +102,4 @@ export function voicesForProvider(provider, { scanInstalledVoices, getVoiceMeta
|
|
|
123
102
|
return [];
|
|
124
103
|
}
|
|
125
104
|
|
|
126
|
-
export default { ELEVENLABS_VOICES, KOKORO_VOICE_IDS, kokoroGender, voicesForProvider };
|
|
105
|
+
export default { ELEVENLABS_VOICES, KOKORO_VOICE_IDS, WINDOWS_SAPI_VOICES, MACOS_VOICES, kokoroGender, voicesForProvider };
|
|
@@ -53,6 +53,22 @@ export function getAvailableEngines() {
|
|
|
53
53
|
return TTS_ENGINES.filter(e => !e.platform || e.platform === process.platform);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Is this engine runnable on the current OS? (Native engines are platform-bound.) */
|
|
57
|
+
export function isEngineSupported(engineId) {
|
|
58
|
+
const e = TTS_ENGINES.find(x => x.id === engineId);
|
|
59
|
+
return !!e && (!e.platform || e.platform === process.platform);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every engine, each tagged with `supported` for the current platform. Unlike
|
|
64
|
+
* getAvailableEngines() (which hides off-platform engines), this lists them all
|
|
65
|
+
* so the picker can show e.g. Windows SAPI greyed "(Not supported)" on Linux
|
|
66
|
+
* instead of appearing to be missing.
|
|
67
|
+
*/
|
|
68
|
+
export function getAllEngines() {
|
|
69
|
+
return TTS_ENGINES.map(e => ({ ...e, supported: !e.platform || e.platform === process.platform }));
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
export function checkEngineInstalled(engineId) {
|
|
57
73
|
const engine = TTS_ENGINES.find(e => e.id === engineId);
|
|
58
74
|
if (!engine) return false;
|
|
@@ -110,6 +126,19 @@ export function checkEngineInstalled(engineId) {
|
|
|
110
126
|
return false;
|
|
111
127
|
}
|
|
112
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Map a picker engine id to the provider name the SSH-remote sender + receiver
|
|
131
|
+
* understand. play-tts-ssh-remote.sh validates
|
|
132
|
+
* piper|soprano|macos|windows-sapi|kokoro|elevenlabs, but the picker/config use
|
|
133
|
+
* 'sapi' and 'macos-say'. This is the single translation point.
|
|
134
|
+
* @param {string} engineId
|
|
135
|
+
* @returns {string} receiver/sender provider name
|
|
136
|
+
*/
|
|
137
|
+
const RECEIVER_PROVIDER_MAP = { sapi: 'windows-sapi', 'macos-say': 'macos' };
|
|
138
|
+
export function receiverProviderId(engineId) {
|
|
139
|
+
return RECEIVER_PROVIDER_MAP[engineId] || engineId;
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
export function getEngineStatuses() {
|
|
114
143
|
return getAvailableEngines().map(e => ({
|
|
115
144
|
...e,
|