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.
@@ -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)