Skip to content
RobinMillfordPublic

About

This is a Flask web application that allows users to get recommendations for movies and TV shows with Ai integrated based on genres and specific titles

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

381 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FrameIQ

FrameIQ

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.

Live Demo CI/CD License Python Flask LangGraph Stars


🎬 What is FrameIQ?

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

✨ Features

πŸ“š Library & Tracking

  • 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

πŸ“Ί TV Show Tracking (First-Class)

  • 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

πŸ‘₯ Social Layer

  • 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

▢️ Inline Watching

  • 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

πŸ” Discovery & Search

  • 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"

πŸ€– CineBot β€” Multi-Agent AI Assistant

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 AsyncSqliteSaver checkpointer (no cross-loop use); graph + checkpoint verified before headers commit or the API returns 503 JSON (mid-stream failures arrive as SSE error events, 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-Remaining headers

πŸ“Š Personal Analytics

  • 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

πŸ— Tech Stack

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

🎨 Design System

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.


πŸš€ Quick Start

Prerequisites

Local Development

# 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:5000

Run Tests

pytest tests/ -v        # offline suite (~7 min), no TMDb key needed
uv run flake8 .         # lint (max-line 127, complexity 10)

Docker (Production Stack)

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   # status of the versioned migration ledger (READ-ONLY)

Schema changes (new tables AND new columns)

The 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.

Application startup never creates schema. There is no db.create_all() at runtime: not for new tables, not for new columns. Every schema change β€” new table or new column β€” is applied by the versioned migration runner, before the new web image starts:

git pull origin main
docker compose build web
docker compose run --rm --no-deps web \
  python scripts/migrate.py init-ledger
docker compose run --rm --no-deps web \
  python migrates/migrate_schema_convergence.py
docker compose run --rm --no-deps web \
  python scripts/migrate.py upgrade --allow-production
docker compose up -d
docker compose exec web python -m utils.schema_guard

Run the migrations in that order β€” they are the same steps the deploy workflow runs. Do not run individual migrates/migrate_*.py scripts: they are historical, unregistered, and several drop tables. scripts/migrate.py is the only supported path, and it records what it applied in the schema_migrations ledger so it can be verified afterwards.

docker 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.


βš™οΈ Environment Variables

Copy .env.example β†’ .env and 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)

πŸ“ Project Structure

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

πŸ”„ CI/CD Pipeline

Workflow Trigger Actions
ci-cd.yml Push to main/develop offline pytest + 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.


πŸ§ͺ Testing & Quality

The ordinary test suite is offline: no TMDB_API_KEY, no DNS, no internet.

# Full offline suite (the default, and what CI runs)
pytest tests/

# Smoke only
pytest tests/test_basic.py

# Models
pytest tests/test_models.py

# Single test
pytest -k "test_name"

How the offline boundary works

Every TMDb payload is served from deterministic fixtures, so tests exercise the real cache, fetchers, parsers and resolvers against fake payloads rather than against a live service:

Concern Where
Fixture payloads + fake transport tests/tmdb_offline.py
Network guard + per-test isolation tests/conftest.py
Shared deterministic TV fixtures tests/tv_fixtures.py
The contract itself (pinned) tests/test_tmdb_offline.py

requests is intercepted at requests.sessions.Session.send, so an unregistered TMDb endpoint returns the real "invalid API key" payload and any other host raises immediately. Raw sockets, socket.getaddrinfo, httpx and urllib are blocked too β€” an accidental request fails loudly with Unexpected network access during offline test: <url> rather than silently reaching api.themoviedb.org.

TMDb integration tests (opt-in, needs a real key)

Genuine external verification lives behind a marker and is deselected by default:

pytest -m tmdb             # real TMDb; requires TMDB_API_KEY in the env
pytest -m "not tmdb"       # the offline suite (same as plain `pytest tests/`)

Browser/data verification scripts under scripts/ (verify_tv_*_browser.py) are a separate, manual layer: they use a configured TMDb key and are never part of pytest tests/.

Quality gates: offline suite green with no TMDb secret β€’ flake8 clean β€’ vulture clean β€’ 0 secrets β€’ 0 dead code


🀝 Contributing

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 main

πŸ“„ License

Licensed 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


πŸ™ Acknowledgements


Built with ❀️ by Robin Millford and contributors.
Star ⭐ the repo if you find it useful β€” it helps more people discover FrameIQ.

About

This is a Flask web application that allows users to get recommendations for movies and TV shows with Ai integrated based on genres and specific titles

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages