Files
operating-room-monitor-server/app/config.py
Kevin 8a4bad99d3 feat: 配置写死与 baked 模块,Alembic 建表,百度仅 BAIDU_*
- 新增 app/baked/algorithm|pipeline,非部署参数不再走 env;Settings 保留 DB/HTTP/RTSP/海康/百度/MinIO/Demo
- 移除 init_db_schema 与 reload 配置;main 仅 check_database;start*.sh 在 uvicorn 前执行 alembic upgrade head
- 依赖 psycopg[binary] 供 Alembic 同步 URL;alembic/env 注释与预发清单更新
- 撕段门控消费管线、各视频/语音/归档调用改为 baked
- 百度环境变量仅 BAIDU_APP_ID、BAIDU_API_KEY、BAIDU_SECRET_KEY 与 BAIDU_* 超时/ASR;人脸脚本与 baidu_speech 文案同步
- 全量单测与 .env.example 更新;.gitignore 忽略 refs/(本地权重/视频不入库)

Made-with: Cursor
2026-04-24 15:33:22 +08:00

313 lines
9.3 KiB
Python

import json
from pathlib import Path
from urllib.parse import quote_plus
from typing import Any, Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.baked import algorithm as baked_algorithm
class _SettingsGroup:
"""按主题分组的 Settings 视图;属性访问代理回主 Settings 实例。"""
_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):
"""仅含 RTSP 解析与按路后端;抽帧/推理/耗材等见 app.baked.pipeline / algorithm。"""
_FIELDS = (
"video_default_backend",
"video_camera_backend_overrides_json",
"video_rtsp_url_template",
"video_rtsp_urls_json",
"video_rtsp_urls_json_file",
)
class _VoiceGroup(_SettingsGroup):
_FIELDS = ()
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",
)
class _ServerGroup(_SettingsGroup):
_FIELDS = (
"server_host",
"server_port",
)
_PACKAGE_DIR = Path(__file__).resolve().parent
_REPO_ROOT = _PACKAGE_DIR.parent
_DEFAULT_ENV_FILE = _REPO_ROOT / ".env"
class Settings(BaseSettings):
"""Application configuration loaded from environment / .env.
算法与管线默认可调项见 ``app.baked.algorithm`` / ``app.baked.pipeline``。
"""
database_url: str | None = None
postgres_user: str = "postgres"
postgres_password: str = "postgres"
postgres_db: str = "operation_room"
postgres_host: str = "localhost"
postgres_port: int = 35432
server_host: str = "0.0.0.0"
server_port: int = Field(default=38080, ge=1, le=65535)
video_default_backend: Literal["rtsp", "hikvision_sdk", "auto"] = "rtsp"
video_camera_backend_overrides_json: str = ""
video_rtsp_url_template: str = ""
video_rtsp_urls_json: str = ""
video_rtsp_urls_json_file: str = ""
hikvision_lib_dir: str = "/opt/hikvision/lib"
hikvision_sdk_enabled: bool = False
hikvision_device_ip: str = ""
hikvision_device_port: int = Field(default=8000, ge=1, le=65535)
hikvision_user: str = ""
hikvision_password: str = ""
hikvision_channel: int = Field(default=1, ge=1, le=512)
hikvision_preview_rtsp_template: str = ""
hikvision_camera_rtsp_urls_json: str = ""
hikvision_sdk_fallback_to_rtsp: bool = True
baidu_speech_app_id: str = Field(default="", validation_alias="BAIDU_APP_ID")
baidu_speech_api_key: str = Field(default="", validation_alias="BAIDU_API_KEY")
baidu_speech_secret_key: str = Field(default="", validation_alias="BAIDU_SECRET_KEY")
baidu_speech_connection_timeout_ms: int | None = Field(
default=None, validation_alias="BAIDU_CONNECTION_TIMEOUT_MS"
)
baidu_speech_socket_timeout_ms: int | None = Field(
default=None, validation_alias="BAIDU_SOCKET_TIMEOUT_MS"
)
baidu_speech_asr_dev_pid: int = Field(
default=1537, ge=1000, le=99999, validation_alias="BAIDU_ASR_DEV_PID"
)
minio_endpoint: str = ""
minio_access_key: str = ""
minio_secret_key: str = ""
minio_bucket: str = "operation-room-voice"
minio_secure: bool = False
minio_region: str = ""
demo_cors_enabled: bool = True
demo_cors_origins: str = "*"
demo_orchestrator_enabled: bool = False
demo_orchestrator_rtsp_port: int = Field(default=18554, ge=1, le=65535)
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:
return []
if raw == "*":
return ["*"]
return [item.strip() for item in raw.split(",") if item.strip()]
model_config = SettingsConfigDict(
env_file=(str(_DEFAULT_ENV_FILE),),
env_file_encoding="utf-8",
extra="ignore",
env_ignore_empty=True,
populate_by_name=True,
)
@property
def sqlalchemy_database_url(self) -> str:
component_values = (
self.postgres_user,
self.postgres_password,
self.postgres_db,
self.postgres_host,
self.postgres_port,
)
default_component_values = (
"postgres",
"postgres",
"operation_room",
"localhost",
35432,
)
if component_values != default_component_values or not self.database_url:
user = quote_plus(self.postgres_user)
password = quote_plus(self.postgres_password)
database = quote_plus(self.postgres_db)
return (
"postgresql+asyncpg://"
f"{user}:{password}@{self.postgres_host}:{self.postgres_port}/{database}"
)
return self.database_url
@property
def baidu_speech_configured(self) -> bool:
return bool(
self.baidu_speech_app_id.strip()
and self.baidu_speech_api_key.strip()
and self.baidu_speech_secret_key.strip()
)
@property
def minio_configured(self) -> bool:
return bool(
self.minio_endpoint.strip()
and self.minio_access_key.strip()
and self.minio_secret_key.strip()
and self.minio_bucket.strip()
)
@staticmethod
def _parse_rtsp_urls_object(raw: str) -> dict[str, str]:
raw = (raw or "").strip()
if not raw:
return {}
try:
data: Any = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid VIDEO_RTSP_URLS_JSON: {exc}") from exc
if not isinstance(data, dict):
raise ValueError("VIDEO_RTSP_URLS_JSON must be a JSON object")
return {str(k): str(v) for k, v in data.items()}
def video_rtsp_url_map(self) -> dict[str, str]:
merged: dict[str, str] = {}
path_raw = (self.video_rtsp_urls_json_file or "").strip()
if path_raw:
path = Path(path_raw).expanduser()
if not path.is_file():
raise ValueError(
f"VIDEO_RTSP_URLS_JSON_FILE is set but file not found: {path}"
)
try:
file_obj: Any = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ValueError(
f"Invalid JSON in VIDEO_RTSP_URLS_JSON_FILE {path}: {exc}"
) from exc
if not isinstance(file_obj, dict):
raise ValueError(
f"VIDEO_RTSP_URLS_JSON_FILE must contain a JSON object: {path}"
)
merged = {str(k): str(v) for k, v in file_obj.items()}
merged.update(self._parse_rtsp_urls_object(self.video_rtsp_urls_json))
return merged
@property
def camera_rtsp_urls_sample_path(self) -> str:
return baked_algorithm.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()