195 lines
15 KiB
Python
195 lines
15 KiB
Python
"""Analyze copied remote results against the existing local baselines."""
|
||||
|
|
from collections import Counter
|
|||
|
|
import csv
|
|||
|
|
import hashlib
|
|||
|
|
import json
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
HERE = Path(__file__).resolve().parent
|
|||
|
|
ROOT = HERE.parents[1]
|
|||
|
|
RESULTS = HERE/'results'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read(path):
|
|||
|
|
with path.open(encoding='utf-8-sig',newline='') as f:
|
|||
|
|
return list(csv.DictReader(f))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def metrics(rows, key):
|
|||
|
|
counts = Counter((int(r['can_reply_now']),int(r[key])) for r in rows)
|
|||
|
|
tn,fp,fn,tp = counts[0,0],counts[0,1],counts[1,0],counts[1,1]
|
|||
|
|
return dict(n=tn+fp+fn+tp,tn=tn,fp=fp,fn=fn,tp=tp,
|
|||
|
|
accuracy=(tn+tp)/len(rows),false_reply_rate=fp/(tn+fp),false_hold_rate=fn/(tp+fn))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
labels_path = ROOT/'labels/EOT标注/工作数据/eot_labels.csv'
|
|||
|
|
label_hash = hashlib.sha256(labels_path.read_bytes()).hexdigest()
|
|||
|
|
soul = json.loads((RESULTS/'work-data-can-reply/summary.json').read_text())
|
|||
|
|
soul_latency = json.loads((RESULTS/'latency.json').read_text())
|
|||
|
|
maai_dir = ROOT/'experiments/maai-ch-kyoto/results'
|
|||
|
|
smart_dir = ROOT/'experiments/smart-turn-v3.2/results'
|
|||
|
|
maai = json.loads((maai_dir/'work-data-can-reply/summary.json').read_text())
|
|||
|
|
smart = json.loads((smart_dir/'work-data-can-reply/summary.json').read_text())
|
|||
|
|
assert soul['complete'] and soul['total_points']==44 and soul_latency['complete']
|
|||
|
|
assert soul['metadata']['label_sha256']==maai['config']['label_sha256']==smart['config']['label_sha256']==label_hash
|
|||
|
|
for call,checksum in soul['audio_sha256'].items():
|
|||
|
|
path=ROOT/'data/韩泰机器人抢话/音频'/f'{call}.mp3'
|
|||
|
|
assert hashlib.sha256(path.read_bytes()).hexdigest()==checksum==maai['config']['audio_sha256'][call]
|
|||
|
|
key=lambda r:(r['call_id'],r['decision_ms'])
|
|||
|
|
points=read(RESULTS/'work-data-can-reply/predictions.csv')
|
|||
|
|
smoke=read(RESULTS/'smoke/predictions.csv')[0]
|
|||
|
|
repeated=next(r for r in points if key(r)==key(smoke))
|
|||
|
|
repeat_check=dict(key=list(key(smoke)),
|
|||
|
|
probability_difference=abs(float(smoke['probability'])-float(repeated['probability'])),
|
|||
|
|
same_asr=smoke['asr_text']==repeated['asr_text'],
|
|||
|
|
same_raw_state=smoke['raw_state']==repeated['raw_state'])
|
|||
|
|
assert repeat_check['probability_difference'] < 1e-6 and repeat_check['same_asr'], repeat_check
|
|||
|
|
m={key(r):r for r in read(maai_dir/'work-data-can-reply/predictions.csv')}
|
|||
|
|
s={key(r):r for r in read(smart_dir/'work-data-can-reply/predictions.csv')}
|
|||
|
|
labels=read(labels_path)
|
|||
|
|
assert len({key(r) for r in points})==44
|
|||
|
|
assert set(map(key,points))==set(m)==set(s)==set(map(key,labels))
|
|||
|
|
rows=[]
|
|||
|
|
frames={}
|
|||
|
|
for f in read(RESULTS/'work-data-can-reply/frame-times.csv'):
|
|||
|
|
frames.setdefault(key(f),[]).append(f)
|
|||
|
|
for r in points:
|
|||
|
|
a,b=m[key(r)],s[key(r)]
|
|||
|
|
assert r['can_reply_now']==a['can_reply_now']==b['can_reply_now']
|
|||
|
|
history=frames[key(r)]
|
|||
|
|
assert len(history)==int(r['frame_count'])
|
|||
|
|
assert int(history[-1]['input_available_ms'])==int(r['decision_ms'])
|
|||
|
|
allow=0
|
|||
|
|
last_state='initial_hold'
|
|||
|
|
last_ms=r['audio_start_ms']
|
|||
|
|
for index,f in enumerate(history):
|
|||
|
|
assert int(f['frame'])==index and int(f['input_available_ms'])<=int(r['decision_ms'])
|
|||
|
|
if f['state']=='<|user_complete|>':
|
|||
|
|
allow=1;last_state=f['state'];last_ms=f['input_available_ms']
|
|||
|
|
elif f['state'] in ('<|user_incomplete|>','<|user_nonidle|>'):
|
|||
|
|
allow=0;last_state=f['state'];last_ms=f['input_available_ms']
|
|||
|
|
rows.append(dict(call_id=r['call_id'],decision_ms=r['decision_ms'],
|
|||
|
|
user_text_at_decision=r['user_text_at_decision'],can_reply_now=r['can_reply_now'],
|
|||
|
|
soulx_probability=float(r['probability']),soulx_05=int(float(r['probability'])>.5),
|
|||
|
|
soulx_08=int(float(r['probability'])>.8),soulx_raw_state=r['raw_state'],
|
|||
|
|
soulx_native_complete=int(r['raw_state']=='<|user_complete|>'),soulx_asr=r['asr_text'],
|
|||
|
|
soulx_corrected_complete=int(r['corrected_state']=='<|user_complete|>'),
|
|||
|
|
soulx_state_can_reply=allow,soulx_last_native_decision_state=last_state,
|
|||
|
|
soulx_last_native_decision_ms=last_ms,
|
|||
|
|
maai_probability=float(a['p_now_robot']),maai_05=int(float(a['p_now_robot'])>.5),
|
|||
|
|
maai_08=int(float(a['p_now_robot'])>.8),
|
|||
|
|
maai_turnbench_score=1-float(a['p_now_user']),
|
|||
|
|
maai_zero_false_hold=int(1-float(a['p_now_user'])>.11),
|
|||
|
|
smart_probability=float(b['probability']),smart_05=int(float(b['probability'])>.5),
|
|||
|
|
smart_08=int(float(b['probability'])>.8)))
|
|||
|
|
with (RESULTS/'comparison-points.csv').open('w',encoding='utf-8-sig',newline='') as f:
|
|||
|
|
w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
|
|||
|
|
metric={k:metrics(rows,k) for k in ('soulx_05','soulx_08','soulx_native_complete','soulx_corrected_complete','soulx_state_can_reply',
|
|||
|
|
'maai_05','maai_08','maai_zero_false_hold','smart_05','smart_08')}
|
|||
|
|
assert metric['soulx_05']==soul['metrics']
|
|||
|
|
for x in ('tn','fp','fn','tp','accuracy'):
|
|||
|
|
assert metric['maai_05'][x]==maai['main_metrics'][x]
|
|||
|
|
assert metric['smart_05'][x]==smart['metrics'][x]
|
|||
|
|
baseline_latency=json.loads((ROOT/'experiments/latency-comparison.json').read_text())
|
|||
|
|
comparisons={base:dict(
|
|||
|
|
fixed=sum(r[base]!=int(r['can_reply_now']) and r['soulx_05']==int(r['can_reply_now']) for r in rows),
|
|||
|
|
regressed=sum(r[base]==int(r['can_reply_now']) and r['soulx_05']!=int(r['can_reply_now']) for r in rows))
|
|||
|
|
for base in ('maai_05','smart_05','smart_08')}
|
|||
|
|
native_comparisons={base:dict(
|
|||
|
|
fixed=sum(r[base]!=int(r['can_reply_now']) and r['soulx_state_can_reply']==int(r['can_reply_now']) for r in rows),
|
|||
|
|
regressed=sum(r[base]==int(r['can_reply_now']) and r['soulx_state_can_reply']!=int(r['can_reply_now']) for r in rows))
|
|||
|
|
for base in ('maai_05','smart_05','smart_08')}
|
|||
|
|
lowest_positive=min((r for r in rows if int(r['can_reply_now'])==1),
|
|||
|
|
key=lambda r:r['maai_turnbench_score'])
|
|||
|
|
zero_hold_scheme=dict(rule='1 - p_now[user] > 0.11',threshold=.11,
|
|||
|
|
metrics=metric['maai_zero_false_hold'],label_sha256=label_hash,
|
|||
|
|
selection='Supplementary operating point selected on these same 44 labels; not an independent validation or a guarantee of zero false holds on new audio.',
|
|||
|
|
lowest_positive=dict(call_id=lowest_positive['call_id'],
|
|||
|
|
decision_ms=lowest_positive['decision_ms'],
|
|||
|
|
score=lowest_positive['maai_turnbench_score'],
|
|||
|
|
text=lowest_positive['user_text_at_decision']),
|
|||
|
|
model_inference_unchanged=True,
|
|||
|
|
same_predictions_using_p_now_robot=all(
|
|||
|
|
r['maai_zero_false_hold']==int(r['maai_probability']>.11) for r in rows))
|
|||
|
|
assert zero_hold_scheme['metrics']['fn']==0
|
|||
|
|
assert zero_hold_scheme['same_predictions_using_p_now_robot']
|
|||
|
|
(maai_dir/'work-data-can-reply/zero-false-hold.json').write_text(
|
|||
|
|
json.dumps(zero_hold_scheme,ensure_ascii=False,indent=2)+'\n')
|
|||
|
|
summary=dict(label_sha256=label_hash,metrics=metric,changed_vs_baseline=comparisons,
|
|||
|
|
maai_zero_false_hold_scheme=zero_hold_scheme,
|
|||
|
|
repeated_point_check=repeat_check,
|
|||
|
|
native_can_reply_mapping='Supplementary post-hoc mapping: start hold; complete -> allow; incomplete/nonidle -> hold; idle/backchannel retain previous value. No timeout, no service resets, no external EOT signals, no future frames.',
|
|||
|
|
native_can_reply_changed_vs_baseline=native_comparisons,
|
|||
|
|
soulx_native_state_metrics=soul['native_state_metrics'],
|
|||
|
|
soulx_corrected_state_metrics=soul['corrected_state_metrics'],
|
|||
|
|
soulx_threshold_diagnostics=soul['thresholds'],
|
|||
|
|
latency=dict(soulx=soul_latency,local_baselines=baseline_latency),
|
|||
|
|
method=soul['method'],limitations=soul['limitations'])
|
|||
|
|
(RESULTS/'comparison-summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n')
|
|||
|
|
lines=['SoulX-Duplug / MaAI / Smart Turn:当前44个标注点对比',
|
|||
|
|
'', '数据:6段录音,can_reply=0/1各22点;标签和音频SHA256与本机基线一致。',
|
|||
|
|
'标签中29点来自xlsx草稿、15点没有明确审核信息,结果属于探索性标注点评估。',
|
|||
|
|
'', '准确率及误判(误放行:不该回复却允许;误阻止:可以回复却拦截)']
|
|||
|
|
for name,k in [('SoulX 条件完成分数 >0.5(预先定义的分数口径)','soulx_05'),('SoulX 条件完成分数 >0.8','soulx_08'),
|
|||
|
|
('SoulX 原生状态派生can_reply(辅助映射)','soulx_state_can_reply'),
|
|||
|
|
('MaAI p_now_robot >0.5','maai_05'),('MaAI p_now_robot >0.8','maai_08'),
|
|||
|
|
('MaAI 1-p_now_user >0.11(本批误阻止为0的补充方案)','maai_zero_false_hold'),
|
|||
|
|
('Smart Turn >0.5','smart_05'),('Smart Turn >0.8','smart_08')]:
|
|||
|
|
x=metric[k]
|
|||
|
|
lines.append(f"{name}:{x['accuracy']:.1%};误放行{x['fp']}/22,误阻止{x['fn']}/22;TN/FP/FN/TP={x['tn']}/{x['fp']}/{x['fn']}/{x['tp']}")
|
|||
|
|
lines.extend(['',
|
|||
|
|
'MaAI补充方案:1-p_now_user >0.11;在本批44点中与p_now_robot >0.11的判断完全一致。',
|
|||
|
|
f"可回复点的最低分为{lowest_positive['maai_turnbench_score']:.9f},来自{lowest_positive['call_id']} {lowest_positive['decision_ms']}ms:{lowest_positive['user_text_at_decision']}。",
|
|||
|
|
'阈值0.11在这批标签上选择,误阻止0/22、误放行17/22(77.27%),不能保证新数据仍零误阻止;模型推理及已测量计算延迟不变。',
|
|||
|
|
'在同为误阻止0的配置下,MaAI 0.11比Smart Turn 0.5少1个误放行,比Smart Turn 0.8多1个误放行。',
|
|||
|
|
'', 'SoulX原生五状态argmax恰为user_complete的诊断指标:'+json.dumps(metric['soulx_native_complete']),
|
|||
|
|
'SoulX内部状态修正后恰为user_complete的诊断指标:'+json.dumps(metric['soulx_corrected_complete']),
|
|||
|
|
'主指标在每个判断点比较complete/incomplete logits,排除idle/nonidle等类别后归一化;分数不是官方校准的can_reply概率。',
|
|||
|
|
'原生状态诊断与主指标不是同一种决策,不能把主指标宣称为完整官方服务效果。',
|
|||
|
|
'补充原生状态映射:开始为不可回复;complete置为可回复,incomplete/nonidle置为不可回复,idle/backchannel保持前值。只使用该标注点之前的原生预测,不加入超时、外部结束信号或音频清空。',
|
|||
|
|
'这是为区分瞬时完成事件与持续可回复状态而增加的辅助映射,保留预先定义的条件分数结果,未按标签调参。',
|
|||
|
|
'只在complete/incomplete间归一化,会忽略idle/nonidle等类别,可能产生虚高分数;条件分数结果不能直接代表原生SoulX性能。',
|
|||
|
|
'SoulX使用用户单声道及最多19.88秒真实音频;MaAI使用双声道及最多20秒;Smart Turn使用用户单声道及最多8秒。',
|
|||
|
|
'SoulX每个判断点重新初始化KV,直接使用原生infer,保留内部ASR和状态计算,不使用超时回复、完成后的服务重置或最终整句ASR。',
|
|||
|
|
'最终处理块在判断点前40ms结束,前向音频在判断点结束;不读标注点后的音频,也不输入标签转写。',
|
|||
|
|
'已知cb3238dc 13540和e304f740 11531存在标注/ASR对齐问题,未改动人工标签。',
|
|||
|
|
'', '计算延迟(跨机器配置对比)'])
|
|||
|
|
for name,x in [('MaAI / M4 CPU4线程,每100ms增量',baseline_latency['maai_4_threads_incremental']),
|
|||
|
|
('Smart Turn / M4 CPU4线程,8秒窗口',baseline_latency['smart_turn_4_threads']),
|
|||
|
|
('Smart Turn / M4 CPU1线程,8秒窗口',baseline_latency['smart_turn_1_thread']),
|
|||
|
|
('SoulX / RTX3090 + i5 CPU4线程,每160ms增量,含ASR',soul_latency['summary']['steady']['total_ms']),
|
|||
|
|
('SoulX / 同上,扣除cascade ASR调用时间',soul_latency['summary']['steady']['non_cascade_ms'])]:
|
|||
|
|
lines.append(f"{name}:均值{x['mean_ms']:.2f}ms,P95 {x['p95_ms']:.2f}ms,样本数{x['n']}")
|
|||
|
|
lines.extend([f"SoulX全段计算RTF={soul_latency['realtime_factor']:.3f},超过160ms更新周期的帧比例={soul_latency['fraction_over_160ms']:.1%}。",
|
|||
|
|
'SoulX用CUDA同步后的墙钟计时,分解音频特征/分词器、ASR阶段和状态模型,模型加载与离线重放单独记录。',
|
|||
|
|
'扣除cascade ASR的数值仍包括ASR触发检测、文本处理等,不等同于纯状态网络时间。',
|
|||
|
|
'MaAI是100ms双声道增量,Smart Turn每次计算8秒窗口,SoulX是160ms增量;处理量和硬件不同,不能据此断言同机速度排名。',
|
|||
|
|
'Smart Turn包含约0.16ms重采样;MaAI和SoulX计时输入已为16kHz,重采样/文件解码不包含在增量计时内。',
|
|||
|
|
f"SoulX实际参数dtype={soul['metadata']['parameter_dtypes']};官方配置写bf16但加载器没有转换,本次未添加自动混合精度。",
|
|||
|
|
f"SoulX点评估平均离线重放={sum(float(r['replay_inference_ms']) for r in points)/len(points):.2f}ms,不作为在线单步延迟。",
|
|||
|
|
f"SoulX热缓存加载耗时{soul['metadata']['load_ms']/1000:.2f}s(不包含Python导入),完整连续测试峰值GPU分配{soul_latency['peak_gpu_allocated_bytes']/2**30:.2f}GiB。",
|
|||
|
|
'160ms更新周期和40ms前向窗口没有计入计算耗时;约21.8%的帧计算时间超过160ms,应区分平均实时吞吐与局部排队延迟。',
|
|||
|
|
'原始最终实验日志有32次ASR list index out of range警告,官方适配器捕获后返回空文本;未过滤这些样本,日志保留以便进一步定位。',
|
|||
|
|
'', 'SoulX误判点(0.5)'])
|
|||
|
|
for r in rows:
|
|||
|
|
if r['soulx_05']!=int(r['can_reply_now']):
|
|||
|
|
lines.append(f"{r['call_id']} {r['decision_ms']}ms 标签={r['can_reply_now']} 预测={r['soulx_05']} p={r['soulx_probability']:.5f} 文本={r['user_text_at_decision']} ASR={r['soulx_asr']}")
|
|||
|
|
lines.extend(['','SoulX原生状态派生can_reply的误判点(辅助映射)'])
|
|||
|
|
for r in rows:
|
|||
|
|
if r['soulx_state_can_reply']!=int(r['can_reply_now']):
|
|||
|
|
lines.append(f"{r['call_id']} {r['decision_ms']}ms 标签={r['can_reply_now']} 预测={r['soulx_state_can_reply']} 最后决定状态={r['soulx_last_native_decision_state']} 文本={r['user_text_at_decision']} ASR={r['soulx_asr']}")
|
|||
|
|
lines.extend(['','本次数据的判断:',
|
|||
|
|
'MaAI在相同误阻止数量4点下,比SoulX原生状态辅助映射少7个误放行(7 vs 14),整体准确率最高。',
|
|||
|
|
'SoulX原生状态辅助映射相比Smart Turn 0.5少4个误放行,但多4个误阻止,准确率均为59.1%。',
|
|||
|
|
'SoulX误放行主要仍是完整短语中的停顿;原生状态辅助映射的4个误阻止集中在贵州省遵义市仁怀市、我姓宋、辽宁葫芦岛、姓赵,涉及地点/姓名和两个已知对齐疑点。',
|
|||
|
|
'条件两类分数整体偏高,0.8阈值仅拦截正确2/22个不可回复点,不适合直接当作当前业务的can_reply概率。',
|
|||
|
|
'在本次默认FP32、当前音频及映射配置下,SoulX未显示质量或计算延迟优势;不能推断其他数据、完整官方策略或BF16优化的表现。'])
|
|||
|
|
(RESULTS/'对比分析.txt').write_text('\n'.join(lines)+'\n')
|
|||
|
|
print(json.dumps(dict(metrics=metric,changed_vs_baseline=comparisons,
|
|||
|
|
soulx_latency=soul_latency['summary']['steady']['total_ms']),ensure_ascii=False,indent=2))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__=='__main__':
|
|||
|
|
main()
|