feat(ai-service): 摄像头流抽帧检测骨架与监控指标(#23/#26)

This commit is contained in:
weijuesen
2026-08-13 15:55:17 +08:00
parent 5b88ed2618
commit 156b670b3d
3 changed files with 99 additions and 1 deletions
+85 -1
View File
@@ -1,9 +1,30 @@
from fastapi import FastAPI, File, HTTPException, UploadFile import shutil
import subprocess
import threading
import time
from fastapi import FastAPI, File, HTTPException, Request, UploadFile
from . import config from . import config
from .detector import MockDetector, ONNXDetector from .detector import MockDetector, ONNXDetector
app = FastAPI(title="Silk AI Service", version="0.1.0") app = FastAPI(title="Silk AI Service", version="0.1.0")
START_TIME = time.time()
_lock = threading.Lock()
_requests = 0
_latency_total = 0.0
@app.middleware("http")
async def count_requests(request: Request, call_next):
global _requests, _latency_total
start = time.time()
response = await call_next(request)
latency = (time.time() - start) * 1000
with _lock:
_requests += 1
_latency_total += latency
return response
if config.MODEL_MODE == "onnx": if config.MODEL_MODE == "onnx":
detector = ONNXDetector(config.MODEL_PATH, tuple(config.MODEL_LABELS)) detector = ONNXDetector(config.MODEL_PATH, tuple(config.MODEL_LABELS))
@@ -36,3 +57,66 @@ async def detect(file: UploadFile = File(...)):
for d in detections for d in detections
], ],
} }
try:
import cv2 # noqa: E402
except ImportError:
cv2 = None
@app.post("/stream-detect")
async def stream_detect(body: dict):
"""摄像头流 AI 巡检骨架(#23):拉流抽帧 → 检测。
二期功能:正式接入前需补摄像头视角数据与巡检任务编排。
"""
url = (body or {}).get("url") or ""
if not url:
raise HTTPException(status_code=400, detail="缺少 urlrtsp/http-flv 流地址)")
if cv2 is None:
raise HTTPException(status_code=503, detail="OpenCV 未安装,无法拉流")
capture = cv2.VideoCapture(url)
if not capture.isOpened():
raise HTTPException(status_code=502, detail="无法连接视频流")
try:
frames = 0
detections = []
while frames < 3:
ok, frame = capture.read()
if not ok:
break
ok_encode, buf = cv2.imencode(".jpg", frame)
if ok_encode:
detections.extend(detector.detect(buf.tobytes()))
frames += 1
finally:
capture.release()
return {"model": config.MODEL_MODE, "frames": frames, "detections": detections[:10]}
@app.get("/metrics")
def metrics():
"""AI 服务监控(#26 骨架):请求量/延迟/GPU 信息(nvidia-smi 可选)。"""
with _lock:
reqs = _requests
avg = _latency_total / _requests if _requests else 0.0
gpu = None
if shutil.which("nvidia-smi"):
try:
out = subprocess.run(
["nvidia-smi", "--query-gpu=name,temperature.gpu,memory.used,memory.total", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=10,
)
if out.returncode == 0 and out.stdout.strip():
parts = [p.strip() for p in out.stdout.strip().split(",")]
gpu = {"name": parts[0], "tempC": parts[1], "memUsedMB": parts[2], "memTotalMB": parts[3]}
except Exception:
gpu = None
return {
"model": config.MODEL_MODE,
"uptimeSeconds": int(time.time() - START_TIME),
"requests": reqs,
"avgLatencyMs": round(avg, 2),
"gpu": gpu,
}
+1
View File
@@ -7,3 +7,4 @@ pydantic>=2.5
python-multipart>=0.0.9 python-multipart>=0.0.9
pytest>=8.0 pytest>=8.0
httpx>=0.27 httpx>=0.27
opencv-python-headless>=4.8
+13
View File
@@ -40,3 +40,16 @@ def test_detect_empty_file_rejected():
def test_detect_invalid_image_rejected(): def test_detect_invalid_image_rejected():
r = client.post("/detect", files={"file": ("a.png", b"junk", "image/png")}) r = client.post("/detect", files={"file": ("a.png", b"junk", "image/png")})
assert r.status_code == 400 assert r.status_code == 400
def test_stream_detect_missing_url():
r = client.post("/stream-detect", json={})
assert r.status_code == 400
def test_metrics_shape():
r = client.get("/metrics")
assert r.status_code == 200
body = r.json()
for key in ("model", "uptimeSeconds", "requests", "avgLatencyMs", "gpu"):
assert key in body