返回 MoneyPrinterTurbo
test_task.py
根目录 / test / services / test_task.py
1 import unittest
2 import os
3 import shutil
4 import sys
5 import tempfile
6 from pathlib import Path
7 from unittest.mock import patch
8
9 # add project root to python path
10 sys.path.insert(0, str(Path(__file__).parent.parent.parent))
11
12 from app.services import task as tm
13 from app.models.schema import MaterialInfo, VideoParams
14 from app.utils import utils
15
16 resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources")
17 RUN_INTEGRATION_TESTS = os.environ.get("MPT_RUN_INTEGRATION_TESTS", "").lower() in {
18 "1",
19 "true",
20 "yes",
21 }
22
23 class TestTaskService(unittest.TestCase):
24 def setUp(self):
25 pass
26
27 def tearDown(self):
28 pass
29
30 def test_generate_script_forwards_advanced_prompt_options(self):
31 """
32 任务生成入口和 WebUI/API 共用 VideoParams。这里验证自动生成文案时,
33 高级提示词参数会继续传到 LLM 服务层,避免只在 /scripts 接口生效。
34 """
35 params = VideoParams(
36 video_subject="咖啡",
37 video_script="",
38 video_language="zh-CN",
39 paragraph_number=2,
40 video_script_prompt="语气轻松",
41 custom_system_prompt="Only write short narration.",
42 )
43
44 with patch.object(tm.llm, "generate_script", return_value="生成的文案") as generate:
45 result = tm.generate_script("task-id", params)
46
47 self.assertEqual(result, "生成的文案")
48 generate.assert_called_once_with(
49 video_subject="咖啡",
50 language="zh-CN",
51 paragraph_number=2,
52 video_script_prompt="语气轻松",
53 custom_system_prompt="Only write short narration.",
54 )
55
56 def test_generate_terms_uses_script_order_mode_when_enabled(self):
57 """
58 默认模式不受影响;只有用户显式开启素材按文案顺序匹配时,任务层才
59 要求 LLM 生成有序关键词,并适当增加关键词数量以覆盖更多脚本片段。
60 """
61 params = VideoParams(
62 video_subject="城市通勤",
63 video_script="",
64 match_materials_to_script=True,
65 )
66
67 with patch.object(tm.llm, "generate_terms", return_value=["city", "train"]) as generate:
68 result = tm.generate_terms("task-id", params, "先城市,再地铁")
69
70 self.assertEqual(result, ["city", "train"])
71 generate.assert_called_once_with(
72 video_subject="城市通勤",
73 video_script="先城市,再地铁",
74 amount=8,
75 match_script_order=True,
76 )
77
78 def test_generate_audio_uses_custom_file_inside_task_directory(self):
79 task_id = "test-custom-audio-safe"
80 task_dir = utils.task_dir(task_id)
81 custom_audio_file = os.path.join(task_dir, "custom-audio.mp3")
82 with open(custom_audio_file, "wb") as audio:
83 audio.write(b"fake audio")
84
85 params = VideoParams(
86 video_subject="custom audio",
87 video_script="",
88 custom_audio_file=custom_audio_file,
89 voice_name="test-voice",
90 )
91
92 try:
93 with (
94 patch.object(tm.voice, "tts") as tts,
95 patch.object(tm.voice, "get_audio_duration", return_value=7),
96 ):
97 audio_file, audio_duration, sub_maker = tm.generate_audio(
98 task_id, params, "script"
99 )
100 finally:
101 shutil.rmtree(task_dir, ignore_errors=True)
102
103 self.assertEqual(audio_file, os.path.realpath(custom_audio_file))
104 self.assertEqual(audio_duration, 7)
105 self.assertIsNone(sub_maker)
106 tts.assert_not_called()
107
108 def test_generate_audio_accepts_server_side_custom_file(self):
109 task_id = "test-custom-audio-server-side"
110 task_dir = utils.task_dir(task_id)
111
112 with tempfile.NamedTemporaryFile(suffix=".mp3") as server_audio:
113 server_audio.write(b"fake audio")
114 server_audio.flush()
115 params = VideoParams(
116 video_subject="custom audio",
117 video_script="",
118 custom_audio_file=server_audio.name,
119 voice_name="test-voice",
120 )
121
122 try:
123 with (
124 patch.object(tm.voice, "tts") as tts,
125 patch.object(tm.voice, "get_audio_duration", return_value=6),
126 ):
127 audio_file, audio_duration, result_sub_maker = tm.generate_audio(
128 task_id, params, "script"
129 )
130 finally:
131 shutil.rmtree(task_dir, ignore_errors=True)
132
133 self.assertEqual(audio_file, os.path.realpath(server_audio.name))
134 self.assertEqual(audio_duration, 6)
135 self.assertIsNone(result_sub_maker)
136 tts.assert_not_called()
137
138 def test_generate_audio_rejects_missing_custom_file_without_tts(self):
139 task_id = "test-custom-audio-missing"
140 task_dir = utils.task_dir(task_id)
141 missing_audio_file = os.path.join(task_dir, "missing.mp3")
142 params = VideoParams(
143 video_subject="custom audio",
144 video_script="",
145 custom_audio_file=missing_audio_file,
146 voice_name="test-voice",
147 )
148
149 try:
150 with (
151 patch.object(tm.voice, "tts") as tts,
152 patch.object(tm.sm.state, "update_task") as update_task,
153 ):
154 audio_file, audio_duration, result_sub_maker = tm.generate_audio(
155 task_id, params, "script"
156 )
157 finally:
158 shutil.rmtree(task_dir, ignore_errors=True)
159
160 self.assertIsNone(audio_file)
161 self.assertIsNone(audio_duration)
162 self.assertIsNone(result_sub_maker)
163 tts.assert_not_called()
164 update_task.assert_called_with(task_id, state=tm.const.TASK_STATE_FAILED)
165
166 def test_generate_subtitle_uses_whisper_for_custom_audio_without_sub_maker(self):
167 """
168 自定义音频不会经过 TTS,所以没有 sub_maker。
169 Whisper 可以直接从音频文件转写,此时不能被 sub_maker 为空的保护逻辑提前跳过。
170 """
171 task_id = "test-custom-audio-whisper-subtitle"
172 task_dir = utils.task_dir(task_id)
173 audio_file = os.path.join(task_dir, "custom-audio.mp3")
174 Path(audio_file).write_bytes(b"fake audio")
175 params = VideoParams(
176 video_subject="custom audio",
177 video_script="Hello world.",
178 subtitle_enabled=True,
179 )
180
181 def fake_whisper_create(audio_file, subtitle_file):
182 Path(subtitle_file).write_text(
183 "1\n00:00:00,000 --> 00:00:01,000\nHello world.\n\n",
184 encoding="utf-8",
185 )
186
187 try:
188 with (
189 patch.object(
190 tm.config,
191 "app",
192 dict(tm.config.app, subtitle_provider="whisper"),
193 ),
194 patch.object(
195 tm.subtitle, "create", side_effect=fake_whisper_create
196 ) as create,
197 patch.object(tm.subtitle, "correct") as correct,
198 ):
199 subtitle_path = tm.generate_subtitle(
200 task_id=task_id,
201 params=params,
202 video_script="Hello world.",
203 sub_maker=None,
204 audio_file=audio_file,
205 )
206 finally:
207 shutil.rmtree(task_dir, ignore_errors=True)
208
209 self.assertTrue(subtitle_path.endswith("subtitle.srt"))
210 create.assert_called_once_with(audio_file=audio_file, subtitle_file=subtitle_path)
211 correct.assert_called_once_with(
212 subtitle_file=subtitle_path, video_script="Hello world."
213 )
214
215 def test_generate_subtitle_skips_edge_provider_without_sub_maker(self):
216 """
217 Edge 字幕依赖 TTS 返回的 sub_maker 时间轴。
218 自定义音频缺少该对象时应继续跳过,避免产生不可信的字幕时间轴。
219 """
220 task_id = "test-custom-audio-edge-no-submaker"
221 task_dir = utils.task_dir(task_id)
222 audio_file = os.path.join(task_dir, "custom-audio.mp3")
223 Path(audio_file).write_bytes(b"fake audio")
224 params = VideoParams(
225 video_subject="custom audio",
226 video_script="Hello world.",
227 subtitle_enabled=True,
228 )
229
230 try:
231 with (
232 patch.object(
233 tm.config,
234 "app",
235 dict(tm.config.app, subtitle_provider="edge"),
236 ),
237 patch.object(tm.voice, "create_subtitle") as create_subtitle,
238 patch.object(tm.subtitle, "create") as whisper_create,
239 ):
240 subtitle_path = tm.generate_subtitle(
241 task_id=task_id,
242 params=params,
243 video_script="Hello world.",
244 sub_maker=None,
245 audio_file=audio_file,
246 )
247 finally:
248 shutil.rmtree(task_dir, ignore_errors=True)
249
250 self.assertEqual(subtitle_path, "")
251 create_subtitle.assert_not_called()
252 whisper_create.assert_not_called()
253
254 @unittest.skipUnless(
255 RUN_INTEGRATION_TESTS,
256 "MPT_RUN_INTEGRATION_TESTS not set",
257 )
258 def test_task_local_materials(self):
259 task_id = "00000000-0000-0000-0000-000000000000"
260 video_materials=[]
261 for i in range(1, 4):
262 video_materials.append(MaterialInfo(
263 provider="local",
264 url=os.path.join(resources_dir, f"{i}.png"),
265 duration=0
266 ))
267
268 params = VideoParams(
269 video_subject="金钱的作用",
270 video_script="金钱不仅是交换媒介,更是社会资源的分配工具。它能满足基本生存需求,如食物和住房,也能提供教育、医疗等提升生活品质的机会。拥有足够的金钱意味着更多选择权,比如职业自由或创业可能。但金钱的作用也有边界,它无法直接购买幸福、健康或真诚的人际关系。过度追逐财富可能导致价值观扭曲,忽视精神层面的需求。理想的状态是理性看待金钱,将其作为实现目标的工具而非终极目的。",
271 video_terms="money importance, wealth and society, financial freedom, money and happiness, role of money",
272 video_aspect="9:16",
273 video_concat_mode="random",
274 video_transition_mode="None",
275 video_clip_duration=3,
276 video_count=1,
277 video_source="local",
278 video_materials=video_materials,
279 video_language="",
280 voice_name="zh-CN-XiaoxiaoNeural-Female",
281 voice_volume=1.0,
282 voice_rate=1.0,
283 bgm_type="random",
284 bgm_file="",
285 bgm_volume=0.2,
286 subtitle_enabled=True,
287 subtitle_position="bottom",
288 custom_position=70.0,
289 font_name="MicrosoftYaHeiBold.ttc",
290 text_fore_color="#FFFFFF",
291 text_background_color=True,
292 font_size=60,
293 stroke_color="#000000",
294 stroke_width=1.5,
295 n_threads=2,
296 paragraph_number=1
297 )
298 result = tm.start(task_id=task_id, params=params)
299 print(result)
300
301
302 if __name__ == "__main__":
303 unittest.main()
304
304 lines PYTHON