agentvibes 5.7.7 → 5.10.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/config.json +0 -2
- package/.claude/config/audio-effects.cfg +4 -4
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/play-tts-piper.sh +20 -13
- package/.claude/hooks/play-tts-ssh-remote.sh +2 -2
- package/.claude/hooks/voice-manager.sh +6 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
- package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
- package/.mcp.json +13 -9
- package/README.md +33 -2
- package/RELEASE_NOTES.md +80 -0
- package/mcp-server/server.py +17 -7
- package/package.json +2 -2
- package/src/commands/install-mcp.js +270 -16
- package/src/console/app.js +3 -3
- package/src/console/audio-env.js +4 -1
- package/src/console/tabs/agents-tab.js +89 -66
- package/src/console/tabs/music-tab.js +4 -3
- package/src/console/tabs/receiver-tab.js +13 -13
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +291 -47
- package/src/console/tabs/voices-tab.js +17 -5
- package/src/console/widgets/personality-picker.js +2 -2
- package/src/console/widgets/reverb-picker.js +1 -1
- package/src/installer.js +32 -27
- package/src/services/provider-service.js +1 -1
- package/src/services/tts-engine-service.js +2 -2
- package/src/utils/audio-duration-validator.js +2 -2
- package/src/utils/list-formatter.js +9 -3
- package/src/utils/platform-resolver.js +369 -0
- package/src/utils/provider-validator.js +9 -9
- package/.agentvibes/install-manifest.json +0 -442
- package/.claude/config/background-music-position.txt +0 -27
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks/bmad-party-manager.sh +0 -225
- package/.claude/hooks/stop.sh +0 -38
- package/.claude/piper-voices-dir.txt +0 -1
- /package/.claude/audio/tracks/{CelestialVelvet.mp3 → celestial_velvet.mp3} +0 -0
|
@@ -47,6 +47,7 @@ import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
|
47
47
|
import { spawn } from 'node:child_process';
|
|
48
48
|
import os from 'node:os';
|
|
49
49
|
import crypto from 'node:crypto';
|
|
50
|
+
import net from 'node:net';
|
|
50
51
|
|
|
51
52
|
const _execFileAsync = promisify(execFile);
|
|
52
53
|
|
|
@@ -87,6 +88,60 @@ const COLORS = {
|
|
|
87
88
|
|
|
88
89
|
const FOOTER_TEXT = '[Enter] Continue [Esc] Back [Tab] Next Tab [Q] Quit';
|
|
89
90
|
|
|
91
|
+
// Maps non-Piper engine IDs to their canonical voice ID and display label.
|
|
92
|
+
// Used by the voice picker, _buildFields display, and auto-save logic.
|
|
93
|
+
const NATIVE_ENGINE_VOICES = {
|
|
94
|
+
soprano: { id: 'soprano', label: 'Soprano' },
|
|
95
|
+
sapi: { id: 'sapi', label: 'Windows SAPI' },
|
|
96
|
+
'macos-say': { id: 'macos-say', label: 'macOS Say' },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Soprano WebUI auto-start helpers
|
|
101
|
+
|
|
102
|
+
function _checkSopranoPort(port) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const socket = net.createConnection({ host: '127.0.0.1', port });
|
|
105
|
+
socket.setTimeout(2000);
|
|
106
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
107
|
+
socket.once('error', () => resolve(false));
|
|
108
|
+
socket.once('timeout', () => { socket.destroy(); resolve(false); });
|
|
109
|
+
// Absorb any late errors emitted after destroy() to prevent uncaught 'error' crash
|
|
110
|
+
socket.on('error', () => {});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Timestamp of last soprano-webui spawn; prevents duplicate processes on rapid re-entry
|
|
115
|
+
let _sopranoSpawnedAt = 0;
|
|
116
|
+
|
|
117
|
+
async function _ensureSopranoWebUI(onStatus, signal) {
|
|
118
|
+
const port = parseInt(process.env.SOPRANO_PORT || '7860', 10);
|
|
119
|
+
if (signal?.aborted) return false;
|
|
120
|
+
if (await _checkSopranoPort(port)) return true;
|
|
121
|
+
onStatus('Starting Soprano WebUI...');
|
|
122
|
+
// Only spawn a new soprano-webui process if we haven't done so in the last 10 s
|
|
123
|
+
if (Date.now() - _sopranoSpawnedAt > 10_000) {
|
|
124
|
+
_sopranoSpawnedAt = Date.now();
|
|
125
|
+
try {
|
|
126
|
+
const p = spawn('soprano-webui', [], { // NOSONAR
|
|
127
|
+
stdio: 'ignore', detached: true, windowsHide: true,
|
|
128
|
+
shell: process.platform === 'win32',
|
|
129
|
+
});
|
|
130
|
+
p.unref();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
process.stderr.write(`[AgentVibes] soprano-webui spawn failed: ${e.message}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < 45; i++) {
|
|
136
|
+
if (signal?.aborted) return false;
|
|
137
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
138
|
+
if (signal?.aborted) return false;
|
|
139
|
+
if (await _checkSopranoPort(port)) return true;
|
|
140
|
+
onStatus(`Starting Soprano WebUI... ${(i + 1) * 2}s`);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
90
145
|
// ---------------------------------------------------------------------------
|
|
91
146
|
// Exported pure helpers (kept from install-tab for backward compat)
|
|
92
147
|
|
|
@@ -807,7 +862,7 @@ export function createSetupTab(screen, services) {
|
|
|
807
862
|
function _buildFields() {
|
|
808
863
|
const base = [
|
|
809
864
|
{ key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || `(global: ${globalEngine})` },
|
|
810
|
-
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || `(global: ${globalVoice})` },
|
|
865
|
+
{ key: 'voice', label: 'Voice', getValue: () => NATIVE_ENGINE_VOICES[draft.voice]?.label ?? (draft.voice || `(global: ${globalVoice})`) },
|
|
811
866
|
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(none)' },
|
|
812
867
|
{ key: 'reverb', label: 'Reverb', getValue: () => {
|
|
813
868
|
const p = REVERB_PRESETS.find(r => r.value === draft.reverbPreset);
|
|
@@ -893,7 +948,7 @@ export function createSetupTab(screen, services) {
|
|
|
893
948
|
|
|
894
949
|
// Auto-save: persist both audio config and Hermes SSH config
|
|
895
950
|
function _autoSave(silent) {
|
|
896
|
-
const engine = draft.ttsEngine || (draft.voice ? 'piper' : '');
|
|
951
|
+
const engine = draft.ttsEngine || (draft.voice && !NATIVE_ENGINE_VOICES[draft.voice] ? 'piper' : '');
|
|
897
952
|
saveLlmConfigSync('hermes', {
|
|
898
953
|
voice: draft.voice,
|
|
899
954
|
pretext: draft.pretext,
|
|
@@ -938,7 +993,19 @@ export function createSetupTab(screen, services) {
|
|
|
938
993
|
env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
|
|
939
994
|
});
|
|
940
995
|
_previewModalProc = proc;
|
|
941
|
-
proc.on('exit', () => {
|
|
996
|
+
proc.on('exit', (code) => {
|
|
997
|
+
_previewModalProc = null;
|
|
998
|
+
if (!_closed) {
|
|
999
|
+
if (code !== 0 && code !== null) {
|
|
1000
|
+
const engineLabel = NATIVE_ENGINE_VOICES[draft.ttsEngine]?.label || draft.ttsEngine || 'engine';
|
|
1001
|
+
previewLine.setContent(`{red-fg}Preview failed — is ${engineLabel} running/installed?{/red-fg}`);
|
|
1002
|
+
screen.render();
|
|
1003
|
+
setTimeout(() => { if (!_closed) { previewLine.setContent(''); screen.render(); } }, 4000);
|
|
1004
|
+
} else {
|
|
1005
|
+
previewLine.setContent(''); screen.render();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
942
1009
|
proc.on('error', () => { _previewModalProc = null; if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); } });
|
|
943
1010
|
}
|
|
944
1011
|
|
|
@@ -1092,7 +1159,7 @@ export function createSetupTab(screen, services) {
|
|
|
1092
1159
|
const raw = fs.readFileSync(sshConfigPath, 'utf8');
|
|
1093
1160
|
const seen = new Set();
|
|
1094
1161
|
for (const line of raw.split('\n')) {
|
|
1095
|
-
const m = line.match(/^\s*IdentityFile\s+(.+)\s*$/i);
|
|
1162
|
+
const m = line.match(/^\s*IdentityFile\s+(.+)\s*$/i); // NOSONAR
|
|
1096
1163
|
if (m) {
|
|
1097
1164
|
const expanded = m[1].trim().replace(/^~/, os.homedir());
|
|
1098
1165
|
if (!seen.has(expanded)) { seen.add(expanded); keys.push(expanded); }
|
|
@@ -1230,7 +1297,7 @@ export function createSetupTab(screen, services) {
|
|
|
1230
1297
|
try {
|
|
1231
1298
|
const lines = fs.readFileSync(path.join(os.homedir(), '.ssh', 'config'), 'utf8').split('\n');
|
|
1232
1299
|
for (const line of lines) {
|
|
1233
|
-
const m = line.match(/^\s*[Hh]ost\s+(.+)$/);
|
|
1300
|
+
const m = line.match(/^\s*[Hh]ost\s+(.+)$/); // NOSONAR
|
|
1234
1301
|
if (m) {
|
|
1235
1302
|
for (const name of m[1].trim().split(/\s+/)) {
|
|
1236
1303
|
if (!name.includes('*') && !name.includes('?') && !seen.has(name)) {
|
|
@@ -1541,10 +1608,10 @@ export function createSetupTab(screen, services) {
|
|
|
1541
1608
|
const sampleText = 'This is how your audio settings sound right now.';
|
|
1542
1609
|
const script = path.join(targetDir, '.claude', hooksSubdir, isWin ? 'play-tts.ps1' : 'play-tts.sh');
|
|
1543
1610
|
const proc = isWin
|
|
1544
|
-
? spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText], {
|
|
1611
|
+
? spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText], { // NOSONAR
|
|
1545
1612
|
stdio: 'ignore', windowsHide: true, env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
|
|
1546
1613
|
})
|
|
1547
|
-
: spawn('bash', [script, sampleText], {
|
|
1614
|
+
: spawn('bash', [script, sampleText], { // NOSONAR
|
|
1548
1615
|
stdio: 'ignore', env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
|
|
1549
1616
|
});
|
|
1550
1617
|
_previewProc = proc;
|
|
@@ -1675,7 +1742,7 @@ export function createSetupTab(screen, services) {
|
|
|
1675
1742
|
function _buildFields() {
|
|
1676
1743
|
const base = [
|
|
1677
1744
|
{ key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || `(global: ${globalEngine})` },
|
|
1678
|
-
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || `(global: ${globalVoice})` },
|
|
1745
|
+
{ key: 'voice', label: 'Voice', getValue: () => NATIVE_ENGINE_VOICES[draft.voice]?.label ?? (draft.voice || `(global: ${globalVoice})`) },
|
|
1679
1746
|
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(none)' },
|
|
1680
1747
|
{ key: 'reverb', label: 'Reverb', getValue: () => {
|
|
1681
1748
|
const p = REVERB_PRESETS.find(r => r.value === draft.reverbPreset);
|
|
@@ -1781,11 +1848,13 @@ export function createSetupTab(screen, services) {
|
|
|
1781
1848
|
// eliminating the race condition when Preview is clicked twice rapidly.
|
|
1782
1849
|
let _previewModalProc = null;
|
|
1783
1850
|
let _bgRestoreFn = null;
|
|
1851
|
+
let _previewEnsureAbort = null;
|
|
1784
1852
|
function _killPreview() {
|
|
1785
1853
|
// Restore bg music immediately (synchronously) before killing the process.
|
|
1786
1854
|
// This prevents the async exit-handler race where a second Preview invocation
|
|
1787
1855
|
// reads bgWas=true (music already enabled) before the first's exit fires.
|
|
1788
1856
|
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1857
|
+
if (_previewEnsureAbort) { _previewEnsureAbort.abort(); _previewEnsureAbort = null; }
|
|
1789
1858
|
if (_previewModalProc) {
|
|
1790
1859
|
try { _previewModalProc.kill(); } catch {}
|
|
1791
1860
|
_previewModalProc = null;
|
|
@@ -1828,41 +1897,73 @@ export function createSetupTab(screen, services) {
|
|
|
1828
1897
|
}
|
|
1829
1898
|
}
|
|
1830
1899
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1900
|
+
function _doSpawnPreview() {
|
|
1901
|
+
let cmd, args;
|
|
1902
|
+
if (isWin) {
|
|
1903
|
+
const script = path.join(_hooksBase, '.claude', hooksSubdir, 'play-tts.ps1');
|
|
1904
|
+
cmd = 'powershell';
|
|
1905
|
+
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText, '', '-llm', llmKey];
|
|
1906
|
+
} else {
|
|
1907
|
+
const script = path.join(_hooksBase, '.claude', hooksSubdir, 'play-tts.sh');
|
|
1908
|
+
cmd = 'bash';
|
|
1909
|
+
args = [script, sampleText, '', '--llm', llmKey];
|
|
1910
|
+
}
|
|
1911
|
+
const proc = spawn(cmd, args, {
|
|
1912
|
+
stdio: 'ignore',
|
|
1913
|
+
windowsHide: true,
|
|
1914
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir, AGENTVIBES_LLM_KEY: `llm:${llmKey}` },
|
|
1915
|
+
});
|
|
1916
|
+
_previewModalProc = proc;
|
|
1917
|
+
|
|
1918
|
+
proc.on('exit', (code) => {
|
|
1919
|
+
_previewModalProc = null;
|
|
1920
|
+
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1921
|
+
if (!_closed) {
|
|
1922
|
+
if (code !== 0 && code !== null) {
|
|
1923
|
+
const engineLabel = NATIVE_ENGINE_VOICES[draft.ttsEngine]?.label || draft.ttsEngine || 'engine';
|
|
1924
|
+
previewLine.setContent(`{red-fg}Preview failed — is ${engineLabel} running/installed?{/red-fg}`);
|
|
1925
|
+
screen.render();
|
|
1926
|
+
setTimeout(() => { if (!_closed) { previewLine.setContent(''); screen.render(); } }, 4000);
|
|
1927
|
+
} else {
|
|
1928
|
+
previewLine.setContent(''); screen.render();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
proc.on('error', () => {
|
|
1933
|
+
_previewModalProc = null;
|
|
1934
|
+
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1935
|
+
if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); }
|
|
1936
|
+
});
|
|
1937
|
+
} // end _doSpawnPreview
|
|
1848
1938
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1939
|
+
// Soprano on Windows: ensure WebUI server is running before preview
|
|
1940
|
+
if (draft.ttsEngine === 'soprano' && isWin) {
|
|
1941
|
+
previewLine.setContent('{cyan-fg}Checking Soprano...{/cyan-fg}');
|
|
1942
|
+
screen.render();
|
|
1943
|
+
_previewEnsureAbort = new AbortController();
|
|
1944
|
+
_ensureSopranoWebUI((msg) => {
|
|
1945
|
+
if (!_closed) { previewLine.setContent(`{cyan-fg}${msg}{/cyan-fg}`); screen.render(); }
|
|
1946
|
+
}, _previewEnsureAbort.signal).then((ready) => {
|
|
1947
|
+
_previewEnsureAbort = null;
|
|
1948
|
+
if (_closed) return;
|
|
1949
|
+
if (!ready) {
|
|
1950
|
+
previewLine.setContent('{red-fg}Soprano WebUI failed to start{/red-fg}');
|
|
1951
|
+
screen.render();
|
|
1952
|
+
setTimeout(() => { if (!_closed) { previewLine.setContent(''); screen.render(); } }, 4000);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
_doSpawnPreview();
|
|
1956
|
+
}).catch(() => { _previewEnsureAbort = null; });
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
_doSpawnPreview();
|
|
1960
|
+
} // end _playPreview
|
|
1860
1961
|
|
|
1861
1962
|
// Auto-save: persist draft to config immediately on any change
|
|
1862
1963
|
function _autoSave(silent) {
|
|
1863
|
-
//
|
|
1864
|
-
//
|
|
1865
|
-
const engine = draft.ttsEngine || (draft.voice ? 'piper' : '');
|
|
1964
|
+
// Preserve draft.ttsEngine as authoritative; only infer 'piper' when engine
|
|
1965
|
+
// is unset AND voice is not a native-engine canonical ID.
|
|
1966
|
+
const engine = draft.ttsEngine || (draft.voice && !NATIVE_ENGINE_VOICES[draft.voice] ? 'piper' : '');
|
|
1866
1967
|
saveLlmConfigSync(llmKey, {
|
|
1867
1968
|
voice: draft.voice,
|
|
1868
1969
|
pretext: draft.pretext,
|
|
@@ -2087,11 +2188,11 @@ export function createSetupTab(screen, services) {
|
|
|
2087
2188
|
|
|
2088
2189
|
picker.key(['enter'], () => {
|
|
2089
2190
|
const idx = picker.selected;
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2191
|
+
const selectedEngine = idx === 0 ? '' : engines[idx - 1].id;
|
|
2192
|
+
draft.ttsEngine = selectedEngine;
|
|
2193
|
+
// Auto-set voice to native engine canonical ID so the Voice field updates
|
|
2194
|
+
// immediately. For piper or empty engine, clear to '' (shows global default).
|
|
2195
|
+
draft.voice = NATIVE_ENGINE_VOICES[selectedEngine]?.id || '';
|
|
2095
2196
|
_closePicker();
|
|
2096
2197
|
});
|
|
2097
2198
|
|
|
@@ -2151,6 +2252,149 @@ export function createSetupTab(screen, services) {
|
|
|
2151
2252
|
destroyList(vpModal, screen, onDone);
|
|
2152
2253
|
}
|
|
2153
2254
|
|
|
2255
|
+
// AVI-S5.1/5.2: Single-item overlay for non-Piper engines.
|
|
2256
|
+
// scanInstalledVoices() is NOT called; Space previews via the correct engine binary.
|
|
2257
|
+
const nativeVoice = NATIVE_ENGINE_VOICES[draft.ttsEngine];
|
|
2258
|
+
if (nativeVoice) {
|
|
2259
|
+
draft.voice = nativeVoice.id;
|
|
2260
|
+
let _nvClosed = false;
|
|
2261
|
+
let _nvPreviewProc = null;
|
|
2262
|
+
let _nvEnsureAbort = null;
|
|
2263
|
+
|
|
2264
|
+
function _killNvPreview() {
|
|
2265
|
+
if (_nvPreviewProc) { try { _nvPreviewProc.kill(); } catch {} _nvPreviewProc = null; }
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function _closeNV() {
|
|
2269
|
+
if (_nvClosed) return;
|
|
2270
|
+
_nvClosed = true;
|
|
2271
|
+
_killNvPreview();
|
|
2272
|
+
if (_nvEnsureAbort) { _nvEnsureAbort.abort(); _nvEnsureAbort = null; }
|
|
2273
|
+
navigationService?.closeModal();
|
|
2274
|
+
destroyList(nvPicker, screen, onDone);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
const nvPicker = blessed.list({
|
|
2278
|
+
parent: screen,
|
|
2279
|
+
top: 'center',
|
|
2280
|
+
left: 'center',
|
|
2281
|
+
width: 52,
|
|
2282
|
+
height: 7,
|
|
2283
|
+
border: { type: 'line' },
|
|
2284
|
+
tags: true,
|
|
2285
|
+
label: ' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} ',
|
|
2286
|
+
keys: true,
|
|
2287
|
+
vi: false,
|
|
2288
|
+
mouse: true,
|
|
2289
|
+
style: {
|
|
2290
|
+
fg: COLORS.labelFg,
|
|
2291
|
+
bg: COLORS.contentBg,
|
|
2292
|
+
border: { fg: 'cyan' },
|
|
2293
|
+
selected: { bg: 'green', fg: 'white', bold: true },
|
|
2294
|
+
item: { fg: COLORS.labelFg },
|
|
2295
|
+
},
|
|
2296
|
+
});
|
|
2297
|
+
nvPicker.setFront();
|
|
2298
|
+
nvPicker.setItems([` ${nativeVoice.label} {gray-fg}[Space] preview [Enter] select{/gray-fg}`]);
|
|
2299
|
+
nvPicker.select(0);
|
|
2300
|
+
|
|
2301
|
+
function _previewNativeVoice() {
|
|
2302
|
+
if (_nvPreviewProc) {
|
|
2303
|
+
_killNvPreview();
|
|
2304
|
+
nvPicker.setLabel(' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} ');
|
|
2305
|
+
screen.render();
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const phrase = `Hi, I am the ${nativeVoice.label} voice.`;
|
|
2309
|
+
const engine = nativeVoice.id;
|
|
2310
|
+
|
|
2311
|
+
function _spawnAndTrack(cmd, args, opts) {
|
|
2312
|
+
let proc;
|
|
2313
|
+
try { proc = spawn(cmd, args, opts); } catch (e) {
|
|
2314
|
+
process.stderr.write(`[AgentVibes] preview spawn failed: ${e.message}\n`);
|
|
2315
|
+
if (!_nvClosed) { nvPicker.setLabel(' {red-fg}Engine not installed{/red-fg} '); screen.render(); }
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
_nvPreviewProc = proc;
|
|
2319
|
+
nvPicker.setLabel(` {cyan-fg}♪ ${nativeVoice.label}... (Space=stop){/cyan-fg} `);
|
|
2320
|
+
screen.render();
|
|
2321
|
+
proc.on('exit', (code) => {
|
|
2322
|
+
_nvPreviewProc = null;
|
|
2323
|
+
if (!_nvClosed) {
|
|
2324
|
+
if (code !== 0 && code !== null) {
|
|
2325
|
+
nvPicker.setLabel(` {red-fg}Preview failed (exit ${code}){/red-fg} `);
|
|
2326
|
+
setTimeout(() => { if (!_nvClosed) { nvPicker.setLabel(' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} '); screen.render(); } }, 3000);
|
|
2327
|
+
} else {
|
|
2328
|
+
nvPicker.setLabel(' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} ');
|
|
2329
|
+
}
|
|
2330
|
+
screen.render();
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
proc.on('error', () => {
|
|
2334
|
+
_nvPreviewProc = null;
|
|
2335
|
+
if (!_nvClosed) { nvPicker.setLabel(' {red-fg}Engine not installed{/red-fg} '); screen.render(); }
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (engine === 'soprano' && process.platform === 'win32' && !process.env.WSL_DISTRO_NAME) {
|
|
2340
|
+
// Ensure soprano WebUI is running before preview; start it if not.
|
|
2341
|
+
nvPicker.setLabel(' {cyan-fg}Checking Soprano...{/cyan-fg} ');
|
|
2342
|
+
screen.render();
|
|
2343
|
+
_nvEnsureAbort = new AbortController();
|
|
2344
|
+
_ensureSopranoWebUI((msg) => {
|
|
2345
|
+
if (!_nvClosed) { nvPicker.setLabel(` {cyan-fg}${msg}{/cyan-fg} `); screen.render(); }
|
|
2346
|
+
}, _nvEnsureAbort.signal).then((ready) => {
|
|
2347
|
+
_nvEnsureAbort = null;
|
|
2348
|
+
if (_nvClosed) return;
|
|
2349
|
+
if (!ready) {
|
|
2350
|
+
nvPicker.setLabel(' {red-fg}Soprano WebUI failed to start{/red-fg} ');
|
|
2351
|
+
screen.render();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const scriptPath = path.join(os.homedir(), '.claude', 'hooks-windows', 'play-tts-soprano.ps1');
|
|
2355
|
+
_spawnAndTrack('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, phrase], { stdio: 'ignore', windowsHide: true });
|
|
2356
|
+
}).catch(() => { _nvEnsureAbort = null; });
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
let proc = null;
|
|
2361
|
+
try {
|
|
2362
|
+
if (engine === 'soprano') {
|
|
2363
|
+
proc = spawn('soprano', [phrase], { stdio: 'ignore' }); // NOSONAR
|
|
2364
|
+
} else if (engine === 'sapi') {
|
|
2365
|
+
const safePhrase = phrase.replace(/'/g, "''");
|
|
2366
|
+
const sapiScript = `Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('${safePhrase}')`;
|
|
2367
|
+
proc = spawn('powershell', ['-NoProfile', '-Command', sapiScript], { stdio: 'ignore', windowsHide: true }); // NOSONAR
|
|
2368
|
+
} else if (engine === 'macos-say') {
|
|
2369
|
+
proc = spawn('say', [phrase], { stdio: 'ignore' }); // NOSONAR
|
|
2370
|
+
}
|
|
2371
|
+
} catch {}
|
|
2372
|
+
if (!proc) {
|
|
2373
|
+
nvPicker.setLabel(' {red-fg}Engine not installed{/red-fg} ');
|
|
2374
|
+
screen.render();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
_nvPreviewProc = proc;
|
|
2378
|
+
nvPicker.setLabel(` {cyan-fg}♪ ${nativeVoice.label}... (Space=stop){/cyan-fg} `);
|
|
2379
|
+
screen.render();
|
|
2380
|
+
proc.on('exit', () => {
|
|
2381
|
+
_nvPreviewProc = null;
|
|
2382
|
+
if (!_nvClosed) { nvPicker.setLabel(' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} '); screen.render(); }
|
|
2383
|
+
});
|
|
2384
|
+
proc.on('error', () => {
|
|
2385
|
+
_nvPreviewProc = null;
|
|
2386
|
+
if (!_nvClosed) { nvPicker.setLabel(' {red-fg}Engine not installed{/red-fg} '); screen.render(); }
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
nvPicker.key(['enter'], () => { draft.voice = nativeVoice.id; _closeNV(); });
|
|
2391
|
+
nvPicker.key(['space'], _previewNativeVoice);
|
|
2392
|
+
nvPicker.key(['escape', 'q', 'Q'], _closeNV);
|
|
2393
|
+
nvPicker.focus();
|
|
2394
|
+
screen.render();
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2154
2398
|
const vpModal = blessed.box({
|
|
2155
2399
|
parent: screen,
|
|
2156
2400
|
top: '6%',
|
|
@@ -2182,7 +2426,7 @@ export function createSetupTab(screen, services) {
|
|
|
2182
2426
|
style: {
|
|
2183
2427
|
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
2184
2428
|
border: { fg: 'blue' },
|
|
2185
|
-
selected: { bg: 'green', fg: '
|
|
2429
|
+
selected: { bg: 'green', fg: 'white', bold: true },
|
|
2186
2430
|
item: { fg: COLORS.labelFg },
|
|
2187
2431
|
},
|
|
2188
2432
|
});
|
|
@@ -2275,13 +2519,13 @@ export function createSetupTab(screen, services) {
|
|
|
2275
2519
|
if (_isWin) {
|
|
2276
2520
|
const _playTts = path.join(_hooksBase, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
2277
2521
|
const _llmArgs = llmKey ? ['-llm', llmKey] : [];
|
|
2278
|
-
rProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _playTts, phrase, voiceId, ..._llmArgs], {
|
|
2522
|
+
rProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _playTts, phrase, voiceId, ..._llmArgs], { // NOSONAR
|
|
2279
2523
|
stdio: 'ignore', detached: false, windowsHide: true, env: _rEnv,
|
|
2280
2524
|
});
|
|
2281
2525
|
} else {
|
|
2282
2526
|
const _playTts = path.join(_hooksBase, '.claude', 'hooks', 'play-tts.sh');
|
|
2283
2527
|
const _llmArgs = llmKey ? ['--llm', llmKey] : [];
|
|
2284
|
-
rProc = spawn('bash', [_playTts, phrase, voiceId, ..._llmArgs], {
|
|
2528
|
+
rProc = spawn('bash', [_playTts, phrase, voiceId, ..._llmArgs], { // NOSONAR
|
|
2285
2529
|
stdio: 'ignore', detached: true, env: _rEnv, cwd: targetDir,
|
|
2286
2530
|
});
|
|
2287
2531
|
}
|
|
@@ -429,7 +429,7 @@ export function scanInstalledVoices() {
|
|
|
429
429
|
const jsonPath = path.join(PIPER_VOICES_DIR, voiceId + '.onnx.json');
|
|
430
430
|
try {
|
|
431
431
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
432
|
-
if (data.num_speakers > 1 && data.speaker_id_map) {
|
|
432
|
+
if (data.num_speakers > 1 && data.speaker_id_map && Object.keys(data.speaker_id_map).length > 0) {
|
|
433
433
|
// Expand multi-speaker model into individual entries
|
|
434
434
|
for (const speakerName of Object.keys(data.speaker_id_map)) {
|
|
435
435
|
result.push(`${voiceId}${MS_SEP}${speakerName}`);
|
|
@@ -598,6 +598,18 @@ export function getVoiceMeta(voiceId) {
|
|
|
598
598
|
return result;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
// Prefer catalog name for known curated single-speaker voices (avoids dataset-based duplicates)
|
|
602
|
+
const catEntry = _catalogMap.get(voiceId);
|
|
603
|
+
if (catEntry) {
|
|
604
|
+
const result = {
|
|
605
|
+
displayName: catEntry.displayName,
|
|
606
|
+
gender: catEntry.gender || inferGender(voiceId, null),
|
|
607
|
+
provider: 'Piper',
|
|
608
|
+
};
|
|
609
|
+
_metaCache.set(voiceId, result);
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
|
|
601
613
|
let dataset = null;
|
|
602
614
|
try {
|
|
603
615
|
const jsonPath = path.join(PIPER_VOICES_DIR, voiceId + '.onnx.json');
|
|
@@ -924,12 +936,12 @@ export function createVoicesTab(screen, services) {
|
|
|
924
936
|
let proc;
|
|
925
937
|
if (isWindows) {
|
|
926
938
|
const playTts = path.join(hooksBase, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
927
|
-
proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase, voiceId], {
|
|
939
|
+
proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase, voiceId], { // NOSONAR
|
|
928
940
|
stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
|
|
929
941
|
});
|
|
930
942
|
} else {
|
|
931
943
|
const playTts = path.join(hooksBase, '.claude', 'hooks', 'play-tts.sh');
|
|
932
|
-
proc = spawn('bash', [playTts, phrase, voiceId], {
|
|
944
|
+
proc = spawn('bash', [playTts, phrase, voiceId], { // NOSONAR
|
|
933
945
|
stdio: ['ignore', 'ignore', 'pipe'], detached: true, env: _spawnEnv,
|
|
934
946
|
cwd: process.cwd(),
|
|
935
947
|
});
|
|
@@ -1500,13 +1512,13 @@ export function createVoicesTab(screen, services) {
|
|
|
1500
1512
|
Invoke-WebRequest -Uri '${configUrl}' -OutFile '${configFile.replace(/'/g, "''")}' -ErrorAction Stop
|
|
1501
1513
|
Write-Output 'PHASE:done'
|
|
1502
1514
|
`;
|
|
1503
|
-
dlProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript], {
|
|
1515
|
+
dlProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript], { // NOSONAR
|
|
1504
1516
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1505
1517
|
env: _spawnEnv,
|
|
1506
1518
|
});
|
|
1507
1519
|
} else {
|
|
1508
1520
|
const managerScript = path.resolve(packageRoot, '.claude', 'hooks', 'piper-voice-manager.sh');
|
|
1509
|
-
dlProc = spawn('bash', ['-c', 'source "$1" && download_voice "$2"', '_', managerScript, modelToDownload], {
|
|
1521
|
+
dlProc = spawn('bash', ['-c', 'source "$1" && download_voice "$2"', '_', managerScript, modelToDownload], { // NOSONAR
|
|
1510
1522
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1511
1523
|
env: _spawnEnv,
|
|
1512
1524
|
});
|
|
@@ -132,7 +132,7 @@ export function openPersonalityPicker(screen, currentPersonality, onSelect, onCl
|
|
|
132
132
|
const _cwdScript = path.join(process.cwd(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
133
133
|
const _homeScript = path.join(os.homedir(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
134
134
|
const ttsScript = fs.existsSync(_cwdScript) ? _cwdScript : _homeScript;
|
|
135
|
-
_pickerTtsProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ttsScript, phrase], {
|
|
135
|
+
_pickerTtsProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ttsScript, phrase], { // NOSONAR
|
|
136
136
|
stdio: 'ignore',
|
|
137
137
|
env: _env,
|
|
138
138
|
});
|
|
@@ -141,7 +141,7 @@ export function openPersonalityPicker(screen, currentPersonality, onSelect, onCl
|
|
|
141
141
|
const remoteLlm = detectRemoteLlm();
|
|
142
142
|
const ttsArgs = [ttsScript, phrase];
|
|
143
143
|
if (remoteLlm) ttsArgs.push('--llm', remoteLlm);
|
|
144
|
-
_pickerTtsProc = spawn('bash', ttsArgs, {
|
|
144
|
+
_pickerTtsProc = spawn('bash', ttsArgs, { // NOSONAR
|
|
145
145
|
stdio: 'ignore',
|
|
146
146
|
detached: true,
|
|
147
147
|
env: _env,
|
|
@@ -78,7 +78,7 @@ export function openReverbPicker(screen, currentPreset, onSelect, onClose, opts
|
|
|
78
78
|
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
79
79
|
if (!_isWin) {
|
|
80
80
|
const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
|
|
81
|
-
spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], {
|
|
81
|
+
spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], { // NOSONAR
|
|
82
82
|
stdio: 'ignore',
|
|
83
83
|
timeout: 5000,
|
|
84
84
|
env: { ...process.env },
|