feat(ai-service): 摄像头流抽帧检测骨架与监控指标(#23/#26)
This commit is contained in:
+85
-1
@@ -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="缺少 url(rtsp/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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user