feat(ocr): PDF 文本层可用时跳过 worker 调用并补装 poppler-data
- OcrService 提取 PDF 文本层后若有效字符达到阈值,直接构建文档并写入结果缓存,不再触发 OCR worker,仅无文本层时才解析 python_bin/worker_path 调用 worker - _build_text_layer_document 复用 AggregatedOcrDocument 聚合文本层片段,_has_usable_pdf_text_layer 基于 meaningful_char_count 判定 - docker-compose 与 paddleocr bootstrap 脚本补装 poppler-data,保证 PDF 文本层抽取的中文编码正确 - 新增文本层直取与运行时依赖两项 ocr_service 单测
This commit is contained in:
@@ -77,8 +77,6 @@ class OcrService:
|
||||
documents: list[OcrRecognizeDocumentRead] = []
|
||||
prepared_inputs: list[PreparedOcrInput] = []
|
||||
cleanup_paths: list[Path] = []
|
||||
python_bin = self._resolve_python_bin()
|
||||
worker_path = self._resolve_worker_path()
|
||||
worker_payload: dict = {}
|
||||
cache_keys_by_source: dict[str, str] = {}
|
||||
|
||||
@@ -144,6 +142,16 @@ class OcrService:
|
||||
cleanup_paths=cleanup_paths,
|
||||
text_layer=text_layer,
|
||||
)
|
||||
if self._has_usable_pdf_text_layer(text_layer):
|
||||
document = self._build_text_layer_document(
|
||||
filename=normalized_name,
|
||||
media_type=resolved_media_type,
|
||||
text_layer=text_layer,
|
||||
pdf_inputs=pdf_inputs,
|
||||
)
|
||||
documents.append(document)
|
||||
self._write_cached_document(cache_key, document)
|
||||
continue
|
||||
prepared_inputs.extend(pdf_inputs)
|
||||
for item in pdf_inputs:
|
||||
cache_keys_by_source.setdefault(item.source_key, cache_key)
|
||||
@@ -175,6 +183,8 @@ class OcrService:
|
||||
cache_keys_by_source[source_key] = cache_key
|
||||
|
||||
if prepared_inputs:
|
||||
python_bin = self._resolve_python_bin()
|
||||
worker_path = self._resolve_worker_path()
|
||||
worker_payload = self._invoke_worker(
|
||||
python_bin=python_bin,
|
||||
worker_path=worker_path,
|
||||
@@ -308,6 +318,23 @@ class OcrService:
|
||||
while len(cls._result_cache) > OCR_RESULT_CACHE_LIMIT:
|
||||
cls._result_cache.popitem(last=False)
|
||||
|
||||
@classmethod
|
||||
def _write_cached_document(cls, cache_key: str, document: OcrRecognizeDocumentRead) -> None:
|
||||
if not cache_key:
|
||||
return
|
||||
with cls._cache_lock:
|
||||
cls._result_cache[cache_key] = document.model_copy(
|
||||
update={
|
||||
"receipt_id": "",
|
||||
"receipt_status": "",
|
||||
"receipt_preview_url": "",
|
||||
"receipt_source_url": "",
|
||||
}
|
||||
)
|
||||
cls._result_cache.move_to_end(cache_key)
|
||||
while len(cls._result_cache) > OCR_RESULT_CACHE_LIMIT:
|
||||
cls._result_cache.popitem(last=False)
|
||||
|
||||
@classmethod
|
||||
def _resolve_worker_semaphore(cls, limit: int) -> threading.Semaphore:
|
||||
normalized_limit = max(1, int(limit or 1))
|
||||
@@ -568,6 +595,30 @@ class OcrService:
|
||||
|
||||
return documents
|
||||
|
||||
def _build_text_layer_document(
|
||||
self,
|
||||
*,
|
||||
filename: str,
|
||||
media_type: str,
|
||||
text_layer: str,
|
||||
pdf_inputs: list[PreparedOcrInput],
|
||||
) -> OcrRecognizeDocumentRead:
|
||||
first_input = pdf_inputs[0] if pdf_inputs else None
|
||||
aggregated = AggregatedOcrDocument(
|
||||
filename=filename,
|
||||
media_type=media_type,
|
||||
source_key=first_input.source_key if first_input is not None else uuid4().hex,
|
||||
page_count=max(1, len(pdf_inputs)),
|
||||
preview_kind=str(first_input.preview_kind if first_input is not None else ""),
|
||||
preview_data_url=str(first_input.preview_data_url if first_input is not None else ""),
|
||||
)
|
||||
aggregated.text_layer_fragments.append(text_layer)
|
||||
return self._finalize_document(aggregated)
|
||||
|
||||
@classmethod
|
||||
def _has_usable_pdf_text_layer(cls, text_layer: str) -> bool:
|
||||
return cls._meaningful_char_count(text_layer) >= 8
|
||||
|
||||
@staticmethod
|
||||
def _collect_descriptor_text_layer(descriptors: list[PreparedOcrInput]) -> str:
|
||||
for descriptor in descriptors:
|
||||
|
||||
Reference in New Issue
Block a user