mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
137 lines
5.8 KiB
Python
137 lines
5.8 KiB
Python
import os
|
||
import time
|
||
import json
|
||
import base64
|
||
import logging
|
||
import requests
|
||
from typing import Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
IAM_TOKEN_URL = "https://iam.api.cloud.yandex.net/iam/v1/tokens"
|
||
VISION_URL = "https://ocr.api.cloud.yandex.net/ocr/v1/recognizeText"
|
||
|
||
class YandexOCREngine:
|
||
def __init__(self):
|
||
self.oauth_token = os.getenv("YANDEX_OAUTH_TOKEN")
|
||
self.folder_id = os.getenv("YANDEX_FOLDER_ID")
|
||
|
||
# Кэширование IAM токена
|
||
self._iam_token = None
|
||
self._token_expire_time = 0
|
||
|
||
if not self.oauth_token or not self.folder_id:
|
||
logger.warning("Yandex OCR credentials (YANDEX_OAUTH_TOKEN, YANDEX_FOLDER_ID) not set. Yandex OCR will be unavailable.")
|
||
|
||
def _get_iam_token(self) -> Optional[str]:
|
||
"""
|
||
Получает IAM-токен. Если есть живой кэшированный — возвращает его.
|
||
Если нет — обменивает OAuth на IAM.
|
||
"""
|
||
current_time = time.time()
|
||
|
||
# Если токен есть и он "свежий" (с запасом в 5 минут)
|
||
if self._iam_token and current_time < self._token_expire_time - 300:
|
||
return self._iam_token
|
||
|
||
logger.info("Obtaining new IAM token from Yandex...")
|
||
try:
|
||
response = requests.post(
|
||
IAM_TOKEN_URL,
|
||
json={"yandexPassportOauthToken": self.oauth_token},
|
||
timeout=10
|
||
)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
self._iam_token = data["iamToken"]
|
||
|
||
# Токен обычно живет 12 часов, но мы будем ориентироваться на поле expiresAt если нужно,
|
||
# или просто поставим таймер. Для простоты берем 1 час жизни кэша.
|
||
self._token_expire_time = current_time + 3600
|
||
|
||
logger.info("IAM token received successfully.")
|
||
return self._iam_token
|
||
except Exception as e:
|
||
logger.error(f"Failed to get IAM token: {e}")
|
||
return None
|
||
|
||
def recognize(self, image_bytes: bytes) -> str:
|
||
"""
|
||
Отправляет изображение в Yandex Vision и возвращает полный текст.
|
||
"""
|
||
if not self.oauth_token or not self.folder_id:
|
||
logger.error("Yandex credentials missing.")
|
||
return ""
|
||
|
||
iam_token = self._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...")
|
||
self._iam_token = None # сброс кэша
|
||
iam_token = self._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() |