Local MCP server: recordings or public video URLs into transcript, frames, OCR, wall-clock evidence.
Local MCP server that takes recordings or public video URLs and converts them into transcript output, frame data, OCR results, and wall-clock evidence. It is positioned for video analysis workflows such as speech-to-text, transcription, and extraction of on-screen content.
π οΈ Key Features
Ingests recordings or public video URLs
Produces transcript content
Extracts frames
Runs OCR on video/frame content
Outputs wall-clock evidence
Uses video-analysis tooling (e.g., ffmpeg)
π Use Cases
Meeting or session documentation
Speech-to-text from recorded video
OCR-based capture of displayed text
Evidence generation for time-referenced review
β‘ Developer Benefits
Local-first processing for developer workflows
Tooling aligned with MCP and agent-based pipelines
Supports common developer tooling stacks (Python)
Designed to support βfeedbackβ and βagent-skillsβ style iterative work
β οΈ Limitations
Scope described as video inputs (recordings or public URLs); no other input types are indicated
Give Claude Code or Codex a narrated .mov/.mp4 β or a public video link β
and talkthrough turns it into searchable transcript, exact frames, OCR and
wall-clock timestamps, locally β so your agent writes an evidence-backed
issue draft or investigates the fix.
Also works for meetings, workshops, product demos, and production incidents.
Illustration of a /talkthrough:bug run: a recorded checkout bug is indexed locally, the evidence is found, and a ready-to-file issue draft is assembled
Illustration β an animation of the /talkthrough:bug storyline, not a screen
capture: the recording is indexed locally (transcript Β· keyframes Β· OCR Β·
wall-clock), the evidence checkpoint is assembled, and the draft is ready to
file with your own tracker tooling (gh, Jira, a GitHub MCP server β
talkthrough itself never leaves your machine and never posts anything).
Silent recording β issue draft β
the whole thing as files you can re-run: a Playwright-recorded, audio-free
.mp4 and the unedited agent output. Every number is reproducible β
processing that file gives job_id 8703a66bbe77a7d0 (the job id is the
sha256 prefix, so shasum -a 256 predicts it), 17 keyframes / 4 unique, and
0 transcript segments because there is no audio track.
Quickstart
One command, no system dependencies: ffmpeg falls back to a bundled build,
OCR is pip-only, and whisper models download themselves on first use. The
only prerequisite is uv (brew install uv or
curl -LsSf https://astral.sh/uv/install.sh | sh).
Cold setup has two separate stages: uvx first resolves a compatible Python
and the pinned server environment, then the first process_media downloads
any missing media/model assets. A plugin update can create a new environment
and, without system ffmpeg, fetch the ~80 MB bundled ffmpeg again; shared
Whisper/OCR/diarization caches and warm, network-free jobs remain reusable.
See Troubleshooting.
Claude Code
Two install paths β pick one, not both (the plugin already includes
the server; installing both would register it twice):
Server only β the 9 tools + 6 prompts, and nothing else on your
system. Choose this for a minimal setup, or when you manage MCP servers
yourself across several clients:
bash
claude mcp add -s user talkthrough -- uvx --python ">=3.11,<3.14""talkthrough-mcp[diarization,url]"
Full plugin β the same server, plus native slash commands
(/talkthrough:bug, /talkthrough:triage-recording, β¦) that handle the
ceremony for you, a ready-made triage subagent, and an agent skill that
teaches Claude the workflow. Choose this for the best out-of-the-box
experience:
Any other MCP stdio client uses the same server command: uvx --python ">=3.11,<3.14" "talkthrough-mcp[diarization,url]".
Per-engine folders with exactly these snippets plus verification steps live
in integrations/; agents can self-install via
llms-install.md.
Who said what (speaker diarization) β included in the configs above
Multi-person recordings (meetings, interviews, panels) can carry S1/S2/β¦
speaker labels. Every install button, snippet, and the plugin above already
ship the [diarization] engine, so asking your agent "who said what" just
works β diarization itself still runs only when requested per call
(process_media(path=..., diarize=true, num_speakers=<count if known>)),
and its models download once on first use.
Prefer the minimal server without the diarization engine? Use
uvx --python ">=3.11,<3.14" talkthrough-mcp as the command instead (the MCP registry entry also
resolves to this lean form) β an explicit diarize=true will then answer
with the one-line install fix. Details in
Speakers.
Upgrading from 0.3.x
Regenerated configs and the plugin carry [diarization,url]. A config you
wrote by hand for 0.3.x β uvx --python ">=3.11,<3.14" talkthrough-mcp, or a
pin without the url extra β upgrades the server in place and lists the new
process_url tool,
but only direct https:// media links work until the extra is there:
YouTube and video pages answer with the one-line install fix. Add url to
your command (uvx --python ">=3.11,<3.14" "talkthrough-mcp[diarization,url]"),
restart the client, and check with talkthrough-mcp --version (0.4.1+),
which names the extras the environment has; the server logs the same line
to stderr at every start, so your client's MCP log shows it too.
Local checkout (development)
bash
git clone https://github.com/korovin-aa97/talkthrough-mcp
claude mcp add talkthrough -- uv run --directory /path/to/talkthrough-mcp talkthrough-mcp
Then, in your agent:
Process ~/Desktop/recording.mov and triage it β or just invoke the
triage-recording server prompt.
Ingest a video/audio file: local STT, keyframes, OCR, wall-clock, opt-in speaker labels. Returns a compact summary. Idempotent by content hash β re-calls are instant; diarize=true on a processed job adds speakers without re-transcribing.
The one network tool: download one public video/audio URL once (a direct https:// media link, one YouTube video, or any public video page yt-dlp can read β with the [url] extra), keep the source inside the job, then run the same local pipeline. Same-URL re-calls serve the stored job without network unless refresh=true; force=true rebuilds from the kept source; the raw URL is never stored.
Keyframe images nearest a timestamp or evenly thinned across a range (unique frames by default, max 6/call); each frame names its absolute path.
get_moment(job_id, start_ms, end_ms)
The "one remark" bundle: transcript slice + up to 3 frames + their OCR text + wall-clock range (+ speakers_in_range when diarized).
search(job_id, query, speaker?, match_mode?)
Substring search over the transcript AND on-screen OCR text. all_words remains the default; any_word broadens lexical recall. Hits carry t_ms/t_wall, frame refs, and the speaker when diarized. The optional filter accepts a raw label or saved name.
label_speakers(job_id, labels, evidence?)
Atomically persist verified names for anonymous speaker labels. Raw S1/S2 labels remain canonical; blank/null removes a name.
extract_frame(job_id, at_ms, crop?)
Exact-timestamp full-resolution re-extract from the source video (optional crop) when keyframes miss the instant; returns the file's absolute path. URL jobs decode their kept source β no network.
list_jobs()
Recent processed recordings with source paths, durations, wall-clock starts, counts, speaker counts when diarized, and the provider/id for URL jobs.
Every tool description ships 10+ usage examples, so agents pick the right tool
without extra prompting.
Server prompts (slash commands in MCP clients)
Prompt
Workflow
bug
One recording β evidence-backed GitHub issue draft (silent, narration-free recordings work too)
The same workflow ships as a cross-engine Agent Skill
at .agents/skills/talkthrough/ β Claude Code,
Codex CLI ($talkthrough), Cursor, Copilot, Gemini CLI, Goose and other
SKILL.md-compatible tools read it. Agents without MCP wiring can drive the
CLI directly: talkthrough-mcp process recording.mov --json prints the
same summary the MCP tool returns, and the job store is shared either way.
Wall-clock anchoring
Every timestamped result carries both t_ms (video-relative) and t_wall
(ISO 8601 real time) once the recording start is known. Resolution ladder:
QuickTime com.apple.quicktime.creationdate tag, carries the local
timezone (QuickTime Player recordings; ββ§5 wrote it before macOS 26) β high
Container creation_time tag (UTC) β medium β macOS 26+ ββ§5/ReplayKit
screen recordings land here (no creationdate tag anymore); pass
recorded_at= when local-tz t_wall matters
File mtime minus duration (recorders finalize files at recording END) β low
Nothing β tools still work with relative t_ms only
Why it matters: "the upload spinner froze here" becomes a Β±30 s grep window
in your server logs.
Speakers (optional diarization)
With the [diarization] extra installed (included in every generated config β
see Quickstart),
process_media(diarize=true) labels who said what β locally, like everything
else here (sherpa-onnx runtime, no
torch, no accounts, no GPU):
Speakers become S1, S2, β¦ in order of first appearance; new
diarized jobs split speaker changes at word boundaries, while old jobs
continue to report honest segment-level precision. Every
transcript segment gets a speaker, and the tools surface it everywhere β
roster with talk time in get_transcript, speakers_in_range in
get_moment, speaker on search hits, S1: prefixes in the text/SRT
formats, a speaker count in list_jobs.
Know the headcount? Pass num_speakers. Clustering toward an exact k
removes the main failure mode of unknown-count mode (similar voices merging
or one voice splitting). It is a target, not a guarantee: the clusterer can
converge on fewer clusters than k, and a re-run that changed nothing says
so in the payload (labels_changed: false). Agents are instructed to pass
the headcount via the tool guidance; do the same in your own calls.
Already processed a recording? Calling process_media(diarize=true) on
it re-runs only diarization β whisper is not re-run, and labels land in
the existing job. Same for changing num_speakers. The diarization stage
itself still re-scans the full audio: minutes on long recordings.
Full rebuilds keep named jobs safe. If a job has active or pending names,
force=true also requires diarize=true; otherwise the call refuses before
changing stored data. A successful force rebuilds in staging and moves every
previous identity to pending review against the fresh roster. Any processing
or commit failure leaves the prior manifest and frames intact.
Labels start anonymous. After checking self-introductions, vocatives, or
video evidence, call label_speakers to preserve a verified mapping such
as S1 β "Alice" across sessions. The roster can expose bounded OCR
name_candidates, but those are raw hints and are never saved
automatically. The raw label remains present beside speaker_name. If a
later diarization amend changes the labels, verified names stop being
active and move to bounded speaker_names_pending_review evidence instead
of being silently lost. Re-check the current roster and explicitly confirm
or remove each affected label with label_speakers.
Video jobs produced before 0.3.1 keep their original flat OCR and can return
name_candidates_note to explain why hints are absent. They remain fully
readable without migration; force=true, diarize=true regenerates line-aware
OCR while preserving saved identities for review.
Models download once (~47 MB total) from pinned, checksum-verified URLs into
~/.talkthrough/models/; warm runs are zero-network like the rest of the
pipeline. Speed on an M-series CPU (4 threads): a 26-minute meeting diarizes
in about 2 minutes (RTF β 0.08), on top of the transcription time. Memory:
expect on the order of 1β1.5 GB peak RSS while an hour-plus meeting is being
diarized (measured on a real 73-minute recording); it is released when the
stage completes.
The default won a real-meeting accept-eval (RU/EN/ES + a 3-speaker 26-minute
meeting): it was the only candidate to isolate all three real voices at
num_speakers=3, at 2Γ the speed of the runner-up.
Pick an alternate embedding model (or point at your own .onnx file for
offline machines) via TALKTHROUGH_DIARIZATION_EMB_MODEL; tune the
unknown-count sensitivity via TALKTHROUGH_DIARIZATION_THRESHOLD (see
docs/TROUBLESHOOTING.md). Honest quality notes
live in Limitations.
Privacy
Everything runs locally: your recordings never leave your machine, speech is
transcribed by a local whisper model, OCR and speaker diarization are local
ONNX inference, and there is no telemetry. For local files the only network
access is one-time tool/model downloads (ffmpeg build, whisper model, OCR
models, diarization models β the latter pinned by URL + sha256). The one
deliberate exception is process_url: it downloads the public source you
name from its provider or CDN, once, and nothing else β no media ever goes
up, no cloud STT or LLM is called, and after that download every tool on
the job is network-free again. The raw URL (which may carry signed tokens)
is not stored: the job keeps a hash, the public provider id or host and a
bounded title. Diarization keeps no voiceprint database: voice embeddings
live only in process memory, and only anonymous turn labels (S1/S2) land
on disk. Your agent sees only the payloads the MCP tools return (text and
selected frames) in your existing session; talkthrough itself makes no LLM
calls.
Languages
Narration in any of Whisper's ~99 languages works: the language is
auto-detected per recording, and the summary reports both language and
language_probability so agents can tell a confident detection from a shaky
one (silence or music at the start can fool the detector β pin it with
language="ru" and force=true when that happens). Speaker diarization is
acoustic β it fingerprints voices, not words β so it is language-independent
and works across all of those languages unchanged.
Pick the model for your languages β per call (model= parameter, agents do
this themselves when a transcript comes back garbled) or as the server
default (TALKTHROUGH_WHISPER_MODEL):
Model
Size
Best for
small (default)
464 MB
English and major-language narration on CPU
large-v3-turbo
~1.5 GB
recommended for non-English β near-large quality at near-small speed
medium
~1.5 GB
conservative alternative to turbo
tiny / base
75β145 MB
quick drafts, CI
*.en variants
β
English-only, slightly faster/better for EN
Tips that work in every language: pass product names via
vocabulary="Term1, Term2" (biases the decoder so jargon survives), and note
that the workflow prompts instruct agents to write digests in the
narrator's language while keeping quotes verbatim β the server never
translates (exact quotes are evidence; translation is the agent's job).
On-screen text (OCR) defaults to RapidOCR's Latin + Chinese models. For other
scripts set TALKTHROUGH_OCR_LANG to your language β ru/uk (β the
eslav pack), ja, ko, ar, hi, el, th, or any RapidOCR pack name
like cyrillic β and reprocess with force=true; the matching recognition
model downloads once. Spoken-language support is unaffected either way.
Configuration
Env var
Default
Meaning
TALKTHROUGH_WHISPER_MODEL
small
default whisper model (tiny/base/small/medium/large-v3/large-v3-turbo); the model tool param overrides per call
TALKTHROUGH_OCR
on
set off to skip OCR
TALKTHROUGH_OCR_LANG
Latin+Chinese
recognition script for on-screen text: a language code (ru, ja, ko, ar, hi, β¦) or a RapidOCR pack name (eslav, cyrillic, latin, β¦); the model downloads once
TALKTHROUGH_OCR_PARAMS
β
advanced: JSON object of raw RapidOCR params merged over the derived ones, e.g. {"Rec.lang_type": "cyrillic"}
TALKTHROUGH_DIARIZE
off
set on to diarize by default (needs the [diarization] extra; degrades with a warning without it); an explicit diarize tool param always wins
TALKTHROUGH_DIARIZATION_THRESHOLD
0.5
clustering sensitivity when num_speakers is unknown: fewer speakers than expected β lower it; more β raise it
TALKTHROUGH_DIARIZATION_SEG_MODEL
pyannote-segmentation-3-0
segmentation model: allowlist name or a path to a local .onnx (offline preseed)
TALKTHROUGH_DIARIZATION_EMB_MODEL
nemo_en_titanet_small
embedding model: allowlist name (see Speakers) or a local .onnx path
TALKTHROUGH_DIARIZATION_THREADS
min(4, cpus)
ONNX threads for both diarization models
TALKTHROUGH_MAX_SECONDS
7200
max media duration (also checked against provider metadata before a process_url download)
TALKTHROUGH_MAX_FRAMES
600
keyframe budget per job, spread across the whole duration (the 1 s selection floor auto-grows to duration/budget on long recordings)
TALKTHROUGH_MAX_DOWNLOAD_BYTES
2147483648 (2 GiB)
hard cap for one process_url download, enforced before and during the transfer
TALKTHROUGH_HOME
~/.talkthrough
job store root (URL jobs keep their downloaded source under jobs/<id>/source/)
CLI
The pipeline is also a CLI β useful for pre-processing long recordings outside
an agent session (the store is content-addressed, so the agent then queries the
same job instantly):
bash
talkthrough-mcp process ~/Videos/long-session.mov # prints the summary
talkthrough-mcp process demo.mov --json # machine-readable
talkthrough-mcp process sync.m4a --diarize --num-speakers 3 # who said what
talkthrough-mcp process-url "https://youtu.be/nHfGfEiVdE8"# one public URL, downloaded once
talkthrough-mcp gc --keep-days 30 # clean the job store (sources go with their jobs)
talkthrough-mcp serve # stdio MCP server (default)
talkthrough-mcp --version # package version + which extras this environment has
--json keeps stdout machine-readable on failure too (0.4.1), including a
missing argument or unknown option: the process exits with code 2, stderr
carries the human error: β¦ line, and stdout
carries one JSON document, {"error": {"type": "UnsupportedUrlError", "message": "β¦"}}. --version also says which optional extras the
environment has β the quickest check when a hand-written config launches
the minimal server (a launcher without the url extra) that advertises
process_url but can only read direct media links; the server logs the
same line to stderr at every start.
First run notes: missing system ffmpeg triggers a one-time static-ffmpeg
download; the first transcription downloads the whisper model (~460 MB for
small); both are cached. After that, expect roughly 3Γ faster than real time
on an Apple-Silicon CPU with the default model, OCR included (a 2-minute clip
processes in ~40 s) β and instant re-runs on the same file. Progress streams
as MCP progress notifications, and the CLI prints stage lines. More:
docs/TROUBLESHOOTING.md.
Windows
CI runs lint, the unit suite, a full CLI smoke, and a diarize smoke on
windows-latest (static-ffmpeg Windows build, whisper tiny transcription,
OCR, the instant idempotent re-run, and a speaker-roster assert through the
native sherpa-onnx stack). Notes: the per-job lock always serializes threads;
POSIX also uses fcntl for cross-process locking. Quote paths with spaces
(uv run talkthrough-mcp process "C:\Videos\Screen Recording.mp4").
If something breaks, please open an issue.
URLs (via process_url, since 0.4.0): a direct https:// link to one of
those media files; one public YouTube video (watch, youtu.be, shorts,
a completed live); or any public video page yt-dlp can read β public
Instagram reels, TikTok, Wikimedia Commons file pages (each verified on
release day), the rest of yt-dlp's ~1800 site extractors and pages with a
plain HTML5/HLS player (the [url] extra, which the generated configs above
already carry, brings yt-dlp). Always anonymous: a site that demands a
sign-in from anonymous clients (Vimeo does, with this yt-dlp) is refused
with the reason. The source is
downloaded once, kept inside the job, and never re-fetched for later
questions. Not supported: playlists, channels, active live streams,
private, members-only, age-restricted or DRM-protected videos, cookies or
logins β a site that hides a video behind a login or a bot wall answers
with a clear refusal, not a workaround (Instagram in particular rate-limits
anonymous access). You are responsible for having the right to download
and process what you point it at; talkthrough does not bypass any
restriction.
Limitations
Honest edges, so you can decide fast:
Speaker labels are word-level and opt-in on new jobs. Each Whisper word
is assigned by maximum overlap with the diarization turns, so fast exchanges
split without losing the raw label. Old jobs remain readable and honestly
report attribution_precision="segment"; reprocess with force=true to add
word timings. Sub-second interjections ("yeah", "mhm") can still be absorbed
when the diarization engine does not detect a separate turn, and heavy
crosstalk degrades clustering (the segmentation model tracks at most 2
simultaneous voices). Quality is
pyannote-3.x-generation. The comfort zone without hints is roughly 2β8
speakers; pass num_speakers whenever the headcount is known β it
removes the worst failure mode at any size, and it is the way to go for
large meetings (10+).
URL ingestion covers one public video at a time, without logins.
Direct HTTPS media links, single public YouTube videos and public video
pages yt-dlp can read; playlists, channels, live streams, gated or DRM
content and anything behind a login or a bot wall are refused with a
reason. Sites change; a page that worked yesterday can need a newer yt-dlp
tomorrow. A provider's upload date is not a recording time, so URL jobs
have wall_clock: null unless you pass recorded_at.
Memory: budget about 2 GB for a cold run. Whisper, the OCR models and
the frame pass live in one process. A 78-second video on tiny with OCR
peaked at 1.6 GB RSS during the 0.4.0 release QA (download included); the
default small model needs more, larger models proportionally so. An
8 GB laptop copes; on anything tighter keep the model small or run the
CLI ahead of the agent session.
Keyframes + transcript, not motion analysis. A glitch between scene
changes can be invisible in the frame set; extract_frame re-checks any
instant, but frame-by-frame motion reasoning is your multimodal model's job.
STT quality tracks the model you pick. The default small favors speed;
non-English narration wants model="large-v3-turbo" (see
Languages).
OCR reads crisp UI text well; tiny or low-contrast print is best-effort.
Wall-clock confidence depends on recorder metadata β worst case pass
recorded_at= (see the ladder above).
Windows caveats β POSIX lock degrades to a no-op; see the Windows
section above.
How it compares
talkthrough
cloud recorder SaaS
meeting notetakers
typical video-analyzer MCPs
Runs fully locally
β
β
β
varies
Any local video/audio file
β
browser/app captures
meetings only
β
Public video URL: downloaded once, kept with the job, analyzed locally
Why not just upload the video to a multimodal model (e.g. Gemini)?
For a short, non-sensitive clip β do that. The trade-offs appear with length
and sensitivity: an hour of screen recording costs on the order of a million
tokens per question, the file leaves your machine, and you still can't map a
remark to 14:32:07 UTC to grep your server logs. talkthrough indexes once,
locally, then answers any number of follow-ups from the index.
Why not screenpipe?
Different job. screenpipe is an always-on recorder of your machine going
forward (commercial license). It can't open the .mov a teammate or customer
just sent you. talkthrough analyzes any file it's handed β the two compose
fine.
There are agent skills that "watch" videos. Why a server with an index?
Watch-style skills push a budgeted frame dump into the context window (and go
sparse on long videos), often call cloud STT for the audio, and keep nothing.
talkthrough builds a persistent local index β transcript + OCR, full-text
searchable β retrieves exact frames lazily, anchors everything to wall-clock
time, and answers the next question without reprocessing.
I use Jam for bug reports β do I need this?
Keep Jam for browser bugs: console+network captured at record time is great
evidence. talkthrough covers what a browser extension can't β desktop apps,
mobile screencasts, ops incidents, meetings, any file β with no account, and
correlates with server-side logs via wall-clock time.
Which agent model do I need to drive this?
For v0.4.0 the six model configs below ran 61 isolated behaviour cells on
URL ingestion (YouTube, a TikTok page, a speechless Instagram reel,
playlist refusal, the missing wall clock) and the 0.3.2 integrity fixes;
every cell passes on every runner after two product fixes the first
attempts exposed (Codex needs a per-tool approval for the open-world
process_url, and the server now says itself when a URL job has no wall
clock). For v0.3.0 we ran 210 isolated agent cells across 6 model configs (Claude
haiku/sonnet/opus, Codex gpt-5.5 at two reasoning efforts, and gpt-5.4-mini)
and 35 logical scenarios on 5 real recordings plus safety and speaker-label
fixtures. All 102 LLM-judged full-grid results and every mechanical zero were
manually audited; the 30 new speaker behavior runs passed, and old-server
control left 0 release-caused regressions. This is a model-drift snapshot,
not a leaderboard: see the current matrix in
docs/MODEL-NOTES.md. The chart and narrative in
benchmarks/ remain the historical v0.2.0 snapshot.
Can't I just script ffmpeg + whisper myself?
Yes β that's exactly this pipeline. What you'd be rebuilding: scene-change
detection with perceptual dedup, OCR, transcript+OCR search, the wall-clock
ladder, MCP tools with embedded usage examples, six workflow prompts, and a
findings contract. One uvx command instead of an afternoon of glue.
Is it really local? What leaves my machine?
Nothing goes up, ever. For local files the network is used only for one-time
downloads (ffmpeg build, whisper/OCR/diarization models). process_url is
the single tool that talks to the network at runtime, and only down: it
fetches the public source you named, once. No telemetry. See
Privacy β and SECURITY.md treats a violation of
this promise as a vulnerability.
For agents & tooling
Machine-readable entry points, so AI agents can install and use this server
without a human reading docs:
llms-install.md β step-by-step install instructions for agents
.agents/skills/talkthrough/SKILL.md β an Agent Skill teaching the tool workflow; discovered automatically inside a checkout by Codex CLI ($talkthrough) and readable by Claude Code, Cursor, Copilot, Gemini CLI and other SKILL.md-compatible tools
AGENTS.md β instructions for coding agents contributing to this repo
integrations/ β per-engine adapters, all generated from one source of truth and drift-tested (incl. the Claude Code plugin under integrations/claude-code/)
docs/URL_ACCEPTANCE_CORPUS.md β the live URL corpus behind the release QA of process_url (manual, needs the network; CI stays offline)
Whisper model (tiny|base|small|medium|large-v3|large-v3-turbo). Default: small. Use large-v3-turbo for non-English narration; the process_media `model` param overrides per call.
TALKTHROUGH_OCR
Set to 'off' to disable OCR of keyframes. Default: on.
TALKTHROUGH_OCR_LANG
Recognition script for on-screen text: a language code (ru, ja, ko, ar, hi, ...) or a RapidOCR pack name (eslav, cyrillic, ...). Default: Latin+Chinese.
TALKTHROUGH_HOME
Job store root. Default: ~/.talkthrough
TALKTHROUGH_MAX_DOWNLOAD_BYTES
Hard cap for one process_url download. Default: 2147483648 (2 GiB).