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.
@@ -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