Веб-приложение диагностики по видео (ФПГ GigaDoc)
- Загрузка/сканирование видео, извлечение кадров, анализ через SDK ФПГ - Понятный диагноз с сохранением в SQLite и share-ссылками - Экспорт PDF/DOCX, скачивание записанного видео - Камерное сканирование на мобильных (canvas + MediaRecorder) - Секреты вынесены в переменные окружения
This commit is contained in:
+314
@@ -0,0 +1,314 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user