agentgui 1.0.757 → 1.0.759
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/lib/claude-runner.js +1266 -189
- package/lib/routes-oauth.js +105 -0
- package/lib/routes-speech.js +173 -0
- package/package.json +1 -1
- package/server.js +8 -325
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { startGeminiOAuth, exchangeGeminiOAuthCode, getGeminiOAuthState } from './oauth-gemini.js';
|
|
2
|
+
import { startCodexOAuth, exchangeCodexOAuthCode, getCodexOAuthState } from './oauth-codex.js';
|
|
3
|
+
|
|
4
|
+
export function register(deps) {
|
|
5
|
+
const { sendJSON, parseBody, PORT, BASE_URL, rootDir } = deps;
|
|
6
|
+
const routes = {};
|
|
7
|
+
|
|
8
|
+
routes['POST /api/gemini-oauth/start'] = async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
11
|
+
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
|
|
14
|
+
sendJSON(req, res, 500, { error: e.message });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
routes['GET /api/gemini-oauth/status'] = async (req, res) => {
|
|
19
|
+
sendJSON(req, res, 200, getGeminiOAuthState());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
routes['POST /api/gemini-oauth/relay'] = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const body = await parseBody(req);
|
|
25
|
+
const { code, state: stateParam } = body;
|
|
26
|
+
if (!code || !stateParam) { sendJSON(req, res, 400, { error: 'Missing code or state' }); return; }
|
|
27
|
+
const email = await exchangeGeminiOAuthCode(code, stateParam);
|
|
28
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
routes['POST /api/gemini-oauth/complete'] = async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const body = await parseBody(req);
|
|
37
|
+
const pastedUrl = (body.url || '').trim();
|
|
38
|
+
if (!pastedUrl) { sendJSON(req, res, 400, { error: 'No URL provided' }); return; }
|
|
39
|
+
let parsed;
|
|
40
|
+
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
41
|
+
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' }); return;
|
|
42
|
+
}
|
|
43
|
+
const error = parsed.searchParams.get('error');
|
|
44
|
+
if (error) { sendJSON(req, res, 200, { error: parsed.searchParams.get('error_description') || error }); return; }
|
|
45
|
+
const code = parsed.searchParams.get('code');
|
|
46
|
+
const state = parsed.searchParams.get('state');
|
|
47
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
48
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
routes['POST /api/codex-oauth/start'] = async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
57
|
+
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
|
|
60
|
+
sendJSON(req, res, 500, { error: e.message });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
routes['GET /api/codex-oauth/status'] = async (req, res) => {
|
|
65
|
+
sendJSON(req, res, 200, getCodexOAuthState());
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
routes['POST /api/codex-oauth/relay'] = async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const body = await parseBody(req);
|
|
71
|
+
const { code, state: stateParam } = body;
|
|
72
|
+
if (!code || !stateParam) { sendJSON(req, res, 400, { error: 'Missing code or state' }); return; }
|
|
73
|
+
const email = await exchangeCodexOAuthCode(code, stateParam);
|
|
74
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
75
|
+
} catch (e) {
|
|
76
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
routes['POST /api/codex-oauth/complete'] = async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const body = await parseBody(req);
|
|
83
|
+
const pastedUrl = (body.url || '').trim();
|
|
84
|
+
if (!pastedUrl) { sendJSON(req, res, 400, { error: 'No URL provided' }); return; }
|
|
85
|
+
let parsed;
|
|
86
|
+
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
87
|
+
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' }); return;
|
|
88
|
+
}
|
|
89
|
+
const error = parsed.searchParams.get('error');
|
|
90
|
+
if (error) { sendJSON(req, res, 200, { error: parsed.searchParams.get('error_description') || error }); return; }
|
|
91
|
+
const code = parsed.searchParams.get('code');
|
|
92
|
+
const state = parsed.searchParams.get('state');
|
|
93
|
+
const email = await exchangeCodexOAuthCode(code, state);
|
|
94
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
95
|
+
} catch (e) {
|
|
96
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
routes['_match'] = (method, pathOnly) => {
|
|
101
|
+
return routes[`${method} ${pathOnly}`] || null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return routes;
|
|
105
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { getSpeech, ensurePocketTtsSetup, ensureModelsDownloaded, modelDownloadState } from './speech-manager.js';
|
|
2
|
+
|
|
3
|
+
export function register(deps) {
|
|
4
|
+
const { sendJSON, parseBody, broadcastSync, debugLog } = deps;
|
|
5
|
+
const routes = {};
|
|
6
|
+
|
|
7
|
+
routes['POST /api/stt'] = async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
11
|
+
const audioBuffer = Buffer.concat(chunks);
|
|
12
|
+
if (audioBuffer.length === 0) { sendJSON(req, res, 400, { error: 'No audio data' }); return; }
|
|
13
|
+
broadcastSync({ type: 'stt_progress', status: 'transcribing', percentComplete: 0 });
|
|
14
|
+
const { transcribe } = await getSpeech();
|
|
15
|
+
const text = await transcribe(audioBuffer);
|
|
16
|
+
const finalText = (text || '').trim();
|
|
17
|
+
broadcastSync({ type: 'stt_progress', status: 'completed', percentComplete: 100, transcript: finalText });
|
|
18
|
+
sendJSON(req, res, 200, { text: finalText });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
debugLog('[STT] Error: ' + err.message);
|
|
21
|
+
let errorMsg = err.message || 'STT failed';
|
|
22
|
+
if (errorMsg.includes('VERS_1.21') || errorMsg.includes('onnxruntime')) {
|
|
23
|
+
errorMsg = 'STT model load failed: onnxruntime version mismatch. Try: npm install or npm ci';
|
|
24
|
+
} else if (errorMsg.includes('not valid JSON') || errorMsg.includes('Unexpected token') || errorMsg.includes('corrupted files cleared')) {
|
|
25
|
+
try {
|
|
26
|
+
const speech = await getSpeech();
|
|
27
|
+
const cleared = speech.clearCorruptedSTTCache();
|
|
28
|
+
speech.resetSTTError();
|
|
29
|
+
await ensureModelsDownloaded();
|
|
30
|
+
errorMsg = 'STT cache was corrupted, re-downloaded models. Please try again.';
|
|
31
|
+
} catch (e) {
|
|
32
|
+
errorMsg = 'STT model load failed: corrupted cache. Recovery attempted, try again.';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
broadcastSync({ type: 'stt_progress', status: 'failed', percentComplete: 0, error: errorMsg });
|
|
36
|
+
if (!res.headersSent) sendJSON(req, res, 500, { error: errorMsg });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
routes['GET /api/voices'] = async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const { getVoices } = await getSpeech();
|
|
43
|
+
sendJSON(req, res, 200, { ok: true, voices: getVoices() });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
sendJSON(req, res, 200, { ok: true, voices: [] });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
routes['POST /api/tts'] = async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const body = await parseBody(req);
|
|
52
|
+
const text = body.text || '';
|
|
53
|
+
const voiceId = body.voiceId || null;
|
|
54
|
+
if (!text) { sendJSON(req, res, 400, { error: 'No text provided' }); return; }
|
|
55
|
+
if (process.platform === 'win32') {
|
|
56
|
+
const setupOk = await ensurePocketTtsSetup((msg) => {
|
|
57
|
+
broadcastSync({ type: 'tts_setup_progress', ...msg });
|
|
58
|
+
});
|
|
59
|
+
if (!setupOk) { sendJSON(req, res, 503, { error: 'pocket-tts setup failed', retryable: false }); return; }
|
|
60
|
+
const speech = await getSpeech();
|
|
61
|
+
if (speech.preloadTTS) { speech.preloadTTS(); await new Promise(r => setTimeout(r, 2000)); }
|
|
62
|
+
}
|
|
63
|
+
const speech = await getSpeech();
|
|
64
|
+
const wavBuffer = await speech.synthesize(text, voiceId);
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': wavBuffer.length });
|
|
66
|
+
res.end(wavBuffer);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
debugLog('[TTS] Error: ' + err.message);
|
|
69
|
+
const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
|
|
70
|
+
const statusCode = isModelError ? 503 : 500;
|
|
71
|
+
if (!res.headersSent) sendJSON(req, res, statusCode, { error: err.message || 'TTS failed', retryable: !isModelError });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
routes['POST /api/tts-stream'] = async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const body = await parseBody(req);
|
|
78
|
+
const text = body.text || '';
|
|
79
|
+
const voiceId = body.voiceId || null;
|
|
80
|
+
if (!text) { sendJSON(req, res, 400, { error: 'No text provided' }); return; }
|
|
81
|
+
const speech = await getSpeech();
|
|
82
|
+
const gen = speech.synthesizeStream(text, voiceId);
|
|
83
|
+
const firstResult = await gen.next();
|
|
84
|
+
if (firstResult.done) { sendJSON(req, res, 500, { error: 'TTS stream returned no audio', retryable: true }); return; }
|
|
85
|
+
res.writeHead(200, {
|
|
86
|
+
'Content-Type': 'application/octet-stream',
|
|
87
|
+
'Transfer-Encoding': 'chunked',
|
|
88
|
+
'X-Content-Type': 'audio/wav-stream',
|
|
89
|
+
'Cache-Control': 'no-cache'
|
|
90
|
+
});
|
|
91
|
+
const writeChunk = (wavChunk) => {
|
|
92
|
+
const lenBuf = Buffer.alloc(4);
|
|
93
|
+
lenBuf.writeUInt32BE(wavChunk.length, 0);
|
|
94
|
+
res.write(lenBuf);
|
|
95
|
+
res.write(wavChunk);
|
|
96
|
+
};
|
|
97
|
+
writeChunk(firstResult.value);
|
|
98
|
+
for await (const wavChunk of gen) writeChunk(wavChunk);
|
|
99
|
+
res.end();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
debugLog('[TTS-STREAM] Error: ' + err.message);
|
|
102
|
+
const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
|
|
103
|
+
if (!res.headersSent) sendJSON(req, res, isModelError ? 503 : 500, { error: err.message || 'TTS stream failed', retryable: !isModelError });
|
|
104
|
+
else res.end();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
routes['GET /api/speech-status'] = async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { getStatus } = await getSpeech();
|
|
111
|
+
const baseStatus = getStatus();
|
|
112
|
+
sendJSON(req, res, 200, {
|
|
113
|
+
...baseStatus,
|
|
114
|
+
setupMessage: baseStatus.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
|
|
115
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
116
|
+
modelsComplete: modelDownloadState.complete,
|
|
117
|
+
modelsError: modelDownloadState.error,
|
|
118
|
+
modelsProgress: modelDownloadState.progress,
|
|
119
|
+
});
|
|
120
|
+
} catch (err) {
|
|
121
|
+
sendJSON(req, res, 200, {
|
|
122
|
+
sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
|
|
123
|
+
setupMessage: 'Will setup on first TTS request',
|
|
124
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
125
|
+
modelsComplete: modelDownloadState.complete,
|
|
126
|
+
modelsError: modelDownloadState.error,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
routes['POST /api/speech-status'] = async (req, res) => {
|
|
132
|
+
const body = await parseBody(req);
|
|
133
|
+
if (body.forceDownload) {
|
|
134
|
+
if (modelDownloadState.complete) { sendJSON(req, res, 200, { ok: true, modelsComplete: true, message: 'Models already ready' }); return; }
|
|
135
|
+
if (!modelDownloadState.downloading) {
|
|
136
|
+
modelDownloadState.error = null;
|
|
137
|
+
ensureModelsDownloaded().then(ok => {
|
|
138
|
+
broadcastSync({ type: 'model_download_progress', progress: { done: true, complete: ok, error: ok ? null : 'Download failed' } });
|
|
139
|
+
}).catch(err => {
|
|
140
|
+
broadcastSync({ type: 'model_download_progress', progress: { done: true, error: err.message } });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
sendJSON(req, res, 200, { ok: true, message: 'Starting model download' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
sendJSON(req, res, 400, { error: 'Unknown request' });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
routes['_match'] = (method, pathOnly) => {
|
|
150
|
+
const key = `${method} ${pathOnly}`;
|
|
151
|
+
if (routes[key]) return routes[key];
|
|
152
|
+
if (method === 'GET' && pathOnly.startsWith('/api/tts-cache/')) return routes['_tts-cache'];
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
routes['_tts-cache'] = async (req, res, pathOnly) => {
|
|
157
|
+
const cacheKey = decodeURIComponent(pathOnly.slice('/api/tts-cache/'.length));
|
|
158
|
+
try {
|
|
159
|
+
const speech = await getSpeech();
|
|
160
|
+
const cached = speech.ttsCacheGet(cacheKey);
|
|
161
|
+
if (cached) {
|
|
162
|
+
res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': cached.length, 'Cache-Control': 'public, max-age=3600' });
|
|
163
|
+
res.end(cached);
|
|
164
|
+
} else {
|
|
165
|
+
sendJSON(req, res, 404, { error: 'not cached' });
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return routes;
|
|
173
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -19,6 +19,8 @@ import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descripto
|
|
|
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
21
|
import { initSpeechManager, getSpeech, ensurePocketTtsSetup, voiceCacheManager, modelDownloadState, broadcastModelProgress, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
|
|
22
|
+
import { register as registerSpeechRoutes } from './lib/routes-speech.js';
|
|
23
|
+
import { register as registerOAuthRoutes } from './lib/routes-oauth.js';
|
|
22
24
|
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
23
25
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
24
26
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
@@ -1998,130 +2000,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1998
2000
|
return;
|
|
1999
2001
|
}
|
|
2000
2002
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
2004
|
-
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2005
|
-
} catch (e) {
|
|
2006
|
-
console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
|
|
2007
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
2008
|
-
}
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
|
|
2013
|
-
sendJSON(req, res, 200, getGeminiOAuthState());
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
if (pathOnly === '/api/gemini-oauth/relay' && req.method === 'POST') {
|
|
2018
|
-
try {
|
|
2019
|
-
const body = await parseBody(req);
|
|
2020
|
-
const { code, state: stateParam } = body;
|
|
2021
|
-
if (!code || !stateParam) {
|
|
2022
|
-
sendJSON(req, res, 400, { error: 'Missing code or state' });
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
const email = await exchangeGeminiOAuthCode(code, stateParam);
|
|
2026
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2027
|
-
} catch (e) {
|
|
2028
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2029
|
-
}
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
|
|
2034
|
-
try {
|
|
2035
|
-
const body = await parseBody(req);
|
|
2036
|
-
const pastedUrl = (body.url || '').trim();
|
|
2037
|
-
if (!pastedUrl) {
|
|
2038
|
-
sendJSON(req, res, 400, { error: 'No URL provided' });
|
|
2039
|
-
return;
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
let parsed;
|
|
2043
|
-
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
2044
|
-
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
|
|
2045
|
-
return;
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
const error = parsed.searchParams.get('error');
|
|
2049
|
-
if (error) {
|
|
2050
|
-
const desc = parsed.searchParams.get('error_description') || error;
|
|
2051
|
-
sendJSON(req, res, 200, { error: desc });
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
const code = parsed.searchParams.get('code');
|
|
2056
|
-
const state = parsed.searchParams.get('state');
|
|
2057
|
-
const email = await exchangeGeminiOAuthCode(code, state);
|
|
2058
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2059
|
-
} catch (e) {
|
|
2060
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2061
|
-
}
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
|
|
2066
|
-
try {
|
|
2067
|
-
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
2068
|
-
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2069
|
-
} catch (e) {
|
|
2070
|
-
console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
|
|
2071
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
2072
|
-
}
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
|
|
2077
|
-
sendJSON(req, res, 200, getCodexOAuthState());
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
if (pathOnly === '/api/codex-oauth/relay' && req.method === 'POST') {
|
|
2082
|
-
try {
|
|
2083
|
-
const body = await parseBody(req);
|
|
2084
|
-
const { code, state: stateParam } = body;
|
|
2085
|
-
if (!code || !stateParam) {
|
|
2086
|
-
sendJSON(req, res, 400, { error: 'Missing code or state' });
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
const email = await exchangeCodexOAuthCode(code, stateParam);
|
|
2090
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2091
|
-
} catch (e) {
|
|
2092
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2093
|
-
}
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (pathOnly === '/api/codex-oauth/complete' && req.method === 'POST') {
|
|
2098
|
-
try {
|
|
2099
|
-
const body = await parseBody(req);
|
|
2100
|
-
const pastedUrl = (body.url || '').trim();
|
|
2101
|
-
if (!pastedUrl) {
|
|
2102
|
-
sendJSON(req, res, 400, { error: 'No URL provided' });
|
|
2103
|
-
return;
|
|
2104
|
-
}
|
|
2105
|
-
let parsed;
|
|
2106
|
-
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
2107
|
-
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
const error = parsed.searchParams.get('error');
|
|
2111
|
-
if (error) {
|
|
2112
|
-
const desc = parsed.searchParams.get('error_description') || error;
|
|
2113
|
-
sendJSON(req, res, 200, { error: desc });
|
|
2114
|
-
return;
|
|
2115
|
-
}
|
|
2116
|
-
const code = parsed.searchParams.get('code');
|
|
2117
|
-
const state = parsed.searchParams.get('state');
|
|
2118
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
2119
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2120
|
-
} catch (e) {
|
|
2121
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2122
|
-
}
|
|
2123
|
-
return;
|
|
2124
|
-
}
|
|
2003
|
+
const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
|
|
2004
|
+
if (oauthHandler) { await oauthHandler(req, res); return; }
|
|
2125
2005
|
|
|
2126
2006
|
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
2127
2007
|
if (agentAuthMatch && req.method === 'POST') {
|
|
@@ -2317,207 +2197,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
2317
2197
|
return;
|
|
2318
2198
|
}
|
|
2319
2199
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const chunks = [];
|
|
2323
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
2324
|
-
const audioBuffer = Buffer.concat(chunks);
|
|
2325
|
-
if (audioBuffer.length === 0) {
|
|
2326
|
-
sendJSON(req, res, 400, { error: 'No audio data' });
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
broadcastSync({ type: 'stt_progress', status: 'transcribing', percentComplete: 0 });
|
|
2330
|
-
const { transcribe } = await getSpeech();
|
|
2331
|
-
const text = await transcribe(audioBuffer);
|
|
2332
|
-
const finalText = (text || '').trim();
|
|
2333
|
-
broadcastSync({ type: 'stt_progress', status: 'completed', percentComplete: 100, transcript: finalText });
|
|
2334
|
-
sendJSON(req, res, 200, { text: finalText });
|
|
2335
|
-
} catch (err) {
|
|
2336
|
-
debugLog('[STT] Error: ' + err.message);
|
|
2337
|
-
let errorMsg = err.message || 'STT failed';
|
|
2338
|
-
if (errorMsg.includes('VERS_1.21') || errorMsg.includes('onnxruntime')) {
|
|
2339
|
-
errorMsg = 'STT model load failed: onnxruntime version mismatch. Try: npm install or npm ci';
|
|
2340
|
-
} else if (errorMsg.includes('not valid JSON') || errorMsg.includes('Unexpected token') || errorMsg.includes('corrupted files cleared')) {
|
|
2341
|
-
try {
|
|
2342
|
-
const speech = await getSpeech();
|
|
2343
|
-
const cleared = speech.clearCorruptedSTTCache();
|
|
2344
|
-
speech.resetSTTError();
|
|
2345
|
-
console.log('[STT] Cleared', cleared, 'corrupted files and reset error state');
|
|
2346
|
-
await ensureModelsDownloaded();
|
|
2347
|
-
errorMsg = 'STT cache was corrupted, re-downloaded models. Please try again.';
|
|
2348
|
-
} catch (e) {
|
|
2349
|
-
console.warn('[STT] Recovery failed:', e.message);
|
|
2350
|
-
errorMsg = 'STT model load failed: corrupted cache. Recovery attempted, try again.';
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
broadcastSync({ type: 'stt_progress', status: 'failed', percentComplete: 0, error: errorMsg });
|
|
2354
|
-
if (!res.headersSent) sendJSON(req, res, 500, { error: errorMsg });
|
|
2355
|
-
}
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
if (pathOnly === '/api/voices' && req.method === 'GET') {
|
|
2360
|
-
try {
|
|
2361
|
-
const { getVoices } = await getSpeech();
|
|
2362
|
-
sendJSON(req, res, 200, { ok: true, voices: getVoices() });
|
|
2363
|
-
} catch (err) {
|
|
2364
|
-
sendJSON(req, res, 200, { ok: true, voices: [] });
|
|
2365
|
-
}
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
if (pathOnly === '/api/tts' && req.method === 'POST') {
|
|
2370
|
-
try {
|
|
2371
|
-
const body = await parseBody(req);
|
|
2372
|
-
const text = body.text || '';
|
|
2373
|
-
const voiceId = body.voiceId || null;
|
|
2374
|
-
if (!text) {
|
|
2375
|
-
sendJSON(req, res, 400, { error: 'No text provided' });
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
if (process.platform === 'win32') {
|
|
2380
|
-
const setupOk = await ensurePocketTtsSetup((msg) => {
|
|
2381
|
-
broadcastSync({ type: 'tts_setup_progress', ...msg });
|
|
2382
|
-
});
|
|
2383
|
-
if (!setupOk) {
|
|
2384
|
-
sendJSON(req, res, 503, { error: 'pocket-tts setup failed', retryable: false });
|
|
2385
|
-
return;
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
// After successful setup, start the TTS sidecar if not already running
|
|
2389
|
-
const speech = await getSpeech();
|
|
2390
|
-
if (speech.preloadTTS) {
|
|
2391
|
-
speech.preloadTTS();
|
|
2392
|
-
// Wait a bit for it to start
|
|
2393
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
const speech = await getSpeech();
|
|
2398
|
-
const wavBuffer = await speech.synthesize(text, voiceId);
|
|
2399
|
-
res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': wavBuffer.length });
|
|
2400
|
-
res.end(wavBuffer);
|
|
2401
|
-
} catch (err) {
|
|
2402
|
-
debugLog('[TTS] Error: ' + err.message);
|
|
2403
|
-
const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
|
|
2404
|
-
const statusCode = isModelError ? 503 : 500;
|
|
2405
|
-
if (!res.headersSent) sendJSON(req, res, statusCode, { error: err.message || 'TTS failed', retryable: !isModelError });
|
|
2406
|
-
}
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
if (pathOnly === '/api/tts-stream' && req.method === 'POST') {
|
|
2411
|
-
try {
|
|
2412
|
-
const body = await parseBody(req);
|
|
2413
|
-
const text = body.text || '';
|
|
2414
|
-
const voiceId = body.voiceId || null;
|
|
2415
|
-
if (!text) {
|
|
2416
|
-
sendJSON(req, res, 400, { error: 'No text provided' });
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
const speech = await getSpeech();
|
|
2420
|
-
const gen = speech.synthesizeStream(text, voiceId);
|
|
2421
|
-
const firstResult = await gen.next();
|
|
2422
|
-
if (firstResult.done) {
|
|
2423
|
-
sendJSON(req, res, 500, { error: 'TTS stream returned no audio', retryable: true });
|
|
2424
|
-
return;
|
|
2425
|
-
}
|
|
2426
|
-
res.writeHead(200, {
|
|
2427
|
-
'Content-Type': 'application/octet-stream',
|
|
2428
|
-
'Transfer-Encoding': 'chunked',
|
|
2429
|
-
'X-Content-Type': 'audio/wav-stream',
|
|
2430
|
-
'Cache-Control': 'no-cache'
|
|
2431
|
-
});
|
|
2432
|
-
const writeChunk = (wavChunk) => {
|
|
2433
|
-
const lenBuf = Buffer.alloc(4);
|
|
2434
|
-
lenBuf.writeUInt32BE(wavChunk.length, 0);
|
|
2435
|
-
res.write(lenBuf);
|
|
2436
|
-
res.write(wavChunk);
|
|
2437
|
-
};
|
|
2438
|
-
writeChunk(firstResult.value);
|
|
2439
|
-
for await (const wavChunk of gen) {
|
|
2440
|
-
writeChunk(wavChunk);
|
|
2441
|
-
}
|
|
2442
|
-
res.end();
|
|
2443
|
-
} catch (err) {
|
|
2444
|
-
debugLog('[TTS-STREAM] Error: ' + err.message);
|
|
2445
|
-
const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
|
|
2446
|
-
const statusCode = isModelError ? 503 : 500;
|
|
2447
|
-
if (!res.headersSent) sendJSON(req, res, statusCode, { error: err.message || 'TTS stream failed', retryable: !isModelError });
|
|
2448
|
-
else res.end();
|
|
2449
|
-
}
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
if (pathOnly.startsWith('/api/tts-cache/') && req.method === 'GET') {
|
|
2454
|
-
const cacheKey = decodeURIComponent(pathOnly.slice('/api/tts-cache/'.length));
|
|
2455
|
-
try {
|
|
2456
|
-
const speech = await getSpeech();
|
|
2457
|
-
const cached = speech.ttsCacheGet(cacheKey);
|
|
2458
|
-
if (cached) {
|
|
2459
|
-
res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': cached.length, 'Cache-Control': 'public, max-age=3600' });
|
|
2460
|
-
res.end(cached);
|
|
2461
|
-
} else {
|
|
2462
|
-
sendJSON(req, res, 404, { error: 'not cached' });
|
|
2463
|
-
}
|
|
2464
|
-
} catch (err) {
|
|
2465
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
2466
|
-
}
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
if (pathOnly === '/api/speech-status' && req.method === 'GET') {
|
|
2471
|
-
try {
|
|
2472
|
-
const { getStatus } = await getSpeech();
|
|
2473
|
-
const baseStatus = getStatus();
|
|
2474
|
-
sendJSON(req, res, 200, {
|
|
2475
|
-
...baseStatus,
|
|
2476
|
-
setupMessage: baseStatus.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
|
|
2477
|
-
modelsDownloading: modelDownloadState.downloading,
|
|
2478
|
-
modelsComplete: modelDownloadState.complete,
|
|
2479
|
-
modelsError: modelDownloadState.error,
|
|
2480
|
-
modelsProgress: modelDownloadState.progress,
|
|
2481
|
-
});
|
|
2482
|
-
} catch (err) {
|
|
2483
|
-
sendJSON(req, res, 200, {
|
|
2484
|
-
sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
|
|
2485
|
-
setupMessage: 'Will setup on first TTS request',
|
|
2486
|
-
modelsDownloading: modelDownloadState.downloading,
|
|
2487
|
-
modelsComplete: modelDownloadState.complete,
|
|
2488
|
-
modelsError: modelDownloadState.error,
|
|
2489
|
-
});
|
|
2490
|
-
}
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
if (pathOnly === '/api/speech-status' && req.method === 'POST') {
|
|
2495
|
-
const body = await parseBody(req);
|
|
2496
|
-
if (body.forceDownload) {
|
|
2497
|
-
if (modelDownloadState.complete) {
|
|
2498
|
-
sendJSON(req, res, 200, { ok: true, modelsComplete: true, message: 'Models already ready' });
|
|
2499
|
-
return;
|
|
2500
|
-
}
|
|
2501
|
-
if (!modelDownloadState.downloading) {
|
|
2502
|
-
modelDownloadState.error = null;
|
|
2503
|
-
ensureModelsDownloaded().then(ok => {
|
|
2504
|
-
broadcastSync({
|
|
2505
|
-
type: 'model_download_progress',
|
|
2506
|
-
progress: { done: true, complete: ok, error: ok ? null : 'Download failed' }
|
|
2507
|
-
});
|
|
2508
|
-
}).catch(err => {
|
|
2509
|
-
broadcastSync({
|
|
2510
|
-
type: 'model_download_progress',
|
|
2511
|
-
progress: { done: true, error: err.message }
|
|
2512
|
-
});
|
|
2513
|
-
});
|
|
2514
|
-
}
|
|
2515
|
-
sendJSON(req, res, 200, { ok: true, message: 'Starting model download' });
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
sendJSON(req, res, 400, { error: 'Unknown request' });
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2200
|
+
const speechHandler = _speechRoutes._match(req.method, pathOnly);
|
|
2201
|
+
if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
|
|
2521
2202
|
|
|
2522
2203
|
if (pathOnly === '/api/clone' && req.method === 'POST') {
|
|
2523
2204
|
const body = await parseBody(req);
|
|
@@ -3742,6 +3423,8 @@ function broadcastSync(event) {
|
|
|
3742
3423
|
const wsRouter = new WsRouter();
|
|
3743
3424
|
|
|
3744
3425
|
initSpeechManager({ broadcastSync, syncClients, queries });
|
|
3426
|
+
const _speechRoutes = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
|
|
3427
|
+
const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
|
|
3745
3428
|
|
|
3746
3429
|
registerConvHandlers(wsRouter, {
|
|
3747
3430
|
queries, activeExecutions, rateLimitState,
|