How It Works#
High-Level Architecture#
The Behavioral Analysis Service is a single-process Python microservice that combines three distinct AI capabilities: skeletal pose extraction (YOLO-Pose via OpenVINO), declarative pattern matching (rule engine), and optional visual confirmation (VLM via OVMS). It is fully event-driven; processing is triggered by MQTT messages.
graph TD
A[upstream service<br/>e.g. swlp-service] -- MQTT ba/requests --> B[BAQueueConsumer]
B --> C[SeaweedFSClient<br/>fetch frames]
C --> D[YOLOPipelineRunner<br/>extract_poses]
D --> E[PoseRuleEngine<br/>evaluate patterns]
E -->|pattern matched| F[VLMClient<br/>visual confirmation]
E -->|no match| G[publish result]
F --> G
G -- MQTT ba/results --> H[downstream consumer]
Component Responsibilities#
Component |
Module |
Responsibility |
|---|---|---|
Application entry |
|
Application lifecycle, MQTT consumer initialization |
Configuration |
|
Pydantic |
MQTT consumer |
|
Subscribe to |
Frame store client |
|
Async S3-compatible reads from SeaweedFS; bucket creation and health check |
Pose pipeline |
|
Lazy-initialized YOLO model singleton; per-frame inference |
YOLO OpenVINO wrapper |
|
OpenVINO IR model loader; letterbox preprocessing; NMS postprocessing |
Pose analyzer |
|
Orchestrates pose detection + VLM confirmation; |
Rule engine |
|
Evaluates YAML-defined pose conditions and phases against pose sequences |
VLM client |
|
Async HTTP client for OpenAI-compatible VLM endpoint; circuit breaker; image encoding |
Package / Module Overview#
src/
├── main.py — Application startup, lifespan management, MQTT consumer initialization
├── config.py — Settings (env vars) + YAML config parsing
├── ba_queue.py — MQTT subscriber, analysis task dispatcher, result publisher
├── seaweedfs_client.py — Async frame retrieval from SeaweedFS
├── yolo_pipeline.py — extract_poses(): run YOLO-Pose on a list of frames
├── yolo_pose_ov.py — YOLOPoseOV: OpenVINO YOLO wrapper (no PyTorch)
├── pose_analyzer.py — PoseAnalyzer: detect_pattern(), analyze_with_vlm()
├── pose_rule_engine.py — PoseRuleEngine: declarative YAML rule evaluation
└── vlm_client.py — VLMClient: async multimodal HTTP client with circuit breaker
Request Lifecycle (MQTT Path)#
sequenceDiagram
participant US as Upstream Service
participant MQ as MQTT Broker
participant BA as BAQueueConsumer
participant SF as SeaweedFS
participant YO as YOLOPoseOV
participant RE as PoseRuleEngine
participant VL as VLMClient (OVMS)
US->>MQ: publish ba/requests {person_id, region_id, entry_timestamp, ...}
MQ->>BA: on_message callback
BA->>BA: Dedup check (skip if in-flight)
BA->>SF: get_frames(entity_id, max_frames=120)
SF-->>BA: list[(frame_bgr, timestamp_ms)]
BA->>YO: extract_poses(frames)
YO-->>BA: list[Pose] (COCO 17-keypoint)
BA->>RE: detect_all_patterns(poses)
RE-->>BA: PatternResult[]
alt pattern matched AND vlm_enabled
BA->>VL: analyze(key_frames, prompt)
VL-->>BA: VLMResult {suspicious, confidence, reasoning}
end
BA->>MQ: publish ba/results {person_id, status, confidence, vlm_response, ...}
Frame Storage Layout#
Frames are stored in SeaweedFS under the following path structure:
bucket: behavioral-frames
└── {entity_id}/
└── {region_id}/
└── {entry_timestamp}/
└── frames/
├── {timestamp_1}.jpg
├── {timestamp_2}.jpg
└── ...
The client sorts objects by timestamp filename to maintain chronological order before pose extraction.
Pose Extraction#
The pose extraction pipeline is orchestrated by extract_poses() in yolo_pipeline.py and implemented via the YOLOPoseOV class in yolo_pose_ov.py.
Model Architecture:
OpenVINO IR format (no PyTorch at runtime): XML model definition + BIN weights
Input: Letterboxed 640×640 float32 tensor normalized to [0, 1]
Output shape:
(1, 300, 57)— up to 300 detections per image4 values: bounding box (x_center, y_center, width, height)
1 value: detection confidence score
1 value: class ID (ignored; always 0 for person)
51 values: 17 keypoints × 3 (x, y, confidence per keypoint)
Processing pipeline:
Preprocessing: Image resized via letterboxing (maintains aspect ratio, pads with 114 gray); converted to float32 [0, 1]
Inference: OpenVINO compiled model runs on configured device (CPU/GPU via
GST_INFERENCE_DEVICE)Postprocessing:
NMS (Non-Maximum Suppression) filters overlapping detections
Detections with confidence <
POSE_CONFIDENCE_THRESHOLD(default 0.5) are discardedKeypoints with per-keypoint confidence < threshold are zeroed
Highest-confidence detection per frame is selected for analysis
COCO 17-keypoint format (indexes 0–16): Nose, Left Eye, Right Eye, Left Ear, Right Ear, Left Shoulder, Right Shoulder, Left Elbow, Right Elbow, Left Wrist, Right Wrist, Left Hip, Right Hip, Left Knee, Right Knee, Left Ankle, Right Ankle
For feature overview, see Key Features: Pose Extraction.
Pattern Rule Engine#
The PoseRuleEngine evaluates patterns defined in config/patterns.yaml. Patterns are declarative: no code changes required to add new behaviors.
Supported relation types:
Relation |
Semantics |
Example Use |
|---|---|---|
|
Vertical positioning between keypoints |
Wrist above shoulder (reaching) |
|
Horizontal positioning |
Hand left of torso (asymmetry) |
|
Distance relative to torso length (normalized 0–1) |
Wrist near waist (0.4× torso length) |
|
Joint angle (degrees) at a vertex keypoint |
Elbow bent 20–165° |
|
Velocity magnitude between consecutive frames |
Arm moving quickly |
|
Negation of any relation |
Not above (i.e., below or equal) |
Phase-based evaluation:
Patterns are organized into ordered phases representing temporal stages of behavior
Each phase specifies
min_frames: minimum consecutive frames where all conditions holdThe engine performs sliding-window matching: seeks best-matching N-frame partition in the pose sequence where all phases satisfy their constraints in order
If
per_side: truein pattern config: conditions auto-expand into left/right variants (e.g.,elbow_bent→left_elbow_bent,right_elbow_bent)
Example: shelf_to_waist pattern Detects concealment via hand movement from shoulder-height to waist with bent elbow:
patterns:
shelf_to_waist:
enabled: true
alert_type: CONCEALMENT
pose:
phases:
- name: arm_handling_near_body
min_frames: 20
conditions:
- subject: elbow
relation: bent
reference: [shoulder, wrist] # bent at elbow joint
min_angle: 20
max_angle: 165
- subject: wrist
relation: near
reference: waist_midpoint
threshold: 0.40 # within 40% of torso length
When all phases match, the engine returns a PatternResult with matched frames, confidence, and phase details.
For feature overview, see Key Features: Declarative Pattern Engine.
VLM Confirmation#
When a pose pattern matches and the global VLM switch is enabled via VLM_ENABLED=true, the service performs frame-level visual confirmation through an OpenAI-compatible VLM endpoint. Individual pattern vlm.enabled values can further opt a given pattern in or out when needed.
Request pipeline:
Frame selection: Samples up to
num_frameskey frames identified during pose matching; this value is configurable per pattern.Encoding: Each frame is:
Resized to
max_image_size(default 256px, configured viaVLM_MAX_IMAGE_SIZE)Encoded as progressive JPEG (smaller payload vs. PNG)
Converted to base64 for transmission
API call: Sends to OpenAI-compatible endpoint (
VLM_ENDPOINT, default OVMS):{ "model": "<VLM_MODEL_NAME>", "messages": [{"role": "user", "content": [...images..., "<pattern_prompt>"]}], "temperature": <VLM_TEMPERATURE>, "max_tokens": <VLM_MAX_TOKENS> }Response parsing: Expects JSON response:
{"suspicious": boolean, "confidence": float, "reasoning": "string"}
Confidence scoring logic:
If VLM confirms suspicion (suspicious=true): confidence = average(pose_confidence, vlm_confidence)
If VLM disagrees (suspicious=false): confidence = pose_confidence × 0.5 (penalized)
If VLM call fails: pose_confidence returned as-is; vlm_confirmed=null
Resilience mechanisms:
Circuit breaker: VLMClient tracks consecutive failures; opens after 3 failures, auto-recovers after 30s cooldown to prevent request storms against degraded OVMS
Concurrency semaphore:
vlm_max_concurrency(default 1) limits concurrent VLM requests (Semaphore-based) to prevent unbounded fan-inTimeout handling:
VLM_TIMEOUT(configurable) aborts slow requests
For feature overview, see Key Features: VLM Confirmation.
Entity Deduplication and Backpressure#
BAQueueConsumer enforces concurrency constraints using two mechanisms:
1. Entity deduplication:
Implementation: In-memory set
_inflight_entitiestracks person_ids currently being analyzedLogic: When a new
ba/requestarrives for person IDP:If
Pis in_inflight_entities: the request is silently dropped (logged at debug level)If
Pis not in_inflight_entities:Pis added, the analysis task is dispatched, andPis removed when the task completes
Purpose: Prevents duplicate analysis of the same entity in rapid succession (common with high frame-rate upstream systems)
2. Max concurrency backpressure:
Implementation: Counter
_current_analysestracks in-flight analysis tasks (not entities; one entity may spawn multiple phases)Limit:
max_inflight_analyses(default 3, configurable via env var)Logic: When a task would begin:
If
_current_analyses >= max_inflight_analyses: new analysis request is dropped (logged at warning level)Otherwise: task proceeds; counter incremented; decremented upon completion
Purpose: Caps memory usage and SeaweedFS/YOLO pipeline load; prevents a cascade when frame storage is slow
Dropped request handling:
Requests exceeding these constraints are logged but not queued or retried
Upstream should implement its own retry logic or buffer management
No backpressure signal is sent to MQTT publisher (asynchronous; no ACK mechanism defined)
For feature overview, see Key Features: Entity Deduplication & Backpressure.
Error Handling#
Scenario |
Behavior |
|---|---|
SeaweedFS unavailable at startup |
Retries up to 5 times with exponential backoff (2s, 4s, 6s, 8s, 10s) |
No frames in storage |
Returns |
Fewer frames than |
Returns |
No person detected by YOLO |
Returns |
VLM call fails |
Falls back to pose-only result; |
VLM circuit breaker open |
VLM call skipped; pose result returned as-is |
MQTT connection failure |
Logged at error level; paho-mqtt reconnects automatically |
Logging Strategy#
Standard Python
loggingmodule used throughout.Log level defaults to
INFO; configurable viaLOG_LEVELenvironment variable.Key events logged: service startup, frame counts, pose extraction results, pattern match outcomes, VLM calls and results, MQTT connection events, bucket creation, and analysis errors.
Structured log extras (e.g.
person_id,status) are added to MQTT-related log events.
Monitoring / Observability#
Health endpoint:
GET /health— returnsmodel_loadedandseaweedfs_connectedbooleans.Performance metrics:
vlm_metrics_logger(from theintel-retail/performance-toolspackage) logs OVMS inference performance metrics per VLM call.