agentgui 1.0.370 → 1.0.371
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 +18 -274
- package/package.json +1 -1
- package/server.js +0 -58
- package/static/index.html +26 -2
- package/static/js/client.js +0 -128
- package/static/js/features.js +76 -307
- package/IPFS-INTEGRATION-COMPLETE.md +0 -185
- package/IPFS_DOWNLOADER.md +0 -277
- package/TASK_2C_COMPLETION.md +0 -334
- package/scripts/inject-pe-section.py +0 -185
- package/setup-npm-token.sh +0 -68
- package/static/js/progress-dialog.js +0 -130
- package/static/js/tts-websocket-handler.js +0 -152
- package/telemetry-id +0 -1
- package/test-websocket-broadcast.js +0 -147
package/CLAUDE.md
CHANGED
|
@@ -31,7 +31,7 @@ static/js/websocket-manager.js WebSocket connection handling
|
|
|
31
31
|
static/js/ui-components.js UI component helpers
|
|
32
32
|
static/js/syntax-highlighter.js Code syntax highlighting
|
|
33
33
|
static/js/voice.js Voice input/output
|
|
34
|
-
static/js/features.js
|
|
34
|
+
static/js/features.js View toggle, drag-drop upload, model progress indicator
|
|
35
35
|
static/templates/ 31 HTML template fragments for event rendering
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -89,282 +89,26 @@ Server broadcasts:
|
|
|
89
89
|
- `streaming_complete` - Execution finished
|
|
90
90
|
- `streaming_error` - Execution failed
|
|
91
91
|
- `conversation_created`, `conversation_updated`, `conversation_deleted`
|
|
92
|
-
- `
|
|
92
|
+
- `model_download_progress` - Voice model download progress
|
|
93
|
+
- `voice_list` - Available TTS voices
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
+
## Voice Model Download
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
Speech models (~470MB total) are downloaded automatically on server startup. No credentials required.
|
|
97
98
|
|
|
98
|
-
###
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- **Progress reporting**: Real-time updates via WebSocket to UI
|
|
102
|
-
- **Partial install cleanup**: Failed venvs are removed to allow retry
|
|
103
|
-
- **Installation verification**: Binary validation via `--version` check
|
|
104
|
-
- **Concurrent waiting**: Multiple simultaneous requests wait for single setup (600s timeout)
|
|
99
|
+
### Download Sources (fallback chain)
|
|
100
|
+
1. **GitHub LFS** (primary): `https://github.com/AnEntrypoint/models` - LFS-tracked ONNX files via `media.githubusercontent.com`, small files via `raw.githubusercontent.com`
|
|
101
|
+
2. **HuggingFace** (fallback): `onnx-community/whisper-base` for STT, `AnEntrypoint/sttttsmodels` for TTS
|
|
105
102
|
|
|
106
|
-
###
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
PIP_TIMEOUT: 120000, // 2 minutes
|
|
110
|
-
VENV_CREATION_TIMEOUT: 30000, // 30 seconds
|
|
111
|
-
MAX_RETRIES: 3, // 3 attempts
|
|
112
|
-
RETRY_DELAY_MS: 1000, // 1 second initial
|
|
113
|
-
RETRY_BACKOFF_MULTIPLIER: 2, // 2x exponential
|
|
114
|
-
};
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Network Requirements
|
|
118
|
-
- **Minimum**: 50 kbps sustained, < 5s latency, < 10% packet loss
|
|
119
|
-
- **Recommended**: 256+ kbps, < 2s latency, < 1% packet loss
|
|
120
|
-
- **Expected time on slow connection**: 2-6 minutes with retries
|
|
121
|
-
|
|
122
|
-
### Progress Messages
|
|
123
|
-
During TTS setup on first use, WebSocket broadcasts:
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"type": "tts_setup_progress",
|
|
127
|
-
"step": "detecting-python|creating-venv|installing|verifying",
|
|
128
|
-
"status": "in-progress|success|error",
|
|
129
|
-
"message": "descriptive status message with retry count if applicable"
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Recovery Behavior
|
|
134
|
-
1. Network timeout → auto-retry with backoff
|
|
135
|
-
2. Partial venv → auto-cleanup before retry
|
|
136
|
-
3. Failed verification → auto-cleanup and error
|
|
137
|
-
4. Concurrent requests → first starts setup, others wait up to 600s
|
|
138
|
-
5. Interrupted setup → cleanup allows fresh retry
|
|
139
|
-
|
|
140
|
-
### Testing
|
|
141
|
-
Setup validates by running pocket-tts binary with `--version` flag to confirm functional installation, not just file existence.
|
|
142
|
-
|
|
143
|
-
## Model Download Fallback Chain Architecture (Task 1C)
|
|
144
|
-
|
|
145
|
-
Three-layer resilient fallback for speech models (280MB whisper-base + 197MB TTS). Designed to eliminate single points of failure while maintaining backward compatibility.
|
|
146
|
-
|
|
147
|
-
### Layer 1: IPFS Gateway (Primary)
|
|
148
|
-
|
|
149
|
-
Decentralized distribution across three gateways with automatic failover:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
Cloudflare IPFS https://cloudflare-ipfs.com/ipfs/ Priority 1 (99.9% reliable)
|
|
153
|
-
dweb.link https://dweb.link/ipfs/ Priority 2 (99% reliable)
|
|
154
|
-
Pinata https://gateway.pinata.cloud/ipfs/ Priority 3 (99.5% reliable)
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**Model Distribution**:
|
|
158
|
-
- Whisper Base (280MB): `TBD_WHISPER_HASH` → encoder (78.6MB) + decoder (198.9MB) + configs
|
|
159
|
-
- TTS Models (197MB): `TBD_TTS_HASH` → mimi_encoder (73MB) + decoders + text_conditioner + flow_lm
|
|
160
|
-
|
|
161
|
-
**Characteristics**: 30s timeout per gateway, 2 retries before fallback, SHA-256 per-file verification against IPFS-stored manifest
|
|
162
|
-
|
|
163
|
-
### Layer 2: HuggingFace (Secondary)
|
|
164
|
-
|
|
165
|
-
Current working implementation via webtalk package. Proven reliable with region-dependent latency.
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
Whisper https://huggingface.co/onnx-community/whisper-base/resolve/main/
|
|
169
|
-
TTS https://huggingface.co/datasets/AnEntrypoint/sttttsmodels/resolve/main/tts/
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Characteristics**: 3 retries with exponential backoff (2^attempt seconds), 30s timeout, file size validation (minBytes thresholds: encoder ≥40MB, decoder ≥100MB, TTS files ≥18-61MB range)
|
|
173
|
-
|
|
174
|
-
**Implementation Location**: webtalk/whisper-models.js, webtalk/tts-models.js (unchanged, wrapped by fallback logic)
|
|
175
|
-
|
|
176
|
-
### Layer 3: Local Cache + Fallbacks
|
|
177
|
-
|
|
178
|
-
**Primary Cache**: `~/.gmgui/models/` with manifest at `~/.gmgui/models/.manifests.json`
|
|
179
|
-
|
|
180
|
-
**Verification Algorithms**:
|
|
181
|
-
1. Size check (minBytes threshold) → corrupted: delete & retry
|
|
182
|
-
2. SHA-256 hash against manifest → mismatch: delete & re-download
|
|
183
|
-
3. ONNX format validation (header check) → invalid: delete & escalate to primary
|
|
184
|
-
|
|
185
|
-
**Bundled Models** (future): `agentgui/bundled-models.tar.gz` (~50-80MB) for offline-first deployments
|
|
186
|
-
|
|
187
|
-
**Peer-to-Peer** (future): mDNS discovery for LAN sharing across multiple AgentGUI instances
|
|
188
|
-
|
|
189
|
-
### Download Decision Logic
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
1. Check local cache validity → RETURN if valid, record cache_hit metric
|
|
193
|
-
2. TRY PRIMARY (IPFS): attempt 3 gateways sequentially, 2 retries each
|
|
194
|
-
- VERIFY size + sha256 → ON SUCCESS: record primary_success, return
|
|
195
|
-
3. TRY SECONDARY (HuggingFace): 3 attempts with exponential backoff
|
|
196
|
-
- VERIFY file size → ON SUCCESS: record secondary_success, return
|
|
197
|
-
4. TRY TERTIARY (Bundled): extract tarball if present
|
|
198
|
-
- VERIFY extraction → ON SUCCESS: record tertiary_bundled_success, return
|
|
199
|
-
5. TRY TERTIARY (Peer): query mDNS if enabled, fetch from peer
|
|
200
|
-
- VERIFY checksum → ON SUCCESS: record tertiary_peer_success, return
|
|
201
|
-
6. FAILURE: record all_layers_exhausted metric, throw error (optional: activate degraded mode)
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Metrics Collection
|
|
205
|
-
|
|
206
|
-
**Storage**: `~/.gmgui/models/.metrics.json` (append-only, rotated daily)
|
|
207
|
-
|
|
208
|
-
**Per-Download Fields**: timestamp, modelType, layer, gateway, status, latency_ms, bytes_downloaded/total, error_type/message
|
|
209
|
-
|
|
210
|
-
**Aggregations**: per-layer success rate, per-gateway success rate, avg latency per layer, cache effectiveness
|
|
211
|
-
|
|
212
|
-
**Dashboard Endpoints**:
|
|
213
|
-
- `GET /api/metrics/downloads` - all metrics
|
|
214
|
-
- `GET /api/metrics/downloads/summary` - aggregated stats
|
|
215
|
-
- `GET /api/metrics/downloads/health` - per-layer health
|
|
216
|
-
- `POST /api/metrics/downloads/reset` - clear history
|
|
217
|
-
|
|
218
|
-
### Cache Invalidation Strategy
|
|
219
|
-
|
|
220
|
-
**Version Manifest** (`~/.gmgui/models/.manifests.json`):
|
|
221
|
-
```json
|
|
222
|
-
{
|
|
223
|
-
"whisper-base": {
|
|
224
|
-
"currentVersion": "1.0.0",
|
|
225
|
-
"ipfsHash": "QmXXXX...",
|
|
226
|
-
"huggingfaceTag": "revision-hash",
|
|
227
|
-
"downloadedAt": "ISO8601",
|
|
228
|
-
"sha256": { "file": "hash...", ... }
|
|
229
|
-
},
|
|
230
|
-
"tts-models": { ... }
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Version Mismatch Detection** (on startup + periodic background check):
|
|
235
|
-
- Query HuggingFace API HEAD for latest revision
|
|
236
|
-
- Query IPFS gateway for latest dag-json manifest
|
|
237
|
-
- If new version: log warning, set flag in `/api/status`, prompt user (not auto-download)
|
|
238
|
-
- If corrupted: quarantine to `.bak`, mark invalid, trigger auto-download from primary on next request
|
|
239
|
-
|
|
240
|
-
**Stale Cache Handling**:
|
|
241
|
-
- Max age: 90 days → background check queries IPFS for new hash
|
|
242
|
-
- Stale window: 7 days after max age → serve stale if live fetch fails
|
|
243
|
-
- Offline degradation: serve even if 365 days old when network down
|
|
244
|
-
|
|
245
|
-
**Cleanup Policy**:
|
|
246
|
-
- Backup retention: 1 previous version (`.bak`) for 7 days
|
|
247
|
-
- Failed downloads: delete `*.tmp` after 1 hour idle
|
|
248
|
-
- Old versions: delete if > 90 days old
|
|
249
|
-
- Disk threshold: warn if `~/.gmgui/models` exceeds 2GB
|
|
250
|
-
|
|
251
|
-
### Design Rationale
|
|
252
|
-
|
|
253
|
-
**Why Three Layers?** IPFS (decentralized, no SPoF) + HuggingFace (proven, existing) + Local (offline-ready, LAN-resilient)
|
|
254
|
-
|
|
255
|
-
**Why Metrics First?** Enables data-driven gateway selection, identifies reliability in production, guides timeout/retry tuning
|
|
256
|
-
|
|
257
|
-
**Why No Auto-Upgrade?** User controls timing, allows staged rollout, supports version pinning, reduces surprise breakage
|
|
258
|
-
|
|
259
|
-
**Why Bundled Models?** Enables air-gapped deployments, reduces network load, supports edge environments with poor connectivity
|
|
260
|
-
|
|
261
|
-
### Implementation Status
|
|
262
|
-
|
|
263
|
-
| Phase | Description | Status | File(s) |
|
|
264
|
-
|-------|-------------|--------|---------|
|
|
265
|
-
| 1 | IPFS gateway discovery | ✅ DONE | `webtalk/ipfs-downloader.js` |
|
|
266
|
-
| 2 | 3-layer fallback chain | ✅ DONE | `lib/model-downloader.js` |
|
|
267
|
-
| 3 | Metrics collection | ✅ DONE | `lib/model-downloader.js` (JSON storage) |
|
|
268
|
-
| 4 | Manifest generation with SHA-256 | ✅ DONE | Generated to `~/.gmgui/models/.manifests.json` |
|
|
269
|
-
| 5 | Metrics API endpoints | ✅ DONE | `server.js` (4 endpoints added) |
|
|
270
|
-
| 6 | IPFS publishing script | ✅ DONE | `scripts/publish-models-to-ipfs.js` |
|
|
271
|
-
| 7 | Database IPFS tables | ✅ EXISTS | `database.js` (ipfs_cids, ipfs_downloads) |
|
|
272
|
-
| 8 | Integration into ensureModels | ⏳ TODO | Need to wire into `server.js` |
|
|
273
|
-
| 9 | Publish to IPFS (get real CIDs) | ⏳ TODO | Requires Pinata API keys |
|
|
274
|
-
| 10 | Update database.js with real CIDs | ⏳ TODO | After publishing |
|
|
275
|
-
| 11 | Stale-while-revalidate checks | 📋 FUTURE | Background job |
|
|
276
|
-
| 12 | Bundled models | 📋 FUTURE | Tarball creation |
|
|
277
|
-
| 13 | Peer-to-peer discovery | 📋 FUTURE | mDNS implementation |
|
|
278
|
-
|
|
279
|
-
### Current Model Inventory
|
|
280
|
-
|
|
281
|
-
**Models Downloaded Locally**: `~/.gmgui/models/`
|
|
282
|
-
|
|
283
|
-
**Whisper Base** (280.15 MB) - 7 files:
|
|
284
|
-
- `config.json` (0.00 MB) - SHA256: `f4d0608f7d918166...`
|
|
285
|
-
- `tokenizer.json` (2.37 MB) - SHA256: `27fc476bfe7f1729...`
|
|
286
|
-
- `tokenizer_config.json` (0.27 MB) - SHA256: `2e036e4dbacfdeb7...`
|
|
287
|
-
- `onnx/encoder_model.onnx` (78.65 MB) - SHA256: `a9f3b752833b49e8...`
|
|
288
|
-
- `onnx/decoder_model_merged.onnx` (198.86 MB) - SHA256: `514903744bb1b458...`
|
|
289
|
-
|
|
290
|
-
**TTS Models** (189.40 MB) - 6 files:
|
|
291
|
-
- `mimi_encoder.onnx` (69.78 MB) - SHA256: `360f050cd0b1e1c9...`
|
|
292
|
-
- `flow_lm_main_int8.onnx` (72.81 MB) - SHA256: `fd5cdd7f7ab05f63...`
|
|
293
|
-
- `mimi_decoder_int8.onnx` (21.63 MB) - SHA256: `501e16f51cf3fb91...`
|
|
294
|
-
- `text_conditioner.onnx` (15.63 MB) - SHA256: `80ea69f46d8153a9...`
|
|
295
|
-
- `flow_lm_flow_int8.onnx` (9.50 MB) - SHA256: `8d627d235c44a597...`
|
|
296
|
-
- `tokenizer.model` (0.06 MB) - SHA256: `d461765ae1795666...`
|
|
297
|
-
|
|
298
|
-
**Manifest Location**: `~/.gmgui/models/.manifests.json` (auto-generated with full SHA-256 hashes)
|
|
299
|
-
|
|
300
|
-
### Next Steps to Complete Task 1C
|
|
301
|
-
|
|
302
|
-
#### 1. Publish Models to IPFS (Get Real CIDs)
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
# Get free Pinata API keys at https://www.pinata.cloud/
|
|
306
|
-
export PINATA_API_KEY=your_api_key
|
|
307
|
-
export PINATA_SECRET_KEY=your_secret_key
|
|
308
|
-
|
|
309
|
-
# Run publishing script
|
|
310
|
-
node scripts/publish-models-to-ipfs.js
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
This will output real IPFS CIDs for both model sets.
|
|
314
|
-
|
|
315
|
-
#### 2. Update database.js with Real CIDs
|
|
316
|
-
|
|
317
|
-
Replace placeholder CIDs in `database.js` (lines 389-390):
|
|
318
|
-
```javascript
|
|
319
|
-
const WHISPER_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy'; // PLACEHOLDER
|
|
320
|
-
const TTS_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy'; // PLACEHOLDER
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Update with real CIDs from step 1.
|
|
324
|
-
|
|
325
|
-
#### 3. Integrate Fallback Chain
|
|
326
|
-
|
|
327
|
-
Modify `server.js` `ensureModelsDownloaded()` (starting line 66) to use the new 3-layer fallback:
|
|
328
|
-
|
|
329
|
-
```javascript
|
|
330
|
-
import { downloadWithFallback } from './lib/model-downloader.js';
|
|
331
|
-
import { queries } from './database.js';
|
|
332
|
-
|
|
333
|
-
// Get IPFS CIDs from database
|
|
334
|
-
const whisperCidRecord = queries.getIpfsCidByModel('whisper-base', 'stt');
|
|
335
|
-
const ttsCidRecord = queries.getIpfsCidByModel('tts', 'voice');
|
|
336
|
-
|
|
337
|
-
// Load manifest
|
|
338
|
-
const manifestPath = path.join(modelsDir, '.manifests.json');
|
|
339
|
-
const manifests = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
340
|
-
|
|
341
|
-
// For each required file, use fallback chain:
|
|
342
|
-
for (const [filename, fileInfo] of Object.entries(manifests['whisper-base'].files)) {
|
|
343
|
-
const destPath = path.join(whisperDir, filename);
|
|
344
|
-
await downloadWithFallback({
|
|
345
|
-
ipfsCid: `${whisperCidRecord.cid}/${filename}`,
|
|
346
|
-
huggingfaceUrl: `https://huggingface.co/onnx-community/whisper-base/resolve/main/${filename}`,
|
|
347
|
-
destPath,
|
|
348
|
-
manifest: fileInfo,
|
|
349
|
-
minBytes: fileInfo.size * 0.8,
|
|
350
|
-
preferredLayer: 'ipfs'
|
|
351
|
-
}, (progress) => {
|
|
352
|
-
broadcastModelProgress({ ...progress, file: filename, type: 'whisper' });
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Metrics API Endpoints (Live)
|
|
358
|
-
|
|
359
|
-
- `GET /gm/api/metrics/downloads` - All download metrics (last 24 hours)
|
|
360
|
-
- `GET /gm/api/metrics/downloads/summary` - Aggregated statistics
|
|
361
|
-
- `GET /gm/api/metrics/downloads/health` - Per-layer health status (success rates, latency)
|
|
362
|
-
- `POST /gm/api/metrics/downloads/reset` - Clear metrics history
|
|
103
|
+
### Models
|
|
104
|
+
- **Whisper Base** (~280MB): encoder + decoder ONNX models, tokenizer, config files
|
|
105
|
+
- **TTS Models** (~190MB): mimi encoder/decoder, flow_lm, text_conditioner, tokenizer
|
|
363
106
|
|
|
364
|
-
###
|
|
107
|
+
### UI Behavior
|
|
108
|
+
- Voice tab is hidden until models are ready
|
|
109
|
+
- A circular progress indicator appears in the header during download
|
|
110
|
+
- Once models are downloaded, the Voice tab becomes visible
|
|
111
|
+
- Model status is broadcast via WebSocket `model_download_progress` events
|
|
365
112
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
- `scripts/publish-models-to-ipfs.js` - Pinata-based publishing (no local IPFS needed)
|
|
369
|
-
- `~/.gmgui/models/.manifests.json` - Auto-generated with SHA-256 hashes
|
|
370
|
-
- `~/.gmgui/models/.metrics.json` - Download metrics (auto-rotated daily)
|
|
113
|
+
### Cache Location
|
|
114
|
+
Models are stored at `~/.gmgui/models/` (whisper in `onnx-community/whisper-base/`, TTS in `tts/`).
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -2652,64 +2652,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2652
2652
|
return;
|
|
2653
2653
|
}
|
|
2654
2654
|
|
|
2655
|
-
if (pathOnly === '/api/metrics/downloads' && req.method === 'GET') {
|
|
2656
|
-
try {
|
|
2657
|
-
const { getMetrics } = await import('./lib/model-downloader.js');
|
|
2658
|
-
const metrics = getMetrics();
|
|
2659
|
-
sendJSON(req, res, 200, { metrics });
|
|
2660
|
-
} catch (err) {
|
|
2661
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
2662
|
-
}
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
if (pathOnly === '/api/metrics/downloads/summary' && req.method === 'GET') {
|
|
2667
|
-
try {
|
|
2668
|
-
const { getMetricsSummary } = await import('./lib/model-downloader.js');
|
|
2669
|
-
const summary = getMetricsSummary();
|
|
2670
|
-
sendJSON(req, res, 200, summary);
|
|
2671
|
-
} catch (err) {
|
|
2672
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
2673
|
-
}
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
if (pathOnly === '/api/metrics/downloads/health' && req.method === 'GET') {
|
|
2678
|
-
try {
|
|
2679
|
-
const { getMetricsSummary } = await import('./lib/model-downloader.js');
|
|
2680
|
-
const summary = getMetricsSummary();
|
|
2681
|
-
const health = {
|
|
2682
|
-
huggingface: {
|
|
2683
|
-
status: summary.huggingface.success > 0 ? 'healthy' : summary.huggingface.error > 0 ? 'degraded' : 'unknown',
|
|
2684
|
-
success_rate: summary.huggingface.success + summary.huggingface.error > 0
|
|
2685
|
-
? ((summary.huggingface.success / (summary.huggingface.success + summary.huggingface.error)) * 100).toFixed(2)
|
|
2686
|
-
: 0,
|
|
2687
|
-
avg_latency_ms: summary.huggingface.avg_latency
|
|
2688
|
-
},
|
|
2689
|
-
cache: {
|
|
2690
|
-
hit_rate: summary.total > 0
|
|
2691
|
-
? ((summary.cache_hits / summary.total) * 100).toFixed(2)
|
|
2692
|
-
: 0
|
|
2693
|
-
}
|
|
2694
|
-
};
|
|
2695
|
-
sendJSON(req, res, 200, health);
|
|
2696
|
-
} catch (err) {
|
|
2697
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
2698
|
-
}
|
|
2699
|
-
return;
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
if (pathOnly === '/api/metrics/downloads/reset' && req.method === 'POST') {
|
|
2703
|
-
try {
|
|
2704
|
-
const { resetMetrics } = await import('./lib/model-downloader.js');
|
|
2705
|
-
resetMetrics();
|
|
2706
|
-
sendJSON(req, res, 200, { ok: true, message: 'Metrics reset' });
|
|
2707
|
-
} catch (err) {
|
|
2708
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
2709
|
-
}
|
|
2710
|
-
return;
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
2655
|
if (pathOnly === '/api/speech-status' && req.method === 'POST') {
|
|
2714
2656
|
const body = await parseBody(req);
|
|
2715
2657
|
if (body.forceDownload) {
|
package/static/index.html
CHANGED
|
@@ -2694,6 +2694,26 @@
|
|
|
2694
2694
|
.connection-dot.reconnecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
|
|
2695
2695
|
.connection-dot.downloading { background: #3b82f6; animation: pulse 1s ease-in-out infinite; }
|
|
2696
2696
|
|
|
2697
|
+
.model-dl-indicator {
|
|
2698
|
+
display: none;
|
|
2699
|
+
position: relative;
|
|
2700
|
+
width: 24px;
|
|
2701
|
+
height: 24px;
|
|
2702
|
+
cursor: pointer;
|
|
2703
|
+
}
|
|
2704
|
+
.model-dl-indicator.active { display: inline-flex; align-items: center; justify-content: center; }
|
|
2705
|
+
.model-dl-indicator svg { width: 24px; height: 24px; transform: rotate(-90deg); }
|
|
2706
|
+
.model-dl-indicator .track { fill: none; stroke: var(--color-bg-secondary); stroke-width: 3; }
|
|
2707
|
+
.model-dl-indicator .progress { fill: none; stroke: var(--color-primary); stroke-width: 3; stroke-linecap: round; transition: stroke-dashoffset 0.3s ease; }
|
|
2708
|
+
.model-dl-indicator .icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
|
2709
|
+
.model-dl-indicator .icon svg { width: 12px; height: 12px; transform: none; }
|
|
2710
|
+
.model-dl-indicator-tooltip {
|
|
2711
|
+
display: none; position: absolute; top: calc(100% + 6px); right: 0; background: var(--color-bg-primary);
|
|
2712
|
+
border: 1px solid var(--color-border); border-radius: 6px; padding: 8px 12px; font-size: 0.75rem;
|
|
2713
|
+
white-space: nowrap; z-index: 1000; box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
2714
|
+
}
|
|
2715
|
+
.model-dl-indicator:hover .model-dl-indicator-tooltip { display: block; }
|
|
2716
|
+
|
|
2697
2717
|
.connection-tooltip {
|
|
2698
2718
|
position: absolute;
|
|
2699
2719
|
top: 100%;
|
|
@@ -2972,6 +2992,11 @@
|
|
|
2972
2992
|
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="1"></rect></svg>
|
|
2973
2993
|
</button>
|
|
2974
2994
|
</div>
|
|
2995
|
+
<div class="model-dl-indicator" id="modelDlIndicator" title="Downloading voice models...">
|
|
2996
|
+
<svg viewBox="0 0 24 24"><circle class="track" cx="12" cy="12" r="10"/><circle class="progress" cx="12" cy="12" r="10" stroke-dasharray="62.83" stroke-dashoffset="62.83"/></svg>
|
|
2997
|
+
<div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/></svg></div>
|
|
2998
|
+
<div class="model-dl-indicator-tooltip" id="modelDlTooltip">Preparing voice models...</div>
|
|
2999
|
+
</div>
|
|
2975
3000
|
<button class="header-icon-btn agent-auth-btn" id="agentAuthBtn" title="Agent authentication" aria-label="Agent authentication" style="display:none;">
|
|
2976
3001
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
|
2977
3002
|
<div class="agent-auth-dropdown" id="agentAuthDropdown"></div>
|
|
@@ -3000,7 +3025,7 @@
|
|
|
3000
3025
|
<div class="view-toggle-bar" id="viewToggleBar">
|
|
3001
3026
|
<button class="view-toggle-btn active" data-view="chat">Chat</button>
|
|
3002
3027
|
<button class="view-toggle-btn" data-view="files">Files</button>
|
|
3003
|
-
<button class="view-toggle-btn" data-view="voice">Voice</button>
|
|
3028
|
+
<button class="view-toggle-btn" data-view="voice" id="voiceTabBtn" style="display:none;">Voice</button>
|
|
3004
3029
|
<button class="view-toggle-btn" data-view="terminal" id="terminalTabBtn">Terminal</button>
|
|
3005
3030
|
</div>
|
|
3006
3031
|
|
|
@@ -3129,7 +3154,6 @@
|
|
|
3129
3154
|
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
3130
3155
|
<script defer src="/gm/js/dialogs.js"></script>
|
|
3131
3156
|
<script defer src="/gm/js/ui-components.js"></script>
|
|
3132
|
-
<script defer src="/gm/js/progress-dialog.js"></script>
|
|
3133
3157
|
<script defer src="/gm/js/conversations.js"></script>
|
|
3134
3158
|
<script defer src="/gm/js/client.js"></script>
|
|
3135
3159
|
<script type="module" src="/gm/js/voice.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -1890,19 +1890,14 @@ class AgentGUIClient {
|
|
|
1890
1890
|
} else if (status.modelsDownloading) {
|
|
1891
1891
|
this._modelDownloadProgress = status.modelsProgress || { downloading: true };
|
|
1892
1892
|
this._modelDownloadInProgress = true;
|
|
1893
|
-
setTimeout(function() {
|
|
1894
|
-
if (window.__showVoiceDownloadProgress) window.__showVoiceDownloadProgress();
|
|
1895
|
-
}, 0);
|
|
1896
1893
|
} else {
|
|
1897
1894
|
this._modelDownloadProgress = { done: false };
|
|
1898
1895
|
this._modelDownloadInProgress = false;
|
|
1899
1896
|
}
|
|
1900
|
-
this._updateVoiceTabState();
|
|
1901
1897
|
} catch (error) {
|
|
1902
1898
|
console.error('Failed to check speech status:', error);
|
|
1903
1899
|
this._modelDownloadProgress = { done: false };
|
|
1904
1900
|
this._modelDownloadInProgress = false;
|
|
1905
|
-
this._updateVoiceTabState();
|
|
1906
1901
|
}
|
|
1907
1902
|
}
|
|
1908
1903
|
|
|
@@ -2044,75 +2039,21 @@ class AgentGUIClient {
|
|
|
2044
2039
|
|
|
2045
2040
|
_handleModelDownloadProgress(progress) {
|
|
2046
2041
|
this._modelDownloadProgress = progress;
|
|
2047
|
-
|
|
2048
2042
|
if (progress.status === 'failed' || progress.error) {
|
|
2049
2043
|
this._modelDownloadInProgress = false;
|
|
2050
2044
|
console.error('[Models] Download error:', progress.error || progress.status);
|
|
2051
2045
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2052
|
-
if (window._voiceProgressDialog) {
|
|
2053
|
-
window._voiceProgressDialog.close();
|
|
2054
|
-
window._voiceProgressDialog = null;
|
|
2055
|
-
}
|
|
2056
|
-
const errorMsg = 'Failed to download voice models: ' + (progress.error || 'unknown error');
|
|
2057
|
-
if (window.UIDialog) {
|
|
2058
|
-
window.UIDialog.alert(errorMsg, 'Download Error');
|
|
2059
|
-
} else {
|
|
2060
|
-
alert(errorMsg);
|
|
2061
|
-
}
|
|
2062
2046
|
return;
|
|
2063
2047
|
}
|
|
2064
|
-
|
|
2065
2048
|
if (progress.done || progress.status === 'completed') {
|
|
2066
2049
|
this._modelDownloadInProgress = false;
|
|
2067
2050
|
console.log('[Models] Download complete');
|
|
2068
2051
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2069
|
-
this._updateVoiceTabState();
|
|
2070
|
-
if (window._voiceProgressDialog) {
|
|
2071
|
-
window._voiceProgressDialog.update(100, 'Voice models ready!');
|
|
2072
|
-
setTimeout(function() {
|
|
2073
|
-
if (window._voiceProgressDialog) {
|
|
2074
|
-
window._voiceProgressDialog.close();
|
|
2075
|
-
window._voiceProgressDialog = null;
|
|
2076
|
-
}
|
|
2077
|
-
}, 500);
|
|
2078
|
-
}
|
|
2079
|
-
if (window._voiceTabPendingOpen) {
|
|
2080
|
-
window._voiceTabPendingOpen = false;
|
|
2081
|
-
var voiceBtn = document.querySelector('[data-view="voice"]');
|
|
2082
|
-
if (voiceBtn) voiceBtn.click();
|
|
2083
|
-
}
|
|
2084
2052
|
return;
|
|
2085
2053
|
}
|
|
2086
|
-
|
|
2087
2054
|
if (progress.started || progress.downloading || progress.status === 'downloading' || progress.status === 'connecting') {
|
|
2088
2055
|
this._modelDownloadInProgress = true;
|
|
2089
2056
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2090
|
-
|
|
2091
|
-
if (!window._voiceProgressDialog && window.__showVoiceDownloadProgress) {
|
|
2092
|
-
window.__showVoiceDownloadProgress();
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
if (window._voiceProgressDialog) {
|
|
2096
|
-
let displayText = 'Downloading models...';
|
|
2097
|
-
|
|
2098
|
-
if (progress.status === 'connecting') {
|
|
2099
|
-
displayText = 'Connecting to ' + (progress.currentGateway || 'gateway') + '...';
|
|
2100
|
-
} else if (progress.totalBytes > 0) {
|
|
2101
|
-
const downloaded = (progress.bytesDownloaded || 0) / 1024 / 1024;
|
|
2102
|
-
const total = progress.totalBytes / 1024 / 1024;
|
|
2103
|
-
const speed = progress.downloadSpeed ? (progress.downloadSpeed / 1024 / 1024).toFixed(2) : '0';
|
|
2104
|
-
const eta = progress.eta ? Math.ceil(progress.eta) + 's' : '...';
|
|
2105
|
-
const retryInfo = progress.retryCount > 0 ? ` (retry ${progress.retryCount})` : '';
|
|
2106
|
-
|
|
2107
|
-
displayText = `Downloading ${downloaded.toFixed(1)}MB / ${total.toFixed(1)}MB @ ${speed}MB/s (ETA: ${eta})${retryInfo}`;
|
|
2108
|
-
} else if (progress.file) {
|
|
2109
|
-
displayText = 'Loading ' + progress.file + '...';
|
|
2110
|
-
} else if (progress.completedFiles && progress.totalFiles) {
|
|
2111
|
-
displayText = `Downloaded ${progress.completedFiles}/${progress.totalFiles} files`;
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
window._voiceProgressDialog.update(progress.percentComplete || 0, displayText);
|
|
2115
|
-
}
|
|
2116
2057
|
}
|
|
2117
2058
|
}
|
|
2118
2059
|
|
|
@@ -2626,24 +2567,10 @@ class AgentGUIClient {
|
|
|
2626
2567
|
};
|
|
2627
2568
|
}
|
|
2628
2569
|
|
|
2629
|
-
_handleModelDownloadProgress(data) {
|
|
2630
|
-
if (!data) return;
|
|
2631
|
-
this._modelDownloadProgress = data;
|
|
2632
|
-
if (data.done && data.complete) {
|
|
2633
|
-
this._modelDownloadInProgress = false;
|
|
2634
|
-
this._hideModelDownloadProgress();
|
|
2635
|
-
} else if (data.downloading || data.done === false) {
|
|
2636
|
-
this._modelDownloadInProgress = true;
|
|
2637
|
-
this._showModelDownloadProgress(data);
|
|
2638
|
-
}
|
|
2639
|
-
this._updateVoiceTabState();
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
2570
|
_handleSTTProgress(data) {
|
|
2643
2571
|
if (!data) return;
|
|
2644
2572
|
const transcriptEl = document.getElementById('voiceTranscript');
|
|
2645
2573
|
if (!transcriptEl) return;
|
|
2646
|
-
|
|
2647
2574
|
if (data.status === 'transcribing') {
|
|
2648
2575
|
transcriptEl.textContent = 'Transcribing...';
|
|
2649
2576
|
transcriptEl.style.color = 'var(--color-text-secondary)';
|
|
@@ -2656,61 +2583,6 @@ class AgentGUIClient {
|
|
|
2656
2583
|
}
|
|
2657
2584
|
}
|
|
2658
2585
|
|
|
2659
|
-
_handleTTSSetupProgress(data) {
|
|
2660
|
-
if (!data) return;
|
|
2661
|
-
this._showModelDownloadProgress({
|
|
2662
|
-
downloading: true,
|
|
2663
|
-
message: data.message || 'Setting up TTS...'
|
|
2664
|
-
});
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
_showModelDownloadProgress(progress) {
|
|
2668
|
-
let indicator = document.getElementById('modelDownloadIndicator');
|
|
2669
|
-
if (!indicator) {
|
|
2670
|
-
const header = document.querySelector('.main-header');
|
|
2671
|
-
if (!header) return;
|
|
2672
|
-
|
|
2673
|
-
indicator = document.createElement('div');
|
|
2674
|
-
indicator.id = 'modelDownloadIndicator';
|
|
2675
|
-
indicator.style = `
|
|
2676
|
-
display: flex;
|
|
2677
|
-
align-items: center;
|
|
2678
|
-
gap: 0.5rem;
|
|
2679
|
-
padding: 0.375rem 0.75rem;
|
|
2680
|
-
background: var(--color-bg-secondary);
|
|
2681
|
-
border-radius: 0.375rem;
|
|
2682
|
-
font-size: 0.875rem;
|
|
2683
|
-
color: var(--color-text-secondary);
|
|
2684
|
-
`;
|
|
2685
|
-
|
|
2686
|
-
const controls = header.querySelector('.header-controls');
|
|
2687
|
-
if (controls) {
|
|
2688
|
-
controls.insertBefore(indicator, controls.firstChild);
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
indicator.style.display = 'flex';
|
|
2693
|
-
indicator.innerHTML = `
|
|
2694
|
-
<span style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin 1s linear infinite;"></span>
|
|
2695
|
-
<span>${progress.message || 'Loading models...'}</span>
|
|
2696
|
-
`;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
_hideModelDownloadProgress() {
|
|
2700
|
-
const indicator = document.getElementById('modelDownloadIndicator');
|
|
2701
|
-
if (indicator) {
|
|
2702
|
-
indicator.style.display = 'none';
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
_updateVoiceTabState() {
|
|
2707
|
-
const voiceBtn = document.querySelector('[data-view="voice"]');
|
|
2708
|
-
if (!voiceBtn) return;
|
|
2709
|
-
const isReady = this._modelDownloadProgress?.done === true &&
|
|
2710
|
-
this._modelDownloadProgress?.complete === true;
|
|
2711
|
-
voiceBtn.title = isReady ? 'Voice' : 'Voice (click to download models)';
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
2586
|
/**
|
|
2715
2587
|
* Cleanup resources
|
|
2716
2588
|
*/
|