mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 04:15:35 +00:00
Fixes batch 2
This commit is contained in:
1 parent
c17caf24ec
commit
4e14a79923
16 files changed
+278
-31
No files matched your search
@@ -46,7 +46,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
# backend, read_document's structured output) — OFF by default: it pulls the
|
||||
# layout/OCR model stack and adds gigabytes to the image. anydoc (in
|
||||
# requirements.txt) is the default parser and needs none of it, and OCR runs
|
||||
# natively on tesseract (below) without it.
|
||||
# natively on tesseract (below, also opt-in) without it.
|
||||
ARG INSTALL_DOCLING=false
|
||||
RUN if [ "$INSTALL_DOCLING" = "true" ]; then \
|
||||
pip install --no-cache-dir -r requirements-docling.txt; \
|
||||
@@ -67,12 +67,14 @@ RUN apt-get update && \
|
||||
ln -s /usr/bin/python3.12 /usr/bin/python && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# OCR (OCR_ENABLED=true) runs on the system tesseract binary by default,
|
||||
# with or without the docling extra, so ship it in every image (~35 MB).
|
||||
# Opt out with --build-arg INSTALL_TESSERACT=false (e.g. a DeepSeek-OCR-only
|
||||
# deployment). Extra language packs are a deployment concern
|
||||
# (apt: tesseract-ocr-<lang>, then list them in OCR_LANGS).
|
||||
ARG INSTALL_TESSERACT=true
|
||||
# Optional tesseract OCR engine (OCR_ENABLED=true with OCR_ENGINE=tesseract,
|
||||
# the default engine) — OFF by default like every other OCR dependency; OCR
|
||||
# itself is off unless configured. Opt in with --build-arg
|
||||
# INSTALL_TESSERACT=true (setup.sh writes it to .env when OCR is enabled);
|
||||
# ~35 MB of system packages. Extra language packs are a deployment concern
|
||||
# (apt: tesseract-ocr-<lang>, then list them in OCR_LANGS). A DeepSeek-OCR
|
||||
# endpoint (OCR_ENGINE=deepseek) needs none of this.
|
||||
ARG INSTALL_TESSERACT=false
|
||||
RUN if [ "$INSTALL_TESSERACT" = "true" ]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends tesseract-ocr tesseract-ocr-eng && \
|
||||
|
||||
@@ -165,8 +165,9 @@ class Settings(BaseSettings):
|
||||
# menu):
|
||||
# tesseract — recommended: best classic-engine accuracy (perfect EN word
|
||||
# recall, 0.000 bilingual CER, 100% table cells), ~35 MB, CPU-only.
|
||||
# Needs the system binary + language packs (the Docker image ships
|
||||
# them unless built with INSTALL_TESSERACT=false). Both backends.
|
||||
# Needs the system binary + language packs: an optional install like
|
||||
# every OCR dependency (build with INSTALL_TESSERACT=true, or apt/brew
|
||||
# install tesseract-ocr for a local run). Both backends.
|
||||
# deepseek — DeepSeek-OCR against an Ollama/vLLM endpoint
|
||||
# (OCR_DEEPSEEK_*). Best table/CJK quality; the worker stays light
|
||||
# (no layout models) but each page costs seconds on the model server.
|
||||
|
||||
@@ -186,6 +186,13 @@ def _pick_parser(suffix: str, *, ocr_enabled: bool, engine: str):
|
||||
legacy = _legacy_parser_for(suffix)
|
||||
if legacy is not None:
|
||||
return legacy
|
||||
from application.parser.file.anydoc_parser import ANYDOC_GAINED_SUFFIXES
|
||||
|
||||
if suffix in ANYDOC_GAINED_SUFFIXES:
|
||||
# anydoc-only office formats have no legacy parser. Falling through
|
||||
# would run the configured engine the caller explicitly opted out
|
||||
# of; images/audio/.txt still fall through (no legacy alternative).
|
||||
return None
|
||||
if engine in ("docling", "anydoc"):
|
||||
extractor = get_default_file_extractor(ocr_enabled=ocr_enabled, engine=engine)
|
||||
else:
|
||||
@@ -529,6 +536,16 @@ def _shape(
|
||||
}
|
||||
|
||||
parser = _pick_parser(suffix, ocr_enabled=ocr_enabled, engine=engine)
|
||||
# Under OCR_BACKEND=native with the docling engine the PDF parser is the
|
||||
# native OCR parser wrapping a DoclingParser as its text_parser. A PDF
|
||||
# with a text layer on every page is handed to that parser untouched, so
|
||||
# when tables are wanted run it directly: the single docling conversion
|
||||
# below then yields both the markdown and the tables. Scans and mixed
|
||||
# documents keep the native parser (and the exclusion just below).
|
||||
if include_tables and output != "chunks" and _is_native_ocr_parser(parser):
|
||||
delegate = parser.text_layer_delegate(path)
|
||||
if _is_docling_parser(delegate):
|
||||
parser = delegate
|
||||
# Tables come from a Docling conversion, so they are only collected when
|
||||
# a Docling parser is actually in play: under anydoc/fast a table pass
|
||||
# would be a second, full docling conversion that costs more than the
|
||||
@@ -569,6 +586,11 @@ def _shape(
|
||||
from application.parser.file.anydoc_parser import ANYDOC_GAINED_SUFFIXES
|
||||
|
||||
if suffix in ANYDOC_GAINED_SUFFIXES:
|
||||
if engine == "fast":
|
||||
return {
|
||||
"error": f"The fast engine has no parser for {suffix} files; "
|
||||
"use engine='anydoc' or 'auto'."
|
||||
}
|
||||
return {
|
||||
"error": f"No parser is available for {suffix} files: firecrawl-anydoc "
|
||||
"is not installed (pip install firecrawl-anydoc)"
|
||||
|
||||
@@ -103,8 +103,8 @@ def _resolve_ocr_engine(requested: Optional[str]) -> str:
|
||||
if engine == "tesseract" and shutil.which("tesseract") is None:
|
||||
logger.warning(
|
||||
"OCR_ENGINE=tesseract but no tesseract binary is on PATH (install "
|
||||
"tesseract-ocr plus language packs; the Docker image ships them unless "
|
||||
"built with INSTALL_TESSERACT=false); using docling auto-selection"
|
||||
"tesseract-ocr plus language packs: Docker builds need "
|
||||
"INSTALL_TESSERACT=true); using docling auto-selection"
|
||||
)
|
||||
return "auto"
|
||||
if engine == "ocrmac" and (
|
||||
|
||||
@@ -315,9 +315,9 @@ class TesseractEngine:
|
||||
if not self.available():
|
||||
raise OcrUnavailableError(
|
||||
"OCR_ENGINE=tesseract but no tesseract binary is on PATH. Install "
|
||||
"tesseract-ocr plus language packs (the Docker image ships them "
|
||||
"unless built with INSTALL_TESSERACT=false), or set "
|
||||
"OCR_ENGINE=deepseek / install the docling extra."
|
||||
"tesseract-ocr plus language packs (Docker: build with "
|
||||
"INSTALL_TESSERACT=true; local: apt/brew install tesseract-ocr), "
|
||||
"or set OCR_ENGINE=deepseek / install the docling extra."
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
@@ -699,6 +699,32 @@ class NativeOcrPdfParser(BaseParser):
|
||||
self.last_engine = engine.name
|
||||
return texts
|
||||
|
||||
def text_layer_delegate(self, file: Path) -> Optional[BaseParser]:
|
||||
"""The ``text_parser`` that ``parse_file`` would hand ``file`` to, or None.
|
||||
|
||||
None when there is no text parser, the file cannot be opened, or any
|
||||
page falls below ``min_text_chars`` (those are OCR'd here). Callers
|
||||
that want a by-product of the delegate's conversion (docling tables)
|
||||
can run it directly instead of converting the document twice.
|
||||
"""
|
||||
if self.text_parser is None:
|
||||
return None
|
||||
try:
|
||||
import pypdfium2 as pdfium
|
||||
|
||||
pdf = pdfium.PdfDocument(str(Path(file)))
|
||||
except Exception: # noqa: BLE001 - a probe; parse_file reports the real error
|
||||
return None
|
||||
try:
|
||||
counts = self._text_layer_counts(pdf)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
finally:
|
||||
pdf.close()
|
||||
if counts and all(count >= self.min_text_chars for count in counts):
|
||||
return self.text_parser
|
||||
return None
|
||||
|
||||
def _delegate_text(self, path: Path, errors: str) -> Union[str, List[str]]:
|
||||
text_parser = self.text_parser
|
||||
assert text_parser is not None
|
||||
|
||||
@@ -29,7 +29,7 @@ import zlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List, Optional, Union
|
||||
|
||||
_OBJ = re.compile(rb"\d+\s+\d+\s+obj(.*?)endobj", re.DOTALL)
|
||||
_OBJ_HEADER = re.compile(rb"\d+\s+\d+\s+obj")
|
||||
_TYPE0 = re.compile(rb"/Subtype\s*/Type0")
|
||||
# The CJK expectation is keyed on CIDSystemInfo /Ordering ALONE — the
|
||||
# authoritative "this PDF maps text through a CJK character collection"
|
||||
@@ -99,6 +99,28 @@ def _stream_bodies(raw: bytes) -> Iterator[bytes]:
|
||||
yield body
|
||||
|
||||
|
||||
def _object_bodies(raw: bytes) -> Iterator[bytes]:
|
||||
"""The bytes between each ``N G obj`` header and the next ``endobj``.
|
||||
|
||||
A forward walk for the same reason as ``_stream_bodies``: the regex
|
||||
``\\d+\\s+\\d+\\s+obj(.*?)endobj`` rescans to end-of-file for every header
|
||||
that has no ``endobj`` after it, so a malformed (or crafted) PDF with many
|
||||
such headers costs headers x size — 2000 orphan headers in 1 MB measured
|
||||
13.5 s, on the default upload path. Advancing past each ``endobj`` keeps
|
||||
the walk linear and yields the same bodies.
|
||||
"""
|
||||
pos = 0
|
||||
while True:
|
||||
header = _OBJ_HEADER.search(raw, pos)
|
||||
if header is None:
|
||||
return
|
||||
end = raw.find(b"endobj", header.end())
|
||||
if end < 0:
|
||||
return
|
||||
yield raw[header.end():end]
|
||||
pos = end + 6
|
||||
|
||||
|
||||
def _decompressed_streams(raw: bytes) -> Iterator[bytes]:
|
||||
"""Each Flate stream in ``raw`` that decompresses, one at a time, capped."""
|
||||
for body in _stream_bodies(raw):
|
||||
@@ -145,8 +167,7 @@ def check_pdf_fonts(data: bytes) -> Dict[str, Union[int, bool]]:
|
||||
if b"/ToUnicode" not in unit:
|
||||
bare += 1
|
||||
|
||||
for match in _OBJ.finditer(data):
|
||||
body = match.group(1)
|
||||
for body in _object_bodies(data):
|
||||
if _TYPE0.search(body):
|
||||
count_font(body)
|
||||
# Each decompressed stream is scanned for every signal in one pass and
|
||||
|
||||
Reference in new issue
Block a user