@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.
Files changed (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +122 -65
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalSensingClient = void 0;
4
+ exports.fieldSetForProfileType = fieldSetForProfileType;
5
+ exports.isAlertWorthy = isAlertWorthy;
6
+ exports.alertSignature = alertSignature;
7
+ exports.isCloudEscalationEnabled = isCloudEscalationEnabled;
8
+ exports.buildLocalSensingLoops = buildLocalSensingLoops;
9
+ const AudienceAnalyzeClient_1 = require("./AudienceAnalyzeClient");
10
+ const LiteRtVlmProvider_1 = require("./LiteRtVlmProvider");
11
+ const SensingProfileClient_1 = require("./SensingProfileClient");
12
+ const CloudSensingManager_1 = require("./CloudSensingManager");
13
+ const localInferenceSchema_1 = require("./localInferenceSchema");
14
+ const CloudEscalationPolicy_1 = require("./CloudEscalationPolicy");
15
+ const legacyRecorderProfile_1 = require("./legacyRecorderProfile");
16
+ /** How long a fetched profile is cached before the connector re-checks for a new alert. */
17
+ const DEFAULT_PROFILE_TTL_MS = 5 * 60 * 1000;
18
+ /**
19
+ * The server's own name for a Sense-Anything profile — one an operator authored
20
+ * from their venue intent, as opposed to a seeded DOOH audience preset.
21
+ * `sensingProfileService.createProfile` defaults `profile_type` to this and
22
+ * derives `profile_origin='custom_intent'` from it; 147 of the 157 active
23
+ * profiles carried it on 2026-08-11.
24
+ */
25
+ const CUSTOM_INTENT_PROFILE_TYPE = 'custom_intent';
26
+ /**
27
+ * Map a deployed profile's SELF-DESCRIBING type to the on-device field set.
28
+ *
29
+ * A Sense-Anything (`custom_intent`) profile asked a specific operational
30
+ * question about the operator's own venue, so the on-device model is asked for
31
+ * that profile's fields plus the presence primitives — NOT the 12 DOOH audience
32
+ * fields, which answer nothing the operator asked and cost 1.09-1.53x the
33
+ * per-frame latency depending on delegate (measured; see localInferenceSchema's
34
+ * header). Every other type — a seeded builtin preset, or NO deployed profile at
35
+ * all — is a screen/audience measurement deployment and keeps the full audience
36
+ * contract.
37
+ *
38
+ * This reads the kind the SERVER already reports rather than an allowlist of
39
+ * profile names maintained on the device.
40
+ */
41
+ function fieldSetForProfileType(profileType) {
42
+ return profileType === CUSTOM_INTENT_PROFILE_TYPE ? 'operator' : 'audience';
43
+ }
44
+ /**
45
+ * Sentinel values the schema uses for an EMPTY scene (see localInferenceSchema):
46
+ * audience_* enums collapse to 'none'/'unclear', crowd_density to 'empty',
47
+ * narratives to '' when no people are present. A custom enum's "nothing here"
48
+ * value typically reads the same ('empty'/'unknown'). Treated as "no signal" so
49
+ * a blank room doesn't ship a frame.
50
+ */
51
+ const EMPTY_SCENE_SENTINELS = new Set(['', 'none', 'unclear', 'empty', 'unknown']);
52
+ /**
53
+ * Keys ignored by the gate — they don't DISCRIMINATE an empty scene from a busy
54
+ * one, so gating on them would attach a frame to every observation (defeating
55
+ * the privacy filter):
56
+ * - source / captured_at: connector-stamped provenance + time (never a signal);
57
+ * - confidence: a meta score the model emits ~always > 0 even for a blank room;
58
+ * - scene_description: free text that is non-empty even when the scene is empty
59
+ * ("an empty room") — not a meaningful route equality target.
60
+ * Discriminative scene fields (person_count, people_present, crowd_density,
61
+ * ad_receptivity_score, the audience_* enums, the narrative) are NOT ignored:
62
+ * each reads its empty-sentinel/0/false for a blank scene, so they correctly
63
+ * contribute no signal when empty but DO when people are present.
64
+ */
65
+ const NON_DISCRIMINATIVE_KEYS = new Set(['source', 'captured_at', 'confidence', 'scene_description']);
66
+ /**
67
+ * Decide whether an on-device result is potentially ALERT-WORTHY — the gate that
68
+ * keeps the evidence frame OFF the wire for an EMPTY/uninteresting scene (privacy
69
+ * + bandwidth) while still attaching it for any observation an operator route
70
+ * could fire on. Sense-Anything routes evaluate gt/gte/lt/lte/eq/neq over
71
+ * numbers (counts AND gauges), booleans, and strings/enums (routeEvaluatorService
72
+ * `compare()`), so the gate cannot be limited to "true boolean / positive count"
73
+ * — a gauge route (`avg_wait_minutes gt 5`) or an enum route
74
+ * (`shelf_state eq "empty"`) would otherwise fire WITHOUT a snapshot.
75
+ *
76
+ * A frame is attached when the result carries ANY meaningful signal:
77
+ * - any boolean field that is `true` (safety_concern, people_present, a custom
78
+ * shelf_empty, …);
79
+ * - any positive finite number — a count (person_count / *_count) OR a gauge
80
+ * (avg_wait_minutes, ad_receptivity_score, …);
81
+ * - any non-empty array (a populated `list` custom field), or non-empty object
82
+ * (a `distribution` field);
83
+ * - any string/enum whose value is NOT an empty-scene sentinel
84
+ * (''/none/unclear/empty/unknown) — e.g. shelf_state='low'.
85
+ * Non-discriminative keys (source, captured_at, confidence, scene_description)
86
+ * are ignored — see NON_DISCRIMINATIVE_KEYS for why.
87
+ *
88
+ * KNOWN BOUNDARY (deliberate privacy tradeoff): a route that alerts on ABSENCE
89
+ * — `person_count lte 0`, `people_present eq false`, `shelf_state eq "empty"` —
90
+ * fires on a genuinely empty scene, which this gate suppresses by design (a
91
+ * blank-room frame is exactly the privacy harm the gate exists to prevent). The
92
+ * server owns the route predicates and can request a frame for those routes;
93
+ * operators who want every frame can disable the gate (attachEvidenceFrame).
94
+ */
95
+ function isAlertWorthy(result) {
96
+ if (!result || typeof result !== 'object')
97
+ return false;
98
+ for (const [key, value] of Object.entries(result)) {
99
+ if (NON_DISCRIMINATIVE_KEYS.has(key))
100
+ continue;
101
+ if (value === true)
102
+ return true;
103
+ if (typeof value === 'number') {
104
+ if (Number.isFinite(value) && value > 0)
105
+ return true;
106
+ continue;
107
+ }
108
+ if (typeof value === 'string') {
109
+ const v = value.trim().toLowerCase();
110
+ if (v.length > 0 && !EMPTY_SCENE_SENTINELS.has(v))
111
+ return true;
112
+ continue;
113
+ }
114
+ if (Array.isArray(value)) {
115
+ if (value.length > 0)
116
+ return true;
117
+ continue;
118
+ }
119
+ if (value && typeof value === 'object') {
120
+ if (Object.keys(value).length > 0)
121
+ return true;
122
+ continue;
123
+ }
124
+ }
125
+ return false;
126
+ }
127
+ /**
128
+ * A stable fingerprint of the ALERT-BEARING state of one result — "has the
129
+ * operator-visible situation changed?" in one comparable string.
130
+ *
131
+ * Built from EXACTLY the keys `isAlertWorthy` judges (NON_DISCRIMINATIVE_KEYS
132
+ * excluded), so the two can never drift: the same fields that decide whether a
133
+ * frame carries signal decide whether that signal is NEW. Keys are sorted so
134
+ * the model's property order cannot manufacture a change, and values are
135
+ * compared as emitted — a `queue_length` of 3 and of 4 are genuinely different
136
+ * facts and an operator route may key on exactly that boundary, so this
137
+ * deliberately does NOT bucket numbers into invented bands.
138
+ *
139
+ * Used by CloudEscalationPolicy's novelty bound: an unchanged signature means a
140
+ * verdict the cloud has already adjudicated, so the frame stays on-premises.
141
+ */
142
+ function alertSignature(result) {
143
+ if (!result || typeof result !== 'object')
144
+ return '';
145
+ const parts = [];
146
+ for (const key of Object.keys(result).sort()) {
147
+ if (NON_DISCRIMINATIVE_KEYS.has(key))
148
+ continue;
149
+ const value = result[key];
150
+ // Arrays/objects (a `list` / `distribution` custom field) serialize
151
+ // structurally; scalars stringify. Undefined is elided so an absent key and
152
+ // an explicitly-undefined key read the same.
153
+ if (value === undefined)
154
+ continue;
155
+ parts.push(`${key}=${typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value)}`);
156
+ }
157
+ return parts.join('|');
158
+ }
159
+ /**
160
+ * Is cloud escalation actually ON for this local-sensing configuration?
161
+ *
162
+ * TWO settings decide it, and this is the ONLY place that combines them:
163
+ * `cloudEscalation.enabled` opts in, and `attachEvidenceFrame: false` — the
164
+ * operator's "no frame ever leaves this machine" switch — outranks it, because
165
+ * escalation sends a frame. `LocalSensingClient` enforces the rule and the
166
+ * startup banner reports it; they must read the same expression or the banner
167
+ * will one day claim an egress posture the client is not running.
168
+ */
169
+ function isCloudEscalationEnabled(options) {
170
+ if (!options)
171
+ return false;
172
+ return options.cloudEscalation?.enabled === true && options.attachEvidenceFrame !== false;
173
+ }
174
+ /**
175
+ * LocalSensingClient — the SensingClient the cadence loop uses in LOCAL mode.
176
+ * It runs the VLM ON-DEVICE (LiteRtVlmProvider — Gemma 4 on LiteRT-LM via
177
+ * plain-completion + client-side schema validation, the real CCTV runtime) and
178
+ * POSTs the STRUCTURED result as `preAnalyzed` (uploadClient.analyzePreAnalyzed),
179
+ * skipping the cloud VLM. Implements the same `analyze({jpeg,captureMode})`
180
+ * shape AudienceAnalyzeClient exposes, so CloudSensingManager runs it UNCHANGED.
181
+ *
182
+ * PROFILE-DRIVEN: before analyzing, it fetches (and caches) the screen's active
183
+ * Sense-Anything profile via SensingProfileClient and rebuilds the constrained
184
+ * schema + prompt to include the operator's CUSTOM fields — the on-device
185
+ * equivalent of the cloud `buildProfileDrivenJsonSchema`. So a deployed alert
186
+ * like "queue_length >= 3" makes Gemma emit `queue_length`, which flows through
187
+ * preAnalyzed → OFV → the route → the webhook, exactly like the cloud VLM. The
188
+ * fetch is best-effort: on any failure the last-good (or default) schema is kept,
189
+ * so on-device sensing NEVER goes silent.
190
+ *
191
+ * The connector OVERRIDES `source='vlm_edge'` and `captured_at` (the actual
192
+ * capture time) — never trusting the model for provenance/time. The SERVER
193
+ * additionally forces provenance `local_connector` on the preAnalyzed ingest,
194
+ * gating local-inferred data out of the sold bidstream while still firing the
195
+ * operator's Sense-Anything routes.
196
+ *
197
+ * ── CLOUD ESCALATION (opt-in, Phase 6d) ─────────────────────────────────────
198
+ * Local Gemma is the always-on gate; the CLOUD is the judge for the frames it
199
+ * flags. When `cloudEscalation.enabled` and the policy admits this frame, the
200
+ * SAME JPEG is POSTed through the SAME upload client's `analyze()` — the cloud
201
+ * path the connector already owns — and the cloud VLM's verdict becomes the
202
+ * observation. Three properties are load-bearing:
203
+ *
204
+ * 1. ONE FRAME, ONE OBSERVATION. An escalated frame does NOT also post its
205
+ * local `preAnalyzed` result. Posting both would double-count the same
206
+ * physical frame and put two verdicts for one moment in the stream —
207
+ * exactly the dual write the codebase forbids. The cloud verdict
208
+ * supersedes; the local one was a sampling decision, not a datum.
209
+ * 2. NO PROVENANCE LAUNDERING. What lands is what the CLOUD inferred from a
210
+ * real frame, so the server's normal cloud provenance is the truthful
211
+ * stamp — identical to `cloud-sense` and to the existing
212
+ * `cloudFallbackOnError` frame. Nothing the local model produced enters
213
+ * that record, so `ingest_source='local_connector'` (which exists to keep
214
+ * operator-forgeable structured input out of the sold bidstream) does not
215
+ * apply and is not evaded: there IS no local structured input on this path.
216
+ * 3. THE PRIVACY GATE IS UNCHANGED. Escalation reuses `isAlertWorthy`, so the
217
+ * set of frames that can leave the premises is exactly the set that
218
+ * already leaves as evidence photos — and `attachEvidenceFrame: false`
219
+ * (the operator's "no frame ever leaves" switch) disables escalation
220
+ * outright. An empty scene never escalates.
221
+ *
222
+ * A failed cloud call falls back to posting the local result, so an escalation
223
+ * can degrade the ACCURACY of one frame but can never LOSE it.
224
+ */
225
+ class LocalSensingClient {
226
+ constructor(localProvider, uploadClient, options = {}) {
227
+ this.localProvider = localProvider;
228
+ this.uploadClient = uploadClient;
229
+ this.lastProfileFetchMs = 0;
230
+ /** The active profile name (for logging / observability). */
231
+ this.activeProfileName = null;
232
+ /**
233
+ * Which field set the active profile resolved to (for logging / observability):
234
+ * 'operator' for a Sense-Anything profile, 'audience' for a DOOH preset or no
235
+ * profile. See fieldSetForProfileType.
236
+ */
237
+ this.activeFieldSet = 'audience';
238
+ this.profileClient = options.profileClient;
239
+ this.profileTtlMs = options.profileTtlMs ?? DEFAULT_PROFILE_TTL_MS;
240
+ this.baseSchema = options.schema ?? (0, localInferenceSchema_1.buildLocalSensingSchema)();
241
+ this.basePrompt = options.prompt ?? (0, localInferenceSchema_1.buildLocalSensingPrompt)();
242
+ this.cachedSchema = this.baseSchema;
243
+ this.cachedPrompt = this.basePrompt;
244
+ this.attachEvidenceFrame = options.attachEvidenceFrame !== false;
245
+ this.cloudFallbackOnError = options.cloudFallbackOnError === true;
246
+ this.feedName = options.feedName || 'local-sense';
247
+ // `attachEvidenceFrame: false` is the operator saying NO frame may ever
248
+ // leave this machine, and escalation sends a frame — isCloudEscalationEnabled
249
+ // is the one expression that resolves the pair (the startup banner reads the
250
+ // same one, so the two can never disagree).
251
+ this.escalationPolicy = new CloudEscalationPolicy_1.CloudEscalationPolicy({
252
+ ...(options.cloudEscalation || {}),
253
+ enabled: isCloudEscalationEnabled({
254
+ attachEvidenceFrame: this.attachEvidenceFrame,
255
+ cloudEscalation: options.cloudEscalation,
256
+ }),
257
+ });
258
+ }
259
+ getEndpoint() {
260
+ return this.uploadClient.getEndpoint();
261
+ }
262
+ reportCameraHealth(cameraHealth) {
263
+ return this.uploadClient.reportCameraHealth(cameraHealth);
264
+ }
265
+ /**
266
+ * Re-fetch the active profile if the cache is stale and rebuild the schema +
267
+ * prompt from its custom fields AND its KIND. Best-effort: a fetch failure
268
+ * keeps the existing schema (never disrupts an in-flight profile).
269
+ *
270
+ * The field set follows the profile's own reported type (fieldSetForProfileType):
271
+ * a Sense-Anything profile gets its declared fields plus the presence
272
+ * primitives; a seeded DOOH preset — or a profile that declares nothing —
273
+ * keeps the full audience contract.
274
+ */
275
+ async refreshProfileIfStale() {
276
+ if (!this.profileClient)
277
+ return;
278
+ const now = Date.now();
279
+ if (now - this.lastProfileFetchMs < this.profileTtlMs)
280
+ return;
281
+ this.lastProfileFetchMs = now;
282
+ const profile = await this.profileClient.fetchProfile();
283
+ if (profile === null)
284
+ return; // transient error — keep last-good schema
285
+ this.activeProfileName = profile.profileName;
286
+ const fieldSet = fieldSetForProfileType(profile.profileType);
287
+ this.activeFieldSet = fieldSet;
288
+ if (fieldSet === 'operator' || profile.fields.length > 0) {
289
+ this.cachedSchema = (0, localInferenceSchema_1.buildLocalSensingSchema)({ fieldSet, extraFields: profile.fields });
290
+ this.cachedPrompt = (0, localInferenceSchema_1.buildLocalSensingPrompt)({ fieldSet, extraFields: profile.fields });
291
+ }
292
+ else {
293
+ // A DOOH/seeded profile with no custom fields (or no active profile at
294
+ // all) → the default audience schema, byte-for-byte unchanged.
295
+ this.cachedSchema = this.baseSchema;
296
+ this.cachedPrompt = this.basePrompt;
297
+ }
298
+ }
299
+ /**
300
+ * What the on-device runtime resolved about this host's accelerator (which
301
+ * litert-lm delegate it selected, and why it is not on the GPU when it is not).
302
+ * Surfaced so an operator/support can see a silent CPU fallback without
303
+ * log-diving. See LiteRtVlmProvider.getAcceleratorHealth.
304
+ */
305
+ getAcceleratorHealth() {
306
+ return this.localProvider.getAcceleratorHealth();
307
+ }
308
+ /**
309
+ * Cloud-escalation counters for this feed — how many candidate frames were
310
+ * adjudicated by the cloud VLM, and why the rest were not. Read by
311
+ * `CloudSensingManager.getSensingHealth()` so escalation shows up on the
312
+ * operator's periodic capacity line: sending frames off-premises must be
313
+ * VISIBLE, not merely documented.
314
+ */
315
+ getEscalationStats() {
316
+ return this.escalationPolicy.getStats();
317
+ }
318
+ async analyze(request) {
319
+ await this.refreshProfileIfStale();
320
+ let structured;
321
+ try {
322
+ structured = await this.localProvider.analyze({
323
+ jpeg: request.jpeg,
324
+ schema: this.cachedSchema,
325
+ prompt: this.cachedPrompt,
326
+ });
327
+ }
328
+ catch (err) {
329
+ if (!this.cloudFallbackOnError)
330
+ throw err;
331
+ // Per-frame degrade: on-device inference threw for THIS frame, so ship it
332
+ // to the cloud VLM instead of dropping it. Reuses the SAME upload client —
333
+ // the cloud path the connector already has — so no new provider/logic. The
334
+ // server provenance-gates a cloud-served frame exactly like cloud-sense.
335
+ // eslint-disable-next-line no-console
336
+ console.warn(`[local-sense] on-device inference failed for a frame (${err.message}) — ` +
337
+ 'falling back to the cloud VLM for this frame');
338
+ return this.uploadClient.analyze({
339
+ jpeg: request.jpeg,
340
+ capturedAt: Date.now(),
341
+ captureMode: request.captureMode,
342
+ });
343
+ }
344
+ // Connector-controlled provenance + time (don't trust the model for these).
345
+ const capturedAtMs = Date.now();
346
+ // PRIVACY-CONSCIOUS evidence frame: attach the SAME JPEG the VLM analyzed
347
+ // (no re-capture) only when enabled AND the observation is potentially
348
+ // alert-worthy. Empty/uninteresting scenes send results only — no frame.
349
+ // Evaluate over the raw model output (not the provenance-stamped `result`,
350
+ // though both yield the same gate since source/captured_at are strings).
351
+ const hasSignal = isAlertWorthy(structured);
352
+ // ESCALATION: a candidate the local model flagged is adjudicated by the
353
+ // cloud VLM BEFORE it can fire an alert. Same gate as the evidence frame,
354
+ // so no frame leaves that would not already have left; the policy adds the
355
+ // novelty/pacing/budget bounds. On success this frame's observation is the
356
+ // CLOUD's — the local result is deliberately not also posted (one frame,
357
+ // one observation).
358
+ const canEscalate = this.escalationPolicy.isEnabled() && hasSignal;
359
+ if (this.escalationPolicy.tryEscalate({
360
+ hasSignal,
361
+ // Only computed when the frame could actually escalate. This runs on
362
+ // every frame of every camera and the common case — escalation off, or
363
+ // an empty scene — needs no signature at all (tryEscalate rejects on
364
+ // `disabled`/`no_signal` before it reads one).
365
+ signature: canEscalate ? alertSignature(structured) : '',
366
+ }).escalate) {
367
+ try {
368
+ const escalated = await this.uploadClient.analyze({
369
+ jpeg: request.jpeg,
370
+ capturedAt: capturedAtMs,
371
+ captureMode: request.captureMode,
372
+ });
373
+ // Rare by construction (bounded per feed per hour), so log every one:
374
+ // an operator must be able to see each frame that left the premises.
375
+ // eslint-disable-next-line no-console
376
+ console.log(`[${this.feedName}] local candidate escalated to the cloud VLM for confirmation ` +
377
+ `→ HTTP ${escalated.statusCode}${escalated.ok ? '' : ' (NOT OK)'}`);
378
+ if (escalated.ok)
379
+ return escalated;
380
+ // A non-2xx cloud verdict is no verdict at all — fall through and ship
381
+ // the local result rather than losing the observation.
382
+ // eslint-disable-next-line no-console
383
+ console.warn(`[${this.feedName}] cloud escalation returned HTTP ${escalated.statusCode} — ` +
384
+ 'keeping the on-device verdict for this frame');
385
+ }
386
+ catch (err) {
387
+ // eslint-disable-next-line no-console
388
+ console.warn(`[${this.feedName}] cloud escalation failed (${err.message}) — ` +
389
+ 'keeping the on-device verdict for this frame');
390
+ }
391
+ }
392
+ const result = {
393
+ ...structured,
394
+ source: 'vlm_edge',
395
+ captured_at: new Date(capturedAtMs).toISOString(),
396
+ };
397
+ const includeFrame = this.attachEvidenceFrame && hasSignal;
398
+ return this.uploadClient.analyzePreAnalyzed(result, {
399
+ capturedAt: capturedAtMs,
400
+ captureMode: request.captureMode,
401
+ ...(includeFrame ? { jpeg: request.jpeg } : {}),
402
+ });
403
+ }
404
+ }
405
+ exports.LocalSensingClient = LocalSensingClient;
406
+ /**
407
+ * Build per-feed capture loops for LOCAL sensing — one RTSP provider + one
408
+ * LocalSensingClient (local VLM + preAnalyzed upload) per feed, each under its
409
+ * OWN device token. Each client fetches its screen's active Sense-Anything
410
+ * profile so the on-device VLM emits that screen's custom fields. Mirrors
411
+ * buildSensingLoops but swaps the cloud client for the local one.
412
+ */
413
+ function buildLocalSensingLoops(config, deps) {
414
+ const feeds = (0, CloudSensingManager_1.resolveFeeds)(config);
415
+ if (feeds.length === 0) {
416
+ throw new Error('buildLocalSensingLoops: no camera feeds configured (set `cameras: [...]` or `camera.rtspUrl`)');
417
+ }
418
+ const litertLmBaseUrl = config.localSensing?.litertLmBaseUrl;
419
+ const model = config.localSensing?.model;
420
+ // Which litert-lm delegate to run. Default 'auto' (resolve the `,gpu` variant
421
+ // from the live model list and prefer it) — see LiteRtVlmProvider.
422
+ const accelerator = config.localSensing?.accelerator;
423
+ // Evidence frame defaults ON; resolveConfig normalizes it, but treat a bare
424
+ // (un-normalized) config or absent localSensing the same way (explicit false
425
+ // is the only opt-out).
426
+ const attachEvidenceFrame = config.localSensing?.attachEvidenceFrame !== false;
427
+ // Per-frame cloud fallback defaults OFF (opt-in) so the plain local-sense path
428
+ // is unchanged. When true, a frame whose on-device inference throws is POSTed
429
+ // to the cloud VLM (same upload client) rather than dropped.
430
+ const cloudFallbackOnError = config.localSensing?.cloudFallbackOnError === true;
431
+ // Cloud escalation defaults OFF. Each feed gets its OWN policy instance so the
432
+ // per-camera budget is per camera (a shared one would let a busy feed spend a
433
+ // quiet feed's confirmations).
434
+ const cloudEscalation = config.localSensing?.cloudEscalation;
435
+ const rtspFactory = deps?.rtspProviderFactory ??
436
+ ((feed) => {
437
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
438
+ const { RTSPCameraProvider } = require('@trillboards/edge-platform-rtsp');
439
+ return new RTSPCameraProvider({
440
+ rtspUrl: feed.rtspUrl,
441
+ width: feed.width,
442
+ height: feed.height,
443
+ fps: feed.fps,
444
+ transport: feed.transport,
445
+ ffmpegPath: feed.ffmpegPath,
446
+ // Honor a mosaic tile's crop in the per-tile-pull fallback path (shared
447
+ // grouping handles the single-pull case). Without this, a mosaic config
448
+ // in local-sense would send the whole composite for each tile.
449
+ crop: feed.crop,
450
+ outputFormat: feed.outputFormat ?? 'jpeg',
451
+ jpegQuality: feed.jpegQuality,
452
+ ...(deps?.streamUrlResolver
453
+ ? {
454
+ streamUrlResolver: (currentUrl, error) => deps.streamUrlResolver(feed, currentUrl, error),
455
+ }
456
+ : {}),
457
+ ...(0, legacyRecorderProfile_1.legacyRecorderRtspOptions)(feed.sourceType),
458
+ });
459
+ });
460
+ const localProviderFactory = deps?.localProviderFactory ??
461
+ (() => new LiteRtVlmProvider_1.LiteRtVlmProvider({ litertLmBaseUrl, model, accelerator }));
462
+ const uploadClientFactory = deps?.uploadClientFactory ??
463
+ ((feed) => new AudienceAnalyzeClient_1.AudienceAnalyzeClient({
464
+ apiBaseUrl: config.apiBaseUrl,
465
+ deviceToken: feed.deviceToken,
466
+ screenId: feed.screenId,
467
+ imageWidth: feed.width,
468
+ imageHeight: feed.height,
469
+ }, deps?.httpPoster));
470
+ // Each feed fetches its OWN screen's active profile (device token binds to one
471
+ // screen). Falls back to the default schema if the profile fetch ever fails.
472
+ const profileClientFactory = deps?.profileClientFactory ??
473
+ ((feed) => new SensingProfileClient_1.SensingProfileClient({ apiBaseUrl: config.apiBaseUrl, deviceToken: feed.deviceToken }, deps?.httpPoster));
474
+ // Mosaic-aware (shared with cloud sensing): collapses tiles of one source onto
475
+ // a single shared pull + crops each tile, so local-sense honors mosaic configs
476
+ // identically to cloud-sense. `sharedSources` lets the VLM attach to the SAME
477
+ // pull the on-device zoo uses (combined-sense), so one RTSP pull feeds both.
478
+ return (0, CloudSensingManager_1.buildTiledCameras)(feeds, rtspFactory, {
479
+ sharedSources: deps?.sharedSources,
480
+ }).map(({ feed, camera }) => ({
481
+ name: feed.name,
482
+ camera,
483
+ client: new LocalSensingClient(localProviderFactory(feed), uploadClientFactory(feed), {
484
+ profileClient: profileClientFactory(feed),
485
+ attachEvidenceFrame,
486
+ cloudFallbackOnError,
487
+ feedName: feed.name,
488
+ ...(cloudEscalation ? { cloudEscalation } : {}),
489
+ }),
490
+ }));
491
+ }
492
+ //# sourceMappingURL=localSensing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localSensing.js","sourceRoot":"","sources":["../src/localSensing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Is this file actually an ONNX model?
3
+ *
4
+ * THE FAILURE THIS PREVENTS
5
+ * -------------------------------------------------------------------------
6
+ * `models.trillboards.com` was never provisioned. The `trillboards.com` zone
7
+ * has no `models` record, so the name is caught by the `*.trillboards.com`
8
+ * wildcard A-alias and answered by the application ALB, which returns the
9
+ * marketing SPA's `index.html` — HTTP **200**, `content-type: text/html`,
10
+ * 2490 bytes — for every path under it.
11
+ *
12
+ * Every consumer of that URL treated 200 as success and, at most, checked a
13
+ * SIZE FLOOR:
14
+ *
15
+ * deploy/docker/Dockerfile.{cpu,cuda,openvino} `if (size < 1000) reject`
16
+ * scripts/download-models.sh `curl -f` (does not fail on 200)
17
+ * src/cli.ts / src/init.ts statusCode !== 200 only
18
+ * src/diagnose.ts `size === 0` only
19
+ *
20
+ * 2490 > 1000, so every one of them accepted a web page as a model, wrote it
21
+ * to `blazeface.onnx`, and reported success — `download-models` printed
22
+ * `[OK] blazeface.onnx (0.0 MB)` and `diagnose` reported `ONNX Models: pass`.
23
+ * At runtime onnxruntime then throws `Protobuf parsing failed`, the camera
24
+ * subsystem reports ERROR, and the operator has been told everything is fine.
25
+ *
26
+ * A size floor cannot distinguish a model from a web page and never will. The
27
+ * check has to be on the CONTENT.
28
+ *
29
+ * WHAT IS CHECKED
30
+ * -------------------------------------------------------------------------
31
+ * An ONNX file is a serialized `ModelProto`. Protobuf encodes field 1
32
+ * (`ir_version`, a varint) as tag byte `0x08`, so every real ONNX model begins
33
+ * with `0x08`. HTML begins with `<` (0x3C). The two are decisive and cheap.
34
+ *
35
+ * Deliberately reports WHAT it found rather than a bare "invalid": an operator
36
+ * who is told "this is an HTML page, the model host is serving a web page" can
37
+ * act, where "invalid model" sends them looking at their disk.
38
+ */
39
+ export interface ModelIntegrityResult {
40
+ valid: boolean;
41
+ /** Human-readable reason, present only when `valid` is false. */
42
+ reason?: string;
43
+ /** What the bytes actually look like, when we can tell. */
44
+ detected?: 'html' | 'empty' | 'truncated' | 'not-onnx';
45
+ bytes?: number;
46
+ }
47
+ /** Classify a model file's leading bytes. Pure — takes the buffer, touches no disk. */
48
+ export declare function classifyModelBytes(buf: Buffer, totalBytes: number): ModelIntegrityResult;
49
+ /** Classify a model file on disk. Returns `{valid:false}` rather than throwing for a missing file. */
50
+ export declare function inspectModelFile(filePath: string): ModelIntegrityResult;
51
+ /**
52
+ * Throw unless `filePath` is a real ONNX model, DELETING it if it is not.
53
+ *
54
+ * Deleting is the point. A rejected download that stays on disk is worse than
55
+ * no download at all: the next `download-models` run sees the file, prints
56
+ * `[SKIP] (already exists)`, and the bad bytes become permanent.
57
+ */
58
+ export declare function assertOnnxModel(filePath: string, label?: string): void;
59
+ //# sourceMappingURL=modelIntegrity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelIntegrity.d.ts","sourceRoot":"","sources":["../src/modelIntegrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAaH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,oBAAoB,CAyCxF;AAED,sGAAsG;AACtG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,CAkBvE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAW,GAAG,IAAI,CASxE"}