diagnostic: /point perception probe (Molmo2-ER pointing)
Browse files- molmoact2_server.py +60 -0
molmoact2_server.py
CHANGED
|
@@ -164,6 +164,66 @@ def health() -> dict:
|
|
| 164 |
return _status()
|
| 165 |
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
@app.post("/act", response_model=ActResponse)
|
| 168 |
def act(req: ActRequest, authorization: Optional[str] = Header(None)) -> ActResponse:
|
| 169 |
# Constant-time compare so a bad token can't be recovered via response timing.
|
|
|
|
| 164 |
return _status()
|
| 165 |
|
| 166 |
|
| 167 |
+
class PointRequest(BaseModel):
|
| 168 |
+
image: str # base64 JPEG/PNG, one camera view
|
| 169 |
+
query: str = "the red cup"
|
| 170 |
+
max_new_tokens: int = 96
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class PointResponse(BaseModel):
|
| 174 |
+
raw: str # the VLM's verbatim generation
|
| 175 |
+
points: list[list[float]] # parsed [[x, y], ...] in PERCENT of image size
|
| 176 |
+
compute_ms: Optional[float] = None
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.post("/point", response_model=PointResponse)
|
| 180 |
+
def point(req: PointRequest, authorization: Optional[str] = Header(None)) -> PointResponse:
|
| 181 |
+
"""Perception probe (diagnostic, not on the control path): ask the Molmo2-ER
|
| 182 |
+
backbone — a pixel-accurate pointing model — to point at `query` in ONE
|
| 183 |
+
frame. Separates "does the model SEE the target in our camera domain" from
|
| 184 |
+
"does it act correctly": wrong/absent points on live robot frames = visual
|
| 185 |
+
domain gap (no calibration work can fix it); correct points + wrong motion
|
| 186 |
+
= the failure is downstream of perception."""
|
| 187 |
+
if not authorization or not secrets.compare_digest(authorization, f"Bearer {AUTH_TOKEN}"):
|
| 188 |
+
raise HTTPException(status_code=401, detail="bad or missing bearer token")
|
| 189 |
+
if _model is None:
|
| 190 |
+
detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
|
| 191 |
+
raise HTTPException(status_code=503, detail=detail)
|
| 192 |
+
img = Image.fromarray(_decode(req.image))
|
| 193 |
+
prompt = f"Point to {req.query}."
|
| 194 |
+
t0 = time.time()
|
| 195 |
+
try:
|
| 196 |
+
with _lock, torch.inference_mode():
|
| 197 |
+
# Preferred: the processor's chat template (Molmo2 family). Fallback:
|
| 198 |
+
# the classic Molmo processor.process() API. Both produce tensors the
|
| 199 |
+
# underlying ImageTextToText model can generate from.
|
| 200 |
+
try:
|
| 201 |
+
inputs = _processor.apply_chat_template(
|
| 202 |
+
[{"role": "user",
|
| 203 |
+
"content": [{"type": "image", "image": img},
|
| 204 |
+
{"type": "text", "text": prompt}]}],
|
| 205 |
+
add_generation_prompt=True, tokenize=True,
|
| 206 |
+
return_dict=True, return_tensors="pt")
|
| 207 |
+
except Exception:
|
| 208 |
+
inputs = _processor.process(images=[img], text=prompt)
|
| 209 |
+
inputs = {k: (v.unsqueeze(0) if hasattr(v, "dim") and v.dim() in (1, 3) else v)
|
| 210 |
+
for k, v in inputs.items()}
|
| 211 |
+
inputs = {k: (v.to(_model.device) if hasattr(v, "to") else v)
|
| 212 |
+
for k, v in inputs.items()}
|
| 213 |
+
out = _model.generate(**inputs, max_new_tokens=int(req.max_new_tokens))
|
| 214 |
+
n_in = inputs["input_ids"].shape[1] if "input_ids" in inputs else 0
|
| 215 |
+
text = _processor.tokenizer.decode(out[0][n_in:], skip_special_tokens=False)
|
| 216 |
+
except Exception as exc:
|
| 217 |
+
raise HTTPException(status_code=500, detail=f"pointing failed: {type(exc).__name__}: {exc}")
|
| 218 |
+
# Parse Molmo point markup: <point x="53.1" y="42.2" ...> (single) and the
|
| 219 |
+
# <points x1=".." y1=".." x2=".." ...> multi-point form. Percent coordinates.
|
| 220 |
+
import re
|
| 221 |
+
pts = [[float(x), float(y)] for x, y in
|
| 222 |
+
re.findall(r'x\d*="([0-9.]+)"\s+y\d*="([0-9.]+)"', text)]
|
| 223 |
+
return PointResponse(raw=text, points=pts,
|
| 224 |
+
compute_ms=round((time.time() - t0) * 1000.0, 1))
|
| 225 |
+
|
| 226 |
+
|
| 227 |
@app.post("/act", response_model=ActResponse)
|
| 228 |
def act(req: ActRequest, authorization: Optional[str] = Header(None)) -> ActResponse:
|
| 229 |
# Constant-time compare so a bad token can't be recovered via response timing.
|