phase 12
This commit is contained in:
@@ -35,7 +35,7 @@ def push(r, key, event):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="pipeline-test")
|
||||
parser.add_argument("--job", default=f"pipeline-{int(__import__('time').time()) % 100000}")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
parser.add_argument("--delay", type=float, default=0.5)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -15,7 +15,7 @@ import sys
|
||||
|
||||
# Parse args early so we can set REDIS_URL before imports
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="extract-filter-test")
|
||||
parser.add_argument("--job", default=f"extract-{int(__import__('time').time()) % 100000}")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import time as _time
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="graph-test")
|
||||
parser.add_argument("--job", default=f"graph-{int(_time.time()) % 100000}")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def push(r, key, event):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="brand-table-test")
|
||||
parser.add_argument("--job", default=f"brand-{int(__import__('time').time()) % 100000}")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
parser.add_argument("--delay", type=float, default=0.6)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -23,8 +23,8 @@ import redis
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(name)s — %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NODES = ["extract_frames", "filter_scenes", "detect_objects", "run_ocr",
|
||||
"match_brands", "escalate_vlm", "escalate_cloud", "compile_report"]
|
||||
NODES = ["extract_frames", "filter_scenes", "detect_objects", "preprocess",
|
||||
"run_ocr", "match_brands", "escalate_vlm", "escalate_cloud", "compile_report"]
|
||||
|
||||
|
||||
def ts():
|
||||
@@ -70,12 +70,22 @@ def push_stats(r, key, **fields):
|
||||
push(r, key, base)
|
||||
|
||||
|
||||
_bbox_idx = 0
|
||||
|
||||
def push_detection(r, key, brand, conf, source, timestamp, frame_ref, delay):
|
||||
global _bbox_idx
|
||||
# Spread fake bboxes across the frame so they don't overlap
|
||||
col = _bbox_idx % 4
|
||||
row = _bbox_idx // 4
|
||||
bbox = {"x": 50 + col * 200, "y": 50 + row * 120, "w": 160, "h": 80}
|
||||
_bbox_idx += 1
|
||||
|
||||
push(r, key, {
|
||||
"event": "detection",
|
||||
"brand": brand, "confidence": conf, "source": source,
|
||||
"timestamp": timestamp, "duration": 0.5,
|
||||
"content_type": "soccer_broadcast", "frame_ref": frame_ref,
|
||||
"bbox": bbox,
|
||||
})
|
||||
logger.info(" [%s] %s %.2f t=%.1fs", source, brand, conf, timestamp)
|
||||
time.sleep(delay * 0.3)
|
||||
@@ -83,7 +93,9 @@ def push_detection(r, key, brand, conf, source, timestamp, frame_ref, delay):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="escalation-test")
|
||||
import time as _time
|
||||
default_job = f"escalation-{int(_time.time()) % 100000}"
|
||||
parser.add_argument("--job", default=default_job)
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
parser.add_argument("--delay", type=float, default=0.5)
|
||||
args = parser.parse_args()
|
||||
@@ -121,6 +133,32 @@ def main():
|
||||
push(r, key, {"event": "log", "level": "INFO", "stage": "YOLODetector",
|
||||
"msg": "Running yolov8n on 52 frames"})
|
||||
time.sleep(delay)
|
||||
|
||||
# Push a sample frame with YOLO boxes
|
||||
import base64, io
|
||||
from PIL import Image as PILImage, ImageDraw
|
||||
frame_img = PILImage.new("RGB", (960, 540), "#1a1a2e")
|
||||
draw = ImageDraw.Draw(frame_img)
|
||||
draw.rectangle([40, 440, 900, 520], outline="#444", width=2)
|
||||
draw.text((100, 460), "SPONSOR BOARD AREA", fill="#666")
|
||||
draw.rectangle([350, 150, 610, 380], outline="#333", width=1)
|
||||
draw.text((400, 200), "PLAYER", fill="#555")
|
||||
buf = io.BytesIO()
|
||||
frame_img.save(buf, "JPEG")
|
||||
frame_b64 = base64.b64encode(buf.getvalue()).decode()
|
||||
|
||||
yolo_boxes = [
|
||||
{"x": 40, "y": 440, "w": 860, "h": 80, "confidence": 0.92,
|
||||
"label": "ad_board", "stage": "detect_objects", "source": "yolo"},
|
||||
{"x": 350, "y": 150, "w": 260, "h": 230, "confidence": 0.87,
|
||||
"label": "person", "stage": "detect_objects", "source": "yolo"},
|
||||
{"x": 700, "y": 30, "w": 200, "h": 60, "confidence": 0.78,
|
||||
"label": "scoreboard", "stage": "detect_objects", "source": "yolo"},
|
||||
]
|
||||
push(r, key, {"event": "frame_update", "frame_ref": 25, "timestamp": 12.5,
|
||||
"jpeg_b64": frame_b64, "boxes": yolo_boxes})
|
||||
time.sleep(delay)
|
||||
|
||||
push_stats(r, key, frames_extracted=180, frames_after_scene_filter=52,
|
||||
regions_detected=41, processing_time_seconds=14.2)
|
||||
push_graph(r, key, "detect_objects", "done", delay)
|
||||
|
||||
@@ -85,7 +85,7 @@ def push_stats(r, key, **overrides):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="timeline-cost-test")
|
||||
parser.add_argument("--job", default=f"timeline-{int(__import__('time').time()) % 100000}")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
parser.add_argument("--delay", type=float, default=0.4)
|
||||
args = parser.parse_args()
|
||||
|
||||
44
tests/detect/test_config_endpoint.py
Normal file
44
tests/detect/test_config_endpoint.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Tests for the config endpoint and stage palette."""
|
||||
|
||||
from detect.stages import list_stages, get_palette
|
||||
|
||||
|
||||
def test_stage_palette_has_config_fields():
|
||||
"""Every stage with config fields should be servable by the endpoint."""
|
||||
stages = list_stages()
|
||||
stages_with_config = [s for s in stages if s.config_fields]
|
||||
|
||||
assert len(stages_with_config) > 0
|
||||
|
||||
for stage in stages_with_config:
|
||||
for field in stage.config_fields:
|
||||
assert field.name
|
||||
assert field.type
|
||||
assert field.default is not None or field.type == "bool"
|
||||
|
||||
|
||||
def test_palette_categories():
|
||||
palette = get_palette()
|
||||
|
||||
expected_categories = {"preprocessing", "detection", "resolution", "escalation", "output"}
|
||||
actual_categories = set(palette.keys())
|
||||
|
||||
assert actual_categories == expected_categories
|
||||
|
||||
|
||||
def test_stage_config_serializable():
|
||||
"""Config fields should be JSON-serializable for the API response."""
|
||||
import json
|
||||
|
||||
stages = list_stages()
|
||||
for stage in stages:
|
||||
data = {
|
||||
"name": stage.name,
|
||||
"label": stage.label,
|
||||
"config_fields": [
|
||||
{"name": f.name, "type": f.type, "default": f.default}
|
||||
for f in stage.config_fields
|
||||
],
|
||||
}
|
||||
json_str = json.dumps(data)
|
||||
assert len(json_str) > 0
|
||||
84
tests/detect/test_preprocess.py
Normal file
84
tests/detect/test_preprocess.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Tests for OpenCV preprocessing — runs without GPU."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
try:
|
||||
import cv2
|
||||
HAS_CV2 = True
|
||||
except ImportError:
|
||||
HAS_CV2 = False
|
||||
|
||||
requires_cv2 = pytest.mark.skipif(not HAS_CV2, reason="opencv-python-headless not installed")
|
||||
|
||||
# Add gpu/ to path so imports resolve (gpu modules use relative imports)
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "gpu"))
|
||||
|
||||
|
||||
def _make_image(w: int = 200, h: int = 60) -> np.ndarray:
|
||||
"""White image with black text-like region."""
|
||||
img = np.ones((h, w, 3), dtype=np.uint8) * 255
|
||||
img[15:45, 20:180] = 30 # dark band simulating text
|
||||
return img
|
||||
|
||||
|
||||
@requires_cv2
|
||||
def test_binarize():
|
||||
from gpu.models.preprocess import binarize
|
||||
|
||||
img = _make_image()
|
||||
result = binarize(img)
|
||||
|
||||
assert result.shape == img.shape
|
||||
assert result.dtype == np.uint8
|
||||
# Should be mostly black and white (no grays)
|
||||
unique_values = np.unique(result)
|
||||
assert len(unique_values) <= 3 # 0, 255, maybe one more from anti-aliasing
|
||||
|
||||
|
||||
@requires_cv2
|
||||
def test_enhance_contrast():
|
||||
from gpu.models.preprocess import enhance_contrast
|
||||
|
||||
img = _make_image()
|
||||
result = enhance_contrast(img)
|
||||
|
||||
assert result.shape == img.shape
|
||||
assert result.dtype == np.uint8
|
||||
|
||||
|
||||
@requires_cv2
|
||||
def test_deskew_no_rotation():
|
||||
from gpu.models.preprocess import deskew
|
||||
|
||||
img = _make_image()
|
||||
result = deskew(img)
|
||||
|
||||
assert result.shape == img.shape
|
||||
# Straight image should be unchanged (angle < 0.5 deg)
|
||||
assert np.allclose(result, img, atol=5)
|
||||
|
||||
|
||||
@requires_cv2
|
||||
def test_preprocess_pipeline():
|
||||
from gpu.models.preprocess import preprocess
|
||||
|
||||
img = _make_image()
|
||||
|
||||
result = preprocess(img, do_binarize=False, do_deskew=False, do_contrast=True)
|
||||
assert result.shape == img.shape
|
||||
|
||||
result = preprocess(img, do_binarize=True, do_deskew=True, do_contrast=True)
|
||||
assert result.shape[:2] == img.shape[:2] # h, w same; channels may differ then get converted back
|
||||
|
||||
|
||||
@requires_cv2
|
||||
def test_preprocess_all_disabled():
|
||||
from gpu.models.preprocess import preprocess
|
||||
|
||||
img = _make_image()
|
||||
result = preprocess(img, do_binarize=False, do_deskew=False, do_contrast=False)
|
||||
|
||||
assert np.array_equal(result, img)
|
||||
@@ -4,8 +4,8 @@ from detect.stages import list_stages, get_stage, get_palette
|
||||
|
||||
|
||||
EXPECTED_STAGES = [
|
||||
"extract_frames", "filter_scenes", "detect_objects", "run_ocr",
|
||||
"match_brands", "escalate_vlm", "escalate_cloud", "compile_report",
|
||||
"extract_frames", "filter_scenes", "detect_objects", "preprocess",
|
||||
"run_ocr", "match_brands", "escalate_vlm", "escalate_cloud", "compile_report",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user