A cinematic social movie & TV platform with a multi-agent AI assistant.
Track, discover, and discuss what you watch β powered by LangGraph, Flask, and TMDb.
FrameIQ is a Letterboxd-inspired social platform for film and TV enthusiasts, elevated with a production-grade multi-agent AI assistant (CineBot) that understands natural language, recommends by vibe, and streams its reasoning in real-time.
Built for people who care about what they watch β and want an intelligent companion to help them discover more.
| Track everything you watch | Discover by mood, genre, era | Discuss with friends & community |
| Watch inline with resume | Analyze your taste DNA | Chat with an AI film expert |
- Watchlist with priority tiers (High / Medium / Low)
- Diary β chronological log with dates, ratings, notes
- Viewed library with rich filtering
- Star ratings (Β½β5β ) + written reviews with markdown
- Custom lists β public, private, collaborative
- Tags with autocomplete & trending suggestions
- Episode-by-episode progress with timestamps
- Batch season operations β mark entire seasons watched
- Smart completion β only marks Completed when TMDb confirms series ended
- Upcoming Episodes calendar β 60-day view, auto-synced daily
- Continue Watching row with resume points
- Follow / unfollow with activity feeds (Following / Global / Personal)
- Friends' activity on every title page
- Popular With Friends algorithmic ranking
- Review likes, comments, helpful votes
- Suggested follows based on taste overlap
- Stream movies & episodes directly β no redirects
- Resume playback with per-title progress persistence
- Auto-logs diary entry at 85% completion
- Full watch history with timestamps
- TMDb-powered filters: genre, year, language, rating, provider
- Trending movies & shows (daily/weekly)
- Entertainment news feed (NewsAPI)
- Semantic similarity β "movies that feel like a rainy Sunday"
Architecture: LangGraph pipeline
User message
β
βΌ
βββββββββββββββ
β Supervisor β β zero-LLM heuristic router (saves 2β3 API calls)
ββββββββ¬βββββββ
β
βββββ΄ββββ
βΌ βΌ
βββββββββ βββββββ
βRetrieverβ β Chatβ β Retriever: 7 TMDb/vector tools
β β β β Chat: open-ended film knowledge
βββββ¬ββββ ββββ¬βββ
ββββββ¬βββββ
βΌ
ββββββββββββ
β Enricher β β concurrent TMDb poster & metadata fetch
ββββββ¬ββββββ
βΌ
SSE stream β browser (markdown + poster cards)
| Agent | Role | Model |
|---|---|---|
| Supervisor | Route + extract entities (titles, people, genres, years) | gpt-4.1-mini (structured) |
| Retriever | ReAct agent with 7 cached tools | gpt-4.1-mini |
| Chat | General film knowledge, recommendations | gpt-5-mini |
| Enricher | Extract titles from reply β fetch posters/meta | gpt-4.1-mini |
- Streaming responses via SSE β see each tool call as it happens
- Poster & metadata cards injected inline in replies
- Conversation memory persisted (SQLite checkpointing, survives restarts)
- User personalization β ratings, favorite genres, TV progress, watchlist injected per request
- Command palette (βK) + Slide-over panel (βJ) for keyboard-first UX
- Production-hardened streaming: single process event loop owns the
AsyncSqliteSavercheckpointer (no cross-loop use); graph + checkpoint verified before headers commit or the API returns503 JSON(mid-stream failures arrive as SSEerrorevents, never a status flip); per-stream checkpoint threads so concurrent chats can't collide - Limits: 5 questions per user per UTC day, 20/min + 100/hour per IP β
the UI shows remaining quota live via
X-Chat-Remainingheaders
- Watch time, genre breakdown, top directors/actors
- Year-in-review with interactive Chart.js visualizations
- Taste DNA β genre affinity bars with gradient fills
- Taste Match badges on every title page
| Layer | Technology |
|---|---|
| Backend | Flask 3.1, SQLAlchemy 2.0, PostgreSQL 16 |
| AI / Agents | LangGraph, LangChain, OpenAI (gpt-4.1-mini / gpt-5-mini) |
| Auth | Google OAuth 2.0 (Authlib) + Flask-Login + Flask-WTF CSRF |
| Frontend | Jinja2, Tailwind CSS, Vanilla JS (ESM), Chart.js, Lucide icons |
| Media APIs | TMDb, Cloudinary (avatars), NewsAPI |
| Streaming | Rive, VidKing, Vidy, 1Embed providers |
| Infra | Docker Compose, systemd-nginx (reverse proxy), GitHub Actions |
| Observability | Langfuse-ready, structured logging |
Monochrome Marquee β true black (#000), white type, single amber accent (#F6B73C)
- Display: Archivo Expanded (variable width)
- Body: Inter
- Metadata: JetBrains Mono
- Motion: cubic-bezier(0.16, 1, 0.3, 1) β 180ms/320ms
- Radii: 4 / 6 / 10px β sharp, broadcast-grade
- Film grain overlay + projector beam/dust atmosphere
All 47 templates unified on a single design token system. Zero Poppins, zero indigo/violet, zero unused dependencies.
- Python 3.12+
- PostgreSQL 16 (local or Docker)
- API keys (see Environment Variables)
# 1. Clone & enter
git clone https://github.com/RobinMillford/FrameIQ.git
cd FrameIQ
# 2. Virtual environment
python -m venv .venv && source .venv/bin/activate
# 3. Dependencies (uv recommended)
uv sync
# or: pip install -r requirements.txt
# 4. Environment
cp .env.example .env
# Edit .env with your keys
# 5. Run
python app.py # http://localhost:5000pytest tests/ -v # 143 tests, ~45s
uv run flake8 . # lint (max-line 127, complexity 10)cp .env.example .env # production values
make deploy # pull + clean build + start (run from /opt/frameiq on the VPS)Stack: web (Gunicorn 2 workers Γ 4 threads, Python 3.11-slim) + db (Postgres 16-alpine). TLS terminates at the host nginx (ports 80/443, certbot); Postgres is bound to loopback only β never exposed to the internet.
make status # container status + /agent_health check
make logs # tail web logs (logs-db for Postgres)
make restart # restart web container
make backup # pg_dump snapshot via scripts/backup.sh
make db-shell # psql into the production DB
make migrate # create_all() for new tablesThe startup schema guard REQUIRES schema parity with the declared models
and will fail Gunicorn workers if the database is behind β that guard is
intentional and must not be weakened. New TABLES are created automatically
at startup (db.create_all()); COLUMN/index changes are not, so run the
matching migrates/ script BEFORE starting the new web image:
git pull origin main
docker compose build web
docker compose run --rm --no-deps web python migrates/<migration>.py
docker compose up -d
docker compose exec web python -m utils.schema_guarddocker compose run --rm --no-deps executes the migration against the
shared database in a one-off container (removed on exit, no port bindings,
no dependency restarts) β the migration scripts set SKIP_SCHEMA_GUARD=1
themselves, so the guard (enforced at application startup via
ensure_schema_compatible()) stays active for normal application runs
and never runs migrations for you. Never start web before its required
schema exists; the guard failing fast is the system protecting you from a
half-migrated deploy.
Chat memory (instance/chat_memory.db, WAL mode) persists on the chat_memory
volume across restarts and rebuilds.
Copy
.env.exampleβ.envand fill in.
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
β | Flask session secret (32+ chars) |
DATABASE_URL |
β (local dev) | postgresql://user:pass@host:5432/db β auto-built from POSTGRES_* in Docker |
TMDB_API_KEY |
β | TMDb API |
OPENAI_API_KEY |
β | OpenAI API key (chat + embeddings) |
GOOGLE_CLIENT_ID |
β | Google OAuth 2.0 |
GOOGLE_CLIENT_SECRET |
β | Google OAuth 2.0 |
CLOUDINARY_CLOUD_NAME |
β | Avatar uploads |
CLOUDINARY_API_KEY |
β | Cloudinary |
CLOUDINARY_API_SECRET |
β | Cloudinary |
CLOUDINARY_URL |
β | Cloudinary (alternative single-var config) |
STREAM_PROVIDER |
β | Default embed source: rive | vidking | vidy | oneembed |
RATELIMIT_STORAGE_URI |
β | Redis URI for multi-worker rate limits (defaults to memory) |
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD |
β (Docker) | Self-hosted DB credentials |
POSTGRES_EXPOSED_PORT |
β | Loopback-bound host port for the episode-sync workflow |
WEB_WORKERS / WEB_THREADS |
β | Gunicorn sizing (defaults 2 / 4 β do not raise blindly) |
FLASK_DEBUG |
β | Never true in production |
MAIL_SERVER / MAIL_* |
β | Password reset emails |
RATELIMIT_STORAGE_URI |
β | Redis for rate limiting (defaults to memory) |
FrameIQ/
βββ app.py # Application factory (create_app)
βββ models/ # SQLAlchemy models package (user, media, chat, β¦)
βββ extensions.py # Flask extensions (limiter, mail, db)
βββ requirements.txt
β
βββ routes/ # 28 Flask blueprints (one per domain)
β βββ auth.py # Login, register, password reset
β βββ main.py # Home, search, news, tonights-pick
β βββ details.py # Movie / TV detail pages
β βββ reviews.py # Reviews & ratings
β βββ diary.py # Watch diary
β βββ lists.py # Custom lists (basic + advanced)
β βββ tv_tracking.py # Episode & season tracking
β βββ chat.py # SSE streaming chat
β βββ social.py # Follow, activity feed
β βββ stats.py # Dashboard, year-in-review
β βββ ...
β
βββ src/
β βββ agents/ # LangGraph multi-agent system
β β βββ graph.py # StateGraph definition
β β βββ nodes.py # Supervisor, Retriever, Chat, Enricher
β β βββ tools.py # 7 LangChain tools (cached TMDb + history)
β β βββ state.py # GraphState schema
β βββ api/
β βββ agent_service.py # User context building, SSE streaming
β βββ flask_integration.py
β
βββ api/ # Shared utilities
β βββ tmdb_client.py # TMDb wrapper (compat shim)
β βββ tmdb/ # TMDb package: cache, movies, tv, people, search
β βββ stream_providers.py # Embed providers (Rive, VidKing, Vidy, 1Embed)
β βββ chatbot.py # LLM helpers
β
βββ templates/ # 47 Jinja2 templates (unified on base.html)
βββ static/
β βββ css/ # tokens.css, chrome.css, detail.css, projector.css
β βββ js/ # chat-page.js, chrome.js, projector-dust.js
β βββ images/
β
βββ scripts/ # Ops (excluded from Docker)
β βββ sync_upcoming_episodes.py
β βββ collect_media.py
β
βββ migrates/ # Manual schema migrations
βββ .github/workflows/ # ci-cd.yml, deploy.yml, sync-upcoming-episodes.yml
βββ nginx/nginx.conf # Reverse proxy + SSL
βββ docker-compose.yml
βββ Dockerfile
βββ Makefile
| Workflow | Trigger | Actions |
|---|---|---|
| ci-cd.yml | Push to main/develop |
pytest (143) + flake8 |
| deploy.yml | Push to main (after CI pass) |
SSH β VPS β make deploy |
| sync-upcoming-episodes.yml | Daily 02:00 UTC | Sync TMDb β PostgreSQL |
VPS: Self-hosted PostgreSQL 16, Docker Compose, system nginx, nightly pg_dump backups, certbot SSL.
Health: /health (app) and /agent_health (chat graph loaded?) β make status hits the latter.
# Full suite (143 tests)
pytest tests/
# Smoke only
pytest tests/test_basic.py
# Models
pytest tests/test_models.py
# Single test
pytest -k "test_name"Quality gates: 143 tests passing β’ flake8 clean β’ vulture clean β’ 0 secrets β’ 0 dead code
We welcome contributions! Please read CLA.md before submitting a PR.
Good first issues:
- π Add more languages/regions to TMDb discover tools
- π§ͺ Write missing unit tests in
tests/ - βΏ Improve accessibility (ARIA, keyboard nav)
- π Add chart types to stats dashboard
- π¨ Extend design token system
# 1. Fork & clone
git clone https://github.com/YOUR-USERNAME/FrameIQ.git
# 2. Branch
git checkout -b feat/your-feature
# 3. Develop
# ... make changes ...
# 4. Verify
pytest tests/ && uv run flake8 .
# 5. Push & PR
git push origin feat/your-feature
# Open PR against mainLicensed under GNU Affero General Public License v3.0 β see LICENSE.
Commercial use, SaaS hosting, or white-labelling requires a separate licence.
Contact:robinmill4d@gmail.com
- TMDb β The movie database powering our data
- LangGraph β Multi-agent orchestration
- Letterboxd β Product inspiration
- Archivo β’ Inter β’ JetBrains Mono β Typefaces
- Tailwind CSS β’ Chart.js β’ Lucide β Frontend toolkit
Built with β€οΈ by Robin Millford and contributors.
Star β the repo if you find it useful β it helps more people discover FrameIQ.