This commit is contained in:
Daniil
2026-04-07 13:42:45 +03:00
parent 7d2f444e1c
commit 259d3da89f
34 changed files with 2130 additions and 788 deletions
+40 -5
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import logging
import ssl
import threading
import time
import uuid
@@ -79,6 +80,13 @@ ERROR_SALUTE_UPLOAD_FAILED = "Ошибка загрузки файла в Salute
ERROR_SALUTE_TASK_FAILED = "Ошибка распознавания SaluteSpeech: {detail}"
ERROR_SALUTE_TIMEOUT = "Превышено время ожидания распознавания SaluteSpeech"
ERROR_SALUTE_UNSUPPORTED_FORMAT = "Неподдерживаемый формат аудио для SaluteSpeech: {ext}"
ERROR_SALUTE_AUTH_KEY_MISSING = "Не задан SALUTE_AUTH_KEY для авторизации SaluteSpeech"
ERROR_SALUTE_SSL_FAILED = (
"SSL ошибка при обращении к SaluteSpeech: {detail}. "
"Если используется корпоративный или локальный сертификат, "
"укажите путь в SALUTE_CA_CERT_PATH. "
"Для локальной отладки можно отключить проверку через SALUTE_SSL_VERIFY=false."
)
_salute_token_lock = threading.Lock()
_salute_token: str | None = None
@@ -487,6 +495,27 @@ def _parse_salute_time(s: str) -> float:
return float(s.rstrip("s"))
def _build_salute_ssl_context() -> ssl.SSLContext:
"""Build SSL context for SaluteSpeech using system trust plus optional custom CA."""
settings = get_settings()
if not settings.salute_ssl_verify:
return ssl._create_unverified_context()
ssl_context = ssl.create_default_context()
if settings.salute_ca_cert_path is not None:
ssl_context.load_verify_locations(cafile=str(settings.salute_ca_cert_path))
return ssl_context
def _get_salute_auth_header_value() -> str:
"""Build Basic auth header for SaluteSpeech from settings."""
settings = get_settings()
auth_key = settings.salute_auth_key.strip()
if not auth_key:
raise RuntimeError(ERROR_SALUTE_AUTH_KEY_MISSING)
return f"Basic {auth_key}"
def _get_salute_access_token(client: httpx.Client) -> str:
"""Get or refresh SaluteSpeech OAuth token. Thread-safe."""
global _salute_token, _salute_token_expires_at
@@ -500,7 +529,7 @@ def _get_salute_access_token(client: httpx.Client) -> str:
response = client.post(
SALUTE_AUTH_URL,
headers={
"Authorization": f"Basic {settings.salute_auth_key}",
"Authorization": _get_salute_auth_header_value(),
"RqUID": str(uuid.uuid4()),
"Content-Type": "application/x-www-form-urlencoded",
},
@@ -708,8 +737,6 @@ def _salute_transcribe_sync(
on_progress: ProgressCallback | None = None,
) -> Document:
"""Synchronous SaluteSpeech transcription (runs in Dramatiq worker thread)."""
settings = get_settings()
ext = Path(local_file_path).suffix.lower()
audio_encoding = SALUTE_ENCODING_MAP.get(ext)
content_type = SALUTE_CONTENT_TYPE_MAP.get(ext)
@@ -725,8 +752,8 @@ def _salute_transcribe_sync(
salute_language = SALUTE_LANGUAGE_MAP.get(language or "", "ru-RU")
try:
verify = str(settings.salute_ca_cert_path) if settings.salute_ca_cert_path else True
with httpx.Client(verify=verify, timeout=30.0) as client:
ssl_context = _build_salute_ssl_context()
with httpx.Client(verify=ssl_context, timeout=30.0) as client:
token = _get_salute_access_token(client)
with open(local_file_path, "rb") as f:
@@ -748,6 +775,14 @@ def _salute_transcribe_sync(
raw_result = _download_salute_result(client, token, response_file_id)
return _build_document_from_salute_result(raw_result, language=salute_language)
except ssl.SSLError as exc:
raise RuntimeError(ERROR_SALUTE_SSL_FAILED.format(detail=str(exc))) from exc
except httpx.ConnectError as exc:
if isinstance(exc.__cause__, ssl.SSLError):
raise RuntimeError(
ERROR_SALUTE_SSL_FAILED.format(detail=str(exc.__cause__))
) from exc
raise
finally:
if cleanup_fn is not None:
cleanup_fn()