Toggle navigation
Toggle navigation
This project
Loading...
Sign in
lizhengwei
/
aigc-embedding-service
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
lizhengwei
2026-05-27 15:49:06 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
840141635d156e5bd47a24012a47e202ba381d44
84014163
1 parent
51946aee
jira:NYJ-1540 desc: not think
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
232 additions
and
104 deletions
src/football_replay_match/core/api.py
src/football_replay_match/core/api_test.py
src/football_replay_match/core/utils/football_replay_match_live.py
src/football_replay_match/core/utils/football_replay_video_event_by_llm.py
src/football_replay_match/core/api.py
View file @
8401416
import
os
import
time
import
logging
from
aabd.base.enhance_dict
import
value_or_default
...
...
@@ -25,6 +26,8 @@ class FootballReplayMatch:
def
match_by_time
(
self
,
replay
,
events
):
start_utc
=
replay
.
get
(
'start_utc'
)
if
start_utc
is
None
:
return
None
events
=
[(
start_utc
-
e
.
get
(
'event_utc'
),
e
)
for
e
in
events
if
e
.
get
(
'event_utc'
)
is
not
None
]
if
len
(
events
)
==
0
:
...
...
@@ -79,30 +82,36 @@ class FootballReplayMatch:
# 该时间段附近是否有进球事件
replay_info
=
data
[
'replay'
]
replay_id
=
replay_info
.
get
(
'id'
)
replay_start_utc
=
replay_info
.
get
(
'start_utc'
,
None
)
replay_end_utc
=
replay_info
.
get
(
'end_utc'
,
None
)
live_events
=
data
[
'events'
]
goal_live_events
=
[
e
for
e
in
live_events
if
str
(
e
.
get
(
'type'
,
''
))
==
'1'
]
matched_event
=
self
.
match_by_time
(
replay_info
,
goal_live_events
)
# print(f"{task_id}_match_by_time matched_event: {matched_event}")
if
matched_event
is
None
:
start_time
=
time
.
time
()
replay_det_info
=
self
.
det_goal_replay
(
replay_info
,
task_id
)
end_time
=
time
.
time
()
logger
.
info
(
f
"判断是否是进球耗时: {end_time - start_time} 秒"
)
print
(
f
"判断是否是进球耗时: {end_time - start_time} 秒"
)
replay_event_name
=
replay_det_info
.
get
(
'event_name'
,
''
)
if
replay_event_name
==
'进球'
:
matched_event
=
self
.
match_by_llm
(
replay_info
,
goal_live_events
,
task_id
)
if
matched_event
is
None
:
logger
.
info
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件"
)
# print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")
logger
.
info
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 是进球但是未能找到匹配的事件"
)
print
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 是进球但是未能找到匹配的事件"
)
else
:
matched_event_id
=
matched_event
.
get
(
'video_id'
)
logger
.
info
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}"
)
# print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
logger
.
info
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}"
)
print
(
f
"{task_id}_LLM认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}"
)
else
:
logger
.
info
(
f
"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配"
)
# print(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")
logger
.
info
(
f
"{task_id}_LLM判断{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 不是进球,无需匹配"
)
print
(
f
"{task_id}_LLM判断{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 不是进球,无需匹配"
)
else
:
matched_event_id
=
matched_event
.
get
(
'id'
)
logger
.
info
(
f
"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}"
)
# print(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
logger
.
info
(
f
"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}"
)
print
(
f
"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_start_utc}_{replay_end_utc} 对应的进球的事件视频是{matched_event_id}"
)
return
{
"id"
:
task_id
,
"match_id"
:
match_id
,
...
...
src/football_replay_match/core/api_test.py
View file @
8401416
...
...
@@ -9,6 +9,7 @@ api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测
5. 无进球事件 / events 为空
6. 边界:缺少 replay 键时抛出异常
"""
import
os
import
time
import
unittest
from
types
import
SimpleNamespace
...
...
@@ -19,47 +20,65 @@ try:
except
ImportError
:
from
.api
import
FootballReplayMatch
from
pathlib
import
Path
def
get_video_files_pathlib
(
folder_path
,
extensions
=
None
):
if
extensions
is
None
:
extensions
=
{
'.mp4'
}
else
:
extensions
=
{
ext
.
lower
()
for
ext
in
extensions
}
folder
=
Path
(
folder_path
)
image_files
=
[
f
.
name
for
f
in
folder
.
iterdir
()
if
f
.
is_file
()
and
f
.
suffix
.
lower
()
in
extensions
]
return
sorted
(
image_files
)
def
_fake_settings
(
match_by_time_threshold
:
int
=
30
):
"""构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。"""
return
SimpleNamespace
(
match_by_time_threshold
=
match_by_time_threshold
,
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
),
common
=
SimpleNamespace
(
cache_dir
=
"/root/lzw/tmp_0526_replay_cache0526/videos_cache"
,
task_log_dir
=
"/root/lzw/tmp_0526_replay_cache0526/tasks_log"
),
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
,
api_key
=
'no_key'
),
common
=
SimpleNamespace
(
cache_dir
=
"/root/lzw/tmp_0527_replay_cache_08/videos_cache"
,
task_log_dir
=
"/root/lzw/tmp_0527_replay_cache_08/tasks_log"
),
save_frames_enable
=
True
,
)
def
_base_data
():
"""返回一份基础伪数据,replay.start_utc = 1000(毫秒)。"""
def
get_event_list
():
# url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
events_mp4_dir
=
r"/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/goal"
events_mp4_names
=
get_video_files_pathlib
(
events_mp4_dir
)
event_lis
=
[]
for
mp4
in
events_mp4_names
:
mp4_path
=
os
.
path
.
join
(
events_mp4_dir
,
mp4
)
event_lis
.
append
({
"id"
:
f
"event_{mp4.split('.')[0]}"
,
"type"
:
"1"
,
"url"
:
mp4_path
,
# "event_utc": None
})
return
event_lis
def
_base_data
(
i
,
replay_path
,
event_lis
):
return
{
"id"
:
"task_001"
,
"match_id"
:
"match_001"
,
"id"
:
f
"task_{str(i).zfill(3)}"
,
"match_id"
:
f
"match_{str(i).zfill(3)}"
,
"replay"
:
{
"id"
:
"replay_001"
,
"url"
:
"/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/replays/videos/00_14_32_080-00_15_10_560.mp4"
,
"start_utc"
:
5
_000
,
"end_utc"
:
21
_000
},
"events"
:
[
{
"id"
:
"event_001"
,
"type"
:
"1"
,
"url"
:
"/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/00-14-09-052.mp4"
,
"event_utc"
:
None
},
{
"id"
:
"event_002"
,
"type"
:
"1"
,
"url"
:
"/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/00-38-40-052.mp4"
,
"event_utc"
:
None
"id"
:
f
"replay_{str(i).zfill(3)}"
,
"url"
:
replay_path
,
# "start_utc": None,
# "end_utc": None
},
{
"id"
:
"event_003"
,
"type"
:
"1"
,
"url"
:
"/root/lzw/finished/69dd5845dd0412067b8d5587-auto-1776074760653/live/videos/01-36-04-972.mp4"
,
"event_utc"
:
None
}
],
"events"
:
event_lis
,
}
...
...
@@ -68,18 +87,39 @@ class TestReplayMatchEvent(unittest.TestCase):
@patch
(
"utils.football_replay_match_live.FootballReplayMatchLive"
)
@patch
(
"utils.football_replay_video_event_by_llm.FootballReplayVideoEvent"
)
def
test_match_by_time_success
(
self
,
mock_video_cls
,
mock_live_cls
):
"""场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
#
"""场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
settings
=
_fake_settings
(
match_by_time_threshold
=
10
)
# threshold = 30 * 1000 = 30000 ms
frm
=
FootballReplayMatch
(
settings
)
data
=
_base_data
()
replays_mp4_dir
=
"/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/replay"
relays_mp4_names
=
get_video_files_pathlib
(
replays_mp4_dir
)
event_list
=
get_event_list
()
all_time
=
0.0
max_time
=
0.0
for
i
,
replay_name
in
enumerate
(
relays_mp4_names
):
print
(
100
*
"*"
+
f
"replay_name: {replay_name}"
)
replay_path
=
os
.
path
.
join
(
replays_mp4_dir
,
replay_name
)
data
=
_base_data
(
i
,
replay_path
,
event_list
)
start_time
=
time
.
time
()
result
=
frm
.
replay_match_event
(
data
)
end_time
=
time
.
time
()
print
(
f
"cost time:{end_time - start_time}"
)
cost_time
=
end_time
-
start_time
all_time
+=
cost_time
print
(
f
"replay_name: {replay_name}, cost time:{cost_time}"
)
if
max_time
<
cost_time
:
max_time
=
cost_time
print
(
result
)
print
()
print
()
print
()
print
(
100
*
"#"
)
print
(
f
"max_time: {max_time}"
)
print
(
f
"all_time: {all_time}"
)
# exit()
# self.assertEqual(result["id"], "task_001")
...
...
src/football_replay_match/core/utils/football_replay_match_live.py
View file @
8401416
import
re
import
json
import
os.path
from
pathlib
import
Path
...
...
@@ -11,64 +12,103 @@ try:
except
:
from
llm_image
import
Video2Frame
# req_prompt = """
# # Role
# 你是一名拥有20年经验的足球视频技术分析师,擅长结合**视觉画面**与**解说音频(ASR)**进行跨镜头的事件匹配。你能通过解说员的描述锁定时间背景,并通过视觉特征确认物理细节。
# # Task
# 输入包含:
# 1. 【回放片段】(Replay):包含视频帧 + **对应的解说文本**。
# 2. 【直播进球片段列表】(Live Candidates):包含视频帧 + **对应的解说文本**。
# 目标:在【直播片段】中找到与【回放片段】属于**同一个进球事件**的片段。如果没有任何片段匹配,返回 null。
# # Analysis Workflow (多模态分析流程)
# 请按照以下步骤进行推理:
# ### 第一步:解说词元数据提取 (听觉线索)
# 分析【回放片段】的解说文本,寻找以下关键信息:
# - **时间指代**:解说员是否提到了具体时间?(如“上半场”、“第10分钟”、“开场不久”)。
# - **事件描述**:解说员如何描述这个进球?(如“世界波”、“点球”、“补射”、“乌龙球”)。
# - **球员提及**:解说员念出了谁的名字?
# ### 第二步:视觉物理指纹提取 (视觉线索)
# 忽略镜头语言(慢放、特写),提取核心物理特征:
# - **进攻与射门**:进攻方式(传中/直塞)、射门部位(头/脚)、射门位置。
# - **球路与防守**:球的轨迹(高/低/折射)、门将扑救动作(侧扑/倒地/未动)。
# - **庆祝**:进球后的庆祝动作(仅作为辅助验证)。
# ### 第三步:跨模态匹配与验证
# 遍历【直播片段】,结合视觉和听觉进行判断:
# - **视觉一致性**:直播片段的动作、轨迹、门将反应是否与回放完全吻合?
# - **听觉一致性**:
# - 如果回放解说提到“这是第5分钟的进球”,而直播片段的时间戳是90分钟,**不要直接排除**,而是检查直播片段的解说是否也提到了“回顾第5分钟”或者直播片段的视觉内容确实是第5分钟的动作。
# - **核心原则**:视觉物理特征 > 解说词描述 > 时间戳元数据。
# - **特殊情况**:如果视觉特征高度相似(如同一个球员、同一个角度),但解说词明确说“这是另一个进球(例如:这是他的第二个进球,而回放说是第一个)”,则判定为不匹配。
# # Logic Constraints (逻辑约束 - 必须严格遵守)
# - **时间单向性原则(铁律)**:
# - **回看中的比赛时间一定在比赛片段画面时间之后**。
# - 逻辑:回放是对过去发生事件的回顾。如果【回放片段】中解说提到的比赛时间(或画面显示的比赛时钟)**早于**【直播片段】中解说提到的比赛时间(或画面时钟),则**绝对不可能**是同一个事件。
# - *示例*:回放解说在描述“第10分钟的进球”,而直播片段明确发生在“第5分钟”,则该直播片段**一定不是**目标。
# - **时间陷阱**:回放可能是赛后集锦。如果解说员说“让我们看看**刚才**那个球”或者“**上半场**那个球”,即使当前比赛时间是90分钟,也要去匹配对应时间段的直播片段(或视觉特征)。
# - **同名陷阱**:如果解说提到“又是**凯恩**进球了”,不能只看凯恩,必须看**怎么进的**(头球还是点球)。
# - **无匹配处理**:如果所有候选片段在视觉动作(如射门方式、进球位置)或关键事件逻辑上与回放明显不符,必须判定为无匹配,将 `video_id` 设为 `null`。
# ### 输出要求
# 请仅输出一个JSON格式的结果,不要输出任何分析过程。不要包含 markdown 标记(如 ```json ... ```),不要包含任何解释或额外文本。
# 格式如下:
# {
# "replay_summary": {
# "audio_cues": "解说提到的关键信息(如:'第15分钟', '远射', '德布劳内')",
# "visual_cues": "视觉关键特征(如:'禁区外右脚', '球挂死角', '门将飞身扑救')"
# },
# "reasoning": "综合分析:回放解说提到是'上半场的远射',视觉显示'17号球员禁区外起脚'。Candidate_1 视觉是'近距离推射',排除。Candidate_2 视觉是'禁区外远射',且门将动作一致,虽然直播时间显示是下半场(可能是集锦回顾),但解说也提到了'回顾上半场',确认为同一事件。",
# "video_id": "Candidate_2"
# }"""
req_prompt
=
"""
# Role
你是一名拥有20年经验的足球视频技术分析师,擅长
结合**视觉画面**与**解说音频(ASR)**进行跨镜头的事件匹配。你能通过解说员的描述锁定时间背景,并通过视觉特征确认物理
细节。
你是一名拥有20年经验的足球视频技术分析师,擅长
通过**纯视觉画面分析**进行跨镜头的事件匹配。你能精准捕捉球员动作、球衣号码、比赛时钟及比分牌等视觉
细节。
# Task
输入包含:
1. 【回放片段】(Replay):包含视频帧 + **对应的解说文本**。
2. 【直播进球片段列表】(Live Candidates):包含视频帧 + **对应的解说文本**。
1. 【回放片段】(Replay):包含视频帧画面。
2. 【直播进球片段列表】(Live Candidates):包含视频帧画面。
目标:在【直播片段】中找到与【回放片段】属于**同一个进球事件**的片段。如果没有任何片段匹配,返回 null。
# Analysis Workflow (多模态分析流程)
请按照以下步骤进行推理:
### 第一步:解说词元数据提取 (听觉线索)
分析【回放片段】的解说文本,寻找以下关键信息:
- **时间指代**:解说员是否提到了具体时间?(如“上半场”、“第10分钟”、“开场不久”)。
- **事件描述**:解说员如何描述这个进球?(如“世界波”、“点球”、“补射”、“乌龙球”)。
- **球员提及**:解说员念出了谁的名字?
# Visual Analysis Rules (纯视觉匹配规则)
### 第二步:视觉物理指纹提取 (视觉线索)
忽略镜头语言(慢放、特写),提取核心物理特征:
- **进攻与射门**:进攻方式(传中/直塞)、射门部位(头/脚)、射门位置。
- **球路与防守**:球的轨迹(高/低/折射)、门将扑救动作(侧扑/倒地/未动)。
- **庆祝**:进球后的庆祝动作(仅作为辅助验证)。
1. **核心物理特征比对(最高优先级)**:
- **球员身份**:进攻方球衣颜色、进球者号码、助攻者号码。
- **进球方式**:射门部位(左脚/右脚/头球)、射门位置(禁区内/禁区外/点球)、球路轨迹(贴地斩/世界波/挑射)。
- **防守反应**:门将的扑救动作(向左/向右/未动)、后卫的封堵动作。
- **庆祝动作**:进球后的标志性庆祝(如滑跪、指天、双手比心等)。
- *判定标准*:上述物理特征必须完全一致,否则视为不同进球。
### 第三步:跨模态匹配与验证
遍历【直播片段】,结合视觉和听觉进行判断:
- **视觉一致性**:直播片段的动作、轨迹、门将反应是否与回放完全吻合?
- **听觉一致性**:
- 如果回放解说提到“这是第5分钟的进球”,而直播片段的时间戳是90分钟,**不要直接排除**,而是检查直播片段的解说是否也提到了“回顾第5分钟”或者直播片段的视觉内容确实是第5分钟的动作。
- **核心原则**:视觉物理特征 > 解说词描述 > 时间戳元数据。
- **特殊情况**:如果视觉特征高度相似(如同一个球员、同一个角度),但解说词明确说“这是另一个进球(例如:这是他的第二个进球,而回放说是第一个)”,则判定为不匹配。
2. **画面内嵌元数据验证**:
- **比分牌逻辑**:检查回放画面中的比分(如 1-0)与直播片段进球前后的比分变化是否逻辑自洽。
- **比赛时钟**:虽然回放可能是赛后集锦(时间戳不准),但如果回放画面明确显示比赛时间(如 18:36),而直播片段发生在完全不同的时间段(如 85:00),且视觉特征不符,则排除。
# Logic Constraints (逻辑约束 - 必须严格遵守)
3. **无匹配处理**:
- 如果候选片段的进球者号码、射门方式或球衣颜色与回放不一致,必须判定为无匹配。
- **时间单向性原则(铁律)**:
- **回看中的比赛时间一定在比赛片段画面时间之后**。
- 逻辑:回放是对过去发生事件的回顾。如果【回放片段】中解说提到的比赛时间(或画面显示的比赛时钟)**早于**【直播片段】中解说提到的比赛时间(或画面时钟),则**绝对不可能**是同一个事件。
- *示例*:回放解说在描述“第10分钟的进球”,而直播片段明确发生在“第5分钟”,则该直播片段**一定不是**目标。
- **时间陷阱**:回放可能是赛后集锦。如果解说员说“让我们看看**刚才**那个球”或者“**上半场**那个球”,即使当前比赛时间是90分钟,也要去匹配对应时间段的直播片段(或视觉特征)。
- **同名陷阱**:如果解说提到“又是**凯恩**进球了”,不能只看凯恩,必须看**怎么进的**(头球还是点球)。
- **无匹配处理**:如果所有候选片段在视觉动作(如射门方式、进球位置)或关键事件逻辑上与回放明显不符,必须判定为无匹配,将 `video_id` 设为 `null`。
# Output Format (输出要求)
### 输出要求
请**仅**输出一个标准的 JSON 对象,**不要**包含任何推理过程(reasoning)、Markdown 标记(如 ```json)或额外的解释文本。
请仅输出一个JSON格式的结果,不要输出任何分析过程。不要包含 markdown 标记(如 ```json ... ```),不要包含任何解释或额外文本。
格式如下:
JSON 格式如下:
{
"replay_summary": {
"audio_cues": "解说提到的关键信息(如:'第15分钟', '远射', '德布劳内')",
"visual_cues": "视觉关键特征(如:'禁区外右脚', '球挂死角', '门将飞身扑救')"
"visual_cues": "提取回放画面的核心特征(如:'蓝队6号禁区外远射', '门将扑救不及', '比分变为1-1')"
},
"reasoning": "综合分析:回放解说提到是'上半场的远射',视觉显示'17号球员禁区外起脚'。Candidate_1 视觉是'近距离推射',排除。Candidate_2 视觉是'禁区外远射',且门将动作一致,虽然直播时间显示是下半场(可能是集锦回顾),但解说也提到了'回顾上半场',确认为同一事件。",
"video_id": "Candidate_2"
}"""
"video_id": "匹配到的直播片段ID,如果没有匹配则填 null"
}
"""
class
FootballReplayMatchLive
:
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:
# self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7,
# api_key='no_key')
self
.
model
=
ChatOpenAI
(
base_url
=
base_url
,
model
=
model
,
temperature
=
temperature
,
api_key
=
api_key
,
extra_body
=
{
"chat_template_kwargs"
:
{
"enable_thinking"
:
False
}})
model_kwargs
=
{
"extra_body"
:
{
"enable_thinking"
:
False
}
}
)
#extra_body={"chat_template_kwargs": {"enable_thinking": False}})
self
.
cache_dir
=
cache_dir
self
.
video2frame
=
Video2Frame
(
cache_dir
=
cache_dir
,
save_frames_enable
=
save_frames_enable
)
...
...
@@ -91,12 +136,12 @@ class FootballReplayMatchLive:
return
None
elif
len
(
live_videos
)
==
1
:
live_video_path
=
live_videos
[
0
]
.
get
(
"url"
,
None
)
live_video_id
=
live_videos
[
0
]
.
get
(
"video_id"
,
os
.
path
.
basename
(
live_video_path
))
asr_text
=
live_videos
[
0
]
.
get
(
"asr_text"
,
''
)
live_video_id
=
live_videos
[
0
]
.
get
(
"id"
,
os
.
path
.
basename
(
live_video_path
))
# asr_text = live_videos[0].get("asr_text", '')
live
=
{
"video_id"
:
live_video_id
,
"video_path"
:
live_video_path
,
"asr_text"
:
asr_text
,
"url"
:
live_video_path
,
# "asr_text": asr_text,
}
if
record
is
not
None
:
record
.
append
({
"live"
:
live
,
"llm_result"
:
None
,
'live_list'
:
[
live
]})
...
...
@@ -107,9 +152,9 @@ class FootballReplayMatchLive:
live_records
=
{}
for
live_video
in
live_videos
:
live_video_path
=
live_video
.
get
(
"url"
,
None
)
live_video_id
=
live_video
.
get
(
"
video_
id"
,
os
.
path
.
basename
(
live_video_path
))
live_video_id
=
live_video
.
get
(
"id"
,
os
.
path
.
basename
(
live_video_path
))
event_utc
=
live_video
.
get
(
"event_utc"
,
None
)
asr_text
=
live_video
.
get
(
"asr_text"
,
''
)
#
asr_text = live_video.get("asr_text", '')
live_video_contents
=
live_video
.
get
(
"llm_contents"
,
None
)
if
live_video_contents
is
None
:
# event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
...
...
@@ -124,38 +169,67 @@ class FootballReplayMatchLive:
roi
=
None
,
max_px_area
=
400
_000
,
prompt_start
=
f
"### 候选片段 video_id: {live_video_id} ###"
,
prompt_end
=
f
"
\n
该片段解说内容:
{asr_text}
\n
"
prompt_end
=
f
"
\n
该片段解说内容:
无
\n
"
)
live_map
[
live_video_id
]
=
{
"video_id"
:
live_video_id
,
"
video_path
"
:
live_video_path
,
"
url
"
:
live_video_path
,
"event_utc"
:
event_utc
,
"asr_text"
:
asr_text
,
#
"asr_text": asr_text,
"contents"
:
live_video_contents
,
}
live_records
[
live_video_id
]
=
{
"video_id"
:
live_video_id
,
"
video_path
"
:
live_video_path
,
"
url
"
:
live_video_path
,
"event_utc"
:
event_utc
,
"asr_text"
:
asr_text
,
#
"asr_text": asr_text,
}
live_videos_contents
.
extend
(
live_video_contents
)
user_contents
.
extend
(
replay_video_contents
)
user_contents
.
append
({
'type'
:
"text"
,
"text"
:
"
\n
【候选直播片段列表】
\n
"
})
user_contents
.
extend
(
live_videos_contents
)
user_contents
.
append
({
'type'
:
"text"
,
"text"
:
"
\n
请根据上述片段进行匹配并按要求输出结果
\n
"
})
user_contents
.
append
({
'type'
:
"text"
,
"text"
:
"
\n
请根据上述片段进行匹配并按要求输出结果
/no_think
\n
"
})
system_message
=
SystemMessage
(
content
=
req_prompt
)
user_message
=
HumanMessage
(
content
=
user_contents
)
result
=
self
.
model
.
invoke
([
system_message
,
user_message
])
.
content
print
(
100
*
"3"
)
print
(
result
)
print
(
100
*
"4"
)
# try:
# result_json = json.loads(result)
# except json.JSONDecodeError:
# try:
# result_json = json.loads(result.replace("```json", "").replace("```", ""))
# except Exception as e:
# print("JSON解析失败:", result)
# raise e
try
:
result_json
=
json
.
loads
(
result
)
except
json
.
JSONDecodeError
:
try
:
result_json
=
json
.
loads
(
result
.
replace
(
"```json"
,
""
)
.
replace
(
"```"
,
""
))
# 1. 深度清洗:去掉 Markdown 标记
cleaned_result
=
result
.
replace
(
"```json"
,
""
)
.
replace
(
"```"
,
""
)
# 2. 核心修复:使用 re.DOTALL 确保正则能跨行匹配,精准提取最外层完整的 JSON 对象
# 这样可以防止因为模型输出换行导致提取不完整
json_match
=
re
.
search
(
r'(
\
{.*
\
})'
,
cleaned_result
,
re
.
DOTALL
)
if
json_match
:
json_str
=
json_match
.
group
(
1
)
# 截断第一个 } 之后的所有内容,彻底干掉 <time_location> 等尾巴
json_str
=
json_str
[:
json_str
.
rfind
(
'}'
)
+
1
]
else
:
# 如果连花括号都找不到,直接抛出原始内容方便排查
print
(
"无法提取JSON,原始内容:"
,
result
)
raise
ValueError
(
"响应中未找到有效的 JSON 对象"
)
# 3. 使用 strict=False 容忍字符串中的非法控制字符(如直接换行)
result_json
=
json
.
loads
(
json_str
,
strict
=
False
)
except
Exception
as
e
:
print
(
"JSON解析失败:"
,
result
)
print
(
"JSON解析依然失败,清洗后的内容:"
,
json_str
if
'json_str'
in
locals
()
else
result
)
raise
e
video_id
=
result_json
.
get
(
"video_id"
,
None
)
...
...
@@ -233,9 +307,9 @@ class FootballReplayMatchLive:
if
result
is
not
None
:
result_no_content
=
{
"video_id"
:
result
.
get
(
"video_id"
,
None
),
"
video_path"
:
result
.
get
(
"video_path
"
,
None
),
"
url"
:
result
.
get
(
"url
"
,
None
),
"event_utc"
:
result
.
get
(
"event_utc"
,
None
),
"asr_text"
:
result
.
get
(
"asr_text"
,
None
),
#
"asr_text": result.get("asr_text", None),
}
else
:
result_no_content
=
None
...
...
src/football_replay_match/core/utils/football_replay_video_event_by_llm.py
View file @
8401416
...
...
@@ -76,15 +76,20 @@ class FootballReplayVideoEvent:
# self.model = ChatOpenAI(base_url="http://192.168.1.59:11434/v1", model="qwen3.6:35b-a3b-q8_0", temperature=0.7,
# api_key='no_key')
self
.
model
=
ChatOpenAI
(
base_url
=
base_url
,
model
=
model
,
temperature
=
temperature
,
api_key
=
api_key
,
extra_body
=
{
"chat_template_kwargs"
:
{
"enable_thinking"
:
False
}})
model_kwargs
=
{
"extra_body"
:
{
"enable_thinking"
:
False
}
}
)
# extra_body={"enable_thinking": False})
# extra_body={"chat_template_kwargs": {"enable_thinking": False}})
self
.
video2frame
=
Video2Frame
(
cache_dir
=
cache_dir
,
save_frames_enable
=
save_frames_enable
)
def
video_event
(
self
,
replay_pack
:
dict
,
asr_text
:
str
=
'无'
,
cache_dir
=
None
,
task_log_dir
=
None
):
replay_video_path
=
replay_pack
.
get
(
"url"
,
None
)
cache_path
=
os
.
path
.
join
(
task_log_dir
,
'video_event.json'
)
video_name
=
os
.
path
.
basename
(
replay_video_path
)
cache_path
=
os
.
path
.
join
(
task_log_dir
,
f
'{video_name}_replay.json'
)
if
cache_path
is
not
None
and
os
.
path
.
exists
(
cache_path
):
with
open
(
cache_path
,
'r'
,
encoding
=
'utf-8'
)
as
f
:
return
json
.
loads
(
f
.
read
())
...
...
@@ -104,8 +109,8 @@ class FootballReplayVideoEvent:
system_message
=
SystemMessage
(
content
=
req_prompt
)
video_message
=
HumanMessage
(
content
=
contents
)
asr_message
=
HumanMessage
(
content
=
f
"解说内容:{asr_text}"
)
result
=
self
.
model
.
invoke
([
system_message
,
video_message
,
asr_message
])
.
content
# asr_message = HumanMessage(content=f"解说内容:{asr_text}")
result
=
self
.
model
.
invoke
([
system_message
,
video_message
])
.
content
try
:
result_json
=
json
.
loads
(
result
)
except
json
.
JSONDecodeError
:
...
...
Please
register
or
login
to post a comment