infinitylogesh commited on
Commit
0c5d1aa
·
verified ·
1 Parent(s): 85ad04b

GUI-Decisions Space

Browse files
Files changed (3) hide show
  1. intro.md +1 -1
  2. slotread/client.py +63 -4
  3. slotread/local.py +3 -17
intro.md CHANGED
@@ -8,7 +8,7 @@ The heatmap is that distribution.
8
  Gemma 4 31B with a LoRA ([GUI-Decisions-31B-LoRA](https://huggingface.co/infinitylogesh/GUI-Decisions-31B-LoRA),
9
  merged: [GUI-Decisions-31B](https://huggingface.co/infinitylogesh/GUI-Decisions-31B)).
10
  Blog post: [Stop Decoding Coordinates](https://logeshumapathi.com/blog/2026/10/06/slot-reading.html) ·
11
- code: [slot-reading](https://github.com/infinitylogesh/slot-reading).
12
 
13
  *Timings here are this Space's shared GPU running bf16 in plain transformers. Served on vLLM (NVFP4, one
14
  RTX PRO 6000) a step takes ~146 ms against ~470–550 ms for the same model writing its native JSON.*
 
8
  Gemma 4 31B with a LoRA ([GUI-Decisions-31B-LoRA](https://huggingface.co/infinitylogesh/GUI-Decisions-31B-LoRA),
9
  merged: [GUI-Decisions-31B](https://huggingface.co/infinitylogesh/GUI-Decisions-31B)).
10
  Blog post: [Stop Decoding Coordinates](https://logeshumapathi.com/blog/2026/10/06/slot-reading.html) ·
11
+ code: [gui-decisions](https://github.com/infinitylogesh/gui-decisions).
12
 
13
  *Timings here are this Space's shared GPU running bf16 in plain transformers. Served on vLLM (NVFP4, one
14
  RTX PRO 6000) a step takes ~146 ms against ~470–550 ms for the same model writing its native JSON.*
slotread/client.py CHANGED
@@ -61,12 +61,32 @@ def generate(model, url, text, max_tokens=64, think=False, system=None, **extra)
61
  return d["choices"][0]["message"].get("content") or "", ms, d.get("usage", {})
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  class SlotModel:
65
- """A served slot adapter: `name` as vLLM serves it, `path` the run folder holding config.json."""
 
 
66
 
67
- def __init__(self, name, path):
68
- self.name, self.path = name, path
69
- c = json.load(open(os.path.join(path, "config.json")))
70
  self.cfg = c
71
  self.arm = Arm(c["arm"], c["coarse"], c["bins"], c["tokens"], c["fine"])
72
  self.tasks = {"agent": AgentTask(), "box": BoxTask(), "seg": SegTask(c["rays"])}
@@ -178,6 +198,45 @@ class SlotModel:
178
  return out
179
 
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  _PROC = {}
182
 
183
 
 
61
  return d["choices"][0]["message"].get("content") or "", ms, d.get("usage", {})
62
 
63
 
64
+ CONFIG_NAME = "slotread_config.json"
65
+
66
+
67
+ def load_config(path):
68
+ """A model's slot config: a run folder (config.json, as slotread.train writes it), a folder with
69
+ slotread_config.json (the released models), or a Hugging Face repo id holding slotread_config.json."""
70
+ if os.path.isdir(path):
71
+ for name in (CONFIG_NAME, "config.json"):
72
+ p = os.path.join(path, name)
73
+ if os.path.exists(p):
74
+ c = json.load(open(p))
75
+ if "tasks" in c:
76
+ return c
77
+ raise FileNotFoundError(f"no {CONFIG_NAME} in {path}")
78
+ from huggingface_hub import hf_hub_download
79
+ return json.load(open(hf_hub_download(path, CONFIG_NAME)))
80
+
81
+
82
  class SlotModel:
83
+ """A served slot model: `name` as vLLM serves it (the adapter's --lora-modules name, or the merged
84
+ model's --served-model-name), `path` its slot config (run folder, released model folder or HF repo id).
85
+ `text_model`: the served base model that writes text steps (what to type, the answer)."""
86
 
87
+ def __init__(self, name, path, text_model=None):
88
+ self.name, self.path, self.text_model = name, path, text_model
89
+ c = load_config(path)
90
  self.cfg = c
91
  self.arm = Arm(c["arm"], c["coarse"], c["bins"], c["tokens"], c["fine"])
92
  self.tasks = {"agent": AgentTask(), "box": BoxTask(), "seg": SegTask(c["rays"])}
 
198
  return out
199
 
200
 
201
+ def image_url(self, image):
202
+ """A PIL image as the data URL sent to the server: JPEG at the model's input size by default (the
203
+ fast path; IMG_FMT / IMG_MAX_PX), capped as in training when the config says so (GUI-Owl: 1 MP).
204
+ A string is taken as a URL already."""
205
+ if isinstance(image, str):
206
+ return image
207
+ from .images import cap_image, data_url
208
+ cap = self.cfg.get("image_cap")
209
+ return data_url(cap_image(image.convert("RGB"), cap), max_px=cap) if cap else data_url(image)
210
+
211
+ def decide(self, image, instruction, previous="None"):
212
+ """A computer-use step: action and point; the key, scroll or swipe end when the action needs them.
213
+ Reads action, x and y first, then the action's other slots (lazy)."""
214
+ from .data.aguvis import KEYS, SDIR
215
+ t = time.perf_counter()
216
+ lp = self.read("agent", self.image_url(image), agent_user(instruction, previous), lazy=True)
217
+ d = self.tasks["agent"].decode(lp, self.arm)
218
+ if "key" in lp:
219
+ d["key"] = KEYS[int(lp["key"].argmax())]
220
+ if "sdir" in lp:
221
+ d.update(sdir=SDIR[int(lp["sdir"].argmax())], samt=int(lp["samt"].argmax()))
222
+ return dict(d, ms=round((time.perf_counter() - t) * 1000, 1))
223
+
224
+ def box(self, image, description):
225
+ """An element's or object's box [x1, y1, x2, y2] in [0, 1]."""
226
+ t = time.perf_counter()
227
+ lp = self.read("box", self.image_url(image), self.tasks["box"].user({"instruction": description}))
228
+ return dict(box=self.tasks["box"].decode(lp, self.arm), ms=round((time.perf_counter() - t) * 1000, 1))
229
+
230
+ def write_text(self, image, instruction, action, previous="None", max_tokens=24):
231
+ """The text a type / answer / open step needs, written by the served base model (text_model)."""
232
+ from .agent import TEXT_ASK
233
+ if not self.text_model:
234
+ raise RuntimeError("text steps need the base model: serve it and pass text_model=<its served name>")
235
+ text, _, _ = generate(self.text_model, self.image_url(image),
236
+ agent_user(instruction, previous) + "\n\n" + TEXT_ASK[action], max_tokens=max_tokens)
237
+ return text.strip().strip('"').split("\n")[0][:120]
238
+
239
+
240
  _PROC = {}
241
 
242
 
slotread/local.py CHANGED
@@ -11,30 +11,16 @@ The slot template and label ids come from slotread_config.json in the model (or
11
  config.json that slotread.train writes, renamed so it does not collide with the model's own config.json.
12
  """
13
 
14
- import json, os, time
15
 
16
  import torch
17
 
18
  from . import labels as L
19
- from .client import agent_user
20
  from .images import cap_image
21
  from .labels import Arm
22
  from .tasks import AgentTask, BoxTask, SegTask
23
 
24
- CONFIG_NAME = "slotread_config.json"
25
-
26
-
27
- def _config(repo_or_dir):
28
- if os.path.isdir(repo_or_dir):
29
- for name in (CONFIG_NAME, "config.json"):
30
- p = os.path.join(repo_or_dir, name)
31
- if os.path.exists(p) and "tasks" in json.load(open(p)):
32
- return json.load(open(p))
33
- raise FileNotFoundError(f"no {CONFIG_NAME} in {repo_or_dir}")
34
- from huggingface_hub import hf_hub_download
35
- return json.load(open(hf_hub_download(repo_or_dir, CONFIG_NAME)))
36
-
37
-
38
  class LocalSlotModel:
39
  def __init__(self, model, processor, cfg):
40
  self.model, self.proc, self.cfg = model, processor, cfg
@@ -54,7 +40,7 @@ class LocalSlotModel:
54
  kw = {"torch_device": "cpu"} if device_map == "cpu" else {}
55
  model = PeftModel.from_pretrained(model, adapter, **kw)
56
  model.eval()
57
- cfg = json.load(open(config)) if config else _config(adapter or model_id)
58
  return cls(model, AutoProcessor.from_pretrained(processor or model_id), cfg)
59
 
60
  @torch.no_grad()
 
11
  config.json that slotread.train writes, renamed so it does not collide with the model's own config.json.
12
  """
13
 
14
+ import json, time
15
 
16
  import torch
17
 
18
  from . import labels as L
19
+ from .client import agent_user, load_config
20
  from .images import cap_image
21
  from .labels import Arm
22
  from .tasks import AgentTask, BoxTask, SegTask
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  class LocalSlotModel:
25
  def __init__(self, model, processor, cfg):
26
  self.model, self.proc, self.cfg = model, processor, cfg
 
40
  kw = {"torch_device": "cpu"} if device_map == "cpu" else {}
41
  model = PeftModel.from_pretrained(model, adapter, **kw)
42
  model.eval()
43
+ cfg = json.load(open(config)) if config else load_config(adapter or model_id)
44
  return cls(model, AutoProcessor.from_pretrained(processor or model_id), cfg)
45
 
46
  @torch.no_grad()