2612-есть ок OCR, нужно допиливать бота под новый flow для операторов

This commit is contained in:
2026-01-27 00:17:10 +03:00
parent 7d2ffb54b5
commit 1843cb9c20
22 changed files with 1011 additions and 577 deletions

View File

@@ -0,0 +1,114 @@
from abc import ABC, abstractmethod
from typing import Optional
import os
import json
import base64
import logging
import requests
from app.services.auth import yandex_auth
logger = logging.getLogger(__name__)
VISION_URL = "https://ocr.api.cloud.yandex.net/ocr/v1/recognizeText"
class OCREngine(ABC):
@abstractmethod
def recognize(self, image_bytes: bytes) -> str:
"""Распознает текст из изображения и возвращает строку."""
pass
@abstractmethod
def is_configured(self) -> bool:
"""Проверяет, настроен ли движок (наличие ключей/настроек)."""
pass
class YandexOCREngine(OCREngine):
def __init__(self):
self.folder_id = os.getenv("YANDEX_FOLDER_ID")
if not yandex_auth.is_configured() or not self.folder_id:
logger.warning("Yandex OCR credentials (YANDEX_OAUTH_TOKEN, YANDEX_FOLDER_ID) not set. Yandex OCR will be unavailable.")
def is_configured(self) -> bool:
return yandex_auth.is_configured() and bool(self.folder_id)
def recognize(self, image_bytes: bytes) -> str:
"""
Отправляет изображение в Yandex Vision и возвращает полный текст.
"""
if not self.is_configured():
logger.error("Yandex credentials missing.")
return ""
iam_token = yandex_auth.get_iam_token()
if not iam_token:
return ""
# 1. Кодируем в Base64
b64_image = base64.b64encode(image_bytes).decode("utf-8")
# 2. Формируем тело запроса
# Используем модель 'page' (для документов) и '*' для автоопределения языка
payload = {
"mimeType": "JPEG", # Yandex переваривает и PNG под видом JPEG часто, но лучше быть аккуратным.
# В идеале определять mime-type из файла, но JPEG - безопасный дефолт для фото.
"languageCodes": ["*"],
"model": "page",
"content": b64_image
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {iam_token}",
"x-folder-id": self.folder_id,
"x-data-logging-enabled": "true"
}
# 3. Отправляем запрос
try:
logger.info("Sending request to Yandex Vision OCR...")
response = requests.post(VISION_URL, headers=headers, json=payload, timeout=20)
# Если 401 Unauthorized, возможно токен протух раньше времени (редко, но бывает)
if response.status_code == 401:
logger.warning("Got 401 from Yandex. Retrying with fresh token...")
yandex_auth.reset_token() # сброс кэша
iam_token = yandex_auth.get_iam_token()
if iam_token:
headers["Authorization"] = f"Bearer {iam_token}"
response = requests.post(VISION_URL, headers=headers, json=payload, timeout=20)
response.raise_for_status()
result_json = response.json()
# 4. Парсим ответ
# Структура: result -> textAnnotation -> fullText
# Или (если fullText нет) blocks -> lines -> text
text_annotation = result_json.get("result", {}).get("textAnnotation", {})
if not text_annotation:
logger.warning("Yandex returned success but no textAnnotation found.")
return ""
# Самый простой способ - взять fullText, он обычно склеен с \n
full_text = text_annotation.get("fullText", "")
if not full_text:
# Фолбэк: если fullText пуст, собираем вручную по блокам
logger.info("fullText empty, assembling from blocks...")
lines_text = []
for block in text_annotation.get("blocks", []):
for line in block.get("lines", []):
lines_text.append(line.get("text", ""))
full_text = "\n".join(lines_text)
return full_text
except Exception as e:
logger.error(f"Error during Yandex Vision request: {e}")
return ""
# Глобальный инстанс
yandex_engine = YandexOCREngine()