feat: 百度通用文字识别提取供应商/日期(config api_key 持久化)

This commit is contained in:
2026-07-19 19:23:38 +08:00
parent cc66448327
commit 056e7d8e75
14 changed files with 1198 additions and 17 deletions
+162
View File
@@ -0,0 +1,162 @@
"""批量处理服务:扫描 data/input/ 下图片,串行跑完整 OCR → 采购单流程。
- 不合并(每张图片单独出一个 result)
- 默认串行(避免百度 OCR 限流)
- 返回 dict 汇总:total / success / failed / results[]
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from ..config.settings import ConfigManager
from ..core.utils.log_utils import get_logger
from .ocr_service import OCRService
from .order_service import OrderService
logger = get_logger(__name__)
IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.bmp', '.webp', '.tif', '.tiff'}
class BatchService:
"""批量处理服务。"""
def __init__(self, config: Optional[ConfigManager] = None):
self.config = config or ConfigManager()
self.ocr_service = OCRService(self.config)
self.order_service = OrderService(self.config)
self._input_folder = self.config.get_path(
'Paths', 'input_folder', fallback='data/input', create=True
)
@property
def input_folder(self) -> str:
return self._input_folder
def list_input_images(self) -> List[str]:
"""列出 data/input/ 下所有待处理图片(按 mtime 升序,先入先出)。"""
if not os.path.isdir(self._input_folder):
return []
files = []
for name in os.listdir(self._input_folder):
p = os.path.join(self._input_folder, name)
if not os.path.isfile(p):
continue
ext = os.path.splitext(name)[1].lower()
if ext in IMAGE_EXTS:
files.append(p)
files.sort(key=lambda p: os.path.getmtime(p))
return files
def process_all_inputs(
self,
progress_cb: Optional[Callable[[int, int, dict], None]] = None,
) -> Dict[str, Any]:
"""批量处理 data/input/ 下全部图片。
Args:
progress_cb: 可选回调 fn(done_count, total_count, latest_result_dict)
Returns:
{
'total': int,
'success': int,
'failed': int,
'results': [
{
'image': str,
'hash': str,
'status': 'success' | 'failed',
'supplier': str,
'bill_date': str,
'total_amount': float,
'result_file': str | None,
'error': str | None,
},
...
]
}
"""
images = self.list_input_images()
total = len(images)
results: List[Dict[str, Any]] = []
if total == 0:
logger.info("data/input/ 下无待处理图片")
return {'total': 0, 'success': 0, 'failed': 0, 'results': []}
logger.info(f"批量处理开始: 共 {total} 张图片")
success = 0
failed = 0
for idx, image_path in enumerate(images, start=1):
entry: Dict[str, Any] = {
'image': image_path,
'hash': '',
'status': 'failed',
'supplier': '',
'bill_date': '',
'total_amount': 0.0,
'result_file': None,
'error': None,
}
try:
file_hash = Path(image_path).stem
entry['hash'] = file_hash
# 1) OCR(写 data/output/{hash}.xlsx + .meta.json
excel_path = self.ocr_service.process_image(image_path)
if not excel_path:
entry['error'] = 'OCR 失败'
failed += 1
results.append(entry)
if progress_cb:
progress_cb(idx, total, entry)
continue
# 2) process_excel(识别元信息 + 重命名 + 落库)
result_file = self.order_service.process_excel(excel_path)
if not result_file:
entry['error'] = '处理失败'
failed += 1
results.append(entry)
if progress_cb:
progress_cb(idx, total, entry)
continue
# 3) 回查元信息(process_excel 内部已落库)
meta_row = self.order_service.metadata_db.get(file_hash) or {}
entry['result_file'] = result_file
entry['supplier'] = meta_row.get('supplier', '') or ''
entry['bill_date'] = meta_row.get('bill_date', '') or ''
entry['total_amount'] = float(meta_row.get('total_amount') or 0.0)
entry['status'] = 'success'
success += 1
logger.info(
f"[{idx}/{total}] 处理完成: {Path(image_path).name} -> "
f"{Path(result_file).name} | 供应商={entry['supplier']!r} "
f"日期={entry['bill_date']!r} 金额={entry['total_amount']:.2f}"
)
except Exception as e:
logger.error(f"[{idx}/{total}] 处理异常: {image_path}: {e}", exc_info=True)
entry['error'] = str(e)
failed += 1
results.append(entry)
if progress_cb:
progress_cb(idx, total, entry)
summary = {
'total': total,
'success': success,
'failed': failed,
'results': results,
}
logger.info(
f"批量处理完成: 总 {total} 张, 成功 {success}, 失败 {failed}"
)
return summary