OpenSTBench 1.2.2__tar.gz → 1.2.3__tar.gz

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.
Files changed (26) hide show
  1. {openstbench-1.2.2/src/OpenSTBench.egg-info → openstbench-1.2.3}/PKG-INFO +1 -1
  2. {openstbench-1.2.2 → openstbench-1.2.3}/pyproject.toml +1 -1
  3. {openstbench-1.2.2 → openstbench-1.2.3/src/OpenSTBench.egg-info}/PKG-INFO +1 -1
  4. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/__init__.py +1 -1
  5. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/translation_evaluator.py +139 -84
  6. {openstbench-1.2.2 → openstbench-1.2.3}/LICENSE +0 -0
  7. {openstbench-1.2.2 → openstbench-1.2.3}/README.md +0 -0
  8. {openstbench-1.2.2 → openstbench-1.2.3}/setup.cfg +0 -0
  9. {openstbench-1.2.2 → openstbench-1.2.3}/src/OpenSTBench.egg-info/SOURCES.txt +0 -0
  10. {openstbench-1.2.2 → openstbench-1.2.3}/src/OpenSTBench.egg-info/dependency_links.txt +0 -0
  11. {openstbench-1.2.2 → openstbench-1.2.3}/src/OpenSTBench.egg-info/requires.txt +0 -0
  12. {openstbench-1.2.2 → openstbench-1.2.3}/src/OpenSTBench.egg-info/top_level.txt +0 -0
  13. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/_model_loading.py +0 -0
  14. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/emotion_evaluator.py +0 -0
  15. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/language_policy.py +0 -0
  16. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/__init__.py +0 -0
  17. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/agent.py +0 -0
  18. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/basics.py +0 -0
  19. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/cli.py +0 -0
  20. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/instance.py +0 -0
  21. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/metrics.py +0 -0
  22. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/latency/utils.py +0 -0
  23. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/paralinguistic_evaluator.py +0 -0
  24. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/speaker_similarity_evaluator.py +0 -0
  25. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/speech_quality_evaluator.py +0 -0
  26. {openstbench-1.2.2 → openstbench-1.2.3}/src/openstbench/temporal_consistency_evaluator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenSTBench
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Unified multidimensional evaluation toolkit for S2TT and S2ST systems in offline and streaming speech translation settings
5
5
  Author-email: Yanjie An <691476922@qq.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OpenSTBench"
7
- version = "1.2.2"
7
+ version = "1.2.3"
8
8
  description = "Unified multidimensional evaluation toolkit for S2TT and S2ST systems in offline and streaming speech translation settings"
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  requires-python = ">=3.9,<3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenSTBench
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Unified multidimensional evaluation toolkit for S2TT and S2ST systems in offline and streaming speech translation settings
5
5
  Author-email: Yanjie An <691476922@qq.com>
6
6
  License: MIT
@@ -9,7 +9,7 @@ actually accessed.
9
9
  from importlib import import_module
10
10
  from typing import Dict, Tuple
11
11
 
12
- __version__ = "1.2.2"
12
+ __version__ = "1.2.3"
13
13
 
14
14
  __all__ = [
15
15
  "TranslationEvaluator",
@@ -1,24 +1,28 @@
1
- import os
2
1
  import gc
3
2
  import json
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Union
6
+
4
7
  import numpy as np
5
8
  import sacrebleu
6
9
  import torch
7
- from typing import Dict, List, Optional, Union
8
- from pathlib import Path
9
10
 
10
11
  from ._model_loading import resolve_pretrained_source
11
12
 
13
+
12
14
  # ==================== Configuration ====================
13
15
 
14
16
  CACHE_PATHS = {
15
17
  "huggingface": os.path.expanduser("~/.cache/huggingface/hub"),
16
18
  }
17
19
 
20
+
18
21
  # ==================== Optional Dependencies ====================
19
22
 
20
23
  try:
21
24
  from bleurt_pytorch import BleurtConfig, BleurtForSequenceClassification, BleurtTokenizer
25
+
22
26
  HAS_BLEURT = True
23
27
  except ImportError:
24
28
  HAS_BLEURT = False
@@ -29,46 +33,50 @@ except ImportError:
29
33
  download_model = None
30
34
  load_from_checkpoint = None
31
35
 
36
+
32
37
  # ==================== Input Loaders ====================
33
38
 
34
39
  def load_text_from_file_or_list(input_data: Union[str, List[str]], name: str = "text") -> List[str]:
35
40
 
36
41
  if isinstance(input_data, list):
37
42
  return input_data
38
-
43
+
39
44
  if not isinstance(input_data, str):
40
45
  raise ValueError(f"{name} must be a file path (str) or a list of strings (List[str])")
41
46
 
42
47
  path = Path(input_data)
43
48
  if not path.exists():
44
49
  raise FileNotFoundError(f"{name} file not found: {input_data}")
45
-
50
+
46
51
  suffix = path.suffix.lower()
47
-
52
+
48
53
  if suffix == ".json":
49
54
  with open(path, "r", encoding="utf-8") as f:
50
55
  data = json.load(f)
51
-
56
+
52
57
  if isinstance(data, list):
53
- if len(data) == 0: return []
54
- if isinstance(data[0], str): return data
58
+ if len(data) == 0:
59
+ return []
60
+ if isinstance(data[0], str):
61
+ return data
55
62
  if isinstance(data[0], dict):
56
63
  for key in ["target_text", "hypothesis", "text", "ref", "reference", "src", "source"]:
57
64
  if key in data[0]:
58
65
  return [item[key] for item in data]
59
- raise ValueError(f"JSON list items do not contain common text fields")
66
+ raise ValueError("JSON list items do not contain common text fields")
60
67
 
61
68
  if isinstance(data, dict):
62
69
  for key in ["target_text", "hypothesis", "text", "ref", "reference", "src", "source"]:
63
70
  if key in data:
64
71
  return data[key]
65
72
  raise ValueError("JSON dictionary does not contain common text fields")
66
-
73
+
67
74
  raise ValueError("Unsupported JSON format")
68
75
  else:
69
76
  with open(path, "r", encoding="utf-8") as f:
70
77
  return [line.strip() for line in f if line.strip()]
71
78
 
79
+
72
80
  def load_audio_from_folder(folder_path: str, extensions: tuple = (".wav", ".mp3", ".flac")) -> List[str]:
73
81
  folder = Path(folder_path)
74
82
  if not folder.exists():
@@ -84,25 +92,32 @@ def load_audio_from_folder(folder_path: str, extensions: tuple = (".wav", ".mp3"
84
92
 
85
93
  # ==================== Evaluator Core Class ====================
86
94
 
87
- DEFAULT_COMET_MODEL = "Unbabel/wmt22-cometkiwi-da"
95
+ DEFAULT_COMET_MODEL = "Unbabel/wmt22-comet-da"
96
+ DEFAULT_COMET_QE_MODEL = "Unbabel/wmt22-cometkiwi-da"
88
97
  DEFAULT_BLEURT_MODEL = "lucadiliello/BLEURT-20"
98
+ COMET_METRIC_NAME = "COMET"
89
99
  COMET_QE_METRIC_NAME = "COMETKiwi"
90
100
 
101
+
91
102
  class TranslationEvaluator:
92
103
  """
93
- Text-side Translation Quality Evaluator: Supports Text Translation Extraction and Comparison (BLEU, COMET, BLEURT...)
104
+ Text-side Translation Quality Evaluator: Supports text translation metrics
105
+ such as BLEU, chrF++, COMET, COMETKiwi, and BLEURT.
94
106
  """
95
107
 
96
- def __init__(self,
97
- use_bleu: bool = True,
98
- use_chrf: bool = True,
99
- use_comet: bool = True,
100
- use_bleurt: bool = False,
101
- comet_model: str = DEFAULT_COMET_MODEL,
102
- bleurt_path: Optional[str] = None,
103
- bleurt_model: Optional[str] = None,
104
- device: Optional[str] = None):
105
-
108
+ def __init__(
109
+ self,
110
+ use_bleu: bool = True,
111
+ use_chrf: bool = True,
112
+ use_comet: bool = True,
113
+ use_bleurt: bool = False,
114
+ comet_model: str = DEFAULT_COMET_MODEL,
115
+ comet_qe_model: str = DEFAULT_COMET_QE_MODEL,
116
+ bleurt_path: Optional[str] = None,
117
+ bleurt_model: Optional[str] = None,
118
+ device: Optional[str] = None,
119
+ ):
120
+
106
121
  self.use_bleu = use_bleu
107
122
  self.use_chrf = use_chrf
108
123
  self.use_comet = use_comet
@@ -114,28 +129,30 @@ class TranslationEvaluator:
114
129
  self.device = "cuda"
115
130
  else:
116
131
  self.device = "cpu"
117
-
118
- print(f"🚀 Initializing Translation Evaluator (Text-only) on {self.device}...")
119
132
 
120
- # Load language models
133
+ print(f"Initializing Translation Evaluator (Text-only) on {self.device}...")
134
+
135
+ self.comet_model = comet_model
136
+ self.comet_qe_model = comet_qe_model
121
137
  self.comet = None
122
- if self.use_comet:
123
- self.comet = self._load_comet(comet_model)
138
+ self.comet_qe = None
124
139
 
125
140
  self.bleurt_model = None
126
141
  self.bleurt_tokenizer = None
127
142
  if self.use_bleurt:
128
143
  self._load_bleurt(bleurt_path, bleurt_model)
129
144
 
130
- print("Translation text evaluation metrics system is ready!")
145
+ print("Translation text evaluation metrics system is ready!")
146
+
147
+ def __enter__(self):
148
+ return self
131
149
 
132
- def __enter__(self): return self
133
- def __exit__(self, exc_type, exc_val, exc_tb):
150
+ def __exit__(self, exc_type, exc_val, exc_tb):
134
151
  self.cleanup()
135
152
  return False
136
153
 
137
154
  def cleanup(self):
138
- for attr in ['comet', 'bleurt_model', 'bleurt_tokenizer']:
155
+ for attr in ["comet", "comet_qe", "bleurt_model", "bleurt_tokenizer"]:
139
156
  if hasattr(self, attr) and getattr(self, attr) is not None:
140
157
  delattr(self, attr)
141
158
  if torch.cuda.is_available():
@@ -159,38 +176,38 @@ class TranslationEvaluator:
159
176
  return str(ckpt.resolve())
160
177
  return None
161
178
 
162
- def _load_comet(self, model_name: str):
179
+ def _load_comet(self, model_name: str, *, fallback_model: str, metric_name: str):
163
180
  if not download_model:
164
- print("⚠️ COMET is not installed, skipping...")
181
+ print("COMET is not installed, skipping...")
165
182
  return None
166
183
  try:
167
184
  model_source, source_kind = resolve_pretrained_source(
168
185
  model_name,
169
- fallback_source=DEFAULT_COMET_MODEL,
186
+ fallback_source=fallback_model,
170
187
  )
171
188
  local_ckpt = self._resolve_local_comet_checkpoint(model_source)
172
189
  if local_ckpt is not None:
173
- print(f"[Local] Loading COMET: {local_ckpt}")
190
+ print(f"[Local] Loading {metric_name}: {local_ckpt}")
174
191
  model = load_from_checkpoint(local_ckpt)
175
192
  else:
176
- remote_source = model_source if source_kind == "remote" else DEFAULT_COMET_MODEL
193
+ remote_source = model_source if source_kind == "remote" else fallback_model
177
194
  cache = os.path.join(CACHE_PATHS["huggingface"], f"models--{remote_source.replace('/', '--')}")
178
195
  status = "[Local]" if os.path.exists(cache) else "[Online]"
179
- print(f"{status} Loading COMET: {model_name}")
180
- print(f"Loading COMET ({status}) from {remote_source}")
196
+ print(f"{status} Loading {metric_name}: {model_name}")
197
+ print(f"Loading {metric_name} ({status}) from {remote_source}")
181
198
  model = load_from_checkpoint(download_model(remote_source))
182
199
  if self.device.startswith("cuda"):
183
200
  model = model.to(self.device)
184
201
  return model
185
202
  except Exception as e:
186
- print(f" COMET loading failed: {e}")
203
+ print(f"{metric_name} loading failed: {e}")
187
204
  return None
188
205
 
189
206
  def _load_bleurt(self, path: Optional[str], model_name: Optional[str]):
190
207
  if not HAS_BLEURT:
191
- print("⚠️ bleurt-pytorch is not installed, skipping...")
208
+ print("bleurt-pytorch is not installed, skipping...")
192
209
  return
193
-
210
+
194
211
  if path:
195
212
  model_source, _source_kind = resolve_pretrained_source(
196
213
  path,
@@ -198,30 +215,40 @@ class TranslationEvaluator:
198
215
  )
199
216
  else:
200
217
  model_source, _source_kind = resolve_pretrained_source(model_name or DEFAULT_BLEURT_MODEL)
201
- print(f"Loading BLEURT: {model_source}")
202
-
218
+ print(f"Loading BLEURT: {model_source}")
219
+
203
220
  try:
204
221
  self.bleurt_tokenizer = BleurtTokenizer.from_pretrained(model_source)
205
222
  self.bleurt_model = BleurtForSequenceClassification.from_pretrained(model_source)
206
223
  self.bleurt_model = self.bleurt_model.to(self.device).eval()
207
224
  except Exception as e:
208
- print(f"BLEURT loading failed: {e}")
209
-
225
+ print(f"BLEURT loading failed: {e}")
210
226
 
211
227
  def _get_bleu_tokenizer_name(self, lang: str) -> str:
212
- if lang == 'zh': return 'zh'
213
- elif lang == 'ja': return 'ja-mecab'
214
- elif lang == 'ko': return 'ko-mecab'
215
- else: return '13a'
228
+ if lang == "zh":
229
+ return "zh"
230
+ elif lang == "ja":
231
+ return "ja-mecab"
232
+ elif lang == "ko":
233
+ return "ko-mecab"
234
+ else:
235
+ return "13a"
216
236
 
217
237
  def _compute_bleurt_score(self, references: List[str], candidates: List[str]) -> float:
218
238
  all_scores = []
219
239
  batch_size = 32
220
240
  for i in range(0, len(references), batch_size):
221
- br = references[i:i+batch_size]
222
- bc = candidates[i:i+batch_size]
241
+ br = references[i : i + batch_size]
242
+ bc = candidates[i : i + batch_size]
223
243
  with torch.no_grad():
224
- inputs = self.bleurt_tokenizer(br, bc, padding='longest', truncation=True, max_length=512, return_tensors='pt')
244
+ inputs = self.bleurt_tokenizer(
245
+ br,
246
+ bc,
247
+ padding="longest",
248
+ truncation=True,
249
+ max_length=512,
250
+ return_tensors="pt",
251
+ )
225
252
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
226
253
  scores = self.bleurt_model(**inputs).logits.flatten().tolist()
227
254
  all_scores.extend(scores)
@@ -229,67 +256,95 @@ class TranslationEvaluator:
229
256
 
230
257
  def evaluate_all(
231
258
  self,
232
- reference: Union[List[str], str],
233
- target_text: Union[List[str], str],
259
+ reference: Optional[Union[List[str], str]] = None,
260
+ target_text: Optional[Union[List[str], str]] = None,
234
261
  source: Optional[Union[List[str], str]] = None,
235
- target_lang: str = "en"
262
+ target_lang: str = "en",
236
263
  ) -> Dict[str, float]:
237
264
  """
238
265
  Args:
239
- reference: Ground truth benchmark translation reference from the dataset (List or file path).
240
- target_text: Direct text translation output from the translation model (List or file path).
241
- source: Optional original source text (mandatory for COMETKiwi/QE).
242
- target_lang: Target language, affects BLEU Tokenizer selection.
266
+ reference: Optional benchmark translation reference from the dataset.
267
+ target_text: Direct text translation output from the translation model.
268
+ source: Optional original source text, mandatory for COMET and COMETKiwi.
269
+ target_lang: Target language, affects BLEU tokenizer selection.
243
270
  """
244
271
  results = {}
245
272
  print(f"\n--- Starting Text Translation Quality Evaluation (Target Lang: {target_lang}) ---")
246
273
 
247
- final_ref = load_text_from_file_or_list(reference, "Reference")
274
+ if target_text is None:
275
+ raise ValueError("target_text is required")
248
276
  final_text = load_text_from_file_or_list(target_text, "Target Text")
249
-
277
+
278
+ final_ref = None
279
+ if reference is not None:
280
+ loaded_ref = load_text_from_file_or_list(reference, "Reference")
281
+ final_ref = loaded_ref if loaded_ref else None
282
+
250
283
  final_src = None
251
284
  if source:
252
285
  final_src = load_text_from_file_or_list(source, "Source")
253
- if len(final_src) != len(final_ref):
254
- raise ValueError("Source and Reference have different lengths")
255
-
256
- if len(final_text) != len(final_ref):
286
+ if len(final_src) != len(final_text):
287
+ raise ValueError("Source and Target Text have different lengths")
288
+
289
+ if final_ref is not None and len(final_text) != len(final_ref):
257
290
  raise ValueError("Target Text and Reference have different lengths")
258
291
 
259
292
  # 1. sacreBLEU
260
- if self.use_bleu:
293
+ if self.use_bleu and final_ref is not None:
261
294
  tokenizer_name = self._get_bleu_tokenizer_name(target_lang)
262
295
  try:
263
296
  results["sacreBLEU"] = sacrebleu.corpus_bleu(final_text, [final_ref], tokenize=tokenizer_name).score
264
- except Exception as e:
297
+ except Exception:
265
298
  results["sacreBLEU"] = -1.0
266
299
 
267
300
  # 2. chrF++
268
- if self.use_chrf:
301
+ if self.use_chrf and final_ref is not None:
269
302
  try:
270
303
  results["chrF++"] = sacrebleu.corpus_chrf(final_text, [final_ref], word_order=2).score
271
- except: results["chrF++"] = -1.0
304
+ except Exception:
305
+ results["chrF++"] = -1.0
272
306
 
273
307
  # 3. BLEURT
274
- if self.use_bleurt and self.bleurt_model:
308
+ if self.use_bleurt and self.bleurt_model and final_ref is not None:
275
309
  try:
276
310
  results["BLEURT"] = self._compute_bleurt_score(final_ref, final_text)
277
- except Exception as e:
311
+ except Exception:
278
312
  results["BLEURT"] = -1.0
279
313
 
280
- # 4. COMETKiwi reference-free QE. References may still be passed for
281
- # BLEU/chrF++/BLEURT, but COMETKiwi must only receive source and MT.
282
- if self.use_comet and self.comet:
314
+ # 4. COMET and COMETKiwi.
315
+ if self.use_comet:
283
316
  if not final_src:
317
+ if final_ref is not None:
318
+ results[COMET_METRIC_NAME] = -1.0
284
319
  results[COMET_QE_METRIC_NAME] = -1.0
285
- return {k: round(v, 4) if v >= 0 else v for k, v in results.items()}
286
- if len(final_src) != len(final_text):
287
- raise ValueError("Source and Target Text have different lengths")
288
- try:
289
- data = [{"src": s, "mt": t} for s, t in zip(final_src, final_text)]
320
+ else:
290
321
  gpus = 1 if self.device.startswith("cuda") else 0
291
- results[COMET_QE_METRIC_NAME] = self.comet.predict(data, batch_size=8, gpus=gpus).system_score
292
- except Exception as e:
293
- results[COMET_QE_METRIC_NAME] = -1.0
294
-
322
+
323
+ if final_ref is not None:
324
+ if self.comet is None:
325
+ self.comet = self._load_comet(
326
+ self.comet_model,
327
+ fallback_model=DEFAULT_COMET_MODEL,
328
+ metric_name=COMET_METRIC_NAME,
329
+ )
330
+ if self.comet:
331
+ try:
332
+ data = [{"src": s, "mt": t, "ref": r} for s, t, r in zip(final_src, final_text, final_ref)]
333
+ results[COMET_METRIC_NAME] = self.comet.predict(data, batch_size=8, gpus=gpus).system_score
334
+ except Exception:
335
+ results[COMET_METRIC_NAME] = -1.0
336
+
337
+ if self.comet_qe is None:
338
+ self.comet_qe = self._load_comet(
339
+ self.comet_qe_model,
340
+ fallback_model=DEFAULT_COMET_QE_MODEL,
341
+ metric_name=COMET_QE_METRIC_NAME,
342
+ )
343
+ if self.comet_qe:
344
+ try:
345
+ data = [{"src": s, "mt": t} for s, t in zip(final_src, final_text)]
346
+ results[COMET_QE_METRIC_NAME] = self.comet_qe.predict(data, batch_size=8, gpus=gpus).system_score
347
+ except Exception:
348
+ results[COMET_QE_METRIC_NAME] = -1.0
349
+
295
350
  return {k: round(v, 4) if v >= 0 else v for k, v in results.items()}
File without changes
File without changes
File without changes