Skip to content

Commit c66468a

Browse files
abrichrclaude
andauthored
feat: add GLM-OCR as primary OCR backend, pytesseract as fallback (#260)
run_ocr() now tries backends in order: 1. GLM-OCR (VLM-based, pip install glmocr, better accuracy on complex UIs) 2. pytesseract (traditional OCR, requires system Tesseract binary) 3. Empty list (graceful degradation) Added [ocr] optional dependency group for glmocr. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6a3dc5a commit c66468a

2 files changed

Lines changed: 82 additions & 4 deletions

File tree

openadapt_evals/grounding.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,10 @@ def _bbox_distance(
349349
def run_ocr(screenshot: bytes) -> list[dict]:
350350
"""Run OCR on a screenshot and return detected text regions.
351351
352-
Uses ``pytesseract`` when available. If it is not installed, returns
353-
an empty list (graceful degradation — callers must handle ``[]``).
352+
Tries backends in order:
353+
1. GLM-OCR (VLM-based, better accuracy on complex UIs, ``pip install glmocr``)
354+
2. pytesseract (traditional OCR, requires system Tesseract binary)
355+
3. Returns ``[]`` if neither is available (graceful degradation).
354356
355357
Args:
356358
screenshot: PNG image bytes.
@@ -359,10 +361,79 @@ def run_ocr(screenshot: bytes) -> list[dict]:
359361
List of dicts with keys ``"text"``, ``"bbox"`` (``[x1, y1, x2, y2]``),
360362
and ``"confidence"`` (``0.0``–``1.0``).
361363
"""
364+
# --- Try GLM-OCR first (VLM-based, better accuracy) ---
365+
results = _run_glm_ocr(screenshot)
366+
if results:
367+
return results
368+
369+
# --- Fallback to pytesseract ---
370+
results = _run_pytesseract(screenshot)
371+
if results:
372+
return results
373+
374+
logger.debug("No OCR backend available (tried glmocr, pytesseract)")
375+
return []
376+
377+
378+
def _run_glm_ocr(screenshot: bytes) -> list[dict]:
379+
"""Run GLM-OCR on a screenshot.
380+
381+
GLM-OCR uses a VLM (CogViT + GLM-0.5B) for semantic text extraction.
382+
Returns structured results with bounding boxes.
383+
384+
Requires: ``pip install glmocr``
385+
"""
386+
try:
387+
from glmocr import parse # type: ignore[import-untyped]
388+
except ImportError:
389+
return []
390+
391+
try:
392+
import io
393+
import tempfile
394+
from pathlib import Path
395+
396+
# GLM-OCR expects a file path, not bytes
397+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
398+
f.write(screenshot)
399+
tmp_path = f.name
400+
401+
try:
402+
result = parse(tmp_path)
403+
finally:
404+
Path(tmp_path).unlink(missing_ok=True)
405+
406+
# Convert GLM-OCR output to our standard format
407+
results: list[dict] = []
408+
# GLM-OCR returns structured blocks with text and coordinates
409+
for block in getattr(result, "blocks", []):
410+
text = getattr(block, "text", "").strip()
411+
if not text or len(text) < 2:
412+
continue
413+
bbox = getattr(block, "bbox", None)
414+
if bbox and len(bbox) >= 4:
415+
results.append({
416+
"text": text,
417+
"bbox": [int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])],
418+
"confidence": getattr(block, "confidence", 0.9),
419+
})
420+
if results:
421+
logger.info("GLM-OCR extracted %d text regions", len(results))
422+
return results
423+
424+
except Exception as exc:
425+
logger.debug("GLM-OCR failed: %s", exc)
426+
return []
427+
428+
429+
def _run_pytesseract(screenshot: bytes) -> list[dict]:
430+
"""Run pytesseract OCR on a screenshot.
431+
432+
Requires: ``pip install pytesseract`` + system Tesseract binary.
433+
"""
362434
try:
363435
import pytesseract # type: ignore[import-untyped]
364436
except ImportError:
365-
logger.debug("pytesseract not installed — returning empty OCR results")
366437
return []
367438

368439
try:
@@ -372,7 +443,7 @@ def run_ocr(screenshot: bytes) -> list[dict]:
372443
image = Image.open(io.BytesIO(screenshot))
373444
data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)
374445
except Exception as exc:
375-
logger.warning("OCR failed: %s", exc)
446+
logger.debug("pytesseract failed: %s", exc)
376447
return []
377448

378449
results: list[dict] = []
@@ -393,6 +464,8 @@ def run_ocr(screenshot: bytes) -> list[dict]:
393464
"bbox": [x, y, x + w, y + h],
394465
"confidence": conf / 100.0,
395466
})
467+
if results:
468+
logger.info("pytesseract extracted %d text regions", len(results))
396469
return results
397470

398471

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ aws = [
8080
# AWS EC2 management for VM pool operations
8181
"boto3>=1.34.0",
8282
]
83+
ocr = [
84+
# OCR for Tier 1.5a text anchoring in grounding cascade
85+
# GLM-OCR (VLM-based, better accuracy): pip install glmocr
86+
"glmocr>=0.1.0",
87+
]
8388
retrieval = [
8489
# For RetrievalAugmentedAgent with automatic demo selection
8590
"openadapt-retrieval>=0.1.0",

0 commit comments

Comments
 (0)