Spaces:
Running on Zero
Running on Zero
File size: 6,668 Bytes
b92cb36 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # MUST come before torch / any CUDA-touching import
import torch
import gradio as gr
from transformers import AutoModelForMultimodalLM, AutoProcessor
from PIL import Image, ImageDraw
import json
import re
MODEL_ID = "JingyuanHuang/GUI-RD-9B"
processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForMultimodalLM.from_pretrained(
MODEL_ID,
dtype=torch.bfloat16,
trust_remote_code=True,
).to("cuda").eval()
SYSTEM_PROMPT = (
'You may call one or more functions to assist with the user query.\n\n'
'You are provided with function signatures within <tools></tools> XML tags:\n'
'<tools>\n'
'{"name":"computer_use","description":"Use a mouse to interact with a computer.",'
'"notes":"Click with the cursor tip centered on targets;avoid edges unless asked.'
'Do not use other tools(type,key,scroll,left_click_drag).Only left_click are allowed.",'
'"parameters":{"type":"object","required":["action"],"properties":{'
'"action":{"type":"string","enum":["left_click"],"description":"The action to perform."},'
'"coordinate":{"type":"array","description":"(x,y):pixels from left/top.Required for action=left_click."}'
'}}}\n'
'</tools>\n\n'
'For each function call, return a JSON object with function name and arguments within <toolcall></toolcall> XML tags:\n'
'<toolcall>\n{"name":"<function-name>","arguments":<args-json-object>}\n</toolcall>'
)
def parse_coordinate(response_text: str):
"""Extract (x, y) pixel coordinates from the model's tool-call response."""
# Try parsing as JSON tool call first
# Look for JSON with "coordinate" key
match = re.search(r'\{[^{}]*"coordinate"\s*:\s*\[(\d+)\s*,\s*(\d+)\][^{}]*\}', response_text)
if match:
x = int(match.group(1))
y = int(match.group(2))
return x, y
# Fallback: look for any [x, y] pattern
match = re.search(r'\[(\d+)\s*,\s*(\d+)\]', response_text)
if match:
x = int(match.group(1))
y = int(match.group(2))
return x, y
return None
def draw_marker(image: Image.Image, x: int, y: int) -> Image.Image:
"""Draw a crosshair marker and circle at the predicted coordinate."""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
radius = max(10, min(image.width, image.height) // 50)
# Draw a circle
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline="red",
width=3,
)
# Draw crosshair lines
line_len = radius + 8
draw.line([(x - line_len, y), (x + line_len, y)], fill="red", width=2)
draw.line([(x, y - line_len), (x, y + line_len)], fill="red", width=2)
return annotated
@spaces.GPU(duration=120)
def predict(image: Image.Image, instruction: str):
"""Predict the click coordinate for a target element on a GUI screenshot.
Args:
image: A GUI screenshot (PNG/JPG).
instruction: Natural-language description of the target element to click.
Returns:
A tuple of (annotated image, coordinate string, raw model output).
"""
if image is None:
return None, "Please upload an image.", ""
if not instruction.strip():
return None, "Please enter an instruction.", ""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": instruction},
],
},
]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = processor(
text=[text],
images=[image],
padding=True,
return_tensors="pt",
).to("cuda")
with torch.no_grad():
generated_ids = model.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
)
generated_ids_trimmed = [
out_ids[len(in_ids):]
for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed,
skip_special_tokens=True,
clean_up_tokenization_spaces=False,
)[0]
coord = parse_coordinate(output_text)
if coord is not None:
x, y = coord
# Clamp to image bounds
x = max(0, min(x, image.width - 1))
y = max(0, min(y, image.height - 1))
annotated = draw_marker(image, x, y)
coord_str = f"({x}, {y})"
return annotated, coord_str, output_text
else:
return image, "Could not parse coordinates from model output.", output_text
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
gr.Markdown(
"# 🎯 GUI-RD-9B: GUI Grounding\n"
"Upload a GUI screenshot and describe the element you want to click. "
"The model predicts the pixel coordinate and marks it on the image.\n\n"
"Based on [Trust the Right Teacher: Quality-Aware Self-Distillation for GUI Grounding](https://arxiv.org/abs/2606.18101)"
)
with gr.Row(elem_id="col-container"):
with gr.Column(scale=1):
input_img = gr.Image(label="GUI Screenshot", type="pil")
instruction = gr.Textbox(
label="Instruction",
placeholder="e.g. click the search box",
lines=2,
)
run_btn = gr.Button("Predict Coordinate", variant="primary")
with gr.Column(scale=1):
output_img = gr.Image(label="Predicted Click Location")
coord_text = gr.Textbox(label="Predicted Coordinate (x, y)", interactive=False)
with gr.Accordion("Raw Model Output", open=False):
raw_output = gr.Textbox(label="Model Response", lines=4, interactive=False)
gr.Examples(
examples=[
["assets/web_6f93090a-81f6-489e-bb35-1a2838b18c01.png", "select search textfield"],
["assets/web_6f93090a-81f6-489e-bb35-1a2838b18c01.png", "switch to discussions"],
],
inputs=[input_img, instruction],
outputs=[output_img, coord_text, raw_output],
fn=predict,
cache_examples=True,
cache_mode="lazy",
)
run_btn.click(
fn=predict,
inputs=[input_img, instruction],
outputs=[output_img, coord_text, raw_output],
)
if __name__ == "__main__":
demo.launch(mcp_server=True)
|