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.
- {openstbench-1.2.0/src/OpenSTBench.egg-info → openstbench-1.2.2}/PKG-INFO +1 -1
- {openstbench-1.2.0 → openstbench-1.2.2}/pyproject.toml +1 -1
- {openstbench-1.2.0 → openstbench-1.2.2/src/OpenSTBench.egg-info}/PKG-INFO +1 -1
- {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/SOURCES.txt +1 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/__init__.py +4 -2
- openstbench-1.2.2/src/openstbench/language_policy.py +91 -0
- openstbench-1.2.2/src/openstbench/latency/__init__.py +39 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/speech_quality_evaluator.py +173 -184
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/translation_evaluator.py +14 -7
- openstbench-1.2.0/src/openstbench/latency/__init__.py +0 -14
- {openstbench-1.2.0 → openstbench-1.2.2}/LICENSE +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/README.md +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/setup.cfg +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/dependency_links.txt +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/requires.txt +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/OpenSTBench.egg-info/top_level.txt +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/_model_loading.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/emotion_evaluator.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/agent.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/basics.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/cli.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/instance.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/metrics.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/latency/utils.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/paralinguistic_evaluator.py +0 -0
- {openstbench-1.2.0 → openstbench-1.2.2}/src/openstbench/speaker_similarity_evaluator.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if self.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for path in tqdm(audio_paths, desc="
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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.
|
|
280
|
-
|
|
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
|
|
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[
|
|
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[
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|