A photo curation and display platform that ingests, AI-analyzes, and curates photos of Door County, Wisconsin for display on HDMI screens at business locations throughout the region.
Host: 192.168.11.18
Domain: doorcounty-wi.com (Cloudflare proxied)
Project root: /opt/doorcounty-wi/
Last updated: 2026-04-07
[Photo Sources] → [Ingestion] → [MinIO Staging] → [YOLOv8 Analyzer] → [Approval UI] → [MinIO Production] → [Pi Display Devices]
Both ingestion paths (iCloud file-based and web upload) converge at MinIO staging. After that the pipeline is identical.
See ARCHITECTURE.md for full container and network documentation.
| Container | Image | Port(s) | Role |
|---|---|---|---|
minio |
minio/minio | 9000 (API), 9001 (console) | S3-compatible object storage |
postgres |
postgres | 5432 | Metadata database |
fastapi-app |
custom | 8000 | Web app, REST API, approval UI |
nginx |
nginx | 80, 443 | Reverse proxy / TLS termination |
wireguard-provisioning |
custom | 143 TCP | Stage 1 device registration VPN |
wireguard-production |
custom | 587 TCP | Production device management VPN |
photo-analyzer |
ultralytics/ultralytics | — | YOLOv8 analysis (GPU: Quadro P400) |
ingestion-service |
python:3.11-slim | — | Pluggable file-based ingestion |
Database: doorcounty on postgres:5432
User: doorcounty
photos| Column | Type | Notes |
|---|---|---|
| photo_id | UUID PK | auto-gen |
| file_path | TEXT | MinIO object path |
| quality_score | SMALLINT | 0–255 (AI-generated) |
| approval_status | VARCHAR(20) | pending / approved / rejected |
| approver | TEXT | |
| approval_timestamp | TIMESTAMP | |
| source | TEXT | Ingestion source identifier |
| source_owner | TEXT | |
| ingestion_timestamp | TIMESTAMP | |
| taken_date / taken_time | DATE / TIME | From EXIF |
| season | VARCHAR(10) | winter / spring / summer / fall |
| time_of_day | VARCHAR(10) | morning / afternoon / evening / night |
| weather | TEXT | |
| location_name | TEXT | |
| gps_lat / gps_lng | DECIMAL | From EXIF |
| camera_device | TEXT | |
| exposure_settings | JSONB | EXIF exposure data |
| content_type | TEXT | Scene classification |
| face_count | INTEGER | |
| faces_identifiable | BOOLEAN | |
| blur_score | DECIMAL | Sharpness metric |
| dominant_colors | JSONB | |
| aspect_ratio | TEXT | |
| file_size_bytes | BIGINT | |
| file_format | TEXT | |
| image_width / image_height | INTEGER | |
| tags | TEXT[] | GIN indexed; YOLO detections |
| notes | TEXT | |
| display_duration_ms | INTEGER | Per-photo display time override |
devices| Column | Type | Notes |
|---|---|---|
| device_id | TEXT PK | |
| prefix | VARCHAR(6) | |
| sequence_number | INTEGER | Unique with prefix |
| mac_address | TEXT | |
| wireguard_pubkey | TEXT | |
| provisioning_status | VARCHAR(20) | provisioning / active / retired |
| last_checkin | TIMESTAMP | |
| current_ip / public_ip | TEXT | |
| app_version / os_version | TEXT | |
| package_hash | TEXT | |
| is_canary | BOOLEAN | |
| display_profile | JSONB | Per-device display config |
| config_version | INTEGER | |
| created_at | TIMESTAMP |
Analysis of 1,050 photos shows the current quality score is non-discriminating:
| Status | Count | Avg Score | Median |
|---|---|---|---|
| Approved | 163 | 178.5 | 191 |
| Rejected | 206 | 177.3 | 191 |
| Pending | 681 | 93.0 | 86 |
Approved and rejected photos have identical median scores. Root cause: all photos are iPhone 14 Pro (same resolution), and YOLO detections return empty — so the current algorithm reduces to sharpness only.
See HANDOFF_FINAL.md for the full implementation plan. Summary:
Target: rate 200–300 photos → train model → re-score all 1,050 pending.
Active planning underway for a full admin backend. Will cover:
| Volume / Mount | Purpose |
|---|---|
minio-data |
All object storage (staging + production buckets) |
postgres-data |
Database files |
wireguard-provisioning-data |
Provisioning VPN configs |
wireguard-production-data |
Production VPN configs |
/mnt/icloud (read-only) |
3.5TB external SSD — 315GB iCloud source photos |
Configured via /opt/doorcounty-wi/ingestion/sources.yaml. Currently only iCloud is enabled.
| Plugin | Status |
|---|---|
plugins/icloud.py |
✅ Enabled (MVP) |
plugins/smb.py |
🔜 Future |
plugins/google_photos.py |
🔜 Future |
plugins/dropbox.py |
🔜 Future |
CREDENTIALS.txt (chmod 600, never committed)/opt/doorcounty-wi/
├── docker-compose.yml
├── CREDENTIALS.txt # chmod 600, not in git
├── README.md
├── ARCHITECTURE.md
├── schema.sql
├── app/ # FastAPI application
├── analyzer/ # YOLOv8 analyzer (analyze.py)
├── ingestion/ # Ingestion service + plugins
│ ├── sources.yaml
│ └── plugins/
├── ml/ # ML model training (future)
├── nginx/ # nginx.conf + ssl/
├── scripts/
└── logs/