agentgui 1.0.917 → 1.0.918
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/database-schema.js +0 -58
- package/lib/db-queries-cleanup.js +0 -12
- package/lib/db-queries-del.js +0 -1
- package/lib/db-queries.js +0 -4
- package/lib/http-handler.js +1 -10
- package/lib/plugins/database-plugin.js +1 -1
- package/lib/process-message.js +2 -2
- package/lib/provider-config.js +0 -16
- package/lib/recovery.js +2 -12
- package/lib/routes-agent-actions.js +2 -58
- package/lib/routes-debug.js +1 -7
- package/lib/routes-registry.js +5 -17
- package/lib/server-startup.js +1 -59
- package/lib/stream-event-handler.js +1 -3
- package/lib/ws-handlers-session2.js +2 -23
- package/lib/ws-handlers-util.js +106 -175
- package/lib/ws-legacy-handlers.js +1 -104
- package/lib/ws-setup.js +1 -3
- package/package.json +1 -15
- package/server.js +9 -26
- package/test.js +1 -21
- package/ecosystem.config.cjs +0 -22
- package/lib/checkpoint-manager.js +0 -182
- package/lib/db-queries-tools.js +0 -131
- package/lib/db-queries-voice.js +0 -85
- package/lib/oauth-codex.js +0 -164
- package/lib/oauth-common.js +0 -92
- package/lib/oauth-gemini.js +0 -199
- package/lib/plugins/auth-plugin.js +0 -132
- package/lib/plugins/speech-plugin.js +0 -72
- package/lib/plugins/tools-plugin.js +0 -120
- package/lib/pm2-manager.js +0 -170
- package/lib/routes-oauth.js +0 -105
- package/lib/routes-speech.js +0 -173
- package/lib/routes-tools.js +0 -184
- package/lib/speech-manager.js +0 -200
- package/lib/speech.js +0 -50
- package/lib/tool-install-machine.js +0 -157
- package/lib/tool-manager.js +0 -98
- package/lib/tool-provisioner.js +0 -98
- package/lib/tool-spawner.js +0 -163
- package/lib/tool-version-check.js +0 -196
- package/lib/tool-version-fetch.js +0 -68
- package/lib/ws-handlers-oauth.js +0 -76
- package/static/js/agent-auth-oauth.js +0 -159
- package/static/js/pm2-monitor.js +0 -151
- package/static/js/stt-handler.js +0 -147
- package/static/js/tool-install-machine.js +0 -155
- package/static/js/tools-manager-ui.js +0 -124
- package/static/js/tools-manager.js +0 -172
- package/static/js/voice-machine.js +0 -145
- package/static/js/voice.js +0 -134
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
(function() {
|
|
2
|
-
var btn = document.getElementById('toolsManagerBtn');
|
|
3
|
-
var popup = document.getElementById('toolsPopup');
|
|
4
|
-
var tools = [];
|
|
5
|
-
var isRefreshing = false;
|
|
6
|
-
|
|
7
|
-
var hasRefreshed = false;
|
|
8
|
-
function init() {
|
|
9
|
-
if (!btn || !popup) return;
|
|
10
|
-
btn.style.display = 'flex';
|
|
11
|
-
btn.addEventListener('click', function() { if (!hasRefreshed) { hasRefreshed = true; refresh(); } togglePopup(); });
|
|
12
|
-
document.addEventListener('click', function(e) {
|
|
13
|
-
if (!btn.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
14
|
-
});
|
|
15
|
-
window.addEventListener('ws-message', onWsMessage);
|
|
16
|
-
initVoiceControls();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function initVoiceControls() {
|
|
20
|
-
var autoSpeakToggle = document.getElementById('toolsAutoSpeakToggle');
|
|
21
|
-
var voiceSelector = document.getElementById('toolsVoiceSelector');
|
|
22
|
-
if (!autoSpeakToggle || !voiceSelector) return;
|
|
23
|
-
autoSpeakToggle.checked = localStorage.getItem('toolsAutoSpeak') === 'true';
|
|
24
|
-
window.addEventListener('ws-message', function(e) {
|
|
25
|
-
var data = e.detail;
|
|
26
|
-
if (data && data.type === 'voice_list') window.toolsManagerUI.updateVoiceSelector(data.voices);
|
|
27
|
-
});
|
|
28
|
-
function trySubscribe() {
|
|
29
|
-
if (window.wsManager && window.wsManager.subscribeToVoiceList) {
|
|
30
|
-
window.wsManager.subscribeToVoiceList(window.toolsManagerUI.updateVoiceSelector);
|
|
31
|
-
} else {
|
|
32
|
-
var BASE = window.__BASE_URL || '';
|
|
33
|
-
fetch(BASE + '/api/voices').then(r => r.json()).then(d => {
|
|
34
|
-
if (d.ok && Array.isArray(d.voices)) window.toolsManagerUI.updateVoiceSelector(d.voices);
|
|
35
|
-
}).catch(() => {});
|
|
36
|
-
setTimeout(function() {
|
|
37
|
-
if (window.wsManager && window.wsManager.subscribeToVoiceList)
|
|
38
|
-
window.wsManager.subscribeToVoiceList(window.toolsManagerUI.updateVoiceSelector);
|
|
39
|
-
}, 2000);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
trySubscribe();
|
|
43
|
-
autoSpeakToggle.addEventListener('change', function() {
|
|
44
|
-
localStorage.setItem('toolsAutoSpeak', this.checked);
|
|
45
|
-
if (window.voiceModule) window.voiceModule.setAutoSpeak(this.checked);
|
|
46
|
-
});
|
|
47
|
-
voiceSelector.addEventListener('change', function() {
|
|
48
|
-
localStorage.setItem('toolsVoice', this.value);
|
|
49
|
-
if (window.voiceModule) window.voiceModule.setVoice(this.value);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function fetchTools() {
|
|
54
|
-
window.wsClient.rpc('tools.list')
|
|
55
|
-
.then(d => { tools = d.tools || []; syncMachineStates(); render(); })
|
|
56
|
-
.catch(() => {
|
|
57
|
-
fetch('/gm/api/tools').then(r => r.json()).then(d => { tools = d.tools || []; syncMachineStates(); render(); })
|
|
58
|
-
.catch(e => console.error('[TOOLS-MGR]', e.message));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function syncMachineStates() {
|
|
63
|
-
if (!window.toolInstallMachineAPI) return;
|
|
64
|
-
tools.forEach(function(tool) {
|
|
65
|
-
var ms = window.toolInstallMachineAPI.getState(tool.id);
|
|
66
|
-
if (ms !== 'installing' && ms !== 'updating') {
|
|
67
|
-
if (tool.status === 'installed') window.toolInstallMachineAPI.send(tool.id, { type: 'SET_INSTALLED', installedVersion: tool.installedVersion, publishedVersion: tool.publishedVersion });
|
|
68
|
-
else if (tool.status === 'needs_update') window.toolInstallMachineAPI.send(tool.id, { type: 'SET_NEEDS_UPDATE', installedVersion: tool.installedVersion, publishedVersion: tool.publishedVersion });
|
|
69
|
-
else if (tool.status === 'failed') window.toolInstallMachineAPI.send(tool.id, { type: 'SET_FAILED', error: tool.error_message });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function install(toolId) {
|
|
75
|
-
if (window.toolInstallMachineAPI && window.toolInstallMachineAPI.isLocked(toolId)) return;
|
|
76
|
-
if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'INSTALL' });
|
|
77
|
-
render();
|
|
78
|
-
fetch('/gm/api/tools/' + toolId + '/install', { method: 'POST' }).then(r => r.json()).then(d => {
|
|
79
|
-
if (!d.success) { alert('Install failed: ' + (d.error || 'Unknown error')); if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'FAILED', error: d.error }); render(); }
|
|
80
|
-
}).catch(e => { alert('Install failed: ' + e.message); if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'FAILED', error: e.message }); render(); });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function update(toolId) {
|
|
84
|
-
if (window.toolInstallMachineAPI && window.toolInstallMachineAPI.isLocked(toolId)) return;
|
|
85
|
-
if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'UPDATE' });
|
|
86
|
-
render();
|
|
87
|
-
fetch('/gm/api/tools/' + toolId + '/update', { method: 'POST' }).then(r => r.json()).then(d => {
|
|
88
|
-
if (!d.success) { alert('Update failed: ' + (d.error || 'Unknown error')); if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'FAILED', error: d.error }); render(); }
|
|
89
|
-
}).catch(e => { alert('Update failed: ' + e.message); if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(toolId, { type: 'FAILED', error: e.message }); render(); });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function onWsMessage(e) {
|
|
93
|
-
var data = e.detail;
|
|
94
|
-
if (!data) return;
|
|
95
|
-
if (data.type === 'streaming_progress' && data.block && data.block.type === 'text' && data.block.text) {
|
|
96
|
-
var toggle = document.getElementById('toolsAutoSpeakToggle');
|
|
97
|
-
if (toggle && toggle.checked && (!data.blockRole || data.blockRole === 'assistant'))
|
|
98
|
-
if (window.voiceModule) window.voiceModule.speakText(data.block.text);
|
|
99
|
-
}
|
|
100
|
-
var api = window.toolInstallMachineAPI, tid = data.toolId;
|
|
101
|
-
if (data.type === 'tools_update_started') { (data.tools || []).forEach(t => api && api.send(t, { type: 'UPDATE' })); render(); }
|
|
102
|
-
else if (data.type === 'tool_install_started' || data.type === 'tool_install_progress') { if (api) api.send(tid, { type: 'PROGRESS' }); render(); }
|
|
103
|
-
else if (data.type === 'tool_update_progress') { if (api) api.send(tid, { type: 'PROGRESS' }); render(); }
|
|
104
|
-
else if (data.type === 'tool_install_complete' || data.type === 'tool_update_complete') {
|
|
105
|
-
var d = data.data || {};
|
|
106
|
-
if (api) api.send(tid, { type: 'COMPLETE', version: d.version, installedVersion: d.installedVersion, publishedVersion: d.publishedVersion });
|
|
107
|
-
var tool = tools.find(t => t.id === tid);
|
|
108
|
-
if (tool) { tool.version = d.version || tool.version; tool.installedVersion = d.installedVersion || tool.installedVersion; tool.publishedVersion = d.publishedVersion || tool.publishedVersion; tool.isUpToDate = d.isUpToDate ?? false; tool.upgradeNeeded = d.upgradeNeeded ?? false; tool.hasUpdate = (d.upgradeNeeded && d.installed) ?? false; }
|
|
109
|
-
render(); setTimeout(fetchTools, 1000);
|
|
110
|
-
} else if (data.type === 'tool_install_failed' || data.type === 'tool_update_failed') {
|
|
111
|
-
if (api) api.send(tid, { type: 'FAILED', error: (data.data || {}).error });
|
|
112
|
-
var tool = tools.find(t => t.id === tid); if (tool) tool.error_message = (data.data || {}).error;
|
|
113
|
-
render();
|
|
114
|
-
} else if (data.type === 'tool_status_update') {
|
|
115
|
-
var tool = tools.find(t => t.id === tid);
|
|
116
|
-
if (tool && data.data && data.data.installed) {
|
|
117
|
-
tool.installed = true; tool.isUpToDate = data.data.isUpToDate ?? true; tool.installedVersion = data.data.installedVersion || tool.installedVersion;
|
|
118
|
-
if (api) api.send(tid, data.data.isUpToDate ? { type: 'SET_INSTALLED', installedVersion: tool.installedVersion } : { type: 'SET_NEEDS_UPDATE', installedVersion: tool.installedVersion });
|
|
119
|
-
render();
|
|
120
|
-
}
|
|
121
|
-
} else if (data.type === 'tools_update_complete' || data.type === 'tools_refresh_complete') { isRefreshing = false; fetchTools(); }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function render() {
|
|
125
|
-
var scroll = popup.querySelector('.tools-popup-scroll');
|
|
126
|
-
if (!scroll) return;
|
|
127
|
-
var h = window.webjsx && window.webjsx.createElement;
|
|
128
|
-
var applyDiff = window.webjsx && window.webjsx.applyDiff;
|
|
129
|
-
var ui = window.toolsManagerUI;
|
|
130
|
-
if (!h || !applyDiff) { scroll.innerHTML = '<div class="tool-empty-state"><div class="tool-empty-state-text">Loading...</div></div>'; return; }
|
|
131
|
-
if (!tools.length) {
|
|
132
|
-
applyDiff(scroll, [h('div', { class: 'tool-empty-state', style: 'grid-column:1/-1;' }, h('div', { class: 'tool-empty-state-text' }, 'No tools available'))]);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
var cli = tools.filter(t => t.category === 'cli');
|
|
136
|
-
var plugin = tools.filter(t => t.category === 'plugin');
|
|
137
|
-
var other = tools.filter(t => !t.category);
|
|
138
|
-
var vnodes = [];
|
|
139
|
-
if (cli.length) vnodes.push(h('div', { class: 'tool-section-header' }, 'CLI Agents'), ...cli.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
|
|
140
|
-
if (plugin.length) vnodes.push(h('div', { class: 'tool-section-header' }, 'GM Plugins'), ...plugin.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
|
|
141
|
-
if (other.length) vnodes.push(...other.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
|
|
142
|
-
applyDiff(scroll, vnodes);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function togglePopup(e) { if (e) e.stopPropagation(); if (!popup.classList.contains('open')) { isRefreshing = false; refresh(); } popup.classList.toggle('open'); }
|
|
146
|
-
function closePopup() { popup.classList.remove('open'); }
|
|
147
|
-
function refresh() { fetchTools(); }
|
|
148
|
-
|
|
149
|
-
function updateAll() {
|
|
150
|
-
var toUpdate = tools.filter(t => t.hasUpdate || t.status === 'needs_update' || t.status === 'failed');
|
|
151
|
-
if (!toUpdate.length) { alert('All tools are up-to-date'); return; }
|
|
152
|
-
toUpdate.forEach(t => { if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(t.id, { type: 'UPDATE' }); });
|
|
153
|
-
render();
|
|
154
|
-
fetch('/gm/api/tools/update', { method: 'POST' }).then(r => r.json()).then(d => {
|
|
155
|
-
if (!d.updating) { alert('Update started, but response unexpected'); toUpdate.forEach(t => { if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(t.id, { type: 'FAILED', error: 'unexpected response' }); }); }
|
|
156
|
-
}).catch(e => { alert('Update failed: ' + e.message); toUpdate.forEach(t => { if (window.toolInstallMachineAPI) window.toolInstallMachineAPI.send(t.id, { type: 'FAILED', error: e.message }); }); });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
160
|
-
else init();
|
|
161
|
-
|
|
162
|
-
window.toolsManager = {
|
|
163
|
-
refresh: function() { isRefreshing = true; render(); fetch('/gm/api/tools/refresh-all', { method: 'POST' }).catch(e => console.error('[TOOLS-MGR]', e.message)); },
|
|
164
|
-
install: function(toolId) { install(toolId); },
|
|
165
|
-
update: function(toolId) { update(toolId); },
|
|
166
|
-
updateAll: function() { updateAll(); },
|
|
167
|
-
getAutoSpeak: function() { var t = document.getElementById('toolsAutoSpeakToggle'); return t ? t.checked : false; },
|
|
168
|
-
getVoice: function() { var s = document.getElementById('toolsVoiceSelector'); return s ? s.value : 'default'; },
|
|
169
|
-
setAutoSpeak: function(v) { var t = document.getElementById('toolsAutoSpeakToggle'); if (t) { t.checked = v; localStorage.setItem('toolsAutoSpeak', v); } },
|
|
170
|
-
setVoice: function(v) { var s = document.getElementById('toolsVoiceSelector'); if (s && Array.from(s.options).some(o => o.value === v)) { s.value = v; localStorage.setItem('toolsVoice', v); } }
|
|
171
|
-
};
|
|
172
|
-
})();
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
(function() {
|
|
2
|
-
const { createMachine, createActor, assign } = XState;
|
|
3
|
-
|
|
4
|
-
const MAX_FAILURES = 3;
|
|
5
|
-
|
|
6
|
-
const voiceMachine = createMachine({
|
|
7
|
-
id: 'voice',
|
|
8
|
-
initial: 'idle',
|
|
9
|
-
context: {
|
|
10
|
-
queue: [],
|
|
11
|
-
consecutiveFailures: 0,
|
|
12
|
-
currentText: null,
|
|
13
|
-
enabled: true,
|
|
14
|
-
},
|
|
15
|
-
states: {
|
|
16
|
-
idle: {
|
|
17
|
-
on: {
|
|
18
|
-
SPEAK: {
|
|
19
|
-
target: 'queued',
|
|
20
|
-
guard: ({ context }) => context.enabled,
|
|
21
|
-
actions: assign(({ context, event }) => ({
|
|
22
|
-
queue: [...context.queue, event.text],
|
|
23
|
-
})),
|
|
24
|
-
},
|
|
25
|
-
SET_ENABLED: {
|
|
26
|
-
actions: assign(({ event }) => ({ enabled: event.enabled })),
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
queued: {
|
|
31
|
-
always: [
|
|
32
|
-
{
|
|
33
|
-
guard: ({ context }) => context.queue.length > 0,
|
|
34
|
-
target: 'speaking',
|
|
35
|
-
actions: assign(({ context }) => ({
|
|
36
|
-
currentText: context.queue[0],
|
|
37
|
-
queue: context.queue.slice(1),
|
|
38
|
-
})),
|
|
39
|
-
},
|
|
40
|
-
{ target: 'idle' },
|
|
41
|
-
],
|
|
42
|
-
on: {
|
|
43
|
-
SPEAK: {
|
|
44
|
-
guard: ({ context }) => context.enabled,
|
|
45
|
-
actions: assign(({ context, event }) => ({
|
|
46
|
-
queue: [...context.queue, event.text],
|
|
47
|
-
})),
|
|
48
|
-
},
|
|
49
|
-
STOP: {
|
|
50
|
-
target: 'idle',
|
|
51
|
-
actions: assign({ queue: [], currentText: null }),
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
speaking: {
|
|
56
|
-
on: {
|
|
57
|
-
SPEAK: {
|
|
58
|
-
guard: ({ context }) => context.enabled,
|
|
59
|
-
actions: assign(({ context, event }) => ({
|
|
60
|
-
queue: [...context.queue, event.text],
|
|
61
|
-
})),
|
|
62
|
-
},
|
|
63
|
-
PLAYBACK_DONE: [
|
|
64
|
-
{
|
|
65
|
-
guard: ({ context }) => context.queue.length > 0,
|
|
66
|
-
target: 'speaking',
|
|
67
|
-
actions: assign(({ context }) => ({
|
|
68
|
-
currentText: context.queue[0],
|
|
69
|
-
queue: context.queue.slice(1),
|
|
70
|
-
consecutiveFailures: 0,
|
|
71
|
-
})),
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
target: 'idle',
|
|
75
|
-
actions: assign({ currentText: null, consecutiveFailures: 0 }),
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
PLAYBACK_ERROR: [
|
|
79
|
-
{
|
|
80
|
-
guard: ({ context }) => context.consecutiveFailures + 1 >= MAX_FAILURES,
|
|
81
|
-
target: 'disabled',
|
|
82
|
-
actions: assign({ queue: [], currentText: null, consecutiveFailures: MAX_FAILURES }),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
target: 'idle',
|
|
86
|
-
actions: assign(({ context }) => ({
|
|
87
|
-
consecutiveFailures: context.consecutiveFailures + 1,
|
|
88
|
-
currentText: null,
|
|
89
|
-
})),
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
STOP: {
|
|
93
|
-
target: 'idle',
|
|
94
|
-
actions: assign({ queue: [], currentText: null }),
|
|
95
|
-
},
|
|
96
|
-
SET_ENABLED: {
|
|
97
|
-
actions: assign(({ event }) => ({ enabled: event.enabled })),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
disabled: {
|
|
102
|
-
on: {
|
|
103
|
-
RESET: {
|
|
104
|
-
target: 'idle',
|
|
105
|
-
actions: assign({ consecutiveFailures: 0, queue: [], currentText: null }),
|
|
106
|
-
},
|
|
107
|
-
SET_ENABLED: {
|
|
108
|
-
actions: assign(({ event }) => ({ enabled: event.enabled })),
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const actor = createActor(voiceMachine);
|
|
116
|
-
actor.start();
|
|
117
|
-
|
|
118
|
-
function sendEvent(event) {
|
|
119
|
-
actor.send(event);
|
|
120
|
-
return actor.getSnapshot();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function getState() {
|
|
124
|
-
return actor.getSnapshot().value;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function isActive() {
|
|
128
|
-
return getState() === 'speaking' || getState() === 'queued';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function isDisabled() {
|
|
132
|
-
return getState() === 'disabled';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function getContext() {
|
|
136
|
-
return actor.getSnapshot().context;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function subscribe(fn) {
|
|
140
|
-
return actor.subscribe(fn);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
window.__voiceMachine = actor;
|
|
144
|
-
window.voiceMachineAPI = { send: sendEvent, getState, isActive, isDisabled, getContext, subscribe };
|
|
145
|
-
})();
|
package/static/js/voice.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
(function() {
|
|
2
|
-
var BASE = window.__BASE_URL || '';
|
|
3
|
-
var ttsEnabled = true;
|
|
4
|
-
var currentAudio = null;
|
|
5
|
-
var selectedVoiceId = localStorage.getItem('gmgui-voice-selection') || 'default';
|
|
6
|
-
var ttsAudioCache = new Map();
|
|
7
|
-
var TTS_CLIENT_CACHE_MAX = 50;
|
|
8
|
-
var audioChunkQueue = [];
|
|
9
|
-
var streamingSupported = true;
|
|
10
|
-
|
|
11
|
-
window.addEventListener('ws-message', function(e) {
|
|
12
|
-
var data = e.detail;
|
|
13
|
-
if (!data) return;
|
|
14
|
-
if (data.type === 'tts_audio' && data.audio && data.voiceId === selectedVoiceId) cacheTTSAudio(data.cacheKey, data.audio);
|
|
15
|
-
if (data.type === 'sync_connected') sendVoiceToServer();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
var api = function() { return window.voiceMachineAPI; };
|
|
19
|
-
|
|
20
|
-
if (window.voiceMachineAPI) {
|
|
21
|
-
window.voiceMachineAPI.subscribe(function(snapshot) {
|
|
22
|
-
if (snapshot.value === 'speaking' && snapshot.context.currentText) {
|
|
23
|
-
_doSpeak(snapshot.context.currentText);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
} else {
|
|
27
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
28
|
-
if (window.voiceMachineAPI) {
|
|
29
|
-
window.voiceMachineAPI.subscribe(function(snapshot) {
|
|
30
|
-
if (snapshot.value === 'speaking' && snapshot.context.currentText) {
|
|
31
|
-
_doSpeak(snapshot.context.currentText);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function cacheTTSAudio(cacheKey, b64) {
|
|
39
|
-
if (ttsAudioCache.size >= TTS_CLIENT_CACHE_MAX) ttsAudioCache.delete(ttsAudioCache.keys().next().value);
|
|
40
|
-
var binary = atob(b64), bytes = new Uint8Array(binary.length);
|
|
41
|
-
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
42
|
-
ttsAudioCache.set(cacheKey, new Blob([bytes], { type: 'audio/wav' }));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function playNextChunk() {
|
|
46
|
-
if (!audioChunkQueue.length) {
|
|
47
|
-
if (api()) api().send({ type: 'PLAYBACK_DONE' });
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
var blob = audioChunkQueue.shift(), url = URL.createObjectURL(blob);
|
|
51
|
-
currentAudio = new Audio(url);
|
|
52
|
-
var next = function() { URL.revokeObjectURL(url); currentAudio = null; playNextChunk(); };
|
|
53
|
-
currentAudio.onended = next;
|
|
54
|
-
currentAudio.onerror = function() { URL.revokeObjectURL(url); currentAudio = null; if (api()) api().send({ type: 'PLAYBACK_ERROR' }); };
|
|
55
|
-
currentAudio.play().catch(function() { URL.revokeObjectURL(url); currentAudio = null; if (api()) api().send({ type: 'PLAYBACK_ERROR' }); });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function _doSpeak(text) {
|
|
59
|
-
audioChunkQueue = [];
|
|
60
|
-
var cacheKey = selectedVoiceId + ':' + text;
|
|
61
|
-
var cached = ttsAudioCache.get(cacheKey);
|
|
62
|
-
if (cached) { ttsAudioCache.delete(cacheKey); ttsAudioCache.set(cacheKey, cached); audioChunkQueue.push(cached); playNextChunk(); return; }
|
|
63
|
-
function stream() {
|
|
64
|
-
if (!streamingSupported) { nonStream(text); return; }
|
|
65
|
-
fetch(BASE + '/api/tts-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, voiceId: selectedVoiceId }) })
|
|
66
|
-
.then(function(r) {
|
|
67
|
-
if (!r.ok) { streamingSupported = false; throw 0; }
|
|
68
|
-
var reader = r.body.getReader(), buf = new Uint8Array(0);
|
|
69
|
-
function cat(a, b) { var c = new Uint8Array(a.length + b.length); c.set(a); c.set(b, a.length); return c; }
|
|
70
|
-
function pump() { return reader.read().then(function(res) {
|
|
71
|
-
if (res.done) { if (!audioChunkQueue.length) { if (api()) api().send({ type: 'PLAYBACK_DONE' }); } return; }
|
|
72
|
-
buf = cat(buf, res.value);
|
|
73
|
-
while (buf.length >= 4) {
|
|
74
|
-
var len = new DataView(buf.buffer, buf.byteOffset, 4).getUint32(0, false);
|
|
75
|
-
if (len > 10 * 1024 * 1024) { buf = new Uint8Array(0); if (api()) api().send({ type: 'PLAYBACK_ERROR' }); return; }
|
|
76
|
-
if (buf.length < 4 + len) break;
|
|
77
|
-
audioChunkQueue.push(new Blob([buf.slice(4, 4 + len)], { type: 'audio/wav' }));
|
|
78
|
-
buf = buf.slice(4 + len);
|
|
79
|
-
if (audioChunkQueue.length === 1) playNextChunk();
|
|
80
|
-
}
|
|
81
|
-
return pump();
|
|
82
|
-
}); }
|
|
83
|
-
return pump();
|
|
84
|
-
}).catch(function() { nonStream(text); });
|
|
85
|
-
}
|
|
86
|
-
function nonStream(txt) {
|
|
87
|
-
fetch(BASE + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: txt, voiceId: selectedVoiceId }) })
|
|
88
|
-
.then(function(r) { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
|
89
|
-
.then(function(b) { audioChunkQueue.push(new Blob([b], { type: 'audio/wav' })); playNextChunk(); })
|
|
90
|
-
.catch(function() { if (api()) api().send({ type: 'PLAYBACK_ERROR' }); });
|
|
91
|
-
}
|
|
92
|
-
stream();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function speakDirect(text) {
|
|
96
|
-
if (!ttsEnabled) return;
|
|
97
|
-
var vmApi = api();
|
|
98
|
-
if (vmApi && vmApi.isDisabled()) return;
|
|
99
|
-
var clean = text.replace(/<[^>]*>/g, '').trim();
|
|
100
|
-
if (!clean) return;
|
|
101
|
-
var parts = (typeof agentGUIClient !== 'undefined' && agentGUIClient && typeof agentGUIClient.parseMarkdownCodeBlocks === 'function')
|
|
102
|
-
? agentGUIClient.parseMarkdownCodeBlocks(clean) : [{ type: 'text', content: clean }];
|
|
103
|
-
parts.forEach(function(p) {
|
|
104
|
-
if (p.type !== 'code' && p.content.trim()) {
|
|
105
|
-
if (vmApi) vmApi.send({ type: 'SPEAK', text: p.content.trim() });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function stopSpeaking() {
|
|
111
|
-
audioChunkQueue = [];
|
|
112
|
-
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
|
|
113
|
-
if (api()) api().send({ type: 'STOP' });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function sendVoiceToServer() {
|
|
117
|
-
if (typeof agentGUIClient !== 'undefined' && agentGUIClient && agentGUIClient.wsManager && agentGUIClient.wsManager.isConnected)
|
|
118
|
-
agentGUIClient.wsManager.sendMessage({ type: 'set_voice', voiceId: selectedVoiceId });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
window.voiceModule = {
|
|
122
|
-
getAutoSpeak: function() { return ttsEnabled; },
|
|
123
|
-
setAutoSpeak: function(v) {
|
|
124
|
-
ttsEnabled = Boolean(v);
|
|
125
|
-
localStorage.setItem('gmgui-auto-speak', ttsEnabled);
|
|
126
|
-
if (!ttsEnabled) stopSpeaking();
|
|
127
|
-
if (api()) api().send({ type: 'SET_ENABLED', enabled: ttsEnabled });
|
|
128
|
-
},
|
|
129
|
-
getVoice: function() { return selectedVoiceId; },
|
|
130
|
-
setVoice: function(id) { selectedVoiceId = String(id); localStorage.setItem('gmgui-voice-selection', selectedVoiceId); sendVoiceToServer(); },
|
|
131
|
-
speakText: speakDirect,
|
|
132
|
-
stopSpeaking: stopSpeaking
|
|
133
|
-
};
|
|
134
|
-
})();
|