HongzeFu commited on
Commit
0581606
·
1 Parent(s): d10d370

ground truth action only press once!

Browse files
gradio-web/gradio_callbacks.py CHANGED
@@ -855,17 +855,25 @@ def _is_valid_coords_text(coords_text: str) -> bool:
855
  return True
856
 
857
 
858
- def on_option_select(uid, option_value, coords_str=None):
859
  """
860
  处理选项选择事件
861
  """
862
  default_msg = _ui_text("coords", "not_needed")
 
 
 
 
 
 
 
 
863
 
864
  if option_value is None:
865
  LOGGER.debug("on_option_select uid=%s option=None", _uid_for_log(uid))
866
  session = get_session(uid) if uid else None
867
  base_img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW) if session else _LIVE_OBS_UPDATE_SKIP
868
- return default_msg, _live_obs_update(value=base_img, interactive=False), _action_selection_log()
869
 
870
  # 更新session活动时间(选择选项操作)
871
  if uid:
@@ -874,7 +882,7 @@ def on_option_select(uid, option_value, coords_str=None):
874
  session = get_session(uid)
875
  if not session:
876
  LOGGER.warning("on_option_select: missing session uid=%s", _uid_for_log(uid))
877
- return default_msg, _live_obs_update(interactive=False), format_log_markdown(_ui_text("log", "session_error"))
878
 
879
  option_idx = _parse_option_idx(option_value)
880
  base_img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
@@ -891,13 +899,14 @@ def on_option_select(uid, option_value, coords_str=None):
891
  _ui_text("coords", "select_keypoint"),
892
  _live_obs_update(value=base_img, interactive=True, waiting_for_keypoint=True),
893
  _keypoint_selection_log(),
 
894
  )
895
 
896
  LOGGER.debug("on_option_select uid=%s option=%s requires_coords=False", _uid_for_log(uid), option_idx)
897
- return default_msg, _live_obs_update(value=base_img, interactive=False), _action_selection_log()
898
 
899
 
900
- def on_reference_action(uid):
901
  """
902
  自动获取并回填当前步参考 action + 像素坐标(不执行)。
903
  """
@@ -912,6 +921,7 @@ def on_reference_action(uid):
912
  gr.update(),
913
  _ui_text("coords", "not_needed"),
914
  format_log_markdown(_ui_text("log", "session_error")),
 
915
  )
916
 
917
  LOGGER.info("on_reference_action uid=%s env=%s", _uid_for_log(uid), getattr(session, "env_id", None))
@@ -926,6 +936,7 @@ def on_reference_action(uid):
926
  gr.update(),
927
  gr.update(),
928
  format_log_markdown(_ui_text("log", "reference_action_error", error=exc)),
 
929
  )
930
 
931
  if not isinstance(reference, dict) or not reference.get("ok", False):
@@ -937,14 +948,17 @@ def on_reference_action(uid):
937
  gr.update(),
938
  gr.update(),
939
  format_log_markdown(_ui_text("log", "reference_action_status", message=message)),
 
940
  )
941
 
942
  option_idx = reference.get("option_idx")
 
943
  option_label = str(reference.get("option_label", "")).strip()
944
  option_action = str(reference.get("option_action", "")).strip()
945
  option_action = get_ui_action_text(getattr(session, "env_id", None), option_action)
946
  need_coords = bool(reference.get("need_coords", False))
947
  coords_xy = reference.get("coords_xy")
 
948
 
949
  updated_img = current_img
950
  coords_text = _ui_text("coords", "not_needed")
@@ -980,6 +994,7 @@ def on_reference_action(uid):
980
  gr.update(value=option_idx),
981
  coords_text,
982
  format_log_markdown(log_text),
 
983
  )
984
 
985
 
 
855
  return True
856
 
857
 
858
+ def on_option_select(uid, option_value, coords_str=None, suppress_next_option_change=False):
859
  """
860
  处理选项选择事件
861
  """
862
  default_msg = _ui_text("coords", "not_needed")
863
+
864
+ if suppress_next_option_change:
865
+ LOGGER.debug(
866
+ "on_option_select suppressed uid=%s option=%s",
867
+ _uid_for_log(uid),
868
+ option_value,
869
+ )
870
+ return gr.update(), gr.update(), gr.update(), False
871
 
872
  if option_value is None:
873
  LOGGER.debug("on_option_select uid=%s option=None", _uid_for_log(uid))
874
  session = get_session(uid) if uid else None
875
  base_img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW) if session else _LIVE_OBS_UPDATE_SKIP
876
+ return default_msg, _live_obs_update(value=base_img, interactive=False), _action_selection_log(), False
877
 
878
  # 更新session活动时间(选择选项操作)
879
  if uid:
 
882
  session = get_session(uid)
883
  if not session:
884
  LOGGER.warning("on_option_select: missing session uid=%s", _uid_for_log(uid))
885
+ return default_msg, _live_obs_update(interactive=False), format_log_markdown(_ui_text("log", "session_error")), False
886
 
887
  option_idx = _parse_option_idx(option_value)
888
  base_img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
 
899
  _ui_text("coords", "select_keypoint"),
900
  _live_obs_update(value=base_img, interactive=True, waiting_for_keypoint=True),
901
  _keypoint_selection_log(),
902
+ False,
903
  )
904
 
905
  LOGGER.debug("on_option_select uid=%s option=%s requires_coords=False", _uid_for_log(uid), option_idx)
906
+ return default_msg, _live_obs_update(value=base_img, interactive=False), _action_selection_log(), False
907
 
908
 
909
+ def on_reference_action(uid, current_option_value=None):
910
  """
911
  自动获取并回填当前步参考 action + 像素坐标(不执行)。
912
  """
 
921
  gr.update(),
922
  _ui_text("coords", "not_needed"),
923
  format_log_markdown(_ui_text("log", "session_error")),
924
+ False,
925
  )
926
 
927
  LOGGER.info("on_reference_action uid=%s env=%s", _uid_for_log(uid), getattr(session, "env_id", None))
 
936
  gr.update(),
937
  gr.update(),
938
  format_log_markdown(_ui_text("log", "reference_action_error", error=exc)),
939
+ False,
940
  )
941
 
942
  if not isinstance(reference, dict) or not reference.get("ok", False):
 
948
  gr.update(),
949
  gr.update(),
950
  format_log_markdown(_ui_text("log", "reference_action_status", message=message)),
951
+ False,
952
  )
953
 
954
  option_idx = reference.get("option_idx")
955
+ current_option_idx = _parse_option_idx(current_option_value)
956
  option_label = str(reference.get("option_label", "")).strip()
957
  option_action = str(reference.get("option_action", "")).strip()
958
  option_action = get_ui_action_text(getattr(session, "env_id", None), option_action)
959
  need_coords = bool(reference.get("need_coords", False))
960
  coords_xy = reference.get("coords_xy")
961
+ suppress_next_option_change = option_idx != current_option_idx
962
 
963
  updated_img = current_img
964
  coords_text = _ui_text("coords", "not_needed")
 
994
  gr.update(value=option_idx),
995
  coords_text,
996
  format_log_markdown(log_text),
997
+ suppress_next_option_change,
998
  )
999
 
1000
 
gradio-web/test/test_reference_action_callbacks.py CHANGED
@@ -48,7 +48,7 @@ def test_on_reference_action_success_updates_option_and_coords(monkeypatch, relo
48
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
49
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
50
 
51
- img_update, option_update, coords_text, log_html = callbacks.on_reference_action("uid-1")
52
 
53
  assert img_update.get("__type__") == "update"
54
  assert isinstance(img_update.get("value"), Image.Image)
@@ -56,6 +56,7 @@ def test_on_reference_action_success_updates_option_and_coords(monkeypatch, relo
56
  assert img_update.get("elem_classes") == config.get_live_obs_elem_classes()
57
  assert option_update.get("value") == 2
58
  assert coords_text == "5, 6"
 
59
  expected_log = config.UI_TEXT["log"]["reference_action_message_with_coords"].format(
60
  option_label="c",
61
  option_action="press the button",
@@ -71,13 +72,14 @@ def test_on_reference_action_session_missing(monkeypatch, reload_module):
71
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
72
  monkeypatch.setattr(callbacks, "get_session", lambda uid: None)
73
 
74
- img_update, option_update, coords_text, log_html = callbacks.on_reference_action("uid-missing")
75
 
76
  assert img_update.get("__type__") == "update"
77
  assert img_update.get("value") is None
78
  assert option_update.get("__type__") == "update"
79
  assert coords_text == config.UI_TEXT["coords"]["not_needed"]
80
  assert log_html == config.UI_TEXT["log"]["session_error"]
 
81
 
82
 
83
  def test_on_reference_action_error_message_from_reference(monkeypatch, reload_module):
@@ -88,8 +90,33 @@ def test_on_reference_action_error_message_from_reference(monkeypatch, reload_mo
88
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
89
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
90
 
91
- _img, _opt, _coords, log_html = callbacks.on_reference_action("uid-1")
92
  assert log_html == config.UI_TEXT["log"]["reference_action_status"].format(message="bad ref")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
 
95
  def test_on_option_select_resets_to_keypoint_wait_state_for_point_action(monkeypatch, reload_module):
@@ -100,12 +127,24 @@ def test_on_option_select_resets_to_keypoint_wait_state_for_point_action(monkeyp
100
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
101
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
102
 
103
- coords_text, img_update, log_text = callbacks.on_option_select("uid-1", 0, "12, 34")
104
 
105
  assert coords_text == config.UI_TEXT["coords"]["select_keypoint"]
106
  assert img_update.get("interactive") is True
107
  assert img_update.get("elem_classes") == config.get_live_obs_elem_classes(waiting_for_keypoint=True)
108
  assert log_text == config.UI_TEXT["log"]["keypoint_selection_prompt"]
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
 
111
  def test_on_map_click_clears_wait_state_and_restores_action_prompt(monkeypatch, reload_module):
@@ -147,10 +186,11 @@ def test_on_reference_action_uses_configured_action_text_override(monkeypatch, r
147
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
148
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
149
 
150
- _img, _option_update, coords_text, log_html = callbacks.on_reference_action("uid-1")
151
 
152
  assert coords_text == config.UI_TEXT["coords"]["not_needed"]
153
  assert log_html == config.UI_TEXT["log"]["reference_action_message"].format(
154
  option_label="a",
155
  option_action="move forward↓",
156
  )
 
 
48
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
49
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
50
 
51
+ img_update, option_update, coords_text, log_html, suppress_flag = callbacks.on_reference_action("uid-1", None)
52
 
53
  assert img_update.get("__type__") == "update"
54
  assert isinstance(img_update.get("value"), Image.Image)
 
56
  assert img_update.get("elem_classes") == config.get_live_obs_elem_classes()
57
  assert option_update.get("value") == 2
58
  assert coords_text == "5, 6"
59
+ assert suppress_flag is True
60
  expected_log = config.UI_TEXT["log"]["reference_action_message_with_coords"].format(
61
  option_label="c",
62
  option_action="press the button",
 
72
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
73
  monkeypatch.setattr(callbacks, "get_session", lambda uid: None)
74
 
75
+ img_update, option_update, coords_text, log_html, suppress_flag = callbacks.on_reference_action("uid-missing", None)
76
 
77
  assert img_update.get("__type__") == "update"
78
  assert img_update.get("value") is None
79
  assert option_update.get("__type__") == "update"
80
  assert coords_text == config.UI_TEXT["coords"]["not_needed"]
81
  assert log_html == config.UI_TEXT["log"]["session_error"]
82
+ assert suppress_flag is False
83
 
84
 
85
  def test_on_reference_action_error_message_from_reference(monkeypatch, reload_module):
 
90
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
91
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
92
 
93
+ _img, _opt, _coords, log_html, suppress_flag = callbacks.on_reference_action("uid-1", None)
94
  assert log_html == config.UI_TEXT["log"]["reference_action_status"].format(message="bad ref")
95
+ assert suppress_flag is False
96
+
97
+
98
+ def test_on_reference_action_same_selected_option_does_not_set_suppression(monkeypatch, reload_module):
99
+ callbacks = reload_module("gradio_callbacks")
100
+
101
+ session = _FakeSession(
102
+ {
103
+ "ok": True,
104
+ "option_idx": 0,
105
+ "option_label": "a",
106
+ "option_action": "pick up the cube",
107
+ "need_coords": True,
108
+ "coords_xy": [3, 4],
109
+ "message": "ok",
110
+ }
111
+ )
112
+
113
+ monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
114
+ monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
115
+
116
+ _img, _option_update, coords_text, _log_html, suppress_flag = callbacks.on_reference_action("uid-1", 0)
117
+
118
+ assert coords_text == "3, 4"
119
+ assert suppress_flag is False
120
 
121
 
122
  def test_on_option_select_resets_to_keypoint_wait_state_for_point_action(monkeypatch, reload_module):
 
127
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
128
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
129
 
130
+ coords_text, img_update, log_text, suppress_flag = callbacks.on_option_select("uid-1", 0, "12, 34", False)
131
 
132
  assert coords_text == config.UI_TEXT["coords"]["select_keypoint"]
133
  assert img_update.get("interactive") is True
134
  assert img_update.get("elem_classes") == config.get_live_obs_elem_classes(waiting_for_keypoint=True)
135
  assert log_text == config.UI_TEXT["log"]["keypoint_selection_prompt"]
136
+ assert suppress_flag is False
137
+
138
+
139
+ def test_on_option_select_suppresses_programmatic_reference_change(reload_module):
140
+ callbacks = reload_module("gradio_callbacks")
141
+
142
+ coords_update, img_update, log_update, suppress_flag = callbacks.on_option_select("uid-1", 0, "12, 34", True)
143
+
144
+ assert coords_update.get("__type__") == "update"
145
+ assert img_update.get("__type__") == "update"
146
+ assert log_update.get("__type__") == "update"
147
+ assert suppress_flag is False
148
 
149
 
150
  def test_on_map_click_clears_wait_state_and_restores_action_prompt(monkeypatch, reload_module):
 
186
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
187
  monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
188
 
189
+ _img, _option_update, coords_text, log_html, suppress_flag = callbacks.on_reference_action("uid-1", None)
190
 
191
  assert coords_text == config.UI_TEXT["coords"]["not_needed"]
192
  assert log_html == config.UI_TEXT["log"]["reference_action_message"].format(
193
  option_label="a",
194
  option_action="move forward↓",
195
  )
196
+ assert suppress_flag is True
gradio-web/test/test_ui_phase_machine_runtime_e2e.py CHANGED
@@ -1415,6 +1415,190 @@ def test_keypoint_wait_state_pulses_live_obs_and_updates_system_log(monkeypatch)
1415
  demo.close()
1416
 
1417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch):
1419
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
1420
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
 
1415
  demo.close()
1416
 
1417
 
1418
+ def test_reference_action_single_click_applies_coords_without_wait_state(monkeypatch):
1419
+ config_module = importlib.reload(importlib.import_module("config"))
1420
+ callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
1421
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
1422
+
1423
+ fake_obs = np.zeros((24, 48, 3), dtype=np.uint8)
1424
+ fake_obs[:, :] = [15, 20, 25]
1425
+ fake_obs_img = Image.fromarray(fake_obs)
1426
+
1427
+ class FakeSession:
1428
+ env_id = "BinFill"
1429
+ raw_solve_options = [
1430
+ {"label": "a", "action": "pick the left cube", "available": [object()]},
1431
+ {"label": "b", "action": "pick the right cube", "available": [object()]},
1432
+ ]
1433
+ available_options = [
1434
+ ("a. pick the left cube", 0),
1435
+ ("b. pick the right cube", 1),
1436
+ ]
1437
+
1438
+ def get_pil_image(self, use_segmented=False):
1439
+ _ = use_segmented
1440
+ return fake_obs_img.copy()
1441
+
1442
+ def get_reference_action(self):
1443
+ return {
1444
+ "ok": True,
1445
+ "option_idx": 0,
1446
+ "option_label": "a",
1447
+ "option_action": "pick the left cube",
1448
+ "need_coords": True,
1449
+ "coords_xy": [5, 6],
1450
+ "message": "ok",
1451
+ }
1452
+
1453
+ def fake_init_app(_request=None):
1454
+ return (
1455
+ "uid-reference-action",
1456
+ gr.update(visible=True), # main_interface
1457
+ gr.update(
1458
+ value=fake_obs_img.copy(),
1459
+ interactive=False,
1460
+ elem_classes=config_module.get_live_obs_elem_classes(),
1461
+ ), # img_display
1462
+ config_module.UI_TEXT["log"]["action_selection_prompt"], # log_output
1463
+ gr.update(
1464
+ choices=[
1465
+ ("a. pick the left cube", 0),
1466
+ ("b. pick the right cube", 1),
1467
+ ],
1468
+ value=None,
1469
+ ), # options_radio
1470
+ "goal", # goal_box
1471
+ gr.update(
1472
+ value=config_module.UI_TEXT["coords"]["not_needed"],
1473
+ visible=True,
1474
+ interactive=False,
1475
+ ), # coords_box
1476
+ gr.update(value=None, visible=False), # video_display
1477
+ gr.update(visible=False, interactive=False), # watch_demo_video_btn
1478
+ "BinFill (Episode 1)", # task_info_box
1479
+ "Completed: 0", # progress_info_box
1480
+ gr.update(interactive=True), # restart_episode_btn
1481
+ gr.update(interactive=True), # next_task_btn
1482
+ gr.update(interactive=True), # exec_btn
1483
+ gr.update(visible=False), # video_phase_group
1484
+ gr.update(visible=True), # action_phase_group
1485
+ gr.update(visible=True), # control_panel_group
1486
+ gr.update(value="hint"), # task_hint_display
1487
+ gr.update(visible=False), # loading_overlay
1488
+ gr.update(interactive=True), # reference_action_btn
1489
+ )
1490
+
1491
+ monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
1492
+ monkeypatch.setattr(callbacks, "get_session", lambda uid: FakeSession())
1493
+ monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
1494
+
1495
+ demo = ui_layout.create_ui_blocks()
1496
+
1497
+ port = _free_port()
1498
+ host = "127.0.0.1"
1499
+ root_url = f"http://{host}:{port}/"
1500
+
1501
+ app = FastAPI(title="reference-action-single-click-test")
1502
+ app = gr.mount_gradio_app(app, demo, path="/")
1503
+
1504
+ config = uvicorn.Config(app, host=host, port=port, log_level="error")
1505
+ server = uvicorn.Server(config)
1506
+ thread = threading.Thread(target=server.run, daemon=True)
1507
+ thread.start()
1508
+ _wait_http_ready(root_url)
1509
+
1510
+ try:
1511
+ with sync_playwright() as p:
1512
+ browser = p.chromium.launch(headless=True)
1513
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
1514
+ page.goto(root_url, wait_until="domcontentloaded")
1515
+ page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
1516
+ page.wait_for_selector("#live_obs img", timeout=15000)
1517
+ page.wait_for_selector("#reference_action_btn button, button#reference_action_btn", timeout=15000)
1518
+
1519
+ expected_reference_log = config_module.UI_TEXT["log"]["reference_action_message_with_coords"].format(
1520
+ option_label="a",
1521
+ option_action="pick the left cube",
1522
+ coords_text="5, 6",
1523
+ )
1524
+ page.locator("#reference_action_btn button, button#reference_action_btn").first.click()
1525
+
1526
+ page.wait_for_function(
1527
+ """(state) => {
1528
+ const coordsRoot = document.getElementById('coords_box');
1529
+ const coordsField = coordsRoot?.querySelector('textarea, input');
1530
+ const logRoot = document.getElementById('log_output');
1531
+ const logField = logRoot?.querySelector('textarea, input');
1532
+ const liveObs = document.getElementById('live_obs');
1533
+ const checked = document.querySelector('#action_radio input[type="radio"]:checked');
1534
+ const coordsValue = coordsField ? coordsField.value.trim() : '';
1535
+ const logValue = logField ? logField.value.trim() : (logRoot?.textContent || '').trim();
1536
+ return (
1537
+ !!checked &&
1538
+ checked.value === state.checkedValue &&
1539
+ coordsValue === state.coordsValue &&
1540
+ logValue === state.logValue &&
1541
+ !!liveObs &&
1542
+ !liveObs.classList.contains(state.waitClass)
1543
+ );
1544
+ }""",
1545
+ arg={
1546
+ "checkedValue": "0",
1547
+ "coordsValue": "5, 6",
1548
+ "logValue": expected_reference_log,
1549
+ "waitClass": config_module.LIVE_OBS_KEYPOINT_WAIT_CLASS,
1550
+ },
1551
+ timeout=5000,
1552
+ )
1553
+
1554
+ classes_after_reference = _read_elem_classes(page, "live_obs")
1555
+ assert classes_after_reference is not None
1556
+ assert config_module.LIVE_OBS_KEYPOINT_WAIT_CLASS not in classes_after_reference
1557
+ assert _read_coords_box_value(page) == "5, 6"
1558
+ assert _read_log_output_value(page) == expected_reference_log
1559
+
1560
+ page.locator("#action_radio input[type='radio']").nth(1).check(force=True)
1561
+ page.wait_for_function(
1562
+ """(state) => {
1563
+ const coordsRoot = document.getElementById('coords_box');
1564
+ const coordsField = coordsRoot?.querySelector('textarea, input');
1565
+ const logRoot = document.getElementById('log_output');
1566
+ const logField = logRoot?.querySelector('textarea, input');
1567
+ const liveObs = document.getElementById('live_obs');
1568
+ const checked = document.querySelector('#action_radio input[type="radio"]:checked');
1569
+ const coordsValue = coordsField ? coordsField.value.trim() : '';
1570
+ const logValue = logField ? logField.value.trim() : (logRoot?.textContent || '').trim();
1571
+ return (
1572
+ !!checked &&
1573
+ checked.value === state.checkedValue &&
1574
+ coordsValue === state.coordsValue &&
1575
+ logValue === state.logValue &&
1576
+ !!liveObs &&
1577
+ liveObs.classList.contains(state.waitClass)
1578
+ );
1579
+ }""",
1580
+ arg={
1581
+ "checkedValue": "1",
1582
+ "coordsValue": config_module.UI_TEXT["coords"]["select_keypoint"],
1583
+ "logValue": config_module.UI_TEXT["log"]["keypoint_selection_prompt"],
1584
+ "waitClass": config_module.LIVE_OBS_KEYPOINT_WAIT_CLASS,
1585
+ },
1586
+ timeout=5000,
1587
+ )
1588
+
1589
+ classes_after_manual_change = _read_elem_classes(page, "live_obs")
1590
+ assert classes_after_manual_change is not None
1591
+ assert config_module.LIVE_OBS_KEYPOINT_WAIT_CLASS in classes_after_manual_change
1592
+ assert _read_coords_box_value(page) == config_module.UI_TEXT["coords"]["select_keypoint"]
1593
+ assert _read_log_output_value(page) == config_module.UI_TEXT["log"]["keypoint_selection_prompt"]
1594
+
1595
+ browser.close()
1596
+ finally:
1597
+ server.should_exit = True
1598
+ thread.join(timeout=10)
1599
+ demo.close()
1600
+
1601
+
1602
  def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch):
1603
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
1604
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
gradio-web/test/test_ui_text_config.py CHANGED
@@ -43,12 +43,13 @@ def test_on_option_select_uses_configured_select_keypoint_and_log_messages(monke
43
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
44
  monkeypatch.setattr(callbacks, "get_session", lambda uid: _FakeOptionSession())
45
 
46
- coords_text, img_update, log_text = callbacks.on_option_select("uid-1", 0, None)
47
 
48
  assert coords_text == "pick a point from config"
49
  assert img_update.get("interactive") is True
50
  assert callbacks.get_live_obs_elem_classes(waiting_for_keypoint=True) == img_update.get("elem_classes")
51
  assert log_text == "custom log prompt from config"
 
52
 
53
 
54
  def test_precheck_execute_inputs_uses_configured_before_execute_message(monkeypatch, reload_module):
@@ -112,11 +113,12 @@ def test_missing_session_paths_use_configured_session_error(monkeypatch, reload_
112
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
113
  monkeypatch.setattr(callbacks, "get_session", lambda uid: None)
114
 
115
- _img, _option_update, coords_text, log_text = callbacks.on_reference_action("uid-missing")
116
  map_img, map_coords, map_log = callbacks.on_map_click("uid-missing", None, None)
117
 
118
  assert coords_text == callbacks.UI_TEXT["coords"]["not_needed"]
119
  assert log_text == "Session Error From Config"
 
120
  assert map_img.get("__type__") == "update"
121
  assert map_img.get("value") is None
122
  assert map_coords == callbacks.UI_TEXT["coords"]["not_needed"]
 
43
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
44
  monkeypatch.setattr(callbacks, "get_session", lambda uid: _FakeOptionSession())
45
 
46
+ coords_text, img_update, log_text, suppress_flag = callbacks.on_option_select("uid-1", 0, None, False)
47
 
48
  assert coords_text == "pick a point from config"
49
  assert img_update.get("interactive") is True
50
  assert callbacks.get_live_obs_elem_classes(waiting_for_keypoint=True) == img_update.get("elem_classes")
51
  assert log_text == "custom log prompt from config"
52
+ assert suppress_flag is False
53
 
54
 
55
  def test_precheck_execute_inputs_uses_configured_before_execute_message(monkeypatch, reload_module):
 
113
  monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
114
  monkeypatch.setattr(callbacks, "get_session", lambda uid: None)
115
 
116
+ _img, _option_update, coords_text, log_text, suppress_flag = callbacks.on_reference_action("uid-missing", None)
117
  map_img, map_coords, map_log = callbacks.on_map_click("uid-missing", None, None)
118
 
119
  assert coords_text == callbacks.UI_TEXT["coords"]["not_needed"]
120
  assert log_text == "Session Error From Config"
121
+ assert suppress_flag is False
122
  assert map_img.get("__type__") == "update"
123
  assert map_img.get("value") is None
124
  assert map_coords == callbacks.UI_TEXT["coords"]["not_needed"]
gradio-web/ui_layout.py CHANGED
@@ -586,6 +586,7 @@ def create_ui_blocks():
586
  uid_state = gr.State(value=None)
587
  ui_phase_state = gr.State(value=PHASE_INIT)
588
  current_task_env_state = gr.State(value=None)
 
589
  live_obs_timer = gr.Timer(value=1.0 / LIVE_OBS_REFRESH_HZ, active=True)
590
 
591
  task_info_box = gr.Textbox(visible=False, elem_id="task_info_box")
@@ -925,8 +926,8 @@ def create_ui_blocks():
925
 
926
  options_radio.change(
927
  fn=on_option_select,
928
- inputs=[uid_state, options_radio, coords_box],
929
- outputs=[coords_box, img_display, log_output],
930
  )
931
 
932
  watch_demo_video_btn.click(
@@ -939,8 +940,8 @@ def create_ui_blocks():
939
 
940
  reference_action_btn.click(
941
  fn=on_reference_action,
942
- inputs=[uid_state],
943
- outputs=[img_display, options_radio, coords_box, log_output],
944
  )
945
 
946
  exec_btn.click(
 
586
  uid_state = gr.State(value=None)
587
  ui_phase_state = gr.State(value=PHASE_INIT)
588
  current_task_env_state = gr.State(value=None)
589
+ suppress_next_option_change_state = gr.State(value=False)
590
  live_obs_timer = gr.Timer(value=1.0 / LIVE_OBS_REFRESH_HZ, active=True)
591
 
592
  task_info_box = gr.Textbox(visible=False, elem_id="task_info_box")
 
926
 
927
  options_radio.change(
928
  fn=on_option_select,
929
+ inputs=[uid_state, options_radio, coords_box, suppress_next_option_change_state],
930
+ outputs=[coords_box, img_display, log_output, suppress_next_option_change_state],
931
  )
932
 
933
  watch_demo_video_btn.click(
 
940
 
941
  reference_action_btn.click(
942
  fn=on_reference_action,
943
+ inputs=[uid_state, options_radio],
944
+ outputs=[img_display, options_radio, coords_box, log_output, suppress_next_option_change_state],
945
  )
946
 
947
  exec_btn.click(