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
This commit is contained in:
3
app/repositories/__init__.py
Normal file
3
app/repositories/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from app.repositories.surgery_results import SurgeryResultRepository
|
||||
|
||||
__all__ = ["SurgeryResultRepository"]
|
||||
73
app/repositories/surgery_results.py
Normal file
73
app/repositories/surgery_results.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import SurgeryFinalResult, SurgeryResultDetailRow
|
||||
from app.schemas import SurgeryConsumptionDetail
|
||||
|
||||
|
||||
class SurgeryResultRepository:
|
||||
"""持久化 / 读取手术结束后的最终结果(仅客户端返回结构)。"""
|
||||
|
||||
async def save_final_result(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
surgery_id: str,
|
||||
details: list[SurgeryConsumptionDetail],
|
||||
completed_at: datetime | None = None,
|
||||
) -> None:
|
||||
when = completed_at or datetime.now(timezone.utc)
|
||||
await session.execute(
|
||||
delete(SurgeryResultDetailRow).where(
|
||||
SurgeryResultDetailRow.surgery_id == surgery_id
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
delete(SurgeryFinalResult).where(SurgeryFinalResult.surgery_id == surgery_id)
|
||||
)
|
||||
row = SurgeryFinalResult(surgery_id=surgery_id, completed_at=when)
|
||||
session.add(row)
|
||||
for d in details:
|
||||
session.add(
|
||||
SurgeryResultDetailRow(
|
||||
surgery_id=surgery_id,
|
||||
item_id=d.item_id,
|
||||
item_name=d.item_name,
|
||||
quantity=d.quantity,
|
||||
doctor_id=d.doctor_id,
|
||||
recorded_at=d.timestamp,
|
||||
source=d.source,
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
async def load_final_details(
|
||||
self, session: AsyncSession, surgery_id: str
|
||||
) -> list[SurgeryConsumptionDetail] | None:
|
||||
res = await session.execute(
|
||||
select(SurgeryFinalResult).where(SurgeryFinalResult.surgery_id == surgery_id)
|
||||
)
|
||||
meta = res.scalar_one_or_none()
|
||||
if meta is None:
|
||||
return None
|
||||
q = await session.execute(
|
||||
select(SurgeryResultDetailRow)
|
||||
.where(SurgeryResultDetailRow.surgery_id == surgery_id)
|
||||
.order_by(SurgeryResultDetailRow.id)
|
||||
)
|
||||
rows = q.scalars().all()
|
||||
return [
|
||||
SurgeryConsumptionDetail(
|
||||
item_id=r.item_id,
|
||||
item_name=r.item_name,
|
||||
quantity=r.quantity,
|
||||
doctor_id=r.doctor_id,
|
||||
timestamp=r.recorded_at,
|
||||
source=r.source,
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
46
app/repositories/voice_audits.py
Normal file
46
app/repositories/voice_audits.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import VoiceConfirmationAudit
|
||||
|
||||
|
||||
class VoiceAuditRepository:
|
||||
"""Persist voice confirmation audit rows."""
|
||||
|
||||
async def save_audit(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
surgery_id: str,
|
||||
confirmation_id: str,
|
||||
status: str,
|
||||
audio_object_key: str | None,
|
||||
audio_content_type: str | None,
|
||||
audio_size_bytes: int | None,
|
||||
audio_sha256: str | None,
|
||||
asr_text: str | None,
|
||||
resolved_label: str | None,
|
||||
options_snapshot_json: str | None,
|
||||
error_message: str | None,
|
||||
created_at: datetime | None = None,
|
||||
) -> None:
|
||||
when = created_at or datetime.now(timezone.utc)
|
||||
row = VoiceConfirmationAudit(
|
||||
surgery_id=surgery_id,
|
||||
confirmation_id=confirmation_id,
|
||||
status=status,
|
||||
audio_object_key=audio_object_key,
|
||||
audio_content_type=audio_content_type,
|
||||
audio_size_bytes=audio_size_bytes,
|
||||
audio_sha256=audio_sha256,
|
||||
asr_text=asr_text,
|
||||
resolved_label=resolved_label,
|
||||
options_snapshot_json=options_snapshot_json,
|
||||
error_message=error_message,
|
||||
created_at=when,
|
||||
)
|
||||
session.add(row)
|
||||
await session.flush()
|
||||
Reference in New Issue
Block a user