muddasser commited on
Commit
1d173c9
·
verified ·
1 Parent(s): a892593

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -12
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 = 300 # lower (e.g. 200) for faster OCR at some cost in accuracy
29
  OCR_LANGS = "ara+eng"
 
 
 
30
 
31
  # ========================= TEXT NORMALIZATION =========================
32
- DIACRITICS_RE = re.compile(r"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]")
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("\u0640", "")
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 ocr_page(page):
79
- """OCR a scanned page. Returns "" if Tesseract or the language data is missing."""
 
80
  try:
81
- _set_tessdata()
82
- tp = page.get_textpage_ocr(language=OCR_LANGS, dpi=OCR_DPI, full=True)
83
- return unicodedata.normalize("NFKC", page.get_text("text", textpage=tp) or "").strip()
 
 
 
 
 
 
 
 
 
 
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(f"No readable text found on page(s) {shown}, even after OCR.")
 
 
 
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):