@camstack/addon-post-analysis 1.0.0 → 1.0.1

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,9 +1,12 @@
1
- import { a as PYTHON_SCRIPT, f as embeddingEncoderCapability, n as BaseAddon, o as RUNTIME_TO_FORMAT$1, t as BACKEND_TO_FORMAT$1 } from "../dist-4mTLJ7BJ.mjs";
2
- import sharp from "sharp";
3
- import { ModelDownloadService } from "@camstack/core";
1
+ import { l as embeddingEncoderCapability, t as BaseAddon } from "../dist-DbD5zJj7.mjs";
2
+ import { createRequire } from "node:module";
4
3
  import * as path from "node:path";
5
4
  import * as fs from "node:fs";
5
+ import { ModelDownloadService } from "@camstack/core";
6
6
  import { spawn } from "node:child_process";
7
+ //#region \0rolldown/runtime.js
8
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
9
+ //#endregion
7
10
  //#region src/embedding-encoder/catalogs/embedding-models.ts
8
11
  var CLIP_IMAGE_MODELS = [
9
12
  {
@@ -114,359 +117,51 @@ function createNoopLogger() {
114
117
  return logger;
115
118
  }
116
119
  //#endregion
117
- //#region src/embedding-encoder/shared/node-raw-tensor-engine.ts
118
- var BACKEND_TO_DEVICE$1 = {
119
- cpu: "cpu",
120
- coreml: "gpu-mps",
121
- cuda: "gpu-cuda",
122
- tensorrt: "tensorrt"
123
- };
120
+ //#region src/embedding-encoder/shared/python-raw-tensor-engine.ts
124
121
  /**
125
- * Raw tensor engine runs ONNX inference on pre-processed Float32Array input.
126
- * Used by addons that handle their own preprocessing (e.g. CLIP embedding encoder).
122
+ * Raw-tensor ONNX engine backed by an embedded-Python subprocess
123
+ * (`raw_tensor_inference.py`). Replaces the Node `onnxruntime-node` raw-tensor
124
+ * engine so the platform ships no Node ONNX runtime. The caller preprocesses to
125
+ * a Float32Array; this engine ships it to Python, which runs onnxruntime and
126
+ * returns the output tensor. Wire protocol = length-prefixed binary frames
127
+ * ([4B LE length][payload]).
127
128
  */
128
- var NodeRawTensorEngine = class {
129
- modelPath;
130
- backend;
131
- runtime = "onnx";
132
- device;
133
- session = null;
134
- log;
135
- constructor(modelPath, backend, logger) {
136
- this.modelPath = modelPath;
137
- this.backend = backend;
138
- this.device = BACKEND_TO_DEVICE$1[backend] ?? "cpu";
139
- this.log = logger ?? createNoopLogger();
140
- }
141
- async initialize() {
142
- const ort = await import("onnxruntime-node");
143
- const provider = this.backend === "coreml" ? "coreml" : this.backend === "cuda" ? "cuda" : "cpu";
144
- const absModelPath = path.isAbsolute(this.modelPath) ? this.modelPath : path.resolve(process.cwd(), this.modelPath);
145
- this.session = await ort.InferenceSession.create(absModelPath, { executionProviders: [provider] });
146
- this.log.info("ONNX session loaded", { meta: {
147
- modelPath: absModelPath,
148
- backend: this.backend,
149
- provider
150
- } });
151
- }
152
- async run(input, inputShape) {
153
- if (!this.session) throw new Error("NodeRawTensorEngine: not initialized — call initialize() first");
154
- const ort = await import("onnxruntime-node");
155
- const sess = this.session;
156
- const inputName = sess.inputNames[0];
157
- const tensor = new ort.Tensor("float32", input, [...inputShape]);
158
- const feeds = { [inputName]: tensor };
159
- const start = Date.now();
160
- let results;
161
- try {
162
- results = await sess.run(feeds);
163
- } catch (err) {
164
- const error = err instanceof Error ? err : new Error(String(err));
165
- this.log.error("Inference failed", { meta: { error: error.message } });
166
- throw error;
167
- }
168
- const outputName = sess.outputNames[0];
169
- this.log.debug("Inference complete", { meta: {
170
- durationMs: Date.now() - start,
171
- outputKeys: [outputName],
172
- preprocessMode: "raw-tensor"
173
- } });
174
- return results[outputName].data;
175
- }
176
- async dispose() {
177
- this.session = null;
178
- this.log.debug("Session disposed");
179
- }
180
- };
181
- //#endregion
182
- //#region src/embedding-encoder/shared/image-utils.ts
183
- /** Letterbox resize for YOLO: resize preserving aspect ratio, pad to square */
184
- async function letterbox(jpeg, targetSize) {
185
- const meta = await sharp(jpeg).metadata();
186
- const originalWidth = meta.width ?? 0;
187
- const originalHeight = meta.height ?? 0;
188
- const scale = Math.min(targetSize / originalWidth, targetSize / originalHeight);
189
- const scaledWidth = Math.round(originalWidth * scale);
190
- const scaledHeight = Math.round(originalHeight * scale);
191
- const padX = Math.floor((targetSize - scaledWidth) / 2);
192
- const padY = Math.floor((targetSize - scaledHeight) / 2);
193
- const { data } = await sharp(jpeg).resize(scaledWidth, scaledHeight).extend({
194
- top: padY,
195
- bottom: targetSize - scaledHeight - padY,
196
- left: padX,
197
- right: targetSize - scaledWidth - padX,
198
- background: {
199
- r: 114,
200
- g: 114,
201
- b: 114
202
- }
203
- }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
204
- const numPixels = targetSize * targetSize;
205
- const float32 = new Float32Array(3 * numPixels);
206
- for (let i = 0; i < numPixels; i++) {
207
- const srcBase = i * 3;
208
- float32[0 * numPixels + i] = data[srcBase] / 255;
209
- float32[1 * numPixels + i] = data[srcBase + 1] / 255;
210
- float32[2 * numPixels + i] = data[srcBase + 2] / 255;
211
- }
212
- return {
213
- data: float32,
214
- scale,
215
- padX,
216
- padY,
217
- originalWidth,
218
- originalHeight
219
- };
220
- }
221
- /** Resize and normalize to Float32Array */
222
- async function resizeAndNormalize(jpeg, targetWidth, targetHeight, normalization, layout) {
223
- const { data } = await sharp(jpeg).resize(targetWidth, targetHeight, { fit: "fill" }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
224
- const numPixels = targetWidth * targetHeight;
225
- const float32 = new Float32Array(3 * numPixels);
226
- const mean = [
227
- .485,
228
- .456,
229
- .406
230
- ];
231
- const std = [
232
- .229,
233
- .224,
234
- .225
235
- ];
236
- if (layout === "nchw") for (let i = 0; i < numPixels; i++) {
237
- const srcBase = i * 3;
238
- for (let c = 0; c < 3; c++) {
239
- const raw = data[srcBase + c] / 255;
240
- let val;
241
- if (normalization === "zero-one") val = raw;
242
- else if (normalization === "imagenet") val = (raw - mean[c]) / std[c];
243
- else val = data[srcBase + c];
244
- float32[c * numPixels + i] = val;
245
- }
246
- }
247
- else for (let i = 0; i < numPixels; i++) {
248
- const srcBase = i * 3;
249
- for (let c = 0; c < 3; c++) {
250
- const raw = data[srcBase + c] / 255;
251
- let val;
252
- if (normalization === "zero-one") val = raw;
253
- else if (normalization === "imagenet") val = (raw - mean[c]) / std[c];
254
- else val = data[srcBase + c];
255
- float32[i * 3 + c] = val;
256
- }
257
- }
258
- return float32;
259
- }
260
- //#endregion
261
- //#region src/embedding-encoder/shared/node-engine.ts
262
- var BACKEND_TO_PROVIDER = {
263
- cpu: "cpu",
264
- coreml: "coreml",
265
- cuda: "cuda",
266
- tensorrt: "tensorrt",
267
- dml: "dml"
268
- };
269
- var BACKEND_TO_DEVICE = {
270
- cpu: "cpu",
271
- coreml: "gpu-mps",
272
- cuda: "gpu-cuda",
273
- tensorrt: "tensorrt"
274
- };
275
- var NodeInferenceEngine = class {
276
- modelPath;
277
- backend;
278
- modelMeta;
279
- runtime = "onnx";
280
- device;
281
- session = null;
282
- log;
283
- constructor(modelPath, backend, modelMeta, logger) {
284
- this.modelPath = modelPath;
285
- this.backend = backend;
286
- this.modelMeta = modelMeta;
287
- this.device = BACKEND_TO_DEVICE[backend] ?? "cpu";
288
- this.log = logger ?? createNoopLogger();
289
- }
290
- async initialize() {
291
- const ort = await import("onnxruntime-node");
292
- const provider = BACKEND_TO_PROVIDER[this.backend] ?? "cpu";
293
- const absModelPath = path.isAbsolute(this.modelPath) ? this.modelPath : path.resolve(process.cwd(), this.modelPath);
294
- const sessionOptions = { executionProviders: [provider] };
295
- this.session = await ort.InferenceSession.create(absModelPath, sessionOptions);
296
- this.log.info("ONNX session loaded", { meta: {
297
- modelPath: absModelPath,
298
- backend: this.backend,
299
- provider
300
- } });
301
- }
302
- async infer(input) {
303
- const jpeg = input.kind === "jpeg" ? input.data : await this.encodeRawAsJpeg(input.data, input.width, input.height, input.format);
304
- const { data, letterboxMeta } = await this.preprocess(jpeg);
305
- const { inputSize } = this.modelMeta;
306
- const inputShape = this.modelMeta.preprocessMode === "letterbox" ? [
307
- 1,
308
- 3,
309
- inputSize.height,
310
- inputSize.width
311
- ] : [
312
- 1,
313
- 3,
314
- inputSize.height,
315
- inputSize.width
316
- ];
317
- const start = Date.now();
318
- let result;
319
- try {
320
- result = await this.runSession(data, inputShape);
321
- } catch (err) {
322
- const error = err instanceof Error ? err : new Error(String(err));
323
- this.log.error("Inference failed", { meta: { error: error.message } });
324
- throw error;
325
- }
326
- const durationMs = Date.now() - start;
327
- if ("tensor" in result) {
328
- this.log.debug("Inference complete", { meta: {
329
- durationMs,
330
- outputKeys: ["tensor"],
331
- preprocessMode: this.modelMeta.preprocessMode
332
- } });
333
- return {
334
- tensor: result.tensor,
335
- letterbox: letterboxMeta,
336
- inferenceMs: durationMs
337
- };
338
- }
339
- this.log.debug("Inference complete", { meta: {
340
- durationMs,
341
- outputKeys: Object.keys(result.tensors),
342
- preprocessMode: this.modelMeta.preprocessMode
343
- } });
344
- return {
345
- tensors: result.tensors,
346
- letterbox: letterboxMeta,
347
- inferenceMs: durationMs
348
- };
349
- }
350
- /** Preprocess JPEG to Float32Array using the configured mode */
351
- async preprocess(jpeg) {
352
- const { inputSize, inputNormalization, inputLayout, preprocessMode } = this.modelMeta;
353
- if (preprocessMode === "letterbox") {
354
- const result = await letterbox(jpeg, Math.max(inputSize.width, inputSize.height));
355
- const letterboxMeta = {
356
- scale: result.scale,
357
- padX: result.padX,
358
- padY: result.padY,
359
- originalWidth: result.originalWidth,
360
- originalHeight: result.originalHeight
361
- };
362
- return {
363
- data: result.data,
364
- letterboxMeta
365
- };
366
- }
367
- return { data: await resizeAndNormalize(jpeg, inputSize.width, inputSize.height, inputNormalization, inputLayout) };
368
- }
369
- async encodeRawAsJpeg(raw, width, height, format) {
370
- const sharp = (await import("sharp")).default;
371
- return sharp(raw, { raw: {
372
- width,
373
- height,
374
- channels: format === "gray" ? 1 : 3
375
- } }).jpeg({
376
- quality: 80,
377
- mozjpeg: false
378
- }).toBuffer();
379
- }
380
- /** Run an ONNX session with a single input, handling both single and multi-output models */
381
- async runSession(input, inputShape) {
382
- if (!this.session) throw new Error("NodeInferenceEngine: not initialized — call initialize() first");
383
- const ort = await import("onnxruntime-node");
384
- const sess = this.session;
385
- const inputName = sess.inputNames[0];
386
- const tensor = new ort.Tensor("float32", input, [...inputShape]);
387
- const feeds = { [inputName]: tensor };
388
- const results = await sess.run(feeds);
389
- const outputNames = sess.outputNames;
390
- if (outputNames.length === 1) return { tensor: results[outputNames[0]].data };
391
- const tensors = {};
392
- for (const name of outputNames) tensors[name] = results[name].data;
393
- return { tensors };
394
- }
395
- async run(input, inputShape) {
396
- const result = await this.runSession(input, inputShape);
397
- if ("tensor" in result) return result.tensor;
398
- const firstKey = Object.keys(result.tensors)[0];
399
- return result.tensors[firstKey];
400
- }
401
- async dispose() {
402
- this.session = null;
403
- this.log.debug("Session disposed");
404
- }
405
- };
406
- //#endregion
407
- //#region src/embedding-encoder/shared/python-engine.ts
408
- var PythonInferenceEngine = class {
129
+ var PythonRawTensorEngine = class {
409
130
  pythonPath;
410
131
  scriptPath;
411
132
  modelPath;
412
- extraArgs;
413
- runtime;
414
- device;
133
+ runtime = "onnx";
134
+ device = "cpu";
415
135
  process = null;
416
136
  receiveBuffer = Buffer.alloc(0);
417
137
  pendingResolve = null;
418
138
  pendingReject = null;
419
139
  log;
420
- constructor(pythonPath, scriptPath, runtime, modelPath, extraArgs = [], logger) {
140
+ constructor(pythonPath, scriptPath, modelPath, logger) {
421
141
  this.pythonPath = pythonPath;
422
142
  this.scriptPath = scriptPath;
423
143
  this.modelPath = modelPath;
424
- this.extraArgs = extraArgs;
425
- this.runtime = runtime;
426
- const runtimeDeviceMap = {
427
- onnx: "cpu",
428
- coreml: "gpu-mps",
429
- pytorch: "cpu",
430
- openvino: "cpu",
431
- tflite: "cpu"
432
- };
433
- this.device = runtimeDeviceMap[runtime];
434
144
  this.log = logger ?? createNoopLogger();
435
145
  }
436
146
  async initialize() {
437
- const args = [
438
- this.scriptPath,
439
- this.modelPath,
440
- ...this.extraArgs
441
- ];
442
- this.process = spawn(this.pythonPath, args, { stdio: [
147
+ this.process = spawn(this.pythonPath, [this.scriptPath, this.modelPath], { stdio: [
443
148
  "pipe",
444
149
  "pipe",
445
150
  "pipe"
446
151
  ] });
447
- if (!this.process.stdout || !this.process.stdin) throw new Error("PythonInferenceEngine: failed to create process pipes");
448
- this.log.info("Python process started", { meta: {
449
- pythonPath: this.pythonPath,
450
- scriptPath: this.scriptPath,
451
- modelPath: this.modelPath
452
- } });
453
152
  this.process.stderr?.on("data", (chunk) => {
454
- const lines = chunk.toString().split("\n");
455
- for (const line of lines) {
456
- const trimmed = line.trim();
457
- if (trimmed) this.log.warn(trimmed);
458
- }
153
+ const text = chunk.toString().trim();
154
+ if (text) this.log.warn(text);
459
155
  });
460
156
  this.process.on("error", (err) => {
461
- this.log.error("Process error", { meta: { error: err.message } });
157
+ this.log.error("Python raw-tensor process error", { meta: { error: err.message } });
462
158
  this.pendingReject?.(err);
463
159
  this.pendingReject = null;
464
160
  this.pendingResolve = null;
465
161
  });
466
162
  this.process.on("exit", (code) => {
467
- if (code !== 0) {
468
- this.log.error("Process exited", { meta: { code } });
469
- const err = /* @__PURE__ */ new Error(`PythonInferenceEngine: process exited with code ${code}`);
163
+ if (code !== 0 && code !== null) {
164
+ const err = /* @__PURE__ */ new Error(`PythonRawTensorEngine: process exited with code ${code}`);
470
165
  this.pendingReject?.(err);
471
166
  this.pendingReject = null;
472
167
  this.pendingResolve = null;
@@ -474,70 +169,29 @@ var PythonInferenceEngine = class {
474
169
  });
475
170
  this.process.stdout.on("data", (chunk) => {
476
171
  this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
477
- this._tryReceive();
172
+ this.tryReceive();
478
173
  });
479
- await new Promise((resolve, reject) => {
480
- const timeout = setTimeout(() => resolve(), 2e3);
481
- this.process?.on("error", (err) => {
482
- clearTimeout(timeout);
483
- reject(err);
484
- });
485
- this.process?.on("exit", (code) => {
486
- clearTimeout(timeout);
487
- if (code !== 0) reject(/* @__PURE__ */ new Error(`PythonInferenceEngine: process exited early with code ${code}`));
488
- });
489
- });
490
- }
491
- _tryReceive() {
492
- if (this.receiveBuffer.length < 4) return;
493
- const length = this.receiveBuffer.readUInt32LE(0);
494
- if (this.receiveBuffer.length < 4 + length) return;
495
- const jsonBytes = this.receiveBuffer.subarray(4, 4 + length);
496
- this.receiveBuffer = this.receiveBuffer.subarray(4 + length);
497
- const resolve = this.pendingResolve;
498
- const reject = this.pendingReject;
499
- this.pendingResolve = null;
500
- this.pendingReject = null;
501
- if (!resolve) return;
502
- try {
503
- resolve(JSON.parse(jsonBytes.toString("utf8")));
504
- } catch (err) {
505
- reject?.(err instanceof Error ? err : new Error(String(err)));
506
- }
507
- }
508
- /** Run inference, returning structured detection results. Encodes raw input to JPEG when needed. */
509
- async infer(input) {
510
- const start = Date.now();
511
- const jpeg = input.kind === "jpeg" ? input.data : await this.encodeRawAsJpeg(input.data, input.width, input.height, input.format);
512
- const result = await this.sendJpeg(jpeg);
513
- const durationMs = Date.now() - start;
514
- this.log.debug("Inference complete", { meta: { durationMs } });
515
- return {
516
- structured: result,
517
- inferenceMs: durationMs
518
- };
174
+ const ready = await this.receiveFrame();
175
+ if (ready.length !== 1 || ready[0] !== 1) throw new Error("PythonRawTensorEngine: unexpected ready frame");
176
+ this.log.info("ONNX raw-tensor engine ready (embedded Python)", { meta: { modelPath: this.modelPath } });
519
177
  }
520
- async encodeRawAsJpeg(raw, width, height, format) {
521
- const sharp = (await import("sharp")).default;
522
- return sharp(raw, { raw: {
523
- width,
524
- height,
525
- channels: format === "gray" ? 1 : 3
526
- } }).jpeg({
527
- quality: 80,
528
- mozjpeg: false
529
- }).toBuffer();
530
- }
531
- /** Send JPEG buffer via binary IPC, receive JSON detection results */
532
- async sendJpeg(jpeg) {
533
- if (!this.process?.stdin) throw new Error("PythonInferenceEngine: process not initialized");
534
- return new Promise((resolve, reject) => {
535
- this.pendingResolve = resolve;
536
- this.pendingReject = reject;
537
- const lengthBuf = Buffer.allocUnsafe(4);
538
- lengthBuf.writeUInt32LE(jpeg.length, 0);
539
- this.process.stdin.write(Buffer.concat([lengthBuf, jpeg]));
540
- });
178
+ async run(input, inputShape) {
179
+ if (!this.process?.stdin) throw new Error("PythonRawTensorEngine: not initialized — call initialize() first");
180
+ const ndims = inputShape.length;
181
+ const meta = Buffer.allocUnsafe(1 + ndims * 4);
182
+ meta.writeUInt8(ndims, 0);
183
+ for (let i = 0; i < ndims; i++) meta.writeUInt32LE(inputShape[i], 1 + i * 4);
184
+ const dataBuf = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
185
+ const payload = Buffer.concat([meta, dataBuf]);
186
+ const lenBuf = Buffer.allocUnsafe(4);
187
+ lenBuf.writeUInt32LE(payload.length, 0);
188
+ this.process.stdin.write(Buffer.concat([lenBuf, payload]));
189
+ const resp = await this.receiveFrame();
190
+ const floatStart = 1 + resp.readUInt8(0) * 4;
191
+ const count = (resp.length - floatStart) / 4;
192
+ const out = new Float32Array(count);
193
+ for (let i = 0; i < count; i++) out[i] = resp.readFloatLE(floatStart + i * 4);
194
+ return out;
541
195
  }
542
196
  async dispose() {
543
197
  const proc = this.process;
@@ -545,168 +199,37 @@ var PythonInferenceEngine = class {
545
199
  this.process = null;
546
200
  proc.stdin?.end();
547
201
  proc.kill("SIGTERM");
548
- if (!await new Promise((resolve) => {
202
+ await new Promise((resolve) => {
549
203
  const timer = setTimeout(() => {
550
- resolve(false);
204
+ try {
205
+ proc.kill("SIGKILL");
206
+ } catch {}
207
+ resolve();
551
208
  }, 5e3);
552
209
  proc.once("exit", () => {
553
210
  clearTimeout(timer);
554
- resolve(true);
211
+ resolve();
555
212
  });
556
- })) {
557
- try {
558
- proc.kill("SIGKILL");
559
- } catch {}
560
- this.log.warn("Python process did not exit gracefully — sent SIGKILL");
561
- } else this.log.debug("Python process terminated");
562
- }
563
- };
564
- //#endregion
565
- //#region src/embedding-encoder/shared/engine-resolver.ts
566
- /** Priority order for auto-selection of ONNX backends */
567
- var AUTO_BACKEND_PRIORITY = [
568
- "coreml",
569
- "cuda",
570
- "tensorrt",
571
- "cpu"
572
- ];
573
- var BACKEND_TO_FORMAT = BACKEND_TO_FORMAT$1;
574
- var RUNTIME_TO_FORMAT = RUNTIME_TO_FORMAT$1;
575
- function extractModelMeta(entry) {
576
- return {
577
- inputSize: entry.inputSize,
578
- inputNormalization: entry.inputNormalization ?? "zero-one",
579
- inputLayout: entry.inputLayout ?? "nchw",
580
- preprocessMode: entry.preprocessMode ?? "letterbox"
581
- };
582
- }
583
- function modelFilePath(modelsDir, modelEntry, format) {
584
- const formatEntry = modelEntry.formats[format];
585
- if (!formatEntry) throw new Error(`Model ${modelEntry.id} has no ${format} format`);
586
- const urlParts = formatEntry.url.split("/");
587
- const filename = urlParts[urlParts.length - 1] ?? `${modelEntry.id}.${format}`;
588
- return path.join(modelsDir, filename);
589
- }
590
- function modelExists(filePath) {
591
- try {
592
- return fs.existsSync(filePath);
593
- } catch {
594
- return false;
595
- }
596
- }
597
- async function resolveEngine(options) {
598
- const { runtime, backend, modelEntry, modelsDir, models } = options;
599
- const log = options.logger ?? createNoopLogger();
600
- let selectedFormat;
601
- let selectedBackend;
602
- if (runtime === "auto") {
603
- const available = await probeOnnxBackends();
604
- let chosen = null;
605
- for (const b of AUTO_BACKEND_PRIORITY) {
606
- if (!available.includes(b)) continue;
607
- const fmt = BACKEND_TO_FORMAT[b];
608
- if (!fmt) continue;
609
- if (!modelEntry.formats[fmt]) continue;
610
- chosen = {
611
- backend: b,
612
- format: fmt
613
- };
614
- break;
615
- }
616
- if (!chosen) throw new Error(`resolveEngine: no compatible backend found for model ${modelEntry.id}. Available backends: ${available.join(", ")}`);
617
- selectedFormat = chosen.format;
618
- selectedBackend = chosen.backend;
619
- } else {
620
- const fmt = RUNTIME_TO_FORMAT[runtime];
621
- if (!fmt) throw new Error(`resolveEngine: unsupported runtime "${runtime}"`);
622
- if (!modelEntry.formats[fmt]) if (fmt !== "onnx" && modelEntry.formats["onnx"]) {
623
- selectedFormat = "onnx";
624
- selectedBackend = backend || "cpu";
625
- } else throw new Error(`resolveEngine: model ${modelEntry.id} has no ${fmt} format for runtime ${runtime}`);
626
- else {
627
- selectedFormat = fmt;
628
- selectedBackend = runtime === "onnx" ? backend || "cpu" : runtime;
629
- }
630
- }
631
- let modelPath;
632
- if (models) modelPath = await models.ensure(modelEntry.id, selectedFormat);
633
- else {
634
- modelPath = modelFilePath(modelsDir, modelEntry, selectedFormat);
635
- if (!modelExists(modelPath)) throw new Error(`resolveEngine: model file not found at ${modelPath} and no model service provided`);
636
- }
637
- log.info("Engine resolved", { meta: {
638
- format: selectedFormat,
639
- backend: selectedBackend,
640
- modelId: modelEntry.id
641
- } });
642
- if (selectedFormat === "onnx") {
643
- const engine = new NodeInferenceEngine(modelPath, selectedBackend, extractModelMeta(modelEntry), options.logger);
644
- await engine.initialize();
645
- return {
646
- engine,
647
- format: selectedFormat,
648
- modelPath
649
- };
650
- }
651
- const effectiveRuntime = runtime === "auto" ? selectedBackend : runtime;
652
- let { pythonPath } = options;
653
- if (!pythonPath) {
654
- const { execFileSync: efs } = await import("node:child_process");
655
- for (const cmd of ["python3", "python"]) try {
656
- efs(cmd, ["--version"], {
657
- timeout: 3e3,
658
- stdio: "ignore"
659
- });
660
- pythonPath = cmd;
661
- break;
662
- } catch {}
213
+ });
663
214
  }
664
- const scriptName = PYTHON_SCRIPT[effectiveRuntime];
665
- if (scriptName && pythonPath) {
666
- const candidates = [
667
- path.join(__dirname, "../../python", scriptName),
668
- path.join(__dirname, "../python", scriptName),
669
- path.join(__dirname, "../../../python", scriptName)
670
- ];
671
- const scriptPath = candidates.find((p) => fs.existsSync(p));
672
- if (!scriptPath) throw new Error(`resolveEngine: Python script "${scriptName}" not found. Searched:\n${candidates.join("\n")}`);
673
- const inputSize = Math.max(modelEntry.inputSize.width, modelEntry.inputSize.height);
674
- const engine = new PythonInferenceEngine(pythonPath, scriptPath, effectiveRuntime, modelPath, [`--input-size=${inputSize}`, `--confidence=0.25`], options.logger);
675
- await engine.initialize();
676
- return {
677
- engine,
678
- format: selectedFormat,
679
- modelPath
680
- };
215
+ receiveFrame() {
216
+ return new Promise((resolve, reject) => {
217
+ this.pendingResolve = resolve;
218
+ this.pendingReject = reject;
219
+ });
681
220
  }
682
- const fallbackPath = modelFilePath(modelsDir, modelEntry, "onnx");
683
- if (modelEntry.formats["onnx"] && modelExists(fallbackPath)) {
684
- const engine = new NodeInferenceEngine(fallbackPath, "cpu", extractModelMeta(modelEntry), options.logger);
685
- await engine.initialize();
686
- return {
687
- engine,
688
- format: "onnx",
689
- modelPath: fallbackPath
690
- };
221
+ tryReceive() {
222
+ if (this.receiveBuffer.length < 4) return;
223
+ const length = this.receiveBuffer.readUInt32LE(0);
224
+ if (this.receiveBuffer.length < 4 + length) return;
225
+ const payload = Buffer.from(this.receiveBuffer.subarray(4, 4 + length));
226
+ this.receiveBuffer = this.receiveBuffer.subarray(4 + length);
227
+ const resolve = this.pendingResolve;
228
+ this.pendingResolve = null;
229
+ this.pendingReject = null;
230
+ resolve?.(payload);
691
231
  }
692
- throw new Error(`resolveEngine: format ${selectedFormat} is not yet supported by NodeInferenceEngine, no Python runtime is available, and no ONNX fallback exists`);
693
- }
694
- /** Probe which ONNX execution providers are available on this system */
695
- async function probeOnnxBackends() {
696
- const available = ["cpu"];
697
- try {
698
- const ort = await import("onnxruntime-node");
699
- const providers = ort.env?.webgl?.disabled !== void 0 ? ort.InferenceSession.getAvailableProviders?.() ?? [] : [];
700
- for (const p of providers) {
701
- const normalized = p.toLowerCase().replace("executionprovider", "");
702
- if (normalized === "coreml") available.push("coreml");
703
- else if (normalized === "cuda") available.push("cuda");
704
- else if (normalized === "tensorrt") available.push("tensorrt");
705
- }
706
- } catch {}
707
- if (process.platform === "darwin" && !available.includes("coreml")) available.push("coreml");
708
- return [...new Set(available)];
709
- }
232
+ };
710
233
  //#endregion
711
234
  //#region src/embedding-encoder/addon/clip-models.ts
712
235
  var CLIP_MODEL_META = {
@@ -783,10 +306,7 @@ function l2Normalize(vec) {
783
306
  var EmbeddingEncoderAddon = class extends BaseAddon {
784
307
  imageRawEngine = null;
785
308
  textRawEngine = null;
786
- imagePythonEngine = null;
787
- textPythonEngine = null;
788
309
  models = null;
789
- isPython = false;
790
310
  constructor() {
791
311
  super({
792
312
  modelId: DEFAULT_CLIP_MODEL,
@@ -810,19 +330,6 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
810
330
  await this.ensureImageEngine();
811
331
  const meta = getModelMeta(this.config.modelId);
812
332
  const start = Date.now();
813
- if (this.isPython && this.imagePythonEngine) {
814
- const jpegBuffer = Buffer.isBuffer(crop) ? crop : Buffer.from(crop);
815
- const result = await this.imagePythonEngine.infer({
816
- kind: "jpeg",
817
- data: jpegBuffer
818
- });
819
- const rawEmbedding = result.structured?.["embedding"];
820
- const normalized = l2Normalize(new Float32Array(rawEmbedding));
821
- return {
822
- embedding: Array.from(normalized),
823
- inferenceMs: result.inferenceMs ?? Date.now() - start
824
- };
825
- }
826
333
  const preprocessed = preprocessForClip(Buffer.isBuffer(crop) ? crop : Buffer.from(crop), width, height, meta.inputSize, meta.inputSize);
827
334
  const output = await this.imageRawEngine.run(preprocessed, [
828
335
  1,
@@ -842,19 +349,6 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
842
349
  await this.ensureTextEngine();
843
350
  const meta = getModelMeta(this.config.modelId);
844
351
  const start = Date.now();
845
- if (this.isPython && this.textPythonEngine) {
846
- const textBuffer = Buffer.from(JSON.stringify({ text }), "utf-8");
847
- const result = await this.textPythonEngine.infer({
848
- kind: "jpeg",
849
- data: textBuffer
850
- });
851
- const rawEmbedding = result.structured?.["embedding"];
852
- const normalized = l2Normalize(new Float32Array(rawEmbedding));
853
- return {
854
- embedding: Array.from(normalized),
855
- inferenceMs: result.inferenceMs ?? Date.now() - start
856
- };
857
- }
858
352
  const tokenIds = clipTokenize(text);
859
353
  const inputTensor = new Float32Array(tokenIds);
860
354
  const output = await this.textRawEngine.run(inputTensor, [1, tokenIds.length]);
@@ -870,57 +364,42 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
870
364
  return {
871
365
  modelId: this.config.modelId,
872
366
  embeddingDim: meta.embeddingDim,
873
- ready: this.imageRawEngine !== null || this.imagePythonEngine !== null
367
+ ready: this.imageRawEngine !== null
874
368
  };
875
369
  }
876
370
  async ensureImageEngine() {
877
- if (this.imageRawEngine || this.imagePythonEngine) return;
371
+ if (this.imageRawEngine) return;
878
372
  const meta = getModelMeta(this.config.modelId);
879
373
  const imageEntry = CLIP_IMAGE_MODELS.find((m) => m.id === meta.imageModelId);
880
374
  if (!imageEntry) throw new Error(`EmbeddingEncoderAddon: unknown image model "${meta.imageModelId}"`);
881
375
  await this.resolveForEntry(imageEntry, "image");
882
376
  }
883
377
  async ensureTextEngine() {
884
- if (this.textRawEngine || this.textPythonEngine) return;
378
+ if (this.textRawEngine) return;
885
379
  const meta = getModelMeta(this.config.modelId);
886
380
  const textEntry = CLIP_TEXT_MODELS.find((m) => m.id === meta.textModelId);
887
381
  if (!textEntry) throw new Error(`EmbeddingEncoderAddon: unknown text model "${meta.textModelId}"`);
888
382
  await this.resolveForEntry(textEntry, "text");
889
383
  }
890
384
  async resolveForEntry(entry, target) {
891
- const runtime = this.config.runtime === "auto" ? "auto" : this.config.runtime === "node" ? "onnx" : this.config.runtime;
892
- const modelsDir = this.models.getModelsDir();
893
385
  const engineLogger = this.ctx.logger.withTags({
894
386
  modelId: entry.id,
895
387
  runtime: this.config.runtime,
896
388
  backend: this.config.backend
897
389
  });
898
- await this.models.ensure(entry.id, "onnx");
899
- const resolved = await resolveEngine({
900
- runtime,
901
- backend: this.config.backend,
902
- modelEntry: entry,
903
- modelsDir,
904
- models: this.models ?? void 0,
905
- logger: engineLogger
906
- });
907
- if (resolved.format !== "onnx") {
908
- this.isPython = true;
909
- if (target === "image") this.imagePythonEngine = resolved.engine;
910
- else this.textPythonEngine = resolved.engine;
911
- } else {
912
- const rawEngine = new NodeRawTensorEngine(resolved.modelPath, this.config.backend, engineLogger);
913
- await rawEngine.initialize();
914
- await resolved.engine.dispose();
915
- if (target === "image") this.imageRawEngine = rawEngine;
916
- else this.textRawEngine = rawEngine;
917
- }
390
+ const modelPath = await this.models.ensure(entry.id, "onnx");
391
+ const pythonPath = await this.ctx.deps.ensurePython();
392
+ if (!pythonPath) throw new Error("EmbeddingEncoder: embedded Python is unavailable — cannot run ONNX embeddings. ctx.deps.ensurePython() returned null (portable Python download likely failed).");
393
+ const pythonDir = resolveEmbeddingPythonDir();
394
+ await this.ctx.deps.installPythonRequirements(path.join(pythonDir, "requirements-embedding.txt"));
395
+ const rawEngine = new PythonRawTensorEngine(pythonPath, path.join(pythonDir, "raw_tensor_inference.py"), modelPath, engineLogger);
396
+ await rawEngine.initialize();
397
+ if (target === "image") this.imageRawEngine = rawEngine;
398
+ else this.textRawEngine = rawEngine;
918
399
  }
919
400
  async onShutdown() {
920
401
  await this.imageRawEngine?.dispose();
921
402
  await this.textRawEngine?.dispose();
922
- await this.imagePythonEngine?.dispose();
923
- await this.textPythonEngine?.dispose();
924
403
  }
925
404
  globalSettingsSchema() {
926
405
  return this.schema({ sections: [{
@@ -939,22 +418,15 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
939
418
  type: "select",
940
419
  key: "runtime",
941
420
  label: "Runtime",
942
- description: "Inference runtime (auto selects the best available)",
421
+ description: "Inference runtime ONNX runs in the embedded Python.",
943
422
  default: "auto",
944
- options: [
945
- {
946
- label: "Auto",
947
- value: "auto"
948
- },
949
- {
950
- label: "Node (ONNX)",
951
- value: "node"
952
- },
953
- {
954
- label: "Python",
955
- value: "python"
956
- }
957
- ]
423
+ options: [{
424
+ label: "Auto",
425
+ value: "auto"
426
+ }, {
427
+ label: "Python",
428
+ value: "python"
429
+ }]
958
430
  },
959
431
  {
960
432
  type: "select",
@@ -997,5 +469,20 @@ function clipTokenize(text, maxLength = 77) {
997
469
  while (tokens.length < maxLength) tokens.push(0);
998
470
  return tokens;
999
471
  }
472
+ /**
473
+ * Locate the addon's bundled `python/` dir (holds `raw_tensor_inference.py` +
474
+ * `requirements-embedding.txt`). Published package first, then `__dirname`
475
+ * candidates for the in-tree dev build.
476
+ */
477
+ function resolveEmbeddingPythonDir() {
478
+ const candidates = [];
479
+ try {
480
+ const pkgPath = __require.resolve("@camstack/addon-post-analysis/package.json");
481
+ candidates.push(path.join(path.dirname(pkgPath), "python"));
482
+ } catch {}
483
+ candidates.push(path.join(__dirname, "../../python"), path.join(__dirname, "../../../python"), path.join(__dirname, "../python"), path.join(__dirname, "../../../../python"));
484
+ for (const c of candidates) if (fs.existsSync(path.join(c, "raw_tensor_inference.py"))) return c;
485
+ throw new Error(`EmbeddingEncoder: python/ dir (raw_tensor_inference.py) not found. Searched:\n${candidates.join("\n")}`);
486
+ }
1000
487
  //#endregion
1001
488
  export { EmbeddingEncoderAddon, EmbeddingEncoderAddon as default };