agentvibes 4.5.7 → 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/bmad-party-speak.sh +175 -0
- package/.claude/hooks-windows/bmad-party-speak.ps1 +207 -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 +42 -0
- package/bin/agent-vibes +1 -1
- package/bin/agentvibes-voice-browser.js +1 -1
- package/bin/mcp-server.js +1 -1
- package/bin/test-bmad-pr +1 -1
- package/package.json +110 -110
- package/src/console/tabs/agents-tab.js +240 -34
- package/src/console/tabs/install-tab.js +1 -0
- package/src/console/tabs/voices-tab.js +38 -5
- package/src/console/widgets/track-picker.js +50 -18
- package/src/installer.js +97 -3
- package/templates/agentvibes-receiver.sh +1 -1
|
@@ -31,6 +31,84 @@ import { spawn } from 'node:child_process';
|
|
|
31
31
|
// Max pretext length to prevent excessively long TTS utterances
|
|
32
32
|
const MAX_PRETEXT_LENGTH = 200;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Attach a blinking █ cursor to a set of blessed buttons.
|
|
36
|
+
* Works alongside existing focus/blur handlers (e.g. ►..◄ indicators).
|
|
37
|
+
* While a spinner is active on a button, blink is paused for that button.
|
|
38
|
+
* Returns { cleanup, startSpinner(btn, screen), stopSpinner(btn, screen) }.
|
|
39
|
+
*/
|
|
40
|
+
export function attachBtnBlink(btns, screen) {
|
|
41
|
+
let _interval = null;
|
|
42
|
+
let _on = true;
|
|
43
|
+
let _active = null;
|
|
44
|
+
let _spinning = null; // button currently showing a spinner
|
|
45
|
+
|
|
46
|
+
const _SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
47
|
+
let _spinIdx = 0;
|
|
48
|
+
let _spinInterval = null;
|
|
49
|
+
|
|
50
|
+
// Store original label on each button at attach time — never derive from current content
|
|
51
|
+
btns.forEach(btn => { btn._blinkBase = btn.content; });
|
|
52
|
+
|
|
53
|
+
// Focused with indicator ch right after ► e.g. ►█Preview◄ / ► Preview◄ (same width)
|
|
54
|
+
function _focused(base, ch) { return `►${ch}${base}◄`; }
|
|
55
|
+
|
|
56
|
+
function _tick() {
|
|
57
|
+
if (!_active || _active === _spinning) return;
|
|
58
|
+
_on = !_on;
|
|
59
|
+
_active.setContent(_focused(_active._blinkBase, _on ? '█' : ' '));
|
|
60
|
+
screen.render();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
btns.forEach(btn => {
|
|
64
|
+
btn.on('focus', () => {
|
|
65
|
+
_active = btn;
|
|
66
|
+
_on = true;
|
|
67
|
+
if (btn !== _spinning) {
|
|
68
|
+
btn.setContent(_focused(btn._blinkBase, '█'));
|
|
69
|
+
screen.render();
|
|
70
|
+
}
|
|
71
|
+
if (!_interval) _interval = setInterval(_tick, 500);
|
|
72
|
+
});
|
|
73
|
+
btn.on('blur', () => {
|
|
74
|
+
if (_active !== btn) return;
|
|
75
|
+
_active = null;
|
|
76
|
+
if (_interval) { clearInterval(_interval); _interval = null; _on = true; }
|
|
77
|
+
if (btn !== _spinning) {
|
|
78
|
+
btn.setContent(btn._blinkBase);
|
|
79
|
+
screen.render();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function startSpinner(btn) {
|
|
85
|
+
_spinning = btn;
|
|
86
|
+
_spinIdx = 0;
|
|
87
|
+
if (_spinInterval) clearInterval(_spinInterval);
|
|
88
|
+
_spinInterval = setInterval(() => {
|
|
89
|
+
_spinIdx = (_spinIdx + 1) % _SPIN.length;
|
|
90
|
+
btn.setContent(_active === btn
|
|
91
|
+
? _focused(btn._blinkBase, _SPIN[_spinIdx])
|
|
92
|
+
: `${_SPIN[_spinIdx]}${btn._blinkBase}`);
|
|
93
|
+
screen.render();
|
|
94
|
+
}, 80);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopSpinner(btn) {
|
|
98
|
+
if (_spinInterval) { clearInterval(_spinInterval); _spinInterval = null; }
|
|
99
|
+
_spinning = null;
|
|
100
|
+
btn.setContent(_active === btn ? _focused(btn._blinkBase, '█') : btn._blinkBase);
|
|
101
|
+
screen.render();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cleanup() {
|
|
105
|
+
if (_interval) { clearInterval(_interval); _interval = null; }
|
|
106
|
+
if (_spinInterval){ clearInterval(_spinInterval); _spinInterval = null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { cleanup, startSpinner, stopSpinner };
|
|
110
|
+
}
|
|
111
|
+
|
|
34
112
|
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
35
113
|
|
|
36
114
|
let blessed;
|
|
@@ -547,7 +625,7 @@ ${_tl('bmadDesc')}
|
|
|
547
625
|
top: 'center',
|
|
548
626
|
left: 'center',
|
|
549
627
|
width: 72,
|
|
550
|
-
height:
|
|
628
|
+
height: 19,
|
|
551
629
|
border: { type: 'line' },
|
|
552
630
|
tags: true,
|
|
553
631
|
label: _modalTitle(`${agent.icon || '🧙'} ${agent.displayName} (${agent.title || 'Agent'})`),
|
|
@@ -569,9 +647,13 @@ ${_tl('bmadDesc')}
|
|
|
569
647
|
const emoji = PERSONALITY_EMOJIS[p] || '';
|
|
570
648
|
return `${emoji} ${p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1)}`;
|
|
571
649
|
}},
|
|
572
|
-
{ key: '
|
|
650
|
+
{ key: 'musicTrack', label: 'Music Track', getValue: () => {
|
|
651
|
+
if (!draft.backgroundMusic.enabled) return '(disabled)';
|
|
652
|
+
return formatTrackName(draft.backgroundMusic.track) || '(none)';
|
|
653
|
+
}},
|
|
654
|
+
{ key: 'musicVol', label: 'Music Vol', getValue: () => {
|
|
573
655
|
if (!draft.backgroundMusic.enabled) return '(disabled)';
|
|
574
|
-
return `${
|
|
656
|
+
return `${draft.backgroundMusic.volume ?? 20}%`;
|
|
575
657
|
}},
|
|
576
658
|
];
|
|
577
659
|
|
|
@@ -612,7 +694,7 @@ ${_tl('bmadDesc')}
|
|
|
612
694
|
left: 2,
|
|
613
695
|
right: 2,
|
|
614
696
|
tags: true,
|
|
615
|
-
content: '{
|
|
697
|
+
content: '{white-fg}[↑↓] Navigate [Enter] Edit [Tab] → Preview/Save [Esc] Cancel{/white-fg}',
|
|
616
698
|
style: { bg: COLORS.contentBg },
|
|
617
699
|
});
|
|
618
700
|
|
|
@@ -634,22 +716,20 @@ ${_tl('bmadDesc')}
|
|
|
634
716
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
635
717
|
},
|
|
636
718
|
});
|
|
637
|
-
|
|
638
|
-
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
639
|
-
btn.setContent(`►${raw}◄`);
|
|
640
|
-
screen.render();
|
|
641
|
-
});
|
|
642
|
-
btn.on('blur', () => {
|
|
643
|
-
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
644
|
-
btn.setContent(raw);
|
|
645
|
-
screen.render();
|
|
646
|
-
});
|
|
719
|
+
// Focus indicator handled by attachBtnBlink
|
|
647
720
|
btn.key(['enter', 'space'], () => onClick());
|
|
648
721
|
btn.on('click', () => onClick());
|
|
649
722
|
return btn;
|
|
650
723
|
}
|
|
651
724
|
|
|
652
|
-
const
|
|
725
|
+
const previewBtn = _modalBtn('Preview', 4, () => {
|
|
726
|
+
_sampleAgentWithDraft({ ...agent }, draft, () => {
|
|
727
|
+
btnBlink.stopSpinner(previewBtn);
|
|
728
|
+
});
|
|
729
|
+
btnBlink.startSpinner(previewBtn);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const saveBtn = _modalBtn('Save', 18, () => {
|
|
653
733
|
// Only save fields that differ from global
|
|
654
734
|
const toSave = {};
|
|
655
735
|
if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
|
|
@@ -668,20 +748,26 @@ ${_tl('bmadDesc')}
|
|
|
668
748
|
_showSavedToast(agent.displayName);
|
|
669
749
|
});
|
|
670
750
|
|
|
671
|
-
const resetAllBtn = _modalBtn('Reset to Defaults',
|
|
751
|
+
const resetAllBtn = _modalBtn('Reset to Defaults', 26, () => {
|
|
672
752
|
voiceStore.resetAgentProfile(agent.id);
|
|
673
753
|
_closeModal();
|
|
674
754
|
refreshDisplay();
|
|
675
755
|
});
|
|
676
756
|
|
|
677
|
-
const cancelBtn = _modalBtn('Cancel',
|
|
757
|
+
const cancelBtn = _modalBtn('Cancel', 50, _closeModal);
|
|
758
|
+
|
|
759
|
+
// Blinking █ cursor + preview spinner — reusable across all modal buttons
|
|
760
|
+
const btnBlink = attachBtnBlink([previewBtn, saveBtn, resetAllBtn, cancelBtn], screen);
|
|
678
761
|
|
|
679
762
|
function _closeModal() {
|
|
680
763
|
if (_closed) return;
|
|
681
764
|
_closed = true;
|
|
682
765
|
_killPreview();
|
|
766
|
+
btnBlink.cleanup();
|
|
683
767
|
navigationService?.closeModal();
|
|
684
768
|
destroyList(modal, screen);
|
|
769
|
+
agentList.focus();
|
|
770
|
+
screen.render();
|
|
685
771
|
}
|
|
686
772
|
|
|
687
773
|
// Field editing via Enter
|
|
@@ -735,10 +821,9 @@ ${_tl('bmadDesc')}
|
|
|
735
821
|
});
|
|
736
822
|
break;
|
|
737
823
|
|
|
738
|
-
case '
|
|
739
|
-
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track
|
|
824
|
+
case 'musicTrack':
|
|
825
|
+
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track) => {
|
|
740
826
|
draft.backgroundMusic.track = track;
|
|
741
|
-
draft.backgroundMusic.volume = volume;
|
|
742
827
|
draft.backgroundMusic.enabled = true;
|
|
743
828
|
fieldList.setItems(_fieldItems());
|
|
744
829
|
fieldList.select(idx);
|
|
@@ -747,6 +832,20 @@ ${_tl('bmadDesc')}
|
|
|
747
832
|
}, () => {
|
|
748
833
|
fieldList.focus();
|
|
749
834
|
screen.render();
|
|
835
|
+
}, { skipVolume: true });
|
|
836
|
+
break;
|
|
837
|
+
|
|
838
|
+
case 'musicVol':
|
|
839
|
+
openVolumeInput(screen, draft.backgroundMusic.volume, (volume) => {
|
|
840
|
+
draft.backgroundMusic.volume = volume;
|
|
841
|
+
if (draft.backgroundMusic.track) draft.backgroundMusic.enabled = true;
|
|
842
|
+
fieldList.setItems(_fieldItems());
|
|
843
|
+
fieldList.select(idx);
|
|
844
|
+
fieldList.focus();
|
|
845
|
+
screen.render();
|
|
846
|
+
}, () => {
|
|
847
|
+
fieldList.focus();
|
|
848
|
+
screen.render();
|
|
750
849
|
});
|
|
751
850
|
break;
|
|
752
851
|
}
|
|
@@ -761,15 +860,50 @@ ${_tl('bmadDesc')}
|
|
|
761
860
|
|
|
762
861
|
// Escape = close
|
|
763
862
|
fieldList.key(['escape', 'q'], _closeModal);
|
|
863
|
+
previewBtn.key(['escape'], _closeModal);
|
|
764
864
|
saveBtn.key(['escape'], _closeModal);
|
|
765
865
|
resetAllBtn.key(['escape'], _closeModal);
|
|
766
866
|
cancelBtn.key(['escape'], _closeModal);
|
|
767
867
|
|
|
768
|
-
// Tab navigation within modal
|
|
769
|
-
fieldList.key(['tab'], () => {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
868
|
+
// Tab + arrow navigation within modal
|
|
869
|
+
fieldList.key(['tab'], () => { previewBtn.focus(); screen.render(); });
|
|
870
|
+
|
|
871
|
+
// Wrap: down on last field → focus first button; up on first field → focus first button
|
|
872
|
+
// One extra arrow press at boundary moves to button row.
|
|
873
|
+
// Track previous selection so arriving at boundary doesn't immediately jump.
|
|
874
|
+
let _prevFieldSel = 0;
|
|
875
|
+
fieldList.key(['down'], () => {
|
|
876
|
+
const cur = fieldList.selected ?? 0;
|
|
877
|
+
if (cur === FIELDS.length - 1 && _prevFieldSel === FIELDS.length - 1) {
|
|
878
|
+
previewBtn.focus(); screen.render();
|
|
879
|
+
}
|
|
880
|
+
_prevFieldSel = cur;
|
|
881
|
+
});
|
|
882
|
+
fieldList.key(['up'], () => {
|
|
883
|
+
const cur = fieldList.selected ?? 0;
|
|
884
|
+
if (cur === 0 && _prevFieldSel === 0) {
|
|
885
|
+
previewBtn.focus(); screen.render();
|
|
886
|
+
}
|
|
887
|
+
_prevFieldSel = cur;
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Wrap: up on buttons → back to field list (last/first field respectively)
|
|
891
|
+
previewBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
892
|
+
saveBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
893
|
+
resetAllBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
894
|
+
cancelBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
895
|
+
|
|
896
|
+
previewBtn.key(['tab', 'right'], () => { saveBtn.focus(); screen.render(); });
|
|
897
|
+
previewBtn.key(['left'], () => { cancelBtn.focus(); screen.render(); });
|
|
898
|
+
|
|
899
|
+
saveBtn.key(['tab', 'right'], () => { resetAllBtn.focus(); screen.render(); });
|
|
900
|
+
saveBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
|
|
901
|
+
|
|
902
|
+
resetAllBtn.key(['tab', 'right'], () => { cancelBtn.focus(); screen.render(); });
|
|
903
|
+
resetAllBtn.key(['left'], () => { saveBtn.focus(); screen.render(); });
|
|
904
|
+
|
|
905
|
+
cancelBtn.key(['tab', 'right'], () => { fieldList.focus(); screen.render(); });
|
|
906
|
+
cancelBtn.key(['left'], () => { resetAllBtn.focus(); screen.render(); });
|
|
773
907
|
|
|
774
908
|
fieldList.focus();
|
|
775
909
|
screen.render();
|
|
@@ -1047,8 +1181,8 @@ ${_tl('bmadDesc')}
|
|
|
1047
1181
|
// -------------------------------------------------------------------------
|
|
1048
1182
|
// Sample agent with a draft profile (no save) — same full pipeline
|
|
1049
1183
|
|
|
1050
|
-
function _sampleAgentWithDraft(agent, draft) {
|
|
1051
|
-
_sampleWithFullProfile(agent, draft);
|
|
1184
|
+
function _sampleAgentWithDraft(agent, draft, onComplete) {
|
|
1185
|
+
_sampleWithFullProfile(agent, draft, onComplete);
|
|
1052
1186
|
}
|
|
1053
1187
|
|
|
1054
1188
|
// -------------------------------------------------------------------------
|
|
@@ -1056,7 +1190,7 @@ ${_tl('bmadDesc')}
|
|
|
1056
1190
|
// Writes a temp agent profile JSON, then calls the enhanced TTS pipeline
|
|
1057
1191
|
// which applies voice + reverb + background music.
|
|
1058
1192
|
|
|
1059
|
-
function _sampleWithFullProfile(agent, profile) {
|
|
1193
|
+
function _sampleWithFullProfile(agent, profile, onComplete) {
|
|
1060
1194
|
_killPreview();
|
|
1061
1195
|
const gen = ++_playGeneration;
|
|
1062
1196
|
|
|
@@ -1071,9 +1205,9 @@ ${_tl('bmadDesc')}
|
|
|
1071
1205
|
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1072
1206
|
|
|
1073
1207
|
if (isWindows) {
|
|
1074
|
-
// On Windows,
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1208
|
+
// On Windows, call play-tts.ps1 via PowerShell with draft settings patched
|
|
1209
|
+
// in so reverb, background music, and personality are applied.
|
|
1210
|
+
_sampleWithFullEffectsWindows(gen, agent, profile, phrase, onComplete);
|
|
1077
1211
|
} else {
|
|
1078
1212
|
// On Linux/macOS/WSL, use play-tts.sh
|
|
1079
1213
|
const _spawnEnv = buildAudioEnv();
|
|
@@ -1178,6 +1312,77 @@ ${_tl('bmadDesc')}
|
|
|
1178
1312
|
});
|
|
1179
1313
|
}
|
|
1180
1314
|
|
|
1315
|
+
/** Windows full-effects preview: temporarily patches config files then calls play-tts.ps1 */
|
|
1316
|
+
function _sampleWithFullEffectsWindows(gen, agent, profile, phrase, onComplete) {
|
|
1317
|
+
const _spawnEnv = buildAudioEnv();
|
|
1318
|
+
const homeDir = process.env.USERPROFILE || os.homedir();
|
|
1319
|
+
// Use project-local .claude if it exists, else global ~/.claude
|
|
1320
|
+
const claudeDir = fs.existsSync(path.join(_projectRoot, '.claude'))
|
|
1321
|
+
? path.join(_projectRoot, '.claude')
|
|
1322
|
+
: path.join(homeDir, '.claude');
|
|
1323
|
+
const configDir = path.join(claudeDir, 'config');
|
|
1324
|
+
const hooksDir = path.join(claudeDir, 'hooks-windows');
|
|
1325
|
+
const playTts = path.join(hooksDir, 'play-tts.ps1');
|
|
1326
|
+
if (!fs.existsSync(playTts)) { _sampleWithPiperDirect(gen, profile.voice || '', phrase); return; }
|
|
1327
|
+
|
|
1328
|
+
// Files to temporarily patch
|
|
1329
|
+
const personalityFile = path.join(configDir, 'personality.txt');
|
|
1330
|
+
const reverbFile = path.join(configDir, 'reverb-level.txt');
|
|
1331
|
+
const bgEnabledFile = path.join(configDir, 'background-music-enabled.txt');
|
|
1332
|
+
const audioEffectsCfg = path.join(configDir, 'audio-effects.cfg');
|
|
1333
|
+
|
|
1334
|
+
// Save originals
|
|
1335
|
+
const _read = f => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } };
|
|
1336
|
+
const origPersonality = _read(personalityFile);
|
|
1337
|
+
const origReverb = _read(reverbFile);
|
|
1338
|
+
const origBgEnabled = _read(bgEnabledFile);
|
|
1339
|
+
const origAudioEffects = _read(audioEffectsCfg);
|
|
1340
|
+
|
|
1341
|
+
const bgMusic = profile.backgroundMusic;
|
|
1342
|
+
let tempCfgLine = '';
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
if (profile.personality && profile.personality !== 'none')
|
|
1346
|
+
fs.writeFileSync(personalityFile, profile.personality);
|
|
1347
|
+
if (profile.reverbPreset)
|
|
1348
|
+
fs.writeFileSync(reverbFile, profile.reverbPreset);
|
|
1349
|
+
if (bgMusic?.enabled && bgMusic?.track) {
|
|
1350
|
+
fs.writeFileSync(bgEnabledFile, 'true');
|
|
1351
|
+
const vol = ((bgMusic.volume ?? 20) / 100).toFixed(2);
|
|
1352
|
+
tempCfgLine = `${agent.id}||${bgMusic.track}|${vol}`;
|
|
1353
|
+
fs.writeFileSync(audioEffectsCfg, `${tempCfgLine}\n${origAudioEffects || ''}`);
|
|
1354
|
+
}
|
|
1355
|
+
} catch { /* degrade gracefully */ }
|
|
1356
|
+
|
|
1357
|
+
const voiceId = profile.voice || '';
|
|
1358
|
+
const psArgs = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase];
|
|
1359
|
+
if (voiceId) psArgs.push(voiceId);
|
|
1360
|
+
|
|
1361
|
+
const proc = spawn('powershell', psArgs, {
|
|
1362
|
+
stdio: 'ignore', detached: false, windowsHide: true,
|
|
1363
|
+
// CLAUDE_PROJECT_DIR tells play-tts.ps1 to use the project's .claude/config
|
|
1364
|
+
// rather than falling back to ~/.claude where our patches don't exist.
|
|
1365
|
+
env: { ..._spawnEnv, AGENTVIBES_AGENT_NAME: agent.id, CLAUDE_PROJECT_DIR: _projectRoot },
|
|
1366
|
+
});
|
|
1367
|
+
_playingProcess = proc;
|
|
1368
|
+
|
|
1369
|
+
function _restore() {
|
|
1370
|
+
try {
|
|
1371
|
+
if (origPersonality !== null) fs.writeFileSync(personalityFile, origPersonality);
|
|
1372
|
+
else try { fs.unlinkSync(personalityFile); } catch {}
|
|
1373
|
+
if (origReverb !== null) fs.writeFileSync(reverbFile, origReverb);
|
|
1374
|
+
if (bgMusic?.enabled && bgMusic?.track) {
|
|
1375
|
+
if (origBgEnabled !== null) fs.writeFileSync(bgEnabledFile, origBgEnabled);
|
|
1376
|
+
else try { fs.unlinkSync(bgEnabledFile); } catch {}
|
|
1377
|
+
if (origAudioEffects !== null) fs.writeFileSync(audioEffectsCfg, origAudioEffects);
|
|
1378
|
+
}
|
|
1379
|
+
} catch {}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
proc.on('exit', () => { if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); } _restore(); if (onComplete) onComplete(); });
|
|
1383
|
+
proc.on('error', () => { if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); } _restore(); if (onComplete) onComplete(); });
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1181
1386
|
// -------------------------------------------------------------------------
|
|
1182
1387
|
// Auto-assign helpers
|
|
1183
1388
|
|
|
@@ -1199,9 +1404,10 @@ ${_tl('bmadDesc')}
|
|
|
1199
1404
|
grace: 'Female', heather: 'Female', ivy: 'Female', jane: 'Female',
|
|
1200
1405
|
jenny: 'Female', julia: 'Female', kate: 'Female', laura: 'Female',
|
|
1201
1406
|
lily: 'Female', maria: 'Female', mary: 'Female', nina: 'Female',
|
|
1202
|
-
olivia: 'Female', paige: 'Female',
|
|
1203
|
-
|
|
1204
|
-
wendy: 'Female', zoe: 'Female',
|
|
1407
|
+
olivia: 'Female', paige: 'Female', quinn: 'Female', rachel: 'Female',
|
|
1408
|
+
sally: 'Female', sara: 'Female', sarah: 'Female', sophie: 'Female',
|
|
1409
|
+
tina: 'Female', wendy: 'Female', zoe: 'Female',
|
|
1410
|
+
freya: 'Female', saga: 'Female',
|
|
1205
1411
|
// Male
|
|
1206
1412
|
alan: 'Male', barry: 'Male', bob: 'Male', carl: 'Male',
|
|
1207
1413
|
charlie: 'Male', dan: 'Male', david: 'Male', eric: 'Male',
|
|
@@ -974,6 +974,7 @@ export function createInstallTab(screen, services) {
|
|
|
974
974
|
screen.key(['left'], () => {
|
|
975
975
|
if (box.hidden || _checking) return;
|
|
976
976
|
if (_screen === 4) return;
|
|
977
|
+
if (_screen === 5) return; // Screen 5: ← handled by button nav
|
|
977
978
|
if (_screen > 0) {
|
|
978
979
|
_screen--;
|
|
979
980
|
_showCurrentScreen();
|
|
@@ -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'], () => {
|