@camstack/addon-pipeline 1.2.621 → 1.2.623

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 (49) 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-Il8UY4D4.js → custom-model-catalog-BTBdgN2p.js} +29 -16
  4. package/dist/{custom-model-catalog-0_SnSXzN.mjs → custom-model-catalog-D5zTw1LU.mjs} +29 -10
  5. package/dist/detection-pipeline/index.js +67 -416
  6. package/dist/detection-pipeline/index.mjs +91 -440
  7. package/dist/{dist-ClkumZgv.js → dist-BVo3wa7s.js} +711 -302
  8. package/dist/{dist-DHqkQhTQ.mjs → dist-opSx6v5i.mjs} +651 -296
  9. package/dist/{model-catalogs-BagalaYp.js → model-catalogs-CQytR2AN.js} +1 -1
  10. package/dist/{model-catalogs-Z1GZAhSb.mjs → model-catalogs-rIvLUVlw.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-CkJLxlWI.mjs → node-CGIYuJMN.mjs} +1 -1
  14. package/dist/{node-0O_61WVW.js → node-DqLsuN_A.js} +1 -1
  15. package/dist/pipeline-runner/index.js +115 -460
  16. package/dist/pipeline-runner/index.mjs +108 -453
  17. package/dist/{process-memory-Dj3UgL1e.mjs → process-memory-Bd-TuWfM.mjs} +1 -1
  18. package/dist/{process-memory-DdIOhjYB.js → process-memory-DyxG-0mG.js} +1 -1
  19. package/dist/recorder/index.js +2 -2
  20. package/dist/recorder/index.mjs +2 -2
  21. package/dist/{segment-demux-js-BePzacki.js → segment-demux-js-B4bfTQpK.js} +1 -1
  22. package/dist/{segment-demux-js-Bv0t0-tQ.mjs → segment-demux-js-CdVRzwSd.mjs} +1 -1
  23. package/dist/session-decode/decode-worker-main.js +12 -21
  24. package/dist/session-decode/decode-worker-main.mjs +12 -21
  25. package/dist/stream-broker/_stub.js +2 -2
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dNk74ahz.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DF3FGCR8.mjs} +2 -2
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-oUZNn2Kr.mjs +26 -0
  28. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bb-ETNGC.mjs +26 -0
  29. package/dist/stream-broker/demux-worker-child.js +1 -1
  30. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  31. package/dist/stream-broker/{hostInit-CH5g78CJ.mjs → hostInit-BiyFzSqq.mjs} +2 -2
  32. package/dist/stream-broker/index.js +3 -3
  33. package/dist/stream-broker/index.mjs +3 -3
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/dist/{worker-protocol-D_NzPcnh.mjs → worker-protocol-DngGEAT2.js} +102 -2
  36. package/dist/{worker-protocol-C-G8qmye.js → worker-protocol-_avq7y8O.mjs} +67 -25
  37. package/package.json +1 -1
  38. package/python/accelerator_facts.py +275 -0
  39. package/python/coreml_load_probe.py +105 -15
  40. package/python/inference_pool.py +19 -2
  41. package/python/openvino_compile_probe.py +72 -18
  42. package/python/postprocessors/ssd.py +12 -4
  43. package/python/postprocessors/test_ssd.py +64 -0
  44. package/python/test_accelerator_facts.py +105 -0
  45. package/python/test_coreml_load_probe.py +83 -0
  46. package/python/test_openvino_compile_probe.py +114 -0
  47. package/python/webgpu_isolated.py +19 -0
  48. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2KkGoQ55.mjs +0 -26
  49. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dx-WrMx0.mjs +0 -26
@@ -3,9 +3,10 @@ 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-ClkumZgv.js");
7
- const require_node = require("../node-0O_61WVW.js");
8
- const require_custom_model_catalog = require("../custom-model-catalog-Il8UY4D4.js");
6
+ const require_dist = require("../dist-BVo3wa7s.js");
7
+ const require_node = require("../node-DqLsuN_A.js");
8
+ const require_custom_model_catalog = require("../custom-model-catalog-BTBdgN2p.js");
9
+ const require_worker_protocol = require("../worker-protocol-DngGEAT2.js");
9
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
11
12
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
@@ -1773,6 +1774,7 @@ function startSessionDecodePump(deps) {
1773
1774
  return child;
1774
1775
  },
1775
1776
  lease: deps.lease,
1777
+ hwaccel: deps.hwaccel,
1776
1778
  logger: deps.logger,
1777
1779
  teardownMode: () => teardownMode,
1778
1780
  onSessionTeardown: (teardown) => {
@@ -4555,456 +4557,6 @@ function summarizeRunnerHardware(hardware, cpuCores = node_os.cpus().length) {
4555
4557
  cpuCores
4556
4558
  };
4557
4559
  }
4558
- /** Area of a normalised rectangle, never negative. Shared with the D396 recipe. */
4559
- function normalizedArea(r) {
4560
- return Math.max(0, r.w) * Math.max(0, r.h);
4561
- }
4562
- var area = normalizedArea;
4563
- function intersection(a, b) {
4564
- 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));
4565
- }
4566
- /**
4567
- * Share of `region` not covered by any detection box. The sum of pairwise
4568
- * intersections over-counts where detections overlap each other, which only
4569
- * makes the pass MORE conservative; clipped at 1.
4570
- */
4571
- function uncoveredFraction(region, detections) {
4572
- const regionArea = area(region);
4573
- if (regionArea <= 0) return 0;
4574
- let covered = 0;
4575
- for (const detection of detections) covered += intersection(region, detection);
4576
- return 1 - Math.min(1, covered / regionArea);
4577
- }
4578
- /**
4579
- * THE square-placement rule of this pass — one owner, shared by both recipes.
4580
- *
4581
- * A crop is a square of `side` native pixels centred where it can be and slid
4582
- * inside the frame where it cannot. Both region rules place their squares
4583
- * through this function so a crop means the same rectangle whichever rule
4584
- * chose the region.
4585
- */
4586
- function squareAround(centreX, centreY, side, native) {
4587
- return {
4588
- left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
4589
- top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
4590
- side
4591
- };
4592
- }
4593
- function contains(crop, x, y) {
4594
- return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
4595
- }
4596
- /**
4597
- * Squares for one region: one when it fits the largest side, otherwise up to
4598
- * two tiles along its longer axis (a person-sized blob with something small
4599
- * at either end). The padded region decides the side, never the frame.
4600
- */
4601
- function cropsForRegion(region, native) {
4602
- const widthPx = region.w * native.width;
4603
- const heightPx = region.h * native.height;
4604
- const longest = Math.max(widthPx, heightPx);
4605
- const padded = longest * 1.5;
4606
- const side = Math.round(Math.min(Math.max(padded, 640), 800));
4607
- const centreX = (region.x + region.w / 2) * native.width;
4608
- const centreY = (region.y + region.h / 2) * native.height;
4609
- if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
4610
- const alongX = widthPx >= heightPx;
4611
- const quarter = longest / 4;
4612
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4613
- }
4614
- /**
4615
- * The ONE place the two region gates are decided. `planRecoveryCrops` filters
4616
- * on it and {@link countRecoverySkips} counts it, so the report can never
4617
- * describe a gate the planner does not actually apply.
4618
- */
4619
- function classifyRegion(region, uncovered) {
4620
- if (area(region) > .25) return "too-large";
4621
- if (uncovered < .35) return "covered";
4622
- return "eligible";
4623
- }
4624
- /**
4625
- * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
4626
- *
4627
- * The pass report counts the passes that RAN, so "the second pass stops running
4628
- * when a person is beside the dog" — a plausible reading of
4629
- * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
4630
- * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
4631
- * — could not be confirmed or killed from the logs at all. This classifies the
4632
- * regions a frame offered, against the same predicate the planner filters on.
4633
- */
4634
- function countRecoverySkips(input) {
4635
- let tooLarge = 0;
4636
- let covered = 0;
4637
- let eligible = 0;
4638
- for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
4639
- case "too-large":
4640
- tooLarge += 1;
4641
- break;
4642
- case "covered":
4643
- covered += 1;
4644
- break;
4645
- case "eligible":
4646
- eligible += 1;
4647
- break;
4648
- }
4649
- return {
4650
- tooLarge,
4651
- covered,
4652
- eligible
4653
- };
4654
- }
4655
- function planRecoveryCrops(input) {
4656
- const { native } = input;
4657
- if (Math.min(native.width, native.height) < 640) return [];
4658
- const ranked = input.regions.map((region) => ({
4659
- region,
4660
- uncovered: uncoveredFraction(region, input.detections)
4661
- })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
4662
- const planned = [];
4663
- for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
4664
- if (planned.length >= 2) return planned;
4665
- const centreX = crop.left + crop.side / 2;
4666
- const centreY = crop.top + crop.side / 2;
4667
- if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
4668
- planned.push(crop);
4669
- }
4670
- return planned;
4671
- }
4672
- /**
4673
- * Boxes the executor produced on the crop VIEW → the result's own space.
4674
- *
4675
- * Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
4676
- * square it was cut from (`crop`, native px) and the space the full-frame
4677
- * result reports in (`result`, native px on the `frameRef` path, the inline
4678
- * ≤640 view otherwise). A box touching the view edge was clipped by the crop
4679
- * and is dropped: half a person is not a person, and the tracker would take
4680
- * the half as the whole.
4681
- */
4682
- function remapCropDetections(detections, crop, view, result, native) {
4683
- const viewToNative = crop.side / Math.max(1, view.width);
4684
- const nativeToResultX = result.width / Math.max(1, native.width);
4685
- const nativeToResultY = result.height / Math.max(1, native.height);
4686
- const out = [];
4687
- for (const detection of detections) {
4688
- const { x, y, width, height } = detection.bbox;
4689
- if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
4690
- out.push({
4691
- ...detection,
4692
- nativeRecovery: {
4693
- score: detection.score,
4694
- cropSidePx: crop.side,
4695
- viewPx: view.width
4696
- },
4697
- bbox: {
4698
- x: (crop.left + x * viewToNative) * nativeToResultX,
4699
- y: (crop.top + y * viewToNative) * nativeToResultY,
4700
- width: width * viewToNative * nativeToResultX,
4701
- height: height * viewToNative * nativeToResultY
4702
- }
4703
- });
4704
- }
4705
- return out;
4706
- }
4707
- function boxIntersection(a, b) {
4708
- 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));
4709
- }
4710
- function sameSubject(recovered, existing) {
4711
- const inter = boxIntersection(recovered, existing);
4712
- if (inter <= 0) return false;
4713
- const areaR = recovered.width * recovered.height;
4714
- const union = areaR + existing.width * existing.height - inter;
4715
- if (union > 0 && inter / union >= .5) return true;
4716
- return areaR > 0 && inter / areaR >= .7;
4717
- }
4718
- /**
4719
- * Class-aware NMS of the recovered boxes against the full-frame ones. Every
4720
- * full-frame box is kept as it is — the second pass only ADDS. A recovered box
4721
- * is added when no full-frame box of the same macro class is the same subject
4722
- * (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
4723
- * re-minted so a crop's `d0` cannot collide with the frame's.
4724
- */
4725
- function fuseRecoveredDetections(full, recovered) {
4726
- const accepted = [];
4727
- for (const [index, candidate] of recovered.entries()) {
4728
- if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
4729
- accepted.push({
4730
- ...candidate,
4731
- id: `recovered-${index}-${candidate.id}`
4732
- });
4733
- }
4734
- return [...full, ...accepted];
4735
- }
4736
- /**
4737
- * The step the second pass runs: the enabled first-level object detector of
4738
- * the camera's own tree, with its own model and no children. Same model as
4739
- * the full pass so a recovered box is comparable to a full-frame one.
4740
- */
4741
- function selectRecoveryStep(steps) {
4742
- const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
4743
- if (!detector) return null;
4744
- return {
4745
- addonId: detector.addonId,
4746
- ...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
4747
- enabled: true
4748
- };
4749
- }
4750
- /**
4751
- * The analyzer's last regions per camera, normalised, with the instant they
4752
- * were measured. Refreshed off the motion path; read on the detection path.
4753
- * A stale entry answers nothing — the regions of two seconds ago are not this
4754
- * frame's. An EMPTY analyzer result replaces the previous regions: motion that
4755
- * stopped is not motion.
4756
- */
4757
- var MotionRegionMirror = class {
4758
- byDevice = /* @__PURE__ */ new Map();
4759
- note(deviceId, regions, frameWidth, frameHeight, now) {
4760
- if (frameWidth <= 0 || frameHeight <= 0) return;
4761
- this.byDevice.set(deviceId, {
4762
- at: now,
4763
- regions: regions.map((region) => ({
4764
- x: region.bbox.x / frameWidth,
4765
- y: region.bbox.y / frameHeight,
4766
- w: region.bbox.w / frameWidth,
4767
- h: region.bbox.h / frameHeight
4768
- }))
4769
- });
4770
- }
4771
- fresh(deviceId, now) {
4772
- const entry = this.byDevice.get(deviceId);
4773
- if (!entry || now - entry.at > 2e3) return [];
4774
- return entry.regions;
4775
- }
4776
- forget(deviceId) {
4777
- this.byDevice.delete(deviceId);
4778
- }
4779
- };
4780
- /**
4781
- * Read the opt-in. Anything that is not the boolean `true` is the shipped
4782
- * rule: a stored string or a number is a mis-write (this repo has shipped a
4783
- * setting written into the wrong sqlite table before), and a mis-write must
4784
- * not silently change what a camera detects on.
4785
- */
4786
- function readRecoveryRecipe(settings) {
4787
- return settings?.["motionCropDispatch"] === true ? "motion-crop" : "uncovered-region";
4788
- }
4789
- /**
4790
- * The recipe this camera's resolved step tree selects.
4791
- *
4792
- * Reads the ENABLED first-level detector's own settings, which is where
4793
- * `pipelineOrchestrator.setCameraStepOverride` writes a per-camera value. A
4794
- * tree with no enabled detector runs no second pass at all, so the answer
4795
- * there is the shipped rule and it is never used.
4796
- */
4797
- function readRecoveryRecipeForSteps(steps) {
4798
- return readRecoveryRecipe(steps.find((step) => step.addonId === "object-detection" && step.enabled !== false)?.settings);
4799
- }
4800
- /**
4801
- * A motion box is grown ×1.5 about its centre before anything else.
4802
- *
4803
- * The motion step's box is the CHANGED pixels, which on a moving animal is the
4804
- * leading edge plus its shadow, not the animal. The detector needs the subject
4805
- * and the context it was trained on.
4806
- */
4807
- var MOTION_CROP_MOTION_PAD = 1.5;
4808
- /**
4809
- * An intersecting tracked box is grown ×1.25 — less, because it is already a
4810
- * subject rectangle rather than a change mask.
4811
- */
4812
- var MOTION_CROP_TRACKED_PAD = 1.25;
4813
- /**
4814
- * The floor a `person` or `vehicle` box must clear when it came out of a CROP
4815
- * rather than out of the full frame.
4816
- *
4817
- * Collected, not assumed: 3 085 quiet frames produced 27 motion-positive
4818
- * frames and 28 crop invocations. The full-frame arm produced 0 boxes above
4819
- * any gate; the crop arm produced 3, of which two are genuine false positives,
4820
- * and both are night-IR corners upscaled into a 320 model — `vehicle 0.749`
4821
- * and `person 0.701`. Zero false `animal` at any gate, which is why `animal`
4822
- * is deliberately absent from this table: the class the pass exists to recover
4823
- * is the class that never produced a false positive, and gating it would cost
4824
- * the whole measured gain.
4825
- *
4826
- * This is a CROP-ONLY floor, applied on top of the per-macro floors the
4827
- * executor already enforced. A full-frame `person` at 0.70 is untouched — the
4828
- * full-frame arm is the one arm with ground truth behind its floor
4829
- * (`minConfidencePerson` 0.65, raised on 2026-07-30 from a measured
4830
- * distribution).
4831
- *
4832
- * The cost of this floor is stated rather than assumed away: it can also
4833
- * remove a real distant person or vehicle recovered from a crop at 0.70–0.80.
4834
- * Nothing in the quiet-footage sample measures that, so the floor is a
4835
- * constant here rather than a slider, and the runner reports every drop it
4836
- * makes per camera.
4837
- */
4838
- var CROP_CLASS_FLOORS = Object.freeze({
4839
- person: .8,
4840
- vehicle: .8
4841
- });
4842
- /**
4843
- * Apply the crop-only class floors, REPORTING every drop.
4844
- *
4845
- * A class with no entry in {@link CROP_CLASS_FLOORS} is kept: a floor this
4846
- * table cannot express is not a floor of zero, and silently dropping a class
4847
- * nobody measured is the exact shape of silent drop this repo has paid for
4848
- * three separate times.
4849
- */
4850
- function applyCropClassFloors(detections) {
4851
- const kept = [];
4852
- const dropped = [];
4853
- for (const detection of detections) {
4854
- const floor = CROP_CLASS_FLOORS[detection.macroClass];
4855
- if (floor !== void 0 && detection.score < floor) {
4856
- dropped.push({
4857
- macroClass: detection.macroClass,
4858
- score: detection.score,
4859
- floor
4860
- });
4861
- continue;
4862
- }
4863
- kept.push(detection);
4864
- }
4865
- return {
4866
- kept,
4867
- dropped
4868
- };
4869
- }
4870
- function scaleAbout(rect, factor) {
4871
- const w = rect.w * factor;
4872
- const h = rect.h * factor;
4873
- return {
4874
- x: rect.x + rect.w / 2 - w / 2,
4875
- y: rect.y + rect.h / 2 - h / 2,
4876
- w,
4877
- h
4878
- };
4879
- }
4880
- function overlaps(a, b) {
4881
- 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);
4882
- }
4883
- function union(a, b) {
4884
- const x = Math.min(a.x, b.x);
4885
- const y = Math.min(a.y, b.y);
4886
- return {
4887
- x,
4888
- y,
4889
- w: Math.max(a.x + a.w, b.x + b.w) - x,
4890
- h: Math.max(a.y + a.h, b.y + b.h) - y
4891
- };
4892
- }
4893
- function clampToFrame(rect) {
4894
- const x = Math.max(0, rect.x);
4895
- const y = Math.max(0, rect.y);
4896
- return {
4897
- x,
4898
- y,
4899
- w: Math.min(1, rect.x + rect.w) - x,
4900
- h: Math.min(1, rect.y + rect.h) - y
4901
- };
4902
- }
4903
- /**
4904
- * Merge transitively every seed whose padded box overlaps another, and answer
4905
- * the union rectangles.
4906
- *
4907
- * Transitive on purpose: a person, the dog beside them and the dog's shadow
4908
- * are three components of one subject, and cutting three crops for them spends
4909
- * two inferences on pixels the first one already showed the detector.
4910
- */
4911
- function mergeSeeds(seeds) {
4912
- const merged = [];
4913
- for (const seed of seeds) {
4914
- let current = seed;
4915
- let changed = true;
4916
- while (changed) {
4917
- changed = false;
4918
- for (let i = merged.length - 1; i >= 0; i -= 1) {
4919
- const other = merged[i];
4920
- if (other === void 0 || !overlaps(current, other)) continue;
4921
- current = union(current, other);
4922
- merged.splice(i, 1);
4923
- changed = true;
4924
- }
4925
- }
4926
- merged.push(current);
4927
- }
4928
- return merged;
4929
- }
4930
- /**
4931
- * The regions the measured recipe judges this frame on.
4932
- *
4933
- * Motion boxes padded {@link MOTION_CROP_MOTION_PAD}; every full-frame box
4934
- * that intersects a RAW motion box padded {@link MOTION_CROP_TRACKED_PAD} and
4935
- * added as a seed of its own; everything that overlaps merged; the result
4936
- * clamped to the frame, ordered largest first and capped at
4937
- * {@link MOTION_CROP_MAX_REGIONS}.
4938
- *
4939
- * There is deliberately no coverage gate and no size gate here. Those belong
4940
- * to the shipped rule, and they are what makes it a rule about a subject
4941
- * BESIDE a detected one; the measurement says the pass is worth running on the
4942
- * detected subject's own region too.
4943
- */
4944
- function planMotionCropRegions(input) {
4945
- if (input.regions.length === 0) return [];
4946
- const seeds = input.regions.map((r) => scaleAbout(r, MOTION_CROP_MOTION_PAD));
4947
- for (const detection of input.detections) {
4948
- if (!input.regions.some((region) => overlaps(detection, region))) continue;
4949
- seeds.push(scaleAbout(detection, MOTION_CROP_TRACKED_PAD));
4950
- }
4951
- return mergeSeeds(seeds).map(clampToFrame).filter((r) => r.w > 0 && r.h > 0).toSorted((a, b) => normalizedArea(b) - normalizedArea(a)).slice(0, 3);
4952
- }
4953
- /**
4954
- * The square the detector is fed for one measured region.
4955
- *
4956
- * "Grow to the detector's input aspect ratio without ever shrinking" is a
4957
- * SQUARE here, and that is a fact about the production detector rather than a
4958
- * simplification: `yolov9m-320-int8` takes 320×320, as does every hardware
4959
- * default in the catalog.
4960
- *
4961
- * The side is the region's longer side, floored at
4962
- * {@link RECOVERY_CROP_MIN_SIDE_PX} — the growth is what magnifies the subject
4963
- * — and ceilinged at {@link RECOVERY_CROP_MAX_SIDE_PX}. The ceiling is NOT a
4964
- * simplification either: at 1024 px the same 320 model scored the reference
4965
- * dog 0.52–0.76, and at 1280 px lost it entirely
4966
- * (`docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`). A
4967
- * region longer than the ceiling is TILED along its longer axis, never zoomed
4968
- * out.
4969
- */
4970
- function motionCropsForRegion(region, native) {
4971
- const widthPx = region.w * native.width;
4972
- const heightPx = region.h * native.height;
4973
- const longest = Math.max(widthPx, heightPx);
4974
- const side = Math.round(Math.min(Math.max(longest, 640), 800));
4975
- const centreX = (region.x + region.w / 2) * native.width;
4976
- const centreY = (region.y + region.h / 2) * native.height;
4977
- if (longest <= 800) return [squareAround(centreX, centreY, side, native)];
4978
- const alongX = widthPx >= heightPx;
4979
- const quarter = longest / 4;
4980
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4981
- }
4982
- /** `true` when `inner` lies entirely inside `outer`. */
4983
- function cropContains(outer, inner) {
4984
- 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;
4985
- }
4986
- /**
4987
- * The crops the measured recipe runs on this frame.
4988
- *
4989
- * Dedupe is by CONTAINMENT only. Two squares that merely overlap both run: the
4990
- * pixels the second adds are pixels no other crop shows the detector, and the
4991
- * fusion step downstream already refuses a duplicate BOX. Dropping a crop
4992
- * because it overlapped would drop the subject sitting at its edge, which is
4993
- * the whole population this pass exists for.
4994
- *
4995
- * A frame that cannot hold the smallest crop yields nothing: it is already
4996
- * seen at scale.
4997
- */
4998
- function planMotionCrops(input) {
4999
- const { native } = input;
5000
- if (Math.min(native.width, native.height) < 640) return [];
5001
- const planned = [];
5002
- for (const region of planMotionCropRegions(input)) for (const crop of motionCropsForRegion(region, native)) {
5003
- if (planned.some((existing) => cropContains(existing, crop))) continue;
5004
- planned.push(crop);
5005
- }
5006
- return planned;
5007
- }
5008
4560
  //#endregion
5009
4561
  //#region src/pipeline-runner/motion-frame-view.ts
5010
4562
  var MOTION_FRAME_VIEW = {
@@ -7302,6 +6854,97 @@ var PipelineRunner = class {
7302
6854
  }
7303
6855
  };
7304
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
7305
6948
  //#region src/pipeline-runner/stateless-step.ts
7306
6949
  function modelServability(step, modelId, format) {
7307
6950
  const entry = step.models.find((m) => m.id === modelId);
@@ -10955,6 +10598,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10955
10598
  */
10956
10599
  resolveSessionLeaseKnobs = () => resolveNativeLeaseKnobs(process.env, this.nativeLease.current());
10957
10600
  /**
10601
+ * The decode hwaccel backends this node's kernel resolver offers (D926),
10602
+ * refreshed on the acquire leg with the lease knobs and read when `start` is
10603
+ * sent. A node whose libva cannot initialise the driver offers no vaapi/qsv,
10604
+ * so its session workers never try them.
10605
+ */
10606
+ sessionHwaccel = new require_worker_protocol.SessionHwaccelSource();
10607
+ resolveSessionHwaccel = () => this.sessionHwaccel.current();
10608
+ /**
10958
10609
  * Refresh the lease knobs before a decode worker is spawned. Awaited on the
10959
10610
  * `acquire` leg of `startGuardedSessionDecode` (which always completes before
10960
10611
  * `startPump` forks), so the spawn sees a value at most one TTL stale rather
@@ -11547,6 +11198,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11547
11198
  */
11548
11199
  async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId, detectionIntent) {
11549
11200
  await this.refreshNativeLeaseSettings();
11201
+ await this.sessionHwaccel.refresh(this.ctxIfReady?.kernel.hwaccel);
11550
11202
  return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
11551
11203
  deviceId,
11552
11204
  streamId,
@@ -11732,6 +11384,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11732
11384
  return startSessionDecodePump({
11733
11385
  fork: this.forkDecodeWorkerWithLease,
11734
11386
  lease: this.resolveSessionLeaseKnobs,
11387
+ hwaccel: this.resolveSessionHwaccel,
11735
11388
  onWorkerGone: hooks.onWorkerGone,
11736
11389
  logger: log,
11737
11390
  source,
@@ -11913,6 +11566,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11913
11566
  return startSessionDecodePump({
11914
11567
  fork: this.forkDecodeWorkerWithLease,
11915
11568
  lease: this.resolveSessionLeaseKnobs,
11569
+ hwaccel: this.resolveSessionHwaccel,
11916
11570
  onWorkerGone: hooks.onWorkerGone,
11917
11571
  logger: log,
11918
11572
  source,
@@ -12169,7 +11823,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12169
11823
  this.addRecoverySkips(deviceId, { noMotion: 1 });
12170
11824
  return result;
12171
11825
  }
12172
- const step = selectRecoveryStep(steps);
11826
+ const step = require_dist.selectRecoveryStep(steps);
12173
11827
  if (step === null) return result;
12174
11828
  if (result.width <= 0 || result.height <= 0) return result;
12175
11829
  const native = {
@@ -12186,12 +11840,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12186
11840
  })),
12187
11841
  native
12188
11842
  };
12189
- const recipe = readRecoveryRecipeForSteps(steps);
12190
- 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);
12191
11845
  if (crops.length === 0) {
12192
11846
  if (recipe === "motion-crop") this.addRecoverySkips(deviceId, { unmagnifiable: 1 });
12193
11847
  else {
12194
- const skipped = countRecoverySkips(planInput);
11848
+ const skipped = require_dist.countRecoverySkips(planInput);
12195
11849
  this.addRecoverySkips(deviceId, {
12196
11850
  tooLarge: skipped.tooLarge,
12197
11851
  covered: skipped.covered
@@ -12217,7 +11871,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12217
11871
  ...deviceKey ? { deviceKey } : {}
12218
11872
  });
12219
11873
  if (!cropResult) continue;
12220
- recovered.push(...remapCropDetections(cropResult.detections, crop, {
11874
+ recovered.push(...require_dist.remapCropDetections(cropResult.detections, crop, {
12221
11875
  width: view.width,
12222
11876
  height: view.height
12223
11877
  }, {
@@ -12232,13 +11886,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12232
11886
  } });
12233
11887
  }
12234
11888
  }
12235
- const floor = applyCropClassFloors(recovered);
11889
+ const floor = require_dist.applyCropClassFloors(recovered);
12236
11890
  if (floor.dropped.length > 0) log.info("small-subject recovery: crop boxes below the crop-only class floor", { meta: {
12237
11891
  recipe,
12238
11892
  dropped: floor.dropped,
12239
11893
  nodeId: this.nodeId
12240
11894
  } });
12241
- const fused = fuseRecoveredDetections(result.detections, floor.kept);
11895
+ const fused = require_dist.fuseRecoveredDetections(result.detections, floor.kept);
12242
11896
  const added = fused.length - result.detections.length;
12243
11897
  this.reportRecovery(deviceId, {
12244
11898
  crops: cropsRun,
@@ -12782,6 +12436,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
12782
12436
  return startSessionDecodePump({
12783
12437
  fork: this.forkDecodeWorkerWithLease,
12784
12438
  lease: this.resolveSessionLeaseKnobs,
12439
+ hwaccel: this.resolveSessionHwaccel,
12785
12440
  onWorkerGone: hooks.onWorkerGone,
12786
12441
  logger: log,
12787
12442
  source,