lizhengwei

jira:NYJ-1540 desc: add api.py

@@ -193,6 +193,7 @@ files/ @@ -193,6 +193,7 @@ files/
193 cache_data/ 193 cache_data/
194 data/ 194 data/
195 test/ 195 test/
  196 +tests/
196 # Media files 197 # Media files
197 *.jpg 198 *.jpg
198 *.jpeg 199 *.jpeg
  1 +input_kafka:
  2 + servers: prod-kafka:9092
  3 + group_id: ai_match_service_prod
  4 + topic: football_replay_match_req
  5 + # username: xxx
  6 + # password: xxx
  7 +
  8 +output_kafka:
  9 + servers: prod-kafka:9092
  10 + topic: football_replay_match_resp
  11 +
  12 +cache_dir: /data/cache
  13 +llm_base_url: http://prod-llm:11434
  14 +llm_model: qwen3.6:35b-a3b-q8_0
  15 +llm_temperature: 0.7
1 -app_name: "Football Replay Service"  
2 -app_version: "1.0.0" 1 +input_kafka:
  2 + servers: 192.168.0.14:9092
  3 + group_id: ai_match_service
  4 + topic: football_replay_match_req
  5 + # username: xxx
  6 + # password: xxx
  7 +
  8 +output_kafka:
  9 + servers: 192.168.0.14:9092
  10 + topic: football_replay_match_resp
  11 +
  12 +
  13 +llm:
  14 + base_url: http://192.168.1.59:11434
  15 + model_name: qwen3.6:35b-a3b-q8_0
  16 + temperature: 0.7
  17 +
  18 +common:
  19 + cache_dir: ./cache
1 -from aabd.base.cfg_loader import load_yaml_by_file_with_env  
2 -from aabd.base.enhance_dict import EnhanceDict, read_prefixed_env_vars 1 +import os
3 from pathlib import Path 2 from pathlib import Path
4 from logging import getLogger 3 from logging import getLogger
  4 +
  5 +from aabd.base.cfg_loader import load_yaml_by_file_with_env
  6 +from aabd.base.enhance_dict import EnhanceDict, read_prefixed_env_vars
  7 +
5 logger = getLogger(__name__) 8 logger = getLogger(__name__)
6 cfg_dir = Path(__file__).parent.absolute() / 'cfg' 9 cfg_dir = Path(__file__).parent.absolute() / 'cfg'
7 -settings = EnhanceDict(load_yaml_by_file_with_env((cfg_dir / 'config.yaml').as_posix())) 10 +
  11 +# 加载基础配置
  12 +base_config = load_yaml_by_file_with_env((cfg_dir / 'config.yaml').as_posix())
  13 +
  14 +# 根据 ENV 环境变量加载环境特定配置并合并(如 config.dev.yaml / config.prod.yaml)
  15 +env = os.getenv('ENV', 'dev')
  16 +env_file = cfg_dir / f'config.{env}.yaml'
  17 +if env_file.exists():
  18 + env_config = load_yaml_by_file_with_env(env_file.as_posix())
  19 + if isinstance(base_config, dict) and isinstance(env_config, dict):
  20 + for key, value in env_config.items():
  21 + if key in base_config and isinstance(base_config[key], dict) and isinstance(value, dict):
  22 + base_config[key].update(value)
  23 + else:
  24 + base_config[key] = value
  25 + logger.info(f"已加载环境配置: {env_file.name}")
  26 +
  27 +settings = EnhanceDict(base_config)
8 settings.update(read_prefixed_env_vars('FRM_')) 28 settings.update(read_prefixed_env_vars('FRM_'))
9 29
10 -logger.info(f'Settings: {settings}')  
  30 +logger.info(f'Settings: {settings}')
1 -def replay_match_event(data):  
2 - pass  
  1 +import json
  2 +import os
  3 +from pathlib import Path
  4 +
  5 +try:
  6 + from .llm_image import Video2Frame, frames2content
  7 + from ..util.football_replay_match_live import FootballReplayMatchLive
  8 + from config import settings
  9 +except ImportError:
  10 + import sys
  11 + sys.path.insert(0, str(Path(__file__).parent.parent))
  12 + from core.llm_image import Video2Frame, frames2content
  13 + from util.football_replay_match_live import FootballReplayMatchLive
  14 + from config import settings
  15 +
  16 +
  17 +# 从配置中读取参数(配置项需在 config.yaml 或对应环境配置中定义)
  18 +_cache_dir = settings.common.get('cache_dir', None)
  19 +_llm_base_url = settings.llm.get('base_url', None)
  20 +_llm_model = settings.llm.get('model_name', None)
  21 +_llm_temperature = settings.llm.get('temperature', None)
  22 +
  23 +# 初始化视频处理与匹配器
  24 +v2f = Video2Frame(_cache_dir)
  25 +matcher = FootballReplayMatchLive(
  26 + base_url=_llm_base_url,
  27 + model=_llm_model,
  28 + temperature=_llm_temperature,
  29 +)
  30 +
  31 +
  32 +def replay_match_event(data: dict) -> dict:
  33 + """
  34 + 足球回看与直播事件匹配接口。
  35 +
  36 + 请求参数:
  37 + id : str 任务唯一键
  38 + match_id : str 比赛id
  39 + replay : dict 回看信息
  40 + |- id : str 回看id
  41 + |- url : str 回看url(m3u8)
  42 + |- start_utc : int 回看开始utc时间
  43 + |- end_utc : int 回看结束utc时间
  44 + events : list 赛事事件片段(直播候选)
  45 + |- id : str 事件id
  46 + |- type : str 事件类型(1:进球)
  47 + |- url : str 事件片段url(m3u8)
  48 + |- event_utc : int 事件发生的utc时间点
  49 +
  50 + 响应参数:
  51 + id : str 任务唯一键
  52 + match_id : str 比赛id
  53 + replay_id : str 回看id
  54 + event_id : str|null 匹配到的比赛片段id,null表示无匹配
  55 + """
  56 + task_id = data.get("id")
  57 + match_id = data.get("match_id")
  58 + replay = data.get("replay", {})
  59 + events = data.get("events", [])
  60 +
  61 + replay_id = replay.get("id")
  62 + replay_url = replay.get("url")
  63 + replay_start = replay.get("start_utc")
  64 + replay_end = replay.get("end_utc")
  65 +
  66 + if not replay_url or replay_start is None or replay_end is None:
  67 + raise ValueError("replay 参数不完整,需要 url、start_utc、end_utc")
  68 +
  69 + # 1. 为回看视频提取帧并构建 LLM content
  70 + try:
  71 + replay_frames = v2f.to_frames(
  72 + url=replay_url,
  73 + start_utc=replay_start,
  74 + end_utc=replay_end,
  75 + fps=2,
  76 + )
  77 + replay_contents = frames2content(replay_frames)
  78 + # 保持与 football_replay_match_live.py 中一致的提示结构
  79 + replay_contents.insert(0, {"type": "text", "text": "\n【回放片段信息】\n"})
  80 + replay_contents.append({"type": "text", "text": "\n回放解说内容:无\n"})
  81 + except Exception as e:
  82 + raise RuntimeError(f"回看视频处理失败: {e}")
  83 +
  84 + # 2. 为每个 event(直播候选)提取帧并构建 LLM content
  85 + # event 的 url 本身已是视频片段,直接处理完整片段
  86 + live_videos = []
  87 + for event in events:
  88 + event_id = event.get("id")
  89 + event_url = event.get("url")
  90 +
  91 + if not event_url:
  92 + continue
  93 +
  94 + try:
  95 + event_frames = v2f.to_frames(
  96 + url=event_url,
  97 + start_utc=0,
  98 + end_utc=999999, # 传极大值,实际以视频真实长度为准
  99 + fps=2,
  100 + )
  101 + event_contents = frames2content(event_frames)
  102 + event_contents.insert(
  103 + 0, {"type": "text", "text": f"### 候选片段 video_id: {event_id} ###"}
  104 + )
  105 + event_contents.append({"type": "text", "text": "\n该片段解说内容: 无\n"})
  106 + except Exception:
  107 + # 单个 event 处理失败不影响整体流程
  108 + continue
  109 +
  110 + live_videos.append({
  111 + "video_id": event_id,
  112 + "video_path": "", # 已提供 llm_contents,无需本地路径
  113 + "asr_text": "",
  114 + "llm_contents": event_contents,
  115 + })
  116 +
  117 + # 3. 执行匹配
  118 + if len(live_videos) == 0:
  119 + matched_event_id = None
  120 + elif len(live_videos) == 1:
  121 + # 只有一个候选片段,直接视为匹配结果
  122 + matched_event_id = live_videos[0]["video_id"]
  123 + else:
  124 + try:
  125 + result = matcher._match_batch(replay_contents, live_videos, max_parallel=3)
  126 + matched_event_id = result.get("video_id") if result else None
  127 + except Exception:
  128 + matched_event_id = None
  129 +
  130 + return {
  131 + "id": task_id,
  132 + "match_id": match_id,
  133 + "replay_id": replay_id,
  134 + "event_id": matched_event_id,
  135 + }
@@ -55,9 +55,11 @@ def kafka_message_iterator_thread(message_iterator, config, callback_func): @@ -55,9 +55,11 @@ def kafka_message_iterator_thread(message_iterator, config, callback_func):
55 for message in message_iterator: 55 for message in message_iterator:
56 try: 56 try:
57 json_message = json.loads(message) 57 json_message = json.loads(message)
58 - replay_match_event(json_message)  
59 - logger.info(message)  
60 - except: 58 + result = replay_match_event(json_message)
  59 + task_id = json_message.get("id")
  60 + callback_func(task_id, json.dumps(result, ensure_ascii=False))
  61 + logger.info(f"Processed task_id={task_id}, result={result}")
  62 + except Exception:
61 logger.exception(f"Error processing message: {message}") 63 logger.exception(f"Error processing message: {message}")
62 64
63 65