@camstack/types 1.2.404 → 1.2.405

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.
package/dist/index.js CHANGED
@@ -12883,7 +12883,7 @@ function kindOfValue(value) {
12883
12883
  function only(kind) {
12884
12884
  return new Set([kind]);
12885
12885
  }
12886
- function union(...sets) {
12886
+ function union$1(...sets) {
12887
12887
  return new Set(sets.flatMap((s) => [...s]));
12888
12888
  }
12889
12889
  function withoutNull(set) {
@@ -12911,8 +12911,8 @@ function callKinds(callee, args) {
12911
12911
  const rule = BUILTIN_RESULT_KINDS[callee];
12912
12912
  if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
12913
12913
  if (rule.kind === "fixed") return new Set(rule.result);
12914
- const nonNull = union(...args.map(withoutNull));
12915
- return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
12914
+ const nonNull = union$1(...args.map(withoutNull));
12915
+ return args.every((a) => a.has("null")) ? union$1(nonNull, only("null")) : nonNull;
12916
12916
  }
12917
12917
  function kindsOf(node, bindings) {
12918
12918
  switch (node.kind) {
@@ -12925,8 +12925,8 @@ function kindsOf(node, bindings) {
12925
12925
  }
12926
12926
  case "unary": return only(node.op === "!" ? "boolean" : "number");
12927
12927
  case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
12928
- case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
12929
- case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
12928
+ case "logical": return union$1(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
12929
+ case "conditional": return union$1(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
12930
12930
  case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
12931
12931
  }
12932
12932
  }
@@ -47289,6 +47289,20 @@ var CAMERA_SENSORS_NOT_LOADED = Object.freeze({
47289
47289
  reason: "camera sensor mappings not loaded"
47290
47290
  });
47291
47291
  //#endregion
47292
+ //#region src/capabilities/schemas/package-llm-verdict.ts
47293
+ /**
47294
+ * What the vision model must answer before a package event is emitted
47295
+ * when a packages rule has "Confirm with the LLM" on.
47296
+ *
47297
+ * `package` is the verdict. `confidence` is 0..1. `reason` is the model's
47298
+ * one-line explanation, stored on the event so a drop is not silent.
47299
+ */
47300
+ var PackageLlmVerdictSchema = zod.z.object({
47301
+ package: zod.z.boolean(),
47302
+ confidence: zod.z.number().min(0).max(1),
47303
+ reason: zod.z.string().min(1)
47304
+ });
47305
+ //#endregion
47292
47306
  //#region src/pipeline/cluster-model-scope.ts
47293
47307
  /**
47294
47308
  * A model choice has a SCOPE, and a scope change has a workflow.
@@ -48407,6 +48421,21 @@ var ObjectEventSchema = zod.z.object({
48407
48421
  * pick-up written before D684 — UNKNOWN, never 0 and never `timestamp`.
48408
48422
  */
48409
48423
  observedAt: zod.z.number().optional(),
48424
+ /**
48425
+ * The delivery was emitted without a vision-model yes (D931).
48426
+ *
48427
+ * Set only when the packages rule asked for an LLM confirm and the model
48428
+ * was unreachable, timed out, or answered something that did not parse.
48429
+ * A notification reads it so it can say "possible" instead of a definite
48430
+ * delivery. Absent on every other row, and on a confirm that said yes.
48431
+ */
48432
+ packagePossible: zod.z.boolean().optional(),
48433
+ /**
48434
+ * The vision model's verdict, when a packages rule asked for one and the
48435
+ * model answered the schema (D931). A "no" never becomes a row — the
48436
+ * detector drops it and logs the reason. Present on a yes.
48437
+ */
48438
+ packageLlmVerdict: PackageLlmVerdictSchema.optional(),
48410
48439
  /** The parent track's key-event importance [0,1], propagated to every object
48411
48440
  * event of the track (so an event row can be sorted by importance without a
48412
48441
  * track join). Absent on legacy rows / before the track was scored. */
@@ -51632,11 +51661,18 @@ var KeptReasonSchema = zod.z.enum(["custom-pin"]);
51632
51661
  * Where a choice's latency came from (D791): this node's own benchmark
51633
51662
  * result, or the hardware-class table. Measured on the node beats the table.
51634
51663
  */
51635
- var LatencySourceSchema = zod.z.discriminatedUnion("kind", [zod.z.object({ kind: zod.z.literal("table") }), zod.z.object({
51636
- kind: zod.z.literal("node-benchmark"),
51637
- resultId: zod.z.string(),
51638
- measuredAtMs: zod.z.number()
51639
- })]);
51664
+ var LatencySourceSchema = zod.z.discriminatedUnion("kind", [
51665
+ zod.z.object({ kind: zod.z.literal("table") }),
51666
+ zod.z.object({
51667
+ kind: zod.z.literal("node-benchmark"),
51668
+ resultId: zod.z.string(),
51669
+ measuredAtMs: zod.z.number()
51670
+ }),
51671
+ zod.z.object({
51672
+ kind: zod.z.literal("estimated"),
51673
+ estimatedFrom: HardwareClassIdSchema
51674
+ })
51675
+ ]);
51640
51676
  var DeviceModelChoiceSchema = zod.z.object({
51641
51677
  modelId: zod.z.string(),
51642
51678
  p50Ms: zod.z.number(),
@@ -51644,6 +51680,11 @@ var DeviceModelChoiceSchema = zod.z.object({
51644
51680
  measured: zod.z.boolean(),
51645
51681
  /** Absent on a plan from before D791 — read as the table. */
51646
51682
  source: LatencySourceSchema.optional(),
51683
+ /**
51684
+ * The measured class this latency was scaled from (D930). Absent when the
51685
+ * cell is the table or a node benchmark.
51686
+ */
51687
+ estimatedFrom: HardwareClassIdSchema.optional(),
51647
51688
  /** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
51648
51689
  capacitySessions: zod.z.number(),
51649
51690
  personRecall: zod.z.number()
@@ -67662,681 +67703,1455 @@ includeCrops: zod.z.boolean().optional() }).optional(), zod.z.array(VehicleSchem
67662
67703
  }
67663
67704
  };
67664
67705
  //#endregion
67665
- //#region src/capabilities/platform-probe.cap.ts
67666
- var ModelFormatSchema = zod.z.enum(require_sleep.MODEL_FORMATS);
67667
- var HwAccelBackendInputSchema = zod.z.enum([
67668
- "videotoolbox",
67669
- "cuda",
67670
- "nvdec",
67671
- "vaapi",
67672
- "qsv",
67673
- "d3d11va",
67674
- "dxva2",
67675
- "amf",
67676
- "vdpau",
67677
- "drm",
67678
- "coreml",
67679
- "tensorrt",
67680
- "openvino",
67681
- "directml",
67682
- "webgpu",
67683
- "none"
67684
- ]).nullable().optional();
67685
- var HwAccelResolutionSchema = zod.z.object({
67686
- preferred: zod.z.array(zod.z.string()).readonly(),
67687
- rationale: zod.z.string()
67688
- });
67689
- var HardwareEncoderIdSchema = zod.z.enum([
67690
- "h264_videotoolbox",
67691
- "hevc_videotoolbox",
67692
- "h264_vaapi",
67693
- "hevc_vaapi",
67694
- "h264_nvenc",
67695
- "hevc_nvenc",
67696
- "h264_qsv",
67697
- "hevc_qsv",
67698
- "h264_amf",
67699
- "hevc_amf",
67700
- "libx264",
67701
- "libx265"
67702
- ]);
67703
- var HardwareEncoderProbeSchema = zod.z.object({
67704
- encoder: HardwareEncoderIdSchema,
67705
- codec: zod.z.enum(["H264", "H265"]),
67706
- family: zod.z.enum([
67707
- "videotoolbox",
67708
- "vaapi",
67709
- "nvenc",
67710
- "qsv",
67711
- "amf",
67712
- "software"
67713
- ]),
67714
- available: zod.z.boolean(),
67715
- reason: zod.z.string().optional()
67716
- });
67717
- zod.z.object({
67718
- encoders: zod.z.array(HardwareEncoderProbeSchema).readonly(),
67719
- defaultH264: HardwareEncoderIdSchema,
67720
- defaultH265: HardwareEncoderIdSchema,
67721
- probedAt: zod.z.number()
67722
- });
67723
- zod.z.object({
67724
- methods: zod.z.array(zod.z.string()).readonly(),
67725
- probedAt: zod.z.number()
67726
- });
67727
- var HardwarePlatformSchema = zod.z.enum([
67728
- "darwin",
67729
- "linux",
67730
- "win32"
67731
- ]);
67732
- var HardwareArchSchema = zod.z.enum(["arm64", "x64"]);
67733
- var GpuInfoSchema = zod.z.object({
67734
- type: zod.z.enum([
67735
- "nvidia",
67736
- "amd",
67737
- "intel",
67738
- "apple"
67739
- ]),
67740
- name: zod.z.string(),
67741
- memoryMB: zod.z.number().optional()
67742
- });
67743
- var NpuInfoSchema = zod.z.object({
67744
- type: zod.z.enum([
67745
- "apple-ane",
67746
- "intel-npu",
67747
- "amd-xdna"
67748
- ]),
67749
- name: zod.z.string().optional(),
67750
- generation: zod.z.enum(["xdna1", "xdna2"]).optional()
67751
- });
67752
- var CoralBusSchema = zod.z.enum(["usb", "pci"]);
67753
- var CoralInfoSchema = zod.z.object({
67754
- type: zod.z.literal("coral-edgetpu"),
67755
- /** Legacy single-bus report (older nodes) — read via `coralUsableUnits`. */
67756
- bus: zod.z.string().optional(),
67757
- /** Legacy USB unit count (older nodes) — read via `coralUsableUnits`. */
67758
- count: zod.z.number().int().nonnegative().optional(),
67759
- /** Usable units per bus — each one is its own `edgetpu:<bus>[:N]` device. */
67760
- units: zod.z.array(zod.z.object({
67761
- bus: CoralBusSchema,
67762
- count: zod.z.number().int().nonnegative()
67763
- })).readonly().optional(),
67764
- /** Present-but-unusable units, each with its typed reason (never "absent"). */
67765
- unavailable: zod.z.array(zod.z.object({
67766
- bus: CoralBusSchema,
67767
- count: zod.z.number().int().nonnegative(),
67768
- reason: zod.z.enum(["driver-missing", "device-not-accessible"])
67769
- })).readonly().optional()
67770
- });
67771
- var HardwareInfoSchema = zod.z.object({
67772
- platform: HardwarePlatformSchema,
67773
- arch: HardwareArchSchema,
67774
- cpuModel: zod.z.string(),
67775
- cpuCores: zod.z.number(),
67776
- totalRAM_MB: zod.z.number(),
67777
- availableRAM_MB: zod.z.number(),
67778
- gpu: GpuInfoSchema.nullable(),
67779
- npu: NpuInfoSchema.nullable(),
67780
- coral: CoralInfoSchema.nullable().optional()
67781
- });
67782
- var PlatformScoreSchema = zod.z.object({
67783
- runtime: zod.z.literal("python"),
67784
- backend: zod.z.string(),
67785
- format: zod.z.enum([
67786
- "onnx",
67787
- "coreml",
67788
- "openvino",
67789
- "tflite"
67790
- ]),
67791
- score: zod.z.number(),
67792
- reason: zod.z.string(),
67793
- available: zod.z.boolean()
67794
- });
67795
- var InferenceDeviceDescriptorSchema = zod.z.object({
67796
- key: zod.z.string(),
67797
- backend: zod.z.string(),
67798
- device: zod.z.string(),
67799
- format: ModelFormatSchema,
67800
- runtime: zod.z.literal("python"),
67801
- score: zod.z.number(),
67802
- available: zod.z.boolean()
67803
- });
67804
- var PlatformCapabilitiesSchema = zod.z.object({
67805
- hardware: HardwareInfoSchema,
67806
- scores: zod.z.array(PlatformScoreSchema).readonly(),
67807
- bestScore: PlatformScoreSchema,
67808
- pythonPath: zod.z.string().nullable(),
67809
- devices: zod.z.array(InferenceDeviceDescriptorSchema).readonly()
67810
- });
67811
- var ModelRequirementSchema = zod.z.object({
67812
- modelId: zod.z.string(),
67813
- name: zod.z.string(),
67814
- minRAM_MB: zod.z.number(),
67815
- accuracyScore: zod.z.number(),
67816
- formats: zod.z.array(zod.z.string()).readonly()
67817
- });
67818
- var ResolvedInferenceConfigSchema = zod.z.object({
67819
- modelId: zod.z.string(),
67820
- runtime: zod.z.literal("python"),
67821
- backend: zod.z.string(),
67822
- format: ModelFormatSchema,
67823
- reason: zod.z.string()
67824
- });
67825
- /**
67826
- * The answer of an accelerator rescan (D919): the node's device list after
67827
- * the re-probe, what changed, and the evidence that decided the Coral answer
67828
- * (`usbBus` — could the USB bus be read at all; `decidedBy` — USB ids, the PCI
67829
- * bus, the installed-delegate fallback, or nothing).
67830
- */
67831
- var AcceleratorRescanSchema = zod.z.object({
67832
- devices: zod.z.array(InferenceDeviceDescriptorSchema).readonly(),
67833
- appeared: zod.z.array(zod.z.string()).readonly(),
67834
- disappeared: zod.z.array(zod.z.string()).readonly(),
67835
- usbBus: zod.z.enum(["observed", "unobservable"]),
67836
- decidedBy: zod.z.enum([
67837
- "usb-ids",
67838
- "pci",
67839
- "delegate-fallback",
67840
- "none"
67841
- ])
67842
- });
67843
- var platformProbeCapability = {
67844
- name: "platform-probe",
67845
- scope: "system",
67846
- mode: "singleton",
67847
- methods: {
67848
- getCapabilities: require_sleep.method(zod.z.void(), PlatformCapabilitiesSchema),
67849
- getHardware: require_sleep.method(zod.z.void(), HardwareInfoSchema),
67850
- resolveInferenceConfig: require_sleep.method(zod.z.object({ requirements: zod.z.array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema),
67851
- resolveHwAccel: require_sleep.method(zod.z.object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema),
67852
- /**
67853
- * Re-probe this node's accelerators NOW (Coral USB ids + PCIe apex units;
67854
- * `/sys` and `/dev` reads, no model load) and rebuild the device list that
67855
- * `getCapabilities` serves. The same re-probe runs every 60 s on its own;
67856
- * this is the operator's "rescan inference devices". Target a node with the
67857
- * `{ nodeId }` routing input, like `getCapabilities`.
67858
- */
67859
- rescanAccelerators: require_sleep.method(zod.z.void(), AcceleratorRescanSchema, {
67860
- kind: "mutation",
67861
- auth: "admin"
67862
- })
67863
- }
67864
- };
67865
- //#endregion
67866
- //#region src/capabilities/power-meter.cap.ts
67867
- /**
67868
- * Multi-metric electrical meter. One slice can carry any combination
67869
- * of instantaneous power (W), cumulative energy (kWh), voltage (V),
67870
- * and current (A) — all fields optional so a single-metric source
67871
- * (HA `sensor` with `device_class: power`) populates only what it
67872
- * has and aggregators (energy dashboards, billing exports) compose
67873
- * across providers without needing one cap per metric.
67874
- *
67875
- * Trade-off acknowledged: a HomeKit export that wants to surface
67876
- * "power" and "energy" as separate services has to crack open the
67877
- * slice and emit two characteristics from one cap. Worth it to avoid
67878
- * exploding the cap catalog with `power-w` / `energy-kwh` /
67879
- * `voltage-v` / `current-a` quartets that always travel together
67880
- * in real-world devices.
67881
- */
67882
- var PowerMeterStatusSchema = zod.z.object({
67883
- /** Instantaneous power draw in watts. */
67884
- watts: zod.z.number().optional(),
67885
- /** Cumulative energy in kilowatt-hours since the meter was reset. */
67886
- kwhTotal: zod.z.number().optional(),
67887
- /** Voltage in volts. */
67888
- volts: zod.z.number().optional(),
67889
- /** Current in amperes. */
67890
- amps: zod.z.number().optional(),
67891
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
67892
- va: zod.z.number().optional(),
67893
- /** Power factor (0..1) if reported. */
67894
- powerFactor: zod.z.number().min(0).max(1).optional(),
67895
- /** Ms epoch when the slice was last updated. */
67896
- lastFetchedAt: zod.z.number(),
67897
- /** Live display unit of the single metric this slice carries (e.g. HA
67898
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
67899
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
67900
- * unit per slice is unambiguous. The UI prefers this over the role's
67901
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
67902
- unit: zod.z.string().optional(),
67903
- /** Suggested decimal places for numeric display.
67904
- * Populated live from the upstream source when provided (e.g. HA
67905
- * `attributes.suggested_display_precision`). Falls back to
67906
- * auto-formatting when absent. */
67907
- precision: zod.z.number().int().min(0).max(10).optional()
67908
- });
67909
- var powerMeterCapability = {
67910
- name: "power-meter",
67911
- scope: "device",
67912
- deviceNative: true,
67913
- mode: "singleton",
67914
- deviceTypes: [require_err_msg.DeviceType.Sensor],
67915
- methods: {},
67916
- status: {
67917
- schema: PowerMeterStatusSchema,
67918
- kind: "push"
67919
- },
67920
- runtimeState: PowerMeterStatusSchema,
67921
- /**
67922
- * Runtime-state durability: **restored** — as `numeric-sensor`; `kwhTotal` is an accumulator whose restored value is the baseline.
67923
- *
67924
- * See `RuntimeStateDurability`. Enforced by
67925
- * `scripts/check-runtime-state-durability.ts`.
67926
- */
67927
- durability: "restored",
67928
- /**
67929
- * History (D850): recorded series. Gauges are floored at 30 s; `kwhTotal` is the one monotonic meter (counter).
67930
- */
67931
- history: { fields: {
67932
- watts: {
67933
- kind: "gauge",
67934
- unit: "W",
67935
- deadband: 1,
67936
- minIntervalMs: 3e4
67937
- },
67938
- kwhTotal: {
67939
- kind: "counter",
67940
- unit: "kWh",
67941
- deadband: .01,
67942
- minIntervalMs: 6e4
67943
- },
67944
- volts: {
67945
- kind: "gauge",
67946
- unit: "V",
67947
- deadband: 1,
67948
- minIntervalMs: 3e4
67706
+ //#region src/pipeline/auto-tune/detector-performance.ts
67707
+ var DETECTOR_PERF = {
67708
+ "intel-core-ultra": {
67709
+ "intel-npu": {
67710
+ "yolov9t-320": {
67711
+ p50Ms: 2.9,
67712
+ fps: 383,
67713
+ measured: true
67714
+ },
67715
+ "yolov9t-320-int8": {
67716
+ p50Ms: 3.8,
67717
+ fps: 333,
67718
+ measured: true
67719
+ },
67720
+ "yolov9t-640": {
67721
+ p50Ms: 7.4,
67722
+ fps: 139,
67723
+ measured: true
67724
+ },
67725
+ "yolov9t-640-int8": {
67726
+ p50Ms: 7.5,
67727
+ fps: 144,
67728
+ measured: true
67729
+ },
67730
+ "yolov9s-320": {
67731
+ p50Ms: 4.4,
67732
+ fps: 241,
67733
+ measured: true
67734
+ },
67735
+ "yolov9s-320-int8": {
67736
+ p50Ms: 4.1,
67737
+ fps: 264,
67738
+ measured: true
67739
+ },
67740
+ "yolov9s-640": {
67741
+ p50Ms: 12.7,
67742
+ fps: 82,
67743
+ measured: true
67744
+ },
67745
+ "yolov9s-640-int8": {
67746
+ p50Ms: 10.7,
67747
+ fps: 99,
67748
+ measured: true
67749
+ },
67750
+ "yolov9m-320": {
67751
+ p50Ms: 8.4,
67752
+ fps: 123,
67753
+ measured: true
67754
+ },
67755
+ "yolov9m-320-int8": {
67756
+ p50Ms: 6.5,
67757
+ fps: 165,
67758
+ measured: true
67759
+ },
67760
+ "yolov9m-640": {
67761
+ p50Ms: 29.5,
67762
+ fps: 32,
67763
+ measured: true
67764
+ },
67765
+ "yolov9m-640-int8": {
67766
+ p50Ms: 19.5,
67767
+ fps: 52,
67768
+ measured: true
67769
+ }
67949
67770
  },
67950
- amps: {
67951
- kind: "gauge",
67952
- unit: "A",
67953
- deadband: .05,
67954
- minIntervalMs: 3e4
67771
+ "intel-igpu": {
67772
+ "yolov9t-320": {
67773
+ p50Ms: 2.6,
67774
+ fps: 405,
67775
+ measured: true
67776
+ },
67777
+ "yolov9t-320-int8": {
67778
+ p50Ms: 2.8,
67779
+ fps: 324,
67780
+ measured: true
67781
+ },
67782
+ "yolov9t-640": {
67783
+ p50Ms: 6.5,
67784
+ fps: 157,
67785
+ measured: true
67786
+ },
67787
+ "yolov9t-640-int8": {
67788
+ p50Ms: 5.6,
67789
+ fps: 185,
67790
+ measured: true
67791
+ },
67792
+ "yolov9s-320": {
67793
+ p50Ms: 4.6,
67794
+ fps: 200,
67795
+ measured: true
67796
+ },
67797
+ "yolov9s-320-int8": {
67798
+ p50Ms: 4.1,
67799
+ fps: 239,
67800
+ measured: true
67801
+ },
67802
+ "yolov9s-640": {
67803
+ p50Ms: 15.2,
67804
+ fps: 69,
67805
+ measured: true
67806
+ },
67807
+ "yolov9s-640-int8": {
67808
+ p50Ms: 10.5,
67809
+ fps: 99,
67810
+ measured: true
67811
+ },
67812
+ "yolov9m-320": {
67813
+ p50Ms: 11.4,
67814
+ fps: 88,
67815
+ measured: true
67816
+ },
67817
+ "yolov9m-320-int8": {
67818
+ p50Ms: 7,
67819
+ fps: 127,
67820
+ measured: true
67821
+ },
67822
+ "yolov9m-640": {
67823
+ p50Ms: 40,
67824
+ fps: 25,
67825
+ measured: true
67826
+ },
67827
+ "yolov9m-640-int8": {
67828
+ p50Ms: 23.5,
67829
+ fps: 44,
67830
+ measured: true
67831
+ }
67955
67832
  },
67956
- va: {
67957
- kind: "gauge",
67958
- unit: "VA",
67959
- deadband: 1,
67960
- minIntervalMs: 3e4
67833
+ "x86-cpu": {
67834
+ "yolov9t-320": {
67835
+ p50Ms: 6.9,
67836
+ fps: 145,
67837
+ measured: true
67838
+ },
67839
+ "yolov9t-320-int8": {
67840
+ p50Ms: 2.7,
67841
+ fps: 405,
67842
+ measured: true
67843
+ },
67844
+ "yolov9t-640": {
67845
+ p50Ms: 23,
67846
+ fps: 44,
67847
+ measured: true
67848
+ },
67849
+ "yolov9t-640-int8": {
67850
+ p50Ms: 8.8,
67851
+ fps: 124,
67852
+ measured: true
67853
+ },
67854
+ "yolov9s-320": {
67855
+ p50Ms: 15.8,
67856
+ fps: 63,
67857
+ measured: true
67858
+ },
67859
+ "yolov9s-320-int8": {
67860
+ p50Ms: 6.5,
67861
+ fps: 180,
67862
+ measured: true
67863
+ },
67864
+ "yolov9s-640": {
67865
+ p50Ms: 58.2,
67866
+ fps: 17,
67867
+ measured: true
67868
+ },
67869
+ "yolov9s-640-int8": {
67870
+ p50Ms: 20.1,
67871
+ fps: 52,
67872
+ measured: true
67873
+ },
67874
+ "yolov9m-320": {
67875
+ p50Ms: 40,
67876
+ fps: 25,
67877
+ measured: true
67878
+ },
67879
+ "yolov9m-320-int8": {
67880
+ p50Ms: 14.2,
67881
+ fps: 74,
67882
+ measured: true
67883
+ },
67884
+ "yolov9m-640": {
67885
+ p50Ms: 155.8,
67886
+ fps: 6,
67887
+ measured: true
67888
+ },
67889
+ "yolov9m-640-int8": {
67890
+ p50Ms: 52.4,
67891
+ fps: 19,
67892
+ measured: true
67893
+ }
67961
67894
  },
67962
- powerFactor: {
67963
- kind: "gauge",
67964
- unit: "ratio",
67965
- deadband: .01,
67966
- minIntervalMs: 6e4
67967
- }
67968
- } },
67969
- /** Clock fields: written, but excluded from the compare that decides
67970
- * whether persisting is worth a SQLite commit. */
67971
- volatileStateFields: ["lastFetchedAt"]
67972
- };
67973
- //#endregion
67974
- //#region src/capabilities/presence.cap.ts
67975
- /**
67976
- * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67977
- * on `DeviceType.Presence`. Read-only — no setters: presence is
67978
- * derived from upstream signals (mobile-app location, router DHCP
67979
- * lease, Bluetooth beacon, manual zone change) and pushed into the
67980
- * slice.
67981
- *
67982
- * `state` follows HA's canonical values — `home` / `not_home` /
67983
- * `<zone-name>` for any other named zone. The zone name is freeform
67984
- * because users define arbitrary zones (`work`, `gym`, `school`).
67985
- *
67986
- * GPS coordinates are nullable — tracker types without location
67987
- * (Bluetooth scanner, MQTT presence) omit them. The presence of
67988
- * GPS data is signaled by `DeviceFeature.PresenceGps` for fast
67989
- * UI map-pin rendering.
67990
- */
67991
- var GpsLocationSchema = zod.z.object({
67992
- /** Latitude in decimal degrees, -90..90. */
67993
- latitude: zod.z.number().min(-90).max(90),
67994
- /** Longitude in decimal degrees, -180..180. */
67995
- longitude: zod.z.number().min(-180).max(180),
67996
- /** Reported accuracy in meters (lower = better). */
67997
- accuracyMeters: zod.z.number().nonnegative()
67998
- });
67999
- var PresenceStatusSchema = zod.z.object({
68000
- /** `home` / `not_home` / any user-defined zone name. */
68001
- state: zod.z.string(),
68002
- /** Optional textual location label (zone name, city, address). Null
68003
- * when only the binary state is known. */
68004
- location: zod.z.string().nullable(),
68005
- /** GPS coordinates when available. Null otherwise. */
68006
- gps: GpsLocationSchema.nullable(),
68007
- /** Optional battery level of the tracking device (0..100). Null
68008
- * when not reported. */
68009
- batteryPercent: zod.z.number().min(0).max(100).nullable(),
68010
- /** Ms epoch when the slice was last updated. */
68011
- lastChangedAt: zod.z.number()
68012
- });
68013
- var presenceCapability = {
68014
- name: "presence",
68015
- scope: "device",
68016
- deviceNative: true,
68017
- mode: "singleton",
68018
- deviceTypes: [require_err_msg.DeviceType.Presence],
68019
- methods: {},
68020
- status: {
68021
- schema: PresenceStatusSchema,
68022
- kind: "push"
67895
+ coral: { "yolov9s-320-edgetpu": {
67896
+ p50Ms: 32,
67897
+ fps: 31,
67898
+ measured: false
67899
+ } }
68023
67900
  },
68024
- /**
68025
- * Runtime-state slice — mirrored by the kernel. UI presence card
68026
- * reads `state` + `location` for the text label; iff `gps !== null`
68027
- * the map pin is rendered (use `DeviceFeature.PresenceGps` for the
68028
- * pre-fetch fast-path check).
68029
- */
68030
- runtimeState: PresenceStatusSchema,
68031
- /**
68032
- * Runtime-state durability: **restored** — occupancy-relevant: the restored state is what an occupancy rule compares the first post-restart observation against.
68033
- *
68034
- * See `RuntimeStateDurability`. Enforced by
68035
- * `scripts/check-runtime-state-durability.ts`.
68036
- */
68037
- durability: "restored",
68038
- /**
68039
- * History (D850): recorded series.
68040
- */
68041
- history: { fields: { batteryPercent: {
68042
- kind: "gauge",
68043
- unit: "%",
68044
- deadband: 1
68045
- } } },
68046
- /** Clock fields: written, but excluded from the compare that decides
68047
- * whether persisting is worth a SQLite commit. */
68048
- volatileStateFields: ["lastChangedAt"]
67901
+ "intel-n-series": {
67902
+ "intel-igpu": {
67903
+ "yolov9t-320": {
67904
+ p50Ms: 15,
67905
+ fps: 48,
67906
+ measured: true
67907
+ },
67908
+ "yolov9t-320-int8": {
67909
+ p50Ms: 14.1,
67910
+ fps: 67,
67911
+ measured: true
67912
+ },
67913
+ "yolov9t-640": {
67914
+ p50Ms: 38.5,
67915
+ fps: 26,
67916
+ measured: true
67917
+ },
67918
+ "yolov9t-640-int8": {
67919
+ p50Ms: 50,
67920
+ fps: 20,
67921
+ measured: true
67922
+ },
67923
+ "yolov9s-320": {
67924
+ p50Ms: 41.4,
67925
+ fps: 24,
67926
+ measured: true
67927
+ },
67928
+ "yolov9s-320-int8": {
67929
+ p50Ms: 23.3,
67930
+ fps: 34,
67931
+ measured: true
67932
+ },
67933
+ "yolov9s-640": {
67934
+ p50Ms: 127.4,
67935
+ fps: 8,
67936
+ measured: true
67937
+ },
67938
+ "yolov9s-640-int8": {
67939
+ p50Ms: 81,
67940
+ fps: 12,
67941
+ measured: true
67942
+ },
67943
+ "yolov9m-320": {
67944
+ p50Ms: 63.5,
67945
+ fps: 16,
67946
+ measured: true
67947
+ },
67948
+ "yolov9m-320-int8": {
67949
+ p50Ms: 66,
67950
+ fps: 15,
67951
+ measured: true
67952
+ },
67953
+ "yolov9m-640": {
67954
+ p50Ms: 215.5,
67955
+ fps: 5,
67956
+ measured: true
67957
+ },
67958
+ "yolov9m-640-int8": {
67959
+ p50Ms: 163,
67960
+ fps: 6,
67961
+ measured: true
67962
+ }
67963
+ },
67964
+ "x86-cpu": {
67965
+ "yolov9t-320": {
67966
+ p50Ms: 86.2,
67967
+ fps: 12,
67968
+ measured: true
67969
+ },
67970
+ "yolov9t-320-int8": {
67971
+ p50Ms: 52.7,
67972
+ fps: 20,
67973
+ measured: true
67974
+ },
67975
+ "yolov9t-640": {
67976
+ p50Ms: 322.1,
67977
+ fps: 3,
67978
+ measured: true
67979
+ },
67980
+ "yolov9t-640-int8": {
67981
+ p50Ms: 205.6,
67982
+ fps: 5,
67983
+ measured: true
67984
+ },
67985
+ "yolov9s-320": {
67986
+ p50Ms: 164.4,
67987
+ fps: 6,
67988
+ measured: true
67989
+ },
67990
+ "yolov9s-320-int8": {
67991
+ p50Ms: 86.9,
67992
+ fps: 12,
67993
+ measured: true
67994
+ },
67995
+ "yolov9s-640": {
67996
+ p50Ms: 341.3,
67997
+ fps: 3,
67998
+ measured: true
67999
+ },
68000
+ "yolov9s-640-int8": {
68001
+ p50Ms: 185.3,
68002
+ fps: 5,
68003
+ measured: true
68004
+ },
68005
+ "yolov9m-320": {
68006
+ p50Ms: 166.8,
68007
+ fps: 6,
68008
+ measured: true
68009
+ },
68010
+ "yolov9m-320-int8": {
68011
+ p50Ms: 226.3,
68012
+ fps: 4,
68013
+ measured: true
68014
+ },
68015
+ "yolov9m-640": {
68016
+ p50Ms: 663.5,
68017
+ fps: 2,
68018
+ measured: true
68019
+ },
68020
+ "yolov9m-640-int8": {
68021
+ p50Ms: 324.4,
68022
+ fps: 3,
68023
+ measured: true
68024
+ }
68025
+ },
68026
+ coral: { "yolov9s-320-edgetpu": {
68027
+ p50Ms: 32,
68028
+ fps: 31,
68029
+ measured: false
68030
+ } }
68031
+ },
68032
+ "amd-phoenix": {
68033
+ "x86-cpu": {
68034
+ "yolov9t-320": {
68035
+ p50Ms: 24,
68036
+ fps: 42,
68037
+ measured: true
68038
+ },
68039
+ "yolov9t-320-int8": {
68040
+ p50Ms: 12,
68041
+ fps: 83,
68042
+ measured: true
68043
+ },
68044
+ "yolov9t-640": {
68045
+ p50Ms: 50,
68046
+ fps: 20,
68047
+ measured: true
68048
+ },
68049
+ "yolov9t-640-int8": {
68050
+ p50Ms: 20,
68051
+ fps: 50,
68052
+ measured: true
68053
+ },
68054
+ "yolov9s-320": {
68055
+ p50Ms: 30,
68056
+ fps: 33,
68057
+ measured: true
68058
+ },
68059
+ "yolov9s-320-int8": {
68060
+ p50Ms: 15,
68061
+ fps: 67,
68062
+ measured: true
68063
+ },
68064
+ "yolov9s-640": {
68065
+ p50Ms: 92,
68066
+ fps: 11,
68067
+ measured: true
68068
+ },
68069
+ "yolov9s-640-int8": {
68070
+ p50Ms: 27,
68071
+ fps: 37,
68072
+ measured: true
68073
+ },
68074
+ "yolov9m-320": {
68075
+ p50Ms: 67,
68076
+ fps: 15,
68077
+ measured: true
68078
+ },
68079
+ "yolov9m-320-int8": {
68080
+ p50Ms: 21,
68081
+ fps: 48,
68082
+ measured: true
68083
+ },
68084
+ "yolov9m-640": {
68085
+ p50Ms: 229,
68086
+ fps: 4,
68087
+ measured: true
68088
+ },
68089
+ "yolov9m-640-int8": {
68090
+ p50Ms: 48,
68091
+ fps: 21,
68092
+ measured: true
68093
+ }
68094
+ },
68095
+ "amd-igpu": {
68096
+ "yolov9t-320": {
68097
+ p50Ms: 40,
68098
+ fps: 25,
68099
+ measured: true
68100
+ },
68101
+ "yolov9t-640": {
68102
+ p50Ms: 89,
68103
+ fps: 11,
68104
+ measured: true
68105
+ },
68106
+ "yolov9s-320": {
68107
+ p50Ms: 65,
68108
+ fps: 15,
68109
+ measured: true
68110
+ },
68111
+ "yolov9s-640": {
68112
+ p50Ms: 125,
68113
+ fps: 8,
68114
+ measured: true
68115
+ },
68116
+ "yolov9m-320": {
68117
+ p50Ms: 85,
68118
+ fps: 12,
68119
+ measured: true
68120
+ },
68121
+ "yolov9m-640": {
68122
+ p50Ms: 228,
68123
+ fps: 4,
68124
+ measured: true
68125
+ }
68126
+ },
68127
+ coral: { "yolov9s-320-edgetpu": {
68128
+ p50Ms: 32,
68129
+ fps: 31,
68130
+ measured: true
68131
+ } }
68132
+ },
68133
+ "apple-m-pro": {
68134
+ "apple-ane": {
68135
+ "yolov9t-320": {
68136
+ p50Ms: 1.1,
68137
+ fps: 952,
68138
+ measured: true
68139
+ },
68140
+ "yolov9t-640": {
68141
+ p50Ms: 3,
68142
+ fps: 333,
68143
+ measured: true
68144
+ },
68145
+ "yolov9s-320": {
68146
+ p50Ms: 1.5,
68147
+ fps: 667,
68148
+ measured: true
68149
+ },
68150
+ "yolov9s-640": {
68151
+ p50Ms: 5.1,
68152
+ fps: 196,
68153
+ measured: true
68154
+ },
68155
+ "yolov9m-320": {
68156
+ p50Ms: 3.2,
68157
+ fps: 312,
68158
+ measured: true
68159
+ },
68160
+ "yolov9m-640": {
68161
+ p50Ms: 10.2,
68162
+ fps: 98,
68163
+ measured: true
68164
+ }
68165
+ },
68166
+ coral: { "yolov9s-320-edgetpu": {
68167
+ p50Ms: 32,
68168
+ fps: 31,
68169
+ measured: false
68170
+ } }
68171
+ },
68172
+ "apple-m-base": {
68173
+ "apple-ane": {
68174
+ "yolov9t-320": {
68175
+ p50Ms: 1.32,
68176
+ fps: 758,
68177
+ measured: false
68178
+ },
68179
+ "yolov9t-640": {
68180
+ p50Ms: 3.45,
68181
+ fps: 290,
68182
+ measured: true
68183
+ },
68184
+ "yolov9s-320": {
68185
+ p50Ms: 1.8,
68186
+ fps: 556,
68187
+ measured: false
68188
+ },
68189
+ "yolov9s-640": {
68190
+ p50Ms: 5.46,
68191
+ fps: 183,
68192
+ measured: true
68193
+ },
68194
+ "yolov9m-320": {
68195
+ p50Ms: 3.79,
68196
+ fps: 264,
68197
+ measured: true
68198
+ },
68199
+ "yolov9m-640": {
68200
+ p50Ms: 10.34,
68201
+ fps: 97,
68202
+ measured: true
68203
+ }
68204
+ },
68205
+ coral: { "yolov9s-320-edgetpu": {
68206
+ p50Ms: 32,
68207
+ fps: 31,
68208
+ measured: false
68209
+ } }
68210
+ }
68049
68211
  };
68050
- //#endregion
68051
- //#region src/capabilities/pressure-sensor.cap.ts
68052
- /**
68053
- * Atmospheric pressure reading in hectopascals. Drives Home Assistant
68054
- * `sensor` entries with `device_class: pressure`.
68055
- *
68056
- * Unit normalisation: hPa. The canonical display unit (`hPa`) is a
68057
- * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68058
- * `sourceInfo`.
68059
- */
68060
- var PressureSensorStatusSchema = zod.z.object({
68061
- /** Current pressure in hPa. */
68062
- hpa: zod.z.number(),
68063
- /** Ms epoch when the slice was last updated. */
68064
- lastFetchedAt: zod.z.number(),
68065
- /** Live display unit from the upstream source (e.g. HA
68066
- * `attributes.unit_of_measurement`). The UI prefers this over the
68067
- * role's canonical unit. Absent → fall back to the canonical unit. */
68068
- unit: zod.z.string().optional(),
68069
- /** Suggested decimal places for numeric display.
68070
- * Populated live from the upstream source when provided (e.g. HA
68071
- * `attributes.suggested_display_precision`). Falls back to
68072
- * auto-formatting when absent. */
68073
- precision: zod.z.number().int().min(0).max(10).optional()
68074
- });
68075
- var pressureSensorCapability = {
68076
- name: "pressure-sensor",
68077
- scope: "device",
68078
- deviceNative: true,
68079
- mode: "singleton",
68080
- deviceTypes: [require_err_msg.DeviceType.Sensor],
68081
- methods: {},
68082
- status: {
68083
- schema: PressureSensorStatusSchema,
68084
- kind: "push"
68212
+ var DETECTOR_ACCURACY = {
68213
+ "yolov9m-320": {
68214
+ personPrecision: .979,
68215
+ personRecall: .649
68085
68216
  },
68086
- runtimeState: PressureSensorStatusSchema,
68087
- /**
68088
- * Runtime-state durability: **restored** — as `numeric-sensor`.
68089
- *
68090
- * See `RuntimeStateDurability`. Enforced by
68091
- * `scripts/check-runtime-state-durability.ts`.
68092
- */
68093
- durability: "restored",
68094
- /**
68095
- * History (D850): recorded series.
68096
- */
68097
- history: { fields: { hpa: {
68098
- kind: "gauge",
68099
- unit: "hPa",
68100
- deadband: .1
68101
- } } },
68102
- /** Clock fields: written, but excluded from the compare that decides
68103
- * whether persisting is worth a SQLite commit. */
68104
- volatileStateFields: ["lastFetchedAt"]
68217
+ "yolov9m-320-int8": {
68218
+ personPrecision: .974,
68219
+ personRecall: .684
68220
+ },
68221
+ "yolov9m-640": {
68222
+ personPrecision: .976,
68223
+ personRecall: .854
68224
+ },
68225
+ "yolov9m-640-int8": {
68226
+ personPrecision: .976,
68227
+ personRecall: .847
68228
+ },
68229
+ "yolov9s-320": {
68230
+ personPrecision: .958,
68231
+ personRecall: .479
68232
+ },
68233
+ "yolov9s-320-int8": {
68234
+ personPrecision: .969,
68235
+ personRecall: .438
68236
+ },
68237
+ "yolov9s-640": {
68238
+ personPrecision: .986,
68239
+ personRecall: .753
68240
+ },
68241
+ "yolov9s-640-int8": {
68242
+ personPrecision: .963,
68243
+ personRecall: .823
68244
+ },
68245
+ "yolov9t-320": {
68246
+ personPrecision: .946,
68247
+ personRecall: .368
68248
+ },
68249
+ "yolov9t-320-int8": {
68250
+ personPrecision: .943,
68251
+ personRecall: .347
68252
+ },
68253
+ "yolov9t-640": {
68254
+ personPrecision: .979,
68255
+ personRecall: .646
68256
+ },
68257
+ "yolov9t-640-int8": {
68258
+ personPrecision: .979,
68259
+ personRecall: .646
68260
+ }
68105
68261
  };
68106
68262
  //#endregion
68107
- //#region src/capabilities/privacy-mask.cap.ts
68108
- /**
68109
- * PRIVACY — what the camera deliberately does not capture. Two planes:
68110
- *
68111
- * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
68112
- * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
68113
- * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
68114
- * 4 vertices, not necessarily axis-aligned). The cap composes the shared
68115
- * rect|polygon subset of the MaskShape vocabulary. All coords are
68116
- * normalized 0..1 (top-left origin).
68117
- * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
68118
- * camera encoding an audio track at all, so EVERY consumer — live view,
68119
- * recording, the audio analyzer, an export — sees silent video. There is
68120
- * no server-side copy of this fact; the camera is the store and every read
68121
- * is a read-through, which is why a switch over it cannot drift
68122
- * ([D62](../../../../docs/decisions/adr-0062.md)).
68123
- *
68124
- * Both belong here for one reason: they are the two things an operator turns
68125
- * off when the answer to "what is this camera allowed to record" changes, and
68126
- * both are applied ON the device, before anything leaves it.
68127
- *
68128
- * **The audio flag has exactly one writer.** `stream-params` used to carry a
68129
- * per-profile `audio` in its patch schema — reachable from no UI and honoured
68130
- * by one provider — and it was removed when this landed. A second writer onto
68131
- * one device register is the shape of every knob this repo has shipped that
68132
- * disagreed with the one the reader read.
68263
+ //#region src/pipeline/auto-tune/accelerator-compute.ts
68264
+ /**
68265
+ * Capacity of an accelerator we have not measured (D930).
68266
+ *
68267
+ * The latency table stays the measured columns. A device that reports compute
68268
+ * (OpenVINO GOPS, or execution units × clock, or compute units, or TOPS) is
68269
+ * estimated from the nearest measured column of the same accelerator kind,
68270
+ * scaled by the compute ratio and clamped. A node benchmark still replaces
68271
+ * the estimate. A device that reports neither compute nor a known class stays
68272
+ * `unknown`.
68273
+ *
68274
+ * A failed probe field is `null`, never `0` (D393): zero would be a compute
68275
+ * ratio of zero and would size the device as if it could not run.
68276
+ */
68277
+ /**
68278
+ * An unmeasured device is never estimated outside this band of its reference.
68279
+ *
68280
+ * Peak GOPS and detector latency do not move together: a 640 model is memory
68281
+ * bound, a 320 model less so, and the N100→N305 public EU×clock ratio is
68282
+ * about 2.2×. 2× is the widest that spread justifies without a measurement
68283
+ * on the device; 0.5× is the symmetric floor so a tiny iGPU is not told it
68284
+ * is many times slower than the weakest column we measured. A node benchmark
68285
+ * replaces the estimate and is not clamped.
68286
+ */
68287
+ var ESTIMATE_COMPUTE_RATIO_MIN = .5;
68288
+ var ESTIMATE_COMPUTE_RATIO_MAX = 2;
68289
+ /**
68290
+ * Within this of 1.0, a device whose class IS the reference is that measured
68291
+ * machine (a probe clock a few percent off the spec), not a different chip.
68292
+ * An unknown class is still estimated, so the plan says so.
68293
+ */
68294
+ var REFERENCE_MATCH_FRACTION = .05;
68295
+ /** N100 iGPU: 24 execution units, 750 MHz graphics max (Intel specification). */
68296
+ var N100_IGPU_EU = 24;
68297
+ var N100_IGPU_CLOCK_MHZ = 750;
68298
+ /**
68299
+ * Core Ultra 7 265K iGPU: 4 Xe-cores. OpenVINO's execution-unit count for
68300
+ * Xe-LP is 16 per Xe-core, and the graphics max dynamic frequency is 2000 MHz.
68301
+ * Used only to choose the nearest column for a chip that is not itself a
68302
+ * Core Ultra — that class is the measurement and is never scaled.
68303
+ */
68304
+ var CORE_ULTRA_265K_IGPU_EU = 64;
68305
+ var CORE_ULTRA_265K_IGPU_CLOCK_MHZ = 2e3;
68306
+ /** Phoenix / Hawk Point Radeon 780M: 12 compute units. */
68307
+ var PHOENIX_780M_CU = 12;
68308
+ var positiveOrNull = zod.z.number().nullable().transform((value) => value !== null && Number.isFinite(value) && value > 0 ? value : null);
68309
+ var textOrNull = zod.z.string().nullable().transform((value) => value !== null && value.trim() !== "" ? value : null);
68310
+ var PrecisionGopsSchema = zod.z.object({
68311
+ int8: positiveOrNull,
68312
+ fp16: positiveOrNull
68313
+ });
68314
+ /** What one accelerator device reported. Every failed query is null. */
68315
+ var AcceleratorComputeSchema = zod.z.object({
68316
+ deviceKey: zod.z.string(),
68317
+ fullDeviceName: textOrNull,
68318
+ executionUnits: positiveOrNull,
68319
+ clockMHz: positiveOrNull,
68320
+ gops: PrecisionGopsSchema.nullable(),
68321
+ architecture: textOrNull,
68322
+ npuName: textOrNull,
68323
+ tops: positiveOrNull,
68324
+ vendor: textOrNull,
68325
+ webgpuArchitecture: textOrNull,
68326
+ webgpuDevice: textOrNull,
68327
+ computeUnits: positiveOrNull
68328
+ });
68329
+ var AcceleratorFactsPayloadSchema = zod.z.object({ accelerators: zod.z.array(AcceleratorComputeSchema).nullable() });
68330
+ /**
68331
+ * Compute of the machines the latency columns were measured on.
68332
+ * GOPS is preferred when a column carries it; none of these do yet — the
68333
+ * boxes were measured before the probe reported `DEVICE_GOPS` — so the scale
68334
+ * uses execution units × clock (Intel) or compute units (AMD) until a
68335
+ * re-measure locks a GOPS figure on the column.
68336
+ */
68337
+ var REFERENCE_COMPUTE = [
68338
+ {
68339
+ hardwareClass: "intel-n-series",
68340
+ accelerator: "intel-igpu",
68341
+ score: {
68342
+ metric: "eu-mhz",
68343
+ value: 24 * 750
68344
+ }
68345
+ },
68346
+ {
68347
+ hardwareClass: "intel-core-ultra",
68348
+ accelerator: "intel-igpu",
68349
+ score: {
68350
+ metric: "eu-mhz",
68351
+ value: 64 * CORE_ULTRA_265K_IGPU_CLOCK_MHZ
68352
+ }
68353
+ },
68354
+ {
68355
+ hardwareClass: "amd-phoenix",
68356
+ accelerator: "amd-igpu",
68357
+ score: {
68358
+ metric: "cu",
68359
+ value: 12
68360
+ }
68361
+ }
68362
+ ];
68363
+ /** Parse a probe payload. `null` means the probe failed — not "no accelerators". */
68364
+ function parseAcceleratorFactsJson(raw) {
68365
+ const parsed = AcceleratorFactsPayloadSchema.safeParse(raw);
68366
+ if (!parsed.success || parsed.data.accelerators === null) return null;
68367
+ return parsed.data.accelerators;
68368
+ }
68369
+ function positive$1(value) {
68370
+ return value !== null && value > 0 ? value : null;
68371
+ }
68372
+ /** Scores in the order the estimate prefers. A non-positive field is absent. */
68373
+ function computeScores(report) {
68374
+ const scores = [];
68375
+ const int8 = positive$1(report.gops?.int8 ?? null);
68376
+ if (int8 !== null) scores.push({
68377
+ metric: "gops-int8",
68378
+ value: int8
68379
+ });
68380
+ const fp16 = positive$1(report.gops?.fp16 ?? null);
68381
+ if (fp16 !== null) scores.push({
68382
+ metric: "gops-fp16",
68383
+ value: fp16
68384
+ });
68385
+ const units = positive$1(report.executionUnits);
68386
+ const clock = positive$1(report.clockMHz);
68387
+ if (units !== null && clock !== null) scores.push({
68388
+ metric: "eu-mhz",
68389
+ value: units * clock
68390
+ });
68391
+ const computeUnits = positive$1(report.computeUnits);
68392
+ if (computeUnits !== null) scores.push({
68393
+ metric: "cu",
68394
+ value: computeUnits
68395
+ });
68396
+ const tops = positive$1(report.tops);
68397
+ if (tops !== null) scores.push({
68398
+ metric: "tops",
68399
+ value: tops
68400
+ });
68401
+ return scores;
68402
+ }
68403
+ function nearestReference(kind, score, references) {
68404
+ const same = references.filter((ref) => ref.accelerator === kind && ref.score.metric === score.metric && ref.score.value > 0);
68405
+ const first = same[0];
68406
+ if (first === void 0) return null;
68407
+ return same.reduce((best, ref) => {
68408
+ return Math.abs(Math.log(score.value / ref.score.value)) < Math.abs(Math.log(score.value / best.score.value)) ? ref : best;
68409
+ }, first);
68410
+ }
68411
+ function scoreAgainstReferences(report, kind, references) {
68412
+ for (const score of computeScores(report)) if (nearestReference(kind, score, references) !== null) return score;
68413
+ return null;
68414
+ }
68415
+ /** True when this report can scale a measured column of `kind`. */
68416
+ function reportIsScalable(report, kind, references = REFERENCE_COMPUTE) {
68417
+ return scoreAgainstReferences(report, kind, references) !== null;
68418
+ }
68419
+ function classColumn(perf, hardwareClass, accelerator) {
68420
+ if (hardwareClass === "unknown") return void 0;
68421
+ return perf[hardwareClass]?.[accelerator];
68422
+ }
68423
+ function round1(value) {
68424
+ return Math.round(value * 10) / 10;
68425
+ }
68426
+ function clampRatio(ratio) {
68427
+ return Math.min(2, Math.max(ESTIMATE_COMPUTE_RATIO_MIN, ratio));
68428
+ }
68429
+ function scaleColumn(column, ratio, estimatedFrom) {
68430
+ const out = {};
68431
+ for (const [modelId, cell] of Object.entries(column)) out[modelId] = {
68432
+ p50Ms: round1(cell.p50Ms / ratio),
68433
+ fps: Math.max(1, Math.round(cell.fps * ratio)),
68434
+ measured: false,
68435
+ estimatedFrom
68436
+ };
68437
+ return out;
68438
+ }
68439
+ /**
68440
+ * The latency column for one device. A measured class other than the N-series
68441
+ * spread is returned unchanged — one class there is one measured machine.
68442
+ * `intel-n-series` is the N100 column; a different reported compute scales it.
68443
+ * An unknown class scales from the nearest reference of the same kind.
68133
68444
  */
68134
- /** A privacy-mask region's geometry — rectangle or free polygon. */
68135
- var PrivacyMaskShapeSchema = zod.z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
68136
- /** A single privacy-mask zone. */
68137
- var PrivacyMaskRegionSchema = zod.z.object({
68138
- /** Slot id, 0-based. Stable across read/write. */
68139
- id: zod.z.number(),
68140
- /** Whether this zone is active (blanked out by the camera). */
68141
- enabled: zod.z.boolean(),
68142
- shape: PrivacyMaskShapeSchema
68445
+ function resolveLatencyColumn(input) {
68446
+ const perf = input.perf ?? DETECTOR_PERF;
68447
+ const references = input.references ?? REFERENCE_COMPUTE;
68448
+ const measured = classColumn(perf, input.hardwareClass, input.accelerator);
68449
+ if (measured !== void 0 && input.hardwareClass !== "intel-n-series") return {
68450
+ column: measured,
68451
+ estimatedFrom: null
68452
+ };
68453
+ const score = input.report === null ? null : scoreAgainstReferences(input.report, input.accelerator, references);
68454
+ if (score === null) return measured === void 0 ? null : {
68455
+ column: measured,
68456
+ estimatedFrom: null
68457
+ };
68458
+ const nearest = nearestReference(input.accelerator, score, references);
68459
+ const base = nearest === null ? void 0 : classColumn(perf, nearest.hardwareClass, input.accelerator);
68460
+ if (nearest === null || base === void 0) return measured === void 0 ? null : {
68461
+ column: measured,
68462
+ estimatedFrom: null
68463
+ };
68464
+ const rawRatio = score.value / nearest.score.value;
68465
+ if (input.hardwareClass === nearest.hardwareClass && Math.abs(rawRatio - 1) <= .05) return {
68466
+ column: base,
68467
+ estimatedFrom: null
68468
+ };
68469
+ return {
68470
+ column: scaleColumn(base, clampRatio(rawRatio), nearest.hardwareClass),
68471
+ estimatedFrom: nearest.hardwareClass
68472
+ };
68473
+ }
68474
+ //#endregion
68475
+ //#region src/capabilities/platform-probe.cap.ts
68476
+ var ModelFormatSchema = zod.z.enum(require_sleep.MODEL_FORMATS);
68477
+ var HwAccelBackendInputSchema = zod.z.enum([
68478
+ "videotoolbox",
68479
+ "cuda",
68480
+ "nvdec",
68481
+ "vaapi",
68482
+ "qsv",
68483
+ "d3d11va",
68484
+ "dxva2",
68485
+ "amf",
68486
+ "vdpau",
68487
+ "drm",
68488
+ "coreml",
68489
+ "tensorrt",
68490
+ "openvino",
68491
+ "directml",
68492
+ "webgpu",
68493
+ "none"
68494
+ ]).nullable().optional();
68495
+ var HwAccelResolutionSchema = zod.z.object({
68496
+ preferred: zod.z.array(zod.z.string()).readonly(),
68497
+ rationale: zod.z.string()
68143
68498
  });
68144
- /** Current on-camera privacy state — mask master enable + zones + microphone. */
68145
- var PrivacyMaskStatusSchema = zod.z.object({
68146
- enabled: zod.z.boolean(),
68147
- /**
68148
- * Active zones (normalized 0..1). Length ≤ maxRegions.
68149
- *
68150
- * `null` means "no answer" — the provider has never reached this camera
68151
- * (a cold `runtimeState` slice, a battery camera asleep) or the read
68152
- * failed. A consumer must render it as UNKNOWN and never as "this camera
68153
- * has no zones": those two look identical to an operator right up to the
68154
- * moment one of them is acted on, and acting on them differs completely.
68155
- * The switch group turned an unanswered read into *"nothing is set up for
68156
- * it to act on"* — an instruction to go and draw a zone that already
68157
- * exists — and this editor seeded an EMPTY canvas from it, one Save away
68158
- * from deleting the mask it could not read.
68159
- *
68160
- * Empty-and-answered is `[]` and stays a real answer: this camera genuinely
68161
- * has no zones. The distinction is the same one {@link
68162
- * PrivacyMaskStatusSchema.shape.audioEnabled} already draws for the
68163
- * microphone, on the very same payload — both planes come from ONE refresh,
68164
- * so it was never coherent for one of them to be able to say "unknown"
68165
- * while the other could not.
68166
- */
68167
- regions: zod.z.array(PrivacyMaskRegionSchema).nullable(),
68168
- /**
68169
- * Is the camera capturing sound right now? Read from the camera, never from
68170
- * a server-side mirror.
68171
- *
68172
- * `null` means "no answer" — either this camera exposes no controllable
68173
- * microphone (`getOptions().supportsAudioMute === false`) or the read
68174
- * failed. A consumer must render `null` as UNKNOWN and never as `false`:
68175
- * "the microphone is off" and "we could not ask" look identical to an
68176
- * operator only until one of them is wrong.
68177
- *
68178
- * On a camera whose profiles carry the flag independently (Reolink writes
68179
- * it per stream), `true` means AT LEAST ONE profile still carries audio —
68180
- * privacy is only satisfied when every one of them is silent.
68181
- */
68182
- audioEnabled: zod.z.boolean().nullable(),
68183
- lastFetchedAt: zod.z.number()
68499
+ var HardwareEncoderIdSchema = zod.z.enum([
68500
+ "h264_videotoolbox",
68501
+ "hevc_videotoolbox",
68502
+ "h264_vaapi",
68503
+ "hevc_vaapi",
68504
+ "h264_nvenc",
68505
+ "hevc_nvenc",
68506
+ "h264_qsv",
68507
+ "hevc_qsv",
68508
+ "h264_amf",
68509
+ "hevc_amf",
68510
+ "libx264",
68511
+ "libx265"
68512
+ ]);
68513
+ var HardwareEncoderProbeSchema = zod.z.object({
68514
+ encoder: HardwareEncoderIdSchema,
68515
+ codec: zod.z.enum(["H264", "H265"]),
68516
+ family: zod.z.enum([
68517
+ "videotoolbox",
68518
+ "vaapi",
68519
+ "nvenc",
68520
+ "qsv",
68521
+ "amf",
68522
+ "software"
68523
+ ]),
68524
+ available: zod.z.boolean(),
68525
+ reason: zod.z.string().optional()
68184
68526
  });
68185
- /** Per-camera availability. Probed, never assumed from the model name. */
68186
- var PrivacyMaskOptionsSchema = zod.z.object({
68187
- /** Maximum number of supported zones. */
68188
- maxRegions: zod.z.number(),
68189
- /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
68190
- supportedShapes: zod.z.array(MaskShapeKindSchema),
68191
- /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
68192
- polygonVertices: MaskPolygonVerticesSchema.optional(),
68527
+ zod.z.object({
68528
+ encoders: zod.z.array(HardwareEncoderProbeSchema).readonly(),
68529
+ defaultH264: HardwareEncoderIdSchema,
68530
+ defaultH265: HardwareEncoderIdSchema,
68531
+ probedAt: zod.z.number()
68532
+ });
68533
+ zod.z.object({
68534
+ methods: zod.z.array(zod.z.string()).readonly(),
68535
+ probedAt: zod.z.number()
68536
+ });
68537
+ var HardwarePlatformSchema = zod.z.enum([
68538
+ "darwin",
68539
+ "linux",
68540
+ "win32"
68541
+ ]);
68542
+ var HardwareArchSchema = zod.z.enum(["arm64", "x64"]);
68543
+ var GpuInfoSchema = zod.z.object({
68544
+ type: zod.z.enum([
68545
+ "nvidia",
68546
+ "amd",
68547
+ "intel",
68548
+ "apple"
68549
+ ]),
68550
+ name: zod.z.string(),
68551
+ memoryMB: zod.z.number().optional()
68552
+ });
68553
+ var NpuInfoSchema = zod.z.object({
68554
+ type: zod.z.enum([
68555
+ "apple-ane",
68556
+ "intel-npu",
68557
+ "amd-xdna"
68558
+ ]),
68559
+ name: zod.z.string().optional(),
68560
+ generation: zod.z.enum(["xdna1", "xdna2"]).optional()
68561
+ });
68562
+ var CoralBusSchema = zod.z.enum(["usb", "pci"]);
68563
+ var CoralInfoSchema = zod.z.object({
68564
+ type: zod.z.literal("coral-edgetpu"),
68565
+ /** Legacy single-bus report (older nodes) — read via `coralUsableUnits`. */
68566
+ bus: zod.z.string().optional(),
68567
+ /** Legacy USB unit count (older nodes) — read via `coralUsableUnits`. */
68568
+ count: zod.z.number().int().nonnegative().optional(),
68569
+ /** Usable units per bus — each one is its own `edgetpu:<bus>[:N]` device. */
68570
+ units: zod.z.array(zod.z.object({
68571
+ bus: CoralBusSchema,
68572
+ count: zod.z.number().int().nonnegative()
68573
+ })).readonly().optional(),
68574
+ /** Present-but-unusable units, each with its typed reason (never "absent"). */
68575
+ unavailable: zod.z.array(zod.z.object({
68576
+ bus: CoralBusSchema,
68577
+ count: zod.z.number().int().nonnegative(),
68578
+ reason: zod.z.enum(["driver-missing", "device-not-accessible"])
68579
+ })).readonly().optional()
68580
+ });
68581
+ var HardwareInfoSchema = zod.z.object({
68582
+ platform: HardwarePlatformSchema,
68583
+ arch: HardwareArchSchema,
68584
+ cpuModel: zod.z.string(),
68585
+ cpuCores: zod.z.number(),
68586
+ totalRAM_MB: zod.z.number(),
68587
+ availableRAM_MB: zod.z.number(),
68588
+ gpu: GpuInfoSchema.nullable(),
68589
+ npu: NpuInfoSchema.nullable(),
68590
+ coral: CoralInfoSchema.nullable().optional(),
68193
68591
  /**
68194
- * Does this camera expose a microphone switch we can actually write?
68195
- *
68196
- * Camera-probed: `true` only when the firmware answered with an audio flag
68197
- * we know how to patch. A camera that never answered is `false` — a control
68198
- * the operator can press that changes nothing is worse than no control, and
68199
- * the switch group renders "not available" instead.
68592
+ * Per-accelerator compute (D930). Omitted by a node that has not probed it.
68593
+ * `null` fields inside an entry are a failed query, never a zero.
68200
68594
  */
68201
- supportsAudioMute: zod.z.boolean()
68595
+ accelerators: zod.z.array(AcceleratorComputeSchema).readonly().optional()
68202
68596
  });
68203
- /** Partial change — every field optional. */
68204
- var PrivacyMaskPatchSchema = zod.z.object({
68205
- enabled: zod.z.boolean().optional(),
68206
- regions: zod.z.array(PrivacyMaskRegionSchema).optional()
68597
+ var PlatformScoreSchema = zod.z.object({
68598
+ runtime: zod.z.literal("python"),
68599
+ backend: zod.z.string(),
68600
+ format: zod.z.enum([
68601
+ "onnx",
68602
+ "coreml",
68603
+ "openvino",
68604
+ "tflite"
68605
+ ]),
68606
+ score: zod.z.number(),
68607
+ reason: zod.z.string(),
68608
+ available: zod.z.boolean()
68207
68609
  });
68208
- var privacyMaskCapability = {
68209
- name: "privacy-mask",
68210
- scope: "device",
68211
- deviceNative: true,
68610
+ var InferenceDeviceDescriptorSchema = zod.z.object({
68611
+ key: zod.z.string(),
68612
+ backend: zod.z.string(),
68613
+ device: zod.z.string(),
68614
+ format: ModelFormatSchema,
68615
+ runtime: zod.z.literal("python"),
68616
+ score: zod.z.number(),
68617
+ available: zod.z.boolean()
68618
+ });
68619
+ var PlatformCapabilitiesSchema = zod.z.object({
68620
+ hardware: HardwareInfoSchema,
68621
+ scores: zod.z.array(PlatformScoreSchema).readonly(),
68622
+ bestScore: PlatformScoreSchema,
68623
+ pythonPath: zod.z.string().nullable(),
68624
+ devices: zod.z.array(InferenceDeviceDescriptorSchema).readonly()
68625
+ });
68626
+ var ModelRequirementSchema = zod.z.object({
68627
+ modelId: zod.z.string(),
68628
+ name: zod.z.string(),
68629
+ minRAM_MB: zod.z.number(),
68630
+ accuracyScore: zod.z.number(),
68631
+ formats: zod.z.array(zod.z.string()).readonly()
68632
+ });
68633
+ var ResolvedInferenceConfigSchema = zod.z.object({
68634
+ modelId: zod.z.string(),
68635
+ runtime: zod.z.literal("python"),
68636
+ backend: zod.z.string(),
68637
+ format: ModelFormatSchema,
68638
+ reason: zod.z.string()
68639
+ });
68640
+ /**
68641
+ * The answer of an accelerator rescan (D919): the node's device list after
68642
+ * the re-probe, what changed, and the evidence that decided the Coral answer
68643
+ * (`usbBus` — could the USB bus be read at all; `decidedBy` — USB ids, the PCI
68644
+ * bus, the installed-delegate fallback, or nothing).
68645
+ */
68646
+ var AcceleratorRescanSchema = zod.z.object({
68647
+ devices: zod.z.array(InferenceDeviceDescriptorSchema).readonly(),
68648
+ appeared: zod.z.array(zod.z.string()).readonly(),
68649
+ disappeared: zod.z.array(zod.z.string()).readonly(),
68650
+ usbBus: zod.z.enum(["observed", "unobservable"]),
68651
+ decidedBy: zod.z.enum([
68652
+ "usb-ids",
68653
+ "pci",
68654
+ "delegate-fallback",
68655
+ "none"
68656
+ ])
68657
+ });
68658
+ var platformProbeCapability = {
68659
+ name: "platform-probe",
68660
+ scope: "system",
68212
68661
  mode: "singleton",
68213
- deviceTypes: [require_err_msg.DeviceType.Camera],
68214
- deviceConfig: { ui: {
68215
- kind: "widget",
68216
- widgetId: "host/privacy-mask-grid",
68217
- tab: "image",
68218
- label: "Privacy Mask"
68219
- } },
68220
68662
  methods: {
68221
- getOptions: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), PrivacyMaskOptionsSchema),
68222
- setMask: require_sleep.method(zod.z.object({
68223
- deviceId: zod.z.number(),
68224
- patch: PrivacyMaskPatchSchema
68225
- }), zod.z.void(), {
68226
- kind: "mutation",
68227
- auth: "admin"
68228
- }),
68663
+ getCapabilities: require_sleep.method(zod.z.void(), PlatformCapabilitiesSchema),
68664
+ getHardware: require_sleep.method(zod.z.void(), HardwareInfoSchema),
68665
+ resolveInferenceConfig: require_sleep.method(zod.z.object({ requirements: zod.z.array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema),
68666
+ resolveHwAccel: require_sleep.method(zod.z.object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema),
68229
68667
  /**
68230
- * Turn the camera's microphone on or off, at the camera.
68231
- *
68232
- * Deliberately its OWN mutation rather than a field on
68233
- * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
68234
- * mask master switch", and overloading it would make one boolean mean two
68235
- * unrelated things on the same call. It is also the only method here whose
68236
- * write leaves the device in a state a later `getStatus` reads back
68237
- * verbatim, which is what makes it safe as a switch authority.
68238
- *
68239
- * A camera whose `getOptions().supportsAudioMute` is false must REJECT
68240
- * this rather than silently accept it — a write nothing applies is exactly
68241
- * what the switch group exists to remove.
68668
+ * Re-probe this node's accelerators NOW (Coral USB ids + PCIe apex units;
68669
+ * `/sys` and `/dev` reads, no model load) and rebuild the device list that
68670
+ * `getCapabilities` serves. The same re-probe runs every 60 s on its own;
68671
+ * this is the operator's "rescan inference devices". Target a node with the
68672
+ * `{ nodeId }` routing input, like `getCapabilities`.
68242
68673
  */
68243
- setAudioEnabled: require_sleep.method(zod.z.object({
68244
- deviceId: zod.z.number(),
68245
- enabled: zod.z.boolean()
68246
- }), zod.z.void(), {
68674
+ rescanAccelerators: require_sleep.method(zod.z.void(), AcceleratorRescanSchema, {
68247
68675
  kind: "mutation",
68248
68676
  auth: "admin"
68249
68677
  })
68250
- },
68678
+ }
68679
+ };
68680
+ //#endregion
68681
+ //#region src/capabilities/power-meter.cap.ts
68682
+ /**
68683
+ * Multi-metric electrical meter. One slice can carry any combination
68684
+ * of instantaneous power (W), cumulative energy (kWh), voltage (V),
68685
+ * and current (A) — all fields optional so a single-metric source
68686
+ * (HA `sensor` with `device_class: power`) populates only what it
68687
+ * has and aggregators (energy dashboards, billing exports) compose
68688
+ * across providers without needing one cap per metric.
68689
+ *
68690
+ * Trade-off acknowledged: a HomeKit export that wants to surface
68691
+ * "power" and "energy" as separate services has to crack open the
68692
+ * slice and emit two characteristics from one cap. Worth it to avoid
68693
+ * exploding the cap catalog with `power-w` / `energy-kwh` /
68694
+ * `voltage-v` / `current-a` quartets that always travel together
68695
+ * in real-world devices.
68696
+ */
68697
+ var PowerMeterStatusSchema = zod.z.object({
68698
+ /** Instantaneous power draw in watts. */
68699
+ watts: zod.z.number().optional(),
68700
+ /** Cumulative energy in kilowatt-hours since the meter was reset. */
68701
+ kwhTotal: zod.z.number().optional(),
68702
+ /** Voltage in volts. */
68703
+ volts: zod.z.number().optional(),
68704
+ /** Current in amperes. */
68705
+ amps: zod.z.number().optional(),
68706
+ /** Apparent power in volt-amperes (W × power-factor reciprocal). */
68707
+ va: zod.z.number().optional(),
68708
+ /** Power factor (0..1) if reported. */
68709
+ powerFactor: zod.z.number().min(0).max(1).optional(),
68710
+ /** Ms epoch when the slice was last updated. */
68711
+ lastFetchedAt: zod.z.number(),
68712
+ /** Live display unit of the single metric this slice carries (e.g. HA
68713
+ * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
68714
+ * Each upstream `sensor.*` entity surfaces ONE device_class, so one
68715
+ * unit per slice is unambiguous. The UI prefers this over the role's
68716
+ * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
68717
+ unit: zod.z.string().optional(),
68718
+ /** Suggested decimal places for numeric display.
68719
+ * Populated live from the upstream source when provided (e.g. HA
68720
+ * `attributes.suggested_display_precision`). Falls back to
68721
+ * auto-formatting when absent. */
68722
+ precision: zod.z.number().int().min(0).max(10).optional()
68723
+ });
68724
+ var powerMeterCapability = {
68725
+ name: "power-meter",
68726
+ scope: "device",
68727
+ deviceNative: true,
68728
+ mode: "singleton",
68729
+ deviceTypes: [require_err_msg.DeviceType.Sensor],
68730
+ methods: {},
68251
68731
  status: {
68252
- schema: PrivacyMaskStatusSchema,
68253
- kind: "poll"
68732
+ schema: PowerMeterStatusSchema,
68733
+ kind: "push"
68254
68734
  },
68255
- runtimeState: PrivacyMaskStatusSchema,
68735
+ runtimeState: PowerMeterStatusSchema,
68256
68736
  /**
68257
- * Runtime-state durability: **restored** — operator-drawn regions, zero real churn — 22 writes in 25 minutes, every one of them the clock.
68737
+ * Runtime-state durability: **restored** — as `numeric-sensor`; `kwhTotal` is an accumulator whose restored value is the baseline.
68258
68738
  *
68259
68739
  * See `RuntimeStateDurability`. Enforced by
68260
68740
  * `scripts/check-runtime-state-durability.ts`.
68261
68741
  */
68262
68742
  durability: "restored",
68263
68743
  /**
68264
- * History (D850): none — configuration.
68744
+ * History (D850): recorded series. Gauges are floored at 30 s; `kwhTotal` is the one monotonic meter (counter).
68265
68745
  */
68266
- history: "none",
68746
+ history: { fields: {
68747
+ watts: {
68748
+ kind: "gauge",
68749
+ unit: "W",
68750
+ deadband: 1,
68751
+ minIntervalMs: 3e4
68752
+ },
68753
+ kwhTotal: {
68754
+ kind: "counter",
68755
+ unit: "kWh",
68756
+ deadband: .01,
68757
+ minIntervalMs: 6e4
68758
+ },
68759
+ volts: {
68760
+ kind: "gauge",
68761
+ unit: "V",
68762
+ deadband: 1,
68763
+ minIntervalMs: 3e4
68764
+ },
68765
+ amps: {
68766
+ kind: "gauge",
68767
+ unit: "A",
68768
+ deadband: .05,
68769
+ minIntervalMs: 3e4
68770
+ },
68771
+ va: {
68772
+ kind: "gauge",
68773
+ unit: "VA",
68774
+ deadband: 1,
68775
+ minIntervalMs: 3e4
68776
+ },
68777
+ powerFactor: {
68778
+ kind: "gauge",
68779
+ unit: "ratio",
68780
+ deadband: .01,
68781
+ minIntervalMs: 6e4
68782
+ }
68783
+ } },
68267
68784
  /** Clock fields: written, but excluded from the compare that decides
68268
68785
  * whether persisting is worth a SQLite commit. */
68269
68786
  volatileStateFields: ["lastFetchedAt"]
68270
68787
  };
68271
- /**
68272
- * Collapse a camera's PER-PROFILE audio flags into the one answer
68273
- * {@link PrivacyMaskStatusSchema.shape.audioEnabled} promises.
68274
- *
68275
- * Both firmwares this cap talks to store the flag per stream profile, and
68276
- * both let those profiles disagree. The rule is `some`, not `every`: privacy
68277
- * is only satisfied when NOTHING is carrying sound, so a camera whose sub
68278
- * stream is still audible must read as `true` and be switchable off — not as
68279
- * `false` because the main stream happens to be muted already.
68280
- *
68281
- * An empty list is `null` ("this camera reported no audio flag at all"),
68282
- * never `false`.
68283
- *
68284
- * Lives here rather than in each provider so the rule the schema documents
68285
- * and the rule the providers apply cannot drift apart.
68286
- */
68287
- function summarisePrivacyAudio(profiles) {
68288
- if (profiles.length === 0) return null;
68289
- return profiles.some((p) => p.audioEnabled);
68290
- }
68291
68788
  //#endregion
68292
- //#region src/capabilities/ptz.cap.ts
68789
+ //#region src/capabilities/presence.cap.ts
68293
68790
  /**
68294
- * What a preset id DOES on this camera.
68295
- *
68296
- * On several PTZ firmwares the preset namespace is shared: low ids are stored
68297
- * positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
68298
- * (Hikvision) returns its own reserved band in the preset list, named by the
68299
- * firmware:
68300
- *
68301
- * 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
68302
- * 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
68303
- * 95 Call OSD menu
68791
+ * Presence cap. Models HA `person.*` and `device_tracker.*` entities
68792
+ * on `DeviceType.Presence`. Read-only — no setters: presence is
68793
+ * derived from upstream signals (mobile-app location, router DHCP
68794
+ * lease, Bluetooth beacon, manual zone change) and pushed into the
68795
+ * slice.
68304
68796
  *
68305
- * We used to hand all of these to the UI as ordinary presets. Saving over one
68306
- * is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
68307
- * camera refusing to let "Back to origin" be overwritten, surfaced to the
68308
- * operator as a transport failure.
68797
+ * `state` follows HA's canonical values — `home` / `not_home` /
68798
+ * `<zone-name>` for any other named zone. The zone name is freeform
68799
+ * because users define arbitrary zones (`work`, `gym`, `school`).
68309
68800
  *
68310
- * A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
68311
- * is the single most useful thing that camera can do. Only `savePreset` and
68312
- * `deletePreset` are gated.
68801
+ * GPS coordinates are nullable — tracker types without location
68802
+ * (Bluetooth scanner, MQTT presence) omit them. The presence of
68803
+ * GPS data is signaled by `DeviceFeature.PresenceGps` for fast
68804
+ * UI map-pin rendering.
68313
68805
  */
68314
- var PtzPresetFunctionSchema = zod.z.enum([
68315
- "home",
68316
- "day-mode",
68317
- "night-mode",
68318
- "day-night-auto",
68319
- "set-limits",
68320
- "save-limits",
68321
- "reboot",
68322
- "osd-menu"
68323
- ]);
68324
- var PtzPresetSchema = zod.z.object({
68325
- id: zod.z.string(),
68326
- name: zod.z.string(),
68327
- /**
68328
- * The firmware function this id performs, when the provider KNOWS the id is
68329
- * reserved. `null` = an ordinary stored position, OR a reserved id this
68330
- * provider does not recognise -- the two are told apart by `writable`.
68331
- */
68332
- fn: PtzPresetFunctionSchema.nullable(),
68333
- /**
68334
- * Whether `savePreset` / `deletePreset` may target this id. A provider sets
68335
- * it false only for a band it KNOWS is reserved; where the firmware is not
68336
- * documented it stays true and a refusal comes from the camera, named.
68337
- */
68338
- writable: zod.z.boolean()
68339
- });
68806
+ var GpsLocationSchema = zod.z.object({
68807
+ /** Latitude in decimal degrees, -90..90. */
68808
+ latitude: zod.z.number().min(-90).max(90),
68809
+ /** Longitude in decimal degrees, -180..180. */
68810
+ longitude: zod.z.number().min(-180).max(180),
68811
+ /** Reported accuracy in meters (lower = better). */
68812
+ accuracyMeters: zod.z.number().nonnegative()
68813
+ });
68814
+ var PresenceStatusSchema = zod.z.object({
68815
+ /** `home` / `not_home` / any user-defined zone name. */
68816
+ state: zod.z.string(),
68817
+ /** Optional textual location label (zone name, city, address). Null
68818
+ * when only the binary state is known. */
68819
+ location: zod.z.string().nullable(),
68820
+ /** GPS coordinates when available. Null otherwise. */
68821
+ gps: GpsLocationSchema.nullable(),
68822
+ /** Optional battery level of the tracking device (0..100). Null
68823
+ * when not reported. */
68824
+ batteryPercent: zod.z.number().min(0).max(100).nullable(),
68825
+ /** Ms epoch when the slice was last updated. */
68826
+ lastChangedAt: zod.z.number()
68827
+ });
68828
+ var presenceCapability = {
68829
+ name: "presence",
68830
+ scope: "device",
68831
+ deviceNative: true,
68832
+ mode: "singleton",
68833
+ deviceTypes: [require_err_msg.DeviceType.Presence],
68834
+ methods: {},
68835
+ status: {
68836
+ schema: PresenceStatusSchema,
68837
+ kind: "push"
68838
+ },
68839
+ /**
68840
+ * Runtime-state slice — mirrored by the kernel. UI presence card
68841
+ * reads `state` + `location` for the text label; iff `gps !== null`
68842
+ * the map pin is rendered (use `DeviceFeature.PresenceGps` for the
68843
+ * pre-fetch fast-path check).
68844
+ */
68845
+ runtimeState: PresenceStatusSchema,
68846
+ /**
68847
+ * Runtime-state durability: **restored** — occupancy-relevant: the restored state is what an occupancy rule compares the first post-restart observation against.
68848
+ *
68849
+ * See `RuntimeStateDurability`. Enforced by
68850
+ * `scripts/check-runtime-state-durability.ts`.
68851
+ */
68852
+ durability: "restored",
68853
+ /**
68854
+ * History (D850): recorded series.
68855
+ */
68856
+ history: { fields: { batteryPercent: {
68857
+ kind: "gauge",
68858
+ unit: "%",
68859
+ deadband: 1
68860
+ } } },
68861
+ /** Clock fields: written, but excluded from the compare that decides
68862
+ * whether persisting is worth a SQLite commit. */
68863
+ volatileStateFields: ["lastChangedAt"]
68864
+ };
68865
+ //#endregion
68866
+ //#region src/capabilities/pressure-sensor.cap.ts
68867
+ /**
68868
+ * Atmospheric pressure reading in hectopascals. Drives Home Assistant
68869
+ * `sensor` entries with `device_class: pressure`.
68870
+ *
68871
+ * Unit normalisation: hPa. The canonical display unit (`hPa`) is a
68872
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68873
+ * `sourceInfo`.
68874
+ */
68875
+ var PressureSensorStatusSchema = zod.z.object({
68876
+ /** Current pressure in hPa. */
68877
+ hpa: zod.z.number(),
68878
+ /** Ms epoch when the slice was last updated. */
68879
+ lastFetchedAt: zod.z.number(),
68880
+ /** Live display unit from the upstream source (e.g. HA
68881
+ * `attributes.unit_of_measurement`). The UI prefers this over the
68882
+ * role's canonical unit. Absent → fall back to the canonical unit. */
68883
+ unit: zod.z.string().optional(),
68884
+ /** Suggested decimal places for numeric display.
68885
+ * Populated live from the upstream source when provided (e.g. HA
68886
+ * `attributes.suggested_display_precision`). Falls back to
68887
+ * auto-formatting when absent. */
68888
+ precision: zod.z.number().int().min(0).max(10).optional()
68889
+ });
68890
+ var pressureSensorCapability = {
68891
+ name: "pressure-sensor",
68892
+ scope: "device",
68893
+ deviceNative: true,
68894
+ mode: "singleton",
68895
+ deviceTypes: [require_err_msg.DeviceType.Sensor],
68896
+ methods: {},
68897
+ status: {
68898
+ schema: PressureSensorStatusSchema,
68899
+ kind: "push"
68900
+ },
68901
+ runtimeState: PressureSensorStatusSchema,
68902
+ /**
68903
+ * Runtime-state durability: **restored** — as `numeric-sensor`.
68904
+ *
68905
+ * See `RuntimeStateDurability`. Enforced by
68906
+ * `scripts/check-runtime-state-durability.ts`.
68907
+ */
68908
+ durability: "restored",
68909
+ /**
68910
+ * History (D850): recorded series.
68911
+ */
68912
+ history: { fields: { hpa: {
68913
+ kind: "gauge",
68914
+ unit: "hPa",
68915
+ deadband: .1
68916
+ } } },
68917
+ /** Clock fields: written, but excluded from the compare that decides
68918
+ * whether persisting is worth a SQLite commit. */
68919
+ volatileStateFields: ["lastFetchedAt"]
68920
+ };
68921
+ //#endregion
68922
+ //#region src/capabilities/privacy-mask.cap.ts
68923
+ /**
68924
+ * PRIVACY — what the camera deliberately does not capture. Two planes:
68925
+ *
68926
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
68927
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
68928
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
68929
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
68930
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
68931
+ * normalized 0..1 (top-left origin).
68932
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
68933
+ * camera encoding an audio track at all, so EVERY consumer — live view,
68934
+ * recording, the audio analyzer, an export — sees silent video. There is
68935
+ * no server-side copy of this fact; the camera is the store and every read
68936
+ * is a read-through, which is why a switch over it cannot drift
68937
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
68938
+ *
68939
+ * Both belong here for one reason: they are the two things an operator turns
68940
+ * off when the answer to "what is this camera allowed to record" changes, and
68941
+ * both are applied ON the device, before anything leaves it.
68942
+ *
68943
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
68944
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
68945
+ * by one provider — and it was removed when this landed. A second writer onto
68946
+ * one device register is the shape of every knob this repo has shipped that
68947
+ * disagreed with the one the reader read.
68948
+ */
68949
+ /** A privacy-mask region's geometry — rectangle or free polygon. */
68950
+ var PrivacyMaskShapeSchema = zod.z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
68951
+ /** A single privacy-mask zone. */
68952
+ var PrivacyMaskRegionSchema = zod.z.object({
68953
+ /** Slot id, 0-based. Stable across read/write. */
68954
+ id: zod.z.number(),
68955
+ /** Whether this zone is active (blanked out by the camera). */
68956
+ enabled: zod.z.boolean(),
68957
+ shape: PrivacyMaskShapeSchema
68958
+ });
68959
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
68960
+ var PrivacyMaskStatusSchema = zod.z.object({
68961
+ enabled: zod.z.boolean(),
68962
+ /**
68963
+ * Active zones (normalized 0..1). Length ≤ maxRegions.
68964
+ *
68965
+ * `null` means "no answer" — the provider has never reached this camera
68966
+ * (a cold `runtimeState` slice, a battery camera asleep) or the read
68967
+ * failed. A consumer must render it as UNKNOWN and never as "this camera
68968
+ * has no zones": those two look identical to an operator right up to the
68969
+ * moment one of them is acted on, and acting on them differs completely.
68970
+ * The switch group turned an unanswered read into *"nothing is set up for
68971
+ * it to act on"* — an instruction to go and draw a zone that already
68972
+ * exists — and this editor seeded an EMPTY canvas from it, one Save away
68973
+ * from deleting the mask it could not read.
68974
+ *
68975
+ * Empty-and-answered is `[]` and stays a real answer: this camera genuinely
68976
+ * has no zones. The distinction is the same one {@link
68977
+ * PrivacyMaskStatusSchema.shape.audioEnabled} already draws for the
68978
+ * microphone, on the very same payload — both planes come from ONE refresh,
68979
+ * so it was never coherent for one of them to be able to say "unknown"
68980
+ * while the other could not.
68981
+ */
68982
+ regions: zod.z.array(PrivacyMaskRegionSchema).nullable(),
68983
+ /**
68984
+ * Is the camera capturing sound right now? Read from the camera, never from
68985
+ * a server-side mirror.
68986
+ *
68987
+ * `null` means "no answer" — either this camera exposes no controllable
68988
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
68989
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
68990
+ * "the microphone is off" and "we could not ask" look identical to an
68991
+ * operator only until one of them is wrong.
68992
+ *
68993
+ * On a camera whose profiles carry the flag independently (Reolink writes
68994
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
68995
+ * privacy is only satisfied when every one of them is silent.
68996
+ */
68997
+ audioEnabled: zod.z.boolean().nullable(),
68998
+ lastFetchedAt: zod.z.number()
68999
+ });
69000
+ /** Per-camera availability. Probed, never assumed from the model name. */
69001
+ var PrivacyMaskOptionsSchema = zod.z.object({
69002
+ /** Maximum number of supported zones. */
69003
+ maxRegions: zod.z.number(),
69004
+ /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
69005
+ supportedShapes: zod.z.array(MaskShapeKindSchema),
69006
+ /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
69007
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
69008
+ /**
69009
+ * Does this camera expose a microphone switch we can actually write?
69010
+ *
69011
+ * Camera-probed: `true` only when the firmware answered with an audio flag
69012
+ * we know how to patch. A camera that never answered is `false` — a control
69013
+ * the operator can press that changes nothing is worse than no control, and
69014
+ * the switch group renders "not available" instead.
69015
+ */
69016
+ supportsAudioMute: zod.z.boolean()
69017
+ });
69018
+ /** Partial change — every field optional. */
69019
+ var PrivacyMaskPatchSchema = zod.z.object({
69020
+ enabled: zod.z.boolean().optional(),
69021
+ regions: zod.z.array(PrivacyMaskRegionSchema).optional()
69022
+ });
69023
+ var privacyMaskCapability = {
69024
+ name: "privacy-mask",
69025
+ scope: "device",
69026
+ deviceNative: true,
69027
+ mode: "singleton",
69028
+ deviceTypes: [require_err_msg.DeviceType.Camera],
69029
+ deviceConfig: { ui: {
69030
+ kind: "widget",
69031
+ widgetId: "host/privacy-mask-grid",
69032
+ tab: "image",
69033
+ label: "Privacy Mask"
69034
+ } },
69035
+ methods: {
69036
+ getOptions: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), PrivacyMaskOptionsSchema),
69037
+ setMask: require_sleep.method(zod.z.object({
69038
+ deviceId: zod.z.number(),
69039
+ patch: PrivacyMaskPatchSchema
69040
+ }), zod.z.void(), {
69041
+ kind: "mutation",
69042
+ auth: "admin"
69043
+ }),
69044
+ /**
69045
+ * Turn the camera's microphone on or off, at the camera.
69046
+ *
69047
+ * Deliberately its OWN mutation rather than a field on
69048
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
69049
+ * mask master switch", and overloading it would make one boolean mean two
69050
+ * unrelated things on the same call. It is also the only method here whose
69051
+ * write leaves the device in a state a later `getStatus` reads back
69052
+ * verbatim, which is what makes it safe as a switch authority.
69053
+ *
69054
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
69055
+ * this rather than silently accept it — a write nothing applies is exactly
69056
+ * what the switch group exists to remove.
69057
+ */
69058
+ setAudioEnabled: require_sleep.method(zod.z.object({
69059
+ deviceId: zod.z.number(),
69060
+ enabled: zod.z.boolean()
69061
+ }), zod.z.void(), {
69062
+ kind: "mutation",
69063
+ auth: "admin"
69064
+ })
69065
+ },
69066
+ status: {
69067
+ schema: PrivacyMaskStatusSchema,
69068
+ kind: "poll"
69069
+ },
69070
+ runtimeState: PrivacyMaskStatusSchema,
69071
+ /**
69072
+ * Runtime-state durability: **restored** — operator-drawn regions, zero real churn — 22 writes in 25 minutes, every one of them the clock.
69073
+ *
69074
+ * See `RuntimeStateDurability`. Enforced by
69075
+ * `scripts/check-runtime-state-durability.ts`.
69076
+ */
69077
+ durability: "restored",
69078
+ /**
69079
+ * History (D850): none — configuration.
69080
+ */
69081
+ history: "none",
69082
+ /** Clock fields: written, but excluded from the compare that decides
69083
+ * whether persisting is worth a SQLite commit. */
69084
+ volatileStateFields: ["lastFetchedAt"]
69085
+ };
69086
+ /**
69087
+ * Collapse a camera's PER-PROFILE audio flags into the one answer
69088
+ * {@link PrivacyMaskStatusSchema.shape.audioEnabled} promises.
69089
+ *
69090
+ * Both firmwares this cap talks to store the flag per stream profile, and
69091
+ * both let those profiles disagree. The rule is `some`, not `every`: privacy
69092
+ * is only satisfied when NOTHING is carrying sound, so a camera whose sub
69093
+ * stream is still audible must read as `true` and be switchable off — not as
69094
+ * `false` because the main stream happens to be muted already.
69095
+ *
69096
+ * An empty list is `null` ("this camera reported no audio flag at all"),
69097
+ * never `false`.
69098
+ *
69099
+ * Lives here rather than in each provider so the rule the schema documents
69100
+ * and the rule the providers apply cannot drift apart.
69101
+ */
69102
+ function summarisePrivacyAudio(profiles) {
69103
+ if (profiles.length === 0) return null;
69104
+ return profiles.some((p) => p.audioEnabled);
69105
+ }
69106
+ //#endregion
69107
+ //#region src/capabilities/ptz.cap.ts
69108
+ /**
69109
+ * What a preset id DOES on this camera.
69110
+ *
69111
+ * On several PTZ firmwares the preset namespace is shared: low ids are stored
69112
+ * positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
69113
+ * (Hikvision) returns its own reserved band in the preset list, named by the
69114
+ * firmware:
69115
+ *
69116
+ * 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
69117
+ * 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
69118
+ * 95 Call OSD menu
69119
+ *
69120
+ * We used to hand all of these to the UI as ordinary presets. Saving over one
69121
+ * is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
69122
+ * camera refusing to let "Back to origin" be overwritten, surfaced to the
69123
+ * operator as a transport failure.
69124
+ *
69125
+ * A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
69126
+ * is the single most useful thing that camera can do. Only `savePreset` and
69127
+ * `deletePreset` are gated.
69128
+ */
69129
+ var PtzPresetFunctionSchema = zod.z.enum([
69130
+ "home",
69131
+ "day-mode",
69132
+ "night-mode",
69133
+ "day-night-auto",
69134
+ "set-limits",
69135
+ "save-limits",
69136
+ "reboot",
69137
+ "osd-menu"
69138
+ ]);
69139
+ var PtzPresetSchema = zod.z.object({
69140
+ id: zod.z.string(),
69141
+ name: zod.z.string(),
69142
+ /**
69143
+ * The firmware function this id performs, when the provider KNOWS the id is
69144
+ * reserved. `null` = an ordinary stored position, OR a reserved id this
69145
+ * provider does not recognise -- the two are told apart by `writable`.
69146
+ */
69147
+ fn: PtzPresetFunctionSchema.nullable(),
69148
+ /**
69149
+ * Whether `savePreset` / `deletePreset` may target this id. A provider sets
69150
+ * it false only for a band it KNOWS is reserved; where the firmware is not
69151
+ * documented it stays true and a refusal comes from the camera, named.
69152
+ */
69153
+ writable: zod.z.boolean()
69154
+ });
68340
69155
  /**
68341
69156
  * Where the head is pointing -- per axis, and `null` when the driver cannot
68342
69157
  * read it.
@@ -71686,7 +72501,20 @@ var ZoneRuleSchema = zod.z.object({
71686
72501
  * Defaults to `true` so operators creating a rule via the UI
71687
72502
  * see it active immediately.
71688
72503
  */
71689
- enabled: zod.z.boolean().default(true)
72504
+ enabled: zod.z.boolean().default(true),
72505
+ /**
72506
+ * Package stage only. When true, a package candidate may open from a
72507
+ * scene difference in this rule's zone after a person leaves. The
72508
+ * package model, when it boxes something overlapping, is extra evidence
72509
+ * and is not the gate. Absent or false keeps today's model path.
72510
+ */
72511
+ detectBySceneDifference: zod.z.boolean().optional(),
72512
+ /**
72513
+ * Package stage only. When true, a promoted package is shown to the
72514
+ * `llm` cap before the delivery event is emitted. Absent or false emits
72515
+ * as today. Usable with or without `detectBySceneDifference`.
72516
+ */
72517
+ confirmWithLlm: zod.z.boolean().optional()
71690
72518
  });
71691
72519
  /**
71692
72520
  * Convenience array schema — used by addon device-settings
@@ -105603,898 +106431,341 @@ function stepModelRunsOnFormat(models, modelId, format) {
105603
106431
  const named = models.find((m) => m.id === modelId);
105604
106432
  if (named !== void 0 && hasBuild(named, format)) return true;
105605
106433
  }
105606
- return models.some((m) => m.legacy !== true && hasBuild(m, format));
105607
- }
105608
- //#endregion
105609
- //#region src/pipeline/removed-model-ids.ts
105610
- /**
105611
- * Model ids DELETED from a step's catalog (D703, D710, D719), per step — and
105612
- * the one question asked of them: is a stored pin to this id a pin to a model
105613
- * that no longer exists? (D725)
105614
- *
105615
- * This is NOT a resolver input. `resolveModelForFormat` still returns an
105616
- * unknown id unchanged (a custom model), and a bulk pass pinned to one still
105617
- * refuses rather than substitutes (D54). The ONE reader is the orchestrator's
105618
- * stored-pin reset (`removed-model-pin-reset.ts`), which clears such a pin from
105619
- * the store so the step follows its default again — unless a registered custom
105620
- * model carries the same id.
105621
- *
105622
- * `removed-model-ids.spec.ts` (addon-pipeline) pins that every id here is
105623
- * really absent from its step's catalog. A model deleted from a catalog is
105624
- * added here in the same change, or its stored pins stay behind as custom ids.
105625
- */
105626
- /** `stepId → ids deleted from that step's catalog`. */
105627
- var REMOVED_MODEL_IDS = {
105628
- "object-detection": ["efficientdet-lite0-edgetpu", "ssdlite-mobiledet-coco-edgetpu"],
105629
- "face-detection": [
105630
- "scrypted-yolov9t-face",
105631
- "scrfd-2.5g",
105632
- "ssd-mobilenet-v2-face-edgetpu"
105633
- ],
105634
- "face-embedding": ["arcface-r100", "inception-resnet-v1"],
105635
- "plate-detection": ["yolov8n-plate"],
105636
- "plate-ocr": ["vgg-english-g2"],
105637
- "clip-embedding": [
105638
- "mobileclip-s0",
105639
- "mobileclip-s1",
105640
- "mobileclip-s2"
105641
- ],
105642
- "package-detection": ["yolov8n-package"],
105643
- "bird-classifier": ["bird-classifier-aiy-edgetpu", "bird-nabirds-404"],
105644
- "animal-classifier": ["animals-10"],
105645
- "vehicle-classifier": ["vehicle-type-efficientnet"]
105646
- };
105647
- /** True when `modelId` was deleted from `stepId`'s catalog. */
105648
- function isRemovedModelId(stepId, modelId) {
105649
- return REMOVED_MODEL_IDS[stepId]?.includes(modelId) ?? false;
105650
- }
105651
- //#endregion
105652
- //#region src/pipeline/model-bound-settings.ts
105653
- /** Suffix of the sibling key that names the model a stored value was made for. */
105654
- var MODEL_BINDING_SUFFIX = "ModelId";
105655
- /** `minConfidencePerson` → `minConfidencePersonModelId`. */
105656
- function modelBindingKey(key) {
105657
- return `${key}${MODEL_BINDING_SUFFIX}`;
105658
- }
105659
- /** The model a stored value was made for, or `undefined` when unbound. */
105660
- function storedBinding(settings, key) {
105661
- const binding = settings?.[modelBindingKey(key)];
105662
- return typeof binding === "string" && binding.length > 0 ? binding : void 0;
105663
- }
105664
- /**
105665
- * The effective value of `key` for the model that runs (`runningModelId`,
105666
- * `undefined` when unknown — it then matches no binding).
105667
- */
105668
- function resolveModelBoundSetting(settings, key, runningModelId, modelDefault, globalDefault) {
105669
- const stored = settings?.[key];
105670
- const binding = storedBinding(settings, key);
105671
- if (stored !== void 0 && binding === void 0) return {
105672
- value: stored,
105673
- source: "override"
105674
- };
105675
- if (stored !== void 0 && binding !== void 0 && binding === runningModelId) return {
105676
- value: stored,
105677
- source: "override",
105678
- boundModelId: binding
105679
- };
105680
- const inapplicable = stored !== void 0 && binding !== void 0 ? { inapplicableOverride: {
105681
- value: stored,
105682
- modelId: binding
105683
- } } : {};
105684
- if (modelDefault !== void 0) return {
105685
- value: modelDefault,
105686
- source: "model-default",
105687
- ...inapplicable
105688
- };
105689
- return {
105690
- value: globalDefault,
105691
- source: "global-default",
105692
- ...inapplicable
105693
- };
105694
- }
105695
- /**
105696
- * The OPERATOR layer of a step's settings as the running model sees it: every
105697
- * model-bound value made for another model is removed (so the default
105698
- * underneath — the running model's — wins the merge), and every binding key is
105699
- * stripped. Unbound values are kept. Returns a fresh object; `undefined` stays
105700
- * `undefined`.
105701
- */
105702
- function applyModelBoundSettings(settings, boundKeys, runningModelId) {
105703
- if (settings === void 0) return void 0;
105704
- const out = { ...settings };
105705
- for (const key of boundKeys) {
105706
- const binding = storedBinding(settings, key);
105707
- if (binding !== void 0 && binding !== runningModelId) delete out[key];
105708
- delete out[modelBindingKey(key)];
105709
- }
105710
- return out;
105711
- }
105712
- /** Store `value` for `key`, made for `modelId`. Returns a fresh object. */
105713
- function writeModelBoundSetting(settings, key, value, modelId) {
105714
- return {
105715
- ...settings,
105716
- [key]: value,
105717
- [modelBindingKey(key)]: modelId
105718
- };
105719
- }
105720
- /** Remove an override and its binding — "reset to the model default". */
105721
- function clearModelBoundSetting(settings, key) {
105722
- const out = { ...settings };
105723
- delete out[key];
105724
- delete out[modelBindingKey(key)];
105725
- return out;
105726
- }
105727
- /** True when any of `boundKeys` has an override IN FORCE for `modelId`. */
105728
- function hasModelBoundOverrides(settings, boundKeys, modelId) {
105729
- return boundKeys.some((key) => resolveModelBoundSetting(settings, key, modelId, void 0, void 0).source === "override");
105730
- }
105731
- /**
105732
- * "Reset thresholds to model defaults": remove every override of `boundKeys`
105733
- * that is in force for `modelId` (bound to it, or unbound), with its binding —
105734
- * one result, so the editor writes it in one save. An override stored for
105735
- * ANOTHER model is kept: it is not in force here, and it applies again when
105736
- * that model runs.
105737
- */
105738
- function clearModelBoundOverrides(settings, boundKeys, modelId) {
105739
- return boundKeys.reduce((acc, key) => resolveModelBoundSetting(acc, key, modelId, void 0, void 0).source === "override" ? clearModelBoundSetting(acc, key) : acc, { ...settings });
105740
- }
105741
- /**
105742
- * The keys of every `bindsToModel` field of a step's config schema, groups
105743
- * flattened (D738). One definition for the runner (which applies the binding)
105744
- * and the orchestrator's Auto tune (which clears those overrides, D745).
105745
- */
105746
- function collectModelBoundKeys(fields) {
105747
- return fields.flatMap((field) => {
105748
- if (field.type === "group") return collectModelBoundKeys(field.fields);
105749
- return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
105750
- });
105751
- }
105752
- //#endregion
105753
- //#region src/pipeline/step-settings-layers.ts
105754
- /**
105755
- * The layers of a detection step's settings, and the field-level edits on them
105756
- * (D754, D927).
105757
- *
105758
- * A step's settings reach a session through two stored layers, merged key by
105759
- * key — a key in the higher layer wins:
105760
- *
105761
- * node the node's base for the accelerator that runs the step
105762
- * camera `cameraSettings[cam].stepSettings[step]` — camera-semantic
105763
- * keys only, on every accelerator (never a model-bound field)
105764
- *
105765
- * Below them sits the default: the running model's (D738) or the field's own.
105766
- *
105767
- * Every layer is SPARSE and FIELD-LEVEL. A key present in a layer is an
105768
- * override; an absent key inherits. Two consequences that every editor must
105769
- * respect, and why this module exists:
105770
- *
105771
- * - A model-bound field (D738) occupies TWO keys, its value and
105772
- * `<key>ModelId`. They are written, compared and removed as one unit — a
105773
- * value without its binding over a lower layer's binding would be judged
105774
- * against the wrong model.
105775
- * - An editor writes a DIFF against the merged layers below, never a copy of
105776
- * them. A copy pins every field at the value it had that day, so later
105777
- * edits below never reach it (the full-copy "Override" this replaced).
105778
- */
105779
- /** Top-down order of the stored layers. */
105780
- var LAYER_ORDER = ["camera", "node"];
105781
- /**
105782
- * The keys one field occupies in a bag: its own, plus its binding key when it
105783
- * binds to a model. `boundKeys` = `collectModelBoundKeys(schema)`.
105784
- */
105785
- function fieldUnitKeys(key, boundKeys) {
105786
- return boundKeys.includes(key) ? [key, modelBindingKey(key)] : [key];
105787
- }
105788
- /** The field a stored key belongs to (`minConfidencePersonModelId` → `minConfidencePerson`). */
105789
- function unitOf(storedKey, boundKeys) {
105790
- return boundKeys.find((k) => modelBindingKey(k) === storedKey) ?? storedKey;
105791
- }
105792
- /** Structural equality for stored values (numbers, strings, booleans, arrays, plain objects). */
105793
- function settingValuesEqual(a, b) {
105794
- if (Object.is(a, b)) return true;
105795
- if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => settingValuesEqual(v, b[i]));
105796
- if (isPlainObject(a) && isPlainObject(b)) {
105797
- const ka = Object.keys(a);
105798
- const kb = Object.keys(b);
105799
- return ka.length === kb.length && ka.every((k) => settingValuesEqual(a[k], b[k]));
105800
- }
105801
- return false;
105802
- }
105803
- function isPlainObject(v) {
105804
- return typeof v === "object" && v !== null && !Array.isArray(v);
105805
- }
105806
- /** Merge bags bottom-up (later wins, per key). Fresh object. */
105807
- function mergeSettingsBags(...bags) {
105808
- return bags.reduce((acc, bag) => ({
105809
- ...acc,
105810
- ...bag
105811
- }), {});
105812
- }
105813
- /** The merged value of the layers BELOW `layer` (what removing a key from `layer` falls back to). */
105814
- function settingsBelow(layers, layer) {
105815
- return mergeSettingsBags(...LAYER_ORDER.slice(LAYER_ORDER.indexOf(layer) + 1).toReversed().map((id) => layers[id]));
105816
- }
105817
- /** The effective bag: every stored layer merged. */
105818
- function effectiveStepSettings(layers) {
105819
- return mergeSettingsBags(layers.node, layers.camera);
105820
- }
105821
- /** The highest stored layer that holds any key of `key`'s unit, or `default`. */
105822
- function stepSettingSource(layers, key, boundKeys) {
105823
- const unit = fieldUnitKeys(key, boundKeys);
105824
- for (const id of LAYER_ORDER) {
105825
- const bag = layers[id];
105826
- if (bag !== void 0 && unit.some((k) => bag[k] !== void 0)) return id;
105827
- }
105828
- return "default";
105829
- }
105830
- /**
105831
- * `layer` with `key`'s unit set from `values` (a bag carrying the unit's new
105832
- * value — e.g. the editor's whole next settings bag). A unit key that `values`
105833
- * does not carry is removed from the layer. Fresh object.
105834
- */
105835
- function withFieldFrom(layer, key, values, boundKeys) {
105836
- const out = { ...layer };
105837
- for (const k of fieldUnitKeys(key, boundKeys)) if (values[k] === void 0) delete out[k];
105838
- else out[k] = values[k];
105839
- return out;
105840
- }
105841
- /** `layer` without `key`'s unit — "reset to the next level". Fresh object. */
105842
- function withoutField(layer, key, boundKeys) {
105843
- const out = { ...layer };
105844
- for (const k of fieldUnitKeys(key, boundKeys)) delete out[k];
105845
- return out;
105846
- }
105847
- /** Every field unit stored in `layer`, by its field key. */
105848
- function unitsIn(layer, boundKeys) {
105849
- return [...new Set(Object.keys(layer).map((k) => unitOf(k, boundKeys)))];
105850
- }
105851
- /** True when every key of `key`'s unit has the same value in `layer` as in `below`. */
105852
- function unitEqualsBelow(layer, below, key, boundKeys) {
105853
- return fieldUnitKeys(key, boundKeys).every((k) => settingValuesEqual(layer[k], below[k]));
105854
- }
105855
- /**
105856
- * `layer` reduced to the fields that DIFFER from `below` — what an editor
105857
- * stores. A unit is kept or dropped whole. `undefined` when nothing is left.
105858
- */
105859
- function diffStepSettings(layer, below, boundKeys) {
105860
- if (layer === void 0) return void 0;
105861
- const out = {};
105862
- for (const unit of unitsIn(layer, boundKeys)) {
105863
- if (unitEqualsBelow(layer, below, unit, boundKeys)) continue;
105864
- for (const k of fieldUnitKeys(unit, boundKeys)) if (layer[k] !== void 0) out[k] = layer[k];
105865
- }
105866
- return Object.keys(out).length > 0 ? out : void 0;
105867
- }
105868
- /**
105869
- * The fields `layer` stores at exactly the value of the layers below — a
105870
- * legacy full copy's leftovers. They change nothing today and stop every
105871
- * later edit below from reaching this layer. Sorted.
105872
- */
105873
- function fieldsPinnedAtValueBelow(layer, below, boundKeys) {
105874
- if (layer === void 0) return [];
105875
- return unitsIn(layer, boundKeys).filter((unit) => unitEqualsBelow(layer, below, unit, boundKeys)).toSorted();
105876
- }
105877
- //#endregion
105878
- //#region src/pipeline/auto-tune/hardware-class.ts
105879
- var APPLE_CHIP = /^Apple M(\d+)(?:\s+(Pro|Max|Ultra))?\b/i;
105880
- var INTEL_CORE_ULTRA = /Core\(TM\)\s+Ultra|Core Ultra/i;
105881
- var INTEL_N_SERIES = /Intel\(R\)\s+N\d{2,3}\b|\bIntel\s+N\d{2,3}\b/i;
105882
- /** Phoenix / Hawk Point mobile (7040/8040) and Ryzen 8000G desktop APUs. */
105883
- var AMD_PHOENIX = /Ryzen \d+ (7[0-9]40|8[0-9]40|8[0-9]45)[HSU]|Ryzen \d+ 8[0-9]00G/i;
105884
- function parseAppleChip(cpuModel) {
105885
- const match = APPLE_CHIP.exec(cpuModel.trim());
105886
- if (match === null) return null;
105887
- const generation = Number(match[1]);
105888
- const variantRaw = (match[2] ?? "base").toLowerCase();
105889
- return {
105890
- generation,
105891
- variant: variantRaw === "pro" || variantRaw === "max" || variantRaw === "ultra" ? variantRaw : "base"
105892
- };
105893
- }
105894
- /**
105895
- * Classify a node. Apple base chips (M1–M4) map to the measured M2 column, the
105896
- * Pro/Max/Ultra chips to the measured M3 Pro column — the M2 was measured at
105897
- * 1.0–1.2× the M3 Pro on the Neural Engine, so the generation inside a variant
105898
- * matters less than the column's own error.
105899
- */
105900
- function classifyInferenceHardware(hw) {
105901
- const cpuModel = hw.cpuModel;
105902
- const apple = parseAppleChip(cpuModel);
105903
- if (apple !== null) return {
105904
- hardwareClass: apple.variant === "base" ? "apple-m-base" : "apple-m-pro",
105905
- cpuModel,
105906
- apple
105907
- };
105908
- if (INTEL_CORE_ULTRA.test(cpuModel)) return {
105909
- hardwareClass: "intel-core-ultra",
105910
- cpuModel
105911
- };
105912
- if (INTEL_N_SERIES.test(cpuModel)) return {
105913
- hardwareClass: "intel-n-series",
105914
- cpuModel
105915
- };
105916
- if (AMD_PHOENIX.test(cpuModel)) return {
105917
- hardwareClass: "amd-phoenix",
105918
- cpuModel
105919
- };
105920
- return {
105921
- hardwareClass: "unknown",
105922
- cpuModel
105923
- };
105924
- }
105925
- /**
105926
- * The accelerator kind of one of a node's device keys (`openvino:npu`,
105927
- * `openvino:gpu`, `coreml:all`, `edgetpu:usb`, `cpu`, `onnx:cpu`, …). `null`
105928
- * for a key this table has no column for.
105929
- */
105930
- function acceleratorKindOf(deviceKey, hw) {
105931
- const [backend = "", device = ""] = deviceKey.split(":");
105932
- if (backend === "edgetpu") return "coral";
105933
- if (backend === "coreml") return device === "cpu" ? "arm-cpu" : "apple-ane";
105934
- if (backend === "openvino" && device === "npu") return "intel-npu";
105935
- if (backend === "openvino" && device === "gpu") return "intel-igpu";
105936
- if (backend === "onnx" && device === "webgpu") return "amd-igpu";
105937
- if (backend === "vitisai" && device === "npu") return "amd-npu";
105938
- if (backend === "cpu" || device === "cpu") return hw.arch === "arm64" ? "arm-cpu" : "x86-cpu";
105939
- return null;
105940
- }
105941
- //#endregion
105942
- //#region src/pipeline/auto-tune/detector-performance.ts
105943
- var DETECTOR_PERF = {
105944
- "intel-core-ultra": {
105945
- "intel-npu": {
105946
- "yolov9t-320": {
105947
- p50Ms: 2.9,
105948
- fps: 383,
105949
- measured: true
105950
- },
105951
- "yolov9t-320-int8": {
105952
- p50Ms: 3.8,
105953
- fps: 333,
105954
- measured: true
105955
- },
105956
- "yolov9t-640": {
105957
- p50Ms: 7.4,
105958
- fps: 139,
105959
- measured: true
105960
- },
105961
- "yolov9t-640-int8": {
105962
- p50Ms: 7.5,
105963
- fps: 144,
105964
- measured: true
105965
- },
105966
- "yolov9s-320": {
105967
- p50Ms: 4.4,
105968
- fps: 241,
105969
- measured: true
105970
- },
105971
- "yolov9s-320-int8": {
105972
- p50Ms: 4.1,
105973
- fps: 264,
105974
- measured: true
105975
- },
105976
- "yolov9s-640": {
105977
- p50Ms: 12.7,
105978
- fps: 82,
105979
- measured: true
105980
- },
105981
- "yolov9s-640-int8": {
105982
- p50Ms: 10.7,
105983
- fps: 99,
105984
- measured: true
105985
- },
105986
- "yolov9m-320": {
105987
- p50Ms: 8.4,
105988
- fps: 123,
105989
- measured: true
105990
- },
105991
- "yolov9m-320-int8": {
105992
- p50Ms: 6.5,
105993
- fps: 165,
105994
- measured: true
105995
- },
105996
- "yolov9m-640": {
105997
- p50Ms: 29.5,
105998
- fps: 32,
105999
- measured: true
106000
- },
106001
- "yolov9m-640-int8": {
106002
- p50Ms: 19.5,
106003
- fps: 52,
106004
- measured: true
106005
- }
106006
- },
106007
- "intel-igpu": {
106008
- "yolov9t-320": {
106009
- p50Ms: 2.6,
106010
- fps: 405,
106011
- measured: true
106012
- },
106013
- "yolov9t-320-int8": {
106014
- p50Ms: 2.8,
106015
- fps: 324,
106016
- measured: true
106017
- },
106018
- "yolov9t-640": {
106019
- p50Ms: 6.5,
106020
- fps: 157,
106021
- measured: true
106022
- },
106023
- "yolov9t-640-int8": {
106024
- p50Ms: 5.6,
106025
- fps: 185,
106026
- measured: true
106027
- },
106028
- "yolov9s-320": {
106029
- p50Ms: 4.6,
106030
- fps: 200,
106031
- measured: true
106032
- },
106033
- "yolov9s-320-int8": {
106034
- p50Ms: 4.1,
106035
- fps: 239,
106036
- measured: true
106037
- },
106038
- "yolov9s-640": {
106039
- p50Ms: 15.2,
106040
- fps: 69,
106041
- measured: true
106042
- },
106043
- "yolov9s-640-int8": {
106044
- p50Ms: 10.5,
106045
- fps: 99,
106046
- measured: true
106047
- },
106048
- "yolov9m-320": {
106049
- p50Ms: 11.4,
106050
- fps: 88,
106051
- measured: true
106052
- },
106053
- "yolov9m-320-int8": {
106054
- p50Ms: 7,
106055
- fps: 127,
106056
- measured: true
106057
- },
106058
- "yolov9m-640": {
106059
- p50Ms: 40,
106060
- fps: 25,
106061
- measured: true
106062
- },
106063
- "yolov9m-640-int8": {
106064
- p50Ms: 23.5,
106065
- fps: 44,
106066
- measured: true
106067
- }
106068
- },
106069
- "x86-cpu": {
106070
- "yolov9t-320": {
106071
- p50Ms: 6.9,
106072
- fps: 145,
106073
- measured: true
106074
- },
106075
- "yolov9t-320-int8": {
106076
- p50Ms: 2.7,
106077
- fps: 405,
106078
- measured: true
106079
- },
106080
- "yolov9t-640": {
106081
- p50Ms: 23,
106082
- fps: 44,
106083
- measured: true
106084
- },
106085
- "yolov9t-640-int8": {
106086
- p50Ms: 8.8,
106087
- fps: 124,
106088
- measured: true
106089
- },
106090
- "yolov9s-320": {
106091
- p50Ms: 15.8,
106092
- fps: 63,
106093
- measured: true
106094
- },
106095
- "yolov9s-320-int8": {
106096
- p50Ms: 6.5,
106097
- fps: 180,
106098
- measured: true
106099
- },
106100
- "yolov9s-640": {
106101
- p50Ms: 58.2,
106102
- fps: 17,
106103
- measured: true
106104
- },
106105
- "yolov9s-640-int8": {
106106
- p50Ms: 20.1,
106107
- fps: 52,
106108
- measured: true
106109
- },
106110
- "yolov9m-320": {
106111
- p50Ms: 40,
106112
- fps: 25,
106113
- measured: true
106114
- },
106115
- "yolov9m-320-int8": {
106116
- p50Ms: 14.2,
106117
- fps: 74,
106118
- measured: true
106119
- },
106120
- "yolov9m-640": {
106121
- p50Ms: 155.8,
106122
- fps: 6,
106123
- measured: true
106124
- },
106125
- "yolov9m-640-int8": {
106126
- p50Ms: 52.4,
106127
- fps: 19,
106128
- measured: true
106129
- }
106130
- },
106131
- coral: { "yolov9s-320-edgetpu": {
106132
- p50Ms: 32,
106133
- fps: 31,
106134
- measured: false
106135
- } }
106136
- },
106137
- "intel-n-series": {
106138
- "intel-igpu": {
106139
- "yolov9t-320": {
106140
- p50Ms: 15,
106141
- fps: 48,
106142
- measured: true
106143
- },
106144
- "yolov9t-320-int8": {
106145
- p50Ms: 14.1,
106146
- fps: 67,
106147
- measured: true
106148
- },
106149
- "yolov9t-640": {
106150
- p50Ms: 38.5,
106151
- fps: 26,
106152
- measured: true
106153
- },
106154
- "yolov9t-640-int8": {
106155
- p50Ms: 33.1,
106156
- fps: 33,
106157
- measured: true
106158
- },
106159
- "yolov9s-320": {
106160
- p50Ms: 41.4,
106161
- fps: 24,
106162
- measured: true
106163
- },
106164
- "yolov9s-320-int8": {
106165
- p50Ms: 23.3,
106166
- fps: 34,
106167
- measured: true
106168
- },
106169
- "yolov9s-640": {
106170
- p50Ms: 127.4,
106171
- fps: 8,
106172
- measured: true
106173
- },
106174
- "yolov9s-640-int8": {
106175
- p50Ms: 92.9,
106176
- fps: 11,
106177
- measured: true
106178
- },
106179
- "yolov9m-320": {
106180
- p50Ms: 63.5,
106181
- fps: 16,
106182
- measured: true
106183
- },
106184
- "yolov9m-320-int8": {
106185
- p50Ms: 39.4,
106186
- fps: 26,
106187
- measured: true
106188
- },
106189
- "yolov9m-640": {
106190
- p50Ms: 215.5,
106191
- fps: 5,
106192
- measured: true
106193
- },
106194
- "yolov9m-640-int8": {
106195
- p50Ms: 139.1,
106196
- fps: 7,
106197
- measured: true
106198
- }
106199
- },
106200
- "x86-cpu": {
106201
- "yolov9t-320": {
106202
- p50Ms: 86.2,
106203
- fps: 12,
106204
- measured: true
106205
- },
106206
- "yolov9t-320-int8": {
106207
- p50Ms: 52.7,
106208
- fps: 20,
106209
- measured: true
106210
- },
106211
- "yolov9t-640": {
106212
- p50Ms: 322.1,
106213
- fps: 3,
106214
- measured: true
106215
- },
106216
- "yolov9t-640-int8": {
106217
- p50Ms: 205.6,
106218
- fps: 5,
106219
- measured: true
106220
- },
106221
- "yolov9s-320": {
106222
- p50Ms: 164.4,
106223
- fps: 6,
106224
- measured: true
106225
- },
106226
- "yolov9s-320-int8": {
106227
- p50Ms: 86.9,
106228
- fps: 12,
106229
- measured: true
106230
- },
106231
- "yolov9s-640": {
106232
- p50Ms: 341.3,
106233
- fps: 3,
106234
- measured: true
106235
- },
106236
- "yolov9s-640-int8": {
106237
- p50Ms: 185.3,
106238
- fps: 5,
106239
- measured: true
106240
- },
106241
- "yolov9m-320": {
106242
- p50Ms: 166.8,
106243
- fps: 6,
106244
- measured: true
106245
- },
106246
- "yolov9m-320-int8": {
106247
- p50Ms: 226.3,
106248
- fps: 4,
106249
- measured: true
106250
- },
106251
- "yolov9m-640": {
106252
- p50Ms: 663.5,
106253
- fps: 2,
106254
- measured: true
106255
- },
106256
- "yolov9m-640-int8": {
106257
- p50Ms: 324.4,
106258
- fps: 3,
106259
- measured: true
106260
- }
106261
- },
106262
- coral: { "yolov9s-320-edgetpu": {
106263
- p50Ms: 32,
106264
- fps: 31,
106265
- measured: false
106266
- } }
106267
- },
106268
- "amd-phoenix": {
106269
- "x86-cpu": {
106270
- "yolov9t-320": {
106271
- p50Ms: 24,
106272
- fps: 42,
106273
- measured: true
106274
- },
106275
- "yolov9t-320-int8": {
106276
- p50Ms: 12,
106277
- fps: 83,
106278
- measured: true
106279
- },
106280
- "yolov9t-640": {
106281
- p50Ms: 50,
106282
- fps: 20,
106283
- measured: true
106284
- },
106285
- "yolov9t-640-int8": {
106286
- p50Ms: 20,
106287
- fps: 50,
106288
- measured: true
106289
- },
106290
- "yolov9s-320": {
106291
- p50Ms: 30,
106292
- fps: 33,
106293
- measured: true
106294
- },
106295
- "yolov9s-320-int8": {
106296
- p50Ms: 15,
106297
- fps: 67,
106298
- measured: true
106299
- },
106300
- "yolov9s-640": {
106301
- p50Ms: 92,
106302
- fps: 11,
106303
- measured: true
106304
- },
106305
- "yolov9s-640-int8": {
106306
- p50Ms: 27,
106307
- fps: 37,
106308
- measured: true
106309
- },
106310
- "yolov9m-320": {
106311
- p50Ms: 67,
106312
- fps: 15,
106313
- measured: true
106314
- },
106315
- "yolov9m-320-int8": {
106316
- p50Ms: 21,
106317
- fps: 48,
106318
- measured: true
106319
- },
106320
- "yolov9m-640": {
106321
- p50Ms: 229,
106322
- fps: 4,
106323
- measured: true
106324
- },
106325
- "yolov9m-640-int8": {
106326
- p50Ms: 48,
106327
- fps: 21,
106328
- measured: true
106329
- }
106330
- },
106331
- "amd-igpu": {
106332
- "yolov9t-320": {
106333
- p50Ms: 40,
106334
- fps: 25,
106335
- measured: true
106336
- },
106337
- "yolov9t-640": {
106338
- p50Ms: 89,
106339
- fps: 11,
106340
- measured: true
106341
- },
106342
- "yolov9s-320": {
106343
- p50Ms: 65,
106344
- fps: 15,
106345
- measured: true
106346
- },
106347
- "yolov9s-640": {
106348
- p50Ms: 125,
106349
- fps: 8,
106350
- measured: true
106351
- },
106352
- "yolov9m-320": {
106353
- p50Ms: 85,
106354
- fps: 12,
106355
- measured: true
106356
- },
106357
- "yolov9m-640": {
106358
- p50Ms: 228,
106359
- fps: 4,
106360
- measured: true
106361
- }
106362
- },
106363
- coral: { "yolov9s-320-edgetpu": {
106364
- p50Ms: 32,
106365
- fps: 31,
106366
- measured: true
106367
- } }
106368
- },
106369
- "apple-m-pro": {
106370
- "apple-ane": {
106371
- "yolov9t-320": {
106372
- p50Ms: 1.1,
106373
- fps: 952,
106374
- measured: true
106375
- },
106376
- "yolov9t-640": {
106377
- p50Ms: 3,
106378
- fps: 333,
106379
- measured: true
106380
- },
106381
- "yolov9s-320": {
106382
- p50Ms: 1.5,
106383
- fps: 667,
106384
- measured: true
106385
- },
106386
- "yolov9s-640": {
106387
- p50Ms: 5.1,
106388
- fps: 196,
106389
- measured: true
106390
- },
106391
- "yolov9m-320": {
106392
- p50Ms: 3.2,
106393
- fps: 312,
106394
- measured: true
106395
- },
106396
- "yolov9m-640": {
106397
- p50Ms: 10.2,
106398
- fps: 98,
106399
- measured: true
106400
- }
106401
- },
106402
- coral: { "yolov9s-320-edgetpu": {
106403
- p50Ms: 32,
106404
- fps: 31,
106405
- measured: false
106406
- } }
106407
- },
106408
- "apple-m-base": {
106409
- "apple-ane": {
106410
- "yolov9t-320": {
106411
- p50Ms: 1.32,
106412
- fps: 758,
106413
- measured: false
106414
- },
106415
- "yolov9t-640": {
106416
- p50Ms: 3.45,
106417
- fps: 290,
106418
- measured: true
106419
- },
106420
- "yolov9s-320": {
106421
- p50Ms: 1.8,
106422
- fps: 556,
106423
- measured: false
106424
- },
106425
- "yolov9s-640": {
106426
- p50Ms: 5.46,
106427
- fps: 183,
106428
- measured: true
106429
- },
106430
- "yolov9m-320": {
106431
- p50Ms: 3.79,
106432
- fps: 264,
106433
- measured: true
106434
- },
106435
- "yolov9m-640": {
106436
- p50Ms: 10.34,
106437
- fps: 97,
106438
- measured: true
106439
- }
106440
- },
106441
- coral: { "yolov9s-320-edgetpu": {
106442
- p50Ms: 32,
106443
- fps: 31,
106444
- measured: false
106445
- } }
106446
- }
106434
+ return models.some((m) => m.legacy !== true && hasBuild(m, format));
106435
+ }
106436
+ //#endregion
106437
+ //#region src/pipeline/removed-model-ids.ts
106438
+ /**
106439
+ * Model ids DELETED from a step's catalog (D703, D710, D719), per step — and
106440
+ * the one question asked of them: is a stored pin to this id a pin to a model
106441
+ * that no longer exists? (D725)
106442
+ *
106443
+ * This is NOT a resolver input. `resolveModelForFormat` still returns an
106444
+ * unknown id unchanged (a custom model), and a bulk pass pinned to one still
106445
+ * refuses rather than substitutes (D54). The ONE reader is the orchestrator's
106446
+ * stored-pin reset (`removed-model-pin-reset.ts`), which clears such a pin from
106447
+ * the store so the step follows its default again — unless a registered custom
106448
+ * model carries the same id.
106449
+ *
106450
+ * `removed-model-ids.spec.ts` (addon-pipeline) pins that every id here is
106451
+ * really absent from its step's catalog. A model deleted from a catalog is
106452
+ * added here in the same change, or its stored pins stay behind as custom ids.
106453
+ */
106454
+ /** `stepId → ids deleted from that step's catalog`. */
106455
+ var REMOVED_MODEL_IDS = {
106456
+ "object-detection": ["efficientdet-lite0-edgetpu", "ssdlite-mobiledet-coco-edgetpu"],
106457
+ "face-detection": [
106458
+ "scrypted-yolov9t-face",
106459
+ "scrfd-2.5g",
106460
+ "ssd-mobilenet-v2-face-edgetpu"
106461
+ ],
106462
+ "face-embedding": ["arcface-r100", "inception-resnet-v1"],
106463
+ "plate-detection": ["yolov8n-plate"],
106464
+ "plate-ocr": ["vgg-english-g2"],
106465
+ "clip-embedding": [
106466
+ "mobileclip-s0",
106467
+ "mobileclip-s1",
106468
+ "mobileclip-s2"
106469
+ ],
106470
+ "package-detection": ["yolov8n-package"],
106471
+ "bird-classifier": ["bird-classifier-aiy-edgetpu", "bird-nabirds-404"],
106472
+ "animal-classifier": ["animals-10"],
106473
+ "vehicle-classifier": ["vehicle-type-efficientnet"]
106447
106474
  };
106448
- var DETECTOR_ACCURACY = {
106449
- "yolov9m-320": {
106450
- personPrecision: .979,
106451
- personRecall: .649
106452
- },
106453
- "yolov9m-320-int8": {
106454
- personPrecision: .974,
106455
- personRecall: .684
106456
- },
106457
- "yolov9m-640": {
106458
- personPrecision: .976,
106459
- personRecall: .854
106460
- },
106461
- "yolov9m-640-int8": {
106462
- personPrecision: .976,
106463
- personRecall: .847
106464
- },
106465
- "yolov9s-320": {
106466
- personPrecision: .958,
106467
- personRecall: .479
106468
- },
106469
- "yolov9s-320-int8": {
106470
- personPrecision: .969,
106471
- personRecall: .438
106472
- },
106473
- "yolov9s-640": {
106474
- personPrecision: .986,
106475
- personRecall: .753
106476
- },
106477
- "yolov9s-640-int8": {
106478
- personPrecision: .963,
106479
- personRecall: .823
106480
- },
106481
- "yolov9t-320": {
106482
- personPrecision: .946,
106483
- personRecall: .368
106484
- },
106485
- "yolov9t-320-int8": {
106486
- personPrecision: .943,
106487
- personRecall: .347
106488
- },
106489
- "yolov9t-640": {
106490
- personPrecision: .979,
106491
- personRecall: .646
106492
- },
106493
- "yolov9t-640-int8": {
106494
- personPrecision: .979,
106495
- personRecall: .646
106475
+ /** True when `modelId` was deleted from `stepId`'s catalog. */
106476
+ function isRemovedModelId(stepId, modelId) {
106477
+ return REMOVED_MODEL_IDS[stepId]?.includes(modelId) ?? false;
106478
+ }
106479
+ //#endregion
106480
+ //#region src/pipeline/model-bound-settings.ts
106481
+ /** Suffix of the sibling key that names the model a stored value was made for. */
106482
+ var MODEL_BINDING_SUFFIX = "ModelId";
106483
+ /** `minConfidencePerson` → `minConfidencePersonModelId`. */
106484
+ function modelBindingKey(key) {
106485
+ return `${key}${MODEL_BINDING_SUFFIX}`;
106486
+ }
106487
+ /** The model a stored value was made for, or `undefined` when unbound. */
106488
+ function storedBinding(settings, key) {
106489
+ const binding = settings?.[modelBindingKey(key)];
106490
+ return typeof binding === "string" && binding.length > 0 ? binding : void 0;
106491
+ }
106492
+ /**
106493
+ * The effective value of `key` for the model that runs (`runningModelId`,
106494
+ * `undefined` when unknown — it then matches no binding).
106495
+ */
106496
+ function resolveModelBoundSetting(settings, key, runningModelId, modelDefault, globalDefault) {
106497
+ const stored = settings?.[key];
106498
+ const binding = storedBinding(settings, key);
106499
+ if (stored !== void 0 && binding === void 0) return {
106500
+ value: stored,
106501
+ source: "override"
106502
+ };
106503
+ if (stored !== void 0 && binding !== void 0 && binding === runningModelId) return {
106504
+ value: stored,
106505
+ source: "override",
106506
+ boundModelId: binding
106507
+ };
106508
+ const inapplicable = stored !== void 0 && binding !== void 0 ? { inapplicableOverride: {
106509
+ value: stored,
106510
+ modelId: binding
106511
+ } } : {};
106512
+ if (modelDefault !== void 0) return {
106513
+ value: modelDefault,
106514
+ source: "model-default",
106515
+ ...inapplicable
106516
+ };
106517
+ return {
106518
+ value: globalDefault,
106519
+ source: "global-default",
106520
+ ...inapplicable
106521
+ };
106522
+ }
106523
+ /**
106524
+ * The OPERATOR layer of a step's settings as the running model sees it: every
106525
+ * model-bound value made for another model is removed (so the default
106526
+ * underneath — the running model's — wins the merge), and every binding key is
106527
+ * stripped. Unbound values are kept. Returns a fresh object; `undefined` stays
106528
+ * `undefined`.
106529
+ */
106530
+ function applyModelBoundSettings(settings, boundKeys, runningModelId) {
106531
+ if (settings === void 0) return void 0;
106532
+ const out = { ...settings };
106533
+ for (const key of boundKeys) {
106534
+ const binding = storedBinding(settings, key);
106535
+ if (binding !== void 0 && binding !== runningModelId) delete out[key];
106536
+ delete out[modelBindingKey(key)];
106496
106537
  }
106497
- };
106538
+ return out;
106539
+ }
106540
+ /** Store `value` for `key`, made for `modelId`. Returns a fresh object. */
106541
+ function writeModelBoundSetting(settings, key, value, modelId) {
106542
+ return {
106543
+ ...settings,
106544
+ [key]: value,
106545
+ [modelBindingKey(key)]: modelId
106546
+ };
106547
+ }
106548
+ /** Remove an override and its binding — "reset to the model default". */
106549
+ function clearModelBoundSetting(settings, key) {
106550
+ const out = { ...settings };
106551
+ delete out[key];
106552
+ delete out[modelBindingKey(key)];
106553
+ return out;
106554
+ }
106555
+ /** True when any of `boundKeys` has an override IN FORCE for `modelId`. */
106556
+ function hasModelBoundOverrides(settings, boundKeys, modelId) {
106557
+ return boundKeys.some((key) => resolveModelBoundSetting(settings, key, modelId, void 0, void 0).source === "override");
106558
+ }
106559
+ /**
106560
+ * "Reset thresholds to model defaults": remove every override of `boundKeys`
106561
+ * that is in force for `modelId` (bound to it, or unbound), with its binding —
106562
+ * one result, so the editor writes it in one save. An override stored for
106563
+ * ANOTHER model is kept: it is not in force here, and it applies again when
106564
+ * that model runs.
106565
+ */
106566
+ function clearModelBoundOverrides(settings, boundKeys, modelId) {
106567
+ return boundKeys.reduce((acc, key) => resolveModelBoundSetting(acc, key, modelId, void 0, void 0).source === "override" ? clearModelBoundSetting(acc, key) : acc, { ...settings });
106568
+ }
106569
+ /**
106570
+ * The keys of every `bindsToModel` field of a step's config schema, groups
106571
+ * flattened (D738). One definition for the runner (which applies the binding)
106572
+ * and the orchestrator's Auto tune (which clears those overrides, D745).
106573
+ */
106574
+ function collectModelBoundKeys(fields) {
106575
+ return fields.flatMap((field) => {
106576
+ if (field.type === "group") return collectModelBoundKeys(field.fields);
106577
+ return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
106578
+ });
106579
+ }
106580
+ //#endregion
106581
+ //#region src/pipeline/step-settings-layers.ts
106582
+ /**
106583
+ * The layers of a detection step's settings, and the field-level edits on them
106584
+ * (D754, D927).
106585
+ *
106586
+ * A step's settings reach a session through two stored layers, merged key by
106587
+ * key — a key in the higher layer wins:
106588
+ *
106589
+ * node the node's base for the accelerator that runs the step
106590
+ * camera `cameraSettings[cam].stepSettings[step]` — camera-semantic
106591
+ * keys only, on every accelerator (never a model-bound field)
106592
+ *
106593
+ * Below them sits the default: the running model's (D738) or the field's own.
106594
+ *
106595
+ * Every layer is SPARSE and FIELD-LEVEL. A key present in a layer is an
106596
+ * override; an absent key inherits. Two consequences that every editor must
106597
+ * respect, and why this module exists:
106598
+ *
106599
+ * - A model-bound field (D738) occupies TWO keys, its value and
106600
+ * `<key>ModelId`. They are written, compared and removed as one unit — a
106601
+ * value without its binding over a lower layer's binding would be judged
106602
+ * against the wrong model.
106603
+ * - An editor writes a DIFF against the merged layers below, never a copy of
106604
+ * them. A copy pins every field at the value it had that day, so later
106605
+ * edits below never reach it (the full-copy "Override" this replaced).
106606
+ */
106607
+ /** Top-down order of the stored layers. */
106608
+ var LAYER_ORDER = ["camera", "node"];
106609
+ /**
106610
+ * The keys one field occupies in a bag: its own, plus its binding key when it
106611
+ * binds to a model. `boundKeys` = `collectModelBoundKeys(schema)`.
106612
+ */
106613
+ function fieldUnitKeys(key, boundKeys) {
106614
+ return boundKeys.includes(key) ? [key, modelBindingKey(key)] : [key];
106615
+ }
106616
+ /** The field a stored key belongs to (`minConfidencePersonModelId` → `minConfidencePerson`). */
106617
+ function unitOf(storedKey, boundKeys) {
106618
+ return boundKeys.find((k) => modelBindingKey(k) === storedKey) ?? storedKey;
106619
+ }
106620
+ /** Structural equality for stored values (numbers, strings, booleans, arrays, plain objects). */
106621
+ function settingValuesEqual(a, b) {
106622
+ if (Object.is(a, b)) return true;
106623
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => settingValuesEqual(v, b[i]));
106624
+ if (isPlainObject(a) && isPlainObject(b)) {
106625
+ const ka = Object.keys(a);
106626
+ const kb = Object.keys(b);
106627
+ return ka.length === kb.length && ka.every((k) => settingValuesEqual(a[k], b[k]));
106628
+ }
106629
+ return false;
106630
+ }
106631
+ function isPlainObject(v) {
106632
+ return typeof v === "object" && v !== null && !Array.isArray(v);
106633
+ }
106634
+ /** Merge bags bottom-up (later wins, per key). Fresh object. */
106635
+ function mergeSettingsBags(...bags) {
106636
+ return bags.reduce((acc, bag) => ({
106637
+ ...acc,
106638
+ ...bag
106639
+ }), {});
106640
+ }
106641
+ /** The merged value of the layers BELOW `layer` (what removing a key from `layer` falls back to). */
106642
+ function settingsBelow(layers, layer) {
106643
+ return mergeSettingsBags(...LAYER_ORDER.slice(LAYER_ORDER.indexOf(layer) + 1).toReversed().map((id) => layers[id]));
106644
+ }
106645
+ /** The effective bag: every stored layer merged. */
106646
+ function effectiveStepSettings(layers) {
106647
+ return mergeSettingsBags(layers.node, layers.camera);
106648
+ }
106649
+ /** The highest stored layer that holds any key of `key`'s unit, or `default`. */
106650
+ function stepSettingSource(layers, key, boundKeys) {
106651
+ const unit = fieldUnitKeys(key, boundKeys);
106652
+ for (const id of LAYER_ORDER) {
106653
+ const bag = layers[id];
106654
+ if (bag !== void 0 && unit.some((k) => bag[k] !== void 0)) return id;
106655
+ }
106656
+ return "default";
106657
+ }
106658
+ /**
106659
+ * `layer` with `key`'s unit set from `values` (a bag carrying the unit's new
106660
+ * value — e.g. the editor's whole next settings bag). A unit key that `values`
106661
+ * does not carry is removed from the layer. Fresh object.
106662
+ */
106663
+ function withFieldFrom(layer, key, values, boundKeys) {
106664
+ const out = { ...layer };
106665
+ for (const k of fieldUnitKeys(key, boundKeys)) if (values[k] === void 0) delete out[k];
106666
+ else out[k] = values[k];
106667
+ return out;
106668
+ }
106669
+ /** `layer` without `key`'s unit — "reset to the next level". Fresh object. */
106670
+ function withoutField(layer, key, boundKeys) {
106671
+ const out = { ...layer };
106672
+ for (const k of fieldUnitKeys(key, boundKeys)) delete out[k];
106673
+ return out;
106674
+ }
106675
+ /** Every field unit stored in `layer`, by its field key. */
106676
+ function unitsIn(layer, boundKeys) {
106677
+ return [...new Set(Object.keys(layer).map((k) => unitOf(k, boundKeys)))];
106678
+ }
106679
+ /** True when every key of `key`'s unit has the same value in `layer` as in `below`. */
106680
+ function unitEqualsBelow(layer, below, key, boundKeys) {
106681
+ return fieldUnitKeys(key, boundKeys).every((k) => settingValuesEqual(layer[k], below[k]));
106682
+ }
106683
+ /**
106684
+ * `layer` reduced to the fields that DIFFER from `below` — what an editor
106685
+ * stores. A unit is kept or dropped whole. `undefined` when nothing is left.
106686
+ */
106687
+ function diffStepSettings(layer, below, boundKeys) {
106688
+ if (layer === void 0) return void 0;
106689
+ const out = {};
106690
+ for (const unit of unitsIn(layer, boundKeys)) {
106691
+ if (unitEqualsBelow(layer, below, unit, boundKeys)) continue;
106692
+ for (const k of fieldUnitKeys(unit, boundKeys)) if (layer[k] !== void 0) out[k] = layer[k];
106693
+ }
106694
+ return Object.keys(out).length > 0 ? out : void 0;
106695
+ }
106696
+ /**
106697
+ * The fields `layer` stores at exactly the value of the layers below — a
106698
+ * legacy full copy's leftovers. They change nothing today and stop every
106699
+ * later edit below from reaching this layer. Sorted.
106700
+ */
106701
+ function fieldsPinnedAtValueBelow(layer, below, boundKeys) {
106702
+ if (layer === void 0) return [];
106703
+ return unitsIn(layer, boundKeys).filter((unit) => unitEqualsBelow(layer, below, unit, boundKeys)).toSorted();
106704
+ }
106705
+ //#endregion
106706
+ //#region src/pipeline/auto-tune/hardware-class.ts
106707
+ var APPLE_CHIP = /^Apple M(\d+)(?:\s+(Pro|Max|Ultra))?\b/i;
106708
+ var INTEL_CORE_ULTRA = /Core\(TM\)\s+Ultra|Core Ultra/i;
106709
+ var INTEL_N_SERIES = /Intel\(R\)\s+N\d{2,3}\b|\bIntel\s+N\d{2,3}\b/i;
106710
+ /** Phoenix / Hawk Point mobile (7040/8040) and Ryzen 8000G desktop APUs. */
106711
+ var AMD_PHOENIX = /Ryzen \d+ (7[0-9]40|8[0-9]40|8[0-9]45)[HSU]|Ryzen \d+ 8[0-9]00G/i;
106712
+ function parseAppleChip(cpuModel) {
106713
+ const match = APPLE_CHIP.exec(cpuModel.trim());
106714
+ if (match === null) return null;
106715
+ const generation = Number(match[1]);
106716
+ const variantRaw = (match[2] ?? "base").toLowerCase();
106717
+ return {
106718
+ generation,
106719
+ variant: variantRaw === "pro" || variantRaw === "max" || variantRaw === "ultra" ? variantRaw : "base"
106720
+ };
106721
+ }
106722
+ /**
106723
+ * Classify a node. Apple base chips (M1–M4) map to the measured M2 column, the
106724
+ * Pro/Max/Ultra chips to the measured M3 Pro column — the M2 was measured at
106725
+ * 1.0–1.2× the M3 Pro on the Neural Engine, so the generation inside a variant
106726
+ * matters less than the column's own error.
106727
+ */
106728
+ function classifyInferenceHardware(hw) {
106729
+ const cpuModel = hw.cpuModel;
106730
+ const apple = parseAppleChip(cpuModel);
106731
+ if (apple !== null) return {
106732
+ hardwareClass: apple.variant === "base" ? "apple-m-base" : "apple-m-pro",
106733
+ cpuModel,
106734
+ apple
106735
+ };
106736
+ if (INTEL_CORE_ULTRA.test(cpuModel)) return {
106737
+ hardwareClass: "intel-core-ultra",
106738
+ cpuModel
106739
+ };
106740
+ if (INTEL_N_SERIES.test(cpuModel)) return {
106741
+ hardwareClass: "intel-n-series",
106742
+ cpuModel
106743
+ };
106744
+ if (AMD_PHOENIX.test(cpuModel)) return {
106745
+ hardwareClass: "amd-phoenix",
106746
+ cpuModel
106747
+ };
106748
+ return {
106749
+ hardwareClass: "unknown",
106750
+ cpuModel
106751
+ };
106752
+ }
106753
+ /**
106754
+ * The accelerator kind of one of a node's device keys (`openvino:npu`,
106755
+ * `openvino:gpu`, `coreml:all`, `edgetpu:usb`, `cpu`, `onnx:cpu`, …). `null`
106756
+ * for a key this table has no column for.
106757
+ */
106758
+ function acceleratorKindOf(deviceKey, hw) {
106759
+ const [backend = "", device = ""] = deviceKey.split(":");
106760
+ if (backend === "edgetpu") return "coral";
106761
+ if (backend === "coreml") return device === "cpu" ? "arm-cpu" : "apple-ane";
106762
+ if (backend === "openvino" && device === "npu") return "intel-npu";
106763
+ if (backend === "openvino" && device === "gpu") return "intel-igpu";
106764
+ if (backend === "onnx" && device === "webgpu") return "amd-igpu";
106765
+ if (backend === "vitisai" && device === "npu") return "amd-npu";
106766
+ if (backend === "cpu" || device === "cpu") return hw.arch === "arm64" ? "arm-cpu" : "x86-cpu";
106767
+ return null;
106768
+ }
106498
106769
  //#endregion
106499
106770
  //#region src/pipeline/auto-tune/recommend.ts
106500
106771
  /** The family the cluster runs. */
@@ -106510,10 +106781,35 @@ var RECALL_TIE = .02;
106510
106781
  var DEFAULT_HEADROOM = .4;
106511
106782
  /** Detection rate a session is counted at when no camera says otherwise. */
106512
106783
  var DEFAULT_CAMERA_FPS = 10;
106513
- /** Parallel sessions one device sustains at `fps` with `headroom` kept free. */
106514
- function capacitySessionsOf(fps, headroom, cameraFps) {
106515
- if (!(fps > 0) || !(cameraFps > 0)) return 0;
106516
- return Math.max(0, Math.floor(fps * (1 - headroom) / cameraFps + 1e-9));
106784
+ /**
106785
+ * Camera rate at which serial `1000/p50` matched the cameras that held
106786
+ * (N100 iGPU, 2026-10-24). Below this the second InferRequest's overlap
106787
+ * becomes extra sessions; at and above it the serial ceiling is the count.
106788
+ */
106789
+ var SERIAL_CAPACITY_CAMERA_FPS = 10;
106790
+ /**
106791
+ * Aggregate throughput ÷ serial throughput when the frame interval is wide
106792
+ * enough to hide the second request. intel-igpu is the N100 measurement
106793
+ * (~1.4× under LATENCY with 2 InferRequests). Every other kind stays 1
106794
+ * until it is measured the same way.
106795
+ */
106796
+ var AGGREGATE_THROUGHPUT_BY_KIND = { "intel-igpu": 1.4 };
106797
+ /** The overlap factor for this kind at this camera rate. 1 at {@link SERIAL_CAPACITY_CAMERA_FPS} and above. */
106798
+ function aggregateThroughputFactor(kind, cameraFps) {
106799
+ if (!(cameraFps > 0) || cameraFps >= 10) return 1;
106800
+ return AGGREGATE_THROUGHPUT_BY_KIND[kind] ?? 1;
106801
+ }
106802
+ /**
106803
+ * Parallel sessions one device sustains at `fps` with `headroom` kept free.
106804
+ * `aggregateFactor` is the kind's overlap (1 unless the caller measured
106805
+ * extra throughput at this camera rate).
106806
+ */
106807
+ function capacitySessionsOf(fps, headroom, cameraFps, aggregateFactor = 1) {
106808
+ if (!(fps > 0) || !(cameraFps > 0) || !(aggregateFactor > 0)) return 0;
106809
+ return Math.max(0, Math.floor(fps * aggregateFactor * (1 - headroom) / cameraFps + 1e-9));
106810
+ }
106811
+ function reportFor(hardware, deviceKey) {
106812
+ return hardware.accelerators?.find((report) => report.deviceKey === deviceKey) ?? null;
106517
106813
  }
106518
106814
  /** The table column with the node's own measurements laid over it. */
106519
106815
  function overlayMeasured(table, measured) {
@@ -106535,11 +106831,12 @@ function round(value, digits = 3) {
106535
106831
  const f = 10 ** digits;
106536
106832
  return Math.round(value * f) / f;
106537
106833
  }
106538
- function sessionsOn(cell, sizing) {
106539
- return cell === void 0 ? 0 : capacitySessionsOf(cell.fps, sizing.headroom, sizing.cameraFps);
106834
+ function sessionsOn(cell, sizing, kind) {
106835
+ if (cell === void 0) return 0;
106836
+ return capacitySessionsOf(cell.fps, sizing.headroom, sizing.cameraFps, aggregateThroughputFactor(kind, sizing.cameraFps));
106540
106837
  }
106541
- function fitsOn(cell, sizing) {
106542
- return sessionsOn(cell, sizing) >= sizing.minSessions;
106838
+ function fitsOn(ctx, modelId, sizing) {
106839
+ return sessionsOn(ctx.column[modelId], sizing, ctx.accelerator) >= sizing.minSessions;
106543
106840
  }
106544
106841
  function candidatesFor(contexts, formatsByModel) {
106545
106842
  return Object.keys(DETECTOR_ACCURACY).filter((id) => id.startsWith(AUTO_TUNE_FAMILY)).filter((id) => contexts.every((c) => c.column[id] !== void 0 && (formatsByModel[id] ?? []).includes(c.device.format))).map((id) => ({
@@ -106552,13 +106849,13 @@ function totalLatency(contexts, modelId) {
106552
106849
  }
106553
106850
  /** The fewest sessions any device of the group sustains with this model. */
106554
106851
  function worstSessions(contexts, modelId, sizing) {
106555
- return contexts.reduce((worst, c) => Math.min(worst, sessionsOn(c.column[modelId], sizing)), Number.POSITIVE_INFINITY);
106852
+ return contexts.reduce((worst, c) => Math.min(worst, sessionsOn(c.column[modelId], sizing, c.accelerator)), Number.POSITIVE_INFINITY);
106556
106853
  }
106557
106854
  /** One model for a group of devices that must agree (step 2–4 of the header). */
106558
106855
  function pickForGroup(contexts, formatsByModel, sizing) {
106559
106856
  const candidates = candidatesFor(contexts, formatsByModel);
106560
106857
  if (candidates.length === 0) return null;
106561
- const fitting = candidates.filter((c) => contexts.every((ctx) => fitsOn(ctx.column[c.modelId], sizing)));
106858
+ const fitting = candidates.filter((c) => contexts.every((ctx) => fitsOn(ctx, c.modelId, sizing)));
106562
106859
  if (fitting.length === 0) {
106563
106860
  const lightest = candidates.toSorted((a, b) => worstSessions(contexts, b.modelId, sizing) - worstSessions(contexts, a.modelId, sizing) || totalLatency(contexts, a.modelId) - totalLatency(contexts, b.modelId))[0];
106564
106861
  if (lightest === void 0) return null;
@@ -106577,7 +106874,7 @@ function pickForGroup(contexts, formatsByModel, sizing) {
106577
106874
  const reasons = [];
106578
106875
  const mostAccurate = candidates[0];
106579
106876
  if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
106580
- const limitedBy = contexts.find((ctx) => !fitsOn(ctx.column[mostAccurate.modelId], sizing))?.device.key ?? "";
106877
+ const limitedBy = contexts.find((ctx) => !fitsOn(ctx, mostAccurate.modelId, sizing))?.device.key ?? "";
106581
106878
  reasons.push({
106582
106879
  kind: "stepped-down",
106583
106880
  from: mostAccurate.modelId,
@@ -106598,13 +106895,19 @@ function pickForGroup(contexts, formatsByModel, sizing) {
106598
106895
  function choiceOn(ctx, modelId, sizing) {
106599
106896
  const cell = ctx.column[modelId];
106600
106897
  if (cell === void 0) return null;
106898
+ const estimatedFrom = cell.source?.kind === "node-benchmark" ? void 0 : cell.estimatedFrom;
106899
+ const source = cell.source ?? (estimatedFrom !== void 0 ? {
106900
+ kind: "estimated",
106901
+ estimatedFrom
106902
+ } : { kind: "table" });
106601
106903
  return {
106602
106904
  modelId,
106603
106905
  p50Ms: cell.p50Ms,
106604
106906
  fps: cell.fps,
106605
106907
  measured: cell.measured,
106606
- source: cell.source ?? { kind: "table" },
106607
- capacitySessions: sessionsOn(cell, sizing),
106908
+ source,
106909
+ ...estimatedFrom !== void 0 ? { estimatedFrom } : {},
106910
+ capacitySessions: sessionsOn(cell, sizing, ctx.accelerator),
106608
106911
  personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
106609
106912
  };
106610
106913
  }
@@ -106655,7 +106958,11 @@ function recommendNode(input) {
106655
106958
  const measured = input.measured ?? {};
106656
106959
  const hasMeasured = Object.values(measured).some((m) => Object.keys(m).length > 0);
106657
106960
  const knownTable = hardwareClass === "unknown" ? void 0 : perf[hardwareClass];
106658
- if (knownTable === void 0 && !hasMeasured) return skippedNode(node, {
106961
+ const canEstimate = (hardware.accelerators ?? []).some((report) => {
106962
+ const kind = acceleratorKindOf(report.deviceKey, hardware);
106963
+ return kind !== null && reportIsScalable(report, kind);
106964
+ });
106965
+ if (knownTable === void 0 && !hasMeasured && !canEstimate) return skippedNode(node, {
106659
106966
  kind: "no-measured-data",
106660
106967
  hardwareClass
106661
106968
  }, hardwareClass);
@@ -106691,7 +106998,12 @@ function recommendNode(input) {
106691
106998
  });
106692
106999
  continue;
106693
107000
  }
106694
- const column = accelerator === null ? void 0 : overlayMeasured(classTable[accelerator], measured[device.key]);
107001
+ const column = accelerator === null ? void 0 : overlayMeasured(resolveLatencyColumn({
107002
+ hardwareClass,
107003
+ accelerator,
107004
+ report: reportFor(hardware, device.key),
107005
+ perf
107006
+ })?.column, measured[device.key]);
106695
107007
  if (accelerator === null || column === void 0) {
106696
107008
  early.set(device.key, [{
106697
107009
  kind: "no-family-build",
@@ -107439,6 +107751,507 @@ function buildBenchStepTree(schema, rootModelId, enabledStepIds) {
107439
107751
  }];
107440
107752
  }
107441
107753
  //#endregion
107754
+ //#region src/pipeline/second-pass/small-subject-recovery.ts
107755
+ /** The object-detection step id — the first-level detector both recipes run. */
107756
+ var OBJECT_DETECTION_STEP_ID = "object-detection";
107757
+ /**
107758
+ * Smallest crop side, native pixels. Below this the model no longer
107759
+ * magnifies enough to matter on a 4K frame, and on a 1080p frame 640 is
107760
+ * already a third of the picture. Measured: 480–800 px all recover the dog.
107761
+ */
107762
+ var RECOVERY_CROP_MIN_SIDE_PX = 640;
107763
+ /**
107764
+ * Largest crop side. A 320 model on a 1024 px square is a 3.2× reduction and
107765
+ * scored the dog at 0.52–0.76; at 1280 px it was absent again. A region wider
107766
+ * than this is tiled, never zoomed out.
107767
+ */
107768
+ var RECOVERY_CROP_MAX_SIDE_PX = 800;
107769
+ /** Context around the region before squaring, as a fraction of its larger side. */
107770
+ var RECOVERY_CROP_PADDING_RATIO = .25;
107771
+ /**
107772
+ * The view the crop is resolved at, compressed. Never larger than the crop
107773
+ * (the floor is {@link RECOVERY_CROP_MIN_SIDE_PX}), so it never upscales, and
107774
+ * the model letterboxes it to its own input from here as it does the frame.
107775
+ */
107776
+ var RECOVERY_VIEW_PX = 640;
107777
+ /**
107778
+ * Crops per frame. Each is one more inference on the same pool (15–25 ms on
107779
+ * CoreML for a 640 view); two cover a person-and-dog blob and a second mover.
107780
+ */
107781
+ var RECOVERY_MAX_CROPS_PER_FRAME = 2;
107782
+ /**
107783
+ * The pass runs at most this often per camera — at 10 fps that is one frame
107784
+ * in five. A subject the full pass misses is missed for seconds, not for one
107785
+ * frame, so the recovery does not need every frame to find it.
107786
+ *
107787
+ * The floor under this number is the tracker: a track coasts for
107788
+ * `maxMissedMs` (4 000 ms by default) before it is dropped, so 2 recoveries a
107789
+ * second keep a subject the full-frame pass has lost ALIVE with 8× margin,
107790
+ * and reach `minHits` (3) in 1.5 s. Raising it past ~1 300 ms would start to
107791
+ * lose tracks between passes.
107792
+ */
107793
+ var RECOVERY_MIN_INTERVAL_MS = 500;
107794
+ /**
107795
+ * A region larger than this share of the frame is not a small subject: a
107796
+ * lighting change, a car filling the picture, a person at the lens. The
107797
+ * full-frame pass already sees those at scale.
107798
+ */
107799
+ var RECOVERY_MAX_REGION_FRAME_FRACTION = .25;
107800
+ /**
107801
+ * How much of a region must lie OUTSIDE every full-frame detection for the
107802
+ * pass to run on it. A person's own blob (dilated, with shadow) is covered
107803
+ * 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
107804
+ */
107805
+ var RECOVERY_MIN_UNCOVERED_FRACTION = .35;
107806
+ /** Motion regions older than this describe a different frame. */
107807
+ var RECOVERY_MOTION_MAX_AGE_MS = 2e3;
107808
+ /** A recovered box overlapping a same-class full-frame box this much is the same subject. */
107809
+ var RECOVERY_FUSE_IOU = .5;
107810
+ /**
107811
+ * A recovered box CONTAINED this much in a same-class full-frame box is a part
107812
+ * of it — a torso the crop cut out of a person the full pass already has.
107813
+ */
107814
+ var RECOVERY_FUSE_CONTAINMENT = .7;
107815
+ /** A box within this many crop-view pixels of the crop edge was clipped by it. */
107816
+ var RECOVERY_EDGE_MARGIN_PX = 2;
107817
+ /** Area of a normalised rectangle, never negative. Shared with the D396 recipe. */
107818
+ function normalizedArea(r) {
107819
+ return Math.max(0, r.w) * Math.max(0, r.h);
107820
+ }
107821
+ var area = normalizedArea;
107822
+ function intersection(a, b) {
107823
+ 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));
107824
+ }
107825
+ /**
107826
+ * Share of `region` not covered by any detection box. The sum of pairwise
107827
+ * intersections over-counts where detections overlap each other, which only
107828
+ * makes the pass MORE conservative; clipped at 1.
107829
+ */
107830
+ function uncoveredFraction(region, detections) {
107831
+ const regionArea = area(region);
107832
+ if (regionArea <= 0) return 0;
107833
+ let covered = 0;
107834
+ for (const detection of detections) covered += intersection(region, detection);
107835
+ return 1 - Math.min(1, covered / regionArea);
107836
+ }
107837
+ /**
107838
+ * THE square-placement rule of this pass — one owner, shared by both recipes.
107839
+ *
107840
+ * A crop is a square of `side` native pixels centred where it can be and slid
107841
+ * inside the frame where it cannot. Both region rules place their squares
107842
+ * through this function so a crop means the same rectangle whichever rule
107843
+ * chose the region.
107844
+ */
107845
+ function squareAround(centreX, centreY, side, native) {
107846
+ return {
107847
+ left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
107848
+ top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
107849
+ side
107850
+ };
107851
+ }
107852
+ function contains(crop, x, y) {
107853
+ return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
107854
+ }
107855
+ /**
107856
+ * Squares for one region: one when it fits the largest side, otherwise up to
107857
+ * two tiles along its longer axis (a person-sized blob with something small
107858
+ * at either end). The padded region decides the side, never the frame.
107859
+ */
107860
+ function cropsForRegion(region, native) {
107861
+ const widthPx = region.w * native.width;
107862
+ const heightPx = region.h * native.height;
107863
+ const longest = Math.max(widthPx, heightPx);
107864
+ const padded = longest * 1.5;
107865
+ const side = Math.round(Math.min(Math.max(padded, 640), 800));
107866
+ const centreX = (region.x + region.w / 2) * native.width;
107867
+ const centreY = (region.y + region.h / 2) * native.height;
107868
+ if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
107869
+ const alongX = widthPx >= heightPx;
107870
+ const quarter = longest / 4;
107871
+ return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
107872
+ }
107873
+ /**
107874
+ * The ONE place the two region gates are decided. `planRecoveryCrops` filters
107875
+ * on it and {@link countRecoverySkips} counts it, so the report can never
107876
+ * describe a gate the planner does not actually apply.
107877
+ */
107878
+ function classifyRegion(region, uncovered) {
107879
+ if (area(region) > .25) return "too-large";
107880
+ if (uncovered < .35) return "covered";
107881
+ return "eligible";
107882
+ }
107883
+ /**
107884
+ * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
107885
+ *
107886
+ * The pass report counts the passes that RAN, so "the second pass stops running
107887
+ * when a person is beside the dog" — a plausible reading of
107888
+ * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
107889
+ * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
107890
+ * — could not be confirmed or killed from the logs at all. This classifies the
107891
+ * regions a frame offered, against the same predicate the planner filters on.
107892
+ */
107893
+ function countRecoverySkips(input) {
107894
+ let tooLarge = 0;
107895
+ let covered = 0;
107896
+ let eligible = 0;
107897
+ for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
107898
+ case "too-large":
107899
+ tooLarge += 1;
107900
+ break;
107901
+ case "covered":
107902
+ covered += 1;
107903
+ break;
107904
+ case "eligible":
107905
+ eligible += 1;
107906
+ break;
107907
+ }
107908
+ return {
107909
+ tooLarge,
107910
+ covered,
107911
+ eligible
107912
+ };
107913
+ }
107914
+ function planRecoveryCrops(input) {
107915
+ const { native } = input;
107916
+ if (Math.min(native.width, native.height) < 640) return [];
107917
+ const ranked = input.regions.map((region) => ({
107918
+ region,
107919
+ uncovered: uncoveredFraction(region, input.detections)
107920
+ })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
107921
+ const planned = [];
107922
+ for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
107923
+ if (planned.length >= 2) return planned;
107924
+ const centreX = crop.left + crop.side / 2;
107925
+ const centreY = crop.top + crop.side / 2;
107926
+ if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
107927
+ planned.push(crop);
107928
+ }
107929
+ return planned;
107930
+ }
107931
+ /**
107932
+ * Boxes the executor produced on the crop VIEW → the result's own space.
107933
+ *
107934
+ * Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
107935
+ * square it was cut from (`crop`, native px) and the space the full-frame
107936
+ * result reports in (`result`, native px on the `frameRef` path, the inline
107937
+ * ≤640 view otherwise). A box touching the view edge was clipped by the crop
107938
+ * and is dropped: half a person is not a person, and the tracker would take
107939
+ * the half as the whole.
107940
+ */
107941
+ function remapCropDetections(detections, crop, view, result, native) {
107942
+ const viewToNative = crop.side / Math.max(1, view.width);
107943
+ const nativeToResultX = result.width / Math.max(1, native.width);
107944
+ const nativeToResultY = result.height / Math.max(1, native.height);
107945
+ const out = [];
107946
+ for (const detection of detections) {
107947
+ const { x, y, width, height } = detection.bbox;
107948
+ if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
107949
+ out.push({
107950
+ ...detection,
107951
+ nativeRecovery: {
107952
+ score: detection.score,
107953
+ cropSidePx: crop.side,
107954
+ viewPx: view.width
107955
+ },
107956
+ bbox: {
107957
+ x: (crop.left + x * viewToNative) * nativeToResultX,
107958
+ y: (crop.top + y * viewToNative) * nativeToResultY,
107959
+ width: width * viewToNative * nativeToResultX,
107960
+ height: height * viewToNative * nativeToResultY
107961
+ }
107962
+ });
107963
+ }
107964
+ return out;
107965
+ }
107966
+ function boxIntersection(a, b) {
107967
+ 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));
107968
+ }
107969
+ function sameSubject(recovered, existing) {
107970
+ const inter = boxIntersection(recovered, existing);
107971
+ if (inter <= 0) return false;
107972
+ const areaR = recovered.width * recovered.height;
107973
+ const union = areaR + existing.width * existing.height - inter;
107974
+ if (union > 0 && inter / union >= .5) return true;
107975
+ return areaR > 0 && inter / areaR >= .7;
107976
+ }
107977
+ /**
107978
+ * Class-aware NMS of the recovered boxes against the full-frame ones. Every
107979
+ * full-frame box is kept as it is — the second pass only ADDS. A recovered box
107980
+ * is added when no full-frame box of the same macro class is the same subject
107981
+ * (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
107982
+ * re-minted so a crop's `d0` cannot collide with the frame's.
107983
+ */
107984
+ function fuseRecoveredDetections(full, recovered) {
107985
+ const accepted = [];
107986
+ for (const [index, candidate] of recovered.entries()) {
107987
+ if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
107988
+ accepted.push({
107989
+ ...candidate,
107990
+ id: `recovered-${index}-${candidate.id}`
107991
+ });
107992
+ }
107993
+ return [...full, ...accepted];
107994
+ }
107995
+ /**
107996
+ * The step the second pass runs: the enabled first-level object detector of
107997
+ * the camera's own tree, with its own model and no children. Same model as
107998
+ * the full pass so a recovered box is comparable to a full-frame one.
107999
+ */
108000
+ function selectRecoveryStep(steps) {
108001
+ const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
108002
+ if (!detector) return null;
108003
+ return {
108004
+ addonId: detector.addonId,
108005
+ ...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
108006
+ enabled: true
108007
+ };
108008
+ }
108009
+ //#endregion
108010
+ //#region src/pipeline/second-pass/motion-crop-plan.ts
108011
+ /**
108012
+ * Step-settings key of the opt-in, on the `object-detection` step — so it is
108013
+ * per camera through `pipelineOrchestrator.setCameraStepSettings` (a
108014
+ * camera-semantic key, D927), and it rides the same
108015
+ * `PipelineStepInputOutput.settings` the per-macro confidence floors ride.
108016
+ */
108017
+ var MOTION_CROP_DISPATCH_KEY = "motionCropDispatch";
108018
+ /**
108019
+ * Read the opt-in. Anything that is not the boolean `true` is the shipped
108020
+ * rule: a stored string or a number is a mis-write (this repo has shipped a
108021
+ * setting written into the wrong sqlite table before), and a mis-write must
108022
+ * not silently change what a camera detects on.
108023
+ */
108024
+ function readRecoveryRecipe(settings) {
108025
+ return settings?.["motionCropDispatch"] === true ? "motion-crop" : "uncovered-region";
108026
+ }
108027
+ /**
108028
+ * The recipe this camera's resolved step tree selects.
108029
+ *
108030
+ * Reads the ENABLED first-level detector's own settings, which is where
108031
+ * `pipelineOrchestrator.setCameraStepSettings` writes a per-camera value. A
108032
+ * tree with no enabled detector runs no second pass at all, so the answer
108033
+ * there is the shipped rule and it is never used.
108034
+ */
108035
+ function readRecoveryRecipeForSteps(steps) {
108036
+ return readRecoveryRecipe(steps.find((step) => step.addonId === "object-detection" && step.enabled !== false)?.settings);
108037
+ }
108038
+ /**
108039
+ * A motion box is grown ×1.5 about its centre before anything else.
108040
+ *
108041
+ * The motion step's box is the CHANGED pixels, which on a moving animal is the
108042
+ * leading edge plus its shadow, not the animal. The detector needs the subject
108043
+ * and the context it was trained on.
108044
+ */
108045
+ var MOTION_CROP_MOTION_PAD = 1.5;
108046
+ /**
108047
+ * An intersecting tracked box is grown ×1.25 — less, because it is already a
108048
+ * subject rectangle rather than a change mask.
108049
+ */
108050
+ var MOTION_CROP_TRACKED_PAD = 1.25;
108051
+ /**
108052
+ * Regions per frame, largest area first.
108053
+ *
108054
+ * Measured as NON-BINDING: over the 156 frames of the recall harness the cap
108055
+ * bound on 0 of them. It is the bound on a storm, not a tuning knob, and the
108056
+ * runner reports when it binds so a camera on which it starts to is visible.
108057
+ */
108058
+ var MOTION_CROP_MAX_REGIONS = 3;
108059
+ /**
108060
+ * The floor a `person` or `vehicle` box must clear when it came out of a CROP
108061
+ * rather than out of the full frame.
108062
+ *
108063
+ * Collected, not assumed: 3 085 quiet frames produced 27 motion-positive
108064
+ * frames and 28 crop invocations. The full-frame arm produced 0 boxes above
108065
+ * any gate; the crop arm produced 3, of which two are genuine false positives,
108066
+ * and both are night-IR corners upscaled into a 320 model — `vehicle 0.749`
108067
+ * and `person 0.701`. Zero false `animal` at any gate, which is why `animal`
108068
+ * is deliberately absent from this table: the class the pass exists to recover
108069
+ * is the class that never produced a false positive, and gating it would cost
108070
+ * the whole measured gain.
108071
+ *
108072
+ * This is a CROP-ONLY floor, applied on top of the per-macro floors the
108073
+ * executor already enforced. A full-frame `person` at 0.70 is untouched — the
108074
+ * full-frame arm is the one arm with ground truth behind its floor
108075
+ * (`minConfidencePerson` 0.65, raised on 2026-07-30 from a measured
108076
+ * distribution).
108077
+ *
108078
+ * The cost of this floor is stated rather than assumed away: it can also
108079
+ * remove a real distant person or vehicle recovered from a crop at 0.70–0.80.
108080
+ * Nothing in the quiet-footage sample measures that, so the floor is a
108081
+ * constant here rather than a slider, and the runner reports every drop it
108082
+ * makes per camera.
108083
+ */
108084
+ var CROP_CLASS_FLOORS = Object.freeze({
108085
+ person: .8,
108086
+ vehicle: .8
108087
+ });
108088
+ /**
108089
+ * Apply the crop-only class floors, REPORTING every drop.
108090
+ *
108091
+ * A class with no entry in {@link CROP_CLASS_FLOORS} is kept: a floor this
108092
+ * table cannot express is not a floor of zero, and silently dropping a class
108093
+ * nobody measured is the exact shape of silent drop this repo has paid for
108094
+ * three separate times.
108095
+ */
108096
+ function applyCropClassFloors(detections) {
108097
+ const kept = [];
108098
+ const dropped = [];
108099
+ for (const detection of detections) {
108100
+ const floor = CROP_CLASS_FLOORS[detection.macroClass];
108101
+ if (floor !== void 0 && detection.score < floor) {
108102
+ dropped.push({
108103
+ macroClass: detection.macroClass,
108104
+ score: detection.score,
108105
+ floor
108106
+ });
108107
+ continue;
108108
+ }
108109
+ kept.push(detection);
108110
+ }
108111
+ return {
108112
+ kept,
108113
+ dropped
108114
+ };
108115
+ }
108116
+ function scaleAbout(rect, factor) {
108117
+ const w = rect.w * factor;
108118
+ const h = rect.h * factor;
108119
+ return {
108120
+ x: rect.x + rect.w / 2 - w / 2,
108121
+ y: rect.y + rect.h / 2 - h / 2,
108122
+ w,
108123
+ h
108124
+ };
108125
+ }
108126
+ function overlaps(a, b) {
108127
+ 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);
108128
+ }
108129
+ function union(a, b) {
108130
+ const x = Math.min(a.x, b.x);
108131
+ const y = Math.min(a.y, b.y);
108132
+ return {
108133
+ x,
108134
+ y,
108135
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
108136
+ h: Math.max(a.y + a.h, b.y + b.h) - y
108137
+ };
108138
+ }
108139
+ function clampToFrame(rect) {
108140
+ const x = Math.max(0, rect.x);
108141
+ const y = Math.max(0, rect.y);
108142
+ return {
108143
+ x,
108144
+ y,
108145
+ w: Math.min(1, rect.x + rect.w) - x,
108146
+ h: Math.min(1, rect.y + rect.h) - y
108147
+ };
108148
+ }
108149
+ /**
108150
+ * Merge transitively every seed whose padded box overlaps another, and answer
108151
+ * the union rectangles.
108152
+ *
108153
+ * Transitive on purpose: a person, the dog beside them and the dog's shadow
108154
+ * are three components of one subject, and cutting three crops for them spends
108155
+ * two inferences on pixels the first one already showed the detector.
108156
+ */
108157
+ function mergeSeeds(seeds) {
108158
+ const merged = [];
108159
+ for (const seed of seeds) {
108160
+ let current = seed;
108161
+ let changed = true;
108162
+ while (changed) {
108163
+ changed = false;
108164
+ for (let i = merged.length - 1; i >= 0; i -= 1) {
108165
+ const other = merged[i];
108166
+ if (other === void 0 || !overlaps(current, other)) continue;
108167
+ current = union(current, other);
108168
+ merged.splice(i, 1);
108169
+ changed = true;
108170
+ }
108171
+ }
108172
+ merged.push(current);
108173
+ }
108174
+ return merged;
108175
+ }
108176
+ /**
108177
+ * The regions the measured recipe judges this frame on.
108178
+ *
108179
+ * Motion boxes padded {@link MOTION_CROP_MOTION_PAD}; every full-frame box
108180
+ * that intersects a RAW motion box padded {@link MOTION_CROP_TRACKED_PAD} and
108181
+ * added as a seed of its own; everything that overlaps merged; the result
108182
+ * clamped to the frame, ordered largest first and capped at
108183
+ * {@link MOTION_CROP_MAX_REGIONS}.
108184
+ *
108185
+ * There is deliberately no coverage gate and no size gate here. Those belong
108186
+ * to the shipped rule, and they are what makes it a rule about a subject
108187
+ * BESIDE a detected one; the measurement says the pass is worth running on the
108188
+ * detected subject's own region too.
108189
+ */
108190
+ function planMotionCropRegions(input) {
108191
+ if (input.regions.length === 0) return [];
108192
+ const seeds = input.regions.map((r) => scaleAbout(r, MOTION_CROP_MOTION_PAD));
108193
+ for (const detection of input.detections) {
108194
+ if (!input.regions.some((region) => overlaps(detection, region))) continue;
108195
+ seeds.push(scaleAbout(detection, MOTION_CROP_TRACKED_PAD));
108196
+ }
108197
+ return mergeSeeds(seeds).map(clampToFrame).filter((r) => r.w > 0 && r.h > 0).toSorted((a, b) => normalizedArea(b) - normalizedArea(a)).slice(0, 3);
108198
+ }
108199
+ /**
108200
+ * The square the detector is fed for one measured region.
108201
+ *
108202
+ * "Grow to the detector's input aspect ratio without ever shrinking" is a
108203
+ * SQUARE here, and that is a fact about the production detector rather than a
108204
+ * simplification: `yolov9m-320-int8` takes 320×320, as does every hardware
108205
+ * default in the catalog.
108206
+ *
108207
+ * The side is the region's longer side, floored at
108208
+ * {@link RECOVERY_CROP_MIN_SIDE_PX} — the growth is what magnifies the subject
108209
+ * — and ceilinged at {@link RECOVERY_CROP_MAX_SIDE_PX}. The ceiling is NOT a
108210
+ * simplification either: at 1024 px the same 320 model scored the reference
108211
+ * dog 0.52–0.76, and at 1280 px lost it entirely
108212
+ * (`docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`). A
108213
+ * region longer than the ceiling is TILED along its longer axis, never zoomed
108214
+ * out.
108215
+ */
108216
+ function motionCropsForRegion(region, native) {
108217
+ const widthPx = region.w * native.width;
108218
+ const heightPx = region.h * native.height;
108219
+ const longest = Math.max(widthPx, heightPx);
108220
+ const side = Math.round(Math.min(Math.max(longest, 640), 800));
108221
+ const centreX = (region.x + region.w / 2) * native.width;
108222
+ const centreY = (region.y + region.h / 2) * native.height;
108223
+ if (longest <= 800) return [squareAround(centreX, centreY, side, native)];
108224
+ const alongX = widthPx >= heightPx;
108225
+ const quarter = longest / 4;
108226
+ return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
108227
+ }
108228
+ /** `true` when `inner` lies entirely inside `outer`. */
108229
+ function cropContains(outer, inner) {
108230
+ 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;
108231
+ }
108232
+ /**
108233
+ * The crops the measured recipe runs on this frame.
108234
+ *
108235
+ * Dedupe is by CONTAINMENT only. Two squares that merely overlap both run: the
108236
+ * pixels the second adds are pixels no other crop shows the detector, and the
108237
+ * fusion step downstream already refuses a duplicate BOX. Dropping a crop
108238
+ * because it overlapped would drop the subject sitting at its edge, which is
108239
+ * the whole population this pass exists for.
108240
+ *
108241
+ * A frame that cannot hold the smallest crop yields nothing: it is already
108242
+ * seen at scale.
108243
+ */
108244
+ function planMotionCrops(input) {
108245
+ const { native } = input;
108246
+ if (Math.min(native.width, native.height) < 640) return [];
108247
+ const planned = [];
108248
+ for (const region of planMotionCropRegions(input)) for (const crop of motionCropsForRegion(region, native)) {
108249
+ if (planned.some((existing) => cropContains(existing, crop))) continue;
108250
+ planned.push(crop);
108251
+ }
108252
+ return planned;
108253
+ }
108254
+ //#endregion
107442
108255
  //#region src/pipeline/root-face-crop.ts
107443
108256
  /** Side of the square, as a multiple of the face box's longer side. */
107444
108257
  var ROOT_FACE_REFINE_SCALE = 1.6;
@@ -109793,6 +110606,7 @@ exports.ADMIN_SCOPE = ADMIN_SCOPE;
109793
110606
  exports.AGENT_MDNS_QUERY_NAME = AGENT_MDNS_QUERY_NAME;
109794
110607
  exports.AGENT_MDNS_SERVICE_TYPE = AGENT_MDNS_SERVICE_TYPE;
109795
110608
  exports.AGENT_MDNS_TXT_KEYS = AGENT_MDNS_TXT_KEYS;
110609
+ exports.AGGREGATE_THROUGHPUT_BY_KIND = AGGREGATE_THROUGHPUT_BY_KIND;
109796
110610
  exports.ALEXA_EGRESS_PROFILE = ALEXA_EGRESS_PROFILE;
109797
110611
  exports.ALL_CAPABILITY_DEFINITIONS = ALL_CAPABILITY_DEFINITIONS;
109798
110612
  exports.ALL_METHOD_ACCESS = ALL_METHOD_ACCESS;
@@ -109820,6 +110634,7 @@ exports.AUDIO_SCALE_EPOCH_MS = AUDIO_SCALE_EPOCH_MS;
109820
110634
  exports.AUTOMATIC_HUB_ADDRESS_FLAGS = AUTOMATIC_HUB_ADDRESS_FLAGS;
109821
110635
  exports.AUTO_SHARE_HEADROOM = AUTO_SHARE_HEADROOM;
109822
110636
  exports.AUTO_TUNE_FAMILY = AUTO_TUNE_FAMILY;
110637
+ exports.AcceleratorComputeSchema = AcceleratorComputeSchema;
109823
110638
  exports.AcceleratorKindSchema = AcceleratorKindSchema;
109824
110639
  exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
109825
110640
  exports.AccessoryKind = AccessoryKind;
@@ -110006,8 +110821,11 @@ exports.CONTAINER_CHILD_PRIORITY = CONTAINER_CHILD_PRIORITY;
110006
110821
  exports.CORAL_DEFAULT_MODEL_ID = CORAL_DEFAULT_MODEL_ID;
110007
110822
  exports.CORE_BLOCKS_ADDON_ID = CORE_BLOCKS_ADDON_ID;
110008
110823
  exports.CORE_BLOCK_ADDON_PREFIX = CORE_BLOCK_ADDON_PREFIX;
110824
+ exports.CORE_ULTRA_265K_IGPU_CLOCK_MHZ = CORE_ULTRA_265K_IGPU_CLOCK_MHZ;
110825
+ exports.CORE_ULTRA_265K_IGPU_EU = CORE_ULTRA_265K_IGPU_EU;
110009
110826
  exports.CREDENTIAL_TOKEN_AUDIENCE = CREDENTIAL_TOKEN_AUDIENCE;
110010
110827
  exports.CREDENTIAL_TOKEN_KIND = CREDENTIAL_TOKEN_KIND;
110828
+ exports.CROP_CLASS_FLOORS = CROP_CLASS_FLOORS;
110011
110829
  exports.CROP_SUPPLY_SOURCES = CROP_SUPPLY_SOURCES;
110012
110830
  exports.CROP_SUPPLY_STEP_IDS = CROP_SUPPLY_STEP_IDS;
110013
110831
  exports.CUSTOMIZATION_BLOCK_NAME_PREFIX = require_composition.CUSTOMIZATION_BLOCK_NAME_PREFIX;
@@ -110358,6 +111176,8 @@ exports.DoorbellStatusSchema = DoorbellStatusSchema;
110358
111176
  exports.EMBEDDING_MODEL_FAILED = EMBEDDING_MODEL_FAILED;
110359
111177
  exports.EMBEDDING_REQUIREMENTS_FAILED = EMBEDDING_REQUIREMENTS_FAILED;
110360
111178
  exports.EMBEDDING_REQUIREMENTS_UNAVAILABLE = EMBEDDING_REQUIREMENTS_UNAVAILABLE;
111179
+ exports.ESTIMATE_COMPUTE_RATIO_MAX = ESTIMATE_COMPUTE_RATIO_MAX;
111180
+ exports.ESTIMATE_COMPUTE_RATIO_MIN = ESTIMATE_COMPUTE_RATIO_MIN;
110361
111181
  exports.EVENTFUL_CAP_NAMES = EVENTFUL_CAP_NAMES;
110362
111182
  exports.EVENT_DENSITY_BATCH_MAX = EVENT_DENSITY_BATCH_MAX;
110363
111183
  exports.EVENT_KIND_BY_CAP = EVENT_KIND_BY_CAP;
@@ -110708,6 +111528,10 @@ exports.MODEL_FORMATS = require_sleep.MODEL_FORMATS;
110708
111528
  exports.MODEL_HOSTING = MODEL_HOSTING;
110709
111529
  exports.MODEL_PROVIDER_IDS = MODEL_PROVIDER_IDS;
110710
111530
  exports.MOTION_CLOSE_AFTER_MS = MOTION_CLOSE_AFTER_MS;
111531
+ exports.MOTION_CROP_DISPATCH_KEY = MOTION_CROP_DISPATCH_KEY;
111532
+ exports.MOTION_CROP_MAX_REGIONS = MOTION_CROP_MAX_REGIONS;
111533
+ exports.MOTION_CROP_MOTION_PAD = MOTION_CROP_MOTION_PAD;
111534
+ exports.MOTION_CROP_TRACKED_PAD = MOTION_CROP_TRACKED_PAD;
110711
111535
  exports.MOTION_SOURCE_LABELS = MOTION_SOURCE_LABELS;
110712
111536
  exports.MOTION_TRIGGER_FEATURE = MOTION_TRIGGER_FEATURE;
110713
111537
  exports.ManagedModelCatalogEntrySchema = ManagedModelCatalogEntrySchema;
@@ -110772,6 +111596,8 @@ exports.MotionZoneStatusSchema = MotionZoneStatusSchema;
110772
111596
  exports.MqttBrokerInfoSchema = BrokerInfoSchema$1;
110773
111597
  exports.MqttBrokerStatusSchema = StatusSchema;
110774
111598
  exports.MutationFilterSchema = MutationFilterSchema;
111599
+ exports.N100_IGPU_CLOCK_MHZ = N100_IGPU_CLOCK_MHZ;
111600
+ exports.N100_IGPU_EU = N100_IGPU_EU;
110775
111601
  exports.NATIVE_LEASE_ACTIVITY_FIELD = NATIVE_LEASE_ACTIVITY_FIELD;
110776
111602
  exports.NATIVE_LEASE_ACTIVITY_KEY = NATIVE_LEASE_ACTIVITY_KEY;
110777
111603
  exports.NATIVE_LEASE_ADMISSION_FIELD = NATIVE_LEASE_ADMISSION_FIELD;
@@ -111037,6 +111863,7 @@ exports.NotifierStatusSchema = NotifierStatusSchema;
111037
111863
  exports.NumericSensorStatusSchema = NumericSensorStatusSchema;
111038
111864
  exports.OAUTH_LANDING_PATH = OAUTH_LANDING_PATH;
111039
111865
  exports.OAUTH_LANDING_REASONS = OAUTH_LANDING_REASONS;
111866
+ exports.OBJECT_DETECTION_STEP_ID = OBJECT_DETECTION_STEP_ID;
111040
111867
  exports.OPERATOR_WRITTEN_STALE_MS = OPERATOR_WRITTEN_STALE_MS;
111041
111868
  exports.OPS_LOG_DEFAULT_LIMIT = OPS_LOG_DEFAULT_LIMIT;
111042
111869
  exports.OPS_LOG_RING_DEFAULT_MAX = OPS_LOG_RING_DEFAULT_MAX;
@@ -111087,6 +111914,7 @@ exports.PEER_BYTES_TICKET_TTL_MS = require_sleep.PEER_BYTES_TICKET_TTL_MS;
111087
111914
  exports.PENDING_BLOCK_ID = PENDING_BLOCK_ID;
111088
111915
  exports.PET_FEEDER_MANUAL_FEED_MAX = PET_FEEDER_MANUAL_FEED_MAX;
111089
111916
  exports.PET_FEEDER_MANUAL_FEED_MIN = PET_FEEDER_MANUAL_FEED_MIN;
111917
+ exports.PHOENIX_780M_CU = PHOENIX_780M_CU;
111090
111918
  exports.PIPELINE_ANALYTICS_CAP_NAME = PIPELINE_ANALYTICS_CAP_NAME;
111091
111919
  exports.PIPELINE_FLOW_CAPABILITY_NAMES = PIPELINE_FLOW_CAPABILITY_NAMES;
111092
111920
  exports.PIPELINE_OWNER_CAPABILITY_NAMES = PIPELINE_OWNER_CAPABILITY_NAMES;
@@ -111103,6 +111931,7 @@ exports.PUBLIC_ENDPOINT_HTTP_REASON = PUBLIC_ENDPOINT_HTTP_REASON;
111103
111931
  exports.PYTHON_SCRIPT = PYTHON_SCRIPT;
111104
111932
  exports.PackageInstallationSchema = PackageInstallationSchema;
111105
111933
  exports.PackageInstallationsSchema = PackageInstallationsSchema;
111934
+ exports.PackageLlmVerdictSchema = PackageLlmVerdictSchema;
111106
111935
  exports.PackageUpdateSchema = PackageUpdateSchema;
111107
111936
  exports.PackageVersionInfoSchema = PackageVersionInfoSchema;
111108
111937
  exports.ParkRefusalSchema = ParkRefusalSchema;
@@ -111135,6 +111964,7 @@ exports.PlaintextSecretError = require_sleep.PlaintextSecretError;
111135
111964
  exports.PolygonPointSchema = PolygonPointSchema;
111136
111965
  exports.PoolMemoryWatchdog = PoolMemoryWatchdog;
111137
111966
  exports.PowerMeterStatusSchema = PowerMeterStatusSchema;
111967
+ exports.PrecisionGopsSchema = PrecisionGopsSchema;
111138
111968
  exports.PreferenceDeviceIdSchema = PreferenceDeviceIdSchema;
111139
111969
  exports.PreferenceNamespaceSchema = PreferenceNamespaceSchema;
111140
111970
  exports.PreferenceRowSchema = PreferenceRowSchema;
@@ -111203,7 +112033,21 @@ exports.RECORD_QUERY_MAX = require_sleep.RECORD_QUERY_MAX;
111203
112033
  exports.RECORD_SECRET_COLLECTION_PREFIX = require_sleep.RECORD_SECRET_COLLECTION_PREFIX;
111204
112034
  exports.RECORD_SETTINGS_MAX_ROWS = require_sleep.RECORD_SETTINGS_MAX_ROWS;
111205
112035
  exports.RECORD_SWEEP_INTERVAL_MS = require_sleep.RECORD_SWEEP_INTERVAL_MS;
112036
+ exports.RECOVERY_CROP_MAX_SIDE_PX = RECOVERY_CROP_MAX_SIDE_PX;
112037
+ exports.RECOVERY_CROP_MIN_SIDE_PX = RECOVERY_CROP_MIN_SIDE_PX;
112038
+ exports.RECOVERY_CROP_PADDING_RATIO = RECOVERY_CROP_PADDING_RATIO;
112039
+ exports.RECOVERY_EDGE_MARGIN_PX = RECOVERY_EDGE_MARGIN_PX;
112040
+ exports.RECOVERY_FUSE_CONTAINMENT = RECOVERY_FUSE_CONTAINMENT;
112041
+ exports.RECOVERY_FUSE_IOU = RECOVERY_FUSE_IOU;
112042
+ exports.RECOVERY_MAX_CROPS_PER_FRAME = RECOVERY_MAX_CROPS_PER_FRAME;
112043
+ exports.RECOVERY_MAX_REGION_FRAME_FRACTION = RECOVERY_MAX_REGION_FRAME_FRACTION;
112044
+ exports.RECOVERY_MIN_INTERVAL_MS = RECOVERY_MIN_INTERVAL_MS;
112045
+ exports.RECOVERY_MIN_UNCOVERED_FRACTION = RECOVERY_MIN_UNCOVERED_FRACTION;
112046
+ exports.RECOVERY_MOTION_MAX_AGE_MS = RECOVERY_MOTION_MAX_AGE_MS;
112047
+ exports.RECOVERY_VIEW_PX = RECOVERY_VIEW_PX;
111206
112048
  exports.REDACTED_SECRET = require_sleep.REDACTED_SECRET;
112049
+ exports.REFERENCE_COMPUTE = REFERENCE_COMPUTE;
112050
+ exports.REFERENCE_MATCH_FRACTION = REFERENCE_MATCH_FRACTION;
111207
112051
  exports.REMOVAL_RECONCILE_INTERVAL_MS = REMOVAL_RECONCILE_INTERVAL_MS;
111208
112052
  exports.REMOVED_MODEL_IDS = REMOVED_MODEL_IDS;
111209
112053
  exports.REMOVE_LEAF = require_sleep.REMOVE_LEAF;
@@ -111390,6 +112234,7 @@ exports.SECRET_META = require_sleep.SECRET_META;
111390
112234
  exports.SECRET_REF_SCHEME = require_sleep.SECRET_REF_SCHEME;
111391
112235
  exports.SENSOR_FEATURES = SENSOR_FEATURES;
111392
112236
  exports.SENSOR_MAP = SENSOR_MAP;
112237
+ exports.SERIAL_CAPACITY_CAMERA_FPS = SERIAL_CAPACITY_CAMERA_FPS;
111393
112238
  exports.SERVER_NAME_MAX_LENGTH = SERVER_NAME_MAX_LENGTH;
111394
112239
  exports.SERVER_NAME_SETTING_KEY = SERVER_NAME_SETTING_KEY;
111395
112240
  exports.SESSION_PLACEMENT_SETTING_KEY = SESSION_PLACEMENT_SETTING_KEY;
@@ -111745,6 +112590,7 @@ exports.addonWidgetsSourceCapability = addonWidgetsSourceCapability;
111745
112590
  exports.addonsCapability = addonsCapability;
111746
112591
  exports.adminUiCapability = require_sleep.adminUiCapability;
111747
112592
  exports.advertisedHubOrigins = advertisedHubOrigins;
112593
+ exports.aggregateThroughputFactor = aggregateThroughputFactor;
111748
112594
  exports.airQualitySensorCapability = airQualitySensorCapability;
111749
112595
  exports.alarmDeviceBlocksArming = alarmDeviceBlocksArming;
111750
112596
  exports.alarmDeviceSecureOnArm = alarmDeviceSecureOnArm;
@@ -111762,6 +112608,7 @@ exports.ambientLightSensorCapability = ambientLightSensorCapability;
111762
112608
  exports.annotateClipsWithEventLabels = annotateClipsWithEventLabels;
111763
112609
  exports.appFlowLocation = appFlowLocation;
111764
112610
  exports.applyBlockingEdit = applyBlockingEdit;
112611
+ exports.applyCropClassFloors = applyCropClassFloors;
111765
112612
  exports.applyModeDelaysEdits = applyModeDelaysEdits;
111766
112613
  exports.applyModeDeviceListEdit = applyModeDeviceListEdit;
111767
112614
  exports.applyModelBoundSettings = applyModelBoundSettings;
@@ -111897,6 +112744,7 @@ exports.compositionSliceKey = require_composition.compositionSliceKey;
111897
112744
  exports.compositionSourceKey = require_composition.compositionSourceKey;
111898
112745
  exports.compositionSourceRefs = compositionSourceRefs;
111899
112746
  exports.computeCapacityShares = computeCapacityShares;
112747
+ exports.computeScores = computeScores;
111900
112748
  exports.conditionDepth = conditionDepth;
111901
112749
  exports.conditionExclusionReason = conditionExclusionReason;
111902
112750
  exports.conditionVisibleForKind = conditionVisibleForKind;
@@ -111919,6 +112767,7 @@ exports.coreBlockSourceKind = coreBlockSourceKind;
111919
112767
  exports.coreBlocksCapability = coreBlocksCapability;
111920
112768
  exports.cosineSimilarity = cosineSimilarity;
111921
112769
  exports.countConditionLeaves = countConditionLeaves;
112770
+ exports.countRecoverySkips = countRecoverySkips;
111922
112771
  exports.coverCapability = coverCapability;
111923
112772
  exports.createAlarmProfile = createAlarmProfile;
111924
112773
  exports.createDeviceExtensionProvider = createDeviceExtensionProvider;
@@ -112089,6 +112938,7 @@ exports.formatSecretScope = require_sleep.formatSecretScope;
112089
112938
  exports.freshResults = freshResults;
112090
112939
  exports.frigateDefaultClassFloors = frigateDefaultClassFloors;
112091
112940
  exports.frigateLabelFields = frigateLabelFields;
112941
+ exports.fuseRecoveredDetections = fuseRecoveredDetections;
112092
112942
  exports.gasCapability = gasCapability;
112093
112943
  exports.gbToBytes = gbToBytes;
112094
112944
  exports.generateAutomationBlock = generateAutomationBlock;
@@ -112299,6 +113149,7 @@ exports.normalizeOrigin = normalizeOrigin;
112299
113149
  exports.normalizeSecretOwner = require_sleep.normalizeSecretOwner;
112300
113150
  exports.normalizeTokenScopes = normalizeTokenScopes;
112301
113151
  exports.normalizeUnit = normalizeUnit;
113152
+ exports.normalizedArea = normalizedArea;
112302
113153
  exports.notificationOutputCapability = notificationOutputCapability;
112303
113154
  exports.notificationRulesCapability = notificationRulesCapability;
112304
113155
  exports.notifierCapability = notifierCapability;
@@ -112316,6 +113167,7 @@ exports.osdCapability = osdCapability;
112316
113167
  exports.osdManagerCapability = osdManagerCapability;
112317
113168
  exports.overlayClusterStepSettings = overlayClusterStepSettings;
112318
113169
  exports.packagePickerItems = packagePickerItems;
113170
+ exports.parseAcceleratorFactsJson = parseAcceleratorFactsJson;
112319
113171
  exports.parseAdminBase = parseAdminBase;
112320
113172
  exports.parseAlertEpisodePayload = parseAlertEpisodePayload;
112321
113173
  exports.parseAppAuthCallback = parseAppAuthCallback;
@@ -112366,6 +113218,9 @@ exports.pipelineRunnerCapability = pipelineRunnerCapability;
112366
113218
  exports.planComposition = planComposition;
112367
113219
  exports.planCompositionEvents = planCompositionEvents;
112368
113220
  exports.planItems = planItems;
113221
+ exports.planMotionCropRegions = planMotionCropRegions;
113222
+ exports.planMotionCrops = planMotionCrops;
113223
+ exports.planRecoveryCrops = planRecoveryCrops;
112369
113224
  exports.plateGalleryCapability = plateGalleryCapability;
112370
113225
  exports.platformProbeCapability = platformProbeCapability;
112371
113226
  exports.poolMemoryThreshold = poolMemoryThreshold;
@@ -112394,6 +113249,8 @@ exports.readHubMotionStatus = readHubMotionStatus;
112394
113249
  exports.readMotionTriggeredBy = readMotionTriggeredBy;
112395
113250
  exports.readNativeLeaseOverride = readNativeLeaseOverride;
112396
113251
  exports.readNodePin = require_sleep.readNodePin;
113252
+ exports.readRecoveryRecipe = readRecoveryRecipe;
113253
+ exports.readRecoveryRecipeForSteps = readRecoveryRecipeForSteps;
112397
113254
  exports.readTimelapseGeneratedAt = readTimelapseGeneratedAt;
112398
113255
  exports.readUseRootModel = readUseRootModel;
112399
113256
  exports.readinessKey = require_sleep.readinessKey;
@@ -112415,9 +113272,11 @@ exports.recordsErrorFromMessage = require_sleep.recordsErrorFromMessage;
112415
113272
  exports.rectsToCells = rectsToCells;
112416
113273
  exports.reducePoints = reducePoints;
112417
113274
  exports.registerKnownSecret = require_sleep.registerKnownSecret;
113275
+ exports.remapCropDetections = remapCropDetections;
112418
113276
  exports.renameAlarmProfile = renameAlarmProfile;
112419
113277
  exports.renderVocabPhrase = renderVocabPhrase;
112420
113278
  exports.replacedFieldKeys = replacedFieldKeys;
113279
+ exports.reportIsScalable = reportIsScalable;
112421
113280
  exports.requestMountPrefix = requestMountPrefix;
112422
113281
  exports.requiredTopLevelKeys = requiredTopLevelKeys;
112423
113282
  exports.requiresPython = requiresPython;
@@ -112445,6 +113304,7 @@ exports.resolveHubAddressRoles = resolveHubAddressRoles;
112445
113304
  exports.resolveHydratedFieldValue = require_sleep.resolveHydratedFieldValue;
112446
113305
  exports.resolveIconKey = resolveIconKey;
112447
113306
  exports.resolveInternalAddress = resolveInternalAddress;
113307
+ exports.resolveLatencyColumn = resolveLatencyColumn;
112448
113308
  exports.resolveLocationMode = resolveLocationMode;
112449
113309
  exports.resolveMethodAuth = resolveMethodAuth;
112450
113310
  exports.resolveMethodRole = resolveMethodRole;
@@ -112511,6 +113371,7 @@ exports.secretTreeFromSchema = require_sleep.secretTreeFromSchema;
112511
113371
  exports.secureActuationFor = secureActuationFor;
112512
113372
  exports.selectAppConnectionEndpoints = selectAppConnectionEndpoints;
112513
113373
  exports.selectAssignedProfileSlots = require_sleep.selectAssignedProfileSlots;
113374
+ exports.selectRecoveryStep = selectRecoveryStep;
112514
113375
  exports.sendTimesForEditor = sendTimesForEditor;
112515
113376
  exports.sendTimesOfSchedule = sendTimesOfSchedule;
112516
113377
  exports.sensorRaisedOf = sensorRaisedOf;
@@ -112537,6 +113398,7 @@ exports.smokeCapability = smokeCapability;
112537
113398
  exports.smtpProviderCapability = smtpProviderCapability;
112538
113399
  exports.snapshotCapability = snapshotCapability;
112539
113400
  exports.splitTranslationRef = splitTranslationRef;
113401
+ exports.squareAround = squareAround;
112540
113402
  exports.ssoBridgeCapability = ssoBridgeCapability;
112541
113403
  exports.startMoverUnderClearance = startMoverUnderClearance;
112542
113404
  exports.startReachabilityPoll = startReachabilityPoll;