HongzeFu commited on
Commit
537c169
·
1 Parent(s): 651ba90

fix keypoint selection image size!!!!!!

Browse files
gradio-web/test/conftest.py CHANGED
@@ -18,7 +18,9 @@ def _find_repo_root(start_file: str | Path) -> Path:
18
 
19
  REPO_ROOT = _find_repo_root(__file__)
20
  SRC_ROOT = REPO_ROOT / "src"
21
- GRADIO_ROOT = REPO_ROOT / "gradio"
 
 
22
 
23
  for p in (str(REPO_ROOT), str(SRC_ROOT), str(GRADIO_ROOT)):
24
  if p not in sys.path:
 
18
 
19
  REPO_ROOT = _find_repo_root(__file__)
20
  SRC_ROOT = REPO_ROOT / "src"
21
+ GRADIO_ROOT = REPO_ROOT / "gradio-web"
22
+ if not GRADIO_ROOT.exists():
23
+ GRADIO_ROOT = REPO_ROOT / "gradio"
24
 
25
  for p in (str(REPO_ROOT), str(SRC_ROOT), str(GRADIO_ROOT)):
26
  if p not in sys.path:
gradio-web/test/test_ui_phase_machine_runtime_e2e.py CHANGED
@@ -61,6 +61,43 @@ def _read_header_task_value(page) -> str | None:
61
  )
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  @pytest.fixture
65
  def phase_machine_ui_url():
66
  state = {"precheck_calls": 0}
@@ -455,6 +492,245 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
455
  assert calls["init"] >= 1
456
 
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  def test_header_task_shows_env_after_init(monkeypatch):
459
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
460
 
 
61
  )
62
 
63
 
64
+ def _read_coords_box_value(page) -> str | None:
65
+ return page.evaluate(
66
+ """() => {
67
+ const root = document.getElementById('coords_box');
68
+ if (!root) return null;
69
+ const field = root.querySelector('textarea, input');
70
+ if (!field) return null;
71
+ const value = typeof field.value === 'string' ? field.value.trim() : '';
72
+ return value || null;
73
+ }"""
74
+ )
75
+
76
+
77
+ def _read_live_obs_geometry(page) -> dict[str, dict[str, float] | None]:
78
+ return page.evaluate(
79
+ """() => {
80
+ const root = document.getElementById('live_obs');
81
+ const container = root?.querySelector('.image-container');
82
+ const uploadContainer = root?.querySelector('.upload-container');
83
+ const frame = root?.querySelector('.image-frame');
84
+ const img = root?.querySelector('img');
85
+ const measure = (node) => {
86
+ if (!node) return null;
87
+ const rect = node.getBoundingClientRect();
88
+ return { width: rect.width, height: rect.height };
89
+ };
90
+ return {
91
+ root: measure(root),
92
+ container: measure(container),
93
+ uploadContainer: measure(uploadContainer),
94
+ frame: measure(frame),
95
+ img: measure(img),
96
+ };
97
+ }"""
98
+ )
99
+
100
+
101
  @pytest.fixture
102
  def phase_machine_ui_url():
103
  state = {"precheck_calls": 0}
 
492
  assert calls["init"] >= 1
493
 
494
 
495
+ def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch):
496
+ callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
497
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
498
+
499
+ fake_obs = np.zeros((24, 48, 3), dtype=np.uint8)
500
+ fake_obs_img = Image.fromarray(fake_obs)
501
+
502
+ class FakeSession:
503
+ raw_solve_options = [{"available": True}]
504
+
505
+ def get_pil_image(self, use_segmented=False):
506
+ _ = use_segmented
507
+ return fake_obs_img.copy()
508
+
509
+ def fake_init_app(_request=None):
510
+ return (
511
+ "uid-live-obs-resize",
512
+ gr.update(visible=True), # main_interface
513
+ gr.update(value=fake_obs_img.copy(), interactive=False), # img_display
514
+ "ready", # log_output
515
+ gr.update(choices=[("pick", 0)], value=0), # options_radio
516
+ "goal", # goal_box
517
+ gr.update(
518
+ value="please click the keypoint selection image",
519
+ visible=True,
520
+ interactive=False,
521
+ ), # coords_box
522
+ gr.update(value=None, visible=False), # video_display
523
+ "ResizeEnv (Episode 1)", # task_info_box
524
+ "Completed: 0", # progress_info_box
525
+ gr.update(interactive=True), # restart_episode_btn
526
+ gr.update(interactive=True), # next_task_btn
527
+ gr.update(interactive=True), # exec_btn
528
+ gr.update(visible=False), # video_phase_group
529
+ gr.update(visible=True), # action_phase_group
530
+ gr.update(visible=True), # control_panel_group
531
+ gr.update(value="hint"), # task_hint_display
532
+ gr.update(visible=False), # loading_overlay
533
+ gr.update(interactive=True), # reference_action_btn
534
+ )
535
+
536
+ monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
537
+ monkeypatch.setattr(callbacks, "get_session", lambda uid: FakeSession())
538
+ monkeypatch.setattr(callbacks, "update_session_activity", lambda uid: None)
539
+
540
+ demo = ui_layout.create_ui_blocks()
541
+
542
+ port = _free_port()
543
+ host = "127.0.0.1"
544
+ root_url = f"http://{host}:{port}/"
545
+
546
+ app = FastAPI(title="live-obs-client-resize-test")
547
+ app = gr.mount_gradio_app(app, demo, path="/")
548
+
549
+ config = uvicorn.Config(app, host=host, port=port, log_level="error")
550
+ server = uvicorn.Server(config)
551
+ thread = threading.Thread(target=server.run, daemon=True)
552
+ thread.start()
553
+ _wait_http_ready(root_url)
554
+
555
+ try:
556
+ with sync_playwright() as p:
557
+ browser = p.chromium.launch(headless=True)
558
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
559
+ page.goto(root_url, wait_until="domcontentloaded")
560
+ page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
561
+ page.wait_for_selector("#live_obs img", timeout=15000)
562
+ page.wait_for_selector("#coords_box textarea, #coords_box input", timeout=15000)
563
+ page.wait_for_function(
564
+ """() => {
565
+ const container = document.querySelector('#live_obs .image-container');
566
+ const img = document.querySelector('#live_obs img');
567
+ if (!container || !img) return false;
568
+ const containerRect = container.getBoundingClientRect();
569
+ const imgRect = img.getBoundingClientRect();
570
+ return imgRect.width > 200 && Math.abs(containerRect.width - imgRect.width) <= 2;
571
+ }""",
572
+ timeout=10000,
573
+ )
574
+
575
+ initial_geometry = _read_live_obs_geometry(page)
576
+ assert initial_geometry["container"] is not None
577
+ assert initial_geometry["img"] is not None
578
+ assert initial_geometry["uploadContainer"] is not None
579
+ assert initial_geometry["frame"] is not None
580
+ assert initial_geometry["img"]["width"] > 200
581
+ assert abs(initial_geometry["container"]["width"] - initial_geometry["img"]["width"]) <= 2
582
+ assert abs(initial_geometry["uploadContainer"]["width"] - initial_geometry["img"]["width"]) <= 2
583
+ assert abs(initial_geometry["frame"]["width"] - initial_geometry["img"]["width"]) <= 2
584
+ assert initial_geometry["img"]["width"] / initial_geometry["img"]["height"] == pytest.approx(2.0, rel=0.02)
585
+
586
+ page.set_viewport_size({"width": 1024, "height": 900})
587
+ page.wait_for_function(
588
+ """(prevWidth) => {
589
+ const container = document.querySelector('#live_obs .image-container');
590
+ const img = document.querySelector('#live_obs img');
591
+ if (!container || !img) return false;
592
+ const containerRect = container.getBoundingClientRect();
593
+ const imgRect = img.getBoundingClientRect();
594
+ return imgRect.width < prevWidth - 20 && Math.abs(containerRect.width - imgRect.width) <= 2;
595
+ }""",
596
+ arg=initial_geometry["img"]["width"],
597
+ timeout=10000,
598
+ )
599
+
600
+ resized_geometry = _read_live_obs_geometry(page)
601
+ assert resized_geometry["img"] is not None
602
+ assert resized_geometry["container"] is not None
603
+ assert resized_geometry["img"]["width"] < initial_geometry["img"]["width"] - 20
604
+ assert abs(resized_geometry["container"]["width"] - resized_geometry["img"]["width"]) <= 2
605
+ assert resized_geometry["img"]["width"] / resized_geometry["img"]["height"] == pytest.approx(2.0, rel=0.02)
606
+
607
+ box = page.locator("#live_obs img").bounding_box()
608
+ assert box is not None
609
+ target_x = box["x"] + ((36.5) / 48.0) * box["width"]
610
+ target_y = box["y"] + ((12.5) / 24.0) * box["height"]
611
+ page.mouse.click(target_x, target_y)
612
+ page.wait_for_function(
613
+ """() => {
614
+ const root = document.getElementById('coords_box');
615
+ const field = root?.querySelector('textarea, input');
616
+ return !!field && /^\\d+\\s*,\\s*\\d+$/.test(field.value.trim());
617
+ }""",
618
+ timeout=5000,
619
+ )
620
+ coords_value = _read_coords_box_value(page)
621
+ assert coords_value is not None
622
+ coord_x, coord_y = [int(part.strip()) for part in coords_value.split(",", 1)]
623
+ assert abs(coord_x - 36) <= 1
624
+ assert abs(coord_y - 12) <= 1
625
+
626
+ browser.close()
627
+ finally:
628
+ server.should_exit = True
629
+ thread.join(timeout=10)
630
+ demo.close()
631
+
632
+
633
+ def test_live_obs_client_resize_after_hidden_phase_becomes_visible(tmp_path):
634
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
635
+
636
+ full_red = np.zeros((256, 256, 3), dtype=np.uint8)
637
+ full_red[:, :] = [255, 0, 0]
638
+
639
+ with gr.Blocks() as demo:
640
+ demo.css = ui_layout.CSS
641
+
642
+ show_btn = gr.Button("Show", elem_id="show_btn")
643
+
644
+ with gr.Column(visible=False, elem_id="action_phase_group") as action_phase_group:
645
+ gr.Image(
646
+ value=full_red,
647
+ elem_id="live_obs",
648
+ elem_classes=["live-obs-resizable"],
649
+ buttons=[],
650
+ sources=[],
651
+ )
652
+
653
+ demo.load(
654
+ fn=None,
655
+ js=ui_layout.LIVE_OBS_CLIENT_RESIZE_JS,
656
+ queue=False,
657
+ )
658
+
659
+ show_btn.click(
660
+ fn=lambda: gr.update(visible=True),
661
+ outputs=[action_phase_group],
662
+ queue=False,
663
+ )
664
+
665
+ port = _free_port()
666
+ host = "127.0.0.1"
667
+ root_url = f"http://{host}:{port}/"
668
+
669
+ app = FastAPI(title="live-obs-hidden-phase-resize-test")
670
+ app = gr.mount_gradio_app(app, demo, path="/")
671
+
672
+ config = uvicorn.Config(app, host=host, port=port, log_level="error")
673
+ server = uvicorn.Server(config)
674
+ thread = threading.Thread(target=server.run, daemon=True)
675
+ thread.start()
676
+ _wait_http_ready(root_url)
677
+ screenshot_path = tmp_path / "live_obs_hidden_phase.png"
678
+
679
+ try:
680
+ with sync_playwright() as p:
681
+ browser = p.chromium.launch(headless=True)
682
+ page = browser.new_page(viewport={"width": 1440, "height": 900})
683
+ page.goto(root_url, wait_until="domcontentloaded")
684
+ page.wait_for_function(
685
+ "() => !!window.__robommeLiveObsResizerInstalled",
686
+ timeout=5000,
687
+ )
688
+
689
+ page.click("#show_btn")
690
+ page.wait_for_selector("#live_obs img", timeout=10000)
691
+ page.wait_for_function(
692
+ """() => {
693
+ const container = document.querySelector('#live_obs .image-container');
694
+ const img = document.querySelector('#live_obs img');
695
+ if (!container || !img) return false;
696
+ const containerRect = container.getBoundingClientRect();
697
+ const imgRect = img.getBoundingClientRect();
698
+ return imgRect.width > 300 && Math.abs(containerRect.width - imgRect.width) <= 2;
699
+ }""",
700
+ timeout=10000,
701
+ )
702
+
703
+ geometry = _read_live_obs_geometry(page)
704
+ assert geometry["container"] is not None
705
+ assert geometry["img"] is not None
706
+ assert geometry["img"]["width"] > 300
707
+ assert abs(geometry["container"]["width"] - geometry["img"]["width"]) <= 2
708
+ object_fit = page.evaluate(
709
+ """() => getComputedStyle(document.querySelector('#live_obs img')).objectFit"""
710
+ )
711
+ assert object_fit == "contain"
712
+
713
+ page.locator("#live_obs img").screenshot(path=str(screenshot_path))
714
+
715
+ browser.close()
716
+ finally:
717
+ server.should_exit = True
718
+ thread.join(timeout=10)
719
+ demo.close()
720
+
721
+ screenshot = Image.open(screenshot_path).convert("RGB")
722
+ width, height = screenshot.size
723
+ samples = [
724
+ screenshot.getpixel((width // 2, height // 2)),
725
+ screenshot.getpixel((max(1, width // 10), height // 2)),
726
+ screenshot.getpixel((min(width - 2, (width * 9) // 10), height // 2)),
727
+ ]
728
+ for pixel in samples:
729
+ assert pixel[0] > 200
730
+ assert pixel[1] < 30
731
+ assert pixel[2] < 30
732
+
733
+
734
  def test_header_task_shows_env_after_init(monkeypatch):
735
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
736
 
gradio-web/ui_layout.py CHANGED
@@ -40,10 +40,159 @@ PHASE_ACTION_KEYPOINT = "action_keypoint"
40
  PHASE_EXECUTION_PLAYBACK = "execution_playback"
41
 
42
 
43
- # Deprecated: no runtime JS logic in native Gradio mode.
44
  SYNC_JS = ""
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  CSS = f"""
48
  .native-card {{
49
  }}
@@ -81,6 +230,14 @@ button#reference_action_btn:not(:disabled):hover,
81
  background: #19713d !important;
82
  border-color: #19713d !important;
83
  }}
 
 
 
 
 
 
 
 
84
  """
85
 
86
 
@@ -186,6 +343,7 @@ def create_ui_blocks():
186
  interactive=False,
187
  type="pil",
188
  elem_id="live_obs",
 
189
  show_label=True,
190
  buttons=[],
191
  sources=[],
@@ -514,6 +672,12 @@ def create_ui_blocks():
514
  show_progress="hidden",
515
  )
516
 
 
 
 
 
 
 
517
  demo.load(
518
  fn=init_app_with_phase,
519
  inputs=[],
 
40
  PHASE_EXECUTION_PLAYBACK = "execution_playback"
41
 
42
 
43
+ # Deprecated: no legacy runtime JS logic in native Gradio mode.
44
  SYNC_JS = ""
45
 
46
 
47
+ LIVE_OBS_CLIENT_RESIZE_JS = r"""
48
+ () => {
49
+ if (window.__robommeLiveObsResizerInstalled) {
50
+ if (typeof window.__robommeLiveObsSchedule === "function") {
51
+ window.__robommeLiveObsSchedule();
52
+ }
53
+ return;
54
+ }
55
+
56
+ const state = {
57
+ rafId: null,
58
+ intervalId: null,
59
+ lastAppliedWidth: null,
60
+ lastWrapperNode: null,
61
+ lastFrameNode: null,
62
+ lastImageNode: null,
63
+ rootObserver: null,
64
+ layoutObserver: null,
65
+ phaseObserver: null,
66
+ bodyObserver: null,
67
+ };
68
+
69
+ const getTargets = () => {
70
+ const root = document.getElementById("live_obs");
71
+ if (!root) {
72
+ return null;
73
+ }
74
+ return {
75
+ root,
76
+ container: root.querySelector(".image-container"),
77
+ frame: root.querySelector(".image-frame"),
78
+ image: root.querySelector("img"),
79
+ mediaCard: document.getElementById("media_card"),
80
+ actionPhaseGroup: document.getElementById("action_phase_group"),
81
+ };
82
+ };
83
+
84
+ const applyResize = () => {
85
+ state.rafId = null;
86
+ const targets = getTargets();
87
+ const wrapper = targets?.root?.querySelector(".upload-container") || targets?.frame?.parentElement;
88
+ if (!targets || !targets.container || !wrapper || !targets.frame || !targets.image) {
89
+ return;
90
+ }
91
+
92
+ const containerWidth = Math.floor(targets.container.getBoundingClientRect().width);
93
+ if (!Number.isFinite(containerWidth) || containerWidth < 2) {
94
+ return;
95
+ }
96
+
97
+ if (
98
+ state.lastAppliedWidth === containerWidth &&
99
+ state.lastWrapperNode === wrapper &&
100
+ state.lastFrameNode === targets.frame &&
101
+ state.lastImageNode === targets.image
102
+ ) {
103
+ return;
104
+ }
105
+
106
+ wrapper.style.width = `${containerWidth}px`;
107
+ wrapper.style.maxWidth = "none";
108
+ wrapper.style.display = "block";
109
+
110
+ targets.frame.style.width = `${containerWidth}px`;
111
+ targets.frame.style.maxWidth = "none";
112
+ targets.frame.style.display = "block";
113
+
114
+ targets.image.style.width = `${containerWidth}px`;
115
+ targets.image.style.maxWidth = "none";
116
+ targets.image.style.height = "auto";
117
+ targets.image.style.display = "block";
118
+ targets.image.style.objectFit = "contain";
119
+ targets.image.style.objectPosition = "center center";
120
+
121
+ state.lastAppliedWidth = containerWidth;
122
+ state.lastWrapperNode = wrapper;
123
+ state.lastFrameNode = targets.frame;
124
+ state.lastImageNode = targets.image;
125
+ };
126
+
127
+ const scheduleResize = () => {
128
+ if (state.rafId !== null) {
129
+ return;
130
+ }
131
+ state.rafId = window.requestAnimationFrame(applyResize);
132
+ };
133
+
134
+ const observeLiveObs = () => {
135
+ const targets = getTargets();
136
+ if (!targets) {
137
+ return false;
138
+ }
139
+
140
+ state.rootObserver?.disconnect();
141
+ state.rootObserver = new MutationObserver(scheduleResize);
142
+ state.rootObserver.observe(targets.root, {
143
+ childList: true,
144
+ subtree: true,
145
+ attributes: true,
146
+ });
147
+
148
+ state.layoutObserver?.disconnect();
149
+ if (window.ResizeObserver) {
150
+ state.layoutObserver = new ResizeObserver(scheduleResize);
151
+ [targets.root, targets.container, targets.mediaCard, targets.actionPhaseGroup]
152
+ .filter(Boolean)
153
+ .forEach((node) => state.layoutObserver.observe(node));
154
+ }
155
+
156
+ state.phaseObserver?.disconnect();
157
+ state.phaseObserver = new MutationObserver(scheduleResize);
158
+ [targets.root, targets.actionPhaseGroup, targets.root.parentElement, targets.root.parentElement?.parentElement]
159
+ .filter(Boolean)
160
+ .forEach((node) =>
161
+ state.phaseObserver.observe(node, {
162
+ attributes: true,
163
+ attributeFilter: ["class", "style", "hidden"],
164
+ })
165
+ );
166
+
167
+ scheduleResize();
168
+ return true;
169
+ };
170
+
171
+ window.__robommeLiveObsSchedule = scheduleResize;
172
+ window.addEventListener("resize", scheduleResize, { passive: true });
173
+ document.addEventListener("visibilitychange", scheduleResize);
174
+
175
+ if (!observeLiveObs()) {
176
+ state.bodyObserver = new MutationObserver(() => {
177
+ if (observeLiveObs()) {
178
+ state.bodyObserver?.disconnect();
179
+ state.bodyObserver = null;
180
+ }
181
+ scheduleResize();
182
+ });
183
+ state.bodyObserver.observe(document.body, {
184
+ childList: true,
185
+ subtree: true,
186
+ });
187
+ }
188
+
189
+ state.intervalId = window.setInterval(scheduleResize, 250);
190
+
191
+ window.__robommeLiveObsResizerInstalled = true;
192
+ }
193
+ """
194
+
195
+
196
  CSS = f"""
197
  .native-card {{
198
  }}
 
230
  background: #19713d !important;
231
  border-color: #19713d !important;
232
  }}
233
+
234
+ #live_obs.live-obs-resizable .image-container {{
235
+ width: 100%;
236
+ }}
237
+
238
+ #live_obs.live-obs-resizable .upload-container {{
239
+ width: 100%;
240
+ }}
241
  """
242
 
243
 
 
343
  interactive=False,
344
  type="pil",
345
  elem_id="live_obs",
346
+ elem_classes=["live-obs-resizable"],
347
  show_label=True,
348
  buttons=[],
349
  sources=[],
 
672
  show_progress="hidden",
673
  )
674
 
675
+ demo.load(
676
+ fn=None,
677
+ js=LIVE_OBS_CLIENT_RESIZE_JS,
678
+ queue=False,
679
+ )
680
+
681
  demo.load(
682
  fn=init_app_with_phase,
683
  inputs=[],