docs(office): add section 19 to roadmap and changelog for two-phase handwritten ocr pipeline

This commit is contained in:
2026-09-25 16:35:27 +03:00
parent 90656944c5
commit 11691701e1
8 changed files with 346 additions and 86 deletions
+50 -56
View File
@@ -1,21 +1,22 @@
"""
===============================================================================
FILE: modules/web_api/routers/chat.py
ROLE: Полнофункциональный роутер чата с извлечением текста из PDF и сканов,
поддержкой Function Calling, Fast-Path и оптического распознавания OCR.
ROLE: Роутер чата с чистым разделением:
- Диалог и команды СКУД/1С (через agent.py).
- Парсинг и извлечение документов без обрезания (через file_parser.py).
===============================================================================
"""
import os
import shutil
import base64
import logging
from typing import Optional, List, Dict, Any
from typing import Optional
from fastapi import APIRouter, Header, HTTPException, UploadFile, File, Form
from pydantic import BaseModel
from llm.agent import process_chat_message
from llm.file_parser import parse_uploaded_file
from config import BASE_DIR
logger = logging.getLogger("CHAT_API")
@@ -39,11 +40,6 @@ def resolve_user_id(authorization: Optional[str] = None, explicit_user_id: Optio
token = authorization.replace("Bearer ", "").strip()
if token.isdigit():
return int(token)
elif token.startswith("dev_token_"):
try:
return int(token.replace("dev_token_", ""))
except ValueError:
pass
return 1
@@ -56,8 +52,6 @@ async def chat_endpoint(payload: ChatMessageRequest, authorization: Optional[str
if not user_msg:
raise HTTPException(status_code=400, detail="Пустое сообщение")
logger.info(f"Сообщение от user_id={user_id}, session_id={session_id}: {user_msg}")
reply_text, history, action_payload = process_chat_message(
user_id=user_id,
user_message=user_msg,
@@ -86,60 +80,60 @@ async def chat_upload_endpoint(
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
file_context = ""
image_b64 = None
user_msg = (message or "").strip()
msg_lower = user_msg.lower()
fn_lower = file.filename.lower()
# 1. Текстовые форматы
if fn_lower.endswith((".txt", ".csv", ".log", ".md")):
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
file_context = f.read(6000)
except Exception as e:
logger.warning(f"Не удалось прочитать текст: {e}")
# ⭐️ 1. ПРЯМАЯ ИЗОЛИРОВАННАЯ ОБРАБОТКА PDF/СКАНОВ В WORD ЧЕРЕЗ OFFICE МОДУЛЬ
ocr_keywords = [
"распознай", "распознать", "в word", "в ворд", "для ворда", "для word",
"отформатируй", "текст для вставки", "извлеки текст", "сделай документ", "переведи в ворд"
]
if fn_lower.endswith(".pdf") and (any(k in msg_lower for k in ocr_keywords) or not user_msg):
logger.info(f"[Office] Прямой запуск распознавания PDF в Word: {file.filename}")
from services.office.service import convert_pdf_to_word_service
from modules.web_api.llm.db_tools import db_save_chat_message, db_get_chat_history
# 2. Изображения (прямой OCR)
elif fn_lower.endswith((".png", ".jpg", ".jpeg", ".webp")):
try:
with open(file_path, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode("utf-8")
except Exception as e:
logger.warning(f"Ошибка кодирования картинки в base64: {e}")
# Сохраняем вопрос пользователя в историю
prompt_text = user_msg or f"Распознать документ {file.filename} для MS Word"
db_save_chat_message(session_id, "user", prompt_text, is_ephemeral=0)
# 3. PDF документы (текстовый слой + рендеринг скана при необходимости)
elif fn_lower.endswith(".pdf"):
# Попытка извлечь встроенный текстовый слой
try:
import pypdf
reader = pypdf.PdfReader(file_path)
extracted = []
for page in reader.pages:
t = page.extract_text()
if t:
extracted.append(t)
file_context = "\n".join(extracted).strip()
except Exception:
pass
# Вызываем офисный сервис постраничного OCR и сборки DOCX
office_res = convert_pdf_to_word_service(file_path, file.filename)
# Если текстового слоя мало (скан или фото документа), рендерим страницу в картинку для Vision OCR
if len(file_context) < 40:
try:
import fitz # PyMuPDF
doc = fitz.open(file_path)
if len(doc) > 0:
page = doc[0]
pix = page.get_pixmap(dpi=150)
img_bytes = pix.tobytes("png")
image_b64 = base64.b64encode(img_bytes).decode("utf-8")
file_context = ""
except Exception as e:
logger.warning(f"PyMuPDF не установлен или сбой рендеринга PDF: {e}")
reply_text = (
f"📄 **{office_res['message']}**\n\n"
f"**Фрагмент первой страницы документа:**\n"
f"```text\n{office_res['preview_text']}...\n```\n\n"
f"Файл готов к скачиванию и редактированию в MS Word."
)
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=0)
user_msg = message.strip() or f"Распознай и проанализируй прикрепленный документ {file.filename}"
action_payload = {
"type": "FILE_DOWNLOAD_CARD",
"filename": office_res["filename"],
"download_url": office_res["download_url"],
"tasks_count": f"{office_res['total_pages']} стр."
}
return {
"status": "success",
"user_id": user_id,
"session_id": session_id,
"response": reply_text,
"action_payload": action_payload
}
# 2. Если это не задача OCR в Word — отправляем файл в обычный диалог агента
parsed = parse_uploaded_file(file_path, file.filename)
file_context = parsed.get("context_text", "")
image_b64 = parsed.get("image_b64")
prompt_for_agent = user_msg or f"Проанализируй документ {file.filename}"
reply_text, history, action_payload = process_chat_message(
user_id=user_id,
user_message=user_msg,
user_message=prompt_for_agent,
file_context=file_context,
image_b64=image_b64,
session_id=session_id