Views
No views yet
meta-models/Muse-Glimmer-30B to point at UI elements in web
screenshots: given a screenshot and an instruction like "filter by MATEIN brand", it returns a
click point as a percentage of image width/height.y values as high as 970 on screenshots 712 pixels tall.
Those numbers do not correspond to original-image pixels or to the resized canvas the model was
shown, and no rescaling rescues them: scored as percentages they give 4.0%, divided by 1000 6.3%,
taken as raw pixels 4.0%. This adapter puts every one of 300 answers in range, and stops narrating:
290 generated tokens down to 99.1import json, torch
2from transformers import AutoModelForMultimodalLM, AutoProcessor, BitsAndBytesConfig
3from peft import PeftModel
4
5BASE = "meta-models/Muse-Glimmer-30B"
6processor = AutoProcessor.from_pretrained(BASE)
7model = AutoModelForMultimodalLM.from_pretrained(
8 BASE,
9 dtype=torch.bfloat16,
10 device_map="auto",
11 quantization_config=BitsAndBytesConfig(
12 load_in_4bit=True,
13 bnb_4bit_compute_dtype=torch.bfloat16,
14 bnb_4bit_use_double_quant=True,
15 bnb_4bit_quant_type="nf4",
16 # the vision tower shares weight names with the LLM; quantizing it degrades grounding
17 llm_int8_skip_modules=["model.vision_tower", "model.vision_adapter", "lm_head"],
18 ),
19)
20model = PeftModel.from_pretrained(model, "merve/muse-glimmer-ft-clicking-v2")
21model.eval()
22
23PROMPT = (
24 "You are looking at a screenshot of a webpage. Follow this instruction: {question}\n\n"
25 "Respond with a single JSON object of the form "
26 '{{"thought": "<brief reasoning>", "action": {{"name": "click", "button": "left", '
27 '"click_type": "single", "x": <0-100>, "y": <0-100>}}}}, '
28 "where x and y are the click point as a percentage of the image width and height. "
29 "Output only the JSON object."
30)
31
32def click(image, question):
33 small = image.convert("RGB")
34 small.thumbnail((512, 512)) # trained at max side 512; percentages are resize-invariant
35 messages = [{"role": "user", "content": [
36 {"type": "image", "image": small},
37 {"type": "text", "text": PROMPT.format(question=question)},
38 ]}]
39 text = processor.apply_chat_template(
40 messages, add_generation_prompt=True, tokenize=False, reasoning_strength="low"
41 )
42 inputs = processor(images=[[small]], text=[text], return_tensors="pt",
43 add_special_tokens=False).to(model.device)
44 with torch.inference_mode():
45 out = model.generate(**inputs, max_new_tokens=256, do_sample=False)
46 generated = out[0, inputs["input_ids"].shape[-1]:].tolist()
47
48 # Read ONLY the `to=user` channel. The model also emits a `to=self` reasoning channel,
49 # and numbers scraped from that are coordinates it already discarded. The tokenizer
50 # carries a `response_template`, so let transformers do the parsing; `prefix` is the
51 # assistant header the chat template pre-writes before generation starts.
52 tok = processor.tokenizer
53 answer = tok.parse_response(
54 generated, prefix=tok("<|start|>assistant", add_special_tokens=False)["input_ids"]
55 )["content"]
56 action = json.loads(answer)["action"]
57 return action["x"] / 100 * image.width, action["y"] / 100 * image.height # original pixels.eval() (in train mode LoRA
dropout stays active and generation degrades badly), and parse only from the to=user channel.bf16 compute, vision tower left unquantized and untrained.| LoRA | r=16, alpha=32, dropout 0.05, on q_proj/k_proj/v_proj/o_proj, exclude_modules=".*vision_tower.*" |
| trainable params | 29,392,896 (0.099% of 29.8B) |
| data | 22,364 click messages from 4,500 screenshots (≤5 instructions per screenshot) |
| schedule | 350 steps × effective batch 64 (16 × 4 accum) ≈ 1 epoch, lr 1e-4, 7 warmup steps, adamw_8bit |
| loss | completion-only — masked to the assistant's answer, so image tokens are not loss targets |
| final train loss | 0.529 (last logged 0.43, token accuracy 85.7%) |
| hardware | 1×H200, 68 min |
MolmoWeb-mini desktop web screenshots at ≤512px max side. Mobile layouts,
desktop applications, dense spreadsheets, and full-resolution inputs are out of distribution.[0, 100], not pixels. Multiply by the original image size.