Files
operating-room-monitor-server/app/schemas.py
Kevin 69980d8073 feat: align surgery API with schemas and extend client tooling
- Refactor app API and schemas; adjust surgery pipeline, repository, and session manager.

- Improve consumption TSV logging and consumable vision integration; trim voice resolution.

- Add Baidu Face 1:N search script, .env.example entries, and client API integration doc.

- Update demo client, staging checklist, surgery interface doc, and related tests; add sample face image.

Made-with: Cursor
2026-04-23 16:09:20 +08:00

269 lines
9.0 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 dataclasses import dataclass
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):
"""单条消耗明细HTTP 与 OpenAPI按事件发生可能多行
JSON 字段顺序item_id → item_name → qty → doctor_id → timestamp。
(可选落盘耗材 TSV 明细列与此一致item_id、item_name、qty、doctor_id、timestamp。
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"item_id": "HC001",
"item_name": "纱布",
"qty": 1,
"doctor_id": "D1001",
"timestamp": "2026-04-21T10:30:00+08:00",
}
}
)
item_id: str = Field(
description=(
"业务物品标识:优先为耗材目录中的产品编码;"
"目录键经名称归一化后与分类类名匹配,未命中目录时与模型输出类名一致。"
),
)
item_name: str = Field(description="物品名称(分类或确认后的展示名)。")
qty: int = Field(ge=0, description="本条记录对应的消耗数量。")
doctor_id: str = Field(description="医生 ID。")
timestamp: datetime = Field(description="记录时间ISO 8601date-time")
@dataclass
class SurgeryConsumptionStored:
"""内存 / 数据库持久化用的明细行(含 source仅服务端内部使用不随 HTTP 返回)。"""
item_id: str
item_name: str
qty: int
doctor_id: str
timestamp: datetime
source: str = "vision"
def as_response(self) -> SurgeryConsumptionDetail:
return SurgeryConsumptionDetail(
item_id=self.item_id,
item_name=self.item_name,
qty=self.qty,
doctor_id=self.doctor_id,
timestamp=self.timestamp,
)
class SurgeryConsumptionSummary(BaseModel):
"""按物品汇总该手术下该物品消耗数量合计item_id、item_name、total_quantity"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"item_id": "HC001",
"item_name": "纱布",
"total_quantity": 3,
}
}
)
item_id: str = Field(description="物品 ID与明细中 item_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.qty)
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 PendingConfirmationOption(BaseModel):
label: str
confidence: float
class SurgeryPendingConfirmationResponse(BaseModel):
"""当前待医生确认的一条低置信度识别。"""
surgery_id: str
confirmation_id: str
prompt_text: str = Field(description="可直接用于展示或无障碍朗读的话术(与 MP3 内容一致)。")
prompt_audio_mp3_base64: str = Field(
description=(
"与 prompt_text 对应的百度 TTS 音频MP3的标准 Base64 字符串(无换行);"
"客户端解码为二进制后以 audio/mpeg 播放。"
),
)
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": "纱布",
"qty": 2,
"doctor_id": "D1001",
"timestamp": "2026-04-21T10:30:00+08:00",
},
{
"item_id": "HC001",
"item_name": "纱布",
"qty": 1,
"doctor_id": "D1002",
"timestamp": "2026-04-21T11:05:00+08:00",
},
{
"item_id": "HC002",
"item_name": "缝线",
"qty": 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=(
"消耗明细多行。每行字段顺序item_id、item_name、qty、doctor_id、timestamp"
"同一 item_id 可多次出现。"
),
)
summary: list[SurgeryConsumptionSummary] = Field(
default_factory=list,
description=(
"按 item_id 汇总的合计表(仅 item_id、item_name、total_quantity"
"应与 details 按 item_id 汇总 qty 一致。"
),
)