lizhengwei

jira:NYJ-1540 desc: add api_test.py

  1 +import os
1 import logging 2 import logging
2 from aabd.base.enhance_dict import value_or_default 3 from aabd.base.enhance_dict import value_or_default
3 4
@@ -18,25 +19,31 @@ class FootballReplayMatch: @@ -18,25 +19,31 @@ class FootballReplayMatch:
18 save_frames_enable = value_or_default(settings.save_frames_enable, False) 19 save_frames_enable = value_or_default(settings.save_frames_enable, False)
19 20
20 self.videoEventRecognition = FootballReplayVideoEvent(llm_base_url, model_name, temperature, 'no_key', self.cache_dir, save_frames_enable) 21 self.videoEventRecognition = FootballReplayVideoEvent(llm_base_url, model_name, temperature, 'no_key', self.cache_dir, save_frames_enable)
21 - self.videoMatchLive = FootballReplayMatchLive(llm_base_url, model_name, temperature, 'no_key', self.cache_dir) 22 + self.videoMatchLive = FootballReplayMatchLive(llm_base_url, model_name, temperature, 'no_key', self.cache_dir, save_frames_enable)
22 23
23 def match_by_time(self, replay, events): 24 def match_by_time(self, replay, events):
24 start_utc = replay.get('start_utc') 25 start_utc = replay.get('start_utc')
25 26
26 events = [(start_utc - e.get('event_utc'), e) for e in events] 27 events = [(start_utc - e.get('event_utc'), e) for e in events]
27 - events.sort(key=lambda x: abs(x[0]))  
28 - time_diff, event = events[0] 28 + events.sort(key=lambda x: x[0])
  29 +
  30 + target_index = next((i for i, e in enumerate(events) if e[0] > 0), -1) # 找到第一个 event_utc 早于 start_utc 的事件
  31 +
  32 + if target_index == -1:
  33 + return None
  34 +
  35 + time_diff, event = events[target_index]
29 if time_diff < self.match_by_time_threshold: 36 if time_diff < self.match_by_time_threshold:
30 return event 37 return event
31 else: 38 else:
32 return None 39 return None
33 40
34 - def det_goal_replay(self, replay):  
35 - return self.videoEventRecognition.video_event(replay, cache_dir=self.cache_dir) 41 + def det_goal_replay(self, replay, task_id):
  42 + return self.videoEventRecognition.video_event(replay, cache_dir=os.path.join(self.cache_dir, task_id, "replay"))
36 43
37 44
38 - def match_by_llm(self, replay, events):  
39 - return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=self.cache_dir) 45 + def match_by_llm(self, replay, events, task_id):
  46 + return self.videoMatchLive.match_batch(replay, events, max_parallel=2, cache_dir=os.path.join(self.cache_dir, task_id, "envents"))
40 47
41 def replay_match_event(self, data): 48 def replay_match_event(self, data):
42 """ 49 """
@@ -72,22 +79,25 @@ class FootballReplayMatch: @@ -72,22 +79,25 @@ class FootballReplayMatch:
72 goal_live_events = [e for e in live_events if str(e.get('type', '')) == '1'] 79 goal_live_events = [e for e in live_events if str(e.get('type', '')) == '1']
73 80
74 matched_event = self.match_by_time(replay_info, goal_live_events) 81 matched_event = self.match_by_time(replay_info, goal_live_events)
75 -  
76 if matched_event is None: 82 if matched_event is None:
77 - replay_det_info = self.det_goal_replay(replay_info) 83 + replay_det_info = self.det_goal_replay(replay_info, task_id)
78 replay_event_name = replay_det_info.get('event_name', '') 84 replay_event_name = replay_det_info.get('event_name', '')
79 if replay_event_name == '进球': 85 if replay_event_name == '进球':
80 - matched_event = self.match_by_llm(replay_info, goal_live_events) 86 + matched_event = self.match_by_llm(replay_info, goal_live_events, task_id)
81 if matched_event is None: 87 if matched_event is None:
82 logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件") 88 logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")
  89 + # print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 是进球但是未能找到匹配的事件")
83 else: 90 else:
84 matched_event_id = matched_event.get('video_id') 91 matched_event_id = matched_event.get('video_id')
85 logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}") 92 logger.info(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
  93 + # print(f"{task_id}_LLM认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
86 else: 94 else:
87 logger.info(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配") 95 logger.info(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")
  96 + # print(f"{task_id}_LLM判断{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 不是进球,无需匹配")
88 else: 97 else:
89 matched_event_id = matched_event.get('id') 98 matched_event_id = matched_event.get('id')
90 logger.info(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}") 99 logger.info(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
  100 + # print(f"{task_id}_通过时间找到匹配认为视频{replay_info['url']}_{replay_info['start_utc']}_{replay_info['end_utc']} 对应的进球的事件视频是{matched_event_id}")
91 return { 101 return {
92 "id": task_id, 102 "id": task_id,
93 "match_id": match_id, 103 "match_id": match_id,
  1 +"""
  2 +api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测试
  3 +
  4 +使用伪数据覆盖主要分支:
  5 +1. 时间阈值内直接匹配成功
  6 +2. 时间匹配失败 + LLM 判定不是进球
  7 +3. 时间匹配失败 + LLM 判定是进球 + LLM 匹配成功
  8 +4. 时间匹配失败 + LLM 判定是进球 + LLM 匹配失败
  9 +5. 无进球事件 / events 为空
  10 +6. 边界:缺少 replay 键时抛出异常
  11 +"""
  12 +
  13 +import unittest
  14 +from types import SimpleNamespace
  15 +from unittest.mock import MagicMock, patch
  16 +
  17 +try:
  18 + from api import FootballReplayMatch
  19 +except ImportError:
  20 + from .api import FootballReplayMatch
  21 +
  22 +
  23 +def _fake_settings(match_by_time_threshold: int = 30):
  24 + """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。"""
  25 + return SimpleNamespace(
  26 + match_by_time_threshold=match_by_time_threshold,
  27 + 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),
  28 + common=SimpleNamespace(cache_dir="/root/lzw/tmp_0518_replay_cache0521_01"),
  29 + save_frames_enable=True,
  30 + )
  31 +
  32 +
  33 +def _base_data():
  34 + """返回一份基础伪数据,replay.start_utc = 1000(毫秒)。"""
  35 +
  36 + url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
  37 + return {
  38 + "id": "task_001",
  39 + "match_id": "match_001",
  40 + "replay": {
  41 + "id": "replay_001",
  42 + "url": url,
  43 + "start_utc": 1777444502543,
  44 + "end_utc": 1777444531063
  45 + },
  46 + "events": [
  47 + {"id": "event_001", "type": "1", "url": url, "event_utc": 1777444502543-40000},
  48 + {"id": "event_001", "type": "1", "url": url, "event_utc": 1777444502543-20000},
  49 + {"id": "event_001", "type": "1", "url": url, "event_utc": 1777444531063+20000}
  50 + ],
  51 + }
  52 +
  53 +
  54 +class TestReplayMatchEvent(unittest.TestCase):
  55 +
  56 + @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  57 + @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  58 + def test_match_by_time_success(self, mock_video_cls, mock_live_cls):
  59 + """场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
  60 + settings = _fake_settings(match_by_time_threshold=10) # threshold = 30 * 1000 = 30000 ms
  61 + frm = FootballReplayMatch(settings)
  62 +
  63 + data = _base_data()
  64 + result = frm.replay_match_event(data)
  65 +
  66 + print(result)
  67 + # exit()
  68 +
  69 + # self.assertEqual(result["id"], "task_001")
  70 + # self.assertEqual(result["match_id"], "match_001")
  71 + # self.assertEqual(result["replay_id"], "replay_001")
  72 + # # event_001 与 replay.start_utc 差值 20 ms < 30000 ms,应命中 event_001 的 id
  73 + # self.assertEqual(result["event_id"], "event_001")
  74 + # # 未调用 LLM 相关方法
  75 + # mock_video_cls.return_value.video_event.assert_not_called()
  76 + # mock_live_cls.return_value.match_batch.assert_not_called()
  77 +
  78 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  79 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  80 + # def test_match_by_time_fail_then_not_goal(self, mock_video_cls, mock_live_cls):
  81 + # """场景2:时间匹配失败,det_goal_replay 判定不是进球。"""
  82 + # mock_video_instance = MagicMock()
  83 + # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "宣传片"}
  84 + # mock_video_cls.return_value = mock_video_instance
  85 +
  86 + # settings = _fake_settings(match_by_time_threshold=30)
  87 + # frm = FootballReplayMatch(settings)
  88 +
  89 + # data = _base_data()
  90 + # result = frm.replay_match_event(data)
  91 +
  92 + # self.assertEqual(result["event_id"], None)
  93 + # mock_video_instance.video_event.assert_called_once()
  94 + # mock_live_cls.return_value.match_batch.assert_not_called()
  95 +
  96 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  97 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  98 + # def test_match_by_time_fail_then_goal_then_llm_match(self, mock_video_cls, mock_live_cls):
  99 + # """场景3:时间匹配失败 -> 判定是进球 -> LLM 匹配成功。"""
  100 + # mock_video_instance = MagicMock()
  101 + # mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "世界波"}
  102 + # mock_video_cls.return_value = mock_video_instance
  103 +
  104 + # mock_live_instance = MagicMock()
  105 + # mock_live_instance.match_batch.return_value = {"video_id": "event_003", "video_path": "http://example.com/event3.mp4", "asr_text": ""}
  106 + # mock_live_cls.return_value = mock_live_instance
  107 +
  108 + # settings = _fake_settings(match_by_time_threshold=30)
  109 + # frm = FootballReplayMatch(settings)
  110 +
  111 + # data = _base_data()
  112 + # result = frm.replay_match_event(data)
  113 +
  114 + # self.assertEqual(result["event_id"], "event_003")
  115 + # mock_video_instance.video_event.assert_called_once()
  116 + # mock_live_instance.match_batch.assert_called_once()
  117 +
  118 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  119 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  120 + # def test_match_by_time_fail_then_goal_then_llm_no_match(self, mock_video_cls, mock_live_cls):
  121 + # """场景4:时间匹配失败 -> 判定是进球 -> LLM 未匹配到任何事件。"""
  122 + # mock_video_instance = MagicMock()
  123 + # mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "头球破门"}
  124 + # mock_video_cls.return_value = mock_video_instance
  125 +
  126 + # mock_live_instance = MagicMock()
  127 + # mock_live_instance.match_batch.return_value = None
  128 + # mock_live_cls.return_value = mock_live_instance
  129 +
  130 + # settings = _fake_settings(match_by_time_threshold=30)
  131 + # frm = FootballReplayMatch(settings)
  132 +
  133 + # data = _base_data()
  134 + # result = frm.replay_match_event(data)
  135 +
  136 + # self.assertEqual(result["event_id"], None)
  137 + # mock_video_instance.video_event.assert_called_once()
  138 + # mock_live_instance.match_batch.assert_called_once()
  139 +
  140 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  141 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  142 + # def test_no_goal_events(self, mock_video_cls, mock_live_cls):
  143 + # """场景5:events 中没有任何 type='1' 的进球事件。"""
  144 + # mock_video_instance = MagicMock()
  145 + # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "无候选事件"}
  146 + # mock_video_cls.return_value = mock_video_instance
  147 +
  148 + # settings = _fake_settings(match_by_time_threshold=30)
  149 + # frm = FootballReplayMatch(settings)
  150 +
  151 + # data = _base_data()
  152 + # data["events"] = [
  153 + # {"id": "event_005", "type": "2", "url": "http://example.com/event5.mp4", "event_utc": 1020},
  154 + # ]
  155 + # result = frm.replay_match_event(data)
  156 +
  157 + # self.assertEqual(result["event_id"], None)
  158 + # mock_video_instance.video_event.assert_called_once()
  159 + # mock_live_cls.return_value.match_batch.assert_not_called()
  160 +
  161 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  162 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  163 + # def test_empty_events(self, mock_video_cls, mock_live_cls):
  164 + # """场景6:events 为空列表。"""
  165 + # mock_video_instance = MagicMock()
  166 + # mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "空列表"}
  167 + # mock_video_cls.return_value = mock_video_instance
  168 +
  169 + # settings = _fake_settings(match_by_time_threshold=30)
  170 + # frm = FootballReplayMatch(settings)
  171 +
  172 + # data = _base_data()
  173 + # data["events"] = []
  174 + # result = frm.replay_match_event(data)
  175 +
  176 + # self.assertEqual(result["event_id"], None)
  177 + # mock_video_instance.video_event.assert_called_once()
  178 + # mock_live_cls.return_value.match_batch.assert_not_called()
  179 +
  180 + # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
  181 + # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
  182 + # def test_missing_replay_key(self, mock_video_cls, mock_live_cls):
  183 + # """场景7:data 缺少 replay 键,应抛出 KeyError。"""
  184 + # settings = _fake_settings(match_by_time_threshold=30)
  185 + # frm = FootballReplayMatch(settings)
  186 +
  187 + # data = _base_data()
  188 + # del data["replay"]
  189 +
  190 + # with self.assertRaises(KeyError):
  191 + # frm.replay_match_event(data)
  192 +
  193 +
  194 +if __name__ == "__main__":
  195 + unittest.main()
@@ -82,7 +82,6 @@ class FootballReplayMatchLive: @@ -82,7 +82,6 @@ class FootballReplayMatchLive:
82 # api_key='no_key') 82 # api_key='no_key')
83 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key, 83 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
84 extra_body={"chat_template_kwargs": {"enable_thinking": False}}) 84 extra_body={"chat_template_kwargs": {"enable_thinking": False}})
85 -  
86 self.cache_dir = cache_dir 85 self.cache_dir = cache_dir
87 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable) 86 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
88 87
@@ -117,8 +116,10 @@ class FootballReplayMatchLive: @@ -117,8 +116,10 @@ class FootballReplayMatchLive:
117 asr_text = live_video.get("asr_text", '') 116 asr_text = live_video.get("asr_text", '')
118 live_video_contents = live_video.get("llm_contents", None) 117 live_video_contents = live_video.get("llm_contents", None)
119 if live_video_contents is None: 118 if live_video_contents is None:
120 - event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None  
121 - event_end = event_utc + timedelta(seconds=5) if event_utc is not None else None 119 + # event_start = event_utc - timedelta(seconds=10) if event_utc is not None else None
  120 + event_start = event_utc - 10000 if event_utc is not None else None
  121 + # event_end = event_utc + timedelta(seconds=5) if event_utc is not None else None
  122 + event_end = event_utc + 5000 if event_utc is not None else None
122 live_video_contents = self.video2frame.to_llm_contents( live_video_path, 123 live_video_contents = self.video2frame.to_llm_contents( live_video_path,
123 cache=self.cache_dir, 124 cache=self.cache_dir,
124 fps=2, 125 fps=2,
@@ -132,6 +133,7 @@ class FootballReplayMatchLive: @@ -132,6 +133,7 @@ class FootballReplayMatchLive:
132 live_map[live_video_id] = { 133 live_map[live_video_id] = {
133 "video_id": live_video_id, 134 "video_id": live_video_id,
134 "video_path": live_video_path, 135 "video_path": live_video_path,
  136 + "event_utc": event_utc,
135 "asr_text": asr_text, 137 "asr_text": asr_text,
136 "contents": live_video_contents, 138 "contents": live_video_contents,
137 139
@@ -139,6 +141,7 @@ class FootballReplayMatchLive: @@ -139,6 +141,7 @@ class FootballReplayMatchLive:
139 live_records[live_video_id] = { 141 live_records[live_video_id] = {
140 "video_id": live_video_id, 142 "video_id": live_video_id,
141 "video_path": live_video_path, 143 "video_path": live_video_path,
  144 + "event_utc": event_utc,
142 "asr_text": asr_text, 145 "asr_text": asr_text,
143 } 146 }
144 live_videos_contents.extend(live_video_contents) 147 live_videos_contents.extend(live_video_contents)
@@ -207,6 +210,7 @@ class FootballReplayMatchLive: @@ -207,6 +210,7 @@ class FootballReplayMatchLive:
207 210
208 def match_batch(self, replay_video: dict, live_videos: list[dict], max_parallel: int = 3, cache_dir=None): 211 def match_batch(self, replay_video: dict, live_videos: list[dict], max_parallel: int = 3, cache_dir=None):
209 212
  213 + self.cache_dir = cache_dir
210 cache_path = os.path.join(cache_dir, 'match_live.json') 214 cache_path = os.path.join(cache_dir, 'match_live.json')
211 if cache_path is not None and os.path.exists(cache_path): 215 if cache_path is not None and os.path.exists(cache_path):
212 try: 216 try:
@@ -219,7 +223,7 @@ class FootballReplayMatchLive: @@ -219,7 +223,7 @@ class FootballReplayMatchLive:
219 event_start = replay_video.get("start_utc", None) 223 event_start = replay_video.get("start_utc", None)
220 event_end = replay_video.get("end_utc", None) 224 event_end = replay_video.get("end_utc", None)
221 replay_video_contents = self.video2frame.to_llm_contents(replay_video_path, 225 replay_video_contents = self.video2frame.to_llm_contents(replay_video_path,
222 - cache=self.cache_dir, 226 + cache=os.path.join(os.path.dirname(cache_dir), "replay"),
223 fps=2, 227 fps=2,
224 start=event_start, 228 start=event_start,
225 end=event_end, 229 end=event_end,
@@ -234,6 +238,7 @@ class FootballReplayMatchLive: @@ -234,6 +238,7 @@ class FootballReplayMatchLive:
234 result_no_content = { 238 result_no_content = {
235 "video_id": result.get("video_id", None), 239 "video_id": result.get("video_id", None),
236 "video_path": result.get("video_path", None), 240 "video_path": result.get("video_path", None),
  241 + "event_utc": result.get("event_utc", None),
237 "asr_text": result.get("asr_text", None), 242 "asr_text": result.get("asr_text", None),
238 } 243 }
239 else: 244 else:
@@ -78,7 +78,7 @@ class FootballReplayVideoEvent: @@ -78,7 +78,7 @@ class FootballReplayVideoEvent:
78 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key, 78 self.model = ChatOpenAI(base_url=base_url, model=model, temperature=temperature, api_key=api_key,
79 extra_body={"chat_template_kwargs": {"enable_thinking": False}}) 79 extra_body={"chat_template_kwargs": {"enable_thinking": False}})
80 80
81 - self.cache_dir = cache_dir 81 +
82 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable) 82 self.video2frame = Video2Frame(cache_dir=cache_dir, save_frames_enable=save_frames_enable)
83 83
84 def video_event(self, replay_pack: dict, asr_text: str = '无', cache_dir=None): 84 def video_event(self, replay_pack: dict, asr_text: str = '无', cache_dir=None):
@@ -92,7 +92,7 @@ class FootballReplayVideoEvent: @@ -92,7 +92,7 @@ class FootballReplayVideoEvent:
92 event_start = replay_pack.get("start_utc", None) 92 event_start = replay_pack.get("start_utc", None)
93 event_end = replay_pack.get("end_utc", None) 93 event_end = replay_pack.get("end_utc", None)
94 contents = self.video2frame.to_llm_contents(replay_video_path, 94 contents = self.video2frame.to_llm_contents(replay_video_path,
95 - cache=self.cache_dir, 95 + cache=cache_dir,
96 fps=2, 96 fps=2,
97 start=event_start, 97 start=event_start,
98 end=event_end, 98 end=event_end,
@@ -87,7 +87,9 @@ class Video2Frame: @@ -87,7 +87,9 @@ class Video2Frame:
87 87
88 if cache_name.exists(): 88 if cache_name.exists():
89 logger.info(f"use_cache: {cache_name}") 89 logger.info(f"use_cache: {cache_name}")
90 - return json.load(cache_name.open('r')) 90 + # return json.load(cache_name.open('r'))
  91 + with cache_name.open('r', encoding='utf-8') as f:
  92 + return json.load(f)
91 else: 93 else:
92 cache_dir.mkdir(parents=True, exist_ok=True) 94 cache_dir.mkdir(parents=True, exist_ok=True)
93 data = { 95 data = {
@@ -117,7 +119,9 @@ class Video2Frame: @@ -117,7 +119,9 @@ class Video2Frame:
117 cv2.imwrite(frame_path, frame) 119 cv2.imwrite(frame_path, frame)
118 120
119 cache_name.parent.mkdir(parents=True, exist_ok=True) 121 cache_name.parent.mkdir(parents=True, exist_ok=True)
120 - json.dump(data, cache_name.open('w'), indent=4) 122 + # json.dump(data, cache_name.open('w'), indent=4)
  123 + with cache_name.open('w', encoding='utf-8') as f:
  124 + json.dump(data, f, indent=4)
121 logger.info(f"save_cache: {cache_name}") 125 logger.info(f"save_cache: {cache_name}")
122 return data 126 return data
123 127