agentvibes 5.7.7 → 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.
Files changed (138) hide show
  1. package/.agentvibes/config.json +12 -5
  2. package/.agentvibes/install-manifest.json +188 -300
  3. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  4. package/.claude/commands/agent-vibes-rdp.md +24 -24
  5. package/.claude/config/audio-effects.cfg +3 -2
  6. package/.claude/config/audio-effects.cfg.sample +52 -52
  7. package/.claude/config/background-music-enabled.txt +1 -0
  8. package/.claude/config/background-music-position.txt +1 -1
  9. package/.claude/config/language.txt +1 -0
  10. package/.claude/docs/TERMUX_SETUP.md +408 -408
  11. package/.claude/github-star-reminder.txt +1 -1
  12. package/.claude/hooks/audio-cache-utils.sh +0 -0
  13. package/.claude/hooks/audio-processor.sh +0 -0
  14. package/.claude/hooks/background-music-manager.sh +0 -0
  15. package/.claude/hooks/bmad-party-speak.sh +0 -0
  16. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  17. package/.claude/hooks/bmad-speak.sh +0 -0
  18. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  19. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  20. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  21. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  22. package/.claude/hooks/clean-audio-cache.sh +0 -0
  23. package/.claude/hooks/cleanup-cache.sh +0 -0
  24. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  25. package/.claude/hooks/download-extra-voices.sh +0 -0
  26. package/.claude/hooks/effects-manager.sh +0 -0
  27. package/.claude/hooks/github-star-reminder.sh +0 -0
  28. package/.claude/hooks/language-manager.sh +0 -0
  29. package/.claude/hooks/learn-manager.sh +0 -0
  30. package/.claude/hooks/macos-voice-manager.sh +0 -0
  31. package/.claude/hooks/migrate-background-music.sh +0 -0
  32. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  33. package/.claude/hooks/optimize-background-music.sh +0 -0
  34. package/.claude/hooks/path-resolver.sh +0 -0
  35. package/.claude/hooks/personality-manager.sh +0 -0
  36. package/.claude/hooks/piper-download-voices.sh +0 -0
  37. package/.claude/hooks/piper-installer.sh +0 -0
  38. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  39. package/.claude/hooks/piper-voice-manager.sh +0 -0
  40. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  41. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  42. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  43. package/.claude/hooks/play-tts-macos.sh +0 -0
  44. package/.claude/hooks/play-tts-piper.sh +0 -0
  45. package/.claude/hooks/play-tts-soprano.sh +0 -0
  46. package/.claude/hooks/play-tts-ssh-remote.sh +2 -2
  47. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  48. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  49. package/.claude/hooks/play-tts.sh +0 -0
  50. package/.claude/hooks/prepare-release.sh +0 -0
  51. package/.claude/hooks/provider-commands.sh +0 -0
  52. package/.claude/hooks/provider-manager.sh +0 -0
  53. package/.claude/hooks/replay-target-audio.sh +0 -0
  54. package/.claude/hooks/requirements.txt +6 -6
  55. package/.claude/hooks/sentiment-manager.sh +0 -0
  56. package/.claude/hooks/session-start-tts.sh +0 -0
  57. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  58. package/.claude/hooks/speed-manager.sh +0 -0
  59. package/.claude/hooks/stop-tts.sh +0 -0
  60. package/.claude/hooks/termux-installer.sh +0 -0
  61. package/.claude/hooks/translate-manager.sh +0 -0
  62. package/.claude/hooks/translator.py +237 -237
  63. package/.claude/hooks/tts-queue-worker.sh +0 -0
  64. package/.claude/hooks/tts-queue.sh +0 -0
  65. package/.claude/hooks/verbosity-manager.sh +0 -0
  66. package/.claude/hooks/voice-manager.sh +0 -0
  67. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  68. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  69. package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
  70. package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
  71. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  72. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  73. package/.claude/piper-voices-dir.txt +1 -1
  74. package/.claude/verbosity.txt +1 -1
  75. package/.clawdbot/README.md +105 -105
  76. package/.mcp.json +5 -14
  77. package/README.md +33 -2
  78. package/RELEASE_NOTES.md +80 -0
  79. package/WINDOWS-SETUP.md +208 -208
  80. package/bin/agent-vibes +39 -39
  81. package/bin/agentvibes-voice-browser.js +0 -0
  82. package/bin/agentvibes.js +0 -0
  83. package/bin/mcp-server.js +121 -121
  84. package/bin/mcp-server.sh +0 -0
  85. package/bin/test-bmad-pr +78 -78
  86. package/mcp-server/QUICK_START.md +203 -203
  87. package/mcp-server/README.md +345 -345
  88. package/mcp-server/WINDOWS_SETUP.md +0 -0
  89. package/mcp-server/examples/claude_desktop_config.json +11 -11
  90. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  91. package/mcp-server/examples/custom_instructions.md +169 -169
  92. package/mcp-server/install-deps.js +0 -0
  93. package/mcp-server/server.py +1797 -1797
  94. package/mcp-server/test_server.py +0 -0
  95. package/package.json +1 -1
  96. package/src/cli/list-personalities.js +110 -110
  97. package/src/cli/list-voices.js +114 -114
  98. package/src/commands/bmad-voices.js +394 -394
  99. package/src/commands/install-mcp.js +476 -476
  100. package/src/console/audio-env.js +4 -1
  101. package/src/console/brand-colors.js +13 -13
  102. package/src/console/constants/personalities.js +44 -44
  103. package/src/console/tabs/agents-tab.js +84 -61
  104. package/src/console/tabs/help-tab.js +314 -314
  105. package/src/console/tabs/music-tab.js +3 -2
  106. package/src/console/tabs/readme-tab.js +272 -272
  107. package/src/console/tabs/setup-tab.js +285 -41
  108. package/src/console/tabs/voices-tab.js +13 -1
  109. package/src/console/widgets/destroy-list.js +25 -25
  110. package/src/console/widgets/notice.js +55 -55
  111. package/src/i18n/de.js +202 -202
  112. package/src/i18n/es.js +202 -202
  113. package/src/i18n/fr.js +202 -202
  114. package/src/i18n/hi.js +202 -202
  115. package/src/i18n/ja.js +202 -202
  116. package/src/i18n/ko.js +202 -202
  117. package/src/i18n/pt.js +202 -202
  118. package/src/i18n/strings.js +54 -54
  119. package/src/i18n/zh-CN.js +202 -202
  120. package/src/installer/language-screen.js +31 -31
  121. package/src/installer/music-file-input.js +304 -304
  122. package/src/installer.js +0 -0
  123. package/src/services/config-service.js +264 -264
  124. package/src/services/language-service.js +47 -47
  125. package/src/services/provider-service.js +143 -143
  126. package/src/utils/audio-duration-validator.js +298 -298
  127. package/src/utils/audio-format-validator.js +277 -277
  128. package/src/utils/dependency-checker.js +469 -469
  129. package/src/utils/file-ownership-verifier.js +358 -358
  130. package/src/utils/list-formatter.js +194 -194
  131. package/src/utils/music-file-validator.js +285 -285
  132. package/src/utils/preview-list-prompt.js +136 -136
  133. package/src/utils/secure-music-storage.js +412 -412
  134. package/templates/agentvibes-receiver.sh +231 -231
  135. package/templates/audio/welcome-music.mp3 +0 -0
  136. package/.claude/hooks/bmad-party-manager.sh +0 -225
  137. package/.claude/hooks/stop.sh +0 -38
  138. /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', [], {
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', () => { _previewModalProc = null; if (!_closed) { previewLine.setContent(''); screen.render(); } });
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
- let cmd, args;
1832
- if (isWin) {
1833
- const script = path.join(_hooksBase, '.claude', hooksSubdir, 'play-tts.ps1');
1834
- cmd = 'powershell';
1835
- args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText, '', '-llm', llmKey];
1836
- } else {
1837
- const script = path.join(_hooksBase, '.claude', hooksSubdir, 'play-tts.sh');
1838
- cmd = 'bash';
1839
- args = [script, sampleText, '', '--llm', llmKey];
1840
- }
1841
-
1842
- const proc = spawn(cmd, args, {
1843
- stdio: 'ignore',
1844
- windowsHide: true,
1845
- env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir, AGENTVIBES_LLM_KEY: `llm:${llmKey}` },
1846
- });
1847
- _previewModalProc = proc;
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
- proc.on('exit', () => {
1850
- _previewModalProc = null;
1851
- if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
1852
- if (!_closed) { previewLine.setContent(''); screen.render(); }
1853
- });
1854
- proc.on('error', () => {
1855
- _previewModalProc = null;
1856
- if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
1857
- if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); }
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
- // Infer engine from voice voice picker only shows Piper voices,
1864
- // so if a voice is set but no engine chosen, default to piper
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
- if (idx === 0) {
2091
- draft.ttsEngine = '';
2092
- } else {
2093
- draft.ttsEngine = engines[idx - 1].id;
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: 'black', bold: true },
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');
@@ -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
+ }