agentvibes 5.1.4 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/config.json +23 -13
- package/.claude/commands/agent-vibes/verbosity.md +98 -89
- package/.claude/config/audio-effects.cfg +4 -1
- package/.claude/hooks/bmad-speak.sh +2 -2
- package/.claude/hooks/piper-download-voices.sh +233 -225
- package/.claude/hooks/piper-installer.sh +1 -1
- package/.claude/hooks/piper-voice-manager.sh +125 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
- package/.claude/hooks/play-tts-enhanced.sh +1 -1
- package/.claude/hooks/play-tts-piper.sh +16 -5
- package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
- package/.claude/hooks/play-tts.sh +21 -9
- package/.claude/hooks/session-start-tts.sh +4 -1
- package/.claude/hooks/stop-tts.sh +1 -1
- package/.claude/hooks/verbosity-manager.sh +185 -178
- package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
- package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
- package/.claude/hooks-windows/play-tts.ps1 +7 -1
- package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
- package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
- package/README.md +10 -2
- package/RELEASE_NOTES.md +36 -0
- package/bin/agentvibes-voice-browser.js +1939 -1840
- package/mcp-server/server.py +52 -9
- package/package.json +1 -1
- package/src/console/tabs/receiver-tab.js +1527 -1483
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +53 -8
- package/src/console/tabs/voices-tab.js +130 -13
- package/src/i18n/en.js +202 -202
- package/src/services/verbosity-service.js +159 -157
- package/templates/agentvibes-receiver.sh +3 -2
|
@@ -1,1841 +1,1940 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AgentVibes Voice Browser
|
|
5
|
-
* Browse and preview 914+ Piper TTS voices
|
|
6
|
-
* Press 'I' to install/select a voice for AgentVibes
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import blessed from 'blessed';
|
|
10
|
-
import chalk from 'chalk';
|
|
11
|
-
import { exec, spawn, spawnSync } from 'child_process';
|
|
12
|
-
import { promisify } from 'util';
|
|
13
|
-
import fs from 'fs/promises';
|
|
14
|
-
import fsSync from 'fs';
|
|
15
|
-
import path from 'path';
|
|
16
|
-
import { fileURLToPath } from 'url';
|
|
17
|
-
import os from 'os';
|
|
18
|
-
|
|
19
|
-
const execAsync = promisify(exec);
|
|
20
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
22
|
-
const CONFIG = {
|
|
23
|
-
MODEL_PATH: path.join(os.homedir(), '.local/share/piper/en_US-libritts-high.onnx'),
|
|
24
|
-
TOTAL_SPEAKERS: 904,
|
|
25
|
-
TOTAL_CURATED: 10,
|
|
26
|
-
TOTAL_ITEMS: 914,
|
|
27
|
-
SAMPLE_TEXT: 'Hello! This is a sample of my voice. I can speak clearly and naturally with expression.',
|
|
28
|
-
OUTPUT_DIR: path.join(os.homedir(), '.cache/agentvibes/voice-samples'),
|
|
29
|
-
CURATED_DIR: path.join(os.homedir(), '.cache/agentvibes/curated-samples'),
|
|
30
|
-
PROGRESS_FILE: path.join(os.homedir(), '.cache/agentvibes/browser-progress.json'),
|
|
31
|
-
PIPER_PATH: path.join(os.homedir(), '.local/bin/piper'),
|
|
32
|
-
PIPER_VOICES_DIR: path.join(os.homedir(), '.local/share/piper/voices'),
|
|
33
|
-
AGENTVIBES_CONFIG: path.join(os.homedir(), '.agentvibes/config.json'),
|
|
34
|
-
VOICE_METADATA: path.join(__dirname, '..', '.agentvibes', 'config', 'voice-metadata.json')
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Sample script templates showcasing AgentVibes features
|
|
38
|
-
const SAMPLE_TEMPLATES = [
|
|
39
|
-
"Hi, I'm {NAME}. AgentVibes supports multiple TTS providers including Piper for local processing, Windows SAPI, macOS system voices, and Soprano. Choose the best fit for your platform.",
|
|
40
|
-
"Hey there, I'm {NAME}! AgentVibes supports Soprano, a high-quality neural TTS engine that produces incredibly natural-sounding voices. The audio quality is seriously impressive.",
|
|
41
|
-
"Good day, I'm {NAME}. AgentVibes integrates with PulseAudio to stream TTS from headless remote servers to your local machine. Essential when developing on voiceless cloud instances.",
|
|
42
|
-
"Hi, I'm {NAME}. AgentVibes provides access to over thirty-seven Piper voices, plus system voices from Windows, macOS, and Linux. Maximum flexibility for your needs.",
|
|
43
|
-
"Hey team, I'm {NAME}! AgentVibes lets you add custom background music to your TTS output. Jazz, lo-fi, classical—whatever helps you stay in the zone while coding!",
|
|
44
|
-
"Oh wonderful, I'm {NAME}. AgentVibes has a sarcastic personality mode. Because clearly what your development workflow was missing was an AI with attitude. How delightful.",
|
|
45
|
-
"Hi, I'm {NAME}. AgentVibes includes a receiver mode that lets you stream TTS from one machine to another. Perfect for using remote servers while hearing audio on your local device.",
|
|
46
|
-
"Hi there, I'm {NAME}! AgentVibes includes audio effects like reverb, pitch adjustment, and EQ. Add some atmosphere and personality to your AI assistant's voice!",
|
|
47
|
-
"Hello, I'm {NAME}. AgentVibes includes a bundled MCP server that makes configuration incredibly easy. Just use natural language to configure voices, personalities, and settings.",
|
|
48
|
-
"Good afternoon, I'm {NAME}. If you're enjoying AgentVibes, we'd be tremendously grateful for a GitHub star. Your support helps the project grow and improve."
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
class AgentVibesVoiceBrowser {
|
|
52
|
-
constructor() {
|
|
53
|
-
this.tableData = [];
|
|
54
|
-
this.filteredData = [];
|
|
55
|
-
this.currentRow = 0;
|
|
56
|
-
this.sortColumn = 'id';
|
|
57
|
-
this.sortAsc = true;
|
|
58
|
-
this.searchTerm = '';
|
|
59
|
-
this.favorites = new Set();
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await fs.mkdir(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await this.
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
width: '70%',
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
border: { type: 'line', fg: 'cyan' },
|
|
583
|
-
label: '
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
border: { type: 'line', fg: 'cyan' },
|
|
634
|
-
label:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.
|
|
690
|
-
|
|
691
|
-
this.
|
|
692
|
-
this.
|
|
693
|
-
this.
|
|
694
|
-
this.screen.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
this.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
this.
|
|
704
|
-
this.
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
info
|
|
723
|
-
info += `{
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
info += `{cyan-fg}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
info +=
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
info += `{cyan-fg}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
this.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
} else {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
//
|
|
913
|
-
this.
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
this.
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
this.
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
this.
|
|
977
|
-
this.
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
this.
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
this.musicList.key(['
|
|
1039
|
-
if (this.currentTab !== 'music') return;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
this.
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
`#
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
`#
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
this.
|
|
1282
|
-
this.
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
this.
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
{
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
this.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
);
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
this.
|
|
1451
|
-
this.screen.render();
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
this.
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
this.
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
'
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentVibes Voice Browser
|
|
5
|
+
* Browse and preview 914+ Piper TTS voices
|
|
6
|
+
* Press 'I' to install/select a voice for AgentVibes
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import blessed from 'blessed';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { exec, spawn, spawnSync } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import fsSync from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
const CONFIG = {
|
|
23
|
+
MODEL_PATH: path.join(os.homedir(), '.local/share/piper/en_US-libritts-high.onnx'),
|
|
24
|
+
TOTAL_SPEAKERS: 904,
|
|
25
|
+
TOTAL_CURATED: 10,
|
|
26
|
+
TOTAL_ITEMS: 914,
|
|
27
|
+
SAMPLE_TEXT: 'Hello! This is a sample of my voice. I can speak clearly and naturally with expression.',
|
|
28
|
+
OUTPUT_DIR: path.join(os.homedir(), '.cache/agentvibes/voice-samples'),
|
|
29
|
+
CURATED_DIR: path.join(os.homedir(), '.cache/agentvibes/curated-samples'),
|
|
30
|
+
PROGRESS_FILE: path.join(os.homedir(), '.cache/agentvibes/browser-progress.json'),
|
|
31
|
+
PIPER_PATH: path.join(os.homedir(), '.local/bin/piper'),
|
|
32
|
+
PIPER_VOICES_DIR: path.join(os.homedir(), '.local/share/piper/voices'),
|
|
33
|
+
AGENTVIBES_CONFIG: path.join(os.homedir(), '.agentvibes/config.json'),
|
|
34
|
+
VOICE_METADATA: path.join(__dirname, '..', '.agentvibes', 'config', 'voice-metadata.json')
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Sample script templates showcasing AgentVibes features
|
|
38
|
+
const SAMPLE_TEMPLATES = [
|
|
39
|
+
"Hi, I'm {NAME}. AgentVibes supports multiple TTS providers including Piper for local processing, Windows SAPI, macOS system voices, and Soprano. Choose the best fit for your platform.",
|
|
40
|
+
"Hey there, I'm {NAME}! AgentVibes supports Soprano, a high-quality neural TTS engine that produces incredibly natural-sounding voices. The audio quality is seriously impressive.",
|
|
41
|
+
"Good day, I'm {NAME}. AgentVibes integrates with PulseAudio to stream TTS from headless remote servers to your local machine. Essential when developing on voiceless cloud instances.",
|
|
42
|
+
"Hi, I'm {NAME}. AgentVibes provides access to over thirty-seven Piper voices, plus system voices from Windows, macOS, and Linux. Maximum flexibility for your needs.",
|
|
43
|
+
"Hey team, I'm {NAME}! AgentVibes lets you add custom background music to your TTS output. Jazz, lo-fi, classical—whatever helps you stay in the zone while coding!",
|
|
44
|
+
"Oh wonderful, I'm {NAME}. AgentVibes has a sarcastic personality mode. Because clearly what your development workflow was missing was an AI with attitude. How delightful.",
|
|
45
|
+
"Hi, I'm {NAME}. AgentVibes includes a receiver mode that lets you stream TTS from one machine to another. Perfect for using remote servers while hearing audio on your local device.",
|
|
46
|
+
"Hi there, I'm {NAME}! AgentVibes includes audio effects like reverb, pitch adjustment, and EQ. Add some atmosphere and personality to your AI assistant's voice!",
|
|
47
|
+
"Hello, I'm {NAME}. AgentVibes includes a bundled MCP server that makes configuration incredibly easy. Just use natural language to configure voices, personalities, and settings.",
|
|
48
|
+
"Good afternoon, I'm {NAME}. If you're enjoying AgentVibes, we'd be tremendously grateful for a GitHub star. Your support helps the project grow and improve."
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
class AgentVibesVoiceBrowser {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.tableData = [];
|
|
54
|
+
this.filteredData = [];
|
|
55
|
+
this.currentRow = 0;
|
|
56
|
+
this.sortColumn = 'id';
|
|
57
|
+
this.sortAsc = true;
|
|
58
|
+
this.searchTerm = '';
|
|
59
|
+
this.favorites = new Set(); // backward compat — migrated to thumbsUp on load
|
|
60
|
+
this.thumbsUp = new Set();
|
|
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)
|
|
64
|
+
this.sampleText = CONFIG.SAMPLE_TEXT;
|
|
65
|
+
this.playing = false;
|
|
66
|
+
this.currentAudioProcess = null;
|
|
67
|
+
this.voiceAssignments = null;
|
|
68
|
+
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
|
+
}
|
|
76
|
+
|
|
77
|
+
async init() {
|
|
78
|
+
await fs.mkdir(CONFIG.OUTPUT_DIR, { recursive: true });
|
|
79
|
+
await fs.mkdir(CONFIG.CURATED_DIR, { recursive: true });
|
|
80
|
+
await fs.mkdir(path.dirname(CONFIG.PROGRESS_FILE), { recursive: true });
|
|
81
|
+
|
|
82
|
+
// Clean up old cached samples (without text hash in filename)
|
|
83
|
+
try {
|
|
84
|
+
const files = await fs.readdir(CONFIG.OUTPUT_DIR);
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
if (file.match(/^speaker_\d+\.wav$/)) {
|
|
87
|
+
await fs.unlink(path.join(CONFIG.OUTPUT_DIR, file));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Ignore cleanup errors
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.loadProgress();
|
|
95
|
+
await this.loadVoiceData();
|
|
96
|
+
await this.loadMusicData();
|
|
97
|
+
this.prepareTable();
|
|
98
|
+
this.setupUI();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async loadProgress() {
|
|
102
|
+
try {
|
|
103
|
+
const data = JSON.parse(await fs.readFile(CONFIG.PROGRESS_FILE, 'utf8'));
|
|
104
|
+
this.thumbsUp = new Set(data.thumbsUp || []);
|
|
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 || []);
|
|
112
|
+
this.sampleText = data.sampleText || CONFIG.SAMPLE_TEXT;
|
|
113
|
+
this.sortColumn = data.sortColumn || 'id';
|
|
114
|
+
this.sortAsc = data.sortAsc !== undefined ? data.sortAsc : true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// No previous progress
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async saveProgress() {
|
|
121
|
+
await fs.writeFile(CONFIG.PROGRESS_FILE, JSON.stringify({
|
|
122
|
+
thumbsUp: Array.from(this.thumbsUp),
|
|
123
|
+
thumbsDown: Array.from(this.thumbsDown),
|
|
124
|
+
favorites: Array.from(this.thumbsUp), // backward compat
|
|
125
|
+
musicFavorites: Array.from(this.musicFavorites),
|
|
126
|
+
sampleText: this.sampleText,
|
|
127
|
+
sortColumn: this.sortColumn,
|
|
128
|
+
sortAsc: this.sortAsc
|
|
129
|
+
}, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
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
|
+
async loadVoiceData() {
|
|
286
|
+
// Detect available providers
|
|
287
|
+
this.availableProviders = await this.detectProviders();
|
|
288
|
+
|
|
289
|
+
// Load voice assignments (for LibriTTS speakers)
|
|
290
|
+
const assignmentsPath = path.join(__dirname, '..', 'voice-assignments.json');
|
|
291
|
+
if (fsSync.existsSync(assignmentsPath)) {
|
|
292
|
+
this.voiceAssignments = JSON.parse(await fs.readFile(assignmentsPath, 'utf8'));
|
|
293
|
+
} else {
|
|
294
|
+
// Generate basic assignments if file doesn't exist
|
|
295
|
+
console.log(chalk.yellow('⚠ voice-assignments.json not found, generating basic data...'));
|
|
296
|
+
this.voiceAssignments = {
|
|
297
|
+
libritts_speakers: {},
|
|
298
|
+
curated_voices: {}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Generate basic speaker assignments
|
|
302
|
+
for (let id = 0; id < CONFIG.TOTAL_SPEAKERS; id++) {
|
|
303
|
+
this.voiceAssignments.libritts_speakers[id] = {
|
|
304
|
+
gender: id % 2 === 0 ? 'male' : 'female',
|
|
305
|
+
voice_name: `Speaker ${id}`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Load voice metadata (for curated voices)
|
|
311
|
+
if (fsSync.existsSync(CONFIG.VOICE_METADATA)) {
|
|
312
|
+
this.voiceMetadata = JSON.parse(await fs.readFile(CONFIG.VOICE_METADATA, 'utf8'));
|
|
313
|
+
|
|
314
|
+
// Merge curated voices into assignments
|
|
315
|
+
if (this.voiceMetadata && this.voiceMetadata.voices) {
|
|
316
|
+
let curatedId = 1000; // Start curated voices at ID 1000
|
|
317
|
+
for (const [friendlyName, voice] of Object.entries(this.voiceMetadata.voices)) {
|
|
318
|
+
this.voiceAssignments.curated_voices[curatedId] = {
|
|
319
|
+
gender: voice.gender,
|
|
320
|
+
voice_name: voice.displayName,
|
|
321
|
+
model_file: voice.id,
|
|
322
|
+
friendly_name: friendlyName
|
|
323
|
+
};
|
|
324
|
+
curatedId++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
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
|
+
}
|
|
348
|
+
|
|
349
|
+
prepareTable() {
|
|
350
|
+
this.tableData = [];
|
|
351
|
+
let nextId = 0;
|
|
352
|
+
|
|
353
|
+
// Add LibriTTS speakers
|
|
354
|
+
for (let id = 0; id < CONFIG.TOTAL_SPEAKERS; id++) {
|
|
355
|
+
const assignment = this.voiceAssignments.libritts_speakers[id];
|
|
356
|
+
if (assignment) {
|
|
357
|
+
// Assign random sample template with voice name
|
|
358
|
+
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
359
|
+
const sampleText = template.replace('{NAME}', assignment.voice_name);
|
|
360
|
+
|
|
361
|
+
this.tableData.push({
|
|
362
|
+
id: nextId++,
|
|
363
|
+
originalId: id,
|
|
364
|
+
gender: assignment.gender,
|
|
365
|
+
name: assignment.voice_name,
|
|
366
|
+
model: 'LibriTTS',
|
|
367
|
+
type: 'libritts',
|
|
368
|
+
provider: 'Piper',
|
|
369
|
+
piperVoiceId: `speaker-${id}`,
|
|
370
|
+
sampleText: sampleText,
|
|
371
|
+
language: 'en_US'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Add curated voices
|
|
377
|
+
for (const [id, curated] of Object.entries(this.voiceAssignments.curated_voices)) {
|
|
378
|
+
// Assign random sample template with voice name
|
|
379
|
+
const template = SAMPLE_TEMPLATES[Math.floor(Math.random() * SAMPLE_TEMPLATES.length)];
|
|
380
|
+
const sampleText = template.replace('{NAME}', curated.voice_name);
|
|
381
|
+
|
|
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
|
+
this.tableData.push({
|
|
387
|
+
id: nextId++,
|
|
388
|
+
originalId: parseInt(id),
|
|
389
|
+
gender: curated.gender,
|
|
390
|
+
name: curated.voice_name,
|
|
391
|
+
model: curated.model_file,
|
|
392
|
+
type: 'curated',
|
|
393
|
+
provider: 'Piper',
|
|
394
|
+
piperVoiceId: curated.model_file,
|
|
395
|
+
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'
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.applyFilter();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
applyFilter() {
|
|
456
|
+
// Start with all voices or thumbs-up only
|
|
457
|
+
let data = this.favoritesOnly
|
|
458
|
+
? this.tableData.filter(row => this.thumbsUp.has(row.id))
|
|
459
|
+
: [...this.tableData];
|
|
460
|
+
|
|
461
|
+
// Apply provider filter
|
|
462
|
+
if (this.providerFilter) {
|
|
463
|
+
data = data.filter(row => row.provider === this.providerFilter);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Apply search filter
|
|
467
|
+
if (this.searchTerm) {
|
|
468
|
+
const term = this.searchTerm.toLowerCase();
|
|
469
|
+
data = data.filter(row =>
|
|
470
|
+
row.id.toString().includes(term) ||
|
|
471
|
+
row.gender.includes(term) ||
|
|
472
|
+
row.name.toLowerCase().includes(term) ||
|
|
473
|
+
row.model.toLowerCase().includes(term) ||
|
|
474
|
+
row.language.toLowerCase().includes(term) ||
|
|
475
|
+
row.provider.toLowerCase().includes(term)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.filteredData = data;
|
|
480
|
+
|
|
481
|
+
// Sort
|
|
482
|
+
this.filteredData.sort((a, b) => {
|
|
483
|
+
let aVal = a[this.sortColumn];
|
|
484
|
+
let bVal = b[this.sortColumn];
|
|
485
|
+
if (typeof aVal === 'string') aVal = aVal.toLowerCase();
|
|
486
|
+
if (typeof bVal === 'string') bVal = bVal.toLowerCase();
|
|
487
|
+
if (aVal < bVal) return this.sortAsc ? -1 : 1;
|
|
488
|
+
if (aVal > bVal) return this.sortAsc ? 1 : -1;
|
|
489
|
+
return 0;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
formatRow(row) {
|
|
494
|
+
const fav = this.thumbsUp.has(row.id) ? '{green-fg}+{/green-fg}' : (this.thumbsDown.has(row.id) ? '{red-fg}-{/red-fg}' : ' ');
|
|
495
|
+
const genderIcon = row.gender === 'male' ? '♂' : (row.gender === 'female' ? '♀' : '-');
|
|
496
|
+
const genderColor = row.gender === 'male' ? 'blue-fg' : (row.gender === 'female' ? 'magenta-fg' : 'gray-fg');
|
|
497
|
+
const gender = `{${genderColor}}${genderIcon}{/${genderColor}}`;
|
|
498
|
+
const id = String(row.id).padStart(4);
|
|
499
|
+
const name = row.name.substring(0, 13).padEnd(13);
|
|
500
|
+
const provider = row.provider.substring(0, 8).padEnd(8);
|
|
501
|
+
const lang = row.language.substring(0, 6).padEnd(6);
|
|
502
|
+
const model = row.model.substring(0, 15).padEnd(15);
|
|
503
|
+
return `${fav} ${id} ${gender} ${name} ${provider} ${lang} ${model}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
setupUI() {
|
|
507
|
+
this.screen = blessed.screen({ smartCSR: true, title: 'AgentVibes Voice Browser' });
|
|
508
|
+
|
|
509
|
+
// Calculate unique models and store as instance variable
|
|
510
|
+
this.uniqueModels = new Set(this.tableData.map(row => row.model)).size;
|
|
511
|
+
|
|
512
|
+
const title = blessed.box({
|
|
513
|
+
top: 0,
|
|
514
|
+
height: 1,
|
|
515
|
+
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}`,
|
|
517
|
+
tags: true,
|
|
518
|
+
style: { fg: 'white' }
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const headerBar = blessed.box({
|
|
522
|
+
top: 1,
|
|
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,
|
|
558
|
+
left: 0,
|
|
559
|
+
height: 1,
|
|
560
|
+
width: '70%',
|
|
561
|
+
content: ` ID G Name Provider Lang Model `,
|
|
562
|
+
style: { fg: 'cyan', bold: true },
|
|
563
|
+
mouse: true,
|
|
564
|
+
clickable: true
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
this.list = blessed.list({
|
|
568
|
+
top: 1,
|
|
569
|
+
left: 0,
|
|
570
|
+
width: '70%',
|
|
571
|
+
height: '100%-1',
|
|
572
|
+
keys: true,
|
|
573
|
+
vi: true,
|
|
574
|
+
mouse: true,
|
|
575
|
+
tags: true,
|
|
576
|
+
style: {
|
|
577
|
+
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
578
|
+
item: { fg: 'white' },
|
|
579
|
+
border: { fg: 'cyan' },
|
|
580
|
+
label: { fg: 'gray' }
|
|
581
|
+
},
|
|
582
|
+
border: { type: 'line', fg: 'cyan' },
|
|
583
|
+
label: ` Voices (${this.filteredData.length}) - Model (${this.uniqueModels}) - Sort: ${this.sortColumn} ${this.sortAsc ? '↑' : '↓'} `
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
this.infoPanel = blessed.box({
|
|
587
|
+
top: 0,
|
|
588
|
+
left: '70%',
|
|
589
|
+
width: '30%',
|
|
590
|
+
height: '100%',
|
|
591
|
+
tags: true,
|
|
592
|
+
border: { type: 'line', fg: 'cyan' },
|
|
593
|
+
label: ' Voice Info ',
|
|
594
|
+
scrollable: true,
|
|
595
|
+
alwaysScroll: true,
|
|
596
|
+
mouse: true,
|
|
597
|
+
keys: true,
|
|
598
|
+
vi: true,
|
|
599
|
+
style: {
|
|
600
|
+
border: { fg: 'cyan' },
|
|
601
|
+
label: { fg: 'gray' }
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
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
|
+
this.statusBar = blessed.box({
|
|
657
|
+
bottom: 4,
|
|
658
|
+
height: 1,
|
|
659
|
+
width: '100%',
|
|
660
|
+
content: 'Ready',
|
|
661
|
+
tags: true,
|
|
662
|
+
style: { fg: 'green' }
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
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
|
+
bottom: 0,
|
|
682
|
+
height: 1,
|
|
683
|
+
width: '100%',
|
|
684
|
+
content: '{center}{gray-fg}Please consider giving us a GitHub star *{/gray-fg} {yellow-fg}github.com/paulpreibisch/agentvibes{/yellow-fg}{/center}',
|
|
685
|
+
tags: true,
|
|
686
|
+
style: { fg: 'white' }
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
this.screen.append(title);
|
|
690
|
+
this.screen.append(headerBar);
|
|
691
|
+
this.screen.append(this.tabBar);
|
|
692
|
+
this.screen.append(this.voicesContainer);
|
|
693
|
+
this.screen.append(this.musicContainer);
|
|
694
|
+
this.screen.append(this.statusBar);
|
|
695
|
+
this.screen.append(this.helpBar);
|
|
696
|
+
this.screen.append(this.githubMessage);
|
|
697
|
+
|
|
698
|
+
this.updateTabBar();
|
|
699
|
+
this.updateMusicList();
|
|
700
|
+
|
|
701
|
+
this.updateList();
|
|
702
|
+
this.list.focus();
|
|
703
|
+
this.setupKeys();
|
|
704
|
+
this.screen.render();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
updateList() {
|
|
708
|
+
const items = this.filteredData.map(row => this.formatRow(row));
|
|
709
|
+
this.list.setItems(items);
|
|
710
|
+
this.list.select(Math.min(this.currentRow, items.length - 1));
|
|
711
|
+
|
|
712
|
+
const modeLabel = this.favoritesOnly ? ' + Thumbs Up ' : ' Voices ';
|
|
713
|
+
this.list.setLabel(`${modeLabel}(${this.filteredData.length}) - Model (${this.uniqueModels}) - Sort: ${this.sortColumn} ${this.sortAsc ? '↑' : '↓'} `);
|
|
714
|
+
this.updateInfo();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
updateInfo() {
|
|
718
|
+
const idx = this.list.selected;
|
|
719
|
+
if (idx < 0 || idx >= this.filteredData.length) return;
|
|
720
|
+
|
|
721
|
+
const row = this.filteredData[idx];
|
|
722
|
+
let info = `{bold}${row.type === 'curated' ? row.name : 'Speaker ' + row.id}{/bold}\n`;
|
|
723
|
+
info += `{gray-fg}${'─'.repeat(20)}{/gray-fg}\n\n`;
|
|
724
|
+
if (this.thumbsUp.has(row.id)) info += '{green-fg}+ Thumbs Up{/green-fg}\n\n';
|
|
725
|
+
else if (this.thumbsDown.has(row.id)) info += '{red-fg}- Thumbs Down{/red-fg}\n\n';
|
|
726
|
+
info += `{cyan-fg}ID:{/cyan-fg} ${row.id}\n`;
|
|
727
|
+
|
|
728
|
+
// Color gender value: blue for male, pink for female
|
|
729
|
+
const genderColor = row.gender === 'male' ? 'blue-fg' : 'magenta-fg';
|
|
730
|
+
info += `{cyan-fg}Gender:{/cyan-fg} {${genderColor}}${row.gender}{/${genderColor}}\n`;
|
|
731
|
+
|
|
732
|
+
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
|
+
|
|
736
|
+
// Color model in yellow
|
|
737
|
+
info += `{cyan-fg}Model:{/cyan-fg} {yellow-fg}${row.model}{/yellow-fg}\n`;
|
|
738
|
+
|
|
739
|
+
if (row.type === 'curated' && row.friendlyName) {
|
|
740
|
+
info += `{cyan-fg}Friendly:{/cyan-fg} ${row.friendlyName}\n`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Color sample text in green - use voice-specific sample
|
|
744
|
+
const voiceSample = row.sampleText || this.sampleText;
|
|
745
|
+
info += `\n{gray-fg}Sample:{/gray-fg}\n{green-fg}"${voiceSample}"{/green-fg}\n`;
|
|
746
|
+
|
|
747
|
+
info += `\n{cyan-fg}Position:{/cyan-fg} ${idx + 1}/${this.filteredData.length}\n`;
|
|
748
|
+
info += `{green-fg}Thumbs Up:{/green-fg} ${this.thumbsUp.size} {red-fg}Thumbs Down:{/red-fg} ${this.thumbsDown.size}\n\n`;
|
|
749
|
+
info += `{green-fg}[I]{/green-fg} Install voice {cyan-fg}[P]{/cyan-fg} Copy prompt`;
|
|
750
|
+
|
|
751
|
+
this.infoPanel.setContent(info);
|
|
752
|
+
this.screen.render();
|
|
753
|
+
}
|
|
754
|
+
|
|
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
|
+
setupKeys() {
|
|
822
|
+
this.screen.key(['q', 'Q', 'C-c'], () => this.exit());
|
|
823
|
+
|
|
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
|
+
// Listen to selection changes (blessed handles arrow keys automatically)
|
|
849
|
+
this.list.on('select', () => {
|
|
850
|
+
this.updateInfo();
|
|
851
|
+
});
|
|
852
|
+
|
|
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
|
+
// Sorting
|
|
913
|
+
this.screen.key(['1'], () => { this.sortColumn = 'id'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
914
|
+
this.screen.key(['2'], () => { this.sortColumn = 'gender'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
915
|
+
this.screen.key(['3'], () => { this.sortColumn = 'name'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
916
|
+
this.screen.key(['4'], () => { this.sortColumn = 'provider'; this.sortAsc = !this.sortAsc; this.applyFilter(); this.updateList(); });
|
|
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(); });
|
|
919
|
+
|
|
920
|
+
// Search
|
|
921
|
+
this.screen.key(['/'], () => this.showSearch());
|
|
922
|
+
|
|
923
|
+
// Play
|
|
924
|
+
this.list.key(['space'], async () => {
|
|
925
|
+
const row = this.filteredData[this.list.selected];
|
|
926
|
+
if (row) await this.playSample(row);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Reverb toggle (on voices tab)
|
|
930
|
+
this.list.key(['r', 'R'], async () => {
|
|
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 () => {
|
|
955
|
+
const row = this.filteredData[this.list.selected];
|
|
956
|
+
if (row) {
|
|
957
|
+
if (this.thumbsDown.has(row.id)) {
|
|
958
|
+
this.thumbsDown.delete(row.id);
|
|
959
|
+
this.statusBar.setContent('{yellow-fg}Removed thumbs down{/yellow-fg}');
|
|
960
|
+
} else {
|
|
961
|
+
this.thumbsDown.add(row.id);
|
|
962
|
+
this.thumbsUp.delete(row.id);
|
|
963
|
+
this.statusBar.setContent('{red-fg}Thumbs down -{/red-fg}');
|
|
964
|
+
}
|
|
965
|
+
await this.saveProgress();
|
|
966
|
+
this.updateList();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Install/Select voice for AgentVibes
|
|
971
|
+
this.screen.key(['i', 'I'], () => this.installVoice());
|
|
972
|
+
|
|
973
|
+
// Toggle favorites filter
|
|
974
|
+
this.screen.key(['f', 'F'], () => {
|
|
975
|
+
this.favoritesOnly = !this.favoritesOnly;
|
|
976
|
+
this.applyFilter();
|
|
977
|
+
this.updateList();
|
|
978
|
+
|
|
979
|
+
if (this.favoritesOnly) {
|
|
980
|
+
this.statusBar.setContent(`{green-fg}+ Showing ${this.filteredData.length} thumbs-up voices - Press [F] or [X] to show all{/green-fg}`);
|
|
981
|
+
} else {
|
|
982
|
+
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter thumbs-up{/cyan-fg}`);
|
|
983
|
+
}
|
|
984
|
+
this.screen.render();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Exit favorites filter with X
|
|
988
|
+
this.screen.key(['x', 'X'], () => {
|
|
989
|
+
if (this.favoritesOnly) {
|
|
990
|
+
this.favoritesOnly = false;
|
|
991
|
+
this.applyFilter();
|
|
992
|
+
this.updateList();
|
|
993
|
+
this.statusBar.setContent(`{cyan-fg}Showing all voices - Press [F] to filter thumbs-up{/cyan-fg}`);
|
|
994
|
+
this.screen.render();
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// Export
|
|
999
|
+
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
|
+
}
|
|
1454
|
+
|
|
1455
|
+
showSearch() {
|
|
1456
|
+
const searchBox = blessed.textbox({
|
|
1457
|
+
parent: this.screen,
|
|
1458
|
+
top: 'center',
|
|
1459
|
+
left: 'center',
|
|
1460
|
+
width: 50,
|
|
1461
|
+
height: 3,
|
|
1462
|
+
border: { type: 'line', fg: 'cyan' },
|
|
1463
|
+
label: ' Search ',
|
|
1464
|
+
inputOnFocus: true
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
searchBox.on('submit', (value) => {
|
|
1468
|
+
this.searchTerm = value.trim();
|
|
1469
|
+
this.applyFilter();
|
|
1470
|
+
this.updateList();
|
|
1471
|
+
this.screen.remove(searchBox);
|
|
1472
|
+
this.list.focus();
|
|
1473
|
+
this.statusBar.setContent(`{cyan-fg}Search: "${this.searchTerm}" - ${this.filteredData.length} results{/cyan-fg}`);
|
|
1474
|
+
this.screen.render();
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
searchBox.key(['escape'], () => {
|
|
1478
|
+
this.screen.remove(searchBox);
|
|
1479
|
+
this.list.focus();
|
|
1480
|
+
this.screen.render();
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
searchBox.focus();
|
|
1484
|
+
this.screen.render();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
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
|
+
async playSample(row) {
|
|
1505
|
+
if (this.currentAudioProcess) {
|
|
1506
|
+
try {
|
|
1507
|
+
this.currentAudioProcess.kill('SIGKILL');
|
|
1508
|
+
this.currentAudioProcess = null;
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
// Process might have already finished
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
this.statusBar.setContent(`{cyan-fg}Playing ${row.name}...{/cyan-fg}`);
|
|
1515
|
+
this.screen.render();
|
|
1516
|
+
|
|
1517
|
+
// Use voice-specific sample text
|
|
1518
|
+
const sampleText = row.sampleText || this.sampleText;
|
|
1519
|
+
|
|
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
|
+
// Sanitize sampleText to prevent command injection
|
|
1721
|
+
const safeSampleText = sampleText.replace(/[`$\\!"]/g, '\\$&');
|
|
1722
|
+
|
|
1723
|
+
// Generate unique filename based on sample text hash to support different samples
|
|
1724
|
+
const textHash = sampleText.substring(0, 20).replace(/[^a-zA-Z0-9]/g, '');
|
|
1725
|
+
|
|
1726
|
+
let outputFile;
|
|
1727
|
+
if (row.type === 'curated') {
|
|
1728
|
+
// Validate model name to prevent path traversal
|
|
1729
|
+
const safeModel = path.basename(row.model);
|
|
1730
|
+
if (safeModel !== row.model || /[^a-zA-Z0-9_-]/.test(safeModel)) {
|
|
1731
|
+
this.statusBar.setContent(`{red-fg}✗ Invalid model name{/red-fg}`);
|
|
1732
|
+
this.screen.render();
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
outputFile = path.join(CONFIG.CURATED_DIR, `${safeModel}_${textHash}.wav`);
|
|
1737
|
+
|
|
1738
|
+
// Verify output path stays within intended directory
|
|
1739
|
+
const resolvedOutput = path.resolve(outputFile);
|
|
1740
|
+
const resolvedDir = path.resolve(CONFIG.CURATED_DIR);
|
|
1741
|
+
if (!resolvedOutput.startsWith(resolvedDir + path.sep)) {
|
|
1742
|
+
this.statusBar.setContent(`{red-fg}✗ Invalid output path{/red-fg}`);
|
|
1743
|
+
this.screen.render();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
const modelPath = path.join(CONFIG.PIPER_VOICES_DIR, `${safeModel}.onnx`);
|
|
1748
|
+
// SECURITY: Always regenerate instead of TOCTOU check (#132)
|
|
1749
|
+
{
|
|
1750
|
+
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1751
|
+
'--model', modelPath,
|
|
1752
|
+
'--output_file', outputFile
|
|
1753
|
+
], { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
1754
|
+
|
|
1755
|
+
piperProcess.stdin.write(safeSampleText);
|
|
1756
|
+
piperProcess.stdin.end();
|
|
1757
|
+
|
|
1758
|
+
await new Promise((resolve, reject) => {
|
|
1759
|
+
piperProcess.on('close', code => code === 0 ? resolve() : reject(new Error(`Piper exit ${code}`)));
|
|
1760
|
+
piperProcess.on('error', reject);
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
} else {
|
|
1764
|
+
// Validate speaker ID is numeric
|
|
1765
|
+
if (!Number.isInteger(row.id) || row.id < 0) {
|
|
1766
|
+
this.statusBar.setContent(`{red-fg}✗ Invalid speaker ID{/red-fg}`);
|
|
1767
|
+
this.screen.render();
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
outputFile = path.join(CONFIG.OUTPUT_DIR, `speaker_${row.id}_${textHash}.wav`);
|
|
1772
|
+
|
|
1773
|
+
// Verify output path stays within intended directory
|
|
1774
|
+
const resolvedOutput = path.resolve(outputFile);
|
|
1775
|
+
const resolvedDir = path.resolve(CONFIG.OUTPUT_DIR);
|
|
1776
|
+
if (!resolvedOutput.startsWith(resolvedDir + path.sep)) {
|
|
1777
|
+
this.statusBar.setContent(`{red-fg}✗ Invalid output path{/red-fg}`);
|
|
1778
|
+
this.screen.render();
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// SECURITY: Always regenerate instead of TOCTOU check (#132)
|
|
1783
|
+
{
|
|
1784
|
+
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1785
|
+
'--model', CONFIG.MODEL_PATH,
|
|
1786
|
+
'--speaker', row.id.toString(),
|
|
1787
|
+
'--output_file', outputFile
|
|
1788
|
+
], { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
1789
|
+
|
|
1790
|
+
piperProcess.stdin.write(safeSampleText);
|
|
1791
|
+
piperProcess.stdin.end();
|
|
1792
|
+
|
|
1793
|
+
await new Promise((resolve, reject) => {
|
|
1794
|
+
piperProcess.on('close', code => code === 0 ? resolve() : reject(new Error(`Piper exit ${code}`)));
|
|
1795
|
+
piperProcess.on('error', reject);
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const players = [
|
|
1801
|
+
{ cmd: 'aplay', args: [outputFile] },
|
|
1802
|
+
{ cmd: 'paplay', args: [outputFile] },
|
|
1803
|
+
{ cmd: 'ffplay', args: ['-nodisp', '-autoexit', outputFile] }
|
|
1804
|
+
];
|
|
1805
|
+
|
|
1806
|
+
for (const player of players) {
|
|
1807
|
+
try {
|
|
1808
|
+
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1809
|
+
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1810
|
+
|
|
1811
|
+
// SECURITY: Store process immediately to prevent leak
|
|
1812
|
+
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
1813
|
+
this.currentAudioProcess = audioProcess;
|
|
1814
|
+
|
|
1815
|
+
audioProcess.on('close', () => {
|
|
1816
|
+
if (this.currentAudioProcess === audioProcess) {
|
|
1817
|
+
this.currentAudioProcess = null;
|
|
1818
|
+
}
|
|
1819
|
+
this.statusBar.setContent(`{green-fg}✓ Played ${row.name}{/green-fg}`);
|
|
1820
|
+
this.screen.render();
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
audioProcess.on('error', (err) => {
|
|
1824
|
+
if (this.currentAudioProcess === audioProcess) {
|
|
1825
|
+
this.currentAudioProcess = null;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
break;
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async installVoice() {
|
|
1837
|
+
const row = this.filteredData[this.list.selected];
|
|
1838
|
+
if (!row) return;
|
|
1839
|
+
|
|
1840
|
+
try {
|
|
1841
|
+
// Read current config
|
|
1842
|
+
let config = {};
|
|
1843
|
+
try {
|
|
1844
|
+
const configData = await fs.readFile(CONFIG.AGENTVIBES_CONFIG, 'utf8');
|
|
1845
|
+
config = JSON.parse(configData);
|
|
1846
|
+
} catch (e) {
|
|
1847
|
+
// Config doesn't exist yet, will create it
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// Determine the voice ID to save
|
|
1851
|
+
let voiceId;
|
|
1852
|
+
if (row.type === 'curated' && row.friendlyName) {
|
|
1853
|
+
// For curated voices with friendly names, save the friendly name
|
|
1854
|
+
// This allows users to reference them easily (e.g., "switch to Ryan")
|
|
1855
|
+
voiceId = row.friendlyName;
|
|
1856
|
+
} else if (row.type === 'curated') {
|
|
1857
|
+
// Fallback to Piper ID if no friendly name
|
|
1858
|
+
voiceId = row.piperVoiceId;
|
|
1859
|
+
} else {
|
|
1860
|
+
// For LibriTTS speakers, save as speaker ID
|
|
1861
|
+
voiceId = `libritts-speaker-${row.id}`;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// SECURITY: Validate voiceId to prevent JSON injection
|
|
1865
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(voiceId)) {
|
|
1866
|
+
this.statusBar.setContent(`{red-fg}✗ Invalid voice ID format{/red-fg}`);
|
|
1867
|
+
this.screen.render();
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Update config
|
|
1872
|
+
config.defaultVoice = voiceId;
|
|
1873
|
+
config.ttsProvider = 'piper';
|
|
1874
|
+
|
|
1875
|
+
// Ensure config directory exists with secure permissions
|
|
1876
|
+
const configDir = path.dirname(CONFIG.AGENTVIBES_CONFIG);
|
|
1877
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
1878
|
+
|
|
1879
|
+
// SECURITY: Atomic write to prevent race condition
|
|
1880
|
+
const tempFile = CONFIG.AGENTVIBES_CONFIG + '.tmp.' + Date.now();
|
|
1881
|
+
await fs.writeFile(tempFile, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
1882
|
+
await fs.rename(tempFile, CONFIG.AGENTVIBES_CONFIG);
|
|
1883
|
+
|
|
1884
|
+
this.statusBar.setContent(`{green-fg}✓ Installed: ${row.name} → AgentVibes default voice{/green-fg}`);
|
|
1885
|
+
this.screen.render();
|
|
1886
|
+
|
|
1887
|
+
// Show confirmation dialog
|
|
1888
|
+
setTimeout(() => {
|
|
1889
|
+
const confirmBox = blessed.box({
|
|
1890
|
+
parent: this.screen,
|
|
1891
|
+
top: 'center',
|
|
1892
|
+
left: 'center',
|
|
1893
|
+
width: 60,
|
|
1894
|
+
height: 7,
|
|
1895
|
+
border: { type: 'line', fg: 'green' },
|
|
1896
|
+
label: ' ✓ Voice Installed ',
|
|
1897
|
+
content: `\n{center}${row.name} is now your AgentVibes default voice!{/center}\n\n{center}{gray-fg}Press any key to continue...{/gray-fg}{/center}`,
|
|
1898
|
+
tags: true
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
this.screen.append(confirmBox);
|
|
1902
|
+
this.screen.render();
|
|
1903
|
+
|
|
1904
|
+
const closeDialog = () => {
|
|
1905
|
+
this.screen.remove(confirmBox);
|
|
1906
|
+
this.list.focus();
|
|
1907
|
+
this.screen.render();
|
|
1908
|
+
this.screen.unkey(['space'], closeDialog);
|
|
1909
|
+
this.screen.unkey(['enter'], closeDialog);
|
|
1910
|
+
this.screen.unkey(['escape'], closeDialog);
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
this.screen.key(['space', 'enter', 'escape'], closeDialog);
|
|
1914
|
+
this.screen.onceKey(['space', 'enter', 'escape'], closeDialog);
|
|
1915
|
+
}, 500);
|
|
1916
|
+
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
this.statusBar.setContent(`{red-fg}✗ Error: ${error.message}{/red-fg}`);
|
|
1919
|
+
this.screen.render();
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async exportFavorites() {
|
|
1924
|
+
const upData = this.tableData.filter(row => this.thumbsUp.has(row.id));
|
|
1925
|
+
const downData = this.tableData.filter(row => this.thumbsDown.has(row.id));
|
|
1926
|
+
const exportFile = path.join(os.homedir(), 'agentvibes-favorites.json');
|
|
1927
|
+
await fs.writeFile(exportFile, JSON.stringify({ thumbsUp: upData, thumbsDown: downData }, null, 2));
|
|
1928
|
+
this.statusBar.setContent(`{green-fg}✓ Exported ${upData.length} thumbs-up, ${downData.length} thumbs-down to ${exportFile}{/green-fg}`);
|
|
1929
|
+
this.screen.render();
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
async exit() {
|
|
1933
|
+
await this.saveProgress();
|
|
1934
|
+
this.screen.destroy();
|
|
1935
|
+
console.log('\n✓ Progress saved. Goodbye!\n');
|
|
1936
|
+
process.exit(0);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
new AgentVibesVoiceBrowser().init().catch(console.error);
|