agentvibes 4.6.0 → 4.6.2
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/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/tts-pretext.txt +1 -0
- package/.claude/hooks/audio-processor.sh +1 -1
- package/.claude/hooks-windows/bmad-party-speak.ps1 +66 -0
- package/.claude/hooks-windows/bmad-speak.ps1 +32 -7
- package/.claude/hooks-windows/play-tts-piper.ps1 +43 -6
- package/.claude/hooks-windows/play-tts.ps1 +57 -30
- package/.mcp.json +7 -0
- package/README.md +64 -2
- package/RELEASE_NOTES.md +22 -0
- package/package.json +110 -110
- package/src/console/tabs/agents-tab.js +240 -34
- package/src/console/tabs/voices-tab.js +38 -5
- package/src/console/widgets/track-picker.js +50 -18
- package/templates/agentvibes-receiver.sh +1 -1
|
@@ -190,12 +190,13 @@ export const COL_GENDER_W = 10;
|
|
|
190
190
|
|
|
191
191
|
// Well-known piper dataset → gender
|
|
192
192
|
const GENDER_MAP = {
|
|
193
|
+
// Single-speaker datasets
|
|
193
194
|
amy: 'Female', kristin: 'Female', jenny: 'Female', cori: 'Female',
|
|
194
195
|
aria: 'Female', glados: 'Female', litvyak: 'Female', hfc_female: 'Female',
|
|
195
196
|
ljspeech: 'Female',
|
|
196
197
|
alan: 'Male', joe: 'Male', john: 'Male', ryan: 'Male', lessac: 'Male',
|
|
197
198
|
kusal: 'Male', hfc_male: 'Male', danny: 'Male', arctic: 'Male',
|
|
198
|
-
l2arctic: 'Male',
|
|
199
|
+
l2arctic: 'Male',
|
|
199
200
|
// 16Speakers multi-speaker model (names from speaker_id_map)
|
|
200
201
|
cori_samuel: 'Female', kara_shallenberg: 'Female', kristin_hughes: 'Female',
|
|
201
202
|
maria_kasper: 'Female', rose_ibex: 'Female', owlivia: 'Female',
|
|
@@ -203,6 +204,21 @@ const GENDER_MAP = {
|
|
|
203
204
|
mike_pelton: 'Male', mark_nelson: 'Male', michael_scherer: 'Male',
|
|
204
205
|
james_k_white: 'Male', progressingamerica: 'Male', steve_c: 'Male',
|
|
205
206
|
paul_hampton: 'Male', martin_clifton: 'Male',
|
|
207
|
+
// LibriTTS / common first names used as multi-speaker speaker IDs
|
|
208
|
+
anna: 'Female', bella: 'Female', chloe: 'Female', donna: 'Female',
|
|
209
|
+
ella: 'Female', faith: 'Female', gina: 'Female', holly: 'Female',
|
|
210
|
+
ivy: 'Female', jane: 'Female', kelly: 'Female', laura: 'Female',
|
|
211
|
+
mary: 'Female', nina: 'Female', olivia: 'Female', penny: 'Female',
|
|
212
|
+
rachel: 'Female', sarah: 'Female', tara: 'Female', uma: 'Female',
|
|
213
|
+
vera: 'Female', wendy: 'Female', yara: 'Female', zoe: 'Female',
|
|
214
|
+
betty: 'Female', cindy: 'Female', debra: 'Female', erica: 'Female',
|
|
215
|
+
faye: 'Female', gloria: 'Female', quinn: 'Female',
|
|
216
|
+
alex: 'Male', ben: 'Male', carl: 'Male', dan: 'Male', evan: 'Male',
|
|
217
|
+
frank: 'Male', greg: 'Male', hank: 'Male', ivan: 'Male', jake: 'Male',
|
|
218
|
+
kevin: 'Male', leo: 'Male', mike: 'Male', nathan: 'Male', oscar: 'Male',
|
|
219
|
+
paul: 'Male', rick: 'Male', sam: 'Male', tom: 'Male', victor: 'Male',
|
|
220
|
+
will: 'Male', xavier: 'Male', zach: 'Male', adam: 'Male', brad: 'Male',
|
|
221
|
+
colin: 'Male', derek: 'Male', ethan: 'Male', felix: 'Male',
|
|
206
222
|
};
|
|
207
223
|
|
|
208
224
|
// Well-known piper dataset → nice display name
|
|
@@ -229,9 +245,15 @@ export function inferGender(voiceId, dataset) {
|
|
|
229
245
|
// Explicit in name
|
|
230
246
|
if (id.includes('_female') || ds.includes('female')) return 'Female';
|
|
231
247
|
if (id.includes('_male') || ds.includes('male')) return 'Male';
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
248
|
+
// Dataset lookup first
|
|
249
|
+
if (ds && GENDER_MAP[ds]) return GENDER_MAP[ds];
|
|
250
|
+
// For multi-speaker speaker names like "Anna-9", strip trailing "-N" suffix
|
|
251
|
+
// then look up the base name (e.g. "anna")
|
|
252
|
+
const baseName = id.replace(/-\d+$/, '');
|
|
253
|
+
if (GENDER_MAP[baseName]) return GENDER_MAP[baseName];
|
|
254
|
+
// Fall back to middle segment of voice ID (e.g. "ryan" from "en_US-ryan-high")
|
|
255
|
+
const segment = id.split('-')[1] ?? '';
|
|
256
|
+
return GENDER_MAP[segment] ?? GENDER_MAP[id] ?? '—';
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
/**
|
|
@@ -321,7 +343,18 @@ export function parseMultiSpeaker(voiceId) {
|
|
|
321
343
|
const jsonPath = path.join(PIPER_VOICES_DIR, model + '.onnx.json');
|
|
322
344
|
try {
|
|
323
345
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
324
|
-
|
|
346
|
+
let speakerId = data.speaker_id_map?.[speakerName] ?? null;
|
|
347
|
+
// Fallback: if the .onnx.json still has raw p-names (not yet patched),
|
|
348
|
+
// look up the numeric speaker ID from voice-assignments.json catalog.
|
|
349
|
+
if (speakerId == null && model === 'en_US-libritts-high') {
|
|
350
|
+
try {
|
|
351
|
+
const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
|
|
352
|
+
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
|
353
|
+
const speakers = catalog.libritts_speakers ?? {};
|
|
354
|
+
const entry = Object.entries(speakers).find(([, e]) => e.voice_name === speakerName);
|
|
355
|
+
if (entry) speakerId = parseInt(entry[0], 10);
|
|
356
|
+
} catch { /* non-fatal */ }
|
|
357
|
+
}
|
|
325
358
|
return { model, speakerId, speakerName, isMultiSpeaker: true };
|
|
326
359
|
} catch {
|
|
327
360
|
return { model, speakerId: null, speakerName, isMultiSpeaker: true };
|
|
@@ -42,30 +42,48 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
|
|
|
42
42
|
parent: screen,
|
|
43
43
|
top: 'center',
|
|
44
44
|
left: 'center',
|
|
45
|
-
width:
|
|
46
|
-
height:
|
|
45
|
+
width: 44,
|
|
46
|
+
height: 11,
|
|
47
47
|
border: { type: 'line' },
|
|
48
48
|
tags: true,
|
|
49
49
|
label: _modalTitle('Music Volume'),
|
|
50
50
|
style: { border: { fg: 'bright-cyan' } },
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
blessed.text({
|
|
54
54
|
parent: box,
|
|
55
55
|
top: 1,
|
|
56
56
|
left: 2,
|
|
57
|
-
width:
|
|
57
|
+
width: 38,
|
|
58
|
+
tags: true,
|
|
59
|
+
content: '{cyan-fg}Use ← → arrow keys to adjust volume{/cyan-fg}',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const barText = blessed.text({
|
|
63
|
+
parent: box,
|
|
64
|
+
top: 3,
|
|
65
|
+
left: 2,
|
|
66
|
+
width: 38,
|
|
58
67
|
tags: true,
|
|
59
68
|
content: '',
|
|
60
69
|
});
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
blessed.text({
|
|
63
72
|
parent: box,
|
|
64
73
|
top: 5,
|
|
65
|
-
left:
|
|
66
|
-
width:
|
|
74
|
+
left: 2,
|
|
75
|
+
width: 38,
|
|
67
76
|
tags: true,
|
|
68
|
-
content: '{
|
|
77
|
+
content: '{white-fg}[← →] ±5 [0-9] number [Esc] Cancel{/white-fg}',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
blessed.text({
|
|
81
|
+
parent: box,
|
|
82
|
+
top: 7,
|
|
83
|
+
left: 2,
|
|
84
|
+
width: 38,
|
|
85
|
+
tags: true,
|
|
86
|
+
content: '{white-fg}[Enter] Confirm then {bold}{cyan-fg}[Tab]{/cyan-fg}{/bold} → Save{/white-fg}',
|
|
69
87
|
});
|
|
70
88
|
|
|
71
89
|
function _renderBar() {
|
|
@@ -73,10 +91,13 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
|
|
|
73
91
|
const empty = 20 - filled;
|
|
74
92
|
const bar = '{bright-cyan-fg}' + '█'.repeat(filled) + '{/bright-cyan-fg}' +
|
|
75
93
|
'{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
|
|
76
|
-
barText.setContent(`{
|
|
94
|
+
barText.setContent(`{white-fg}Volume:{/white-fg} ${bar} {bold}${vol}%{/bold}`);
|
|
77
95
|
screen.render();
|
|
78
96
|
}
|
|
79
97
|
_renderBar();
|
|
98
|
+
// Take focus so fieldList's key handlers don't fire while this dialog is open
|
|
99
|
+
box.focus();
|
|
100
|
+
screen.render();
|
|
80
101
|
|
|
81
102
|
// Capture keypress directly on screen to avoid input mode issues
|
|
82
103
|
let _digits = '';
|
|
@@ -99,8 +120,12 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
|
|
|
99
120
|
screen.removeListener('keypress', _onKey);
|
|
100
121
|
box.destroy();
|
|
101
122
|
screen.render();
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
// Defer callbacks so the Enter keypress finishes propagating before fieldList
|
|
124
|
+
// regains focus — otherwise the same Enter event re-opens the track picker.
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (confirm && onConfirm) onConfirm(vol);
|
|
127
|
+
if (onClose) onClose();
|
|
128
|
+
}, 0);
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
|
|
@@ -121,7 +146,7 @@ const BUILT_IN_TRACKS = [
|
|
|
121
146
|
* @param {Function} onSelect - called with (trackFile, volume)
|
|
122
147
|
* @param {Function} [onClose] - called after modal fully closes
|
|
123
148
|
*/
|
|
124
|
-
export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose) {
|
|
149
|
+
export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose, options = {}) {
|
|
125
150
|
const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
|
|
126
151
|
let tracks;
|
|
127
152
|
try {
|
|
@@ -271,17 +296,24 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
|
|
|
271
296
|
if (selected) _previewTrack(selected.file);
|
|
272
297
|
});
|
|
273
298
|
|
|
274
|
-
// Enter = select track,
|
|
299
|
+
// Enter = select track; if skipVolume, return track only, otherwise prompt for volume
|
|
275
300
|
list.key(['enter'], () => {
|
|
276
301
|
const selected = tracks[list.selected];
|
|
277
302
|
if (!selected) return;
|
|
278
|
-
// Close the track list first (without firing onClose yet), then open volume input
|
|
279
303
|
_killPreview();
|
|
280
304
|
if (list._label2) list._label2.destroy();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
305
|
+
if (options.skipVolume) {
|
|
306
|
+
destroyList(list, screen, null);
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
onSelect(selected.file);
|
|
309
|
+
if (onClose) onClose();
|
|
310
|
+
}, 0);
|
|
311
|
+
} else {
|
|
312
|
+
destroyList(list, screen, null);
|
|
313
|
+
openVolumeInput(screen, currentVolume ?? 20, (volume) => {
|
|
314
|
+
onSelect(selected.file, volume);
|
|
315
|
+
}, onClose);
|
|
316
|
+
}
|
|
285
317
|
});
|
|
286
318
|
|
|
287
319
|
list.key(['escape', 'q'], () => {
|
|
@@ -441,7 +441,7 @@ if [[ -n "$BG_FILE" ]] && command -v ffmpeg &>/dev/null; then
|
|
|
441
441
|
TOTAL_DUR=$(awk "BEGIN {printf \"%.2f\", $DURATION + 2}")
|
|
442
442
|
FADE_OUT=$(awk "BEGIN {printf \"%.2f\", $DURATION}")
|
|
443
443
|
timeout 20 ffmpeg -y -i "$PLAY_FILE" -stream_loop -1 -i "$BG_PATH" \
|
|
444
|
-
-filter_complex "[1:a]volume=${BG_VOLUME},afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT}:d=2[bg];[0:a]adelay=
|
|
444
|
+
-filter_complex "[1:a]volume=${BG_VOLUME},afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT}:d=2[bg];[0:a]adelay=1000|1000,volume=1.5[v];[v][bg]amix=inputs=2:duration=longest:normalize=0[out]" \
|
|
445
445
|
-map "[out]" -t "$TOTAL_DUR" "$FINAL_WAV" </dev/null 2>/dev/null && PLAY_FILE="$FINAL_WAV"
|
|
446
446
|
fi
|
|
447
447
|
fi
|