Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -25,17 +25,20 @@ CHUNK_WORDS = 80 # stays under the embedding model's 128-token limit
|
|
| 25 |
CHUNK_OVERLAP = 15
|
| 26 |
TABLE_CHUNK_WORDS = 60 # table/spreadsheet rows are token-heavy, so smaller groups
|
| 27 |
RRF_K = 60
|
| 28 |
-
OCR_DPI =
|
| 29 |
OCR_LANGS = "ara+eng"
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# ========================= TEXT NORMALIZATION =========================
|
| 32 |
-
DIACRITICS_RE = re.compile(r"[
|
| 33 |
DIGIT_MAP = str.maketrans("٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01234567890123456789")
|
| 34 |
|
| 35 |
def light_clean(text):
|
| 36 |
text = unicodedata.normalize("NFKC", text)
|
| 37 |
text = DIACRITICS_RE.sub("", text)
|
| 38 |
-
return text.replace("
|
| 39 |
|
| 40 |
def normalize(text):
|
| 41 |
text = light_clean(text).translate(DIGIT_MAP)
|
|
@@ -75,20 +78,55 @@ def _set_tessdata():
|
|
| 75 |
os.environ["TESSDATA_PREFIX"] = p
|
| 76 |
return
|
| 77 |
|
| 78 |
-
def
|
| 79 |
-
"""
|
|
|
|
| 80 |
try:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
except Exception:
|
| 85 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
def extract_pdf(name, data):
|
| 88 |
blocks = []
|
| 89 |
stats = {"pages": 0, "tables": 0, "notes": []}
|
| 90 |
doc = fitz.open(stream=data, filetype="pdf")
|
| 91 |
-
empty_pages, ocr_pages = [], []
|
| 92 |
for i, page in enumerate(doc, 1):
|
| 93 |
stats["pages"] += 1
|
| 94 |
loc = f"p.{i}"
|
|
@@ -97,7 +135,9 @@ def extract_pdf(name, data):
|
|
| 97 |
blocks.append(make_block(tbl, name, loc, "table"))
|
| 98 |
stats["tables"] += 1
|
| 99 |
if len(text) < 50:
|
| 100 |
-
ocr_text = ocr_page(page)
|
|
|
|
|
|
|
| 101 |
if len(ocr_text) > len(text):
|
| 102 |
text = ocr_text
|
| 103 |
ocr_pages.append(i)
|
|
@@ -108,9 +148,19 @@ def extract_pdf(name, data):
|
|
| 108 |
if ocr_pages:
|
| 109 |
shown = ", ".join(map(str, ocr_pages[:15])) + ("..." if len(ocr_pages) > 15 else "")
|
| 110 |
stats["notes"].append(f"OCR used on page(s) {shown}. Text may contain recognition errors.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
if empty_pages:
|
| 112 |
shown = ", ".join(map(str, empty_pages[:15])) + ("..." if len(empty_pages) > 15 else "")
|
| 113 |
-
stats["notes"].append(
|
|
|
|
|
|
|
|
|
|
| 114 |
return blocks, stats
|
| 115 |
|
| 116 |
def extract_docx(name, data):
|
|
|
|
| 25 |
CHUNK_OVERLAP = 15
|
| 26 |
TABLE_CHUNK_WORDS = 60 # table/spreadsheet rows are token-heavy, so smaller groups
|
| 27 |
RRF_K = 60
|
| 28 |
+
OCR_DPI = 400 # higher than before: small typewriter/fax text needs it. Lower (e.g. 300) for speed.
|
| 29 |
OCR_LANGS = "ara+eng"
|
| 30 |
+
OCR_FALLBACK_LANGS = "eng" # used if Arabic language data is missing on the server
|
| 31 |
+
OCR_CONTENT_THRESHOLD = 200 # gray level below which a pixel counts as "content" (0-255)
|
| 32 |
+
OCR_CLIP_PAD = 8 # points of padding around detected content
|
| 33 |
|
| 34 |
# ========================= TEXT NORMALIZATION =========================
|
| 35 |
+
DIACRITICS_RE = re.compile(r"[ؐ-ًؚ-ٰٟۖ-ۭ]")
|
| 36 |
DIGIT_MAP = str.maketrans("٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01234567890123456789")
|
| 37 |
|
| 38 |
def light_clean(text):
|
| 39 |
text = unicodedata.normalize("NFKC", text)
|
| 40 |
text = DIACRITICS_RE.sub("", text)
|
| 41 |
+
return text.replace("ـ", "")
|
| 42 |
|
| 43 |
def normalize(text):
|
| 44 |
text = light_clean(text).translate(DIGIT_MAP)
|
|
|
|
| 78 |
os.environ["TESSDATA_PREFIX"] = p
|
| 79 |
return
|
| 80 |
|
| 81 |
+
def _content_clip(page):
|
| 82 |
+
"""Bounding box of non-white content, so large blank margins don't shrink the text during OCR.
|
| 83 |
+
Returns None if the page is blank, or the full page rect if the content already fills it."""
|
| 84 |
try:
|
| 85 |
+
pix = page.get_pixmap(dpi=72, colorspace=fitz.csGRAY) # at 72 dpi, 1 pixel = 1 point
|
| 86 |
+
arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width)
|
| 87 |
+
ys, xs = np.where(arr < OCR_CONTENT_THRESHOLD)
|
| 88 |
+
if xs.size == 0:
|
| 89 |
+
return None
|
| 90 |
+
r = page.rect
|
| 91 |
+
clip = fitz.Rect(
|
| 92 |
+
r.x0 + xs.min() - OCR_CLIP_PAD, r.y0 + ys.min() - OCR_CLIP_PAD,
|
| 93 |
+
r.x0 + xs.max() + 1 + OCR_CLIP_PAD, r.y0 + ys.max() + 1 + OCR_CLIP_PAD,
|
| 94 |
+
) & r
|
| 95 |
+
if clip.is_empty or clip.width < 20 or clip.height < 20:
|
| 96 |
+
return r
|
| 97 |
+
return clip
|
| 98 |
except Exception:
|
| 99 |
+
return page.rect
|
| 100 |
+
|
| 101 |
+
def _ocr_with_lang(page, clip, lang):
|
| 102 |
+
pix = page.get_pixmap(dpi=OCR_DPI, clip=clip, colorspace=fitz.csGRAY)
|
| 103 |
+
ocr_doc = fitz.open("pdf", pix.pdfocr_tobytes(language=lang))
|
| 104 |
+
try:
|
| 105 |
+
return unicodedata.normalize("NFKC", ocr_doc[0].get_text("text") or "").strip()
|
| 106 |
+
finally:
|
| 107 |
+
ocr_doc.close()
|
| 108 |
+
|
| 109 |
+
def ocr_page(page):
|
| 110 |
+
"""OCR a scanned page. Returns (text, error).
|
| 111 |
+
error is None unless every language attempt raised, so 'OCR ran and found nothing'
|
| 112 |
+
can be told apart from 'OCR could not run' (e.g. Tesseract or language data missing)."""
|
| 113 |
+
_set_tessdata()
|
| 114 |
+
clip = _content_clip(page)
|
| 115 |
+
if clip is None: # truly blank page: nothing to OCR, and not an error
|
| 116 |
+
return "", None
|
| 117 |
+
errors = []
|
| 118 |
+
for lang in dict.fromkeys((OCR_LANGS, OCR_FALLBACK_LANGS)): # de-duplicated, order kept
|
| 119 |
+
try:
|
| 120 |
+
return _ocr_with_lang(page, clip, lang), None
|
| 121 |
+
except Exception as e:
|
| 122 |
+
errors.append(f"[{lang}] {e}")
|
| 123 |
+
return "", "; ".join(errors)
|
| 124 |
|
| 125 |
def extract_pdf(name, data):
|
| 126 |
blocks = []
|
| 127 |
stats = {"pages": 0, "tables": 0, "notes": []}
|
| 128 |
doc = fitz.open(stream=data, filetype="pdf")
|
| 129 |
+
empty_pages, ocr_pages, ocr_errors = [], [], []
|
| 130 |
for i, page in enumerate(doc, 1):
|
| 131 |
stats["pages"] += 1
|
| 132 |
loc = f"p.{i}"
|
|
|
|
| 135 |
blocks.append(make_block(tbl, name, loc, "table"))
|
| 136 |
stats["tables"] += 1
|
| 137 |
if len(text) < 50:
|
| 138 |
+
ocr_text, ocr_err = ocr_page(page)
|
| 139 |
+
if ocr_err:
|
| 140 |
+
ocr_errors.append((i, ocr_err))
|
| 141 |
if len(ocr_text) > len(text):
|
| 142 |
text = ocr_text
|
| 143 |
ocr_pages.append(i)
|
|
|
|
| 148 |
if ocr_pages:
|
| 149 |
shown = ", ".join(map(str, ocr_pages[:15])) + ("..." if len(ocr_pages) > 15 else "")
|
| 150 |
stats["notes"].append(f"OCR used on page(s) {shown}. Text may contain recognition errors.")
|
| 151 |
+
if ocr_errors:
|
| 152 |
+
# Report the first error once; it usually repeats for every page.
|
| 153 |
+
first_pages = ", ".join(str(p) for p, _ in ocr_errors[:15]) + ("..." if len(ocr_errors) > 15 else "")
|
| 154 |
+
stats["notes"].append(
|
| 155 |
+
f"OCR could not run on page(s) {first_pages}: {ocr_errors[0][1]}. "
|
| 156 |
+
"Check that Tesseract and its language data (eng, ara) are installed."
|
| 157 |
+
)
|
| 158 |
if empty_pages:
|
| 159 |
shown = ", ".join(map(str, empty_pages[:15])) + ("..." if len(empty_pages) > 15 else "")
|
| 160 |
+
stats["notes"].append(
|
| 161 |
+
f"No readable text found on page(s) {shown}"
|
| 162 |
+
+ ("." if ocr_errors else ", even after OCR.")
|
| 163 |
+
)
|
| 164 |
return blocks, stats
|
| 165 |
|
| 166 |
def extract_docx(name, data):
|