Files
operating-room-monitor-server/app/schemas.py
Kevin 557fcee803 Update consumable pipeline, client API docs, and deployment config
- Refine effective candidate consumables and classifier labels
- Adjust vision algorithm, TSV logging, and video session wiring
- Refresh client surgery HTTP contract doc and staging/video docs
- Update settings, docker-compose prod, tests, and uv.lock

Made-with: Cursor
2026-04-24 11:05:17 +08:00

251 lines
8.6 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=(
"本次手术可能使用到的耗材子集(可选)。"
"非空时仅对该清单内名称做自动记账与待确认追问。"
"缺省或空数组时,使用 consumable_classifier_labels.yaml 中全部类名;"
"无有效 yaml 则使用分类模型全部类名。"
),
)
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=(
"业务物品标识:来自 consumable_classifier_labels.yaml 的 label_id"
"类名经归一化后匹配,未在 yaml 中配置 label_id 时与模型类名相同。"
),
)
item_name: str = Field(description="物品名称(分类或确认后的展示名)。")
qty: int = Field(
ge=0,
description="本条记录对应的消耗数量;当前一次识别或一次人工确认仅追加一条明细,因此固定为 1。",
)
doctor_id: str = Field(description="医生 ID。")
timestamp: datetime = Field(description="记录时间ISO 8601date-time")
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": 1,
"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": 2},
{"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 一致。"
),
)