@trillboards/edge-sdk 0.2.3 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +122 -65
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
package/README.md CHANGED
@@ -184,7 +184,7 @@ docker run -d \
184
184
  -e DEVICE_TOKEN=your-token \
185
185
  -p 9090:9090 \
186
186
  --device /dev/video0:/dev/video0 \
187
- trillboards/edge-sdk:latest
187
+ ghcr.io/trillboards/edge-sdk:latest
188
188
 
189
189
  # NVIDIA GPU
190
190
  docker run -d \
@@ -193,7 +193,7 @@ docker run -d \
193
193
  -e DEVICE_TOKEN=your-token \
194
194
  -p 9090:9090 \
195
195
  --device /dev/video0:/dev/video0 \
196
- trillboards/edge-sdk:cuda
196
+ ghcr.io/trillboards/edge-sdk:cuda
197
197
  ```
198
198
 
199
199
  **docker-compose:**
@@ -201,7 +201,7 @@ docker run -d \
201
201
  ```yaml
202
202
  services:
203
203
  edge-agent:
204
- image: trillboards/edge-sdk:latest
204
+ image: ghcr.io/trillboards/edge-sdk:latest
205
205
  restart: unless-stopped
206
206
  environment:
207
207
  - DEVICE_TOKEN=${DEVICE_TOKEN}
@@ -249,6 +249,7 @@ Or register via the [Trillboards API](https://api.trillboards.com/developer).
249
249
  | [docs/CLI.md](../../docs/CLI.md) | Full CLI reference with all commands, flags, and configuration |
250
250
  | [docs/MCP.md](../../docs/MCP.md) | MCP server reference for AI agent integration |
251
251
  | [docs/STATUS_API.md](../../docs/STATUS_API.md) | Status endpoint API reference |
252
+ | [deploy/macos/README.md](deploy/macos/README.md) | macOS one-click `.pkg` installer (cloud-sense CCTV connector) |
252
253
  | [deploy/docker/README.md](../../deploy/docker/README.md) | Docker deployment guide |
253
254
  | [deploy/helm/README.md](../../deploy/helm/README.md) | Helm chart deployment guide |
254
255
  | [create-trillboards-edge](../../../create-trillboards-edge/README.md) | Project scaffolding tool |
@@ -0,0 +1,117 @@
1
+ # Trillboards Edge AI SDK — BYO-CCTV connector container
2
+ #
3
+ # No local ONNX models bundled, so this image never downloads models and is much
4
+ # lighter than the cpu/cuda/openvino variants. It pairs with a short-lived claim
5
+ # code (the device token is delivered at pair, NEVER baked into the image) then
6
+ # senses. Same connector + same CLI as the macOS/Windows installers — ONE image,
7
+ # not a parallel one.
8
+ #
9
+ # TWO TAGS, ONE IMAGE. `SENSE_MODE` bakes the default the entrypoint runs:
10
+ # :auto-sense (SENSE_MODE=auto-sense, the default and the recommended tag)
11
+ # probes localSensing.litertLmBaseUrl at startup and commits to
12
+ # on-device inference if a healthy litert-lm is hosting the model.
13
+ # Because this image bundles NO model, the entrypoint also asserts
14
+ # localSensing.allowCloudFallback=true — auto-sense otherwise
15
+ # treats cloud as opt-in and would WAIT forever for a runtime that
16
+ # is never going to appear here. With that assert, a plain
17
+ # `docker run` behaves exactly like :cloud-sense (frames to the
18
+ # cloud VLM) and additionally AUTO-UPGRADES to on-device with no
19
+ # restart the moment a litert-lm becomes reachable.
20
+ # :cloud-sense (SENSE_MODE=cloud-sense) forces the cloud VLM. Kept because the
21
+ # portal one-liner and existing installs reference it.
22
+ # TRILLBOARDS_SENSE_MODE overrides either at run time.
23
+ #
24
+ # WHAT CLOUD MODE COSTS. Measured 2026-08-12 over 255,812 image-bearing cloud VLM
25
+ # calls: $0.007008 per analyzed frame, so one camera at the 300s default cadence
26
+ # is $60.55/month and a 16-camera store is $969/month. On-device inference has no
27
+ # per-frame cost. That gap is the entire reason this image defaults to probing.
28
+ #
29
+ # Build: docker build -f packages/edge-sdk/deploy/docker/Dockerfile.cloud-sense \
30
+ # --build-arg SENSE_MODE=auto-sense \
31
+ # -t trillboards/edge-sdk:auto-sense . # context = trillboards-edge-sdk
32
+ # Run: docker run -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX \
33
+ # -v trillboards:/app/data \
34
+ # ghcr.io/trillboards/edge-sdk:auto-sense
35
+
36
+ FROM node:20-slim AS builder
37
+
38
+ WORKDIR /build
39
+
40
+ # Workspace package files first (layer caching).
41
+ COPY package.json package-lock.json* ./
42
+ COPY packages/edge-core/package.json packages/edge-core/
43
+ COPY packages/edge-sensing/package.json packages/edge-sensing/
44
+ COPY packages/edge-federated/package.json packages/edge-federated/
45
+ COPY packages/edge-ads/package.json packages/edge-ads/
46
+ COPY packages/edge-cloud/package.json packages/edge-cloud/
47
+ COPY packages/edge-sdk/package.json packages/edge-sdk/
48
+ COPY packages/edge-platform-linux/package.json packages/edge-platform-linux/
49
+ COPY packages/edge-platform-rtsp/package.json packages/edge-platform-rtsp/
50
+
51
+ RUN npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
52
+
53
+ COPY . .
54
+ RUN npm run build --workspaces --if-present
55
+
56
+ # ─── Production image ─────────────────────────────────────────────────────
57
+ FROM node:20-slim
58
+
59
+ LABEL maintainer="Trillboards <engineering@trillboards.com>"
60
+ ARG SENSE_MODE=auto-sense
61
+ LABEL description="Trillboards Edge AI SDK — BYO-CCTV connector (no bundled models)"
62
+
63
+ # ffmpeg = RTSP frame capture; ca-certificates/curl = TLS + healthcheck.
64
+ RUN apt-get update && apt-get install -y --no-install-recommends \
65
+ ffmpeg \
66
+ ca-certificates \
67
+ curl \
68
+ && rm -rf /var/lib/apt/lists/*
69
+
70
+ WORKDIR /app
71
+
72
+ # Built packages (the cloud path lazy-requires the local ONNX stack, so it is
73
+ # never loaded at runtime — but the workspace deps must resolve).
74
+ COPY --from=builder /build/packages/edge-core/dist packages/edge-core/dist
75
+ COPY --from=builder /build/packages/edge-core/package.json packages/edge-core/
76
+ COPY --from=builder /build/packages/edge-sensing/dist packages/edge-sensing/dist
77
+ COPY --from=builder /build/packages/edge-sensing/package.json packages/edge-sensing/
78
+ COPY --from=builder /build/packages/edge-federated/dist packages/edge-federated/dist
79
+ COPY --from=builder /build/packages/edge-federated/package.json packages/edge-federated/
80
+ COPY --from=builder /build/packages/edge-ads/dist packages/edge-ads/dist
81
+ COPY --from=builder /build/packages/edge-ads/package.json packages/edge-ads/
82
+ COPY --from=builder /build/packages/edge-cloud/dist packages/edge-cloud/dist
83
+ COPY --from=builder /build/packages/edge-cloud/package.json packages/edge-cloud/
84
+ COPY --from=builder /build/packages/edge-sdk/dist packages/edge-sdk/dist
85
+ COPY --from=builder /build/packages/edge-sdk/package.json packages/edge-sdk/
86
+ COPY --from=builder /build/packages/edge-platform-linux/dist packages/edge-platform-linux/dist
87
+ COPY --from=builder /build/packages/edge-platform-linux/package.json packages/edge-platform-linux/
88
+ COPY --from=builder /build/packages/edge-platform-rtsp/dist packages/edge-platform-rtsp/dist
89
+ COPY --from=builder /build/packages/edge-platform-rtsp/package.json packages/edge-platform-rtsp/
90
+
91
+ # Production deps only (NO model download — inference is cloud-side, or on a
92
+ # litert-lm the operator runs themselves and points this at).
93
+ COPY --from=builder /build/package.json /build/package-lock.json* ./
94
+ RUN npm ci --legacy-peer-deps --omit=dev 2>/dev/null || npm install --legacy-peer-deps --omit=dev
95
+
96
+ # Pairing writes the config here; mount /app/data on a volume so the paired
97
+ # config (device token + feeds) survives container restarts.
98
+ RUN mkdir -p /app/data
99
+
100
+ # First-run pair → sense (claim code in, token never baked).
101
+ COPY packages/edge-sdk/deploy/docker/connector-entrypoint.sh /usr/local/bin/connector-entrypoint.sh
102
+ RUN chmod +x /usr/local/bin/connector-entrypoint.sh
103
+
104
+ ENV NODE_ENV=production
105
+ ENV TRILLBOARDS_API_BASE="https://api.trillboards.com"
106
+ ENV TRILLBOARDS_CONFIG="/app/data/trillboards.config.json"
107
+ # Baked per tag by the publish matrix; overridable per run.
108
+ ENV TRILLBOARDS_SENSE_MODE=${SENSE_MODE}
109
+
110
+ # No HEALTHCHECK / status-server port: the sensing CLI paths build
111
+ # CloudSensingManager directly and never start the StatusServer (that wiring is
112
+ # only in EdgeAgent.start), so a /health:9090 probe would never respond and Docker
113
+ # would mark a perfectly-streaming container unhealthy (Codex P2 on #7730).
114
+ # Liveness for this streaming worker is observed server-side via the cloud ingest,
115
+ # not a local HTTP probe.
116
+
117
+ ENTRYPOINT ["/usr/local/bin/connector-entrypoint.sh"]
@@ -1,6 +1,6 @@
1
1
  # Trillboards Edge AI SDK — CPU-only container
2
2
  # Usage: docker build -f Dockerfile.cpu -t trillboards/edge-sdk:latest ../../
3
- # Run: docker run -e DEVICE_TOKEN=xxx trillboards/edge-sdk:latest
3
+ # Run: docker run -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:latest
4
4
 
5
5
  FROM node:20-slim AS builder
6
6
 
@@ -65,26 +65,35 @@ RUN npm ci --legacy-peer-deps --omit=dev 2>/dev/null || npm install --legacy-pee
65
65
  # Create models directory
66
66
  RUN mkdir -p /app/models /app/data
67
67
 
68
- # Download default models at build time
69
- RUN node -e " \
70
- const https = require('https'); \
71
- const fs = require('fs'); \
72
- const models = { \
73
- 'blazeface.onnx': 'https://models.trillboards.com/blazeface_128x128.onnx', \
74
- 'yamnet.onnx': 'https://models.trillboards.com/yamnet_classification.onnx', \
75
- }; \
76
- Object.entries(models).forEach(([name, url]) => { \
77
- console.log('Downloading ' + name + '...'); \
78
- const file = fs.createWriteStream('/app/models/' + name); \
79
- https.get(url, (res) => { \
80
- if (res.statusCode === 301 || res.statusCode === 302) { \
81
- https.get(res.headers.location, (r) => r.pipe(file)); \
82
- } else { \
83
- res.pipe(file); \
84
- } \
85
- }); \
86
- }); \
87
- " 2>/dev/null || echo "Model download skipped (build-time network may be unavailable)"
68
+ # NO build-time model download. `models.trillboards.com` was never provisioned:
69
+ # the trillboards.com zone has no `models` record, so the name is caught by the
70
+ # `*.trillboards.com` wildcard A-alias and answered by the application ALB, which
71
+ # returns the marketing SPA's index.html for every path. (An invented subdomain
72
+ # resolves to the identical IPs — it is the wildcard, not a host.) No CloudFront
73
+ # alias, no S3 bucket, no terraform for it has ever existed; the hostname was
74
+ # invented in the same commit as the downloader (#996, 2026-03-09).
75
+ #
76
+ # The download this replaces verified only `if (size < 1000)`, and the SPA response
77
+ # is 2490 bytes — so it PASSED, printed "All models downloaded successfully", and
78
+ # baked two copies of a web page into the image as blazeface.onnx and yamnet.onnx.
79
+ # Reproduced verbatim on 2026-08-13: both files `HTML document text`, build exit 0.
80
+ #
81
+ # That was invisible only because this image had never been built: `runs-on` named
82
+ # a label no runner carries, so cpu/cuda/openvino queued until GitHub's 24h
83
+ # auto-cancel and ECR holds zero `latest`/`cuda`/`openvino` tags, ever. Fixing the
84
+ # runner label without this change would have published corrupt models under
85
+ # `:latest` — the default tag customers pull — on the very first successful build.
86
+ #
87
+ # Shipping model-free is the documented and verified-supported state: ONNX loads
88
+ # are wrapped, a missing file marks the camera/microphone subsystem ERROR and the
89
+ # agent continues (deploy/docker/README.md). It is also strictly better than the
90
+ # HTML it replaces — for audio, EdgeAgent only wires the classifier when the file
91
+ # EXISTS, so an absent file cleanly skips it while a 2490-byte HTML file loads,
92
+ # throws `Protobuf parsing failed`, and reports ERROR.
93
+ #
94
+ # To add models back, provision a real origin first, then restore a download that
95
+ # validates the ONNX magic bytes — never a size floor, which cannot tell a model
96
+ # from a web page. `trillboards-edge download-models` now refuses non-ONNX bytes.
88
97
 
89
98
  # Environment variables with defaults
90
99
  ENV NODE_ENV=production
@@ -1,6 +1,6 @@
1
1
  # Trillboards Edge AI SDK — NVIDIA CUDA container
2
2
  # Usage: docker build -f Dockerfile.cuda -t trillboards/edge-sdk:cuda ../../
3
- # Run: docker run --gpus all -e DEVICE_TOKEN=xxx trillboards/edge-sdk:cuda
3
+ # Run: docker run --gpus all -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:cuda
4
4
 
5
5
  FROM nvidia/cuda:12.3.2-runtime-ubuntu22.04 AS base
6
6
 
@@ -67,26 +67,35 @@ RUN npm ci --legacy-peer-deps --omit=dev 2>/dev/null || npm install --legacy-pee
67
67
 
68
68
  RUN mkdir -p /app/models /app/data
69
69
 
70
- # Download default models
71
- RUN node -e " \
72
- const https = require('https'); \
73
- const fs = require('fs'); \
74
- const models = { \
75
- 'blazeface.onnx': 'https://models.trillboards.com/blazeface_128x128.onnx', \
76
- 'yamnet.onnx': 'https://models.trillboards.com/yamnet_classification.onnx', \
77
- }; \
78
- Object.entries(models).forEach(([name, url]) => { \
79
- console.log('Downloading ' + name + '...'); \
80
- const file = fs.createWriteStream('/app/models/' + name); \
81
- https.get(url, (res) => { \
82
- if (res.statusCode === 301 || res.statusCode === 302) { \
83
- https.get(res.headers.location, (r) => r.pipe(file)); \
84
- } else { \
85
- res.pipe(file); \
86
- } \
87
- }); \
88
- }); \
89
- " 2>/dev/null || echo "Model download skipped"
70
+ # NO build-time model download. `models.trillboards.com` was never provisioned:
71
+ # the trillboards.com zone has no `models` record, so the name is caught by the
72
+ # `*.trillboards.com` wildcard A-alias and answered by the application ALB, which
73
+ # returns the marketing SPA's index.html for every path. (An invented subdomain
74
+ # resolves to the identical IPs — it is the wildcard, not a host.) No CloudFront
75
+ # alias, no S3 bucket, no terraform for it has ever existed; the hostname was
76
+ # invented in the same commit as the downloader (#996, 2026-03-09).
77
+ #
78
+ # The download this replaces verified only `if (size < 1000)`, and the SPA response
79
+ # is 2490 bytes — so it PASSED, printed "All models downloaded successfully", and
80
+ # baked two copies of a web page into the image as blazeface.onnx and yamnet.onnx.
81
+ # Reproduced verbatim on 2026-08-13: both files `HTML document text`, build exit 0.
82
+ #
83
+ # That was invisible only because this image had never been built: `runs-on` named
84
+ # a label no runner carries, so cpu/cuda/openvino queued until GitHub's 24h
85
+ # auto-cancel and ECR holds zero `latest`/`cuda`/`openvino` tags, ever. Fixing the
86
+ # runner label without this change would have published corrupt models under
87
+ # `:latest` — the default tag customers pull — on the very first successful build.
88
+ #
89
+ # Shipping model-free is the documented and verified-supported state: ONNX loads
90
+ # are wrapped, a missing file marks the camera/microphone subsystem ERROR and the
91
+ # agent continues (deploy/docker/README.md). It is also strictly better than the
92
+ # HTML it replaces — for audio, EdgeAgent only wires the classifier when the file
93
+ # EXISTS, so an absent file cleanly skips it while a 2490-byte HTML file loads,
94
+ # throws `Protobuf parsing failed`, and reports ERROR.
95
+ #
96
+ # To add models back, provision a real origin first, then restore a download that
97
+ # validates the ONNX magic bytes — never a size floor, which cannot tell a model
98
+ # from a web page. `trillboards-edge download-models` now refuses non-ONNX bytes.
90
99
 
91
100
  ENV NODE_ENV=production
92
101
  ENV DEVICE_TOKEN=""
@@ -1,6 +1,6 @@
1
1
  # Trillboards Edge AI SDK — Intel OpenVINO container
2
2
  # Usage: docker build -f Dockerfile.openvino -t trillboards/edge-sdk:openvino ../../
3
- # Run: docker run -e DEVICE_TOKEN=xxx trillboards/edge-sdk:openvino
3
+ # Run: docker run -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:openvino
4
4
  # Optimized for Intel HD 620+ GPUs (Backpackster, NUC, mini-PCs)
5
5
 
6
6
  FROM openvino/ubuntu22_runtime:2024.0.0 AS base
@@ -68,25 +68,35 @@ RUN npm ci --legacy-peer-deps --omit=dev 2>/dev/null || npm install --legacy-pee
68
68
 
69
69
  RUN mkdir -p /app/models /app/data
70
70
 
71
- RUN node -e " \
72
- const https = require('https'); \
73
- const fs = require('fs'); \
74
- const models = { \
75
- 'blazeface.onnx': 'https://models.trillboards.com/blazeface_128x128.onnx', \
76
- 'yamnet.onnx': 'https://models.trillboards.com/yamnet_classification.onnx', \
77
- }; \
78
- Object.entries(models).forEach(([name, url]) => { \
79
- console.log('Downloading ' + name + '...'); \
80
- const file = fs.createWriteStream('/app/models/' + name); \
81
- https.get(url, (res) => { \
82
- if (res.statusCode === 301 || res.statusCode === 302) { \
83
- https.get(res.headers.location, (r) => r.pipe(file)); \
84
- } else { \
85
- res.pipe(file); \
86
- } \
87
- }); \
88
- }); \
89
- " 2>/dev/null || echo "Model download skipped"
71
+ # NO build-time model download. `models.trillboards.com` was never provisioned:
72
+ # the trillboards.com zone has no `models` record, so the name is caught by the
73
+ # `*.trillboards.com` wildcard A-alias and answered by the application ALB, which
74
+ # returns the marketing SPA's index.html for every path. (An invented subdomain
75
+ # resolves to the identical IPs — it is the wildcard, not a host.) No CloudFront
76
+ # alias, no S3 bucket, no terraform for it has ever existed; the hostname was
77
+ # invented in the same commit as the downloader (#996, 2026-03-09).
78
+ #
79
+ # The download this replaces verified only `if (size < 1000)`, and the SPA response
80
+ # is 2490 bytes — so it PASSED, printed "All models downloaded successfully", and
81
+ # baked two copies of a web page into the image as blazeface.onnx and yamnet.onnx.
82
+ # Reproduced verbatim on 2026-08-13: both files `HTML document text`, build exit 0.
83
+ #
84
+ # That was invisible only because this image had never been built: `runs-on` named
85
+ # a label no runner carries, so cpu/cuda/openvino queued until GitHub's 24h
86
+ # auto-cancel and ECR holds zero `latest`/`cuda`/`openvino` tags, ever. Fixing the
87
+ # runner label without this change would have published corrupt models under
88
+ # `:latest` — the default tag customers pull — on the very first successful build.
89
+ #
90
+ # Shipping model-free is the documented and verified-supported state: ONNX loads
91
+ # are wrapped, a missing file marks the camera/microphone subsystem ERROR and the
92
+ # agent continues (deploy/docker/README.md). It is also strictly better than the
93
+ # HTML it replaces — for audio, EdgeAgent only wires the classifier when the file
94
+ # EXISTS, so an absent file cleanly skips it while a 2490-byte HTML file loads,
95
+ # throws `Protobuf parsing failed`, and reports ERROR.
96
+ #
97
+ # To add models back, provision a real origin first, then restore a download that
98
+ # validates the ONNX magic bytes — never a size floor, which cannot tell a model
99
+ # from a web page. `trillboards-edge download-models` now refuses non-ONNX bytes.
90
100
 
91
101
  ENV NODE_ENV=production
92
102
  ENV DEVICE_TOKEN=""
@@ -179,14 +179,19 @@ Two directories should be persisted across container restarts:
179
179
  | `/app/models` | ONNX model files | `blazeface.onnx`, `yamnet.onnx`, and any OTA-updated models |
180
180
  | `/app/data` | Agent persistent state | `signal_buffer.db` (SQLite signal buffer), device identity fingerprint, federated learning gradients |
181
181
 
182
- Default models (`blazeface.onnx` and `yamnet.onnx`) are downloaded at image build time from `https://models.trillboards.com/`. If you mount an empty volume, the agent will operate without models until they are downloaded or placed manually.
182
+ **These images ship with NO bundled ONNX models.** `/app/models` is created empty.
183
183
 
184
- **Pre-populating models on the host:**
184
+ They used to claim a build-time download from `https://models.trillboards.com/`, but that host was never provisioned: `trillboards.com` has no `models` DNS record, so the name is caught by the `*.trillboards.com` wildcard and answered by the application load balancer, which returns the marketing site's `index.html` — HTTP 200, `text/html`, 2490 bytes — for every path. The build's only integrity check was `if (size < 1000)`, which 2490 clears, so the download "succeeded" and wrote a web page into `blazeface.onnx`. That was never noticed because these three images had never been built at all (their `runs-on` named a label no runner carries, so every run queued until GitHub's 24h auto-cancel).
185
+
186
+ Running without these models is a supported state: the ONNX load is wrapped, and a missing model marks the camera/microphone subsystem `ERROR` while the agent keeps running. Face detection and audio classification are unavailable until you supply the files.
187
+
188
+ **Supplying models on the host:**
189
+
190
+ Place `blazeface.onnx` (BlazeFace 128×128) and `yamnet.onnx` (YAMNet classification) in a directory and bind-mount it. `trillboards-edge download-models` and `scripts/download-models.sh` will fetch them once a real origin is configured; both now verify the ONNX magic bytes and refuse — loudly, deleting the file — anything that is not a model, so they can no longer write an HTML error page and report `[OK]`.
185
191
 
186
192
  ```bash
187
193
  mkdir -p /opt/trillboards/models
188
- curl -o /opt/trillboards/models/blazeface.onnx https://models.trillboards.com/blazeface_128x128.onnx
189
- curl -o /opt/trillboards/models/yamnet.onnx https://models.trillboards.com/yamnet_classification.onnx
194
+ # Copy blazeface.onnx and yamnet.onnx into /opt/trillboards/models
190
195
  ```
191
196
 
192
197
  Then mount as a bind volume:
@@ -0,0 +1,122 @@
1
+ #!/bin/sh
2
+ # connector-entrypoint.sh — Docker entrypoint for the BYO-CCTV connector. On first
3
+ # run it pairs with a short-lived claim code (exchanging it for a per-feed device
4
+ # token, which is NEVER baked into the image) and writes the paired config;
5
+ # thereafter it senses. Mount /app/data on a volume so the paired config survives
6
+ # restarts — re-pairing needs a fresh code from the portal.
7
+ #
8
+ # ── The mode is a build arg, and the DEFAULT is auto-sense ──────────────────
9
+ #
10
+ # This entrypoint used to `exec cloud-sense` unconditionally, so the Docker path
11
+ # forced the expensive mode while §A5 of cctv-rushi-activation.md recommended
12
+ # auto-sense and §0 told the reader "the connector runs auto-sense". It did not.
13
+ #
14
+ # auto-sense probes `localSensing.litertLmBaseUrl` at startup and commits to
15
+ # local if a healthy litert-lm is hosting the model. An operator who runs
16
+ # `litert-lm serve` on the host and points the connector at it (--network host,
17
+ # or litertLmBaseUrl: http://host.docker.internal:9379 in the config) gets the
18
+ # on-device path from this SAME image, at no per-frame cloud cost.
19
+ #
20
+ # ── Why this image must also enable cloud FALLBACK ─────────────────────────
21
+ #
22
+ # `auto-sense` treats cloud inference as opt-in: with `allowCloudFallback`
23
+ # unset it probes, fails, and WAITS — uploading nothing — because on a laptop or
24
+ # a mini-PC that is downloading a 2.4 GB model, idling briefly is better than
25
+ # silently starting a meter. That is right for the macOS/Windows installers,
26
+ # which provision a local model.
27
+ #
28
+ # It is wrong here. This image bundles NO model and never downloads one, so a
29
+ # plain `docker run` has nothing to wait FOR: without the fallback the connector
30
+ # would sit idle forever and the operator would see cameras that never report.
31
+ # So the entrypoint asserts `localSensing.allowCloudFallback: true` into the
32
+ # paired config for auto-sense. Net behaviour for a plain run is identical to
33
+ # the old `:cloud-sense` — frames go to the cloud VLM — except the connector now
34
+ # AUTO-UPGRADES to on-device with no restart the moment a litert-lm becomes
35
+ # reachable.
36
+ #
37
+ # TRILLBOARDS_SENSE_MODE overrides at run time: cloud-sense forces the cloud VLM,
38
+ # local-sense refuses to fall back.
39
+ set -eu
40
+
41
+ CONFIG="${TRILLBOARDS_CONFIG:-/app/data/trillboards.config.json}"
42
+ API_BASE="${TRILLBOARDS_API_BASE:-https://api.trillboards.com}"
43
+ CLI="node packages/edge-sdk/dist/cli.js"
44
+ # Baked per image variant (:auto-sense -> auto-sense, :cloud-sense -> cloud-sense),
45
+ # overridable per run.
46
+ MODE="${TRILLBOARDS_SENSE_MODE:-auto-sense}"
47
+
48
+ case "$MODE" in
49
+ auto-sense|cloud-sense|local-sense) ;;
50
+ *)
51
+ echo "[connector] Unknown TRILLBOARDS_SENSE_MODE '$MODE' (want auto-sense|cloud-sense|local-sense)." >&2
52
+ exit 1
53
+ ;;
54
+ esac
55
+
56
+ if [ ! -f "$CONFIG" ]; then
57
+ if [ -z "${TRILLBOARDS_CLAIM_CODE:-}" ]; then
58
+ echo "[connector] No paired config and no TRILLBOARDS_CLAIM_CODE." >&2
59
+ echo "[connector] Get a code from the Trillboards portal (Add a camera -> Install), then:" >&2
60
+ echo "[connector] docker run -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX -v trillboards:/app/data ghcr.io/trillboards/edge-sdk:auto-sense" >&2
61
+ exit 1
62
+ fi
63
+ echo "[connector] Pairing with claim code (token delivered at pair, never baked)..."
64
+ # shellcheck disable=SC2086
65
+ $CLI pair --code "$TRILLBOARDS_CLAIM_CODE" --config "$CONFIG" --api-base "$API_BASE"
66
+ fi
67
+
68
+ if [ "$MODE" = "auto-sense" ]; then
69
+ # This image ships no local model, so auto-sense MUST be allowed to fall back
70
+ # or it waits forever for something that will never arrive. Idempotent, and it
71
+ # never touches a value the operator set themselves.
72
+ #
73
+ # The assert WRITES, and a config file is very often mounted read-only
74
+ # (-v ./trillboards.config.json:/app/data/trillboards.config.json:ro)
75
+ # which is a perfectly reasonable way to run this image and skips pairing
76
+ # entirely. Under `set -e` an unguarded writeFileSync there killed the
77
+ # container on every single start, with a raw stack trace, because a DEFAULT
78
+ # could not be persisted. So a failed write degrades to exactly the behaviour
79
+ # the write was there to guarantee — cloud inference this run — and says so.
80
+ set +e
81
+ node -e '
82
+ const fs = require("fs");
83
+ const p = process.argv[1];
84
+ let c;
85
+ try { c = JSON.parse(fs.readFileSync(p, "utf-8")); } catch (e) {
86
+ console.error("[connector] cannot read config for fallback assert: " + e.message);
87
+ process.exit(0);
88
+ }
89
+ c.localSensing = c.localSensing || {};
90
+ if (c.localSensing.allowCloudFallback === true) process.exit(0);
91
+ if (c.localSensing.allowCloudFallback === false) {
92
+ console.error("[connector] localSensing.allowCloudFallback is explicitly false — honoring it; this connector will WAIT for a local runtime and upload nothing until one is reachable.");
93
+ process.exit(0);
94
+ }
95
+ c.localSensing.allowCloudFallback = true;
96
+ try {
97
+ fs.writeFileSync(p, JSON.stringify(c, null, 2));
98
+ } catch (e) {
99
+ console.error("[connector] cannot persist localSensing.allowCloudFallback to " + p + ": " + e.message);
100
+ process.exit(3);
101
+ }
102
+ console.error("[connector] enabled localSensing.allowCloudFallback (this image bundles no local model).");
103
+ ' "$CONFIG"
104
+ FALLBACK_ASSERT_RC=$?
105
+ set -e
106
+ if [ "$FALLBACK_ASSERT_RC" = "3" ]; then
107
+ echo "[connector] config is not writable, so auto-sense cannot record its cloud-fallback policy." >&2
108
+ echo "[connector] Running cloud-sense for this process instead — cameras still report. To get the" >&2
109
+ echo "[connector] on-device auto-upgrade, mount the config writable or set localSensing.allowCloudFallback." >&2
110
+ MODE=cloud-sense
111
+ fi
112
+ fi
113
+
114
+ if [ "$MODE" = "auto-sense" ]; then
115
+ echo "[connector] Starting $MODE (API $API_BASE) — probing for on-device inference," >&2
116
+ echo "[connector] falling back to the cloud VLM if none is reachable, and" >&2
117
+ echo "[connector] auto-upgrading to on-device (no restart) if one appears." >&2
118
+ else
119
+ echo "[connector] Starting explicitly selected $MODE (API $API_BASE)..." >&2
120
+ fi
121
+ # shellcheck disable=SC2086
122
+ exec $CLI "$MODE" --config "$CONFIG"
@@ -450,7 +450,7 @@ kubectl label node <node-name> trillboards.com/edge-device=true
450
450
  ### 4. Customizing the kubectl Manifest
451
451
 
452
452
  Edit `daemonset.yaml` directly to change:
453
- - Image tag (line: `image: trillboards/edge-sdk:latest`)
453
+ - Image tag (line: `image: ghcr.io/trillboards/edge-sdk:latest`)
454
454
  - Environment variable values
455
455
  - Resource requests and limits
456
456
  - Node selector labels
@@ -37,7 +37,7 @@ spec:
37
37
  terminationGracePeriodSeconds: 30
38
38
  containers:
39
39
  - name: edge-agent
40
- image: trillboards/edge-sdk:latest
40
+ image: ghcr.io/trillboards/edge-sdk:latest
41
41
  imagePullPolicy: Always
42
42
  ports:
43
43
  - name: status