@camera.ui/server 2.1.8 → 2.1.10

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 (480) hide show
  1. package/dist/interface/assets/{404-vRjJVgl4.js → 404-BLNSh-sA.js} +1 -1
  2. package/dist/interface/assets/{About-CO9cx5Gp.js → About-ZakRzon7.js} +1 -1
  3. package/dist/interface/assets/Automation-DoVhkuVY.js +3 -0
  4. package/dist/interface/assets/AutomationImport-DVxK1R1y.js +2 -0
  5. package/dist/interface/assets/{AutomationStore-NyXuw3tI.js → AutomationStore-DaNO9_9S.js} +2 -2
  6. package/dist/interface/assets/{AutomationStoreDetail-Dz9Y8j8g.js → AutomationStoreDetail-CTDCEaIz.js} +2 -2
  7. package/dist/interface/assets/Automations-mdnUitWx.js +2 -0
  8. package/dist/interface/assets/Camera-B3cv5IVC.js +2 -0
  9. package/dist/interface/assets/Camera-LSq1bWmA.css +1 -0
  10. package/dist/interface/assets/{CameraConsole-S9Il0g0K.js → CameraConsole-C32X5I91.js} +2 -2
  11. package/dist/interface/assets/{Cameras-lDMBkoUW.js → Cameras-Drv48-Eb.js} +23 -23
  12. package/dist/interface/assets/Camview-Bvy39jSt.js +1 -0
  13. package/dist/interface/assets/{Camview-CS7PIBMz.css → Camview-Lp3QU0de.css} +1 -1
  14. package/dist/interface/assets/{Config-B0cy1RIU.js → Config-B-LDdaS6.js} +1 -1
  15. package/dist/interface/assets/ConfigActionCameraControl-As2qXvnd.js +1 -0
  16. package/dist/interface/assets/{ConfigActionDelay-DrzMP_7q.js → ConfigActionDelay-Cj3yWfEV.js} +1 -1
  17. package/dist/interface/assets/{ConfigActionHttp-BUANm5zh.js → ConfigActionHttp-PlS5vI0v.js} +1 -1
  18. package/dist/interface/assets/{ConfigActionImageInput-CKvnzs_4.js → ConfigActionImageInput-BHflOA-T.js} +1 -1
  19. package/dist/interface/assets/{ConfigActionMqtt-Cz-fMzXH.js → ConfigActionMqtt-DByOwVPe.js} +1 -1
  20. package/dist/interface/assets/{ConfigActionNotification-exxG4m-k.js → ConfigActionNotification-D_8_lp96.js} +1 -1
  21. package/dist/interface/assets/{ConfigActionNotificationControl-BMFbm9lJ.js → ConfigActionNotificationControl-ClFMom6m.js} +1 -1
  22. package/dist/interface/assets/{ConfigActionOutput-js-UUBci.js → ConfigActionOutput-YTAw-Are.js} +1 -1
  23. package/dist/interface/assets/{ConfigActionPlugin-B5hrC_B6.js → ConfigActionPlugin-lF2IH2Bo.js} +1 -1
  24. package/dist/interface/assets/{ConfigActionSensor-BESCtedA.js → ConfigActionSensor-DQZW_Jxr.js} +1 -1
  25. package/dist/interface/assets/{ConfigActionSnapshot-Uxz9dCqh.js → ConfigActionSnapshot-CHxIQ5J0.js} +1 -1
  26. package/dist/interface/assets/{ConfigActionVariable-CsiC6bgF.js → ConfigActionVariable-0VLGuhwP.js} +1 -1
  27. package/dist/interface/assets/{ConfigConditionIfElse-CiScJB42.js → ConfigConditionIfElse-D-Ibn8QL.js} +1 -1
  28. package/dist/interface/assets/{ConfigConditionSensorState-CbDfsuOl.js → ConfigConditionSensorState-BKSGYRdv.js} +1 -1
  29. package/dist/interface/assets/{ConfigConditionSwitch-B7E5DAoC.js → ConfigConditionSwitch-CzkEyWb1.js} +1 -1
  30. package/dist/interface/assets/{ConfigConditionTime-BmeGYmwH.js → ConfigConditionTime-BFKsDI_t.js} +1 -1
  31. package/dist/interface/assets/{ConfigSensorValueInput-CNTToTaw.js → ConfigSensorValueInput-yTiHhlXG.js} +1 -1
  32. package/dist/interface/assets/ConfigTriggerDetection-DFTgRaE0.js +1 -0
  33. package/dist/interface/assets/ConfigTriggerGeofence-B2Cqi5Id.js +1 -0
  34. package/dist/interface/assets/{ConfigTriggerManual-Z1_1wUUM.js → ConfigTriggerManual-BDK6OYxg.js} +1 -1
  35. package/dist/interface/assets/{ConfigTriggerMqtt-BBvD0ow9.js → ConfigTriggerMqtt-BZO14RrW.js} +1 -1
  36. package/dist/interface/assets/{ConfigTriggerSchedule-B0rMjsZL.js → ConfigTriggerSchedule-sMkzMLgn.js} +1 -1
  37. package/dist/interface/assets/{ConfigTriggerSensor-B1p15KGD.js → ConfigTriggerSensor-D6W_exDW.js} +1 -1
  38. package/dist/interface/assets/{ConfigTriggerSystem-Du-AkCKy.js → ConfigTriggerSystem-D4cx1GqI.js} +1 -1
  39. package/dist/interface/assets/{ConfigTriggerWebhook-Debqt-nw.js → ConfigTriggerWebhook-DxZziGkL.js} +1 -1
  40. package/dist/interface/assets/Console-DjA3ZPU_.js +1 -0
  41. package/dist/interface/assets/CuiAutomationRunsDialog-B4ABK72A.js +1 -0
  42. package/dist/interface/assets/CuiBottomSheet-D1zDWsoK.js +1 -0
  43. package/dist/interface/assets/{CuiBottomSheet-DECWGdrp.js → CuiBottomSheet-N9MHq4yZ.js} +1 -1
  44. package/dist/interface/assets/CuiCameraPipCard-CnWs2fAC.js +1 -0
  45. package/dist/interface/assets/CuiCameraRecordings-CahW-f8_.js +1 -0
  46. package/dist/interface/assets/{CuiCameraShares-D32cM63m.js → CuiCameraShares-B2RdPWWh.js} +1 -1
  47. package/dist/interface/assets/CuiCameraTable-4ikGv8QA.js +1 -0
  48. package/dist/interface/assets/{CuiCameraTable-Bk9N-Oc6.js → CuiCameraTable-BKBvhRDc.js} +2 -2
  49. package/dist/interface/assets/CuiChartTable-CqKo74Tf.css +1 -0
  50. package/dist/interface/assets/CuiChartTable-Lvz9Zq0k.js +1 -0
  51. package/dist/interface/assets/CuiConsole-DUswVhFC.js +5 -0
  52. package/dist/interface/assets/CuiDataTable-CIP95jVF.js +1 -0
  53. package/dist/interface/assets/CuiLightControl-CvfWo8mY.js +1 -0
  54. package/dist/interface/assets/{CuiList-w69dERIq.js → CuiList-CajC6jpE.js} +1 -1
  55. package/dist/interface/assets/CuiPluginCard-CQS8C87-.js +2 -0
  56. package/dist/interface/assets/CuiPluginCategoryChip-DCNG8zl1.js +1 -0
  57. package/dist/interface/assets/{CuiPluginOAuthButton-CNxj7cuf.js → CuiPluginOAuthButton-CvZrOWr8.js} +2 -2
  58. package/dist/interface/assets/{CuiPluginTrustBadge-DYsEz6WI.js → CuiPluginTrustBadge-Bc1dQgX8.js} +1 -1
  59. package/dist/interface/assets/CuiQRCode-CjLcB644.js +1 -0
  60. package/dist/interface/assets/CuiRecordingsGrid-DrpErbux.js +1 -0
  61. package/dist/interface/assets/{CuiRecordingsGrid-Q4C3aeLo.css → CuiRecordingsGrid-EAumRo5E.css} +1 -1
  62. package/dist/interface/assets/CuiSchema-BYYeh269.css +1 -0
  63. package/dist/interface/assets/CuiSchema-DG8peseE.js +1 -0
  64. package/dist/interface/assets/{CuiTopNavbarItem-7yu9c68R.js → CuiTopNavbarItem-Dq3Esl1S.js} +1 -1
  65. package/dist/interface/assets/Faces-D0yRgOMj.js +1 -0
  66. package/dist/interface/assets/FirstSteps-BW8AVmd4.js +1 -0
  67. package/dist/interface/assets/{Instances-Bmb-yWM-.js → Instances-BgXJvs0_.js} +1 -1
  68. package/dist/interface/assets/{Logs-DxHqkMpx.js → Logs-DH3rWnlV.js} +2 -2
  69. package/dist/interface/assets/Menu-D9FVgjwT.js +1 -0
  70. package/dist/interface/assets/Metrics-7bYWyGWU.js +9 -0
  71. package/dist/interface/assets/Metrics-DX59XXDc.css +1 -0
  72. package/dist/interface/assets/{OAuthDeviceFlow-CLi6l8jS.js → OAuthDeviceFlow-BS0FQbZh.js} +1 -1
  73. package/dist/interface/assets/Plugin-ZwkTp6C4.js +1 -0
  74. package/dist/interface/assets/{PluginConsole-xOY_kwz_.js → PluginConsole-Dt39TWaO.js} +2 -2
  75. package/dist/interface/assets/PluginDetail-D7ZtOG9Y.js +2 -0
  76. package/dist/interface/assets/{PluginSchema-DG3ou73v.js → PluginSchema-8T7EBq-Z.js} +1 -1
  77. package/dist/interface/assets/PluginSearch-DN3o0wfs.js +1 -0
  78. package/dist/interface/assets/PluginSearch-eRDCvu_8.js +2 -0
  79. package/dist/interface/assets/{PluginUpdateProgress-Bx0MXwAk.js → PluginUpdateProgress-CxjpzkGa.js} +1 -1
  80. package/dist/interface/assets/Plugins-DVsMpyvr.js +1 -0
  81. package/dist/interface/assets/Recordings-BKxSEiQo.js +1 -0
  82. package/dist/interface/assets/Recordings-cR4_6aGN.css +1 -0
  83. package/dist/interface/assets/SensorEdit-BmaMQKEa.js +1 -0
  84. package/dist/interface/assets/{SensorHistory-CTIUwIWN.js → SensorHistory-CmOAk-tD.js} +1 -1
  85. package/dist/interface/assets/Sensors-x08al7Je.js +2 -0
  86. package/dist/interface/assets/Settings-Cp1lzG1i.js +1 -0
  87. package/dist/interface/assets/SettingsAccount-BwCNddkW.js +1 -0
  88. package/dist/interface/assets/SettingsAccount-DC9AxqHe.js +1 -0
  89. package/dist/interface/assets/SettingsAppearance-CMzzDZCX.js +1 -0
  90. package/dist/interface/assets/{SettingsAppearance-CoTdsIwM.js → SettingsAppearance-Dr8hJloI.js} +1 -1
  91. package/dist/interface/assets/SettingsBackup-Dc8pDM3Y.js +1 -0
  92. package/dist/interface/assets/{SettingsList-W5FvbkXm.js → SettingsList-3peF3gFZ.js} +1 -1
  93. package/dist/interface/assets/SettingsMqtt-k-cHaOoA.js +1 -0
  94. package/dist/interface/assets/SettingsNotifications-BJHYlSKX.js +1 -0
  95. package/dist/interface/assets/SettingsPermissions-CymqvfR9.js +1 -0
  96. package/dist/interface/assets/{SettingsRecordings-COUi9BQg.js → SettingsRecordings-gWTD_djR.js} +1 -1
  97. package/dist/interface/assets/SettingsRemote-BWAhI1Gw.js +2 -0
  98. package/dist/interface/assets/SettingsRemote-CtP6Gz9C.css +1 -0
  99. package/dist/interface/assets/SettingsSystem-BGBUQvjg.js +2 -0
  100. package/dist/interface/assets/SettingsUsers-B_Ri5OrM.js +1 -0
  101. package/dist/interface/assets/Terminal-eYxvgFHE.js +4 -0
  102. package/dist/interface/assets/Updates-CweZekTf.js +2 -0
  103. package/dist/interface/assets/{VariableInput-DhAhhfsm.js → VariableInput-CM6T3pUW.js} +1 -1
  104. package/dist/interface/assets/{VariableSuggestions-q0Jd1DVI.js → VariableSuggestions-BGxZxS6U.js} +1 -1
  105. package/dist/interface/assets/{VersionsHandler-XEn3uFy7.js → VersionsHandler-BWIiBPPa.js} +2 -2
  106. package/dist/interface/assets/{VirtualSensorCreate-_E5E0U7E.js → VirtualSensorCreate-CFQC2-9D.js} +1 -1
  107. package/dist/interface/assets/Workers-B-sp43t3.js +2 -0
  108. package/dist/interface/assets/{Workers-7hKMBU4w.css → Workers-DH59zE1C.css} +1 -1
  109. package/dist/interface/assets/{api-C7Yv5Scr.js → api-CG0j1twy.js} +1 -1
  110. package/dist/interface/assets/api-CVOj5SYs.css +2 -0
  111. package/dist/interface/assets/api-C_bOaXk2.js +2 -0
  112. package/dist/interface/assets/{auth-BixzEnBM.js → auth-BPSOBTDX.js} +1 -1
  113. package/dist/interface/assets/{automations-K8XPcP3U.js → automations-BrtwUIUz.js} +1 -1
  114. package/dist/interface/assets/brain-7IVdhMyV.js +1 -0
  115. package/dist/interface/assets/cameras.schema-CoIOnGzE.js +1 -0
  116. package/dist/interface/assets/cloudHandoff-BiFzJEYx.js +1 -0
  117. package/dist/interface/assets/{config-CURuT8_k.js → config-BjldP5A6.js} +1 -1
  118. package/dist/interface/assets/content-copy-CLkJTA4w.js +1 -0
  119. package/dist/interface/assets/eventLabels-BsPoPWS9.js +1 -0
  120. package/dist/interface/assets/{flowSchema-szT9B0sA.js → flowSchema-B_Otn6RU.js} +1 -1
  121. package/dist/interface/assets/{intercomService-BjB4QQiX.js → intercomService-BpTOoSQi.js} +1 -1
  122. package/dist/interface/assets/lan-BP0NuDr7.js +1 -0
  123. package/dist/interface/assets/main-BG-VCb--.js +3 -0
  124. package/dist/interface/assets/main-CFQGq2Yn.css +1 -0
  125. package/dist/interface/assets/{mqtt-uItF3Vd7.js → mqtt-DSPPQwxJ.js} +1 -1
  126. package/dist/interface/assets/{notifications-CuZPzI4-.js → notifications-DEEM2oP2.js} +1 -1
  127. package/dist/interface/assets/{plugins-QEqfAk0G.js → plugins-DBBFdZKH.js} +1 -1
  128. package/dist/interface/assets/refresh-Ds2EayhZ.js +1 -0
  129. package/dist/interface/assets/sensorIcons-CP5EQZ60.js +1 -0
  130. package/dist/interface/assets/{sensors-BxvL01cB.js → sensors-CKTPsYYH.js} +1 -1
  131. package/dist/interface/assets/{server-CDm6FwHH.js → server-B_MuqJ5z.js} +1 -1
  132. package/dist/interface/assets/{theme-CRw47tnu.js → theme-FztlStyg.js} +1 -1
  133. package/dist/interface/assets/types-BHAzjgi3.js +1 -0
  134. package/dist/interface/assets/types-G1_espGV.js +1 -0
  135. package/dist/interface/assets/{types-DtdpTPfZ.js → types-bCt9IrFD.js} +1 -1
  136. package/dist/interface/assets/updates-CVapvGAI.js +1 -0
  137. package/dist/interface/assets/{useAppVersion-BxTTVLKg.js → useAppVersion-SrF7tGCe.js} +1 -1
  138. package/dist/interface/assets/useBackupRestore-Tc23Huvm.js +1 -0
  139. package/dist/interface/assets/{useCameraOptions-DMC2KjMS.js → useCameraOptions-zsIqf0i9.js} +1 -1
  140. package/dist/interface/assets/{useLogsSocket-CmyalQi8.js → useLogsSocket-DSltWBh4.js} +1 -1
  141. package/dist/interface/assets/{useNavLayout-BacPxpZv.js → useNavLayout-BhlxDHV7.js} +1 -1
  142. package/dist/interface/assets/{usePermissions-C2wQBWmH.js → usePermissions-4HqGU99G.js} +1 -1
  143. package/dist/interface/assets/{usePluginUpdates-CyjxFTTi.js → usePluginUpdates-CKH0Hb87.js} +1 -1
  144. package/dist/interface/assets/{usePushRegistration-BPSfvI_w.js → usePushRegistration-DAE3goyn.js} +1 -1
  145. package/dist/interface/assets/{useServerRestart-x8p2rFqn.js → useServerRestart-DoLrF47t.js} +1 -1
  146. package/dist/interface/assets/{useStreamStatus-PMpl519O.js → useStreamStatus-B3QIWjDE.js} +1 -1
  147. package/dist/interface/assets/useTabSwipe-BYFLQ9EY.js +1 -0
  148. package/dist/interface/assets/{useUpdateChannel-ChscS5KW.js → useUpdateChannel-DWEw-ZyB.js} +1 -1
  149. package/dist/interface/assets/useUpdatesSocket-CoxkS7Hr.js +1 -0
  150. package/dist/interface/assets/{useUserOptions-YiEj2ox6.js → useUserOptions-DCDeuz2p.js} +1 -1
  151. package/dist/interface/assets/vendor-cameraui-4iwjOYwz.js +2326 -0
  152. package/dist/interface/assets/vendor-core-gJradu21.js +1 -0
  153. package/dist/interface/assets/{vendor-primevue-BDu2BpXv.js → vendor-primevue-DMAuiM9b.js} +72 -72
  154. package/dist/interface/index.html +32 -27
  155. package/dist/interface/sw.js +1 -1
  156. package/dist/server/package.json +13 -12
  157. package/dist/server/requirements.txt +2 -2
  158. package/dist/server/src/_camera_ui_tools/camera_ui_rpc/client.py +8 -0
  159. package/dist/server/src/_camera_ui_tools/camera_ui_rpc/types.py +20 -0
  160. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/__init__.py +8 -2
  161. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/__init__.py +10 -2
  162. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/config.py +9 -0
  163. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/detection.py +24 -10
  164. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/enums.py +6 -7
  165. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/notification.py +28 -0
  166. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/plugin/interfaces.py +24 -0
  167. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/__init__.py +4 -0
  168. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/base.py +18 -0
  169. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/detection.py +3 -3
  170. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/object.py +6 -1
  171. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/spec.py +27 -3
  172. package/dist/server/src/api/controllers/auth.controller.js +5 -0
  173. package/dist/server/src/api/controllers/auth.controller.js.map +1 -1
  174. package/dist/server/src/api/controllers/frameWorkers.controller.d.ts +14 -0
  175. package/dist/server/src/api/controllers/frameWorkers.controller.js +96 -0
  176. package/dist/server/src/api/controllers/frameWorkers.controller.js.map +1 -1
  177. package/dist/server/src/api/controllers/remote.controller.d.ts +1 -0
  178. package/dist/server/src/api/controllers/remote.controller.js +16 -12
  179. package/dist/server/src/api/controllers/remote.controller.js.map +1 -1
  180. package/dist/server/src/api/controllers/server.controller.js +6 -2
  181. package/dist/server/src/api/controllers/server.controller.js.map +1 -1
  182. package/dist/server/src/api/controllers/updates.controller.d.ts +8 -0
  183. package/dist/server/src/api/controllers/updates.controller.js +40 -0
  184. package/dist/server/src/api/controllers/updates.controller.js.map +1 -0
  185. package/dist/server/src/api/controllers/workers.controller.js +8 -2
  186. package/dist/server/src/api/controllers/workers.controller.js.map +1 -1
  187. package/dist/server/src/api/database/constants.d.ts +0 -1
  188. package/dist/server/src/api/database/constants.js +0 -1
  189. package/dist/server/src/api/database/constants.js.map +1 -1
  190. package/dist/server/src/api/database/index.d.ts +2 -3
  191. package/dist/server/src/api/database/index.js +3 -6
  192. package/dist/server/src/api/database/index.js.map +1 -1
  193. package/dist/server/src/api/database/migrations/v2-1-10.js +33 -0
  194. package/dist/server/src/api/database/migrations/v2-1-10.js.map +1 -0
  195. package/dist/server/src/api/database/migrations/v2-1-11.js +31 -0
  196. package/dist/server/src/api/database/migrations/v2-1-11.js.map +1 -0
  197. package/dist/server/src/api/database/migrations/v2-1-12.js +25 -0
  198. package/dist/server/src/api/database/migrations/v2-1-12.js.map +1 -0
  199. package/dist/server/src/api/database/migrations/v2-1-13.d.ts +3 -0
  200. package/dist/server/src/api/database/migrations/v2-1-13.js +51 -0
  201. package/dist/server/src/api/database/migrations/v2-1-13.js.map +1 -0
  202. package/dist/server/src/api/database/migrations/v2-1-14.d.ts +3 -0
  203. package/dist/server/src/api/database/migrations/v2-1-14.js +87 -0
  204. package/dist/server/src/api/database/migrations/v2-1-14.js.map +1 -0
  205. package/dist/server/src/api/database/migrations/v2-1-6.js.map +1 -1
  206. package/dist/server/src/api/database/migrations/v2-1-9.d.ts +3 -0
  207. package/dist/server/src/api/database/migrations/v2-1-9.js +38 -0
  208. package/dist/server/src/api/database/migrations/v2-1-9.js.map +1 -0
  209. package/dist/server/src/api/database/record-schemas.d.ts +18 -13
  210. package/dist/server/src/api/database/record-schemas.js +3 -3
  211. package/dist/server/src/api/database/record-schemas.js.map +1 -1
  212. package/dist/server/src/api/database/types.d.ts +3 -11
  213. package/dist/server/src/api/routes/frameWorkers.routes.js +22 -1
  214. package/dist/server/src/api/routes/frameWorkers.routes.js.map +1 -1
  215. package/dist/server/src/api/routes/index.js +3 -1
  216. package/dist/server/src/api/routes/index.js.map +1 -1
  217. package/dist/server/src/api/routes/updates.routes.d.ts +2 -0
  218. package/dist/server/src/api/routes/updates.routes.js +47 -0
  219. package/dist/server/src/api/routes/updates.routes.js.map +1 -0
  220. package/dist/server/src/api/routes/workers.routes.js +3 -2
  221. package/dist/server/src/api/routes/workers.routes.js.map +1 -1
  222. package/dist/server/src/api/schemas/cameras.schema.d.ts +228 -79
  223. package/dist/server/src/api/schemas/cameras.schema.js +58 -9
  224. package/dist/server/src/api/schemas/cameras.schema.js.map +1 -1
  225. package/dist/server/src/api/schemas/frameWorkers.schema.d.ts +6 -0
  226. package/dist/server/src/api/schemas/frameWorkers.schema.js +7 -0
  227. package/dist/server/src/api/schemas/frameWorkers.schema.js.map +1 -1
  228. package/dist/server/src/api/schemas/server.schema.d.ts +1 -0
  229. package/dist/server/src/api/schemas/server.schema.js +4 -0
  230. package/dist/server/src/api/schemas/server.schema.js.map +1 -1
  231. package/dist/server/src/api/schemas/users.schema.d.ts +119 -84
  232. package/dist/server/src/api/schemas/users.schema.js +3 -0
  233. package/dist/server/src/api/schemas/users.schema.js.map +1 -1
  234. package/dist/server/src/api/schemas/workers.schema.d.ts +3 -0
  235. package/dist/server/src/api/schemas/workers.schema.js +5 -0
  236. package/dist/server/src/api/schemas/workers.schema.js.map +1 -1
  237. package/dist/server/src/api/services/plugins.service.d.ts +4 -1
  238. package/dist/server/src/api/services/plugins.service.js +23 -4
  239. package/dist/server/src/api/services/plugins.service.js.map +1 -1
  240. package/dist/server/src/api/services/server.service.d.ts +3 -1
  241. package/dist/server/src/api/services/server.service.js +6 -1
  242. package/dist/server/src/api/services/server.service.js.map +1 -1
  243. package/dist/server/src/api/services/updates.service.d.ts +108 -0
  244. package/dist/server/src/api/services/updates.service.js +476 -0
  245. package/dist/server/src/api/services/updates.service.js.map +1 -0
  246. package/dist/server/src/api/types/index.d.ts +6 -0
  247. package/dist/server/src/api/types/index.js.map +1 -1
  248. package/dist/server/src/api/websocket/index.js +2 -0
  249. package/dist/server/src/api/websocket/index.js.map +1 -1
  250. package/dist/server/src/api/websocket/nsp/metrics.d.ts +3 -1
  251. package/dist/server/src/api/websocket/nsp/metrics.js +90 -37
  252. package/dist/server/src/api/websocket/nsp/metrics.js.map +1 -1
  253. package/dist/server/src/api/websocket/nsp/server.js +45 -9
  254. package/dist/server/src/api/websocket/nsp/server.js.map +1 -1
  255. package/dist/server/src/api/websocket/types.d.ts +23 -17
  256. package/dist/server/src/camera/controller.js +1 -0
  257. package/dist/server/src/camera/controller.js.map +1 -1
  258. package/dist/server/src/camera/decoder/audio-loop.d.ts +0 -1
  259. package/dist/server/src/camera/decoder/audio-loop.js +38 -68
  260. package/dist/server/src/camera/decoder/audio-loop.js.map +1 -1
  261. package/dist/server/src/camera/decoder/child.d.ts +4 -1
  262. package/dist/server/src/camera/decoder/child.js +27 -0
  263. package/dist/server/src/camera/decoder/child.js.map +1 -1
  264. package/dist/server/src/camera/decoder/debug/detection-record.d.ts +6 -0
  265. package/dist/server/src/camera/decoder/debug/detection-record.js +21 -0
  266. package/dist/server/src/camera/decoder/debug/detection-record.js.map +1 -1
  267. package/dist/server/src/camera/decoder/detection-coordinator.d.ts +26 -3
  268. package/dist/server/src/camera/decoder/detection-coordinator.js +525 -207
  269. package/dist/server/src/camera/decoder/detection-coordinator.js.map +1 -1
  270. package/dist/server/src/camera/decoder/detection-pipeline.d.ts +4 -2
  271. package/dist/server/src/camera/decoder/detection-pipeline.js +17 -10
  272. package/dist/server/src/camera/decoder/detection-pipeline.js.map +1 -1
  273. package/dist/server/src/camera/decoder/detection-window.d.ts +17 -0
  274. package/dist/server/src/camera/decoder/detection-window.js +188 -0
  275. package/dist/server/src/camera/decoder/detection-window.js.map +1 -0
  276. package/dist/server/src/camera/decoder/event-manager.d.ts +1 -0
  277. package/dist/server/src/camera/decoder/event-manager.js +12 -0
  278. package/dist/server/src/camera/decoder/event-manager.js.map +1 -1
  279. package/dist/server/src/camera/decoder/event-thumbnailer.d.ts +3 -3
  280. package/dist/server/src/camera/decoder/event-thumbnailer.js +2 -2
  281. package/dist/server/src/camera/decoder/event-thumbnailer.js.map +1 -1
  282. package/dist/server/src/camera/decoder/frame-scaler.d.ts +14 -1
  283. package/dist/server/src/camera/decoder/frame-scaler.js +63 -20
  284. package/dist/server/src/camera/decoder/frame-scaler.js.map +1 -1
  285. package/dist/server/src/camera/decoder/motion-localizer.d.ts +16 -0
  286. package/dist/server/src/camera/decoder/motion-localizer.js +49 -0
  287. package/dist/server/src/camera/decoder/motion-localizer.js.map +1 -0
  288. package/dist/server/src/camera/decoder/perf-tracker.d.ts +29 -8
  289. package/dist/server/src/camera/decoder/perf-tracker.js +76 -50
  290. package/dist/server/src/camera/decoder/perf-tracker.js.map +1 -1
  291. package/dist/server/src/camera/decoder/plugin-registry.d.ts +1 -0
  292. package/dist/server/src/camera/decoder/plugin-registry.js +8 -6
  293. package/dist/server/src/camera/decoder/plugin-registry.js.map +1 -1
  294. package/dist/server/src/camera/decoder/secondary-stage.d.ts +4 -1
  295. package/dist/server/src/camera/decoder/secondary-stage.js +26 -3
  296. package/dist/server/src/camera/decoder/secondary-stage.js.map +1 -1
  297. package/dist/server/src/camera/decoder/sources/analysis-source.d.ts +15 -1
  298. package/dist/server/src/camera/decoder/sources/audio-source.d.ts +7 -3
  299. package/dist/server/src/camera/decoder/sources/audio-source.js +89 -78
  300. package/dist/server/src/camera/decoder/sources/audio-source.js.map +1 -1
  301. package/dist/server/src/camera/decoder/sources/buffered-source.d.ts +27 -9
  302. package/dist/server/src/camera/decoder/sources/buffered-source.js +151 -65
  303. package/dist/server/src/camera/decoder/sources/buffered-source.js.map +1 -1
  304. package/dist/server/src/camera/decoder/sources/frame-handle.d.ts +13 -0
  305. package/dist/server/src/camera/decoder/sources/frame-handle.js +106 -0
  306. package/dist/server/src/camera/decoder/sources/frame-handle.js.map +1 -0
  307. package/dist/server/src/camera/decoder/sources/frame-source.d.ts +25 -26
  308. package/dist/server/src/camera/decoder/sources/frame-source.js +213 -268
  309. package/dist/server/src/camera/decoder/sources/frame-source.js.map +1 -1
  310. package/dist/server/src/camera/decoder/sources/reconnect-loop.d.ts +17 -0
  311. package/dist/server/src/camera/decoder/sources/reconnect-loop.js +59 -0
  312. package/dist/server/src/camera/decoder/sources/reconnect-loop.js.map +1 -0
  313. package/dist/server/src/camera/decoder/sources/snapshot-fetcher.d.ts +15 -0
  314. package/dist/server/src/camera/decoder/sources/snapshot-fetcher.js +58 -0
  315. package/dist/server/src/camera/decoder/sources/snapshot-fetcher.js.map +1 -0
  316. package/dist/server/src/camera/decoder/sources/video-input.d.ts +18 -0
  317. package/dist/server/src/camera/decoder/sources/video-input.js +55 -0
  318. package/dist/server/src/camera/decoder/sources/video-input.js.map +1 -0
  319. package/dist/server/src/camera/decoder/types.d.ts +74 -18
  320. package/dist/server/src/camera/decoder/types.js +2 -0
  321. package/dist/server/src/camera/decoder/types.js.map +1 -1
  322. package/dist/server/src/camera/decoder/worker.d.ts +5 -1
  323. package/dist/server/src/camera/decoder/worker.js +35 -5
  324. package/dist/server/src/camera/decoder/worker.js.map +1 -1
  325. package/dist/server/src/camera/index.d.ts +2 -1
  326. package/dist/server/src/camera/index.js +3 -0
  327. package/dist/server/src/camera/index.js.map +1 -1
  328. package/dist/server/src/camera/streaming/fmp4-session.js +1 -1
  329. package/dist/server/src/camera/streaming/fmp4-session.js.map +1 -1
  330. package/dist/server/src/camera/streaming/rtp-session.js +1 -1
  331. package/dist/server/src/camera/streaming/rtp-session.js.map +1 -1
  332. package/dist/server/src/main.js +22 -2
  333. package/dist/server/src/main.js.map +1 -1
  334. package/dist/server/src/manager/types.d.ts +1 -0
  335. package/dist/server/src/manager/types.js +2 -0
  336. package/dist/server/src/manager/types.js.map +1 -1
  337. package/dist/server/src/mqtt/bridge.d.ts +0 -1
  338. package/dist/server/src/mqtt/bridge.js +0 -33
  339. package/dist/server/src/mqtt/bridge.js.map +1 -1
  340. package/dist/server/src/mqtt/ha-discovery.js +0 -1
  341. package/dist/server/src/mqtt/ha-discovery.js.map +1 -1
  342. package/dist/server/src/plugins/index.js +0 -2
  343. package/dist/server/src/plugins/index.js.map +1 -1
  344. package/dist/server/src/plugins/runtime/node/proxy/cameraDevice.js +9 -0
  345. package/dist/server/src/plugins/runtime/node/proxy/cameraDevice.js.map +1 -1
  346. package/dist/server/src/plugins/runtime/node/proxy/sensorManager.js +8 -0
  347. package/dist/server/src/plugins/runtime/node/proxy/sensorManager.js.map +1 -1
  348. package/dist/server/src/plugins/runtime/python/proxy/camera_device.py +14 -0
  349. package/dist/server/src/plugins/runtime/python/proxy/sensor.py +1 -0
  350. package/dist/server/src/plugins/runtime/python/proxy/sensor_manager.py +13 -0
  351. package/dist/server/src/remote/services/cloudflare-managed.d.ts +2 -0
  352. package/dist/server/src/remote/services/cloudflare-managed.js +9 -2
  353. package/dist/server/src/remote/services/cloudflare-managed.js.map +1 -1
  354. package/dist/server/src/remote/services/cloudflare.d.ts +3 -0
  355. package/dist/server/src/remote/services/cloudflare.js +11 -0
  356. package/dist/server/src/remote/services/cloudflare.js.map +1 -1
  357. package/dist/server/src/remote/services/index.d.ts +2 -0
  358. package/dist/server/src/remote/services/index.js +60 -8
  359. package/dist/server/src/remote/services/index.js.map +1 -1
  360. package/dist/server/src/remote/services/tunnelConnections.d.ts +9 -0
  361. package/dist/server/src/remote/services/tunnelConnections.js +45 -0
  362. package/dist/server/src/remote/services/tunnelConnections.js.map +1 -0
  363. package/dist/server/src/remote/services/urlReadiness.d.ts +2 -0
  364. package/dist/server/src/remote/services/urlReadiness.js +7 -4
  365. package/dist/server/src/remote/services/urlReadiness.js.map +1 -1
  366. package/dist/server/src/remote/types.d.ts +1 -2
  367. package/dist/server/src/rpc/interfaces/frameworker.d.ts +4 -1
  368. package/dist/server/src/rpc/interfaces/sensor.d.ts +1 -0
  369. package/dist/server/src/rpc/leafConnection.d.ts +12 -0
  370. package/dist/server/src/rpc/leafConnection.js +60 -0
  371. package/dist/server/src/rpc/leafConnection.js.map +1 -0
  372. package/dist/server/src/rpc/server.d.ts +2 -0
  373. package/dist/server/src/rpc/server.js +7 -1
  374. package/dist/server/src/rpc/server.js.map +1 -1
  375. package/dist/server/src/sensors/registry.d.ts +2 -0
  376. package/dist/server/src/sensors/registry.js +16 -0
  377. package/dist/server/src/sensors/registry.js.map +1 -1
  378. package/dist/server/src/services/config/index.js +2 -0
  379. package/dist/server/src/services/config/index.js.map +1 -1
  380. package/dist/server/src/types.d.ts +10 -1
  381. package/dist/server/src/utils/ipc.d.ts +9 -0
  382. package/dist/server/src/utils/ipc.js +31 -5
  383. package/dist/server/src/utils/ipc.js.map +1 -1
  384. package/dist/server/src/utils/plugin-registry/index.js +1 -1
  385. package/dist/server/src/utils/plugin-registry/index.js.map +1 -1
  386. package/dist/server/src/utils/system-info.d.ts +9 -0
  387. package/dist/server/src/utils/system-info.js +48 -0
  388. package/dist/server/src/utils/system-info.js.map +1 -0
  389. package/dist/server/src/workers/agent.d.ts +3 -0
  390. package/dist/server/src/workers/agent.js +36 -8
  391. package/dist/server/src/workers/agent.js.map +1 -1
  392. package/dist/server/src/workers/capabilities/frame-decoding.d.ts +2 -2
  393. package/dist/server/src/workers/capabilities/frame-decoding.js +5 -5
  394. package/dist/server/src/workers/capabilities/frame-decoding.js.map +1 -1
  395. package/dist/server/src/workers/capabilities/handler.d.ts +2 -2
  396. package/dist/server/src/workers/capabilities/plugin-host.d.ts +2 -2
  397. package/dist/server/src/workers/capabilities/plugin-host.js +6 -6
  398. package/dist/server/src/workers/capabilities/plugin-host.js.map +1 -1
  399. package/dist/server/src/workers/manager.d.ts +9 -1
  400. package/dist/server/src/workers/manager.js +24 -0
  401. package/dist/server/src/workers/manager.js.map +1 -1
  402. package/dist/server/src/workers/types.d.ts +14 -0
  403. package/dist/server/src/workers/types.js.map +1 -1
  404. package/package.json +13 -12
  405. package/dist/interface/assets/Automation-DDNhcjDg.js +0 -3
  406. package/dist/interface/assets/AutomationImport-DAeFd-SZ.js +0 -2
  407. package/dist/interface/assets/Automations-BeGe7iQE.js +0 -2
  408. package/dist/interface/assets/Camera-C0sNg85L.js +0 -2
  409. package/dist/interface/assets/Camera-C5gF7yVN.css +0 -1
  410. package/dist/interface/assets/Camview-DzMmXQvW.js +0 -1
  411. package/dist/interface/assets/ConfigActionCameraControl-BMlRbdRM.js +0 -1
  412. package/dist/interface/assets/ConfigTriggerDetection-ijTkd_SS.js +0 -1
  413. package/dist/interface/assets/ConfigTriggerGeofence-Q7LQpVoM.js +0 -1
  414. package/dist/interface/assets/Console-Gu1eu82G.js +0 -1
  415. package/dist/interface/assets/CuiAutomationRunsDialog-CxcWpqrI.js +0 -1
  416. package/dist/interface/assets/CuiBottomSheet-BkmkKf1u.js +0 -1
  417. package/dist/interface/assets/CuiCameraPipCard-CuZZZ7Cv.js +0 -1
  418. package/dist/interface/assets/CuiCameraRecordings-BxEA_Vhd.js +0 -1
  419. package/dist/interface/assets/CuiCameraTable-67Itb7my.js +0 -1
  420. package/dist/interface/assets/CuiConsole-De9JpSLz.js +0 -5
  421. package/dist/interface/assets/CuiDataTable-P8bWN0ZK.js +0 -1
  422. package/dist/interface/assets/CuiLightControl-D5Z1AqWH.js +0 -1
  423. package/dist/interface/assets/CuiPluginCard-D3x9FPb6.js +0 -2
  424. package/dist/interface/assets/CuiPluginCategoryChip-CQ2FJafE.js +0 -1
  425. package/dist/interface/assets/CuiQRCode-CZnspFFw.js +0 -1
  426. package/dist/interface/assets/CuiRecordingsGrid-wEaU6-3c.js +0 -1
  427. package/dist/interface/assets/CuiSchema--yMBD996.css +0 -1
  428. package/dist/interface/assets/CuiSchema-DLV1lIiB.js +0 -1
  429. package/dist/interface/assets/Faces-BlPQek1-.js +0 -1
  430. package/dist/interface/assets/FirstSteps-C-DKv_P2.js +0 -1
  431. package/dist/interface/assets/Menu-DceReBlq.js +0 -1
  432. package/dist/interface/assets/Metrics-BstlZavQ.css +0 -1
  433. package/dist/interface/assets/Metrics-Cw_Z_e0C.js +0 -1
  434. package/dist/interface/assets/Plugin-BBVxbQeT.js +0 -1
  435. package/dist/interface/assets/PluginDetail-D2RncxpL.js +0 -2
  436. package/dist/interface/assets/PluginSearch-CBhloRm6.js +0 -1
  437. package/dist/interface/assets/PluginSearch-RGhSeM1T.js +0 -2
  438. package/dist/interface/assets/Plugins-COHwVV0A.js +0 -1
  439. package/dist/interface/assets/Recordings-H5qOm_rD.js +0 -1
  440. package/dist/interface/assets/Recordings-zdTzwv7C.css +0 -1
  441. package/dist/interface/assets/SensorEdit-C-H_3ap8.js +0 -1
  442. package/dist/interface/assets/Sensors-lPcOdHJN.js +0 -2
  443. package/dist/interface/assets/Settings-BUoXoFiH.js +0 -1
  444. package/dist/interface/assets/SettingsAccount-CudWgknL.js +0 -1
  445. package/dist/interface/assets/SettingsAccount-DUy_W02_.js +0 -1
  446. package/dist/interface/assets/SettingsAppearance-eZsdWWp3.js +0 -1
  447. package/dist/interface/assets/SettingsBackup-ibb5I5Jv.js +0 -1
  448. package/dist/interface/assets/SettingsMqtt-Ct42icAW.js +0 -1
  449. package/dist/interface/assets/SettingsNotifications-B4lyMlLR.js +0 -1
  450. package/dist/interface/assets/SettingsPermissions-Bsv293ri.js +0 -1
  451. package/dist/interface/assets/SettingsRemote-DYld6-1F.css +0 -1
  452. package/dist/interface/assets/SettingsRemote-Dq_BD0fk.js +0 -1
  453. package/dist/interface/assets/SettingsSystem-C5xROdA0.js +0 -2
  454. package/dist/interface/assets/SettingsUsers-Cjk6mevW.js +0 -1
  455. package/dist/interface/assets/Terminal-BIdBrL4g.js +0 -4
  456. package/dist/interface/assets/Workers-DVABUc39.js +0 -2
  457. package/dist/interface/assets/api-AVHJeggR.css +0 -2
  458. package/dist/interface/assets/api-CmdQlVDD.js +0 -2
  459. package/dist/interface/assets/cameras.schema-A_6JklLs.js +0 -1
  460. package/dist/interface/assets/cloudHandoff-D0ytFfHK.js +0 -1
  461. package/dist/interface/assets/deployed-code-update-xHE-blDN.js +0 -1
  462. package/dist/interface/assets/main-B1ryIyTr.css +0 -1
  463. package/dist/interface/assets/main-BdIdmDhs.js +0 -3
  464. package/dist/interface/assets/round-restart-alt-B_7HR24a.js +0 -1
  465. package/dist/interface/assets/round-restart-alt-CmrjVFat.css +0 -1
  466. package/dist/interface/assets/sensorIcons-BujySw0j.js +0 -1
  467. package/dist/interface/assets/types-R10eIequ.js +0 -1
  468. package/dist/interface/assets/types-p_uC0rfp.js +0 -1
  469. package/dist/interface/assets/useBackupRestore-DLieDBFC.js +0 -1
  470. package/dist/interface/assets/vendor-cameraui-Cf0XVNQ7.js +0 -2326
  471. package/dist/interface/assets/vendor-core-u4xNQ-aF.js +0 -1
  472. package/dist/server/src/api/database/migrations/v2-0-50.js +0 -45
  473. package/dist/server/src/api/database/migrations/v2-0-50.js.map +0 -1
  474. package/dist/server/src/api/database/migrations/v2-0-51.js +0 -43
  475. package/dist/server/src/api/database/migrations/v2-0-51.js.map +0 -1
  476. package/dist/server/src/api/database/migrations/v2-1-0.js +0 -72
  477. package/dist/server/src/api/database/migrations/v2-1-0.js.map +0 -1
  478. /package/dist/server/src/api/database/migrations/{v2-0-50.d.ts → v2-1-10.d.ts} +0 -0
  479. /package/dist/server/src/api/database/migrations/{v2-0-51.d.ts → v2-1-11.d.ts} +0 -0
  480. /package/dist/server/src/api/database/migrations/{v2-1-0.d.ts → v2-1-12.d.ts} +0 -0
@@ -19,19 +19,21 @@ import { AudioDetectionLoop } from './audio-loop.js';
19
19
  import { CascadeManager } from './cascade-manager.js';
20
20
  import { detectionRecord } from './debug/detection-record.js';
21
21
  import { DetectionPipeline } from './detection-pipeline.js';
22
+ import { clusterBoxes, DetectionWindow, mergeWindowDetections, MOTION_PAD, planWindowsOnce, TRACK_PAD } from './detection-window.js';
22
23
  import { DwellManager } from './dwell-manager.js';
23
24
  import { DetectionEventManager, MOMENT_RANK_ATTRIBUTE, MOMENT_RANK_OBJECT } from './event-manager.js';
24
25
  import { EventThumbnailer } from './event-thumbnailer.js';
25
26
  import { FrameScaler } from './frame-scaler.js';
26
27
  import { directionBetween, directionOf, MOMENT_FORMATS, MOMENT_QUALITY, momentWindow, unionBox } from './moment-crop.js';
28
+ import { MotionLocalizer } from './motion-localizer.js';
27
29
  import { PerfTracker } from './perf-tracker.js';
28
30
  import { normalizePlateText } from './plate-vote.js';
29
- import { isVideoInputSpec, PluginRegistry } from './plugin-registry.js';
31
+ import { isVideoInputSpec, modelIdentity, PluginRegistry } from './plugin-registry.js';
30
32
  import { PtzAutotracker } from './ptz/autotracker.js';
31
- import { ReconnectBackoff } from './reconnect-backoff.js';
32
33
  import { SecondaryStage } from './secondary-stage.js';
34
+ import { BufferedSource } from './sources/buffered-source.js';
33
35
  import { FrameSource } from './sources/frame-source.js';
34
- import { DETECT_TIMEOUT_MS, ensureDetectionBoxes, isFullFrameBox, MOTION_WIDTH_MAP } from './types.js';
36
+ import { DETECT_TIMEOUT_MS, DETECTOR_METRIC_TYPES, ensureDetectionBoxes, isFullFrameBox, MOTION_WIDTH_MAP } from './types.js';
35
37
  const MOMENT_EVENTS = new Set(['objectEntered', 'objectWoke', 'objectRecovered', 'bestShotUpdated']);
36
38
  const MOMENT_MOVING_SPEED = 0.05;
37
39
  const MOMENT_ATTRIBUTE_MIN_AREA = 600;
@@ -46,6 +48,7 @@ const ACTIVE_TICK_MS = 100;
46
48
  const MOTION_INTERVAL_MS = 200;
47
49
  const TICK_SLACK_MS = 20;
48
50
  const MAIN_STREAM_HOLD_MS = 5000;
51
+ const EXTERNAL_FRAME_MAX_AGE_MS = 1000;
49
52
  let DetectionCoordinator = class DetectionCoordinator {
50
53
  config;
51
54
  proxy;
@@ -69,10 +72,17 @@ let DetectionCoordinator = class DetectionCoordinator {
69
72
  feedingSensors = new Map();
70
73
  dwell = new DwellManager();
71
74
  privacy;
72
- videoBackoff = new ReconnectBackoff();
73
75
  // debugging
74
76
  perf = new PerfTracker();
77
+ benchmarkRunning = false;
75
78
  hqUpgrade;
79
+ localizer;
80
+ lastLocalizeAt = 0;
81
+ anchorBoxes = [];
82
+ trackAnchorBoxes = [];
83
+ farewellBoxes = [];
84
+ lastTrackedById = new Map();
85
+ detectionWindow = new DetectionWindow();
76
86
  mainStreamActive = false;
77
87
  idleSince = 0;
78
88
  lastMotionAt = 0;
@@ -92,28 +102,31 @@ let DetectionCoordinator = class DetectionCoordinator {
92
102
  this.config = config;
93
103
  this.proxy = proxy;
94
104
  this.logger = logger;
95
- const frameSourceConfig = {
96
- streamUrl: config.streamUrl,
97
- snapshotUrl: config.snapshotUrl,
98
- decoder: this.config.frameWorkerSettings.decoder,
99
- };
105
+ const snapshot = { snapshotUrl: config.snapshotUrl };
100
106
  if (config.controllerSnapshotSourceId) {
101
107
  const sourceId = config.controllerSnapshotSourceId;
102
108
  const controllerProxy = this.proxy.createProxy(NamespaceManager.cameraNamespaces(config.cameraId).cameraControllerRpc);
103
- frameSourceConfig.snapshotProvider = async () => {
109
+ snapshot.snapshotProvider = async () => {
104
110
  const jpeg = await controllerProxy.snapshot(sourceId, true, true);
105
111
  if (!jpeg || jpeg.byteLength === 0)
106
112
  return null;
107
113
  return Buffer.from(jpeg);
108
114
  };
109
115
  }
110
- this.frameSource = new FrameSource(frameSourceConfig, logger);
111
116
  this.config.zones = normalizeZones(config.zones);
112
117
  this.privacy = new PrivacyMask(logger);
113
118
  this.privacy.update(this.config.zones);
119
+ const decoder = this.config.frameWorkerSettings.decoder;
120
+ this.frameSource = config.streamHot
121
+ ? new BufferedSource({ url: config.streamUrl, decoder, privacy: this.privacy, snapshot }, logger)
122
+ : new FrameSource({ streamUrl: config.streamUrl, decoder, ...snapshot }, logger);
123
+ // a hot source buffers from the start, the loop only decides what is analysed
124
+ if (config.streamHot)
125
+ this.frameSource.start();
114
126
  this.frameScaler = new FrameScaler(null, logger, this.privacy);
127
+ this.localizer = new MotionLocalizer();
115
128
  this.pipeline = new DetectionPipeline(this.pipelineZones(), config.detectionSettings);
116
- this.secondaries = new SecondaryStage(this, this.plugins, this.pipeline, this.frameScaler, this.proxy, logger);
129
+ this.secondaries = new SecondaryStage(this, this.plugins, this.pipeline, this.frameScaler, this.proxy, this.perf, logger);
117
130
  if (config.zones.lines.length > 0) {
118
131
  this.pipeline.updateLines(config.zones.lines, this.videoAspectRatio);
119
132
  }
@@ -164,6 +177,7 @@ let DetectionCoordinator = class DetectionCoordinator {
164
177
  this.cascadeUnsubscribe = this.cascade.onChange((event) => {
165
178
  if (event.type === 'activated') {
166
179
  this.startAdHocVideoLoopIfNeeded();
180
+ this.seedLocalizerReference();
167
181
  }
168
182
  else {
169
183
  this.stopAdHocVideoLoopIfIdle();
@@ -191,6 +205,7 @@ let DetectionCoordinator = class DetectionCoordinator {
191
205
  if (!this.dwell.hasActive() && this.worldSpans.size === 0 && !this.eventManager.hasActiveSegment()) {
192
206
  this.eventManager.forceEndActiveEvent();
193
207
  }
208
+ this.stopAdHocVideoLoopIfIdle();
194
209
  }
195
210
  });
196
211
  }
@@ -201,7 +216,86 @@ let DetectionCoordinator = class DetectionCoordinator {
201
216
  return this.config.detectionSettings;
202
217
  }
203
218
  getPerfSnapshot() {
204
- return { ...this.perf.snapshot(), mainStreamEnabled: this.mainStreamAvailable, frameAnalysis: this.plugins.shouldVideoBeActive() };
219
+ const { timings, ...counters } = this.perf.snapshot();
220
+ const detectors = {};
221
+ for (const type of DETECTOR_METRIC_TYPES) {
222
+ const plugin = this.plugins.get(type);
223
+ // motion without frames is the camera's own signal, there is no detector to report
224
+ if (!plugin || (type === SensorType.Motion && !plugin.requiresFrames))
225
+ continue;
226
+ const input = plugin.modelSpec?.input;
227
+ detectors[type] = {
228
+ plugin: plugin.pluginId,
229
+ input: isVideoInputSpec(input) ? `${input.width}x${input.height}` : undefined,
230
+ runtime: plugin.modelSpec?.runtime,
231
+ models: plugin.modelSpec?.models,
232
+ ...timings[type],
233
+ };
234
+ }
235
+ return {
236
+ ...counters,
237
+ detectors,
238
+ mainStreamEnabled: this.mainStreamAvailable,
239
+ frameAnalysis: this.plugins.shouldVideoBeActive(),
240
+ };
241
+ }
242
+ resetPerf() {
243
+ this.perf.reset();
244
+ }
245
+ pauseForBenchmark(paused) {
246
+ this.benchmarkRunning = paused;
247
+ }
248
+ async runObjectBenchmark(iterations, concurrency) {
249
+ const plugin = this.plugins.get(SensorType.Object);
250
+ const input = plugin?.modelSpec?.input;
251
+ if (!plugin || !isVideoInputSpec(input))
252
+ return null;
253
+ const channels = input.format === 'gray' ? 1 : input.format === 'nv12' ? 1.5 : 3;
254
+ const frame = {
255
+ id: 'benchmark',
256
+ data: Buffer.alloc(Math.round(input.width * input.height * channels)),
257
+ width: input.width,
258
+ height: input.height,
259
+ format: input.format,
260
+ };
261
+ let handlerMs = 0;
262
+ let completed = 0;
263
+ let failed = 0;
264
+ const timed = this.proxy.createProxy(NamespaceManager.sensorProviderNamespaces(plugin.pluginId, plugin.sensorId).sensorRpc, {
265
+ onTiming: (_method, timing) => {
266
+ handlerMs += timing.handlerMs;
267
+ },
268
+ });
269
+ const startedAt = Date.now();
270
+ const workers = Array.from({ length: Math.max(1, concurrency) }, async () => {
271
+ while (completed + failed < iterations) {
272
+ completed++;
273
+ try {
274
+ await PromiseTimeout(timed.detectObjects(frame), DETECT_TIMEOUT_MS, undefined, `Benchmark call timed out after ${DETECT_TIMEOUT_MS}ms`);
275
+ }
276
+ catch (error) {
277
+ completed--;
278
+ failed++;
279
+ this.logger.debug('Benchmark call failed:', error);
280
+ }
281
+ }
282
+ });
283
+ await Promise.all(workers);
284
+ const totalMs = Date.now() - startedAt;
285
+ return {
286
+ camera: '',
287
+ plugin: plugin.pluginId,
288
+ sensorId: plugin.sensorId,
289
+ runtime: plugin.modelSpec?.runtime,
290
+ models: plugin.modelSpec?.models,
291
+ input: `${input.width}x${input.height}`,
292
+ iterations: completed,
293
+ failed,
294
+ concurrency,
295
+ totalMs,
296
+ perSecond: totalMs > 0 ? Math.round((completed / (totalMs / 1000)) * 10) / 10 : 0,
297
+ handlerMs: completed > 0 ? Math.round((handlerMs / completed) * 10) / 10 : 0,
298
+ };
205
299
  }
206
300
  updateZoneConfig(zones) {
207
301
  zones = normalizeZones(zones);
@@ -234,6 +328,7 @@ let DetectionCoordinator = class DetectionCoordinator {
234
328
  }
235
329
  async dispose() {
236
330
  await this.stopVideoLoop();
331
+ await this.frameSource.stop();
237
332
  await this.audioLoop.stop();
238
333
  await this.thumbnailer.stop();
239
334
  this.ptzAutotracker.dispose();
@@ -293,6 +388,12 @@ let DetectionCoordinator = class DetectionCoordinator {
293
388
  // the registry only announces the plugin assigned for this type, everyone else reports into the void
294
389
  if (!this.feedingSensors.has(sensorId))
295
390
  return;
391
+ // a detector reports its spec again once its models finished loading
392
+ if (properties.modelSpec) {
393
+ this.applyModelSpec(sensorId, sensorType, properties.modelSpec);
394
+ if (Object.keys(properties).length === 1)
395
+ return;
396
+ }
296
397
  if (sensorType === SensorType.Object) {
297
398
  const filtered = this.applyExternalDetectionFilters(sensorType, properties);
298
399
  // presence feed keeps the autotracker's return-home timer alive even while
@@ -301,6 +402,9 @@ let DetectionCoordinator = class DetectionCoordinator {
301
402
  this.ptzAutotracker.handlePresenceDetections(externalDetections);
302
403
  if (this.ptzAutotracker.suppressionActive)
303
404
  return;
405
+ detectionRecord.tick({
406
+ externalObject: { detected: filtered.detected === true, count: (filtered.detections ?? []).length },
407
+ });
304
408
  this.ingestDetectionResult(SensorType.Object, sensorId, filtered);
305
409
  this.eventManager.processResults(this.buildSnapshot());
306
410
  // re-shoot the event thumbnail: smart-camera reports arrive after the
@@ -314,78 +418,78 @@ let DetectionCoordinator = class DetectionCoordinator {
314
418
  if (filtered.detected !== true)
315
419
  return;
316
420
  // assist alone is enough: its boxes upgrade zones, bboxes and thumbnails
317
- // even when no face/plate/clip secondary consumes them
318
- if (!this.plugins.hasFrameBasedSecondary() && !this.plugins.hasEligibleObjectAssist())
421
+ // even when no face/plate/clip secondary consumes them; without either,
422
+ // the embedded diff's anchors can still localize a moment crop
423
+ const hasAnchors = this.anchorBoxes.length > 0 || this.farewellBoxes.length > 0;
424
+ if (!this.plugins.hasFrameBasedSecondary() && !this.plugins.hasEligibleObjectAssist() && !hasAnchors)
319
425
  return;
320
426
  if (this.processingExternalSecondary)
321
427
  return; // previous RPC still running
322
428
  this.processingExternalSecondary = true;
323
429
  try {
324
430
  const rawExternal = filtered.detections ?? [];
325
- const peeked = this.frameSource.peekLatestFrame();
326
- if (peeked) {
327
- try {
328
- const results = { timestamp: Date.now() };
329
- const objects = await this.runObjectAssist(peeked.frame, rawExternal);
330
- this.ingestAssistedObjects(sensorId, objects);
331
- // processExternal only adapts the shape, external boxes get no real tracking
332
- const objectDetections = this.pipeline.processExternal(objects.detections);
333
- await this.runSecondariesAndThumbnails({ frame: peeked.frame, scaler: this.frameScaler, isMainStream: false }, objectDetections, results);
334
- this.ingestResultsForAllSecondaries(results);
335
- const snapshot = this.buildSnapshot();
336
- if (results.thumbnails && results.thumbnails.length > 0) {
337
- snapshot.thumbnails = results.thumbnails;
338
- }
339
- this.eventManager.processResults(snapshot);
340
- }
341
- finally {
342
- await peeked[Symbol.asyncDispose]();
343
- }
344
- return;
345
- }
346
- const fetched = await this.frameSource.fetchSnapshotFrame();
347
- if (!fetched)
431
+ const handle = await this.frameSource.getFrame(EXTERNAL_FRAME_MAX_AGE_MS);
432
+ if (!handle)
348
433
  return;
349
434
  try {
350
435
  const results = { timestamp: Date.now() };
351
- const objects = await this.runObjectAssist(fetched.frame, rawExternal);
352
- this.ingestAssistedObjects(sensorId, objects);
353
- if (objects.assisted) {
354
- // real boxes: crop the object like the frame pipeline, so face and
355
- // plate secondaries run on the subject instead of the whole scene
356
- const objectDetections = this.pipeline.processExternal(objects.detections);
357
- await this.runSecondariesAndThumbnails({ frame: fetched.frame, scaler: this.frameScaler, isMainStream: false }, objectDetections, results);
358
- this.ingestResultsForAllSecondaries(results);
359
- const snapshot = this.buildSnapshot();
360
- if (results.thumbnails && results.thumbnails.length > 0) {
361
- snapshot.thumbnails = results.thumbnails;
362
- }
363
- this.eventManager.processResults(snapshot);
364
- }
365
- else {
366
- const sceneJpeg = await this.secondaries.detectFullFrame(fetched.frame, results);
367
- try {
368
- results.thumbnails = await this.secondaries.generateThumbnails(fetched.frame, this.frameScaler, results);
369
- }
370
- catch (error) {
371
- this.logger.error('Thumbnail generation error:', error);
372
- }
373
- this.ingestResultsForAllSecondaries(results);
374
- const snapshot = this.buildSnapshot();
375
- if (results.thumbnails && results.thumbnails.length > 0) {
376
- snapshot.thumbnails = results.thumbnails;
436
+ // anchors may have switched analysis to the main stream: the assist
437
+ // windows and secondary crops should see those native pixels
438
+ const analysis = await this.acquireAnalysisFrame(handle.frame);
439
+ try {
440
+ const objects = await this.runObjectAssist(analysis.frame, rawExternal, analysis.scaler);
441
+ this.ingestAssistedObjects(sensorId, objects);
442
+ if (objects.assisted) {
443
+ // real boxes: crop the object like the frame pipeline, so face and
444
+ // plate secondaries run on the subject instead of the whole scene
445
+ // (processExternal only adapts the shape, external boxes get no real tracking)
446
+ const objectDetections = this.pipeline.processExternal(objects.detections);
447
+ detectionRecord.tick({ assistProcessed: { count: objectDetections.length, bufferedObjects: this.currentDetectionState.object?.detections?.length ?? 0 } });
448
+ await this.runSecondariesAndThumbnails(analysis, objectDetections, results);
449
+ await this.captureExternalMoment(objectDetections, analysis, results.timestamp);
450
+ this.ingestResultsForAllSecondaries(results);
451
+ const snapshot = this.buildSnapshot();
452
+ detectionRecord.tick({ assistSnapshot: { objects: snapshot.objects.length } });
453
+ if (results.thumbnails && results.thumbnails.length > 0) {
454
+ snapshot.thumbnails = results.thumbnails;
455
+ }
456
+ this.eventManager.processResults(snapshot);
377
457
  }
378
- if (sceneJpeg)
379
- snapshot.eventThumbnail = sceneJpeg;
380
- const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
381
- this.eventManager.processResults(snapshot);
382
- if (wantsEventThumb) {
383
- this.thumbnailer.upgradeEventThumbnailAsync();
458
+ else {
459
+ // no assist boxes: the diff's motion clusters are the only
460
+ // localization, still better than a full-scene card
461
+ const anchorDetections = this.anchorMomentDetections(rawExternal);
462
+ if (anchorDetections.length > 0) {
463
+ await this.captureExternalMoment(anchorDetections, analysis, results.timestamp);
464
+ }
465
+ const sceneJpeg = await this.secondaries.detectFullFrame(analysis.frame, results);
466
+ try {
467
+ results.thumbnails = await this.secondaries.generateThumbnails(analysis.frame, analysis.scaler, results);
468
+ }
469
+ catch (error) {
470
+ this.logger.error('Thumbnail generation error:', error);
471
+ }
472
+ this.ingestResultsForAllSecondaries(results);
473
+ const snapshot = this.buildSnapshot();
474
+ if (results.thumbnails && results.thumbnails.length > 0) {
475
+ snapshot.thumbnails = results.thumbnails;
476
+ }
477
+ if (sceneJpeg)
478
+ snapshot.eventThumbnail = sceneJpeg;
479
+ const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
480
+ this.eventManager.processResults(snapshot);
481
+ if (wantsEventThumb) {
482
+ this.thumbnailer.upgradeEventThumbnailAsync();
483
+ }
384
484
  }
385
485
  }
486
+ finally {
487
+ if (analysis.frame !== handle.frame)
488
+ analysis.frame[Symbol.dispose]?.();
489
+ }
386
490
  }
387
491
  finally {
388
- await fetched[Symbol.asyncDispose]();
492
+ await handle[Symbol.asyncDispose]();
389
493
  }
390
494
  }
391
495
  finally {
@@ -442,18 +546,28 @@ let DetectionCoordinator = class DetectionCoordinator {
442
546
  hasPlugin(sensorType) {
443
547
  return this.plugins.has(sensorType);
444
548
  }
445
- externalObjectSpanActive() {
446
- // the registry holds frame-based providers only; a smart-camera object
447
- // sensor (Reolink AI, ONVIF) lives in the feeding set, and its dwell is
448
- // what keeps the span open between its report edges
449
- if (this.plugins.get(SensorType.Object)?.requiresFrames)
450
- return false;
451
- for (const [sensorId, sensorType] of this.feedingSensors) {
452
- if (sensorType === SensorType.Object && this.dwell.isActive(sensorId))
549
+ get windowAnchorsWanted() {
550
+ return this.plugins.get(SensorType.Object)?.requiresFrames === true || this.plugins.hasEligibleObjectAssist();
551
+ }
552
+ get localizerWanted() {
553
+ // the registry only holds frame-based providers; a smart-camera object
554
+ // sensor (Reolink AI, ONVIF) feeds externally and needs the anchors just
555
+ // as much — for assist zoom windows and for framing its moment pictures
556
+ if (this.plugins.has(SensorType.Object) || this.plugins.hasEligibleObjectAssist())
557
+ return true;
558
+ for (const sensorType of this.feedingSensors.values()) {
559
+ if (sensorType === SensorType.Object)
453
560
  return true;
454
561
  }
455
562
  return false;
456
563
  }
564
+ get windowWantsMainStream() {
565
+ if (!this.cascade.isActive)
566
+ return false;
567
+ if (this.anchorBoxes.length === 0 && this.trackAnchorBoxes.length === 0)
568
+ return false;
569
+ return this.windowAnchorsWanted;
570
+ }
457
571
  get cascadeEnabled() {
458
572
  return this.config.detectionSettings.cascadeDetection !== false;
459
573
  }
@@ -470,7 +584,22 @@ let DetectionCoordinator = class DetectionCoordinator {
470
584
  const [w, h] = this.config.interfaceSettings.aspectRatio.split(':').map(Number);
471
585
  return w / h;
472
586
  }
587
+ externalObjectSpanActive() {
588
+ // the registry holds frame-based providers only; a smart-camera object
589
+ // sensor (Reolink AI, ONVIF) lives in the feeding set, and its dwell is
590
+ // what keeps the span open between its report edges
591
+ if (this.plugins.get(SensorType.Object)?.requiresFrames)
592
+ return false;
593
+ for (const [sensorId, sensorType] of this.feedingSensors) {
594
+ if (sensorType === SensorType.Object && this.dwell.isActive(sensorId))
595
+ return true;
596
+ }
597
+ return false;
598
+ }
473
599
  handleCascadeDeactivated() {
600
+ this.anchorBoxes = [];
601
+ this.farewellBoxes = [];
602
+ this.detectionWindow.reset();
474
603
  // clear cascade-gated buffer slots, keep motion/audio
475
604
  const cs = this.currentDetectionState;
476
605
  cs.object = undefined;
@@ -546,7 +675,7 @@ let DetectionCoordinator = class DetectionCoordinator {
546
675
  faceEmbeddingModel: cs.faceEmbeddingModel,
547
676
  plates: cs.licensePlate?.detections ?? [],
548
677
  plateVoting: this.plugins.get(SensorType.LicensePlate)?.requiresFrames === true,
549
- plateMinConfidence: this.config.detectionSettings.licensePlate?.confidence,
678
+ plateMinConfidence: this.config.detectionSettings.licensePlate?.ocrConfidence,
550
679
  plateMinLength: this.config.detectionSettings.licensePlate?.minLength,
551
680
  faceMinConfidence: this.config.detectionSettings.face?.confidence,
552
681
  classifiers: cs.classifiers ? Object.values(cs.classifiers).flatMap((c) => c.detections) : [],
@@ -596,12 +725,12 @@ let DetectionCoordinator = class DetectionCoordinator {
596
725
  let boxlessLabel;
597
726
  switch (sensorType) {
598
727
  case SensorType.Face:
599
- boxlessLabel = 'person';
600
- filtered = this.pipeline.runZoneFilterWithLabel(positioned, 'person');
728
+ boxlessLabel = 'face';
729
+ filtered = this.pipeline.runZoneFilterWithLabel(positioned, 'face');
601
730
  break;
602
731
  case SensorType.LicensePlate:
603
- boxlessLabel = 'vehicle';
604
- filtered = this.pipeline.runZoneFilterWithLabel(positioned, 'vehicle');
732
+ boxlessLabel = 'license_plate';
733
+ filtered = this.pipeline.runZoneFilterWithLabel(positioned, 'license_plate');
605
734
  break;
606
735
  case SensorType.Object:
607
736
  filtered = this.pipeline.runZoneFilter(positioned);
@@ -755,6 +884,18 @@ let DetectionCoordinator = class DetectionCoordinator {
755
884
  break;
756
885
  }
757
886
  }
887
+ applyModelSpec(sensorId, sensorType, modelSpec) {
888
+ const current = this.plugins.get(sensorType);
889
+ if (current?.sensorId !== sensorId)
890
+ return;
891
+ // another model means other numbers, keeping the old average would mislead;
892
+ // load time is not part of the identity, a reload of the same model is not a change
893
+ if (sensorType === SensorType.Object || sensorType === SensorType.Motion) {
894
+ if (modelIdentity(current.modelSpec) !== modelIdentity(modelSpec))
895
+ this.perf.reset();
896
+ }
897
+ this.plugins.updateModelSpec(sensorId, modelSpec);
898
+ }
758
899
  async registerDetectionPluginInternal(sensor) {
759
900
  if (this.videoStopPromise)
760
901
  await this.videoStopPromise;
@@ -762,7 +903,9 @@ let DetectionCoordinator = class DetectionCoordinator {
762
903
  const wasVideoNeeded = this.plugins.shouldVideoBeActive();
763
904
  const wasAudioNeeded = this.plugins.shouldAudioBeActive();
764
905
  const namespaces = NamespaceManager.sensorProviderNamespaces(sensor.pluginId, sensor.sensorId);
765
- const sensorProxy = this.proxy.createProxy(namespaces.sensorRpc);
906
+ const sensorProxy = this.proxy.createProxy(namespaces.sensorRpc, {
907
+ onTiming: (_method, timing) => this.perf.trackTiming(sensor.sensorType, timing.handlerMs, timing.transportMs),
908
+ });
766
909
  const registered = this.plugins.register({
767
910
  pluginId: sensor.pluginId,
768
911
  sensorId: sensor.sensorId,
@@ -773,7 +916,8 @@ let DetectionCoordinator = class DetectionCoordinator {
773
916
  });
774
917
  if (!registered) {
775
918
  // re-push from the registry: the sensor is already live, only the spec may have changed
776
- this.plugins.updateModelSpec(sensor.sensorId, sensor.modelSpec);
919
+ if (sensor.modelSpec)
920
+ this.applyModelSpec(sensor.sensorId, sensor.sensorType, sensor.modelSpec);
777
921
  return;
778
922
  }
779
923
  this.logger.trace(`Plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
@@ -854,7 +998,6 @@ let DetectionCoordinator = class DetectionCoordinator {
854
998
  return;
855
999
  this.logger.debug('Starting video detection loop');
856
1000
  this.loopRunning = true;
857
- this.videoBackoff.reset();
858
1001
  this.loopPromise = this.runDetectionLoop();
859
1002
  }
860
1003
  async stopVideoLoop() {
@@ -869,7 +1012,7 @@ let DetectionCoordinator = class DetectionCoordinator {
869
1012
  this.mainStreamActive = false;
870
1013
  this.idleSince = 0;
871
1014
  const doStop = async () => {
872
- await this.frameSource.stop();
1015
+ await this.frameSource.detach();
873
1016
  await this.loopPromise;
874
1017
  this.loopPromise = undefined;
875
1018
  };
@@ -882,95 +1025,111 @@ let DetectionCoordinator = class DetectionCoordinator {
882
1025
  }
883
1026
  }
884
1027
  startAdHocVideoLoopIfNeeded() {
885
- if (!this.loopRunning && this.plugins.needsAdHocLoop()) {
1028
+ // localizerWanted covers what the registry cannot see: an object assist or
1029
+ // an external object sensor needs the loop for the localizer's anchors
1030
+ if (!this.loopRunning && (this.plugins.needsAdHocLoop() || this.localizerWanted)) {
886
1031
  this.adHocVideoLoop = true;
887
1032
  this.logger.debug('Starting ad-hoc video loop for sensor cascade trigger');
888
1033
  this.startVideoLoop();
889
1034
  }
890
1035
  }
891
1036
  stopAdHocVideoLoopIfIdle() {
892
- if (this.adHocVideoLoop && !this.plugins.shouldVideoBeActive()) {
893
- this.adHocVideoLoop = false;
894
- this.logger.debug('Stopping ad-hoc video loop — cascade ended');
895
- this.stopVideoLoop();
896
- }
1037
+ if (!this.adHocVideoLoop || this.plugins.shouldVideoBeActive())
1038
+ return;
1039
+ // the localizer and assist still need frames while the event runs;
1040
+ // stopping on the cascade edge alone churned the main-stream decoder
1041
+ // open/closed mid-event, which starves the hw device (vaapi surface sync
1042
+ // failures) right when the next report arrives
1043
+ if (this.cascade.isActive || this.dwell.hasActive() || this.eventManager.hasActiveSegment())
1044
+ return;
1045
+ this.adHocVideoLoop = false;
1046
+ this.logger.debug('Stopping ad-hoc video loop — idle');
1047
+ this.stopVideoLoop();
897
1048
  }
898
1049
  async runDetectionLoop() {
1050
+ // the source reconnects on its own, nextFrame simply waits it out
1051
+ const startedAt = Date.now();
1052
+ await this.frameSource.start();
1053
+ let lastFrameId = -1;
1054
+ let seenGeneration = -1;
1055
+ let firstFrame = true;
899
1056
  while (this.loopRunning) {
1057
+ const tickStart = Date.now();
1058
+ const snap = await this.frameSource.nextFrame(lastFrameId);
1059
+ if (!snap)
1060
+ break; // source stopped
1061
+ if (firstFrame) {
1062
+ firstFrame = false;
1063
+ this.logger.debug(`Analysing after ${Date.now() - startedAt}ms`);
1064
+ }
1065
+ lastFrameId = snap.id;
1066
+ if (this.benchmarkRunning) {
1067
+ // the detector is saturated on purpose, feeding it real frames would
1068
+ // both distort the measurement and queue up behind it
1069
+ snap.frame[Symbol.dispose]?.();
1070
+ await sleep(IDLE_TICK_MS);
1071
+ continue;
1072
+ }
900
1073
  try {
901
- await this.frameSource.start();
902
- this.frameScaler.updateHardwareContext(this.frameSource.hardwareContext);
903
- // the hardware context exists only after start, re-evaluate the HQ session
904
- this.thumbnailer.sync(this.mainStreamAnalysisWanted);
905
- this.logger.debug('Stream connected, processing frames...');
906
- this.videoBackoff.reset();
907
- let frameCount = 0;
908
- let lastFrameId = -1;
909
- while (this.loopRunning) {
910
- const tickStart = Date.now();
911
- const snap = await this.frameSource.nextFrame(lastFrameId);
912
- if (!snap)
913
- break; // source ended (stop or EOF)
914
- lastFrameId = snap.id;
915
- frameCount++;
916
- // motion always reads the low stream: switching its input resolution
917
- // would reset the background model on every transition
918
- const analysis = await this.acquireAnalysisFrame(snap.frame);
919
- // debugging
920
- detectionRecord.setFrame(analysis.isMainStream ? 'main' : 'low', analysis.frame.width, analysis.frame.height);
1074
+ if (this.frameSource.generation !== seenGeneration) {
1075
+ seenGeneration = this.frameSource.generation;
1076
+ // a fresh connection brings its own hardware context and resolution
1077
+ this.frameScaler.updateHardwareContext(this.frameSource.hardwareContext);
1078
+ this.frameScaler.clearCache();
1079
+ this.thumbnailer.sync(this.mainStreamAnalysisWanted);
1080
+ this.localizer.reset();
1081
+ this.detectionWindow.reset();
1082
+ }
1083
+ // motion always reads the low stream: switching its input resolution
1084
+ // would reset the background model on every transition
1085
+ const analysis = await this.acquireAnalysisFrame(snap.frame);
1086
+ // debugging
1087
+ detectionRecord.setFrame(analysis.isMainStream ? 'main' : 'low', analysis.frame.width, analysis.frame.height);
1088
+ try {
1089
+ await this.processRawFrame(analysis, snap.frame);
1090
+ }
1091
+ finally {
921
1092
  try {
922
- await this.processRawFrame(analysis, snap.frame);
923
- }
924
- finally {
925
- try {
926
- if (this.hqUpgrade) {
927
- this.hqUpgrade.frame[Symbol.dispose]?.();
928
- this.hqUpgrade = undefined;
929
- }
930
- if (analysis.frame !== snap.frame)
931
- analysis.frame[Symbol.dispose]?.();
932
- snap.frame[Symbol.dispose]?.();
933
- }
934
- catch {
935
- // best-effort
1093
+ if (this.hqUpgrade) {
1094
+ this.hqUpgrade.frame[Symbol.dispose]?.();
1095
+ this.hqUpgrade = undefined;
936
1096
  }
1097
+ if (analysis.frame !== snap.frame)
1098
+ analysis.frame[Symbol.dispose]?.();
1099
+ }
1100
+ catch {
1101
+ // ignore
937
1102
  }
938
- this.updateStreamState();
939
- // activity, not stream choice: cameras without main-stream analysis still count as active
940
- if (this.worldSpans.size > 0 || this.eventManager.hasActiveSegment())
941
- this.perf.activeTicks++;
942
- else
943
- this.perf.idleTicks++;
944
- this.perf.report(this.logger);
945
- const remaining = (this.mainStreamActive ? ACTIVE_TICK_MS : IDLE_TICK_MS) - (Date.now() - tickStart);
946
- if (remaining > 0)
947
- await sleep(remaining);
948
- this.perf.loopMs += Date.now() - tickStart;
949
- }
950
- const streamError = this.frameSource.lastError;
951
- await this.frameSource.stop();
952
- this.frameScaler.clearCache();
953
- if (this.loopRunning && streamError) {
954
- const delay = this.videoBackoff.nextDelayMs();
955
- this.logger.warn(`Stream ended with read error, reconnecting in ${delay / 1000}s: ${streamError.message}`);
956
- await sleep(delay);
957
- }
958
- else if (this.loopRunning && frameCount === 0) {
959
- this.logger.debug('Stream ended without frames, waiting before reconnect...');
960
- await sleep(this.videoBackoff.idleDelayMs);
961
1103
  }
1104
+ this.updateStreamState();
962
1105
  }
963
1106
  catch (error) {
964
- if (!this.loopRunning)
965
- break;
966
- await this.frameSource.stop();
967
- this.frameScaler.clearCache();
968
- const delay = this.videoBackoff.nextDelayMs();
969
- this.logger.warn(`Stream error, reconnecting in ${delay / 1000}s: ${error.message}`);
970
- await sleep(delay);
1107
+ // the stream keeps running, a broken tick must not end the loop
1108
+ this.logger.error('Detection tick failed:', error);
971
1109
  }
972
- }
973
- await this.frameSource.stop();
1110
+ finally {
1111
+ try {
1112
+ snap.frame[Symbol.dispose]?.();
1113
+ }
1114
+ catch {
1115
+ // ignore
1116
+ }
1117
+ }
1118
+ const decode = this.frameSource.takeDecodeStats();
1119
+ this.perf.decodeMs += decode.ms;
1120
+ this.perf.decodedFrames += decode.frames;
1121
+ // activity, not stream choice: cameras without main-stream analysis still count as active
1122
+ if (this.worldSpans.size > 0 || this.eventManager.hasActiveSegment())
1123
+ this.perf.activeTicks++;
1124
+ else
1125
+ this.perf.idleTicks++;
1126
+ this.perf.report(this.logger);
1127
+ const remaining = (this.mainStreamActive ? ACTIVE_TICK_MS : IDLE_TICK_MS) - (Date.now() - tickStart);
1128
+ if (remaining > 0)
1129
+ await sleep(remaining);
1130
+ this.perf.loopMs += Date.now() - tickStart;
1131
+ }
1132
+ await this.frameSource.detach();
974
1133
  this.frameScaler.clearCache();
975
1134
  this.logger.debug('Detection loop ended');
976
1135
  }
@@ -989,7 +1148,7 @@ let DetectionCoordinator = class DetectionCoordinator {
989
1148
  const main = await this.thumbnailer.acquireHqFrame(0);
990
1149
  if (main) {
991
1150
  this.perf.mainFrames++;
992
- this.perf.decodeMs += Date.now() - t0;
1151
+ this.perf.mainDecodeMs += Date.now() - t0;
993
1152
  return { frame: main.frame, scaler: main.scaler, isMainStream: true };
994
1153
  }
995
1154
  }
@@ -1007,7 +1166,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1007
1166
  const main = await this.thumbnailer.acquireHqFrame(0);
1008
1167
  if (main) {
1009
1168
  this.perf.mainFrames++;
1010
- this.perf.decodeMs += Date.now() - t0;
1169
+ this.perf.mainDecodeMs += Date.now() - t0;
1011
1170
  return { frame: main.frame, scaler: main.scaler, isMainStream: true };
1012
1171
  }
1013
1172
  }
@@ -1017,7 +1176,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1017
1176
  return undefined;
1018
1177
  }
1019
1178
  updateStreamState() {
1020
- if (this.worldSpans.size > 0 || this.eventManager.hasActiveSegment()) {
1179
+ if (this.worldSpans.size > 0 || this.eventManager.hasActiveSegment() || this.windowWantsMainStream) {
1021
1180
  this.idleSince = 0;
1022
1181
  if (!this.mainStreamActive && this.mainStreamAvailable) {
1023
1182
  this.mainStreamActive = true;
@@ -1052,6 +1211,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1052
1211
  const t0 = Date.now();
1053
1212
  let motionDetected = false;
1054
1213
  let objectDetections = [];
1214
+ let staticDetections = [];
1055
1215
  const results = { timestamp: t0 };
1056
1216
  // while the PTZ repositions: motion and object keep running so the
1057
1217
  // background model and track ids survive the pan, but nothing below
@@ -1071,11 +1231,13 @@ let DetectionCoordinator = class DetectionCoordinator {
1071
1231
  return;
1072
1232
  if (motionFrame) {
1073
1233
  this.lastMotionAt = t0;
1234
+ if (this.localizerWanted)
1235
+ this.feedLocalizer(motionFrame, t0, ptzSuppressed);
1074
1236
  try {
1075
1237
  const motionInferStart = Date.now();
1076
1238
  const result = await motionPlugin.proxy.detectMotion(motionFrame);
1077
- this.perf.inferMs += Date.now() - motionInferStart;
1078
- this.perf.inferCount++;
1239
+ this.perf.motionMs += Date.now() - motionInferStart;
1240
+ this.perf.motionCount++;
1079
1241
  if (!this.loopRunning)
1080
1242
  return;
1081
1243
  if (ptzSuppressed) {
@@ -1104,6 +1266,17 @@ let DetectionCoordinator = class DetectionCoordinator {
1104
1266
  // no frame-based motion plugin, the cascade is the only motion-equivalent signal
1105
1267
  motionDetected = true;
1106
1268
  results.cascadeTriggered = true;
1269
+ // the localizer scales its own gray here, nobody else needed one
1270
+ const localizeDue = t0 - this.lastLocalizeAt >= MOTION_INTERVAL_MS - TICK_SLACK_MS;
1271
+ if (localizeDue && this.localizerWanted) {
1272
+ const scaleStart = Date.now();
1273
+ const gray = await this.scaleForMotionInput(motionRawFrame);
1274
+ this.perf.scaleMs += Date.now() - scaleStart;
1275
+ if (!this.loopRunning)
1276
+ return;
1277
+ if (gray)
1278
+ this.feedLocalizer(gray, t0, ptzSuppressed);
1279
+ }
1107
1280
  }
1108
1281
  // detected is re-derived after zone filtering, the plugin's own flag is ignored
1109
1282
  if (results.motion && motionPlugin) {
@@ -1117,25 +1290,18 @@ let DetectionCoordinator = class DetectionCoordinator {
1117
1290
  const objectPlugin = this.plugins.get(SensorType.Object);
1118
1291
  const shouldDetectObjects = objectPlugin?.requiresFrames === true && (ptzSuppressed || !this.cascadeEnabled || this.cascade.isActive);
1119
1292
  if (shouldDetectObjects) {
1120
- const scaleStart = Date.now();
1121
- const objectFrame = await this.scaleForObject(analysis.frame, analysis.scaler);
1122
- this.perf.scaleMs += Date.now() - scaleStart;
1123
- if (!this.loopRunning)
1124
- return;
1125
- if (objectFrame) {
1126
- try {
1127
- const inferStart = Date.now();
1128
- const result = await PromiseTimeout(objectPlugin.proxy.detectObjects(objectFrame.model), DETECT_TIMEOUT_MS, undefined, `Object detection timed out after ${DETECT_TIMEOUT_MS}ms`);
1129
- this.perf.inferMs += Date.now() - inferStart;
1130
- this.perf.inferCount++;
1131
- if (!this.loopRunning)
1132
- return;
1293
+ try {
1294
+ const detected = await this.runObjectDetection(objectPlugin, analysis, results.motion?.detections ?? []);
1295
+ if (!this.loopRunning)
1296
+ return;
1297
+ if (detected) {
1133
1298
  this.trackDetectionCadence();
1134
1299
  // run the pipeline even on empty frames so Norfair advances its
1135
1300
  // Kalman state; the pose delta keeps predictions stable across pans
1136
1301
  const poseDelta = this.ptzAutotracker.consumePoseDelta();
1137
- const detected = FrameScaler.undoLetterbox(ensureDetectionBoxes(result.detections), objectFrame.geometry);
1302
+ const postStart = Date.now();
1138
1303
  const pipelineResult = this.pipeline.process(detected, poseDelta);
1304
+ this.perf.postMs += Date.now() - postStart;
1139
1305
  // the tick that opens a span analysed the low stream (the switch lands
1140
1306
  // next tick), its picture work is worth a one-off HQ decode
1141
1307
  if (this.updateWorldSpans(pipelineResult) && !analysis.isMainStream) {
@@ -1167,25 +1333,35 @@ let DetectionCoordinator = class DetectionCoordinator {
1167
1333
  return cx > -0.1 && cx < 1.1 && cy > -0.1 && cy < 1.1;
1168
1334
  });
1169
1335
  objectDetections = visibleTracks.filter((t) => !t.trackLost);
1170
- results.object = { ...result, detections: visibleTracks };
1336
+ results.object = { detected: objectDetections.length > 0, detections: visibleTracks };
1337
+ staticDetections = pipelineResult.staticTracks;
1171
1338
  if (pipelineResult.crossings.length > 0)
1172
1339
  results.lineCrossings = pipelineResult.crossings;
1173
1340
  // incl. extrapolated tracks, otherwise a single missed detector
1174
1341
  // frame flips the autotracker into LOST/REACQUIRE churn
1175
1342
  this.ptzAutotracker.handleObjectDetections(pipelineResult.tracked);
1176
- }
1177
- catch (error) {
1178
- if (!this.loopRunning || isNoRespondersError(error))
1179
- return;
1180
- this.logger.error('Object detection error:', error);
1343
+ // live tracks anchor the next tick's window; a track that just died
1344
+ // gets one farewell look at its last position
1345
+ this.trackAnchorBoxes = objectDetections.map((t) => t.box);
1346
+ for (const id of pipelineResult.removed) {
1347
+ const last = this.lastTrackedById.get(id);
1348
+ if (last)
1349
+ this.farewellBoxes.push(last);
1350
+ }
1351
+ this.lastTrackedById = new Map(pipelineResult.tracked.filter((t) => t.trackId !== undefined).map((t) => [t.trackId, t.box]));
1181
1352
  }
1182
1353
  }
1354
+ catch (error) {
1355
+ if (!this.loopRunning || isNoRespondersError(error))
1356
+ return;
1357
+ this.logger.error('Object detection error:', error);
1358
+ }
1183
1359
  }
1184
1360
  // tracker and autotracker are fed, everything event-facing stays quiet
1185
1361
  if (ptzSuppressed) {
1186
1362
  // object boxes stay live in the UI through the move, bbox publish only
1187
1363
  if (objectPlugin && results.object) {
1188
- this.writeSensorProperties(objectPlugin.sensorId, { detections: results.object.detections });
1364
+ this.writeSensorProperties(objectPlugin.sensorId, { detections: results.object.detections, staticDetections });
1189
1365
  }
1190
1366
  // keep an already-running object dwell alive so a chase doesn't end its
1191
1367
  // own event, but never activate it from suppressed frames
@@ -1204,6 +1380,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1204
1380
  this.ingestDetectionResult(SensorType.Object, objectPlugin.sensorId, {
1205
1381
  detected: objectDetections.length > 0,
1206
1382
  detections: results.object.detections,
1383
+ staticDetections,
1207
1384
  });
1208
1385
  }
1209
1386
  if (!this.loopRunning)
@@ -1259,6 +1436,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1259
1436
  if (objectDetections.length === 0)
1260
1437
  return;
1261
1438
  this.perf.objects += objectDetections.length;
1439
+ this.perf.framesWithObjects++;
1262
1440
  const secondaryStart = Date.now();
1263
1441
  await this.secondaries.detect(analysis.frame, analysis.scaler, objectDetections, results);
1264
1442
  this.perf.secondaryMs += Date.now() - secondaryStart;
@@ -1280,15 +1458,18 @@ let DetectionCoordinator = class DetectionCoordinator {
1280
1458
  }
1281
1459
  const facePlugin = this.plugins.get(SensorType.Face);
1282
1460
  if (facePlugin) {
1461
+ // the crop path reaches ingestDetectionResult directly, so the zone
1462
+ // filter that guards the plugin path has to be applied here too
1463
+ const detections = this.pipeline.runZoneFilterWithLabel(results.face.detections, 'face');
1283
1464
  // no mid-segment clear: a turned-away head yields a no-face frame and
1284
1465
  // would flap the list, departure is reflected when the segment ends
1285
- for (const detection of results.face.detections) {
1466
+ for (const detection of detections) {
1286
1467
  if (detection.identity)
1287
1468
  this.faceIdentities.add(detection.identity);
1288
1469
  }
1289
1470
  this.ingestDetectionResult(SensorType.Face, facePlugin.sensorId, {
1290
- detected: results.face.detected,
1291
- detections: results.face.detections,
1471
+ detected: detections.length > 0 && results.face.detected,
1472
+ detections,
1292
1473
  identities: [...this.faceIdentities].sort(),
1293
1474
  });
1294
1475
  }
@@ -1296,14 +1477,15 @@ let DetectionCoordinator = class DetectionCoordinator {
1296
1477
  if (results.licensePlate) {
1297
1478
  const lpdPlugin = this.plugins.get(SensorType.LicensePlate);
1298
1479
  if (lpdPlugin) {
1299
- for (const detection of results.licensePlate.detections) {
1480
+ const detections = this.pipeline.runZoneFilterWithLabel(results.licensePlate.detections, 'license_plate');
1481
+ for (const detection of detections) {
1300
1482
  const plate = detection.plateText ? normalizePlateText(detection.plateText) || detection.plateText : '';
1301
1483
  if (plate)
1302
1484
  this.platesSeen.add(plate);
1303
1485
  }
1304
1486
  this.ingestDetectionResult(SensorType.LicensePlate, lpdPlugin.sensorId, {
1305
- detected: results.licensePlate.detected,
1306
- detections: results.licensePlate.detections,
1487
+ detected: detections.length > 0 && results.licensePlate.detected,
1488
+ detections,
1307
1489
  plates: [...this.platesSeen].sort(),
1308
1490
  });
1309
1491
  }
@@ -1397,6 +1579,41 @@ let DetectionCoordinator = class DetectionCoordinator {
1397
1579
  return;
1398
1580
  await this.captureMoment(subject, bestScore, trigger, result.tracked, analysis, at);
1399
1581
  }
1582
+ anchorMomentDetections(reported) {
1583
+ const anchors = [...this.anchorBoxes, ...this.farewellBoxes];
1584
+ if (anchors.length === 0 || reported.length === 0)
1585
+ return [];
1586
+ const subject = reported.find((d) => d.label !== 'motion') ?? reported[0];
1587
+ const clusters = clusterBoxes(anchors);
1588
+ const largest = Math.max(...clusters.map((b) => b.width * b.height));
1589
+ // specks would drag the base union across the frame
1590
+ return clusters.filter((b) => b.width * b.height >= largest * 0.2).map((b) => ({ label: subject.label, confidence: subject.confidence, box: b }));
1591
+ }
1592
+ async captureExternalMoment(detections, analysis, at) {
1593
+ let subject;
1594
+ let bestScore = 0;
1595
+ for (const detection of detections) {
1596
+ const box = detection.box;
1597
+ if (!box)
1598
+ continue;
1599
+ const score = detection.confidence * Math.sqrt(box.width * box.height);
1600
+ if (score > bestScore) {
1601
+ bestScore = score;
1602
+ subject = detection;
1603
+ }
1604
+ }
1605
+ if (!subject)
1606
+ return;
1607
+ if (!this.eventManager.wantsMoment(MOMENT_RANK_OBJECT, bestScore, 'low', at))
1608
+ return;
1609
+ const target = { subject: subject.box, base: unionBox(detections.map((d) => d.box).filter(Boolean)) };
1610
+ const rendered = await this.renderMoment(target, analysis);
1611
+ if (!rendered)
1612
+ return;
1613
+ this.eventManager.offerMoment({ strip: rendered.strip, card: rendered.card, capturedAt: at, score: bestScore, rank: MOMENT_RANK_OBJECT, stream: 'low' });
1614
+ // debugging
1615
+ await this.recordMoment(target, rendered, analysis, `${subject.label}-external`, bestScore, 'external', at);
1616
+ }
1400
1617
  async captureMoment(subject, score, trigger, tracked, analysis, at) {
1401
1618
  const box = { x: subject.x, y: subject.y, width: subject.width, height: subject.height };
1402
1619
  const moving = tracked.filter((d) => (d.trackSpeed ?? 0) >= MOMENT_MOVING_SPEED).map((d) => d.box);
@@ -1431,7 +1648,6 @@ let DetectionCoordinator = class DetectionCoordinator {
1431
1648
  // legible one frames the shot, the rest stay their own attribute tiles
1432
1649
  let best;
1433
1650
  let bestScore = -1;
1434
- let bestCard;
1435
1651
  for (const candidate of candidates) {
1436
1652
  const cut = results.thumbnails?.find((t) => t.label === candidate.label);
1437
1653
  // a subject cut off by the frame edge makes a poor card
@@ -1446,20 +1662,21 @@ let DetectionCoordinator = class DetectionCoordinator {
1446
1662
  if (score > bestScore) {
1447
1663
  bestScore = score;
1448
1664
  best = candidate;
1449
- bestCard = cut?.jpeg;
1450
1665
  }
1451
1666
  }
1452
1667
  // ask before cutting: the same face rides along on every tick of a segment
1453
1668
  // and re-encoding for a picture that loses anyway is pure cost
1454
1669
  if (!best || !this.eventManager.wantsMoment(MOMENT_RANK_ATTRIBUTE, bestScore, analysis.isMainStream ? 'main' : 'low', at))
1455
1670
  return;
1671
+ // the card frames the whole person with the face as its center, not the
1672
+ // bare face box: the zoomed face already exists as an attribute tile, and
1673
+ // a misdetected "face" zoomed to card size ruins the event picture
1456
1674
  const target = {
1457
1675
  subject: best.box,
1676
+ base: best.parentBox ?? best.box,
1458
1677
  hint: best.parentBox ? directionBetween(best.box, best.parentBox) : undefined,
1459
1678
  };
1460
- // the attribute tile was cut from this frame with this very target, so the
1461
- // card is already encoded; only the 16:9 companion is still missing
1462
- const rendered = await this.renderMoment(target, analysis, bestCard);
1679
+ const rendered = await this.renderMoment(target, analysis);
1463
1680
  if (!rendered)
1464
1681
  return;
1465
1682
  const stream = analysis.isMainStream ? 'main' : 'low';
@@ -1523,10 +1740,42 @@ let DetectionCoordinator = class DetectionCoordinator {
1523
1740
  const motionPlugin = this.plugins.get(SensorType.Motion);
1524
1741
  if (!motionPlugin?.requiresFrames)
1525
1742
  return undefined;
1743
+ return this.scaleForMotionInput(rawFrame);
1744
+ }
1745
+ async scaleForMotionInput(rawFrame) {
1526
1746
  const maxWidth = MOTION_WIDTH_MAP[this.motionResolution];
1527
1747
  const scaled = await this.frameScaler.scaleProportional(rawFrame, maxWidth, 'gray');
1528
1748
  return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
1529
1749
  }
1750
+ feedLocalizer(gray, t0, ptzSuppressed) {
1751
+ this.lastLocalizeAt = t0;
1752
+ const data = Buffer.isBuffer(gray.data) ? gray.data : Buffer.from(gray.data);
1753
+ const boxes = this.localizer.localize({ data, width: gray.width, height: gray.height });
1754
+ // during ego-motion the diff is garbage, feeding keeps the reference alive
1755
+ this.anchorBoxes = ptzSuppressed ? [] : boxes;
1756
+ }
1757
+ async seedLocalizerReference() {
1758
+ if (!this.localizerWanted || !this.frameSource.decodeOldestKeyframe)
1759
+ return;
1760
+ try {
1761
+ const frame = await this.frameSource.decodeOldestKeyframe();
1762
+ if (!frame)
1763
+ return;
1764
+ try {
1765
+ const gray = await this.scaleForMotionInput(frame);
1766
+ if (gray) {
1767
+ const data = Buffer.isBuffer(gray.data) ? gray.data : Buffer.from(gray.data);
1768
+ this.localizer.seedReference({ data, width: gray.width, height: gray.height });
1769
+ }
1770
+ }
1771
+ finally {
1772
+ frame[Symbol.dispose]?.();
1773
+ }
1774
+ }
1775
+ catch (error) {
1776
+ this.logger.debug('Localizer reference seed failed:', error);
1777
+ }
1778
+ }
1530
1779
  trackDetectionCadence() {
1531
1780
  const now = Date.now();
1532
1781
  const previous = this.lastObjectCallAt;
@@ -1560,6 +1809,43 @@ let DetectionCoordinator = class DetectionCoordinator {
1560
1809
  return undefined;
1561
1810
  return this.scaleFrameForPlugin(rawFrame, objectPlugin, scaler);
1562
1811
  }
1812
+ async runObjectDetection(objectPlugin, analysis, pluginMotionBoxes) {
1813
+ const inputSpec = objectPlugin.modelSpec?.input;
1814
+ const motionAnchors = [...this.anchorBoxes, ...pluginMotionBoxes.map((d) => d.box).filter((box) => box !== undefined), ...this.farewellBoxes];
1815
+ this.farewellBoxes = [];
1816
+ const windows = isVideoInputSpec(inputSpec)
1817
+ ? this.detectionWindow.plan(motionAnchors, this.trackAnchorBoxes, analysis.frame.width, analysis.frame.height, inputSpec.width)
1818
+ : null;
1819
+ if (windows && windows.length > 0) {
1820
+ this.perf.zoomTicks++;
1821
+ this.perf.zoomWindows += windows.length;
1822
+ detectionRecord.zoom({ kind: 'object', anchors: motionAnchors, tracks: this.trackAnchorBoxes, windows });
1823
+ const inferStart = Date.now();
1824
+ const perWindow = await Promise.all(windows.map((window) => this.detectInWindow(objectPlugin, analysis.frame, analysis.scaler, window, inputSpec)));
1825
+ this.perf.objectMs += Date.now() - inferStart;
1826
+ this.perf.objectCount++;
1827
+ return mergeWindowDetections(perWindow.flat());
1828
+ }
1829
+ const scaleStart = Date.now();
1830
+ const objectFrame = await this.scaleForObject(analysis.frame, analysis.scaler);
1831
+ this.perf.scaleMs += Date.now() - scaleStart;
1832
+ if (!objectFrame || !this.loopRunning)
1833
+ return undefined;
1834
+ const inferStart = Date.now();
1835
+ const result = await PromiseTimeout(objectPlugin.proxy.detectObjects(objectFrame.model), DETECT_TIMEOUT_MS, undefined, `Object detection timed out after ${DETECT_TIMEOUT_MS}ms`);
1836
+ this.perf.objectMs += Date.now() - inferStart;
1837
+ this.perf.objectCount++;
1838
+ return FrameScaler.undoLetterbox(ensureDetectionBoxes(result.detections), objectFrame.geometry);
1839
+ }
1840
+ async detectInWindow(plugin, frame, scaler, window, spec) {
1841
+ const scaleStart = Date.now();
1842
+ const cropped = await scaler.cropToSpec(frame, window, spec);
1843
+ this.perf.scaleMs += Date.now() - scaleStart;
1844
+ if (!cropped)
1845
+ return [];
1846
+ const result = await PromiseTimeout(plugin.proxy.detectObjects(scaler.toVideoFrameData(cropped.padded, 'model')), DETECT_TIMEOUT_MS, undefined, `Object detection timed out after ${DETECT_TIMEOUT_MS}ms`);
1847
+ return FrameScaler.undoLetterbox(ensureDetectionBoxes(result.detections), cropped.geometry);
1848
+ }
1563
1849
  async scaleFrameForPlugin(rawFrame, plugin, scaler = this.frameScaler) {
1564
1850
  const inputSpec = plugin.modelSpec?.input;
1565
1851
  if (!isVideoInputSpec(inputSpec))
@@ -1576,27 +1862,34 @@ let DetectionCoordinator = class DetectionCoordinator {
1576
1862
  ingestAssistedObjects(sensorId, objects) {
1577
1863
  if (!objects.assisted)
1578
1864
  return;
1865
+ // the assist returns raw detector output; the frame path dedupes via NMS
1866
+ // before ingest, so this path must merge too or near-identical candidate
1867
+ // boxes inflate the object count
1868
+ const merged = this.pipeline.runMergeAndZoneFilter(objects.detections);
1579
1869
  const refiltered = this.applyExternalDetectionFilters(SensorType.Object, {
1580
- detected: objects.detections.length > 0,
1581
- detections: objects.detections,
1870
+ detected: merged.length > 0,
1871
+ detections: merged,
1872
+ });
1873
+ detectionRecord.tick({
1874
+ assistIngest: {
1875
+ raw: objects.detections.map((d) => ({ label: d.label, score: d.confidence, box: d.box })),
1876
+ merged: merged.length,
1877
+ kept: (refiltered.detections ?? []).length,
1878
+ },
1582
1879
  });
1583
1880
  this.ingestDetectionResult(SensorType.Object, sensorId, refiltered);
1584
1881
  }
1585
- async runObjectAssist(frame, reported) {
1882
+ async runObjectAssist(frame, reported, scaler = this.frameScaler) {
1586
1883
  if (!this.plugins.hasEligibleObjectAssist()) {
1587
1884
  return { detections: reported, assisted: false };
1588
1885
  }
1589
1886
  const assist = this.plugins.get(SensorType.ObjectAssist);
1590
- const scaled = await this.scaleFrameForPlugin(frame, assist);
1591
- if (!scaled)
1592
- return { detections: reported, assisted: false };
1593
1887
  try {
1594
1888
  const inferStart = Date.now();
1595
- const result = await PromiseTimeout(assist.proxy.detectObjects(scaled.model), DETECT_TIMEOUT_MS, undefined, `Object assist timed out after ${DETECT_TIMEOUT_MS}ms`);
1596
- this.perf.inferMs += Date.now() - inferStart;
1597
- this.perf.inferCount++;
1889
+ const boxed = await this.assistDetections(assist, frame, scaler, reported);
1890
+ this.perf.assistMs += Date.now() - inferStart;
1891
+ this.perf.assistCount++;
1598
1892
  const reportedLabels = new Set(reported.map((d) => d.label.toLowerCase()));
1599
- const boxed = FrameScaler.undoLetterbox(ensureDetectionBoxes(result?.detections ?? []), scaled.geometry);
1600
1893
  const found = boxed.filter((d) => reportedLabels.size === 0 || reportedLabels.has(d.label.toLowerCase()));
1601
1894
  if (found.length === 0)
1602
1895
  return { detections: reported, assisted: false };
@@ -1610,6 +1903,31 @@ let DetectionCoordinator = class DetectionCoordinator {
1610
1903
  return { detections: reported, assisted: false };
1611
1904
  }
1612
1905
  }
1906
+ async assistDetections(assist, frame, scaler, reported) {
1907
+ const inputSpec = assist.modelSpec?.input;
1908
+ const reportedBoxes = reported.map((d) => d.box).filter((box) => box !== undefined);
1909
+ // a coordinate-less report still has the localizer's diff anchors: zoom
1910
+ // the assist into the moving regions instead of letterboxing the whole
1911
+ // frame, which shrinks the subject to nothing on a main-stream frame
1912
+ const anchors = reportedBoxes.length > 0 ? reportedBoxes : [...this.anchorBoxes, ...this.farewellBoxes];
1913
+ const pad = reportedBoxes.length > 0 ? TRACK_PAD : MOTION_PAD;
1914
+ if (isVideoInputSpec(inputSpec) && anchors.length > 0) {
1915
+ const windows = planWindowsOnce(anchors, pad, frame.width, frame.height, inputSpec.width);
1916
+ if (windows.length > 0) {
1917
+ detectionRecord.zoom({ kind: 'assist', anchors, windows });
1918
+ const perWindow = await Promise.all(windows.map((window) => this.detectInWindow(assist, frame, scaler, window, inputSpec)));
1919
+ return mergeWindowDetections(perWindow.flat());
1920
+ }
1921
+ }
1922
+ detectionRecord.tick({
1923
+ assistFullFrame: { anchors: anchors.length, videoSpec: isVideoInputSpec(inputSpec), loop: this.loopRunning, cascade: this.cascade.isActive },
1924
+ });
1925
+ const scaled = await this.scaleFrameForPlugin(frame, assist, scaler);
1926
+ if (!scaled)
1927
+ return [];
1928
+ const result = await PromiseTimeout(assist.proxy.detectObjects(scaled.model), DETECT_TIMEOUT_MS, undefined, `Object assist timed out after ${DETECT_TIMEOUT_MS}ms`);
1929
+ return FrameScaler.undoLetterbox(ensureDetectionBoxes(result?.detections ?? []), scaled.geometry);
1930
+ }
1613
1931
  };
1614
1932
  __decorate([
1615
1933
  RPCMethod,