diff --git a/labels/eot-studio/README.md b/labels/eot-studio/README.md index 8e909b5..db5dfb9 100644 --- a/labels/eot-studio/README.md +++ b/labels/eot-studio/README.md @@ -32,6 +32,7 @@ npm run dev - 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。保存的分数不会随标注或转写编辑而重新计算。 +- 两个模型名称旁各有独立显示开关,默认都打开。关闭后隐藏该模型的曲线、原标注点评分、参考线及分数读数;重新打开保留原阈值。两个都关闭时显示提示,切换通话或收起再展开会保留当前显示选择。 - 仅在展开时读取当前项目录音的评分,并核对音频SHA256。更换为本地新录音、音频变化或没有对应推理数据时,不显示旧曲线。切换通话会取消旧请求,避免前一段数据覆盖当前段。 评分数据位于各模型实验目录的 `results/score-timeline.json`。更新数据时,在两个各自的本机uv环境中运行: diff --git a/labels/eot-studio/components/model-score-track.tsx b/labels/eot-studio/components/model-score-track.tsx index 669d3d0..81e885d 100644 --- a/labels/eot-studio/components/model-score-track.tsx +++ b/labels/eot-studio/components/model-score-track.tsx @@ -9,6 +9,7 @@ const EMPTY: ScoreSeries[] = []; const COLORS: Record = { maai: "#8866b2", smart_turn: "#278d95" }; export const SCORE_HEIGHT = 172; export type ScoreThresholds = Record; +export type ScoreVisibility = Record; const time = (ms: number) => `${Math.floor(ms / 60000).toString().padStart(2, "0")}:${(ms % 60000 / 1000).toFixed(3).padStart(6, "0")}`; export function useModelScores(callId: string, recordingPath: string | null, expanded: boolean) { @@ -43,17 +44,22 @@ export function useModelScores(callId: string, recordingPath: string | null, exp }; } -export function ScoreControls({ series, currentMs, thresholds, onThreshold }: { +export function ScoreControls({ series, currentMs, thresholds, onThreshold, visibility, onVisibility }: { series: ScoreSeries[]; currentMs: number; thresholds: ScoreThresholds; onThreshold: (model: ModelId, value: number) => void; + visibility: ScoreVisibility; onVisibility: (model: ModelId, visible: boolean) => void; }) { return
{series.map(row => { - const point = scoreAt(row.points, currentMs, row.sample_interval_ms * 2); + const visible = visibility[row.model]; + const point = visible ? scoreAt(row.points, currentMs, row.sample_interval_ms * 2) : null; return
- {row.name} - {point ? point[1].toFixed(3) : "—"} - diff --git a/labels/eot-studio/components/timeline.module.css b/labels/eot-studio/components/timeline.module.css index bbf829d..0585081 100644 --- a/labels/eot-studio/components/timeline.module.css +++ b/labels/eot-studio/components/timeline.module.css @@ -130,7 +130,13 @@ .modelControl output { width: 37px; font-family: monospace; font-variant-numeric: tabular-nums; text-align: center; } .modelControl label { display: flex; align-items: center; gap: 4px; color: #88927e; padding-left: 4px; border-left: 1px solid #e2e5db; } .modelControl select { padding: 1px 0; border: 0; background: transparent; font-size: 10px; color: #637258; cursor: pointer; } -.modelSwatch { width: 14px; height: 2px; background: currentColor; } +.modelVisibility { display: inline-flex; align-items: center; gap: 6px; padding: 3px 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; } +.modelSwitch { display: inline-block; position: relative; width: 26px; height: 15px; border-radius: 20px; background: currentColor; } +.modelSwitch::after { content: ""; position: absolute; top: 3px; left: 14px; width: 9px; height: 9px; border-radius: 50%; background: white; transition: left .15s ease; } +.modelVisibility[aria-checked="false"] { color: #92998c; } +.modelVisibility[aria-checked="false"] .modelSwitch { background: #c6cbbe; } +.modelVisibility[aria-checked="false"] .modelSwitch::after { left: 3px; } +.modelControl select:disabled { opacity: .45; cursor: default; } .scoreDescription { padding: 0 22px 10px; font-size: 10px; line-height: 1.7; color: #88927f; } .scoreDescription button { border: 0; background: none; color: #536f40; cursor: pointer; text-decoration: underline; font-size: inherit; } .trackNames > .scoreAxis { padding: 12px 8px; flex-direction: column; justify-content: space-between; font-size: 10px; color: #929a87; border-top: 1px solid #e7e9e0; } @@ -144,7 +150,7 @@ .scoreInspection b { font-weight: 500; } .scoreEmpty { position: sticky; left: 0; display: grid; place-items: center; color: #929b88; font-size: 11px; background: #fdfefb; border-top: 1px solid #e7e9e0; } .scoreFootnote { margin: 0; padding: 9px 22px 0; color: #8c9583; font-size: 10px; line-height: 1.7; } -.scoreToggle:focus-visible, .modelControl select:focus-visible { outline: 2px solid #758962; outline-offset: 3px; border-radius: 3px; } +.scoreToggle:focus-visible, .modelVisibility:focus-visible, .modelControl select:focus-visible { outline: 2px solid #758962; outline-offset: 3px; border-radius: 3px; } @media (max-width: 640px) { .scorePanelHeader { padding-inline: 12px; gap: 5px; } .scoreDescription, .scoreFootnote { padding-inline: 12px; } diff --git a/labels/eot-studio/components/timeline.tsx b/labels/eot-studio/components/timeline.tsx index 74ec89b..a0bd87d 100644 --- a/labels/eot-studio/components/timeline.tsx +++ b/labels/eot-studio/components/timeline.tsx @@ -12,7 +12,7 @@ import { } from "react"; import styles from "./timeline.module.css"; import { resizeTranscriptRange, snapTimelineTime, type SnapPoint } from "@/lib/timeline-edit"; -import ModelScoreTrack, { ScoreControls, SCORE_HEIGHT, useModelScores, type ScoreThresholds } from "./model-score-track"; +import ModelScoreTrack, { ScoreControls, SCORE_HEIGHT, useModelScores, type ScoreThresholds, type ScoreVisibility } from "./model-score-track"; export interface TimelineTranscript { index: number; @@ -91,7 +91,9 @@ export default function Timeline({ const scoreRegionId = useId(); const [scoresOpen, setScoresOpen] = useState(false); const [thresholds, setThresholds] = useState({ maai: .5, smart_turn: .5 }); + const [scoreVisibility, setScoreVisibility] = useState({ maai: true, smart_turn: true }); const scores = useModelScores(callId, recordingPath, scoresOpen); + const visibleScores = useMemo(() => scores.series.filter(row => scoreVisibility[row.model]), [scores.series, scoreVisibility]); const viewportRef = useRef(null); const canvasRef = useRef(null); const draggingRef = useRef< @@ -337,6 +339,7 @@ export default function Timeline({ 模型 EOT 分数{scoresOpen ? "收起" : "展开 MaAI / Smart Turn 对比"} {scoresOpen && setScoreVisibility(previous => ({ ...previous, [model]: visible }))} thresholds={thresholds} onThreshold={(model, value) => setThresholds(previous => ({ ...previous, [model]: value }))} />}
{scoresOpen &&
@@ -449,9 +452,9 @@ export default function Timeline({
{scoresOpen &&
- {scores.series.length ? - :
{scores.status === "loading" ? "正在载入评分曲线…" : "当前录音暂无可显示的模型分数"}
} + :
{scores.status === "loading" ? "正在载入评分曲线…" : scores.series.length ? "已隐藏全部模型分数,请打开上方开关查看" : "当前录音暂无可显示的模型分数"}
}
} {activeSnap &&