This commit is contained in:
2026-03-23 09:58:40 -03:00
parent 9c9c7dff09
commit 8186bb5fe6
40 changed files with 3996 additions and 17 deletions

0
detect/__init__.py Normal file
View File

41
detect/events.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Detection pipeline event helpers.
Non-generated runtime code for pushing SSE events.
The event payload types are in sse_contract.py (generated by modelgen).
"""
from pydantic import BaseModel
from core.events import push_event
DETECT_EVENTS_PREFIX = "detect_events"
# SSE event type names
EVENT_GRAPH_UPDATE = "graph_update"
EVENT_STATS_UPDATE = "stats_update"
EVENT_FRAME_UPDATE = "frame_update"
EVENT_DETECTION = "detection"
EVENT_LOG = "log"
EVENT_JOB_COMPLETE = "job_complete"
ALL_EVENT_TYPES = [
EVENT_GRAPH_UPDATE,
EVENT_STATS_UPDATE,
EVENT_FRAME_UPDATE,
EVENT_DETECTION,
EVENT_LOG,
EVENT_JOB_COMPLETE,
]
TERMINAL_EVENTS = [EVENT_JOB_COMPLETE]
def push_detect_event(job_id: str, event_type: str, data: BaseModel) -> None:
"""Push a typed detection event to Redis."""
push_event(
job_id=job_id,
event_type=event_type,
data=data.model_dump(mode="json"),
prefix=DETECT_EVENTS_PREFIX,
)

86
detect/models.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Core domain models for the detection pipeline.
These are pipeline-internal models — the data structures that flow
between LangGraph nodes. SSE event payloads (sse_contract.py) are
derived from these when emitting to the UI.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
import numpy as np
@dataclass
class Frame:
sequence: int
chunk_id: int
timestamp: float # position in video (seconds)
image: np.ndarray
perceptual_hash: str = ""
@dataclass
class BoundingBox:
x: int
y: int
w: int
h: int
confidence: float
label: str
@dataclass
class TextCandidate:
frame: Frame
bbox: BoundingBox
text: str
ocr_confidence: float
@dataclass
class BrandDetection:
brand: str
timestamp: float
duration: float
confidence: float
source: Literal["ocr", "local_vlm", "cloud_llm", "logo_match", "auxiliary"]
bbox: BoundingBox | None = None
frame_ref: int | None = None
content_type: str = ""
@dataclass
class BrandStats:
total_appearances: int = 0
total_screen_time: float = 0.0
avg_confidence: float = 0.0
first_seen: float = 0.0
last_seen: float = 0.0
@dataclass
class PipelineStats:
frames_extracted: int = 0
frames_after_scene_filter: int = 0
regions_detected: int = 0
regions_resolved_by_ocr: int = 0
regions_escalated_to_local_vlm: int = 0
regions_escalated_to_cloud_llm: int = 0
auxiliary_detections: int = 0
cloud_llm_calls: int = 0
processing_time_seconds: float = 0.0
estimated_cloud_cost_usd: float = 0.0
@dataclass
class DetectionReport:
video_source: str
content_type: str
duration_seconds: float
brands: dict[str, BrandStats] = field(default_factory=dict)
timeline: list[BrandDetection] = field(default_factory=list)
pipeline_stats: PipelineStats = field(default_factory=PipelineStats)

103
detect/sse_contract.py Normal file
View File

@@ -0,0 +1,103 @@
"""
Pydantic Models - GENERATED FILE
Do not edit directly. Regenerate using modelgen.
"""
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
class GraphNode(BaseModel):
"""A pipeline stage node."""
id: str
status: str = "idle"
items_in: int = 0
items_out: int = 0
class GraphEdge(BaseModel):
"""An edge between pipeline stages."""
source: str
target: str
throughput: int = 0
class BoundingBoxEvent(BaseModel):
"""Bounding box in SSE event payloads."""
x: int
y: int
w: int
h: int
confidence: float
label: str
resolved_brand: Optional[str] = None
source: Optional[str] = None
class BrandSummary(BaseModel):
"""Per-brand stats in the final report."""
brand: str
total_appearances: int = 0
total_screen_time: float = 0.0
avg_confidence: float = 0.0
first_seen: float = 0.0
last_seen: float = 0.0
class GraphUpdate(BaseModel):
"""Pipeline node state transition. SSE event: graph_update"""
nodes: List[GraphNode] = Field(default_factory=list)
edges: List[GraphEdge] = Field(default_factory=list)
active_path: List[str] = Field(default_factory=list)
class StatsUpdate(BaseModel):
"""Funnel statistics snapshot. SSE event: stats_update"""
frames_extracted: int = 0
frames_after_scene_filter: int = 0
regions_detected: int = 0
regions_resolved_by_ocr: int = 0
regions_escalated_to_local_vlm: int = 0
regions_escalated_to_cloud_llm: int = 0
cloud_llm_calls: int = 0
processing_time_seconds: float = 0.0
estimated_cloud_cost_usd: float = 0.0
class FrameUpdate(BaseModel):
"""Current frame being processed. SSE event: frame_update"""
frame_ref: int
timestamp: float
jpeg_b64: str
boxes: List[BoundingBoxEvent] = Field(default_factory=list)
class Detection(BaseModel):
"""A confirmed brand detection. SSE event: detection"""
brand: str
timestamp: float
duration: float
confidence: float
source: str
content_type: str
bbox: Optional[BoundingBoxEvent] = None
frame_ref: Optional[int] = None
class LogEvent(BaseModel):
"""Pipeline log line. SSE event: log"""
level: str
stage: str
msg: str
ts: str
trace_id: Optional[str] = None
class DetectionReportSummary(BaseModel):
"""Final detection report summary."""
video_source: str
content_type: str
duration_seconds: float
total_detections: int = 0
brands: List[BrandSummary] = Field(default_factory=list)
stats: Optional[StatsUpdate] = None
class JobComplete(BaseModel):
"""Final report when pipeline finishes. SSE event: job_complete"""
job_id: str
report: Optional[DetectionReportSummary] = None