diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d71a409..d7092f3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,20 @@ Все важные изменения проекта документируются в этом файле. +## [Unreleased] + +### ✨ Добавлено (Added) +- **Изолированный домен работы с документами (`services/office/`):** + - Создан специализированный пакет для постраничной обработки многостраничных PDF и сканов без ограничений контекста диалога. + - Автоматическая сборка распознанного текста в файл Microsoft Word (`.docx`) со стандартизированным оформлением. + - Интеграция с роутером чата и выдача контрастной карточки скачивания документа. + +### 📝 Запланировано (Planned) +- **Двухфазный OCR-конвейер рукописных виз и резолюций (`services/office/`):** + - Рендеринг сканов в высоком разрешении (`250–300 DPI`) для точного захвата линий чернил. + - Фаза 2 (Refiner на `qwen2.5:14b`): автоматическое устранение артефактов OCR, склейка абзацев и преобразование списков в таблицы Word. + - Распознавание рукописных виз «Согласовано», подписей и дат руководства с выделением в блок `[Резолюция: ...]`. + ## [3.4.0] — 2026-09-25 ### ✨ Добавлено (Added) diff --git a/docs/in_progress/ROADMAP.md b/docs/in_progress/ROADMAP.md index 7fcb3ce..5aeb089 100644 --- a/docs/in_progress/ROADMAP.md +++ b/docs/in_progress/ROADMAP.md @@ -227,4 +227,22 @@ ## 18. Исследовательский трек: Изолированная песочница кода `[ОТЛОЖЕНО]` - [ ] **Docker/gVisor контур:** - [ ] Изолированный контейнер без доступа к внешней сети (`network: none`) с жесткими cgroups-лимитами и монтированием данных в режиме Read-Only. - - [ ] Генерация и выполнение Python/Pandas скриптов для построения сложных графиков и нестандартной статистики на лету. \ No newline at end of file + - [ ] Генерация и выполнение Python/Pandas скриптов для построения сложных графиков и нестандартной статистики на лету. + +--- + +## 19. Доменный модуль канцелярии и документов (`services/office/`) `[В РАБОТЕ]` +- [x] **Изоляция офисного домена от кадрового ядра СКУД:** + - [x] Создание независимого пакета `services/office/` (`document_extractor.py`, `word_builder.py`, `service.py`). + - [x] Снятие ограничений контекста диалога: многостраничный постраничный обход PDF вместо обрезания первой страницы. + - [x] Генерация ГОСТ-документов Word (`.docx`) с выдачей контрастной карточки скачивания в интерфейс чата. +- [ ] **Двухфазный конвейер OCR и глубокая нормализация (Vision + LLM Refiner):** + - [ ] **Фаза 1 (Vision Extraction):** повышение разрешения рендеринга до `250–300 DPI` для чёткой отрисовки тонких линий шариковых ручек и штампов. + - [ ] **Фаза 2 (LLM Post-Processing Refiner):** вычитка текста через `qwen2.5:14b`: + - Устранение диалогового шума и случайных приветствий модели. + - Склейка разорванных строк внутри абзацев. + - Автоматическая сборка списков участников и табличных данных в полноценные таблицы Word (`Table Grid`). +- [ ] **Специализированное распознавание рукописных виз и резолюций канцелярии:** + - [ ] Распознавание наклонного и беглого почерка на полях и в шапке документов (визы «Согласовано», резолюции руководства, подписи, даты). + - [ ] Выделение рукописных пометок в стандартизированные структурные блоки: `[Резолюция: ...]` в начале страницы. + - [ ] **Two-Pass Context Injection:** перекрёстное сопоставление неразборчивых фамилий и инициалов с реестром участников совещания/штатом из тела самого документа. \ No newline at end of file diff --git a/modules/web_api/routers/chat.py b/modules/web_api/routers/chat.py index 4fe386c..7745c52 100644 --- a/modules/web_api/routers/chat.py +++ b/modules/web_api/routers/chat.py @@ -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 diff --git a/modules/web_api/static/index.html b/modules/web_api/static/index.html index 2394c2b..7fbe205 100644 --- a/modules/web_api/static/index.html +++ b/modules/web_api/static/index.html @@ -11,16 +11,32 @@ * { overflow-anchor: none !important; } #chat-messages-container, main { overflow-anchor: none !important; } #chat-messages-container { padding-bottom: clamp(400px, 85vh, 900px) !important; } + + /* ⭐️ Комфортный размер шрифта в стиле Gemini */ #chat-messages-container .message-content, + #chat-messages-container .text-sm, #chat-messages-container .text-xs, - #chat-messages-container .text-sm { - font-size: 14.5px !important; - line-height: 1.6 !important; + #chat-messages-container p, + #chat-messages-container li { + font-size: 16px !important; + line-height: 1.65 !important; + color: #1e293b !important; /* slate-800 */ } + + #chat-messages-container .user-chat-bubble .text-sm, + #chat-messages-container .user-chat-bubble p, + #chat-messages-container .user-chat-bubble div { + font-size: 16px !important; + line-height: 1.6 !important; + color: #ffffff !important; + } + #chat-messages-container pre, #chat-messages-container code { - font-size: 13.5px !important; + font-size: 14.5px !important; + line-height: 1.5 !important; } + .user-chat-bubble { scroll-margin-top: 24px !important; } @@ -234,12 +250,12 @@