# STORY-06: Results Persistence & Export ## Epic **E1: Core Pipeline** — As a system, I can persist processing results and export data for analytics. ## Related Requirements | ID | Requirement | |----|------| | FR-07 | Metadata logging & audit trail: Stores video ID, timestamps, frame counts, confidence scores, routing decision, model version | | FR-04 | Video-level confidence aggregation & threshold routing: Results saved to Parquet | | NFR-05 | Fault Tolerance: Metadata persisted before cleanup | | NFR-07 | Data Volume Handling: Efficient indexing for ~30TB dataset | | TC-03 | Storage I/O: Shared NAS/SMB for video input/output | ## Description Implement result persistence, metadata logging, and data export. Update the database with processing results, insert processing log entries, and export summary data in Parquet/JSONL format for analytics. Ensure all persistence happens before scratch space cleanup to prevent data loss. ## Scope ### In Scope - Database update for processed videos (status, confidence, routing, timestamps) - Processing log entry insertion (atomic transaction) - Parquet/JSONL export to `/data/output` - Confidence score persistence (frame-level and video-level) - Model version tracking in DB and export - Idempotent result persistence (re-processing doesn't duplicate entries) - Transaction-safe updates (all-or-nothing) ### Out of Scope - Frame sampling (covered in STORY-03) - Face detection (covered in STORY-04) - Classification (covered in STORY-05) - Review UI (covered in STORY-07) - Active learning pipeline (covered in STORY-08) ## Deliverables ### 6.1 Result Updater **File:** `src/result_updater.py` Features: - **Video Record Update:** Update `videos` table with processing results ```sql UPDATE videos SET status = 'COMPLETED', last_processed_time = NOW(), confidence_score = %s, routing_decision = %s, model_version = %s, frame_count = %s, updated_at = NOW() WHERE id = %s AND status = 'PROCESSING' ``` - **Atomic Transaction:** Wrap video update + log insert in single transaction - **State Guard:** Only update if status is PROCESSING (prevents duplicate processing) - **Error Handling:** Rollback transaction on any error; re-queue video as PENDING ### 6.2 Processing Logger **File:** `src/processing_logger.py` Features: - **Log Entry Insertion:** Insert record into `processing_logs` table ```python def insert_processing_log(video_id, model_version, frame_count, confidence_score, routing_decision): cursor.execute(""" INSERT INTO processing_logs (video_id, model_version, frame_count, confidence_score, routing_decision, processed_at) VALUES (%s, %s, %s, %s, %s, NOW()) """, (video_id, model_version, frame_count, confidence_score, routing_decision)) ``` - **Frame-Level Confidence Storage:** Store all frame-level confidence scores - Option A: JSON array in processing_logs.confidence_scores (TEXT column) - Option B: Separate `frame_confidences` table (for large datasets) - Default: JSON array in processing_logs (simpler, sufficient for typical frame counts) - **Audit Trail:** All log entries include timestamp, model version, and routing decision ### 6.3 Data Export **File:** `src/data_export.py` Features: - **Parquet Export:** Write summary data to Parquet format ```python # Schema: # video_id (int64), file_path (string), file_hash (string), # model_version (string), sample_count (int32), # confidence_scores (list), video_confidence (float), # routing (string), processed_at (timestamp) ``` - **JSONL Export:** Alternative line-delimited JSON format for streaming analytics ```json {"video_id": 12345, "file_path": "/data/input/video.mp4", "model_version": "v1.2.0", "sample_count": 8, "confidence_scores": [0.82, 0.79, 0.85, 0.76, 0.81, 0.83, 0.78, 0.80], "video_confidence": 0.85, "routing": "MATCH", "processed_at": "2026-08-03T10:30:00Z"} ``` - **Output Directory:** `/data/output/{model_version}/` organized by model version - **File Naming:** `{model_version}_{batch_id}.parquet` and `{model_version}_{batch_id}.jsonl` - **Batch ID:** Sequential counter or timestamp-based (e.g., `v1.2.0_20260803_103000`) - **Compression:** Parquet with Snappy compression - **Export Trigger:** Export after all videos in batch are processed (or periodically) ### 6.4 Scratch Cleanup **File:** `src/scratch_manager.py` (cleanup section) Features: - **Post-Processing Cleanup:** Delete all frames and crops after results are persisted - **Cleanup Order:** Results → Logs → Export → Cleanup (critical ordering) - **Cleanup Verification:** Verify DB update before deleting scratch files - **Cleanup on Error:** If persistence fails, do NOT delete scratch (allow retry) - **Cleanup Logging:** Log cleanup completion with video_id and files removed ### 6.5 Configuration Updates **File:** `config.yaml` (updates) New fields: ```yaml results: persist_before_cleanup: true transaction_safe: true state_guard: true # only update PROCESSING → COMPLETED export: format: parquet # parquet, jsonl, or both output_path: /data/output compression: snappy batch_size: 100 # export after N videos include_frame_confidences: true ``` ## Acceptance Criteria ### Functional - [ ] Video status transitions from PROCESSING to COMPLETED after successful processing - [ ] Processing log entry is inserted for every processed video - [ ] Frame-level confidence scores are stored (all p_i values) - [ ] Video-level confidence score is stored correctly - [ ] Routing decision is persisted accurately - [ ] Model version is recorded in both videos and processing_logs tables - [ ] Parquet export produces valid, readable files with correct schema - [ ] JSONL export produces valid JSON lines with correct fields - [ ] Export files are organized by model version in /data/output/ - [ ] Scratch space is cleaned up after persistence (verified by directory check) - [ ] Re-processing a video updates existing records (no duplicates) - [ ] Transaction rollback on error prevents partial state updates ### Non-Functional - [ ] Result persistence completes in < 5 seconds per video - [ ] Parquet export for 100 videos completes in < 10 seconds - [ ] JSONL export for 100 videos completes in < 10 seconds - [ ] Export files are valid (verified by parquet.read_table and jsonl parsing) - [ ] Scratch cleanup frees > 95% of allocated space per video - [ ] No data loss if worker crashes between processing and persistence (PROCESSING state preserved) ### Technical Constraints - [ ] All DB updates use parameterized queries (no SQL injection) - [ ] Transaction isolation level is READ COMMITTED or higher - [ ] Parquet files use Snappy compression (not gzip, for speed) - [ ] JSONL files are UTF-8 encoded - [ ] Confidence scores are stored with float64 precision - [ ] Timestamps are in UTC (ISO 8601 format) ## Dependencies - **Prerequisites:** STORY-01 (Foundation — DB schema), STORY-05 (Classification — provides results) - **Depends on:** None (runs after classification) - **Enables:** STORY-07 (Review UI — provides routing decisions), STORY-08 (Active Learning — provides training data) ## Risks & Mitigations | Risk | Mitigation | |------|--| | NAS write latency slows export | Batch writes; use local tmpfs for export staging | | Parquet library compatibility with CUDA container | Test pyarrow in Worker container early; pin version | | Transaction rollback leaves video in PROCESSING state | DB lock timeout (5 min) prevents permanent lock | | Scratch cleanup before persistence causes data loss | Enforce ordering: persist → verify → cleanup | ## Estimated Effort - **Sprint:** 5-6 (second half) - **Story Points:** 13 - **Dependencies:** STORY-01, STORY-05