Spaces:
Sleeping
Sleeping
| """ | |
| utils.py — file/URL text extraction with thorough cleaning. | |
| RCA fixes: | |
| - Regex syntax error on line 63 of old version fixed | |
| - Cleaning now returns proper text before chunking | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from typing import Tuple | |
| def _pip_install(*pkgs): | |
| subprocess.check_call( | |
| [sys.executable, "-m", "pip", "install", "-q", "--no-warn-script-location", *pkgs], | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, | |
| ) | |
| def _sniff_ext(filepath: str) -> str: | |
| try: | |
| with open(filepath, "rb") as f: | |
| h = f.read(8) | |
| if h[:4] == b"%PDF": | |
| return ".pdf" | |
| if h[:4] == b"PK\x03\x04": | |
| return ".docx" | |
| except Exception: | |
| pass | |
| return ".txt" | |
| # --------------------------------------------------------------------------- | |
| # Main entry point | |
| # --------------------------------------------------------------------------- | |
| def extract_text_from_file(filepath: str) -> Tuple[str, str]: | |
| path = Path(filepath) | |
| ext = path.suffix.lower().strip() | |
| name = path.stem or "document" | |
| if ext not in (".pdf", ".docx", ".doc", ".txt", ".md"): | |
| ext = _sniff_ext(filepath) | |
| if ext == ".pdf": | |
| raw = _extract_pdf(filepath) | |
| elif ext in (".docx", ".doc"): | |
| raw = _extract_docx(filepath) | |
| else: | |
| raw = path.read_text(encoding="utf-8", errors="ignore") | |
| text = clean_text(raw) | |
| if len(text) < 50: | |
| raise ValueError( | |
| f"Almost no usable text found in '{path.name}'. " | |
| "It may be a scanned/image-only PDF." | |
| ) | |
| return text, name | |
| # --------------------------------------------------------------------------- | |
| # Text cleaning (THE most important function — fixes RCA #1 and #3) | |
| # --------------------------------------------------------------------------- | |
| def clean_text(text: str) -> str: | |
| """ | |
| Clean raw PDF/DOCX text into fluent readable prose. | |
| This runs BEFORE chunking so the LLM only ever sees clean text. | |
| """ | |
| # 1. Fix PDF hyphenation breaks: "develop-\nment" → "development" | |
| # Only rejoin when next line starts lowercase (continuation of word) | |
| text = re.sub(r"-\n\s*([a-z])", r"\1", text) | |
| # If next line starts uppercase, new sentence — add space instead | |
| text = re.sub(r"-\n\s*([A-Z])", r" \1", text) | |
| # 2. Rejoin wrapped lines that are clearly mid-sentence | |
| # e.g. "This is a long\nsentence that continues" → one line | |
| text = re.sub(r"(?<=[a-zA-Z,;])\n(?=[a-z])", " ", text) | |
| # 3. Remove known academic PDF junk lines | |
| junk_patterns = [ | |
| r"this (document|material) is authorized for use only", | |
| r"this material has been prepared", | |
| r"no part of this material", | |
| r"reproduced.*?without.*?permission", | |
| r"transmitted in any form", | |
| r"meant for use only", | |
| r"all rights reserved", | |
| r"©|copyright\s*\d{4}", | |
| r"^\s*page\s*\d+\s*$", | |
| r"^\s*\d+\s*$", | |
| r"hbs no\.\s*[\d\-]+", | |
| r"harvard business school", | |
| r"s p jain|spjimr", | |
| r"prof\.\s+\w+.*?©", | |
| r"from\s+\w+\s+\d{4}\s+to\s+\w+\s+\d{4}", | |
| ] | |
| lines = text.splitlines() | |
| clean_lines = [] | |
| for line in lines: | |
| stripped = line.strip() | |
| if not stripped: | |
| clean_lines.append("") | |
| continue | |
| if any(re.search(p, stripped, re.IGNORECASE) for p in junk_patterns): | |
| continue | |
| clean_lines.append(stripped) | |
| text = "\n".join(clean_lines) | |
| # 4. Fix PDF column extraction: missing spaces before capital mid-sentence | |
| # e.g. "strategyand" → "strategy and", "resourcesto" → "resources to" | |
| # We do NOT remove spaces — that causes "she must" → "shemust". | |
| # Instead only ADD spaces where clearly missing between two lowercase words. | |
| # Pattern: lowercase letter immediately followed by uppercase (no space) — split there. | |
| text = re.sub(r"([a-z])([A-Z][a-z])", r"\1 \2", text) | |
| # 5. Remove ALL-CAPS lines that are just headers/titles repeated in body | |
| text = re.sub(r"\n[A-Z][A-Z\s:\-]{25,}\n", "\n", text) | |
| # 6. Collapse excessive blank lines | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| # 7. Strip whitespace per line | |
| text = "\n".join(l.strip() for l in text.splitlines()) | |
| return text.strip() | |
| # --------------------------------------------------------------------------- | |
| # PDF extraction | |
| # --------------------------------------------------------------------------- | |
| def _extract_pdf(filepath: str) -> str: | |
| try: | |
| from pypdf import PdfReader | |
| except ImportError: | |
| _pip_install("pypdf") | |
| from pypdf import PdfReader | |
| reader = PdfReader(filepath) | |
| pages = [] | |
| for page in reader.pages: | |
| try: | |
| t = page.extract_text() | |
| if t and t.strip(): | |
| pages.append(t.strip()) | |
| except Exception: | |
| pass | |
| if not pages: | |
| raise ValueError( | |
| "No text extracted from PDF. It may be a scanned image-only PDF. " | |
| "Please use a text-based (selectable text) PDF." | |
| ) | |
| return "\n\n".join(pages) | |
| # --------------------------------------------------------------------------- | |
| # DOCX extraction | |
| # --------------------------------------------------------------------------- | |
| def _extract_docx(filepath: str) -> str: | |
| try: | |
| import docx as _docx | |
| except ImportError: | |
| _pip_install("python-docx") | |
| import docx as _docx | |
| doc = _docx.Document(filepath) | |
| parts = [p.text.strip() for p in doc.paragraphs if p.text.strip()] | |
| for table in doc.tables: | |
| for row in table.rows: | |
| for cell in row.cells: | |
| if cell.text.strip(): | |
| parts.append(cell.text.strip()) | |
| return "\n\n".join(parts) | |
| # --------------------------------------------------------------------------- | |
| # URL extraction | |
| # --------------------------------------------------------------------------- | |
| def extract_text_from_url(url: str) -> Tuple[str, str]: | |
| import requests | |
| from urllib.parse import urlparse | |
| if "arxiv.org" in url: | |
| url = url.replace("/pdf/", "/abs/").replace(".pdf", "") | |
| if not url.startswith("http"): | |
| url = "https://" + url | |
| headers = {"User-Agent": "Mozilla/5.0 (compatible; VoiceVerse/1.0)"} | |
| try: | |
| resp = requests.get(url, headers=headers, timeout=20) | |
| resp.raise_for_status() | |
| except Exception as e: | |
| raise ValueError(f"Could not fetch URL: {e}") | |
| html = resp.text | |
| doc_name = _title_from_html(html) or urlparse(url).netloc | |
| text = _html_to_text(html) | |
| text = clean_text(text) | |
| if len(text.strip()) < 100: | |
| raise ValueError("Could not extract enough text from that URL.") | |
| return text, doc_name | |
| def _title_from_html(html: str) -> str: | |
| m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S) | |
| return re.sub(r"\s+", " ", m.group(1)).strip()[:80] if m else "" | |
| def _html_to_text(html: str) -> str: | |
| try: | |
| from bs4 import BeautifulSoup | |
| soup = BeautifulSoup(html, "html.parser") | |
| for tag in soup(["script", "style", "nav", "footer", "header", | |
| "aside", "form", "button", "svg"]): | |
| tag.decompose() | |
| main = (soup.find("article") or soup.find("main") | |
| or soup.find("body") or soup) | |
| lines = [l.strip() for l in main.get_text("\n").splitlines() | |
| if len(l.strip()) > 15] | |
| return "\n\n".join(lines) | |
| except Exception: | |
| text = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", html, flags=re.S | re.I) | |
| text = re.sub(r"<[^>]+>", " ", text) | |
| return re.sub(r"\s+", " ", text).strip() | |
| def get_short_summary(text: str, max_chars: int = 250) -> str: | |
| clean = re.sub(r"\s+", " ", text.strip()) | |
| if len(clean) <= max_chars: | |
| return clean | |
| cut = clean[:max_chars].rfind(" ") | |
| return clean[: cut if cut > max_chars - 30 else max_chars] + "…" |