@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.
- package/dist/addon.js +289 -71
- package/dist/addon.mjs +289 -71
- 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:
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
/**
|
|
23988
|
-
|
|
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
|
-
|
|
30173
|
-
|
|
30174
|
-
|
|
30175
|
-
|
|
30176
|
-
|
|
30177
|
-
|
|
30178
|
-
|
|
30179
|
-
|
|
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
|
-
}
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
/**
|
|
23989
|
-
|
|
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
|
-
|
|
30174
|
-
|
|
30175
|
-
|
|
30176
|
-
|
|
30177
|
-
|
|
30178
|
-
|
|
30179
|
-
|
|
30180
|
-
|
|
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
|
-
}
|
|
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:
|
|
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.
|
|
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",
|