Files
jc-video-recognize/apps/server/core/settings.py
T

354 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一应用配置 (MVP-1 / 新增)
基于 pydantic-settings 的多环境配置管理,集中收敛原先散落在
``main.py``、各 service 模块中的环境变量、路径、阈值等配置。
加载顺序 (优先级从高到低)::
1. 显式构造参数
2. 环境变量 (大小写不敏感)
3. ``.env`` 文件 (位于 apps/server/.env)
4. 默认值
使用方式::
from core.settings import get_settings
settings = get_settings()
print(settings.api.port)
print(settings.detection.default_confidence)
"""
from __future__ import annotations
import os
from functools import lru_cache
from pathlib import Path
from typing import List, Optional
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# 项目根 (apps/server)
SERVER_DIR: Path = Path(__file__).resolve().parent.parent
PROJECT_ROOT: Path = SERVER_DIR.parent.parent
# 共享 .env 文件路径 (所有嵌套子配置共用)
_ENV_FILE = str(SERVER_DIR / ".env")
# ---------------------------------------------------------------------------
# 子配置
# ---------------------------------------------------------------------------
class APISettings(BaseSettings):
"""API 与服务器相关配置。"""
model_config = SettingsConfigDict(env_prefix="API_", env_file=_ENV_FILE, extra="ignore")
host: str = Field(default="0.0.0.0", description="监听地址")
port: int = Field(default=8000, description="监听端口")
reload: bool = Field(default=True, description="开发模式自动重载")
cors_origins: List[str] = Field(
default_factory=lambda: ["*"], description="允许的跨域来源"
)
class DetectionSettings(BaseSettings):
"""检测相关全局默认值。"""
model_config = SettingsConfigDict(env_prefix="DETECTION_", env_file=_ENV_FILE, extra="ignore")
default_confidence: float = Field(default=0.5, ge=0.0, le=1.0)
default_iou: float = Field(default=0.45, ge=0.0, le=1.0)
# 决策引擎过滤的最低置信度
min_confidence: float = Field(default=0.3, ge=0.0, le=1.0)
class ActionDetectionSettings(BaseSettings):
"""ppTSM 行为识别 (Docker) 服务配置。"""
model_config = SettingsConfigDict(env_prefix="ACTION_DETECTION_", env_file=_ENV_FILE, extra="ignore")
api_url: str = Field(default="http://localhost:8081")
timeout: int = Field(default=30, ge=1)
class EventEngineSettings(BaseSettings):
"""事件决策 + 聚合 + 规则引擎配置。"""
model_config = SettingsConfigDict(env_prefix="EVENT_", env_file=_ENV_FILE, extra="ignore")
# 时间窗口去重 (秒),同一 (source_id, event_type, track_id) 在窗口内只产生一条
dedup_window_seconds: float = Field(default=30.0, ge=0.0)
# 规则 YAML 目录 (相对 server 根)
rules_dir: str = Field(default="config/rules")
# 事件聚合最大活跃事件数 (超过将按 LRU 淘汰)
max_active_events: int = Field(default=1000, ge=1)
class RTSPSettings(BaseSettings):
"""RTSP 流接入相关配置 (MVP-2)。"""
model_config = SettingsConfigDict(env_prefix="RTSP_", env_file=_ENV_FILE, extra="ignore")
max_streams: int = Field(default=16, ge=1, description="最大同时接入流数量")
buffer_capacity: int = Field(default=300, ge=1, description="每路流帧缓冲区容量")
reconnect_attempts: int = Field(default=10, ge=0, description="最大重连次数,0=无限")
reconnect_interval_base: float = Field(default=2.0, ge=0.5, description="首次重连间隔(秒)")
reconnect_interval_max: float = Field(default=60.0, ge=1.0, description="最大重连间隔(秒)")
reconnect_backoff_factor: float = Field(default=2.0, ge=1.0, description="退避因子")
frame_skip: int = Field(default=0, ge=0, description="帧采样间隔,0=每帧都取")
read_timeout: float = Field(default=5.0, ge=1.0, description="单帧读取超时(秒)")
detect_interval: float = Field(default=0.0, ge=0.0, description="检测轮询间隔(秒)0=每帧检测")
class MQTTSettings(BaseSettings):
"""MQTT 预警发布相关配置 (MVP-2 / D16-D18)。"""
model_config = SettingsConfigDict(env_prefix="MQTT_", env_file=_ENV_FILE, extra="ignore")
enabled: bool = Field(default=False, description="是否启用 MQTT 发布")
broker_host: str = Field(default="localhost", description="MQTT broker 主机")
broker_port: int = Field(default=1883, ge=1, le=65535, description="MQTT broker 端口")
client_id: str = Field(default="jc-video-recognize", description="MQTT 客户端ID")
username: Optional[str] = Field(default=None, description="MQTT 用户名")
password: Optional[str] = Field(default=None, description="MQTT 密码")
keepalive: int = Field(default=60, ge=10, description="心跳间隔(秒)")
qos: int = Field(default=1, ge=0, le=2, description="QoS 等级")
retain: bool = Field(default=False, description="是否保留消息")
# 主题模板
alert_topic_prefix: str = Field(default="video/alerts", description="预警主题前缀")
# 重连
reconnect_min_delay: float = Field(default=1.0, ge=0.1, description="最小重连间隔(秒)")
reconnect_max_delay: float = Field(default=60.0, ge=1.0, description="最大重连间隔(秒)")
class TrackingSettings(BaseSettings):
"""目标跟踪 (ByteTrack) 配置 (MVP-2 / D19)。"""
model_config = SettingsConfigDict(env_prefix="TRACKING_", env_file=_ENV_FILE, extra="ignore")
enabled: bool = Field(default=True, description="是否启用目标跟踪")
track_thresh: float = Field(default=0.5, ge=0.0, le=1.0, description="跟踪置信度阈值")
high_thresh: float = Field(default=0.6, ge=0.0, le=1.0, description="高置信度阈值")
match_thresh: float = Field(default=0.8, ge=0.0, le=1.0, description="IOU 匹配阈值")
max_lost_frames: int = Field(default=30, ge=1, description="目标丢失最大帧数")
min_box_area: float = Field(default=10.0, ge=0.0, description="最小框面积")
class AggregatorSettings(BaseSettings):
"""事件聚合器扩展配置 (MVP-2 / D20)。"""
model_config = SettingsConfigDict(env_prefix="AGGREGATOR_", env_file=_ENV_FILE, extra="ignore")
enable_spatial_merge: bool = Field(default=True, description="是否启用空间邻近合并")
spatial_iou_threshold: float = Field(
default=0.3, ge=0.0, le=1.0, description="空间合并 IOU 阈值"
)
confidence_fusion_strategy: str = Field(
default="weighted",
description="置信度融合策略: weighted/max/avg",
)
fusion_decay_factor: float = Field(
default=0.9, ge=0.0, le=1.0, description="历史置信度衰减因子"
)
class LLMSettings(BaseSettings):
"""LLM 二次判断服务配置 (MVP-3 / D29)。
支持 OpenAI 兼容协议 (GPT-4V / Qwen-VL / GLM-4V 等)
通过 ``provider=mock`` 在无 API Key 环境下保持离线可用。
"""
model_config = SettingsConfigDict(env_prefix="LLM_", env_file=_ENV_FILE, extra="ignore")
enabled: bool = Field(default=False, description="是否启用 LLM 二次判断")
provider: str = Field(
default="mock",
description="LLM provider: openai / qwen / glm / mock",
)
api_base: Optional[str] = Field(default=None, description="API base URL (OpenAI 兼容)")
api_key: Optional[str] = Field(default=None, description="API Key")
model: str = Field(default="gpt-4o", description="LLM 模型名称")
timeout: float = Field(default=15.0, ge=1.0, description="单次调用超时(秒)")
max_retries: int = Field(default=2, ge=0, description="失败重试次数")
max_tokens: int = Field(default=512, ge=64, description="最大返回 token 数")
temperature: float = Field(default=0.0, ge=0.0, le=2.0, description="采样温度")
max_concurrency: int = Field(default=2, ge=1, description="最大并发调用数")
image_max_side: int = Field(
default=768, ge=128, description="图像编码前最大边长 (节省 token)"
)
class LLMTriggerSettings(BaseSettings):
"""LLM 触发器配置 (MVP-3 / D26-D28)。"""
model_config = SettingsConfigDict(env_prefix="LLM_TRIGGER_", env_file=_ENV_FILE, extra="ignore")
enabled: bool = Field(default=False, description="是否启用 LLM 触发")
window_seconds: float = Field(
default=3.0, ge=0.5, description="多帧累积时间窗口(秒)"
)
min_consecutive_hits: int = Field(
default=3, ge=1, description="触发所需的最小连续命中帧数"
)
min_avg_confidence: float = Field(
default=0.55, ge=0.0, le=1.0, description="累积平均置信度阈值"
)
cooldown_seconds: float = Field(
default=20.0, ge=0.0, description="同目标 LLM 冷却时间(秒)"
)
max_track_capacity: int = Field(
default=2000, ge=1, description="累积器最大跟踪条目"
)
severity_bypass: List[str] = Field(
default_factory=lambda: ["critical"],
description="无需累积、立即触发的严重性级别",
)
class FusionSettings(BaseSettings):
"""LLM/YOLO 结果融合配置 (MVP-3 / D30)。"""
model_config = SettingsConfigDict(env_prefix="FUSION_", env_file=_ENV_FILE, extra="ignore")
strategy: str = Field(
default="weighted",
description="融合策略: weighted / conservative / llm_priority",
)
yolo_weight: float = Field(default=0.4, ge=0.0, le=1.0)
llm_weight: float = Field(default=0.6, ge=0.0, le=1.0)
# conservative 策略下,LLM 判定为否时直接抑制预警
suppress_on_llm_negative: bool = Field(default=True)
# LLM 不可用时是否回退到 YOLO 结果
fallback_to_yolo: bool = Field(default=True)
class LLMCostSettings(BaseSettings):
"""LLM 成本追踪 + 降级策略配置 (MVP-3 / D34)。"""
model_config = SettingsConfigDict(env_prefix="LLM_COST_", env_file=_ENV_FILE, extra="ignore")
daily_budget_usd: float = Field(
default=0.0, ge=0.0, description="日预算 (USD)0=不限"
)
history_days: int = Field(default=7, ge=1, description="保留多少天的日级统计")
recent_window: int = Field(
default=20, ge=3, description="错误率统计窗口 (最近 N 次调用)"
)
error_rate_threshold: float = Field(
default=0.5, gt=0.0, le=1.0, description="触发熔断的错误率阈值"
)
cooldown_seconds: float = Field(
default=60.0, ge=0.0, description="熔断冷却时间 (秒)"
)
class LoggingSettings(BaseSettings):
"""日志配置。"""
model_config = SettingsConfigDict(env_prefix="LOG_", env_file=_ENV_FILE, extra="ignore")
level: str = Field(default="INFO")
json_format: bool = Field(default=False)
class PathSettings(BaseSettings):
"""关键路径 (静态资源 / 外部模型) 配置。"""
model_config = SettingsConfigDict(env_prefix="PATH_", extra="ignore")
server_dir: Path = SERVER_DIR
project_root: Path = PROJECT_ROOT
static_dir: Path = SERVER_DIR / "static"
results_dir: Path = SERVER_DIR / "static" / "results"
temp_dir: Path = SERVER_DIR / "static" / "temp"
uploads_dir: Path = SERVER_DIR / "static" / "uploads"
external_paddle: Path = (
PROJECT_ROOT / "external" / "video-recognition-system" / "PaddlePaddle"
)
def ensure(self) -> None:
"""确保所有需要的目录存在 (启动时调用)。"""
for p in (self.static_dir, self.results_dir, self.temp_dir, self.uploads_dir):
p.mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# 总配置
# ---------------------------------------------------------------------------
class Settings(BaseSettings):
"""应用总配置。
子配置统一在此聚合,便于以 ``settings.api.port`` 这种命名空间方式访问。
"""
model_config = SettingsConfigDict(
env_file=str(SERVER_DIR / ".env"),
env_file_encoding="utf-8",
env_nested_delimiter="__",
case_sensitive=False,
extra="ignore",
)
env: str = Field(default="development", description="运行环境")
debug: bool = Field(default=True)
api: APISettings = Field(default_factory=APISettings)
detection: DetectionSettings = Field(default_factory=DetectionSettings)
action_detection: ActionDetectionSettings = Field(
default_factory=ActionDetectionSettings
)
event_engine: EventEngineSettings = Field(default_factory=EventEngineSettings)
rtsp: RTSPSettings = Field(default_factory=RTSPSettings)
mqtt: MQTTSettings = Field(default_factory=MQTTSettings)
tracking: TrackingSettings = Field(default_factory=TrackingSettings)
aggregator: AggregatorSettings = Field(default_factory=AggregatorSettings)
llm: LLMSettings = Field(default_factory=LLMSettings)
llm_trigger: LLMTriggerSettings = Field(default_factory=LLMTriggerSettings)
fusion: FusionSettings = Field(default_factory=FusionSettings)
llm_cost: LLMCostSettings = Field(default_factory=LLMCostSettings)
logging: LoggingSettings = Field(default_factory=LoggingSettings)
paths: PathSettings = Field(default_factory=PathSettings)
@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""获取全局 Settings 单例 (带缓存)。
测试场景下可调用 ``get_settings.cache_clear()`` 重新加载。
"""
settings = Settings()
settings.paths.ensure()
return settings
__all__ = [
"Settings",
"APISettings",
"DetectionSettings",
"ActionDetectionSettings",
"EventEngineSettings",
"RTSPSettings",
"MQTTSettings",
"TrackingSettings",
"AggregatorSettings",
"LLMSettings",
"LLMTriggerSettings",
"FusionSettings",
"LLMCostSettings",
"LoggingSettings",
"PathSettings",
"get_settings",
"SERVER_DIR",
"PROJECT_ROOT",
]