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,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