99
1010import argparse
1111import logging
12+ import re
1213import subprocess
1314import sys
1415import unicodedata
@@ -28,6 +29,12 @@ class ModelSpec:
2829 mmproj_arg : str
2930 model_default : str
3031 mmproj_default : str
32+ prompt : str = "Free OCR. "
33+ n_predict : int = 512
34+ n_ctx : int | None = None
35+ # Unlimited-OCR's "document parsing" prompt emits <|det|> grounding markup that
36+ # the HF reference strips in result.md; drop it before scoring to match.
37+ strip_grounding : bool = False
3138
3239
3340@dataclass
@@ -63,6 +70,20 @@ def chrf_min(self) -> float:
6370 model_default = "gguf_models/deepseek-ai/deepseek-ocr-2-bf16.gguf" ,
6471 mmproj_default = "gguf_models/deepseek-ai/mmproj-deepseek-ocr-2-bf16.gguf" ,
6572 ),
73+ "unlimited" : ModelSpec (
74+ key = "unlimited" , label = "Unlimited-OCR" ,
75+ model_arg = "--llama-model-unlimited" , mmproj_arg = "--mmproj-unlimited" ,
76+ model_default = "gguf_models/baidu/unlimited-ocr-bf16.gguf" ,
77+ mmproj_default = "gguf_models/baidu/mmproj-unlimited-ocr-bf16.gguf" ,
78+ # "Free OCR." immediately emits EOS on this checkpoint; the HF reference
79+ # (demo/unlimited_ocr_scores.py) uses "document parsing.", which grounds.
80+ prompt = "document parsing." ,
81+ # Grounding emits ~3x the tokens of plain OCR, so it needs a larger budget
82+ # and context to reach the article body the ground truth covers.
83+ n_predict = 4096 ,
84+ n_ctx = 16384 ,
85+ strip_grounding = True ,
86+ ),
6687}
6788
6889CASES = [
@@ -82,9 +103,26 @@ def chrf_min(self) -> float:
82103 # is one pixel off and lands at ~0.69 instead.
83104 hf_cer = 0.7761 , hf_chrf = 28.70 , cer_tol = 0.12 , chrf_tol = 8.0 ,
84105 ),
106+ TestCase (
107+ model_key = "unlimited" , label = "single-view scan" ,
108+ image = "tools/mtmd/test-1.jpeg" ,
109+ ground_truth = "tools/mtmd/tests/test-1-ground-truth.txt" ,
110+ # HF reference: Unlimited-OCR scoring (gundam, bf16) on this image/ground-truth.
111+ # Decoder runs full MHA, not R-SWA; the band absorbs that gap + bf16 variance.
112+ hf_cer = 0.1869 , hf_chrf = 75.23 , cer_tol = 0.06 , chrf_tol = 6.0 ,
113+ ),
85114]
86115
87116
117+ GROUNDING_TAG_RE = re .compile (r"<\|(ref|det)\|>.*?<\|/\1\|>" , re .DOTALL )
118+
119+
120+ def strip_grounding (text : str ) -> str :
121+ """Drop <|ref|>..<|/ref|> / <|det|>..<|/det|> grounding markup, matching the
122+ cleaned result.md the HF reference scores against."""
123+ return GROUNDING_TAG_RE .sub ("" , text )
124+
125+
88126def arg_dest (flag : str ) -> str :
89127 return flag .lstrip ("-" ).replace ("-" , "_" )
90128
@@ -129,19 +167,19 @@ def compute_chrf(expected: str, ocr_out: str) -> float:
129167 return CHRF ().sentence_score (ocr_out , [expected ]).score
130168
131169
132- def run_mtmd_cli (model_path , mmproj_path , image_path , bin_path ) -> str :
170+ def run_mtmd_cli (spec : "ModelSpec" , model_path , mmproj_path , image_path , bin_path ) -> str :
133171 """Run mtmd-cli on the image and return its output."""
134172 cmd = [
135173 str (bin_path ),
136174 "-m" , str (model_path ),
137175 "--mmproj" , str (mmproj_path ),
138176 "--image" , str (image_path ),
139- "-p" , "Free OCR. " ,
177+ "-p" , spec . prompt ,
140178 "--chat-template" , "deepseek-ocr" ,
141179 "--temp" , "0" ,
142180 "--flash-attn" , "off" , # match the HF "eager" attention reference
143181 "--no-warmup" ,
144- "-n" , "512" , # cap loops on hard images (KV would otherwise fill)
182+ "-n" , str ( spec . n_predict ) , # cap loops on hard images (KV would otherwise fill)
145183 # HF decodes with no_repeat_ngram_size; llama.cpp's analog is DRY.
146184 # Default DRY breakers include "\n", so they are cleared below.
147185 "--dry-multiplier" , "0.8" ,
@@ -150,6 +188,8 @@ def run_mtmd_cli(model_path, mmproj_path, image_path, bin_path) -> str:
150188 "--dry-penalty-last-n" , "-1" ,
151189 "--dry-sequence-breaker" , "none" ,
152190 ]
191+ if spec .n_ctx is not None :
192+ cmd += ["-c" , str (spec .n_ctx )]
153193 logger .debug (f" command: { ' ' .join (cmd )} " )
154194
155195 try :
@@ -164,6 +204,8 @@ def run_mtmd_cli(model_path, mmproj_path, image_path, bin_path) -> str:
164204 raise RuntimeError (f"llama-mtmd-cli failed with code { result .returncode } " )
165205
166206 output = result .stdout .decode ("utf-8" , errors = "replace" ).strip ()
207+ if spec .strip_grounding :
208+ output = strip_grounding (output )
167209 if not output :
168210 raise RuntimeError ("llama-mtmd-cli produced no output on stdout" )
169211 logger .info (f" output: { len (output )} chars" )
@@ -193,7 +235,7 @@ def evaluate(case: "TestCase", expected: str, ocr_out: str) -> bool:
193235
194236 logger .info ("" )
195237 logger .info ("=" * 60 )
196- logger .info ("Free OCR evaluation:" )
238+ logger .info ("OCR evaluation:" )
197239 logger .info ("=" * 60 )
198240 logger .info (f" CER { cer :>7.4f} (HF { case .hf_cer :.4f} , <= { case .cer_max :>7.4f} -> { verdict (cer_pass )} )" )
199241 logger .info (f" chrF (0-100) { chrf :>7.2f} (HF { case .hf_chrf :.2f} , >= { case .chrf_min :>7.2f} -> { verdict (chrf_pass )} )" )
@@ -269,9 +311,9 @@ def main() -> int:
269311 expected = read_expected_text (ground_truth )
270312 logger .info (f" Image: { case .image } " )
271313 logger .info (f" Expected text: { len (expected )} chars" )
272- logger .info (" Running llama.cpp 'Free OCR' " )
314+ logger .info (f " Running llama.cpp prompt { model_spec . prompt !r } " )
273315 try :
274- ocr_out = run_mtmd_cli (model , mmproj , image , binary )
316+ ocr_out = run_mtmd_cli (model_spec , model , mmproj , image , binary )
275317 except RuntimeError as e :
276318 logger .error (f" Error: { e } " )
277319 results [title ] = False
0 commit comments