mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
qr-manager fixed for both qr-codes
This commit is contained in:
@@ -7,7 +7,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: rmser
|
POSTGRES_USER: rmser
|
||||||
POSTGRES_PASSWORD: rmser_password
|
POSTGRES_PASSWORD: mhrcadmin994525
|
||||||
POSTGRES_DB: rmser_db
|
POSTGRES_DB: rmser_db
|
||||||
ports:
|
ports:
|
||||||
- "5455:5432"
|
- "5455:5432"
|
||||||
@@ -47,11 +47,11 @@ services:
|
|||||||
# Формат: СЕКЦИЯ_КЛЮЧ (Viper AutomaticEnv с заменой точки на _)
|
# Формат: СЕКЦИЯ_КЛЮЧ (Viper AutomaticEnv с заменой точки на _)
|
||||||
environment:
|
environment:
|
||||||
# Настройки БД (внутри докера хост 'db')
|
# Настройки БД (внутри докера хост '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 (внутри докера хост 'redis')
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
# Настройки OCR (внутри докера хост 'ocr')
|
# Настройки OCR (внутри докера хост 'ocr')
|
||||||
- OCR_SERVICE_URL=http://ocr:5005
|
- OCR_SERVICE_URL=http://ocr:5000
|
||||||
# Остальные настройки (RMS, Telegram) берутся из config.yaml
|
# Остальные настройки (RMS, Telegram) берутся из config.yaml
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -5,33 +5,67 @@ from pyzbar.pyzbar import decode
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# Импортируем модель из parser.py, чтобы типы совпадали!
|
# Импортируем модель из parser.py
|
||||||
from parser import ParsedItem
|
from parser import ParsedItem
|
||||||
|
|
||||||
# В продакшене лучше вынести в конфиг
|
|
||||||
API_TOKEN = "36590.yqtiephCvvkYUKM2W"
|
API_TOKEN = "36590.yqtiephCvvkYUKM2W"
|
||||||
API_URL = "https://proverkacheka.com/api/v1/check/get"
|
API_URL = "https://proverkacheka.com/api/v1/check/get"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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]:
|
def detect_and_decode_qr(image: np.ndarray) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Пытается найти QR-код на изображении и вернуть его сырое содержимое.
|
Ищет ВСЕ QR-коды на изображении и возвращает только тот,
|
||||||
|
который похож на фискальный чек.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Pyzbar лучше работает с PIL Image
|
|
||||||
pil_img = Image.fromarray(image)
|
pil_img = Image.fromarray(image)
|
||||||
|
|
||||||
# Декодируем
|
# Декодируем все коды на картинке
|
||||||
decoded_objects = decode(pil_img)
|
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:
|
for obj in decoded_objects:
|
||||||
if obj.type == 'QRCODE':
|
if obj.type == 'QRCODE':
|
||||||
qr_data = obj.data.decode("utf-8")
|
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
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during QR detection: {e}")
|
logger.error(f"Error during QR detection: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -55,7 +89,6 @@ def fetch_data_from_api(qr_raw: str) -> List[ParsedItem]:
|
|||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# Проверяем успешность ответа (code: 1 - успех)
|
|
||||||
if data.get('code') != 1:
|
if data.get('code') != 1:
|
||||||
logger.warning(f"API returned non-success code: {data.get('code')}")
|
logger.warning(f"API returned non-success code: {data.get('code')}")
|
||||||
return []
|
return []
|
||||||
@@ -66,7 +99,6 @@ def fetch_data_from_api(qr_raw: str) -> List[ParsedItem]:
|
|||||||
parsed_items = []
|
parsed_items = []
|
||||||
|
|
||||||
for item in items_data:
|
for item in items_data:
|
||||||
# API возвращает цены в копейках (int), нужно делить на 100
|
|
||||||
price = float(item.get('price', 0)) / 100.0
|
price = float(item.get('price', 0)) / 100.0
|
||||||
total_sum = float(item.get('sum', 0)) / 100.0
|
total_sum = float(item.get('sum', 0)) / 100.0
|
||||||
quantity = float(item.get('quantity', 0))
|
quantity = float(item.get('quantity', 0))
|
||||||
|
|||||||
87
ocr-service/system-prompt.md
Normal file
87
ocr-service/system-prompt.md
Normal file
@@ -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, ни текст не прочитался). Предложите пользователю переснять фото.
|
||||||
Reference in New Issue
Block a user