mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 10:36:32 +00:00
[AZ-172] Update documentation for distributed architecture, add Update Docs step to workflow
- Update module docs: main, inference, ai_config, loader_http_client - Add new module doc: media_hash - Update component docs: inference_pipeline, api - Update system-flows (F2, F3) and data_parameters - Add Task Mode to document skill for incremental doc updates - Insert Step 11 (Update Docs) in existing-code flow, renumber 11-13 to 12-14 Made-with: Cursor
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Data class holding all AI recognition configuration parameters, with factory methods for deserialization from msgpack and dict formats.
|
||||
Data class holding all AI recognition configuration parameters, with factory method for deserialization from dict format.
|
||||
|
||||
## Public Interface
|
||||
|
||||
@@ -18,8 +18,6 @@ Data class holding all AI recognition configuration parameters, with factory met
|
||||
| `tracking_distance_confidence` | double | 0.0 | Distance threshold for tracking (model-width units) |
|
||||
| `tracking_probability_increase` | double | 0.0 | Required confidence increase for tracking update |
|
||||
| `tracking_intersection_threshold` | double | 0.6 | IoU threshold for overlapping detection removal |
|
||||
| `file_data` | bytes | `b''` | Raw file data (msgpack use) |
|
||||
| `paths` | list[str] | `[]` | Media file paths to process |
|
||||
| `model_batch_size` | int | 1 | Batch size for inference |
|
||||
| `big_image_tile_overlap_percent` | int | 20 | Tile overlap percentage for large image splitting |
|
||||
| `altitude` | double | 400 | Camera altitude in meters |
|
||||
@@ -30,18 +28,17 @@ Data class holding all AI recognition configuration parameters, with factory met
|
||||
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `from_msgpack` | `(bytes data) -> AIRecognitionConfig` | Static cdef; deserializes from msgpack binary |
|
||||
| `from_dict` | `(dict data) -> AIRecognitionConfig` | Static def; deserializes from Python dict |
|
||||
| `from_dict` | `(dict data) -> AIRecognitionConfig` | Static cdef; deserializes from Python dict |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
Both factory methods apply defaults for missing keys. `from_msgpack` uses compact single-character keys (`f_pr`, `pt`, `t_dc`, etc.) while `from_dict` uses full descriptive keys.
|
||||
`from_dict` applies defaults for missing keys using full descriptive key names.
|
||||
|
||||
**Legacy/unused**: `from_msgpack()` is defined but never called in the current codebase — it is a remnant of a previous queue-based architecture. Only `from_dict()` is actively used. The `file_data` field is stored but never read anywhere.
|
||||
**Removed**: `paths` field and `file_data` field were removed as part of the distributed architecture shift (AZ-174). Media paths are now resolved via the Annotations service API, not passed in config. `from_msgpack()` was also removed as it was unused.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **External**: `msgpack`
|
||||
- **External**: none
|
||||
- **Internal**: none (leaf module)
|
||||
|
||||
## Consumers
|
||||
@@ -66,4 +63,4 @@ None.
|
||||
|
||||
## Tests
|
||||
|
||||
None found.
|
||||
- `tests/test_ai_config_from_dict.py` — tests `ai_config_from_dict()` helper with defaults and overrides
|
||||
|
||||
@@ -6,32 +6,43 @@ Core inference orchestrator — manages the AI engine lifecycle, preprocesses me
|
||||
|
||||
## Public Interface
|
||||
|
||||
### Free Functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `ai_config_from_dict` | `(dict data) -> AIRecognitionConfig` | Python-callable wrapper around `AIRecognitionConfig.from_dict` |
|
||||
|
||||
### Class: Inference
|
||||
|
||||
#### Fields
|
||||
|
||||
| Field | Type | Access | Description |
|
||||
|-------|------|--------|-------------|
|
||||
| `loader_client` | object | internal | LoaderHttpClient instance |
|
||||
| `loader_client` | LoaderHttpClient | internal | HTTP client for model download/upload |
|
||||
| `engine` | InferenceEngine | internal | Active engine (OnnxEngine or TensorRTEngine), None if unavailable |
|
||||
| `ai_availability_status` | AIAvailabilityStatus | public | Current AI readiness status |
|
||||
| `stop_signal` | bool | internal | Flag to abort video processing |
|
||||
| `model_width` | int | internal | Model input width in pixels |
|
||||
| `model_height` | int | internal | Model input height in pixels |
|
||||
| `detection_counts` | dict[str, int] | internal | Per-media detection count |
|
||||
| `is_building_engine` | bool | internal | True during async TensorRT conversion |
|
||||
|
||||
#### Properties
|
||||
|
||||
| Property | Return Type | Description |
|
||||
|----------|-------------|-------------|
|
||||
| `is_engine_ready` | bool | True if engine is not None |
|
||||
| `engine_name` | str or None | Engine type name from the active engine |
|
||||
|
||||
#### Methods
|
||||
|
||||
| Method | Signature | Access | Description |
|
||||
|--------|-----------|--------|-------------|
|
||||
| `__init__` | `(loader_client)` | public | Initializes state, calls `init_ai()` |
|
||||
| `run_detect` | `(dict config_dict, annotation_callback, status_callback=None)` | cpdef | Main entry: parses config, separates images/videos, processes each |
|
||||
| `detect_single_image` | `(bytes image_bytes, dict config_dict) -> list` | cpdef | Single-image detection from raw bytes, returns list[Detection] |
|
||||
| `run_detect_image` | `(bytes image_bytes, AIRecognitionConfig ai_config, str media_name, annotation_callback, status_callback=None)` | cpdef | Decodes image from bytes, runs tiling + inference + postprocessing |
|
||||
| `run_detect_video` | `(bytes video_bytes, AIRecognitionConfig ai_config, str media_name, str save_path, annotation_callback, status_callback=None)` | cpdef | Processes video from in-memory bytes via PyAV, concurrently writes to save_path |
|
||||
| `stop` | `()` | cpdef | Sets stop_signal to True |
|
||||
| `init_ai` | `()` | cdef | Engine initialization: tries TensorRT engine file → falls back to ONNX → background TensorRT conversion |
|
||||
| `preprocess` | `(frames) -> ndarray` | cdef | OpenCV blobFromImage: resize, normalize to 0..1, swap RGB, stack batch |
|
||||
| `postprocess` | `(output, ai_config) -> list[list[Detection]]` | cdef | Parses engine output to Detection objects, applies confidence threshold and overlap removal |
|
||||
| `init_ai` | `()` | cdef | Engine initialization: tries TensorRT → falls back to ONNX → background TensorRT conversion |
|
||||
| `preprocess` | `(frames) -> ndarray` | via engine | OpenCV blobFromImage: resize, normalize to 0..1, swap RGB, stack batch |
|
||||
| `postprocess` | `(output, ai_config) -> list[list[Detection]]` | via engine | Parses engine output to Detection objects, applies confidence threshold and overlap removal |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
@@ -42,36 +53,27 @@ Core inference orchestrator — manages the AI engine lifecycle, preprocesses me
|
||||
3. If download fails → download ONNX model, start background thread for ONNX→TensorRT conversion
|
||||
4. If no GPU → load OnnxEngine from ONNX model bytes
|
||||
|
||||
### Preprocessing
|
||||
### Stream-Based Media Processing (AZ-173)
|
||||
|
||||
- `cv2.dnn.blobFromImage`: scale 1/255, resize to model dims, BGR→RGB, no crop
|
||||
- Stack multiple frames via `np.vstack` for batched inference
|
||||
Both `run_detect_image` and `run_detect_video` accept raw bytes instead of file paths. This supports the distributed architecture where media arrives as HTTP uploads or is read from storage by the API layer.
|
||||
|
||||
### Postprocessing
|
||||
### Image Processing (`run_detect_image`)
|
||||
|
||||
- Engine output format: `[batch][detection_index][x1, y1, x2, y2, confidence, class_id]`
|
||||
- Coordinates normalized to 0..1 by dividing by model width/height
|
||||
- Converted to center-format (cx, cy, w, h) Detection objects
|
||||
- Filtered by `probability_threshold`
|
||||
- Overlapping detections removed via `remove_overlapping_detections` (greedy, keeps higher confidence)
|
||||
1. Decodes image bytes via `cv2.imdecode`
|
||||
2. Small images (≤1.5× model size): processed as single frame
|
||||
3. Large images: split into tiles based on GSD. Tile size = `METERS_IN_TILE / GSD` pixels. Tiles overlap by configurable percentage.
|
||||
4. Tile deduplication: absolute-coordinate comparison across adjacent tiles
|
||||
5. Size filtering: detections exceeding `AnnotationClass.max_object_size_meters` are removed
|
||||
|
||||
### Image Processing
|
||||
### Video Processing (`run_detect_video`)
|
||||
|
||||
- Small images (≤1.5× model size): processed as single frame
|
||||
- Large images: split into tiles based on ground sampling distance. Tile size = `METERS_IN_TILE / GSD` pixels. Tiles overlap by configurable percentage.
|
||||
- Tile deduplication: absolute-coordinate comparison across adjacent tiles using `Detection.__eq__`
|
||||
- Size filtering: detections whose physical size (meters) exceeds `AnnotationClass.max_object_size_meters` are removed. Physical size computed from GSD × pixel dimensions.
|
||||
|
||||
### Video Processing
|
||||
|
||||
- Frame sampling: every Nth frame (`frame_period_recognition`)
|
||||
- Batch accumulation up to engine batch size
|
||||
- Annotation validity: must differ from previous annotation by either:
|
||||
- Time gap ≥ `frame_recognition_seconds`
|
||||
- More detections than previous
|
||||
- Any detection moved beyond `tracking_distance_confidence` threshold
|
||||
- Any detection confidence increased beyond `tracking_probability_increase`
|
||||
- Valid frames get JPEG-encoded image attached
|
||||
1. Concurrently writes raw bytes to `save_path` in a background thread (for persistent storage)
|
||||
2. Opens video from in-memory `BytesIO` via PyAV (`av.open`)
|
||||
3. Decodes frames via `container.decode(vstream)` — no temporary file needed for reading
|
||||
4. Frame sampling: every Nth frame (`frame_period_recognition`)
|
||||
5. Batch accumulation up to engine batch size
|
||||
6. Annotation validity heuristics (time gap, detection count increase, spatial movement, confidence improvement)
|
||||
7. Valid frames get JPEG-encoded image attached
|
||||
|
||||
### Ground Sampling Distance (GSD)
|
||||
|
||||
@@ -79,12 +81,12 @@ Core inference orchestrator — manages the AI engine lifecycle, preprocesses me
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **External**: `cv2`, `numpy`, `pynvml`, `mimetypes`, `pathlib`, `threading`
|
||||
- **External**: `cv2`, `numpy`, `av` (PyAV), `io`, `threading`
|
||||
- **Internal**: `constants_inf`, `ai_availability_status`, `annotation`, `ai_config`, `tensorrt_engine` (conditional), `onnx_engine` (conditional), `inference_engine` (type)
|
||||
|
||||
## Consumers
|
||||
|
||||
- `main` — lazy-initializes Inference, calls `run_detect`, `detect_single_image`, reads `ai_availability_status`
|
||||
- `main` — lazy-initializes Inference, calls `run_detect_image`/`run_detect_video`, reads `ai_availability_status` and `is_engine_ready`
|
||||
|
||||
## Data Models
|
||||
|
||||
@@ -104,4 +106,6 @@ None.
|
||||
|
||||
## Tests
|
||||
|
||||
None found.
|
||||
- `tests/test_ai_config_from_dict.py` — tests `ai_config_from_dict` helper
|
||||
- `e2e/tests/test_video.py` — exercises `run_detect_video` via the full API
|
||||
- `e2e/tests/test_single_image.py` — exercises `run_detect_image` via the full API
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
HTTP client for downloading and uploading model files (and other binary resources) via an external Loader microservice.
|
||||
HTTP client for downloading/uploading model files via the Loader service, and for querying the Annotations service API (user AI settings, media path resolution).
|
||||
|
||||
## Public Interface
|
||||
|
||||
@@ -17,16 +17,19 @@ Simple result wrapper.
|
||||
|
||||
### Class: LoaderHttpClient
|
||||
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `__init__` | `(str base_url)` | Stores base URL, strips trailing slash |
|
||||
| `load_big_small_resource` | `(str filename, str directory) -> LoadResult` | POST to `/load/{filename}` with JSON body `{filename, folder}`, returns raw bytes |
|
||||
| `upload_big_small_resource` | `(bytes content, str filename, str directory) -> LoadResult` | POST to `/upload/{filename}` with multipart file + form data `{folder}` |
|
||||
| `stop` | `() -> None` | No-op placeholder |
|
||||
| Method | Signature | Access | Description |
|
||||
|--------|-----------|--------|-------------|
|
||||
| `__init__` | `(str base_url)` | public | Stores base URL, strips trailing slash |
|
||||
| `load_big_small_resource` | `(str filename, str directory) -> LoadResult` | cdef | POST to `/load/{filename}` with JSON body, returns raw bytes |
|
||||
| `upload_big_small_resource` | `(bytes content, str filename, str directory) -> LoadResult` | cdef | POST to `/upload/{filename}` with multipart file |
|
||||
| `fetch_user_ai_settings` | `(str user_id, str bearer_token) -> object` | cpdef | GET `/api/users/{user_id}/ai-settings`, returns parsed JSON dict or None |
|
||||
| `fetch_media_path` | `(str media_id, str bearer_token) -> object` | cpdef | GET `/api/media/{media_id}`, returns `path` string from response or None |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
Both load/upload methods wrap all exceptions into `LoadResult(err=str(e))`. Errors are logged via loguru but never raised.
|
||||
Model load/upload methods wrap all exceptions into `LoadResult(err=str(e))`. Errors are logged via loguru but never raised.
|
||||
|
||||
`fetch_user_ai_settings` and `fetch_media_path` (added in AZ-174) call the Annotations service API with Bearer auth. On non-200 response or exception, they return None.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -36,7 +39,7 @@ Both load/upload methods wrap all exceptions into `LoadResult(err=str(e))`. Erro
|
||||
## Consumers
|
||||
|
||||
- `inference` — downloads ONNX/TensorRT models, uploads converted TensorRT engines
|
||||
- `main` — instantiates client with `LOADER_URL`
|
||||
- `main` — instantiates two clients: one for Loader (`LOADER_URL`), one for Annotations (`ANNOTATIONS_URL`). Uses `fetch_user_ai_settings` and `fetch_media_path` on the annotations client.
|
||||
|
||||
## Data Models
|
||||
|
||||
@@ -44,18 +47,19 @@ Both load/upload methods wrap all exceptions into `LoadResult(err=str(e))`. Erro
|
||||
|
||||
## Configuration
|
||||
|
||||
- `base_url` — provided at construction time, sourced from `LOADER_URL` environment variable in `main.py`
|
||||
- `base_url` — provided at construction time, sourced from env vars in `main.py`
|
||||
|
||||
## External Integrations
|
||||
|
||||
| Integration | Protocol | Endpoint Pattern |
|
||||
|-------------|----------|-----------------|
|
||||
| Loader service | HTTP POST | `/load/{filename}` (download), `/upload/{filename}` (upload) |
|
||||
| Annotations service | HTTP GET | `/api/users/{user_id}/ai-settings`, `/api/media/{media_id}` |
|
||||
|
||||
## Security
|
||||
|
||||
None (no auth headers sent to loader).
|
||||
Bearer token forwarded in Authorization header for Annotations service calls.
|
||||
|
||||
## Tests
|
||||
|
||||
None found.
|
||||
- `tests/test_az174_db_driven_config.py` — tests `_resolve_media_for_detect` which exercises `fetch_user_ai_settings` and `fetch_media_path`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
FastAPI application entry point — exposes HTTP API for object detection on images and video media, health checks, and Server-Sent Events (SSE) streaming of detection results.
|
||||
FastAPI application entry point — exposes HTTP API for object detection on images and video media, health checks, and Server-Sent Events (SSE) streaming of detection results. Manages media lifecycle (content hashing, persistent storage, media record creation, status updates) and DB-driven AI configuration.
|
||||
|
||||
## Public Interface
|
||||
|
||||
@@ -11,8 +11,8 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/health` | Returns AI engine availability status |
|
||||
| POST | `/detect` | Single image detection (multipart file upload) |
|
||||
| POST | `/detect/{media_id}` | Start async detection on media from loader service |
|
||||
| POST | `/detect` | Image/video detection with media lifecycle management |
|
||||
| POST | `/detect/{media_id}` | Start async detection on media resolved from Annotations service |
|
||||
| GET | `/detect/stream` | SSE stream of detection events |
|
||||
|
||||
### DTOs (Pydantic Models)
|
||||
@@ -21,8 +21,8 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
|
||||
|-------|--------|-------------|
|
||||
| `DetectionDto` | centerX, centerY, width, height, classNum, label, confidence | Single detection result |
|
||||
| `DetectionEvent` | annotations (list[DetectionDto]), mediaId, mediaStatus, mediaPercent | SSE event payload |
|
||||
| `HealthResponse` | status, aiAvailability, errorMessage | Health check response |
|
||||
| `AIConfigDto` | frame_period_recognition, frame_recognition_seconds, probability_threshold, tracking_*, model_batch_size, big_image_tile_overlap_percent, altitude, focal_length, sensor_width, paths | Configuration input for media detection |
|
||||
| `HealthResponse` | status, aiAvailability, engineType, errorMessage | Health check response |
|
||||
| `AIConfigDto` | frame_period_recognition, frame_recognition_seconds, probability_threshold, tracking_*, model_batch_size, big_image_tile_overlap_percent, altitude, focal_length, sensor_width | Configuration input (no `paths` field — removed in AZ-174) |
|
||||
|
||||
### Class: TokenManager
|
||||
|
||||
@@ -30,31 +30,55 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
|
||||
|--------|-----------|-------------|
|
||||
| `__init__` | `(str access_token, str refresh_token)` | Stores tokens |
|
||||
| `get_valid_token` | `() -> str` | Returns access_token; auto-refreshes if expiring within 60s |
|
||||
| `decode_user_id` | `(str token) -> Optional[str]` | Static. Extracts user ID from JWT claims (sub, userId, user_id, nameid, or SAML nameidentifier) |
|
||||
|
||||
### Helper Functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `_merged_annotation_settings_payload` | `(raw: object) -> dict` | Merges nested AI settings from Annotations service response (handles `aiRecognitionSettings`, `cameraSettings` sub-objects and PascalCase/camelCase/snake_case aliases) |
|
||||
| `_resolve_media_for_detect` | `(media_id, token_mgr, override) -> tuple[dict, str]` | Fetches user AI settings + media path from Annotations service, merges with client overrides |
|
||||
| `_detect_upload_kind` | `(filename, data) -> tuple[str, str]` | Determines if upload is image or video by extension, falls back to content probing (cv2/PyAV) |
|
||||
| `_post_media_record` | `(payload, bearer) -> bool` | Creates media record via `POST /api/media` on Annotations service |
|
||||
| `_put_media_status` | `(media_id, status, bearer) -> bool` | Updates media status via `PUT /api/media/{media_id}/status` on Annotations service |
|
||||
| `compute_media_content_hash` | (imported from `media_hash`) | XxHash64 content hash with sampling |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### `/health`
|
||||
|
||||
Returns `HealthResponse` with `status="healthy"` always. `aiAvailability` reflects the engine's `AIAvailabilityStatus`. On exception, returns `aiAvailability="None"`.
|
||||
Returns `HealthResponse` with `status="healthy"` always. `aiAvailability` reflects the engine's `AIAvailabilityStatus`. `engineType` reports the active engine name. On exception, returns `aiAvailability="None"`.
|
||||
|
||||
### `/detect` (single image)
|
||||
### `/detect` (unified upload — AZ-173, AZ-175)
|
||||
|
||||
1. Reads uploaded file bytes
|
||||
2. Parses optional JSON config
|
||||
3. Runs `inference.detect_single_image` in ThreadPoolExecutor (max 2 workers)
|
||||
4. Returns list of DetectionDto
|
||||
1. Reads uploaded file bytes, rejects empty
|
||||
2. Detects kind (image/video) via `_detect_upload_kind` (extension → content probe)
|
||||
3. Validates image data with `cv2.imdecode` if kind is image
|
||||
4. Parses optional JSON config
|
||||
5. Extracts auth tokens; if authenticated:
|
||||
a. Computes XxHash64 content hash
|
||||
b. Persists file to `VIDEOS_DIR` or `IMAGES_DIR`
|
||||
c. Creates media record via `POST /api/media`
|
||||
d. Sets status to `AI_PROCESSING` via `PUT /api/media/{id}/status`
|
||||
6. Runs `run_detect_image` or `run_detect_video` in ThreadPoolExecutor
|
||||
7. On success: sets status to `AI_PROCESSED`
|
||||
8. On failure: sets status to `ERROR`
|
||||
9. Returns list of `DetectionDto`
|
||||
|
||||
Error mapping: RuntimeError("not available") → 503, RuntimeError → 422, ValueError → 400.
|
||||
### `/detect/{media_id}` (async — AZ-174)
|
||||
|
||||
### `/detect/{media_id}` (async media)
|
||||
|
||||
1. Checks for duplicate active detection (409 if already running)
|
||||
2. Extracts auth tokens from Authorization header and x-refresh-token header
|
||||
3. Creates `asyncio.Task` for background detection
|
||||
4. Detection runs `inference.run_detect` in ThreadPoolExecutor
|
||||
5. Callbacks push `DetectionEvent` to all SSE queues
|
||||
6. If auth token present, also POSTs annotations to the Annotations service
|
||||
7. Returns immediately: `{"status": "started", "mediaId": media_id}`
|
||||
1. Checks for duplicate active detection (409)
|
||||
2. Extracts auth tokens
|
||||
3. Resolves media via `_resolve_media_for_detect`:
|
||||
a. Fetches user AI settings from `GET /api/users/{user_id}/ai-settings`
|
||||
b. Merges with client overrides
|
||||
c. Fetches media path from `GET /api/media/{media_id}`
|
||||
4. Reads file bytes from resolved path
|
||||
5. Creates `asyncio.Task` for background detection
|
||||
6. Calls `run_detect_video` or `run_detect_image` depending on file extension
|
||||
7. Callbacks push `DetectionEvent` to SSE queues and POST annotations to Annotations service
|
||||
8. Updates media status via `PUT /api/media/{id}/status`
|
||||
9. Returns immediately: `{"status": "started", "mediaId": media_id}`
|
||||
|
||||
### `/detect/stream` (SSE)
|
||||
|
||||
@@ -64,73 +88,18 @@ Error mapping: RuntimeError("not available") → 503, RuntimeError → 422, Valu
|
||||
|
||||
### Token Management
|
||||
|
||||
- Decodes JWT exp claim from base64 payload (no signature verification)
|
||||
- `_decode_exp`: Decodes JWT exp claim from base64 payload (no signature verification)
|
||||
- Auto-refreshes via POST to `{ANNOTATIONS_URL}/auth/refresh` when within 60s of expiry
|
||||
- `decode_user_id`: Extracts user identity from multiple possible JWT claim keys
|
||||
|
||||
### Annotations Service Integration
|
||||
|
||||
Detections posts results to the Annotations service (`POST {ANNOTATIONS_URL}/annotations`) server-to-server during async media detection (F3). This only happens when an auth token is present in the original request.
|
||||
|
||||
**Endpoint:** `POST {ANNOTATIONS_URL}/annotations`
|
||||
|
||||
**Headers:**
|
||||
|
||||
| Header | Value |
|
||||
|--------|-------|
|
||||
| Authorization | `Bearer {accessToken}` (forwarded from the original client request) |
|
||||
| Content-Type | `application/json` |
|
||||
|
||||
**Request body — payload sent by Detections:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| mediaId | string | ID of the media being processed |
|
||||
| source | int | `0` (AnnotationSource.AI) |
|
||||
| videoTime | string | Video playback position formatted from ms as `"HH:MM:SS"` — mapped to `Annotations.Time` |
|
||||
| detections | list | Detection results for this batch (see below) |
|
||||
| image | string (base64) | Optional — base64-encoded frame image bytes |
|
||||
|
||||
`userId` is not included in the payload. The Annotations service resolves the user identity from the Bearer JWT.
|
||||
|
||||
**Detection object (as sent by Detections):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| centerX | float | X center, normalized 0.0–1.0 |
|
||||
| centerY | float | Y center, normalized 0.0–1.0 |
|
||||
| width | float | Width, normalized 0.0–1.0 |
|
||||
| height | float | Height, normalized 0.0–1.0 |
|
||||
| classNum | int | Detection class number |
|
||||
| label | string | Human-readable class name |
|
||||
| confidence | float | Model confidence 0.0–1.0 |
|
||||
|
||||
The Annotations API contract (`CreateAnnotationRequest`) also accepts `description` (string), `affiliation` (AffiliationEnum), and `combatReadiness` (CombatReadinessEnum) on each Detection, but the Detections service does not populate these — the Annotations service uses defaults.
|
||||
|
||||
**Responses from Annotations service:**
|
||||
|
||||
| Status | Condition |
|
||||
|--------|-----------|
|
||||
| 201 | Annotation created |
|
||||
| 400 | Neither image nor mediaId provided |
|
||||
| 404 | MediaId not found in Annotations DB |
|
||||
|
||||
**Failure handling:** POST failures are silently caught — detection processing continues regardless. Annotations that fail to post are not retried.
|
||||
|
||||
**Downstream pipeline (Annotations service side):**
|
||||
|
||||
1. Saves annotation to local PostgreSQL (image → XxHash64 ID, label file in YOLO format)
|
||||
2. Publishes SSE event to UI via `GET /annotations/events`
|
||||
3. Enqueues annotation ID to `annotations_queue_records` buffer table (unless SilentDetection mode is enabled in system settings)
|
||||
4. `FailsafeProducer` (BackgroundService) drains the buffer to RabbitMQ Stream (`azaion-annotations`) using MessagePack + Gzip
|
||||
|
||||
**Token refresh for long-running video:**
|
||||
|
||||
For video detection that may outlast the JWT lifetime, the `TokenManager` auto-refreshes via `POST {ANNOTATIONS_URL}/auth/refresh` when the token is within 60s of expiry. The refresh token is provided by the client in the `X-Refresh-Token` request header.
|
||||
Detections posts results to `POST {ANNOTATIONS_URL}/annotations` during async media detection (F3). Media lifecycle (create record, update status) uses `POST /api/media` and `PUT /api/media/{media_id}/status`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **External**: `asyncio`, `base64`, `json`, `os`, `time`, `concurrent.futures`, `typing`, `requests`, `fastapi`, `pydantic`
|
||||
- **Internal**: `inference` (lazy import), `constants_inf` (label lookup), `loader_http_client` (client instantiation)
|
||||
- **External**: `asyncio`, `base64`, `io`, `json`, `os`, `tempfile`, `time`, `concurrent.futures`, `pathlib`, `typing`, `av`, `cv2`, `numpy`, `requests`, `fastapi`, `pydantic`
|
||||
- **Internal**: `inference` (lazy import), `constants_inf` (label lookup), `loader_http_client` (client instantiation), `media_hash` (content hashing)
|
||||
|
||||
## Consumers
|
||||
|
||||
@@ -147,22 +116,29 @@ None (entry point).
|
||||
|---------|---------|-------------|
|
||||
| `LOADER_URL` | `http://loader:8080` | Loader service base URL |
|
||||
| `ANNOTATIONS_URL` | `http://annotations:8080` | Annotations service base URL |
|
||||
| `VIDEOS_DIR` | `{cwd}/data/videos` | Persistent video storage directory |
|
||||
| `IMAGES_DIR` | `{cwd}/data/images` | Persistent image storage directory |
|
||||
|
||||
## External Integrations
|
||||
|
||||
| Service | Protocol | Purpose |
|
||||
|---------|----------|---------|
|
||||
| Loader | HTTP (via LoaderHttpClient) | Model loading |
|
||||
| Annotations | HTTP POST | Auth refresh (`/auth/refresh`), annotation posting (`/annotations`) |
|
||||
| Annotations | HTTP GET | User AI settings (`/api/users/{id}/ai-settings`), media path resolution (`/api/media/{id}`) |
|
||||
| Annotations | HTTP POST | Annotation posting (`/annotations`), media record creation (`/api/media`) |
|
||||
| Annotations | HTTP PUT | Media status updates (`/api/media/{id}/status`) |
|
||||
| Annotations | HTTP POST | Auth refresh (`/auth/refresh`) |
|
||||
|
||||
## Security
|
||||
|
||||
- Bearer token from request headers, refreshed via Annotations service
|
||||
- JWT exp decoded (base64, no signature verification) — token validation is not performed locally
|
||||
- Image data validated via `cv2.imdecode` before processing
|
||||
- No CORS configuration
|
||||
- No rate limiting
|
||||
- No input validation on media_id path parameter beyond string type
|
||||
|
||||
## Tests
|
||||
|
||||
None found.
|
||||
- `tests/test_az174_db_driven_config.py` — `decode_user_id`, `_merged_annotation_settings_payload`, `_resolve_media_for_detect`
|
||||
- `tests/test_az175_api_calls.py` — `_post_media_record`, `_put_media_status`
|
||||
- `e2e/tests/test_*.py` — full API e2e tests (health, single image, video, async, SSE, negative, security, performance, resilience)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Module: media_hash
|
||||
|
||||
## Purpose
|
||||
|
||||
Content-based hashing for media files using XxHash64 with a deterministic sampling algorithm. Produces a stable, unique ID for any media file based on its content.
|
||||
|
||||
## Public Interface
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `compute_media_content_hash` | `(data: bytes, virtual: bool = False) -> str` | Returns hex XxHash64 digest of sampled content. If `virtual=True`, prefixes with "V". |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### Sampling Algorithm (`_sampling_payload`)
|
||||
|
||||
- **Small files** (< 3072 bytes): uses entire content
|
||||
- **Large files** (≥ 3072 bytes): samples 3 × 1024-byte windows: first 1024, middle 1024, last 1024
|
||||
- All payloads are prefixed with the 8-byte little-endian file size for collision resistance
|
||||
|
||||
The sampling avoids reading the full file through the hash function while still providing high uniqueness — the head, middle, and tail capture format headers, content, and EOF markers.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **External**: `xxhash` (pinned at 3.5.0 in requirements.txt)
|
||||
- **Internal**: none (leaf module)
|
||||
|
||||
## Consumers
|
||||
|
||||
- `main` — computes content hash for uploaded media in `POST /detect` to use as the media record ID and storage filename
|
||||
|
||||
## Data Models
|
||||
|
||||
None.
|
||||
|
||||
## Configuration
|
||||
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
|
||||
None.
|
||||
|
||||
## Security
|
||||
|
||||
None. The hash is non-cryptographic (fast, not tamper-resistant).
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/test_media_hash.py` — covers small files, large files, and virtual prefix behavior
|
||||
Reference in New Issue
Block a user