Showing
1 changed file
with
141 additions
and
0 deletions
| @@ -69,3 +69,144 @@ class FootballReplayMatch: | @@ -69,3 +69,144 @@ class FootballReplayMatch: | ||
| 69 | logger.info(f'LLM判断不是进球,无需匹配') | 69 | logger.info(f'LLM判断不是进球,无需匹配') |
| 70 | else: | 70 | else: |
| 71 | logger.info(f'通过时间找到匹配的事件') | 71 | logger.info(f'通过时间找到匹配的事件') |
| 72 | + | ||
| 73 | + | ||
| 74 | + | ||
| 75 | +import json | ||
| 76 | +import os | ||
| 77 | +from pathlib import Path | ||
| 78 | + | ||
| 79 | +try: | ||
| 80 | + from .llm_image import Video2Frame, frames2content | ||
| 81 | + from ..util.football_replay_match_live import FootballReplayMatchLive | ||
| 82 | + from config import settings | ||
| 83 | +except ImportError: | ||
| 84 | + import sys | ||
| 85 | + sys.path.insert(0, str(Path(__file__).parent.parent)) | ||
| 86 | + from core.llm_image import Video2Frame, frames2content | ||
| 87 | + from util.football_replay_match_live import FootballReplayMatchLive | ||
| 88 | + from config import settings | ||
| 89 | + | ||
| 90 | + | ||
| 91 | +# 从配置中读取参数(配置项需在 config.yaml 或对应环境配置中定义) | ||
| 92 | +_cache_dir = settings.common.get('cache_dir', None) | ||
| 93 | +_llm_base_url = settings.llm.get('base_url', None) | ||
| 94 | +_llm_model = settings.llm.get('model_name', None) | ||
| 95 | +_llm_temperature = settings.llm.get('temperature', None) | ||
| 96 | + | ||
| 97 | +# 初始化视频处理与匹配器 | ||
| 98 | +v2f = Video2Frame(_cache_dir) | ||
| 99 | +matcher = FootballReplayMatchLive( | ||
| 100 | + base_url=_llm_base_url, | ||
| 101 | + model=_llm_model, | ||
| 102 | + temperature=_llm_temperature, | ||
| 103 | +) | ||
| 104 | + | ||
| 105 | + | ||
| 106 | +def replay_match_event(data: dict) -> dict: | ||
| 107 | + """ | ||
| 108 | + 足球回看与直播事件匹配接口。 | ||
| 109 | + | ||
| 110 | + 请求参数: | ||
| 111 | + id : str 任务唯一键 | ||
| 112 | + match_id : str 比赛id | ||
| 113 | + replay : dict 回看信息 | ||
| 114 | + |- id : str 回看id | ||
| 115 | + |- url : str 回看url(m3u8) | ||
| 116 | + |- start_utc : int 回看开始utc时间 | ||
| 117 | + |- end_utc : int 回看结束utc时间 | ||
| 118 | + events : list 赛事事件片段(直播候选) | ||
| 119 | + |- id : str 事件id | ||
| 120 | + |- type : str 事件类型(1:进球) | ||
| 121 | + |- url : str 完整视频url(m3u8) | ||
| 122 | + |- event_utc : int 事件发生的utc时间点 | ||
| 123 | + | ||
| 124 | + 响应参数: | ||
| 125 | + id : str 任务唯一键 | ||
| 126 | + match_id : str 比赛id | ||
| 127 | + replay_id : str 回看id | ||
| 128 | + event_id : str|null 匹配到的比赛片段id,null表示无匹配 | ||
| 129 | + """ | ||
| 130 | + task_id = data.get("id") | ||
| 131 | + match_id = data.get("match_id") | ||
| 132 | + replay = data.get("replay", {}) | ||
| 133 | + events = data.get("events", []) | ||
| 134 | + | ||
| 135 | + replay_id = replay.get("id") | ||
| 136 | + replay_url = replay.get("url") | ||
| 137 | + replay_start = replay.get("start_utc") | ||
| 138 | + replay_end = replay.get("end_utc") | ||
| 139 | + | ||
| 140 | + if not replay_url or replay_start is None or replay_end is None: | ||
| 141 | + raise ValueError("replay 参数不完整,需要 url、start_utc、end_utc") | ||
| 142 | + | ||
| 143 | + # 1. 为回看视频提取帧并构建 LLM content | ||
| 144 | + try: | ||
| 145 | + replay_frames = v2f.to_frames( | ||
| 146 | + url=replay_url, | ||
| 147 | + start_utc=replay_start, | ||
| 148 | + end_utc=replay_end, | ||
| 149 | + fps=2, | ||
| 150 | + ) | ||
| 151 | + replay_contents = frames2content(replay_frames) | ||
| 152 | + # 保持与 football_replay_match_live.py 中一致的提示结构 | ||
| 153 | + replay_contents.insert(0, {"type": "text", "text": "\n【回放片段信息】\n"}) | ||
| 154 | + replay_contents.append({"type": "text", "text": "\n回放解说内容:无\n"}) | ||
| 155 | + except Exception as e: | ||
| 156 | + raise RuntimeError(f"回看视频处理失败: {e}") | ||
| 157 | + | ||
| 158 | + # 2. 为每个 event(直播候选)提取帧并构建 LLM content | ||
| 159 | + # event 的 url 为完整视频,通过 event_utc 向前延长10秒、向后延长5秒截取片段 | ||
| 160 | + live_videos = [] | ||
| 161 | + for event in events: | ||
| 162 | + event_id = event.get("id") | ||
| 163 | + event_url = event.get("url") | ||
| 164 | + event_utc = event.get("event_utc") | ||
| 165 | + | ||
| 166 | + if not event_url or event_utc is None: | ||
| 167 | + continue | ||
| 168 | + | ||
| 169 | + try: | ||
| 170 | + event_start = event_utc - 10 | ||
| 171 | + event_end = event_utc + 5 | ||
| 172 | + event_frames = v2f.to_frames( | ||
| 173 | + url=event_url, | ||
| 174 | + start_utc=event_start, | ||
| 175 | + end_utc=event_end, | ||
| 176 | + fps=2, | ||
| 177 | + ) | ||
| 178 | + event_contents = frames2content(event_frames) | ||
| 179 | + event_contents.insert( | ||
| 180 | + 0, {"type": "text", "text": f"### 候选片段 video_id: {event_id} ###"} | ||
| 181 | + ) | ||
| 182 | + event_contents.append({"type": "text", "text": "\n该片段解说内容: 无\n"}) | ||
| 183 | + except Exception: | ||
| 184 | + # 单个 event 处理失败不影响整体流程 | ||
| 185 | + continue | ||
| 186 | + | ||
| 187 | + live_videos.append({ | ||
| 188 | + "video_id": event_id, | ||
| 189 | + "video_path": "", # 已提供 llm_contents,无需本地路径 | ||
| 190 | + "asr_text": "", | ||
| 191 | + "llm_contents": event_contents, | ||
| 192 | + }) | ||
| 193 | + | ||
| 194 | + # 3. 执行匹配 | ||
| 195 | + if len(live_videos) == 0: | ||
| 196 | + matched_event_id = None | ||
| 197 | + elif len(live_videos) == 1: | ||
| 198 | + # 只有一个候选片段,直接视为匹配结果 | ||
| 199 | + matched_event_id = live_videos[0]["video_id"] | ||
| 200 | + else: | ||
| 201 | + try: | ||
| 202 | + result = matcher._match_batch(replay_contents, live_videos, max_parallel=3) | ||
| 203 | + matched_event_id = result.get("video_id") if result else None | ||
| 204 | + except Exception: | ||
| 205 | + matched_event_id = None | ||
| 206 | + | ||
| 207 | + return { | ||
| 208 | + "id": task_id, | ||
| 209 | + "match_id": match_id, | ||
| 210 | + "replay_id": replay_id, | ||
| 211 | + "event_id": matched_event_id, | ||
| 212 | + } |
-
Please register or login to post a comment