Spaces:
Paused
Paused
| """Parser base types and shared file utilities.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Optional, Protocol | |
| TEXT_EXTENSIONS = {".txt", ".csv", ".tsv", ".json", ".xml", ".md", ".html", ".htm", ".log"} | |
| IMAGE_MIME = { | |
| ".png": "image/png", | |
| ".jpg": "image/jpeg", | |
| ".jpeg": "image/jpeg", | |
| ".gif": "image/gif", | |
| ".bmp": "image/bmp", | |
| ".webp": "image/webp", | |
| ".tiff": "image/tiff", | |
| ".tif": "image/tiff", | |
| } | |
| class ParserFile: | |
| """A file uploaded for parsing.""" | |
| filename: str | |
| content: bytes | |
| class ParserResult: | |
| """Structured output of a parser run.""" | |
| data: dict | |
| parser_name: str | |
| notes: tuple[str, ...] = field(default_factory=tuple) | |
| class AnswerSheetParser(Protocol): | |
| """Protocol for answer-sheet parsers. | |
| Implementations should be stateless — one instance per process is fine. | |
| """ | |
| name: str | |
| display_name: str | |
| description: str | |
| def can_handle(self, file_bytes: bytes, filename: str, data_type: str) -> bool: ... | |
| async def parse( | |
| self, | |
| files: list[ParserFile], | |
| data_type: str, | |
| description: str = "", | |
| model: str = "gpt-5.4", | |
| ) -> ParserResult: ... | |
| # --------------------------------------------------------------------------- | |
| # Shared file helpers | |
| # --------------------------------------------------------------------------- | |
| def get_extension(filename: str) -> str: | |
| return Path(filename).suffix.lower() | |
| def is_pdf(filename: str) -> bool: | |
| return get_extension(filename) == ".pdf" | |
| def is_image(filename: str) -> bool: | |
| return get_extension(filename) in IMAGE_MIME | |
| def get_image_mime(filename: str) -> Optional[str]: | |
| return IMAGE_MIME.get(get_extension(filename)) | |
| def try_read_as_text(file_bytes: bytes, filename: str) -> Optional[str]: | |
| """Decode text files using a list of likely encodings.""" | |
| if get_extension(filename) not in TEXT_EXTENSIONS: | |
| return None | |
| for encoding in ("utf-8", "utf-8-sig", "big5", "gb2312", "shift_jis", "latin-1"): | |
| try: | |
| return file_bytes.decode(encoding) | |
| except (UnicodeDecodeError, ValueError): | |
| continue | |
| return None | |
| def pdf_has_text_layer(pdf_bytes: bytes) -> bool: | |
| """True if the PDF has an extractable text layer (i.e., not an image scan).""" | |
| try: | |
| import fitz # local import so parsers without pymupdf don't die | |
| except ImportError: | |
| return False | |
| try: | |
| doc = fitz.open(stream=pdf_bytes, filetype="pdf") | |
| try: | |
| for page in doc: | |
| text = page.get_text("text").strip() | |
| if text: | |
| return True | |
| return False | |
| finally: | |
| doc.close() | |
| except Exception: | |
| return False | |