@coroboros/scrybe
v0.2.0
Published
Collapse sound into signal — a fast, offline Whisper transcription & speaker-diarization CLI. No Python, no account.
Downloads
640
Maintainers
Readme
scrybe
Collapse sound into signal — a fast, offline Whisper transcription & speaker-diarization CLI. No Python, no account.
Pure-Rust audio decode, whisper.cpp via whisper-rs, Metal on Apple Silicon and CPU everywhere. Point it at one file or a whole folder and get text — and who spoke — back from the terminal. No system ffmpeg.
Contents
- Requirements
- Install
- Usage
- Why this exists
- Models
- Codecs
- Options
- Output formats
- Agents
- Exit codes
- Limitations
- Compared to alternatives
- Contributing
- License
Requirements
- macOS (Apple Silicon or Intel), Linux, or Windows.
- A few hundred MB to ~8 GB of free RAM, depending on the model. scrybe auto-selects the largest model that fits detected RAM, so it runs on small machines and scales up on large ones.
- From a prebuilt binary — nothing else. The binary embeds whisper.cpp and the voice-activity model.
- From source — a C/C++ toolchain and CMake (whisper.cpp is built by
whisper-rs-sys). Apple Silicon adds the Metal backend with--features metal. Diarization's ONNX Runtime downloads a prebuilt static lib on most targets; on Intel macOS and glibc-2.35 Linux it must be self-built (ORT_LIB_PATH) — the prebuilt binaries already ship it, so prefer those there.
Install
brew install coroboros/tap/scrybe # macOS — the blessed path
npx @coroboros/scrybe # Node toolchains
cargo binstall coroboros-scrybe # prebuilt binary via cargoFrom source (any platform; Apple Silicon adds the Metal backend):
cargo install --path . # CPU
cargo install --path . --features metal # Apple Silicon (Metal)A raw GitHub-release download on macOS may be quarantined — clear it with xattr -d com.apple.quarantine ./scrybe. The Homebrew, npx, and cargo paths are not quarantined.
Usage
scrybe ./recordings # transcribe a folder
scrybe talk.mp3 --format srt # one file, SubRip output
scrybe talk.mp3 --json # stream a JSON transcript to stdout
scrybe call.wav --diarize # label who spoke, in every format
scrybe panel.mp3 --diarize --speakers 3 # pin the speaker count
scrybe --dry-run ./in # resolve the plan without transcribing
scrybe models list # show the model family, sizes, cache status
scrybe models pull large-v3 # pre-fetch a model
scrybe --offline ./in # cached models only, no networkRun scrybe --help for the full flag list.
Why this exists
Running Whisper from the terminal usually means a Python environment, a system ffmpeg, and a separate step to convert audio to 16 kHz WAV before the model ever sees it. scrybe collapses that into one binary.
- Offline and private. Models download once, verified against a pinned SHA-256, into the standard Hugging Face cache. After that, nothing leaves the machine —
--offlineenforces it. - No Python, no system
ffmpeg. A single Rust binary decodes mp3, wav, flac, ogg, and m4a natively via symphonia, resamples to 16 kHz mono, and runs whisper.cpp through whisper-rs. - Metal-accelerated on Apple Silicon, CPU everywhere else. The default build needs no GPU toolchain.
- Zero-config. Omit
--modeland--jobsand scrybe picks the largest model and the concurrency that fit detected RAM, so a flag-free run is never refused by its own memory guard. - Fails loud, never silent. Voice-activity segmentation is always on as a correctness floor. Unsupported codecs, out-of-memory runs, and output collisions stop with an actionable message and a stable exit code rather than emitting garbled or overwritten files.
Models
| Model | Notes |
|-------|-------|
| tiny / base / small | small and fast, lower accuracy |
| large-v3 | most accurate, translation-capable |
| large-v3-turbo | default — near-large-v3 accuracy, much faster |
| distil-large-v3.5 | distilled, fast, English-only (rejects a non-English --lang) |
Weights are ggml builds from the whisper.cpp Hugging Face repos. Only large-v3 translates to English (--task translate); the gate rejects the others before any download. With --model omitted, scrybe resolves the largest model that fits detected RAM at the chosen job count.
--diarize adds a pair of small ONNX models (pyannote segmentation-3.0 + a wespeaker speaker embedding, ~31 MB total), fetched from ungated Hugging Face mirrors with the same pinned-SHA-256 verification — no account, no token. Pre-fetch them with scrybe models pull diarization; they resolve before any transcription starts, so a missing model fails fast.
On an Apple M1, tiny transcribes at ~31×RT on CPU and large-v3-turbo at ~3×RT, rising to ~8×RT on Metal. Full numbers and method: bench/baseline.md.
Codecs
Decoded natively, no system ffmpeg required:
| Extension | Codec |
|-----------|-------|
| wav | PCM |
| mp3 | MP3 |
| flac | FLAC |
| ogg / oga | Vorbis |
| m4a / mp4 / m4b | AAC-LC, ALAC |
HE-AAC/SBR is not handled by the built-in decoder — it fails with exit code 10 rather than emit garbled audio. Re-encode with ffmpeg, or pass --decoder ffmpeg to decode through a system ffmpeg when one is on PATH.
Both decoders stream straight to 16 kHz mono, so the full-resolution source is never held in memory — a long, high-bitrate file (an hour of 48 kHz stereo) decodes fine. Only the 16 kHz output is resident, bounded at ~4.6 hours per file; a longer single clip fails loud with exit code 10, so split marathon recordings first.
Options
Every flag; scrybe --help prints the same surface.
| Option | Default | Description |
| --- | --- | --- |
| <paths>... | (required) | Audio files or directories to transcribe. |
| --recursive | false | Recurse into subdirectories. |
| --model <MODEL> | largest that fits RAM | Whisper model. See Models. |
| --lang <LANG> | auto-detect | Source language code (en, fr, …). |
| --task <TASK> | transcribe | transcribe or translate (to English). |
| --format <FMT,…> | txt | Output formats, comma-separated. See Output formats. |
| --out-dir <DIR> | beside input | Write outputs here instead of next to each input. |
| --jobs <N> | device-aware | Files decoded concurrently ahead of inference. |
| --threads <N> | device-aware | CPU threads per inference job. |
| --force | false | Reprocess inputs even when an up-to-date output exists. |
| --dry-run | false | Print the resolved plan without transcribing. |
| --decoder <BACKEND> | symphonia | symphonia (built-in) or ffmpeg (system). |
| --diarize | false | Label who spoke; speakers land in every output format. |
| --speakers <N> | auto | Exact speaker count (requires --diarize). |
| --json | false | Force JSON; stream to stdout for one file, .json sidecars for many. |
| --offline | false | Use only cached models; never access the network. |
| --no-color | false | Disable colored output. |
| Command | Description |
| --- | --- |
| models list | List the model family, sizes, and which are cached. |
| models pull <MODEL> | Download a model into the cache. |
| models remove <MODEL> | Remove a cached model. |
| models path | Print the model cache directory. |
scrybe models list
scrybe models pull large-v3-turbo
scrybe models path # → ~/.cache/huggingface/hubThe cache honors HF_HOME. Downloads are resumable and verified against a pinned SHA-256; a corrupt cache entry is re-fetched once, or rejected under --offline.
Output formats
--format accepts any comma-separated combination; --json overrides it. Outputs land beside each input (talk.mp3 → talk.srt) or in --out-dir. An up-to-date output is skipped unless --force, and two inputs that would write the same file stop the run rather than overwrite.
| Format | Extension | Contents |
| --- | --- | --- |
| txt | .txt | One segment per line; Speaker 1: prefixes with --diarize. |
| srt | .srt | SubRip cues with HH:MM:SS,mmm timing; Speaker 1: prefixes with --diarize. |
| vtt | .vtt | WebVTT cues with HH:MM:SS.mmm timing; <v Speaker 1> voice tags with --diarize. |
| tsv | .tsv | start, end (milliseconds), text columns; a speaker column with --diarize. |
| csv | .csv | start, end (milliseconds), text — RFC 4180 quoted; a speaker column with --diarize. |
| json | .json | Stable versioned schema — model, language, duration, segments. |
Subtitle timestamps are sanitized: never negative, never overlapping. JSON carries a schema_version so downstream tooling can pin it.
Speaker labels differ by audience: the human formats (txt, srt, vtt) read Speaker 1, Speaker 2, … while the machine formats (json, tsv, csv) use SPEAKER_00, SPEAKER_01, … — WhisperX-compatible and zero-indexed, so the same speaker is Speaker 1 in a subtitle and SPEAKER_00 in the JSON.
{
"schema_version": 1,
"model": "large-v3-turbo",
"language": "en",
"duration": 12.84,
"segments": [
{
"start": 0.0,
"end": 2.4,
"text": "the quick brown fox",
"speaker": "SPEAKER_00",
"words": [
{ "start": 0.0, "end": 0.5, "text": "the", "speaker": "SPEAKER_00" },
{ "start": 0.5, "end": 1.1, "text": "quick", "speaker": "SPEAKER_00" }
]
}
]
}Timestamps are in seconds. Each segment carries a words array of per-word timing, emitted only with JSON output (the other formats carry segment-level timing only). speaker labels (SPEAKER_00, WhisperX-compatible) appear per segment and per word with --diarize. Both fields are additive and optional — absent without --diarize or on a word-less segment — so schema_version stays 1.
Agents
scrybe ships an agent skill — its own usage guide — for coding agents. Install it into your agent:
npx skills add coroboros/scrybeOr read it inline without installing:
scrybe skills get scrybe # print the bundled skill to stdout
scrybe skills list # list bundled skillsscrybe --help carries an Agents: footer with the same pointers. The skill installs the scrybe binary if it is not already on PATH, then drives the CLI for transcription, batch folders, and machine-readable --json output. The same Markdown is the single source — embedded in the binary for skills get and published for npx skills add.
Exit codes
Stable across releases — only ever added, never renumbered.
| Code | Meaning |
|------|---------|
| 0 | success |
| 1 | unexpected error (e.g. failed to write output) |
| 2 | usage error (bad flag or value) |
| 10 | unsupported codec |
| 11 | model download failed |
| 12 | out of memory |
| 13 | GPU init failed |
| 14 | file not found |
| 15 | model load failed (corrupt or incompatible ggml) |
| 16 | transcription failed (compute or decode failure) |
| 17 | speaker diarization failed |
| 20 | partial batch failure, or interrupted before completion |
Limitations
- Per-file length ceiling — decode streams to 16 kHz mono, so the source size is unbounded, but the resident output caps at ~4.6 hours per file (exit
10beyond that). Split marathon recordings. - HE-AAC/SBR — the built-in decoder rejects it rather than mis-decode. Use
--decoder ffmpegor re-encode. - Diarization trades peak accuracy for zero setup —
--diarizeruns the pyannote 3.1 recipe fully offline with agglomerative clustering; on hard audio (heavy overlap, many similar voices) a tuned pyannote/WhisperX Python setup can still edge it out. Segments keep one dominant speaker; per-word labels in JSON carry the detail. Clustering memory grows with the recording's speech length (as in pyannote), so a multi-hour file wants a few GB of headroom. - GPU backends build from source — Metal ships in Apple Silicon prebuilts;
cudaandvulkanare opt-in cargo features built locally.
Compared to alternatives
| Feature | openai-whisper | whisper.cpp (cli) | faster-whisper | WhisperX | scrybe |
| --- | :---: | :---: | :---: | :---: | :---: |
| Runtime | Python + PyTorch | C/C++ | Python | Python | Rust |
| No Python required | no | yes | no | no | yes |
| Single self-contained binary | no | yes (after build) | no | no | yes |
| Native multi-codec decode, no system ffmpeg | no | no (16 kHz WAV / ffmpeg) | no | no | yes |
| Apple Silicon GPU (Metal) | no (CPU/CUDA) | yes | no (CPU/CUDA) | no (CUDA) | yes |
| Folder/batch with progress + summary | no | no | no | no | yes |
| Output txt/srt/vtt/json/tsv/csv | yes (no csv) | yes (no tsv) | via wrapper | yes (no csv) | yes |
| Zero-config model + concurrency | no | no | no | no | yes |
| Stable exit-code contract | no | no | no | no | yes |
| Word-level timestamps | yes | yes | yes | yes (alignment) | yes (JSON) |
| Speaker diarization | no | no | no | yes (HF token) | yes — offline, no account |
scrybe's niche is a single self-contained binary that decodes any common codec, batch-transcribes offline, and labels who spoke — with Metal acceleration, no Python environment, no system ffmpeg, and no Hugging Face account. The Python tools — openai-whisper, faster-whisper, and WhisperX — require a Python environment, a system ffmpeg, and usually CUDA; WhisperX diarization additionally needs an HF token with gated-model access, and its tuned pipeline can still win on the hardest multi-speaker audio. whisper.cpp is the engine scrybe embeds; its own CLI expects pre-converted 16 kHz WAV (or an ffmpeg-enabled build), and leaves model selection, codec decode, batch UX, and output formatting to the caller. scrybe adds those on top.
Contributing
Bug reports and PRs welcome.
- Open an issue before submitting non-trivial PRs.
- Commits follow Conventional Commits.
- Run
cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo testbefore pushing. - Target the
mainbranch.
