feat: 语音确认、联调与运维增强

- 语音:序数解析(第一个/第二个等)、解析失败计数与 API detail.retry_remaining;
  百度 ASR 固定 dev_pid 为普通话;SurgeryPipelineError 支持 extra 并入 HTTP detail。
- Demo:demo 路由与假 RTSP、客户端 index 与 README;BackendResolver 与配置调整。
- 可观测:消耗 TSV 日志、语音文件日志、终端 Markdown 辅助;相关测试与依赖更新。
- 注意:.env 仍被 gitignore,本地密钥不会进入本提交。

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-23 14:24:20 +08:00
parent 42720f81cf
commit 0c05463617
39 changed files with 3030 additions and 143 deletions

View File

@@ -7,6 +7,9 @@ from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
_PACKAGE_DIR = Path(__file__).resolve().parent
# 仓库根目录(含 .env。用绝对路径读 .env避免从子目录/IDE 启动时 cwd 不同导致联调项未生效。
_REPO_ROOT = _PACKAGE_DIR.parent
_DEFAULT_ENV_FILE = _REPO_ROOT / ".env"
def _default_consumable_classifier_weights() -> str:
@@ -74,9 +77,9 @@ class Settings(BaseSettings):
video_inference_confidence_threshold: float = Field(
default=0.35, ge=0.0, le=1.0
)
#: 达到或超过该置信度时,自动记一条耗材消耗(需通过候选清单校验)
video_auto_confirm_confidence: float = Field(default=0.55, ge=0.0, le=1.0)
#: 置信度处于 [本值, video_auto_confirm_confidence) 时尝试语音追问(需有可播报的 top 候选)。
#: 达到或超过该置信度且 Top1 在候选内时,自动记一条 vision 消耗;低于该值时走待确认(不低于 video_voice_confirm_min 且可展示候选项时)。默认 0.9:不足 0.9 的需人工确认
video_auto_confirm_confidence: float = Field(default=0.9, ge=0.0, le=1.0)
#: 低于本值的帧不进入自动/待确认逻辑(与 `video_auto_confirm_confidence` 下沿之间的区间可入队待确认)。
video_voice_confirm_min_confidence: float = Field(default=0.35, ge=0.0, le=1.0)
#: 是否启用低置信度时的人工确认(客户端拉取待确认项并回传结果;不依赖服务端麦克风/扬声器)。
voice_confirmation_enabled: bool = True
@@ -96,6 +99,21 @@ class Settings(BaseSettings):
video_jpeg_quality: int = Field(default=85, ge=40, le=100)
#: 写入消耗明细时的 doctor_id无外部医生 ID 来源时的占位)。
video_result_doctor_id: str = "vision"
#: 为 true 时,每次单帧分类得到 top1 等结果会打一条 INFO 日志(联调用;高流量时建议关)。
video_log_inference_results: bool = False
#: 为 true 时,将时间窗级识别写入文本日志(`start_surgery` 时按手术截断/初始化窗内结果追加Top2/3 仅名称;数量恒 1
consumption_tsv_log_enabled: bool = True
#: 路径模板,须含 `{surgery_id}`(每例手术独立文件)。不含占位时自动在扩展名前追加 `_<surgery_id>`。
consumption_tsv_log_path: str = "logs/consumption_{surgery_id}.txt"
#: 为 true 时,同一时间窗结果在终端以 Markdown 表格打印Top13 分列 id / 名称 / 置信度)。
consumption_log_markdown_terminal: bool = True
#: 消耗日志「时间戳」列的时区IANA 名如 `Asia/Shanghai`;空串则使用「当前系统时区」。
consumption_log_timezone: str = ""
#: 为 true 时语音确认WAV/文本)的 ASR/解析结果写 TSV 文件,并在终端打 `VoiceConfirm` 行;`start_surgery` 时与消耗日志同寿命截断初始化。
voice_file_log_enabled: bool = True
#: 路径模板,须含 `{surgery_id}`,与 `consumption_tsv_log_path` 规则相同。
voice_file_log_path: str = "logs/voice_{surgery_id}.txt"
#: 海康 SDK `.so` 所在目录(容器内可挂载 `/opt/hikvision/lib`)。
hikvision_lib_dir: str = "/opt/hikvision/lib"
@@ -121,6 +139,8 @@ class Settings(BaseSettings):
baidu_speech_connection_timeout_ms: int | None = None
#: 传输数据超时(毫秒)。未设置则使用 SDK 默认。
baidu_speech_socket_timeout_ms: int | None = None
#: 百度短语音识别 `dev_pid`**始终**用于 ASR调用方传入的 options 不会覆盖。1537=普通话通用(与百度控制台一致;勿用 1737 英语、1837 粤语等)。
baidu_speech_asr_dev_pid: int = Field(default=1537, ge=1000, le=99999)
# --- MinIO语音确认原始 WAV 追溯存储 ---
#: 为空则视为未配置 MinIO语音确认接口将返回业务错误联调需配置
@@ -134,6 +154,8 @@ class Settings(BaseSettings):
minio_region: str = ""
#: 上传医生语音 WAV 的最大字节数(默认 10MB
voice_upload_max_bytes: int = Field(default=10 * 1024 * 1024, ge=64, le=50 * 1024 * 1024)
#: 同一条待确认在 ASR/文本解析为选项或耗材名失败时,计数的最大失败轮数。默认 2 表示首败后再允许 1 次「显式重试」语义API 的 retry_remaining 首轮为 1、再败为 0不阻止后续继续上传直至成功或否认。
voice_confirm_max_failed_parse_rounds: int = Field(default=2, ge=1, le=20)
# --- Demo 客户端跨源(仅用于 scripts/demo_client 联调;生产置 false ---
#: 为 true 时挂载 CORSMiddleware便于浏览器 demo 从另一个端口访问本服务。
@@ -141,6 +163,15 @@ class Settings(BaseSettings):
#: 逗号分隔的允许来源;`*` 表示允许全部来源demo/联调用,生产应显式指定)。
demo_cors_origins: str = "*"
# --- 一键联调:上传视频 → 起假 RTSP → 写 VIDEO_RTSP_URLS_JSON_FILE → 开始手术(仅开发;生产必须 false ---
#: 为 true 时注册 `POST /internal/demo/orchestrate-and-start`。
demo_orchestrator_enabled: bool = False
#: 假 RTSPMediaMTX在宿主机上映射的端口与 scripts/demo_client 默认一致)。
demo_orchestrator_rtsp_port: int = Field(default=18554, ge=1, le=65535)
#: 手配假流时:写入 JSON 可把 `rtsp://127.0.0.1` 换成此主机,便于**别一进程**(如仅容器内的监控)访问宿主机推流。
#: `POST /internal/demo/orchestrate-and-start` 在本进程起流+拉流,始终写 `127.0.0.1`**不读**此字段。
demo_orchestrator_rtsp_json_host: str = "host.docker.internal"
def parsed_demo_cors_origins(self) -> list[str]:
raw = (self.demo_cors_origins or "").strip()
if not raw:
@@ -157,7 +188,7 @@ class Settings(BaseSettings):
return str(value)
model_config = SettingsConfigDict(
env_file=".env",
env_file=(str(_DEFAULT_ENV_FILE),),
env_file_encoding="utf-8",
extra="ignore",
)