mirror of
https://github.com/azaion/detections-semantic.git
synced 2026-04-22 20:26:38 +00:00
Initial commit
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# OutputManager
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Handles all persistent output: detection logging (JSON-lines), frame recording (JPEG), health logging, gimbal command logging, and operator detection delivery. Manages NVMe write operations and circular buffer for storage.
|
||||
|
||||
**Architectural Pattern**: Facade over multiple output writers (async file I/O).
|
||||
|
||||
**Upstream dependencies**: Config helper (output paths, recording rates, storage limits), Types helper
|
||||
|
||||
**Downstream consumers**: ScanController
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Interface: OutputManager
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `init(output_dir)` | str | — | No | IOError |
|
||||
| `log_detection(entry)` | DetectionLogEntry dict | — | No (non-blocking write) | WriteError |
|
||||
| `record_frame(frame, frame_id, level)` | numpy, uint64, int | — | No (non-blocking write) | WriteError |
|
||||
| `log_health(health)` | HealthLogEntry dict | — | No | WriteError |
|
||||
| `log_gimbal_command(cmd_str)` | str | — | No | WriteError |
|
||||
| `report_to_operator(detections)` | list[Detection] | — | No | — |
|
||||
| `get_storage_status()` | — | StorageStatus | No | — |
|
||||
|
||||
**StorageStatus**:
|
||||
```
|
||||
nvme_free_pct: float (0-100)
|
||||
frames_recorded: uint64
|
||||
detections_logged: uint64
|
||||
should_reduce_recording: bool — true if free < 20%
|
||||
```
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Storage Estimates
|
||||
|
||||
| Output | Write Rate | Per Hour | Per 4h Flight |
|
||||
|--------|-----------|----------|---------------|
|
||||
| detections.jsonl | ~1 KB/det, ~100 det/min | ~6 MB | ~24 MB |
|
||||
| frames/ (L1, 2 FPS) | ~100 KB/frame | ~720 MB | ~2.9 GB |
|
||||
| frames/ (L2, 30 FPS) | ~100 KB/frame | ~10.8 GB | ~43 GB |
|
||||
| health.jsonl | ~200 B/s | ~720 KB | ~3 MB |
|
||||
| gimbal.log | ~500 B/s | ~1.8 MB | ~7 MB |
|
||||
|
||||
### Circular Buffer Strategy
|
||||
|
||||
When NVMe free space < 20%:
|
||||
1. Signal ScanController via `should_reduce_recording`
|
||||
2. ScanController switches to L1 recording rate only
|
||||
3. If still < 10%: stop L1 frame recording, keep detection log only
|
||||
4. Never overwrite detection logs (most valuable data)
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**File Writers**:
|
||||
- Detection log: open file handle, append JSON line, flush periodically (every 10 entries or 5s)
|
||||
- Frame recorder: JPEG encode via OpenCV, write to sequential filename `{frame_id}.jpg`
|
||||
- Health log: append JSON line every 1s
|
||||
- Gimbal log: append text line per command
|
||||
|
||||
**Operator Delivery**: Format detections into existing YOLO output schema (centerX, centerY, width, height, classNum, label, confidence) and make available via the same interface the existing YOLO pipeline uses.
|
||||
|
||||
**Key Dependencies**:
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| OpenCV | 4.x | JPEG encoding for frame recording |
|
||||
| json (stdlib) | — | JSON-lines serialization |
|
||||
| os (stdlib) | — | NVMe free space check (statvfs) |
|
||||
|
||||
**Error Handling Strategy**:
|
||||
- WriteError: log to stderr, increment error counter, continue processing (recording failure must not block inference)
|
||||
- NVMe full: stop recording, log warning, continue detection-only mode
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
**Known limitations**:
|
||||
- Frame recording at 30 FPS (L2) writes ~3 MB/s — well within NVMe bandwidth but significant storage consumption
|
||||
- JSON-lines flush interval means up to 10 detections or 5s of data could be lost on hard crash
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: Config helper, Types helper
|
||||
**Can be implemented in parallel with**: Tier1Detector, Tier2SpatialAnalyzer, VLMClient, GimbalDriver
|
||||
**Blocks**: ScanController (needs OutputManager for logging)
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | NVMe write failure, disk full | `Frame write failed: No space left on device` |
|
||||
| WARN | Storage low, reducing recording | `NVMe 18% free, reducing to L1 recording only` |
|
||||
| INFO | Session started, stats | `Output session started: /data/output/2026-03-19T14:00/` |
|
||||
@@ -0,0 +1,346 @@
|
||||
# Test Specification — OutputManager
|
||||
|
||||
## Acceptance Criteria Traceability
|
||||
|
||||
| AC ID | Acceptance Criterion | Test IDs | Coverage |
|
||||
|-------|---------------------|----------|----------|
|
||||
| AC-26 | Total RAM ≤6GB (OutputManager must not contribute significant memory) | PT-02 | Covered |
|
||||
| AC-27 | Coexist with YOLO pipeline — recording must not block inference | IT-01, PT-01 | Covered |
|
||||
|
||||
Note: OutputManager has no direct performance ACs from the acceptance criteria. Its tests ensure it supports the system's recording, logging, and operator delivery requirements defined in the architecture.
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### IT-01: log_detection Writes Valid JSON-Line
|
||||
|
||||
**Summary**: Verify log_detection appends a correctly formatted JSON line to the detection log file.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- DetectionLogEntry: {frame_id: 1000, label: "footpath_winter", confidence: 0.72, tier: 2, centerX: 0.5, centerY: 0.3}
|
||||
- Output dir: temporary test directory
|
||||
|
||||
**Expected result**:
|
||||
- detections.jsonl file exists in output dir
|
||||
- Last line is valid JSON parseable to a dict with all input fields
|
||||
- Trailing newline present
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-02: record_frame Saves JPEG to Sequential Filename
|
||||
|
||||
**Summary**: Verify record_frame encodes and saves frame as JPEG with correct naming.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- Frame: numpy array (1080, 1920, 3), random pixel data
|
||||
- frame_id: 42, level: 1
|
||||
|
||||
**Expected result**:
|
||||
- File `42.jpg` exists in output dir `frames/` subdirectory
|
||||
- File is a valid JPEG (OpenCV can re-read it)
|
||||
- File size > 0 and < 500KB (reasonable JPEG of 1080p noise)
|
||||
|
||||
**Max execution time**: 100ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-03: log_health Writes Health Entry
|
||||
|
||||
**Summary**: Verify log_health appends a JSON line with health data.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- HealthLogEntry: {timestamp: epoch, t_junction_c: 65.0, vlm_available: true, gimbal_available: true, semantic_available: true}
|
||||
|
||||
**Expected result**:
|
||||
- health.jsonl file exists
|
||||
- Last line contains all input fields as valid JSON
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-04: log_gimbal_command Appends to Gimbal Log
|
||||
|
||||
**Summary**: Verify gimbal command strings are appended to the gimbal log file.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- cmd_str: "SET_ANGLES pan=10.0 tilt=-20.0 zoom=5.0"
|
||||
|
||||
**Expected result**:
|
||||
- gimbal.log file exists
|
||||
- Last line matches the input command string
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-05: report_to_operator Formats Detection in YOLO Schema
|
||||
|
||||
**Summary**: Verify operator delivery formats detections with centerX, centerY, width, height, classNum, label, confidence.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- list of 3 Detection objects
|
||||
|
||||
**Expected result**:
|
||||
- Output matches existing YOLO output format (same field names, same coordinate normalization)
|
||||
- All 3 detections present in output
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: None
|
||||
|
||||
---
|
||||
|
||||
### IT-06: get_storage_status Returns Correct NVMe Stats
|
||||
|
||||
**Summary**: Verify storage status reports accurate free space percentage.
|
||||
|
||||
**Traces to**: AC-26
|
||||
|
||||
**Input data**:
|
||||
- Output dir on test filesystem
|
||||
|
||||
**Expected result**:
|
||||
- StorageStatus: nvme_free_pct in [0, 100], frames_recorded ≥ 0, detections_logged ≥ 0
|
||||
- should_reduce_recording matches threshold logic (true if free < 20%)
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-07: Circular Buffer Triggers on Low Storage
|
||||
|
||||
**Summary**: Verify should_reduce_recording becomes true when free space drops below 20%.
|
||||
|
||||
**Traces to**: AC-26
|
||||
|
||||
**Input data**:
|
||||
- Mock statvfs to report 15% free space
|
||||
|
||||
**Expected result**:
|
||||
- get_storage_status().should_reduce_recording == true
|
||||
- At 25% free → should_reduce_recording == false
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Mock filesystem stats
|
||||
|
||||
---
|
||||
|
||||
### IT-08: Init Creates Output Directory Structure
|
||||
|
||||
**Summary**: Verify init() creates the expected directory structure.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- output_dir: temporary path that does not exist yet
|
||||
|
||||
**Expected result**:
|
||||
- Directory created with subdirectories for frames
|
||||
- No errors
|
||||
|
||||
**Max execution time**: 100ms
|
||||
|
||||
**Dependencies**: Writable filesystem
|
||||
|
||||
---
|
||||
|
||||
### IT-09: WriteError Does Not Block Caller
|
||||
|
||||
**Summary**: Verify that a disk write failure (e.g., permission denied) is caught and does not propagate as an unhandled exception.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Input data**:
|
||||
- Output dir set to a read-only path
|
||||
|
||||
**Expected result**:
|
||||
- log_detection raises no unhandled exception (catches WriteError internally)
|
||||
- Error counter incremented
|
||||
- Function returns normally
|
||||
|
||||
**Max execution time**: 50ms
|
||||
|
||||
**Dependencies**: Read-only filesystem path
|
||||
|
||||
---
|
||||
|
||||
## Performance Tests
|
||||
|
||||
### PT-01: Frame Recording Throughput at L2 Rate
|
||||
|
||||
**Summary**: Verify OutputManager can sustain 30 FPS frame recording without becoming a bottleneck.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Load scenario**:
|
||||
- 30 frames/second, 1080p JPEG encoding + write
|
||||
- Duration: 10 seconds (300 frames)
|
||||
- Ramp-up: immediate
|
||||
|
||||
**Expected results**:
|
||||
|
||||
| Metric | Target | Failure Threshold |
|
||||
|--------|--------|-------------------|
|
||||
| Sustained write rate | ≥30 FPS | <25 FPS |
|
||||
| Encoding latency (p95) | ≤20ms | >33ms |
|
||||
| Dropped frames | 0 | >5 |
|
||||
| Write throughput | ≥3 MB/s | <2 MB/s |
|
||||
|
||||
**Resource limits**:
|
||||
- CPU: ≤20% (JPEG encoding)
|
||||
- Memory: ≤100MB (buffer)
|
||||
|
||||
---
|
||||
|
||||
### PT-02: Memory Usage Under Sustained Load
|
||||
|
||||
**Summary**: Verify no memory leak during continuous logging and recording.
|
||||
|
||||
**Traces to**: AC-26
|
||||
|
||||
**Load scenario**:
|
||||
- 1000 log_detection calls + 300 record_frame calls
|
||||
- Duration: 60 seconds
|
||||
- Measure RSS before and after
|
||||
|
||||
**Expected results**:
|
||||
|
||||
| Metric | Target | Failure Threshold |
|
||||
|--------|--------|-------------------|
|
||||
| Memory growth | ≤10MB | >50MB |
|
||||
| Memory leak rate | 0 MB/min | >5 MB/min |
|
||||
|
||||
**Resource limits**:
|
||||
- Memory: ≤100MB total for OutputManager
|
||||
|
||||
---
|
||||
|
||||
## Security Tests
|
||||
|
||||
### ST-01: Detection Log Does Not Contain Raw Image Data
|
||||
|
||||
**Summary**: Verify detection JSON lines contain metadata only, not embedded image data.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Attack vector**: Information leakage through oversized log entries
|
||||
|
||||
**Test procedure**:
|
||||
1. Log 10 detections
|
||||
2. Read detections.jsonl
|
||||
3. Verify no field contains base64, raw bytes, or binary data
|
||||
|
||||
**Expected behavior**: Each JSON line is < 1KB; only text/numeric fields.
|
||||
|
||||
**Pass criteria**: All lines < 1KB, no binary data patterns.
|
||||
|
||||
**Fail criteria**: Any line contains embedded image data or exceeds 10KB.
|
||||
|
||||
---
|
||||
|
||||
### ST-02: Path Traversal Prevention in Output Directory
|
||||
|
||||
**Summary**: Verify frame_id or other inputs cannot cause writes outside the output directory.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Attack vector**: Path traversal via crafted frame_id
|
||||
|
||||
**Test procedure**:
|
||||
1. Call record_frame with frame_id containing "../" characters (e.g., as uint64 this shouldn't be possible, but verify string conversion)
|
||||
2. Verify file is written inside output_dir only
|
||||
|
||||
**Expected behavior**: File written within output_dir; no file created outside.
|
||||
|
||||
**Pass criteria**: All files within output_dir subtree.
|
||||
|
||||
**Fail criteria**: File created outside output_dir.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
### AT-01: Full Flight Recording Session
|
||||
|
||||
**Summary**: Verify OutputManager correctly handles a simulated 5-minute flight with mixed L1 and L2 recording.
|
||||
|
||||
**Traces to**: AC-27
|
||||
|
||||
**Preconditions**:
|
||||
- Temporary output directory with sufficient space
|
||||
- Config: recording_l1_fps=2, recording_l2_fps=30
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|------|--------|-----------------|
|
||||
| 1 | init(output_dir) | Directory structure created |
|
||||
| 2 | Simulate 3 min L1: record_frame at 2 FPS | 360 frames written |
|
||||
| 3 | Simulate 1 min L2: record_frame at 30 FPS | 1800 frames written |
|
||||
| 4 | Log 50 detections during L2 | detections.jsonl has 50 lines |
|
||||
| 5 | get_storage_status() | frames_recorded=2160, detections_logged=50 |
|
||||
|
||||
---
|
||||
|
||||
### AT-02: Storage Reduction Under Pressure
|
||||
|
||||
**Summary**: Verify the storage management signals reduce recording at the right thresholds.
|
||||
|
||||
**Traces to**: AC-26
|
||||
|
||||
**Preconditions**:
|
||||
- Mock filesystem with configurable free space
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|------|--------|-----------------|
|
||||
| 1 | Set free space to 25% | should_reduce_recording = false |
|
||||
| 2 | Set free space to 15% | should_reduce_recording = true |
|
||||
| 3 | Set free space to 8% | should_reduce_recording = true (critical) |
|
||||
|
||||
---
|
||||
|
||||
## Test Data Management
|
||||
|
||||
**Required test data**:
|
||||
|
||||
| Data Set | Description | Source | Size |
|
||||
|----------|-------------|--------|------|
|
||||
| sample_frames | 10 sample 1080p frames for recording tests | Generated (random or real) | ~10 MB |
|
||||
| sample_detections | 50 DetectionLogEntry dicts | Generated fixtures | ~5 KB |
|
||||
| sample_health | 10 HealthLogEntry dicts | Generated fixtures | ~2 KB |
|
||||
|
||||
**Setup procedure**:
|
||||
1. Create temporary output directory
|
||||
2. Call init(output_dir)
|
||||
|
||||
**Teardown procedure**:
|
||||
1. Delete temporary output directory and all contents
|
||||
|
||||
**Data isolation strategy**: Each test uses its own temporary directory. No shared output paths between tests.
|
||||
Reference in New Issue
Block a user