fa0628f0e2
- slices.py: extract overlapping audio chunks for manual review (filename embeds start/end timestamps for easy reference) - speech_regions.py: find/extract only the speech regions (silence stripping), useful as a faster-whisper pre-processing step - waveform.py: fix silencedetect verbosity (info, not error) + regex parser to handle '[silencedetect @ 0x...] silence_start: 0' prefix - cli.py: new 'slices' subcommand - tests: 12 new tests (slices, speech_regions, regex); 61 total all passing - README: updated with subcommand table + step-by-step workflow
83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
"""Extract overlapping audio slices for manual bad-segment marking.
|
|
|
|
This is the workflow that the original audio-splice-workflow.md described,
|
|
but as a one-shot command. Useful when you don't know the exact timestamps
|
|
and want to scrub through the audio in a media player.
|
|
|
|
Output: 5-second overlapping slices (configurable overlap) for the entire
|
|
audio, each named with start/end timestamps so a media player can display
|
|
them in the filename.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
def extract_slices(
|
|
src: Path,
|
|
output_dir: Path,
|
|
*,
|
|
slice_seconds: float = 5.0,
|
|
overlap_seconds: float = 1.0,
|
|
codec: str | None = None,
|
|
) -> list[Path]:
|
|
"""Cut the audio into overlapping slices for manual review.
|
|
|
|
Returns the list of generated slice files.
|
|
"""
|
|
src = Path(src)
|
|
output_dir = Path(output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Get duration
|
|
out = subprocess.run([
|
|
"ffprobe", "-v", "error",
|
|
"-show_entries", "format=duration",
|
|
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
str(src),
|
|
], capture_output=True, text=True)
|
|
duration = float(out.stdout.strip())
|
|
|
|
stride = slice_seconds - overlap_seconds
|
|
if stride <= 0:
|
|
raise ValueError("overlap_seconds must be < slice_seconds")
|
|
|
|
paths = []
|
|
cursor = 0.0
|
|
idx = 0
|
|
while cursor < duration:
|
|
end = min(cursor + slice_seconds, duration)
|
|
# Filename: slice_NNN_MMSS-MMSS.<ext>
|
|
ext = codec if codec else src.suffix.lstrip(".")
|
|
if not ext.startswith("."):
|
|
ext = "." + ext
|
|
name = f"slice_{idx:03d}_{_fmt(cursor)}-{_fmt(end)}{ext}"
|
|
out_path = output_dir / name
|
|
# Use copy when possible (codec-aligned trims)
|
|
# Re-encode for very precise start times
|
|
subprocess.run([
|
|
"ffmpeg", "-y", "-v", "error",
|
|
"-ss", f"{cursor:.3f}",
|
|
"-to", f"{end:.3f}",
|
|
"-i", str(src),
|
|
"-ar", "48000", "-ac", "1",
|
|
str(out_path),
|
|
], check=True)
|
|
paths.append(out_path)
|
|
idx += 1
|
|
cursor += stride
|
|
return paths
|
|
|
|
|
|
def _fmt(seconds: float) -> str:
|
|
"""Render seconds as MM-SS (filename-safe, no colons)."""
|
|
h = int(seconds // 3600)
|
|
m = int((seconds % 3600) // 60)
|
|
s = int(seconds % 60)
|
|
if h:
|
|
return f"{h}-{m:02d}-{s:02d}"
|
|
return f"{m}-{s:02d}"
|