HongzeFu commited on
Commit
a74e633
·
1 Parent(s): 4ccc0e4
gradio-web/gradio_callbacks.py CHANGED
@@ -383,6 +383,7 @@ def switch_to_execute_phase(uid):
383
  gr.update(interactive=False), # next_task_btn
384
  _live_obs_update(interactive=False), # img_display
385
  gr.update(interactive=False), # reference_action_btn
 
386
  )
387
 
388
 
@@ -400,26 +401,50 @@ def switch_to_action_phase(uid=None):
400
  )
401
 
402
 
403
- def on_video_end_transition(uid, ui_phase=None):
404
- """Transition from video phase back to the action phase."""
405
  LOGGER.debug(
406
- "on_video_end_transition uid=%s ui_phase=%s",
407
  _uid_for_log(uid),
408
  ui_phase,
409
  )
410
- log_update = gr.update()
411
- if ui_phase == "demo_video" or ui_phase is None:
412
- log_update = _action_selection_log()
413
  return (
414
  gr.update(visible=False), # video_phase_group
415
  gr.update(visible=True), # action_phase_group
416
  gr.update(visible=True), # control_panel_group
417
- log_update, # log_output
418
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
419
  "action_point", # ui_phase_state
420
  )
421
 
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  def _task_load_failed_response(uid, message):
424
  LOGGER.warning("task_load_failed uid=%s message=%s", _uid_for_log(uid), message)
425
  _clear_execution_video_path(uid)
@@ -432,6 +457,7 @@ def _task_load_failed_response(uid, message):
432
  "", # goal_box
433
  _ui_text("coords", "not_needed"), # coords_box
434
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display
 
435
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
436
  "", # task_info_box
437
  "", # progress_info_box
@@ -439,6 +465,7 @@ def _task_load_failed_response(uid, message):
439
  gr.update(interactive=False), # next_task_btn
440
  gr.update(interactive=False), # exec_btn
441
  gr.update(visible=False), # video_phase_group
 
442
  gr.update(visible=False), # action_phase_group
443
  gr.update(visible=False), # control_panel_group
444
  gr.update(value=""), # task_hint_display
@@ -507,6 +534,7 @@ def _load_status_task(uid, status):
507
  "", # goal_box
508
  _ui_text("coords", "not_needed"), # coords_box
509
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display
 
510
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
511
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
512
  progress_text, # progress_info_box
@@ -514,6 +542,7 @@ def _load_status_task(uid, status):
514
  gr.update(interactive=True), # next_task_btn
515
  gr.update(interactive=False), # exec_btn
516
  gr.update(visible=False), # video_phase_group
 
517
  gr.update(visible=True), # action_phase_group
518
  gr.update(visible=True), # control_panel_group
519
  gr.update(value=get_task_hint(env_id) if env_id else ""), # task_hint_display
@@ -574,6 +603,7 @@ def _load_status_task(uid, status):
574
  goal_text, # goal_box
575
  _ui_text("coords", "not_needed"), # coords_box
576
  gr.update(value=demo_video_path, visible=True, autoplay=False, playback_position=0), # video_display
 
577
  gr.update(visible=True, interactive=True), # watch_demo_video_btn
578
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
579
  progress_text, # progress_info_box
@@ -581,6 +611,7 @@ def _load_status_task(uid, status):
581
  gr.update(interactive=True), # next_task_btn
582
  gr.update(interactive=True), # exec_btn
583
  gr.update(visible=True), # video_phase_group
 
584
  gr.update(visible=False), # action_phase_group
585
  gr.update(visible=False), # control_panel_group
586
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
@@ -598,6 +629,7 @@ def _load_status_task(uid, status):
598
  goal_text, # goal_box
599
  _ui_text("coords", "not_needed"), # coords_box
600
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display (no video)
 
601
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
602
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
603
  progress_text, # progress_info_box
@@ -605,6 +637,7 @@ def _load_status_task(uid, status):
605
  gr.update(interactive=True), # next_task_btn
606
  gr.update(interactive=True), # exec_btn
607
  gr.update(visible=False), # video_phase_group
 
608
  gr.update(visible=True), # action_phase_group
609
  gr.update(visible=True), # control_panel_group
610
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
@@ -1010,15 +1043,17 @@ def execute_step(uid, option_idx, coords_str):
1010
  restart_update=gr.update(interactive=True),
1011
  next_update=gr.update(interactive=True),
1012
  exec_update=gr.update(interactive=True),
1013
- video_update=None,
1014
  options_update=gr.update(interactive=True),
1015
  coords_update=None,
1016
  reference_update=gr.update(interactive=True),
 
 
1017
  show_execution_video=False,
1018
  ui_phase="action_point",
1019
  ):
1020
- if video_update is None:
1021
- video_update = gr.update(value=None, visible=False, autoplay=False, playback_position=0)
1022
  if coords_update is None:
1023
  coords_update = _ui_text("coords", "not_needed")
1024
  return (
@@ -1029,14 +1064,15 @@ def execute_step(uid, option_idx, coords_str):
1029
  restart_update,
1030
  next_update,
1031
  exec_update,
1032
- video_update,
1033
- gr.update(visible=False, interactive=False), # watch_demo_video_btn
1034
- gr.update(visible=show_execution_video), # video_phase_group
1035
  gr.update(visible=not show_execution_video), # action_phase_group
1036
- gr.update(visible=not show_execution_video), # control_panel_group
 
1037
  options_update,
1038
  coords_update,
1039
  reference_update,
 
 
1040
  ui_phase,
1041
  )
1042
 
@@ -1051,6 +1087,8 @@ def execute_step(uid, option_idx, coords_str):
1051
  exec_update=gr.update(interactive=False),
1052
  options_update=gr.update(interactive=False),
1053
  reference_update=gr.update(interactive=False),
 
 
1054
  show_execution_video=False,
1055
  )
1056
 
@@ -1092,6 +1130,8 @@ def execute_step(uid, option_idx, coords_str):
1092
  exec_update=gr.update(interactive=True),
1093
  options_update=gr.update(choices=_build_radio_choices(session), value=None, interactive=True),
1094
  reference_update=gr.update(interactive=True),
 
 
1095
  show_execution_video=False,
1096
  )
1097
 
@@ -1115,6 +1155,8 @@ def execute_step(uid, option_idx, coords_str):
1115
  options_update=gr.update(choices=_build_radio_choices(session), value=None, interactive=True),
1116
  coords_update=coords_str,
1117
  reference_update=gr.update(interactive=True),
 
 
1118
  show_execution_video=False,
1119
  )
1120
 
@@ -1246,15 +1288,25 @@ def execute_step(uid, option_idx, coords_str):
1246
 
1247
  # 根据视图模式重新获取图片
1248
  img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
1249
- video_update = _build_execution_video_update(uid, session)
1250
- show_execution_video = video_update.get("visible") is True
1251
  radio_choices = _build_radio_choices(session)
1252
- restart_episode_update = gr.update(interactive=True)
1253
- next_task_update = gr.update(interactive=True)
1254
- exec_btn_update = gr.update(interactive=False) if done else gr.update(interactive=True)
1255
- options_update = gr.update(choices=radio_choices, value=None, interactive=True)
 
 
 
 
 
 
 
 
 
1256
  coords_update = _ui_text("coords", "not_needed")
1257
- reference_update = gr.update(interactive=True)
 
1258
 
1259
  # 格式化日志消息为 HTML 格式(支持颜色显示)
1260
  formatted_status = format_log_markdown(status)
@@ -1276,10 +1328,12 @@ def execute_step(uid, option_idx, coords_str):
1276
  restart_update=restart_episode_update,
1277
  next_update=next_task_update,
1278
  exec_update=exec_btn_update,
1279
- video_update=video_update,
1280
  options_update=options_update,
1281
  coords_update=coords_update,
1282
  reference_update=reference_update,
 
 
1283
  show_execution_video=show_execution_video,
1284
  ui_phase="execution_video" if show_execution_video else "action_point",
1285
  )
 
383
  gr.update(interactive=False), # next_task_btn
384
  _live_obs_update(interactive=False), # img_display
385
  gr.update(interactive=False), # reference_action_btn
386
+ gr.update(interactive=False), # task_hint_display
387
  )
388
 
389
 
 
401
  )
402
 
403
 
404
+ def on_demo_video_end_transition(uid, ui_phase=None):
405
+ """Transition from demo video phase back to the action phase."""
406
  LOGGER.debug(
407
+ "on_demo_video_end_transition uid=%s ui_phase=%s",
408
  _uid_for_log(uid),
409
  ui_phase,
410
  )
 
 
 
411
  return (
412
  gr.update(visible=False), # video_phase_group
413
  gr.update(visible=True), # action_phase_group
414
  gr.update(visible=True), # control_panel_group
415
+ _action_selection_log(), # log_output
416
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
417
  "action_point", # ui_phase_state
418
  )
419
 
420
 
421
+ def on_video_end_transition(uid, ui_phase=None):
422
+ """Backward-compatible alias for legacy tests and demo end handling."""
423
+ return on_demo_video_end_transition(uid, ui_phase)
424
+
425
+
426
+ def on_execute_video_end_transition(uid, exec_btn_interactive=True):
427
+ """Transition from execute video phase back to the action phase."""
428
+ LOGGER.debug(
429
+ "on_execute_video_end_transition uid=%s exec_btn_interactive=%s",
430
+ _uid_for_log(uid),
431
+ exec_btn_interactive,
432
+ )
433
+ return (
434
+ gr.update(visible=False), # execution_video_group
435
+ gr.update(visible=True), # action_phase_group
436
+ gr.update(visible=True), # control_panel_group
437
+ gr.update(interactive=True), # options_radio
438
+ gr.update(interactive=bool(exec_btn_interactive)), # exec_btn
439
+ gr.update(interactive=True), # restart_episode_btn
440
+ gr.update(interactive=True), # next_task_btn
441
+ _live_obs_update(interactive=False), # img_display
442
+ gr.update(interactive=True), # reference_action_btn
443
+ gr.update(interactive=True), # task_hint_display
444
+ "action_point", # ui_phase_state
445
+ )
446
+
447
+
448
  def _task_load_failed_response(uid, message):
449
  LOGGER.warning("task_load_failed uid=%s message=%s", _uid_for_log(uid), message)
450
  _clear_execution_video_path(uid)
 
457
  "", # goal_box
458
  _ui_text("coords", "not_needed"), # coords_box
459
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display
460
+ gr.update(value=None, visible=False, playback_position=0), # execute_video_display
461
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
462
  "", # task_info_box
463
  "", # progress_info_box
 
465
  gr.update(interactive=False), # next_task_btn
466
  gr.update(interactive=False), # exec_btn
467
  gr.update(visible=False), # video_phase_group
468
+ gr.update(visible=False), # execution_video_group
469
  gr.update(visible=False), # action_phase_group
470
  gr.update(visible=False), # control_panel_group
471
  gr.update(value=""), # task_hint_display
 
534
  "", # goal_box
535
  _ui_text("coords", "not_needed"), # coords_box
536
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display
537
+ gr.update(value=None, visible=False, playback_position=0), # execute_video_display
538
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
539
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
540
  progress_text, # progress_info_box
 
542
  gr.update(interactive=True), # next_task_btn
543
  gr.update(interactive=False), # exec_btn
544
  gr.update(visible=False), # video_phase_group
545
+ gr.update(visible=False), # execution_video_group
546
  gr.update(visible=True), # action_phase_group
547
  gr.update(visible=True), # control_panel_group
548
  gr.update(value=get_task_hint(env_id) if env_id else ""), # task_hint_display
 
603
  goal_text, # goal_box
604
  _ui_text("coords", "not_needed"), # coords_box
605
  gr.update(value=demo_video_path, visible=True, autoplay=False, playback_position=0), # video_display
606
+ gr.update(value=None, visible=False, playback_position=0), # execute_video_display
607
  gr.update(visible=True, interactive=True), # watch_demo_video_btn
608
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
609
  progress_text, # progress_info_box
 
611
  gr.update(interactive=True), # next_task_btn
612
  gr.update(interactive=True), # exec_btn
613
  gr.update(visible=True), # video_phase_group
614
+ gr.update(visible=False), # execution_video_group
615
  gr.update(visible=False), # action_phase_group
616
  gr.update(visible=False), # control_panel_group
617
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
 
629
  goal_text, # goal_box
630
  _ui_text("coords", "not_needed"), # coords_box
631
  gr.update(value=None, visible=False, autoplay=False, playback_position=0), # video_display (no video)
632
+ gr.update(value=None, visible=False, playback_position=0), # execute_video_display
633
  gr.update(visible=False, interactive=False), # watch_demo_video_btn
634
  f"{actual_env_id} (Episode {ep_num})", # task_info_box
635
  progress_text, # progress_info_box
 
637
  gr.update(interactive=True), # next_task_btn
638
  gr.update(interactive=True), # exec_btn
639
  gr.update(visible=False), # video_phase_group
640
+ gr.update(visible=False), # execution_video_group
641
  gr.update(visible=True), # action_phase_group
642
  gr.update(visible=True), # control_panel_group
643
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
 
1043
  restart_update=gr.update(interactive=True),
1044
  next_update=gr.update(interactive=True),
1045
  exec_update=gr.update(interactive=True),
1046
+ execute_video_update=None,
1047
  options_update=gr.update(interactive=True),
1048
  coords_update=None,
1049
  reference_update=gr.update(interactive=True),
1050
+ task_hint_update=gr.update(interactive=True),
1051
+ post_execute_exec_interactive=True,
1052
  show_execution_video=False,
1053
  ui_phase="action_point",
1054
  ):
1055
+ if execute_video_update is None:
1056
+ execute_video_update = gr.update(value=None, visible=False, playback_position=0)
1057
  if coords_update is None:
1058
  coords_update = _ui_text("coords", "not_needed")
1059
  return (
 
1064
  restart_update,
1065
  next_update,
1066
  exec_update,
1067
+ execute_video_update,
 
 
1068
  gr.update(visible=not show_execution_video), # action_phase_group
1069
+ gr.update(visible=True), # control_panel_group
1070
+ gr.update(visible=show_execution_video), # execution_video_group
1071
  options_update,
1072
  coords_update,
1073
  reference_update,
1074
+ task_hint_update,
1075
+ bool(post_execute_exec_interactive),
1076
  ui_phase,
1077
  )
1078
 
 
1087
  exec_update=gr.update(interactive=False),
1088
  options_update=gr.update(interactive=False),
1089
  reference_update=gr.update(interactive=False),
1090
+ task_hint_update=gr.update(interactive=False),
1091
+ post_execute_exec_interactive=False,
1092
  show_execution_video=False,
1093
  )
1094
 
 
1130
  exec_update=gr.update(interactive=True),
1131
  options_update=gr.update(choices=_build_radio_choices(session), value=None, interactive=True),
1132
  reference_update=gr.update(interactive=True),
1133
+ task_hint_update=gr.update(interactive=True),
1134
+ post_execute_exec_interactive=True,
1135
  show_execution_video=False,
1136
  )
1137
 
 
1155
  options_update=gr.update(choices=_build_radio_choices(session), value=None, interactive=True),
1156
  coords_update=coords_str,
1157
  reference_update=gr.update(interactive=True),
1158
+ task_hint_update=gr.update(interactive=True),
1159
+ post_execute_exec_interactive=True,
1160
  show_execution_video=False,
1161
  )
1162
 
 
1288
 
1289
  # 根据视图模式重新获取图片
1290
  img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
1291
+ execute_video_update = _build_execution_video_update(uid, session)
1292
+ show_execution_video = execute_video_update.get("visible") is True
1293
  radio_choices = _build_radio_choices(session)
1294
+ post_execute_exec_interactive = not done
1295
+ restart_episode_update = gr.update(interactive=False) if show_execution_video else gr.update(interactive=True)
1296
+ next_task_update = gr.update(interactive=False) if show_execution_video else gr.update(interactive=True)
1297
+ exec_btn_update = (
1298
+ gr.update(interactive=False)
1299
+ if show_execution_video
1300
+ else gr.update(interactive=post_execute_exec_interactive)
1301
+ )
1302
+ options_update = gr.update(
1303
+ choices=radio_choices,
1304
+ value=None,
1305
+ interactive=not show_execution_video,
1306
+ )
1307
  coords_update = _ui_text("coords", "not_needed")
1308
+ reference_update = gr.update(interactive=not show_execution_video)
1309
+ task_hint_update = gr.update(interactive=not show_execution_video)
1310
 
1311
  # 格式化日志消息为 HTML 格式(支持颜色显示)
1312
  formatted_status = format_log_markdown(status)
 
1328
  restart_update=restart_episode_update,
1329
  next_update=next_task_update,
1330
  exec_update=exec_btn_update,
1331
+ execute_video_update=execute_video_update,
1332
  options_update=options_update,
1333
  coords_update=coords_update,
1334
  reference_update=reference_update,
1335
+ task_hint_update=task_hint_update,
1336
+ post_execute_exec_interactive=post_execute_exec_interactive,
1337
  show_execution_video=show_execution_video,
1338
  ui_phase="execution_video" if show_execution_video else "action_point",
1339
  )
gradio-web/test/test_live_obs_refresh.py CHANGED
@@ -57,12 +57,14 @@ def test_execute_step_builds_video_from_last_execution_frames(monkeypatch, reloa
57
  assert [int(frame[0, 0, 0]) for frame in saved_frames] == [11, 22]
58
  assert suffix.startswith("execute_")
59
  assert result[7]["visible"] is True
60
- assert result[7]["autoplay"] is True
61
  assert result[9]["visible"] is True
62
- assert result[10]["visible"] is False
63
- assert result[11]["visible"] is False
64
- assert result[12]["value"] is None
65
- assert result[15] == "execution_video"
 
 
66
 
67
 
68
  def test_execute_step_falls_back_to_single_frame_clip_when_no_new_frames(monkeypatch, reload_module):
@@ -95,7 +97,8 @@ def test_execute_step_falls_back_to_single_frame_clip_when_no_new_frames(monkeyp
95
  assert len(captured["frames"]) == 1
96
  assert int(captured["frames"][0][0, 0, 0]) == 33
97
  assert result[7]["visible"] is True
98
- assert result[15] == "execution_video"
 
99
 
100
 
101
  def test_switch_phase_toggles_live_obs_interactive_without_refresh_queue(reload_module):
@@ -103,11 +106,12 @@ def test_switch_phase_toggles_live_obs_interactive_without_refresh_queue(reload_
103
  callbacks = reload_module("gradio_callbacks")
104
 
105
  to_exec = callbacks.switch_to_execute_phase("uid-3")
106
- assert len(to_exec) == 6
107
  assert to_exec[0].get("interactive") is False
108
  assert to_exec[4].get("interactive") is False
109
  assert to_exec[4].get("elem_classes") == config.get_live_obs_elem_classes()
110
  assert to_exec[5].get("interactive") is False
 
111
 
112
  to_action = callbacks.switch_to_action_phase()
113
  assert len(to_action) == 6
 
57
  assert [int(frame[0, 0, 0]) for frame in saved_frames] == [11, 22]
58
  assert suffix.startswith("execute_")
59
  assert result[7]["visible"] is True
60
+ assert result[8]["visible"] is False
61
  assert result[9]["visible"] is True
62
+ assert result[10]["visible"] is True
63
+ assert result[11]["value"] is None
64
+ assert result[11]["interactive"] is False
65
+ assert result[14]["interactive"] is False
66
+ assert result[15] is True
67
+ assert result[16] == "execution_video"
68
 
69
 
70
  def test_execute_step_falls_back_to_single_frame_clip_when_no_new_frames(monkeypatch, reload_module):
 
97
  assert len(captured["frames"]) == 1
98
  assert int(captured["frames"][0][0, 0, 0]) == 33
99
  assert result[7]["visible"] is True
100
+ assert result[10]["visible"] is True
101
+ assert result[16] == "execution_video"
102
 
103
 
104
  def test_switch_phase_toggles_live_obs_interactive_without_refresh_queue(reload_module):
 
106
  callbacks = reload_module("gradio_callbacks")
107
 
108
  to_exec = callbacks.switch_to_execute_phase("uid-3")
109
+ assert len(to_exec) == 7
110
  assert to_exec[0].get("interactive") is False
111
  assert to_exec[4].get("interactive") is False
112
  assert to_exec[4].get("elem_classes") == config.get_live_obs_elem_classes()
113
  assert to_exec[5].get("interactive") is False
114
+ assert to_exec[6].get("interactive") is False
115
 
116
  to_action = callbacks.switch_to_action_phase()
117
  assert len(to_action) == 6
gradio-web/test/test_queue_session_limit_e2e.py CHANGED
@@ -423,6 +423,7 @@ def test_execute_does_not_use_episode_loading_copy(monkeypatch):
423
  gr.update(interactive=False),
424
  gr.update(interactive=False),
425
  gr.update(interactive=False),
 
426
  )
427
 
428
  def fake_execute_step(uid, option_idx, coords_str):
@@ -435,14 +436,15 @@ def test_execute_does_not_use_episode_loading_copy(monkeypatch):
435
  gr.update(interactive=True),
436
  gr.update(interactive=True),
437
  gr.update(interactive=True),
438
- gr.update(value=None, visible=False, autoplay=False, playback_position=0),
439
- gr.update(visible=False, interactive=False),
440
- gr.update(visible=False),
441
  gr.update(visible=True),
442
  gr.update(visible=True),
 
443
  gr.update(choices=[("pick", 0)], value=None, interactive=True),
444
  "No need for coordinates",
445
  gr.update(interactive=True),
 
 
446
  "action_point",
447
  )
448
 
 
423
  gr.update(interactive=False),
424
  gr.update(interactive=False),
425
  gr.update(interactive=False),
426
+ gr.update(interactive=False),
427
  )
428
 
429
  def fake_execute_step(uid, option_idx, coords_str):
 
436
  gr.update(interactive=True),
437
  gr.update(interactive=True),
438
  gr.update(interactive=True),
439
+ gr.update(value=None, visible=False, playback_position=0),
 
 
440
  gr.update(visible=True),
441
  gr.update(visible=True),
442
+ gr.update(visible=False),
443
  gr.update(choices=[("pick", 0)], value=None, interactive=True),
444
  "No need for coordinates",
445
  gr.update(interactive=True),
446
+ gr.update(interactive=True),
447
+ True,
448
  "action_point",
449
  )
450
 
gradio-web/test/test_ui_native_layout_contract.py CHANGED
@@ -143,8 +143,10 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
143
  "right_log_col",
144
  "control_panel_group",
145
  "video_phase_group",
 
146
  "action_phase_group",
147
  "demo_video",
 
148
  "watch_demo_video_btn",
149
  "live_obs",
150
  "action_radio",
@@ -184,11 +186,18 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
184
  if comp.get("props", {}).get("elem_id") == "demo_video"
185
  )
186
  assert demo_video_comp.get("props", {}).get("autoplay") is False
 
 
 
 
 
 
187
  component_types = [comp.get("type") for comp in cfg.get("components", [])]
188
  assert "timer" not in component_types
189
 
190
  api_names = [dep.get("api_name") for dep in cfg.get("dependencies", [])]
191
  assert "on_demo_video_play" in api_names
 
192
  assert "precheck_execute_inputs" in api_names
193
  assert "switch_to_execute_phase" in api_names
194
  assert "execute_step" in api_names
 
143
  "right_log_col",
144
  "control_panel_group",
145
  "video_phase_group",
146
+ "execution_video_group",
147
  "action_phase_group",
148
  "demo_video",
149
+ "execute_video",
150
  "watch_demo_video_btn",
151
  "live_obs",
152
  "action_radio",
 
186
  if comp.get("props", {}).get("elem_id") == "demo_video"
187
  )
188
  assert demo_video_comp.get("props", {}).get("autoplay") is False
189
+ execute_video_comp = next(
190
+ comp
191
+ for comp in cfg.get("components", [])
192
+ if comp.get("props", {}).get("elem_id") == "execute_video"
193
+ )
194
+ assert execute_video_comp.get("props", {}).get("autoplay") is True
195
  component_types = [comp.get("type") for comp in cfg.get("components", [])]
196
  assert "timer" not in component_types
197
 
198
  api_names = [dep.get("api_name") for dep in cfg.get("dependencies", [])]
199
  assert "on_demo_video_play" in api_names
200
+ assert "on_execute_video_end_transition" in api_names
201
  assert "precheck_execute_inputs" in api_names
202
  assert "switch_to_execute_phase" in api_names
203
  assert "execute_step" in api_names
gradio-web/test/test_ui_phase_machine_runtime_e2e.py CHANGED
@@ -251,41 +251,47 @@ def _read_phase_visibility(page) -> dict[str, bool | str | None]:
251
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
252
  };
253
  const videoEl = document.querySelector('#demo_video video');
 
254
  return {
255
  videoPhase: visible('video_phase_group'),
256
  video: visible('demo_video'),
 
 
257
  watchButton: visible('watch_demo_video_btn'),
258
  actionPhase: visible('action_phase_group'),
259
  action: visible('live_obs'),
260
  controlPhase: visible('control_panel_group'),
261
  control: visible('action_radio'),
262
  currentSrc: videoEl ? videoEl.currentSrc : null,
 
263
  };
264
  }"""
265
  )
266
 
267
 
268
- def _read_demo_video_controls(page) -> dict[str, bool | None]:
269
  return page.evaluate(
270
- """() => {
271
  const visible = (id) => {
 
272
  const el = document.getElementById(id);
273
  if (!el) return false;
274
  const st = getComputedStyle(el);
275
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
276
  };
277
- const videoEl = document.querySelector('#demo_video video');
278
- const button =
279
- document.querySelector('#watch_demo_video_btn button') ||
280
- document.querySelector('button#watch_demo_video_btn');
281
  return {
282
- videoVisible: visible('demo_video'),
283
- buttonVisible: visible('watch_demo_video_btn'),
284
  buttonDisabled: button ? button.disabled : null,
285
  autoplay: videoEl ? videoEl.autoplay : null,
286
  paused: videoEl ? videoEl.paused : null,
287
  };
288
- }"""
 
289
  )
290
 
291
 
@@ -293,12 +299,12 @@ def _click_demo_video_button(page) -> None:
293
  page.locator("#watch_demo_video_btn button, button#watch_demo_video_btn").first.click()
294
 
295
 
296
- def _dispatch_video_event(page, event_name: str) -> bool:
297
  return page.evaluate(
298
- """(eventName) => {
299
  const targets = [
300
- document.querySelector('#demo_video video'),
301
- document.getElementById('demo_video'),
302
  ].filter(Boolean);
303
  if (!targets.length) return false;
304
  for (const target of targets) {
@@ -306,7 +312,7 @@ def _dispatch_video_event(page, event_name: str) -> bool:
306
  }
307
  return true;
308
  }""",
309
- event_name,
310
  )
311
 
312
 
@@ -422,6 +428,7 @@ def phase_machine_ui_url():
422
  with gr.Blocks(title="Native phase machine test") as demo:
423
  gr.HTML(f"<style>{ui_layout.CSS}</style>")
424
  phase_state = gr.State("init")
 
425
 
426
  with gr.Column(visible=True, elem_id="login_group") as login_group:
427
  login_btn = gr.Button("Login", elem_id="login_btn")
@@ -436,6 +443,9 @@ def phase_machine_ui_url():
436
  visible=False,
437
  )
438
 
 
 
 
439
  with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
440
  img_display = gr.Image(value=np.zeros((24, 24, 3), dtype=np.uint8), elem_id="live_obs")
441
 
@@ -450,6 +460,7 @@ def phase_machine_ui_url():
450
  interactive=False,
451
  )
452
  next_task_btn = gr.Button("Next Task", elem_id="next_task_btn")
 
453
 
454
  log_output = gr.Markdown("", elem_id="log_output")
455
  simulate_stop_btn = gr.Button("Simulate Stop", elem_id="simulate_stop_btn")
@@ -472,6 +483,7 @@ def phase_machine_ui_url():
472
  gr.update(visible=False),
473
  gr.update(interactive=False),
474
  gr.update(value="please click the point selection image"),
 
475
  "demo_video",
476
  )
477
 
@@ -493,6 +505,20 @@ def phase_machine_ui_url():
493
  "action_point",
494
  )
495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  def precheck_fn(_option_idx, _coords):
497
  state["precheck_calls"] += 1
498
  if state["precheck_calls"] == 1:
@@ -505,22 +531,23 @@ def phase_machine_ui_url():
505
  gr.update(interactive=False),
506
  gr.update(interactive=False),
507
  gr.update(interactive=False),
 
508
  )
509
 
510
  def execute_fn():
511
  time.sleep(0.8)
512
  return (
513
  "executed",
514
- gr.update(interactive=True),
515
- gr.update(interactive=True),
516
- gr.update(value=execution_video_path, visible=True, autoplay=True, playback_position=0),
517
- gr.update(visible=False, interactive=False),
518
- gr.update(visible=True),
519
- gr.update(visible=False),
520
  gr.update(visible=False),
521
- gr.update(interactive=True),
 
 
 
 
522
  "No need for coordinates",
523
- gr.update(interactive=True),
 
 
524
  "execution_video",
525
  )
526
 
@@ -537,6 +564,7 @@ def phase_machine_ui_url():
537
  action_buttons_row,
538
  reference_action_btn,
539
  coords_box,
 
540
  phase_state,
541
  ],
542
  queue=False,
@@ -574,6 +602,40 @@ def phase_machine_ui_url():
574
  ],
575
  queue=False,
576
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  simulate_stop_btn.click(
578
  fn=on_simulate_stop_fn,
579
  outputs=[log_output],
@@ -585,6 +647,8 @@ def phase_machine_ui_url():
585
  };
586
  show('video_phase_group', false);
587
  show('demo_video', false);
 
 
588
  show('action_phase_group', true);
589
  show('live_obs', true);
590
  show('control_panel_group', true);
@@ -615,22 +679,23 @@ def phase_machine_ui_url():
615
  next_task_btn,
616
  img_display,
617
  reference_action_btn,
 
618
  ],
619
  queue=False,
620
  ).then(
621
  fn=execute_fn,
622
  outputs=[
623
  log_output,
624
- next_task_btn,
625
- exec_btn,
626
- video_display,
627
- watch_demo_video_btn,
628
- video_phase_group,
629
  action_phase_group,
630
- control_panel_group,
 
 
 
631
  options_radio,
632
  coords_box,
633
  reference_action_btn,
 
 
634
  phase_state,
635
  ],
636
  queue=False,
@@ -972,17 +1037,37 @@ def test_phase_machine_runtime_flow_and_execute_precheck(phase_machine_ui_url):
972
 
973
  page.wait_for_function(
974
  """() => {
975
- const videoEl = document.querySelector('#demo_video video');
976
  return !!videoEl && videoEl.autoplay === true && (videoEl.paused === false || videoEl.currentTime > 0);
977
  }""",
978
  timeout=6000,
979
  )
980
- execute_video_controls = _read_demo_video_controls(page)
981
- assert execute_video_controls["videoVisible"] is True
982
  assert execute_video_controls["autoplay"] is True
983
  assert execute_video_controls["paused"] is False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
 
985
- did_dispatch_end = _dispatch_video_event(page, "ended")
986
  assert did_dispatch_end
987
 
988
  page.wait_for_function(
@@ -2523,6 +2608,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2523
  with gr.Blocks(title="Native phase machine local video test") as demo:
2524
  uid_state = gr.State(value="uid-local-video")
2525
  phase_state = gr.State(value="action_point")
 
2526
  suppress_state = gr.State(value=False)
2527
  with gr.Column(visible=True, elem_id="main_interface") as main_interface:
2528
  with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
@@ -2534,6 +2620,9 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2534
  visible=False,
2535
  )
2536
 
 
 
 
2537
  with gr.Column(visible=True, elem_id="action_phase_group") as action_phase_group:
2538
  img_display = gr.Image(value=fake_obs.copy(), elem_id="live_obs")
2539
 
@@ -2544,6 +2633,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2544
  reference_action_btn = gr.Button("reference", interactive=True, elem_id="reference_action_btn")
2545
  restart_episode_btn = gr.Button("restart", interactive=True, elem_id="restart_episode_btn")
2546
  next_task_btn = gr.Button("next", interactive=True, elem_id="next_task_btn")
 
2547
 
2548
  log_output = gr.Markdown("", elem_id="log_output")
2549
  task_info_box = gr.Textbox("")
@@ -2564,6 +2654,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2564
  next_task_btn,
2565
  img_display,
2566
  reference_action_btn,
 
2567
  ],
2568
  queue=False,
2569
  ).then(
@@ -2577,14 +2668,15 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2577
  restart_episode_btn,
2578
  next_task_btn,
2579
  exec_btn,
2580
- video_display,
2581
- watch_demo_video_btn,
2582
- video_phase_group,
2583
  action_phase_group,
2584
  control_panel_group,
 
2585
  options_radio,
2586
  coords_box,
2587
  reference_action_btn,
 
 
2588
  phase_state,
2589
  ],
2590
  queue=False,
@@ -2596,28 +2688,38 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2596
  queue=False,
2597
  )
2598
 
2599
- video_display.end(
2600
- fn=cb.on_video_end_transition,
2601
- inputs=[uid_state, phase_state],
2602
  outputs=[
2603
- video_phase_group,
2604
  action_phase_group,
2605
  control_panel_group,
2606
- log_output,
2607
- watch_demo_video_btn,
 
 
 
 
 
2608
  phase_state,
2609
  ],
2610
  queue=False,
2611
  )
2612
- video_display.stop(
2613
- fn=cb.on_video_end_transition,
2614
- inputs=[uid_state, phase_state],
2615
  outputs=[
2616
- video_phase_group,
2617
  action_phase_group,
2618
  control_panel_group,
2619
- log_output,
2620
- watch_demo_video_btn,
 
 
 
 
 
2621
  phase_state,
2622
  ],
2623
  queue=False,
@@ -2644,7 +2746,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2644
  page.wait_for_selector("#main_interface", state="visible", timeout=20000)
2645
  page.locator("#action_radio input[type='radio']").first.check(force=True)
2646
  page.locator("#exec_btn button, button#exec_btn").first.click()
2647
- page.wait_for_selector("#demo_video video", timeout=5000)
2648
  page.wait_for_function(
2649
  """() => {
2650
  const visible = (id) => {
@@ -2653,13 +2755,12 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2653
  const st = getComputedStyle(el);
2654
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
2655
  };
2656
- const videoEl = document.querySelector('#demo_video video');
2657
  return (
2658
- visible('video_phase_group') &&
2659
- visible('demo_video') &&
2660
- !visible('watch_demo_video_btn') &&
2661
  !visible('action_phase_group') &&
2662
- !visible('control_panel_group') &&
2663
  !!videoEl &&
2664
  videoEl.autoplay === true &&
2665
  (videoEl.paused === false || videoEl.currentTime > 0)
@@ -2667,13 +2768,37 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2667
  }""",
2668
  timeout=10000,
2669
  )
2670
- controls_after_execute = _read_demo_video_controls(page)
2671
- assert controls_after_execute["videoVisible"] is True
2672
- assert controls_after_execute["buttonVisible"] is False
2673
  assert controls_after_execute["autoplay"] is True
2674
  assert controls_after_execute["paused"] is False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2675
 
2676
- did_dispatch_end = _dispatch_video_event(page, "ended")
2677
  assert did_dispatch_end
2678
 
2679
  page.wait_for_function(
@@ -2687,8 +2812,8 @@ def test_phase_machine_runtime_local_video_path_end_transition():
2687
  return (
2688
  visible('live_obs') &&
2689
  visible('action_radio') &&
2690
- !visible('demo_video') &&
2691
- !visible('watch_demo_video_btn')
2692
  );
2693
  }""",
2694
  timeout=2000,
 
251
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
252
  };
253
  const videoEl = document.querySelector('#demo_video video');
254
+ const executeVideoEl = document.querySelector('#execute_video video');
255
  return {
256
  videoPhase: visible('video_phase_group'),
257
  video: visible('demo_video'),
258
+ executionVideoPhase: visible('execution_video_group'),
259
+ executionVideo: visible('execute_video'),
260
  watchButton: visible('watch_demo_video_btn'),
261
  actionPhase: visible('action_phase_group'),
262
  action: visible('live_obs'),
263
  controlPhase: visible('control_panel_group'),
264
  control: visible('action_radio'),
265
  currentSrc: videoEl ? videoEl.currentSrc : null,
266
+ executeCurrentSrc: executeVideoEl ? executeVideoEl.currentSrc : null,
267
  };
268
  }"""
269
  )
270
 
271
 
272
+ def _read_demo_video_controls(page, elem_id: str = "demo_video", button_elem_id: str | None = "watch_demo_video_btn") -> dict[str, bool | None]:
273
  return page.evaluate(
274
+ """({ elemId, buttonElemId }) => {
275
  const visible = (id) => {
276
+ if (!id) return false;
277
  const el = document.getElementById(id);
278
  if (!el) return false;
279
  const st = getComputedStyle(el);
280
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
281
  };
282
+ const videoEl = document.querySelector(`#${elemId} video`);
283
+ const button = buttonElemId
284
+ ? (document.querySelector(`#${buttonElemId} button`) || document.querySelector(`button#${buttonElemId}`))
285
+ : null;
286
  return {
287
+ videoVisible: visible(elemId),
288
+ buttonVisible: visible(buttonElemId),
289
  buttonDisabled: button ? button.disabled : null,
290
  autoplay: videoEl ? videoEl.autoplay : null,
291
  paused: videoEl ? videoEl.paused : null,
292
  };
293
+ }""",
294
+ {"elemId": elem_id, "buttonElemId": button_elem_id},
295
  )
296
 
297
 
 
299
  page.locator("#watch_demo_video_btn button, button#watch_demo_video_btn").first.click()
300
 
301
 
302
+ def _dispatch_video_event(page, event_name: str, elem_id: str = "demo_video") -> bool:
303
  return page.evaluate(
304
+ """({ eventName, elemId }) => {
305
  const targets = [
306
+ document.querySelector(`#${elemId} video`),
307
+ document.getElementById(elemId),
308
  ].filter(Boolean);
309
  if (!targets.length) return false;
310
  for (const target of targets) {
 
312
  }
313
  return true;
314
  }""",
315
+ {"eventName": event_name, "elemId": elem_id},
316
  )
317
 
318
 
 
428
  with gr.Blocks(title="Native phase machine test") as demo:
429
  gr.HTML(f"<style>{ui_layout.CSS}</style>")
430
  phase_state = gr.State("init")
431
+ post_execute_exec_state = gr.State(True)
432
 
433
  with gr.Column(visible=True, elem_id="login_group") as login_group:
434
  login_btn = gr.Button("Login", elem_id="login_btn")
 
443
  visible=False,
444
  )
445
 
446
+ with gr.Column(visible=False, elem_id="execution_video_group") as execution_video_group:
447
+ execute_video_display = gr.Video(value=None, elem_id="execute_video", autoplay=True)
448
+
449
  with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
450
  img_display = gr.Image(value=np.zeros((24, 24, 3), dtype=np.uint8), elem_id="live_obs")
451
 
 
460
  interactive=False,
461
  )
462
  next_task_btn = gr.Button("Next Task", elem_id="next_task_btn")
463
+ task_hint_display = gr.Textbox(value="hint", interactive=True, elem_id="task_hint_display")
464
 
465
  log_output = gr.Markdown("", elem_id="log_output")
466
  simulate_stop_btn = gr.Button("Simulate Stop", elem_id="simulate_stop_btn")
 
483
  gr.update(visible=False),
484
  gr.update(interactive=False),
485
  gr.update(value="please click the point selection image"),
486
+ gr.update(visible=False),
487
  "demo_video",
488
  )
489
 
 
505
  "action_point",
506
  )
507
 
508
+ def on_execute_video_end_fn(exec_enabled):
509
+ return (
510
+ gr.update(visible=False),
511
+ gr.update(visible=True),
512
+ gr.update(visible=True),
513
+ gr.update(interactive=True),
514
+ gr.update(interactive=bool(exec_enabled)),
515
+ gr.update(interactive=True),
516
+ gr.update(interactive=False),
517
+ gr.update(interactive=True),
518
+ gr.update(interactive=True),
519
+ "action_point",
520
+ )
521
+
522
  def precheck_fn(_option_idx, _coords):
523
  state["precheck_calls"] += 1
524
  if state["precheck_calls"] == 1:
 
531
  gr.update(interactive=False),
532
  gr.update(interactive=False),
533
  gr.update(interactive=False),
534
+ gr.update(interactive=False),
535
  )
536
 
537
  def execute_fn():
538
  time.sleep(0.8)
539
  return (
540
  "executed",
 
 
 
 
 
 
541
  gr.update(visible=False),
542
+ gr.update(interactive=False),
543
+ gr.update(interactive=False),
544
+ gr.update(value=execution_video_path, visible=True, playback_position=0),
545
+ gr.update(visible=True),
546
+ gr.update(interactive=False),
547
  "No need for coordinates",
548
+ gr.update(interactive=False),
549
+ gr.update(interactive=False),
550
+ True,
551
  "execution_video",
552
  )
553
 
 
564
  action_buttons_row,
565
  reference_action_btn,
566
  coords_box,
567
+ execution_video_group,
568
  phase_state,
569
  ],
570
  queue=False,
 
602
  ],
603
  queue=False,
604
  )
605
+ execute_video_display.end(
606
+ fn=on_execute_video_end_fn,
607
+ inputs=[post_execute_exec_state],
608
+ outputs=[
609
+ execution_video_group,
610
+ action_phase_group,
611
+ control_panel_group,
612
+ options_radio,
613
+ exec_btn,
614
+ next_task_btn,
615
+ img_display,
616
+ reference_action_btn,
617
+ task_hint_display,
618
+ phase_state,
619
+ ],
620
+ queue=False,
621
+ )
622
+ execute_video_display.stop(
623
+ fn=on_execute_video_end_fn,
624
+ inputs=[post_execute_exec_state],
625
+ outputs=[
626
+ execution_video_group,
627
+ action_phase_group,
628
+ control_panel_group,
629
+ options_radio,
630
+ exec_btn,
631
+ next_task_btn,
632
+ img_display,
633
+ reference_action_btn,
634
+ task_hint_display,
635
+ phase_state,
636
+ ],
637
+ queue=False,
638
+ )
639
  simulate_stop_btn.click(
640
  fn=on_simulate_stop_fn,
641
  outputs=[log_output],
 
647
  };
648
  show('video_phase_group', false);
649
  show('demo_video', false);
650
+ show('execution_video_group', false);
651
+ show('execute_video', false);
652
  show('action_phase_group', true);
653
  show('live_obs', true);
654
  show('control_panel_group', true);
 
679
  next_task_btn,
680
  img_display,
681
  reference_action_btn,
682
+ task_hint_display,
683
  ],
684
  queue=False,
685
  ).then(
686
  fn=execute_fn,
687
  outputs=[
688
  log_output,
 
 
 
 
 
689
  action_phase_group,
690
+ exec_btn,
691
+ next_task_btn,
692
+ execute_video_display,
693
+ execution_video_group,
694
  options_radio,
695
  coords_box,
696
  reference_action_btn,
697
+ task_hint_display,
698
+ post_execute_exec_state,
699
  phase_state,
700
  ],
701
  queue=False,
 
1037
 
1038
  page.wait_for_function(
1039
  """() => {
1040
+ const videoEl = document.querySelector('#execute_video video');
1041
  return !!videoEl && videoEl.autoplay === true && (videoEl.paused === false || videoEl.currentTime > 0);
1042
  }""",
1043
  timeout=6000,
1044
  )
1045
+ execute_video_controls = _read_demo_video_controls(page, elem_id="execute_video", button_elem_id=None)
 
1046
  assert execute_video_controls["autoplay"] is True
1047
  assert execute_video_controls["paused"] is False
1048
+ execute_phase_snapshot = _read_phase_visibility(page)
1049
+ assert execute_phase_snapshot["actionPhase"] is False
1050
+ assert execute_phase_snapshot["controlPhase"] is True
1051
+ panel_snapshot = page.evaluate(
1052
+ """() => {
1053
+ const resolveButton = (id) => {
1054
+ return document.querySelector(`#${id} button`) || document.querySelector(`button#${id}`);
1055
+ };
1056
+ const radio = document.querySelector('#action_radio input[type="radio"]');
1057
+ const refBtn = resolveButton('reference_action_btn');
1058
+ const hint = document.querySelector('#task_hint_display textarea, #task_hint_display input');
1059
+ return {
1060
+ radioDisabled: radio ? radio.disabled : null,
1061
+ refDisabled: refBtn ? refBtn.disabled : null,
1062
+ hintDisabled: hint ? hint.disabled : null,
1063
+ };
1064
+ }"""
1065
+ )
1066
+ assert panel_snapshot["radioDisabled"] is True
1067
+ assert panel_snapshot["refDisabled"] is True
1068
+ assert panel_snapshot["hintDisabled"] is True
1069
 
1070
+ did_dispatch_end = _dispatch_video_event(page, "ended", elem_id="execute_video")
1071
  assert did_dispatch_end
1072
 
1073
  page.wait_for_function(
 
2608
  with gr.Blocks(title="Native phase machine local video test") as demo:
2609
  uid_state = gr.State(value="uid-local-video")
2610
  phase_state = gr.State(value="action_point")
2611
+ post_execute_exec_state = gr.State(value=True)
2612
  suppress_state = gr.State(value=False)
2613
  with gr.Column(visible=True, elem_id="main_interface") as main_interface:
2614
  with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
 
2620
  visible=False,
2621
  )
2622
 
2623
+ with gr.Column(visible=False, elem_id="execution_video_group") as execution_video_group:
2624
+ execute_video_display = gr.Video(value=None, elem_id="execute_video", autoplay=True)
2625
+
2626
  with gr.Column(visible=True, elem_id="action_phase_group") as action_phase_group:
2627
  img_display = gr.Image(value=fake_obs.copy(), elem_id="live_obs")
2628
 
 
2633
  reference_action_btn = gr.Button("reference", interactive=True, elem_id="reference_action_btn")
2634
  restart_episode_btn = gr.Button("restart", interactive=True, elem_id="restart_episode_btn")
2635
  next_task_btn = gr.Button("next", interactive=True, elem_id="next_task_btn")
2636
+ task_hint_display = gr.Textbox("hint", interactive=True, elem_id="task_hint_display")
2637
 
2638
  log_output = gr.Markdown("", elem_id="log_output")
2639
  task_info_box = gr.Textbox("")
 
2654
  next_task_btn,
2655
  img_display,
2656
  reference_action_btn,
2657
+ task_hint_display,
2658
  ],
2659
  queue=False,
2660
  ).then(
 
2668
  restart_episode_btn,
2669
  next_task_btn,
2670
  exec_btn,
2671
+ execute_video_display,
 
 
2672
  action_phase_group,
2673
  control_panel_group,
2674
+ execution_video_group,
2675
  options_radio,
2676
  coords_box,
2677
  reference_action_btn,
2678
+ task_hint_display,
2679
+ post_execute_exec_state,
2680
  phase_state,
2681
  ],
2682
  queue=False,
 
2688
  queue=False,
2689
  )
2690
 
2691
+ execute_video_display.end(
2692
+ fn=cb.on_execute_video_end_transition,
2693
+ inputs=[uid_state, post_execute_exec_state],
2694
  outputs=[
2695
+ execution_video_group,
2696
  action_phase_group,
2697
  control_panel_group,
2698
+ options_radio,
2699
+ exec_btn,
2700
+ restart_episode_btn,
2701
+ next_task_btn,
2702
+ img_display,
2703
+ reference_action_btn,
2704
+ task_hint_display,
2705
  phase_state,
2706
  ],
2707
  queue=False,
2708
  )
2709
+ execute_video_display.stop(
2710
+ fn=cb.on_execute_video_end_transition,
2711
+ inputs=[uid_state, post_execute_exec_state],
2712
  outputs=[
2713
+ execution_video_group,
2714
  action_phase_group,
2715
  control_panel_group,
2716
+ options_radio,
2717
+ exec_btn,
2718
+ restart_episode_btn,
2719
+ next_task_btn,
2720
+ img_display,
2721
+ reference_action_btn,
2722
+ task_hint_display,
2723
  phase_state,
2724
  ],
2725
  queue=False,
 
2746
  page.wait_for_selector("#main_interface", state="visible", timeout=20000)
2747
  page.locator("#action_radio input[type='radio']").first.check(force=True)
2748
  page.locator("#exec_btn button, button#exec_btn").first.click()
2749
+ page.wait_for_selector("#execute_video video", timeout=5000)
2750
  page.wait_for_function(
2751
  """() => {
2752
  const visible = (id) => {
 
2755
  const st = getComputedStyle(el);
2756
  return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
2757
  };
2758
+ const videoEl = document.querySelector('#execute_video video');
2759
  return (
2760
+ visible('execution_video_group') &&
2761
+ visible('execute_video') &&
 
2762
  !visible('action_phase_group') &&
2763
+ visible('control_panel_group') &&
2764
  !!videoEl &&
2765
  videoEl.autoplay === true &&
2766
  (videoEl.paused === false || videoEl.currentTime > 0)
 
2768
  }""",
2769
  timeout=10000,
2770
  )
2771
+ controls_after_execute = _read_demo_video_controls(page, elem_id="execute_video", button_elem_id=None)
 
 
2772
  assert controls_after_execute["autoplay"] is True
2773
  assert controls_after_execute["paused"] is False
2774
+ panel_snapshot = page.evaluate(
2775
+ """() => {
2776
+ const resolveButton = (id) => {
2777
+ return document.querySelector(`#${id} button`) || document.querySelector(`button#${id}`);
2778
+ };
2779
+ const radio = document.querySelector('#action_radio input[type="radio"]');
2780
+ const refBtn = resolveButton('reference_action_btn');
2781
+ const restartBtn = resolveButton('restart_episode_btn');
2782
+ const nextBtn = resolveButton('next_task_btn');
2783
+ const hint = document.querySelector('#task_hint_display textarea, #task_hint_display input');
2784
+ return {
2785
+ radioDisabled: radio ? radio.disabled : null,
2786
+ refDisabled: refBtn ? refBtn.disabled : null,
2787
+ restartDisabled: restartBtn ? restartBtn.disabled : null,
2788
+ nextDisabled: nextBtn ? nextBtn.disabled : null,
2789
+ hintDisabled: hint ? hint.disabled : null,
2790
+ };
2791
+ }"""
2792
+ )
2793
+ assert panel_snapshot == {
2794
+ "radioDisabled": True,
2795
+ "refDisabled": True,
2796
+ "restartDisabled": True,
2797
+ "nextDisabled": True,
2798
+ "hintDisabled": True,
2799
+ }
2800
 
2801
+ did_dispatch_end = _dispatch_video_event(page, "ended", elem_id="execute_video")
2802
  assert did_dispatch_end
2803
 
2804
  page.wait_for_function(
 
2812
  return (
2813
  visible('live_obs') &&
2814
  visible('action_radio') &&
2815
+ !visible('execute_video') &&
2816
+ visible('control_panel_group')
2817
  );
2818
  }""",
2819
  timeout=2000,
gradio-web/test/test_ui_text_config.py CHANGED
@@ -83,6 +83,23 @@ def test_on_video_end_transition_uses_configured_action_prompt(monkeypatch, relo
83
  assert result[5] == "action_point"
84
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  def test_on_demo_video_play_disables_button_and_sets_single_use_state(monkeypatch, reload_module):
87
  reload_module("config")
88
  callbacks = reload_module("gradio_callbacks")
@@ -234,11 +251,12 @@ def test_load_status_task_shows_demo_video_button_for_valid_video(monkeypatch, r
234
 
235
  assert result[7]["visible"] is True
236
  assert result[7]["value"] == str(video_path)
237
- assert result[8]["visible"] is True
238
- assert result[8]["interactive"] is True
239
- assert result[14]["visible"] is True
240
- assert result[15]["visible"] is False
241
  assert result[16]["visible"] is False
 
242
  assert callbacks.UI_TEXT["log"]["demo_video_prompt"] in result[3]
243
 
244
 
@@ -268,10 +286,12 @@ def test_load_status_task_hides_demo_video_button_when_video_is_missing(monkeypa
268
 
269
  assert result[7]["visible"] is False
270
  assert result[8]["visible"] is False
271
- assert result[8]["interactive"] is False
272
- assert result[14]["visible"] is False
273
- assert result[15]["visible"] is True
274
- assert result[16]["visible"] is True
 
 
275
  assert callbacks.UI_TEXT["log"]["action_selection_prompt"] in result[3]
276
 
277
 
 
83
  assert result[5] == "action_point"
84
 
85
 
86
+ def test_on_execute_video_end_transition_preserves_log_and_restores_controls(reload_module):
87
+ callbacks = reload_module("gradio_callbacks")
88
+
89
+ result = callbacks.on_execute_video_end_transition("uid-1", False)
90
+
91
+ assert result[0]["visible"] is False
92
+ assert result[1]["visible"] is True
93
+ assert result[2]["visible"] is True
94
+ assert result[3]["interactive"] is True
95
+ assert result[4]["interactive"] is False
96
+ assert result[5]["interactive"] is True
97
+ assert result[6]["interactive"] is True
98
+ assert result[8]["interactive"] is True
99
+ assert result[9]["interactive"] is True
100
+ assert result[10] == "action_point"
101
+
102
+
103
  def test_on_demo_video_play_disables_button_and_sets_single_use_state(monkeypatch, reload_module):
104
  reload_module("config")
105
  callbacks = reload_module("gradio_callbacks")
 
251
 
252
  assert result[7]["visible"] is True
253
  assert result[7]["value"] == str(video_path)
254
+ assert result[8]["visible"] is False
255
+ assert result[9]["visible"] is True
256
+ assert result[9]["interactive"] is True
257
+ assert result[15]["visible"] is True
258
  assert result[16]["visible"] is False
259
+ assert result[17]["visible"] is False
260
  assert callbacks.UI_TEXT["log"]["demo_video_prompt"] in result[3]
261
 
262
 
 
286
 
287
  assert result[7]["visible"] is False
288
  assert result[8]["visible"] is False
289
+ assert result[9]["visible"] is False
290
+ assert result[9]["interactive"] is False
291
+ assert result[15]["visible"] is False
292
+ assert result[16]["visible"] is False
293
+ assert result[17]["visible"] is True
294
+ assert result[18]["visible"] is True
295
  assert callbacks.UI_TEXT["log"]["action_selection_prompt"] in result[3]
296
 
297
 
gradio-web/ui_layout.py CHANGED
@@ -31,13 +31,13 @@ from gradio_callbacks import (
31
  load_next_task_wrapper,
32
  on_map_click,
33
  on_demo_video_play,
 
 
34
  on_option_select,
35
  on_reference_action,
36
- on_video_end_transition,
37
  precheck_execute_inputs,
38
  restart_episode_wrapper,
39
  switch_env_wrapper,
40
- switch_to_action_phase,
41
  switch_to_execute_phase,
42
  touch_session,
43
  )
@@ -740,12 +740,15 @@ button#watch_demo_video_btn {{
740
  #media_card > div,
741
  #media_card #action_phase_group,
742
  #media_card #video_phase_group,
 
743
  #media_card #live_obs,
744
  #media_card #live_obs button,
745
  #media_card #live_obs .image-frame,
746
  #media_card #live_obs img,
747
  #media_card #demo_video,
748
- #media_card #demo_video video {{
 
 
749
  border-radius: var(--media-card-radius);
750
  }}
751
 
@@ -835,16 +838,17 @@ def render_header_goal(goal_text):
835
  return capitalize_first_letter(last_goal)
836
 
837
 
838
- def _phase_from_updates(main_interface_update, video_phase_update):
839
  if isinstance(main_interface_update, dict) and main_interface_update.get("visible") is False:
840
  return PHASE_INIT
841
- if isinstance(video_phase_update, dict) and video_phase_update.get("visible") is True:
842
  return PHASE_DEMO_VIDEO
843
  return PHASE_ACTION_POINT
844
 
845
 
846
  def _with_phase_from_load(load_result):
847
- phase = _phase_from_updates(load_result[1], load_result[14])
 
848
  return (
849
  *load_result,
850
  phase,
@@ -853,6 +857,7 @@ def _with_phase_from_load(load_result):
853
 
854
 
855
  def _with_rejected_init(load_result, message):
 
856
  return (
857
  *load_result,
858
  PHASE_INIT,
@@ -860,15 +865,34 @@ def _with_rejected_init(load_result, message):
860
  )
861
 
862
 
 
 
 
 
 
 
 
 
 
 
863
  def _phase_visibility_updates(phase):
864
- if phase in {PHASE_DEMO_VIDEO, PHASE_EXECUTION_VIDEO}:
865
  return (
866
  gr.update(visible=True),
867
  gr.update(visible=False),
868
  gr.update(visible=False),
 
 
 
 
 
 
 
 
869
  )
870
  if phase == PHASE_ACTION_POINT:
871
  return (
 
872
  gr.update(visible=False),
873
  gr.update(visible=True),
874
  gr.update(visible=True),
@@ -877,6 +901,7 @@ def _phase_visibility_updates(phase):
877
  gr.update(visible=False),
878
  gr.update(visible=False),
879
  gr.update(visible=False),
 
880
  )
881
 
882
 
@@ -927,6 +952,7 @@ def create_ui_blocks():
927
  delete_callback=cleanup_user_session,
928
  )
929
  ui_phase_state = gr.State(value=PHASE_INIT)
 
930
  current_task_env_state = gr.State(value=None)
931
  suppress_next_option_change_state = gr.State(value=False)
932
 
@@ -962,6 +988,16 @@ def create_ui_blocks():
962
  elem_id="watch_demo_video_btn",
963
  )
964
 
 
 
 
 
 
 
 
 
 
 
965
  with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
966
  img_display = gr.Image(
967
  label="Point Selection",
@@ -1069,6 +1105,7 @@ def create_ui_blocks():
1069
  goal_box,
1070
  coords_box,
1071
  video_display,
 
1072
  watch_demo_video_btn,
1073
  task_info_box,
1074
  progress_info_box,
@@ -1076,6 +1113,7 @@ def create_ui_blocks():
1076
  next_task_btn,
1077
  exec_btn,
1078
  video_phase_group,
 
1079
  action_phase_group,
1080
  control_panel_group,
1081
  task_hint_display,
@@ -1085,6 +1123,7 @@ def create_ui_blocks():
1085
  ]
1086
  phase_visibility_outputs = [
1087
  video_phase_group,
 
1088
  action_phase_group,
1089
  control_panel_group,
1090
  ]
@@ -1312,7 +1351,7 @@ def create_ui_blocks():
1312
  )
1313
 
1314
  video_display.end(
1315
- fn=on_video_end_transition,
1316
  inputs=[uid_state, ui_phase_state],
1317
  outputs=[
1318
  video_phase_group,
@@ -1332,7 +1371,7 @@ def create_ui_blocks():
1332
  show_progress="hidden",
1333
  )
1334
  video_display.stop(
1335
- fn=on_video_end_transition,
1336
  inputs=[uid_state, ui_phase_state],
1337
  outputs=[
1338
  video_phase_group,
@@ -1352,6 +1391,57 @@ def create_ui_blocks():
1352
  show_progress="hidden",
1353
  )
1354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1355
  img_display.select(
1356
  fn=on_map_click,
1357
  inputs=[uid_state, options_radio],
@@ -1423,6 +1513,7 @@ def create_ui_blocks():
1423
  next_task_btn,
1424
  img_display,
1425
  reference_action_btn,
 
1426
  ],
1427
  queue=False,
1428
  show_progress="hidden",
@@ -1443,14 +1534,15 @@ def create_ui_blocks():
1443
  restart_episode_btn,
1444
  next_task_btn,
1445
  exec_btn,
1446
- video_display,
1447
- watch_demo_video_btn,
1448
- video_phase_group,
1449
  action_phase_group,
1450
  control_panel_group,
 
1451
  options_radio,
1452
  coords_box,
1453
  reference_action_btn,
 
 
1454
  ui_phase_state,
1455
  ],
1456
  show_progress="hidden",
 
31
  load_next_task_wrapper,
32
  on_map_click,
33
  on_demo_video_play,
34
+ on_demo_video_end_transition,
35
+ on_execute_video_end_transition,
36
  on_option_select,
37
  on_reference_action,
 
38
  precheck_execute_inputs,
39
  restart_episode_wrapper,
40
  switch_env_wrapper,
 
41
  switch_to_execute_phase,
42
  touch_session,
43
  )
 
740
  #media_card > div,
741
  #media_card #action_phase_group,
742
  #media_card #video_phase_group,
743
+ #media_card #execution_video_group,
744
  #media_card #live_obs,
745
  #media_card #live_obs button,
746
  #media_card #live_obs .image-frame,
747
  #media_card #live_obs img,
748
  #media_card #demo_video,
749
+ #media_card #demo_video video,
750
+ #media_card #execute_video,
751
+ #media_card #execute_video video {{
752
  border-radius: var(--media-card-radius);
753
  }}
754
 
 
838
  return capitalize_first_letter(last_goal)
839
 
840
 
841
+ def _phase_from_updates(main_interface_update, demo_video_phase_update):
842
  if isinstance(main_interface_update, dict) and main_interface_update.get("visible") is False:
843
  return PHASE_INIT
844
+ if isinstance(demo_video_phase_update, dict) and demo_video_phase_update.get("visible") is True:
845
  return PHASE_DEMO_VIDEO
846
  return PHASE_ACTION_POINT
847
 
848
 
849
  def _with_phase_from_load(load_result):
850
+ load_result = _normalize_load_result(load_result)
851
+ phase = _phase_from_updates(load_result[1], load_result[15])
852
  return (
853
  *load_result,
854
  phase,
 
857
 
858
 
859
  def _with_rejected_init(load_result, message):
860
+ load_result = _normalize_load_result(load_result)
861
  return (
862
  *load_result,
863
  PHASE_INIT,
 
865
  )
866
 
867
 
868
+ def _normalize_load_result(load_result):
869
+ if not isinstance(load_result, (tuple, list)):
870
+ return load_result
871
+ normalized = list(load_result)
872
+ if len(normalized) in {19, 20}:
873
+ normalized.insert(8, gr.update(value=None, visible=False, playback_position=0))
874
+ normalized.insert(16, gr.update(visible=False))
875
+ return tuple(normalized)
876
+
877
+
878
  def _phase_visibility_updates(phase):
879
+ if phase == PHASE_DEMO_VIDEO:
880
  return (
881
  gr.update(visible=True),
882
  gr.update(visible=False),
883
  gr.update(visible=False),
884
+ gr.update(visible=False),
885
+ )
886
+ if phase == PHASE_EXECUTION_VIDEO:
887
+ return (
888
+ gr.update(visible=False),
889
+ gr.update(visible=True),
890
+ gr.update(visible=False),
891
+ gr.update(visible=True),
892
  )
893
  if phase == PHASE_ACTION_POINT:
894
  return (
895
+ gr.update(visible=False),
896
  gr.update(visible=False),
897
  gr.update(visible=True),
898
  gr.update(visible=True),
 
901
  gr.update(visible=False),
902
  gr.update(visible=False),
903
  gr.update(visible=False),
904
+ gr.update(visible=False),
905
  )
906
 
907
 
 
952
  delete_callback=cleanup_user_session,
953
  )
954
  ui_phase_state = gr.State(value=PHASE_INIT)
955
+ post_execute_exec_interactive_state = gr.State(value=True)
956
  current_task_env_state = gr.State(value=None)
957
  suppress_next_option_change_state = gr.State(value=False)
958
 
 
988
  elem_id="watch_demo_video_btn",
989
  )
990
 
991
+ with gr.Column(visible=False, elem_id="execution_video_group") as execution_video_group:
992
+ execute_video_display = gr.Video(
993
+ label="Execution Playback 🎬",
994
+ interactive=False,
995
+ elem_id="execute_video",
996
+ autoplay=True,
997
+ show_label=True,
998
+ visible=True,
999
+ )
1000
+
1001
  with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
1002
  img_display = gr.Image(
1003
  label="Point Selection",
 
1105
  goal_box,
1106
  coords_box,
1107
  video_display,
1108
+ execute_video_display,
1109
  watch_demo_video_btn,
1110
  task_info_box,
1111
  progress_info_box,
 
1113
  next_task_btn,
1114
  exec_btn,
1115
  video_phase_group,
1116
+ execution_video_group,
1117
  action_phase_group,
1118
  control_panel_group,
1119
  task_hint_display,
 
1123
  ]
1124
  phase_visibility_outputs = [
1125
  video_phase_group,
1126
+ execution_video_group,
1127
  action_phase_group,
1128
  control_panel_group,
1129
  ]
 
1351
  )
1352
 
1353
  video_display.end(
1354
+ fn=on_demo_video_end_transition,
1355
  inputs=[uid_state, ui_phase_state],
1356
  outputs=[
1357
  video_phase_group,
 
1371
  show_progress="hidden",
1372
  )
1373
  video_display.stop(
1374
+ fn=on_demo_video_end_transition,
1375
  inputs=[uid_state, ui_phase_state],
1376
  outputs=[
1377
  video_phase_group,
 
1391
  show_progress="hidden",
1392
  )
1393
 
1394
+ execute_video_display.end(
1395
+ fn=on_execute_video_end_transition,
1396
+ inputs=[uid_state, post_execute_exec_interactive_state],
1397
+ outputs=[
1398
+ execution_video_group,
1399
+ action_phase_group,
1400
+ control_panel_group,
1401
+ options_radio,
1402
+ exec_btn,
1403
+ restart_episode_btn,
1404
+ next_task_btn,
1405
+ img_display,
1406
+ reference_action_btn,
1407
+ task_hint_display,
1408
+ ui_phase_state,
1409
+ ],
1410
+ queue=False,
1411
+ show_progress="hidden",
1412
+ ).then(
1413
+ fn=touch_session,
1414
+ inputs=[uid_state],
1415
+ outputs=[uid_state],
1416
+ queue=False,
1417
+ show_progress="hidden",
1418
+ )
1419
+ execute_video_display.stop(
1420
+ fn=on_execute_video_end_transition,
1421
+ inputs=[uid_state, post_execute_exec_interactive_state],
1422
+ outputs=[
1423
+ execution_video_group,
1424
+ action_phase_group,
1425
+ control_panel_group,
1426
+ options_radio,
1427
+ exec_btn,
1428
+ restart_episode_btn,
1429
+ next_task_btn,
1430
+ img_display,
1431
+ reference_action_btn,
1432
+ task_hint_display,
1433
+ ui_phase_state,
1434
+ ],
1435
+ queue=False,
1436
+ show_progress="hidden",
1437
+ ).then(
1438
+ fn=touch_session,
1439
+ inputs=[uid_state],
1440
+ outputs=[uid_state],
1441
+ queue=False,
1442
+ show_progress="hidden",
1443
+ )
1444
+
1445
  img_display.select(
1446
  fn=on_map_click,
1447
  inputs=[uid_state, options_radio],
 
1513
  next_task_btn,
1514
  img_display,
1515
  reference_action_btn,
1516
+ task_hint_display,
1517
  ],
1518
  queue=False,
1519
  show_progress="hidden",
 
1534
  restart_episode_btn,
1535
  next_task_btn,
1536
  exec_btn,
1537
+ execute_video_display,
 
 
1538
  action_phase_group,
1539
  control_panel_group,
1540
+ execution_video_group,
1541
  options_radio,
1542
  coords_box,
1543
  reference_action_btn,
1544
+ task_hint_display,
1545
+ post_execute_exec_interactive_state,
1546
  ui_phase_state,
1547
  ],
1548
  show_progress="hidden",