getpatter 0.5.4 → 0.6.0

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.
@@ -1,4 +1,9 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-N565J3CF.mjs";
4
+
1
5
  // src/logger.ts
6
+ init_esm_shims();
2
7
  var defaultLogger = {
3
8
  info: (msg, ...args) => console.info(`[PATTER] ${msg}`, ...args),
4
9
  warn: (msg, ...args) => console.warn(`[PATTER] WARNING: ${msg}`, ...args),
@@ -0,0 +1,69 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __glob = (map) => (path2) => {
14
+ var fn = map[path2];
15
+ if (fn) return fn();
16
+ throw new Error("Module not found in bundle: " + path2);
17
+ };
18
+ var __esm = (fn, res) => function __init() {
19
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
20
+ };
21
+ var __commonJS = (cb, mod) => function __require2() {
22
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
23
+ };
24
+ var __export = (target, all) => {
25
+ for (var name in all)
26
+ __defProp(target, name, { get: all[name], enumerable: true });
27
+ };
28
+ var __copyProps = (to, from, except, desc) => {
29
+ if (from && typeof from === "object" || typeof from === "function") {
30
+ for (let key of __getOwnPropNames(from))
31
+ if (!__hasOwnProp.call(to, key) && key !== except)
32
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
33
+ }
34
+ return to;
35
+ };
36
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
37
+ // If the importer is in node compatibility mode or this is not an ESM
38
+ // file that has been converted to a CommonJS file using a Babel-
39
+ // compatible transform (i.e. "__esModule" has not been set), then set
40
+ // "default" to the CommonJS "module.exports" for node compatibility.
41
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
42
+ mod
43
+ ));
44
+
45
+ // node_modules/tsup/assets/esm_shims.js
46
+ import path from "path";
47
+ import { fileURLToPath } from "url";
48
+ var getFilename, getDirname, __dirname, __filename;
49
+ var init_esm_shims = __esm({
50
+ "node_modules/tsup/assets/esm_shims.js"() {
51
+ "use strict";
52
+ getFilename = () => fileURLToPath(import.meta.url);
53
+ getDirname = () => path.dirname(getFilename());
54
+ __dirname = /* @__PURE__ */ getDirname();
55
+ __filename = /* @__PURE__ */ getFilename();
56
+ }
57
+ });
58
+
59
+ export {
60
+ __require,
61
+ __glob,
62
+ __esm,
63
+ __commonJS,
64
+ __export,
65
+ __toESM,
66
+ __dirname,
67
+ __filename,
68
+ init_esm_shims
69
+ };
@@ -0,0 +1,363 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-N565J3CF.mjs";
4
+
5
+ // src/providers/silero-vad.ts
6
+ init_esm_shims();
7
+ import { createRequire } from "module";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import { fileURLToPath } from "url";
11
+ var SUPPORTED_SAMPLE_RATES = [8e3, 16e3];
12
+ function resolveModuleDirs() {
13
+ const candidates = [];
14
+ try {
15
+ const cjsDir = new Function("return typeof __dirname !== 'undefined' ? __dirname : null")();
16
+ if (typeof cjsDir === "string") candidates.push(cjsDir);
17
+ } catch {
18
+ }
19
+ try {
20
+ const url = import.meta.url;
21
+ if (url) candidates.push(path.dirname(fileURLToPath(url)));
22
+ } catch {
23
+ }
24
+ try {
25
+ const url = import.meta.url;
26
+ if (url) {
27
+ const req = createRequire(url);
28
+ candidates.push(path.dirname(req.resolve("getpatter/package.json")));
29
+ }
30
+ } catch {
31
+ }
32
+ try {
33
+ const req = createRequire(path.join(process.cwd(), "package.json"));
34
+ candidates.push(path.dirname(req.resolve("getpatter/package.json")));
35
+ } catch {
36
+ }
37
+ candidates.push(process.cwd());
38
+ return candidates;
39
+ }
40
+ var MODULE_DIRS = resolveModuleDirs();
41
+ function resolveDefaultModelPath() {
42
+ for (const dir of MODULE_DIRS) {
43
+ const candidates = [
44
+ path.join(dir, "resources", "silero_vad.onnx"),
45
+ path.join(dir, "..", "resources", "silero_vad.onnx"),
46
+ path.join(dir, "dist", "resources", "silero_vad.onnx")
47
+ ];
48
+ for (const c of candidates) if (fs.existsSync(c)) return c;
49
+ }
50
+ return path.join(MODULE_DIRS[0] ?? process.cwd(), "resources", "silero_vad.onnx");
51
+ }
52
+ var DEFAULT_MODEL_PATH = resolveDefaultModelPath();
53
+ function classifyOnnxError(err) {
54
+ const msg = err?.message ?? String(err);
55
+ if (/Cannot find module ['"]?onnxruntime-node['"]?$/m.test(msg)) return "missing";
56
+ if (/onnxruntime_binding\.node|napi-v\d/.test(msg)) return "binding";
57
+ if (/listSupportedBackends|backend_\d/.test(msg)) return "api-drift";
58
+ return "unknown";
59
+ }
60
+ async function loadOnnxRuntime() {
61
+ let firstErr;
62
+ try {
63
+ const mod = await import("./dist-RYMPCILF.mjs");
64
+ return mod;
65
+ } catch (e) {
66
+ firstErr = e;
67
+ }
68
+ try {
69
+ const req = createRequire(path.join(process.cwd(), "package.json"));
70
+ return req("onnxruntime-node");
71
+ } catch (secondErr) {
72
+ const importClass = classifyOnnxError(firstErr);
73
+ const requireClass = classifyOnnxError(secondErr);
74
+ const original = firstErr?.message ?? String(firstErr);
75
+ const detail = secondErr?.message ?? String(secondErr);
76
+ let header;
77
+ let remedy;
78
+ if (importClass === "missing" && requireClass === "missing") {
79
+ header = 'SileroVAD requires the "onnxruntime-node" package \u2014 it is not installed.';
80
+ remedy = " Install: npm install onnxruntime-node@~1.18.0\n\n (~210 MB. Only needed when you actually use SileroVAD in pipeline mode.)";
81
+ } else if (importClass === "api-drift" || requireClass === "api-drift") {
82
+ header = "SileroVAD found onnxruntime-node but the installed version uses an API the SDK does not support.";
83
+ remedy = " Patter is currently tested against onnxruntime-node 1.18.x.\n\n Fix: npm install onnxruntime-node@~1.18.0\n\n Versions 1.24+ removed `listSupportedBackends` from the public surface \u2014 track\n https://github.com/PatterAI/Patter/issues for the SDK update that targets 1.24.";
84
+ } else if (importClass === "binding" || requireClass === "binding") {
85
+ header = "SileroVAD found onnxruntime-node but the native binding for this platform is missing.";
86
+ remedy = " Common cause on macOS x86_64: the prebuilt bin/ layout drifted between releases.\n\n Fix: npm install onnxruntime-node@~1.18.0\n\n Or rebuild from source: npm rebuild onnxruntime-node";
87
+ } else {
88
+ header = 'SileroVAD requires the "onnxruntime-node" package, which could not be resolved.';
89
+ remedy = " Install: npm install onnxruntime-node@~1.18.0\n\n This is an optional peer dependency of getpatter (~210 MB).";
90
+ }
91
+ const err = new Error(
92
+ `
93
+ ${header}
94
+
95
+ ${remedy}
96
+
97
+ import() failed: ${original}
98
+ cwd-require failed: ${detail}
99
+ `
100
+ );
101
+ err.cause = secondErr ?? firstErr;
102
+ throw err;
103
+ }
104
+ }
105
+ var ExpFilter = class {
106
+ constructor(alpha) {
107
+ this.alpha = alpha;
108
+ if (!(alpha > 0 && alpha <= 1)) {
109
+ throw new Error("alpha must be in (0, 1].");
110
+ }
111
+ }
112
+ alpha;
113
+ filtered = null;
114
+ apply(exp, sample) {
115
+ if (this.filtered === null) {
116
+ this.filtered = sample;
117
+ } else {
118
+ const a = Math.pow(this.alpha, exp);
119
+ this.filtered = a * this.filtered + (1 - a) * sample;
120
+ }
121
+ return this.filtered;
122
+ }
123
+ reset() {
124
+ this.filtered = null;
125
+ }
126
+ };
127
+ var OnnxModel = class {
128
+ constructor(runtime, session, sampleRate) {
129
+ this.runtime = runtime;
130
+ this.session = session;
131
+ if (!SUPPORTED_SAMPLE_RATES.includes(sampleRate)) {
132
+ throw new Error("Silero VAD only supports 8KHz and 16KHz sample rates");
133
+ }
134
+ this.sampleRate = sampleRate;
135
+ this.windowSizeSamples = sampleRate === 8e3 ? 256 : 512;
136
+ this.contextSize = sampleRate === 8e3 ? 32 : 64;
137
+ this.context = new Float32Array(this.contextSize);
138
+ this.rnnState = new Float32Array(2 * 1 * 128);
139
+ this.inputBuffer = new Float32Array(this.contextSize + this.windowSizeSamples);
140
+ this.sampleRateTensor = BigInt64Array.from([BigInt(sampleRate)]);
141
+ }
142
+ runtime;
143
+ session;
144
+ sampleRate;
145
+ windowSizeSamples;
146
+ contextSize;
147
+ context;
148
+ rnnState;
149
+ inputBuffer;
150
+ sampleRateTensor;
151
+ async run(window) {
152
+ if (window.length !== this.windowSizeSamples) {
153
+ throw new Error(
154
+ `window must have exactly ${this.windowSizeSamples} samples, got ${window.length}`
155
+ );
156
+ }
157
+ this.inputBuffer.set(this.context, 0);
158
+ this.inputBuffer.set(window, this.contextSize);
159
+ const { Tensor } = this.runtime;
160
+ const feeds = {
161
+ input: new Tensor("float32", this.inputBuffer, [1, this.inputBuffer.length]),
162
+ state: new Tensor("float32", this.rnnState, [2, 1, 128]),
163
+ sr: new Tensor("int64", this.sampleRateTensor, [])
164
+ };
165
+ const results = await this.session.run(feeds);
166
+ const outputKey = Object.keys(results).find((k) => k !== "stateN") ?? "output";
167
+ const stateKey = "stateN" in results ? "stateN" : Object.keys(results).find((k) => k !== outputKey);
168
+ const out = results[outputKey];
169
+ const newState = stateKey ? results[stateKey] : void 0;
170
+ if (newState && newState.data instanceof Float32Array) {
171
+ this.rnnState = Float32Array.from(newState.data);
172
+ }
173
+ this.context = this.inputBuffer.slice(-this.contextSize);
174
+ const data = out.data;
175
+ return data[0] ?? 0;
176
+ }
177
+ };
178
+ var SileroVAD = class _SileroVAD {
179
+ constructor(model, opts) {
180
+ this.model = model;
181
+ this.opts = opts;
182
+ }
183
+ model;
184
+ opts;
185
+ pending = new Float32Array(0);
186
+ expFilter = new ExpFilter(0.35);
187
+ pubSpeaking = false;
188
+ speechThresholdDuration = 0;
189
+ silenceThresholdDuration = 0;
190
+ closed = false;
191
+ /**
192
+ * Load the Silero VAD model.
193
+ * Throws if `onnxruntime-node` is not installed.
194
+ */
195
+ static async load(options = {}) {
196
+ const sampleRate = options.sampleRate ?? 16e3;
197
+ if (!SUPPORTED_SAMPLE_RATES.includes(sampleRate)) {
198
+ throw new Error("Silero VAD only supports 8KHz and 16KHz sample rates");
199
+ }
200
+ const activationThreshold = options.activationThreshold ?? 0.5;
201
+ const deactivationThreshold = options.deactivationThreshold ?? Math.max(activationThreshold - 0.15, 0.01);
202
+ if (deactivationThreshold <= 0) {
203
+ throw new Error("deactivationThreshold must be greater than 0");
204
+ }
205
+ const runtime = await loadOnnxRuntime();
206
+ const modelPath = options.onnxFilePath ?? DEFAULT_MODEL_PATH;
207
+ const session = await runtime.InferenceSession.create(modelPath, {
208
+ interOpNumThreads: 1,
209
+ intraOpNumThreads: 1,
210
+ executionMode: "sequential",
211
+ executionProviders: options.forceCpu === false ? void 0 : ["cpu"]
212
+ });
213
+ const model = new OnnxModel(runtime, session, sampleRate);
214
+ return new _SileroVAD(model, {
215
+ minSpeechDuration: options.minSpeechDuration ?? 0.25,
216
+ minSilenceDuration: options.minSilenceDuration ?? 0.1,
217
+ prefixPaddingDuration: options.prefixPaddingDuration ?? 0.03,
218
+ activationThreshold,
219
+ deactivationThreshold,
220
+ sampleRate
221
+ });
222
+ }
223
+ /**
224
+ * Convenience factory for telephony pipelines.
225
+ *
226
+ * Identical to {@link SileroVAD.load} but pins `sampleRate` to 16000 Hz
227
+ * — the only sample rate Patter's pipeline-mode audio bus uses (8 kHz
228
+ * mulaw from Twilio is upsampled to 16 kHz PCM before reaching the
229
+ * VAD). Every other parameter mirrors the upstream Silero VAD
230
+ * defaults from `snakers4/silero-vad` (`get_speech_timestamps` /
231
+ * `VADIterator`):
232
+ *
233
+ * - `activationThreshold = 0.5` — upstream `threshold`
234
+ * - `deactivationThreshold = 0.35` — upstream `neg_threshold = threshold - 0.15`
235
+ * - `minSpeechDuration = 0.25` — upstream `min_speech_duration_ms = 250`
236
+ * - `minSilenceDuration = 0.1` — upstream `min_silence_duration_ms = 100`
237
+ * - `prefixPaddingDuration = 0.03` — upstream `speech_pad_ms = 30`
238
+ *
239
+ * Override any field by passing `options`. Deployments that experience
240
+ * truncation on natural pauses can raise `minSilenceDuration` (e.g.
241
+ * 0.5–1.0 s) per call site rather than as a global default.
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * const vad = await SileroVAD.forPhoneCall();
246
+ * // or, if natural-pause truncation is observed:
247
+ * const vad = await SileroVAD.forPhoneCall({ minSilenceDuration: 0.5 });
248
+ * ```
249
+ */
250
+ static forPhoneCall(options = {}) {
251
+ return _SileroVAD.load({
252
+ sampleRate: 16e3,
253
+ ...options
254
+ });
255
+ }
256
+ /**
257
+ * Internal factory used by tests — bypasses onnxruntime-node loading.
258
+ * @internal
259
+ */
260
+ static fromOnnxModel(runtime, session, options) {
261
+ const model = new OnnxModel(runtime, session, options.sampleRate);
262
+ return new _SileroVAD(model, options);
263
+ }
264
+ /** Sample rate (Hz) the underlying ONNX model was loaded with. */
265
+ get sampleRate() {
266
+ return this.opts.sampleRate;
267
+ }
268
+ /**
269
+ * Number of int16 PCM samples that must be provided per call to
270
+ * processFrame for the model to run one inference window.
271
+ *
272
+ * Constraint (Silero ONNX spec):
273
+ * - 16 000 Hz → 512 samples (32 ms)
274
+ * - 8 000 Hz → 256 samples (32 ms)
275
+ *
276
+ * Callers that feed raw audio in fixed-size chunks (e.g. WebSocket frames)
277
+ * should buffer incoming audio until at least numFramesRequired() int16
278
+ * samples are available before calling processFrame. The provider
279
+ * internally buffers partial windows so smaller chunks are also safe, but
280
+ * passing exactly one window per call minimises heap allocation.
281
+ */
282
+ numFramesRequired() {
283
+ return this.opts.sampleRate === 8e3 ? 256 : 512;
284
+ }
285
+ /** Run VAD on a PCM16 chunk; returns a transition event or null if no change. */
286
+ async processFrame(pcmChunk, sampleRate) {
287
+ if (this.closed) {
288
+ throw new Error("SileroVAD is closed");
289
+ }
290
+ if (sampleRate !== this.opts.sampleRate) {
291
+ throw new Error(
292
+ `input sampleRate ${sampleRate} does not match model sampleRate ${this.opts.sampleRate}; resampling is not implemented in the Patter port`
293
+ );
294
+ }
295
+ if (pcmChunk.length === 0) {
296
+ return null;
297
+ }
298
+ const numSamples = Math.floor(pcmChunk.length / 2);
299
+ if (numSamples === 0) {
300
+ return null;
301
+ }
302
+ const samples = new Float32Array(numSamples);
303
+ for (let i = 0; i < numSamples; i++) {
304
+ samples[i] = pcmChunk.readInt16LE(i * 2) / 32767;
305
+ }
306
+ const merged = new Float32Array(this.pending.length + samples.length);
307
+ merged.set(this.pending, 0);
308
+ merged.set(samples, this.pending.length);
309
+ this.pending = merged;
310
+ const windowSize = this.model.windowSizeSamples;
311
+ let event = null;
312
+ while (this.pending.length >= windowSize) {
313
+ const window = this.pending.slice(0, windowSize);
314
+ this.pending = this.pending.slice(windowSize);
315
+ const rawP = await this.model.run(window);
316
+ const p = this.expFilter.apply(1, rawP);
317
+ const windowDuration = windowSize / this.opts.sampleRate;
318
+ const transition = this.advanceState(p, windowDuration);
319
+ if (transition !== null) {
320
+ event = transition;
321
+ }
322
+ }
323
+ return event;
324
+ }
325
+ advanceState(p, windowDuration) {
326
+ const opts = this.opts;
327
+ if (p >= opts.activationThreshold || this.pubSpeaking && p > opts.deactivationThreshold) {
328
+ this.speechThresholdDuration += windowDuration;
329
+ this.silenceThresholdDuration = 0;
330
+ if (!this.pubSpeaking) {
331
+ if (this.speechThresholdDuration >= opts.minSpeechDuration) {
332
+ this.pubSpeaking = true;
333
+ return {
334
+ type: "speech_start",
335
+ confidence: p,
336
+ durationMs: this.speechThresholdDuration * 1e3
337
+ };
338
+ }
339
+ }
340
+ } else {
341
+ this.silenceThresholdDuration += windowDuration;
342
+ this.speechThresholdDuration = 0;
343
+ if (this.pubSpeaking && this.silenceThresholdDuration >= opts.minSilenceDuration) {
344
+ this.pubSpeaking = false;
345
+ return {
346
+ type: "speech_end",
347
+ confidence: p,
348
+ durationMs: this.silenceThresholdDuration * 1e3
349
+ };
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ /** Mark the VAD as closed; subsequent processFrame calls throw. */
355
+ async close() {
356
+ if (this.closed) return;
357
+ this.closed = true;
358
+ }
359
+ };
360
+
361
+ export {
362
+ SileroVAD
363
+ };
@@ -1,8 +1,12 @@
1
1
  import {
2
2
  getLogger
3
- } from "./chunk-VJVDG4V5.mjs";
3
+ } from "./chunk-MVOQFAEO.mjs";
4
+ import {
5
+ init_esm_shims
6
+ } from "./chunk-N565J3CF.mjs";
4
7
 
5
8
  // src/tunnel.ts
9
+ init_esm_shims();
6
10
  var log = getLogger();
7
11
  async function startTunnel(port, timeoutMs = 3e4) {
8
12
  let tunnelMod;