Turn an Instagram reel URL (or an uploaded video) into an editable, natural-sounding X thread — Hebrew-first, run entirely on your own machine.
Reel2Thread downloads or accepts a reel, extracts audio, transcribes it (Hebrew-aware ASR), builds a traceable content brief and a style fingerprint, then generates an anti-slop thread you can edit, reorder, split, merge, regenerate, and export.
A pnpm monorepo with two apps:
-
apps/web— Next.js 16 + React 19 frontend: intake, progress polling, the RTL review editor, shared weighted-character counting, and publishing actions. -
apps/api— FastAPI service running an asynchronous, file-backed job pipeline of swappable adapters:acquire (yt-dlp / upload) → extract (ffmpeg) → transcribe (faster-whisper / RunPod) → brief + style (Anthropic) → generate + validate thread (Anthropic + deterministic rules)
Job records and artifacts live under .data/jobs/; no database is required.
- Node.js 20+ and pnpm 10+
- Python 3.12+ and uv
ffmpegandffprobeon thePATH- Optional: an NVIDIA GPU with CUDA for fast local Hebrew ASR (otherwise a configured RunPod endpoint, or local CPU as a slow fallback)
- An Anthropic API key
- For URL acquisition: an exported Instagram cookies file
If you are using Claude Code or another coding agent to set this up on Windows,
start with AgentSetup.md. It gives the agent the intended
local setup path and tells it to prefer the upload flow before configuring
Instagram cookies.
pnpm install
uv sync --project apps/apiCopy .env.example to .env and fill it in. Key settings:
R2T_ANTHROPIC_API_KEYandR2T_GENERATION__MODEL— required for generation.R2T_INSTAGRAM_COOKIES_FILE— path to your cookies export (keep it private).R2T_RUNPOD_ENDPOINT_URL/R2T_RUNPOD_API_KEY— for RunPod ASR.
Non-secret defaults (limits, ASR model ids, ban-list, feature flags) live in
config/default.yaml. External model names are
configuration values, so they can change without code edits.
See docs/operations/local-setup.md for the full walkthrough.
# API (http://localhost:8000)
uv run --project apps/api uvicorn reel2thread.main:create_default_app --factory --reload
# Web (http://localhost:3000)
pnpm --dir apps/web devOpen http://localhost:3000, paste a reel URL (or upload an MP4/MOV), and edit the generated thread.
pnpm lint # web eslint + api ruff/mypy
pnpm test # web vitest + api pytest
pnpm --dir apps/web exec playwright test # browser end-to-end (offline fake mode)The Playwright flow and the cross-runtime contract test run against a
deterministic fake pipeline (R2T_FAKE_ADAPTERS=true); no Instagram, ASR, or
Anthropic access is required.
- ASR mode:
auto— localfaster-whisper(ivrit.ai Hebrew CT2 models) when CUDA is available, RunPod when configured without local CUDA, local CPU otherwise. Configured inconfig/default.yamlunderasr. - Model ids: Hebrew fast
ivrit-ai/whisper-large-v3-turbo-ct2, Hebrew accurateivrit-ai/whisper-large-v3-ct2, non-Hebrewlarge-v3. Anthropic model is operator-set viaR2T_GENERATION__MODEL. - Thread rules: numbering off by default, attribution on by default, max weighted tweet length 280, critique pass disabled.
- Character counting: backend mirrors twitter-text's default weighted
configuration; backend and frontend are pinned to a shared conformance fixture
(
twitter_text_cases.json) so counts never drift. - PRD defaults changed: none. Added operational settings beyond the PRD:
R2T_JOB_RETENTION_DAYS(default 7),R2T_FAKE_ADAPTERS, andR2T_ENV. - Target hardware / measured latency: to be recorded on the target machine after the real smoke test in docs/operations/local-setup.md.
Runs locally for personal use. The X API publisher is intentionally disabled in
v1 (POST /api/v1/publish/x returns 501); the OAuth token slot exists but is
never used or logged. /healthz reports dependency status without exposing
secrets.