lizhengwei

jira:NYJ-1540 desc: not think

1 import os 1 import os
  2 +import time
2 import logging 3 import logging
3 from aabd.base.enhance_dict import value_or_default 4 from aabd.base.enhance_dict import value_or_default
4 5
@@ -25,6 +26,8 @@ class FootballReplayMatch: @@ -25,6 +26,8 @@ class FootballReplayMatch:
25 def match_by_time(self, replay, events): 26 def match_by_time(self, replay, events):
26 27
27 start_utc = replay.get('start_utc') 28 start_utc = replay.get('start_utc')
  29 + if start_utc is None:
  30 + return None
28 31
29 events = [(start_utc - e.get('event_utc'), e) for e in events if e.get('event_utc') is not None] 32 events = [(start_utc - e.get('event_utc'), e) for e in events if e.get('event_utc') is not None]
30 if len(events) == 0: 33 if len(events) == 0:
@@ -79,30 +82,36 @@ class FootballReplayMatch: @@ -79,30 +82,36 @@ class FootballReplayMatch:
79 # 该时间段附近是否有进球事件 82 # 该时间段附近是否有进球事件
80 replay_info = data['replay'] 83 replay_info = data['replay']
81 replay_id = replay_info.get('id') 84 replay_id = replay_info.get('id')
  85 + replay_start_utc = replay_info.get('start_utc', None)
  86 + replay_end_utc = replay_info.get('end_utc', None)
82 live_events = data['events'] 87 live_events = data['events']
83 goal_live_events = [e for e in live_events if str(e.get('type', '')) == '1'] 88 goal_live_events = [e for e in live_events if str(e.get('type', '')) == '1']
84 89
85 matched_event = self.match_by_time(replay_info, goal_live_events) 90 matched_event = self.match_by_time(replay_info, goal_live_events)
86 # print(f"{task_id}_match_by_time matched_event: {matched_event}") 91 # print(f"{task_id}_match_by_time matched_event: {matched_event}")
87 if matched_event is None: 92 if matched_event is None:
  93 + start_time = time.time()
88 replay_det_info = self.det_goal_replay(replay_info, task_id) 94 replay_det_info = self.det_goal_replay(replay_info, task_id)
  95 + end_time = time.time()
  96 + logger.info(f"判断是否是进球耗时: {end_time - start_time} 秒")
  97 + print(f"判断是否是进球耗时: {end_time - start_time} 秒")
89 replay_event_name = replay_det_info.get('event_name', '') 98 replay_event_name = replay_det_info.get('event_name', '')
90 if replay_event_name == '进球': 99 if replay_event_name == '进球':
91 matched_event = self.match_by_llm(replay_info, goal_live_events, task_id) 100 matched_event = self.match_by_llm(replay_info, goal_live_events, task_id)
92 if matched_event is None: 101 if matched_event is None:
93 - logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")  
94 - # print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件") 102 + logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 是进球但是未能找到匹配的事件")
  103 + print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 是进球但是未能找到匹配的事件")
95 else: 104 else:
96 matched_event_id = matched_event.get('video_id') 105 matched_event_id = matched_event.get('video_id')
97 - logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")  
98 - # print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}") 106 + logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}")
  107 + print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}")
99 else: 108 else:
100 - logger.info(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")  
101 - # print(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配") 109 + logger.info(f"{task_id}_LLM判断{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 不是进球,无需匹配")
  110 + print(f"{task_id}_LLM判断{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 不是进球,无需匹配")
102 else: 111 else:
103 matched_event_id = matched_event.get('id') 112 matched_event_id = matched_event.get('id')
104 - logger.info(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")  
105 - # print(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}") 113 + logger.info(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}")
  114 + print(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}")
106 return { 115 return {
107 "id": task_id, 116 "id": task_id,
108 "match_id": match_id, 117 "match_id": match_id,
@@ -9,6 +9,7 @@ api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测 @@ -9,6 +9,7 @@ api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测
9 5. 无进球事件 / events 为空 9 5. 无进球事件 / events 为空
10 6. 边界:缺少 replay 键时抛出异常 10 6. 边界:缺少 replay 键时抛出异常
11 """ 11 """
  12 +import os
12 import time 13 import time
13 import unittest 14 import unittest
14 from types import SimpleNamespace 15 from types import SimpleNamespace
@@ -19,47 +20,65 @@ try: @@ -19,47 +20,65 @@ try:
19 except ImportError: 20 except ImportError:
20 from .api import FootballReplayMatch 21 from .api import FootballReplayMatch
21 22
  23 +from pathlib import Path
  24 +def get_video_files_pathlib(folder_path, extensions=None):
  25 + if extensions is None:
  26 + extensions = {'.mp4'}
  27 + else:
  28 + extensions = {ext.lower() for ext in extensions}
  29 +
  30 + folder = Path(folder_path)
  31 + image_files = [f.name for f in folder.iterdir()
  32 + if f.is_file() and f.suffix.lower() in extensions]
  33 + return sorted(image_files)
22 34
23 def _fake_settings(match_by_time_threshold: int = 30): 35 def _fake_settings(match_by_time_threshold: int = 30):
24 """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。""" 36 """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。"""
25 return SimpleNamespace( 37 return SimpleNamespace(
26 match_by_time_threshold=match_by_time_threshold, 38 match_by_time_threshold=match_by_time_threshold,
27 - 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),  
28 - common=SimpleNamespace(cache_dir="/root/lzw/tmp_0526_replay_cache0526/videos_cache", task_log_dir="/root/lzw/tmp_0526_replay_cache0526/tasks_log"), 39 + llm=SimpleNamespace(base_url="http://192.168.1.59:11434/v1",
  40 + model_name="Qwen3.6-35B-A3B-UD-Q8_K_XL.gguf",
  41 + temperature=0.7,
  42 + api_key='no_key'
  43 + ),
  44 + common=SimpleNamespace(cache_dir="/root/lzw/tmp_0527_replay_cache_08/videos_cache",
  45 + task_log_dir="/root/lzw/tmp_0527_replay_cache_08/tasks_log"
  46 + ),
29 save_frames_enable=True, 47 save_frames_enable=True,
30 ) 48 )
31 49
32 50
33 -def _base_data():  
34 - """返回一份基础伪数据,replay.start_utc = 1000(毫秒)。"""  
35 51
  52 +
  53 +def get_event_list():
36 # url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8" 54 # url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
  55 + events_mp4_dir = r"/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/goal"
  56 +
  57 + events_mp4_names = get_video_files_pathlib(events_mp4_dir)
  58 +
  59 + event_lis = []
  60 + for mp4 in events_mp4_names:
  61 + mp4_path = os.path.join(events_mp4_dir, mp4)
  62 + event_lis.append({
  63 + "id": f"event_{mp4.split('.')[0]}",
  64 + "type": "1",
  65 + "url": mp4_path,
  66 + # "event_utc": None
  67 + })
  68 + return event_lis
  69 +
  70 +def _base_data(i, replay_path, event_lis):
  71 +
37 return { 72 return {
38 - "id": "task_001",  
39 - "match_id": "match_001", 73 + "id": f"task_{str(i).zfill(3)}",
  74 + "match_id": f"match_{str(i).zfill(3)}",
40 "replay": { 75 "replay": {
41 - "id": "replay_001",  
42 - "url": "/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/replays/videos/00_14_32_080-00_15_10_560.mp4",  
43 - "start_utc": 5_000,  
44 - "end_utc": 21_000  
45 - },  
46 - "events": [  
47 - {"id": "event_001",  
48 - "type": "1",  
49 - "url": "/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/00-14-09-052.mp4",  
50 - "event_utc": None  
51 - },  
52 - {"id": "event_002",  
53 - "type": "1",  
54 - "url": "/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/00-38-40-052.mp4",  
55 - "event_utc": None 76 + "id": f"replay_{str(i).zfill(3)}",
  77 + "url": replay_path,
  78 + # "start_utc": None,
  79 + # "end_utc": None
56 }, 80 },
57 - {"id": "event_003",  
58 - "type": "1",  
59 - "url": "/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/01-36-04-972.mp4",  
60 - "event_utc": None  
61 - }  
62 - ], 81 + "events": event_lis,
63 } 82 }
64 83
65 84
@@ -68,18 +87,39 @@ class TestReplayMatchEvent(unittest.TestCase): @@ -68,18 +87,39 @@ class TestReplayMatchEvent(unittest.TestCase):
68 @patch("utils.football_replay_match_live.FootballReplayMatchLive") 87 @patch("utils.football_replay_match_live.FootballReplayMatchLive")
69 @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent") 88 @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
70 def test_match_by_time_success(self, mock_video_cls, mock_live_cls): 89 def test_match_by_time_success(self, mock_video_cls, mock_live_cls):
71 - """场景1:时间差在阈值内,直接通过 match_by_time 命中。""" 90 + # """场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
72 settings = _fake_settings(match_by_time_threshold=10) # threshold = 30 * 1000 = 30000 ms 91 settings = _fake_settings(match_by_time_threshold=10) # threshold = 30 * 1000 = 30000 ms
73 frm = FootballReplayMatch(settings) 92 frm = FootballReplayMatch(settings)
74 93
75 - data = _base_data() 94 + replays_mp4_dir = "/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/replay"
  95 + relays_mp4_names = get_video_files_pathlib(replays_mp4_dir)
  96 +
  97 + event_list = get_event_list()
  98 +
  99 + all_time = 0.0
  100 + max_time = 0.0
  101 + for i, replay_name in enumerate(relays_mp4_names):
  102 + print(100*"*" + f"replay_name: {replay_name}")
  103 + replay_path = os.path.join(replays_mp4_dir, replay_name)
  104 + data = _base_data(i, replay_path, event_list)
76 105
77 start_time = time.time() 106 start_time = time.time()
78 result = frm.replay_match_event(data) 107 result = frm.replay_match_event(data)
79 end_time = time.time() 108 end_time = time.time()
80 - print(f"cost time:{end_time - start_time}") 109 + cost_time = end_time - start_time
  110 + all_time += cost_time
  111 + print(f"replay_name: {replay_name}, cost time:{cost_time}")
  112 + if max_time < cost_time:
  113 + max_time = cost_time
81 114
82 print(result) 115 print(result)
  116 + print()
  117 + print()
  118 + print()
  119 +
  120 + print(100*"#")
  121 + print(f"max_time: {max_time}")
  122 + print(f"all_time: {all_time}")
83 # exit() 123 # exit()
84 124
85 # self.assertEqual(result["id"], "task_001") 125 # self.assertEqual(result["id"], "task_001")
  1 +import re
1 import json 2 import json
2 import os.path 3 import os.path
3 from pathlib import Path 4 from pathlib import Path
@@ -11,64 +12,103 @@ try: @@ -11,64 +12,103 @@ try:
11 except: 12 except:
12 from llm_image import Video2Frame 13 from llm_image import Video2Frame
13 14
  15 +# req_prompt = """
  16 +# # Role
  17 +# 你是一名拥有20年经验的足球视频技术分析师,擅长结合**视觉画面**与**解说音频(ASR)**进行跨镜头的事件匹配。你能通过解说员的描述锁定时间背景,并通过视觉特征确认物理细节。
  18 +
  19 +# # Task
  20 +# 输入包含:
  21 +# 1. 【回放片段】(Replay):包含视频帧 + **对应的解说文本**。
  22 +# 2. 【直播进球片段列表】(Live Candidates):包含视频帧 + **对应的解说文本**。
  23 +
  24 +# 目标:在【直播片段】中找到与【回放片段】属于**同一个进球事件**的片段。如果没有任何片段匹配,返回 null。
  25 +
  26 +# # Analysis Workflow (多模态分析流程)
  27 +
  28 +# 请按照以下步骤进行推理:
  29 +
  30 +# ### 第一步:解说词元数据提取 (听觉线索)
  31 +# 分析【回放片段】的解说文本,寻找以下关键信息:
  32 +# - **时间指代**:解说员是否提到了具体时间?(如“上半场”、“第10分钟”、“开场不久”)。
  33 +# - **事件描述**:解说员如何描述这个进球?(如“世界波”、“点球”、“补射”、“乌龙球”)。
  34 +# - **球员提及**:解说员念出了谁的名字?
  35 +
  36 +# ### 第二步:视觉物理指纹提取 (视觉线索)
  37 +# 忽略镜头语言(慢放、特写),提取核心物理特征:
  38 +# - **进攻与射门**:进攻方式(传中/直塞)、射门部位(头/脚)、射门位置。
  39 +# - **球路与防守**:球的轨迹(高/低/折射)、门将扑救动作(侧扑/倒地/未动)。
  40 +# - **庆祝**:进球后的庆祝动作(仅作为辅助验证)。
  41 +
  42 +# ### 第三步:跨模态匹配与验证
  43 +# 遍历【直播片段】,结合视觉和听觉进行判断:
  44 +# - **视觉一致性**:直播片段的动作、轨迹、门将反应是否与回放完全吻合?
  45 +# - **听觉一致性**:
  46 +# - 如果回放解说提到“这是第5分钟的进球”,而直播片段的时间戳是90分钟,**不要直接排除**,而是检查直播片段的解说是否也提到了“回顾第5分钟”或者直播片段的视觉内容确实是第5分钟的动作。
  47 +# - **核心原则**:视觉物理特征 > 解说词描述 > 时间戳元数据。
  48 +# - **特殊情况**:如果视觉特征高度相似(如同一个球员、同一个角度),但解说词明确说“这是另一个进球(例如:这是他的第二个进球,而回放说是第一个)”,则判定为不匹配。
  49 +
  50 +# # Logic Constraints (逻辑约束 - 必须严格遵守)
  51 +
  52 +# - **时间单向性原则(铁律)**:
  53 +# - **回看中的比赛时间一定在比赛片段画面时间之后**。
  54 +# - 逻辑:回放是对过去发生事件的回顾。如果【回放片段】中解说提到的比赛时间(或画面显示的比赛时钟)**早于**【直播片段】中解说提到的比赛时间(或画面时钟),则**绝对不可能**是同一个事件。
  55 +# - *示例*:回放解说在描述“第10分钟的进球”,而直播片段明确发生在“第5分钟”,则该直播片段**一定不是**目标。
  56 +# - **时间陷阱**:回放可能是赛后集锦。如果解说员说“让我们看看**刚才**那个球”或者“**上半场**那个球”,即使当前比赛时间是90分钟,也要去匹配对应时间段的直播片段(或视觉特征)。
  57 +# - **同名陷阱**:如果解说提到“又是**凯恩**进球了”,不能只看凯恩,必须看**怎么进的**(头球还是点球)。
  58 +# - **无匹配处理**:如果所有候选片段在视觉动作(如射门方式、进球位置)或关键事件逻辑上与回放明显不符,必须判定为无匹配,将 `video_id` 设为 `null`。
  59 +
  60 +# ### 输出要求
  61 +
  62 +# 请仅输出一个JSON格式的结果,不要输出任何分析过程。不要包含 markdown 标记(如 ```json ... ```),不要包含任何解释或额外文本。
  63 +# 格式如下:
  64 +# {
  65 +# "replay_summary": {
  66 +# "audio_cues": "解说提到的关键信息(如:'第15分钟', '远射', '德布劳内')",
  67 +# "visual_cues": "视觉关键特征(如:'禁区外右脚', '球挂死角', '门将飞身扑救')"
  68 +# },
  69 +# "reasoning": "综合分析:回放解说提到是'上半场的远射',视觉显示'17号球员禁区外起脚'。Candidate_1 视觉是'近距离推射',排除。Candidate_2 视觉是'禁区外远射',且门将动作一致,虽然直播时间显示是下半场(可能是集锦回顾),但解说也提到了'回顾上半场',确认为同一事件。",
  70 +# "video_id": "Candidate_2"
  71 +# }"""
  72 +
14 req_prompt = """ 73 req_prompt = """
15 # Role 74 # Role
16 -你是一名拥有20年经验的足球视频技术分析师,擅长结合**视觉画面**与**解说音频(ASR)**进行跨镜头的事件匹配。你能通过解说员的描述锁定时间背景,并通过视觉特征确认物理细节。 75 +你是一名拥有20年经验的足球视频技术分析师,擅长通过**纯视觉画面分析**进行跨镜头的事件匹配。你能精准捕捉球员动作、球衣号码、比赛时钟及比分牌等视觉细节。
17 76
18 # Task 77 # Task
19 输入包含: 78 输入包含:
20 -1. 【回放片段】(Replay):包含视频帧 + **对应的解说文本**。  
21 -2. 【直播进球片段列表】(Live Candidates):包含视频帧 + **对应的解说文本**。 79 +1. 【回放片段】(Replay):包含视频帧画面。
  80 +2. 【直播进球片段列表】(Live Candidates):包含视频帧画面。
22 81
23 目标:在【直播片段】中找到与【回放片段】属于**同一个进球事件**的片段。如果没有任何片段匹配,返回 null。 82 目标:在【直播片段】中找到与【回放片段】属于**同一个进球事件**的片段。如果没有任何片段匹配,返回 null。
24 83
25 -# Analysis Workflow (多模态分析流程)  
26 -  
27 -请按照以下步骤进行推理:  
28 -  
29 -### 第一步:解说词元数据提取 (听觉线索)  
30 -分析【回放片段】的解说文本,寻找以下关键信息:  
31 -- **时间指代**:解说员是否提到了具体时间?(如“上半场”、“第10分钟”、“开场不久”)。  
32 -- **事件描述**:解说员如何描述这个进球?(如“世界波”、“点球”、“补射”、“乌龙球”)。  
33 -- **球员提及**:解说员念出了谁的名字? 84 +# Visual Analysis Rules (纯视觉匹配规则)
34 85
35 -### 第二步:视觉物理指纹提取 (视觉线索)  
36 -忽略镜头语言(慢放、特写),提取核心物理特征:  
37 -- **进攻与射门**:进攻方式(传中/直塞)、射门部位(头/脚)、射门位置。  
38 -- **球路与防守**:球的轨迹(高/低/折射)、门将扑救动作(侧扑/倒地/未动)。  
39 -- **庆祝**:进球后的庆祝动作(仅作为辅助验证)。 86 +1. **核心物理特征比对(最高优先级)**:
  87 + - **球员身份**:进攻方球衣颜色、进球者号码、助攻者号码。
  88 + - **进球方式**:射门部位(左脚/右脚/头球)、射门位置(禁区内/禁区外/点球)、球路轨迹(贴地斩/世界波/挑射)。
  89 + - **防守反应**:门将的扑救动作(向左/向右/未动)、后卫的封堵动作。
  90 + - **庆祝动作**:进球后的标志性庆祝(如滑跪、指天、双手比心等)。
  91 + - *判定标准*:上述物理特征必须完全一致,否则视为不同进球。
40 92
41 -### 第三步:跨模态匹配与验证  
42 -遍历【直播片段】,结合视觉和听觉进行判断:  
43 -- **视觉一致性**:直播片段的动作、轨迹、门将反应是否与回放完全吻合?  
44 -- **听觉一致性**:  
45 - - 如果回放解说提到“这是第5分钟的进球”,而直播片段的时间戳是90分钟,**不要直接排除**,而是检查直播片段的解说是否也提到了“回顾第5分钟”或者直播片段的视觉内容确实是第5分钟的动作。  
46 - - **核心原则**:视觉物理特征 > 解说词描述 > 时间戳元数据。  
47 - - **特殊情况**:如果视觉特征高度相似(如同一个球员、同一个角度),但解说词明确说“这是另一个进球(例如:这是他的第二个进球,而回放说是第一个)”,则判定为不匹配。 93 +2. **画面内嵌元数据验证**:
  94 + - **比分牌逻辑**:检查回放画面中的比分(如 1-0)与直播片段进球前后的比分变化是否逻辑自洽。
  95 + - **比赛时钟**:虽然回放可能是赛后集锦(时间戳不准),但如果回放画面明确显示比赛时间(如 18:36),而直播片段发生在完全不同的时间段(如 85:00),且视觉特征不符,则排除。
48 96
49 -# Logic Constraints (逻辑约束 - 必须严格遵守) 97 +3. **无匹配处理**:
  98 + - 如果候选片段的进球者号码、射门方式或球衣颜色与回放不一致,必须判定为无匹配。
50 99
51 -- **时间单向性原则(铁律)**:  
52 - - **回看中的比赛时间一定在比赛片段画面时间之后**。  
53 - - 逻辑:回放是对过去发生事件的回顾。如果【回放片段】中解说提到的比赛时间(或画面显示的比赛时钟)**早于**【直播片段】中解说提到的比赛时间(或画面时钟),则**绝对不可能**是同一个事件。  
54 - - *示例*:回放解说在描述“第10分钟的进球”,而直播片段明确发生在“第5分钟”,则该直播片段**一定不是**目标。  
55 -- **时间陷阱**:回放可能是赛后集锦。如果解说员说“让我们看看**刚才**那个球”或者“**上半场**那个球”,即使当前比赛时间是90分钟,也要去匹配对应时间段的直播片段(或视觉特征)。  
56 -- **同名陷阱**:如果解说提到“又是**凯恩**进球了”,不能只看凯恩,必须看**怎么进的**(头球还是点球)。  
57 -- **无匹配处理**:如果所有候选片段在视觉动作(如射门方式、进球位置)或关键事件逻辑上与回放明显不符,必须判定为无匹配,将 `video_id` 设为 `null`。 100 +# Output Format (输出要求)
58 101
59 -### 输出要求 102 +请**仅**输出一个标准的 JSON 对象,**不要**包含任何推理过程(reasoning)、Markdown 标记(如 ```json)或额外的解释文本。
60 103
61 -请仅输出一个JSON格式的结果,不要输出任何分析过程。不要包含 markdown 标记(如 ```json ... ```),不要包含任何解释或额外文本。  
62 -格式如下: 104 +JSON 格式如下:
63 { 105 {
64 "replay_summary": { 106 "replay_summary": {
65 - "audio_cues": "解说提到的关键信息(如:'第15分钟', '远射', '德布劳内')",  
66 - "visual_cues": "视觉关键特征(如:'禁区外右脚', '球挂死角', '门将飞身扑救')" 107 + "visual_cues": "提取回放画面的核心特征(如:'蓝队6号禁区外远射', '门将扑救不及', '比分变为1-1')"
67 }, 108 },
68 - "reasoning": "综合分析:回放解说提到是'上半场的远射',视觉显示'17号球员禁区外起脚'。Candidate_1 视觉是'近距离推射',排除。Candidate_2 视觉是'禁区外远射',且门将动作一致,虽然直播时间显示是下半场(可能是集锦回顾),但解说也提到了'回顾上半场',确认为同一事件。",  
69 - "video_id": "Candidate_2"  
70 -}"""  
71 - 109 + "video_id": "匹配到的直播片段ID,如果没有匹配则填 null"
  110 +}
  111 +"""
72 112
73 class FootballReplayMatchLive: 113 class FootballReplayMatchLive:
74 def __init__(self, base_url: str, model: str, temperature: float = 0.0, api_key: str = 'no_key', cache_dir:str=None, save_frames_enable:bool=False): 114 def __init__(self, base_url: str, model: str, temperature: float = 0.0, api_key: str = 'no_key', cache_dir:str=None, save_frames_enable:bool=False):
@@ -81,7 +121,12 @@ class FootballReplayMatchLive: @@ -81,7 +121,12 @@ class FootballReplayMatchLive:
81 # self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7, 121 # self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7,
82 # api_key='no_key') 122 # api_key='no_key')
83 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key, 123 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
84 - extra_body={"chat_template_kwargs": {"enable_thinking": False}}) 124 +
  125 + model_kwargs={
  126 + "extra_body": {"enable_thinking": False}
  127 + }
  128 + )
  129 + #extra_body={"chat_template_kwargs": {"enable_thinking": False}})
85 self.cache_dir = cache_dir 130 self.cache_dir = cache_dir
86 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable) 131 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
87 132
@@ -91,12 +136,12 @@ class FootballReplayMatchLive: @@ -91,12 +136,12 @@ class FootballReplayMatchLive:
91 return None 136 return None
92 elif len(live_videos) == 1: 137 elif len(live_videos) == 1:
93 live_video_path = live_videos[0].get("url", None) 138 live_video_path = live_videos[0].get("url", None)
94 - live_video_id = live_videos[0].get("video_id", os.path.basename(live_video_path))  
95 - asr_text = live_videos[0].get("asr_text", '') 139 + live_video_id = live_videos[0].get("id", os.path.basename(live_video_path))
  140 + # asr_text = live_videos[0].get("asr_text", '')
96 live = { 141 live = {
97 "video_id": live_video_id, 142 "video_id": live_video_id,
98 - "video_path": live_video_path,  
99 - "asr_text": asr_text, 143 + "url": live_video_path,
  144 + # "asr_text": asr_text,
100 } 145 }
101 if record is not None: 146 if record is not None:
102 record.append({"live": live, "llm_result": None, 'live_list': [live]}) 147 record.append({"live": live, "llm_result": None, 'live_list': [live]})
@@ -107,9 +152,9 @@ class FootballReplayMatchLive: @@ -107,9 +152,9 @@ class FootballReplayMatchLive:
107 live_records = {} 152 live_records = {}
108 for live_video in live_videos: 153 for live_video in live_videos:
109 live_video_path = live_video.get("url", None) 154 live_video_path = live_video.get("url", None)
110 - live_video_id = live_video.get("video_id", os.path.basename(live_video_path)) 155 + live_video_id = live_video.get("id", os.path.basename(live_video_path))
111 event_utc = live_video.get("event_utc", None) 156 event_utc = live_video.get("event_utc", None)
112 - asr_text = live_video.get("asr_text", '') 157 + # asr_text = live_video.get("asr_text", '')
113 live_video_contents = live_video.get("llm_contents", None) 158 live_video_contents = live_video.get("llm_contents", None)
114 if live_video_contents is None: 159 if live_video_contents is None:
115 # event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None 160 # event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
@@ -124,38 +169,67 @@ class FootballReplayMatchLive: @@ -124,38 +169,67 @@ class FootballReplayMatchLive:
124 roi=None, 169 roi=None,
125 max_px_area=400_000, 170 max_px_area=400_000,
126 prompt_start=f"### 候选片段 video_id: {live_video_id} ###", 171 prompt_start=f"### 候选片段 video_id: {live_video_id} ###",
127 - prompt_end=f"\n该片段解说内容: {asr_text}\n" 172 + prompt_end=f"\n该片段解说内容: \n"
128 ) 173 )
129 live_map[live_video_id] = { 174 live_map[live_video_id] = {
130 "video_id": live_video_id, 175 "video_id": live_video_id,
131 - "video_path": live_video_path, 176 + "url": live_video_path,
132 "event_utc": event_utc, 177 "event_utc": event_utc,
133 - "asr_text": asr_text, 178 + # "asr_text": asr_text,
134 "contents": live_video_contents, 179 "contents": live_video_contents,
135 180
136 } 181 }
137 live_records[live_video_id] = { 182 live_records[live_video_id] = {
138 "video_id": live_video_id, 183 "video_id": live_video_id,
139 - "video_path": live_video_path, 184 + "url": live_video_path,
140 "event_utc": event_utc, 185 "event_utc": event_utc,
141 - "asr_text": asr_text, 186 + # "asr_text": asr_text,
142 } 187 }
143 live_videos_contents.extend(live_video_contents) 188 live_videos_contents.extend(live_video_contents)
144 189
145 user_contents.extend(replay_video_contents) 190 user_contents.extend(replay_video_contents)
146 user_contents.append({'type': "text", "text": "\n【候选直播片段列表】\n"}) 191 user_contents.append({'type': "text", "text": "\n【候选直播片段列表】\n"})
147 user_contents.extend(live_videos_contents) 192 user_contents.extend(live_videos_contents)
148 - user_contents.append({'type': "text", "text": "\n请根据上述片段进行匹配并按要求输出结果\n"}) 193 + user_contents.append({'type': "text", "text": "\n请根据上述片段进行匹配并按要求输出结果/no_think\n"})
149 system_message = SystemMessage(content=req_prompt) 194 system_message = SystemMessage(content=req_prompt)
150 user_message = HumanMessage(content=user_contents) 195 user_message = HumanMessage(content=user_contents)
151 result = self.model.invoke([system_message, user_message]).content 196 result = self.model.invoke([system_message, user_message]).content
  197 +
  198 +
  199 + print(100*"3")
  200 + print(result)
  201 + print(100*"4")
  202 + # try:
  203 + # result_json = json.loads(result)
  204 + # except json.JSONDecodeError:
  205 + # try:
  206 + # result_json = json.loads(result.replace("```json", "").replace("```", ""))
  207 + # except Exception as e:
  208 + # print("JSON解析失败:", result)
  209 + # raise e
  210 +
152 try: 211 try:
153 - result_json = json.loads(result)  
154 - except json.JSONDecodeError:  
155 - try:  
156 - result_json = json.loads(result.replace("```json", "").replace("```", "")) 212 + # 1. 深度清洗:去掉 Markdown 标记
  213 + cleaned_result = result.replace("```json", "").replace("```", "")
  214 +
  215 + # 2. 核心修复:使用 re.DOTALL 确保正则能跨行匹配,精准提取最外层完整的 JSON 对象
  216 + # 这样可以防止因为模型输出换行导致提取不完整
  217 + json_match = re.search(r'(\{.*\})', cleaned_result, re.DOTALL)
  218 +
  219 + if json_match:
  220 + json_str = json_match.group(1)
  221 + # 截断第一个 } 之后的所有内容,彻底干掉 <time_location> 等尾巴
  222 + json_str = json_str[:json_str.rfind('}') + 1]
  223 + else:
  224 + # 如果连花括号都找不到,直接抛出原始内容方便排查
  225 + print("无法提取JSON,原始内容:", result)
  226 + raise ValueError("响应中未找到有效的 JSON 对象")
  227 +
  228 + # 3. 使用 strict=False 容忍字符串中的非法控制字符(如直接换行)
  229 + result_json = json.loads(json_str, strict=False)
  230 +
157 except Exception as e: 231 except Exception as e:
158 - print("JSON解析失败:", result) 232 + print("JSON解析依然失败,清洗后的内容:", json_str if 'json_str' in locals() else result)
159 raise e 233 raise e
160 234
161 video_id = result_json.get("video_id", None) 235 video_id = result_json.get("video_id", None)
@@ -233,9 +307,9 @@ class FootballReplayMatchLive: @@ -233,9 +307,9 @@ class FootballReplayMatchLive:
233 if result is not None: 307 if result is not None:
234 result_no_content = { 308 result_no_content = {
235 "video_id": result.get("video_id", None), 309 "video_id": result.get("video_id", None),
236 - "video_path": result.get("video_path", None), 310 + "url": result.get("url", None),
237 "event_utc": result.get("event_utc", None), 311 "event_utc": result.get("event_utc", None),
238 - "asr_text": result.get("asr_text", None), 312 + # "asr_text": result.get("asr_text", None),
239 } 313 }
240 else: 314 else:
241 result_no_content = None 315 result_no_content = None
@@ -76,15 +76,20 @@ class FootballReplayVideoEvent: @@ -76,15 +76,20 @@ class FootballReplayVideoEvent:
76 # self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7, 76 # self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7,
77 # api_key='no_key') 77 # api_key='no_key')
78 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key, 78 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
79 - extra_body={"chat_template_kwargs": {"enable_thinking": False}}) 79 + model_kwargs={
  80 + "extra_body": {"enable_thinking": False}
  81 + }
  82 + )
  83 + # extra_body={"enable_thinking": False})
  84 + # extra_body={"chat_template_kwargs": {"enable_thinking": False}})
80 85
81 86
82 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable) 87 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
83 88
84 def video_event(self, replay_pack: dict, asr_text: str = '无', cache_dir=None, task_log_dir=None): 89 def video_event(self, replay_pack: dict, asr_text: str = '无', cache_dir=None, task_log_dir=None):
85 replay_video_path = replay_pack.get("url", None) 90 replay_video_path = replay_pack.get("url", None)
86 -  
87 - cache_path = os.path.join(task_log_dir, 'video_event.json') 91 + video_name = os.path.basename(replay_video_path)
  92 + cache_path = os.path.join(task_log_dir, f'{video_name}_replay.json')
88 if cache_path is not None and os.path.exists(cache_path): 93 if cache_path is not None and os.path.exists(cache_path):
89 with open(cache_path, 'r', encoding='utf-8') as f: 94 with open(cache_path, 'r', encoding='utf-8') as f:
90 return json.loads(f.read()) 95 return json.loads(f.read())
@@ -104,8 +109,8 @@ class FootballReplayVideoEvent: @@ -104,8 +109,8 @@ class FootballReplayVideoEvent:
104 109
105 system_message = SystemMessage(content=req_prompt) 110 system_message = SystemMessage(content=req_prompt)
106 video_message = HumanMessage(content=contents) 111 video_message = HumanMessage(content=contents)
107 - asr_message = HumanMessage(content=f"解说内容:{asr_text}")  
108 - result = self.model.invoke([system_message, video_message, asr_message]).content 112 + # asr_message = HumanMessage(content=f"解说内容:{asr_text}")
  113 + result = self.model.invoke([system_message, video_message]).content
109 try: 114 try:
110 result_json = json.loads(result) 115 result_json = json.loads(result)
111 except json.JSONDecodeError: 116 except json.JSONDecodeError: