butts is a lightweight Python CLI that prepares .webm, .mp4 (and similar) audio/video files for OpenAI’s gpt-4o-transcribe-diarize endpoint, cuts oversized or overlong media into upload-friendly chunks, and merges the speaker-aware transcripts back into neatly formatted JSON and/or TXT outputs.
- Automatic chunking of recordings that exceed the 25 MB OpenAI upload limit or the configured duration limit (ffmpeg-based, with overlap handling).
- Audio-only preprocessing for video or large inputs (
--preprocess-audio autoby default) to strip video and create compact mono Opus audio before chunking. - Speaker diarization via
gpt-4o-transcribe-diarize, including optional speaker-map remapping for human-friendly names. - Multiple output formats: JSON (with metadata & pricing estimate) and plaintext transcripts.
- Resumable chunk cache under
transcripts/.butts-cache/so completed chunk transcriptions can be reused on rerun. - Speaker diagnostics to inspect speaker labels by chunk before building a richer speaker map.
- Verbose logging to follow audio preprocessing, chunk creation, cache hits/misses, retries, per-chunk elapsed time, merging, and final cost/timing summaries.
- Python 3.11+ (the repo pins
requires-python >=3.10, but tooling prefers 3.11 viauv). uv(https://github.com/astral-sh/uv) for environment management.ffmpeg/ffprobeinstalled and on your$PATH.- An
OPENAI_API_KEYwith access togpt-4o-transcribe-diarize.
git clone https://github.com/<you>/butts.git
cd butts
# Setup virtual environment via uv
uv venv .venv
uv pip install --python .venv/bin/python -e .
# Run the CLI (auto-chunk, JSON output)
OPENAI_API_KEY=sk-your-key \
uv run butts path/to/video.mp4 --out transcripts --verboseusage: butts [-h] [-o OUT] [--format {json,txt,json+txt}] [--chunking {auto,none}]
[--max-chunk-mb MAX_CHUNK_MB]
[--max-chunk-seconds MAX_CHUNK_SECONDS]
[--preprocess-audio {auto,always,never}]
[--large-file-strategy {quality,fast,legacy}]
[--speaker-map SPEAKER_MAP] [--apply-speaker-map] [--remap-only]
[--inspect-speakers] [--overwrite] [--skip-existing]
[--verbose] [inputs ...]
Common flags:
--chunking auto|none– chunk large files (defaultauto).--max-chunk-mb– target chunk size (default 24 MB).--max-chunk-seconds– maximum chunk duration, including overlap (default 1200 seconds).--preprocess-audio auto|always|never– extract compact mono Opus audio before chunking/transcription.autopreprocesses video inputs and files larger than--max-chunk-mb.--large-file-strategy quality|fast|legacy– apply large-file presets. Explicit chunk/preprocess flags override the preset.--speaker-map SPEAKER_MAP+--apply-speaker-map– apply friendly names.--remap-only– apply--speaker-mapto existing JSON transcripts without calling OpenAI.--inspect-speakers– summarize speaker labels and representative snippets in existing JSON transcripts without calling OpenAI.--format json|txt|json+txt– choose desired output(s); defaults to JSON only.--overwrite/--skip-existing– control existing outputs.
By default, butts preprocesses video inputs and large files into temporary audio-only WebM before deciding whether to chunk. This usually reduces the number of chunks because video bitrate no longer drives upload size. Chunking then respects both --max-chunk-mb and --max-chunk-seconds, choosing the fewest chunks that fit both limits:
uv run butts meeting.webm --out transcripts --format json+txt --overwrite --verboseUse --preprocess-audio never only when you need legacy behavior that uploads/chunks the original media. Use --preprocess-audio always to force audio extraction for every input, including small audio-only files.
Use --max-chunk-seconds if OpenAI changes model duration limits or if you want shorter requests for operational reasons. The default 1200 seconds is intentionally below the currently observed 1400-second model limit.
Large-file strategy presets:
quality– default behavior: audio preprocessingauto, chunkingauto, 1200-second chunks.fast– audio preprocessingauto, chunkingauto, 600-second chunks for shorter individual requests/retries.legacy– disables audio preprocessing unless explicitly overridden; useful when you need original-media upload/chunk behavior.
Temporary preprocessed audio files are deleted automatically after the run.
Each successfully transcribed chunk is written immediately to:
transcripts/.butts-cache/<source-hash>/chunk-000.json
On rerun, matching cached chunks are reused instead of being sent to OpenAI again. Cache entries are invalidated when the source file fingerprint, model, preprocessing settings, chunk size, chunk duration, or chunk metadata changes. Passing --overwrite bypasses existing cache entries and refreshes them.
The cache stores normalized transcript segments and metadata, not raw OpenAI responses, and the cache directory is gitignored.
The original flat speaker map still works:
{
"A": "Derek",
"B": "Kelly"
}For chunked files where model speaker labels change between requests, use richer maps with optional global, per-chunk, and time-window corrections:
{
"global": {
"A": "Derek",
"B": "Kelly"
},
"chunks": {
"0": {
"A": "Derek",
"B": "Kelly"
},
"1": {
"A": "Kelly",
"B": "Derek"
}
},
"windows": [
{
"start": 1500,
"end": 2000,
"map": {
"A": "Kelly",
"C": "Derek"
}
}
]
}Time windows use transcript timestamps in seconds. More specific maps override less specific ones in this order: time window, chunk, then global. When speaker maps are applied, JSON output includes raw_speaker and chunk_index fields so the model’s original labels remain auditable.
To repair an existing JSON transcript without re-transcribing:
uv run butts transcripts/meeting.json \
--remap-only \
--speaker-map speaker_map.json \
--out transcripts/remapped \
--format json+txt \
--overwriteTo inspect model speaker labels before writing a speaker map:
uv run butts transcripts/meeting.json --inspect-speakersInspection groups labels by chunk_index when available and prints representative snippets for each raw speaker label.
# Install dev tools
uv pip install --python .venv/bin/python ruff mypy
# Lint & format
uv run ruff check src
uv run ruff format --check src
# (Future) run tests
uv run pytest
uv run pytest tests/test_cli.py::test_specific_caseManual regression run (since no automated tests yet):
uv run butts samples/manual-test.webm --chunking none --out transcripts --overwrite --verboseInspect transcripts/manual-test.json to ensure diarized segments still look sane.
src/butts_cli/cli.py– all CLI logic (arg parsing, chunking, transcription, merging, output).pyproject.toml– Hatch/uv metadata and entry point (butts_cli.cli:main).spec.md– product requirements & known limitations.plan.md– implementation checklist (current status).samples/– small fixture clips;transcripts/– gitignored outputs.
- 25 MB upload limit: The CLI autogenerates chunks under
--max-chunk-mb, but a very low limit may be impossible for high-bitrate media unless audio preprocessing is enabled. - Model duration limit: The default
--max-chunk-seconds 1200keeps chunks below the currently observed model duration cap, but--chunking noneintentionally bypasses local chunk planning. - Diarization quality: dependent on audio clarity; overlapping speakers or heavy noise reduce accuracy. Describe mitigation steps in PRs.
- Cost/time: Each chunk incurs an OpenAI request; long recordings can take several minutes and cost proportionally.
- Cache behavior:
--overwriteintentionally bypasses the chunk cache. Deletetranscripts/.butts-cache/if you want to remove all cached chunk transcripts.
Contributions are welcome! Please:
- Follow
AGENTS.mdfor coding conventions (imports, logging, error handling, etc.). - Use
uv-managed environments and do not hardcode secrets. - Provide sample command/output or unit tests where feasible.
- Document notable changes in
spec.mdorplan.mdif applicable.
This project is licensed under the MIT License. Feel free to fork, modify, and distribute per the license terms.