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:
@@ -12,7 +12,7 @@ from loguru import logger
|
||||
from app.config import Settings
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.repositories.surgery_results import SurgeryResultRepository
|
||||
from app.schemas import SurgeryConsumptionDetail
|
||||
from app.schemas import SurgeryConsumptionDetail, SurgeryConsumptionStored
|
||||
from app.services.consumable_vision_algorithm import (
|
||||
ClsTop3,
|
||||
ConsumableVisionAlgorithmService,
|
||||
@@ -26,7 +26,12 @@ from app.services.video.backend_resolver import BackendResolver
|
||||
from app.services.video.hikvision_runtime import HikvisionInitRefCount, HikvisionRuntime
|
||||
from app.services.video.rtsp_capture import RtspCapture
|
||||
from app.services.video.types import VideoBackendKind
|
||||
from app.services.consumption_tsv_log import append_consumption_window, init_consumption_log_file
|
||||
from app.services.consumption_tsv_log import (
|
||||
append_consumption_log_summary,
|
||||
append_consumption_window,
|
||||
init_consumption_log_file,
|
||||
print_consumption_summary_markdown,
|
||||
)
|
||||
from app.services.voice_file_log import init_voice_log_file
|
||||
from app.services.voice_confirm import build_prompt_text
|
||||
from app.surgery_errors import SurgeryPipelineError
|
||||
@@ -64,7 +69,7 @@ class SurgerySessionState:
|
||||
#: 分类类名(归一化) -> 业务物品 id(Excel 产品编码或名称)。
|
||||
name_to_code: dict[str, str] = field(default_factory=dict)
|
||||
camera_infer: dict[str, CameraStreamInferState] = field(default_factory=dict)
|
||||
details: list[SurgeryConsumptionDetail] = field(default_factory=list)
|
||||
details: list[SurgeryConsumptionStored] = field(default_factory=list)
|
||||
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
ready: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
last_detail_monotonic: dict[str, float] = field(default_factory=dict)
|
||||
@@ -76,6 +81,8 @@ class SurgerySessionState:
|
||||
last_asr_text: str | None = None
|
||||
#: 最近一次语音确认错误说明(ASR/解析失败等)。
|
||||
last_voice_error: str | None = None
|
||||
#: 视觉时间窗落盘用量累计,供停录时写汇总(item_id -> 首次名称, 次数)。
|
||||
consumption_log_totals: dict[str, tuple[str, int]] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -87,7 +94,7 @@ class RunningSurgery:
|
||||
|
||||
@dataclass
|
||||
class ArchivedSurgery:
|
||||
details: list[SurgeryConsumptionDetail]
|
||||
details: list[SurgeryConsumptionStored]
|
||||
|
||||
|
||||
def _rank_topk_for_candidates(
|
||||
@@ -293,7 +300,7 @@ class CameraSessionManager:
|
||||
async def _persist_archived_details(
|
||||
self,
|
||||
surgery_id: str,
|
||||
details: list[SurgeryConsumptionDetail],
|
||||
details: list[SurgeryConsumptionStored],
|
||||
) -> bool:
|
||||
if self._repo is None:
|
||||
return True
|
||||
@@ -331,6 +338,10 @@ class CameraSessionManager:
|
||||
if isinstance(res, BaseException):
|
||||
logger.warning("surgery task finished with error: {}", res)
|
||||
|
||||
totals = dict(run.state.consumption_log_totals)
|
||||
append_consumption_log_summary(surgery_id, totals)
|
||||
print_consumption_summary_markdown(totals)
|
||||
|
||||
details = list(run.state.details)
|
||||
|
||||
persisted = False
|
||||
@@ -364,26 +375,13 @@ class CameraSessionManager:
|
||||
rows = list(self._active[surgery_id].state.details)
|
||||
if not rows:
|
||||
return None
|
||||
return rows
|
||||
return [r.as_response() for r in rows]
|
||||
|
||||
def archived_consumption_fallback(self, surgery_id: str) -> list[SurgeryConsumptionDetail] | None:
|
||||
arch = self._archive.get(surgery_id)
|
||||
if arch is None:
|
||||
return None
|
||||
return list(arch.details)
|
||||
|
||||
def voice_status(self, surgery_id: str) -> dict[str, object] | None:
|
||||
if surgery_id not in self._active:
|
||||
return None
|
||||
st = self._active[surgery_id].state
|
||||
return {
|
||||
"surgery_id": surgery_id,
|
||||
"voice_enabled": bool(self._s.voice_confirmation_enabled),
|
||||
"pending_queue_approx": len(st.pending_fifo),
|
||||
"last_prompt_snippet": st.last_pending_prompt_snippet,
|
||||
"last_asr_text": st.last_asr_text,
|
||||
"last_error": st.last_voice_error,
|
||||
}
|
||||
return [r.as_response() for r in arch.details]
|
||||
|
||||
def record_voice_trace(
|
||||
self,
|
||||
@@ -525,10 +523,10 @@ class CameraSessionManager:
|
||||
return
|
||||
state.last_detail_monotonic[item_id] = now_m
|
||||
state.details.append(
|
||||
SurgeryConsumptionDetail(
|
||||
SurgeryConsumptionStored(
|
||||
item_id=item_id,
|
||||
item_name=item_name,
|
||||
quantity=1,
|
||||
qty=1,
|
||||
doctor_id=doctor_id,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
source=source,
|
||||
@@ -698,6 +696,7 @@ class CameraSessionManager:
|
||||
camera_id=camera_id,
|
||||
wall_start_epoch=wall_lo,
|
||||
wall_end_epoch=wall_hi,
|
||||
running_totals=state.consumption_log_totals,
|
||||
)
|
||||
pending_preds.append(
|
||||
cls_top3_to_prediction_result(best)
|
||||
|
||||
Reference in New Issue
Block a user