agentvibes 4.4.0 → 4.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "4.4.0",
4
+ "version": "4.4.1",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [
@@ -37,8 +37,18 @@ function _resolvePiperBin() {
37
37
  const lad = process.env.LOCALAPPDATA ||
38
38
  (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
39
39
  if (lad) {
40
+ // Standalone binary install
40
41
  const exe = path.join(lad, 'Programs', 'Piper', 'piper.exe');
41
42
  if (fs.existsSync(exe)) return exe;
43
+ // pip-installed piper (Python Scripts directory)
44
+ const pyScripts = path.join(lad, 'Programs', 'Python');
45
+ try {
46
+ const pyDirs = fs.readdirSync(pyScripts).filter(d => d.startsWith('Python'));
47
+ for (const d of pyDirs) {
48
+ const pipExe = path.join(pyScripts, d, 'Scripts', 'piper.exe');
49
+ if (fs.existsSync(pipExe)) return pipExe;
50
+ }
51
+ } catch { /* no Python installs */ }
42
52
  }
43
53
  }
44
54
  return 'piper';
@@ -906,13 +916,18 @@ export function createSettingsTab(screen, services) {
906
916
  if (!_samplePlaying) { try { fs.unlinkSync(tempWav); } catch {} return; }
907
917
  if (code !== 0) {
908
918
  _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn);
919
+ _showNotice(screen, 'Voice synthesis failed — check voice model');
909
920
  try { fs.unlinkSync(tempWav); } catch {}
910
921
  return;
911
922
  }
912
923
  playBtn.setContent('■ Stop');
913
924
  screen.render();
914
925
  const _wavPlayer2 = detectWavPlayer(_sampleEnv);
915
- if (!_wavPlayer2) { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); screen.render(); return; }
926
+ if (!_wavPlayer2) {
927
+ _stopSpinner(); _killSample(); playBtn.setContent('▶ Play');
928
+ _showNotice(screen, 'No audio player found — install ffplay, sox, or mpv');
929
+ screen.render(); return;
930
+ }
916
931
  const playProc = spawn(_wavPlayer2.bin, _wavPlayer2.args(tempWav), _spawnOpts(_sampleEnv));
917
932
  _sampleProcess = playProc;
918
933
  const _done = () => { _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); try { fs.unlinkSync(tempWav); } catch {} };
@@ -1027,23 +1042,59 @@ export function createSettingsTab(screen, services) {
1027
1042
  // Piper (default): pipe text via stdin
1028
1043
  _startSpinner(playBtn, 'Synthesizing…');
1029
1044
  const voiceId = providerService.getActiveVoiceId();
1030
- if (!voiceId) { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); screen.render(); return; }
1045
+ if (!voiceId) {
1046
+ _stopSpinner(); _killSample(); playBtn.setContent('▶ Play');
1047
+ _showNotice(screen, 'No voice selected — choose a voice first');
1048
+ screen.render(); return;
1049
+ }
1031
1050
  const _ms2 = parseMultiSpeaker(voiceId);
1032
1051
  const voicePath = path.resolve(PIPER_VOICES_DIR, _ms2.model + '.onnx');
1033
1052
  const safeBase = path.resolve(PIPER_VOICES_DIR);
1034
1053
  if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) {
1035
- _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); screen.render(); return;
1054
+ _stopSpinner(); _killSample(); playBtn.setContent('▶ Play');
1055
+ _showNotice(screen, 'Invalid voice path');
1056
+ screen.render(); return;
1057
+ }
1058
+ const piperBin2 = _resolvePiperBin();
1059
+ if (piperBin2 === 'piper') {
1060
+ // Bare command — verify it exists in PATH before spawning
1061
+ const whichCmd = _IS_WINDOWS ? 'where' : 'which';
1062
+ const whichResult = spawnSync(whichCmd, [_IS_WINDOWS ? 'piper.exe' : 'piper'], { stdio: 'pipe', env: _sampleEnv });
1063
+ if (whichResult.status !== 0) {
1064
+ _stopSpinner(); _killSample(); playBtn.setContent('▶ Play');
1065
+ _showNotice(screen, 'Piper not installed — run the installer or: pip install piper-tts');
1066
+ _focusButton(playBtn); screen.render(); return;
1067
+ }
1036
1068
  }
1037
1069
  const _piperArgs2 = ['--model', voicePath, '--output_file', tempWav];
1038
1070
  if (_ms2.speakerId != null) _piperArgs2.push('--speaker', String(_ms2.speakerId));
1039
- const piper = spawn(_resolvePiperBin(), _piperArgs2, {
1040
- stdio: ['pipe', 'ignore', 'ignore'], detached: !_IS_WINDOWS, windowsHide: true, env: _sampleEnv,
1071
+ const piper = spawn(piperBin2, _piperArgs2, {
1072
+ stdio: ['pipe', 'ignore', 'pipe'], detached: !_IS_WINDOWS, windowsHide: true, env: _sampleEnv,
1041
1073
  });
1074
+ let _piperStderr = '';
1075
+ piper.stderr.on('data', (d) => { _piperStderr += d.toString(); });
1042
1076
  piper.stdin.write(phrase + '\n');
1043
1077
  piper.stdin.end();
1044
1078
  _sampleProcess = piper;
1045
- piper.on('exit', _onSynthDone);
1046
- piper.on('error', () => { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); });
1079
+ piper.on('exit', (code) => {
1080
+ if (code !== 0 && _piperStderr) {
1081
+ // Python tracebacks: actual error is the LAST non-empty line
1082
+ const lines = _piperStderr.split('\n').map(l => l.trim()).filter(Boolean);
1083
+ const errLine = lines[lines.length - 1] || lines[0] || 'unknown error';
1084
+ _stopSpinner();
1085
+ if (!_samplePlaying) { try { fs.unlinkSync(tempWav); } catch {} return; }
1086
+ _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn);
1087
+ _showNotice(screen, errLine.length > 100 ? errLine.substring(0, 97) + '…' : errLine);
1088
+ try { fs.unlinkSync(tempWav); } catch {}
1089
+ return;
1090
+ }
1091
+ _onSynthDone(code);
1092
+ });
1093
+ piper.on('error', (e) => {
1094
+ _stopSpinner(); _killSample(); playBtn.setContent('▶ Play');
1095
+ _showNotice(screen, `Piper failed: ${e.message}`);
1096
+ _focusButton(playBtn);
1097
+ });
1047
1098
  }
1048
1099
  });
1049
1100
  playBtn.top = 7;
package/src/installer.js CHANGED
@@ -5139,6 +5139,19 @@ Troubleshooting:
5139
5139
  }
5140
5140
  await fs.writeFile(voiceConfigPath, defaultVoice);
5141
5141
 
5142
+ // Sync voice + provider to global .agentvibes/config.json so TUI finds them
5143
+ // regardless of which directory it's launched from
5144
+ const globalAvDir = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes');
5145
+ try {
5146
+ await fs.mkdir(globalAvDir, { recursive: true });
5147
+ const globalCfgPath = path.join(globalAvDir, 'config.json');
5148
+ let globalCfg = {};
5149
+ try { globalCfg = JSON.parse(await fs.readFile(globalCfgPath, 'utf8')); } catch { /* new file */ }
5150
+ globalCfg.voice = defaultVoice;
5151
+ globalCfg.provider = selectedProvider;
5152
+ await fs.writeFile(globalCfgPath, JSON.stringify(globalCfg, null, 2), { mode: 0o600 });
5153
+ } catch { /* best-effort global sync */ }
5154
+
5142
5155
  // Detect and migrate old configuration
5143
5156
  await detectAndMigrateOldConfig(targetDir, silentSpinner);
5144
5157
 
@@ -39,6 +39,7 @@ export class ProviderService {
39
39
  */
40
40
  setActiveProvider(provider) {
41
41
  this._config.set('provider', provider);
42
+ this._config.setGlobal('provider', provider);
42
43
  this._syncProviderFile(provider);
43
44
  }
44
45
 
@@ -95,19 +96,29 @@ export class ProviderService {
95
96
 
96
97
  /**
97
98
  * Returns the currently active voice ID from config.
98
- * Defaults to 'en_US-amy-medium' if not configured.
99
- * @returns {string}
99
+ * Falls back to first installed voice if not configured.
100
+ * @returns {string|null}
100
101
  */
101
102
  getActiveVoiceId() {
102
- return this._config.getConfig().voice ?? 'en_US-amy-medium';
103
+ const voice = this._config.getConfig().voice;
104
+ if (voice) return voice;
105
+ // Detect first installed voice instead of hardcoding a default that may not exist
106
+ const voicesDir = path.join(os.homedir(), '.claude', 'piper-voices');
107
+ try {
108
+ const models = fs.readdirSync(voicesDir).filter(f => f.endsWith('.onnx'));
109
+ if (models.length > 0) return models[0].replace(/\.onnx$/, '');
110
+ } catch { /* dir may not exist */ }
111
+ return null;
103
112
  }
104
113
 
105
114
  /**
106
115
  * Sets the active voice ID in config.
116
+ * Writes to both project (if exists) and global config for portability.
107
117
  * @param {string} voiceId
108
118
  */
109
119
  setActiveVoice(voiceId) {
110
120
  this._config.set('voice', voiceId);
121
+ this._config.setGlobal('voice', voiceId);
111
122
  }
112
123
 
113
124
  // ---------------------------------------------------------------------------