[AZ-178] Implement streaming video detection endpoint

- Added `/detect/video` endpoint for true streaming video detection, allowing inference to start as upload bytes arrive.
- Introduced `run_detect_video_stream` method in the inference module to handle video processing from a file-like object.
- Updated media hashing to include a new function for computing hashes directly from files with minimal I/O.
- Enhanced documentation to reflect changes in video processing and API behavior.

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-01 03:11:43 +03:00
parent e65d8da6a3
commit be4cab4fcb
42 changed files with 2983 additions and 29 deletions
+24 -6
View File
@@ -11,7 +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` | Image/video detection with media lifecycle management |
| POST | `/detect` | Image/video detection with media lifecycle management (buffered) |
| POST | `/detect/video` | Streaming video detection — inference starts as upload bytes arrive (AZ-178) |
| POST | `/detect/{media_id}` | Start async detection on media resolved from Annotations service |
| GET | `/detect/stream` | SSE stream of detection events |
@@ -41,7 +42,8 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
| `_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 |
| `compute_media_content_hash` | (imported from `media_hash`) | XxHash64 content hash with sampling (from bytes) |
| `compute_media_content_hash_from_file` | (imported from `media_hash`) | XxHash64 content hash from file on disk — reads only 3 KB |
## Internal Logic
@@ -57,9 +59,10 @@ Returns `HealthResponse` with `status="healthy"` always. `aiAvailability` reflec
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`
b. For images: persists file to `IMAGES_DIR` synchronously (since `run_detect_image` does not write to disk)
c. For videos: file path is prepared but writing is deferred to `run_detect_video` which writes concurrently with frame detection (AZ-177)
d. Creates media record via `POST /api/media`
e. 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`
@@ -80,6 +83,20 @@ Returns `HealthResponse` with `status="healthy"` always. `aiAvailability` reflec
8. Updates media status via `PUT /api/media/{id}/status`
9. Returns immediately: `{"status": "started", "mediaId": media_id}`
### `/detect/video` (streaming upload — AZ-178)
1. Parses `X-Filename`, `X-Config`, auth headers (no multipart — raw binary body)
2. Validates video extension
3. Creates `StreamingBuffer` backed by a temp file in `VIDEOS_DIR`
4. Starts inference thread via `run_in_executor`: `run_detect_video_stream(buffer, ...)`
5. Reads HTTP body chunks via `request.stream()`, feeds each to `buffer.append()` via executor
6. Inference thread reads from the same buffer concurrently — PyAV decodes frames as data arrives
7. Detections are broadcast to SSE queues in real-time during upload
8. After upload completes: signals EOF, computes content hash from temp file (3 KB read), renames to permanent path
9. If authenticated: creates media record, tracks status lifecycle
10. Returns `{"status": "started", "mediaId": "..."}` — inference continues in background task
11. Background task awaits inference completion, updates status to AI_PROCESSED or Error
### `/detect/stream` (SSE)
- Creates asyncio.Queue per client (maxsize=100)
@@ -99,7 +116,7 @@ Detections posts results to `POST {ANNOTATIONS_URL}/annotations` during async me
## Dependencies
- **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)
- **Internal**: `inference` (lazy import), `constants_inf` (label lookup), `loader_http_client` (client instantiation), `media_hash` (content hashing), `streaming_buffer` (streaming video upload)
## Consumers
@@ -141,4 +158,5 @@ None (entry point).
- `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`
- `tests/test_az177_video_single_write.py` — video single-write, image unchanged, concurrent writer thread, temp cleanup
- `e2e/tests/test_*.py` — full API e2e tests (health, single image, video, async, SSE, negative, security, performance, resilience)