@chatpanel/gateway 0.6.58 → 0.6.59

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.58",
3
+ "version": "0.6.59",
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/server.js CHANGED
@@ -54,7 +54,7 @@ import * as openai from './openai.js';
54
54
  import * as responses from './responses.js';
55
55
  import * as anthropic from './anthropic.js';
56
56
 
57
- export const VERSION = '0.6.58';
57
+ export const VERSION = '0.6.59';
58
58
 
59
59
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
60
60
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
@@ -1389,7 +1389,9 @@ export function createGateway(cfg = loadConfig()) {
1389
1389
  // same composable model as everything else: any stage, with or without).
1390
1390
  // `diarize: true` (+ optional `speakerLabel` to pin the mic channel to a
1391
1391
  // name) attaches a speaker to each final.
1392
- const { id } = sttEngine.createSession({ lang: body?.lang, redact: body?.redact === true, diarize: wantDiarize, speakerLabel: body?.speakerLabel });
1392
+ // `endSilenceMs` (additive) lets a voice conversation wait longer for a
1393
+ // sentence to finish than dictation into a text box needs to.
1394
+ const { id } = sttEngine.createSession({ lang: body?.lang, redact: body?.redact === true, diarize: wantDiarize, speakerLabel: body?.speakerLabel, endSilenceMs: body?.endSilenceMs });
1393
1395
  return sendJson(res, 201, { id, state: sttEngine.state() });
1394
1396
  } catch (e) {
1395
1397
  return sendJson(res, e.code === 'too_many_sessions' ? 429 : 500, { error: { message: e.message, type: e.code || 'stt_error' } });
package/src/stt-engine.js CHANGED
@@ -238,13 +238,26 @@ let _decodeChain = Promise.resolve(); // whisper is effectively single-threaded
238
238
  export function sessionCount() { return _sessions.size; }
239
239
 
240
240
  /** @param {{ lang?: string, redact?: boolean, diarize?: boolean, speakerLabel?: string }} [opts] */
241
- export function createSession({ lang, redact = false, diarize: diarizeOpt = false, speakerLabel = null } = {}) {
241
+ // How long a pause commits a segment. The default suits dictation into a text box,
242
+ // where a final just appends and a short pause costs nothing. In a VOICE
243
+ // conversation every final is SENT as a question, so a 700ms pause mid-thought
244
+ // ("I want to… um… go to Google") sends half a sentence. Callers may ask for more.
245
+ const END_SILENCE_MIN_MS = 300;
246
+ const END_SILENCE_MAX_MS = 3000;
247
+ export function clampEndSilence(ms) {
248
+ const n = Number(ms);
249
+ if (!Number.isFinite(n)) return SILENCE_FINAL_MS;
250
+ return Math.min(END_SILENCE_MAX_MS, Math.max(END_SILENCE_MIN_MS, Math.round(n)));
251
+ }
252
+
253
+ export function createSession({ lang, redact = false, diarize: diarizeOpt = false, speakerLabel = null, endSilenceMs = SILENCE_FINAL_MS } = {}) {
242
254
  if (_sessions.size >= MAX_SESSIONS) {
243
255
  const e = /** @type {Error & { code?: string }} */ (new Error('too many concurrent dictation sessions'));
244
256
  e.code = 'too_many_sessions'; throw e;
245
257
  }
246
258
  const s = {
247
259
  id: randomUUID(),
260
+ endSilenceMs: clampEndSilence(endSilenceMs),
248
261
  lang: typeof lang === 'string' && lang ? lang.slice(0, 12) : null,
249
262
  langTried: false, // language auto-detect runs once per session (multilingual models)
250
263
  // Opaque to this engine: the server applies the redaction hop to finals when
@@ -374,7 +387,7 @@ async function decodeSession(s, { flush = false } = {}) {
374
387
  s.lastDecodeAt = Date.now();
375
388
 
376
389
  const audio = concatBuffer(s);
377
- const tail = Math.round((SILENCE_FINAL_MS / 1000) * SAMPLE_RATE);
390
+ const tail = Math.round(((s.endSilenceMs || SILENCE_FINAL_MS) / 1000) * SAMPLE_RATE);
378
391
  const trailingQuiet = audio.length > tail && rms(audio, audio.length - tail) < SILENCE_RMS;
379
392
 
380
393
  // Nothing but room tone? Don't decode (whisper hallucinates on silence) and