@camstack/addon-pipeline 1.2.622 → 1.2.624

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 (41) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-CKCK-QcT.js → custom-model-catalog-6zwaRe6H.js} +24 -11
  4. package/dist/{custom-model-catalog-wpMi2U7e.mjs → custom-model-catalog-Dx6W1kAo.mjs} +24 -5
  5. package/dist/detection-pipeline/index.js +16 -6
  6. package/dist/detection-pipeline/index.mjs +16 -6
  7. package/dist/{dist-DUFGDd_2.js → dist-63VVVhpw.js} +1371 -240
  8. package/dist/{dist-BF60SjmI.mjs → dist-BK3z-nti.mjs} +1311 -240
  9. package/dist/{model-catalogs-CqFDhb0B.mjs → model-catalogs-CllYj2Q2.mjs} +1 -1
  10. package/dist/{model-catalogs-CjTm_0O-.js → model-catalogs-DyOMg-xZ.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-DkphOOM2.mjs → node-BAcWHimZ.mjs} +2 -2
  14. package/dist/{node-DzGVn7bD.js → node-DdaBnHEq.js} +2 -2
  15. package/dist/pipeline-runner/index.js +101 -460
  16. package/dist/pipeline-runner/index.mjs +94 -453
  17. package/dist/{process-memory-8kKexHYR.js → process-memory-CUlknrLl.js} +1 -1
  18. package/dist/{process-memory-CBzfdSvC.mjs → process-memory-OZwib1pK.mjs} +1 -1
  19. package/dist/recorder/index.js +21 -2
  20. package/dist/recorder/index.mjs +21 -2
  21. package/dist/{segment-demux-js-DQNoNSJt.mjs → segment-demux-js-C3rhCLCG.mjs} +1 -1
  22. package/dist/{segment-demux-js-DRLQb1pc.js → segment-demux-js-WNlg0SXP.js} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BmANAxAe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Di7t7N3r.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DDRGMoJ1.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CPQe09h1.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-B3hXAfr1.mjs → hostInit-CMDvBrqv.mjs} +2 -2
  30. package/dist/stream-broker/index.js +4 -4
  31. package/dist/stream-broker/index.mjs +4 -4
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/accelerator_facts.py +275 -0
  35. package/python/inference_pool.py +19 -2
  36. package/python/postprocessors/ssd.py +12 -4
  37. package/python/postprocessors/test_ssd.py +64 -0
  38. package/python/test_accelerator_facts.py +105 -0
  39. package/python/webgpu_isolated.py +19 -0
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-H6RKsaiZ.mjs +0 -26
  41. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CNoWeZHH.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { Kn as hfModelUrl, f as COCO_80_LABELS } from "./dist-BF60SjmI.mjs";
1
+ import { f as COCO_80_LABELS, nr as hfModelUrl } from "./dist-BK3z-nti.mjs";
2
2
  //#region src/detection-pipeline/registry/model-licenses.ts
3
3
  var COCO_DATA = {
4
4
  name: "COCO 2017",
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DUFGDd_2.js");
1
+ const require_dist = require("./dist-63VVVhpw.js");
2
2
  //#region src/detection-pipeline/registry/model-licenses.ts
3
3
  var COCO_DATA = {
4
4
  name: "COCO 2017",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DUFGDd_2.js");
5
+ const require_dist = require("../dist-63VVVhpw.js");
6
6
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Fn as BaseAddon, Kt as motionDetectionCapability, Mn as DeviceType, qn as hydrateSchema, wt as evaluateZoneRules } from "../dist-BF60SjmI.mjs";
1
+ import { Gn as BaseAddon, Hn as DeviceType, Ot as evaluateZoneRules, Zt as motionDetectionCapability, rr as hydrateSchema } from "../dist-BK3z-nti.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -1,4 +1,4 @@
1
- import { An as isSoftwareDecode, Dn as buildFfmpegArgs, En as Fmp4BoxSplitter, Nn as errMsg } from "./dist-BF60SjmI.mjs";
1
+ import { Bn as isSoftwareDecode, In as Fmp4BoxSplitter, Ln as buildFfmpegArgs, Un as errMsg } from "./dist-BK3z-nti.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
@@ -338,7 +338,7 @@ var Fmp4FragmentChild = class {
338
338
  * for it must be **dual-use**, so a HomeKit-triggered recording also lands in
339
339
  * CamStack as an additional videoclip source alongside the recorder and the NC
340
340
  * clip ring — *one fragmenter, two consumers; do not build an HKSV-only pipe*
341
- * (`docs/roadmap.md` item 4b). A single-callback pipe makes the second consumer
341
+ * (roadmap item 4b, now in the private planning board). A single-callback pipe makes the second consumer
342
342
  * a second ffmpeg child of the same camera. So this is the same shape the
343
343
  * broker's other multi-consumer surfaces already have
344
344
  * (`AudioChunkPlane`, the push packet plane): N independent subscriptions over
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-DUFGDd_2.js");
2
+ const require_dist = require("./dist-63VVVhpw.js");
3
3
  require("node:os");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs = require("node:fs");
@@ -342,7 +342,7 @@ var Fmp4FragmentChild = class {
342
342
  * for it must be **dual-use**, so a HomeKit-triggered recording also lands in
343
343
  * CamStack as an additional videoclip source alongside the recorder and the NC
344
344
  * clip ring — *one fragmenter, two consumers; do not build an HKSV-only pipe*
345
- * (`docs/roadmap.md` item 4b). A single-callback pipe makes the second consumer
345
+ * (roadmap item 4b, now in the private planning board). A single-callback pipe makes the second consumer
346
346
  * a second ffmpeg child of the same camera. So this is the same shape the
347
347
  * broker's other multi-consumer surfaces already have
348
348
  * (`AudioChunkPlane`, the push packet plane): N independent subscriptions over
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-DUFGDd_2.js");
7
- const require_node = require("../node-DzGVn7bD.js");
8
- const require_custom_model_catalog = require("../custom-model-catalog-CKCK-QcT.js");
6
+ const require_dist = require("../dist-63VVVhpw.js");
7
+ const require_node = require("../node-DdaBnHEq.js");
8
+ const require_custom_model_catalog = require("../custom-model-catalog-6zwaRe6H.js");
9
9
  const require_worker_protocol = require("../worker-protocol-DngGEAT2.js");
10
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
11
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
@@ -4557,456 +4557,6 @@ function summarizeRunnerHardware(hardware, cpuCores = node_os.cpus().length) {
4557
4557
  cpuCores
4558
4558
  };
4559
4559
  }
4560
- /** Area of a normalised rectangle, never negative. Shared with the D396 recipe. */
4561
- function normalizedArea(r) {
4562
- return Math.max(0, r.w) * Math.max(0, r.h);
4563
- }
4564
- var area = normalizedArea;
4565
- function intersection(a, b) {
4566
- return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
4567
- }
4568
- /**
4569
- * Share of `region` not covered by any detection box. The sum of pairwise
4570
- * intersections over-counts where detections overlap each other, which only
4571
- * makes the pass MORE conservative; clipped at 1.
4572
- */
4573
- function uncoveredFraction(region, detections) {
4574
- const regionArea = area(region);
4575
- if (regionArea <= 0) return 0;
4576
- let covered = 0;
4577
- for (const detection of detections) covered += intersection(region, detection);
4578
- return 1 - Math.min(1, covered / regionArea);
4579
- }
4580
- /**
4581
- * THE square-placement rule of this pass — one owner, shared by both recipes.
4582
- *
4583
- * A crop is a square of `side` native pixels centred where it can be and slid
4584
- * inside the frame where it cannot. Both region rules place their squares
4585
- * through this function so a crop means the same rectangle whichever rule
4586
- * chose the region.
4587
- */
4588
- function squareAround(centreX, centreY, side, native) {
4589
- return {
4590
- left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
4591
- top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
4592
- side
4593
- };
4594
- }
4595
- function contains(crop, x, y) {
4596
- return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
4597
- }
4598
- /**
4599
- * Squares for one region: one when it fits the largest side, otherwise up to
4600
- * two tiles along its longer axis (a person-sized blob with something small
4601
- * at either end). The padded region decides the side, never the frame.
4602
- */
4603
- function cropsForRegion(region, native) {
4604
- const widthPx = region.w * native.width;
4605
- const heightPx = region.h * native.height;
4606
- const longest = Math.max(widthPx, heightPx);
4607
- const padded = longest * 1.5;
4608
- const side = Math.round(Math.min(Math.max(padded, 640), 800));
4609
- const centreX = (region.x + region.w / 2) * native.width;
4610
- const centreY = (region.y + region.h / 2) * native.height;
4611
- if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
4612
- const alongX = widthPx >= heightPx;
4613
- const quarter = longest / 4;
4614
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4615
- }
4616
- /**
4617
- * The ONE place the two region gates are decided. `planRecoveryCrops` filters
4618
- * on it and {@link countRecoverySkips} counts it, so the report can never
4619
- * describe a gate the planner does not actually apply.
4620
- */
4621
- function classifyRegion(region, uncovered) {
4622
- if (area(region) > .25) return "too-large";
4623
- if (uncovered < .35) return "covered";
4624
- return "eligible";
4625
- }
4626
- /**
4627
- * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
4628
- *
4629
- * The pass report counts the passes that RAN, so "the second pass stops running
4630
- * when a person is beside the dog" — a plausible reading of
4631
- * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
4632
- * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
4633
- * — could not be confirmed or killed from the logs at all. This classifies the
4634
- * regions a frame offered, against the same predicate the planner filters on.
4635
- */
4636
- function countRecoverySkips(input) {
4637
- let tooLarge = 0;
4638
- let covered = 0;
4639
- let eligible = 0;
4640
- for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
4641
- case "too-large":
4642
- tooLarge += 1;
4643
- break;
4644
- case "covered":
4645
- covered += 1;
4646
- break;
4647
- case "eligible":
4648
- eligible += 1;
4649
- break;
4650
- }
4651
- return {
4652
- tooLarge,
4653
- covered,
4654
- eligible
4655
- };
4656
- }
4657
- function planRecoveryCrops(input) {
4658
- const { native } = input;
4659
- if (Math.min(native.width, native.height) < 640) return [];
4660
- const ranked = input.regions.map((region) => ({
4661
- region,
4662
- uncovered: uncoveredFraction(region, input.detections)
4663
- })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
4664
- const planned = [];
4665
- for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
4666
- if (planned.length >= 2) return planned;
4667
- const centreX = crop.left + crop.side / 2;
4668
- const centreY = crop.top + crop.side / 2;
4669
- if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
4670
- planned.push(crop);
4671
- }
4672
- return planned;
4673
- }
4674
- /**
4675
- * Boxes the executor produced on the crop VIEW → the result's own space.
4676
- *
4677
- * Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
4678
- * square it was cut from (`crop`, native px) and the space the full-frame
4679
- * result reports in (`result`, native px on the `frameRef` path, the inline
4680
- * ≤640 view otherwise). A box touching the view edge was clipped by the crop
4681
- * and is dropped: half a person is not a person, and the tracker would take
4682
- * the half as the whole.
4683
- */
4684
- function remapCropDetections(detections, crop, view, result, native) {
4685
- const viewToNative = crop.side / Math.max(1, view.width);
4686
- const nativeToResultX = result.width / Math.max(1, native.width);
4687
- const nativeToResultY = result.height / Math.max(1, native.height);
4688
- const out = [];
4689
- for (const detection of detections) {
4690
- const { x, y, width, height } = detection.bbox;
4691
- if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
4692
- out.push({
4693
- ...detection,
4694
- nativeRecovery: {
4695
- score: detection.score,
4696
- cropSidePx: crop.side,
4697
- viewPx: view.width
4698
- },
4699
- bbox: {
4700
- x: (crop.left + x * viewToNative) * nativeToResultX,
4701
- y: (crop.top + y * viewToNative) * nativeToResultY,
4702
- width: width * viewToNative * nativeToResultX,
4703
- height: height * viewToNative * nativeToResultY
4704
- }
4705
- });
4706
- }
4707
- return out;
4708
- }
4709
- function boxIntersection(a, b) {
4710
- return Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
4711
- }
4712
- function sameSubject(recovered, existing) {
4713
- const inter = boxIntersection(recovered, existing);
4714
- if (inter <= 0) return false;
4715
- const areaR = recovered.width * recovered.height;
4716
- const union = areaR + existing.width * existing.height - inter;
4717
- if (union > 0 && inter / union >= .5) return true;
4718
- return areaR > 0 && inter / areaR >= .7;
4719
- }
4720
- /**
4721
- * Class-aware NMS of the recovered boxes against the full-frame ones. Every
4722
- * full-frame box is kept as it is — the second pass only ADDS. A recovered box
4723
- * is added when no full-frame box of the same macro class is the same subject
4724
- * (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
4725
- * re-minted so a crop's `d0` cannot collide with the frame's.
4726
- */
4727
- function fuseRecoveredDetections(full, recovered) {
4728
- const accepted = [];
4729
- for (const [index, candidate] of recovered.entries()) {
4730
- if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
4731
- accepted.push({
4732
- ...candidate,
4733
- id: `recovered-${index}-${candidate.id}`
4734
- });
4735
- }
4736
- return [...full, ...accepted];
4737
- }
4738
- /**
4739
- * The step the second pass runs: the enabled first-level object detector of
4740
- * the camera's own tree, with its own model and no children. Same model as
4741
- * the full pass so a recovered box is comparable to a full-frame one.
4742
- */
4743
- function selectRecoveryStep(steps) {
4744
- const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
4745
- if (!detector) return null;
4746
- return {
4747
- addonId: detector.addonId,
4748
- ...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
4749
- enabled: true
4750
- };
4751
- }
4752
- /**
4753
- * The analyzer's last regions per camera, normalised, with the instant they
4754
- * were measured. Refreshed off the motion path; read on the detection path.
4755
- * A stale entry answers nothing — the regions of two seconds ago are not this
4756
- * frame's. An EMPTY analyzer result replaces the previous regions: motion that
4757
- * stopped is not motion.
4758
- */
4759
- var MotionRegionMirror = class {
4760
- byDevice = /* @__PURE__ */ new Map();
4761
- note(deviceId, regions, frameWidth, frameHeight, now) {
4762
- if (frameWidth <= 0 || frameHeight <= 0) return;
4763
- this.byDevice.set(deviceId, {
4764
- at: now,
4765
- regions: regions.map((region) => ({
4766
- x: region.bbox.x / frameWidth,
4767
- y: region.bbox.y / frameHeight,
4768
- w: region.bbox.w / frameWidth,
4769
- h: region.bbox.h / frameHeight
4770
- }))
4771
- });
4772
- }
4773
- fresh(deviceId, now) {
4774
- const entry = this.byDevice.get(deviceId);
4775
- if (!entry || now - entry.at > 2e3) return [];
4776
- return entry.regions;
4777
- }
4778
- forget(deviceId) {
4779
- this.byDevice.delete(deviceId);
4780
- }
4781
- };
4782
- /**
4783
- * Read the opt-in. Anything that is not the boolean `true` is the shipped
4784
- * rule: a stored string or a number is a mis-write (this repo has shipped a
4785
- * setting written into the wrong sqlite table before), and a mis-write must
4786
- * not silently change what a camera detects on.
4787
- */
4788
- function readRecoveryRecipe(settings) {
4789
- return settings?.["motionCropDispatch"] === true ? "motion-crop" : "uncovered-region";
4790
- }
4791
- /**
4792
- * The recipe this camera's resolved step tree selects.
4793
- *
4794
- * Reads the ENABLED first-level detector's own settings, which is where
4795
- * `pipelineOrchestrator.setCameraStepSettings` writes a per-camera value. A
4796
- * tree with no enabled detector runs no second pass at all, so the answer
4797
- * there is the shipped rule and it is never used.
4798
- */
4799
- function readRecoveryRecipeForSteps(steps) {
4800
- return readRecoveryRecipe(steps.find((step) => step.addonId === "object-detection" && step.enabled !== false)?.settings);
4801
- }
4802
- /**
4803
- * A motion box is grown ×1.5 about its centre before anything else.
4804
- *
4805
- * The motion step's box is the CHANGED pixels, which on a moving animal is the
4806
- * leading edge plus its shadow, not the animal. The detector needs the subject
4807
- * and the context it was trained on.
4808
- */
4809
- var MOTION_CROP_MOTION_PAD = 1.5;
4810
- /**
4811
- * An intersecting tracked box is grown ×1.25 — less, because it is already a
4812
- * subject rectangle rather than a change mask.
4813
- */
4814
- var MOTION_CROP_TRACKED_PAD = 1.25;
4815
- /**
4816
- * The floor a `person` or `vehicle` box must clear when it came out of a CROP
4817
- * rather than out of the full frame.
4818
- *
4819
- * Collected, not assumed: 3 085 quiet frames produced 27 motion-positive
4820
- * frames and 28 crop invocations. The full-frame arm produced 0 boxes above
4821
- * any gate; the crop arm produced 3, of which two are genuine false positives,
4822
- * and both are night-IR corners upscaled into a 320 model — `vehicle 0.749`
4823
- * and `person 0.701`. Zero false `animal` at any gate, which is why `animal`
4824
- * is deliberately absent from this table: the class the pass exists to recover
4825
- * is the class that never produced a false positive, and gating it would cost
4826
- * the whole measured gain.
4827
- *
4828
- * This is a CROP-ONLY floor, applied on top of the per-macro floors the
4829
- * executor already enforced. A full-frame `person` at 0.70 is untouched — the
4830
- * full-frame arm is the one arm with ground truth behind its floor
4831
- * (`minConfidencePerson` 0.65, raised on 2026-07-30 from a measured
4832
- * distribution).
4833
- *
4834
- * The cost of this floor is stated rather than assumed away: it can also
4835
- * remove a real distant person or vehicle recovered from a crop at 0.70–0.80.
4836
- * Nothing in the quiet-footage sample measures that, so the floor is a
4837
- * constant here rather than a slider, and the runner reports every drop it
4838
- * makes per camera.
4839
- */
4840
- var CROP_CLASS_FLOORS = Object.freeze({
4841
- person: .8,
4842
- vehicle: .8
4843
- });
4844
- /**
4845
- * Apply the crop-only class floors, REPORTING every drop.
4846
- *
4847
- * A class with no entry in {@link CROP_CLASS_FLOORS} is kept: a floor this
4848
- * table cannot express is not a floor of zero, and silently dropping a class
4849
- * nobody measured is the exact shape of silent drop this repo has paid for
4850
- * three separate times.
4851
- */
4852
- function applyCropClassFloors(detections) {
4853
- const kept = [];
4854
- const dropped = [];
4855
- for (const detection of detections) {
4856
- const floor = CROP_CLASS_FLOORS[detection.macroClass];
4857
- if (floor !== void 0 && detection.score < floor) {
4858
- dropped.push({
4859
- macroClass: detection.macroClass,
4860
- score: detection.score,
4861
- floor
4862
- });
4863
- continue;
4864
- }
4865
- kept.push(detection);
4866
- }
4867
- return {
4868
- kept,
4869
- dropped
4870
- };
4871
- }
4872
- function scaleAbout(rect, factor) {
4873
- const w = rect.w * factor;
4874
- const h = rect.h * factor;
4875
- return {
4876
- x: rect.x + rect.w / 2 - w / 2,
4877
- y: rect.y + rect.h / 2 - h / 2,
4878
- w,
4879
- h
4880
- };
4881
- }
4882
- function overlaps(a, b) {
4883
- return Math.min(a.x + a.w, b.x + b.w) > Math.max(a.x, b.x) && Math.min(a.y + a.h, b.y + b.h) > Math.max(a.y, b.y);
4884
- }
4885
- function union(a, b) {
4886
- const x = Math.min(a.x, b.x);
4887
- const y = Math.min(a.y, b.y);
4888
- return {
4889
- x,
4890
- y,
4891
- w: Math.max(a.x + a.w, b.x + b.w) - x,
4892
- h: Math.max(a.y + a.h, b.y + b.h) - y
4893
- };
4894
- }
4895
- function clampToFrame(rect) {
4896
- const x = Math.max(0, rect.x);
4897
- const y = Math.max(0, rect.y);
4898
- return {
4899
- x,
4900
- y,
4901
- w: Math.min(1, rect.x + rect.w) - x,
4902
- h: Math.min(1, rect.y + rect.h) - y
4903
- };
4904
- }
4905
- /**
4906
- * Merge transitively every seed whose padded box overlaps another, and answer
4907
- * the union rectangles.
4908
- *
4909
- * Transitive on purpose: a person, the dog beside them and the dog's shadow
4910
- * are three components of one subject, and cutting three crops for them spends
4911
- * two inferences on pixels the first one already showed the detector.
4912
- */
4913
- function mergeSeeds(seeds) {
4914
- const merged = [];
4915
- for (const seed of seeds) {
4916
- let current = seed;
4917
- let changed = true;
4918
- while (changed) {
4919
- changed = false;
4920
- for (let i = merged.length - 1; i >= 0; i -= 1) {
4921
- const other = merged[i];
4922
- if (other === void 0 || !overlaps(current, other)) continue;
4923
- current = union(current, other);
4924
- merged.splice(i, 1);
4925
- changed = true;
4926
- }
4927
- }
4928
- merged.push(current);
4929
- }
4930
- return merged;
4931
- }
4932
- /**
4933
- * The regions the measured recipe judges this frame on.
4934
- *
4935
- * Motion boxes padded {@link MOTION_CROP_MOTION_PAD}; every full-frame box
4936
- * that intersects a RAW motion box padded {@link MOTION_CROP_TRACKED_PAD} and
4937
- * added as a seed of its own; everything that overlaps merged; the result
4938
- * clamped to the frame, ordered largest first and capped at
4939
- * {@link MOTION_CROP_MAX_REGIONS}.
4940
- *
4941
- * There is deliberately no coverage gate and no size gate here. Those belong
4942
- * to the shipped rule, and they are what makes it a rule about a subject
4943
- * BESIDE a detected one; the measurement says the pass is worth running on the
4944
- * detected subject's own region too.
4945
- */
4946
- function planMotionCropRegions(input) {
4947
- if (input.regions.length === 0) return [];
4948
- const seeds = input.regions.map((r) => scaleAbout(r, MOTION_CROP_MOTION_PAD));
4949
- for (const detection of input.detections) {
4950
- if (!input.regions.some((region) => overlaps(detection, region))) continue;
4951
- seeds.push(scaleAbout(detection, MOTION_CROP_TRACKED_PAD));
4952
- }
4953
- return mergeSeeds(seeds).map(clampToFrame).filter((r) => r.w > 0 && r.h > 0).toSorted((a, b) => normalizedArea(b) - normalizedArea(a)).slice(0, 3);
4954
- }
4955
- /**
4956
- * The square the detector is fed for one measured region.
4957
- *
4958
- * "Grow to the detector's input aspect ratio without ever shrinking" is a
4959
- * SQUARE here, and that is a fact about the production detector rather than a
4960
- * simplification: `yolov9m-320-int8` takes 320×320, as does every hardware
4961
- * default in the catalog.
4962
- *
4963
- * The side is the region's longer side, floored at
4964
- * {@link RECOVERY_CROP_MIN_SIDE_PX} — the growth is what magnifies the subject
4965
- * — and ceilinged at {@link RECOVERY_CROP_MAX_SIDE_PX}. The ceiling is NOT a
4966
- * simplification either: at 1024 px the same 320 model scored the reference
4967
- * dog 0.52–0.76, and at 1280 px lost it entirely
4968
- * (`docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`). A
4969
- * region longer than the ceiling is TILED along its longer axis, never zoomed
4970
- * out.
4971
- */
4972
- function motionCropsForRegion(region, native) {
4973
- const widthPx = region.w * native.width;
4974
- const heightPx = region.h * native.height;
4975
- const longest = Math.max(widthPx, heightPx);
4976
- const side = Math.round(Math.min(Math.max(longest, 640), 800));
4977
- const centreX = (region.x + region.w / 2) * native.width;
4978
- const centreY = (region.y + region.h / 2) * native.height;
4979
- if (longest <= 800) return [squareAround(centreX, centreY, side, native)];
4980
- const alongX = widthPx >= heightPx;
4981
- const quarter = longest / 4;
4982
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4983
- }
4984
- /** `true` when `inner` lies entirely inside `outer`. */
4985
- function cropContains(outer, inner) {
4986
- return inner.left >= outer.left && inner.top >= outer.top && inner.left + inner.side <= outer.left + outer.side && inner.top + inner.side <= outer.top + outer.side;
4987
- }
4988
- /**
4989
- * The crops the measured recipe runs on this frame.
4990
- *
4991
- * Dedupe is by CONTAINMENT only. Two squares that merely overlap both run: the
4992
- * pixels the second adds are pixels no other crop shows the detector, and the
4993
- * fusion step downstream already refuses a duplicate BOX. Dropping a crop
4994
- * because it overlapped would drop the subject sitting at its edge, which is
4995
- * the whole population this pass exists for.
4996
- *
4997
- * A frame that cannot hold the smallest crop yields nothing: it is already
4998
- * seen at scale.
4999
- */
5000
- function planMotionCrops(input) {
5001
- const { native } = input;
5002
- if (Math.min(native.width, native.height) < 640) return [];
5003
- const planned = [];
5004
- for (const region of planMotionCropRegions(input)) for (const crop of motionCropsForRegion(region, native)) {
5005
- if (planned.some((existing) => cropContains(existing, crop))) continue;
5006
- planned.push(crop);
5007
- }
5008
- return planned;
5009
- }
5010
4560
  //#endregion
5011
4561
  //#region src/pipeline-runner/motion-frame-view.ts
5012
4562
  var MOTION_FRAME_VIEW = {
@@ -7304,6 +6854,97 @@ var PipelineRunner = class {
7304
6854
  }
7305
6855
  };
7306
6856
  //#endregion
6857
+ //#region src/pipeline-runner/small-subject-recovery.ts
6858
+ /**
6859
+ * Small-subject recovery — a second detector pass at NATIVE resolution on
6860
+ * the moving regions the full-frame pass did not account for.
6861
+ *
6862
+ * ## Why
6863
+ *
6864
+ * The first-level detector sees the frame at its input size (320 px on every
6865
+ * hardware-aware default). A 4K frame is reduced 12×: a dog measured at
6866
+ * 89–277 px in the source is 7–38 px for the model and is not emitted. Camera 615, six
6867
+ * instants with a dog in the scene, five with no `animal` detection at all —
6868
+ * nothing downstream (tracker, gate, notifications) ever saw a subject. The
6869
+ * confirmation gate, which re-runs the same detector on a 320 px native crop
6870
+ * of the PERSON, found the dog at 0.74–0.91 as `bestIncompatibleClass`. The
6871
+ * measurement that decided the constants below is
6872
+ * `docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`: a
6873
+ * 640–800 px native square around the moving region brings the dog to
6874
+ * 0.82–0.91 with the SAME 320 model; a 1280 px square loses it again.
6875
+ *
6876
+ * ## What this is NOT (D52)
6877
+ *
6878
+ * Not a detail/CLIP crop. `deriveDetailCropRect` derives the rectangle a
6879
+ * detail or embedding step is fed from a KNOWN subject box, and one vector
6880
+ * index tolerates exactly one such derivation. This module derives a square
6881
+ * from a MOTION region, before any subject exists, for the FIRST-LEVEL
6882
+ * detector; its output is a detection, never a vector. Nothing here may be
6883
+ * reused for a detail crop, and nothing here calls the detail derivation.
6884
+ *
6885
+ * ## When it runs — NOT only when the frame is empty
6886
+ *
6887
+ * The trigger is an UNCOVERED MOTION REGION, deliberately broader than "the
6888
+ * full-frame pass found nothing". Camera 615, 2026-09-06 19:54:33 local: the
6889
+ * gate DID confirm an `animal` birth (bestScore 0.8983) — and the track held
6890
+ * 9 positions over 1 666 ms, then coasted out its `maxMissedMs` (4 000 ms) and
6891
+ * died at 19:54:39, before the dog reached the Aiuola zone. Both of its object
6892
+ * events carry `zones: []`, while in the same seconds the animal was in fact
6893
+ * at x≈3200, inside the zone. So the defect is not only "the subject is never
6894
+ * born" — it is "the subject is born and then the full-frame pass stops seeing
6895
+ * it", and a pass gated on "no detections at all" would not have run on a
6896
+ * single one of those frames, because the PERSON was still detected.
6897
+ *
6898
+ * Gating on the motion region instead makes the pass fire in both cases with
6899
+ * one rule: a moving thing the full-frame boxes do not account for. A live
6900
+ * track feeding on recovered boxes needs one hit per `maxMissedMs` to survive
6901
+ * and `minHits` (3) to be born; at {@link RECOVERY_MIN_INTERVAL_MS} the pass
6902
+ * supplies 2/s, which is 8× the coasting budget and births a subject in 1.5 s.
6903
+ *
6904
+ * ## Where
6905
+ *
6906
+ * In the runner, in-process, on the frame the full pass just ran on. The
6907
+ * pixels come from the same process-local `frameRef` lease the full pass
6908
+ * used; the crop is resolved as one compressed ≤640 px view. Frames never
6909
+ * cross a process boundary at frame rate (D9/D18) — a second crop of the
6910
+ * same frame on the same channel does not change that.
6911
+ *
6912
+ * The pure planning lives in `@camstack/types` (`pipeline/second-pass/`) so the
6913
+ * offline accuracy eval runs the same planner; this module re-exports it and
6914
+ * keeps the one stateful piece, the per-camera motion-region mirror. The
6915
+ * runner wires the lease, the executor call and the rate limit around it.
6916
+ */
6917
+ /**
6918
+ * The analyzer's last regions per camera, normalised, with the instant they
6919
+ * were measured. Refreshed off the motion path; read on the detection path.
6920
+ * A stale entry answers nothing — the regions of two seconds ago are not this
6921
+ * frame's. An EMPTY analyzer result replaces the previous regions: motion that
6922
+ * stopped is not motion.
6923
+ */
6924
+ var MotionRegionMirror = class {
6925
+ byDevice = /* @__PURE__ */ new Map();
6926
+ note(deviceId, regions, frameWidth, frameHeight, now) {
6927
+ if (frameWidth <= 0 || frameHeight <= 0) return;
6928
+ this.byDevice.set(deviceId, {
6929
+ at: now,
6930
+ regions: regions.map((region) => ({
6931
+ x: region.bbox.x / frameWidth,
6932
+ y: region.bbox.y / frameHeight,
6933
+ w: region.bbox.w / frameWidth,
6934
+ h: region.bbox.h / frameHeight
6935
+ }))
6936
+ });
6937
+ }
6938
+ fresh(deviceId, now) {
6939
+ const entry = this.byDevice.get(deviceId);
6940
+ if (!entry || now - entry.at > 2e3) return [];
6941
+ return entry.regions;
6942
+ }
6943
+ forget(deviceId) {
6944
+ this.byDevice.delete(deviceId);
6945
+ }
6946
+ };
6947
+ //#endregion
7307
6948
  //#region src/pipeline-runner/stateless-step.ts
7308
6949
  function modelServability(step, modelId, format) {
7309
6950
  const entry = step.models.find((m) => m.id === modelId);
@@ -12182,7 +11823,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12182
11823
  this.addRecoverySkips(deviceId, { noMotion: 1 });
12183
11824
  return result;
12184
11825
  }
12185
- const step = selectRecoveryStep(steps);
11826
+ const step = require_dist.selectRecoveryStep(steps);
12186
11827
  if (step === null) return result;
12187
11828
  if (result.width <= 0 || result.height <= 0) return result;
12188
11829
  const native = {
@@ -12199,12 +11840,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12199
11840
  })),
12200
11841
  native
12201
11842
  };
12202
- const recipe = readRecoveryRecipeForSteps(steps);
12203
- const crops = recipe === "motion-crop" ? planMotionCrops(planInput) : planRecoveryCrops(planInput);
11843
+ const recipe = require_dist.readRecoveryRecipeForSteps(steps);
11844
+ const crops = recipe === "motion-crop" ? require_dist.planMotionCrops(planInput) : require_dist.planRecoveryCrops(planInput);
12204
11845
  if (crops.length === 0) {
12205
11846
  if (recipe === "motion-crop") this.addRecoverySkips(deviceId, { unmagnifiable: 1 });
12206
11847
  else {
12207
- const skipped = countRecoverySkips(planInput);
11848
+ const skipped = require_dist.countRecoverySkips(planInput);
12208
11849
  this.addRecoverySkips(deviceId, {
12209
11850
  tooLarge: skipped.tooLarge,
12210
11851
  covered: skipped.covered
@@ -12230,7 +11871,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12230
11871
  ...deviceKey ? { deviceKey } : {}
12231
11872
  });
12232
11873
  if (!cropResult) continue;
12233
- recovered.push(...remapCropDetections(cropResult.detections, crop, {
11874
+ recovered.push(...require_dist.remapCropDetections(cropResult.detections, crop, {
12234
11875
  width: view.width,
12235
11876
  height: view.height
12236
11877
  }, {
@@ -12245,13 +11886,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12245
11886
  } });
12246
11887
  }
12247
11888
  }
12248
- const floor = applyCropClassFloors(recovered);
11889
+ const floor = require_dist.applyCropClassFloors(recovered);
12249
11890
  if (floor.dropped.length > 0) log.info("small-subject recovery: crop boxes below the crop-only class floor", { meta: {
12250
11891
  recipe,
12251
11892
  dropped: floor.dropped,
12252
11893
  nodeId: this.nodeId
12253
11894
  } });
12254
- const fused = fuseRecoveredDetections(result.detections, floor.kept);
11895
+ const fused = require_dist.fuseRecoveredDetections(result.detections, floor.kept);
12255
11896
  const added = fused.length - result.detections.length;
12256
11897
  this.reportRecovery(deviceId, {
12257
11898
  crops: cropsRun,