@camstack/addon-pipeline 1.1.52 → 1.1.54

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 (37) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +191 -12
  4. package/dist/detection-pipeline/index.mjs +191 -12
  5. package/dist/{dist-B-pQhc30.js → dist-BbaoC680.js} +111 -7
  6. package/dist/{dist-DM6-eQRs.mjs → dist-CEcTeu1h.mjs} +111 -7
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +114 -5
  10. package/dist/pipeline-runner/index.mjs +114 -5
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{remote-source-plane-DivMEw6Y.mjs → remote-source-plane-CNCqC_XG.mjs} +1 -1
  14. package/dist/{remote-source-plane-dSe5NlH9.js → remote-source-plane-vN45KFSm.js} +1 -1
  15. package/dist/session-decode/decode-worker-child.js +319 -35
  16. package/dist/session-decode/decode-worker-child.mjs +319 -35
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D6UvBYwr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-HS0tc6Sa.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3gTdHEh.mjs +26 -0
  20. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9fwKMfg.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_v5r5ya.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-DpF_eilj.mjs → hostInit-DgohfEDA.mjs} +3 -3
  22. package/dist/stream-broker/index.js +2 -2
  23. package/dist/stream-broker/index.mjs +2 -2
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/{worker-protocol-pk7qdYXt.mjs → worker-protocol-DGIt_waM.mjs} +6 -0
  26. package/dist/{worker-protocol-BCfO8gUF.js → worker-protocol-DkL6GDxe.js} +6 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DRCvljqU.js → MaskShapeCanvas-DI4BY7W2-CA2Hd6xx.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DmQAEfWC.js → MotionZonesSettings-NcxxQN8r-DUN1VeC7.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DNJ5RGGN.js → PrivacyMaskSettings-APgPLF7p-BqZIAU5s.js} +1 -1
  30. package/embed-dist/assets/index-CC06JBcl.css +2 -0
  31. package/embed-dist/assets/{index-BHKG2Pw-.js → index-CMkYypxB.js} +10 -10
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +1 -1
  34. package/python/inference_pool.py +42 -1
  35. package/python/test_inference_pool_layout.py +44 -0
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-o4tu_xuc.mjs +0 -26
  37. package/embed-dist/assets/index-JqUY2p33.css +0 -2
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B-pQhc30.js");
6
+ const require_dist = require("../dist-BbaoC680.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { C as mapAudioLabelToMacro, I as errMsg, L as BaseAddon, U as hydrateSchema, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-DM6-eQRs.mjs";
2
+ import { C as mapAudioLabelToMacro, I as errMsg, L as BaseAddon, U as hydrateSchema, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-CEcTeu1h.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import * as fs from "node:fs";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B-pQhc30.js");
6
+ const require_dist = require("../dist-BbaoC680.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
9
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
@@ -1543,11 +1543,16 @@ var STEP_CLIP_EMBEDDING = new PipelineStepBase({
1543
1543
  slot: "classifier",
1544
1544
  postprocessor: "clip",
1545
1545
  extractMode: "crop-roi",
1546
- inputClasses: null,
1546
+ inputClasses: [
1547
+ "person",
1548
+ "vehicle",
1549
+ "animal"
1550
+ ],
1547
1551
  outputClasses: ["embedding"],
1548
1552
  models: [...CLIP_EMBEDDING_MODELS],
1549
1553
  defaultModelId: "mobileclip-s1",
1550
1554
  enabledByDefault: true,
1555
+ backfillIntoExistingOverrides: true,
1551
1556
  defaultConfidence: 0,
1552
1557
  defaultMinParentScore: .4,
1553
1558
  group: "CLIP"
@@ -3718,6 +3723,93 @@ function bboxShortSide(bbox) {
3718
3723
  return Math.min(bbox[2] - bbox[0], bbox[3] - bbox[1]);
3719
3724
  }
3720
3725
  /**
3726
+ * Padded region (clamped to the frame) around a face bbox — the area a native
3727
+ * crop must cover so the warp has room around a tilted / edge face. Mirrors the
3728
+ * padding {@link alignFaceCrop} applies internally (`padRatio` default 0.4), so
3729
+ * the native and fallback paths sample the same neighbourhood.
3730
+ */
3731
+ function paddedFaceRegion(faceBbox, imageWidth, imageHeight, padRatio = .4) {
3732
+ const bw = faceBbox[2] - faceBbox[0];
3733
+ const bh = faceBbox[3] - faceBbox[1];
3734
+ const padX = bw * padRatio;
3735
+ const padY = bh * padRatio;
3736
+ return {
3737
+ left: Math.max(0, Math.floor(faceBbox[0] - padX)),
3738
+ top: Math.max(0, Math.floor(faceBbox[1] - padY)),
3739
+ right: Math.min(imageWidth, Math.ceil(faceBbox[2] + padX)),
3740
+ bottom: Math.min(imageHeight, Math.ceil(faceBbox[3] + padY))
3741
+ };
3742
+ }
3743
+ /**
3744
+ * Warp a raw RGB crop of `region` (native-resolution pixels of exactly that
3745
+ * detection-space region) so `landmarksImageSpace` land on the ArcFace
3746
+ * template. Landmarks are mapped from image space into the crop's local pixel
3747
+ * space by the per-axis region→crop scale (handles `maxWidth` downscale of the
3748
+ * native crop). Channels default to RGB (3) — the format the decode worker's
3749
+ * native surface produces.
3750
+ */
3751
+ async function alignFaceCropFromRgb(rgb, region, landmarksImageSpace, opts = {}) {
3752
+ const outSize = opts.outSize ?? 112;
3753
+ const quality = opts.quality ?? 95;
3754
+ const channels = opts.channels ?? 3;
3755
+ const regionW = region.right - region.left;
3756
+ const regionH = region.bottom - region.top;
3757
+ if (regionW < 1 || regionH < 1) throw new Error(`alignFaceCropFromRgb: degenerate region ${regionW}x${regionH}`);
3758
+ if (rgb.width < 2 || rgb.height < 2) throw new Error(`alignFaceCropFromRgb: degenerate crop ${rgb.width}x${rgb.height}`);
3759
+ const scaleX = rgb.width / regionW;
3760
+ const scaleY = rgb.height / regionH;
3761
+ const localLandmarks = landmarksImageSpace.map((l) => ({
3762
+ x: (l.x - region.left) * scaleX,
3763
+ y: (l.y - region.top) * scaleY
3764
+ }));
3765
+ const aligned = warpToTemplate({
3766
+ data: rgb.bytes,
3767
+ width: rgb.width,
3768
+ height: rgb.height,
3769
+ channels
3770
+ }, localLandmarks, opts.template ?? ARCFACE_TEMPLATE_112, outSize);
3771
+ return {
3772
+ jpeg: await (0, sharp.default)(Buffer.from(aligned.data), { raw: {
3773
+ width: aligned.width,
3774
+ height: aligned.height,
3775
+ channels: aligned.channels
3776
+ } }).jpeg({ quality }).toBuffer(),
3777
+ width: aligned.width,
3778
+ height: aligned.height
3779
+ };
3780
+ }
3781
+ /**
3782
+ * Produce a landmark-aligned face crop, preferring a NATIVE-resolution crop
3783
+ * (design B — the quality win) and FALLING BACK to today's downscaled
3784
+ * full-frame alignment on any native miss/error (never worse than before).
3785
+ *
3786
+ * The native path requests the padded face region from `nativeCropProvider`
3787
+ * (a best-effort local cap round-trip bound to this frame), warps the native
3788
+ * pixels; the fallback path re-uses {@link alignFaceCrop} on the downscaled
3789
+ * JPEG. ArcFace input is a fixed `outSize`×`outSize` either way — only the crop
3790
+ * SOURCE changes, so model cost is unchanged.
3791
+ */
3792
+ async function buildAlignedFaceCrop(args) {
3793
+ if (args.nativeCropProvider) try {
3794
+ const region = paddedFaceRegion(args.faceBbox, args.imageWidth, args.imageHeight);
3795
+ const roi = {
3796
+ x: region.left / args.imageWidth,
3797
+ y: region.top / args.imageHeight,
3798
+ w: (region.right - region.left) / args.imageWidth,
3799
+ h: (region.bottom - region.top) / args.imageHeight
3800
+ };
3801
+ const native = await args.nativeCropProvider(roi, args.nativeMaxWidth);
3802
+ if (native && native.width >= 2 && native.height >= 2 && native.bytes.length >= native.width * native.height * 3) return {
3803
+ ...await alignFaceCropFromRgb(native, region, args.landmarksImageSpace, { outSize: args.outSize }),
3804
+ source: "native"
3805
+ };
3806
+ } catch {}
3807
+ return {
3808
+ ...await alignFaceCrop(args.fullFrameJpeg, args.faceBbox, args.landmarksImageSpace, args.imageWidth, args.imageHeight, { outSize: args.outSize }),
3809
+ source: "fallback"
3810
+ };
3811
+ }
3812
+ /**
3721
3813
  * Produce an aligned 112x112 JPEG of a face from the full-frame JPEG and the
3722
3814
  * face's 5 landmarks (in absolute image-space coordinates). Decodes a padded
3723
3815
  * region around the face bbox to raw RGB, warps to the ArcFace template, and
@@ -4132,15 +4224,42 @@ function formatTopResult(output) {
4132
4224
  /**
4133
4225
  * Default minimum short-side (px, source-frame resolution) of a face bbox for
4134
4226
  * which a recognition embedding is attempted. Faces smaller than this upscale
4135
- * into garbage embeddings; we skip them rather than pollute clustering. Override
4227
+ * into weaker embeddings; we skip them rather than pollute clustering. Override
4136
4228
  * per-step via `settings.minFaceSize`.
4229
+ *
4230
+ * Lowered 40 → 28 (2026-07-14): detection runs on a downscaled frame (e.g.
4231
+ * 640×360), so an indoor person's face is often 28-38px there and was being
4232
+ * skipped entirely — no faces collected. 28px still warps acceptably onto the
4233
+ * 112×112 ArcFace input; the honest below-threshold count is surfaced by the
4234
+ * throttled "too small to embed" diagnostic (with the actual `faceShortSidePx`).
4235
+ * The quality-preserving alternative is to run face-detection at a higher input
4236
+ * resolution — a deeper change tracked separately.
4137
4237
  */
4138
- var DEFAULT_MIN_FACE_SIZE_PX = 40;
4238
+ var DEFAULT_MIN_FACE_SIZE_PX = 28;
4239
+ /**
4240
+ * Cap (px) on the width of the NATIVE face crop fetched for embedding (design
4241
+ * B). The arcface input is a fixed 112² warp, so a wider native crop buys no
4242
+ * embedding quality past a modest margin but does inflate the per-face transport
4243
+ * of the best-effort native-crop round-trip. 320px keeps a distant face well
4244
+ * above the downscaled detection resolution while bounding the crop payload.
4245
+ */
4246
+ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
4139
4247
  var PipelineExecutor = class {
4140
4248
  opts;
4141
4249
  constructor(opts) {
4142
4250
  this.opts = opts;
4143
4251
  }
4252
+ /** Per-key last-warn timestamps, so a persistently-failing/skipping child
4253
+ * step surfaces a diagnostic without spamming the log every frame. */
4254
+ warnThrottleAt = /* @__PURE__ */ new Map();
4255
+ throttledWarn(key, everyMs, message, meta) {
4256
+ const logger = this.opts.logger;
4257
+ if (!logger) return;
4258
+ const now = Date.now();
4259
+ if (now - (this.warnThrottleAt.get(key) ?? 0) < everyMs) return;
4260
+ this.warnThrottleAt.set(key, now);
4261
+ logger.warn(message, { meta });
4262
+ }
4144
4263
  /**
4145
4264
  * Execute the full pipeline tree against a frame.
4146
4265
  *
@@ -4154,7 +4273,7 @@ var PipelineExecutor = class {
4154
4273
  *
4155
4274
  * @returns FrameResult + optional trace
4156
4275
  */
4157
- async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts) {
4276
+ async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
4158
4277
  const startMs = Date.now();
4159
4278
  const verbosity = runOpts?.traceVerbosity ?? "off";
4160
4279
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
@@ -4192,7 +4311,7 @@ var PipelineExecutor = class {
4192
4311
  continue;
4193
4312
  }
4194
4313
  try {
4195
- await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
4314
+ await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider);
4196
4315
  } catch (err) {
4197
4316
  this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
4198
4317
  tags: { deviceId },
@@ -4294,7 +4413,7 @@ var PipelineExecutor = class {
4294
4413
  });
4295
4414
  return output;
4296
4415
  }
4297
- async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
4416
+ async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider) {
4298
4417
  for (const child of children) {
4299
4418
  if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
4300
4419
  const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
@@ -4309,10 +4428,30 @@ var PipelineExecutor = class {
4309
4428
  let cropH;
4310
4429
  if (modelEntry?.faceAlignment) {
4311
4430
  const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
4312
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
4431
+ const faceShortSide = bboxShortSide(parentDetection.bbox);
4432
+ if (faceShortSide < minFaceSize) {
4433
+ this.throttledWarn(`face-small:${child.stepId}`, 3e4, "face detected but too small to embed — skipping", {
4434
+ step: child.stepId,
4435
+ model: child.modelId,
4436
+ faceShortSidePx: Math.round(faceShortSide),
4437
+ minFaceSizePx: minFaceSize
4438
+ });
4439
+ continue;
4440
+ }
4313
4441
  const lms = parentDetection.landmarks;
4314
4442
  if (lms && lms.length >= 5) {
4315
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
4443
+ const aligned = await buildAlignedFaceCrop({
4444
+ fullFrameJpeg,
4445
+ faceBbox: parentDetection.bbox,
4446
+ landmarksImageSpace: lms,
4447
+ imageWidth,
4448
+ imageHeight,
4449
+ outSize: modelEntry.inputSize.width,
4450
+ ...nativeFaceCropProvider ? {
4451
+ nativeCropProvider: nativeFaceCropProvider,
4452
+ nativeMaxWidth: NATIVE_FACE_CROP_MAX_WIDTH
4453
+ } : {}
4454
+ });
4316
4455
  cropJpegBuf = aligned.jpeg;
4317
4456
  cropW = aligned.width;
4318
4457
  cropH = aligned.height;
@@ -4346,10 +4485,16 @@ var PipelineExecutor = class {
4346
4485
  y: l.y + py1
4347
4486
  }));
4348
4487
  }
4349
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
4488
+ await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider);
4350
4489
  }
4351
4490
  }
4352
- } catch {}
4491
+ } catch (err) {
4492
+ this.throttledWarn(`child-fail:${child.stepId}`, 15e3, "pipeline child step failed", {
4493
+ step: child.stepId,
4494
+ model: child.modelId,
4495
+ error: require_dist.errMsg(err)
4496
+ });
4497
+ }
4353
4498
  }
4354
4499
  }
4355
4500
  matchesInputClasses(className, inputClasses) {
@@ -5832,6 +5977,38 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5832
5977
  debug: { totalInferenceMs: 0 }
5833
5978
  };
5834
5979
  }
5980
+ /**
5981
+ * Build the design-B native-face-crop provider for a runtime frame. Present
5982
+ * ONLY when the frame arrived as a retention `frameHandle` (the key the owning
5983
+ * pipeline-runner registered the frame's native surface under) and the sibling
5984
+ * `pipeline-runner` cap is reachable via `ctx.api`. Each call is a best-effort
5985
+ * round-trip pinned to the frame's OWNING node (`nodePin(handle.nodeId)` — the
5986
+ * node that decoded it, i.e. THIS node for a local frame, never a cross-node
5987
+ * hop) that returns the native ROI or `null` on a miss; the executor then
5988
+ * falls back to the downscaled crop, so behaviour is never worse than before.
5989
+ */
5990
+ buildNativeFaceCropProvider(handle) {
5991
+ if (!handle) return void 0;
5992
+ const api = this.addonCtx?.api;
5993
+ if (!api) return void 0;
5994
+ return async (roi, maxWidth) => {
5995
+ try {
5996
+ const native = await api.pipelineRunner.getNativeCrop.query({
5997
+ handle,
5998
+ bbox: roi,
5999
+ ...maxWidth !== void 0 ? { maxWidth } : {}
6000
+ }, require_dist.nodePin(handle.nodeId));
6001
+ if (!native || native.width <= 0 || native.height <= 0) return null;
6002
+ return {
6003
+ bytes: native.bytes,
6004
+ width: native.width,
6005
+ height: native.height
6006
+ };
6007
+ } catch {
6008
+ return null;
6009
+ }
6010
+ };
6011
+ }
5835
6012
  async runPipeline(input, onProgress) {
5836
6013
  const nodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
5837
6014
  const sessionId = input.sessionId ?? `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
@@ -6088,7 +6265,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6088
6265
  const effectiveDeviceId = input.deviceId ?? 0;
6089
6266
  const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
6090
6267
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
6091
- const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, { traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full" });
6268
+ const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
6269
+ const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, { traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full" }, nativeFaceCropProvider);
6092
6270
  if (isRuntime) {
6093
6271
  if (trace && this.eventBus) this.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineTrace, {
6094
6272
  type: "device",
@@ -7173,6 +7351,7 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
7173
7351
  defaultModelId: step.defaultModelId,
7174
7352
  ...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
7175
7353
  ...step.enabledByDefault !== void 0 ? { enabledByDefault: step.enabledByDefault } : {},
7354
+ ...step.backfillIntoExistingOverrides !== void 0 ? { backfillIntoExistingOverrides: step.backfillIntoExistingOverrides } : {},
7176
7355
  defaultConfidence: step.defaultConfidence,
7177
7356
  ...step.group ? { group: step.group } : {},
7178
7357
  ...configSchema.length > 0 ? { configSchema: [...configSchema] } : {}
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { H as createEvent, I as errMsg, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, X as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-DM6-eQRs.mjs";
2
+ import { D as nodePin, H as createEvent, I as errMsg, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, X as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-CEcTeu1h.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
@@ -1535,11 +1535,16 @@ var STEP_CLIP_EMBEDDING = new PipelineStepBase({
1535
1535
  slot: "classifier",
1536
1536
  postprocessor: "clip",
1537
1537
  extractMode: "crop-roi",
1538
- inputClasses: null,
1538
+ inputClasses: [
1539
+ "person",
1540
+ "vehicle",
1541
+ "animal"
1542
+ ],
1539
1543
  outputClasses: ["embedding"],
1540
1544
  models: [...CLIP_EMBEDDING_MODELS],
1541
1545
  defaultModelId: "mobileclip-s1",
1542
1546
  enabledByDefault: true,
1547
+ backfillIntoExistingOverrides: true,
1543
1548
  defaultConfidence: 0,
1544
1549
  defaultMinParentScore: .4,
1545
1550
  group: "CLIP"
@@ -3710,6 +3715,93 @@ function bboxShortSide(bbox) {
3710
3715
  return Math.min(bbox[2] - bbox[0], bbox[3] - bbox[1]);
3711
3716
  }
3712
3717
  /**
3718
+ * Padded region (clamped to the frame) around a face bbox — the area a native
3719
+ * crop must cover so the warp has room around a tilted / edge face. Mirrors the
3720
+ * padding {@link alignFaceCrop} applies internally (`padRatio` default 0.4), so
3721
+ * the native and fallback paths sample the same neighbourhood.
3722
+ */
3723
+ function paddedFaceRegion(faceBbox, imageWidth, imageHeight, padRatio = .4) {
3724
+ const bw = faceBbox[2] - faceBbox[0];
3725
+ const bh = faceBbox[3] - faceBbox[1];
3726
+ const padX = bw * padRatio;
3727
+ const padY = bh * padRatio;
3728
+ return {
3729
+ left: Math.max(0, Math.floor(faceBbox[0] - padX)),
3730
+ top: Math.max(0, Math.floor(faceBbox[1] - padY)),
3731
+ right: Math.min(imageWidth, Math.ceil(faceBbox[2] + padX)),
3732
+ bottom: Math.min(imageHeight, Math.ceil(faceBbox[3] + padY))
3733
+ };
3734
+ }
3735
+ /**
3736
+ * Warp a raw RGB crop of `region` (native-resolution pixels of exactly that
3737
+ * detection-space region) so `landmarksImageSpace` land on the ArcFace
3738
+ * template. Landmarks are mapped from image space into the crop's local pixel
3739
+ * space by the per-axis region→crop scale (handles `maxWidth` downscale of the
3740
+ * native crop). Channels default to RGB (3) — the format the decode worker's
3741
+ * native surface produces.
3742
+ */
3743
+ async function alignFaceCropFromRgb(rgb, region, landmarksImageSpace, opts = {}) {
3744
+ const outSize = opts.outSize ?? 112;
3745
+ const quality = opts.quality ?? 95;
3746
+ const channels = opts.channels ?? 3;
3747
+ const regionW = region.right - region.left;
3748
+ const regionH = region.bottom - region.top;
3749
+ if (regionW < 1 || regionH < 1) throw new Error(`alignFaceCropFromRgb: degenerate region ${regionW}x${regionH}`);
3750
+ if (rgb.width < 2 || rgb.height < 2) throw new Error(`alignFaceCropFromRgb: degenerate crop ${rgb.width}x${rgb.height}`);
3751
+ const scaleX = rgb.width / regionW;
3752
+ const scaleY = rgb.height / regionH;
3753
+ const localLandmarks = landmarksImageSpace.map((l) => ({
3754
+ x: (l.x - region.left) * scaleX,
3755
+ y: (l.y - region.top) * scaleY
3756
+ }));
3757
+ const aligned = warpToTemplate({
3758
+ data: rgb.bytes,
3759
+ width: rgb.width,
3760
+ height: rgb.height,
3761
+ channels
3762
+ }, localLandmarks, opts.template ?? ARCFACE_TEMPLATE_112, outSize);
3763
+ return {
3764
+ jpeg: await sharp(Buffer.from(aligned.data), { raw: {
3765
+ width: aligned.width,
3766
+ height: aligned.height,
3767
+ channels: aligned.channels
3768
+ } }).jpeg({ quality }).toBuffer(),
3769
+ width: aligned.width,
3770
+ height: aligned.height
3771
+ };
3772
+ }
3773
+ /**
3774
+ * Produce a landmark-aligned face crop, preferring a NATIVE-resolution crop
3775
+ * (design B — the quality win) and FALLING BACK to today's downscaled
3776
+ * full-frame alignment on any native miss/error (never worse than before).
3777
+ *
3778
+ * The native path requests the padded face region from `nativeCropProvider`
3779
+ * (a best-effort local cap round-trip bound to this frame), warps the native
3780
+ * pixels; the fallback path re-uses {@link alignFaceCrop} on the downscaled
3781
+ * JPEG. ArcFace input is a fixed `outSize`×`outSize` either way — only the crop
3782
+ * SOURCE changes, so model cost is unchanged.
3783
+ */
3784
+ async function buildAlignedFaceCrop(args) {
3785
+ if (args.nativeCropProvider) try {
3786
+ const region = paddedFaceRegion(args.faceBbox, args.imageWidth, args.imageHeight);
3787
+ const roi = {
3788
+ x: region.left / args.imageWidth,
3789
+ y: region.top / args.imageHeight,
3790
+ w: (region.right - region.left) / args.imageWidth,
3791
+ h: (region.bottom - region.top) / args.imageHeight
3792
+ };
3793
+ const native = await args.nativeCropProvider(roi, args.nativeMaxWidth);
3794
+ if (native && native.width >= 2 && native.height >= 2 && native.bytes.length >= native.width * native.height * 3) return {
3795
+ ...await alignFaceCropFromRgb(native, region, args.landmarksImageSpace, { outSize: args.outSize }),
3796
+ source: "native"
3797
+ };
3798
+ } catch {}
3799
+ return {
3800
+ ...await alignFaceCrop(args.fullFrameJpeg, args.faceBbox, args.landmarksImageSpace, args.imageWidth, args.imageHeight, { outSize: args.outSize }),
3801
+ source: "fallback"
3802
+ };
3803
+ }
3804
+ /**
3713
3805
  * Produce an aligned 112x112 JPEG of a face from the full-frame JPEG and the
3714
3806
  * face's 5 landmarks (in absolute image-space coordinates). Decodes a padded
3715
3807
  * region around the face bbox to raw RGB, warps to the ArcFace template, and
@@ -4124,15 +4216,42 @@ function formatTopResult(output) {
4124
4216
  /**
4125
4217
  * Default minimum short-side (px, source-frame resolution) of a face bbox for
4126
4218
  * which a recognition embedding is attempted. Faces smaller than this upscale
4127
- * into garbage embeddings; we skip them rather than pollute clustering. Override
4219
+ * into weaker embeddings; we skip them rather than pollute clustering. Override
4128
4220
  * per-step via `settings.minFaceSize`.
4221
+ *
4222
+ * Lowered 40 → 28 (2026-07-14): detection runs on a downscaled frame (e.g.
4223
+ * 640×360), so an indoor person's face is often 28-38px there and was being
4224
+ * skipped entirely — no faces collected. 28px still warps acceptably onto the
4225
+ * 112×112 ArcFace input; the honest below-threshold count is surfaced by the
4226
+ * throttled "too small to embed" diagnostic (with the actual `faceShortSidePx`).
4227
+ * The quality-preserving alternative is to run face-detection at a higher input
4228
+ * resolution — a deeper change tracked separately.
4129
4229
  */
4130
- var DEFAULT_MIN_FACE_SIZE_PX = 40;
4230
+ var DEFAULT_MIN_FACE_SIZE_PX = 28;
4231
+ /**
4232
+ * Cap (px) on the width of the NATIVE face crop fetched for embedding (design
4233
+ * B). The arcface input is a fixed 112² warp, so a wider native crop buys no
4234
+ * embedding quality past a modest margin but does inflate the per-face transport
4235
+ * of the best-effort native-crop round-trip. 320px keeps a distant face well
4236
+ * above the downscaled detection resolution while bounding the crop payload.
4237
+ */
4238
+ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
4131
4239
  var PipelineExecutor = class {
4132
4240
  opts;
4133
4241
  constructor(opts) {
4134
4242
  this.opts = opts;
4135
4243
  }
4244
+ /** Per-key last-warn timestamps, so a persistently-failing/skipping child
4245
+ * step surfaces a diagnostic without spamming the log every frame. */
4246
+ warnThrottleAt = /* @__PURE__ */ new Map();
4247
+ throttledWarn(key, everyMs, message, meta) {
4248
+ const logger = this.opts.logger;
4249
+ if (!logger) return;
4250
+ const now = Date.now();
4251
+ if (now - (this.warnThrottleAt.get(key) ?? 0) < everyMs) return;
4252
+ this.warnThrottleAt.set(key, now);
4253
+ logger.warn(message, { meta });
4254
+ }
4136
4255
  /**
4137
4256
  * Execute the full pipeline tree against a frame.
4138
4257
  *
@@ -4146,7 +4265,7 @@ var PipelineExecutor = class {
4146
4265
  *
4147
4266
  * @returns FrameResult + optional trace
4148
4267
  */
4149
- async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts) {
4268
+ async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
4150
4269
  const startMs = Date.now();
4151
4270
  const verbosity = runOpts?.traceVerbosity ?? "off";
4152
4271
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
@@ -4184,7 +4303,7 @@ var PipelineExecutor = class {
4184
4303
  continue;
4185
4304
  }
4186
4305
  try {
4187
- await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
4306
+ await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider);
4188
4307
  } catch (err) {
4189
4308
  this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
4190
4309
  tags: { deviceId },
@@ -4286,7 +4405,7 @@ var PipelineExecutor = class {
4286
4405
  });
4287
4406
  return output;
4288
4407
  }
4289
- async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
4408
+ async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider) {
4290
4409
  for (const child of children) {
4291
4410
  if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
4292
4411
  const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
@@ -4301,10 +4420,30 @@ var PipelineExecutor = class {
4301
4420
  let cropH;
4302
4421
  if (modelEntry?.faceAlignment) {
4303
4422
  const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
4304
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
4423
+ const faceShortSide = bboxShortSide(parentDetection.bbox);
4424
+ if (faceShortSide < minFaceSize) {
4425
+ this.throttledWarn(`face-small:${child.stepId}`, 3e4, "face detected but too small to embed — skipping", {
4426
+ step: child.stepId,
4427
+ model: child.modelId,
4428
+ faceShortSidePx: Math.round(faceShortSide),
4429
+ minFaceSizePx: minFaceSize
4430
+ });
4431
+ continue;
4432
+ }
4305
4433
  const lms = parentDetection.landmarks;
4306
4434
  if (lms && lms.length >= 5) {
4307
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
4435
+ const aligned = await buildAlignedFaceCrop({
4436
+ fullFrameJpeg,
4437
+ faceBbox: parentDetection.bbox,
4438
+ landmarksImageSpace: lms,
4439
+ imageWidth,
4440
+ imageHeight,
4441
+ outSize: modelEntry.inputSize.width,
4442
+ ...nativeFaceCropProvider ? {
4443
+ nativeCropProvider: nativeFaceCropProvider,
4444
+ nativeMaxWidth: NATIVE_FACE_CROP_MAX_WIDTH
4445
+ } : {}
4446
+ });
4308
4447
  cropJpegBuf = aligned.jpeg;
4309
4448
  cropW = aligned.width;
4310
4449
  cropH = aligned.height;
@@ -4338,10 +4477,16 @@ var PipelineExecutor = class {
4338
4477
  y: l.y + py1
4339
4478
  }));
4340
4479
  }
4341
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
4480
+ await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, nativeFaceCropProvider);
4342
4481
  }
4343
4482
  }
4344
- } catch {}
4483
+ } catch (err) {
4484
+ this.throttledWarn(`child-fail:${child.stepId}`, 15e3, "pipeline child step failed", {
4485
+ step: child.stepId,
4486
+ model: child.modelId,
4487
+ error: errMsg(err)
4488
+ });
4489
+ }
4345
4490
  }
4346
4491
  }
4347
4492
  matchesInputClasses(className, inputClasses) {
@@ -5824,6 +5969,38 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5824
5969
  debug: { totalInferenceMs: 0 }
5825
5970
  };
5826
5971
  }
5972
+ /**
5973
+ * Build the design-B native-face-crop provider for a runtime frame. Present
5974
+ * ONLY when the frame arrived as a retention `frameHandle` (the key the owning
5975
+ * pipeline-runner registered the frame's native surface under) and the sibling
5976
+ * `pipeline-runner` cap is reachable via `ctx.api`. Each call is a best-effort
5977
+ * round-trip pinned to the frame's OWNING node (`nodePin(handle.nodeId)` — the
5978
+ * node that decoded it, i.e. THIS node for a local frame, never a cross-node
5979
+ * hop) that returns the native ROI or `null` on a miss; the executor then
5980
+ * falls back to the downscaled crop, so behaviour is never worse than before.
5981
+ */
5982
+ buildNativeFaceCropProvider(handle) {
5983
+ if (!handle) return void 0;
5984
+ const api = this.addonCtx?.api;
5985
+ if (!api) return void 0;
5986
+ return async (roi, maxWidth) => {
5987
+ try {
5988
+ const native = await api.pipelineRunner.getNativeCrop.query({
5989
+ handle,
5990
+ bbox: roi,
5991
+ ...maxWidth !== void 0 ? { maxWidth } : {}
5992
+ }, nodePin(handle.nodeId));
5993
+ if (!native || native.width <= 0 || native.height <= 0) return null;
5994
+ return {
5995
+ bytes: native.bytes,
5996
+ width: native.width,
5997
+ height: native.height
5998
+ };
5999
+ } catch {
6000
+ return null;
6001
+ }
6002
+ };
6003
+ }
5827
6004
  async runPipeline(input, onProgress) {
5828
6005
  const nodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
5829
6006
  const sessionId = input.sessionId ?? `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
@@ -6080,7 +6257,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6080
6257
  const effectiveDeviceId = input.deviceId ?? 0;
6081
6258
  const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
6082
6259
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
6083
- const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, { traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full" });
6260
+ const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
6261
+ const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, { traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full" }, nativeFaceCropProvider);
6084
6262
  if (isRuntime) {
6085
6263
  if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
6086
6264
  type: "device",
@@ -7165,6 +7343,7 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
7165
7343
  defaultModelId: step.defaultModelId,
7166
7344
  ...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
7167
7345
  ...step.enabledByDefault !== void 0 ? { enabledByDefault: step.enabledByDefault } : {},
7346
+ ...step.backfillIntoExistingOverrides !== void 0 ? { backfillIntoExistingOverrides: step.backfillIntoExistingOverrides } : {},
7168
7347
  defaultConfidence: step.defaultConfidence,
7169
7348
  ...step.group ? { group: step.group } : {},
7170
7349
  ...configSchema.length > 0 ? { configSchema: [...configSchema] } : {}