fix(studio): 回放视图里选了不是记录的视频时,明确说明原因并给出可行路径

- What: `_load_replay_video()` 先用与 `list_recordings()` 相同的判据确认"这是一条记录"(视频同目录
  必须有 `forces.csv`);不是记录时不再把该目录当成记录打开,而是给出原因(状态栏 + 2D 面板常驻文案)、
  在 rail 上保留该视频路径,并把「从视频重算力值」置为可用;`_can_recompute()` 统一"重算有没有东西可读"
  (记录或已选视频),`_sync_menu_checks`/`_refresh_replay_state`/`_on_recompute_toggled` 共用它;
  回放按钮在"已选视频但不是记录"时给出对应 tooltip;`_start_playback()` 遇到零长度时间轴时改为明确提示,
  不再静默取消勾选。
- Why: 用户报告"历史回放面板,选择视频后按回放键没有反应"。复现确认:`RecordingReader` 容忍空目录,
  所以随便一段视频所在目录都会被"成功"打开成一条空记录,`duration_s()==0`,按回放→`_start_playback`
  直接静默 return——按钮点了像没点。一条记录的定义(录像 + forces.csv + session.json)此前只写在
  `list_recordings` 里,回放入口没有用同一个判据。
- Verification: 复现脚本三种状态(未选/裸视频/记录内视频)行为符合预期;新增三项 UI 用例
  (裸视频被拒且说明原因并能重算、记录内视频可打开且能播、缺视频段的记录给出提示);
  apps/studio 全量通过;ui_check 576 项 0 失败(新增 6 项覆盖同一行为);locale_check 0 缺失。
- Not in this change: 让回放直接播放"非记录"的视频(那需要一条没有力值的纯视频通道;现在用「从视频重算」
  覆盖这个需求);`RecordingReader` 对空目录的容忍行为(它要允许正在写入、尚未写 session.json 的记录)。
This commit is contained in:
ykcheungab
2026-09-14 17:21:29 +08:00
parent 981c250559
commit a26f77bb67
5 changed files with 226 additions and 12 deletions
+55
View File
@@ -2992,6 +2992,61 @@ def main() -> int:
f"{replay_window.toolbar.source_summary.text()!r}",
)
# A video that is not a recording must be refused *with a reason*, because the
# reader tolerates an empty folder: accepting it left the transport playing a
# zero-length timeline, which looked like "pressing 回放 does nothing".
import cv2 as _cv2
import numpy as _np
bare_video = replay_root / "bare_video.mp4"
bare_writer = _cv2.VideoWriter(
str(bare_video), _cv2.VideoWriter_fourcc(*"mp4v"), 30.0, (64, 48)
)
for _index in range(10):
bare_writer.write(_np.zeros((48, 64, 3), dtype=_np.uint8))
bare_writer.release()
other_window = StudioMainWindow(
prefs={"language": "zh_CN", "theme": "dark", "sensor_type": "box"}
)
other_window.resize(1280, 720)
other_window.show()
app.processEvents()
other_window.brand_bar.btn_playback.click()
app.processEvents()
check(
other_window._load_replay_video(bare_video) is False,
"a video with no forces.csv beside it was accepted as a recording",
)
app.processEvents()
check(
not other_window.replay.is_loaded(),
"an empty folder was opened as a recording",
)
check(
not other_window.toolbar.btn_run.isEnabled(),
"the transport is offered although there is nothing to play",
)
check(
"forces.csv" in other_window.toolbar.btn_run.toolTip()
or "记录" in other_window.toolbar.btn_run.toolTip(),
f"the transport does not say why it cannot play: "
f"{other_window.toolbar.btn_run.toolTip()!r}",
)
check(
other_window.act_recompute.isEnabled(),
"a chosen video cannot be recomputed either: the operator has no way to view it",
)
check(
"forces.csv" in other_window.arrow_panel.image_label.text()
or "记录" in other_window.arrow_panel.image_label.text(),
f"the panel does not say what is missing: "
f"{other_window.arrow_panel.image_label.text()!r}",
)
other_window.close()
app.processEvents()
replay_window.activateWindow()
app.processEvents()
replay = replay_window.replay
replay_plots = replay_window.force_plots
check(