phase 8
This commit is contained in:
196
tests/detect/manual/test_timeline_cost.py
Normal file
196
tests/detect/manual/test_timeline_cost.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Push detection + stats events to test TimelinePanel and CostStatsPanel.
|
||||
|
||||
Simulates a pipeline run with detections spread across video time, escalation
|
||||
events, and accumulating cost — exercises both new phase 8 panels.
|
||||
|
||||
Usage:
|
||||
python tests/detect/manual/test_timeline_cost.py [--job JOB_ID] [--port PORT] [--delay SECS]
|
||||
|
||||
Opens: http://mpr.local.ar/detection/?job=<JOB_ID>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
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"]
|
||||
|
||||
# Detections spread across video time with different sources
|
||||
DETECTIONS = [
|
||||
("Nike", 0.97, "ocr", 2.0, 0.5),
|
||||
("Nike", 0.95, "ocr", 4.5, 1.0),
|
||||
("Emirates", 0.92, "ocr", 5.0, 2.0),
|
||||
("Adidas", 0.89, "ocr", 8.0, 0.5),
|
||||
("Nike", 0.94, "ocr", 12.0, 1.5),
|
||||
("Coca-Cola", 0.85, "ocr", 15.0, 0.5),
|
||||
("Emirates", 0.88, "ocr", 18.0, 2.0),
|
||||
("Adidas", 0.91, "ocr", 22.0, 1.0),
|
||||
("Mastercard", 0.78, "local_vlm", 25.0, 0.5),
|
||||
("Nike", 0.96, "ocr", 28.0, 1.0),
|
||||
("Emirates", 0.90, "ocr", 32.0, 2.0),
|
||||
("Heineken", 0.72, "cloud_llm", 35.0, 0.5),
|
||||
("Coca-Cola", 0.87, "ocr", 38.0, 0.5),
|
||||
("Nike", 0.93, "ocr", 42.0, 1.5),
|
||||
("Unknown", 0.65, "cloud_llm", 45.0, 0.5),
|
||||
("Adidas", 0.90, "ocr", 48.0, 1.0),
|
||||
("Emirates", 0.91, "ocr", 52.0, 2.0),
|
||||
("Nike", 0.95, "ocr", 55.0, 1.0),
|
||||
]
|
||||
|
||||
|
||||
def ts():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def push(r, key, event):
|
||||
event["ts"] = event.get("ts", ts())
|
||||
r.rpush(key, json.dumps(event))
|
||||
return event
|
||||
|
||||
|
||||
def push_graph(r, key, active_node, status, delay):
|
||||
nodes = []
|
||||
for n in NODES:
|
||||
if n == active_node:
|
||||
nodes.append({"id": n, "status": status})
|
||||
elif NODES.index(n) < NODES.index(active_node):
|
||||
nodes.append({"id": n, "status": "done"})
|
||||
else:
|
||||
nodes.append({"id": n, "status": "pending"})
|
||||
push(r, key, {"event": "graph_update", "nodes": nodes})
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def push_stats(r, key, **overrides):
|
||||
base = {
|
||||
"event": "stats_update",
|
||||
"frames_extracted": 0, "frames_after_scene_filter": 0,
|
||||
"regions_detected": 0, "regions_resolved_by_ocr": 0,
|
||||
"regions_escalated_to_local_vlm": 0, "regions_escalated_to_cloud_llm": 0,
|
||||
"cloud_llm_calls": 0, "processing_time_seconds": 0, "estimated_cloud_cost_usd": 0,
|
||||
}
|
||||
base.update(overrides)
|
||||
push(r, key, base)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", default="timeline-cost-test")
|
||||
parser.add_argument("--port", type=int, default=6382)
|
||||
parser.add_argument("--delay", type=float, default=0.4)
|
||||
args = parser.parse_args()
|
||||
|
||||
r = redis.Redis(port=args.port, decode_responses=True)
|
||||
key = f"detect_events:{args.job}"
|
||||
|
||||
r.delete(key)
|
||||
|
||||
logger.info("Pushing %d detections to %s", len(DETECTIONS), key)
|
||||
logger.info("Open: http://mpr.local.ar/detection/?job=%s", args.job)
|
||||
input("\nPress Enter to start...")
|
||||
|
||||
delay = args.delay
|
||||
|
||||
# Pipeline stages with progressive stats
|
||||
push_graph(r, key, "extract_frames", "running", delay)
|
||||
push_stats(r, key, frames_extracted=120, processing_time_seconds=3.2)
|
||||
push_graph(r, key, "extract_frames", "done", delay)
|
||||
|
||||
push_graph(r, key, "filter_scenes", "running", delay)
|
||||
push_stats(r, key, frames_extracted=120, frames_after_scene_filter=45, processing_time_seconds=5.1)
|
||||
push_graph(r, key, "filter_scenes", "done", delay)
|
||||
|
||||
push_graph(r, key, "detect_objects", "running", delay)
|
||||
push_stats(r, key, frames_extracted=120, frames_after_scene_filter=45,
|
||||
regions_detected=38, processing_time_seconds=12.4)
|
||||
push_graph(r, key, "detect_objects", "done", delay)
|
||||
|
||||
push_graph(r, key, "run_ocr", "running", delay)
|
||||
push_stats(r, key, frames_extracted=120, frames_after_scene_filter=45,
|
||||
regions_detected=38, regions_resolved_by_ocr=28, processing_time_seconds=18.7)
|
||||
push_graph(r, key, "run_ocr", "done", delay)
|
||||
|
||||
# Brand matching — push detections one by one
|
||||
push_graph(r, key, "match_brands", "running", delay)
|
||||
|
||||
for i, (brand, conf, source, timestamp, duration) in enumerate(DETECTIONS):
|
||||
if source != "ocr":
|
||||
continue
|
||||
push(r, key, {"event": "detection",
|
||||
"brand": brand, "confidence": conf, "source": source,
|
||||
"timestamp": timestamp, "duration": duration,
|
||||
"content_type": "soccer_broadcast", "frame_ref": i * 3})
|
||||
logger.info("[%d] %s %.2f %s t=%.1fs", i + 1, brand, conf, source, timestamp)
|
||||
time.sleep(delay * 0.3)
|
||||
|
||||
push_graph(r, key, "match_brands", "done", delay)
|
||||
|
||||
# VLM escalation
|
||||
push_graph(r, key, "escalate_vlm", "running", delay)
|
||||
push(r, key, {"event": "log", "level": "INFO", "stage": "VLMLocal",
|
||||
"msg": "Processing 3 unresolved crops with moondream2"})
|
||||
time.sleep(delay)
|
||||
|
||||
for i, (brand, conf, source, timestamp, duration) in enumerate(DETECTIONS):
|
||||
if source != "local_vlm":
|
||||
continue
|
||||
push(r, key, {"event": "detection",
|
||||
"brand": brand, "confidence": conf, "source": source,
|
||||
"timestamp": timestamp, "duration": duration,
|
||||
"content_type": "soccer_broadcast", "frame_ref": i * 3})
|
||||
logger.info("[vlm] %s %.2f t=%.1fs", brand, conf, timestamp)
|
||||
time.sleep(delay * 0.3)
|
||||
|
||||
push_stats(r, key, frames_extracted=120, frames_after_scene_filter=45,
|
||||
regions_detected=38, regions_resolved_by_ocr=28,
|
||||
regions_escalated_to_local_vlm=3, processing_time_seconds=25.1,
|
||||
estimated_cloud_cost_usd=0)
|
||||
push_graph(r, key, "escalate_vlm", "done", delay)
|
||||
|
||||
# Cloud escalation
|
||||
push_graph(r, key, "escalate_cloud", "running", delay)
|
||||
|
||||
for i, (brand, conf, source, timestamp, duration) in enumerate(DETECTIONS):
|
||||
if source != "cloud_llm":
|
||||
continue
|
||||
push(r, key, {"event": "detection",
|
||||
"brand": brand, "confidence": conf, "source": source,
|
||||
"timestamp": timestamp, "duration": duration,
|
||||
"content_type": "soccer_broadcast", "frame_ref": i * 3})
|
||||
logger.info("[cloud] %s %.2f t=%.1fs", brand, conf, timestamp)
|
||||
time.sleep(delay * 0.3)
|
||||
|
||||
push_stats(r, key, frames_extracted=120, frames_after_scene_filter=45,
|
||||
regions_detected=38, regions_resolved_by_ocr=28,
|
||||
regions_escalated_to_local_vlm=3, regions_escalated_to_cloud_llm=2,
|
||||
cloud_llm_calls=2, processing_time_seconds=31.4,
|
||||
estimated_cloud_cost_usd=0.0042)
|
||||
push_graph(r, key, "escalate_cloud", "done", delay)
|
||||
|
||||
# Report
|
||||
push_graph(r, key, "compile_report", "running", delay)
|
||||
push(r, key, {"event": "log", "level": "INFO", "stage": "Aggregator",
|
||||
"msg": f"Report: {len(set(d[0] for d in DETECTIONS))} brands, {len(DETECTIONS)} detections"})
|
||||
push_graph(r, key, "compile_report", "done", delay)
|
||||
|
||||
push(r, key, {"event": "job_complete", "job_id": args.job, "report": {
|
||||
"video_source": "soccer_clip.mp4",
|
||||
"content_type": "soccer_broadcast",
|
||||
"duration_seconds": 60.0,
|
||||
}})
|
||||
|
||||
logger.info("Done. Check Timeline (brand bars over time) and Cost & Stats panels.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
tests/detect/test_tracing.py
Normal file
34
tests/detect/test_tracing.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Tests for Langfuse tracing — works without Langfuse configured (no-op mode)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from detect.tracing import trace_node, SpanContext, flush
|
||||
|
||||
|
||||
def test_trace_node_noop():
|
||||
"""Without LANGFUSE_SECRET_KEY, tracing is a no-op but doesn't error."""
|
||||
state = {"job_id": "test-job", "profile_name": "soccer_broadcast"}
|
||||
|
||||
with trace_node(state, "extract_frames") as span:
|
||||
assert isinstance(span, SpanContext)
|
||||
span.set_output({"frames": 42})
|
||||
|
||||
assert span.metadata["frames"] == 42
|
||||
assert span.metadata["status"] == "ok"
|
||||
assert "duration_seconds" in span.metadata
|
||||
|
||||
|
||||
def test_trace_node_error():
|
||||
"""Span records error status on exception."""
|
||||
state = {"job_id": "test-job"}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with trace_node(state, "bad_node") as span:
|
||||
raise ValueError("boom")
|
||||
|
||||
assert span.metadata["status"] == "error"
|
||||
|
||||
|
||||
def test_flush_noop():
|
||||
"""Flush works when Langfuse is not configured."""
|
||||
flush()
|
||||
Reference in New Issue
Block a user