@camstack/addon-pipeline 1.2.101 → 1.2.103

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 (32) hide show
  1. package/dist/{addon-utils-eUS6n_Zj.js → addon-utils-CLc6yHCN.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +163 -42
  5. package/dist/detection-pipeline/index.mjs +161 -40
  6. package/dist/{dist-dm3t4BOt.js → dist-C11WuNUP.js} +87 -51
  7. package/dist/{dist-gXdWP96z.mjs → dist-Ccmt3fGJ.mjs} +82 -52
  8. package/dist/{event-loop-stall-monitor-Lki2_mYY.js → event-loop-stall-monitor-Cq_NeC4o.js} +175 -132
  9. package/dist/{event-loop-stall-monitor-TnQ21mu0.mjs → event-loop-stall-monitor-OJrOMeuu.mjs} +170 -133
  10. package/dist/{lazy-sharp-oYppDIZR.js → lazy-sharp-RxUs6on_.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +5 -4
  14. package/dist/pipeline-runner/index.mjs +4 -3
  15. package/dist/{process-memory-CKWh609B.mjs → process-memory-D0zDmXLI.mjs} +1 -1
  16. package/dist/{process-memory-vkFVOdkV.js → process-memory-DOjQ3MgC.js} +1 -1
  17. package/dist/recorder/index.js +2 -2
  18. package/dist/recorder/index.mjs +1 -1
  19. package/dist/session-decode/decode-worker-child.js +2 -2
  20. package/dist/session-decode/decode-worker-child.mjs +1 -1
  21. package/dist/stream-broker/_stub.js +1 -1
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C4fRkYcj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BdgcF1lL.mjs} +2 -2
  23. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zT-cYUdF.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DY31bUCj.mjs} +1 -1
  24. package/dist/stream-broker/{hostInit-CS6xpCdb.mjs → hostInit-Da9wVA2r.mjs} +2 -2
  25. package/dist/stream-broker/index.js +31 -11
  26. package/dist/stream-broker/index.mjs +31 -11
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-BrlrMxXu.js → worker-protocol-BePduZVV.js} +1 -1
  29. package/dist/{worker-protocol-DJWdPLKF.mjs → worker-protocol-DDpliBIW.mjs} +1 -1
  30. package/package.json +1 -1
  31. package/python/postprocessors/ctc.py +56 -3
  32. package/python/postprocessors/test_ctc.py +37 -0
@@ -1,8 +1,8 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { Ct as nodePin, Dt as sleep, Ft as object, H as evaluateZoneRules, Lt as string, N as defaultDeviceFor$1, R as detectionPipelineCapability, Rt as union, T as YAMNET_TO_MACRO, V as enumerateInferenceDevices, X as pipelineExecutorCapability, ct as errMsg, d as DEVICE_BACKEND_TO_FORMAT, kt as array, mt as BaseAddon, ot as supportedRuntimes$1, rt as runtimeDevices$1, t as APPLE_SA_TO_MACRO, tt as resolvePoolMemoryPolicy, vt as createEvent, wt as parseJsonUnknown, y as PoolMemoryWatchdog, yt as hydrateSchema, zt as EventCategory } from "../dist-gXdWP96z.mjs";
3
- import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-TnQ21mu0.mjs";
2
+ import { At as array, Bt as EventCategory, H as enumerateInferenceDevices, It as object, Ot as sleep, P as defaultDeviceFor$1, Rt as string, T as YAMNET_TO_MACRO, Tt as parseJsonUnknown, U as evaluateZoneRules, Z as pipelineExecutorCapability, bt as hydrateSchema, d as DEVICE_BACKEND_TO_FORMAT, ht as BaseAddon, it as runtimeDevices$1, lt as errMsg, nt as resolvePoolMemoryPolicy, st as supportedRuntimes$1, t as APPLE_SA_TO_MACRO, wt as nodePin, y as PoolMemoryWatchdog, yt as createEvent, z as detectionPipelineCapability, zt as union } from "../dist-Ccmt3fGJ.mjs";
3
+ import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep, u as landmarkPrecisionVerdict } from "../event-loop-stall-monitor-OJrOMeuu.mjs";
4
4
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
5
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-CKWh609B.mjs";
5
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-D0zDmXLI.mjs";
6
6
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-A2S9D7pu.mjs";
7
7
  import sharp from "sharp";
8
8
  import { spawn } from "node:child_process";
@@ -3033,7 +3033,9 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
3033
3033
  * peaky (a few high rows) rather than smeared across many rows.
3034
3034
  * 3. pick the angle maximising the score (coarse 2° sweep + a 0.5° local
3035
3035
  * refinement); apply it to the full-resolution RGB tile only when it is
3036
- * meaningfully non-zero.
3036
+ * meaningfully non-zero AND the optimum was BRACKETED by the sweep — an
3037
+ * argmax sitting on the boundary means the profile never turned over and
3038
+ * the "angle" is an artefact of where the search stopped.
3037
3039
  *
3038
3040
  * All of the geometry is pure functions over raw buffers, so the estimator is
3039
3041
  * fully deterministic and unit-testable without any image I/O.
@@ -3212,22 +3214,28 @@ function scoreAngle(gradient, angleRad) {
3212
3214
  height: rotated.height
3213
3215
  });
3214
3216
  }
3217
+ var DEFAULT_COARSE_STEP_DEG = 2;
3218
+ var NO_SKEW = {
3219
+ angleDeg: 0,
3220
+ score: 0
3221
+ };
3215
3222
  /**
3216
3223
  * Estimate the dominant text-baseline skew of a grayscale plate crop and return
3217
3224
  * the CORRECTION angle (degrees) that levels it. A coarse sweep over
3218
3225
  * ±`maxAngleDeg` in `coarseStepDeg` steps finds the best bucket; a `fineStepDeg`
3219
3226
  * local search refines it. Returns `0` when the flat (no-rotation) profile is
3220
- * already the strongest response.
3227
+ * already the strongest response, and — critically — also when the coarse
3228
+ * winner is not BRACKETED by the sweep (see {@link BOUNDARY_REJECT_DEG}): an
3229
+ * optimum sitting on the edge of the searched interval is not a measurement,
3230
+ * it is the place the search ran out of road.
3221
3231
  */
3222
3232
  function estimateSkewAngle(gray, opts = {}) {
3223
- const maxAngleDeg = opts.maxAngleDeg ?? 30;
3224
- const coarseStepDeg = opts.coarseStepDeg ?? 2;
3233
+ const maxAngleDeg = opts.maxAngleDeg ?? 16;
3234
+ const coarseStepDeg = opts.coarseStepDeg ?? DEFAULT_COARSE_STEP_DEG;
3225
3235
  const fineStepDeg = opts.fineStepDeg ?? .5;
3226
3236
  const scoreMaxDim = opts.scoreMaxDim ?? 480;
3227
- if (gray.width < 4 || gray.height < 4) return {
3228
- angleDeg: 0,
3229
- score: 0
3230
- };
3237
+ const boundaryRejectDeg = Math.max(0, maxAngleDeg - coarseStepDeg);
3238
+ if (gray.width < 4 || gray.height < 4) return NO_SKEW;
3231
3239
  const gradient = sobelMagnitude(downscaleGray(gray, scoreMaxDim));
3232
3240
  let bestAngle = 0;
3233
3241
  let bestScore = scoreAngle(gradient, 0);
@@ -3239,6 +3247,7 @@ function estimateSkewAngle(gray, opts = {}) {
3239
3247
  bestAngle = deg;
3240
3248
  }
3241
3249
  }
3250
+ if (Math.abs(bestAngle) >= boundaryRejectDeg) return NO_SKEW;
3242
3251
  const lo = bestAngle - coarseStepDeg;
3243
3252
  const hi = bestAngle + coarseStepDeg;
3244
3253
  for (let deg = lo; deg <= hi + 1e-9; deg += fineStepDeg) {
@@ -3254,6 +3263,50 @@ function estimateSkewAngle(gray, opts = {}) {
3254
3263
  score: bestScore
3255
3264
  };
3256
3265
  }
3266
+ function toSharpChannels(channels) {
3267
+ switch (channels) {
3268
+ case 1: return 1;
3269
+ case 2: return 2;
3270
+ case 4: return 4;
3271
+ default: return 3;
3272
+ }
3273
+ }
3274
+ /**
3275
+ * Cut an expand-rotated canvas back to the LEVELLED SOURCE BAND.
3276
+ *
3277
+ * `rotateRaw(expand: true)` grows the canvas to `w·|cos| + h·|sin|` ×
3278
+ * `w·|sin| + h·|cos|` so no corner is clipped — but the `|sin|` terms are pure
3279
+ * corner spread: flat fill above/below (and left/right of) the plate. Handing
3280
+ * that to OCR is not free. The recognizer letterboxes the whole tile into a
3281
+ * fixed 64px-tall input, so a 155px crop returned at 194px shrinks every glyph
3282
+ * by ~20% before a single character is read — the warp paying back part of what
3283
+ * it just won.
3284
+ *
3285
+ * The band kept is the source rectangle scaled by `|cos angle|`, centred: a
3286
+ * source edge of length L, once levelled, spans `L·|cos|` along its own axis.
3287
+ * Everything outside is the fill the rotation introduced.
3288
+ */
3289
+ function cropLevelledBand(rotated, srcWidth, srcHeight, angleRad) {
3290
+ const c = Math.abs(Math.cos(angleRad));
3291
+ const bw = Math.max(1, Math.min(rotated.width, Math.round(srcWidth * c)));
3292
+ const bh = Math.max(1, Math.min(rotated.height, Math.round(srcHeight * c)));
3293
+ if (bw === rotated.width && bh === rotated.height) return rotated;
3294
+ const ch = rotated.channels;
3295
+ const x0 = Math.max(0, Math.floor((rotated.width - bw) / 2));
3296
+ const y0 = Math.max(0, Math.floor((rotated.height - bh) / 2));
3297
+ const out = new Uint8Array(bw * bh * ch);
3298
+ const rowBytes = bw * ch;
3299
+ for (let y = 0; y < bh; y++) {
3300
+ const srcOff = ((y0 + y) * rotated.width + x0) * ch;
3301
+ out.set(rotated.data.subarray(srcOff, srcOff + rowBytes), y * rowBytes);
3302
+ }
3303
+ return {
3304
+ data: out,
3305
+ width: bw,
3306
+ height: bh,
3307
+ channels: ch
3308
+ };
3309
+ }
3257
3310
  /**
3258
3311
  * Rectify an oblique plate crop: decode the JPEG, estimate the baseline skew,
3259
3312
  * and — only when it exceeds {@link MIN_RECTIFY_ANGLE_DEG} — rotate the
@@ -3281,19 +3334,20 @@ async function rectifyPlateJpeg(jpeg, opts = {}) {
3281
3334
  let mean = 0;
3282
3335
  for (let i = 0; i < gray.data.length; i++) mean += gray.data[i];
3283
3336
  const fill = gray.data.length > 0 ? mean / gray.data.length | 0 : 0;
3284
- const rotated = rotateRaw(rgb, angleDeg * DEG2RAD, {
3337
+ const angleRad = angleDeg * DEG2RAD;
3338
+ const band = cropLevelledBand(rotateRaw(rgb, angleRad, {
3285
3339
  expand: true,
3286
3340
  fill
3287
- });
3341
+ }), rgb.width, rgb.height, angleRad);
3288
3342
  try {
3289
3343
  return {
3290
- jpeg: await sharp(Buffer.from(rotated.data), { raw: {
3291
- width: rotated.width,
3292
- height: rotated.height,
3293
- channels: rotated.channels
3344
+ jpeg: await sharp(Buffer.from(band.data), { raw: {
3345
+ width: band.width,
3346
+ height: band.height,
3347
+ channels: toSharpChannels(band.channels)
3294
3348
  } }).jpeg({ quality: opts.quality ?? 90 }).toBuffer(),
3295
- width: rotated.width,
3296
- height: rotated.height,
3349
+ width: band.width,
3350
+ height: band.height,
3297
3351
  angleDeg
3298
3352
  };
3299
3353
  } catch {
@@ -3302,6 +3356,12 @@ async function rectifyPlateJpeg(jpeg, opts = {}) {
3302
3356
  }
3303
3357
  //#endregion
3304
3358
  //#region src/detection-pipeline/pipeline/result-assembler.ts
3359
+ /** L2 norm of a raw embedding vector. */
3360
+ function vectorMagnitude(v) {
3361
+ let sum = 0;
3362
+ for (const x of v) sum += x * x;
3363
+ return Math.sqrt(sum);
3364
+ }
3305
3365
  /**
3306
3366
  * Small deterministic id generator scoped to a single frame. Each call
3307
3367
  * returns the next `d{n}` id so detail parentId references can be set
@@ -3395,6 +3455,7 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
3395
3455
  case "embedding":
3396
3456
  parent.embedding = output.embeddingNorm;
3397
3457
  parent.embeddingModelId = childStep.modelId;
3458
+ parent.embeddingMagnitude = vectorMagnitude(output.embedding);
3398
3459
  parent.ownLabels.push({
3399
3460
  label: `embedding:${output.embedding.length}d`,
3400
3461
  score: 1
@@ -3617,20 +3678,50 @@ var MIN_FACE_DETECTION_FLOOR_PX = 6;
3617
3678
  */
3618
3679
  var NATIVE_FACE_CROP_MAX_WIDTH = 320;
3619
3680
  /**
3620
- * Minimum short side (px) of the ACTUAL crop fed to the plate-OCR (`ctc`) leaf
3621
- * the plate analogue of the face path's min-face-size gate. Below ~20px the
3622
- * CTC logit map collapses toward the blank and the surviving timesteps emit
3623
- * symbol junk ("[miv4o)", "6ou.2" live-observed 2026-07-22). Measured on the
3624
- * NATIVE crop when the native ROI fetch succeeded (its dimensions ARE the
3625
- * plate's real native size `nativeChildCrop`'s 1920 cap never binds for a
3626
- * plate), else on the fallback detection-space tile which IS the input OCR
3627
- * would read, so the same floor applies honestly in both cases.
3681
+ * Minimum LONG side (px) of the ACTUAL crop fed to the plate-OCR (`ctc`) leaf
3682
+ * the plate analogue of the face path's min-face-size gate.
3683
+ *
3684
+ * The long side, not the short one. A plate's information lives along its text
3685
+ * run: the recognizer resizes the tile to a fixed 64px height and reads left to
3686
+ * right, so what decides whether a glyph survives is how many horizontal pixels
3687
+ * each character got. The previous 20px SHORT-side floor measured the wrong
3688
+ * axis and passed essentially everything.
3689
+ *
3690
+ * The number comes from the 2026-08 audit of 11 cam-617 plate tracks with
3691
+ * ground truth. Native long side vs read:
3692
+ *
3693
+ * 117px → "DNRO3O9" (truth DNRO309 — one glyph out, the only near-miss)
3694
+ * 82px → "VONND9592" (truth DNDS597)
3695
+ * 77px → "DEPNOQ1" (truth DNP1091)
3696
+ * 76px → "VDHPXOE8" (truth DNPK838)
3697
+ * 73px → "DAEOR2D8" (truth DNDR298)
3698
+ * ≤62px → "I0IAZ", "EUSM9", "0IA3", "DYAGI2", "I0AA", "IC1E"
3699
+ *
3700
+ * Not one read under 100px resembled its plate, and the gap between 82 and 117
3701
+ * is empty — 100 sits in it. Below this floor the OCR call is pure cost: it
3702
+ * produces a label that is wrong, and a wrong plate label is worse than none.
3703
+ *
3704
+ * Measured on the NATIVE crop when the native ROI fetch succeeded (its
3705
+ * dimensions ARE the plate's real native size — `nativeChildCrop`'s 1920 cap
3706
+ * never binds for a plate), else on the fallback detection-space tile — which
3707
+ * IS the input OCR would read, so the same floor applies honestly in both cases.
3628
3708
  */
3629
- var MIN_PLATE_OCR_SHORT_SIDE_PX = 20;
3709
+ var MIN_PLATE_OCR_LONG_SIDE_PX = 100;
3630
3710
  function isEnrichmentOutput(output) {
3631
3711
  return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
3632
3712
  }
3633
3713
  /**
3714
+ * Read a numeric step setting, falling back to `fallback` when the key is
3715
+ * absent or not a finite number. A type guard rather than a cast — a step's
3716
+ * `settings` bag is `Record<string, unknown>` because it is operator data, and
3717
+ * asserting `as number` over it is how a string from a form becomes a silent
3718
+ * NaN comparison that admits everything.
3719
+ */
3720
+ function numericSetting(settings, key, fallback) {
3721
+ const raw = settings?.[key];
3722
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
3723
+ }
3724
+ /**
3634
3725
  * Stamp the camera onto an inference input so a failure inside the SHARED pool
3635
3726
  * can name it (`InferenceInput.deviceId` — diagnostic only, it selects
3636
3727
  * nothing). Immutable, and a no-op for a caller with no real camera: the
@@ -4071,12 +4162,27 @@ var PipelineExecutor = class {
4071
4162
  let cropW;
4072
4163
  let cropH;
4073
4164
  if (modelEntry?.faceAlignment) {
4074
- const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
4165
+ const minFaceSize = numericSetting(child.settings, "minFaceSize", DEFAULT_MIN_FACE_SIZE_PX);
4075
4166
  const detShortSide = bboxShortSide(parentDetection.bbox);
4076
4167
  if (detShortSide < MIN_FACE_DETECTION_FLOOR_PX) {
4077
4168
  this.warnFaceTooSmall(child, detShortSide, minFaceSize, void 0, deviceId);
4078
4169
  continue;
4079
4170
  }
4171
+ const precision = landmarkPrecisionVerdict({
4172
+ detectionShortSidePx: detShortSide,
4173
+ floorPx: numericSetting(child.settings, "minLandmarkFaceSize", 24)
4174
+ });
4175
+ if (precision !== null) {
4176
+ this.throttledWarn(`face-landmark-floor:${child.stepId}:${deviceId}`, 3e4, "face detected but its landmarks are below the precision floor — no embedding", {
4177
+ step: child.stepId,
4178
+ model: child.modelId,
4179
+ detectionShortSidePx: Math.round(precision.detectionShortSidePx),
4180
+ minLandmarkFaceSizePx: precision.floorPx,
4181
+ tileW: imageWidth,
4182
+ tileH: imageHeight
4183
+ }, deviceId);
4184
+ continue;
4185
+ }
4080
4186
  const lms = parentDetection.landmarks;
4081
4187
  if (lms && lms.length >= 5) {
4082
4188
  if (!nativeCropProvider && detShortSide < minFaceSize) {
@@ -4140,15 +4246,16 @@ var PipelineExecutor = class {
4140
4246
  cropH = crop.height;
4141
4247
  }
4142
4248
  if (child.definition.postprocessor === "ctc") {
4143
- const plateShortSidePx = Math.min(cropW, cropH);
4144
- if (plateShortSidePx < MIN_PLATE_OCR_SHORT_SIDE_PX) {
4145
- this.opts.logger?.debug("plate ROI too small for OCR — skipping", {
4249
+ const plateLongSidePx = Math.max(cropW, cropH);
4250
+ if (plateLongSidePx < MIN_PLATE_OCR_LONG_SIDE_PX) {
4251
+ this.opts.logger?.info("plate ROI too small for OCR — skipping", {
4146
4252
  tags: { deviceId },
4147
4253
  meta: {
4148
4254
  step: child.stepId,
4149
4255
  model: child.modelId,
4150
- plateShortSidePx: Math.round(plateShortSidePx),
4151
- minPlateShortSidePx: MIN_PLATE_OCR_SHORT_SIDE_PX,
4256
+ plateLongSidePx: Math.round(plateLongSidePx),
4257
+ plateShortSidePx: Math.round(Math.min(cropW, cropH)),
4258
+ minPlateLongSidePx: MIN_PLATE_OCR_LONG_SIDE_PX,
4152
4259
  source: nativeChild ? "native" : "detection-tile"
4153
4260
  }
4154
4261
  });
@@ -4161,12 +4268,15 @@ var PipelineExecutor = class {
4161
4268
  cropJpegBuf = rectified.jpeg;
4162
4269
  cropW = rectified.width;
4163
4270
  cropH = rectified.height;
4164
- this.opts.logger?.debug("Plate crop deskewed before OCR", { meta: {
4165
- stepId: child.definition.id,
4166
- angleDeg: Number(rectified.angleDeg.toFixed(2)),
4167
- width: cropW,
4168
- height: cropH
4169
- } });
4271
+ this.opts.logger?.info("plate crop deskewed before OCR", {
4272
+ tags: { deviceId },
4273
+ meta: {
4274
+ step: child.stepId,
4275
+ angleDeg: Number(rectified.angleDeg.toFixed(2)),
4276
+ width: cropW,
4277
+ height: cropH
4278
+ }
4279
+ });
4170
4280
  }
4171
4281
  } catch {}
4172
4282
  }
@@ -4176,6 +4286,17 @@ var PipelineExecutor = class {
4176
4286
  }, deviceId), cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
4177
4287
  const childMs = Date.now() - childStart;
4178
4288
  if (childOutput === null) continue;
4289
+ if (child.definition.postprocessor === "ctc" && childOutput.kind === "text") this.opts.logger?.info("plate OCR read", {
4290
+ tags: { deviceId },
4291
+ meta: {
4292
+ step: child.stepId,
4293
+ model: child.modelId,
4294
+ text: childOutput.text,
4295
+ confidence: Number(childOutput.confidence.toFixed(3)),
4296
+ formatValid: childOutput.formatValid ?? null,
4297
+ tilePx: `${cropW}x${cropH}`
4298
+ }
4299
+ });
4179
4300
  const detailsBefore = ctx.details.length;
4180
4301
  applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
4181
4302
  if (childOutput.kind === "detections" && child.children.length > 0) {
@@ -15342,128 +15342,103 @@ var COCO_TO_MACRO = {
15342
15342
  var AUDIO_MACRO_LABELS = [
15343
15343
  {
15344
15344
  id: "speech",
15345
- name: "Speech",
15346
- icon: "🗣️"
15345
+ name: "Speech"
15347
15346
  },
15348
15347
  {
15349
15348
  id: "scream",
15350
- name: "Scream / Shout",
15351
- icon: "😱"
15349
+ name: "Scream / Shout"
15352
15350
  },
15353
15351
  {
15354
15352
  id: "crying",
15355
- name: "Crying / Baby",
15356
- icon: "😢"
15353
+ name: "Crying / Baby"
15357
15354
  },
15358
15355
  {
15359
15356
  id: "laughter",
15360
- name: "Laughter",
15361
- icon: "😂"
15357
+ name: "Laughter"
15362
15358
  },
15363
15359
  {
15364
15360
  id: "music",
15365
- name: "Music",
15366
- icon: "🎵"
15361
+ name: "Music"
15367
15362
  },
15368
15363
  {
15369
15364
  id: "dog",
15370
- name: "Dog",
15371
- icon: "🐕"
15365
+ name: "Dog"
15372
15366
  },
15373
15367
  {
15374
15368
  id: "cat",
15375
- name: "Cat",
15376
- icon: "🐈"
15369
+ name: "Cat"
15377
15370
  },
15378
15371
  {
15379
15372
  id: "bird",
15380
- name: "Bird",
15381
- icon: "🐦"
15373
+ name: "Bird"
15382
15374
  },
15383
15375
  {
15384
15376
  id: "animal",
15385
- name: "Animal (other)",
15386
- icon: "🐾"
15377
+ name: "Animal (other)"
15387
15378
  },
15388
15379
  {
15389
15380
  id: "alarm",
15390
- name: "Alarm / Siren",
15391
- icon: "🚨"
15381
+ name: "Alarm / Siren"
15392
15382
  },
15393
15383
  {
15394
15384
  id: "doorbell",
15395
- name: "Doorbell / Knock",
15396
- icon: "🔔"
15385
+ name: "Doorbell / Knock"
15397
15386
  },
15398
15387
  {
15399
15388
  id: "glass_breaking",
15400
- name: "Glass Breaking",
15401
- icon: "💥"
15389
+ name: "Glass Breaking"
15402
15390
  },
15403
15391
  {
15404
15392
  id: "gunshot",
15405
- name: "Gunshot / Explosion",
15406
- icon: "💣"
15393
+ name: "Gunshot / Explosion"
15407
15394
  },
15408
15395
  {
15409
15396
  id: "vehicle",
15410
- name: "Vehicle",
15411
- icon: "🚗"
15397
+ name: "Vehicle"
15412
15398
  },
15413
15399
  {
15414
15400
  id: "siren",
15415
- name: "Emergency Siren",
15416
- icon: "🚑"
15401
+ name: "Emergency Siren"
15417
15402
  },
15418
15403
  {
15419
15404
  id: "fire",
15420
- name: "Fire / Smoke",
15421
- icon: "🔥"
15405
+ name: "Fire / Smoke"
15422
15406
  },
15423
15407
  {
15424
15408
  id: "water",
15425
- name: "Water",
15426
- icon: "💧"
15409
+ name: "Water"
15427
15410
  },
15428
15411
  {
15429
15412
  id: "wind",
15430
- name: "Wind / Weather",
15431
- icon: "🌬️"
15413
+ name: "Wind / Weather"
15432
15414
  },
15433
15415
  {
15434
15416
  id: "door",
15435
- name: "Door",
15436
- icon: "🚪"
15417
+ name: "Door"
15437
15418
  },
15438
15419
  {
15439
15420
  id: "footsteps",
15440
- name: "Footsteps",
15441
- icon: "👣"
15421
+ name: "Footsteps"
15442
15422
  },
15443
15423
  {
15444
15424
  id: "crowd",
15445
- name: "Crowd / Chatter",
15446
- icon: "👥"
15425
+ name: "Crowd / Chatter"
15447
15426
  },
15448
15427
  {
15449
15428
  id: "telephone",
15450
- name: "Telephone",
15451
- icon: "📞"
15429
+ name: "Telephone"
15452
15430
  },
15453
15431
  {
15454
15432
  id: "engine",
15455
- name: "Engine / Motor",
15456
- icon: "⚙️"
15433
+ name: "Engine / Motor"
15457
15434
  },
15458
15435
  {
15459
15436
  id: "tools",
15460
- name: "Tools / Construction",
15461
- icon: "🔨"
15437
+ name: "Tools / Construction"
15462
15438
  },
15463
15439
  {
15464
15440
  id: "silence",
15465
- name: "Silence",
15466
- icon: "🤫"
15441
+ name: "Silence"
15467
15442
  }
15468
15443
  ];
15469
15444
  var YAMNET_TO_MACRO = {
@@ -19727,6 +19702,12 @@ var DetailResultSchema = object({
19727
19702
  /** FRAME-space bbox (already mapped back from crop space). */
19728
19703
  bbox: NativeCropBboxSchema.optional(),
19729
19704
  embedding: string().optional(),
19705
+ /**
19706
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
19707
+ * so an older runner that does not send it degrades to "unmeasurable", which
19708
+ * the consuming gate treats as accept — never as reject.
19709
+ */
19710
+ embeddingMagnitude: number().optional(),
19730
19711
  label: string().optional(),
19731
19712
  /**
19732
19713
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -35841,12 +35822,61 @@ Object.freeze({
35841
35822
  "network-access": "ingress",
35842
35823
  "smtp-provider": "email"
35843
35824
  });
35825
+ /**
35826
+ * audio-condition — the authoring logic behind the sustained-sound condition
35827
+ * (`NcConditions.audio`), pure and UI-framework-free.
35828
+ *
35829
+ * WHY IT LIVES HERE
35830
+ * ─────────────────
35831
+ * Same reason as `condition-taxonomy.ts` next door: two editors author this
35832
+ * condition (the admin UI and the viewer), the vocabulary it offers is already
35833
+ * in this package (`AUDIO_MACRO_LABELS`), and a second copy of "what a cleared
35834
+ * threshold writes" is a mirror free to drift. The viewer keeps its own copy
35835
+ * because it is a separate app with its own toolchain; every surface that CAN
35836
+ * import `@camstack/types` uses this one.
35837
+ *
35838
+ * Four behaviours here are not cosmetic:
35839
+ *
35840
+ * 1. **dBFS is negative-going.** `0` is full scale; digital silence reads
35841
+ * {@link NC_AUDIO_DBFS_FLOOR}. A control that let an operator author `60`
35842
+ * meaning "loud" would write a threshold no sample can reach, and the rule
35843
+ * would look broken rather than mis-configured. Every write is clamped.
35844
+ * 2. **Neither filter given = the engine REFUSES.** With no level floor and no
35845
+ * labels every sample is trivially a hit, so the window would fire on
35846
+ * silence. {@link audioIsFailClosed} is how an editor says that BEFORE the
35847
+ * save instead of shipping a rule that never notifies and never says why.
35848
+ * 3. **Both label spellings are the same label.** The hub's taxonomy serves
35849
+ * `audio-dog`; the macro catalog and an operator both say `dog`, and the
35850
+ * matcher normalizes the prefix away on both sides. Selection therefore
35851
+ * compares NORMALIZED ids — a rule authored in the admin UI must not read
35852
+ * as "nothing selected" on the phone.
35853
+ * 4. **A stored label the build does not know stays visible.** It is offered
35854
+ * as an explicitly-unknown, removable choice, never silently dropped:
35855
+ * rewriting the rule the operator is looking at is worse than showing a
35856
+ * stale id.
35857
+ */
35858
+ /** The `audio-` namespace the NC taxonomy uses for these same macro ids. */
35859
+ var AUDIO_KIND_PREFIX = "audio-";
35860
+ /**
35861
+ * The inverse: a macro id in the taxonomy's namespace (`dog` → `audio-dog`),
35862
+ * which is the `iconId` every UI resolves to a glyph (`EVENT_KIND_ICONS` in
35863
+ * `@camstack/ui-library`, `event-kind-icons.ts` in the viewer). Idempotent, so
35864
+ * a value that already carries the namespace is passed through unchanged.
35865
+ *
35866
+ * This lives here — beside `normalizeAudioLabel` and next to the vocabulary
35867
+ * itself — rather than in a UI package, because the *id* is data and the *glyph*
35868
+ * is presentation: the server puts the id on a config option, and whichever
35869
+ * renderer picks it up (lucide-react on the web, lucide-react-native in the
35870
+ * viewer) supplies its own component for it.
35871
+ */
35872
+ function audioKindId(macroId) {
35873
+ return macroId.startsWith(AUDIO_KIND_PREFIX) ? macroId : `${AUDIO_KIND_PREFIX}${macroId}`;
35874
+ }
35844
35875
  /** Schema defaults — an untouched sub-field must author exactly these. */
35845
35876
  var NC_AUDIO_DEFAULTS = {
35846
35877
  hitPercent: 60,
35847
35878
  samplingSeconds: 10
35848
35879
  };
35849
- new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
35850
35880
  new Set(["devices", "classes"]);
35851
35881
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
35852
35882
  /**
@@ -37367,6 +37397,12 @@ Object.defineProperty(exports, "audioAnalyzerCapability", {
37367
37397
  return audioAnalyzerCapability;
37368
37398
  }
37369
37399
  });
37400
+ Object.defineProperty(exports, "audioKindId", {
37401
+ enumerable: true,
37402
+ get: function() {
37403
+ return audioKindId;
37404
+ }
37405
+ });
37370
37406
  Object.defineProperty(exports, "batteryCapability", {
37371
37407
  enumerable: true,
37372
37408
  get: function() {