video play button
Browse files
gradio-web/gradio_callbacks.py
CHANGED
|
@@ -24,6 +24,8 @@ from state_manager import (
|
|
| 24 |
update_session_activity,
|
| 25 |
get_session_activity,
|
| 26 |
cleanup_session,
|
|
|
|
|
|
|
| 27 |
reset_play_button_clicked,
|
| 28 |
GLOBAL_SESSIONS,
|
| 29 |
SESSION_LAST_ACTIVITY,
|
|
@@ -225,6 +227,21 @@ def on_video_end(uid):
|
|
| 225 |
return format_log_markdown(_ui_text("log", "action_selection_prompt"))
|
| 226 |
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
def switch_to_execute_phase(uid):
|
| 229 |
"""Disable controls and keypoint clicking during execute playback."""
|
| 230 |
if uid:
|
|
@@ -406,7 +423,8 @@ def on_video_end_transition(uid):
|
|
| 406 |
gr.update(visible=False), # video_phase_group
|
| 407 |
gr.update(visible=True), # action_phase_group
|
| 408 |
gr.update(visible=True), # control_panel_group
|
| 409 |
-
format_log_markdown(_ui_text("log", "action_selection_prompt"))
|
|
|
|
| 410 |
)
|
| 411 |
|
| 412 |
|
|
@@ -421,6 +439,7 @@ def _task_load_failed_response(uid, message):
|
|
| 421 |
"", # goal_box
|
| 422 |
_ui_text("coords", "not_needed"), # coords_box
|
| 423 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 424 |
"", # task_info_box
|
| 425 |
"", # progress_info_box
|
| 426 |
gr.update(interactive=False), # restart_episode_btn
|
|
@@ -501,6 +520,7 @@ def _load_status_task(uid, status):
|
|
| 501 |
"", # goal_box
|
| 502 |
_ui_text("coords", "not_needed"), # coords_box
|
| 503 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 504 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 505 |
progress_text, # progress_info_box
|
| 506 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -540,13 +560,10 @@ def _load_status_task(uid, status):
|
|
| 540 |
)
|
| 541 |
|
| 542 |
demo_video_path = None
|
| 543 |
-
has_demo_video = False
|
| 544 |
should_show = should_show_demo_video(actual_env_id) if actual_env_id else False
|
| 545 |
initial_log_msg = format_log_markdown(_ui_text("log", "action_selection_prompt"))
|
| 546 |
|
| 547 |
if should_show:
|
| 548 |
-
has_demo_video = True
|
| 549 |
-
initial_log_msg = format_log_markdown(_ui_text("log", "demo_video_prompt"))
|
| 550 |
if session.demonstration_frames:
|
| 551 |
try:
|
| 552 |
demo_video_path = save_video(session.demonstration_frames, "demo")
|
|
@@ -565,6 +582,10 @@ def _load_status_task(uid, status):
|
|
| 565 |
bool(demo_video_path),
|
| 566 |
)
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
|
| 569 |
|
| 570 |
if has_demo_video:
|
|
@@ -579,6 +600,7 @@ def _load_status_task(uid, status):
|
|
| 579 |
goal_text, # goal_box
|
| 580 |
_ui_text("coords", "not_needed"), # coords_box
|
| 581 |
gr.update(value=demo_video_path, visible=True), # video_display
|
|
|
|
| 582 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 583 |
progress_text, # progress_info_box
|
| 584 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -603,6 +625,7 @@ def _load_status_task(uid, status):
|
|
| 603 |
goal_text, # goal_box
|
| 604 |
_ui_text("coords", "not_needed"), # coords_box
|
| 605 |
gr.update(value=None, visible=False), # video_display (no video)
|
|
|
|
| 606 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 607 |
progress_text, # progress_info_box
|
| 608 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 24 |
update_session_activity,
|
| 25 |
get_session_activity,
|
| 26 |
cleanup_session,
|
| 27 |
+
get_play_button_clicked,
|
| 28 |
+
set_play_button_clicked,
|
| 29 |
reset_play_button_clicked,
|
| 30 |
GLOBAL_SESSIONS,
|
| 31 |
SESSION_LAST_ACTIVITY,
|
|
|
|
| 227 |
return format_log_markdown(_ui_text("log", "action_selection_prompt"))
|
| 228 |
|
| 229 |
|
| 230 |
+
def on_demo_video_play(uid):
|
| 231 |
+
"""Mark the demo video as consumed and disable the play button."""
|
| 232 |
+
if uid:
|
| 233 |
+
update_session_activity(uid)
|
| 234 |
+
already_clicked = get_play_button_clicked(uid)
|
| 235 |
+
if not already_clicked:
|
| 236 |
+
set_play_button_clicked(uid, True)
|
| 237 |
+
LOGGER.debug(
|
| 238 |
+
"demo video play clicked uid=%s already_clicked=%s",
|
| 239 |
+
_uid_for_log(uid),
|
| 240 |
+
already_clicked,
|
| 241 |
+
)
|
| 242 |
+
return gr.update(visible=True, interactive=False)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
def switch_to_execute_phase(uid):
|
| 246 |
"""Disable controls and keypoint clicking during execute playback."""
|
| 247 |
if uid:
|
|
|
|
| 423 |
gr.update(visible=False), # video_phase_group
|
| 424 |
gr.update(visible=True), # action_phase_group
|
| 425 |
gr.update(visible=True), # control_panel_group
|
| 426 |
+
format_log_markdown(_ui_text("log", "action_selection_prompt")),
|
| 427 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 428 |
)
|
| 429 |
|
| 430 |
|
|
|
|
| 439 |
"", # goal_box
|
| 440 |
_ui_text("coords", "not_needed"), # coords_box
|
| 441 |
gr.update(value=None, visible=False), # video_display
|
| 442 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 443 |
"", # task_info_box
|
| 444 |
"", # progress_info_box
|
| 445 |
gr.update(interactive=False), # restart_episode_btn
|
|
|
|
| 520 |
"", # goal_box
|
| 521 |
_ui_text("coords", "not_needed"), # coords_box
|
| 522 |
gr.update(value=None, visible=False), # video_display
|
| 523 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 524 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 525 |
progress_text, # progress_info_box
|
| 526 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 560 |
)
|
| 561 |
|
| 562 |
demo_video_path = None
|
|
|
|
| 563 |
should_show = should_show_demo_video(actual_env_id) if actual_env_id else False
|
| 564 |
initial_log_msg = format_log_markdown(_ui_text("log", "action_selection_prompt"))
|
| 565 |
|
| 566 |
if should_show:
|
|
|
|
|
|
|
| 567 |
if session.demonstration_frames:
|
| 568 |
try:
|
| 569 |
demo_video_path = save_video(session.demonstration_frames, "demo")
|
|
|
|
| 582 |
bool(demo_video_path),
|
| 583 |
)
|
| 584 |
|
| 585 |
+
has_demo_video = bool(demo_video_path)
|
| 586 |
+
if has_demo_video:
|
| 587 |
+
initial_log_msg = format_log_markdown(_ui_text("log", "demo_video_prompt"))
|
| 588 |
+
|
| 589 |
img = session.get_pil_image(use_segmented=USE_SEGMENTED_VIEW)
|
| 590 |
|
| 591 |
if has_demo_video:
|
|
|
|
| 600 |
goal_text, # goal_box
|
| 601 |
_ui_text("coords", "not_needed"), # coords_box
|
| 602 |
gr.update(value=demo_video_path, visible=True), # video_display
|
| 603 |
+
gr.update(visible=True, interactive=True), # watch_demo_video_btn
|
| 604 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 605 |
progress_text, # progress_info_box
|
| 606 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 625 |
goal_text, # goal_box
|
| 626 |
_ui_text("coords", "not_needed"), # coords_box
|
| 627 |
gr.update(value=None, visible=False), # video_display (no video)
|
| 628 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 629 |
f"{actual_env_id} (Episode {ep_num})", # task_info_box
|
| 630 |
progress_text, # progress_info_box
|
| 631 |
gr.update(interactive=True), # restart_episode_btn
|
gradio-web/test/test_ui_native_layout_contract.py
CHANGED
|
@@ -77,6 +77,7 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
|
|
| 77 |
"video_phase_group",
|
| 78 |
"action_phase_group",
|
| 79 |
"demo_video",
|
|
|
|
| 80 |
"live_obs",
|
| 81 |
"action_radio",
|
| 82 |
"coords_box",
|
|
@@ -107,7 +108,15 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
|
|
| 107 |
)
|
| 108 |
assert log_output_comp.get("props", {}).get("max_lines") is None
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
api_names = [dep.get("api_name") for dep in cfg.get("dependencies", [])]
|
|
|
|
| 111 |
assert "precheck_execute_inputs" in api_names
|
| 112 |
assert "switch_to_execute_phase" in api_names
|
| 113 |
assert "execute_step" in api_names
|
|
|
|
| 77 |
"video_phase_group",
|
| 78 |
"action_phase_group",
|
| 79 |
"demo_video",
|
| 80 |
+
"watch_demo_video_btn",
|
| 81 |
"live_obs",
|
| 82 |
"action_radio",
|
| 83 |
"coords_box",
|
|
|
|
| 108 |
)
|
| 109 |
assert log_output_comp.get("props", {}).get("max_lines") is None
|
| 110 |
|
| 111 |
+
demo_video_comp = next(
|
| 112 |
+
comp
|
| 113 |
+
for comp in cfg.get("components", [])
|
| 114 |
+
if comp.get("props", {}).get("elem_id") == "demo_video"
|
| 115 |
+
)
|
| 116 |
+
assert demo_video_comp.get("props", {}).get("autoplay") is False
|
| 117 |
+
|
| 118 |
api_names = [dep.get("api_name") for dep in cfg.get("dependencies", [])]
|
| 119 |
+
assert "on_demo_video_play" in api_names
|
| 120 |
assert "precheck_execute_inputs" in api_names
|
| 121 |
assert "switch_to_execute_phase" in api_names
|
| 122 |
assert "execute_step" in api_names
|
gradio-web/test/test_ui_phase_machine_runtime_e2e.py
CHANGED
|
@@ -113,6 +113,7 @@ def _read_phase_visibility(page) -> dict[str, bool | str | None]:
|
|
| 113 |
return {
|
| 114 |
videoPhase: visible('video_phase_group'),
|
| 115 |
video: visible('demo_video'),
|
|
|
|
| 116 |
actionPhase: visible('action_phase_group'),
|
| 117 |
action: visible('live_obs'),
|
| 118 |
controlPhase: visible('control_panel_group'),
|
|
@@ -123,6 +124,51 @@ def _read_phase_visibility(page) -> dict[str, bool | str | None]:
|
|
| 123 |
)
|
| 124 |
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
def _read_live_obs_geometry(page) -> dict[str, dict[str, float] | None]:
|
| 127 |
return page.evaluate(
|
| 128 |
"""() => {
|
|
@@ -197,7 +243,7 @@ def font_size_probe_ui_url(monkeypatch):
|
|
| 197 |
|
| 198 |
@pytest.fixture
|
| 199 |
def phase_machine_ui_url():
|
| 200 |
-
state = {"precheck_calls": 0}
|
| 201 |
demo_video_url = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
|
| 202 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 203 |
|
|
@@ -210,7 +256,13 @@ def phase_machine_ui_url():
|
|
| 210 |
|
| 211 |
with gr.Column(visible=False, elem_id="main_interface") as main_interface:
|
| 212 |
with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
|
| 213 |
-
video_display = gr.Video(value=None, elem_id="demo_video", autoplay=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
|
| 216 |
img_display = gr.Image(value=np.zeros((24, 24, 3), dtype=np.uint8), elem_id="live_obs")
|
|
@@ -228,6 +280,13 @@ def phase_machine_ui_url():
|
|
| 228 |
next_task_btn = gr.Button("Next Task", elem_id="next_task_btn")
|
| 229 |
|
| 230 |
log_output = gr.Markdown("", elem_id="log_output")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
def login_fn():
|
| 233 |
return (
|
|
@@ -235,6 +294,7 @@ def phase_machine_ui_url():
|
|
| 235 |
gr.update(visible=True),
|
| 236 |
gr.update(visible=True),
|
| 237 |
gr.update(value=demo_video_url, visible=True),
|
|
|
|
| 238 |
gr.update(visible=False),
|
| 239 |
gr.update(visible=False),
|
| 240 |
gr.update(visible=False),
|
|
@@ -243,6 +303,13 @@ def phase_machine_ui_url():
|
|
| 243 |
"demo_video",
|
| 244 |
)
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
def on_video_end_fn():
|
| 247 |
return (
|
| 248 |
gr.update(visible=False),
|
|
@@ -250,6 +317,7 @@ def phase_machine_ui_url():
|
|
| 250 |
gr.update(visible=True),
|
| 251 |
gr.update(visible=True),
|
| 252 |
gr.update(interactive=True),
|
|
|
|
| 253 |
"action_keypoint",
|
| 254 |
)
|
| 255 |
|
|
@@ -293,6 +361,7 @@ def phase_machine_ui_url():
|
|
| 293 |
main_interface,
|
| 294 |
video_phase_group,
|
| 295 |
video_display,
|
|
|
|
| 296 |
action_phase_group,
|
| 297 |
control_panel_group,
|
| 298 |
action_buttons_row,
|
|
@@ -303,6 +372,12 @@ def phase_machine_ui_url():
|
|
| 303 |
queue=False,
|
| 304 |
)
|
| 305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
video_display.end(
|
| 307 |
fn=on_video_end_fn,
|
| 308 |
outputs=[
|
|
@@ -311,10 +386,51 @@ def phase_machine_ui_url():
|
|
| 311 |
control_panel_group,
|
| 312 |
action_buttons_row,
|
| 313 |
reference_action_btn,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
phase_state,
|
| 315 |
],
|
| 316 |
queue=False,
|
| 317 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
exec_btn.click(
|
| 320 |
fn=precheck_fn,
|
|
@@ -427,6 +543,7 @@ def test_phase_machine_runtime_flow_and_execute_precheck(phase_machine_ui_url):
|
|
| 427 |
};
|
| 428 |
return {
|
| 429 |
video: visible('demo_video'),
|
|
|
|
| 430 |
action: visible('live_obs'),
|
| 431 |
control: visible('action_radio'),
|
| 432 |
};
|
|
@@ -434,27 +551,59 @@ def test_phase_machine_runtime_flow_and_execute_precheck(phase_machine_ui_url):
|
|
| 434 |
)
|
| 435 |
assert phase_after_login == {
|
| 436 |
"video": True,
|
|
|
|
| 437 |
"action": False,
|
| 438 |
"control": False,
|
| 439 |
}
|
| 440 |
|
| 441 |
page.wait_for_selector("#demo_video video", timeout=5000)
|
| 442 |
-
|
| 443 |
"""() => {
|
| 444 |
const videoEl = document.querySelector('#demo_video video');
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
| 449 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
assert did_dispatch_end
|
| 451 |
|
| 452 |
page.wait_for_function(
|
| 453 |
"""() => {
|
| 454 |
const action = document.getElementById('live_obs');
|
| 455 |
const control = document.getElementById('action_radio');
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
}"""
|
| 459 |
)
|
| 460 |
|
|
@@ -549,6 +698,7 @@ def test_phase_machine_runtime_flow_and_execute_precheck(phase_machine_ui_url):
|
|
| 549 |
browser.close()
|
| 550 |
|
| 551 |
assert state["precheck_calls"] >= 2
|
|
|
|
| 552 |
|
| 553 |
|
| 554 |
def test_reference_action_button_is_green_only_when_interactive(phase_machine_ui_url):
|
|
@@ -569,14 +719,17 @@ def test_reference_action_button_is_green_only_when_interactive(phase_machine_ui
|
|
| 569 |
assert disabled_snapshot["backgroundColor"] != "rgb(31, 139, 76)"
|
| 570 |
|
| 571 |
page.wait_for_selector("#demo_video video", timeout=5000)
|
| 572 |
-
|
|
|
|
| 573 |
"""() => {
|
| 574 |
-
const
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
return true;
|
| 578 |
-
}"""
|
|
|
|
| 579 |
)
|
|
|
|
| 580 |
assert did_dispatch_end
|
| 581 |
|
| 582 |
page.wait_for_function(
|
|
@@ -597,6 +750,59 @@ def test_reference_action_button_is_green_only_when_interactive(phase_machine_ui
|
|
| 597 |
browser.close()
|
| 598 |
|
| 599 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
def test_unified_loading_overlay_init_flow(monkeypatch):
|
| 601 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 602 |
|
|
@@ -621,6 +827,7 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
|
|
| 621 |
"goal", # goal_box
|
| 622 |
"No need for coordinates", # coords_box
|
| 623 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 624 |
"PickXtimes (Episode 1)", # task_info_box
|
| 625 |
"Completed: 0", # progress_info_box
|
| 626 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -690,6 +897,95 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
|
|
| 690 |
assert calls["init"] >= 1
|
| 691 |
|
| 692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch):
|
| 694 |
callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
|
| 695 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
|
@@ -718,6 +1014,7 @@ def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch)
|
|
| 718 |
interactive=False,
|
| 719 |
), # coords_box
|
| 720 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 721 |
"ResizeEnv (Episode 1)", # task_info_box
|
| 722 |
"Completed: 0", # progress_info_box
|
| 723 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -946,6 +1243,7 @@ def test_header_task_shows_env_after_init(monkeypatch):
|
|
| 946 |
"goal", # goal_box
|
| 947 |
"No need for coordinates", # coords_box
|
| 948 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 949 |
"PickXtimes (Episode 1)", # task_info_box
|
| 950 |
"Completed: 0", # progress_info_box
|
| 951 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -1021,6 +1319,7 @@ def test_header_task_env_normalization_and_fallback(monkeypatch, task_info_text,
|
|
| 1021 |
"goal", # goal_box
|
| 1022 |
"No need for coordinates", # coords_box
|
| 1023 |
gr.update(value=None, visible=False), # video_display
|
|
|
|
| 1024 |
task_info_text, # task_info_box
|
| 1025 |
"Completed: 0", # progress_info_box
|
| 1026 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -1092,6 +1391,7 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1092 |
"video goal" if show_video else "goal", # goal_box
|
| 1093 |
"No need for coordinates", # coords_box
|
| 1094 |
gr.update(value=demo_video_path if show_video else None, visible=show_video), # video_display
|
|
|
|
| 1095 |
f"{task_name} (Episode 1)", # task_info_box
|
| 1096 |
"Completed: 0", # progress_info_box
|
| 1097 |
gr.update(interactive=True), # restart_episode_btn
|
|
@@ -1164,12 +1464,20 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1164 |
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 1165 |
};
|
| 1166 |
const videoEl = document.querySelector('#demo_video video');
|
|
|
|
|
|
|
|
|
|
| 1167 |
return (
|
| 1168 |
visible('video_phase_group') &&
|
| 1169 |
visible('demo_video') &&
|
|
|
|
| 1170 |
!visible('action_phase_group') &&
|
| 1171 |
!visible('control_panel_group') &&
|
| 1172 |
-
!!(videoEl && videoEl.currentSrc)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
);
|
| 1174 |
}""",
|
| 1175 |
timeout=10000,
|
|
@@ -1178,6 +1486,7 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1178 |
phase_after_switch = _read_phase_visibility(page)
|
| 1179 |
assert phase_after_switch["videoPhase"] is True
|
| 1180 |
assert phase_after_switch["video"] is True
|
|
|
|
| 1181 |
assert phase_after_switch["actionPhase"] is False
|
| 1182 |
assert phase_after_switch["controlPhase"] is False
|
| 1183 |
assert phase_after_switch["currentSrc"]
|
|
@@ -1187,14 +1496,18 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1187 |
assert switch_calls == [("uid-header-video", "VideoPlaceButton")]
|
| 1188 |
assert _read_header_task_value(page) == "VideoPlaceButton"
|
| 1189 |
|
| 1190 |
-
|
|
|
|
| 1191 |
"""() => {
|
| 1192 |
-
const
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
return true;
|
| 1196 |
-
}"""
|
|
|
|
| 1197 |
)
|
|
|
|
|
|
|
| 1198 |
assert did_dispatch_end
|
| 1199 |
|
| 1200 |
page.wait_for_function(
|
|
@@ -1208,6 +1521,7 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1208 |
return (
|
| 1209 |
!visible('video_phase_group') &&
|
| 1210 |
!visible('demo_video') &&
|
|
|
|
| 1211 |
visible('action_phase_group') &&
|
| 1212 |
visible('control_panel_group') &&
|
| 1213 |
visible('live_obs') &&
|
|
@@ -1220,6 +1534,7 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1220 |
phase_after_end = _read_phase_visibility(page)
|
| 1221 |
assert phase_after_end["videoPhase"] is False
|
| 1222 |
assert phase_after_end["video"] is False
|
|
|
|
| 1223 |
assert phase_after_end["actionPhase"] is True
|
| 1224 |
assert phase_after_end["action"] is True
|
| 1225 |
assert phase_after_end["controlPhase"] is True
|
|
@@ -1234,6 +1549,7 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
|
|
| 1234 |
|
| 1235 |
def test_phase_machine_runtime_local_video_path_end_transition():
|
| 1236 |
import gradio_callbacks as cb
|
|
|
|
| 1237 |
|
| 1238 |
demo_video_path = gr.get_video("world.mp4")
|
| 1239 |
fake_obs = np.zeros((24, 24, 3), dtype=np.uint8)
|
|
@@ -1275,9 +1591,20 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1275 |
try:
|
| 1276 |
with gr.Blocks(title="Native phase machine local video test") as demo:
|
| 1277 |
uid_state = gr.State(value="uid-local-video")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
with gr.Column(visible=False, elem_id="main_interface") as main_interface:
|
| 1279 |
with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
|
| 1280 |
video_display = gr.Video(value=None, elem_id="demo_video", autoplay=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
|
| 1282 |
with gr.Column(visible=True, elem_id="action_phase_group") as action_phase_group:
|
| 1283 |
img_display = gr.Image(value=fake_obs.copy(), elem_id="live_obs")
|
|
@@ -1317,6 +1644,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1317 |
goal_box,
|
| 1318 |
coords_box,
|
| 1319 |
video_display,
|
|
|
|
| 1320 |
task_info_box,
|
| 1321 |
progress_info_box,
|
| 1322 |
restart_episode_btn,
|
|
@@ -1332,10 +1660,23 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1332 |
queue=False,
|
| 1333 |
)
|
| 1334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1335 |
video_display.end(
|
| 1336 |
fn=cb.on_video_end_transition,
|
| 1337 |
inputs=[uid_state],
|
| 1338 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
queue=False,
|
| 1340 |
)
|
| 1341 |
|
|
@@ -1370,6 +1711,7 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1370 |
};
|
| 1371 |
return {
|
| 1372 |
video: visible('demo_video'),
|
|
|
|
| 1373 |
action: visible('live_obs'),
|
| 1374 |
control: visible('action_radio'),
|
| 1375 |
};
|
|
@@ -1377,18 +1719,29 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1377 |
)
|
| 1378 |
assert phase_after_login == {
|
| 1379 |
"video": True,
|
|
|
|
| 1380 |
"action": False,
|
| 1381 |
"control": False,
|
| 1382 |
}
|
| 1383 |
|
| 1384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1385 |
"""() => {
|
| 1386 |
-
const
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
return true;
|
| 1390 |
-
}"""
|
|
|
|
| 1391 |
)
|
|
|
|
|
|
|
| 1392 |
assert did_dispatch_end
|
| 1393 |
|
| 1394 |
page.wait_for_function(
|
|
@@ -1399,7 +1752,12 @@ def test_phase_machine_runtime_local_video_path_end_transition():
|
|
| 1399 |
const st = getComputedStyle(el);
|
| 1400 |
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 1401 |
};
|
| 1402 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
}""",
|
| 1404 |
timeout=2000,
|
| 1405 |
)
|
|
|
|
| 113 |
return {
|
| 114 |
videoPhase: visible('video_phase_group'),
|
| 115 |
video: visible('demo_video'),
|
| 116 |
+
watchButton: visible('watch_demo_video_btn'),
|
| 117 |
actionPhase: visible('action_phase_group'),
|
| 118 |
action: visible('live_obs'),
|
| 119 |
controlPhase: visible('control_panel_group'),
|
|
|
|
| 124 |
)
|
| 125 |
|
| 126 |
|
| 127 |
+
def _read_demo_video_controls(page) -> dict[str, bool | None]:
|
| 128 |
+
return page.evaluate(
|
| 129 |
+
"""() => {
|
| 130 |
+
const visible = (id) => {
|
| 131 |
+
const el = document.getElementById(id);
|
| 132 |
+
if (!el) return false;
|
| 133 |
+
const st = getComputedStyle(el);
|
| 134 |
+
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 135 |
+
};
|
| 136 |
+
const videoEl = document.querySelector('#demo_video video');
|
| 137 |
+
const button =
|
| 138 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 139 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 140 |
+
return {
|
| 141 |
+
videoVisible: visible('demo_video'),
|
| 142 |
+
buttonVisible: visible('watch_demo_video_btn'),
|
| 143 |
+
buttonDisabled: button ? button.disabled : null,
|
| 144 |
+
autoplay: videoEl ? videoEl.autoplay : null,
|
| 145 |
+
paused: videoEl ? videoEl.paused : null,
|
| 146 |
+
};
|
| 147 |
+
}"""
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _click_demo_video_button(page) -> None:
|
| 152 |
+
page.locator("#watch_demo_video_btn button, button#watch_demo_video_btn").first.click()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _dispatch_video_event(page, event_name: str) -> bool:
|
| 156 |
+
return page.evaluate(
|
| 157 |
+
"""(eventName) => {
|
| 158 |
+
const targets = [
|
| 159 |
+
document.querySelector('#demo_video video'),
|
| 160 |
+
document.getElementById('demo_video'),
|
| 161 |
+
].filter(Boolean);
|
| 162 |
+
if (!targets.length) return false;
|
| 163 |
+
for (const target of targets) {
|
| 164 |
+
target.dispatchEvent(new Event(eventName, { bubbles: true, composed: true }));
|
| 165 |
+
}
|
| 166 |
+
return true;
|
| 167 |
+
}""",
|
| 168 |
+
event_name,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
def _read_live_obs_geometry(page) -> dict[str, dict[str, float] | None]:
|
| 173 |
return page.evaluate(
|
| 174 |
"""() => {
|
|
|
|
| 243 |
|
| 244 |
@pytest.fixture
|
| 245 |
def phase_machine_ui_url():
|
| 246 |
+
state = {"precheck_calls": 0, "play_clicks": 0}
|
| 247 |
demo_video_url = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
|
| 248 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 249 |
|
|
|
|
| 256 |
|
| 257 |
with gr.Column(visible=False, elem_id="main_interface") as main_interface:
|
| 258 |
with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
|
| 259 |
+
video_display = gr.Video(value=None, elem_id="demo_video", autoplay=False)
|
| 260 |
+
watch_demo_video_btn = gr.Button(
|
| 261 |
+
"Watch Video Input🎬",
|
| 262 |
+
elem_id="watch_demo_video_btn",
|
| 263 |
+
interactive=False,
|
| 264 |
+
visible=False,
|
| 265 |
+
)
|
| 266 |
|
| 267 |
with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
|
| 268 |
img_display = gr.Image(value=np.zeros((24, 24, 3), dtype=np.uint8), elem_id="live_obs")
|
|
|
|
| 280 |
next_task_btn = gr.Button("Next Task", elem_id="next_task_btn")
|
| 281 |
|
| 282 |
log_output = gr.Markdown("", elem_id="log_output")
|
| 283 |
+
simulate_stop_btn = gr.Button("Simulate Stop", elem_id="simulate_stop_btn")
|
| 284 |
+
|
| 285 |
+
demo.load(
|
| 286 |
+
fn=None,
|
| 287 |
+
js=ui_layout.DEMO_VIDEO_PLAY_BINDING_JS,
|
| 288 |
+
queue=False,
|
| 289 |
+
)
|
| 290 |
|
| 291 |
def login_fn():
|
| 292 |
return (
|
|
|
|
| 294 |
gr.update(visible=True),
|
| 295 |
gr.update(visible=True),
|
| 296 |
gr.update(value=demo_video_url, visible=True),
|
| 297 |
+
gr.update(visible=True, interactive=True),
|
| 298 |
gr.update(visible=False),
|
| 299 |
gr.update(visible=False),
|
| 300 |
gr.update(visible=False),
|
|
|
|
| 303 |
"demo_video",
|
| 304 |
)
|
| 305 |
|
| 306 |
+
def on_play_demo_fn():
|
| 307 |
+
state["play_clicks"] += 1
|
| 308 |
+
return gr.update(visible=True, interactive=False)
|
| 309 |
+
|
| 310 |
+
def on_simulate_stop_fn():
|
| 311 |
+
return "stopped"
|
| 312 |
+
|
| 313 |
def on_video_end_fn():
|
| 314 |
return (
|
| 315 |
gr.update(visible=False),
|
|
|
|
| 317 |
gr.update(visible=True),
|
| 318 |
gr.update(visible=True),
|
| 319 |
gr.update(interactive=True),
|
| 320 |
+
gr.update(visible=False, interactive=False),
|
| 321 |
"action_keypoint",
|
| 322 |
)
|
| 323 |
|
|
|
|
| 361 |
main_interface,
|
| 362 |
video_phase_group,
|
| 363 |
video_display,
|
| 364 |
+
watch_demo_video_btn,
|
| 365 |
action_phase_group,
|
| 366 |
control_panel_group,
|
| 367 |
action_buttons_row,
|
|
|
|
| 372 |
queue=False,
|
| 373 |
)
|
| 374 |
|
| 375 |
+
watch_demo_video_btn.click(
|
| 376 |
+
fn=on_play_demo_fn,
|
| 377 |
+
outputs=[watch_demo_video_btn],
|
| 378 |
+
queue=False,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
video_display.end(
|
| 382 |
fn=on_video_end_fn,
|
| 383 |
outputs=[
|
|
|
|
| 386 |
control_panel_group,
|
| 387 |
action_buttons_row,
|
| 388 |
reference_action_btn,
|
| 389 |
+
watch_demo_video_btn,
|
| 390 |
+
phase_state,
|
| 391 |
+
],
|
| 392 |
+
queue=False,
|
| 393 |
+
)
|
| 394 |
+
video_display.stop(
|
| 395 |
+
fn=on_video_end_fn,
|
| 396 |
+
outputs=[
|
| 397 |
+
video_phase_group,
|
| 398 |
+
action_phase_group,
|
| 399 |
+
control_panel_group,
|
| 400 |
+
action_buttons_row,
|
| 401 |
+
reference_action_btn,
|
| 402 |
+
watch_demo_video_btn,
|
| 403 |
phase_state,
|
| 404 |
],
|
| 405 |
queue=False,
|
| 406 |
)
|
| 407 |
+
simulate_stop_btn.click(
|
| 408 |
+
fn=on_simulate_stop_fn,
|
| 409 |
+
outputs=[log_output],
|
| 410 |
+
js="""() => {
|
| 411 |
+
const show = (id, visible) => {
|
| 412 |
+
const el = document.getElementById(id);
|
| 413 |
+
if (!el) return;
|
| 414 |
+
el.style.display = visible ? '' : 'none';
|
| 415 |
+
};
|
| 416 |
+
show('video_phase_group', false);
|
| 417 |
+
show('demo_video', false);
|
| 418 |
+
show('action_phase_group', true);
|
| 419 |
+
show('live_obs', true);
|
| 420 |
+
show('control_panel_group', true);
|
| 421 |
+
show('action_radio', true);
|
| 422 |
+
show('action_buttons_row', true);
|
| 423 |
+
show('watch_demo_video_btn', false);
|
| 424 |
+
const refBtn =
|
| 425 |
+
document.querySelector('#reference_action_btn button') ||
|
| 426 |
+
document.querySelector('button#reference_action_btn');
|
| 427 |
+
if (refBtn) {
|
| 428 |
+
refBtn.disabled = false;
|
| 429 |
+
}
|
| 430 |
+
return [];
|
| 431 |
+
}""",
|
| 432 |
+
queue=False,
|
| 433 |
+
)
|
| 434 |
|
| 435 |
exec_btn.click(
|
| 436 |
fn=precheck_fn,
|
|
|
|
| 543 |
};
|
| 544 |
return {
|
| 545 |
video: visible('demo_video'),
|
| 546 |
+
watchButton: visible('watch_demo_video_btn'),
|
| 547 |
action: visible('live_obs'),
|
| 548 |
control: visible('action_radio'),
|
| 549 |
};
|
|
|
|
| 551 |
)
|
| 552 |
assert phase_after_login == {
|
| 553 |
"video": True,
|
| 554 |
+
"watchButton": True,
|
| 555 |
"action": False,
|
| 556 |
"control": False,
|
| 557 |
}
|
| 558 |
|
| 559 |
page.wait_for_selector("#demo_video video", timeout=5000)
|
| 560 |
+
page.wait_for_function(
|
| 561 |
"""() => {
|
| 562 |
const videoEl = document.querySelector('#demo_video video');
|
| 563 |
+
const button =
|
| 564 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 565 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 566 |
+
return !!videoEl && !!videoEl.currentSrc && !!button && button.disabled === false && videoEl.paused === true;
|
| 567 |
+
}""",
|
| 568 |
+
timeout=10000,
|
| 569 |
)
|
| 570 |
+
controls_after_login = _read_demo_video_controls(page)
|
| 571 |
+
assert controls_after_login["videoVisible"] is True
|
| 572 |
+
assert controls_after_login["buttonVisible"] is True
|
| 573 |
+
assert controls_after_login["buttonDisabled"] is False
|
| 574 |
+
assert controls_after_login["autoplay"] is False
|
| 575 |
+
assert controls_after_login["paused"] is True
|
| 576 |
+
|
| 577 |
+
_click_demo_video_button(page)
|
| 578 |
+
page.wait_for_function(
|
| 579 |
+
"""() => {
|
| 580 |
+
const videoEl = document.querySelector('#demo_video video');
|
| 581 |
+
const button =
|
| 582 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 583 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 584 |
+
if (!videoEl || !button) return false;
|
| 585 |
+
return button.disabled === true && (videoEl.paused === false || videoEl.currentTime > 0);
|
| 586 |
+
}""",
|
| 587 |
+
timeout=10000,
|
| 588 |
+
)
|
| 589 |
+
controls_after_click = _read_demo_video_controls(page)
|
| 590 |
+
assert controls_after_click["buttonDisabled"] is True
|
| 591 |
+
assert controls_after_click["paused"] is False
|
| 592 |
+
|
| 593 |
+
did_dispatch_end = _dispatch_video_event(page, "ended")
|
| 594 |
assert did_dispatch_end
|
| 595 |
|
| 596 |
page.wait_for_function(
|
| 597 |
"""() => {
|
| 598 |
const action = document.getElementById('live_obs');
|
| 599 |
const control = document.getElementById('action_radio');
|
| 600 |
+
const watchButton = document.getElementById('watch_demo_video_btn');
|
| 601 |
+
if (!action || !control || !watchButton) return false;
|
| 602 |
+
return (
|
| 603 |
+
getComputedStyle(action).display !== 'none' &&
|
| 604 |
+
getComputedStyle(control).display !== 'none' &&
|
| 605 |
+
getComputedStyle(watchButton).display === 'none'
|
| 606 |
+
);
|
| 607 |
}"""
|
| 608 |
)
|
| 609 |
|
|
|
|
| 698 |
browser.close()
|
| 699 |
|
| 700 |
assert state["precheck_calls"] >= 2
|
| 701 |
+
assert state["play_clicks"] == 1
|
| 702 |
|
| 703 |
|
| 704 |
def test_reference_action_button_is_green_only_when_interactive(phase_machine_ui_url):
|
|
|
|
| 719 |
assert disabled_snapshot["backgroundColor"] != "rgb(31, 139, 76)"
|
| 720 |
|
| 721 |
page.wait_for_selector("#demo_video video", timeout=5000)
|
| 722 |
+
_click_demo_video_button(page)
|
| 723 |
+
page.wait_for_function(
|
| 724 |
"""() => {
|
| 725 |
+
const button =
|
| 726 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 727 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 728 |
+
return !!button && button.disabled === true;
|
| 729 |
+
}""",
|
| 730 |
+
timeout=5000,
|
| 731 |
)
|
| 732 |
+
did_dispatch_end = _dispatch_video_event(page, "ended")
|
| 733 |
assert did_dispatch_end
|
| 734 |
|
| 735 |
page.wait_for_function(
|
|
|
|
| 750 |
browser.close()
|
| 751 |
|
| 752 |
|
| 753 |
+
@pytest.mark.xfail(
|
| 754 |
+
reason="Gradio 6.9.0 output video stop path is not reliably triggerable in headless Chromium; transition contract is covered by unit tests.",
|
| 755 |
+
strict=False,
|
| 756 |
+
)
|
| 757 |
+
def test_demo_video_stop_event_transitions_and_hides_button(phase_machine_ui_url):
|
| 758 |
+
root_url, state = phase_machine_ui_url
|
| 759 |
+
|
| 760 |
+
with sync_playwright() as p:
|
| 761 |
+
browser = p.chromium.launch(headless=True)
|
| 762 |
+
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
| 763 |
+
page.goto(root_url, wait_until="domcontentloaded")
|
| 764 |
+
|
| 765 |
+
page.wait_for_timeout(2500)
|
| 766 |
+
page.wait_for_selector("#login_btn", timeout=20000)
|
| 767 |
+
page.click("#login_btn")
|
| 768 |
+
page.wait_for_selector("#demo_video video", timeout=5000)
|
| 769 |
+
|
| 770 |
+
_click_demo_video_button(page)
|
| 771 |
+
page.wait_for_function(
|
| 772 |
+
"""() => {
|
| 773 |
+
const button =
|
| 774 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 775 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 776 |
+
return !!button && button.disabled === true;
|
| 777 |
+
}""",
|
| 778 |
+
timeout=5000,
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
page.locator("#simulate_stop_btn button, button#simulate_stop_btn").first.click()
|
| 782 |
+
|
| 783 |
+
page.wait_for_function(
|
| 784 |
+
"""() => {
|
| 785 |
+
const visible = (id) => {
|
| 786 |
+
const el = document.getElementById(id);
|
| 787 |
+
if (!el) return false;
|
| 788 |
+
const st = getComputedStyle(el);
|
| 789 |
+
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 790 |
+
};
|
| 791 |
+
return (
|
| 792 |
+
!visible('watch_demo_video_btn') &&
|
| 793 |
+
!visible('demo_video') &&
|
| 794 |
+
visible('live_obs') &&
|
| 795 |
+
visible('action_radio')
|
| 796 |
+
);
|
| 797 |
+
}""",
|
| 798 |
+
timeout=5000,
|
| 799 |
+
)
|
| 800 |
+
|
| 801 |
+
browser.close()
|
| 802 |
+
|
| 803 |
+
assert state["play_clicks"] == 1
|
| 804 |
+
|
| 805 |
+
|
| 806 |
def test_unified_loading_overlay_init_flow(monkeypatch):
|
| 807 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 808 |
|
|
|
|
| 827 |
"goal", # goal_box
|
| 828 |
"No need for coordinates", # coords_box
|
| 829 |
gr.update(value=None, visible=False), # video_display
|
| 830 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 831 |
"PickXtimes (Episode 1)", # task_info_box
|
| 832 |
"Completed: 0", # progress_info_box
|
| 833 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 897 |
assert calls["init"] >= 1
|
| 898 |
|
| 899 |
|
| 900 |
+
def test_no_video_task_hides_manual_demo_button(monkeypatch):
|
| 901 |
+
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 902 |
+
|
| 903 |
+
fake_obs = np.zeros((24, 24, 3), dtype=np.uint8)
|
| 904 |
+
fake_obs_img = Image.fromarray(fake_obs)
|
| 905 |
+
|
| 906 |
+
def fake_init_app(_request=None):
|
| 907 |
+
return (
|
| 908 |
+
"uid-no-video",
|
| 909 |
+
gr.update(visible=True), # main_interface
|
| 910 |
+
gr.update(value=fake_obs_img.copy(), interactive=False), # img_display
|
| 911 |
+
"ready", # log_output
|
| 912 |
+
gr.update(choices=[("pick", 0)], value=None), # options_radio
|
| 913 |
+
"goal", # goal_box
|
| 914 |
+
"No need for coordinates", # coords_box
|
| 915 |
+
gr.update(value=None, visible=False), # video_display
|
| 916 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 917 |
+
"PickXtimes (Episode 1)", # task_info_box
|
| 918 |
+
"Completed: 0", # progress_info_box
|
| 919 |
+
gr.update(interactive=True), # restart_episode_btn
|
| 920 |
+
gr.update(interactive=True), # next_task_btn
|
| 921 |
+
gr.update(interactive=True), # exec_btn
|
| 922 |
+
gr.update(visible=False), # video_phase_group
|
| 923 |
+
gr.update(visible=True), # action_phase_group
|
| 924 |
+
gr.update(visible=True), # control_panel_group
|
| 925 |
+
gr.update(value="hint"), # task_hint_display
|
| 926 |
+
gr.update(visible=False), # loading_overlay
|
| 927 |
+
gr.update(interactive=True), # reference_action_btn
|
| 928 |
+
)
|
| 929 |
+
|
| 930 |
+
monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
|
| 931 |
+
|
| 932 |
+
demo = ui_layout.create_ui_blocks()
|
| 933 |
+
|
| 934 |
+
port = _free_port()
|
| 935 |
+
host = "127.0.0.1"
|
| 936 |
+
root_url = f"http://{host}:{port}/"
|
| 937 |
+
|
| 938 |
+
app = FastAPI(title="native-no-video-test")
|
| 939 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
| 940 |
+
|
| 941 |
+
config = uvicorn.Config(app, host=host, port=port, log_level="error")
|
| 942 |
+
server = uvicorn.Server(config)
|
| 943 |
+
thread = threading.Thread(target=server.run, daemon=True)
|
| 944 |
+
thread.start()
|
| 945 |
+
_wait_http_ready(root_url)
|
| 946 |
+
|
| 947 |
+
try:
|
| 948 |
+
with sync_playwright() as p:
|
| 949 |
+
browser = p.chromium.launch(headless=True)
|
| 950 |
+
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
| 951 |
+
page.goto(root_url, wait_until="domcontentloaded")
|
| 952 |
+
page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
|
| 953 |
+
|
| 954 |
+
page.wait_for_function(
|
| 955 |
+
"""() => {
|
| 956 |
+
const visible = (id) => {
|
| 957 |
+
const el = document.getElementById(id);
|
| 958 |
+
if (!el) return false;
|
| 959 |
+
const st = getComputedStyle(el);
|
| 960 |
+
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 961 |
+
};
|
| 962 |
+
return (
|
| 963 |
+
!visible('video_phase_group') &&
|
| 964 |
+
!visible('demo_video') &&
|
| 965 |
+
!visible('watch_demo_video_btn') &&
|
| 966 |
+
visible('action_phase_group') &&
|
| 967 |
+
visible('control_panel_group')
|
| 968 |
+
);
|
| 969 |
+
}""",
|
| 970 |
+
timeout=5000,
|
| 971 |
+
)
|
| 972 |
+
|
| 973 |
+
phase_snapshot = _read_phase_visibility(page)
|
| 974 |
+
controls_snapshot = _read_demo_video_controls(page)
|
| 975 |
+
assert phase_snapshot["videoPhase"] is False
|
| 976 |
+
assert phase_snapshot["video"] is False
|
| 977 |
+
assert phase_snapshot["watchButton"] is False
|
| 978 |
+
assert phase_snapshot["actionPhase"] is True
|
| 979 |
+
assert phase_snapshot["controlPhase"] is True
|
| 980 |
+
assert controls_snapshot["buttonVisible"] is False
|
| 981 |
+
|
| 982 |
+
browser.close()
|
| 983 |
+
finally:
|
| 984 |
+
server.should_exit = True
|
| 985 |
+
thread.join(timeout=10)
|
| 986 |
+
demo.close()
|
| 987 |
+
|
| 988 |
+
|
| 989 |
def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch):
|
| 990 |
callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
|
| 991 |
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
|
|
|
| 1014 |
interactive=False,
|
| 1015 |
), # coords_box
|
| 1016 |
gr.update(value=None, visible=False), # video_display
|
| 1017 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 1018 |
"ResizeEnv (Episode 1)", # task_info_box
|
| 1019 |
"Completed: 0", # progress_info_box
|
| 1020 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 1243 |
"goal", # goal_box
|
| 1244 |
"No need for coordinates", # coords_box
|
| 1245 |
gr.update(value=None, visible=False), # video_display
|
| 1246 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 1247 |
"PickXtimes (Episode 1)", # task_info_box
|
| 1248 |
"Completed: 0", # progress_info_box
|
| 1249 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 1319 |
"goal", # goal_box
|
| 1320 |
"No need for coordinates", # coords_box
|
| 1321 |
gr.update(value=None, visible=False), # video_display
|
| 1322 |
+
gr.update(visible=False, interactive=False), # watch_demo_video_btn
|
| 1323 |
task_info_text, # task_info_box
|
| 1324 |
"Completed: 0", # progress_info_box
|
| 1325 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 1391 |
"video goal" if show_video else "goal", # goal_box
|
| 1392 |
"No need for coordinates", # coords_box
|
| 1393 |
gr.update(value=demo_video_path if show_video else None, visible=show_video), # video_display
|
| 1394 |
+
gr.update(visible=show_video, interactive=show_video), # watch_demo_video_btn
|
| 1395 |
f"{task_name} (Episode 1)", # task_info_box
|
| 1396 |
"Completed: 0", # progress_info_box
|
| 1397 |
gr.update(interactive=True), # restart_episode_btn
|
|
|
|
| 1464 |
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 1465 |
};
|
| 1466 |
const videoEl = document.querySelector('#demo_video video');
|
| 1467 |
+
const button =
|
| 1468 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 1469 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 1470 |
return (
|
| 1471 |
visible('video_phase_group') &&
|
| 1472 |
visible('demo_video') &&
|
| 1473 |
+
visible('watch_demo_video_btn') &&
|
| 1474 |
!visible('action_phase_group') &&
|
| 1475 |
!visible('control_panel_group') &&
|
| 1476 |
+
!!(videoEl && videoEl.currentSrc) &&
|
| 1477 |
+
!!button &&
|
| 1478 |
+
button.disabled === false &&
|
| 1479 |
+
videoEl.paused === true &&
|
| 1480 |
+
videoEl.autoplay === false
|
| 1481 |
);
|
| 1482 |
}""",
|
| 1483 |
timeout=10000,
|
|
|
|
| 1486 |
phase_after_switch = _read_phase_visibility(page)
|
| 1487 |
assert phase_after_switch["videoPhase"] is True
|
| 1488 |
assert phase_after_switch["video"] is True
|
| 1489 |
+
assert phase_after_switch["watchButton"] is True
|
| 1490 |
assert phase_after_switch["actionPhase"] is False
|
| 1491 |
assert phase_after_switch["controlPhase"] is False
|
| 1492 |
assert phase_after_switch["currentSrc"]
|
|
|
|
| 1496 |
assert switch_calls == [("uid-header-video", "VideoPlaceButton")]
|
| 1497 |
assert _read_header_task_value(page) == "VideoPlaceButton"
|
| 1498 |
|
| 1499 |
+
_click_demo_video_button(page)
|
| 1500 |
+
page.wait_for_function(
|
| 1501 |
"""() => {
|
| 1502 |
+
const button =
|
| 1503 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 1504 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 1505 |
+
return !!button && button.disabled === true;
|
| 1506 |
+
}""",
|
| 1507 |
+
timeout=5000,
|
| 1508 |
)
|
| 1509 |
+
|
| 1510 |
+
did_dispatch_end = _dispatch_video_event(page, "ended")
|
| 1511 |
assert did_dispatch_end
|
| 1512 |
|
| 1513 |
page.wait_for_function(
|
|
|
|
| 1521 |
return (
|
| 1522 |
!visible('video_phase_group') &&
|
| 1523 |
!visible('demo_video') &&
|
| 1524 |
+
!visible('watch_demo_video_btn') &&
|
| 1525 |
visible('action_phase_group') &&
|
| 1526 |
visible('control_panel_group') &&
|
| 1527 |
visible('live_obs') &&
|
|
|
|
| 1534 |
phase_after_end = _read_phase_visibility(page)
|
| 1535 |
assert phase_after_end["videoPhase"] is False
|
| 1536 |
assert phase_after_end["video"] is False
|
| 1537 |
+
assert phase_after_end["watchButton"] is False
|
| 1538 |
assert phase_after_end["actionPhase"] is True
|
| 1539 |
assert phase_after_end["action"] is True
|
| 1540 |
assert phase_after_end["controlPhase"] is True
|
|
|
|
| 1549 |
|
| 1550 |
def test_phase_machine_runtime_local_video_path_end_transition():
|
| 1551 |
import gradio_callbacks as cb
|
| 1552 |
+
ui_layout = importlib.reload(importlib.import_module("ui_layout"))
|
| 1553 |
|
| 1554 |
demo_video_path = gr.get_video("world.mp4")
|
| 1555 |
fake_obs = np.zeros((24, 24, 3), dtype=np.uint8)
|
|
|
|
| 1591 |
try:
|
| 1592 |
with gr.Blocks(title="Native phase machine local video test") as demo:
|
| 1593 |
uid_state = gr.State(value="uid-local-video")
|
| 1594 |
+
demo.load(
|
| 1595 |
+
fn=None,
|
| 1596 |
+
js=ui_layout.DEMO_VIDEO_PLAY_BINDING_JS,
|
| 1597 |
+
queue=False,
|
| 1598 |
+
)
|
| 1599 |
with gr.Column(visible=False, elem_id="main_interface") as main_interface:
|
| 1600 |
with gr.Column(visible=False, elem_id="video_phase_group") as video_phase_group:
|
| 1601 |
video_display = gr.Video(value=None, elem_id="demo_video", autoplay=False)
|
| 1602 |
+
watch_demo_video_btn = gr.Button(
|
| 1603 |
+
"Watch Video Input🎬",
|
| 1604 |
+
elem_id="watch_demo_video_btn",
|
| 1605 |
+
interactive=False,
|
| 1606 |
+
visible=False,
|
| 1607 |
+
)
|
| 1608 |
|
| 1609 |
with gr.Column(visible=True, elem_id="action_phase_group") as action_phase_group:
|
| 1610 |
img_display = gr.Image(value=fake_obs.copy(), elem_id="live_obs")
|
|
|
|
| 1644 |
goal_box,
|
| 1645 |
coords_box,
|
| 1646 |
video_display,
|
| 1647 |
+
watch_demo_video_btn,
|
| 1648 |
task_info_box,
|
| 1649 |
progress_info_box,
|
| 1650 |
restart_episode_btn,
|
|
|
|
| 1660 |
queue=False,
|
| 1661 |
)
|
| 1662 |
|
| 1663 |
+
watch_demo_video_btn.click(
|
| 1664 |
+
fn=cb.on_demo_video_play,
|
| 1665 |
+
inputs=[uid_state],
|
| 1666 |
+
outputs=[watch_demo_video_btn],
|
| 1667 |
+
queue=False,
|
| 1668 |
+
)
|
| 1669 |
+
|
| 1670 |
video_display.end(
|
| 1671 |
fn=cb.on_video_end_transition,
|
| 1672 |
inputs=[uid_state],
|
| 1673 |
+
outputs=[
|
| 1674 |
+
video_phase_group,
|
| 1675 |
+
action_phase_group,
|
| 1676 |
+
control_panel_group,
|
| 1677 |
+
log_output,
|
| 1678 |
+
watch_demo_video_btn,
|
| 1679 |
+
],
|
| 1680 |
queue=False,
|
| 1681 |
)
|
| 1682 |
|
|
|
|
| 1711 |
};
|
| 1712 |
return {
|
| 1713 |
video: visible('demo_video'),
|
| 1714 |
+
watchButton: visible('watch_demo_video_btn'),
|
| 1715 |
action: visible('live_obs'),
|
| 1716 |
control: visible('action_radio'),
|
| 1717 |
};
|
|
|
|
| 1719 |
)
|
| 1720 |
assert phase_after_login == {
|
| 1721 |
"video": True,
|
| 1722 |
+
"watchButton": True,
|
| 1723 |
"action": False,
|
| 1724 |
"control": False,
|
| 1725 |
}
|
| 1726 |
|
| 1727 |
+
controls_after_login = _read_demo_video_controls(page)
|
| 1728 |
+
assert controls_after_login["buttonVisible"] is True
|
| 1729 |
+
assert controls_after_login["buttonDisabled"] is False
|
| 1730 |
+
assert controls_after_login["autoplay"] is False
|
| 1731 |
+
assert controls_after_login["paused"] is True
|
| 1732 |
+
|
| 1733 |
+
_click_demo_video_button(page)
|
| 1734 |
+
page.wait_for_function(
|
| 1735 |
"""() => {
|
| 1736 |
+
const button =
|
| 1737 |
+
document.querySelector('#watch_demo_video_btn button') ||
|
| 1738 |
+
document.querySelector('button#watch_demo_video_btn');
|
| 1739 |
+
return !!button && button.disabled === true;
|
| 1740 |
+
}""",
|
| 1741 |
+
timeout=5000,
|
| 1742 |
)
|
| 1743 |
+
|
| 1744 |
+
did_dispatch_end = _dispatch_video_event(page, "ended")
|
| 1745 |
assert did_dispatch_end
|
| 1746 |
|
| 1747 |
page.wait_for_function(
|
|
|
|
| 1752 |
const st = getComputedStyle(el);
|
| 1753 |
return st.display !== 'none' && st.visibility !== 'hidden' && el.getClientRects().length > 0;
|
| 1754 |
};
|
| 1755 |
+
return (
|
| 1756 |
+
visible('live_obs') &&
|
| 1757 |
+
visible('action_radio') &&
|
| 1758 |
+
!visible('demo_video') &&
|
| 1759 |
+
!visible('watch_demo_video_btn')
|
| 1760 |
+
);
|
| 1761 |
}""",
|
| 1762 |
timeout=2000,
|
| 1763 |
)
|
gradio-web/test/test_ui_text_config.py
CHANGED
|
@@ -10,12 +10,12 @@ class _FakeOptionSession:
|
|
| 10 |
|
| 11 |
|
| 12 |
class _FakeLoadSession:
|
| 13 |
-
def __init__(self, env_id, available_options, raw_solve_options):
|
| 14 |
self.env_id = env_id
|
| 15 |
self.available_options = available_options
|
| 16 |
self.raw_solve_options = raw_solve_options
|
| 17 |
-
self.language_goal =
|
| 18 |
-
self.demonstration_frames = []
|
| 19 |
|
| 20 |
def load_episode(self, env_id, episode_idx):
|
| 21 |
self.env_id = env_id
|
|
@@ -67,6 +67,29 @@ def test_on_video_end_transition_uses_configured_action_prompt(monkeypatch, relo
|
|
| 67 |
result = callbacks.on_video_end_transition("uid-1")
|
| 68 |
|
| 69 |
assert result[3] == "choose an action from config"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
def test_missing_session_paths_use_configured_session_error(monkeypatch, reload_module):
|
|
@@ -90,8 +113,8 @@ def test_get_ui_action_text_uses_configured_overrides_and_fallback(reload_module
|
|
| 90 |
config = reload_module("config")
|
| 91 |
|
| 92 |
patternlock_expected = {
|
| 93 |
-
"move forward": "move forward
|
| 94 |
-
"move backward": "move backward
|
| 95 |
"move left": "move left→",
|
| 96 |
"move right": "move right←",
|
| 97 |
"move forward-left": "move forward-left↘︎",
|
|
@@ -100,10 +123,10 @@ def test_get_ui_action_text_uses_configured_overrides_and_fallback(reload_module
|
|
| 100 |
"move backward-right": "move backward-right↖︎",
|
| 101 |
}
|
| 102 |
routestick_expected = {
|
| 103 |
-
"move to the nearest left target by circling around the stick clockwise": "move left clockwise↘︎→↗︎",
|
| 104 |
-
"move to the nearest right target by circling around the stick clockwise": "move right clockwise↖︎←↙︎",
|
| 105 |
-
"move to the nearest left target by circling around the stick counterclockwise": "move left counterclockwise↗︎→↘︎",
|
| 106 |
-
"move to the nearest right target by circling around the stick counterclockwise": "move right counterclockwise
|
| 107 |
}
|
| 108 |
|
| 109 |
for raw_action, expected in patternlock_expected.items():
|
|
@@ -121,7 +144,7 @@ def test_ui_option_label_uses_patternlock_configured_action_text(reload_module):
|
|
| 121 |
raw_solve_options=[{"label": "a", "action": "move forward", "available": False}],
|
| 122 |
)
|
| 123 |
|
| 124 |
-
assert callbacks._ui_option_label(session, "fallback", 0) == "a. move forward
|
| 125 |
|
| 126 |
|
| 127 |
def test_ui_option_label_uses_routestick_configured_action_text(reload_module):
|
|
@@ -138,7 +161,7 @@ def test_ui_option_label_uses_routestick_configured_action_text(reload_module):
|
|
| 138 |
],
|
| 139 |
)
|
| 140 |
|
| 141 |
-
assert callbacks._ui_option_label(session, "fallback", 0) == "d. move right counterclockwise
|
| 142 |
|
| 143 |
|
| 144 |
def test_load_status_task_appends_configured_keypoint_suffix_after_mapped_label(monkeypatch, reload_module):
|
|
@@ -165,12 +188,81 @@ def test_load_status_task_appends_configured_keypoint_suffix_after_mapped_label(
|
|
| 165 |
|
| 166 |
assert result[4]["choices"] == [
|
| 167 |
(
|
| 168 |
-
f"a. move forward
|
| 169 |
0,
|
| 170 |
)
|
| 171 |
]
|
| 172 |
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
def test_draw_coordinate_axes_uses_configured_routestick_overlay_labels(monkeypatch, reload_module):
|
| 175 |
config = reload_module("config")
|
| 176 |
image_utils = reload_module("image_utils")
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class _FakeLoadSession:
|
| 13 |
+
def __init__(self, env_id, available_options, raw_solve_options, demonstration_frames=None, language_goal=""):
|
| 14 |
self.env_id = env_id
|
| 15 |
self.available_options = available_options
|
| 16 |
self.raw_solve_options = raw_solve_options
|
| 17 |
+
self.language_goal = language_goal
|
| 18 |
+
self.demonstration_frames = demonstration_frames or []
|
| 19 |
|
| 20 |
def load_episode(self, env_id, episode_idx):
|
| 21 |
self.env_id = env_id
|
|
|
|
| 67 |
result = callbacks.on_video_end_transition("uid-1")
|
| 68 |
|
| 69 |
assert result[3] == "choose an action from config"
|
| 70 |
+
assert result[4]["visible"] is False
|
| 71 |
+
assert result[4]["interactive"] is False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_on_demo_video_play_disables_button_and_sets_single_use_state(monkeypatch, reload_module):
|
| 75 |
+
reload_module("config")
|
| 76 |
+
callbacks = reload_module("gradio_callbacks")
|
| 77 |
+
recorded = {"activity": [], "clicked": []}
|
| 78 |
+
|
| 79 |
+
monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: recorded["activity"].append(uid))
|
| 80 |
+
monkeypatch.setattr(callbacks, "get_play_button_clicked", lambda uid: False)
|
| 81 |
+
monkeypatch.setattr(
|
| 82 |
+
callbacks,
|
| 83 |
+
"set_play_button_clicked",
|
| 84 |
+
lambda uid, clicked=True: recorded["clicked"].append((uid, clicked)),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
result = callbacks.on_demo_video_play("uid-play")
|
| 88 |
+
|
| 89 |
+
assert recorded["activity"] == ["uid-play"]
|
| 90 |
+
assert recorded["clicked"] == [("uid-play", True)]
|
| 91 |
+
assert result["visible"] is True
|
| 92 |
+
assert result["interactive"] is False
|
| 93 |
|
| 94 |
|
| 95 |
def test_missing_session_paths_use_configured_session_error(monkeypatch, reload_module):
|
|
|
|
| 113 |
config = reload_module("config")
|
| 114 |
|
| 115 |
patternlock_expected = {
|
| 116 |
+
"move forward": "move forward↓",
|
| 117 |
+
"move backward": "move backward↑",
|
| 118 |
"move left": "move left→",
|
| 119 |
"move right": "move right←",
|
| 120 |
"move forward-left": "move forward-left↘︎",
|
|
|
|
| 123 |
"move backward-right": "move backward-right↖︎",
|
| 124 |
}
|
| 125 |
routestick_expected = {
|
| 126 |
+
"move to the nearest left target by circling around the stick clockwise": "move left clockwise↘︎→↗︎ ◟→◞",
|
| 127 |
+
"move to the nearest right target by circling around the stick clockwise": "move right clockwise↖︎←↙︎ ◟←◞",
|
| 128 |
+
"move to the nearest left target by circling around the stick counterclockwise": "move left counterclockwise↗︎→↘︎ ◜→◝",
|
| 129 |
+
"move to the nearest right target by circling around the stick counterclockwise": "move right counterclockwise↙︎←↖︎ ◜←◝",
|
| 130 |
}
|
| 131 |
|
| 132 |
for raw_action, expected in patternlock_expected.items():
|
|
|
|
| 144 |
raw_solve_options=[{"label": "a", "action": "move forward", "available": False}],
|
| 145 |
)
|
| 146 |
|
| 147 |
+
assert callbacks._ui_option_label(session, "fallback", 0) == "a. move forward↓"
|
| 148 |
|
| 149 |
|
| 150 |
def test_ui_option_label_uses_routestick_configured_action_text(reload_module):
|
|
|
|
| 161 |
],
|
| 162 |
)
|
| 163 |
|
| 164 |
+
assert callbacks._ui_option_label(session, "fallback", 0) == "d. move right counterclockwise↙︎←↖︎ ◜←◝"
|
| 165 |
|
| 166 |
|
| 167 |
def test_load_status_task_appends_configured_keypoint_suffix_after_mapped_label(monkeypatch, reload_module):
|
|
|
|
| 188 |
|
| 189 |
assert result[4]["choices"] == [
|
| 190 |
(
|
| 191 |
+
f"a. move forward↓{config.UI_TEXT['actions']['keypoint_required_suffix']}",
|
| 192 |
0,
|
| 193 |
)
|
| 194 |
]
|
| 195 |
|
| 196 |
|
| 197 |
+
def test_load_status_task_shows_demo_video_button_for_valid_video(monkeypatch, reload_module, tmp_path):
|
| 198 |
+
callbacks = reload_module("gradio_callbacks")
|
| 199 |
+
session = _FakeLoadSession(
|
| 200 |
+
env_id="VideoUnmask",
|
| 201 |
+
available_options=[("pick", 0)],
|
| 202 |
+
raw_solve_options=[{"label": "a", "action": "pick", "available": False}],
|
| 203 |
+
demonstration_frames=["frame-1"],
|
| 204 |
+
language_goal="remember the cube",
|
| 205 |
+
)
|
| 206 |
+
video_path = tmp_path / "demo.mp4"
|
| 207 |
+
video_path.write_bytes(b"demo")
|
| 208 |
+
|
| 209 |
+
monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
|
| 210 |
+
monkeypatch.setattr(callbacks, "reset_play_button_clicked", lambda uid: None)
|
| 211 |
+
monkeypatch.setattr(callbacks, "reset_execute_count", lambda uid, env_id, episode_idx: None)
|
| 212 |
+
monkeypatch.setattr(callbacks, "set_task_start_time", lambda uid, env_id, episode_idx, start_time: None)
|
| 213 |
+
monkeypatch.setattr(callbacks, "set_ui_phase", lambda uid, phase: None)
|
| 214 |
+
monkeypatch.setattr(callbacks, "get_task_hint", lambda env_id: "")
|
| 215 |
+
monkeypatch.setattr(callbacks, "should_show_demo_video", lambda env_id: True)
|
| 216 |
+
monkeypatch.setattr(callbacks, "save_video", lambda frames, suffix="": str(video_path))
|
| 217 |
+
|
| 218 |
+
result = callbacks._load_status_task(
|
| 219 |
+
"uid-video",
|
| 220 |
+
{"current_task": {"env_id": "VideoUnmask", "episode_idx": 1}, "completed_count": 0},
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
assert result[7]["visible"] is True
|
| 224 |
+
assert result[7]["value"] == str(video_path)
|
| 225 |
+
assert result[8]["visible"] is True
|
| 226 |
+
assert result[8]["interactive"] is True
|
| 227 |
+
assert result[14]["visible"] is True
|
| 228 |
+
assert result[15]["visible"] is False
|
| 229 |
+
assert result[16]["visible"] is False
|
| 230 |
+
assert callbacks.UI_TEXT["log"]["demo_video_prompt"] in result[3]
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def test_load_status_task_hides_demo_video_button_when_video_is_missing(monkeypatch, reload_module):
|
| 234 |
+
callbacks = reload_module("gradio_callbacks")
|
| 235 |
+
session = _FakeLoadSession(
|
| 236 |
+
env_id="VideoUnmask",
|
| 237 |
+
available_options=[("pick", 0)],
|
| 238 |
+
raw_solve_options=[{"label": "a", "action": "pick", "available": False}],
|
| 239 |
+
demonstration_frames=["frame-1"],
|
| 240 |
+
language_goal="remember the cube",
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
monkeypatch.setattr(callbacks, "get_session", lambda uid: session)
|
| 244 |
+
monkeypatch.setattr(callbacks, "reset_play_button_clicked", lambda uid: None)
|
| 245 |
+
monkeypatch.setattr(callbacks, "reset_execute_count", lambda uid, env_id, episode_idx: None)
|
| 246 |
+
monkeypatch.setattr(callbacks, "set_task_start_time", lambda uid, env_id, episode_idx, start_time: None)
|
| 247 |
+
monkeypatch.setattr(callbacks, "set_ui_phase", lambda uid, phase: None)
|
| 248 |
+
monkeypatch.setattr(callbacks, "get_task_hint", lambda env_id: "")
|
| 249 |
+
monkeypatch.setattr(callbacks, "should_show_demo_video", lambda env_id: True)
|
| 250 |
+
monkeypatch.setattr(callbacks, "save_video", lambda frames, suffix="": None)
|
| 251 |
+
|
| 252 |
+
result = callbacks._load_status_task(
|
| 253 |
+
"uid-no-video",
|
| 254 |
+
{"current_task": {"env_id": "VideoUnmask", "episode_idx": 1}, "completed_count": 0},
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
assert result[7]["visible"] is False
|
| 258 |
+
assert result[8]["visible"] is False
|
| 259 |
+
assert result[8]["interactive"] is False
|
| 260 |
+
assert result[14]["visible"] is False
|
| 261 |
+
assert result[15]["visible"] is True
|
| 262 |
+
assert result[16]["visible"] is True
|
| 263 |
+
assert callbacks.UI_TEXT["log"]["action_selection_prompt"] in result[3]
|
| 264 |
+
|
| 265 |
+
|
| 266 |
def test_draw_coordinate_axes_uses_configured_routestick_overlay_labels(monkeypatch, reload_module):
|
| 267 |
config = reload_module("config")
|
| 268 |
image_utils = reload_module("image_utils")
|
gradio-web/ui_layout.py
CHANGED
|
@@ -21,6 +21,7 @@ from gradio_callbacks import (
|
|
| 21 |
init_app,
|
| 22 |
load_next_task_wrapper,
|
| 23 |
on_map_click,
|
|
|
|
| 24 |
on_option_select,
|
| 25 |
on_reference_action,
|
| 26 |
on_video_end_transition,
|
|
@@ -45,6 +46,42 @@ PHASE_EXECUTION_PLAYBACK = "execution_playback"
|
|
| 45 |
SYNC_JS = ""
|
| 46 |
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
LIVE_OBS_CLIENT_RESIZE_JS = r"""
|
| 49 |
() => {
|
| 50 |
if (window.__robommeLiveObsResizerInstalled) {
|
|
@@ -256,6 +293,12 @@ button#reference_action_btn:not(:disabled):hover {{
|
|
| 256 |
#live_obs.live-obs-resizable .upload-container {{
|
| 257 |
width: 100%;
|
| 258 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
"""
|
| 260 |
|
| 261 |
|
|
@@ -286,12 +329,12 @@ def _phase_from_updates(main_interface_update, video_phase_update):
|
|
| 286 |
|
| 287 |
|
| 288 |
def _with_phase_from_load(load_result):
|
| 289 |
-
phase = _phase_from_updates(load_result[1], load_result[
|
| 290 |
return (*load_result, phase)
|
| 291 |
|
| 292 |
|
| 293 |
def _skip_load_flow():
|
| 294 |
-
return tuple(gr.skip() for _ in range(
|
| 295 |
|
| 296 |
|
| 297 |
def _phase_visibility_updates(phase):
|
|
@@ -377,10 +420,18 @@ def create_ui_blocks():
|
|
| 377 |
label="Demonstration Video 🎬",
|
| 378 |
interactive=False,
|
| 379 |
elem_id="demo_video",
|
| 380 |
-
autoplay=
|
| 381 |
show_label=True,
|
| 382 |
visible=True,
|
| 383 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
|
| 386 |
img_display = gr.Image(
|
|
@@ -484,6 +535,7 @@ def create_ui_blocks():
|
|
| 484 |
goal_box,
|
| 485 |
coords_box,
|
| 486 |
video_display,
|
|
|
|
| 487 |
task_info_box,
|
| 488 |
progress_info_box,
|
| 489 |
restart_episode_btn,
|
|
@@ -650,7 +702,13 @@ def create_ui_blocks():
|
|
| 650 |
video_display.end(
|
| 651 |
fn=on_video_end_transition,
|
| 652 |
inputs=[uid_state],
|
| 653 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
queue=False,
|
| 655 |
show_progress="hidden",
|
| 656 |
).then(
|
|
@@ -662,7 +720,13 @@ def create_ui_blocks():
|
|
| 662 |
video_display.stop(
|
| 663 |
fn=on_video_end_transition,
|
| 664 |
inputs=[uid_state],
|
| 665 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
queue=False,
|
| 667 |
show_progress="hidden",
|
| 668 |
).then(
|
|
@@ -684,6 +748,14 @@ def create_ui_blocks():
|
|
| 684 |
outputs=[coords_box, img_display],
|
| 685 |
)
|
| 686 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
reference_action_btn.click(
|
| 688 |
fn=on_reference_action,
|
| 689 |
inputs=[uid_state],
|
|
@@ -748,6 +820,12 @@ def create_ui_blocks():
|
|
| 748 |
queue=False,
|
| 749 |
)
|
| 750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
demo.load(
|
| 752 |
fn=init_app_with_phase,
|
| 753 |
inputs=[],
|
|
|
|
| 21 |
init_app,
|
| 22 |
load_next_task_wrapper,
|
| 23 |
on_map_click,
|
| 24 |
+
on_demo_video_play,
|
| 25 |
on_option_select,
|
| 26 |
on_reference_action,
|
| 27 |
on_video_end_transition,
|
|
|
|
| 46 |
SYNC_JS = ""
|
| 47 |
|
| 48 |
|
| 49 |
+
DEMO_VIDEO_PLAY_BINDING_JS = r"""
|
| 50 |
+
() => {
|
| 51 |
+
const bindPlayButton = () => {
|
| 52 |
+
const button =
|
| 53 |
+
document.querySelector("#watch_demo_video_btn button") ||
|
| 54 |
+
document.querySelector("button#watch_demo_video_btn");
|
| 55 |
+
if (!button || button.dataset.robommeDemoPlayBound === "1") {
|
| 56 |
+
return;
|
| 57 |
+
}
|
| 58 |
+
button.dataset.robommeDemoPlayBound = "1";
|
| 59 |
+
button.addEventListener("click", () => {
|
| 60 |
+
const videoEl = document.querySelector("#demo_video video");
|
| 61 |
+
if (!videoEl) {
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
const playPromise = videoEl.play();
|
| 65 |
+
if (playPromise && typeof playPromise.catch === "function") {
|
| 66 |
+
playPromise.catch(() => {});
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
if (!window.__robommeDemoPlayBindingInstalled) {
|
| 72 |
+
const observer = new MutationObserver(() => bindPlayButton());
|
| 73 |
+
observer.observe(document.body, {
|
| 74 |
+
childList: true,
|
| 75 |
+
subtree: true,
|
| 76 |
+
});
|
| 77 |
+
window.__robommeDemoPlayBindingInstalled = true;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
bindPlayButton();
|
| 81 |
+
}
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
LIVE_OBS_CLIENT_RESIZE_JS = r"""
|
| 86 |
() => {
|
| 87 |
if (window.__robommeLiveObsResizerInstalled) {
|
|
|
|
| 293 |
#live_obs.live-obs-resizable .upload-container {{
|
| 294 |
width: 100%;
|
| 295 |
}}
|
| 296 |
+
|
| 297 |
+
#watch_demo_video_btn,
|
| 298 |
+
#watch_demo_video_btn button,
|
| 299 |
+
button#watch_demo_video_btn {{
|
| 300 |
+
width: 100%;
|
| 301 |
+
}}
|
| 302 |
"""
|
| 303 |
|
| 304 |
|
|
|
|
| 329 |
|
| 330 |
|
| 331 |
def _with_phase_from_load(load_result):
|
| 332 |
+
phase = _phase_from_updates(load_result[1], load_result[14])
|
| 333 |
return (*load_result, phase)
|
| 334 |
|
| 335 |
|
| 336 |
def _skip_load_flow():
|
| 337 |
+
return tuple(gr.skip() for _ in range(21))
|
| 338 |
|
| 339 |
|
| 340 |
def _phase_visibility_updates(phase):
|
|
|
|
| 420 |
label="Demonstration Video 🎬",
|
| 421 |
interactive=False,
|
| 422 |
elem_id="demo_video",
|
| 423 |
+
autoplay=False,
|
| 424 |
show_label=True,
|
| 425 |
visible=True,
|
| 426 |
)
|
| 427 |
+
watch_demo_video_btn = gr.Button(
|
| 428 |
+
"Watch Video Input🎬",
|
| 429 |
+
variant="primary",
|
| 430 |
+
size="lg",
|
| 431 |
+
interactive=False,
|
| 432 |
+
visible=False,
|
| 433 |
+
elem_id="watch_demo_video_btn",
|
| 434 |
+
)
|
| 435 |
|
| 436 |
with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
|
| 437 |
img_display = gr.Image(
|
|
|
|
| 535 |
goal_box,
|
| 536 |
coords_box,
|
| 537 |
video_display,
|
| 538 |
+
watch_demo_video_btn,
|
| 539 |
task_info_box,
|
| 540 |
progress_info_box,
|
| 541 |
restart_episode_btn,
|
|
|
|
| 702 |
video_display.end(
|
| 703 |
fn=on_video_end_transition,
|
| 704 |
inputs=[uid_state],
|
| 705 |
+
outputs=[
|
| 706 |
+
video_phase_group,
|
| 707 |
+
action_phase_group,
|
| 708 |
+
control_panel_group,
|
| 709 |
+
log_output,
|
| 710 |
+
watch_demo_video_btn,
|
| 711 |
+
],
|
| 712 |
queue=False,
|
| 713 |
show_progress="hidden",
|
| 714 |
).then(
|
|
|
|
| 720 |
video_display.stop(
|
| 721 |
fn=on_video_end_transition,
|
| 722 |
inputs=[uid_state],
|
| 723 |
+
outputs=[
|
| 724 |
+
video_phase_group,
|
| 725 |
+
action_phase_group,
|
| 726 |
+
control_panel_group,
|
| 727 |
+
log_output,
|
| 728 |
+
watch_demo_video_btn,
|
| 729 |
+
],
|
| 730 |
queue=False,
|
| 731 |
show_progress="hidden",
|
| 732 |
).then(
|
|
|
|
| 748 |
outputs=[coords_box, img_display],
|
| 749 |
)
|
| 750 |
|
| 751 |
+
watch_demo_video_btn.click(
|
| 752 |
+
fn=on_demo_video_play,
|
| 753 |
+
inputs=[uid_state],
|
| 754 |
+
outputs=[watch_demo_video_btn],
|
| 755 |
+
queue=False,
|
| 756 |
+
show_progress="hidden",
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
reference_action_btn.click(
|
| 760 |
fn=on_reference_action,
|
| 761 |
inputs=[uid_state],
|
|
|
|
| 820 |
queue=False,
|
| 821 |
)
|
| 822 |
|
| 823 |
+
demo.load(
|
| 824 |
+
fn=None,
|
| 825 |
+
js=DEMO_VIDEO_PLAY_BINDING_JS,
|
| 826 |
+
queue=False,
|
| 827 |
+
)
|
| 828 |
+
|
| 829 |
demo.load(
|
| 830 |
fn=init_app_with_phase,
|
| 831 |
inputs=[],
|