@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.
- package/README.md +4 -3
- package/deploy/docker/Dockerfile.cloud-sense +117 -0
- package/deploy/docker/Dockerfile.cpu +30 -21
- package/deploy/docker/Dockerfile.cuda +30 -21
- package/deploy/docker/Dockerfile.openvino +30 -20
- package/deploy/docker/README.md +9 -4
- package/deploy/docker/connector-entrypoint.sh +122 -0
- package/deploy/helm/README.md +1 -1
- package/deploy/k8s/daemonset.yaml +1 -1
- package/deploy/macos/README.md +161 -0
- package/deploy/macos/TrillboardsSetupApp.m +603 -0
- package/deploy/macos/build-macos-pkg.sh +588 -0
- package/deploy/macos/com.trillboards.edge.plist +62 -0
- package/deploy/macos/com.trillboards.litertlm.plist +45 -0
- package/deploy/macos/pair.command +519 -0
- package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
- package/deploy/macos/scripts/postinstall +153 -0
- package/deploy/macos/scripts/setup-litertlm.sh +272 -0
- package/deploy/macos/test-install-pair.sh +383 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
- package/deploy/systemd/install.sh +25 -15
- package/deploy/systemd/trillboards-edge.service +4 -1
- package/deploy/windows/README.md +70 -0
- package/deploy/windows/build-windows.ps1 +225 -0
- package/deploy/windows/bundle-windows-installer.ps1 +153 -0
- package/deploy/windows/check-feeds.js +26 -0
- package/deploy/windows/e2e/fixtures/person.jpg +0 -0
- package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
- package/deploy/windows/install-update.ps1 +101 -0
- package/deploy/windows/installer.nsi +138 -0
- package/deploy/windows/register-litertlm-task.ps1 +42 -0
- package/deploy/windows/register-task.ps1 +44 -0
- package/deploy/windows/run-connector.ps1 +69 -0
- package/deploy/windows/setup-litertlm.ps1 +361 -0
- package/deploy/windows/stop-existing.ps1 +72 -0
- package/deploy/windows/trillboards-edge-task.vbs +25 -0
- package/deploy/windows/trillboards-edge.cmd +33 -0
- package/deploy/windows/trillboards-pair.cmd +276 -0
- package/deploy/windows/trillboards-setup.ps1 +456 -0
- package/deploy/windows/trillboards-setup.vbs +13 -0
- package/dist/AudienceAnalyzeClient.d.ts +136 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
- package/dist/AudienceAnalyzeClient.js +246 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -0
- package/dist/ChannelProber.d.ts +150 -0
- package/dist/ChannelProber.d.ts.map +1 -0
- package/dist/ChannelProber.js +887 -0
- package/dist/ChannelProber.js.map +1 -0
- package/dist/CloudEscalationPolicy.d.ts +143 -0
- package/dist/CloudEscalationPolicy.d.ts.map +1 -0
- package/dist/CloudEscalationPolicy.js +137 -0
- package/dist/CloudEscalationPolicy.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +533 -0
- package/dist/CloudSensingManager.d.ts.map +1 -0
- package/dist/CloudSensingManager.js +1254 -0
- package/dist/CloudSensingManager.js.map +1 -0
- package/dist/DiscoveryReportClient.d.ts +66 -0
- package/dist/DiscoveryReportClient.d.ts.map +1 -0
- package/dist/DiscoveryReportClient.js +85 -0
- package/dist/DiscoveryReportClient.js.map +1 -0
- package/dist/EdgeAgent.d.ts +12 -2
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +122 -65
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +193 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -0
- package/dist/LiteRtVlmProvider.js +436 -0
- package/dist/LiteRtVlmProvider.js.map +1 -0
- package/dist/MosaicSource.d.ts +108 -0
- package/dist/MosaicSource.d.ts.map +1 -0
- package/dist/MosaicSource.js +274 -0
- package/dist/MosaicSource.js.map +1 -0
- package/dist/OnvifClient.d.ts +83 -0
- package/dist/OnvifClient.d.ts.map +1 -0
- package/dist/OnvifClient.js +209 -0
- package/dist/OnvifClient.js.map +1 -0
- package/dist/PairClient.d.ts +106 -0
- package/dist/PairClient.d.ts.map +1 -0
- package/dist/PairClient.js +186 -0
- package/dist/PairClient.js.map +1 -0
- package/dist/RecorderAddressRecovery.d.ts +53 -0
- package/dist/RecorderAddressRecovery.d.ts.map +1 -0
- package/dist/RecorderAddressRecovery.js +359 -0
- package/dist/RecorderAddressRecovery.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +291 -0
- package/dist/SceneChangeDetector.d.ts.map +1 -0
- package/dist/SceneChangeDetector.js +682 -0
- package/dist/SceneChangeDetector.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +59 -0
- package/dist/SensingProfileClient.d.ts.map +1 -0
- package/dist/SensingProfileClient.js +78 -0
- package/dist/SensingProfileClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +114 -0
- package/dist/SetupSessionClient.d.ts.map +1 -0
- package/dist/SetupSessionClient.js +95 -0
- package/dist/SetupSessionClient.js.map +1 -0
- package/dist/VlmAdmissionGate.d.ts +126 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -0
- package/dist/VlmAdmissionGate.js +166 -0
- package/dist/VlmAdmissionGate.js.map +1 -0
- package/dist/autoSense.d.ts +216 -0
- package/dist/autoSense.d.ts.map +1 -0
- package/dist/autoSense.js +521 -0
- package/dist/autoSense.js.map +1 -0
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +511 -21
- package/dist/cli.js.map +1 -1
- package/dist/combinedSense.d.ts +128 -0
- package/dist/combinedSense.d.ts.map +1 -0
- package/dist/combinedSense.js +261 -0
- package/dist/combinedSense.js.map +1 -0
- package/dist/config.d.ts +351 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +99 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorCadence.d.ts +135 -0
- package/dist/connectorCadence.d.ts.map +1 -0
- package/dist/connectorCadence.js +151 -0
- package/dist/connectorCadence.js.map +1 -0
- package/dist/dataDir.d.ts +33 -0
- package/dist/dataDir.d.ts.map +1 -0
- package/dist/dataDir.js +76 -0
- package/dist/dataDir.js.map +1 -0
- package/dist/deployedCadence.d.ts +167 -0
- package/dist/deployedCadence.d.ts.map +1 -0
- package/dist/deployedCadence.js +209 -0
- package/dist/deployedCadence.js.map +1 -0
- package/dist/deployedCadenceSource.d.ts +48 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -0
- package/dist/deployedCadenceSource.js +74 -0
- package/dist/deployedCadenceSource.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +13 -12
- package/dist/diagnose.js.map +1 -1
- package/dist/discovery.d.ts +131 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +499 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gemmaModelOta.d.ts +118 -0
- package/dist/gemmaModelOta.d.ts.map +1 -0
- package/dist/gemmaModelOta.js +291 -0
- package/dist/gemmaModelOta.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +41 -18
- package/dist/init.js.map +1 -1
- package/dist/keepAwake.d.ts +33 -0
- package/dist/keepAwake.d.ts.map +1 -0
- package/dist/keepAwake.js +54 -0
- package/dist/keepAwake.js.map +1 -0
- package/dist/legacyRecorderProfile.d.ts +20 -0
- package/dist/legacyRecorderProfile.d.ts.map +1 -0
- package/dist/legacyRecorderProfile.js +27 -0
- package/dist/legacyRecorderProfile.js.map +1 -0
- package/dist/localInferenceSchema.d.ts +132 -0
- package/dist/localInferenceSchema.d.ts.map +1 -0
- package/dist/localInferenceSchema.js +274 -0
- package/dist/localInferenceSchema.js.map +1 -0
- package/dist/localSenseE2eHarness.d.ts +101 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -0
- package/dist/localSenseE2eHarness.js +279 -0
- package/dist/localSenseE2eHarness.js.map +1 -0
- package/dist/localSensing.d.ts +268 -0
- package/dist/localSensing.d.ts.map +1 -0
- package/dist/localSensing.js +492 -0
- package/dist/localSensing.js.map +1 -0
- package/dist/modelIntegrity.d.ts +59 -0
- package/dist/modelIntegrity.d.ts.map +1 -0
- package/dist/modelIntegrity.js +165 -0
- package/dist/modelIntegrity.js.map +1 -0
- package/dist/pairVerify.d.ts +100 -0
- package/dist/pairVerify.d.ts.map +1 -0
- package/dist/pairVerify.js +296 -0
- package/dist/pairVerify.js.map +1 -0
- package/dist/qrScanner.d.ts +33 -0
- package/dist/qrScanner.d.ts.map +1 -0
- package/dist/qrScanner.js +103 -0
- package/dist/qrScanner.js.map +1 -0
- package/dist/runtimePaths.d.ts +10 -0
- package/dist/runtimePaths.d.ts.map +1 -0
- package/dist/runtimePaths.js +79 -0
- package/dist/runtimePaths.js.map +1 -0
- package/dist/sensingRunMode.d.ts +68 -0
- package/dist/sensingRunMode.d.ts.map +1 -0
- package/dist/sensingRunMode.js +346 -0
- package/dist/sensingRunMode.js.map +1 -0
- package/dist/setupMode.d.ts +98 -0
- package/dist/setupMode.d.ts.map +1 -0
- package/dist/setupMode.js +828 -0
- package/dist/setupMode.js.map +1 -0
- package/dist/setupProgress.d.ts +31 -0
- package/dist/setupProgress.d.ts.map +1 -0
- package/dist/setupProgress.js +173 -0
- package/dist/setupProgress.js.map +1 -0
- 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
|
+
}
|
|
Binary file
|
|
@@ -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
|