@camstack/addon-pipeline 1.2.0 → 1.2.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.
Files changed (34) 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 +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-DWy-H6bo.js → dist-CVjqvU8G.js} +95 -13
  6. package/dist/{dist-DLWXeXC1.mjs → dist-ClyAFBZk.mjs} +95 -13
  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 +228 -24
  10. package/dist/pipeline-runner/index.mjs +228 -24
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/session-decode/decode-worker-child.js +55 -7
  14. package/dist/session-decode/decode-worker-child.mjs +55 -7
  15. package/dist/{step-definitions-BQG_6dmZ.js → step-definitions-D76MMwbo.js} +1 -1
  16. package/dist/{step-definitions-CVk19Y0u.mjs → step-definitions-DMOw4fzn.mjs} +1 -1
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CpB1jG0N.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-7jRy3Uzr.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-CQC_f4Yv.mjs +26 -0
  20. package/dist/stream-broker/{hostInit--Zair8Av.mjs → hostInit-4pxims1d.mjs} +3 -3
  21. package/dist/stream-broker/index.js +1 -1
  22. package/dist/stream-broker/index.mjs +1 -1
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/dist/{worker-protocol-CyVJTZEO.mjs → worker-protocol-D7RzZIla.mjs} +16 -2
  25. package/dist/{worker-protocol-PP4jKHHJ.js → worker-protocol-DextwlTX.js} +21 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-sOOxiGvP.js → MaskShapeCanvas-DI4BY7W2-BPRZW9TX.js} +1 -1
  27. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DdTQuT23.js → MotionZonesSettings-NcxxQN8r-Bw6Nlg-Q.js} +1 -1
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BrR0L3ql.js → PrivacyMaskSettings-APgPLF7p-BLF9QKG8.js} +1 -1
  29. package/embed-dist/assets/{index-Dk21l1_3.js → index-DiUhKfSk.js} +13 -13
  30. package/embed-dist/assets/index-Dv5H-32C.css +2 -0
  31. package/embed-dist/index.html +2 -2
  32. package/package.json +1 -1
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BflyNuZv.mjs +0 -26
  34. package/embed-dist/assets/index-D4MGuECB.css +0 -2
@@ -3,11 +3,11 @@ 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-DWy-H6bo.js");
6
+ const require_dist = require("../dist-CVjqvU8G.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
8
  const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
9
- const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
10
- const require_step_definitions = require("../step-definitions-BQG_6dmZ.js");
9
+ const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
10
+ const require_step_definitions = require("../step-definitions-D76MMwbo.js");
11
11
  let node_child_process = require("node:child_process");
12
12
  let node_url = require("node:url");
13
13
  let sharp = require("sharp");
@@ -1152,6 +1152,17 @@ function summarizeDeviceLoad(attachedDeviceKeys) {
1152
1152
  })).toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
1153
1153
  }
1154
1154
  //#endregion
1155
+ //#region src/pipeline-runner/native-crop-tier.ts
1156
+ /**
1157
+ * Pure tier decision for `getNativeCrop`. A native HIT always resolves to the
1158
+ * native surface. On a native MISS, only a full-frame request falls back to the
1159
+ * ≤640 RAM store; an ROI miss is reported as `miss` (no quality masquerade).
1160
+ */
1161
+ function resolveNativeCropSource(isFullFrame, nativeHit) {
1162
+ if (nativeHit) return "native";
1163
+ return isFullFrame ? "ram-fullframe" : "miss";
1164
+ }
1165
+ //#endregion
1155
1166
  //#region src/session-decode/fork-decode-worker.ts
1156
1167
  /**
1157
1168
  * Real `fork` implementation for the session-decode coordinator — Epic C P1,
@@ -1669,11 +1680,18 @@ var SessionDecodeSession = class {
1669
1680
  });
1670
1681
  return;
1671
1682
  case "nativeCropMiss":
1683
+ if (raw.reason !== void 0) this.logger.debug("session-decode: native crop miss", { meta: {
1684
+ requestId: raw.requestId,
1685
+ reason: raw.reason
1686
+ } });
1672
1687
  this.settleNativeCrop(raw.requestId, null);
1673
1688
  return;
1674
1689
  case "error":
1675
1690
  this.settleError(raw.message, raw.frameId);
1676
1691
  return;
1692
+ case "log":
1693
+ this.logger[raw.level](raw.message);
1694
+ return;
1677
1695
  }
1678
1696
  }
1679
1697
  handleExit(info) {
@@ -2242,13 +2260,93 @@ async function runDetailSubtree(deps, input) {
2242
2260
  if (!crop) return null;
2243
2261
  const details = [];
2244
2262
  for (const step of steps) {
2245
- const result = await deps.runPipeline([step], crop.jpeg, input.deviceId);
2263
+ const deviceKey = deps.resolveStepDevice?.(input.deviceId, step);
2264
+ const result = await deps.runPipeline([step], crop.jpeg, input.deviceId, deviceKey);
2246
2265
  if (!result) continue;
2247
2266
  for (const detection of result.detections) details.push(toDetailResult(detection, step.addonId, crop));
2248
2267
  }
2249
2268
  return { details };
2250
2269
  }
2251
2270
  //#endregion
2271
+ //#region src/pipeline-runner/device-jump.ts
2272
+ /**
2273
+ * Step-tree device jump (same-node, phase 1) — the PURE resolution core.
2274
+ *
2275
+ * Given the camera's elected ("effective") inference device and the node's
2276
+ * enabled-device roster, decide which device ONE enrichment step should run on:
2277
+ *
2278
+ * 1. A manual `jumpDeviceKey` override wins when it names a candidate that can
2279
+ * run the step (bypasses the auto choice; operator intent).
2280
+ * 2. Otherwise, if the effective device's format can run the step's model →
2281
+ * NO jump (the step inherits the effective device — the common case).
2282
+ * 3. Otherwise AUTO-jump: among the node's OTHER enabled devices whose format
2283
+ * can run the step (and that are under their `maxSessions` cap), pick the
2284
+ * weighted-least-loaded one — mirroring the balancer's per-device core.
2285
+ * 4. If nothing on the node can run it → `no-candidate` (the caller emits the
2286
+ * loud once-per-(camera,step) warn that replaces today's silent degrade).
2287
+ *
2288
+ * Framework-free + side-effect-free so the whole decision table is unit-testable
2289
+ * without a live runner/catalog. The `canRun(format)` predicate is injected by
2290
+ * the runner (which owns the model catalog); this module never imports it.
2291
+ */
2292
+ /** The `ModelFormat` of a `<backend>:<device>` key, or `undefined` for an unknown backend. */
2293
+ function formatOf(deviceKey) {
2294
+ const colon = deviceKey.indexOf(":");
2295
+ return require_dist.DEVICE_BACKEND_TO_FORMAT[colon >= 0 ? deviceKey.slice(0, colon) : deviceKey];
2296
+ }
2297
+ /** True when the step's model has a build for `deviceKey`'s format. */
2298
+ function canRunOn(deviceKey, canRun) {
2299
+ const format = formatOf(deviceKey);
2300
+ return format !== void 0 && canRun(format);
2301
+ }
2302
+ /** Effective weighted load of a device (session count ÷ weight); lower = preferred. */
2303
+ function weightedLoad(d) {
2304
+ return d.attachedCameras / (d.weight > 0 ? d.weight : 1);
2305
+ }
2306
+ /**
2307
+ * Weighted-least-loaded pick with a deterministic `deviceKey` tie-break —
2308
+ * mirrors the node/device balancer's decision core (least `load/weight`, ties
2309
+ * broken stably). `devices` is assumed non-empty.
2310
+ */
2311
+ function pickWeightedLeastLoaded(devices) {
2312
+ return devices.reduce((best, d) => {
2313
+ const dScore = weightedLoad(d);
2314
+ const bScore = weightedLoad(best);
2315
+ if (dScore < bScore) return d;
2316
+ if (dScore > bScore) return best;
2317
+ return d.deviceKey < best.deviceKey ? d : best;
2318
+ });
2319
+ }
2320
+ /**
2321
+ * Resolve the device one enrichment step runs on. See the module header for the
2322
+ * decision table. Pure — never throws, no I/O.
2323
+ */
2324
+ function resolveStepDevice(input) {
2325
+ const { effectiveDeviceKey, manualJumpDeviceKey, canRun } = input;
2326
+ if (manualJumpDeviceKey !== void 0 && manualJumpDeviceKey !== effectiveDeviceKey) {
2327
+ const target = input.devices.find((d) => d.deviceKey === manualJumpDeviceKey);
2328
+ if (target && canRunOn(target.deviceKey, canRun)) return {
2329
+ kind: "jumped",
2330
+ deviceKey: target.deviceKey,
2331
+ reason: "manual"
2332
+ };
2333
+ }
2334
+ if (canRunOn(effectiveDeviceKey, canRun)) return {
2335
+ kind: "no-jump",
2336
+ deviceKey: effectiveDeviceKey
2337
+ };
2338
+ const runnable = input.devices.filter((d) => d.deviceKey !== effectiveDeviceKey && (d.maxSessions === null || d.attachedCameras < d.maxSessions) && canRunOn(d.deviceKey, canRun));
2339
+ if (runnable.length === 0) return {
2340
+ kind: "no-candidate",
2341
+ deviceKey: effectiveDeviceKey
2342
+ };
2343
+ return {
2344
+ kind: "jumped",
2345
+ deviceKey: pickWeightedLeastLoaded(runnable).deviceKey,
2346
+ reason: "auto"
2347
+ };
2348
+ }
2349
+ //#endregion
2252
2350
  //#region src/pipeline-runner/index.ts
2253
2351
  var DEFAULT_CONFIG = {
2254
2352
  maxQueueDepth: 30,
@@ -3184,31 +3282,77 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3184
3282
  return { success: true };
3185
3283
  }
3186
3284
  /**
3187
- * Best-effort native-resolution crop for a retention-ring frame (native-crop
3188
- * feature). Looks the handle up in {@link nativeCropRegistry}; a miss (never
3189
- * registered, or the worker's tiny native ring already evicted the frame)
3190
- * returns `null` so the caller falls back to a detection-frame crop. The
3191
- * worker downloads ONLY the ROI — a full native frame never travels the wire.
3285
+ * Native-resolution crop for a retention-ring frame (native-crop feature).
3286
+ * Looks the handle up in {@link nativeCropRegistry}; the worker downloads ONLY
3287
+ * the ROI a full native frame never travels the wire.
3288
+ *
3289
+ * Tier decision ({@link resolveNativeCropSource}): a native-lease HIT always
3290
+ * wins. On a MISS (never registered, or the worker's tiny native ring already
3291
+ * evicted the frame), an ROI (subject-crop) request returns `null` so the
3292
+ * caller reports the miss and RETRIES — it is NEVER handed the ≤640 detection
3293
+ * raster masquerading as a native crop (Task S1). A full-frame request keeps
3294
+ * the ≤640 RAM `RetainedFrameStore` fallback (blank-frame guard / 640-snapshot
3295
+ * resolve legitimately want the lower-RES honest frame).
3192
3296
  */
3193
3297
  async getNativeCrop(input) {
3194
3298
  const b = input.bbox;
3195
- if (!(b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999 && input.maxWidth === void 0)) {
3299
+ const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
3300
+ const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
3301
+ let nativeResult = null;
3302
+ if (!isUnboundedFullFrame) {
3196
3303
  const requester = this.nativeCropRegistry.get(input.handle);
3197
3304
  if (requester) {
3198
3305
  const result = await requester(input.bbox, input.maxWidth);
3199
- if (result) return {
3306
+ if (result) nativeResult = {
3200
3307
  bytes: result.bytes,
3201
3308
  width: result.width,
3202
3309
  height: result.height
3203
3310
  };
3204
3311
  }
3205
3312
  }
3313
+ const tier = resolveNativeCropSource(isFullFrame, nativeResult !== null);
3314
+ if (tier === "native" && nativeResult) return this.encodeNativeCropResult(nativeResult, tier, input.encodeJpeg);
3315
+ if (tier !== "ram-fullframe") return null;
3206
3316
  const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
3207
- return crop ? {
3317
+ if (!crop) return null;
3318
+ return this.encodeNativeCropResult({
3208
3319
  bytes: crop.bytes,
3209
3320
  width: crop.width,
3210
3321
  height: crop.height
3211
- } : null;
3322
+ }, tier, input.encodeJpeg);
3323
+ }
3324
+ /**
3325
+ * Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
3326
+ * return the zero-copy `bytes` — same-node callers encode locally. Compressed
3327
+ * path (`encodeJpeg`, the CROSS-NODE case): encode to JPEG in-process, on the
3328
+ * OWNING node, and return `jpeg` (base64) with NO `bytes` — the payload then
3329
+ * fits Moleculer's `maxPacketSize` and no raw pixels cross the process
3330
+ * boundary (CLAUDE.md invariant #21). On an encode failure the crop is a miss
3331
+ * (`null`) — the caller retries — never a silent raw-RGB cross-node fallback.
3332
+ */
3333
+ async encodeNativeCropResult(crop, tier, encodeJpeg) {
3334
+ if (encodeJpeg !== true) return {
3335
+ bytes: crop.bytes,
3336
+ width: crop.width,
3337
+ height: crop.height,
3338
+ tier
3339
+ };
3340
+ try {
3341
+ const sharp$3 = (await import("sharp")).default;
3342
+ return {
3343
+ jpeg: (await sharp$3(Buffer.from(crop.bytes), { raw: {
3344
+ width: crop.width,
3345
+ height: crop.height,
3346
+ channels: 3
3347
+ } }).jpeg({ quality: 90 }).toBuffer()).toString("base64"),
3348
+ width: crop.width,
3349
+ height: crop.height,
3350
+ tier
3351
+ };
3352
+ } catch (err) {
3353
+ this.ctx.logger.warn("getNativeCrop: cross-node JPEG encode failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
3354
+ return null;
3355
+ }
3212
3356
  }
3213
3357
  /**
3214
3358
  * Two-plane design: run the DETAIL subtree (crop children) for a single
@@ -3223,10 +3367,72 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3223
3367
  return runDetailSubtree({
3224
3368
  resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
3225
3369
  getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
3226
- runPipeline: (steps, imageJpeg, deviceId) => this.runDetailPipeline(steps, imageJpeg, deviceId)
3370
+ runPipeline: (steps, imageJpeg, deviceId, deviceKey) => this.runDetailPipeline(steps, imageJpeg, deviceId, deviceKey),
3371
+ resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step)
3227
3372
  }, input);
3228
3373
  }
3229
3374
  /**
3375
+ * Step-tree device jump (phase 1) — pick which same-node inference device an
3376
+ * enrichment `step` runs on. AUTO-jumps only when the camera's elected device
3377
+ * cannot run the step's model (its format has no build); an operator
3378
+ * `step.jumpDeviceKey` overrides. Returns the resolved `<backend>:<device>`
3379
+ * key, or `undefined` (⇒ the runner's per-camera default pool) when there is
3380
+ * nothing to route: no elected device, or no jump roster on the attach.
3381
+ *
3382
+ * When NO enabled device on the node can run the step, logs a LOUD warn once
3383
+ * per (camera, step) — killing the historical SILENT per-child degrade (an
3384
+ * `edgetpu` root would otherwise drop faces/plates/clip with no signal).
3385
+ */
3386
+ resolveDetailStepDevice(deviceId, step) {
3387
+ const config = this.attached.get(deviceId)?.config;
3388
+ const effectiveDeviceKey = config?.deviceKey;
3389
+ if (!effectiveDeviceKey) return void 0;
3390
+ const roster = config?.inferenceDevices;
3391
+ if (!roster || roster.length < 2) return effectiveDeviceKey;
3392
+ const attachedByKey = this.perDeviceAttachedCounts();
3393
+ const devices = roster.map((d) => ({
3394
+ deviceKey: d.deviceKey,
3395
+ weight: d.weight,
3396
+ maxSessions: d.maxSessions,
3397
+ attachedCameras: attachedByKey.get(d.deviceKey) ?? 0
3398
+ }));
3399
+ const def = tryGetStepDefinition(step.addonId);
3400
+ const canRun = (format) => def === null || def.models.some((m) => m.formats[format]);
3401
+ const result = resolveStepDevice({
3402
+ effectiveDeviceKey,
3403
+ devices,
3404
+ ...step.jumpDeviceKey !== void 0 ? { manualJumpDeviceKey: step.jumpDeviceKey } : {},
3405
+ canRun
3406
+ });
3407
+ if (result.kind === "no-candidate") this.warnStepHasNoRunnableDevice(deviceId, step.addonId, effectiveDeviceKey);
3408
+ return result.deviceKey;
3409
+ }
3410
+ /** Live attached-camera count per inference `deviceKey` on this runner. */
3411
+ perDeviceAttachedCounts() {
3412
+ const counts = /* @__PURE__ */ new Map();
3413
+ for (const cam of this.attached.values()) {
3414
+ const key = cam.config.deviceKey;
3415
+ if (key === void 0) continue;
3416
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3417
+ }
3418
+ return counts;
3419
+ }
3420
+ /** `${deviceId}:${addonId}` pairs already warned about a no-runnable-device step. */
3421
+ warnedNoJumpDevice = /* @__PURE__ */ new Set();
3422
+ warnStepHasNoRunnableDevice(deviceId, addonId, effectiveDeviceKey) {
3423
+ const key = `${deviceId}:${addonId}`;
3424
+ if (this.warnedNoJumpDevice.has(key)) return;
3425
+ this.warnedNoJumpDevice.add(key);
3426
+ this.ctx?.logger.warn("enrichment step has no runnable inference device on this node — producing NO results for it", {
3427
+ tags: { deviceId },
3428
+ meta: {
3429
+ step: addonId,
3430
+ electedDeviceKey: effectiveDeviceKey,
3431
+ hint: "the elected device format has no build of this step, and no enabled same-node device can run it — enable a compatible accelerator or add a jumpDeviceKey"
3432
+ }
3433
+ });
3434
+ }
3435
+ /**
3230
3436
  * Real `resolveChildSteps` dep: `null` when the camera isn't attached
3231
3437
  * (matches the cap doc — "camera not attached" is a distinct case from
3232
3438
  * "attached but no matching child steps", which returns an empty array
@@ -3248,16 +3454,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3248
3454
  async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
3249
3455
  const native = await this.getNativeCrop({
3250
3456
  handle,
3251
- bbox: paddedBbox
3457
+ bbox: paddedBbox,
3458
+ encodeJpeg: true
3252
3459
  });
3253
- if (!native) return null;
3254
- const sharp$3 = (await import("sharp")).default;
3460
+ if (!native?.jpeg) return null;
3255
3461
  return {
3256
- jpeg: await sharp$3(Buffer.from(native.bytes), { raw: {
3257
- width: native.width,
3258
- height: native.height,
3259
- channels: 3
3260
- } }).jpeg({ quality: 90 }).toBuffer(),
3462
+ jpeg: Buffer.from(native.jpeg, "base64"),
3261
3463
  width: native.width,
3262
3464
  height: native.height,
3263
3465
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
@@ -3317,10 +3519,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3317
3519
  })();
3318
3520
  }
3319
3521
  /** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
3320
- async runDetailPipeline(steps, imageJpeg, deviceId) {
3522
+ async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey) {
3321
3523
  const api = this.ctxIfReady?.api;
3322
3524
  if (!api) return null;
3323
- const detailDeviceKey = this.attached.get(deviceId)?.config?.deviceKey;
3525
+ const detailDeviceKey = stepDeviceKey ?? this.attached.get(deviceId)?.config?.deviceKey;
3324
3526
  try {
3325
3527
  return await api.pipelineExecutor.runPipeline.mutate({
3326
3528
  steps: [...steps],
@@ -3397,6 +3599,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3397
3599
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
3398
3600
  this.thumbSampler?.forget(deviceId);
3399
3601
  this.packageGovernor.forget(deviceId);
3602
+ const warnPrefix = `${deviceId}:`;
3603
+ for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
3400
3604
  this.attached.delete(deviceId);
3401
3605
  this.lastMotionAt.delete(deviceId);
3402
3606
  this.lastEmittedCameraMetrics.delete(deviceId);