Spaces:
Sleeping
Sleeping
perf: numpy silence (drop 2nd ffmpeg pass) + phase timings
Browse files- music_detector.py +44 -57
music_detector.py
CHANGED
|
@@ -16,9 +16,9 @@ are top-level so they can be unit-tested without a model.
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
import importlib
|
| 19 |
-
import re
|
| 20 |
import subprocess
|
| 21 |
import threading
|
|
|
|
| 22 |
from concurrent.futures import ThreadPoolExecutor
|
| 23 |
from dataclasses import dataclass, field
|
| 24 |
from pathlib import Path
|
|
@@ -43,13 +43,14 @@ MUSIC_CLASSES = {
|
|
| 43 |
"Jingle (music)",
|
| 44 |
}
|
| 45 |
|
| 46 |
-
_SILENCE_START_RE = re.compile(r"silence_start:\s*([0-9]+(?:\.[0-9]+)?)")
|
| 47 |
-
_SILENCE_END_RE = re.compile(r"silence_end:\s*([0-9]+(?:\.[0-9]+)?)")
|
| 48 |
-
|
| 49 |
_AST_LOCK = threading.Lock()
|
| 50 |
# (feature_extractor, model, music_indices)
|
| 51 |
_AST_RUNTIME: tuple[Any, Any, list[int]] | None = None
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# ---------------------------------------------------------------------------
|
| 55 |
# Result type
|
|
@@ -157,60 +158,34 @@ def _top_labels_from_probs(
|
|
| 157 |
return [id2label[int(i)] for i in top_idx if int(i) in id2label]
|
| 158 |
|
| 159 |
|
| 160 |
-
def
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
except Exception:
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def _compute_max_silence(path: str | Path) -> float | None:
|
| 176 |
-
"""Return longest silence gap in seconds via ffmpeg silencedetect, or None on error.
|
| 177 |
-
|
| 178 |
-
Handles trailing silence (file ends while still silent) by using the file
|
| 179 |
-
duration as the implicit silence_end.
|
| 180 |
"""
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
pending: float | None = None
|
| 193 |
-
for line in result.stderr.splitlines():
|
| 194 |
-
m = _SILENCE_START_RE.search(line)
|
| 195 |
-
if m:
|
| 196 |
-
pending = float(m.group(1))
|
| 197 |
-
continue
|
| 198 |
-
m = _SILENCE_END_RE.search(line)
|
| 199 |
-
if m and pending is not None:
|
| 200 |
-
gap = float(m.group(1)) - pending
|
| 201 |
-
if gap > 0:
|
| 202 |
-
gaps.append(gap)
|
| 203 |
-
pending = None
|
| 204 |
-
|
| 205 |
-
# Trailing silence: file ended before silence_end was emitted
|
| 206 |
-
if pending is not None:
|
| 207 |
-
duration = _probe_duration(path)
|
| 208 |
-
if duration is not None and duration > pending:
|
| 209 |
-
gaps.append(duration - pending)
|
| 210 |
-
|
| 211 |
-
return float(max(gaps)) if gaps else None
|
| 212 |
-
except Exception:
|
| 213 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
|
| 216 |
def _signal_checks(
|
|
@@ -238,7 +213,7 @@ def _signal_checks(
|
|
| 238 |
if rms_db < loudness_threshold_db:
|
| 239 |
flags.append("low_loudness")
|
| 240 |
|
| 241 |
-
max_silence_sec =
|
| 242 |
if max_silence_sec is not None and max_silence_sec > silence_threshold_sec:
|
| 243 |
flags.append("long_silence")
|
| 244 |
|
|
@@ -283,8 +258,10 @@ def judge_chunk_files_batched(
|
|
| 283 |
return samples, clipping, rms_db, max_silence, flags
|
| 284 |
|
| 285 |
workers = min(n, n_decode_workers)
|
|
|
|
| 286 |
with ThreadPoolExecutor(max_workers=workers) as ex:
|
| 287 |
file_results = list(ex.map(_process_one, audio_paths))
|
|
|
|
| 288 |
|
| 289 |
# Step 2: build flat window list for batched AST inference
|
| 290 |
# windows[i] → chunk index window_owners[i]
|
|
@@ -303,19 +280,29 @@ def judge_chunk_files_batched(
|
|
| 303 |
# Step 3: batched AST inference
|
| 304 |
win_music_scores: list[float] = []
|
| 305 |
win_top_labels: list[list[str]] = []
|
|
|
|
|
|
|
| 306 |
|
| 307 |
for i in range(0, len(windows), batch_size):
|
| 308 |
batch_wins = [w for w in windows[i:i + batch_size]]
|
|
|
|
| 309 |
inputs = fe(batch_wins, sampling_rate=SAMPLE_RATE, return_tensors="pt", padding=True)
|
| 310 |
inputs = {k: v.to(model.device) for k, v in inputs.items()}
|
|
|
|
|
|
|
|
|
|
| 311 |
with torch.no_grad():
|
| 312 |
logits = model(**inputs).logits
|
| 313 |
probs_batch = torch.sigmoid(logits).cpu().numpy() # (B, 527)
|
|
|
|
| 314 |
|
| 315 |
for row_probs in probs_batch:
|
| 316 |
win_music_scores.append(_music_score_from_probs(row_probs, music_indices))
|
| 317 |
win_top_labels.append(_top_labels_from_probs(row_probs, id2label))
|
| 318 |
|
|
|
|
|
|
|
|
|
|
| 319 |
# Step 4: max-pool windows → per-chunk score
|
| 320 |
chunk_music_scores = [0.0] * n
|
| 321 |
chunk_top_labels: list[list[str]] = [[] for _ in range(n)]
|
|
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
import importlib
|
|
|
|
| 19 |
import subprocess
|
| 20 |
import threading
|
| 21 |
+
import time
|
| 22 |
from concurrent.futures import ThreadPoolExecutor
|
| 23 |
from dataclasses import dataclass, field
|
| 24 |
from pathlib import Path
|
|
|
|
| 43 |
"Jingle (music)",
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
| 46 |
_AST_LOCK = threading.Lock()
|
| 47 |
# (feature_extractor, model, music_indices)
|
| 48 |
_AST_RUNTIME: tuple[Any, Any, list[int]] | None = None
|
| 49 |
|
| 50 |
+
# Per-call phase timings (seconds), overwritten on every judge_chunk_files_batched call.
|
| 51 |
+
# Read by app.py to surface decode / fbank / inference breakdown in the log.
|
| 52 |
+
_LAST_TIMINGS: dict[str, float] = {}
|
| 53 |
+
|
| 54 |
|
| 55 |
# ---------------------------------------------------------------------------
|
| 56 |
# Result type
|
|
|
|
| 158 |
return [id2label[int(i)] for i in top_idx if int(i) in id2label]
|
| 159 |
|
| 160 |
|
| 161 |
+
def _max_silence_from_samples(
|
| 162 |
+
samples: np.ndarray,
|
| 163 |
+
*,
|
| 164 |
+
noise_db: float = -35.0,
|
| 165 |
+
frame_sec: float = 0.02,
|
| 166 |
+
) -> float | None:
|
| 167 |
+
"""Longest contiguous silence (seconds) computed from decoded samples.
|
| 168 |
+
|
| 169 |
+
Replaces a second ffmpeg `silencedetect` pass: we already have the mono16k
|
| 170 |
+
signal, so frame it (20 ms), threshold per-frame RMS at noise_db, and return
|
| 171 |
+
the longest run of silent frames. Matches ffmpeg's noise=-35dB intent.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
"""
|
| 173 |
+
if len(samples) == 0:
|
| 174 |
+
return None
|
| 175 |
+
win = max(int(frame_sec * SAMPLE_RATE), 1)
|
| 176 |
+
n_frames = len(samples) // win
|
| 177 |
+
if n_frames == 0:
|
| 178 |
+
return None
|
| 179 |
+
frames = samples[: n_frames * win].reshape(n_frames, win)
|
| 180 |
+
rms = np.sqrt(np.mean(frames.astype(np.float32) ** 2, axis=1))
|
| 181 |
+
db = 20.0 * np.log10(np.maximum(rms, 1e-9))
|
| 182 |
+
silent = db < noise_db
|
| 183 |
+
if not silent.any():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
return None
|
| 185 |
+
# Run-length of True: diff of zero-padded int mask gives run boundaries.
|
| 186 |
+
edges = np.flatnonzero(np.diff(np.concatenate(([0], silent.view(np.int8), [0]))))
|
| 187 |
+
max_run = int((edges[1::2] - edges[0::2]).max())
|
| 188 |
+
return float(max_run * win / SAMPLE_RATE)
|
| 189 |
|
| 190 |
|
| 191 |
def _signal_checks(
|
|
|
|
| 213 |
if rms_db < loudness_threshold_db:
|
| 214 |
flags.append("low_loudness")
|
| 215 |
|
| 216 |
+
max_silence_sec = _max_silence_from_samples(samples)
|
| 217 |
if max_silence_sec is not None and max_silence_sec > silence_threshold_sec:
|
| 218 |
flags.append("long_silence")
|
| 219 |
|
|
|
|
| 258 |
return samples, clipping, rms_db, max_silence, flags
|
| 259 |
|
| 260 |
workers = min(n, n_decode_workers)
|
| 261 |
+
t_decode = time.time()
|
| 262 |
with ThreadPoolExecutor(max_workers=workers) as ex:
|
| 263 |
file_results = list(ex.map(_process_one, audio_paths))
|
| 264 |
+
decode_sec = time.time() - t_decode
|
| 265 |
|
| 266 |
# Step 2: build flat window list for batched AST inference
|
| 267 |
# windows[i] → chunk index window_owners[i]
|
|
|
|
| 280 |
# Step 3: batched AST inference
|
| 281 |
win_music_scores: list[float] = []
|
| 282 |
win_top_labels: list[list[str]] = []
|
| 283 |
+
fbank_sec = 0.0
|
| 284 |
+
infer_sec = 0.0
|
| 285 |
|
| 286 |
for i in range(0, len(windows), batch_size):
|
| 287 |
batch_wins = [w for w in windows[i:i + batch_size]]
|
| 288 |
+
t_fb = time.time()
|
| 289 |
inputs = fe(batch_wins, sampling_rate=SAMPLE_RATE, return_tensors="pt", padding=True)
|
| 290 |
inputs = {k: v.to(model.device) for k, v in inputs.items()}
|
| 291 |
+
fbank_sec += time.time() - t_fb
|
| 292 |
+
|
| 293 |
+
t_inf = time.time()
|
| 294 |
with torch.no_grad():
|
| 295 |
logits = model(**inputs).logits
|
| 296 |
probs_batch = torch.sigmoid(logits).cpu().numpy() # (B, 527)
|
| 297 |
+
infer_sec += time.time() - t_inf
|
| 298 |
|
| 299 |
for row_probs in probs_batch:
|
| 300 |
win_music_scores.append(_music_score_from_probs(row_probs, music_indices))
|
| 301 |
win_top_labels.append(_top_labels_from_probs(row_probs, id2label))
|
| 302 |
|
| 303 |
+
_LAST_TIMINGS.clear()
|
| 304 |
+
_LAST_TIMINGS.update(decode=decode_sec, fbank=fbank_sec, infer=infer_sec)
|
| 305 |
+
|
| 306 |
# Step 4: max-pool windows → per-chunk score
|
| 307 |
chunk_music_scores = [0.0] * n
|
| 308 |
chunk_top_labels: list[list[str]] = [[] for _ in range(n)]
|