@chatpanel/gateway 0.6.25 → 0.6.26
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/README.md +9 -0
- package/package.json +1 -1
- package/src/ner-engine.js +21 -12
- package/src/server.js +1 -1
- package/src/stt-engine.js +22 -12
package/README.md
CHANGED
|
@@ -54,6 +54,15 @@ chatpanel-gateway
|
|
|
54
54
|
# backend : bridge (agent: codex, via http://127.0.0.1:4319)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
> **Binary vs. npm — same features, very different local-AI speed.** Both run
|
|
58
|
+
> identical redaction/routing. But the standalone binary runs the local models
|
|
59
|
+
> (speech-to-text, diarization, NER) on the **WASM** runtime — **fp32-only,
|
|
60
|
+
> single-threaded, slow**. The **npm** install uses the **native** runtime with
|
|
61
|
+
> **quantized (q8)** models — in our tests **~10× faster** speech-to-text
|
|
62
|
+
> (real-time even on larger, more accurate models). If you'll use voice/meeting
|
|
63
|
+
> features, install via **npm**. Don't run both — they can shadow each other on
|
|
64
|
+
> `PATH`; check `GET /health` → `stt.runtime` (`native` vs `wasm`).
|
|
65
|
+
|
|
57
66
|
Then point your front-end agent at it. **opencode** (`opencode.json`):
|
|
58
67
|
|
|
59
68
|
```jsonc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
4
4
|
"description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/ner-engine.js
CHANGED
|
@@ -179,6 +179,7 @@ export async function ensureLib() {
|
|
|
179
179
|
// (Re)load a specific model into _pipe. Downloads it first if missing (and allowed).
|
|
180
180
|
// Reusable by init() and setModel(). Fail-open: on error, state='error', _pipe stays
|
|
181
181
|
// whatever it was (so a failed SWITCH doesn't kill a working detector).
|
|
182
|
+
/** @param {string} modelId @param {{ log?: (m: string) => void, allowDownload?: boolean }} [opts] */
|
|
182
183
|
async function loadModel(modelId, { log = () => {}, allowDownload = true } = {}) {
|
|
183
184
|
const prevPipe = _pipe;
|
|
184
185
|
const prevModel = _model;
|
|
@@ -211,18 +212,26 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
|
|
|
211
212
|
if (!haveLocal) { _progress = { model: modelId, file: null, pct: 0 }; log(`[ner] downloading model ${modelId} (one-time${isCustom ? ', from Hugging Face' : ''})…`); }
|
|
212
213
|
|
|
213
214
|
try {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
215
|
+
const progress_callback = (p) => {
|
|
216
|
+
if (!p) return;
|
|
217
|
+
const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
|
|
218
|
+
if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
|
|
219
|
+
_progress = { model: modelId, file: p.file || _progress?.file || null, pct };
|
|
220
|
+
} else if (p.status === 'done' && p.file) {
|
|
221
|
+
log(`[ner] fetched ${p.file}`);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
let pipe;
|
|
225
|
+
try {
|
|
226
|
+
pipe = await lib.pipeline('token-classification', modelId, { dtype: 'q8', progress_callback });
|
|
227
|
+
} catch (e) {
|
|
228
|
+
// Mirror gap → fall back to Hugging Face (same as stt-engine).
|
|
229
|
+
if (haveLocal || isCustom || !allowDownload) throw e;
|
|
230
|
+
log(`[ner] mirror fetch failed (${String(e.message).slice(0, 80)}) — retrying from Hugging Face…`);
|
|
231
|
+
lib.env.remoteHost = 'https://huggingface.co/';
|
|
232
|
+
lib.env.allowRemoteModels = true;
|
|
233
|
+
pipe = await lib.pipeline('token-classification', modelId, { dtype: 'q8', progress_callback });
|
|
234
|
+
}
|
|
226
235
|
// Swap in the new pipeline, dispose the old one (free its WASM/native session).
|
|
227
236
|
_pipe = pipe; _model = modelId; _state = 'ready'; _err = null; _progress = null;
|
|
228
237
|
if (prevPipe && prevPipe !== pipe) { try { await prevPipe.dispose?.(); } catch { /* ignore */ } }
|
package/src/server.js
CHANGED
|
@@ -43,7 +43,7 @@ import * as openai from './openai.js';
|
|
|
43
43
|
import * as responses from './responses.js';
|
|
44
44
|
import * as anthropic from './anthropic.js';
|
|
45
45
|
|
|
46
|
-
export const VERSION = '0.6.
|
|
46
|
+
export const VERSION = '0.6.26';
|
|
47
47
|
|
|
48
48
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
49
49
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
package/src/stt-engine.js
CHANGED
|
@@ -119,18 +119,28 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
|
|
|
119
119
|
try {
|
|
120
120
|
// Per-model override wins (catalog `dtype`), else the runtime-appropriate default.
|
|
121
121
|
const dtype = sttModelDtype(modelId) || runtimeDtype();
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
const progress_callback = (p) => {
|
|
123
|
+
if (!p) return;
|
|
124
|
+
const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
|
|
125
|
+
if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
|
|
126
|
+
_progress = { model: modelId, file: p.file || _progress?.file || null, pct };
|
|
127
|
+
} else if (p.status === 'done' && p.file) {
|
|
128
|
+
log(`[stt] fetched ${p.file}`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
let pipe;
|
|
132
|
+
try {
|
|
133
|
+
pipe = await lib.pipeline('automatic-speech-recognition', modelId, { dtype, progress_callback });
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// The dl.chatpanel.net mirror only proxies an allowlist; a catalog model
|
|
136
|
+
// missing from it (or any mirror hiccup) 403s. Fall back to Hugging Face so
|
|
137
|
+
// a download is never blocked by a mirror gap. (Custom ids already use HF.)
|
|
138
|
+
if (haveLocal || isCustom || !allowDownload) throw e;
|
|
139
|
+
log(`[stt] mirror fetch failed (${String(e.message).slice(0, 80)}) — retrying from Hugging Face…`);
|
|
140
|
+
lib.env.remoteHost = 'https://huggingface.co/';
|
|
141
|
+
lib.env.allowRemoteModels = true;
|
|
142
|
+
pipe = await lib.pipeline('automatic-speech-recognition', modelId, { dtype, progress_callback });
|
|
143
|
+
}
|
|
134
144
|
_pipe = pipe; _model = modelId; _state = 'ready'; _err = null; _progress = null; _dtype = dtype;
|
|
135
145
|
if (prevPipe && prevPipe !== pipe) { try { await prevPipe.dispose?.(); } catch { /* ignore */ } }
|
|
136
146
|
log(`[stt] ready — model ${modelId} @ ${dtype} (in-process, offline) — local dictation active`);
|