agentvibes 5.7.6 → 5.9.0
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 +12 -5
- package/.agentvibes/install-manifest.json +188 -300
- package/.claude/audio/tracks/celestial_velvet.mp3 +0 -0
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/config/language.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +0 -0
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-speak.sh +27 -6
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +0 -0
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
- package/.claude/hooks/clawdbot-receiver.sh +0 -0
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/path-resolver.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +0 -0
- package/.claude/hooks/piper-installer.sh +0 -0
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +0 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
- package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-macos.sh +0 -0
- package/.claude/hooks/play-tts-piper.sh +0 -0
- package/.claude/hooks/play-tts-soprano.sh +0 -0
- package/.claude/hooks/play-tts-ssh-remote.sh +11 -8
- package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
- package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
- package/.claude/hooks/play-tts.sh +0 -0
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +0 -0
- package/.claude/hooks/provider-manager.sh +0 -0
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +0 -0
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop-tts.sh +0 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +0 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
- package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/piper-voices-dir.txt +1 -1
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +5 -14
- package/README.md +43 -2
- package/RELEASE_NOTES.md +110 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +0 -0
- package/bin/agentvibes.js +0 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +0 -0
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +0 -0
- package/mcp-server/server.py +1797 -1797
- package/mcp-server/test_server.py +0 -0
- package/package.json +1 -1
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/audio-env.js +4 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/tabs/agents-tab.js +85 -62
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +3 -0
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/setup-tab.js +285 -41
- package/src/console/tabs/voices-tab.js +14 -2
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/i18n/de.js +202 -202
- package/src/i18n/es.js +202 -202
- package/src/i18n/fr.js +202 -202
- package/src/i18n/hi.js +202 -202
- package/src/i18n/ja.js +202 -202
- package/src/i18n/ko.js +202 -202
- package/src/i18n/pt.js +202 -202
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +202 -202
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +0 -0
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/provider-service.js +143 -143
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +231 -231
- package/templates/audio/welcome-music.mp3 +0 -0
- package/.claude/hooks/bmad-party-manager.sh +0 -225
- package/.claude/hooks/stop.sh +0 -38
|
@@ -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', [], {
|
|
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
|
|
|
@@ -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' });
|
|
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 });
|
|
2368
|
+
} else if (engine === 'macos-say') {
|
|
2369
|
+
proc = spawn('say', [phrase], { stdio: 'ignore' });
|
|
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
|
});
|
|
@@ -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');
|
|
@@ -770,7 +782,7 @@ export function createVoicesTab(screen, services) {
|
|
|
770
782
|
// Inline selection hint appended to the currently highlighted voice row.
|
|
771
783
|
// _hintBase stores the item's clean content (no hint, no █) — no sentinel needed.
|
|
772
784
|
// Use getter functions so hints re-translate when language changes.
|
|
773
|
-
const _rowHintInstalled = () => ` {
|
|
785
|
+
const _rowHintInstalled = () => ` {white-fg}${_tl('voicesRowHintInstalled')}{/white-fg}`;
|
|
774
786
|
const _rowHintUninstalled = () => ` {bright-yellow-fg}${_tl('voicesRowHintUninstalled')}{/bright-yellow-fg}`;
|
|
775
787
|
let _hintIdx = -1;
|
|
776
788
|
let _hintBase = ''; // content of items[_hintIdx] before hint was appended
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
-
*
|
|
4
|
-
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
-
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
-
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} widget - blessed widget to destroy
|
|
13
|
-
* @param {object} screen - blessed screen instance
|
|
14
|
-
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
-
*/
|
|
16
|
-
export function destroyList(widget, screen, onClose) {
|
|
17
|
-
widget.destroy();
|
|
18
|
-
try {
|
|
19
|
-
for (let r = 0; r < screen.height; r++)
|
|
20
|
-
for (let c = 0; c < screen.width; c++)
|
|
21
|
-
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
-
} catch {}
|
|
23
|
-
onClose?.();
|
|
24
|
-
screen.render();
|
|
25
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
+
*
|
|
4
|
+
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
+
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
+
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} widget - blessed widget to destroy
|
|
13
|
+
* @param {object} screen - blessed screen instance
|
|
14
|
+
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
+
*/
|
|
16
|
+
export function destroyList(widget, screen, onClose) {
|
|
17
|
+
widget.destroy();
|
|
18
|
+
try {
|
|
19
|
+
for (let r = 0; r < screen.height; r++)
|
|
20
|
+
for (let c = 0; c < screen.width; c++)
|
|
21
|
+
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
+
} catch {}
|
|
23
|
+
onClose?.();
|
|
24
|
+
screen.render();
|
|
25
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
-
*
|
|
4
|
-
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
-
* Usable from any tab; no settings-specific state required.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { destroyList } from './destroy-list.js';
|
|
9
|
-
|
|
10
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
-
let blessed;
|
|
12
|
-
if (!IS_TEST) {
|
|
13
|
-
const { default: b } = await import('blessed');
|
|
14
|
-
blessed = b;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} screen - blessed screen instance
|
|
21
|
-
* @param {string} message - text to display
|
|
22
|
-
* @param {object} [opts]
|
|
23
|
-
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
-
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
-
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
-
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
-
*/
|
|
28
|
-
export function showNotice(screen, message, opts = {}) {
|
|
29
|
-
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
-
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
-
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
-
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
-
|
|
34
|
-
const width = Math.max(28, message.length + 6);
|
|
35
|
-
const modal = blessed.box({
|
|
36
|
-
parent: screen,
|
|
37
|
-
top: 'center',
|
|
38
|
-
left: 'center',
|
|
39
|
-
width,
|
|
40
|
-
height: 3,
|
|
41
|
-
border: { type: 'line' },
|
|
42
|
-
tags: true,
|
|
43
|
-
content: `{center}${message}{/center}`,
|
|
44
|
-
style: {
|
|
45
|
-
fg,
|
|
46
|
-
bg,
|
|
47
|
-
border: { fg: borderFg },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
screen.render();
|
|
51
|
-
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
destroyList(modal, screen);
|
|
54
|
-
}, durationMs);
|
|
55
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
+
*
|
|
4
|
+
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
+
* Usable from any tab; no settings-specific state required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { destroyList } from './destroy-list.js';
|
|
9
|
+
|
|
10
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
+
let blessed;
|
|
12
|
+
if (!IS_TEST) {
|
|
13
|
+
const { default: b } = await import('blessed');
|
|
14
|
+
blessed = b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} screen - blessed screen instance
|
|
21
|
+
* @param {string} message - text to display
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
+
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
+
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
+
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
+
*/
|
|
28
|
+
export function showNotice(screen, message, opts = {}) {
|
|
29
|
+
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
+
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
+
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
+
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
+
|
|
34
|
+
const width = Math.max(28, message.length + 6);
|
|
35
|
+
const modal = blessed.box({
|
|
36
|
+
parent: screen,
|
|
37
|
+
top: 'center',
|
|
38
|
+
left: 'center',
|
|
39
|
+
width,
|
|
40
|
+
height: 3,
|
|
41
|
+
border: { type: 'line' },
|
|
42
|
+
tags: true,
|
|
43
|
+
content: `{center}${message}{/center}`,
|
|
44
|
+
style: {
|
|
45
|
+
fg,
|
|
46
|
+
bg,
|
|
47
|
+
border: { fg: borderFg },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
screen.render();
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
destroyList(modal, screen);
|
|
54
|
+
}, durationMs);
|
|
55
|
+
}
|