Showing
3 changed files
with
45 additions
and
190 deletions
| @@ -56,11 +56,21 @@ class FootballReplayMatch: | @@ -56,11 +56,21 @@ class FootballReplayMatch: | ||
| 56 | return None | 56 | return None |
| 57 | 57 | ||
| 58 | def det_goal_replay(self, replay, task_id): | 58 | def det_goal_replay(self, replay, task_id): |
| 59 | - return self.videoEventRecognition.video_event(replay, cache_dir=os.path.join(self.cache_dir, "replays"), task_log_dir=os.path.join(self.task_log_dir, task_id)) | 59 | + video_url = replay.get('url') |
| 60 | + if video_url.endswith('.m3u8'): | ||
| 61 | + mp4_name = os.path.basename(video_url) | ||
| 62 | + else: | ||
| 63 | + mp4_name = os.path.basename(os.path.dirname(os.path.dirname(os.path.dirname(video_url)))) | ||
| 64 | + return self.videoEventRecognition.video_event(replay, cache_dir=os.path.join(self.cache_dir, mp4_name, "replays"), task_log_dir=os.path.join(self.task_log_dir, mp4_name, task_id)) | ||
| 60 | 65 | ||
| 61 | 66 | ||
| 62 | def match_by_llm(self, replay, events, task_id): | 67 | def match_by_llm(self, replay, events, task_id): |
| 63 | - return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=os.path.join(self.cache_dir, "events"), task_log_dir=os.path.join(self.task_log_dir, task_id)) | 68 | + video_url = replay.get('url') |
| 69 | + if video_url.endswith('.m3u8'): | ||
| 70 | + mp4_name = os.path.basename(video_url) | ||
| 71 | + else: | ||
| 72 | + mp4_name = os.path.basename(os.path.dirname(os.path.dirname(os.path.dirname(video_url)))) | ||
| 73 | + return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=os.path.join(self.cache_dir, mp4_name, "events"), task_log_dir=os.path.join(self.task_log_dir, mp4_name, task_id)) | ||
| 64 | 74 | ||
| 65 | def replay_match_event(self, data): | 75 | def replay_match_event(self, data): |
| 66 | """ | 76 | """ |
| 1 | """ | 1 | """ |
| 2 | -api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测试 | 2 | +api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的测试 |
| 3 | 3 | ||
| 4 | 使用伪数据覆盖主要分支: | 4 | 使用伪数据覆盖主要分支: |
| 5 | 1. 时间阈值内直接匹配成功 | 5 | 1. 时间阈值内直接匹配成功 |
| @@ -20,10 +20,6 @@ try: | @@ -20,10 +20,6 @@ try: | ||
| 20 | except ImportError: | 20 | except ImportError: |
| 21 | from .api import FootballReplayMatch | 21 | from .api import FootballReplayMatch |
| 22 | 22 | ||
| 23 | - | ||
| 24 | - | ||
| 25 | - | ||
| 26 | - | ||
| 27 | from pathlib import Path | 23 | from pathlib import Path |
| 28 | def get_video_files_pathlib(folder_path, extensions=None): | 24 | def get_video_files_pathlib(folder_path, extensions=None): |
| 29 | if extensions is None: | 25 | if extensions is None: |
| @@ -36,17 +32,17 @@ def get_video_files_pathlib(folder_path, extensions=None): | @@ -36,17 +32,17 @@ def get_video_files_pathlib(folder_path, extensions=None): | ||
| 36 | if f.is_file() and f.suffix.lower() in extensions] | 32 | if f.is_file() and f.suffix.lower() in extensions] |
| 37 | return sorted(image_files) | 33 | return sorted(image_files) |
| 38 | 34 | ||
| 39 | -def _fake_settings(match_by_time_threshold: int = 30, cache_dir_tag: str = "test"): | 35 | +def _fake_settings(): |
| 40 | """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。""" | 36 | """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。""" |
| 41 | return SimpleNamespace( | 37 | return SimpleNamespace( |
| 42 | - match_by_time_threshold=match_by_time_threshold, | 38 | + match_by_time_threshold=30, |
| 43 | llm=SimpleNamespace(base_url="http://192.168.1.59:11434/v1", | 39 | llm=SimpleNamespace(base_url="http://192.168.1.59:11434/v1", |
| 44 | model_name="Qwen3.6-35B-A3B-UD-Q8_K_XL.gguf", | 40 | model_name="Qwen3.6-35B-A3B-UD-Q8_K_XL.gguf", |
| 45 | temperature=0.7, | 41 | temperature=0.7, |
| 46 | api_key='no_key' | 42 | api_key='no_key' |
| 47 | ), | 43 | ), |
| 48 | - common=SimpleNamespace(cache_dir=f"/root/lzw/tmp_0601_replay_cache_{cache_dir_tag}/videos_cache", | ||
| 49 | - task_log_dir=f"/root/lzw/tmp_0601_replay_cache_{cache_dir_tag}/tasks_log" | 44 | + common=SimpleNamespace(cache_dir=f"/root/lzw/tmp_0602_replay_cache/videos_cache", |
| 45 | + task_log_dir=f"/root/lzw/tmp_0602_replay_cache/tasks_log" | ||
| 50 | ), | 46 | ), |
| 51 | save_frames_enable=True, | 47 | save_frames_enable=True, |
| 52 | ) | 48 | ) |
| @@ -102,36 +98,21 @@ def _base_data(i, replay_path, event_lis): | @@ -102,36 +98,21 @@ def _base_data(i, replay_path, event_lis): | ||
| 102 | } | 98 | } |
| 103 | 99 | ||
| 104 | 100 | ||
| 105 | - | ||
| 106 | - | ||
| 107 | - | ||
| 108 | - | ||
| 109 | def test_match_by_time_success(): | 101 | def test_match_by_time_success(): |
| 110 | - # """场景1:时间差在阈值内,直接通过 match_by_time 命中。""" | ||
| 111 | - | 102 | + |
| 112 | replays_and_events_pair = get_replays_and_events_pair() | 103 | replays_and_events_pair = get_replays_and_events_pair() |
| 113 | - | ||
| 114 | for replays_mp4_dir, events_mp4_dir in replays_and_events_pair: | 104 | for replays_mp4_dir, events_mp4_dir in replays_and_events_pair: |
| 115 | # replays_mp4_dir = "/root/lzw/finished/69e5102bfd87d43d09bba90d-auto-10154993/replays/videos" | 105 | # replays_mp4_dir = "/root/lzw/finished/69e5102bfd87d43d09bba90d-auto-10154993/replays/videos" |
| 116 | # events_mp4_dir = r"/root/lzw/finished/69e5102bfd87d43d09bba90d-auto-10154993/live/videos" | 106 | # events_mp4_dir = r"/root/lzw/finished/69e5102bfd87d43d09bba90d-auto-10154993/live/videos" |
| 117 | print(100*"=") | 107 | print(100*"=") |
| 118 | print(f"replays_mp4_dir: {replays_mp4_dir}, events_mp4_dir: {events_mp4_dir}") | 108 | print(f"replays_mp4_dir: {replays_mp4_dir}, events_mp4_dir: {events_mp4_dir}") |
| 119 | 109 | ||
| 120 | - tag = os.path.basename(os.path.dirname(os.path.dirname(replays_mp4_dir))) | ||
| 121 | - settings = _fake_settings(match_by_time_threshold=30, cache_dir_tag=tag) # threshold = 30 * 1000 = 30000 ms | ||
| 122 | - | ||
| 123 | - | 110 | + settings = _fake_settings() |
| 124 | frm = FootballReplayMatch(settings) | 111 | frm = FootballReplayMatch(settings) |
| 125 | 112 | ||
| 126 | - | ||
| 127 | relays_mp4_names = get_video_files_pathlib(replays_mp4_dir) | 113 | relays_mp4_names = get_video_files_pathlib(replays_mp4_dir) |
| 128 | - | ||
| 129 | - | ||
| 130 | - | ||
| 131 | event_list = get_event_list(events_mp4_dir) | 114 | event_list = get_event_list(events_mp4_dir) |
| 132 | 115 | ||
| 133 | - | ||
| 134 | - | ||
| 135 | all_time = 0.0 | 116 | all_time = 0.0 |
| 136 | max_time = 0.0 | 117 | max_time = 0.0 |
| 137 | for i, replay_name in enumerate(relays_mp4_names): | 118 | for i, replay_name in enumerate(relays_mp4_names): |
| @@ -157,145 +138,5 @@ def test_match_by_time_success(): | @@ -157,145 +138,5 @@ def test_match_by_time_success(): | ||
| 157 | print(f"all_time: {all_time}") | 138 | print(f"all_time: {all_time}") |
| 158 | # exit() | 139 | # exit() |
| 159 | 140 | ||
| 160 | - | ||
| 161 | - | ||
| 162 | - | ||
| 163 | if __name__ == "__main__": | 141 | if __name__ == "__main__": |
| 164 | test_match_by_time_success() | 142 | test_match_by_time_success() |
| 165 | - | ||
| 166 | - | ||
| 167 | - | ||
| 168 | - | ||
| 169 | - | ||
| 170 | - | ||
| 171 | - | ||
| 172 | - | ||
| 173 | - | ||
| 174 | - | ||
| 175 | - | ||
| 176 | - | ||
| 177 | - | ||
| 178 | - | ||
| 179 | - # self.assertEqual(result["id"], "task_001") | ||
| 180 | - # self.assertEqual(result["match_id"], "match_001") | ||
| 181 | - # self.assertEqual(result["replay_id"], "replay_001") | ||
| 182 | - # # event_001 与 replay.start_utc 差值 20 ms < 30000 ms,应命中 event_001 的 id | ||
| 183 | - # self.assertEqual(result["event_id"], "event_001") | ||
| 184 | - # # 未调用 LLM 相关方法 | ||
| 185 | - # mock_video_cls.return_value.video_event.assert_not_called() | ||
| 186 | - # mock_live_cls.return_value.match_batch.assert_not_called() | ||
| 187 | - | ||
| 188 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 189 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 190 | - # def test_match_by_time_fail_then_not_goal(self, mock_video_cls, mock_live_cls): | ||
| 191 | - # """场景2:时间匹配失败,det_goal_replay 判定不是进球。""" | ||
| 192 | - # mock_video_instance = MagicMock() | ||
| 193 | - # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "宣传片"} | ||
| 194 | - # mock_video_cls.return_value = mock_video_instance | ||
| 195 | - | ||
| 196 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 197 | - # frm = FootballReplayMatch(settings) | ||
| 198 | - | ||
| 199 | - # data = _base_data() | ||
| 200 | - # result = frm.replay_match_event(data) | ||
| 201 | - | ||
| 202 | - # self.assertEqual(result["event_id"], None) | ||
| 203 | - # mock_video_instance.video_event.assert_called_once() | ||
| 204 | - # mock_live_cls.return_value.match_batch.assert_not_called() | ||
| 205 | - | ||
| 206 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 207 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 208 | - # def test_match_by_time_fail_then_goal_then_llm_match(self, mock_video_cls, mock_live_cls): | ||
| 209 | - # """场景3:时间匹配失败 -> 判定是进球 -> LLM 匹配成功。""" | ||
| 210 | - # mock_video_instance = MagicMock() | ||
| 211 | - # mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "世界波"} | ||
| 212 | - # mock_video_cls.return_value = mock_video_instance | ||
| 213 | - | ||
| 214 | - # mock_live_instance = MagicMock() | ||
| 215 | - # mock_live_instance.match_batch.return_value = {"video_id": "event_003", "video_path": "http://example.com/event3.mp4", "asr_text": ""} | ||
| 216 | - # mock_live_cls.return_value = mock_live_instance | ||
| 217 | - | ||
| 218 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 219 | - # frm = FootballReplayMatch(settings) | ||
| 220 | - | ||
| 221 | - # data = _base_data() | ||
| 222 | - # result = frm.replay_match_event(data) | ||
| 223 | - | ||
| 224 | - # self.assertEqual(result["event_id"], "event_003") | ||
| 225 | - # mock_video_instance.video_event.assert_called_once() | ||
| 226 | - # mock_live_instance.match_batch.assert_called_once() | ||
| 227 | - | ||
| 228 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 229 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 230 | - # def test_match_by_time_fail_then_goal_then_llm_no_match(self, mock_video_cls, mock_live_cls): | ||
| 231 | - # """场景4:时间匹配失败 -> 判定是进球 -> LLM 未匹配到任何事件。""" | ||
| 232 | - # mock_video_instance = MagicMock() | ||
| 233 | - # mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "头球破门"} | ||
| 234 | - # mock_video_cls.return_value = mock_video_instance | ||
| 235 | - | ||
| 236 | - # mock_live_instance = MagicMock() | ||
| 237 | - # mock_live_instance.match_batch.return_value = None | ||
| 238 | - # mock_live_cls.return_value = mock_live_instance | ||
| 239 | - | ||
| 240 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 241 | - # frm = FootballReplayMatch(settings) | ||
| 242 | - | ||
| 243 | - # data = _base_data() | ||
| 244 | - # result = frm.replay_match_event(data) | ||
| 245 | - | ||
| 246 | - # self.assertEqual(result["event_id"], None) | ||
| 247 | - # mock_video_instance.video_event.assert_called_once() | ||
| 248 | - # mock_live_instance.match_batch.assert_called_once() | ||
| 249 | - | ||
| 250 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 251 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 252 | - # def test_no_goal_events(self, mock_video_cls, mock_live_cls): | ||
| 253 | - # """场景5:events 中没有任何 type='1' 的进球事件。""" | ||
| 254 | - # mock_video_instance = MagicMock() | ||
| 255 | - # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "无候选事件"} | ||
| 256 | - # mock_video_cls.return_value = mock_video_instance | ||
| 257 | - | ||
| 258 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 259 | - # frm = FootballReplayMatch(settings) | ||
| 260 | - | ||
| 261 | - # data = _base_data() | ||
| 262 | - # data["events"] = [ | ||
| 263 | - # {"id": "event_005", "type": "2", "url": "http://example.com/event5.mp4", "event_utc": 1020}, | ||
| 264 | - # ] | ||
| 265 | - # result = frm.replay_match_event(data) | ||
| 266 | - | ||
| 267 | - # self.assertEqual(result["event_id"], None) | ||
| 268 | - # mock_video_instance.video_event.assert_called_once() | ||
| 269 | - # mock_live_cls.return_value.match_batch.assert_not_called() | ||
| 270 | - | ||
| 271 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 272 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 273 | - # def test_empty_events(self, mock_video_cls, mock_live_cls): | ||
| 274 | - # """场景6:events 为空列表。""" | ||
| 275 | - # mock_video_instance = MagicMock() | ||
| 276 | - # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "空列表"} | ||
| 277 | - # mock_video_cls.return_value = mock_video_instance | ||
| 278 | - | ||
| 279 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 280 | - # frm = FootballReplayMatch(settings) | ||
| 281 | - | ||
| 282 | - # data = _base_data() | ||
| 283 | - # data["events"] = [] | ||
| 284 | - # result = frm.replay_match_event(data) | ||
| 285 | - | ||
| 286 | - # self.assertEqual(result["event_id"], None) | ||
| 287 | - # mock_video_instance.video_event.assert_called_once() | ||
| 288 | - # mock_live_cls.return_value.match_batch.assert_not_called() | ||
| 289 | - | ||
| 290 | - # @patch("utils.football_replay_match_live.FootballReplayMatchLive") | ||
| 291 | - # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") | ||
| 292 | - # def test_missing_replay_key(self, mock_video_cls, mock_live_cls): | ||
| 293 | - # """场景7:data 缺少 replay 键,应抛出 KeyError。""" | ||
| 294 | - # settings = _fake_settings(match_by_time_threshold=30) | ||
| 295 | - # frm = FootballReplayMatch(settings) | ||
| 296 | - | ||
| 297 | - # data = _base_data() | ||
| 298 | - # del data["replay"] | ||
| 299 | - | ||
| 300 | - # with self.assertRaises(KeyError): | ||
| 301 | - # frm.replay_match_event(data) |
| @@ -218,29 +218,33 @@ class FootballReplayMatchLive: | @@ -218,29 +218,33 @@ class FootballReplayMatchLive: | ||
| 218 | if result.strip() == "null": | 218 | if result.strip() == "null": |
| 219 | result_json = {"video_id": None} | 219 | result_json = {"video_id": None} |
| 220 | else: | 220 | else: |
| 221 | - try: | ||
| 222 | - # 1. 深度清洗:去掉 Markdown 标记 | ||
| 223 | - cleaned_result = result.replace("```json", "").replace("```", "") | ||
| 224 | - | ||
| 225 | - # 2. 核心修复:使用 re.DOTALL 确保正则能跨行匹配,精准提取最外层完整的 JSON 对象 | ||
| 226 | - # 这样可以防止因为模型输出换行导致提取不完整 | ||
| 227 | - json_match = re.search(r'(\{.*\})', cleaned_result, re.DOTALL) | ||
| 228 | - | ||
| 229 | - if json_match: | ||
| 230 | - json_str = json_match.group(1) | ||
| 231 | - # 截断第一个 } 之后的所有内容,彻底干掉 <time_location> 等尾巴 | ||
| 232 | - json_str = json_str[:json_str.rfind('}') + 1] | ||
| 233 | - else: | ||
| 234 | - # 如果连花括号都找不到,直接抛出原始内容方便排查 | ||
| 235 | - print("无法提取JSON,原始内容:", result) | ||
| 236 | - raise ValueError("响应中未找到有效的 JSON 对象") | ||
| 237 | - | ||
| 238 | - # 3. 使用 strict=False 容忍字符串中的非法控制字符(如直接换行) | ||
| 239 | - result_json = json.loads(json_str, strict=False) | 221 | + |
| 222 | + # 1. 深度清洗:去掉 Markdown 标记 | ||
| 223 | + cleaned_result = result.replace("```json", "").replace("```", "") | ||
| 224 | + | ||
| 225 | + # 2. 核心修复:使用 re.DOTALL 确保正则能跨行匹配,精准提取最外层完整的 JSON 对象 | ||
| 226 | + # 这样可以防止因为模型输出换行导致提取不完整 | ||
| 227 | + json_match = re.search(r'(\{.*\})', cleaned_result, re.DOTALL) | ||
| 228 | + | ||
| 229 | + if json_match: | ||
| 230 | + json_str = json_match.group(1) | ||
| 231 | + # 截断第一个 } 之后的所有内容,彻底干掉 <time_location> 等尾巴 | ||
| 232 | + json_str = json_str[:json_str.rfind('}') + 1] | ||
| 233 | + else: | ||
| 234 | + # 如果连花括号都找不到,直接抛出原始内容方便排查 | ||
| 235 | + json_str = { | ||
| 236 | + "replay_summary": { | ||
| 237 | + "visual_cues": "" | ||
| 238 | + }, | ||
| 239 | + "video_id": "null" | ||
| 240 | + } | ||
| 241 | + logger.info("Unable_to_extract_JSON,original_content:", result) | ||
| 242 | + | ||
| 243 | + | ||
| 244 | + # 3. 使用 strict=False 容忍字符串中的非法控制字符(如直接换行) | ||
| 245 | + result_json = json.loads(json_str, strict=False) | ||
| 240 | 246 | ||
| 241 | - except Exception as e: | ||
| 242 | - print("JSON解析依然失败,清洗后的内容:", json_str if 'json_str' in locals() else result) | ||
| 243 | - raise e | 247 | + |
| 244 | 248 | ||
| 245 | video_id = result_json.get("video_id", None) | 249 | video_id = result_json.get("video_id", None) |
| 246 | result_live = live_map.get(video_id, None) | 250 | result_live = live_map.get(video_id, None) |
-
Please register or login to post a comment