- Загрузка/сканирование видео, извлечение кадров, анализ через SDK ФПГ - Понятный диагноз с сохранением в SQLite и share-ссылками - Экспорт PDF/DOCX, скачивание записанного видео - Камерное сканирование на мобильных (canvas + MediaRecorder) - Секреты вынесены в переменные окружения
450 lines
18 KiB
Python
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())
|