voice-normalization / scripts /segment_audio.py
RishiKar210's picture
Initial backend deploy
99df98c
Raw
History Blame Contribute Delete
3.63 kB
"""Sentence-level audio segmentation using Silero VAD.
Splits a long audio clip into per-sentence WAVs based on natural speech
boundaries. Tuned for sentences (not word-level chunks) by using a longer
silence-duration threshold to merge intra-sentence pauses but split between
sentences.
Usage:
python scripts/segment_audio.py INPUT_WAV OUTPUT_DIR
Outputs:
OUTPUT_DIR/seg_001.wav, seg_002.wav, ...
OUTPUT_DIR/manifest.json (start, end, duration, filename per segment)
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import soundfile as sf
import torch
SAMPLE_RATE = 16000
# Tuned for sentence-level splits (not word-level).
VAD_THRESHOLD = 0.5 # speech probability threshold
MIN_SPEECH_MS = 300 # ignore blips shorter than this
MIN_SILENCE_MS = 500 # silence > this = sentence boundary
SPEECH_PAD_MS = 200 # pad each segment to avoid clipping word edges
def segment(input_wav: Path, out_dir: Path) -> dict:
out_dir.mkdir(parents=True, exist_ok=True)
# Load audio
audio, sr = sf.read(str(input_wav), dtype="float32")
if sr != SAMPLE_RATE:
raise ValueError(f"Expected {SAMPLE_RATE} Hz, got {sr}. Re-export with ffmpeg -ar 16000.")
if audio.ndim > 1:
audio = audio.mean(axis=1)
audio_t = torch.from_numpy(audio)
# Load Silero VAD (cached from prior Phase 1 setup)
model, utils = torch.hub.load(
repo_or_dir="snakers4/silero-vad",
model="silero_vad",
force_reload=False,
trust_repo=True,
)
get_speech_timestamps = utils[0]
timestamps = get_speech_timestamps(
audio_t, model,
sampling_rate=SAMPLE_RATE,
threshold=VAD_THRESHOLD,
min_speech_duration_ms=MIN_SPEECH_MS,
min_silence_duration_ms=MIN_SILENCE_MS,
speech_pad_ms=SPEECH_PAD_MS,
)
# Write each segment as its own WAV + build manifest
manifest = []
for i, ts in enumerate(timestamps, start=1):
start_s = ts["start"] / SAMPLE_RATE
end_s = ts["end"] / SAMPLE_RATE
clip = audio[ts["start"]:ts["end"]]
fname = f"seg_{i:03d}.wav"
out_path = out_dir / fname
sf.write(str(out_path), clip, SAMPLE_RATE, subtype="PCM_16")
manifest.append({
"filename": fname,
"index": i,
"start_seconds": round(start_s, 3),
"end_seconds": round(end_s, 3),
"duration_seconds": round(end_s - start_s, 3),
})
manifest_data = {
"source": str(input_wav),
"sample_rate": SAMPLE_RATE,
"vad_settings": {
"threshold": VAD_THRESHOLD,
"min_speech_ms": MIN_SPEECH_MS,
"min_silence_ms": MIN_SILENCE_MS,
"speech_pad_ms": SPEECH_PAD_MS,
},
"n_segments": len(manifest),
"segments": manifest,
}
manifest_path = out_dir / "manifest.json"
manifest_path.write_text(json.dumps(manifest_data, indent=2), encoding="utf-8")
return manifest_data
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: segment_audio.py INPUT_WAV OUTPUT_DIR")
sys.exit(1)
in_wav = Path(sys.argv[1])
out_dir = Path(sys.argv[2])
if not in_wav.exists():
print(f"Input not found: {in_wav}")
sys.exit(1)
result = segment(in_wav, out_dir)
print(f"Wrote {result['n_segments']} segments to {out_dir}")
for s in result["segments"]:
print(f" {s['filename']} {s['start_seconds']:6.2f}s -> {s['end_seconds']:6.2f}s ({s['duration_seconds']:.2f}s)")