138 lines
3.8 KiB
Python
138 lines
3.8 KiB
Python
import asyncio
|
|
|
|
import httpx
|
|
|
|
from app import video
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, body):
|
|
self.body = body
|
|
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return self.body
|
|
|
|
|
|
class _FakeClient:
|
|
responses = []
|
|
payloads = []
|
|
|
|
def __init__(self, **_):
|
|
pass
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_):
|
|
return None
|
|
|
|
async def post(self, _, json):
|
|
self.payloads.append(json)
|
|
return _FakeResponse(self.responses.pop(0))
|
|
|
|
|
|
def test_vision_request_retries_empty_content(monkeypatch):
|
|
_FakeClient.responses = [
|
|
{
|
|
"done_reason": "length",
|
|
"eval_count": 1024,
|
|
"message": {"content": "", "thinking": "reasoning"},
|
|
},
|
|
{
|
|
"done_reason": "stop",
|
|
"eval_count": 20,
|
|
"message": {"content": '{"passed": true}', "thinking": ""},
|
|
},
|
|
]
|
|
_FakeClient.payloads = []
|
|
monkeypatch.setattr(httpx, "AsyncClient", _FakeClient)
|
|
payload = {
|
|
"think": False,
|
|
"messages": [{"role": "user", "content": "Analyze"}],
|
|
"options": {"num_predict": 1024},
|
|
}
|
|
|
|
content, metadata = asyncio.run(video._request_vision_model(payload))
|
|
|
|
assert content == '{"passed": true}'
|
|
assert metadata["doneReason"] == "stop"
|
|
assert len(_FakeClient.payloads) == 2
|
|
assert _FakeClient.payloads[0]["think"] is False
|
|
assert _FakeClient.payloads[1]["options"]["num_predict"] == 1536
|
|
|
|
|
|
def test_fast_result_quality_controls_accurate_fallback():
|
|
complete = {
|
|
"passed": True,
|
|
"conclusion": "通过",
|
|
"summary": "状态正常",
|
|
"events": [],
|
|
"warnings": [],
|
|
}
|
|
|
|
assert video._fallback_reason(complete, True) is None
|
|
assert video._fallback_reason({**complete, "passed": None}, True) == "FAST_RESULT_INSUFFICIENT"
|
|
assert video._fallback_reason({**complete, "passed": None}, False) is None
|
|
assert video._fallback_reason({"summary": "缺少字段"}, True).startswith(
|
|
"FAST_RESULT_MISSING_FIELDS:"
|
|
)
|
|
|
|
|
|
def test_auto_mode_falls_back_to_accurate_result(monkeypatch, tmp_path):
|
|
frame_dir = tmp_path / "frames"
|
|
frame_dir.mkdir()
|
|
frame = frame_dir / "frame-001.jpg"
|
|
frame.write_bytes(b"frame")
|
|
calls = []
|
|
|
|
monkeypatch.setattr(
|
|
video,
|
|
"_extract_frames",
|
|
lambda *_: ([frame], 2.0, frame_dir),
|
|
)
|
|
|
|
async def fake_analyze(_, mode, *args):
|
|
calls.append(mode)
|
|
if mode == "FAST":
|
|
return (
|
|
{
|
|
"passed": None,
|
|
"conclusion": "证据不足",
|
|
"summary": "快速分析无法判断",
|
|
"events": [],
|
|
"warnings": [],
|
|
},
|
|
{"mode": "FAST", "model": "fast", "sampledFrameCount": 1, "maximumWidth": 896},
|
|
)
|
|
return (
|
|
{
|
|
"passed": True,
|
|
"conclusion": "通过",
|
|
"summary": "精确分析确认通过",
|
|
"events": [],
|
|
"warnings": [],
|
|
},
|
|
{"mode": "ACCURATE", "model": "accurate", "sampledFrameCount": 1, "maximumWidth": 1280},
|
|
)
|
|
|
|
monkeypatch.setattr(video, "_analyze_frames", fake_analyze)
|
|
profile_dir = tmp_path / "profile"
|
|
profile_dir.mkdir()
|
|
(profile_dir / "prompt.txt").write_text("Analyze", encoding="utf-8")
|
|
profile = video.Profile(
|
|
"test.video.v1",
|
|
"VIDEO_ANALYSIS",
|
|
profile_dir,
|
|
{"fallbackWhenPassedUnknown": True},
|
|
)
|
|
|
|
result, _, metadata = asyncio.run(video.analyze_video(profile, tmp_path / "video.mp4"))
|
|
|
|
assert calls == ["FAST", "ACCURATE"]
|
|
assert result["passed"] is True
|
|
assert metadata["fallback"] is True
|
|
assert metadata["fallbackReason"] == "FAST_RESULT_INSUFFICIENT"
|