Refactor constants management to use Pydantic BaseModel for configuration

- Replaced module-level path variables in constants.py with a structured Pydantic Config class.
- Updated all relevant modules (train.py, augmentation.py, exports.py, dataset-visualiser.py, manual_run.py) to access paths through the new config structure.
- Fixed bugs related to image processing and model saving.
- Enhanced test infrastructure to accommodate the new configuration approach.

This refactor improves code maintainability and clarity by centralizing configuration management.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-03-27 18:18:30 +02:00
parent b68c07b540
commit 142c6c4de8
106 changed files with 5706 additions and 654 deletions
@@ -0,0 +1,53 @@
# Component: Core Infrastructure
## Overview
Shared constants and utility classes that form the foundation for all other components. Provides path definitions, config file references, and helper data structures.
**Pattern**: Configuration constants + utility library
**Upstream**: None (leaf component)
**Downstream**: All other components
## Modules
- `constants` — filesystem paths, config keys, thresholds
- `utils` — Dotdict helper class
## Internal Interfaces
### constants (public symbols)
All path/string constants — see module doc for full list. Key exports:
- Directory paths: `data_dir`, `processed_dir`, `datasets_dir`, `models_dir` and their images/labels subdirectories
- Config references: `CONFIG_FILE`, `CDN_CONFIG`, `OFFSET_FILE`
- Model paths: `CURRENT_PT_MODEL`, `CURRENT_ONNX_MODEL`
- Thresholds: `SMALL_SIZE_KB = 3`
### utils.Dotdict
```python
class Dotdict(dict):
# Enables config.url instead of config["url"]
```
## Data Access Patterns
None — pure constants, no I/O.
## Implementation Details
- All paths rooted at `/azaion/` — assumes a fixed deployment directory structure
- No environment-variable override for any path — paths are entirely static
## Caveats
- Hardcoded root `/azaion/` makes local development without that directory structure impossible
- No `.env` or environment-based configuration override mechanism
- `Dotdict.__getattr__` uses `dict.get` which returns `None` for missing keys instead of raising `AttributeError`
## Dependency Graph
```mermaid
graph TD
constants --> api_client_comp[API & CDN]
constants --> training_comp[Training]
constants --> data_pipeline_comp[Data Pipeline]
constants --> inference_comp[Inference]
utils --> training_comp
utils --> inference_comp
```
## Logging Strategy
None.
@@ -0,0 +1,59 @@
# Component: Security & Hardware Identity
## Overview
Provides cryptographic operations (AES-256-CBC encryption/decryption) and hardware fingerprinting. Used for protecting model files in transit and at rest, and for binding API encryption keys to specific machines.
**Pattern**: Utility/service library (static methods)
**Upstream**: None (leaf component)
**Downstream**: API & CDN, Training, Inference
## Modules
- `security` — AES encryption, key derivation (SHA-384), hardcoded model key
- `hardware_service` — cross-platform hardware info collection (CPU, GPU, RAM, drive serial)
## Internal Interfaces
### Security (static methods)
```python
Security.encrypt_to(input_bytes: bytes, key: str) -> bytes
Security.decrypt_to(ciphertext_with_iv: bytes, key: str) -> bytes
Security.calc_hash(key: str) -> str
Security.get_hw_hash(hardware: str) -> str
Security.get_api_encryption_key(creds, hardware_hash: str) -> str
Security.get_model_encryption_key() -> str
```
### hardware_service
```python
get_hardware_info() -> str
```
## Data Access Patterns
- `hardware_service` executes shell commands to query OS/hardware info
- `security` performs in-memory cryptographic operations only
## Implementation Details
- **Encryption**: AES-256-CBC. Key = SHA-256(key_string). IV = 16 random bytes prepended to ciphertext. PKCS7 padding.
- **Key derivation hierarchy**:
1. `get_model_encryption_key()` → hardcoded secret → SHA-384 → base64
2. `get_hw_hash(hardware_string)` → salted hardware string → SHA-384 → base64
3. `get_api_encryption_key(creds, hw_hash)` → email+password+hw_hash+salt → SHA-384 → base64
- **Hardware fingerprint format**: `CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {serial}`
## Caveats
- **Hardcoded model encryption key** in `get_model_encryption_key()` — anyone with source code access can derive the key
- **Shell command injection risk**: `hardware_service` uses `shell=True` subprocess — safe since no user input is involved, but fragile
- **PKCS7 unpadding** in `decrypt_to` uses manual check instead of the `cryptography` library's unpadder — potential padding oracle if error handling is observed
- `BUFFER_SIZE` constant declared but unused in security.py
## Dependency Graph
```mermaid
graph TD
hardware_service --> api_client[API & CDN: api_client]
security --> api_client
security --> training[Training]
security --> inference[Inference: start_inference]
```
## Logging Strategy
None — operations are silent except for exceptions.
@@ -0,0 +1,90 @@
# Component: API & CDN Client
## Overview
Communication layer for the Azaion backend API and S3-compatible CDN. Handles authentication, encrypted file transfer, and the split-resource pattern for secure model distribution.
**Pattern**: Client library with split-storage resource management
**Upstream**: Core (constants), Security (encryption, hardware identity)
**Downstream**: Training, Inference, Exports
## Modules
- `api_client` — REST client for Azaion API, JWT auth, encrypted resource download/upload, split big/small pattern
- `cdn_manager` — boto3 S3 client with separate read/write credentials
## Internal Interfaces
### CDNCredentials
```python
CDNCredentials(host, downloader_access_key, downloader_access_secret, uploader_access_key, uploader_access_secret)
```
### CDNManager
```python
CDNManager(credentials: CDNCredentials)
CDNManager.upload(bucket: str, filename: str, file_bytes: bytearray) -> bool
CDNManager.download(bucket: str, filename: str) -> bool
```
### ApiCredentials
```python
ApiCredentials(url, email, password)
```
### ApiClient
```python
ApiClient()
ApiClient.login() -> None
ApiClient.upload_file(filename: str, file_bytes: bytearray, folder: str) -> None
ApiClient.load_bytes(filename: str, folder: str) -> bytes
ApiClient.load_big_small_resource(resource_name: str, folder: str, key: str) -> bytes
ApiClient.upload_big_small_resource(resource: bytes, resource_name: str, folder: str, key: str) -> None
```
## External API Specification
### Azaion REST API (consumed)
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/login` | POST | None (returns JWT) | `{"email": ..., "password": ...}``{"token": ...}` |
| `/resources/{folder}` | POST | Bearer JWT | Multipart file upload |
| `/resources/get/{folder}` | POST | Bearer JWT | Download encrypted resource (sends hardware info in body) |
### S3-compatible CDN
| Operation | Description |
|-----------|-------------|
| `upload_fileobj` | Upload bytes to S3 bucket |
| `download_file` | Download file from S3 bucket to disk |
## Data Access Patterns
- API Client reads `config.yaml` on init for API credentials
- CDN credentials loaded by API Client from encrypted `cdn.yaml` (downloaded from API)
- Split resources: big part stored locally + CDN, small part on API server
## Implementation Details
- **JWT auto-refresh**: On 401/403 response, automatically re-authenticates and retries
- **Split-resource pattern**: Encrypts data → splits at ~20% (SMALL_SIZE_KB * 1024 min) boundary → small part to API, big part to CDN. Neither part alone can reconstruct the original.
- **CDN credential isolation**: Separate S3 access keys for upload vs download (least-privilege)
- **CDN self-bootstrap**: `cdn.yaml` credentials are themselves encrypted and downloaded from the API during ApiClient init
## Caveats
- Credentials hardcoded in `config.yaml` and `cdn.yaml` — not using environment variables or secrets manager
- `cdn_manager.download()` saves to current working directory with the same filename
- No retry logic beyond JWT refresh (no exponential backoff, no connection retry)
- `CDNManager` imports `sys`, `yaml`, `os` but doesn't use them
## Dependency Graph
```mermaid
graph TD
constants --> api_client
security --> api_client
hardware_service --> api_client
cdn_manager --> api_client
api_client --> exports
api_client --> train
api_client --> start_inference
cdn_manager --> exports
cdn_manager --> train
```
## Logging Strategy
Print statements for upload/download confirmations and errors. No structured logging.
@@ -0,0 +1,61 @@
# Component: Data Models
## Overview
Shared data transfer objects for the training pipeline: annotation class definitions (with weather modes) and image+label containers for visualization and augmentation.
**Pattern**: Plain data classes / value objects
**Upstream**: None (leaf)
**Downstream**: Data Pipeline (augmentation, dataset-visualiser), Training (YAML generation)
## Modules
- `dto/annotationClass` — AnnotationClass, WeatherMode enum, classes.json reader
- `dto/imageLabel` — ImageLabel container with bbox visualization
## Internal Interfaces
### WeatherMode (Enum)
| Member | Value | Description |
|--------|-------|-------------|
| Norm | 0 | Normal weather |
| Wint | 20 | Winter |
| Night | 40 | Night |
### AnnotationClass
```python
AnnotationClass(id: int, name: str, color: str)
AnnotationClass.read_json() -> dict[int, AnnotationClass] # static
AnnotationClass.color_tuple -> tuple # property, RGB ints
```
### ImageLabel
```python
ImageLabel(image_path: str, image: np.ndarray, labels_path: str, labels: list)
ImageLabel.visualize(annotation_classes: dict) -> None
```
## Data Access Patterns
- `AnnotationClass.read_json()` reads `classes.json` from project root (relative to `dto/` parent)
- `ImageLabel.visualize()` renders to matplotlib window (no disk I/O)
## Implementation Details
- 17 base annotation classes × 3 weather modes = 51 classes with offset IDs (016, 2036, 4056)
- System reserves 80 class slots (DEFAULT_CLASS_NUM in train.py)
- YOLO label format: [x_center, y_center, width, height, class_id] — all normalized 01
- `color_tuple` parsing strips first 3 chars (assumes "#ff" prefix format) — fragile if color format changes
## Caveats
- `AnnotationClass` duplicated in 3 locations (dto, inference/dto, annotation-queue/annotation_queue_dto) with slight differences
- `color_tuple` property has a non-obvious parsing approach that may break on different color string formats
- Empty files: `dto/annotation_bulk_message.py` and `dto/annotation_message.py` suggest planned but unimplemented DTOs
## Dependency Graph
```mermaid
graph TD
dto_annotationClass[dto/annotationClass] --> train
dto_annotationClass --> dataset-visualiser
dto_imageLabel[dto/imageLabel] --> augmentation
dto_imageLabel --> dataset-visualiser
```
## Logging Strategy
None.
@@ -0,0 +1,74 @@
# Component: Data Pipeline
## Overview
Tools for preparing and managing annotation data: augmentation of training images, format conversion from external annotation systems, and visual inspection of annotated datasets.
**Pattern**: Batch processing tools (standalone scripts + library)
**Upstream**: Core (constants), Data Models (ImageLabel, AnnotationClass)
**Downstream**: Training (augmented images feed into dataset formation)
## Modules
- `augmentation` — image augmentation pipeline (albumentations)
- `convert-annotations` — Pascal VOC / oriented bbox → YOLO format converter
- `dataset-visualiser` — interactive annotation visualization tool
## Internal Interfaces
### Augmentator
```python
Augmentator()
Augmentator.augment_annotations(from_scratch: bool = False) -> None
Augmentator.augment_inner(img_ann: ImageLabel) -> list[ImageLabel]
Augmentator.correct_bboxes(labels) -> list
Augmentator.read_labels(labels_path) -> list[list]
```
### convert-annotations (functions)
```python
convert(folder, dest_folder, read_annotations, ann_format) -> None
minmax2yolo(width, height, xmin, xmax, ymin, ymax) -> tuple
read_pascal_voc(width, height, s: str) -> list[str]
read_bbox_oriented(width, height, s: str) -> list[str]
```
### dataset-visualiser (functions)
```python
visualise_dataset() -> None
visualise_processed_folder() -> None
```
## Data Access Patterns
- **Augmentation**: Reads from `/azaion/data/images/` + `/azaion/data/labels/`, writes to `/azaion/data-processed/images/` + `/azaion/data-processed/labels/`
- **Conversion**: Reads from user-specified source folder, writes to destination folder
- **Visualiser**: Reads from datasets or processed folder, renders to matplotlib window
## Implementation Details
- **Augmentation pipeline**: Per image → 1 original copy + 7 augmented variants (8× data expansion)
- HorizontalFlip (60%), BrightnessContrast (40%), Affine (80%), MotionBlur (10%), HueSaturation (40%)
- Bbox correction clips outside-boundary boxes, removes boxes < 1% of image
- Incremental: skips already-processed images
- Continuous mode: infinite loop with 5-minute sleep between rounds
- Concurrent: ThreadPoolExecutor for parallel image processing
- **Format conversion**: Pluggable reader pattern — `convert()` accepts any reader function that maps (width, height, text) → YOLO lines
- **Visualiser**: Interactive (waits for keypress) — developer debugging tool
## Caveats
- `dataset-visualiser` imports from `preprocessing` module which does not exist — broken import
- `dataset-visualiser` has hardcoded dataset date (`2024-06-18`) and start index (35247)
- `convert-annotations` hardcodes class mappings (Truck=1, Car/Taxi=2) — not configurable
- Augmentation parameters are hardcoded, not configurable via config file
- Augmentation `total_to_process` attribute referenced in `augment_annotation` but never set (uses `total_images_to_process`)
## Dependency Graph
```mermaid
graph TD
constants --> augmentation
dto_imageLabel[dto/imageLabel] --> augmentation
constants --> dataset-visualiser
dto_annotationClass[dto/annotationClass] --> dataset-visualiser
dto_imageLabel --> dataset-visualiser
augmentation --> manual_run
```
## Logging Strategy
Print statements for progress tracking (processed count, errors). No structured logging.
@@ -0,0 +1,87 @@
# Component: Training Pipeline
## Overview
End-to-end YOLOv11 object detection training workflow: dataset formation from augmented annotations, model training, multi-format export (ONNX, TensorRT, RKNN), and encrypted model upload.
**Pattern**: Pipeline / orchestrator
**Upstream**: Core, Security, API & CDN, Data Models, Data Pipeline (augmented images)
**Downstream**: None (produces trained models consumed externally)
## Modules
- `train` — main pipeline: dataset formation → YOLO training → export → upload
- `exports` — model format conversion (ONNX, TensorRT, RKNN) + upload utilities
- `manual_run` — ad-hoc developer script for selective pipeline steps
## Internal Interfaces
### train
```python
form_dataset() -> None
copy_annotations(images, folder: str) -> None
check_label(label_path: str) -> bool
create_yaml() -> None
resume_training(last_pt_path: str) -> None
train_dataset() -> None
export_current_model() -> None
```
### exports
```python
export_rknn(model_path: str) -> None
export_onnx(model_path: str, batch_size: int = 4) -> None
export_tensorrt(model_path: str) -> None
form_data_sample(destination_path: str, size: int = 500, write_txt_log: bool = False) -> None
show_model(model: str = None) -> None
upload_model(model_path: str, filename: str, size_small_in_kb: int = 3) -> None
```
## Data Access Patterns
- **Input**: Reads augmented images from `/azaion/data-processed/images/` + labels
- **Dataset output**: Creates dated dataset at `/azaion/datasets/azaion-{YYYY-MM-DD}/` with train/valid/test splits
- **Model output**: Saves trained models to `/azaion/models/azaion-{YYYY-MM-DD}/`, copies best.pt to `/azaion/models/azaion.pt`
- **Upload**: Encrypted model uploaded as split big/small to CDN + API
- **Corrupted data**: Invalid labels moved to `/azaion/data-corrupted/`
## Implementation Details
- **Dataset split**: 70% train / 20% valid / 10% test (random shuffle)
- **Label validation**: `check_label()` verifies all YOLO coordinates are ≤ 1.0
- **YAML generation**: Writes `data.yaml` with 80 class names (17 actual from classes.json × 3 weather modes, rest as placeholders)
- **Training config**: YOLOv11 medium (`yolo11m.yaml`), epochs=120, batch=11 (tuned for 24GB VRAM), imgsz=1280, save_period=1, workers=24
- **Post-training**: Removes intermediate epoch checkpoints, keeps only `best.pt`
- **Export chain**: `.pt` → ONNX (1280px, batch=4, NMS) → encrypted → split → upload
- **TensorRT export**: batch=4, FP16, NMS, simplify
- **RKNN export**: targets RK3588 SoC (OrangePi5)
- **Concurrent file copying**: ThreadPoolExecutor for parallel image/label copying during dataset formation
- **`__main__`** in `train.py`: `train_dataset()``export_current_model()`
## Caveats
- Training hyperparameters are hardcoded (not configurable via config file)
- `old_images_percentage = 75` declared but unused
- `train.py` imports `subprocess`, `sleep` but doesn't use them
- `train.py` imports `OnnxEngine` but doesn't use it
- `exports.upload_model()` creates `ApiClient` with different constructor signature than the one in `api_client.py` — likely stale code
- `copy_annotations` uses a global `total_files_copied` counter with a local `copied` variable that stays at 0 — reporting bug
- `resume_training` references `yaml` (the module) instead of a YAML file path in the `data` parameter
## Dependency Graph
```mermaid
graph TD
constants --> train
constants --> exports
api_client --> train
api_client --> exports
cdn_manager --> train
cdn_manager --> exports
security --> train
security --> exports
utils --> train
utils --> exports
dto_annotationClass[dto/annotationClass] --> train
inference_onnx[inference/onnx_engine] --> train
exports --> train
train --> manual_run
augmentation --> manual_run
```
## Logging Strategy
Print statements for progress (file count, shuffling status, training results). No structured logging.
@@ -0,0 +1,85 @@
# Component: Inference Engine
## Overview
Real-time object detection inference subsystem supporting ONNX Runtime and TensorRT backends. Processes video streams with batched inference, custom NMS, and live visualization.
**Pattern**: Strategy pattern (InferenceEngine ABC) + pipeline orchestrator
**Upstream**: Core, Security, API & CDN (for model download)
**Downstream**: None (end-user facing — processes video input)
## Modules
- `inference/dto` — Detection, Annotation, AnnotationClass data classes
- `inference/onnx_engine` — InferenceEngine ABC + OnnxEngine implementation
- `inference/tensorrt_engine` — TensorRTEngine implementation with CUDA memory management + ONNX converter
- `inference/inference` — Video processing pipeline (preprocess → infer → postprocess → draw)
- `start_inference` — Entry point: downloads model, initializes engine, runs on video
## Internal Interfaces
### InferenceEngine (ABC)
```python
InferenceEngine.__init__(model_path: str, batch_size: int = 1, **kwargs)
InferenceEngine.get_input_shape() -> Tuple[int, int]
InferenceEngine.get_batch_size() -> int
InferenceEngine.run(input_data: np.ndarray) -> List[np.ndarray]
```
### OnnxEngine (extends InferenceEngine)
Constructor takes `model_bytes` (not path). Uses CUDAExecutionProvider + CPUExecutionProvider.
### TensorRTEngine (extends InferenceEngine)
Constructor takes `model_bytes: bytes`. Additional static methods:
```python
TensorRTEngine.get_gpu_memory_bytes(device_id=0) -> int
TensorRTEngine.get_engine_filename(device_id=0) -> str | None
TensorRTEngine.convert_from_onnx(onnx_model: bytes) -> bytes | None
```
### Inference
```python
Inference(engine: InferenceEngine, confidence_threshold, iou_threshold)
Inference.preprocess(frames: list) -> np.ndarray
Inference.postprocess(batch_frames, batch_timestamps, output) -> list[Annotation]
Inference.process(video: str) -> None
Inference.draw(annotation: Annotation) -> None
Inference.remove_overlapping_detections(detections) -> list[Detection]
```
## Data Access Patterns
- Model bytes loaded by caller (start_inference via ApiClient.load_big_small_resource)
- Video input via cv2.VideoCapture (file path)
- No disk writes during inference
## Implementation Details
- **Video processing**: Every 4th frame processed (25% frame sampling), batched to engine batch size
- **Preprocessing**: cv2.dnn.blobFromImage (1/255 scale, model input size, BGR→RGB)
- **Postprocessing**: Raw detections filtered by confidence, coordinates normalized to [0,1], custom NMS applied
- **Custom NMS**: Pairwise IoU comparison. Keeps higher confidence; ties broken by lower class ID.
- **TensorRT**: Async CUDA execution (memcpy_htod_async → execute_async_v3 → synchronize → memcpy_dtoh)
- **TensorRT shapes**: Default 1280×1280 input, 300 max detections, 6 values per detection (x1,y1,x2,y2,conf,cls)
- **ONNX conversion**: TensorRT builder with 90% GPU memory workspace, FP16 if supported
- **Engine filename**: GPU-architecture-specific: `azaion.cc_{major}.{minor}_sm_{sm_count}.engine`
- **start_inference flow**: ApiClient → load encrypted TensorRT model (big/small split) → decrypt → TensorRTEngine → Inference.process()
## Caveats
- `start_inference.get_engine_filename()` duplicates `TensorRTEngine.get_engine_filename()`
- Video path hardcoded in `start_inference` (`tests/ForAI_test.mp4`)
- `inference/dto` has its own AnnotationClass — duplicated from `dto/annotationClass`
- cv2.imshow display requires a GUI environment — won't work headless
- TensorRT `batch_size` attribute used before assignment if engine input shape has dynamic batch — potential NameError
## Dependency Graph
```mermaid
graph TD
inference_dto[inference/dto] --> inference_inference[inference/inference]
inference_onnx[inference/onnx_engine] --> inference_inference
inference_onnx --> inference_trt[inference/tensorrt_engine]
inference_trt --> start_inference
inference_inference --> start_inference
constants --> start_inference
api_client --> start_inference
security --> start_inference
```
## Logging Strategy
Print statements for metadata, download progress, timing. cv2.imshow for visual output.
@@ -0,0 +1,71 @@
# Component: Annotation Queue Service
## Overview
Self-contained async service that consumes annotation CRUD events from a RabbitMQ Streams queue and persists images + labels to the filesystem. Operates independently from the training pipeline.
**Pattern**: Message-driven event handler / consumer service
**Upstream**: External RabbitMQ Streams queue (Azaion platform)
**Downstream**: Data Pipeline (files written become input for augmentation)
## Modules
- `annotation-queue/annotation_queue_dto` — message DTOs (AnnotationMessage, AnnotationBulkMessage, AnnotationStatus, Detection, etc.)
- `annotation-queue/annotation_queue_handler` — async queue consumer with message routing and file management
## Internal Interfaces
### AnnotationQueueHandler
```python
AnnotationQueueHandler()
AnnotationQueueHandler.start() -> async
AnnotationQueueHandler.on_message(message: AMQPMessage, context: MessageContext) -> None
AnnotationQueueHandler.save_annotation(ann: AnnotationMessage) -> None
AnnotationQueueHandler.validate(msg: AnnotationBulkMessage) -> None
AnnotationQueueHandler.delete(msg: AnnotationBulkMessage) -> None
```
### Key DTOs
```python
AnnotationMessage(msgpack_bytes) # Full annotation with image + detections
AnnotationBulkMessage(msgpack_bytes) # Bulk validate/delete
AnnotationStatus: Created(10), Edited(20), Validated(30), Deleted(40)
RoleEnum: Operator(10), Validator(20), CompanionPC(30), Admin(40), ApiAdmin(1000)
```
## Data Access Patterns
- **Queue**: Consumes from RabbitMQ Streams queue `azaion-annotations` using rstream library
- **Offset persistence**: `offset.yaml` tracks last processed message offset for resume
- **Filesystem writes**:
- Validated annotations → `{root}/data/images/` + `{root}/data/labels/`
- Unvalidated (seed) → `{root}/data-seed/images/` + `{root}/data-seed/labels/`
- Deleted → `{root}/data_deleted/images/` + `{root}/data_deleted/labels/`
## Implementation Details
- **Message routing**: Based on `AnnotationStatus` from AMQP application properties:
- Created/Edited → save label + optionally image; validator role writes to data, operator to seed
- Validated (bulk) → move from seed to data
- Deleted (bulk) → move to deleted directory
- **Role-based logic**: `RoleEnum.is_validator()` returns True for Validator, Admin, ApiAdmin — these roles write directly to validated data directory
- **Serialization**: Messages are msgpack-encoded with positional integer keys. Detections are embedded as a JSON string within the msgpack payload.
- **Offset tracking**: After each successfully processed message, offset is persisted to `offset.yaml` (survives restarts)
- **Logging**: TimedRotatingFileHandler with daily rotation, 7-day retention, writes to `logs/` directory
- **Separate dependencies**: Own `requirements.txt` (pyyaml, msgpack, rstream only)
- **Own config.yaml**: Points to test directories by default (`data-test`, `data-test-seed`)
## Caveats
- Credentials hardcoded in `config.yaml` (queue host, user, password)
- AnnotationClass duplicated (third copy) with slight differences from dto/ version
- No reconnection logic for queue disconnections
- No dead-letter queue or message retry on processing failures
- `save_annotation` writes empty label files when detections list has no newline separators between entries
- The annotation-queue `config.yaml` uses different directory names (`data-test` vs `data`) than the main `config.yaml` — likely a test vs production configuration issue
## Dependency Graph
```mermaid
graph TD
annotation_queue_dto --> annotation_queue_handler
rstream_ext[rstream library] --> annotation_queue_handler
msgpack_ext[msgpack library] --> annotation_queue_dto
```
## Logging Strategy
`logging` module with TimedRotatingFileHandler. Format: `HH:MM:SS|message`. Daily rotation, 7-day retention. Also outputs to stdout.