Spaces:
Sleeping
Sleeping
| """Pydantic v2 schemas for all API request/response types. | |
| These are the wire-format models β separate from the internal dataclasses | |
| in core/ so the API contract is explicit and independently versioned. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from typing import TYPE_CHECKING, Any | |
| from pydantic import BaseModel, Field, field_validator | |
| if TYPE_CHECKING: | |
| from core.analytics import CountingLineSpec | |
| # ββ WebSocket message schemas βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TrackData(BaseModel): | |
| """Single tracked vehicle, sent in metrics WebSocket messages.""" | |
| id: int | |
| bbox: list[float] | |
| class_id: int | |
| class_name: str | |
| confidence: float | |
| speed_kmh: float | None = None | |
| class MetricsMessage(BaseModel): | |
| """JSON payload pushed over /ws/metrics each processed frame.""" | |
| timestamp: float | |
| frame_index: int | |
| # Counts | |
| total_count: int | |
| count_per_class: dict[str, int] | |
| count_per_line: dict[str, int] = Field(default_factory=dict) | |
| vehicles_in_frame: int | |
| # Speed | |
| avg_speed_kmh: float | |
| speed_samples: list[float] | |
| # Alerts | |
| alerts: list[str] | |
| # Tracks for frontend overlay | |
| tracks: list[TrackData] | |
| def from_frame_metrics(cls, m: Any) -> "MetricsMessage": | |
| """Convert core.analytics.FrameMetrics β MetricsMessage.""" | |
| return cls( | |
| timestamp=m.timestamp, | |
| frame_index=m.frame_index, | |
| total_count=m.total_count, | |
| count_per_class=m.count_per_class, | |
| count_per_line=m.count_per_line, | |
| vehicles_in_frame=m.vehicles_in_frame, | |
| avg_speed_kmh=m.avg_speed_kmh, | |
| speed_samples=m.speed_samples, | |
| alerts=m.alerts, | |
| tracks=[TrackData(**t) for t in m.tracks], | |
| ) | |
| # ββ REST: pipeline config βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CountingLineConfig(BaseModel): | |
| """A named counting line defined by two pixel-coordinate endpoints.""" | |
| name: str = "line" | |
| x1: int | |
| y1: int | |
| x2: int | |
| y2: int | |
| def as_spec(self) -> "CountingLineSpec": | |
| from core.analytics import CountingLineSpec # noqa: PLC0415 | |
| return CountingLineSpec( | |
| name=self.name, | |
| p1=(self.x1, self.y1), | |
| p2=(self.x2, self.y2), | |
| ) | |
| class PipelineStartRequest(BaseModel): | |
| """POST /pipeline/start body.""" | |
| source: str | int = Field( | |
| default=0, | |
| description="Video file path, webcam index, RTSP URL, or 'demo'", | |
| ) | |
| confidence_threshold: float = Field(default=0.35, ge=0.0, le=1.0) | |
| iou_threshold: float = Field(default=0.45, ge=0.0, le=1.0) | |
| skip_frames: int = Field(default=0, ge=0) | |
| counting_lines: list[CountingLineConfig] = Field(default_factory=list) | |
| pixels_per_meter: float = Field(default=10.0, gt=0) | |
| loop: bool = False | |
| draw_annotations: bool = True | |
| jpeg_quality: int = Field(default=80, ge=1, le=100) | |
| def coerce_source(cls, v: Any) -> str | int: | |
| if isinstance(v, str) and v.lower() != "demo": | |
| try: | |
| return int(v) | |
| except ValueError: | |
| pass | |
| return v | |
| # ββ REST: analytics history βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class HourlyAggregate(BaseModel): | |
| hour: str | |
| total_count: int | |
| count_per_class: dict[str, int] | |
| avg_speed_kmh: float | |
| peak_vehicles: int | |
| alert_count: int | |
| class AnalyticsSummaryResponse(BaseModel): | |
| period_start: str | |
| period_end: str | |
| total_vehicles: int | |
| count_per_class: dict[str, int] | |
| avg_speed_kmh: float | |
| peak_hour: str | None | |
| peak_count: int | |
| total_alerts: int | |
| class RecentMetricsResponse(BaseModel): | |
| window_seconds: int | |
| frames: list[MetricsMessage] | |
| # ββ REST: pipeline status βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CountingLineStatus(BaseModel): | |
| """Counting line info returned in pipeline status.""" | |
| name: str | |
| x1: int | |
| y1: int | |
| x2: int | |
| y2: int | |
| class PipelineStatus(BaseModel): | |
| running: bool | |
| source: str | int | None = None | |
| frame_index: int = 0 | |
| uptime_seconds: float = 0.0 | |
| fps: float = 0.0 | |
| demo_mode: bool = False | |
| counting_lines: list[CountingLineStatus] = Field(default_factory=list) | |
| # ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class HealthResponse(BaseModel): | |
| status: str = "ok" | |
| version: str = "1.0.0" | |
| timestamp: float = Field(default_factory=time.time) | |
| gpu: bool = False | |