@trillboards/edge-sdk 0.2.138 → 0.2.150

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 (191) hide show
  1. package/README.md +2 -2
  2. package/deploy/detector/pyproject.toml +20 -2
  3. package/deploy/detector/tests/test_execution.py +124 -0
  4. package/deploy/detector/tests/test_model_identity.py +80 -0
  5. package/deploy/detector/tests/test_registry.py +18 -0
  6. package/deploy/detector/tests/test_server.py +3 -1
  7. package/deploy/detector/tests/test_version_stamp.py +4 -1
  8. package/deploy/detector/trillboards_detector/_version.py +2 -2
  9. package/deploy/detector/trillboards_detector/cli.py +3 -0
  10. package/deploy/detector/trillboards_detector/detections.py +7 -0
  11. package/deploy/detector/trillboards_detector/errors.py +4 -0
  12. package/deploy/detector/trillboards_detector/execution.py +109 -0
  13. package/deploy/detector/trillboards_detector/loader.py +6 -3
  14. package/deploy/detector/trillboards_detector/registry.py +27 -5
  15. package/deploy/detector/trillboards_detector/server.py +29 -2
  16. package/deploy/detector/trillboards_detector/session.py +15 -3
  17. package/deploy/litertlm/test_trillboards_litertlm_shim.py +81 -0
  18. package/deploy/litertlm/test_verify_model_proof.py +30 -0
  19. package/deploy/litertlm/trillboards_litertlm_shim.py +150 -1
  20. package/deploy/litertlm/verify_model_proof.py +30 -0
  21. package/deploy/macos/TrillboardsSetupApp.m +131 -3
  22. package/deploy/macos/build-macos-pkg.sh +13 -10
  23. package/deploy/macos/scripts/setup-detector.sh +2 -2
  24. package/deploy/macos/scripts/setup-litertlm.sh +66 -30
  25. package/deploy/macos/test-setup-litertlm-reinstall.sh +57 -4
  26. package/deploy/setup-guidance.json +112 -0
  27. package/deploy/systemd/setup-detector.sh +2 -2
  28. package/deploy/systemd/setup-litertlm.sh +51 -27
  29. package/deploy/windows/build-windows.ps1 +11 -6
  30. package/deploy/windows/bundle-windows-installer.ps1 +3 -3
  31. package/deploy/windows/e2e/test-local-sense.ps1 +6 -5
  32. package/deploy/windows/install-update.ps1 +52 -9
  33. package/deploy/windows/set-executable-icon.ps1 +24 -5
  34. package/deploy/windows/setup-detector.ps1 +142 -4
  35. package/deploy/windows/setup-litertlm.ps1 +53 -10
  36. package/deploy/windows/trillboards-setup.ps1 +101 -2
  37. package/dist/AudienceAnalyzeClient.d.ts +10 -0
  38. package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
  39. package/dist/AudienceAnalyzeClient.js +5 -4
  40. package/dist/AudienceAnalyzeClient.js.map +1 -1
  41. package/dist/CloudSensingManager.d.ts +38 -1
  42. package/dist/CloudSensingManager.d.ts.map +1 -1
  43. package/dist/CloudSensingManager.js +403 -25
  44. package/dist/CloudSensingManager.js.map +1 -1
  45. package/dist/DetectorFieldProducer.d.ts +5 -1
  46. package/dist/DetectorFieldProducer.d.ts.map +1 -1
  47. package/dist/DetectorFieldProducer.js +8 -1
  48. package/dist/DetectorFieldProducer.js.map +1 -1
  49. package/dist/DurableVlmCandidateSpool.d.ts +83 -2
  50. package/dist/DurableVlmCandidateSpool.d.ts.map +1 -1
  51. package/dist/DurableVlmCandidateSpool.js +596 -30
  52. package/dist/DurableVlmCandidateSpool.js.map +1 -1
  53. package/dist/EdgeAgent.js +1 -1
  54. package/dist/FrameGroupingShadow.d.ts +51 -0
  55. package/dist/FrameGroupingShadow.d.ts.map +1 -0
  56. package/dist/FrameGroupingShadow.js +158 -0
  57. package/dist/FrameGroupingShadow.js.map +1 -0
  58. package/dist/ImageClassifierFieldProducer.d.ts +36 -0
  59. package/dist/ImageClassifierFieldProducer.d.ts.map +1 -0
  60. package/dist/ImageClassifierFieldProducer.js +150 -0
  61. package/dist/ImageClassifierFieldProducer.js.map +1 -0
  62. package/dist/LiteRtVlmProvider.d.ts +5 -0
  63. package/dist/LiteRtVlmProvider.d.ts.map +1 -1
  64. package/dist/LiteRtVlmProvider.js +72 -7
  65. package/dist/LiteRtVlmProvider.js.map +1 -1
  66. package/dist/LocalDetectorClient.d.ts +6 -0
  67. package/dist/LocalDetectorClient.d.ts.map +1 -1
  68. package/dist/LocalDetectorClient.js +8 -0
  69. package/dist/LocalDetectorClient.js.map +1 -1
  70. package/dist/OrderedFrameReview.d.ts +57 -0
  71. package/dist/OrderedFrameReview.d.ts.map +1 -0
  72. package/dist/OrderedFrameReview.js +121 -0
  73. package/dist/OrderedFrameReview.js.map +1 -0
  74. package/dist/OrderedReviewClient.d.ts +38 -0
  75. package/dist/OrderedReviewClient.d.ts.map +1 -0
  76. package/dist/OrderedReviewClient.js +121 -0
  77. package/dist/OrderedReviewClient.js.map +1 -0
  78. package/dist/OrderedSequenceStore.d.ts +69 -0
  79. package/dist/OrderedSequenceStore.d.ts.map +1 -0
  80. package/dist/OrderedSequenceStore.js +257 -0
  81. package/dist/OrderedSequenceStore.js.map +1 -0
  82. package/dist/ProfileReviewWork.d.ts +68 -0
  83. package/dist/ProfileReviewWork.d.ts.map +1 -0
  84. package/dist/ProfileReviewWork.js +150 -0
  85. package/dist/ProfileReviewWork.js.map +1 -0
  86. package/dist/RetainedFieldContract.d.ts +21 -0
  87. package/dist/RetainedFieldContract.d.ts.map +1 -0
  88. package/dist/RetainedFieldContract.js +92 -0
  89. package/dist/RetainedFieldContract.js.map +1 -0
  90. package/dist/ReviewModelUse.d.ts +7 -0
  91. package/dist/ReviewModelUse.d.ts.map +1 -0
  92. package/dist/ReviewModelUse.js +30 -0
  93. package/dist/ReviewModelUse.js.map +1 -0
  94. package/dist/ReviewPriority.d.ts +12 -0
  95. package/dist/ReviewPriority.d.ts.map +1 -0
  96. package/dist/ReviewPriority.js +45 -0
  97. package/dist/ReviewPriority.js.map +1 -0
  98. package/dist/RuntimeModelSelection.d.ts +10 -0
  99. package/dist/RuntimeModelSelection.d.ts.map +1 -0
  100. package/dist/RuntimeModelSelection.js +58 -0
  101. package/dist/RuntimeModelSelection.js.map +1 -0
  102. package/dist/SceneChangeDetector.d.ts +1 -1
  103. package/dist/SceneChangeDetector.d.ts.map +1 -1
  104. package/dist/SceneChangeDetector.js +18 -6
  105. package/dist/SceneChangeDetector.js.map +1 -1
  106. package/dist/ScopedVlmReview.d.ts +76 -0
  107. package/dist/ScopedVlmReview.d.ts.map +1 -0
  108. package/dist/ScopedVlmReview.js +267 -0
  109. package/dist/ScopedVlmReview.js.map +1 -0
  110. package/dist/SensingProfileClient.d.ts +50 -0
  111. package/dist/SensingProfileClient.d.ts.map +1 -1
  112. package/dist/SensingProfileClient.js +58 -2
  113. package/dist/SensingProfileClient.js.map +1 -1
  114. package/dist/TemporalReviewClient.d.ts +43 -0
  115. package/dist/TemporalReviewClient.d.ts.map +1 -0
  116. package/dist/TemporalReviewClient.js +117 -0
  117. package/dist/TemporalReviewClient.js.map +1 -0
  118. package/dist/TemporalReviewJournal.d.ts +91 -0
  119. package/dist/TemporalReviewJournal.d.ts.map +1 -0
  120. package/dist/TemporalReviewJournal.js +337 -0
  121. package/dist/TemporalReviewJournal.js.map +1 -0
  122. package/dist/UrgentReviewTriage.d.ts +35 -0
  123. package/dist/UrgentReviewTriage.d.ts.map +1 -0
  124. package/dist/UrgentReviewTriage.js +52 -0
  125. package/dist/UrgentReviewTriage.js.map +1 -0
  126. package/dist/VideoSegmentUploader.d.ts +293 -0
  127. package/dist/VideoSegmentUploader.d.ts.map +1 -0
  128. package/dist/VideoSegmentUploader.js +1002 -0
  129. package/dist/VideoSegmentUploader.js.map +1 -0
  130. package/dist/VlmCandidateBuffer.d.ts +19 -0
  131. package/dist/VlmCandidateBuffer.d.ts.map +1 -1
  132. package/dist/VlmCandidateBuffer.js.map +1 -1
  133. package/dist/autoSense.d.ts +2 -2
  134. package/dist/autoSense.d.ts.map +1 -1
  135. package/dist/autoSense.js +16 -11
  136. package/dist/autoSense.js.map +1 -1
  137. package/dist/cli.js +1 -1
  138. package/dist/cli.js.map +1 -1
  139. package/dist/config.d.ts +2 -0
  140. package/dist/config.d.ts.map +1 -1
  141. package/dist/config.js.map +1 -1
  142. package/dist/continuousReview.d.ts +71 -0
  143. package/dist/continuousReview.d.ts.map +1 -0
  144. package/dist/continuousReview.js +53 -0
  145. package/dist/continuousReview.js.map +1 -0
  146. package/dist/detectorCountReview.d.ts +9 -0
  147. package/dist/detectorCountReview.d.ts.map +1 -0
  148. package/dist/detectorCountReview.js +79 -0
  149. package/dist/detectorCountReview.js.map +1 -0
  150. package/dist/diagnose.d.ts +1 -1
  151. package/dist/diagnose.js +1 -1
  152. package/dist/installerOnboardingMode.d.ts.map +1 -1
  153. package/dist/installerOnboardingMode.js +5 -3
  154. package/dist/installerOnboardingMode.js.map +1 -1
  155. package/dist/localInferenceSchema.d.ts +8 -0
  156. package/dist/localInferenceSchema.d.ts.map +1 -1
  157. package/dist/localInferenceSchema.js +3 -3
  158. package/dist/localInferenceSchema.js.map +1 -1
  159. package/dist/localSenseE2eHarness.d.ts +12 -0
  160. package/dist/localSenseE2eHarness.d.ts.map +1 -1
  161. package/dist/localSenseE2eHarness.js +77 -4
  162. package/dist/localSenseE2eHarness.js.map +1 -1
  163. package/dist/localSensing.d.ts +91 -18
  164. package/dist/localSensing.d.ts.map +1 -1
  165. package/dist/localSensing.js +678 -153
  166. package/dist/localSensing.js.map +1 -1
  167. package/dist/mixedScopedCounts.d.ts +17 -0
  168. package/dist/mixedScopedCounts.d.ts.map +1 -0
  169. package/dist/mixedScopedCounts.js +90 -0
  170. package/dist/mixedScopedCounts.js.map +1 -0
  171. package/dist/sensingCatchRate.d.ts +9 -5
  172. package/dist/sensingCatchRate.d.ts.map +1 -1
  173. package/dist/sensingCatchRate.js +10 -6
  174. package/dist/sensingCatchRate.js.map +1 -1
  175. package/dist/sensingRunMode.d.ts +5 -0
  176. package/dist/sensingRunMode.d.ts.map +1 -1
  177. package/dist/sensingRunMode.js +56 -0
  178. package/dist/sensingRunMode.js.map +1 -1
  179. package/package.json +7 -4
  180. package/retained/ArchiveLifecycle.cjs +79 -0
  181. package/retained/ModelStageScheduler.cjs +66 -0
  182. package/retained/ProfileAuthorityClient.cjs +88 -0
  183. package/retained/RetainedCaptureClient.cjs +164 -0
  184. package/retained/WaitRecheckCaptureClient.cjs +109 -0
  185. package/retained/__tests__/archive-lifecycle.test.cjs +117 -0
  186. package/retained/__tests__/runtime.test.cjs +384 -0
  187. package/retained/alert-rule-policy.cjs +37 -0
  188. package/retained/manager.cjs +133 -0
  189. package/retained/pilot-profile-client.cjs +82 -0
  190. package/retained/temporal-grouping.cjs +78 -0
  191. package/retained/website-api-capability.cjs +122 -0
@@ -0,0 +1,112 @@
1
+ {
2
+ "welcome": {
3
+ "title": "Start on the computer that will stay here.",
4
+ "body": "Keep this computer powered and connected to the camera network. Have your recorder login ready.",
5
+ "detail": "A recorder is the box your cameras connect to, often called an NVR or DVR. Internet access alone does not mean this computer can reach it."
6
+ },
7
+ "starting": {
8
+ "title": "Start on the computer that will stay here.",
9
+ "body": "Keep this computer powered and connected to the camera network. Have your recorder login ready.",
10
+ "detail": "A recorder is the box your cameras connect to, often called an NVR or DVR. Internet access alone does not mean this computer can reach it."
11
+ },
12
+ "onboarding_account": {
13
+ "title": "Choose email or Google.",
14
+ "body": "Use your Sense Anything email and password, or continue with Google. New here? Create an account with either option.",
15
+ "detail": "Your Sense Anything login connects your account. Later, camera setup asks for the separate login for your recorder or camera."
16
+ },
17
+ "onboarding_google": {
18
+ "title": "Finish signing in in the setup browser tab.",
19
+ "body": "Keep the setup tab open, complete Google sign-in, then return to this window. Your setup stays here.",
20
+ "detail": "Choose the intended Google account. If the handoff does not complete, use the recovery action in this window instead of reinstalling."
21
+ },
22
+ "onboarding_choose_account": {
23
+ "title": "Check which account will own the cameras.",
24
+ "body": "Choose the account for this location before continuing. Use an account you are authorized to manage.",
25
+ "detail": "If you manage several businesses, check the account name. Follow any account-access notice above before proceeding."
26
+ },
27
+ "account_attention": {
28
+ "title": "Check which account will own the cameras.",
29
+ "body": "Choose the account for this location before continuing. Use an account you are authorized to manage.",
30
+ "detail": "If you manage several businesses, check the account name. Follow any account-access notice above before proceeding."
31
+ },
32
+ "onboarding_terms": {
33
+ "title": "Review the agreements before continuing.",
34
+ "body": "Open the agreement links and review the current terms. Accept only when you are authorized to do so.",
35
+ "detail": "Guiding tips are optional. Turning them off never accepts an agreement or changes a required account step."
36
+ },
37
+ "onboarding_plan": {
38
+ "title": "Choose the plan for this account.",
39
+ "body": "Check the plan and billing interval before continuing to secure Stripe checkout.",
40
+ "detail": "Existing subscription access is checked automatically. If payment is needed, complete checkout in the setup browser and return here."
41
+ },
42
+ "onboarding_checkout": {
43
+ "title": "Complete checkout in your browser.",
44
+ "body": "Keep the setup browser tab open. The Connector continues after subscription access is confirmed.",
45
+ "detail": "If you cancel checkout, account setup is preserved. Use Back to plans to retry. Avoid a second purchase while payment is still being confirmed."
46
+ },
47
+ "onboarding_install": {
48
+ "title": "Continue in camera setup.",
49
+ "body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
50
+ "detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
51
+ },
52
+ "waiting_for_account": {
53
+ "title": "Continue in camera setup.",
54
+ "body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
55
+ "detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
56
+ },
57
+ "account_connected": {
58
+ "title": "Continue in camera setup.",
59
+ "body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
60
+ "detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
61
+ },
62
+ "discovering": {
63
+ "title": "Keep the recorder within reach.",
64
+ "body": "The Connector searches the local network. Select your recorder in camera setup and enter that device\u2019s login.",
65
+ "detail": "If nothing appears, check power and network connections or enter the recorder address manually. Guest Wi-Fi and isolated camera ports can prevent discovery."
66
+ },
67
+ "devices_found": {
68
+ "title": "Keep the recorder within reach.",
69
+ "body": "The Connector searches the local network. Select your recorder in camera setup and enter that device\u2019s login.",
70
+ "detail": "If nothing appears, check power and network connections or enter the recorder address manually. Guest Wi-Fi and isolated camera ports can prevent discovery."
71
+ },
72
+ "validating": {
73
+ "title": "Check each camera before adding it.",
74
+ "body": "Keep setup open while every channel is checked. Only validated feeds can be selected and added.",
75
+ "detail": "Empty, black or unreadable channels are left out. Check a missing camera\u2019s power, cable, permissions and stream settings, then retry in camera setup."
76
+ },
77
+ "cameras_created": {
78
+ "title": "Keep this computer awake while we finish.",
79
+ "body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
80
+ "detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
81
+ },
82
+ "local_ai": {
83
+ "title": "Keep this computer awake while we finish.",
84
+ "body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
85
+ "detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
86
+ },
87
+ "starting_service": {
88
+ "title": "Keep this computer awake while we finish.",
89
+ "body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
90
+ "detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
91
+ },
92
+ "local_ai_error": {
93
+ "title": "Retry local AI without pairing again.",
94
+ "body": "Your camera pairing is saved. Use Retry local AI to prepare the local model and service again.",
95
+ "detail": "Keep the computer powered and check its available disk space. Follow the error above. If the retry fails, contact support with the setup error, without camera credentials."
96
+ },
97
+ "ready": {
98
+ "title": "Leave this computer powered and connected.",
99
+ "body": "Sense Anything starts automatically when this computer logs in. Use View cameras to check sensing status.",
100
+ "detail": "Sleep, power loss or a disconnected camera network can interrupt sensing. Reopen Sense Anything Connector to resume or repair setup."
101
+ },
102
+ "error": {
103
+ "title": "Follow the setup notice above.",
104
+ "body": "Use the recovery action shown in this window. Keep your account and recorder credentials private.",
105
+ "detail": "If the problem continues, contact support with the visible error. Do not share passwords or camera stream links in screenshots."
106
+ },
107
+ "update_required": {
108
+ "title": "Follow the setup notice above.",
109
+ "body": "Use the recovery action shown in this window. Keep your account and recorder credentials private.",
110
+ "detail": "If the problem continues, contact support with the visible error. Do not share passwords or camera stream links in screenshots."
111
+ }
112
+ }
@@ -93,7 +93,7 @@ SOURCE="$(find_detector_source)" || { log "ERROR: detector package source not fo
93
93
  # re-downloads the pinned ~250 MB onnxruntime wheel on a shop PC every time, for
94
94
  # nothing — an exact pin cannot resolve to different bytes.
95
95
  log "installing/repairing trillboards-detector from $SOURCE with uv-managed Python $DETECTOR_PYTHON"
96
- if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1; then
96
+ if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1; then
97
97
  log "ERROR: trillboards-detector install failed; the cascade stays off"
98
98
  exit 0
99
99
  fi
@@ -126,7 +126,7 @@ elif [ "$HAVE_VERSION" != "$WANT_VERSION" ]; then
126
126
  # asking it again the same way is not a repair.
127
127
  log "WARN: the installed detector reports '$HAVE_VERSION' but the staged package is '$WANT_VERSION' - reinstalling from scratch"
128
128
  "$UV" tool uninstall trillboards-detector >>"$LOG" 2>&1 || true
129
- "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1 || true
129
+ "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1 || true
130
130
  DETECTOR="$LOCAL_BIN/trillboards-detector"
131
131
  [ -x "$DETECTOR" ] || DETECTOR="$(command -v trillboards-detector || echo "$DETECTOR")"
132
132
  HAVE_VERSION="$(installed_detector_version)"
@@ -37,6 +37,9 @@ LOG_DIR="${TRILLBOARDS_LOG_DIR:-/var/log/trillboards}"
37
37
  LOCAL_BIN="$USER_HOME/.local/bin"
38
38
  MODEL_ID="gemma-4-E2B-it.litertlm"
39
39
  HF_REPO="litert-community/gemma-4-E2B-it-litert-lm"
40
+ MODEL_REVISION="84b6978eff6e4eea02825bc2ee4ea48579f13109"
41
+ APPROVED_MODEL_SHA256="ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42"
42
+ MODEL_URL="https://huggingface.co/$HF_REPO/resolve/$MODEL_REVISION/$MODEL_ID"
40
43
  SERVE_PORT="9379"
41
44
  SERVE_UNIT="trillboards-litertlm"
42
45
  LITERT_PYTHON="${LITERT_LM_PYTHON:-3.12}"
@@ -253,16 +256,29 @@ install_litertlm_shim() {
253
256
  # Deliberately NOT a /v1/models check: that answers 200 without ever building an
254
257
  # engine, so it cannot tell a working GPU encoder from one that fails on first
255
258
  # use.
256
- verify_litertlm_vision() {
257
- _body="$LITERT_DIR/.vision-probe.json"
258
- printf '{"model":"%s,gpu","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
259
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
260
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
259
+ verify_litertlm_probe() {
260
+ local requested="$1" body response code tool_dir py verifier candidate valid=1
261
+ body="$LITERT_DIR/.identity-probe.json"
262
+ response="$LITERT_DIR/.identity-response.json"
263
+ printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
264
+ "$requested" "$LITERTLM_PROBE_JPEG_B64" > "$body" 2>/dev/null || return 1
265
+ code=$(curl -s -o "$response" -w '%{http_code}' --max-time 180 \
261
266
  -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
262
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
263
- rm -f "$_body" 2>/dev/null || true
264
- [ "$_code" = "200" ]
267
+ -H 'Content-Type: application/json' -H "X-Trillboards-Model-Sha256: $APPROVED_MODEL_SHA256" \
268
+ --data-binary @"$body" 2>/dev/null)
269
+ if [ "$code" = "200" ]; then
270
+ tool_dir="$("${UV:-$LOCAL_BIN/uv}" tool dir 2>/dev/null | tr -d '\r')"
271
+ py="$tool_dir/litert-lm/bin/python"
272
+ for candidate in "$SCRIPT_DIR/verify_model_proof.py" "$SCRIPT_DIR/../litertlm/verify_model_proof.py" "$SCRIPT_DIR/../../litertlm/verify_model_proof.py"; do
273
+ [ -f "$candidate" ] && { verifier="$candidate"; break; }
274
+ done
275
+ [ -n "${verifier:-}" ] && [ -x "$py" ] \
276
+ && "$py" "$verifier" "$response" "$requested" "$APPROVED_MODEL_SHA256" && valid=0
277
+ fi
278
+ rm -f "$body" "$response" 2>/dev/null || true
279
+ return "$valid"
265
280
  }
281
+ verify_litertlm_vision() { verify_litertlm_probe "$MODEL_ID,gpu"; }
266
282
 
267
283
  # The same probe, but asking for NO delegate suffix rather than `,gpu`.
268
284
  #
@@ -273,16 +289,7 @@ verify_litertlm_vision() {
273
289
  # carries no suffix at all, so a 200 means the box built an engine and ran a real
274
290
  # image through it under the config now on disk, which is exactly the claim each
275
291
  # rung needs.
276
- verify_litertlm_plain_inference() {
277
- _body="$LITERT_DIR/.plain-probe.json"
278
- printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
279
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
280
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
281
- -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
282
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
283
- rm -f "$_body" 2>/dev/null || true
284
- [ "$_code" = "200" ]
285
- }
292
+ verify_litertlm_plain_inference() { verify_litertlm_probe "$MODEL_ID"; }
286
293
 
287
294
  local_ai_healthy() {
288
295
  curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
@@ -362,20 +369,37 @@ fi
362
369
 
363
370
  install_litertlm_shim || true
364
371
 
365
- # 3. Model. Idempotent: skip entirely if litert-lm already has it.
372
+ # 3. Model: a matching name is not a matching artifact. Upgrade from the immutable
373
+ # revision, preserve the old bytes, and verify the installed copy.
366
374
  HAVE_MODEL=0
367
- if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
368
- log "model $MODEL_ID already present — skipping import"
375
+ INSTALLED_SHA="$(sha256sum "$STORE_DIR/model.litertlm" 2>/dev/null | awk '{print $1}')"
376
+ if [ "$INSTALLED_SHA" = "$APPROVED_MODEL_SHA256" ]; then
377
+ log "approved model $MODEL_ID already present — skipping import"
369
378
  HAVE_MODEL=1
370
379
  else
371
380
  [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "local AI model is missing and the network import is disabled"
372
- log "importing $MODEL_ID from $HF_REPO (~2.4 GB, public repo) — this takes a while"
373
- if "$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" >>"$LOG" 2>&1; then
374
- log "model imported"
375
- HAVE_MODEL=1
376
- else
377
- fail "model import failed — the connector will WAIT (and beacon) until this succeeds"
381
+ mkdir -p "$STORE_DIR" || fail "model store is unavailable"
382
+ DOWNLOAD="$(mktemp "$STORE_DIR/approved-model.XXXXXX")" || fail "could not create a model download file"
383
+ log "downloading approved $MODEL_ID from immutable revision $MODEL_REVISION"
384
+ if ! curl -fL --retry 3 --output "$DOWNLOAD" "$MODEL_URL" >>"$LOG" 2>&1 \
385
+ || [ "$(sha256sum "$DOWNLOAD" | awk '{print $1}')" != "$APPROVED_MODEL_SHA256" ]; then
386
+ rm -f "$DOWNLOAD"
387
+ fail "approved model download/checksum failed"
388
+ fi
389
+ if [ -n "$INSTALLED_SHA" ]; then
390
+ ROLLBACK="$STORE_DIR/rollback/$INSTALLED_SHA.litertlm"
391
+ mkdir -p "$STORE_DIR/rollback" || fail "could not preserve previous model"
392
+ [ -f "$ROLLBACK" ] || cp "$STORE_DIR/model.litertlm" "$ROLLBACK" || fail "could not preserve previous model"
393
+ [ "$(sha256sum "$ROLLBACK" | awk '{print $1}')" = "$INSTALLED_SHA" ] || fail "previous model backup verification failed"
378
394
  fi
395
+ if ! "$LITERTLM" import "$DOWNLOAD" "$MODEL_ID" >>"$LOG" 2>&1 \
396
+ || [ "$(sha256sum "$STORE_DIR/model.litertlm" | awk '{print $1}')" != "$APPROVED_MODEL_SHA256" ]; then
397
+ rm -f "$DOWNLOAD"
398
+ fail "approved model import verification failed — the connector will WAIT until this succeeds"
399
+ fi
400
+ rm -f "$DOWNLOAD"
401
+ HAVE_MODEL=1
402
+ log "model imported from verified immutable revision"
379
403
  fi
380
404
 
381
405
  # Record the installed version next to the store model so the connector's
@@ -16,11 +16,11 @@
16
16
  bin\trillboards-pair.cmd first-run claim-code pairing helper
17
17
  lib\edge-sdk\dist\cli.js the connector CLI
18
18
  lib\node_modules\... runtime subset (edge-core + edge-platform-rtsp +
19
- edge-sdk + transitive deps), WITHOUT the heavy
20
- local ONNX/sharp/sqlite inference stack.
19
+ edge-sdk + transitive deps, including Sharp for
20
+ focused image masks), WITHOUT local ONNX/sqlite.
21
21
 
22
- The connector is loaded WITHOUT the heavy ONNX/sharp/sqlite stack (cli.ts
23
- lazy-requires EdgeAgent), so the bundle omits onnxruntime-node/sharp/
22
+ The connector is loaded WITHOUT the heavy ONNX/sqlite stack (cli.ts
23
+ lazy-requires EdgeAgent), so the bundle omits onnxruntime-node/
24
24
  better-sqlite3 — keeping the inner .exe small. The release workflow signs this
25
25
  connector installer, then bundle-windows-installer.ps1 wraps it with the
26
26
  pinned Gemma model in one customer-facing executable. The inner installer
@@ -107,6 +107,7 @@ Push-Location $WorkspaceRoot; npm run build; if ($LASTEXITCODE -ne 0) { Die "edg
107
107
  Log "Staging edge-sdk dist…"
108
108
  New-Item -ItemType Directory -Force -Path "$Payload\lib\edge-sdk" | Out-Null
109
109
  Copy-Item -Recurse -Force "$PkgRoot\dist" "$Payload\lib\edge-sdk\dist"
110
+ Copy-Item -Recurse -Force "$PkgRoot\retained" "$Payload\lib\edge-sdk\retained"
110
111
  Copy-Item -Force "$PkgRoot\package.json" "$Payload\lib\edge-sdk\package.json"
111
112
 
112
113
  # ─── 3. Runtime node_modules subset (NO heavy native stack) ────────────────────
@@ -123,9 +124,11 @@ Copy-Item -Force "$PkgRoot\package.json" "$Payload\lib\edge-sdk\package.json"
123
124
  # the job was killed at its timeout on EVERY release run, so the .exe never
124
125
  # published. /MT parallelises the small-file copy; /R:1 /W:1 bound retries so a
125
126
  # single transiently-locked file can't trigger robocopy's default 1e6×30s hang.
126
- Log "Staging runtime node_modules (excluding ONNX/sharp/sqlite/dev)…"
127
+ Log "Staging runtime node_modules (including focused-image masks; excluding ONNX/sqlite/dev)…"
127
128
  New-Item -ItemType Directory -Force -Path "$Payload\lib\node_modules" | Out-Null
128
- $xd = @("onnxruntime-node","sharp","@img","better-sqlite3","ffmpeg-static",".cache",".bin","typescript","jest","jest-cli","@jest","ts-jest","@types")
129
+ # Focused retained reviews use Sharp to mask pixels, not just prompt geometry.
130
+ # Keep its platform-native dependencies; detectors still use the local HTTP service.
131
+ $xd = @("onnxruntime-node","better-sqlite3","ffmpeg-static",".cache",".bin","typescript","jest","jest-cli","@jest","ts-jest","@types")
129
132
  $rcArgs = @("$WorkspaceRoot\node_modules", "$Payload\lib\node_modules", "/E", "/XJ", "/MT:16", "/R:1", "/W:1", "/NFL","/NDL","/NJH","/NJS","/NP")
130
133
  foreach ($d in $xd) { $rcArgs += @("/XD", $d) }
131
134
  robocopy @rcArgs | Out-Null
@@ -141,6 +144,7 @@ foreach ($p in @("edge-core","edge-sensing","edge-ads","edge-federated","edge-cl
141
144
  if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
142
145
  New-Item -ItemType Directory -Force -Path $dest | Out-Null
143
146
  if (Test-Path "$src\dist") { Copy-Item -Recurse -Force "$src\dist" "$dest\dist" }
147
+ if ($p -eq 'edge-sdk') { Copy-Item -Recurse -Force "$src\retained" "$dest\retained" }
144
148
  Copy-Item -Force "$src\package.json" "$dest\package.json"
145
149
  }
146
150
 
@@ -193,6 +197,7 @@ Log "Bundled ffmpeg OK"
193
197
  # PATH=bin first so the connector's default ffmpeg='ffmpeg' resolves to OURS.
194
198
  Copy-Item -Force "$Here\trillboards-edge.cmd" "$Payload\bin\trillboards-edge.cmd"
195
199
  Copy-Item -Force "$Here\trillboards-pair.cmd" "$Payload\bin\trillboards-pair.cmd"
200
+ Copy-Item -Force "$Here\..\setup-guidance.json" "$Payload\bin\setup-guidance.json"
196
201
  Copy-Item -Force "$Here\trillboards-setup.ps1" "$Payload\bin\trillboards-setup.ps1"
197
202
  Copy-Item -Force "$Here\trillboards-setup.vbs" "$Payload\bin\trillboards-setup.vbs"
198
203
  Copy-Item -Force "$Here\trillboards-edge-task.vbs" "$Payload\bin\trillboards-edge-task.vbs"
@@ -15,8 +15,8 @@ param(
15
15
  [Parameter(Mandatory = $true)][string]$InnerInstaller,
16
16
  [string]$OutputPath = "",
17
17
  [string]$ModelPath = "",
18
- [string]$ModelUrl = "https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm",
19
- [string]$ModelSha256 = "181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c",
18
+ [string]$ModelUrl = "https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/84b6978eff6e4eea02825bc2ee4ea48579f13109/gemma-4-E2B-it.litertlm",
19
+ [string]$ModelSha256 = "ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42",
20
20
  [string]$SevenZipSdkUrl = "https://www.7-zip.org/a/lzma2602.7z",
21
21
  [string]$SevenZipSdkSha256 = "2878c85f5f43a4a4e0952b1fd4e5fe097c1c143997a8047c7e1e788892aa9357",
22
22
  [string]$IconPath = ""
@@ -87,7 +87,7 @@ if (-not $sfxModule) { Fail "official 7zSD.sfx installer module was not found" }
87
87
  # Brand the executable module before appending the config and archive. This
88
88
  # keeps the SFX overlay byte-for-byte intact and the final Authenticode signing
89
89
  # step covers both the application icon and the customer payload.
90
- & (Join-Path $Here "set-executable-icon.ps1") -ExecutablePath $sfxModule.FullName -IconPath $IconPath
90
+ & (Join-Path $Here "set-executable-icon.ps1") -ExecutablePath $sfxModule.FullName -IconPath $IconPath -EnsurePerUserExecution
91
91
 
92
92
  $innerName = "TrillboardsEdgeSetup-core.exe"
93
93
  Copy-Item -LiteralPath $InnerInstaller -Destination (Join-Path $Stage $innerName) -Force
@@ -12,11 +12,12 @@
12
12
  wiring (DEFAULT, runs on EVERY CI event incl. PRs):
13
13
  Builds the connector, then runs the cross-platform Node wiring runner
14
14
  (scripts/run-local-sense-wiring-e2e.mjs) which drives the REAL built
15
- `cli.js local-sense` against a STUB :9379 VLM (returns a fixed valid Gemma
16
- JSON, person_count>=1) + a STUB ingest API, feeding the committed person
17
- fixture through the connector's real RTSP/ffmpeg path. Proves the
18
- connector → serve → preAnalyzed → assert plumbing WITHOUT the ~2.4 GB model.
19
- This is a STUB-VLM proof — NOT a real-model proof.
15
+ `cli.js local-sense` against STUB local VLM/detector services and an
16
+ authenticated retained capability/profile/ingest API, feeding the committed
17
+ fixture through real RTSP/ffmpeg. Checks declared field ownership, pinned
18
+ requests and original evidence/hash/capture time with the genuine retained
19
+ protocol parser/acknowledgement, WITHOUT downloading the real models.
20
+ This is a STUB-MODEL wiring proof — NOT an inference-accuracy proof.
20
21
 
21
22
  bench (workflow_dispatch only):
22
23
  Provisions exactly as 'real' does, then runs scripts/bench-local-vlm.mjs
@@ -56,12 +56,10 @@ function Register-Connector([string]$Root) {
56
56
  2026-08-19 Windows BOM incident, and it is why this is here rather than left
57
57
  to the repair path.
58
58
 
59
- RE-REGISTER, NOT RE-PROVISION. `setup-detector.ps1` runs `uv tool install
60
- --force`, which rebuilds a venv and needs the network; putting that inside an
61
- update transaction would give the upgrade a new way to be slow and a new way
62
- to fail. Re-registering the task is the whole of what has to change, and
63
- register-detector-task.ps1 already stops the running service, replaces the
64
- action and starts it again - so this both migrates and restarts it.
59
+ Re-register when the runtime source stamp matches. When it differs, run the
60
+ owned provisioner so a healthy old CPU-only service cannot survive a GPU
61
+ update indefinitely. The provisioner serializes replacement, verifies its
62
+ installed stamp and chooses one CPU/CUDA dependency set. Pairings stay intact.
65
63
 
66
64
  BEST EFFORT, ALWAYS. The detector is a cost optimisation: without it every
67
65
  frame goes to the VLM exactly as it does today. It must never be able to fail
@@ -77,6 +75,47 @@ function Update-DetectorTask([string]$Root) {
77
75
  # register-task.ps1 states for its own repair.
78
76
  $exe = ($task.Actions | Select-Object -First 1).Execute
79
77
  if (-not $exe) { return }
78
+ # Older updater array-splatting bound the text '-DetectorPath' positionally
79
+ # as the executable, shifting the real path into --port. Recover ONLY this
80
+ # known shape, under the current user's SID and exact local tool path.
81
+ if ($exe -eq '-DetectorPath') {
82
+ $taskUser = [string]$task.Principal.UserId
83
+ $taskSid = if ($taskUser -match '^S-1-') { $taskUser } else {
84
+ (New-Object System.Security.Principal.NTAccount($taskUser)).Translate([System.Security.Principal.SecurityIdentifier]).Value
85
+ }
86
+ $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
87
+ $oldArgs = [string]($task.Actions | Select-Object -First 1).Arguments
88
+ $ownTool = Join-Path $env:USERPROFILE '.local\bin\trillboards-detector.exe'
89
+ if ($taskSid -ne $currentSid -or $oldArgs -notmatch '^serve --host 127\.0\.0\.1 --port (.+?) --log-file "-RamBudgetMb" --ram-budget-mb \d+$') {
90
+ throw 'Cannot verify ownership of the malformed detector task'
91
+ }
92
+ $recovered = $Matches[1]
93
+ if ([IO.Path]::GetFullPath($recovered) -ine [IO.Path]::GetFullPath($ownTool) -or -not (Test-Path -LiteralPath $ownTool)) {
94
+ throw 'Malformed detector task does not identify this user installation'
95
+ }
96
+ $exe = $ownTool
97
+ }
98
+
99
+ $detectorSource = Join-Path $Root 'bin\detector'
100
+ $project = Join-Path $detectorSource 'pyproject.toml'
101
+ $setup = Join-Path $Root 'bin\setup-detector.ps1'
102
+ if ((Test-Path -LiteralPath $project) -and (Test-Path -LiteralPath $setup)) {
103
+ $versionLine = @(Get-Content -LiteralPath $project | Where-Object { $_ -match '^version\s*=\s*"([^"]+)"\s*$' })
104
+ if ($versionLine.Count -ne 1 -or $versionLine[0] -notmatch '^version\s*=\s*"([^"]+)"\s*$') {
105
+ throw 'Cannot resolve the staged detector source version'
106
+ }
107
+ $stagedDetectorVersion = $Matches[1]
108
+ $installedDetectorVersion = ((& $exe --version 2>$null | Out-String).Trim() -split '\s+')[-1]
109
+ if ($LASTEXITCODE -ne 0 -or $installedDetectorVersion -ne $stagedDetectorVersion) {
110
+ Write-Host "Updating the managed detector runtime to $stagedDetectorVersion."
111
+ & $setup -SourcePath $detectorSource
112
+ $verifiedDetectorVersion = ((& $exe --version 2>$null | Out-String).Trim() -split '\s+')[-1]
113
+ if ($LASTEXITCODE -ne 0 -or $verifiedDetectorVersion -ne $stagedDetectorVersion) {
114
+ throw 'Detector runtime update did not verify; pairing preserved, local detector needs repair'
115
+ }
116
+ return # the owned provisioner already registered and restarted the task
117
+ }
118
+ }
80
119
 
81
120
  $register = Join-Path $Root "bin\register-detector-task.ps1"
82
121
  if (-not (Test-Path -LiteralPath $register)) { return }
@@ -93,12 +132,16 @@ function Update-DetectorTask([string]$Root) {
93
132
  if ($LASTEXITCODE -eq 0 -and $derived -match '^\d+$') { $budget = $derived }
94
133
  }
95
134
 
96
- $registerArgs = @('-DetectorPath', $exe)
97
- if ($budget) { $registerArgs += @('-RamBudgetMb', $budget) }
135
+ # A PowerShell SCRIPT takes named parameters from a hashtable. An array of
136
+ # flag strings is positional; it creates Execute='-DetectorPath' while
137
+ # reporting registration success. Native powershell.exe argument arrays
138
+ # elsewhere are a different boundary and must not be changed with this.
139
+ $registerArgs = @{ DetectorPath = $exe }
140
+ if ($budget) { $registerArgs.RamBudgetMb = $budget }
98
141
  & $register @registerArgs | Out-Null
99
142
  Write-Host "Detector task migrated$(if ($budget) { " (RAM ceiling $budget MiB)" })."
100
143
  } catch {
101
- Write-Warning "Could not migrate the detector task: $($_.Exception.Message). Sensing is unaffected; every frame keeps going to the VLM."
144
+ Write-Warning "Could not migrate the detector task: $($_.Exception.Message). Detector-backed profiles need the local detector repaired; camera pairing is preserved."
102
145
  }
103
146
  }
104
147
 
@@ -1,16 +1,19 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Replaces every application icon group in a Windows executable.
3
+ Updates installer icons and optionally declares per-user execution.
4
4
 
5
5
  .DESCRIPTION
6
6
  The customer installer uses the official 7-Zip SFX module, whose generic
7
7
  icon would otherwise become the icon customers see in Explorer. This helper
8
8
  updates that module before the installer payload is appended and signed.
9
+ EnsurePerUserExecution adds an asInvoker manifest to avoid Windows installer
10
+ heuristics requesting elevation for this per-user connector wrapper.
9
11
  #>
10
12
  [CmdletBinding()]
11
13
  param(
12
14
  [Parameter(Mandatory = $true)][string]$ExecutablePath,
13
- [Parameter(Mandatory = $true)][string]$IconPath
15
+ [Parameter(Mandatory = $true)][string]$IconPath,
16
+ [switch]$EnsurePerUserExecution
14
17
  )
15
18
 
16
19
  $ErrorActionPreference = "Stop"
@@ -28,6 +31,7 @@ using System.Collections.Generic;
28
31
  using System.ComponentModel;
29
32
  using System.IO;
30
33
  using System.Runtime.InteropServices;
34
+ using System.Text;
31
35
 
32
36
  namespace SenseAnything.Build
33
37
  {
@@ -140,7 +144,7 @@ namespace SenseAnything.Build
140
144
  IntPtr update,
141
145
  [MarshalAs(UnmanagedType.Bool)] bool discard);
142
146
 
143
- public static void Replace(string executablePath, string iconPath)
147
+ public static void Replace(string executablePath, string iconPath, bool ensurePerUserExecution)
144
148
  {
145
149
  List<IconImage> images = ReadIcon(iconPath);
146
150
  List<ResourceGroup> groups = ReadIconGroups(executablePath);
@@ -210,7 +214,22 @@ namespace SenseAnything.Build
210
214
  }
211
215
  }
212
216
 
213
- Require(EndUpdateResourceW(update, false), "Could not commit the executable icon");
217
+ if (ensurePerUserExecution)
218
+ {
219
+ // The official SFX module has no execution-level manifest.
220
+ // Declare the same per-user level as the inner NSIS package;
221
+ // do not let installer heuristics request elevation instead.
222
+ byte[] manifest = Encoding.UTF8.GetBytes(@"<?xml version=""1.0"" encoding=""UTF-8""?>
223
+ <assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0"">
224
+ <assemblyIdentity name=""Trillboards.SenseAnything.Connector"" version=""1.0.0.0"" type=""win32""/>
225
+ <trustInfo xmlns=""urn:schemas-microsoft-com:asm.v3""><security><requestedPrivileges>
226
+ <requestedExecutionLevel level=""asInvoker"" uiAccess=""false""/>
227
+ </requestedPrivileges></security></trustInfo>
228
+ </assembly>");
229
+ Require(UpdateResourceById(update, MakeResource(24), MakeResource(1), 0,
230
+ manifest, (uint)manifest.Length), "Could not write the per-user execution manifest");
231
+ }
232
+ Require(EndUpdateResourceW(update, false), "Could not commit the executable resources");
214
233
  committed = true;
215
234
  }
216
235
  finally
@@ -350,5 +369,5 @@ namespace SenseAnything.Build
350
369
  }
351
370
  '@
352
371
 
353
- [SenseAnything.Build.ExecutableIcon]::Replace($ExecutablePath, $IconPath)
372
+ [SenseAnything.Build.ExecutableIcon]::Replace($ExecutablePath, $IconPath, $EnsurePerUserExecution.IsPresent)
354
373
  Write-Host "[icon] applied Sense Anything icon to $ExecutablePath" -ForegroundColor Green