@adhd/sox-embedding-provider 0.1.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.
package/dist/remote.js ADDED
@@ -0,0 +1,91 @@
1
+ import { PermanentEmbeddingError, TransientEmbeddingError } from './index.js';
2
+ /**
3
+ * Remote provider adapter — typed reference implementation.
4
+ *
5
+ * Implements EmbeddingProvider against the same async+batch-first contract.
6
+ * NOT wired to a live/paid endpoint — proves context-agnosticism without spend.
7
+ */
8
+ export class RemoteProvider {
9
+ metadata;
10
+ endpoint;
11
+ apiKey;
12
+ constructor(modelId, dimensions, endpoint, apiKey) {
13
+ this.metadata = {
14
+ modelId,
15
+ dimensions,
16
+ maxTokens: 8192,
17
+ isRemote: true,
18
+ isDeterministic: false,
19
+ providerUri: endpoint,
20
+ };
21
+ this.endpoint = endpoint;
22
+ this.apiKey = apiKey;
23
+ }
24
+ async embedSingle(text, role) {
25
+ void role;
26
+ this.validateEndpoint();
27
+ try {
28
+ const vec = await this.simulatedRemoteEmbed(text);
29
+ return vec;
30
+ }
31
+ catch (err) {
32
+ if (err instanceof TransientEmbeddingError || err instanceof PermanentEmbeddingError) {
33
+ throw err;
34
+ }
35
+ throw new TransientEmbeddingError(`Remote embed failed: ${err instanceof Error ? err.message : String(err)}`);
36
+ }
37
+ }
38
+ async *embedBatch(texts, opts) {
39
+ void opts;
40
+ this.validateEndpoint();
41
+ const batchSize = opts?.batchSize ?? 256;
42
+ for (let i = 0; i < texts.length; i += batchSize) {
43
+ const chunk = texts.slice(i, i + batchSize);
44
+ for (const text of chunk) {
45
+ try {
46
+ yield await this.simulatedRemoteEmbed(text);
47
+ }
48
+ catch (err) {
49
+ if (err instanceof TransientEmbeddingError || err instanceof PermanentEmbeddingError) {
50
+ throw err;
51
+ }
52
+ throw new TransientEmbeddingError(`Remote batch embed failed: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ health() {
58
+ return {
59
+ configured: `remote:${this.metadata.modelId}`,
60
+ active: this.metadata.modelId,
61
+ state: 'real',
62
+ dimensions: this.metadata.dimensions,
63
+ last_error: null,
64
+ };
65
+ }
66
+ async warmUp(_texts) {
67
+ // No-op: isDeterministic is false, cache would be unreliable.
68
+ }
69
+ validateEndpoint() {
70
+ if (!this.endpoint.startsWith('https://') && !this.endpoint.startsWith('http://')) {
71
+ throw new PermanentEmbeddingError(`Invalid remote endpoint: ${this.endpoint} (must start with http:// or https://)`);
72
+ }
73
+ if (!this.apiKey) {
74
+ throw new PermanentEmbeddingError(`Remote provider requires an API key (endpoint: ${this.endpoint})`);
75
+ }
76
+ }
77
+ async simulatedRemoteEmbed(text) {
78
+ if (!this.apiKey || this.apiKey.length < 8) {
79
+ throw new PermanentEmbeddingError('Invalid API key: too short');
80
+ }
81
+ if (text.length === 0) {
82
+ throw new PermanentEmbeddingError('Cannot embed empty text');
83
+ }
84
+ if (text.length > 8192) {
85
+ throw new PermanentEmbeddingError('Text exceeds maximum token length');
86
+ }
87
+ // Simulate: return a zero vector (reference impl, not wired to real endpoint)
88
+ return new Float32Array(this.metadata.dimensions);
89
+ }
90
+ }
91
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Process-wide singleton client for the ONE fastembed-hosting child
3
+ * **process** allowed to exist in this process (BL-238/BL-171).
4
+ *
5
+ * See `fastembedProcessHost.ts` for the full root-cause writeup: fastembed's
6
+ * onnxruntime-node@1.21.0 cannot safely share a `worker_threads.Worker` (or
7
+ * any thread) with `@huggingface/transformers`' onnxruntime-node@1.24.3 —
8
+ * proven not just for concurrent execution (whole-process HandleScope fatal)
9
+ * but even for strictly JS-serialized sequential loading in the same thread
10
+ * (deterministic `std::bad_alloc`, confirmed via instrumented tracing showing
11
+ * zero JS-level overlap). Only a real OS process boundary is proven safe.
12
+ *
13
+ * This mirrors `sharedOnnxWorker.ts`'s client shape (lazy singleton,
14
+ * request/response correlation by `id`, `unref()`'d so it never keeps a real
15
+ * process alive) but forks a child **process** instead of constructing a
16
+ * `worker_threads.Worker`.
17
+ */
18
+ export declare class SharedFastembedProcessClient {
19
+ private child;
20
+ private startingPromise;
21
+ private nextId;
22
+ private pending;
23
+ /** True once the underlying child process has been forked. */
24
+ get started(): boolean;
25
+ /** Lazily fork (exactly once) and return the single shared child process. */
26
+ private ensureProcess;
27
+ /**
28
+ * Send a request to the shared fastembed process and await its correlated
29
+ * response. Assigns a globally-unique `id` — the caller must NOT set its
30
+ * own `id` (any `id` field on `payload` is ignored/overwritten).
31
+ */
32
+ request<T = Record<string, unknown>>(payload: Record<string, unknown>, timeoutMs?: number): Promise<T>;
33
+ /**
34
+ * Forcefully terminate the shared fastembed process. Intended ONLY for
35
+ * full process shutdown or test teardown that genuinely owns the whole
36
+ * process's fastembed lifecycle.
37
+ */
38
+ terminate(): Promise<void>;
39
+ }
40
+ /**
41
+ * Process-wide singleton accessor — the ONLY sanctioned place a fastembed
42
+ * child process is forked anywhere in `@adhd/sox-embedding-provider`
43
+ * (BL-238/BL-171). Every `FastembedProvider` obtains its handle through this
44
+ * function instead of constructing its own `worker_threads.Worker` or
45
+ * `child_process`.
46
+ */
47
+ export declare function getSharedFastembedProcess(): SharedFastembedProcessClient;
48
+ /**
49
+ * Test-only: reset the module-level singleton so a test can exercise a
50
+ * fresh shared-process lifecycle (e.g. after deliberately crashing/
51
+ * terminating it). Does NOT terminate any existing process itself — call
52
+ * `.terminate()` on the previous instance first if a clean shutdown is
53
+ * needed.
54
+ */
55
+ export declare function __resetSharedFastembedProcessForTests(): void;
56
+ //# sourceMappingURL=sharedFastembedProcess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedFastembedProcess.d.ts","sourceRoot":"","sources":["../src/sharedFastembedProcess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAqCH,qBAAa,4BAA4B;IACvC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAmC;IAElD,8DAA8D;IAC9D,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,6EAA6E;IAC7E,OAAO,CAAC,aAAa;IAuDrB;;;;OAIG;IACG,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC;IA6Bb;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAYjC;AAID;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI,4BAA4B,CAGxE;AAED;;;;;;GAMG;AACH,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Process-wide singleton client for the ONE fastembed-hosting child
3
+ * **process** allowed to exist in this process (BL-238/BL-171).
4
+ *
5
+ * See `fastembedProcessHost.ts` for the full root-cause writeup: fastembed's
6
+ * onnxruntime-node@1.21.0 cannot safely share a `worker_threads.Worker` (or
7
+ * any thread) with `@huggingface/transformers`' onnxruntime-node@1.24.3 —
8
+ * proven not just for concurrent execution (whole-process HandleScope fatal)
9
+ * but even for strictly JS-serialized sequential loading in the same thread
10
+ * (deterministic `std::bad_alloc`, confirmed via instrumented tracing showing
11
+ * zero JS-level overlap). Only a real OS process boundary is proven safe.
12
+ *
13
+ * This mirrors `sharedOnnxWorker.ts`'s client shape (lazy singleton,
14
+ * request/response correlation by `id`, `unref()`'d so it never keeps a real
15
+ * process alive) but forks a child **process** instead of constructing a
16
+ * `worker_threads.Worker`.
17
+ */
18
+ import { fork } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { dirname, join } from 'node:path';
21
+ import { existsSync } from 'node:fs';
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ /**
24
+ * Resolve `fastembedProcessHost.js` regardless of whether this module is
25
+ * running compiled (`dist/sharedFastembedProcess.js`, sitting next to the
26
+ * compiled `dist/fastembedProcessHost.js`) or transformed-in-place from
27
+ * source (vitest runs `.ts` files directly via its SSR transform, so
28
+ * `__dirname` resolves to `src/`, which never contains a compiled `.js`) —
29
+ * mirrors the same `dist`-fallback pattern already proven in
30
+ * `sharedOnnxWorker.ts`.
31
+ */
32
+ function resolveFastembedHostPath() {
33
+ const sibling = join(__dirname, 'fastembedProcessHost.js');
34
+ if (existsSync(sibling))
35
+ return sibling;
36
+ const distSibling = join(__dirname, '..', 'dist', 'fastembedProcessHost.js');
37
+ if (existsSync(distSibling))
38
+ return distSibling;
39
+ // Last resort: return the original candidate so the resulting error names
40
+ // the path that was actually attempted.
41
+ return sibling;
42
+ }
43
+ export class SharedFastembedProcessClient {
44
+ child = null;
45
+ startingPromise = null;
46
+ nextId = 1;
47
+ pending = new Map();
48
+ /** True once the underlying child process has been forked. */
49
+ get started() {
50
+ return this.child !== null;
51
+ }
52
+ /** Lazily fork (exactly once) and return the single shared child process. */
53
+ ensureProcess() {
54
+ if (this.child)
55
+ return Promise.resolve(this.child);
56
+ if (this.startingPromise)
57
+ return this.startingPromise;
58
+ this.startingPromise = new Promise((resolveStart) => {
59
+ const hostPath = resolveFastembedHostPath();
60
+ const c = fork(hostPath, [], {
61
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
62
+ // Real inference is CPU-bound in native code; no need to keep the
63
+ // parent process alive on this child's account.
64
+ detached: false,
65
+ });
66
+ c.unref();
67
+ c.on('message', (msg) => {
68
+ const pending = this.pending.get(msg.id);
69
+ if (!pending)
70
+ return;
71
+ this.pending.delete(msg.id);
72
+ if ('error' in msg && typeof msg['error'] === 'string') {
73
+ pending.reject(new Error(msg['error']));
74
+ }
75
+ else {
76
+ pending.resolve(msg);
77
+ }
78
+ });
79
+ c.on('error', (err) => {
80
+ for (const { reject } of this.pending.values())
81
+ reject(err);
82
+ this.pending.clear();
83
+ this.child = null;
84
+ this.startingPromise = null;
85
+ });
86
+ c.on('exit', (code) => {
87
+ if (code !== 0 && code !== null) {
88
+ const err = new Error(`shared fastembed process exited with code ${code}`);
89
+ for (const { reject } of this.pending.values())
90
+ reject(err);
91
+ this.pending.clear();
92
+ }
93
+ this.child = null;
94
+ this.startingPromise = null;
95
+ });
96
+ // Same re-unref pattern as `sharedOnnxWorker.ts` — attaching listeners
97
+ // can re-ref the underlying handle; re-assert `unref()` once every
98
+ // listener is attached so a real process can exit when its own work is
99
+ // done instead of hanging on this child forever.
100
+ c.unref();
101
+ this.child = c;
102
+ resolveStart(c);
103
+ });
104
+ return this.startingPromise;
105
+ }
106
+ /**
107
+ * Send a request to the shared fastembed process and await its correlated
108
+ * response. Assigns a globally-unique `id` — the caller must NOT set its
109
+ * own `id` (any `id` field on `payload` is ignored/overwritten).
110
+ */
111
+ async request(payload, timeoutMs) {
112
+ const child = await this.ensureProcess();
113
+ const id = this.nextId++;
114
+ return new Promise((resolve, reject) => {
115
+ let to;
116
+ if (timeoutMs && timeoutMs > 0) {
117
+ to = setTimeout(() => {
118
+ this.pending.delete(id);
119
+ reject(new Error(`shared fastembed process request timed out after ${timeoutMs}ms`));
120
+ }, timeoutMs);
121
+ if (typeof to.unref === 'function')
122
+ to.unref();
123
+ }
124
+ this.pending.set(id, {
125
+ resolve: (v) => {
126
+ if (to)
127
+ clearTimeout(to);
128
+ resolve(v);
129
+ },
130
+ reject: (e) => {
131
+ if (to)
132
+ clearTimeout(to);
133
+ reject(e);
134
+ },
135
+ });
136
+ child.send({ ...payload, id });
137
+ });
138
+ }
139
+ /**
140
+ * Forcefully terminate the shared fastembed process. Intended ONLY for
141
+ * full process shutdown or test teardown that genuinely owns the whole
142
+ * process's fastembed lifecycle.
143
+ */
144
+ async terminate() {
145
+ const c = this.child;
146
+ this.child = null;
147
+ this.startingPromise = null;
148
+ for (const { reject } of this.pending.values()) {
149
+ reject(new Error('shared fastembed process terminated'));
150
+ }
151
+ this.pending.clear();
152
+ if (c) {
153
+ c.kill();
154
+ }
155
+ }
156
+ }
157
+ let _singleton = null;
158
+ /**
159
+ * Process-wide singleton accessor — the ONLY sanctioned place a fastembed
160
+ * child process is forked anywhere in `@adhd/sox-embedding-provider`
161
+ * (BL-238/BL-171). Every `FastembedProvider` obtains its handle through this
162
+ * function instead of constructing its own `worker_threads.Worker` or
163
+ * `child_process`.
164
+ */
165
+ export function getSharedFastembedProcess() {
166
+ if (!_singleton)
167
+ _singleton = new SharedFastembedProcessClient();
168
+ return _singleton;
169
+ }
170
+ /**
171
+ * Test-only: reset the module-level singleton so a test can exercise a
172
+ * fresh shared-process lifecycle (e.g. after deliberately crashing/
173
+ * terminating it). Does NOT terminate any existing process itself — call
174
+ * `.terminate()` on the previous instance first if a clean shutdown is
175
+ * needed.
176
+ */
177
+ export function __resetSharedFastembedProcessForTests() {
178
+ _singleton = null;
179
+ }
180
+ //# sourceMappingURL=sharedFastembedProcess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedFastembedProcess.js","sourceRoot":"","sources":["../src/sharedFastembedProcess.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Process-wide singleton client for the ONE onnxruntime-native-bearing
3
+ * `worker_threads.Worker` allowed to exist in this process (BL-238/BL-171).
4
+ *
5
+ * This worker (`embedWorker.ts`) hosts the MS-MARCO cross-encoder reranker
6
+ * (`@adhd/sox-hybrid-search`) and the DeBERTa NLI verifier
7
+ * (`@adhd/sox-claim-verification`) — both driven by
8
+ * `@huggingface/transformers`' onnxruntime-node@1.24.3. fastembed embeddings
9
+ * (`fastembed.ts`, onnxruntime-node@1.21.0) are DELIBERATELY routed
10
+ * elsewhere — see `sharedFastembedProcess.ts` / `fastembedProcessHost.ts` —
11
+ * for a second, independent native hazard (see below).
12
+ *
13
+ * ── Root cause #1 (proven via a from-scratch minimal repro, no test
14
+ * harness, no mocks — two independent worker_threads.Worker instances, each
15
+ * performing real ONNX inference, concurrently) ──
16
+ *
17
+ * onnxruntime-node's native N-API addon crashes the ENTIRE process (not
18
+ * just the offending worker) with:
19
+ *
20
+ * FATAL ERROR: HandleScope::HandleScope Entering the V8 API without
21
+ * proper locking in place
22
+ * ... Napi::FunctionReference::New(...)
23
+ * ... OrtValueToNapiValue(Napi::Env, Ort::Value&&)
24
+ * ... InferenceSessionWrap::Run(...)
25
+ *
26
+ * whenever 2+ *separate* `worker_threads.Worker` instances (i.e. 2+ separate
27
+ * V8 isolates) each hold an active onnxruntime-node `InferenceSession` and
28
+ * run inference concurrently. The crash fires from inside a `setImmediate`
29
+ * completion callback (`onnxruntime-node/dist/backend.js`), which strongly
30
+ * suggests the native binding keeps some global/static Napi reference that
31
+ * is not isolate-scoped, so a background completion callback belonging to
32
+ * one isolate's session fires while the wrong isolate (or no isolate/
33
+ * HandleScope at all) is active.
34
+ *
35
+ * This reproduces even with TWO workers using the exact SAME
36
+ * onnxruntime-node version (fastembed's 1.21.0, twice) — so it is NOT an
37
+ * ABI/version-mismatch bug between fastembed's onnxruntime-node@1.21.0 and
38
+ * @huggingface/transformers' onnxruntime-node@1.24.3; it is a genuine
39
+ * thread-safety limitation of the onnxruntime-node native addon itself when
40
+ * 2+ instances are concurrently active in one process, regardless of which
41
+ * package loaded which version.
42
+ *
43
+ * ── Root cause #2 (why fastembed is NOT also hosted in this same worker) ──
44
+ *
45
+ * A single shared worker hosting BOTH onnxruntime-node@1.21.0 (fastembed)
46
+ * AND onnxruntime-node@1.24.3 (transformers) was tried and instrumented:
47
+ * even with the two `init` calls strictly serialized in JS (proven via
48
+ * tracing — the second `init` provably did not begin until the first's
49
+ * promise had fully settled), fastembed's init still deterministically threw
50
+ * `std::bad_alloc` when it ran second. This means the two onnxruntime-node
51
+ * major versions leave native state that JS-level Promise resolution does
52
+ * not observe/synchronize (e.g. lingering background native thread-pool
53
+ * teardown) — a hazard below what JS scheduling can prevent. Only a real OS
54
+ * process boundary is proven safe for mixing the two versions; see
55
+ * `sharedFastembedProcess.ts` for that isolation.
56
+ *
57
+ * ── Fix ──
58
+ *
59
+ * Every rerank/verify ONNX consumer in this codebase — the MS-MARCO
60
+ * cross-encoder reranker (`@adhd/sox-hybrid-search`) and the DeBERTa NLI
61
+ * verifier (`@adhd/sox-claim-verification`) — is routed through exactly ONE
62
+ * lazily created, process-wide `worker_threads.Worker` running
63
+ * `embedWorker.ts`. There is never a second onnxruntime-bearing worker alive
64
+ * in the process, so root cause #1 is structurally impossible. fastembed
65
+ * never shares a thread (or process) with this worker at all, so root cause
66
+ * #2 is structurally impossible too.
67
+ *
68
+ * This is the ONLY place a `new Worker(embedWorker.js)` (or any other
69
+ * onnxruntime-bearing worker) should be constructed anywhere in the
70
+ * `@adhd/sox-embedding-provider` / `@adhd/sox-hybrid-search` /
71
+ * `@adhd/sox-claim-verification` triangle. `CrossEncoderWorker` and
72
+ * `WorkerProxy` delegate their wire traffic to
73
+ * `getSharedOnnxWorker().request(...)` instead of spawning their own
74
+ * `Worker`; `FastembedProvider` delegates to
75
+ * `getSharedFastembedProcess().request(...)` instead (a separate child
76
+ * PROCESS, not this worker).
77
+ *
78
+ * Trade-off (accepted, documented): `@adhd/sox-claim-verification`'s
79
+ * `workerCount` pool option (`ClaimVerifierConfig.workerCount`) previously
80
+ * gave real parallelism by spawning N separate worker threads. Since
81
+ * rerank+verify work in the process now funnels through this single shared
82
+ * worker, a `workerCount > 1` no longer buys extra parallelism (every
83
+ * `WorkerProxy` in the pool proxies to the same underlying worker) — but it
84
+ * remains safe (no crash) and does not regress correctness, only
85
+ * throughput under artificially-forced pool concurrency. This is the
86
+ * correct trade for a HIGH-severity whole-process crash.
87
+ */
88
+ export declare class SharedOnnxWorkerClient {
89
+ private worker;
90
+ private startingPromise;
91
+ private nextId;
92
+ private pending;
93
+ /** True once the underlying Worker has been created. */
94
+ get started(): boolean;
95
+ /** Lazily create (exactly once) and return the single shared Worker instance. */
96
+ private ensureWorker;
97
+ /**
98
+ * Send a request to the shared worker and await its correlated response.
99
+ * Assigns a globally-unique `id` — the caller must NOT set its own `id`
100
+ * (any `id` field on `payload` is ignored/overwritten).
101
+ *
102
+ * Resolves with the raw response message (everything embedWorker.ts sent
103
+ * back except the correlation `id` is semantically meaningful to the
104
+ * caller — e.g. `{ initOk, dim }`, `{ embedding }`, `{ scores }`,
105
+ * `{ result }`). Rejects if the response carries an `error` string, if
106
+ * the shared worker itself errors/exits non-zero while the request is
107
+ * pending, or if `timeoutMs` elapses first.
108
+ */
109
+ request<T = Record<string, unknown>>(payload: Record<string, unknown>, timeoutMs?: number): Promise<T>;
110
+ /**
111
+ * Forcefully terminate the shared worker. Intended ONLY for full process
112
+ * shutdown or test teardown that genuinely owns the whole process's ONNX
113
+ * lifecycle — an individual consumer's dispose()/stop() must NOT call
114
+ * this, since other consumers (embed / rerank / verify) may still depend
115
+ * on the shared worker.
116
+ */
117
+ terminate(): Promise<void>;
118
+ }
119
+ /**
120
+ * Process-wide singleton accessor — the ONLY sanctioned place a
121
+ * `new Worker(embedWorker.js)` is constructed anywhere in the embed/rerank/
122
+ * verify triangle (BL-238/BL-171). Every ONNX consumer (fastembed
123
+ * embeddings, cross-encoder rerank, NLI verify) must obtain its worker
124
+ * handle through this function instead of constructing its own `Worker`.
125
+ */
126
+ export declare function getSharedOnnxWorker(): SharedOnnxWorkerClient;
127
+ /**
128
+ * Test-only: reset the module-level singleton so a test can exercise a
129
+ * fresh shared-worker lifecycle (e.g. after deliberately crashing/
130
+ * terminating it). Does NOT terminate any existing worker itself — call
131
+ * `.terminate()` on the previous instance first if a clean shutdown is
132
+ * needed.
133
+ */
134
+ export declare function __resetSharedOnnxWorkerForTests(): void;
135
+ //# sourceMappingURL=sharedOnnxWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedOnnxWorker.d.ts","sourceRoot":"","sources":["../src/sharedOnnxWorker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AA2CH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAmC;IAElD,wDAAwD;IACxD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,iFAAiF;IACjF,OAAO,CAAC,YAAY;IAqDpB;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC;IA6Bb;;;;;;OAMG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAUjC;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,sBAAsB,CAG5D;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}