@camstack/addon-provider-amcrest 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +289 -71
  2. package/dist/addon.mjs +289 -71
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7559,7 +7559,7 @@ var RecordingBandSchema = object({
7559
7559
  postBufferSec: number().min(0).optional()
7560
7560
  });
7561
7561
  ({
7562
- preBufferSec: 10,
7562
+ preBufferSec: 15,
7563
7563
  postBufferSec: 30
7564
7564
  }).postBufferSec * 1e3;
7565
7565
  /**
@@ -13762,128 +13762,103 @@ var COCO_TO_MACRO = {
13762
13762
  var AUDIO_MACRO_LABELS = [
13763
13763
  {
13764
13764
  id: "speech",
13765
- name: "Speech",
13766
- icon: "🗣️"
13765
+ name: "Speech"
13767
13766
  },
13768
13767
  {
13769
13768
  id: "scream",
13770
- name: "Scream / Shout",
13771
- icon: "😱"
13769
+ name: "Scream / Shout"
13772
13770
  },
13773
13771
  {
13774
13772
  id: "crying",
13775
- name: "Crying / Baby",
13776
- icon: "😢"
13773
+ name: "Crying / Baby"
13777
13774
  },
13778
13775
  {
13779
13776
  id: "laughter",
13780
- name: "Laughter",
13781
- icon: "😂"
13777
+ name: "Laughter"
13782
13778
  },
13783
13779
  {
13784
13780
  id: "music",
13785
- name: "Music",
13786
- icon: "🎵"
13781
+ name: "Music"
13787
13782
  },
13788
13783
  {
13789
13784
  id: "dog",
13790
- name: "Dog",
13791
- icon: "🐕"
13785
+ name: "Dog"
13792
13786
  },
13793
13787
  {
13794
13788
  id: "cat",
13795
- name: "Cat",
13796
- icon: "🐈"
13789
+ name: "Cat"
13797
13790
  },
13798
13791
  {
13799
13792
  id: "bird",
13800
- name: "Bird",
13801
- icon: "🐦"
13793
+ name: "Bird"
13802
13794
  },
13803
13795
  {
13804
13796
  id: "animal",
13805
- name: "Animal (other)",
13806
- icon: "🐾"
13797
+ name: "Animal (other)"
13807
13798
  },
13808
13799
  {
13809
13800
  id: "alarm",
13810
- name: "Alarm / Siren",
13811
- icon: "🚨"
13801
+ name: "Alarm / Siren"
13812
13802
  },
13813
13803
  {
13814
13804
  id: "doorbell",
13815
- name: "Doorbell / Knock",
13816
- icon: "🔔"
13805
+ name: "Doorbell / Knock"
13817
13806
  },
13818
13807
  {
13819
13808
  id: "glass_breaking",
13820
- name: "Glass Breaking",
13821
- icon: "💥"
13809
+ name: "Glass Breaking"
13822
13810
  },
13823
13811
  {
13824
13812
  id: "gunshot",
13825
- name: "Gunshot / Explosion",
13826
- icon: "💣"
13813
+ name: "Gunshot / Explosion"
13827
13814
  },
13828
13815
  {
13829
13816
  id: "vehicle",
13830
- name: "Vehicle",
13831
- icon: "🚗"
13817
+ name: "Vehicle"
13832
13818
  },
13833
13819
  {
13834
13820
  id: "siren",
13835
- name: "Emergency Siren",
13836
- icon: "🚑"
13821
+ name: "Emergency Siren"
13837
13822
  },
13838
13823
  {
13839
13824
  id: "fire",
13840
- name: "Fire / Smoke",
13841
- icon: "🔥"
13825
+ name: "Fire / Smoke"
13842
13826
  },
13843
13827
  {
13844
13828
  id: "water",
13845
- name: "Water",
13846
- icon: "💧"
13829
+ name: "Water"
13847
13830
  },
13848
13831
  {
13849
13832
  id: "wind",
13850
- name: "Wind / Weather",
13851
- icon: "🌬️"
13833
+ name: "Wind / Weather"
13852
13834
  },
13853
13835
  {
13854
13836
  id: "door",
13855
- name: "Door",
13856
- icon: "🚪"
13837
+ name: "Door"
13857
13838
  },
13858
13839
  {
13859
13840
  id: "footsteps",
13860
- name: "Footsteps",
13861
- icon: "👣"
13841
+ name: "Footsteps"
13862
13842
  },
13863
13843
  {
13864
13844
  id: "crowd",
13865
- name: "Crowd / Chatter",
13866
- icon: "👥"
13845
+ name: "Crowd / Chatter"
13867
13846
  },
13868
13847
  {
13869
13848
  id: "telephone",
13870
- name: "Telephone",
13871
- icon: "📞"
13849
+ name: "Telephone"
13872
13850
  },
13873
13851
  {
13874
13852
  id: "engine",
13875
- name: "Engine / Motor",
13876
- icon: "⚙️"
13853
+ name: "Engine / Motor"
13877
13854
  },
13878
13855
  {
13879
13856
  id: "tools",
13880
- name: "Tools / Construction",
13881
- icon: "🔨"
13857
+ name: "Tools / Construction"
13882
13858
  },
13883
13859
  {
13884
13860
  id: "silence",
13885
- name: "Silence",
13886
- icon: "🤫"
13861
+ name: "Silence"
13887
13862
  }
13888
13863
  ];
13889
13864
  var YAMNET_TO_MACRO = {
@@ -16385,7 +16360,7 @@ var TrackEnvelopeSchema = object({
16385
16360
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16386
16361
  * keeps every scalar the list surfaces actually render (ids, class(es),
16387
16362
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16388
- * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
16363
+ * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
16389
16364
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16390
16365
  * `getTrack`. Mirrors the event-store `projection` convention
16391
16366
  * (`getObjectEvents` et al.).
@@ -16680,6 +16655,35 @@ var TrackSchema = object({
16680
16655
  */
16681
16656
  hasFace: boolean().optional(),
16682
16657
  /**
16658
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
16659
+ * face an operator could ASSIGN to an identity.
16660
+ *
16661
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
16662
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
16663
+ * branch, before every gate, and means no more than "a face detector produced
16664
+ * a face detail". This one is stamped at the single moment the gallery row
16665
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
16666
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
16667
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
16668
+ * the crop-store drop. Everything between the detector and that insert can
16669
+ * legitimately refuse the face, so a flag written any earlier promises the
16670
+ * operator something to assign and delivers nothing.
16671
+ *
16672
+ * **Independent of recognition.** A face collected but never auto-matched is
16673
+ * still assignable — it is in fact the face an operator most wants to reach —
16674
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
16675
+ * `subLabel`; this says only that the raw material exists.
16676
+ *
16677
+ * **Set once, never cleared.** A track that produced a gallery row produced
16678
+ * one; deleting the row later is the gallery's business, not this flag's.
16679
+ *
16680
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
16681
+ * before the column omits it, and so does every server that predates the
16682
+ * field. A consumer must test `=== true` and render nothing otherwise —
16683
+ * never infer "no assignable face".
16684
+ */
16685
+ hasEmbeddedFace: boolean().optional(),
16686
+ /**
16683
16687
  * This subject CONTAINS a folded rider — a person the rider-pairing step
16684
16688
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
16685
16689
  * so the passage is tracked once and as a VEHICLE.
@@ -18069,6 +18073,32 @@ var DetailResultSchema = object({
18069
18073
  /** FRAME-space bbox (already mapped back from crop space). */
18070
18074
  bbox: NativeCropBboxSchema.optional(),
18071
18075
  embedding: string().optional(),
18076
+ /**
18077
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
18078
+ * so an older runner that does not send it degrades to "unmeasurable", which
18079
+ * the consuming gate treats as accept — never as reject.
18080
+ */
18081
+ embeddingMagnitude: number().optional(),
18082
+ /**
18083
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
18084
+ * `label`, and for the same reason: the producing node is the only side that
18085
+ * knows which model actually ran, and re-deriving it downstream is exactly
18086
+ * the inference this model exists to forbid.
18087
+ *
18088
+ * Additive and optional, so a runner that predates it degrades to
18089
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
18090
+ * its previous assumption instead of refusing.
18091
+ *
18092
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
18093
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
18094
+ * with nothing on the wire it took the id from the GALLERY, so the gate
18095
+ * compared the gallery against itself and could never fire. That turned the
18096
+ * one guard protecting the face feature space into a tautology, and it is
18097
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
18098
+ * gallery as the catalog comment predicts — it would have scored the new
18099
+ * model's vectors against the old model's samples in one cosine space.
18100
+ */
18101
+ embeddingModelId: string().optional(),
18072
18102
  label: string().optional(),
18073
18103
  /**
18074
18104
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -18944,6 +18974,12 @@ var CameraStatusSchema = object({
18944
18974
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
18945
18975
  fetchedAt: number()
18946
18976
  });
18977
+ var InferenceDeviceExclusionReasonSchema = _enum([
18978
+ "disabled",
18979
+ "unavailable",
18980
+ "cannot-host-camera-root",
18981
+ "accelerator-preferred"
18982
+ ]);
18947
18983
  var NodeInferenceDeviceSchema = object({
18948
18984
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
18949
18985
  key: string(),
@@ -18974,7 +19010,17 @@ var NodeInferenceDeviceSchema = object({
18974
19010
  * available per format; this is the stored selection that becomes the
18975
19011
  * default for EVERY camera landing on this accelerator.
18976
19012
  */
18977
- steps: record(string(), DeviceStepConfigSchema).optional()
19013
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19014
+ /**
19015
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19016
+ * the reason the dispatcher drops it. Computed by the SAME
19017
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19018
+ * never disagree with the election — deriving it in the UI from
19019
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19020
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19021
+ * "an accelerator is serving" predicate).
19022
+ */
19023
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
18978
19024
  });
18979
19025
  var NodeInferenceDevicesSchema = object({
18980
19026
  nodeId: string(),
@@ -19533,6 +19579,7 @@ var snapshotCapability = {
19533
19579
  isBattery: boolean(),
19534
19580
  reason: _enum([
19535
19581
  "disabled",
19582
+ "offline",
19536
19583
  "sleeping",
19537
19584
  "unreachable",
19538
19585
  "waking"
@@ -19622,9 +19669,17 @@ targets: array(object({
19622
19669
  sleeping: boolean(),
19623
19670
  /** Current device state rendered over the cached frame. State images
19624
19671
  * remain authoritative even when their photographic background is
19625
- * old; null means the link must carry a current camera frame. */
19672
+ * old; null means the link must carry a current camera frame.
19673
+ *
19674
+ * **This is the ONLY source a surface may derive an inactive tile
19675
+ * from.** A client that reads a device's `online`/`disabled` flags and
19676
+ * draws its own verdict will disagree with the pixels it is painting
19677
+ * — the frame comes from here, and so must the sentence over it
19678
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
19679
+ * state. */
19626
19680
  stateReason: _enum([
19627
19681
  "disabled",
19682
+ "offline",
19628
19683
  "sleeping",
19629
19684
  "unreachable",
19630
19685
  "waking"
@@ -23920,7 +23975,12 @@ var ListResultSchema = object({
23920
23975
  probedAt: number()
23921
23976
  });
23922
23977
  var PreferredSchema = LocalInterfaceSchema.nullable();
23923
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23978
+ /**
23979
+ * Candidate base URL for the SDK to race on connect. Order matters —
23980
+ * the SDK should attempt these top-to-bottom with a short per-candidate
23981
+ * timeout (e.g. 1500ms) and cache the winner for the session.
23982
+ */
23983
+ var ConnectionEndpointSchema = object({
23924
23984
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
23925
23985
  label: string(),
23926
23986
  /** Fully-formed base URL with scheme + host + port. */
@@ -23963,7 +24023,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23963
24023
  * ordering between polls.
23964
24024
  */
23965
24025
  priority: number()
23966
- })).readonly() });
24026
+ });
24027
+ /**
24028
+ * Where the advertised local port came from. Ordered most → least
24029
+ * authoritative, and the whole point of returning it: a client must be able to
24030
+ * tell a FACT about the hub's socket from an echo of its own guess.
24031
+ */
24032
+ var LocalPortSourceEnum = _enum([
24033
+ "server-config",
24034
+ "server-env",
24035
+ "caller-hint",
24036
+ "default"
24037
+ ]);
24038
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24039
+ var AdvertisedLocalPortSchema = object({
24040
+ port: number().int().min(1).max(65535),
24041
+ source: LocalPortSourceEnum
24042
+ });
24043
+ var GetConnectionEndpointsResultSchema = object({
24044
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24045
+ /**
24046
+ * The port the hub built the LAN/loopback URLs with, and where that number
24047
+ * came from.
24048
+ *
24049
+ * Returned rather than merely applied, because "the URL is right" and "the
24050
+ * client can KNOW the URL is right" are different properties. A client that
24051
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24052
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24053
+ * race the candidate or discard it. With `source` it can: anything but
24054
+ * `caller-hint` is the hub's own socket.
24055
+ *
24056
+ * Absent on hubs predating this field — a client that finds it missing is
24057
+ * talking to an echoing hub and must degrade exactly as it does for
24058
+ * `caller-hint`.
24059
+ */
24060
+ localPort: AdvertisedLocalPortSchema
24061
+ });
23967
24062
  /**
23968
24063
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23969
24064
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -23984,8 +24079,13 @@ var AllowedAddressesSchema = object({
23984
24079
  */
23985
24080
  addresses: array(string()).readonly() });
23986
24081
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23987
- /** Local hub HTTP port to use in base URLs. */
23988
- port: number().int().min(1).max(65535),
24082
+ /**
24083
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24084
+ * written against the echoing contract keep working; the hub uses it
24085
+ * only when it cannot read its own port, and says so via
24086
+ * `localPort.source === 'caller-hint'`.
24087
+ */
24088
+ port: number().int().min(1).max(65535).optional(),
23989
24089
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23990
24090
  * candidate. Default `true`. */
23991
24091
  includeLoopback: boolean().optional(),
@@ -30144,11 +30244,51 @@ function startReachabilityPoll(options) {
30144
30244
  } };
30145
30245
  }
30146
30246
  var LAST_FETCHED_FIELD = "lastFetchedAt";
30247
+ /**
30248
+ * The freshness window for a cap whose value changes ONLY when WE write it.
30249
+ *
30250
+ * The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
30251
+ * battery level, a day/night state that flips at dusk. It is the wrong number
30252
+ * for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
30253
+ * microphone on) change when an operator changes them, through a `setMask` /
30254
+ * `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
30255
+ * A write is therefore its own invalidation: the window never delays an
30256
+ * operator's own change, however long it is.
30257
+ *
30258
+ * What the window really bounds is the ONE case we do not write: somebody
30259
+ * changing the mask in the vendor app or the camera's web UI. Ten minutes is
30260
+ * the trade — that change is visible within ten minutes, and a fleet of 29
30261
+ * cameras behind a page that polls every 5s costs about 3 camera round trips a
30262
+ * minute instead of 696
30263
+ * ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
30264
+ *
30265
+ * Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
30266
+ * On the awaiting default a window this long does not remove the stall, it
30267
+ * merely makes it rarer and just as long.
30268
+ */
30269
+ var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
30147
30270
  function createRuntimeStateBridge(params) {
30148
30271
  const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
30149
30272
  const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
30273
+ const staleRead = params.staleRead ?? "await-refresh";
30150
30274
  /** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
30151
30275
  let missCooldownUntil = 0;
30276
+ /**
30277
+ * The refresh this bridge currently has in the air, if any.
30278
+ *
30279
+ * Providers single-flight their own camera client, so this is not what stops
30280
+ * a second round trip. What it stops is a POLLED reader JOINING one: under
30281
+ * `'serve-and-revalidate'` a read that finds a refresh already outstanding is
30282
+ * answered from the slice at once — even a cold, empty slice, which reports
30283
+ * UNKNOWN, which is the truth about a camera nobody has reached.
30284
+ *
30285
+ * That distinction is the whole cost of an unreachable camera. Measured on
30286
+ * the live hub (device 3629, an offline battery Reolink): its refresh takes
30287
+ * 23.1s to give up, so without this every 5s poll landing inside those 23s
30288
+ * joined the wait and spent the caller's full 1.2s source budget — five
30289
+ * stalled polls per cooldown cycle, for one camera, forever.
30290
+ */
30291
+ let refreshInFlight = null;
30152
30292
  const readFetchedAt = () => {
30153
30293
  const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
30154
30294
  return typeof value === "number" ? value : 0;
@@ -30169,14 +30309,14 @@ function createRuntimeStateBridge(params) {
30169
30309
  }
30170
30310
  });
30171
30311
  };
30172
- const ensureFresh = async () => {
30173
- const slice = runtimeState.getCapState(cap.name);
30174
- const fetchedAt = readFetchedAt();
30175
- if (slice && Date.now() - fetchedAt <= staleMs) {
30176
- missCooldownUntil = 0;
30177
- return;
30178
- }
30179
- if (Date.now() < missCooldownUntil) return;
30312
+ /**
30313
+ * One refresh attempt, plus the LANDED check that decides the cooldown.
30314
+ *
30315
+ * @param fetchedAt What `lastFetchedAt` was before the attempt — the only
30316
+ * evidence the bridge has that the refresh persisted
30317
+ * anything, since providers swallow their own camera errors.
30318
+ */
30319
+ const runRefresh = async (fetchedAt) => {
30180
30320
  try {
30181
30321
  await refresh();
30182
30322
  } catch (err) {
@@ -30192,6 +30332,41 @@ function createRuntimeStateBridge(params) {
30192
30332
  }
30193
30333
  openMissCooldown(void 0);
30194
30334
  };
30335
+ /**
30336
+ * Start a refresh and remember it, at most one at a time.
30337
+ *
30338
+ * It never rejects: under `'serve-and-revalidate'` the caller is answered
30339
+ * from the slice either way, so a floating rejection would take the process
30340
+ * down for a fault the miss cooldown has already recorded and logged.
30341
+ */
30342
+ const startRefresh = (fetchedAt) => {
30343
+ const existing = refreshInFlight;
30344
+ if (existing !== null) return existing;
30345
+ const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
30346
+ refreshInFlight = null;
30347
+ });
30348
+ refreshInFlight = started;
30349
+ return started;
30350
+ };
30351
+ const ensureFresh = async () => {
30352
+ const slice = runtimeState.getCapState(cap.name);
30353
+ const fetchedAt = readFetchedAt();
30354
+ if (slice && Date.now() - fetchedAt <= staleMs) {
30355
+ missCooldownUntil = 0;
30356
+ return;
30357
+ }
30358
+ if (Date.now() < missCooldownUntil) return;
30359
+ if (staleRead === "serve-and-revalidate") {
30360
+ if (refreshInFlight !== null) return;
30361
+ if (slice && fetchedAt > 0) {
30362
+ startRefresh(fetchedAt);
30363
+ return;
30364
+ }
30365
+ await startRefresh(fetchedAt);
30366
+ return;
30367
+ }
30368
+ await runRefresh(fetchedAt);
30369
+ };
30195
30370
  const projectStatus = () => {
30196
30371
  const slice = runtimeState.getCapState(cap.name);
30197
30372
  if (!slice) return empty();
@@ -37590,7 +37765,6 @@ var NC_AUDIO_DEFAULTS = {
37590
37765
  hitPercent: 60,
37591
37766
  samplingSeconds: 10
37592
37767
  };
37593
- new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
37594
37768
  new Set(["devices", "classes"]);
37595
37769
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
37596
37770
  /**
@@ -37824,6 +37998,39 @@ TimelapseRuleInputSchema.extend({
37824
37998
  createdAt: number(),
37825
37999
  updatedAt: number()
37826
38000
  });
38001
+ Object.freeze(Object.fromEntries([{
38002
+ stepId: "face-embedding",
38003
+ label: "Face recognition model",
38004
+ defaultModelId: "arcface-r100",
38005
+ indexName: "the enrolled face gallery",
38006
+ options: [
38007
+ {
38008
+ id: "arcface-r100",
38009
+ label: "ArcFace (ResNet34)"
38010
+ },
38011
+ {
38012
+ id: "auraface-r100",
38013
+ label: "AuraFace R100"
38014
+ },
38015
+ {
38016
+ id: "inception-resnet-v1",
38017
+ label: "Inception ResNet V1"
38018
+ }
38019
+ ]
38020
+ }, {
38021
+ stepId: "clip-embedding",
38022
+ label: "Semantic search model",
38023
+ defaultModelId: "mobileclip-s1",
38024
+ indexName: "the object semantic-search index",
38025
+ options: [{
38026
+ id: "mobileclip-s1",
38027
+ label: "MobileCLIP S1"
38028
+ }, {
38029
+ id: "mobileclip-s2",
38030
+ label: "MobileCLIP S2"
38031
+ }]
38032
+ }].map((s) => [s.stepId, s.defaultModelId])));
38033
+ string().min(1);
37827
38034
  object({
37828
38035
  /**
37829
38036
  * Fraction of the box's own size added on EACH side before cutting.
@@ -37844,10 +38051,21 @@ object({
37844
38051
  */
37845
38052
  square: boolean()
37846
38053
  });
37847
- ({
38054
+ /**
38055
+ * The convention in force when nobody has configured one — byte-for-byte the
38056
+ * behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
38057
+ */
38058
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
37848
38059
  paddingRatio: .15,
37849
38060
  square: false
37850
- }).paddingRatio;
38061
+ };
38062
+ Object.freeze({
38063
+ x: 0,
38064
+ y: 0,
38065
+ w: 1,
38066
+ h: 1
38067
+ });
38068
+ DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
37851
38069
  /**
37852
38070
  * WHICH delivered frames the decode worker retains a native copy of.
37853
38071
  *
@@ -39373,7 +39591,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
39373
39591
  static PRIVACY_COORD_MAX = 8191;
39374
39592
  registerPrivacyMaskCap() {
39375
39593
  const CAP_NAME = "privacy-mask";
39376
- const STALE_MS = 1e4;
39377
39594
  const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
39378
39595
  const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
39379
39596
  const refreshFromCamera = async () => {
@@ -39428,7 +39645,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
39428
39645
  cap: privacyMaskCapability,
39429
39646
  ownDeviceId: this.id,
39430
39647
  refresh: refreshFromCamera,
39431
- staleMs: STALE_MS,
39648
+ staleMs: OPERATOR_WRITTEN_STALE_MS,
39649
+ staleRead: "serve-and-revalidate",
39432
39650
  logger: this.ctx.logger,
39433
39651
  empty: () => ({
39434
39652
  enabled: false,
package/dist/addon.mjs CHANGED
@@ -7560,7 +7560,7 @@ var RecordingBandSchema = object({
7560
7560
  postBufferSec: number().min(0).optional()
7561
7561
  });
7562
7562
  ({
7563
- preBufferSec: 10,
7563
+ preBufferSec: 15,
7564
7564
  postBufferSec: 30
7565
7565
  }).postBufferSec * 1e3;
7566
7566
  /**
@@ -13763,128 +13763,103 @@ var COCO_TO_MACRO = {
13763
13763
  var AUDIO_MACRO_LABELS = [
13764
13764
  {
13765
13765
  id: "speech",
13766
- name: "Speech",
13767
- icon: "🗣️"
13766
+ name: "Speech"
13768
13767
  },
13769
13768
  {
13770
13769
  id: "scream",
13771
- name: "Scream / Shout",
13772
- icon: "😱"
13770
+ name: "Scream / Shout"
13773
13771
  },
13774
13772
  {
13775
13773
  id: "crying",
13776
- name: "Crying / Baby",
13777
- icon: "😢"
13774
+ name: "Crying / Baby"
13778
13775
  },
13779
13776
  {
13780
13777
  id: "laughter",
13781
- name: "Laughter",
13782
- icon: "😂"
13778
+ name: "Laughter"
13783
13779
  },
13784
13780
  {
13785
13781
  id: "music",
13786
- name: "Music",
13787
- icon: "🎵"
13782
+ name: "Music"
13788
13783
  },
13789
13784
  {
13790
13785
  id: "dog",
13791
- name: "Dog",
13792
- icon: "🐕"
13786
+ name: "Dog"
13793
13787
  },
13794
13788
  {
13795
13789
  id: "cat",
13796
- name: "Cat",
13797
- icon: "🐈"
13790
+ name: "Cat"
13798
13791
  },
13799
13792
  {
13800
13793
  id: "bird",
13801
- name: "Bird",
13802
- icon: "🐦"
13794
+ name: "Bird"
13803
13795
  },
13804
13796
  {
13805
13797
  id: "animal",
13806
- name: "Animal (other)",
13807
- icon: "🐾"
13798
+ name: "Animal (other)"
13808
13799
  },
13809
13800
  {
13810
13801
  id: "alarm",
13811
- name: "Alarm / Siren",
13812
- icon: "🚨"
13802
+ name: "Alarm / Siren"
13813
13803
  },
13814
13804
  {
13815
13805
  id: "doorbell",
13816
- name: "Doorbell / Knock",
13817
- icon: "🔔"
13806
+ name: "Doorbell / Knock"
13818
13807
  },
13819
13808
  {
13820
13809
  id: "glass_breaking",
13821
- name: "Glass Breaking",
13822
- icon: "💥"
13810
+ name: "Glass Breaking"
13823
13811
  },
13824
13812
  {
13825
13813
  id: "gunshot",
13826
- name: "Gunshot / Explosion",
13827
- icon: "💣"
13814
+ name: "Gunshot / Explosion"
13828
13815
  },
13829
13816
  {
13830
13817
  id: "vehicle",
13831
- name: "Vehicle",
13832
- icon: "🚗"
13818
+ name: "Vehicle"
13833
13819
  },
13834
13820
  {
13835
13821
  id: "siren",
13836
- name: "Emergency Siren",
13837
- icon: "🚑"
13822
+ name: "Emergency Siren"
13838
13823
  },
13839
13824
  {
13840
13825
  id: "fire",
13841
- name: "Fire / Smoke",
13842
- icon: "🔥"
13826
+ name: "Fire / Smoke"
13843
13827
  },
13844
13828
  {
13845
13829
  id: "water",
13846
- name: "Water",
13847
- icon: "💧"
13830
+ name: "Water"
13848
13831
  },
13849
13832
  {
13850
13833
  id: "wind",
13851
- name: "Wind / Weather",
13852
- icon: "🌬️"
13834
+ name: "Wind / Weather"
13853
13835
  },
13854
13836
  {
13855
13837
  id: "door",
13856
- name: "Door",
13857
- icon: "🚪"
13838
+ name: "Door"
13858
13839
  },
13859
13840
  {
13860
13841
  id: "footsteps",
13861
- name: "Footsteps",
13862
- icon: "👣"
13842
+ name: "Footsteps"
13863
13843
  },
13864
13844
  {
13865
13845
  id: "crowd",
13866
- name: "Crowd / Chatter",
13867
- icon: "👥"
13846
+ name: "Crowd / Chatter"
13868
13847
  },
13869
13848
  {
13870
13849
  id: "telephone",
13871
- name: "Telephone",
13872
- icon: "📞"
13850
+ name: "Telephone"
13873
13851
  },
13874
13852
  {
13875
13853
  id: "engine",
13876
- name: "Engine / Motor",
13877
- icon: "⚙️"
13854
+ name: "Engine / Motor"
13878
13855
  },
13879
13856
  {
13880
13857
  id: "tools",
13881
- name: "Tools / Construction",
13882
- icon: "🔨"
13858
+ name: "Tools / Construction"
13883
13859
  },
13884
13860
  {
13885
13861
  id: "silence",
13886
- name: "Silence",
13887
- icon: "🤫"
13862
+ name: "Silence"
13888
13863
  }
13889
13864
  ];
13890
13865
  var YAMNET_TO_MACRO = {
@@ -16386,7 +16361,7 @@ var TrackEnvelopeSchema = object({
16386
16361
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16387
16362
  * keeps every scalar the list surfaces actually render (ids, class(es),
16388
16363
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16389
- * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
16364
+ * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
16390
16365
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16391
16366
  * `getTrack`. Mirrors the event-store `projection` convention
16392
16367
  * (`getObjectEvents` et al.).
@@ -16681,6 +16656,35 @@ var TrackSchema = object({
16681
16656
  */
16682
16657
  hasFace: boolean().optional(),
16683
16658
  /**
16659
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
16660
+ * face an operator could ASSIGN to an identity.
16661
+ *
16662
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
16663
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
16664
+ * branch, before every gate, and means no more than "a face detector produced
16665
+ * a face detail". This one is stamped at the single moment the gallery row
16666
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
16667
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
16668
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
16669
+ * the crop-store drop. Everything between the detector and that insert can
16670
+ * legitimately refuse the face, so a flag written any earlier promises the
16671
+ * operator something to assign and delivers nothing.
16672
+ *
16673
+ * **Independent of recognition.** A face collected but never auto-matched is
16674
+ * still assignable — it is in fact the face an operator most wants to reach —
16675
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
16676
+ * `subLabel`; this says only that the raw material exists.
16677
+ *
16678
+ * **Set once, never cleared.** A track that produced a gallery row produced
16679
+ * one; deleting the row later is the gallery's business, not this flag's.
16680
+ *
16681
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
16682
+ * before the column omits it, and so does every server that predates the
16683
+ * field. A consumer must test `=== true` and render nothing otherwise —
16684
+ * never infer "no assignable face".
16685
+ */
16686
+ hasEmbeddedFace: boolean().optional(),
16687
+ /**
16684
16688
  * This subject CONTAINS a folded rider — a person the rider-pairing step
16685
16689
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
16686
16690
  * so the passage is tracked once and as a VEHICLE.
@@ -18070,6 +18074,32 @@ var DetailResultSchema = object({
18070
18074
  /** FRAME-space bbox (already mapped back from crop space). */
18071
18075
  bbox: NativeCropBboxSchema.optional(),
18072
18076
  embedding: string().optional(),
18077
+ /**
18078
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
18079
+ * so an older runner that does not send it degrades to "unmeasurable", which
18080
+ * the consuming gate treats as accept — never as reject.
18081
+ */
18082
+ embeddingMagnitude: number().optional(),
18083
+ /**
18084
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
18085
+ * `label`, and for the same reason: the producing node is the only side that
18086
+ * knows which model actually ran, and re-deriving it downstream is exactly
18087
+ * the inference this model exists to forbid.
18088
+ *
18089
+ * Additive and optional, so a runner that predates it degrades to
18090
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
18091
+ * its previous assumption instead of refusing.
18092
+ *
18093
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
18094
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
18095
+ * with nothing on the wire it took the id from the GALLERY, so the gate
18096
+ * compared the gallery against itself and could never fire. That turned the
18097
+ * one guard protecting the face feature space into a tautology, and it is
18098
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
18099
+ * gallery as the catalog comment predicts — it would have scored the new
18100
+ * model's vectors against the old model's samples in one cosine space.
18101
+ */
18102
+ embeddingModelId: string().optional(),
18073
18103
  label: string().optional(),
18074
18104
  /**
18075
18105
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -18945,6 +18975,12 @@ var CameraStatusSchema = object({
18945
18975
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
18946
18976
  fetchedAt: number()
18947
18977
  });
18978
+ var InferenceDeviceExclusionReasonSchema = _enum([
18979
+ "disabled",
18980
+ "unavailable",
18981
+ "cannot-host-camera-root",
18982
+ "accelerator-preferred"
18983
+ ]);
18948
18984
  var NodeInferenceDeviceSchema = object({
18949
18985
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
18950
18986
  key: string(),
@@ -18975,7 +19011,17 @@ var NodeInferenceDeviceSchema = object({
18975
19011
  * available per format; this is the stored selection that becomes the
18976
19012
  * default for EVERY camera landing on this accelerator.
18977
19013
  */
18978
- steps: record(string(), DeviceStepConfigSchema).optional()
19014
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19015
+ /**
19016
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19017
+ * the reason the dispatcher drops it. Computed by the SAME
19018
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19019
+ * never disagree with the election — deriving it in the UI from
19020
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19021
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19022
+ * "an accelerator is serving" predicate).
19023
+ */
19024
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
18979
19025
  });
18980
19026
  var NodeInferenceDevicesSchema = object({
18981
19027
  nodeId: string(),
@@ -19534,6 +19580,7 @@ var snapshotCapability = {
19534
19580
  isBattery: boolean(),
19535
19581
  reason: _enum([
19536
19582
  "disabled",
19583
+ "offline",
19537
19584
  "sleeping",
19538
19585
  "unreachable",
19539
19586
  "waking"
@@ -19623,9 +19670,17 @@ targets: array(object({
19623
19670
  sleeping: boolean(),
19624
19671
  /** Current device state rendered over the cached frame. State images
19625
19672
  * remain authoritative even when their photographic background is
19626
- * old; null means the link must carry a current camera frame. */
19673
+ * old; null means the link must carry a current camera frame.
19674
+ *
19675
+ * **This is the ONLY source a surface may derive an inactive tile
19676
+ * from.** A client that reads a device's `online`/`disabled` flags and
19677
+ * draws its own verdict will disagree with the pixels it is painting
19678
+ * — the frame comes from here, and so must the sentence over it
19679
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
19680
+ * state. */
19627
19681
  stateReason: _enum([
19628
19682
  "disabled",
19683
+ "offline",
19629
19684
  "sleeping",
19630
19685
  "unreachable",
19631
19686
  "waking"
@@ -23921,7 +23976,12 @@ var ListResultSchema = object({
23921
23976
  probedAt: number()
23922
23977
  });
23923
23978
  var PreferredSchema = LocalInterfaceSchema.nullable();
23924
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23979
+ /**
23980
+ * Candidate base URL for the SDK to race on connect. Order matters —
23981
+ * the SDK should attempt these top-to-bottom with a short per-candidate
23982
+ * timeout (e.g. 1500ms) and cache the winner for the session.
23983
+ */
23984
+ var ConnectionEndpointSchema = object({
23925
23985
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
23926
23986
  label: string(),
23927
23987
  /** Fully-formed base URL with scheme + host + port. */
@@ -23964,7 +24024,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23964
24024
  * ordering between polls.
23965
24025
  */
23966
24026
  priority: number()
23967
- })).readonly() });
24027
+ });
24028
+ /**
24029
+ * Where the advertised local port came from. Ordered most → least
24030
+ * authoritative, and the whole point of returning it: a client must be able to
24031
+ * tell a FACT about the hub's socket from an echo of its own guess.
24032
+ */
24033
+ var LocalPortSourceEnum = _enum([
24034
+ "server-config",
24035
+ "server-env",
24036
+ "caller-hint",
24037
+ "default"
24038
+ ]);
24039
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24040
+ var AdvertisedLocalPortSchema = object({
24041
+ port: number().int().min(1).max(65535),
24042
+ source: LocalPortSourceEnum
24043
+ });
24044
+ var GetConnectionEndpointsResultSchema = object({
24045
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24046
+ /**
24047
+ * The port the hub built the LAN/loopback URLs with, and where that number
24048
+ * came from.
24049
+ *
24050
+ * Returned rather than merely applied, because "the URL is right" and "the
24051
+ * client can KNOW the URL is right" are different properties. A client that
24052
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24053
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24054
+ * race the candidate or discard it. With `source` it can: anything but
24055
+ * `caller-hint` is the hub's own socket.
24056
+ *
24057
+ * Absent on hubs predating this field — a client that finds it missing is
24058
+ * talking to an echoing hub and must degrade exactly as it does for
24059
+ * `caller-hint`.
24060
+ */
24061
+ localPort: AdvertisedLocalPortSchema
24062
+ });
23968
24063
  /**
23969
24064
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23970
24065
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -23985,8 +24080,13 @@ var AllowedAddressesSchema = object({
23985
24080
  */
23986
24081
  addresses: array(string()).readonly() });
23987
24082
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23988
- /** Local hub HTTP port to use in base URLs. */
23989
- port: number().int().min(1).max(65535),
24083
+ /**
24084
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24085
+ * written against the echoing contract keep working; the hub uses it
24086
+ * only when it cannot read its own port, and says so via
24087
+ * `localPort.source === 'caller-hint'`.
24088
+ */
24089
+ port: number().int().min(1).max(65535).optional(),
23990
24090
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23991
24091
  * candidate. Default `true`. */
23992
24092
  includeLoopback: boolean().optional(),
@@ -30145,11 +30245,51 @@ function startReachabilityPoll(options) {
30145
30245
  } };
30146
30246
  }
30147
30247
  var LAST_FETCHED_FIELD = "lastFetchedAt";
30248
+ /**
30249
+ * The freshness window for a cap whose value changes ONLY when WE write it.
30250
+ *
30251
+ * The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
30252
+ * battery level, a day/night state that flips at dusk. It is the wrong number
30253
+ * for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
30254
+ * microphone on) change when an operator changes them, through a `setMask` /
30255
+ * `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
30256
+ * A write is therefore its own invalidation: the window never delays an
30257
+ * operator's own change, however long it is.
30258
+ *
30259
+ * What the window really bounds is the ONE case we do not write: somebody
30260
+ * changing the mask in the vendor app or the camera's web UI. Ten minutes is
30261
+ * the trade — that change is visible within ten minutes, and a fleet of 29
30262
+ * cameras behind a page that polls every 5s costs about 3 camera round trips a
30263
+ * minute instead of 696
30264
+ * ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
30265
+ *
30266
+ * Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
30267
+ * On the awaiting default a window this long does not remove the stall, it
30268
+ * merely makes it rarer and just as long.
30269
+ */
30270
+ var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
30148
30271
  function createRuntimeStateBridge(params) {
30149
30272
  const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
30150
30273
  const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
30274
+ const staleRead = params.staleRead ?? "await-refresh";
30151
30275
  /** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
30152
30276
  let missCooldownUntil = 0;
30277
+ /**
30278
+ * The refresh this bridge currently has in the air, if any.
30279
+ *
30280
+ * Providers single-flight their own camera client, so this is not what stops
30281
+ * a second round trip. What it stops is a POLLED reader JOINING one: under
30282
+ * `'serve-and-revalidate'` a read that finds a refresh already outstanding is
30283
+ * answered from the slice at once — even a cold, empty slice, which reports
30284
+ * UNKNOWN, which is the truth about a camera nobody has reached.
30285
+ *
30286
+ * That distinction is the whole cost of an unreachable camera. Measured on
30287
+ * the live hub (device 3629, an offline battery Reolink): its refresh takes
30288
+ * 23.1s to give up, so without this every 5s poll landing inside those 23s
30289
+ * joined the wait and spent the caller's full 1.2s source budget — five
30290
+ * stalled polls per cooldown cycle, for one camera, forever.
30291
+ */
30292
+ let refreshInFlight = null;
30153
30293
  const readFetchedAt = () => {
30154
30294
  const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
30155
30295
  return typeof value === "number" ? value : 0;
@@ -30170,14 +30310,14 @@ function createRuntimeStateBridge(params) {
30170
30310
  }
30171
30311
  });
30172
30312
  };
30173
- const ensureFresh = async () => {
30174
- const slice = runtimeState.getCapState(cap.name);
30175
- const fetchedAt = readFetchedAt();
30176
- if (slice && Date.now() - fetchedAt <= staleMs) {
30177
- missCooldownUntil = 0;
30178
- return;
30179
- }
30180
- if (Date.now() < missCooldownUntil) return;
30313
+ /**
30314
+ * One refresh attempt, plus the LANDED check that decides the cooldown.
30315
+ *
30316
+ * @param fetchedAt What `lastFetchedAt` was before the attempt — the only
30317
+ * evidence the bridge has that the refresh persisted
30318
+ * anything, since providers swallow their own camera errors.
30319
+ */
30320
+ const runRefresh = async (fetchedAt) => {
30181
30321
  try {
30182
30322
  await refresh();
30183
30323
  } catch (err) {
@@ -30193,6 +30333,41 @@ function createRuntimeStateBridge(params) {
30193
30333
  }
30194
30334
  openMissCooldown(void 0);
30195
30335
  };
30336
+ /**
30337
+ * Start a refresh and remember it, at most one at a time.
30338
+ *
30339
+ * It never rejects: under `'serve-and-revalidate'` the caller is answered
30340
+ * from the slice either way, so a floating rejection would take the process
30341
+ * down for a fault the miss cooldown has already recorded and logged.
30342
+ */
30343
+ const startRefresh = (fetchedAt) => {
30344
+ const existing = refreshInFlight;
30345
+ if (existing !== null) return existing;
30346
+ const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
30347
+ refreshInFlight = null;
30348
+ });
30349
+ refreshInFlight = started;
30350
+ return started;
30351
+ };
30352
+ const ensureFresh = async () => {
30353
+ const slice = runtimeState.getCapState(cap.name);
30354
+ const fetchedAt = readFetchedAt();
30355
+ if (slice && Date.now() - fetchedAt <= staleMs) {
30356
+ missCooldownUntil = 0;
30357
+ return;
30358
+ }
30359
+ if (Date.now() < missCooldownUntil) return;
30360
+ if (staleRead === "serve-and-revalidate") {
30361
+ if (refreshInFlight !== null) return;
30362
+ if (slice && fetchedAt > 0) {
30363
+ startRefresh(fetchedAt);
30364
+ return;
30365
+ }
30366
+ await startRefresh(fetchedAt);
30367
+ return;
30368
+ }
30369
+ await runRefresh(fetchedAt);
30370
+ };
30196
30371
  const projectStatus = () => {
30197
30372
  const slice = runtimeState.getCapState(cap.name);
30198
30373
  if (!slice) return empty();
@@ -37591,7 +37766,6 @@ var NC_AUDIO_DEFAULTS = {
37591
37766
  hitPercent: 60,
37592
37767
  samplingSeconds: 10
37593
37768
  };
37594
- new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
37595
37769
  new Set(["devices", "classes"]);
37596
37770
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
37597
37771
  /**
@@ -37825,6 +37999,39 @@ TimelapseRuleInputSchema.extend({
37825
37999
  createdAt: number(),
37826
38000
  updatedAt: number()
37827
38001
  });
38002
+ Object.freeze(Object.fromEntries([{
38003
+ stepId: "face-embedding",
38004
+ label: "Face recognition model",
38005
+ defaultModelId: "arcface-r100",
38006
+ indexName: "the enrolled face gallery",
38007
+ options: [
38008
+ {
38009
+ id: "arcface-r100",
38010
+ label: "ArcFace (ResNet34)"
38011
+ },
38012
+ {
38013
+ id: "auraface-r100",
38014
+ label: "AuraFace R100"
38015
+ },
38016
+ {
38017
+ id: "inception-resnet-v1",
38018
+ label: "Inception ResNet V1"
38019
+ }
38020
+ ]
38021
+ }, {
38022
+ stepId: "clip-embedding",
38023
+ label: "Semantic search model",
38024
+ defaultModelId: "mobileclip-s1",
38025
+ indexName: "the object semantic-search index",
38026
+ options: [{
38027
+ id: "mobileclip-s1",
38028
+ label: "MobileCLIP S1"
38029
+ }, {
38030
+ id: "mobileclip-s2",
38031
+ label: "MobileCLIP S2"
38032
+ }]
38033
+ }].map((s) => [s.stepId, s.defaultModelId])));
38034
+ string().min(1);
37828
38035
  object({
37829
38036
  /**
37830
38037
  * Fraction of the box's own size added on EACH side before cutting.
@@ -37845,10 +38052,21 @@ object({
37845
38052
  */
37846
38053
  square: boolean()
37847
38054
  });
37848
- ({
38055
+ /**
38056
+ * The convention in force when nobody has configured one — byte-for-byte the
38057
+ * behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
38058
+ */
38059
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
37849
38060
  paddingRatio: .15,
37850
38061
  square: false
37851
- }).paddingRatio;
38062
+ };
38063
+ Object.freeze({
38064
+ x: 0,
38065
+ y: 0,
38066
+ w: 1,
38067
+ h: 1
38068
+ });
38069
+ DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
37852
38070
  /**
37853
38071
  * WHICH delivered frames the decode worker retains a native copy of.
37854
38072
  *
@@ -39374,7 +39592,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
39374
39592
  static PRIVACY_COORD_MAX = 8191;
39375
39593
  registerPrivacyMaskCap() {
39376
39594
  const CAP_NAME = "privacy-mask";
39377
- const STALE_MS = 1e4;
39378
39595
  const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
39379
39596
  const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
39380
39597
  const refreshFromCamera = async () => {
@@ -39429,7 +39646,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
39429
39646
  cap: privacyMaskCapability,
39430
39647
  ownDeviceId: this.id,
39431
39648
  refresh: refreshFromCamera,
39432
- staleMs: STALE_MS,
39649
+ staleMs: OPERATOR_WRITTEN_STALE_MS,
39650
+ staleRead: "serve-and-revalidate",
39433
39651
  logger: this.ctx.logger,
39434
39652
  empty: () => ({
39435
39653
  enabled: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",