新增 Silero VAD 时间轴评分对比与可复现推理
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
models/
|
||||
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -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。
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
File diff suppressed because one or more lines are too long
Generated
+162
@@ -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" },
|
||||
]
|
||||
@@ -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)));
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user