wangdongxu

jira:NYJ-1550 desc:m3u8 images

@@ -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 + }