Architecture#
vid2context turns a video (a URL, a playlist, a podcast episode, a local file) into context an AI agent can read and cite: metadata, chapters and a timed transcript, plus optional on-screen notes, comments and cited briefs across many videos. One TypeScript core runs behind every surface: a CLI, an MCP server, an HTTP API and, later, a web app and a browser extension.
Layout#
packages/
core/ types, provider contracts, rung chain, pipeline, formatters, storage contract (runtime-neutral)
registry/ per-runtime provider registries, generated from providers/* (node, workers, browser)
cli/ the vid2context command
mcp/ the MCP server (stdio now, Streamable HTTP later)
server/ the HTTP API on Hono
providers/
youtube/ source + captions (innertube, keyless)
media-file/ local files (node only)
stt-openai-compatible/ speech-to-text rungs
llm-openai-compatible/ chat rungs
scripts/gen-registry.mjs scans providers/*/package.json, writes packages/registry/src/generated/
tests/fixtures/ shared real fixtures (public-domain audio, real VideoContext JSON)
docs/contracts/ the contracts parallel work builds againstEvery package exports its TypeScript source directly ("exports": { ".": "./src/index.ts" }) and imports with explicit .ts extensions. tsx runs it, vitest tests it, Biome lints it. Bundles for npx (CLI, MCP) come later with tsup.
Data model#
input string ──resolve──▶ VideoRef { platform, kind, id = "platform:nativeId", url }
│
▼
VideoContext {
metadata: VideoMetadata (title, channel, description, duration, chapters[], tags, language)
transcript: Transcript (language, origin, producer, model, segments[{startSec, endSec, text}])
visual?: VisualNote[] (slide text, code, charts seen on screen)
comments?: Comment[]
attempts: Attempt[] (every provider and rung tried, with outcome and reason)
}TranscriptOrigin says how trustworthy the text is: creator-captions, auto-captions, native-transcript, stt, video-model.
Design rule: raw transcripts are always kept. Summaries, briefs, extractions and translations are layers stored next to them, never replacements.
The pipeline#
getVideoContext(registry, input, opts, ctx) in packages/core/src/pipeline.ts:
resolve input ─▶ storage hit? ─▶ metadata ─▶ transcript ─▶ comments? ─▶ save
│
mode auto: captions (creator, auto, translated) ─▶ audio + speech-to-text chain
mode captions: captions only, never download audio
mode stt: always transcribe the audio
mode video: a multimodal video rung reads the whole video (transcript + visual notes)- Source resolution: the registry asks each source's synchronous
match(); platform-specific sources go before catch-alls (yt-dlp, web page discovery). - Metadata failures that mean "this video cannot be had" (
not-found,unavailable,blocked) stop the pipeline; any other metadata failure falls back to minimal metadata and keeps going. - Audio can come from any source whose
canProvideAudio(ref)is true, so yt-dlp can supply audio for YouTube refs. - Speech-to-text rungs whose
maxBytesis below the audio size are skipped before the chain runs (long audio is split by the longform orchestrator, item 04). - Every step appends to
attempts. Errors carry the attempts too, so every surface can show why something failed.
expandInput turns playlist, channel, feed and page refs into video refs. transcribe and chat expose the speech-to-text and LLM chains directly.
The rung chain#
packages/core/src/chain.ts. A rung is one provider + one model + one credential (Groq and Cerebras meter quota per model, so each model is its own rung; each pooled key is its own rung too).
- A rung is present only when its credential is set; keyless and local rungs need none.
- Default policy
freeorders by tier:local(this machine, private),free(hosted free tier, your key),keyless(hosted, no account),paid(only when its key is set). Then bypriority. local-onlykeeps only local rungs.qualityorders by quality score, tier breaking ties.runChaintries rungs in order, falls through on any error except caller input errors, and records every attempt.- Per-process cooldowns: rate-limited for
Retry-Afteror 60 s, auth failures 10 min, a retired model 30 min, 5xx 30 s. - Env controls:
VID2CONTEXT_POLICY,VID2CONTEXT_ALLOW_PAID=0,VID2CONTEXT_SKIP=svc1,svc2,VID2CONTEXT_KEYLESS=0.
The same chain serves every rung kind: stt, llm, embed, video, visual. Providers build rungs from env (rungs(env)), so model overrides and key pools need no code change.
Providers and the registry#
Providers are separate workspace packages under providers/<name>/. Each declares "vid2context": { "runtimes": [...] } in its package.json and default-exports one provider or an array. scripts/gen-registry.mjs (run on install and before typecheck and test) writes one registry per runtime:
packages/registry/src/generated/node.ts every provider with "node"
packages/registry/src/generated/workers.ts ... "workers"
packages/registry/src/generated/browser.ts ... "browser"So node-only providers (child processes, filesystem) never reach a Workers or browser bundle, and adding a provider never means editing a shared file: fifty agents can each add one at once. The generated files are gitignored.
Providers never read process.env or call the global fetch. Everything comes through the Ctx passed to each call (env, fetch, log, signal, runtime), which is what lets one provider run on Node, Workers, the browser and in tests with recorded responses. See contracts/providers.md.
Formats#
packages/core/src/format/index.ts holds a formatters map (text, json today). Every surface lists formats from that map and renders through render(), so a formatter added there appears in the CLI, MCP, HTTP API and web app at once.
Storage#
The Storage interface in packages/core/src/storage.ts covers saved contexts, collections and search. MemoryStorage is the zero-dependency default. Storage providers (SQLite, Cloudflare D1, Postgres) register like any provider; the enabled one with the lowest priority wins. The pipeline reads through storage as a cache and saves every result.
Surfaces#
| Surface | Package | Notes |
|---|---|---|
| CLI | @vid2context/cli |
stdout is program output only; errors, hints and attempts go to stderr |
| MCP | @vid2context/mcp |
stdio for Claude Code and other local agents; Streamable HTTP with auth for ChatGPT and Claude.ai |
| HTTP API | @vid2context/server |
one Hono app for Node, Workers, Vercel, Cloud Run, Railway, Alibaba Cloud |
| Web app | packages/web (planned) |
paste a link or upload a file, pick an LLM, copy or download results |
| Extension | packages/extension (planned) |
send the current tab's video to vid2context |
All of them log to stderr. stdout belongs to program output and to the MCP stdio protocol.
Who synthesizes#
By default the calling agent does. The MCP server returns transcripts, sections and search hits, and offers a "brief" prompt, so there is no extra API key or cost. Server-side map-reduce on a cheap free model is optional, for collections too large for the agent's context (item 32).
Size guide: speech is about 12k tokens per hour, so a 15-episode series of 30-minute videos is about 90k tokens. That is why outlines, sections, chapters and search exist.
Runtimes and deployment#
| Target | How |
|---|---|
| Node (local, Docker, Cloud Run, Railway, Alibaba SAE / Function Compute) | packages/server/src/node.ts, a container with Node, yt-dlp, deno, ffmpeg |
| Cloudflare Workers + Pages, D1, R2, KV, Workers AI | a Workers entry using the workers registry (item 50) |
| Vercel | the same Hono app on Vercel's Node or edge runtime |
| GitHub Pages | client-only build: in-browser Whisper and LLMs on local files, plus a configurable API base URL |
YouTube blocks many datacenter IPs ("sign in to confirm you're not a bot"). Running locally, or behind a Cloudflare Tunnel to a home network, is the robust path; the YouTube provider reports a blocked error with that hint.
Decisions and why#
| Decision | Why |
|---|---|
| TypeScript, ESM, npm workspaces | Workers, Vercel edge and the browser all run JS; Python would split the codebase. Node 22 or newer. |
Source-exporting packages, explicit .ts imports |
No build step in development; tsx and vitest run the source. |
| Hono for HTTP | One app runs on every target. |
| Official MCP SDK | stdio and Streamable HTTP from one server definition. |
Plain fetch over vendor SDKs |
Most speech-to-text and LLM APIs are OpenAI-compatible HTTP; this keeps providers edge-compatible and dependencies few. |
| Providers as packages plus codegen | Parallel work never collides on a shared registry file, and runtime filtering is automatic. |
| Free first, keyless safety net | Works with zero configuration; every key added only adds rungs. |
Real recorded fixtures, LIVE=1 live tests |
Tests reflect what services really return, without hitting them on every run. |