lizhengwei

jira:NYJ-1540 desc: add video_event_for_contents func

@@ -122,7 +122,7 @@ class Video2Frame: @@ -122,7 +122,7 @@ class Video2Frame:
122 if not cap.isOpened(): 122 if not cap.isOpened():
123 raise ValueError(f"无法打开视频片段: {clip_path}") 123 raise ValueError(f"无法打开视频片段: {clip_path}")
124 124
125 - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 125 + # total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
126 video_fps = cap.get(cv2.CAP_PROP_FPS) 126 video_fps = cap.get(cv2.CAP_PROP_FPS)
127 if video_fps <= 0: 127 if video_fps <= 0:
128 video_fps = 25.0 128 video_fps = 25.0
@@ -132,10 +132,9 @@ class Video2Frame: @@ -132,10 +132,9 @@ class Video2Frame:
132 if frame_interval < 1: 132 if frame_interval < 1:
133 frame_interval = 1 133 frame_interval = 1
134 134
135 - frame_paths = [] 135 + frames = []
136 saved_count = 0 136 saved_count = 0
137 frame_count = 0 137 frame_count = 0
138 -  
139 while True: 138 while True:
140 ret, frame = cap.read() 139 ret, frame = cap.read()
141 if not ret: 140 if not ret:
@@ -145,27 +144,26 @@ class Video2Frame: @@ -145,27 +144,26 @@ class Video2Frame:
145 if roi is not None: 144 if roi is not None:
146 x, y, w, h = roi 145 x, y, w, h = roi
147 frame = frame[y:y + h, x:x + w] 146 frame = frame[y:y + h, x:x + w]
148 -  
149 - if max_px_area is not None:  
150 - h, w = frame.shape[:2]  
151 - area = h * w  
152 - if area > max_px_area:  
153 - scale = (max_px_area / area) ** 0.5  
154 - new_w = int(w * scale)  
155 - new_h = int(h * scale)  
156 - frame = cv2.resize(  
157 - frame, (new_w, new_h), interpolation=cv2.INTER_AREA  
158 - )  
159 - 147 + frame = self.reisize_frame(frame, max_px_area)
  148 + frames.append(frame)
160 frame_path = frame_output_dir / f"frame_{saved_count:06d}.jpg" 149 frame_path = frame_output_dir / f"frame_{saved_count:06d}.jpg"
161 cv2.imwrite(str(frame_path), frame) 150 cv2.imwrite(str(frame_path), frame)
162 - frame_paths.append(str(frame_path))  
163 saved_count += 1 151 saved_count += 1
164 -  
165 frame_count += 1 152 frame_count += 1
166 -  
167 cap.release() 153 cap.release()
168 - return frame_paths 154 + return frames
  155 +
  156 + def reisize_frame(self, frame, max_px_area):
  157 + if max_px_area is None:
  158 + return frame
  159 + h, w = frame.shape[:2]
  160 + area = h * w
  161 + if area > max_px_area:
  162 + scale = (max_px_area / area) ** 0.5
  163 + new_w = int(w * scale)
  164 + new_h = int(h * scale)
  165 + frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
  166 + return frame
169 167
170 @staticmethod 168 @staticmethod
171 def _find_frame_idx(utc_list, target_utc, find_last_not_greater=True): 169 def _find_frame_idx(utc_list, target_utc, find_last_not_greater=True):
@@ -241,28 +239,34 @@ def frames2content(frames): @@ -241,28 +239,34 @@ def frames2content(frames):
241 将帧列表(图片路径列表)转为 LLM content 列表。 239 将帧列表(图片路径列表)转为 LLM content 列表。
242 格式与 llm_video_content.py 中的 contents 返回的 content 一致。 240 格式与 llm_video_content.py 中的 contents 返回的 content 一致。
243 """ 241 """
244 - content = [] 242 + contents = []
245 video_prompt = ( 243 video_prompt = (
246 f"以下是从视频中按时间顺序提取的 {len(frames)} 帧画面," 244 f"以下是从视频中按时间顺序提取的 {len(frames)} 帧画面,"
247 f"请将它们视为一个连续的视频进行分析。" 245 f"请将它们视为一个连续的视频进行分析。"
248 ) 246 )
249 - content.append({"type": "text", "text": video_prompt}) 247 + contents.append({"type": "text", "text": video_prompt})
250 248
251 - for frame_path in frames:  
252 - with open(frame_path, "rb") as f:  
253 - img_bytes = f.read()  
254 - b64_str = base64.b64encode(img_bytes).decode("utf-8")  
255 - content.append({ 249 + for frame in frames:
  250 + _, buffer = cv2.imencode('.jpg', frame)
  251 + b64_str = base64.b64encode(buffer).decode('utf-8')
  252 +
  253 + # with open(frame_path, "rb") as f:
  254 + # img_bytes = f.read()
  255 + # b64_str = base64.b64encode(img_bytes).decode("utf-8")
  256 +
  257 + contents.append({
256 "type": "image_url", 258 "type": "image_url",
257 "image_url": { 259 "image_url": {
258 "url": f"data:image/jpeg;base64,{b64_str}" 260 "url": f"data:image/jpeg;base64,{b64_str}"
259 } 261 }
260 }) 262 })
261 263
262 - return content 264 + return contents
263 265
264 266
265 if __name__ == "__main__": 267 if __name__ == "__main__":
  268 + # pass
  269 +
266 import argparse 270 import argparse
267 import sys 271 import sys
268 import tempfile 272 import tempfile
@@ -275,7 +279,7 @@ if __name__ == "__main__": @@ -275,7 +279,7 @@ if __name__ == "__main__":
275 ) 279 )
276 parser.add_argument( 280 parser.add_argument(
277 "--cache-dir", 281 "--cache-dir",
278 - default=None, 282 + default=r"C:\Users\lzw\AppData\Local\Temp\video2frame_test_mn26tcy_my",
279 help="缓存目录(默认自动创建临时目录)", 283 help="缓存目录(默认自动创建临时目录)",
280 ) 284 )
281 parser.add_argument( 285 parser.add_argument(
@@ -293,7 +297,7 @@ if __name__ == "__main__": @@ -293,7 +297,7 @@ if __name__ == "__main__":
293 parser.add_argument( 297 parser.add_argument(
294 "--fps", 298 "--fps",
295 type=float, 299 type=float,
296 - default=1.0, 300 + default=2.0,
297 help="抽帧帧率(默认 1.0)", 301 help="抽帧帧率(默认 1.0)",
298 ) 302 )
299 args = parser.parse_args() 303 args = parser.parse_args()
@@ -314,11 +318,13 @@ if __name__ == "__main__": @@ -314,11 +318,13 @@ if __name__ == "__main__":
314 start_utc=args.start, 318 start_utc=args.start,
315 end_utc=args.end, 319 end_utc=args.end,
316 fps=args.fps, 320 fps=args.fps,
  321 + roi=None,
  322 + max_px_area=200_000,
317 ) 323 )
318 print(f" 成功提取 {len(frames)} 帧") 324 print(f" 成功提取 {len(frames)} 帧")
319 if frames: 325 if frames:
320 - print(f" 首帧: {frames[0]}")  
321 - print(f" 末帧: {frames[-1]}") 326 + print(f" 首帧: {frames[0].shape}")
  327 + print(f" 末帧: {frames[-1].shape}")
322 except Exception as e: 328 except Exception as e:
323 print(f" 失败: {e}") 329 print(f" 失败: {e}")
324 import traceback 330 import traceback
@@ -340,27 +346,46 @@ if __name__ == "__main__": @@ -340,27 +346,46 @@ if __name__ == "__main__":
340 if len(content) > 3: 346 if len(content) > 3:
341 print(f" ... 还有 {len(content) - 3} 个元素") 347 print(f" ... 还有 {len(content) - 3} 个元素")
342 348
343 - # TEST 3: ROI + max_px_area  
344 - print("\n[TEST 3] to_frames with roi + max_px_area")  
345 - try:  
346 - end_crop = min(args.start + 5, args.end)  
347 - frames_cropped = v2f.to_frames(  
348 - url=args.url,  
349 - start_utc=args.start,  
350 - end_utc=end_crop,  
351 - fps=1.0,  
352 - roi=None,  
353 - max_px_area=200_000,  
354 - )  
355 - print(f" 成功提取 {len(frames_cropped)} 帧(带 ROI 裁剪和面积缩放)")  
356 - except Exception as e:  
357 - print(f" 跳过/失败: {e}") 349 + print("\n[TEST 3] FootballReplayVideoEvent 进球识别(可选)")
358 350
359 - # TEST 4: SEI UTC 模式说明(仅演示,需真实含 SEI 的流才能运行)  
360 - print("\n[TEST 4] SEI UTC 绝对时间戳模式(示例代码,需替换为含 SEI 的流)")  
361 - print(  
362 - " # 示例:假设视频第一帧 UTC 为 1715600000,提取 10 秒片段\n"  
363 - " # frames = v2f.to_frames(url, start_utc=1715600000, end_utc=1715600010, fps=1)" 351 + try:
  352 + from ..util.football_replay_video_event_by_llm import FootballReplayVideoEvent
  353 + except ImportError:
  354 + import sys
  355 + from pathlib import Path
  356 + sys.path.insert(0, str(Path(__file__).parent.parent))
  357 + from util.football_replay_video_event_by_llm import FootballReplayVideoEvent
  358 +
  359 + fbrv = FootballReplayVideoEvent(
  360 + base_url="http://192.168.1.59:11434",
  361 + model="qwen3.6:35b-a3b-q8_0",
  362 + temperature=0.7,
364 ) 363 )
365 -  
366 - print("\n[INFO] 所有测试完成") 364 + event_json = fbrv.video_event_for_contents(content, None)
  365 + print(f" 识别结果: {event_json}")
  366 +
  367 +
  368 + # # TEST 3: ROI + max_px_area
  369 + # print("\n[TEST 3] to_frames with roi + max_px_area")
  370 + # try:
  371 + # end_crop = min(args.start + 5, args.end)
  372 + # frames_cropped = v2f.to_frames(
  373 + # url=args.url,
  374 + # start_utc=args.start,
  375 + # end_utc=end_crop,
  376 + # fps=1.0,
  377 + # roi=None,
  378 + # max_px_area=200_000,
  379 + # )
  380 + # print(f" 成功提取 {len(frames_cropped)} 帧(带 ROI 裁剪和面积缩放)")
  381 + # except Exception as e:
  382 + # print(f" 跳过/失败: {e}")
  383 + #
  384 + # # TEST 4: SEI UTC 模式说明(仅演示,需真实含 SEI 的流才能运行)
  385 + # print("\n[TEST 4] SEI UTC 绝对时间戳模式(示例代码,需替换为含 SEI 的流)")
  386 + # print(
  387 + # " # 示例:假设视频第一帧 UTC 为 1715600000,提取 10 秒片段\n"
  388 + # " # frames = v2f.to_frames(url, start_utc=1715600000, end_utc=1715600010, fps=1)"
  389 + # )
  390 + #
  391 + # print("\n[INFO] 所有测试完成")
@@ -96,3 +96,21 @@ class FootballReplayVideoEvent: @@ -96,3 +96,21 @@ class FootballReplayVideoEvent:
96 with open(cache_path, 'w', encoding='utf-8') as f: 96 with open(cache_path, 'w', encoding='utf-8') as f:
97 f.write(result) 97 f.write(result)
98 return result_json 98 return result_json
  99 +
  100 + def video_event_for_contents(self, contents: list, asr_text: str = '无', cache_path=None):
  101 + if cache_path is not None and os.path.exists(cache_path):
  102 + with open(cache_path, 'r', encoding='utf-8') as f:
  103 + return json.loads(f.read())
  104 + system_message = SystemMessage(content=req_prompt)
  105 + video_message = HumanMessage(content=contents)
  106 + asr_message = HumanMessage(content=f"解说内容:{asr_text}")
  107 + result = self.model.invoke([system_message, video_message, asr_message]).content
  108 + try:
  109 + result_json = json.loads(result)
  110 + except json.JSONDecodeError:
  111 + result_json = json.loads(result.replace("```json", "").replace("```", ""))
  112 + if cache_path is not None:
  113 + os.makedirs(Path(cache_path).parent, exist_ok=True)
  114 + with open(cache_path, 'w', encoding='utf-8') as f:
  115 + f.write(result)
  116 + return result_json