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,295 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import textgrid
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import whisper
|
|
12
|
+
except ImportError:
|
|
13
|
+
whisper = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_WHISPER_MODELS = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def submit_slurm(args, python_script_path):
|
|
20
|
+
cmd = f"python {python_script_path} " + " ".join([a for a in sys.argv[1:] if "--slurm" not in a])
|
|
21
|
+
script = f"""#!/bin/bash
|
|
22
|
+
#SBATCH --job-name=latency_eval
|
|
23
|
+
#SBATCH --output={args.output}/slurm.log
|
|
24
|
+
#SBATCH --time=2:00:00
|
|
25
|
+
#SBATCH --gpus-per-node=1
|
|
26
|
+
#SBATCH --cpus-per-task=4
|
|
27
|
+
|
|
28
|
+
mkdir -p {args.output}
|
|
29
|
+
{cmd}
|
|
30
|
+
"""
|
|
31
|
+
script_path = Path(args.output) / "run.sh"
|
|
32
|
+
with open(script_path, "w", encoding="utf-8") as f:
|
|
33
|
+
f.write(script)
|
|
34
|
+
print(f"Submitting Slurm job: {script_path}")
|
|
35
|
+
os.system(f"sbatch {script_path}")
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Visualizer:
|
|
40
|
+
def __init__(self, output_dir):
|
|
41
|
+
self.output_dir = Path(output_dir) / "visual"
|
|
42
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
def plot(self, instance_data):
|
|
45
|
+
try:
|
|
46
|
+
import matplotlib.pyplot as plt
|
|
47
|
+
except ImportError:
|
|
48
|
+
print("Warning: matplotlib not installed, skipping visualization.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
data = instance_data
|
|
52
|
+
idx = data["index"]
|
|
53
|
+
delays = [d / 1000 for d in data["delays"]]
|
|
54
|
+
if not delays:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
pred = data["prediction"]
|
|
58
|
+
is_text = not str(pred).endswith(".wav")
|
|
59
|
+
|
|
60
|
+
plt.figure(figsize=(10, 6))
|
|
61
|
+
x_points = [0] + delays
|
|
62
|
+
y_points = list(range(len(x_points)))
|
|
63
|
+
plt.step(x_points, y_points, where="post", marker="o")
|
|
64
|
+
plt.xlabel("Source Time (s)")
|
|
65
|
+
plt.ylabel("Output Unit")
|
|
66
|
+
plt.title(f"Instance {idx}")
|
|
67
|
+
|
|
68
|
+
if is_text:
|
|
69
|
+
words = pred.split()
|
|
70
|
+
for i, txt in enumerate(words):
|
|
71
|
+
if i + 1 < len(delays):
|
|
72
|
+
plt.text(delays[i + 1], i + 1, txt)
|
|
73
|
+
else:
|
|
74
|
+
plt.text(0, 0, f"Saved to {pred}")
|
|
75
|
+
|
|
76
|
+
plt.grid(True)
|
|
77
|
+
plt.savefig(self.output_dir / f"{idx}.png")
|
|
78
|
+
plt.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def tokenize_latency_units(text, unit="word"):
|
|
82
|
+
text = " ".join(str(text or "").strip().split())
|
|
83
|
+
if not text:
|
|
84
|
+
return []
|
|
85
|
+
if unit == "char":
|
|
86
|
+
compact = "".join(text.split())
|
|
87
|
+
return [ch for ch in compact if ch.strip()]
|
|
88
|
+
return [part for part in text.split(" ") if part]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _prepare_alignment_transcript(text, unit="word"):
|
|
92
|
+
units = tokenize_latency_units(text, unit=unit)
|
|
93
|
+
return units, " ".join(units)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_whisper_model(model_name="medium", device=None):
|
|
97
|
+
if whisper is None:
|
|
98
|
+
return None
|
|
99
|
+
key = (model_name, device or "auto")
|
|
100
|
+
if key not in _WHISPER_MODELS:
|
|
101
|
+
kwargs = {}
|
|
102
|
+
if device:
|
|
103
|
+
kwargs["device"] = device
|
|
104
|
+
_WHISPER_MODELS[key] = whisper.load_model(model_name, **kwargs)
|
|
105
|
+
return _WHISPER_MODELS[key]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def transcribe_audio_with_whisper(audio_paths, model_name="medium", device=None):
|
|
109
|
+
model = _load_whisper_model(model_name=model_name, device=device)
|
|
110
|
+
if model is None:
|
|
111
|
+
return [""] * len(audio_paths)
|
|
112
|
+
|
|
113
|
+
results = []
|
|
114
|
+
use_fp16 = False
|
|
115
|
+
if device:
|
|
116
|
+
use_fp16 = "cuda" in device
|
|
117
|
+
|
|
118
|
+
for path in audio_paths:
|
|
119
|
+
try:
|
|
120
|
+
res = model.transcribe(path, fp16=use_fp16)
|
|
121
|
+
results.append(str(res.get("text", "")).strip())
|
|
122
|
+
except Exception:
|
|
123
|
+
results.append("")
|
|
124
|
+
return results
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def materialize_s2s_alignment_artifacts(
|
|
128
|
+
instances,
|
|
129
|
+
output_dir,
|
|
130
|
+
unit="word",
|
|
131
|
+
asr_fallback=False,
|
|
132
|
+
asr_model="medium",
|
|
133
|
+
asr_device=None,
|
|
134
|
+
):
|
|
135
|
+
wav_dir = Path(output_dir) / "wavs"
|
|
136
|
+
wav_dir.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
pending_asr = []
|
|
139
|
+
for ins in instances.values():
|
|
140
|
+
wav_path = Path(ins.get_prediction_content())
|
|
141
|
+
ins.prediction_audio_path = str(wav_path)
|
|
142
|
+
txt_path = wav_path.with_suffix(".txt")
|
|
143
|
+
|
|
144
|
+
transcript = (getattr(ins, "prediction_text", "") or "").strip()
|
|
145
|
+
transcript_source = getattr(ins, "prediction_text_source", "none")
|
|
146
|
+
|
|
147
|
+
if transcript:
|
|
148
|
+
units, align_text = _prepare_alignment_transcript(transcript, unit=unit)
|
|
149
|
+
ins.target_units = units
|
|
150
|
+
ins.alignment_mode = transcript_source
|
|
151
|
+
if units:
|
|
152
|
+
txt_path.write_text(align_text, encoding="utf-8")
|
|
153
|
+
elif txt_path.exists():
|
|
154
|
+
txt_path.unlink()
|
|
155
|
+
elif asr_fallback:
|
|
156
|
+
pending_asr.append((ins, wav_path))
|
|
157
|
+
ins.target_units = []
|
|
158
|
+
ins.alignment_mode = "pending_asr_fallback"
|
|
159
|
+
else:
|
|
160
|
+
ins.target_units = []
|
|
161
|
+
ins.alignment_mode = "none"
|
|
162
|
+
if txt_path.exists():
|
|
163
|
+
txt_path.unlink()
|
|
164
|
+
|
|
165
|
+
if pending_asr:
|
|
166
|
+
wav_paths = [str(path) for _, path in pending_asr]
|
|
167
|
+
transcripts = transcribe_audio_with_whisper(
|
|
168
|
+
wav_paths,
|
|
169
|
+
model_name=asr_model,
|
|
170
|
+
device=asr_device,
|
|
171
|
+
)
|
|
172
|
+
for (ins, wav_path), transcript in zip(pending_asr, transcripts):
|
|
173
|
+
txt_path = wav_path.with_suffix(".txt")
|
|
174
|
+
transcript = (transcript or "").strip()
|
|
175
|
+
if not transcript:
|
|
176
|
+
ins.target_units = []
|
|
177
|
+
ins.alignment_mode = "none"
|
|
178
|
+
if txt_path.exists():
|
|
179
|
+
txt_path.unlink()
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
ins.prediction_text = transcript
|
|
183
|
+
ins.prediction_text_source = "asr_fallback"
|
|
184
|
+
ins.alignment_mode = "asr_fallback"
|
|
185
|
+
|
|
186
|
+
units, align_text = _prepare_alignment_transcript(transcript, unit=unit)
|
|
187
|
+
ins.target_units = units
|
|
188
|
+
if units:
|
|
189
|
+
txt_path.write_text(align_text, encoding="utf-8")
|
|
190
|
+
elif txt_path.exists():
|
|
191
|
+
txt_path.unlink()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def map_audio_offsets_to_output_times_and_chunks(offsets_ms, chunk_times_ms, chunk_durations_ms):
|
|
195
|
+
if not offsets_ms or not chunk_times_ms or not chunk_durations_ms:
|
|
196
|
+
return [], []
|
|
197
|
+
|
|
198
|
+
count = min(len(chunk_times_ms), len(chunk_durations_ms))
|
|
199
|
+
if count <= 0:
|
|
200
|
+
return [], []
|
|
201
|
+
|
|
202
|
+
chunk_times_ms = list(chunk_times_ms[:count])
|
|
203
|
+
chunk_durations_ms = list(chunk_durations_ms[:count])
|
|
204
|
+
|
|
205
|
+
output_times = []
|
|
206
|
+
output_chunk_ids = []
|
|
207
|
+
cumulative_end = 0.0
|
|
208
|
+
chunk_starts = []
|
|
209
|
+
for duration in chunk_durations_ms:
|
|
210
|
+
chunk_starts.append(cumulative_end)
|
|
211
|
+
cumulative_end += float(duration)
|
|
212
|
+
|
|
213
|
+
for offset in offsets_ms:
|
|
214
|
+
offset = float(offset)
|
|
215
|
+
if offset < 0:
|
|
216
|
+
offset = 0.0
|
|
217
|
+
|
|
218
|
+
chosen_idx = count - 1
|
|
219
|
+
for idx, start in enumerate(chunk_starts):
|
|
220
|
+
end = start + float(chunk_durations_ms[idx])
|
|
221
|
+
if offset <= end or idx == count - 1:
|
|
222
|
+
chosen_idx = idx
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
local_offset = offset - chunk_starts[chosen_idx]
|
|
226
|
+
local_offset = max(0.0, min(local_offset, float(chunk_durations_ms[chosen_idx])))
|
|
227
|
+
output_times.append(float(chunk_times_ms[chosen_idx]) + local_offset)
|
|
228
|
+
output_chunk_ids.append(chosen_idx + 1)
|
|
229
|
+
|
|
230
|
+
return output_times, output_chunk_ids
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def map_audio_offsets_to_output_times(offsets_ms, chunk_times_ms, chunk_durations_ms):
|
|
234
|
+
output_times, _ = map_audio_offsets_to_output_times_and_chunks(
|
|
235
|
+
offsets_ms,
|
|
236
|
+
chunk_times_ms,
|
|
237
|
+
chunk_durations_ms,
|
|
238
|
+
)
|
|
239
|
+
return output_times
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Aligner:
|
|
243
|
+
def __init__(self, output_dir, acoustic_model="english_mfa", dictionary_model="english_mfa"):
|
|
244
|
+
self.output_dir = Path(output_dir)
|
|
245
|
+
self.align_dir = self.output_dir / "align"
|
|
246
|
+
self.wav_dir = self.output_dir / "wavs"
|
|
247
|
+
self.temp_dir = self.output_dir / "mfa_temp"
|
|
248
|
+
self.acoustic_model = acoustic_model
|
|
249
|
+
self.dictionary_model = dictionary_model
|
|
250
|
+
|
|
251
|
+
def run_mfa(self):
|
|
252
|
+
if self.align_dir.exists() and any(self.align_dir.iterdir()):
|
|
253
|
+
return
|
|
254
|
+
try:
|
|
255
|
+
subprocess.check_output("mfa version", shell=True)
|
|
256
|
+
except Exception:
|
|
257
|
+
print("Error: 'mfa' command not found. Cannot run alignment.")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if not self.wav_dir.exists() or not any(self.wav_dir.glob("*.txt")):
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if self.temp_dir.exists():
|
|
264
|
+
shutil.rmtree(self.temp_dir)
|
|
265
|
+
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
self.align_dir.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
cmd = (
|
|
269
|
+
f"mfa align {self.wav_dir} {self.dictionary_model} {self.acoustic_model} {self.align_dir} "
|
|
270
|
+
f"--clean --overwrite --temporary_directory {self.temp_dir} --verbose"
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
subprocess.run(cmd, shell=True, check=True)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
print(f"MFA Alignment failed: {e}")
|
|
276
|
+
|
|
277
|
+
def get_unit_alignment(self, index):
|
|
278
|
+
tg_path = self.align_dir / f"{index}_pred.TextGrid"
|
|
279
|
+
if not tg_path.exists():
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
tg = textgrid.TextGrid.fromFile(str(tg_path))
|
|
283
|
+
tier = tg[0] if len(tg) > 0 else None
|
|
284
|
+
if tier is None:
|
|
285
|
+
return None, None
|
|
286
|
+
|
|
287
|
+
units = []
|
|
288
|
+
offsets_ms = []
|
|
289
|
+
for interval in tier:
|
|
290
|
+
mark = str(interval.mark or "").strip()
|
|
291
|
+
if not mark or mark == "<eps>":
|
|
292
|
+
continue
|
|
293
|
+
units.append(mark)
|
|
294
|
+
offsets_ms.append(float(interval.maxTime) * 1000.0)
|
|
295
|
+
return units, offsets_ms
|