mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 04:15:35 +00:00
fix: handle non text uploads more carefully
This commit is contained in:
1 parent
82691314ca
commit
3030aba39d
21 files changed
+729
-40
No files matched your search
@@ -23,6 +23,7 @@ from docsgpt.core.settings import settings
|
||||
from docsgpt.parser.file.base_parser import (
|
||||
BaseParser,
|
||||
DocumentParseError,
|
||||
NoTextLayerError,
|
||||
delegate_parse,
|
||||
module_available,
|
||||
)
|
||||
@@ -424,7 +425,7 @@ class AnydocParser(BaseParser):
|
||||
"tesseract binary or a DeepSeek-OCR endpoint (OCR_ENGINE), or "
|
||||
"through the optional docling extra when installed."
|
||||
)
|
||||
raise DocumentParseError(
|
||||
raise NoTextLayerError(
|
||||
f"{path.name} appears to be a scanned PDF (no text layer), and "
|
||||
f"{type(fallback).__name__} extracted almost nothing{hint}"
|
||||
)
|
||||
|
||||
@@ -26,6 +26,17 @@ class DocumentParseError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
class NoTextLayerError(DocumentParseError):
|
||||
"""A readable document with no text to extract, such as a scanned PDF.
|
||||
|
||||
Still a failed parse wherever text is the point (source ingestion needs
|
||||
something to embed), so it subclasses ``DocumentParseError`` and callers
|
||||
that do not know about it keep failing loudly. The file itself is intact,
|
||||
though: a chat attachment can still go to a model that reads the format
|
||||
natively, which is why the attachment worker catches this type.
|
||||
"""
|
||||
|
||||
|
||||
class BaseParser:
|
||||
"""Base class for all parsers."""
|
||||
|
||||
|
||||
@@ -105,9 +105,6 @@ def _image_entries(factory: Callable[[], BaseParser], suffixes) -> Dict[str, Bas
|
||||
return {suffix: factory() for suffix in suffixes}
|
||||
|
||||
|
||||
_LEGACY_IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg")
|
||||
|
||||
|
||||
def _legacy_file_extractor(pdf_text_fast_path: bool = False, ocr_enabled: bool = False) -> Dict[str, BaseParser]:
|
||||
"""Parser map that needs neither docling nor anydoc.
|
||||
|
||||
@@ -124,7 +121,10 @@ def _legacy_file_extractor(pdf_text_fast_path: bool = False, ocr_enabled: bool =
|
||||
from docsgpt.parser.file.ocr_parser import IMAGE_SUFFIXES
|
||||
|
||||
pdf_parser: BaseParser = PDFParser()
|
||||
images: Dict[str, BaseParser] = _image_entries(ImageParser, _LEGACY_IMAGE_SUFFIXES)
|
||||
# Every image suffix gets an entry even without OCR: the image itself is
|
||||
# what a vision model is sent, so a missing entry only means the upload is
|
||||
# refused before any model sees it (.webp was, in production).
|
||||
images: Dict[str, BaseParser] = _image_entries(ImageParser, sorted(IMAGE_SUFFIXES))
|
||||
if ocr_enabled:
|
||||
native = _native_ocr_parsers()
|
||||
if native is not None:
|
||||
@@ -490,6 +490,7 @@ class SimpleDirectoryReader(BaseReader):
|
||||
data: Union[str, List[str]] = ""
|
||||
data_list: List[str] = []
|
||||
metadata_list = []
|
||||
first_error: Optional[DocumentParseError] = None
|
||||
self.file_token_counts = {}
|
||||
self.failed_files = []
|
||||
|
||||
@@ -529,6 +530,7 @@ class SimpleDirectoryReader(BaseReader):
|
||||
except DocumentParseError as e:
|
||||
logging.warning(f"Skipping unreadable file {input_file.name}: {e}")
|
||||
self.failed_files.append((input_file, str(e)))
|
||||
first_error = first_error or e
|
||||
report_progress(file_index + 1)
|
||||
continue
|
||||
|
||||
@@ -579,10 +581,10 @@ class SimpleDirectoryReader(BaseReader):
|
||||
# ingest tasks) treat it as terminal and tell the user.
|
||||
if self.failed_files and not data_list:
|
||||
if len(self.failed_files) == 1:
|
||||
# Single-file read (the attachment path): the parser's own
|
||||
# message reaches the user verbatim, so don't wrap it in
|
||||
# "None of the 1 file(s)…".
|
||||
raise DocumentParseError(self.failed_files[0][1])
|
||||
# Single-file read (the attachment path): re-raise the parser's
|
||||
# own exception, so its message reaches the user verbatim and
|
||||
# its type (``NoTextLayerError`` for a scan) reaches the caller.
|
||||
raise first_error
|
||||
names = ", ".join(p.name for p, _ in self.failed_files[:5])
|
||||
raise DocumentParseError(
|
||||
f"None of the {len(self.failed_files)} files could be parsed: "
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
"""Image parser.
|
||||
|
||||
Contains parser for .png, .jpg, .jpeg files.
|
||||
Contains the parser for image files (.png, .jpg, .jpeg, .tiff, .tif, .bmp,
|
||||
.webp) and the PNG re-encoding for the formats model providers reject.
|
||||
|
||||
"""
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Dict, Tuple, Union
|
||||
|
||||
import requests
|
||||
from typing import Dict, Union
|
||||
|
||||
from docsgpt.parser.file.base_parser import BaseParser
|
||||
from docsgpt.core.settings import settings
|
||||
from docsgpt.parser.file.base_parser import BaseParser, DocumentParseError
|
||||
|
||||
# Image types the vision APIs refuse: OpenAI and Anthropic take png, jpeg,
|
||||
# webp and gif only. A chat attachment in one of these formats is re-encoded
|
||||
# to PNG before it is stored for the model.
|
||||
VISION_CONVERTIBLE_MIME_TYPES = frozenset({"image/tiff", "image/bmp", "image/x-ms-bmp"})
|
||||
|
||||
|
||||
def convert_image_to_png(file_obj: BinaryIO) -> Tuple[bytes, int]:
|
||||
"""Re-encode the first frame of an image as PNG.
|
||||
|
||||
Args:
|
||||
file_obj: Readable binary stream of the source image.
|
||||
|
||||
Returns:
|
||||
Tuple[bytes, int]: The PNG bytes, and how many frames (pages) the
|
||||
source had. Only the first is kept.
|
||||
|
||||
Raises:
|
||||
DocumentParseError: If the bytes are not an image Pillow can decode.
|
||||
"""
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
try:
|
||||
with Image.open(file_obj) as image:
|
||||
frames = getattr(image, "n_frames", 1)
|
||||
image.seek(0)
|
||||
frame = image
|
||||
if frame.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||
frame = frame.convert("RGBA" if "A" in frame.getbands() else "RGB")
|
||||
out = io.BytesIO()
|
||||
frame.save(out, format="PNG")
|
||||
except (UnidentifiedImageError, OSError) as exc:
|
||||
raise DocumentParseError(f"Could not read the image: {exc}") from exc
|
||||
return out.getvalue(), frames
|
||||
|
||||
|
||||
class ImageParser(BaseParser):
|
||||
@@ -24,8 +61,8 @@ class ImageParser(BaseParser):
|
||||
# alternatively you can use local vision capable LLM
|
||||
with open(file, "rb") as file_loaded:
|
||||
files = {'file': file_loaded}
|
||||
response = requests.post(doc2md_service, files=files, timeout=100)
|
||||
data = response.json()["markdown"]
|
||||
response = requests.post(doc2md_service, files=files, timeout=100)
|
||||
data = response.json()["markdown"]
|
||||
else:
|
||||
data = ""
|
||||
return data
|
||||
Reference in new issue
Block a user