Serve replay playback from a cached analysis result

Replay re-ran the SDK chain on every seek; Part A's pass stores the flow
samples once. Opening a recording now reuses a compatible result or runs the
pass behind a progress/cancel surface, and one cursor with two modes
(recorded CSV, analysed) feeds the charts, the 2D canvas and the 3D pane by
frame id. A threshold, colour or idle-dot change redraws from the stored
frames and writes nothing; a cancelled pass is reported and never played.

Verified: apps/studio/tests 440 items / 0 failed / 18 skipped (15 new, no
existing assertion changed); shared 417 / 0 / 1; autocal 358.
This commit is contained in:
ykcheungab
2026-10-04 13:38:03 +08:00
parent b6f6a06851
commit cf49b0836e
11 changed files with 2731 additions and 29 deletions
+38 -4
View File
@@ -155,7 +155,8 @@
"units_newton": "Newton",
"units_raw": "Raw",
"settings": "Settings",
"settings_tip": "Show or hide the unit, record-mode and preview controls"
"settings_tip": "Show or hide the unit, record-mode and preview controls",
"stopped_analysis": "analysis"
},
"units": {
"newton_short": "N",
@@ -218,7 +219,10 @@
"mesh3d_rejected": "The 3D scene was refused: {message}\nOnly the 3D view is affected; measuring, recording and the charts work as usual.",
"mesh3d_unavailable_gl_reason": "3D is not available on this machine: {reason}\nMeasuring, recording and the charts work as usual.",
"mesh3d_settings_tip": "3D display settings (view, housing, colour scale, frame rate)",
"mesh3d_reset_tip": "Reset view: back to the framing the scene itself uses"
"mesh3d_reset_tip": "Reset view: back to the framing the scene itself uses",
"analysis_pipeline": "Analysis result (one finished pass)",
"analysis_pipeline_tip": "This frame's arrows come from a stored analysis result rather than a chain running now; changing the threshold only changes which stored samples count as active.",
"analysis_empty": "the analysis result holds no frame here"
},
"chart": {
"fn": "FN",
@@ -278,7 +282,16 @@
"warn_config_missing": "no config hash was recorded, so recomputation cannot be verified",
"warn_rows_skipped": "some unparsable CSV rows were skipped",
"warn_frames_dropped": "frames were lost to a full queue; only their index rows remain (shown as \"not recorded\")",
"warn_out_of_order": "this recording was written by an older version: its rows are not in time order, so the replay re-sorts them and window edges may differ"
"warn_out_of_order": "this recording was written by an older version: its rows are not in time order, so the replay re-sorts them and window edges may differ",
"analysis_empty": "this analysis result holds no frame to show",
"analysis_unreadable": "this frame cannot be read ({error})",
"analysis_complete": "replaying from the analysis result: {frames} frames, cursor on signal time (the recording's own frames.csv)",
"analysis_reused": "an analysis result was already stored and is being replayed: {frames} frames",
"analysis_failed": "the analysis did not finish ({reason}): playback is not served. Re-run it, or turn 「从视频重算」 off to read the recorded forces.csv",
"analysis_cancelled": "the analysis was cancelled: no complete result was written, so no playback is served",
"analysis_no_source": "this recording has no video segment to analyse",
"analysis_busy": "an analysis is already running: wait for it, or cancel it",
"analysis_grid_mismatch": "the current display settings moved the sample grid ({keys}); the stored candidate locations cannot serve it, so the replay keeps drawing the result's own grid"
},
"record": {
"label": "Record",
@@ -497,6 +510,27 @@
"severity_info": "Note",
"severity_warning": "Warning",
"severity_error": "Error",
"stats": "shown {displayed} · display drops {dropped} · compose {compose} ms · paint {paint} ms"
"stats": "shown {displayed} · display drops {dropped} · compose {compose} ms · paint {paint} ms",
"source_analysis": "Analysis result: one finished pass, read by frame",
"source_analysis_tip": "The picture comes from a stored analysis result: the chain has already run and its output is on disk, so scrubbing and playing are file reads. Changing a threshold or a colour does not recompute."
},
"analysis": {
"title": "Analysing the recording",
"subtitle": "once only: scrubbing, playing and stepping afterwards are reads",
"stage_open": "open chain",
"stage_frames": "frame by frame",
"stage_write": "write result",
"frames": "{written} / {expected} frames",
"segment": "segment {index} / {count}",
"elapsed": "{seconds:.1f} s elapsed",
"cancel": "Cancel",
"cancelling": "Cancelling…",
"cancel_tip": "Cancel this pass: the frames already written stay, but the result is marked incomplete and is never played back as if it were whole",
"reused": "a stored result is already there — nothing to recompute",
"complete": "analysis finished: {frames} frames in {seconds:.1f} s",
"cancelled": "analysis cancelled ({frames} frames written; the result is marked incomplete)",
"failed": "analysis failed: {reason}",
"incomplete": "the result is incomplete: {reason}",
"blocked": "a session is running: stop it before analysing"
}
}
+38 -4
View File
@@ -155,7 +155,8 @@
"units_newton": "牛顿",
"units_raw": "原始",
"settings": "设置",
"settings_tip": "展开或收起单位、记录模式与预览"
"settings_tip": "展开或收起单位、记录模式与预览",
"stopped_analysis": "分析"
},
"units": {
"newton_short": "N",
@@ -218,7 +219,10 @@
"mesh3d_rejected": "三维场景被拒绝:{message}\n只影响三维视图:采集、记录与力曲线照常工作。",
"mesh3d_unavailable_gl_reason": "本机无法使用三维显示:{reason}\n采集、记录与力曲线照常工作。",
"mesh3d_settings_tip": "三维显示设置(视角、外壳、色标、显示频率)",
"mesh3d_reset_tip": "复位视角:回到场景自带的取景"
"mesh3d_reset_tip": "复位视角:回到场景自带的取景",
"analysis_pipeline": "分析结果(已跑完的一遍)",
"analysis_pipeline_tip": "这一帧的箭头来自存下来的分析结果,不是此刻在跑的链路;换阈值只改变哪些样本算「有效」,不会重算。",
"analysis_empty": "分析结果里没有这一帧"
},
"chart": {
"fn": "FN",
@@ -278,7 +282,16 @@
"warn_config_missing": "找不到配置哈希,无法校验重算一致性",
"warn_rows_skipped": "有无法解析的 CSV 行被跳过",
"warn_frames_dropped": "有帧在队列溢出时丢失,仅保留索引记录(回放显示为“该帧未录制”)",
"warn_out_of_order": "该录制由旧版本写入,行序不是时间序;回放已按时间重排,区间边界可能与当时不同"
"warn_out_of_order": "该录制由旧版本写入,行序不是时间序;回放已按时间重排,区间边界可能与当时不同",
"analysis_empty": "这个分析结果里没有可显示的帧",
"analysis_unreadable": "这一帧读不出来({error})",
"analysis_complete": "已用分析结果回放:{frames} 帧,游标按信号时间(录制自己的 frames.csv)",
"analysis_reused": "找到了现成的分析结果,直接回放:{frames} 帧",
"analysis_failed": "分析未完成({reason}):不提供回放。可以重新分析,或关闭「从视频重算」看录制的 forces.csv",
"analysis_cancelled": "分析已取消:未写出完整结果,因此不提供回放",
"analysis_no_source": "这段录制没有可用于分析的视频分段",
"analysis_busy": "分析进行中:先等待或取消当前这一遍",
"analysis_grid_mismatch": "当前显示设置改了采样网格({keys}),存下来的候选位置无法服务它:回放仍按结果自己的网格绘制"
},
"record": {
"label": "记录",
@@ -497,6 +510,27 @@
"severity_info": "提示",
"severity_warning": "警告",
"severity_error": "错误",
"stats": "已显示 {displayed} · 显示跳帧 {dropped} · 合成 {compose} ms · 绘制 {paint} ms"
"stats": "已显示 {displayed} · 显示跳帧 {dropped} · 合成 {compose} ms · 绘制 {paint} ms",
"source_analysis": "分析结果:已跑完的一遍,按帧读取",
"source_analysis_tip": "画面来自存下来的分析结果:算法链路已经跑完并写入缓存,拖动时间轴与播放都只是读文件。换阈值或配色不会重算。"
},
"analysis": {
"title": "正在分析录像",
"subtitle": "只跑一遍:之后拖动时间轴、播放与单步都只是读结果",
"stage_open": "打开链路",
"stage_frames": "逐帧分析",
"stage_write": "写入结果",
"frames": "{written} / {expected} 帧",
"segment": "分段 {index} / {count}",
"elapsed": "已用 {seconds:.1f} s",
"cancel": "取消",
"cancelling": "正在取消…",
"cancel_tip": "取消这一遍:已完成的帧会留下,但结果会被标记为未完成,永远不会被当作有效结果回放",
"reused": "已有结果,无需重算",
"complete": "分析完成:{frames} 帧,用时 {seconds:.1f} s",
"cancelled": "分析已取消({frames} 帧已写完,结果标记为未完成)",
"failed": "分析失败:{reason}",
"incomplete": "结果未完成:{reason}",
"blocked": "正在采集:停止后再分析"
}
}