OpenSTBench 0.2.0__py3-none-any.whl
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/__init__.py +143 -0
- openstbench/dataset.py +199 -0
- openstbench/emotion_evaluator.py +262 -0
- openstbench/latency/__init__.py +4 -0
- openstbench/latency/agent.py +108 -0
- openstbench/latency/basics.py +57 -0
- openstbench/latency/cli.py +257 -0
- openstbench/latency/instance.py +176 -0
- openstbench/latency/metrics.py +418 -0
- openstbench/latency/utils.py +295 -0
- openstbench/paralinguistic_evaluator.py +1318 -0
- openstbench/speaker_similarity_evaluator.py +147 -0
- openstbench/speech_quality_evaluator.py +171 -0
- openstbench/translation_evaluator.py +257 -0
- openstbench-0.2.0.dist-info/METADATA +181 -0
- openstbench-0.2.0.dist-info/RECORD +18 -0
- openstbench-0.2.0.dist-info/WHEEL +5 -0
- openstbench-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from inspect import signature
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from .basics import (
|
|
5
|
+
Action,
|
|
6
|
+
AgentStates,
|
|
7
|
+
EmptySegment,
|
|
8
|
+
Segment,
|
|
9
|
+
SpeechSegment,
|
|
10
|
+
TextSegment,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenericAgent:
|
|
15
|
+
"""Base agent interface for latency evaluation."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.states = AgentStates()
|
|
19
|
+
self._pending_model_inference_time = None
|
|
20
|
+
self.reset()
|
|
21
|
+
|
|
22
|
+
def reset(self):
|
|
23
|
+
self.states.reset()
|
|
24
|
+
self._pending_model_inference_time = None
|
|
25
|
+
|
|
26
|
+
def record_model_inference_time(self, seconds: float) -> None:
|
|
27
|
+
if seconds is None:
|
|
28
|
+
return
|
|
29
|
+
seconds = float(seconds)
|
|
30
|
+
if seconds < 0:
|
|
31
|
+
return
|
|
32
|
+
if self._pending_model_inference_time is None:
|
|
33
|
+
self._pending_model_inference_time = 0.0
|
|
34
|
+
self._pending_model_inference_time += seconds
|
|
35
|
+
|
|
36
|
+
def consume_model_inference_time(self):
|
|
37
|
+
value = self._pending_model_inference_time
|
|
38
|
+
self._pending_model_inference_time = None
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
def policy(self, states=None) -> Action:
|
|
42
|
+
raise NotImplementedError("Please implement policy().")
|
|
43
|
+
|
|
44
|
+
def push(self, segment: Segment, states=None) -> None:
|
|
45
|
+
(states or self.states).update_source(segment)
|
|
46
|
+
|
|
47
|
+
def pop(self, states=None) -> Segment:
|
|
48
|
+
state = states or self.states
|
|
49
|
+
if state.target_finished:
|
|
50
|
+
return EmptySegment(finished=True)
|
|
51
|
+
|
|
52
|
+
if len(signature(self.policy).parameters) > 0:
|
|
53
|
+
action = self.policy(state)
|
|
54
|
+
else:
|
|
55
|
+
action = self.policy()
|
|
56
|
+
|
|
57
|
+
if action.is_read():
|
|
58
|
+
return EmptySegment()
|
|
59
|
+
|
|
60
|
+
if isinstance(action.content, Segment):
|
|
61
|
+
segment = action.content
|
|
62
|
+
if action.finished is not None:
|
|
63
|
+
segment.finished = action.finished
|
|
64
|
+
elif isinstance(action.content, str):
|
|
65
|
+
segment = TextSegment(content=action.content, finished=action.finished)
|
|
66
|
+
elif isinstance(action.content, list):
|
|
67
|
+
segment = SpeechSegment(content=action.content, finished=action.finished)
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(f"Unknown content type: {type(action.content)}")
|
|
70
|
+
|
|
71
|
+
state.update_target(segment)
|
|
72
|
+
return segment
|
|
73
|
+
|
|
74
|
+
def pushpop(self, segment: Segment) -> Segment:
|
|
75
|
+
self.push(segment)
|
|
76
|
+
return self.pop()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AgentPipeline(GenericAgent):
|
|
80
|
+
"""Sequentially compose multiple agents."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, agents: List[GenericAgent]):
|
|
83
|
+
self.pipeline = agents
|
|
84
|
+
self.states = [agent.states for agent in agents]
|
|
85
|
+
self._pending_model_inference_time = None
|
|
86
|
+
|
|
87
|
+
def reset(self):
|
|
88
|
+
self._pending_model_inference_time = None
|
|
89
|
+
for agent in self.pipeline:
|
|
90
|
+
agent.reset()
|
|
91
|
+
|
|
92
|
+
def pushpop(self, segment: Segment) -> Segment:
|
|
93
|
+
current_input = segment
|
|
94
|
+
for index, agent in enumerate(self.pipeline):
|
|
95
|
+
if index == 0:
|
|
96
|
+
agent.push(current_input)
|
|
97
|
+
current_output = agent.pop()
|
|
98
|
+
else:
|
|
99
|
+
if not current_output.is_empty:
|
|
100
|
+
agent.push(current_output)
|
|
101
|
+
current_output = agent.pop()
|
|
102
|
+
|
|
103
|
+
if hasattr(agent, "consume_model_inference_time"):
|
|
104
|
+
self.record_model_inference_time(agent.consume_model_inference_time())
|
|
105
|
+
|
|
106
|
+
if current_output.is_empty and index < len(self.pipeline) - 1:
|
|
107
|
+
return EmptySegment()
|
|
108
|
+
return current_output
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Union, List
|
|
3
|
+
|
|
4
|
+
class Action:
|
|
5
|
+
def is_read(self) -> bool: raise NotImplementedError
|
|
6
|
+
|
|
7
|
+
class ReadAction(Action):
|
|
8
|
+
def is_read(self) -> bool: return True
|
|
9
|
+
def __repr__(self): return "ReadAction"
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class WriteAction(Action):
|
|
13
|
+
content: Union[str, List[float]]
|
|
14
|
+
finished: bool
|
|
15
|
+
def is_read(self) -> bool: return False
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Segment:
|
|
19
|
+
index: int = 0
|
|
20
|
+
content: list = field(default_factory=list)
|
|
21
|
+
finished: bool = False
|
|
22
|
+
is_empty: bool = False
|
|
23
|
+
data_type: str = None
|
|
24
|
+
config: dict = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class EmptySegment(Segment):
|
|
28
|
+
is_empty: bool = True
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TextSegment(Segment):
|
|
32
|
+
content: str = ""
|
|
33
|
+
data_type: str = "text"
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SpeechSegment(Segment):
|
|
37
|
+
sample_rate: int = 16000
|
|
38
|
+
data_type: str = "speech"
|
|
39
|
+
|
|
40
|
+
class AgentStates:
|
|
41
|
+
def __init__(self): self.reset()
|
|
42
|
+
def reset(self):
|
|
43
|
+
self.source, self.target = [], []
|
|
44
|
+
self.source_finished = self.target_finished = False
|
|
45
|
+
self.upstream_states = []
|
|
46
|
+
|
|
47
|
+
def update_source(self, segment: Segment):
|
|
48
|
+
self.source_finished = segment.finished
|
|
49
|
+
if not segment.is_empty:
|
|
50
|
+
if isinstance(segment, TextSegment): self.source.append(segment.content)
|
|
51
|
+
elif isinstance(segment, SpeechSegment): self.source += segment.content
|
|
52
|
+
|
|
53
|
+
def update_target(self, segment: Segment):
|
|
54
|
+
self.target_finished = segment.finished
|
|
55
|
+
if not segment.is_empty and not self.target_finished:
|
|
56
|
+
if isinstance(segment, TextSegment): self.target.append(segment.content)
|
|
57
|
+
elif isinstance(segment, SpeechSegment): self.target += segment.content
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib.util
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
|
|
8
|
+
from .agent import GenericAgent
|
|
9
|
+
from .instance import SpeechToTextInstance, SpeechToSpeechInstance
|
|
10
|
+
from .metrics import SCORERS
|
|
11
|
+
from .utils import Visualizer, materialize_s2s_alignment_artifacts, submit_slurm
|
|
12
|
+
|
|
13
|
+
# --- 引入兄弟模块 (紧耦合) ---
|
|
14
|
+
# 注意:使用相对导入或包绝对导入
|
|
15
|
+
from ..translation_evaluator import TranslationEvaluator
|
|
16
|
+
|
|
17
|
+
class LatencyEvaluator:
|
|
18
|
+
"""同传延迟与质量评测器"""
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
agent: GenericAgent,
|
|
22
|
+
segment_size=20,
|
|
23
|
+
latency_unit="word",
|
|
24
|
+
asr_fallback_for_s2s_alignment=True,
|
|
25
|
+
asr_model="medium",
|
|
26
|
+
asr_device=None,
|
|
27
|
+
alignment_acoustic_model="english_mfa",
|
|
28
|
+
alignment_dictionary_model="english_mfa",
|
|
29
|
+
):
|
|
30
|
+
self.agent = agent
|
|
31
|
+
self.segment_size = segment_size
|
|
32
|
+
self.instances = {}
|
|
33
|
+
self.latency_unit = latency_unit
|
|
34
|
+
self.asr_fallback_for_s2s_alignment = asr_fallback_for_s2s_alignment
|
|
35
|
+
self.asr_model = asr_model
|
|
36
|
+
self.asr_device = asr_device
|
|
37
|
+
self.alignment_acoustic_model = alignment_acoustic_model
|
|
38
|
+
self.alignment_dictionary_model = alignment_dictionary_model
|
|
39
|
+
|
|
40
|
+
def run(self, source_files, ref_files, task="s2t", output_dir="./output", visualize=False):
|
|
41
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
42
|
+
visualizer = Visualizer(output_dir) if visualize else None
|
|
43
|
+
|
|
44
|
+
print(f"🚀 Running Latency Evaluation ({task}, {len(source_files)} samples)...")
|
|
45
|
+
for i, (src, ref) in tqdm(enumerate(zip(source_files, ref_files)), total=len(source_files)):
|
|
46
|
+
if task == "s2t":
|
|
47
|
+
ins = SpeechToTextInstance(i, src, ref, output_dir)
|
|
48
|
+
else:
|
|
49
|
+
ins = SpeechToSpeechInstance(i, src, ref, output_dir)
|
|
50
|
+
|
|
51
|
+
self.agent.reset()
|
|
52
|
+
while not ins.finish_prediction:
|
|
53
|
+
s_in = ins.send_source(self.segment_size)
|
|
54
|
+
import time
|
|
55
|
+
t0 = time.perf_counter()
|
|
56
|
+
s_out = self.agent.pushpop(s_in)
|
|
57
|
+
t1 = time.perf_counter()
|
|
58
|
+
ins.add_inference_time(t1 - t0)
|
|
59
|
+
if hasattr(self.agent, "consume_model_inference_time"):
|
|
60
|
+
ins.add_model_inference_time(self.agent.consume_model_inference_time())
|
|
61
|
+
ins.receive_prediction(s_out)
|
|
62
|
+
|
|
63
|
+
self.instances[i] = ins
|
|
64
|
+
if visualizer: visualizer.plot(ins.summarize())
|
|
65
|
+
|
|
66
|
+
# S2S 准备文本
|
|
67
|
+
if task == "s2s":
|
|
68
|
+
self._prepare_s2s_transcripts(output_dir)
|
|
69
|
+
|
|
70
|
+
return self.instances
|
|
71
|
+
|
|
72
|
+
def _prepare_s2s_transcripts(self, output_dir):
|
|
73
|
+
materialize_s2s_alignment_artifacts(
|
|
74
|
+
self.instances,
|
|
75
|
+
output_dir,
|
|
76
|
+
unit=self.latency_unit,
|
|
77
|
+
asr_fallback=self.asr_fallback_for_s2s_alignment,
|
|
78
|
+
asr_model=self.asr_model,
|
|
79
|
+
asr_device=self.asr_device,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 增加 show_all_metrics 开关,默认为 False
|
|
83
|
+
def compute_latency(self, computation_aware=False, output_dir="./output", show_all_metrics=False):
|
|
84
|
+
results = {}
|
|
85
|
+
# 1. 扫描并算出所有注册的 Scorer 成绩
|
|
86
|
+
for name, cls in SCORERS.items():
|
|
87
|
+
try:
|
|
88
|
+
if "Align" in name:
|
|
89
|
+
scorer = cls(
|
|
90
|
+
computation_aware=computation_aware,
|
|
91
|
+
output_dir=output_dir,
|
|
92
|
+
alignment_acoustic_model=self.alignment_acoustic_model,
|
|
93
|
+
alignment_dictionary_model=self.alignment_dictionary_model,
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
scorer = cls(computation_aware=computation_aware)
|
|
97
|
+
|
|
98
|
+
score = scorer(self.instances)
|
|
99
|
+
key = f"{name}_CA" if computation_aware else name
|
|
100
|
+
results[key] = score
|
|
101
|
+
except Exception as e:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# 2. 智能化筛选最高级/最准确的一个版本展示给用户
|
|
105
|
+
cleaned_results = {}
|
|
106
|
+
|
|
107
|
+
# [A] 评估: 第一声开口延迟 (StartOffset/ALAL)
|
|
108
|
+
so_key = "StartOffset_CA" if computation_aware else "StartOffset"
|
|
109
|
+
align_so_key = "StartOffset_SpeechAlign_CA" if computation_aware else "StartOffset_SpeechAlign"
|
|
110
|
+
|
|
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]
|
|
113
|
+
else:
|
|
114
|
+
cleaned_results["First_Audio_Delay_(ALAL_ms)"] = results.get(so_key, 0)
|
|
115
|
+
|
|
116
|
+
# [B] 评估: 整句同传综合延迟 (标准版 ATD: 连同阅读/播放音频时间一起计算)
|
|
117
|
+
atd_key = "ATD_CA" if computation_aware else "ATD"
|
|
118
|
+
align_atd_key = "ATD_SpeechAlign_CA" if computation_aware else "ATD_SpeechAlign"
|
|
119
|
+
|
|
120
|
+
if results.get(align_atd_key) is not None and results[align_atd_key] > 0:
|
|
121
|
+
cleaned_results["Overall_Translation_Delay_(ATD_ms)"] = results[align_atd_key]
|
|
122
|
+
else:
|
|
123
|
+
cleaned_results["Overall_Translation_Delay_(ATD_ms)"] = results.get(atd_key, 0)
|
|
124
|
+
|
|
125
|
+
# [C] 评估: 模型结单同传延迟 (纯净版 CustomATD: 剔除音频合成自身的物理时长,只看模型推断何时结束)
|
|
126
|
+
catd_key = "CustomATD_CA" if computation_aware else "CustomATD"
|
|
127
|
+
align_catd_key = "CustomATD_SpeechAlign_CA" if computation_aware else "CustomATD_SpeechAlign"
|
|
128
|
+
|
|
129
|
+
if results.get(align_catd_key) is not None and results[align_catd_key] > 0:
|
|
130
|
+
cleaned_results["End_Action_Delay_(CustomATD_ms)"] = results[align_catd_key]
|
|
131
|
+
else:
|
|
132
|
+
cleaned_results["End_Action_Delay_(CustomATD_ms)"] = results.get(catd_key, 0)
|
|
133
|
+
|
|
134
|
+
# [D] 评估: 实时率指标 (RTF)
|
|
135
|
+
if "RTF" in results:
|
|
136
|
+
cleaned_results["Real_Time_Factor_(RTF)"] = results["RTF"]
|
|
137
|
+
elif "RTF_CA" in results:
|
|
138
|
+
cleaned_results["Real_Time_Factor_(RTF)"] = results["RTF_CA"]
|
|
139
|
+
|
|
140
|
+
if "ModelGenerateRTF" in results and results["ModelGenerateRTF"] is not None:
|
|
141
|
+
cleaned_results["Model_Generate_RTF"] = results["ModelGenerateRTF"]
|
|
142
|
+
elif "ModelGenerateRTF_CA" in results and results["ModelGenerateRTF_CA"] is not None:
|
|
143
|
+
cleaned_results["Model_Generate_RTF"] = results["ModelGenerateRTF_CA"]
|
|
144
|
+
|
|
145
|
+
# ================= 修改开始 =================
|
|
146
|
+
# 如果用户显式开启了展示开关,则将原始杂乱数据装入 "detailed_all_metrics"
|
|
147
|
+
if show_all_metrics:
|
|
148
|
+
cleaned_results["detailed_all_metrics"] = results
|
|
149
|
+
# ================= 修改结束 =================
|
|
150
|
+
|
|
151
|
+
return cleaned_results
|
|
152
|
+
|
|
153
|
+
def load_agent_from_file(path, class_name):
|
|
154
|
+
"""动态加载用户 Agent"""
|
|
155
|
+
spec = importlib.util.spec_from_file_location("user_agent", path)
|
|
156
|
+
mod = importlib.util.module_from_spec(spec)
|
|
157
|
+
spec.loader.exec_module(mod)
|
|
158
|
+
return getattr(mod, class_name)
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
parser = argparse.ArgumentParser(description="OpenSTBench Latency Evaluator")
|
|
162
|
+
parser.add_argument("--source", required=True)
|
|
163
|
+
parser.add_argument("--target", default=None)
|
|
164
|
+
parser.add_argument("--output", default="./output")
|
|
165
|
+
parser.add_argument("--task", choices=["s2t", "s2s"], default="s2t")
|
|
166
|
+
parser.add_argument("--agent-script", required=True, help="Path to python file containing Agent class")
|
|
167
|
+
parser.add_argument("--agent-class", required=True, help="Name of the Agent class")
|
|
168
|
+
parser.add_argument("--segment-size", type=int, default=20)
|
|
169
|
+
parser.add_argument("--latency-unit", choices=["word", "char"], default="word")
|
|
170
|
+
parser.add_argument("--disable-asr-fallback", action="store_true")
|
|
171
|
+
parser.add_argument("--asr-model", default="medium")
|
|
172
|
+
parser.add_argument("--alignment-acoustic-model", default="english_mfa")
|
|
173
|
+
parser.add_argument("--alignment-dictionary-model", default="english_mfa")
|
|
174
|
+
parser.add_argument("--computation-aware", action="store_true")
|
|
175
|
+
parser.add_argument("--quality", action="store_true", help="Run Quality Evaluation (BLEU/WER) after latency")
|
|
176
|
+
parser.add_argument("--slurm", action="store_true")
|
|
177
|
+
args = parser.parse_args()
|
|
178
|
+
|
|
179
|
+
if args.slurm:
|
|
180
|
+
submit_slurm(args, __file__)
|
|
181
|
+
|
|
182
|
+
# 1. 加载数据
|
|
183
|
+
with open(args.source) as f: src = [l.strip() for l in f if l.strip()]
|
|
184
|
+
ref = [None]*len(src)
|
|
185
|
+
if args.target:
|
|
186
|
+
with open(args.target) as f: ref = [l.strip() for l in f if l.strip()]
|
|
187
|
+
|
|
188
|
+
# 2. 加载用户 Agent
|
|
189
|
+
AgentClass = load_agent_from_file(args.agent_script, args.agent_class)
|
|
190
|
+
agent = AgentClass()
|
|
191
|
+
|
|
192
|
+
# 3. 运行 Latency 评测
|
|
193
|
+
evaluator = LatencyEvaluator(
|
|
194
|
+
agent,
|
|
195
|
+
args.segment_size,
|
|
196
|
+
latency_unit=args.latency_unit,
|
|
197
|
+
asr_fallback_for_s2s_alignment=not args.disable_asr_fallback,
|
|
198
|
+
asr_model=args.asr_model,
|
|
199
|
+
alignment_acoustic_model=args.alignment_acoustic_model,
|
|
200
|
+
alignment_dictionary_model=args.alignment_dictionary_model,
|
|
201
|
+
)
|
|
202
|
+
instances = evaluator.run(src, ref, args.task, args.output)
|
|
203
|
+
|
|
204
|
+
# 4. 计算 Latency 指标
|
|
205
|
+
print("\n--- Latency Metrics ---")
|
|
206
|
+
scores = evaluator.compute_latency(False, args.output)
|
|
207
|
+
if args.computation_aware:
|
|
208
|
+
scores.update(evaluator.compute_latency(True, args.output))
|
|
209
|
+
|
|
210
|
+
for k, v in scores.items():
|
|
211
|
+
print(f"{k:<25}: {v:.4f}")
|
|
212
|
+
|
|
213
|
+
# 5. [紧耦合] 运行 Quality 指标
|
|
214
|
+
if args.quality and args.target:
|
|
215
|
+
print("\n--- Quality Metrics (Integrated) ---")
|
|
216
|
+
|
|
217
|
+
# 收集预测结果
|
|
218
|
+
# 注意: 对于 s2s, instances[i].prediction 应该是音频文件路径
|
|
219
|
+
predictions = [ins.get_prediction_content() for ins in instances.values()]
|
|
220
|
+
|
|
221
|
+
if args.task == "s2t":
|
|
222
|
+
# [修改] 调用 TranslationEvaluator 算 BLEU
|
|
223
|
+
# 默认只开 BLEU 以保证速度
|
|
224
|
+
qual_eval = TranslationEvaluator(
|
|
225
|
+
use_bleu=True,
|
|
226
|
+
use_chrf=False,
|
|
227
|
+
use_comet=False,
|
|
228
|
+
use_whisper=False
|
|
229
|
+
)
|
|
230
|
+
# 使用新的 evaluate_all 接口
|
|
231
|
+
q_res = qual_eval.evaluate_all(
|
|
232
|
+
target_text=predictions,
|
|
233
|
+
reference=ref,
|
|
234
|
+
source=src
|
|
235
|
+
)
|
|
236
|
+
print(q_res)
|
|
237
|
+
|
|
238
|
+
elif args.task == "s2s":
|
|
239
|
+
# [修改] 调用 TranslationEvaluator 替代原 SpeechEvaluator
|
|
240
|
+
# 开启语音相关功能: use_wer=True, use_whisper=True
|
|
241
|
+
# predictions 是 wav 路径列表
|
|
242
|
+
speech_eval = TranslationEvaluator(
|
|
243
|
+
use_wer=True,
|
|
244
|
+
use_whisper=True,
|
|
245
|
+
whisper_model="tiny", # 用 tiny 快速出 WER
|
|
246
|
+
use_utmos=False # 可选开启
|
|
247
|
+
)
|
|
248
|
+
# 使用 evaluate_all 接口传入 target_speech
|
|
249
|
+
s_res = speech_eval.evaluate_all(
|
|
250
|
+
target_speech=predictions,
|
|
251
|
+
reference=ref,
|
|
252
|
+
source=src
|
|
253
|
+
)
|
|
254
|
+
print(s_res)
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import math
|
|
3
|
+
import soundfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .basics import EmptySegment, SpeechSegment, Segment
|
|
6
|
+
|
|
7
|
+
class Instance:
|
|
8
|
+
def __init__(self, index: int, source_path, reference=None, output_dir="./output"):
|
|
9
|
+
self.index = index
|
|
10
|
+
self.source_path = source_path
|
|
11
|
+
self.reference = reference
|
|
12
|
+
self.output_dir = Path(output_dir)
|
|
13
|
+
self.reset()
|
|
14
|
+
|
|
15
|
+
def reset(self):
|
|
16
|
+
self.step = 0
|
|
17
|
+
self.elapsed = []
|
|
18
|
+
self.delays = []
|
|
19
|
+
self.prediction_list = []
|
|
20
|
+
self.start_time = None
|
|
21
|
+
self.durations = [] # S2S 专用
|
|
22
|
+
self.finish_prediction = False
|
|
23
|
+
self.total_inference_time = 0.0
|
|
24
|
+
self.total_model_inference_time = None
|
|
25
|
+
self.prediction_text = ""
|
|
26
|
+
self.prediction_text_source = "none"
|
|
27
|
+
self.target_units = []
|
|
28
|
+
self.target_unit_times_ms = []
|
|
29
|
+
self.alignment_mode = "none"
|
|
30
|
+
self.prediction_audio_path = None
|
|
31
|
+
self.source_chunk_end_times_ms = []
|
|
32
|
+
|
|
33
|
+
def summarize(self):
|
|
34
|
+
return {
|
|
35
|
+
"index": self.index,
|
|
36
|
+
"source": (self.source_path, ""),
|
|
37
|
+
"reference": self.reference,
|
|
38
|
+
"prediction": self.get_prediction_content(),
|
|
39
|
+
"prediction_length": len(self.prediction_list),
|
|
40
|
+
"delays": self.delays,
|
|
41
|
+
"elapsed": self.elapsed,
|
|
42
|
+
"durations": self.durations,
|
|
43
|
+
"prediction_text": self.prediction_text,
|
|
44
|
+
"prediction_text_source": self.prediction_text_source,
|
|
45
|
+
"alignment_mode": self.alignment_mode,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def get_prediction_content(self): raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def add_model_inference_time(self, time_spent_in_seconds):
|
|
51
|
+
if time_spent_in_seconds is None:
|
|
52
|
+
return
|
|
53
|
+
time_spent_in_seconds = float(time_spent_in_seconds)
|
|
54
|
+
if time_spent_in_seconds < 0:
|
|
55
|
+
return
|
|
56
|
+
if self.total_model_inference_time is None:
|
|
57
|
+
self.total_model_inference_time = 0.0
|
|
58
|
+
self.total_model_inference_time += time_spent_in_seconds
|
|
59
|
+
|
|
60
|
+
def append_prediction_text(self, text: str, source: str = "native_transcript"):
|
|
61
|
+
text = (text or "").strip()
|
|
62
|
+
if not text:
|
|
63
|
+
return
|
|
64
|
+
if self.prediction_text:
|
|
65
|
+
sep = "" if self.prediction_text.endswith(" ") else " "
|
|
66
|
+
self.prediction_text = f"{self.prediction_text}{sep}{text}".strip()
|
|
67
|
+
else:
|
|
68
|
+
self.prediction_text = text
|
|
69
|
+
self.prediction_text_source = source
|
|
70
|
+
def get_prediction_raw(self): return self.prediction_list # 返回原始 list 供质量评测
|
|
71
|
+
|
|
72
|
+
class SpeechToTextInstance(Instance):
|
|
73
|
+
def __init__(self, index, source_path, reference=None, output_dir="./output"):
|
|
74
|
+
data, sr = soundfile.read(source_path, dtype="float32")
|
|
75
|
+
self.samples = data.tolist()
|
|
76
|
+
self.sample_rate = sr
|
|
77
|
+
self.source_finished_reading = False
|
|
78
|
+
super().__init__(index, source_path, reference, output_dir)
|
|
79
|
+
|
|
80
|
+
def len_sample_to_ms(self, length): return length * 1000 / self.sample_rate
|
|
81
|
+
|
|
82
|
+
def send_source(self, segment_size=10):
|
|
83
|
+
if self.step == 0: self.start_time = time.time()
|
|
84
|
+
num_samples = math.ceil(segment_size / 1000 * self.sample_rate)
|
|
85
|
+
|
|
86
|
+
if self.step < len(self.samples):
|
|
87
|
+
chunk = self.samples[self.step : self.step + num_samples]
|
|
88
|
+
is_finished = (self.step + num_samples >= len(self.samples))
|
|
89
|
+
self.step += len(chunk)
|
|
90
|
+
self.source_chunk_end_times_ms.append(self.len_sample_to_ms(self.step))
|
|
91
|
+
return SpeechSegment(content=chunk, sample_rate=self.sample_rate, finished=is_finished)
|
|
92
|
+
else:
|
|
93
|
+
self.source_finished_reading = True
|
|
94
|
+
return EmptySegment(finished=True)
|
|
95
|
+
|
|
96
|
+
def add_inference_time(self, time_spent_in_seconds: float):
|
|
97
|
+
"""累加纯模型计算时间"""
|
|
98
|
+
self.total_inference_time += time_spent_in_seconds
|
|
99
|
+
|
|
100
|
+
def receive_prediction(self, seg: Segment):
|
|
101
|
+
if self.finish_prediction or seg.is_empty:
|
|
102
|
+
self.finish_prediction = seg.finished
|
|
103
|
+
return
|
|
104
|
+
if not seg.content: return
|
|
105
|
+
if getattr(seg, "config", None):
|
|
106
|
+
self.add_model_inference_time(seg.config.get("model_inference_time"))
|
|
107
|
+
|
|
108
|
+
current_time = time.time()
|
|
109
|
+
content_list = seg.content.strip().split()
|
|
110
|
+
self.prediction_list += content_list
|
|
111
|
+
self.append_prediction_text(seg.content, source="native_transcript")
|
|
112
|
+
|
|
113
|
+
curr_delay = self.len_sample_to_ms(self.step)
|
|
114
|
+
curr_elapsed = curr_delay + (current_time - self.start_time) * 1000
|
|
115
|
+
|
|
116
|
+
self.delays += [curr_delay] * len(content_list)
|
|
117
|
+
self.elapsed += [curr_elapsed] * len(content_list)
|
|
118
|
+
self.finish_prediction = seg.finished
|
|
119
|
+
|
|
120
|
+
def get_prediction_content(self): return " ".join(self.prediction_list)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def source_length(self): return self.len_sample_to_ms(len(self.samples))
|
|
124
|
+
@property
|
|
125
|
+
def reference_length(self): return len(self.reference.split()) if self.reference else 0
|
|
126
|
+
|
|
127
|
+
class SpeechToSpeechInstance(SpeechToTextInstance):
|
|
128
|
+
def __init__(self, index, source_path, reference=None, output_dir="./output"):
|
|
129
|
+
super().__init__(index, source_path, reference, output_dir)
|
|
130
|
+
self.target_sample_rate = 16000
|
|
131
|
+
|
|
132
|
+
def receive_prediction(self, seg: Segment):
|
|
133
|
+
if self.finish_prediction or seg.is_empty:
|
|
134
|
+
self.finish_prediction = seg.finished
|
|
135
|
+
return
|
|
136
|
+
if not seg.content: return
|
|
137
|
+
if not self.start_time: self.start_time = time.time()
|
|
138
|
+
if getattr(seg, "config", None):
|
|
139
|
+
self.add_model_inference_time(seg.config.get("model_inference_time"))
|
|
140
|
+
transcript = seg.config.get("transcript")
|
|
141
|
+
if transcript:
|
|
142
|
+
source = seg.config.get("transcript_source", "native_transcript")
|
|
143
|
+
self.append_prediction_text(transcript, source=source)
|
|
144
|
+
|
|
145
|
+
current_time = time.time()
|
|
146
|
+
duration_ms = len(seg.content) * 1000 / seg.sample_rate
|
|
147
|
+
self.target_sample_rate = seg.sample_rate
|
|
148
|
+
|
|
149
|
+
self.prediction_list.append(seg.content)
|
|
150
|
+
self.durations.append(duration_ms)
|
|
151
|
+
|
|
152
|
+
curr_delay = self.len_sample_to_ms(self.step)
|
|
153
|
+
curr_elapsed = curr_delay + (current_time - self.start_time) * 1000
|
|
154
|
+
|
|
155
|
+
self.delays.append(curr_delay)
|
|
156
|
+
self.elapsed.append(curr_elapsed)
|
|
157
|
+
self.finish_prediction = seg.finished
|
|
158
|
+
|
|
159
|
+
def get_prediction_content(self):
|
|
160
|
+
# 返回 wav 路径
|
|
161
|
+
wav_dir = self.output_dir / "wavs"
|
|
162
|
+
wav_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
wav_path = wav_dir / f"{self.index}_pred.wav"
|
|
164
|
+
|
|
165
|
+
flat_samples = [item for sublist in self.prediction_list for item in sublist]
|
|
166
|
+
if flat_samples:
|
|
167
|
+
soundfile.write(wav_path, flat_samples, self.target_sample_rate)
|
|
168
|
+
self.prediction_audio_path = str(wav_path.absolute())
|
|
169
|
+
return self.prediction_audio_path
|
|
170
|
+
|
|
171
|
+
def get_prediction_raw(self):
|
|
172
|
+
# S2S 的原始预测结果是音频路径
|
|
173
|
+
return self.get_prediction_content()
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def reference_length(self): return len(self.prediction_list)
|