Files
sami7777 fa0628f0e2 Add slices + speech_regions modules
- 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
2026-07-27 04:00:45 -07:00

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}"