smartpi 0.1.34__py3-none-any.whl → 0.1.36__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,173 @@
1
+ import numpy as np
2
+ import onnxruntime as ort
3
+ import onnx
4
+ import json
5
+ import os
6
+ import time
7
+ from transformers import AutoTokenizer
8
+
9
+ # 获取当前文件的绝对路径
10
+ current_dir = os.path.dirname(os.path.abspath(__file__))
11
+ # 构建默认的GTE模型和分词器配置路径
12
+ default_feature_model = os.path.join(current_dir, 'text_gte_model', 'gte', 'gte_model.onnx')
13
+ default_tokenizer_path = os.path.join(current_dir, 'text_gte_model', 'config')
14
+
15
+ class TextClassificationWorkflow:
16
+ def __init__(self, class_model_path, feature_model_path=None, tokenizer_path=None):
17
+ # 如果没有提供路径,则使用默认路径
18
+ self.feature_model_path = feature_model_path or default_feature_model
19
+ self.tokenizer_path = tokenizer_path or default_tokenizer_path
20
+ self.class_model_path = class_model_path
21
+ # 记录模型初始化开始时间
22
+ init_start_time = time.time()
23
+
24
+ # 加载分词器
25
+ print("加载分词器...")
26
+ tokenizer_start = time.time()
27
+ self.tokenizer = AutoTokenizer.from_pretrained(
28
+ self.tokenizer_path,
29
+ local_files_only=True
30
+ )
31
+ tokenizer_time = time.time() - tokenizer_start
32
+ print(f"分词器加载完成,耗时: {tokenizer_time:.3f} 秒")
33
+
34
+ # 加载特征提取模型
35
+ print("加载特征提取模型...")
36
+ feature_start = time.time()
37
+ self.feature_session = ort.InferenceSession(self.feature_model_path)
38
+ self.feature_input_names = [input.name for input in self.feature_session.get_inputs()]
39
+ feature_load_time = time.time() - feature_start
40
+ print(f"特征提取模型加载完成,耗时: {feature_load_time:.3f} 秒")
41
+
42
+ # 加载分类模型
43
+ print("加载分类模型...")
44
+ class_start = time.time()
45
+ self.class_session = ort.InferenceSession(class_model_path)
46
+ self.class_input_name = self.class_session.get_inputs()[0].name
47
+ self.class_output_name = self.class_session.get_outputs()[0].name
48
+ class_load_time = time.time() - class_start
49
+ print(f"分类模型加载完成,耗时: {class_load_time:.3f} 秒")
50
+
51
+ # 加载元数据(类别标签)
52
+ meta_start = time.time()
53
+ self.label_names = self._load_metadata(class_model_path)
54
+ meta_time = time.time() - meta_start
55
+
56
+ # 计算总初始化时间
57
+ init_total_time = time.time() - init_start_time
58
+
59
+ print(f"元数据加载完成,耗时: {meta_time:.3f} 秒")
60
+ print(f"分类模型加载成功,共 {len(self.label_names)} 个类别: {self.label_names}")
61
+ print(f"模型初始化总耗时: {init_total_time:.3f} 秒")
62
+
63
+ def _load_metadata(self, model_path):
64
+ """从ONNX模型元数据中加载类别标签"""
65
+ try:
66
+ # 使用 ONNX 库加载模型文件
67
+ onnx_model = onnx.load(model_path)
68
+
69
+ # 尝试从metadata_props获取
70
+ if onnx_model.metadata_props:
71
+ for prop in onnx_model.metadata_props:
72
+ if prop.key == 'classes':
73
+ try:
74
+ # 尝试解析JSON格式的类别
75
+ return json.loads(prop.value)
76
+ except json.JSONDecodeError:
77
+ # 如果是逗号分隔的字符串
78
+ return prop.value.split(',')
79
+
80
+ # 尝试从doc_string获取
81
+ if onnx_model.doc_string:
82
+ try:
83
+ doc_dict = json.loads(onnx_model.doc_string)
84
+ if 'classes' in doc_dict:
85
+ return doc_dict['classes']
86
+ except:
87
+ pass
88
+ except Exception as e:
89
+ print(f"元数据读取错误: {e}")
90
+
91
+ # 默认值:根据输出形状生成类别名称
92
+ num_classes = self.class_session.get_outputs()[0].shape[-1]
93
+ label_names = [f"Class_{i}" for i in range(num_classes)]
94
+ print(f"警告: 未在模型元数据中找到类别信息,使用自动生成的类别名称: {label_names}")
95
+ return label_names
96
+
97
+ def _extract_features(self, texts):
98
+ """对文本进行分词并提取特征向量"""
99
+ # 文本预处理
100
+ inputs = self.tokenizer(
101
+ texts,
102
+ padding=True,
103
+ truncation=True,
104
+ max_length=512,
105
+ return_tensors="np"
106
+ )
107
+
108
+ # 转换输入类型为int64
109
+ onnx_inputs = {name: inputs[name].astype(np.int64) for name in self.feature_input_names}
110
+
111
+ # 提取文本特征
112
+ onnx_outputs = self.feature_session.run(None, onnx_inputs)
113
+ last_hidden_state = onnx_outputs[0]
114
+ return last_hidden_state[:, 0, :].astype(np.float32) # 确保float32类型
115
+
116
+ def _classify(self, embeddings):
117
+ """对特征向量进行分类预测"""
118
+ # 分类模型推理
119
+ class_results = self.class_session.run(
120
+ [self.class_output_name],
121
+ {self.class_input_name: embeddings}
122
+ )[0]
123
+
124
+ # 应用softmax获取概率分布
125
+ probs = np.exp(class_results) / np.sum(np.exp(class_results), axis=1, keepdims=True)
126
+ return probs
127
+
128
+ def predict(self, texts):
129
+ """执行文本分类预测,包含时间测量功能"""
130
+ if not texts:
131
+ return [], []
132
+
133
+ # 记录总开始时间
134
+ total_start_time = time.time()
135
+
136
+ # 记录特征提取时间
137
+ feature_start_time = time.time()
138
+ embeddings = self._extract_features(texts)
139
+ feature_time = time.time() - feature_start_time
140
+
141
+ # 记录分类推理时间
142
+ classify_start_time = time.time()
143
+ probs = self._classify(embeddings)
144
+ classify_time = time.time() - classify_start_time
145
+
146
+ # 计算总时间
147
+ total_time = time.time() - total_start_time
148
+
149
+ predicted_indices = np.argmax(probs, axis=1)
150
+
151
+ # 格式化结果
152
+ raw_results = []
153
+ formatted_results = []
154
+
155
+ for i, (text, idx, prob_vec) in enumerate(zip(texts, predicted_indices, probs)):
156
+ label = self.label_names[idx] if idx < len(self.label_names) else f"未知类别 {idx}"
157
+ confidence = float(prob_vec[idx])
158
+
159
+ raw_results.append(prob_vec.tolist())
160
+ formatted_results.append({
161
+ 'text': text,
162
+ 'class': label,
163
+ 'confidence': confidence,
164
+ 'class_id': int(idx),
165
+ 'probabilities': prob_vec.tolist(),
166
+ # 添加时间信息
167
+ 'preprocess_time': 0.0, # 文本不需要传统的图像预处理
168
+ 'feature_extract_time': feature_time / len(texts), # 平均到每个文本
169
+ 'inference_time': classify_time / len(texts), # 平均到每个文本
170
+ 'total_time': total_time / len(texts) # 平均到每个文本
171
+ })
172
+
173
+ return raw_results, formatted_results
@@ -0,0 +1,437 @@
1
+ import numpy as np
2
+ import onnxruntime as ort
3
+ import librosa
4
+ import onnx
5
+ import time
6
+ import os
7
+
8
+
9
+ class Workflow:
10
+ def __init__(self, model_path=None, smoothing_time_constant=0, step_size=43):
11
+ self.model = None
12
+ self.classes = []
13
+ self.metadata = {}
14
+ self.model_params = {
15
+ 'fft_size': 2048,
16
+ 'sample_rate': 44100,
17
+ 'num_frames': 43, # 每块帧数
18
+ 'spec_features': 232
19
+ }
20
+ self.global_mean = None
21
+ self.global_std = None
22
+ self.smoothing_time_constant = smoothing_time_constant
23
+ self.step_size = step_size
24
+ self.frame_duration = None
25
+ self.hop_length = 735 # 44100/60=735 (每帧时长 ~16.67ms)
26
+ self.previous_spec = None
27
+
28
+ if model_path:
29
+ self.load_model(model_path)
30
+
31
+ # 计算帧时间信息
32
+ self.frame_duration = self.hop_length / self.model_params['sample_rate']
33
+ self.block_duration = self.model_params['num_frames'] * self.frame_duration
34
+
35
+ def load_model(self, model_path):
36
+ """加载模型并解析元数据"""
37
+ onnx_model = onnx.load(model_path)
38
+ for meta in onnx_model.metadata_props:
39
+ self.metadata[meta.key] = meta.value
40
+
41
+ if 'classes' in self.metadata:
42
+ self.classes = eval(self.metadata['classes'])
43
+
44
+ if 'global_mean' in self.metadata:
45
+ self.global_mean = np.array(eval(self.metadata['global_mean']))
46
+ if 'global_std' in self.metadata:
47
+ self.global_std = np.array(eval(self.metadata['global_std']))
48
+
49
+ self.session = ort.InferenceSession(model_path)
50
+ self.input_shape = self._get_fixed_shape(self.session.get_inputs()[0].shape)
51
+
52
+ def _get_fixed_shape(self, shape):
53
+ fixed = []
54
+ for dim in shape:
55
+ if isinstance(dim, str) or dim < 0:
56
+ fixed.append(1)
57
+ else:
58
+ fixed.append(int(dim))
59
+ return fixed
60
+
61
+ def _apply_hann_window(self, frame):
62
+ """应用汉宁窗函数"""
63
+ return frame * np.hanning(len(frame))
64
+
65
+ def _apply_temporal_smoothing(self, current_spec):
66
+ """应用时域指数平滑"""
67
+ if self.previous_spec is None:
68
+ self.previous_spec = current_spec
69
+ return current_spec
70
+
71
+ smoothed = (self.smoothing_time_constant * self.previous_spec
72
+ + (1 - self.smoothing_time_constant) * current_spec)
73
+
74
+ self.previous_spec = smoothed.copy()
75
+ return smoothed
76
+
77
+ def _load_audio(self, audio_path):
78
+ """加载音频文件(支持wav和webm),返回音频数组和采样率"""
79
+ ext = os.path.splitext(audio_path)[1].lower()
80
+
81
+ if ext == '.wav':
82
+ # 使用librosa加载wav文件
83
+ audio, sr = librosa.load(audio_path, sr=self.model_params['sample_rate'])
84
+ return audio, sr
85
+
86
+ elif ext == '.webm':
87
+ # 使用pydub加载webm文件(需要ffmpeg支持)
88
+ try:
89
+ from pydub import AudioSegment
90
+ except ImportError:
91
+ raise ImportError("处理webm格式需要pydub库,请先安装:pip install pydub")
92
+
93
+ try:
94
+ # 加载webm文件
95
+ audio_segment = AudioSegment.from_file(audio_path, format='webm')
96
+
97
+ # 转换为单声道
98
+ audio_segment = audio_segment.set_channels(1)
99
+
100
+ # 转换采样率
101
+ audio_segment = audio_segment.set_frame_rate(self.model_params['sample_rate'])
102
+
103
+ # 转换为numpy数组(范围:[-1, 1])
104
+ samples = np.array(audio_segment.get_array_of_samples(), dtype=np.float32)
105
+ samples = samples / 32768.0 # 16位音频的归一化
106
+
107
+ return samples, self.model_params['sample_rate']
108
+
109
+ except FileNotFoundError as e:
110
+ if 'ffmpeg' in str(e).lower() or 'avconv' in str(e).lower():
111
+ print("\n" + "="*60)
112
+ print("检测到错误:缺少ffmpeg支持,无法处理webm格式音频")
113
+ print("="*60)
114
+ print("ffmpeg是处理webm等音频格式的必要工具,请按照以下教程安装:\n")
115
+
116
+ print("【Linux系统安装教程】")
117
+ print("1. Ubuntu/Debian系统:")
118
+ print(" sudo apt update")
119
+ print(" sudo apt install ffmpeg\n")
120
+
121
+ print("2. CentOS/RHEL系统:")
122
+ print(" sudo yum install epel-release")
123
+ print(" sudo yum install ffmpeg ffmpeg-devel\n")
124
+
125
+ print("3. Fedora系统:")
126
+ print(" sudo dnf install ffmpeg\n")
127
+
128
+ print("4. Arch Linux系统:")
129
+ print(" sudo pacman -S ffmpeg\n")
130
+
131
+ print("【Windows系统安装教程】")
132
+ print("1. 访问ffmpeg官网下载页:https://ffmpeg.org/download.html#build-windows")
133
+ print("2. 推荐下载方式:")
134
+ print(" - 从 Gyan.dev 下载:https://www.gyan.dev/ffmpeg/builds/")
135
+ print(" - 选择 'ffmpeg-release-essentials.zip' 版本")
136
+ print("3. 解压下载的zip文件到任意目录(例如:C:\\ffmpeg)")
137
+ print("4. 配置环境变量:")
138
+ print(" - 右键点击'此电脑' -> '属性' -> '高级系统设置' -> '环境变量'")
139
+ print(" - 在'系统变量'中找到'Path',点击'编辑'")
140
+ print(" - 点击'新建',添加ffmpeg的bin目录路径(例如:C:\\ffmpeg\\bin)")
141
+ print(" - 点击所有窗口的'确定'保存设置")
142
+ print("5. 验证安装:打开新的命令提示符,输入 'ffmpeg -version',能显示版本信息即为安装成功\n")
143
+
144
+ print("安装完成后,请重新运行程序。")
145
+ print("="*60 + "\n")
146
+ raise # 重新抛出异常终止程序
147
+ else:
148
+ raise # 其他文件未找到错误,正常抛出
149
+ except Exception as e:
150
+ print(f"处理webm音频时发生其他错误:{str(e)}")
151
+ raise
152
+
153
+ else:
154
+ raise ValueError(f"不支持的音频格式: {ext},目前支持 .wav 和 .webm")
155
+
156
+ def _preprocess_audio(self, audio_path):
157
+ """预处理整个音频文件,返回分贝谱"""
158
+ audio, sr = self._load_audio(audio_path)
159
+ assert sr == self.model_params['sample_rate'], f"采样率不匹配,需要 {self.model_params['sample_rate']}Hz"
160
+
161
+ # 使用新参数计算STFT
162
+ hop_length = self.hop_length
163
+ win_length = self.model_params['fft_size']
164
+ n_fft = self.model_params['fft_size']
165
+
166
+ # 手动分帧并加窗
167
+ frames = librosa.util.frame(audio, frame_length=win_length, hop_length=hop_length)
168
+ windowed_frames = np.zeros_like(frames)
169
+ for i in range(frames.shape[1]):
170
+ windowed_frames[:, i] = self._apply_hann_window(frames[:, i])
171
+
172
+ # 执行FFT
173
+ D = np.fft.rfft(windowed_frames, n=n_fft, axis=0)
174
+
175
+ # 计算幅度谱并转分贝
176
+ magnitude = np.abs(D)
177
+ db = 20 * np.log10(np.maximum(1e-5, magnitude))
178
+
179
+ # 截取需要的特征维度并转置
180
+ db = db[:self.model_params['spec_features'], :]
181
+ spec = db.T # 转置为[时间帧, 频率特征]
182
+
183
+ return spec
184
+
185
+ def preprocess_audio_segment(self, audio_segment):
186
+ """预处理音频片段(用于实时处理),返回分贝谱"""
187
+ # 确保音频是单声道且采样率正确
188
+ sr = self.model_params['sample_rate']
189
+
190
+ # 使用新参数计算STFT
191
+ hop_length = self.hop_length
192
+ win_length = self.model_params['fft_size']
193
+ n_fft = self.model_params['fft_size']
194
+
195
+ # 手动分帧并加窗
196
+ frames = librosa.util.frame(audio_segment, frame_length=win_length, hop_length=hop_length)
197
+ windowed_frames = np.zeros_like(frames)
198
+ for i in range(frames.shape[1]):
199
+ windowed_frames[:, i] = self._apply_hann_window(frames[:, i])
200
+
201
+ # 执行FFT
202
+ D = np.fft.rfft(windowed_frames, n=n_fft, axis=0)
203
+
204
+ # 计算幅度谱并转分贝
205
+ magnitude = np.abs(D)
206
+ db = 20 * np.log10(np.maximum(1e-5, magnitude))
207
+
208
+ # 截取需要的特征维度并转置
209
+ db = db[:self.model_params['spec_features'], :]
210
+ spec = db.T # 转置为[时间帧, 频率特征]
211
+
212
+ return spec
213
+
214
+ def _extract_blocks(self, full_spec):
215
+ """从完整频谱中提取指定帧数的块"""
216
+ total_frames = full_spec.shape[0]
217
+ blocks = []
218
+ start_indices = []
219
+
220
+ num_blocks = (total_frames - self.model_params['num_frames']) // self.step_size + 1
221
+
222
+ for i in range(num_blocks):
223
+ start = i * self.step_size
224
+ end = start + self.model_params['num_frames']
225
+
226
+ block = full_spec[start:end, :]
227
+
228
+ if block.shape[0] < self.model_params['num_frames']:
229
+ padded = np.zeros((self.model_params['num_frames'], self.model_params['spec_features']))
230
+ padded[:block.shape[0]] = block
231
+ block = padded
232
+
233
+ blocks.append(block)
234
+ start_indices.append(start)
235
+
236
+ return blocks, start_indices
237
+
238
+ def _normalize(self, spec):
239
+ """归一化处理"""
240
+ epsilon = 1e-8
241
+ mean = np.mean(spec)
242
+ variance = np.var(spec)
243
+ std = np.sqrt(variance)
244
+ normalized = (spec - mean) / (std + epsilon)
245
+ return normalized.astype(np.float32)
246
+
247
+ def inference(self, audio_path, model_path=None):
248
+ if model_path and not hasattr(self, 'session'):
249
+ self.load_model(model_path)
250
+
251
+ full_spec = self._preprocess_audio(audio_path)
252
+ blocks, start_indices = self._extract_blocks(full_spec)
253
+
254
+ block_results = []
255
+
256
+ print(f"开始处理音频: {audio_path}")
257
+ print(f"总帧数: {full_spec.shape[0]}, 总时长: {full_spec.shape[0] * self.frame_duration:.2f}秒")
258
+ print(f"将处理 {len(blocks)} 个块 (每块 {self.model_params['num_frames']}帧 = {self.block_duration:.3f}秒)")
259
+ print("=" * 60)
260
+
261
+ for i, block in enumerate(blocks):
262
+ start_time = time.time()
263
+
264
+ normalized_block = self._normalize(block)
265
+ input_tensor = normalized_block.flatten().reshape(self.input_shape)
266
+
267
+ input_name = self.session.get_inputs()[0].name
268
+ outputs = self.session.run(None, {input_name: input_tensor})
269
+
270
+ raw_output = outputs[0][0]
271
+ result = self._format_output(raw_output)
272
+
273
+ process_time = time.time() - start_time
274
+ start_frame = start_indices[i]
275
+ end_frame = start_frame + self.model_params['num_frames']
276
+ start_time_sec = start_frame * self.frame_duration
277
+ end_time_sec = end_frame * self.frame_duration
278
+
279
+ block_result = {
280
+ 'block_index': i,
281
+ 'start_frame': start_frame,
282
+ 'end_frame': end_frame,
283
+ 'start_time': start_time_sec,
284
+ 'end_time': end_time_sec,
285
+ 'process_time': process_time,
286
+ 'result': result,
287
+ 'raw_output': raw_output
288
+ }
289
+
290
+ block_results.append(block_result)
291
+
292
+ print(f"块 #{i+1} [时间: {start_time_sec:.2f}-{end_time_sec:.2f}s]")
293
+ print(f" 分类: {result['class']}, 置信度: {result['confidence']}%")
294
+ print(f" 处理时间: {process_time * 1000:.2f}ms")
295
+ print("-" * 50)
296
+
297
+ final_result = self._aggregate_results(block_results)
298
+ return block_results, final_result
299
+
300
+ def process_audio_segment(self, audio_segment):
301
+ """处理音频片段(用于实时处理),包含时间测量功能"""
302
+ if not hasattr(self, 'session'):
303
+ raise ValueError("请先加载模型")
304
+
305
+ # 记录总开始时间
306
+ total_start_time = time.time()
307
+
308
+ # 记录预处理时间
309
+ preprocess_start_time = time.time()
310
+ full_spec = self.preprocess_audio_segment(audio_segment)
311
+ blocks, start_indices = self._extract_blocks(full_spec)
312
+ preprocess_time = time.time() - preprocess_start_time
313
+
314
+ block_results = []
315
+ inference_time = 0.0
316
+
317
+ for i, block in enumerate(blocks):
318
+ # 记录归一化时间
319
+ normalize_start_time = time.time()
320
+ normalized_block = self._normalize(block)
321
+ input_tensor = normalized_block.flatten().reshape(self.input_shape)
322
+ normalize_time = time.time() - normalize_start_time
323
+
324
+ # 记录推理时间
325
+ inference_start_time = time.time()
326
+ input_name = self.session.get_inputs()[0].name
327
+ outputs = self.session.run(None, {input_name: input_tensor})
328
+ block_inference_time = time.time() - inference_start_time
329
+ inference_time += block_inference_time
330
+
331
+ raw_output = outputs[0][0]
332
+ result = self._format_output(raw_output)
333
+
334
+ start_frame = start_indices[i]
335
+ end_frame = start_frame + self.model_params['num_frames']
336
+ start_time_sec = start_frame * self.frame_duration
337
+ end_time_sec = end_frame * self.frame_duration
338
+
339
+ block_result = {
340
+ 'block_index': i,
341
+ 'start_frame': start_frame,
342
+ 'end_frame': end_frame,
343
+ 'start_time': start_time_sec,
344
+ 'end_time': end_time_sec,
345
+ 'result': result,
346
+ 'raw_output': raw_output,
347
+ 'normalize_time': normalize_time,
348
+ 'inference_time': block_inference_time
349
+ }
350
+
351
+ block_results.append(block_result)
352
+
353
+ final_result = self._aggregate_results(block_results)
354
+
355
+ # 计算总时间
356
+ total_time = time.time() - total_start_time
357
+
358
+ # 如果有最终结果,添加时间信息
359
+ if final_result:
360
+ final_result['preprocess_time'] = preprocess_time
361
+ final_result['inference_time'] = inference_time
362
+ final_result['total_time'] = total_time
363
+
364
+ return block_results, final_result
365
+
366
+ def _format_output(self, predictions):
367
+ class_idx = np.argmax(predictions)
368
+ confidence = int(predictions[class_idx] * 100)
369
+ if len(self.classes) > 0:
370
+ label = self.classes[class_idx] if class_idx < len(self.classes) else "未知"
371
+ else:
372
+ label = str(class_idx)
373
+ return {
374
+ 'class': label,
375
+ 'confidence': confidence,
376
+ 'probabilities': predictions.tolist()
377
+ }
378
+
379
+ def _aggregate_results(self, block_results):
380
+ """聚合所有块的结果"""
381
+ if len(block_results) == 2:
382
+ # 两个块时取置信度最高的
383
+ max_confidence = -1
384
+ best_result = None
385
+ for result in block_results:
386
+ if result['result']['confidence'] > max_confidence:
387
+ max_confidence = result['result']['confidence']
388
+ best_result = result
389
+ return {
390
+ 'class': best_result['result']['class'],
391
+ 'confidence': best_result['result']['confidence'],
392
+ 'occurrence_percentage': 100.0,
393
+ 'total_blocks': len(block_results),
394
+ 'best_raw_output': best_result['raw_output'],
395
+ 'class_distribution': {best_result['result']['class']: 1},
396
+ 'aggregation_method': 'highest_confidence'
397
+ }
398
+
399
+ # 正常情况:统计每个类别的出现次数
400
+ class_counts = {}
401
+ max_confidence = {}
402
+
403
+ for result in block_results:
404
+ class_label = result['result']['class']
405
+ confidence = result['result']['confidence']
406
+
407
+ class_counts[class_label] = class_counts.get(class_label, 0) + 1
408
+ if class_label not in max_confidence or confidence > max_confidence[class_label]:
409
+ max_confidence[class_label] = confidence
410
+
411
+ if not class_counts:
412
+ return None
413
+
414
+ # 找出最频繁的类别
415
+ most_common = max(class_counts.items(), key=lambda x: x[1])
416
+ most_common_class = most_common[0]
417
+ count = most_common[1]
418
+ percentage = (count / len(block_results)) * 100
419
+ confidence = max_confidence[most_common_class]
420
+
421
+ # 找出该类别中置信度最高的原始输出
422
+ best_raw_output = None
423
+ for result in block_results:
424
+ if result['result']['class'] == most_common_class:
425
+ if best_raw_output is None or result['result']['confidence'] > best_raw_output['result']['confidence']:
426
+ best_raw_output = result
427
+
428
+ return {
429
+ 'class': most_common_class,
430
+ 'confidence': confidence,
431
+ 'occurrence_percentage': percentage,
432
+ 'total_blocks': len(block_results),
433
+ 'best_raw_output': best_raw_output['raw_output'] if best_raw_output else None,
434
+ 'class_distribution': class_counts,
435
+ 'aggregation_method': 'majority_vote'
436
+ }
437
+