Abstract
This article documents the architecture, implementation, and operational characteristics of BabyMind AI — a multi-service AI infant monitoring system deployed on Raspberry Pi 5 hardware. The system integrates computer vision (Claude Haiku Vision API), large language model inference (Groq LLaMA-70b), real-time audio processing (WebRTC VAD), persistent storage (SQLite WAL), and a conversational interface (Twilio WhatsApp) into a unified 9-service production stack. Total hardware cost: $60. Monthly operating cost: under $5. The system has operated continuously in a home environment with an infant, providing 24/7 monitoring, structured health record logging, and natural language Q&A over WhatsApp.
1. Problem Definition
Commercial infant monitors provide video streams and threshold-based alerts. They perform no contextual analysis, maintain no health records, and cannot answer natural language queries. The engineering challenge is:
- Capture continuous video and audio from a deployed camera
- Apply computer vision at regular intervals to generate structured observations
- Detect and classify audio events (cry detection) in real time
- Persist all observations and health events to a queryable database
- Expose a natural language Q&A interface grounded in real-time observation data
- Run all of the above on a $60 embedded Linux device with 4GB RAM, 24/7
2. System Architecture
2.1 Hardware
| Component | Specification | Cost |
|---|---|---|
| Compute | Raspberry Pi 5 (4GB RAM, 2.4GHz quad-core) | ~$60 |
| Camera | Ring Indoor Camera (WebRTC, 1080p) | Already owned |
| Audio | USB microphone (plughw:2,0, 44100Hz, S16_LE) | ~$12 |
| Display | FREENOVE 4.3" DSI touchscreen (optional) | ~$25 |
| Storage | 128GB microSD (Class 10) | ~$15 |
| OS | Raspberry Pi OS (Linux 6.12, 64-bit) | Free |
Total hardware: under $100.
2.2 Service Architecture
The system runs 9 independent systemd services with defined startup dependencies and memory limits:
┌─────────────────────────────────────────────────────────────────┐
│ Raspberry Pi 5 (4GB RAM) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ babymind-ring│ │babymind-vision│ │ babymind-audio │ │
│ │ WebRTC live │──▶│ Claude Haiku │ │ WebRTC VAD │ │
│ │ stream │ │ Vision, 30s │ │ cry detection │ │
│ │ MemMax:256MB │ │ MemMax:512MB │ │ MemMax:256MB │ │
│ └──────────────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌────────▼────────────────────▼───────────┐ │
│ │ SQLite (WAL mode) │ │
│ │ 7 tables · WAL · indexes · 30s timeout │ │
│ └────────────────┬────────────────────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────▼──────────┐ │
│ │babymind-ctrl │ │ babymind-whatsapp │ │
│ │ HTTP control │ │ Flask + Twilio + │ │
│ │ MemMax:128MB │ │ Groq LLaMA-70b Q&A │ │
│ └──────────────┘ │ MemMax:600MB │ │
│ └─────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │babymind-dash │ │babymind-reports│ │babymind-cloudflare│ │
│ │ Streamlit │ │ 7AM/9PM │ │ Tunnel → Twilio │ │
│ │ MemMax:512MB │ │ email+WhatsApp │ │ webhook │ │
│ └──────────────┘ └───────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2.3 Service Startup Order and Dependencies
Services use staggered ExecStartPre delays to prevent boot-time resource contention:
| Service | Boot Delay | Memory Limit | Restart Policy |
|---|---|---|---|
| babymind-cloudflare | 5s | 128MB | always, 10s |
| babymind-ring | 10s | 256MB | always, 10s |
| babymind-audio | 10s | 256MB | always, 15s |
| babymind-vision | 15s | 512MB | always, 15s |
| babymind-whatsapp | 20s | 600MB | always, 15s |
| babymind-dash | 20s | 512MB | always, 15s |
| babymind-ctrl | 15s | 128MB | always, 10s |
| babymind-reports | 25s | 256MB | always, 15s |
| babymind-health | 30s | 128MB | always, 30s |
All services use StartLimitIntervalSec=120 and StartLimitBurst=5.
3. Layer 1: Video Acquisition — Ring WebRTC Integration
The Ring Indoor Camera streams via WebRTC using the ring_doorbell Python library. The vision service:
- Maintains a persistent WebRTC session with automatic reconnection
- Reads JPEG snapshots from the live stream on a 30-second interval
- Validates frame size — frames under 40KB are skipped (dark/empty frames)
- Sends valid frames to the Vision API pipeline
Known issue: A RuntimeError: Task cannot await on itself appears in logs from the ring_doorbell library's asyncio implementation. This does not affect functionality — it is a library-level bug that does not crash the service.
Frame validation logic:
def is_valid_frame(frame_data: bytes) -> bool:
# Skip dark/empty frames — saves ~40% of API calls at night
if len(frame_data) < 40_000: # 40KB threshold
return False
return True
This threshold alone eliminates approximately 40% of API calls during overnight hours when the room is dark, directly reducing Vision API costs.
4. Layer 2: Computer Vision — Claude Haiku Vision API
4.1 Vision Pipeline
Every valid frame is processed through Claude Haiku Vision with a structured prompt that returns JSON:
VISION_PROMPT = """You are monitoring an infant for safety and wellbeing.
Return ONLY valid JSON matching this exact schema:
{
"baby_state": "sleeping|awake|crying|playing|fussing|unknown",
"face_visible": true|false,
"face_covered": true|false,
"position": "on_back|on_side|on_tummy|sitting|standing|held|unknown",
"safety_concern": true|false,
"safety_detail": "string or null",
"people_present": ["mom", "dad", "other"],
"activity_changed": true|false,
"observation": "Precise observational narrative under 100 words"
}"""
4.2 Model Selection and Cost Analysis
| Model | Cost per 1K images | Observation quality | Latency |
|---|---|---|---|
| Claude Haiku 4.5 | ~$0.25 | Clinical precision | 1.2s avg |
| Groq Llama 4 Scout (fallback) | Free tier | Good | 0.8s avg |
| GPT-4o mini (evaluated, not used) | ~$0.30 | Good | 1.8s avg |
Monthly cost at 30-second intervals:
- Images per day: 2,880 (minus ~40% dark frame skip = ~1,728 API calls)
- Images per month: ~51,840
- Cost at $0.25/1K: ~$1.30/month
In practice, monthly Vision API costs run $1.50–$2.50 depending on overnight lighting conditions.
4.3 VisionMemory — Rolling Observation Store
All observations are stored in SQLite with a rolling limit of 200 per camera:
class VisionMemory:
MAX_OBSERVATIONS = 200
def add_observation(self, camera_id: str, observation: dict):
with self.db.connect() as conn:
conn.execute("""
INSERT INTO vision_observations
(camera_id, timestamp, baby_state, position,
face_visible, safety_concern, people_present, observation)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (...))
# Enforce rolling limit
conn.execute("""
DELETE FROM vision_observations
WHERE camera_id = ?
AND id NOT IN (
SELECT id FROM vision_observations
WHERE camera_id = ?
ORDER BY timestamp DESC
LIMIT ?
)
""", (camera_id, camera_id, self.MAX_OBSERVATIONS))
4.4 Fallback Chain
Claude Haiku Vision (primary)
↓ on timeout/error (3 retries, exponential backoff: 2s/4s/8s)
Groq Llama 4 Scout Vision (fallback)
↓ on failure
Skip frame, log warning, continue
5. Layer 3: Audio Processing — Cry Detection
5.1 WebRTC VAD Pipeline
USB Mic (44100Hz, S16_LE)
→ PyAudio (30ms frames, 16000Hz resampled)
→ webrtcvad.Vad(mode=2) # mode 2 = balanced sensitivity
→ Frame classification: voiced | unvoiced
→ State machine: 8 voiced → CRY_START, 25 unvoiced → CRY_END
5.2 Intensity Classification
RMS amplitude is computed per frame during active cry events:
def classify_intensity(rms: float) -> str:
if rms < 300:
return "mild" # Fussing, stirring
elif rms < 800:
return "moderate" # Real cry, attention needed
else:
return "severe" # Urgent, immediate alert
5.3 Alert Thresholds
| Condition | Duration | Action |
|---|---|---|
| Any cry detected | Immediate | Log to DB |
| Cry continues | > 60s | Pushover HIGH priority |
| Cry continues | > 120s | Pushover EMERGENCY (repeats until acknowledged) |
| Cry resolves | On silence | Update DB record with duration |
5.4 Performance Characteristics
- False positive rate: approximately 8% (environmental noise, primarily HVAC)
- False negative rate: approximately 3% (very quiet fussing below VAD threshold)
- Detection latency: 240ms (8 × 30ms frames)
- Resolution latency: 750ms (25 × 30ms frames)
6. Layer 4: Database — SQLite WAL Architecture
6.1 Schema
CREATE TABLE sleep_sessions (
id INTEGER PRIMARY KEY,
start_time DATETIME NOT NULL,
end_time DATETIME,
duration_minutes REAL,
position TEXT,
INDEX idx_sleep_start (start_time)
);
CREATE TABLE feed_sessions (
id INTEGER PRIMARY KEY,
start_time DATETIME NOT NULL,
duration_minutes INTEGER,
feed_type TEXT, -- bottle|breast|solids
INDEX idx_feed_start (start_time)
);
CREATE TABLE cry_events (
id INTEGER PRIMARY KEY,
start_time DATETIME NOT NULL,
end_time DATETIME,
duration_seconds REAL,
intensity TEXT, -- mild|moderate|severe
resolved BOOLEAN DEFAULT FALSE,
INDEX idx_cry_start (start_time)
);
CREATE TABLE vision_observations (
id INTEGER PRIMARY KEY,
camera_id TEXT NOT NULL,
timestamp DATETIME NOT NULL,
baby_state TEXT,
position TEXT,
face_visible BOOLEAN,
safety_concern BOOLEAN,
people_present TEXT, -- JSON array
observation TEXT,
INDEX idx_vision_ts (camera_id, timestamp)
);
-- Additional tables: diaper_events, events, daily_summaries
6.2 Concurrency Configuration
Nine services write to the same database concurrently. Configuration:
PRAGMA journal_mode=WAL; # Write-Ahead Logging
PRAGMA synchronous=NORMAL; # Safe with WAL, 5x faster than FULL
PRAGMA cache_size=-64000; # 64MB cache
PRAGMA temp_store=MEMORY;
connect_timeout=30 # Wait 30s on lock, don't fail
Result: Zero lock contention errors in production across 9 concurrent writers.
6.3 Storage Projections
| Data type | Daily volume | Annual projection |
|---|---|---|
| Vision observations | ~200 records × 200 bytes | ~14MB |
| Health events (feeds/cries/diapers) | ~20 records × 100 bytes | ~0.7MB |
| Daily summaries | 1 record × 500 bytes | ~0.2MB |
|
| |
|
Automated cleanup runs at 2 AM daily: vision observations older than 7 days are purged, events older than 90 days are archived, VACUUM is run weekly.
7. Layer 5: LLM Q&A — Grounded Natural Language Interface
7.1 Context Assembly
Each WhatsApp query assembles a context window from three sources:
def build_context(query: str, camera_id: str) -> str:
# 1. Current vision state (what Claude sees right now)
current_obs = vision_memory.get_latest(camera_id)
# 2. Recent observation history (last 8 hours with timestamps)
recent_obs = vision_memory.get_recent(camera_id, hours=8)
# 3. Today's health records from SQLite
today_data = db.get_today_summary() # sleep, feeds, cries, diapers
# 4. Recent chat history (last 3 exchanges for context continuity)
chat_history = conversation_store.get_recent(n=3)
return f"""
CURRENT STATE: {current_obs}
RECENT OBSERVATIONS (last 8h): {recent_obs}
TODAY'S HEALTH DATA: {today_data}
RECENT CHAT: {chat_history}
QUERY: {query}
"""
Total context: approximately 1,800–2,200 tokens per query.
7.2 Model and Latency
- Model:
llama-3.3-70b-versatilevia Groq API - Average response latency: 1.8s (P50), 2.9s (P95)
- Monthly cost: $0 (Groq free tier covers current query volume)
- Rate limit: 6,000 tokens/minute — never reached in production
7.3 Smart Narration — Activity Change Detection
The system generates automatic WhatsApp messages when activity changes are detected, without requiring user queries:
def should_send_narration(
current_state: dict,
previous_state: dict,
last_message_time: datetime
) -> bool:
# Always send on safety concerns
if current_state.get("safety_concern"):
return True
# Minimum 30-minute gap between activity messages
if (datetime.now() - last_message_time).seconds < 1800:
return False
# Send on meaningful state changes
state_changed = (
current_state["baby_state"] != previous_state["baby_state"] or
set(current_state["people_present"]) !=
set(previous_state["people_present"])
)
return state_changed
8. Layer 6: Photo Intelligence — Multimodal Activity Logging
Photos sent via WhatsApp are processed by Claude Haiku Vision for automatic activity classification:
PHOTO_CLASSIFICATION_PROMPT = """
Analyze this photo and classify the activity.
Return JSON:
{
"activity_type": "feed|diaper|bath|tummy_time|milestone|sleep|other",
"confidence": 0.0-1.0,
"duration_estimate_minutes": integer or null,
"notes": "brief observation"
}
Minimum confidence for auto-logging: 0.60
"""
Activities with confidence ≥ 0.60 are automatically logged to SQLite. Below threshold, the system requests confirmation before logging.
Classification accuracy by activity type (sampled over 30 days):
| Activity | Precision | Recall |
|---|---|---|
| Feed (bottle/breast) | 94% | 91% |
| Tummy time | 89% | 87% |
| Bath | 97% | 94% |
| Diaper change | 85% | 82% |
| Milestone | 78% | 71% |
9. Alert and Notification System
9.1 Alert Priority Matrix
| Event | Channel | Pushover Priority | Bypasses Night Mode |
|---|---|---|---|
| Face covered | WhatsApp + Pushover | EMERGENCY (repeats) | Yes |
| Safety concern | WhatsApp + Pushover | HIGH | Yes |
| Cry > 60s | Pushover | HIGH | No |
| No feed > 3.5h | Pushover | NORMAL | No |
| Sleep > 4.5h | Pushover | NORMAL | No |
| Activity narration | | N/A | No |
| Tummy time detected | Pushover | LOW | No |
9.2 Deduplication
COOLDOWN_PERIODS = {
"activity_update": 1800, # 30 minutes
"safety_alert": 600, # 10 minutes
"cry_alert": 300, # 5 minutes
"feed_reminder": 12600, # 3.5 hours
"sleep_reminder": 16200, # 4.5 hours
}
9.3 Night Mode
Single WhatsApp command (/night) sets NIGHT_MODE=True. All non-safety alerts are suppressed. Safety alerts (face covered, safety concern) bypass night mode unconditionally.
10. Security Architecture
10.1 Twilio Webhook Validation
Every incoming WhatsApp message validates the Twilio HMAC signature:
from twilio.request_validator import RequestValidator
def validate_twilio_request(request) -> bool:
validator = RequestValidator(TWILIO_AUTH_TOKEN)
return validator.validate(
request.url,
request.form,
request.headers.get('X-Twilio-Signature', '')
)
Requests failing validation return HTTP 403 and are logged.
10.2 Access Control
AUTHORIZED_NUMBERS = {"+1XXXXXXXXXX", "+1XXXXXXXXXX"} # env var
def is_authorized(phone_number: str) -> bool:
return phone_number in AUTHORIZED_NUMBERS
10.3 Rate Limiting
RATE_LIMITS = {
"messages_per_minute": 10, # Per authorized number
"camera_queries_per_30s": 1, # Vision API protection
"photo_analysis_per_minute": 3, # Vision API protection
}
10.4 Secrets Management
All API keys stored as environment variables, loaded via python-dotenv. Never logged, never hardcoded. Cloudflare Tunnel eliminates port forwarding requirements — no inbound firewall rules needed.
11. Networking — Cloudflare Tunnel
Twilio requires a public HTTPS endpoint for WhatsApp webhook delivery. Cloudflare Tunnel provides this without static IP or port forwarding:
Twilio → https://[uuid].trycloudflare.com
→ Cloudflare Edge
→ cloudflared daemon (Pi)
→ Flask webhook (localhost:5001)
The tunnel is managed as a systemd service with Restart=always. Tunnel reconnection is automatic. The public URL is stable across reconnections when using a named tunnel with a configured domain.
12. Automated Operations
12.1 Health Monitoring (Every 15 Minutes)
HEALTH_CHECKS = [
check_all_services_running, # systemctl is-active for each
check_camera_freshness, # last frame < 5 minutes old
check_database_integrity, # PRAGMA integrity_check
check_disk_space, # alert if < 10GB free
check_pi_temperature, # alert if > 80°C
check_api_credentials, # test Anthropic + Groq connectivity
check_tunnel_connectivity, # HTTP GET to webhook URL
]
12.2 Automated Backup (3 AM Daily)
def backup_database():
source = sqlite3.connect(DB_PATH)
backup = sqlite3.connect(BACKUP_PATH)
source.backup(backup) # Hot backup — consistent under concurrent writes
backup.close()
source.close()
7-day rolling retention. Backup completes in under 1 second for the current database size (~1.1MB).
12.3 Automated Cleanup (2 AM Daily)
- Vision observations older than 7 days: purged
- Events older than 90 days: archived to separate table
- Weekly
VACUUMto reclaim space - Projected annual storage without cleanup: ~15MB
- Projected annual storage with cleanup: ~3MB
13. API Cost Summary
| Service | Usage | Monthly Cost |
|---|---|---|
| Claude Haiku Vision | ~51,840 images/mo (with dark frame skip) | ~$1.50–$2.50 |
| Groq LLaMA-70b | ~500 queries/mo | Free tier |
| Twilio WhatsApp | < 1,000 messages/mo | Free sandbox |
| Pushover | Unlimited alerts | $5 one-time |
| Cloudflare Tunnel | Always-on | Free |
| Gmail SMTP | 2 emails/day | Free |
|
| |
|
14. Performance Characteristics
| Metric | Value |
|---|---|
| Vision analysis interval | 30 seconds |
| Vision API latency (P50) | 1.2s |
| Vision API latency (P95) | 2.8s |
| WhatsApp Q&A latency (P50) | 1.8s |
| WhatsApp Q&A latency (P95) | 2.9s |
| Cry detection latency | 240ms |
| Pi 5 CPU utilization (idle) | 12–18% |
| Pi 5 CPU utilization (vision processing) | 35–45% |
| Pi 5 temperature (sustained load) | 62–68°C |
| RAM utilization (all 9 services) | 1.8–2.4GB |
| Database size (3 months) | ~1.1MB |
| System uptime (measured period) | 99.2% |
15. Known Issues and Limitations
Ring WebRTC library bug: RuntimeError: Task cannot await on itself appears in logs but does not crash the service or affect functionality. This is a known issue in the ring_doorbell library's asyncio implementation.
Vision API dark frame behavior: Frames under 40KB are skipped entirely. In unusual lighting conditions (IR nightlight variations), the threshold may need adjustment.
Cry detection false positives: The WebRTC VAD mode 2 setting produces approximately 8% false positives from environmental noise. Mode 3 (most aggressive filtering) reduces false positives to ~3% but increases false negatives to ~12%. Mode 2 is the production choice.
Groq free tier limits: LLaMA-70b on Groq has a 6,000 tokens/minute limit. Current usage (~500 queries/month) is well within limits. At significantly higher query volumes, rate limiting may become a factor.
SQLite vs PostgreSQL: SQLite WAL handles 9 concurrent writers without contention at current write volume (~100 writes/hour). At significantly higher write rates, migration to PostgreSQL would be warranted.
16. Tech Stack Reference
| Layer | Technology | Version |
|---|---|---|
| OS | Raspberry Pi OS (Debian 12) | Linux 6.12 |
| Language | Python | 3.11 |
| Vision AI | Claude Haiku (Anthropic) | claude-haiku-4-5 |
| Vision fallback | Llama 4 Scout (Groq) | llama-4-scout |
| Language AI | LLaMA-3.3-70b (Groq) | llama-3.3-70b-versatile |
| Audio VAD | webrtcvad | 2.0.10 |
| Audio I/O | PyAudio | 0.2.14 |
| Camera | ring_doorbell | 0.9.13 |
| Database | SQLite | 3.45 (WAL mode) |
| Web framework | Flask | 3.0.3 |
| Dashboard | Streamlit | 1.35.0 |
| SMS/WhatsApp | Twilio | 9.1.0 |
| Notifications | Pushover | HTTP API |
| Tunnel | cloudflared | 2024.x |
| Service manager | systemd | 254 |
| Computer vision (local) | OpenCV | 4.10 |
17. Conclusion
BabyMind AI demonstrates that production-grade AI inference pipelines can be deployed on commodity embedded hardware at minimal cost. The key architectural decisions — SQLite WAL for concurrent writes, WebRTC VAD for low-latency audio processing, Claude Haiku for cost-efficient vision analysis, and WhatsApp as the user interface — each individually reduce complexity compared to alternatives.
The system processes approximately 51,840 vision frames per month, handles 9 concurrent service writers to a single database without contention, and delivers natural language Q&A responses grounded in real-time observation data in under 2 seconds — all on a $60 device drawing under 10 watts.
The codebase is approximately 3,500 lines of Python across 20 files. It will be open-sourced.
Hardware cost: ~$60. Monthly operating cost: under $5. Codebase: ~3,500 lines Python. Services: 9 systemd units. Vision API calls: ~51,840/month. Database: SQLite WAL, 7 tables. Uptime: 99.2%.
Tags: raspberry-pi · computer-vision · llm · iot · python · sqlite · systemd · claude · groq · whatsapp-api