Behavision: face recognition for retail, edge to head office
Five components that ship as one product:
- behavision/ the recognition engine. RTSP ingest, YuNet detection, IoU
tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
FastAPI dashboard. Identity is decided once per TRACK from an
average of at least three embeddings, never per frame.
- agent/ the Go edge agent: supervises the engine, holds a durable
spool, and drains it to MQTT. Nothing is acked before the
broker confirms.
- desktop/ the shop PC application (Wails + React + tray).
- server/ the cloud API, MQTT consumer, reports and assistant.
- web/ platform.loyaly.ai, the head-office app, embedded in the
server binary.
The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.
CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
162
installer/behavision.iss
Normal file
162
installer/behavision.iss
Normal file
@@ -0,0 +1,162 @@
|
||||
; Behavision installer (Inno Setup 6).
|
||||
;
|
||||
; Built by installer\build.ps1, which stages dist\Behavision first. Compile it
|
||||
; by hand with:
|
||||
; ISCC.exe /DMyAppVersion=0.1.0 installer\behavision.iss
|
||||
;
|
||||
; Three decisions worth reading before changing anything here:
|
||||
;
|
||||
; 1. ADMIN AT INSTALL TIME, NEVER AT RUN TIME. The files go under Program
|
||||
; Files, so installing needs elevation. Running does not: the app is a
|
||||
; normal user-session process that spawns the engine as a child, which is
|
||||
; also why it can draw a tray icon at all - a Windows service runs in
|
||||
; session 0 and cannot.
|
||||
;
|
||||
; 2. NO MODELS IN THE PACKAGE. They are ~200 MB and `setup-models` downloads
|
||||
; them resumably into the state root on first run. Bundling them would
|
||||
; quadruple this file and force a re-sign for a model change.
|
||||
;
|
||||
; 3. NOTHING WRITABLE UNDER Program Files. The database, logs, camera list,
|
||||
; downloaded models and agent config all live in %PROGRAMDATA%\Behavision,
|
||||
; which is what makes an upgrade a file copy rather than a migration. That
|
||||
; split is behavision/paths.py and agent/pkg/paths, and this file must not
|
||||
; contradict it.
|
||||
|
||||
#define MyAppName "Behavision"
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "0.1.0"
|
||||
#endif
|
||||
#define MyAppPublisher "Loyaly"
|
||||
#define MyAppURL "https://platform.loyaly.ai"
|
||||
#define MyAppExeName "Behavision.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{7C4B9E2A-3F51-4C86-9D0A-B1E7A2F65D11}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=Behavision-Setup-{#MyAppVersion}
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
; Admin, because Program Files is. See decision 1 above.
|
||||
PrivilegesRequired=admin
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
; The engine folder alone is ~400 MB unpacked; saying so up front beats a
|
||||
; wizard that stops halfway on a small shop PC.
|
||||
ExtraDiskSpaceRequired=450000000
|
||||
; Ask Windows' Restart Manager to close a running copy instead of writing DLLs
|
||||
; underneath it. The engine holds the SQLite WAL and the camera, so a half
|
||||
; replaced install fails on the NEXT start - long after anyone would connect
|
||||
; the two events. RestartApplications=no because the [Run] section starts the
|
||||
; app again itself, and twice is one process too many for one webcam.
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription: "Shortcuts:"
|
||||
; Ticked by default: the product is meaningless if it is not watching. A shop
|
||||
; PC reboots after a power cut at 3am with nobody there to open anything.
|
||||
Name: "startup"; Description: "Start Behavision when this PC starts"; GroupDescription: "Startup:"
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Behavision\Behavision.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\dist\Behavision\behavision-agent.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\dist\Behavision\engine\*"; DestDir: "{app}\engine"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; ~2 MB bootstrapper, downloaded at build time by build.ps1. The app is a
|
||||
; WebView2 window; without the runtime it opens BLANK - not an error, just an
|
||||
; empty white rectangle, which is the single worst failure mode to hand a shop.
|
||||
; Present on Windows 11 and recent Windows 10, absent on plenty of older
|
||||
; machines, and a shop PC is exactly where an older machine lives.
|
||||
Source: "vendor\MicrosoftEdgeWebview2Setup.exe"; DestDir: "{tmp}"; \
|
||||
Flags: deleteafterinstall; Check: WebView2Missing
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
; Per-user, not HKLM\Run: the app draws a tray icon and a window, so it has to
|
||||
; start in an interactive session. A machine-wide entry would try before anyone
|
||||
; has logged in.
|
||||
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startup
|
||||
; Named in the installer's own text when somebody skips the download, so it has
|
||||
; to exist. Console window on purpose: it is a 200 MB download with a progress
|
||||
; line, and a silent one looks like nothing happened.
|
||||
Name: "{group}\Download models"; Filename: "{app}\engine\behavision.exe"; \
|
||||
Parameters: "setup-models"; Comment: "Download the face recognition models"
|
||||
Name: "{group}\Where is my data"; Filename: "{app}\engine\behavision.exe"; \
|
||||
Parameters: "paths"; Comment: "Print the installed layout"
|
||||
|
||||
[Dirs]
|
||||
; Created here so a first run does not have to, and ACLed to Administrators
|
||||
; plus the installing user rather than Everyone: it holds face templates, which
|
||||
; are biometric personal data, and the engine's generated API password.
|
||||
Name: "{commonappdata}\Behavision"; Permissions: admins-full users-modify
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\MicrosoftEdgeWebview2Setup.exe"; Parameters: "/silent /install"; \
|
||||
StatusMsg: "Installing Microsoft Edge WebView2 (required to show the app)..."; \
|
||||
Flags: waituntilterminated; Check: WebView2Missing
|
||||
|
||||
; ~200 MB over the network, so it is offered rather than forced, and it is
|
||||
; resumable: a killed download leaves a .part file and the next run continues.
|
||||
Filename: "{app}\engine\behavision.exe"; Parameters: "setup-models"; \
|
||||
StatusMsg: "Downloading face recognition models (about 200 MB)..."; \
|
||||
Flags: runhidden waituntilterminated; Check: WantModels
|
||||
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "Start {#MyAppName} now"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
; The unpacked engine writes nothing here, but PyInstaller leaves stray
|
||||
; __pycache__ directories that would keep {app} alive after an uninstall.
|
||||
Type: filesandordirs; Name: "{app}\engine"
|
||||
|
||||
[Code]
|
||||
var
|
||||
ModelsPage: TInputOptionWizardPage;
|
||||
|
||||
procedure InitializeWizard;
|
||||
begin
|
||||
ModelsPage := CreateInputOptionPage(wpSelectTasks,
|
||||
'Face recognition models',
|
||||
'Behavision needs about 200 MB of model files to recognise faces.',
|
||||
'These are downloaded once. If this PC has no internet connection now, ' +
|
||||
'skip this and run "Download models" from the Start menu later - the app ' +
|
||||
'will not recognise anyone until they are present.',
|
||||
True, False);
|
||||
ModelsPage.Add('Download the models now (recommended)');
|
||||
ModelsPage.Values[0] := True;
|
||||
end;
|
||||
|
||||
function WantModels: Boolean;
|
||||
begin
|
||||
Result := ModelsPage.Values[0];
|
||||
end;
|
||||
|
||||
// The WebView2 runtime registers itself under EdgeUpdate with a non-empty
|
||||
// version. Checked in three places because the runtime can be installed
|
||||
// per-machine (both registry views on 64-bit) or per-user.
|
||||
function WebView2Missing: Boolean;
|
||||
const
|
||||
CLIENT = '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
|
||||
var
|
||||
pv: string;
|
||||
begin
|
||||
Result := True;
|
||||
if RegQueryStringValue(HKLM, 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\' + CLIENT, 'pv', pv) and (pv <> '') then
|
||||
Result := False
|
||||
else if RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\EdgeUpdate\Clients\' + CLIENT, 'pv', pv) and (pv <> '') then
|
||||
Result := False
|
||||
else if RegQueryStringValue(HKCU, 'SOFTWARE\Microsoft\EdgeUpdate\Clients\' + CLIENT, 'pv', pv) and (pv <> '') then
|
||||
Result := False;
|
||||
end;
|
||||
134
installer/build.ps1
Normal file
134
installer/build.ps1
Normal file
@@ -0,0 +1,134 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the Behavision Windows package: engine, app, agent, installer.
|
||||
|
||||
.DESCRIPTION
|
||||
This script must run ON WINDOWS. Everything else in this repository
|
||||
cross-compiles from a Mac - the Go binaries with GOOS=windows, the web bundle
|
||||
with npm - but the ENGINE cannot. PyInstaller freezes the interpreter and the
|
||||
native wheels (onnxruntime, OpenCV) of the machine it runs on; there is no
|
||||
cross-target flag, and there never has been. So the engine .exe is built here
|
||||
or it is not built at all.
|
||||
|
||||
Output: dist\Behavision-Setup-<version>.exe, plus dist\Behavision\ which is
|
||||
the unpacked tree the installer copies (useful for testing without
|
||||
installing).
|
||||
|
||||
.PARAMETER Version
|
||||
Stamped into the installer and shown in Add/Remove Programs.
|
||||
|
||||
.PARAMETER SkipInstaller
|
||||
Build the payload but not the setup .exe. Use when Inno Setup is absent.
|
||||
#>
|
||||
param(
|
||||
[string]$Version = "0.1.0",
|
||||
[switch]$SkipInstaller
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$dist = Join-Path $root "dist"
|
||||
$stage = Join-Path $dist "Behavision"
|
||||
|
||||
function Step($msg) { Write-Host "`n=== $msg ===" -ForegroundColor Cyan }
|
||||
function Need($exe, $hint) {
|
||||
if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) {
|
||||
throw "$exe not found on PATH. $hint"
|
||||
}
|
||||
}
|
||||
|
||||
Need python "Install Python 3.11+ and tick 'Add to PATH'."
|
||||
Need go "Install Go 1.21+ from https://go.dev/dl/."
|
||||
Need npm "Install Node.js LTS from https://nodejs.org/."
|
||||
|
||||
Step "Python environment"
|
||||
Push-Location $root
|
||||
if (-not (Test-Path ".venv")) { python -m venv .venv }
|
||||
& .\.venv\Scripts\python -m pip install --upgrade pip | Out-Null
|
||||
& .\.venv\Scripts\python -m pip install -r requirements.txt pyinstaller | Out-Null
|
||||
|
||||
Step "Engine tests"
|
||||
# The package is not worth building if the engine is broken, and finding that
|
||||
# out after the installer is signed is the expensive order to do it in.
|
||||
& .\.venv\Scripts\python -m pytest tests -q
|
||||
if ($LASTEXITCODE -ne 0) { throw "engine tests failed" }
|
||||
|
||||
Step "Engine (PyInstaller, one-folder)"
|
||||
if (Test-Path (Join-Path $root "build")) { Remove-Item -Recurse -Force (Join-Path $root "build") }
|
||||
& .\.venv\Scripts\pyinstaller behavision.spec --noconfirm --distpath (Join-Path $dist "engine-build")
|
||||
if ($LASTEXITCODE -ne 0) { throw "pyinstaller failed" }
|
||||
|
||||
Step "Desktop app (Wails)"
|
||||
Push-Location (Join-Path $root "desktop\frontend")
|
||||
npm ci
|
||||
npm run build
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $root "desktop")
|
||||
# Wails v2 talks to WebView2 through pure-Go bindings, so no cgo and no
|
||||
# toolchain beyond Go itself. Verified by cross-compiling the same package from
|
||||
# a Mac with CGO_ENABLED=0.
|
||||
$env:CGO_ENABLED = "0"
|
||||
if (Get-Command wails -ErrorAction SilentlyContinue) {
|
||||
wails build -platform windows/amd64 -clean -ldflags "-X main.version=$Version"
|
||||
} else {
|
||||
Write-Warning "wails CLI not found - falling back to a plain go build (no icon, no manifest)."
|
||||
go build -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") .
|
||||
}
|
||||
Pop-Location
|
||||
|
||||
Step "Headless agent"
|
||||
Push-Location (Join-Path $root "agent")
|
||||
$env:CGO_ENABLED = "0" # the cgo resolver forces external linking
|
||||
go build -o (Join-Path $root "dist\behavision-agent.exe") .
|
||||
Pop-Location
|
||||
|
||||
Step "WebView2 bootstrapper"
|
||||
# Bundled rather than downloaded at install time: a shop PC being set up often
|
||||
# has no working internet yet, and the app is a blank white window without it.
|
||||
$vendor = Join-Path $root "installer\vendor"
|
||||
New-Item -ItemType Directory -Force -Path $vendor | Out-Null
|
||||
$wv2 = Join-Path $vendor "MicrosoftEdgeWebview2Setup.exe"
|
||||
if (-not (Test-Path $wv2)) {
|
||||
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile $wv2
|
||||
}
|
||||
|
||||
Step "Staging"
|
||||
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
|
||||
New-Item -ItemType Directory -Force -Path $stage | Out-Null
|
||||
# The engine keeps its own folder: it is a one-folder PyInstaller build with
|
||||
# ~150 native DLLs beside it, and `behavision.exe` would otherwise collide with
|
||||
# `Behavision.exe` on a case-insensitive filesystem.
|
||||
Copy-Item -Recurse (Join-Path $dist "engine-build\behavision") (Join-Path $stage "engine")
|
||||
Copy-Item (Join-Path $root "desktop\build\bin\Behavision.exe") $stage
|
||||
Copy-Item (Join-Path $dist "behavision-agent.exe") $stage
|
||||
Copy-Item (Join-Path $root "LICENSE") $stage -ErrorAction SilentlyContinue
|
||||
|
||||
$engineExe = Join-Path $stage "engine\behavision.exe"
|
||||
if (-not (Test-Path $engineExe)) { throw "engine exe missing at $engineExe" }
|
||||
& $engineExe paths
|
||||
if ($LASTEXITCODE -ne 0) { throw "the frozen engine cannot start - `paths` failed" }
|
||||
|
||||
Pop-Location
|
||||
|
||||
if ($SkipInstaller) {
|
||||
Step "Done (payload only)"
|
||||
Write-Host "Unpacked tree: $stage"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Step "Installer (Inno Setup)"
|
||||
$iscc = @(
|
||||
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
||||
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
||||
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $iscc) {
|
||||
throw "Inno Setup 6 not found. Install it from https://jrsoftware.org/isdl.php, or re-run with -SkipInstaller."
|
||||
}
|
||||
& $iscc "/DMyAppVersion=$Version" (Join-Path $root "installer\behavision.iss")
|
||||
if ($LASTEXITCODE -ne 0) { throw "ISCC failed" }
|
||||
|
||||
Step "Done"
|
||||
Get-ChildItem (Join-Path $dist "Behavision-Setup-*.exe") | ForEach-Object {
|
||||
Write-Host ("{0} ({1:N1} MB)" -f $_.FullName, ($_.Length / 1MB))
|
||||
}
|
||||
Reference in New Issue
Block a user