GUI-Decisions / slotread /tasks.py
infinitylogesh's picture
GUI-Decisions Space
d0e7c02 verified
Raw History Blame Contribute Delete
12.4 kB
"""Tasks: what the model sees and answers. A task gives its examples, its slot layout for an arm,
the soft targets, and how a read turns back into a prediction and a score.
agent computer use: action + point (+ swipe end, key, scroll), AGUVIS steps + Wave-UI clicks
box an element's or object's box as centre + log size, Wave-UI elements + RefCOCO referents
seg a referred object's mask as a centre + K ray lengths, RefCOCO
The assistant turn is the slot layout, each slot ending in the placeholder:
action: ___ x: ___ y: ___ x2: ___ y2: ___ key: ___ sdir: ___ samt: ___
and a slot's answer is the next-token distribution at the token before its placeholder.
"""
import math
from . import labels as L
from .data import aguvis, refcoco, waveui
from .geometry import box_iou, mask_iou, poly_mask, radial_decode, radial_encode
from .images import image_of, image_size
from .numbers import LOG_MIN, number_targets, one_hot, read_number, slots_for, unwarp, warp
def coarse_desc(arm, what):
"""How a number's slots are described in a system prompt."""
return f"the tens digit (00-99) and then the fine code of {what}" if arm.coarse else f"the code of {what}"
class Task:
name = ""
numbers = {} # number -> "lin" | "log"
def layout(self, arm):
raise NotImplementedError
def system(self, arm):
raise NotImplementedError
def user(self, ex):
raise NotImplementedError
def number_of(self, slot):
return slot[:-1] if slot.endswith("c") and slot[:-1] in self.numbers else slot
def messages(self, ex, arm, assistant=None):
return [{"role": "system", "content": [{"type": "text", "text": self.system(arm)}]},
{"role": "user", "content": [{"type": "image", "image": image_of(ex)},
{"type": "text", "text": self.user(ex)}]},
{"role": "assistant", "content": [{"type": "text", "text": assistant if assistant is not None
else " ".join(f"{s}: ___" for s in self.layout(arm))}]}]
def labels(self, arm, tok):
out = {}
for s in self.layout(arm):
n = self.number_of(s)
out[s] = L.DIGITS if (n in self.numbers and s.endswith("c") and n != s) else (
arm.fine if n in self.numbers else self.categorical_labels(s, tok))
return out
def categorical_labels(self, slot, tok):
raise KeyError(slot)
def config(self, arm, tok):
"""Everything a reader needs to rebuild this task's prompt and labels (saved in config.json)."""
return dict(slots=self.layout(arm), system=self.system(arm), numbers=self.numbers,
labels=self.labels(arm, tok))
class AgentTask(Task):
"""Computer use. Only the slots an action uses are trained on that example (MASK), so x2 is free
to mean nothing on a click. Text arguments (what to type, the final answer) are not slots: the base
model writes them."""
name = "agent"
numbers = {"x": "lin", "y": "lin", "x2": "lin", "y2": "lin"}
MASK = {"click": ["x", "y"], "select": ["x", "y"], "swipe": ["x", "y", "x2", "y2"],
"scroll": ["sdir", "samt"], "press": ["key"]}
def layout(self, arm):
return (["action"] + slots_for("x", arm) + slots_for("y", arm) + slots_for("x2", arm)
+ slots_for("y2", arm) + ["key", "sdir", "samt"])
def system(self, arm):
return ("You are a GUI agent. Given a screenshot, the task and the previous actions, answer the next step. "
"action: " + ", ".join(aguvis.ACTIONS) + ". "
f"For x, {coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
f"for y, the same vertically from the top to the bottom. x2 and y2: the end point of a swipe, written "
f"the same way. key: the key to press. sdir and samt: scroll direction and amount.")
def user(self, ex):
return ex["user"]
def categorical_labels(self, slot, tok):
return {"action": L.word_label_ids(tok, "action", aguvis.ACTIONS),
"key": L.word_label_ids(tok, "key", aguvis.KEYS),
"sdir": L.word_label_ids(tok, "sdir", aguvis.SDIR), "samt": L.DIGITS}[slot]
def targets(self, ex, arm):
a, g = ex["action"], ex["args"]
tg = {"action": one_hot(len(aguvis.ACTIONS), aguvis.ACTIONS.index(a))}
for n in self.MASK.get(a, []):
if n in self.numbers:
if g.get(n) is None:
continue
w = g.get("box" if n.startswith("x") else "boy", 0.02) # the target's width / height, if known
tg.update(number_targets(n, g[n], 0.25 * w, arm))
elif n == "key":
tg["key"] = one_hot(len(aguvis.KEYS), aguvis.KEYS.index(g.get("key", "other")))
elif n == "sdir" and g.get("sdir") in aguvis.SDIR:
tg["sdir"] = one_hot(len(aguvis.SDIR), aguvis.SDIR.index(g["sdir"]))
elif n == "samt" and g.get("samt") is not None:
tg["samt"] = one_hot(10, int(g["samt"]))
return tg
def decode(self, lp, arm, how="default"):
out = dict(action=aguvis.ACTIONS[int(lp["action"].argmax())],
x=read_number(lp, "x", arm, how), y=read_number(lp, "y", arm, how))
if "x2" in lp:
out.update(x2=read_number(lp, "x2", arm, how), y2=read_number(lp, "y2", arm, how))
return out
def score(self, pred, ex, thresh=0.14):
"""A step is right when the action is, and (for a click, select or swipe) the point is within
0.14 of the target on each axis."""
ok_a = pred["action"] == ex["action"]
ok_p = True
if ex["action"] in ("click", "select", "swipe"):
ok_p = abs(pred["x"] - ex["args"]["x"]) <= thresh and abs(pred["y"] - ex["args"]["y"]) <= thresh
return dict(action=float(ok_a), point=float(ok_p) if ex["action"] in ("click", "select", "swipe") else None,
step=float(ok_a and ok_p))
def examples(self, n_agent, n_click, n_val):
"""(train, val): AGUVIS steps + Wave-UI clicks; val is the first n_val steps of the same pool
(held out from this run; use aguvis.test_steps() to compare runs)."""
pool = aguvis.steps(n_agent + n_val)
val, train = pool[:n_val], pool[n_val:]
return train + (waveui.clicks(n_click) if n_click else []), val
class BoxTask(Task):
"""An element's or object's box as centre + size; sizes log-warped."""
name = "box"
numbers = {"cx": "lin", "cy": "lin", "w": "log", "h": "log"}
def layout(self, arm):
return slots_for("cx", arm) + slots_for("cy", arm) + slots_for("w", arm) + slots_for("h", arm)
def system(self, arm):
return ("You locate an element or object in an image and give its bounding box as a centre and a size. "
f"For cx, {coarse_desc(arm, 'the horizontal position of the centre, from the left edge to the right edge')}; "
"for cy, the same vertically from the top to the bottom. "
f"For w, {coarse_desc(arm, 'the box width on a scale from tiny to the whole image')}; "
"for h, the same for the height.")
def user(self, ex):
return f"Element: {ex['instruction']}"
def norm_box(self, ex):
W, H = image_size(ex)
x1, y1, x2, y2 = ex["box"]
return [x1 / W, y1 / H, x2 / W, y2 / H] if not ex.get("norm") else list(ex["box"])
def targets(self, ex, arm):
x1, y1, x2, y2 = self.norm_box(ex)
w, h = max(x2 - x1, LOG_MIN), max(y2 - y1, LOG_MIN)
tg = {}
tg.update(number_targets("cx", (x1 + x2) / 2, 0.12 * w, arm))
tg.update(number_targets("cy", (y1 + y2) / 2, 0.12 * h, arm))
tg.update(number_targets("w", warp(w, "log"), 0.02, arm))
tg.update(number_targets("h", warp(h, "log"), 0.02, arm))
return tg
def decode(self, lp, arm, how="default"):
cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
w, h = unwarp(read_number(lp, "w", arm, how), "log"), unwarp(read_number(lp, "h", arm, how), "log")
return [max(0.0, cx - w / 2), max(0.0, cy - h / 2), min(1.0, cx + w / 2), min(1.0, cy + h / 2)]
def score(self, pred, ex):
b = self.norm_box(ex)
i = box_iou(pred, b)
cx, cy = (pred[0] + pred[2]) / 2, (pred[1] + pred[3]) / 2
return dict(iou=i, iou50=float(i >= 0.5), iou75=float(i >= 0.75),
center_in=float(b[0] <= cx <= b[2] and b[1] <= cy <= b[3]))
def examples(self, n_train, n_val, n_photo=0, seed=0):
"""(train, val): Wave-UI element boxes, plus n_photo RefCOCO referent boxes (general objects).
val: Wave-UI validation boxes, and val_photo: RefCOCO val referents (None without photos)."""
train = waveui.boxes(n_train)
val = waveui.val_boxes(n_val)
val_photo = None
if n_photo:
train += refcoco.photo_boxes(n_photo, "train", seed + 100)
val_photo = refcoco.photo_boxes(n_val, "validation", seed + 100)
return train, val, val_photo
class SegTask(Task):
"""A referred object's mask as a centre + K ray lengths; rays are fractions of the image diagonal,
log-warped."""
name = "seg"
def __init__(self, K=24):
self.K = K
self.numbers = {"cx": "lin", "cy": "lin", **{f"r{k}": "log" for k in range(K)}}
def layout(self, arm):
out = slots_for("cx", arm) + slots_for("cy", arm)
for k in range(self.K):
out += slots_for(f"r{k}", arm)
return out
def system(self, arm):
K = self.K
return ("You outline an object in an image. cx and cy: a point well inside the object; "
f"r0 to r{K - 1}: how far the object reaches from that point along {K} directions, r0 pointing right "
f"and the directions turning clockwise in equal steps. For cx, "
f"{coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
"for cy, the same vertically from the top to the bottom. For each r, "
f"{coarse_desc(arm, 'the distance on a scale from tiny to the whole image diagonal')}.")
def user(self, ex):
return f"Object: {ex['sentence']}"
def encoded(self, ex):
"""(cx, cy, d) in pixels, computed once per example."""
if "radial" not in ex:
W, H = ex["size"]
ex["radial"] = radial_encode(poly_mask(ex["polys"], W, H), self.K)
return ex["radial"]
def targets(self, ex, arm):
W, H = ex["size"]
cx, cy, d = self.encoded(ex)
bx1, by1, bx2, by2 = ex["bbox"]
diag = math.hypot(W, H)
tg = {}
tg.update(number_targets("cx", cx / W, 0.12 * (bx2 - bx1) / W, arm))
tg.update(number_targets("cy", cy / H, 0.12 * (by2 - by1) / H, arm))
for k in range(self.K):
tg.update(number_targets(f"r{k}", warp(d[k] / diag, "log"), 0.03, arm))
return tg
def decode(self, lp, arm, how="default"):
cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
r = [unwarp(read_number(lp, f"r{k}", arm, how), "log") for k in range(self.K)]
return dict(cx=cx, cy=cy, r=r)
def score(self, pred, ex):
W, H = ex["size"]
diag = math.hypot(W, H)
m, pts = radial_decode(pred["cx"] * W, pred["cy"] * H, [r * diag for r in pred["r"]], W, H)
gt = poly_mask(ex["polys"], W, H)
iou, inter, union = mask_iou(m, gt)
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
pb = [max(0, min(xs)), max(0, min(ys)), min(W, max(xs)), min(H, max(ys))]
rec = box_iou([pb[0] / W, pb[1] / H, pb[2] / W, pb[3] / H],
[ex["bbox"][0] / W, ex["bbox"][1] / H, ex["bbox"][2] / W, ex["bbox"][3] / H])
return dict(iou=float(iou), inter=inter, union=union, rec50=float(rec >= 0.5))
def examples(self, n_train, n_val, seed=0):
train = refcoco.examples(refcoco.rows("train"), n_train, seed)
val = refcoco.examples(refcoco.rows("validation"), n_val, seed + 1, one_sentence=False)
return train, val
TASKS = {"agent": AgentTask, "box": BoxTask, "seg": SegTask}