Перейти к содержанию

API Reference

REST API for Iris. All application endpoints are JSON over HTTP and are mounted under /api; media endpoints (clips, thumbnails, HLS) return binary streams. The service binds to 127.0.0.1 only and is reached through an nginx cookie-SSO vhost that terminates TLS, authenticates the session and proxies to the app.

Routers (source: app/api/*.py):

Resource Prefix File
Cameras /api/cameras app/api/cameras.py
Live monitoring /api/live app/api/live.py
Events / recordings /api/events app/api/events.py
Identities (auto cross-camera) /api/identities app/api/identities.py
People (manual face DB) /api/people app/api/people.py
Face groups /api/face-groups app/api/face_groups.py
System / introspection /api/system app/api/system.py
Health /health app/main.py

Authentication

Defined in app/auth.py. Every /api/* route depends on require_auth (an alias of require_user), which accepts two credentials in priority order:

  1. Trusted SSO header — the nginx gateway injects X-Email (configurable via sso_email_header; the username comes from sso_user_header). The app trusts this header because it only binds to loopback and is reachable solely through nginx, which overrides any client-supplied value (anti-spoof). Presence of the header ⇒ the request is authenticated.
  2. Bearer API keyAuthorization: Bearer <API_KEY>, for SSH-tunnel / CLI use that bypasses nginx. The token is compared in constant time (hmac.compare_digest) against the configured API_KEY.

Fail-closed behaviour:

  • When auth_required is true (the default) and neither credential is present, the request gets 401 (WWW-Authenticate: Bearer). A supplied-but-wrong bearer token yields Invalid API key.; a missing credential yields Authentication required.
  • When auth_required is false (local dev with no nginx and no key), the dependency degrades to an anonymous principal so the API is usable out of the box.

/health is the only unauthenticated route (used by an external monitoring probe).

Media endpoints (event clip/thumbnail, identity/sighting/face thumbnails) also work behind the bearer-key path so an HTML <video>/<img> tag can fetch them through nginx carrying the SSO cookie.


Cameras — /api/cameras

CRUD over the cameras table. Mutations drive the per-camera WorkerManager: enabling/disabling a camera or changing its RTSP URL spawns, stops or restarts the camera's subprocess. Online/offline status is derived from the live worker heartbeat (published ~1 s) with a staleness fallback to the persisted row.

Method Path Purpose
GET /api/cameras List all cameras with derived status.
POST /api/cameras Create a camera (starts its worker immediately when enabled). 201.
GET /api/cameras/{id} Fetch one camera.
PUT /api/cameras/{id} Partial update.
DELETE /api/cameras/{id} Delete camera + all derived data. 204.
GET /api/cameras/{id}/status Live status: status, last_seen, fps, detector.
GET /api/cameras/detect/classes Detector's supported COCO classes + the default trigger set (used by the camera form).

Create / update body (all tuning fields optional on update):

Field Type Notes
name str
rtsp_url str Write-only secret; never returned in clear (see masking below).
enabled bool
detect_conf float 0–1 Detection confidence threshold.
pre_seconds / post_seconds int Clip pre/post-roll (0–120 / 0–300).
trigger_classes csv str COCO classes that trigger recording, e.g. person,car,dog. Empty ⇒ person.
detect_iou float 0–1 NMS IoU.
detect_imgsz int 128–1920 Detector input size (px).
detect_interval float 0–60 Seconds between detections (0 = every frame).
trigger_cooldown float 0–3600 Min seconds between event triggers.
min_trigger_frames int 1–100 Consecutive detection frames before a trigger.
rtsp_transport tcp|udp Validated.
faces_enabled bool Run face recognition on this camera.
reid_enabled bool Run cross-camera re-ID on this camera.

Notable behaviour:

  • Masked rtsp_url — every response redacts embedded credentials (rtsp://user:pass@host/srtsp://***@host/s); the host is kept so operators can identify the camera. The raw URL stays server-side (the worker reads it straight from the DB).
  • Update echo handling — because the API only ever returns a masked URL, a blank or ***-containing rtsp_url on update is treated as "keep the current one"; only a real new URL replaces the stored secret.
  • Worker lifecycle on update — worker is started on enable, stopped on disable, and restarted when the RTSP URL changes or any tuning field changes.
  • RTSP repoint purges history — changing the RTSP URL purges this camera's prior events and their on-disk files (it is effectively a different physical camera).
  • Delete is recursive — stops the worker and HLS session, removes on-disk artifacts (clips/thumbnails/segments/HLS/frame slot) before the FK cascade drops event rows, deletes camera-scoped rows with no FK cascade (face samples, presence segments, face/appearance exemplars), then drops any orphan identities left with no sightings (cross-camera identities still seen elsewhere survive) and unlinks them from events. Finally reloads the identity gallery + FAISS face index.
  • Status semantics — disabled ⇒ offline; worker errorerror; an online heartbeat older than status_stale_seconds (default 30 s) ⇒ offline.

Live monitoring — /api/live

Low-latency MJPEG / snapshot streaming of each worker's latest annotated JPEG frame, manual recording, and on-demand RTSP→HLS for live-with-sound. No transcoding happens here for MJPEG — the worker already encodes annotated frames (boxes drawn); this router only multiplexes bytes onto HTTP.

Method Path Purpose
GET /api/live/{id}/snapshot Latest annotated frame as a single image/jpeg.
GET /api/live/{id}/stream MJPEG (multipart/x-mixed-replace) stream of annotated frames.
POST /api/live/{id}/record/start Begin a manual recording.
POST /api/live/{id}/record/stop Finish a manual recording → persist a manual event + clip.
GET /api/live/{id}/hls/index.m3u8 Start (idempotent) the HLS session and serve the playlist.
GET/POST /api/live/{id}/hls/close Stop the camera's HLS session (player close / sendBeacon).
GET /api/live/{id}/hls/{segment} Serve one .ts segment.

Notable behaviour:

  • ?fps= throttle — the MJPEG stream rate is capped by live_mjpeg_fps (default 10). ?fps= lets a caller request a lower rate (clamped to [1, cap]): the grid asks for a low rate across many tiles, a focused/fullscreen viewer asks for full rate. The generator only pushes a part when the frame actually changes, and stops when the client disconnects.
  • Graceful start-upsnapshot and the MJPEG prime frame return a "no signal" placeholder JPEG (HTTP 200) when the worker has not yet produced a frame, so <img> elements don't break. 404 is returned only if the camera row doesn't exist.
  • Manual recording is statelessrecord/start returns a server-trusted started_at timestamp; the client echoes it back to record/stop (body {"started_at": "<iso>"}). Stop assembles a clip spanning [started_at, now] from the warm on-disk segment buffer (≈1 s pre/post-roll), bounded by segment_retention_seconds, and persists it as a manual event. Returns {event_id, clip, duration}. Invalid started_at ⇒ 400.
  • HLS — playback is for live-with-sound (MJPEG carries no audio). index.m3u8 starts an ffmpeg RTSP→HLS session (returns 503 if HLS is disabled or at capacity), waits briefly for the playlist to appear, then serves it. Segment names are validated against ^seg\d{5}\.ts$. nginx must run with proxy_buffering off (and the stream sets X-Accel-Buffering: no) for per-frame flushing.

Events / recordings — /api/events

Read/serve side of recordings. Events are created by the per-camera track-mode recorder (one event per presence) or by manual recording.

Method Path Purpose
GET /api/events List/filter events, newest-first, paginated.
GET /api/events/{id} Full event detail.
DELETE /api/events/{id} Delete event row + clip + thumbnail files. 204.
POST /api/events/bulk-delete Delete the given event ids (+ their files).
POST /api/events/clear-all Delete ALL events (optionally filtered) — requires confirm.
GET /api/events/{id}/clip Stream the recorded mp4 clip (HTTP Range supported).
GET /api/events/{id}/thumbnail Event thumbnail JPEG.

List filters (query): camera_id, person_id, from (ISO, inclusive), to (ISO, exclusive), label, limit (1–500, default 50), offset. Response: { total, items[] }.

Bulk-delete body: { "ids": [int, …] } (max_length 1000). Clear-all body: { "confirm": true, "camera_id"?, "label"? }; confirm is mandatory (400 otherwise), deletes in chunks of 200, and sightings referencing the events are SET NULL (they survive).

Notable behaviour:

  • Range streaming/clip supports Range: bytes=start-end (and suffix ranges bytes=-N) for <video> seeking, returning 206 Partial Content with Content-Range; unsatisfiable ranges return 416; full requests stream the whole file in 1 MiB chunks (flat memory). Accept-Ranges: bytes is always advertised.
  • Path-traversal containment — every served file path is resolved against the data root and rejected (400) if it escapes (os.path.commonpath).
  • 404s — missing event, or event with no clip/thumbnail, or file missing on disk.

Identities — /api/identities

The automatic, cross-camera person/object layer built by the re-ID pipeline. Identities are created automatically on sightings and corrected by operators after the fact. The ORM models are imported lazily; if they are absent the router returns 503.

Method Path Purpose
GET /api/identities List identities, most-recently-seen first.
GET /api/identities/analytics Aggregates for the People analytics dashboard.
GET /api/identities/{id} One identity + recent inline sightings.
GET /api/identities/{id}/sightings Paginated cross-camera/time sightings.
GET /api/identities/{id}/events Recorded clips this identity appears in.
GET /api/identities/{id}/faces Face samples captured for this identity.
GET /api/identities/{id}/thumbnail Representative body-crop JPEG.
GET /api/identities/sightings/{sid}/thumbnail One sighting's body-crop JPEG.
PUT /api/identities/{id} Rename / annotate / confirm.
POST /api/identities/merge Merge source_ids into target_id.
POST /api/identities/{id}/split Split off sightings into a new identity.
DELETE /api/identities/{id} Recursive delete. 204.
POST /api/identities/bulk-delete Delete the given identity ids.
POST /api/identities/clear-all Delete ALL identities (optionally one class) — requires confirm.

List filters (query): named_only (operator-named only), min_sightings (hide sparse/provisional clusters), object_class (e.g. person/car/dog), limit (1–500, default 100), offset. Each item carries id, name, is_named, object_class, optional vehicle attrs (color/color_hex/make/vehicle_type), total_seconds, num_sightings, first_seen/last_seen, list of cameras, rep_thumb_url, face_thumb_url.

Detail (GET /{id}) — query recent (0–200, default 24) inlines that many recent sightings; also returns num_face_exemplars, num_appearance_exemplars, notes.

Analytics (GET /analytics) — query object_class (default person); returns a summary (totals, named/unnamed, created-today, seen-today, seen-7d, total sightings), by_camera (sightings, distinct people, avg dwell from presence segments), by_hour (last 24 h), and by_match_kind.

Rename body (PUT /{id}): { name?, notes?, is_named? }. Setting a non-null name marks is_named=true (freezes the maintenance auto-merge for that identity); is_named can be toggled explicitly.

Merge body: { "target_id": int, "source_ids": [int, …] } (1–1000 sources). Self-merge ids are dropped; all sources' sightings + face/appearance exemplars + denormalized events are reassigned to the target, then sources are deleted and counters/first-last-seen recomputed. A named source's name is preserved onto an unnamed target. Delegates to the re-ID core IdentityManager.merge when available, else a conservative DB-level fallback. Returns IdentityOpResult (ok, target_id, affected_ids, moved_sightings, detail).

Split body (POST /{id}/split): { sighting_ids?, auto?, new_name? }. Provide explicit sighting_ids to peel off, or auto=true to re-cluster the identity into two by face-priority. Explicit split has a DB-level fallback; auto split requires the re-ID core (returns 501 / 500 if unavailable). Without either parameter ⇒ 400.

Delete semantics (recursive):

  • DB cascade removes the identity's sightings, face/appearance exemplars and presence segments.
  • On-disk crops are purged: linked FaceSample crops + their rows (no FK cascade), and the whole per-identity crop dir data/identities/<id>/ (where sighting body-thumbnails live).
  • Solo vs shared events — an event whose subjects are all being deleted is removed together with its clip + thumbnail files; an event shared with other surviving people is kept, only the deleted person's denormalized link is nulled.
  • The identity gallery is reloaded afterward. bulk-delete ({ids}, max 1000) and clear-all ({confirm, object_class?}, chunked by 200) apply the same purge logic.

People — /api/people

Manual face-enrollment DB. Enrollment runs the insightface (SCRFD + ArcFace) recognizer to produce a 512-d L2-normalized embedding, stores it as a FaceEmbedding blob (numpy frombuffer, little-endian f32 — not pickle) and adds it to the in-memory FAISS index. The DB is the single source of truth; the index is rebuilt from it on any change.

Method Path Purpose
GET /api/people List people with num_faces.
POST /api/people Create a face-less person. 201.
GET /api/people/{id} Fetch one person.
PUT /api/people/{id} Update name / notes.
DELETE /api/people/{id} Delete person + faces + images; rebuild index. 204.
POST /api/people/bulk-delete Delete the given people (+ images).
POST /api/people/clear-all Delete ALL people (+ images) — requires confirm.
POST /api/people/{id}/faces Enroll a face from an uploaded photo. 201.
GET /api/people/{id}/faces List a person's face embeddings.
GET /api/people/{id}/faces/{fid}/image Serve the source enrollment photo.
DELETE /api/people/{id}/faces/{fid} Remove one embedding + image; rebuild index. 204.

Create/update body: { name, notes? }.

Enrollment (POST /{id}/faces) — multipart/form-data with file (one clear face). The largest detected face is embedded. Returns { embedding_id, faces_detected, image_path }. Errors: 415 unsupported content type, 400 empty upload / undecodable image, 413 too large (>8 MiB) or resolution too large (>50 MP decompression-bomb guard), 422 no face detected, 503 recognizer unavailable.

Notable behaviour:

  • Index consistency — every delete (single / bulk / clear-all / face delete) detaches events (person_id → NULL, keeping the denormalized person_name snapshot), removes source images from disk, and rebuilds FAISS once from the DB. bulk-delete body {ids} (max 1000); clear-all body {confirm} (chunked by 200).
  • Containment — stored image paths are always relative (data/faces/<id>/…); resolution is guarded against ..-traversal outside the data dir.

Face groups — /api/face-groups

Unsupervised clustering of captured FaceSample rows into same-person groups by cosine similarity, computed on demand (so settings are just query params — no precomputed state to invalidate).

Method Path Purpose
GET /api/face-groups Cluster + list groups (largest first).
GET /api/face-groups/samples/{id}/thumbnail Face-crop JPEG.
POST /api/face-groups/enroll Turn a group into a known Person.
POST /api/face-groups/label Set a label on the given samples.
POST /api/face-groups/clips Recorded clips a face group appears in.
POST /api/face-groups/samples/bulk-delete Drop the given captured faces (+ crops).
POST /api/face-groups/samples/clear-all Delete ALL face samples (optionally one label) — requires confirm.
DELETE /api/face-groups/samples/{id} Drop one captured face (+ crop). 204.

Clustering params (query on GET ""): face_threshold (0–1, default 0.5, min cosine to link), clothing_weight (0–1, blend of clothing similarity), min_size (hide smaller groups), max_members (member thumbnails per group, default 40), max_samples (cap on samples clustered, ≤2000). Response: { total_samples, total_groups, settings, groups[] }; each group exposes a stable group_id (its representative sample id), size, majority label, cameras, first/last seen, representative thumb and member thumbs.

Notable behaviour:

  • Enroll (POST /enroll) — body { sample_ids, name }. Creates a Person, copies up to 12 highest-quality sample vectors into FaceEmbedding rows (same 512-d f32 layout copies directly), labels the group's samples with the name, and rebuilds the FAISS index so live matching uses the new person. Returns { person_id, name, enrolled_faces }.
  • Label (POST /label) — body { sample_ids, label }; sets the label on those samples. Returns { updated, label }.
  • Clips (POST /clips) — body { sample_ids }. Because sighting.event_id is not reliably set, clips are resolved by camera + time overlap: a clip-event whose [ts-90s, end_ts+20s] window contains one of the group's face captures (or its linked identities' sightings). De-duplicated by event, newest-first; returns an EventListResponse.

System — /api/system

Method Path Purpose
GET /api/system Authenticated introspection: version, Python, the authenticated user (email/name/via), backend (detector, device, ONNX providers, faces-enabled), model paths (YOLO, insightface pack), face-index status + count, GPU memory (NVML, best-effort), and per-worker states.

Health — /health

Method Path Auth Purpose
GET /health none Liveness probe for external monitoring: { status, version, gpu: {used_mb, total_mb}, workers[] }.

The vanilla-JS SPA is served by StaticFiles(html=True) mounted at /; the API routers and /health take precedence over static paths.