docs(office): add section 19 to roadmap and changelog for two-phase handwritten ocr pipeline
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: services/office/document_extractor.py
|
||||
PROJECT: SCUD Orion AI (Office Domain)
|
||||
ROLE: Постраничное извлечение текста и OCR сканов документов через Vision LLM.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
import fitz # PyMuPDF
|
||||
from modules.web_api.llm.core.ollama_client import call_ollama_chat
|
||||
|
||||
logger = logging.getLogger("OFFICE_EXTRACTOR")
|
||||
|
||||
|
||||
def ocr_image_b64(image_b64: str, page_num: int = 1) -> str:
|
||||
"""
|
||||
Распознает текст с одного растрового изображения через Qwen 2.5 VL.
|
||||
"""
|
||||
system_prompt = (
|
||||
"Ты — высокоточный профессиональный модуль OCR для канцелярии и документооборота.\n"
|
||||
"Твоя задача — точно переписать весь текст с предоставленного изображения документа.\n"
|
||||
"ПРАВИЛА:\n"
|
||||
"1. Переписывай текст дословно, сохраняя структуру, заголовки, списки, нумерацию и таблицы.\n"
|
||||
"2. Запрещено добавлять вводные слова, приветствия, комментарии ('Спасибо за обращение', 'Вот текст' и т.д.).\n"
|
||||
"3. Выводи СТРОГО чистый распознанный текст документа."
|
||||
)
|
||||
user_message = {
|
||||
"role": "user",
|
||||
"content": f"Распознай весь печатный и рукописный текст страницы №{page_num} без пропусков.",
|
||||
"images": [image_b64]
|
||||
}
|
||||
try:
|
||||
res = call_ollama_chat(
|
||||
messages=[{"role": "system", "content": system_prompt}, user_message],
|
||||
is_vision=True,
|
||||
timeout=300 # 5 минут на тяжелые страницы
|
||||
)
|
||||
return (res.get("content") or "").strip()
|
||||
except Exception as e:
|
||||
logger.error(f"[Office OCR] Ошибка распознавания страницы {page_num}: {e}")
|
||||
return f"[Ошибка распознавания страницы {page_num}: {e}]"
|
||||
|
||||
|
||||
def process_pdf_full(file_path: str, max_pages: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Обрабатывает PDF целиком:
|
||||
- Если есть качественный цифровой текст — мгновенно извлекает его со всех страниц.
|
||||
- Если страница является сканом/картинкой — рендерит ее в 200 DPI и прогоняет через Vision OCR.
|
||||
"""
|
||||
results = []
|
||||
doc = fitz.open(file_path)
|
||||
total_pages = len(doc)
|
||||
limit = min(total_pages, max_pages) if max_pages else total_pages
|
||||
|
||||
logger.info(f"[Office] Начало обработки PDF: {os.path.basename(file_path)} (всего страниц: {total_pages})")
|
||||
|
||||
for idx in range(limit):
|
||||
page_num = idx + 1
|
||||
page = doc[idx]
|
||||
extracted_text = (page.get_text("text") or "").strip()
|
||||
|
||||
# Если на странице есть хороший машинный текст (не скан)
|
||||
if len(extracted_text) > 80:
|
||||
logger.info(f"[Office] Страница {page_num}/{limit}: извлечен цифровой текст ({len(extracted_text)} симв.)")
|
||||
results.append({
|
||||
"page": page_num,
|
||||
"method": "DIGITAL_TEXT",
|
||||
"text": extracted_text
|
||||
})
|
||||
else:
|
||||
# Чистый скан — рендерим страницу в PNG и передаем в Vision LLM
|
||||
logger.info(f"[Office] Страница {page_num}/{limit}: распознавание скана через Ollama Vision...")
|
||||
pix = page.get_pixmap(dpi=200)
|
||||
img_b64 = base64.b64encode(pix.tobytes("png")).decode("utf-8")
|
||||
|
||||
ocr_text = ocr_image_b64(img_b64, page_num=page_num)
|
||||
results.append({
|
||||
"page": page_num,
|
||||
"method": "VISION_OCR",
|
||||
"text": ocr_text
|
||||
})
|
||||
|
||||
doc.close()
|
||||
return results
|
||||
Reference in New Issue
Block a user