Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
# app.py - Hybrid RAG (FAISS semantic + BM25, fused with RRF) for multi-format documents.
|
| 2 |
-
# Handles: PDFs (text + tables), DOCX (with tables), PPTX, XLSX/CSV, TXT/MD. Arabic + English.
|
|
|
|
| 3 |
import io
|
| 4 |
import os
|
| 5 |
import re
|
|
@@ -24,6 +25,8 @@ CHUNK_WORDS = 80 # stays under the embedding model's 128-token limit
|
|
| 24 |
CHUNK_OVERLAP = 15
|
| 25 |
TABLE_CHUNK_WORDS = 60 # table/spreadsheet rows are token-heavy, so smaller groups
|
| 26 |
RRF_K = 60
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# ========================= TEXT NORMALIZATION =========================
|
| 29 |
DIACRITICS_RE = re.compile(r"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]")
|
|
@@ -63,11 +66,29 @@ def pdf_tables(page):
|
|
| 63 |
pass
|
| 64 |
return out
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
def extract_pdf(name, data):
|
| 67 |
blocks = []
|
| 68 |
stats = {"pages": 0, "tables": 0, "notes": []}
|
| 69 |
doc = fitz.open(stream=data, filetype="pdf")
|
| 70 |
-
empty_pages = []
|
| 71 |
for i, page in enumerate(doc, 1):
|
| 72 |
stats["pages"] += 1
|
| 73 |
loc = f"p.{i}"
|
|
@@ -75,13 +96,21 @@ def extract_pdf(name, data):
|
|
| 75 |
for tbl in pdf_tables(page):
|
| 76 |
blocks.append(make_block(tbl, name, loc, "table"))
|
| 77 |
stats["tables"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if len(text) < 50:
|
| 79 |
empty_pages.append(i)
|
| 80 |
if text:
|
| 81 |
blocks.append(make_block(text, name, loc, "text"))
|
|
|
|
|
|
|
|
|
|
| 82 |
if empty_pages:
|
| 83 |
shown = ", ".join(map(str, empty_pages[:15])) + ("..." if len(empty_pages) > 15 else "")
|
| 84 |
-
stats["notes"].append(f"
|
| 85 |
return blocks, stats
|
| 86 |
|
| 87 |
def extract_docx(name, data):
|
|
@@ -301,7 +330,7 @@ if uploaded_files:
|
|
| 301 |
signature = tuple(sorted((f.name, f.size) for f in uploaded_files))
|
| 302 |
if st.session_state.get("signature") != signature:
|
| 303 |
all_blocks, report = [], {}
|
| 304 |
-
with st.spinner("Reading documents..."):
|
| 305 |
for f in uploaded_files:
|
| 306 |
try:
|
| 307 |
blocks, stats = extract_file(f.name, f.getvalue())
|
|
|
|
| 1 |
# app.py - Hybrid RAG (FAISS semantic + BM25, fused with RRF) for multi-format documents.
|
| 2 |
+
# Handles: PDFs (text + tables, OCR for scanned pages), DOCX (with tables), PPTX, XLSX/CSV, TXT/MD. Arabic + English.
|
| 3 |
+
import glob
|
| 4 |
import io
|
| 5 |
import os
|
| 6 |
import re
|
|
|
|
| 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]")
|
|
|
|
| 66 |
pass
|
| 67 |
return out
|
| 68 |
|
| 69 |
+
def _set_tessdata():
|
| 70 |
+
"""Point PyMuPDF at Tesseract's language data (path differs by distro/version)."""
|
| 71 |
+
if os.environ.get("TESSDATA_PREFIX"):
|
| 72 |
+
return
|
| 73 |
+
for p in glob.glob("/usr/share/tesseract-ocr/*/tessdata") + ["/usr/share/tessdata"]:
|
| 74 |
+
if os.path.isdir(p):
|
| 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}"
|
|
|
|
| 96 |
for tbl in pdf_tables(page):
|
| 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)
|
| 104 |
if len(text) < 50:
|
| 105 |
empty_pages.append(i)
|
| 106 |
if text:
|
| 107 |
blocks.append(make_block(text, name, loc, "text"))
|
| 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):
|
|
|
|
| 330 |
signature = tuple(sorted((f.name, f.size) for f in uploaded_files))
|
| 331 |
if st.session_state.get("signature") != signature:
|
| 332 |
all_blocks, report = [], {}
|
| 333 |
+
with st.spinner("Reading documents (scanned pages are OCR'd and may take a while)..."):
|
| 334 |
for f in uploaded_files:
|
| 335 |
try:
|
| 336 |
blocks, stats = extract_file(f.name, f.getvalue())
|