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:
56
agent/pkg/bridge/engine_client.go
Normal file
56
agent/pkg/bridge/engine_client.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EngineEmbeddings reads templates from the local engine's API.
|
||||
type EngineEmbeddings struct {
|
||||
Base string
|
||||
User string
|
||||
Password string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func NewEngineEmbeddings(base, user, password string) *EngineEmbeddings {
|
||||
return &EngineEmbeddings{
|
||||
Base: strings.TrimRight(base, "/"), User: user, Password: password,
|
||||
Client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EngineEmbeddings) Embedding(ctx context.Context, id int64) ([]float32, string, error) {
|
||||
url := fmt.Sprintf("%s/api/identities/%d/embedding", e.Base, id)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if e.User != "" {
|
||||
req.SetBasicAuth(e.User, e.Password)
|
||||
}
|
||||
resp, err := e.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("engine returned %s", resp.Status)
|
||||
}
|
||||
var body struct {
|
||||
Model string `json:"model"`
|
||||
Embedding []float32 `json:"embedding"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(body.Embedding) == 0 {
|
||||
return nil, "", fmt.Errorf("engine returned an empty embedding")
|
||||
}
|
||||
return body.Embedding, body.Model, nil
|
||||
}
|
||||
Reference in New Issue
Block a user