@trillboards/edge-sdk 0.2.4 → 0.2.7

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 (204) 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 -5
  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-service.ps1 +6 -1
  30. package/deploy/windows/install-update.ps1 +101 -0
  31. package/deploy/windows/installer.nsi +138 -0
  32. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  33. package/deploy/windows/register-task.ps1 +44 -0
  34. package/deploy/windows/run-connector.ps1 +69 -0
  35. package/deploy/windows/setup-litertlm.ps1 +361 -0
  36. package/deploy/windows/stop-existing.ps1 +72 -0
  37. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  38. package/deploy/windows/trillboards-edge.cmd +33 -0
  39. package/deploy/windows/trillboards-pair.cmd +276 -0
  40. package/deploy/windows/trillboards-setup.ps1 +456 -0
  41. package/deploy/windows/trillboards-setup.vbs +13 -0
  42. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  43. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  44. package/dist/AudienceAnalyzeClient.js +246 -0
  45. package/dist/AudienceAnalyzeClient.js.map +1 -0
  46. package/dist/ChannelProber.d.ts +150 -0
  47. package/dist/ChannelProber.d.ts.map +1 -0
  48. package/dist/ChannelProber.js +887 -0
  49. package/dist/ChannelProber.js.map +1 -0
  50. package/dist/CloudEscalationPolicy.d.ts +143 -0
  51. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  52. package/dist/CloudEscalationPolicy.js +137 -0
  53. package/dist/CloudEscalationPolicy.js.map +1 -0
  54. package/dist/CloudSensingManager.d.ts +557 -0
  55. package/dist/CloudSensingManager.d.ts.map +1 -0
  56. package/dist/CloudSensingManager.js +1267 -0
  57. package/dist/CloudSensingManager.js.map +1 -0
  58. package/dist/DiscoveryReportClient.d.ts +66 -0
  59. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  60. package/dist/DiscoveryReportClient.js +85 -0
  61. package/dist/DiscoveryReportClient.js.map +1 -0
  62. package/dist/EdgeAgent.d.ts +12 -2
  63. package/dist/EdgeAgent.d.ts.map +1 -1
  64. package/dist/EdgeAgent.js +55 -13
  65. package/dist/EdgeAgent.js.map +1 -1
  66. package/dist/LiteRtVlmProvider.d.ts +193 -0
  67. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  68. package/dist/LiteRtVlmProvider.js +436 -0
  69. package/dist/LiteRtVlmProvider.js.map +1 -0
  70. package/dist/MosaicSource.d.ts +108 -0
  71. package/dist/MosaicSource.d.ts.map +1 -0
  72. package/dist/MosaicSource.js +274 -0
  73. package/dist/MosaicSource.js.map +1 -0
  74. package/dist/OnvifClient.d.ts +83 -0
  75. package/dist/OnvifClient.d.ts.map +1 -0
  76. package/dist/OnvifClient.js +209 -0
  77. package/dist/OnvifClient.js.map +1 -0
  78. package/dist/PairClient.d.ts +106 -0
  79. package/dist/PairClient.d.ts.map +1 -0
  80. package/dist/PairClient.js +186 -0
  81. package/dist/PairClient.js.map +1 -0
  82. package/dist/RecorderAddressRecovery.d.ts +53 -0
  83. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  84. package/dist/RecorderAddressRecovery.js +359 -0
  85. package/dist/RecorderAddressRecovery.js.map +1 -0
  86. package/dist/SceneChangeDetector.d.ts +291 -0
  87. package/dist/SceneChangeDetector.d.ts.map +1 -0
  88. package/dist/SceneChangeDetector.js +682 -0
  89. package/dist/SceneChangeDetector.js.map +1 -0
  90. package/dist/SensingProfileClient.d.ts +59 -0
  91. package/dist/SensingProfileClient.d.ts.map +1 -0
  92. package/dist/SensingProfileClient.js +78 -0
  93. package/dist/SensingProfileClient.js.map +1 -0
  94. package/dist/SetupSessionClient.d.ts +114 -0
  95. package/dist/SetupSessionClient.d.ts.map +1 -0
  96. package/dist/SetupSessionClient.js +95 -0
  97. package/dist/SetupSessionClient.js.map +1 -0
  98. package/dist/VlmAdmissionGate.d.ts +126 -0
  99. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  100. package/dist/VlmAdmissionGate.js +166 -0
  101. package/dist/VlmAdmissionGate.js.map +1 -0
  102. package/dist/autoSense.d.ts +266 -0
  103. package/dist/autoSense.d.ts.map +1 -0
  104. package/dist/autoSense.js +606 -0
  105. package/dist/autoSense.js.map +1 -0
  106. package/dist/cli.d.ts +27 -1
  107. package/dist/cli.d.ts.map +1 -1
  108. package/dist/cli.js +526 -27
  109. package/dist/cli.js.map +1 -1
  110. package/dist/combinedSense.d.ts +128 -0
  111. package/dist/combinedSense.d.ts.map +1 -0
  112. package/dist/combinedSense.js +261 -0
  113. package/dist/combinedSense.js.map +1 -0
  114. package/dist/config.d.ts +351 -0
  115. package/dist/config.d.ts.map +1 -1
  116. package/dist/config.js +99 -2
  117. package/dist/config.js.map +1 -1
  118. package/dist/connectorCadence.d.ts +135 -0
  119. package/dist/connectorCadence.d.ts.map +1 -0
  120. package/dist/connectorCadence.js +151 -0
  121. package/dist/connectorCadence.js.map +1 -0
  122. package/dist/dataDir.d.ts +33 -0
  123. package/dist/dataDir.d.ts.map +1 -0
  124. package/dist/dataDir.js +76 -0
  125. package/dist/dataDir.js.map +1 -0
  126. package/dist/deployedCadence.d.ts +167 -0
  127. package/dist/deployedCadence.d.ts.map +1 -0
  128. package/dist/deployedCadence.js +209 -0
  129. package/dist/deployedCadence.js.map +1 -0
  130. package/dist/deployedCadenceSource.d.ts +48 -0
  131. package/dist/deployedCadenceSource.d.ts.map +1 -0
  132. package/dist/deployedCadenceSource.js +74 -0
  133. package/dist/deployedCadenceSource.js.map +1 -0
  134. package/dist/diagnose.d.ts +1 -1
  135. package/dist/diagnose.d.ts.map +1 -1
  136. package/dist/diagnose.js +15 -13
  137. package/dist/diagnose.js.map +1 -1
  138. package/dist/discovery.d.ts +131 -0
  139. package/dist/discovery.d.ts.map +1 -0
  140. package/dist/discovery.js +499 -0
  141. package/dist/discovery.js.map +1 -0
  142. package/dist/gemmaModelOta.d.ts +118 -0
  143. package/dist/gemmaModelOta.d.ts.map +1 -0
  144. package/dist/gemmaModelOta.js +291 -0
  145. package/dist/gemmaModelOta.js.map +1 -0
  146. package/dist/index.d.ts +16 -1
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +40 -1
  149. package/dist/index.js.map +1 -1
  150. package/dist/init.d.ts.map +1 -1
  151. package/dist/init.js +25 -87
  152. package/dist/init.js.map +1 -1
  153. package/dist/keepAwake.d.ts +33 -0
  154. package/dist/keepAwake.d.ts.map +1 -0
  155. package/dist/keepAwake.js +54 -0
  156. package/dist/keepAwake.js.map +1 -0
  157. package/dist/legacyRecorderProfile.d.ts +20 -0
  158. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  159. package/dist/legacyRecorderProfile.js +27 -0
  160. package/dist/legacyRecorderProfile.js.map +1 -0
  161. package/dist/localInferenceSchema.d.ts +132 -0
  162. package/dist/localInferenceSchema.d.ts.map +1 -0
  163. package/dist/localInferenceSchema.js +274 -0
  164. package/dist/localInferenceSchema.js.map +1 -0
  165. package/dist/localSenseE2eHarness.d.ts +101 -0
  166. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  167. package/dist/localSenseE2eHarness.js +279 -0
  168. package/dist/localSenseE2eHarness.js.map +1 -0
  169. package/dist/localSensing.d.ts +284 -0
  170. package/dist/localSensing.d.ts.map +1 -0
  171. package/dist/localSensing.js +512 -0
  172. package/dist/localSensing.js.map +1 -0
  173. package/dist/mcp-server.d.ts.map +1 -1
  174. package/dist/mcp-server.js +3 -1
  175. package/dist/mcp-server.js.map +1 -1
  176. package/dist/modelIntegrity.d.ts +80 -0
  177. package/dist/modelIntegrity.d.ts.map +1 -0
  178. package/dist/modelIntegrity.js +194 -0
  179. package/dist/modelIntegrity.js.map +1 -0
  180. package/dist/pairVerify.d.ts +100 -0
  181. package/dist/pairVerify.d.ts.map +1 -0
  182. package/dist/pairVerify.js +296 -0
  183. package/dist/pairVerify.js.map +1 -0
  184. package/dist/qrScanner.d.ts +33 -0
  185. package/dist/qrScanner.d.ts.map +1 -0
  186. package/dist/qrScanner.js +103 -0
  187. package/dist/qrScanner.js.map +1 -0
  188. package/dist/runtimePaths.d.ts +10 -0
  189. package/dist/runtimePaths.d.ts.map +1 -0
  190. package/dist/runtimePaths.js +79 -0
  191. package/dist/runtimePaths.js.map +1 -0
  192. package/dist/sensingRunMode.d.ts +86 -0
  193. package/dist/sensingRunMode.d.ts.map +1 -0
  194. package/dist/sensingRunMode.js +346 -0
  195. package/dist/sensingRunMode.js.map +1 -0
  196. package/dist/setupMode.d.ts +98 -0
  197. package/dist/setupMode.d.ts.map +1 -0
  198. package/dist/setupMode.js +828 -0
  199. package/dist/setupMode.js.map +1 -0
  200. package/dist/setupProgress.d.ts +31 -0
  201. package/dist/setupProgress.d.ts.map +1 -0
  202. package/dist/setupProgress.js +173 -0
  203. package/dist/setupProgress.js.map +1 -0
  204. package/package.json +13 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SceneChangeDetector.d.ts","sourceRoot":"","sources":["../src/SceneChangeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAuCD;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAE1C,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA8C;IAC1E,OAAO,CAAC,aAAa,CAAK;IAC1B,qFAAqF;IACrF,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,GAAE,0BAA+B;IAUpD,qEAAqE;IACrE,cAAc,IAAI,MAAM;IAIxB,aAAa,IAAI,OAAO;IAIxB,6EAA6E;IAC7E,aAAa,IAAI,MAAM;IAIvB;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAab;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,kBAAkB;IA8D5C;;;;OAIG;IACH,OAAO,CAAC,eAAe;CAwCxB;AAID,sDAAsD;AACtD,MAAM,MAAM,gBAAgB;AAC1B,sEAAsE;AACpE,WAAW;AACb,yBAAyB;GACvB,QAAQ;AACV,qBAAqB;GACnB,WAAW;AACb,uEAAuE;GACrE,aAAa;AACf,+EAA+E;GAC7E,YAAY;AACd,mDAAmD;GACjD,cAAc;AAChB,wEAAwE;GACtE,aAAa,CAAC;AAElB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,gBAAgB,CAAC;IACzB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IAC3C,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAkB,SAAQ,0BAA0B;IACnE;;;;;OAKG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC3E,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAID;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C;;;;OAIG;IACH,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAS;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoE;IAChG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,qBAAqB,CAAuB;IACpD,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAQvB;gBAEU,OAAO,EAAE,iBAAiB;IAYtC;;;;OAIG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoDvE;;;;OAIG;IACH,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlC,+DAA+D;IAC/D,sBAAsB,IAAI,MAAM;IAIhC;;;;;;;;;;;;;;;;OAgBG;IACH,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IASnD,0EAA0E;IAC1E,KAAK,IAAI,IAAI;IAMb,QAAQ,IAAI,eAAe;IAW3B,OAAO,CAAC,MAAM;CAwBf;AA4CD,oDAAoD;AACpD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAe3B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,SAAS,CAwBX;AAsKD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAWzF"}
@@ -0,0 +1,682 @@
1
+ "use strict";
2
+ /**
3
+ * SceneChangeDetector + MotionGate — the connector's ONE motion implementation.
4
+ *
5
+ * This is the cheap stage of the cascade that stands in front of the expensive
6
+ * one: in a real store most frames are an empty aisle, and today every one of
7
+ * them is decoded, prompted and generated over by Gemma at 6–32 s a frame. The
8
+ * cheap stage answers "did anything change?" in single-digit milliseconds on a
9
+ * ~178x100 greyscale frame — about 116x fewer pixels than the 640x480 JPEG the
10
+ * VLM would otherwise chew through.
11
+ *
12
+ * PORTED FROM FRIGATE (MIT), `frigate/motion/improved_motion.py` +
13
+ * `frigate/config/camera/motion.py`, keeping its defaults and — more
14
+ * importantly — its two non-obvious behaviors:
15
+ *
16
+ * 1. THE BACKGROUND ONLY MOVES AFTER MOTION PERSISTS. `accumulateWeighted`
17
+ * runs every frame while the scene is still, but while motion is present
18
+ * it runs only once the motion has lasted `BACKGROUND_UPDATE_AFTER_FRAMES`
19
+ * (10) frames. Without that rule a slowly-moving object is absorbed into
20
+ * the background and becomes invisible mid-walk.
21
+ * 2. A CALIBRATION STATE MACHINE. A camera flipping its IR-cut filter at dusk
22
+ * changes every pixel at once; so does a cloud crossing the sun. Frigate
23
+ * treats "more than `lightningThreshold` of the frame moved" as a signal
24
+ * to re-enter calibration rather than as motion, and stays calibrating
25
+ * until the scene settles (<5% moved AND <=4 regions). We suppress
26
+ * VLM-triggering while calibrating for exactly that reason — otherwise
27
+ * dusk costs a 16-camera store one full inference storm per day.
28
+ *
29
+ * RELATIONSHIP TO THE ANDROID `SceneChangeDetector`. agent-core carries a class
30
+ * of the same name (`ctv/core/audience/SceneChangeDetector.kt`) built on
31
+ * luminance-HISTOGRAM chi-squared distance over Android `Bitmap`s. That is the
32
+ * right shape for a tablet whose camera frames a single face at arm's length,
33
+ * and the wrong shape here: a person crossing a wide CCTV frame moves a few
34
+ * percent of the pixels and barely perturbs a global histogram, which is why
35
+ * this port is region-based (threshold → dilate → connected components) instead.
36
+ * Two runtimes (Kotlin/Bitmap vs Node/Buffer) with two different jobs; there is
37
+ * exactly ONE motion implementation on each side, and this is the connector's.
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.MotionGate = exports.SceneChangeDetector = void 0;
41
+ exports.resetGreyDecoderCache = resetGreyDecoderCache;
42
+ exports.decodeGreyFrame = decodeGreyFrame;
43
+ exports.downscaleRgbaToGrey = downscaleRgbaToGrey;
44
+ exports.motionSignature = motionSignature;
45
+ const DEFAULT_FRAME_HEIGHT = 100;
46
+ const DEFAULT_THRESHOLD = 30;
47
+ const DEFAULT_CONTOUR_AREA = 10;
48
+ const DEFAULT_FRAME_ALPHA = 0.01;
49
+ const DEFAULT_CALIBRATING_ALPHA = 0.2;
50
+ const DEFAULT_LIGHTNING_THRESHOLD = 0.8;
51
+ /** Frames motion must persist before the background is allowed to absorb it. */
52
+ const BACKGROUND_UPDATE_AFTER_FRAMES = 10;
53
+ /**
54
+ * Frigate keeps the last 50 contrast readings and stretches to their mean.
55
+ *
56
+ * Frigate SEEDS that ring with (0, 255) and lets real readings displace the
57
+ * seed one frame at a time. At its 5 fps that seed is gone in ten seconds. We
58
+ * look every `scanIntervalMs` — 30 s by default — so the same ring takes
59
+ * 50 x 30 s = 25 MINUTES to fill, and for all of it the stretch mapping is
60
+ * still crawling from the synthetic seed toward the scene's real percentiles.
61
+ *
62
+ * A moving mapping means the CONTRAST-STRETCHED frame changes every look even
63
+ * when the camera is bolted to a wall pointing at an empty aisle. Measured on a
64
+ * byte-identical still frame at the 30 s cadence: no motion through look 32,
65
+ * then phantom motion from look 33 to look 93 (wall clock 16.5 min -> 46.5 min
66
+ * after start) — 53 inferences, ~387 s of the single admission slot, per camera,
67
+ * against a scene where literally nothing happened. `reset()` re-arms it, so a
68
+ * feed that flaps re-enters the window every reconnect.
69
+ *
70
+ * Seeding from the FIRST real reading instead makes the mapping stationary from
71
+ * frame 1. Once the ring holds 50 real readings the behaviour is identical to
72
+ * Frigate's; only the startup transient differs, and only because our cadence
73
+ * is 150x slower than the one Frigate chose the seed for.
74
+ */
75
+ const CONTRAST_HISTORY = 50;
76
+ /** Cells per axis in the motion signature grid (stationary suppression). */
77
+ const SIGNATURE_GRID = 16;
78
+ /** scipy `gaussian_filter(sigma=1, radius=1)`, normalized. Separable 1-D. */
79
+ const GAUSS_1D = [0.2740686, 0.4518628, 0.2740686];
80
+ /**
81
+ * The Frigate motion core, one instance per camera feed. Stateful: it owns the
82
+ * running background model, the contrast history and the calibration state, so
83
+ * frames must be fed to the SAME instance in capture order.
84
+ */
85
+ class SceneChangeDetector {
86
+ constructor(options = {}) {
87
+ this.avgFrame = null;
88
+ this.avgWidth = 0;
89
+ this.avgHeight = 0;
90
+ this.motionFrameCount = 0;
91
+ this.calibrating = true;
92
+ this.contrastMin = new Int32Array(CONTRAST_HISTORY);
93
+ this.contrastMax = new Int32Array(CONTRAST_HISTORY).fill(255);
94
+ this.contrastIndex = 0;
95
+ /** False until the ring has been seeded from a real frame (see CONTRAST_HISTORY). */
96
+ this.contrastSeeded = false;
97
+ this.framesSeen = 0;
98
+ this.frameHeight = Math.max(16, Math.trunc(options.frameHeight ?? DEFAULT_FRAME_HEIGHT));
99
+ this.threshold = Math.min(255, Math.max(1, Math.trunc(options.threshold ?? DEFAULT_THRESHOLD)));
100
+ this.contourArea = Math.max(1, Math.trunc(options.contourArea ?? DEFAULT_CONTOUR_AREA));
101
+ this.frameAlpha = clamp01(options.frameAlpha ?? DEFAULT_FRAME_ALPHA);
102
+ this.calibratingAlpha = clamp01(options.calibratingAlpha ?? DEFAULT_CALIBRATING_ALPHA);
103
+ this.lightningThreshold = clamp01(options.lightningThreshold ?? DEFAULT_LIGHTNING_THRESHOLD);
104
+ this.improveContrast = options.improveContrast !== false;
105
+ }
106
+ /** Height (px) callers must downscale to before calling `detect`. */
107
+ getFrameHeight() {
108
+ return this.frameHeight;
109
+ }
110
+ isCalibrating() {
111
+ return this.calibrating;
112
+ }
113
+ /** Frames fed so far (a feed with 0 has never produced a motion verdict). */
114
+ getFramesSeen() {
115
+ return this.framesSeen;
116
+ }
117
+ /**
118
+ * Forget the background model. Call when the view itself changed (camera
119
+ * re-opened on a different channel, connector resumed after a long stop) so a
120
+ * stale baseline cannot report a whole-frame "change" that never happened.
121
+ */
122
+ reset() {
123
+ this.avgFrame = null;
124
+ this.avgWidth = 0;
125
+ this.avgHeight = 0;
126
+ this.motionFrameCount = 0;
127
+ this.calibrating = true;
128
+ this.contrastMin.fill(0);
129
+ this.contrastMax.fill(255);
130
+ this.contrastIndex = 0;
131
+ this.contrastSeeded = false;
132
+ this.framesSeen = 0;
133
+ }
134
+ /**
135
+ * One motion verdict. `frame` must already be greyscale at `frameHeight`
136
+ * (see `decodeGreyFrame`), which is what keeps this stage ~116x cheaper than
137
+ * handing the full JPEG to the model.
138
+ */
139
+ detect(frame) {
140
+ const { width, height } = frame;
141
+ this.framesSeen++;
142
+ let working = this.improveContrast ? this.stretchContrast(frame) : Uint8Array.from(frame.data);
143
+ working = blur3x3(working, width, height);
144
+ // A new (or resized) view has no baseline: seed the background from this
145
+ // frame and report NO motion rather than a whole-frame false positive.
146
+ if (!this.avgFrame || this.avgWidth !== width || this.avgHeight !== height) {
147
+ this.avgFrame = Float32Array.from(working);
148
+ this.avgWidth = width;
149
+ this.avgHeight = height;
150
+ this.motionFrameCount = 0;
151
+ this.calibrating = true;
152
+ return { motion: false, boxes: [], pctMotion: 0, calibrating: true, signature: '' };
153
+ }
154
+ // |frame - background| > threshold, then dilate to close holes.
155
+ const avg = this.avgFrame;
156
+ const mask = new Uint8Array(width * height);
157
+ for (let i = 0; i < mask.length; i++) {
158
+ // cv2.convertScaleAbs rounds the float background before differencing.
159
+ if (Math.abs(working[i] - Math.round(avg[i])) > this.threshold)
160
+ mask[i] = 1;
161
+ }
162
+ const dilated = dilate3x3(mask, width, height);
163
+ const components = connectedComponents(dilated, width, height);
164
+ const boxes = [];
165
+ let totalArea = 0;
166
+ for (const c of components) {
167
+ totalArea += c.area;
168
+ if (c.area > this.contourArea)
169
+ boxes.push(c);
170
+ }
171
+ const pctMotion = totalArea / (width * height);
172
+ // Calibration state machine (Frigate's order: settle first, then re-arm).
173
+ if (pctMotion < 0.05 && boxes.length <= 4)
174
+ this.calibrating = false;
175
+ if (this.calibrating || pctMotion > this.lightningThreshold)
176
+ this.calibrating = true;
177
+ // Background update: every still frame, but only once motion has PERSISTED
178
+ // — otherwise a slow walker is absorbed and disappears mid-frame.
179
+ const alpha = this.calibrating ? this.calibratingAlpha : this.frameAlpha;
180
+ if (boxes.length > 0) {
181
+ this.motionFrameCount++;
182
+ if (this.motionFrameCount >= BACKGROUND_UPDATE_AFTER_FRAMES) {
183
+ accumulateWeighted(working, avg, alpha);
184
+ }
185
+ }
186
+ else {
187
+ accumulateWeighted(working, avg, alpha);
188
+ this.motionFrameCount = 0;
189
+ }
190
+ return {
191
+ motion: boxes.length > 0,
192
+ boxes,
193
+ pctMotion,
194
+ calibrating: this.calibrating,
195
+ signature: motionSignature(boxes, width, height),
196
+ };
197
+ }
198
+ /**
199
+ * Frigate's `improve_contrast`: stretch the 4th–96th percentile band to
200
+ * 0..255 using the MEAN of the last 50 readings, so one odd frame cannot
201
+ * yank the mapping and manufacture motion everywhere.
202
+ */
203
+ stretchContrast(frame) {
204
+ const src = frame.data;
205
+ const out = new Uint8Array(src.length);
206
+ const hist = new Int32Array(256);
207
+ for (let i = 0; i < src.length; i++)
208
+ hist[src[i]]++;
209
+ const minValue = percentileFromHistogram(hist, src.length, 0.04);
210
+ const maxValue = percentileFromHistogram(hist, src.length, 0.96);
211
+ if (minValue < maxValue) {
212
+ if (!this.contrastSeeded) {
213
+ // First real reading fills the whole ring, so the stretch mapping is
214
+ // stationary from frame 1 instead of crawling off the (0,255) seed for
215
+ // the next 25 minutes of 30 s looks. See CONTRAST_HISTORY.
216
+ this.contrastMin.fill(minValue);
217
+ this.contrastMax.fill(maxValue);
218
+ this.contrastSeeded = true;
219
+ this.contrastIndex = 0;
220
+ }
221
+ else {
222
+ this.contrastMin[this.contrastIndex] = minValue;
223
+ this.contrastMax[this.contrastIndex] = maxValue;
224
+ }
225
+ this.contrastIndex = (this.contrastIndex + 1) % CONTRAST_HISTORY;
226
+ }
227
+ let sumMin = 0;
228
+ let sumMax = 0;
229
+ for (let i = 0; i < CONTRAST_HISTORY; i++) {
230
+ sumMin += this.contrastMin[i];
231
+ sumMax += this.contrastMax[i];
232
+ }
233
+ const avgMin = sumMin / CONTRAST_HISTORY;
234
+ const avgMax = sumMax / CONTRAST_HISTORY;
235
+ if (!(avgMax > avgMin))
236
+ return Uint8Array.from(src);
237
+ const scale = 255 / (avgMax - avgMin);
238
+ for (let i = 0; i < src.length; i++) {
239
+ const clipped = src[i] < avgMin ? avgMin : src[i] > avgMax ? avgMax : src[i];
240
+ out[i] = (clipped - avgMin) * scale;
241
+ }
242
+ return out;
243
+ }
244
+ }
245
+ exports.SceneChangeDetector = SceneChangeDetector;
246
+ const DEFAULT_STATIONARY_REANALYZE_EVERY = 10;
247
+ /**
248
+ * Per-feed cascade decision: decode cheap → motion → should this frame cost a
249
+ * VLM call? Owns the feed's `SceneChangeDetector`, its heartbeat clock and its
250
+ * stationary-suppression counter.
251
+ *
252
+ * FAILS OPEN. Any decode failure analyzes the frame. A motion gate that cannot
253
+ * see must not be allowed to silence a camera — the whole product is the alert
254
+ * that fires.
255
+ */
256
+ class MotionGate {
257
+ constructor(options) {
258
+ this.lastAnalyzedAtMs = null;
259
+ this.lastAnalyzedSignature = null;
260
+ this.stationarySuppressions = 0;
261
+ this.evaluated = 0;
262
+ this.analyzed = 0;
263
+ this.byReason = {
264
+ heartbeat: 0,
265
+ motion: 0,
266
+ no_motion: 0,
267
+ calibrating: 0,
268
+ stationary: 0,
269
+ rate_limited: 0,
270
+ undecodable: 0,
271
+ };
272
+ this.detector = new SceneChangeDetector(options);
273
+ this.heartbeatIntervalMs = Math.max(0, Math.trunc(options.heartbeatIntervalMs));
274
+ this.minAnalyzeIntervalMs = Math.max(0, Math.trunc(options.minAnalyzeIntervalMs ?? 0));
275
+ this.stationaryReanalyzeEvery = Math.max(0, Math.trunc(options.stationaryReanalyzeEvery ?? DEFAULT_STATIONARY_REANALYZE_EVERY));
276
+ this.decodeFrame = options.decode ?? decodeGreyFrame;
277
+ this.now = options.now ?? Date.now;
278
+ }
279
+ /**
280
+ * Decide whether this JPEG earns an inference. Decision order matters:
281
+ * heartbeat first (proof-of-life outranks everything), then the motion
282
+ * verdict, then the suppressions that make a busy-but-boring scene cheap.
283
+ */
284
+ async evaluate(jpeg, nowMs) {
285
+ const now = nowMs ?? this.now();
286
+ this.evaluated++;
287
+ const heartbeatDue = this.lastAnalyzedAtMs === null || now - this.lastAnalyzedAtMs >= this.heartbeatIntervalMs;
288
+ let grey = null;
289
+ try {
290
+ grey = await this.decodeFrame(jpeg, this.detector.getFrameHeight());
291
+ }
292
+ catch {
293
+ grey = null;
294
+ }
295
+ if (!grey) {
296
+ // No eyes → no opinion → do not suppress. A gate that cannot see must
297
+ // never be the reason a camera goes quiet.
298
+ return this.decide(true, 'undecodable', false, 0, [], null);
299
+ }
300
+ const result = this.detector.detect(grey);
301
+ const live = result.motion && !result.calibrating;
302
+ if (heartbeatDue) {
303
+ return this.decide(true, 'heartbeat', live, result.pctMotion, result.boxes, result.signature);
304
+ }
305
+ if (!result.motion) {
306
+ return this.decide(false, 'no_motion', false, result.pctMotion, result.boxes, result.signature);
307
+ }
308
+ if (result.calibrating) {
309
+ // Dusk IR-cut flip / cloud shadow: everything "moved" and none of it is real.
310
+ return this.decide(false, 'calibrating', false, result.pctMotion, result.boxes, result.signature);
311
+ }
312
+ if (this.stationaryReanalyzeEvery > 0 &&
313
+ this.lastAnalyzedSignature !== null &&
314
+ result.signature === this.lastAnalyzedSignature) {
315
+ this.stationarySuppressions++;
316
+ if (this.stationarySuppressions < this.stationaryReanalyzeEvery) {
317
+ return this.decide(false, 'stationary', live, result.pctMotion, result.boxes, result.signature);
318
+ }
319
+ }
320
+ if (this.minAnalyzeIntervalMs > 0 &&
321
+ this.lastAnalyzedAtMs !== null &&
322
+ now - this.lastAnalyzedAtMs < this.minAnalyzeIntervalMs) {
323
+ return this.decide(false, 'rate_limited', live, result.pctMotion, result.boxes, result.signature);
324
+ }
325
+ return this.decide(true, 'motion', live, result.pctMotion, result.boxes, result.signature);
326
+ }
327
+ /**
328
+ * Confirm the frame really reached the model. Called by the manager only
329
+ * after `analyze()` resolves, so a frame the admission gate or the network
330
+ * dropped does not reset the heartbeat and silence the feed.
331
+ */
332
+ markAnalyzed(nowMs) {
333
+ this.lastAnalyzedAtMs = nowMs ?? this.now();
334
+ }
335
+ /** The still-scene reporting floor currently in force (ms). */
336
+ getHeartbeatIntervalMs() {
337
+ return this.heartbeatIntervalMs;
338
+ }
339
+ /**
340
+ * Retune the still-scene reporting floor WITHOUT forgetting anything.
341
+ *
342
+ * Changing a cadence is a statement about how often a still scene must report.
343
+ * It is NOT a statement that the scene changed, that the camera moved, or that
344
+ * the last analysis never happened — but rebuilding the gate says all three:
345
+ * a new `SceneChangeDetector` has no background model, is back in
346
+ * `calibrating`, has an un-seeded contrast ring (the exact state the
347
+ * CONTRAST_HISTORY seeding fix exists to avoid), and a null
348
+ * `lastAnalyzedAtMs`, which makes the very next look heartbeat-DUE and buys a
349
+ * forced inference per feed for a scene the gate had already proven still.
350
+ *
351
+ * The heartbeat is a scalar the next `evaluate()` reads. Moving it is the
352
+ * whole change.
353
+ *
354
+ * @returns true when the interval actually changed.
355
+ */
356
+ setHeartbeatIntervalMs(intervalMs) {
357
+ const raw = Number(intervalMs);
358
+ if (!Number.isFinite(raw))
359
+ return false;
360
+ const next = Math.max(0, Math.trunc(raw));
361
+ if (next === this.heartbeatIntervalMs)
362
+ return false;
363
+ this.heartbeatIntervalMs = next;
364
+ return true;
365
+ }
366
+ /** Forget the background model (camera re-opened on a different view). */
367
+ reset() {
368
+ this.detector.reset();
369
+ this.lastAnalyzedSignature = null;
370
+ this.stationarySuppressions = 0;
371
+ }
372
+ getStats() {
373
+ return {
374
+ evaluated: this.evaluated,
375
+ analyzed: this.analyzed,
376
+ skipped: this.evaluated - this.analyzed,
377
+ byReason: { ...this.byReason },
378
+ calibrating: this.detector.isCalibrating(),
379
+ framesSeen: this.detector.getFramesSeen(),
380
+ };
381
+ }
382
+ decide(analyze, reason, motion, pctMotion, boxes, signature) {
383
+ this.byReason[reason]++;
384
+ if (analyze) {
385
+ this.analyzed++;
386
+ // The signature/counter track what the MODEL last saw, so a suppressed
387
+ // frame never moves the stationary baseline.
388
+ //
389
+ // The heartbeat CLOCK is deliberately not touched here: only
390
+ // markAnalyzed() moves it, and only after the call landed. A feed whose
391
+ // inference failed therefore stays heartbeat-due and retries on its next
392
+ // look, instead of going quiet for a whole heartbeat window because of
393
+ // one bad request.
394
+ this.lastAnalyzedSignature = signature;
395
+ this.stationarySuppressions = 0;
396
+ }
397
+ return { analyze, reason, motion, pctMotion, boxes };
398
+ }
399
+ }
400
+ exports.MotionGate = MotionGate;
401
+ let sharpModule;
402
+ let jpegModule;
403
+ function loadSharp() {
404
+ if (sharpModule === undefined) {
405
+ try {
406
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
407
+ sharpModule = require('sharp');
408
+ }
409
+ catch {
410
+ sharpModule = null;
411
+ }
412
+ }
413
+ return sharpModule;
414
+ }
415
+ function loadJpeg() {
416
+ if (jpegModule === undefined) {
417
+ try {
418
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
419
+ jpegModule = require('jpeg-js');
420
+ }
421
+ catch {
422
+ jpegModule = null;
423
+ }
424
+ }
425
+ return jpegModule;
426
+ }
427
+ /** Test seam: forget which decoder was resolved. */
428
+ function resetGreyDecoderCache() {
429
+ sharpModule = undefined;
430
+ jpegModule = undefined;
431
+ }
432
+ /**
433
+ * JPEG → greyscale at `targetHeight`, the ~116x pixel reduction the whole
434
+ * cascade rests on. `sharp` (a hard dependency of @trillboards/edge-sensing, so
435
+ * present wherever sensing runs) does it natively; `jpeg-js` is the pure-JS
436
+ * fallback. Returns null when neither is available, which makes the gate fail
437
+ * open rather than blind a camera.
438
+ *
439
+ * WHY NOT FRIGATE'S LITERAL `INTER_NEAREST`. Frigate picks nearest because
440
+ * `cv2.resize` nearest is the cheapest thing that exists and it blurs the
441
+ * result immediately afterwards anyway. sharp's "nearest" is not a true
442
+ * point-sample — it pre-shrinks on load — so asking for it buys none of
443
+ * Frigate's determinism and costs the two decoders their agreement (measured
444
+ * MAD 21.8 grey levels against a real point-sample, versus 4.2 for cubic
445
+ * against the fallback's box average). Both decoders low-pass before
446
+ * decimating, which is what the subsequent absdiff+threshold wants, and they
447
+ * land within ~4 grey levels of each other — far inside the threshold of 30, so
448
+ * a host with sharp and a host without cannot disagree about whether a scene
449
+ * moved.
450
+ */
451
+ async function decodeGreyFrame(jpeg, targetHeight) {
452
+ const sharp = loadSharp();
453
+ if (sharp) {
454
+ const { data, info } = await sharp(jpeg)
455
+ .resize({ height: targetHeight, fit: 'inside', kernel: 'cubic', withoutEnlargement: false })
456
+ .greyscale()
457
+ .raw()
458
+ .toBuffer({ resolveWithObject: true });
459
+ return { width: info.width, height: info.height, data: new Uint8Array(data.buffer, data.byteOffset, data.length) };
460
+ }
461
+ const jpegJs = loadJpeg();
462
+ if (!jpegJs)
463
+ return null;
464
+ const decoded = jpegJs.decode(jpeg, { useTArray: true });
465
+ return downscaleRgbaToGrey(decoded.data, decoded.width, decoded.height, targetHeight);
466
+ }
467
+ /**
468
+ * RGBA → greyscale + box-average downscale: each output pixel is the mean of
469
+ * the source rectangle it covers. Averaging (rather than point-sampling) is
470
+ * what keeps this in step with the sharp path and what stops a single noisy
471
+ * pixel from surviving a 4.8x decimation into the difference image.
472
+ */
473
+ function downscaleRgbaToGrey(rgba, width, height, targetHeight) {
474
+ const outH = Math.max(1, Math.min(height, Math.round(targetHeight)));
475
+ const outW = Math.max(1, Math.round((width * outH) / height));
476
+ const out = new Uint8Array(outW * outH);
477
+ for (let y = 0; y < outH; y++) {
478
+ const sy0 = Math.floor((y * height) / outH);
479
+ const sy1 = Math.max(sy0 + 1, Math.floor(((y + 1) * height) / outH));
480
+ for (let x = 0; x < outW; x++) {
481
+ const sx0 = Math.floor((x * width) / outW);
482
+ const sx1 = Math.max(sx0 + 1, Math.floor(((x + 1) * width) / outW));
483
+ let sum = 0;
484
+ let n = 0;
485
+ for (let sy = sy0; sy < sy1 && sy < height; sy++) {
486
+ for (let sx = sx0; sx < sx1 && sx < width; sx++) {
487
+ const p = (sy * width + sx) * 4;
488
+ // ITU-R BT.601 luma, the same weights sharp's greyscale uses.
489
+ sum += 0.299 * rgba[p] + 0.587 * rgba[p + 1] + 0.114 * rgba[p + 2];
490
+ n++;
491
+ }
492
+ }
493
+ out[y * outW + x] = n > 0 ? Math.round(sum / n) : 0;
494
+ }
495
+ }
496
+ return { width: outW, height: outH, data: out };
497
+ }
498
+ // ─── Pixel kernels ───────────────────────────────────────────────────────────
499
+ function clamp01(v) {
500
+ return v < 0 ? 0 : v > 1 ? 1 : v;
501
+ }
502
+ /** Value at quantile `q` from a 256-bin histogram (np.percentile, floored). */
503
+ function percentileFromHistogram(hist, total, q) {
504
+ if (total <= 0)
505
+ return 0;
506
+ const target = Math.round(q * (total - 1));
507
+ let seen = 0;
508
+ for (let v = 0; v < 256; v++) {
509
+ seen += hist[v];
510
+ if (seen > target)
511
+ return v;
512
+ }
513
+ return 255;
514
+ }
515
+ /** Separable 3x3 gaussian (sigma=1, radius=1) with edge clamping. */
516
+ function blur3x3(src, width, height) {
517
+ const tmp = new Float32Array(src.length);
518
+ const out = new Uint8Array(src.length);
519
+ const [k0, k1, k2] = GAUSS_1D;
520
+ for (let y = 0; y < height; y++) {
521
+ const row = y * width;
522
+ for (let x = 0; x < width; x++) {
523
+ const l = src[row + (x > 0 ? x - 1 : 0)];
524
+ const c = src[row + x];
525
+ const r = src[row + (x < width - 1 ? x + 1 : width - 1)];
526
+ tmp[row + x] = k0 * l + k1 * c + k2 * r;
527
+ }
528
+ }
529
+ for (let y = 0; y < height; y++) {
530
+ const up = (y > 0 ? y - 1 : 0) * width;
531
+ const row = y * width;
532
+ const down = (y < height - 1 ? y + 1 : height - 1) * width;
533
+ for (let x = 0; x < width; x++) {
534
+ const v = k0 * tmp[up + x] + k1 * tmp[row + x] + k2 * tmp[down + x];
535
+ out[row + x] = v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
536
+ }
537
+ }
538
+ return out;
539
+ }
540
+ /**
541
+ * cv2.dilate with the default 3x3 rectangular element, 1 iteration. A rect
542
+ * element is separable, so this is two 1-D passes instead of a 9-tap window.
543
+ */
544
+ function dilate3x3(src, width, height) {
545
+ const tmp = new Uint8Array(src.length);
546
+ const out = new Uint8Array(src.length);
547
+ for (let y = 0; y < height; y++) {
548
+ const row = y * width;
549
+ for (let x = 0; x < width; x++) {
550
+ const l = x > 0 ? src[row + x - 1] : 0;
551
+ const r = x < width - 1 ? src[row + x + 1] : 0;
552
+ tmp[row + x] = src[row + x] | l | r;
553
+ }
554
+ }
555
+ for (let y = 0; y < height; y++) {
556
+ const row = y * width;
557
+ const up = y > 0 ? row - width : row;
558
+ const down = y < height - 1 ? row + width : row;
559
+ for (let x = 0; x < width; x++) {
560
+ out[row + x] = tmp[row + x] | tmp[up + x] | tmp[down + x];
561
+ }
562
+ }
563
+ return out;
564
+ }
565
+ /**
566
+ * 8-connected components — the counterpart of `cv2.findContours(RETR_EXTERNAL)`
567
+ * + `contourArea` + `boundingRect`. Area is the foreground PIXEL count, which
568
+ * is marginally more permissive than OpenCV's polygon area for the same blob;
569
+ * on a 100 px-tall frame with a floor of 10 px that difference is well inside
570
+ * the noise the dilate step already introduces.
571
+ */
572
+ function connectedComponents(src, width, height) {
573
+ const labels = new Int32Array(src.length);
574
+ // Union-find over provisional labels (index 0 unused).
575
+ const parent = [0];
576
+ const find = (a) => {
577
+ let root = a;
578
+ while (parent[root] !== root)
579
+ root = parent[root];
580
+ let cur = a;
581
+ while (parent[cur] !== root) {
582
+ const next = parent[cur];
583
+ parent[cur] = root;
584
+ cur = next;
585
+ }
586
+ return root;
587
+ };
588
+ const union = (a, b) => {
589
+ const ra = find(a);
590
+ const rb = find(b);
591
+ if (ra !== rb)
592
+ parent[Math.max(ra, rb)] = Math.min(ra, rb);
593
+ };
594
+ let next = 1;
595
+ // Already-scanned 8-neighbours of (x, y): NW, N, NE, W. Reused, never
596
+ // reallocated — this runs once per foreground pixel on every frame.
597
+ const neighbours = new Int32Array(4);
598
+ for (let y = 0; y < height; y++) {
599
+ for (let x = 0; x < width; x++) {
600
+ const i = y * width + x;
601
+ if (!src[i])
602
+ continue;
603
+ neighbours[0] = y > 0 && x > 0 ? labels[i - width - 1] : 0;
604
+ neighbours[1] = y > 0 ? labels[i - width] : 0;
605
+ neighbours[2] = y > 0 && x < width - 1 ? labels[i - width + 1] : 0;
606
+ neighbours[3] = x > 0 ? labels[i - 1] : 0;
607
+ let best = 0;
608
+ let bestRoot = 0;
609
+ for (let n = 0; n < 4; n++) {
610
+ const label = neighbours[n];
611
+ if (!label)
612
+ continue;
613
+ const root = find(label);
614
+ if (best === 0 || root < bestRoot) {
615
+ best = label;
616
+ bestRoot = root;
617
+ }
618
+ }
619
+ if (best === 0) {
620
+ labels[i] = next;
621
+ parent[next] = next;
622
+ next++;
623
+ }
624
+ else {
625
+ labels[i] = best;
626
+ for (let n = 0; n < 4; n++)
627
+ if (neighbours[n])
628
+ union(best, neighbours[n]);
629
+ }
630
+ }
631
+ }
632
+ const boxes = new Map();
633
+ for (let y = 0; y < height; y++) {
634
+ for (let x = 0; x < width; x++) {
635
+ const i = y * width + x;
636
+ if (!labels[i])
637
+ continue;
638
+ const root = find(labels[i]);
639
+ const box = boxes.get(root);
640
+ if (!box) {
641
+ boxes.set(root, { x, y, w: 1, h: 1, area: 1 });
642
+ }
643
+ else {
644
+ const x1 = Math.max(box.x + box.w - 1, x);
645
+ const y1 = Math.max(box.y + box.h - 1, y);
646
+ const x0 = Math.min(box.x, x);
647
+ const y0 = Math.min(box.y, y);
648
+ box.x = x0;
649
+ box.y = y0;
650
+ box.w = x1 - x0 + 1;
651
+ box.h = y1 - y0 + 1;
652
+ box.area++;
653
+ }
654
+ }
655
+ }
656
+ return [...boxes.values()];
657
+ }
658
+ /** `avg = (1 - alpha) * avg + alpha * src` — cv2.accumulateWeighted. */
659
+ function accumulateWeighted(src, avg, alpha) {
660
+ const keep = 1 - alpha;
661
+ for (let i = 0; i < avg.length; i++)
662
+ avg[i] = keep * avg[i] + alpha * src[i];
663
+ }
664
+ /**
665
+ * Quantize motion regions onto a coarse grid so "the same thing is still there"
666
+ * produces a stable string. Deliberately lossy: a person walking one cell over
667
+ * changes it, a swinging sign does not.
668
+ */
669
+ function motionSignature(boxes, width, height) {
670
+ if (boxes.length === 0)
671
+ return '';
672
+ const cells = new Set();
673
+ for (const b of boxes) {
674
+ const cx0 = Math.floor((b.x * SIGNATURE_GRID) / width);
675
+ const cy0 = Math.floor((b.y * SIGNATURE_GRID) / height);
676
+ const cx1 = Math.floor(((b.x + b.w - 1) * SIGNATURE_GRID) / width);
677
+ const cy1 = Math.floor(((b.y + b.h - 1) * SIGNATURE_GRID) / height);
678
+ cells.add(`${cx0},${cy0},${cx1},${cy1}`);
679
+ }
680
+ return [...cells].sort().join('|');
681
+ }
682
+ //# sourceMappingURL=SceneChangeDetector.js.map