Rthur2003 commited on
Commit
56f8e05
·
1 Parent(s): d3432f6

feat: add wav2vec2 inference service for AURIS music detection

Browse files
Files changed (1) hide show
  1. app/services/wav2vec2_detector.py +175 -0
app/services/wav2vec2_detector.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ wav2vec2 inference service for AURIS (Tower 1).
3
+
4
+ Loads the fine-tuned wav2vec2 model and provides
5
+ real-time predictions. Falls back gracefully if
6
+ model file is not available.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Optional, Union
15
+
16
+ import numpy as np
17
+
18
+ from .logging_config import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ _MODEL_PATH = Path(__file__).resolve().parents[2] / "models" / "wav2vec2_auris_v1.pt"
23
+ _SAMPLE_RATE = 16000
24
+ _MAX_SEC = 30.0
25
+
26
+
27
+ @dataclass
28
+ class Wav2Vec2Result:
29
+ """Result from wav2vec2 tower."""
30
+
31
+ available: bool
32
+ p_ai: float = 0.5
33
+ # Hidden state statistics for meta-classifier
34
+ hidden_mean: float = 0.0
35
+ hidden_std: float = 0.0
36
+ hidden_max: float = 0.0
37
+ hidden_min: float = 0.0
38
+ hidden_kurtosis: float = 0.0
39
+ error: Optional[str] = None
40
+
41
+
42
+ class Wav2Vec2DetectorService:
43
+ """
44
+ Tower 1: wav2vec2-based AI music detection.
45
+
46
+ Loads the fine-tuned model from disk or HuggingFace Hub.
47
+ On CPU, inference takes ~0.5s for 30s audio.
48
+ """
49
+
50
+ def __init__(self, model_path: Optional[Path] = None):
51
+ self._model = None
52
+ self._device = None
53
+ self._initialized = False
54
+ self._model_path = model_path or _MODEL_PATH
55
+
56
+ def _ensure_loaded(self) -> bool:
57
+ """Lazy-load model on first use."""
58
+ if self._initialized:
59
+ return self._model is not None
60
+
61
+ self._initialized = True
62
+
63
+ if not self._model_path.exists():
64
+ logger.warning(
65
+ f"wav2vec2 model not found: {self._model_path}. "
66
+ "Run training pipeline first."
67
+ )
68
+ return False
69
+
70
+ try:
71
+ import torch
72
+ from app.training.wav2vec2_classifier import (
73
+ Wav2Vec2MusicClassifier,
74
+ Wav2Vec2Config,
75
+ )
76
+
77
+ self._device = torch.device(
78
+ "cuda" if torch.cuda.is_available() else "cpu"
79
+ )
80
+
81
+ config = Wav2Vec2Config()
82
+ self._model = Wav2Vec2MusicClassifier(config)
83
+ state = torch.load(
84
+ self._model_path,
85
+ map_location=self._device,
86
+ weights_only=True,
87
+ )
88
+ self._model.load_state_dict(state)
89
+ self._model.to(self._device)
90
+ self._model.eval()
91
+
92
+ logger.info(
93
+ f"wav2vec2 model loaded from {self._model_path} "
94
+ f"on {self._device}"
95
+ )
96
+ return True
97
+
98
+ except Exception as e:
99
+ logger.error(f"Failed to load wav2vec2 model: {e}")
100
+ self._model = None
101
+ return False
102
+
103
+ def predict(
104
+ self, source: Union[Path, bytes, io.BytesIO]
105
+ ) -> Wav2Vec2Result:
106
+ """
107
+ Run wav2vec2 inference on audio.
108
+
109
+ Args:
110
+ source: Audio file path, raw bytes, or BytesIO.
111
+
112
+ Returns:
113
+ Wav2Vec2Result with prediction and hidden stats.
114
+ """
115
+ if not self._ensure_loaded():
116
+ return Wav2Vec2Result(
117
+ available=False,
118
+ error="model_not_loaded",
119
+ )
120
+
121
+ try:
122
+ import torch
123
+ import librosa
124
+
125
+ # Load audio at 16kHz
126
+ if isinstance(source, (bytes, io.BytesIO)):
127
+ if isinstance(source, bytes):
128
+ source = io.BytesIO(source)
129
+ y, _ = librosa.load(source, sr=_SAMPLE_RATE, mono=True)
130
+ else:
131
+ y, _ = librosa.load(
132
+ str(source), sr=_SAMPLE_RATE, mono=True
133
+ )
134
+
135
+ # Truncate or pad
136
+ max_samples = int(_MAX_SEC * _SAMPLE_RATE)
137
+ if len(y) > max_samples:
138
+ y = y[:max_samples]
139
+ elif len(y) < _SAMPLE_RATE:
140
+ return Wav2Vec2Result(
141
+ available=False,
142
+ error="audio_too_short",
143
+ )
144
+
145
+ # Inference
146
+ input_tensor = torch.tensor(
147
+ y, dtype=torch.float32
148
+ ).unsqueeze(0).to(self._device)
149
+
150
+ with torch.no_grad():
151
+ logits, hidden = self._model(input_tensor)
152
+ p_ai = float(
153
+ torch.sigmoid(logits).cpu().item()
154
+ )
155
+
156
+ # Hidden state statistics for meta-classifier
157
+ h = hidden.cpu().numpy().flatten()
158
+ from scipy.stats import kurtosis
159
+
160
+ return Wav2Vec2Result(
161
+ available=True,
162
+ p_ai=round(p_ai, 4),
163
+ hidden_mean=float(np.mean(h)),
164
+ hidden_std=float(np.std(h)),
165
+ hidden_max=float(np.max(h)),
166
+ hidden_min=float(np.min(h)),
167
+ hidden_kurtosis=float(kurtosis(h)),
168
+ )
169
+
170
+ except Exception as e:
171
+ logger.warning(f"wav2vec2 prediction failed: {e}")
172
+ return Wav2Vec2Result(
173
+ available=False,
174
+ error=str(e),
175
+ )