@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,6 +1,6 @@
1
- import { $n as nodePin, $t as pipelineRunnerCapability, A as NativeLeaseAdmissionSchema, E as FailureCounters, Fn as BaseAddon, Hn as customAction, Lt as linkedStepOf, M as NodeBenchRunnerInputSchema, Nn as errMsg, P as PIPELINE_RAMP, Rt as loadContributionCapability, Un as defineCustomActions, V as ROOT_SUPPLIABLE_STEP_IDS, Vn as createEvent, Xt as pickDetailCropConvention, Y as allowedSuppliedEffect, Zt as pickNativeLeaseOverride, _ as DEFAULT_DETAIL_CROP_CONVENTION, a as BackgroundRunRegistry, ar as array, cr as lazy, dr as object, fn as resolveRootSupply, gr as EventCategory, gt as deriveRootFaceRefineRect, ir as _enum, j as NativeLeaseSettingsSchema, kt as failureContributionCapability, ln as resolveModelBoundSetting, lr as literal, m as CROP_SUPPLY_SOURCES, mr as string, mt as deriveDetailCropRect, nt as buildBenchStepTree, or as boolean, pn as rootSuppliesOf, rt as cameraOwnsMotionViaSensor, ur as number, x as DEVICE_RAMP, y as DEFAULT_NATIVE_LEASE_SETTINGS, zn as MODEL_FORMATS } from "../dist-BF60SjmI.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DkphOOM2.mjs";
3
- import { a as stepsForElectedPool, b as getStepDefinition, d as rootObjectGate, f as localFrameRegistry, h as resolveFrameViewGeometry, i as placeDetailStep, l as resolveDefaultDetectionModel, m as createSessionDecodeCoordinator, p as isSessionDecodeReadGone, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, y as getStep } from "../custom-model-catalog-wpMi2U7e.mjs";
1
+ import { $n as defineCustomActions, A as NativeLeaseAdmissionSchema, E as FailureCounters, Er as EventCategory, F as PIPELINE_RAMP, Ft as fuseRecoveredDetections, Gn as BaseAddon, Ht as linkedStepOf, M as NodeBenchRunnerInputSchema, Nt as failureContributionCapability, Q as applyCropClassFloors, Qn as customAction, Sn as rootSuppliesOf, U as ROOT_SUPPLIABLE_STEP_IDS, Un as errMsg, Ut as loadContributionCapability, Yn as MODEL_FORMATS, Z as allowedSuppliedEffect, Zn as createEvent, _ as DEFAULT_DETAIL_CROP_CONVENTION, a as BackgroundRunRegistry, an as planMotionCrops, at as buildBenchStepTree, br as number, bt as deriveRootFaceRefineRect, cn as readRecoveryRecipeForSteps, ft as countRecoverySkips, gr as boolean, hr as array, in as pipelineRunnerCapability, j as NativeLeaseSettingsSchema, lr as nodePin, m as CROP_SUPPLY_SOURCES, mr as _enum, nn as pickNativeLeaseOverride, on as planRecoveryCrops, ot as cameraOwnsMotionViaSensor, pn as remapCropDetections, tn as pickDetailCropConvention, vn as resolveModelBoundSetting, vr as lazy, vt as deriveDetailCropRect, wn as selectRecoveryStep, wr as string, x as DEVICE_RAMP, xn as resolveRootSupply, xr as object, y as DEFAULT_NATIVE_LEASE_SETTINGS, yr as literal } from "../dist-BK3z-nti.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-BAcWHimZ.mjs";
3
+ import { a as stepsForElectedPool, c as resolveDefaultDetectionModel, d as localFrameRegistry, f as isSessionDecodeReadGone, i as placeDetailStep, l as macroGateVerdict, m as resolveFrameViewGeometry, p as createSessionDecodeCoordinator, s as resolveDeviceEngine, t as CustomModelCatalog, u as rootObjectGate, v as getStep, y as getStepDefinition } from "../custom-model-catalog-Dx6W1kAo.mjs";
4
4
  import { i as SessionHwaccelSource } from "../worker-protocol-_avq7y8O.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
@@ -4549,456 +4549,6 @@ function summarizeRunnerHardware(hardware, cpuCores = os$1.cpus().length) {
4549
4549
  cpuCores
4550
4550
  };
4551
4551
  }
4552
- /** Area of a normalised rectangle, never negative. Shared with the D396 recipe. */
4553
- function normalizedArea(r) {
4554
- return Math.max(0, r.w) * Math.max(0, r.h);
4555
- }
4556
- var area = normalizedArea;
4557
- function intersection(a, b) {
4558
- 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));
4559
- }
4560
- /**
4561
- * Share of `region` not covered by any detection box. The sum of pairwise
4562
- * intersections over-counts where detections overlap each other, which only
4563
- * makes the pass MORE conservative; clipped at 1.
4564
- */
4565
- function uncoveredFraction(region, detections) {
4566
- const regionArea = area(region);
4567
- if (regionArea <= 0) return 0;
4568
- let covered = 0;
4569
- for (const detection of detections) covered += intersection(region, detection);
4570
- return 1 - Math.min(1, covered / regionArea);
4571
- }
4572
- /**
4573
- * THE square-placement rule of this pass — one owner, shared by both recipes.
4574
- *
4575
- * A crop is a square of `side` native pixels centred where it can be and slid
4576
- * inside the frame where it cannot. Both region rules place their squares
4577
- * through this function so a crop means the same rectangle whichever rule
4578
- * chose the region.
4579
- */
4580
- function squareAround(centreX, centreY, side, native) {
4581
- return {
4582
- left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
4583
- top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
4584
- side
4585
- };
4586
- }
4587
- function contains(crop, x, y) {
4588
- return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
4589
- }
4590
- /**
4591
- * Squares for one region: one when it fits the largest side, otherwise up to
4592
- * two tiles along its longer axis (a person-sized blob with something small
4593
- * at either end). The padded region decides the side, never the frame.
4594
- */
4595
- function cropsForRegion(region, native) {
4596
- const widthPx = region.w * native.width;
4597
- const heightPx = region.h * native.height;
4598
- const longest = Math.max(widthPx, heightPx);
4599
- const padded = longest * 1.5;
4600
- const side = Math.round(Math.min(Math.max(padded, 640), 800));
4601
- const centreX = (region.x + region.w / 2) * native.width;
4602
- const centreY = (region.y + region.h / 2) * native.height;
4603
- if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
4604
- const alongX = widthPx >= heightPx;
4605
- const quarter = longest / 4;
4606
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4607
- }
4608
- /**
4609
- * The ONE place the two region gates are decided. `planRecoveryCrops` filters
4610
- * on it and {@link countRecoverySkips} counts it, so the report can never
4611
- * describe a gate the planner does not actually apply.
4612
- */
4613
- function classifyRegion(region, uncovered) {
4614
- if (area(region) > .25) return "too-large";
4615
- if (uncovered < .35) return "covered";
4616
- return "eligible";
4617
- }
4618
- /**
4619
- * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
4620
- *
4621
- * The pass report counts the passes that RAN, so "the second pass stops running
4622
- * when a person is beside the dog" — a plausible reading of
4623
- * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
4624
- * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
4625
- * — could not be confirmed or killed from the logs at all. This classifies the
4626
- * regions a frame offered, against the same predicate the planner filters on.
4627
- */
4628
- function countRecoverySkips(input) {
4629
- let tooLarge = 0;
4630
- let covered = 0;
4631
- let eligible = 0;
4632
- for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
4633
- case "too-large":
4634
- tooLarge += 1;
4635
- break;
4636
- case "covered":
4637
- covered += 1;
4638
- break;
4639
- case "eligible":
4640
- eligible += 1;
4641
- break;
4642
- }
4643
- return {
4644
- tooLarge,
4645
- covered,
4646
- eligible
4647
- };
4648
- }
4649
- function planRecoveryCrops(input) {
4650
- const { native } = input;
4651
- if (Math.min(native.width, native.height) < 640) return [];
4652
- const ranked = input.regions.map((region) => ({
4653
- region,
4654
- uncovered: uncoveredFraction(region, input.detections)
4655
- })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
4656
- const planned = [];
4657
- for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
4658
- if (planned.length >= 2) return planned;
4659
- const centreX = crop.left + crop.side / 2;
4660
- const centreY = crop.top + crop.side / 2;
4661
- if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
4662
- planned.push(crop);
4663
- }
4664
- return planned;
4665
- }
4666
- /**
4667
- * Boxes the executor produced on the crop VIEW → the result's own space.
4668
- *
4669
- * Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
4670
- * square it was cut from (`crop`, native px) and the space the full-frame
4671
- * result reports in (`result`, native px on the `frameRef` path, the inline
4672
- * ≤640 view otherwise). A box touching the view edge was clipped by the crop
4673
- * and is dropped: half a person is not a person, and the tracker would take
4674
- * the half as the whole.
4675
- */
4676
- function remapCropDetections(detections, crop, view, result, native) {
4677
- const viewToNative = crop.side / Math.max(1, view.width);
4678
- const nativeToResultX = result.width / Math.max(1, native.width);
4679
- const nativeToResultY = result.height / Math.max(1, native.height);
4680
- const out = [];
4681
- for (const detection of detections) {
4682
- const { x, y, width, height } = detection.bbox;
4683
- if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
4684
- out.push({
4685
- ...detection,
4686
- nativeRecovery: {
4687
- score: detection.score,
4688
- cropSidePx: crop.side,
4689
- viewPx: view.width
4690
- },
4691
- bbox: {
4692
- x: (crop.left + x * viewToNative) * nativeToResultX,
4693
- y: (crop.top + y * viewToNative) * nativeToResultY,
4694
- width: width * viewToNative * nativeToResultX,
4695
- height: height * viewToNative * nativeToResultY
4696
- }
4697
- });
4698
- }
4699
- return out;
4700
- }
4701
- function boxIntersection(a, b) {
4702
- 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));
4703
- }
4704
- function sameSubject(recovered, existing) {
4705
- const inter = boxIntersection(recovered, existing);
4706
- if (inter <= 0) return false;
4707
- const areaR = recovered.width * recovered.height;
4708
- const union = areaR + existing.width * existing.height - inter;
4709
- if (union > 0 && inter / union >= .5) return true;
4710
- return areaR > 0 && inter / areaR >= .7;
4711
- }
4712
- /**
4713
- * Class-aware NMS of the recovered boxes against the full-frame ones. Every
4714
- * full-frame box is kept as it is — the second pass only ADDS. A recovered box
4715
- * is added when no full-frame box of the same macro class is the same subject
4716
- * (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
4717
- * re-minted so a crop's `d0` cannot collide with the frame's.
4718
- */
4719
- function fuseRecoveredDetections(full, recovered) {
4720
- const accepted = [];
4721
- for (const [index, candidate] of recovered.entries()) {
4722
- if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
4723
- accepted.push({
4724
- ...candidate,
4725
- id: `recovered-${index}-${candidate.id}`
4726
- });
4727
- }
4728
- return [...full, ...accepted];
4729
- }
4730
- /**
4731
- * The step the second pass runs: the enabled first-level object detector of
4732
- * the camera's own tree, with its own model and no children. Same model as
4733
- * the full pass so a recovered box is comparable to a full-frame one.
4734
- */
4735
- function selectRecoveryStep(steps) {
4736
- const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
4737
- if (!detector) return null;
4738
- return {
4739
- addonId: detector.addonId,
4740
- ...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
4741
- enabled: true
4742
- };
4743
- }
4744
- /**
4745
- * The analyzer's last regions per camera, normalised, with the instant they
4746
- * were measured. Refreshed off the motion path; read on the detection path.
4747
- * A stale entry answers nothing — the regions of two seconds ago are not this
4748
- * frame's. An EMPTY analyzer result replaces the previous regions: motion that
4749
- * stopped is not motion.
4750
- */
4751
- var MotionRegionMirror = class {
4752
- byDevice = /* @__PURE__ */ new Map();
4753
- note(deviceId, regions, frameWidth, frameHeight, now) {
4754
- if (frameWidth <= 0 || frameHeight <= 0) return;
4755
- this.byDevice.set(deviceId, {
4756
- at: now,
4757
- regions: regions.map((region) => ({
4758
- x: region.bbox.x / frameWidth,
4759
- y: region.bbox.y / frameHeight,
4760
- w: region.bbox.w / frameWidth,
4761
- h: region.bbox.h / frameHeight
4762
- }))
4763
- });
4764
- }
4765
- fresh(deviceId, now) {
4766
- const entry = this.byDevice.get(deviceId);
4767
- if (!entry || now - entry.at > 2e3) return [];
4768
- return entry.regions;
4769
- }
4770
- forget(deviceId) {
4771
- this.byDevice.delete(deviceId);
4772
- }
4773
- };
4774
- /**
4775
- * Read the opt-in. Anything that is not the boolean `true` is the shipped
4776
- * rule: a stored string or a number is a mis-write (this repo has shipped a
4777
- * setting written into the wrong sqlite table before), and a mis-write must
4778
- * not silently change what a camera detects on.
4779
- */
4780
- function readRecoveryRecipe(settings) {
4781
- return settings?.["motionCropDispatch"] === true ? "motion-crop" : "uncovered-region";
4782
- }
4783
- /**
4784
- * The recipe this camera's resolved step tree selects.
4785
- *
4786
- * Reads the ENABLED first-level detector's own settings, which is where
4787
- * `pipelineOrchestrator.setCameraStepSettings` writes a per-camera value. A
4788
- * tree with no enabled detector runs no second pass at all, so the answer
4789
- * there is the shipped rule and it is never used.
4790
- */
4791
- function readRecoveryRecipeForSteps(steps) {
4792
- return readRecoveryRecipe(steps.find((step) => step.addonId === "object-detection" && step.enabled !== false)?.settings);
4793
- }
4794
- /**
4795
- * A motion box is grown ×1.5 about its centre before anything else.
4796
- *
4797
- * The motion step's box is the CHANGED pixels, which on a moving animal is the
4798
- * leading edge plus its shadow, not the animal. The detector needs the subject
4799
- * and the context it was trained on.
4800
- */
4801
- var MOTION_CROP_MOTION_PAD = 1.5;
4802
- /**
4803
- * An intersecting tracked box is grown ×1.25 — less, because it is already a
4804
- * subject rectangle rather than a change mask.
4805
- */
4806
- var MOTION_CROP_TRACKED_PAD = 1.25;
4807
- /**
4808
- * The floor a `person` or `vehicle` box must clear when it came out of a CROP
4809
- * rather than out of the full frame.
4810
- *
4811
- * Collected, not assumed: 3 085 quiet frames produced 27 motion-positive
4812
- * frames and 28 crop invocations. The full-frame arm produced 0 boxes above
4813
- * any gate; the crop arm produced 3, of which two are genuine false positives,
4814
- * and both are night-IR corners upscaled into a 320 model — `vehicle 0.749`
4815
- * and `person 0.701`. Zero false `animal` at any gate, which is why `animal`
4816
- * is deliberately absent from this table: the class the pass exists to recover
4817
- * is the class that never produced a false positive, and gating it would cost
4818
- * the whole measured gain.
4819
- *
4820
- * This is a CROP-ONLY floor, applied on top of the per-macro floors the
4821
- * executor already enforced. A full-frame `person` at 0.70 is untouched — the
4822
- * full-frame arm is the one arm with ground truth behind its floor
4823
- * (`minConfidencePerson` 0.65, raised on 2026-07-30 from a measured
4824
- * distribution).
4825
- *
4826
- * The cost of this floor is stated rather than assumed away: it can also
4827
- * remove a real distant person or vehicle recovered from a crop at 0.70–0.80.
4828
- * Nothing in the quiet-footage sample measures that, so the floor is a
4829
- * constant here rather than a slider, and the runner reports every drop it
4830
- * makes per camera.
4831
- */
4832
- var CROP_CLASS_FLOORS = Object.freeze({
4833
- person: .8,
4834
- vehicle: .8
4835
- });
4836
- /**
4837
- * Apply the crop-only class floors, REPORTING every drop.
4838
- *
4839
- * A class with no entry in {@link CROP_CLASS_FLOORS} is kept: a floor this
4840
- * table cannot express is not a floor of zero, and silently dropping a class
4841
- * nobody measured is the exact shape of silent drop this repo has paid for
4842
- * three separate times.
4843
- */
4844
- function applyCropClassFloors(detections) {
4845
- const kept = [];
4846
- const dropped = [];
4847
- for (const detection of detections) {
4848
- const floor = CROP_CLASS_FLOORS[detection.macroClass];
4849
- if (floor !== void 0 && detection.score < floor) {
4850
- dropped.push({
4851
- macroClass: detection.macroClass,
4852
- score: detection.score,
4853
- floor
4854
- });
4855
- continue;
4856
- }
4857
- kept.push(detection);
4858
- }
4859
- return {
4860
- kept,
4861
- dropped
4862
- };
4863
- }
4864
- function scaleAbout(rect, factor) {
4865
- const w = rect.w * factor;
4866
- const h = rect.h * factor;
4867
- return {
4868
- x: rect.x + rect.w / 2 - w / 2,
4869
- y: rect.y + rect.h / 2 - h / 2,
4870
- w,
4871
- h
4872
- };
4873
- }
4874
- function overlaps(a, b) {
4875
- 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);
4876
- }
4877
- function union(a, b) {
4878
- const x = Math.min(a.x, b.x);
4879
- const y = Math.min(a.y, b.y);
4880
- return {
4881
- x,
4882
- y,
4883
- w: Math.max(a.x + a.w, b.x + b.w) - x,
4884
- h: Math.max(a.y + a.h, b.y + b.h) - y
4885
- };
4886
- }
4887
- function clampToFrame(rect) {
4888
- const x = Math.max(0, rect.x);
4889
- const y = Math.max(0, rect.y);
4890
- return {
4891
- x,
4892
- y,
4893
- w: Math.min(1, rect.x + rect.w) - x,
4894
- h: Math.min(1, rect.y + rect.h) - y
4895
- };
4896
- }
4897
- /**
4898
- * Merge transitively every seed whose padded box overlaps another, and answer
4899
- * the union rectangles.
4900
- *
4901
- * Transitive on purpose: a person, the dog beside them and the dog's shadow
4902
- * are three components of one subject, and cutting three crops for them spends
4903
- * two inferences on pixels the first one already showed the detector.
4904
- */
4905
- function mergeSeeds(seeds) {
4906
- const merged = [];
4907
- for (const seed of seeds) {
4908
- let current = seed;
4909
- let changed = true;
4910
- while (changed) {
4911
- changed = false;
4912
- for (let i = merged.length - 1; i >= 0; i -= 1) {
4913
- const other = merged[i];
4914
- if (other === void 0 || !overlaps(current, other)) continue;
4915
- current = union(current, other);
4916
- merged.splice(i, 1);
4917
- changed = true;
4918
- }
4919
- }
4920
- merged.push(current);
4921
- }
4922
- return merged;
4923
- }
4924
- /**
4925
- * The regions the measured recipe judges this frame on.
4926
- *
4927
- * Motion boxes padded {@link MOTION_CROP_MOTION_PAD}; every full-frame box
4928
- * that intersects a RAW motion box padded {@link MOTION_CROP_TRACKED_PAD} and
4929
- * added as a seed of its own; everything that overlaps merged; the result
4930
- * clamped to the frame, ordered largest first and capped at
4931
- * {@link MOTION_CROP_MAX_REGIONS}.
4932
- *
4933
- * There is deliberately no coverage gate and no size gate here. Those belong
4934
- * to the shipped rule, and they are what makes it a rule about a subject
4935
- * BESIDE a detected one; the measurement says the pass is worth running on the
4936
- * detected subject's own region too.
4937
- */
4938
- function planMotionCropRegions(input) {
4939
- if (input.regions.length === 0) return [];
4940
- const seeds = input.regions.map((r) => scaleAbout(r, MOTION_CROP_MOTION_PAD));
4941
- for (const detection of input.detections) {
4942
- if (!input.regions.some((region) => overlaps(detection, region))) continue;
4943
- seeds.push(scaleAbout(detection, MOTION_CROP_TRACKED_PAD));
4944
- }
4945
- return mergeSeeds(seeds).map(clampToFrame).filter((r) => r.w > 0 && r.h > 0).toSorted((a, b) => normalizedArea(b) - normalizedArea(a)).slice(0, 3);
4946
- }
4947
- /**
4948
- * The square the detector is fed for one measured region.
4949
- *
4950
- * "Grow to the detector's input aspect ratio without ever shrinking" is a
4951
- * SQUARE here, and that is a fact about the production detector rather than a
4952
- * simplification: `yolov9m-320-int8` takes 320×320, as does every hardware
4953
- * default in the catalog.
4954
- *
4955
- * The side is the region's longer side, floored at
4956
- * {@link RECOVERY_CROP_MIN_SIDE_PX} — the growth is what magnifies the subject
4957
- * — and ceilinged at {@link RECOVERY_CROP_MAX_SIDE_PX}. The ceiling is NOT a
4958
- * simplification either: at 1024 px the same 320 model scored the reference
4959
- * dog 0.52–0.76, and at 1280 px lost it entirely
4960
- * (`docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`). A
4961
- * region longer than the ceiling is TILED along its longer axis, never zoomed
4962
- * out.
4963
- */
4964
- function motionCropsForRegion(region, native) {
4965
- const widthPx = region.w * native.width;
4966
- const heightPx = region.h * native.height;
4967
- const longest = Math.max(widthPx, heightPx);
4968
- const side = Math.round(Math.min(Math.max(longest, 640), 800));
4969
- const centreX = (region.x + region.w / 2) * native.width;
4970
- const centreY = (region.y + region.h / 2) * native.height;
4971
- if (longest <= 800) return [squareAround(centreX, centreY, side, native)];
4972
- const alongX = widthPx >= heightPx;
4973
- const quarter = longest / 4;
4974
- return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
4975
- }
4976
- /** `true` when `inner` lies entirely inside `outer`. */
4977
- function cropContains(outer, inner) {
4978
- 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;
4979
- }
4980
- /**
4981
- * The crops the measured recipe runs on this frame.
4982
- *
4983
- * Dedupe is by CONTAINMENT only. Two squares that merely overlap both run: the
4984
- * pixels the second adds are pixels no other crop shows the detector, and the
4985
- * fusion step downstream already refuses a duplicate BOX. Dropping a crop
4986
- * because it overlapped would drop the subject sitting at its edge, which is
4987
- * the whole population this pass exists for.
4988
- *
4989
- * A frame that cannot hold the smallest crop yields nothing: it is already
4990
- * seen at scale.
4991
- */
4992
- function planMotionCrops(input) {
4993
- const { native } = input;
4994
- if (Math.min(native.width, native.height) < 640) return [];
4995
- const planned = [];
4996
- for (const region of planMotionCropRegions(input)) for (const crop of motionCropsForRegion(region, native)) {
4997
- if (planned.some((existing) => cropContains(existing, crop))) continue;
4998
- planned.push(crop);
4999
- }
5000
- return planned;
5001
- }
5002
4552
  //#endregion
5003
4553
  //#region src/pipeline-runner/motion-frame-view.ts
5004
4554
  var MOTION_FRAME_VIEW = {
@@ -7296,6 +6846,97 @@ var PipelineRunner = class {
7296
6846
  }
7297
6847
  };
7298
6848
  //#endregion
6849
+ //#region src/pipeline-runner/small-subject-recovery.ts
6850
+ /**
6851
+ * Small-subject recovery — a second detector pass at NATIVE resolution on
6852
+ * the moving regions the full-frame pass did not account for.
6853
+ *
6854
+ * ## Why
6855
+ *
6856
+ * The first-level detector sees the frame at its input size (320 px on every
6857
+ * hardware-aware default). A 4K frame is reduced 12×: a dog measured at
6858
+ * 89–277 px in the source is 7–38 px for the model and is not emitted. Camera 615, six
6859
+ * instants with a dog in the scene, five with no `animal` detection at all —
6860
+ * nothing downstream (tracker, gate, notifications) ever saw a subject. The
6861
+ * confirmation gate, which re-runs the same detector on a 320 px native crop
6862
+ * of the PERSON, found the dog at 0.74–0.91 as `bestIncompatibleClass`. The
6863
+ * measurement that decided the constants below is
6864
+ * `docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`: a
6865
+ * 640–800 px native square around the moving region brings the dog to
6866
+ * 0.82–0.91 with the SAME 320 model; a 1280 px square loses it again.
6867
+ *
6868
+ * ## What this is NOT (D52)
6869
+ *
6870
+ * Not a detail/CLIP crop. `deriveDetailCropRect` derives the rectangle a
6871
+ * detail or embedding step is fed from a KNOWN subject box, and one vector
6872
+ * index tolerates exactly one such derivation. This module derives a square
6873
+ * from a MOTION region, before any subject exists, for the FIRST-LEVEL
6874
+ * detector; its output is a detection, never a vector. Nothing here may be
6875
+ * reused for a detail crop, and nothing here calls the detail derivation.
6876
+ *
6877
+ * ## When it runs — NOT only when the frame is empty
6878
+ *
6879
+ * The trigger is an UNCOVERED MOTION REGION, deliberately broader than "the
6880
+ * full-frame pass found nothing". Camera 615, 2026-09-06 19:54:33 local: the
6881
+ * gate DID confirm an `animal` birth (bestScore 0.8983) — and the track held
6882
+ * 9 positions over 1 666 ms, then coasted out its `maxMissedMs` (4 000 ms) and
6883
+ * died at 19:54:39, before the dog reached the Aiuola zone. Both of its object
6884
+ * events carry `zones: []`, while in the same seconds the animal was in fact
6885
+ * at x≈3200, inside the zone. So the defect is not only "the subject is never
6886
+ * born" — it is "the subject is born and then the full-frame pass stops seeing
6887
+ * it", and a pass gated on "no detections at all" would not have run on a
6888
+ * single one of those frames, because the PERSON was still detected.
6889
+ *
6890
+ * Gating on the motion region instead makes the pass fire in both cases with
6891
+ * one rule: a moving thing the full-frame boxes do not account for. A live
6892
+ * track feeding on recovered boxes needs one hit per `maxMissedMs` to survive
6893
+ * and `minHits` (3) to be born; at {@link RECOVERY_MIN_INTERVAL_MS} the pass
6894
+ * supplies 2/s, which is 8× the coasting budget and births a subject in 1.5 s.
6895
+ *
6896
+ * ## Where
6897
+ *
6898
+ * In the runner, in-process, on the frame the full pass just ran on. The
6899
+ * pixels come from the same process-local `frameRef` lease the full pass
6900
+ * used; the crop is resolved as one compressed ≤640 px view. Frames never
6901
+ * cross a process boundary at frame rate (D9/D18) — a second crop of the
6902
+ * same frame on the same channel does not change that.
6903
+ *
6904
+ * The pure planning lives in `@camstack/types` (`pipeline/second-pass/`) so the
6905
+ * offline accuracy eval runs the same planner; this module re-exports it and
6906
+ * keeps the one stateful piece, the per-camera motion-region mirror. The
6907
+ * runner wires the lease, the executor call and the rate limit around it.
6908
+ */
6909
+ /**
6910
+ * The analyzer's last regions per camera, normalised, with the instant they
6911
+ * were measured. Refreshed off the motion path; read on the detection path.
6912
+ * A stale entry answers nothing — the regions of two seconds ago are not this
6913
+ * frame's. An EMPTY analyzer result replaces the previous regions: motion that
6914
+ * stopped is not motion.
6915
+ */
6916
+ var MotionRegionMirror = class {
6917
+ byDevice = /* @__PURE__ */ new Map();
6918
+ note(deviceId, regions, frameWidth, frameHeight, now) {
6919
+ if (frameWidth <= 0 || frameHeight <= 0) return;
6920
+ this.byDevice.set(deviceId, {
6921
+ at: now,
6922
+ regions: regions.map((region) => ({
6923
+ x: region.bbox.x / frameWidth,
6924
+ y: region.bbox.y / frameHeight,
6925
+ w: region.bbox.w / frameWidth,
6926
+ h: region.bbox.h / frameHeight
6927
+ }))
6928
+ });
6929
+ }
6930
+ fresh(deviceId, now) {
6931
+ const entry = this.byDevice.get(deviceId);
6932
+ if (!entry || now - entry.at > 2e3) return [];
6933
+ return entry.regions;
6934
+ }
6935
+ forget(deviceId) {
6936
+ this.byDevice.delete(deviceId);
6937
+ }
6938
+ };
6939
+ //#endregion
7299
6940
  //#region src/pipeline-runner/stateless-step.ts
7300
6941
  function modelServability(step, modelId, format) {
7301
6942
  const entry = step.models.find((m) => m.id === modelId);
@@ -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
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { Jt as parseProcStatus } from "./dist-BF60SjmI.mjs";
1
+ import { $t as parseProcStatus } from "./dist-BK3z-nti.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -3,8 +3,8 @@ 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");
6
+ const require_dist = require("../dist-63VVVhpw.js");
7
+ const require_node = require("../node-DdaBnHEq.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
10
10
  let node_crypto = require("node:crypto");
@@ -7652,6 +7652,25 @@ Object.freeze({
7652
7652
  "path": "reason",
7653
7653
  "valueType": "string"
7654
7654
  }],
7655
+ "running-state": [
7656
+ {
7657
+ "path": "running",
7658
+ "valueType": "boolean"
7659
+ },
7660
+ {
7661
+ "path": "since",
7662
+ "valueType": "number"
7663
+ },
7664
+ {
7665
+ "path": "source",
7666
+ "valueType": "enum",
7667
+ "enumValues": ["native", "assigned"]
7668
+ },
7669
+ {
7670
+ "path": "phase",
7671
+ "valueType": "string"
7672
+ }
7673
+ ],
7655
7674
  "script-runner": [
7656
7675
  {
7657
7676
  "path": "isRunning",
@@ -1,5 +1,5 @@
1
- import { $n as nodePin, B as RECORDING_SOURCE_CAMSTACK_ADDON, C as EVENT_PAD_MS, Dt as evictionPolicyOfLocation, Et as evictionPolicyForMode, Fn as BaseAddon, Ft as isFirstLevelMacroClass, G as VIDEOCLIPS_MAX_OFFER_BYTES, Gt as modeMayWrite, H as RecordingConfigSchema, Hn as customAction, Ht as mayWriteToLocation, K as VISIT_MERGE_GAP_MS, Mn as DeviceType, N as OpsLogEntrySchema, Nn as errMsg$1, On as evaluateSensorEdge, Ot as exportSubjectOf, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as loadContributionCapability, S as EVENT_OWNER_TYPES, Sn as videoclipsCapability, T as ExportRecordSchema, Tt as eventOverlapsClipWindow, U as RecordingSignalStatusSchema, Un as defineCustomActions, Ut as memoizePinnedFfmpeg, Vt as mayReadLocation, Wn as defineRecordKinds, Wt as migrateLegacyDeviceSignalConfig, Z as armRemovalReconcile, _n as storageEvictableCapability, ar as array, at as clipExportFailure, bn as stripRetiredBandPreBufferSec, cn as resolveLocationMode, dn as resolveRecordingProfiles, dr as object, fr as preprocess, gr as EventCategory, hn as startMoverUnderClearance, ht as deriveRecordingMode, in as recordingSignalCapability, ir as _enum, jn as DeviceFeature, jt as gbToBytes, mr as string, nn as recordingCapability, nr as sleep$1, o as BatteryStatusSchema, on as resolveCreateExportSubject, or as boolean, ot as clipExportFailureOf, pr as record, pt as deriveBatteryPresence, qn as hydrateSchema, rn as recordingExportCapability, rr as userError, s as CLIP_BROKER_PACED_RATES, tn as recordingArchiveCapability, tr as selectAssignedProfileSlots, tt as batteryCapability, ur as number, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, vn as storageOccupancyCapability, vt as deviceRemovalHookCapability, yt as dominantClipLabels, z as RECORDING_SOURCE_CAMSTACK } from "../dist-BF60SjmI.mjs";
2
- import { i as NO_COST_CLAIM, o as physicalRootOf, t as ChildCostRegistry } from "../node-DkphOOM2.mjs";
1
+ import { $n as defineCustomActions, An as stripRetiredBandPreBufferSec, At as evictionPolicyForMode, B as RECORDING_SOURCE_CAMSTACK, Bt as isFirstLevelMacroClass, C as EVENT_PAD_MS, Cr as record, Ct as dominantClipLabels, Dn as storageEvictableCapability, Er as EventCategory, G as RecordingSignalStatusSchema, Gn as BaseAddon, Hn as DeviceType, It as gbToBytes, J as VISIT_MERGE_GAP_MS, Jt as memoizePinnedFfmpeg, Kt as mayReadLocation, Mn as videoclipsCapability, Mt as exportSubjectOf, On as storageOccupancyCapability, P as OpsLogEntrySchema, Qn as customAction, Rn as evaluateSensorEdge, S as EVENT_OWNER_TYPES, Sr as preprocess, St as deviceRemovalHookCapability, T as ExportRecordSchema, Tn as startMoverUnderClearance, Un as errMsg$1, Ut as loadContributionCapability, V as RECORDING_SOURCE_CAMSTACK_ADDON, Vn as DeviceFeature, W as RecordingConfigSchema, Xt as modeMayWrite, Yt as migrateLegacyDeviceSignalConfig, _n as resolveLocationMode, _t as deriveBatteryPresence, bn as resolveRecordingProfiles, br as number, ct as clipExportFailure, dn as recordingExportCapability, dr as selectAssignedProfileSlots, er as defineRecordKinds, et as armRemovalReconcile, fn as recordingSignalCapability, fr as sleep$1, gr as boolean, hn as resolveCreateExportSubject, hr as array, it as batteryCapability, jt as evictionPolicyOfLocation, kt as eventOverlapsClipWindow, ln as recordingArchiveCapability, lr as nodePin, lt as clipExportFailureOf, mr as _enum, o as BatteryStatusSchema, pr as userError, q as VIDEOCLIPS_MAX_OFFER_BYTES, qt as mayWriteToLocation, rr as hydrateSchema, s as CLIP_BROKER_PACED_RATES, un as recordingCapability, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, wr as string, xr as object, yt as deriveRecordingMode, z as RECORDING_EXPORT_MAX_READ_BYTES } from "../dist-BK3z-nti.mjs";
2
+ import { i as NO_COST_CLAIM, o as physicalRootOf, t as ChildCostRegistry } from "../node-BAcWHimZ.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { d as PREBUFFER_MAX_MS, l as withRecordingIntent, r as RECORDING_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { randomUUID } from "node:crypto";
@@ -7644,6 +7644,25 @@ Object.freeze({
7644
7644
  "path": "reason",
7645
7645
  "valueType": "string"
7646
7646
  }],
7647
+ "running-state": [
7648
+ {
7649
+ "path": "running",
7650
+ "valueType": "boolean"
7651
+ },
7652
+ {
7653
+ "path": "since",
7654
+ "valueType": "number"
7655
+ },
7656
+ {
7657
+ "path": "source",
7658
+ "valueType": "enum",
7659
+ "enumValues": ["native", "assigned"]
7660
+ },
7661
+ {
7662
+ "path": "phase",
7663
+ "valueType": "string"
7664
+ }
7665
+ ],
7647
7666
  "script-runner": [
7648
7667
  {
7649
7668
  "path": "isRunning",
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { Nn as errMsg } from "./dist-BF60SjmI.mjs";
2
+ import { Un as errMsg } from "./dist-BK3z-nti.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**