ytscholar
# ytscholar ππΊ
**A local YouTube evidence layer (MVP).** It finds the top YouTube videos on
a topic, pulls their transcripts, and grows a local knowledge base β then
exposes searchable, source-aware **evidence** and **full transcripts** to LLM
clients, so the model can research, compare sources, and synthesize with
citations you can verify by clicking.
It runs entirely on your machine: no API keys, no cloud service, no
subscription. You can use it from the CLI or as an [MCP server](#mcp) inside
Claude Desktop, Cursor, Cline, or any MCP-compatible client.
> **Design boundary:** ytscholar collects, stores, and retrieves evidence.
> It does **not** reason β no claim extraction, no summarizing, no
> contradiction detection. Analysis and synthesis belong to the LLM client
> consuming this data.
> **Status: MVP.** The core loop β research β store β evidence retrieval with
> timestamped citations β works and is covered by offline tests (the network
> path was also validated manually against real YouTube). It is deliberately
> small. See [Limitations](#limitations) for what it is *not*.
## What it does
1. **Research a topic.** Give it a subject; it finds the top YouTube videos
for it, fetches their transcripts, and ingests them into a local knowledge
base.
2. **Transcribe a link.** Give it a video URL/id; it returns the full
transcript (with optional machine translation).
3. **Search what it has learned.** Ask a question; it retrieves the most
relevant passages from *everything ever ingested*, each with a deep link
that opens the source video at the right moment
(`https://youtu.be/VIDEO_ID?t=SECONDS`).
4. **Retrieve evidence, source-aware.** The same retrieval, grouped by video
and channel β so a model (or you) can see whether the evidence comes from
genuinely different sources or from one channel repeated.
No YouTube Data API key is required. Search uses `yt-dlp`; transcripts use
`youtube-transcript-api` with a `yt-dlp` caption-download fallback.
## Why
Fetching one transcript is a solved problem β several tools do it. ytscholar's
value is the **accumulation** and the **evidence layer**: every ingest grows a
persistent local knowledge base, and retrieval returns evidence with precise,
clickable sources plus source-diversity analysis. An LLM client can search for
evidence, judge how independent the sources are, and pull a full transcript
when it needs complete context β then do the actual reasoning itself. That
makes ytscholar a small "research memory" you own: a SQLite file you can back
up, inspect, or delete.
## Architecture
Exactly what the code does today β ytscholar ends at evidence; the LLM client
does the analysis:
```
YouTube
β
Video Discovery yt-dlp ytsearch{N} (YouTube's own ranking);
hard cap per run (default 15); 30-day cache
β
Full Transcript youtube-transcript-api (primary)
β yt-dlp caption download + VTT parse (fallback)
β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ
β Full Transcript β Chunking (~900 chars; each chunk keeps β
β Storage β its start timestamp) β FTS5 index β
βββββββββββββββββββββββ΄ββββββββββββββββββββββββ¬ββββββββββββββββββββ
β
Evidence Retrieval passages + video/channel grouping
(FTS5 bm25; optional experimental re-rank)
β
LLM client analysis + synthesis β not part of
(Claude / GLM / β¦) with verifiable citations ytscholar
```
ytscholar never discards the full transcript: chunking and FTS5 exist for
*retrieval*; `get_transcript` always returns the complete stored text.
## Features
Only what exists and works today:
- 5 CLI commands: `research`, `transcript`, `search`, `evidence`, `stats`
- 5 MCP tools over the same core: `research_topic`, `get_transcript`,
`search_knowledge`, `search_evidence`, `knowledge_stats`
- Keyword retrieval via SQLite FTS5 with bm25 ranking; optional topic filter
- **Evidence retrieval**: passages grouped by video and channel, with
`unique_channels`, `channel_distribution`, and an independence warning
(deterministic β channel variety only, no AI)
- Full transcripts kept in the DB and retrievable at any time
- Timestamped deep links on every search/evidence hit
- Per-video failure isolation β one broken video never kills a research run
- Politeness rails: hard per-run video cap, delay between requests, 30-day
cache (no re-fetching what it already knows)
- Proxy and browser-cookie support for restricted networks (validated against
a real filtered-network setup)
- Clean, actionable CLI errors instead of tracebacks
- Offline test suite (15 tests: URL/VTT parsing, chunking, storage, FTS
retrieval, cache freshness, topic filter) + CI on Python 3.10β3.12
## Installation
The package is **not on PyPI yet** β install from source:
```bash
git clone https://github.com/Elahe-z/ytscholar
cd ytscholar
pip install -e . # core: CLI + MCP server, keyword (FTS5) retrieval
```
Python 3.10+ is required. The optional `[embeddings]` extra is described in
[Search](#search).
## Configuration
All configuration is via environment variables (no config files, no secrets in
the repo):
| Variable | Default | Meaning |
|---|---|---|
| `YTSCHOLAR_HOME` | `~/.ytscholar` | Base dir for the knowledge DB |
| `YTSCHOLAR_DEFAULT_LANGS` | `en` | Preferred transcript languages, e.g. `fa,en` |
| `YTSCHOLAR_MAX_VIDEOS` | `15` | **Hard cap** on videos per research call |
| `YTSCHOLAR_REQUEST_DELAY` | `0.8` | Seconds between transcript fetches |
| `YTSCHOLAR_CACHE_TTL_DAYS` | `30` | Skip re-fetching a video seen within N days |
| `YTSCHOLAR_EMBEDDINGS` | `0` | `1` to enable semantic re-rank (experimental) |
| `YTSCHOLAR_EMBED_MODEL` | `all-MiniLM-L6-v2` | sentence-transformers model |
| `YTSCHOLAR_CHUNK_CHARS` | `900` | Approx chars per retrieval chunk |
| `YTSCHOLAR_HTTP_PROXY` | (from `HTTP_PROXY`) | Proxy for reaching YouTube |
| `YTSCHOLAR_HTTPS_PROXY` | (from `HTTPS_PROXY`) | HTTPS proxy for reaching YouTube |
| `YTSCHOLAR_COOKIES_FROM_BROWSER` | (unset) | Browser to read YouTube cookies from (`firefox`, `chrome`, `chromium`, `brave`, `edge`) |
| `YTSCHOLAR_COOKIES_FILE` | (unset) | Path to an exported `cookies.txt` |
## Usage
```bash
# Learn a topic from its top videos (English topics give the best ranking):
ytscholar-cli research "retrieval augmented generation" --max 5
# Ask questions about everything learned so far:
ytscholar-cli search "how does RAG reduce hallucinations"
# Evidence with source/channel analysis (human-readable):
ytscholar-cli evidence "how does RAG reduce hallucinations" --pretty
# Get one video's transcript without storing it:
ytscholar-cli transcript "https://youtu.be/VIDEO_ID" --text-only --no-store
# What does the agent know?
ytscholar-cli stats
```
On a network where YouTube is filtered, point the agent at your proxy first β
see [Restricted networks](#restricted-networks-iran-and-similar-).
Tip: transcripts are usually English, so phrase `search` queries in English
for the best keyword matches.
## Search
Search is **keyword-based by default**:
1. The query is sanitized (alphanumeric tokens only β no FTS syntax
injection is possible) and turned into an OR-query of its word tokens.
2. SQLite **FTS5** matches chunks with **bm25** ranking (lower = better) over
the *entire* knowledge base, optionally filtered by topic (exact match).
3. A wider candidate pool (β₯30) is fetched, ranked, and the top `k` returned.
4. Each hit carries `video_id`, `title`, `channel`, `start_seconds`, `text`,
`score`, and a `link` deep link built from the chunk's stored start time.
**Optional semantic re-rank (experimental).** With
`pip install "ytscholar[embeddings]"` (pulls in `torch`, hundreds of MB) and
`YTSCHOLAR_EMBEDDINGS=1`, chunks are embedded at ingest time and query
vectors re-rank the FTS candidates by cosine similarity. This path is
implemented but **experimental: it is not covered by the test suite** and is
disabled by default. Without it, everything works via plain keyword search.
Known search limitations (see also [Limitations](#limitations)): queries are
matched as OR-ed words (no quoted-phrase support), and there is no synonym
matching in keyword mode.
### Evidence retrieval (v0.2)
`ytscholar-cli evidence` / MCP `search_evidence` runs the **same** retrieval
engine, then groups the hits so source diversity is visible:
```json
{
"passages": [
{
"video_id": "β¦", "title": "β¦", "channel": "β¦",
"start_seconds": 763.1,
"link": "https://youtu.be/β¦?t=763",
"text": "β¦passage textβ¦", "score": 7.85
}
],
"videos": [
{ "video_id": "β¦", "title": "β¦", "channel": "β¦", "url": "β¦", "passages": 3 }
],
"unique_channels": 2,
"channel_distribution": { "Channel A": 3, "Channel B": 1 },
"independent": false,
"warning": "3 of 4 matching videos come from the same channel ('Channel A'). Evidence may not be fully independent."
}
```
The point: **N passages do not mean N sources.** If four matching videos come
from two channels β three of them from the same one β the model should know
that. `independent` is a deterministic channel-variety heuristic (`true` = no
single channel holds a strict majority of the matching videos); it makes no
stronger epistemic claim. Typical model workflow:
`search_evidence(query)` β judge sources β `get_transcript(video_id)` for any
source that needs full context β synthesize with citations.
## Research
`research_topic(topic, max_videos)` does exactly this, in order:
1. Clamps the video count to `min(max_videos, YTSCHOLAR_MAX_VIDEOS)`.
2. Searches YouTube via `yt-dlp` (`ytsearchN`, flat metadata) β the order is
YouTube's own relevance ranking.
3. For each result: if the video was fetched within the cache TTL
(default 30 days), it is marked `cached` and skipped β no re-download.
4. Otherwise the transcript is fetched (primary API, then yt-dlp fallback),
chunked (~900 chars, timestamps preserved), and stored under that topic.
5. A short polite delay runs between videos.
6. One video failing (no captions, network error) only marks that video
`no_transcript` / `error` with the reason β the run continues.
7. Returns a per-video report plus overall knowledge-base stats.
Statuses you will see: `ingested`, `cached`, `no_transcript`, `error`.
## Storage / Memory
- Single SQLite database: `~/.ytscholar/knowledge.db` (override the location
with `YTSCHOLAR_HOME`).
- Tables: `videos` (metadata + full transcript text + `fetched_at`) and
`chunks` (text, start time, optional embedding), plus an FTS5 full-text
index kept in sync by triggers. WAL mode for safe concurrent reads.
- The DB **is** the agent's memory: it persists across sessions, it is safe to
copy/back up, and deleting it resets what the agent knows.
- It lives outside the repository β no personal data ships with the code.
## MCP
An MCP server over stdio ships with the package (`ytscholar` command). Same
five operations as the CLI, for use inside Claude Desktop, Cursor, Cline, β¦
(`research_topic`, `get_transcript`, `search_knowledge`, `search_evidence`,
`knowledge_stats`). Designed for the model workflow:
`research_topic` to ingest β `search_evidence` to find evidence and judge
source diversity β `get_transcript(video_id)` when full context is needed β
the model does the analysis and synthesis.
Claude Desktop (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ytscholar": {
"command": "ytscholar",
"env": {
"YTSCHOLAR_DEFAULT_LANGS": "en",
"YTSCHOLAR_HTTPS_PROXY": "http://127.0.0.1:12334"
}
}
}
}
```
Cursor (`~/.cursor/mcp.json`):
```json
{ "mcpServers": { "ytscholar": { "command": "ytscholar" } } }
```
If `ytscholar` is not on the client's PATH, use the absolute path
(`which ytscholar`) or `"command": "python", "args": ["-m", "ytscholar.server"]`.
Example prompts once connected: *"Research 'retrieval augmented generation'
from the top 5 YouTube videos"*, then *"From what you've learned, how does
re-ranking improve RAG?"* β answers come with timestamped citations.
## Limitations
Honest list for this MVP:
- **Keyword search only** (by default): OR-ed word tokens, no phrase
support, no synonyms. English queries against English transcripts work
well; Persian queries won't match English content.
- **Semantic re-rank is experimental** β implemented, off by default, not
covered by tests.
- **`independent` in evidence retrieval is a heuristic**: it reflects channel
variety only (same-creator concentration), not true epistemic independence
β two channels may still repeat the same primary source.
- `get_transcript` returns the **full** transcript text; for very long videos
this is a large payload for an LLM context.
- A single-video `transcript` stores the URL you passed as the video *title*
in the knowledge base (real title enrichment is not implemented).
- The `topic` filter is an exact, case-sensitive match on the string passed
to `research`.
- Ingest relies on scraping (`yt-dlp` / `youtube-transcript-api`): YouTube
layout changes or IP blocks can break it. Cookies/proxy options mitigate.
- Video selection trusts YouTube's ranking as-is: no duration, language, or
caption-availability filtering up front.
## Roadmap
Not implemented β kept deliberately out of this MVP:
- Claim extraction, contradiction detection, source lineage, evidence graphs
(these are the LLM client's job; future versions may assist with them)
- Quoted-phrase queries and per-video diversity in search results
- Real title/metadata enrichment for single-video transcripts; transcript
length caps for LLM consumption
- Validate and test the embeddings path; make semantic mode first-class
- Optional tiny HTTP API over the same core, for workflow tools (n8n etc.)
- Publish to PyPI (`pip install ytscholar`)
## Restricted networks (Iran and similar)
If `pip install` fails with `No matching distribution found`, your network is
blocking pypi.org. Point pip at a local mirror:
```bash
pip install -e . -i https://mirror-pypi.runflare.com/simple/ \
--trusted-host mirror-pypi.runflare.com
```
To reach YouTube itself, run your VPN/proxy and point the agent at it:
```bash
export YTSCHOLAR_HTTPS_PROXY="http://127.0.0.1:PORT"
ytscholar-cli transcript "https://youtu.be/VIDEO_ID" --text-only
```
> **Hiddify users:** the local mixed (HTTP+SOCKS) port is **12334** once the
> core is connected, so:
> `export YTSCHOLAR_HTTPS_PROXY=http://127.0.0.1:12334`
> (Ports like `17078` belong to the app itself, not the proxy β they refuse
> connections.)
If YouTube answers `IpBlocked` / "Sign in to confirm you're not a bot"
(common on VPN/datacenter IPs), pass your browser's cookies:
```bash
export YTSCHOLAR_COOKIES_FROM_BROWSER="firefox" # or chrome / brave / edge
```
Or export a `cookies.txt` (browser extension) and set
`YTSCHOLAR_COOKIES_FILE=/path/to/cookies.txt`.
## Development
```bash
pip install -e ".[dev]"
pytest -q # offline tests only β no network needed
```
CI (`.github/workflows/ci.yml`) runs this suite on Python 3.10β3.12 for every
push and pull request.
## License
MIT β see [LICENSE](LICENSE).
TDQS
Scored across 4 tools
Each tool has a clearly distinct purpose: fetching transcripts, researching a topic (which itself uses transcripts), searching the knowledge base, and reporting stats. Even though research_topic uses get_transcript internally, their high-level functions are unambiguous.
All tools follow a verb_noun pattern: get_transcript, research_topic, search_knowledge, knowledge_stats. The pattern is consistent, though 'research_topic' and 'knowledge_stats' are slightly less uniform than 'get_' or 'search_' prefixes, but still readable.
With only 4 tools, the set is lean and well-scoped for a YouTube transcript knowledge-base server. Each tool is essential: ingestion (get_transcript), bulk learning (research_topic), retrieval (search_knowledge), and monitoring (knowledge_stats). No bloat.
The core workflow of ingest, research, search, and stats is covered. Minor gaps include lack of a tool to delete knowledge or list all topics explicitly, but these are not critical for the primary use case of learning and querying.