@trillboards/edge-sdk 0.2.3 → 0.2.6
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/README.md +4 -3
- package/deploy/docker/Dockerfile.cloud-sense +117 -0
- package/deploy/docker/Dockerfile.cpu +30 -21
- package/deploy/docker/Dockerfile.cuda +30 -21
- package/deploy/docker/Dockerfile.openvino +30 -20
- package/deploy/docker/README.md +9 -4
- package/deploy/docker/connector-entrypoint.sh +122 -0
- package/deploy/helm/README.md +1 -1
- package/deploy/k8s/daemonset.yaml +1 -1
- package/deploy/macos/README.md +161 -0
- package/deploy/macos/TrillboardsSetupApp.m +603 -0
- package/deploy/macos/build-macos-pkg.sh +588 -0
- package/deploy/macos/com.trillboards.edge.plist +62 -0
- package/deploy/macos/com.trillboards.litertlm.plist +45 -0
- package/deploy/macos/pair.command +519 -0
- package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
- package/deploy/macos/scripts/postinstall +153 -0
- package/deploy/macos/scripts/setup-litertlm.sh +272 -0
- package/deploy/macos/test-install-pair.sh +383 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
- package/deploy/systemd/install.sh +25 -15
- package/deploy/systemd/trillboards-edge.service +4 -1
- package/deploy/windows/README.md +70 -0
- package/deploy/windows/build-windows.ps1 +225 -0
- package/deploy/windows/bundle-windows-installer.ps1 +153 -0
- package/deploy/windows/check-feeds.js +26 -0
- package/deploy/windows/e2e/fixtures/person.jpg +0 -0
- package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
- package/deploy/windows/install-update.ps1 +101 -0
- package/deploy/windows/installer.nsi +138 -0
- package/deploy/windows/register-litertlm-task.ps1 +42 -0
- package/deploy/windows/register-task.ps1 +44 -0
- package/deploy/windows/run-connector.ps1 +69 -0
- package/deploy/windows/setup-litertlm.ps1 +361 -0
- package/deploy/windows/stop-existing.ps1 +72 -0
- package/deploy/windows/trillboards-edge-task.vbs +25 -0
- package/deploy/windows/trillboards-edge.cmd +33 -0
- package/deploy/windows/trillboards-pair.cmd +276 -0
- package/deploy/windows/trillboards-setup.ps1 +456 -0
- package/deploy/windows/trillboards-setup.vbs +13 -0
- package/dist/AudienceAnalyzeClient.d.ts +136 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
- package/dist/AudienceAnalyzeClient.js +246 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -0
- package/dist/ChannelProber.d.ts +150 -0
- package/dist/ChannelProber.d.ts.map +1 -0
- package/dist/ChannelProber.js +887 -0
- package/dist/ChannelProber.js.map +1 -0
- package/dist/CloudEscalationPolicy.d.ts +143 -0
- package/dist/CloudEscalationPolicy.d.ts.map +1 -0
- package/dist/CloudEscalationPolicy.js +137 -0
- package/dist/CloudEscalationPolicy.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +533 -0
- package/dist/CloudSensingManager.d.ts.map +1 -0
- package/dist/CloudSensingManager.js +1254 -0
- package/dist/CloudSensingManager.js.map +1 -0
- package/dist/DiscoveryReportClient.d.ts +66 -0
- package/dist/DiscoveryReportClient.d.ts.map +1 -0
- package/dist/DiscoveryReportClient.js +85 -0
- package/dist/DiscoveryReportClient.js.map +1 -0
- package/dist/EdgeAgent.d.ts +12 -2
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +122 -65
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +193 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -0
- package/dist/LiteRtVlmProvider.js +436 -0
- package/dist/LiteRtVlmProvider.js.map +1 -0
- package/dist/MosaicSource.d.ts +108 -0
- package/dist/MosaicSource.d.ts.map +1 -0
- package/dist/MosaicSource.js +274 -0
- package/dist/MosaicSource.js.map +1 -0
- package/dist/OnvifClient.d.ts +83 -0
- package/dist/OnvifClient.d.ts.map +1 -0
- package/dist/OnvifClient.js +209 -0
- package/dist/OnvifClient.js.map +1 -0
- package/dist/PairClient.d.ts +106 -0
- package/dist/PairClient.d.ts.map +1 -0
- package/dist/PairClient.js +186 -0
- package/dist/PairClient.js.map +1 -0
- package/dist/RecorderAddressRecovery.d.ts +53 -0
- package/dist/RecorderAddressRecovery.d.ts.map +1 -0
- package/dist/RecorderAddressRecovery.js +359 -0
- package/dist/RecorderAddressRecovery.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +291 -0
- package/dist/SceneChangeDetector.d.ts.map +1 -0
- package/dist/SceneChangeDetector.js +682 -0
- package/dist/SceneChangeDetector.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +59 -0
- package/dist/SensingProfileClient.d.ts.map +1 -0
- package/dist/SensingProfileClient.js +78 -0
- package/dist/SensingProfileClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +114 -0
- package/dist/SetupSessionClient.d.ts.map +1 -0
- package/dist/SetupSessionClient.js +95 -0
- package/dist/SetupSessionClient.js.map +1 -0
- package/dist/VlmAdmissionGate.d.ts +126 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -0
- package/dist/VlmAdmissionGate.js +166 -0
- package/dist/VlmAdmissionGate.js.map +1 -0
- package/dist/autoSense.d.ts +216 -0
- package/dist/autoSense.d.ts.map +1 -0
- package/dist/autoSense.js +521 -0
- package/dist/autoSense.js.map +1 -0
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +511 -21
- package/dist/cli.js.map +1 -1
- package/dist/combinedSense.d.ts +128 -0
- package/dist/combinedSense.d.ts.map +1 -0
- package/dist/combinedSense.js +261 -0
- package/dist/combinedSense.js.map +1 -0
- package/dist/config.d.ts +351 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +99 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorCadence.d.ts +135 -0
- package/dist/connectorCadence.d.ts.map +1 -0
- package/dist/connectorCadence.js +151 -0
- package/dist/connectorCadence.js.map +1 -0
- package/dist/dataDir.d.ts +33 -0
- package/dist/dataDir.d.ts.map +1 -0
- package/dist/dataDir.js +76 -0
- package/dist/dataDir.js.map +1 -0
- package/dist/deployedCadence.d.ts +167 -0
- package/dist/deployedCadence.d.ts.map +1 -0
- package/dist/deployedCadence.js +209 -0
- package/dist/deployedCadence.js.map +1 -0
- package/dist/deployedCadenceSource.d.ts +48 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -0
- package/dist/deployedCadenceSource.js +74 -0
- package/dist/deployedCadenceSource.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +13 -12
- package/dist/diagnose.js.map +1 -1
- package/dist/discovery.d.ts +131 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +499 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gemmaModelOta.d.ts +118 -0
- package/dist/gemmaModelOta.d.ts.map +1 -0
- package/dist/gemmaModelOta.js +291 -0
- package/dist/gemmaModelOta.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +41 -18
- package/dist/init.js.map +1 -1
- package/dist/keepAwake.d.ts +33 -0
- package/dist/keepAwake.d.ts.map +1 -0
- package/dist/keepAwake.js +54 -0
- package/dist/keepAwake.js.map +1 -0
- package/dist/legacyRecorderProfile.d.ts +20 -0
- package/dist/legacyRecorderProfile.d.ts.map +1 -0
- package/dist/legacyRecorderProfile.js +27 -0
- package/dist/legacyRecorderProfile.js.map +1 -0
- package/dist/localInferenceSchema.d.ts +132 -0
- package/dist/localInferenceSchema.d.ts.map +1 -0
- package/dist/localInferenceSchema.js +274 -0
- package/dist/localInferenceSchema.js.map +1 -0
- package/dist/localSenseE2eHarness.d.ts +101 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -0
- package/dist/localSenseE2eHarness.js +279 -0
- package/dist/localSenseE2eHarness.js.map +1 -0
- package/dist/localSensing.d.ts +268 -0
- package/dist/localSensing.d.ts.map +1 -0
- package/dist/localSensing.js +492 -0
- package/dist/localSensing.js.map +1 -0
- package/dist/modelIntegrity.d.ts +59 -0
- package/dist/modelIntegrity.d.ts.map +1 -0
- package/dist/modelIntegrity.js +165 -0
- package/dist/modelIntegrity.js.map +1 -0
- package/dist/pairVerify.d.ts +100 -0
- package/dist/pairVerify.d.ts.map +1 -0
- package/dist/pairVerify.js +296 -0
- package/dist/pairVerify.js.map +1 -0
- package/dist/qrScanner.d.ts +33 -0
- package/dist/qrScanner.d.ts.map +1 -0
- package/dist/qrScanner.js +103 -0
- package/dist/qrScanner.js.map +1 -0
- package/dist/runtimePaths.d.ts +10 -0
- package/dist/runtimePaths.d.ts.map +1 -0
- package/dist/runtimePaths.js +79 -0
- package/dist/runtimePaths.js.map +1 -0
- package/dist/sensingRunMode.d.ts +68 -0
- package/dist/sensingRunMode.d.ts.map +1 -0
- package/dist/sensingRunMode.js +346 -0
- package/dist/sensingRunMode.js.map +1 -0
- package/dist/setupMode.d.ts +98 -0
- package/dist/setupMode.d.ts.map +1 -0
- package/dist/setupMode.js +828 -0
- package/dist/setupMode.js.map +1 -0
- package/dist/setupProgress.d.ts +31 -0
- package/dist/setupProgress.d.ts.map +1 -0
- package/dist/setupProgress.js +173 -0
- package/dist/setupProgress.js.map +1 -0
- package/package.json +13 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connectorCadence — how often THIS connector looks at a scene where nothing
|
|
3
|
+
* is moving.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this is not one constant ────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `DEFAULT_PERIODIC_MS` is 300 s. That number was chosen for the DOOH audience
|
|
8
|
+
* product — a screen sampling the people in front of it, where five-minute
|
|
9
|
+
* granularity is fine because the question is "who saw this ad".
|
|
10
|
+
*
|
|
11
|
+
* The Sense-Anything / SOP product asks a different question, and it is the one
|
|
12
|
+
* the legal argument turns on: HOW LONG has this hazard been there. Courts have
|
|
13
|
+
* found no constructive notice at 111 seconds and at four minutes, and have let
|
|
14
|
+
* the question reach a jury at fifteen-plus. A spill that a PERSON creates is a
|
|
15
|
+
* motion event and the cascade catches it immediately. A spill that nobody
|
|
16
|
+
* creates — a leaking cooler, condensation, a slow seep, product that settled
|
|
17
|
+
* out of a bag — never moves, so the ONLY thing that can put a timestamp on it
|
|
18
|
+
* is the periodic heartbeat. Whatever that interval is, IT IS the resolution of
|
|
19
|
+
* the persistence timeline: the dwell time is bracketed by the last look that
|
|
20
|
+
* showed clean floor and the first that showed the hazard.
|
|
21
|
+
*
|
|
22
|
+
* At 300 s that bracket is five minutes wide, which cannot place a hazard on
|
|
23
|
+
* either side of a 111-second line. So the cadence has to come down — but not
|
|
24
|
+
* globally, because every analysis is spent from ONE budget.
|
|
25
|
+
*
|
|
26
|
+
* ── Why it lives HERE and not on the server ─────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Both inputs are facts about this machine, and the server has neither.
|
|
29
|
+
* `mergeConfigFile` folds a finalize batch into whatever feeds the config
|
|
30
|
+
* already holds, so the API only ever sees the cameras added in the CURRENT
|
|
31
|
+
* setup session: sizing there would send a 1-feed cadence to a connector that
|
|
32
|
+
* ends up running 21 of them, which is ~2,520 still-scene analyses/hour against
|
|
33
|
+
* a budget of 493. The post-merge feed count only exists on this side.
|
|
34
|
+
*
|
|
35
|
+
* ── The budget (measured 2026-08-12, real frames, real code paths) ──────────
|
|
36
|
+
*
|
|
37
|
+
* ANALYZE `litert-lm serve` is a single-conversation server and
|
|
38
|
+
* CloudSensingManager gives local sensing exactly ONE admission
|
|
39
|
+
* slot. Median 7.30 s/frame over 8 real CCTV frames (an empty
|
|
40
|
+
* convenience store and a kitchen with people) through the actual
|
|
41
|
+
* connector path — LiteRtVlmProvider against Gemma 4 E2B on the
|
|
42
|
+
* `,gpu` delegate, with the operator/SOP schema and prompt
|
|
43
|
+
* localInferenceSchema builds. One slot at 7.30 s is a hard ceiling
|
|
44
|
+
* of 493 analyses/hour for the WHOLE connector, however many
|
|
45
|
+
* cameras it runs.
|
|
46
|
+
* (The DOOH audience schema measured 12.48 s median and ranged to
|
|
47
|
+
* 23.8 s because it emits a free-text narrative. The SOP field set
|
|
48
|
+
* is both cheaper and far tighter — 6.0-7.5 s — which is a second,
|
|
49
|
+
* independent reason the two products should not share a cadence.)
|
|
50
|
+
*
|
|
51
|
+
* LOOKING is free and is not the constraint: 3.6 ms median / 7.1 ms p95 to
|
|
52
|
+
* pull a frame down to ~178x100 grey and run the motion detector.
|
|
53
|
+
* Sixteen feeds looking every 30 s is 0.19% of one core.
|
|
54
|
+
*
|
|
55
|
+
* FLOOR on a still scene the motion gate contributes exactly ZERO
|
|
56
|
+
* analyses — verified across byte-identical frames and gaussian
|
|
57
|
+
* sensor noise to sigma=12, at the 30 s look cadence, for
|
|
58
|
+
* heartbeats from 300 s down to 60 s. So a still feed's cost is
|
|
59
|
+
* precisely 3600/heartbeat analyses per hour and nothing else.
|
|
60
|
+
* (Only true with the contrast-history seeding fix in
|
|
61
|
+
* SceneChangeDetector.ts; before it a still scene spent ~53
|
|
62
|
+
* inferences per camera per start on phantom motion.)
|
|
63
|
+
*
|
|
64
|
+
* ── The derivation ──────────────────────────────────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* N feeds x (3600 / H) analyses/hour <= SHARE x (3600 / ANALYZE)
|
|
67
|
+
* H >= N x ANALYZE / SHARE
|
|
68
|
+
*
|
|
69
|
+
* SHARE is the one judgement here and it is stated rather than hidden: the
|
|
70
|
+
* still-scene floor may consume at most HALF the slot, leaving the other half
|
|
71
|
+
* for motion-triggered analyses — the alerts the product actually exists to
|
|
72
|
+
* fire. A floor that ate the whole budget would starve them, and the connector
|
|
73
|
+
* would degrade into reporting empty aisles on time while missing the incident.
|
|
74
|
+
*
|
|
75
|
+
* Two clamps, both from values the system already declares:
|
|
76
|
+
* - never faster than the LOOK interval (`faceTriggeredIntervalMs`, 30 s):
|
|
77
|
+
* you cannot analyze more often than you look.
|
|
78
|
+
* - never slower than the DOOH default (300 s): this can only make a
|
|
79
|
+
* connector sample MORE often than it does today, never less.
|
|
80
|
+
*
|
|
81
|
+
* Result, at the measured 7.30 s: 1-2 feeds 30 s, 4 feeds 59 s, 8 feeds 117 s,
|
|
82
|
+
* 16 feeds 234 s, 21+ feeds pinned at 300 s. A deployment of seven or fewer
|
|
83
|
+
* feeds per host resolves the 111-second threshold; past that the operator
|
|
84
|
+
* needs a second connector host, and `getSensingHealth()` already reports the
|
|
85
|
+
* honest effective interval so that shows up rather than hiding.
|
|
86
|
+
*
|
|
87
|
+
* ── Why not the profile's own capture_interval_ms ───────────────────────────
|
|
88
|
+
*
|
|
89
|
+
* `sensing_profiles.capture_interval_ms` exists and is per-screen, but across
|
|
90
|
+
* the 147 active `custom_intent` profiles it holds 5-30 s. Those are the
|
|
91
|
+
* ANDROID tablet's capture rate, where a frame costs a BlazeFace pass, not a
|
|
92
|
+
* VLM generation. Feeding 5 s to a connector whose ceiling is 493 analyses/hour
|
|
93
|
+
* would demand 11,520 and oversubscribe the single slot ~23x. The profile says
|
|
94
|
+
* what the operator wants; this says what the machine can deliver.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Measured median seconds per on-device analysis, in ms. See the header for the
|
|
98
|
+
* measurement. Re-measure and update this when the model tier, the delegate, or
|
|
99
|
+
* the SOP schema changes — it is the only free variable here.
|
|
100
|
+
*/
|
|
101
|
+
export declare const MEASURED_LOCAL_ANALYZE_MS = 7300;
|
|
102
|
+
/**
|
|
103
|
+
* Share of the single admission slot the STILL-scene heartbeat floor may take.
|
|
104
|
+
* The rest is held for motion-triggered analyses.
|
|
105
|
+
*/
|
|
106
|
+
export declare const STILL_FLOOR_SLOT_SHARE = 0.5;
|
|
107
|
+
/**
|
|
108
|
+
* The connector's look cadence (`faceTriggeredIntervalMs` default, which is
|
|
109
|
+
* also the motion gate's scan interval). A heartbeat can never be finer.
|
|
110
|
+
*/
|
|
111
|
+
export declare const LOOK_INTERVAL_MS = 30000;
|
|
112
|
+
/** The DOOH audience default (`DEFAULT_PERIODIC_MS`). The slow end of the clamp. */
|
|
113
|
+
export declare const DOOH_PERIODIC_MS = 300000;
|
|
114
|
+
/**
|
|
115
|
+
* Sustainable still-scene heartbeat for a connector running `feedCount` feeds
|
|
116
|
+
* against ONE local inference slot.
|
|
117
|
+
*
|
|
118
|
+
* `feedCount` MUST be the connector's TOTAL feed count after any merge, not the
|
|
119
|
+
* feeds added by one setup session — see the header.
|
|
120
|
+
*
|
|
121
|
+
* Returns `null` when the count is not a positive integer, so a caller can omit
|
|
122
|
+
* the key entirely rather than write a cadence derived from a bad count.
|
|
123
|
+
*/
|
|
124
|
+
export declare function periodicIntervalMsForFeedCount(feedCount: unknown): number | null;
|
|
125
|
+
/**
|
|
126
|
+
* The still-scene analyses/hour a whole connector demands at a heartbeat, and
|
|
127
|
+
* what fraction of the single slot that is. Exposed so the derivation is
|
|
128
|
+
* assertable in tests and readable in a log line rather than being folded away
|
|
129
|
+
* inside one expression.
|
|
130
|
+
*/
|
|
131
|
+
export declare function stillSceneLoad(feedCount: unknown, periodicIntervalMs: unknown): {
|
|
132
|
+
analysesPerHour: number;
|
|
133
|
+
slotShare: number;
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=connectorCadence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectorCadence.d.ts","sourceRoot":"","sources":["../src/connectorCadence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,OAAO,EAClB,kBAAkB,EAAE,OAAO,GAC1B;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAOhD"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* connectorCadence — how often THIS connector looks at a scene where nothing
|
|
4
|
+
* is moving.
|
|
5
|
+
*
|
|
6
|
+
* ── Why this is not one constant ────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* `DEFAULT_PERIODIC_MS` is 300 s. That number was chosen for the DOOH audience
|
|
9
|
+
* product — a screen sampling the people in front of it, where five-minute
|
|
10
|
+
* granularity is fine because the question is "who saw this ad".
|
|
11
|
+
*
|
|
12
|
+
* The Sense-Anything / SOP product asks a different question, and it is the one
|
|
13
|
+
* the legal argument turns on: HOW LONG has this hazard been there. Courts have
|
|
14
|
+
* found no constructive notice at 111 seconds and at four minutes, and have let
|
|
15
|
+
* the question reach a jury at fifteen-plus. A spill that a PERSON creates is a
|
|
16
|
+
* motion event and the cascade catches it immediately. A spill that nobody
|
|
17
|
+
* creates — a leaking cooler, condensation, a slow seep, product that settled
|
|
18
|
+
* out of a bag — never moves, so the ONLY thing that can put a timestamp on it
|
|
19
|
+
* is the periodic heartbeat. Whatever that interval is, IT IS the resolution of
|
|
20
|
+
* the persistence timeline: the dwell time is bracketed by the last look that
|
|
21
|
+
* showed clean floor and the first that showed the hazard.
|
|
22
|
+
*
|
|
23
|
+
* At 300 s that bracket is five minutes wide, which cannot place a hazard on
|
|
24
|
+
* either side of a 111-second line. So the cadence has to come down — but not
|
|
25
|
+
* globally, because every analysis is spent from ONE budget.
|
|
26
|
+
*
|
|
27
|
+
* ── Why it lives HERE and not on the server ─────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* Both inputs are facts about this machine, and the server has neither.
|
|
30
|
+
* `mergeConfigFile` folds a finalize batch into whatever feeds the config
|
|
31
|
+
* already holds, so the API only ever sees the cameras added in the CURRENT
|
|
32
|
+
* setup session: sizing there would send a 1-feed cadence to a connector that
|
|
33
|
+
* ends up running 21 of them, which is ~2,520 still-scene analyses/hour against
|
|
34
|
+
* a budget of 493. The post-merge feed count only exists on this side.
|
|
35
|
+
*
|
|
36
|
+
* ── The budget (measured 2026-08-12, real frames, real code paths) ──────────
|
|
37
|
+
*
|
|
38
|
+
* ANALYZE `litert-lm serve` is a single-conversation server and
|
|
39
|
+
* CloudSensingManager gives local sensing exactly ONE admission
|
|
40
|
+
* slot. Median 7.30 s/frame over 8 real CCTV frames (an empty
|
|
41
|
+
* convenience store and a kitchen with people) through the actual
|
|
42
|
+
* connector path — LiteRtVlmProvider against Gemma 4 E2B on the
|
|
43
|
+
* `,gpu` delegate, with the operator/SOP schema and prompt
|
|
44
|
+
* localInferenceSchema builds. One slot at 7.30 s is a hard ceiling
|
|
45
|
+
* of 493 analyses/hour for the WHOLE connector, however many
|
|
46
|
+
* cameras it runs.
|
|
47
|
+
* (The DOOH audience schema measured 12.48 s median and ranged to
|
|
48
|
+
* 23.8 s because it emits a free-text narrative. The SOP field set
|
|
49
|
+
* is both cheaper and far tighter — 6.0-7.5 s — which is a second,
|
|
50
|
+
* independent reason the two products should not share a cadence.)
|
|
51
|
+
*
|
|
52
|
+
* LOOKING is free and is not the constraint: 3.6 ms median / 7.1 ms p95 to
|
|
53
|
+
* pull a frame down to ~178x100 grey and run the motion detector.
|
|
54
|
+
* Sixteen feeds looking every 30 s is 0.19% of one core.
|
|
55
|
+
*
|
|
56
|
+
* FLOOR on a still scene the motion gate contributes exactly ZERO
|
|
57
|
+
* analyses — verified across byte-identical frames and gaussian
|
|
58
|
+
* sensor noise to sigma=12, at the 30 s look cadence, for
|
|
59
|
+
* heartbeats from 300 s down to 60 s. So a still feed's cost is
|
|
60
|
+
* precisely 3600/heartbeat analyses per hour and nothing else.
|
|
61
|
+
* (Only true with the contrast-history seeding fix in
|
|
62
|
+
* SceneChangeDetector.ts; before it a still scene spent ~53
|
|
63
|
+
* inferences per camera per start on phantom motion.)
|
|
64
|
+
*
|
|
65
|
+
* ── The derivation ──────────────────────────────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* N feeds x (3600 / H) analyses/hour <= SHARE x (3600 / ANALYZE)
|
|
68
|
+
* H >= N x ANALYZE / SHARE
|
|
69
|
+
*
|
|
70
|
+
* SHARE is the one judgement here and it is stated rather than hidden: the
|
|
71
|
+
* still-scene floor may consume at most HALF the slot, leaving the other half
|
|
72
|
+
* for motion-triggered analyses — the alerts the product actually exists to
|
|
73
|
+
* fire. A floor that ate the whole budget would starve them, and the connector
|
|
74
|
+
* would degrade into reporting empty aisles on time while missing the incident.
|
|
75
|
+
*
|
|
76
|
+
* Two clamps, both from values the system already declares:
|
|
77
|
+
* - never faster than the LOOK interval (`faceTriggeredIntervalMs`, 30 s):
|
|
78
|
+
* you cannot analyze more often than you look.
|
|
79
|
+
* - never slower than the DOOH default (300 s): this can only make a
|
|
80
|
+
* connector sample MORE often than it does today, never less.
|
|
81
|
+
*
|
|
82
|
+
* Result, at the measured 7.30 s: 1-2 feeds 30 s, 4 feeds 59 s, 8 feeds 117 s,
|
|
83
|
+
* 16 feeds 234 s, 21+ feeds pinned at 300 s. A deployment of seven or fewer
|
|
84
|
+
* feeds per host resolves the 111-second threshold; past that the operator
|
|
85
|
+
* needs a second connector host, and `getSensingHealth()` already reports the
|
|
86
|
+
* honest effective interval so that shows up rather than hiding.
|
|
87
|
+
*
|
|
88
|
+
* ── Why not the profile's own capture_interval_ms ───────────────────────────
|
|
89
|
+
*
|
|
90
|
+
* `sensing_profiles.capture_interval_ms` exists and is per-screen, but across
|
|
91
|
+
* the 147 active `custom_intent` profiles it holds 5-30 s. Those are the
|
|
92
|
+
* ANDROID tablet's capture rate, where a frame costs a BlazeFace pass, not a
|
|
93
|
+
* VLM generation. Feeding 5 s to a connector whose ceiling is 493 analyses/hour
|
|
94
|
+
* would demand 11,520 and oversubscribe the single slot ~23x. The profile says
|
|
95
|
+
* what the operator wants; this says what the machine can deliver.
|
|
96
|
+
*/
|
|
97
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
98
|
+
exports.DOOH_PERIODIC_MS = exports.LOOK_INTERVAL_MS = exports.STILL_FLOOR_SLOT_SHARE = exports.MEASURED_LOCAL_ANALYZE_MS = void 0;
|
|
99
|
+
exports.periodicIntervalMsForFeedCount = periodicIntervalMsForFeedCount;
|
|
100
|
+
exports.stillSceneLoad = stillSceneLoad;
|
|
101
|
+
/**
|
|
102
|
+
* Measured median seconds per on-device analysis, in ms. See the header for the
|
|
103
|
+
* measurement. Re-measure and update this when the model tier, the delegate, or
|
|
104
|
+
* the SOP schema changes — it is the only free variable here.
|
|
105
|
+
*/
|
|
106
|
+
exports.MEASURED_LOCAL_ANALYZE_MS = 7300;
|
|
107
|
+
/**
|
|
108
|
+
* Share of the single admission slot the STILL-scene heartbeat floor may take.
|
|
109
|
+
* The rest is held for motion-triggered analyses.
|
|
110
|
+
*/
|
|
111
|
+
exports.STILL_FLOOR_SLOT_SHARE = 0.5;
|
|
112
|
+
/**
|
|
113
|
+
* The connector's look cadence (`faceTriggeredIntervalMs` default, which is
|
|
114
|
+
* also the motion gate's scan interval). A heartbeat can never be finer.
|
|
115
|
+
*/
|
|
116
|
+
exports.LOOK_INTERVAL_MS = 30000;
|
|
117
|
+
/** The DOOH audience default (`DEFAULT_PERIODIC_MS`). The slow end of the clamp. */
|
|
118
|
+
exports.DOOH_PERIODIC_MS = 300000;
|
|
119
|
+
/**
|
|
120
|
+
* Sustainable still-scene heartbeat for a connector running `feedCount` feeds
|
|
121
|
+
* against ONE local inference slot.
|
|
122
|
+
*
|
|
123
|
+
* `feedCount` MUST be the connector's TOTAL feed count after any merge, not the
|
|
124
|
+
* feeds added by one setup session — see the header.
|
|
125
|
+
*
|
|
126
|
+
* Returns `null` when the count is not a positive integer, so a caller can omit
|
|
127
|
+
* the key entirely rather than write a cadence derived from a bad count.
|
|
128
|
+
*/
|
|
129
|
+
function periodicIntervalMsForFeedCount(feedCount) {
|
|
130
|
+
const n = Number(feedCount);
|
|
131
|
+
if (!Number.isFinite(n) || n < 1)
|
|
132
|
+
return null;
|
|
133
|
+
const required = Math.ceil((Math.trunc(n) * exports.MEASURED_LOCAL_ANALYZE_MS) / exports.STILL_FLOOR_SLOT_SHARE);
|
|
134
|
+
return Math.min(exports.DOOH_PERIODIC_MS, Math.max(exports.LOOK_INTERVAL_MS, required));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The still-scene analyses/hour a whole connector demands at a heartbeat, and
|
|
138
|
+
* what fraction of the single slot that is. Exposed so the derivation is
|
|
139
|
+
* assertable in tests and readable in a log line rather than being folded away
|
|
140
|
+
* inside one expression.
|
|
141
|
+
*/
|
|
142
|
+
function stillSceneLoad(feedCount, periodicIntervalMs) {
|
|
143
|
+
const n = Math.max(0, Math.trunc(Number(feedCount) || 0));
|
|
144
|
+
const h = Number(periodicIntervalMs);
|
|
145
|
+
if (!Number.isFinite(h) || h <= 0)
|
|
146
|
+
return { analysesPerHour: 0, slotShare: 0 };
|
|
147
|
+
const analysesPerHour = (n * 3600000) / h;
|
|
148
|
+
const ceilingPerHour = 3600000 / exports.MEASURED_LOCAL_ANALYZE_MS;
|
|
149
|
+
return { analysesPerHour, slotShare: analysesPerHour / ceilingPerHour };
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=connectorCadence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectorCadence.js","sourceRoot":"","sources":["../src/connectorCadence.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;;;AAkCH,wEAKC;AAQD,wCAUC;AAvDD;;;;GAIG;AACU,QAAA,yBAAyB,GAAG,IAAK,CAAC;AAE/C;;;GAGG;AACU,QAAA,sBAAsB,GAAG,GAAG,CAAC;AAE1C;;;GAGG;AACU,QAAA,gBAAgB,GAAG,KAAM,CAAC;AAEvC,oFAAoF;AACvE,QAAA,gBAAgB,GAAG,MAAO,CAAC;AAExC;;;;;;;;;GASG;AACH,SAAgB,8BAA8B,CAAC,SAAkB;IAC/D,MAAM,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,iCAAyB,CAAC,GAAG,8BAAsB,CAAC,CAAC;IACjG,OAAO,IAAI,CAAC,GAAG,CAAC,wBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,wBAAgB,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;GAKG;AACH,SAAgB,cAAc,CAC5B,SAAkB,EAClB,kBAA2B;IAE3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAC/E,MAAM,eAAe,GAAG,CAAC,CAAC,GAAG,OAAS,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,cAAc,GAAG,OAAS,GAAG,iCAAyB,CAAC;IAC7D,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe,GAAG,cAAc,EAAE,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk roots for the agent, in their OWN module.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not in EdgeAgent.ts: combinedSense.ts lazy-`require`s EdgeAgent so
|
|
5
|
+
* the module can be loaded and unit-tested WITHOUT the ~200 MB onnxruntime stack
|
|
6
|
+
* (see that file's header, and the cli `start` arm that keeps the
|
|
7
|
+
* cloud-sense/pair connector bundle free of it). A static
|
|
8
|
+
* `import { tileDataDir } from './EdgeAgent'` would have made every consumer of
|
|
9
|
+
* combinedSense pull the ONNX stack eagerly and undone that.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The agent's data root. ONE definition, because per-screen isolation depends on
|
|
13
|
+
* it: a mosaic fans out N EdgeAgents that each open `signal_buffer.db`, a
|
|
14
|
+
* SubsystemHealthRegistry and a persistenceDir beneath this path, so a second
|
|
15
|
+
* copy of the default that drifted would silently re-collide the tiles
|
|
16
|
+
* tileDataDir() exists to separate.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveDataDir(configured?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Per-tile data directory for a mosaic fan-out.
|
|
21
|
+
*
|
|
22
|
+
* Each tile is its own EdgeAgent authenticating with its OWN deviceToken, but the
|
|
23
|
+
* `...config` spread handed them all the same dataDir — so they opened one shared
|
|
24
|
+
* signal_buffer.db. SignalBuffer.flushBuffer() reads rows and DELETES them on
|
|
25
|
+
* success, so during an offline/reconnect window screen A's buffered signals
|
|
26
|
+
* could be emitted under screen B's token, or dropped by whichever tile flushed
|
|
27
|
+
* first. This bit even with dataDir unset, since the default is shared too.
|
|
28
|
+
*
|
|
29
|
+
* Keyed on screenId — stable across restarts, which is what makes a persisted
|
|
30
|
+
* buffer worth having. The index is only a fallback for a feed with no screenId.
|
|
31
|
+
*/
|
|
32
|
+
export declare function tileDataDir(configured: string | undefined, screenId: string | undefined, index: number): string;
|
|
33
|
+
//# sourceMappingURL=dataDir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataDir.d.ts","sourceRoot":"","sources":["../src/dataDir.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,GACZ,MAAM,CAGR"}
|
package/dist/dataDir.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveDataDir = resolveDataDir;
|
|
37
|
+
exports.tileDataDir = tileDataDir;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* On-disk roots for the agent, in their OWN module.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not in EdgeAgent.ts: combinedSense.ts lazy-`require`s EdgeAgent so
|
|
43
|
+
* the module can be loaded and unit-tested WITHOUT the ~200 MB onnxruntime stack
|
|
44
|
+
* (see that file's header, and the cli `start` arm that keeps the
|
|
45
|
+
* cloud-sense/pair connector bundle free of it). A static
|
|
46
|
+
* `import { tileDataDir } from './EdgeAgent'` would have made every consumer of
|
|
47
|
+
* combinedSense pull the ONNX stack eagerly and undone that.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* The agent's data root. ONE definition, because per-screen isolation depends on
|
|
51
|
+
* it: a mosaic fans out N EdgeAgents that each open `signal_buffer.db`, a
|
|
52
|
+
* SubsystemHealthRegistry and a persistenceDir beneath this path, so a second
|
|
53
|
+
* copy of the default that drifted would silently re-collide the tiles
|
|
54
|
+
* tileDataDir() exists to separate.
|
|
55
|
+
*/
|
|
56
|
+
function resolveDataDir(configured) {
|
|
57
|
+
return configured || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.trillboards');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Per-tile data directory for a mosaic fan-out.
|
|
61
|
+
*
|
|
62
|
+
* Each tile is its own EdgeAgent authenticating with its OWN deviceToken, but the
|
|
63
|
+
* `...config` spread handed them all the same dataDir — so they opened one shared
|
|
64
|
+
* signal_buffer.db. SignalBuffer.flushBuffer() reads rows and DELETES them on
|
|
65
|
+
* success, so during an offline/reconnect window screen A's buffered signals
|
|
66
|
+
* could be emitted under screen B's token, or dropped by whichever tile flushed
|
|
67
|
+
* first. This bit even with dataDir unset, since the default is shared too.
|
|
68
|
+
*
|
|
69
|
+
* Keyed on screenId — stable across restarts, which is what makes a persisted
|
|
70
|
+
* buffer worth having. The index is only a fallback for a feed with no screenId.
|
|
71
|
+
*/
|
|
72
|
+
function tileDataDir(configured, screenId, index) {
|
|
73
|
+
const key = (screenId && String(screenId).replace(/[^A-Za-z0-9._-]/g, '_')) || `tile-${index}`;
|
|
74
|
+
return path.join(resolveDataDir(configured), 'tiles', key);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=dataDir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataDir.js","sourceRoot":"","sources":["../src/dataDir.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,wCAKC;AAeD,kCAOC;AA/CD,2CAA6B;AAE7B;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,UAAmB;IAChD,OAAO,UAAU,IAAI,IAAI,CAAC,IAAI,CAC5B,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,MAAM,EACrD,cAAc,CACf,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,WAAW,CACzB,UAA8B,EAC9B,QAA4B,EAC5B,KAAa;IAEb,MAAM,GAAG,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,IAAI,QAAQ,KAAK,EAAE,CAAC;IAC/F,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deployedCadence — reconcile the cadence the OPERATOR deployed with the one
|
|
3
|
+
* this machine can actually afford to run.
|
|
4
|
+
*
|
|
5
|
+
* ── The bug this exists to close ────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `sensing_profiles.capture_interval_ms` is per-screen, is set from the portal,
|
|
8
|
+
* and IS served to the connector: `GET /v2/earner/cameras/sensing-profile`
|
|
9
|
+
* returns it on a 5-minute TTL. The connector then throws it away. It is parsed
|
|
10
|
+
* in `SensingProfileClient.fetchProfile()` and never read again — the local
|
|
11
|
+
* refresh consumes `profileName`, `profileType` and `fields` and drops
|
|
12
|
+
* `captureIntervalMs` on the floor, and the CLOUD path does not even construct a
|
|
13
|
+
* profile client. So a venue that sets "look every 10 seconds" in the portal
|
|
14
|
+
* gets 300 000 ms forever, and nothing anywhere says so.
|
|
15
|
+
*
|
|
16
|
+
* Worse, the ONLY other way in is one-shot: `mergeConfigFile` honours a cadence
|
|
17
|
+
* at SETUP time, writes it to `trillboards.config.json`, and
|
|
18
|
+
* `CloudSensingManager` freezes it at construction. Changing a deployed camera's
|
|
19
|
+
* cadence today means editing a file on the venue's machine and restarting the
|
|
20
|
+
* process.
|
|
21
|
+
*
|
|
22
|
+
* ── Why it is not simply "use what the profile says" ───────────────────────
|
|
23
|
+
*
|
|
24
|
+
* Because the two run modes are metered by completely different things, and
|
|
25
|
+
* neither of them is time.
|
|
26
|
+
*
|
|
27
|
+
* LOCAL is metered by a MACHINE. `litert-lm serve` is a single-conversation
|
|
28
|
+
* server with exactly one admission slot at a measured 7.30 s/frame, so the
|
|
29
|
+
* whole connector has a hard ceiling of 493 analyses/hour however many cameras
|
|
30
|
+
* it runs. That ceiling is what `connectorCadence.periodicIntervalMsForFeedCount`
|
|
31
|
+
* already computes, and it is why the profile's own 5-30 s values were rejected
|
|
32
|
+
* as a cadence source when that module was written: they are the ANDROID
|
|
33
|
+
* tablet's capture rate, where a frame costs a BlazeFace pass rather than a VLM
|
|
34
|
+
* generation, and feeding 5 s to a connector would demand 11 520 analyses/hour
|
|
35
|
+
* against a budget of 493.
|
|
36
|
+
*
|
|
37
|
+
* CLOUD is metered by MONEY, and this is the number nobody had:
|
|
38
|
+
*
|
|
39
|
+
* Measured on production 2026-08-12 — 255 812 image-bearing cloud VLM calls
|
|
40
|
+
* over 7 days, `gemini-3.1-flash-lite` via `audienceVisionService.analyzeFrame`
|
|
41
|
+
* — the mean cost of one analyzed frame is $0.007008 (p50 $0.006838, p90
|
|
42
|
+
* $0.008481; $1 790.92 of spend over the window). Per camera per 30-day month:
|
|
43
|
+
*
|
|
44
|
+
* cadence frames/mo $/camera/mo
|
|
45
|
+
* 300 s 8 640 60.55 <- today's silent default
|
|
46
|
+
* 120 s 21 600 151.37
|
|
47
|
+
* 60 s 43 200 302.75
|
|
48
|
+
* 30 s 86 400 605.49
|
|
49
|
+
*
|
|
50
|
+
* Against a $100-500 subscription. So "sensing within 30 seconds" is not a
|
|
51
|
+
* config default on the cloud path — it is a $605/camera/month line item, and
|
|
52
|
+
* a 16-camera store would be $9 688/month of COGS. The same 30 s cadence on
|
|
53
|
+
* the LOCAL path costs nothing per frame; it costs one machine.
|
|
54
|
+
*
|
|
55
|
+
* The honest resolution is therefore not to pick a number. It is to let the
|
|
56
|
+
* operator's request through, clamp it at the floor its OWN mode implies, and
|
|
57
|
+
* make the clamp legible — `resolve()` returns the reason and the projected
|
|
58
|
+
* bill, so `getSensingHealth()` can say "you asked for 30 s; running 303 s;
|
|
59
|
+
* cloud budget $60/camera/month" instead of silently sampling every 5 minutes.
|
|
60
|
+
*
|
|
61
|
+
* ── The default is deliberately a no-op ────────────────────────────────────
|
|
62
|
+
*
|
|
63
|
+
* `DEFAULT_CLOUD_MONTHLY_BUDGET_USD_PER_CAMERA` is 60, which at the measured
|
|
64
|
+
* price is a 302 746 ms floor — within 1% of the 300 000 ms every cloud
|
|
65
|
+
* connector already runs. Fixing the wire therefore cannot change anybody's
|
|
66
|
+
* bill: it changes who is ALLOWED to change it, and makes buying a faster
|
|
67
|
+
* cadence an explicit purchase instead of an invisible one.
|
|
68
|
+
*/
|
|
69
|
+
export type SensingRunModeName = 'cloud' | 'local';
|
|
70
|
+
/**
|
|
71
|
+
* Mean USD per analyzed frame on the cloud VLM path. MEASURED, not listed:
|
|
72
|
+
* 255,812 successful image-bearing `vision_call_log` rows over the 7 days ending
|
|
73
|
+
* 2026-08-12. Re-measure with:
|
|
74
|
+
*
|
|
75
|
+
* SELECT avg(cost_usd) FROM vision_call_log
|
|
76
|
+
* WHERE called_at >= now() - interval '7 days'
|
|
77
|
+
* AND success AND image_s3_key IS NOT NULL;
|
|
78
|
+
*
|
|
79
|
+
* and update this when the model tier or the profile schema size changes — a
|
|
80
|
+
* bigger response schema moves it (per-screen means ranged $0.0054-$0.0158 over
|
|
81
|
+
* the same window, tracking prompt size).
|
|
82
|
+
*/
|
|
83
|
+
export declare const MEASURED_CLOUD_USD_PER_FRAME = 0.007008;
|
|
84
|
+
/**
|
|
85
|
+
* Cloud spend per camera per month the connector may commit to WITHOUT the
|
|
86
|
+
* operator raising it. 60 USD is today's actual spend at the 300 s default, so
|
|
87
|
+
* this default changes no bill; it exists so that going faster is a decision
|
|
88
|
+
* somebody makes rather than a constant somebody edits.
|
|
89
|
+
*/
|
|
90
|
+
export declare const DEFAULT_CLOUD_MONTHLY_BUDGET_USD_PER_CAMERA = 60;
|
|
91
|
+
/** A 30-day month, in ms — the unit the budget is quoted in. */
|
|
92
|
+
export declare const MONTH_MS: number;
|
|
93
|
+
/**
|
|
94
|
+
* The SLOWEST cadence a deployed profile may put on a running connector.
|
|
95
|
+
*
|
|
96
|
+
* This is not a taste bound, it is a MECHANISM bound. The cadence becomes the
|
|
97
|
+
* `setTimeout` delay of every capture loop, and Node's delay is a 32-bit signed
|
|
98
|
+
* int: 2 147 483 648 ms does not wrap to "a long time", it SILENTLY becomes 1 ms
|
|
99
|
+
* and fires on the next tick. So the one input the server controls end-to-end —
|
|
100
|
+
* `sensing_profiles.capture_interval_ms`, a plain integer column with no CHECK —
|
|
101
|
+
* inverts into its exact opposite: a row that reads "look once every 25 days"
|
|
102
|
+
* turns the connector into a hot loop that pulls every camera as fast as ffmpeg
|
|
103
|
+
* can decode. Measured through the real manager: 91 RTSP grabs in 120 ms of wall
|
|
104
|
+
* clock, i.e. ~750 frames/second per feed, which on the cloud path is the whole
|
|
105
|
+
* $60/camera/month budget in about eleven seconds.
|
|
106
|
+
*
|
|
107
|
+
* Nothing upstream stops it. `resolveDeployedCadence` clamped only the FLOOR
|
|
108
|
+
* (going too fast costs money), so every value above the floor was passed
|
|
109
|
+
* through verbatim — including `Number.MAX_SAFE_INTEGER`. A units mistake (s vs
|
|
110
|
+
* ms, ms vs ns), a bad migration, or a fat-fingered portal field is enough.
|
|
111
|
+
*
|
|
112
|
+
* 24 h is the ceiling because a sensing cadence slower than a day is not a
|
|
113
|
+
* cadence anybody deploys on purpose, and it is two orders of magnitude inside
|
|
114
|
+
* the overflow. It is REPORTED (`reason: 'cadence_ceiling'`) rather than
|
|
115
|
+
* silently applied, so a nonsense row shows up as a nonsense row.
|
|
116
|
+
*/
|
|
117
|
+
export declare const MAX_CADENCE_MS: number;
|
|
118
|
+
export type CadenceReason =
|
|
119
|
+
/** No deployed cadence to honour — the caller keeps exactly what it had. */
|
|
120
|
+
'no_request'
|
|
121
|
+
/** The operator's request was affordable and is being run verbatim. */
|
|
122
|
+
| 'operator_request'
|
|
123
|
+
/** Slowed to fit the cloud spend budget. */
|
|
124
|
+
| 'cloud_cost_budget'
|
|
125
|
+
/** Slowed to fit the single local inference slot. */
|
|
126
|
+
| 'local_inference_budget'
|
|
127
|
+
/** Faster than the connector ever looks — a cadence finer than a look is meaningless. */
|
|
128
|
+
| 'look_interval'
|
|
129
|
+
/** SPED UP to `MAX_CADENCE_MS`: the request was slower than a capture loop can express. */
|
|
130
|
+
| 'cadence_ceiling';
|
|
131
|
+
export interface DeployedCadence {
|
|
132
|
+
/** The interval to run, or null when there was nothing to honour. */
|
|
133
|
+
intervalMs: number | null;
|
|
134
|
+
/** What the deployed profile asked for, normalized. null when it asked nothing. */
|
|
135
|
+
requestedMs: number | null;
|
|
136
|
+
/** The fastest this mode may run. */
|
|
137
|
+
floorMs: number;
|
|
138
|
+
/** The slowest a capture loop can express (see MAX_CADENCE_MS). */
|
|
139
|
+
ceilingMs: number;
|
|
140
|
+
/** True when the request is NOT being run verbatim (floor OR ceiling). */
|
|
141
|
+
clamped: boolean;
|
|
142
|
+
reason: CadenceReason;
|
|
143
|
+
/** Projected cloud spend at `intervalMs`. null for local (no per-frame cost). */
|
|
144
|
+
estimatedMonthlyUsdPerCamera: number | null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The fastest cloud cadence that keeps ONE camera inside `budgetUsd` per month
|
|
148
|
+
* at `usdPerFrame`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function cloudFloorMsForBudget(budgetUsd?: number, usdPerFrame?: number): number;
|
|
151
|
+
/** Projected cloud spend for one camera at `intervalMs`. */
|
|
152
|
+
export declare function monthlyUsdPerCamera(intervalMs: number, usdPerFrame?: number): number;
|
|
153
|
+
/**
|
|
154
|
+
* Reconcile a deployed cadence with what this mode can afford.
|
|
155
|
+
*
|
|
156
|
+
* Returns `intervalMs: null` when the profile requested nothing — the caller
|
|
157
|
+
* MUST then leave its current cadence alone, so a connector whose server says
|
|
158
|
+
* nothing behaves exactly as it does today.
|
|
159
|
+
*/
|
|
160
|
+
export declare function resolveDeployedCadence(input: {
|
|
161
|
+
requestedMs: unknown;
|
|
162
|
+
mode: SensingRunModeName;
|
|
163
|
+
feedCount: number;
|
|
164
|
+
monthlyBudgetUsdPerCamera?: number;
|
|
165
|
+
usdPerFrame?: number;
|
|
166
|
+
}): DeployedCadence;
|
|
167
|
+
//# sourceMappingURL=deployedCadence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployedCadence.d.ts","sourceRoot":"","sources":["../src/deployedCadence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAQH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,WAAW,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,2CAA2C,KAAK,CAAC;AAE9D,gEAAgE;AAChE,eAAO,MAAM,QAAQ,QAAwB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,cAAc,QAAmB,CAAC;AAE/C,MAAM,MAAM,aAAa;AACvB,4EAA4E;AAC1E,YAAY;AACd,uEAAuE;GACrE,kBAAkB;AACpB,4CAA4C;GAC1C,mBAAmB;AACrB,qDAAqD;GACnD,wBAAwB;AAC1B,yFAAyF;GACvF,eAAe;AACjB,2FAA2F;GACzF,iBAAiB,CAAC;AAEtB,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mFAAmF;IACnF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,iFAAiF;IACjF,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,GAAE,MAAoD,EAC/D,WAAW,GAAE,MAAqC,GACjD,MAAM,CAUR;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,MAAqC,GACjD,MAAM,CAIR;AASD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,kBAAkB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,eAAe,CAsDlB"}
|