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
This commit is contained in:
176
app/schemas.py
176
app/schemas.py
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
@@ -75,24 +76,72 @@ class SurgeryClientErrorResponse(BaseModel):
|
||||
|
||||
|
||||
class SurgeryConsumptionDetail(BaseModel):
|
||||
"""单条消耗明细(按事件发生,可能多行)。"""
|
||||
"""单条消耗明细(HTTP 与 OpenAPI;按事件发生,可能多行)。
|
||||
|
||||
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 语音确认。",
|
||||
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 8601,date-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)。"""
|
||||
|
||||
item_id: str = Field(description="物品 ID。")
|
||||
item_name: str = Field(description="物品名称。")
|
||||
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="该物品在本台手术中的消耗数量合计。")
|
||||
|
||||
|
||||
@@ -105,7 +154,7 @@ def build_consumption_summary(
|
||||
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)
|
||||
totals[row.item_id] = (name, acc + row.qty)
|
||||
return [
|
||||
SurgeryConsumptionSummary(
|
||||
item_id=iid,
|
||||
@@ -116,73 +165,6 @@ def build_consumption_summary(
|
||||
]
|
||||
|
||||
|
||||
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 SurgeryVoiceAuditItem(BaseModel):
|
||||
"""单条 `voice_confirmation_audits` 行(追溯对账用)。"""
|
||||
|
||||
id: int
|
||||
confirmation_id: str
|
||||
status: str = Field(
|
||||
description=(
|
||||
"recognized / rejected / parse_failed / asr_failed / invalid_audio / "
|
||||
"upload_failed / client_stt_empty / client_stt_parse_failed 等"
|
||||
),
|
||||
)
|
||||
audio_object_key: str | None = None
|
||||
audio_content_type: str | None = None
|
||||
audio_size_bytes: int | None = None
|
||||
audio_sha256: str | None = None
|
||||
asr_text: str | None = None
|
||||
resolved_label: str | None = None
|
||||
options_snapshot_json: str | None = Field(
|
||||
default=None,
|
||||
description="当次候选项与置信度 JSON 快照。",
|
||||
)
|
||||
error_message: str | None = None
|
||||
created_at: datetime = Field(
|
||||
description="记录写入时间(UTC)。",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class SurgeryVoiceAuditsListResponse(BaseModel):
|
||||
"""按手术号分页的语音确认审计列表。"""
|
||||
|
||||
surgery_id: str
|
||||
total: int = Field(ge=0, description="该手术在表中的总条数(不受本页 limit 截断)。")
|
||||
limit: int = Field(ge=1, le=200)
|
||||
offset: int = Field(ge=0)
|
||||
items: list[SurgeryVoiceAuditItem] = Field(
|
||||
default_factory=list,
|
||||
description="按 `created_at` 降序。",
|
||||
)
|
||||
|
||||
|
||||
class PendingConfirmationOption(BaseModel):
|
||||
label: str
|
||||
confidence: float
|
||||
@@ -193,23 +175,19 @@ class SurgeryPendingConfirmationResponse(BaseModel):
|
||||
|
||||
surgery_id: str
|
||||
confirmation_id: str
|
||||
prompt_text: str = Field(description="可直接用于 TTS 播报的话术。")
|
||||
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 SurgeryPendingResolveTextRequest(BaseModel):
|
||||
"""由浏览器 Web Speech 等客户端本地识别后提交的文本,语义与经百度 ASR 得到的文本相同。"""
|
||||
|
||||
recognized_text: str = Field(
|
||||
min_length=1,
|
||||
max_length=2000,
|
||||
description="识别文本;服务端用与语音接口相同的规则解析候选项。",
|
||||
)
|
||||
|
||||
|
||||
class SurgeryPendingConfirmationResolveResponse(BaseModel):
|
||||
surgery_id: str
|
||||
confirmation_id: str
|
||||
@@ -244,21 +222,21 @@ class SurgeryResultResponse(BaseModel):
|
||||
{
|
||||
"item_id": "HC001",
|
||||
"item_name": "纱布",
|
||||
"quantity": 2,
|
||||
"qty": 2,
|
||||
"doctor_id": "D1001",
|
||||
"timestamp": "2026-04-21T10:30:00+08:00",
|
||||
},
|
||||
{
|
||||
"item_id": "HC001",
|
||||
"item_name": "纱布",
|
||||
"quantity": 1,
|
||||
"qty": 1,
|
||||
"doctor_id": "D1002",
|
||||
"timestamp": "2026-04-21T11:05:00+08:00",
|
||||
},
|
||||
{
|
||||
"item_id": "HC002",
|
||||
"item_name": "缝线",
|
||||
"quantity": 1,
|
||||
"qty": 1,
|
||||
"doctor_id": "D1001",
|
||||
"timestamp": "2026-04-21T10:45:00+08:00",
|
||||
},
|
||||
@@ -276,9 +254,15 @@ class SurgeryResultResponse(BaseModel):
|
||||
message: str = Field(description="返回说明。")
|
||||
details: list[SurgeryConsumptionDetail] = Field(
|
||||
default_factory=list,
|
||||
description="消耗明细行:每条含物品、数量、医生与时间;同一物品可多次出现。",
|
||||
description=(
|
||||
"消耗明细(多行)。每行字段顺序:item_id、item_name、qty、doctor_id、timestamp;"
|
||||
"同一 item_id 可多次出现。"
|
||||
),
|
||||
)
|
||||
summary: list[SurgeryConsumptionSummary] = Field(
|
||||
default_factory=list,
|
||||
description="按物品汇总的消耗合计,应与 details 按 item_id 汇总一致。",
|
||||
description=(
|
||||
"按 item_id 汇总的合计表(仅 item_id、item_name、total_quantity),"
|
||||
"应与 details 按 item_id 汇总 qty 一致。"
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user