From da62ea5b989903386cd8d3deead0bc9a6b1aac52 Mon Sep 17 00:00:00 2001 From: SERTY Date: Sat, 29 Nov 2025 13:26:41 +0300 Subject: [PATCH] qr-manager fixed for both qr-codes --- docker-compose.yml | 6 +-- ocr-service/qr_manager.py | 50 +++++++++++++++++---- ocr-service/system-prompt.md | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 ocr-service/system-prompt.md diff --git a/docker-compose.yml b/docker-compose.yml index ffc1b18..89542f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: restart: always environment: POSTGRES_USER: rmser - POSTGRES_PASSWORD: rmser_password + POSTGRES_PASSWORD: mhrcadmin994525 POSTGRES_DB: rmser_db ports: - "5455:5432" @@ -47,11 +47,11 @@ services: # Формат: СЕКЦИЯ_КЛЮЧ (Viper AutomaticEnv с заменой точки на _) environment: # Настройки БД (внутри докера хост 'db') - - DB_DSN=host=db user=rmser password=rmser_password dbname=rmser_db port=5455 sslmode=disable TimeZone=Europe/Moscow + - DB_DSN=host=db user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow # Настройки Redis (внутри докера хост 'redis') - REDIS_ADDR=redis:6379 # Настройки OCR (внутри докера хост 'ocr') - - OCR_SERVICE_URL=http://ocr:5005 + - OCR_SERVICE_URL=http://ocr:5000 # Остальные настройки (RMS, Telegram) берутся из config.yaml volumes: diff --git a/ocr-service/qr_manager.py b/ocr-service/qr_manager.py index ebbf2c2..323765b 100644 --- a/ocr-service/qr_manager.py +++ b/ocr-service/qr_manager.py @@ -5,33 +5,67 @@ from pyzbar.pyzbar import decode from PIL import Image import numpy as np -# Импортируем модель из parser.py, чтобы типы совпадали! +# Импортируем модель из parser.py from parser import ParsedItem -# В продакшене лучше вынести в конфиг API_TOKEN = "36590.yqtiephCvvkYUKM2W" API_URL = "https://proverkacheka.com/api/v1/check/get" logger = logging.getLogger(__name__) +def is_valid_fiscal_qr(qr_string: str) -> bool: + """ + Проверяет, соответствует ли строка формату фискального чека ФНС. + Ожидаемый формат: t=...&s=...&fn=...&i=...&fp=...&n=... + Мы проверяем наличие хотя бы 3-х ключевых параметров. + """ + if not qr_string: + return False + + # Ключевые параметры, которые обязаны быть в строке чека + required_keys = ["t=", "s=", "fn="] + + # Проверяем, что все ключевые параметры присутствуют + # (порядок может отличаться, поэтому проверяем вхождение каждого) + matches = [key in qr_string for key in required_keys] + + return all(matches) + def detect_and_decode_qr(image: np.ndarray) -> Optional[str]: """ - Пытается найти QR-код на изображении и вернуть его сырое содержимое. + Ищет ВСЕ QR-коды на изображении и возвращает только тот, + который похож на фискальный чек. """ try: - # Pyzbar лучше работает с PIL Image pil_img = Image.fromarray(image) - # Декодируем + # Декодируем все коды на картинке decoded_objects = decode(pil_img) + if not decoded_objects: + logger.info("No QR codes detected on the image.") + return None + + logger.info(f"Detected {len(decoded_objects)} code(s). Scanning for fiscal data...") + for obj in decoded_objects: if obj.type == 'QRCODE': qr_data = obj.data.decode("utf-8") - logger.info(f"QR Code detected: {qr_data}") - return qr_data + # Логируем найденное (для отладки, если вдруг формат хитрый) + # Обрезаем длинные строки, чтобы не засорять лог + log_preview = (qr_data[:75] + '..') if len(qr_data) > 75 else qr_data + logger.info(f"Checking QR content: {log_preview}") + + if is_valid_fiscal_qr(qr_data): + logger.info("Valid fiscal QR found!") + return qr_data + else: + logger.info("QR skipped (not a fiscal receipt pattern).") + + logger.warning("QR codes were found, but none matched the fiscal receipt format.") return None + except Exception as e: logger.error(f"Error during QR detection: {e}") return None @@ -55,7 +89,6 @@ def fetch_data_from_api(qr_raw: str) -> List[ParsedItem]: data = response.json() - # Проверяем успешность ответа (code: 1 - успех) if data.get('code') != 1: logger.warning(f"API returned non-success code: {data.get('code')}") return [] @@ -66,7 +99,6 @@ def fetch_data_from_api(qr_raw: str) -> List[ParsedItem]: parsed_items = [] for item in items_data: - # API возвращает цены в копейках (int), нужно делить на 100 price = float(item.get('price', 0)) / 100.0 total_sum = float(item.get('sum', 0)) / 100.0 quantity = float(item.get('quantity', 0)) diff --git a/ocr-service/system-prompt.md b/ocr-service/system-prompt.md new file mode 100644 index 0000000..ba9f664 --- /dev/null +++ b/ocr-service/system-prompt.md @@ -0,0 +1,87 @@ +Вот подробный системный промпт (System Definition), который описывает архитектуру, логику и контракт работы твоего OCR-сервиса. + +Сохрани этот текст как **`SYSTEM_PROMPT.md`** или в документацию проекта (Confluence/Wiki). К нему стоит обращаться при разработке API-клиентов, тестировании или доработке логики. + +--- + +# System Definition: RMSER OCR Service + +## 1. Роль и Назначение +**RMSER OCR Service** — это специализированный микросервис на базе FastAPI, предназначенный для извлечения структурированных данных (товарных позиций) из изображений кассовых чеков РФ. + +Сервис реализует **Гибридную Стратегию Распознавания**, отдавая приоритет получению верифицированных данных через ФНС, и используя оптическое распознавание (OCR) только как запасной вариант (fallback). + +## 2. Логика Обработки (Workflow) + +При получении `POST /recognize` с изображением, сервис выполняет действия в строгой последовательности: + +### Этап А: Поиск QR-кода (Priority 1) +1. **Детекция:** Сервис сканирует изображение на наличие QR-кода (библиотека `pyzbar`). +2. **Декодирование:** Извлекает сырую строку чека (формат: `t=YYYYMMDD...&s=SUM...&fn=...`). +3. **Запрос к API:** Отправляет сырые данные в API `proverkacheka.com` (или аналог). +4. **Результат:** + * Если API возвращает успех: Возвращает идеальный список товаров. + * **Метаданные ответа:** `source: "qr_api"`. + +### Этап Б: Оптическое Распознавание (Fallback Strategy) +*Запускается только если QR-код не найден или API вернул ошибку.* + +1. **Препроцессинг (OpenCV):** + * Поиск контуров документа. + * Выравнивание перспективы (Perspective Warp). + * Бинаризация (Adaptive Threshold) для подготовки к Tesseract. +2. **OCR (Tesseract):** Извлечение сырого текста (rus+eng). +3. **Парсинг (Regex):** + * Поиск строк, содержащих паттерны цен (например, `120.00 * 2 = 240.00`). + * Привязка текстового описания (названия товара) к найденным ценам. +4. **Результат:** Возвращает список товаров, найденных эвристическим путем. + * **Метаданные ответа:** `source: "ocr"`. + +## 3. Контракт API (Interface) + +### Входные данные +* **Endpoint:** `POST /recognize` +* **Format:** `multipart/form-data` +* **Field:** `image` (binary file: jpg, png, heic, etc.) + +### Выходные данные (JSON) +Сервис всегда возвращает объект `RecognitionResult`: + +```json +{ + "source": "qr_api", // или "ocr" + "items": [ + { + "raw_name": "Молоко Домик в Деревне 3.2%", // Название товара + "amount": 2.0, // Количество + "price": 89.99, // Цена за единицу + "sum": 179.98 // Общая сумма позиции + }, + { + "raw_name": "Пакет-майка", + "amount": 1.0, + "price": 5.00, + "sum": 5.00 + } + ], + "raw_text": "..." // Сырой текст (для отладки) или содержимое QR +} +``` + +## 4. Технический Стек и Зависимости +* **Runtime:** Python 3.10+ +* **Web Framework:** FastAPI + Uvicorn +* **Computer Vision:** OpenCV (`cv2`) — обработка изображений. +* **OCR Engine:** Tesseract OCR 5 (`pytesseract`) — движок распознавания текста. +* **QR Decoding:** `pyzbar` + `libzbar0`. +* **External API:** `proverkacheka.com` (требует валидный токен). + +## 5. Ограничения и Известные Проблемы +1. **Качество OCR:** В режиме `ocr` точность зависит от качества фото (освещение, помятость). Возможны ошибки в символах `3/8`, `1/7`, `З/3`. +2. **Зависимость от API:** Для работы режима `qr_api` необходим доступ в интернет и оплаченный токен провайдера. +3. **Скорость:** Режим `qr_api` работает быстрее (0.5-1.5 сек). Режим `ocr` может занимать 2-4 сек в зависимости от разрешения фото. + +## 6. Инструкции для Интеграции +При встраивании сервиса в общую систему (например, Telegram-бот или Backend приложения): +1. Всегда проверяйте поле `source`. Если `source == "ocr"`, помечайте данные для пользователя как "Требующие проверки" (Draft). Если `source == "qr_api"`, данные можно считать верифицированными. +2. Если массив `items` пустой, значит сервис не смог распознать чек (ни QR, ни текст не прочитался). Предложите пользователю переснять фото. \ No newline at end of file