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