@pipedog/core 0.1.0 → 0.2.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.
Files changed (63) hide show
  1. package/dist/audio/resamplers/engine.d.ts +60 -0
  2. package/dist/audio/resamplers/engine.js +89 -0
  3. package/dist/audio/resamplers/sinc_engine.d.ts +10 -0
  4. package/dist/audio/resamplers/sinc_engine.js +235 -0
  5. package/dist/audio/resamplers/soxr_resampler.d.ts +10 -6
  6. package/dist/audio/resamplers/soxr_resampler.js +17 -7
  7. package/dist/audio/resamplers/soxr_stream_resampler.d.ts +9 -6
  8. package/dist/audio/resamplers/soxr_stream_resampler.js +24 -11
  9. package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +16 -8
  10. package/dist/audio/vad/vad_analyzer.d.ts +11 -3
  11. package/dist/audio/vad/vad_analyzer.js +72 -34
  12. package/dist/pipeline/worker.d.ts +7 -0
  13. package/dist/pipeline/worker.js +40 -23
  14. package/dist/utils/file_storage.d.ts +37 -21
  15. package/dist/utils/file_storage.js +87 -9
  16. package/dist/utils/frame_queue.js +2 -2
  17. package/dist/utils/image.d.ts +0 -1
  18. package/dist/utils/image.js +37 -4
  19. package/dist/utils/jpeg_encoder.d.ts +14 -0
  20. package/dist/utils/jpeg_encoder.js +315 -0
  21. package/dist/utils/text/transforms/acronyms.d.ts +21 -0
  22. package/dist/utils/text/transforms/acronyms.js +31 -0
  23. package/dist/utils/text/transforms/currency.d.ts +16 -0
  24. package/dist/utils/text/transforms/currency.js +56 -0
  25. package/dist/utils/text/transforms/dates.d.ts +18 -0
  26. package/dist/utils/text/transforms/dates.js +102 -0
  27. package/dist/utils/text/transforms/email.d.ts +17 -0
  28. package/dist/utils/text/transforms/email.js +35 -0
  29. package/dist/utils/text/transforms/index.d.ts +37 -0
  30. package/dist/utils/text/transforms/index.js +42 -0
  31. package/dist/utils/text/transforms/numbers.d.ts +25 -0
  32. package/dist/utils/text/transforms/numbers.js +59 -0
  33. package/dist/utils/text/transforms/percentages.d.ts +16 -0
  34. package/dist/utils/text/transforms/percentages.js +32 -0
  35. package/dist/utils/text/transforms/phone.d.ts +21 -0
  36. package/dist/utils/text/transforms/phone.js +39 -0
  37. package/dist/utils/text/transforms/pronunciations.js +2 -1
  38. package/dist/utils/text/transforms/replacements.d.ts +39 -0
  39. package/dist/utils/text/transforms/replacements.js +46 -0
  40. package/dist/utils/text/transforms/strip_markdown.d.ts +25 -0
  41. package/dist/utils/text/transforms/strip_markdown.js +60 -0
  42. package/dist/utils/text/transforms/units.d.ts +22 -0
  43. package/dist/utils/text/transforms/units.js +81 -0
  44. package/dist/utils/text/transforms/voice_formatter.d.ts +86 -0
  45. package/dist/utils/text/transforms/voice_formatter.js +98 -0
  46. package/dist/utils/text/unicode_digits.d.ts +3 -0
  47. package/dist/utils/text/unicode_digits.js +34 -0
  48. package/dist/utils/tracing/service_decorators.js +2 -2
  49. package/dist/utils/tracing/setup.d.ts +18 -6
  50. package/dist/utils/tracing/setup.js +63 -13
  51. package/dist/utils/tracing/turn_trace_observer.d.ts +5 -0
  52. package/dist/utils/tracing/turn_trace_observer.js +18 -4
  53. package/dist/utils/yaml.d.ts +151 -36
  54. package/dist/utils/yaml.js +527 -12
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/dist/workers/proxy/index.d.ts +3 -0
  58. package/dist/workers/proxy/index.js +9 -0
  59. package/dist/workers/proxy/websocket/client.d.ts +110 -0
  60. package/dist/workers/proxy/websocket/client.js +196 -0
  61. package/dist/workers/proxy/websocket/index.d.ts +3 -0
  62. package/dist/workers/proxy/websocket/index.js +10 -0
  63. package/package.json +18 -3
@@ -0,0 +1,60 @@
1
+ import type { SoxrQuality } from "./base_audio_resampler.ts";
2
+ /** Options of a streaming resampler (as `soxr.ResampleStream(...)`). */
3
+ export interface ResamplerStreamOptions {
4
+ inRate: number;
5
+ outRate: number;
6
+ numChannels: number;
7
+ quality: SoxrQuality;
8
+ }
9
+ /** A stateful streaming resampler; stands in for `soxr.ResampleStream`. */
10
+ export interface ResamplerStream {
11
+ /**
12
+ * Resample a chunk of interleaved samples, keeping the filter history for
13
+ * the next chunk. With `last` the audio held in the filter is appended and
14
+ * the stream starts over.
15
+ */
16
+ resampleChunk<T extends Int16Array | Float32Array>(chunk: T, last?: boolean): T;
17
+ /** Discard the filter history (`soxr.ResampleStream.clear()`). */
18
+ clear(): void;
19
+ /** Free any native/WASM state. Optional. */
20
+ destroy?(): void;
21
+ }
22
+ /**
23
+ * A resampling engine. Structurally identical to what
24
+ * `createSoxrCompatibleEngine()` from `@pipedog/resampler-wasm` returns.
25
+ */
26
+ export interface ResamplerEngine {
27
+ /** Stands in for `soxr.resample(audio, in_rate, out_rate, quality=...)` (mono). */
28
+ resample<T extends Int16Array | Float32Array>(audio: T, inRate: number, outRate: number, quality: SoxrQuality): T;
29
+ /** Stands in for `soxr.ResampleStream(in_rate=, out_rate=, num_channels=, quality=)`. */
30
+ createStream(options: ResamplerStreamOptions): ResamplerStream;
31
+ }
32
+ type EngineLoader = () => Promise<ResamplerEngine>;
33
+ /**
34
+ * Install the engine every SOXR resampler uses from now on (streams already
35
+ * created keep theirs). Pass `null` to go back to the default
36
+ * (`@pipedog/resampler-wasm`, with the pure-TypeScript fallback).
37
+ */
38
+ export declare function setResamplerEngine(engine: ResamplerEngine | null): void;
39
+ /**
40
+ * The engine in use, loading the default one on first call.
41
+ *
42
+ * Resolves to the injected engine if there is one, else the WASM engine, else
43
+ * (after one warning) the pure-TypeScript fallback. Never rejects.
44
+ */
45
+ export declare function getResamplerEngine(): Promise<ResamplerEngine>;
46
+ /**
47
+ * The engine in use, without loading anything: the injected engine, else the
48
+ * default one if it has finished loading, else `null`. Lets synchronous code
49
+ * (e.g. `soxrResample`) use the engine once `getResamplerEngine()` resolved.
50
+ */
51
+ export declare function getLoadedResamplerEngine(): ResamplerEngine | null;
52
+ /**
53
+ * Replace how the default engine is loaded and forget the loaded one. For
54
+ * tests (e.g. simulating a WASM load failure).
55
+ *
56
+ * @internal
57
+ */
58
+ export declare function _resetResamplerEngineForTests(loader?: EngineLoader): void;
59
+ export {};
60
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1,89 @@
1
+ // Copyright (c) 2026, pipedog contributors
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * The resampling engine behind `SOXRAudioResampler` and
5
+ * `SOXRStreamAudioResampler`.
6
+ *
7
+ * Upstream calls python-soxr (`soxr.resample()` and `soxr.ResampleStream`).
8
+ * Here those calls go through a small engine interface, so the actual
9
+ * implementation can be swapped:
10
+ *
11
+ * - By default the engine is `@pipedog/resampler-wasm` (rubato windowed sinc
12
+ * compiled to WebAssembly), loaded lazily on first use. On Cloudflare
13
+ * Workers the package's "workerd" export condition imports the `.wasm` as a
14
+ * module (workerd forbids compiling WASM from bytes); elsewhere it is
15
+ * fetched/read next to the package and instantiated asynchronously.
16
+ * - If the WASM engine cannot be loaded, core logs one warning and falls back
17
+ * to a pure-TypeScript windowed-sinc engine (`sinc_engine.ts`).
18
+ * - Applications can inject their own engine (for example a native libsoxr
19
+ * binding) with `setResamplerEngine()`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { setResamplerEngine } from "@pipedog/core/audio/resamplers/engine";
24
+ * setResamplerEngine(myNativeSoxrEngine);
25
+ * ```
26
+ */
27
+ import { logger } from "../../utils/logger.js";
28
+ import { createSincResamplerEngine } from "./sinc_engine.js";
29
+ /** Load `@pipedog/resampler-wasm` and wrap it in the soxr-shaped interface. */
30
+ async function loadWasmEngine() {
31
+ const wasm = await import("@pipedog/resampler-wasm");
32
+ return wasm.createSoxrCompatibleEngine(await wasm.initResampler());
33
+ }
34
+ let injected = null;
35
+ let loaded = null;
36
+ let loading = null;
37
+ let defaultLoader = loadWasmEngine;
38
+ /**
39
+ * Install the engine every SOXR resampler uses from now on (streams already
40
+ * created keep theirs). Pass `null` to go back to the default
41
+ * (`@pipedog/resampler-wasm`, with the pure-TypeScript fallback).
42
+ */
43
+ export function setResamplerEngine(engine) {
44
+ injected = engine;
45
+ }
46
+ /**
47
+ * The engine in use, loading the default one on first call.
48
+ *
49
+ * Resolves to the injected engine if there is one, else the WASM engine, else
50
+ * (after one warning) the pure-TypeScript fallback. Never rejects.
51
+ */
52
+ export async function getResamplerEngine() {
53
+ if (injected)
54
+ return injected;
55
+ if (loaded)
56
+ return loaded;
57
+ loading ??= defaultLoader().then((engine) => {
58
+ loaded = engine;
59
+ return engine;
60
+ }, (e) => {
61
+ const reason = e instanceof Error ? e.message : String(e);
62
+ logger.warning(`Could not load the WASM resampler (@pipedog/resampler-wasm): ${reason}. ` +
63
+ "Falling back to the pure-TypeScript windowed-sinc resampler (lower quality, slower).");
64
+ loaded = createSincResamplerEngine();
65
+ return loaded;
66
+ });
67
+ return injected ?? (await loading);
68
+ }
69
+ /**
70
+ * The engine in use, without loading anything: the injected engine, else the
71
+ * default one if it has finished loading, else `null`. Lets synchronous code
72
+ * (e.g. `soxrResample`) use the engine once `getResamplerEngine()` resolved.
73
+ */
74
+ export function getLoadedResamplerEngine() {
75
+ return injected ?? loaded;
76
+ }
77
+ /**
78
+ * Replace how the default engine is loaded and forget the loaded one. For
79
+ * tests (e.g. simulating a WASM load failure).
80
+ *
81
+ * @internal
82
+ */
83
+ export function _resetResamplerEngineForTests(loader = loadWasmEngine) {
84
+ injected = null;
85
+ loaded = null;
86
+ loading = null;
87
+ defaultLoader = loader;
88
+ }
89
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,10 @@
1
+ import type { ResamplerEngine } from "./engine.ts";
2
+ /**
3
+ * Create the pure-TypeScript windowed-sinc engine.
4
+ *
5
+ * Applications normally never call this: core falls back to it on its own
6
+ * when the WASM engine cannot load. It can also be installed explicitly with
7
+ * `setResamplerEngine(createSincResamplerEngine())`.
8
+ */
9
+ export declare function createSincResamplerEngine(): ResamplerEngine;
10
+ //# sourceMappingURL=sinc_engine.d.ts.map
@@ -0,0 +1,235 @@
1
+ // Copyright (c) 2026, pipedog contributors
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ const SPECS = {
4
+ QQ: { passband: 0.8, attenuation: 50 },
5
+ LQ: { passband: 0.676, attenuation: 96 },
6
+ MQ: { passband: 0.91, attenuation: 96 },
7
+ HQ: { passband: 0.91, attenuation: 120 },
8
+ VHQ: { passband: 0.91, attenuation: 140 },
9
+ };
10
+ /** Kaiser window design (Kaiser's formulas for beta and length). */
11
+ function design({ passband, attenuation }) {
12
+ const transition = 1 - passband; // in units of the lower Nyquist frequency
13
+ const cutoff = (1 + passband) / 2;
14
+ const beta = attenuation > 50
15
+ ? 0.1102 * (attenuation - 8.7)
16
+ : 0.5842 * (attenuation - 21) ** 0.4 + 0.07886 * (attenuation - 21);
17
+ // Total length in samples of the lower rate, then in zero crossings per side.
18
+ const length = (attenuation - 7.95) / (2.285 * Math.PI * transition);
19
+ const zeros = Math.ceil((length / 2) * cutoff);
20
+ return { zeros, cutoff, beta };
21
+ }
22
+ const PRESETS = Object.fromEntries(Object.entries(SPECS).map(([q, spec]) => [q, design(spec)]));
23
+ /** Table points per zero crossing (linear interpolation error ~ -118 dB). */
24
+ const OVERSAMPLE = 1024;
25
+ /** Zeroth-order modified Bessel function of the first kind. */
26
+ function besselI0(x) {
27
+ let sum = 1;
28
+ let term = 1;
29
+ const q = (x * x) / 4;
30
+ for (let k = 1; k < 200; k++) {
31
+ term *= q / (k * k);
32
+ sum += term;
33
+ if (term < sum * 1e-17)
34
+ break;
35
+ }
36
+ return sum;
37
+ }
38
+ const TABLES = new Map();
39
+ /** sinc(x) * kaiser(x / zeros) for x in [0, zeros], OVERSAMPLE points per unit (plus guard). */
40
+ function kernelTable(quality) {
41
+ let table = TABLES.get(quality);
42
+ if (table)
43
+ return table;
44
+ const { zeros, beta } = PRESETS[quality];
45
+ const n = zeros * OVERSAMPLE;
46
+ table = new Float32Array(n + 2);
47
+ const i0b = besselI0(beta);
48
+ for (let i = 0; i <= n; i++) {
49
+ const x = i / OVERSAMPLE;
50
+ const r = x / zeros;
51
+ const w = besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / i0b;
52
+ const s = i === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
53
+ table[i] = s * w;
54
+ }
55
+ TABLES.set(quality, table);
56
+ return table;
57
+ }
58
+ function checkRates(inRate, outRate) {
59
+ if (!(inRate > 0) || !(outRate > 0) || !Number.isFinite(inRate) || !Number.isFinite(outRate)) {
60
+ throw new Error(`invalid sample rates: ${inRate}->${outRate}`);
61
+ }
62
+ }
63
+ /** One channel of a streaming windowed-sinc resampler. */
64
+ class SincChannel {
65
+ #step; // input samples per output sample
66
+ #scale; // 2 * fc (fc in cycles per input sample)
67
+ #halfWidth; // kernel half width in input samples
68
+ #table;
69
+ #tableScale; // table index per input-sample distance
70
+ /** Input history; #buf[0] is absolute input index #base. */
71
+ #buf = new Float64Array(256);
72
+ #len = 0;
73
+ #base = 0;
74
+ /** Total input samples seen and next output index (absolute). */
75
+ #totalIn = 0;
76
+ #nextOut = 0;
77
+ constructor(inRate, outRate, quality) {
78
+ const preset = PRESETS[quality];
79
+ this.#step = inRate / outRate;
80
+ const fc = 0.5 * Math.min(1, outRate / inRate) * preset.cutoff;
81
+ this.#scale = 2 * fc;
82
+ this.#halfWidth = preset.zeros / this.#scale;
83
+ this.#table = kernelTable(quality);
84
+ this.#tableScale = this.#scale * OVERSAMPLE;
85
+ }
86
+ #append(samples, start, stride, count) {
87
+ if (this.#len + count > this.#buf.length) {
88
+ const grown = new Float64Array(Math.max(this.#buf.length * 2, this.#len + count));
89
+ grown.set(this.#buf.subarray(0, this.#len));
90
+ this.#buf = grown;
91
+ }
92
+ for (let i = 0; i < count; i++) {
93
+ this.#buf[this.#len + i] = samples[start + i * stride];
94
+ }
95
+ this.#len += count;
96
+ this.#totalIn += count;
97
+ }
98
+ #sampleAt(t) {
99
+ const lo = Math.ceil(t - this.#halfWidth);
100
+ const hi = Math.floor(t + this.#halfWidth);
101
+ const buf = this.#buf;
102
+ const base = this.#base;
103
+ const table = this.#table;
104
+ const ts = this.#tableScale;
105
+ const tableMax = table.length - 2;
106
+ let acc = 0;
107
+ const from = Math.max(lo, base);
108
+ const to = Math.min(hi, base + this.#len - 1);
109
+ for (let n = from; n <= to; n++) {
110
+ const pos = Math.abs(t - n) * ts;
111
+ const i = pos | 0;
112
+ if (i > tableMax)
113
+ continue;
114
+ const frac = pos - i;
115
+ const k = table[i] + (table[i + 1] - table[i]) * frac;
116
+ acc += buf[n - base] * k;
117
+ }
118
+ return acc * this.#scale;
119
+ }
120
+ /** Number of outputs computable now (or, with `end`, the remaining outputs of the stream). */
121
+ #available(end) {
122
+ if (end) {
123
+ const total = Math.round(this.#totalIn / this.#step);
124
+ return Math.max(0, total - this.#nextOut);
125
+ }
126
+ // Output k needs inputs up to floor(k*step + halfWidth) < totalIn.
127
+ const last = Math.floor((this.#totalIn - 1 - this.#halfWidth) / this.#step);
128
+ return Math.max(0, last + 1 - this.#nextOut);
129
+ }
130
+ /** Feed `count` samples (strided) and write the outputs produced to `out` (strided). */
131
+ process(samples, start, stride, count, end) {
132
+ if (count > 0)
133
+ this.#append(samples, start, stride, count);
134
+ const n = this.#available(end);
135
+ const out = new Float64Array(n);
136
+ for (let i = 0; i < n; i++) {
137
+ out[i] = this.#sampleAt((this.#nextOut + i) * this.#step);
138
+ }
139
+ this.#nextOut += n;
140
+ if (end) {
141
+ this.clear();
142
+ }
143
+ else {
144
+ // Drop history no future output can reach.
145
+ const keepFrom = Math.floor(this.#nextOut * this.#step - this.#halfWidth) - 1;
146
+ const drop = Math.min(this.#len, keepFrom - this.#base);
147
+ if (drop > 0) {
148
+ this.#buf.copyWithin(0, drop, this.#len);
149
+ this.#len -= drop;
150
+ this.#base += drop;
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+ clear() {
156
+ this.#len = 0;
157
+ this.#base = 0;
158
+ this.#totalIn = 0;
159
+ this.#nextOut = 0;
160
+ }
161
+ }
162
+ function toInt16(x) {
163
+ const r = Math.round(x);
164
+ return r > 32767 ? 32767 : r < -32768 ? -32768 : r;
165
+ }
166
+ /** A streaming windowed-sinc resampler over interleaved samples. */
167
+ class SincStream {
168
+ #channels;
169
+ #numChannels;
170
+ /** Samples of an incomplete frame carried to the next call. */
171
+ #pending = [];
172
+ constructor(options) {
173
+ checkRates(options.inRate, options.outRate);
174
+ const numChannels = options.numChannels;
175
+ if (!Number.isInteger(numChannels) || numChannels < 1) {
176
+ throw new Error(`invalid channel count: ${numChannels}`);
177
+ }
178
+ this.#numChannels = numChannels;
179
+ this.#channels = Array.from({ length: numChannels }, () => new SincChannel(options.inRate, options.outRate, options.quality));
180
+ }
181
+ resampleChunk(chunk, last = false) {
182
+ const c = this.#numChannels;
183
+ let input = chunk;
184
+ if (this.#pending.length > 0) {
185
+ input = [...this.#pending, ...chunk];
186
+ this.#pending = [];
187
+ }
188
+ const frames = Math.floor(input.length / c);
189
+ const rest = input.length - frames * c;
190
+ if (rest > 0 && !last) {
191
+ this.#pending = Array.from({ length: rest }, (_, i) => input[frames * c + i]);
192
+ }
193
+ const outs = this.#channels.map((ch, i) => ch.process(input, i, c, frames, last));
194
+ const outFrames = outs[0]?.length ?? 0;
195
+ const isFloat = chunk instanceof Float32Array;
196
+ const out = isFloat ? new Float32Array(outFrames * c) : new Int16Array(outFrames * c);
197
+ for (let ch = 0; ch < c; ch++) {
198
+ const data = outs[ch];
199
+ for (let f = 0; f < outFrames; f++) {
200
+ const v = data[f];
201
+ out[f * c + ch] = isFloat ? v : toInt16(v);
202
+ }
203
+ }
204
+ if (last)
205
+ this.#pending = [];
206
+ return out;
207
+ }
208
+ clear() {
209
+ this.#pending = [];
210
+ for (const ch of this.#channels)
211
+ ch.clear();
212
+ }
213
+ destroy() { }
214
+ }
215
+ /**
216
+ * Create the pure-TypeScript windowed-sinc engine.
217
+ *
218
+ * Applications normally never call this: core falls back to it on its own
219
+ * when the WASM engine cannot load. It can also be installed explicitly with
220
+ * `setResamplerEngine(createSincResamplerEngine())`.
221
+ */
222
+ export function createSincResamplerEngine() {
223
+ return {
224
+ resample(audio, inRate, outRate, quality) {
225
+ if (inRate === outRate)
226
+ return audio;
227
+ const stream = new SincStream({ inRate, outRate, numChannels: 1, quality });
228
+ return stream.resampleChunk(audio, true);
229
+ },
230
+ createStream(options) {
231
+ return new SincStream(options);
232
+ },
233
+ };
234
+ }
235
+ //# sourceMappingURL=sinc_engine.js.map
@@ -8,20 +8,24 @@
8
8
  * 1. For batch processing of complete audio files
9
9
  * 2. When you have all the audio data available at once
10
10
  *
11
- * PORTING NOTE: libsoxr has no edge-compatible JavaScript equivalent yet. The
12
- * class logic is ported, but the actual resampling (`soxrResample`) is a stub
13
- * that throws "not ported yet". A WASM build of libsoxr, injected at runtime,
14
- * is the intended replacement.
11
+ * PORTING NOTE: libsoxr is not used. `soxr.resample()` is replaced by the
12
+ * resampler engine from `engine.ts`: `@pipedog/resampler-wasm` (rubato
13
+ * windowed sinc in WebAssembly) by default, a pure-TypeScript windowed-sinc
14
+ * fallback if that cannot load, or whatever `setResamplerEngine()` installed.
15
+ * Output lengths follow soxr's; the samples differ slightly because the
16
+ * filters differ.
15
17
  */
16
18
  import { BaseAudioResampler, type SoxrQuality } from "./base_audio_resampler.ts";
17
19
  /**
18
20
  * Stand-in for `soxr.resample()`: resample a whole mono signal.
19
21
  *
20
- * STUB: libsoxr is not available in JavaScript yet; always throws.
22
+ * Synchronous, like soxr's: it uses the engine already loaded (or injected)
23
+ * and throws if there is none yet. `await getResamplerEngine()` first when
24
+ * calling it outside `SOXRAudioResampler.resample()`.
21
25
  *
22
26
  * @internal
23
27
  */
24
- export declare function soxrResample<T extends Int16Array | Float32Array>(_audio: T, _inRate: number, _outRate: number, _quality: SoxrQuality): T;
28
+ export declare function soxrResample<T extends Int16Array | Float32Array>(audio: T, inRate: number, outRate: number, quality: SoxrQuality): T;
25
29
  export interface SOXRAudioResamplerOptions {
26
30
  /**
27
31
  * SOXR quality preset. Higher quality means higher CPU cost. One of "VHQ"
@@ -13,21 +13,30 @@
13
13
  * 1. For batch processing of complete audio files
14
14
  * 2. When you have all the audio data available at once
15
15
  *
16
- * PORTING NOTE: libsoxr has no edge-compatible JavaScript equivalent yet. The
17
- * class logic is ported, but the actual resampling (`soxrResample`) is a stub
18
- * that throws "not ported yet". A WASM build of libsoxr, injected at runtime,
19
- * is the intended replacement.
16
+ * PORTING NOTE: libsoxr is not used. `soxr.resample()` is replaced by the
17
+ * resampler engine from `engine.ts`: `@pipedog/resampler-wasm` (rubato
18
+ * windowed sinc in WebAssembly) by default, a pure-TypeScript windowed-sinc
19
+ * fallback if that cannot load, or whatever `setResamplerEngine()` installed.
20
+ * Output lengths follow soxr's; the samples differ slightly because the
21
+ * filters differ.
20
22
  */
21
23
  import { BaseAudioResampler } from "./base_audio_resampler.js";
24
+ import { getLoadedResamplerEngine, getResamplerEngine } from "./engine.js";
22
25
  /**
23
26
  * Stand-in for `soxr.resample()`: resample a whole mono signal.
24
27
  *
25
- * STUB: libsoxr is not available in JavaScript yet; always throws.
28
+ * Synchronous, like soxr's: it uses the engine already loaded (or injected)
29
+ * and throws if there is none yet. `await getResamplerEngine()` first when
30
+ * calling it outside `SOXRAudioResampler.resample()`.
26
31
  *
27
32
  * @internal
28
33
  */
29
- export function soxrResample(_audio, _inRate, _outRate, _quality) {
30
- throw new Error("soxr resampling is not ported yet");
34
+ export function soxrResample(audio, inRate, outRate, quality) {
35
+ const engine = getLoadedResamplerEngine();
36
+ if (!engine) {
37
+ throw new Error("no resampler engine loaded yet: `await getResamplerEngine()` (audio/resamplers/engine) first");
38
+ }
39
+ return engine.resample(audio, inRate, outRate, quality);
31
40
  }
32
41
  /**
33
42
  * Audio resampler implementation using the SoX resampler library.
@@ -53,6 +62,7 @@ export class SOXRAudioResampler extends BaseAudioResampler {
53
62
  if (inRate === outRate) {
54
63
  return audio;
55
64
  }
65
+ await getResamplerEngine();
56
66
  const audioData = bytesToInt16(audio);
57
67
  const resampledAudio = soxrResample(audioData, inRate, outRate, this._quality);
58
68
  return new Uint8Array(resampledAudio.buffer, resampledAudio.byteOffset, resampledAudio.byteLength);
@@ -1,20 +1,23 @@
1
1
  import { BaseAudioResampler, type SoxrQuality } from "./base_audio_resampler.ts";
2
+ import { type ResamplerEngine, type ResamplerStream } from "./engine.ts";
2
3
  /**
3
- * Stand-in for `soxr.ResampleStream` (mono, int16).
4
- *
5
- * STUB: libsoxr is not available in JavaScript yet; constructing one throws.
4
+ * Stand-in for `soxr.ResampleStream` (mono, int16): a stream of the resampler
5
+ * engine (the loaded/injected one unless `engine` is given).
6
6
  *
7
7
  * @internal
8
8
  */
9
9
  export declare class SoxrResampleStream {
10
- constructor(_options: {
10
+ protected _stream: ResamplerStream;
11
+ constructor(options: {
11
12
  inRate: number;
12
13
  outRate: number;
13
14
  numChannels: number;
14
15
  quality: SoxrQuality;
15
- });
16
- resampleChunk(_chunk: Int16Array, _last?: boolean): Int16Array;
16
+ }, engine?: ResamplerEngine | null);
17
+ resampleChunk(chunk: Int16Array, last?: boolean): Int16Array;
17
18
  clear(): void;
19
+ /** Free the engine's state (WASM memory). */
20
+ destroy(): void;
18
21
  }
19
22
  export interface SOXRStreamAudioResamplerOptions {
20
23
  /**
@@ -16,30 +16,40 @@
16
16
  * 4. When you need to reuse the same resampler configuration multiple times,
17
17
  * as it saves initialization overhead
18
18
  *
19
- * PORTING NOTE: libsoxr has no edge-compatible JavaScript equivalent yet. The
20
- * stream management (rate checks, idle clearing, flush/reset) is ported, but
21
- * `SoxrResampleStream` is a stub whose constructor throws "not ported yet".
19
+ * PORTING NOTE: libsoxr is not used. `soxr.ResampleStream` is replaced by a
20
+ * stream from the resampler engine (`engine.ts`): `@pipedog/resampler-wasm`
21
+ * by default, a pure-TypeScript windowed-sinc fallback if that cannot load,
22
+ * or whatever `setResamplerEngine()` installed. `resample()` loads the engine
23
+ * on first use.
22
24
  */
23
25
  import { time } from "@pipedog/asyncio";
24
26
  import { BaseAudioResampler } from "./base_audio_resampler.js";
27
+ import { getLoadedResamplerEngine, getResamplerEngine, } from "./engine.js";
25
28
  import { bytesToInt16 } from "./soxr_resampler.js";
26
29
  const EMPTY_AUDIO = new Int16Array(0);
27
30
  /**
28
- * Stand-in for `soxr.ResampleStream` (mono, int16).
29
- *
30
- * STUB: libsoxr is not available in JavaScript yet; constructing one throws.
31
+ * Stand-in for `soxr.ResampleStream` (mono, int16): a stream of the resampler
32
+ * engine (the loaded/injected one unless `engine` is given).
31
33
  *
32
34
  * @internal
33
35
  */
34
36
  export class SoxrResampleStream {
35
- constructor(_options) {
36
- throw new Error("soxr stream resampling is not ported yet");
37
+ _stream;
38
+ constructor(options, engine = getLoadedResamplerEngine()) {
39
+ if (!engine) {
40
+ throw new Error("no resampler engine loaded yet: `await getResamplerEngine()` (audio/resamplers/engine) first");
41
+ }
42
+ this._stream = engine.createStream(options);
37
43
  }
38
- resampleChunk(_chunk, _last = false) {
39
- throw new Error("soxr stream resampling is not ported yet");
44
+ resampleChunk(chunk, last = false) {
45
+ return this._stream.resampleChunk(chunk, last);
40
46
  }
41
47
  clear() {
42
- throw new Error("soxr stream resampling is not ported yet");
48
+ this._stream.clear();
49
+ }
50
+ /** Free the engine's state (WASM memory). */
51
+ destroy() {
52
+ this._stream.destroy?.();
43
53
  }
44
54
  }
45
55
  function int16ToBytes(samples) {
@@ -115,6 +125,9 @@ export class SOXRStreamAudioResampler extends BaseAudioResampler {
115
125
  if (inRate === outRate) {
116
126
  return audio;
117
127
  }
128
+ if (this._soxrStream === null) {
129
+ await getResamplerEngine();
130
+ }
118
131
  const stream = this._maybeInitializeSoxStream(inRate, outRate);
119
132
  const audioData = bytesToInt16(audio);
120
133
  const resampledAudio = stream.resampleChunk(audioData);
@@ -11,18 +11,21 @@
11
11
  *
12
12
  * PORTING NOTES:
13
13
  * - There is no ONNX runtime in core. The analyzer takes an injected
14
- * `createSession` (backed by `onnxruntime-node` on Bun/Node or
15
- * `onnxruntime-web` on Workers); the default one is a stub that throws
16
- * "not ported yet".
17
- * - The model is not bundled; `smartTurnModelPath` defaults to the upstream
18
- * file name and is handed to `createSession` as is.
19
- * - Resampling to 16 kHz uses the soxr stub, so pipelines must run at 16 kHz
20
- * until a resampler is available.
14
+ * `createSession`; `createLocalSmartTurnAnalyzerV3()` in
15
+ * `@pipedog/audio-smart-turn` wires it to `@pipedog/onnx` (onnxruntime-node
16
+ * or onnxruntime-web) and the model. Without one, the default factory throws.
17
+ * - The model is not in core; `smartTurnModelPath` defaults to the upstream
18
+ * file name and is handed to `createSession` as is (`@pipedog/audio-smart-turn`
19
+ * ships the file).
20
+ * - Resampling to 16 kHz uses the resampler engine (`audio/resamplers/engine`)
21
+ * instead of soxr. `_predictEndpoint` makes sure it is loaded before the
22
+ * (synchronous) `_resampleToModelRate` runs.
21
23
  * - `PIPECAT_SMART_TURN_LOG_DATA` WAV dumps need a filesystem, which core
22
24
  * doesn't use; the dump is a stub that logs a warning.
23
25
  */
24
26
  import { envTruthy } from "../../../utils/env.js";
25
27
  import { logger } from "../../../utils/logger.js";
28
+ import { getResamplerEngine } from "../../resamplers/engine.js";
26
29
  import { soxrResample } from "../../resamplers/soxr_resampler.js";
27
30
  import { computeWhisperLogMelFeatures, WHISPER_NUM_FRAMES, WHISPER_NUM_MELS, } from "./_whisper_features.js";
28
31
  import { BaseSmartTurn, } from "./base_smart_turn.js";
@@ -33,7 +36,8 @@ import { BaseSmartTurn, } from "./base_smart_turn.js";
33
36
  */
34
37
  export const MODEL_SAMPLE_RATE = 16000;
35
38
  const notPortedSessionFactory = async () => {
36
- throw new Error("LocalSmartTurnAnalyzerV3: ONNX runtime is not ported yet; pass `createSession`");
39
+ throw new Error("LocalSmartTurnAnalyzerV3: core has no ONNX runtime; use createLocalSmartTurnAnalyzerV3() " +
40
+ "from @pipedog/audio-smart-turn, or pass `createSession`");
37
41
  };
38
42
  function truncateAudioToLastNSeconds(audioArray, nSeconds = 8, sampleRate = MODEL_SAMPLE_RATE) {
39
43
  // Truncate audio to last n seconds or pad with zeros to meet n seconds.
@@ -113,6 +117,10 @@ export class LocalSmartTurnAnalyzerV3 extends BaseSmartTurn {
113
117
  const audioForLogging = audioArray;
114
118
  const actualRate = this._sampleRate || MODEL_SAMPLE_RATE;
115
119
  // Resample to 16 kHz if the pipeline uses a different sample rate
120
+ if (actualRate !== MODEL_SAMPLE_RATE) {
121
+ // soxr is synchronous; our engine may still need loading.
122
+ await getResamplerEngine();
123
+ }
116
124
  audioArray = this._resampleToModelRate(audioArray);
117
125
  // Truncate to 8 seconds (keeping the end) or pad to 8 seconds
118
126
  audioArray = truncateAudioToLastNSeconds(audioArray, 8);
@@ -54,6 +54,7 @@ export interface VADAnalyzerOptions {
54
54
  * on every runtime we target, so the analysis runs inline.
55
55
  */
56
56
  export declare abstract class VADAnalyzer {
57
+ #private;
57
58
  protected _initSampleRate: number | null;
58
59
  protected _sampleRate: number;
59
60
  protected _params: VADParams;
@@ -85,10 +86,13 @@ export declare abstract class VADAnalyzer {
85
86
  /**
86
87
  * Calculate voice activity confidence for the given audio buffer.
87
88
  *
89
+ * May return a promise: JavaScript ONNX runtimes only run models
90
+ * asynchronously. Upstream calls this on the analyzer's model thread.
91
+ *
88
92
  * @param buffer Audio buffer to analyze.
89
93
  * @returns Voice confidence score between 0.0 and 1.0.
90
94
  */
91
- abstract voiceConfidence(buffer: Uint8Array): number;
95
+ abstract voiceConfidence(buffer: Uint8Array): number | Promise<number>;
92
96
  /**
93
97
  * Set the sample rate for audio processing.
94
98
  *
@@ -113,8 +117,12 @@ export declare abstract class VADAnalyzer {
113
117
  * @returns Current VAD state after processing the buffer.
114
118
  */
115
119
  analyzeAudio(buffer: Uint8Array): Promise<VADState>;
116
- /** Analyze audio buffer and return current VAD state. */
117
- protected _runAnalyzer(buffer: Uint8Array): VADState;
120
+ /**
121
+ * Analyze audio buffer and return current VAD state.
122
+ *
123
+ * Synchronous when `voiceConfidence` is; otherwise returns a promise.
124
+ */
125
+ protected _runAnalyzer(buffer: Uint8Array): VADState | Promise<VADState>;
118
126
  /**
119
127
  * Release the analyzer's resources.
120
128
  *