新增 Silero VAD 时间轴评分对比与可复现推理

This commit is contained in:
Xin Wang
2026-10-22 21:25:47 +08:00
parent 43724c13e4
commit c9b618e634
14 changed files with 379 additions and 14 deletions
+3
View File
@@ -0,0 +1,3 @@
.venv/
__pycache__/
models/
+1
View File
@@ -0,0 +1 @@
3.11
+25
View File
@@ -0,0 +1,25 @@
# Silero VAD 时间轴基线
本机 Apple M4 Mac mini / CPU,独立 Python 3.11 uv 环境;依赖锁见 uv.lock。ONNX Runtime CPU 单线程,不修改 MaAI 和 Smart Turn 环境。环境的实际版本、模型和录音 SHA256、标注文件 SHA256 记入 results/score-timeline.json。
曲线为 `1 - speech_probability`,高分仅表示当前用户声道更可能未发声,不是语义话轮结束概率。不加静音时长阈值、hangover 或服务层超时策略。
输入为韩泰六通原录音的用户左声道(channel 0),原生 8 kHz,int16 / 32768;同时支持 16 kHz。每个完整 32 ms 帧执行一次,使用 4 ms 过去上下文并连续保留循环状态,每通录音开始清零。分数的时间戳是帧末:只有整帧收到后才可用。不重采样,不读取标注转写,不因标注点重置,不补齐末尾残帧,不使用未来音频。
44 个标注点取该时刻之前最后一个完整帧的输出,最多陈旧 <32 ms。原始 speech 分数和可用时刻见 results/checkpoints.csv;不覆盖人工标注或其他模型结果。脚本对每通录音重放前 50 帧,验证短前缀与完整回放一致。
权重使用已安装的 `pipecat-ai==1.12.0` 发行包所带 Silero ONNX,SHA256 为 `597d30b3ec076608d059477bb14cfeffdf951bf5cae370d38f65d33bbfe82004`;发行包未标明 Silero 上游版本,故按权重哈希识别,不猜测版本。状态接口参考 [Silero 官方实现](https://github.com/snakers4/silero-vad/blob/master/src/silero_vad/utils_vad.py)。权重不加入 Git。
从项目根目录恢复及运行:
```bash
cd experiments/silero-vad
uv sync --locked
# 将上述 SHA256 的 Silero ONNX 放入 models/silero_vad.onnx。
# 本项目已有 Smart Turn 环境时,可从该固定发行包复制:
mkdir -p models
cp ../smart-turn-v3.2/.venv/lib/python3.11/site-packages/pipecat/audio/vad/data/silero_vad.onnx models/silero_vad.onnx
uv run --locked python export_timeline.py
```
网页读取随项目提交的 results/score-timeline.json 即可显示,无需在网页进程加载模型。修改录音或判断点后需重新生成;接口会校验录音 SHA256。
+104
View File
@@ -0,0 +1,104 @@
"""Causal Silero ONNX baseline: frame ends mark when scores become available."""
import bisect
import csv
import hashlib
import importlib.metadata
import json
import platform
import sys
from pathlib import Path
import numpy as np
import onnxruntime as ort
import soundfile as sf
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
WEIGHT_SHA256 = '597d30b3ec076608d059477bb14cfeffdf951bf5cae370d38f65d33bbfe82004'
class SileroStream:
"""Same state/context contract as the official Silero ONNX wrapper."""
def __init__(self, session, sr):
if sr not in (8000, 16000):
raise ValueError(f'Unsupported sample rate: {sr}')
self.session, self.sr = session, sr
self.frame = sr * 32 // 1000
self.context = np.zeros((1, sr * 4 // 1000), dtype=np.float32)
self.state = np.zeros((2, 1, 128), dtype=np.float32)
def step(self, samples):
if len(samples) != self.frame:
raise ValueError('Only complete 32 ms frames may be submitted')
x = np.concatenate([self.context, samples[None, :]], axis=1)
output, self.state = self.session.run(None, {
'input': x, 'state': self.state, 'sr': np.array(self.sr, dtype=np.int64)})
self.context = x[:, -self.context.shape[1]:].copy()
speech = float(output.item())
if not np.isfinite(speech) or not 0 <= speech <= 1:
raise ValueError('Invalid model output')
return speech
def main():
weight = HERE / 'models/silero_vad.onnx'
if hashlib.sha256(weight.read_bytes()).hexdigest() != WEIGHT_SHA256:
raise ValueError('Silero model checksum mismatch')
options = ort.SessionOptions()
options.intra_op_num_threads = options.inter_op_num_threads = 1
session = ort.InferenceSession(str(weight), sess_options=options, providers=['CPUExecutionProvider'])
label_path = ROOT / 'labels/EOT标注/工作数据/eot_labels.csv'
with label_path.open(encoding='utf-8-sig', newline='') as f:
labels = list(csv.DictReader(f))
calls, checks = [], []
for call in sorted({r['call_id'] for r in labels}):
path = ROOT / 'data/韩泰机器人抢话/音频' / f'{call}.mp3'
pcm, sr = sf.read(path, dtype='int16', always_2d=True)
stream = SileroStream(session, sr) # reset only between recordings
user = np.ascontiguousarray(pcm[:, 0].astype(np.float32) / 32768.)
points = []
for end in range(stream.frame, len(user) + 1, stream.frame):
speech = stream.step(user[end-stream.frame:end])
points.append([end * 1000 // sr, round(1 - speech, 7)])
times = [p[0] for p in points]
checkpoints = []
for row in labels:
if row['call_id'] != call:
continue
decision = int(row['decision_ms'])
if decision > len(user) * 1000 / sr:
raise ValueError('Annotation beyond recording')
index = bisect.bisect_right(times, decision) - 1
if index < 0:
continue # No complete frame is available yet; never invent zero.
available, score = points[index]
checkpoints.append([decision, score])
checks.append(dict(call_id=call, decision_ms=decision, available_ms=available,
non_speech_score=score, speech_score=round(1-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(user)*1000/sr,3),
sample_rate=sr, channel=0, points=points, checkpoints=sorted(checkpoints)))
# A fresh stream reproduces a prefix without any later samples.
witness = SileroStream(session, sr)
for index in range(min(50, len(points))):
end = (index+1)*stream.frame
assert round(1-witness.step(user[end-stream.frame:end]),7) == points[index][1]
print(f'{call}: {len(points)} frames, {len(checkpoints)} checkpoints; prefix witness passed', flush=True)
output = dict(schema_version=1, model='silero_vad', name='Silero VAD',
score='1 - speech_probability (non-speech baseline, not semantic EOT)', sample_interval_ms=32,
method='CPU, one thread. User channel 0, native 8/16 kHz, int16 / 32768. Sequential complete 32ms frames plus 4ms past context; recurrent state reset per call. Timestamp is frame END. No future audio, resampling, silence timeout, hangover or annotation text. Checkpoints hold last available complete frame; trailing partial frame is not padded.',
model_source='Silero ONNX bundled with pipecat-ai==1.12.0; upstream https://github.com/snakers4/silero-vad',
model_sha256=WEIGHT_SHA256, annotation_sha256=hashlib.sha256(label_path.read_bytes()).hexdigest(),
script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
environment=dict(machine=platform.platform(), architecture=platform.machine(), python=sys.version,
dependencies={p:importlib.metadata.version(p) for p in ['numpy','onnxruntime','soundfile']},
provider='CPUExecutionProvider', threads=1), calls=calls)
out = HERE/'results'
out.mkdir(exist_ok=True)
(out/'score-timeline.json').write_text(json.dumps(output,ensure_ascii=False,separators=(',',':'))+'\n')
with (out/'checkpoints.csv').open('w',encoding='utf-8',newline='') as f:
writer=csv.DictWriter(f,fieldnames=list(checks[0]));writer.writeheader();writer.writerows(checks)
if __name__ == '__main__':
main()
+5
View File
@@ -0,0 +1,5 @@
[project]
name = "silero-vad-timeline"
version = "0.1.0"
requires-python = ">=3.11,<3.12"
dependencies = ["numpy==2.2.6", "onnxruntime==1.24.4", "soundfile==0.13.1"]
@@ -0,0 +1,45 @@
call_id,decision_ms,available_ms,non_speech_score,speech_score
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,21780,21760,0.9981366,0.0018634
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,29860,29856,0.9990579,0.0009421
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,48125,48096,0.9909311,0.0090689
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,64000,64000,0.9952381,0.0047619
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,76672,76672,0.9917223,0.0082777
2c83253a-7977-4e3c-a387-0aa1ea06fbe1,88482,88480,0.9115956,0.0884044
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,22007,21984,0.9993713,0.0006287
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,31842,31840,0.9989146,0.0010854
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,52348,52320,0.9952291,0.0047709
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,65298,65280,0.9824033,0.0175967
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,67943,67936,0.9940477,0.0059523
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,70430,70400,0.9725721,0.0274279
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,82941,82912,0.9947128,0.0052872
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,48536,48512,0.9921364,0.0078636
872a1e67-873a-4c49-a598-b38a670afcad,17153,17152,0.9960783,0.0039217
872a1e67-873a-4c49-a598-b38a670afcad,24846,24832,0.998356,0.001644
872a1e67-873a-4c49-a598-b38a670afcad,33471,33440,0.997547,0.002453
872a1e67-873a-4c49-a598-b38a670afcad,47187,47168,0.991351,0.008649
872a1e67-873a-4c49-a598-b38a670afcad,52326,52320,0.987118,0.012882
872a1e67-873a-4c49-a598-b38a670afcad,44810,44800,0.9949316,0.0050684
872a1e67-873a-4c49-a598-b38a670afcad,49500,49472,0.9584358,0.0415642
872a1e67-873a-4c49-a598-b38a670afcad,54318,54304,0.9818157,0.0181843
872a1e67-873a-4c49-a598-b38a670afcad,45581,45568,0.8786313,0.1213687
a266b9ef-5e47-4434-a04c-ce4eacd52c19,18027,18016,0.9984287,0.0015713
a266b9ef-5e47-4434-a04c-ce4eacd52c19,24768,24768,0.9988177,0.0011823
a266b9ef-5e47-4434-a04c-ce4eacd52c19,51080,51072,0.0463527,0.9536473
a266b9ef-5e47-4434-a04c-ce4eacd52c19,68313,68288,0.9863916,0.0136084
a266b9ef-5e47-4434-a04c-ce4eacd52c19,41219,41216,0.994501,0.005499
cb3238dc-3946-42a5-aad8-9b9a54226363,13540,13536,0.9980318,0.0019682
cb3238dc-3946-42a5-aad8-9b9a54226363,27520,27520,0.9996327,0.0003673
cb3238dc-3946-42a5-aad8-9b9a54226363,60790,60768,0.9882597,0.0117403
cb3238dc-3946-42a5-aad8-9b9a54226363,70745,70720,0.9872311,0.0127689
cb3238dc-3946-42a5-aad8-9b9a54226363,19620,19616,0.9984962,0.0015038
cb3238dc-3946-42a5-aad8-9b9a54226363,41207,41184,0.9893807,0.0106193
cb3238dc-3946-42a5-aad8-9b9a54226363,49204,49184,0.9922861,0.0077139
cb3238dc-3946-42a5-aad8-9b9a54226363,58424,58400,0.9901153,0.0098847
cb3238dc-3946-42a5-aad8-9b9a54226363,73640,73632,0.9872359,0.0127641
e304f740-85d4-427e-9fd6-c5474e9cd737,18842,18816,0.9981079,0.0018921
e304f740-85d4-427e-9fd6-c5474e9cd737,29619,29600,0.997772,0.002228
e304f740-85d4-427e-9fd6-c5474e9cd737,40701,40672,0.994202,0.005798
e304f740-85d4-427e-9fd6-c5474e9cd737,54600,54592,0.9944514,0.0055486
e304f740-85d4-427e-9fd6-c5474e9cd737,65819,65792,0.9937913,0.0062087
e304f740-85d4-427e-9fd6-c5474e9cd737,11531,11520,0.9967205,0.0032795
e304f740-85d4-427e-9fd6-c5474e9cd737,62828,62816,0.9892333,0.0107667
1 call_id decision_ms available_ms non_speech_score speech_score
2 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 21780 21760 0.9981366 0.0018634
3 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 29860 29856 0.9990579 0.0009421
4 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 48125 48096 0.9909311 0.0090689
5 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 64000 64000 0.9952381 0.0047619
6 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 76672 76672 0.9917223 0.0082777
7 2c83253a-7977-4e3c-a387-0aa1ea06fbe1 88482 88480 0.9115956 0.0884044
8 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 22007 21984 0.9993713 0.0006287
9 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 31842 31840 0.9989146 0.0010854
10 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 52348 52320 0.9952291 0.0047709
11 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 65298 65280 0.9824033 0.0175967
12 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 67943 67936 0.9940477 0.0059523
13 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 70430 70400 0.9725721 0.0274279
14 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 82941 82912 0.9947128 0.0052872
15 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 48536 48512 0.9921364 0.0078636
16 872a1e67-873a-4c49-a598-b38a670afcad 17153 17152 0.9960783 0.0039217
17 872a1e67-873a-4c49-a598-b38a670afcad 24846 24832 0.998356 0.001644
18 872a1e67-873a-4c49-a598-b38a670afcad 33471 33440 0.997547 0.002453
19 872a1e67-873a-4c49-a598-b38a670afcad 47187 47168 0.991351 0.008649
20 872a1e67-873a-4c49-a598-b38a670afcad 52326 52320 0.987118 0.012882
21 872a1e67-873a-4c49-a598-b38a670afcad 44810 44800 0.9949316 0.0050684
22 872a1e67-873a-4c49-a598-b38a670afcad 49500 49472 0.9584358 0.0415642
23 872a1e67-873a-4c49-a598-b38a670afcad 54318 54304 0.9818157 0.0181843
24 872a1e67-873a-4c49-a598-b38a670afcad 45581 45568 0.8786313 0.1213687
25 a266b9ef-5e47-4434-a04c-ce4eacd52c19 18027 18016 0.9984287 0.0015713
26 a266b9ef-5e47-4434-a04c-ce4eacd52c19 24768 24768 0.9988177 0.0011823
27 a266b9ef-5e47-4434-a04c-ce4eacd52c19 51080 51072 0.0463527 0.9536473
28 a266b9ef-5e47-4434-a04c-ce4eacd52c19 68313 68288 0.9863916 0.0136084
29 a266b9ef-5e47-4434-a04c-ce4eacd52c19 41219 41216 0.994501 0.005499
30 cb3238dc-3946-42a5-aad8-9b9a54226363 13540 13536 0.9980318 0.0019682
31 cb3238dc-3946-42a5-aad8-9b9a54226363 27520 27520 0.9996327 0.0003673
32 cb3238dc-3946-42a5-aad8-9b9a54226363 60790 60768 0.9882597 0.0117403
33 cb3238dc-3946-42a5-aad8-9b9a54226363 70745 70720 0.9872311 0.0127689
34 cb3238dc-3946-42a5-aad8-9b9a54226363 19620 19616 0.9984962 0.0015038
35 cb3238dc-3946-42a5-aad8-9b9a54226363 41207 41184 0.9893807 0.0106193
36 cb3238dc-3946-42a5-aad8-9b9a54226363 49204 49184 0.9922861 0.0077139
37 cb3238dc-3946-42a5-aad8-9b9a54226363 58424 58400 0.9901153 0.0098847
38 cb3238dc-3946-42a5-aad8-9b9a54226363 73640 73632 0.9872359 0.0127641
39 e304f740-85d4-427e-9fd6-c5474e9cd737 18842 18816 0.9981079 0.0018921
40 e304f740-85d4-427e-9fd6-c5474e9cd737 29619 29600 0.997772 0.002228
41 e304f740-85d4-427e-9fd6-c5474e9cd737 40701 40672 0.994202 0.005798
42 e304f740-85d4-427e-9fd6-c5474e9cd737 54600 54592 0.9944514 0.0055486
43 e304f740-85d4-427e-9fd6-c5474e9cd737 65819 65792 0.9937913 0.0062087
44 e304f740-85d4-427e-9fd6-c5474e9cd737 11531 11520 0.9967205 0.0032795
45 e304f740-85d4-427e-9fd6-c5474e9cd737 62828 62816 0.9892333 0.0107667
File diff suppressed because one or more lines are too long
+162
View File
@@ -0,0 +1,162 @@
version = 1
revision = 3
requires-python = "==3.11.*"
[[package]]
name = "cffi"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
{ url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" },
{ url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" },
{ url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" },
{ url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" },
{ url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" },
{ url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" },
{ url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" },
{ url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" },
]
[[package]]
name = "flatbuffers"
version = "25.12.19"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
]
[[package]]
name = "mpmath"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
]
[[package]]
name = "onnxruntime"
version = "1.24.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" },
{ url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" },
{ url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" },
{ url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" },
{ url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "protobuf"
version = "7.36.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/89/5b8517baa72f84a67b8a307ba953c91057af618bf40bf676f3c03551f8f0/protobuf-7.36.2.tar.gz", hash = "sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb", size = 512737, upload-time = "2026-09-17T20:07:59.326Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e", size = 456039, upload-time = "2026-09-17T20:07:51.542Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ea/91fdf7c2b8bbd49cde056f00a9df6773532987e1c00fe2830b895af95c7e/protobuf-7.36.2-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e", size = 344219, upload-time = "2026-09-17T20:07:52.914Z" },
{ url = "https://files.pythonhosted.org/packages/17/ab/5fd5f8ece73fad885c5a09aa849b32d70472f954ba3a92d3bb5974ea953b/protobuf-7.36.2-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf", size = 357223, upload-time = "2026-09-17T20:07:53.985Z" },
{ url = "https://files.pythonhosted.org/packages/db/f3/3996583dd2906297a637af12114deddf7658af6e683fedb83be061983fb5/protobuf-7.36.2-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2", size = 343223, upload-time = "2026-09-17T20:07:54.931Z" },
{ url = "https://files.pythonhosted.org/packages/fc/1b/dcc64f358fcb51811b58ae40b3d28f820725f116d86487cc20bd4b130701/protobuf-7.36.2-cp310-abi3-win32.whl", hash = "sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728", size = 442998, upload-time = "2026-09-17T20:07:55.826Z" },
{ url = "https://files.pythonhosted.org/packages/8a/55/b77bda4e5e5f5971fb51b07663694690e9afdb9402136c16a522bd621cad/protobuf-7.36.2-cp310-abi3-win_amd64.whl", hash = "sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353", size = 456514, upload-time = "2026-09-17T20:07:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/d52c7016b04b6c5108f26691f9d33ec82a9b65d041f1a9c771137693d618/protobuf-7.36.2-py3-none-any.whl", hash = "sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e", size = 179806, upload-time = "2026-09-17T20:07:58.211Z" },
]
[[package]]
name = "pycparser"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" },
]
[[package]]
name = "silero-vad-timeline"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "numpy" },
{ name = "onnxruntime" },
{ name = "soundfile" },
]
[package.metadata]
requires-dist = [
{ name = "numpy", specifier = "==2.2.6" },
{ name = "onnxruntime", specifier = "==1.24.4" },
{ name = "soundfile", specifier = "==0.13.1" },
]
[[package]]
name = "soundfile"
version = "0.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
{ url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
{ url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
{ url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
{ url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
]
[[package]]
name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
]
+5 -4
View File
@@ -24,15 +24,16 @@ npm run dev
浏览器草稿自动保留,刷新后可恢复。网页保存不覆盖原 XLSX、初稿和录音。初稿转换方法、标签映射与限制见 [转换说明](../EOT标注/韩泰初稿/转换说明.md),原行号及原结论保留在初稿目录的 `source_rows.json` 中。原表没有时间信息,录音中未收录的开场与其他发言需人工补充。
## 模型 EOT 分数
## 模型分数对比
点击时间轴上方的“模型 EOT 分数”展开对比,默认收起。MaAI(紫色)和 Smart Turn(青色)共用录音的时间刻度、缩放、播放头及判断点参考线。点击或拖动曲线区域定位播放,悬停查看当时已产生的分数;点击空心评分点定位到原标注时刻。顶部显示播放位置最近的有效评分,暂无评分时显示“—”,不将缺失数据当作 0。
点击时间轴上方的“模型分数对比”展开对比,默认收起。VAP / MaAI(紫色)、Smart Turn(青色)和 Silero VAD(橙色)共用录音的时间刻度、缩放、播放头及判断点参考线。点击或拖动曲线区域定位播放,悬停查看当时已产生的分数;点击空心评分点定位到原标注时刻。顶部显示播放位置最近的有效评分,暂无评分时显示“—”,不将缺失数据当作 0。
- 实线阶梯曲线来自对六通完整录音的真实推理,不是用44个点插值。MaAI每100ms输入一块音频,因流式重采样缓冲,部分评分间隔为200ms;Smart Turn每200ms计算一次,并包含原标注的精确时刻。
- MaAI使用双声道连续回放、20秒KV缓存,分数为 `1 - p_now[user]`,是接话候选分数,不是校准的EOT概率。Smart Turn沿用原回测的用户声道、过去ASR定位、500ms前置音频、3秒内续说及最多8秒窗口;没有额外的结束信号、VAD停止门槛或超时放行。
- 空心点保留原44点的独立窗口回测分数。尤其MaAI的连续状态与独立窗口不同,空心点可能不落在连续曲线上。原始44点指标不会被这次时间序列覆盖。
- 参考阈值只改变虚线位置,不修改人工标签。MaAI可选0.11、0.5、0.8,Smart Turn可选0.5、0.8。保存的分数不会随标注或转写编辑而重新计算。
- 两个模型名称旁各有独立显示开关,默认都打开。关闭后隐藏该模型的曲线、原标注点评分、参考线及分数读数;重新打开保留原阈值。两个都关闭时显示提示,切换通话或收起再展开会保留当前显示选择。
- Silero VAD 为用户声道逐个完整 32 ms 帧的真实 ONNX 推理,使用 `1 - speech_probability`,仅为非发声基线,不是语义 EOT 概率。时间戳为帧末;空心点取原判断时刻最后可用帧,不读取未来音频。独立环境和复现方法见 [Silero 实验说明](../../experiments/silero-vad/README.md)。
- 参考阈值只改变虚线位置,不修改人工标签。MaAI可选0.11、0.5、0.8,Smart Turn和Silero可选0.5、0.8。保存的分数不会随标注或转写编辑而重新计算。
- 三个模型名称旁各有独立显示开关,默认都打开。关闭后隐藏该模型的曲线、原标注点评分、参考线及分数读数;重新打开保留原阈值。全部关闭时显示提示,切换通话或收起再展开会保留当前显示选择。
- 仅在展开时读取当前项目录音的评分,并核对音频SHA256。更换为本地新录音、音频变化或没有对应推理数据时,不显示旧曲线。切换通话会取消旧请求,避免前一段数据覆盖当前段。
评分数据位于各模型实验目录的 `results/score-timeline.json`。更新数据时,在两个各自的本机uv环境中运行:
@@ -9,6 +9,7 @@ const root = path.resolve(process.cwd(), "..", "..");
const sources: { model: ModelId; directory: string; name: string }[] = [
{ model: "maai", directory: "maai-ch-kyoto", name: "MaAI" },
{ model: "smart_turn", directory: "smart-turn-v3.2", name: "Smart Turn" },
{ model: "silero_vad", directory: "silero-vad", name: "Silero VAD" },
];
const json = (data: unknown, status = 200) => Response.json(data, { status, headers: { "Cache-Control": "no-store" } });
@@ -6,7 +6,7 @@ import type { TimelineAnnotation } from "./timeline";
import styles from "./timeline.module.css";
const EMPTY: ScoreSeries[] = [];
const COLORS: Record<ModelId, string> = { maai: "#8866b2", smart_turn: "#278d95" };
const COLORS: Record<ModelId, string> = { maai: "#8866b2", smart_turn: "#278d95", silero_vad: "#c47b32" };
export const SCORE_HEIGHT = 172;
export type ScoreThresholds = Record<ModelId, number>;
export type ScoreVisibility = Record<ModelId, boolean>;
@@ -86,7 +86,7 @@ export default function ModelScoreTrack({ series, width, durationMs, currentMs,
};
return <div className={styles.scoreTrack} onPointerLeave={() => setHoverMs(null)}>
<svg width={width} height={SCORE_HEIGHT} viewBox={`0 0 ${width} ${SCORE_HEIGHT}`}
role="group" aria-label="MaAI 与 Smart Turn 的 EOT 分数,纵轴 0 到 1,横轴与录音时间一致"
role="group" aria-label="VAP、Smart Turn 与 Silero VAD 分数对比,纵轴 0 到 1,横轴与录音时间一致"
onPointerMove={event => {
const bounds = event.currentTarget.getBoundingClientRect();
setHoverMs(Math.max(0, Math.min(durationMs, (event.clientX - bounds.left) / bounds.width * durationMs)));
+6 -6
View File
@@ -90,8 +90,8 @@ export default function Timeline({
const helpId = useId();
const scoreRegionId = useId();
const [scoresOpen, setScoresOpen] = useState(false);
const [thresholds, setThresholds] = useState<ScoreThresholds>({ maai: .5, smart_turn: .5 });
const [scoreVisibility, setScoreVisibility] = useState<ScoreVisibility>({ maai: true, smart_turn: true });
const [thresholds, setThresholds] = useState<ScoreThresholds>({ maai: .5, smart_turn: .5, silero_vad: .5 });
const [scoreVisibility, setScoreVisibility] = useState<ScoreVisibility>({ maai: true, smart_turn: true, silero_vad: true });
const scores = useModelScores(callId, recordingPath, scoresOpen);
const visibleScores = useMemo(() => scores.series.filter(row => scoreVisibility[row.model]), [scores.series, scoreVisibility]);
const viewportRef = useRef<HTMLDivElement>(null);
@@ -336,7 +336,7 @@ export default function Timeline({
<button type="button" className={styles.scoreToggle} aria-expanded={scoresOpen} aria-controls={scoreRegionId}
onClick={() => setScoresOpen(value => !value)}>
<span aria-hidden="true" className={scoresOpen ? styles.chevronOpen : styles.chevron}>›</span>
<strong>模型 EOT 分数</strong><span>{scoresOpen ? "收起" : "展开 MaAI / Smart Turn 对比"}</span>
<strong>模型分数对比</strong><span>{scoresOpen ? "收起" : "展开 VAP / Smart Turn / Silero VAD 对比"}</span>
</button>
{scoresOpen && <ScoreControls series={scores.series} currentMs={current}
visibility={scoreVisibility} onVisibility={(model, visible) => setScoreVisibility(previous => ({ ...previous, [model]: visible }))}
@@ -344,7 +344,7 @@ export default function Timeline({
</div>
{scoresOpen && <div className={styles.scoreDescription} role="status">
{scores.status === "loading" ? "正在读取模型分数…" : scores.status === "error" ? <>{scores.messages.join(" ")} <button type="button" onClick={scores.retry}>重新读取</button></>
: <>实线:连续回放评分 · 空心点:原标注点评分 · 虚线:参考阈值。分数越高越倾向接话,MaAI 使用 1 − 用户近期发声分数。{scores.messages.join(" ")}</>}
: <>实线:连续回放评分 · 空心点:原标注点评分 · 虚线:参考阈值。Smart Turn 表示完成倾向;VAP 使用 1 − 用户近期发声分数,Silero 使用 1 − 当前发声分数,仅为非发声基线,不是语义 EOT 概率。{scores.messages.join(" ")}</>}
</div>}
<div className={styles.trackLayout}>
@@ -354,7 +354,7 @@ export default function Timeline({
<div className={styles.speechName}><span className={styles.agentDot} />机器人</div>
<div className={styles.speechName}><span className={styles.userDot} />用户</div>
<div className={styles.markerName}>判断点</div>
{scoresOpen && <div className={styles.scoreAxis} style={{ height: SCORE_HEIGHT }}><span>1.0</span><span>接话分数</span><span>0.0</span></div>}
{scoresOpen && <div className={styles.scoreAxis} style={{ height: SCORE_HEIGHT }}><span>1.0</span><span>对比分数</span><span>0.0</span></div>}
</div>
<div className={styles.viewport} ref={viewportRef}>
<div
@@ -473,7 +473,7 @@ export default function Timeline({
</div>
</div>
{scoresOpen && scores.series.length > 0 && <p className={styles.scoreFootnote}>点击曲线定位录音;悬停查看分数。MaAI 约每 100 ms、Smart Turn 每 200 ms 更新。原标注点评分使用独立历史窗口,与连续曲线可能不同;编辑标注或转写不会重算已有分数。</p>}
{scoresOpen && scores.series.length > 0 && <p className={styles.scoreFootnote}>点击曲线定位录音;悬停查看分数。VAP 约每 100 ms、Smart Turn 每 200 ms、Silero 每 32 ms 更新。Silero 空心点取判断前最后完整帧;其他模型原标注点评分使用独立历史窗口,与连续曲线可能不同;编辑标注或转写不会重算已有分数。</p>}
<div className={styles.bottomBar}>
<div className={styles.timeReadout}><span className={styles.playheadDot} /> 播放 <strong>{timeLabel(current, true)}</strong></div>
+1 -1
View File
@@ -1,4 +1,4 @@
export type ModelId = "maai" | "smart_turn";
export type ModelId = "maai" | "smart_turn" | "silero_vad";
export type ScorePoint = [timeMs: number, score: number];
export interface ScoreCall {
+18 -1
View File
@@ -38,12 +38,13 @@ test("curve gaps are not connected and steps remain on the audio clock", () => {
test("recorded inference exports agree on audio identity and retain all 44 checkpoints", async () => {
const files = [
["maai", "maai-ch-kyoto"], ["smart_turn", "smart-turn-v3.2"],
["maai", "maai-ch-kyoto"], ["smart_turn", "smart-turn-v3.2"], ["silero_vad", "silero-vad"],
] as const;
const exports = await Promise.all(files.map(async ([model, directory]) => parseScoreExport(
JSON.parse(await readFile(new URL(`../../../experiments/${directory}/results/score-timeline.json`, import.meta.url), "utf8")), model)));
const identity = (data: typeof exports[number]) => data.calls.map(call => [call.call_id, call.audio_path, call.audio_sha256, call.duration_ms]);
assert.deepEqual(identity(exports[0]), identity(exports[1]));
assert.deepEqual(identity(exports[0]), identity(exports[2]));
for (const data of exports) {
assert.equal(data.calls.reduce((n, call) => n + call.checkpoints.length, 0), 44);
for (const call of data.calls) {
@@ -52,3 +53,19 @@ test("recorded inference exports agree on audio identity and retain all 44 check
}
}
});
test("Silero checkpoints use only the last available frame, and speech is inverted once", async () => {
const data = parseScoreExport(JSON.parse(await readFile(new URL(
"../../../experiments/silero-vad/results/score-timeline.json", import.meta.url), "utf8")), "silero_vad");
assert.equal(data.sample_interval_ms, 32);
for (const call of data.calls) {
assert.equal(call.points[0][0], 32);
for (const [ms] of call.points) assert.equal(ms % 32, 0);
for (const [ms, score] of call.checkpoints) {
const frame = scoreAt(call.points, ms, 32);
assert.ok(frame);
assert.equal(frame[1], score);
assert.ok(frame[0] <= ms && ms - frame[0] < 32);
}
}
});