Spaces:
Running on Zero
Running on Zero
Fix README content mapping
Browse files
README.md
CHANGED
|
@@ -1,1683 +1,135 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
pass
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
if spaces is not None:
|
| 140 |
-
@spaces.GPU
|
| 141 |
-
def _ensure_zero_gpu_lease() -> None:
|
| 142 |
-
return None
|
| 143 |
-
|
| 144 |
-
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 145 |
-
REGISTRY_PATH = os.path.join(ROOT_DIR, "model_registry.json")
|
| 146 |
-
DEFAULT_OCR_PROMPT = (
|
| 147 |
-
"You are a high-accuracy OCR engine for Hebrew documents that may contain printed and handwritten text. "
|
| 148 |
-
"Return the exact document text in Hebrew, preserving line breaks and spacing. "
|
| 149 |
-
"Do not add explanation or JSON."
|
| 150 |
-
)
|
| 151 |
-
ZERO_GPU_MAX_WORKERS = 4
|
| 152 |
-
ZERO_GPU_SAFE_HEADROOM = 0.62
|
| 153 |
-
ZERO_GPU_MAX_HEADROOM = 0.82
|
| 154 |
-
ZERO_GPU_MIN_GUARDED_FREE_GB = 1.0
|
| 155 |
-
ZERO_GPU_MODEL_FALLBACK_GB_SAFE = 2.6
|
| 156 |
-
ZERO_GPU_MODEL_FALLBACK_GB_MAX = 2.0
|
| 157 |
-
ZERO_GPU_MAX_SAFE_SELECTED = 6
|
| 158 |
-
DEFAULT_MAX_TOKENS = 4096
|
| 159 |
-
DEFAULT_PRECHECK_TIMEOUT_SEC = 12
|
| 160 |
-
DIRECT_MODEL_CACHE_MAX = 1
|
| 161 |
-
LOCAL_PIPELINE_CACHE_MAX = 1
|
| 162 |
-
TESSERACT_EXECUTABLE_PATHS = ("/usr/bin/tesseract", "/usr/local/bin/tesseract", "/opt/conda/bin/tesseract", "/bin/tesseract")
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def _inference_device() -> int:
|
| 166 |
-
if torch is not None and torch.cuda.is_available():
|
| 167 |
-
return 0
|
| 168 |
-
return -1
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def _inference_device_label() -> str:
|
| 172 |
-
if torch is not None and torch.cuda.is_available():
|
| 173 |
-
return "cuda"
|
| 174 |
-
return "cpu"
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
def _inference_dtype():
|
| 178 |
-
if torch is None:
|
| 179 |
-
return None
|
| 180 |
-
if hasattr(torch, "bfloat16"):
|
| 181 |
-
return torch.bfloat16
|
| 182 |
-
if hasattr(torch, "float16"):
|
| 183 |
-
return torch.float16
|
| 184 |
-
return None
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def _env_flag_true(name: str, default: bool = False) -> bool:
|
| 188 |
-
raw = os.getenv(name)
|
| 189 |
-
if raw is None:
|
| 190 |
-
return default
|
| 191 |
-
return raw.strip().lower() in {"1", "true", "yes", "on", "y"}
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
def _allow_cpu_fallback() -> bool:
|
| 195 |
-
return _env_flag_true("ALLOW_CPU_FALLBACK", default=False)
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def _strict_cuda_required() -> bool:
|
| 199 |
-
return not _allow_cpu_fallback()
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def _ensure_cuda_available(context: str = "Inference") -> None:
|
| 203 |
-
if torch is None:
|
| 204 |
-
raise RuntimeError(f"{context} requires PyTorch, but PyTorch is not available in this Space.")
|
| 205 |
-
if not torch.cuda.is_available():
|
| 206 |
-
raise RuntimeError(
|
| 207 |
-
f"{context} requires CUDA, but no CUDA device is available. "
|
| 208 |
-
"Run this Space on ZeroGPU and unset ALLOW_CPU_FALLBACK, or set ALLOW_CPU_FALLBACK=1."
|
| 209 |
-
)
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def _assert_model_on_cuda(model, model_id: str, context: str) -> None:
|
| 213 |
-
if not _strict_cuda_required():
|
| 214 |
-
return
|
| 215 |
-
if torch is None or not torch.cuda.is_available():
|
| 216 |
-
_ensure_cuda_available(context)
|
| 217 |
-
|
| 218 |
-
if model is None:
|
| 219 |
-
raise RuntimeError(f"{context}: loaded model is missing for {model_id}.")
|
| 220 |
-
|
| 221 |
-
try:
|
| 222 |
-
device_map = getattr(model, "hf_device_map", None)
|
| 223 |
-
if isinstance(device_map, Mapping):
|
| 224 |
-
non_cuda = {
|
| 225 |
-
str(value)
|
| 226 |
-
for value in device_map.values()
|
| 227 |
-
if str(value) not in {"0", "cuda", "cuda:0", "cuda:1", "cuda:2", "cuda:3"}
|
| 228 |
-
and str(value) != "None"
|
| 229 |
-
}
|
| 230 |
-
if non_cuda:
|
| 231 |
-
raise RuntimeError(f"{context}: {model_id} model-map is not on CUDA (found: {sorted(non_cuda)}).")
|
| 232 |
-
except Exception:
|
| 233 |
-
# If this check itself fails, continue to tensor-level validation below.
|
| 234 |
-
pass
|
| 235 |
-
|
| 236 |
-
try:
|
| 237 |
-
labels = {str(getattr(param, "device", "")) for param in model.parameters() if hasattr(param, "device")}
|
| 238 |
-
if not labels:
|
| 239 |
-
return
|
| 240 |
-
non_cuda = [label for label in labels if not str(label).startswith("cuda") and str(label) != "0"]
|
| 241 |
-
if non_cuda:
|
| 242 |
-
raise RuntimeError(
|
| 243 |
-
f"{context}: {model_id} is not on CUDA (tensor devices: {sorted(labels)}). "
|
| 244 |
-
"Set ALLOW_CPU_FALLBACK=1 only if you want CPU execution."
|
| 245 |
-
)
|
| 246 |
-
except Exception:
|
| 247 |
-
# If model has no parameters or doesn't expose devices this way, don't block.
|
| 248 |
-
return
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
def _inference_torch_kwargs_for_model(strict_cuda: bool = False) -> Tuple[dict, list]:
|
| 252 |
-
kwargs_priority = []
|
| 253 |
-
dtype = _inference_dtype()
|
| 254 |
-
if dtype is None:
|
| 255 |
-
kwargs_priority.append({})
|
| 256 |
-
return dtype, kwargs_priority
|
| 257 |
-
|
| 258 |
-
if strict_cuda and torch is not None and torch.cuda.is_available():
|
| 259 |
-
kwargs_priority.append({"torch_dtype": dtype, "low_cpu_mem_usage": True})
|
| 260 |
-
if dtype != getattr(torch, "float16", None):
|
| 261 |
-
kwargs_priority.append({"torch_dtype": torch.float16, "low_cpu_mem_usage": True})
|
| 262 |
-
kwargs_priority.append({"torch_dtype": torch.float16})
|
| 263 |
-
kwargs_priority.append({"torch_dtype": dtype})
|
| 264 |
-
kwargs_priority.append({})
|
| 265 |
-
else:
|
| 266 |
-
kwargs_priority.append({"torch_dtype": dtype, "device_map": "auto", "low_cpu_mem_usage": True})
|
| 267 |
-
if dtype != getattr(torch, "float16", None):
|
| 268 |
-
kwargs_priority.append({"torch_dtype": torch.float16, "device_map": "auto", "low_cpu_mem_usage": True})
|
| 269 |
-
kwargs_priority.append({"torch_dtype": torch.float16})
|
| 270 |
-
kwargs_priority.append({"torch_dtype": dtype, "device_map": "auto"})
|
| 271 |
-
kwargs_priority.append({"torch_dtype": dtype})
|
| 272 |
-
kwargs_priority.append({"torch_dtype": torch.float16})
|
| 273 |
-
kwargs_priority.append({})
|
| 274 |
-
return dtype, kwargs_priority
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
def _pipeline_cache_key(model_id: str, task: str, device: str, dtype: object, trust_remote_code: bool = False) -> Tuple[str, str, str, object, bool]:
|
| 278 |
-
return (model_id, task, device, str(dtype), trust_remote_code)
|
| 279 |
-
|
| 280 |
-
TRUST_REMOTE_CODE_MODELS = {
|
| 281 |
-
"microsoft/Phi-4-multimodal-instruct",
|
| 282 |
-
"PaddlePaddle/PaddleOCR-VL",
|
| 283 |
-
"PaddlePaddle/PaddleOCR-VL-1.5",
|
| 284 |
-
"PaddlePaddle/PaddleOCR-VL-1.6",
|
| 285 |
-
"datalab-to/chandra-ocr-2",
|
| 286 |
-
"datalab-to/surya-ocr-2",
|
| 287 |
-
"ronylicha/gigapdf-ocr-hebrew",
|
| 288 |
-
"liskcell/qunie-v7-mini",
|
| 289 |
-
"0cve0/openmlkitocr",
|
| 290 |
-
"waraja/tzefa-word-ocr-trocr",
|
| 291 |
-
"liskcell/qunie-v7-pico",
|
| 292 |
-
"cyttic/exp10-trocr-hebrew-matan-full",
|
| 293 |
-
"cyttic/exp23-directfit-unfrozen",
|
| 294 |
-
"cyttic/heb-verifier17-connected",
|
| 295 |
-
"cyttic/exp26-composed1m",
|
| 296 |
-
"deepseek-ai/deepseek-ocr",
|
| 297 |
-
"deepseek-ai/deepseek-ocr-2",
|
| 298 |
-
"coherelabs/aya-vision-8b",
|
| 299 |
-
"coherelabs/aya-vision-32b",
|
| 300 |
-
}
|
| 301 |
-
TRUST_REMOTE_CODE_MODELS = {m.lower() for m in TRUST_REMOTE_CODE_MODELS}
|
| 302 |
-
|
| 303 |
-
PIPELINE_TASK_HINTS = {
|
| 304 |
-
"qwen/qwen3-vl-8b-instruct": "image-to-text",
|
| 305 |
-
"qwen/qwen3-vl-4b-instruct": "image-to-text",
|
| 306 |
-
"qwen/qwen3-vl-8b-thinking": "image-to-text",
|
| 307 |
-
"qwen/qwen3-vl-4b-thinking": "image-to-text",
|
| 308 |
-
"qwen/qwen3-vl-30b-a3b-instruct": "image-to-text",
|
| 309 |
-
"qwen/qwen3-vl-30b-a3b-thinking": "image-to-text",
|
| 310 |
-
"google/gemma-4-e4b-it": "image-to-text",
|
| 311 |
-
"google/gemma-4-12b-it": "image-to-text",
|
| 312 |
-
"google/gemma-4-26b-a4b-it": "image-to-text",
|
| 313 |
-
"google/gemma-4-31b-it": "image-to-text",
|
| 314 |
-
"ronylicha/gigapdf-ocr-hebrew": "image-to-text",
|
| 315 |
-
"liskcell/qunie-v7-mini": "image-to-text",
|
| 316 |
-
"0cve0/openmlkitocr": "image-to-text",
|
| 317 |
-
"waraja/tzefa-word-ocr-trocr": "image-to-text",
|
| 318 |
-
"liskcell/qunie-v7-pico": "image-to-text",
|
| 319 |
-
"paddlepaddle/paddleocr-vl": "image-to-text",
|
| 320 |
-
"paddlepaddle/paddleocr-vl-1.5": "image-to-text",
|
| 321 |
-
"paddlepaddle/paddleocr-vl-1.6": "image-to-text",
|
| 322 |
-
"datalab-to/chandra-ocr-2": "image-to-text",
|
| 323 |
-
"datalab-to/surya-ocr-2": "image-to-text",
|
| 324 |
-
"deepseek-ai/deepseek-ocr": "image-to-text",
|
| 325 |
-
"deepseek-ai/deepseek-ocr-2": "image-to-text",
|
| 326 |
-
"cyttic/exp10-trocr-hebrew-matan-full": "image-to-text",
|
| 327 |
-
"cyttic/exp23-directfit-unfrozen": "image-to-text",
|
| 328 |
-
"cyttic/heb-verifier17-connected": "image-to-text",
|
| 329 |
-
"cyttic/exp26-composed1m": "image-to-text",
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
GATED_MODELS_REQUIRING_TOKEN = {
|
| 333 |
-
"coherelabs/aya-vision-8b",
|
| 334 |
-
"coherelabs/aya-vision-32b",
|
| 335 |
-
}
|
| 336 |
-
GATED_MODELS_REQUIRING_TOKEN = {m.lower() for m in GATED_MODELS_REQUIRING_TOKEN}
|
| 337 |
-
|
| 338 |
-
NON_HF_OR_NON_TRANSFORMERS_MODELS = {
|
| 339 |
-
"ronylicha/gigapdf-ocr-hebrew": (
|
| 340 |
-
"This checkpoint is not a standard Transformers OCR package (ONNX/RTen assets only)."
|
| 341 |
-
),
|
| 342 |
-
"liskcell/qunie-v7-mini": (
|
| 343 |
-
"This checkpoint appears distributed as a non-standard GGUF/ONNX package and cannot be loaded through "
|
| 344 |
-
"local Transformers in this Space."
|
| 345 |
-
),
|
| 346 |
-
"liskcell/qunie-v7-pico": (
|
| 347 |
-
"This checkpoint appears distributed as a non-standard GGUF/ONNX package and cannot be loaded through "
|
| 348 |
-
"local Transformers in this Space."
|
| 349 |
-
),
|
| 350 |
-
"0cve0/openmlkitocr": (
|
| 351 |
-
"This repository is not structured as a standard Hugging Face Transformers OCR model."
|
| 352 |
-
),
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
MODEL_PRECHECK_CACHE: Dict[str, Tuple[bool, str]] = {}
|
| 356 |
-
_MODEL_PRECHECK_LOCK = threading.Lock()
|
| 357 |
-
|
| 358 |
-
_LOCAL_PIPELINE_CACHE: OrderedDict = OrderedDict()
|
| 359 |
-
_LOCAL_DIRECT_CACHE: OrderedDict = OrderedDict()
|
| 360 |
-
_LOCAL_PIPELINE_LOCK = threading.Lock()
|
| 361 |
-
_LOCAL_DIRECT_LOCK = threading.Lock()
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
def load_registry() -> List[dict]:
|
| 365 |
-
with open(REGISTRY_PATH, "r", encoding="utf-8") as f:
|
| 366 |
-
payload = json.load(f)
|
| 367 |
-
return payload.get("models", [])
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
def _safe_load_registry() -> List[dict]:
|
| 371 |
-
models = load_registry()
|
| 372 |
-
if not isinstance(models, list):
|
| 373 |
-
raise RuntimeError("Invalid model_registry.json format: expected a list under 'models'.")
|
| 374 |
-
for model in models:
|
| 375 |
-
if not isinstance(model, dict):
|
| 376 |
-
continue
|
| 377 |
-
if "id" not in model:
|
| 378 |
-
raise RuntimeError("Invalid registry entry: missing 'id'.")
|
| 379 |
-
if "model_id" not in model and model.get("provider") != "tesseract":
|
| 380 |
-
raise RuntimeError(f"Invalid registry entry '{model.get('id')}': missing 'model_id'.")
|
| 381 |
-
return models
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
def image_to_data_uri(image_bytes: bytes) -> str:
|
| 385 |
-
return f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}"
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
def normalize_text_for_metrics(text: str) -> str:
|
| 389 |
-
if text is None:
|
| 390 |
-
return ""
|
| 391 |
-
text = text.replace("\u200c", "") # remove Hebrew ligature marks
|
| 392 |
-
text = re.sub(r"\s+", " ", text.strip())
|
| 393 |
-
return text
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
def edit_distance(a: str, b: str) -> int:
|
| 397 |
-
if not a:
|
| 398 |
-
return len(b)
|
| 399 |
-
if not b:
|
| 400 |
-
return len(a)
|
| 401 |
-
prev = list(range(len(b) + 1))
|
| 402 |
-
for i, ca in enumerate(a, 1):
|
| 403 |
-
curr = [i] + [0] * len(b)
|
| 404 |
-
for j, cb in enumerate(b, 1):
|
| 405 |
-
cost = 0 if ca == cb else 1
|
| 406 |
-
curr[j] = min(
|
| 407 |
-
prev[j] + 1,
|
| 408 |
-
curr[j - 1] + 1,
|
| 409 |
-
prev[j - 1] + cost,
|
| 410 |
-
)
|
| 411 |
-
prev = curr
|
| 412 |
-
return prev[-1]
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
def compute_cer_wer(reference: str, hypothesis: str) -> Tuple[Optional[float], Optional[float]]:
|
| 416 |
-
if not reference:
|
| 417 |
-
return None, None
|
| 418 |
-
ref_norm = normalize_text_for_metrics(reference)
|
| 419 |
-
hyp_norm = normalize_text_for_metrics(hypothesis)
|
| 420 |
-
ref_chars = ref_norm
|
| 421 |
-
hyp_chars = hyp_norm
|
| 422 |
-
ref_words = ref_norm.split(" ") if ref_norm else []
|
| 423 |
-
hyp_words = hyp_norm.split(" ") if hyp_norm else []
|
| 424 |
-
cer = edit_distance(ref_chars, hyp_chars) / max(1, len(ref_chars))
|
| 425 |
-
wer = edit_distance(" ".join(ref_words), " ".join(hyp_words)) / max(1, len(ref_words))
|
| 426 |
-
return cer, wer
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
def parse_output(output) -> str:
|
| 430 |
-
if output is None:
|
| 431 |
-
return ""
|
| 432 |
-
if isinstance(output, str):
|
| 433 |
-
return output.strip()
|
| 434 |
-
if isinstance(output, bytes):
|
| 435 |
-
return output.decode("utf-8", errors="ignore").strip()
|
| 436 |
-
if isinstance(output, list):
|
| 437 |
-
if not output:
|
| 438 |
-
return ""
|
| 439 |
-
if len(output) == 1:
|
| 440 |
-
return parse_output(output[0])
|
| 441 |
-
texts = [parse_output(item) for item in output if parse_output(item)]
|
| 442 |
-
return "\n\n".join(texts)
|
| 443 |
-
if isinstance(output, dict):
|
| 444 |
-
for key in ("text", "generated_text", "answer", "output", "prediction"):
|
| 445 |
-
if key in output and isinstance(output[key], str):
|
| 446 |
-
return output[key].strip()
|
| 447 |
-
if "choices" in output and isinstance(output["choices"], list):
|
| 448 |
-
choice0 = output["choices"][0]
|
| 449 |
-
if isinstance(choice0, dict) and "message" in choice0:
|
| 450 |
-
msg = choice0["message"]
|
| 451 |
-
if isinstance(msg, dict) and isinstance(msg.get("content"), str):
|
| 452 |
-
return msg["content"].strip()
|
| 453 |
-
return json.dumps(output, ensure_ascii=False)
|
| 454 |
-
if hasattr(output, "choices"):
|
| 455 |
-
choice = output.choices[0]
|
| 456 |
-
if hasattr(choice, "message") and hasattr(choice.message, "content"):
|
| 457 |
-
return (choice.message.content or "").strip()
|
| 458 |
-
if isinstance(choice, dict) and isinstance(choice.get("message"), dict):
|
| 459 |
-
return str(choice["message"].get("content", "")).strip()
|
| 460 |
-
return str(output)
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
def _sanitize_hf_token(hf_token: str) -> str:
|
| 464 |
-
if hf_token:
|
| 465 |
-
return hf_token.strip()
|
| 466 |
-
return os.getenv("HF_TOKEN", "").strip()
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
def _model_id_value(entry: dict) -> str:
|
| 470 |
-
return str(entry.get("model_id", "") or "").strip()
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
def _model_id_norm(entry: dict) -> str:
|
| 474 |
-
return _model_id_value(entry).lower()
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
def _model_requires_trust_remote_code(entry: dict) -> bool:
|
| 478 |
-
if entry.get("trust_remote_code") is True:
|
| 479 |
-
return True
|
| 480 |
-
model_id = _model_id_norm(entry)
|
| 481 |
-
if not model_id:
|
| 482 |
-
return False
|
| 483 |
-
if model_id in TRUST_REMOTE_CODE_MODELS:
|
| 484 |
-
return True
|
| 485 |
-
return False
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
def _model_pipeline_task(entry: dict) -> str:
|
| 489 |
-
task = (entry.get("pipeline_task") or entry.get("task") or "").strip().lower()
|
| 490 |
-
if task and task != "auto":
|
| 491 |
-
return task
|
| 492 |
-
model_id = _model_id_norm(entry)
|
| 493 |
-
return PIPELINE_TASK_HINTS.get(model_id, "auto")
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
def _precheck_cache_key(entry: dict, hf_token: str) -> str:
|
| 497 |
-
return f"{_model_id_norm(entry)}|{_sanitize_hf_token(hf_token) or 'anon'}"
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
def _classify_precheck_exception(exc: Exception) -> str:
|
| 501 |
-
text = str(exc).lower()
|
| 502 |
-
if "gated repo" in text or "requires authentication" in text or "401" in text:
|
| 503 |
-
return "Repository is gated. Provide a valid HF token with access."
|
| 504 |
-
if "403" in text or "authorization" in text:
|
| 505 |
-
return "Authorization failed for this repository. HF token may be missing or lacks access."
|
| 506 |
-
if "qwen3_5" in text or "gemma4_unified" in text:
|
| 507 |
-
return (
|
| 508 |
-
"Model architecture requires a newer Transformers runtime than this Space currently has. "
|
| 509 |
-
"Rebuild after updating transformers from source."
|
| 510 |
-
)
|
| 511 |
-
if "tokenizersbackend" in text:
|
| 512 |
-
return (
|
| 513 |
-
"Tokenizer backend unavailable in this environment. Rebuild after dependency refresh "
|
| 514 |
-
"(tokenizers / transformers)."
|
| 515 |
-
)
|
| 516 |
-
if "404" in text or "not found" in text or "config.json" in text:
|
| 517 |
-
return "Model config is not available in standard Transformers format."
|
| 518 |
-
if "could not infer task" in text or "document-question-answering" in text:
|
| 519 |
-
return "Model task/config mismatch for current local Transformers runtime."
|
| 520 |
-
return _format_dependency_error(exc)
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
def _get_model_precheck(entry: dict, hf_token: str) -> Tuple[bool, str]:
|
| 524 |
-
provider = str(entry.get("provider", "local_transformer")).lower()
|
| 525 |
-
if provider != "local_transformer":
|
| 526 |
-
return True, ""
|
| 527 |
-
model_id = _model_id_norm(entry)
|
| 528 |
-
if not model_id:
|
| 529 |
-
return False, "Missing model_id."
|
| 530 |
-
if model_id in NON_HF_OR_NON_TRANSFORMERS_MODELS:
|
| 531 |
-
return False, NON_HF_OR_NON_TRANSFORMERS_MODELS[model_id]
|
| 532 |
-
|
| 533 |
-
cache_key = _precheck_cache_key(entry, hf_token)
|
| 534 |
-
with _MODEL_PRECHECK_LOCK:
|
| 535 |
-
cached = MODEL_PRECHECK_CACHE.get(cache_key)
|
| 536 |
-
if cached is not None:
|
| 537 |
-
return cached
|
| 538 |
-
|
| 539 |
-
if AutoConfig is None:
|
| 540 |
-
result = (False, "Transformers AutoConfig is not available in this runtime.")
|
| 541 |
-
with _MODEL_PRECHECK_LOCK:
|
| 542 |
-
MODEL_PRECHECK_CACHE[cache_key] = result
|
| 543 |
-
return result
|
| 544 |
-
|
| 545 |
-
token = _sanitize_hf_token(hf_token) or None
|
| 546 |
-
try:
|
| 547 |
-
# Lightweight preflight to reject incompatible repos before heavy pipeline/model loading.
|
| 548 |
-
AutoConfig.from_pretrained(
|
| 549 |
-
model_id,
|
| 550 |
-
token=token,
|
| 551 |
-
trust_remote_code=_model_requires_trust_remote_code(entry),
|
| 552 |
-
)
|
| 553 |
-
except Exception as exc: # pragma: no cover
|
| 554 |
-
result = (False, _classify_precheck_exception(exc))
|
| 555 |
-
with _MODEL_PRECHECK_LOCK:
|
| 556 |
-
MODEL_PRECHECK_CACHE[cache_key] = result
|
| 557 |
-
return result
|
| 558 |
-
|
| 559 |
-
result = (True, "")
|
| 560 |
-
with _MODEL_PRECHECK_LOCK:
|
| 561 |
-
MODEL_PRECHECK_CACHE[cache_key] = result
|
| 562 |
-
return result
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
def _transformers_runtime_message() -> Optional[str]:
|
| 566 |
-
if hf_pipeline is not None:
|
| 567 |
-
return None
|
| 568 |
-
if TRANSFORMERS_IMPORT_ERROR:
|
| 569 |
-
return f"transformers import failed in this Space: {TRANSFORMERS_IMPORT_ERROR}"
|
| 570 |
-
return "transformers is not installed in this Space."
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
def _format_dependency_error(exc: Exception) -> str:
|
| 574 |
-
text = str(exc)
|
| 575 |
-
if "TokenizersBackend" in text and "not currently imported" in text:
|
| 576 |
-
return (
|
| 577 |
-
"Tokenizer backend import failed. "
|
| 578 |
-
"Rebuild after updating `tokenizers` and `transformers` in requirements."
|
| 579 |
-
)
|
| 580 |
-
if "requires the following packages" in text and "addict" in text:
|
| 581 |
-
return "Missing dependency: addict. Rebuild after adding `addict` to requirements."
|
| 582 |
-
if "requires the following packages" in text and "torchvision" in text:
|
| 583 |
-
return "Missing dependency: torchvision. Rebuild after adding `torchvision` to requirements."
|
| 584 |
-
return text
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
def _runtime_dependency_warning() -> Optional[str]:
|
| 588 |
-
messages = []
|
| 589 |
-
if pytesseract is None:
|
| 590 |
-
messages.append("pytesseract is not installed. Install with `pytesseract` in requirements.")
|
| 591 |
-
if pytesseract is not None and not _tesseract_binary_available():
|
| 592 |
-
messages.append(
|
| 593 |
-
"The tesseract binary is not available in PATH. Add tesseract packages to `apt.txt` and rebuild."
|
| 594 |
-
)
|
| 595 |
-
if torch is None:
|
| 596 |
-
messages.append("PyTorch is not installed; local models cannot run.")
|
| 597 |
-
return " | ".join(messages) if messages else None
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
def _required_token_message(entry: dict, hf_token: str) -> Optional[str]:
|
| 601 |
-
model_id = _model_id_norm(entry)
|
| 602 |
-
if model_id in GATED_MODELS_REQUIRING_TOKEN and not _sanitize_hf_token(hf_token):
|
| 603 |
-
return (
|
| 604 |
-
f"{entry.get('name', model_id)} appears to require an HF token in this space. "
|
| 605 |
-
"Set HF_TOKEN to a token with access to the gated model."
|
| 606 |
-
)
|
| 607 |
-
return None
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
def _tesseract_binary_available() -> bool:
|
| 611 |
-
import shutil
|
| 612 |
-
|
| 613 |
-
for candidate in TESSERACT_EXECUTABLE_PATHS:
|
| 614 |
-
if os.path.exists(candidate):
|
| 615 |
-
return True
|
| 616 |
-
if shutil.which("tesseract"):
|
| 617 |
-
return True
|
| 618 |
-
return False
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
def _decode_image(image_bytes: bytes) -> Image.Image:
|
| 622 |
-
image = Image.open(BytesIO(image_bytes))
|
| 623 |
-
if image.mode != "RGB":
|
| 624 |
-
image = image.convert("RGB")
|
| 625 |
-
return image
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
def _load_image_bytes(image_file) -> bytes:
|
| 629 |
-
if image_file is None:
|
| 630 |
-
raise RuntimeError("No file was uploaded.")
|
| 631 |
-
if isinstance(image_file, (list, tuple)):
|
| 632 |
-
if not image_file:
|
| 633 |
-
raise RuntimeError("No file was uploaded.")
|
| 634 |
-
image_file = image_file[0]
|
| 635 |
-
|
| 636 |
-
if isinstance(image_file, dict):
|
| 637 |
-
image_file = (
|
| 638 |
-
image_file.get("path")
|
| 639 |
-
or image_file.get("name")
|
| 640 |
-
or image_file.get("url")
|
| 641 |
-
or image_file.get("file_path")
|
| 642 |
-
)
|
| 643 |
-
|
| 644 |
-
if isinstance(image_file, bytes):
|
| 645 |
-
return image_file
|
| 646 |
-
|
| 647 |
-
if hasattr(image_file, "read"):
|
| 648 |
-
try:
|
| 649 |
-
image_bytes = image_file.read()
|
| 650 |
-
if image_bytes:
|
| 651 |
-
return image_bytes
|
| 652 |
-
except Exception as exc:
|
| 653 |
-
raise RuntimeError(f"Could not read uploaded file object: {exc}") from exc
|
| 654 |
-
raise RuntimeError("Uploaded file object is empty.")
|
| 655 |
-
|
| 656 |
-
if isinstance(image_file, Image.Image):
|
| 657 |
-
buffer = BytesIO()
|
| 658 |
-
image_file.convert("RGB").save(buffer, format="PNG")
|
| 659 |
-
return buffer.getvalue()
|
| 660 |
-
|
| 661 |
-
if not isinstance(image_file, str):
|
| 662 |
-
if hasattr(image_file, "name") and isinstance(getattr(image_file, "name"), str):
|
| 663 |
-
image_file = getattr(image_file, "name")
|
| 664 |
-
else:
|
| 665 |
-
raise RuntimeError("Uploaded image is not a valid file path.")
|
| 666 |
-
|
| 667 |
-
if not os.path.exists(image_file):
|
| 668 |
-
raise RuntimeError("Uploaded file is missing from disk.")
|
| 669 |
-
|
| 670 |
-
filename = os.path.basename(image_file).lower()
|
| 671 |
-
if filename.endswith(".pdf"):
|
| 672 |
-
raise RuntimeError(
|
| 673 |
-
"PDF files are not supported yet in this Space build. Export the document to PNG/JPG and upload again."
|
| 674 |
-
)
|
| 675 |
-
|
| 676 |
-
try:
|
| 677 |
-
with open(image_file, "rb") as f:
|
| 678 |
-
return f.read()
|
| 679 |
-
except Exception as exc:
|
| 680 |
-
raise RuntimeError(f"Could not read uploaded file: {exc}") from exc
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
def _normalize_model_selection(selected_model_ids) -> List[str]:
|
| 684 |
-
if selected_model_ids is None:
|
| 685 |
-
return []
|
| 686 |
-
if isinstance(selected_model_ids, str):
|
| 687 |
-
try:
|
| 688 |
-
loaded = json.loads(selected_model_ids)
|
| 689 |
-
if isinstance(loaded, list):
|
| 690 |
-
selected_model_ids = loaded
|
| 691 |
-
else:
|
| 692 |
-
selected_model_ids = [selected_model_ids]
|
| 693 |
-
except Exception:
|
| 694 |
-
if "," in selected_model_ids:
|
| 695 |
-
parts = [entry.strip() for entry in selected_model_ids.split(",") if entry.strip()]
|
| 696 |
-
selected_model_ids = parts
|
| 697 |
-
else:
|
| 698 |
-
selected_model_ids = [selected_model_ids]
|
| 699 |
-
elif isinstance(selected_model_ids, set):
|
| 700 |
-
selected_model_ids = list(selected_model_ids)
|
| 701 |
-
elif not isinstance(selected_model_ids, (list, tuple)):
|
| 702 |
-
selected_model_ids = [str(selected_model_ids)]
|
| 703 |
-
|
| 704 |
-
return [str(item) for item in selected_model_ids if item]
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
def _build_local_pipeline(model_id: str, hf_token: str, task: str, trust_remote_code: bool = False):
|
| 708 |
-
if hf_pipeline is None:
|
| 709 |
-
raise RuntimeError(_transformers_runtime_message() or "transformers is not installed in this space.")
|
| 710 |
-
|
| 711 |
-
token = _sanitize_hf_token(hf_token) or None
|
| 712 |
-
device = _inference_device()
|
| 713 |
-
strict_cuda = _strict_cuda_required() and torch is not None and torch.cuda.is_available()
|
| 714 |
-
if strict_cuda:
|
| 715 |
-
_ensure_cuda_available(f"Local pipeline load for {model_id}")
|
| 716 |
-
base_kwargs = {"model": model_id, "token": token, "trust_remote_code": trust_remote_code}
|
| 717 |
-
if device >= 0:
|
| 718 |
-
base_kwargs["device"] = device
|
| 719 |
-
|
| 720 |
-
if task and task != "auto":
|
| 721 |
-
task_candidates = [task]
|
| 722 |
-
else:
|
| 723 |
-
task_candidates = [
|
| 724 |
-
"image-to-text",
|
| 725 |
-
"image-text-to-text",
|
| 726 |
-
"document-question-answering",
|
| 727 |
-
]
|
| 728 |
-
|
| 729 |
-
seen_tasks = set()
|
| 730 |
-
task_candidates = [t for t in task_candidates if t and not (t in seen_tasks or seen_tasks.add(t))]
|
| 731 |
-
last_error = None
|
| 732 |
-
|
| 733 |
-
preferred_dtype, dtype_chain = _inference_torch_kwargs_for_model(strict_cuda=strict_cuda)
|
| 734 |
-
if preferred_dtype is not None:
|
| 735 |
-
base_kwargs["torch_dtype"] = preferred_dtype
|
| 736 |
-
|
| 737 |
-
for candidate_task in task_candidates:
|
| 738 |
-
for dtype_kwargs in dtype_chain:
|
| 739 |
-
cleaned_kwargs = {k: v for k, v in {**base_kwargs, **dtype_kwargs}.items() if v is not None}
|
| 740 |
-
try:
|
| 741 |
-
candidate_pipeline = hf_pipeline(candidate_task, **cleaned_kwargs)
|
| 742 |
-
if strict_cuda:
|
| 743 |
-
candidate_model = getattr(candidate_pipeline, "model", None)
|
| 744 |
-
if candidate_model is not None and torch is not None and torch.cuda.is_available():
|
| 745 |
-
try:
|
| 746 |
-
candidate_model = candidate_model.to("cuda")
|
| 747 |
-
if hasattr(candidate_pipeline, "model"):
|
| 748 |
-
candidate_pipeline.model = candidate_model
|
| 749 |
-
except Exception:
|
| 750 |
-
pass
|
| 751 |
-
_assert_model_on_cuda(candidate_model, model_id, f"pipeline load for task {candidate_task}")
|
| 752 |
-
return candidate_pipeline
|
| 753 |
-
except Exception as exc: # pragma: no cover
|
| 754 |
-
last_error = exc
|
| 755 |
-
|
| 756 |
-
if task and task != "auto":
|
| 757 |
-
return _build_local_pipeline(model_id, hf_token, "auto", trust_remote_code=trust_remote_code)
|
| 758 |
-
|
| 759 |
-
raise RuntimeError(f"Failed to load local pipeline for {model_id}: {last_error}")
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
def _load_direct_components(model_id: str, hf_token: str, trust_remote_code: bool = False):
|
| 763 |
-
if AutoProcessor is None and AutoImageProcessor is None and AutoTokenizer is None:
|
| 764 |
-
return None, None
|
| 765 |
-
token = _sanitize_hf_token(hf_token) or None
|
| 766 |
-
component = (model_id, trust_remote_code)
|
| 767 |
-
with _LOCAL_DIRECT_LOCK:
|
| 768 |
-
cached = _LOCAL_DIRECT_CACHE.get(component)
|
| 769 |
-
if cached is not None:
|
| 770 |
-
_LOCAL_DIRECT_CACHE.move_to_end(component)
|
| 771 |
-
return cached
|
| 772 |
-
|
| 773 |
-
processor = None
|
| 774 |
-
processor_error: Optional[Exception] = None
|
| 775 |
-
for processor_ctor in [AutoProcessor, AutoImageProcessor]:
|
| 776 |
-
if processor_ctor is None:
|
| 777 |
-
continue
|
| 778 |
-
try:
|
| 779 |
-
processor = processor_ctor.from_pretrained(
|
| 780 |
-
model_id,
|
| 781 |
-
token=token,
|
| 782 |
-
trust_remote_code=trust_remote_code,
|
| 783 |
-
)
|
| 784 |
-
break
|
| 785 |
-
except Exception as exc: # pragma: no cover
|
| 786 |
-
processor_error = exc
|
| 787 |
-
|
| 788 |
-
if processor is None:
|
| 789 |
-
if AutoTokenizer is not None:
|
| 790 |
-
try:
|
| 791 |
-
processor = AutoTokenizer.from_pretrained(model_id, token=token, trust_remote_code=trust_remote_code)
|
| 792 |
-
except Exception as exc: # pragma: no cover
|
| 793 |
-
if processor_error is None:
|
| 794 |
-
processor_error = exc
|
| 795 |
-
if processor is None:
|
| 796 |
-
error_message = (
|
| 797 |
-
f"Failed to load processor for {model_id}: {processor_error}"
|
| 798 |
-
if processor_error is not None
|
| 799 |
-
else f"Failed to load processor for {model_id}: no compatible processor classes available."
|
| 800 |
-
)
|
| 801 |
-
raise RuntimeError(error_message)
|
| 802 |
-
|
| 803 |
-
strict_cuda = _strict_cuda_required() and torch is not None and torch.cuda.is_available()
|
| 804 |
-
if strict_cuda:
|
| 805 |
-
_ensure_cuda_available(f"Direct model load for {model_id}")
|
| 806 |
-
|
| 807 |
-
if torch is None:
|
| 808 |
-
raise RuntimeError("PyTorch is required for direct model loading.")
|
| 809 |
-
|
| 810 |
-
model_errors = []
|
| 811 |
-
model = None
|
| 812 |
-
preferred_dtype, model_kwargs_chain = _inference_torch_kwargs_for_model(strict_cuda=strict_cuda)
|
| 813 |
-
if preferred_dtype is not None:
|
| 814 |
-
base_kwargs = {
|
| 815 |
-
"token": token,
|
| 816 |
-
"trust_remote_code": trust_remote_code,
|
| 817 |
-
}
|
| 818 |
-
else:
|
| 819 |
-
base_kwargs = {
|
| 820 |
-
"token": token,
|
| 821 |
-
"trust_remote_code": trust_remote_code,
|
| 822 |
-
}
|
| 823 |
-
model_classes = [
|
| 824 |
-
AutoModelForImageTextToText,
|
| 825 |
-
AutoModelForVision2Seq,
|
| 826 |
-
AutoModelForVisionEncoderDecoder,
|
| 827 |
-
AutoModelForSeq2SeqLM,
|
| 828 |
-
AutoModelForCausalLM,
|
| 829 |
-
AutoModelForConditionalGeneration,
|
| 830 |
-
AutoModelForDocumentQuestionAnswering,
|
| 831 |
-
AutoModel,
|
| 832 |
-
]
|
| 833 |
-
for model_class in model_classes:
|
| 834 |
-
if model_class is None:
|
| 835 |
-
continue
|
| 836 |
-
try:
|
| 837 |
-
for model_kwargs in model_kwargs_chain:
|
| 838 |
-
candidate_kwargs = {k: v for k, v in {**base_kwargs, **model_kwargs}.items() if v is not None}
|
| 839 |
-
try:
|
| 840 |
-
model = model_class.from_pretrained(
|
| 841 |
-
model_id,
|
| 842 |
-
**candidate_kwargs,
|
| 843 |
-
)
|
| 844 |
-
break
|
| 845 |
-
except TypeError as exc:
|
| 846 |
-
model_errors.append(f"{getattr(model_class, '__name__', str(model_class))}: {exc}")
|
| 847 |
-
continue
|
| 848 |
-
except Exception as exc: # pragma: no cover
|
| 849 |
-
model_errors.append(f"{getattr(model_class, '__name__', str(model_class))}: {exc}")
|
| 850 |
-
if model is not None:
|
| 851 |
-
break
|
| 852 |
-
except Exception as exc: # pragma: no cover
|
| 853 |
-
model_errors.append(f"{getattr(model_class, '__name__', str(model_class))}: {exc}")
|
| 854 |
-
|
| 855 |
-
if model is None:
|
| 856 |
-
raise RuntimeError(f"Direct loading failed for {model_id}. " + " | ".join(model_errors))
|
| 857 |
-
|
| 858 |
-
if strict_cuda:
|
| 859 |
-
try:
|
| 860 |
-
model = model.to("cuda")
|
| 861 |
-
except Exception as exc:
|
| 862 |
-
raise RuntimeError(f"Failed to place {model_id} on CUDA: {exc}")
|
| 863 |
-
_assert_model_on_cuda(model, model_id, "direct model load")
|
| 864 |
-
elif torch.cuda.is_available():
|
| 865 |
-
model = model.to("cuda")
|
| 866 |
-
|
| 867 |
-
with _LOCAL_DIRECT_LOCK:
|
| 868 |
-
_LOCAL_DIRECT_CACHE[component] = (model, processor)
|
| 869 |
-
if len(_LOCAL_DIRECT_CACHE) > DIRECT_MODEL_CACHE_MAX:
|
| 870 |
-
_, evicted = _LOCAL_DIRECT_CACHE.popitem(last=False)
|
| 871 |
-
try:
|
| 872 |
-
model_ref, _ = evicted
|
| 873 |
-
del model_ref
|
| 874 |
-
except Exception:
|
| 875 |
-
pass
|
| 876 |
-
if torch is not None and torch.cuda.is_available():
|
| 877 |
-
torch.cuda.empty_cache()
|
| 878 |
-
return model, processor
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
def _direct_infer(model_id: str, image_bytes: bytes, prompt: str, hf_token: str, params: dict, trust_remote_code: bool = False) -> str:
|
| 882 |
-
if torch is None:
|
| 883 |
-
raise RuntimeError("PyTorch is required for direct model inference.")
|
| 884 |
-
model, processor = _load_direct_components(model_id, hf_token, trust_remote_code=trust_remote_code)
|
| 885 |
-
image = _decode_image(image_bytes)
|
| 886 |
-
|
| 887 |
-
input_candidates = []
|
| 888 |
-
if prompt:
|
| 889 |
-
input_candidates.extend(
|
| 890 |
-
[
|
| 891 |
-
lambda: processor(text=prompt, images=image, return_tensors="pt"),
|
| 892 |
-
lambda: processor(images=image, text=prompt, return_tensors="pt"),
|
| 893 |
-
lambda: processor(prompt, image, return_tensors="pt"),
|
| 894 |
-
lambda: processor(prompt, return_tensors="pt"),
|
| 895 |
-
]
|
| 896 |
-
)
|
| 897 |
-
input_candidates.append(lambda: processor(images=image, return_tensors="pt"))
|
| 898 |
-
|
| 899 |
-
prepared_inputs = None
|
| 900 |
-
prep_error = None
|
| 901 |
-
for builder in input_candidates:
|
| 902 |
-
try:
|
| 903 |
-
candidate = builder()
|
| 904 |
-
if isinstance(candidate, Mapping) and candidate:
|
| 905 |
-
prepared_inputs = {}
|
| 906 |
-
for key, value in candidate.items():
|
| 907 |
-
if hasattr(value, "to"):
|
| 908 |
-
prepared_inputs[key] = value.to(model.device)
|
| 909 |
-
elif isinstance(value, (list, tuple)):
|
| 910 |
-
prepared_inputs[key] = value
|
| 911 |
-
if "images" in prepared_inputs and "pixel_values" not in prepared_inputs:
|
| 912 |
-
prepared_inputs["pixel_values"] = prepared_inputs["images"]
|
| 913 |
-
prepared_inputs.pop("images", None)
|
| 914 |
-
if prepared_inputs:
|
| 915 |
-
break
|
| 916 |
-
except Exception as exc: # pragma: no cover
|
| 917 |
-
prep_error = exc
|
| 918 |
-
|
| 919 |
-
if prepared_inputs is None:
|
| 920 |
-
raise RuntimeError(f"Could not prepare inputs for {model_id}: {prep_error}")
|
| 921 |
-
|
| 922 |
-
gen_kwargs = {}
|
| 923 |
-
if "max_new_tokens" in params:
|
| 924 |
-
gen_kwargs["max_new_tokens"] = params["max_new_tokens"]
|
| 925 |
-
if "temperature" in params:
|
| 926 |
-
gen_kwargs["temperature"] = params["temperature"]
|
| 927 |
-
if "top_p" in params:
|
| 928 |
-
gen_kwargs["top_p"] = params["top_p"]
|
| 929 |
-
if "top_k" in params:
|
| 930 |
-
gen_kwargs["top_k"] = params["top_k"]
|
| 931 |
-
if "do_sample" in params:
|
| 932 |
-
gen_kwargs["do_sample"] = params["do_sample"]
|
| 933 |
-
if "num_beams" in params:
|
| 934 |
-
gen_kwargs["num_beams"] = params["num_beams"]
|
| 935 |
-
if not gen_kwargs and hasattr(model, "generation_config"):
|
| 936 |
-
try:
|
| 937 |
-
gen_kwargs = {
|
| 938 |
-
"max_new_tokens": getattr(model.generation_config, "max_new_tokens", None),
|
| 939 |
-
"temperature": getattr(model.generation_config, "temperature", None),
|
| 940 |
-
"top_p": getattr(model.generation_config, "top_p", None),
|
| 941 |
-
"top_k": getattr(model.generation_config, "top_k", None),
|
| 942 |
-
"num_beams": getattr(model.generation_config, "num_beams", None),
|
| 943 |
-
}
|
| 944 |
-
gen_kwargs = {k: v for k, v in gen_kwargs.items() if v is not None}
|
| 945 |
-
except Exception:
|
| 946 |
-
gen_kwargs = {}
|
| 947 |
-
|
| 948 |
-
with torch.no_grad():
|
| 949 |
-
generated = model.generate(**prepared_inputs, **gen_kwargs)
|
| 950 |
-
|
| 951 |
-
if isinstance(generated, torch.Tensor):
|
| 952 |
-
decoded = processor.batch_decode(generated, skip_special_tokens=True)
|
| 953 |
-
return parse_output(decoded)
|
| 954 |
-
if isinstance(generated, (list, tuple)):
|
| 955 |
-
return parse_output(generated)
|
| 956 |
-
return parse_output(str(generated))
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
def _get_local_pipeline(model_id: str, hf_token: str, task: str, trust_remote_code: bool = False):
|
| 960 |
-
device = "gpu" if (torch is not None and torch.cuda.is_available()) else "cpu"
|
| 961 |
-
dtype = _inference_dtype()
|
| 962 |
-
key = _pipeline_cache_key(model_id, task or "auto", device, dtype, trust_remote_code)
|
| 963 |
-
with _LOCAL_PIPELINE_LOCK:
|
| 964 |
-
cached = _LOCAL_PIPELINE_CACHE.get(key)
|
| 965 |
-
if cached is not None:
|
| 966 |
-
_LOCAL_PIPELINE_CACHE.move_to_end(key)
|
| 967 |
-
return cached
|
| 968 |
-
|
| 969 |
-
pipeline = _build_local_pipeline(model_id, hf_token, task, trust_remote_code=trust_remote_code)
|
| 970 |
-
|
| 971 |
-
with _LOCAL_PIPELINE_LOCK:
|
| 972 |
-
_LOCAL_PIPELINE_CACHE[key] = pipeline
|
| 973 |
-
if len(_LOCAL_PIPELINE_CACHE) > LOCAL_PIPELINE_CACHE_MAX:
|
| 974 |
-
_, evicted = _LOCAL_PIPELINE_CACHE.popitem(last=False)
|
| 975 |
-
try:
|
| 976 |
-
model = getattr(evicted, "model", None)
|
| 977 |
-
if model is not None:
|
| 978 |
-
del model
|
| 979 |
-
del evicted
|
| 980 |
-
except Exception:
|
| 981 |
-
pass
|
| 982 |
-
if torch is not None and torch.cuda.is_available():
|
| 983 |
-
torch.cuda.empty_cache()
|
| 984 |
-
gc.collect()
|
| 985 |
-
gc.collect()
|
| 986 |
-
return pipeline
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
def _clear_local_pipeline_cache() -> None:
|
| 990 |
-
global _LOCAL_PIPELINE_CACHE
|
| 991 |
-
with _LOCAL_PIPELINE_LOCK:
|
| 992 |
-
pipelines = list(_LOCAL_PIPELINE_CACHE.values())
|
| 993 |
-
_LOCAL_PIPELINE_CACHE.clear()
|
| 994 |
-
for pipeline in pipelines:
|
| 995 |
-
try:
|
| 996 |
-
if hasattr(pipeline, "model"):
|
| 997 |
-
del pipeline.model
|
| 998 |
-
if hasattr(pipeline, "processor"):
|
| 999 |
-
del pipeline.processor
|
| 1000 |
-
if hasattr(pipeline, "tokenizer"):
|
| 1001 |
-
del pipeline.tokenizer
|
| 1002 |
-
except Exception:
|
| 1003 |
-
pass
|
| 1004 |
-
gc.collect()
|
| 1005 |
-
if torch is not None and torch.cuda.is_available():
|
| 1006 |
-
torch.cuda.empty_cache()
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
def _clear_direct_cache() -> None:
|
| 1010 |
-
global _LOCAL_DIRECT_CACHE
|
| 1011 |
-
with _LOCAL_DIRECT_LOCK:
|
| 1012 |
-
directs = list(_LOCAL_DIRECT_CACHE.values())
|
| 1013 |
-
_LOCAL_DIRECT_CACHE.clear()
|
| 1014 |
-
for model, _ in directs:
|
| 1015 |
-
try:
|
| 1016 |
-
del model
|
| 1017 |
-
except Exception:
|
| 1018 |
-
pass
|
| 1019 |
-
gc.collect()
|
| 1020 |
-
if torch is not None and torch.cuda.is_available():
|
| 1021 |
-
torch.cuda.empty_cache()
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
def run_local_model(entry: dict, image_bytes: bytes, prompt: str, hf_token: str) -> str:
|
| 1025 |
-
if hf_pipeline is None:
|
| 1026 |
-
raise RuntimeError(_transformers_runtime_message() or "transformers is not installed in this space.")
|
| 1027 |
-
if torch is None:
|
| 1028 |
-
raise RuntimeError("PyTorch is required for local model inference.")
|
| 1029 |
-
|
| 1030 |
-
model_id = entry["model_id"]
|
| 1031 |
-
task = _model_pipeline_task(entry)
|
| 1032 |
-
trust_remote_code = _model_requires_trust_remote_code(entry)
|
| 1033 |
-
if _strict_cuda_required():
|
| 1034 |
-
_ensure_cuda_available(f"Local OCR execution for {model_id}")
|
| 1035 |
-
try:
|
| 1036 |
-
pipeline = _get_local_pipeline(model_id, hf_token, task, trust_remote_code=trust_remote_code)
|
| 1037 |
-
except Exception as exc:
|
| 1038 |
-
pipeline = None
|
| 1039 |
-
pipeline_error = exc
|
| 1040 |
-
else:
|
| 1041 |
-
pipeline_error = None
|
| 1042 |
-
params = sanitize_generation_params(entry.get("parameters", {}))
|
| 1043 |
-
image = _decode_image(image_bytes)
|
| 1044 |
-
|
| 1045 |
-
if "extra_body" in params and isinstance(params["extra_body"], Mapping):
|
| 1046 |
-
body = params.pop("extra_body")
|
| 1047 |
-
if isinstance(body, Mapping):
|
| 1048 |
-
supported_body_keys = {
|
| 1049 |
-
"top_p",
|
| 1050 |
-
"top_k",
|
| 1051 |
-
"temperature",
|
| 1052 |
-
"do_sample",
|
| 1053 |
-
"num_beams",
|
| 1054 |
-
"repetition_penalty",
|
| 1055 |
-
"max_new_tokens",
|
| 1056 |
-
"max_length",
|
| 1057 |
-
"min_length",
|
| 1058 |
-
"enable_thinking",
|
| 1059 |
-
"seed",
|
| 1060 |
-
"return_dict_in_generate",
|
| 1061 |
-
"output_scores",
|
| 1062 |
-
}
|
| 1063 |
-
for key, value in body.items():
|
| 1064 |
-
if isinstance(key, str) and key in supported_body_keys:
|
| 1065 |
-
params.setdefault(key, value)
|
| 1066 |
-
|
| 1067 |
-
if pipeline is not None:
|
| 1068 |
-
try:
|
| 1069 |
-
if _strict_cuda_required():
|
| 1070 |
-
_assert_model_on_cuda(getattr(pipeline, "model", None), model_id, "pipeline execution")
|
| 1071 |
-
else:
|
| 1072 |
-
pipeline_model = getattr(pipeline, "model", None)
|
| 1073 |
-
if pipeline_model is not None and torch is not None and torch.cuda.is_available():
|
| 1074 |
-
pipeline_model = pipeline_model.to("cuda")
|
| 1075 |
-
if hasattr(pipeline, "model"):
|
| 1076 |
-
pipeline.model = pipeline_model
|
| 1077 |
-
except Exception:
|
| 1078 |
-
# Keep the failure path resilient; direct fallback may still succeed.
|
| 1079 |
-
pass
|
| 1080 |
-
|
| 1081 |
-
calls = []
|
| 1082 |
-
if prompt:
|
| 1083 |
-
calls.extend(
|
| 1084 |
-
[
|
| 1085 |
-
lambda: pipeline({"image": image, "text": prompt}, **params),
|
| 1086 |
-
lambda: pipeline({"image": image, "question": prompt}, **params),
|
| 1087 |
-
lambda: pipeline({"text": prompt, "image": image}, **params),
|
| 1088 |
-
lambda: pipeline({"images": image, "text": prompt}, **params),
|
| 1089 |
-
lambda: pipeline(image, text=prompt, **params),
|
| 1090 |
-
lambda: pipeline(image, question=prompt, **params),
|
| 1091 |
-
lambda: pipeline(image, **params),
|
| 1092 |
-
lambda: pipeline({"image": image}, **params),
|
| 1093 |
-
]
|
| 1094 |
-
)
|
| 1095 |
-
else:
|
| 1096 |
-
calls.extend([lambda: pipeline(image, **params), lambda: pipeline({"image": image}, **params)])
|
| 1097 |
-
|
| 1098 |
-
for call in calls:
|
| 1099 |
-
try:
|
| 1100 |
-
output = call()
|
| 1101 |
-
parsed = parse_output(output)
|
| 1102 |
-
if parsed:
|
| 1103 |
-
return parsed
|
| 1104 |
-
except Exception as exc: # pragma: no cover
|
| 1105 |
-
pipeline_error = _format_dependency_error(exc)
|
| 1106 |
-
continue
|
| 1107 |
-
|
| 1108 |
-
try:
|
| 1109 |
-
direct_output = _direct_infer(
|
| 1110 |
-
model_id,
|
| 1111 |
-
image_bytes,
|
| 1112 |
-
prompt,
|
| 1113 |
-
hf_token,
|
| 1114 |
-
params,
|
| 1115 |
-
trust_remote_code=trust_remote_code,
|
| 1116 |
-
)
|
| 1117 |
-
parsed = parse_output(direct_output)
|
| 1118 |
-
if parsed:
|
| 1119 |
-
return parsed
|
| 1120 |
-
except Exception as exc: # pragma: no cover
|
| 1121 |
-
if pipeline_error is None:
|
| 1122 |
-
pipeline_error = _format_dependency_error(exc)
|
| 1123 |
-
else:
|
| 1124 |
-
pipeline_error = RuntimeError(f"{pipeline_error}; direct fallback failed: {_format_dependency_error(exc)}")
|
| 1125 |
-
|
| 1126 |
-
raise RuntimeError(f"Local model inference failed for {model_id}: {pipeline_error}")
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
def run_tesseract(image_bytes: bytes, params: dict) -> str:
|
| 1130 |
-
if pytesseract is None:
|
| 1131 |
-
raise RuntimeError("pytesseract is not installed in this Space.")
|
| 1132 |
-
if not _tesseract_binary_available():
|
| 1133 |
-
raise RuntimeError(
|
| 1134 |
-
"pytesseract is installed but the tesseract executable is not available in PATH. "
|
| 1135 |
-
"The Space should install tesseract via apt.txt, please confirm a clean rebuild."
|
| 1136 |
-
)
|
| 1137 |
-
for candidate in TESSERACT_EXECUTABLE_PATHS:
|
| 1138 |
-
if os.path.exists(candidate):
|
| 1139 |
-
try:
|
| 1140 |
-
pytesseract.pytesseract.tesseract_cmd = candidate
|
| 1141 |
-
except Exception:
|
| 1142 |
-
pass
|
| 1143 |
-
image = Image.open(BytesIO(image_bytes))
|
| 1144 |
-
lang = params.get("lang", "heb+eng")
|
| 1145 |
-
psm = params.get("psm", 6)
|
| 1146 |
-
oem = params.get("oem", 1)
|
| 1147 |
-
extra = params.get("extra_config", "").strip()
|
| 1148 |
-
cfg_bits = [f"--psm {int(psm)}", f"--oem {int(oem)}"]
|
| 1149 |
-
if extra:
|
| 1150 |
-
cfg_bits.append(extra)
|
| 1151 |
-
config = " ".join(cfg_bits).strip()
|
| 1152 |
-
return pytesseract.image_to_string(image, lang=lang, config=config).strip()
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
@dataclass
|
| 1156 |
-
class RunnerResult:
|
| 1157 |
-
model_id: str
|
| 1158 |
-
label: str
|
| 1159 |
-
provider: str
|
| 1160 |
-
status: str
|
| 1161 |
-
output: str
|
| 1162 |
-
latency_sec: Optional[float]
|
| 1163 |
-
char_count: int
|
| 1164 |
-
cer: Optional[float]
|
| 1165 |
-
wer: Optional[float]
|
| 1166 |
-
notes: str
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
def sanitize_generation_params(raw: dict) -> dict:
|
| 1170 |
-
params = {}
|
| 1171 |
-
if not raw:
|
| 1172 |
-
return {"max_new_tokens": DEFAULT_MAX_TOKENS}
|
| 1173 |
-
for k, v in raw.items():
|
| 1174 |
-
if v is None:
|
| 1175 |
-
continue
|
| 1176 |
-
if k == "max_new_tokens":
|
| 1177 |
-
requested = int(v)
|
| 1178 |
-
params["max_new_tokens"] = max(DEFAULT_MAX_TOKENS, requested)
|
| 1179 |
-
elif k == "max_tokens":
|
| 1180 |
-
requested = int(v)
|
| 1181 |
-
params["max_new_tokens"] = max(DEFAULT_MAX_TOKENS, requested)
|
| 1182 |
-
elif k == "temperature":
|
| 1183 |
-
params["temperature"] = float(v)
|
| 1184 |
-
elif k == "top_p":
|
| 1185 |
-
params["top_p"] = float(v)
|
| 1186 |
-
elif k == "top_k":
|
| 1187 |
-
params["top_k"] = int(v)
|
| 1188 |
-
elif k == "repetition_penalty":
|
| 1189 |
-
params["repetition_penalty"] = float(v)
|
| 1190 |
-
elif k == "frequency_penalty":
|
| 1191 |
-
params["frequency_penalty"] = float(v)
|
| 1192 |
-
elif k == "presence_penalty":
|
| 1193 |
-
params["presence_penalty"] = float(v)
|
| 1194 |
-
elif k == "seed":
|
| 1195 |
-
params["seed"] = int(v)
|
| 1196 |
-
elif k == "extra_body":
|
| 1197 |
-
params["extra_body"] = v
|
| 1198 |
-
else:
|
| 1199 |
-
params[k] = v
|
| 1200 |
-
return params
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
def run_single_model(entry: dict, image_bytes: bytes, data_uri: str, hf_token: str, timeout_sec: int = 120) -> RunnerResult:
|
| 1204 |
-
provider = entry.get("provider", "local_transformer").lower()
|
| 1205 |
-
if provider != "tesseract":
|
| 1206 |
-
provider = "local_transformer"
|
| 1207 |
-
model_id = entry["model_id"]
|
| 1208 |
-
label = entry.get("name", model_id)
|
| 1209 |
-
prompt = entry.get("prompt", DEFAULT_OCR_PROMPT)
|
| 1210 |
-
params = sanitize_generation_params(entry.get("parameters", {}))
|
| 1211 |
-
notes = entry.get("notes", "")
|
| 1212 |
-
notes = f"{notes} [runtime: {_inference_device_label()}]" if notes else f"runtime: {_inference_device_label()}"
|
| 1213 |
-
start = time.perf_counter()
|
| 1214 |
-
output = ""
|
| 1215 |
-
|
| 1216 |
-
try:
|
| 1217 |
-
token_message = _required_token_message(entry, hf_token)
|
| 1218 |
-
if token_message:
|
| 1219 |
-
raise RuntimeError(token_message)
|
| 1220 |
-
if provider == "tesseract":
|
| 1221 |
-
output = run_tesseract(image_bytes, params)
|
| 1222 |
-
status = "ok"
|
| 1223 |
-
elif provider in {"hf_chat", "hf_image_to_text", "local_transformer", "local"}:
|
| 1224 |
-
output = run_local_model(entry, image_bytes, prompt, hf_token)
|
| 1225 |
-
status = "ok"
|
| 1226 |
-
else:
|
| 1227 |
-
raise RuntimeError(f"Unsupported provider '{provider}'")
|
| 1228 |
-
except Exception as exc: # pragma: no cover
|
| 1229 |
-
status = "error"
|
| 1230 |
-
output = f"{type(exc).__name__}: {exc}"
|
| 1231 |
-
|
| 1232 |
-
duration = round(time.perf_counter() - start, 3)
|
| 1233 |
-
output = output or ""
|
| 1234 |
-
return RunnerResult(
|
| 1235 |
-
model_id=entry["id"],
|
| 1236 |
-
label=label,
|
| 1237 |
-
provider=provider,
|
| 1238 |
-
status=status,
|
| 1239 |
-
output=output,
|
| 1240 |
-
latency_sec=duration,
|
| 1241 |
-
char_count=len(output),
|
| 1242 |
-
cer=None,
|
| 1243 |
-
wer=None,
|
| 1244 |
-
notes=notes,
|
| 1245 |
-
)
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
def _get_runtime_profile() -> Dict[str, object]:
|
| 1249 |
-
profile = {
|
| 1250 |
-
"has_cuda": False,
|
| 1251 |
-
"gpu_name": None,
|
| 1252 |
-
"gpu_vram_gb": None,
|
| 1253 |
-
"gpu_free_vram_gb": None,
|
| 1254 |
-
"gpu_reserved_vram_gb": None,
|
| 1255 |
-
"gpu_allocated_vram_gb": None,
|
| 1256 |
-
"provider": "cpu",
|
| 1257 |
-
}
|
| 1258 |
-
env_hardware = os.getenv("SPACE_HARDWARE", "").lower() or os.getenv("HF_SPACE_HARDWARE", "").lower()
|
| 1259 |
-
if env_hardware:
|
| 1260 |
-
profile["provider"] = env_hardware
|
| 1261 |
-
try:
|
| 1262 |
-
import torch
|
| 1263 |
-
|
| 1264 |
-
if torch.cuda.is_available():
|
| 1265 |
-
profile["has_cuda"] = True
|
| 1266 |
-
props = torch.cuda.get_device_properties(0)
|
| 1267 |
-
profile["gpu_name"] = props.name
|
| 1268 |
-
profile["gpu_vram_gb"] = round(props.total_memory / (1024 ** 3), 1)
|
| 1269 |
-
try:
|
| 1270 |
-
free_mem, total_mem = torch.cuda.mem_get_info(0)
|
| 1271 |
-
profile["gpu_free_vram_gb"] = round(free_mem / (1024 ** 3), 1)
|
| 1272 |
-
profile["gpu_vram_gb"] = round(total_mem / (1024 ** 3), 1)
|
| 1273 |
-
except Exception:
|
| 1274 |
-
free_approx = max(0, props.total_memory - torch.cuda.memory_reserved(0))
|
| 1275 |
-
profile["gpu_free_vram_gb"] = round(free_approx / (1024 ** 3), 1)
|
| 1276 |
-
profile["gpu_reserved_vram_gb"] = round(torch.cuda.memory_reserved(0) / (1024 ** 3), 1)
|
| 1277 |
-
profile["gpu_allocated_vram_gb"] = round(torch.cuda.memory_allocated(0) / (1024 ** 3), 1)
|
| 1278 |
-
if "a10g" in (props.name or "").lower() or "l4" in (props.name or "").lower():
|
| 1279 |
-
profile["provider"] = "zero_gpu"
|
| 1280 |
-
except Exception:
|
| 1281 |
-
pass
|
| 1282 |
-
return profile
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
def _runtime_capacity_note(runtime: Dict[str, object]) -> str:
|
| 1286 |
-
if not runtime.get("has_cuda"):
|
| 1287 |
-
return "Runtime compute mode: CPU-only."
|
| 1288 |
-
if runtime.get("gpu_name"):
|
| 1289 |
-
total = runtime.get("gpu_vram_gb")
|
| 1290 |
-
free = runtime.get("gpu_free_vram_gb")
|
| 1291 |
-
allocated = runtime.get("gpu_allocated_vram_gb")
|
| 1292 |
-
reserved = runtime.get("gpu_reserved_vram_gb")
|
| 1293 |
-
if free is None:
|
| 1294 |
-
return f"Runtime compute mode: {runtime['gpu_name']} with {total}GB total VRAM."
|
| 1295 |
-
return (
|
| 1296 |
-
f"Runtime compute mode: {runtime['gpu_name']} | total {total}GB | "
|
| 1297 |
-
f"free {free}GB | allocated {allocated}GB | reserved {reserved}GB."
|
| 1298 |
-
)
|
| 1299 |
-
return "Runtime compute mode: GPU detected but profile unavailable."
|
| 1300 |
-
|
| 1301 |
-
|
| 1302 |
-
def _estimate_model_vram_gb(entry: dict, *, compute_mode: str) -> float:
|
| 1303 |
-
if not isinstance(entry, dict):
|
| 1304 |
-
return ZERO_GPU_MODEL_FALLBACK_GB_SAFE
|
| 1305 |
-
provider = (entry.get("provider") or "").lower()
|
| 1306 |
-
if provider == "tesseract":
|
| 1307 |
-
return 0.2
|
| 1308 |
-
model_override = entry.get("estimated_vram_gb")
|
| 1309 |
-
if isinstance(model_override, (int, float)) and model_override > 0:
|
| 1310 |
-
return float(model_override)
|
| 1311 |
-
model_id = (entry.get("model_id", "") or "").lower()
|
| 1312 |
-
fallback = ZERO_GPU_MODEL_FALLBACK_GB_MAX if compute_mode == "max" else ZERO_GPU_MODEL_FALLBACK_GB_SAFE
|
| 1313 |
-
if "31b" in model_id or "32b" in model_id:
|
| 1314 |
-
return 22.0
|
| 1315 |
-
if "30b_a3b" in model_id:
|
| 1316 |
-
return 20.0
|
| 1317 |
-
if "26b" in model_id:
|
| 1318 |
-
return 16.0
|
| 1319 |
-
if "12b" in model_id:
|
| 1320 |
-
return 8.0
|
| 1321 |
-
if "8b" in model_id:
|
| 1322 |
-
return 6.0
|
| 1323 |
-
if "4b" in model_id or "e4b" in model_id:
|
| 1324 |
-
return 4.0
|
| 1325 |
-
if "a3b" in model_id:
|
| 1326 |
-
return 10.0
|
| 1327 |
-
if "3b" in model_id:
|
| 1328 |
-
return 2.5
|
| 1329 |
-
return fallback
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
def _execution_plan(selected_entries: List[dict], compute_mode: str = "safe") -> Tuple[int, str]:
|
| 1333 |
-
runtime = _get_runtime_profile()
|
| 1334 |
-
selected_count = len(selected_entries)
|
| 1335 |
-
capacity_note = _runtime_capacity_note(runtime)
|
| 1336 |
-
if selected_count <= 1:
|
| 1337 |
-
return 1, capacity_note
|
| 1338 |
-
|
| 1339 |
-
mode_is_max = compute_mode == "max"
|
| 1340 |
-
estimates = [_estimate_model_vram_gb(entry, compute_mode=compute_mode) for entry in selected_entries]
|
| 1341 |
-
estimated_gb = round(sum(estimates), 1)
|
| 1342 |
-
provider = runtime.get("provider")
|
| 1343 |
-
notes = []
|
| 1344 |
-
max_workers = min(ZERO_GPU_MAX_WORKERS, selected_count)
|
| 1345 |
-
|
| 1346 |
-
gpu_name = str(runtime.get("gpu_name", "")).lower()
|
| 1347 |
-
is_zero_gpu = (
|
| 1348 |
-
provider == "zero_gpu"
|
| 1349 |
-
or "zero-a10g" in str(provider)
|
| 1350 |
-
or "a10g" in gpu_name
|
| 1351 |
-
or "l4" in gpu_name
|
| 1352 |
-
)
|
| 1353 |
-
if is_zero_gpu:
|
| 1354 |
-
if compute_mode in {"sequential", "safe"}:
|
| 1355 |
-
max_workers = 1
|
| 1356 |
-
notes.append("Safe/Sequential mode runs one model at a time on ZeroGPU.")
|
| 1357 |
-
elif runtime.get("gpu_free_vram_gb") and runtime.get("gpu_vram_gb") and selected_count:
|
| 1358 |
-
# Keep a hard headroom + runtime overhead to avoid zeroGPU OOM/rate failures.
|
| 1359 |
-
headroom = float(runtime["gpu_free_vram_gb"]) - ZERO_GPU_MIN_GUARDED_FREE_GB
|
| 1360 |
-
headroom = max(0.0, headroom)
|
| 1361 |
-
headroom *= ZERO_GPU_MAX_HEADROOM if mode_is_max else ZERO_GPU_SAFE_HEADROOM
|
| 1362 |
-
sorted_estimates = sorted(estimates)
|
| 1363 |
-
fit_workers = 0
|
| 1364 |
-
running = 0.0
|
| 1365 |
-
for est in sorted_estimates:
|
| 1366 |
-
# Conservative per-run margin for HTTP/image payload/responses/runtime overhead.
|
| 1367 |
-
needed = est + 0.8
|
| 1368 |
-
if fit_workers < ZERO_GPU_MAX_WORKERS and fit_workers + 1 <= selected_count and running + needed <= headroom:
|
| 1369 |
-
running += needed
|
| 1370 |
-
fit_workers += 1
|
| 1371 |
-
else:
|
| 1372 |
-
break
|
| 1373 |
-
if fit_workers <= 0 and sorted_estimates:
|
| 1374 |
-
fit_workers = 1
|
| 1375 |
-
if fit_workers < selected_count:
|
| 1376 |
-
notes.append(
|
| 1377 |
-
f"ZeroGPU {'max' if mode_is_max else 'safe'} mode: {fit_workers}/{selected_count} models can be "
|
| 1378 |
-
f"run in parallel with current free memory."
|
| 1379 |
-
)
|
| 1380 |
-
max_workers = min(max_workers, fit_workers)
|
| 1381 |
-
elif runtime.get("gpu_vram_gb") and estimated_gb >= runtime["gpu_vram_gb"] * 0.7:
|
| 1382 |
-
notes.append(
|
| 1383 |
-
f"Estimated total selected model VRAM ({estimated_gb:.1f}GB) is above 70% of available GPU ({runtime['gpu_vram_gb']}GB)."
|
| 1384 |
-
)
|
| 1385 |
-
max_workers = 1
|
| 1386 |
-
if mode_is_max and max_workers > 1 and selected_count > 6:
|
| 1387 |
-
# Keep API call fanout bounded for many queued tasks.
|
| 1388 |
-
max_workers = min(max_workers, 2)
|
| 1389 |
-
notes.append("Max-compute mode capped at 2 concurrent workers when many models are selected.")
|
| 1390 |
-
if max_workers > ZERO_GPU_MAX_SAFE_SELECTED:
|
| 1391 |
-
max_workers = ZERO_GPU_MAX_SAFE_SELECTED
|
| 1392 |
-
|
| 1393 |
-
if runtime.get("has_cuda") and estimated_gb and estimated_gb > 0:
|
| 1394 |
-
if estimated_gb > 24:
|
| 1395 |
-
notes.append(
|
| 1396 |
-
f"Estimated total model VRAM {estimated_gb:.1f}GB is high. Running sequentially to be conservative."
|
| 1397 |
-
)
|
| 1398 |
-
max_workers = 1
|
| 1399 |
-
|
| 1400 |
-
if selected_count > ZERO_GPU_MAX_SAFE_SELECTED and is_zero_gpu:
|
| 1401 |
-
notes.append("Running many models at once increases timeout risk on ZeroGPU.")
|
| 1402 |
-
|
| 1403 |
-
notes.append(capacity_note)
|
| 1404 |
-
return max_workers, " | ".join(notes)
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
def run_comparison(
|
| 1408 |
-
image_file,
|
| 1409 |
-
selected_model_ids,
|
| 1410 |
-
ground_truth_text,
|
| 1411 |
-
hf_token,
|
| 1412 |
-
compute_mode,
|
| 1413 |
-
) -> Tuple[str, str, str]:
|
| 1414 |
-
try:
|
| 1415 |
-
models = _safe_load_registry()
|
| 1416 |
-
except Exception as exc: # pragma: no cover
|
| 1417 |
-
return (
|
| 1418 |
-
"Configuration error: failed to load model registry.",
|
| 1419 |
-
"[]",
|
| 1420 |
-
json.dumps({"error": str(exc)}, ensure_ascii=False),
|
| 1421 |
-
)
|
| 1422 |
-
if not image_file:
|
| 1423 |
-
return "Upload an image first.", "[]", json.dumps({"error": "No image provided"}, ensure_ascii=False)
|
| 1424 |
-
try:
|
| 1425 |
-
image_bytes = _load_image_bytes(image_file)
|
| 1426 |
-
except Exception as exc:
|
| 1427 |
-
error_text = f"Failed to load uploaded image: {exc}"
|
| 1428 |
-
return error_text, "[]", json.dumps({"error": error_text}, ensure_ascii=False)
|
| 1429 |
-
selected_model_ids = _normalize_model_selection(selected_model_ids)
|
| 1430 |
-
if not selected_model_ids:
|
| 1431 |
-
selected_model_ids = []
|
| 1432 |
-
|
| 1433 |
-
results: List[RunnerResult] = []
|
| 1434 |
-
selected = set(selected_model_ids)
|
| 1435 |
-
selected_entries = []
|
| 1436 |
-
for entry in models:
|
| 1437 |
-
if entry["id"] not in selected:
|
| 1438 |
-
continue
|
| 1439 |
-
if not entry.get("enabled", True):
|
| 1440 |
-
continue
|
| 1441 |
-
precheck_ok, precheck_message = _get_model_precheck(entry, hf_token)
|
| 1442 |
-
if not precheck_ok:
|
| 1443 |
-
entry_provider = entry.get("provider", "local_transformer")
|
| 1444 |
-
if entry_provider != "tesseract":
|
| 1445 |
-
entry_provider = "local_transformer"
|
| 1446 |
-
results.append(
|
| 1447 |
-
RunnerResult(
|
| 1448 |
-
model_id=entry["id"],
|
| 1449 |
-
label=entry.get("name", entry["id"]),
|
| 1450 |
-
provider=entry_provider,
|
| 1451 |
-
status="unsupported",
|
| 1452 |
-
output=precheck_message,
|
| 1453 |
-
latency_sec=None,
|
| 1454 |
-
char_count=0,
|
| 1455 |
-
cer=None,
|
| 1456 |
-
wer=None,
|
| 1457 |
-
notes=entry.get("notes", ""),
|
| 1458 |
-
)
|
| 1459 |
-
)
|
| 1460 |
-
continue
|
| 1461 |
-
selected_entries.append(entry)
|
| 1462 |
-
if spaces is not None and selected_entries:
|
| 1463 |
-
try:
|
| 1464 |
-
_ensure_zero_gpu_lease()
|
| 1465 |
-
except Exception:
|
| 1466 |
-
# If the lease can’t be acquired (for example due to infra edge cases),
|
| 1467 |
-
# allow the request to continue; model calls will fail fast with clear errors.
|
| 1468 |
-
pass
|
| 1469 |
-
|
| 1470 |
-
max_workers, execution_warning = _execution_plan(selected_entries, compute_mode=compute_mode)
|
| 1471 |
-
hf_token = (hf_token or os.getenv("HF_TOKEN") or "").strip()
|
| 1472 |
-
futures_map = {}
|
| 1473 |
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 1474 |
-
for entry in models:
|
| 1475 |
-
if not entry.get("enabled", True):
|
| 1476 |
-
entry_provider = entry.get("provider", "local_transformer")
|
| 1477 |
-
if entry_provider != "tesseract":
|
| 1478 |
-
entry_provider = "local_transformer"
|
| 1479 |
-
results.append(
|
| 1480 |
-
RunnerResult(
|
| 1481 |
-
model_id=entry["id"],
|
| 1482 |
-
label=entry.get("name", entry["id"]),
|
| 1483 |
-
provider=entry_provider,
|
| 1484 |
-
status="disabled",
|
| 1485 |
-
output="",
|
| 1486 |
-
latency_sec=None,
|
| 1487 |
-
char_count=0,
|
| 1488 |
-
cer=None,
|
| 1489 |
-
wer=None,
|
| 1490 |
-
notes=entry.get("notes", ""),
|
| 1491 |
-
)
|
| 1492 |
-
)
|
| 1493 |
-
continue
|
| 1494 |
-
if entry["id"] not in selected:
|
| 1495 |
-
entry_provider = entry.get("provider", "local_transformer")
|
| 1496 |
-
if entry_provider != "tesseract":
|
| 1497 |
-
entry_provider = "local_transformer"
|
| 1498 |
-
results.append(
|
| 1499 |
-
RunnerResult(
|
| 1500 |
-
model_id=entry["id"],
|
| 1501 |
-
label=entry.get("name", entry["id"]),
|
| 1502 |
-
provider=entry_provider,
|
| 1503 |
-
status="skipped",
|
| 1504 |
-
output="",
|
| 1505 |
-
latency_sec=None,
|
| 1506 |
-
char_count=0,
|
| 1507 |
-
cer=None,
|
| 1508 |
-
wer=None,
|
| 1509 |
-
notes=entry.get("notes", ""),
|
| 1510 |
-
)
|
| 1511 |
-
)
|
| 1512 |
-
continue
|
| 1513 |
-
if entry not in selected_entries:
|
| 1514 |
-
continue
|
| 1515 |
-
futures_map[executor.submit(run_single_model, entry, image_bytes, "", hf_token)] = entry
|
| 1516 |
-
|
| 1517 |
-
for future in as_completed(futures_map):
|
| 1518 |
-
try:
|
| 1519 |
-
result = future.result()
|
| 1520 |
-
if ground_truth_text:
|
| 1521 |
-
cer, wer = compute_cer_wer(ground_truth_text, result.output)
|
| 1522 |
-
result.cer = cer
|
| 1523 |
-
result.wer = wer
|
| 1524 |
-
results.append(result)
|
| 1525 |
-
except Exception as exc: # pragma: no cover
|
| 1526 |
-
results.append(
|
| 1527 |
-
RunnerResult(
|
| 1528 |
-
model_id="__worker_error__",
|
| 1529 |
-
label="Worker failure",
|
| 1530 |
-
provider="local_transformer",
|
| 1531 |
-
status="error",
|
| 1532 |
-
output=f"Unexpected worker failure: {exc}",
|
| 1533 |
-
latency_sec=None,
|
| 1534 |
-
char_count=0,
|
| 1535 |
-
cer=None,
|
| 1536 |
-
wer=None,
|
| 1537 |
-
notes="",
|
| 1538 |
-
)
|
| 1539 |
-
)
|
| 1540 |
-
|
| 1541 |
-
order_map = {model["id"]: idx for idx, model in enumerate(models)}
|
| 1542 |
-
results.sort(key=lambda r: order_map.get(r.model_id, 1_000_000))
|
| 1543 |
-
|
| 1544 |
-
summary = []
|
| 1545 |
-
if execution_warning:
|
| 1546 |
-
summary.append(f"**Execution mode:** {execution_warning}")
|
| 1547 |
-
for result in results:
|
| 1548 |
-
if result.status == "ok":
|
| 1549 |
-
metrics = []
|
| 1550 |
-
if result.cer is not None:
|
| 1551 |
-
metrics.append(f"CER {result.cer:.4f}")
|
| 1552 |
-
if result.wer is not None:
|
| 1553 |
-
metrics.append(f"WER {result.wer:.4f}")
|
| 1554 |
-
metric_str = " | ".join(metrics) if metrics else "N/A"
|
| 1555 |
-
summary.append(
|
| 1556 |
-
f"- **{result.label}**: {result.status} · {result.latency_sec:.2f}s · {result.char_count} chars · {metric_str}"
|
| 1557 |
-
)
|
| 1558 |
-
else:
|
| 1559 |
-
summary.append(f"- **{result.label}**: {result.status} · {result.notes or result.output[:120]}")
|
| 1560 |
-
|
| 1561 |
-
table_rows = []
|
| 1562 |
-
for result in results:
|
| 1563 |
-
row_output = result.output
|
| 1564 |
-
if len(row_output) > 400:
|
| 1565 |
-
row_output = row_output[:397] + "..."
|
| 1566 |
-
table_rows.append(
|
| 1567 |
-
{
|
| 1568 |
-
"Model": result.label,
|
| 1569 |
-
"Provider": result.provider,
|
| 1570 |
-
"Status": result.status,
|
| 1571 |
-
"Time (s)": result.latency_sec,
|
| 1572 |
-
"Chars": result.char_count,
|
| 1573 |
-
"CER": result.cer,
|
| 1574 |
-
"WER": result.wer,
|
| 1575 |
-
"Output preview": row_output,
|
| 1576 |
-
"Notes": result.notes,
|
| 1577 |
-
}
|
| 1578 |
-
)
|
| 1579 |
-
|
| 1580 |
-
json_payload = [r.__dict__ for r in results]
|
| 1581 |
-
markdown = "# OCR comparison results\n\n" + ("\n".join(summary) if summary else "No models selected.")
|
| 1582 |
-
return (
|
| 1583 |
-
markdown,
|
| 1584 |
-
json.dumps(table_rows, ensure_ascii=False, indent=2),
|
| 1585 |
-
json.dumps(json_payload, ensure_ascii=False, indent=2),
|
| 1586 |
-
)
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
def refresh_model_choices():
|
| 1590 |
-
try:
|
| 1591 |
-
models = _safe_load_registry()
|
| 1592 |
-
except Exception:
|
| 1593 |
-
return [("Model registry is unavailable", "__registry_error__")], []
|
| 1594 |
-
options = [(m["name"], m["id"]) for m in models if m.get("enabled", True)]
|
| 1595 |
-
default_checked = [m["id"] for m in models if m.get("enabled", True)]
|
| 1596 |
-
return options, default_checked
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
def build_ui():
|
| 1600 |
-
options, default_checked = refresh_model_choices()
|
| 1601 |
-
runtime_warning = _transformers_runtime_message()
|
| 1602 |
-
dependency_warning = _runtime_dependency_warning()
|
| 1603 |
-
if runtime_warning is None:
|
| 1604 |
-
runtime_warning = dependency_warning
|
| 1605 |
-
elif dependency_warning:
|
| 1606 |
-
runtime_warning = f"{runtime_warning}\n\n{dependency_warning}"
|
| 1607 |
-
with gr.Blocks(title="Hebrew/English OCR Model Comparison (Zero GPU)") as demo:
|
| 1608 |
-
gr.Markdown(
|
| 1609 |
-
"""
|
| 1610 |
-
# Private Zero-GPU HF Space: Hebrew OCR Comparator
|
| 1611 |
-
|
| 1612 |
-
Upload one document image and run only the models you check.
|
| 1613 |
-
The benchmark is built for **Hebrew documents containing printed + handwritten text**.
|
| 1614 |
-
Unchecked models will be skipped and **not executed**.
|
| 1615 |
-
All OCR inference is executed locally in this Space on ZeroGPU (no external inference API calls).
|
| 1616 |
-
By default, CPU fallback is disabled so models that cannot stay on CUDA fail with clear errors.
|
| 1617 |
-
To allow CPU fallback (slower, for compatibility only), set `ALLOW_CPU_FALLBACK=1`.
|
| 1618 |
-
"""
|
| 1619 |
-
)
|
| 1620 |
-
if runtime_warning:
|
| 1621 |
-
gr.Markdown(f"### Runtime warning\n{runtime_warning}")
|
| 1622 |
-
with gr.Row():
|
| 1623 |
-
with gr.Column(scale=2):
|
| 1624 |
-
image_input = gr.Image(type="filepath", label="Document image (printed and handwritten Hebrew, English optional)")
|
| 1625 |
-
hf_token = gr.Textbox(
|
| 1626 |
-
label="HF_TOKEN (optional)",
|
| 1627 |
-
type="password",
|
| 1628 |
-
value=os.getenv("HF_TOKEN", ""),
|
| 1629 |
-
info="Needed only for private/gated model downloads into this Space.",
|
| 1630 |
-
)
|
| 1631 |
-
ground_truth = gr.Textbox(
|
| 1632 |
-
label="Ground truth (optional)",
|
| 1633 |
-
lines=5,
|
| 1634 |
-
placeholder="Paste exact expected text for CER/WER",
|
| 1635 |
-
)
|
| 1636 |
-
with gr.Column(scale=3):
|
| 1637 |
-
compute_mode = gr.Radio(
|
| 1638 |
-
label="Compute mode",
|
| 1639 |
-
choices=[
|
| 1640 |
-
("Safe (recommended, avoid overrun)", "safe"),
|
| 1641 |
-
("Max compute (faster)", "max"),
|
| 1642 |
-
("Sequential only", "sequential"),
|
| 1643 |
-
],
|
| 1644 |
-
value="safe",
|
| 1645 |
-
)
|
| 1646 |
-
model_selector = gr.CheckboxGroup(
|
| 1647 |
-
label="Models to run",
|
| 1648 |
-
choices=options,
|
| 1649 |
-
value=default_checked,
|
| 1650 |
-
interactive=True,
|
| 1651 |
-
)
|
| 1652 |
-
run_btn = gr.Button("Run selected models", variant="primary")
|
| 1653 |
-
with gr.Row():
|
| 1654 |
-
status_md = gr.Markdown("## Results")
|
| 1655 |
-
results_md = gr.Markdown("")
|
| 1656 |
-
results_df = gr.JSON(
|
| 1657 |
-
label="Result rows (JSON)",
|
| 1658 |
-
)
|
| 1659 |
-
results_rows_json = gr.Textbox(
|
| 1660 |
-
label="Result rows (JSON)",
|
| 1661 |
-
lines=12,
|
| 1662 |
-
interactive=False,
|
| 1663 |
-
)
|
| 1664 |
-
raw_json = gr.Textbox(
|
| 1665 |
-
label="Full outputs (JSON)",
|
| 1666 |
-
lines=12,
|
| 1667 |
-
interactive=False,
|
| 1668 |
-
)
|
| 1669 |
-
run_btn.click(
|
| 1670 |
-
fn=run_comparison,
|
| 1671 |
-
inputs=[image_input, model_selector, ground_truth, hf_token, compute_mode],
|
| 1672 |
-
outputs=[results_md, results_rows_json, raw_json],
|
| 1673 |
-
api_name="run_comparison",
|
| 1674 |
-
)
|
| 1675 |
-
return demo
|
| 1676 |
-
|
| 1677 |
-
|
| 1678 |
-
if __name__ == "__main__":
|
| 1679 |
-
if not os.path.exists(REGISTRY_PATH):
|
| 1680 |
-
raise RuntimeError("Missing model_registry.json. Create it before running the app.")
|
| 1681 |
-
demo = build_ui()
|
| 1682 |
-
port = int(os.getenv("PORT", "7860"))
|
| 1683 |
-
demo.launch(server_name="0.0.0.0", server_port=port)
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Hebrew OCR Document Comparator
|
| 3 |
+
emoji: 🧾
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.30.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Hebrew/English OCR Document Comparator (Zero-GPU)
|
| 14 |
+
|
| 15 |
+
This Space is configured specifically for **Hebrew document OCR** (printed + handwritten),
|
| 16 |
+
with an optional English fallback when mixed language pages include English strings.
|
| 17 |
+
|
| 18 |
+
The scheduler is designed for ZeroGPU safety: by default it runs only the maximum number of selected models that fit in memory headroom and will queue the rest in automatic batches. All model inference runs locally inside your ZeroGPU runtime.
|
| 19 |
+
|
| 20 |
+
## What this space includes
|
| 21 |
+
|
| 22 |
+
- Checkbox-controlled model runner (unchecked models are skipped).
|
| 23 |
+
- Per-model configuration file: `model_registry.json` (easy to add/remove models).
|
| 24 |
+
- Ground-truth textbox for quick CER/WER scoring.
|
| 25 |
+
- Local OCR inference for all listed models (Transformers runtime + Tesseract fallback).
|
| 26 |
+
- Per-run preflight now validates each model’s config before execution. Any selected model that is not
|
| 27 |
+
loadable in this Space’s local Transformers environment is marked as `unsupported` immediately and skipped,
|
| 28 |
+
which prevents internal errors from unsupported repositories (for example ONNX/GGUF-only repos).
|
| 29 |
+
- `requirements.txt` and `apt.txt` already include OCR dependencies, plus updated transformer runtime dependencies that improve model compatibility.
|
| 30 |
+
- `.huggingface/metadata` preset for Gradio spaces.
|
| 31 |
+
|
| 32 |
+
## Make it a private Zero-GPU Hugging Face Space
|
| 33 |
+
|
| 34 |
+
1. In your Hugging Face account, create a new Space.
|
| 35 |
+
2. Set **Visibility: Private**.
|
| 36 |
+
3. Set **Space SDK: Gradio**.
|
| 37 |
+
4. Choose **Zero GPU / a10g** hardware (`zero-a10g`).
|
| 38 |
+
5. Upload all files from this directory (`app.py`, `model_registry.json`, `requirements.txt`, `apt.txt`, `.huggingface/metadata`, `README.md`).
|
| 39 |
+
6. On first run, use `HF_TOKEN` from environment only if a selected model is private/gated and needs authenticated download.
|
| 40 |
+
7. ZeroGPU spaces do not currently support changing `sleep-time`, so the runtime keeps its platform default idle behavior.
|
| 41 |
+
8. The app keeps GPU work scoped to active runs by using a short activation hook during processing (instead of continuous warmup).
|
| 42 |
+
|
| 43 |
+
### CLI flow (your current machine is already logged in)
|
| 44 |
+
|
| 45 |
+
If you want CLI-only deploy, run:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
HF_SPACE=ssdataanalysis/hebrew-ocr-comparator
|
| 49 |
+
hf repos create $HF_SPACE --type space --sdk gradio --private --flavor zero-a10g --exist-ok
|
| 50 |
+
hf upload $HF_SPACE /Users/alexanders/ocr-document-ocr-comparison-space . --type space --commit-message "Initial upload"
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
The same repo is already created and uploaded from this machine in this folder:
|
| 54 |
+
|
| 55 |
+
`https://huggingface.co/spaces/ssdataanalysis/hebrew-ocr-comparator`
|
| 56 |
+
|
| 57 |
+
## Model list provided
|
| 58 |
+
|
| 59 |
+
The following models are preloaded in `model_registry.json`:
|
| 60 |
+
|
| 61 |
+
- Qwen3-VL-8B-Instruct
|
| 62 |
+
- Qwen3-VL-8B-Thinking
|
| 63 |
+
- Qwen3-VL-4B-Instruct
|
| 64 |
+
- Qwen3-VL-4B-Thinking
|
| 65 |
+
- Qwen3-VL-30B-A3B-Instruct
|
| 66 |
+
- Qwen3-VL-30B-A3B-Thinking
|
| 67 |
+
- Nemotron-OCR-v2
|
| 68 |
+
- Gemma4 0.4B (E4B) Instruct
|
| 69 |
+
- Gemma4 0.4B (E4B) Thinking
|
| 70 |
+
- Gemma4 12B Instruct
|
| 71 |
+
- Gemma4 12B Thinking
|
| 72 |
+
- Gemma4 26B-A4B Instruct
|
| 73 |
+
- Gemma4 26B-A4B Thinking
|
| 74 |
+
- Gemma4 31B Instruct
|
| 75 |
+
- Gemma4 31B Thinking
|
| 76 |
+
- chandra2 (`datalab-to/chandra-ocr-2`)
|
| 77 |
+
- ronylicha/gigapdf-ocr-hebrew
|
| 78 |
+
- Qunie-V7-mini
|
| 79 |
+
- OpenMLKitOCR
|
| 80 |
+
- Tzefa-Word-OCR-TrOCR
|
| 81 |
+
- Qunie-V7-Pico
|
| 82 |
+
- aya-vision-32b
|
| 83 |
+
- aya-vision-8b
|
| 84 |
+
- Phi-4-multimodal-instruct
|
| 85 |
+
- surya2 (`datalab-to/surya-ocr-2`)
|
| 86 |
+
- PaddleOCR-VL
|
| 87 |
+
- PaddleOCR-VL 1.5
|
| 88 |
+
- PaddleOCR-VL 1.6
|
| 89 |
+
- DeepSeek OCR-1 (`deepseek-ai/DeepSeek-OCR`)
|
| 90 |
+
- DeepSeek OCR-2 (`deepseek-ai/DeepSeek-OCR-2`)
|
| 91 |
+
- cyttic/exp10-trocr-hebrew-matan-full
|
| 92 |
+
- cyttic/exp23-directfit-unfrozen
|
| 93 |
+
- cyttic/heb-verifier17-connected
|
| 94 |
+
- cyttic/exp26-composed1m
|
| 95 |
+
- Tesseract (local)
|
| 96 |
+
|
| 97 |
+
Model outputs now request a high `max_new_tokens` budget by default (`4096`) for stronger coverage of long documents.
|
| 98 |
+
|
| 99 |
+
## Add/remove models in the future
|
| 100 |
+
|
| 101 |
+
Edit `model_registry.json`:
|
| 102 |
+
|
| 103 |
+
- Set `"enabled": false` to hide a model from the checkbox list.
|
| 104 |
+
- Remove an entry entirely to delete it from the UI.
|
| 105 |
+
- Add a new entry with:
|
| 106 |
+
- `id` (unique stable ID)
|
| 107 |
+
- `name` (UI label)
|
| 108 |
+
- `model_id` (Hugging Face model path)
|
| 109 |
+
- `provider`:
|
| 110 |
+
- `hf_chat` or `hf_image_to_text`: both are mapped to local Transformers execution in this Space
|
| 111 |
+
- `tesseract` (local OCR fallback)
|
| 112 |
+
- Optional per-entry compatibility overrides:
|
| 113 |
+
- `pipeline_task`: set explicit task string when auto-task probing is insufficient.
|
| 114 |
+
- `trust_remote_code`: `true` when the repository needs custom loading code.
|
| 115 |
+
- `parameters` and `prompt` (model-specific tuning)
|
| 116 |
+
|
| 117 |
+
## Notes and caveats
|
| 118 |
+
|
| 119 |
+
- Some large or gated models may be unavailable without a token.
|
| 120 |
+
- Some checkpoint/config combinations can still be unsupported by the available local Transformers runtime and will show an error row instead of returning output.
|
| 121 |
+
- This version enables `trust_remote_code=True` where required for model families that rely on remote loading code.
|
| 122 |
+
- Tesseract output depends on the `tesseract` binary; this Space now requests `tesseract-ocr`, `tesseract-ocr-eng`, and `tesseract-ocr-heb` in `apt.txt`.
|
| 123 |
+
- For strict production benchmarks, keep an optional fixed random seed in each entry if the model honors it.
|
| 124 |
+
- This app logs only per-run outputs and derived metrics; it does not persist user images.
|
| 125 |
+
- In the UI, use **Compute mode**:
|
| 126 |
+
- **Safe (recommended, avoid overrun)**: conservative concurrency based on free GPU memory.
|
| 127 |
+
- **Max compute (faster)**: more parallelism when runtime headroom is available.
|
| 128 |
+
- **Sequential only**: one model at a time.
|
| 129 |
+
|
| 130 |
+
## What changed for your requested OCR behavior
|
| 131 |
+
|
| 132 |
+
- Prompts and UI text are aligned to document OCR with an explicit emphasis on **printed + handwritten Hebrew** pages.
|
| 133 |
+
- Run only the models you check in the checkbox list to prevent unnecessary inference calls.
|
| 134 |
+
- README and upload steps are prepared for CLI deployment under your logged-in account.
|
| 135 |
+
- Local inference now uses CUDA-first strict mode by default, with CPU fallback disabled unless `ALLOW_CPU_FALLBACK=1`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|