@chatpanel/gateway 0.6.23 → 0.6.25
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/package.json +1 -1
- package/src/diarize-engine.js +21 -4
- package/src/server.js +22 -2
- package/src/stt-engine.js +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.25",
|
|
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/diarize-engine.js
CHANGED
|
@@ -43,6 +43,7 @@ export function modelOnDisk(modelId = DIARIZE_MODEL, dtype = runtimeDtype()) {
|
|
|
43
43
|
catch { return false; }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** @param {{ log?: (m: string) => void, allowDownload?: boolean }} [opts] */
|
|
46
47
|
async function load({ log = () => {}, allowDownload = true } = {}) {
|
|
47
48
|
let lib;
|
|
48
49
|
try { lib = await ensureLib(); }
|
|
@@ -79,12 +80,25 @@ async function load({ log = () => {}, allowDownload = true } = {}) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export function init(cfg = {}) {
|
|
82
|
-
|
|
83
|
+
// Retry after a FAILED attempt (single-flight only while loading/ready) — else a
|
|
84
|
+
// transient failure (e.g. downloads were disabled, or the model wasn't on disk
|
|
85
|
+
// yet) would be cached forever and never self-heal once the model is present.
|
|
86
|
+
if (_initPromise && _state !== 'error') return _initPromise;
|
|
83
87
|
_state = 'loading';
|
|
84
88
|
_initPromise = load({ log: cfg.onLog, allowDownload: cfg.allowDownload !== false });
|
|
85
89
|
return _initPromise;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
// Explicit download (the Gateway-tab "Download" button): force the fetch even if
|
|
93
|
+
// the config disabled auto-downloads, and clear any cached failure so it retries.
|
|
94
|
+
/** @param {{ onLog?: (m: string) => void }} [opts] */
|
|
95
|
+
export function download({ onLog } = {}) {
|
|
96
|
+
_initPromise = null;
|
|
97
|
+
_state = 'loading';
|
|
98
|
+
_initPromise = load({ log: onLog, allowDownload: true });
|
|
99
|
+
return _initPromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
// Embed a mono 16 kHz Float32 segment → Float32Array x-vector, or null if not ready.
|
|
89
103
|
export async function embed(audio) {
|
|
90
104
|
if (!isReady() || !(audio instanceof Float32Array) || !audio.length) return null;
|
|
@@ -108,10 +122,13 @@ function cosine(a, b) {
|
|
|
108
122
|
return denom ? d / denom : 0;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
// A per-session speaker tracker.
|
|
112
|
-
//
|
|
125
|
+
// A per-session speaker tracker. NOT limited to 2 speakers — this is embedding +
|
|
126
|
+
// online clustering, so it scales to N (a 10-person meeting is fine; accuracy just
|
|
127
|
+
// softens as more voices sound alike). `threshold` = min cosine to be the SAME
|
|
128
|
+
// speaker (lower → fewer speakers/more merging, higher → more speakers/more
|
|
129
|
+
// splitting); `maxSpeakers` caps the roster (extra turns fold into the nearest).
|
|
113
130
|
export class Diarizer {
|
|
114
|
-
constructor({ threshold = 0.75, maxSpeakers =
|
|
131
|
+
constructor({ threshold = 0.75, maxSpeakers = 12 } = {}) {
|
|
115
132
|
this.threshold = threshold;
|
|
116
133
|
this.maxSpeakers = maxSpeakers;
|
|
117
134
|
this.centroids = []; // { id, label, vec: Float32Array, n }
|
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.25';
|
|
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.
|
|
@@ -499,7 +499,9 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
499
499
|
const stt = sttEngine.health();
|
|
500
500
|
return sendJson(res, 200, {
|
|
501
501
|
ok: true, version: VERSION, backend: cfg.backend, tier: cfg.redaction.tier,
|
|
502
|
-
|
|
502
|
+
// `runtime` = 'native' (npm, fast quantized) | 'wasm' (binary, slow fp32) —
|
|
503
|
+
// the extension uses it to advise the far-faster native gateway.
|
|
504
|
+
stt: { enabled: cfg.stt?.enabled !== false, state: stt.state, ready: stt.ok, model: stt.model || cfg.stt?.model || DEFAULT_STT_MODEL, runtime: stt.runtime, dtype: stt.dtype },
|
|
503
505
|
});
|
|
504
506
|
}
|
|
505
507
|
|
|
@@ -698,6 +700,24 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
698
700
|
return sendJson(res, 202, { accepted: true, active: id, state: sttEngine.state(), progress: sttEngine.progress() });
|
|
699
701
|
}
|
|
700
702
|
}
|
|
703
|
+
// Speaker (diarization) model manager — the "who said what" x-vector model.
|
|
704
|
+
// GET → status + install state; POST → force-download it (ignores the
|
|
705
|
+
// allowDownload config flag; explicit user action from the Gateway tab).
|
|
706
|
+
if (pathname === '/diarize/model') {
|
|
707
|
+
if (req.method === 'GET') {
|
|
708
|
+
const h = diarizeEngine.health();
|
|
709
|
+
return sendJson(res, 200, {
|
|
710
|
+
active: diarizeEngine.DIARIZE_MODEL,
|
|
711
|
+
state: h.state,
|
|
712
|
+
progress: diarizeEngine.progress(),
|
|
713
|
+
available: [{ id: diarizeEngine.DIARIZE_MODEL, label: 'Speaker embeddings (wavlm)', lang: 'any', approxMB: 100, note: 'Tells voices apart for meeting transcription (who said what).', installed: diarizeEngine.modelOnDisk() }],
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (req.method === 'POST') {
|
|
717
|
+
diarizeEngine.download({ onLog: (m) => console.log(m) });
|
|
718
|
+
return sendJson(res, 202, { accepted: true, active: diarizeEngine.DIARIZE_MODEL, state: diarizeEngine.state(), progress: diarizeEngine.progress() });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
701
721
|
if (pathname === '/stt/sessions' && req.method === 'POST') {
|
|
702
722
|
if (cfg.stt?.enabled === false) return sendJson(res, 403, { error: { message: 'STT disabled in gateway config', type: 'stt_disabled' } });
|
|
703
723
|
let body = null;
|
package/src/stt-engine.js
CHANGED
|
@@ -39,6 +39,14 @@ export function runtimeDtype() {
|
|
|
39
39
|
return globalThis.__CHATPANEL_WASM_PATHS__ ? 'fp32' : 'q8';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Which ONNX runtime is active: the standalone binary uses onnxruntime-web WASM
|
|
43
|
+
// (fp32-only, single-thread — ~10× slower); the npm package uses onnxruntime-node
|
|
44
|
+
// (native, quantized q8). The extension surfaces this so users on the slow WASM
|
|
45
|
+
// build know the native gateway is far faster.
|
|
46
|
+
export function runtimeName() {
|
|
47
|
+
return globalThis.__CHATPANEL_WASM_PATHS__ ? 'wasm' : 'native';
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
let _state = 'off'; // 'off' | 'loading' | 'downloading' | 'ready' | 'error'
|
|
43
51
|
let _model = null; // active model id
|
|
44
52
|
let _pipe = null; // the loaded automatic-speech-recognition pipeline
|
|
@@ -72,7 +80,7 @@ export function isReady() { return _state === 'ready' && !!_pipe; }
|
|
|
72
80
|
export function progress() { return _progress; }
|
|
73
81
|
|
|
74
82
|
export function health() {
|
|
75
|
-
return { configured: _state !== 'off', ok: isReady(), state: _state, model: _model, dtype: _dtype, error: _err };
|
|
83
|
+
return { configured: _state !== 'off', ok: isReady(), state: _state, model: _model, dtype: _dtype, runtime: runtimeName(), error: _err };
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
// (Re)load a model into _pipe. Same contract as ner-engine.loadModel: fail-open,
|