agentvibes 4.0.1 → 4.4.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/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -389
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -112
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -70
- package/.claude/hooks/play-tts-macos.sh +368 -345
- package/.claude/hooks/play-tts-piper.sh +679 -578
- package/.claude/hooks/play-tts-soprano.sh +356 -320
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -298
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -71
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -114
- package/.claude/hooks/tts-queue.sh +165 -136
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -544
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -181
- package/README.md +2029 -1909
- package/RELEASE_NOTES.md +1310 -66
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1826
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- 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 +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- 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 +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1417
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -112
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- 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/app.js +824 -806
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +50 -46
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -61
- package/src/console/tabs/agents-tab.js +1684 -369
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -46
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -0
- package/src/console/tabs/settings-tab.js +185 -402
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5882 -5777
- package/src/services/agent-voice-store.js +423 -163
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +132 -132
- package/src/services/verbosity-service.js +157 -157
- 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 -466
- 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 -275
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -162
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -21,8 +21,33 @@ import {
|
|
|
21
21
|
import { formatTrackLabel, scanTracks, getMusicFavorites, toggleMusicFavorite, applyTrackToAudioEffects } from './music-tab.js';
|
|
22
22
|
import { BRAND_PINK, BRAND_BLUE } from '../brand-colors.js';
|
|
23
23
|
import { buildAudioEnv, detectMp3Player, detectWavPlayer } from '../audio-env.js';
|
|
24
|
+
import { destroyList } from '../widgets/destroy-list.js';
|
|
25
|
+
import { openReverbPicker } from '../widgets/reverb-picker.js';
|
|
26
|
+
import { openPersonalityPicker } from '../widgets/personality-picker.js';
|
|
27
|
+
import { PERSONALITY_EMOJIS } from '../constants/personalities.js';
|
|
28
|
+
import { formatTrackName as _sharedFormatTrackName, formatReverbState as _sharedFormatReverbState } from '../widgets/format-utils.js';
|
|
29
|
+
import { showNotice as _showNoticeWidget } from '../widgets/notice.js';
|
|
24
30
|
|
|
25
31
|
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
32
|
+
const _IS_WINDOWS = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
33
|
+
|
|
34
|
+
/** Resolve piper binary path — uses exe on Windows, 'piper' in PATH on Unix */
|
|
35
|
+
function _resolvePiperBin() {
|
|
36
|
+
if (_IS_WINDOWS) {
|
|
37
|
+
const lad = process.env.LOCALAPPDATA ||
|
|
38
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
39
|
+
if (lad) {
|
|
40
|
+
const exe = path.join(lad, 'Programs', 'Piper', 'piper.exe');
|
|
41
|
+
if (fs.existsSync(exe)) return exe;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 'piper';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Build spawn options with Windows-safe defaults (no visible console, no detached) */
|
|
48
|
+
function _spawnOpts(env, extraOpts = {}) {
|
|
49
|
+
return { stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env, ...extraOpts };
|
|
50
|
+
}
|
|
26
51
|
|
|
27
52
|
// Sanitize strings before passing as env vars to shell commands.
|
|
28
53
|
// Removes characters that could cause shell injection when expanded inside sh -c.
|
|
@@ -45,19 +70,19 @@ const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
|
45
70
|
|
|
46
71
|
const COLORS = {
|
|
47
72
|
contentBg: '#0a0e1a', // Near-black content background
|
|
48
|
-
sectionHdr: '
|
|
73
|
+
sectionHdr: 'bright-cyan', // Matches header "Agent" color
|
|
49
74
|
labelFg: '#e3f2fd', // Light blue text — labels
|
|
50
75
|
valueFg: '#ffff00', // Yellow — current values
|
|
51
76
|
btnDefault: '#37474f', // Dark slate — default button bg
|
|
52
|
-
btnFocus: '#
|
|
53
|
-
btnFocusFg: '#
|
|
77
|
+
btnFocus: '#2e7d32', // Green — focused/selected button bg
|
|
78
|
+
btnFocusFg: '#ffffff', // White — focused button text
|
|
54
79
|
btnPress: '#ff00ff', // Magenta — pressed button bg
|
|
55
80
|
btnChange: '#37474f', // Dark slate — Change buttons
|
|
56
81
|
btnTest: '#37474f', // Dark slate — Test buttons
|
|
57
82
|
btnEdit: '#37474f', // Dark slate — Edit buttons
|
|
58
83
|
btnEnableOn: '#37474f', // Dark slate — Enabled toggle
|
|
59
84
|
btnEnableOff: '#37474f', // Dark slate — Disabled toggle
|
|
60
|
-
borderFg: '
|
|
85
|
+
borderFg: 'bright-cyan', // Matches section headers
|
|
61
86
|
footerBg: '#2196f3', // Blue — settings footer
|
|
62
87
|
noticeFg: '#90a4ae', // Gray — stub notice text
|
|
63
88
|
};
|
|
@@ -74,82 +99,11 @@ const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_f
|
|
|
74
99
|
// Verbosity display labels
|
|
75
100
|
const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
|
|
76
101
|
|
|
77
|
-
// Personality emojis
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
dramatic: '🎭',
|
|
83
|
-
'dry-humor': '😐',
|
|
84
|
-
flirty: '😘',
|
|
85
|
-
funny: '😂',
|
|
86
|
-
grandpa: '👴',
|
|
87
|
-
millennial: '🙄',
|
|
88
|
-
moody: '😒',
|
|
89
|
-
none: '😊',
|
|
90
|
-
normal: '😊',
|
|
91
|
-
pirate: '⚓',
|
|
92
|
-
poetic: '📜',
|
|
93
|
-
professional: '👔',
|
|
94
|
-
rapper: '🎤',
|
|
95
|
-
robot: '🤖',
|
|
96
|
-
sarcastic: '😏',
|
|
97
|
-
sassy: '💁',
|
|
98
|
-
'surfer-dude':'🏄',
|
|
99
|
-
zen: '🧘',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Known personalities (matches .claude/personalities/ directory)
|
|
103
|
-
const PERSONALITIES = Object.freeze([
|
|
104
|
-
'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
|
|
105
|
-
'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
|
|
106
|
-
'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
|
|
107
|
-
'sassy', 'surfer-dude', 'zen',
|
|
108
|
-
]);
|
|
109
|
-
|
|
110
|
-
// Preview phrases — one short, exemplary, in-character line per personality.
|
|
111
|
-
// Spoken automatically when the cursor lands on a personality in the picker.
|
|
112
|
-
const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
|
|
113
|
-
angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
|
|
114
|
-
annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
|
|
115
|
-
crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
|
|
116
|
-
dramatic: "The tests... have failed. I don't know how much longer I can do this.",
|
|
117
|
-
'dry-humor': "Your code worked. I too am surprised.",
|
|
118
|
-
flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
|
|
119
|
-
funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
|
|
120
|
-
grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
|
|
121
|
-
millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
|
|
122
|
-
moody: "...It works. Whatever. Do not get used to it.",
|
|
123
|
-
pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
|
|
124
|
-
poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
|
|
125
|
-
professional: "I have completed the requested task and am prepared to document outcomes.",
|
|
126
|
-
rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
|
|
127
|
-
robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
|
|
128
|
-
sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
|
|
129
|
-
sassy: "Honey, whoever told you that was good code was not your friend.",
|
|
130
|
-
'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
|
|
131
|
-
zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
|
|
132
|
-
random: "Who will I be today? Even I do not know. Expect the unexpected.",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Human-readable track display names — matches installer track picker (src/installer.js:2280)
|
|
136
|
-
const TRACK_NAMES = Object.freeze({
|
|
137
|
-
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
138
|
-
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
139
|
-
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
140
|
-
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
141
|
-
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
142
|
-
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
143
|
-
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
144
|
-
'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
|
|
145
|
-
'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
|
|
146
|
-
'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
|
|
147
|
-
'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
|
|
148
|
-
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
|
|
149
|
-
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
150
|
-
'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
|
|
151
|
-
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
152
|
-
});
|
|
102
|
+
// Personality emojis and names imported from src/console/constants/personalities.js
|
|
103
|
+
// (via the import at the top of this file)
|
|
104
|
+
|
|
105
|
+
// Human-readable track display names — moved to shared widgets/format-utils.js
|
|
106
|
+
// TRACK_NAMES constant removed (M1 dedup). Use formatTrackName() instead.
|
|
153
107
|
|
|
154
108
|
// Built-in track list for the picker (fallback when tracks dir is missing)
|
|
155
109
|
const BUILT_IN_TRACKS = [
|
|
@@ -166,10 +120,7 @@ const BUILT_IN_TRACKS = [
|
|
|
166
120
|
* @param {string} preset - 'off' | 'light' | 'medium' | 'heavy' | 'cathedral'
|
|
167
121
|
* @returns {string}
|
|
168
122
|
*/
|
|
169
|
-
export
|
|
170
|
-
const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
|
|
171
|
-
return LABELS[preset] ?? LABELS.light;
|
|
172
|
-
}
|
|
123
|
+
export const formatReverbState = _sharedFormatReverbState;
|
|
173
124
|
|
|
174
125
|
/**
|
|
175
126
|
* @param {boolean} enabled
|
|
@@ -192,18 +143,7 @@ export function formatVolume(volume) {
|
|
|
192
143
|
* @param {string} track - filename (e.g. 'agentvibes_soft_flamenco_loop.mp3')
|
|
193
144
|
* @returns {string}
|
|
194
145
|
*/
|
|
195
|
-
export
|
|
196
|
-
if (!track) return 'None';
|
|
197
|
-
if (TRACK_NAMES[track]) return TRACK_NAMES[track];
|
|
198
|
-
// Custom/unknown track: strip extension, agentvibes_/agent_vibes_ prefix,
|
|
199
|
-
// _v1/_v2/_loop/_v1_loop/_v2_loop suffixes, then title-case each word
|
|
200
|
-
return track
|
|
201
|
-
.replace(/\.[^.]+$/, '')
|
|
202
|
-
.replace(/^agentvibes_|^agent_vibes_/, '')
|
|
203
|
-
.replace(/_v\d+_loop$|_loop$|_v\d+$/, '')
|
|
204
|
-
.replace(/_/g, ' ')
|
|
205
|
-
.replace(/\b\w/g, c => c.toUpperCase());
|
|
206
|
-
}
|
|
146
|
+
export const formatTrackName = _sharedFormatTrackName;
|
|
207
147
|
|
|
208
148
|
/**
|
|
209
149
|
* @param {string} verbosity - 'high' | 'medium' | 'low'
|
|
@@ -492,9 +432,14 @@ export function createSettingsTab(screen, services) {
|
|
|
492
432
|
`play "${trackPath}" repeat 9999 vol ${volFraction}`,
|
|
493
433
|
`mpg123 -q --loop -1 "${trackPath}"`,
|
|
494
434
|
].join(' 2>/dev/null || ') + ' 2>/dev/null';
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
435
|
+
if (_IS_WINDOWS) {
|
|
436
|
+
const _mp3P = detectMp3Player(_testEnv);
|
|
437
|
+
_testMusicProc = _mp3P
|
|
438
|
+
? spawn(_mp3P.bin, _mp3P.args(trackPath), _spawnOpts(_testEnv))
|
|
439
|
+
: null;
|
|
440
|
+
} else {
|
|
441
|
+
_testMusicProc = spawn('sh', ['-c', musicCmd], _spawnOpts(_testEnv));
|
|
442
|
+
}
|
|
498
443
|
_testMusicProc.unref();
|
|
499
444
|
}
|
|
500
445
|
}
|
|
@@ -542,7 +487,7 @@ export function createSettingsTab(screen, services) {
|
|
|
542
487
|
`soprano "$_AV_PHRASE" -o "$_AV_WAV"`,
|
|
543
488
|
].join(' || ');
|
|
544
489
|
synthProc = spawn('sh', ['-c', cmd], {
|
|
545
|
-
stdio: 'ignore', detached: true, env: sopranoEnv,
|
|
490
|
+
stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env: sopranoEnv,
|
|
546
491
|
});
|
|
547
492
|
} else {
|
|
548
493
|
const voiceId = providerService.getActiveVoiceId();
|
|
@@ -555,8 +500,8 @@ export function createSettingsTab(screen, services) {
|
|
|
555
500
|
}
|
|
556
501
|
const _piperArgs = ['--model', voicePath, '--output_file', tempWav];
|
|
557
502
|
if (_ms.speakerId != null) _piperArgs.push('--speaker', String(_ms.speakerId));
|
|
558
|
-
synthProc = spawn(
|
|
559
|
-
stdio: ['pipe', 'ignore', 'ignore'], detached: true, env: _testEnv,
|
|
503
|
+
synthProc = spawn(_resolvePiperBin(), _piperArgs, {
|
|
504
|
+
stdio: ['pipe', 'ignore', 'ignore'], detached: !_IS_WINDOWS, windowsHide: true, env: _testEnv,
|
|
560
505
|
});
|
|
561
506
|
synthProc.stdin.write(ttsInput + '\n');
|
|
562
507
|
synthProc.stdin.end();
|
|
@@ -607,7 +552,7 @@ export function createSettingsTab(screen, services) {
|
|
|
607
552
|
_setTestBtnsLabel('■ Stop');
|
|
608
553
|
const _wavPlayer1 = detectWavPlayer(_testEnv);
|
|
609
554
|
const playProc = _wavPlayer1
|
|
610
|
-
? spawn(_wavPlayer1.bin, _wavPlayer1.args(wavToPlay),
|
|
555
|
+
? spawn(_wavPlayer1.bin, _wavPlayer1.args(wavToPlay), _spawnOpts(_testEnv))
|
|
611
556
|
: null;
|
|
612
557
|
if (!playProc) { _killTest(); _restoreTestBtnsLabels(); return; }
|
|
613
558
|
_testVoiceProc = playProc;
|
|
@@ -712,9 +657,14 @@ export function createSettingsTab(screen, services) {
|
|
|
712
657
|
`mpg123 -q "${trackPath}"`,
|
|
713
658
|
].join(' 2>/dev/null || ') + ' 2>/dev/null';
|
|
714
659
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
660
|
+
if (_IS_WINDOWS) {
|
|
661
|
+
const _mp3P2 = detectMp3Player(_testEnv);
|
|
662
|
+
_musicTestProc = _mp3P2
|
|
663
|
+
? spawn(_mp3P2.bin, _mp3P2.args(trackPath), _spawnOpts(_testEnv))
|
|
664
|
+
: null;
|
|
665
|
+
} else {
|
|
666
|
+
_musicTestProc = spawn('sh', ['-c', cmd], _spawnOpts(_testEnv));
|
|
667
|
+
}
|
|
718
668
|
_musicTestProc.unref();
|
|
719
669
|
musicTestBtn.setContent('■ Stop');
|
|
720
670
|
screen.render();
|
|
@@ -807,7 +757,7 @@ export function createSettingsTab(screen, services) {
|
|
|
807
757
|
content: lbl, width: lbl.length, height: 1,
|
|
808
758
|
top: 0, left: _xOff,
|
|
809
759
|
keys: true, focusable: true,
|
|
810
|
-
style: { fg: '
|
|
760
|
+
style: { fg: 'bright-cyan', bg: '#263238' },
|
|
811
761
|
});
|
|
812
762
|
_subTabItemsMap[id] = item;
|
|
813
763
|
_xOff += lbl.length;
|
|
@@ -821,7 +771,7 @@ export function createSettingsTab(screen, services) {
|
|
|
821
771
|
item.style.bg = '#0288d1'; // light blue — active tab
|
|
822
772
|
item.style.bold = true;
|
|
823
773
|
} else {
|
|
824
|
-
item.style.fg = '
|
|
774
|
+
item.style.fg = 'bright-cyan';
|
|
825
775
|
item.style.bg = '#263238';
|
|
826
776
|
item.style.bold = false;
|
|
827
777
|
}
|
|
@@ -855,12 +805,24 @@ export function createSettingsTab(screen, services) {
|
|
|
855
805
|
});
|
|
856
806
|
}
|
|
857
807
|
|
|
808
|
+
// -------------------------------------------------------------------------
|
|
809
|
+
// Section header: ── Provider & Voice ──
|
|
810
|
+
|
|
811
|
+
const providerVoiceHeader = blessed.text({
|
|
812
|
+
parent: box,
|
|
813
|
+
top: 3,
|
|
814
|
+
left: 1,
|
|
815
|
+
content: '{bright-cyan-fg} 🎤 Provider & Voice {/bright-cyan-fg}',
|
|
816
|
+
tags: true,
|
|
817
|
+
style: { bg: COLORS.contentBg },
|
|
818
|
+
});
|
|
819
|
+
|
|
858
820
|
// -------------------------------------------------------------------------
|
|
859
821
|
// Provider row: label + value + [Switch] button
|
|
860
822
|
|
|
861
823
|
const providerLabel = blessed.text({
|
|
862
824
|
parent: box,
|
|
863
|
-
top:
|
|
825
|
+
top: 5,
|
|
864
826
|
left: 6,
|
|
865
827
|
content: 'Provider:',
|
|
866
828
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -868,7 +830,7 @@ export function createSettingsTab(screen, services) {
|
|
|
868
830
|
|
|
869
831
|
const providerValue = blessed.text({
|
|
870
832
|
parent: box,
|
|
871
|
-
top:
|
|
833
|
+
top: 5,
|
|
872
834
|
left: 22,
|
|
873
835
|
width: 26, // truncate before [Switch] at left:40
|
|
874
836
|
wrap: false,
|
|
@@ -884,7 +846,7 @@ export function createSettingsTab(screen, services) {
|
|
|
884
846
|
screen.render();
|
|
885
847
|
}, _restoreFocus);
|
|
886
848
|
});
|
|
887
|
-
switchBtn.top =
|
|
849
|
+
switchBtn.top = 5;
|
|
888
850
|
switchBtn.left = 52;
|
|
889
851
|
|
|
890
852
|
// -------------------------------------------------------------------------
|
|
@@ -892,7 +854,7 @@ export function createSettingsTab(screen, services) {
|
|
|
892
854
|
|
|
893
855
|
const voiceLabel = blessed.text({
|
|
894
856
|
parent: box,
|
|
895
|
-
top:
|
|
857
|
+
top: 7,
|
|
896
858
|
left: 6,
|
|
897
859
|
content: 'Current Voice:',
|
|
898
860
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -900,7 +862,7 @@ export function createSettingsTab(screen, services) {
|
|
|
900
862
|
|
|
901
863
|
const voiceValue = blessed.text({
|
|
902
864
|
parent: box,
|
|
903
|
-
top:
|
|
865
|
+
top: 7,
|
|
904
866
|
left: 22,
|
|
905
867
|
width: 26, // truncate before [Change] at left:40
|
|
906
868
|
wrap: false,
|
|
@@ -916,7 +878,7 @@ export function createSettingsTab(screen, services) {
|
|
|
916
878
|
screen.render();
|
|
917
879
|
}, _restoreFocus);
|
|
918
880
|
}, { bg: COLORS.btnChange });
|
|
919
|
-
changeBtn.top =
|
|
881
|
+
changeBtn.top = 7;
|
|
920
882
|
changeBtn.left = 52;
|
|
921
883
|
|
|
922
884
|
const playBtn = _createButton(box, screen, '▶ Play', COLORS, () => {
|
|
@@ -951,7 +913,7 @@ export function createSettingsTab(screen, services) {
|
|
|
951
913
|
screen.render();
|
|
952
914
|
const _wavPlayer2 = detectWavPlayer(_sampleEnv);
|
|
953
915
|
if (!_wavPlayer2) { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); screen.render(); return; }
|
|
954
|
-
const playProc = spawn(_wavPlayer2.bin, _wavPlayer2.args(tempWav),
|
|
916
|
+
const playProc = spawn(_wavPlayer2.bin, _wavPlayer2.args(tempWav), _spawnOpts(_sampleEnv));
|
|
955
917
|
_sampleProcess = playProc;
|
|
956
918
|
const _done = () => { _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); try { fs.unlinkSync(tempWav); } catch {} };
|
|
957
919
|
playProc.on('exit', _done);
|
|
@@ -1052,7 +1014,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1052
1014
|
].join(' || ');
|
|
1053
1015
|
|
|
1054
1016
|
const soprano = spawn('sh', ['-c', cmd], {
|
|
1055
|
-
stdio: 'ignore', detached: true, env: sopranoEnv,
|
|
1017
|
+
stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env: sopranoEnv,
|
|
1056
1018
|
});
|
|
1057
1019
|
_sampleProcess = soprano;
|
|
1058
1020
|
soprano.on('exit', (code) => {
|
|
@@ -1074,8 +1036,8 @@ export function createSettingsTab(screen, services) {
|
|
|
1074
1036
|
}
|
|
1075
1037
|
const _piperArgs2 = ['--model', voicePath, '--output_file', tempWav];
|
|
1076
1038
|
if (_ms2.speakerId != null) _piperArgs2.push('--speaker', String(_ms2.speakerId));
|
|
1077
|
-
const piper = spawn(
|
|
1078
|
-
stdio: ['pipe', 'ignore', 'ignore'], detached: true, env: _sampleEnv,
|
|
1039
|
+
const piper = spawn(_resolvePiperBin(), _piperArgs2, {
|
|
1040
|
+
stdio: ['pipe', 'ignore', 'ignore'], detached: !_IS_WINDOWS, windowsHide: true, env: _sampleEnv,
|
|
1079
1041
|
});
|
|
1080
1042
|
piper.stdin.write(phrase + '\n');
|
|
1081
1043
|
piper.stdin.end();
|
|
@@ -1084,12 +1046,12 @@ export function createSettingsTab(screen, services) {
|
|
|
1084
1046
|
piper.on('error', () => { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); });
|
|
1085
1047
|
}
|
|
1086
1048
|
});
|
|
1087
|
-
playBtn.top =
|
|
1049
|
+
playBtn.top = 7;
|
|
1088
1050
|
playBtn.left = 64;
|
|
1089
1051
|
|
|
1090
1052
|
const voiceFileText = blessed.text({
|
|
1091
1053
|
parent: box,
|
|
1092
|
-
top:
|
|
1054
|
+
top: 8,
|
|
1093
1055
|
left: 22,
|
|
1094
1056
|
right: 2,
|
|
1095
1057
|
wrap: false,
|
|
@@ -1104,7 +1066,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1104
1066
|
parent: box,
|
|
1105
1067
|
top: 3,
|
|
1106
1068
|
left: 1,
|
|
1107
|
-
content: '{
|
|
1069
|
+
content: '{bright-cyan-fg} ⚡ Audio Effects {/bright-cyan-fg}',
|
|
1108
1070
|
tags: true,
|
|
1109
1071
|
style: { bg: COLORS.contentBg },
|
|
1110
1072
|
});
|
|
@@ -1131,7 +1093,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1131
1093
|
});
|
|
1132
1094
|
|
|
1133
1095
|
const reverbChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
|
|
1134
|
-
|
|
1096
|
+
openReverbPicker(screen, configService.getConfig().effects?.reverbPreset ?? 'light', (preset) => {
|
|
1135
1097
|
_setEffects(configService, { reverbPreset: preset });
|
|
1136
1098
|
refreshDisplay();
|
|
1137
1099
|
}, _restoreFocus);
|
|
@@ -1160,7 +1122,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1160
1122
|
parent: box,
|
|
1161
1123
|
top: 7,
|
|
1162
1124
|
left: 1,
|
|
1163
|
-
content: '{
|
|
1125
|
+
content: '{bright-cyan-fg} 🎸 Background Music {/bright-cyan-fg}',
|
|
1164
1126
|
tags: true,
|
|
1165
1127
|
style: { bg: COLORS.contentBg },
|
|
1166
1128
|
});
|
|
@@ -1256,12 +1218,24 @@ export function createSettingsTab(screen, services) {
|
|
|
1256
1218
|
volumeChangeBtn.top = 11;
|
|
1257
1219
|
volumeChangeBtn.left = 52;
|
|
1258
1220
|
|
|
1221
|
+
// -------------------------------------------------------------------------
|
|
1222
|
+
// Section header: ── Style ──
|
|
1223
|
+
|
|
1224
|
+
const styleHeader = blessed.text({
|
|
1225
|
+
parent: box,
|
|
1226
|
+
top: 3,
|
|
1227
|
+
left: 1,
|
|
1228
|
+
content: '{bright-cyan-fg} 🎭 Style {/bright-cyan-fg}',
|
|
1229
|
+
tags: true,
|
|
1230
|
+
style: { bg: COLORS.contentBg },
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1259
1233
|
// -------------------------------------------------------------------------
|
|
1260
1234
|
// Verbosity row: label + value + [Change] button
|
|
1261
1235
|
|
|
1262
1236
|
const verbosityLabel = blessed.text({
|
|
1263
1237
|
parent: box,
|
|
1264
|
-
top:
|
|
1238
|
+
top: 5,
|
|
1265
1239
|
left: 6,
|
|
1266
1240
|
content: 'Verbosity:',
|
|
1267
1241
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -1269,7 +1243,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1269
1243
|
|
|
1270
1244
|
const verbosityValue = blessed.text({
|
|
1271
1245
|
parent: box,
|
|
1272
|
-
top:
|
|
1246
|
+
top: 5,
|
|
1273
1247
|
left: 22,
|
|
1274
1248
|
width: 26, // truncate before [Change] at left:40
|
|
1275
1249
|
wrap: false,
|
|
@@ -1280,12 +1254,12 @@ export function createSettingsTab(screen, services) {
|
|
|
1280
1254
|
const verbosityChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
|
|
1281
1255
|
_openVerbosityPicker(screen, configService, () => refreshDisplay(), _restoreFocus);
|
|
1282
1256
|
}, { bg: COLORS.btnChange });
|
|
1283
|
-
verbosityChangeBtn.top =
|
|
1257
|
+
verbosityChangeBtn.top = 5;
|
|
1284
1258
|
verbosityChangeBtn.left = 52;
|
|
1285
1259
|
|
|
1286
1260
|
const verbosityPathText = blessed.text({
|
|
1287
1261
|
parent: box,
|
|
1288
|
-
top:
|
|
1262
|
+
top: 6,
|
|
1289
1263
|
left: 22,
|
|
1290
1264
|
right: 2,
|
|
1291
1265
|
wrap: false,
|
|
@@ -1298,7 +1272,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1298
1272
|
|
|
1299
1273
|
const personalityLabel = blessed.text({
|
|
1300
1274
|
parent: box,
|
|
1301
|
-
top:
|
|
1275
|
+
top: 7,
|
|
1302
1276
|
left: 6,
|
|
1303
1277
|
content: 'Personality:',
|
|
1304
1278
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -1306,7 +1280,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1306
1280
|
|
|
1307
1281
|
const personalityValue = blessed.text({
|
|
1308
1282
|
parent: box,
|
|
1309
|
-
top:
|
|
1283
|
+
top: 7,
|
|
1310
1284
|
left: 22,
|
|
1311
1285
|
width: 26, // truncate before [Change] at left:40
|
|
1312
1286
|
wrap: false,
|
|
@@ -1315,12 +1289,12 @@ export function createSettingsTab(screen, services) {
|
|
|
1315
1289
|
});
|
|
1316
1290
|
|
|
1317
1291
|
const personalityChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
|
|
1318
|
-
|
|
1292
|
+
openPersonalityPicker(screen, configService.getConfig().personality ?? 'none', (name) => {
|
|
1319
1293
|
configService.set('personality', name);
|
|
1320
1294
|
refreshDisplay();
|
|
1321
1295
|
}, _restoreFocus);
|
|
1322
1296
|
}, { bg: COLORS.btnChange });
|
|
1323
|
-
personalityChangeBtn.top =
|
|
1297
|
+
personalityChangeBtn.top = 7;
|
|
1324
1298
|
personalityChangeBtn.left = 52;
|
|
1325
1299
|
|
|
1326
1300
|
const personalityTestBtn = _createButton(box, screen, '▶ Preview', COLORS, () => {
|
|
@@ -1331,12 +1305,12 @@ export function createSettingsTab(screen, services) {
|
|
|
1331
1305
|
: _buildPreviewPhrase();
|
|
1332
1306
|
_runTest(false, phrase);
|
|
1333
1307
|
}, { bg: COLORS.btnTest });
|
|
1334
|
-
personalityTestBtn.top =
|
|
1308
|
+
personalityTestBtn.top = 7;
|
|
1335
1309
|
personalityTestBtn.left = 64;
|
|
1336
1310
|
|
|
1337
1311
|
const personalityFileText = blessed.text({
|
|
1338
1312
|
parent: box,
|
|
1339
|
-
top:
|
|
1313
|
+
top: 8,
|
|
1340
1314
|
left: 22,
|
|
1341
1315
|
right: 2,
|
|
1342
1316
|
wrap: false,
|
|
@@ -1350,9 +1324,9 @@ export function createSettingsTab(screen, services) {
|
|
|
1350
1324
|
|
|
1351
1325
|
const introTextHeader = blessed.text({
|
|
1352
1326
|
parent: box,
|
|
1353
|
-
top:
|
|
1327
|
+
top: 10,
|
|
1354
1328
|
left: 1,
|
|
1355
|
-
content: '{
|
|
1329
|
+
content: '{bright-cyan-fg} ✍️ Intro Text {/bright-cyan-fg}',
|
|
1356
1330
|
tags: true,
|
|
1357
1331
|
style: { bg: COLORS.contentBg },
|
|
1358
1332
|
});
|
|
@@ -1362,7 +1336,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1362
1336
|
|
|
1363
1337
|
const introTextLabel = blessed.text({
|
|
1364
1338
|
parent: box,
|
|
1365
|
-
top:
|
|
1339
|
+
top: 12,
|
|
1366
1340
|
left: 6,
|
|
1367
1341
|
content: 'Intro Text:',
|
|
1368
1342
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -1370,7 +1344,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1370
1344
|
|
|
1371
1345
|
const introTextValue = blessed.text({
|
|
1372
1346
|
parent: box,
|
|
1373
|
-
top:
|
|
1347
|
+
top: 12,
|
|
1374
1348
|
left: 22,
|
|
1375
1349
|
width: 26, // truncate before [Edit] at left:40
|
|
1376
1350
|
wrap: false,
|
|
@@ -1381,19 +1355,19 @@ export function createSettingsTab(screen, services) {
|
|
|
1381
1355
|
const introEditBtn = _createButton(box, screen, 'Edit', COLORS, () => {
|
|
1382
1356
|
_openIntroTextEditor(screen, configService, () => { refreshDisplay(); }, _restoreFocus);
|
|
1383
1357
|
}, { bg: COLORS.btnEdit });
|
|
1384
|
-
introEditBtn.top =
|
|
1358
|
+
introEditBtn.top = 12;
|
|
1385
1359
|
introEditBtn.left = 52;
|
|
1386
1360
|
|
|
1387
1361
|
const introClearBtn = _createButton(box, screen, 'Clear', COLORS, () => {
|
|
1388
1362
|
configService.set('pretext', '');
|
|
1389
1363
|
refreshDisplay();
|
|
1390
1364
|
}, { bg: '#c62828' });
|
|
1391
|
-
introClearBtn.top =
|
|
1365
|
+
introClearBtn.top = 12;
|
|
1392
1366
|
introClearBtn.left = 64;
|
|
1393
1367
|
|
|
1394
1368
|
const introPathText = blessed.text({
|
|
1395
1369
|
parent: box,
|
|
1396
|
-
top:
|
|
1370
|
+
top: 13,
|
|
1397
1371
|
left: 22,
|
|
1398
1372
|
right: 2,
|
|
1399
1373
|
wrap: false,
|
|
@@ -1413,7 +1387,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1413
1387
|
parent: box,
|
|
1414
1388
|
top: 3,
|
|
1415
1389
|
left: 2,
|
|
1416
|
-
content: '{
|
|
1390
|
+
content: '{bright-cyan-fg} 📡 Audio Destination {/bright-cyan-fg}',
|
|
1417
1391
|
tags: true,
|
|
1418
1392
|
style: { bg: COLORS.contentBg },
|
|
1419
1393
|
});
|
|
@@ -1542,7 +1516,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1542
1516
|
const current = configService.getConfig().audio_stream_mode ?? 'text';
|
|
1543
1517
|
configService.set('audio_stream_mode', current === 'text' ? 'pulse' : 'text');
|
|
1544
1518
|
refreshDisplay();
|
|
1545
|
-
}, { bg: '#
|
|
1519
|
+
}, { bg: '#1565c0' }); // blue — distinct from green focus
|
|
1546
1520
|
audioStreamModeBtn.top = 7;
|
|
1547
1521
|
audioStreamModeBtn.left = 64;
|
|
1548
1522
|
audioStreamModeBtn.hide();
|
|
@@ -1566,7 +1540,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1566
1540
|
parent: box,
|
|
1567
1541
|
top: 11,
|
|
1568
1542
|
left: 2,
|
|
1569
|
-
content: '{
|
|
1543
|
+
content: '{bright-cyan-fg} 💾 Config Storage {/bright-cyan-fg}',
|
|
1570
1544
|
tags: true,
|
|
1571
1545
|
style: { bg: COLORS.contentBg },
|
|
1572
1546
|
});
|
|
@@ -1632,7 +1606,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1632
1606
|
refreshConfigDisplay();
|
|
1633
1607
|
_showNotice(screen, 'Settings Saved');
|
|
1634
1608
|
}, () => { _currentIdx = _buttons.indexOf(saveLocallyBtn); _focusButton(saveLocallyBtn); });
|
|
1635
|
-
}, { bg: '#
|
|
1609
|
+
}, { bg: '#1565c0' }); // blue — distinct from green focus
|
|
1636
1610
|
saveLocallyBtn.bottom = 0;
|
|
1637
1611
|
saveLocallyBtn.left = 46;
|
|
1638
1612
|
|
|
@@ -1660,6 +1634,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1660
1634
|
// Widget groups for each sub-tab (used by _showSubTab to show/hide)
|
|
1661
1635
|
const _subTabWidgets = {
|
|
1662
1636
|
voice: [
|
|
1637
|
+
providerVoiceHeader,
|
|
1663
1638
|
providerLabel, providerValue, switchBtn,
|
|
1664
1639
|
voiceLabel, voiceValue, changeBtn, playBtn, voiceFileText,
|
|
1665
1640
|
],
|
|
@@ -1671,6 +1646,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1671
1646
|
volumeLabel, volumeValue, volumeChangeBtn,
|
|
1672
1647
|
],
|
|
1673
1648
|
personality: [
|
|
1649
|
+
styleHeader,
|
|
1674
1650
|
verbosityLabel, verbosityValue, verbosityChangeBtn, verbosityPathText,
|
|
1675
1651
|
personalityLabel, personalityValue, personalityChangeBtn, personalityTestBtn, personalityFileText,
|
|
1676
1652
|
introTextHeader,
|
|
@@ -2131,7 +2107,7 @@ export function createSettingsTab(screen, services) {
|
|
|
2131
2107
|
audioSshValue.setContent(audioAlias || '(none)');
|
|
2132
2108
|
const streamMode = cfg.audio_stream_mode ?? 'text';
|
|
2133
2109
|
audioStreamModeBtn.setContent(streamMode === 'pulse' ? 'Streaming Pulse Audio' : 'Streaming Text Only ✓');
|
|
2134
|
-
audioStreamModeBtn.style.bg = streamMode === 'text' ? '#
|
|
2110
|
+
audioStreamModeBtn.style.bg = streamMode === 'text' ? '#1565c0' : COLORS.btnChange;
|
|
2135
2111
|
} else {
|
|
2136
2112
|
audioSshLabel.hide();
|
|
2137
2113
|
audioSshValue.hide();
|
|
@@ -2302,14 +2278,14 @@ function _createButton(parent, screen, label, COLORS, onClick, opts = {}) {
|
|
|
2302
2278
|
const _ALL_PROVIDERS = [
|
|
2303
2279
|
{ id: 'piper', name: 'Piper TTS', platforms: ['linux', 'darwin', 'win32'], desc: 'High-quality local neural TTS' },
|
|
2304
2280
|
{ id: 'soprano', name: 'Soprano', platforms: ['linux', 'darwin'], desc: 'Ultra-fast neural TTS (single voice)' },
|
|
2305
|
-
{ id: '
|
|
2281
|
+
{ id: 'sapi', name: 'Windows SAPI', platforms: ['win32'], desc: 'Windows built-in text-to-speech' },
|
|
2306
2282
|
{ id: 'macos', name: 'Mac Say', platforms: ['darwin'], desc: 'macOS built-in text-to-speech' },
|
|
2307
2283
|
];
|
|
2308
2284
|
|
|
2309
2285
|
const _INSTALL_CMDS = {
|
|
2310
2286
|
piper: ['pip install piper-tts', 'OR: pipx install piper-tts', '', 'Voices are downloaded separately:', 'Run: agentvibes install (then choose Piper)'],
|
|
2311
2287
|
soprano: ['pip install soprano-tts', 'OR: pipx install soprano-tts', '', 'Keep model loaded for fast synthesis:', 'soprano-webui'],
|
|
2312
|
-
|
|
2288
|
+
sapi: ['Built-in on Windows — no install required.', 'Only works in a native Windows shell,', 'not inside WSL. Use piper or soprano in WSL.'],
|
|
2313
2289
|
macos: ['Built-in on macOS — no install required.', 'The say command ships with every Mac.'],
|
|
2314
2290
|
};
|
|
2315
2291
|
|
|
@@ -2354,7 +2330,7 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
|
|
|
2354
2330
|
// Environment header
|
|
2355
2331
|
blessed.text({
|
|
2356
2332
|
parent: modal, top: 0, left: 1, tags: true,
|
|
2357
|
-
content: `{
|
|
2333
|
+
content: `{bright-cyan-fg}🖥 Environment:{/bright-cyan-fg} {bold}${envLabel}{/bold}`,
|
|
2358
2334
|
style: { bg: COLORS.contentBg },
|
|
2359
2335
|
});
|
|
2360
2336
|
blessed.text({
|
|
@@ -2404,8 +2380,8 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
|
|
|
2404
2380
|
_close(); onSelect(prov.id);
|
|
2405
2381
|
} else {
|
|
2406
2382
|
const lines = _INSTALL_CMDS[prov.id] ?? ['No instructions available.'];
|
|
2407
|
-
instrTitle.setContent(`{
|
|
2408
|
-
instrContent.setContent(lines.map(l => l ? `{
|
|
2383
|
+
instrTitle.setContent(`{bright-cyan-fg}Install — ${prov.name}:{/bright-cyan-fg}`);
|
|
2384
|
+
instrContent.setContent(lines.map(l => l ? `{bright-cyan-fg}${l}{/bright-cyan-fg}` : '').join('\n'));
|
|
2409
2385
|
screen.render();
|
|
2410
2386
|
}
|
|
2411
2387
|
});
|
|
@@ -2419,7 +2395,7 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
|
|
|
2419
2395
|
|
|
2420
2396
|
const instrTitle = blessed.text({
|
|
2421
2397
|
parent: modal, top: 11, left: 1, width: 66, tags: true,
|
|
2422
|
-
content: '{
|
|
2398
|
+
content: '{bright-cyan-fg}Install instructions — click Install beside a provider:{/bright-cyan-fg}',
|
|
2423
2399
|
style: { bg: COLORS.contentBg },
|
|
2424
2400
|
});
|
|
2425
2401
|
const instrContent = blessed.text({
|
|
@@ -2453,19 +2429,14 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
|
|
|
2453
2429
|
}
|
|
2454
2430
|
|
|
2455
2431
|
// ---------------------------------------------------------------------------
|
|
2456
|
-
// Private: Destroy
|
|
2457
|
-
//
|
|
2432
|
+
// Private: Destroy helper — now imported from shared widgets/destroy-list.js
|
|
2433
|
+
// (kept as comment for git blame traceability)
|
|
2458
2434
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
2465
|
-
} catch {}
|
|
2466
|
-
onClose?.();
|
|
2467
|
-
screen.render();
|
|
2468
|
-
}
|
|
2435
|
+
// NOTE: The following line was the old _destroyList definition, now using shared import:
|
|
2436
|
+
// import { destroyList } from '../widgets/destroy-list.js';
|
|
2437
|
+
//
|
|
2438
|
+
// Old code removed to eliminate duplication (M1 fix).
|
|
2439
|
+
// The shared destroyList has identical behavior.
|
|
2469
2440
|
|
|
2470
2441
|
// ---------------------------------------------------------------------------
|
|
2471
2442
|
// Private: Show a temporary stub notice text
|
|
@@ -2536,11 +2507,11 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
|
|
|
2536
2507
|
style: {
|
|
2537
2508
|
fg: '#e3f2fd',
|
|
2538
2509
|
bg: COLORS.contentBg,
|
|
2539
|
-
border: { fg: '
|
|
2510
|
+
border: { fg: 'bright-cyan' },
|
|
2540
2511
|
},
|
|
2541
2512
|
});
|
|
2542
2513
|
|
|
2543
|
-
function _close() {
|
|
2514
|
+
function _close() { destroyList(modal, screen, onClose); }
|
|
2544
2515
|
|
|
2545
2516
|
modal.key(['escape'], _close);
|
|
2546
2517
|
|
|
@@ -2555,7 +2526,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
|
|
|
2555
2526
|
const okBtn = _createButton(modal, screen, 'OK — Save', COLORS, () => {
|
|
2556
2527
|
_close();
|
|
2557
2528
|
onConfirm();
|
|
2558
|
-
}, { bg: '#
|
|
2529
|
+
}, { bg: '#1565c0' });
|
|
2559
2530
|
okBtn.top = btnRow;
|
|
2560
2531
|
okBtn.left = midX + 2;
|
|
2561
2532
|
|
|
@@ -2568,27 +2539,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
|
|
|
2568
2539
|
}
|
|
2569
2540
|
|
|
2570
2541
|
function _showNotice(screen, message) {
|
|
2571
|
-
|
|
2572
|
-
const modal = blessed.box({
|
|
2573
|
-
parent: screen,
|
|
2574
|
-
top: 'center',
|
|
2575
|
-
left: 'center',
|
|
2576
|
-
width,
|
|
2577
|
-
height: 3,
|
|
2578
|
-
border: { type: 'line' },
|
|
2579
|
-
tags: true,
|
|
2580
|
-
content: `{center}${message}{/center}`,
|
|
2581
|
-
style: {
|
|
2582
|
-
fg: '#e3f2fd',
|
|
2583
|
-
bg: COLORS.contentBg,
|
|
2584
|
-
border: { fg: '#00e5ff' },
|
|
2585
|
-
},
|
|
2586
|
-
});
|
|
2587
|
-
screen.render();
|
|
2588
|
-
|
|
2589
|
-
setTimeout(() => {
|
|
2590
|
-
_destroyList(modal, screen);
|
|
2591
|
-
}, 2500);
|
|
2542
|
+
_showNoticeWidget(screen, message, { bg: COLORS.contentBg });
|
|
2592
2543
|
}
|
|
2593
2544
|
|
|
2594
2545
|
// ---------------------------------------------------------------------------
|
|
@@ -2605,64 +2556,8 @@ function _setEffects(configService, partial) {
|
|
|
2605
2556
|
}
|
|
2606
2557
|
|
|
2607
2558
|
// ---------------------------------------------------------------------------
|
|
2608
|
-
// Private:
|
|
2609
|
-
|
|
2610
|
-
function _openReverbPicker(screen, configService, onSelect, onClose) {
|
|
2611
|
-
const PRESETS = [
|
|
2612
|
-
{ label: 'Off (Dry, no reverb)', value: 'off' },
|
|
2613
|
-
{ label: 'Light (Small room)', value: 'light' },
|
|
2614
|
-
{ label: 'Medium (Conference room)', value: 'medium' },
|
|
2615
|
-
{ label: 'Heavy (Large hall)', value: 'heavy' },
|
|
2616
|
-
{ label: 'Cathedral (Epic space)', value: 'cathedral' },
|
|
2617
|
-
];
|
|
2618
|
-
|
|
2619
|
-
const currentPreset = configService.getConfig().effects?.reverbPreset ?? 'light';
|
|
2620
|
-
const currentIdx = Math.max(0, PRESETS.findIndex(p => p.value === currentPreset));
|
|
2621
|
-
|
|
2622
|
-
const list = blessed.list({
|
|
2623
|
-
parent: screen,
|
|
2624
|
-
top: 'center',
|
|
2625
|
-
left: 'center',
|
|
2626
|
-
width: 40,
|
|
2627
|
-
height: PRESETS.length + 4,
|
|
2628
|
-
border: { type: 'line' },
|
|
2629
|
-
tags: true,
|
|
2630
|
-
label: _modalTitle('Select Reverb Preset'),
|
|
2631
|
-
items: PRESETS.map((p, i) => (i === currentIdx ? `● ${p.label}` : ` ${p.label}`)),
|
|
2632
|
-
keys: true,
|
|
2633
|
-
vi: false,
|
|
2634
|
-
mouse: true,
|
|
2635
|
-
style: {
|
|
2636
|
-
border: { fg: COLORS.btnFocus },
|
|
2637
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
2638
|
-
item: { fg: '#e3f2fd' },
|
|
2639
|
-
},
|
|
2640
|
-
});
|
|
2641
|
-
|
|
2642
|
-
list.select(currentIdx);
|
|
2643
|
-
list.focus();
|
|
2644
|
-
screen.render();
|
|
2645
|
-
|
|
2646
|
-
list.key(['enter', 'space'], () => {
|
|
2647
|
-
const selected = PRESETS[list.selected];
|
|
2648
|
-
if (!selected) return;
|
|
2649
|
-
_destroyList(list, screen, onClose);
|
|
2650
|
-
|
|
2651
|
-
// Apply to audio config via effects-manager.sh
|
|
2652
|
-
const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
|
|
2653
|
-
spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], {
|
|
2654
|
-
stdio: 'ignore',
|
|
2655
|
-
timeout: 5000,
|
|
2656
|
-
env: { ...process.env },
|
|
2657
|
-
});
|
|
2658
|
-
|
|
2659
|
-
onSelect(selected.value);
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
list.key(['escape', 'q'], () => {
|
|
2663
|
-
_destroyList(list, screen, onClose);
|
|
2664
|
-
});
|
|
2665
|
-
}
|
|
2559
|
+
// Private: _openReverbPicker removed — now using shared import:
|
|
2560
|
+
// import { openReverbPicker } from '../widgets/reverb-picker.js';
|
|
2666
2561
|
|
|
2667
2562
|
// ---------------------------------------------------------------------------
|
|
2668
2563
|
// Private: Background music config read/write helpers
|
|
@@ -2700,7 +2595,7 @@ function _openTrackPicker(screen, configService, onSelect, onClose) {
|
|
|
2700
2595
|
const currentTrack = (configService.getConfig().backgroundMusic?.track ?? MUSIC_DEFAULTS.track);
|
|
2701
2596
|
const items = allItems.map(t =>
|
|
2702
2597
|
t.file === ADD_SENTINEL
|
|
2703
|
-
? ` {
|
|
2598
|
+
? ` {bright-cyan-fg}+ Add Custom Track{/bright-cyan-fg}`
|
|
2704
2599
|
: (t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`)
|
|
2705
2600
|
);
|
|
2706
2601
|
const currentIdx = tracks.findIndex(t => t.file === currentTrack);
|
|
@@ -2737,18 +2632,18 @@ function _openTrackPicker(screen, configService, onSelect, onClose) {
|
|
|
2737
2632
|
if (!selected) return;
|
|
2738
2633
|
if (selected.file === ADD_SENTINEL) {
|
|
2739
2634
|
// Destroy list first, then open path-input dialog
|
|
2740
|
-
|
|
2635
|
+
destroyList(list, screen);
|
|
2741
2636
|
_openCustomTrackInput(screen, tracksDir, (newFile) => {
|
|
2742
2637
|
onSelect(newFile);
|
|
2743
2638
|
}, onClose);
|
|
2744
2639
|
return;
|
|
2745
2640
|
}
|
|
2746
|
-
|
|
2641
|
+
destroyList(list, screen, onClose);
|
|
2747
2642
|
onSelect(selected.file);
|
|
2748
2643
|
});
|
|
2749
2644
|
|
|
2750
2645
|
list.key(['escape', 'q'], () => {
|
|
2751
|
-
|
|
2646
|
+
destroyList(list, screen, onClose);
|
|
2752
2647
|
});
|
|
2753
2648
|
}
|
|
2754
2649
|
|
|
@@ -2939,9 +2834,14 @@ function _openVolumePicker(screen, configService, onSelect, onClose) {
|
|
|
2939
2834
|
`mpg123 -q "${trackPath}"`,
|
|
2940
2835
|
].join(' 2>/dev/null || ') + ' 2>/dev/null';
|
|
2941
2836
|
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2837
|
+
if (_IS_WINDOWS) {
|
|
2838
|
+
const _mp3P3 = detectMp3Player(_previewEnv);
|
|
2839
|
+
_previewProcess = _mp3P3
|
|
2840
|
+
? spawn(_mp3P3.bin, _mp3P3.args(trackPath), _spawnOpts(_previewEnv))
|
|
2841
|
+
: null;
|
|
2842
|
+
} else {
|
|
2843
|
+
_previewProcess = spawn('sh', ['-c', cmd], _spawnOpts(_previewEnv));
|
|
2844
|
+
}
|
|
2945
2845
|
_previewProcess.unref();
|
|
2946
2846
|
_refreshList();
|
|
2947
2847
|
|
|
@@ -3077,7 +2977,7 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
|
|
|
3077
2977
|
fg: COLORS.labelFg,
|
|
3078
2978
|
bg: COLORS.contentBg,
|
|
3079
2979
|
border: { fg: COLORS.borderFg },
|
|
3080
|
-
selected: { bg: '#
|
|
2980
|
+
selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
|
|
3081
2981
|
item: { fg: COLORS.labelFg },
|
|
3082
2982
|
},
|
|
3083
2983
|
});
|
|
@@ -3090,7 +2990,7 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
|
|
|
3090
2990
|
right: 2,
|
|
3091
2991
|
tags: true,
|
|
3092
2992
|
content: '',
|
|
3093
|
-
style: { fg: '
|
|
2993
|
+
style: { fg: 'bright-cyan', bg: COLORS.contentBg },
|
|
3094
2994
|
});
|
|
3095
2995
|
|
|
3096
2996
|
// ---- File location hint ----
|
|
@@ -3184,15 +3084,16 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
|
|
|
3184
3084
|
|
|
3185
3085
|
const _mp3Player = detectMp3Player(_modalEnv);
|
|
3186
3086
|
if (!_mp3Player) return;
|
|
3087
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
3187
3088
|
_previewProcess = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
|
|
3188
|
-
stdio: 'ignore', detached: true, env: _modalEnv,
|
|
3089
|
+
stdio: 'ignore', detached: !_isWin, windowsHide: true, env: _modalEnv,
|
|
3189
3090
|
});
|
|
3190
3091
|
_previewProcess.unref();
|
|
3191
3092
|
_previewTrackId = trackId;
|
|
3192
3093
|
|
|
3193
3094
|
const label = _allTracks.find(t => t.id === trackId)?.label ?? formatTrackLabel(trackId);
|
|
3194
3095
|
if (!_closed) {
|
|
3195
|
-
modalPreviewLine.setContent(`{
|
|
3096
|
+
modalPreviewLine.setContent(`{bright-cyan-fg}\u266A Previewing: ${label} (Space to stop){/bright-cyan-fg}`);
|
|
3196
3097
|
screen.render();
|
|
3197
3098
|
}
|
|
3198
3099
|
|
|
@@ -3301,13 +3202,13 @@ function _openVerbosityPicker(screen, configService, onDone, onClose) {
|
|
|
3301
3202
|
list.key(['enter', 'space'], () => {
|
|
3302
3203
|
const selected = levels[list.selected];
|
|
3303
3204
|
if (!selected) return;
|
|
3304
|
-
|
|
3205
|
+
destroyList(list, screen, onClose);
|
|
3305
3206
|
configService.set('verbosity', selected.toLowerCase());
|
|
3306
3207
|
onDone();
|
|
3307
3208
|
});
|
|
3308
3209
|
|
|
3309
3210
|
list.key(['escape', 'q'], () => {
|
|
3310
|
-
|
|
3211
|
+
destroyList(list, screen, onClose);
|
|
3311
3212
|
});
|
|
3312
3213
|
}
|
|
3313
3214
|
|
|
@@ -3487,7 +3388,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3487
3388
|
keys: true,
|
|
3488
3389
|
style: {
|
|
3489
3390
|
fg: COLORS.valueFg,
|
|
3490
|
-
bg: '#
|
|
3391
|
+
bg: '#1a3a5c',
|
|
3491
3392
|
focus: { bg: '#283593' },
|
|
3492
3393
|
},
|
|
3493
3394
|
});
|
|
@@ -3497,7 +3398,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3497
3398
|
parent: modal,
|
|
3498
3399
|
top: 2,
|
|
3499
3400
|
left: 6,
|
|
3500
|
-
content: `{
|
|
3401
|
+
content: `{bright-cyan-fg}${'Name'.padEnd(COL_NAME_W)}${'Gender'.padEnd(COL_GENDER_W)}Provider{/bright-cyan-fg}`,
|
|
3501
3402
|
tags: true,
|
|
3502
3403
|
style: { bg: COLORS.contentBg },
|
|
3503
3404
|
});
|
|
@@ -3518,7 +3419,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3518
3419
|
fg: COLORS.labelFg,
|
|
3519
3420
|
bg: COLORS.contentBg,
|
|
3520
3421
|
border: { fg: COLORS.borderFg },
|
|
3521
|
-
selected: { bg: '#
|
|
3422
|
+
selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
|
|
3522
3423
|
item: { fg: COLORS.labelFg },
|
|
3523
3424
|
},
|
|
3524
3425
|
});
|
|
@@ -3528,7 +3429,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3528
3429
|
parent: modal,
|
|
3529
3430
|
bottom: 5,
|
|
3530
3431
|
left: 2,
|
|
3531
|
-
content: `{
|
|
3432
|
+
content: `{bright-cyan-fg}── Voice Info ${'─'.repeat(50)}{/bright-cyan-fg}`,
|
|
3532
3433
|
tags: true,
|
|
3533
3434
|
style: { bg: COLORS.contentBg },
|
|
3534
3435
|
});
|
|
@@ -3550,7 +3451,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3550
3451
|
right: 2,
|
|
3551
3452
|
tags: true,
|
|
3552
3453
|
content: '',
|
|
3553
|
-
style: { fg: '
|
|
3454
|
+
style: { fg: 'bright-cyan', bg: COLORS.contentBg },
|
|
3554
3455
|
});
|
|
3555
3456
|
|
|
3556
3457
|
// ---- Key hint bar ----
|
|
@@ -3655,11 +3556,22 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3655
3556
|
const tempWav = path.join(os.tmpdir(), `agentvibes-preview-${Date.now()}.wav`);
|
|
3656
3557
|
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
3657
3558
|
|
|
3559
|
+
const _isWinPreview = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
3560
|
+
let _piperBin3 = 'piper';
|
|
3561
|
+
if (_isWinPreview) {
|
|
3562
|
+
const _lad = process.env.LOCALAPPDATA ||
|
|
3563
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
3564
|
+
if (_lad) {
|
|
3565
|
+
const _exe = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
|
|
3566
|
+
if (fs.existsSync(_exe)) _piperBin3 = _exe;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3658
3569
|
const _piperArgs3 = ['--model', voicePath, '--output_file', tempWav];
|
|
3659
3570
|
if (_ms3.speakerId != null) _piperArgs3.push('--speaker', String(_ms3.speakerId));
|
|
3660
|
-
const piper = spawn(
|
|
3571
|
+
const piper = spawn(_piperBin3, _piperArgs3, {
|
|
3661
3572
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
3662
|
-
detached:
|
|
3573
|
+
detached: !_isWinPreview,
|
|
3574
|
+
windowsHide: true,
|
|
3663
3575
|
env: _spawnEnv,
|
|
3664
3576
|
});
|
|
3665
3577
|
piper.stdin.write(phrase + '\n');
|
|
@@ -3668,7 +3580,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3668
3580
|
_playingProcess = piper;
|
|
3669
3581
|
_playingVoiceId = voiceId;
|
|
3670
3582
|
if (!_closed) {
|
|
3671
|
-
modalPreviewLine.setContent(`{
|
|
3583
|
+
modalPreviewLine.setContent(`{bright-cyan-fg}♪ Synthesizing: ${voiceId}…{/bright-cyan-fg}`);
|
|
3672
3584
|
screen.render();
|
|
3673
3585
|
}
|
|
3674
3586
|
|
|
@@ -3681,7 +3593,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3681
3593
|
_playingVoiceId = null;
|
|
3682
3594
|
_playingProcess = null;
|
|
3683
3595
|
if (!_closed) {
|
|
3684
|
-
modalPreviewLine.setContent('{
|
|
3596
|
+
modalPreviewLine.setContent('{bright-cyan-fg}♪ Preview failed (piper error — is piper installed?){/bright-cyan-fg}');
|
|
3685
3597
|
screen.render();
|
|
3686
3598
|
setTimeout(() => { if (!_closed) { modalPreviewLine.setContent(''); screen.render(); } }, 4000);
|
|
3687
3599
|
}
|
|
@@ -3692,13 +3604,14 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3692
3604
|
if (!_wavPlayer3) return;
|
|
3693
3605
|
const playProc = spawn(_wavPlayer3.bin, _wavPlayer3.args(tempWav), {
|
|
3694
3606
|
stdio: 'ignore',
|
|
3695
|
-
detached:
|
|
3607
|
+
detached: !_isWinPreview,
|
|
3608
|
+
windowsHide: true,
|
|
3696
3609
|
env: _spawnEnv,
|
|
3697
3610
|
});
|
|
3698
3611
|
_playingProcess = playProc;
|
|
3699
3612
|
|
|
3700
3613
|
if (!_closed) {
|
|
3701
|
-
modalPreviewLine.setContent(`{
|
|
3614
|
+
modalPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId} (Space to stop){/bright-cyan-fg}`);
|
|
3702
3615
|
screen.render();
|
|
3703
3616
|
}
|
|
3704
3617
|
|
|
@@ -3723,7 +3636,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3723
3636
|
_playingVoiceId = null;
|
|
3724
3637
|
_playingProcess = null;
|
|
3725
3638
|
if (!_closed) {
|
|
3726
|
-
modalPreviewLine.setContent('{
|
|
3639
|
+
modalPreviewLine.setContent('{bright-cyan-fg}♪ Cannot find piper — install with: pipx install piper-tts{/bright-cyan-fg}');
|
|
3727
3640
|
screen.render();
|
|
3728
3641
|
setTimeout(() => { if (!_closed) { modalPreviewLine.setContent(''); screen.render(); } }, 4000);
|
|
3729
3642
|
}
|
|
@@ -3815,135 +3728,5 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3815
3728
|
}
|
|
3816
3729
|
|
|
3817
3730
|
// ---------------------------------------------------------------------------
|
|
3818
|
-
// Private:
|
|
3819
|
-
|
|
3820
|
-
function _openPersonalityPicker(screen, configService, onSelect, onClose) {
|
|
3821
|
-
const current = configService.getConfig().personality ?? 'none';
|
|
3822
|
-
const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
|
|
3823
|
-
|
|
3824
|
-
const list = blessed.list({
|
|
3825
|
-
parent: screen,
|
|
3826
|
-
top: 'center',
|
|
3827
|
-
left: 'center',
|
|
3828
|
-
width: 44,
|
|
3829
|
-
height: Math.min(PERSONALITIES.length + 4, 22),
|
|
3830
|
-
border: { type: 'line' },
|
|
3831
|
-
tags: true,
|
|
3832
|
-
label: _modalTitle('Select Personality'),
|
|
3833
|
-
items: PERSONALITIES.map((p, i) => {
|
|
3834
|
-
const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
|
|
3835
|
-
const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
|
|
3836
|
-
const mark = i === currentIdx ? '✅' : ' ';
|
|
3837
|
-
return `${mark} ${emoji} ${label}`;
|
|
3838
|
-
}),
|
|
3839
|
-
keys: true,
|
|
3840
|
-
vi: true,
|
|
3841
|
-
mouse: true,
|
|
3842
|
-
style: {
|
|
3843
|
-
border: { fg: COLORS.btnFocus },
|
|
3844
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
3845
|
-
item: { fg: '#e3f2fd' },
|
|
3846
|
-
},
|
|
3847
|
-
});
|
|
3848
|
-
|
|
3849
|
-
list.select(currentIdx);
|
|
3850
|
-
list.focus();
|
|
3851
|
-
screen.render();
|
|
3852
|
-
|
|
3853
|
-
// ---------- Hover TTS preview ----------
|
|
3854
|
-
|
|
3855
|
-
let _pickerTtsProc = null;
|
|
3856
|
-
let _playingItemIdx = -1;
|
|
3857
|
-
|
|
3858
|
-
// Add or remove " (playing)" from a list item (strips any trailing █ first)
|
|
3859
|
-
function _setItemPlaying(idx, playing) {
|
|
3860
|
-
const item = list.items?.[idx];
|
|
3861
|
-
if (!item) return;
|
|
3862
|
-
const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
|
|
3863
|
-
item.setContent(playing ? `${base} (playing)` : base);
|
|
3864
|
-
}
|
|
3865
|
-
|
|
3866
|
-
function _killPickerTts() {
|
|
3867
|
-
if (_pickerTtsProc) {
|
|
3868
|
-
try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
|
|
3869
|
-
_pickerTtsProc = null;
|
|
3870
|
-
}
|
|
3871
|
-
if (_playingItemIdx >= 0) {
|
|
3872
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
3873
|
-
_playingItemIdx = -1;
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
function _speakPersonalityPreview(personality) {
|
|
3878
|
-
_killPickerTts();
|
|
3879
|
-
const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
|
|
3880
|
-
if (!phrase) return;
|
|
3881
|
-
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
3882
|
-
_pickerTtsProc = spawn('bash', [ttsScript, phrase], {
|
|
3883
|
-
stdio: 'ignore',
|
|
3884
|
-
detached: true,
|
|
3885
|
-
env: buildAudioEnv(),
|
|
3886
|
-
});
|
|
3887
|
-
_playingItemIdx = list.selected;
|
|
3888
|
-
_setItemPlaying(_playingItemIdx, true);
|
|
3889
|
-
screen.render();
|
|
3890
|
-
// Clear indicator when audio finishes naturally
|
|
3891
|
-
_pickerTtsProc.on('exit', () => {
|
|
3892
|
-
if (_playingItemIdx >= 0) {
|
|
3893
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
3894
|
-
_playingItemIdx = -1;
|
|
3895
|
-
screen.render();
|
|
3896
|
-
}
|
|
3897
|
-
_pickerTtsProc = null;
|
|
3898
|
-
});
|
|
3899
|
-
_pickerTtsProc.unref();
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
// Hover: auto-speaks preview phrase when cursor moves
|
|
3903
|
-
list.on('select item', () => {
|
|
3904
|
-
_speakPersonalityPreview(PERSONALITIES[list.selected]);
|
|
3905
|
-
});
|
|
3906
|
-
|
|
3907
|
-
// [Space] plays the selected personality, or stops if the same item is already playing.
|
|
3908
|
-
// Uses item-aware toggle so navigating with ↓ (which auto-plays) doesn't prevent Space from working.
|
|
3909
|
-
list.key(['space'], () => {
|
|
3910
|
-
if (_pickerTtsProc && _playingItemIdx === list.selected) {
|
|
3911
|
-
_killPickerTts(); // true toggle: stop only if this exact item is playing
|
|
3912
|
-
} else {
|
|
3913
|
-
_speakPersonalityPreview(PERSONALITIES[list.selected]); // play or switch
|
|
3914
|
-
}
|
|
3915
|
-
});
|
|
3916
|
-
|
|
3917
|
-
// Type-to-jump: press a letter to jump to the first matching personality (cycles on repeat)
|
|
3918
|
-
const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
|
|
3919
|
-
list.on('keypress', (ch, key) => {
|
|
3920
|
-
if (!ch || key.ctrl || key.meta) return;
|
|
3921
|
-
const lower = ch.toLowerCase();
|
|
3922
|
-
if (!/^[a-z]$/.test(lower)) return;
|
|
3923
|
-
if (_jumpBlocked.has(lower)) return;
|
|
3924
|
-
const count = PERSONALITIES.length;
|
|
3925
|
-
const start = list.selected ?? 0;
|
|
3926
|
-
for (let i = 1; i <= count; i++) {
|
|
3927
|
-
const idx = (start + i) % count;
|
|
3928
|
-
if (PERSONALITIES[idx].startsWith(lower)) {
|
|
3929
|
-
list.select(idx);
|
|
3930
|
-
screen.render();
|
|
3931
|
-
break;
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
});
|
|
3935
|
-
|
|
3936
|
-
// [Enter] confirms selection
|
|
3937
|
-
list.key(['enter'], () => {
|
|
3938
|
-
const selected = PERSONALITIES[list.selected];
|
|
3939
|
-
if (!selected) return;
|
|
3940
|
-
_killPickerTts();
|
|
3941
|
-
_destroyList(list, screen, onClose);
|
|
3942
|
-
onSelect(selected);
|
|
3943
|
-
});
|
|
3944
|
-
|
|
3945
|
-
list.key(['escape', 'q'], () => {
|
|
3946
|
-
_killPickerTts();
|
|
3947
|
-
_destroyList(list, screen, onClose);
|
|
3948
|
-
});
|
|
3949
|
-
}
|
|
3731
|
+
// Private: _openPersonalityPicker removed — now using shared import:
|
|
3732
|
+
// import { openPersonalityPicker } from '../widgets/personality-picker.js';
|