agentgui 1.0.753 → 1.0.754

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.
@@ -0,0 +1,203 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { createRequire } from 'module';
5
+
6
+ let speechModule = null;
7
+ let _broadcastSync = null;
8
+ let _syncClients = null;
9
+ let _queries = null;
10
+
11
+ export function initSpeechManager({ broadcastSync, syncClients, queries }) {
12
+ _broadcastSync = broadcastSync;
13
+ _syncClients = syncClients;
14
+ _queries = queries;
15
+ }
16
+
17
+ export async function ensurePocketTtsSetup(onProgress) {
18
+ const r = createRequire(import.meta.url);
19
+ const serverTTS = r('webtalk/server-tts');
20
+ return serverTTS.ensureInstalled(onProgress);
21
+ }
22
+
23
+ export async function getSpeech() {
24
+ if (!speechModule) speechModule = await import('./speech.js');
25
+ return speechModule;
26
+ }
27
+
28
+ const ttsTextAccumulators = new Map();
29
+
30
+ export const voiceCacheManager = {
31
+ generating: new Map(),
32
+ maxCacheSize: 10 * 1024 * 1024,
33
+ async getOrGenerateCache(conversationId, text) {
34
+ const cacheKey = `${conversationId}:${text}`;
35
+ if (this.generating.has(cacheKey)) {
36
+ return new Promise((resolve) => {
37
+ const checkInterval = setInterval(() => {
38
+ const cached = _queries.getVoiceCache(conversationId, text);
39
+ if (cached) { clearInterval(checkInterval); resolve(cached); }
40
+ }, 50);
41
+ });
42
+ }
43
+ const cached = _queries.getVoiceCache(conversationId, text);
44
+ if (cached) return cached;
45
+ this.generating.set(cacheKey, true);
46
+ try {
47
+ const speech = await getSpeech();
48
+ const audioBlob = await speech.synthesize(text, 'default');
49
+ const saved = _queries.saveVoiceCache(conversationId, text, audioBlob);
50
+ const totalSize = _queries.getVoiceCacheSize(conversationId);
51
+ if (totalSize > this.maxCacheSize) {
52
+ const needed = totalSize - this.maxCacheSize;
53
+ _queries.deleteOldestVoiceCache(conversationId, needed);
54
+ }
55
+ return saved;
56
+ } finally {
57
+ this.generating.delete(cacheKey);
58
+ }
59
+ }
60
+ };
61
+
62
+ export const modelDownloadState = {
63
+ downloading: false,
64
+ progress: null,
65
+ error: null,
66
+ complete: false,
67
+ startTime: null,
68
+ downloadMetrics: new Map()
69
+ };
70
+
71
+ export function broadcastModelProgress(progress) {
72
+ modelDownloadState.progress = progress;
73
+ const broadcastData = {
74
+ type: 'model_download_progress',
75
+ modelId: progress.type || 'unknown',
76
+ bytesDownloaded: progress.bytesDownloaded || 0,
77
+ bytesRemaining: progress.bytesRemaining || 0,
78
+ totalBytes: progress.totalBytes || 0,
79
+ downloadSpeed: progress.downloadSpeed || 0,
80
+ eta: progress.eta || 0,
81
+ retryCount: progress.retryCount || 0,
82
+ currentGateway: progress.currentGateway || '',
83
+ status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
84
+ percentComplete: progress.percentComplete || 0,
85
+ completedFiles: progress.completedFiles || 0,
86
+ totalFiles: progress.totalFiles || 0,
87
+ timestamp: Date.now(),
88
+ ...progress
89
+ };
90
+ _broadcastSync(broadcastData);
91
+ }
92
+
93
+ async function validateAndCleanupModels(modelsDir) {
94
+ try {
95
+ const manifestPath = path.join(modelsDir, '.manifests.json');
96
+ if (fs.existsSync(manifestPath)) {
97
+ try {
98
+ const content = fs.readFileSync(manifestPath, 'utf8');
99
+ JSON.parse(content);
100
+ } catch (e) {
101
+ console.error('[MODELS] Manifest corrupted, removing:', e.message);
102
+ fs.unlinkSync(manifestPath);
103
+ }
104
+ }
105
+ const files = fs.readdirSync(modelsDir);
106
+ for (const file of files) {
107
+ if (file.endsWith('.tmp')) {
108
+ try { fs.unlinkSync(path.join(modelsDir, file)); console.log('[MODELS] Cleaned up temp file:', file); }
109
+ catch (e) { console.warn('[MODELS] Failed to clean:', file); }
110
+ }
111
+ }
112
+ } catch (e) {
113
+ console.warn('[MODELS] Cleanup check failed:', e.message);
114
+ }
115
+ }
116
+
117
+ export async function ensureModelsDownloaded() {
118
+ if (modelDownloadState.downloading) {
119
+ while (modelDownloadState.downloading) { await new Promise(r => setTimeout(r, 100)); }
120
+ return modelDownloadState.complete;
121
+ }
122
+ modelDownloadState.downloading = true;
123
+ modelDownloadState.error = null;
124
+ try {
125
+ const r = createRequire(import.meta.url);
126
+ const { createConfig } = r('webtalk/config');
127
+ const { ensureModel } = r('webtalk/whisper-models');
128
+ const { ensureTTSModels } = r('webtalk/tts-models');
129
+ const gmguiModels = path.join(os.homedir(), '.gmgui', 'models');
130
+ const modelsBase = process.env.PORTABLE_EXE_DIR
131
+ ? (fs.existsSync(path.join(process.env.PORTABLE_EXE_DIR, 'models', 'onnx-community')) ? path.join(process.env.PORTABLE_EXE_DIR, 'models') : gmguiModels)
132
+ : gmguiModels;
133
+ await validateAndCleanupModels(modelsBase);
134
+ const config = createConfig({ modelsDir: modelsBase, ttsModelsDir: path.join(modelsBase, 'tts') });
135
+ const onProgress = (progress) => { broadcastModelProgress({ ...progress, started: true, done: false, downloading: true }); };
136
+ broadcastModelProgress({ started: true, done: false, downloading: true, type: 'whisper', status: 'starting' });
137
+ await ensureModel('onnx-community/whisper-base', config, onProgress);
138
+ broadcastModelProgress({ started: true, done: false, downloading: true, type: 'tts', status: 'starting' });
139
+ await ensureTTSModels(config, onProgress);
140
+ modelDownloadState.complete = true;
141
+ broadcastModelProgress({ started: true, done: true, complete: true, downloading: false });
142
+ return true;
143
+ } catch (err) {
144
+ console.error('[MODELS] Download error:', err.message);
145
+ modelDownloadState.error = err.message;
146
+ broadcastModelProgress({ done: true, error: err.message });
147
+ return false;
148
+ } finally {
149
+ modelDownloadState.downloading = false;
150
+ }
151
+ }
152
+
153
+ export function eagerTTS(text, conversationId, sessionId) {
154
+ const key = `${conversationId}:${sessionId}`;
155
+ let acc = ttsTextAccumulators.get(key);
156
+ if (!acc) { acc = { text: '', timer: null }; ttsTextAccumulators.set(key, acc); }
157
+ acc.text += text;
158
+ if (acc.timer) clearTimeout(acc.timer);
159
+ acc.timer = setTimeout(() => flushTTSaccumulator(key, conversationId, sessionId), 600);
160
+ }
161
+
162
+ function flushTTSaccumulator(key, conversationId, sessionId) {
163
+ const acc = ttsTextAccumulators.get(key);
164
+ if (!acc || !acc.text) return;
165
+ const text = acc.text.trim();
166
+ acc.text = '';
167
+ ttsTextAccumulators.delete(key);
168
+ getSpeech().then(speech => {
169
+ const status = speech.getStatus();
170
+ if (!status.ttsReady || status.ttsError) return;
171
+ const voices = new Set();
172
+ for (const ws of _syncClients) {
173
+ const vid = ws.ttsVoiceId || 'default';
174
+ const convKey = `conv-${conversationId}`;
175
+ if (ws.subscriptions && (ws.subscriptions.has(sessionId) || ws.subscriptions.has(convKey))) {
176
+ voices.add(vid);
177
+ }
178
+ }
179
+ if (voices.size === 0) return;
180
+ for (const vid of voices) {
181
+ const cacheKey = speech.ttsCacheKey(text, vid);
182
+ const cached = speech.ttsCacheGet(cacheKey);
183
+ if (cached) { pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid); continue; }
184
+ speech.synthesize(text, vid).then(wav => {
185
+ if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
186
+ pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
187
+ }).catch(() => {});
188
+ }
189
+ }).catch(() => {});
190
+ }
191
+
192
+ function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
193
+ const b64 = wav.toString('base64');
194
+ _broadcastSync({
195
+ type: 'tts_audio',
196
+ cacheKey,
197
+ audio: b64,
198
+ voiceId,
199
+ conversationId,
200
+ sessionId,
201
+ timestamp: Date.now()
202
+ });
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.753",
3
+ "version": "1.0.754",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -18,6 +18,7 @@ import { runClaudeWithStreaming } from './lib/claude-runner.js';
18
18
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
19
19
  import { findCommand, queryACPServerAgents, discoverAgents, discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
20
20
  import { getGeminiOAuthCreds, startGeminiOAuth, exchangeGeminiOAuthCode, handleGeminiOAuthCallback, getGeminiOAuthStatus, getGeminiOAuthState } from './lib/oauth-gemini.js';
21
+ import { initSpeechManager, getSpeech, ensurePocketTtsSetup, voiceCacheManager, modelDownloadState, broadcastModelProgress, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
21
22
  import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
22
23
  import { WSOptimizer } from './lib/ws-optimizer.js';
23
24
  import { WsRouter } from './lib/ws-protocol.js';
@@ -69,232 +70,6 @@ process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - un
69
70
  process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
70
71
  process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
71
72
 
72
- const ttsTextAccumulators = new Map();
73
- const voiceCacheManager = {
74
- generating: new Map(),
75
- maxCacheSize: 10 * 1024 * 1024,
76
- async getOrGenerateCache(conversationId, text) {
77
- const cacheKey = `${conversationId}:${text}`;
78
- if (this.generating.has(cacheKey)) {
79
- return new Promise((resolve) => {
80
- const checkInterval = setInterval(() => {
81
- const cached = queries.getVoiceCache(conversationId, text);
82
- if (cached) {
83
- clearInterval(checkInterval);
84
- resolve(cached);
85
- }
86
- }, 50);
87
- });
88
- }
89
- const cached = queries.getVoiceCache(conversationId, text);
90
- if (cached) return cached;
91
- this.generating.set(cacheKey, true);
92
- try {
93
- const speech = await getSpeech();
94
- const audioBlob = await speech.synthesize(text, 'default');
95
- const saved = queries.saveVoiceCache(conversationId, text, audioBlob);
96
- const totalSize = queries.getVoiceCacheSize(conversationId);
97
- if (totalSize > this.maxCacheSize) {
98
- const needed = totalSize - this.maxCacheSize;
99
- queries.deleteOldestVoiceCache(conversationId, needed);
100
- }
101
- return saved;
102
- } finally {
103
- this.generating.delete(cacheKey);
104
- }
105
- }
106
- };
107
-
108
- let speechModule = null;
109
- async function getSpeech() {
110
- if (!speechModule) speechModule = await import('./lib/speech.js');
111
- return speechModule;
112
- }
113
-
114
- async function ensurePocketTtsSetup(onProgress) {
115
- const { createRequire: cr } = await import('module');
116
- const r = cr(import.meta.url);
117
- const serverTTS = r('webtalk/server-tts');
118
- return serverTTS.ensureInstalled(onProgress);
119
- }
120
-
121
- // Model download manager
122
- const modelDownloadState = {
123
- downloading: false,
124
- progress: null,
125
- error: null,
126
- complete: false,
127
- startTime: null,
128
- downloadMetrics: new Map()
129
- };
130
-
131
- function broadcastModelProgress(progress) {
132
- modelDownloadState.progress = progress;
133
- const broadcastData = {
134
- type: 'model_download_progress',
135
- modelId: progress.type || 'unknown',
136
- bytesDownloaded: progress.bytesDownloaded || 0,
137
- bytesRemaining: progress.bytesRemaining || 0,
138
- totalBytes: progress.totalBytes || 0,
139
- downloadSpeed: progress.downloadSpeed || 0,
140
- eta: progress.eta || 0,
141
- retryCount: progress.retryCount || 0,
142
- currentGateway: progress.currentGateway || '',
143
- status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
144
- percentComplete: progress.percentComplete || 0,
145
- completedFiles: progress.completedFiles || 0,
146
- totalFiles: progress.totalFiles || 0,
147
- timestamp: Date.now(),
148
- ...progress
149
- };
150
- broadcastSync(broadcastData);
151
- }
152
-
153
- async function validateAndCleanupModels(modelsDir) {
154
- try {
155
- const manifestPath = path.join(modelsDir, '.manifests.json');
156
- if (fs.existsSync(manifestPath)) {
157
- try {
158
- const content = fs.readFileSync(manifestPath, 'utf8');
159
- JSON.parse(content);
160
- } catch (e) {
161
- console.error('[MODELS] Manifest corrupted, removing:', e.message);
162
- fs.unlinkSync(manifestPath);
163
- }
164
- }
165
-
166
- const files = fs.readdirSync(modelsDir);
167
- for (const file of files) {
168
- if (file.endsWith('.tmp')) {
169
- try {
170
- fs.unlinkSync(path.join(modelsDir, file));
171
- console.log('[MODELS] Cleaned up temp file:', file);
172
- } catch (e) {
173
- console.warn('[MODELS] Failed to clean:', file);
174
- }
175
- }
176
- }
177
- } catch (e) {
178
- console.warn('[MODELS] Cleanup check failed:', e.message);
179
- }
180
- }
181
-
182
- async function ensureModelsDownloaded() {
183
- if (modelDownloadState.downloading) {
184
- while (modelDownloadState.downloading) {
185
- await new Promise(r => setTimeout(r, 100));
186
- }
187
- return modelDownloadState.complete;
188
- }
189
-
190
- modelDownloadState.downloading = true;
191
- modelDownloadState.error = null;
192
-
193
- try {
194
- const r = createRequire(import.meta.url);
195
- const { createConfig } = r('webtalk/config');
196
- const { ensureModel } = r('webtalk/whisper-models');
197
- const { ensureTTSModels } = r('webtalk/tts-models');
198
- const gmguiModels = path.join(os.homedir(), '.gmgui', 'models');
199
- const modelsBase = process.env.PORTABLE_EXE_DIR
200
- ? (fs.existsSync(path.join(process.env.PORTABLE_EXE_DIR, 'models', 'onnx-community')) ? path.join(process.env.PORTABLE_EXE_DIR, 'models') : gmguiModels)
201
- : gmguiModels;
202
-
203
- await validateAndCleanupModels(modelsBase);
204
-
205
- const config = createConfig({
206
- modelsDir: modelsBase,
207
- ttsModelsDir: path.join(modelsBase, 'tts'),
208
- });
209
-
210
- // Progress callback for broadcasting download progress
211
- const onProgress = (progress) => {
212
- broadcastModelProgress({
213
- ...progress,
214
- started: true,
215
- done: false,
216
- downloading: true
217
- });
218
- };
219
-
220
- broadcastModelProgress({ started: true, done: false, downloading: true, type: 'whisper', status: 'starting' });
221
- await ensureModel('onnx-community/whisper-base', config, onProgress);
222
-
223
- broadcastModelProgress({ started: true, done: false, downloading: true, type: 'tts', status: 'starting' });
224
- await ensureTTSModels(config, onProgress);
225
-
226
- modelDownloadState.complete = true;
227
- broadcastModelProgress({ started: true, done: true, complete: true, downloading: false });
228
- return true;
229
- } catch (err) {
230
- console.error('[MODELS] Download error:', err.message);
231
- modelDownloadState.error = err.message;
232
- broadcastModelProgress({ done: true, error: err.message });
233
- return false;
234
- } finally {
235
- modelDownloadState.downloading = false;
236
- }
237
- }
238
-
239
- function eagerTTS(text, conversationId, sessionId) {
240
- const key = `${conversationId}:${sessionId}`;
241
- let acc = ttsTextAccumulators.get(key);
242
- if (!acc) {
243
- acc = { text: '', timer: null };
244
- ttsTextAccumulators.set(key, acc);
245
- }
246
- acc.text += text;
247
- if (acc.timer) clearTimeout(acc.timer);
248
- acc.timer = setTimeout(() => flushTTSaccumulator(key, conversationId, sessionId), 600);
249
- }
250
-
251
- function flushTTSaccumulator(key, conversationId, sessionId) {
252
- const acc = ttsTextAccumulators.get(key);
253
- if (!acc || !acc.text) return;
254
- const text = acc.text.trim();
255
- acc.text = '';
256
- ttsTextAccumulators.delete(key);
257
-
258
- getSpeech().then(speech => {
259
- const status = speech.getStatus();
260
- if (!status.ttsReady || status.ttsError) return;
261
- const voices = new Set();
262
- for (const ws of syncClients) {
263
- const vid = ws.ttsVoiceId || 'default';
264
- const convKey = `conv-${conversationId}`;
265
- if (ws.subscriptions && (ws.subscriptions.has(sessionId) || ws.subscriptions.has(convKey))) {
266
- voices.add(vid);
267
- }
268
- }
269
- if (voices.size === 0) return;
270
- for (const vid of voices) {
271
- const cacheKey = speech.ttsCacheKey(text, vid);
272
- const cached = speech.ttsCacheGet(cacheKey);
273
- if (cached) {
274
- pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
275
- continue;
276
- }
277
- speech.synthesize(text, vid).then(wav => {
278
- if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
279
- pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
280
- }).catch(() => {});
281
- }
282
- }).catch(() => {});
283
- }
284
-
285
- function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
286
- const b64 = wav.toString('base64');
287
- broadcastSync({
288
- type: 'tts_audio',
289
- cacheKey,
290
- audio: b64,
291
- voiceId,
292
- conversationId,
293
- sessionId,
294
- timestamp: Date.now()
295
- });
296
- }
297
-
298
73
 
299
74
  function buildSystemPrompt(agentId, model, subAgent) {
300
75
  const parts = [];
@@ -3943,6 +3718,8 @@ function broadcastSync(event) {
3943
3718
  // WebSocket protocol router
3944
3719
  const wsRouter = new WsRouter();
3945
3720
 
3721
+ initSpeechManager({ broadcastSync, syncClients, queries });
3722
+
3946
3723
  registerConvHandlers(wsRouter, {
3947
3724
  queries, activeExecutions, rateLimitState,
3948
3725
  broadcastSync, processMessageWithStreaming, cleanupExecution,