OpenSTBench 1.2.0__tar.gz → 1.2.2__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 (27) hide show
  1. {openstbench-1.2.0/src/OpenSTBench.egg-info → openstbench-1.2.2}/PKG-INFO +1 -1
  2. {openstbench-1.2.0 → openstbench-1.2.2}/pyproject.toml +1 -1
  3. {openstbench-1.2.0 → openstbench-1.2.2/src/OpenSTBench.egg-info}/PKG-INFO +1 -1
  4. {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/SOURCES.txt +1 -0
  5. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/__init__.py +4 -2
  6. openstbench-1.2.2/src/openstbench/language_policy.py +91 -0
  7. openstbench-1.2.2/src/openstbench/latency/__init__.py +39 -0
  8. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/speech_quality_evaluator.py +173 -184
  9. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/translation_evaluator.py +14 -7
  10. openstbench-1.2.0/src/openstbench/latency/__init__.py +0 -14
  11. {openstbench-1.2.0 → openstbench-1.2.2}/LICENSE +0 -0
  12. {openstbench-1.2.0 → openstbench-1.2.2}/README.md +0 -0
  13. {openstbench-1.2.0 → openstbench-1.2.2}/setup.cfg +0 -0
  14. {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/dependency_links.txt +0 -0
  15. {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/requires.txt +0 -0
  16. {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/top_level.txt +0 -0
  17. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/_model_loading.py +0 -0
  18. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/emotion_evaluator.py +0 -0
  19. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/agent.py +0 -0
  20. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/basics.py +0 -0
  21. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/cli.py +0 -0
  22. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/instance.py +0 -0
  23. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/metrics.py +0 -0
  24. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/utils.py +0 -0
  25. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/paralinguistic_evaluator.py +0 -0
  26. {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/speaker_similarity_evaluator.py +0 -0
  27. {openstbench-1.2.0 → openstbench-1.2.2}/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.0
3
+ Version: 1.2.2
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.0"
7
+ version = "1.2.2"
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.0
3
+ Version: 1.2.2
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,6 +9,7 @@ src/OpenSTBench.egg-info/top_level.txt
9
9
  src/openstbench/__init__.py
10
10
  src/openstbench/_model_loading.py
11
11
  src/openstbench/emotion_evaluator.py
12
+ src/openstbench/language_policy.py
12
13
  src/openstbench/paralinguistic_evaluator.py
13
14
  src/openstbench/speaker_similarity_evaluator.py
14
15
  src/openstbench/speech_quality_evaluator.py
@@ -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.0"
12
+ __version__ = "1.2.2"
13
13
 
14
14
  __all__ = [
15
15
  "TranslationEvaluator",
@@ -35,6 +35,8 @@ __all__ = [
35
35
  "AgentPipeline",
36
36
  "ReadAction",
37
37
  "WriteAction",
38
+ "TextSegment",
39
+ "SpeechSegment",
38
40
  "load_text_from_file_or_list",
39
41
  "load_audio_from_folder",
40
42
  # "Dataset",
@@ -87,7 +89,7 @@ _EXPORT_SPECS: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
87
89
  # ),
88
90
  # ),
89
91
  ("latency.agent", ("AgentPipeline", "GenericAgent")),
90
- ("latency.basics", ("ReadAction", "WriteAction")),
92
+ ("latency.basics", ("ReadAction", "WriteAction", "TextSegment", "SpeechSegment")),
91
93
  ("latency.cli", ("LatencyEvaluator",)),
92
94
  )
93
95
 
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+ LANGUAGE_POLICY_VERSION = "2026-07-fleurs-wer-cer-v1"
7
+
8
+ _LANGUAGE_ALIASES = {
9
+ "cmn": "zh",
10
+ "zho": "zh",
11
+ "chi": "zh",
12
+ "zh": "zh",
13
+ "yue": "zh",
14
+ "eng": "en",
15
+ "en": "en",
16
+ "fra": "fr",
17
+ "fre": "fr",
18
+ "fr": "fr",
19
+ "deu": "de",
20
+ "ger": "de",
21
+ "de": "de",
22
+ "spa": "es",
23
+ "es": "es",
24
+ "jpn": "ja",
25
+ "jp": "ja",
26
+ "ja": "ja",
27
+ "kor": "ko",
28
+ "ko": "ko",
29
+ "tha": "th",
30
+ "th": "th",
31
+ "lao": "lo",
32
+ "lo": "lo",
33
+ "khm": "km",
34
+ "km": "km",
35
+ "mya": "my",
36
+ "bur": "my",
37
+ "my": "my",
38
+ "bod": "bo",
39
+ "tib": "bo",
40
+ "bo": "bo",
41
+ "dzo": "dz",
42
+ "dz": "dz",
43
+ }
44
+
45
+ _CER_LANGUAGES = {
46
+ "zh",
47
+ "ja",
48
+ "ko",
49
+ "th",
50
+ "lo",
51
+ "km",
52
+ "my",
53
+ "bo",
54
+ "dz",
55
+ }
56
+
57
+
58
+ def normalize_language_code(language: Optional[str]) -> str:
59
+ text = str(language or "").strip().lower()
60
+ if not text:
61
+ return ""
62
+ if text.startswith("<|") and text.endswith("|>"):
63
+ text = text[2:-2]
64
+ text = text.replace("-", "_")
65
+ base = text.split("_", 1)[0]
66
+ return _LANGUAGE_ALIASES.get(base, base)
67
+
68
+
69
+ def speech_consistency_unit(language: Optional[str]) -> str:
70
+ normalized = normalize_language_code(language)
71
+ return "cer" if normalized in _CER_LANGUAGES else "wer"
72
+
73
+
74
+ def whisper_language_code(language: Optional[str]) -> Optional[str]:
75
+ normalized = normalize_language_code(language)
76
+ return normalized or None
77
+
78
+
79
+ def tokenization_metadata(language: Optional[str]) -> dict:
80
+ normalized = normalize_language_code(language)
81
+ warnings = []
82
+ if not normalized:
83
+ warnings.append("empty_language_code_defaulted_to_wer")
84
+ return {
85
+ "language_policy_version": LANGUAGE_POLICY_VERSION,
86
+ "target_language": str(language or ""),
87
+ "normalized_language": normalized,
88
+ "speech_consistency_unit": speech_consistency_unit(language),
89
+ "whisper_language": whisper_language_code(language),
90
+ "warnings": warnings,
91
+ }
@@ -0,0 +1,39 @@
1
+ # This file contains code adapted from SimulEval:
2
+ # https://github.com/facebookresearch/SimulEval
3
+ #
4
+ # SimulEval is licensed under the Creative Commons Attribution-ShareAlike
5
+ # 4.0 International License (CC BY-SA 4.0):
6
+ # https://creativecommons.org/licenses/by-sa/4.0/
7
+ #
8
+ # The adapted portions in this file are distributed under CC BY-SA 4.0.
9
+ # Modifications were made for OpenSTBench.
10
+
11
+ from .agent import GenericAgent, AgentPipeline
12
+ from .basics import ReadAction, WriteAction, TextSegment, SpeechSegment
13
+
14
+ __all__ = [
15
+ "GenericAgent",
16
+ "AgentPipeline",
17
+ "ReadAction",
18
+ "WriteAction",
19
+ "TextSegment",
20
+ "SpeechSegment",
21
+ "LatencyEvaluator",
22
+ "register",
23
+ ]
24
+
25
+
26
+ def __getattr__(name):
27
+ if name == "LatencyEvaluator":
28
+ from .cli import LatencyEvaluator
29
+
30
+ return LatencyEvaluator
31
+ if name == "register":
32
+ from .metrics import register
33
+
34
+ return register
35
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
36
+
37
+
38
+ def __dir__():
39
+ return sorted(set(globals()) | set(__all__))
@@ -1,15 +1,16 @@
1
- import os
2
- import torch
3
- import torchaudio
4
- import jiwer
5
- from pathlib import Path
6
- from typing import List, Dict, Optional, Union
7
- from tqdm import tqdm
8
-
9
- # Reuse existing loading utilities
1
+ import os
2
+ import torch
3
+ import torchaudio
4
+ import jiwer
5
+ from pathlib import Path
6
+ from typing import List, Dict, Optional, Union
7
+ from tqdm import tqdm
8
+
9
+ # Reuse existing loading utilities
10
10
  from ._model_loading import resolve_pretrained_source
11
+ from .language_policy import normalize_language_code, speech_consistency_unit, whisper_language_code
11
12
  from .translation_evaluator import load_text_from_file_or_list, load_audio_from_folder
12
-
13
+
13
14
  try:
14
15
  import whisper
15
16
  except ImportError:
@@ -19,143 +20,132 @@ try:
19
20
  from opencc import OpenCC
20
21
  except ImportError:
21
22
  OpenCC = None
22
-
23
- class SpeechQualityEvaluator:
24
- DEFAULT_WHISPER_MODEL = "medium"
25
- """
26
- Speech Quality and Consistency Evaluator.
27
- Specifically designed for:
28
- 1. UTMOS (Audio naturalness / perceived quality).
29
- 2. WER/CER Consistency Calculation (compares the model's generated text with its generated audio).
30
- """
31
- def __init__(self,
32
- use_wer: bool = True,
33
- use_utmos: bool = True,
34
- whisper_model: str = DEFAULT_WHISPER_MODEL,
35
- whisper_language: Optional[str] = None,
36
- utmos_model_path: Optional[str] = None,
37
- utmos_ckpt_path: Optional[str] = None,
38
- device: Optional[str] = None):
39
-
40
- self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu")
41
- self.use_wer = use_wer
42
- self.use_utmos = use_utmos
43
-
23
+
24
+ class SpeechQualityEvaluator:
25
+ DEFAULT_WHISPER_MODEL = "medium"
26
+ """
27
+ Speech Quality and Consistency Evaluator.
28
+ Specifically designed for:
29
+ 1. UTMOS (Audio naturalness / perceived quality).
30
+ 2. WER/CER Consistency Calculation (compares the model's generated text with its generated audio).
31
+ """
32
+ def __init__(self,
33
+ use_wer: bool = True,
34
+ use_utmos: bool = True,
35
+ whisper_model: str = DEFAULT_WHISPER_MODEL,
36
+ whisper_language: Optional[str] = None,
37
+ utmos_model_path: Optional[str] = None,
38
+ utmos_ckpt_path: Optional[str] = None,
39
+ device: Optional[str] = None):
40
+
41
+ self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu")
42
+ self.use_wer = use_wer
43
+ self.use_utmos = use_utmos
44
+
44
45
  self.whisper_model_name = whisper_model
45
46
  self.whisper_language = self._normalize_whisper_language(whisper_language)
46
47
  self.utmos_path = utmos_model_path
47
48
  self.utmos_ckpt = utmos_ckpt_path
48
49
  self.zh_converter = OpenCC("t2s") if OpenCC is not None else None
49
50
  self._warned_missing_opencc = False
50
-
51
- self.whisper_model = None
52
- self.utmos_model = None
53
-
54
- if self.use_wer:
55
- self.wer_transform = jiwer.Compose([
56
- jiwer.ToLowerCase(),
57
- jiwer.RemovePunctuation(),
58
- jiwer.RemoveMultipleSpaces(),
59
- jiwer.Strip(),
60
- jiwer.RemoveEmptyStrings(),
61
- ])
62
-
51
+
52
+ self.whisper_model = None
53
+ self.utmos_model = None
54
+
55
+ if self.use_wer:
56
+ self.wer_transform = jiwer.Compose([
57
+ jiwer.ToLowerCase(),
58
+ jiwer.RemovePunctuation(),
59
+ jiwer.RemoveMultipleSpaces(),
60
+ jiwer.Strip(),
61
+ jiwer.RemoveEmptyStrings(),
62
+ ])
63
+
63
64
  @staticmethod
64
65
  def _normalize_whisper_language(language: Optional[str]) -> Optional[str]:
65
- if language is None:
66
- return None
67
- normalized = str(language).strip().lower()
68
- if normalized in {"zh", "zh-cn", "cmn", "<|cmn|>"}:
69
- return "zh"
70
- if normalized in {"en", "eng", "<|eng|>"}:
71
- return "en"
72
- if normalized in {"ja", "jp", "jpn"}:
73
- return "ja"
74
- if normalized in {"ko", "kor"}:
75
- return "ko"
76
- return normalized or None
77
-
78
- def _load_whisper(self):
79
- if self.whisper_model is None and self.use_wer:
80
- if not whisper:
81
- print("⚠️ Whisper is not installed, skipping load.")
82
- self.use_wer = False
83
- return
84
- print(f"⏳ Loading Whisper ({self.whisper_model_name})...")
85
- model_source, source_kind = resolve_pretrained_source(
86
- self.whisper_model_name,
87
- fallback_source=self.DEFAULT_WHISPER_MODEL,
88
- )
89
- print(f"Loading Whisper ({source_kind}) from {model_source}...")
90
- self.whisper_model = whisper.load_model(model_source, device=self.device)
91
-
92
- def _load_utmos(self):
93
- if self.utmos_model is None and self.use_utmos:
94
- print("⏳ Loading UTMOS model...")
95
- try:
96
- source = "github"
97
- repo_or_dir = "tarepan/SpeechMOS"
98
- if self.utmos_path and os.path.exists(self.utmos_path):
99
- source = "local"
100
- repo_or_dir = self.utmos_path
101
-
102
- load_pretrained = (self.utmos_ckpt is None)
103
- self.utmos_model = torch.hub.load(
104
- repo_or_dir, "utmos22_strong", source=source, trust_repo=True, pretrained=load_pretrained
105
- )
106
-
107
- if self.utmos_ckpt and os.path.exists(self.utmos_ckpt):
108
- state_dict = torch.load(self.utmos_ckpt, map_location=self.device)
109
- self.utmos_model.load_state_dict(state_dict)
110
-
111
- self.utmos_model.to(self.device).eval()
112
- except Exception as e:
113
- print(f"⚠️ UTMOS model failed to load: {e}")
114
- self.use_utmos = False
115
-
116
- def _transcribe(self, audio_paths: List[str]) -> List[str]:
117
- self._load_whisper()
118
- if not self.whisper_model:
119
- return [""] * len(audio_paths)
120
-
121
- transcribe_kwargs = {
122
- "fp16": torch.cuda.is_available() and "cuda" in self.device,
123
- "task": "transcribe",
124
- }
125
- if self.whisper_language:
126
- transcribe_kwargs["language"] = self.whisper_language
127
-
128
- results = []
129
- for path in tqdm(audio_paths, desc="Whisper transcription"):
130
- res = self.whisper_model.transcribe(path, **transcribe_kwargs)
131
- results.append(res["text"].strip())
132
- return results
133
-
134
- def _compute_utmos(self, audio_paths: List[str]) -> float:
135
- self._load_utmos()
136
- if not self.utmos_model: return 0.0
137
-
138
- scores = []
139
- target_sr = 16000
140
- for path in tqdm(audio_paths, desc="🎧 Computing UTMOS"):
141
- try:
142
- wave, sr = torchaudio.load(path)
143
- if sr != target_sr:
144
- wave = torchaudio.functional.resample(wave, sr, target_sr)
145
- if wave.shape[0] > 1:
146
- wave = torch.mean(wave, dim=0, keepdim=True)
147
-
148
- wave = wave.to(self.device)
149
- with torch.no_grad():
150
- s = self.utmos_model(wave, target_sr)
151
- scores.append(s.item())
152
- except Exception as e:
153
- print(f"⚠️ UTMOS Error on {path}: {e}")
154
-
155
- return sum(scores) / len(scores) if scores else 0.0
156
-
66
+ return whisper_language_code(language)
67
+
68
+ def _load_whisper(self):
69
+ if self.whisper_model is None and self.use_wer:
70
+ if not whisper:
71
+ print("⚠️ Whisper is not installed, skipping load.")
72
+ self.use_wer = False
73
+ return
74
+ print(f"⏳ Loading Whisper ({self.whisper_model_name})...")
75
+ model_source, source_kind = resolve_pretrained_source(
76
+ self.whisper_model_name,
77
+ fallback_source=self.DEFAULT_WHISPER_MODEL,
78
+ )
79
+ print(f"Loading Whisper ({source_kind}) from {model_source}...")
80
+ self.whisper_model = whisper.load_model(model_source, device=self.device)
81
+
82
+ def _load_utmos(self):
83
+ if self.utmos_model is None and self.use_utmos:
84
+ print("⏳ Loading UTMOS model...")
85
+ try:
86
+ source = "github"
87
+ repo_or_dir = "tarepan/SpeechMOS"
88
+ if self.utmos_path and os.path.exists(self.utmos_path):
89
+ source = "local"
90
+ repo_or_dir = self.utmos_path
91
+
92
+ load_pretrained = (self.utmos_ckpt is None)
93
+ self.utmos_model = torch.hub.load(
94
+ repo_or_dir, "utmos22_strong", source=source, trust_repo=True, pretrained=load_pretrained
95
+ )
96
+
97
+ if self.utmos_ckpt and os.path.exists(self.utmos_ckpt):
98
+ state_dict = torch.load(self.utmos_ckpt, map_location=self.device)
99
+ self.utmos_model.load_state_dict(state_dict)
100
+
101
+ self.utmos_model.to(self.device).eval()
102
+ except Exception as e:
103
+ print(f"⚠️ UTMOS model failed to load: {e}")
104
+ self.use_utmos = False
105
+
106
+ def _transcribe(self, audio_paths: List[str]) -> List[str]:
107
+ self._load_whisper()
108
+ if not self.whisper_model:
109
+ return [""] * len(audio_paths)
110
+
111
+ transcribe_kwargs = {
112
+ "fp16": torch.cuda.is_available() and "cuda" in self.device,
113
+ "task": "transcribe",
114
+ }
115
+ if self.whisper_language:
116
+ transcribe_kwargs["language"] = self.whisper_language
117
+
118
+ results = []
119
+ for path in tqdm(audio_paths, desc="Whisper transcription"):
120
+ res = self.whisper_model.transcribe(path, **transcribe_kwargs)
121
+ results.append(res["text"].strip())
122
+ return results
123
+
124
+ def _compute_utmos(self, audio_paths: List[str]) -> float:
125
+ self._load_utmos()
126
+ if not self.utmos_model: return 0.0
127
+
128
+ scores = []
129
+ target_sr = 16000
130
+ for path in tqdm(audio_paths, desc="🎧 Computing UTMOS"):
131
+ try:
132
+ wave, sr = torchaudio.load(path)
133
+ if sr != target_sr:
134
+ wave = torchaudio.functional.resample(wave, sr, target_sr)
135
+ if wave.shape[0] > 1:
136
+ wave = torch.mean(wave, dim=0, keepdim=True)
137
+
138
+ wave = wave.to(self.device)
139
+ with torch.no_grad():
140
+ s = self.utmos_model(wave, target_sr)
141
+ scores.append(s.item())
142
+ except Exception as e:
143
+ print(f"⚠️ UTMOS Error on {path}: {e}")
144
+
145
+ return sum(scores) / len(scores) if scores else 0.0
146
+
157
147
  def _preprocess_for_wer(self, text: str, lang: str) -> str:
158
- normalized_lang = self._normalize_whisper_language(lang) or str(lang).strip().lower()
148
+ normalized_lang = normalize_language_code(lang)
159
149
  text = self.wer_transform(text)
160
150
  if normalized_lang == "zh":
161
151
  if self.zh_converter is not None:
@@ -163,55 +153,54 @@ class SpeechQualityEvaluator:
163
153
  elif not self._warned_missing_opencc:
164
154
  print("Warning: OpenCC is not installed; Chinese CER will not normalize traditional/simplified variants.")
165
155
  self._warned_missing_opencc = True
166
- if normalized_lang in ["zh", "ja", "ko"]:
156
+ if speech_consistency_unit(normalized_lang) == "cer":
167
157
  text = text.replace(" ", "")
168
158
  return " ".join(list(text))
169
159
  return text
170
-
171
- def evaluate_all(self,
172
- target_audio: Union[List[str], str],
173
- target_text: Optional[Union[List[str], str]] = None,
174
- target_lang: str = "en") -> Dict[str, float]:
175
- """
176
- :param target_audio: List of paths or directory to the model-generated audio.
177
- :param target_text: Text generated synchronously by the model, used as reference for WER/CER calculation.
178
- :param target_lang: Target language for both audio and text.
179
- """
180
- results = {}
181
- print(f"\n--- Starting Speech Quality Evaluation (Target Lang: {target_lang}) ---")
182
-
183
- # Load target audio
184
- if isinstance(target_audio, str) and os.path.exists(target_audio) and os.path.isdir(target_audio):
185
- audio_paths = load_audio_from_folder(target_audio)
186
- else:
187
- audio_paths = target_audio if isinstance(target_audio, list) else [target_audio]
188
-
189
- # 1. evaluate UTMOS
190
- if self.use_utmos:
191
- print(" ➤ Computing UTMOS (Audio Naturalness)...")
192
- results["UTMOS"] = round(self._compute_utmos(audio_paths), 4)
193
-
194
- # 2. evaluate Consistency WER/CER
195
- if self.use_wer:
196
- if not target_text:
197
- print(" ⚠️ target_text (synchronously generated text) not provided. Text-speech consistency cannot be evaluated, skipping WER/CER.")
198
- else:
199
- texts = load_text_from_file_or_list(target_text, "target_text")
200
- if len(texts) != len(audio_paths):
201
- raise ValueError(f"Number of generated text lines ({len(texts)}) does not match number of audio files ({len(audio_paths)})!")
202
-
203
- print(" ➤ Transcribing audio, preparing consistency calculation...")
204
- asr_texts = self._transcribe(audio_paths)
205
-
206
- clean_refs = [self._preprocess_for_wer(t, target_lang) for t in texts]
207
- clean_hyps = [self._preprocess_for_wer(t, target_lang) for t in asr_texts]
208
-
160
+
161
+ def evaluate_all(self,
162
+ target_audio: Union[List[str], str],
163
+ target_text: Optional[Union[List[str], str]] = None,
164
+ target_lang: str = "en") -> Dict[str, float]:
165
+ """
166
+ :param target_audio: List of paths or directory to the model-generated audio.
167
+ :param target_text: Text generated synchronously by the model, used as reference for WER/CER calculation.
168
+ :param target_lang: Target language for both audio and text.
169
+ """
170
+ results = {}
171
+ print(f"\n--- Starting Speech Quality Evaluation (Target Lang: {target_lang}) ---")
172
+
173
+ # Load target audio
174
+ if isinstance(target_audio, str) and os.path.exists(target_audio) and os.path.isdir(target_audio):
175
+ audio_paths = load_audio_from_folder(target_audio)
176
+ else:
177
+ audio_paths = target_audio if isinstance(target_audio, list) else [target_audio]
178
+
179
+ # 1. evaluate UTMOS
180
+ if self.use_utmos:
181
+ print(" ➤ Computing UTMOS (Audio Naturalness)...")
182
+ results["UTMOS"] = round(self._compute_utmos(audio_paths), 4)
183
+
184
+ # 2. evaluate Consistency WER/CER
185
+ if self.use_wer:
186
+ if not target_text:
187
+ print(" ⚠️ target_text (synchronously generated text) not provided. Text-speech consistency cannot be evaluated, skipping WER/CER.")
188
+ else:
189
+ texts = load_text_from_file_or_list(target_text, "target_text")
190
+ if len(texts) != len(audio_paths):
191
+ raise ValueError(f"Number of generated text lines ({len(texts)}) does not match number of audio files ({len(audio_paths)})!")
192
+
193
+ print(" ➤ Transcribing audio, preparing consistency calculation...")
194
+ asr_texts = self._transcribe(audio_paths)
195
+
196
+ clean_refs = [self._preprocess_for_wer(t, target_lang) for t in texts]
197
+ clean_hyps = [self._preprocess_for_wer(t, target_lang) for t in asr_texts]
198
+
209
199
  if clean_refs:
210
200
  error_rate = jiwer.wer(clean_refs, clean_hyps)
211
- normalized_target_lang = self._normalize_whisper_language(target_lang) or str(target_lang).strip().lower()
212
- metric_name = "CER_Consistency" if normalized_target_lang in ['zh', 'ja', 'ko'] else "WER_Consistency"
201
+ metric_name = "CER_Consistency" if speech_consistency_unit(target_lang) == "cer" else "WER_Consistency"
213
202
  results[metric_name] = round(error_rate, 4)
214
- else:
215
- print(" ⚠️ The provided generated text is empty after cleaning, cannot compute distribution error rate.")
216
-
217
- return results
203
+ else:
204
+ print(" ⚠️ The provided generated text is empty after cleaning, cannot compute distribution error rate.")
205
+
206
+ return results
@@ -84,8 +84,9 @@ def load_audio_from_folder(folder_path: str, extensions: tuple = (".wav", ".mp3"
84
84
 
85
85
  # ==================== Evaluator Core Class ====================
86
86
 
87
- DEFAULT_COMET_MODEL = "Unbabel/wmt22-comet-da"
87
+ DEFAULT_COMET_MODEL = "Unbabel/wmt22-cometkiwi-da"
88
88
  DEFAULT_BLEURT_MODEL = "lucadiliello/BLEURT-20"
89
+ COMET_QE_METRIC_NAME = "COMETKiwi"
89
90
 
90
91
  class TranslationEvaluator:
91
92
  """
@@ -237,7 +238,7 @@ class TranslationEvaluator:
237
238
  Args:
238
239
  reference: Ground truth benchmark translation reference from the dataset (List or file path).
239
240
  target_text: Direct text translation output from the translation model (List or file path).
240
- source: Optional original source text (mandatory for COMET).
241
+ source: Optional original source text (mandatory for COMETKiwi/QE).
241
242
  target_lang: Target language, affects BLEU Tokenizer selection.
242
243
  """
243
244
  results = {}
@@ -276,13 +277,19 @@ class TranslationEvaluator:
276
277
  except Exception as e:
277
278
  results["BLEURT"] = -1.0
278
279
 
279
- # 4. COMET
280
- if self.use_comet and self.comet and final_src:
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:
283
+ if not final_src:
284
+ 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")
281
288
  try:
282
- data = [{"src": s, "mt": t, "ref": r} for s, t, r in zip(final_src, final_text, final_ref)]
289
+ data = [{"src": s, "mt": t} for s, t in zip(final_src, final_text)]
283
290
  gpus = 1 if self.device.startswith("cuda") else 0
284
- results["COMET"] = self.comet.predict(data, batch_size=8, gpus=gpus).system_score
291
+ results[COMET_QE_METRIC_NAME] = self.comet.predict(data, batch_size=8, gpus=gpus).system_score
285
292
  except Exception as e:
286
- results["COMET"] = -1.0
293
+ results[COMET_QE_METRIC_NAME] = -1.0
287
294
 
288
295
  return {k: round(v, 4) if v >= 0 else v for k, v in results.items()}
@@ -1,14 +0,0 @@
1
- # This file contains code adapted from SimulEval:
2
- # https://github.com/facebookresearch/SimulEval
3
- #
4
- # SimulEval is licensed under the Creative Commons Attribution-ShareAlike
5
- # 4.0 International License (CC BY-SA 4.0):
6
- # https://creativecommons.org/licenses/by-sa/4.0/
7
- #
8
- # The adapted portions in this file are distributed under CC BY-SA 4.0.
9
- # Modifications were made for OpenSTBench.
10
-
11
- from .agent import GenericAgent, AgentPipeline
12
- from .basics import ReadAction, WriteAction, TextSegment, SpeechSegment
13
- from .cli import LatencyEvaluator
14
- from .metrics import register # Allows users to register custom Scorers
File without changes
File without changes
File without changes