agentvibes 5.3.0 → 5.5.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/LITE-MODE.md +236 -0
- package/.agentvibes/README.md +136 -0
- package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +141 -0
- package/.agentvibes/backups/agents/analyst_20260204_144958.md +78 -0
- package/.agentvibes/backups/agents/architect_20260204_144958.md +72 -0
- package/.agentvibes/backups/agents/dev_20260204_144958.md +74 -0
- package/.agentvibes/backups/agents/pm_20260204_144958.md +72 -0
- package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +64 -0
- package/.agentvibes/backups/agents/sm_20260204_144958.md +87 -0
- package/.agentvibes/backups/agents/tea_20260204_144958.md +79 -0
- package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +82 -0
- package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +80 -0
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config/README-personality-defaults.md +162 -0
- package/.agentvibes/config/mode.txt +1 -0
- package/.agentvibes/config/personality-voice-defaults.default.json +21 -0
- package/.agentvibes/config/save-audio.txt +1 -0
- package/.agentvibes/config/voice-metadata.json +160 -0
- package/.agentvibes/config.json +24 -15
- package/.agentvibes/hooks/help.sh +191 -0
- package/.agentvibes/hooks/post-tool-use-lite.sh +111 -0
- package/.agentvibes/hooks/save-audio-manager.sh +162 -0
- package/.agentvibes/hooks/session-start-full-optimized.sh +102 -0
- package/.agentvibes/hooks/session-start-full.sh +142 -0
- package/.agentvibes/hooks/session-start-lite-v2.sh +34 -0
- package/.agentvibes/hooks/session-start-lite.sh +29 -0
- package/.agentvibes/hooks/stop-lite.sh +115 -0
- package/.agentvibes/hooks/switch-mode.sh +215 -0
- package/.agentvibes/output-styles/audio-summary.md +30 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/voice-samples/piper/alan.wav +0 -0
- package/.claude/audio/voice-samples/piper/amy.wav +0 -0
- package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
- package/.claude/audio/voice-samples/piper/joe.wav +0 -0
- package/.claude/audio/voice-samples/piper/john.wav +0 -0
- package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
- package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
- package/.claude/audio/voice-samples/piper/linda.wav +0 -0
- package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
- package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
- 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/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/audio-effects.cfg +16 -11
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -1
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -4
- package/.claude/config/tts-verbosity.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 +0 -0
- package/.claude/hooks/audio-processor.sh +60 -14
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-manager.sh +225 -0
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +12 -15
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +25 -23
- package/.claude/hooks/clawdbot-receiver.sh +4 -28
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +0 -0
- package/.claude/hooks/piper-installer.sh +1 -1
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +0 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-macos.sh +6 -12
- package/.claude/hooks/play-tts-piper.sh +52 -81
- package/.claude/hooks/play-tts-soprano.sh +9 -43
- package/.claude/hooks/play-tts-ssh-remote.sh +43 -215
- package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
- package/.claude/hooks/play-tts.sh +41 -20
- package/.claude/hooks/post-response.sh +41 -0
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +0 -0
- package/.claude/hooks/provider-manager.sh +0 -0
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +56 -39
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop.sh +63 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +26 -4
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/bmad-party-speak.ps1 +278 -278
- package/.claude/hooks-windows/bmad-speak.ps1 +264 -264
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -53
- package/.claude/hooks-windows/effects-manager.ps1 +294 -294
- package/.claude/hooks-windows/language-manager.ps1 +193 -193
- package/.claude/hooks-windows/learn-manager.ps1 +241 -241
- package/.claude/hooks-windows/personality-manager.ps1 +266 -266
- package/.claude/hooks-windows/play-tts-soprano.ps1 +5 -5
- package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -138
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -0
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts.ps1 +265 -507
- package/.claude/hooks-windows/provider-manager.ps1 +158 -192
- package/.claude/hooks-windows/session-start-tts.ps1 +55 -46
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -166
- package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -260
- 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/piper-voices-dir.txt +1 -0
- package/.claude/settings.json +25 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +149 -145
- package/.mcp.json +30 -11
- package/CLAUDE.md +170 -215
- package/README.md +207 -521
- package/RELEASE_NOTES.md +1172 -1976
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +0 -0
- package/bin/agentvibes-voice-browser.js +64 -1289
- package/bin/agentvibes.js +28 -0
- package/bin/ensure-soprano-running.sh +43 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +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 +1467 -1578
- package/mcp-server/test_server.py +395 -395
- package/package.json +1 -3
- package/setup-windows.ps1 +815 -815
- package/src/console/tabs/music-tab.js +5 -2
- package/src/console/tabs/voices-tab.js +71 -37
- package/src/installer.js +52 -5
- package/src/services/llm-provider-service.js +1 -1
- package/templates/agentvibes-receiver.sh +158 -483
- package/templates/audio/welcome-music.mp3 +0 -0
- package/.agentvibes/bmad-voice-map.json +0 -104
- package/.agentvibes/copilot-sessions.log +0 -4
- package/.claude/config/audio-effects-bmad.cfg +0 -50
- package/.claude/config/intro-text.txt +0 -1
- package/.claude/config/personality.txt +0 -1
- package/.claude/config/piper-speech-rate.txt +0 -4
- package/.claude/config/piper-target-speech-rate.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-target-speech-rate.txt +0 -1
- package/voice-assignments.json +0 -8245
- /package/{.claude → .agentvibes}/config/agentvibes.json +0 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import blessed from 'blessed';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import { exec, spawn
|
|
11
|
+
import { exec, spawn } from 'child_process';
|
|
12
12
|
import { promisify } from 'util';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import fsSync from 'fs';
|
|
@@ -56,22 +56,13 @@ class AgentVibesVoiceBrowser {
|
|
|
56
56
|
this.sortColumn = 'id';
|
|
57
57
|
this.sortAsc = true;
|
|
58
58
|
this.searchTerm = '';
|
|
59
|
-
this.favorites = new Set();
|
|
60
|
-
this.
|
|
61
|
-
this.thumbsDown = new Set();
|
|
62
|
-
this.favoritesOnly = false; // Filter to show only thumbs-up voices
|
|
63
|
-
this.providerFilter = null; // Filter by provider (null = all)
|
|
59
|
+
this.favorites = new Set();
|
|
60
|
+
this.favoritesOnly = false; // Filter to show only favorites
|
|
64
61
|
this.sampleText = CONFIG.SAMPLE_TEXT;
|
|
65
62
|
this.playing = false;
|
|
66
63
|
this.currentAudioProcess = null;
|
|
67
64
|
this.voiceAssignments = null;
|
|
68
65
|
this.voiceMetadata = null;
|
|
69
|
-
this.currentTab = 'voices'; // 'voices' or 'music'
|
|
70
|
-
this.musicTracks = [];
|
|
71
|
-
this.currentMusicSelection = null;
|
|
72
|
-
this.musicEnabled = false;
|
|
73
|
-
this.currentlyPlayingTrack = null; // Track which music track is currently playing
|
|
74
|
-
this.musicFavorites = new Set(); // Favorite music tracks
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
async init() {
|
|
@@ -93,7 +84,6 @@ class AgentVibesVoiceBrowser {
|
|
|
93
84
|
|
|
94
85
|
await this.loadProgress();
|
|
95
86
|
await this.loadVoiceData();
|
|
96
|
-
await this.loadMusicData();
|
|
97
87
|
this.prepareTable();
|
|
98
88
|
this.setupUI();
|
|
99
89
|
}
|
|
@@ -101,14 +91,7 @@ class AgentVibesVoiceBrowser {
|
|
|
101
91
|
async loadProgress() {
|
|
102
92
|
try {
|
|
103
93
|
const data = JSON.parse(await fs.readFile(CONFIG.PROGRESS_FILE, 'utf8'));
|
|
104
|
-
this.
|
|
105
|
-
this.thumbsDown = new Set(data.thumbsDown || []);
|
|
106
|
-
// Migrate legacy favorites → thumbsUp
|
|
107
|
-
if (data.favorites && data.favorites.length && !data.thumbsUp) {
|
|
108
|
-
for (const f of data.favorites) this.thumbsUp.add(f);
|
|
109
|
-
}
|
|
110
|
-
this.favorites = this.thumbsUp; // backward compat alias
|
|
111
|
-
this.musicFavorites = new Set(data.musicFavorites || []);
|
|
94
|
+
this.favorites = new Set(data.favorites || []);
|
|
112
95
|
this.sampleText = data.sampleText || CONFIG.SAMPLE_TEXT;
|
|
113
96
|
this.sortColumn = data.sortColumn || 'id';
|
|
114
97
|
this.sortAsc = data.sortAsc !== undefined ? data.sortAsc : true;
|
|
@@ -119,173 +102,14 @@ class AgentVibesVoiceBrowser {
|
|
|
119
102
|
|
|
120
103
|
async saveProgress() {
|
|
121
104
|
await fs.writeFile(CONFIG.PROGRESS_FILE, JSON.stringify({
|
|
122
|
-
|
|
123
|
-
thumbsDown: Array.from(this.thumbsDown),
|
|
124
|
-
favorites: Array.from(this.thumbsUp), // backward compat
|
|
125
|
-
musicFavorites: Array.from(this.musicFavorites),
|
|
105
|
+
favorites: Array.from(this.favorites),
|
|
126
106
|
sampleText: this.sampleText,
|
|
127
107
|
sortColumn: this.sortColumn,
|
|
128
108
|
sortAsc: this.sortAsc
|
|
129
109
|
}, null, 2));
|
|
130
110
|
}
|
|
131
111
|
|
|
132
|
-
async detectProviders() {
|
|
133
|
-
const providers = [];
|
|
134
|
-
|
|
135
|
-
// Check for macOS Say
|
|
136
|
-
if (process.platform === 'darwin') {
|
|
137
|
-
try {
|
|
138
|
-
const result = spawnSync('which', ['say'], { encoding: 'utf8', timeout: 1000 });
|
|
139
|
-
if (result.status === 0) {
|
|
140
|
-
providers.push('macos');
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
// Silently skip if check fails
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check for Windows SAPI (not available in WSL)
|
|
148
|
-
if (process.platform === 'win32') {
|
|
149
|
-
providers.push('windows-sapi');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check for Soprano TTS
|
|
153
|
-
try {
|
|
154
|
-
// Try to start Soprano if available
|
|
155
|
-
const ensureScript = path.join(__dirname, 'ensure-soprano-running.sh');
|
|
156
|
-
if (fsSync.existsSync(ensureScript)) {
|
|
157
|
-
try {
|
|
158
|
-
spawnSync('bash', [ensureScript], { encoding: 'utf8', timeout: 5000 });
|
|
159
|
-
} catch {
|
|
160
|
-
// Failed to start, skip silently
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Check if Soprano server is responding
|
|
165
|
-
const curlResult = spawnSync('curl', ['-s', '-m', '1', 'http://127.0.0.1:7860/openapi.json'], { encoding: 'utf8', timeout: 2000 });
|
|
166
|
-
if (curlResult.status === 0 && curlResult.stdout && curlResult.stdout.includes('Soprano')) {
|
|
167
|
-
providers.push('soprano');
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Silently skip if detection fails
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return providers;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async loadMusicData() {
|
|
177
|
-
// Load background music tracks
|
|
178
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
179
|
-
let tracksDir = path.join(homeDir, '.claude', 'audio', 'tracks');
|
|
180
|
-
|
|
181
|
-
// If running from project directory, also check project's .claude/audio/tracks
|
|
182
|
-
if (!fsSync.existsSync(tracksDir)) {
|
|
183
|
-
const projectTracksDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
184
|
-
if (fsSync.existsSync(projectTracksDir)) {
|
|
185
|
-
tracksDir = projectTracksDir;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const files = await fs.readdir(tracksDir);
|
|
191
|
-
this.musicTracks = files
|
|
192
|
-
.filter(f => f.endsWith('.mp3') && !f.startsWith('.'))
|
|
193
|
-
.map(file => ({
|
|
194
|
-
file,
|
|
195
|
-
name: file.replace(/^agent_vibes_|^agentvibes_|_v\d+|_loop\.mp3$/g, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
196
|
-
path: path.join(tracksDir, file)
|
|
197
|
-
}))
|
|
198
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
199
|
-
|
|
200
|
-
// Load current music selection
|
|
201
|
-
const musicConfigFile = path.join(homeDir, '.claude', 'config', 'background-music.txt');
|
|
202
|
-
try {
|
|
203
|
-
this.currentMusicSelection = (await fs.readFile(musicConfigFile, 'utf8')).trim();
|
|
204
|
-
} catch {
|
|
205
|
-
this.currentMusicSelection = null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Load music enabled status
|
|
209
|
-
const musicEnabledFile = path.join(homeDir, '.claude', 'config', 'background-music-enabled.txt');
|
|
210
|
-
try {
|
|
211
|
-
const enabled = (await fs.readFile(musicEnabledFile, 'utf8')).trim();
|
|
212
|
-
this.musicEnabled = enabled === 'true';
|
|
213
|
-
} catch {
|
|
214
|
-
this.musicEnabled = false;
|
|
215
|
-
}
|
|
216
|
-
} catch (error) {
|
|
217
|
-
this.musicTracks = [];
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async loadMacOSVoices() {
|
|
222
|
-
try {
|
|
223
|
-
const { stdout } = await execAsync('say -v ? 2>/dev/null');
|
|
224
|
-
const voices = [];
|
|
225
|
-
const lines = stdout.trim().split('\n');
|
|
226
|
-
|
|
227
|
-
for (const line of lines) {
|
|
228
|
-
const match = line.match(/^(\S+)\s+(\S+)\s+#\s*(.+)/);
|
|
229
|
-
if (match) {
|
|
230
|
-
const [, name, lang, description] = match;
|
|
231
|
-
voices.push({
|
|
232
|
-
name,
|
|
233
|
-
language: lang,
|
|
234
|
-
description: description || '',
|
|
235
|
-
provider: 'macos'
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return voices;
|
|
240
|
-
} catch {
|
|
241
|
-
return [];
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async loadWindowsSAPIVoices() {
|
|
246
|
-
try {
|
|
247
|
-
const psScript = 'Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | ForEach-Object { $_.VoiceInfo | Select-Object Name, Gender, Culture | ConvertTo-Json -Compress }';
|
|
248
|
-
const { stdout } = await execAsync(`powershell -Command "${psScript}"`, { timeout: 5000 });
|
|
249
|
-
const voices = [];
|
|
250
|
-
const lines = stdout.trim().split('\n').filter(l => l.trim());
|
|
251
|
-
|
|
252
|
-
for (const line of lines) {
|
|
253
|
-
try {
|
|
254
|
-
const voice = JSON.parse(line);
|
|
255
|
-
// SECURITY: Validate expected schema from PowerShell output (#133)
|
|
256
|
-
if (voice && typeof voice.Name === 'string' && voice.Name.length > 0) {
|
|
257
|
-
voices.push({
|
|
258
|
-
name: voice.Name,
|
|
259
|
-
gender: typeof voice.Gender === 'string' ? voice.Gender.toLowerCase() : 'unknown',
|
|
260
|
-
language: typeof voice.Culture === 'string' ? voice.Culture : 'en-US',
|
|
261
|
-
provider: 'windows-sapi'
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
} catch {}
|
|
265
|
-
}
|
|
266
|
-
return voices;
|
|
267
|
-
} catch {
|
|
268
|
-
return [];
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async loadSopranoVoices() {
|
|
273
|
-
// Soprano TTS currently has only one voice
|
|
274
|
-
// It uses OpenAI API format but ignores the voice parameter
|
|
275
|
-
return [
|
|
276
|
-
{
|
|
277
|
-
name: 'Soprano',
|
|
278
|
-
language: 'en-US',
|
|
279
|
-
provider: 'soprano',
|
|
280
|
-
description: 'Neural TTS voice'
|
|
281
|
-
}
|
|
282
|
-
];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
112
|
async loadVoiceData() {
|
|
286
|
-
// Detect available providers
|
|
287
|
-
this.availableProviders = await this.detectProviders();
|
|
288
|
-
|
|
289
113
|
// Load voice assignments (for LibriTTS speakers)
|
|
290
114
|
const assignmentsPath = path.join(__dirname, '..', 'voice-assignments.json');
|
|
291
115
|
if (fsSync.existsSync(assignmentsPath)) {
|
|
@@ -325,30 +149,10 @@ class AgentVibesVoiceBrowser {
|
|
|
325
149
|
}
|
|
326
150
|
}
|
|
327
151
|
}
|
|
328
|
-
|
|
329
|
-
// Load voices from other providers
|
|
330
|
-
this.otherProviderVoices = {
|
|
331
|
-
macos: [],
|
|
332
|
-
'windows-sapi': [],
|
|
333
|
-
soprano: []
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
if (this.availableProviders.includes('macos')) {
|
|
337
|
-
this.otherProviderVoices.macos = await this.loadMacOSVoices();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (this.availableProviders.includes('windows-sapi')) {
|
|
341
|
-
this.otherProviderVoices['windows-sapi'] = await this.loadWindowsSAPIVoices();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (this.availableProviders.includes('soprano')) {
|
|
345
|
-
this.otherProviderVoices.soprano = await this.loadSopranoVoices();
|
|
346
|
-
}
|
|
347
152
|
}
|
|
348
153
|
|
|
349
154
|
prepareTable() {
|
|
350
155
|
this.tableData = [];
|
|
351
|
-
let nextId = 0;
|
|
352
156
|
|
|
353
157
|
// Add LibriTTS speakers
|
|
354
158
|
for (let id = 0; id < CONFIG.TOTAL_SPEAKERS; id++) {
|
|
@@ -359,16 +163,13 @@ class AgentVibesVoiceBrowser {
|
|
|
359
163
|
const sampleText = template.replace('{NAME}', assignment.voice_name);
|
|
360
164
|
|
|
361
165
|
this.tableData.push({
|
|
362
|
-
id
|
|
363
|
-
originalId: id,
|
|
166
|
+
id,
|
|
364
167
|
gender: assignment.gender,
|
|
365
168
|
name: assignment.voice_name,
|
|
366
169
|
model: 'LibriTTS',
|
|
367
170
|
type: 'libritts',
|
|
368
|
-
provider: 'Piper',
|
|
369
171
|
piperVoiceId: `speaker-${id}`,
|
|
370
|
-
sampleText: sampleText
|
|
371
|
-
language: 'en_US'
|
|
172
|
+
sampleText: sampleText
|
|
372
173
|
});
|
|
373
174
|
}
|
|
374
175
|
}
|
|
@@ -379,73 +180,15 @@ class AgentVibesVoiceBrowser {
|
|
|
379
180
|
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
380
181
|
const sampleText = template.replace('{NAME}', curated.voice_name);
|
|
381
182
|
|
|
382
|
-
// Extract language from model file (e.g., en_US-amy-medium -> en_US)
|
|
383
|
-
const langMatch = curated.model_file.match(/^([a-z]{2}_[A-Z]{2})/);
|
|
384
|
-
const language = langMatch ? langMatch[1] : 'en_US';
|
|
385
|
-
|
|
386
183
|
this.tableData.push({
|
|
387
|
-
id:
|
|
388
|
-
originalId: parseInt(id),
|
|
184
|
+
id: parseInt(id),
|
|
389
185
|
gender: curated.gender,
|
|
390
186
|
name: curated.voice_name,
|
|
391
187
|
model: curated.model_file,
|
|
392
188
|
type: 'curated',
|
|
393
|
-
provider: 'Piper',
|
|
394
189
|
piperVoiceId: curated.model_file,
|
|
395
190
|
friendlyName: curated.friendly_name,
|
|
396
|
-
sampleText: sampleText
|
|
397
|
-
language: language
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Add macOS voices
|
|
402
|
-
for (const voice of this.otherProviderVoices.macos || []) {
|
|
403
|
-
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
404
|
-
const sampleText = template.replace('{NAME}', voice.name);
|
|
405
|
-
|
|
406
|
-
this.tableData.push({
|
|
407
|
-
id: nextId++,
|
|
408
|
-
gender: 'unknown',
|
|
409
|
-
name: voice.name,
|
|
410
|
-
model: 'macOS Say',
|
|
411
|
-
type: 'macos',
|
|
412
|
-
provider: 'macOS',
|
|
413
|
-
sampleText: sampleText,
|
|
414
|
-
language: voice.language || 'en_US'
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Add Windows SAPI voices
|
|
419
|
-
for (const voice of this.otherProviderVoices['windows-sapi'] || []) {
|
|
420
|
-
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
421
|
-
const sampleText = template.replace('{NAME}', voice.name);
|
|
422
|
-
|
|
423
|
-
this.tableData.push({
|
|
424
|
-
id: nextId++,
|
|
425
|
-
gender: voice.gender || 'unknown',
|
|
426
|
-
name: voice.name,
|
|
427
|
-
model: 'Windows SAPI',
|
|
428
|
-
type: 'windows-sapi',
|
|
429
|
-
provider: 'Windows',
|
|
430
|
-
sampleText: sampleText,
|
|
431
|
-
language: voice.language || 'en-US'
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Add Soprano voices
|
|
436
|
-
for (const voice of this.otherProviderVoices.soprano || []) {
|
|
437
|
-
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
438
|
-
const sampleText = template.replace('{NAME}', voice.name);
|
|
439
|
-
|
|
440
|
-
this.tableData.push({
|
|
441
|
-
id: nextId++,
|
|
442
|
-
gender: 'unknown',
|
|
443
|
-
name: voice.name,
|
|
444
|
-
model: 'Soprano',
|
|
445
|
-
type: 'soprano',
|
|
446
|
-
provider: 'Soprano',
|
|
447
|
-
sampleText: sampleText,
|
|
448
|
-
language: voice.language || 'en-US'
|
|
191
|
+
sampleText: sampleText
|
|
449
192
|
});
|
|
450
193
|
}
|
|
451
194
|
|
|
@@ -453,16 +196,11 @@ class AgentVibesVoiceBrowser {
|
|
|
453
196
|
}
|
|
454
197
|
|
|
455
198
|
applyFilter() {
|
|
456
|
-
// Start with all voices or
|
|
199
|
+
// Start with all voices or favorites only
|
|
457
200
|
let data = this.favoritesOnly
|
|
458
|
-
? this.tableData.filter(row => this.
|
|
201
|
+
? this.tableData.filter(row => this.favorites.has(row.id))
|
|
459
202
|
: [...this.tableData];
|
|
460
203
|
|
|
461
|
-
// Apply provider filter
|
|
462
|
-
if (this.providerFilter) {
|
|
463
|
-
data = data.filter(row => row.provider === this.providerFilter);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
204
|
// Apply search filter
|
|
467
205
|
if (this.searchTerm) {
|
|
468
206
|
const term = this.searchTerm.toLowerCase();
|
|
@@ -470,9 +208,7 @@ class AgentVibesVoiceBrowser {
|
|
|
470
208
|
row.id.toString().includes(term) ||
|
|
471
209
|
row.gender.includes(term) ||
|
|
472
210
|
row.name.toLowerCase().includes(term) ||
|
|
473
|
-
row.model.toLowerCase().includes(term)
|
|
474
|
-
row.language.toLowerCase().includes(term) ||
|
|
475
|
-
row.provider.toLowerCase().includes(term)
|
|
211
|
+
row.model.toLowerCase().includes(term)
|
|
476
212
|
);
|
|
477
213
|
}
|
|
478
214
|
|
|
@@ -491,16 +227,14 @@ class AgentVibesVoiceBrowser {
|
|
|
491
227
|
}
|
|
492
228
|
|
|
493
229
|
formatRow(row) {
|
|
494
|
-
const fav = this.
|
|
495
|
-
const genderIcon = row.gender === 'male' ? '♂' :
|
|
496
|
-
const genderColor = row.gender === 'male' ? 'blue-fg' :
|
|
230
|
+
const fav = this.favorites.has(row.id) ? '⭐' : ' ';
|
|
231
|
+
const genderIcon = row.gender === 'male' ? '♂' : '♀';
|
|
232
|
+
const genderColor = row.gender === 'male' ? 'blue-fg' : 'magenta-fg';
|
|
497
233
|
const gender = `{${genderColor}}${genderIcon}{/${genderColor}}`;
|
|
498
234
|
const id = String(row.id).padStart(4);
|
|
499
|
-
const name = row.name.
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const model = row.model.substring(0, 15).padEnd(15);
|
|
503
|
-
return `${fav} ${id} ${gender} ${name} ${provider} ${lang} ${model}`;
|
|
235
|
+
const name = row.name.padEnd(15);
|
|
236
|
+
const model = row.model.substring(0, 25).padEnd(25);
|
|
237
|
+
return `${fav} ${id} ${gender} ${name} ${model}`;
|
|
504
238
|
}
|
|
505
239
|
|
|
506
240
|
setupUI() {
|
|
@@ -511,67 +245,30 @@ class AgentVibesVoiceBrowser {
|
|
|
511
245
|
|
|
512
246
|
const title = blessed.box({
|
|
513
247
|
top: 0,
|
|
514
|
-
height:
|
|
248
|
+
height: 3,
|
|
515
249
|
width: '100%',
|
|
516
|
-
content: `{center}{bold}{cyan-fg}Agent{/cyan-fg} {magenta-fg}Vibes{/magenta-fg} {gray-fg}v1.0{/gray-fg} {yellow-fg}Voice Browser{/yellow-fg}{/bold}{/center}`,
|
|
250
|
+
content: `{center}{bold}{cyan-fg}Agent{/cyan-fg} {magenta-fg}Vibes{/magenta-fg} {gray-fg}v1.0{/gray-fg} {yellow-fg}Voice Browser{/yellow-fg}{/bold}{/center}\n{center}{gray-fg}github.com/paulpreibisch/agentvibes{/gray-fg} {white-fg}www.agentvibes.org{/white-fg}{/center}\n{center}{cyan-fg}[1-4]{/cyan-fg}Sort {cyan-fg}[/]{/cyan-fg}Search {cyan-fg}[F/X]{/cyan-fg}Favorites {cyan-fg}[Space]{/cyan-fg}Play {cyan-fg}[*]{/cyan-fg}Fav {cyan-fg}[I]{/cyan-fg}Install {cyan-fg}[Q]{/cyan-fg}Quit{/center}`,
|
|
517
251
|
tags: true,
|
|
518
252
|
style: { fg: 'white' }
|
|
519
253
|
});
|
|
520
254
|
|
|
521
|
-
const
|
|
522
|
-
top:
|
|
523
|
-
height: 4,
|
|
524
|
-
width: '100%',
|
|
525
|
-
content: `{center}{gray-fg}github.com/paulpreibisch/agentvibes{/gray-fg} {white-fg}www.agentvibes.org{/white-fg}{/center}\n{center}{red-fg}[T]{/red-fg}Tabs {cyan-fg}[1-6]{/cyan-fg}Sort {cyan-fg}[/]{/cyan-fg}Search {cyan-fg}[P]{/cyan-fg}Prompt {cyan-fg}[L]{/cyan-fg}Filter {cyan-fg}[F/X]{/cyan-fg}Fav {cyan-fg}[Space]{/cyan-fg}Play {green-fg}[+/*]{/green-fg}Up {red-fg}[-]{/red-fg}Down {cyan-fg}[I]{/cyan-fg}Install{/center}`,
|
|
526
|
-
tags: true,
|
|
527
|
-
padding: 0,
|
|
528
|
-
border: { type: 'line', fg: 'gray' },
|
|
529
|
-
style: {
|
|
530
|
-
bg: 'black',
|
|
531
|
-
fg: 'white',
|
|
532
|
-
border: { bg: 'black' }
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Tab bar
|
|
537
|
-
this.tabBar = blessed.box({
|
|
538
|
-
top: 5,
|
|
539
|
-
height: 1,
|
|
540
|
-
width: '100%',
|
|
541
|
-
tags: true,
|
|
542
|
-
mouse: true,
|
|
543
|
-
clickable: true,
|
|
544
|
-
style: { fg: 'white', bg: 'black' }
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
// Voices Tab Content
|
|
548
|
-
this.voicesContainer = blessed.box({
|
|
549
|
-
top: 6,
|
|
550
|
-
left: 0,
|
|
551
|
-
width: '100%',
|
|
552
|
-
height: '100%-11',
|
|
553
|
-
hidden: false
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
this.tableHeader = blessed.box({
|
|
557
|
-
top: 0,
|
|
255
|
+
const tableHeader = blessed.box({
|
|
256
|
+
top: 3,
|
|
558
257
|
left: 0,
|
|
559
258
|
height: 1,
|
|
560
259
|
width: '70%',
|
|
561
|
-
content: ` ID G Name
|
|
562
|
-
style: { fg: 'cyan', bold: true }
|
|
563
|
-
mouse: true,
|
|
564
|
-
clickable: true
|
|
260
|
+
content: ` ID G Name Model `,
|
|
261
|
+
style: { fg: 'cyan', bold: true }
|
|
565
262
|
});
|
|
566
263
|
|
|
567
264
|
this.list = blessed.list({
|
|
568
|
-
top:
|
|
265
|
+
top: 4,
|
|
569
266
|
left: 0,
|
|
570
267
|
width: '70%',
|
|
571
|
-
height: '100%-
|
|
268
|
+
height: '100%-8',
|
|
572
269
|
keys: true,
|
|
573
270
|
vi: true,
|
|
574
|
-
mouse:
|
|
271
|
+
mouse: false,
|
|
575
272
|
tags: true,
|
|
576
273
|
style: {
|
|
577
274
|
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
@@ -584,77 +281,22 @@ class AgentVibesVoiceBrowser {
|
|
|
584
281
|
});
|
|
585
282
|
|
|
586
283
|
this.infoPanel = blessed.box({
|
|
587
|
-
top:
|
|
284
|
+
top: 3,
|
|
588
285
|
left: '70%',
|
|
589
286
|
width: '30%',
|
|
590
|
-
height: '100
|
|
287
|
+
height: '100%-7',
|
|
591
288
|
tags: true,
|
|
592
289
|
border: { type: 'line', fg: 'cyan' },
|
|
593
290
|
label: ' Voice Info ',
|
|
594
291
|
scrollable: true,
|
|
595
|
-
alwaysScroll: true,
|
|
596
|
-
mouse: true,
|
|
597
|
-
keys: true,
|
|
598
|
-
vi: true,
|
|
599
292
|
style: {
|
|
600
293
|
border: { fg: 'cyan' },
|
|
601
294
|
label: { fg: 'gray' }
|
|
602
295
|
}
|
|
603
296
|
});
|
|
604
297
|
|
|
605
|
-
this.voicesContainer.append(this.tableHeader);
|
|
606
|
-
this.voicesContainer.append(this.list);
|
|
607
|
-
this.voicesContainer.append(this.infoPanel);
|
|
608
|
-
|
|
609
|
-
// Music Tab Content
|
|
610
|
-
this.musicContainer = blessed.box({
|
|
611
|
-
top: 6,
|
|
612
|
-
left: 0,
|
|
613
|
-
width: '100%',
|
|
614
|
-
height: '100%-11',
|
|
615
|
-
hidden: true
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
this.musicList = blessed.list({
|
|
619
|
-
top: 0,
|
|
620
|
-
left: 0,
|
|
621
|
-
width: '70%',
|
|
622
|
-
height: '100%',
|
|
623
|
-
keys: true,
|
|
624
|
-
vi: true,
|
|
625
|
-
mouse: true,
|
|
626
|
-
tags: true,
|
|
627
|
-
style: {
|
|
628
|
-
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
629
|
-
item: { fg: 'white' },
|
|
630
|
-
border: { fg: 'cyan' },
|
|
631
|
-
label: { fg: 'gray' }
|
|
632
|
-
},
|
|
633
|
-
border: { type: 'line', fg: 'cyan' },
|
|
634
|
-
label: ` Background Music (${this.musicTracks.length} tracks) `
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
this.musicInfo = blessed.box({
|
|
638
|
-
top: 0,
|
|
639
|
-
left: '70%',
|
|
640
|
-
width: '30%',
|
|
641
|
-
height: '100%',
|
|
642
|
-
tags: true,
|
|
643
|
-
border: { type: 'line', fg: 'cyan' },
|
|
644
|
-
label: ' Track Info ',
|
|
645
|
-
content: '',
|
|
646
|
-
padding: 1,
|
|
647
|
-
style: {
|
|
648
|
-
border: { fg: 'cyan' },
|
|
649
|
-
label: { fg: 'gray' }
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
this.musicContainer.append(this.musicList);
|
|
654
|
-
this.musicContainer.append(this.musicInfo);
|
|
655
|
-
|
|
656
298
|
this.statusBar = blessed.box({
|
|
657
|
-
bottom:
|
|
299
|
+
bottom: 3,
|
|
658
300
|
height: 1,
|
|
659
301
|
width: '100%',
|
|
660
302
|
content: 'Ready',
|
|
@@ -663,40 +305,20 @@ class AgentVibesVoiceBrowser {
|
|
|
663
305
|
});
|
|
664
306
|
|
|
665
307
|
this.helpBar = blessed.box({
|
|
666
|
-
bottom: 1,
|
|
667
|
-
height: 3,
|
|
668
|
-
width: '100%',
|
|
669
|
-
content: '{cyan-fg}[1-6]{/cyan-fg}Sort {cyan-fg}[/]{/cyan-fg}Search {cyan-fg}[P]{/cyan-fg}Prompt {cyan-fg}[L]{/cyan-fg}Filter {cyan-fg}[F/X]{/cyan-fg}Fav {cyan-fg}[Space]{/cyan-fg}Play {cyan-fg}[R]{/cyan-fg}Reverb {green-fg}[+/*]{/green-fg}Up {red-fg}[-]{/red-fg}Down {cyan-fg}[I]{/cyan-fg}Install',
|
|
670
|
-
tags: true,
|
|
671
|
-
padding: 0,
|
|
672
|
-
border: { type: 'line', fg: 'gray' },
|
|
673
|
-
style: {
|
|
674
|
-
bg: 'black',
|
|
675
|
-
fg: 'white',
|
|
676
|
-
border: { bg: 'black' }
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
this.githubMessage = blessed.box({
|
|
681
308
|
bottom: 0,
|
|
682
|
-
height:
|
|
309
|
+
height: 3,
|
|
683
310
|
width: '100%',
|
|
684
|
-
content: '{center}{gray-fg}Please consider giving us a GitHub star
|
|
311
|
+
content: '{cyan-fg}[1-4]{/cyan-fg}Sort {cyan-fg}[/]{/cyan-fg}Search {cyan-fg}[F/X]{/cyan-fg}Favorites {cyan-fg}[Space]{/cyan-fg}Play {cyan-fg}[*]{/cyan-fg}Toggle★ {cyan-fg}[I]{/cyan-fg}Install {cyan-fg}[E]{/cyan-fg}Export\n{center}{gray-fg}Please consider giving us a GitHub star ⭐ {/gray-fg}{yellow-fg}github.com/paulpreibisch/agentvibes{/yellow-fg}{/center}',
|
|
685
312
|
tags: true,
|
|
686
|
-
style: {
|
|
313
|
+
style: { bg: 'black' }
|
|
687
314
|
});
|
|
688
315
|
|
|
689
316
|
this.screen.append(title);
|
|
690
|
-
this.screen.append(
|
|
691
|
-
this.screen.append(this.
|
|
692
|
-
this.screen.append(this.
|
|
693
|
-
this.screen.append(this.musicContainer);
|
|
317
|
+
this.screen.append(tableHeader);
|
|
318
|
+
this.screen.append(this.list);
|
|
319
|
+
this.screen.append(this.infoPanel);
|
|
694
320
|
this.screen.append(this.statusBar);
|
|
695
321
|
this.screen.append(this.helpBar);
|
|
696
|
-
this.screen.append(this.githubMessage);
|
|
697
|
-
|
|
698
|
-
this.updateTabBar();
|
|
699
|
-
this.updateMusicList();
|
|
700
322
|
|
|
701
323
|
this.updateList();
|
|
702
324
|
this.list.focus();
|
|
@@ -709,8 +331,8 @@ class AgentVibesVoiceBrowser {
|
|
|
709
331
|
this.list.setItems(items);
|
|
710
332
|
this.list.select(Math.min(this.currentRow, items.length - 1));
|
|
711
333
|
|
|
712
|
-
const modeLabel = this.favoritesOnly ? '
|
|
713
|
-
this.list.setLabel(`${modeLabel}(${this.filteredData.length}) -
|
|
334
|
+
const modeLabel = this.favoritesOnly ? ' ⭐ Favorites ' : ' Voices ';
|
|
335
|
+
this.list.setLabel(`${modeLabel}(${this.filteredData.length}) - Sort: ${this.sortColumn} ${this.sortAsc ? '↑' : '↓'} `);
|
|
714
336
|
this.updateInfo();
|
|
715
337
|
}
|
|
716
338
|
|
|
@@ -721,8 +343,7 @@ class AgentVibesVoiceBrowser {
|
|
|
721
343
|
const row = this.filteredData[idx];
|
|
722
344
|
let info = `{bold}${row.type === 'curated' ? row.name : 'Speaker ' + row.id}{/bold}\n`;
|
|
723
345
|
info += `{gray-fg}${'─'.repeat(20)}{/gray-fg}\n\n`;
|
|
724
|
-
if (this.
|
|
725
|
-
else if (this.thumbsDown.has(row.id)) info += '{red-fg}- Thumbs Down{/red-fg}\n\n';
|
|
346
|
+
if (this.favorites.has(row.id)) info += '{yellow-fg}⭐ Favorite{/yellow-fg}\n\n';
|
|
726
347
|
info += `{cyan-fg}ID:{/cyan-fg} ${row.id}\n`;
|
|
727
348
|
|
|
728
349
|
// Color gender value: blue for male, pink for female
|
|
@@ -730,8 +351,6 @@ class AgentVibesVoiceBrowser {
|
|
|
730
351
|
info += `{cyan-fg}Gender:{/cyan-fg} {${genderColor}}${row.gender}{/${genderColor}}\n`;
|
|
731
352
|
|
|
732
353
|
info += `{cyan-fg}Voice:{/cyan-fg} ${row.name}\n`;
|
|
733
|
-
info += `{cyan-fg}Provider:{/cyan-fg} {green-fg}${row.provider}{/green-fg}\n`;
|
|
734
|
-
info += `{cyan-fg}Language:{/cyan-fg} ${row.language}\n`;
|
|
735
354
|
|
|
736
355
|
// Color model in yellow
|
|
737
356
|
info += `{cyan-fg}Model:{/cyan-fg} {yellow-fg}${row.model}{/yellow-fg}\n`;
|
|
@@ -745,177 +364,26 @@ class AgentVibesVoiceBrowser {
|
|
|
745
364
|
info += `\n{gray-fg}Sample:{/gray-fg}\n{green-fg}"${voiceSample}"{/green-fg}\n`;
|
|
746
365
|
|
|
747
366
|
info += `\n{cyan-fg}Position:{/cyan-fg} ${idx + 1}/${this.filteredData.length}\n`;
|
|
748
|
-
info += `{
|
|
749
|
-
info += `{green-fg}[I]
|
|
367
|
+
info += `{cyan-fg}Favorites:{/cyan-fg} ${this.favorites.size}\n\n`;
|
|
368
|
+
info += `{green-fg}Press [I] to install this voice{/green-fg}`;
|
|
750
369
|
|
|
751
370
|
this.infoPanel.setContent(info);
|
|
752
371
|
this.screen.render();
|
|
753
372
|
}
|
|
754
373
|
|
|
755
|
-
updateTabBar() {
|
|
756
|
-
const voicesTab = this.currentTab === 'voices'
|
|
757
|
-
? '{black-bg}{magenta-fg}[V]{/magenta-fg} {cyan-fg}Voices{/cyan-fg}{/black-bg}'
|
|
758
|
-
: '{gray-fg}[V] Voices{/gray-fg}';
|
|
759
|
-
const musicTab = this.currentTab === 'music'
|
|
760
|
-
? '{black-bg}{red-fg}[B]{/red-fg} {cyan-fg}🎶 Background Music{/cyan-fg}{/black-bg}'
|
|
761
|
-
: '{gray-fg}[B] 🎶 Background Music{/gray-fg}';
|
|
762
|
-
|
|
763
|
-
this.tabBar.setContent(` ${voicesTab} │ ${musicTab}`);
|
|
764
|
-
this.screen.render();
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
switchTab(tab) {
|
|
768
|
-
this.currentTab = tab;
|
|
769
|
-
|
|
770
|
-
if (tab === 'voices') {
|
|
771
|
-
this.voicesContainer.show();
|
|
772
|
-
this.musicContainer.hide();
|
|
773
|
-
this.list.focus();
|
|
774
|
-
} else {
|
|
775
|
-
this.voicesContainer.hide();
|
|
776
|
-
this.musicContainer.show();
|
|
777
|
-
this.musicList.focus();
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
this.updateTabBar();
|
|
781
|
-
this.screen.render();
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
updateMusicList() {
|
|
785
|
-
const items = this.musicTracks.map(track => {
|
|
786
|
-
const isCurrent = track.file === this.currentMusicSelection;
|
|
787
|
-
const isFavorite = this.musicFavorites.has(track.file);
|
|
788
|
-
const isEnabled = this.musicEnabled ? '🔊' : '🔇';
|
|
789
|
-
const marker = isCurrent ? `{cyan-fg}▶{/cyan-fg}` : ' ';
|
|
790
|
-
const favMarker = isFavorite ? '+' : ' ';
|
|
791
|
-
return `${marker}${favMarker} ${track.name} ${isCurrent ? isEnabled : ''}`;
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
this.musicList.setItems(items);
|
|
795
|
-
|
|
796
|
-
// Update music info
|
|
797
|
-
this.updateMusicInfo();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
updateMusicInfo() {
|
|
801
|
-
const enabledText = this.musicEnabled ? '{green-fg}Enabled{/green-fg}' : '{red-fg}Disabled{/red-fg}';
|
|
802
|
-
const currentTrack = this.currentMusicSelection
|
|
803
|
-
? this.musicTracks.find(t => t.file === this.currentMusicSelection)?.name || 'None'
|
|
804
|
-
: 'None';
|
|
805
|
-
|
|
806
|
-
let content = '{cyan-fg}{bold}Background Music{/bold}{/cyan-fg}\n\n';
|
|
807
|
-
content += `Status: ${enabledText}\n\n`;
|
|
808
|
-
content += `Current Track:\n{yellow-fg}${currentTrack}{/yellow-fg}\n\n`;
|
|
809
|
-
content += '{gray-fg}Controls:{/gray-fg}\n';
|
|
810
|
-
content += '{cyan-fg}Space{/cyan-fg} - Preview track\n';
|
|
811
|
-
content += '{cyan-fg}Enter{/cyan-fg} - Select track\n';
|
|
812
|
-
content += '{cyan-fg}F/*{/cyan-fg} - Favorite\n';
|
|
813
|
-
content += '{cyan-fg}M{/cyan-fg} - Toggle on/off\n';
|
|
814
|
-
content += '{cyan-fg}R{/cyan-fg} - Toggle reverb\n';
|
|
815
|
-
content += '{cyan-fg}T{/cyan-fg} - Switch tabs\n\n';
|
|
816
|
-
content += `{gray-fg}Total Tracks: {/gray-fg}{white-fg}${this.musicTracks.length}{/white-fg}`;
|
|
817
|
-
|
|
818
|
-
this.musicInfo.setContent(content);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
374
|
setupKeys() {
|
|
822
375
|
this.screen.key(['q', 'Q', 'C-c'], () => this.exit());
|
|
823
376
|
|
|
824
|
-
// Tab switching
|
|
825
|
-
this.screen.key(['t', 'T'], () => {
|
|
826
|
-
const newTab = this.currentTab === 'voices' ? 'music' : 'voices';
|
|
827
|
-
this.switchTab(newTab);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
// Tab bar click handling
|
|
831
|
-
this.tabBar.on('click', (data) => {
|
|
832
|
-
const x = data.x;
|
|
833
|
-
// "[V] Voices" is at position 2-12 (approx)
|
|
834
|
-
// "[B] 🎶 Background Music" starts around position 15+
|
|
835
|
-
if (x < 15) {
|
|
836
|
-
// Clicked on Voices tab
|
|
837
|
-
if (this.currentTab !== 'voices') {
|
|
838
|
-
this.switchTab('voices');
|
|
839
|
-
}
|
|
840
|
-
} else {
|
|
841
|
-
// Clicked on Background Music tab
|
|
842
|
-
if (this.currentTab !== 'music') {
|
|
843
|
-
this.switchTab('music');
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
|
|
848
377
|
// Listen to selection changes (blessed handles arrow keys automatically)
|
|
849
378
|
this.list.on('select', () => {
|
|
850
379
|
this.updateInfo();
|
|
851
380
|
});
|
|
852
381
|
|
|
853
|
-
// Double-click to play voice
|
|
854
|
-
let lastClickTime = 0;
|
|
855
|
-
this.list.on('click', async () => {
|
|
856
|
-
const now = Date.now();
|
|
857
|
-
if (now - lastClickTime < 400) {
|
|
858
|
-
// Double-click detected
|
|
859
|
-
const row = this.filteredData[this.list.selected];
|
|
860
|
-
if (row) await this.playSample(row);
|
|
861
|
-
lastClickTime = 0; // Reset to prevent triple-click
|
|
862
|
-
} else {
|
|
863
|
-
lastClickTime = now;
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
// Double-click column header to sort
|
|
868
|
-
let lastHeaderClickTime = 0;
|
|
869
|
-
let lastHeaderClickX = 0;
|
|
870
|
-
this.tableHeader.on('click', (data) => {
|
|
871
|
-
const now = Date.now();
|
|
872
|
-
const x = data.x;
|
|
873
|
-
|
|
874
|
-
if (now - lastHeaderClickTime < 400 && Math.abs(x - lastHeaderClickX) < 3) {
|
|
875
|
-
// Double-click detected on same column
|
|
876
|
-
let newSortColumn = this.sortColumn;
|
|
877
|
-
|
|
878
|
-
// Map x position to column (accounting for border offset)
|
|
879
|
-
// " ID G Name Provider Lang Model "
|
|
880
|
-
if (x < 8) {
|
|
881
|
-
newSortColumn = 'id';
|
|
882
|
-
} else if (x < 11) {
|
|
883
|
-
newSortColumn = 'gender';
|
|
884
|
-
} else if (x < 25) {
|
|
885
|
-
newSortColumn = 'name';
|
|
886
|
-
} else if (x < 34) {
|
|
887
|
-
newSortColumn = 'provider';
|
|
888
|
-
} else if (x < 41) {
|
|
889
|
-
newSortColumn = 'language';
|
|
890
|
-
} else {
|
|
891
|
-
newSortColumn = 'model';
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Toggle sort direction if same column, otherwise ascending
|
|
895
|
-
if (newSortColumn === this.sortColumn) {
|
|
896
|
-
this.sortAsc = !this.sortAsc;
|
|
897
|
-
} else {
|
|
898
|
-
this.sortColumn = newSortColumn;
|
|
899
|
-
this.sortAsc = true;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
this.applyFilter();
|
|
903
|
-
this.updateList();
|
|
904
|
-
|
|
905
|
-
lastHeaderClickTime = 0; // Reset to prevent triple-click
|
|
906
|
-
} else {
|
|
907
|
-
lastHeaderClickTime = now;
|
|
908
|
-
lastHeaderClickX = x;
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
|
|
912
382
|
// Sorting
|
|
913
383
|
this.screen.key(['1'], () => { this.sortColumn = 'id'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
914
384
|
this.screen.key(['2'], () => { this.sortColumn = 'gender'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
915
385
|
this.screen.key(['3'], () => { this.sortColumn = 'name'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
916
|
-
this.screen.key(['4'], () => { this.sortColumn = '
|
|
917
|
-
this.screen.key(['5'], () => { this.sortColumn = 'language'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
918
|
-
this.screen.key(['6'], () => { this.sortColumn = 'model'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
386
|
+
this.screen.key(['4'], () => { this.sortColumn = 'model'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
919
387
|
|
|
920
388
|
// Search
|
|
921
389
|
this.screen.key(['/'], () => this.showSearch());
|
|
@@ -926,41 +394,16 @@ class AgentVibesVoiceBrowser {
|
|
|
926
394
|
if (row) await this.playSample(row);
|
|
927
395
|
});
|
|
928
396
|
|
|
929
|
-
//
|
|
930
|
-
this.list.key(['
|
|
931
|
-
if (this.currentTab === 'voices') {
|
|
932
|
-
await this.toggleReverb();
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
// Thumbs up (* or +) — toggle, clears thumbs down
|
|
937
|
-
this.list.key(['*', '+'], async () => {
|
|
938
|
-
const row = this.filteredData[this.list.selected];
|
|
939
|
-
if (row) {
|
|
940
|
-
if (this.thumbsUp.has(row.id)) {
|
|
941
|
-
this.thumbsUp.delete(row.id);
|
|
942
|
-
this.statusBar.setContent('{yellow-fg}Removed thumbs up{/yellow-fg}');
|
|
943
|
-
} else {
|
|
944
|
-
this.thumbsUp.add(row.id);
|
|
945
|
-
this.thumbsDown.delete(row.id);
|
|
946
|
-
this.statusBar.setContent('{green-fg}Thumbs up +{/green-fg}');
|
|
947
|
-
}
|
|
948
|
-
await this.saveProgress();
|
|
949
|
-
this.updateList();
|
|
950
|
-
}
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
// Thumbs down (-) — toggle, clears thumbs up
|
|
954
|
-
this.list.key(['-'], async () => {
|
|
397
|
+
// Favorite
|
|
398
|
+
this.list.key(['*', '8'], async () => {
|
|
955
399
|
const row = this.filteredData[this.list.selected];
|
|
956
400
|
if (row) {
|
|
957
|
-
if (this.
|
|
958
|
-
this.
|
|
959
|
-
this.statusBar.setContent('{yellow-fg}Removed
|
|
401
|
+
if (this.favorites.has(row.id)) {
|
|
402
|
+
this.favorites.delete(row.id);
|
|
403
|
+
this.statusBar.setContent('{yellow-fg}Removed from favorites{/yellow-fg}');
|
|
960
404
|
} else {
|
|
961
|
-
this.
|
|
962
|
-
this.
|
|
963
|
-
this.statusBar.setContent('{red-fg}Thumbs down -{/red-fg}');
|
|
405
|
+
this.favorites.add(row.id);
|
|
406
|
+
this.statusBar.setContent('{yellow-fg}Added to favorites ⭐{/yellow-fg}');
|
|
964
407
|
}
|
|
965
408
|
await this.saveProgress();
|
|
966
409
|
this.updateList();
|
|
@@ -977,9 +420,9 @@ class AgentVibesVoiceBrowser {
|
|
|
977
420
|
this.updateList();
|
|
978
421
|
|
|
979
422
|
if (this.favoritesOnly) {
|
|
980
|
-
this.statusBar.setContent(`{
|
|
423
|
+
this.statusBar.setContent(`{yellow-fg}⭐ Showing ${this.filteredData.length} favorites - Press [F] or [X] to show all{/yellow-fg}`);
|
|
981
424
|
} else {
|
|
982
|
-
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter
|
|
425
|
+
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter favorites{/cyan-fg}`);
|
|
983
426
|
}
|
|
984
427
|
this.screen.render();
|
|
985
428
|
});
|
|
@@ -990,466 +433,13 @@ class AgentVibesVoiceBrowser {
|
|
|
990
433
|
this.favoritesOnly = false;
|
|
991
434
|
this.applyFilter();
|
|
992
435
|
this.updateList();
|
|
993
|
-
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter
|
|
436
|
+
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter favorites{/cyan-fg}`);
|
|
994
437
|
this.screen.render();
|
|
995
438
|
}
|
|
996
439
|
});
|
|
997
440
|
|
|
998
441
|
// Export
|
|
999
442
|
this.screen.key(['e', 'E'], () => this.exportFavorites());
|
|
1000
|
-
|
|
1001
|
-
// Navigation: Page Down
|
|
1002
|
-
this.list.key(['pagedown'], () => {
|
|
1003
|
-
const pageSize = Math.floor(this.list.height / 2);
|
|
1004
|
-
const newIndex = Math.min(this.list.selected + pageSize, this.filteredData.length - 1);
|
|
1005
|
-
this.list.select(newIndex);
|
|
1006
|
-
this.screen.render();
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
// Navigation: Page Up
|
|
1010
|
-
this.list.key(['pageup'], () => {
|
|
1011
|
-
const pageSize = Math.floor(this.list.height / 2);
|
|
1012
|
-
const newIndex = Math.max(this.list.selected - pageSize, 0);
|
|
1013
|
-
this.list.select(newIndex);
|
|
1014
|
-
this.screen.render();
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
// Navigation: Home (go to top)
|
|
1018
|
-
this.list.key(['home'], () => {
|
|
1019
|
-
this.list.select(0);
|
|
1020
|
-
this.screen.render();
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
// Navigation: End (go to bottom)
|
|
1024
|
-
this.list.key(['end'], () => {
|
|
1025
|
-
if (this.filteredData.length > 0) {
|
|
1026
|
-
this.list.select(this.filteredData.length - 1);
|
|
1027
|
-
this.screen.render();
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
// Provider filter toggle
|
|
1032
|
-
this.screen.key(['l', 'L'], () => this.showProviderFilter());
|
|
1033
|
-
|
|
1034
|
-
// Voice prompt — copy-pasteable AgentVibes instructions
|
|
1035
|
-
this.list.key(['p', 'P'], () => this.showVoicePrompt());
|
|
1036
|
-
|
|
1037
|
-
// Music tab controls
|
|
1038
|
-
this.musicList.key(['space'], async () => {
|
|
1039
|
-
if (this.currentTab !== 'music') return;
|
|
1040
|
-
const selected = this.musicList.selected;
|
|
1041
|
-
if (selected >= 0 && selected < this.musicTracks.length) {
|
|
1042
|
-
const selectedTrack = this.musicTracks[selected];
|
|
1043
|
-
|
|
1044
|
-
// If this track is already playing, stop it
|
|
1045
|
-
if (this.currentlyPlayingTrack && this.currentlyPlayingTrack.file === selectedTrack.file) {
|
|
1046
|
-
this.stopMusic();
|
|
1047
|
-
} else {
|
|
1048
|
-
// Otherwise, play the new track
|
|
1049
|
-
await this.previewMusic(selectedTrack);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
this.musicList.key(['enter'], async () => {
|
|
1055
|
-
if (this.currentTab !== 'music') return;
|
|
1056
|
-
const selected = this.musicList.selected;
|
|
1057
|
-
if (selected >= 0 && selected < this.musicTracks.length) {
|
|
1058
|
-
await this.selectMusic(this.musicTracks[selected]);
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
this.musicList.key(['m', 'M'], async () => {
|
|
1063
|
-
if (this.currentTab !== 'music') return;
|
|
1064
|
-
await this.toggleMusic();
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
this.musicList.key(['r', 'R'], async () => {
|
|
1068
|
-
if (this.currentTab !== 'music') return;
|
|
1069
|
-
await this.toggleReverb();
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
// Favorite music track (thumbs up)
|
|
1073
|
-
this.musicList.key(['f', 'F', '*', '+'], async () => {
|
|
1074
|
-
if (this.currentTab !== 'music') return;
|
|
1075
|
-
const selected = this.musicList.selected;
|
|
1076
|
-
if (selected >= 0 && selected < this.musicTracks.length) {
|
|
1077
|
-
const track = this.musicTracks[selected];
|
|
1078
|
-
if (this.musicFavorites.has(track.file)) {
|
|
1079
|
-
this.musicFavorites.delete(track.file);
|
|
1080
|
-
this.statusBar.setContent('{yellow-fg}Removed from favorites{/yellow-fg}');
|
|
1081
|
-
} else {
|
|
1082
|
-
this.musicFavorites.add(track.file);
|
|
1083
|
-
this.statusBar.setContent('{green-fg}Thumbs up +{/green-fg}');
|
|
1084
|
-
}
|
|
1085
|
-
await this.saveProgress();
|
|
1086
|
-
this.updateMusicList();
|
|
1087
|
-
this.screen.render();
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
showVoicePrompt() {
|
|
1093
|
-
const row = this.filteredData[this.list.selected];
|
|
1094
|
-
if (!row) return;
|
|
1095
|
-
|
|
1096
|
-
// Build copy-pasteable AgentVibes instructions per voice type
|
|
1097
|
-
let lines = [];
|
|
1098
|
-
let subtitle = '';
|
|
1099
|
-
|
|
1100
|
-
switch (row.type) {
|
|
1101
|
-
case 'curated': {
|
|
1102
|
-
const switchName = row.friendlyName || row.piperVoiceId || row.model;
|
|
1103
|
-
subtitle = `Piper curated voice`;
|
|
1104
|
-
lines = [
|
|
1105
|
-
`# Switch to: ${row.name}`,
|
|
1106
|
-
``,
|
|
1107
|
-
`# If piper is already your active provider:`,
|
|
1108
|
-
`/agent-vibes:switch ${switchName}`,
|
|
1109
|
-
``,
|
|
1110
|
-
`# If switching from another provider first:`,
|
|
1111
|
-
`/agent-vibes:provider switch piper`,
|
|
1112
|
-
`/agent-vibes:switch ${switchName}`,
|
|
1113
|
-
];
|
|
1114
|
-
break;
|
|
1115
|
-
}
|
|
1116
|
-
case 'libritts': {
|
|
1117
|
-
const speakerId = row.originalId;
|
|
1118
|
-
const safeName = row.name.replace(/\s+/g, '_');
|
|
1119
|
-
const modelFile = path.basename(CONFIG.MODEL_PATH, '.onnx');
|
|
1120
|
-
subtitle = `LibriTTS multi-speaker — speaker ID ${speakerId}`;
|
|
1121
|
-
lines = [
|
|
1122
|
-
`# Use LibriTTS Speaker ${speakerId}: ${row.name}`,
|
|
1123
|
-
``,
|
|
1124
|
-
`# Step 1 — Download the model (skip if already downloaded):`,
|
|
1125
|
-
`bash .claude/hooks/piper-voice-manager.sh download ${modelFile}`,
|
|
1126
|
-
``,
|
|
1127
|
-
`# Step 2 — Register speaker in piper-multispeaker-registry.sh:`,
|
|
1128
|
-
`# Add this line to the MULTISPEAKER_VOICES array:`,
|
|
1129
|
-
` "${safeName}:${modelFile}:${speakerId}:LibriTTS Speaker"`,
|
|
1130
|
-
``,
|
|
1131
|
-
`# Step 3 — Switch AgentVibes to this voice:`,
|
|
1132
|
-
`/agent-vibes:switch ${safeName}`,
|
|
1133
|
-
];
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
case 'macos': {
|
|
1137
|
-
subtitle = `macOS built-in voice`;
|
|
1138
|
-
lines = [
|
|
1139
|
-
`# Switch to macOS voice: ${row.name}`,
|
|
1140
|
-
``,
|
|
1141
|
-
`# Step 1 — Switch provider to macOS:`,
|
|
1142
|
-
`/agent-vibes:provider switch macos`,
|
|
1143
|
-
``,
|
|
1144
|
-
`# Step 2 — Switch to this voice:`,
|
|
1145
|
-
`/agent-vibes:switch ${row.name}`,
|
|
1146
|
-
];
|
|
1147
|
-
break;
|
|
1148
|
-
}
|
|
1149
|
-
case 'windows-sapi': {
|
|
1150
|
-
subtitle = `Windows SAPI built-in voice`;
|
|
1151
|
-
lines = [
|
|
1152
|
-
`# Switch to Windows SAPI voice: ${row.name}`,
|
|
1153
|
-
``,
|
|
1154
|
-
`# Step 1 — Switch provider to Windows SAPI:`,
|
|
1155
|
-
`/agent-vibes:provider switch windows-sapi`,
|
|
1156
|
-
``,
|
|
1157
|
-
`# Step 2 — Switch to this voice:`,
|
|
1158
|
-
`/agent-vibes:switch ${row.name}`,
|
|
1159
|
-
];
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
case 'soprano': {
|
|
1163
|
-
subtitle = `Soprano neural TTS — single voice`;
|
|
1164
|
-
lines = [
|
|
1165
|
-
`# Switch to Soprano TTS`,
|
|
1166
|
-
``,
|
|
1167
|
-
`/agent-vibes:provider switch soprano`,
|
|
1168
|
-
``,
|
|
1169
|
-
`# Soprano has one built-in voice — no voice selection needed.`,
|
|
1170
|
-
];
|
|
1171
|
-
break;
|
|
1172
|
-
}
|
|
1173
|
-
default: {
|
|
1174
|
-
subtitle = row.provider;
|
|
1175
|
-
lines = [
|
|
1176
|
-
`# Switch to: ${row.name}`,
|
|
1177
|
-
`/agent-vibes:switch ${row.name}`,
|
|
1178
|
-
];
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const promptText = lines.join('\n');
|
|
1183
|
-
const contentHeight = lines.length + 8;
|
|
1184
|
-
const boxHeight = Math.min(contentHeight, Math.floor(this.screen.height * 0.8));
|
|
1185
|
-
|
|
1186
|
-
const modal = blessed.box({
|
|
1187
|
-
parent: this.screen,
|
|
1188
|
-
top: 'center',
|
|
1189
|
-
left: 'center',
|
|
1190
|
-
width: 72,
|
|
1191
|
-
height: boxHeight,
|
|
1192
|
-
border: { type: 'line', fg: 'green' },
|
|
1193
|
-
label: ` [P] Prompt — ${row.name} `,
|
|
1194
|
-
tags: true,
|
|
1195
|
-
scrollable: true,
|
|
1196
|
-
alwaysScroll: true,
|
|
1197
|
-
keys: true,
|
|
1198
|
-
vi: true,
|
|
1199
|
-
mouse: true,
|
|
1200
|
-
padding: 1,
|
|
1201
|
-
style: {
|
|
1202
|
-
border: { fg: 'green' },
|
|
1203
|
-
bg: 'black',
|
|
1204
|
-
fg: 'white'
|
|
1205
|
-
}
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
let content = `{yellow-fg}{bold}${row.name}{/bold}{/yellow-fg} {gray-fg}${subtitle}{/gray-fg}\n\n`;
|
|
1209
|
-
content += `{gray-fg}Copy and paste these commands into your terminal or Claude session:{/gray-fg}\n\n`;
|
|
1210
|
-
content += `{green-fg}${lines.join('\n')}{/green-fg}\n\n`;
|
|
1211
|
-
content += `{gray-fg}─────────────────────────────────────────────────────────────{/gray-fg}\n`;
|
|
1212
|
-
content += `{gray-fg}[Esc/Q] Close [↑↓] Scroll{/gray-fg}`;
|
|
1213
|
-
|
|
1214
|
-
modal.setContent(content);
|
|
1215
|
-
|
|
1216
|
-
// Try to copy to clipboard (best-effort, silent on failure)
|
|
1217
|
-
const clipboardCmds = [
|
|
1218
|
-
['xclip', ['-selection', 'clipboard']],
|
|
1219
|
-
['xsel', ['--clipboard', '--input']],
|
|
1220
|
-
['pbcopy', []]
|
|
1221
|
-
];
|
|
1222
|
-
for (const [cmd, args] of clipboardCmds) {
|
|
1223
|
-
try {
|
|
1224
|
-
const proc = spawnSync('which', [cmd], { encoding: 'utf8', timeout: 500 });
|
|
1225
|
-
if (proc.status === 0) {
|
|
1226
|
-
const cp = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
1227
|
-
cp.stdin.write(promptText);
|
|
1228
|
-
cp.stdin.end();
|
|
1229
|
-
// Update status bar to let user know
|
|
1230
|
-
this.statusBar.setContent(`{green-fg}✓ Prompt copied to clipboard via ${cmd}{/green-fg}`);
|
|
1231
|
-
break;
|
|
1232
|
-
}
|
|
1233
|
-
} catch {
|
|
1234
|
-
// Silently skip
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
modal.key(['escape', 'q', 'Q'], () => {
|
|
1239
|
-
this.screen.remove(modal);
|
|
1240
|
-
this.list.focus();
|
|
1241
|
-
this.screen.render();
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
modal.focus();
|
|
1245
|
-
this.screen.render();
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
showProviderFilter() {
|
|
1249
|
-
// Get unique providers from tableData
|
|
1250
|
-
const providers = [...new Set(this.tableData.map(row => row.provider))].sort();
|
|
1251
|
-
|
|
1252
|
-
const menu = blessed.list({
|
|
1253
|
-
parent: this.screen,
|
|
1254
|
-
top: 'center',
|
|
1255
|
-
left: 'center',
|
|
1256
|
-
width: 40,
|
|
1257
|
-
height: Math.min(providers.length + 4, 15),
|
|
1258
|
-
border: { type: 'line', fg: 'cyan' },
|
|
1259
|
-
label: ' Filter by Provider ',
|
|
1260
|
-
keys: true,
|
|
1261
|
-
vi: true,
|
|
1262
|
-
mouse: true,
|
|
1263
|
-
style: {
|
|
1264
|
-
selected: { bg: 'cyan', fg: 'black' },
|
|
1265
|
-
border: { fg: 'cyan' }
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
const items = ['All Providers', ...providers];
|
|
1270
|
-
menu.setItems(items);
|
|
1271
|
-
|
|
1272
|
-
// Select current filter
|
|
1273
|
-
if (this.providerFilter) {
|
|
1274
|
-
const index = items.indexOf(this.providerFilter);
|
|
1275
|
-
if (index >= 0) menu.select(index);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
menu.on('select', (item, index) => {
|
|
1279
|
-
if (index === 0) {
|
|
1280
|
-
// All Providers
|
|
1281
|
-
this.providerFilter = null;
|
|
1282
|
-
this.statusBar.setContent(`{cyan-fg}Showing all providers - Press [P] to filter{/cyan-fg}`);
|
|
1283
|
-
} else {
|
|
1284
|
-
// Specific provider
|
|
1285
|
-
this.providerFilter = item.getText();
|
|
1286
|
-
this.statusBar.setContent(`{cyan-fg}Showing ${this.providerFilter} only - Press [P] to change{/cyan-fg}`);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
this.applyFilter();
|
|
1290
|
-
this.updateList();
|
|
1291
|
-
this.screen.remove(menu);
|
|
1292
|
-
this.list.focus();
|
|
1293
|
-
this.screen.render();
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
menu.key(['escape'], () => {
|
|
1297
|
-
this.screen.remove(menu);
|
|
1298
|
-
this.list.focus();
|
|
1299
|
-
this.screen.render();
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
menu.focus();
|
|
1303
|
-
this.screen.render();
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
stopMusic() {
|
|
1307
|
-
// Kill existing audio process if any
|
|
1308
|
-
if (this.currentAudioProcess) {
|
|
1309
|
-
try {
|
|
1310
|
-
this.currentAudioProcess.kill('SIGKILL');
|
|
1311
|
-
this.currentAudioProcess = null;
|
|
1312
|
-
this.currentlyPlayingTrack = null;
|
|
1313
|
-
} catch (error) {}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
this.statusBar.setContent(`{yellow-fg}⏹ Stopped playback{/yellow-fg}`);
|
|
1317
|
-
this.screen.render();
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
async previewMusic(track) {
|
|
1321
|
-
// Kill existing audio process if any
|
|
1322
|
-
if (this.currentAudioProcess) {
|
|
1323
|
-
try {
|
|
1324
|
-
this.currentAudioProcess.kill('SIGKILL');
|
|
1325
|
-
this.currentAudioProcess = null;
|
|
1326
|
-
} catch (error) {}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const trackPath = track.path;
|
|
1330
|
-
this.currentlyPlayingTrack = track;
|
|
1331
|
-
|
|
1332
|
-
this.statusBar.setContent(`{cyan-fg}▶ Playing: ${track.name}...{/cyan-fg}`);
|
|
1333
|
-
this.screen.render();
|
|
1334
|
-
|
|
1335
|
-
// Try different audio players
|
|
1336
|
-
const players = [
|
|
1337
|
-
{ cmd: 'ffplay', args: ['-nodisp', '-autoexit', '-t', '15', trackPath] },
|
|
1338
|
-
{ cmd: 'mpg123', args: ['-q', '--loop', '1', trackPath] },
|
|
1339
|
-
{ cmd: 'afplay', args: [trackPath] }
|
|
1340
|
-
];
|
|
1341
|
-
|
|
1342
|
-
for (const player of players) {
|
|
1343
|
-
try {
|
|
1344
|
-
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1345
|
-
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1346
|
-
|
|
1347
|
-
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
1348
|
-
this.currentAudioProcess = audioProcess;
|
|
1349
|
-
|
|
1350
|
-
audioProcess.on('close', () => {
|
|
1351
|
-
if (this.currentAudioProcess === audioProcess) {
|
|
1352
|
-
this.currentAudioProcess = null;
|
|
1353
|
-
this.currentlyPlayingTrack = null;
|
|
1354
|
-
}
|
|
1355
|
-
this.statusBar.setContent(`{green-fg}✓ Playback complete{/green-fg}`);
|
|
1356
|
-
this.screen.render();
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
audioProcess.on('error', (err) => {
|
|
1360
|
-
if (this.currentAudioProcess === audioProcess) {
|
|
1361
|
-
this.currentAudioProcess = null;
|
|
1362
|
-
this.currentlyPlayingTrack = null;
|
|
1363
|
-
}
|
|
1364
|
-
this.statusBar.setContent(`{red-fg}✗ Error playing track{/red-fg}`);
|
|
1365
|
-
this.screen.render();
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
break;
|
|
1369
|
-
} catch (error) {
|
|
1370
|
-
continue;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
async selectMusic(track) {
|
|
1376
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
1377
|
-
const configDir = path.join(homeDir, '.claude', 'config');
|
|
1378
|
-
const musicConfigFile = path.join(configDir, 'background-music.txt');
|
|
1379
|
-
|
|
1380
|
-
try {
|
|
1381
|
-
// Ensure config directory exists
|
|
1382
|
-
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
1383
|
-
|
|
1384
|
-
await fs.writeFile(musicConfigFile, track.file, { mode: 0o600 });
|
|
1385
|
-
this.currentMusicSelection = track.file;
|
|
1386
|
-
this.updateMusicList();
|
|
1387
|
-
this.statusBar.setContent(`{green-fg}✓ Selected: ${track.name}{/green-fg}`);
|
|
1388
|
-
this.screen.render();
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
this.statusBar.setContent(`{red-fg}✗ Error selecting track: ${error.message}{/red-fg}`);
|
|
1391
|
-
this.screen.render();
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
async toggleMusic() {
|
|
1396
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
1397
|
-
const configDir = path.join(homeDir, '.claude', 'config');
|
|
1398
|
-
const musicEnabledFile = path.join(configDir, 'background-music-enabled.txt');
|
|
1399
|
-
|
|
1400
|
-
try {
|
|
1401
|
-
// Ensure config directory exists
|
|
1402
|
-
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
1403
|
-
|
|
1404
|
-
this.musicEnabled = !this.musicEnabled;
|
|
1405
|
-
await fs.writeFile(musicEnabledFile, this.musicEnabled ? 'true' : 'false', { mode: 0o600 });
|
|
1406
|
-
this.updateMusicList();
|
|
1407
|
-
this.updateMusicInfo();
|
|
1408
|
-
|
|
1409
|
-
const status = this.musicEnabled ? 'Enabled' : 'Disabled';
|
|
1410
|
-
this.statusBar.setContent(`{green-fg}✓ Background Music ${status}{/green-fg}`);
|
|
1411
|
-
this.screen.render();
|
|
1412
|
-
} catch (error) {
|
|
1413
|
-
this.statusBar.setContent(`{red-fg}✗ Error toggling music: ${error.message}{/red-fg}`);
|
|
1414
|
-
this.screen.render();
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
async toggleReverb() {
|
|
1419
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
1420
|
-
const configDir = path.join(homeDir, '.claude', 'config');
|
|
1421
|
-
const audioEffectsPath = path.join(configDir, 'audio-effects.cfg');
|
|
1422
|
-
|
|
1423
|
-
try {
|
|
1424
|
-
// Ensure config directory exists
|
|
1425
|
-
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
1426
|
-
|
|
1427
|
-
// Read current reverb setting
|
|
1428
|
-
let content = '';
|
|
1429
|
-
try {
|
|
1430
|
-
content = await fs.readFile(audioEffectsPath, 'utf8');
|
|
1431
|
-
} catch {
|
|
1432
|
-
content = 'REVERB_ENABLED=false\nREVERB_LEVEL=medium\n';
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Toggle reverb
|
|
1436
|
-
const currentEnabled = content.includes('REVERB_ENABLED=true');
|
|
1437
|
-
const newEnabled = !currentEnabled;
|
|
1438
|
-
|
|
1439
|
-
content = content.replace(
|
|
1440
|
-
/REVERB_ENABLED=(true|false)/,
|
|
1441
|
-
`REVERB_ENABLED=${newEnabled}`
|
|
1442
|
-
);
|
|
1443
|
-
|
|
1444
|
-
await fs.writeFile(audioEffectsPath, content, { mode: 0o600 });
|
|
1445
|
-
|
|
1446
|
-
const status = newEnabled ? 'Enabled' : 'Disabled';
|
|
1447
|
-
this.statusBar.setContent(`{green-fg}✓ Reverb ${status}{/green-fg}`);
|
|
1448
|
-
this.screen.render();
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
this.statusBar.setContent(`{red-fg}✗ Error toggling reverb: ${error.message}{/red-fg}`);
|
|
1451
|
-
this.screen.render();
|
|
1452
|
-
}
|
|
1453
443
|
}
|
|
1454
444
|
|
|
1455
445
|
showSearch() {
|
|
@@ -1484,23 +474,6 @@ class AgentVibesVoiceBrowser {
|
|
|
1484
474
|
this.screen.render();
|
|
1485
475
|
}
|
|
1486
476
|
|
|
1487
|
-
_getActiveProvider() {
|
|
1488
|
-
const remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
|
|
1489
|
-
try {
|
|
1490
|
-
const providerPaths = [
|
|
1491
|
-
path.join(process.cwd(), '.claude', 'tts-provider.txt'),
|
|
1492
|
-
path.join(os.homedir(), '.claude', 'tts-provider.txt'),
|
|
1493
|
-
];
|
|
1494
|
-
for (const p of providerPaths) {
|
|
1495
|
-
if (fsSync.existsSync(p)) {
|
|
1496
|
-
const provider = fsSync.readFileSync(p, 'utf8').trim();
|
|
1497
|
-
if (provider) return { provider, isRemote: remoteProviders.includes(provider) };
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
} catch {}
|
|
1501
|
-
return { provider: 'piper', isRemote: false };
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
477
|
async playSample(row) {
|
|
1505
478
|
if (this.currentAudioProcess) {
|
|
1506
479
|
try {
|
|
@@ -1517,206 +490,6 @@ class AgentVibesVoiceBrowser {
|
|
|
1517
490
|
// Use voice-specific sample text
|
|
1518
491
|
const sampleText = row.sampleText || this.sampleText;
|
|
1519
492
|
|
|
1520
|
-
// Route through remote provider if active
|
|
1521
|
-
const { isRemote } = this._getActiveProvider();
|
|
1522
|
-
if (isRemote) {
|
|
1523
|
-
return await this._playRemote(row, sampleText);
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// Handle different providers
|
|
1527
|
-
switch (row.type) {
|
|
1528
|
-
case 'macos':
|
|
1529
|
-
return await this.playMacOSVoice(row, sampleText);
|
|
1530
|
-
case 'windows-sapi':
|
|
1531
|
-
return await this.playWindowsSAPIVoice(row, sampleText);
|
|
1532
|
-
case 'soprano':
|
|
1533
|
-
return await this.playSopranoVoice(row, sampleText);
|
|
1534
|
-
default:
|
|
1535
|
-
return await this.playPiperVoice(row, sampleText);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
async _playRemote(row, sampleText) {
|
|
1540
|
-
// Build voice ID for play-tts.sh
|
|
1541
|
-
let voiceId;
|
|
1542
|
-
if (row.type === 'curated') {
|
|
1543
|
-
voiceId = row.piperVoiceId || row.model;
|
|
1544
|
-
} else {
|
|
1545
|
-
// LibriTTS multi-speaker: pass as model::SpeakerName-ID
|
|
1546
|
-
voiceId = `en_US-libritts-high::${row.name}-${row.id}`;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1550
|
-
try {
|
|
1551
|
-
let proc;
|
|
1552
|
-
if (isWindows) {
|
|
1553
|
-
const playTts = path.join(__dirname, '..', '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
1554
|
-
proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, sampleText, voiceId], {
|
|
1555
|
-
stdio: 'ignore', detached: false, windowsHide: true,
|
|
1556
|
-
});
|
|
1557
|
-
} else {
|
|
1558
|
-
const playTts = path.join(__dirname, '..', '.claude', 'hooks', 'play-tts.sh');
|
|
1559
|
-
proc = spawn('bash', [playTts, sampleText, voiceId], {
|
|
1560
|
-
stdio: 'ignore', detached: true,
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
this.currentAudioProcess = proc;
|
|
1564
|
-
|
|
1565
|
-
this.statusBar.setContent(`{cyan-fg}Playing ${row.name} (remote)...{/cyan-fg}`);
|
|
1566
|
-
this.screen.render();
|
|
1567
|
-
|
|
1568
|
-
proc.on('close', () => {
|
|
1569
|
-
if (this.currentAudioProcess === proc) this.currentAudioProcess = null;
|
|
1570
|
-
this.statusBar.setContent(`{green-fg}✓ Played ${row.name}{/green-fg}`);
|
|
1571
|
-
this.screen.render();
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
proc.on('error', () => {
|
|
1575
|
-
if (this.currentAudioProcess === proc) this.currentAudioProcess = null;
|
|
1576
|
-
this.statusBar.setContent(`{red-fg}✗ Remote preview failed{/red-fg}`);
|
|
1577
|
-
this.screen.render();
|
|
1578
|
-
});
|
|
1579
|
-
} catch (error) {
|
|
1580
|
-
this.statusBar.setContent(`{red-fg}✗ Error: ${error.message}{/red-fg}`);
|
|
1581
|
-
this.screen.render();
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
async playMacOSVoice(row, sampleText) {
|
|
1586
|
-
try {
|
|
1587
|
-
const process = spawn('say', ['-v', row.name, sampleText], { stdio: 'ignore' });
|
|
1588
|
-
this.currentAudioProcess = process;
|
|
1589
|
-
|
|
1590
|
-
process.on('close', () => {
|
|
1591
|
-
if (this.currentAudioProcess === process) {
|
|
1592
|
-
this.currentAudioProcess = null;
|
|
1593
|
-
}
|
|
1594
|
-
this.statusBar.setContent(`{green-fg}✓ Played ${row.name}{/green-fg}`);
|
|
1595
|
-
this.screen.render();
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
process.on('error', (err) => {
|
|
1599
|
-
if (this.currentAudioProcess === process) {
|
|
1600
|
-
this.currentAudioProcess = null;
|
|
1601
|
-
}
|
|
1602
|
-
this.statusBar.setContent(`{red-fg}✗ Error playing voice{/red-fg}`);
|
|
1603
|
-
this.screen.render();
|
|
1604
|
-
});
|
|
1605
|
-
} catch (error) {
|
|
1606
|
-
this.statusBar.setContent(`{red-fg}✗ Error: ${error.message}{/red-fg}`);
|
|
1607
|
-
this.screen.render();
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
async playWindowsSAPIVoice(row, sampleText) {
|
|
1612
|
-
try {
|
|
1613
|
-
// SECURITY: Escape row.name and sampleText for PowerShell single-quote context (#124)
|
|
1614
|
-
const safeName = row.name.replace(/'/g, "''");
|
|
1615
|
-
const safeText = sampleText.replace(/'/g, "''");
|
|
1616
|
-
const psScript = `Add-Type -AssemblyName System.Speech; $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer; $synth.SelectVoice('${safeName}'); $synth.Speak('${safeText}')`;
|
|
1617
|
-
const process = spawn('powershell', ['-Command', psScript], { stdio: 'ignore' });
|
|
1618
|
-
this.currentAudioProcess = process;
|
|
1619
|
-
|
|
1620
|
-
process.on('close', () => {
|
|
1621
|
-
if (this.currentAudioProcess === process) {
|
|
1622
|
-
this.currentAudioProcess = null;
|
|
1623
|
-
}
|
|
1624
|
-
this.statusBar.setContent(`{green-fg}✓ Played ${row.name}{/green-fg}`);
|
|
1625
|
-
this.screen.render();
|
|
1626
|
-
});
|
|
1627
|
-
|
|
1628
|
-
process.on('error', (err) => {
|
|
1629
|
-
if (this.currentAudioProcess === process) {
|
|
1630
|
-
this.currentAudioProcess = null;
|
|
1631
|
-
}
|
|
1632
|
-
this.statusBar.setContent(`{red-fg}✗ Error playing voice{/red-fg}`);
|
|
1633
|
-
this.screen.render();
|
|
1634
|
-
});
|
|
1635
|
-
} catch (error) {
|
|
1636
|
-
this.statusBar.setContent(`{red-fg}✗ Error: ${error.message}{/red-fg}`);
|
|
1637
|
-
this.screen.render();
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
async playSopranoVoice(row, sampleText) {
|
|
1642
|
-
try {
|
|
1643
|
-
// Soprano uses OpenAI API format
|
|
1644
|
-
const outputFile = path.join(CONFIG.OUTPUT_DIR, `soprano_${row.name.toLowerCase()}_${Date.now()}.wav`);
|
|
1645
|
-
|
|
1646
|
-
// Create JSON payload file to avoid shell escaping issues
|
|
1647
|
-
const payloadFile = path.join(CONFIG.OUTPUT_DIR, `soprano_payload_${Date.now()}.json`);
|
|
1648
|
-
const payload = {
|
|
1649
|
-
input: sampleText,
|
|
1650
|
-
model: 'tts-1',
|
|
1651
|
-
voice: row.name.toLowerCase() // API expects lowercase voice names
|
|
1652
|
-
};
|
|
1653
|
-
await fs.writeFile(payloadFile, JSON.stringify(payload));
|
|
1654
|
-
|
|
1655
|
-
// SECURITY: Use spawn with argument array instead of shell string (#125)
|
|
1656
|
-
await new Promise((resolve, reject) => {
|
|
1657
|
-
const curlProc = spawn('curl', [
|
|
1658
|
-
'-s', '-m', '10', '-X', 'POST',
|
|
1659
|
-
'http://127.0.0.1:7860/v1/audio/speech',
|
|
1660
|
-
'-H', 'Content-Type: application/json',
|
|
1661
|
-
'-d', `@${payloadFile}`,
|
|
1662
|
-
'-o', outputFile
|
|
1663
|
-
], { stdio: 'ignore' });
|
|
1664
|
-
curlProc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`curl exited ${code}`)));
|
|
1665
|
-
curlProc.on('error', reject);
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
// Clean up payload file
|
|
1669
|
-
try {
|
|
1670
|
-
await fs.unlink(payloadFile);
|
|
1671
|
-
} catch {}
|
|
1672
|
-
|
|
1673
|
-
// Play the generated audio
|
|
1674
|
-
const players = [
|
|
1675
|
-
{ cmd: 'aplay', args: [outputFile] },
|
|
1676
|
-
{ cmd: 'paplay', args: [outputFile] },
|
|
1677
|
-
{ cmd: 'ffplay', args: ['-nodisp', '-autoexit', outputFile] }
|
|
1678
|
-
];
|
|
1679
|
-
|
|
1680
|
-
for (const player of players) {
|
|
1681
|
-
try {
|
|
1682
|
-
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1683
|
-
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1684
|
-
|
|
1685
|
-
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
1686
|
-
this.currentAudioProcess = audioProcess;
|
|
1687
|
-
|
|
1688
|
-
audioProcess.on('close', async () => {
|
|
1689
|
-
if (this.currentAudioProcess === audioProcess) {
|
|
1690
|
-
this.currentAudioProcess = null;
|
|
1691
|
-
}
|
|
1692
|
-
// Clean up temp file
|
|
1693
|
-
try {
|
|
1694
|
-
await fs.unlink(outputFile);
|
|
1695
|
-
} catch {}
|
|
1696
|
-
this.statusBar.setContent(`{green-fg}✓ Played ${row.name}{/green-fg}`);
|
|
1697
|
-
this.screen.render();
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
audioProcess.on('error', (err) => {
|
|
1701
|
-
if (this.currentAudioProcess === audioProcess) {
|
|
1702
|
-
this.currentAudioProcess = null;
|
|
1703
|
-
}
|
|
1704
|
-
this.statusBar.setContent(`{red-fg}✗ Error playing voice{/red-fg}`);
|
|
1705
|
-
this.screen.render();
|
|
1706
|
-
});
|
|
1707
|
-
|
|
1708
|
-
break;
|
|
1709
|
-
} catch (error) {
|
|
1710
|
-
continue;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
} catch (error) {
|
|
1714
|
-
this.statusBar.setContent(`{red-fg}✗ Error with Soprano: ${error.message}{/red-fg}`);
|
|
1715
|
-
this.screen.render();
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
async playPiperVoice(row, sampleText) {
|
|
1720
493
|
// Sanitize sampleText to prevent command injection
|
|
1721
494
|
const safeSampleText = sampleText.replace(/[`$\\!"]/g, '\\$&');
|
|
1722
495
|
|
|
@@ -1745,8 +518,10 @@ class AgentVibesVoiceBrowser {
|
|
|
1745
518
|
}
|
|
1746
519
|
|
|
1747
520
|
const modelPath = path.join(CONFIG.PIPER_VOICES_DIR, `${safeModel}.onnx`);
|
|
1748
|
-
|
|
1749
|
-
|
|
521
|
+
try {
|
|
522
|
+
await fs.access(outputFile);
|
|
523
|
+
} catch {
|
|
524
|
+
// Use spawn instead of execAsync to prevent command injection
|
|
1750
525
|
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1751
526
|
'--model', modelPath,
|
|
1752
527
|
'--output_file', outputFile
|
|
@@ -1779,8 +554,10 @@ class AgentVibesVoiceBrowser {
|
|
|
1779
554
|
return;
|
|
1780
555
|
}
|
|
1781
556
|
|
|
1782
|
-
|
|
1783
|
-
|
|
557
|
+
try {
|
|
558
|
+
await fs.access(outputFile);
|
|
559
|
+
} catch {
|
|
560
|
+
// Use spawn instead of execAsync to prevent command injection
|
|
1784
561
|
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1785
562
|
'--model', CONFIG.MODEL_PATH,
|
|
1786
563
|
'--speaker', row.id.toString(),
|
|
@@ -1805,8 +582,7 @@ class AgentVibesVoiceBrowser {
|
|
|
1805
582
|
|
|
1806
583
|
for (const player of players) {
|
|
1807
584
|
try {
|
|
1808
|
-
|
|
1809
|
-
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
585
|
+
await execAsync(`which ${player.cmd} 2>/dev/null`);
|
|
1810
586
|
|
|
1811
587
|
// SECURITY: Store process immediately to prevent leak
|
|
1812
588
|
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
@@ -1921,11 +697,10 @@ class AgentVibesVoiceBrowser {
|
|
|
1921
697
|
}
|
|
1922
698
|
|
|
1923
699
|
async exportFavorites() {
|
|
1924
|
-
const
|
|
1925
|
-
const downData = this.tableData.filter(row => this.thumbsDown.has(row.id));
|
|
700
|
+
const favData = this.tableData.filter(row => this.favorites.has(row.id));
|
|
1926
701
|
const exportFile = path.join(os.homedir(), 'agentvibes-favorites.json');
|
|
1927
|
-
await fs.writeFile(exportFile, JSON.stringify(
|
|
1928
|
-
this.statusBar.setContent(`{green-fg}✓ Exported ${
|
|
702
|
+
await fs.writeFile(exportFile, JSON.stringify(favData, null, 2));
|
|
703
|
+
this.statusBar.setContent(`{green-fg}✓ Exported ${favData.length} favorites to ${exportFile}{/green-fg}`);
|
|
1929
704
|
this.screen.render();
|
|
1930
705
|
}
|
|
1931
706
|
|