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-21 17:31:48 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
a4dfad4d7b430256f464f9e87b81df2e56784e2e
a4dfad4d
1 parent
36157122
jira:NYJ-1540 desc: add api_test.py
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
232 additions
and
18 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/utils/llm_image.py
src/football_replay_match/core/api.py
View file @
a4dfad4
import
os
import
logging
from
aabd.base.enhance_dict
import
value_or_default
...
...
@@ -18,25 +19,31 @@ class FootballReplayMatch:
save_frames_enable
=
value_or_default
(
settings
.
save_frames_enable
,
False
)
self
.
videoEventRecognition
=
FootballReplayVideoEvent
(
llm_base_url
,
model_name
,
temperature
,
'no_key'
,
self
.
cache_dir
,
save_frames_enable
)
self
.
videoMatchLive
=
FootballReplayMatchLive
(
llm_base_url
,
model_name
,
temperature
,
'no_key'
,
self
.
cache_dir
)
self
.
videoMatchLive
=
FootballReplayMatchLive
(
llm_base_url
,
model_name
,
temperature
,
'no_key'
,
self
.
cache_dir
,
save_frames_enable
)
def
match_by_time
(
self
,
replay
,
events
):
start_utc
=
replay
.
get
(
'start_utc'
)
events
=
[(
start_utc
-
e
.
get
(
'event_utc'
),
e
)
for
e
in
events
]
events
.
sort
(
key
=
lambda
x
:
abs
(
x
[
0
]))
time_diff
,
event
=
events
[
0
]
events
.
sort
(
key
=
lambda
x
:
x
[
0
])
target_index
=
next
((
i
for
i
,
e
in
enumerate
(
events
)
if
e
[
0
]
>
0
),
-
1
)
# 找到第一个 event_utc 早于 start_utc 的事件
if
target_index
==
-
1
:
return
None
time_diff
,
event
=
events
[
target_index
]
if
time_diff
<
self
.
match_by_time_threshold
:
return
event
else
:
return
None
def
det_goal_replay
(
self
,
replay
):
return
self
.
videoEventRecognition
.
video_event
(
replay
,
cache_dir
=
self
.
cache_dir
)
def
det_goal_replay
(
self
,
replay
,
task_id
):
return
self
.
videoEventRecognition
.
video_event
(
replay
,
cache_dir
=
os
.
path
.
join
(
self
.
cache_dir
,
task_id
,
"replay"
))
def
match_by_llm
(
self
,
replay
,
events
):
return
self
.
videoMatchLive
.
match_batch
(
replay
,
events
,
max_parallel
=
2
,
cache_dir
=
self
.
cache_dir
)
def
match_by_llm
(
self
,
replay
,
events
,
task_id
):
return
self
.
videoMatchLive
.
match_batch
(
replay
,
events
,
max_parallel
=
2
,
cache_dir
=
os
.
path
.
join
(
self
.
cache_dir
,
task_id
,
"envents"
))
def
replay_match_event
(
self
,
data
):
"""
...
...
@@ -72,22 +79,25 @@ class FootballReplayMatch:
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
)
if
matched_event
is
None
:
replay_det_info
=
self
.
det_goal_replay
(
replay_info
)
replay_det_info
=
self
.
det_goal_replay
(
replay_info
,
task_id
)
replay_event_name
=
replay_det_info
.
get
(
'event_name'
,
''
)
if
replay_event_name
==
'进球'
:
matched_event
=
self
.
match_by_llm
(
replay_info
,
goal_live_events
)
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']} 是进球但是未能找到匹配的事件")
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}")
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']} 不是进球,无需匹配")
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}")
return
{
"id"
:
task_id
,
"match_id"
:
match_id
,
...
...
src/football_replay_match/core/api_test.py
0 → 100644
View file @
a4dfad4
"""
api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测试
使用伪数据覆盖主要分支:
1. 时间阈值内直接匹配成功
2. 时间匹配失败 + LLM 判定不是进球
3. 时间匹配失败 + LLM 判定是进球 + LLM 匹配成功
4. 时间匹配失败 + LLM 判定是进球 + LLM 匹配失败
5. 无进球事件 / events 为空
6. 边界:缺少 replay 键时抛出异常
"""
import
unittest
from
types
import
SimpleNamespace
from
unittest.mock
import
MagicMock
,
patch
try
:
from
api
import
FootballReplayMatch
except
ImportError
:
from
.api
import
FootballReplayMatch
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_0518_replay_cache0521_01"
),
save_frames_enable
=
True
,
)
def
_base_data
():
"""返回一份基础伪数据,replay.start_utc = 1000(毫秒)。"""
url
=
"http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
return
{
"id"
:
"task_001"
,
"match_id"
:
"match_001"
,
"replay"
:
{
"id"
:
"replay_001"
,
"url"
:
url
,
"start_utc"
:
1777444502543
,
"end_utc"
:
1777444531063
},
"events"
:
[
{
"id"
:
"event_001"
,
"type"
:
"1"
,
"url"
:
url
,
"event_utc"
:
1777444502543
-
40000
},
{
"id"
:
"event_001"
,
"type"
:
"1"
,
"url"
:
url
,
"event_utc"
:
1777444502543
-
20000
},
{
"id"
:
"event_001"
,
"type"
:
"1"
,
"url"
:
url
,
"event_utc"
:
1777444531063
+
20000
}
],
}
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 命中。"""
settings
=
_fake_settings
(
match_by_time_threshold
=
10
)
# threshold = 30 * 1000 = 30000 ms
frm
=
FootballReplayMatch
(
settings
)
data
=
_base_data
()
result
=
frm
.
replay_match_event
(
data
)
print
(
result
)
# exit()
# self.assertEqual(result["id"], "task_001")
# self.assertEqual(result["match_id"], "match_001")
# self.assertEqual(result["replay_id"], "replay_001")
# # event_001 与 replay.start_utc 差值 20 ms < 30000 ms,应命中 event_001 的 id
# self.assertEqual(result["event_id"], "event_001")
# # 未调用 LLM 相关方法
# mock_video_cls.return_value.video_event.assert_not_called()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_not_goal(self, mock_video_cls, mock_live_cls):
# """场景2:时间匹配失败,det_goal_replay 判定不是进球。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "宣传片"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_goal_then_llm_match(self, mock_video_cls, mock_live_cls):
# """场景3:时间匹配失败 -> 判定是进球 -> LLM 匹配成功。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "世界波"}
# mock_video_cls.return_value = mock_video_instance
# mock_live_instance = MagicMock()
# mock_live_instance.match_batch.return_value = {"video_id": "event_003", "video_path": "http://example.com/event3.mp4", "asr_text": ""}
# mock_live_cls.return_value = mock_live_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], "event_003")
# mock_video_instance.video_event.assert_called_once()
# mock_live_instance.match_batch.assert_called_once()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_match_by_time_fail_then_goal_then_llm_no_match(self, mock_video_cls, mock_live_cls):
# """场景4:时间匹配失败 -> 判定是进球 -> LLM 未匹配到任何事件。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "头球破门"}
# mock_video_cls.return_value = mock_video_instance
# mock_live_instance = MagicMock()
# mock_live_instance.match_batch.return_value = None
# mock_live_cls.return_value = mock_live_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_instance.match_batch.assert_called_once()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_no_goal_events(self, mock_video_cls, mock_live_cls):
# """场景5:events 中没有任何 type='1' 的进球事件。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "无候选事件"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# data["events"] = [
# {"id": "event_005", "type": "2", "url": "http://example.com/event5.mp4", "event_utc": 1020},
# ]
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_empty_events(self, mock_video_cls, mock_live_cls):
# """场景6:events 为空列表。"""
# mock_video_instance = MagicMock()
# mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "空列表"}
# mock_video_cls.return_value = mock_video_instance
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# data["events"] = []
# result = frm.replay_match_event(data)
# self.assertEqual(result["event_id"], None)
# mock_video_instance.video_event.assert_called_once()
# mock_live_cls.return_value.match_batch.assert_not_called()
# @patch("utils.football_replay_match_live.FootballReplayMatchLive")
# @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
# def test_missing_replay_key(self, mock_video_cls, mock_live_cls):
# """场景7:data 缺少 replay 键,应抛出 KeyError。"""
# settings = _fake_settings(match_by_time_threshold=30)
# frm = FootballReplayMatch(settings)
# data = _base_data()
# del data["replay"]
# with self.assertRaises(KeyError):
# frm.replay_match_event(data)
if
__name__
==
"__main__"
:
unittest
.
main
()
...
...
src/football_replay_match/core/utils/football_replay_match_live.py
View file @
a4dfad4
...
...
@@ -82,7 +82,6 @@ class FootballReplayMatchLive:
# 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
}})
self
.
cache_dir
=
cache_dir
self
.
video2frame
=
Video2Frame
(
cache_dir
=
cache_dir
,
save_frames_enable
=
save_frames_enable
)
...
...
@@ -117,8 +116,10 @@ class FootballReplayMatchLive:
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
event_end
=
event_utc
+
timedelta
(
seconds
=
5
)
if
event_utc
is
not
None
else
None
# event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
event_start
=
event_utc
-
10000
if
event_utc
is
not
None
else
None
# event_end = event_utc + timedelta(seconds=5) if event_utc is not None else None
event_end
=
event_utc
+
5000
if
event_utc
is
not
None
else
None
live_video_contents
=
self
.
video2frame
.
to_llm_contents
(
live_video_path
,
cache
=
self
.
cache_dir
,
fps
=
2
,
...
...
@@ -132,6 +133,7 @@ class FootballReplayMatchLive:
live_map
[
live_video_id
]
=
{
"video_id"
:
live_video_id
,
"video_path"
:
live_video_path
,
"event_utc"
:
event_utc
,
"asr_text"
:
asr_text
,
"contents"
:
live_video_contents
,
...
...
@@ -139,6 +141,7 @@ class FootballReplayMatchLive:
live_records
[
live_video_id
]
=
{
"video_id"
:
live_video_id
,
"video_path"
:
live_video_path
,
"event_utc"
:
event_utc
,
"asr_text"
:
asr_text
,
}
live_videos_contents
.
extend
(
live_video_contents
)
...
...
@@ -207,6 +210,7 @@ class FootballReplayMatchLive:
def
match_batch
(
self
,
replay_video
:
dict
,
live_videos
:
list
[
dict
],
max_parallel
:
int
=
3
,
cache_dir
=
None
):
self
.
cache_dir
=
cache_dir
cache_path
=
os
.
path
.
join
(
cache_dir
,
'match_live.json'
)
if
cache_path
is
not
None
and
os
.
path
.
exists
(
cache_path
):
try
:
...
...
@@ -219,7 +223,7 @@ class FootballReplayMatchLive:
event_start
=
replay_video
.
get
(
"start_utc"
,
None
)
event_end
=
replay_video
.
get
(
"end_utc"
,
None
)
replay_video_contents
=
self
.
video2frame
.
to_llm_contents
(
replay_video_path
,
cache
=
self
.
cache_dir
,
cache
=
os
.
path
.
join
(
os
.
path
.
dirname
(
cache_dir
),
"replay"
)
,
fps
=
2
,
start
=
event_start
,
end
=
event_end
,
...
...
@@ -234,6 +238,7 @@ class FootballReplayMatchLive:
result_no_content
=
{
"video_id"
:
result
.
get
(
"video_id"
,
None
),
"video_path"
:
result
.
get
(
"video_path"
,
None
),
"event_utc"
:
result
.
get
(
"event_utc"
,
None
),
"asr_text"
:
result
.
get
(
"asr_text"
,
None
),
}
else
:
...
...
src/football_replay_match/core/utils/football_replay_video_event_by_llm.py
View file @
a4dfad4
...
...
@@ -78,7 +78,7 @@ class FootballReplayVideoEvent:
self
.
model
=
ChatOpenAI
(
base_url
=
base_url
,
model
=
model
,
temperature
=
temperature
,
api_key
=
api_key
,
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
)
def
video_event
(
self
,
replay_pack
:
dict
,
asr_text
:
str
=
'无'
,
cache_dir
=
None
):
...
...
@@ -92,7 +92,7 @@ class FootballReplayVideoEvent:
event_start
=
replay_pack
.
get
(
"start_utc"
,
None
)
event_end
=
replay_pack
.
get
(
"end_utc"
,
None
)
contents
=
self
.
video2frame
.
to_llm_contents
(
replay_video_path
,
cache
=
self
.
cache_dir
,
cache
=
cache_dir
,
fps
=
2
,
start
=
event_start
,
end
=
event_end
,
...
...
src/football_replay_match/core/utils/llm_image.py
View file @
a4dfad4
...
...
@@ -87,7 +87,9 @@ class Video2Frame:
if
cache_name
.
exists
():
logger
.
info
(
f
"use_cache: {cache_name}"
)
return
json
.
load
(
cache_name
.
open
(
'r'
))
# return json.load(cache_name.open('r'))
with
cache_name
.
open
(
'r'
,
encoding
=
'utf-8'
)
as
f
:
return
json
.
load
(
f
)
else
:
cache_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
data
=
{
...
...
@@ -117,7 +119,9 @@ class Video2Frame:
cv2
.
imwrite
(
frame_path
,
frame
)
cache_name
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
json
.
dump
(
data
,
cache_name
.
open
(
'w'
),
indent
=
4
)
# json.dump(data, cache_name.open('w'), indent=4)
with
cache_name
.
open
(
'w'
,
encoding
=
'utf-8'
)
as
f
:
json
.
dump
(
data
,
f
,
indent
=
4
)
logger
.
info
(
f
"save_cache: {cache_name}"
)
return
data
...
...
Please
register
or
login
to post a comment