新增 Mimi 与仅用户 VAP 对比并统一流式评测
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export type ModelId = "maai" | "smart_turn" | "silero_vad" | "livekit_mini" | "turnsense";
|
||||
export type ModelId = "maai" | "maai_user_only" | "maai_mimi_standard" | "maai_mimi_mc" | "smart_turn" | "silero_vad" | "livekit_mini" | "turnsense";
|
||||
export type ScorePoint = [timeMs: number, score: number];
|
||||
|
||||
export interface ScoreCall {
|
||||
@@ -8,6 +8,7 @@ export interface ScoreCall {
|
||||
duration_ms: number;
|
||||
points: ScorePoint[];
|
||||
checkpoints: ScorePoint[];
|
||||
checkpoint_sources?: [decisionMs: number, sourceTimeMs: number][];
|
||||
}
|
||||
|
||||
export interface ScoreExport {
|
||||
@@ -16,6 +17,7 @@ export interface ScoreExport {
|
||||
name: string;
|
||||
score: string;
|
||||
sample_interval_ms: number;
|
||||
checkpoint_mode?: "streaming" | "independent_window";
|
||||
calls: ScoreCall[];
|
||||
}
|
||||
|
||||
@@ -24,6 +26,7 @@ export interface ScoreSeries extends ScoreCall {
|
||||
name: string;
|
||||
score: string;
|
||||
sample_interval_ms: number;
|
||||
checkpoint_mode?: "streaming" | "independent_window";
|
||||
}
|
||||
|
||||
export interface ScoreResponse {
|
||||
@@ -51,6 +54,7 @@ export function parseScoreExport(value: unknown, model: ModelId): ScoreExport {
|
||||
const data = value as ScoreExport;
|
||||
if (!data || data.schema_version !== 1 || data.model !== model
|
||||
|| typeof data.name !== "string" || typeof data.score !== "string"
|
||||
|| (data.checkpoint_mode !== undefined && !["streaming", "independent_window"].includes(data.checkpoint_mode))
|
||||
|| !Number.isFinite(data.sample_interval_ms) || data.sample_interval_ms <= 0
|
||||
|| !Array.isArray(data.calls)) throw new Error("模型评分文件格式不正确。");
|
||||
const calls = new Set<string>();
|
||||
@@ -61,11 +65,34 @@ export function parseScoreExport(value: unknown, model: ModelId): ScoreExport {
|
||||
|| !validPoints(call.points, call.duration_ms) || !validPoints(call.checkpoints, call.duration_ms)) {
|
||||
throw new Error("模型评分包含重复时间、越界分数或缺失的录音信息。");
|
||||
}
|
||||
if (data.checkpoint_mode === "streaming") {
|
||||
if (!Array.isArray(call.checkpoint_sources) || call.checkpoint_sources.length !== call.checkpoints.length) {
|
||||
throw new Error("流式判断点缺少实际评分时刻。");
|
||||
}
|
||||
call.checkpoints.forEach(([decision, score], index) => {
|
||||
const point = scoreAt(call.points, decision, data.sample_interval_ms * 2);
|
||||
const source = call.checkpoint_sources![index];
|
||||
if (!point || !Array.isArray(source) || source.length !== 2 || source[0] !== decision
|
||||
|| source[1] !== point[0] || score !== point[1]) {
|
||||
throw new Error("流式判断点必须取判断前最后可用的曲线分数。");
|
||||
}
|
||||
});
|
||||
}
|
||||
calls.add(call.call_id);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Streaming markers follow current annotations, using the exact hover rule. */
|
||||
export function checkpointsForDisplay(series: ScoreSeries, decisionTimes: readonly number[]): ScorePoint[] {
|
||||
if (series.checkpoint_mode !== "streaming") return series.checkpoints;
|
||||
return [...new Set(decisionTimes)].sort((a, b) => a - b).flatMap(ms => {
|
||||
if (!Number.isFinite(ms) || ms < 0 || ms > series.duration_ms) return [];
|
||||
const point = scoreAt(series.points, ms, series.sample_interval_ms * 2);
|
||||
return point ? [[ms, point[1]] as ScorePoint] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/** Only an already available sample may describe a playhead/hover position. */
|
||||
export function scoreAt(points: readonly ScorePoint[], ms: number, maxAgeMs: number): ScorePoint | null {
|
||||
let low = 0, high = points.length - 1, found = -1;
|
||||
@@ -81,11 +108,13 @@ export function scorePath(points: readonly ScorePoint[], duration: number, width
|
||||
height: number, interval: number): string {
|
||||
const x = (t: number) => (t / duration * width).toFixed(2);
|
||||
const y = (p: number) => (12 + (1 - p) * (height - 24)).toFixed(2);
|
||||
let previous: ScorePoint | null = null;
|
||||
return points.map(point => {
|
||||
const step = previous && point[0] - previous[0] <= interval * 2
|
||||
? `H${x(point[0])}V${y(point[1])}` : `M${x(point[0])},${y(point[1])}`;
|
||||
previous = point;
|
||||
return step;
|
||||
return points.map((point, index) => {
|
||||
const previous = points[index - 1];
|
||||
const start = previous && point[0] - previous[0] <= interval * 2
|
||||
? `V${y(point[1])}` : `M${x(point[0])},${y(point[1])}`;
|
||||
// Match scoreAt's validity period, including the final sample and the
|
||||
// valid beginning of a gap. Never draw across unavailable/stale time.
|
||||
const until = Math.min(points[index + 1]?.[0] ?? duration, duration, point[0] + interval * 2);
|
||||
return start + (until > point[0] ? `H${x(until)}` : "");
|
||||
}).join("");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ModelId } from "./model-scores";
|
||||
|
||||
export const SCORE_MODELS: Record<ModelId, {
|
||||
name: string; family: string; color: string; directory: string;
|
||||
name: string; family: string; color: string; directory: string; scoreFile?: string;
|
||||
thresholds: readonly number[]; defaultThreshold: number; defaultVisible: boolean; description: string;
|
||||
}> = {
|
||||
maai: { name: "VAP / MaAI", family: "活动预测", color: "#8866b2", directory: "maai-ch-kyoto", thresholds: [.11, .5, .8], defaultThreshold: .5, defaultVisible: true, description: "1 − 用户近期发声分数;约每 100 ms 更新,不是校准的 EOT 概率。" },
|
||||
maai: { name: "VAP / MaAI", family: "活动预测", color: "#8866b2", directory: "maai-ch-kyoto", scoreFile: "streaming/cpc-stereo/score-timeline.json", thresholds: [.11, .5, .8], defaultThreshold: .5, defaultVisible: true, description: "CPC 编码器、降噪版,用户与机器人双声道;流式评分,圆点取判断前最后可用分数。1 − 用户近期发声分数,约每 100 ms 更新,不是校准的 EOT 概率。" },
|
||||
maai_user_only: { name: "VAP / MaAI(仅用户)", family: "活动预测", color: "#6c8746", directory: "maai-ch-kyoto", scoreFile: "streaming/cpc-user-only/score-timeline.json", thresholds: [.11, .5, .8], defaultThreshold: .5, defaultVisible: false, description: "保留用户语音,机器人输入全程静音;流式评分,圆点取判断前最后可用分数。1 − 用户近期发声分数,约每 100 ms 更新,与双声道版使用相同权重。仅改变模型输入,回放仍为原录音;圆点取判断前最后可用的流式分数,不是校准的 EOT 概率。" },
|
||||
maai_mimi_standard: { name: "VAP / MaAI Mimi(标准双声道)", family: "活动预测", color: "#4b859f", directory: "maai-ch-kyoto", scoreFile: "streaming/mimi-standard/score-timeline.json", thresholds: [.11, .5, .8], defaultThreshold: .5, defaultVisible: false, description: "Mimi 编码器、标准版,用户与机器人双声道;流式评分,圆点取判断前最后可用分数。1 − 用户近期发声分数,每 80 ms 更新,20 秒上下文,不是校准的 EOT 概率。" },
|
||||
maai_mimi_mc: { name: "VAP / MaAI Mimi(降噪双声道)", family: "活动预测", color: "#a58537", directory: "maai-ch-kyoto", scoreFile: "streaming/mimi-mc/score-timeline.json", thresholds: [.11, .5, .8], defaultThreshold: .5, defaultVisible: false, description: "Mimi 编码器、降噪版,用户与机器人双声道;流式评分,圆点取判断前最后可用分数。1 − 用户近期发声分数,每 80 ms 更新,20 秒上下文,不是校准的 EOT 概率。" },
|
||||
smart_turn: { name: "Smart Turn", family: "话轮检测", color: "#278d95", directory: "smart-turn-v3.2", thresholds: [.5, .8], defaultThreshold: .5, defaultVisible: true, description: "完成倾向;最近最多 8 秒用户音频,每 200 ms 更新。" },
|
||||
silero_vad: { name: "Silero VAD", family: "活动预测", color: "#c47b32", directory: "silero-vad", thresholds: [.5, .8], defaultThreshold: .5, defaultVisible: false, description: "1 − 发声概率;每 32 ms 更新,仅为非发声基线,不是语义 EOT 概率。" },
|
||||
livekit_mini: { name: "LiveKit v1-mini", family: "话轮检测", color: "#5378c4", directory: "livekit-v1-mini", thresholds: [.355, .5, .8], defaultThreshold: .355, defaultVisible: false, description: "P(turn ended);最近 1.2 秒用户音频,每 200 ms 更新。0.355 为中文参考阈值。" },
|
||||
|
||||
Reference in New Issue
Block a user