kouhxp/yapsnap

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

296

stars

16

commits

Python

primary language

Sep 15, 2026

updated

ai-tools
asr
cli
cpu
diarization
speech-recognition
speech-to-text
subtitles
transcription

README

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

  • Fast on CPU. Streaming Zipformer transducer (Kroko) chews through audio at several times realtime on a laptop. No CUDA. No M-series-only tricks. Plain old cores.
  • 🌐 Any video URL, plus local files. YouTube. X. TikTok. Instagram Reels. Direct .mp4/.mp3 links. Or just point it at a file on disk. yt-dlp handles the fetch, ffmpeg handles the decode, the rest is yours.
  • 📴 Offline after first run. Models download once to your cache (~80 MB per language, ~20 MB for the language detector) and stay there. No API keys. No quotas. Your audio never leaves your machine.
  • 🪶 Lean deps. sherpa-onnx, numpy, yt-dlp — that's the whole runtime, diarization and language detection included. No PyTorch, no cloud SDKs.
  • 🗣 Ten languages, detected for you. --lang auto is the default: a pruned whisper-tiny language-ID head hears two 5-second samples and pulls the matching Kroko model — a fraction of a second, same ONNX runtime, no extra deps. German, English, Spanish, French, Italian, Hebrew, Dutch, Portuguese, Swedish, Turkish. Pin one with --lang fr whenever you already know. See Languages.
  • Sentence-level timestamps when you want them. --timestamps adds [MM:SS] per sentence using Kroko's built-in punctuation. Timing stays correct even when you transcribe at 2x.
  • 🗣️ Speaker labels, optional. --diarize answers "who spoke when" and prefixes each line with SPEAKER_00, SPEAKER_01, … Still CPU-only, still ONNX — no PyTorch, no extra runtime deps. See Diarization.

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:

SourceExample
YouTubehttps://www.youtube.com/watch?v=...
YouTube Shortshttps://www.youtube.com/shorts/...
X / Twitterhttps://x.com/user/status/.../video/1
TikTokhttps://www.tiktok.com/@user/video/...
Instagram Reelshttps://www.instagram.com/reel/.../
Direct media URLhttps://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

OSCommand
macOSbrew install ffmpeg
Linuxsudo apt install ffmpeg or sudo dnf install ffmpeg
Windowswinget 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

FlagDescription
-o, --outputOutput .txt path. Default: ./transcripts/<input>_transcript.txt.
--timestampsEmit [MM:SS] sentence. lines instead of a single paragraph.
--diarizeLabel speakers (SPEAKER_00 [MM:SS]: …). Implies --timestamps.
--diarize-modelSegmentation model: pyannote (default) or reverb. See below.
--diarize-model-dirPath to a local directory with diarization models. Bypasses auto-download.
--num-speakersKnown speaker count for --diarize. Default -1 (auto-detect).
--speedPre-transcription speedup factor, pitch preserved. Default 1.4.
--workersChunks to decode in parallel processes. Default 0 (autodetect).
--threadsONNX threads per worker. Default 0 (autodetect). See Performance.
--keep-audioKeep the downloaded audio (URL inputs only).
--modelOverride the model directory. Also reads KROKO_MODEL env var.
--langLanguage 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:

  • --workers N — number of parallel decode processes (0 = autodetect, 1 = single stream, no chunking).
  • --threads N — ONNX threads per worker (0 = autodetect). Keeping workers × threads at or below your physical core count is the sweet spot; going above it tends to slow things down rather than speed them up.

--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:

  • macOS~/Library/Caches/yapsnap/
  • Linux$XDG_CACHE_HOME/yapsnap/ (or ~/.cache/yapsnap/)
  • Windows%LOCALAPPDATA%\yapsnap\

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:

CodeLanguageCodeLanguage
deGermaniwHebrew
enEnglishnlDutch
esSpanishptPortuguese
frFrenchsvSwedish
itItaliantrTurkish

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:

  • both probe windows are silent (or the file has no readable audio there),
  • the LID model can't be downloaded, or your sherpa-onnx is too old for its spoken-language-ID API,
  • the model returns a language with no Kroko model — only possible if you point YAPSNAP_LID_MODEL_DIR at a full whisper export rather than the pruned ten-language one.

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-modelLicenseNotes
pyannote 3.0pyannote (default)CC-BY-4.0Attribution only; the safe default.
Reverb v1reverbNon-commercialSame 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

  • No overlapping speech. Each moment is assigned to exactly one speaker; simultaneous talking isn't modeled.
  • Speaker counting weakens past ~7 speakers. Pass --num-speakers when you know it.
  • Labels are per-run. SPEAKER_00 is not the same person across different files.

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

  • Language detection is a guess, not a guarantee. It's a tiny model hearing ten seconds. Heavy accents, noisy audio, and code-switching all push it around, and a file whose first minute is in a different language than the rest will fool it. Pass --lang CODE when it matters.
  • Only one language per run — the ASR models are single-language, so a bilingual recording gets transcribed in whichever language wins.
  • --speed trades time-stretching for runtime: higher means less audio to decode and a shorter run (try 2.0 to go faster), lower means cleaner output on noisy, mumbled, or fast-speech sources (drop to 1.0). The default 1.4 is a middle ground that reads well; the difference between nearby values like 1.4 and 1.5 is small, but across the full range (1.0 vs 2.0) it's noticeable.
  • Some social-media URLs are geo-locked or login-walled; yt-dlp will say so explicitly.
  • This is a streaming model, so timestamps come from token positions in the recognized stream. They're accurate enough for navigation, not for subtitling-grade alignment.

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.

Contributors

kouhxp

15 commits

pnposch

1 commits

kouhxp/yapsnap

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

296

stars

16

commits

Python

primary language

Sep 15, 2026

updated

ai-tools
asr
cli
cpu
diarization
speech-recognition
speech-to-text
subtitles
transcription

README

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

  • Fast on CPU. Streaming Zipformer transducer (Kroko) chews through audio at several times realtime on a laptop. No CUDA. No M-series-only tricks. Plain old cores.
  • 🌐 Any video URL, plus local files. YouTube. X. TikTok. Instagram Reels. Direct .mp4/.mp3 links. Or just point it at a file on disk. yt-dlp handles the fetch, ffmpeg handles the decode, the rest is yours.
  • 📴 Offline after first run. Models download once to your cache (~80 MB per language, ~20 MB for the language detector) and stay there. No API keys. No quotas. Your audio never leaves your machine.
  • 🪶 Lean deps. sherpa-onnx, numpy, yt-dlp — that's the whole runtime, diarization and language detection included. No PyTorch, no cloud SDKs.
  • 🗣 Ten languages, detected for you. --lang auto is the default: a pruned whisper-tiny language-ID head hears two 5-second samples and pulls the matching Kroko model — a fraction of a second, same ONNX runtime, no extra deps. German, English, Spanish, French, Italian, Hebrew, Dutch, Portuguese, Swedish, Turkish. Pin one with --lang fr whenever you already know. See Languages.
  • Sentence-level timestamps when you want them. --timestamps adds [MM:SS] per sentence using Kroko's built-in punctuation. Timing stays correct even when you transcribe at 2x.
  • 🗣️ Speaker labels, optional. --diarize answers "who spoke when" and prefixes each line with SPEAKER_00, SPEAKER_01, … Still CPU-only, still ONNX — no PyTorch, no extra runtime deps. See Diarization.

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:

SourceExample
YouTubehttps://www.youtube.com/watch?v=...
YouTube Shortshttps://www.youtube.com/shorts/...
X / Twitterhttps://x.com/user/status/.../video/1
TikTokhttps://www.tiktok.com/@user/video/...
Instagram Reelshttps://www.instagram.com/reel/.../
Direct media URLhttps://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

OSCommand
macOSbrew install ffmpeg
Linuxsudo apt install ffmpeg or sudo dnf install ffmpeg
Windowswinget 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

FlagDescription
-o, --outputOutput .txt path. Default: ./transcripts/<input>_transcript.txt.
--timestampsEmit [MM:SS] sentence. lines instead of a single paragraph.
--diarizeLabel speakers (SPEAKER_00 [MM:SS]: …). Implies --timestamps.
--diarize-modelSegmentation model: pyannote (default) or reverb. See below.
--diarize-model-dirPath to a local directory with diarization models. Bypasses auto-download.
--num-speakersKnown speaker count for --diarize. Default -1 (auto-detect).
--speedPre-transcription speedup factor, pitch preserved. Default 1.4.
--workersChunks to decode in parallel processes. Default 0 (autodetect).
--threadsONNX threads per worker. Default 0 (autodetect). See Performance.
--keep-audioKeep the downloaded audio (URL inputs only).
--modelOverride the model directory. Also reads KROKO_MODEL env var.
--langLanguage 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:

  • --workers N — number of parallel decode processes (0 = autodetect, 1 = single stream, no chunking).
  • --threads N — ONNX threads per worker (0 = autodetect). Keeping workers × threads at or below your physical core count is the sweet spot; going above it tends to slow things down rather than speed them up.

--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:

  • macOS~/Library/Caches/yapsnap/
  • Linux$XDG_CACHE_HOME/yapsnap/ (or ~/.cache/yapsnap/)
  • Windows%LOCALAPPDATA%\yapsnap\

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:

CodeLanguageCodeLanguage
deGermaniwHebrew
enEnglishnlDutch
esSpanishptPortuguese
frFrenchsvSwedish
itItaliantrTurkish

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:

  • both probe windows are silent (or the file has no readable audio there),
  • the LID model can't be downloaded, or your sherpa-onnx is too old for its spoken-language-ID API,
  • the model returns a language with no Kroko model — only possible if you point YAPSNAP_LID_MODEL_DIR at a full whisper export rather than the pruned ten-language one.

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-modelLicenseNotes
pyannote 3.0pyannote (default)CC-BY-4.0Attribution only; the safe default.
Reverb v1reverbNon-commercialSame 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

  • No overlapping speech. Each moment is assigned to exactly one speaker; simultaneous talking isn't modeled.
  • Speaker counting weakens past ~7 speakers. Pass --num-speakers when you know it.
  • Labels are per-run. SPEAKER_00 is not the same person across different files.

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

  • Language detection is a guess, not a guarantee. It's a tiny model hearing ten seconds. Heavy accents, noisy audio, and code-switching all push it around, and a file whose first minute is in a different language than the rest will fool it. Pass --lang CODE when it matters.
  • Only one language per run — the ASR models are single-language, so a bilingual recording gets transcribed in whichever language wins.
  • --speed trades time-stretching for runtime: higher means less audio to decode and a shorter run (try 2.0 to go faster), lower means cleaner output on noisy, mumbled, or fast-speech sources (drop to 1.0). The default 1.4 is a middle ground that reads well; the difference between nearby values like 1.4 and 1.5 is small, but across the full range (1.0 vs 2.0) it's noticeable.
  • Some social-media URLs are geo-locked or login-walled; yt-dlp will say so explicitly.
  • This is a streaming model, so timestamps come from token positions in the recognized stream. They're accurate enough for navigation, not for subtitling-grade alignment.

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.

Contributors

kouhxp

15 commits

pnposch

1 commits

Languages

Python

90.6%

Shell

9.4%