@chatpanel/gateway 0.6.22 → 0.6.24
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 +28 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.24",
|
|
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.24';
|
|
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.
|
|
@@ -698,6 +698,24 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
698
698
|
return sendJson(res, 202, { accepted: true, active: id, state: sttEngine.state(), progress: sttEngine.progress() });
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
|
+
// Speaker (diarization) model manager — the "who said what" x-vector model.
|
|
702
|
+
// GET → status + install state; POST → force-download it (ignores the
|
|
703
|
+
// allowDownload config flag; explicit user action from the Gateway tab).
|
|
704
|
+
if (pathname === '/diarize/model') {
|
|
705
|
+
if (req.method === 'GET') {
|
|
706
|
+
const h = diarizeEngine.health();
|
|
707
|
+
return sendJson(res, 200, {
|
|
708
|
+
active: diarizeEngine.DIARIZE_MODEL,
|
|
709
|
+
state: h.state,
|
|
710
|
+
progress: diarizeEngine.progress(),
|
|
711
|
+
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() }],
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
if (req.method === 'POST') {
|
|
715
|
+
diarizeEngine.download({ onLog: (m) => console.log(m) });
|
|
716
|
+
return sendJson(res, 202, { accepted: true, active: diarizeEngine.DIARIZE_MODEL, state: diarizeEngine.state(), progress: diarizeEngine.progress() });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
701
719
|
if (pathname === '/stt/sessions' && req.method === 'POST') {
|
|
702
720
|
if (cfg.stt?.enabled === false) return sendJson(res, 403, { error: { message: 'STT disabled in gateway config', type: 'stt_disabled' } });
|
|
703
721
|
let body = null;
|
|
@@ -760,9 +778,15 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
760
778
|
send({ type: 'state', state: sttEngine.state() });
|
|
761
779
|
const progressTimer = setInterval(() => {
|
|
762
780
|
const st = sttEngine.state();
|
|
763
|
-
if (st === 'downloading' || st === 'loading') send({ type: 'progress', state: st, ...(sttEngine.progress() || {}) });
|
|
764
|
-
|
|
765
|
-
|
|
781
|
+
if (st === 'downloading' || st === 'loading') { send({ type: 'progress', state: st, ...(sttEngine.progress() || {}) }); return; }
|
|
782
|
+
if (st === 'error') { send({ type: 'error', code: 'model_failed', message: sttEngine.health().error || 'model failed to load', fatal: true }); clearInterval(progressTimer); return; }
|
|
783
|
+
// STT ready — if diarization is on, surface ITS one-time download too, so
|
|
784
|
+
// "who said what" doesn't silently lag while the speaker model fetches.
|
|
785
|
+
if (sess.diarize) {
|
|
786
|
+
const ds = diarizeEngine.state();
|
|
787
|
+
if (ds === 'downloading' || ds === 'loading') { send({ type: 'diarize-progress', state: ds, ...(diarizeEngine.progress() || {}) }); return; }
|
|
788
|
+
}
|
|
789
|
+
send({ type: 'state', state: st }); clearInterval(progressTimer);
|
|
766
790
|
}, 500);
|
|
767
791
|
progressTimer.unref?.();
|
|
768
792
|
// Redaction is async — chain events so finals can't overtake interims.
|