Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ReelScope

Semantic search over your saved Instagram Reels — running entirely on your own hardware.

CI Python Platform License: MIT

You saved 2999 Reels. You remember one of them had a purple car in it. Instagram gives you an endless grid and no way to ask.

ReelScope watches each video with a vision-language model, listens to it with Whisper, reads the text on screen, and indexes the result so you can just ask for what you remember — by object, action, mood, spoken advice, or on-screen text. No cloud API, no third-party service: every model runs locally on vLLM (NVIDIA) or MLX / LM Studio (Apple Silicon).


Demo

Real queries answered by the Telegram bot, against an index built from 3,395 saved posts — the corpus this project was developed and tested on.

Query: exercises in the gym for shoulders
“exercises in the gym for shoulders”
Finds the movement, not the hashtag.
Query: series of motivational quotes
“series of motivational quotes”
Matches text rendered inside the video.
Query: purple or pink car with fish design
“purple or pink car with fish design”
The video from the top of this page — found from memory alone.
Query: neon streets of Japan at night
“neon streets of Japan at night”
Place, time of day, and mood in one query.

How it works

Ingestion — from a saved post to an indexed document

flowchart TD
    A[Saved collection<br/>Playwright] --> B[(URL registry · SQLite)]
    B --> C[Download<br/>gallery-dl · yt-dlp]
    C --> D[Adaptive frame sampling<br/>ffmpeg]
    C --> E[Speech-to-text<br/>Whisper]
    D --> F[Vision-language model<br/>Qwen3.5]
    E --> F
    F --> G[Structured description<br/>JSON]
    G --> H[(Dense vectors<br/>sqlite-vec)]
    G --> I[(Lexical index<br/>FTS5)]
Loading

Retrieval — from a question to a ranked answer

flowchart TD
    Q([Search query]) --> H[Dense retrieval<br/>Qwen3-Embedding]
    Q --> I[Lexical retrieval<br/>BM25 · FTS5]
    H --> J[Reciprocal rank fusion]
    I --> J
    J --> K[Cross-encoder rerank<br/>Qwen3-Reranker]
    K --> L([Ranked results<br/>CLI · Telegram bot])
Loading

Retrieval is a two-stage pipeline, not a single vector lookup:

  1. Dense retrieval — the VLM description is embedded with Qwen3-Embedding-0.6B (1024-d, stored in sqlite-vec). The retrieval instruction is applied to the query only; documents are embedded bare.
  2. Lexical retrieval — transcript, caption, hashtags, and on-screen text go into SQLite FTS5 with independently tunable BM25 field weights.
  3. Fusion — the two rankings are merged with reciprocal rank fusion, so neither branch can dominate on raw score scale.
  4. RerankingQwen3-Reranker-0.6B reads each query/description pair and scores the probability that they match. Because those are absolute probabilities rather than ranks, a threshold can return fewer results instead of padding the list with noise.

Features

  • Multimodal understanding — frames, speech, and on-screen text are fused into one description per post.
  • Structured output — category, mood, topics, objects, actions, setting, and language, not just a blob of prose.
  • Hybrid search — dense + BM25 + RRF + cross-encoder reranking, all tunable per request.
  • Multilingual — queries and descriptions share one embedding space, so the query language does not have to match the video's.
  • Fully local — vLLM on CUDA, MLX / LM Studio on Apple Silicon. Weights and data never leave the machine.
  • Telegram bot — the whole pipeline (export, ingest, search, status, settings, cookie rotation) from your phone.
  • Resumable by design — every URL carries independent processing and embedding lifecycle state, so an interrupted run picks up exactly where it stopped.

Stack

Every model runs on both platforms — only the runtime underneath changes.

Layer Model Linux · NVIDIA macOS · Apple Silicon
Vision-language Qwen3.5 vLLM server LM Studio server
Embeddings Qwen3-Embedding-0.6B · 1024-d vLLM server MLX in-process, 4-bit
Reranking Qwen3-Reranker-0.6B vLLM server MLX in-process
Speech Whisper large-v3-turbo faster-whisper · CUDA mlx-whisper
Storage SQLite + sqlite-vec + FTS5
Media ffmpeg, gallery-dl, yt-dlp, Playwright
Interface argparse CLI, aiogram 3 Telegram bot
Tooling uv, ruff, pytest, Docker Compose, GitHub Actions

Hardware

The models are never all resident at once. Each stage loads what it needs, does its work, and releases the memory before the next one starts — which is what makes a 16 GB laptop a viable target rather than a compromise.

Requirement
Linux · NVIDIA 24 GB VRAM for the default Qwen3.5-9B at bf16 with a long context. Smaller context or a quantized model fits less.
macOS · Apple Silicon 16 GB unified memory. Developed and tested on a MacBook Air M4 (16 GB) running the 9B model.

On Apple Silicon the memory-hungry stages are staged rather than stacked. Whisper transcribes videos in batches of 25 and its MLX weights are explicitly released — ModelHolder.model = None, gc.collect(), mx.clear_cache() — before the vision-language model runs inference on that same batch. LM Studio serves the VLM for the duration of the describe stage and is shut down when it ends; only then does the embed stage load the MLX embedding model in-process.

On Linux the same discipline is automated through Docker Compose: the ingest and search commands start the container they need, wait for the model, and stop it when the stage finishes.

With 8 GB, drop to a smaller vision-language model — the 9B will not leave room for the rest of the pipeline.


Quickstart

git clone https://github.com/matarseks/ReelScope.git
cd ReelScope
uv sync

.env is created from the matching platform profile on first run. Bring up the inference backend for your platform first — Docker Compose on Linux, or LM Studio on macOS:

docker compose -f infra/vllm/docker-compose.yml up -d   # Linux / NVIDIA

Option A — through the Telegram bot (easiest)

The bot walks you through the two things that need credentials, so there is no file to place by hand.

uv sync --extra telegram --extra browser
uv run playwright install chromium
# set TELEGRAM_BOT_TOKEN in .env
uv run reelscope bot

Send /start. The first user to do so claims the bot and is written to the allowlist. Onboarding then asks for two things in order:

  1. Instagram cookies — export a Netscape cookies.txt while logged in (the bot links the browser extension it expects) and send the file into the chat. They are stored locally at data/assets/www.instagram.com_cookies.txt.
  2. Your Instagram username — saved as INSTAGRAM_SAVED_URL so Export saved knows which collection to read.

After that, Export saved, Ingest, and Search are buttons. Cookies grant access to your Instagram session — treat them exactly like a password, and rotate them with /update_cookies when Instagram invalidates them.

Option B — through the CLI

Same credentials, placed manually. export-saved drives a real browser, so it needs the browser extra and a cookie file — it exits immediately if the file is missing.

uv sync --extra browser
uv run playwright install chromium

# Export cookies.txt while logged into Instagram, then:
cp ~/Downloads/www.instagram.com_cookies.txt data/assets/

uv run reelscope export-saved --url "https://www.instagram.com/USERNAME/saved/all-posts/"
uv run reelscope ingest-content
uv run reelscope search "morning coffee tutorial"

Override the cookie location with INSTAGRAM_COOKIES_FILE or --cookies. Full walkthrough: Getting started.

Without Instagram at all

Nothing in the description stage is tied to Instagram, and this path needs no cookies or browser. ingest-content takes a local video, a folder of videos, an ordered image carousel, or a text file of paths. For a folder, each video is stored and embedded separately in data/local_dirs.sqlite3:

uv run reelscope ingest-content ./my-videos
uv run reelscope search "morning workout" --database data/local_dirs.sqlite3

Use describe-content when you only want the structured JSON export.


What the pipeline produces

Each described post becomes a structured record. Fields are populated only when the source actually supplied them.

{
  "summary": "A person performs lateral raises with dumbbells in a gym, then demonstrates a seated overhead press. Text overlays name each movement and the target muscle head.",
  "category": "fitness",
  "topics": ["shoulder training", "lateral raise", "overhead press", "dumbbell workout"],
  "objects": ["dumbbells", "weight bench", "gym mirror"],
  "actions": ["lateral raise", "seated overhead press", "rest between sets"],
  "setting": "indoor gym",
  "text_on_screen": "SIDE DELTS · 3x12",
  "mood": "energetic",
  "language": "en"
}

Search returns a versioned envelope containing the effective request parameters and the ranked hits, so a result is reproducible from its own payload:

{
  "schema_version": 2,
  "request": {
    "query": "exercises in the gym for shoulders",
    "mode": "hybrid",
    "top_k": 5,
    "rerank": true,
    "rerank_threshold": 0.0
  },
  "hits": [
    {
      "rank": 1,
      "score": 0.94,
      "url": "https://www.instagram.com/reel/EXAMPLE123/",
      "post_type": "reel",
      "summary": "A person performs lateral raises with dumbbells in a gym…",
      "category": "fitness",
      "mood": "energetic",
      "actions": ["lateral raise", "seated overhead press"],
      "objects": ["dumbbells", "weight bench"],
      "topics": ["shoulder training", "dumbbell workout"],
      "model": "Qwen/Qwen3-Embedding-0.6B",
      "dimension": 1024,
      "vector_rank": 2,
      "lexical_rank": 1,
      "vector_score": 0.71,
      "fts_score": 8.42,
      "rerank_score": 0.94
    }
  ]
}

vector_rank, lexical_rank, and rerank_score are kept in the output on purpose: you can see which retrieval branch found a result and how much the cross-encoder moved it.


Project layout

src/reelscope/
├── ingest/     Saved-collection export, downloads, URL registry
├── describe/   Frame sampling, VLM prompting, Whisper, result assembly
├── embed/      Embeddings, BM25, RRF fusion, reranking, search CLI
├── storage/    SQLite schema, migrations, sqlite-vec and FTS5 backends
├── bot/        aiogram routers, FSM onboarding, formatting
└── config/     Layered YAML + env configuration

Documentation


Development

uv run ruff format --check .
uv run ruff check .
uv run pytest

CI runs formatting, linting, and the full test suite on every push.


License

MIT — see LICENSE.

ReelScope is a personal research tool for content you already saved. You are responsible for how you use it and for complying with the terms of any platform you point it at.

About

Semantic search over your saved Instagram Reels. A vision-language model, Whisper, and on-screen text are fused into one index; hybrid dense + BM25 retrieval with cross-encoder reranking. Runs entirely on your own hardware — vLLM on NVIDIA, MLX on Apple Silicon.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages