@trillboards/edge-sdk 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -5
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-service.ps1 +6 -1
  30. package/deploy/windows/install-update.ps1 +101 -0
  31. package/deploy/windows/installer.nsi +138 -0
  32. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  33. package/deploy/windows/register-task.ps1 +44 -0
  34. package/deploy/windows/run-connector.ps1 +69 -0
  35. package/deploy/windows/setup-litertlm.ps1 +361 -0
  36. package/deploy/windows/stop-existing.ps1 +72 -0
  37. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  38. package/deploy/windows/trillboards-edge.cmd +33 -0
  39. package/deploy/windows/trillboards-pair.cmd +276 -0
  40. package/deploy/windows/trillboards-setup.ps1 +456 -0
  41. package/deploy/windows/trillboards-setup.vbs +13 -0
  42. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  43. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  44. package/dist/AudienceAnalyzeClient.js +246 -0
  45. package/dist/AudienceAnalyzeClient.js.map +1 -0
  46. package/dist/ChannelProber.d.ts +150 -0
  47. package/dist/ChannelProber.d.ts.map +1 -0
  48. package/dist/ChannelProber.js +887 -0
  49. package/dist/ChannelProber.js.map +1 -0
  50. package/dist/CloudEscalationPolicy.d.ts +143 -0
  51. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  52. package/dist/CloudEscalationPolicy.js +137 -0
  53. package/dist/CloudEscalationPolicy.js.map +1 -0
  54. package/dist/CloudSensingManager.d.ts +557 -0
  55. package/dist/CloudSensingManager.d.ts.map +1 -0
  56. package/dist/CloudSensingManager.js +1267 -0
  57. package/dist/CloudSensingManager.js.map +1 -0
  58. package/dist/DiscoveryReportClient.d.ts +66 -0
  59. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  60. package/dist/DiscoveryReportClient.js +85 -0
  61. package/dist/DiscoveryReportClient.js.map +1 -0
  62. package/dist/EdgeAgent.d.ts +12 -2
  63. package/dist/EdgeAgent.d.ts.map +1 -1
  64. package/dist/EdgeAgent.js +55 -13
  65. package/dist/EdgeAgent.js.map +1 -1
  66. package/dist/LiteRtVlmProvider.d.ts +193 -0
  67. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  68. package/dist/LiteRtVlmProvider.js +436 -0
  69. package/dist/LiteRtVlmProvider.js.map +1 -0
  70. package/dist/MosaicSource.d.ts +108 -0
  71. package/dist/MosaicSource.d.ts.map +1 -0
  72. package/dist/MosaicSource.js +274 -0
  73. package/dist/MosaicSource.js.map +1 -0
  74. package/dist/OnvifClient.d.ts +83 -0
  75. package/dist/OnvifClient.d.ts.map +1 -0
  76. package/dist/OnvifClient.js +209 -0
  77. package/dist/OnvifClient.js.map +1 -0
  78. package/dist/PairClient.d.ts +106 -0
  79. package/dist/PairClient.d.ts.map +1 -0
  80. package/dist/PairClient.js +186 -0
  81. package/dist/PairClient.js.map +1 -0
  82. package/dist/RecorderAddressRecovery.d.ts +53 -0
  83. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  84. package/dist/RecorderAddressRecovery.js +359 -0
  85. package/dist/RecorderAddressRecovery.js.map +1 -0
  86. package/dist/SceneChangeDetector.d.ts +291 -0
  87. package/dist/SceneChangeDetector.d.ts.map +1 -0
  88. package/dist/SceneChangeDetector.js +682 -0
  89. package/dist/SceneChangeDetector.js.map +1 -0
  90. package/dist/SensingProfileClient.d.ts +59 -0
  91. package/dist/SensingProfileClient.d.ts.map +1 -0
  92. package/dist/SensingProfileClient.js +78 -0
  93. package/dist/SensingProfileClient.js.map +1 -0
  94. package/dist/SetupSessionClient.d.ts +114 -0
  95. package/dist/SetupSessionClient.d.ts.map +1 -0
  96. package/dist/SetupSessionClient.js +95 -0
  97. package/dist/SetupSessionClient.js.map +1 -0
  98. package/dist/VlmAdmissionGate.d.ts +126 -0
  99. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  100. package/dist/VlmAdmissionGate.js +166 -0
  101. package/dist/VlmAdmissionGate.js.map +1 -0
  102. package/dist/autoSense.d.ts +266 -0
  103. package/dist/autoSense.d.ts.map +1 -0
  104. package/dist/autoSense.js +606 -0
  105. package/dist/autoSense.js.map +1 -0
  106. package/dist/cli.d.ts +27 -1
  107. package/dist/cli.d.ts.map +1 -1
  108. package/dist/cli.js +526 -27
  109. package/dist/cli.js.map +1 -1
  110. package/dist/combinedSense.d.ts +128 -0
  111. package/dist/combinedSense.d.ts.map +1 -0
  112. package/dist/combinedSense.js +261 -0
  113. package/dist/combinedSense.js.map +1 -0
  114. package/dist/config.d.ts +351 -0
  115. package/dist/config.d.ts.map +1 -1
  116. package/dist/config.js +99 -2
  117. package/dist/config.js.map +1 -1
  118. package/dist/connectorCadence.d.ts +135 -0
  119. package/dist/connectorCadence.d.ts.map +1 -0
  120. package/dist/connectorCadence.js +151 -0
  121. package/dist/connectorCadence.js.map +1 -0
  122. package/dist/dataDir.d.ts +33 -0
  123. package/dist/dataDir.d.ts.map +1 -0
  124. package/dist/dataDir.js +76 -0
  125. package/dist/dataDir.js.map +1 -0
  126. package/dist/deployedCadence.d.ts +167 -0
  127. package/dist/deployedCadence.d.ts.map +1 -0
  128. package/dist/deployedCadence.js +209 -0
  129. package/dist/deployedCadence.js.map +1 -0
  130. package/dist/deployedCadenceSource.d.ts +48 -0
  131. package/dist/deployedCadenceSource.d.ts.map +1 -0
  132. package/dist/deployedCadenceSource.js +74 -0
  133. package/dist/deployedCadenceSource.js.map +1 -0
  134. package/dist/diagnose.d.ts +1 -1
  135. package/dist/diagnose.d.ts.map +1 -1
  136. package/dist/diagnose.js +15 -13
  137. package/dist/diagnose.js.map +1 -1
  138. package/dist/discovery.d.ts +131 -0
  139. package/dist/discovery.d.ts.map +1 -0
  140. package/dist/discovery.js +499 -0
  141. package/dist/discovery.js.map +1 -0
  142. package/dist/gemmaModelOta.d.ts +118 -0
  143. package/dist/gemmaModelOta.d.ts.map +1 -0
  144. package/dist/gemmaModelOta.js +291 -0
  145. package/dist/gemmaModelOta.js.map +1 -0
  146. package/dist/index.d.ts +16 -1
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +40 -1
  149. package/dist/index.js.map +1 -1
  150. package/dist/init.d.ts.map +1 -1
  151. package/dist/init.js +25 -87
  152. package/dist/init.js.map +1 -1
  153. package/dist/keepAwake.d.ts +33 -0
  154. package/dist/keepAwake.d.ts.map +1 -0
  155. package/dist/keepAwake.js +54 -0
  156. package/dist/keepAwake.js.map +1 -0
  157. package/dist/legacyRecorderProfile.d.ts +20 -0
  158. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  159. package/dist/legacyRecorderProfile.js +27 -0
  160. package/dist/legacyRecorderProfile.js.map +1 -0
  161. package/dist/localInferenceSchema.d.ts +132 -0
  162. package/dist/localInferenceSchema.d.ts.map +1 -0
  163. package/dist/localInferenceSchema.js +274 -0
  164. package/dist/localInferenceSchema.js.map +1 -0
  165. package/dist/localSenseE2eHarness.d.ts +101 -0
  166. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  167. package/dist/localSenseE2eHarness.js +279 -0
  168. package/dist/localSenseE2eHarness.js.map +1 -0
  169. package/dist/localSensing.d.ts +284 -0
  170. package/dist/localSensing.d.ts.map +1 -0
  171. package/dist/localSensing.js +512 -0
  172. package/dist/localSensing.js.map +1 -0
  173. package/dist/mcp-server.d.ts.map +1 -1
  174. package/dist/mcp-server.js +3 -1
  175. package/dist/mcp-server.js.map +1 -1
  176. package/dist/modelIntegrity.d.ts +80 -0
  177. package/dist/modelIntegrity.d.ts.map +1 -0
  178. package/dist/modelIntegrity.js +194 -0
  179. package/dist/modelIntegrity.js.map +1 -0
  180. package/dist/pairVerify.d.ts +100 -0
  181. package/dist/pairVerify.d.ts.map +1 -0
  182. package/dist/pairVerify.js +296 -0
  183. package/dist/pairVerify.js.map +1 -0
  184. package/dist/qrScanner.d.ts +33 -0
  185. package/dist/qrScanner.d.ts.map +1 -0
  186. package/dist/qrScanner.js +103 -0
  187. package/dist/qrScanner.js.map +1 -0
  188. package/dist/runtimePaths.d.ts +10 -0
  189. package/dist/runtimePaths.d.ts.map +1 -0
  190. package/dist/runtimePaths.js +79 -0
  191. package/dist/runtimePaths.js.map +1 -0
  192. package/dist/sensingRunMode.d.ts +86 -0
  193. package/dist/sensingRunMode.d.ts.map +1 -0
  194. package/dist/sensingRunMode.js +346 -0
  195. package/dist/sensingRunMode.js.map +1 -0
  196. package/dist/setupMode.d.ts +98 -0
  197. package/dist/setupMode.d.ts.map +1 -0
  198. package/dist/setupMode.js +828 -0
  199. package/dist/setupMode.js.map +1 -0
  200. package/dist/setupProgress.d.ts +31 -0
  201. package/dist/setupProgress.d.ts.map +1 -0
  202. package/dist/setupProgress.js +173 -0
  203. package/dist/setupProgress.js.map +1 -0
  204. package/package.json +13 -4
@@ -0,0 +1,225 @@
1
+ <#
2
+ .SYNOPSIS
3
+ build-windows.ps1 — build a self-contained Windows installer (.exe) for the
4
+ Trillboards on-device (local-sense) CCTV connector.
5
+
6
+ .DESCRIPTION
7
+ Windows analogue of deploy/macos/build-macos-pkg.sh. Lays down a fully
8
+ self-contained tree the operator can run with NO system Node, ffmpeg, Docker,
9
+ or terminal:
10
+
11
+ <install>\Trillboards\edge\
12
+ bin\node.exe official Node x64 runtime (pinned)
13
+ bin\ffmpeg.exe pinned static ffmpeg (eugeneware/ffmpeg-static b6.0,
14
+ the SAME upstream the macOS build uses; SHA-256 verified)
15
+ bin\trillboards-edge.cmd launcher: PATH=bin first; node lib\...\cli.js
16
+ bin\trillboards-pair.cmd first-run claim-code pairing helper
17
+ lib\edge-sdk\dist\cli.js the connector CLI
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.
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/
24
+ better-sqlite3 — keeping the inner .exe small. The release workflow signs this
25
+ connector installer, then bundle-windows-installer.ps1 wraps it with the
26
+ pinned Gemma model in one customer-facing executable. The inner installer
27
+ requires that model and verifies local AI before camera pairing.
28
+ RTSP capture is via the bundled ffmpeg (cross-platform), so a single x64 build
29
+ serves every Win10/11.
30
+
31
+ This script produces the unsigned build artifact. The production release
32
+ workflow applies Azure Trusted Signing Authenticode and verifies the signature
33
+ before publishing the installer to the CDN.
34
+
35
+ .PARAMETER NodeVersion Pinned Node runtime (default v22.22.0, matches macOS).
36
+ .PARAMETER Offline Skip downloads; copy the host node/ffmpeg (air-gapped CI).
37
+ .PARAMETER SkipNsis Stage only; do not compile the NSIS .exe (for unit checks).
38
+
39
+ .OUTPUTS
40
+ Prints the inner installer path on the last line:
41
+ <pkgroot>\deploy\windows\dist-installer\inner\TrillboardsEdgeSetup-core-<ver>-x64.exe
42
+ #>
43
+ [CmdletBinding()]
44
+ param(
45
+ [string]$NodeVersion = "v22.22.0",
46
+ [switch]$Offline,
47
+ [switch]$SkipNsis
48
+ )
49
+
50
+ $ErrorActionPreference = "Stop"
51
+ Set-StrictMode -Version Latest
52
+
53
+ # ─── Pinned static ffmpeg (win32-x64) — eugeneware/ffmpeg-static b6.0 ──────────
54
+ # Same upstream + release the macOS build pins, so behavior matches. SHA-256
55
+ # verified after download; refuse to bundle an unverified binary.
56
+ $FfmpegStaticTag = if ($env:FFMPEG_STATIC_TAG) { $env:FFMPEG_STATIC_TAG } else { "b6.0" }
57
+ $FfmpegSha256Win = if ($env:FFMPEG_SHA256_WIN) { $env:FFMPEG_SHA256_WIN } else { "e9fd5e711debab9d680955fc1e38a2c1160fd280b144476cc3f62bc43ef49db1" }
58
+
59
+ # ─── Locate ourselves + the edge-sdk package root ─────────────────────────────
60
+ $Here = Split-Path -Parent $MyInvocation.MyCommand.Path # deploy\windows
61
+ $PkgRoot = (Resolve-Path "$Here\..\..").Path # …\packages\edge-sdk
62
+ $WorkspaceRoot = (Resolve-Path "$PkgRoot\..\..").Path # …\trillboards-edge-sdk
63
+
64
+ $InstallSubdir = "Trillboards\edge"
65
+ $DistDir = Join-Path $PkgRoot "deploy\windows\dist-installer"
66
+ $Stage = Join-Path $DistDir "stage-x64"
67
+ $Payload = Join-Path $Stage $InstallSubdir
68
+ $Scratch = Join-Path $DistDir "scratch-x64"
69
+
70
+ function Log ($m) { Write-Host "[build] $m" -ForegroundColor Blue }
71
+ function Warn ($m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
72
+ function Die ($m) { Write-Host "[err] $m" -ForegroundColor Red; exit 1 }
73
+
74
+ $Version = (node -e "process.stdout.write(require('$($PkgRoot -replace '\\','/')/package.json').version)")
75
+ Log "edge-sdk v$Version arch=x64 node=$NodeVersion"
76
+
77
+ # ─── Clean + recreate the staging tree (idempotent) ───────────────────────────
78
+ if (Test-Path $Stage) { Remove-Item -Recurse -Force $Stage }
79
+ if (Test-Path $Scratch) { Remove-Item -Recurse -Force $Scratch }
80
+ New-Item -ItemType Directory -Force -Path "$Payload\bin","$Payload\lib","$Scratch" | Out-Null
81
+
82
+ # ─── 1. Build ALL edge workspaces (tsc, dependency order) ──────────────────────
83
+ # edge-sdk's tsc imports edge-core, edge-platform-rtsp/linux/windows, edge-sensing,
84
+ # edge-ads, edge-federated AND edge-cloud — every workspace's dist must exist before
85
+ # edge-sdk compiles. Building only edge-core + edge-platform-rtsp first (and the rest
86
+ # AFTER edge-sdk) made edge-sdk's tsc fail on missing dep types in a clean CI checkout.
87
+ # The root `build` = `npm run build --workspaces` compiles every workspace in
88
+ # dependency order. (No `*> $null`: a future build error must surface in the CI log.)
89
+ Log "Building all edge workspaces (tsc, dependency order)…"
90
+ Push-Location $WorkspaceRoot; npm run build; if ($LASTEXITCODE -ne 0) { Die "edge workspace build failed (tsc)" }; Pop-Location
91
+
92
+ # ─── 2. Lay down the edge-sdk dist ─────────────────────────────────────────────
93
+ Log "Staging edge-sdk dist…"
94
+ New-Item -ItemType Directory -Force -Path "$Payload\lib\edge-sdk" | Out-Null
95
+ Copy-Item -Recurse -Force "$PkgRoot\dist" "$Payload\lib\edge-sdk\dist"
96
+ Copy-Item -Force "$PkgRoot\package.json" "$Payload\lib\edge-sdk\package.json"
97
+
98
+ # ─── 3. Runtime node_modules subset (NO heavy native stack) ────────────────────
99
+ # robocopy /E mirrors the tree; /XD prunes the heavy/native/dev packages cloud-
100
+ # sense + pair never require (EdgeAgent → onnxruntime-node is lazy). Keeps the
101
+ # installer small. robocopy exit codes 0-7 are success; >=8 is a real error.
102
+ #
103
+ # /XJ is REQUIRED, not an optimisation: this is an npm-workspaces install, so
104
+ # node_modules\@trillboards\* are directory JUNCTIONS back into ..\packages\*
105
+ # (re-materialised from their built dist in step 3b just below). Without /XJ,
106
+ # robocopy /E FOLLOWS those junctions into the package trees — which contain
107
+ # their own nested workspace junctions — recursing through a huge/cyclic graph
108
+ # that never terminates. That hung this step for >88 min on windows-latest and
109
+ # the job was killed at its timeout on EVERY release run, so the .exe never
110
+ # published. /MT parallelises the small-file copy; /R:1 /W:1 bound retries so a
111
+ # single transiently-locked file can't trigger robocopy's default 1e6×30s hang.
112
+ Log "Staging runtime node_modules (excluding ONNX/sharp/sqlite/dev)…"
113
+ New-Item -ItemType Directory -Force -Path "$Payload\lib\node_modules" | Out-Null
114
+ $xd = @("onnxruntime-node","sharp","@img","better-sqlite3","ffmpeg-static",".cache",".bin","typescript","jest","jest-cli","@jest","ts-jest","@types")
115
+ $rcArgs = @("$WorkspaceRoot\node_modules", "$Payload\lib\node_modules", "/E", "/XJ", "/MT:16", "/R:1", "/W:1", "/NFL","/NDL","/NJH","/NJS","/NP")
116
+ foreach ($d in $xd) { $rcArgs += @("/XD", $d) }
117
+ robocopy @rcArgs | Out-Null
118
+ if ($LASTEXITCODE -ge 8) { Die "robocopy node_modules failed (code $LASTEXITCODE)" }
119
+ $global:LASTEXITCODE = 0
120
+
121
+ # Overlay the @trillboards workspace packages with their REAL built dist (in the
122
+ # workspace they are symlinks → copy materialized dist + package.json instead).
123
+ foreach ($p in @("edge-core","edge-sensing","edge-ads","edge-federated","edge-cloud","edge-platform-rtsp","edge-platform-linux","edge-platform-windows","edge-csi","edge-sdk")) {
124
+ $src = "$WorkspaceRoot\packages\$p"
125
+ if (-not (Test-Path $src)) { continue }
126
+ $dest = "$Payload\lib\node_modules\@trillboards\$p"
127
+ if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
128
+ New-Item -ItemType Directory -Force -Path $dest | Out-Null
129
+ if (Test-Path "$src\dist") { Copy-Item -Recurse -Force "$src\dist" "$dest\dist" }
130
+ Copy-Item -Force "$src\package.json" "$dest\package.json"
131
+ }
132
+
133
+ # ─── 4. Bundle the Node runtime (official Windows x64) ─────────────────────────
134
+ $NodeBin = "$Payload\bin\node.exe"
135
+ if ($Offline) {
136
+ Log "Offline mode: copying host node…"
137
+ Copy-Item -Force (Get-Command node).Source $NodeBin
138
+ } else {
139
+ $zip = "node-$NodeVersion-win-x64.zip"
140
+ $url = "https://nodejs.org/dist/$NodeVersion/$zip"
141
+ Log "Downloading official Node runtime: $url"
142
+ Invoke-WebRequest -Uri $url -OutFile "$Scratch\$zip" -UseBasicParsing
143
+ Expand-Archive -Path "$Scratch\$zip" -DestinationPath $Scratch -Force
144
+ Copy-Item -Force "$Scratch\node-$NodeVersion-win-x64\node.exe" $NodeBin
145
+ }
146
+ & $NodeBin --version *> $null; if ($LASTEXITCODE -ne 0) { Die "bundled node failed --version" }
147
+ Log "Bundled node: $(& $NodeBin --version)"
148
+
149
+ # ─── 5. Bundle ffmpeg (pinned static win32-x64 — SHA-256 verified) ─────────────
150
+ $FfmpegBin = "$Payload\bin\ffmpeg.exe"
151
+ if ($Offline) {
152
+ $hostFf = (Get-Command ffmpeg -ErrorAction SilentlyContinue).Source
153
+ if (-not $hostFf) { Die "offline mode: no host ffmpeg on PATH to copy." }
154
+ Copy-Item -Force $hostFf $FfmpegBin
155
+ } else {
156
+ $url = "https://github.com/eugeneware/ffmpeg-static/releases/download/$FfmpegStaticTag/ffmpeg-win32-x64"
157
+ Log "Downloading pinned static ffmpeg: $url"
158
+ Invoke-WebRequest -Uri $url -OutFile "$Scratch\ffmpeg.exe" -UseBasicParsing
159
+ $actual = (Get-FileHash -Algorithm SHA256 "$Scratch\ffmpeg.exe").Hash.ToLower()
160
+ if ($actual -ne $FfmpegSha256Win.ToLower()) {
161
+ Die "ffmpeg SHA-256 mismatch (expected $FfmpegSha256Win, got $actual). Refusing to bundle an unverified binary."
162
+ }
163
+ Log "Verified ffmpeg SHA-256 ($FfmpegSha256Win)"
164
+ Copy-Item -Force "$Scratch\ffmpeg.exe" $FfmpegBin
165
+ }
166
+ & $FfmpegBin -version *> $null; if ($LASTEXITCODE -ne 0) { Die "bundled ffmpeg failed -version" }
167
+ Log "Bundled ffmpeg OK"
168
+
169
+ # ─── 6. Launcher + pair helper (bundled runtime — no system deps) ──────────────
170
+ # PATH=bin first so the connector's default ffmpeg='ffmpeg' resolves to OURS.
171
+ Copy-Item -Force "$Here\trillboards-edge.cmd" "$Payload\bin\trillboards-edge.cmd"
172
+ Copy-Item -Force "$Here\trillboards-pair.cmd" "$Payload\bin\trillboards-pair.cmd"
173
+ Copy-Item -Force "$Here\trillboards-setup.ps1" "$Payload\bin\trillboards-setup.ps1"
174
+ Copy-Item -Force "$Here\trillboards-setup.vbs" "$Payload\bin\trillboards-setup.vbs"
175
+ Copy-Item -Force "$Here\trillboards-edge-task.vbs" "$Payload\bin\trillboards-edge-task.vbs"
176
+ Copy-Item -Force "$Here\run-connector.ps1" "$Payload\bin\run-connector.ps1"
177
+ Copy-Item -Force "$Here\register-task.ps1" "$Payload\bin\register-task.ps1"
178
+ Copy-Item -Force "$Here\stop-existing.ps1" "$Payload\bin\stop-existing.ps1"
179
+ Copy-Item -Force "$Here\install-update.ps1" "$Payload\bin\install-update.ps1"
180
+
181
+ # Construct the real customer window with Windows PowerShell before packaging.
182
+ # -SmokeTest creates and validates every control but never displays the form or
183
+ # starts pairing, so this runs safely on CI and catches runtime-only Forms bugs.
184
+ $WindowsPowerShell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
185
+ & $WindowsPowerShell -NoProfile -ExecutionPolicy Bypass -File "$Here\trillboards-setup.ps1" -SmokeTest
186
+ if ($LASTEXITCODE -ne 0) { Die "customer setup window smoke test failed" }
187
+ Log "Customer setup window smoke test passed"
188
+
189
+ # On-device LiteRT-LM / Gemma provisioning. The inner installer receives the
190
+ # model path from the customer SFX and verifies local AI before pairing; the
191
+ # serve task keeps `litert-lm serve :9379` alive for local-sense.
192
+ Copy-Item -Force "$Here\setup-litertlm.ps1" "$Payload\bin\setup-litertlm.ps1"
193
+ Copy-Item -Force "$Here\register-litertlm-task.ps1" "$Payload\bin\register-litertlm-task.ps1"
194
+ Copy-Item -Force "$Here\check-feeds.js" "$Payload\bin\check-feeds.js"
195
+
196
+ # ─── 7. Smoke-test the staged bundle BEFORE packaging ──────────────────────────
197
+ Log "Smoke-testing the staged launcher (version)…"
198
+ $env:PATH = "$Payload\bin;$env:PATH"
199
+ $env:NODE_PATH = "$Payload\lib\node_modules"
200
+ $ver = & $NodeBin "$Payload\lib\edge-sdk\dist\cli.js" version 2>&1 | Out-String
201
+ if ($ver -notmatch "edge-sdk v") { Die "staged launcher failed: $ver" }
202
+ Log "Staged launcher OK: $($ver.Trim())"
203
+
204
+ # ─── 8. Compile the NSIS installer (.exe) ──────────────────────────────────────
205
+ if ($SkipNsis) { Log "SkipNsis set — staged tree only at $Stage"; Write-Output $Stage; exit 0 }
206
+
207
+ $makensis = (Get-Command makensis -ErrorAction SilentlyContinue).Source
208
+ if (-not $makensis) { Die "makensis not found on PATH (install NSIS: choco install nsis -y)" }
209
+ $InnerDir = Join-Path $DistDir "inner"
210
+ New-Item -ItemType Directory -Force -Path $InnerDir | Out-Null
211
+ $out = Join-Path $InnerDir "TrillboardsEdgeSetup-core-$Version-x64.exe"
212
+ Log "Compiling inner NSIS connector installer…"
213
+ & $makensis "/DPAYLOAD_DIR=$Payload" "/DAPP_VERSION=$Version" "/DOUT_FILE=$out" "$Here\installer.nsi"
214
+ if ($LASTEXITCODE -ne 0) { Die "makensis failed (code $LASTEXITCODE)" }
215
+ if (-not (Test-Path $out)) { Die "installer .exe not produced at $out" }
216
+ $sizeMb = [math]::Round((Get-Item $out).Length / 1MB, 1)
217
+ Log "Built: $out ($sizeMb MB)"
218
+ # Emit the installer path on a RELIABLE channel for CI. The historical caller
219
+ # (`$out = ./build-windows.ps1 | Select-Object -Last 1`) was fragile: makensis,
220
+ # npm and Invoke-WebRequest leak objects onto the success stream, and a `Die`
221
+ # aborts before any path is written — so the caller silently captured $null and
222
+ # failed downstream on `Test-Path $null` with a misleading error. GITHUB_OUTPUT
223
+ # is unambiguous and survives stream pollution.
224
+ if ($env:GITHUB_OUTPUT) { "exe=$out" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append }
225
+ Write-Output $out
@@ -0,0 +1,153 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Wrap the signed NSIS connector installer and pinned Gemma model in one
4
+ customer-facing self-extracting executable.
5
+
6
+ .DESCRIPTION
7
+ NSIS cannot emit an installer larger than roughly 2 GB, while the local AI
8
+ model is about 2.4 GB. This script uses the official 7-Zip installer SFX
9
+ module to produce one executable that extracts both files and launches the
10
+ inner installer with /MODEL_SOURCE. The release workflow signs the inner
11
+ installer before this step and signs the final SFX afterward.
12
+ #>
13
+ [CmdletBinding()]
14
+ param(
15
+ [Parameter(Mandatory = $true)][string]$InnerInstaller,
16
+ [string]$OutputPath = "",
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",
20
+ [string]$SevenZipSdkUrl = "https://www.7-zip.org/a/lzma2301.7z",
21
+ [string]$SevenZipSdkSha256 = "317dd834d6bbfd95433488b832e823cd3d4d420101436422c03af88507dd1370"
22
+ )
23
+
24
+ $ErrorActionPreference = "Stop"
25
+ $Here = Split-Path -Parent $MyInvocation.MyCommand.Path
26
+ $DistDir = Join-Path $Here "dist-installer"
27
+ $Scratch = Join-Path $DistDir "bundle-scratch"
28
+ $Stage = Join-Path $Scratch "payload"
29
+ $ModelId = "gemma-4-E2B-it.litertlm"
30
+
31
+ function Log([string]$Message) { Write-Host "[bundle] $Message" -ForegroundColor Blue }
32
+ function Fail([string]$Message) { throw "[bundle] $Message" }
33
+
34
+ $InnerInstaller = (Resolve-Path -LiteralPath $InnerInstaller).Path
35
+ if (-not (Test-Path -LiteralPath $InnerInstaller -PathType Leaf)) {
36
+ Fail "inner installer not found: $InnerInstaller"
37
+ }
38
+
39
+ if ([string]::IsNullOrWhiteSpace($OutputPath)) {
40
+ $version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($InnerInstaller).ProductVersion
41
+ if ([string]::IsNullOrWhiteSpace($version)) { $version = "unknown" }
42
+ $OutputPath = Join-Path (Join-Path $DistDir "customer") "TrillboardsEdgeSetup-$version-x64.exe"
43
+ }
44
+ $OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
45
+
46
+ if (Test-Path -LiteralPath $Scratch) { Remove-Item -LiteralPath $Scratch -Recurse -Force }
47
+ New-Item -ItemType Directory -Force -Path $Stage, (Split-Path -Parent $OutputPath) | Out-Null
48
+
49
+ $sevenZip = Get-Command 7z.exe -ErrorAction SilentlyContinue
50
+ if (-not $sevenZip) { $sevenZip = Get-Command 7z -ErrorAction SilentlyContinue }
51
+ if (-not $sevenZip) { Fail "7z.exe is required to build the customer installer" }
52
+
53
+ $stagedModelPath = Join-Path $Stage $ModelId
54
+ if ([string]::IsNullOrWhiteSpace($ModelPath)) {
55
+ Log "downloading pinned local AI model"
56
+ & curl.exe -fL --retry 3 --output $stagedModelPath $ModelUrl
57
+ if ($LASTEXITCODE -ne 0) { Fail "model download failed (exit $LASTEXITCODE)" }
58
+ } else {
59
+ $ModelPath = (Resolve-Path -LiteralPath $ModelPath).Path
60
+ Copy-Item -LiteralPath $ModelPath -Destination $stagedModelPath -Force
61
+ }
62
+ $actualModelSha256 = (Get-FileHash -LiteralPath $stagedModelPath -Algorithm SHA256).Hash.ToLowerInvariant()
63
+ if ($actualModelSha256 -ne $ModelSha256.ToLowerInvariant()) {
64
+ Fail "model SHA-256 mismatch (expected $ModelSha256, got $actualModelSha256)"
65
+ }
66
+ Log "verified local AI model SHA-256"
67
+
68
+ $sdkArchive = Join-Path $Scratch "lzma-sdk.7z"
69
+ Log "downloading pinned official 7-Zip installer module"
70
+ & curl.exe -fL --retry 3 --output $sdkArchive $SevenZipSdkUrl
71
+ if ($LASTEXITCODE -ne 0) { Fail "LZMA SDK download failed (exit $LASTEXITCODE)" }
72
+ $actualSdkSha256 = (Get-FileHash -LiteralPath $sdkArchive -Algorithm SHA256).Hash.ToLowerInvariant()
73
+ if ($actualSdkSha256 -ne $SevenZipSdkSha256.ToLowerInvariant()) {
74
+ Fail "LZMA SDK SHA-256 mismatch (expected $SevenZipSdkSha256, got $actualSdkSha256)"
75
+ }
76
+
77
+ $sfxTools = Join-Path $Scratch "lzma-sdk"
78
+ New-Item -ItemType Directory -Force -Path $sfxTools | Out-Null
79
+ & $sevenZip.Source x $sdkArchive "-o$sfxTools" -y | Out-Null
80
+ if ($LASTEXITCODE -ne 0) { Fail "could not extract the LZMA SDK" }
81
+ $sfxModule = Get-ChildItem -LiteralPath $sfxTools -Recurse -File -Filter "7zSD.sfx" | Select-Object -First 1
82
+ if (-not $sfxModule) { Fail "official 7zSD.sfx installer module was not found" }
83
+
84
+ $innerName = "TrillboardsEdgeSetup-core.exe"
85
+ Copy-Item -LiteralPath $InnerInstaller -Destination (Join-Path $Stage $innerName) -Force
86
+ $sevenZipNotice = @'
87
+ This installer uses the 7-Zip SFX installer module from the LZMA SDK.
88
+ 7-Zip is licensed under the GNU LGPL and other included licenses.
89
+ Source and license information: https://www.7-zip.org/
90
+ '@
91
+ [System.IO.File]::WriteAllText((Join-Path $Stage "7zip-NOTICE.txt"), $sevenZipNotice, [System.Text.UTF8Encoding]::new($false))
92
+
93
+ $launcher = @'
94
+ Option Explicit
95
+ Dim shell, fso, base, installer, model, command, result
96
+ Set shell = CreateObject("WScript.Shell")
97
+ Set fso = CreateObject("Scripting.FileSystemObject")
98
+ base = fso.GetParentFolderName(WScript.ScriptFullName)
99
+ installer = fso.BuildPath(base, "TrillboardsEdgeSetup-core.exe")
100
+ model = fso.BuildPath(base, "gemma-4-E2B-it.litertlm")
101
+ shell.Environment("Process")("TRILLBOARDS_BUNDLED_MODEL_PATH") = model
102
+ command = Chr(34) & installer & Chr(34) & " /MODEL_SOURCE=" & Chr(34) & model & Chr(34)
103
+ result = shell.Run(command, 1, True)
104
+ WScript.Quit result
105
+ '@
106
+ [System.IO.File]::WriteAllText((Join-Path $Stage "install-bundled-ai.vbs"), $launcher, [System.Text.UTF8Encoding]::new($false))
107
+
108
+ $archive = Join-Path $Scratch "trillboards-cctv.7z"
109
+ Push-Location $Stage
110
+ try {
111
+ # Store the already-compressed model. This keeps the release deterministic and
112
+ # avoids spending CI time for negligible additional compression.
113
+ & $sevenZip.Source a -t7z -mx=0 -ms=off $archive "*" | Out-Host
114
+ if ($LASTEXITCODE -ne 0) { Fail "payload archive creation failed" }
115
+ } finally {
116
+ Pop-Location
117
+ }
118
+ & $sevenZip.Source t $archive | Out-Host
119
+ if ($LASTEXITCODE -ne 0) { Fail "payload archive integrity test failed" }
120
+
121
+ $config = @'
122
+ ;!@Install@!UTF-8!
123
+ Title="Trillboards CCTV Setup"
124
+ Progress="yes"
125
+ GUIMode="1"
126
+ RunProgram="wscript.exe install-bundled-ai.vbs"
127
+ ;!@InstallEnd@!
128
+ '@
129
+ $configPath = Join-Path $Scratch "sfx-config.txt"
130
+ [System.IO.File]::WriteAllText($configPath, $config, [System.Text.UTF8Encoding]::new($false))
131
+
132
+ if (Test-Path -LiteralPath $OutputPath) { Remove-Item -LiteralPath $OutputPath -Force }
133
+ $output = [System.IO.File]::Open($OutputPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
134
+ try {
135
+ foreach ($part in @($sfxModule.FullName, $configPath, $archive)) {
136
+ $input = [System.IO.File]::OpenRead($part)
137
+ try { $input.CopyTo($output) } finally { $input.Dispose() }
138
+ }
139
+ } finally {
140
+ $output.Dispose()
141
+ }
142
+
143
+ $modelBytes = (Get-Item -LiteralPath $stagedModelPath).Length
144
+ $outputBytes = (Get-Item -LiteralPath $OutputPath).Length
145
+ if ($outputBytes -le $modelBytes) { Fail "customer installer is unexpectedly smaller than its required model" }
146
+ Log "built one-file customer installer: $OutputPath ($([Math]::Round($outputBytes / 1GB, 2)) GB)"
147
+
148
+ # The customer executable is self-contained. Reclaim the staged model and raw
149
+ # archive now so signing and publish do not carry an extra ~4.8 GB on the runner.
150
+ Remove-Item -LiteralPath $Scratch -Recurse -Force -ErrorAction SilentlyContinue
151
+
152
+ if ($env:GITHUB_OUTPUT) { "exe=$OutputPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append }
153
+ Write-Output $OutputPath
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+ //
3
+ // check-feeds.js — RTSP-readiness gate for the Windows pairing helper.
4
+ //
5
+ // Exit 0 = every paired feed has an rtspUrl (ready to stream → start the task).
6
+ // Exit 1 = at least one paired feed is still missing its rtspUrl.
7
+ //
8
+ // Mirrors the macOS pair.command `count_feeds_missing_rtsp` guard: in the
9
+ // creds-stay-on-prem pairing path the server returns cameras WITHOUT an rtspUrl
10
+ // (written into the config as empty placeholders) and `runPair` still exits 0 with
11
+ // only a warning. cloud-sense then skips the URL-less feeds and exits with nothing
12
+ // to run — so the helper must NOT start the background task or claim "connected"
13
+ // until the operator pastes the rtsp:// URL(s) into the config.
14
+ //
15
+ // Usage: node check-feeds.js <path-to-trillboards.config.json>
16
+ // Unparseable / missing config → exit 0 (fail open, matching the macOS guard) so a
17
+ // transient read error never blocks an otherwise-valid start.
18
+ try {
19
+ const cfg = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8'));
20
+ const feeds = Array.isArray(cfg.cameras)
21
+ ? cfg.cameras
22
+ : (cfg.camera && (cfg.camera.rtspUrl || cfg.deviceToken) ? [cfg.camera] : []);
23
+ process.exit(feeds.some((f) => !f || !f.rtspUrl) ? 1 : 0);
24
+ } catch (e) {
25
+ process.exit(0);
26
+ }
@@ -0,0 +1,185 @@
1
+ <#
2
+ .SYNOPSIS
3
+ test-local-sense.ps1 — Windows on-device-Gemma (LOCAL-sense) E2E for the
4
+ BYO-CCTV connector. PowerShell port of the proven macOS harness
5
+ (deploy/macos/test-install-pair.sh), focused on the local-inference path that
6
+ ONLY windows-latest can prove (Docker can't run Windows containers; Mac CI is
7
+ ARM and Gemma's Windows LiteRT-LM runtime is x86_64).
8
+
9
+ .DESCRIPTION
10
+ Two modes — pick with -Mode:
11
+
12
+ wiring (DEFAULT, runs on EVERY CI event incl. PRs):
13
+ Builds the connector, then runs the cross-platform Node wiring runner
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.
20
+
21
+ real (workflow_dispatch / scheduled only):
22
+ Provisions litert-lm + imports Gemma 4 E2B (setup-litertlm.ps1, model cached),
23
+ waits for the real `litert-lm serve` :9379 to be healthy, then runs the REAL
24
+ on-device inference (scripts/verify-gemma4-vlm.mjs --require-person) on the
25
+ committed person fixture and asserts the model reports a person. This proves
26
+ ACTUAL Gemma inference on windows-latest.
27
+
28
+ .PARAMETER Mode 'wiring' (default) or 'real'.
29
+ .PARAMETER Frame Person JPEG fixture. Default: e2e/fixtures/person.jpg.
30
+ .PARAMETER Port litert-lm serve port (real mode). Default 9379.
31
+ .PARAMETER FfmpegPath ffmpeg for the connector's RTSP capture (wiring mode).
32
+ Default: the bundled bin\ffmpeg.exe if staged, else 'ffmpeg'.
33
+
34
+ .OUTPUTS exit 0 = E2E passed; exit 1 = failed.
35
+ #>
36
+ [CmdletBinding()]
37
+ param(
38
+ [ValidateSet('wiring', 'real')][string]$Mode = 'wiring',
39
+ [string]$Frame,
40
+ [string]$Port = '9379',
41
+ [string]$FfmpegPath
42
+ )
43
+ $ErrorActionPreference = 'Stop'
44
+ Set-StrictMode -Version Latest
45
+
46
+ $Here = Split-Path -Parent $MyInvocation.MyCommand.Path # deploy\windows\e2e
47
+ $PkgRoot = (Resolve-Path "$Here\..\..\..").Path # ...\edge-sdk
48
+ $WorkspaceRoot = (Resolve-Path "$PkgRoot\..\..").Path # ...\trillboards-edge-sdk
49
+ if (-not $Frame) { $Frame = Join-Path $Here 'fixtures\person.jpg' }
50
+
51
+ function Log ($m) { Write-Host "[local-sense-e2e] $m" -ForegroundColor Blue }
52
+ function Pass ($m) { Write-Host "PASS - $m" -ForegroundColor Green }
53
+ function Die ($m) { Write-Host "FAIL - $m" -ForegroundColor Red; exit 1 }
54
+
55
+ Log "mode=$Mode pkgRoot=$PkgRoot"
56
+ if (-not (Test-Path $Frame)) { Die "person fixture not found: $Frame" }
57
+ Log "person fixture: $Frame ($((Get-Item $Frame).Length) bytes)"
58
+
59
+ # ─── Build the connector (both modes need dist/cli.js + the dist harness) ──────
60
+ # Plain tsc (npm run build) — fast + reliable, NOT the full NSIS staging (whose
61
+ # node_modules robocopy is the slow step that timed out the release build at 45
62
+ # min; the wiring gate must stay well under its 20-min cap). dist/cli.js is the
63
+ # SAME shipped entrypoint either way; ffmpeg is resolved separately below.
64
+ Log "Building the connector (tsc, all workspaces in dependency order)…"
65
+ Push-Location $WorkspaceRoot
66
+ npm run build
67
+ if ($LASTEXITCODE -ne 0) { Pop-Location; Die "npm run build (tsc) failed" }
68
+ Pop-Location
69
+
70
+ $Cli = Join-Path $PkgRoot 'dist\cli.js'
71
+ if (-not (Test-Path $Cli)) { Die "dist\cli.js not built at $Cli" }
72
+ Log "connector built: $Cli"
73
+
74
+ # ─── Resolve ffmpeg for the connector's RTSP/file capture ──────────────────────
75
+ # Order: explicit -FfmpegPath → a staged bundle ffmpeg (if a prior full build ran)
76
+ # → host ffmpeg on PATH → download the SAME pinned static ffmpeg the shipped
77
+ # connector uses (small, no robocopy). The connector needs SOME ffmpeg to turn
78
+ # the file: fixture into a JPEG; which one doesn't change what the test proves.
79
+ function Resolve-Ffmpeg {
80
+ if ($FfmpegPath -and (Get-Command $FfmpegPath -ErrorAction SilentlyContinue)) { return $FfmpegPath }
81
+ $staged = Join-Path $PkgRoot 'deploy\windows\dist-installer\stage-x64\Trillboards\edge\bin\ffmpeg.exe'
82
+ if (Test-Path $staged) { return $staged }
83
+ if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { return 'ffmpeg' }
84
+ # Download the pinned static win32-x64 ffmpeg (same tag/sha the build pins).
85
+ $tag = if ($env:FFMPEG_STATIC_TAG) { $env:FFMPEG_STATIC_TAG } else { 'b6.0' }
86
+ $sha = if ($env:FFMPEG_SHA256_WIN) { $env:FFMPEG_SHA256_WIN } else { 'e9fd5e711debab9d680955fc1e38a2c1160fd280b144476cc3f62bc43ef49db1' }
87
+ $dest = Join-Path ([System.IO.Path]::GetTempPath()) 'tb-e2e-ffmpeg.exe'
88
+ if (-not (Test-Path $dest)) {
89
+ $url = "https://github.com/eugeneware/ffmpeg-static/releases/download/$tag/ffmpeg-win32-x64"
90
+ Log "no ffmpeg found — downloading pinned static ffmpeg ($tag)"
91
+ Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
92
+ $actual = (Get-FileHash -Algorithm SHA256 $dest).Hash.ToLower()
93
+ if ($actual -ne $sha.ToLower()) { Remove-Item $dest -Force; Die "ffmpeg SHA-256 mismatch (expected $sha, got $actual)" }
94
+ }
95
+ return $dest
96
+ }
97
+ $FfmpegPath = Resolve-Ffmpeg
98
+ Log "ffmpeg for capture: $FfmpegPath"
99
+
100
+ # ─── Mode: wiring (stub VLM, deterministic, every run) ─────────────────────────
101
+ if ($Mode -eq 'wiring') {
102
+ Log "WIRING E2E — STUB :9379 VLM + STUB ingest; real connector cli.js local-sense"
103
+ Log "(this proves plumbing, NOT real Gemma inference)"
104
+ $runner = Join-Path $PkgRoot 'scripts\run-local-sense-wiring-e2e.mjs'
105
+ if (-not (Test-Path $runner)) { Die "wiring runner missing: $runner" }
106
+ node $runner --frame $Frame --ffmpeg $FfmpegPath
107
+ if ($LASTEXITCODE -ne 0) { Die "wiring E2E runner exited $LASTEXITCODE" }
108
+ Pass "Windows LOCAL-sense WIRING proven (stub VLM): connector→serve→preAnalyzed→person."
109
+ exit 0
110
+ }
111
+
112
+ # ─── Mode: real (actual Gemma 4 E2B via litert-lm serve) ───────────────────────
113
+ Log "REAL-MODEL SMOKE — provisioning litert-lm + Gemma 4 E2B, then real inference"
114
+
115
+ # 1. Provision litert-lm + import the model (idempotent; model may be cached by
116
+ # the workflow's actions/cache step before this runs).
117
+ $setup = Join-Path $PkgRoot 'deploy\windows\setup-litertlm.ps1'
118
+ if (-not (Test-Path $setup)) { Die "setup-litertlm.ps1 missing: $setup" }
119
+ Log "running setup-litertlm.ps1 (uv tool install litert-lm + import Gemma 4 E2B + serve task)…"
120
+ & powershell -NoProfile -ExecutionPolicy Bypass -File $setup -Port $Port -AllowModelDownload
121
+ Log "setup-litertlm.ps1 finished (exit $LASTEXITCODE — non-fatal by design)"
122
+
123
+ # 2. Locate litert-lm + ensure a serve is up. setup registers a Scheduled Task,
124
+ # but in CI (no interactive logon) the AtLogOn task won't fire — so start
125
+ # `litert-lm serve` directly as a background process and wait for health.
126
+ $LocalBin = Join-Path $env:USERPROFILE '.local\bin'
127
+ $env:Path = "$LocalBin;$env:Path"
128
+ $litertlmCmd = Get-Command litert-lm -ErrorAction SilentlyContinue
129
+ $litertlm = if ($litertlmCmd) { $litertlmCmd.Source } else { Join-Path $LocalBin 'litert-lm.exe' }
130
+ if (-not (Test-Path $litertlm)) { Die "litert-lm not installed (uv tool install failed) — cannot run real-model smoke" }
131
+ Log "litert-lm: $litertlm"
132
+
133
+ function Test-ServeHealthy {
134
+ try {
135
+ $r = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/v1/models" -UseBasicParsing -TimeoutSec 3
136
+ return ($r.StatusCode -eq 200)
137
+ } catch { return $false }
138
+ }
139
+
140
+ $LogBase = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { $env:TEMP } else { $env:RUNNER_TEMP }
141
+ $ServeLog = Join-Path $LogBase 'litertlm-serve.log'
142
+ $serveProc = $null
143
+ if (Test-ServeHealthy) {
144
+ Log "litert-lm serve already healthy on :$Port"
145
+ } else {
146
+ Log "starting litert-lm serve on 127.0.0.1:$Port (log: $ServeLog)"
147
+ $serveProc = Start-Process -FilePath $litertlm `
148
+ -ArgumentList @('serve', '--host', '127.0.0.1', '--port', $Port) `
149
+ -RedirectStandardOutput $ServeLog -RedirectStandardError "$ServeLog.err" `
150
+ -PassThru -NoNewWindow
151
+ }
152
+
153
+ # 3. Wait for health (model load on first run can take a while; serve must answer
154
+ # /v1/models). Bounded to keep the job well under the 60-min cap.
155
+ $healthy = $false
156
+ for ($i = 0; $i -lt 120; $i++) {
157
+ if (Test-ServeHealthy) { $healthy = $true; break }
158
+ if ($serveProc -and $serveProc.HasExited) {
159
+ Log "litert-lm serve exited early (code $($serveProc.ExitCode))"
160
+ break
161
+ }
162
+ Start-Sleep -Seconds 5
163
+ }
164
+ if (-not $healthy) {
165
+ Log "--- serve log tail ---"
166
+ Get-Content $ServeLog -Tail 40 -ErrorAction SilentlyContinue
167
+ Get-Content "$ServeLog.err" -Tail 40 -ErrorAction SilentlyContinue
168
+ if ($serveProc) { try { $serveProc.Kill() } catch {} }
169
+ Die "litert-lm serve did not become healthy on :$Port within timeout"
170
+ }
171
+ Log "litert-lm serve healthy on :$Port"
172
+
173
+ # 4. REAL on-device inference on the person fixture + assert a person was seen.
174
+ $verifyExit = 1
175
+ try {
176
+ $verify = Join-Path $PkgRoot 'scripts\verify-gemma4-vlm.mjs'
177
+ Log "running real Gemma inference (verify-gemma4-vlm.mjs --require-person)…"
178
+ node $verify $Frame --litert "http://127.0.0.1:$Port" --require-person
179
+ $verifyExit = $LASTEXITCODE
180
+ } finally {
181
+ if ($serveProc) { try { $serveProc.Kill() } catch {} }
182
+ }
183
+ if ($verifyExit -ne 0) { Die "real-model smoke FAILED (verify-gemma4-vlm.mjs exit $verifyExit)" }
184
+ Pass "Windows on-device Gemma 4 E2B detected a person on the fixture (REAL inference)."
185
+ exit 0
@@ -45,7 +45,12 @@ Pop-Location
45
45
  # Download models
46
46
  Write-Host "Downloading ONNX models..."
47
47
  Push-Location $InstallDir
48
- npx trillboards-edge download-models --dir $ModelsDir
48
+ # No model download: Trillboards does not distribute blazeface.onnx/yamnet.onnx,
49
+ # and this bundle excludes onnxruntime-node anyway (see build-windows.ps1), so the
50
+ # ONNX sensing path does not run here — it uses LiteRT-LM/Gemma. The previously
51
+ # hardcoded models.trillboards.com was never provisioned and answered every path
52
+ # with the marketing SPA's index.html (HTTP 200, 2490 bytes of HTML).
53
+ New-Item -ItemType Directory -Force -Path $ModelsDir | Out-Null
49
54
  Pop-Location
50
55
 
51
56
  # Install config template