Add rotating file logs, access noise filters, and longer RTSP start timeout.
Persist app logs under logs/app with 7-day rotation, suppress routine health/pending access lines, raise default VIDEO_OPEN_TIMEOUT_SEC to 90s, and document consumable codes plus client timeout guidance. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,6 +51,7 @@ backend/.env.*
|
||||
|
||||
# Backend runtime & caches
|
||||
backend/logs/
|
||||
backend/logs/app/
|
||||
backend/Ultralytics/
|
||||
|
||||
# Large duplicate ActionFormer checkpoint (canonical copy lives under algorithm_subprocesses/5.15/weights/)
|
||||
|
||||
@@ -28,13 +28,22 @@ POSTGRES_PORT=45432
|
||||
# http://<GPU服务器局域网IP>:38080 作为服务端 Base URL。
|
||||
# API_PORT=38080
|
||||
|
||||
# --- 应用日志(loguru 持久化;目录已在 .gitignore)---
|
||||
# APP_LOG_DIR=logs/app
|
||||
# APP_LOG_FILE_NAME=server.log
|
||||
# APP_LOG_FILE_ENABLED=true
|
||||
# APP_LOG_ROTATION=7 days
|
||||
# APP_LOG_RETENTION=7 days
|
||||
# APP_LOG_LEVEL=INFO
|
||||
|
||||
# --- GPU 推理(可选覆盖,默认自动选 cuda:0)---
|
||||
# CONSUMABLE_CLASSIFIER_DEVICE=cuda:0
|
||||
# HAND_DETECTION_DEVICE=cuda:0
|
||||
|
||||
# --- Video:RTSP 与按路后端(须与客户端 camera_ids 一致)---
|
||||
# 单路 RTSP 首次打开超时(秒);四路须在「该值 + 5」秒内全部就绪,穿透/公网链路可调大(默认 45)。
|
||||
# VIDEO_OPEN_TIMEOUT_SEC=45
|
||||
# 单路 RTSP 首次打开超时(秒);开录 API 在「该值 + 5」秒内确认主摄就绪,失败最多重试 3 次。
|
||||
# 客户端 HTTP 超时建议 ≥ 300s(默认 90+5 秒 × 3 次)。穿透/公网链路可调大。
|
||||
# VIDEO_OPEN_TIMEOUT_SEC=90
|
||||
# VIDEO_DEFAULT_BACKEND=rtsp
|
||||
# VIDEO_CAMERA_BACKEND_OVERRIDES_JSON={"or-cam-01":"rtsp","or-cam-02":"hikvision_sdk"}
|
||||
# 站点 JSON:术间↔摄像头↔语音终端只在这里维护(voice_or_room_bindings);须同时含 video_rtsp_urls;可为 []。
|
||||
|
||||
33
backend/app/access_log_filters.py
Normal file
33
backend/app/access_log_filters.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Uvicorn access log 降噪:隐藏常态轮询/探活,保留异常与业务相关请求。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def should_log_uvicorn_access_line(message: str) -> bool:
|
||||
"""Return False to suppress a uvicorn access log line."""
|
||||
msg = message or ""
|
||||
if "GET" in msg and " 200 " in msg and "/health" in msg:
|
||||
return False
|
||||
if "GET" in msg and " 404 " in msg and "/pending-confirmation" in msg:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class _UvicornAccessNoiseFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
try:
|
||||
msg = record.getMessage()
|
||||
except Exception:
|
||||
return True
|
||||
return should_log_uvicorn_access_line(msg)
|
||||
|
||||
|
||||
def install_uvicorn_access_log_filters() -> None:
|
||||
"""Install filters on uvicorn.access (idempotent for repeated calls)."""
|
||||
access_logger = logging.getLogger("uvicorn.access")
|
||||
for existing in access_logger.filters:
|
||||
if isinstance(existing, _UvicornAccessNoiseFilter):
|
||||
return
|
||||
access_logger.addFilter(_UvicornAccessNoiseFilter())
|
||||
@@ -123,7 +123,6 @@ async def _call_recording_with_retries(
|
||||
|
||||
@router.get("/health", response_model=HealthResponse, tags=["health"])
|
||||
async def health() -> HealthResponse | JSONResponse:
|
||||
logger.debug("Health check")
|
||||
try:
|
||||
await check_database()
|
||||
except SQLAlchemyError as exc:
|
||||
|
||||
@@ -6,7 +6,7 @@ SURGERY_RECORDING_RETRY_DELAY_SECONDS: float = 1.0
|
||||
|
||||
# --- RTSP 连接与抽帧、推理门控(不含 URL,URL 在 Settings)---
|
||||
# 运行时以 Settings.video_open_timeout_sec(环境变量 VIDEO_OPEN_TIMEOUT_SEC)为准;此处为未注入 Settings 时的回退默认。
|
||||
VIDEO_OPEN_TIMEOUT_SEC: float = 45.0
|
||||
VIDEO_OPEN_TIMEOUT_SEC: float = 90.0
|
||||
VIDEO_READ_FAILURE_RECONNECT_THRESHOLD: int = 15
|
||||
VIDEO_RECONNECT_BACKOFF_SECONDS: float = 1.0
|
||||
VIDEO_INFERENCE_INTERVAL_SEC: float = 2.0
|
||||
|
||||
@@ -115,6 +115,12 @@ class _ServerGroup(_SettingsGroup):
|
||||
"server_host",
|
||||
"server_port",
|
||||
"server_reload",
|
||||
"app_log_dir",
|
||||
"app_log_file_name",
|
||||
"app_log_file_enabled",
|
||||
"app_log_rotation",
|
||||
"app_log_retention",
|
||||
"app_log_level",
|
||||
)
|
||||
|
||||
|
||||
@@ -143,12 +149,37 @@ class Settings(BaseSettings):
|
||||
validation_alias=AliasChoices("server_reload", "UVICORN_RELOAD"),
|
||||
)
|
||||
|
||||
app_log_dir: str = Field(
|
||||
default="logs/app",
|
||||
validation_alias=AliasChoices("APP_LOG_DIR", "app_log_dir"),
|
||||
)
|
||||
app_log_file_name: str = Field(
|
||||
default="server.log",
|
||||
validation_alias=AliasChoices("APP_LOG_FILE_NAME", "app_log_file_name"),
|
||||
)
|
||||
app_log_file_enabled: bool = Field(
|
||||
default=True,
|
||||
validation_alias=AliasChoices("APP_LOG_FILE_ENABLED", "app_log_file_enabled"),
|
||||
)
|
||||
app_log_rotation: str = Field(
|
||||
default="7 days",
|
||||
validation_alias=AliasChoices("APP_LOG_ROTATION", "app_log_rotation"),
|
||||
)
|
||||
app_log_retention: str = Field(
|
||||
default="7 days",
|
||||
validation_alias=AliasChoices("APP_LOG_RETENTION", "app_log_retention"),
|
||||
)
|
||||
app_log_level: str = Field(
|
||||
default="INFO",
|
||||
validation_alias=AliasChoices("APP_LOG_LEVEL", "app_log_level"),
|
||||
)
|
||||
|
||||
video_default_backend: Literal["rtsp", "hikvision_sdk", "auto"] = "rtsp"
|
||||
video_camera_backend_overrides_json: str = ""
|
||||
video_rtsp_url_template: str = ""
|
||||
#: 单路 RTSP 首次打开超时(秒);术间「全部摄像头就绪」等待为该值 + 5s。穿透/公网链路可调大。
|
||||
video_open_timeout_sec: float = Field(
|
||||
default=45.0,
|
||||
default=90.0,
|
||||
ge=5.0,
|
||||
le=900.0,
|
||||
validation_alias=AliasChoices("VIDEO_OPEN_TIMEOUT_SEC", "video_open_timeout_sec"),
|
||||
|
||||
79
backend/app/logging_setup.py
Normal file
79
backend/app/logging_setup.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Loguru + stdlib 桥接:stderr、持久化文件(7 天轮转)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
_LOG_FORMAT = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>"
|
||||
)
|
||||
_FILE_LOG_FORMAT = (
|
||||
"{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function} - {message}"
|
||||
)
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""将 stdlib logging(uvicorn 等)转发到 loguru。"""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
frame = logging.currentframe()
|
||||
depth = 2
|
||||
while frame is not None and frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
|
||||
def _resolve_app_log_path(settings: Settings) -> Path:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
log_dir = Path(settings.app_log_dir).expanduser()
|
||||
if not log_dir.is_absolute():
|
||||
log_dir = repo_root / log_dir
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return log_dir / settings.app_log_file_name
|
||||
|
||||
|
||||
def configure_application_logging(settings: Settings) -> Path | None:
|
||||
"""配置 loguru sink 并桥接 uvicorn/fastapi stdlib 日志。返回文件路径(若启用)。"""
|
||||
logger.remove()
|
||||
|
||||
logger.add(sys.stderr, format=_LOG_FORMAT, level=settings.app_log_level)
|
||||
|
||||
log_path: Path | None = None
|
||||
if settings.app_log_file_enabled:
|
||||
log_path = _resolve_app_log_path(settings)
|
||||
logger.add(
|
||||
str(log_path),
|
||||
format=_FILE_LOG_FORMAT,
|
||||
level=settings.app_log_level,
|
||||
rotation=settings.app_log_rotation,
|
||||
retention=settings.app_log_retention,
|
||||
encoding="utf-8",
|
||||
enqueue=True,
|
||||
)
|
||||
logger.info(
|
||||
"Application log file enabled: {} (rotation={}, retention={})",
|
||||
log_path,
|
||||
settings.app_log_rotation,
|
||||
settings.app_log_retention,
|
||||
)
|
||||
|
||||
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
||||
for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"):
|
||||
lib_logger = logging.getLogger(name)
|
||||
lib_logger.handlers = [InterceptHandler()]
|
||||
lib_logger.propagate = False
|
||||
|
||||
return log_path
|
||||
@@ -116,6 +116,11 @@ services:
|
||||
RTSP_PRIMARY_CAMERA_ID: ${RTSP_PRIMARY_CAMERA_ID:-or-cam-03}
|
||||
RTSP_RECORD_ALL_CAMERAS: ${RTSP_RECORD_ALL_CAMERAS:-false}
|
||||
RTSP_SEGMENT_DURATION_SEC: ${RTSP_SEGMENT_DURATION_SEC:-120}
|
||||
VIDEO_OPEN_TIMEOUT_SEC: ${VIDEO_OPEN_TIMEOUT_SEC:-90}
|
||||
APP_LOG_DIR: ${APP_LOG_DIR:-logs/app}
|
||||
APP_LOG_FILE_ENABLED: ${APP_LOG_FILE_ENABLED:-true}
|
||||
APP_LOG_ROTATION: ${APP_LOG_ROTATION:-7 days}
|
||||
APP_LOG_RETENTION: ${APP_LOG_RETENTION:-7 days}
|
||||
# api 在 Compose 桥接网内反代 mediamtx-hls:8888;宿主机调试 HLS 仍用 127.0.0.1:18888
|
||||
DEMO_HLS_PREVIEW_UPSTREAM: ${DEMO_HLS_PREVIEW_UPSTREAM:-http://mediamtx-hls:8888}
|
||||
DEMO_HLS_PREVIEW_CONFIG_DIR: ${DEMO_HLS_PREVIEW_CONFIG_DIR:-/hls-preview-config}
|
||||
@@ -128,6 +133,7 @@ services:
|
||||
- "${API_PORT:-38080}:8000"
|
||||
volumes:
|
||||
- hls_preview_config:/hls-preview-config
|
||||
- ./logs:/app/logs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
db:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@@ -8,39 +6,12 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from loguru import logger
|
||||
|
||||
from app.access_log_filters import install_uvicorn_access_log_filters
|
||||
from app.api import router as api_router
|
||||
from app.config import settings
|
||||
from app.database import check_database, engine
|
||||
from app.dependencies import build_container
|
||||
|
||||
|
||||
def _configure_uvicorn_access_log_filters() -> None:
|
||||
"""第三方或 Demo 若轮询 pending-confirmation,无条目时 404 为常态;压低 uvicorn access 刷屏。"""
|
||||
|
||||
class _SuppressPendingPoll404(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
try:
|
||||
msg = record.getMessage()
|
||||
except Exception:
|
||||
return True
|
||||
if "/pending-confirmation" in msg and "GET" in msg and " 404 " in msg:
|
||||
return False
|
||||
return True
|
||||
|
||||
logging.getLogger("uvicorn.access").addFilter(_SuppressPendingPoll404())
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""集中配置 loguru sink;由 create_app 显式调用,避免 import-time 副作用。"""
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
format=(
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>"
|
||||
),
|
||||
)
|
||||
from app.logging_setup import configure_application_logging
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -89,8 +60,8 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
configure_logging()
|
||||
_configure_uvicorn_access_log_filters()
|
||||
configure_application_logging(settings)
|
||||
install_uvicorn_access_log_filters()
|
||||
application = FastAPI(
|
||||
title="Operation Room Monitor",
|
||||
lifespan=lifespan,
|
||||
|
||||
30
backend/tests/test_access_log_filters.py
Normal file
30
backend/tests/test_access_log_filters.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from app.access_log_filters import should_log_uvicorn_access_line
|
||||
|
||||
|
||||
def test_suppress_health_200() -> None:
|
||||
line = '127.0.0.1:52854 - "GET /health HTTP/1.1" 200 OK'
|
||||
assert should_log_uvicorn_access_line(line) is False
|
||||
|
||||
|
||||
def test_keep_health_503() -> None:
|
||||
line = '127.0.0.1:52854 - "GET /health HTTP/1.1" 503 Service Unavailable'
|
||||
assert should_log_uvicorn_access_line(line) is True
|
||||
|
||||
|
||||
def test_suppress_pending_poll_404() -> None:
|
||||
line = (
|
||||
'192.168.3.140:12345 - "GET /client/surgeries/123456/pending-confirmation HTTP/1.1" 404 Not Found'
|
||||
)
|
||||
assert should_log_uvicorn_access_line(line) is False
|
||||
|
||||
|
||||
def test_keep_pending_200() -> None:
|
||||
line = (
|
||||
'192.168.3.140:12345 - "GET /client/surgeries/123456/pending-confirmation HTTP/1.1" 200 OK'
|
||||
)
|
||||
assert should_log_uvicorn_access_line(line) is True
|
||||
|
||||
|
||||
def test_keep_start_post() -> None:
|
||||
line = '192.168.3.85:9999 - "POST /client/surgeries/start HTTP/1.1" 503 Service Unavailable'
|
||||
assert should_log_uvicorn_access_line(line) is True
|
||||
@@ -4,6 +4,23 @@
|
||||
|
||||
对接方请按部署版本核对本节;**已发布路径与字段语义以正文为准**,本节仅记录相对上一版的客户端需改动点。
|
||||
|
||||
### 2026-05-26(续)
|
||||
|
||||
**行为 / 运维**
|
||||
|
||||
| 项 | 变更 |
|
||||
| --- | --- |
|
||||
| `POST /client/surgeries/start` | 默认 RTSP 开录等待由 45s 调至 **90s**;客户端 HTTP 超时建议 ≥ **300s** |
|
||||
| 服务端日志 | 应用日志持久化至 `backend/logs/app/server.log`,**7 天轮转/保留**;access 日志隐藏 `GET /health` 200 与 pending 轮询 404 |
|
||||
|
||||
### 2026-05-26
|
||||
|
||||
**文档**
|
||||
|
||||
| 接口 | 变更 | 客户端需改动 |
|
||||
| ---- | ---- | ------------ |
|
||||
| `POST /internal/demo/offline-batch` 及配套 GET | **新增** §5.7–§5.9:链路 3 离线 MP4 上传、耗时轮询、标注视频下载 | 非 RTSP 集成时可走上传视频路径;仍用 `GET /client/surgeries/{surgery_id}/result` 查消耗,**无需** `start` / `end` |
|
||||
|
||||
### 2026-05-25
|
||||
|
||||
**行为对齐(实现已与正文一致)**
|
||||
@@ -16,8 +33,7 @@
|
||||
|
||||
**未变更**
|
||||
|
||||
- 路由路径、HTTP 方法、`surgery_id` 约束、成功体字段名与 §5 各节描述一致。
|
||||
- 可新增 `/internal/demo/...` 等内部接口,不影响本文档契约。
|
||||
- `/client/...` 路由路径、HTTP 方法、`surgery_id` 约束、成功体字段名与 §5 各节描述一致。
|
||||
|
||||
---
|
||||
|
||||
@@ -26,10 +42,11 @@
|
||||
| **能力** | **说明** |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **探活** | `GET /health`,用于检查进程和数据库状态,详见 5.1 节。 |
|
||||
| **开始手术** | `POST /client/surgeries/start`,只有在开录确认成功后才返回 `200`。 |
|
||||
| **结束手术** | `POST /client/surgeries/end`,只有在停录确认成功后才返回 `200`。 |
|
||||
| **查询结果** | `GET /client/surgeries/{surgery_id}/result`,至少存在一条消耗明细时返回 `200`;否则返回 `503`,常见错误码为 `RESULT_NOT_READY`。 |
|
||||
| **待确认播报** | **官方浏览器客户端**(仓库 `clients/voice-confirmation/`,可独立部署):同一 WebSocket 上推送 `voice_pending`(载荷与 `GET .../pending-confirmation` 成功体一致,另含 `type`);无队首时 `voice_pending_empty`;**不轮询 GET**。**第三方**仍可用 `GET .../pending-confirmation` 拉取队首。 |
|
||||
| **开始手术** | `POST /client/surgeries/start`,只有在开录确认成功后才返回 `200`(**链路 1 · 真 RTSP**)。 |
|
||||
| **结束手术** | `POST /client/surgeries/end`,只有在停录确认成功后才返回 `200`(链路 1)。 |
|
||||
| **上传视频** | `POST /internal/demo/offline-batch`,上传单路 MP4 离线推理(**链路 3**);**无需** `start` / `end`,不触发语音终端;详见 5.7 节。需运维开启 `DEMO_ORCHESTRATOR_ENABLED=true`。 |
|
||||
| **查询结果** | `GET /client/surgeries/{surgery_id}/result`,至少存在一条消耗明细时返回 `200`;否则返回 `503`,`detail.code` 为 `RESULT_NOT_READY`。链路 1 / 3 共用。 |
|
||||
| **待确认播报** | **官方浏览器客户端**(仓库 `clients/voice-confirmation/`,可独立部署):同一 WebSocket 上推送 `voice_pending`(载荷与 `GET .../pending-confirmation` 成功体一致,另含 `type`);无队首时 `voice_pending_empty`;**不轮询 GET**。**第三方**仍可用 `GET .../pending-confirmation` 拉取队首。**链路 3 无待确认。** |
|
||||
| **待确认答复** | `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。 |
|
||||
|
||||
## 1. 服务与基础信息
|
||||
@@ -38,8 +55,9 @@
|
||||
| ----------------------- | ------------------------------------------- |
|
||||
| **协议** | `HTTP/HTTPS` |
|
||||
| **端口** | `38080`,生产环境以实际入口为准 |
|
||||
| **路由** | 无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health` |
|
||||
| **路由** | 无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health`;链路 3 上传位于 `/internal/demo/...` |
|
||||
| **`start` / `end` 请求体** | JSON |
|
||||
| **链路 3 上传请求体** | `multipart/form-data`,字段见 5.7 节 |
|
||||
| **`resolve` 请求体** | `multipart/form-data`,字段名为 `audio` |
|
||||
| **在线文档** | `/docs`、`/redoc` |
|
||||
| **语音确认官方页面** | 仓库 `clients/voice-confirmation/`(静态资源,与 API 分宿;需为浏览器配置 CORS,见该目录 `README.md`) |
|
||||
@@ -48,7 +66,7 @@
|
||||
|
||||
## 2. 摄像头 ID 与 RTSP
|
||||
|
||||
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`。
|
||||
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 **链路 1** 的 `POST /client/surgeries/start` 中传 `camera_ids`。**链路 3 上传视频不需要 `camera_ids`。**
|
||||
|
||||
| **camera_id** | **RTSP** | **备注** |
|
||||
| ------------- | -------- | -------- |
|
||||
@@ -64,13 +82,17 @@ RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运
|
||||
| **序号** | **方法** | **路径** | **说明** |
|
||||
| ------ | ------ | ------------------------------------------------------------------------------- | ------- |
|
||||
| 1 | `GET` | `/health` | 探活 |
|
||||
| 2 | `POST` | `/client/surgeries/start` | 开始手术 |
|
||||
| 3 | `POST` | `/client/surgeries/end` | 结束手术 |
|
||||
| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
|
||||
| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材 |
|
||||
| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复 |
|
||||
| 7 | `GET` | `/client/voice-terminals/{terminal_id}/assignment` | 可选:查询当前指派(调试或简易集成;**官方浏览器客户端仅用 WebSocket**) |
|
||||
| 8 | `WS` | `/client/voice-terminals/ws?terminal_id=...` | 语音终端长连接,接收开录/停录指派(**推荐**;与 `clients/voice-confirmation` 共用) |
|
||||
| 2 | `POST` | `/client/surgeries/start` | 开始手术(链路 1) |
|
||||
| 3 | `POST` | `/client/surgeries/end` | 结束手术(链路 1) |
|
||||
| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果(链路 1 / 3 共用) |
|
||||
| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材(链路 1) |
|
||||
| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复(链路 1) |
|
||||
| 7 | `POST` | `/internal/demo/offline-batch` | **链路 3**:上传 MP4 离线推理(需 `DEMO_ORCHESTRATOR_ENABLED=true`) |
|
||||
| 8 | `GET` | `/internal/demo/offline-batch/{surgery_id}/timing` | 链路 3:查询文本 / 标注视频各阶段耗时 |
|
||||
| 9 | `GET` | `/internal/demo/offline-batch/{surgery_id}/visualization` | 链路 3:下载标注 MP4(可选) |
|
||||
| 10 | `GET` | `/internal/demo/recording-modes-status` | 可选:探测链路 3 是否已启用 |
|
||||
| 11 | `GET` | `/client/voice-terminals/{terminal_id}/assignment` | 可选:查询当前指派(调试或简易集成;**官方浏览器客户端仅用 WebSocket**) |
|
||||
| 12 | `WS` | `/client/voice-terminals/ws?terminal_id=...` | 语音终端长连接,接收开录/停录指派(**推荐**;与 `clients/voice-confirmation` 共用) |
|
||||
| | | | |
|
||||
|
||||
**术间与语音终端绑定(服务端配置)**
|
||||
@@ -84,7 +106,7 @@ RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运
|
||||
|
||||
## 4. 流程
|
||||
|
||||
### 4.1 时序图
|
||||
### 4.1 时序图(链路 1 · 真 RTSP)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -118,7 +140,7 @@ sequenceDiagram
|
||||
Server-->>Client: 200(持久化后可查时返回)
|
||||
```
|
||||
|
||||
### 4.2 状态图
|
||||
### 4.2 状态图(链路 1)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -130,6 +152,46 @@ flowchart LR
|
||||
E -->|result 200| C
|
||||
```
|
||||
|
||||
### 4.3 时序图(链路 3 · 上传 MP4)
|
||||
|
||||
与 §4.1 并行存在,适用于**已有完整术间录像**、无需实时拉流与语音待确认的场景。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant Server as 服务端
|
||||
|
||||
opt 探测(可选)
|
||||
Client->>Server: GET /internal/demo/recording-modes-status
|
||||
Server-->>Client: demo_recording_modes_enabled
|
||||
end
|
||||
|
||||
Client->>Server: POST /internal/demo/offline-batch
|
||||
Note over Client,Server: multipart: surgery_id, video1, candidate_consumables_json, include_visualization
|
||||
Server-->>Client: 200 accepted(TSV 推理已完成)
|
||||
|
||||
Client->>Server: GET /client/surgeries/{surgery_id}/result
|
||||
Server-->>Client: 200 或 503 RESULT_NOT_READY
|
||||
|
||||
opt include_visualization=true
|
||||
loop 直至 video_status=ready 或 failed
|
||||
Client->>Server: GET .../offline-batch/{surgery_id}/timing
|
||||
Server-->>Client: video_status pending / ready / failed
|
||||
end
|
||||
Client->>Server: GET .../offline-batch/{surgery_id}/visualization
|
||||
Server-->>Client: video/mp4
|
||||
end
|
||||
```
|
||||
|
||||
**与链路 1 的差异**
|
||||
|
||||
| 项目 | 链路 1(RTSP) | 链路 3(上传 MP4) |
|
||||
| ---- | -------------- | ------------------ |
|
||||
| 开录 / 停录 | 需要 `start` / `end` | **不需要** |
|
||||
| 语音终端 / 待确认 | 有 | **无**(结果直接入库) |
|
||||
| 查结果 | `GET .../result` | 同上 |
|
||||
| 服务端开关 | RTSP 站点配置 | 另需 `DEMO_ORCHESTRATOR_ENABLED=true` |
|
||||
|
||||
## 5. 接口详情
|
||||
|
||||
|
||||
@@ -143,7 +205,7 @@ flowchart LR
|
||||
|
||||
**业务错误响应**
|
||||
|
||||
多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
|
||||
`/client/...` 多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -155,6 +217,8 @@ flowchart LR
|
||||
}
|
||||
```
|
||||
|
||||
§5.7–§5.9(链路 3)的部分校验失败返回 FastAPI 默认 `detail` **字符串**(无 `code` 包装),见各节状态码表。
|
||||
|
||||
### 5.1 探活
|
||||
|
||||
**基本信息**
|
||||
@@ -185,9 +249,11 @@ flowchart LR
|
||||
**业务说明**
|
||||
|
||||
- 服务端会为 `camera_ids` 中的每个摄像头建立拉流与推理任务,只有在确认开录成功(如首帧就绪)后才返回 HTTP `200`。
|
||||
- 本接口**同步阻塞**直至开录确认;默认 RTSP 等待约 **95 秒/次**、最多 **3 次重试**。客户端 HTTP 超时建议 **≥ 300 秒**(Apipost 等工具默认 30–60 秒易触发 `ESOCKETTIMEDOUT`)。服务端可通过 `VIDEO_OPEN_TIMEOUT_SEC` 调大(默认 90)。
|
||||
|
||||
- `candidate_consumables` 为空时,服务端会展开为目录中的全部耗材名。
|
||||
- 非空时,每项可为**耗材名称**或**产品编码**(`label_id`);编码通过 `consumable_classifier_labels.yaml` 解析为类名。亦支持医院导出对象(见下表)。
|
||||
- `candidate_consumables` 缺省或 `[]` 时,服务端会展开为 `consumable_classifier_labels.yaml` 中的全部类名(无有效 yaml 时开录失败)。
|
||||
- 非空时,每项可为**耗材名称**或**产品编码**(`label_id`);编码通过 yaml 解析为类名后再参与推理与白名单。亦支持医院导出对象(见下表)。
|
||||
- **41 类完整类名与产品编码对照**:见 [`docs/耗材产品编码与类名对照表.md`](耗材产品编码与类名对照表.md)(须用完整类名或编码,口语简称如「纱布」不会自动映射)。
|
||||
|
||||
**请求体(JSON)**
|
||||
|
||||
@@ -201,7 +267,7 @@ flowchart LR
|
||||
|
||||
| **形式** | **示例** | **说明** |
|
||||
| -------- | -------- | -------- |
|
||||
| 名称字符串 | `"纱布"` | 与 yaml / 模型类名一致 |
|
||||
| 名称字符串 | `"医用纱布敷料"` | 须与 yaml / 模型类名**完全一致**(见对照表) |
|
||||
| 编码字符串 | `"14764-2-4"` | 与 yaml 中 `label_id` 一致,服务端解析为类名 |
|
||||
| 导出对象(名称) | `{"消耗品编号":"14764-2-4","名称":"一次性使用手术单"}` | 以 `名称`(或 `name`)为准 |
|
||||
| 导出对象(仅编号) | `{"消耗品编号":"14764-2-4"}` | 按编号解析为类名 |
|
||||
@@ -311,6 +377,8 @@ flowchart LR
|
||||
|
||||
- 上述 `503` 的 `detail.code` 为 **`RESULT_NOT_READY`**;`detail.message` 说明具体原因(如未开始、进行中尚无明细、已结束无消耗等)。
|
||||
|
||||
- **链路 3**:离线 batch 完成后结果直接写入数据库,通过本接口查询;**无需**先调用 `start` / `end`。若 batch 识别结果为零条明细,本接口仍返回 `503`。
|
||||
|
||||
|
||||
**响应体(200)**
|
||||
|
||||
@@ -326,7 +394,7 @@ flowchart LR
|
||||
|
||||
| **字段** | **类型** | **说明** |
|
||||
| ----------- | --------- | ---------------------------------- |
|
||||
| `item_id` | `string` | 物品 ID;有目录时多为产品编码,否则通常与名称或模型类名一致 |
|
||||
| `item_id` | `string` | 物品 ID;有 yaml 时多为 `label_id`,否则通常与类名一致 |
|
||||
| `item_name` | `string` | 物品名称 |
|
||||
| `qty` | `integer` | 本条记录数量,当前恒为 `1`;一次识别或一次人工确认只追加一条明细 |
|
||||
| `doctor_id` | `string` | 记账关联的医生或系统标识 |
|
||||
@@ -422,8 +490,8 @@ flowchart LR
|
||||
| **HTTP** | **说明** |
|
||||
| -------- | ----------------------------------------------------- |
|
||||
| `200` | 当前有一条待确认 |
|
||||
| `404` | 无待确认或手术未活跃;常见错误码为 `NO_PENDING_CONFIRMATION` |
|
||||
| `422` | 例如话术为空导致无法 TTS;错误码见响应,如 `TTS_TEXT_EMPTY` |
|
||||
| `404` | 无待确认或手术未活跃;`detail.code` 为 `NO_PENDING_CONFIRMATION` |
|
||||
| `422` | 例如话术为空导致无法 TTS;`detail.code` 如 `TTS_TEXT_EMPTY` |
|
||||
| `503` | 语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED`、`TTS_ERROR` |
|
||||
|
||||
### 5.6 提交待确认结果(医生语音)
|
||||
@@ -440,7 +508,7 @@ flowchart LR
|
||||
|
||||
| **参数** | **约束** | **说明** |
|
||||
| ----------------- | ---------- | -------------------------------- |
|
||||
| `surgery_id` | 6 位数字 | 同 5.0 节 |
|
||||
| `surgery_id` | 6 位数字 | 同 §5 路径参数 |
|
||||
| `confirmation_id` | 长度 1 到 128 | 与 5.5 节响应中的 `confirmation_id` 一致 |
|
||||
|
||||
**请求体(multipart)**
|
||||
@@ -451,7 +519,7 @@ flowchart LR
|
||||
|
||||
**业务说明**
|
||||
|
||||
音频上传至对象存储后执行 ASR 和候选解析。若识别为确认某个候选项,则记一条消耗;若识别为否认全部候选,则不记消耗。
|
||||
音频上传至对象存储后执行 ASR 和候选解析。若识别为确认某个候选项,则记一条消耗;若识别为否认全部候选,则不记消耗。ASR/解析可重试失败时队首待确认项**不弹出**,便于客户端重录。
|
||||
|
||||
**响应体(200)**
|
||||
|
||||
@@ -471,9 +539,9 @@ flowchart LR
|
||||
| **HTTP** | **说明** |
|
||||
| -------- | ------------------------------------------------------------------------------------- |
|
||||
| `200` | 已受理并完成解析 |
|
||||
| `404` | 确认项不存在或手术未活跃;例如 `CONFIRMATION_NOT_FOUND` |
|
||||
| `409` | 当前确认项已处理过;例如 `CONFIRMATION_ALREADY_RESOLVED` |
|
||||
| `422` | 空文件、非 `.wav`、`VOICE_AUDIO_INVALID`、**ASR/解析可重试失败**(如 `VOICE_ASR_FAILED`、`VOICE_TEXT_EMPTY`、`VOICE_PARSE_FAILED`)等,具体错误码见 `detail.code` |
|
||||
| `404` | 确认项不存在或手术未活跃;`detail.code` 如 `CONFIRMATION_NOT_FOUND` |
|
||||
| `409` | 当前确认项已处理过;`detail.code` 如 `CONFIRMATION_ALREADY_RESOLVED` |
|
||||
| `422` | 空文件、非 `.wav`、`VOICE_AUDIO_INVALID`、**ASR/解析可重试失败**(如 `VOICE_ASR_FAILED`、`VOICE_TEXT_EMPTY`、`VOICE_PARSE_FAILED`)等 |
|
||||
| `503` | MinIO、百度等依赖不可用;例如 `MINIO_NOT_CONFIGURED`、`MINIO_UPLOAD_FAILED`、`BAIDU_NOT_CONFIGURED` |
|
||||
|
||||
**cURL 示例**
|
||||
@@ -484,11 +552,159 @@ curl -sS -X POST \
|
||||
-F "audio=@/path/to/voice.wav;type=audio/wav"
|
||||
```
|
||||
|
||||
### 5.7 语音终端 assignment(HTTP,可选)
|
||||
### 5.7 上传手术视频(链路 3 · 离线 batch)
|
||||
|
||||
**基本信息**
|
||||
|
||||
| **项目** | **内容** |
|
||||
| ---------------- | --------------------------------- |
|
||||
| **方法** | `POST` |
|
||||
| **路径** | `/internal/demo/offline-batch` |
|
||||
| **Content-Type** | `multipart/form-data` |
|
||||
|
||||
**前置条件**
|
||||
|
||||
- 服务端环境变量 **`DEMO_ORCHESTRATOR_ENABLED=true`**。未开启时本路径返回 **`404`**,`detail` 为 `Demo recording modes disabled (set DEMO_ORCHESTRATOR_ENABLED=true).`。
|
||||
- 可先调用 **`GET /internal/demo/recording-modes-status`** 查看 `demo_recording_modes_enabled` 是否为 `true`(该探测接口始终注册,不依赖上述开关)。
|
||||
|
||||
**业务说明**
|
||||
|
||||
- 上传**单路完整 MP4**(字段名 `video1`),服务端调用离线算法包(`algorithm_subprocesses/5.15/main.py`)完成整段推理,解析 TSV 后将消耗明细**直接写入数据库**。
|
||||
- **不**启动 RTSP 实时会话,**不**调用 `start` / `end`,**不**触发语音终端或待确认队列。
|
||||
- 请求在服务端**同步阻塞**直至 TSV 推理结束才返回 `200`;长视频可能耗时较久,客户端应设置足够长的 HTTP 超时。
|
||||
- 相同视频内容(SHA-256)+ 相同候选清单会命中结果缓存(跨 `surgery_id` 复用),响应 `message` 中含 `cache=hit` 或 `cache=miss`。
|
||||
- `candidate_consumables_json` 的语义与 5.2 节 `candidate_consumables` 一致(名称、产品编码、医院导出对象);缺省 `"[]"` 时展开为 yaml 全部类名。
|
||||
|
||||
**请求体(multipart)**
|
||||
|
||||
| **字段名** | **类型** | **必填** | **说明** |
|
||||
| ------- | ------ | ------ | ------ |
|
||||
| `surgery_id` | `string` | 是 | 6 位数字 |
|
||||
| `video1` | `file` | 是 | 单路完整 MP4;空文件返回 `422` |
|
||||
| `candidate_consumables_json` | `string` | 否 | JSON 数组字符串,默认 `"[]"`;格式同 5.2 节候选清单 |
|
||||
| `include_visualization` | `boolean` | 否 | 是否后台生成标注 MP4;默认 `false`。为 `true` 时响应含 `visualization_url`,标注视频异步生成,需轮询 5.8 节 |
|
||||
|
||||
**响应体(200)**
|
||||
|
||||
| **字段** | **类型** | **说明** |
|
||||
| -------- | -------- | -------- |
|
||||
| `surgery_id` | `string` | 与请求一致 |
|
||||
| `status` | `string` | 成功时为 `accepted` |
|
||||
| `message` | `string` | 含 `rows=` 行数、`cache=hit|miss`、医生识别摘要等 |
|
||||
| `visualization_url` | `string \| null` | `include_visualization=true` 时为相对路径 `/internal/demo/offline-batch/{surgery_id}/visualization`;否则为 `null` |
|
||||
| `doctor_name` | `string \| null` | 算法输出的医生姓名(若识别到) |
|
||||
| `doctor_id` | `string \| null` | 医生 ID(若识别到) |
|
||||
| `doctor_display` | `string \| null` | 展示用医生信息 |
|
||||
| `text_duration_sec` | `number` | TSV / 主流程耗时(秒,保留 3 位小数) |
|
||||
| `video_duration_sec` | `number \| null` | 标注视频耗时;未请求或仍在后台时为 `null` |
|
||||
| `total_duration_sec` | `number` | 已完成阶段的合计耗时(秒) |
|
||||
|
||||
**状态码**
|
||||
|
||||
| **HTTP** | **说明** |
|
||||
| -------- | -------- |
|
||||
| `200` | 离线推理已完成,结果已入库(可能为零条明细) |
|
||||
| `404` | Demo 模式未启用 |
|
||||
| `422` | `surgery_id` 非 6 位、`candidate_consumables_json` 非法、`video1` 为空等 |
|
||||
| `500` | 上传落盘失败 |
|
||||
| `503` | 离线 batch 子进程失败;`detail` 形如 `offline batch failed: ...` |
|
||||
|
||||
**后续步骤**
|
||||
|
||||
1. 调用 **`GET /client/surgeries/{surgery_id}/result`**(5.4 节)查询消耗明细。
|
||||
2. 若 `include_visualization=true`,轮询 **`GET .../offline-batch/{surgery_id}/timing`**(5.8 节)直至 `video_status` 为 `ready` 或 `failed`,再 **`GET .../visualization`**(5.9 节)下载 MP4。
|
||||
|
||||
**cURL 示例**
|
||||
|
||||
```
|
||||
curl -sS -X POST "http://<主机>:38080/internal/demo/offline-batch" \
|
||||
-F "surgery_id=123456" \
|
||||
-F "video1=@/path/to/surgery.mp4;type=video/mp4" \
|
||||
-F 'candidate_consumables_json=["14764-2-4","8036-5-22"]' \
|
||||
-F "include_visualization=true"
|
||||
```
|
||||
|
||||
**响应示例(200)**
|
||||
|
||||
```
|
||||
{
|
||||
"surgery_id": "123456",
|
||||
"status": "accepted",
|
||||
"message": "非实时精确视频处理完成;rows=3 cache=miss;医生=张三 (6611);标注视频后台生成中(完成后刷新 visualization URL,24 小时内有效)",
|
||||
"visualization_url": "/internal/demo/offline-batch/123456/visualization",
|
||||
"doctor_name": "张三",
|
||||
"doctor_id": "6611",
|
||||
"doctor_display": "张三 (6611)",
|
||||
"text_duration_sec": 842.5,
|
||||
"video_duration_sec": null,
|
||||
"total_duration_sec": 842.5
|
||||
}
|
||||
```
|
||||
|
||||
### 5.8 查询离线 batch 各阶段耗时
|
||||
|
||||
**基本信息**
|
||||
|
||||
| **项目** | **内容** |
|
||||
| -------- | ------ |
|
||||
| **方法** | `GET` |
|
||||
| **路径** | `/internal/demo/offline-batch/{surgery_id}/timing` |
|
||||
| **路径参数** | `surgery_id`(6 位数字) |
|
||||
| **请求体** | 无 |
|
||||
|
||||
**业务说明**
|
||||
|
||||
- 在 5.7 节上传完成后可用;用于轮询标注视频是否生成完毕。
|
||||
- `video_status`:`skipped`(未请求标注视频)、`pending`(后台生成中)、`ready`(可下载)、`failed`(生成失败)。
|
||||
|
||||
**响应体(200)**
|
||||
|
||||
| **字段** | **类型** | **说明** |
|
||||
| -------- | -------- | -------- |
|
||||
| `surgery_id` | `string` | 手术号 |
|
||||
| `text_duration_sec` | `number` | TSV 主流程耗时(秒) |
|
||||
| `video_duration_sec` | `number \| null` | 标注视频耗时;未完成时为 `null` |
|
||||
| `total_duration_sec` | `number` | 合计耗时(秒) |
|
||||
| `video_status` | `string` | `skipped` / `pending` / `ready` / `failed` |
|
||||
|
||||
**状态码**
|
||||
|
||||
| **HTTP** | **说明** |
|
||||
| -------- | -------- |
|
||||
| `200` | 有该 `surgery_id` 的 timing 记录 |
|
||||
| `404` | Demo 未启用,或尚无 timing 记录 |
|
||||
| `422` | `surgery_id` 不符合约束 |
|
||||
|
||||
### 5.9 获取离线 batch 标注视频
|
||||
|
||||
**基本信息**
|
||||
|
||||
| **项目** | **内容** |
|
||||
| -------- | ------ |
|
||||
| **方法** | `GET` |
|
||||
| **路径** | `/internal/demo/offline-batch/{surgery_id}/visualization` |
|
||||
| **路径参数** | `surgery_id`(6 位数字) |
|
||||
| **请求体** | 无 |
|
||||
|
||||
**业务说明**
|
||||
|
||||
- 仅当 5.7 节 `include_visualization=true` 且后台生成成功时可用。
|
||||
- 响应为 **`video/mp4`** 文件流,文件名 `{surgery_id}_result_vis.mp4`。
|
||||
- 标注文件默认保留 **24 小时**(`VIDEO_BATCH_VIS_TTL_HOURS`),过期后返回 `404`。
|
||||
|
||||
**状态码**
|
||||
|
||||
| **HTTP** | **说明** |
|
||||
| -------- | -------- |
|
||||
| `200` | MP4 文件流 |
|
||||
| `404` | Demo 未启用、尚未生成或已过期 |
|
||||
| `422` | `surgery_id` 不符合约束 |
|
||||
|
||||
### 5.10 语音终端 assignment(HTTP,可选)
|
||||
|
||||
**路径** `GET /client/voice-terminals/{terminal_id}/assignment`
|
||||
|
||||
仓库内 **手术室耗材语音确认浏览器客户端**(`clients/voice-confirmation/`)仅通过 **§5.8 WebSocket** 接收指派,**不调用**本接口。此处供运维脚本、未实现 WS 的第三方临时拉取 `active_surgery_id`。
|
||||
仓库内 **手术室耗材语音确认浏览器客户端**(`clients/voice-confirmation/`)仅通过 **§5.11 WebSocket** 接收指派,**不调用**本接口。此处供运维脚本、未实现 WS 的第三方临时拉取 `active_surgery_id`。
|
||||
|
||||
**响应 200**
|
||||
|
||||
@@ -497,7 +713,7 @@ curl -sS -X POST \
|
||||
| `voice_terminal_id` | `string` | 与路径一致 |
|
||||
| `active_surgery_id` | `string \| null` | 当前指派手术 6 位号;无指派时为 `null` |
|
||||
|
||||
### 5.8 语音终端 WebSocket
|
||||
### 5.11 语音终端 WebSocket
|
||||
|
||||
**路径** `GET ws://<主机>:<端口>/client/voice-terminals/ws?terminal_id=<终端ID>`(HTTPS 部署时使用 `wss://`)
|
||||
|
||||
|
||||
138
docs/耗材产品编码与类名对照表.md
Normal file
138
docs/耗材产品编码与类名对照表.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 耗材产品编码与类名对照表
|
||||
|
||||
本文档列出视觉耗材分类模型(YOLO 耗材分类 + `consumable_classifier_labels.yaml`)支持的 **41 类**耗材。开录接口 `candidate_consumables`、离线批处理白名单、结果中的 `item_id` 均以此为准。
|
||||
|
||||
**数据源(随代码发布)**:`backend/app/resources/consumable_classifier_labels.yaml`
|
||||
|
||||
**重要**
|
||||
|
||||
- 推理时类名以 **模型权重内嵌 `names`** 为准;本表与 yaml 保持一致。
|
||||
- `candidate_consumables` 须传 **完整类名** 或 **产品编码(label_id)**;口语简称(如「纱布」「缝线」)**不会**自动映射,会导致算法白名单与模型类名无交集。
|
||||
- 同一 `label_id` 若对应多个规格,yaml 中以 `/` 连接;开录时可传其中任一段编码,服务端会解析为对应类名。
|
||||
- `candidate_consumables` 缺省或 `[]` 时,服务端展开为下表 **全部 41 个类名**。
|
||||
|
||||
---
|
||||
|
||||
## 全量对照表
|
||||
|
||||
| 序号 | 类名(`candidate_consumables` 可用) | 产品编码(`label_id`) |
|
||||
| ---: | --- | --- |
|
||||
| 0 | MCuⅡ功能性宫内节育器 | `740-2-14` |
|
||||
| 1 | 一次性中性电极板 | `4787-2-55` |
|
||||
| 2 | 一次性使用乳胶导尿管 | `7386-10-89` |
|
||||
| 3 | 一次性使用冲洗袋 | `1644-37-3` |
|
||||
| 4 | 一次性使用医疗卫生用品 | `2241272` |
|
||||
| 5 | 一次性使用单极手术电极 | `4805-2-50` |
|
||||
| 6 | 一次性使用导尿管 | `735592/14556-4-18` |
|
||||
| 7 | 一次性使用手术单 | `14764-2-4` |
|
||||
| 8 | 一次性使用无菌敷贴 | `215-93-1` |
|
||||
| 9 | 一次性使用无菌气管插管Tracheal Tube | `14780-3-5` |
|
||||
| 10 | 一次性使用无菌注射器带针 | `1531-3-2/1531-3-1/1174-42-4/1174-42-1` |
|
||||
| 11 | 一次性使用无菌采样拭子 | `15026-1-1` |
|
||||
| 12 | 一次性使用气管插管 | `21444-1-2` |
|
||||
| 13 | 一次性使用灭菌橡胶外科手套 | `10362-1-4` |
|
||||
| 14 | 一次性使用牙垫 | `975961` |
|
||||
| 15 | 一次性使用精密过滤输液器 带针 | `2295950` |
|
||||
| 16 | 一次性使用肛门管 | `1518-22-4` |
|
||||
| 17 | 一次性使用胃管 | `1518-34-17` |
|
||||
| 18 | 一次性使用血液透析管路 | `14730-10-10` |
|
||||
| 19 | 一次性使用输卵管导管 | `1380-15-1` |
|
||||
| 20 | 一次性使用雾化器 | `5019-4-43` |
|
||||
| 21 | 一次性使用静脉留置针 | `12591-1-184` |
|
||||
| 22 | 一次性使用静脉输液针 | `129-5-30` |
|
||||
| 23 | 一次性使用麻醉面罩 | `2003984` |
|
||||
| 24 | 一次性内窥镜护套 | `521-31-1` |
|
||||
| 25 | 一次性医用灭菌棉签 | `2237844/10183-1-29` |
|
||||
| 26 | 一次性无菌喉罩 | `7386-61-46` |
|
||||
| 27 | 医用凡士林敷料 | `10870-25-16` |
|
||||
| 28 | 医用纱布敷料 | `19246-3-14` |
|
||||
| 29 | 医用缝合针 | `583039/11207-1-64` |
|
||||
| 30 | 医用脱脂棉纱布块 | `8028-4-39` |
|
||||
| 31 | 可吸收性外科缝线 | `11765-1-101/1330-49-185` |
|
||||
| 32 | 密闭式防针刺伤型静脉留置针 | `1281-39-3` |
|
||||
| 33 | 导管固定器 | `1441340` |
|
||||
| 34 | 气管切开插管 | `10869-30-7` |
|
||||
| 35 | 结扎夹Ligating Clips | `14780-2-12` |
|
||||
| 36 | 自粘性薄膜敷料 | `1819-4-1` |
|
||||
| 37 | 血液净化装置的体外循环血路 | `739-2-1` |
|
||||
| 38 | 负压引流器 | `1518-20-8` |
|
||||
| 39 | 非吸收性外科缝线 | `4142-1-46` |
|
||||
| 40 | 非吸收性外科缝线(蚕丝线) | `654032` |
|
||||
|
||||
---
|
||||
|
||||
## 常用口语与正确写法
|
||||
|
||||
下列简称 **不在** 模型类名表中,直接传入会导致算法白名单失效(日志:`allowed_names 与模型类名无交集`)。
|
||||
|
||||
| 口语/错误写法 | 建议改用(类名或编码) |
|
||||
| --- | --- |
|
||||
| 纱布 | `医用纱布敷料`(`19246-3-14`)或 `医用脱脂棉纱布块`(`8028-4-39`) |
|
||||
| 缝线 | `可吸收性外科缝线`(`11765-1-101` 等)或 `非吸收性外科缝线`(`4142-1-46`)或 `非吸收性外科缝线(蚕丝线)`(`654032`) |
|
||||
| 止血钳 | 表中无「止血钳」类;若业务需要请确认是否应对应其它器械类或扩展训练标签 |
|
||||
| 垫单 | `一次性使用手术单`(`14764-2-4`);别名「一次性医用垫单」在服务端会归一化为该类名 |
|
||||
|
||||
---
|
||||
|
||||
## 开录请求示例
|
||||
|
||||
**按产品编码(推荐,与 HIS 编号一致)**
|
||||
|
||||
```json
|
||||
{
|
||||
"surgery_id": "123456",
|
||||
"camera_ids": ["or-cam-01", "or-cam-03"],
|
||||
"candidate_consumables": ["19246-3-14", "11765-1-101", "4142-1-46"]
|
||||
}
|
||||
```
|
||||
|
||||
**按完整类名**
|
||||
|
||||
```json
|
||||
{
|
||||
"surgery_id": "123456",
|
||||
"camera_ids": ["or-cam-01"],
|
||||
"candidate_consumables": ["医用纱布敷料", "可吸收性外科缝线", "非吸收性外科缝线"]
|
||||
}
|
||||
```
|
||||
|
||||
**医院导出对象(含消耗品编号)**
|
||||
|
||||
```json
|
||||
{
|
||||
"candidate_consumables": [
|
||||
{ "消耗品编号": "14764-2-4", "名称": "一次性使用手术单" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**不传候选(使用全部 41 类)**
|
||||
|
||||
```json
|
||||
{
|
||||
"surgery_id": "123456",
|
||||
"camera_ids": ["or-cam-03"],
|
||||
"candidate_consumables": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多规格编码说明
|
||||
|
||||
部分 `label_id` 含多个规格,以 `/` 分隔。任一段均可作为 `candidate_consumables` 传入,例如:
|
||||
|
||||
| 类名 | 完整 label_id | 可单独传入的编码示例 |
|
||||
| --- | --- | --- |
|
||||
| 一次性使用无菌注射器带针 | `1531-3-2/1531-3-1/1174-42-4/1174-42-1` | `1531-3-2`、`1174-42-1` |
|
||||
| 一次性使用导尿管 | `735592/14556-4-18` | `735592`、`14556-4-18` |
|
||||
| 可吸收性外科缝线 | `11765-1-101/1330-49-185` | `11765-1-101`、`1330-49-185` |
|
||||
| 一次性医用灭菌棉签 | `2237844/10183-1-29` | `2237844`、`10183-1-29` |
|
||||
| 医用缝合针 | `583039/11207-1-64` | `583039`、`11207-1-64` |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 开录与结果接口:`docs/客户端手术通信接口说明.md` §5.2
|
||||
- 候选清单行为说明:`docs/video-backends.md`
|
||||
Reference in New Issue
Block a user