yapsnap README

The project's README from github.com/kouhxp/yapsnap. As one markdown file: README.md.

yapsnap

Snap any video URL or audio file into plaintext. No GPU. No cloud. One command.

Python License Platforms

yapsnap "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

That's it. You get a .txt next to your shell, transcribed on your CPU, in less time than it took the video to play.


Why yapsnap


Quickstart

# 1. ffmpeg on PATH (one-time, per OS — see below)
# 2. Install (from PyPI, or `pip install .` from a clone)
pip install yapsnap

# 3. Snap something
yapsnap https://www.tiktok.com/@user/video/7234567890123456789
yapsnap meeting.mp4 --timestamps
yapsnap interview.mp3 --diarize          # label speakers
yapsnap interview.mp3 --lang fr          # skip detection, force French
yapsnap podcast.mp3 -o ~/notes/episode.txt

The language is detected automatically — you don't have to say what you're feeding it.

The first run downloads the language-ID head (20 MB) and the ASR model for whatever language it hears (80 MB). Every run after is offline.


What it handles

Any URL yt-dlp understands works. The big ones:

Source Example
YouTube https://www.youtube.com/watch?v=...
YouTube Shorts https://www.youtube.com/shorts/...
X / Twitter https://x.com/user/status/.../video/1
TikTok https://www.tiktok.com/@user/video/...
Instagram Reels https://www.instagram.com/reel/.../
Direct media URL https://example.com/clip.mp4

Plus any local file ffmpeg can decode: .mp3, .mp4, .m4a, .wav, .webm, .mov, .mkv, .aac, .opus, .ogg, .flac, and friends.


Install

1. ffmpeg

OS Command
macOS brew install ffmpeg
Linux sudo apt install ffmpeg or sudo dnf install ffmpeg
Windows winget install ffmpeg or choco install ffmpeg

2. yapsnap

yt-dlp is installed as a dependency but is not version-pinned on purpose — sites change their extractors constantly, so the newest release is always the one you want. If a URL suddenly fails, pip install -U yt-dlp first.

From PyPI (recommended):

pip install yapsnap

From source:

git clone https://github.com/kouhxp/yapsnap
cd yapsnap
pip install .

Installs two equivalent commands on your PATH: yapsnap (canonical) and transcribe (alias, for when the name slips your mind).


Usage

# Local file
yapsnap path/to/audio.mp3

# Any video URL
yapsnap "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

# Sentence-level timestamps
yapsnap input.mp4 --timestamps

# Speaker labels ("who spoke when")
yapsnap interview.mp3 --diarize

# Speaker labels with a known speaker count (more reliable than auto-detect)
yapsnap call.mp3 --diarize --num-speakers 2

# Language is auto-detected; pin it to skip detection (or to override it)
yapsnap interview.mp3 --lang fr
yapsnap podcast.mp3 --lang de --timestamps

# Custom output path
yapsnap input.mp4 -o ./transcripts/talk.txt

# Don't speed audio up before transcribing (default is 1.4x, pitch preserved)
yapsnap input.mp4 --speed 1.0

# Keep the downloaded audio (URL inputs only)
yapsnap "https://..." --keep-audio

Output

Plaintext, UTF-8. Default location is ./transcripts/ (created if missing) under the current working directory; override with -o. For URL inputs the filename is derived from the video ID (dQw4w9WgXcQ_transcript.txt, etc.).

Without --timestamps — one paragraph of recognized text:

Welcome to the show. Today we're talking about transcription. Let's get started.

With --timestamps — one sentence per line, timed against the original audio:

[00:00] Welcome to the show.
[00:03] Today we're talking about transcription.
[00:08] Let's get started.

Timestamps stay in original-audio time even at --speed 1.4 or higher.

With --diarize — one sentence per line, each tagged with a speaker and timestamp:

SPEAKER_00 [00:00]: Welcome to the show.
SPEAKER_01 [00:03]: Glad to be here, thanks for having me.
SPEAKER_00 [00:08]: Let's get started.

Speaker numbers are assigned in order of appearance and are stable within a single run, but they carry no identity across files — SPEAKER_00 in one transcript is unrelated to SPEAKER_00 in another.


Flags

Flag Description
-o, --output Output .txt path. Default: ./transcripts/<input>_transcript.txt.
--timestamps Emit [MM:SS] sentence. lines instead of a single paragraph.
--diarize Label speakers (SPEAKER_00 [MM:SS]: …). Implies --timestamps.
--diarize-model Segmentation model: pyannote (default) or reverb. See below.
--diarize-model-dir Path to a local directory with diarization models. Bypasses auto-download.
--num-speakers Known speaker count for --diarize. Default -1 (auto-detect).
--speed Pre-transcription speedup factor, pitch preserved. Default 1.4.
--workers Chunks to decode in parallel processes. Default 0 (autodetect).
--threads ONNX threads per worker. Default 0 (autodetect). See Performance.
--keep-audio Keep the downloaded audio (URL inputs only).
--model Override the model directory. Also reads KROKO_MODEL env var.
--lang Language code (e.g. fr, de), or auto (default) to detect it from the audio. Auto-downloads the matching Kroko model. See Languages. Ignored if --model is given.

How it works

  1. Fetch. If the input is a URL, yt-dlp grabs the best audio-only stream to a temp directory. If it's a local path, this step is skipped.
  2. Identify. Under the default --lang auto, two 5-second windows — cut at roughly 15% and 55% into the file, each its own keyframe-seeked ffmpeg call — are decoded at original speed and run through a whisper-tiny language-ID head pruned to yapsnap's ten languages (INT8 ONNX, ~20 MB), via sherpa-onnx's spoken-language-ID API. If the two windows agree, that's the language; if not, they're concatenated and identified once more as a tiebreak. Skipped entirely when --lang CODE or --model is given.
  3. Decode. ffmpeg pipes the media into 16 kHz mono PCM. The optional atempo filter speeds it up without raising pitch.
  4. Recognize. A streaming Zipformer2 transducer (Kroko, INT8 ONNX, ~80 MB) eats the PCM in chunks. CPU-only. Greedy decode.
  5. Format. Plain text by default. With --timestamps, token timestamps are grouped on .!? into sentences and scaled back to original-audio time.

With --diarize, a second pass runs the audio (decoded at original speed) through a speaker-segmentation model and a speaker-embedding model, clusters the voiceprints into speakers, and tags each sentence with the speaker active at its start. All ONNX, all CPU.

No frame is sent anywhere. No state is kept between runs except the cached model.


Performance

yapsnap tunes itself to your CPU — you shouldn't need to touch any flags.

On first decode it detects your physical core count (not the logical/hyperthread count, which oversubscribes and runs slower) and splits the work into a few parallel decode processes that, together, use about one thread per physical core. On a 4-core laptop that's two workers of two threads each; on an 8-core machine, four workers of two threads. Short clips skip the split entirely and decode as a single stream, since chunking only pays off once there's enough audio to outweigh its per-chunk warmup.

Two knobs let you override the autotuning if you want to experiment:

--speed is the other lever: it shortens the audio before decoding, so higher values mean a faster run (at some accuracy cost on hard audio). The default 1.4 balances speed and readability.

YAPSNAP_THREADS overrides the detected core budget for a run if autodetection guesses wrong on an unusual machine.


Model & cache

Models are downloaded on first run to:

To use a different streaming transducer (other languages, larger Kroko variants, etc.), point --model at a directory containing encoder(.int8).onnx, decoder(.int8).onnx, joiner(.int8).onnx, and tokens.txt. Or set KROKO_MODEL in your environment.

The language-ID head lands in a kouhxp__whisper-tiny-lid/ subfolder of the same cache; each ASR model gets its own kouhxp__sherpa-onnx-streaming-zipformer-<lang>-kroko/ folder, so several languages can live side by side. If you use --diarize, the segmentation and embedding models download to diarization-models/. Everything is reused offline thereafter.

Set YAPSNAP_LID_MODEL_DIR to a directory holding lid-encoder.int8.onnx and lid-decoder.int8.onnx to bypass the language-ID download entirely.


Languages

yapsnap detects the spoken language by default (--lang auto) and downloads the matching Kroko model — nothing to pass:

yapsnap interview.mp3                     # figures out it's French, fetches the French model

Detection runs a whisper-tiny language-ID head (INT8 ONNX, ~20 MB, MIT) pruned to exactly these ten languages, through sherpa-onnx — no extra runtime dependency, same as diarization. It listens to two 5-second windows taken from different points in the file (never the whole recording), so it costs a fraction of a second and a couple of keyframe seeks even on a three-hour podcast. Near-silent windows are skipped; if the two windows disagree, they're identified together as a tiebreak.

Available language codes:

Code Language Code Language
de German iw Hebrew
en English nl Dutch
es Spanish pt Portuguese
fr French sv Swedish
it Italian tr Turkish

Pin a language to skip detection entirely — faster, and the right move when you already know or when the audio is code-switched:

yapsnap interview.mp3 --lang fr          # French
yapsnap podcast.mp3 --lang de            # German
yapsnap meeting.mp4 --lang es --timestamps  # Spanish with timestamps

--lang is ignored if --model is also given — an explicit model always wins.

When detection falls back

yapsnap transcribes with English and prints a note on stderr when detection can't commit:

Detection never aborts a run. If a fallback is wrong for your file, --lang CODE settles it.

Manual model setup

If you prefer to manage models yourself, or want to use a model not in the table above (Swiss German, larger Kroko variants, or any other sherpa-onnx streaming transducer), download it, unpack it into its own folder, and run:

# Per-run: pass the model folder explicitly
yapsnap interview.mp3 --model /path/to/kroko-french

# Or set it once as your default for the session
export KROKO_MODEL=/path/to/kroko-french
yapsnap interview.mp3

Kroko publishes streaming models for a growing list of languages on Hugging Face: https://huggingface.co/Banafo/Kroko-ASR/tree/main. --model skips language detection too.

Each model is single-language, so to work across several languages keep them in separate folders and switch with --model (or re-export KROKO_MODEL) as you go. Any other sherpa-onnx streaming transducer with the standard encoder / decoder / joiner / tokens.txt layout works too, not just the Kroko ones.


Diarization

--diarize adds speaker labels to the transcript — "who spoke when" — so each line is prefixed with SPEAKER_00, SPEAKER_01, and so on:

yapsnap interview.mp3 --diarize
SPEAKER_00 [00:00]: Welcome to the show.
SPEAKER_01 [00:03]: Glad to be here, thanks for having me.
SPEAKER_00 [00:08]: Let's get started.

It stays true to yapsnap's design: CPU-only, ONNX, no PyTorch, no extra runtime dependencies beyond the sherpa-onnx you already have. Two small models download once on first use (a speaker-segmentation model plus a speaker-embedding model) and cache alongside the ASR model.

How the labels are produced

--diarize implies --timestamps — the two share a clock. Transcription runs on the sped-up audio as usual, while diarization runs on the same source decoded at original speed (1.0x), because speeding audio up degrades both speaker-boundary detection and the voiceprint embeddings. Each transcript sentence is then matched to whichever speaker was active at its start time.

Because diarization needs sentence timestamps to attach labels to, --diarize will stop with an error if your sherpa-onnx build doesn't expose timestamp data, rather than silently dropping the speaker labels.

Speaker count

By default the number of speakers is detected automatically. Auto-detection is solid up to about seven speakers and degrades above that. If you know the count, pass it — it's more reliable:

yapsnap call.mp3 --diarize --num-speakers 2

Choosing a segmentation model

Model --diarize-model License Notes
pyannote 3.0 pyannote (default) CC-BY-4.0 Attribution only; the safe default.
Reverb v1 reverb Non-commercial Same architecture, fine-tuned for accuracy.
yapsnap panel.mp4 --diarize --diarize-model reverb

pyannote is the default because its license is clean for most uses. reverb (Rev's fine-tune of the same architecture) can be more accurate but is distributed under a non-commercial license — yapsnap prints a reminder the first time you download it. Check the Rev model card before using it for anything commercial.

Limits

To override the embedding model (for example if the default asset name ever changes), set YAPSNAP_EMBEDDING_MODEL to a different .onnx filename from the diarization model repo.

To skip auto-download entirely and use local diarization models, pass --diarize-model-dir pointing to a directory that contains the segmentation model (an extracted subdirectory with model.onnx) and the embedding .onnx file.


Notes & limits


License

Apache-2.0 for this project. The Kroko model is distributed under its own license — see https://huggingface.co/Banafo/Kroko-ASR. The language-ID head is derived from OpenAI's whisper-tiny and is MIT licensed. Powered by sherpa-onnx and yt-dlp.

The optional diarization models carry their own licenses, separate from yapsnap's: the default pyannote segmentation model is CC-BY-4.0 (attribution), the speaker-embedding model is Apache-2.0, and the opt-in reverb segmentation model (--diarize-model reverb) is non-commercial. If you use diarization, review the license of the model you select before relying on it.

yapsnap (kouhxp/yapsnap)

Searching Database…

What is this?A command-line tool that turns any video URL or local audio/video file into a plaintext transcript, running entirely on your own CPU — no GPU, no cloud, one command.
Is it an app with an interface or a command-line tool?Pure CLI: installing it puts two equivalent commands on your PATH — yapsnap and its alias transcribe — and you point either at a URL or a file path in your terminal.
How do I get my first transcript?Install ffmpeg, then pip install yapsnap from PyPI. The first run downloads the language detector (~20 MB) and the matching ASR model (~80 MB); every run after that is fully offline.
Anything that breaks in practice?Sites change their extractors constantly and yt-dlp is deliberately not version-pinned, so a suddenly failing URL is usually fixed with pip install -U yt-dlp. ffmpeg must also be present first.
Does my audio get uploaded anywhere?No. Fetch, decode and recognition all happen locally; the only network traffic is the one-time model download into your cache, and there are no API keys or quotas.
Which sites and file formats can it take as input?Anything yt-dlp understands — YouTube, YouTube Shorts, X/Twitter, TikTok, Instagram Reels, direct media links — plus local files ffmpeg can decode: .mp3, .mp4, .m4a, .wav, .webm, .mov, .mkv, .aac, .opus, .ogg, .flac and more.
What languages does it recognize, and do I have to pick one?Ten: English, German, Spanish, French, Italian, Hebrew, Dutch, Portuguese, Swedish, Turkish. --lang auto detects from two 5-second audio samples; pin --lang fr to skip detection. If detection can't commit, it falls back to English with a note on stderr — the run never aborts.
Can it show who spoke when in a conversation?Yes — --diarize labels each sentence SPEAKER00, SPEAKER01... using segmentation plus voiceprint-embedding models, still CPU-only ONNX. Speaker count auto-detects (solid up to about seven speakers), and numbers are stable within a run but carry no identity across files.
What does the transcript output look like?UTF-8 plaintext .txt, by default in ./transcripts/ under your current directory (override with -o). A single paragraph by default; --timestamps adds [MM:SS] per sentence, correct even at 2x; --diarize implies timestamps and prefixes speaker labels.
Creator
kouhxp
Size
0.1 MB
Stars
379
Updated
CommitSep 15
Age
4mo
Commits · 90 Days