@camera.ui/server 0.0.113 → 0.0.117

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 (242) hide show
  1. package/dist/interface/assets/{404-CZtDF9in.js → 404-Boj4Vl-R.js} +1 -1
  2. package/dist/interface/assets/{About-BzuuHdW1.js → About-C555n8hm.js} +1 -1
  3. package/dist/interface/assets/{Adminpanel-uj7eTmlN.js → Adminpanel-CzhTcMyI.js} +1 -1
  4. package/dist/interface/assets/Automation-c-WoDz5p.js +2 -0
  5. package/dist/interface/assets/{AutomationImport-CR4Vsxkb.js → AutomationImport-TTsCdeBi.js} +2 -2
  6. package/dist/interface/assets/{AutomationStore-BcMX7WbG.js → AutomationStore-Dfv66tKr.js} +2 -2
  7. package/dist/interface/assets/{AutomationStoreDetail-D6Ugj8ba.js → AutomationStoreDetail-D6Kxd8qb.js} +2 -2
  8. package/dist/interface/assets/{Automations-DBACrYHy.js → Automations-B-Vx8ES0.js} +2 -2
  9. package/dist/interface/assets/{Camera-Cf4QZbsx.js → Camera-D9lOlctO.js} +2 -2
  10. package/dist/interface/assets/{CameraConsole-uD4aWou8.js → CameraConsole-D7INO7No.js} +1 -1
  11. package/dist/interface/assets/{Cameras-DbBU5mou.js → Cameras-CoS5FGgt.js} +2 -2
  12. package/dist/interface/assets/{Camview-CDM4Q_tc.css → Camview-CS2a2okQ.css} +1 -1
  13. package/dist/interface/assets/{Camview-DD9jLT7-.js → Camview-D9dWFP3P.js} +1 -1
  14. package/dist/interface/assets/{Config-B9jI0lTQ.js → Config-Bt-dYeu3.js} +1 -1
  15. package/dist/interface/assets/{ConfigActionCameraControl-D4hJLWmj.js → ConfigActionCameraControl-BG6SX9R7.js} +1 -1
  16. package/dist/interface/assets/{ConfigActionDelay-DOUAx4lh.js → ConfigActionDelay-aDgo-kGM.js} +1 -1
  17. package/dist/interface/assets/{ConfigActionHttp-BGz4r_Nr.js → ConfigActionHttp-CO5QD14x.js} +1 -1
  18. package/dist/interface/assets/{ConfigActionImageInput-BhO3ekJT.js → ConfigActionImageInput-DapP_uBr.js} +1 -1
  19. package/dist/interface/assets/{ConfigActionNotification-B5cCmBXr.js → ConfigActionNotification-BTO9Mcqc.js} +1 -1
  20. package/dist/interface/assets/{ConfigActionOutput-P0WY0DlF.js → ConfigActionOutput-CXJ03zBN.js} +1 -1
  21. package/dist/interface/assets/{ConfigActionPlugin-B7YAbFlf.js → ConfigActionPlugin-BddeoS5Q.js} +1 -1
  22. package/dist/interface/assets/{ConfigActionSensor-D_Dt1qtt.js → ConfigActionSensor-Bs4CS6z9.js} +1 -1
  23. package/dist/interface/assets/{ConfigActionSnapshot-ClyCQuB3.js → ConfigActionSnapshot-Ba5eeeDx.js} +1 -1
  24. package/dist/interface/assets/{ConfigActionVariable-B2BoiQPb.js → ConfigActionVariable-CtDsvCYd.js} +1 -1
  25. package/dist/interface/assets/{ConfigConditionIfElse-Bhd2FYyz.js → ConfigConditionIfElse-4vRtS2gU.js} +1 -1
  26. package/dist/interface/assets/{ConfigConditionSensorState-x0M0kHwL.js → ConfigConditionSensorState-DXsn0AoB.js} +1 -1
  27. package/dist/interface/assets/{ConfigConditionSwitch-CC0w-Hvy.js → ConfigConditionSwitch-BAAlXCNL.js} +1 -1
  28. package/dist/interface/assets/{ConfigConditionTime-lhWiIy2x.js → ConfigConditionTime-CbGe7r2f.js} +1 -1
  29. package/dist/interface/assets/{ConfigTriggerDetection-DWjlecON.js → ConfigTriggerDetection-CUvVQzWC.js} +1 -1
  30. package/dist/interface/assets/{ConfigTriggerGeofence-CTqptMUN.js → ConfigTriggerGeofence-C8JI5HfV.js} +1 -1
  31. package/dist/interface/assets/{ConfigTriggerManual-ChsUsNCK.js → ConfigTriggerManual-CHe56Uwq.js} +1 -1
  32. package/dist/interface/assets/{ConfigTriggerSchedule-CE47WiIV.js → ConfigTriggerSchedule-CfYWEMEa.js} +1 -1
  33. package/dist/interface/assets/{ConfigTriggerSensor-DuCtavZo.js → ConfigTriggerSensor-CxfydqlA.js} +1 -1
  34. package/dist/interface/assets/{ConfigTriggerSystem-D1oGAhLn.js → ConfigTriggerSystem-CKY5ikOU.js} +1 -1
  35. package/dist/interface/assets/{ConfigTriggerWebhook-D3cHvW8H.js → ConfigTriggerWebhook-B38CUm9k.js} +1 -1
  36. package/dist/interface/assets/Console-iQYK1wv3.js +1 -0
  37. package/dist/interface/assets/{CuiActionButton-Bl7gqhIV.js → CuiActionButton-_24B3o_r.js} +1 -1
  38. package/dist/interface/assets/{CuiAvatar-of_eVQsv.js → CuiAvatar-Dq7tZWI5.js} +1 -1
  39. package/dist/interface/assets/{CuiBBoxPlayground-CIU5kteO.js → CuiBBoxPlayground-wkKhVNXF.js} +1 -1
  40. package/dist/interface/assets/CuiBottomSheet-BCwkNHr0.js +1 -0
  41. package/dist/interface/assets/{CuiBottomSheet-B7aQ7GCw.js → CuiBottomSheet-CmcHyY52.js} +1 -1
  42. package/dist/interface/assets/CuiCameraCard-Cjltc6jX.js +1 -0
  43. package/dist/interface/assets/{CuiCameraPipCard-KUBxGPoU.js → CuiCameraPipCard-Fqb-8bT0.js} +1 -1
  44. package/dist/interface/assets/CuiCameraRecordings-8rROz71S.js +1 -0
  45. package/dist/interface/assets/{CuiCameraShares-BCMYN11A.js → CuiCameraShares-CVqgvvb9.js} +1 -1
  46. package/dist/interface/assets/{CuiCameraSnapshot-DTtvEMy1.js → CuiCameraSnapshot-CDqd-Ncv.js} +1 -1
  47. package/dist/interface/assets/{CuiCameraTable-B7g53tPM.js → CuiCameraTable-CZI750xd.js} +1 -1
  48. package/dist/interface/assets/CuiDataTable-N4GSbqxH.js +1 -0
  49. package/dist/interface/assets/CuiDataTable-W68cyoLf.css +1 -0
  50. package/dist/interface/assets/{CuiFloatingButton-B5YMdr0X.js → CuiFloatingButton-PyVdoOUT.js} +1 -1
  51. package/dist/interface/assets/{CuiImage-B97QDAl7.js → CuiImage-kthY6BIz.js} +1 -1
  52. package/dist/interface/assets/{CuiMenu-Ds6GufVn.js → CuiMenu-9X-MfyCk.js} +1 -1
  53. package/dist/interface/assets/{CuiPluginCard-D1SylQbM.js → CuiPluginCard-CN3TW6AC.js} +2 -2
  54. package/dist/interface/assets/CuiPluginCategoryChip-CwNmRXlc.js +1 -0
  55. package/dist/interface/assets/{CuiPluginOAuthButton-X8ZGFPwW.js → CuiPluginOAuthButton-BBHNQ3Jp.js} +2 -2
  56. package/dist/interface/assets/{CuiPluginTrustBadge-YhfqGTyR.js → CuiPluginTrustBadge-DX-P6_uN.js} +1 -1
  57. package/dist/interface/assets/{CuiQRCode-D_31ri5b.js → CuiQRCode-C6O_MBni.js} +1 -1
  58. package/dist/interface/assets/{CuiRecordingsGrid-CzMzhUXb.js → CuiRecordingsGrid-CwKCU5_u.js} +1 -1
  59. package/dist/interface/assets/{CuiSchema-CfqOJmDH.js → CuiSchema-VI2Luf-k.js} +1 -1
  60. package/dist/interface/assets/{CuiTopNavbarItem-Q7M4nMw5.js → CuiTopNavbarItem-Ds70WyHx.js} +1 -1
  61. package/dist/interface/assets/{CuiTopbarSlot-ac4GLLbf.js → CuiTopbarSlot-B9Et-Pj7.js} +1 -1
  62. package/dist/interface/assets/{Faces-IVmqcGuF.js → Faces-lzakFq5o.js} +1 -1
  63. package/dist/interface/assets/{FirstSteps-Boec1bzd.js → FirstSteps-Bqmgm_iz.js} +1 -1
  64. package/dist/interface/assets/{Instances-kmkRiBrV.js → Instances-qk8oD08W.js} +1 -1
  65. package/dist/interface/assets/{Logs-DzR8mmLu.js → Logs-72k5RAn7.js} +1 -1
  66. package/dist/interface/assets/{Menu-Cl-biyd7.js → Menu-1_PvQcZH.js} +1 -1
  67. package/dist/interface/assets/{OAuthDeviceFlow-Cp9uZWm1.js → OAuthDeviceFlow-DuZPGhPb.js} +1 -1
  68. package/dist/interface/assets/Plugin-CoIr19GV.js +1 -0
  69. package/dist/interface/assets/Plugin-DHhrlRqP.css +1 -0
  70. package/dist/interface/assets/{PluginConsole-BdbmROpF.js → PluginConsole-CnqDVu4K.js} +1 -1
  71. package/dist/interface/assets/PluginDetail-BSao1Bk3.js +2 -0
  72. package/dist/interface/assets/PluginDetail-L1Vtmr5x.css +1 -0
  73. package/dist/interface/assets/{PluginSchema-CWcq67pj.js → PluginSchema-BNB_PpKf.js} +1 -1
  74. package/dist/interface/assets/PluginSearch-Cq0r9Usy.js +2 -0
  75. package/dist/interface/assets/{PluginSearch-XjGr5qdW.css → PluginSearch-NuKgD1r3.css} +1 -1
  76. package/dist/interface/assets/PluginSearch-ZRpBJ3RH.js +1 -0
  77. package/dist/interface/assets/{Plugins-DMLQY_6J.js → Plugins-Bm0cyXcG.js} +1 -1
  78. package/dist/interface/assets/Recordings-BArCkOPF.js +1 -0
  79. package/dist/interface/assets/{Settings-BljKDhYY.js → Settings-CcwljkEU.js} +1 -1
  80. package/dist/interface/assets/SettingsAccount-UGedhV9S.js +1 -0
  81. package/dist/interface/assets/{SettingsAccount-DKHO4mDj.js → SettingsAccount-g0Xg9CEA.js} +1 -1
  82. package/dist/interface/assets/{SettingsAppearance-V5aeQ9c8.js → SettingsAppearance-D7vpw4Pf.js} +1 -1
  83. package/dist/interface/assets/SettingsAppearance-dUmIIO1_.js +1 -0
  84. package/dist/interface/assets/SettingsBackup-DDMNMyGv.js +1 -0
  85. package/dist/interface/assets/{SettingsNotifications-BZ1IXj0a.js → SettingsNotifications-DHXDTw47.js} +1 -1
  86. package/dist/interface/assets/{SettingsRecordings-BsJ9z-RE.js → SettingsRecordings-B8pVTT97.js} +1 -1
  87. package/dist/interface/assets/{SettingsRemote-Bf8bqXYs.js → SettingsRemote-Cm72h9O2.js} +1 -1
  88. package/dist/interface/assets/{SettingsSystem-obAYrm8J.js → SettingsSystem-IU5zzJ8r.js} +2 -2
  89. package/dist/interface/assets/{SettingsUsers-Ciu1ry18.js → SettingsUsers-Wgu60ci9.js} +1 -1
  90. package/dist/interface/assets/{Terminal-CD9he5xW.js → Terminal-D1CRBDlt.js} +1 -1
  91. package/dist/interface/assets/{VariableInput-CFs-kzqd.js → VariableInput-D5330Mfo.js} +1 -1
  92. package/dist/interface/assets/{VariableSuggestions-PEzupqiE.js → VariableSuggestions-BYRGZZhC.js} +1 -1
  93. package/dist/interface/assets/VersionsHandler-hDdaLt6c.js +7 -0
  94. package/dist/interface/assets/Workers-De7GyYLY.js +2 -0
  95. package/dist/interface/assets/{Workers-DCx1wSq5.css → Workers-E6LM_yAl.css} +1 -1
  96. package/dist/interface/assets/api-CEmKJS_k.js +2 -0
  97. package/dist/interface/assets/{api-CbBOEbKQ.js → api-CniTJ7zF.js} +1 -1
  98. package/dist/interface/assets/api-D7tGlI-z.css +2 -0
  99. package/dist/interface/assets/{auth-cdPNNpNC.js → auth-XrGrvKJw.js} +1 -1
  100. package/dist/interface/assets/{automations-E7vwN3kw.js → automations-BTNkOZVN.js} +1 -1
  101. package/dist/interface/assets/{automations-C309DNNg.js → automations-DBL-AVqV.js} +1 -1
  102. package/dist/interface/assets/backup-DCACCZlp.js +1 -0
  103. package/dist/interface/assets/{cameras-CvJDLEZ3.js → cameras-BU1ZCguU.js} +1 -1
  104. package/dist/interface/assets/{cameras.schema-Clwro_L9.js → cameras.schema-xccbs98C.js} +1 -1
  105. package/dist/interface/assets/cloudHandoff-l8EehWAq.js +1 -0
  106. package/dist/interface/assets/{lightbulb-outline-B0CFho4y.js → cog-outline-DUNHSyxs.js} +1 -1
  107. package/dist/interface/assets/{deviceId-OXLTtG-_.js → deviceId-BDIV3wVa.js} +1 -1
  108. package/dist/interface/assets/download-D5N3Bjsy.js +1 -0
  109. package/dist/interface/assets/{hasPermission-P1TgpAur.js → hasPermission-Dt66r7m6.js} +1 -1
  110. package/dist/interface/assets/{instances-DwnEOXk1.js → instances-Cm2uvc3I.js} +1 -1
  111. package/dist/interface/assets/lightbulb-outline-CSHur8g1.js +1 -0
  112. package/dist/interface/assets/{locale-BWvy9Nlz.js → locale-DHFGj38b.js} +1 -1
  113. package/dist/interface/assets/main-BpXAVhDw.js +3 -0
  114. package/dist/interface/assets/{main-DudyqPaR.css → main-DYLy_5Pn.css} +1 -1
  115. package/dist/interface/assets/nodeDefinitions-BvHN6Rgq.js +1 -0
  116. package/dist/interface/assets/{notifications-DHmx6O_s.js → notifications-CcPAq-AU.js} +1 -1
  117. package/dist/interface/assets/{plugins-NVAodP6s.js → plugins-Byvz--5u.js} +1 -1
  118. package/dist/interface/assets/round-restart-alt-BhSxsPhN.css +1 -0
  119. package/dist/interface/assets/{round-restart-alt-c8MQjhLh.js → round-restart-alt-wY5ZWF_I.js} +1 -1
  120. package/dist/interface/assets/router-BI5yR4wx.js +2 -0
  121. package/dist/interface/assets/{router-BUTKN1yM.css → router-qrIK2v9X.css} +1 -1
  122. package/dist/interface/assets/{routerState-dQtObsFs.js → routerState-1OSE3RrK.js} +1 -1
  123. package/dist/interface/assets/{server-CRxodRik.js → server-BZ6moxcL.js} +1 -1
  124. package/dist/interface/assets/{shares-4w6GDwjp.js → shares-Dnb9ln4i.js} +1 -1
  125. package/dist/interface/assets/{theme-DhaXe7yA.js → theme-CMPHpEel.js} +1 -1
  126. package/dist/interface/assets/{types-6RvFwckU.js → types-CxqXp8-9.js} +1 -1
  127. package/dist/interface/assets/types-Dhfpp18Q.js +1 -0
  128. package/dist/interface/assets/{ui-DrzKhUXW.js → ui-DoqHmuEp.js} +1 -1
  129. package/dist/interface/assets/{useBiometricCredentials-BGRyKe48.js → useBiometricCredentials-D0zMU6nI.js} +1 -1
  130. package/dist/interface/assets/{useCameraOptions-CO1HD_gL.js → useCameraOptions-ICvT3348.js} +1 -1
  131. package/dist/interface/assets/{useCuiCameraDrawer-DnN9lwiy.js → useCuiCameraDrawer-Cz6XoSl4.js} +1 -1
  132. package/dist/interface/assets/{useCuiDialog-DHktK-mZ.js → useCuiDialog-D3TnoSLA.js} +1 -1
  133. package/dist/interface/assets/{useFileDownload-Cp1oH80I.js → useFileDownload-s0dzBJ-h.js} +1 -1
  134. package/dist/interface/assets/{useLogsSocket-OnTDnkPr.js → useLogsSocket-GhBHm7PL.js} +1 -1
  135. package/dist/interface/assets/{useSharedComposables-gpJ--qZh.js → useSharedComposables-BwzJuqID.js} +1 -1
  136. package/dist/interface/assets/{useStreamStatus-N--czepD.js → useStreamStatus-DffcpWzm.js} +1 -1
  137. package/dist/interface/assets/{useUserOptions-CPkl1O0A.js → useUserOptions-Cvb_O9Oh.js} +1 -1
  138. package/dist/interface/assets/{users.schema-BIL9DqRo.js → users.schema-Csh8aGMQ.js} +1 -1
  139. package/dist/interface/assets/{vendor-cameraui-DgSMexWY.js → vendor-cameraui-DQVBM-Sz.js} +38 -38
  140. package/dist/interface/assets/{vendor-core-BKHiXSX7.js → vendor-core-C5n49w0D.js} +1 -1
  141. package/dist/interface/assets/{vendor-primevue-7iEzPOvf.js → vendor-primevue-CCUVk9_F.js} +1 -1
  142. package/dist/interface/assets/{view-grid-B0KB2pRf.js → view-grid-CK2x8Wqf.js} +1 -1
  143. package/dist/interface/index.html +46 -45
  144. package/dist/interface/sw.js +1 -1
  145. package/dist/server/package.json +3 -3
  146. package/dist/server/requirements.txt +1 -1
  147. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/frames.py +3 -1
  148. package/dist/server/src/api/controllers/backup.controller.d.ts +7 -1
  149. package/dist/server/src/api/controllers/backup.controller.js +65 -1
  150. package/dist/server/src/api/controllers/backup.controller.js.map +1 -1
  151. package/dist/server/src/api/database/index.d.ts +1 -1
  152. package/dist/server/src/api/database/index.js +5 -5
  153. package/dist/server/src/api/database/index.js.map +1 -1
  154. package/dist/server/src/api/database/types.d.ts +18 -0
  155. package/dist/server/src/api/routes/backup.routes.js +54 -1
  156. package/dist/server/src/api/routes/backup.routes.js.map +1 -1
  157. package/dist/server/src/api/schemas/backup.schema.d.ts +28 -25
  158. package/dist/server/src/api/schemas/backup.schema.js +23 -60
  159. package/dist/server/src/api/schemas/backup.schema.js.map +1 -1
  160. package/dist/server/src/api/schemas/cameras.schema.d.ts +8 -0
  161. package/dist/server/src/api/schemas/cameras.schema.js +2 -0
  162. package/dist/server/src/api/schemas/cameras.schema.js.map +1 -1
  163. package/dist/server/src/api/services/backup.service.d.ts +1 -1
  164. package/dist/server/src/api/services/backup.service.js +2 -1
  165. package/dist/server/src/api/services/backup.service.js.map +1 -1
  166. package/dist/server/src/api/services/backupScheduler.service.d.ts +31 -0
  167. package/dist/server/src/api/services/backupScheduler.service.js +203 -0
  168. package/dist/server/src/api/services/backupScheduler.service.js.map +1 -0
  169. package/dist/server/src/api/types/index.d.ts +7 -1
  170. package/dist/server/src/api/websocket/nsp/notifications.js +3 -3
  171. package/dist/server/src/api/websocket/nsp/notifications.js.map +1 -1
  172. package/dist/server/src/camera/decoder/detection-coordinator.d.ts +13 -1
  173. package/dist/server/src/camera/decoder/detection-coordinator.js +242 -242
  174. package/dist/server/src/camera/decoder/detection-coordinator.js.map +1 -1
  175. package/dist/server/src/camera/decoder/event-manager.js +41 -18
  176. package/dist/server/src/camera/decoder/event-manager.js.map +1 -1
  177. package/dist/server/src/camera/decoder/frame-scaler.d.ts +1 -0
  178. package/dist/server/src/camera/decoder/frame-scaler.js +2 -2
  179. package/dist/server/src/camera/decoder/frame-scaler.js.map +1 -1
  180. package/dist/server/src/camera/decoder/frame-source.js +2 -1
  181. package/dist/server/src/camera/decoder/frame-source.js.map +1 -1
  182. package/dist/server/src/camera/decoder/snapshot-source.d.ts +49 -0
  183. package/dist/server/src/camera/decoder/snapshot-source.js +279 -0
  184. package/dist/server/src/camera/decoder/snapshot-source.js.map +1 -0
  185. package/dist/server/src/camera/decoder/worker.d.ts +1 -0
  186. package/dist/server/src/camera/decoder/worker.js +27 -4
  187. package/dist/server/src/camera/decoder/worker.js.map +1 -1
  188. package/dist/server/src/go2rtc/index.js +3 -2
  189. package/dist/server/src/go2rtc/index.js.map +1 -1
  190. package/dist/server/src/main.d.ts +1 -0
  191. package/dist/server/src/main.js +11 -0
  192. package/dist/server/src/main.js.map +1 -1
  193. package/dist/server/src/manager/downloadManager.js +2 -2
  194. package/dist/server/src/manager/downloadManager.js.map +1 -1
  195. package/dist/server/src/plugins/runtime/python/child.py +11 -0
  196. package/dist/server/src/plugins/runtime/python/namespaces.py +4 -0
  197. package/dist/server/src/plugins/runtime/python/proxy/download_manager.py +12 -2
  198. package/dist/server/src/plugins/runtime/python/proxy/file_serve.py +49 -0
  199. package/dist/server/src/plugins/worker.js +14 -4
  200. package/dist/server/src/plugins/worker.js.map +1 -1
  201. package/dist/server/src/remote/tunnel/client.js +3 -2
  202. package/dist/server/src/remote/tunnel/client.js.map +1 -1
  203. package/dist/server/src/rpc/server.d.ts +0 -1
  204. package/dist/server/src/rpc/server.js +8 -7
  205. package/dist/server/src/rpc/server.js.map +1 -1
  206. package/dist/server/src/shutdown-state.d.ts +3 -0
  207. package/dist/server/src/shutdown-state.js +11 -0
  208. package/dist/server/src/shutdown-state.js.map +1 -0
  209. package/dist/server/src/workers/agent.js +4 -4
  210. package/dist/server/src/workers/agent.js.map +1 -1
  211. package/dist/server/src/workers/capabilities/plugin-host.js +5 -4
  212. package/dist/server/src/workers/capabilities/plugin-host.js.map +1 -1
  213. package/package.json +3 -3
  214. package/dist/interface/assets/Automation-D0ps_z9e.js +0 -2
  215. package/dist/interface/assets/Console-DrDqDUtO.js +0 -1
  216. package/dist/interface/assets/CuiBottomSheet-DKUg_g5x.js +0 -1
  217. package/dist/interface/assets/CuiCameraCard-IyJNZMGs.js +0 -1
  218. package/dist/interface/assets/CuiCameraRecordings-iWAFGo8e.js +0 -1
  219. package/dist/interface/assets/CuiDataTable-DWuXg9Zg.css +0 -1
  220. package/dist/interface/assets/CuiDataTable-saRt0sBc.js +0 -1
  221. package/dist/interface/assets/CuiPluginCategoryChip-rfdhCEi-.js +0 -1
  222. package/dist/interface/assets/Plugin-CeLrucXT.js +0 -1
  223. package/dist/interface/assets/Plugin-Yv0ZmTNZ.css +0 -1
  224. package/dist/interface/assets/PluginDetail-DKywYzmS.js +0 -2
  225. package/dist/interface/assets/PluginDetail-DbYxVe3g.css +0 -1
  226. package/dist/interface/assets/PluginSearch-BC1L0-VN.js +0 -1
  227. package/dist/interface/assets/PluginSearch-DCuLwE3v.js +0 -2
  228. package/dist/interface/assets/Recordings--XaVJZJi.js +0 -1
  229. package/dist/interface/assets/SettingsAccount-DFMOh1ci.js +0 -1
  230. package/dist/interface/assets/SettingsAppearance-DzG2tlwK.js +0 -1
  231. package/dist/interface/assets/SettingsBackup-CfOHDar8.js +0 -1
  232. package/dist/interface/assets/VersionsHandler-Dpo-L314.js +0 -7
  233. package/dist/interface/assets/Workers-DiIBLgfr.js +0 -2
  234. package/dist/interface/assets/api-aSYFKxo6.js +0 -2
  235. package/dist/interface/assets/api-vCJ1T8Ow.css +0 -2
  236. package/dist/interface/assets/backup-Co1bwdSv.js +0 -1
  237. package/dist/interface/assets/cloudHandoff-Bym-XqR1.js +0 -1
  238. package/dist/interface/assets/main-CIorSaV7.js +0 -3
  239. package/dist/interface/assets/nodeDefinitions-Dvawtk76.js +0 -1
  240. package/dist/interface/assets/round-restart-alt-fCNtJi_k.css +0 -1
  241. package/dist/interface/assets/router-i0r3QZvk.js +0 -2
  242. package/dist/interface/assets/types-BXo-JDGR.js +0 -1
@@ -62,7 +62,6 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
62
62
  });
63
63
  import { isNoRespondersError, RPCClass, RPCMethod } from '@camera.ui/rpc';
64
64
  import { SensorType } from '@camera.ui/sdk';
65
- // import { writeFileSync } from 'node:fs';
66
65
  import { NamespaceManager } from '../../rpc/namespaces.js';
67
66
  import { DETECTION_SENSOR_TYPES, SENSOR_TYPE_CONFIG } from '../sensors/types.js';
68
67
  import { normalizeZone } from '../utils/filter.js';
@@ -74,19 +73,26 @@ import { DetectionEventManager } from './event-manager.js';
74
73
  import { FrameScaler } from './frame-scaler.js';
75
74
  import { FrameSource } from './frame-source.js';
76
75
  import { PtzAutotracker } from './ptz-autotracker.js';
76
+ import { SnapshotSource } from './snapshot-source.js';
77
77
  import { MOTION_WIDTH_MAP } from './types.js';
78
78
  // Default cascade hold-open window in seconds
79
79
  export const DEFAULT_CASCADE_TIMEOUT = 10;
80
80
  // Tracks below this speed (normalized units/s) are considered stationary.
81
81
  // Typical values: parked bike ~0.0006, standing person ~0.003–0.03.
82
82
  export const STATIONARY_SPEED_THRESHOLD = 0.002;
83
- // Object detection dwell window — bridges 1-2 missed frames inside an active
84
- // cascade so a single uncertain frame doesn't fire `detected=false` and split
85
- // the event. Mirrors the motion/audio dwell pattern.
86
83
  const OBJECT_DWELL_SECONDS = 2;
87
- // Hard deadline on a detector-plugin inference RPC. A hung detector must not
88
- // stall the whole detection loop — on timeout we skip the frame and continue.
89
84
  const DETECT_TIMEOUT_MS = 30_000;
85
+ const EVENT_THUMB_MAX_WIDTH = 640;
86
+ const DETECTION_THUMB_MAX_WIDTH = 640;
87
+ const DETECTION_THUMB_MIN_CROP = 160;
88
+ const DETECTION_THUMB_HQ_MAX_WIDTH = 640;
89
+ const DETECTION_THUMB_HQ_MIN_CROP = 640;
90
+ const DETECTION_THUMB_HQ_QUALITY = 80;
91
+ const ATTRIBUTE_THUMB_MAX_WIDTH = 640;
92
+ const ATTRIBUTE_THUMB_MIN_CROP = 128;
93
+ const EVENT_THUMB_HQ_MAX_WIDTH = 960;
94
+ const EVENT_THUMB_HQ_QUALITY = 75;
95
+ const HQ_FRAME_MAX_AGE_MS = 500;
90
96
  async function withTimeout(promise, ms, label) {
91
97
  let timer;
92
98
  const timeout = new Promise((_resolve, reject) => {
@@ -121,6 +127,9 @@ let DetectionCoordinator = class DetectionCoordinator {
121
127
  pipeline;
122
128
  adHocVideoLoop = false;
123
129
  processingExternalSecondary = false;
130
+ hqSnapshotSource;
131
+ hqUpgradeInflight = false;
132
+ hqAspectWarned = false;
124
133
  cascadeUnsubscribe;
125
134
  dwellUnsubscribe;
126
135
  seenStationaryTracks = new Set();
@@ -159,6 +168,11 @@ let DetectionCoordinator = class DetectionCoordinator {
159
168
  this.frameSource = new FrameSource(frameSourceConfig, logger);
160
169
  this.frameScaler = new FrameScaler(null, logger);
161
170
  this.pipeline = new DetectionPipeline(config.zones, config.detectionSettings);
171
+ const hqUrl = this.resolveHqSourceUrl(config.availableSources);
172
+ if (hqUrl) {
173
+ this.hqSnapshotSource = new SnapshotSource({ url: hqUrl }, logger);
174
+ }
175
+ this.syncHqSnapshotSource();
162
176
  if (config.lines.length > 0) {
163
177
  this.pipeline.updateLines(config.lines, this.videoAspectRatio);
164
178
  }
@@ -312,19 +326,13 @@ let DetectionCoordinator = class DetectionCoordinator {
312
326
  detected: filtered.length > 0,
313
327
  };
314
328
  }
329
+ // PTZ-suppression gating happens upstream — callers decide whether a write
330
+ // belongs in the full pipeline or autotracker-only.
315
331
  ingestDetectionResult(sensorType, sensorId, properties, pluginId) {
316
- // Note: PTZ-suppression gating happens upstream — processDetection and
317
- // reportSensorWrite decide whether to route to autotracker-only vs. full
318
- // ingest. By the time we land here, the caller has already decided this
319
- // write belongs in the full pipeline.
320
- // Update aggregated buffer based on sensor type. Each type maps to a different
321
- // ProcessedDetectionData field — partial-merge with what's already buffered.
322
332
  this.updateBufferForType(sensorType, properties, pluginId);
323
333
  const detected = properties.detected === true;
324
- // Motion/Audio: refresh dwell on positive detection. Dwell drives the
325
- // `detected` / `blocked` / `lastTriggered` lifecycle entirely see the
326
- // dwell.onChange listener wired in the constructor. Motion additionally
327
- // triggers cascade.
334
+ // Motion/Audio: dwell drives the detected/blocked/lastTriggered lifecycle
335
+ // entirely (see dwell.onChange in the constructor); motion also cascades.
328
336
  if (sensorType === SensorType.Motion && detected) {
329
337
  const motionTimeout = this.config.detectionSettings.motion.timeout;
330
338
  this.dwell.refresh(sensorId, motionTimeout);
@@ -340,15 +348,11 @@ let DetectionCoordinator = class DetectionCoordinator {
340
348
  }
341
349
  }
342
350
  else if (sensorType === SensorType.Object && detected) {
343
- // Object dwell — anti-chatter for object detection. Single missed frame
344
- // (model uncertain for ~100ms) must not split the event into two.
345
- //
346
- // Stationary object gating: a track that is stationary AND was already
347
- // seen in a previous frame during this event must NOT refresh the dwell.
348
- // This prevents parked cars/bikes from keeping events alive forever.
349
- // Moving tracks always refresh. Stationary tracks refresh only once
350
- // (first time seen) — after that they're in seenStationaryTracks and
351
- // get skipped. The set is cleared on cascade deactivate (event boundary).
351
+ // Object dwell — anti-chatter: a single missed frame must not split the
352
+ // event. Stationary tracks refresh the dwell only ONCE (first sighting);
353
+ // afterwards they're in seenStationaryTracks and don't count, so parked
354
+ // cars/bikes can't keep events alive forever. Moving tracks always count.
355
+ // The set is cleared on cascade deactivate (event boundary).
352
356
  const detections = properties.detections ?? [];
353
357
  let hasActiveTrack = false;
354
358
  for (const t of detections) {
@@ -357,40 +361,31 @@ let DetectionCoordinator = class DetectionCoordinator {
357
361
  const isStationary = (t.trackSpeed ?? 0) < STATIONARY_SPEED_THRESHOLD;
358
362
  if (isStationary) {
359
363
  if (t.trackId !== undefined && !this.seenStationaryTracks.has(t.trackId)) {
360
- // First time seeing this stationary track — count it as active
361
364
  this.seenStationaryTracks.add(t.trackId);
362
365
  hasActiveTrack = true;
363
366
  }
364
- // Already seen stationary track — does not count
365
367
  }
366
368
  else {
367
- // Moving track — always counts
368
369
  hasActiveTrack = true;
369
370
  }
370
371
  }
371
372
  if (hasActiveTrack) {
372
373
  this.dwell.refresh(sensorId, OBJECT_DWELL_SECONDS);
373
- // this.logger.debug('[stationary-debug] active track found, dwell refreshed');
374
374
  }
375
375
  else {
376
- // Only known stationary tracks — suppress detected and clear detections
377
- // from the buffer so the EventManager doesn't open a new segment for
378
- // objects we've already seen. The UI still gets the bboxes via the
379
- // property publish (publishProps path below).
376
+ // Only known stationary tracks — suppress detected and clear buffered
377
+ // detections so the EventManager doesn't open a new segment; the UI
378
+ // still gets the bboxes via the property publish below.
380
379
  properties.detected = false;
381
380
  if (this.currentDetectionState.object) {
382
381
  this.currentDetectionState.object.detected = false;
383
382
  this.currentDetectionState.object.detections = [];
384
383
  }
385
- // this.logger.debug('[stationary-debug] suppressed — all tracks stationary+known');
386
384
  }
387
385
  }
388
- // Publish path. For motion/audio sensors, dwell is the SOLE owner of
389
- // `detected` / `blocked` / `lastTriggered` regardless of the current
390
- // frame's detected value anti-chatter means a "no detection" frame
391
- // must NOT overwrite the dwell-set `detected=true`. Always strip these
392
- // dwell-managed properties from the publish, and let `dwell.onChange`
393
- // (activated/expired) be the only path that writes them.
386
+ // Dwell is the SOLE owner of detected/blocked/lastTriggered for these
387
+ // types a "no detection" frame must not overwrite the dwell-set state,
388
+ // so strip them from the publish; dwell.onChange is the only writer.
394
389
  let publishProps = properties;
395
390
  if (sensorType === SensorType.Motion || sensorType === SensorType.Audio || sensorType === SensorType.Object) {
396
391
  const { detected: _d, blocked: _b, lastTriggered: _t, ...rest } = properties;
@@ -399,8 +394,7 @@ let DetectionCoordinator = class DetectionCoordinator {
399
394
  if (Object.keys(publishProps).length > 0) {
400
395
  this.writeSensorProperties(sensorId, publishProps);
401
396
  }
402
- // EventManager is NOT ticked here — detection-coordinator flushes once
403
- // per frame at the end of processDetection() after all detectors + thumbnails.
397
+ // EventManager is NOT ticked here — processDetection flushes once per frame.
404
398
  }
405
399
  updateBufferForType(sensorType, properties, pluginId) {
406
400
  const cs = this.currentDetectionState;
@@ -455,6 +449,7 @@ let DetectionCoordinator = class DetectionCoordinator {
455
449
  }
456
450
  updateFrameWorkerSettings(settings) {
457
451
  this.config.frameWorkerSettings = settings;
452
+ this.syncHqSnapshotSource();
458
453
  }
459
454
  updateInterfaceSettings(settings) {
460
455
  this.config.interfaceSettings = settings;
@@ -465,6 +460,7 @@ let DetectionCoordinator = class DetectionCoordinator {
465
460
  async dispose() {
466
461
  await this.stopVideoLoop();
467
462
  await this.stopAudioLoop();
463
+ await this.hqSnapshotSource?.stop();
468
464
  this.ptzAutotracker.dispose();
469
465
  this.cascadeUnsubscribe?.();
470
466
  this.dwellUnsubscribe?.();
@@ -571,9 +567,8 @@ let DetectionCoordinator = class DetectionCoordinator {
571
567
  if (!this.singlePlugins.has(SensorType.Object)) {
572
568
  this.adHocVideoLoop = false;
573
569
  }
574
- // Hot-reload: if the ad-hoc loop is no longer needed after removing the
575
- // plugin, stop it. Uses needsAdHocLoop() (not shouldVideoBeActive) so we
576
- // don't accidentally stop a loop still needed for F object (requiresFrames=true).
570
+ // Hot-reload: uses needsAdHocLoop() (not shouldVideoBeActive) so a loop
571
+ // still needed by a frame-based object plugin isn't stopped accidentally.
577
572
  if (this.adHocVideoLoop && !this.needsAdHocLoop()) {
578
573
  this.adHocVideoLoop = false;
579
574
  this.logger.debug('Stopping ad-hoc video loop — last frame-based consumer removed');
@@ -591,17 +586,13 @@ let DetectionCoordinator = class DetectionCoordinator {
591
586
  async reportSensorWrite(sensorId, sensorType, properties) {
592
587
  if (sensorType === SensorType.Object) {
593
588
  const filtered = this.applyExternalDetectionFilters(sensorType, properties);
594
- // Always feed the autotracker it needs fresh data every write, even
595
- // (especially) during suppression, so it can decide when to stop.
596
- // External-sensor object writes are presence-only (SDK synthesises a
597
- // full-frame bbox when the plugin reports without coordinates), so use
598
- // the presence channel — it holds the return-to-home timer without
599
- // driving the motor.
589
+ // Always feed the autotracker (even during suppression, so it can decide
590
+ // when to stop). External writes are presence-only the presence channel
591
+ // holds the return-to-home timer without driving the motor.
600
592
  const externalDetections = filtered.detections ?? [];
601
593
  this.ptzAutotracker.handlePresenceDetections(externalDetections);
602
- // During PTZ suppression: autotracker got the data, but tracker/pipeline/
603
- // events/UI are gated (no stale bboxes, no false events from camera pose
604
- // change). Skip the rest.
594
+ // During suppression tracker/pipeline/events stay gated no stale
595
+ // bboxes or false events from the camera pose change.
605
596
  if (this.ptzAutotracker.suppressionActive)
606
597
  return;
607
598
  this.ingestDetectionResult(SensorType.Object, sensorId, filtered);
@@ -612,9 +603,8 @@ let DetectionCoordinator = class DetectionCoordinator {
612
603
  return; // busy-skip: previous RPC still running
613
604
  this.processingExternalSecondary = true;
614
605
  try {
615
- // Wrap the zone-filtered external boxes as TrackedDetection (no real
616
- // tracking, just shape adapter `trackLost: false`) so the secondary
617
- // pipeline can treat them uniformly with frame-driven output.
606
+ // Wrap external boxes as TrackedDetection (shape adapter, no real
607
+ // tracking) so the secondary pipeline treats them like loop output.
618
608
  const rawExternal = filtered.detections ?? [];
619
609
  const objectDetections = this.pipeline.processExternal(rawExternal);
620
610
  // Fast path: loop is running and has a frame → crop mode.
@@ -635,9 +625,8 @@ let DetectionCoordinator = class DetectionCoordinator {
635
625
  }
636
626
  return;
637
627
  }
638
- // Slow path: loop hasn't produced a frame yet → snapshot fetch fallback.
639
- // Full-frame mode (no per-detection crops) — secondaries run global
640
- // detection on the whole scaled frame.
628
+ // Slow path: no loop frame yet → snapshot fetch, secondaries run
629
+ // full-frame (no per-detection crops).
641
630
  const fetched = await this.frameSource.fetchSnapshotFrame();
642
631
  if (!fetched)
643
632
  return;
@@ -648,7 +637,11 @@ let DetectionCoordinator = class DetectionCoordinator {
648
637
  const snapshot = this.buildSnapshot();
649
638
  if (sceneJpeg)
650
639
  snapshot.eventThumbnail = sceneJpeg;
640
+ const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
651
641
  this.eventManager.processResults(snapshot);
642
+ if (wantsEventThumb) {
643
+ this.upgradeEventThumbnailAsync();
644
+ }
652
645
  }
653
646
  finally {
654
647
  await fetched[Symbol.asyncDispose]();
@@ -660,8 +653,7 @@ let DetectionCoordinator = class DetectionCoordinator {
660
653
  return;
661
654
  }
662
655
  if (sensorType === SensorType.Motion) {
663
- // Motion: fully gated during suppression. Frame-diff is fake during
664
- // camera repositioning.
656
+ // Fully gated during suppression — frame-diff is fake while the camera repositions.
665
657
  if (this.ptzAutotracker.suppressionActive)
666
658
  return;
667
659
  const filtered = this.applyExternalDetectionFilters(sensorType, properties);
@@ -670,9 +662,8 @@ let DetectionCoordinator = class DetectionCoordinator {
670
662
  this.fetchEventThumbnailAsync();
671
663
  return;
672
664
  }
673
- // External secondaries (Face/Plate/Classifier/Clip with requiresFrames=false):
674
- // direct ingest, no pixel work. For multi-provider types (Classifier), look
675
- // up the owning pluginId for buffer keying.
665
+ // External secondaries: direct ingest, no pixel work. Classifier (multi-
666
+ // provider) needs the owning pluginId for buffer keying.
676
667
  let pluginId;
677
668
  if (sensorType === SensorType.Classifier) {
678
669
  const typePlugins = this.multiPlugins.get(SensorType.Classifier);
@@ -690,26 +681,19 @@ let DetectionCoordinator = class DetectionCoordinator {
690
681
  this.eventManager.processResults(this.buildSnapshot());
691
682
  }
692
683
  reportSensorTrigger(sensorId, triggerType, action, _sustained, timeoutSeconds) {
693
- // Use namespaced dwell key so sensor trigger dwells don't overwrite
694
- // the sensor's own properties (detected/blocked). Motion/audio sensors
695
- // have their properties managed by dwell; contact/switch/light sensors don't.
684
+ // Namespaced dwell key so trigger dwells don't overwrite the sensor's own
685
+ // detected/blocked properties (contact/switch/light manage those themselves).
696
686
  const dwellKey = `trigger:${sensorId}`;
697
- // Track active sensor trigger types for snapshot
698
687
  if (action === 'activate') {
699
688
  this.activeSensorTriggerTypes.set(dwellKey, triggerType);
700
689
  }
701
- // Dwell for event lifecycle — consistent with motion/audio.
702
- // Activate: starts/extends the event. Deactivate: refreshes for cooldown window.
703
690
  this.dwell.refresh(dwellKey, timeoutSeconds);
704
- // Cascade for object-detection gating — only on activate
705
691
  if (action === 'activate' && this.cascadeEnabled) {
706
692
  this.cascade.triggerMomentary(this.cascadeTimeoutSeconds);
707
693
  }
708
- // Flush immediately so EventManager sees the trigger (like reportSensorWrite)
709
694
  this.eventManager.processResults(this.buildSnapshot());
710
- // Async event-thumb fetch via snapshot URL covers the trigger-only path
711
- // (doorbell, contact sensor) where no loop frame is available to encode from.
712
- // No-op if a loop frame already populated the thumb earlier.
695
+ // Trigger-only path (doorbell, contact) has no loop frame to encode from —
696
+ // fetch the event thumb async; no-op if one was already captured.
713
697
  if (action === 'activate') {
714
698
  this.fetchEventThumbnailAsync();
715
699
  }
@@ -913,10 +897,15 @@ let DetectionCoordinator = class DetectionCoordinator {
913
897
  try {
914
898
  const env_1 = { stack: [], error: void 0, hasError: false };
915
899
  try {
900
+ const hqJpeg = await this.hqSnapshotSource?.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
901
+ if (hqJpeg) {
902
+ this.eventManager.publishEventThumbnail(hqJpeg);
903
+ return;
904
+ }
916
905
  const handle = __addDisposableResource(env_1, await this.frameSource.fetchSnapshotFrame(), true);
917
906
  if (!handle)
918
907
  return;
919
- const jpeg = await this.frameScaler.frameToJPEG(handle.frame, 320);
908
+ const jpeg = await this.frameScaler.frameToJPEG(handle.frame, EVENT_THUMB_MAX_WIDTH);
920
909
  if (!jpeg)
921
910
  return;
922
911
  this.eventManager.publishEventThumbnail(jpeg);
@@ -936,6 +925,81 @@ let DetectionCoordinator = class DetectionCoordinator {
936
925
  }
937
926
  })();
938
927
  }
928
+ // Fallback: async snapshot replacement of a substream event thumbnail —
929
+ // only reached when the HQ source couldn't deliver a frame inline at event
930
+ // start (still connecting, buffer not warm yet).
931
+ upgradeEventThumbnailAsync() {
932
+ const source = this.hqSnapshotSource;
933
+ if (!source?.isRunning || !source.hasBuffer || this.hqUpgradeInflight)
934
+ return;
935
+ this.hqUpgradeInflight = true;
936
+ void (async () => {
937
+ try {
938
+ const jpeg = await source.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
939
+ if (jpeg && this.eventManager.hasActiveEvent()) {
940
+ this.eventManager.publishEventThumbnail(jpeg);
941
+ }
942
+ }
943
+ catch (error) {
944
+ this.logger.debug('Snapshot event thumbnail upgrade failed:', error);
945
+ }
946
+ finally {
947
+ this.hqUpgradeInflight = false;
948
+ }
949
+ })();
950
+ }
951
+ // Recent snapshot frame + its scaler (own hardware context); caller disposes
952
+ // the frame. With a reference size the aspect ratios must match — normalized
953
+ // boxes only transfer between streams showing the same sensor area.
954
+ async acquireHqFrame(reference) {
955
+ const source = this.hqSnapshotSource;
956
+ if (!source?.isRunning || !source.hasBuffer)
957
+ return null;
958
+ const scaler = source.scaler;
959
+ if (!scaler)
960
+ return null;
961
+ const frame = await source.getFrame(HQ_FRAME_MAX_AGE_MS);
962
+ if (!frame)
963
+ return null;
964
+ if (reference) {
965
+ const refAspect = reference.width / reference.height;
966
+ const hqAspect = frame.width / frame.height;
967
+ if (Math.abs(refAspect - hqAspect) / refAspect > 0.02) {
968
+ if (!this.hqAspectWarned) {
969
+ this.hqAspectWarned = true;
970
+ this.logger.warn(`[hq-thumb] aspect mismatch (detection ${reference.width}x${reference.height} vs snapshot ${frame.width}x${frame.height}) — detection crops stay on the substream`);
971
+ }
972
+ frame[Symbol.dispose]?.();
973
+ return null;
974
+ }
975
+ }
976
+ return { frame, scaler };
977
+ }
978
+ resolveHqSourceUrl(sources) {
979
+ if (!sources?.length)
980
+ return undefined;
981
+ const order = ['high-resolution', 'mid-resolution', 'low-resolution'];
982
+ const byRole = new Map(sources.map((s) => [s.role, s]));
983
+ // Detection consumes the LOWEST available role (worker: lowRes ?? midRes
984
+ // ?? streamSource) — the HQ companion is only worth a session when a
985
+ // strictly higher-resolution role exists.
986
+ const detectionRole = [...order].reverse().find((role) => byRole.has(role));
987
+ const hqRole = order.find((role) => byRole.has(role));
988
+ if (!detectionRole || !hqRole || hqRole === detectionRole)
989
+ return undefined;
990
+ return byRole.get(hqRole).url;
991
+ }
992
+ syncHqSnapshotSource() {
993
+ if (!this.hqSnapshotSource)
994
+ return;
995
+ const wanted = this.config.frameWorkerSettings.hqSnapshots === true;
996
+ if (wanted && !this.hqSnapshotSource.isRunning) {
997
+ this.hqSnapshotSource.start();
998
+ }
999
+ else if (!wanted && this.hqSnapshotSource.isRunning) {
1000
+ void this.hqSnapshotSource.stop();
1001
+ }
1002
+ }
939
1003
  calculateDBFS(data, format) {
940
1004
  let sumSquares = 0;
941
1005
  let sampleCount = 0;
@@ -1118,28 +1182,19 @@ let DetectionCoordinator = class DetectionCoordinator {
1118
1182
  let motionDetected = false;
1119
1183
  let objectDetections = [];
1120
1184
  const results = { timestamp: t0 };
1121
- // Refresh ReID counters every frame while cascade is active so dead
1122
- // tracks never expire during the cascade window — exactly tied to
1123
- // cascade lifecycle, no estimated timeout needed.
1185
+ // Refresh ReID every frame while cascade is active so dead tracks never
1186
+ // expire during the cascade window.
1124
1187
  if (this.cascade.isActive) {
1125
1188
  this.pipeline.refreshReid();
1126
1189
  }
1127
- // PTZ-suppression gating:
1128
- // - Motion: skipped entirely. Frame-diff is genuinely wrong while the camera
1129
- // is repositioning (whole scene "moves") the detector produces garbage.
1130
- // - Object: runs through the full pipeline unchanged. Norfair is fed the
1131
- // per-frame PTZ pose delta (via `consumePoseDelta()`) so Kalman
1132
- // predictions stay aligned with the world frame across pans — track ids
1133
- // survive camera motion instead of being lost-and-re-acquired every
1134
- // cycle. The autotracker itself gates motor decisions on its own
1135
- // `isPtzMoving` state, so it doesn't issue commands during a pan.
1190
+ // PTZ suppression: motion is skipped entirely (frame-diff is garbage while
1191
+ // the camera repositions); object keeps running Norfair gets the pose
1192
+ // delta so track ids survive pans, and the autotracker gates motor
1193
+ // commands on its own isPtzMoving state.
1136
1194
  const ptzSuppressed = this.ptzAutotracker.suppressionActive;
1137
- // 1. Motion detection runs every frame whenever a motion plugin is registered,
1138
- // regardless of cascade state. Two reasons:
1139
- // a) Motion sensor properties (bboxes shown in UI) need to stay fresh during cascade
1140
- // b) Re-triggering cascade.triggerMomentary on each detected frame extends the cascade
1141
- // naturally, so a sustained motion event keeps cascade alive as long as motion happens.
1142
- // The cascade is the gate for OBJECT detection, not for motion.
1195
+ // 1. Motion — every frame regardless of cascade state: keeps UI bboxes
1196
+ // fresh AND re-triggers the cascade so sustained motion keeps it alive.
1197
+ // The cascade gates OBJECT detection, not motion.
1143
1198
  const motionPlugin = this.singlePlugins.get(SensorType.Motion);
1144
1199
  if (motionPlugin?.requiresFrames && !ptzSuppressed) {
1145
1200
  const motionFrame = await this.scaleForMotion(rawFrame);
@@ -1172,29 +1227,18 @@ let DetectionCoordinator = class DetectionCoordinator {
1172
1227
  motionDetected = true;
1173
1228
  results.cascadeTriggered = true;
1174
1229
  }
1175
- // Funnel motion result through unified ingest path (handles dwell + cascade + buffer + EventManager tick).
1176
- // `detected` is re-derived from post-zone-filter detections: if the plugin reported
1177
- // motion but the zone filter wiped all boxes, effective detected is false. The plugin's
1178
- // own `detected` flag is ignored on purpose — zone filtering wins.
1230
+ // `detected` is re-derived from post-zone-filter detections the plugin's
1231
+ // own flag is ignored on purpose, zone filtering wins.
1179
1232
  if (results.motion && motionPlugin) {
1180
1233
  this.ingestDetectionResult(SensorType.Motion, motionPlugin.sensorId, {
1181
1234
  detected: motionDetected,
1182
1235
  detections: results.motion.detections,
1183
1236
  });
1184
1237
  }
1185
- // 2. Object detection runs only when the cascade is active (i.e. some
1186
- // wake source has fired: frame-based motion, external motion, audio, or
1187
- // a configured sensor trigger). When cascade is disabled in the camera
1188
- // config, runs unconditionally as an always-on detector.
1189
- //
1190
- // A motion plugin (frame-based OR external) must be registered for object
1191
- // detection to ever fire — without one, no wake source exists for object,
1192
- // so the cascade would never become active.
1238
+ // 2. Object — gated by the cascade (wake sources: motion, audio, sensor
1239
+ // trigger), always-on when cascade is disabled, and kept running during
1240
+ // PTZ suppression so the tracker holds its lock across pans.
1193
1241
  const objectPlugin = this.singlePlugins.get(SensorType.Object);
1194
- // Object detection runs during suppression too — the tracker stays alive
1195
- // across pans via Norfair's camera-motion compensation so autotracker
1196
- // keeps a stable lock. Without suppression: needs an active cascade (or
1197
- // cascade disabled globally).
1198
1242
  const shouldDetectObjects = objectPlugin?.requiresFrames === true && (ptzSuppressed || !this.cascadeEnabled || this.cascade.isActive);
1199
1243
  if (shouldDetectObjects) {
1200
1244
  const objectFrame = await this.scaleForObject(rawFrame);
@@ -1205,23 +1249,14 @@ let DetectionCoordinator = class DetectionCoordinator {
1205
1249
  const result = await withTimeout(objectPlugin.proxy.detectObjects(objectFrame), DETECT_TIMEOUT_MS, 'Object');
1206
1250
  if (!this.loopRunning)
1207
1251
  return;
1208
- // Full pipeline (always): NMS merge zone filter → track (with
1209
- // appearance ReID) line crossing. Runs even on empty-detection
1210
- // frames so Norfair advances Kalman state and the autotracker sees
1211
- // extrapolated positions instead of a momentary hole.
1212
- //
1213
- // The PTZ pose delta is handed to Norfair so Kalman predictions
1214
- // are transformed into a world-stable frame — track ids survive
1215
- // pans instead of going through LOST → REACQUIRE every cycle.
1252
+ // Full pipeline even on empty frames so Norfair advances Kalman
1253
+ // state; the PTZ pose delta keeps predictions world-stable across
1254
+ // pans (track ids survive instead of LOST REACQUIRE).
1216
1255
  const poseDelta = this.ptzAutotracker.consumePoseDelta();
1217
1256
  const pipelineResult = this.pipeline.process(result.detections, objectFrame, poseDelta);
1218
- // Tracks for UI: matched always pass, extrapolated (Kalman-only)
1219
- // are shown for smooth bbox display across single-frame detector
1220
- // misses but filtered when they've drifted too far off-frame.
1221
- // When a fast-moving target walks off-screen, Norfair keeps
1222
- // extrapolating based on the last-known velocity; without this
1223
- // guard the UI ends up drawing ghost bboxes floating outside the
1224
- // stream and the autotracker has no reason to chase them anyway.
1257
+ // Extrapolated (Kalman-only) tracks smooth single-frame detector
1258
+ // misses in the UI, but are dropped once they drift off-frame
1259
+ // otherwise ghost bboxes float outside the stream.
1225
1260
  const visibleTracks = pipelineResult.tracked.filter((t) => {
1226
1261
  if (!t.trackLost)
1227
1262
  return true;
@@ -1233,16 +1268,9 @@ let DetectionCoordinator = class DetectionCoordinator {
1233
1268
  results.object = { ...result, detections: visibleTracks };
1234
1269
  if (pipelineResult.crossings.length > 0)
1235
1270
  results.lineCrossings = pipelineResult.crossings;
1236
- // Feed the autotracker with ALL tracked detections, incl. Kalman-
1237
- // extrapolated ones. When the object detector misses a single frame
1238
- // (normal with YOLO-style models), Norfair's extrapolation gives
1239
- // the best estimate of where the target still is — hiding that
1240
- // from the autotracker would cause a one-frame "target gone" blip
1241
- // and flip it into LOST, only to REACQUIRE on the very next frame
1242
- // when the detection returns. That churn is orthogonal to camera
1243
- // motion and shouldn't drive state transitions. The autotracker
1244
- // gates motor decisions on its own `isPtzMoving` flag, so during
1245
- // suppression it observes without commanding.
1271
+ // Feed the autotracker ALL tracked detections incl. extrapolated —
1272
+ // a single missed detector frame must not flip it into LOST/REACQUIRE
1273
+ // churn. It gates motor decisions on its own isPtzMoving flag.
1246
1274
  this.ptzAutotracker.handleObjectDetections(pipelineResult.tracked);
1247
1275
  }
1248
1276
  catch (error) {
@@ -1259,9 +1287,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1259
1287
  // Buffer line crossings so the EventManager snapshot picks them up
1260
1288
  this.currentDetectionState.lineCrossings = results.lineCrossings;
1261
1289
  }
1262
- // Funnel object result through unified ingest path. Same rule as motion:
1263
- // detected is re-derived from post-pipeline detections, the plugin's own
1264
- // detected flag is ignored after pipeline filtering.
1290
+ // Same rule as motion: detected is re-derived from post-pipeline detections.
1265
1291
  if (results.object && objectPlugin) {
1266
1292
  this.ingestDetectionResult(SensorType.Object, objectPlugin.sensorId, {
1267
1293
  detected: objectDetections.length > 0,
@@ -1270,49 +1296,62 @@ let DetectionCoordinator = class DetectionCoordinator {
1270
1296
  }
1271
1297
  if (!this.loopRunning)
1272
1298
  return;
1273
- // 3. Secondary detectors + per-detection thumbnails. Result-collection only —
1274
- // ingest happens via ingestResultsForAllSecondaries below so the loop tick
1275
- // and the external-RPC path share the same secondary-processing helper.
1299
+ // 3. Secondary detectors + per-detection thumbnails. Result-collection
1300
+ // only the shared ingest below keeps loop tick and external-RPC path
1301
+ // on the same helper.
1276
1302
  if (objectDetections.length > 0) {
1277
1303
  await this.runSecondariesAndThumbnails(rawFrame, objectDetections, results);
1278
1304
  }
1279
1305
  if (!this.loopRunning)
1280
1306
  return;
1281
- // 4. Funnel secondary results through unified ingest path.
1307
+ // 4. Unified ingest.
1282
1308
  this.ingestResultsForAllSecondaries(results);
1283
- // 5. Single flush — one processResults per frame, AFTER all detectors + thumbnails.
1284
- // This ensures segment-start messages have thumbnails and all secondary detection
1285
- // data (faces, plates, classifiers, clips) is available when the segment opens.
1309
+ // 5. Single flush per frame, AFTER all detectors + thumbnails — so
1310
+ // segment-start messages already carry thumbnails and secondary data.
1286
1311
  if (!this.loopRunning)
1287
1312
  return;
1288
1313
  const snapshot = this.buildSnapshot();
1289
1314
  if (results.thumbnails && results.thumbnails.length > 0) {
1290
1315
  snapshot.thumbnails = results.thumbnails;
1291
1316
  }
1292
- // 5a. Event-level thumbnail — full-frame downscaled JPEG. Generated lazily
1293
- // whenever the EventManager signals it still needs one. This covers two cases:
1294
- // 1. Event was already started by a non-frame path (sensor trigger, motion
1295
- // ingest from a plugin) first frame after start fills it in.
1296
- // 2. Event will be started by THIS frame's triggers pre-encode so the
1297
- // 'start' message carries the thumbnail inline.
1298
- // We use `eventManagerWillStart` heuristically here: if there's a trigger
1299
- // signal in the snapshot AND no active event yet, the event will be started
1300
- // by this processResults call. Either way, attaching is harmless because
1301
- // EventManager only consumes it if `needsEventThumbnail` is true.
1302
- if (this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot)) {
1317
+ // Event-level thumbnail — generated lazily when the EventManager still
1318
+ // needs one OR this frame's triggers will start an event (pre-encode so
1319
+ // the 'start' message carries it inline). Attaching is harmless either
1320
+ // way; EventManager only consumes it while needsEventThumbnail is true.
1321
+ // Full frame prefers the snapshot source (no aspect constraint needed).
1322
+ const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
1323
+ let hqThumbAttached = false;
1324
+ if (wantsEventThumb) {
1303
1325
  try {
1304
- const jpeg = await this.frameScaler.frameToJPEG(rawFrame, 320);
1305
- if (jpeg)
1306
- snapshot.eventThumbnail = jpeg;
1326
+ const hq = await this.acquireHqFrame();
1327
+ try {
1328
+ if (hq) {
1329
+ const jpeg = await hq.scaler.frameToJPEG(hq.frame, EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
1330
+ if (jpeg) {
1331
+ snapshot.eventThumbnail = jpeg;
1332
+ hqThumbAttached = true;
1333
+ }
1334
+ }
1335
+ if (!snapshot.eventThumbnail) {
1336
+ const jpeg = await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
1337
+ if (jpeg)
1338
+ snapshot.eventThumbnail = jpeg;
1339
+ }
1340
+ }
1341
+ finally {
1342
+ hq?.frame[Symbol.dispose]?.();
1343
+ }
1307
1344
  }
1308
1345
  catch (error) {
1309
1346
  this.logger.error('Event thumbnail generation error:', error);
1310
1347
  }
1311
1348
  }
1312
- // this.logger.debug(
1313
- // `[flush-debug] objects=${snapshot.objects.length} faces=${snapshot.faces.length} plates=${snapshot.plates.length} cls=${snapshot.classifiers.length} clips=${snapshot.clips.length} cascade=${snapshot.hasCascadeTrigger} thumbs=${snapshot.thumbnails?.length ?? 0}`,
1314
- // );
1315
1349
  this.eventManager.processResults(snapshot);
1350
+ // Inline HQ succeeded → nothing to upgrade; otherwise the async path
1351
+ // covers the window where the HQ source wasn't ready at event start.
1352
+ if (wantsEventThumb && !hqThumbAttached) {
1353
+ this.upgradeEventThumbnailAsync();
1354
+ }
1316
1355
  }
1317
1356
  snapshotWillStartEvent(snapshot) {
1318
1357
  if (this.eventManager.hasActiveEvent())
@@ -1375,7 +1414,6 @@ let DetectionCoordinator = class DetectionCoordinator {
1375
1414
  }
1376
1415
  async prepareSecondaryRegions(rawFrame, objectDetections) {
1377
1416
  const result = new Map();
1378
- // Collect all active secondary plugins and their requirements
1379
1417
  const consumers = [];
1380
1418
  const facePlugin = this.singlePlugins.get(SensorType.Face);
1381
1419
  if (facePlugin && this.hasSecondaryModelSpec(facePlugin.modelSpec) && this.isVideoInputSpec(facePlugin.modelSpec.input)) {
@@ -1399,7 +1437,6 @@ let DetectionCoordinator = class DetectionCoordinator {
1399
1437
  }
1400
1438
  if (consumers.length === 0)
1401
1439
  return result;
1402
- // For each detection, determine which consumers need it and what sizes
1403
1440
  for (const detection of objectDetections) {
1404
1441
  const detLabel = detection.label.toLowerCase();
1405
1442
  const targets = [];
@@ -1410,8 +1447,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1410
1447
  }
1411
1448
  if (targets.length === 0)
1412
1449
  continue;
1413
- // Deduplicate targets with identical dimensions (e.g. two classifiers at same size)
1414
- // — keep first key, share the result
1450
+ // Consumers with identical dimensions share one scaled region
1415
1451
  const uniqueTargets = [];
1416
1452
  const dupeMap = new Map(); // sizeKey → [consumerKey, ...]
1417
1453
  for (const t of targets) {
@@ -1426,7 +1462,6 @@ let DetectionCoordinator = class DetectionCoordinator {
1426
1462
  }
1427
1463
  }
1428
1464
  const regions = await this.frameScaler.cropAndScaleMulti(rawFrame, detection, uniqueTargets);
1429
- // Distribute results to all consumers (including those sharing dimensions)
1430
1465
  for (const [_sizeKey, consumerKeys] of dupeMap) {
1431
1466
  const primaryKey = consumerKeys[0];
1432
1467
  const region = regions.get(primaryKey);
@@ -1534,7 +1569,15 @@ let DetectionCoordinator = class DetectionCoordinator {
1534
1569
  this.currentDetectionState.faceEmbeddingModel = results.faceEmbeddingModel;
1535
1570
  }
1536
1571
  try {
1537
- results.thumbnails = await this.generateThumbnails(rawFrame, objectDetections, results);
1572
+ // Detectors consumed the substream frame above; images destined for the
1573
+ // event (chips, scene) prefer the snapshot frame when available.
1574
+ const hq = await this.acquireHqFrame({ width: rawFrame.width, height: rawFrame.height });
1575
+ try {
1576
+ results.thumbnails = await this.generateThumbnails(hq?.frame ?? rawFrame, hq?.scaler ?? this.frameScaler, hq !== null, objectDetections, results);
1577
+ }
1578
+ finally {
1579
+ hq?.frame[Symbol.dispose]?.();
1580
+ }
1538
1581
  }
1539
1582
  catch (error) {
1540
1583
  this.logger.error('Thumbnail generation error:', error);
@@ -1556,7 +1599,7 @@ let DetectionCoordinator = class DetectionCoordinator {
1556
1599
  }
1557
1600
  }
1558
1601
  try {
1559
- return await this.frameScaler.frameToJPEG(rawFrame, 320);
1602
+ return await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
1560
1603
  }
1561
1604
  catch (error) {
1562
1605
  this.logger.debug('Full-frame scene thumbnail failed:', error);
@@ -1617,65 +1660,12 @@ let DetectionCoordinator = class DetectionCoordinator {
1617
1660
  const facePlugin = this.singlePlugins.get(SensorType.Face);
1618
1661
  if (!facePlugin || croppedRegions.length === 0)
1619
1662
  return undefined;
1620
- // for (let i = 0; i < croppedRegions.length; i++) {
1621
- // const r = croppedRegions[i];
1622
- // // prettier-ignore
1623
- // this.logger.debug(
1624
- // `[face-debug] crop[${i}] label=${r.detection.label} ` +
1625
- // `cropSize=${r.cropSize.width}x${r.cropSize.height} ` +
1626
- // `scaledTo=${r.frame.width}x${r.frame.height} ` +
1627
- // `offset=(${r.offset.x},${r.offset.y}) ` +
1628
- // `original=${r.originalSize.width}x${r.originalSize.height}`,
1629
- // );
1630
- // // Debug: save person crop as raw RGB for visual inspection
1631
- // if (r.frame.data && r.detection.label === 'person') {
1632
- // try {
1633
- // const ts = Date.now();
1634
- // writeFileSync(`/tmp/crop_person_${ts}_${r.frame.width}x${r.frame.height}.rgb`, Buffer.from(r.frame.data as Buffer));
1635
- // this.logger.debug(`[face-debug] saved person crop: /tmp/crop_person_${ts}_${r.frame.width}x${r.frame.height}.rgb`);
1636
- // } catch {
1637
- // /* ignore */
1638
- // }
1639
- // }
1640
- // }
1641
1663
  const batchResults = await withTimeout(facePlugin.proxy.detectFaces(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'Face');
1642
1664
  const allFaces = [];
1643
1665
  for (let i = 0; i < batchResults.length; i++) {
1644
1666
  const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
1645
1667
  for (const face of batchResults[i].detections) {
1646
1668
  const transformed = this.transformBoxToOriginal(face.box, croppedRegions[i]);
1647
- // const rawBox = face.box;
1648
- // // prettier-ignore
1649
- // this.logger.debug(
1650
- // `[face-debug] face in crop[${i}] [parentTrackId=${parentTrackId}]: ` +
1651
- // `raw=(${rawBox.x.toFixed(4)},${rawBox.y.toFixed(4)},${rawBox.width.toFixed(4)},${rawBox.height.toFixed(4)}) ` +
1652
- // `transformed=(${transformed.x.toFixed(4)},${transformed.y.toFixed(4)},${transformed.width.toFixed(4)},${transformed.height.toFixed(4)}) ` +
1653
- // `conf=${face.confidence.toFixed(3)}`,
1654
- // );
1655
- // // Debug: save face crop region from the person crop for visual inspection
1656
- // if (croppedRegions[i].frame.data && face.confidence > 0.6) {
1657
- // try {
1658
- // const cropData = croppedRegions[i].frame.data as Buffer;
1659
- // const fw = croppedRegions[i].frame.width;
1660
- // const fh = croppedRegions[i].frame.height;
1661
- // const bpp = cropData.length / (fw * fh); // bytes per pixel (3 for rgb)
1662
- // const fx = Math.floor(rawBox.x * fw);
1663
- // const fy = Math.floor(rawBox.y * fh);
1664
- // const fbw = Math.floor(rawBox.width * fw);
1665
- // const fbh = Math.floor(rawBox.height * fh);
1666
- // const faceBuf = Buffer.alloc(fbw * fbh * bpp);
1667
- // for (let row = 0; row < fbh; row++) {
1668
- // const srcOff = ((fy + row) * fw + fx) * bpp;
1669
- // const dstOff = row * fbw * bpp;
1670
- // cropData.copy(faceBuf, dstOff, srcOff, srcOff + fbw * bpp);
1671
- // }
1672
- // const ts = Date.now();
1673
- // writeFileSync(`/tmp/crop_face_${ts}_${fbw}x${fbh}.rgb`, faceBuf);
1674
- // this.logger.debug(`[face-debug] saved face crop: /tmp/crop_face_${ts}_${fbw}x${fbh}.rgb`);
1675
- // } catch {
1676
- // /* ignore */
1677
- // }
1678
- // }
1679
1669
  allFaces.push({ ...face, box: transformed, parentTrackId });
1680
1670
  }
1681
1671
  }
@@ -1753,10 +1743,19 @@ let DetectionCoordinator = class DetectionCoordinator {
1753
1743
  const embeddingModel = clipPlugin.modelSpec?.embeddingModel ?? '';
1754
1744
  return { embeddings: allEmbeddings, embeddingModel };
1755
1745
  }
1756
- async generateThumbnails(rawFrame, objectDetections, results) {
1746
+ async generateThumbnails(sourceFrame, scaler, usingHq, objectDetections, results) {
1757
1747
  const thumbnails = [];
1748
+ const attributePadding = usingHq ? 0.4 : 0.75;
1749
+ const detectionMaxWidth = usingHq ? DETECTION_THUMB_HQ_MAX_WIDTH : DETECTION_THUMB_MAX_WIDTH;
1750
+ const detectionMinCrop = usingHq ? DETECTION_THUMB_HQ_MIN_CROP : DETECTION_THUMB_MIN_CROP;
1751
+ const detectionQuality = usingHq ? DETECTION_THUMB_HQ_QUALITY : undefined;
1758
1752
  // Object thumbnails — objectDetections are TrackedDetection[] when tracker ran
1759
- const objectCrops = await this.frameScaler.cropToJPEG(rawFrame, objectDetections);
1753
+ const objectCrops = await scaler.cropToJPEG(sourceFrame, objectDetections, {
1754
+ maxWidth: detectionMaxWidth,
1755
+ padding: 0.3,
1756
+ minCrop: detectionMinCrop,
1757
+ quality: detectionQuality,
1758
+ });
1760
1759
  for (const crop of objectCrops) {
1761
1760
  const detection = objectDetections[crop.index];
1762
1761
  if (detection) {
@@ -1772,24 +1771,16 @@ let DetectionCoordinator = class DetectionCoordinator {
1772
1771
  }
1773
1772
  }
1774
1773
  if (results.face?.detections) {
1775
- const faceCrops = await this.frameScaler.cropToJPEG(rawFrame, results.face.detections, { padding: 0.1 });
1774
+ const faceCrops = await scaler.cropToJPEG(sourceFrame, results.face.detections, {
1775
+ maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
1776
+ padding: attributePadding,
1777
+ minCrop: ATTRIBUTE_THUMB_MIN_CROP,
1778
+ });
1776
1779
  for (const crop of faceCrops) {
1777
1780
  const detection = results.face.detections[crop.index];
1778
1781
  if (detection) {
1779
- // Debug: save face thumbnail (cropped from rawFrame with transformed coords) for verification
1780
- // try {
1781
- // const ts = Date.now();
1782
- // writeFileSync(`/tmp/face_thumb_${ts}_${crop.index}.jpg`, crop.jpeg);
1783
- // this.logger.debug(
1784
- // `[face-debug] saved face thumbnail from rawFrame: /tmp/face_thumb_${ts}_${crop.index}.jpg ` +
1785
- // `box=(${detection.box.x.toFixed(4)},${detection.box.y.toFixed(4)},${detection.box.width.toFixed(4)},${detection.box.height.toFixed(4)}) ` +
1786
- // `rawFrame=${rawFrame.width}x${rawFrame.height}`,
1787
- // );
1788
- // } catch {
1789
- // /* ignore */
1790
- // }
1791
- // Set thumbnail on the detection itself so enrichSegment can inline it
1792
- // for unknown faces (needed for face store persistence).
1782
+ // Inline on the detection so enrichSegment can persist unknown
1783
+ // faces to the face store.
1793
1784
  detection.thumbnail ??= crop.jpeg;
1794
1785
  thumbnails.push({
1795
1786
  label: `face:${detection.identity ?? 'unknown'}`,
@@ -1802,7 +1793,11 @@ let DetectionCoordinator = class DetectionCoordinator {
1802
1793
  }
1803
1794
  }
1804
1795
  if (results.licensePlate?.detections) {
1805
- const plateCrops = await this.frameScaler.cropToJPEG(rawFrame, results.licensePlate.detections, { padding: 0.1 });
1796
+ const plateCrops = await scaler.cropToJPEG(sourceFrame, results.licensePlate.detections, {
1797
+ maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
1798
+ padding: attributePadding,
1799
+ minCrop: ATTRIBUTE_THUMB_MIN_CROP,
1800
+ });
1806
1801
  for (const crop of plateCrops) {
1807
1802
  const detection = results.licensePlate.detections[crop.index];
1808
1803
  if (detection) {
@@ -1818,7 +1813,12 @@ let DetectionCoordinator = class DetectionCoordinator {
1818
1813
  }
1819
1814
  if (results.classifiers) {
1820
1815
  for (const classifierResult of Object.values(results.classifiers)) {
1821
- const clsCrops = await this.frameScaler.cropToJPEG(rawFrame, classifierResult.detections);
1816
+ const clsCrops = await scaler.cropToJPEG(sourceFrame, classifierResult.detections, {
1817
+ maxWidth: detectionMaxWidth,
1818
+ padding: 0.3,
1819
+ minCrop: detectionMinCrop,
1820
+ quality: detectionQuality,
1821
+ });
1822
1822
  for (const crop of clsCrops) {
1823
1823
  const detection = classifierResult.detections[crop.index];
1824
1824
  if (detection) {