@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.22",
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": {
@@ -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
- if (_initPromise) return _initPromise;
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. `threshold` = min cosine to be the SAME speaker;
112
- // lower fewer speakers (merges more), higher more speakers (splits more).
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 = 8 } = {}) {
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.22';
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
- else if (st === 'error') { send({ type: 'error', code: 'model_failed', message: sttEngine.health().error || 'model failed to load', fatal: true }); clearInterval(progressTimer); }
765
- else { send({ type: 'state', state: st }); clearInterval(progressTimer); }
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.