@trillboards/edge-sdk 0.2.4 → 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 +55 -13
  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,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"}
@@ -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"}