Files
sber_med/fpg_connect.py
apuc 04a7b31a75 Веб-приложение диагностики по видео (ФПГ GigaDoc)
- Загрузка/сканирование видео, извлечение кадров, анализ через SDK ФПГ
- Понятный диагноз с сохранением в SQLite и share-ссылками
- Экспорт PDF/DOCX, скачивание записанного видео
- Камерное сканирование на мобильных (canvas + MediaRecorder)
- Секреты вынесены в переменные окружения
2026-09-07 19:01:06 +03:00

450 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Скрипт для подключения к SDK ФПГ (GigaDoc) и тестирования всех этапов.
Этапы:
1. Авторизация (получение access token)
2. Создание сессии
3. Подключение к WebSocket
4. Отправка изображений
5. Получение результатов
6. Удаление сессии
Логирование ведётся в файл fpg_test.log
"""
import argparse
import asyncio
import glob
import json
import logging
import os
import sys
import time
from datetime import datetime
from pathlib import Path
import requests
# ─── Конфигурация ─────────────────────────────────────────────
OAUTH_URL = "https://oauth.sbermed.ai/token"
CLIENT_ID = "client_fpg_front"
CLIENT_SECRET = os.environ.get("FPG_CLIENT_SECRET", "")
REST_HOST = "gigadoc-sdk.sbermed.ai"
REST_BASE = f"https://{REST_HOST}"
# WebSocket (рабочий хост подтверждён: gigadoc-sdk.sbermed.ai:8443)
WS_HOST = "gigadoc-sdk.sbermed.ai"
WS_PORT = 8443
WS_PATH_TEMPLATE = "/api/v2/ws/predict/{uid}/image"
DEVICE_ID = "web_prod"
FORM_FACTOR = "web_prod"
LOG_FILE = "fpg_test.log"
# ─── Логирование ──────────────────────────────────────────────
def setup_logging(log_file: str = LOG_FILE):
"""Настроить логирование в файл и консоль."""
logger = logging.getLogger("fpg")
logger.setLevel(logging.DEBUG)
# Формат
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Файловый хендлер
fh = logging.FileHandler(log_file, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
logger.addHandler(fh)
# Консольный хендлер
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)
# Включаем логирование библиотек (requests, websockets, urllib3)
logging.getLogger("urllib3").setLevel(logging.DEBUG)
logging.getLogger("urllib3").addHandler(fh)
logging.getLogger("websockets").setLevel(logging.DEBUG)
logging.getLogger("websockets").addHandler(fh)
logging.getLogger("asyncio").setLevel(logging.DEBUG)
logging.getLogger("asyncio").addHandler(fh)
return logger
logger = logging.getLogger("fpg")
def log_headers(headers: dict, prefix: str = " "):
"""Залогировать HTTP-заголовки (маскируя секреты)."""
for k, v in headers.items():
if k.lower() in ("authorization", "client_secret", "cookie", "set-cookie"):
# Маскируем секретные заголовки
if isinstance(v, str) and len(v) > 20:
v = v[:20] + "..." + v[-5:]
else:
v = "***"
logger.debug(f"{prefix}{k}: {v}")
def log_response(resp, prefix: str = " "):
"""Залогировать HTTP-ответ подробно."""
logger.debug(f"{prefix}HTTP {resp.status_code} {resp.reason}")
logger.debug(f"{prefix}Headers:")
log_headers(dict(resp.headers), prefix + " ")
body = resp.text
if len(body) > 2000:
logger.debug(f"{prefix}Body ({len(body)} bytes): {body[:2000]}...")
else:
logger.debug(f"{prefix}Body: {body}")
# ─── Этап 1: Авторизация ──────────────────────────────────────
def get_token() -> str:
"""Получить access token через client_credentials."""
logger.info("=" * 60)
logger.info("ЭТАП 1: Авторизация")
logger.info("=" * 60)
logger.info(f"POST {OAUTH_URL}")
logger.debug(" Запрос:")
logger.debug(f" grant_type=client_credentials")
logger.debug(f" client_id={CLIENT_ID}")
logger.debug(f" client_secret={'*' * len(CLIENT_SECRET)}")
try:
resp = requests.post(
OAUTH_URL,
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
timeout=15,
)
logger.info(f"HTTP {resp.status_code}")
logger.debug(" Ответ:")
log_response(resp, " ")
resp.raise_for_status()
data = resp.json()
token = data["access_token"]
expires_in = data.get("expires_in", "?")
logger.info(f"✅ Токен получен (действует {expires_in} сек)")
logger.debug(f" token_type: {data.get('token_type')}")
logger.debug(f" scope: {data.get('scope')}")
logger.debug(f" refresh_expires_in: {data.get('refresh_expires_in')}")
logger.debug(f" not-before-policy: {data.get('not-before-policy')}")
logger.debug(f" access_token: {token[:80]}...")
logger.debug(f" access_token (полный, {len(token)} chars): {token}")
return token
except requests.exceptions.RequestException as e:
logger.error(f"❌ Ошибка авторизации: {e}")
if hasattr(e, "response") and e.response is not None:
logger.error(f" Статус: {e.response.status_code}")
logger.error(f" Ответ: {e.response.text}")
raise
# ─── Этап 2: Создание сессии ──────────────────────────────────
def create_session(token: str) -> str:
"""Создать сессию и вернуть uid."""
logger.info("=" * 60)
logger.info("ЭТАП 2: Создание сессии")
logger.info("=" * 60)
url = f"{REST_BASE}/api/v1/predict/new"
logger.info(f"POST {url}")
logger.debug(" Запрос:")
logger.debug(f" device_id={DEVICE_ID}")
logger.debug(f" form_factor={FORM_FACTOR}")
logger.debug(f" Authorization: Bearer {token[:30]}...")
try:
resp = requests.post(
url,
params={"device_id": DEVICE_ID, "form_factor": FORM_FACTOR},
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
timeout=15,
)
logger.info(f"HTTP {resp.status_code}")
logger.debug(" Ответ:")
log_response(resp, " ")
resp.raise_for_status()
uid = resp.json()["uid"]
logger.info(f"✅ Сессия создана: {uid}")
return uid
except requests.exceptions.RequestException as e:
logger.error(f"❌ Ошибка создания сессии: {e}")
if hasattr(e, "response") and e.response is not None:
logger.error(f" Статус: {e.response.status_code}")
logger.error(f" Ответ: {e.response.text}")
raise
# ─── Этап 3: WebSocket ────────────────────────────────────────
async def ws_scan(token: str, uid: str, ws_host: str, ws_port: int, image_dir: str):
"""Подключиться по WebSocket, отправить изображения, получить результат."""
import socket
import ssl
import websockets
logger.info("=" * 60)
logger.info("ЭТАП 3: WebSocket подключение")
logger.info("=" * 60)
# ── DNS резолв ──
logger.info(f"DNS резолв: {ws_host}")
try:
ips = socket.getaddrinfo(ws_host, ws_port)
for addr in ips:
logger.debug(f" {addr[0].name} -> {addr[4]}")
logger.info(f"✅ DNS резолв успешен: {len(ips)} записей")
except socket.gaierror as e:
logger.warning(f"⚠️ DNS резолв не удался: {e}")
logger.warning(f" Хост {ws_host} не найден. Пробуем другие варианты...")
# ── Собираем изображения ──
patterns = ["*.jpg", "*.jpeg", "*.png"]
images = []
for p in patterns:
images.extend(glob.glob(os.path.join(image_dir, p)))
images.sort()
if not images:
logger.warning(f"Нет изображений в {image_dir}, использую тестовое")
images = ["/tmp/test_face.jpg"]
if not os.path.exists(images[0]):
logger.error("Тестовое изображение не найдено")
return None
logger.info(f"Найдено изображений: {len(images)}")
for img in images:
size = os.path.getsize(img)
logger.debug(f" {img} ({size} bytes)")
# ── Пробуем несколько вариантов URI ──
uris = [
f"wss://{ws_host}:{ws_port}{WS_PATH_TEMPLATE.format(uid=uid)}",
f"wss://{REST_HOST}:{ws_port}{WS_PATH_TEMPLATE.format(uid=uid)}",
f"wss://{REST_HOST}:443{WS_PATH_TEMPLATE.format(uid=uid)}",
]
last_error = None
for uri in uris:
logger.info(f"Попытка подключения: {uri}")
logger.info(" ── Заголовки WebSocket-запроса ──")
logger.info(f" GET {WS_PATH_TEMPLATE.format(uid=uid)} HTTP/1.1")
logger.info(f" Host: {ws_host}")
logger.info(f" Upgrade: websocket")
logger.info(f" Connection: Upgrade")
logger.info(f" Sec-WebSocket-Version: 13")
logger.info(f" Sec-WebSocket-Protocol: bearer.<TOKEN> (авторизация через subprotocol)")
logger.info(f" Origin: https://gigadoc.sbermed.ai")
logger.info(f" (полный токен: {token})")
logger.info(" ────────────────────────────────")
try:
async with websockets.connect(
uri,
open_timeout=10,
subprotocols=[f"bearer.{token}"],
origin="https://gigadoc.sbermed.ai",
extra_headers={
"Cache-Control": "no-cache",
"Pragma": "no-cache",
},
compression="deflate",
ping_interval=20,
ping_timeout=20,
max_size=None,
) as ws:
logger.info(f"✅ WebSocket подключен: {uri}")
logger.debug(f" Протокол: {ws.subprotocol}")
logger.debug(f" Расширения: {ws.extensions if hasattr(ws, 'extensions') else 'n/a'}")
# Отправляем изображения
logger.info("=" * 60)
logger.info("ЭТАП 4: Отправка изображений")
logger.info("=" * 60)
result = None
for i, img_path in enumerate(images):
with open(img_path, "rb") as f:
img_data = f.read()
logger.info(f" [{i+1}/{len(images)}] WS → отправка изображения: {img_path} ({len(img_data)} bytes, binary)")
t_start = time.time()
await ws.send(img_data)
t_send = time.time() - t_start
logger.debug(f" Отправлено за {t_send*1000:.1f} мс")
# Читаем ответ
try:
resp = await asyncio.wait_for(ws.recv(), timeout=5)
t_recv = time.time() - t_start
logger.debug(f" Ответ получен за {t_recv*1000:.1f} мс")
try:
data = json.loads(resp)
progress = data.get("progress", "?")
face_ok = data.get("face_ok", "?")
# Логируем прогресс
if i % 10 == 0 or (isinstance(progress, (int, float)) and progress >= 1.0):
logger.info(f" [{i+1}/{len(images)}] progress={progress} face_ok={face_ok}")
# Логируем полный ответ на DEBUG
logger.debug(f" Полный ответ: {json.dumps(data, ensure_ascii=False)}")
# Финальный результат
if isinstance(progress, (int, float)) and progress >= 1.0:
logger.info("=" * 60)
logger.info("ЭТАП 5: ФИНАЛЬНЫЙ РЕЗУЛЬТАТ")
logger.info("=" * 60)
logger.info(json.dumps(data, indent=2, ensure_ascii=False))
result = data
break
except json.JSONDecodeError:
logger.debug(f" Ответ не JSON: {resp[:200]}")
except asyncio.TimeoutError:
logger.debug(f" [{i+1}/{len(images)}] Таймаут ожидания ответа (5 сек)")
if result is None:
logger.warning("Финальный результат не получен (прогресс не достиг 1.0)")
return result
except Exception as e:
last_error = e
logger.warning(f"❌ Не удалось подключиться к {uri}: {type(e).__name__}: {e}")
logger.debug(f" Детали ошибки: {e}")
logger.error(f"❌ Все варианты WebSocket подключения не удались")
logger.error(f" Последняя ошибка: {type(last_error).__name__}: {last_error}")
return None
# ─── Этап 6: Удаление сессии ──────────────────────────────────
def delete_session(token: str, uid: str):
"""Удалить сессию."""
logger.info("=" * 60)
logger.info("ЭТАП 6: Удаление сессии")
logger.info("=" * 60)
url = f"{REST_BASE}/api/v1/predict/{uid}"
logger.info(f"DELETE {url}")
logger.debug(f" Authorization: Bearer {token[:30]}...")
try:
resp = requests.delete(
url,
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
logger.info(f"HTTP {resp.status_code}")
logger.debug(" Ответ:")
log_response(resp, " ")
if resp.status_code == 200:
logger.info(f"✅ Сессия {uid} удалена")
else:
logger.warning(f"⚠️ Сессия {uid} не удалена: {resp.status_code} {resp.text}")
except requests.exceptions.RequestException as e:
logger.error(f"❌ Ошибка удаления сессии: {e}")
if hasattr(e, "response") and e.response is not None:
logger.error(f" Статус: {e.response.status_code}")
logger.error(f" Ответ: {e.response.text}")
# ─── Создание тестового изображения ──────────────────────────
def create_test_image():
"""Создать тестовое JPEG-изображение если нет своих."""
try:
from PIL import Image
img = Image.new("RGB", (1280, 720), color=(128, 128, 128))
img.save("/tmp/test_face.jpg", "JPEG", quality=80)
logger.info("Создано тестовое изображение /tmp/test_face.jpg")
except ImportError:
logger.warning("PIL не установлен, тестовое изображение не создано")
# ─── Main ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Тест SDK ФПГ (GigaDoc)")
parser.add_argument("--ws-host", default=WS_HOST, help=f"WebSocket host (default: {WS_HOST})")
parser.add_argument("--ws-port", type=int, default=WS_PORT, help=f"WebSocket port (default: {WS_PORT})")
parser.add_argument("--image-dir", default=".", help="Директория с JPEG-файлами (default: .)")
parser.add_argument("--log-file", default=LOG_FILE, help=f"Файл лога (default: {LOG_FILE})")
parser.add_argument("--rest-only", action="store_true", help="Только REST API (без WebSocket)")
args = parser.parse_args()
# Настройка логирования
setup_logging(args.log_file)
logger.info("=" * 60)
logger.info("🏥 Тестирование SDK ФПГ (GigaDoc)")
logger.info(f"Дата: {datetime.now().isoformat()}")
logger.info(f"REST: {REST_BASE}")
logger.info(f"WS: {args.ws_host}:{args.ws_port}")
logger.info("=" * 60)
# 0. Тестовое изображение
create_test_image()
# 1. Авторизация
try:
token = get_token()
except Exception:
logger.error("Тест прерван: не удалось авторизоваться")
return 1
# 2. Создание сессии
try:
uid = create_session(token)
except Exception:
logger.error("Тест прерван: не удалось создать сессию")
return 1
# 3-5. WebSocket
result = None
if not args.rest_only:
try:
result = asyncio.run(
ws_scan(token, uid, args.ws_host, args.ws_port, args.image_dir)
)
except Exception as e:
logger.error(f"Ошибка WebSocket: {type(e).__name__}: {e}")
else:
logger.info("WebSocket тест пропущен (--rest-only)")
# 6. Удаление сессии
delete_session(token, uid)
# Итог
logger.info("=" * 60)
if result:
logger.info("✅ Тест завершён успешно — результат получен")
else:
logger.info("⚠️ REST API работает, WebSocket недоступен")
logger.info(f"Лог сохранён в: {args.log_file}")
logger.info("=" * 60)
return 0 if result else 2
if __name__ == "__main__":
sys.exit(main())