@camstack/addon-export-hap 1.2.15 → 1.2.16

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.
@@ -15546,6 +15546,60 @@ var TrackFlagsSchema = object({
15546
15546
  * `trained` without a re-fetch. */
15547
15547
  retrainStatus: RetrainStatusSchema
15548
15548
  });
15549
+ union([literal(1), literal(2)]);
15550
+ /**
15551
+ * WHO decided a label, and when. Carried per tier so a value can be traced to
15552
+ * the step and model that produced it — which is what makes the write rule
15553
+ * arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
15554
+ * and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
15555
+ *
15556
+ * `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
15557
+ * `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
15558
+ * `migration:4g` for a value the 4g migration moved from the single-slot era —
15559
+ * that value has no provenance, and the write rule lets ANY properly-attributed
15560
+ * write of the same tier replace it regardless of score.
15561
+ */
15562
+ var LabelAttributionSchema = object({
15563
+ stepId: string(),
15564
+ modelId: string().optional(),
15565
+ decidedAt: number()
15566
+ });
15567
+ /**
15568
+ * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
15569
+ * `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
15570
+ * track and its events always answer the same question the same way.
15571
+ *
15572
+ * Two scalar columns, not an array: every consumer wants "the coarse one" or
15573
+ * "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
15574
+ * is tier 2, and each carries its own score + attribution.
15575
+ *
15576
+ * **Reading it.** What a human should be shown is `subLabel ?? label` — the
15577
+ * finest thing known. Before 4g the single `label` column held the finest
15578
+ * value, so a consumer that has not been updated reads the tier-1 slot and
15579
+ * shows nothing on a species-only row; that is why the migration puts every
15580
+ * pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
15581
+ * and why the read surfaces were changed in the same train.
15582
+ *
15583
+ * **Writing it.** The slots are independent, which is the whole point: a
15584
+ * tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
15585
+ * migratorius`), so fineness cannot regress by construction. Within a tier the
15586
+ * higher score wins. One rule, one implementation — see
15587
+ * `pipeline/label-tier.ts` in addon-post-analysis.
15588
+ */
15589
+ var TieredLabelFields = {
15590
+ /** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
15591
+ label: string().optional(),
15592
+ /** Confidence of the tier-1 value, as reported by the deciding step. */
15593
+ labelScore: number().optional(),
15594
+ /** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
15595
+ labelMeta: LabelAttributionSchema.optional(),
15596
+ /** Tier 2 — the instance. See {@link LabelTierSchema}. */
15597
+ subLabel: string().optional(),
15598
+ /** Confidence of the tier-2 value, as reported by the deciding step. */
15599
+ subLabelScore: number().optional(),
15600
+ /** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
15601
+ subLabelMeta: LabelAttributionSchema.optional()
15602
+ };
15549
15603
  /** Per-camera slice of a training-export estimate. */
15550
15604
  var TrainingExportDeviceTotalsSchema = object({
15551
15605
  deviceId: number(),
@@ -15570,7 +15624,7 @@ var TrackSchema = object({
15570
15624
  trackId: string(),
15571
15625
  deviceId: number(),
15572
15626
  className: string(),
15573
- label: string().optional(),
15627
+ ...TieredLabelFields,
15574
15628
  producingDeviceName: string().optional(),
15575
15629
  /** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
15576
15630
  source: TrackSourceSchema.optional(),
@@ -15683,7 +15737,7 @@ var ObjectEventSchema = object({
15683
15737
  /** Omitted in slim projection. */
15684
15738
  trackId: string().optional(),
15685
15739
  className: string(),
15686
- label: string().optional(),
15740
+ ...TieredLabelFields,
15687
15741
  /** Omitted in slim projection. */
15688
15742
  confidence: number().optional(),
15689
15743
  /** Heavy JSON — omitted in slim projection. */
@@ -15764,6 +15818,173 @@ var MediaFileSchema = object({
15764
15818
  * stored blob and a `?variant=thumb` rendering without fetching either.
15765
15819
  */
15766
15820
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
15821
+ /**
15822
+ * The MACRO tier of an annotation — a CLOSED set.
15823
+ *
15824
+ * This is what the exported detector predicts, so a typo here is a new class
15825
+ * with one example in it. `label` and `subLabel` are open strings by contrast:
15826
+ * the whole point of the page is teaching the model things it does not know
15827
+ * yet, and constraining that vocabulary would make it useless.
15828
+ *
15829
+ * A macro class is NEVER a label. The provider refuses a write whose `label` or
15830
+ * `subLabel` is one of these values, in any casing, because once `person`
15831
+ * exists in both tiers "every person box" stops being answerable without
15832
+ * knowing every string anyone ever typed — and the damage is retroactive.
15833
+ */
15834
+ var RetrainMacroClassSchema = _enum([
15835
+ "person",
15836
+ "vehicle",
15837
+ "animal",
15838
+ "package",
15839
+ "face",
15840
+ "plate"
15841
+ ]);
15842
+ /** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
15843
+ var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
15844
+ /** Did a human draw this box, or did the assist propose it? */
15845
+ var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
15846
+ /** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
15847
+ var RetrainBboxSchema = object({
15848
+ x: number(),
15849
+ y: number(),
15850
+ w: number(),
15851
+ h: number()
15852
+ });
15853
+ /**
15854
+ * One annotated subject.
15855
+ *
15856
+ * `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
15857
+ * (letterboxed root / zone-cropped package / subject-cropped classifier) are
15858
+ * derived from it at export and never stored — storing them is how one feature
15859
+ * space ends up holding two crops of the same subject (D52).
15860
+ */
15861
+ var RetrainAnnotationSchema = object({
15862
+ id: string(),
15863
+ trackId: string(),
15864
+ deviceId: number(),
15865
+ /** The COPY in retrain storage — never the source track's media key. */
15866
+ mediaKey: string(),
15867
+ bbox: RetrainBboxSchema,
15868
+ macroClass: RetrainMacroClassSchema,
15869
+ label: string().optional(),
15870
+ subLabel: string().optional(),
15871
+ kind: RetrainAnnotationKindSchema,
15872
+ source: RetrainAnnotationSourceSchema,
15873
+ /** Which model proposed this box — or, on a `model_error`, drew the phantom. */
15874
+ assistModelId: string().optional(),
15875
+ assistScore: number().optional(),
15876
+ exportedInBatch: string().optional(),
15877
+ createdAt: number()
15878
+ });
15879
+ /** The write form — the server owns `id`, `createdAt` and the frame binding. */
15880
+ var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
15881
+ id: true,
15882
+ trackId: true,
15883
+ deviceId: true,
15884
+ mediaKey: true,
15885
+ createdAt: true,
15886
+ exportedInBatch: true
15887
+ });
15888
+ /** A track sitting in `staging`, with everything the worklist needs to rank it. */
15889
+ var RetrainTrackSchema = object({
15890
+ trackId: string(),
15891
+ deviceId: number(),
15892
+ className: string(),
15893
+ label: string().optional(),
15894
+ firstSeen: number(),
15895
+ lastSeen: number(),
15896
+ /** How many frames the dataset already holds from this track. */
15897
+ frameCount: number().int(),
15898
+ /** How many subjects have been annotated on those frames. `0` with
15899
+ * `frameCount: 0` is exactly "staging, still to work". */
15900
+ annotationCount: number().int()
15901
+ });
15902
+ /** A frame the picker may offer — an index row, no blob was read to produce it. */
15903
+ var RetrainFrameCandidateSchema = object({
15904
+ mediaKey: string(),
15905
+ kind: MediaFileKindEnum,
15906
+ timestamp: number(),
15907
+ sizeBytes: number().int(),
15908
+ /** A copy of this original already exists — selecting it is free and cannot
15909
+ * fail, whatever became of the original. */
15910
+ copied: boolean()
15911
+ });
15912
+ /** A frame the dataset OWNS: bytes copied at selection time. */
15913
+ var RetrainFrameSchema = object({
15914
+ frameId: string(),
15915
+ deviceId: number(),
15916
+ trackId: string(),
15917
+ /** Provenance only. It may already point at nothing — that is expected. */
15918
+ sourceMediaKey: string(),
15919
+ sourceKind: MediaFileKindEnum,
15920
+ sizeBytes: number().int(),
15921
+ width: number().int(),
15922
+ height: number().int(),
15923
+ copiedAt: number()
15924
+ });
15925
+ /** Why a copy-on-select could not be honoured — named, never a silent skip. */
15926
+ var RetrainCopyRefusalSchema = _enum([
15927
+ "source-missing",
15928
+ "unreadable-image",
15929
+ "write-failed"
15930
+ ]);
15931
+ var RetrainFrameSelectionSchema = object({
15932
+ copied: array(RetrainFrameSchema).readonly(),
15933
+ refused: array(object({
15934
+ sourceMediaKey: string(),
15935
+ reason: RetrainCopyRefusalSchema
15936
+ })).readonly()
15937
+ });
15938
+ var RetrainFrameListSchema = object({
15939
+ candidates: array(RetrainFrameCandidateSchema).readonly(),
15940
+ copies: array(RetrainFrameSchema).readonly(),
15941
+ /** What the page pre-selects — the native key frame when one survives. */
15942
+ autoPickMediaKey: string().optional()
15943
+ });
15944
+ /** What the operator asked the assist to look for. */
15945
+ var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
15946
+ kind: literal("package"),
15947
+ zone: RetrainBboxSchema.optional()
15948
+ }), object({
15949
+ kind: literal("objects"),
15950
+ modelId: string(),
15951
+ minScore: number().optional()
15952
+ })]);
15953
+ /**
15954
+ * The assist's answer — a discriminated union, because "the model saw nothing"
15955
+ * and "this node cannot run that model" lead to different next moves and a
15956
+ * nullable result cannot tell them apart.
15957
+ */
15958
+ var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
15959
+ kind: literal("proposed"),
15960
+ modelId: string(),
15961
+ stepId: string(),
15962
+ minScore: number(),
15963
+ /** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
15964
+ proposals: array(RetrainAnnotationDraftSchema).readonly(),
15965
+ /** Returned by the runner but removed by the threshold. */
15966
+ belowThreshold: number().int()
15967
+ }), object({
15968
+ kind: literal("refused"),
15969
+ /** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
15970
+ reason: string(),
15971
+ detail: string().optional()
15972
+ })]);
15973
+ /** The outcome of a lifecycle move owned by the retrain page. */
15974
+ var RetrainTransitionResultSchema = object({
15975
+ trackId: string(),
15976
+ /** Where the track ended up, whatever happened. */
15977
+ retrainStatus: RetrainStatusSchema,
15978
+ /** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
15979
+ changed: boolean(),
15980
+ reason: _enum([
15981
+ "unknown-track",
15982
+ "no-frames-copied",
15983
+ "not-staging",
15984
+ "not-trained",
15985
+ "unchanged"
15986
+ ]).optional()
15987
+ });
15767
15988
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
15768
15989
  var MAX_EVENT_QUERY_LIMIT = 5e3;
15769
15990
  var DeviceEventQueryInput = object({
@@ -15818,7 +16039,7 @@ var KeyEventSchema = object({
15818
16039
  /** Track start time (firstSeen). */
15819
16040
  timestamp: number(),
15820
16041
  className: string(),
15821
- label: string().optional(),
16042
+ ...TieredLabelFields,
15822
16043
  importance: number(),
15823
16044
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15824
16045
  bestEventId: string(),
@@ -16092,6 +16313,79 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16092
16313
  }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16093
16314
  kind: "query",
16094
16315
  auth: "admin"
16316
+ }), method(object({
16317
+ /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
16318
+ * `deviceId`, deliberately: `deviceId` would make this device-bound and
16319
+ * route it at one camera's owner, and "every camera" would stop being
16320
+ * expressible at all. */
16321
+ deviceIds: array(number()).optional(),
16322
+ limit: number().int().min(1).max(500).optional()
16323
+ }), array(RetrainTrackSchema).readonly(), {
16324
+ kind: "query",
16325
+ auth: "admin"
16326
+ }), method(object({ trackId: string() }), RetrainFrameListSchema, {
16327
+ kind: "query",
16328
+ auth: "admin"
16329
+ }), method(object({
16330
+ deviceId: number(),
16331
+ trackId: string(),
16332
+ mediaKeys: array(string()).min(1)
16333
+ }), RetrainFrameSelectionSchema, {
16334
+ kind: "mutation",
16335
+ auth: "admin"
16336
+ }), method(object({
16337
+ deviceId: number(),
16338
+ trackId: string(),
16339
+ frameId: string()
16340
+ }), object({
16341
+ removed: boolean(),
16342
+ removedAnnotations: number().int()
16343
+ }), {
16344
+ kind: "mutation",
16345
+ auth: "admin"
16346
+ }), method(object({ frameId: string() }), object({
16347
+ base64: string(),
16348
+ width: number().int(),
16349
+ height: number().int()
16350
+ }), {
16351
+ kind: "query",
16352
+ auth: "admin"
16353
+ }), method(object({
16354
+ deviceId: number(),
16355
+ trackId: string(),
16356
+ frameId: string(),
16357
+ subject: RetrainAssistSubjectSchema,
16358
+ /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
16359
+ nodeId: string().optional()
16360
+ }), RetrainAssistResultSchema, {
16361
+ kind: "mutation",
16362
+ auth: "admin"
16363
+ }), method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
16364
+ kind: "query",
16365
+ auth: "admin"
16366
+ }), method(object({
16367
+ deviceId: number(),
16368
+ trackId: string(),
16369
+ frameId: string(),
16370
+ annotations: array(RetrainAnnotationDraftSchema)
16371
+ }), array(RetrainAnnotationSchema).readonly(), {
16372
+ kind: "mutation",
16373
+ auth: "admin"
16374
+ }), method(object({
16375
+ deviceId: number(),
16376
+ trackId: string()
16377
+ }), RetrainTransitionResultSchema, {
16378
+ kind: "mutation",
16379
+ auth: "admin"
16380
+ }), method(object({
16381
+ deviceId: number(),
16382
+ trackId: string()
16383
+ }), RetrainTransitionResultSchema, {
16384
+ kind: "mutation",
16385
+ auth: "admin"
16386
+ }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16387
+ kind: "query",
16388
+ auth: "admin"
16095
16389
  }), method(object({
16096
16390
  eventId: string(),
16097
16391
  kind: MediaFileKindEnum.optional()
@@ -16671,6 +16965,22 @@ var DetailResultSchema = object({
16671
16965
  bbox: NativeCropBboxSchema.optional(),
16672
16966
  embedding: string().optional(),
16673
16967
  label: string().optional(),
16968
+ /**
16969
+ * The tier `label` occupies, copied VERBATIM from the producing step's
16970
+ * `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
16971
+ *
16972
+ * It rides the wire rather than being resolved by the consumer because the
16973
+ * declaration lives with the step definition, which only the executing node
16974
+ * has: post-analysis holds no step registry, and re-deriving the tier from
16975
+ * `className` there would be exactly the inference this model exists to
16976
+ * forbid. A `label` that arrives WITHOUT this field is refused by the write
16977
+ * rule and logged (`label tier undeclared`) — an older runner therefore
16978
+ * stops enriching rather than guessing, which is why addon-pipeline is
16979
+ * deployed BEFORE addon-post-analysis.
16980
+ */
16981
+ labelTier: union([literal(1), literal(2)]).optional(),
16982
+ /** Model that produced `label` — carried into the tier's attribution. */
16983
+ labelModelId: string().optional(),
16674
16984
  alignedCropJpeg: string().optional(),
16675
16985
  /** Face short side (px) measured on the NATIVE crop surface. The `bbox`
16676
16986
  * above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
@@ -26988,6 +27298,12 @@ Object.freeze({
26988
27298
  addonId: null,
26989
27299
  access: "delete"
26990
27300
  },
27301
+ "pipelineAnalytics.completeRetrainTrack": {
27302
+ capName: "pipeline-analytics",
27303
+ capScope: "device",
27304
+ addonId: null,
27305
+ access: "create"
27306
+ },
26991
27307
  "pipelineAnalytics.deleteDeviceEvents": {
26992
27308
  capName: "pipeline-analytics",
26993
27309
  capScope: "device",
@@ -27000,6 +27316,12 @@ Object.freeze({
27000
27316
  addonId: null,
27001
27317
  access: "delete"
27002
27318
  },
27319
+ "pipelineAnalytics.deselectRetrainFrame": {
27320
+ capName: "pipeline-analytics",
27321
+ capScope: "device",
27322
+ addonId: null,
27323
+ access: "create"
27324
+ },
27003
27325
  "pipelineAnalytics.getActiveTracks": {
27004
27326
  capName: "pipeline-analytics",
27005
27327
  capScope: "device",
@@ -27060,6 +27382,18 @@ Object.freeze({
27060
27382
  addonId: null,
27061
27383
  access: "view"
27062
27384
  },
27385
+ "pipelineAnalytics.getRetrainExportUrl": {
27386
+ capName: "pipeline-analytics",
27387
+ capScope: "device",
27388
+ addonId: null,
27389
+ access: "view"
27390
+ },
27391
+ "pipelineAnalytics.getRetrainFrameImage": {
27392
+ capName: "pipeline-analytics",
27393
+ capScope: "device",
27394
+ addonId: null,
27395
+ access: "view"
27396
+ },
27063
27397
  "pipelineAnalytics.getSensorEvents": {
27064
27398
  capName: "pipeline-analytics",
27065
27399
  capScope: "device",
@@ -27114,6 +27448,24 @@ Object.freeze({
27114
27448
  addonId: null,
27115
27449
  access: "view"
27116
27450
  },
27451
+ "pipelineAnalytics.listRetrainAnnotations": {
27452
+ capName: "pipeline-analytics",
27453
+ capScope: "device",
27454
+ addonId: null,
27455
+ access: "view"
27456
+ },
27457
+ "pipelineAnalytics.listRetrainFrames": {
27458
+ capName: "pipeline-analytics",
27459
+ capScope: "device",
27460
+ addonId: null,
27461
+ access: "view"
27462
+ },
27463
+ "pipelineAnalytics.listRetrainStaging": {
27464
+ capName: "pipeline-analytics",
27465
+ capScope: "device",
27466
+ addonId: null,
27467
+ access: "view"
27468
+ },
27117
27469
  "pipelineAnalytics.listTrackMedia": {
27118
27470
  capName: "pipeline-analytics",
27119
27471
  capScope: "device",
@@ -27126,6 +27478,12 @@ Object.freeze({
27126
27478
  addonId: null,
27127
27479
  access: "view"
27128
27480
  },
27481
+ "pipelineAnalytics.proposeRetrainAnnotations": {
27482
+ capName: "pipeline-analytics",
27483
+ capScope: "device",
27484
+ addonId: null,
27485
+ access: "create"
27486
+ },
27129
27487
  "pipelineAnalytics.pruneEvents": {
27130
27488
  capName: "pipeline-analytics",
27131
27489
  capScope: "device",
@@ -27156,12 +27514,30 @@ Object.freeze({
27156
27514
  addonId: null,
27157
27515
  access: "create"
27158
27516
  },
27517
+ "pipelineAnalytics.restageRetrainTrack": {
27518
+ capName: "pipeline-analytics",
27519
+ capScope: "device",
27520
+ addonId: null,
27521
+ access: "create"
27522
+ },
27523
+ "pipelineAnalytics.saveRetrainAnnotations": {
27524
+ capName: "pipeline-analytics",
27525
+ capScope: "device",
27526
+ addonId: null,
27527
+ access: "create"
27528
+ },
27159
27529
  "pipelineAnalytics.searchObjectEvents": {
27160
27530
  capName: "pipeline-analytics",
27161
27531
  capScope: "device",
27162
27532
  addonId: null,
27163
27533
  access: "view"
27164
27534
  },
27535
+ "pipelineAnalytics.selectRetrainFrames": {
27536
+ capName: "pipeline-analytics",
27537
+ capScope: "device",
27538
+ addonId: null,
27539
+ access: "create"
27540
+ },
27165
27541
  "pipelineAnalytics.setTrackFlags": {
27166
27542
  capName: "pipeline-analytics",
27167
27543
  capScope: "device",
@@ -45421,8 +45797,21 @@ function syncStateToJson(map) {
45421
45797
  */
45422
45798
  var DEFAULT_DEVICE_SETTINGS = {
45423
45799
  streamPreference: "auto",
45424
- hksvRecording: false
45800
+ hksvRecording: true
45425
45801
  };
45802
+ /**
45803
+ * ON unless explicitly switched off — operator decision 2026-08-08 (flipped
45804
+ * from the launch default of off). ABSENT must resolve to ON or the flip is a
45805
+ * lie for every entry persisted before the field existed, so every read goes
45806
+ * through this one resolver (`!== false`), never a scattered `=== true`. The
45807
+ * cost that made off-by-default look prudent is measured and small on the only
45808
+ * branch the recorder accepts (copy: 0.7 % of a core / ~30 MB RSS, D84), and a
45809
+ * camera the recorder cannot copy refuses recording with a logged reason
45810
+ * rather than paying for a transcode.
45811
+ */
45812
+ function resolveHksvRecording(settings) {
45813
+ return settings?.hksvRecording !== false;
45814
+ }
45426
45815
  var HAP_STREAM_PREFERENCE_OPTIONS = [
45427
45816
  {
45428
45817
  value: "auto",
@@ -45699,7 +46088,7 @@ var ExportHapAddon = class extends BaseAddon {
45699
46088
  decodeMemos: this.decodeMemos,
45700
46089
  hapDeviceSettings: {
45701
46090
  streamPreference: entrySettings.streamPreference ?? "auto",
45702
- hksvRecording: entrySettings.hksvRecording === true
46091
+ hksvRecording: resolveHksvRecording(entrySettings)
45703
46092
  }
45704
46093
  }
45705
46094
  });
@@ -46050,7 +46439,7 @@ var ExportHapAddon = class extends BaseAddon {
46050
46439
  label: "HomeKit recording (Secure Video)",
46051
46440
  description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
46052
46441
  style: "switch",
46053
- value: settings.hksvRecording === true,
46442
+ value: resolveHksvRecording(settings),
46054
46443
  showWhen: {
46055
46444
  field: enabledKey,
46056
46445
  equals: true
@@ -46084,7 +46473,7 @@ var ExportHapAddon = class extends BaseAddon {
46084
46473
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
46085
46474
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
46086
46475
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
46087
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : current?.settings?.hksvRecording === true;
46476
+ const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
46088
46477
  const nextSettings = {
46089
46478
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
46090
46479
  streamPreference,
@@ -46100,7 +46489,7 @@ var ExportHapAddon = class extends BaseAddon {
46100
46489
  return { success: true };
46101
46490
  }
46102
46491
  const currentPref = current?.settings?.streamPreference ?? "auto";
46103
- const currentHksv = current?.settings?.hksvRecording === true;
46492
+ const currentHksv = resolveHksvRecording(current?.settings);
46104
46493
  await this.updateEntrySettings(deviceIdStr, nextSettings);
46105
46494
  if (currentPref !== streamPreference || currentHksv !== hksvRecording) {
46106
46495
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
@@ -46160,4 +46549,5 @@ exports.default = ExportHapAddon;
46160
46549
  exports.deriveUsername = deriveUsername;
46161
46550
  exports.initHapStorage = initHapStorage;
46162
46551
  exports.publishStandalone = publishStandalone;
46552
+ exports.resolveHksvRecording = resolveHksvRecording;
46163
46553
  exports.unpublishAccessory = unpublishAccessory;
@@ -15520,6 +15520,60 @@ var TrackFlagsSchema = object({
15520
15520
  * `trained` without a re-fetch. */
15521
15521
  retrainStatus: RetrainStatusSchema
15522
15522
  });
15523
+ union([literal(1), literal(2)]);
15524
+ /**
15525
+ * WHO decided a label, and when. Carried per tier so a value can be traced to
15526
+ * the step and model that produced it — which is what makes the write rule
15527
+ * arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
15528
+ * and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
15529
+ *
15530
+ * `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
15531
+ * `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
15532
+ * `migration:4g` for a value the 4g migration moved from the single-slot era —
15533
+ * that value has no provenance, and the write rule lets ANY properly-attributed
15534
+ * write of the same tier replace it regardless of score.
15535
+ */
15536
+ var LabelAttributionSchema = object({
15537
+ stepId: string(),
15538
+ modelId: string().optional(),
15539
+ decidedAt: number()
15540
+ });
15541
+ /**
15542
+ * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
15543
+ * `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
15544
+ * track and its events always answer the same question the same way.
15545
+ *
15546
+ * Two scalar columns, not an array: every consumer wants "the coarse one" or
15547
+ * "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
15548
+ * is tier 2, and each carries its own score + attribution.
15549
+ *
15550
+ * **Reading it.** What a human should be shown is `subLabel ?? label` — the
15551
+ * finest thing known. Before 4g the single `label` column held the finest
15552
+ * value, so a consumer that has not been updated reads the tier-1 slot and
15553
+ * shows nothing on a species-only row; that is why the migration puts every
15554
+ * pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
15555
+ * and why the read surfaces were changed in the same train.
15556
+ *
15557
+ * **Writing it.** The slots are independent, which is the whole point: a
15558
+ * tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
15559
+ * migratorius`), so fineness cannot regress by construction. Within a tier the
15560
+ * higher score wins. One rule, one implementation — see
15561
+ * `pipeline/label-tier.ts` in addon-post-analysis.
15562
+ */
15563
+ var TieredLabelFields = {
15564
+ /** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
15565
+ label: string().optional(),
15566
+ /** Confidence of the tier-1 value, as reported by the deciding step. */
15567
+ labelScore: number().optional(),
15568
+ /** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
15569
+ labelMeta: LabelAttributionSchema.optional(),
15570
+ /** Tier 2 — the instance. See {@link LabelTierSchema}. */
15571
+ subLabel: string().optional(),
15572
+ /** Confidence of the tier-2 value, as reported by the deciding step. */
15573
+ subLabelScore: number().optional(),
15574
+ /** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
15575
+ subLabelMeta: LabelAttributionSchema.optional()
15576
+ };
15523
15577
  /** Per-camera slice of a training-export estimate. */
15524
15578
  var TrainingExportDeviceTotalsSchema = object({
15525
15579
  deviceId: number(),
@@ -15544,7 +15598,7 @@ var TrackSchema = object({
15544
15598
  trackId: string(),
15545
15599
  deviceId: number(),
15546
15600
  className: string(),
15547
- label: string().optional(),
15601
+ ...TieredLabelFields,
15548
15602
  producingDeviceName: string().optional(),
15549
15603
  /** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
15550
15604
  source: TrackSourceSchema.optional(),
@@ -15657,7 +15711,7 @@ var ObjectEventSchema = object({
15657
15711
  /** Omitted in slim projection. */
15658
15712
  trackId: string().optional(),
15659
15713
  className: string(),
15660
- label: string().optional(),
15714
+ ...TieredLabelFields,
15661
15715
  /** Omitted in slim projection. */
15662
15716
  confidence: number().optional(),
15663
15717
  /** Heavy JSON — omitted in slim projection. */
@@ -15738,6 +15792,173 @@ var MediaFileSchema = object({
15738
15792
  * stored blob and a `?variant=thumb` rendering without fetching either.
15739
15793
  */
15740
15794
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
15795
+ /**
15796
+ * The MACRO tier of an annotation — a CLOSED set.
15797
+ *
15798
+ * This is what the exported detector predicts, so a typo here is a new class
15799
+ * with one example in it. `label` and `subLabel` are open strings by contrast:
15800
+ * the whole point of the page is teaching the model things it does not know
15801
+ * yet, and constraining that vocabulary would make it useless.
15802
+ *
15803
+ * A macro class is NEVER a label. The provider refuses a write whose `label` or
15804
+ * `subLabel` is one of these values, in any casing, because once `person`
15805
+ * exists in both tiers "every person box" stops being answerable without
15806
+ * knowing every string anyone ever typed — and the damage is retroactive.
15807
+ */
15808
+ var RetrainMacroClassSchema = _enum([
15809
+ "person",
15810
+ "vehicle",
15811
+ "animal",
15812
+ "package",
15813
+ "face",
15814
+ "plate"
15815
+ ]);
15816
+ /** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
15817
+ var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
15818
+ /** Did a human draw this box, or did the assist propose it? */
15819
+ var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
15820
+ /** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
15821
+ var RetrainBboxSchema = object({
15822
+ x: number(),
15823
+ y: number(),
15824
+ w: number(),
15825
+ h: number()
15826
+ });
15827
+ /**
15828
+ * One annotated subject.
15829
+ *
15830
+ * `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
15831
+ * (letterboxed root / zone-cropped package / subject-cropped classifier) are
15832
+ * derived from it at export and never stored — storing them is how one feature
15833
+ * space ends up holding two crops of the same subject (D52).
15834
+ */
15835
+ var RetrainAnnotationSchema = object({
15836
+ id: string(),
15837
+ trackId: string(),
15838
+ deviceId: number(),
15839
+ /** The COPY in retrain storage — never the source track's media key. */
15840
+ mediaKey: string(),
15841
+ bbox: RetrainBboxSchema,
15842
+ macroClass: RetrainMacroClassSchema,
15843
+ label: string().optional(),
15844
+ subLabel: string().optional(),
15845
+ kind: RetrainAnnotationKindSchema,
15846
+ source: RetrainAnnotationSourceSchema,
15847
+ /** Which model proposed this box — or, on a `model_error`, drew the phantom. */
15848
+ assistModelId: string().optional(),
15849
+ assistScore: number().optional(),
15850
+ exportedInBatch: string().optional(),
15851
+ createdAt: number()
15852
+ });
15853
+ /** The write form — the server owns `id`, `createdAt` and the frame binding. */
15854
+ var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
15855
+ id: true,
15856
+ trackId: true,
15857
+ deviceId: true,
15858
+ mediaKey: true,
15859
+ createdAt: true,
15860
+ exportedInBatch: true
15861
+ });
15862
+ /** A track sitting in `staging`, with everything the worklist needs to rank it. */
15863
+ var RetrainTrackSchema = object({
15864
+ trackId: string(),
15865
+ deviceId: number(),
15866
+ className: string(),
15867
+ label: string().optional(),
15868
+ firstSeen: number(),
15869
+ lastSeen: number(),
15870
+ /** How many frames the dataset already holds from this track. */
15871
+ frameCount: number().int(),
15872
+ /** How many subjects have been annotated on those frames. `0` with
15873
+ * `frameCount: 0` is exactly "staging, still to work". */
15874
+ annotationCount: number().int()
15875
+ });
15876
+ /** A frame the picker may offer — an index row, no blob was read to produce it. */
15877
+ var RetrainFrameCandidateSchema = object({
15878
+ mediaKey: string(),
15879
+ kind: MediaFileKindEnum,
15880
+ timestamp: number(),
15881
+ sizeBytes: number().int(),
15882
+ /** A copy of this original already exists — selecting it is free and cannot
15883
+ * fail, whatever became of the original. */
15884
+ copied: boolean()
15885
+ });
15886
+ /** A frame the dataset OWNS: bytes copied at selection time. */
15887
+ var RetrainFrameSchema = object({
15888
+ frameId: string(),
15889
+ deviceId: number(),
15890
+ trackId: string(),
15891
+ /** Provenance only. It may already point at nothing — that is expected. */
15892
+ sourceMediaKey: string(),
15893
+ sourceKind: MediaFileKindEnum,
15894
+ sizeBytes: number().int(),
15895
+ width: number().int(),
15896
+ height: number().int(),
15897
+ copiedAt: number()
15898
+ });
15899
+ /** Why a copy-on-select could not be honoured — named, never a silent skip. */
15900
+ var RetrainCopyRefusalSchema = _enum([
15901
+ "source-missing",
15902
+ "unreadable-image",
15903
+ "write-failed"
15904
+ ]);
15905
+ var RetrainFrameSelectionSchema = object({
15906
+ copied: array(RetrainFrameSchema).readonly(),
15907
+ refused: array(object({
15908
+ sourceMediaKey: string(),
15909
+ reason: RetrainCopyRefusalSchema
15910
+ })).readonly()
15911
+ });
15912
+ var RetrainFrameListSchema = object({
15913
+ candidates: array(RetrainFrameCandidateSchema).readonly(),
15914
+ copies: array(RetrainFrameSchema).readonly(),
15915
+ /** What the page pre-selects — the native key frame when one survives. */
15916
+ autoPickMediaKey: string().optional()
15917
+ });
15918
+ /** What the operator asked the assist to look for. */
15919
+ var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
15920
+ kind: literal("package"),
15921
+ zone: RetrainBboxSchema.optional()
15922
+ }), object({
15923
+ kind: literal("objects"),
15924
+ modelId: string(),
15925
+ minScore: number().optional()
15926
+ })]);
15927
+ /**
15928
+ * The assist's answer — a discriminated union, because "the model saw nothing"
15929
+ * and "this node cannot run that model" lead to different next moves and a
15930
+ * nullable result cannot tell them apart.
15931
+ */
15932
+ var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
15933
+ kind: literal("proposed"),
15934
+ modelId: string(),
15935
+ stepId: string(),
15936
+ minScore: number(),
15937
+ /** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
15938
+ proposals: array(RetrainAnnotationDraftSchema).readonly(),
15939
+ /** Returned by the runner but removed by the threshold. */
15940
+ belowThreshold: number().int()
15941
+ }), object({
15942
+ kind: literal("refused"),
15943
+ /** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
15944
+ reason: string(),
15945
+ detail: string().optional()
15946
+ })]);
15947
+ /** The outcome of a lifecycle move owned by the retrain page. */
15948
+ var RetrainTransitionResultSchema = object({
15949
+ trackId: string(),
15950
+ /** Where the track ended up, whatever happened. */
15951
+ retrainStatus: RetrainStatusSchema,
15952
+ /** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
15953
+ changed: boolean(),
15954
+ reason: _enum([
15955
+ "unknown-track",
15956
+ "no-frames-copied",
15957
+ "not-staging",
15958
+ "not-trained",
15959
+ "unchanged"
15960
+ ]).optional()
15961
+ });
15741
15962
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
15742
15963
  var MAX_EVENT_QUERY_LIMIT = 5e3;
15743
15964
  var DeviceEventQueryInput = object({
@@ -15792,7 +16013,7 @@ var KeyEventSchema = object({
15792
16013
  /** Track start time (firstSeen). */
15793
16014
  timestamp: number(),
15794
16015
  className: string(),
15795
- label: string().optional(),
16016
+ ...TieredLabelFields,
15796
16017
  importance: number(),
15797
16018
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15798
16019
  bestEventId: string(),
@@ -16066,6 +16287,79 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16066
16287
  }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16067
16288
  kind: "query",
16068
16289
  auth: "admin"
16290
+ }), method(object({
16291
+ /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
16292
+ * `deviceId`, deliberately: `deviceId` would make this device-bound and
16293
+ * route it at one camera's owner, and "every camera" would stop being
16294
+ * expressible at all. */
16295
+ deviceIds: array(number()).optional(),
16296
+ limit: number().int().min(1).max(500).optional()
16297
+ }), array(RetrainTrackSchema).readonly(), {
16298
+ kind: "query",
16299
+ auth: "admin"
16300
+ }), method(object({ trackId: string() }), RetrainFrameListSchema, {
16301
+ kind: "query",
16302
+ auth: "admin"
16303
+ }), method(object({
16304
+ deviceId: number(),
16305
+ trackId: string(),
16306
+ mediaKeys: array(string()).min(1)
16307
+ }), RetrainFrameSelectionSchema, {
16308
+ kind: "mutation",
16309
+ auth: "admin"
16310
+ }), method(object({
16311
+ deviceId: number(),
16312
+ trackId: string(),
16313
+ frameId: string()
16314
+ }), object({
16315
+ removed: boolean(),
16316
+ removedAnnotations: number().int()
16317
+ }), {
16318
+ kind: "mutation",
16319
+ auth: "admin"
16320
+ }), method(object({ frameId: string() }), object({
16321
+ base64: string(),
16322
+ width: number().int(),
16323
+ height: number().int()
16324
+ }), {
16325
+ kind: "query",
16326
+ auth: "admin"
16327
+ }), method(object({
16328
+ deviceId: number(),
16329
+ trackId: string(),
16330
+ frameId: string(),
16331
+ subject: RetrainAssistSubjectSchema,
16332
+ /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
16333
+ nodeId: string().optional()
16334
+ }), RetrainAssistResultSchema, {
16335
+ kind: "mutation",
16336
+ auth: "admin"
16337
+ }), method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
16338
+ kind: "query",
16339
+ auth: "admin"
16340
+ }), method(object({
16341
+ deviceId: number(),
16342
+ trackId: string(),
16343
+ frameId: string(),
16344
+ annotations: array(RetrainAnnotationDraftSchema)
16345
+ }), array(RetrainAnnotationSchema).readonly(), {
16346
+ kind: "mutation",
16347
+ auth: "admin"
16348
+ }), method(object({
16349
+ deviceId: number(),
16350
+ trackId: string()
16351
+ }), RetrainTransitionResultSchema, {
16352
+ kind: "mutation",
16353
+ auth: "admin"
16354
+ }), method(object({
16355
+ deviceId: number(),
16356
+ trackId: string()
16357
+ }), RetrainTransitionResultSchema, {
16358
+ kind: "mutation",
16359
+ auth: "admin"
16360
+ }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16361
+ kind: "query",
16362
+ auth: "admin"
16069
16363
  }), method(object({
16070
16364
  eventId: string(),
16071
16365
  kind: MediaFileKindEnum.optional()
@@ -16645,6 +16939,22 @@ var DetailResultSchema = object({
16645
16939
  bbox: NativeCropBboxSchema.optional(),
16646
16940
  embedding: string().optional(),
16647
16941
  label: string().optional(),
16942
+ /**
16943
+ * The tier `label` occupies, copied VERBATIM from the producing step's
16944
+ * `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
16945
+ *
16946
+ * It rides the wire rather than being resolved by the consumer because the
16947
+ * declaration lives with the step definition, which only the executing node
16948
+ * has: post-analysis holds no step registry, and re-deriving the tier from
16949
+ * `className` there would be exactly the inference this model exists to
16950
+ * forbid. A `label` that arrives WITHOUT this field is refused by the write
16951
+ * rule and logged (`label tier undeclared`) — an older runner therefore
16952
+ * stops enriching rather than guessing, which is why addon-pipeline is
16953
+ * deployed BEFORE addon-post-analysis.
16954
+ */
16955
+ labelTier: union([literal(1), literal(2)]).optional(),
16956
+ /** Model that produced `label` — carried into the tier's attribution. */
16957
+ labelModelId: string().optional(),
16648
16958
  alignedCropJpeg: string().optional(),
16649
16959
  /** Face short side (px) measured on the NATIVE crop surface. The `bbox`
16650
16960
  * above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
@@ -26962,6 +27272,12 @@ Object.freeze({
26962
27272
  addonId: null,
26963
27273
  access: "delete"
26964
27274
  },
27275
+ "pipelineAnalytics.completeRetrainTrack": {
27276
+ capName: "pipeline-analytics",
27277
+ capScope: "device",
27278
+ addonId: null,
27279
+ access: "create"
27280
+ },
26965
27281
  "pipelineAnalytics.deleteDeviceEvents": {
26966
27282
  capName: "pipeline-analytics",
26967
27283
  capScope: "device",
@@ -26974,6 +27290,12 @@ Object.freeze({
26974
27290
  addonId: null,
26975
27291
  access: "delete"
26976
27292
  },
27293
+ "pipelineAnalytics.deselectRetrainFrame": {
27294
+ capName: "pipeline-analytics",
27295
+ capScope: "device",
27296
+ addonId: null,
27297
+ access: "create"
27298
+ },
26977
27299
  "pipelineAnalytics.getActiveTracks": {
26978
27300
  capName: "pipeline-analytics",
26979
27301
  capScope: "device",
@@ -27034,6 +27356,18 @@ Object.freeze({
27034
27356
  addonId: null,
27035
27357
  access: "view"
27036
27358
  },
27359
+ "pipelineAnalytics.getRetrainExportUrl": {
27360
+ capName: "pipeline-analytics",
27361
+ capScope: "device",
27362
+ addonId: null,
27363
+ access: "view"
27364
+ },
27365
+ "pipelineAnalytics.getRetrainFrameImage": {
27366
+ capName: "pipeline-analytics",
27367
+ capScope: "device",
27368
+ addonId: null,
27369
+ access: "view"
27370
+ },
27037
27371
  "pipelineAnalytics.getSensorEvents": {
27038
27372
  capName: "pipeline-analytics",
27039
27373
  capScope: "device",
@@ -27088,6 +27422,24 @@ Object.freeze({
27088
27422
  addonId: null,
27089
27423
  access: "view"
27090
27424
  },
27425
+ "pipelineAnalytics.listRetrainAnnotations": {
27426
+ capName: "pipeline-analytics",
27427
+ capScope: "device",
27428
+ addonId: null,
27429
+ access: "view"
27430
+ },
27431
+ "pipelineAnalytics.listRetrainFrames": {
27432
+ capName: "pipeline-analytics",
27433
+ capScope: "device",
27434
+ addonId: null,
27435
+ access: "view"
27436
+ },
27437
+ "pipelineAnalytics.listRetrainStaging": {
27438
+ capName: "pipeline-analytics",
27439
+ capScope: "device",
27440
+ addonId: null,
27441
+ access: "view"
27442
+ },
27091
27443
  "pipelineAnalytics.listTrackMedia": {
27092
27444
  capName: "pipeline-analytics",
27093
27445
  capScope: "device",
@@ -27100,6 +27452,12 @@ Object.freeze({
27100
27452
  addonId: null,
27101
27453
  access: "view"
27102
27454
  },
27455
+ "pipelineAnalytics.proposeRetrainAnnotations": {
27456
+ capName: "pipeline-analytics",
27457
+ capScope: "device",
27458
+ addonId: null,
27459
+ access: "create"
27460
+ },
27103
27461
  "pipelineAnalytics.pruneEvents": {
27104
27462
  capName: "pipeline-analytics",
27105
27463
  capScope: "device",
@@ -27130,12 +27488,30 @@ Object.freeze({
27130
27488
  addonId: null,
27131
27489
  access: "create"
27132
27490
  },
27491
+ "pipelineAnalytics.restageRetrainTrack": {
27492
+ capName: "pipeline-analytics",
27493
+ capScope: "device",
27494
+ addonId: null,
27495
+ access: "create"
27496
+ },
27497
+ "pipelineAnalytics.saveRetrainAnnotations": {
27498
+ capName: "pipeline-analytics",
27499
+ capScope: "device",
27500
+ addonId: null,
27501
+ access: "create"
27502
+ },
27133
27503
  "pipelineAnalytics.searchObjectEvents": {
27134
27504
  capName: "pipeline-analytics",
27135
27505
  capScope: "device",
27136
27506
  addonId: null,
27137
27507
  access: "view"
27138
27508
  },
27509
+ "pipelineAnalytics.selectRetrainFrames": {
27510
+ capName: "pipeline-analytics",
27511
+ capScope: "device",
27512
+ addonId: null,
27513
+ access: "create"
27514
+ },
27139
27515
  "pipelineAnalytics.setTrackFlags": {
27140
27516
  capName: "pipeline-analytics",
27141
27517
  capScope: "device",
@@ -45395,8 +45771,21 @@ function syncStateToJson(map) {
45395
45771
  */
45396
45772
  var DEFAULT_DEVICE_SETTINGS = {
45397
45773
  streamPreference: "auto",
45398
- hksvRecording: false
45774
+ hksvRecording: true
45399
45775
  };
45776
+ /**
45777
+ * ON unless explicitly switched off — operator decision 2026-08-08 (flipped
45778
+ * from the launch default of off). ABSENT must resolve to ON or the flip is a
45779
+ * lie for every entry persisted before the field existed, so every read goes
45780
+ * through this one resolver (`!== false`), never a scattered `=== true`. The
45781
+ * cost that made off-by-default look prudent is measured and small on the only
45782
+ * branch the recorder accepts (copy: 0.7 % of a core / ~30 MB RSS, D84), and a
45783
+ * camera the recorder cannot copy refuses recording with a logged reason
45784
+ * rather than paying for a transcode.
45785
+ */
45786
+ function resolveHksvRecording(settings) {
45787
+ return settings?.hksvRecording !== false;
45788
+ }
45400
45789
  var HAP_STREAM_PREFERENCE_OPTIONS = [
45401
45790
  {
45402
45791
  value: "auto",
@@ -45673,7 +46062,7 @@ var ExportHapAddon = class extends BaseAddon {
45673
46062
  decodeMemos: this.decodeMemos,
45674
46063
  hapDeviceSettings: {
45675
46064
  streamPreference: entrySettings.streamPreference ?? "auto",
45676
- hksvRecording: entrySettings.hksvRecording === true
46065
+ hksvRecording: resolveHksvRecording(entrySettings)
45677
46066
  }
45678
46067
  }
45679
46068
  });
@@ -46024,7 +46413,7 @@ var ExportHapAddon = class extends BaseAddon {
46024
46413
  label: "HomeKit recording (Secure Video)",
46025
46414
  description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
46026
46415
  style: "switch",
46027
- value: settings.hksvRecording === true,
46416
+ value: resolveHksvRecording(settings),
46028
46417
  showWhen: {
46029
46418
  field: enabledKey,
46030
46419
  equals: true
@@ -46058,7 +46447,7 @@ var ExportHapAddon = class extends BaseAddon {
46058
46447
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
46059
46448
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
46060
46449
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
46061
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : current?.settings?.hksvRecording === true;
46450
+ const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
46062
46451
  const nextSettings = {
46063
46452
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
46064
46453
  streamPreference,
@@ -46074,7 +46463,7 @@ var ExportHapAddon = class extends BaseAddon {
46074
46463
  return { success: true };
46075
46464
  }
46076
46465
  const currentPref = current?.settings?.streamPreference ?? "auto";
46077
- const currentHksv = current?.settings?.hksvRecording === true;
46466
+ const currentHksv = resolveHksvRecording(current?.settings);
46078
46467
  await this.updateEntrySettings(deviceIdStr, nextSettings);
46079
46468
  if (currentPref !== streamPreference || currentHksv !== hksvRecording) {
46080
46469
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
@@ -46129,4 +46518,4 @@ function errMsg(err) {
46129
46518
  return err instanceof Error ? err.message : String(err);
46130
46519
  }
46131
46520
  //#endregion
46132
- export { ExportHapAddon, ExportHapAddon as default, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, deriveUsername as t };
46521
+ export { ExportHapAddon, ExportHapAddon as default, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, resolveHksvRecording, deriveUsername as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.15",
3
+ "version": "1.2.16",
4
4
  "description": "HomeKit (HAP) bridge exporter for CamStack devices. Publishes a bridged accessory per exposed device — MotionSensor in this MVP; Camera/Doorbell/Switch/Light follow.",
5
5
  "keywords": [
6
6
  "camstack",