abigailkeegan's picture
Deploy ReelShield to Hugging Face Space
8af1084
|
Raw
History Blame Contribute Delete
20.6 kB

πŸ“Š Data Engineering - ReelShield

Overview

ReelShield uses SQLite for both local/Docker development and the deployed Hugging Face Space. All schema decisions are documented below.


Entity-Relationship Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              movies                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ tmdb_id       INTEGER  PK            β”‚
β”‚ title         TEXT     NOT NULL      β”‚
β”‚ year          TEXT                   β”‚
β”‚ imdb_id       TEXT                   β”‚
β”‚ runtime_min   INTEGER                β”‚
β”‚ metadata_json TEXT     (full blob)   β”‚
β”‚ last_updated  TEXT     (ISO 8601)    β”‚
β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚ 1           β”‚ 1           β”‚ 1
   β”‚             β”‚             β”‚
   β”‚ 1           β”‚ 1           β”‚ 1
β”Œβ”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚content_warn β”‚ β”‚movie_embeddngβ”‚ β”‚  movie_clusters   β”‚
β”‚-ings        β”‚ β”‚              β”‚ β”‚                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ tmdb_id  PK β”‚ β”‚ tmdb_id  PK  β”‚ β”‚ tmdb_id  PK       β”‚
β”‚ warnings_j  β”‚ β”‚ embedding    β”‚ β”‚ cluster_id        β”‚
β”‚ last_updatedβ”‚ β”‚ model_name   β”‚ β”‚ distance_to_cent  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ text_hash    β”‚ β”‚ model_version     β”‚
                β”‚ created_at   β”‚ β”‚ created_at        β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Relationships: Each movie has at most one cached warning set, one MiniLM embedding, and one K-Means cluster assignment (all 1:1). Warnings are regenerated only when explicitly refreshed; embeddings are written by backend/embed_all_movies.py (a one-pass batch over the cached films using the pretrained all-MiniLM-L6-v2 model); cluster assignments are written by backend/train_cluster_model.py (the K-Means trainer, the only classic-ML model on this side). Both self-heal on the HF Space at container cold start when missing.


Schema Documentation

Table: movies

Column Type Constraints Description
tmdb_id INTEGER PRIMARY KEY TMDB's unique film identifier
title TEXT NOT NULL Film title as returned by TMDB
year TEXT 4-digit release year
imdb_id TEXT IMDb identifier (for future cross-referencing)
runtime_min INTEGER Runtime in minutes
metadata_json TEXT Full JSON blob of TMDB + computed metadata
last_updated TEXT ISO 8601 timestamp of last cache update

Indexes:

  • tmdb_id (PRIMARY KEY - automatically indexed)
  • Recommended future index: title for text search

Why JSON blob? The metadata structure evolves as we add new data sources (TMDB keywords, OMDb ratings, etc.). Storing as JSON avoids schema migrations every time a new field is added, while still allowing the app to access structured data at runtime.


Table: content_warnings

Column Type Constraints Description
tmdb_id INTEGER PRIMARY KEY, FK References movies.tmdb_id
warnings_json TEXT Full JSON blob of Gemini-generated warnings
last_updated TEXT ISO 8601 timestamp

Warning JSON structure:

{
  "disclaimer": "Gemini knowledge-based",
  "spoiler_free": {
    "violence_gore":             {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "self_harm_suicide":         {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "miscarriage_pregnancy_loss":{"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "sexual_content_nudity":     {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "animal_abuse":              {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "substances":                {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "language":                  {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "horror_intensity":          {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "flashing_lights":           {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""}
  },
  "spoiler_full": {
    "violence_gore":             {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "self_harm_suicide":         {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "miscarriage_pregnancy_loss":{"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "sexual_content_nudity":     {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "animal_abuse":              {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "substances":                {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "language":                  {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "horror_intensity":          {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""},
    "flashing_lights":           {"severity": 0-3, "confidence": 0.0-1.0, "notes": ""}
  }
}

Severity scale: 0 = absent, 1 = mild/brief, 2 = moderate, 3 = severe/graphic Confidence scale: 1.0 = confirmed by AI knowledge, <1.0 = estimated (obscure film)

Why two blocks? spoiler_free and spoiler_full carry the same severity and confidence values for each category β€” only the notes text differs. The spoiler-mode toggle in the UI picks which notes to render; the underlying numbers describe the film itself, not the description style. Older cached rows may only contain spoiler_free; the frontend falls back to it when spoiler_full is absent.


Table: users

Column Type Constraints Description
id INTEGER PRIMARY KEY, AUTOINCREMENT Internal user id
username TEXT UNIQUE, NOT NULL Login name
password_hash TEXT NOT NULL werkzeug pbkdf2(sha256) hash
created_at TEXT NOT NULL ISO 8601 timestamp

Indexes: idx_users_username on username.


Table: reviews

Column Type Constraints Description
id INTEGER PRIMARY KEY, AUTOINCREMENT Review id
tmdb_id INTEGER NOT NULL, FK β†’ movies.tmdb_id Film being reviewed
user_id INTEGER NOT NULL, FK β†’ users.id Author
username TEXT NOT NULL Denormalized for fast display
rating INTEGER 1–5 stars; nullable for text-only reviews
review_text TEXT NOT NULL Body
created_at TEXT NOT NULL ISO 8601 timestamp

Indexes: idx_reviews_tmdb_id, idx_reviews_user_id.


Table: watchlist

Column Type Constraints Description
id INTEGER PRIMARY KEY, AUTOINCREMENT Row id
user_id INTEGER NOT NULL, FK β†’ users.id Owner
tmdb_id INTEGER NOT NULL Film
added_at TEXT NOT NULL ISO 8601 timestamp

Constraints: UNIQUE(user_id, tmdb_id) prevents duplicate entries.
Indexes: idx_watchlist_user_id.


Table: user_sensitivities

Column Type Constraints Description
user_id INTEGER PRIMARY KEY, FK β†’ users.id One row per user
sensitivities_json TEXT NOT NULL JSON array of warning-category strings the user always wants to avoid
updated_at TEXT NOT NULL ISO 8601 timestamp

Table: movie_loads

Instrumentation table. One row per /api/load_movie call.

Column Type Constraints Description
load_id INTEGER PRIMARY KEY, AUTOINCREMENT Load id
tmdb_id INTEGER NOT NULL Film loaded
title, year, genre, mpaa_rating TEXT Snapshot of movie metadata
was_cached INTEGER NOT NULL DEFAULT 0 0/1 boolean - cache hit
load_time_ms INTEGER End-to-end load latency
warnings_generated TEXT JSON array of triggered category names
confidence_scores TEXT JSON map of category β†’ confidence
spoiler_mode_on INTEGER NOT NULL DEFAULT 0 0/1 boolean
timestamp TEXT NOT NULL ISO 8601 timestamp

Indexes: idx_movie_loads_tmdb_id, idx_movie_loads_timestamp.


Table: warning_analytics

One row per (load_id, category). Used to compute per-category trigger rates and average confidence over time.

Column Type Constraints Description
warning_id INTEGER PRIMARY KEY, AUTOINCREMENT Row id
load_id INTEGER FK β†’ movie_loads.load_id The load this row belongs to
tmdb_id INTEGER NOT NULL Film
category TEXT NOT NULL Warning category name
was_triggered INTEGER NOT NULL DEFAULT 0 0/1 boolean
confidence_score REAL 0.0–1.0
severity_level INTEGER 0–3
user_flagged_inaccurate INTEGER NOT NULL DEFAULT 0 Reserved for a future "flag this warning" UI
flag_reason TEXT Reserved for the same
timestamp TEXT NOT NULL ISO 8601 timestamp

Indexes: idx_warning_analytics_tmdb_id, idx_warning_analytics_category.


Table: movie_embeddings

Sentence-transformer (all-MiniLM-L6-v2) embeddings of each film's title + overview + keywords. Used to re-rank recommendations and match free-text mood queries on the Discover page.

Column Type Constraints Description
tmdb_id INTEGER PRIMARY KEY, FK β†’ movies.tmdb_id (ON DELETE CASCADE) One row per film
embedding BLOB NOT NULL 384-dim float32 vector packed as bytes
model_name TEXT NOT NULL Embedding model identifier (all-MiniLM-L6-v2)
text_hash TEXT NOT NULL SHA1 of the source text β€” lets the pipeline re-embed only when the source string changes
created_at TEXT NOT NULL ISO 8601 timestamp

Populated by backend/embed_all_movies.py; consumed by backend/embeddings.py:cosine_top_k.


Table: movie_clusters

K-Means cluster assignment per film. Used by the "Content twins" UI section (/api/content_twins/<tmdb_id>) and the offline cluster-archetype analysis.

Column Type Constraints Description
tmdb_id INTEGER PRIMARY KEY, FK β†’ movies.tmdb_id (ON DELETE CASCADE) One row per film
cluster_id INTEGER NOT NULL 0..K-1; mapping to a human-readable label lives in the .pkl (cluster_names dict)
distance_to_centroid REAL Euclidean distance to the assigned centroid in 9-dim warning space
model_version TEXT NOT NULL E.g. kmeans_k5_seed42 β€” bumps when the model is retrained
created_at TEXT NOT NULL ISO 8601 timestamp

Populated by backend/train_cluster_model.py. On the HF Space, the table is also self-healed at cold start (see _populate_movie_clusters_if_empty() in backend/app.py) so the feature works even when the hydrated movie_cache.db pre-dates the K-Means commit.


Table: warning_feedback

Thumbs up / thumbs down feedback the user gives on specific warning categories and chat replies.

Column Type Constraints Description
id INTEGER PRIMARY KEY, AUTOINCREMENT Row id
user_id INTEGER Nullable - anonymous sessions may also leave feedback
movie_id INTEGER NOT NULL Film
category TEXT Warning category, or NULL for chat feedback
feedback_type TEXT NOT NULL 'warning' or 'chat'
rating TEXT NOT NULL 'up' or 'down'
chat_message_text TEXT The chat reply being rated, if applicable
created_at TEXT NOT NULL ISO 8601 timestamp

Indexes: idx_warning_feedback_movie_id, idx_warning_feedback_user_id.


Data Sources

Source Type What We Fetch Rate Limit
TMDB API REST Title, year, genres, runtime, rating, keywords, poster, overview 50 req/sec
Gemini 2.5 Flash AI API Content warnings (9 categories) 15 req/min (free), higher on paid
HF Dataset (abigailkeegan/reelshield-cache) File pull Pre-warmed movie_cache.db + classic-ML .pkl artifacts on cold start N/A
all-MiniLM-L6-v2 (sentence-transformers) Local model 384-dim embedding per film N/A β€” runs in-process
cluster_model.pkl (K-Means) Local model Cluster assignments + content-twins queries N/A β€” runs in-process
mpa_classifier.pkl (LogisticRegression) Local model Predicted MPA bucket for the disagreement badge N/A β€” runs in-process
SQLite Local DB Cached movies, warnings, embeddings, cluster assignments N/A

Data Flow

[Container cold start]
        β”‚
        β–Ό
Hydrate /data/movie_cache.db from HF Dataset (if missing/empty)
Hydrate /data/{cluster,mpa}_classifier.pkl from HF Dataset
Self-heal movie_clusters table if empty (uses cluster_model.pkl)
        β”‚
        β–Ό
[Request: User searches "Titanic"]
        β”‚
        β–Ό
TMDB /search/movie
        β”‚
        β–Ό
User selects result
        β”‚
        β–Ό
Check SQLite cache ──── HIT ──▢ Return cached warnings + run MPA classifier
        β”‚                       on cached warning vector β†’ optional badge
       MISS                     ──▢ Return to frontend
        β”‚
        β–Ό
TMDB /movie/{id}          (title, genres, runtime, rating)
TMDB /movie/{id}/keywords (community content tags)
TMDB /movie/{id}/release_dates (MPA certification)
        β”‚
        β–Ό
Gemini 2.5 Flash
  Input: title + year + rating + genres + overview + keywords
  Output: JSON with 9 warning categories
        β”‚
        β–Ό
Save to SQLite cache; run MPA classifier on the new warning vector
        β”‚
        β–Ό
Return to frontend (warnings + optional mpa_prediction)

Backup & Recovery Strategy

SQLite/Docker (current):

  • SQLite file is mounted at /data/movie_cache.db inside the container via the ./data bind mount in docker-compose.yml.
  • Backup: docker run --rm -v movie-warnings_movie_data:/data -v $(pwd):/backup alpine tar czf /backup/db-backup.tar.gz /data
  • On Hugging Face Spaces, the SQLite file lives on the Space's filesystem; production-grade durability is delegated to the planned RDS migration described below.

Planned production (RDS):

  • Automated daily snapshots via AWS RDS
  • Point-in-time recovery enabled
  • Cross-region snapshot copy for disaster recovery

Production Architecture (Planned: AWS RDS)

The current Hugging Face Space runs SQLite for demo simplicity, but the schema is designed to migrate to PostgreSQL on AWS RDS for production scale. The table layout above is portable as-is - every column type maps cleanly to PostgreSQL (INTEGER β†’ BIGINT/INTEGER, TEXT β†’ TEXT, REAL β†’ DOUBLE PRECISION) and the JSON blobs become JSONB for indexable queries.

Architecture diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Hugging Face Space / EC2                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Flask + Gunicorn (backend/app.py)                           β”‚  β”‚
β”‚  β”‚     ─ /api/search, /api/load_movie, /api/discover, …         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚               β”‚                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚ psycopg2 (5432, TLS in transit)
                β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚   AWS RDS - PostgreSQL 15          β”‚
       β”‚   ─ db.t3.micro (dev/MVP)          β”‚
       β”‚   ─ Multi-AZ (prod)                β”‚
       β”‚   ─ Encryption at rest (KMS)       β”‚
       β”‚   ─ Automated 7-day backups        β”‚
       β”‚   ─ Cross-region snapshot copy     β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β–²
                β”‚ /find/{imdb_id}, /movie/{id}, /search/movie
                β”‚ generate_content()
                β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚   TMDB API      β”‚ β”‚  OMDb API      β”‚ β”‚  Gemini 2.5 Flash β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

In production, the .pkl model artifacts would migrate from the public
HF Dataset to a private S3 bucket alongside the RDS schema, with model
versions referenced from a config table so retrains don't require code
pushes.

Migration plan (SQLite β†’ PostgreSQL RDS)

Step 1 - Provision RDS:

Setting Value Reason
Engine PostgreSQL 15 First-class JSONB, mature
Instance db.t3.micro Sufficient for demo/MVP traffic
Storage 20 GB gp2 Generous for movie cache
Multi-AZ No (dev), Yes (prod) Cost vs availability tradeoff
Backup 7-day retention Recovery window
Encryption At rest (KMS) + in transit (TLS) API key data in JSON blobs

Step 2 - Add the driver to requirements.txt:

psycopg2-binary==2.9.9

Step 3 - Wire DATABASE_URL into docker-compose.yml:

services:
  app:
    environment:
      DATABASE_URL: postgresql://user:pass@your-rds-endpoint:5432/reelshield

Step 4 - Swap the connection logic in backend/app.py:

import psycopg2
DATABASE_URL = os.environ.get("DATABASE_URL")
conn = psycopg2.connect(DATABASE_URL)

Step 5 - Run the migration scripts (001_initial_schema.sql, 002_users_and_social.sql, 003_movie_embeddings.sql, 004_movie_clusters.sql) against the new database. The DDL is SQLite-compatible but uses only standard SQL features, so it runs cleanly on PostgreSQL after replacing INTEGER PRIMARY KEY AUTOINCREMENT with BIGSERIAL PRIMARY KEY, changing the JSON TEXT columns to JSONB, and swapping the BLOB column on movie_embeddings to BYTEA.


Query Optimization

Current queries and their performance:

-- Lookup by tmdb_id (primary key) - O(1), instant
SELECT metadata_json FROM movies WHERE tmdb_id = ?;
SELECT warnings_json FROM content_warnings WHERE tmdb_id = ?;

-- Insert/replace - O(log n) on primary key index
INSERT OR REPLACE INTO movies VALUES (?,?,?,?,?,?,?);

Future optimization for scale:

-- Add index on title for text search
CREATE INDEX idx_movies_title ON movies(title);

-- Add index on last_updated to find stale cache entries
CREATE INDEX idx_warnings_updated ON content_warnings(last_updated);

Analytics Examples

A worked example of running analytics against this schema β€” average warning severity per category per decade, plus trend findings β€” is in decade-analysis.md. It includes the json_extract-based SQL used to compute the per-category averages from content_warnings.warnings_json.