feat: 手术视频消耗、待确认与持久化改造
- 新增 Alembic 初始迁移、领域明细模型及归档持久化与重试链路\n- 拆分视频会话注册表、分类处理、推理时间窗聚合与流处理\n- 消耗日志:TSV/Markdown 含 top2/top3;item_id 优先产品编码;待确认记「待确认」行,语音确认后落正式行并更新汇总\n- 待确认时内存/DB 明细为占位行,确认后替换;拒绝时移除占位\n- 分类 probs 先 detach/cpu 再转 NumPy,修复 MPS/CUDA 上推理被静默跳过\n- 补充集成测试、归档与设备张量等单测 Made-with: Cursor
This commit is contained in:
209
app/config.py
209
app/config.py
@@ -6,6 +6,156 @@ from typing import Any, Literal
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class _SettingsGroup:
|
||||
"""按主题分组的 Settings 视图;属性访问代理回主 Settings 实例。
|
||||
|
||||
主 Settings 保留所有原始平坦字段作为事实来源;此类仅提供 ``settings.video.xxx``
|
||||
之类的分组读写入口,减少跨文件的耦合面,同时保持向后兼容。
|
||||
"""
|
||||
|
||||
_FIELDS: tuple[str, ...] = ()
|
||||
|
||||
def __init__(self, root: "Settings") -> None:
|
||||
object.__setattr__(self, "_root", root)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name not in self._FIELDS:
|
||||
raise AttributeError(
|
||||
f"{type(self).__name__} has no field '{name}'; "
|
||||
f"available: {self._FIELDS}"
|
||||
)
|
||||
return getattr(self._root, name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in self._FIELDS:
|
||||
setattr(self._root, name, value)
|
||||
else:
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
|
||||
class _VideoGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"video_default_backend",
|
||||
"video_camera_backend_overrides_json",
|
||||
"video_rtsp_url_template",
|
||||
"video_rtsp_urls_json",
|
||||
"video_rtsp_urls_json_file",
|
||||
"video_open_timeout_sec",
|
||||
"video_read_failure_reconnect_threshold",
|
||||
"video_reconnect_backoff_seconds",
|
||||
"video_inference_interval_sec",
|
||||
"video_inference_confidence_threshold",
|
||||
"video_auto_confirm_confidence",
|
||||
"video_voice_confirm_min_confidence",
|
||||
"video_voice_confirm_doctor_id",
|
||||
"video_detail_cooldown_sec",
|
||||
"video_jpeg_quality",
|
||||
"video_result_doctor_id",
|
||||
"video_log_inference_results",
|
||||
"consumable_classifier_weights",
|
||||
"consumable_classifier_imgsz",
|
||||
"consumable_classifier_device",
|
||||
"consumable_classifier_topk",
|
||||
"consumable_min_cls_confidence",
|
||||
"consumable_catalog_xlsx_path",
|
||||
"consumable_vision_window_sec",
|
||||
"hand_detection_weights",
|
||||
"hand_detection_imgsz",
|
||||
"hand_detection_conf",
|
||||
"hand_detection_pad_ratio",
|
||||
"hand_detection_min_crop_px",
|
||||
"hand_detection_device",
|
||||
"surgery_recording_max_attempts",
|
||||
"surgery_recording_retry_delay_seconds",
|
||||
"archive_persist_retry_interval_seconds",
|
||||
"archive_persist_max_retries",
|
||||
"archive_persist_backoff_cap_seconds",
|
||||
"archive_persist_durable_fallback_dir",
|
||||
"archive_persist_durable_fallback_enabled",
|
||||
"consumption_tsv_log_enabled",
|
||||
"consumption_tsv_log_path",
|
||||
"consumption_log_markdown_terminal",
|
||||
"consumption_log_timezone",
|
||||
)
|
||||
|
||||
|
||||
class _VoiceGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"voice_confirmation_enabled",
|
||||
"voice_upload_max_bytes",
|
||||
"voice_confirm_max_failed_parse_rounds",
|
||||
"voice_file_log_enabled",
|
||||
"voice_file_log_path",
|
||||
)
|
||||
|
||||
|
||||
class _HikvisionGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"hikvision_lib_dir",
|
||||
"hikvision_sdk_enabled",
|
||||
"hikvision_device_ip",
|
||||
"hikvision_device_port",
|
||||
"hikvision_user",
|
||||
"hikvision_password",
|
||||
"hikvision_channel",
|
||||
"hikvision_preview_rtsp_template",
|
||||
"hikvision_camera_rtsp_urls_json",
|
||||
"hikvision_sdk_fallback_to_rtsp",
|
||||
)
|
||||
|
||||
|
||||
class _MinioGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"minio_endpoint",
|
||||
"minio_access_key",
|
||||
"minio_secret_key",
|
||||
"minio_bucket",
|
||||
"minio_secure",
|
||||
"minio_region",
|
||||
)
|
||||
|
||||
|
||||
class _BaiduGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"baidu_speech_app_id",
|
||||
"baidu_speech_api_key",
|
||||
"baidu_speech_secret_key",
|
||||
"baidu_speech_connection_timeout_ms",
|
||||
"baidu_speech_socket_timeout_ms",
|
||||
"baidu_speech_asr_dev_pid",
|
||||
)
|
||||
|
||||
|
||||
class _DemoGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"demo_cors_enabled",
|
||||
"demo_cors_origins",
|
||||
"demo_orchestrator_enabled",
|
||||
"demo_orchestrator_rtsp_port",
|
||||
"demo_orchestrator_rtsp_json_host",
|
||||
)
|
||||
|
||||
|
||||
class _DatabaseGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"database_url",
|
||||
"postgres_user",
|
||||
"postgres_password",
|
||||
"postgres_db",
|
||||
"postgres_host",
|
||||
"postgres_port",
|
||||
"auto_create_schema",
|
||||
)
|
||||
|
||||
|
||||
class _ServerGroup(_SettingsGroup):
|
||||
_FIELDS = (
|
||||
"server_host",
|
||||
"server_port",
|
||||
"server_reload",
|
||||
)
|
||||
|
||||
_PACKAGE_DIR = Path(__file__).resolve().parent
|
||||
# 仓库根目录(含 .env)。用绝对路径读 .env,避免从子目录/IDE 启动时 cwd 不同导致联调项未生效。
|
||||
_REPO_ROOT = _PACKAGE_DIR.parent
|
||||
@@ -31,6 +181,17 @@ class Settings(BaseSettings):
|
||||
postgres_db: str = "operation_room"
|
||||
postgres_host: str = "localhost"
|
||||
postgres_port: int = 35432
|
||||
#: 为 true 时,lifespan 启动会调用 Base.metadata.create_all 确保表存在(开发/测试用)。
|
||||
#: 生产请置 false,并通过 ``alembic upgrade head`` 进行版本化迁移。
|
||||
auto_create_schema: bool = True
|
||||
|
||||
# --- Uvicorn / API server ---
|
||||
#: `uvicorn.run` 绑定的地址;默认监听所有接口(开发/容器联调常用)。
|
||||
server_host: str = "0.0.0.0"
|
||||
#: HTTP 端口;生产请按部署策略显式设置。
|
||||
server_port: int = Field(default=38080, ge=1, le=65535)
|
||||
#: 是否启用 `--reload`(仅本地开发;生产必须 false)。
|
||||
server_reload: bool = False
|
||||
consumable_classifier_weights: str | None = None
|
||||
consumable_classifier_imgsz: int = 224
|
||||
#: Explicit Ultralytics device (e.g. cpu, mps, cuda:0). Empty -> macOS prefers MPS; Linux prefers CUDA if available.
|
||||
@@ -85,14 +246,20 @@ class Settings(BaseSettings):
|
||||
voice_confirmation_enabled: bool = True
|
||||
#: 语音确认记帐时的 doctor_id。
|
||||
video_voice_confirm_doctor_id: str = "voice"
|
||||
#: (已弃用)服务端本机录音秒数;当前闭环由客户端采集语音,此项仅保留兼容旧配置。
|
||||
voice_record_seconds: float = Field(default=5.0, ge=1.0, le=30.0)
|
||||
#: (已弃用)服务端 ffmpeg 音频输入;当前闭环不依赖服务端录音。
|
||||
voice_ffmpeg_input: str = ""
|
||||
#: 手术结束后归档写库失败时,后台重试落库的间隔(秒)。
|
||||
#: 手术结束后归档写库失败时,后台重试落库的间隔(秒),用作指数退避的基数。
|
||||
archive_persist_retry_interval_seconds: float = Field(
|
||||
default=30.0, ge=5.0, le=3600.0
|
||||
)
|
||||
#: 单条归档允许的最大连续重试次数。达到上限后保持 durable fallback,直到进程重启或手动介入。
|
||||
archive_persist_max_retries: int = Field(default=12, ge=1, le=10000)
|
||||
#: 指数退避上限(秒),防止间隔被放大到不切实际的值。
|
||||
archive_persist_backoff_cap_seconds: float = Field(
|
||||
default=900.0, ge=5.0, le=86400.0
|
||||
)
|
||||
#: 归档 durable fallback 的磁盘目录;启动/重试时会扫描其中 `*.json` 尝试恢复。
|
||||
archive_persist_durable_fallback_dir: str = "logs/pending_archive"
|
||||
#: 为 true 时,首次写库失败后立即把归档写到 durable fallback 目录,避免进程重启丢数据。
|
||||
archive_persist_durable_fallback_enabled: bool = True
|
||||
#: 同一物品重复记一条消耗的最短间隔(秒)。
|
||||
video_detail_cooldown_sec: float = Field(default=15.0, ge=0.0, le=3600.0)
|
||||
#: 送模型 JPEG 质量。
|
||||
@@ -280,5 +447,37 @@ class Settings(BaseSettings):
|
||||
"""仓库内示例映射路径(供文档与联调引用)。"""
|
||||
return _default_camera_rtsp_urls_sample_path()
|
||||
|
||||
@property
|
||||
def video(self) -> _VideoGroup:
|
||||
return _VideoGroup(self)
|
||||
|
||||
@property
|
||||
def voice(self) -> _VoiceGroup:
|
||||
return _VoiceGroup(self)
|
||||
|
||||
@property
|
||||
def hikvision(self) -> _HikvisionGroup:
|
||||
return _HikvisionGroup(self)
|
||||
|
||||
@property
|
||||
def minio(self) -> _MinioGroup:
|
||||
return _MinioGroup(self)
|
||||
|
||||
@property
|
||||
def baidu(self) -> _BaiduGroup:
|
||||
return _BaiduGroup(self)
|
||||
|
||||
@property
|
||||
def demo(self) -> _DemoGroup:
|
||||
return _DemoGroup(self)
|
||||
|
||||
@property
|
||||
def database(self) -> _DatabaseGroup:
|
||||
return _DatabaseGroup(self)
|
||||
|
||||
@property
|
||||
def server(self) -> _ServerGroup:
|
||||
return _ServerGroup(self)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
Reference in New Issue
Block a user