Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit Β·
8c23eee
1
Parent(s): ad64790
Prevent OCR stalls by timing out Surya load and falling back
Browse files- 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 |
-
|
|
|
|
| 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}) β
|
| 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
|
| 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 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
"
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
except Exception as e:
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"]
|