agentgui 1.0.753 → 1.0.755
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 +10 -1
- package/lib/speech-manager.js +203 -0
- package/package.json +1 -1
- package/server.js +3 -226
package/CLAUDE.md
CHANGED
|
@@ -24,6 +24,7 @@ lib/acp-runner.js ACP JSON-RPC session lifecycle (init, session/new, prompt
|
|
|
24
24
|
lib/acp-protocol.js ACP session/update message normalization (shared by all ACP agents)
|
|
25
25
|
lib/acp-sdk-manager.js ACP tool lifecycle - on-demand start opencode/kilo/codex, health checks, idle timeout
|
|
26
26
|
lib/acp-server-machine.js XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
|
|
27
|
+
lib/agent-discovery.js Agent binary detection (findCommand), ACP server query, discoverAgents, CLI wrapper logic
|
|
27
28
|
lib/agent-registry-configs.js Agent registration configs (Claude Code, OpenCode, Gemini, 10+ ACP agents)
|
|
28
29
|
lib/agent-descriptors.js Data-driven ACP agent descriptor builder
|
|
29
30
|
lib/checkpoint-manager.js Session recovery - load checkpoints, inject into resume flow, idempotency
|
|
@@ -32,15 +33,19 @@ lib/db-queries.js All 88 query functions (createQueries factory, extracted
|
|
|
32
33
|
lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
|
|
33
34
|
lib/gm-agent-configs.js GM agent configuration and spawning
|
|
34
35
|
lib/jsonl-watcher.js Watches ~/.claude/projects for JSONL file changes
|
|
36
|
+
lib/oauth-common.js Shared OAuth helpers (buildBaseUrl, isRemoteRequest, encodeOAuthState, result/relay pages)
|
|
37
|
+
lib/oauth-gemini.js Gemini OAuth flow (credential discovery, token exchange, callback handling)
|
|
38
|
+
lib/oauth-codex.js Codex CLI OAuth flow (PKCE S256, token exchange, callback handling)
|
|
35
39
|
lib/plugin-interface.js Plugin interface contract definition
|
|
36
40
|
lib/plugin-loader.js Plugin discovery and loading (EventEmitter-based)
|
|
37
41
|
lib/pm2-manager.js PM2 process management wrapper
|
|
42
|
+
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
43
|
+
lib/speech-manager.js TTS orchestration (eager TTS, voice cache, model download, broadcastModelProgress)
|
|
38
44
|
lib/tool-install-machine.js XState v5 machine per tool: unchecked/checking/idle/installing/installed/updating/needs_update/failed states
|
|
39
45
|
lib/tool-manager.js Tool facade - re-exports from tool-version, tool-spawner, tool-provisioner
|
|
40
46
|
lib/tool-version.js Version detection for CLI tools and plugins (data-driven framework paths)
|
|
41
47
|
lib/tool-spawner.js npm/bun install/update spawn with timeout and heartbeat
|
|
42
48
|
lib/tool-provisioner.js Auto-provisioning and periodic update checking
|
|
43
|
-
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
44
49
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
45
50
|
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
46
51
|
lib/ws-handlers-conv.js Conversation CRUD, chunks, cancel, steer, inject RPC handlers
|
|
@@ -130,6 +135,9 @@ XState v5 machines are authoritative for their respective state domains. Ad-hoc
|
|
|
130
135
|
- `STARTUP_CWD` - Working directory passed to agents
|
|
131
136
|
- `HOT_RELOAD` - Set to "false" to disable watch mode
|
|
132
137
|
- `CODEX_HOME` - Override Codex CLI home directory (default: `~/.codex`)
|
|
138
|
+
- `RATE_LIMIT_MAX` - Max HTTP requests per IP per minute (default: 300)
|
|
139
|
+
- `PASSWORD` - Basic auth password for all HTTP routes (optional)
|
|
140
|
+
- `AGENTGUI_BASE_URL` - Override base URL for OAuth callbacks (e.g., `https://myserver.com`)
|
|
133
141
|
|
|
134
142
|
## ACP Tool Lifecycle
|
|
135
143
|
|
|
@@ -159,6 +167,7 @@ All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
|
159
167
|
- `GET /api/sessions/:id/execution` - Get execution events (query: limit, offset, filterType)
|
|
160
168
|
- `GET /api/agents` - List discovered agents
|
|
161
169
|
- `GET /api/acp/status` - ACP tool lifecycle status (ports, health, PIDs, restart counts)
|
|
170
|
+
- `GET /api/health` - Server health check (version, uptime, agents, wsClients, memory, acp status)
|
|
162
171
|
- `GET /api/home` - Get home directory
|
|
163
172
|
- `POST /api/stt` - Speech-to-text (raw audio body)
|
|
164
173
|
- `POST /api/tts` - Text-to-speech (body: text)
|
|
@@ -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
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,
|