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

315 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Отправка потока кадров (одно изображение многократно) в SDK ФПГ (GigaDoc)
для получения полных медицинских показателей.
По документации: ~300 кадров JPEG при ~30 кадрах/сек (~10 сек видео).
"""
import argparse
import asyncio
import json
import logging
import os
import sys
import time
from datetime import datetime
import requests
import websockets
# ─── Конфигурация ─────────────────────────────────────────────
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}"
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_stream.log"
# ─── Логирование ──────────────────────────────────────────────
def setup_logging(log_file: str = LOG_FILE):
logger = logging.getLogger("fpg_stream")
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()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)
return logger
logger = setup_logging()
# ─── Этап 1: Авторизация ──────────────────────────────────────
def get_token():
logger.info("=" * 60)
logger.info("ЭТАП 1: Авторизация")
logger.info("=" * 60)
logger.info(f"POST {OAUTH_URL}")
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}")
if resp.status_code != 200:
raise RuntimeError(f"Ошибка авторизации: {resp.status_code} {resp.text}")
data = resp.json()
token = data["access_token"]
logger.info(f"✅ Токен получен (действует {data.get('expires_in', '?')} сек)")
return token
# ─── Этап 2: Создание сессии ──────────────────────────────────
def create_session(token: str):
logger.info("=" * 60)
logger.info("ЭТАП 2: Создание сессии")
logger.info("=" * 60)
url = f"{REST_BASE}/api/v1/predict/new"
logger.info(f"POST {url}")
logger.info(f" device_id={DEVICE_ID}")
logger.info(f" form_factor={FORM_FACTOR}")
resp = requests.post(
url,
params={"device_id": DEVICE_ID, "form_factor": FORM_FACTOR},
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
logger.info(f"HTTP {resp.status_code}")
if resp.status_code != 200:
raise RuntimeError(f"Ошибка создания сессии: {resp.status_code} {resp.text}")
uid = resp.json()["uid"]
logger.info(f"✅ Сессия создана: {uid}")
return uid
# ─── Этап 3-5: WebSocket + отправка потока + результат ────────
async def ws_stream(token: str, uid: str, image_path: str, frames: int, fps: float):
logger.info("=" * 60)
logger.info("ЭТАП 3: WebSocket подключение")
logger.info("=" * 60)
uri = f"wss://{WS_HOST}:{WS_PORT}{WS_PATH_TEMPLATE.format(uid=uid)}"
logger.info(f"URL: {uri}")
# Читаем изображение (или директорию с кадрами)
import glob
if os.path.isdir(image_path):
# Директория с кадрами
patterns = ["*.jpg", "*.jpeg", "*.png"]
frame_files = []
for p in patterns:
frame_files.extend(glob.glob(os.path.join(image_path, p)))
frame_files.sort()
logger.info(f"Директория с кадрами: {image_path} ({len(frame_files)} файлов)")
if not frame_files:
logger.error("Нет кадров в директории")
return None
# Ограничиваем количество кадров
frame_files = frame_files[:frames]
logger.info(f"Будет отправлено кадров: {len(frame_files)}")
else:
# Один файл — повторяем его frames раз
frame_files = [image_path] * frames
logger.info(f"Изображение: {image_path} ({os.path.getsize(image_path)} bytes), повторяем {frames} раз")
logger.info(f"Частота: {fps} fps, интервал: {1000/fps:.0f} мс")
interval = 1.0 / fps
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.info(f" Subprotocol: {ws.subprotocol}")
logger.info("=" * 60)
logger.info("ЭТАП 4: Отправка потока кадров")
logger.info("=" * 60)
result = None
last_progress = -1
t_start = time.time()
for i, frame_file in enumerate(frame_files):
frame_start = time.time()
# Читаем кадр
with open(frame_file, "rb") as f:
img_data = f.read()
# Отправляем кадр
await ws.send(img_data)
# Читаем ответ (не блокируемся долго)
try:
resp = await asyncio.wait_for(ws.recv(), timeout=2.0)
try:
data = json.loads(resp)
progress = data.get("progress", 0.0)
face_ok = data.get("face_ok", False)
# Логируем прогресс при изменении
pct = int(progress * 100)
if pct != last_progress:
logger.info(f" [{i+1}/{frames}] progress={progress:.2f} ({pct}%) face_ok={face_ok}")
last_progress = pct
# Финальный результат
if 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:
pass # нет ответа на этот кадр — нормально
# Соблюдаем частоту кадров
elapsed = time.time() - frame_start
if elapsed < interval:
await asyncio.sleep(interval - elapsed)
# Прогресс-индикатор каждые 50 кадров
if (i + 1) % 50 == 0:
elapsed_total = time.time() - t_start
logger.info(f" ... отправлено {i+1}/{frames} кадров за {elapsed_total:.1f} сек")
if result is None:
logger.warning(f"Финальный результат не получен после {frames} кадров")
logger.info(f" Отправлено кадров: {frames}, время: {time.time()-t_start:.1f} сек")
return result
except Exception as e:
logger.error(f"❌ Ошибка WebSocket: {type(e).__name__}: {e}")
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}")
resp = requests.delete(
url,
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
logger.info(f"HTTP {resp.status_code}")
if resp.status_code == 200:
logger.info(f"✅ Сессия {uid} удалена")
else:
logger.warning(f"⚠️ Сессия {uid} не удалена: {resp.status_code} {resp.text}")
# ─── Main ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Отправка потока кадров в SDK ФПГ (GigaDoc)")
parser.add_argument("--source", required=True, help="Путь к JPEG-изображению или директории с кадрами")
parser.add_argument("--frames", type=int, default=300, help="Количество кадров (default: 300)")
parser.add_argument("--fps", type=float, default=30.0, help="Частота кадров (default: 30)")
parser.add_argument("--log-file", default=LOG_FILE, help=f"Файл лога (default: {LOG_FILE})")
args = parser.parse_args()
# Настройка логирования
global logger
logger = setup_logging(args.log_file)
logger.info("=" * 60)
logger.info("🏥 Отправка потока кадров в SDK ФПГ (GigaDoc)")
logger.info(f"Дата: {datetime.now().isoformat()}")
logger.info(f"Источник: {args.source}")
logger.info(f"Кадры: {args.frames} @ {args.fps} fps")
logger.info("=" * 60)
if not os.path.exists(args.source):
logger.error(f"Файл/директория не найден: {args.source}")
return 1
# 1. Авторизация
try:
token = get_token()
except Exception as e:
logger.error(f"Тест прерван: {e}")
return 1
# 2. Создание сессии
try:
uid = create_session(token)
except Exception as e:
logger.error(f"Тест прерван: {e}")
return 1
# 3-5. WebSocket + поток
result = asyncio.run(ws_stream(token, uid, args.source, args.frames, args.fps))
# 6. Удаление сессии
delete_session(token, uid)
# Итог
logger.info("=" * 60)
if result:
logger.info("✅ Тест завершён успешно — полный результат получен")
else:
logger.info("⚠️ Результат не получен (сервер не завершил диагностику)")
logger.info(f"Лог сохранён в: {args.log_file}")
logger.info("=" * 60)
return 0 if result else 2
if __name__ == "__main__":
sys.exit(main())