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.
|
|
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) {
|
|
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) {
|
|
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');
|
|
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(
|
|
1040
|
-
stdio: ['pipe', 'ignore', '
|
|
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',
|
|
1046
|
-
|
|
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
|
-
*
|
|
99
|
-
* @returns {string}
|
|
99
|
+
* Falls back to first installed voice if not configured.
|
|
100
|
+
* @returns {string|null}
|
|
100
101
|
*/
|
|
101
102
|
getActiveVoiceId() {
|
|
102
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|