Same inaSpeechSegmenter engine split into reusable core/ (fftools, segmenter, exporters) + utils/ and a CustomTkinter multi-tab UI. Adds JSON and ffmpeg-script export. Extracted from the stream_tools app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
"""
|
|
Singing segment detection via inaSpeechSegmenter.
|
|
|
|
Pipeline:
|
|
1. Run inaSpeechSegmenter to classify speech/music/noise
|
|
2. Filter for 'music' segments (singing voice is classified as music)
|
|
3. Merge nearby segments, apply padding, filter by minimum duration
|
|
"""
|
|
|
|
import time
|
|
from utils.formats import format_timecode
|
|
|
|
|
|
def check_segmenter_available() -> tuple[bool, str]:
|
|
"""Check if inaSpeechSegmenter is installed."""
|
|
try:
|
|
from inaSpeechSegmenter import Segmenter # noqa: F401
|
|
return True, ""
|
|
except ImportError:
|
|
return False, (
|
|
"inaSpeechSegmenter is not installed.\n\n"
|
|
"Install it with:\n"
|
|
" pip install inaSpeechSegmenter\n\n"
|
|
"You also need TensorFlow:\n"
|
|
" pip install tensorflow[and-cuda] (GPU)\n"
|
|
" pip install tensorflow (CPU)"
|
|
)
|
|
|
|
|
|
def run_segmentation(wav_path: str, progress_cb=None) -> list:
|
|
"""
|
|
Run inaSpeechSegmenter on audio file.
|
|
Returns list of (label, start_sec, end_sec) tuples.
|
|
Labels: 'music', 'speech', 'noise', 'noEnergy'
|
|
"""
|
|
from inaSpeechSegmenter import Segmenter
|
|
|
|
if progress_cb:
|
|
progress_cb("Loading segmentation model…")
|
|
|
|
start_time = time.time()
|
|
seg = Segmenter(vad_engine='smn', detect_gender=False)
|
|
|
|
if progress_cb:
|
|
progress_cb("Running audio segmentation (this may take a while)…")
|
|
|
|
segments = seg(wav_path)
|
|
elapsed = time.time() - start_time
|
|
|
|
if progress_cb:
|
|
progress_cb(f"Segmentation complete in {elapsed:.1f}s — {len(segments)} raw segments")
|
|
|
|
return segments
|
|
|
|
|
|
def merge_singing_segments(
|
|
segments: list,
|
|
max_gap: float = 10.0,
|
|
min_duration: float = 20.0,
|
|
padding: float = 2.0,
|
|
) -> list:
|
|
"""
|
|
Filter for 'music' segments and merge nearby ones.
|
|
|
|
Returns list of dicts:
|
|
[{"index", "start", "end", "duration", "original_start", "original_end"}, ...]
|
|
"""
|
|
music_segs = [(start, end) for label, start, end in segments if label == "music"]
|
|
|
|
if not music_segs:
|
|
return []
|
|
|
|
music_segs.sort(key=lambda x: x[0])
|
|
|
|
# Merge segments with gaps smaller than max_gap
|
|
merged = []
|
|
current_start, current_end = music_segs[0]
|
|
|
|
for start, end in music_segs[1:]:
|
|
if start - current_end <= max_gap:
|
|
current_end = max(current_end, end)
|
|
else:
|
|
merged.append((current_start, current_end))
|
|
current_start, current_end = start, end
|
|
merged.append((current_start, current_end))
|
|
|
|
# Apply padding and minimum duration filter
|
|
results = []
|
|
idx = 1
|
|
for start, end in merged:
|
|
duration = end - start
|
|
if duration >= min_duration:
|
|
padded_start = max(0, start - padding)
|
|
padded_end = end + padding
|
|
results.append({
|
|
"index": idx,
|
|
"start": padded_start,
|
|
"end": padded_end,
|
|
"duration": padded_end - padded_start,
|
|
"original_start": start,
|
|
"original_end": end,
|
|
})
|
|
idx += 1
|
|
|
|
return results
|
|
|
|
|
|
def get_segment_stats(segments: list) -> dict:
|
|
"""Get summary statistics from raw segmentation output."""
|
|
type_counts = {}
|
|
type_durations = {}
|
|
for label, start, end in segments:
|
|
type_counts[label] = type_counts.get(label, 0) + 1
|
|
type_durations[label] = type_durations.get(label, 0) + (end - start)
|
|
return {
|
|
"counts": type_counts,
|
|
"durations": type_durations,
|
|
}
|