diff --git a/apps/server/core/settings.py b/apps/server/core/settings.py index 463f9e7..c8a67b1 100644 --- a/apps/server/core/settings.py +++ b/apps/server/core/settings.py @@ -34,6 +34,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict SERVER_DIR: Path = Path(__file__).resolve().parent.parent PROJECT_ROOT: Path = SERVER_DIR.parent.parent +# 共享 .env 文件路径 (所有嵌套子配置共用) +_ENV_FILE = str(SERVER_DIR / ".env") + # --------------------------------------------------------------------------- # 子配置 @@ -43,7 +46,7 @@ PROJECT_ROOT: Path = SERVER_DIR.parent.parent class APISettings(BaseSettings): """API 与服务器相关配置。""" - model_config = SettingsConfigDict(env_prefix="API_", extra="ignore") + 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="监听端口") @@ -56,7 +59,7 @@ class APISettings(BaseSettings): class DetectionSettings(BaseSettings): """检测相关全局默认值。""" - model_config = SettingsConfigDict(env_prefix="DETECTION_", extra="ignore") + 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) @@ -67,7 +70,7 @@ class DetectionSettings(BaseSettings): class ActionDetectionSettings(BaseSettings): """ppTSM 行为识别 (Docker) 服务配置。""" - model_config = SettingsConfigDict(env_prefix="ACTION_DETECTION_", extra="ignore") + 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) @@ -76,7 +79,7 @@ class ActionDetectionSettings(BaseSettings): class EventEngineSettings(BaseSettings): """事件决策 + 聚合 + 规则引擎配置。""" - model_config = SettingsConfigDict(env_prefix="EVENT_", extra="ignore") + 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) @@ -89,7 +92,7 @@ class EventEngineSettings(BaseSettings): class RTSPSettings(BaseSettings): """RTSP 流接入相关配置 (MVP-2)。""" - model_config = SettingsConfigDict(env_prefix="RTSP_", extra="ignore") + 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="每路流帧缓冲区容量") @@ -105,7 +108,7 @@ class RTSPSettings(BaseSettings): class MQTTSettings(BaseSettings): """MQTT 预警发布相关配置 (MVP-2 / D16-D18)。""" - model_config = SettingsConfigDict(env_prefix="MQTT_", extra="ignore") + 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 主机") @@ -126,7 +129,7 @@ class MQTTSettings(BaseSettings): class TrackingSettings(BaseSettings): """目标跟踪 (ByteTrack) 配置 (MVP-2 / D19)。""" - model_config = SettingsConfigDict(env_prefix="TRACKING_", extra="ignore") + 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="跟踪置信度阈值") @@ -139,7 +142,7 @@ class TrackingSettings(BaseSettings): class AggregatorSettings(BaseSettings): """事件聚合器扩展配置 (MVP-2 / D20)。""" - model_config = SettingsConfigDict(env_prefix="AGGREGATOR_", extra="ignore") + 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( @@ -154,10 +157,101 @@ class AggregatorSettings(BaseSettings): ) +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_", extra="ignore") + model_config = SettingsConfigDict(env_prefix="LOG_", env_file=_ENV_FILE, extra="ignore") level: str = Field(default="INFO") json_format: bool = Field(default=False) @@ -217,6 +311,10 @@ class Settings(BaseSettings): 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) @@ -243,6 +341,10 @@ __all__ = [ "MQTTSettings", "TrackingSettings", "AggregatorSettings", + "LLMSettings", + "LLMTriggerSettings", + "FusionSettings", + "LLMCostSettings", "LoggingSettings", "PathSettings", "get_settings",