| Model | Size | Open Source | ScreenSpot-V2 | ScreenSpotPro | OSWORLD-G | OSWORLD-G-Refined |
|---|---|---|---|---|---|---|
| OpenAI CUA | — | ❌ | 87.9 | 23.4 | — | — |
| Claude 3.7 | — | ❌ | 87.6 | 27.7 | — | — |
| JEDI-7B | 7B | ✅ | 91.7 | 39.5 | 54.1 | — |
| SE-GUI | 7B | ✅ | 90.3 | 47.0 | — | — |
| UI-TARS | 7B | ✅ | 91.6 | 35.7 | 47.5 | — |
| UI-TARS-1.5* | 7B | ✅ | 89.7* | 42.0* | 52.8* | 64.2* |
| UGround-v1-7B | 7B | ✅ | — | 31.1 | — | 36.4 |
| Qwen2.5-VL-32B-Instruct | 32B | ✅ | 91.9* | 48.0 | 46.5 | 59.6* |
| UGround-v1-72B | 72B | ✅ | — | 34.5 | — | — |
| Qwen2.5-VL-72B-Instruct | 72B | ✅ | 94.00* | 53.3 | — | 62.2* |
| UI-TARS | 72B | ✅ | 90.3 | 38.1 | — | — |
| OpenCUA | 7B | ✅ | 92.3 | 50.0 | 55.3 | 68.3* |
| OpenCUA | 32B | ✅ | 93.4 | 55.3 | 59.6 | 70.2* |
| GTA1-2507 (Ours) | 7B | ✅ | 92.4 (∆ +2.7) | 50.1(∆ +8.1) | 55.1 (∆ +2.3) | 67.7 (∆ +3.5) |
| GTA1 (Ours) | 7B | ✅ | 93.4 (∆ +0.1) | 55.5(∆ +5.5) | 60.1(∆ +4.8) | 68.8(∆ +0.5) |
| GTA1 (Ours) | 32B | ✅ | 95.2 (∆ +1.8) | 63.6(∆ +8.3) | 65.2 (∆ +5.6) | 72.2(∆ +2.0) |
Note:
- Model size is indicated in billions (B) of parameters.
- A dash (—) denotes results that are currently unavailable.
- A superscript asterisk (﹡) denotes our evaluated result.
- UI-TARS-1.5 7B, OpenCUA-7B, and OpenCUA-32B are applied as our baseline models.
- ∆ indicates the performance improvement (∆) of our model compared to its baseline.
| Agent Model | Step | OSWorld | OSWorld-Verified |
|---|---|---|---|
| Proprietary Models | |||
| Claude 3.7 Sonnet | 100 | 28.0 | — |
| OpenAI CUA 4o | 200 | 38.1 | — |
| UI-TARS-1.5 | 100 | 42.5 | 41.8 |
| OpenAI CUA o3 | 200 | 42.9 | — |
| Open-Source Models | |||
| Aria-UI w/ GPT-4o | 15 | 15.2 | — |
| Aguvis-72B w/ GPT-4o | 15 | 17.0 | — |
| UI-TARS-72B-SFT | 50 | 18.8 | — |
| Agent S w/ Claude-3.5-Sonnet | 15 | 20.5 | — |
| Agent S w/ GPT-4o | 15 | 20.6 | — |
| UI-TARS-72B-DPO | 15 | 22.7 | — |
| UI-TARS-72B-DPO | 50 | 24.6 | — |
| UI-TARS-1.5-7B | 100 | 26.9 | 27.4 |
| Jedi-7B w/ o3 | 100 | — | 51.0 |
| Jedi-7B w/ GPT-4o | 100 | 27.0 | — |
| Agent S2 w/ Claude-3.7-Sonnet | 50 | 34.5 | — |
| Agent S2 w/ Gemini-2.5-Pro | 50 | 41.4 | 45.8 |
| Agent S2.5 w/ o3 | 100 | — | 56.0 |
| Agent S2.5 w/ GPT-5 | 100 | — | 58.4 |
| CoAct-1 w/o3 & o4mini & OpenAI CUA 4o | 150 | — | 60.8 |
| GTA1-7B-2507 w/ o3 | 100 | 45.2 | 53.1 |
| GTA1-7B-2507 w/ GPT-5 | 100 | — | 61.0 |
| GTA1-32B w/ o3 | 100 | — | 55.4 |
| GTA1-32B w/ GPT-5 | 100 | — | 63.4 |
Note: A dash (—) indicates unavailable results.
| Agent Model | Step | Success Rate |
|---|---|---|
| Kimi-VL | 15 | 10.4 |
| WAA | — | 19.5 |
| Jedi w/ GPT-4o | 100 | 33.7 |
| GTA1-7B-2507 w/ o3 | 100 | 47.9 |
| GTA1-7B-2507 w/ GPT-5 | 100 | 49.2 |
| GTA1-32B w/ o3 | 100 | 51.2 |
| GTA1-32B w/ GPT-5 | 100 | 50.6 |
Note: A dash (—) indicates unavailable results.
1from transformers import AutoTokenizer, AutoImageProcessor
2from transformers.models.qwen2_vl.image_processing_qwen2_vl_fast import smart_resize
3from PIL import Image
4from io import BytesIO
5import base64
6import re
7from vllm import LLM, SamplingParams
8
9instruction="click start"
10image_path="example.png"
11
12CLICK_REGEXES = [
13 # pyautogui.click(x=123, y=456)
14 re.compile(r"click\s*\(\s*x\s*=\s*(\d+)\s*,\s*y\s*=\s*(\d+)\s*\)", re.IGNORECASE),
15 # pyautogui.click(123, 456) or click(123,456)
16 re.compile(r"click\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)", re.IGNORECASE),
17 ]
18
19def format_message(image_path,instruction):
20 SYSTEM_PROMPT = (
21 "You are a GUI agent. You are given a task and a screenshot of the screen. "
22 "You need to perform a series of pyautogui actions to complete the task."
23 )
24 messages = [
25 {"role": "system", "content": SYSTEM_PROMPT},
26 {"role": "user", "content": [
27 {"type": "image", "image": image_path},
28 {"type": "text", "text": instruction},
29 ]},
30 ]
31 text = prompt_tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
32
33 text2, n = re.subn(
34 r"<\|media_begin\|>.*?<\|media_end\|>",
35 "<|vision_start|><|image_pad|><|vision_end|>",
36 text,
37 flags=re.S
38 )
39 if n == 0:
40 raise RuntimeError("Cannot find <|media_begin|>...<|media_end|> token.")
41 return text2
42
43def parse_xy_from_text(text: str):
44 if "click" not in text.lower():
45 return [-1, -1]
46 for rx in CLICK_REGEXES:
47 m = rx.search(text)
48 if m:
49 try:
50 return int(m.group(1)), int(m.group(2))
51 except Exception:
52 continue
53 return [-1,-1]
54
55def convert_pil_image_to_base64(image):
56 buffered = BytesIO()
57 image.save(buffered, format="PNG")
58 return base64.b64encode(buffered.getvalue()).decode()
59
60llm = LLM(
61 model="Salesforce/GTA1-7B",
62 tokenizer="Salesforce/GTA1-7B",
63 tokenizer_mode="slow",
64 trust_remote_code=True,
65 dtype="bfloat16",
66 limit_mm_per_prompt={"image": 1},
67 tensor_parallel_size=1,
68 )
69prompt_tok = AutoTokenizer.from_pretrained("Salesforce/GTA1-7B", trust_remote_code=True)
70sp = SamplingParams(max_tokens=512, temperature=0.0)
71tokenizer = llm.get_tokenizer()
72processor=AutoImageProcessor.from_pretrained("Salesforce/GTA1-7B", trust_remote_code=True)
73
74image = Image.open(image_path).convert('RGB')
75resized_height, resized_width = smart_resize(
76 image.height,
77 image.width,
78 factor=processor.patch_size * processor.merge_size,
79 min_pixels=processor.min_pixels,
80 max_pixels=processor.max_pixels,
81 )
82resized_image = image.resize((resized_width, resized_height))
83messages = format_message(image_path, instruction)
84response = llm.generate(
85 [{"prompt": messages, "multi_modal_data": {"image": [resized_image]}}],
86 sampling_params=sp
87 )[0].outputs[0].text
88
89
90coordinates = parse_xy_from_text(response)
91print(coordinates[0]/resized_width*image.width, coordinates[1]/resized_height*image.height)1import torch
2import os
3# -------------------------
4# System / Torch defaults
5# -------------------------
6os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") # avoid CPU oversubscription
7os.environ.setdefault("VLLM_USE_V1", "1")
8os.environ.setdefault("VLLM_ENGINE_IN_BACKGROUND_THREAD", "0")
9import base64
10import re
11from typing import Dict, List, Union
12from PIL import Image
13from io import BytesIO
14import traceback
15import argparse
16import asyncio
17import requests
18import ray
19from ray import serve
20from fastapi import FastAPI
21from transformers import AutoTokenizer
22from vllm import LLM, SamplingParams
23import uuid
24
25
26N_REPLICAS = 2
27
28try:
29 torch.backends.cuda.matmul.allow_tf32 = True
30 torch.backends.cudnn.benchmark = True
31except Exception:
32 pass
33
34
35# -------------------------
36# IO helpers
37# -------------------------
38
39def pil_to_base64(img: Image.Image, format: str = "PNG") -> str:
40 buffer = BytesIO()
41 img.save(buffer, format=format)
42 img_bytes = buffer.getvalue()
43 img_b64 = base64.b64encode(img_bytes).decode("utf-8")
44 return img_b64
45
46
47def data_uri_to_pil(data_uri: str) -> Image.Image:
48 header, b64_str = data_uri.split(",", 1)
49 img_data = base64.b64decode(b64_str)
50 buffer = BytesIO(img_data)
51 img = Image.open(buffer)
52 return img
53
54
55def extract_images(messages: List[Dict]) -> List[Image.Image]:
56 images = []
57 for msg in messages:
58 if msg.get("role") == "user":
59 for content in msg.get("content", []):
60 if content.get("type") in ["image", "image_url"]:
61 if content["type"] == "image":
62 images.append(data_uri_to_pil(content["image"]).convert("RGB"))
63 else:
64 images.append(data_uri_to_pil(content["image_url"]["url"]).convert("RGB"))
65 return images
66
67
68# -------------------------
69# Prompt builder
70# -------------------------
71
72def build_prompt_with_template(tokenizer: AutoTokenizer, messages: List[Dict]) -> str:
73 text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
74 text2, n = re.subn(
75 r"<\|media_begin\|>.*?<\|media_end\|>",
76 "<|vision_start|><|image_pad|><|vision_end|>",
77 text,
78 flags=re.S,
79 )
80 if n == 0:
81 raise RuntimeError("Did not find <|media_begin|>...<|media_end|> block in template.")
82 return text2
83
84# -------------------------
85# Deployment
86# -------------------------
87
88def build_app(model_path: str, num_replicas: int, port: int):
89 api = FastAPI(title="GTA1-7B Multi-GPU Service (High-throughput)")
90
91 @serve.deployment(
92 num_replicas=num_replicas,
93 ray_actor_options={"num_gpus": 1, "num_cpus": 4},
94 max_ongoing_requests=16,
95 )
96 class GTA1Model:
97 def __init__(self, model_path: str):
98 gpu_ids = ray.get_gpu_ids()
99 self.gpu_id = gpu_ids[0] if gpu_ids else 0
100 print(f"🔍 Ray assigned GPU IDs: {gpu_ids}")
101 # Initialize vLLM within this replica (Ray sets CUDA_VISIBLE_DEVICES)
102 print(f"🔄 Initializing vLLM on GPU {self.gpu_id}[ray id] from {model_path}")
103 if not torch.cuda.is_available():
104 raise RuntimeError("CUDA is not available")
105
106 self.llm = LLM(
107 model=model_path,
108 tokenizer=model_path,
109 tokenizer_mode="slow",
110 trust_remote_code=True,
111 dtype="bfloat16",
112 limit_mm_per_prompt={"image": 1},
113 max_model_len=32768,
114 tensor_parallel_size=1,
115 )
116 self.vllm_tokenizer = self.llm.get_tokenizer()
117 self.hf_tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
118 self.model_path = model_path
119 self.dtype = "bfloat16"
120 print(f"✅ vLLM initialized successfully (Ray GPU Id: {self.gpu_id})")
121
122 # ------------ batching core ------------
123 @serve.batch(max_batch_size=8, batch_wait_timeout_s=0.1) # increase if GPU allows
124 async def _generate_batch(self, payload: Union[Dict, List[Dict]]):
125 """Build prompts, enforce single image, and call vLLM.generate."""
126 if isinstance(payload, dict):
127 list_of_payloads = [payload]
128 else:
129 list_of_payloads = payload
130 request_id = uuid.uuid4().hex[:8]
131 # --- Build per-sample prompt/image ---
132 prompts: List[str] = []
133 images_per_req: List[Image.Image] = []
134 error_results = []
135 early_exit = False
136 for p in list_of_payloads:
137 try:
138 messages = p["messages"]
139 imgs = extract_images(messages)
140 if len(imgs) != 1:
141 raise RuntimeError(f"Exactly one image is required, got {len(imgs)}")
142 prompt_text = build_prompt_with_template(self.hf_tokenizer, messages)
143 # Sanity check on tokens: 1 <|image_pad|>, no <|media_placeholder|>
144 tok = self.vllm_tokenizer
145 id_imgpad = tok.encode("<|image_pad|>", add_special_tokens=False)[0]
146 id_media = tok.encode("<|media_placeholder|>", add_special_tokens=False)[0]
147 ids = tok.encode(prompt_text, add_special_tokens=False)
148 if sum(i == id_imgpad for i in ids) != 1 or any(i == id_media for i in ids):
149 raise RuntimeError("Prompt media tokens invalid after conversion")
150 prompts.append(prompt_text)
151 images_per_req.append(imgs[0])
152 except Exception as e:
153 early_exit = True
154 trace = traceback.format_exc()
155 error_results.append(
156 {
157 "response": "",
158 "error": {
159 "message": str(e),
160 "trace": trace,
161 'type_of_payload': str(type(payload)),
162 'type_of_list_of_payloads': str(type(list_of_payloads)),
163 'type_of_p': str(type(p)),
164 'p_keys': str(p.keys()) if isinstance(p, dict) else str(p),
165 },
166 "usage": {},
167 "gpu_id": self.gpu_id
168 }
169 )
170 if early_exit:
171 return error_results
172 # --- vLLM generation ---
173 args_base = list_of_payloads[0]
174 sp = SamplingParams(
175 max_tokens=args_base.get("max_new_tokens", 512),
176 temperature=args_base.get("temperature", 0.0),
177 top_p=args_base.get("top_p", 0.9),
178 )
179
180 requests_list = [
181 {"prompt": pr, "multi_modal_data": {"image": [im]}}
182 for pr, im in zip(prompts, images_per_req)
183 ]
184
185 outs = self.llm.generate(requests_list, sampling_params=sp)
186
187 tok = self.vllm_tokenizer
188 results: List[Dict] = []
189 for pr, o in zip(prompts, outs):
190 text = o.outputs[0].text if o.outputs else ""
191 gen_tokens = len(o.outputs[0].token_ids) if (o.outputs and hasattr(o.outputs[0], 'token_ids')) else None
192 prompt_tokens = len(tok.encode(pr, add_special_tokens=False))
193 usage = {
194 "prompt_tokens": prompt_tokens,
195 "generated_tokens": gen_tokens if gen_tokens is not None else None,
196 "total_tokens": (prompt_tokens + gen_tokens) if gen_tokens is not None else None,
197 }
198 results.append({
199 "response": text,
200 "error": "",
201 "usage": usage,
202 "gpu_id": self.gpu_id,
203 'bs_size_in_this_request': f"{request_id}:{len(list_of_payloads)}"
204 })
205
206 return results
207
208 # Exposed single-call entry that joins the batch
209 async def call_llm(self, payload: Dict):
210 try:
211 res = await self._generate_batch(payload)
212 return res
213 except Exception as e:
214 trace = traceback.format_exc()
215 return {"response": "", "error": {"message": str(e), "trace": trace}, "usage": {}, "gpu_id": self.gpu_id}
216
217 def health(self):
218 return {
219 "status": "ok",
220 "gpu_id": self.gpu_id,
221 "dtype": self.dtype,
222 "model_path": self.model_path,
223 }
224
225 model = GTA1Model.bind(model_path)
226
227 @serve.deployment(max_ongoing_requests=96)
228 @serve.ingress(api)
229 class GTA1App:
230 def __init__(self, model_handle):
231 self.model_deployment = model_handle
232
233 @api.get("/health")
234 async def health_all(self):
235 # Calling the same Serve handle N times does not guarantee each call hits a different replica
236 attempts = max(8, N_REPLICAS * 4) # oversample
237 calls = [self.model_deployment.health.remote() for i in range(attempts)]
238 replies = await asyncio.gather(*calls)
239 # dedupe by replica_id (or by tuple(gpu_id))
240 seen = {}
241 for r in replies:
242 seen[r.get("gpu_id", f"unknown-{len(seen)}")] = r
243 if len(seen) >= N_REPLICAS:
244 break
245 return {"replicas": list(seen.values())}
246
247 @api.post("/call_llm")
248 async def call_llm(self, req: Dict):
249 return await self.model_deployment.call_llm.remote(req)
250
251 return GTA1App.bind(model)
252
253
254# -------------------------
255# Main
256# -------------------------
257if __name__ == "__main__":
258 parser = argparse.ArgumentParser()
259 parser.add_argument("--model_path", type=str, default="Salesforce/GTA1-7B")
260 parser.add_argument("--host", type=str, default="0.0.0.0")
261 parser.add_argument("--port", type=int, default=3005)
262 parser.add_argument("--num_replicas", type=int, default=2)
263 args = parser.parse_args()
264 N_REPLICAS = args.num_replicas
265 ray.init(ignore_reinit_error=True)
266
267 print(f"🚀 Starting GTA1-7B service on {args.host}:{args.port}")
268 serve.start(detached=True, http_options={"host": args.host, "port": args.port})
269
270 app = build_app(args.model_path, args.num_replicas, args.port)
271 serve.run(app, name="GTA1-7B", route_prefix="/")
272
273 # Quick health sample
274 try:
275 r = requests.get(f"http://0.0.0.0:{args.port}/health", timeout=5)
276 print(r.json())
277 except Exception as e:
278 print("Health probe failed:", e)
2791import argparse
2import base64
3import concurrent.futures
4import json
5import os
6import re
7from typing import Dict, List, Tuple
8from gui_agent.agent.gta1.format_message import encode_numpy_image_to_base64, encode_image_bytes, smart_resize
9
10import requests
11from PIL import Image, ImageDraw
12
13
14def image_file_to_data_uri(image_path: str) -> str:
15 if not os.path.exists(image_path):
16 raise FileNotFoundError(f"Image not found: {image_path}")
17 with open(image_path, "rb") as f:
18 b64 = base64.b64encode(f.read()).decode("utf-8")
19 # default to png; serverside only requires a data URI header then comma
20 return f"data:image/png;base64,{b64}"
21
22
23def build_messages(image_path: str, instruction: str, system_prompt: str) -> List[Dict]:
24 return [
25 {"role": "system", "content": system_prompt},
26 {
27 "role": "user",
28 "content": [
29 {"type": "image", "image": image_file_to_data_uri(image_path)},
30 {"type": "text", "text": instruction},
31 ],
32 },
33 ]
34
35
36def call_health(base_url: str, timeout: float = 10.0) -> Dict:
37 r = requests.get(f"{base_url}/health", timeout=timeout)
38 r.raise_for_status()
39 return r.json()
40
41
42def call_single(
43 base_url: str,
44 image_path: str,
45 instruction: str,
46 system_prompt: str,
47 max_new_tokens: int = 512,
48 temperature: float = 0.0,
49 top_p: float = 0.9,
50 timeout: float = 120.0,
51) -> List[Dict]:
52 payload = {
53 "messages": build_messages(image_path, instruction, system_prompt),
54 "max_new_tokens": max_new_tokens,
55 "temperature": temperature,
56 "top_p": top_p,
57 }
58 r = requests.post(f"{base_url}/call_llm", json=payload, timeout=timeout)
59 r.raise_for_status()
60 resp = r.json()
61 if isinstance(resp, dict):
62 return [resp]
63 return resp
64
65
66def call_many_concurrent(
67 base_url: str,
68 image_path: str,
69 instruction: str,
70 system_prompt: str,
71 num_requests: int,
72 concurrency: int,
73 max_new_tokens: int = 512,
74 temperature: float = 0.0,
75 top_p: float = 0.9,
76 timeout: float = 120.0,
77) -> List[List[Dict]]:
78 results: List[List[Dict]] = []
79
80 def _one(i: int) -> List[Dict]:
81 # Vary instruction slightly so you can trace requests
82 instr = f"{instruction} [req {i+1}/{num_requests}]"
83 return call_single(
84 base_url,
85 image_path,
86 instr,
87 system_prompt,
88 max_new_tokens,
89 temperature,
90 top_p,
91 timeout,
92 )
93
94 with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
95 futures = [pool.submit(_one, i) for i in range(num_requests)]
96 for fut in concurrent.futures.as_completed(futures):
97 results.append(fut.result())
98 return results
99
100
101def pretty_print_response(batch_results: List[Dict]) -> None:
102 if isinstance(batch_results, dict):
103 batch_results = [batch_results]
104 for idx, item in enumerate(batch_results):
105 if item.get("error"):
106 print(f"[#{idx}] ERROR: {json.dumps(item['error'], ensure_ascii=False)})")
107 else:
108 usage = item.get("usage", {})
109 print(f"[#{idx}] gpu={item.get('gpu_id')} tokens={usage} text=\n{item.get('response','').strip()}\n")
110
111CLICK_KWARGS_REGEX = re.compile(r"pyautogui\.click\(\s*x\s*=\s*(\d+)\s*,\s*y\s*=\s*(\d+)\s*\)")
112CLICK_POSARGS_REGEX = re.compile(r"pyautogui\.click\(\s*(\d+)\s*,\s*(\d+)\s*\)")
113
114def extract_clicks_from_text(text: str) -> List[Tuple[int, int]]:
115 clicks: List[Tuple[int, int]] = []
116 for x, y in CLICK_KWARGS_REGEX.findall(text or ""):
117 clicks.append((int(x), int(y)))
118 for x, y in CLICK_POSARGS_REGEX.findall(text or ""):
119 clicks.append((int(x), int(y)))
120 return clicks
121
122def extract_clicks_from_results(result_items: List[Dict]) -> List[Tuple[int, int]]:
123 clicks: List[Tuple[int, int]] = []
124 if isinstance(result_items, dict):
125 result_items = [result_items]
126 for item in result_items:
127 if item.get("error"):
128 continue
129 clicks.extend(extract_clicks_from_text(item.get("response", "")))
130 return clicks
131
132def compute_resized_dims_for_server_mapping(image_path: str) -> Tuple[int, int, int, int]:
133 with Image.open(image_path) as im:
134 width, height = im.size
135 resized_H, resized_W = smart_resize(
136 height,
137 width,
138 factor=28,
139 min_pixels=1000,
140 max_pixels=1000000000000,
141 )
142 return width, height, int(resized_W), int(resized_H)
143
144def map_clicks_to_original(clicks_resized: List[Tuple[int, int]],
145 original_w: int,
146 original_h: int,
147 resized_w: int,
148 resized_h: int) -> List[Tuple[int, int]]:
149 if resized_w == 0 or resized_h == 0:
150 return []
151 scale_x = original_w / float(resized_w)
152 scale_y = original_h / float(resized_h)
153 mapped: List[Tuple[int, int]] = []
154 for x, y in clicks_resized:
155 mapped_x = int(round(x * scale_x))
156 mapped_y = int(round(y * scale_y))
157 mapped.append((mapped_x, mapped_y))
158 return mapped
159
160def draw_circles_on_image(image_path: str,
161 points: List[Tuple[int, int]],
162 output_path: str,
163 radius: int = 8,
164 color: Tuple[int, int, int] = (255, 0, 0),
165 width: int = 3) -> None:
166 if not points:
167 return
168 with Image.open(image_path).convert("RGB") as img:
169 drawer = ImageDraw.Draw(img)
170 for (x, y) in points:
171 left = x - radius
172 top = y - radius
173 right = x + radius
174 bottom = y + radius
175 drawer.ellipse([(left, top), (right, bottom)], outline=color, fill=(0,255,0), width=width)
176 img.save(output_path)
177 print(f"Annotated image saved to: {output_path} (points drawn: {len(points)})")
178
179SYSTEM_PROMPT = (
180 "You are a GUI agent. You are given a task and a screenshot of the screen. "
181 "You need to perform a series of pyautogui actions to complete the task."
182)
183def main():
184 parser = argparse.ArgumentParser(description="Examples: single and batched inference against GTA1-7B Ray Serve.")
185 parser.add_argument("--host", type=str, default="http://localhost", help="Ray Serve host, e.g. http://localhost or http://IP")
186 parser.add_argument("--port", type=int, default=3005, help="Ray Serve port")
187 parser.add_argument("--image", type=str, required=False, default="example.jpg", help="Path to input image")
188 parser.add_argument("--instruction", type=str, default="click the icon in the bottom row, third from the left", help="User instruction")
189 parser.add_argument("--system", type=str, default=SYSTEM_PROMPT)
190 parser.add_argument("--mode", type=str, choices=["single", "batch", "health"], default="batch")
191 parser.add_argument("--num_requests", type=int, default=8, help="Number of requests in batch mode")
192 parser.add_argument("--concurrency", type=int, default=8, help="Max concurrent HTTP calls in batch mode")
193 parser.add_argument("--max_new_tokens", type=int, default=512)
194 parser.add_argument("--temperature", type=float, default=0.0)
195 parser.add_argument("--top_p", type=float, default=0.9)
196 parser.add_argument("--timeout", type=float, default=180.0)
197 args = parser.parse_args()
198
199 base_url = f"{args.host}:{args.port}"
200
201 if args.mode == "health":
202 info = call_health(base_url, timeout=10.0)
203 print(json.dumps(info, indent=2))
204 return
205
206 if args.mode == "single":
207 result_list = call_single(
208 base_url=base_url,
209 image_path=args.image,
210 instruction=args.instruction,
211 system_prompt=args.system,
212 max_new_tokens=args.max_new_tokens,
213 temperature=args.temperature,
214 top_p=args.top_p,
215 timeout=args.timeout,
216 )
217 print(result_list)
218 pretty_print_response(result_list)
219 clicks_resized = extract_clicks_from_results(result_list)
220 if clicks_resized:
221 orig_w, orig_h, resized_w, resized_h = compute_resized_dims_for_server_mapping(args.image)
222 mapped_clicks = map_clicks_to_original(clicks_resized, orig_w, orig_h, resized_w, resized_h)
223 out_path = f"ray_serve/annotated.png"
224 draw_circles_on_image(args.image, mapped_clicks, out_path)
225 return
226
227 if args.mode == "batch":
228 print(f"Submitting {args.num_requests} requests with concurrency={args.concurrency}...")
229 batch_outs = call_many_concurrent(
230 base_url=base_url,
231 image_path=args.image,
232 instruction=args.instruction,
233 system_prompt=args.system,
234 num_requests=args.num_requests,
235 concurrency=args.concurrency,
236 max_new_tokens=args.max_new_tokens,
237 temperature=args.temperature,
238 top_p=args.top_p,
239 timeout=args.timeout,
240 )
241 for i, one_result in enumerate(batch_outs):
242 print(f"===== Result for request {i+1} =====")
243 pretty_print_response(one_result)
244 all_clicks_resized: List[Tuple[int, int]] = []
245 for one_result in batch_outs:
246 all_clicks_resized.extend(extract_clicks_from_results(one_result))
247 if all_clicks_resized:
248 orig_w, orig_h, resized_w, resized_h = compute_resized_dims_for_server_mapping(args.image)
249 mapped_clicks = map_clicks_to_original(all_clicks_resized, orig_w, orig_h, resized_w, resized_h)
250 out_path = f"ray_serve/annotated.png"
251 draw_circles_on_image(args.image, mapped_clicks, out_path)
252 return
253
254
255if __name__ == "__main__":
256 main()1@article{yang2025gta1guitesttimescaling,
2 title={GTA1: GUI Test-time Scaling Agent},
3 author={Yan Yang and Dongxu Li and Yutong Dai and Yuhao Yang and Ziyang Luo and Zirui Zhao and Zhiyuan Hu and Junzhe Huang and Amrita Saha and Zeyuan Chen and Ran Xu and Liyuan Pan and Silvio Savarese and Caiming Xiong and Junnan Li},
4 year={2025},
5 eprint={2507.05791},
6 archivePrefix={arXiv},
7 primaryClass={cs.AI},
8 url={https://arxiv.org/abs/2507.05791},
9}