[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:
Oleksandr Bezdieniezhnykh
2026-03-31 17:25:58 +03:00
parent e29606c313
commit 1fe9425aa8
12 changed files with 447 additions and 245 deletions
+6 -9
View File
@@ -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
+40 -36
View File
@@ -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
+16 -12
View File
@@ -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`
+60 -84
View File
@@ -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.01.0 |
| centerY | float | Y center, normalized 0.01.0 |
| width | float | Width, normalized 0.01.0 |
| height | float | Height, normalized 0.01.0 |
| classNum | int | Detection class number |
| label | string | Human-readable class name |
| confidence | float | Model confidence 0.01.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)
+50
View File
@@ -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