fosters commited on
Commit
3c36f58
·
verified ·
1 Parent(s): b412294

perf: numpy silence (drop 2nd ffmpeg pass) + phase timings

Browse files
Files changed (1) hide show
  1. 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 _probe_duration(path: str | Path) -> float | None:
161
- """Return audio duration in seconds via ffprobe, or None on error."""
162
- try:
163
- cmd = [
164
- "ffprobe", "-v", "error",
165
- "-show_entries", "format=duration",
166
- "-of", "default=noprint_wrappers=1:nokey=1",
167
- str(path),
168
- ]
169
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
170
- return float(result.stdout.strip())
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
- try:
182
- # Note: do NOT use -v quiet here — it suppresses silencedetect filter messages.
183
- # Use -hide_banner + -nostats to keep stderr clean while preserving filter output.
184
- cmd = [
185
- "ffmpeg", "-hide_banner", "-nostats",
186
- "-i", str(path),
187
- "-af", "silencedetect=noise=-35dB:d=0.5",
188
- "-f", "null", "-",
189
- ]
190
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
191
- gaps: list[float] = []
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 = _compute_max_silence(path)
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)]