Split a voice recording into one clip per utterance, cutting on the silence between them.
Point it at a file or a folder, tell it how long a pause has to be before it counts as a break, and it writes numbered clips plus a manifest of where each one came from. Useful for cutting a take into words, sentences, or prompts — TTS/ASR datasets, voice-over reels, sample libraries, transcript-aligned chunks.
- One dependency: ffmpeg. No Python audio stack, nothing to compile. Detection and cutting both run through ffmpeg, so files are never fully decoded into memory.
- Dry run first. Without
-oit prints the split it would make, so you can tune the threshold without writing a thing. - Every knob is a flag, and every flag can be a default or a per-file rule in a config file.
- Optional: transcribe once and name each clip after the words in it.
$ splitvoice interview.wav
splitvoice 0.1.0 | 1 file(s) | dry run, nothing will be written
------------------------------------------------------------------------
[1/1] interview.wav
00:00:41.320 pcm_s16le 44100 Hz 1 ch
kind=auto min_silence=0.7s thresh=-40.0dBFS
17 silence gaps -> 18 segments
# start end dur(s) clip
1 00:00:00.000 00:00:02.118 2.12 001_untitled.wav
2 00:00:02.940 00:00:05.512 2.57 002_untitled.wav
...
ffmpeg must be on your PATH:
brew install ffmpeg # macOS
sudo apt install ffmpeg # Debian/Ubuntu
winget install Gyan.FFmpeg # WindowsThen:
pip install splitvoice
# or, for the optional transcription feature:
pip install "splitvoice[stt]"From a clone:
git clone https://github.com/ulvxa/splitvoice
cd splitvoice
pip install -e ".[dev]"Prefer not to install it at all? The package runs straight from the source tree with
python -m splitvoice ....
splitvoice recording.wav # preview the split
splitvoice recording.wav -o clips/ # write the clips
splitvoice takes/ -o clips/ -r # a whole folder, subfolders included
splitvoice recording.wav -o clips/ -d 0.35 # split on shorter pauses
splitvoice recording.wav -o clips/ -t auto # derive the threshold from the file
splitvoice recording.wav -o clips/ -k sentences # start from the sentence presetClips land in clips/<recording name>/001_....wav, next to a manifest.csv and
manifest.json listing every segment's source, start, end, duration and filename. Use
--flat to put everything in one folder instead (filenames get the recording name as a
prefix, so they stay unique).
Splitting is decided by exactly two numbers:
| Flag | Means | Turn it… |
|---|---|---|
-d, --min-silence-len |
how long a pause must last to count as a break (seconds) | down if utterances get glued together, up if one utterance gets chopped mid-word |
-t, --silence-thresh |
how quiet audio must be to count as silence (dBFS) | up toward -25 for noisy/roomy recordings, down toward -55 for a clean studio take |
If you get 0 segments, the file never got quiet enough — raise the threshold (-t -30). If
you get 1 giant segment, same fix, or lower -d. If you get far too many, raise -d or add
--min-segment-len 0.3 to throw away scraps.
-t auto measures the file's mean loudness and sets the threshold 12 dB below it, which is
a good starting point for anything recorded at an unfamiliar level.
--kind describes what a recording holds. It is only a starting point — a bundle of default
values that any explicit flag or config entry overrides.
| Kind | min silence | min segment | also |
|---|---|---|---|
auto (default) |
0.7 s | — | plain silence splitting |
letters |
0.5 s | 0.05 s | one-token labels, uniformity check |
words |
0.7 s | 0.1 s | uniformity check, resegment when transcribing |
phrases |
0.9 s | 0.2 s | resegment when transcribing |
sentences |
1.2 s | 0.3 s | resegment when transcribing |
paragraphs |
2.0 s | 0.5 s | merges segments < 0.3 s apart |
splitvoice alphabet.wav -o letters/ --kind letters
splitvoice audiobook.m4a -o chapters/ --kind paragraphs--format mp3 # clip container/extension (default wav)
--codec libmp3lame # force a specific ffmpeg encoder
--sample-rate 16000 # resample on the way out
--mono # downmix
--padding 0.15 # audio kept on each side of a cut (default 0.1)
--name-mode index # 001.wav instead of 001_label.wav; `label` drops the number
--ascii-names # strip diacritics from filenames
--manifest csv # csv | json | both | none
--json # print the manifest to stdout and nothing else--json makes the tool scriptable:
splitvoice take.wav --json | jq '[.[] | select(.duration_s > 3)] | length'Anything you would type as a flag can live in splitvoice.toml — in the current folder,
next to your audio, or passed with -c. [defaults] applies to every file; each
[[profile]] applies to files whose name matches its glob. Later matching profiles win, and
a flag on the command line beats all of it.
[defaults]
kind = "sentences"
silence_thresh = -38.0
padding = 0.15
format = "wav"
[[profile]]
match = "*_letters*" # matched against the filename and the stem
kind = "letters"
min_silence_len = 0.5
[[profile]]
match = "noisy_*"
silence_thresh = -28.0
[[profile]]
match = "scratch_*"
skip = true # ignored unless you pass --allValid keys: kind, min_silence_len, silence_thresh, min_segment_len, merge_gap,
padding, format, transcribe, resegment, sentence_gap, anchor, label_regex,
max_label_words, uniform, skip. Unknown keys are an error rather than a silent no-op.
JSON (splitvoice.json) works too, with "profiles": [...].
With --transcribe, each file is sent once to
ElevenLabs speech-to-text and the
words are mapped onto the segments, so clips come out as 003_good_morning.wav instead of
003_untitled.wav. The transcript is cached in .transcripts/, so re-running with
different splitting flags costs nothing extra.
export ELEVENLABS_API_KEY=... # or put it in a .env file
splitvoice take.wav -o clips/ --transcribe --language enRelated flags:
--model scribe_v2 # STT model id
--language en # omit to auto-detect
--keyterm "Kubernetes" # bias the transcript toward a term (repeatable)
--keyterms-file terms.txt # one term per line
--refresh-transcript # ignore the cache
--label-regex "say (\w+)" # capture groups become the filename
--max-label-words 4 # cap how many words end up in a filename
--align sequential # force 1 word per segment instead of timestamp overlap--resegment rebuilds the segments from the transcript instead of trusting silence
alone: words are grouped into utterances by the gaps between them (--sentence-gap), then
each group's edges snap out to the nearest silence boundaries. This fixes both halves of the
usual problem — utterances merged because the pause fell under the threshold, and utterances
split because the speaker paused mid-sentence.
When a recording repeats the same carrier phrase, --anchor is more reliable than any
gap: give it a regex matching the first word of each repetition and it starts a new utterance
there, whatever the timing.
splitvoice prompts.wav -o clips/ --transcribe --resegment \
--anchor '^[Ss]ay$' --label-regex 'say (.+?)[.!?]*$'--uniform flags segments whose word count differs from the file's usual count with a ! in
the table — a quick way to spot a bad cut in a recording where every take should have the
same shape.
from splitvoice import audio, segment
duration = audio.probe("take.wav")["duration"]
silences = audio.detect_silence("take.wav", min_silence_len=0.7,
threshold_db=-40, duration=duration)
segments = segment.invert_silence(silences, duration)
for i, seg in enumerate(segment.pad(segments, 0.1, duration), 1):
audio.extract_clip("take.wav", f"{i:03d}.wav", seg.start, seg.end)pip install -e ".[dev]"
pytest
ruff check .The end-to-end tests synthesize their own audio with ffmpeg and skip if it is missing.
0 success · 1 finished with per-file failures · 2 bad usage or environment ·
130 interrupted.
MIT — see LICENSE.