为 EOT Studio 增加可展开的模型分数时间轴

This commit is contained in:
Xin Wang
2026-10-14 10:51:29 +08:00
parent 05f67739ce
commit 22556c67a4
12 changed files with 532 additions and 2 deletions
@@ -0,0 +1,57 @@
"""Export causal, continuous MaAI scores for EOT Studio (no label gating)."""
import csv
import hashlib
import json
from smoke_run import HERE, ROOT, EXPECTED, WEIGHTS, CPC, np, sf, soxr, torch
from evaluate_can_reply import model
def main():
torch.set_num_threads(4)
torch.set_num_interop_threads(1)
for p in (WEIGHTS, CPC):
assert hashlib.sha256(p.read_bytes()).hexdigest() == EXPECTED[p.name]
with (HERE/'results/work-data-can-reply/predictions.csv').open(encoding='utf-8-sig') as f:
checkpoints = list(csv.DictReader(f))
calls = []
for call in sorted({r['call_id'] for r in checkpoints}):
path = ROOT/'data/韩泰机器人抢话/音频'/f'{call}.mp3'
pcm, sr = sf.read(path, dtype='float32', always_2d=True)
assert pcm.shape[1] == 2
maai = model()
maai.CALC_PROCESS_TIME_INTERVAL = 10**9
resampler = soxr.ResampleStream(sr,16000,2,dtype='float32',quality='HQ')
pending = np.empty((0,2),dtype=np.float32)
points = []
# Streaming resampling never reads the next input block. Timestamp each
# result when its audio is actually available, including filter delay.
for start in range(0,len(pcm),sr//10):
end = min(len(pcm),start+sr//10)
block = resampler.resample_chunk(pcm[start:end],last=end==len(pcm))
pending = np.concatenate([pending,block])
score = None
while len(pending)>=1600:
frame,pending = pending[:1600],pending[1600:]
maai.process(np.ascontiguousarray(frame[:,0]),np.ascontiguousarray(frame[:,1]))
result = maai.result_dict_queue.get_nowait()
score = 1-float(result['p_now'][0])
assert np.isfinite(score) and 0<=score<=1
if score is not None:
points.append([round(end*1000/sr,3),round(score,7)])
calls.append(dict(call_id=call,audio_path=f'韩泰机器人抢话/音频/{call}.mp3',
audio_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
duration_ms=round(len(pcm)*1000/sr,3),points=points,
checkpoints=sorted([[int(r['decision_ms']),round(1-float(r['p_now_user']),7)]
for r in checkpoints if r['call_id']==call])))
del maai
print(f'{call[:8]}: {len(points)} timeline scores',flush=True)
output = dict(schema_version=1,model='maai',name='MaAI',sample_interval_ms=100,
score='1 - p_now[user]',
method='Continuous stereo replay, 16kHz, 10Hz, CPC + ch_kyoto noise-robust VAP, 20s KV cache. Causal streaming resampler; timestamps denote input availability. Checkpoints retain the earlier independent <=20s point evaluation.',
model_sha256=EXPECTED,script_sha256=hashlib.sha256((HERE/'export_timeline.py').read_bytes()).hexdigest(),calls=calls)
(HERE/'results/score-timeline.json').write_text(json.dumps(output,ensure_ascii=False,separators=(',',':'))+'\n')
if __name__=='__main__':
main()
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
"""Export real Smart Turn scores across complete recordings for EOT Studio."""
import csv
import hashlib
import json
import numpy as np
import soundfile as sf
from loguru import logger
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
from evaluate_can_reply import HERE, ROOT, block_origin
def read(p):
with p.open(encoding='utf-8-sig') as f:
return list(csv.DictReader(f))
def main():
logger.remove()
checkpoints = read(HERE/'results/work-data-can-reply/predictions.csv')
history = read(ROOT/'labels/EOT标注/工作数据/asr_history.csv')
events = read(HERE/'results/work-data-pipecat/events.csv')
weight = HERE/'smart-turn-v3.2-cpu.onnx'
analyzer = LocalSmartTurnAnalyzerV3(smart_turn_model_path=str(weight),cpu_count=4)
calls=[]
for call in sorted({r['call_id'] for r in checkpoints}):
path=ROOT/'data/韩泰机器人抢话/音频'/f'{call}.mp3'
pcm,sr=sf.read(path,dtype='int16',always_2d=True)
duration=round(len(pcm)*1000/sr,3)
call_labels=[r for r in checkpoints if r['call_id']==call]
exact={int(r['decision_ms']):float(r['probability']) for r in call_labels}
times=sorted(set(range(200,int(duration)+1,200))|set(exact)|{int(duration)})
call_history=[r for r in history if r['call_id']==call]
starts=sorted(float(e['event_ms']) for e in events if e['call_id']==call and e['event']=='vad_start')
analyzer.set_sample_rate(sr)
points=[]
for decision in times:
origin=block_origin(call_history,decision)
if origin is None:
past=[t for t in starts if t<=decision]
origin=max(0,past[-1]+32-200-500) if past else 0
end=decision*sr//1000
start=max(round(origin*sr/1000),end-sr*8)
assert 0<=start<end<=len(pcm)
# Endpoint resampling can only read the already cropped prefix.
samples=np.ascontiguousarray(pcm[start:end,0].astype(np.float32)/32768.)
score=float(analyzer._predict_endpoint(samples)['probability'])
assert np.isfinite(score) and 0<=score<=1
if decision in exact:
assert abs(score-exact[decision])<1e-4,(call,decision,score,exact[decision])
points.append([decision,round(score,7)])
calls.append(dict(call_id=call,audio_path=f'韩泰机器人抢话/音频/{call}.mp3',
audio_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),duration_ms=duration,
points=points,checkpoints=[[t,round(p,7)] for t,p in sorted(exact.items())]))
print(f'{call[:8]}: {len(points)} timeline scores; checkpoints verified',flush=True)
output=dict(schema_version=1,model='smart_turn',name='Smart Turn',sample_interval_ms=200,
score='completion probability',
method='Direct endpoint call every 200ms and at every original annotation. Same past-ASR block origin, 500ms pre-speech, <=3s continuation and last 8s user-channel audio as the point baseline; past VAD fallback when ASR missing. No EOT/timeout gates. Crop before resampling.',
model_sha256=hashlib.sha256(weight.read_bytes()).hexdigest(),
script_sha256=hashlib.sha256((HERE/'export_timeline.py').read_bytes()).hexdigest(),calls=calls)
(HERE/'results/score-timeline.json').write_text(json.dumps(output,ensure_ascii=False,separators=(',',':'))+'\n')
if __name__=='__main__':
main()
File diff suppressed because one or more lines are too long