agentvibes 4.6.0 → 4.6.2

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.
@@ -190,12 +190,13 @@ export const COL_GENDER_W = 10;
190
190
 
191
191
  // Well-known piper dataset → gender
192
192
  const GENDER_MAP = {
193
+ // Single-speaker datasets
193
194
  amy: 'Female', kristin: 'Female', jenny: 'Female', cori: 'Female',
194
195
  aria: 'Female', glados: 'Female', litvyak: 'Female', hfc_female: 'Female',
195
196
  ljspeech: 'Female',
196
197
  alan: 'Male', joe: 'Male', john: 'Male', ryan: 'Male', lessac: 'Male',
197
198
  kusal: 'Male', hfc_male: 'Male', danny: 'Male', arctic: 'Male',
198
- l2arctic: 'Male', libritts: 'Male', libritts_r: 'Male',
199
+ l2arctic: 'Male',
199
200
  // 16Speakers multi-speaker model (names from speaker_id_map)
200
201
  cori_samuel: 'Female', kara_shallenberg: 'Female', kristin_hughes: 'Female',
201
202
  maria_kasper: 'Female', rose_ibex: 'Female', owlivia: 'Female',
@@ -203,6 +204,21 @@ const GENDER_MAP = {
203
204
  mike_pelton: 'Male', mark_nelson: 'Male', michael_scherer: 'Male',
204
205
  james_k_white: 'Male', progressingamerica: 'Male', steve_c: 'Male',
205
206
  paul_hampton: 'Male', martin_clifton: 'Male',
207
+ // LibriTTS / common first names used as multi-speaker speaker IDs
208
+ anna: 'Female', bella: 'Female', chloe: 'Female', donna: 'Female',
209
+ ella: 'Female', faith: 'Female', gina: 'Female', holly: 'Female',
210
+ ivy: 'Female', jane: 'Female', kelly: 'Female', laura: 'Female',
211
+ mary: 'Female', nina: 'Female', olivia: 'Female', penny: 'Female',
212
+ rachel: 'Female', sarah: 'Female', tara: 'Female', uma: 'Female',
213
+ vera: 'Female', wendy: 'Female', yara: 'Female', zoe: 'Female',
214
+ betty: 'Female', cindy: 'Female', debra: 'Female', erica: 'Female',
215
+ faye: 'Female', gloria: 'Female', quinn: 'Female',
216
+ alex: 'Male', ben: 'Male', carl: 'Male', dan: 'Male', evan: 'Male',
217
+ frank: 'Male', greg: 'Male', hank: 'Male', ivan: 'Male', jake: 'Male',
218
+ kevin: 'Male', leo: 'Male', mike: 'Male', nathan: 'Male', oscar: 'Male',
219
+ paul: 'Male', rick: 'Male', sam: 'Male', tom: 'Male', victor: 'Male',
220
+ will: 'Male', xavier: 'Male', zach: 'Male', adam: 'Male', brad: 'Male',
221
+ colin: 'Male', derek: 'Male', ethan: 'Male', felix: 'Male',
206
222
  };
207
223
 
208
224
  // Well-known piper dataset → nice display name
@@ -229,9 +245,15 @@ export function inferGender(voiceId, dataset) {
229
245
  // Explicit in name
230
246
  if (id.includes('_female') || ds.includes('female')) return 'Female';
231
247
  if (id.includes('_male') || ds.includes('male')) return 'Male';
232
- // Lookup by dataset, name segment, or full id (for multi-speaker names)
233
- const key = ds || (id.split('-')[1] ?? '');
234
- return GENDER_MAP[key] ?? GENDER_MAP[id] ?? '—';
248
+ // Dataset lookup first
249
+ if (ds && GENDER_MAP[ds]) return GENDER_MAP[ds];
250
+ // For multi-speaker speaker names like "Anna-9", strip trailing "-N" suffix
251
+ // then look up the base name (e.g. "anna")
252
+ const baseName = id.replace(/-\d+$/, '');
253
+ if (GENDER_MAP[baseName]) return GENDER_MAP[baseName];
254
+ // Fall back to middle segment of voice ID (e.g. "ryan" from "en_US-ryan-high")
255
+ const segment = id.split('-')[1] ?? '';
256
+ return GENDER_MAP[segment] ?? GENDER_MAP[id] ?? '—';
235
257
  }
236
258
 
237
259
  /**
@@ -321,7 +343,18 @@ export function parseMultiSpeaker(voiceId) {
321
343
  const jsonPath = path.join(PIPER_VOICES_DIR, model + '.onnx.json');
322
344
  try {
323
345
  const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
324
- const speakerId = data.speaker_id_map?.[speakerName] ?? null;
346
+ let speakerId = data.speaker_id_map?.[speakerName] ?? null;
347
+ // Fallback: if the .onnx.json still has raw p-names (not yet patched),
348
+ // look up the numeric speaker ID from voice-assignments.json catalog.
349
+ if (speakerId == null && model === 'en_US-libritts-high') {
350
+ try {
351
+ const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
352
+ const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
353
+ const speakers = catalog.libritts_speakers ?? {};
354
+ const entry = Object.entries(speakers).find(([, e]) => e.voice_name === speakerName);
355
+ if (entry) speakerId = parseInt(entry[0], 10);
356
+ } catch { /* non-fatal */ }
357
+ }
325
358
  return { model, speakerId, speakerName, isMultiSpeaker: true };
326
359
  } catch {
327
360
  return { model, speakerId: null, speakerName, isMultiSpeaker: true };
@@ -42,30 +42,48 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
42
42
  parent: screen,
43
43
  top: 'center',
44
44
  left: 'center',
45
- width: 38,
46
- height: 8,
45
+ width: 44,
46
+ height: 11,
47
47
  border: { type: 'line' },
48
48
  tags: true,
49
49
  label: _modalTitle('Music Volume'),
50
50
  style: { border: { fg: 'bright-cyan' } },
51
51
  });
52
52
 
53
- const barText = blessed.text({
53
+ blessed.text({
54
54
  parent: box,
55
55
  top: 1,
56
56
  left: 2,
57
- width: 32,
57
+ width: 38,
58
+ tags: true,
59
+ content: '{cyan-fg}Use ← → arrow keys to adjust volume{/cyan-fg}',
60
+ });
61
+
62
+ const barText = blessed.text({
63
+ parent: box,
64
+ top: 3,
65
+ left: 2,
66
+ width: 38,
58
67
  tags: true,
59
68
  content: '',
60
69
  });
61
70
 
62
- const hint = blessed.text({
71
+ blessed.text({
63
72
  parent: box,
64
73
  top: 5,
65
- left: 1,
66
- width: 34,
74
+ left: 2,
75
+ width: 38,
67
76
  tags: true,
68
- content: '{#455a64-fg}[←→] ±5 [1-9] type [Enter] OK [Esc] Cancel{/#455a64-fg}',
77
+ content: '{white-fg}[← →] ±5 [0-9] number [Esc] Cancel{/white-fg}',
78
+ });
79
+
80
+ blessed.text({
81
+ parent: box,
82
+ top: 7,
83
+ left: 2,
84
+ width: 38,
85
+ tags: true,
86
+ content: '{white-fg}[Enter] Confirm then {bold}{cyan-fg}[Tab]{/cyan-fg}{/bold} → Save{/white-fg}',
69
87
  });
70
88
 
71
89
  function _renderBar() {
@@ -73,10 +91,13 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
73
91
  const empty = 20 - filled;
74
92
  const bar = '{bright-cyan-fg}' + '█'.repeat(filled) + '{/bright-cyan-fg}' +
75
93
  '{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
76
- barText.setContent(`{#90a4ae-fg}Volume:{/#90a4ae-fg} ${bar} {bold}${vol}%{/bold}`);
94
+ barText.setContent(`{white-fg}Volume:{/white-fg} ${bar} {bold}${vol}%{/bold}`);
77
95
  screen.render();
78
96
  }
79
97
  _renderBar();
98
+ // Take focus so fieldList's key handlers don't fire while this dialog is open
99
+ box.focus();
100
+ screen.render();
80
101
 
81
102
  // Capture keypress directly on screen to avoid input mode issues
82
103
  let _digits = '';
@@ -99,8 +120,12 @@ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
99
120
  screen.removeListener('keypress', _onKey);
100
121
  box.destroy();
101
122
  screen.render();
102
- if (confirm && onConfirm) onConfirm(vol);
103
- if (onClose) onClose();
123
+ // Defer callbacks so the Enter keypress finishes propagating before fieldList
124
+ // regains focus — otherwise the same Enter event re-opens the track picker.
125
+ setTimeout(() => {
126
+ if (confirm && onConfirm) onConfirm(vol);
127
+ if (onClose) onClose();
128
+ }, 0);
104
129
  }
105
130
  }
106
131
 
@@ -121,7 +146,7 @@ const BUILT_IN_TRACKS = [
121
146
  * @param {Function} onSelect - called with (trackFile, volume)
122
147
  * @param {Function} [onClose] - called after modal fully closes
123
148
  */
124
- export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose) {
149
+ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose, options = {}) {
125
150
  const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
126
151
  let tracks;
127
152
  try {
@@ -271,17 +296,24 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
271
296
  if (selected) _previewTrack(selected.file);
272
297
  });
273
298
 
274
- // Enter = select track, then prompt for volume
299
+ // Enter = select track; if skipVolume, return track only, otherwise prompt for volume
275
300
  list.key(['enter'], () => {
276
301
  const selected = tracks[list.selected];
277
302
  if (!selected) return;
278
- // Close the track list first (without firing onClose yet), then open volume input
279
303
  _killPreview();
280
304
  if (list._label2) list._label2.destroy();
281
- destroyList(list, screen, null);
282
- openVolumeInput(screen, currentVolume ?? 20, (volume) => {
283
- onSelect(selected.file, volume);
284
- }, onClose);
305
+ if (options.skipVolume) {
306
+ destroyList(list, screen, null);
307
+ setTimeout(() => {
308
+ onSelect(selected.file);
309
+ if (onClose) onClose();
310
+ }, 0);
311
+ } else {
312
+ destroyList(list, screen, null);
313
+ openVolumeInput(screen, currentVolume ?? 20, (volume) => {
314
+ onSelect(selected.file, volume);
315
+ }, onClose);
316
+ }
285
317
  });
286
318
 
287
319
  list.key(['escape', 'q'], () => {
@@ -441,7 +441,7 @@ if [[ -n "$BG_FILE" ]] && command -v ffmpeg &>/dev/null; then
441
441
  TOTAL_DUR=$(awk "BEGIN {printf \"%.2f\", $DURATION + 2}")
442
442
  FADE_OUT=$(awk "BEGIN {printf \"%.2f\", $DURATION}")
443
443
  timeout 20 ffmpeg -y -i "$PLAY_FILE" -stream_loop -1 -i "$BG_PATH" \
444
- -filter_complex "[1:a]volume=${BG_VOLUME},afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT}:d=2[bg];[0:a]adelay=2000|2000[v];[v][bg]amix=inputs=2:duration=longest[out]" \
444
+ -filter_complex "[1:a]volume=${BG_VOLUME},afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT}:d=2[bg];[0:a]adelay=1000|1000,volume=1.5[v];[v][bg]amix=inputs=2:duration=longest:normalize=0[out]" \
445
445
  -map "[out]" -t "$TOTAL_DUR" "$FINAL_WAV" </dev/null 2>/dev/null && PLAY_FILE="$FINAL_WAV"
446
446
  fi
447
447
  fi