Pointf5ive commited on
Commit
f2cb114
Β·
verified Β·
1 Parent(s): df9f2bb

Restore latest extractor with OCR fallback

Browse files
Files changed (1) hide show
  1. src/codex_extractor.py +102 -3
src/codex_extractor.py CHANGED
@@ -69,6 +69,7 @@ import re
69
  import string
70
  from collections import Counter
71
  from pathlib import Path
 
72
  from typing import Any
73
 
74
  # ── OPTIONAL IMPORTS WITH GRACEFUL FALLBACK ──────────────────────────────────
@@ -79,6 +80,18 @@ try:
79
  except ImportError:
80
  PDF_AVAILABLE = False
81
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  try:
83
  import nltk
84
  _NLTK_DATA = ["punkt", "punkt_tab", "averaged_perceptron_tagger", "cmudict", "stopwords"]
@@ -221,6 +234,67 @@ def is_artefact_line(line: str) -> bool:
221
 
222
  # ── TEXT EXTRACTION ───────────────────────────────────────────────────────────
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  def extract_text_from_pdf(
225
  pdf_path: str | Path,
226
  start_page: int | None = None,
@@ -270,7 +344,7 @@ def extract_text_from_pdf(
270
 
271
  # Automatic front-matter detection (Fix 2).
272
  # Always skip the first page (cover).
273
- if page_idx == 0:
274
  continue
275
  if is_front_matter_page(page_text):
276
  continue
@@ -279,6 +353,24 @@ def extract_text_from_pdf(
279
 
280
  raw_text = "\n".join(raw_parts)
281
  story_text = "\n".join(story_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  return story_text, raw_text
283
 
284
 
@@ -1303,10 +1395,17 @@ def process_upload(
1303
  )
1304
 
1305
  if not story_text or len(story_text.split()) < 20:
 
 
 
 
 
 
 
1306
  return (
1307
  "ERROR: No usable text extracted from file. "
1308
- "Check the PDF contains selectable text (not scanned images), "
1309
- "or try specifying a story page range.",
1310
  {},
1311
  )
1312
 
 
69
  import string
70
  from collections import Counter
71
  from pathlib import Path
72
+ from shutil import which
73
  from typing import Any
74
 
75
  # ── OPTIONAL IMPORTS WITH GRACEFUL FALLBACK ──────────────────────────────────
 
80
  except ImportError:
81
  PDF_AVAILABLE = False
82
 
83
+ try:
84
+ import pypdfium2 as pdfium
85
+ PDFIUM_AVAILABLE = True
86
+ except Exception:
87
+ PDFIUM_AVAILABLE = False
88
+
89
+ try:
90
+ import pytesseract
91
+ PYTESSERACT_AVAILABLE = True
92
+ except Exception:
93
+ PYTESSERACT_AVAILABLE = False
94
+
95
  try:
96
  import nltk
97
  _NLTK_DATA = ["punkt", "punkt_tab", "averaged_perceptron_tagger", "cmudict", "stopwords"]
 
234
 
235
  # ── TEXT EXTRACTION ───────────────────────────────────────────────────────────
236
 
237
+ def _word_count(text: str) -> int:
238
+ return len(SIMPLE_TOKENISE_PATTERN.findall((text or "").lower()))
239
+
240
+
241
+ def _ocr_runtime_ready() -> bool:
242
+ return PDFIUM_AVAILABLE and PYTESSERACT_AVAILABLE and which("tesseract") is not None
243
+
244
+
245
+ def extract_text_from_pdf_ocr(
246
+ pdf_path: str | Path,
247
+ start_page: int | None = None,
248
+ end_page: int | None = None,
249
+ ) -> tuple[str, str]:
250
+ """
251
+ OCR fallback for scanned/image-only PDFs.
252
+ Returns (story_text, raw_text) using the same page-filter logic as native extraction.
253
+ """
254
+ if not PDFIUM_AVAILABLE:
255
+ raise RuntimeError("OCR fallback unavailable: pypdfium2 is not installed.")
256
+ if not PYTESSERACT_AVAILABLE:
257
+ raise RuntimeError("OCR fallback unavailable: pytesseract is not installed.")
258
+ if which("tesseract") is None:
259
+ raise RuntimeError("OCR fallback unavailable: tesseract binary is not installed.")
260
+
261
+ render_scale = float(os.getenv("OCR_RENDER_SCALE", "2.0"))
262
+ ocr_lang = os.getenv("OCR_LANG", "eng")
263
+ ocr_config = os.getenv("OCR_CONFIG", "--oem 1 --psm 6")
264
+ max_pages = int(os.getenv("OCR_MAX_PAGES", "300"))
265
+
266
+ raw_parts: list[str] = []
267
+ story_parts: list[str] = []
268
+
269
+ pdf = pdfium.PdfDocument(str(pdf_path))
270
+ total_pages = min(len(pdf), max_pages)
271
+
272
+ idx_start = (start_page - 1) if start_page is not None else 0
273
+ idx_end = (end_page - 1) if end_page is not None else (total_pages - 1)
274
+ idx_start = max(0, idx_start)
275
+ idx_end = min(total_pages - 1, idx_end)
276
+
277
+ for page_idx in range(total_pages):
278
+ page = pdf[page_idx]
279
+ bitmap = page.render(scale=render_scale)
280
+ pil_image = bitmap.to_pil()
281
+ page_text = pytesseract.image_to_string(pil_image, lang=ocr_lang, config=ocr_config) or ""
282
+ raw_parts.append(page_text)
283
+
284
+ if start_page is not None or end_page is not None:
285
+ if idx_start <= page_idx <= idx_end:
286
+ story_parts.append(page_text)
287
+ continue
288
+
289
+ if total_pages > 1 and page_idx == 0:
290
+ continue
291
+ if is_front_matter_page(page_text):
292
+ continue
293
+ story_parts.append(page_text)
294
+
295
+ return "\n".join(story_parts), "\n".join(raw_parts)
296
+
297
+
298
  def extract_text_from_pdf(
299
  pdf_path: str | Path,
300
  start_page: int | None = None,
 
344
 
345
  # Automatic front-matter detection (Fix 2).
346
  # Always skip the first page (cover).
347
+ if total_pages > 1 and page_idx == 0:
348
  continue
349
  if is_front_matter_page(page_text):
350
  continue
 
353
 
354
  raw_text = "\n".join(raw_parts)
355
  story_text = "\n".join(story_parts)
356
+
357
+ # If native text-layer extraction looks healthy, use it.
358
+ if _word_count(story_text) >= 20:
359
+ return story_text, raw_text
360
+
361
+ # Otherwise, try OCR fallback for scanned/image-only pages.
362
+ try:
363
+ ocr_story_text, ocr_raw_text = extract_text_from_pdf_ocr(
364
+ pdf_path,
365
+ start_page=start_page,
366
+ end_page=end_page,
367
+ )
368
+ if _word_count(ocr_story_text) >= max(20, _word_count(story_text)):
369
+ return ocr_story_text, ocr_raw_text
370
+ except Exception:
371
+ # Fall through: process_upload will raise a clear error if text is still insufficient.
372
+ pass
373
+
374
  return story_text, raw_text
375
 
376
 
 
1395
  )
1396
 
1397
  if not story_text or len(story_text.split()) < 20:
1398
+ if str(file_path).lower().endswith(".pdf") and not _ocr_runtime_ready():
1399
+ return (
1400
+ "ERROR: No usable text extracted from file and OCR runtime is unavailable. "
1401
+ "Install OCR dependencies (`pypdfium2`, `pytesseract`) and system package "
1402
+ "`tesseract-ocr` in the Space build.",
1403
+ {},
1404
+ )
1405
  return (
1406
  "ERROR: No usable text extracted from file. "
1407
+ "OCR fallback could not recover enough text. "
1408
+ "Try a cleaner scan, higher resolution pages, or a story page range.",
1409
  {},
1410
  )
1411