lizhengwei

jira:NYJ-1540 desc: add api_test.py

import os
import logging
from aabd.base.enhance_dict import value_or_default
... ... @@ -18,25 +19,31 @@ class FootballReplayMatch:
save_frames_enable = value_or_default(settings.save_frames_enable, False)
self.videoEventRecognition = FootballReplayVideoEvent(llm_base_url, model_name, temperature, 'no_key', self.cache_dir, save_frames_enable)
self.videoMatchLive = FootballReplayMatchLive(llm_base_url, model_name, temperature, 'no_key', self.cache_dir)
self.videoMatchLive = FootballReplayMatchLive(llm_base_url, model_name, temperature, 'no_key', self.cache_dir, save_frames_enable)
def match_by_time(self, replay, events):
start_utc = replay.get('start_utc')
events = [(start_utc - e.get('event_utc'), e) for e in events]
events.sort(key=lambda x: abs(x[0]))
time_diff, event = events[0]
events.sort(key=lambda x: x[0])
target_index = next((i for i, e in enumerate(events) if e[0] > 0), -1) # 找到第一个 event_utc 早于 start_utc 的事件
if target_index == -1:
return None
time_diff, event = events[target_index]
if time_diff < self.match_by_time_threshold:
return event
else:
return None
def det_goal_replay(self, replay):
return self.videoEventRecognition.video_event(replay, cache_dir=self.cache_dir)
def det_goal_replay(self, replay, task_id):
return self.videoEventRecognition.video_event(replay, cache_dir=os.path.join(self.cache_dir, task_id, "replay"))
def match_by_llm(self, replay, events):
return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=self.cache_dir)
def match_by_llm(self, replay, events, task_id):
return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=os.path.join(self.cache_dir, task_id, "envents"))
def replay_match_event(self, data):
"""
... ... @@ -72,22 +79,25 @@ class FootballReplayMatch:
goal_live_events = [e for e in live_events if str(e.get('type', '')) == '1']
matched_event = self.match_by_time(replay_info, goal_live_events)
if matched_event is None:
replay_det_info = self.det_goal_replay(replay_info)
replay_det_info = self.det_goal_replay(replay_info, task_id)
replay_event_name = replay_det_info.get('event_name', '')
if replay_event_name == '进球':
matched_event = self.match_by_llm(replay_info, goal_live_events)
matched_event = self.match_by_llm(replay_info, goal_live_events, task_id)
if matched_event is None:
logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")
# print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")
else:
matched_event_id = matched_event.get('video_id')
logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
# print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
else:
logger.info(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")
# print(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")
else:
matched_event_id = matched_event.get('id')
logger.info(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
# print(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
return {
"id": task_id,
"match_id": match_id,
... ...
"""
api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测试
使用伪数据覆盖主要分支:
1. 时间阈值内直接匹配成功
2. 时间匹配失败 + LLM 判定不是进球
3. 时间匹配失败 + LLM 判定是进球 + LLM 匹配成功
4. 时间匹配失败 + LLM 判定是进球 + LLM 匹配失败
5. 无进球事件 / events 为空
6. 边界:缺少 replay 键时抛出异常
"""
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
try:
from api import FootballReplayMatch
except ImportError:
from .api import FootballReplayMatch
def _fake_settings(match_by_time_threshold: int = 30):
"""构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。"""
return SimpleNamespace(
match_by_time_threshold=match_by_time_threshold,
llm=SimpleNamespace(base_url="http://192.168.1.59:11434/v1", model_name="Qwen3.6-35B-A3B-UD-Q8_K_XL.gguf", temperature=0.7),
common=SimpleNamespace(cache_dir="/root/lzw/tmp_0518_replay_cache0521_01"),
save_frames_enable=True,
)
def _base_data():
"""返回一份基础伪数据,replay.start_utc = 1000(毫秒)。"""
url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
return {
"id": "task_001",
"match_id": "match_001",
"replay": {
"id": "replay_001",
"url": url,
"start_utc": 1777444502543,
"end_utc": 1777444531063
},
"events": [
{"id": "event_001", "type": "1", "url": url, "event_utc": 1777444502543-40000},
{"id": "event_001", "type": "1", "url": url, "event_utc": 1777444502543-20000},
{"id": "event_001", "type": "1", "url": url, "event_utc": 1777444531063+20000}
],
}
class TestReplayMatchEvent(unittest.TestCase):
@patch("utils.football_replay_match_live.FootballReplayMatchLive")
@patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
def test_match_by_time_success(self, mock_video_cls, mock_live_cls):
"""场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
settings = _fake_settings(match_by_time_threshold=10) # threshold = 30 * 1000 = 30000 ms
frm = FootballReplayMatch(settings)
data = _base_data()
result = frm.replay_match_event(data)
print(result)
# exit()
# self.assertEqual(result["id"], "task_001")
# self.assertEqual(result["match_id"], "match_001")
# self.assertEqual(result["replay_id"], "replay_001")
# # event_001 与 replay.start_utc 差值 20 ms < 30000 ms,应命中 event_001 的 id
# self.assertEqual(result["event_id"], "event_001")
# # 未调用 LLM 相关方法
# mock_video_cls.return_value.video_event.assert_not_called()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_not_goal(self, mock_video_cls, mock_live_cls):
# """场景2:时间匹配失败,det_goal_replay 判定不是进球。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "宣传片"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_goal_then_llm_match(self, mock_video_cls, mock_live_cls):
# """场景3:时间匹配失败 -> 判定是进球 -> LLM 匹配成功。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "世界波"}
# mock_video_cls.return_value = mock_video_instance
# mock_live_instance = MagicMock()
# mock_live_instance.match_batch.return_value = {"video_id": "event_003", "video_path": "http://example.com/event3.mp4", "asr_text": ""}
# mock_live_cls.return_value = mock_live_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], "event_003")
# mock_video_instance.video_event.assert_called_once()
# mock_live_instance.match_batch.assert_called_once()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_goal_then_llm_no_match(self, mock_video_cls, mock_live_cls):
# """场景4:时间匹配失败 -> 判定是进球 -> LLM 未匹配到任何事件。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "头球破门"}
# mock_video_cls.return_value = mock_video_instance
# mock_live_instance = MagicMock()
# mock_live_instance.match_batch.return_value = None
# mock_live_cls.return_value = mock_live_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_instance.match_batch.assert_called_once()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_no_goal_events(self, mock_video_cls, mock_live_cls):
# """场景5:events 中没有任何 type='1' 的进球事件。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "无候选事件"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# data["events"] = [
# {"id": "event_005", "type": "2", "url": "http://example.com/event5.mp4", "event_utc": 1020},
# ]
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_empty_events(self, mock_video_cls, mock_live_cls):
# """场景6:events 为空列表。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "空列表"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# data["events"] = []
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_missing_replay_key(self, mock_video_cls, mock_live_cls):
# """场景7:data 缺少 replay 键,应抛出 KeyError。"""
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# del data["replay"]
# with self.assertRaises(KeyError):
# frm.replay_match_event(data)
if __name__ == "__main__":
unittest.main()
... ...
... ... @@ -82,7 +82,6 @@ class FootballReplayMatchLive:
# api_key='no_key')
self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
extra_body={"chat_template_kwargs": {"enable_thinking": False}})
self.cache_dir = cache_dir
self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
... ... @@ -117,8 +116,10 @@ class FootballReplayMatchLive:
asr_text = live_video.get("asr_text", '')
live_video_contents = live_video.get("llm_contents", None)
if live_video_contents is None:
event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
event_end = event_utc + timedelta(seconds=5) if event_utc is not None else None
# event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
event_start = event_utc - 10000 if event_utc is not None else None
# event_end = event_utc + timedelta(seconds=5) if event_utc is not None else None
event_end = event_utc + 5000 if event_utc is not None else None
live_video_contents = self.video2frame.to_llm_contents( live_video_path,
cache=self.cache_dir,
fps=2,
... ... @@ -132,6 +133,7 @@ class FootballReplayMatchLive:
live_map[live_video_id] = {
"video_id": live_video_id,
"video_path": live_video_path,
"event_utc": event_utc,
"asr_text": asr_text,
"contents": live_video_contents,
... ... @@ -139,6 +141,7 @@ class FootballReplayMatchLive:
live_records[live_video_id] = {
"video_id": live_video_id,
"video_path": live_video_path,
"event_utc": event_utc,
"asr_text": asr_text,
}
live_videos_contents.extend(live_video_contents)
... ... @@ -207,6 +210,7 @@ class FootballReplayMatchLive:
def match_batch(self, replay_video: dict, live_videos: list[dict], max_parallel: int = 3, cache_dir=None):
self.cache_dir = cache_dir
cache_path = os.path.join(cache_dir, 'match_live.json')
if cache_path is not None and os.path.exists(cache_path):
try:
... ... @@ -219,7 +223,7 @@ class FootballReplayMatchLive:
event_start = replay_video.get("start_utc", None)
event_end = replay_video.get("end_utc", None)
replay_video_contents = self.video2frame.to_llm_contents(replay_video_path,
cache=self.cache_dir,
cache=os.path.join(os.path.dirname(cache_dir), "replay"),
fps=2,
start=event_start,
end=event_end,
... ... @@ -234,6 +238,7 @@ class FootballReplayMatchLive:
result_no_content = {
"video_id": result.get("video_id", None),
"video_path": result.get("video_path", None),
"event_utc": result.get("event_utc", None),
"asr_text": result.get("asr_text", None),
}
else:
... ...
... ... @@ -78,7 +78,7 @@ class FootballReplayVideoEvent:
self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
extra_body={"chat_template_kwargs": {"enable_thinking": False}})
self.cache_dir = cache_dir
self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
def video_event(self, replay_pack: dict, asr_text: str = '无', cache_dir=None):
... ... @@ -92,7 +92,7 @@ class FootballReplayVideoEvent:
event_start = replay_pack.get("start_utc", None)
event_end = replay_pack.get("end_utc", None)
contents = self.video2frame.to_llm_contents(replay_video_path,
cache=self.cache_dir,
cache=cache_dir,
fps=2,
start=event_start,
end=event_end,
... ...
... ... @@ -87,7 +87,9 @@ class Video2Frame:
if cache_name.exists():
logger.info(f"use_cache: {cache_name}")
return json.load(cache_name.open('r'))
# return json.load(cache_name.open('r'))
with cache_name.open('r', encoding='utf-8') as f:
return json.load(f)
else:
cache_dir.mkdir(parents=True, exist_ok=True)
data = {
... ... @@ -117,7 +119,9 @@ class Video2Frame:
cv2.imwrite(frame_path, frame)
cache_name.parent.mkdir(parents=True, exist_ok=True)
json.dump(data, cache_name.open('w'), indent=4)
# json.dump(data, cache_name.open('w'), indent=4)
with cache_name.open('w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
logger.info(f"save_cache: {cache_name}")
return data
... ...