agentgui 1.0.207 → 1.0.209

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.md CHANGED
@@ -89,3 +89,53 @@ Server broadcasts:
89
89
  - `streaming_complete` - Execution finished
90
90
  - `streaming_error` - Execution failed
91
91
  - `conversation_created`, `conversation_updated`, `conversation_deleted`
92
+ - `tts_setup_progress` - Windows pocket-tts setup progress (step, status, message)
93
+
94
+ ## Pocket-TTS Windows Setup (Reliability for Slow/Bad Internet)
95
+
96
+ On Windows, text-to-speech uses pocket-tts which requires Python and pip install. The setup process is now resilient to slow/unreliable connections:
97
+
98
+ ### Features
99
+ - **Extended timeouts**: 120s for pip install (accommodates slow connections)
100
+ - **Retry logic**: 3 attempts with exponential backoff (1s, 2s delays)
101
+ - **Progress reporting**: Real-time updates via WebSocket to UI
102
+ - **Partial install cleanup**: Failed venvs are removed to allow retry
103
+ - **Installation verification**: Binary validation via `--version` check
104
+ - **Concurrent waiting**: Multiple simultaneous requests wait for single setup (600s timeout)
105
+
106
+ ### Configuration (lib/windows-pocket-tts-setup.js)
107
+ ```javascript
108
+ const CONFIG = {
109
+ PIP_TIMEOUT: 120000, // 2 minutes
110
+ VENV_CREATION_TIMEOUT: 30000, // 30 seconds
111
+ MAX_RETRIES: 3, // 3 attempts
112
+ RETRY_DELAY_MS: 1000, // 1 second initial
113
+ RETRY_BACKOFF_MULTIPLIER: 2, // 2x exponential
114
+ };
115
+ ```
116
+
117
+ ### Network Requirements
118
+ - **Minimum**: 50 kbps sustained, < 5s latency, < 10% packet loss
119
+ - **Recommended**: 256+ kbps, < 2s latency, < 1% packet loss
120
+ - **Expected time on slow connection**: 2-6 minutes with retries
121
+
122
+ ### Progress Messages
123
+ During TTS setup on first use, WebSocket broadcasts:
124
+ ```json
125
+ {
126
+ "type": "tts_setup_progress",
127
+ "step": "detecting-python|creating-venv|installing|verifying",
128
+ "status": "in-progress|success|error",
129
+ "message": "descriptive status message with retry count if applicable"
130
+ }
131
+ ```
132
+
133
+ ### Recovery Behavior
134
+ 1. Network timeout → auto-retry with backoff
135
+ 2. Partial venv → auto-cleanup before retry
136
+ 3. Failed verification → auto-cleanup and error
137
+ 4. Concurrent requests → first starts setup, others wait up to 600s
138
+ 5. Interrupted setup → cleanup allows fresh retry
139
+
140
+ ### Testing
141
+ Setup validates by running pocket-tts binary with `--version` flag to confirm functional installation, not just file existence.
package/lib/speech.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createRequire } from 'module';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import os from 'os';
4
5
  import http from 'http';
5
6
  import { fileURLToPath } from 'url';
7
+ import { patchWebtalkForWindows } from './webtalk-patch.js';
6
8
 
7
9
  const require = createRequire(import.meta.url);
8
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -11,6 +13,8 @@ const ROOT = path.dirname(__dirname);
11
13
  const serverSTT = require('webtalk/server-stt');
12
14
  const serverTTS = require('webtalk/server-tts');
13
15
 
16
+ patchWebtalkForWindows(serverTTS);
17
+
14
18
  const EXTRA_VOICE_DIRS = [path.join(ROOT, 'voices')];
15
19
 
16
20
  const POCKET_TTS_VOICES = [
@@ -120,7 +124,14 @@ function getStatus() {
120
124
  function preloadTTS() {
121
125
  const defaultVoice = serverTTS.findVoiceFile('custom_cleetus', EXTRA_VOICE_DIRS) || '/config/voices/cleetus.wav';
122
126
  const voicePath = fs.existsSync(defaultVoice) ? defaultVoice : null;
123
- serverTTS.start(voicePath).then(ok => {
127
+ const options = {
128
+ binaryPaths: [
129
+ path.join(os.homedir(), '.gmgui', 'pocket-venv', 'Scripts', 'pocket-tts.exe'),
130
+ path.join(os.homedir(), '.gmgui', 'pocket-venv', 'bin', 'pocket-tts.exe'),
131
+ path.join(os.homedir(), '.gmgui', 'pocket-venv', 'bin', 'pocket-tts'),
132
+ ]
133
+ };
134
+ serverTTS.start(voicePath, options).then(ok => {
124
135
  if (ok) console.log('[TTS] pocket-tts sidecar started');
125
136
  else console.log('[TTS] pocket-tts failed to start');
126
137
  }).catch(err => {
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export function patchWebtalkForWindows(serverTTS) {
6
+ if (process.platform !== 'win32') return;
7
+
8
+ const venvDir = path.join(os.homedir(), '.gmgui', 'pocket-venv');
9
+
10
+ // Check if pocket-tts exists at Windows paths
11
+ const windowsBinaries = [
12
+ path.join(venvDir, 'Scripts', 'pocket-tts.exe'),
13
+ path.join(venvDir, 'bin', 'pocket-tts.exe'),
14
+ path.join(venvDir, 'bin', 'pocket-tts'),
15
+ ];
16
+
17
+ const found = windowsBinaries.find(p => fs.existsSync(p));
18
+
19
+ if (found) {
20
+ // Patch the start function to use the correct binary
21
+ const originalStart = serverTTS.start;
22
+
23
+ serverTTS.start = function(voicePath, options) {
24
+ if (!options) options = {};
25
+ if (!options.binaryPaths) options.binaryPaths = [];
26
+
27
+ // Ensure Windows paths are first
28
+ options.binaryPaths = [...windowsBinaries, ...options.binaryPaths];
29
+
30
+ return originalStart.call(this, voicePath, options);
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,209 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const PYTHON_VERSION_MIN = [3, 9];
7
+ const VENV_DIR = path.join(os.homedir(), '.gmgui', 'pocket-venv');
8
+ const isWin = process.platform === 'win32';
9
+ const EXECUTABLE_NAME = isWin ? 'pocket-tts.exe' : 'pocket-tts';
10
+
11
+ const CONFIG = {
12
+ PIP_TIMEOUT: 120000,
13
+ VENV_CREATION_TIMEOUT: 30000,
14
+ MAX_RETRIES: 3,
15
+ RETRY_DELAY_MS: 1000,
16
+ RETRY_BACKOFF_MULTIPLIER: 2,
17
+ };
18
+
19
+ function getPocketTtsPath() {
20
+ if (isWin) {
21
+ return path.join(VENV_DIR, 'Scripts', EXECUTABLE_NAME);
22
+ }
23
+ return path.join(VENV_DIR, 'bin', EXECUTABLE_NAME);
24
+ }
25
+
26
+ function detectPython() {
27
+ try {
28
+ const versionOutput = execSync('python --version', { encoding: 'utf-8', timeout: 10000 }).trim();
29
+ const match = versionOutput.match(/(\d+)\.(\d+)/);
30
+ if (!match) return { found: false, version: null, error: 'Could not parse version' };
31
+
32
+ const major = parseInt(match[1], 10);
33
+ const minor = parseInt(match[2], 10);
34
+ const versionOk = major > PYTHON_VERSION_MIN[0] || (major === PYTHON_VERSION_MIN[0] && minor >= PYTHON_VERSION_MIN[1]);
35
+
36
+ if (!versionOk) {
37
+ return { found: true, version: `${major}.${minor}`, error: `Python ${major}.${minor} found but ${PYTHON_VERSION_MIN[0]}.${PYTHON_VERSION_MIN[1]}+ required` };
38
+ }
39
+
40
+ return { found: true, version: `${major}.${minor}`, error: null };
41
+ } catch (e) {
42
+ return { found: false, version: null, error: 'Python not found in PATH' };
43
+ }
44
+ }
45
+
46
+ function isSetup() {
47
+ const exePath = getPocketTtsPath();
48
+ return fs.existsSync(exePath);
49
+ }
50
+
51
+ function cleanupPartialInstall() {
52
+ try {
53
+ if (fs.existsSync(VENV_DIR)) {
54
+ fs.rmSync(VENV_DIR, { recursive: true, force: true });
55
+ return true;
56
+ }
57
+ } catch (e) {
58
+ console.error(`Failed to cleanup partial install: ${e.message}`);
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function verifyInstallation() {
64
+ const exePath = getPocketTtsPath();
65
+ if (!fs.existsSync(exePath)) {
66
+ return { valid: false, error: `Binary not found at ${exePath}` };
67
+ }
68
+
69
+ try {
70
+ const versionOutput = execSync(`"${exePath}" --version`, { encoding: 'utf-8', timeout: 10000, stdio: 'pipe' });
71
+ return { valid: true, version: versionOutput.trim() };
72
+ } catch (e) {
73
+ return { valid: false, error: `Binary exists but failed verification: ${e.message}` };
74
+ }
75
+ }
76
+
77
+ async function executeWithRetry(fn, stepName, maxRetries = CONFIG.MAX_RETRIES) {
78
+ let lastError = null;
79
+ let delayMs = CONFIG.RETRY_DELAY_MS;
80
+
81
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
82
+ try {
83
+ return await fn(attempt);
84
+ } catch (e) {
85
+ lastError = e;
86
+ if (attempt < maxRetries) {
87
+ console.log(`Attempt ${attempt}/${maxRetries} failed for ${stepName}, retrying in ${delayMs}ms`);
88
+ await new Promise(r => setTimeout(r, delayMs));
89
+ delayMs *= CONFIG.RETRY_BACKOFF_MULTIPLIER;
90
+ }
91
+ }
92
+ }
93
+
94
+ const msg = `${stepName} failed after ${maxRetries} attempts: ${lastError.message || lastError}`;
95
+ throw new Error(msg);
96
+ }
97
+
98
+ async function install(onProgress) {
99
+ const pythonDetect = detectPython();
100
+
101
+ if (!pythonDetect.found) {
102
+ const msg = pythonDetect.error || 'Python not found';
103
+ if (onProgress) onProgress({ step: 'detecting-python', status: 'error', message: msg });
104
+ return { success: false, error: msg };
105
+ }
106
+
107
+ if (pythonDetect.error) {
108
+ if (onProgress) onProgress({ step: 'detecting-python', status: 'error', message: pythonDetect.error });
109
+ return { success: false, error: pythonDetect.error };
110
+ }
111
+
112
+ if (onProgress) onProgress({ step: 'detecting-python', status: 'success', message: `Found Python ${pythonDetect.version}` });
113
+
114
+ if (isSetup()) {
115
+ const verify = verifyInstallation();
116
+ if (verify.valid) {
117
+ if (onProgress) onProgress({ step: 'verifying', status: 'success', message: 'pocket-tts already installed' });
118
+ return { success: true };
119
+ }
120
+ }
121
+
122
+ if (onProgress) onProgress({ step: 'creating-venv', status: 'in-progress', message: `Creating virtual environment at ${VENV_DIR}` });
123
+
124
+ try {
125
+ await executeWithRetry(async (attempt) => {
126
+ return execSync(`python -m venv "${VENV_DIR}"`, {
127
+ encoding: 'utf-8',
128
+ stdio: 'pipe',
129
+ timeout: CONFIG.VENV_CREATION_TIMEOUT,
130
+ });
131
+ }, 'venv creation', 2);
132
+
133
+ if (onProgress) onProgress({ step: 'creating-venv', status: 'success', message: 'Virtual environment created' });
134
+ } catch (e) {
135
+ const msg = `Failed to create venv: ${e.message || e}`;
136
+ if (onProgress) onProgress({ step: 'creating-venv', status: 'error', message: msg });
137
+ cleanupPartialInstall();
138
+ return { success: false, error: msg };
139
+ }
140
+
141
+ if (onProgress) onProgress({ step: 'installing', status: 'in-progress', message: 'Installing pocket-tts via pip (this may take 2-5 minutes on slow connections)' });
142
+
143
+ try {
144
+ await executeWithRetry(async (attempt) => {
145
+ if (attempt > 1 && onProgress) {
146
+ onProgress({ step: 'installing', status: 'in-progress', message: `Installing pocket-tts (attempt ${attempt}/${CONFIG.MAX_RETRIES})` });
147
+ }
148
+
149
+ const pipCmd = isWin
150
+ ? `"${path.join(VENV_DIR, 'Scripts', 'pip')}" install --no-cache-dir pocket-tts`
151
+ : `"${path.join(VENV_DIR, 'bin', 'pip')}" install --no-cache-dir pocket-tts`;
152
+
153
+ return execSync(pipCmd, {
154
+ encoding: 'utf-8',
155
+ stdio: 'pipe',
156
+ timeout: CONFIG.PIP_TIMEOUT,
157
+ env: { ...process.env, PIP_DEFAULT_TIMEOUT: '120' },
158
+ });
159
+ }, 'pip install', CONFIG.MAX_RETRIES);
160
+
161
+ if (onProgress) onProgress({ step: 'installing', status: 'success', message: 'pocket-tts installed successfully' });
162
+ } catch (e) {
163
+ const msg = `Failed to install pocket-tts: ${e.message || e}`;
164
+ if (onProgress) onProgress({ step: 'installing', status: 'error', message: msg });
165
+ cleanupPartialInstall();
166
+ return { success: false, error: msg };
167
+ }
168
+
169
+ if (onProgress) onProgress({ step: 'verifying', status: 'in-progress', message: 'Verifying installation' });
170
+
171
+ const verify = verifyInstallation();
172
+ if (!verify.valid) {
173
+ const msg = verify.error || 'Installation verification failed';
174
+ if (onProgress) onProgress({ step: 'verifying', status: 'error', message: msg });
175
+ cleanupPartialInstall();
176
+ return { success: false, error: msg };
177
+ }
178
+
179
+ const exePath = getPocketTtsPath();
180
+ const binDir = path.join(VENV_DIR, 'bin');
181
+ const binExePath = path.join(binDir, 'pocket-tts');
182
+
183
+ if (isWin) {
184
+ try {
185
+ fs.mkdirSync(binDir, { recursive: true });
186
+ } catch (e) {}
187
+
188
+ const exeWithExt = path.join(binDir, 'pocket-tts.exe');
189
+ if (fs.existsSync(exePath) && !fs.existsSync(exeWithExt)) {
190
+ try {
191
+ fs.copyFileSync(exePath, exeWithExt);
192
+ } catch (e) {}
193
+ }
194
+
195
+ const batchFile = path.join(binDir, 'pocket-tts.bat');
196
+ if (!fs.existsSync(batchFile) && fs.existsSync(exeWithExt)) {
197
+ try {
198
+ const batchContent = `@echo off\nsetlocal enabledelayedexpansion\nset PYTHONUNBUFFERED=1\nset HF_HUB_DISABLE_SYMLINKS_WARNING=1\n"${exeWithExt}" %*\n`;
199
+ fs.writeFileSync(batchFile, batchContent, 'utf-8');
200
+ } catch (e) {}
201
+ }
202
+ }
203
+
204
+ if (onProgress) onProgress({ step: 'verifying', status: 'success', message: `pocket-tts ready (${verify.version})` });
205
+
206
+ return { success: true };
207
+ }
208
+
209
+ export { detectPython, isSetup, install, getPocketTtsPath, VENV_DIR, CONFIG, cleanupPartialInstall, verifyInstallation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.207",
3
+ "version": "1.0.209",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/readme.md CHANGED
@@ -37,6 +37,28 @@ Open `http://localhost:3000` in your browser.
37
37
  - `static/` - Browser client with streaming renderer, WebSocket manager, and HTML templates
38
38
  - `bin/gmgui.cjs` - CLI entry point for `npx agentgui`
39
39
 
40
+ ## Text-to-Speech on Windows
41
+
42
+ On Windows, AgentGUI automatically sets up pocket-tts (text-to-speech) on your first TTS request. No manual setup required.
43
+
44
+ ### What Happens
45
+ 1. Server detects Python 3.9+ installation
46
+ 2. Creates virtual environment at `~/.gmgui/pocket-venv`
47
+ 3. Installs pocket-tts via pip
48
+ 4. All subsequent TTS requests use cached installation
49
+
50
+ ### Requirements
51
+ - Python 3.9+ (check with `python --version`)
52
+ - ~200 MB free disk space
53
+ - Internet connection for first setup
54
+
55
+ ### Troubleshooting
56
+ - **Python not found**: Download from https://www.python.org and ensure "Add Python to PATH" is checked
57
+ - **Setup fails**: Check that you have write access to your home directory (~/.gmgui/)
58
+ - **Manual cleanup**: Delete `%USERPROFILE%\.gmgui\pocket-venv` and try again
59
+
60
+ For manual setup or detailed troubleshooting, see the setup instructions in the code or check `/api/speech-status` endpoint for error details.
61
+
40
62
  ## Configuration
41
63
 
42
64
  | Variable | Default | Description |
package/server.js CHANGED
@@ -11,12 +11,46 @@ import { createRequire } from 'module';
11
11
  import { OAuth2Client } from 'google-auth-library';
12
12
  import { queries } from './database.js';
13
13
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
14
+ import { isSetup, install, detectPython } from './lib/windows-pocket-tts-setup.js';
14
15
  let speechModule = null;
15
16
  async function getSpeech() {
16
17
  if (!speechModule) speechModule = await import('./lib/speech.js');
17
18
  return speechModule;
18
19
  }
19
20
 
21
+ const pocketTtsSetupState = { attempted: false, ready: false, error: null, inProgress: false };
22
+
23
+ async function ensurePocketTtsSetup(onProgress) {
24
+ if (pocketTtsSetupState.attempted) {
25
+ return pocketTtsSetupState.ready;
26
+ }
27
+
28
+ if (pocketTtsSetupState.inProgress) {
29
+ let waited = 0;
30
+ const MAX_WAIT = 600000;
31
+ while (pocketTtsSetupState.inProgress && waited < MAX_WAIT) {
32
+ await new Promise(r => setTimeout(r, 100));
33
+ waited += 100;
34
+ }
35
+ return pocketTtsSetupState.ready;
36
+ }
37
+
38
+ pocketTtsSetupState.inProgress = true;
39
+
40
+ if (onProgress) onProgress({ step: 'detecting-python', status: 'in-progress', message: 'Detecting Python installation' });
41
+
42
+ const result = await install((msg) => {
43
+ if (onProgress) onProgress(msg);
44
+ });
45
+
46
+ pocketTtsSetupState.attempted = true;
47
+ pocketTtsSetupState.ready = result.success;
48
+ pocketTtsSetupState.error = result.error || null;
49
+ pocketTtsSetupState.inProgress = false;
50
+
51
+ return pocketTtsSetupState.ready;
52
+ }
53
+
20
54
  function eagerTTS(text, conversationId, sessionId) {
21
55
  getSpeech().then(speech => {
22
56
  const status = speech.getStatus();
@@ -1263,6 +1297,25 @@ const server = http.createServer(async (req, res) => {
1263
1297
  sendJSON(req, res, 400, { error: 'No text provided' });
1264
1298
  return;
1265
1299
  }
1300
+
1301
+ if (!pocketTtsSetupState.attempted && process.platform === 'win32') {
1302
+ const setupOk = await ensurePocketTtsSetup((msg) => {
1303
+ broadcastSync({ type: 'tts_setup_progress', ...msg });
1304
+ });
1305
+ if (!setupOk) {
1306
+ sendJSON(req, res, 503, { error: pocketTtsSetupState.error || 'pocket-tts setup failed', retryable: false });
1307
+ return;
1308
+ }
1309
+
1310
+ // After successful setup, start the TTS sidecar if not already running
1311
+ const speech = await getSpeech();
1312
+ if (speech.preloadTTS) {
1313
+ speech.preloadTTS();
1314
+ // Wait a bit for it to start
1315
+ await new Promise(r => setTimeout(r, 2000));
1316
+ }
1317
+ }
1318
+
1266
1319
  const speech = await getSpeech();
1267
1320
  const status = speech.getStatus();
1268
1321
  if (status.ttsError) {
@@ -1322,9 +1375,29 @@ const server = http.createServer(async (req, res) => {
1322
1375
  if (pathOnly === '/api/speech-status' && req.method === 'GET') {
1323
1376
  try {
1324
1377
  const { getStatus } = await getSpeech();
1325
- sendJSON(req, res, 200, getStatus());
1378
+ const baseStatus = getStatus();
1379
+ const pythonDetect = detectPython();
1380
+ const statusWithSetup = {
1381
+ ...baseStatus,
1382
+ pythonDetected: pythonDetect.found,
1383
+ pythonVersion: pythonDetect.version,
1384
+ pocketTtsSetup: {
1385
+ ready: pocketTtsSetupState.ready,
1386
+ attempted: pocketTtsSetupState.attempted,
1387
+ error: pocketTtsSetupState.error,
1388
+ },
1389
+ setupMessage: pocketTtsSetupState.error || (pocketTtsSetupState.ready ? 'pocket-tts ready' : 'Will setup on first TTS request'),
1390
+ };
1391
+ sendJSON(req, res, 200, statusWithSetup);
1326
1392
  } catch (err) {
1327
- sendJSON(req, res, 200, { sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false });
1393
+ const pythonDetect = detectPython();
1394
+ sendJSON(req, res, 200, {
1395
+ sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
1396
+ pythonDetected: pythonDetect.found,
1397
+ pythonVersion: pythonDetect.version,
1398
+ pocketTtsSetup: { ready: false, attempted: false, error: null },
1399
+ setupMessage: 'Will setup on first TTS request',
1400
+ });
1328
1401
  }
1329
1402
  return;
1330
1403
  }