OpenSTBench 0.2.0__tar.gz → 0.3.1__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-0.2.0 → openstbench-0.3.1}/PKG-INFO +36 -5
- {openstbench-0.2.0 → openstbench-0.3.1}/README.md +32 -1
- {openstbench-0.2.0 → openstbench-0.3.1}/pyproject.toml +4 -4
- {openstbench-0.2.0 → openstbench-0.3.1}/src/OpenSTBench.egg-info/PKG-INFO +36 -5
- {openstbench-0.2.0 → openstbench-0.3.1}/src/OpenSTBench.egg-info/SOURCES.txt +2 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/OpenSTBench.egg-info/requires.txt +2 -2
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/__init__.py +3 -1
- openstbench-0.3.1/src/openstbench/_model_loading.py +62 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/emotion_evaluator.py +9 -2
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/cli.py +3 -3
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/utils.py +7 -1
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/paralinguistic_evaluator.py +20 -7
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/speaker_similarity_evaluator.py +13 -5
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/speech_quality_evaluator.py +10 -3
- openstbench-0.3.1/src/openstbench/temporal_consistency_evaluator.py +219 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/translation_evaluator.py +44 -7
- {openstbench-0.2.0 → openstbench-0.3.1}/setup.cfg +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/OpenSTBench.egg-info/dependency_links.txt +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/OpenSTBench.egg-info/top_level.txt +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/dataset.py +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/__init__.py +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/agent.py +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/basics.py +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/instance.py +0 -0
- {openstbench-0.2.0 → openstbench-0.3.1}/src/openstbench/latency/metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenSTBench
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Open speech and translation benchmarking toolkit supporting MT, ASR, TTS, SimulST, VC, and paralinguistics with optimized CJK language support
|
|
5
5
|
Author-email: Yanjie An <691476922@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -14,10 +14,10 @@ Classifier: Intended Audience :: Science/Research
|
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Requires-Python:
|
|
17
|
+
Requires-Python: <3.11,>=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
-
Requires-Dist: torch
|
|
20
|
-
Requires-Dist: torchaudio
|
|
19
|
+
Requires-Dist: torch<2.7,>=2.6
|
|
20
|
+
Requires-Dist: torchaudio<2.7,>=2.6
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: sacrebleu>=2.0.0
|
|
23
23
|
Requires-Dist: pandas
|
|
@@ -65,6 +65,7 @@ This project is best suited for these directions:
|
|
|
65
65
|
- S2ST evaluation by combining text quality, speech quality, speaker similarity, and latency
|
|
66
66
|
- Streaming or simultaneous speech translation latency evaluation with a custom agent
|
|
67
67
|
- Preservation analysis for speech translation outputs, including speaker similarity, emotion, and paralinguistic similarity
|
|
68
|
+
- Temporal consistency analysis for speech translation or dubbing outputs, including duration compliance and duration error
|
|
68
69
|
|
|
69
70
|
## Core Modules
|
|
70
71
|
|
|
@@ -75,6 +76,7 @@ This project is best suited for these directions:
|
|
|
75
76
|
| `SpeakerSimilarityEvaluator` | Speaker preservation | `wavlm_similarity`, `resemblyzer_similarity` |
|
|
76
77
|
| `EmotionEvaluator` | Emotion preservation or classification accuracy | `Emotion2Vec_Cosine_Similarity`, `Audio_Emotion_Accuracy` |
|
|
77
78
|
| `ParalinguisticEvaluator` | Non-verbal and paralinguistic preservation | `Paralinguistic_Fidelity_Cosine`, `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, `Acoustic_Event_Preservation_Macro_Recall`, `Event_Aligned_Preservation_Rate`, `Conditional_Relative_Onset_Error` |
|
|
79
|
+
| `TemporalConsistencyEvaluator` | Source-target temporal structure consistency | `Duration_Consistency_SLC_0.2`, `Duration_Consistency_SLC_0.4` |
|
|
78
80
|
| `LatencyEvaluator` | Streaming / simultaneous translation latency | `StartOffset`, `ATD`, `CustomATD`, `RTF`, `Model_Generate_RTF` |
|
|
79
81
|
|
|
80
82
|
## Installation
|
|
@@ -119,7 +121,11 @@ openstbench
|
|
|
119
121
|
Example:
|
|
120
122
|
|
|
121
123
|
```python
|
|
122
|
-
from openstbench import
|
|
124
|
+
from openstbench import (
|
|
125
|
+
TranslationEvaluator,
|
|
126
|
+
SpeechQualityEvaluator,
|
|
127
|
+
TemporalConsistencyEvaluator,
|
|
128
|
+
)
|
|
123
129
|
```
|
|
124
130
|
|
|
125
131
|
## Quick Start
|
|
@@ -134,6 +140,7 @@ Python examples:
|
|
|
134
140
|
- `examples/python/emotion_eval.py`
|
|
135
141
|
- `examples/python/paralinguistic_eval.py`
|
|
136
142
|
- `examples/python/paralinguistic_identity_baseline.py`
|
|
143
|
+
- `examples/python/temporal_consistency_eval.py`
|
|
137
144
|
- `examples/python/latency_eval.py`
|
|
138
145
|
|
|
139
146
|
Shell examples:
|
|
@@ -141,6 +148,23 @@ Shell examples:
|
|
|
141
148
|
- `examples/bash/install_extras.sh`
|
|
142
149
|
- `examples/bash/run_latency_cli.sh`
|
|
143
150
|
|
|
151
|
+
Minimal temporal consistency example:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from openstbench import TemporalConsistencyEvaluator
|
|
155
|
+
|
|
156
|
+
evaluator = TemporalConsistencyEvaluator(
|
|
157
|
+
thresholds=(0.2, 0.4),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
results, diagnostics = evaluator.evaluate_all(
|
|
161
|
+
source_audio="./source_wavs",
|
|
162
|
+
target_audio="./generated_wavs",
|
|
163
|
+
sample_ids=["sample_1", "sample_2"],
|
|
164
|
+
return_diagnostics=True,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
144
168
|
Latency output distinguishes two RTF variants:
|
|
145
169
|
|
|
146
170
|
- `Real_Time_Factor_(RTF)`: system-level RTF. This includes agent policy overhead, pre/post-processing, and other runtime costs around model inference.
|
|
@@ -166,6 +190,8 @@ Common audio inputs support:
|
|
|
166
190
|
- For `zh` / `ja` / `ko`, the toolkit uses CJK-aware handling for text-side evaluation.
|
|
167
191
|
- `SpeechQualityEvaluator` returns `CER_Consistency` for `zh` / `ja` / `ko`, and `WER_Consistency` for most other languages.
|
|
168
192
|
- `ParalinguisticEvaluator` always supports `Paralinguistic_Fidelity_Cosine`, a continuous CLAP-based audio similarity score between source and target speech.
|
|
193
|
+
- `TemporalConsistencyEvaluator` supports `List[str]`, audio folders, `.txt` path lists, and `.json` path lists for both `source_audio` and `target_audio`.
|
|
194
|
+
- `TemporalConsistencyEvaluator` reports thresholded duration compliance metrics (`Duration_Consistency_SLC_*`).
|
|
169
195
|
- The discrete preservation branch is an utterance-level single-label task. With source-side gold labels, it reports `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, and `Acoustic_Event_Preservation_Macro_Recall`.
|
|
170
196
|
- If `source_onsets_ms` are available, the evaluator can also report alignment-aware metrics: `Event_Aligned_Preservation_Rate` and `Conditional_Relative_Onset_Error`.
|
|
171
197
|
- Alignment is computed on relative onset position, not absolute wall-clock time. This makes it suitable for cross-lingual S2ST where source and target utterance durations naturally differ.
|
|
@@ -176,6 +202,11 @@ Common audio inputs support:
|
|
|
176
202
|
- The default event localizer is also replaceable. Custom localizers only need to implement `localize(audio_paths, labels, candidate_labels)`.
|
|
177
203
|
- Dataset-specific label mapping is intentionally outside the core package. Pass `candidate_labels` and `label_normalizer` at call time so the same evaluator works across datasets without changing core code.
|
|
178
204
|
- For offline environments, `clap_model_path` accepts either a Hugging Face repo id or a local model directory or snapshot.
|
|
205
|
+
- Model-loading parameters such as `clap_model_path`, `wavlm_model_path`, `whisper_model`, `e2v_model_path`, `comet_model`, and `bleurt_path` now use a consistent local-first rule: if the supplied local path exists, it is used; otherwise the evaluator falls back to the default remote model id.
|
|
179
206
|
- In S2S latency evaluation, alignment prefers the model's native transcript when available. If the model is audio-only, the evaluator can optionally use ASR fallback to prepare alignment text.
|
|
180
207
|
- For S2S forced alignment, pass language-appropriate MFA models through `alignment_acoustic_model` and `alignment_dictionary_model`. The defaults are English.
|
|
181
208
|
- Some modules rely on optional dependencies or local model paths in offline environments.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT License
|
|
@@ -16,6 +16,7 @@ This project is best suited for these directions:
|
|
|
16
16
|
- S2ST evaluation by combining text quality, speech quality, speaker similarity, and latency
|
|
17
17
|
- Streaming or simultaneous speech translation latency evaluation with a custom agent
|
|
18
18
|
- Preservation analysis for speech translation outputs, including speaker similarity, emotion, and paralinguistic similarity
|
|
19
|
+
- Temporal consistency analysis for speech translation or dubbing outputs, including duration compliance and duration error
|
|
19
20
|
|
|
20
21
|
## Core Modules
|
|
21
22
|
|
|
@@ -26,6 +27,7 @@ This project is best suited for these directions:
|
|
|
26
27
|
| `SpeakerSimilarityEvaluator` | Speaker preservation | `wavlm_similarity`, `resemblyzer_similarity` |
|
|
27
28
|
| `EmotionEvaluator` | Emotion preservation or classification accuracy | `Emotion2Vec_Cosine_Similarity`, `Audio_Emotion_Accuracy` |
|
|
28
29
|
| `ParalinguisticEvaluator` | Non-verbal and paralinguistic preservation | `Paralinguistic_Fidelity_Cosine`, `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, `Acoustic_Event_Preservation_Macro_Recall`, `Event_Aligned_Preservation_Rate`, `Conditional_Relative_Onset_Error` |
|
|
30
|
+
| `TemporalConsistencyEvaluator` | Source-target temporal structure consistency | `Duration_Consistency_SLC_0.2`, `Duration_Consistency_SLC_0.4` |
|
|
29
31
|
| `LatencyEvaluator` | Streaming / simultaneous translation latency | `StartOffset`, `ATD`, `CustomATD`, `RTF`, `Model_Generate_RTF` |
|
|
30
32
|
|
|
31
33
|
## Installation
|
|
@@ -70,7 +72,11 @@ openstbench
|
|
|
70
72
|
Example:
|
|
71
73
|
|
|
72
74
|
```python
|
|
73
|
-
from openstbench import
|
|
75
|
+
from openstbench import (
|
|
76
|
+
TranslationEvaluator,
|
|
77
|
+
SpeechQualityEvaluator,
|
|
78
|
+
TemporalConsistencyEvaluator,
|
|
79
|
+
)
|
|
74
80
|
```
|
|
75
81
|
|
|
76
82
|
## Quick Start
|
|
@@ -85,6 +91,7 @@ Python examples:
|
|
|
85
91
|
- `examples/python/emotion_eval.py`
|
|
86
92
|
- `examples/python/paralinguistic_eval.py`
|
|
87
93
|
- `examples/python/paralinguistic_identity_baseline.py`
|
|
94
|
+
- `examples/python/temporal_consistency_eval.py`
|
|
88
95
|
- `examples/python/latency_eval.py`
|
|
89
96
|
|
|
90
97
|
Shell examples:
|
|
@@ -92,6 +99,23 @@ Shell examples:
|
|
|
92
99
|
- `examples/bash/install_extras.sh`
|
|
93
100
|
- `examples/bash/run_latency_cli.sh`
|
|
94
101
|
|
|
102
|
+
Minimal temporal consistency example:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from openstbench import TemporalConsistencyEvaluator
|
|
106
|
+
|
|
107
|
+
evaluator = TemporalConsistencyEvaluator(
|
|
108
|
+
thresholds=(0.2, 0.4),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
results, diagnostics = evaluator.evaluate_all(
|
|
112
|
+
source_audio="./source_wavs",
|
|
113
|
+
target_audio="./generated_wavs",
|
|
114
|
+
sample_ids=["sample_1", "sample_2"],
|
|
115
|
+
return_diagnostics=True,
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
95
119
|
Latency output distinguishes two RTF variants:
|
|
96
120
|
|
|
97
121
|
- `Real_Time_Factor_(RTF)`: system-level RTF. This includes agent policy overhead, pre/post-processing, and other runtime costs around model inference.
|
|
@@ -117,6 +141,8 @@ Common audio inputs support:
|
|
|
117
141
|
- For `zh` / `ja` / `ko`, the toolkit uses CJK-aware handling for text-side evaluation.
|
|
118
142
|
- `SpeechQualityEvaluator` returns `CER_Consistency` for `zh` / `ja` / `ko`, and `WER_Consistency` for most other languages.
|
|
119
143
|
- `ParalinguisticEvaluator` always supports `Paralinguistic_Fidelity_Cosine`, a continuous CLAP-based audio similarity score between source and target speech.
|
|
144
|
+
- `TemporalConsistencyEvaluator` supports `List[str]`, audio folders, `.txt` path lists, and `.json` path lists for both `source_audio` and `target_audio`.
|
|
145
|
+
- `TemporalConsistencyEvaluator` reports thresholded duration compliance metrics (`Duration_Consistency_SLC_*`).
|
|
120
146
|
- The discrete preservation branch is an utterance-level single-label task. With source-side gold labels, it reports `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, and `Acoustic_Event_Preservation_Macro_Recall`.
|
|
121
147
|
- If `source_onsets_ms` are available, the evaluator can also report alignment-aware metrics: `Event_Aligned_Preservation_Rate` and `Conditional_Relative_Onset_Error`.
|
|
122
148
|
- Alignment is computed on relative onset position, not absolute wall-clock time. This makes it suitable for cross-lingual S2ST where source and target utterance durations naturally differ.
|
|
@@ -127,6 +153,11 @@ Common audio inputs support:
|
|
|
127
153
|
- The default event localizer is also replaceable. Custom localizers only need to implement `localize(audio_paths, labels, candidate_labels)`.
|
|
128
154
|
- Dataset-specific label mapping is intentionally outside the core package. Pass `candidate_labels` and `label_normalizer` at call time so the same evaluator works across datasets without changing core code.
|
|
129
155
|
- For offline environments, `clap_model_path` accepts either a Hugging Face repo id or a local model directory or snapshot.
|
|
156
|
+
- Model-loading parameters such as `clap_model_path`, `wavlm_model_path`, `whisper_model`, `e2v_model_path`, `comet_model`, and `bleurt_path` now use a consistent local-first rule: if the supplied local path exists, it is used; otherwise the evaluator falls back to the default remote model id.
|
|
130
157
|
- In S2S latency evaluation, alignment prefers the model's native transcript when available. If the model is audio-only, the evaluator can optionally use ASR fallback to prepare alignment text.
|
|
131
158
|
- For S2S forced alignment, pass language-appropriate MFA models through `alignment_acoustic_model` and `alignment_dictionary_model`. The defaults are English.
|
|
132
159
|
- Some modules rely on optional dependencies or local model paths in offline environments.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT License
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "OpenSTBench"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Open speech and translation benchmarking toolkit supporting MT, ASR, TTS, SimulST, VC, and paralinguistics with optimized CJK language support"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.9,<3.11"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Yanjie An", email = "691476922@qq.com"}
|
|
@@ -29,8 +29,8 @@ classifiers = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
32
|
-
"torch>=
|
|
33
|
-
"torchaudio>=
|
|
32
|
+
"torch>=2.6,<2.7",
|
|
33
|
+
"torchaudio>=2.6,<2.7",
|
|
34
34
|
"numpy",
|
|
35
35
|
"sacrebleu>=2.0.0",
|
|
36
36
|
"pandas",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenSTBench
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Open speech and translation benchmarking toolkit supporting MT, ASR, TTS, SimulST, VC, and paralinguistics with optimized CJK language support
|
|
5
5
|
Author-email: Yanjie An <691476922@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -14,10 +14,10 @@ Classifier: Intended Audience :: Science/Research
|
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Requires-Python:
|
|
17
|
+
Requires-Python: <3.11,>=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
-
Requires-Dist: torch
|
|
20
|
-
Requires-Dist: torchaudio
|
|
19
|
+
Requires-Dist: torch<2.7,>=2.6
|
|
20
|
+
Requires-Dist: torchaudio<2.7,>=2.6
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: sacrebleu>=2.0.0
|
|
23
23
|
Requires-Dist: pandas
|
|
@@ -65,6 +65,7 @@ This project is best suited for these directions:
|
|
|
65
65
|
- S2ST evaluation by combining text quality, speech quality, speaker similarity, and latency
|
|
66
66
|
- Streaming or simultaneous speech translation latency evaluation with a custom agent
|
|
67
67
|
- Preservation analysis for speech translation outputs, including speaker similarity, emotion, and paralinguistic similarity
|
|
68
|
+
- Temporal consistency analysis for speech translation or dubbing outputs, including duration compliance and duration error
|
|
68
69
|
|
|
69
70
|
## Core Modules
|
|
70
71
|
|
|
@@ -75,6 +76,7 @@ This project is best suited for these directions:
|
|
|
75
76
|
| `SpeakerSimilarityEvaluator` | Speaker preservation | `wavlm_similarity`, `resemblyzer_similarity` |
|
|
76
77
|
| `EmotionEvaluator` | Emotion preservation or classification accuracy | `Emotion2Vec_Cosine_Similarity`, `Audio_Emotion_Accuracy` |
|
|
77
78
|
| `ParalinguisticEvaluator` | Non-verbal and paralinguistic preservation | `Paralinguistic_Fidelity_Cosine`, `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, `Acoustic_Event_Preservation_Macro_Recall`, `Event_Aligned_Preservation_Rate`, `Conditional_Relative_Onset_Error` |
|
|
79
|
+
| `TemporalConsistencyEvaluator` | Source-target temporal structure consistency | `Duration_Consistency_SLC_0.2`, `Duration_Consistency_SLC_0.4` |
|
|
78
80
|
| `LatencyEvaluator` | Streaming / simultaneous translation latency | `StartOffset`, `ATD`, `CustomATD`, `RTF`, `Model_Generate_RTF` |
|
|
79
81
|
|
|
80
82
|
## Installation
|
|
@@ -119,7 +121,11 @@ openstbench
|
|
|
119
121
|
Example:
|
|
120
122
|
|
|
121
123
|
```python
|
|
122
|
-
from openstbench import
|
|
124
|
+
from openstbench import (
|
|
125
|
+
TranslationEvaluator,
|
|
126
|
+
SpeechQualityEvaluator,
|
|
127
|
+
TemporalConsistencyEvaluator,
|
|
128
|
+
)
|
|
123
129
|
```
|
|
124
130
|
|
|
125
131
|
## Quick Start
|
|
@@ -134,6 +140,7 @@ Python examples:
|
|
|
134
140
|
- `examples/python/emotion_eval.py`
|
|
135
141
|
- `examples/python/paralinguistic_eval.py`
|
|
136
142
|
- `examples/python/paralinguistic_identity_baseline.py`
|
|
143
|
+
- `examples/python/temporal_consistency_eval.py`
|
|
137
144
|
- `examples/python/latency_eval.py`
|
|
138
145
|
|
|
139
146
|
Shell examples:
|
|
@@ -141,6 +148,23 @@ Shell examples:
|
|
|
141
148
|
- `examples/bash/install_extras.sh`
|
|
142
149
|
- `examples/bash/run_latency_cli.sh`
|
|
143
150
|
|
|
151
|
+
Minimal temporal consistency example:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from openstbench import TemporalConsistencyEvaluator
|
|
155
|
+
|
|
156
|
+
evaluator = TemporalConsistencyEvaluator(
|
|
157
|
+
thresholds=(0.2, 0.4),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
results, diagnostics = evaluator.evaluate_all(
|
|
161
|
+
source_audio="./source_wavs",
|
|
162
|
+
target_audio="./generated_wavs",
|
|
163
|
+
sample_ids=["sample_1", "sample_2"],
|
|
164
|
+
return_diagnostics=True,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
144
168
|
Latency output distinguishes two RTF variants:
|
|
145
169
|
|
|
146
170
|
- `Real_Time_Factor_(RTF)`: system-level RTF. This includes agent policy overhead, pre/post-processing, and other runtime costs around model inference.
|
|
@@ -166,6 +190,8 @@ Common audio inputs support:
|
|
|
166
190
|
- For `zh` / `ja` / `ko`, the toolkit uses CJK-aware handling for text-side evaluation.
|
|
167
191
|
- `SpeechQualityEvaluator` returns `CER_Consistency` for `zh` / `ja` / `ko`, and `WER_Consistency` for most other languages.
|
|
168
192
|
- `ParalinguisticEvaluator` always supports `Paralinguistic_Fidelity_Cosine`, a continuous CLAP-based audio similarity score between source and target speech.
|
|
193
|
+
- `TemporalConsistencyEvaluator` supports `List[str]`, audio folders, `.txt` path lists, and `.json` path lists for both `source_audio` and `target_audio`.
|
|
194
|
+
- `TemporalConsistencyEvaluator` reports thresholded duration compliance metrics (`Duration_Consistency_SLC_*`).
|
|
169
195
|
- The discrete preservation branch is an utterance-level single-label task. With source-side gold labels, it reports `Acoustic_Event_Preservation_Rate`, `Acoustic_Event_Preservation_Macro_F1`, and `Acoustic_Event_Preservation_Macro_Recall`.
|
|
170
196
|
- If `source_onsets_ms` are available, the evaluator can also report alignment-aware metrics: `Event_Aligned_Preservation_Rate` and `Conditional_Relative_Onset_Error`.
|
|
171
197
|
- Alignment is computed on relative onset position, not absolute wall-clock time. This makes it suitable for cross-lingual S2ST where source and target utterance durations naturally differ.
|
|
@@ -176,6 +202,11 @@ Common audio inputs support:
|
|
|
176
202
|
- The default event localizer is also replaceable. Custom localizers only need to implement `localize(audio_paths, labels, candidate_labels)`.
|
|
177
203
|
- Dataset-specific label mapping is intentionally outside the core package. Pass `candidate_labels` and `label_normalizer` at call time so the same evaluator works across datasets without changing core code.
|
|
178
204
|
- For offline environments, `clap_model_path` accepts either a Hugging Face repo id or a local model directory or snapshot.
|
|
205
|
+
- Model-loading parameters such as `clap_model_path`, `wavlm_model_path`, `whisper_model`, `e2v_model_path`, `comet_model`, and `bleurt_path` now use a consistent local-first rule: if the supplied local path exists, it is used; otherwise the evaluator falls back to the default remote model id.
|
|
179
206
|
- In S2S latency evaluation, alignment prefers the model's native transcript when available. If the model is audio-only, the evaluator can optionally use ASR fallback to prepare alignment text.
|
|
180
207
|
- For S2S forced alignment, pass language-appropriate MFA models through `alignment_acoustic_model` and `alignment_dictionary_model`. The defaults are English.
|
|
181
208
|
- Some modules rely on optional dependencies or local model paths in offline environments.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT License
|
|
@@ -6,11 +6,13 @@ src/OpenSTBench.egg-info/dependency_links.txt
|
|
|
6
6
|
src/OpenSTBench.egg-info/requires.txt
|
|
7
7
|
src/OpenSTBench.egg-info/top_level.txt
|
|
8
8
|
src/openstbench/__init__.py
|
|
9
|
+
src/openstbench/_model_loading.py
|
|
9
10
|
src/openstbench/dataset.py
|
|
10
11
|
src/openstbench/emotion_evaluator.py
|
|
11
12
|
src/openstbench/paralinguistic_evaluator.py
|
|
12
13
|
src/openstbench/speaker_similarity_evaluator.py
|
|
13
14
|
src/openstbench/speech_quality_evaluator.py
|
|
15
|
+
src/openstbench/temporal_consistency_evaluator.py
|
|
14
16
|
src/openstbench/translation_evaluator.py
|
|
15
17
|
src/openstbench/latency/__init__.py
|
|
16
18
|
src/openstbench/latency/agent.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__ = "0.
|
|
12
|
+
__version__ = "0.3.1"
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
15
|
"TranslationEvaluator",
|
|
@@ -31,6 +31,7 @@ __all__ = [
|
|
|
31
31
|
"build_paralinguistic_inputs",
|
|
32
32
|
"evaluate_paralinguistic_dataset",
|
|
33
33
|
"SpeechQualityEvaluator",
|
|
34
|
+
"TemporalConsistencyEvaluator",
|
|
34
35
|
"LatencyEvaluator",
|
|
35
36
|
"SpeakerSimilarityEvaluator",
|
|
36
37
|
"GenericAgent",
|
|
@@ -79,6 +80,7 @@ _EXPORT_SPECS: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
|
|
|
79
80
|
),
|
|
80
81
|
),
|
|
81
82
|
("speech_quality_evaluator", ("SpeechQualityEvaluator",)),
|
|
83
|
+
("temporal_consistency_evaluator", ("TemporalConsistencyEvaluator",)),
|
|
82
84
|
("speaker_similarity_evaluator", ("SpeakerSimilarityEvaluator",)),
|
|
83
85
|
(
|
|
84
86
|
"dataset",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_LOCAL_MODEL_FILE_SUFFIXES = {
|
|
6
|
+
".bin",
|
|
7
|
+
".ckpt",
|
|
8
|
+
".onnx",
|
|
9
|
+
".pb",
|
|
10
|
+
".pt",
|
|
11
|
+
".pth",
|
|
12
|
+
".safetensors",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_explicit_local_reference(model_source: str) -> bool:
|
|
17
|
+
normalized = model_source.strip().replace("\\", "/")
|
|
18
|
+
if not normalized:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
if normalized.startswith(("./", "../", "~/", "/")):
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
if len(normalized) >= 3 and normalized[1] == ":" and normalized[2] == "/":
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
if "\\" in model_source:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
return Path(model_source).suffix.lower() in _LOCAL_MODEL_FILE_SUFFIXES
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_pretrained_source(
|
|
34
|
+
preferred_source: Optional[str],
|
|
35
|
+
*,
|
|
36
|
+
fallback_source: Optional[str] = None,
|
|
37
|
+
) -> Tuple[str, str]:
|
|
38
|
+
"""Resolve a model source with local-first behavior.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple[str, str]: (resolved_source, source_kind), where source_kind is
|
|
42
|
+
either "local" or "remote".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
if preferred_source:
|
|
46
|
+
candidate = Path(preferred_source).expanduser()
|
|
47
|
+
if candidate.exists():
|
|
48
|
+
return str(candidate.resolve()), "local"
|
|
49
|
+
|
|
50
|
+
if not _is_explicit_local_reference(preferred_source):
|
|
51
|
+
return preferred_source, "remote"
|
|
52
|
+
|
|
53
|
+
if fallback_source:
|
|
54
|
+
fallback_candidate = Path(fallback_source).expanduser()
|
|
55
|
+
if fallback_candidate.exists():
|
|
56
|
+
return str(fallback_candidate.resolve()), "local"
|
|
57
|
+
return fallback_source, "remote"
|
|
58
|
+
|
|
59
|
+
if preferred_source:
|
|
60
|
+
return preferred_source, "remote"
|
|
61
|
+
|
|
62
|
+
raise ValueError("At least one model source must be provided.")
|
|
@@ -7,6 +7,8 @@ from typing import List, Dict, Optional, Union
|
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
from numpy.linalg import norm
|
|
9
9
|
|
|
10
|
+
from ._model_loading import resolve_pretrained_source
|
|
11
|
+
|
|
10
12
|
def _load_data_list(
|
|
11
13
|
input_data: Union[str, List[str]],
|
|
12
14
|
name: str,
|
|
@@ -97,7 +99,12 @@ class EmotionEvaluator:
|
|
|
97
99
|
print(f"⏳ 正在加载 Emotion2Vec+ 大模型: {self.e2v_model_path}")
|
|
98
100
|
try:
|
|
99
101
|
from funasr import AutoModel
|
|
100
|
-
|
|
102
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
103
|
+
self.e2v_model_path,
|
|
104
|
+
fallback_source=self.DEFAULT_E2V_MODEL,
|
|
105
|
+
)
|
|
106
|
+
print(f"Loading Emotion2Vec+ ({source_kind}) from {model_source}...")
|
|
107
|
+
self.e2v_model = AutoModel(model=model_source, device=self.device, disable_update=True)
|
|
101
108
|
print("✅ Emotion2Vec+ 大模型加载成功!")
|
|
102
109
|
except ImportError:
|
|
103
110
|
print("❌ Emotion2Vec+ 依赖缺失。请执行: pip install funasr modelscope")
|
|
@@ -259,4 +266,4 @@ class EmotionEvaluator:
|
|
|
259
266
|
print("\n [深度学习离散情感识别分类率] (数值越高代表预设标签识别越准)")
|
|
260
267
|
print(f" - Audio Emotion Accuracy (离散情感识别准确率): {results['Audio_Emotion_Accuracy']:.2%}")
|
|
261
268
|
|
|
262
|
-
return results
|
|
269
|
+
return results
|
|
@@ -104,14 +104,14 @@ class LatencyEvaluator:
|
|
|
104
104
|
# 2. 智能化筛选最高级/最准确的一个版本展示给用户
|
|
105
105
|
cleaned_results = {}
|
|
106
106
|
|
|
107
|
-
# [A] 评估: 第一声开口延迟 (StartOffset
|
|
107
|
+
# [A] 评估: 第一声开口延迟 (StartOffset)
|
|
108
108
|
so_key = "StartOffset_CA" if computation_aware else "StartOffset"
|
|
109
109
|
align_so_key = "StartOffset_SpeechAlign_CA" if computation_aware else "StartOffset_SpeechAlign"
|
|
110
110
|
|
|
111
111
|
if results.get(align_so_key) is not None and results[align_so_key] > 0:
|
|
112
|
-
cleaned_results["First_Audio_Delay_(
|
|
112
|
+
cleaned_results["First_Audio_Delay_(StartOffset_ms)"] = results[align_so_key]
|
|
113
113
|
else:
|
|
114
|
-
cleaned_results["First_Audio_Delay_(
|
|
114
|
+
cleaned_results["First_Audio_Delay_(StartOffset_ms)"] = results.get(so_key, 0)
|
|
115
115
|
|
|
116
116
|
# [B] 评估: 整句同传综合延迟 (标准版 ATD: 连同阅读/播放音频时间一起计算)
|
|
117
117
|
atd_key = "ATD_CA" if computation_aware else "ATD"
|
|
@@ -7,6 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
import matplotlib.pyplot as plt
|
|
8
8
|
import textgrid
|
|
9
9
|
|
|
10
|
+
from .._model_loading import resolve_pretrained_source
|
|
11
|
+
|
|
10
12
|
try:
|
|
11
13
|
import whisper
|
|
12
14
|
except ImportError:
|
|
@@ -98,10 +100,14 @@ def _load_whisper_model(model_name="medium", device=None):
|
|
|
98
100
|
return None
|
|
99
101
|
key = (model_name, device or "auto")
|
|
100
102
|
if key not in _WHISPER_MODELS:
|
|
103
|
+
resolved_model_name, _source_kind = resolve_pretrained_source(
|
|
104
|
+
model_name,
|
|
105
|
+
fallback_source="medium",
|
|
106
|
+
)
|
|
101
107
|
kwargs = {}
|
|
102
108
|
if device:
|
|
103
109
|
kwargs["device"] = device
|
|
104
|
-
_WHISPER_MODELS[key] = whisper.load_model(
|
|
110
|
+
_WHISPER_MODELS[key] = whisper.load_model(resolved_model_name, **kwargs)
|
|
105
111
|
return _WHISPER_MODELS[key]
|
|
106
112
|
|
|
107
113
|
|
|
@@ -9,8 +9,11 @@ import torch
|
|
|
9
9
|
import torchaudio
|
|
10
10
|
from tqdm import tqdm
|
|
11
11
|
|
|
12
|
+
from ._model_loading import resolve_pretrained_source
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
LabelNormalizer = Optional[Union[Dict[str, Optional[str]], Callable[[str], Optional[str]]]]
|
|
16
|
+
DEFAULT_CLAP_MODEL_SOURCE = "laion/clap-htsat-fused"
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
@dataclass(frozen=True)
|
|
@@ -524,7 +527,7 @@ class ClapAudioEventPredictor(BaseAudioEventPredictor):
|
|
|
524
527
|
def __init__(
|
|
525
528
|
self,
|
|
526
529
|
*,
|
|
527
|
-
model_path: str =
|
|
530
|
+
model_path: str = DEFAULT_CLAP_MODEL_SOURCE,
|
|
528
531
|
config: Optional[EventPredictionConfig] = None,
|
|
529
532
|
device: Optional[str] = None,
|
|
530
533
|
) -> None:
|
|
@@ -544,8 +547,13 @@ class ClapAudioEventPredictor(BaseAudioEventPredictor):
|
|
|
544
547
|
except ImportError as exc:
|
|
545
548
|
raise RuntimeError("CLAP-based event prediction requires `transformers`.") from exc
|
|
546
549
|
|
|
547
|
-
|
|
548
|
-
|
|
550
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
551
|
+
self.model_path,
|
|
552
|
+
fallback_source=DEFAULT_CLAP_MODEL_SOURCE,
|
|
553
|
+
)
|
|
554
|
+
print(f"Loading CLAP ({source_kind}) from {model_source}...")
|
|
555
|
+
self._processor = ClapProcessor.from_pretrained(model_source)
|
|
556
|
+
self._model = ClapModel.from_pretrained(model_source).to(self.device).eval()
|
|
549
557
|
|
|
550
558
|
@staticmethod
|
|
551
559
|
def _normalize_embedding(embedding: np.ndarray) -> np.ndarray:
|
|
@@ -691,7 +699,7 @@ class ClapSlidingWindowEventLocalizer(BaseAudioEventLocalizer):
|
|
|
691
699
|
def __init__(
|
|
692
700
|
self,
|
|
693
701
|
*,
|
|
694
|
-
model_path: str =
|
|
702
|
+
model_path: str = DEFAULT_CLAP_MODEL_SOURCE,
|
|
695
703
|
prediction_config: Optional[EventPredictionConfig] = None,
|
|
696
704
|
localization_config: Optional[EventLocalizationConfig] = None,
|
|
697
705
|
device: Optional[str] = None,
|
|
@@ -801,7 +809,7 @@ class ClapSlidingWindowEventLocalizer(BaseAudioEventLocalizer):
|
|
|
801
809
|
|
|
802
810
|
|
|
803
811
|
class ParalinguisticEvaluator:
|
|
804
|
-
DEFAULT_CLAP_MODEL =
|
|
812
|
+
DEFAULT_CLAP_MODEL = DEFAULT_CLAP_MODEL_SOURCE
|
|
805
813
|
|
|
806
814
|
def __init__(
|
|
807
815
|
self,
|
|
@@ -842,8 +850,13 @@ class ParalinguisticEvaluator:
|
|
|
842
850
|
except ImportError as exc:
|
|
843
851
|
raise RuntimeError("Paralinguistic_Fidelity_Cosine requires `transformers` to load CLAP.") from exc
|
|
844
852
|
|
|
845
|
-
|
|
846
|
-
|
|
853
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
854
|
+
self.clap_model_path,
|
|
855
|
+
fallback_source=self.DEFAULT_CLAP_MODEL,
|
|
856
|
+
)
|
|
857
|
+
print(f"Loading CLAP ({source_kind}) from {model_source}...")
|
|
858
|
+
self._clap_processor = ClapProcessor.from_pretrained(model_source)
|
|
859
|
+
self._clap_model = ClapModel.from_pretrained(model_source).to(self.device).eval()
|
|
847
860
|
|
|
848
861
|
def _extract_clap_embeddings(self, audio_paths: Sequence[str]) -> List[np.ndarray]:
|
|
849
862
|
self._load_clap()
|
|
@@ -5,6 +5,10 @@ import os
|
|
|
5
5
|
import librosa
|
|
6
6
|
from tqdm import tqdm
|
|
7
7
|
|
|
8
|
+
from ._model_loading import resolve_pretrained_source
|
|
9
|
+
|
|
10
|
+
DEFAULT_WAVLM_MODEL_SOURCE = "microsoft/wavlm-base-plus-sv"
|
|
11
|
+
|
|
8
12
|
# 如果你想用本地的 Resemblyzer,确保之前项目能 import 到
|
|
9
13
|
try:
|
|
10
14
|
from resemblyzer import preprocess_wav, VoiceEncoder
|
|
@@ -26,7 +30,7 @@ class SpeakerSimilarityEvaluator:
|
|
|
26
30
|
def __init__(self,
|
|
27
31
|
model_type="wavlm",
|
|
28
32
|
device=None,
|
|
29
|
-
wavlm_model_path=
|
|
33
|
+
wavlm_model_path=DEFAULT_WAVLM_MODEL_SOURCE,
|
|
30
34
|
resemblyzer_weights_path="pretrained.pt"):
|
|
31
35
|
"""
|
|
32
36
|
初始化打分器,仅在初始化时加载一次模型。
|
|
@@ -50,9 +54,13 @@ class SpeakerSimilarityEvaluator:
|
|
|
50
54
|
if self.model_type in ["wavlm", "both"]:
|
|
51
55
|
if not TRANSFORMERS_AVAILABLE:
|
|
52
56
|
raise ImportError("Please install transformers to use WavLM: pip install transformers")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
58
|
+
wavlm_model_path,
|
|
59
|
+
fallback_source=DEFAULT_WAVLM_MODEL_SOURCE,
|
|
60
|
+
)
|
|
61
|
+
print(f"Loading WavLM ({source_kind}) from {model_source}...")
|
|
62
|
+
self.wavlm_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_source)
|
|
63
|
+
self.wavlm_model = WavLMForXVector.from_pretrained(model_source).to(self.device).eval()
|
|
56
64
|
|
|
57
65
|
if self.model_type in ["resemblyzer", "both"]:
|
|
58
66
|
if not RESEMBLYZER_AVAILABLE:
|
|
@@ -144,4 +152,4 @@ class SpeakerSimilarityEvaluator:
|
|
|
144
152
|
if res_scores:
|
|
145
153
|
batch_results["average_resemblyzer_similarity"] = sum(res_scores) / len(res_scores)
|
|
146
154
|
|
|
147
|
-
return batch_results
|
|
155
|
+
return batch_results
|
|
@@ -7,6 +7,7 @@ from typing import List, Dict, Optional, Union
|
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
9
|
# 复用现有的加载工具
|
|
10
|
+
from ._model_loading import resolve_pretrained_source
|
|
10
11
|
from .translation_evaluator import load_text_from_file_or_list, load_audio_from_folder
|
|
11
12
|
|
|
12
13
|
try:
|
|
@@ -15,6 +16,7 @@ except ImportError:
|
|
|
15
16
|
whisper = None
|
|
16
17
|
|
|
17
18
|
class SpeechQualityEvaluator:
|
|
19
|
+
DEFAULT_WHISPER_MODEL = "medium"
|
|
18
20
|
"""
|
|
19
21
|
语音质量与一致性评测器
|
|
20
22
|
专门用于:
|
|
@@ -24,7 +26,7 @@ class SpeechQualityEvaluator:
|
|
|
24
26
|
def __init__(self,
|
|
25
27
|
use_wer: bool = True,
|
|
26
28
|
use_utmos: bool = True,
|
|
27
|
-
whisper_model: str =
|
|
29
|
+
whisper_model: str = DEFAULT_WHISPER_MODEL,
|
|
28
30
|
utmos_model_path: Optional[str] = None,
|
|
29
31
|
utmos_ckpt_path: Optional[str] = None,
|
|
30
32
|
device: Optional[str] = None):
|
|
@@ -56,7 +58,12 @@ class SpeechQualityEvaluator:
|
|
|
56
58
|
self.use_wer = False
|
|
57
59
|
return
|
|
58
60
|
print(f"⏳ 正在加载 Whisper ({self.whisper_model_name})...")
|
|
59
|
-
|
|
61
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
62
|
+
self.whisper_model_name,
|
|
63
|
+
fallback_source=self.DEFAULT_WHISPER_MODEL,
|
|
64
|
+
)
|
|
65
|
+
print(f"Loading Whisper ({source_kind}) from {model_source}...")
|
|
66
|
+
self.whisper_model = whisper.load_model(model_source, device=self.device)
|
|
60
67
|
|
|
61
68
|
def _load_utmos(self):
|
|
62
69
|
if self.utmos_model is None and self.use_utmos:
|
|
@@ -168,4 +175,4 @@ class SpeechQualityEvaluator:
|
|
|
168
175
|
else:
|
|
169
176
|
print(" ⚠️ 提供的生成文本清洗后为空,无法计算分布错误率。")
|
|
170
177
|
|
|
171
|
-
return results
|
|
178
|
+
return results
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import torchaudio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TemporalConsistencyEvaluator:
|
|
9
|
+
"""Evaluate source-target temporal consistency at the utterance level."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
thresholds: Sequence[float] = (0.2, 0.4),
|
|
14
|
+
) -> None:
|
|
15
|
+
if not thresholds:
|
|
16
|
+
raise ValueError("thresholds must contain at least one value.")
|
|
17
|
+
|
|
18
|
+
normalized_thresholds: List[float] = []
|
|
19
|
+
for threshold in thresholds:
|
|
20
|
+
try:
|
|
21
|
+
value = float(threshold)
|
|
22
|
+
except (TypeError, ValueError) as exc:
|
|
23
|
+
raise ValueError("thresholds must be numeric.") from exc
|
|
24
|
+
if value < 0.0 or value >= 1.0:
|
|
25
|
+
raise ValueError("each threshold must satisfy 0 <= threshold < 1.")
|
|
26
|
+
normalized_thresholds.append(value)
|
|
27
|
+
|
|
28
|
+
self.thresholds = tuple(sorted(set(normalized_thresholds)))
|
|
29
|
+
|
|
30
|
+
def _load_audio_from_folder(self, folder_path: str, name: str) -> List[str]:
|
|
31
|
+
folder = Path(folder_path)
|
|
32
|
+
if not folder.exists():
|
|
33
|
+
raise FileNotFoundError(f"{name} not found: {folder_path}")
|
|
34
|
+
if not folder.is_dir():
|
|
35
|
+
raise ValueError(f"{name} must be a directory when using folder input: {folder_path}")
|
|
36
|
+
|
|
37
|
+
audio_files: List[Path] = []
|
|
38
|
+
for extension in (".wav", ".flac", ".mp3", ".ogg", ".m4a"):
|
|
39
|
+
audio_files.extend(folder.glob(f"*{extension}"))
|
|
40
|
+
audio_files = sorted(audio_files, key=lambda item: item.stem)
|
|
41
|
+
if not audio_files:
|
|
42
|
+
raise ValueError(f"No audio files found under {name}: {folder_path}")
|
|
43
|
+
return [str(item.resolve()) for item in audio_files]
|
|
44
|
+
|
|
45
|
+
def _resolve_audio_paths(self, values: Sequence[str], name: str) -> List[str]:
|
|
46
|
+
resolved: List[str] = []
|
|
47
|
+
for index, value in enumerate(values):
|
|
48
|
+
path = Path(str(value))
|
|
49
|
+
if not path.exists():
|
|
50
|
+
raise FileNotFoundError(f"{name}[{index}] not found: {value}")
|
|
51
|
+
resolved.append(str(path.resolve()))
|
|
52
|
+
return resolved
|
|
53
|
+
|
|
54
|
+
def _load_audio_list(self, data: Union[List[str], str], name: str) -> List[str]:
|
|
55
|
+
if isinstance(data, list):
|
|
56
|
+
return self._resolve_audio_paths(data, name)
|
|
57
|
+
|
|
58
|
+
if not isinstance(data, str):
|
|
59
|
+
raise ValueError(f"{name} must be a path or a list of paths.")
|
|
60
|
+
|
|
61
|
+
path = Path(data)
|
|
62
|
+
if not path.exists():
|
|
63
|
+
raise FileNotFoundError(f"{name} not found: {data}")
|
|
64
|
+
|
|
65
|
+
if path.is_dir():
|
|
66
|
+
return self._load_audio_from_folder(data, name)
|
|
67
|
+
|
|
68
|
+
suffix = path.suffix.lower()
|
|
69
|
+
if suffix == ".json":
|
|
70
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
71
|
+
payload = json.load(file)
|
|
72
|
+
|
|
73
|
+
candidate_keys = ("audio", "path", "file", "wav", "mp3", "source_audio", "target_audio")
|
|
74
|
+
if isinstance(payload, list):
|
|
75
|
+
if not payload:
|
|
76
|
+
return []
|
|
77
|
+
if isinstance(payload[0], str):
|
|
78
|
+
return self._resolve_audio_paths(payload, name)
|
|
79
|
+
if isinstance(payload[0], dict):
|
|
80
|
+
for key in candidate_keys:
|
|
81
|
+
if key in payload[0]:
|
|
82
|
+
return self._resolve_audio_paths([str(item[key]) for item in payload], name)
|
|
83
|
+
raise ValueError(f"{name} JSON list must contain strings or dicts with one of {candidate_keys}.")
|
|
84
|
+
|
|
85
|
+
if isinstance(payload, dict):
|
|
86
|
+
plural_keys = tuple(f"{key}s" for key in candidate_keys)
|
|
87
|
+
for key in plural_keys:
|
|
88
|
+
if key in payload:
|
|
89
|
+
return self._resolve_audio_paths(payload[key], name)
|
|
90
|
+
raise ValueError(f"{name} JSON object must contain one of {plural_keys}.")
|
|
91
|
+
|
|
92
|
+
raise ValueError(f"{name} JSON payload must be a list or dict.")
|
|
93
|
+
|
|
94
|
+
if suffix == ".txt":
|
|
95
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
96
|
+
lines = [line.strip() for line in file if line.strip()]
|
|
97
|
+
return self._resolve_audio_paths(lines, name)
|
|
98
|
+
|
|
99
|
+
return self._resolve_audio_paths([data], name)
|
|
100
|
+
|
|
101
|
+
def _get_audio_duration_ms(self, audio_path: str) -> float:
|
|
102
|
+
try:
|
|
103
|
+
info = torchaudio.info(audio_path)
|
|
104
|
+
if info.sample_rate > 0 and info.num_frames >= 0:
|
|
105
|
+
return float(info.num_frames / info.sample_rate * 1000.0)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
waveform, sample_rate = torchaudio.load(audio_path)
|
|
110
|
+
if sample_rate <= 0:
|
|
111
|
+
return 0.0
|
|
112
|
+
return float(waveform.shape[-1] / sample_rate * 1000.0)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _threshold_suffix(threshold: float) -> str:
|
|
116
|
+
return f"{threshold:.1f}"
|
|
117
|
+
|
|
118
|
+
def _compute_metrics(
|
|
119
|
+
self,
|
|
120
|
+
source_durations_ms: Sequence[float],
|
|
121
|
+
target_durations_ms: Sequence[float],
|
|
122
|
+
sample_ids: Optional[Sequence[str]] = None,
|
|
123
|
+
) -> Tuple[Dict[str, float], Dict[str, Any]]:
|
|
124
|
+
metrics: Dict[str, float] = {}
|
|
125
|
+
sample_records: List[Dict[str, Any]] = []
|
|
126
|
+
valid_ratios: List[float] = []
|
|
127
|
+
hit_counts = {threshold: 0 for threshold in self.thresholds}
|
|
128
|
+
skipped = 0
|
|
129
|
+
|
|
130
|
+
for index, (source_ms, target_ms) in enumerate(zip(source_durations_ms, target_durations_ms)):
|
|
131
|
+
sample_id = sample_ids[index] if sample_ids is not None else str(index)
|
|
132
|
+
record: Dict[str, Any] = {
|
|
133
|
+
"sample_index": int(index),
|
|
134
|
+
"sample_id": str(sample_id),
|
|
135
|
+
"source_duration_ms": round(float(source_ms), 4),
|
|
136
|
+
"target_duration_ms": round(float(target_ms), 4),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if source_ms <= 0.0 or target_ms <= 0.0:
|
|
140
|
+
skipped += 1
|
|
141
|
+
record["valid"] = False
|
|
142
|
+
record["skip_reason"] = "non_positive_duration"
|
|
143
|
+
sample_records.append(record)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
ratio = float(target_ms / source_ms)
|
|
147
|
+
valid_ratios.append(ratio)
|
|
148
|
+
|
|
149
|
+
slc_hits: Dict[str, bool] = {}
|
|
150
|
+
for threshold in self.thresholds:
|
|
151
|
+
hit = (1.0 - threshold) <= ratio <= (1.0 + threshold)
|
|
152
|
+
slc_hits[self._threshold_suffix(threshold)] = bool(hit)
|
|
153
|
+
if hit:
|
|
154
|
+
hit_counts[threshold] += 1
|
|
155
|
+
|
|
156
|
+
record.update(
|
|
157
|
+
{
|
|
158
|
+
"valid": True,
|
|
159
|
+
"duration_ratio": round(ratio, 6),
|
|
160
|
+
"slc_hits": slc_hits,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
sample_records.append(record)
|
|
164
|
+
|
|
165
|
+
num_evaluated = len(valid_ratios)
|
|
166
|
+
for threshold in self.thresholds:
|
|
167
|
+
suffix = self._threshold_suffix(threshold)
|
|
168
|
+
score = float(hit_counts[threshold] / num_evaluated) if num_evaluated > 0 else 0.0
|
|
169
|
+
metrics[f"Duration_Consistency_SLC_{suffix}"] = round(score, 4)
|
|
170
|
+
|
|
171
|
+
diagnostics: Dict[str, Any] = {
|
|
172
|
+
"num_samples": len(source_durations_ms),
|
|
173
|
+
"num_evaluated": num_evaluated,
|
|
174
|
+
"num_skipped": skipped,
|
|
175
|
+
"thresholds": [round(float(value), 4) for value in self.thresholds],
|
|
176
|
+
"samples": sample_records,
|
|
177
|
+
}
|
|
178
|
+
return metrics, diagnostics
|
|
179
|
+
|
|
180
|
+
def evaluate_all(
|
|
181
|
+
self,
|
|
182
|
+
source_audio: Union[List[str], str],
|
|
183
|
+
target_audio: Union[List[str], str],
|
|
184
|
+
*,
|
|
185
|
+
sample_ids: Optional[Sequence[str]] = None,
|
|
186
|
+
verbose: bool = True,
|
|
187
|
+
return_diagnostics: bool = False,
|
|
188
|
+
) -> Union[Tuple[Dict[str, float], Dict[str, Any]], Dict[str, float]]:
|
|
189
|
+
source_audio_paths = self._load_audio_list(source_audio, "source_audio")
|
|
190
|
+
target_audio_paths = self._load_audio_list(target_audio, "target_audio")
|
|
191
|
+
|
|
192
|
+
if len(source_audio_paths) != len(target_audio_paths):
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"Source and target size mismatch: {len(source_audio_paths)} vs {len(target_audio_paths)}"
|
|
195
|
+
)
|
|
196
|
+
if not source_audio_paths:
|
|
197
|
+
raise ValueError("No samples found for temporal consistency evaluation.")
|
|
198
|
+
if sample_ids is not None and len(sample_ids) != len(source_audio_paths):
|
|
199
|
+
raise ValueError(f"sample_ids size mismatch: {len(sample_ids)} vs {len(source_audio_paths)}")
|
|
200
|
+
|
|
201
|
+
source_durations_ms = [self._get_audio_duration_ms(path) for path in source_audio_paths]
|
|
202
|
+
target_durations_ms = [self._get_audio_duration_ms(path) for path in target_audio_paths]
|
|
203
|
+
metrics, diagnostics = self._compute_metrics(
|
|
204
|
+
source_durations_ms,
|
|
205
|
+
target_durations_ms,
|
|
206
|
+
sample_ids=sample_ids,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if verbose:
|
|
210
|
+
print("\n[TemporalConsistencyEvaluator] Summary")
|
|
211
|
+
print(f" Samples: {diagnostics['num_samples']}")
|
|
212
|
+
print(f" Evaluated: {diagnostics['num_evaluated']}")
|
|
213
|
+
print(f" Skipped: {diagnostics['num_skipped']}")
|
|
214
|
+
for metric_name, score in metrics.items():
|
|
215
|
+
print(f" {metric_name}: {score}")
|
|
216
|
+
|
|
217
|
+
if return_diagnostics:
|
|
218
|
+
return metrics, diagnostics
|
|
219
|
+
return metrics
|
|
@@ -10,6 +10,8 @@ import torch
|
|
|
10
10
|
from typing import Dict, List, Optional, Union
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
from ._model_loading import resolve_pretrained_source
|
|
14
|
+
|
|
13
15
|
# ==================== 配置 ====================
|
|
14
16
|
|
|
15
17
|
CACHE_PATHS = {
|
|
@@ -88,6 +90,7 @@ def load_audio_from_folder(folder_path: str, extensions: tuple = (".wav", ".mp3"
|
|
|
88
90
|
|
|
89
91
|
# ==================== 评测器核心类 ====================
|
|
90
92
|
|
|
93
|
+
DEFAULT_COMET_MODEL = "Unbabel/wmt22-comet-da"
|
|
91
94
|
DEFAULT_BLEURT_MODEL = "lucadiliello/BLEURT-20"
|
|
92
95
|
|
|
93
96
|
class TranslationEvaluator:
|
|
@@ -100,7 +103,7 @@ class TranslationEvaluator:
|
|
|
100
103
|
use_chrf: bool = True,
|
|
101
104
|
use_comet: bool = True,
|
|
102
105
|
use_bleurt: bool = False,
|
|
103
|
-
comet_model: str =
|
|
106
|
+
comet_model: str = DEFAULT_COMET_MODEL,
|
|
104
107
|
bleurt_path: Optional[str] = None,
|
|
105
108
|
bleurt_model: Optional[str] = None,
|
|
106
109
|
device: Optional[str] = None):
|
|
@@ -144,15 +147,43 @@ class TranslationEvaluator:
|
|
|
144
147
|
torch.cuda.empty_cache()
|
|
145
148
|
gc.collect()
|
|
146
149
|
|
|
150
|
+
def _resolve_local_comet_checkpoint(self, model_name: str) -> Optional[str]:
|
|
151
|
+
candidate = Path(model_name).expanduser()
|
|
152
|
+
if not candidate.exists():
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
if candidate.is_file():
|
|
156
|
+
return str(candidate.resolve())
|
|
157
|
+
|
|
158
|
+
common_ckpts = [
|
|
159
|
+
candidate / "checkpoints" / "model.ckpt",
|
|
160
|
+
candidate / "model.ckpt",
|
|
161
|
+
]
|
|
162
|
+
for ckpt in common_ckpts:
|
|
163
|
+
if ckpt.exists() and ckpt.is_file():
|
|
164
|
+
return str(ckpt.resolve())
|
|
165
|
+
return None
|
|
166
|
+
|
|
147
167
|
def _load_comet(self, model_name: str):
|
|
148
168
|
if not download_model:
|
|
149
169
|
print("⚠️ COMET 未安装,跳过")
|
|
150
170
|
return None
|
|
151
171
|
try:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
model_source, source_kind = resolve_pretrained_source(
|
|
173
|
+
model_name,
|
|
174
|
+
fallback_source=DEFAULT_COMET_MODEL,
|
|
175
|
+
)
|
|
176
|
+
local_ckpt = self._resolve_local_comet_checkpoint(model_source)
|
|
177
|
+
if local_ckpt is not None:
|
|
178
|
+
print(f"⏳ [Local] 加载 COMET: {local_ckpt}")
|
|
179
|
+
model = load_from_checkpoint(local_ckpt)
|
|
180
|
+
else:
|
|
181
|
+
remote_source = model_source if source_kind == "remote" else DEFAULT_COMET_MODEL
|
|
182
|
+
cache = os.path.join(CACHE_PATHS["huggingface"], f"models--{remote_source.replace('/', '--')}")
|
|
183
|
+
status = "[Local]" if os.path.exists(cache) else "[Online]"
|
|
184
|
+
print(f"⏳ {status} 加载 COMET: {model_name}")
|
|
185
|
+
print(f"Loading COMET ({status}) from {remote_source}")
|
|
186
|
+
model = load_from_checkpoint(download_model(remote_source))
|
|
156
187
|
if self.device.startswith("cuda"):
|
|
157
188
|
model = model.to(self.device)
|
|
158
189
|
return model
|
|
@@ -165,7 +196,13 @@ class TranslationEvaluator:
|
|
|
165
196
|
print("⚠️ bleurt-pytorch 未安装,跳过加载")
|
|
166
197
|
return
|
|
167
198
|
|
|
168
|
-
|
|
199
|
+
if path:
|
|
200
|
+
model_source, _source_kind = resolve_pretrained_source(
|
|
201
|
+
path,
|
|
202
|
+
fallback_source=model_name or DEFAULT_BLEURT_MODEL,
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
model_source, _source_kind = resolve_pretrained_source(model_name or DEFAULT_BLEURT_MODEL)
|
|
169
206
|
print(f"⏳ 加载 BLEURT: {model_source}")
|
|
170
207
|
|
|
171
208
|
try:
|
|
@@ -254,4 +291,4 @@ class TranslationEvaluator:
|
|
|
254
291
|
except Exception as e:
|
|
255
292
|
results["COMET"] = -1.0
|
|
256
293
|
|
|
257
|
-
return {k: round(v, 4) if v >= 0 else v for k, v in results.items()}
|
|
294
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|