@camstack/addon-post-analysis 1.0.0 → 1.0.2

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