376 lines
13 KiB
Markdown
376 lines
13 KiB
Markdown
# VideoDetect - Video Classification System
|
||
|
||
A production-grade video classification system for processing large-scale video corpora (~30TB) to classify videos based on demographic presence using deep learning models.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||
│ Scanner │────▶│ Processor │────▶│ Results │
|
||
│ (STORY-02) │ │ (STORY-03/04)│ │ (STORY-05) │
|
||
└─────────────┘ └──────────────┘ └─────────────┘
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||
│ MariaDB │◀───▶│ GPU (P40) │◀───▶│ Export │
|
||
│ (Metadata) │ │ (Inference) │ │ (Parquet) │
|
||
└─────────────┘ └──────────────┘ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌──────────────┐
|
||
│ Review UI │◀───▶│ Active Learn │
|
||
│ (STORY-06) │ │ (STORY-07) │
|
||
└─────────────┘ └──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Monitoring │
|
||
│ (STORY-08) │
|
||
└─────────────┘
|
||
```
|
||
|
||
## Quick Start
|
||
|
||
### Prerequisites
|
||
- Docker & Docker Compose v2+
|
||
- NVIDIA Container Toolkit
|
||
- 2× Tesla P40 GPUs (or compatible)
|
||
- NAS/SMB mount at `/mnt/nas` (configurable via env vars)
|
||
|
||
### Environment Variables
|
||
|
||
```bash
|
||
# Database
|
||
export DB_ROOT_PASSWORD=your_root_password
|
||
export DB_PASSWORD=your_db_password
|
||
|
||
# Paths (optional, defaults shown)
|
||
export NAS_INPUT_PATH=/mnt/nas/input
|
||
export NAS_OUTPUT_PATH=/mnt/nas/output
|
||
export MODELS_PATH=/mnt/nas/models
|
||
export TRAINING_PATH=/mnt/nas/training
|
||
|
||
# Grafana admin password
|
||
export GRAFANA_PASSWORD=your_grafana_password
|
||
```
|
||
|
||
### Start the Stack
|
||
|
||
```bash
|
||
docker-compose up -d
|
||
```
|
||
|
||
### Verify Services
|
||
|
||
```bash
|
||
# Check all containers are running
|
||
docker-compose ps
|
||
|
||
# Check GPU visibility in worker
|
||
docker exec videodetect-worker nvidia-smi
|
||
|
||
# Check database connectivity
|
||
docker exec videodetect-mariadb mysql -u videodetect -p videodetect -e "SHOW TABLES;"
|
||
|
||
# Access UI
|
||
open http://localhost:5000
|
||
|
||
# Access Grafana
|
||
open http://localhost:3000 (admin / your_grafana_password)
|
||
|
||
# Access Prometheus
|
||
open http://localhost:9090
|
||
```
|
||
|
||
## System Operation
|
||
|
||
### How Processes Start
|
||
|
||
**Service Initialization:**
|
||
|
||
1. **MariaDB** starts first with health check
|
||
2. **Worker** initializes via `src/main.py`:
|
||
- Loads `config.yaml`
|
||
- Sets up JSON logging with rotation
|
||
- Connects to MariaDB (connection pooling)
|
||
- Initializes database schema
|
||
- Verifies GPU availability (CUDA/PyTorch)
|
||
- Starts the `DirectoryScanner` in a background thread (scans permanent storage in place)
|
||
- Creates `WorkerPool` with 1 worker thread
|
||
- Enters job processing loop
|
||
3. **UI** starts Flask review interface via Gunicorn (2 workers)
|
||
4. **Monitoring** starts Prometheus and Grafana independently
|
||
|
||
### Processing Pipeline
|
||
|
||
The worker follows this flow for each video:
|
||
|
||
```
|
||
Pending → Lock → Probe → Sample → Detect → Classify → Aggregate → Route → Persist → Export → Cleanup → Completed
|
||
```
|
||
|
||
**Detailed Steps:**
|
||
|
||
1. **Job Queue** - Atomically lock `PENDING` jobs via `UPDATE status = 'PROCESSING'`
|
||
- Priority: newest files first (`last_scan_time DESC`)
|
||
- Max concurrent: 1 per GPU
|
||
|
||
2. **Probe Video** - Extract metadata via FFprobe
|
||
- Duration, codec, resolution
|
||
- Validate against codec whitelist (H.264, H.265, VP8/9, AV1)
|
||
- Mark `UNSCANNABLE` if invalid
|
||
|
||
3. **Sample Frames** - Extract frames at configured interval (default: 30s)
|
||
- Save as JPEG to `/scratch/{video_id}/frames/`
|
||
- Quality: 2 (lower=better)
|
||
|
||
4. **Detect Faces** - YOLOv8n TensorRT inference (FP32)
|
||
- Batch size auto-tuned by GPU memory monitor
|
||
- NMS filtering (IoU: 0.45, confidence: 0.25)
|
||
- Cap: 10 faces/frame, 100 faces/video
|
||
|
||
5. **Extract Crops** - Resize detected faces to 224×224
|
||
- Save to `/scratch/{video_id}/crops/`
|
||
|
||
6. **Classify Crops** - MobileNetV3-Small TensorRT inference
|
||
- Temperature-scaled softmax (T=1.0)
|
||
- Returns confidence per crop
|
||
|
||
7. **Aggregate Confidence** - Combine crop confidences into video-level score
|
||
- Strategy: `max` (most conservative)
|
||
- Alternatives: `weighted_mean`, `top_k_mean`
|
||
|
||
8. **Route Decision** - Threshold-based routing:
|
||
- `C ≥ 0.75` → **MATCH**
|
||
- `0.45 ≤ C < 0.75` → **REVIEW** (human annotation)
|
||
- `C < 0.45` → **SKIP**
|
||
- No faces → **SKIP**
|
||
|
||
9. **Persist Results** - Atomic transaction:
|
||
- Update `videos` table (confidence, routing, status)
|
||
- Insert `processing_logs` row (audit trail)
|
||
- State guard: only update if `status='PROCESSING'`
|
||
|
||
10. **Export** - Buffer and batch export (default: 100 videos)
|
||
- Format: Parquet with Snappy compression
|
||
- Path: `/data/output/{model_version}/`
|
||
- Fallback: JSONL if Parquet fails
|
||
|
||
11. **Cleanup** - Delete `/scratch/{video_id}/` directory
|
||
- Only after successful persistence
|
||
- Prevents orphaned scratch files
|
||
|
||
**Directory Scanner Service (runs alongside the worker):**
|
||
- Scans the permanent storage location **in place** (no staging/copy step)
|
||
- Walks `/data/input` every **2 hours** by default (configurable via `scanner.scan_interval_seconds`)
|
||
- Detects new, modified, and removed video files by comparing against the DB
|
||
- Filters to video files by extension
|
||
- Computes SHA256 hash, probes metadata, validates codec
|
||
- Queues any video that has not been scanned yet as `PENDING` for the worker pool
|
||
- **Single-instance guard:** an in-process lock plus a DB lock (with a lease) ensure only one scan runs at a time — a long-running scan never overlaps another, even across multiple worker replicas. The lock lease is refreshed via heartbeats during the scan and is taken over automatically if a scanner crashes.
|
||
|
||
### Configuration Reference
|
||
|
||
All configuration is in `config.yaml`. Environment variable override format: `VD_<SECTION>_<KEY>` (e.g., `VD_SAMPLING_INTERVAL_SECONDS=60`).
|
||
|
||
#### Key Configuration Sections
|
||
|
||
**Sampling & Thresholds:**
|
||
```yaml
|
||
sampling:
|
||
interval_seconds: 30 # Frame extraction frequency
|
||
quality: 2 # JPEG quality (1-31, lower=better)
|
||
format: jpeg
|
||
|
||
thresholds:
|
||
T_high: 0.75 # MATCH threshold
|
||
T_low: 0.45 # REVIEW threshold
|
||
```
|
||
|
||
**GPU & Batching:**
|
||
```yaml
|
||
gpu:
|
||
max_memory_gb: 18 # Target VRAM usage
|
||
batch_size: auto # Auto-tune based on available VRAM
|
||
|
||
batching:
|
||
max_batch_size: 16 # Maximum batch size
|
||
vram_target_gb: 16 # Target VRAM for batch tuning
|
||
vram_reduce_threshold_gb: 16 # Reduce batch if above
|
||
vram_increase_threshold_gb: 10 # Increase batch if below
|
||
```
|
||
|
||
**Storage Paths:**
|
||
```yaml
|
||
storage:
|
||
scratch_path: /scratch # Temporary processing (tmpfs)
|
||
input_path: /data/input # Source videos (NFS)
|
||
output_path: /data/output # Results (local/NAS)
|
||
models_path: /models # TensorRT models
|
||
training_path: /data/training # Training data
|
||
```
|
||
|
||
**Database:**
|
||
```yaml
|
||
database:
|
||
host: mariadb
|
||
port: 3306
|
||
name: videodetect
|
||
user: videodetect
|
||
password: videodetect123
|
||
pool_size: 20 # Connection pool size
|
||
pool_min: 5
|
||
pool_recycle: 3600 # Recycle connections after 1h
|
||
```
|
||
|
||
**Face Detection:**
|
||
```yaml
|
||
face_detection:
|
||
model: yolo8n
|
||
model_path: /models/face_detector/face_detector.trt
|
||
input_size: 640
|
||
confidence_threshold: 0.25
|
||
iou_threshold: 0.45
|
||
max_faces_per_frame: 10
|
||
max_faces_per_video: 100
|
||
```
|
||
|
||
**Classification & Aggregation:**
|
||
```yaml
|
||
classifier:
|
||
model: mobilenetv3-small
|
||
model_path: /models/classifier/classifier.trt
|
||
input_size: 224
|
||
temperature: 1.0 # Calibration temperature
|
||
|
||
aggregation:
|
||
strategy: max # max, weighted_mean, top_k_mean
|
||
alpha: 1.0 # weighted_mean weight for mean
|
||
beta: 0.1 # weighted_mean weight for variance
|
||
top_k: 3 # top_k_mean: average top 3 scores
|
||
```
|
||
|
||
**Export:**
|
||
```yaml
|
||
export:
|
||
format: parquet # parquet, jsonl, or both
|
||
compression: snappy
|
||
batch_size: 100 # Export after N videos
|
||
include_frame_confidences: true
|
||
```
|
||
|
||
**Review UI:**
|
||
```yaml
|
||
review_ui:
|
||
host: "0.0.0.0"
|
||
port: 5000
|
||
per_page: 20 # Pagination
|
||
top_k_frames: 5 # Show top-k contributing frames
|
||
auth_enabled: false # No auth per TC-06
|
||
ssl_enabled: false # Internal LAN only
|
||
```
|
||
|
||
#### Volume Mounts
|
||
|
||
From `docker-compose.yml`:
|
||
|
||
- **Input**: NFS mount → `/data/input` (read-only)
|
||
- **Output**: `./output` → `/data/output`
|
||
- **Models**: `./models` → `/models`
|
||
- **Training**: `./training` → `/data/training`
|
||
- **Scratch**: 100GB tmpfs at `/scratch` (RAM disk)
|
||
|
||
### Key Design Principles
|
||
|
||
- **Atomic state transitions** - Database locks prevent race conditions
|
||
- **Crash recovery** - `PROCESSING` jobs automatically requeued on restart
|
||
- **Idempotent** - Re-running same video produces same result
|
||
- **Stateless** - Scratch cleanup after each job
|
||
- **Fail-safe** - 3 retry attempts before marking `ERROR`
|
||
- **No auth/SSL** - Internal LAN deployment per TC-06
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
VideoDetect/
|
||
├── docker-compose.yml # Multi-service orchestration
|
||
├── config.yaml # All configuration
|
||
├── db/
|
||
│ └── schema.sql # MariaDB schema
|
||
├── worker/
|
||
│ ├── Dockerfile # Worker container image
|
||
│ └── requirements.txt # Python dependencies
|
||
├── ui/
|
||
│ ├── Dockerfile # UI container image
|
||
│ └── review/ # Review UI templates
|
||
├── src/
|
||
│ ├── main.py # Worker entry point
|
||
│ ├── db_connector.py # Database connection layer
|
||
│ ├── config_loader.py # Configuration management
|
||
│ ├── logging_config.py # Logging setup
|
||
│ ├── scanner.py # Directory scanner (STORY-02)
|
||
│ ├── prober.py # Video probing (STORY-02)
|
||
│ ├── frame_sampler.py # Frame extraction (STORY-03)
|
||
│ ├── face_detector.py # Face detection (STORY-04)
|
||
│ ├── classifier.py # Classification (STORY-05)
|
||
│ ├── aggregator.py # Confidence aggregation (STORY-05)
|
||
│ ├── router.py # Threshold routing (STORY-05)
|
||
│ ├── result_updater.py # Result persistence (STORY-06)
|
||
│ ├── data_export.py # Parquet/JSONL export (STORY-06)
|
||
│ ├── review_api.py # Review API (STORY-07)
|
||
│ ├── active_learning/ # Active learning pipeline (STORY-08)
|
||
│ │ ├── label_ingestor.py
|
||
│ │ ├── trainer.py
|
||
│ │ ├── validator.py
|
||
│ │ └── registry.py
|
||
│ ├── metrics.py # Prometheus metrics (STORY-09)
|
||
│ ├── crash_recovery.py # Crash recovery (STORY-09)
|
||
│ └── drift_detector.py # Drift detection (STORY-09)
|
||
├── monitoring/
|
||
│ ├── prometheus/
|
||
│ │ └── prometheus.yml # Prometheus config
|
||
│ └── grafana/
|
||
│ ├── dashboards/ # Grafana dashboard JSONs
|
||
│ └── provisioning/ # Grafana data source config
|
||
├── STORY-01.md through STORY-09.md # Implementation stories
|
||
├── Plan.md # Implementation plan
|
||
└── Requirements.md # Requirements document
|
||
```
|
||
|
||
## Implementation Stories
|
||
|
||
| Story | Description | Sprint | Points |
|
||
|-------|-------------|--------|--------|
|
||
| STORY-01 | Foundation & Infrastructure | 1-2 | 13 |
|
||
| STORY-02 | Core Ingestion & Codec Handling | 3-4 | 21 |
|
||
| STORY-03 | Frame Sampling | 5 | 13 |
|
||
| STORY-04 | Face Detection | 5-6 | 21 |
|
||
| STORY-05 | Classification & Confidence | 5-6 | 21 |
|
||
| STORY-06 | Results Persistence & Export | 5-6 | 13 |
|
||
| STORY-07 | Review Interface | 7 | 21 |
|
||
| STORY-08 | Active Learning Pipeline | 7-8 | 34 |
|
||
| STORY-09 | Observability & Hardening | 9+ | 34 |
|
||
|
||
## Hardware Requirements
|
||
|
||
- **GPUs:** 2× Tesla P40 24GB (compute capability 5.2)
|
||
- **CUDA:** ≤ 11.8
|
||
- **PyTorch:** ≤ 2.1.0
|
||
- **Inference:** FP32 only (no Tensor Cores)
|
||
- **Storage:** NVMe/SSD for scratch, NAS/SMB for input/output
|
||
- **RAM:** ≥ 32GB system RAM
|
||
|
||
## Key Design Decisions
|
||
|
||
1. **TensorRT FP32 only** — Tesla P40 has no Tensor Cores; FP16 would not provide benefit
|
||
2. **YOLOv8n + MobileNetV3** — Lightweight models optimized for throughput over accuracy
|
||
3. **Max aggregation** — Most conservative confidence strategy; use highest frame confidence
|
||
4. **Temperature scaling** — Calibrates confidence scores without retraining
|
||
5. **Head-only fine-tuning** — Faster retraining with frozen backbone
|
||
6. **Parquet export** — Efficient columnar format for analytics
|
||
7. **No auth/SSL** — Per TC-06, internal LAN only
|
||
|
||
## License
|
||
|
||
Proprietary — VideoDetect Project
|