Spaces:
Sleeping
Sleeping
Download app/services/timeline.py from Rthur2003/crowncode-backend: direct link, hf CLI and curl.
- Browser
- Download file 6.7 kB
-
https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/timeline.py
- Command line
-
hf download hf://spaces/Rthur2003/crowncode-backend/app/services/timeline.py
-
curl -L -o timeline.py https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/timeline.py
6.7 kB
| """Whole-track scan. | |
| The opening clip gives AURIS its verdict. This module reads the rest of the | |
| track: the first six minutes are cut into windows of about 30 s, every window | |
| goes through the same features and the same LightGBM model, and the readings | |
| come back as a timeline the page draws under the waveform. | |
| A window score is the classifier applied to an excerpt shorter than the 60 s | |
| clips it was trained on, and every training clip was either all AI or all | |
| human. A timeline therefore shows where the music reads AI-like, not where | |
| an AI took over. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import threading | |
| import time | |
| from concurrent.futures import ThreadPoolExecutor | |
| from typing import Any, Callable, Dict, List, Optional, Tuple | |
| import numpy as np | |
| from .feature_extractor import extract_features_array | |
| from .inference_xai import XAIInferenceService | |
| from .logging_config import get_logger | |
| from .vocal_analyzer import analyze_vocals_array | |
| logger = get_logger(__name__) | |
| TARGET_WINDOW_SEC = 30.0 | |
| # A window quieter than this never reaches the classifier. | |
| SILENCE_PEAK = 1e-3 | |
| PEAK_POINTS = 600 | |
| # Windows not started within this many seconds are reported as skipped. | |
| SCAN_BUDGET_SEC = float(os.getenv("AURIS_SCAN_BUDGET_SEC", "180")) | |
| SCAN_WORKERS = max(1, int(os.getenv("AURIS_SCAN_WORKERS", "2"))) | |
| def plan_windows(duration: float) -> List[Tuple[float, float]]: | |
| """Equal windows of about 30 s that tile the whole duration (20 to 45 s each).""" | |
| if duration <= 0: | |
| return [] | |
| count = max(1, int(round(duration / TARGET_WINDOW_SEC))) | |
| size = duration / count | |
| return [ | |
| (round(i * size, 3), round(duration if i == count - 1 else (i + 1) * size, 3)) | |
| for i in range(count) | |
| ] | |
| def waveform_peaks(y: np.ndarray, points: int = PEAK_POINTS) -> List[float]: | |
| """Peak level per slice of the track, 0 to 1, for drawing the waveform.""" | |
| if y.size == 0: | |
| return [] | |
| edges = np.linspace(0, y.size, min(points, y.size) + 1).astype(int) | |
| peaks = np.array([ | |
| float(np.max(np.abs(y[a:b]))) if b > a else 0.0 | |
| for a, b in zip(edges[:-1], edges[1:]) | |
| ]) | |
| top = float(peaks.max()) | |
| if top <= 0: | |
| return [0.0] * len(peaks) | |
| return [round(float(v), 3) for v in peaks / top] | |
| def _read_window( | |
| y: np.ndarray, | |
| sr: int, | |
| index: int, | |
| start: float, | |
| end: float, | |
| xai: XAIInferenceService, | |
| ) -> Dict[str, Any]: | |
| base: Dict[str, Any] = {"index": index, "start": round(start, 2), "end": round(end, 2)} | |
| seg = np.ascontiguousarray(y[int(start * sr):min(y.size, int(end * sr))]) | |
| if seg.size < sr: | |
| return {**base, "state": "skipped"} | |
| if float(np.max(np.abs(seg))) < SILENCE_PEAK: | |
| return {**base, "state": "silent"} | |
| try: | |
| features = extract_features_array(seg, sr) | |
| vocals = None | |
| try: | |
| vocals = analyze_vocals_array(seg, sr) | |
| except Exception as e: # noqa: BLE001 - the main pipeline also goes on without vocals | |
| logger.warning(f"Window {index}: vocal analysis failed: {e}") | |
| reading = xai.predict_window(features, vocals, window_sec=end - start) | |
| if reading is None: | |
| return {**base, "state": "failed"} | |
| return { | |
| **base, | |
| "state": "ok", | |
| "probability": round(reading.probability, 4), | |
| "isAi": reading.is_ai, | |
| "margin": reading.margin, | |
| "hasVocals": bool(vocals and vocals.has_vocals), | |
| "reasons": [ | |
| { | |
| "name": c.name, | |
| "label": c.label, | |
| "labelEn": c.label_en, | |
| "direction": c.direction, | |
| "shapValue": round(c.shap_value, 4), | |
| } | |
| for c in reading.reasons | |
| ], | |
| } | |
| except Exception as e: # noqa: BLE001 - one bad window must not sink the scan | |
| logger.warning(f"Window {index} ({start:.0f}-{end:.0f}s) failed: {e}") | |
| return {**base, "state": "failed"} | |
| def _summarise(segments: List[Dict[str, Any]]) -> Dict[str, Any]: | |
| scored = [s for s in segments if s["state"] == "ok"] | |
| seconds = sum(s["end"] - s["start"] for s in scored) | |
| if not scored or seconds <= 0: | |
| return { | |
| "scoredCount": 0, "flaggedCount": 0, "scoredSec": 0.0, | |
| "aiShare": 0.0, "meanProbability": 0.0, "maxProbability": 0.0, "peakIndex": None, | |
| } | |
| flagged = [s for s in scored if s["isAi"]] | |
| peak = max(scored, key=lambda s: s["probability"]) | |
| return { | |
| "scoredCount": len(scored), | |
| "flaggedCount": len(flagged), | |
| "scoredSec": round(seconds, 1), | |
| "aiShare": round(sum(s["end"] - s["start"] for s in flagged) / seconds, 3), | |
| "meanProbability": round(sum(s["probability"] * (s["end"] - s["start"]) for s in scored) / seconds, 4), | |
| "maxProbability": peak["probability"], | |
| "peakIndex": peak["index"], | |
| } | |
| def scan_track( | |
| y: np.ndarray, | |
| sr: int, | |
| xai: XAIInferenceService, | |
| *, | |
| total_sec: float, | |
| on_progress: Optional[Callable[[int, int], None]] = None, | |
| cancelled: Optional[Callable[[], bool]] = None, | |
| budget_sec: float = SCAN_BUDGET_SEC, | |
| ) -> Dict[str, Any]: | |
| """Read every window of `y` and return the timeline payload (blocking).""" | |
| duration = y.size / sr | |
| windows = plan_windows(duration) | |
| started = time.monotonic() | |
| lock = threading.Lock() | |
| finished = 0 | |
| def work(item: Tuple[int, Tuple[float, float]]) -> Dict[str, Any]: | |
| nonlocal finished | |
| index, (start, end) = item | |
| if (cancelled is not None and cancelled()) or time.monotonic() - started > budget_sec: | |
| out: Dict[str, Any] = {"index": index, "start": round(start, 2), "end": round(end, 2), "state": "skipped"} | |
| else: | |
| out = _read_window(y, sr, index, start, end, xai) | |
| with lock: | |
| finished += 1 | |
| if on_progress is not None: | |
| on_progress(finished, len(windows)) | |
| return out | |
| with ThreadPoolExecutor(max_workers=min(SCAN_WORKERS, max(1, len(windows)))) as pool: | |
| segments = list(pool.map(work, enumerate(windows))) | |
| logger.info( | |
| f"Timeline: {len(windows)} windows over {duration:.0f}s in {time.monotonic() - started:.1f}s, " | |
| f"{sum(1 for s in segments if s['state'] == 'ok')} scored" | |
| ) | |
| return { | |
| "durationSec": round(duration, 2), | |
| "totalSec": round(max(total_sec, duration), 2), | |
| "truncated": total_sec - duration > 1.0, | |
| "threshold": round(float(xai.threshold), 6), | |
| "peaks": waveform_peaks(y), | |
| "segments": segments, | |
| "summary": _summarise(segments), | |
| } | |