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,418 @@
|
|
|
1
|
+
from bisect import bisect_right
|
|
2
|
+
from statistics import mean
|
|
3
|
+
|
|
4
|
+
from .instance import SpeechToSpeechInstance
|
|
5
|
+
from .utils import Aligner, map_audio_offsets_to_output_times_and_chunks
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SCORERS = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(name):
|
|
12
|
+
def _reg(cls):
|
|
13
|
+
SCORERS[name] = cls
|
|
14
|
+
return cls
|
|
15
|
+
|
|
16
|
+
return _reg
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def with_alignment(scorer_cls):
|
|
20
|
+
class AlignedScorer(scorer_cls):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
computation_aware=False,
|
|
24
|
+
output_dir="./output",
|
|
25
|
+
alignment_acoustic_model="english_mfa",
|
|
26
|
+
alignment_dictionary_model="english_mfa",
|
|
27
|
+
):
|
|
28
|
+
super().__init__(computation_aware)
|
|
29
|
+
self.aligner = Aligner(
|
|
30
|
+
output_dir,
|
|
31
|
+
acoustic_model=alignment_acoustic_model,
|
|
32
|
+
dictionary_model=alignment_dictionary_model,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __call__(self, instances):
|
|
36
|
+
self.aligner.run_mfa()
|
|
37
|
+
activated = []
|
|
38
|
+
try:
|
|
39
|
+
for idx, ins in instances.items():
|
|
40
|
+
if not isinstance(ins, SpeechToSpeechInstance):
|
|
41
|
+
continue
|
|
42
|
+
if not ins.durations or not ins.delays:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
units, offsets_ms = self.aligner.get_unit_alignment(idx)
|
|
46
|
+
if not units or not offsets_ms:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
chunk_times_ms = ins.elapsed if self.computation_aware else ins.delays
|
|
50
|
+
aligned_times, output_chunk_ids = map_audio_offsets_to_output_times_and_chunks(
|
|
51
|
+
offsets_ms,
|
|
52
|
+
chunk_times_ms,
|
|
53
|
+
ins.durations,
|
|
54
|
+
)
|
|
55
|
+
if not aligned_times:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
ins._alignment_delays = aligned_times
|
|
59
|
+
ins._alignment_units = units
|
|
60
|
+
ins._alignment_output_type = "text"
|
|
61
|
+
ins._alignment_output_chunk_ids = output_chunk_ids
|
|
62
|
+
activated.append(ins)
|
|
63
|
+
|
|
64
|
+
if not activated:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
return super().__call__(instances)
|
|
68
|
+
finally:
|
|
69
|
+
for ins in activated:
|
|
70
|
+
for attr in [
|
|
71
|
+
"_alignment_delays",
|
|
72
|
+
"_alignment_units",
|
|
73
|
+
"_alignment_output_type",
|
|
74
|
+
"_alignment_output_chunk_ids",
|
|
75
|
+
]:
|
|
76
|
+
if hasattr(ins, attr):
|
|
77
|
+
delattr(ins, attr)
|
|
78
|
+
|
|
79
|
+
return AlignedScorer
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LatencyScorer:
|
|
83
|
+
def __init__(self, computation_aware=False):
|
|
84
|
+
self.computation_aware = computation_aware
|
|
85
|
+
|
|
86
|
+
def subtract(self, arr1, arr2):
|
|
87
|
+
return [x - y for x, y in zip(arr1, arr2)]
|
|
88
|
+
|
|
89
|
+
def get_delays(self, ins):
|
|
90
|
+
override = getattr(ins, "_alignment_delays", None)
|
|
91
|
+
if override is not None:
|
|
92
|
+
return override
|
|
93
|
+
return ins.elapsed if self.computation_aware else ins.delays
|
|
94
|
+
|
|
95
|
+
def get_output_type(self, ins):
|
|
96
|
+
return getattr(
|
|
97
|
+
ins,
|
|
98
|
+
"_alignment_output_type",
|
|
99
|
+
"speech" if isinstance(ins, SpeechToSpeechInstance) else "text",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def get_source_chunk_end_times(self, ins):
|
|
103
|
+
boundaries = list(getattr(ins, "source_chunk_end_times_ms", []) or [])
|
|
104
|
+
if boundaries:
|
|
105
|
+
return boundaries
|
|
106
|
+
|
|
107
|
+
delays = list(getattr(ins, "delays", []) or [])
|
|
108
|
+
if delays:
|
|
109
|
+
return sorted(set(delays))
|
|
110
|
+
|
|
111
|
+
source_length = float(getattr(ins, "source_length", 0.0) or 0.0)
|
|
112
|
+
return [source_length] if source_length > 0 else []
|
|
113
|
+
|
|
114
|
+
def source_chunk_id_from_delay(self, boundaries, delay_ms):
|
|
115
|
+
if not boundaries:
|
|
116
|
+
return 1
|
|
117
|
+
idx = bisect_right(boundaries, float(delay_ms))
|
|
118
|
+
idx = max(1, idx)
|
|
119
|
+
return min(idx, len(boundaries))
|
|
120
|
+
|
|
121
|
+
def split_duration_into_tokens(self, duration_ms, token_len_ms):
|
|
122
|
+
if token_len_ms <= 0:
|
|
123
|
+
return [0]
|
|
124
|
+
num, rest = divmod(float(duration_ms), float(token_len_ms))
|
|
125
|
+
tokens = int(num) * [float(token_len_ms)]
|
|
126
|
+
if rest > 0:
|
|
127
|
+
tokens.append(float(rest))
|
|
128
|
+
return tokens or [float(duration_ms)]
|
|
129
|
+
|
|
130
|
+
def build_source_timeline(self, boundaries, token_len_ms=300.0):
|
|
131
|
+
chunk_sizes = [0]
|
|
132
|
+
token_to_chunk = [0]
|
|
133
|
+
token_to_time = [0.0]
|
|
134
|
+
|
|
135
|
+
prev_boundary = 0.0
|
|
136
|
+
for chunk_id, boundary in enumerate(boundaries, 1):
|
|
137
|
+
duration = max(0.0, float(boundary) - prev_boundary)
|
|
138
|
+
prev_boundary = float(boundary)
|
|
139
|
+
tokens = self.split_duration_into_tokens(duration, token_len_ms)
|
|
140
|
+
chunk_sizes.append(len(tokens))
|
|
141
|
+
for token_duration in tokens:
|
|
142
|
+
token_to_time.append(token_to_time[-1] + token_duration)
|
|
143
|
+
token_to_chunk.append(chunk_id)
|
|
144
|
+
|
|
145
|
+
return chunk_sizes, token_to_chunk, token_to_time
|
|
146
|
+
|
|
147
|
+
def compute_algo(self, chunk_sizes, token_to_chunk, token_to_time):
|
|
148
|
+
tgt_to_src = []
|
|
149
|
+
for t in range(1, len(token_to_chunk["tgt"])):
|
|
150
|
+
chunk_id = token_to_chunk["tgt"][t]
|
|
151
|
+
acc_x = sum(chunk_sizes["src"][:chunk_id])
|
|
152
|
+
acc_y = sum(chunk_sizes["tgt"][:chunk_id])
|
|
153
|
+
s_est = t - max(0, acc_y - acc_x)
|
|
154
|
+
curr_src = sum(chunk_sizes["src"][: chunk_id + 1])
|
|
155
|
+
s_idx = s_est if s_est < curr_src else curr_src
|
|
156
|
+
src_time_idx = min(s_idx, len(token_to_time["src"]) - 1)
|
|
157
|
+
tgt_to_src.append((t, src_time_idx))
|
|
158
|
+
|
|
159
|
+
delays = []
|
|
160
|
+
for t, s in tgt_to_src:
|
|
161
|
+
val = token_to_time["tgt"][t] - token_to_time["src"][s]
|
|
162
|
+
delays.append(val)
|
|
163
|
+
return float(mean(delays)) if delays else 0.0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@register("StartOffset")
|
|
167
|
+
class StartOffset(LatencyScorer):
|
|
168
|
+
def __call__(self, instances):
|
|
169
|
+
scores = []
|
|
170
|
+
for ins in instances.values():
|
|
171
|
+
d = self.get_delays(ins)
|
|
172
|
+
scores.append(d[0] if d else 0)
|
|
173
|
+
return mean(scores) if scores else 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@register("StartOffset_SpeechAlign")
|
|
177
|
+
@with_alignment
|
|
178
|
+
class StartOffsetAligned(StartOffset):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@register("ATD")
|
|
183
|
+
class ATDScorer(LatencyScorer):
|
|
184
|
+
def __call__(self, instances) -> float:
|
|
185
|
+
scores = []
|
|
186
|
+
for ins in instances.values():
|
|
187
|
+
delays = self.get_delays(ins)
|
|
188
|
+
if not delays:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
source_boundaries = self.get_source_chunk_end_times(ins)
|
|
192
|
+
if not source_boundaries:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
chunk_sizes = {"src": [], "tgt": []}
|
|
196
|
+
token_to_chunk = {"src": [], "tgt": [0]}
|
|
197
|
+
token_to_time = {"src": [], "tgt": [0.0]}
|
|
198
|
+
|
|
199
|
+
(
|
|
200
|
+
chunk_sizes["src"],
|
|
201
|
+
token_to_chunk["src"],
|
|
202
|
+
token_to_time["src"],
|
|
203
|
+
) = self.build_source_timeline(source_boundaries, token_len_ms=300.0)
|
|
204
|
+
|
|
205
|
+
output_type = self.get_output_type(ins)
|
|
206
|
+
target_chunk_sizes = [0] * (len(source_boundaries) + 1)
|
|
207
|
+
|
|
208
|
+
target_delays = []
|
|
209
|
+
target_chunk_ids = []
|
|
210
|
+
target_token_lens = []
|
|
211
|
+
compute_times = []
|
|
212
|
+
|
|
213
|
+
if output_type == "text":
|
|
214
|
+
alignment_chunk_ids = getattr(ins, "_alignment_output_chunk_ids", None)
|
|
215
|
+
if alignment_chunk_ids:
|
|
216
|
+
output_emit_delays = list(ins.delays)
|
|
217
|
+
for delay, output_chunk_id in zip(delays, alignment_chunk_ids):
|
|
218
|
+
output_chunk_idx = max(0, min(output_chunk_id - 1, len(output_emit_delays) - 1))
|
|
219
|
+
source_chunk_id = self.source_chunk_id_from_delay(
|
|
220
|
+
source_boundaries,
|
|
221
|
+
output_emit_delays[output_chunk_idx],
|
|
222
|
+
)
|
|
223
|
+
target_delays.append(float(delay))
|
|
224
|
+
target_chunk_ids.append(source_chunk_id)
|
|
225
|
+
target_token_lens.append(0.0)
|
|
226
|
+
compute_times.append(0.0)
|
|
227
|
+
else:
|
|
228
|
+
base_emit_delays = list(getattr(ins, "delays", []) or [])
|
|
229
|
+
text_compute_times = []
|
|
230
|
+
if self.computation_aware and getattr(ins, "elapsed", None):
|
|
231
|
+
compute_elapsed = self.subtract(ins.elapsed, ins.delays)
|
|
232
|
+
text_compute_times = self.subtract(compute_elapsed, [0] + compute_elapsed[:-1])
|
|
233
|
+
else:
|
|
234
|
+
text_compute_times = [0.0] * len(delays)
|
|
235
|
+
|
|
236
|
+
for idx, delay in enumerate(delays):
|
|
237
|
+
emit_delay = base_emit_delays[idx] if idx < len(base_emit_delays) else delay
|
|
238
|
+
source_chunk_id = self.source_chunk_id_from_delay(source_boundaries, emit_delay)
|
|
239
|
+
target_delays.append(float(delay))
|
|
240
|
+
target_chunk_ids.append(source_chunk_id)
|
|
241
|
+
target_token_lens.append(0.0)
|
|
242
|
+
compute_times.append(float(text_compute_times[idx]) if idx < len(text_compute_times) else 0.0)
|
|
243
|
+
else:
|
|
244
|
+
speech_compute_times = []
|
|
245
|
+
if self.computation_aware and getattr(ins, "elapsed", None):
|
|
246
|
+
compute_elapsed = self.subtract(ins.elapsed, ins.delays)
|
|
247
|
+
speech_compute_times = self.subtract(compute_elapsed, [0] + compute_elapsed[:-1])
|
|
248
|
+
else:
|
|
249
|
+
speech_compute_times = [0.0] * len(delays)
|
|
250
|
+
|
|
251
|
+
for idx, duration_ms in enumerate(getattr(ins, "durations", []) or []):
|
|
252
|
+
source_chunk_id = self.source_chunk_id_from_delay(source_boundaries, ins.delays[idx])
|
|
253
|
+
token_lens = self.split_duration_into_tokens(duration_ms, token_len_ms=300.0)
|
|
254
|
+
per_token_compute = float(speech_compute_times[idx]) / len(token_lens) if token_lens else 0.0
|
|
255
|
+
for token_len in token_lens:
|
|
256
|
+
target_delays.append(float(delays[idx]))
|
|
257
|
+
target_chunk_ids.append(source_chunk_id)
|
|
258
|
+
target_token_lens.append(float(token_len))
|
|
259
|
+
compute_times.append(per_token_compute)
|
|
260
|
+
|
|
261
|
+
if not target_delays:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
for chunk_id in target_chunk_ids:
|
|
265
|
+
target_chunk_sizes[chunk_id] += 1
|
|
266
|
+
token_to_chunk["tgt"].append(chunk_id)
|
|
267
|
+
|
|
268
|
+
chunk_sizes["tgt"] = target_chunk_sizes
|
|
269
|
+
|
|
270
|
+
for delay, comp, token_len in zip(target_delays, compute_times, target_token_lens):
|
|
271
|
+
prev_tgt = token_to_time["tgt"][-1]
|
|
272
|
+
start = max(float(delay), prev_tgt)
|
|
273
|
+
token_to_time["tgt"].append(start + float(token_len) + float(comp))
|
|
274
|
+
|
|
275
|
+
scores.append(self.compute_algo(chunk_sizes, token_to_chunk, token_to_time))
|
|
276
|
+
|
|
277
|
+
return mean(scores) if scores else 0.0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@register("ATD_SpeechAlign")
|
|
281
|
+
@with_alignment
|
|
282
|
+
class ATDScorerAligned(ATDScorer):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@register("CustomATD")
|
|
287
|
+
class CustomATD(ATDScorer):
|
|
288
|
+
def __call__(self, instances) -> float:
|
|
289
|
+
scores = []
|
|
290
|
+
for ins in instances.values():
|
|
291
|
+
delays = self.get_delays(ins)
|
|
292
|
+
if not delays:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
source_boundaries = self.get_source_chunk_end_times(ins)
|
|
296
|
+
if not source_boundaries:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
chunk_sizes = {"src": [], "tgt": []}
|
|
300
|
+
token_to_chunk = {"src": [], "tgt": [0]}
|
|
301
|
+
token_to_time = {"src": [], "tgt": [0.0]}
|
|
302
|
+
|
|
303
|
+
(
|
|
304
|
+
chunk_sizes["src"],
|
|
305
|
+
token_to_chunk["src"],
|
|
306
|
+
token_to_time["src"],
|
|
307
|
+
) = self.build_source_timeline(source_boundaries, token_len_ms=300.0)
|
|
308
|
+
|
|
309
|
+
output_type = self.get_output_type(ins)
|
|
310
|
+
target_chunk_sizes = [0] * (len(source_boundaries) + 1)
|
|
311
|
+
|
|
312
|
+
target_delays = []
|
|
313
|
+
target_chunk_ids = []
|
|
314
|
+
compute_times = []
|
|
315
|
+
|
|
316
|
+
if output_type == "text":
|
|
317
|
+
alignment_chunk_ids = getattr(ins, "_alignment_output_chunk_ids", None)
|
|
318
|
+
if alignment_chunk_ids:
|
|
319
|
+
output_emit_delays = list(ins.delays)
|
|
320
|
+
for delay, output_chunk_id in zip(delays, alignment_chunk_ids):
|
|
321
|
+
output_chunk_idx = max(0, min(output_chunk_id - 1, len(output_emit_delays) - 1))
|
|
322
|
+
source_chunk_id = self.source_chunk_id_from_delay(
|
|
323
|
+
source_boundaries,
|
|
324
|
+
output_emit_delays[output_chunk_idx],
|
|
325
|
+
)
|
|
326
|
+
target_delays.append(float(delay))
|
|
327
|
+
target_chunk_ids.append(source_chunk_id)
|
|
328
|
+
compute_times.append(0.0)
|
|
329
|
+
else:
|
|
330
|
+
base_emit_delays = list(getattr(ins, "delays", []) or [])
|
|
331
|
+
text_compute_times = []
|
|
332
|
+
if self.computation_aware and getattr(ins, "elapsed", None):
|
|
333
|
+
compute_elapsed = self.subtract(ins.elapsed, ins.delays)
|
|
334
|
+
text_compute_times = self.subtract(compute_elapsed, [0] + compute_elapsed[:-1])
|
|
335
|
+
else:
|
|
336
|
+
text_compute_times = [0.0] * len(delays)
|
|
337
|
+
|
|
338
|
+
for idx, delay in enumerate(delays):
|
|
339
|
+
emit_delay = base_emit_delays[idx] if idx < len(base_emit_delays) else delay
|
|
340
|
+
source_chunk_id = self.source_chunk_id_from_delay(source_boundaries, emit_delay)
|
|
341
|
+
target_delays.append(float(delay))
|
|
342
|
+
target_chunk_ids.append(source_chunk_id)
|
|
343
|
+
compute_times.append(float(text_compute_times[idx]) if idx < len(text_compute_times) else 0.0)
|
|
344
|
+
else:
|
|
345
|
+
speech_compute_times = []
|
|
346
|
+
if self.computation_aware and getattr(ins, "elapsed", None):
|
|
347
|
+
compute_elapsed = self.subtract(ins.elapsed, ins.delays)
|
|
348
|
+
speech_compute_times = self.subtract(compute_elapsed, [0] + compute_elapsed[:-1])
|
|
349
|
+
else:
|
|
350
|
+
speech_compute_times = [0.0] * len(delays)
|
|
351
|
+
|
|
352
|
+
for idx, duration_ms in enumerate(getattr(ins, "durations", []) or []):
|
|
353
|
+
source_chunk_id = self.source_chunk_id_from_delay(source_boundaries, ins.delays[idx])
|
|
354
|
+
token_lens = self.split_duration_into_tokens(duration_ms, token_len_ms=300.0)
|
|
355
|
+
per_token_compute = float(speech_compute_times[idx]) / len(token_lens) if token_lens else 0.0
|
|
356
|
+
for _ in token_lens:
|
|
357
|
+
target_delays.append(float(delays[idx]))
|
|
358
|
+
target_chunk_ids.append(source_chunk_id)
|
|
359
|
+
compute_times.append(per_token_compute)
|
|
360
|
+
|
|
361
|
+
if not target_delays:
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
for chunk_id in target_chunk_ids:
|
|
365
|
+
target_chunk_sizes[chunk_id] += 1
|
|
366
|
+
token_to_chunk["tgt"].append(chunk_id)
|
|
367
|
+
|
|
368
|
+
chunk_sizes["tgt"] = target_chunk_sizes
|
|
369
|
+
|
|
370
|
+
for delay, comp in zip(target_delays, compute_times):
|
|
371
|
+
prev_tgt = token_to_time["tgt"][-1]
|
|
372
|
+
start = max(float(delay), prev_tgt)
|
|
373
|
+
token_to_time["tgt"].append(start + float(comp))
|
|
374
|
+
|
|
375
|
+
scores.append(self.compute_algo(chunk_sizes, token_to_chunk, token_to_time))
|
|
376
|
+
|
|
377
|
+
return mean(scores) if scores else 0.0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@register("CustomATD_SpeechAlign")
|
|
381
|
+
@with_alignment
|
|
382
|
+
class CustomATDAligned(CustomATD):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@register("RTF")
|
|
387
|
+
class RTFScorer(LatencyScorer):
|
|
388
|
+
def __call__(self, instances) -> float:
|
|
389
|
+
scores = []
|
|
390
|
+
for ins in instances.values():
|
|
391
|
+
if not hasattr(ins, "total_inference_time"):
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
src_len_sec = ins.source_length / 1000.0
|
|
395
|
+
if src_len_sec <= 0:
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
scores.append(ins.total_inference_time / src_len_sec)
|
|
399
|
+
|
|
400
|
+
return mean(scores) if scores else 0.0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@register("ModelGenerateRTF")
|
|
404
|
+
class ModelGenerateRTFScorer(LatencyScorer):
|
|
405
|
+
def __call__(self, instances) -> float:
|
|
406
|
+
scores = []
|
|
407
|
+
for ins in instances.values():
|
|
408
|
+
model_sec = getattr(ins, "total_model_inference_time", None)
|
|
409
|
+
if model_sec is None:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
src_len_sec = ins.source_length / 1000.0
|
|
413
|
+
if src_len_sec <= 0:
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
scores.append(float(model_sec) / src_len_sec)
|
|
417
|
+
|
|
418
|
+
return mean(scores) if scores else None
|