OpenSTBench 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openstbench/__init__.py +143 -0
- openstbench/dataset.py +199 -0
- openstbench/emotion_evaluator.py +262 -0
- openstbench/latency/__init__.py +4 -0
- openstbench/latency/agent.py +108 -0
- openstbench/latency/basics.py +57 -0
- openstbench/latency/cli.py +257 -0
- openstbench/latency/instance.py +176 -0
- openstbench/latency/metrics.py +418 -0
- openstbench/latency/utils.py +295 -0
- openstbench/paralinguistic_evaluator.py +1318 -0
- openstbench/speaker_similarity_evaluator.py +147 -0
- openstbench/speech_quality_evaluator.py +171 -0
- openstbench/translation_evaluator.py +257 -0
- openstbench-0.2.0.dist-info/METADATA +181 -0
- openstbench-0.2.0.dist-info/RECORD +18 -0
- openstbench-0.2.0.dist-info/WHEEL +5 -0
- openstbench-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
import torchaudio
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
LabelNormalizer = Optional[Union[Dict[str, Optional[str]], Callable[[str], Optional[str]]]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ParalinguisticSample:
|
|
18
|
+
sample_id: str
|
|
19
|
+
source_audio: str
|
|
20
|
+
source_text: str = ""
|
|
21
|
+
source_label: Optional[str] = None
|
|
22
|
+
source_onset_ms: Optional[float] = None
|
|
23
|
+
source_offset_ms: Optional[float] = None
|
|
24
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class EventPrediction:
|
|
29
|
+
label: Optional[str]
|
|
30
|
+
score: Optional[float] = None
|
|
31
|
+
scores: Dict[str, float] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
34
|
+
return {
|
|
35
|
+
"label": self.label,
|
|
36
|
+
"score": None if self.score is None else float(self.score),
|
|
37
|
+
"scores": {str(key): float(value) for key, value in self.scores.items()},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EventPredictionConfig:
|
|
43
|
+
score_threshold: float = 0.2
|
|
44
|
+
fallback_top1: bool = False
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"score_threshold": float(self.score_threshold),
|
|
49
|
+
"fallback_top1": bool(self.fallback_top1),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class EventLocalization:
|
|
55
|
+
label: Optional[str]
|
|
56
|
+
onset_ms: Optional[float] = None
|
|
57
|
+
offset_ms: Optional[float] = None
|
|
58
|
+
score: Optional[float] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"label": self.label,
|
|
63
|
+
"onset_ms": None if self.onset_ms is None else float(self.onset_ms),
|
|
64
|
+
"offset_ms": None if self.offset_ms is None else float(self.offset_ms),
|
|
65
|
+
"score": None if self.score is None else float(self.score),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class EventLocalizationConfig:
|
|
71
|
+
window_ms: float = 320.0
|
|
72
|
+
hop_ms: float = 40.0
|
|
73
|
+
score_threshold: Optional[float] = None
|
|
74
|
+
fallback_top1: Optional[bool] = None
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
77
|
+
return {
|
|
78
|
+
"window_ms": float(self.window_ms),
|
|
79
|
+
"hop_ms": float(self.hop_ms),
|
|
80
|
+
"score_threshold": None if self.score_threshold is None else float(self.score_threshold),
|
|
81
|
+
"fallback_top1": None if self.fallback_top1 is None else bool(self.fallback_top1),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class EventAlignmentConfig:
|
|
87
|
+
relative_onset_tolerance: float = 0.15
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"relative_onset_tolerance": float(self.relative_onset_tolerance),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BaseAudioEventPredictor(ABC):
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def predict(
|
|
98
|
+
self,
|
|
99
|
+
audio_paths: Sequence[str],
|
|
100
|
+
candidate_labels: Sequence[str],
|
|
101
|
+
) -> List[EventPrediction]:
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BaseAudioEventLocalizer(ABC):
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def localize(
|
|
108
|
+
self,
|
|
109
|
+
audio_paths: Sequence[str],
|
|
110
|
+
labels: Sequence[Optional[str]],
|
|
111
|
+
candidate_labels: Sequence[str],
|
|
112
|
+
) -> List[EventLocalization]:
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ensure_existing_audio(path: str) -> str:
|
|
117
|
+
audio_path = Path(path)
|
|
118
|
+
if not audio_path.exists():
|
|
119
|
+
raise FileNotFoundError(f"Audio not found: {path}")
|
|
120
|
+
return str(audio_path.resolve())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _to_device(device: Optional[str]) -> str:
|
|
124
|
+
if device:
|
|
125
|
+
return device
|
|
126
|
+
return "cuda" if torch.cuda.is_available() else "cpu"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _load_audio_mono(audio_path: str, target_sr: Optional[int] = None) -> Tuple[np.ndarray, int]:
|
|
130
|
+
path = Path(audio_path)
|
|
131
|
+
if not path.exists():
|
|
132
|
+
raise FileNotFoundError(f"Audio not found: {audio_path}")
|
|
133
|
+
|
|
134
|
+
waveform, sr = torchaudio.load(str(path))
|
|
135
|
+
if waveform.shape[0] > 1:
|
|
136
|
+
waveform = waveform.mean(dim=0, keepdim=True)
|
|
137
|
+
if target_sr is not None and sr != target_sr:
|
|
138
|
+
waveform = torchaudio.functional.resample(waveform, sr, target_sr)
|
|
139
|
+
sr = target_sr
|
|
140
|
+
waveform = waveform.squeeze(0).contiguous().float().cpu().numpy()
|
|
141
|
+
return waveform, int(sr)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _get_audio_duration_ms(audio_path: str) -> float:
|
|
145
|
+
try:
|
|
146
|
+
info = torchaudio.info(str(audio_path))
|
|
147
|
+
if info.sample_rate > 0:
|
|
148
|
+
return float(info.num_frames / info.sample_rate * 1000.0)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
waveform, sr = _load_audio_mono(audio_path)
|
|
153
|
+
if sr <= 0:
|
|
154
|
+
return 0.0
|
|
155
|
+
return float(len(waveform) / sr * 1000.0)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _normalize_text_label(label: str) -> str:
|
|
159
|
+
return " ".join(str(label).strip().split())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _apply_label_normalizer(label: Optional[str], label_normalizer: LabelNormalizer) -> Optional[str]:
|
|
163
|
+
if label is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
normalized = _normalize_text_label(label)
|
|
167
|
+
if not normalized:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
if label_normalizer is None:
|
|
171
|
+
mapped = normalized
|
|
172
|
+
elif callable(label_normalizer):
|
|
173
|
+
mapped = label_normalizer(normalized)
|
|
174
|
+
else:
|
|
175
|
+
mapped = label_normalizer.get(normalized, normalized)
|
|
176
|
+
|
|
177
|
+
if mapped is None:
|
|
178
|
+
return None
|
|
179
|
+
mapped_text = _normalize_text_label(str(mapped))
|
|
180
|
+
return mapped_text or None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _coerce_manifest_source_label(raw_label: Any, index: int) -> Optional[str]:
|
|
184
|
+
if raw_label is None:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
if isinstance(raw_label, list):
|
|
188
|
+
cleaned = []
|
|
189
|
+
for item in raw_label:
|
|
190
|
+
label = _normalize_text_label(str(item))
|
|
191
|
+
if label and label not in cleaned:
|
|
192
|
+
cleaned.append(label)
|
|
193
|
+
if not cleaned:
|
|
194
|
+
return None
|
|
195
|
+
if len(cleaned) > 1:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Manifest item {index} has multiple source labels {cleaned}. "
|
|
198
|
+
"This evaluator expects at most one source event label per sample."
|
|
199
|
+
)
|
|
200
|
+
return cleaned[0]
|
|
201
|
+
|
|
202
|
+
label = _normalize_text_label(str(raw_label))
|
|
203
|
+
return label or None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _coerce_optional_float(raw_value: Any, *, name: str, index: int) -> Optional[float]:
|
|
207
|
+
if raw_value is None:
|
|
208
|
+
return None
|
|
209
|
+
if isinstance(raw_value, bool):
|
|
210
|
+
raise ValueError(f"{name} for manifest item {index} must be numeric or null.")
|
|
211
|
+
try:
|
|
212
|
+
value = float(raw_value)
|
|
213
|
+
except (TypeError, ValueError) as exc:
|
|
214
|
+
raise ValueError(f"{name} for manifest item {index} must be numeric or null.") from exc
|
|
215
|
+
if value < 0.0:
|
|
216
|
+
raise ValueError(f"{name} for manifest item {index} must be non-negative.")
|
|
217
|
+
return value
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _normalize_label_batch(
|
|
221
|
+
labels: Optional[Sequence[Optional[str]]],
|
|
222
|
+
*,
|
|
223
|
+
name: str,
|
|
224
|
+
expected_length: int,
|
|
225
|
+
label_normalizer: LabelNormalizer,
|
|
226
|
+
) -> Optional[List[Optional[str]]]:
|
|
227
|
+
if labels is None:
|
|
228
|
+
return None
|
|
229
|
+
if len(labels) != expected_length:
|
|
230
|
+
raise ValueError(f"{name} size mismatch: {len(labels)} vs {expected_length}")
|
|
231
|
+
|
|
232
|
+
normalized: List[Optional[str]] = []
|
|
233
|
+
for index, label in enumerate(labels):
|
|
234
|
+
if label is None:
|
|
235
|
+
normalized.append(None)
|
|
236
|
+
continue
|
|
237
|
+
if not isinstance(label, str):
|
|
238
|
+
raise ValueError(f"{name}[{index}] must be a string or None.")
|
|
239
|
+
normalized.append(_apply_label_normalizer(label, label_normalizer))
|
|
240
|
+
return normalized
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _normalize_float_batch(
|
|
244
|
+
values: Optional[Sequence[Optional[Union[int, float]]]],
|
|
245
|
+
*,
|
|
246
|
+
name: str,
|
|
247
|
+
expected_length: int,
|
|
248
|
+
) -> Optional[List[Optional[float]]]:
|
|
249
|
+
if values is None:
|
|
250
|
+
return None
|
|
251
|
+
if len(values) != expected_length:
|
|
252
|
+
raise ValueError(f"{name} size mismatch: {len(values)} vs {expected_length}")
|
|
253
|
+
|
|
254
|
+
normalized: List[Optional[float]] = []
|
|
255
|
+
for index, value in enumerate(values):
|
|
256
|
+
if value is None:
|
|
257
|
+
normalized.append(None)
|
|
258
|
+
continue
|
|
259
|
+
if isinstance(value, bool):
|
|
260
|
+
raise ValueError(f"{name}[{index}] must be numeric or None.")
|
|
261
|
+
try:
|
|
262
|
+
float_value = float(value)
|
|
263
|
+
except (TypeError, ValueError) as exc:
|
|
264
|
+
raise ValueError(f"{name}[{index}] must be numeric or None.") from exc
|
|
265
|
+
if float_value < 0.0:
|
|
266
|
+
raise ValueError(f"{name}[{index}] must be non-negative.")
|
|
267
|
+
normalized.append(float_value)
|
|
268
|
+
return normalized
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _normalize_candidate_labels(
|
|
272
|
+
candidate_labels: Optional[Sequence[str]],
|
|
273
|
+
*,
|
|
274
|
+
label_normalizer: LabelNormalizer,
|
|
275
|
+
) -> List[str]:
|
|
276
|
+
if candidate_labels is None:
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
normalized: List[str] = []
|
|
280
|
+
seen = set()
|
|
281
|
+
for label in candidate_labels:
|
|
282
|
+
mapped = _apply_label_normalizer(label, label_normalizer)
|
|
283
|
+
if mapped is None or mapped in seen:
|
|
284
|
+
continue
|
|
285
|
+
normalized.append(mapped)
|
|
286
|
+
seen.add(mapped)
|
|
287
|
+
return normalized
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _resolve_candidate_labels(
|
|
291
|
+
*,
|
|
292
|
+
candidate_labels: Optional[Sequence[str]],
|
|
293
|
+
source_labels: Optional[Sequence[Optional[str]]],
|
|
294
|
+
target_labels: Optional[Sequence[Optional[str]]],
|
|
295
|
+
label_normalizer: LabelNormalizer,
|
|
296
|
+
) -> List[str]:
|
|
297
|
+
resolved = _normalize_candidate_labels(candidate_labels, label_normalizer=label_normalizer)
|
|
298
|
+
if resolved:
|
|
299
|
+
return resolved
|
|
300
|
+
|
|
301
|
+
seen = set()
|
|
302
|
+
derived: List[str] = []
|
|
303
|
+
for batch in (source_labels, target_labels):
|
|
304
|
+
if batch is None:
|
|
305
|
+
continue
|
|
306
|
+
for label in batch:
|
|
307
|
+
mapped = _apply_label_normalizer(label, label_normalizer)
|
|
308
|
+
if mapped is None or mapped in seen:
|
|
309
|
+
continue
|
|
310
|
+
derived.append(mapped)
|
|
311
|
+
seen.add(mapped)
|
|
312
|
+
return derived
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _load_data_list(data: Union[List[str], str], name: str) -> List[str]:
|
|
316
|
+
if isinstance(data, str):
|
|
317
|
+
path = Path(data)
|
|
318
|
+
if path.exists() and path.is_dir():
|
|
319
|
+
return load_audio_from_folder(str(path))
|
|
320
|
+
return [ensure_existing_audio(data)]
|
|
321
|
+
if not isinstance(data, list):
|
|
322
|
+
raise ValueError(f"{name} must be a path or a list of paths.")
|
|
323
|
+
return [ensure_existing_audio(str(item)) for item in data]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def load_audio_from_folder(folder_path: str) -> List[str]:
|
|
327
|
+
folder = Path(folder_path)
|
|
328
|
+
if not folder.exists():
|
|
329
|
+
raise FileNotFoundError(f"Folder not found: {folder_path}")
|
|
330
|
+
if not folder.is_dir():
|
|
331
|
+
raise ValueError(f"Expected a directory, got: {folder_path}")
|
|
332
|
+
|
|
333
|
+
audio_files: List[Path] = []
|
|
334
|
+
for extension in (".wav", ".flac", ".mp3", ".ogg", ".m4a"):
|
|
335
|
+
audio_files.extend(folder.glob(f"*{extension}"))
|
|
336
|
+
audio_files = sorted(audio_files, key=lambda item: item.stem)
|
|
337
|
+
if not audio_files:
|
|
338
|
+
raise ValueError(f"No audio files found under: {folder_path}")
|
|
339
|
+
return [str(item.resolve()) for item in audio_files]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _safe_mean(values: Sequence[float]) -> float:
|
|
343
|
+
return round(float(sum(values) / len(values)), 4) if values else 0.0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _compute_single_label_metrics(
|
|
347
|
+
reference_labels: Sequence[Optional[str]],
|
|
348
|
+
predicted_labels: Sequence[Optional[str]],
|
|
349
|
+
*,
|
|
350
|
+
class_labels: Sequence[str],
|
|
351
|
+
) -> Dict[str, Any]:
|
|
352
|
+
evaluated_indices = [index for index, label in enumerate(reference_labels) if label is not None]
|
|
353
|
+
evaluated_reference = [reference_labels[index] for index in evaluated_indices]
|
|
354
|
+
evaluated_prediction = [predicted_labels[index] for index in evaluated_indices]
|
|
355
|
+
|
|
356
|
+
total = len(evaluated_reference)
|
|
357
|
+
correct = sum(
|
|
358
|
+
1
|
|
359
|
+
for reference_label, predicted_label in zip(evaluated_reference, evaluated_prediction)
|
|
360
|
+
if reference_label == predicted_label
|
|
361
|
+
)
|
|
362
|
+
abstained = sum(1 for predicted_label in evaluated_prediction if predicted_label is None)
|
|
363
|
+
|
|
364
|
+
unique_class_labels = list(dict.fromkeys([label for label in class_labels if label]))
|
|
365
|
+
if not unique_class_labels:
|
|
366
|
+
unique_class_labels = sorted(
|
|
367
|
+
{
|
|
368
|
+
label
|
|
369
|
+
for label in evaluated_reference + evaluated_prediction
|
|
370
|
+
if label is not None
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
per_label: Dict[str, Dict[str, float]] = {}
|
|
375
|
+
macro_f1_values: List[float] = []
|
|
376
|
+
macro_recall_values: List[float] = []
|
|
377
|
+
confusion: Dict[str, Dict[str, int]] = {}
|
|
378
|
+
|
|
379
|
+
for reference_label, predicted_label in zip(evaluated_reference, evaluated_prediction):
|
|
380
|
+
predicted_key = predicted_label if predicted_label is not None else "__none__"
|
|
381
|
+
confusion.setdefault(str(reference_label), {})
|
|
382
|
+
confusion[str(reference_label)][predicted_key] = confusion[str(reference_label)].get(predicted_key, 0) + 1
|
|
383
|
+
|
|
384
|
+
for label in unique_class_labels:
|
|
385
|
+
tp = sum(
|
|
386
|
+
1
|
|
387
|
+
for reference_label, predicted_label in zip(evaluated_reference, evaluated_prediction)
|
|
388
|
+
if reference_label == label and predicted_label == label
|
|
389
|
+
)
|
|
390
|
+
fp = sum(
|
|
391
|
+
1
|
|
392
|
+
for reference_label, predicted_label in zip(evaluated_reference, evaluated_prediction)
|
|
393
|
+
if reference_label != label and predicted_label == label
|
|
394
|
+
)
|
|
395
|
+
fn = sum(
|
|
396
|
+
1
|
|
397
|
+
for reference_label, predicted_label in zip(evaluated_reference, evaluated_prediction)
|
|
398
|
+
if reference_label == label and predicted_label != label
|
|
399
|
+
)
|
|
400
|
+
support = sum(1 for reference_label in evaluated_reference if reference_label == label)
|
|
401
|
+
|
|
402
|
+
precision = float(tp / (tp + fp)) if (tp + fp) > 0 else 0.0
|
|
403
|
+
recall = float(tp / (tp + fn)) if (tp + fn) > 0 else 0.0
|
|
404
|
+
f1 = float((2 * tp) / (2 * tp + fp + fn)) if (2 * tp + fp + fn) > 0 else 0.0
|
|
405
|
+
|
|
406
|
+
per_label[label] = {
|
|
407
|
+
"support": int(support),
|
|
408
|
+
"tp": int(tp),
|
|
409
|
+
"fp": int(fp),
|
|
410
|
+
"fn": int(fn),
|
|
411
|
+
"precision": round(precision, 4),
|
|
412
|
+
"recall": round(recall, 4),
|
|
413
|
+
"f1": round(f1, 4),
|
|
414
|
+
}
|
|
415
|
+
macro_f1_values.append(f1)
|
|
416
|
+
macro_recall_values.append(recall)
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"num_evaluated": int(total),
|
|
420
|
+
"num_skipped": int(len(reference_labels) - total),
|
|
421
|
+
"num_correct": int(correct),
|
|
422
|
+
"num_abstained": int(abstained),
|
|
423
|
+
"preservation_rate": round(float(correct / total), 4) if total > 0 else 0.0,
|
|
424
|
+
"macro_f1": _safe_mean(macro_f1_values),
|
|
425
|
+
"macro_recall": _safe_mean(macro_recall_values),
|
|
426
|
+
"per_label": per_label,
|
|
427
|
+
"confusion_matrix": confusion,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _compute_alignment_metrics(
|
|
432
|
+
reference_labels: Sequence[Optional[str]],
|
|
433
|
+
predicted_labels: Sequence[Optional[str]],
|
|
434
|
+
source_onsets_ms: Sequence[Optional[float]],
|
|
435
|
+
target_onsets_ms: Sequence[Optional[float]],
|
|
436
|
+
source_durations_ms: Sequence[float],
|
|
437
|
+
target_durations_ms: Sequence[float],
|
|
438
|
+
*,
|
|
439
|
+
relative_onset_tolerance: float,
|
|
440
|
+
sample_ids: Optional[Sequence[str]] = None,
|
|
441
|
+
) -> Dict[str, Any]:
|
|
442
|
+
evaluated_indices = [
|
|
443
|
+
index
|
|
444
|
+
for index, label in enumerate(reference_labels)
|
|
445
|
+
if label is not None and source_onsets_ms[index] is not None and source_durations_ms[index] > 0.0
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
aligned_success = 0
|
|
449
|
+
missing_target_onset = 0
|
|
450
|
+
conditional_errors: List[float] = []
|
|
451
|
+
sample_records: List[Dict[str, Any]] = []
|
|
452
|
+
|
|
453
|
+
for index in evaluated_indices:
|
|
454
|
+
reference_label = reference_labels[index]
|
|
455
|
+
predicted_label = predicted_labels[index]
|
|
456
|
+
source_onset_ms = source_onsets_ms[index]
|
|
457
|
+
target_onset_ms = target_onsets_ms[index]
|
|
458
|
+
source_duration_ms = float(source_durations_ms[index])
|
|
459
|
+
target_duration_ms = float(target_durations_ms[index])
|
|
460
|
+
|
|
461
|
+
assert reference_label is not None
|
|
462
|
+
assert source_onset_ms is not None
|
|
463
|
+
|
|
464
|
+
source_relative_onset = float(source_onset_ms / source_duration_ms) if source_duration_ms > 0.0 else None
|
|
465
|
+
target_relative_onset = (
|
|
466
|
+
float(target_onset_ms / target_duration_ms)
|
|
467
|
+
if target_onset_ms is not None and target_duration_ms > 0.0
|
|
468
|
+
else None
|
|
469
|
+
)
|
|
470
|
+
label_correct = predicted_label is not None and predicted_label == reference_label
|
|
471
|
+
|
|
472
|
+
relative_onset_error: Optional[float] = None
|
|
473
|
+
aligned = False
|
|
474
|
+
if label_correct and target_relative_onset is not None and source_relative_onset is not None:
|
|
475
|
+
relative_onset_error = abs(source_relative_onset - target_relative_onset)
|
|
476
|
+
conditional_errors.append(relative_onset_error)
|
|
477
|
+
aligned = relative_onset_error <= relative_onset_tolerance
|
|
478
|
+
elif label_correct and target_onset_ms is None:
|
|
479
|
+
missing_target_onset += 1
|
|
480
|
+
|
|
481
|
+
if aligned:
|
|
482
|
+
aligned_success += 1
|
|
483
|
+
|
|
484
|
+
sample_records.append(
|
|
485
|
+
{
|
|
486
|
+
"sample_index": int(index),
|
|
487
|
+
"sample_id": sample_ids[index] if sample_ids is not None else str(index),
|
|
488
|
+
"reference_label": reference_label,
|
|
489
|
+
"predicted_label": predicted_label,
|
|
490
|
+
"source_onset_ms": float(source_onset_ms),
|
|
491
|
+
"target_onset_ms": None if target_onset_ms is None else float(target_onset_ms),
|
|
492
|
+
"source_duration_ms": round(source_duration_ms, 4),
|
|
493
|
+
"target_duration_ms": round(target_duration_ms, 4),
|
|
494
|
+
"source_relative_onset": None if source_relative_onset is None else round(source_relative_onset, 4),
|
|
495
|
+
"target_relative_onset": None if target_relative_onset is None else round(target_relative_onset, 4),
|
|
496
|
+
"relative_onset_error": None if relative_onset_error is None else round(relative_onset_error, 4),
|
|
497
|
+
"label_correct": bool(label_correct),
|
|
498
|
+
"aligned": bool(aligned),
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
total = len(evaluated_indices)
|
|
503
|
+
return {
|
|
504
|
+
"num_evaluated": int(total),
|
|
505
|
+
"num_skipped": int(len(reference_labels) - total),
|
|
506
|
+
"num_aligned": int(aligned_success),
|
|
507
|
+
"num_missing_target_onset": int(missing_target_onset),
|
|
508
|
+
"num_conditionally_evaluated": int(len(conditional_errors)),
|
|
509
|
+
"aligned_preservation_rate": round(float(aligned_success / total), 4) if total > 0 else 0.0,
|
|
510
|
+
"conditional_relative_onset_error": _safe_mean(conditional_errors),
|
|
511
|
+
"relative_onset_tolerance": round(float(relative_onset_tolerance), 4),
|
|
512
|
+
"samples": sample_records,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class ClapAudioEventPredictor(BaseAudioEventPredictor):
|
|
517
|
+
PROMPT_TEMPLATES = (
|
|
518
|
+
"{label}",
|
|
519
|
+
"the sound of {label}",
|
|
520
|
+
"an audio recording of {label}",
|
|
521
|
+
"a person producing {label}",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def __init__(
|
|
525
|
+
self,
|
|
526
|
+
*,
|
|
527
|
+
model_path: str = "laion/clap-htsat-fused",
|
|
528
|
+
config: Optional[EventPredictionConfig] = None,
|
|
529
|
+
device: Optional[str] = None,
|
|
530
|
+
) -> None:
|
|
531
|
+
self.model_path = model_path
|
|
532
|
+
self.config = config or EventPredictionConfig()
|
|
533
|
+
self.device = _to_device(device)
|
|
534
|
+
self._processor = None
|
|
535
|
+
self._model = None
|
|
536
|
+
self._text_embedding_cache: Dict[str, np.ndarray] = {}
|
|
537
|
+
|
|
538
|
+
def _load_model(self) -> None:
|
|
539
|
+
if self._model is not None and self._processor is not None:
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
from transformers import ClapModel, ClapProcessor
|
|
544
|
+
except ImportError as exc:
|
|
545
|
+
raise RuntimeError("CLAP-based event prediction requires `transformers`.") from exc
|
|
546
|
+
|
|
547
|
+
self._processor = ClapProcessor.from_pretrained(self.model_path)
|
|
548
|
+
self._model = ClapModel.from_pretrained(self.model_path).to(self.device).eval()
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
def _normalize_embedding(embedding: np.ndarray) -> np.ndarray:
|
|
552
|
+
norm = float(np.linalg.norm(embedding))
|
|
553
|
+
if norm <= 0.0:
|
|
554
|
+
return embedding
|
|
555
|
+
return embedding / norm
|
|
556
|
+
|
|
557
|
+
def _build_prompts(self, candidate_labels: Sequence[str]) -> List[Tuple[str, str]]:
|
|
558
|
+
prompt_records: List[Tuple[str, str]] = []
|
|
559
|
+
for label in candidate_labels:
|
|
560
|
+
for template in self.PROMPT_TEMPLATES:
|
|
561
|
+
prompt_records.append((label, template.format(label=label)))
|
|
562
|
+
return prompt_records
|
|
563
|
+
|
|
564
|
+
def _extract_audio_embeddings_from_waveforms(
|
|
565
|
+
self,
|
|
566
|
+
waveforms: Sequence[np.ndarray],
|
|
567
|
+
*,
|
|
568
|
+
sampling_rate: int,
|
|
569
|
+
) -> List[np.ndarray]:
|
|
570
|
+
self._load_model()
|
|
571
|
+
assert self._processor is not None
|
|
572
|
+
assert self._model is not None
|
|
573
|
+
|
|
574
|
+
embeddings: List[np.ndarray] = []
|
|
575
|
+
for waveform in waveforms:
|
|
576
|
+
inputs = self._processor(audio=waveform, sampling_rate=sampling_rate, return_tensors="pt")
|
|
577
|
+
inputs = {key: value.to(self.device) for key, value in inputs.items()}
|
|
578
|
+
with torch.no_grad():
|
|
579
|
+
features = self._model.get_audio_features(**inputs)
|
|
580
|
+
embeddings.append(features[0].detach().cpu().numpy())
|
|
581
|
+
return embeddings
|
|
582
|
+
|
|
583
|
+
def _extract_audio_embeddings(self, audio_paths: Sequence[str]) -> List[np.ndarray]:
|
|
584
|
+
embeddings: List[np.ndarray] = []
|
|
585
|
+
for audio_path in tqdm(audio_paths, desc="Extracting CLAP event embeddings", unit="file"):
|
|
586
|
+
waveform, sr = _load_audio_mono(str(audio_path), target_sr=48000)
|
|
587
|
+
embeddings.extend(self._extract_audio_embeddings_from_waveforms([waveform], sampling_rate=sr))
|
|
588
|
+
return embeddings
|
|
589
|
+
|
|
590
|
+
def _extract_text_embeddings(self, texts: Sequence[str]) -> List[np.ndarray]:
|
|
591
|
+
self._load_model()
|
|
592
|
+
assert self._processor is not None
|
|
593
|
+
assert self._model is not None
|
|
594
|
+
|
|
595
|
+
embeddings: List[np.ndarray] = []
|
|
596
|
+
for text in texts:
|
|
597
|
+
cached = self._text_embedding_cache.get(text)
|
|
598
|
+
if cached is not None:
|
|
599
|
+
embeddings.append(cached)
|
|
600
|
+
continue
|
|
601
|
+
inputs = self._processor(text=[str(text)], return_tensors="pt", padding=True)
|
|
602
|
+
inputs = {key: value.to(self.device) for key, value in inputs.items()}
|
|
603
|
+
with torch.no_grad():
|
|
604
|
+
features = self._model.get_text_features(**inputs)
|
|
605
|
+
embedding = features[0].detach().cpu().numpy()
|
|
606
|
+
self._text_embedding_cache[text] = embedding
|
|
607
|
+
embeddings.append(embedding)
|
|
608
|
+
return embeddings
|
|
609
|
+
|
|
610
|
+
def _score_audio_embeddings(
|
|
611
|
+
self,
|
|
612
|
+
audio_embeddings: Sequence[np.ndarray],
|
|
613
|
+
candidate_labels: Sequence[str],
|
|
614
|
+
) -> List[Dict[str, float]]:
|
|
615
|
+
normalized_candidate_labels = [
|
|
616
|
+
_normalize_text_label(label)
|
|
617
|
+
for label in candidate_labels
|
|
618
|
+
if _normalize_text_label(label)
|
|
619
|
+
]
|
|
620
|
+
unique_candidate_labels = list(dict.fromkeys(normalized_candidate_labels))
|
|
621
|
+
if not unique_candidate_labels:
|
|
622
|
+
return [{} for _ in audio_embeddings]
|
|
623
|
+
|
|
624
|
+
prompt_records = self._build_prompts(unique_candidate_labels)
|
|
625
|
+
normalized_audio_embeddings = [self._normalize_embedding(item) for item in audio_embeddings]
|
|
626
|
+
normalized_text_embeddings = [
|
|
627
|
+
self._normalize_embedding(item)
|
|
628
|
+
for item in self._extract_text_embeddings([prompt for _, prompt in prompt_records])
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
scores_per_audio: List[Dict[str, float]] = []
|
|
632
|
+
for audio_embedding in normalized_audio_embeddings:
|
|
633
|
+
label_scores: Dict[str, float] = {}
|
|
634
|
+
for (label, _prompt), text_embedding in zip(prompt_records, normalized_text_embeddings):
|
|
635
|
+
score = float(np.dot(audio_embedding, text_embedding))
|
|
636
|
+
if label not in label_scores or score > label_scores[label]:
|
|
637
|
+
label_scores[label] = score
|
|
638
|
+
scores_per_audio.append(label_scores)
|
|
639
|
+
return scores_per_audio
|
|
640
|
+
|
|
641
|
+
def score_audio_paths(
|
|
642
|
+
self,
|
|
643
|
+
audio_paths: Sequence[str],
|
|
644
|
+
candidate_labels: Sequence[str],
|
|
645
|
+
) -> List[Dict[str, float]]:
|
|
646
|
+
audio_embeddings = self._extract_audio_embeddings(audio_paths)
|
|
647
|
+
return self._score_audio_embeddings(audio_embeddings, candidate_labels)
|
|
648
|
+
|
|
649
|
+
def score_waveforms(
|
|
650
|
+
self,
|
|
651
|
+
waveforms: Sequence[np.ndarray],
|
|
652
|
+
*,
|
|
653
|
+
sampling_rate: int,
|
|
654
|
+
candidate_labels: Sequence[str],
|
|
655
|
+
) -> List[Dict[str, float]]:
|
|
656
|
+
audio_embeddings = self._extract_audio_embeddings_from_waveforms(waveforms, sampling_rate=sampling_rate)
|
|
657
|
+
return self._score_audio_embeddings(audio_embeddings, candidate_labels)
|
|
658
|
+
|
|
659
|
+
def predict(
|
|
660
|
+
self,
|
|
661
|
+
audio_paths: Sequence[str],
|
|
662
|
+
candidate_labels: Sequence[str],
|
|
663
|
+
) -> List[EventPrediction]:
|
|
664
|
+
label_scores_per_audio = self.score_audio_paths(audio_paths, candidate_labels)
|
|
665
|
+
|
|
666
|
+
predictions: List[EventPrediction] = []
|
|
667
|
+
threshold = float(self.config.score_threshold)
|
|
668
|
+
fallback_top1 = bool(self.config.fallback_top1)
|
|
669
|
+
|
|
670
|
+
for label_scores in label_scores_per_audio:
|
|
671
|
+
if not label_scores:
|
|
672
|
+
predictions.append(EventPrediction(label=None, score=None, scores={}))
|
|
673
|
+
continue
|
|
674
|
+
|
|
675
|
+
top_label = max(label_scores.items(), key=lambda item: item[1])[0]
|
|
676
|
+
top_score = float(label_scores[top_label])
|
|
677
|
+
predicted_label = top_label if top_score >= threshold or fallback_top1 else None
|
|
678
|
+
|
|
679
|
+
predictions.append(
|
|
680
|
+
EventPrediction(
|
|
681
|
+
label=predicted_label,
|
|
682
|
+
score=top_score,
|
|
683
|
+
scores={label: round(score, 4) for label, score in sorted(label_scores.items())},
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
return predictions
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class ClapSlidingWindowEventLocalizer(BaseAudioEventLocalizer):
|
|
691
|
+
def __init__(
|
|
692
|
+
self,
|
|
693
|
+
*,
|
|
694
|
+
model_path: str = "laion/clap-htsat-fused",
|
|
695
|
+
prediction_config: Optional[EventPredictionConfig] = None,
|
|
696
|
+
localization_config: Optional[EventLocalizationConfig] = None,
|
|
697
|
+
device: Optional[str] = None,
|
|
698
|
+
) -> None:
|
|
699
|
+
self.predictor = ClapAudioEventPredictor(
|
|
700
|
+
model_path=model_path,
|
|
701
|
+
config=prediction_config,
|
|
702
|
+
device=device,
|
|
703
|
+
)
|
|
704
|
+
self.prediction_config = prediction_config or EventPredictionConfig()
|
|
705
|
+
self.localization_config = localization_config or EventLocalizationConfig()
|
|
706
|
+
|
|
707
|
+
def _build_windows(self, waveform: np.ndarray, sampling_rate: int) -> Tuple[List[np.ndarray], List[int]]:
|
|
708
|
+
window_samples = max(1, int(round(self.localization_config.window_ms / 1000.0 * sampling_rate)))
|
|
709
|
+
hop_samples = max(1, int(round(self.localization_config.hop_ms / 1000.0 * sampling_rate)))
|
|
710
|
+
total_samples = int(len(waveform))
|
|
711
|
+
|
|
712
|
+
if total_samples <= window_samples:
|
|
713
|
+
return [waveform], [0]
|
|
714
|
+
|
|
715
|
+
starts: List[int] = list(range(0, max(total_samples - window_samples + 1, 1), hop_samples))
|
|
716
|
+
last_start = max(total_samples - window_samples, 0)
|
|
717
|
+
if not starts or starts[-1] != last_start:
|
|
718
|
+
starts.append(last_start)
|
|
719
|
+
|
|
720
|
+
segments = [waveform[start : start + window_samples] for start in starts]
|
|
721
|
+
return segments, starts
|
|
722
|
+
|
|
723
|
+
def localize(
|
|
724
|
+
self,
|
|
725
|
+
audio_paths: Sequence[str],
|
|
726
|
+
labels: Sequence[Optional[str]],
|
|
727
|
+
candidate_labels: Sequence[str],
|
|
728
|
+
) -> List[EventLocalization]:
|
|
729
|
+
if len(audio_paths) != len(labels):
|
|
730
|
+
raise ValueError(f"audio_paths size mismatch: {len(audio_paths)} vs labels {len(labels)}")
|
|
731
|
+
|
|
732
|
+
normalized_candidate_labels = {
|
|
733
|
+
_normalize_text_label(label)
|
|
734
|
+
for label in candidate_labels
|
|
735
|
+
if _normalize_text_label(label)
|
|
736
|
+
}
|
|
737
|
+
threshold = (
|
|
738
|
+
float(self.localization_config.score_threshold)
|
|
739
|
+
if self.localization_config.score_threshold is not None
|
|
740
|
+
else float(self.prediction_config.score_threshold)
|
|
741
|
+
)
|
|
742
|
+
fallback_top1 = (
|
|
743
|
+
bool(self.localization_config.fallback_top1)
|
|
744
|
+
if self.localization_config.fallback_top1 is not None
|
|
745
|
+
else bool(self.prediction_config.fallback_top1)
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
localizations: List[EventLocalization] = []
|
|
749
|
+
for audio_path, label in tqdm(
|
|
750
|
+
list(zip(audio_paths, labels)),
|
|
751
|
+
desc="Localizing acoustic events",
|
|
752
|
+
unit="file",
|
|
753
|
+
):
|
|
754
|
+
normalized_label = _normalize_text_label(label) if isinstance(label, str) and label else None
|
|
755
|
+
if normalized_label is None or (
|
|
756
|
+
normalized_candidate_labels and normalized_label not in normalized_candidate_labels
|
|
757
|
+
):
|
|
758
|
+
localizations.append(EventLocalization(label=normalized_label))
|
|
759
|
+
continue
|
|
760
|
+
|
|
761
|
+
waveform, sampling_rate = _load_audio_mono(str(audio_path), target_sr=48000)
|
|
762
|
+
if waveform.size == 0:
|
|
763
|
+
localizations.append(EventLocalization(label=normalized_label))
|
|
764
|
+
continue
|
|
765
|
+
|
|
766
|
+
segments, starts = self._build_windows(waveform, sampling_rate)
|
|
767
|
+
label_scores_per_window = self.predictor.score_waveforms(
|
|
768
|
+
segments,
|
|
769
|
+
sampling_rate=sampling_rate,
|
|
770
|
+
candidate_labels=[normalized_label],
|
|
771
|
+
)
|
|
772
|
+
best_start = 0
|
|
773
|
+
best_score = None
|
|
774
|
+
best_index = -1
|
|
775
|
+
for index, label_scores in enumerate(label_scores_per_window):
|
|
776
|
+
score = label_scores.get(normalized_label)
|
|
777
|
+
if score is None:
|
|
778
|
+
continue
|
|
779
|
+
if best_score is None or score > best_score:
|
|
780
|
+
best_score = float(score)
|
|
781
|
+
best_start = starts[index]
|
|
782
|
+
best_index = index
|
|
783
|
+
|
|
784
|
+
if best_score is None or (best_score < threshold and not fallback_top1):
|
|
785
|
+
localizations.append(EventLocalization(label=normalized_label, score=best_score))
|
|
786
|
+
continue
|
|
787
|
+
|
|
788
|
+
window_samples = len(segments[best_index]) if best_index >= 0 else 0
|
|
789
|
+
onset_ms = float(best_start / sampling_rate * 1000.0)
|
|
790
|
+
offset_ms = float(min(best_start + window_samples, len(waveform)) / sampling_rate * 1000.0)
|
|
791
|
+
localizations.append(
|
|
792
|
+
EventLocalization(
|
|
793
|
+
label=normalized_label,
|
|
794
|
+
onset_ms=onset_ms,
|
|
795
|
+
offset_ms=offset_ms,
|
|
796
|
+
score=best_score,
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
return localizations
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
class ParalinguisticEvaluator:
|
|
804
|
+
DEFAULT_CLAP_MODEL = "laion/clap-htsat-fused"
|
|
805
|
+
|
|
806
|
+
def __init__(
|
|
807
|
+
self,
|
|
808
|
+
use_continuous_fidelity: bool = True,
|
|
809
|
+
use_event_preservation: bool = True,
|
|
810
|
+
use_event_alignment: bool = True,
|
|
811
|
+
clap_model_path: Optional[str] = None,
|
|
812
|
+
event_predictor: Optional[BaseAudioEventPredictor] = None,
|
|
813
|
+
event_localizer: Optional[BaseAudioEventLocalizer] = None,
|
|
814
|
+
event_prediction_config: Optional[Dict[str, Any]] = None,
|
|
815
|
+
event_localization_config: Optional[Dict[str, Any]] = None,
|
|
816
|
+
event_alignment_config: Optional[Dict[str, Any]] = None,
|
|
817
|
+
device: Optional[str] = None,
|
|
818
|
+
**_: Any,
|
|
819
|
+
) -> None:
|
|
820
|
+
self.device = _to_device(device)
|
|
821
|
+
self.use_continuous_fidelity = bool(use_continuous_fidelity)
|
|
822
|
+
self.use_event_preservation = bool(use_event_preservation)
|
|
823
|
+
self.use_event_alignment = bool(use_event_alignment)
|
|
824
|
+
self.clap_model_path = clap_model_path or self.DEFAULT_CLAP_MODEL
|
|
825
|
+
self.event_prediction_config = EventPredictionConfig(**(event_prediction_config or {}))
|
|
826
|
+
self.event_localization_config = EventLocalizationConfig(**(event_localization_config or {}))
|
|
827
|
+
self.event_alignment_config = EventAlignmentConfig(**(event_alignment_config or {}))
|
|
828
|
+
self.event_predictor = event_predictor
|
|
829
|
+
self.event_localizer = event_localizer
|
|
830
|
+
|
|
831
|
+
self._clap_processor = None
|
|
832
|
+
self._clap_model = None
|
|
833
|
+
self._default_predictor: Optional[ClapAudioEventPredictor] = None
|
|
834
|
+
self._default_localizer: Optional[ClapSlidingWindowEventLocalizer] = None
|
|
835
|
+
|
|
836
|
+
def _load_clap(self) -> None:
|
|
837
|
+
if self._clap_model is not None and self._clap_processor is not None:
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
from transformers import ClapModel, ClapProcessor
|
|
842
|
+
except ImportError as exc:
|
|
843
|
+
raise RuntimeError("Paralinguistic_Fidelity_Cosine requires `transformers` to load CLAP.") from exc
|
|
844
|
+
|
|
845
|
+
self._clap_processor = ClapProcessor.from_pretrained(self.clap_model_path)
|
|
846
|
+
self._clap_model = ClapModel.from_pretrained(self.clap_model_path).to(self.device).eval()
|
|
847
|
+
|
|
848
|
+
def _extract_clap_embeddings(self, audio_paths: Sequence[str]) -> List[np.ndarray]:
|
|
849
|
+
self._load_clap()
|
|
850
|
+
assert self._clap_processor is not None
|
|
851
|
+
assert self._clap_model is not None
|
|
852
|
+
|
|
853
|
+
embeddings: List[np.ndarray] = []
|
|
854
|
+
for audio_path in tqdm(audio_paths, desc="Extracting CLAP embeddings", unit="file"):
|
|
855
|
+
waveform, sr = _load_audio_mono(str(audio_path), target_sr=48000)
|
|
856
|
+
inputs = self._clap_processor(audio=waveform, sampling_rate=sr, return_tensors="pt")
|
|
857
|
+
inputs = {key: value.to(self.device) for key, value in inputs.items()}
|
|
858
|
+
with torch.no_grad():
|
|
859
|
+
features = self._clap_model.get_audio_features(**inputs)
|
|
860
|
+
embeddings.append(features[0].detach().cpu().numpy())
|
|
861
|
+
return embeddings
|
|
862
|
+
|
|
863
|
+
@staticmethod
|
|
864
|
+
def _average_cosine(source_embeddings: Sequence[np.ndarray], target_embeddings: Sequence[np.ndarray]) -> float:
|
|
865
|
+
total = 0.0
|
|
866
|
+
count = 0
|
|
867
|
+
for source_embedding, target_embedding in zip(source_embeddings, target_embeddings):
|
|
868
|
+
source_norm = float(np.linalg.norm(source_embedding))
|
|
869
|
+
target_norm = float(np.linalg.norm(target_embedding))
|
|
870
|
+
if source_norm <= 0.0 or target_norm <= 0.0:
|
|
871
|
+
continue
|
|
872
|
+
total += float(np.dot(source_embedding, target_embedding) / (source_norm * target_norm))
|
|
873
|
+
count += 1
|
|
874
|
+
return round(total / count, 4) if count > 0 else 0.0
|
|
875
|
+
|
|
876
|
+
def _get_event_predictor(self) -> BaseAudioEventPredictor:
|
|
877
|
+
if self.event_predictor is not None:
|
|
878
|
+
return self.event_predictor
|
|
879
|
+
if self._default_predictor is None:
|
|
880
|
+
self._default_predictor = ClapAudioEventPredictor(
|
|
881
|
+
model_path=self.clap_model_path,
|
|
882
|
+
config=self.event_prediction_config,
|
|
883
|
+
device=self.device,
|
|
884
|
+
)
|
|
885
|
+
return self._default_predictor
|
|
886
|
+
|
|
887
|
+
def _get_event_localizer(self) -> BaseAudioEventLocalizer:
|
|
888
|
+
if self.event_localizer is not None:
|
|
889
|
+
return self.event_localizer
|
|
890
|
+
if self._default_localizer is None:
|
|
891
|
+
self._default_localizer = ClapSlidingWindowEventLocalizer(
|
|
892
|
+
model_path=self.clap_model_path,
|
|
893
|
+
prediction_config=self.event_prediction_config,
|
|
894
|
+
localization_config=self.event_localization_config,
|
|
895
|
+
device=self.device,
|
|
896
|
+
)
|
|
897
|
+
return self._default_localizer
|
|
898
|
+
|
|
899
|
+
def _predict_labels(
|
|
900
|
+
self,
|
|
901
|
+
*,
|
|
902
|
+
audio_paths: Sequence[str],
|
|
903
|
+
candidate_labels: Sequence[str],
|
|
904
|
+
label_normalizer: LabelNormalizer,
|
|
905
|
+
) -> List[EventPrediction]:
|
|
906
|
+
predictor = self._get_event_predictor()
|
|
907
|
+
if not hasattr(predictor, "predict"):
|
|
908
|
+
raise TypeError("event_predictor must expose a `predict(audio_paths, candidate_labels)` method.")
|
|
909
|
+
|
|
910
|
+
predictions = predictor.predict(audio_paths, candidate_labels)
|
|
911
|
+
if len(predictions) != len(audio_paths):
|
|
912
|
+
raise ValueError(
|
|
913
|
+
"event_predictor returned a different number of predictions than audio inputs: "
|
|
914
|
+
f"{len(predictions)} vs {len(audio_paths)}"
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
normalized_predictions: List[EventPrediction] = []
|
|
918
|
+
for prediction in predictions:
|
|
919
|
+
normalized_predictions.append(
|
|
920
|
+
EventPrediction(
|
|
921
|
+
label=_apply_label_normalizer(prediction.label, label_normalizer),
|
|
922
|
+
score=prediction.score,
|
|
923
|
+
scores={
|
|
924
|
+
_apply_label_normalizer(label, label_normalizer) or label: float(score)
|
|
925
|
+
for label, score in prediction.scores.items()
|
|
926
|
+
},
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
return normalized_predictions
|
|
930
|
+
|
|
931
|
+
def _localize_events(
|
|
932
|
+
self,
|
|
933
|
+
*,
|
|
934
|
+
audio_paths: Sequence[str],
|
|
935
|
+
labels: Sequence[Optional[str]],
|
|
936
|
+
candidate_labels: Sequence[str],
|
|
937
|
+
label_normalizer: LabelNormalizer,
|
|
938
|
+
) -> List[EventLocalization]:
|
|
939
|
+
localizer = self._get_event_localizer()
|
|
940
|
+
if not hasattr(localizer, "localize"):
|
|
941
|
+
raise TypeError("event_localizer must expose a `localize(audio_paths, labels, candidate_labels)` method.")
|
|
942
|
+
|
|
943
|
+
localizations = localizer.localize(audio_paths, labels, candidate_labels)
|
|
944
|
+
if len(localizations) != len(audio_paths):
|
|
945
|
+
raise ValueError(
|
|
946
|
+
"event_localizer returned a different number of localizations than audio inputs: "
|
|
947
|
+
f"{len(localizations)} vs {len(audio_paths)}"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
normalized_localizations: List[EventLocalization] = []
|
|
951
|
+
for localization in localizations:
|
|
952
|
+
normalized_localizations.append(
|
|
953
|
+
EventLocalization(
|
|
954
|
+
label=_apply_label_normalizer(localization.label, label_normalizer),
|
|
955
|
+
onset_ms=localization.onset_ms,
|
|
956
|
+
offset_ms=localization.offset_ms,
|
|
957
|
+
score=localization.score,
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
return normalized_localizations
|
|
961
|
+
|
|
962
|
+
def evaluate_all(
|
|
963
|
+
self,
|
|
964
|
+
source_audio: Union[List[str], str],
|
|
965
|
+
target_audio: Union[List[str], str],
|
|
966
|
+
*,
|
|
967
|
+
source_labels: Optional[Sequence[Optional[str]]] = None,
|
|
968
|
+
target_labels: Optional[Sequence[Optional[str]]] = None,
|
|
969
|
+
source_onsets_ms: Optional[Sequence[Optional[Union[int, float]]]] = None,
|
|
970
|
+
target_onsets_ms: Optional[Sequence[Optional[Union[int, float]]]] = None,
|
|
971
|
+
candidate_labels: Optional[Sequence[str]] = None,
|
|
972
|
+
label_normalizer: LabelNormalizer = None,
|
|
973
|
+
sample_ids: Optional[Sequence[str]] = None,
|
|
974
|
+
verbose: bool = True,
|
|
975
|
+
return_diagnostics: bool = False,
|
|
976
|
+
) -> Union[Tuple[Dict[str, float], Dict[str, Any]], Dict[str, float]]:
|
|
977
|
+
source_audio_paths = _load_data_list(source_audio, "Source Audio")
|
|
978
|
+
target_audio_paths = _load_data_list(target_audio, "Target Audio")
|
|
979
|
+
|
|
980
|
+
if len(source_audio_paths) != len(target_audio_paths):
|
|
981
|
+
raise ValueError(
|
|
982
|
+
f"Source and target size mismatch: {len(source_audio_paths)} vs {len(target_audio_paths)}"
|
|
983
|
+
)
|
|
984
|
+
if not source_audio_paths:
|
|
985
|
+
raise ValueError("No samples found for paralinguistic evaluation.")
|
|
986
|
+
|
|
987
|
+
num_samples = len(source_audio_paths)
|
|
988
|
+
normalized_source_labels = _normalize_label_batch(
|
|
989
|
+
source_labels,
|
|
990
|
+
name="source_labels",
|
|
991
|
+
expected_length=num_samples,
|
|
992
|
+
label_normalizer=label_normalizer,
|
|
993
|
+
)
|
|
994
|
+
normalized_target_labels = _normalize_label_batch(
|
|
995
|
+
target_labels,
|
|
996
|
+
name="target_labels",
|
|
997
|
+
expected_length=num_samples,
|
|
998
|
+
label_normalizer=label_normalizer,
|
|
999
|
+
)
|
|
1000
|
+
normalized_source_onsets_ms = _normalize_float_batch(
|
|
1001
|
+
source_onsets_ms,
|
|
1002
|
+
name="source_onsets_ms",
|
|
1003
|
+
expected_length=num_samples,
|
|
1004
|
+
)
|
|
1005
|
+
normalized_target_onsets_ms = _normalize_float_batch(
|
|
1006
|
+
target_onsets_ms,
|
|
1007
|
+
name="target_onsets_ms",
|
|
1008
|
+
expected_length=num_samples,
|
|
1009
|
+
)
|
|
1010
|
+
resolved_candidate_labels = _resolve_candidate_labels(
|
|
1011
|
+
candidate_labels=candidate_labels,
|
|
1012
|
+
source_labels=normalized_source_labels,
|
|
1013
|
+
target_labels=normalized_target_labels,
|
|
1014
|
+
label_normalizer=label_normalizer,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
results: Dict[str, float] = {}
|
|
1018
|
+
diagnostics: Dict[str, Any] = {
|
|
1019
|
+
"num_samples": num_samples,
|
|
1020
|
+
"device": self.device,
|
|
1021
|
+
"clap_model_path": self.clap_model_path,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if self.use_continuous_fidelity:
|
|
1025
|
+
source_embeddings = self._extract_clap_embeddings(source_audio_paths)
|
|
1026
|
+
target_embeddings = self._extract_clap_embeddings(target_audio_paths)
|
|
1027
|
+
cosine = self._average_cosine(source_embeddings, target_embeddings)
|
|
1028
|
+
results["Paralinguistic_Fidelity_Cosine"] = cosine
|
|
1029
|
+
diagnostics["continuous_fidelity"] = {
|
|
1030
|
+
"metric": "Paralinguistic_Fidelity_Cosine",
|
|
1031
|
+
"num_embeddings": len(source_embeddings),
|
|
1032
|
+
"score": cosine,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
source_prediction_records: Optional[List[EventPrediction]] = None
|
|
1036
|
+
target_prediction_records: Optional[List[EventPrediction]] = None
|
|
1037
|
+
source_label_origin = "provided_source_labels"
|
|
1038
|
+
target_label_origin = "provided_target_labels"
|
|
1039
|
+
|
|
1040
|
+
if self.use_event_preservation or self.use_event_alignment:
|
|
1041
|
+
if (normalized_source_labels is None or normalized_target_labels is None) and not resolved_candidate_labels:
|
|
1042
|
+
raise ValueError(
|
|
1043
|
+
"Event preservation or alignment requires candidate_labels when either source_labels or "
|
|
1044
|
+
"target_labels are not provided."
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
if normalized_source_labels is None:
|
|
1048
|
+
source_prediction_records = self._predict_labels(
|
|
1049
|
+
audio_paths=source_audio_paths,
|
|
1050
|
+
candidate_labels=resolved_candidate_labels,
|
|
1051
|
+
label_normalizer=label_normalizer,
|
|
1052
|
+
)
|
|
1053
|
+
normalized_source_labels = [prediction.label for prediction in source_prediction_records]
|
|
1054
|
+
source_label_origin = "predicted_source_labels"
|
|
1055
|
+
|
|
1056
|
+
if normalized_target_labels is None:
|
|
1057
|
+
target_prediction_records = self._predict_labels(
|
|
1058
|
+
audio_paths=target_audio_paths,
|
|
1059
|
+
candidate_labels=resolved_candidate_labels,
|
|
1060
|
+
label_normalizer=label_normalizer,
|
|
1061
|
+
)
|
|
1062
|
+
normalized_target_labels = [prediction.label for prediction in target_prediction_records]
|
|
1063
|
+
target_label_origin = "predicted_target_labels"
|
|
1064
|
+
|
|
1065
|
+
if self.use_event_preservation:
|
|
1066
|
+
assert normalized_source_labels is not None
|
|
1067
|
+
assert normalized_target_labels is not None
|
|
1068
|
+
|
|
1069
|
+
metric_payload = _compute_single_label_metrics(
|
|
1070
|
+
normalized_source_labels,
|
|
1071
|
+
normalized_target_labels,
|
|
1072
|
+
class_labels=resolved_candidate_labels,
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
use_predicted_reference = source_labels is None
|
|
1076
|
+
if use_predicted_reference:
|
|
1077
|
+
rate_name = "Predicted_Event_Consistency_Rate"
|
|
1078
|
+
macro_f1_name = "Predicted_Event_Consistency_Macro_F1"
|
|
1079
|
+
macro_recall_name = "Predicted_Event_Consistency_Macro_Recall"
|
|
1080
|
+
else:
|
|
1081
|
+
rate_name = "Acoustic_Event_Preservation_Rate"
|
|
1082
|
+
macro_f1_name = "Acoustic_Event_Preservation_Macro_F1"
|
|
1083
|
+
macro_recall_name = "Acoustic_Event_Preservation_Macro_Recall"
|
|
1084
|
+
|
|
1085
|
+
results[rate_name] = metric_payload["preservation_rate"]
|
|
1086
|
+
results[macro_f1_name] = metric_payload["macro_f1"]
|
|
1087
|
+
results[macro_recall_name] = metric_payload["macro_recall"]
|
|
1088
|
+
|
|
1089
|
+
diagnostics["event_preservation"] = {
|
|
1090
|
+
"candidate_labels": list(resolved_candidate_labels),
|
|
1091
|
+
"source_label_origin": source_label_origin,
|
|
1092
|
+
"target_label_origin": target_label_origin,
|
|
1093
|
+
"config": self.event_prediction_config.to_dict(),
|
|
1094
|
+
"num_evaluated": metric_payload["num_evaluated"],
|
|
1095
|
+
"num_skipped": metric_payload["num_skipped"],
|
|
1096
|
+
"num_abstained": metric_payload["num_abstained"],
|
|
1097
|
+
"per_label": metric_payload["per_label"],
|
|
1098
|
+
"confusion_matrix": metric_payload["confusion_matrix"],
|
|
1099
|
+
"source_predictions": [prediction.to_dict() for prediction in source_prediction_records]
|
|
1100
|
+
if source_prediction_records is not None
|
|
1101
|
+
else None,
|
|
1102
|
+
"target_predictions": [prediction.to_dict() for prediction in target_prediction_records]
|
|
1103
|
+
if target_prediction_records is not None
|
|
1104
|
+
else None,
|
|
1105
|
+
"samples": [
|
|
1106
|
+
{
|
|
1107
|
+
"sample_index": index,
|
|
1108
|
+
"sample_id": sample_ids[index] if sample_ids is not None else str(index),
|
|
1109
|
+
"reference_label": normalized_source_labels[index],
|
|
1110
|
+
"predicted_label": normalized_target_labels[index],
|
|
1111
|
+
"correct": normalized_source_labels[index] is not None
|
|
1112
|
+
and normalized_source_labels[index] == normalized_target_labels[index],
|
|
1113
|
+
}
|
|
1114
|
+
for index in range(num_samples)
|
|
1115
|
+
],
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if self.use_event_alignment and normalized_source_onsets_ms is not None:
|
|
1119
|
+
assert normalized_source_labels is not None
|
|
1120
|
+
assert normalized_target_labels is not None
|
|
1121
|
+
|
|
1122
|
+
target_localization_records: Optional[List[EventLocalization]] = None
|
|
1123
|
+
target_onset_origin = "provided_target_onsets_ms"
|
|
1124
|
+
|
|
1125
|
+
if normalized_target_onsets_ms is None:
|
|
1126
|
+
target_localization_records = self._localize_events(
|
|
1127
|
+
audio_paths=target_audio_paths,
|
|
1128
|
+
labels=normalized_target_labels,
|
|
1129
|
+
candidate_labels=resolved_candidate_labels,
|
|
1130
|
+
label_normalizer=label_normalizer,
|
|
1131
|
+
)
|
|
1132
|
+
normalized_target_onsets_ms = [item.onset_ms for item in target_localization_records]
|
|
1133
|
+
target_onset_origin = "localized_target_onsets_ms"
|
|
1134
|
+
|
|
1135
|
+
assert normalized_target_onsets_ms is not None
|
|
1136
|
+
|
|
1137
|
+
source_durations_ms = [_get_audio_duration_ms(path) for path in source_audio_paths]
|
|
1138
|
+
target_durations_ms = [_get_audio_duration_ms(path) for path in target_audio_paths]
|
|
1139
|
+
|
|
1140
|
+
alignment_payload = _compute_alignment_metrics(
|
|
1141
|
+
normalized_source_labels,
|
|
1142
|
+
normalized_target_labels,
|
|
1143
|
+
normalized_source_onsets_ms,
|
|
1144
|
+
normalized_target_onsets_ms,
|
|
1145
|
+
source_durations_ms,
|
|
1146
|
+
target_durations_ms,
|
|
1147
|
+
relative_onset_tolerance=self.event_alignment_config.relative_onset_tolerance,
|
|
1148
|
+
sample_ids=sample_ids,
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
if source_labels is None:
|
|
1152
|
+
alignment_rate_name = "Predicted_Event_Aligned_Consistency_Rate"
|
|
1153
|
+
onset_error_name = "Predicted_Conditional_Relative_Onset_Error"
|
|
1154
|
+
else:
|
|
1155
|
+
alignment_rate_name = "Event_Aligned_Preservation_Rate"
|
|
1156
|
+
onset_error_name = "Conditional_Relative_Onset_Error"
|
|
1157
|
+
|
|
1158
|
+
results[alignment_rate_name] = alignment_payload["aligned_preservation_rate"]
|
|
1159
|
+
results[onset_error_name] = alignment_payload["conditional_relative_onset_error"]
|
|
1160
|
+
|
|
1161
|
+
diagnostics["event_alignment"] = {
|
|
1162
|
+
"candidate_labels": list(resolved_candidate_labels),
|
|
1163
|
+
"source_label_origin": source_label_origin,
|
|
1164
|
+
"target_label_origin": target_label_origin,
|
|
1165
|
+
"target_onset_origin": target_onset_origin,
|
|
1166
|
+
"prediction_config": self.event_prediction_config.to_dict(),
|
|
1167
|
+
"localization_config": self.event_localization_config.to_dict(),
|
|
1168
|
+
"alignment_config": self.event_alignment_config.to_dict(),
|
|
1169
|
+
"source_onsets_ms": normalized_source_onsets_ms,
|
|
1170
|
+
"target_onsets_ms": normalized_target_onsets_ms,
|
|
1171
|
+
"target_localizations": [item.to_dict() for item in target_localization_records]
|
|
1172
|
+
if target_localization_records is not None
|
|
1173
|
+
else None,
|
|
1174
|
+
"num_evaluated": alignment_payload["num_evaluated"],
|
|
1175
|
+
"num_skipped": alignment_payload["num_skipped"],
|
|
1176
|
+
"num_aligned": alignment_payload["num_aligned"],
|
|
1177
|
+
"num_missing_target_onset": alignment_payload["num_missing_target_onset"],
|
|
1178
|
+
"num_conditionally_evaluated": alignment_payload["num_conditionally_evaluated"],
|
|
1179
|
+
"relative_onset_tolerance": alignment_payload["relative_onset_tolerance"],
|
|
1180
|
+
"samples": alignment_payload["samples"],
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if verbose:
|
|
1184
|
+
print("\n[ParalinguisticEvaluator] Summary")
|
|
1185
|
+
print(f" Samples: {num_samples}")
|
|
1186
|
+
for metric_name, score in results.items():
|
|
1187
|
+
print(f" {metric_name}: {score}")
|
|
1188
|
+
|
|
1189
|
+
if return_diagnostics:
|
|
1190
|
+
return results, diagnostics
|
|
1191
|
+
return results
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def load_paralinguistic_manifest(path: str) -> List[ParalinguisticSample]:
|
|
1195
|
+
manifest_path = Path(path)
|
|
1196
|
+
if not manifest_path.exists():
|
|
1197
|
+
raise FileNotFoundError(f"Paralinguistic manifest not found: {path}")
|
|
1198
|
+
|
|
1199
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
1200
|
+
if not isinstance(data, list):
|
|
1201
|
+
raise ValueError("Paralinguistic manifest must be a JSON list.")
|
|
1202
|
+
|
|
1203
|
+
samples: List[ParalinguisticSample] = []
|
|
1204
|
+
for index, item in enumerate(data):
|
|
1205
|
+
if not isinstance(item, dict):
|
|
1206
|
+
raise ValueError(f"Manifest item {index} must be a dict.")
|
|
1207
|
+
|
|
1208
|
+
source_audio = item.get("source_audio")
|
|
1209
|
+
if not source_audio:
|
|
1210
|
+
raise ValueError(f"Manifest item {index} is missing source_audio.")
|
|
1211
|
+
|
|
1212
|
+
raw_label = item.get("source_label", item.get("label", item.get("labels")))
|
|
1213
|
+
source_label = _coerce_manifest_source_label(raw_label, index)
|
|
1214
|
+
metadata = item.get("metadata") or {}
|
|
1215
|
+
if not isinstance(metadata, dict):
|
|
1216
|
+
metadata = {}
|
|
1217
|
+
|
|
1218
|
+
raw_onset = item.get("source_onset_ms", item.get("onset_ms", item.get("start_ms", metadata.get("source_onset_ms"))))
|
|
1219
|
+
raw_offset = item.get("source_offset_ms", item.get("offset_ms", item.get("end_ms", metadata.get("source_offset_ms"))))
|
|
1220
|
+
|
|
1221
|
+
samples.append(
|
|
1222
|
+
ParalinguisticSample(
|
|
1223
|
+
sample_id=str(item.get("id", index)),
|
|
1224
|
+
source_audio=ensure_existing_audio(str(source_audio)),
|
|
1225
|
+
source_text=str(item.get("source_text", "")).strip(),
|
|
1226
|
+
source_label=source_label,
|
|
1227
|
+
source_onset_ms=_coerce_optional_float(raw_onset, name="source_onset_ms", index=index),
|
|
1228
|
+
source_offset_ms=_coerce_optional_float(raw_offset, name="source_offset_ms", index=index),
|
|
1229
|
+
metadata=metadata,
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
1232
|
+
return samples
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def load_paralinguistic_samples(path: str, max_samples: Optional[int] = None) -> List[ParalinguisticSample]:
|
|
1236
|
+
samples = load_paralinguistic_manifest(path)
|
|
1237
|
+
if max_samples is not None:
|
|
1238
|
+
return samples[:max_samples]
|
|
1239
|
+
return samples
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def build_paralinguistic_inputs(samples: List[ParalinguisticSample]) -> Dict[str, List[Any]]:
|
|
1243
|
+
if not samples:
|
|
1244
|
+
raise ValueError("No paralinguistic samples provided.")
|
|
1245
|
+
return {
|
|
1246
|
+
"sample_ids": [sample.sample_id for sample in samples],
|
|
1247
|
+
"source_audio": [sample.source_audio for sample in samples],
|
|
1248
|
+
"source_text": [sample.source_text for sample in samples],
|
|
1249
|
+
"source_labels": [sample.source_label for sample in samples],
|
|
1250
|
+
"source_onsets_ms": [sample.source_onset_ms for sample in samples],
|
|
1251
|
+
"source_offsets_ms": [sample.source_offset_ms for sample in samples],
|
|
1252
|
+
"metadata": [sample.metadata for sample in samples],
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def evaluate_paralinguistic_dataset(
|
|
1257
|
+
target_audio: List[str],
|
|
1258
|
+
*,
|
|
1259
|
+
samples: Optional[List[ParalinguisticSample]] = None,
|
|
1260
|
+
manifest_path: Optional[str] = None,
|
|
1261
|
+
max_samples: Optional[int] = None,
|
|
1262
|
+
evaluator: Optional[ParalinguisticEvaluator] = None,
|
|
1263
|
+
evaluator_kwargs: Optional[Dict[str, Any]] = None,
|
|
1264
|
+
device: Optional[str] = None,
|
|
1265
|
+
return_diagnostics: bool = False,
|
|
1266
|
+
sample_transform: Optional[Callable[[List[ParalinguisticSample]], List[ParalinguisticSample]]] = None,
|
|
1267
|
+
target_labels: Optional[Sequence[Optional[str]]] = None,
|
|
1268
|
+
target_onsets_ms: Optional[Sequence[Optional[Union[int, float]]]] = None,
|
|
1269
|
+
candidate_labels: Optional[Sequence[str]] = None,
|
|
1270
|
+
label_normalizer: LabelNormalizer = None,
|
|
1271
|
+
) -> Union[Tuple[Dict[str, float], Dict[str, Any]], Dict[str, float]]:
|
|
1272
|
+
if samples is None:
|
|
1273
|
+
if not manifest_path:
|
|
1274
|
+
raise ValueError("manifest_path is required when samples are not provided.")
|
|
1275
|
+
samples = load_paralinguistic_manifest(manifest_path)
|
|
1276
|
+
|
|
1277
|
+
if max_samples is not None:
|
|
1278
|
+
samples = samples[:max_samples]
|
|
1279
|
+
if sample_transform is not None:
|
|
1280
|
+
samples = sample_transform(samples)
|
|
1281
|
+
if not samples:
|
|
1282
|
+
raise ValueError("No paralinguistic samples available.")
|
|
1283
|
+
|
|
1284
|
+
inputs = build_paralinguistic_inputs(samples)
|
|
1285
|
+
if len(inputs["source_audio"]) != len(target_audio):
|
|
1286
|
+
raise ValueError(
|
|
1287
|
+
f"Source/target size mismatch for paralinguistic evaluation: "
|
|
1288
|
+
f"{len(inputs['source_audio'])} vs {len(target_audio)}"
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
if evaluator is None:
|
|
1292
|
+
final_kwargs = dict(evaluator_kwargs or {})
|
|
1293
|
+
final_kwargs.setdefault("use_continuous_fidelity", True)
|
|
1294
|
+
final_kwargs.setdefault("use_event_preservation", True)
|
|
1295
|
+
final_kwargs.setdefault("use_event_alignment", True)
|
|
1296
|
+
evaluator = ParalinguisticEvaluator(device=device, **final_kwargs)
|
|
1297
|
+
|
|
1298
|
+
result = evaluator.evaluate_all(
|
|
1299
|
+
source_audio=inputs["source_audio"],
|
|
1300
|
+
target_audio=target_audio,
|
|
1301
|
+
source_labels=inputs["source_labels"],
|
|
1302
|
+
target_labels=target_labels,
|
|
1303
|
+
source_onsets_ms=inputs["source_onsets_ms"],
|
|
1304
|
+
target_onsets_ms=target_onsets_ms,
|
|
1305
|
+
candidate_labels=candidate_labels,
|
|
1306
|
+
label_normalizer=label_normalizer,
|
|
1307
|
+
sample_ids=inputs["sample_ids"],
|
|
1308
|
+
verbose=True,
|
|
1309
|
+
return_diagnostics=return_diagnostics,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
if return_diagnostics:
|
|
1313
|
+
scores, diagnostics = result
|
|
1314
|
+
diagnostics["sample_ids"] = inputs["sample_ids"]
|
|
1315
|
+
diagnostics["metadata"] = inputs["metadata"]
|
|
1316
|
+
return scores, diagnostics
|
|
1317
|
+
|
|
1318
|
+
return result
|