Files
operating-room-monitor-server/app/schemas.py
Kevin 04866559db feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.

Made-with: Cursor
2026-04-21 18:33:54 +08:00

233 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class HealthResponse(BaseModel):
status: str
database: str
class SurgeryStartRequest(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"example": {
"surgery_id": "123456",
"camera_ids": ["or-cam-01", "or-cam-02"],
"candidate_consumables": ["纱布", "缝线", "止血钳"],
}
}
)
surgery_id: str = Field(
min_length=6,
max_length=6,
pattern=r"^\d{6}$",
description="手术6位号只允许6位数字。",
)
camera_ids: list[str] = Field(
min_length=1,
description="本次手术需要接入的摄像头 ID 列表。",
)
candidate_consumables: list[str] = Field(
default_factory=list,
description=(
"本次手术可能使用到的耗材清单。"
"服务端仅对该清单内的耗材做自动记账与待确认追问;"
"若为空则不会写入任何消耗(仅拉流推理)。"
),
)
class SurgeryEndRequest(BaseModel):
model_config = ConfigDict(
json_schema_extra={"example": {"surgery_id": "123456"}}
)
surgery_id: str = Field(
min_length=6,
max_length=6,
pattern=r"^\d{6}$",
description="手术6位号只允许6位数字。",
)
class SurgeryApiResponse(BaseModel):
surgery_id: str = Field(description="手术6位号。")
status: str = Field(description="接口处理状态。")
message: str = Field(description="返回说明。")
class SurgeryClientErrorDetail(BaseModel):
"""与 `HTTPException(detail={...})` 对应;最终 JSON 为 `{"detail": {...}}`。"""
code: str = Field(description="业务错误码,如 RECORDING_CANNOT_START、RECORDING_NOT_STOPPED、RESULT_NOT_READY。")
message: str = Field(description="人类可读说明。")
surgery_id: str = Field(description="手术 6 位号。")
class SurgeryClientErrorResponse(BaseModel):
"""FastAPI/Starlette 对 HTTPException 序列化后的常见外形(`detail` 为对象时)。"""
detail: SurgeryClientErrorDetail
class SurgeryConsumptionDetail(BaseModel):
"""单条消耗明细(按事件发生,可能多行)。"""
item_id: str = Field(description="物品 ID。")
item_name: str = Field(description="物品名称。")
quantity: int = Field(ge=0, description="本条记录对应的消耗数量。")
doctor_id: str = Field(description="医生 ID。")
timestamp: datetime = Field(description="记录时间ISO 8601")
source: str = Field(
default="vision",
description="记录来源vision 自动识别voice 语音确认。",
)
class SurgeryConsumptionSummary(BaseModel):
"""按物品汇总:该手术下该物品消耗数量合计。"""
item_id: str = Field(description="物品 ID。")
item_name: str = Field(description="物品名称。")
total_quantity: int = Field(ge=0, description="该物品在本台手术中的消耗数量合计。")
def build_consumption_summary(
details: list[SurgeryConsumptionDetail],
) -> list[SurgeryConsumptionSummary]:
"""按 item_id 汇总 total_quantity名称取该物品首条出现时的 item_name。"""
totals: dict[str, tuple[str, int]] = {}
for row in details:
if row.item_id not in totals:
totals[row.item_id] = (row.item_name, 0)
name, acc = totals[row.item_id]
totals[row.item_id] = (name, acc + row.quantity)
return [
SurgeryConsumptionSummary(
item_id=iid,
item_name=name,
total_quantity=qty,
)
for iid, (name, qty) in sorted(totals.items(), key=lambda x: x[0])
]
class SurgeryVoiceStatusResponse(BaseModel):
"""手术进行中人工确认(客户端播报)联调状态。"""
surgery_id: str = Field(description="手术 6 位号。")
voice_enabled: bool = Field(
description="是否启用了低置信度人工确认(客户端拉取待确认项)。",
)
pending_queue_approx: int = Field(
ge=0,
description="待医生确认的追问任务数量FIFO 队列长度)。",
)
last_prompt_snippet: str | None = Field(
default=None,
description="最近一次生成的待确认话术摘要。",
)
last_asr_text: str | None = Field(
default=None,
description="最近一次语音确认接口产生的 ASR 文本。",
)
last_error: str | None = Field(
default=None,
description="最近一次语音确认错误说明(如 ASR/解析失败)。",
)
class PendingConfirmationOption(BaseModel):
label: str
confidence: float
class SurgeryPendingConfirmationResponse(BaseModel):
"""当前待医生确认的一条低置信度识别。"""
surgery_id: str
confirmation_id: str
prompt_text: str = Field(description="可直接用于 TTS 播报的话术。")
options: list[PendingConfirmationOption]
model_top1_label: str = Field(description="模型原始 Top1 标签(可能不在候选清单内)。")
model_top1_confidence: float
created_at: datetime
class SurgeryPendingConfirmationResolveResponse(BaseModel):
surgery_id: str
confirmation_id: str
status: str = Field(description="accepted")
message: str
resolved_label: str | None = Field(
default=None,
description="解析并确认后的耗材名称;否认全部候选时为 null。",
)
rejected: bool = Field(
default=False,
description="是否为否认全部候选(不记消耗)。",
)
asr_text: str | None = Field(
default=None,
description="服务端语音识别得到的文本。",
)
audio_object_key: str | None = Field(
default=None,
description="MinIO 中原始 WAV 的对象键,用于追溯。",
)
class SurgeryResultResponse(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"example": {
"surgery_id": "123456",
"status": "completed",
"message": "结果查询成功。",
"details": [
{
"item_id": "HC001",
"item_name": "纱布",
"quantity": 2,
"doctor_id": "D1001",
"timestamp": "2026-04-21T10:30:00+08:00",
},
{
"item_id": "HC001",
"item_name": "纱布",
"quantity": 1,
"doctor_id": "D1002",
"timestamp": "2026-04-21T11:05:00+08:00",
},
{
"item_id": "HC002",
"item_name": "缝线",
"quantity": 1,
"doctor_id": "D1001",
"timestamp": "2026-04-21T10:45:00+08:00",
},
],
"summary": [
{"item_id": "HC001", "item_name": "纱布", "total_quantity": 3},
{"item_id": "HC002", "item_name": "缝线", "total_quantity": 1},
],
}
}
)
surgery_id: str = Field(description="手术6位号。")
status: str = Field(description="结果状态,例如 pending / completed / failed。")
message: str = Field(description="返回说明。")
details: list[SurgeryConsumptionDetail] = Field(
default_factory=list,
description="消耗明细行:每条含物品、数量、医生与时间;同一物品可多次出现。",
)
summary: list[SurgeryConsumptionSummary] = Field(
default_factory=list,
description="按物品汇总的消耗合计,应与 details 按 item_id 汇总一致。",
)