feat:新增统一事件数据契约,为所有检测服务统一输出格式
This commit is contained in:
226
apps/server/models/event_schemas.py
Normal file
226
apps/server/models/event_schemas.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""统一事件数据契约 (MVP-1 / P0)
|
||||
|
||||
为后续事件决策引擎、规则引擎、聚合器、LLM 复审等模块提供一致的数据模型。
|
||||
所有检测服务 (YOLO / PaddleDetection / ActionDetection 等) 的输出,
|
||||
都应通过 ``services/adapters/detection_adapter.py`` 适配为本文件定义的
|
||||
``UnifiedDetection`` / ``DetectionResult``。
|
||||
|
||||
字段命名遵循 Google Python Style Guide,所有字段使用 snake_case。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 枚举
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""统一事件类型枚举。
|
||||
|
||||
与各服务底层 class_name 解耦,事件引擎只面向本枚举决策。
|
||||
"""
|
||||
|
||||
FIRE = "fire"
|
||||
SMOKE = "smoke"
|
||||
SMOKING = "smoking"
|
||||
FIGHT = "fight"
|
||||
LOITERING = "loitering"
|
||||
STATIONARY = "stationary"
|
||||
INTRUSION = "intrusion"
|
||||
ILLEGAL_PARKING = "illegal_parking"
|
||||
VEHICLE = "vehicle"
|
||||
PERSON = "person"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class SeverityLevel(str, Enum):
|
||||
"""事件严重性级别 (供规则引擎 / 聚合器使用)。"""
|
||||
|
||||
INFO = "info"
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class DetectionSource(str, Enum):
|
||||
"""检测来源 (用于适配器溯源 & 调度策略)。"""
|
||||
|
||||
YOLO = "yolo"
|
||||
PADDLE = "paddle"
|
||||
ACTION_DOCKER = "action_docker"
|
||||
BEHAVIOR = "behavior" # 由徘徊/静止等行为分析模块产出
|
||||
COMPOSITE = "composite" # 复合检测 (例如火灾双模型)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 基础结构
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BBox(BaseModel):
|
||||
"""边界框 (xyxy)。"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
x1: int
|
||||
y1: int
|
||||
x2: int
|
||||
y2: int
|
||||
|
||||
@field_validator("x2")
|
||||
@classmethod
|
||||
def _check_x2(cls, v: int, info) -> int:
|
||||
x1 = info.data.get("x1", 0)
|
||||
if v < x1:
|
||||
raise ValueError(f"bbox.x2({v}) 必须 >= x1({x1})")
|
||||
return v
|
||||
|
||||
@field_validator("y2")
|
||||
@classmethod
|
||||
def _check_y2(cls, v: int, info) -> int:
|
||||
y1 = info.data.get("y1", 0)
|
||||
if v < y1:
|
||||
raise ValueError(f"bbox.y2({v}) 必须 >= y1({y1})")
|
||||
return v
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return self.x2 - self.x1
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return self.y2 - self.y1
|
||||
|
||||
@property
|
||||
def area(self) -> int:
|
||||
return self.width * self.height
|
||||
|
||||
@property
|
||||
def center(self) -> Tuple[float, float]:
|
||||
return (self.x1 + self.x2) / 2.0, (self.y1 + self.y2) / 2.0
|
||||
|
||||
def to_list(self) -> List[int]:
|
||||
return [self.x1, self.y1, self.x2, self.y2]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 统一检测结果
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class UnifiedDetection(BaseModel):
|
||||
"""统一检测对象 (单个目标)。
|
||||
|
||||
适配器将各引擎原始输出转换为本结构,作为事件决策引擎的唯一输入。
|
||||
"""
|
||||
|
||||
detection_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
track_id: Optional[int] = None
|
||||
class_name: str
|
||||
label: str
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
bbox: BBox
|
||||
source: DetectionSource = DetectionSource.YOLO
|
||||
model_id: Optional[str] = None
|
||||
timestamp: float = Field(default_factory=time.time)
|
||||
extra: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DetectionStats(BaseModel):
|
||||
"""检测统计信息 (与现有 API 字段保持兼容)。"""
|
||||
|
||||
total_detections: int = 0
|
||||
avg_confidence: float = 0.0
|
||||
processing_time: float = 0.0
|
||||
model_used: Optional[str] = None
|
||||
fps: Optional[float] = None
|
||||
|
||||
|
||||
class DetectionResult(BaseModel):
|
||||
"""统一检测结果 (单帧 / 单图)。"""
|
||||
|
||||
success: bool = True
|
||||
message: str = ""
|
||||
source: DetectionSource = DetectionSource.YOLO
|
||||
model_id: Optional[str] = None
|
||||
frame_id: Optional[str] = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
timestamp: float = Field(default_factory=time.time)
|
||||
detections: List[UnifiedDetection] = Field(default_factory=list)
|
||||
stats: DetectionStats = Field(default_factory=DetectionStats)
|
||||
|
||||
def to_legacy_dict(self) -> Dict[str, Any]:
|
||||
"""转换为旧接口字段格式 (供 API 层向后兼容输出)。"""
|
||||
|
||||
return {
|
||||
"success": self.success,
|
||||
"message": self.message,
|
||||
"detections": [
|
||||
{
|
||||
"class": d.class_name,
|
||||
"label": d.label,
|
||||
"confidence": round(d.confidence, 3),
|
||||
"bbox": d.bbox.to_list(),
|
||||
**({"track_id": d.track_id} if d.track_id is not None else {}),
|
||||
}
|
||||
for d in self.detections
|
||||
],
|
||||
"stats": self.stats.model_dump(exclude_none=True),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 候选事件 (决策引擎产出)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CandidateEvent(BaseModel):
|
||||
"""候选事件 (供规则引擎与聚合器进一步处理)。"""
|
||||
|
||||
event_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
event_type: EventType
|
||||
severity: SeverityLevel = SeverityLevel.INFO
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
detection: UnifiedDetection
|
||||
source_id: Optional[str] = None # 摄像头/视频流标识
|
||||
timestamp: float = Field(default_factory=time.time)
|
||||
triggered_rules: List[str] = Field(default_factory=list)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AlertEvent(BaseModel):
|
||||
"""最终预警事件 (聚合 + 规则筛选后产出)。"""
|
||||
|
||||
alert_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
event_type: EventType
|
||||
severity: SeverityLevel
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
source_id: Optional[str] = None
|
||||
detections: List[UnifiedDetection] = Field(default_factory=list)
|
||||
rule_name: Optional[str] = None
|
||||
first_seen: float = Field(default_factory=time.time)
|
||||
last_seen: float = Field(default_factory=time.time)
|
||||
occurrence_count: int = 1
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"EventType",
|
||||
"SeverityLevel",
|
||||
"DetectionSource",
|
||||
"BBox",
|
||||
"UnifiedDetection",
|
||||
"DetectionStats",
|
||||
"DetectionResult",
|
||||
"CandidateEvent",
|
||||
"AlertEvent",
|
||||
]
|
||||
Reference in New Issue
Block a user