Pointf5ive commited on
Commit
8c23eee
Β·
1 Parent(s): ad64790

Prevent OCR stalls by timing out Surya load and falling back

Browse files
Files changed (1) hide show
  1. smoke_signal_tab.py +142 -19
smoke_signal_tab.py CHANGED
@@ -26,6 +26,7 @@ import hashlib
26
  import json
27
  import os
28
  import tempfile
 
29
  import time
30
  from datetime import datetime
31
  from pathlib import Path
@@ -709,10 +710,17 @@ def _profile_status_html() -> str:
709
 
710
  # ── Step 3: OCR ────────────────────────────────────────────────────────────────
711
  _SURYA_RUNTIME = None
 
 
 
712
  try:
713
  SS_SURYA_BATCH_SIZE = max(1, int(os.environ.get("SS_SURYA_BATCH_SIZE", "4")))
714
  except Exception:
715
  SS_SURYA_BATCH_SIZE = 4
 
 
 
 
716
 
717
 
718
  def _load_surya_runtime():
@@ -773,6 +781,47 @@ def _load_surya_runtime():
773
  return None, str(surya_error), False
774
 
775
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  def _run_surya_batch(images, surya: dict):
777
  """Run a batch of PIL images through Surya using either API shape."""
778
  if surya.get("api") == "predictor-v2":
@@ -819,6 +868,67 @@ def _regions_from_page_result(page_result):
819
  return regions, conf
820
 
821
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
  def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
823
  """Run Surya OCR on all profiled PDFs."""
824
  df = load_manifest_df()
@@ -849,14 +959,15 @@ def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
849
 
850
  progress(0, desc="Preparing OCR run...")
851
 
852
- surya, surya_error, reused = _load_surya_runtime()
 
853
  if surya:
854
  if reused:
855
  log.append(log_line(f"βœ“ Reusing Surya models ({surya['api']})"))
856
  else:
857
  log.append(log_line(f"βœ“ Surya models loaded ({surya['api']})"))
858
  else:
859
- log.append(log_line(f"⚠ Surya unavailable ({surya_error}) β€” falling back to text extraction only"))
860
 
861
  total_pages_planned = 0
862
  for _, row in eligible.iterrows():
@@ -933,7 +1044,7 @@ def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
933
 
934
  # Batch OCR for non-embedded pages.
935
  ocr_lookup = {}
936
- if surya is not None and ocr_targets:
937
  batch_size = SS_SURYA_BATCH_SIZE
938
  for start in range(0, len(ocr_targets), batch_size):
939
  batch = ocr_targets[start:start + batch_size]
@@ -944,22 +1055,34 @@ def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
944
  if len(batch_images) != len(batch):
945
  raise RuntimeError("One or more page renders missing for OCR batch.")
946
 
947
- predictions = _run_surya_batch(batch_images, surya)
948
- for item, page_result in zip(batch, predictions):
949
- regions, conf = _regions_from_page_result(page_result)
950
- ocr_lookup[item["page_num"]] = {
951
- "regions": regions,
952
- "confidence": conf,
953
- "method": "surya",
954
- }
 
 
 
 
 
955
  except Exception as e:
956
- for item in batch:
957
- ocr_lookup[item["page_num"]] = {
958
- "regions": [],
959
- "confidence": 0.0,
960
- "method": "error",
961
- }
962
- log.append(log_line(f" ⚠ {book_id} batch {batch_pages[0]}-{batch_pages[-1]}: {e}"))
 
 
 
 
 
 
 
963
  finally:
964
  for img in batch_images:
965
  try:
@@ -989,7 +1112,7 @@ def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
989
  regions = []
990
  conf = 0.0
991
  method = "error"
992
- elif surya:
993
  page_out = ocr_lookup.get(page_num, {"regions": [], "confidence": 0.0, "method": "error"})
994
  regions = page_out["regions"]
995
  conf = page_out["confidence"]
 
26
  import json
27
  import os
28
  import tempfile
29
+ import threading
30
  import time
31
  from datetime import datetime
32
  from pathlib import Path
 
710
 
711
  # ── Step 3: OCR ────────────────────────────────────────────────────────────────
712
  _SURYA_RUNTIME = None
713
+ _SURYA_LOAD_THREAD = None
714
+ _SURYA_LOAD_ERROR = None
715
+ _SURYA_LOAD_LOCK = threading.Lock()
716
  try:
717
  SS_SURYA_BATCH_SIZE = max(1, int(os.environ.get("SS_SURYA_BATCH_SIZE", "4")))
718
  except Exception:
719
  SS_SURYA_BATCH_SIZE = 4
720
+ try:
721
+ SS_SURYA_LOAD_TIMEOUT_SEC = max(1, int(os.environ.get("SS_SURYA_LOAD_TIMEOUT_SEC", "20")))
722
+ except Exception:
723
+ SS_SURYA_LOAD_TIMEOUT_SEC = 20
724
 
725
 
726
  def _load_surya_runtime():
 
781
  return None, str(surya_error), False
782
 
783
 
784
+ def _surya_loader_worker():
785
+ """Background loader to avoid blocking OCR forever on slow model downloads."""
786
+ global _SURYA_LOAD_ERROR
787
+ surya, err, _ = _load_surya_runtime()
788
+ if surya is None:
789
+ _SURYA_LOAD_ERROR = err
790
+ else:
791
+ _SURYA_LOAD_ERROR = None
792
+
793
+
794
+ def _get_surya_runtime_with_timeout(timeout_sec: int):
795
+ """
796
+ Return Surya runtime quickly.
797
+ If model load is still in progress after timeout, caller should fallback this run.
798
+ """
799
+ global _SURYA_LOAD_THREAD
800
+
801
+ if _SURYA_RUNTIME is not None:
802
+ return _SURYA_RUNTIME, None, True
803
+
804
+ with _SURYA_LOAD_LOCK:
805
+ if _SURYA_RUNTIME is not None:
806
+ return _SURYA_RUNTIME, None, True
807
+
808
+ if _SURYA_LOAD_THREAD is None or not _SURYA_LOAD_THREAD.is_alive():
809
+ _SURYA_LOAD_THREAD = threading.Thread(target=_surya_loader_worker, daemon=True)
810
+ _SURYA_LOAD_THREAD.start()
811
+
812
+ loader_thread = _SURYA_LOAD_THREAD
813
+
814
+ loader_thread.join(timeout=timeout_sec)
815
+
816
+ if _SURYA_RUNTIME is not None:
817
+ return _SURYA_RUNTIME, None, False
818
+
819
+ if loader_thread.is_alive():
820
+ return None, f"Surya load exceeded {timeout_sec}s (still loading in background)", False
821
+
822
+ return None, _SURYA_LOAD_ERROR or "Surya load failed", False
823
+
824
+
825
  def _run_surya_batch(images, surya: dict):
826
  """Run a batch of PIL images through Surya using either API shape."""
827
  if surya.get("api") == "predictor-v2":
 
868
  return regions, conf
869
 
870
 
871
+ def _run_tesseract_batch(images):
872
+ """
873
+ Tesseract fallback for OCR when Surya is unavailable/slow.
874
+ Returns list of dicts with regions/confidence/method aligned to input order.
875
+ """
876
+ try:
877
+ import pytesseract
878
+ except Exception as e:
879
+ return [{"regions": [], "confidence": 0.0, "method": f"error-no-tesseract ({e})"} for _ in images]
880
+
881
+ outputs = []
882
+ for img in images:
883
+ try:
884
+ data = pytesseract.image_to_data(
885
+ img,
886
+ lang="eng",
887
+ config="--oem 1 --psm 6",
888
+ output_type=pytesseract.Output.DICT,
889
+ )
890
+ regions = []
891
+ conf_weighted = 0.0
892
+ word_count = 0
893
+ n = len(data.get("text", []))
894
+
895
+ for i in range(n):
896
+ txt = str(data["text"][i]).strip()
897
+ if not txt:
898
+ continue
899
+
900
+ try:
901
+ conf_raw = float(data["conf"][i])
902
+ except Exception:
903
+ conf_raw = -1.0
904
+ if conf_raw < 0:
905
+ continue
906
+
907
+ conf = max(0.0, min(1.0, conf_raw / 100.0))
908
+ left = int(data["left"][i])
909
+ top = int(data["top"][i])
910
+ width = int(data["width"][i])
911
+ height = int(data["height"][i])
912
+ bbox = [left, top, left + width, top + height]
913
+ wc = max(len(txt.split()), 1)
914
+
915
+ regions.append({
916
+ "text": txt,
917
+ "confidence": round(conf, 4),
918
+ "bbox": bbox,
919
+ "word_count": wc,
920
+ })
921
+ conf_weighted += conf * wc
922
+ word_count += wc
923
+
924
+ avg_conf = round(conf_weighted / max(word_count, 1), 4) if regions else 0.0
925
+ outputs.append({"regions": regions, "confidence": avg_conf, "method": "tesseract"})
926
+ except Exception as e:
927
+ outputs.append({"regions": [], "confidence": 0.0, "method": f"error-tesseract ({e})"})
928
+
929
+ return outputs
930
+
931
+
932
  def run_ocr(progress=gr.Progress(track_tqdm=False)) -> tuple:
933
  """Run Surya OCR on all profiled PDFs."""
934
  df = load_manifest_df()
 
959
 
960
  progress(0, desc="Preparing OCR run...")
961
 
962
+ progress(0, desc="Loading OCR engine...")
963
+ surya, surya_error, reused = _get_surya_runtime_with_timeout(SS_SURYA_LOAD_TIMEOUT_SEC)
964
  if surya:
965
  if reused:
966
  log.append(log_line(f"βœ“ Reusing Surya models ({surya['api']})"))
967
  else:
968
  log.append(log_line(f"βœ“ Surya models loaded ({surya['api']})"))
969
  else:
970
+ log.append(log_line(f"⚠ Surya unavailable ({surya_error}) β€” using Tesseract fallback for this run"))
971
 
972
  total_pages_planned = 0
973
  for _, row in eligible.iterrows():
 
1044
 
1045
  # Batch OCR for non-embedded pages.
1046
  ocr_lookup = {}
1047
+ if ocr_targets:
1048
  batch_size = SS_SURYA_BATCH_SIZE
1049
  for start in range(0, len(ocr_targets), batch_size):
1050
  batch = ocr_targets[start:start + batch_size]
 
1055
  if len(batch_images) != len(batch):
1056
  raise RuntimeError("One or more page renders missing for OCR batch.")
1057
 
1058
+ if surya is not None:
1059
+ predictions = _run_surya_batch(batch_images, surya)
1060
+ for item, page_result in zip(batch, predictions):
1061
+ regions, conf = _regions_from_page_result(page_result)
1062
+ ocr_lookup[item["page_num"]] = {
1063
+ "regions": regions,
1064
+ "confidence": conf,
1065
+ "method": "surya",
1066
+ }
1067
+ else:
1068
+ fallback_preds = _run_tesseract_batch(batch_images)
1069
+ for item, pred in zip(batch, fallback_preds):
1070
+ ocr_lookup[item["page_num"]] = pred
1071
  except Exception as e:
1072
+ # If Surya batch fails, try Tesseract for this batch before giving up.
1073
+ if surya is not None:
1074
+ log.append(log_line(f" ⚠ {book_id} batch {batch_pages[0]}-{batch_pages[-1]} Surya error: {e}; retrying with Tesseract"))
1075
+ fallback_preds = _run_tesseract_batch(batch_images)
1076
+ for item, pred in zip(batch, fallback_preds):
1077
+ ocr_lookup[item["page_num"]] = pred
1078
+ else:
1079
+ for item in batch:
1080
+ ocr_lookup[item["page_num"]] = {
1081
+ "regions": [],
1082
+ "confidence": 0.0,
1083
+ "method": "error",
1084
+ }
1085
+ log.append(log_line(f" ⚠ {book_id} batch {batch_pages[0]}-{batch_pages[-1]}: {e}"))
1086
  finally:
1087
  for img in batch_images:
1088
  try:
 
1112
  regions = []
1113
  conf = 0.0
1114
  method = "error"
1115
+ elif surya or page_num in ocr_lookup:
1116
  page_out = ocr_lookup.get(page_num, {"regions": [], "confidence": 0.0, "method": "error"})
1117
  regions = page_out["regions"]
1118
  conf = page_out["confidence"]