agentvibes 5.0.0 → 5.1.1
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/.claude/audio/tracks/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/play-tts.sh +10 -3
- package/.claude/hooks-windows/play-tts.ps1 +8 -1
- package/README.md +22 -2
- package/RELEASE_NOTES.md +76 -0
- package/package.json +1 -1
- package/src/console/tabs/agents-tab.js +65 -62
- package/src/console/tabs/music-tab.js +49 -19
- package/src/console/tabs/settings-tab.js +39 -37
- package/src/console/tabs/setup-tab.js +346 -86
- package/src/console/tabs/voices-tab.js +152 -29
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +8 -8
- package/src/services/llm-provider-service.js +79 -0
|
@@ -44,7 +44,7 @@ const COLORS = {
|
|
|
44
44
|
dimFg: '#455a64',
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const FOOTER_TEXT = '[↑↓/jk] Navigate [Space] Preview [Enter] Select/Install [
|
|
47
|
+
const FOOTER_TEXT = '[↑↓/jk] Navigate [Space] Preview [Enter] Select/Install [*] Favorite [/] Search';
|
|
48
48
|
/**
|
|
49
49
|
* Resolve the Piper voice storage directory using the same precedence as the
|
|
50
50
|
* shell-side get_voice_storage_dir() in piper-voice-manager.sh:
|
|
@@ -106,8 +106,10 @@ function loadCatalog() {
|
|
|
106
106
|
const libritts = data.libritts_speakers ?? {};
|
|
107
107
|
for (const [id, entry] of Object.entries(libritts)) {
|
|
108
108
|
_catalogEntries.push({
|
|
109
|
+
// voiceId stays raw so existing user configs continue to resolve
|
|
109
110
|
voiceId: `en_US-libritts-high${MS_SEP}${entry.voice_name}`,
|
|
110
|
-
displayName
|
|
111
|
+
// displayName gets a deterministic surname so every speaker is unique
|
|
112
|
+
displayName: uniquifyVoiceName(entry.voice_name),
|
|
111
113
|
gender: (entry.gender ?? '').charAt(0).toUpperCase() + (entry.gender ?? '').slice(1),
|
|
112
114
|
model: 'en_US-libritts-high',
|
|
113
115
|
type: 'libritts',
|
|
@@ -183,7 +185,77 @@ function patchLibriTTSSpeakerNames() {
|
|
|
183
185
|
|
|
184
186
|
// Column widths for the multi-column voice list
|
|
185
187
|
export const COL_NAME_W = 26;
|
|
186
|
-
export const COL_GENDER_W =
|
|
188
|
+
export const COL_GENDER_W = 4;
|
|
189
|
+
|
|
190
|
+
// Surname pool used to convert "Anna-2" → "Anna Carter" so every speaker
|
|
191
|
+
// in the LibriTTS catalog gets a unique, friendly display name. Indexed by
|
|
192
|
+
// the trailing -N suffix (1-based: no suffix → idx 0, "-2" → idx 1, ...).
|
|
193
|
+
const SURNAME_POOL = [
|
|
194
|
+
'Bell', 'Carter', 'Davis', 'Ellis', 'Foster', 'Gray', 'Hayes', 'Irving',
|
|
195
|
+
'Jones', 'Knox', 'Lane', 'Mason', 'Nash', 'Owens', 'Pierce', 'Quinn',
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert a raw catalog speaker name like "Anna" or "Anna-2" into a unique
|
|
200
|
+
* friendly display name by appending a deterministic surname.
|
|
201
|
+
* "Anna" → "Anna Bell"
|
|
202
|
+
* "Anna-2" → "Anna Carter"
|
|
203
|
+
* "Anna-16" → "Anna Quinn"
|
|
204
|
+
* "Cori Samuel" → "Cori Samuel" (already full name — left alone)
|
|
205
|
+
*
|
|
206
|
+
* @param {string} rawName
|
|
207
|
+
* @returns {string}
|
|
208
|
+
*/
|
|
209
|
+
export function uniquifyVoiceName(rawName) {
|
|
210
|
+
if (!rawName) return rawName;
|
|
211
|
+
const m = rawName.match(/^(.+)-(\d+)$/);
|
|
212
|
+
if (m) {
|
|
213
|
+
const base = m[1];
|
|
214
|
+
const n = parseInt(m[2], 10);
|
|
215
|
+
// Only treat -N as a duplicate counter when N >= 2 (catalog convention).
|
|
216
|
+
// -0 / -1 would otherwise produce collisions or undefined surnames.
|
|
217
|
+
if (n >= 2) {
|
|
218
|
+
const idx = (n - 1) % SURNAME_POOL.length;
|
|
219
|
+
return `${base} ${SURNAME_POOL[idx]}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Only append a surname if the name is a single word — otherwise it's
|
|
223
|
+
// already a full name (e.g. "Cori Samuel") and we leave it alone.
|
|
224
|
+
if (/\s/.test(rawName)) return rawName;
|
|
225
|
+
return `${rawName} ${SURNAME_POOL[0]}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Map a gender string to a single-character icon for compact display.
|
|
230
|
+
* "Female" → "♀"
|
|
231
|
+
* "Male" → "♂"
|
|
232
|
+
* anything else → "—"
|
|
233
|
+
*
|
|
234
|
+
* @param {string} gender
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
export function genderIcon(gender) {
|
|
238
|
+
if (gender === 'Female') return '♀';
|
|
239
|
+
if (gender === 'Male') return '♂';
|
|
240
|
+
return '—';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Same as genderIcon but wrapped in blessed color tags:
|
|
245
|
+
* Female → pink (magenta)
|
|
246
|
+
* Male → light blue (bright-cyan)
|
|
247
|
+
* other → no color
|
|
248
|
+
* The returned string is 1 visible char wide but contains color tags;
|
|
249
|
+
* callers should add padding spaces separately rather than using padEnd().
|
|
250
|
+
*
|
|
251
|
+
* @param {string} gender
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
export function genderIconTag(gender) {
|
|
255
|
+
if (gender === 'Female') return '{magenta-fg}♀{/magenta-fg}';
|
|
256
|
+
if (gender === 'Male') return '{bright-cyan-fg}♂{/bright-cyan-fg}';
|
|
257
|
+
return '—';
|
|
258
|
+
}
|
|
187
259
|
|
|
188
260
|
// ---------------------------------------------------------------------------
|
|
189
261
|
// Pure helpers — exported for testability
|
|
@@ -439,6 +511,9 @@ const _metaCache = new Map();
|
|
|
439
511
|
export function getVoiceMeta(voiceId) {
|
|
440
512
|
if (_metaCache.has(voiceId)) return _metaCache.get(voiceId);
|
|
441
513
|
|
|
514
|
+
// Lazy-load the catalog so callers from any tab get uniquified names
|
|
515
|
+
loadCatalog();
|
|
516
|
+
|
|
442
517
|
const ms = parseMultiSpeaker(voiceId);
|
|
443
518
|
if (ms.isMultiSpeaker) {
|
|
444
519
|
if (!ms.speakerName) {
|
|
@@ -446,7 +521,19 @@ export function getVoiceMeta(voiceId) {
|
|
|
446
521
|
_metaCache.set(voiceId, result);
|
|
447
522
|
return result;
|
|
448
523
|
}
|
|
449
|
-
|
|
524
|
+
// Prefer the catalog entry (which has the uniquified name + gender)
|
|
525
|
+
const cat = _catalogMap.get(voiceId);
|
|
526
|
+
if (cat) {
|
|
527
|
+
const result = {
|
|
528
|
+
displayName: cat.displayName,
|
|
529
|
+
gender: cat.gender || inferGender(ms.speakerName, null),
|
|
530
|
+
provider: `Piper (${ms.model})`,
|
|
531
|
+
};
|
|
532
|
+
_metaCache.set(voiceId, result);
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
// Fallback for speakers not in the catalog (e.g. 16Speakers model)
|
|
536
|
+
const displayName = uniquifyVoiceName(ms.speakerName.replace(/_/g, ' '));
|
|
450
537
|
const result = {
|
|
451
538
|
displayName,
|
|
452
539
|
gender: inferGender(ms.speakerName, null),
|
|
@@ -560,7 +647,7 @@ export function createVoicesTab(screen, services) {
|
|
|
560
647
|
parent: box,
|
|
561
648
|
top: 4,
|
|
562
649
|
left: 6,
|
|
563
|
-
content: `{#00897b-fg}${_tl('voicesColName').padEnd(COL_NAME_W)}
|
|
650
|
+
content: `{#00897b-fg}${_tl('voicesColName').padEnd(COL_NAME_W)}{/#00897b-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {#00897b-fg}${_tl('voicesColProvider')}{/#00897b-fg}`,
|
|
564
651
|
tags: true,
|
|
565
652
|
style: { bg: COLORS.contentBg },
|
|
566
653
|
});
|
|
@@ -621,7 +708,7 @@ export function createVoicesTab(screen, services) {
|
|
|
621
708
|
|
|
622
709
|
// -------------------------------------------------------------------------
|
|
623
710
|
// Hint text shown in previewLine when the list has focus and nothing is playing
|
|
624
|
-
const HINT_TEXT =
|
|
711
|
+
const HINT_TEXT = '{white-fg}[Space] preview [Enter] select as default voice{/white-fg}';
|
|
625
712
|
let _listFocused = false;
|
|
626
713
|
|
|
627
714
|
// Inline selection hint appended to the currently highlighted voice row.
|
|
@@ -647,21 +734,42 @@ export function createVoicesTab(screen, services) {
|
|
|
647
734
|
return g;
|
|
648
735
|
}
|
|
649
736
|
|
|
650
|
-
//
|
|
651
|
-
//
|
|
737
|
+
// Decoration helpers — strip blink cursor and (optionally) hint text from
|
|
738
|
+
// a list item's content. We use a precise approach instead of a generic
|
|
739
|
+
// regex over `bright-black-fg` blocks because uninstalled rows wrap their
|
|
740
|
+
// PROVIDER column in `bright-black-fg` too — a generic strip would erase it.
|
|
741
|
+
//
|
|
742
|
+
// _stripBlink(): only removes ` █` markers (safe to call on any row).
|
|
743
|
+
// _stripHint(): removes hint by anchoring to the exact hint text from
|
|
744
|
+
// _getRowHint(idx). Only safe on rows we know are hinted.
|
|
745
|
+
const _BLINK_RE = / █/g;
|
|
746
|
+
function _stripBlink(raw) {
|
|
747
|
+
return (raw ?? '').replace(_BLINK_RE, '');
|
|
748
|
+
}
|
|
749
|
+
function _stripHint(raw, idx) {
|
|
750
|
+
const hint = _getRowHint(idx);
|
|
751
|
+
if (!hint) return raw;
|
|
752
|
+
// The hint is appended to the end (possibly followed by blink); remove it.
|
|
753
|
+
const noBlink = (raw ?? '').replace(_BLINK_RE, '');
|
|
754
|
+
if (noBlink.endsWith(hint)) return noBlink.slice(0, -hint.length);
|
|
755
|
+
return noBlink;
|
|
756
|
+
}
|
|
757
|
+
function _stripDecorations(raw, idx) {
|
|
758
|
+
return _stripHint(_stripBlink(raw), idx);
|
|
759
|
+
}
|
|
760
|
+
|
|
652
761
|
function _updateHint(idx) {
|
|
653
762
|
const items = voiceList.items;
|
|
654
763
|
const _pad = ' '.repeat(60);
|
|
764
|
+
// Restore previously hinted row to its clean base
|
|
655
765
|
if (_hintIdx >= 0 && _hintIdx !== idx && items[_hintIdx]) {
|
|
656
|
-
|
|
657
|
-
items[_hintIdx].setContent((hadBlink ? _hintBase + ' █' : _hintBase) + _pad);
|
|
766
|
+
items[_hintIdx].setContent(_hintBase + _pad);
|
|
658
767
|
}
|
|
659
768
|
if (idx >= 0 && items[idx]) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
_hintBase
|
|
664
|
-
items[idx].setContent(c + _getRowHint(idx) + (hasBlink ? ' █' : ''));
|
|
769
|
+
// The row may already have a hint (from a prior _updateHint cycle)
|
|
770
|
+
// and/or a blink — strip both before capturing the clean base.
|
|
771
|
+
_hintBase = _stripDecorations(items[idx].content, idx);
|
|
772
|
+
items[idx].setContent(_hintBase + _getRowHint(idx));
|
|
665
773
|
} else {
|
|
666
774
|
_hintBase = '';
|
|
667
775
|
}
|
|
@@ -1008,7 +1116,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1008
1116
|
left: 2,
|
|
1009
1117
|
right: 2,
|
|
1010
1118
|
tags: true,
|
|
1011
|
-
content:
|
|
1119
|
+
content: '{white-fg}Press Preview to audition this voice{/white-fg}',
|
|
1012
1120
|
style: { bg: COLORS.contentBg },
|
|
1013
1121
|
});
|
|
1014
1122
|
|
|
@@ -1079,7 +1187,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1079
1187
|
const isPlaying = _playingVoiceId === voiceId;
|
|
1080
1188
|
_previewVoice(voiceId);
|
|
1081
1189
|
modalStatus.setContent(isPlaying
|
|
1082
|
-
?
|
|
1190
|
+
? '{white-fg}Stopped.{/white-fg}'
|
|
1083
1191
|
: `{${COLORS.activeFg}-fg}♪ Playing: ${displayName}…{/${COLORS.activeFg}-fg}`
|
|
1084
1192
|
);
|
|
1085
1193
|
screen.render();
|
|
@@ -1354,11 +1462,13 @@ export function createVoicesTab(screen, services) {
|
|
|
1354
1462
|
? displayName.slice(0, COL_NAME_W - 1) + '…'
|
|
1355
1463
|
: displayName.padEnd(COL_NAME_W);
|
|
1356
1464
|
|
|
1465
|
+
// genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
|
|
1466
|
+
const gIcon = genderIconTag(gender);
|
|
1357
1467
|
if (!installed) {
|
|
1358
|
-
// Greyed-out row for uninstalled catalog voices
|
|
1359
|
-
return `{bright-black-fg} ${star} ${name}${
|
|
1468
|
+
// Greyed-out row for uninstalled catalog voices — close grey wrap around the icon so its colors show
|
|
1469
|
+
return `{bright-black-fg} ${star} ${name}{/bright-black-fg}${gIcon} {bright-black-fg}${provider}{/bright-black-fg}`;
|
|
1360
1470
|
}
|
|
1361
|
-
return `{${COLORS.labelFg}-fg} ${star}${dot} ${name}${
|
|
1471
|
+
return `{${COLORS.labelFg}-fg} ${star}${dot} ${name}{/${COLORS.labelFg}-fg}${gIcon} {${COLORS.labelFg}-fg}${provider}${isPrev ? ` ${_tl('voicePlaying')}` : ''}{/${COLORS.labelFg}-fg}`;
|
|
1362
1472
|
});
|
|
1363
1473
|
}
|
|
1364
1474
|
|
|
@@ -1511,7 +1621,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1511
1621
|
});
|
|
1512
1622
|
|
|
1513
1623
|
// 'f' or '*' in voiceList toggles favorite
|
|
1514
|
-
voiceList.key(['
|
|
1624
|
+
voiceList.key(['*'], () => {
|
|
1515
1625
|
const voices = _getFilteredVoices();
|
|
1516
1626
|
const selected = voices[voiceList.selected];
|
|
1517
1627
|
if (selected) {
|
|
@@ -1560,13 +1670,16 @@ export function createVoicesTab(screen, services) {
|
|
|
1560
1670
|
_vlBlink.on = !_vlBlink.on;
|
|
1561
1671
|
const items = voiceList.items;
|
|
1562
1672
|
const cur = voiceList.selected ?? 0;
|
|
1673
|
+
// Clean old row — only strip blink (it's not the hinted row anymore,
|
|
1674
|
+
// and we don't want to accidentally erase its provider column).
|
|
1563
1675
|
if (_vlBlink.sel !== cur && _vlBlink.sel >= 0 && items[_vlBlink.sel]) {
|
|
1564
|
-
items[_vlBlink.sel].setContent((items[_vlBlink.sel].content
|
|
1676
|
+
items[_vlBlink.sel].setContent(_stripBlink(items[_vlBlink.sel].content));
|
|
1565
1677
|
}
|
|
1566
1678
|
_vlBlink.sel = cur;
|
|
1567
1679
|
if (items[cur]) {
|
|
1568
|
-
|
|
1569
|
-
items[cur].
|
|
1680
|
+
// Get content without blink but preserve hint
|
|
1681
|
+
const raw = _stripBlink(items[cur].content);
|
|
1682
|
+
items[cur].setContent(_vlBlink.on ? `${raw} █` : raw);
|
|
1570
1683
|
}
|
|
1571
1684
|
screen.render();
|
|
1572
1685
|
}
|
|
@@ -1578,7 +1691,10 @@ export function createVoicesTab(screen, services) {
|
|
|
1578
1691
|
_hintBase = '';
|
|
1579
1692
|
_updateHint(_vlBlink.sel);
|
|
1580
1693
|
const items = voiceList.items;
|
|
1581
|
-
if (items[_vlBlink.sel])
|
|
1694
|
+
if (items[_vlBlink.sel]) {
|
|
1695
|
+
const raw = _stripBlink(items[_vlBlink.sel].content);
|
|
1696
|
+
items[_vlBlink.sel].setContent(raw + ' █');
|
|
1697
|
+
}
|
|
1582
1698
|
if (!_playingVoiceId) previewLine.setContent(HINT_TEXT);
|
|
1583
1699
|
screen.render();
|
|
1584
1700
|
_vlBlink.interval = setInterval(_vlTick, 500);
|
|
@@ -1588,12 +1704,19 @@ export function createVoicesTab(screen, services) {
|
|
|
1588
1704
|
if (!_playingVoiceId) previewLine.setContent('');
|
|
1589
1705
|
if (_vlBlink.interval) { clearInterval(_vlBlink.interval); _vlBlink.interval = null; }
|
|
1590
1706
|
const items = voiceList.items;
|
|
1707
|
+
// Clean up: strip blink from selected row, strip hint from hinted row.
|
|
1708
|
+
// Only the hinted row knows what its hint text was — use _stripDecorations
|
|
1709
|
+
// there. For other rows, only strip blink.
|
|
1591
1710
|
const sel = voiceList.selected ?? 0;
|
|
1592
1711
|
if (items[sel]) {
|
|
1593
|
-
|
|
1712
|
+
if (sel === _hintIdx) {
|
|
1713
|
+
items[sel].setContent(_stripDecorations(items[sel].content, sel));
|
|
1714
|
+
} else {
|
|
1715
|
+
items[sel].setContent(_stripBlink(items[sel].content));
|
|
1716
|
+
}
|
|
1594
1717
|
}
|
|
1595
1718
|
if (_hintIdx >= 0 && _hintIdx !== sel && items[_hintIdx]) {
|
|
1596
|
-
items[_hintIdx].setContent(
|
|
1719
|
+
items[_hintIdx].setContent(_stripDecorations(items[_hintIdx].content, _hintIdx));
|
|
1597
1720
|
}
|
|
1598
1721
|
_hintIdx = -1;
|
|
1599
1722
|
_hintBase = '';
|
|
@@ -1612,7 +1735,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1612
1735
|
});
|
|
1613
1736
|
|
|
1614
1737
|
// Type-to-jump: press a letter to jump to first voice whose display name starts with it
|
|
1615
|
-
const _voiceJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u'
|
|
1738
|
+
const _voiceJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u']);
|
|
1616
1739
|
voiceList.on('keypress', (ch, key) => {
|
|
1617
1740
|
if (!ch || key.ctrl || key.meta) return;
|
|
1618
1741
|
const lower = ch.toLowerCase();
|
|
@@ -1665,7 +1788,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1665
1788
|
function refreshVoicesLabels() {
|
|
1666
1789
|
voicesSectionHdr.setContent(`{#00897b-fg}${_tl('voicesHeader')}${'─'.repeat(58)}{/#00897b-fg}`);
|
|
1667
1790
|
searchLabelText.setContent(_tl('searchLabel'));
|
|
1668
|
-
colHeaderText.setContent(`{#00897b-fg}${_tl('voicesColName').padEnd(COL_NAME_W)}
|
|
1791
|
+
colHeaderText.setContent(`{#00897b-fg}${_tl('voicesColName').padEnd(COL_NAME_W)}{/#00897b-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {#00897b-fg}${_tl('voicesColProvider')}{/#00897b-fg}`);
|
|
1669
1792
|
voiceInfoHdr.setContent(`{#00897b-fg}${_tl('voicesInfoHeader')}${'─'.repeat(54)}{/#00897b-fg}`);
|
|
1670
1793
|
switchBtn.setContent(_tl('voicesSwitchBtn'));
|
|
1671
1794
|
favoriteBtn.setContent(_tl('voicesFavoriteBtn'));
|
|
@@ -1,89 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Format Utilities
|
|
3
|
-
*
|
|
4
|
-
* Pure formatting functions extracted from settings-tab.js to avoid
|
|
5
|
-
* circular imports between widgets and tabs.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const TRACK_NAMES = Object.freeze({
|
|
9
|
-
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
10
|
-
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
11
|
-
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
12
|
-
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
13
|
-
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
14
|
-
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
15
|
-
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
16
|
-
'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
|
|
17
|
-
'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
|
|
18
|
-
'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
|
|
19
|
-
'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
|
|
20
|
-
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
|
|
21
|
-
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
22
|
-
'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
|
|
23
|
-
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.replace(
|
|
38
|
-
.replace(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (voice
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Format Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure formatting functions extracted from settings-tab.js to avoid
|
|
5
|
+
* circular imports between widgets and tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TRACK_NAMES = Object.freeze({
|
|
9
|
+
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
10
|
+
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
11
|
+
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
12
|
+
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
13
|
+
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
14
|
+
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
15
|
+
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
16
|
+
'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
|
|
17
|
+
'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
|
|
18
|
+
'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
|
|
19
|
+
'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
|
|
20
|
+
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
|
|
21
|
+
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
22
|
+
'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
|
|
23
|
+
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
24
|
+
'Late Night Hip Hop Groove.mp3': '🎤 Late Night Hip Hop Groove',
|
|
25
|
+
'Drifting Down the Hall.mp3': '🌃 Drifting Down the Hall',
|
|
26
|
+
'Midnight Charleston Stomp.mp3': '🎩 Midnight Charleston Stomp',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} track - filename (e.g. 'agentvibes_soft_flamenco_loop.mp3')
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function formatTrackName(track) {
|
|
34
|
+
if (!track) return 'None';
|
|
35
|
+
if (TRACK_NAMES[track]) return TRACK_NAMES[track];
|
|
36
|
+
return track
|
|
37
|
+
.replace(/\.[^.]+$/, '')
|
|
38
|
+
.replace(/^agentvibes_|^agent_vibes_/, '')
|
|
39
|
+
.replace(/_v\d+_loop$|_loop$|_v\d+$/, '')
|
|
40
|
+
.replace(/_/g, ' ')
|
|
41
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Beautify a raw voice identifier for display in narrow table columns.
|
|
46
|
+
*
|
|
47
|
+
* Examples:
|
|
48
|
+
* 16Speakers::Rose_Ibex → Rose Ibex
|
|
49
|
+
* 16Speakers::Emily_Cripps → Emily Cripps
|
|
50
|
+
* en_US-kusal-medium → Kusal
|
|
51
|
+
* en_US-lessac-high → Lessac
|
|
52
|
+
* en_US-libritts_r-medium → Libritts R
|
|
53
|
+
* kristin → Kristin
|
|
54
|
+
*
|
|
55
|
+
* @param {string} voice - raw voice identifier
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function formatVoiceName(voice) {
|
|
59
|
+
if (!voice) return '(global)';
|
|
60
|
+
|
|
61
|
+
let name;
|
|
62
|
+
if (voice.includes('::')) {
|
|
63
|
+
// 16Speakers::Rose_Ibex → extract after '::'
|
|
64
|
+
name = voice.split('::')[1];
|
|
65
|
+
} else {
|
|
66
|
+
const parts = voice.split('-');
|
|
67
|
+
const QUALITIES = new Set(['high', 'medium', 'low']);
|
|
68
|
+
if (parts.length >= 2 && /^[a-z]{2}_[A-Z]{2}$/.test(parts[0])) {
|
|
69
|
+
// Strip locale prefix and quality suffix
|
|
70
|
+
name = parts.slice(1).filter(p => !QUALITIES.has(p)).join(' ');
|
|
71
|
+
} else {
|
|
72
|
+
name = voice;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Replace underscores with spaces, title-case each word
|
|
77
|
+
return name
|
|
78
|
+
.replace(/_/g, ' ')
|
|
79
|
+
.split(' ')
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
82
|
+
.join(' ') || '(global)';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} preset - 'off' | 'light' | 'medium' | 'heavy' | 'cathedral'
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function formatReverbState(preset) {
|
|
90
|
+
const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
|
|
91
|
+
return LABELS[preset] ?? LABELS.light;
|
|
92
|
+
}
|