OpenSTBench 0.2.0__tar.gz → 0.3.0__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 (25) hide show
  1. {openstbench-0.2.0 → openstbench-0.3.0}/PKG-INFO +33 -2
  2. {openstbench-0.2.0 → openstbench-0.3.0}/README.md +32 -1
  3. {openstbench-0.2.0 → openstbench-0.3.0}/pyproject.toml +1 -1
  4. {openstbench-0.2.0 → openstbench-0.3.0}/src/OpenSTBench.egg-info/PKG-INFO +33 -2
  5. {openstbench-0.2.0 → openstbench-0.3.0}/src/OpenSTBench.egg-info/SOURCES.txt +2 -0
  6. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/__init__.py +3 -1
  7. openstbench-0.3.0/src/openstbench/_model_loading.py +62 -0
  8. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/emotion_evaluator.py +9 -2
  9. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/cli.py +3 -3
  10. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/utils.py +7 -1
  11. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/paralinguistic_evaluator.py +20 -7
  12. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/speaker_similarity_evaluator.py +13 -5
  13. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/speech_quality_evaluator.py +10 -3
  14. openstbench-0.3.0/src/openstbench/temporal_consistency_evaluator.py +219 -0
  15. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/translation_evaluator.py +44 -7
  16. {openstbench-0.2.0 → openstbench-0.3.0}/setup.cfg +0 -0
  17. {openstbench-0.2.0 → openstbench-0.3.0}/src/OpenSTBench.egg-info/dependency_links.txt +0 -0
  18. {openstbench-0.2.0 → openstbench-0.3.0}/src/OpenSTBench.egg-info/requires.txt +0 -0
  19. {openstbench-0.2.0 → openstbench-0.3.0}/src/OpenSTBench.egg-info/top_level.txt +0 -0
  20. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/dataset.py +0 -0
  21. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/__init__.py +0 -0
  22. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/agent.py +0 -0
  23. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/basics.py +0 -0
  24. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/instance.py +0 -0
  25. {openstbench-0.2.0 → openstbench-0.3.0}/src/openstbench/latency/metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenSTBench
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -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 TranslationEvaluator, SpeechQualityEvaluator
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 TranslationEvaluator, SpeechQualityEvaluator
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,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OpenSTBench"
7
- version = "0.2.0"
7
+ version = "0.3.0"
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
10
  requires-python = ">=3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenSTBench
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -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 TranslationEvaluator, SpeechQualityEvaluator
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.2.0"
12
+ __version__ = "0.3.0"
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
- self.e2v_model = AutoModel(model=self.e2v_model_path, device=self.device, disable_update=True)
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/ALAL)
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_(ALAL_ms)"] = results[align_so_key]
112
+ cleaned_results["First_Audio_Delay_(StartOffset_ms)"] = results[align_so_key]
113
113
  else:
114
- cleaned_results["First_Audio_Delay_(ALAL_ms)"] = results.get(so_key, 0)
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(model_name, **kwargs)
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 = "laion/clap-htsat-fused",
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
- self._processor = ClapProcessor.from_pretrained(self.model_path)
548
- self._model = ClapModel.from_pretrained(self.model_path).to(self.device).eval()
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 = "laion/clap-htsat-fused",
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 = "laion/clap-htsat-fused"
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
- self._clap_processor = ClapProcessor.from_pretrained(self.clap_model_path)
846
- self._clap_model = ClapModel.from_pretrained(self.clap_model_path).to(self.device).eval()
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="microsoft/wavlm-base-plus-sv",
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
- print(f"Loading WavLM from {wavlm_model_path}...")
54
- self.wavlm_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(wavlm_model_path)
55
- self.wavlm_model = WavLMForXVector.from_pretrained(wavlm_model_path).to(self.device).eval()
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 = "medium",
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
- self.whisper_model = whisper.load_model(self.whisper_model_name, device=self.device)
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 = "Unbabel/wmt22-comet-da",
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
- cache = os.path.join(CACHE_PATHS["huggingface"], f"models--{model_name.replace('/', '--')}")
153
- status = "[Local]" if os.path.exists(cache) else "[Online]"
154
- print(f"⏳ {status} 加载 COMET: {model_name}")
155
- model = load_from_checkpoint(download_model(model_name))
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
- model_source = path if (path and os.path.exists(path)) else (model_name or DEFAULT_BLEURT_MODEL)
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