@@ -349,8 +349,10 @@ def _bbox_distance(
349349def 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
0 commit comments