Files
wehub-resource-sync 9a6da7d40d
Publish Docker image / Build and publish (push) Failing after 0s
CI / Python tests (push) Failing after 4s
chore: import upstream snapshot with attribution
2026-07-13 12:03:55 +08:00

304 lines
11 KiB
Python

import unittest
import os
import shutil
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
# add project root to python path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from app.services import task as tm
from app.models.schema import MaterialInfo, VideoParams
from app.utils import utils
resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources")
RUN_INTEGRATION_TESTS = os.environ.get("MPT_RUN_INTEGRATION_TESTS", "").lower() in {
"1",
"true",
"yes",
}
class TestTaskService(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_generate_script_forwards_advanced_prompt_options(self):
"""
任务生成入口和 WebUI/API 共用 VideoParams。这里验证自动生成文案时,
高级提示词参数会继续传到 LLM 服务层,避免只在 /scripts 接口生效。
"""
params = VideoParams(
video_subject="咖啡",
video_script="",
video_language="zh-CN",
paragraph_number=2,
video_script_prompt="语气轻松",
custom_system_prompt="Only write short narration.",
)
with patch.object(tm.llm, "generate_script", return_value="生成的文案") as generate:
result = tm.generate_script("task-id", params)
self.assertEqual(result, "生成的文案")
generate.assert_called_once_with(
video_subject="咖啡",
language="zh-CN",
paragraph_number=2,
video_script_prompt="语气轻松",
custom_system_prompt="Only write short narration.",
)
def test_generate_terms_uses_script_order_mode_when_enabled(self):
"""
默认模式不受影响;只有用户显式开启素材按文案顺序匹配时,任务层才
要求 LLM 生成有序关键词,并适当增加关键词数量以覆盖更多脚本片段。
"""
params = VideoParams(
video_subject="城市通勤",
video_script="",
match_materials_to_script=True,
)
with patch.object(tm.llm, "generate_terms", return_value=["city", "train"]) as generate:
result = tm.generate_terms("task-id", params, "先城市,再地铁")
self.assertEqual(result, ["city", "train"])
generate.assert_called_once_with(
video_subject="城市通勤",
video_script="先城市,再地铁",
amount=8,
match_script_order=True,
)
def test_generate_audio_uses_custom_file_inside_task_directory(self):
task_id = "test-custom-audio-safe"
task_dir = utils.task_dir(task_id)
custom_audio_file = os.path.join(task_dir, "custom-audio.mp3")
with open(custom_audio_file, "wb") as audio:
audio.write(b"fake audio")
params = VideoParams(
video_subject="custom audio",
video_script="",
custom_audio_file=custom_audio_file,
voice_name="test-voice",
)
try:
with (
patch.object(tm.voice, "tts") as tts,
patch.object(tm.voice, "get_audio_duration", return_value=7),
):
audio_file, audio_duration, sub_maker = tm.generate_audio(
task_id, params, "script"
)
finally:
shutil.rmtree(task_dir, ignore_errors=True)
self.assertEqual(audio_file, os.path.realpath(custom_audio_file))
self.assertEqual(audio_duration, 7)
self.assertIsNone(sub_maker)
tts.assert_not_called()
def test_generate_audio_accepts_server_side_custom_file(self):
task_id = "test-custom-audio-server-side"
task_dir = utils.task_dir(task_id)
with tempfile.NamedTemporaryFile(suffix=".mp3") as server_audio:
server_audio.write(b"fake audio")
server_audio.flush()
params = VideoParams(
video_subject="custom audio",
video_script="",
custom_audio_file=server_audio.name,
voice_name="test-voice",
)
try:
with (
patch.object(tm.voice, "tts") as tts,
patch.object(tm.voice, "get_audio_duration", return_value=6),
):
audio_file, audio_duration, result_sub_maker = tm.generate_audio(
task_id, params, "script"
)
finally:
shutil.rmtree(task_dir, ignore_errors=True)
self.assertEqual(audio_file, os.path.realpath(server_audio.name))
self.assertEqual(audio_duration, 6)
self.assertIsNone(result_sub_maker)
tts.assert_not_called()
def test_generate_audio_rejects_missing_custom_file_without_tts(self):
task_id = "test-custom-audio-missing"
task_dir = utils.task_dir(task_id)
missing_audio_file = os.path.join(task_dir, "missing.mp3")
params = VideoParams(
video_subject="custom audio",
video_script="",
custom_audio_file=missing_audio_file,
voice_name="test-voice",
)
try:
with (
patch.object(tm.voice, "tts") as tts,
patch.object(tm.sm.state, "update_task") as update_task,
):
audio_file, audio_duration, result_sub_maker = tm.generate_audio(
task_id, params, "script"
)
finally:
shutil.rmtree(task_dir, ignore_errors=True)
self.assertIsNone(audio_file)
self.assertIsNone(audio_duration)
self.assertIsNone(result_sub_maker)
tts.assert_not_called()
update_task.assert_called_with(task_id, state=tm.const.TASK_STATE_FAILED)
def test_generate_subtitle_uses_whisper_for_custom_audio_without_sub_maker(self):
"""
自定义音频不会经过 TTS,所以没有 sub_maker。
Whisper 可以直接从音频文件转写,此时不能被 sub_maker 为空的保护逻辑提前跳过。
"""
task_id = "test-custom-audio-whisper-subtitle"
task_dir = utils.task_dir(task_id)
audio_file = os.path.join(task_dir, "custom-audio.mp3")
Path(audio_file).write_bytes(b"fake audio")
params = VideoParams(
video_subject="custom audio",
video_script="Hello world.",
subtitle_enabled=True,
)
def fake_whisper_create(audio_file, subtitle_file):
Path(subtitle_file).write_text(
"1\n00:00:00,000 --> 00:00:01,000\nHello world.\n\n",
encoding="utf-8",
)
try:
with (
patch.object(
tm.config,
"app",
dict(tm.config.app, subtitle_provider="whisper"),
),
patch.object(
tm.subtitle, "create", side_effect=fake_whisper_create
) as create,
patch.object(tm.subtitle, "correct") as correct,
):
subtitle_path = tm.generate_subtitle(
task_id=task_id,
params=params,
video_script="Hello world.",
sub_maker=None,
audio_file=audio_file,
)
finally:
shutil.rmtree(task_dir, ignore_errors=True)
self.assertTrue(subtitle_path.endswith("subtitle.srt"))
create.assert_called_once_with(audio_file=audio_file, subtitle_file=subtitle_path)
correct.assert_called_once_with(
subtitle_file=subtitle_path, video_script="Hello world."
)
def test_generate_subtitle_skips_edge_provider_without_sub_maker(self):
"""
Edge 字幕依赖 TTS 返回的 sub_maker 时间轴。
自定义音频缺少该对象时应继续跳过,避免产生不可信的字幕时间轴。
"""
task_id = "test-custom-audio-edge-no-submaker"
task_dir = utils.task_dir(task_id)
audio_file = os.path.join(task_dir, "custom-audio.mp3")
Path(audio_file).write_bytes(b"fake audio")
params = VideoParams(
video_subject="custom audio",
video_script="Hello world.",
subtitle_enabled=True,
)
try:
with (
patch.object(
tm.config,
"app",
dict(tm.config.app, subtitle_provider="edge"),
),
patch.object(tm.voice, "create_subtitle") as create_subtitle,
patch.object(tm.subtitle, "create") as whisper_create,
):
subtitle_path = tm.generate_subtitle(
task_id=task_id,
params=params,
video_script="Hello world.",
sub_maker=None,
audio_file=audio_file,
)
finally:
shutil.rmtree(task_dir, ignore_errors=True)
self.assertEqual(subtitle_path, "")
create_subtitle.assert_not_called()
whisper_create.assert_not_called()
@unittest.skipUnless(
RUN_INTEGRATION_TESTS,
"MPT_RUN_INTEGRATION_TESTS not set",
)
def test_task_local_materials(self):
task_id = "00000000-0000-0000-0000-000000000000"
video_materials=[]
for i in range(1, 4):
video_materials.append(MaterialInfo(
provider="local",
url=os.path.join(resources_dir, f"{i}.png"),
duration=0
))
params = VideoParams(
video_subject="金钱的作用",
video_script="金钱不仅是交换媒介,更是社会资源的分配工具。它能满足基本生存需求,如食物和住房,也能提供教育、医疗等提升生活品质的机会。拥有足够的金钱意味着更多选择权,比如职业自由或创业可能。但金钱的作用也有边界,它无法直接购买幸福、健康或真诚的人际关系。过度追逐财富可能导致价值观扭曲,忽视精神层面的需求。理想的状态是理性看待金钱,将其作为实现目标的工具而非终极目的。",
video_terms="money importance, wealth and society, financial freedom, money and happiness, role of money",
video_aspect="9:16",
video_concat_mode="random",
video_transition_mode="None",
video_clip_duration=3,
video_count=1,
video_source="local",
video_materials=video_materials,
video_language="",
voice_name="zh-CN-XiaoxiaoNeural-Female",
voice_volume=1.0,
voice_rate=1.0,
bgm_type="random",
bgm_file="",
bgm_volume=0.2,
subtitle_enabled=True,
subtitle_position="bottom",
custom_position=70.0,
font_name="MicrosoftYaHeiBold.ttc",
text_fore_color="#FFFFFF",
text_background_color=True,
font_size=60,
stroke_color="#000000",
stroke_width=1.5,
n_threads=2,
paragraph_number=1
)
result = tm.start(task_id=task_id, params=params)
print(result)
if __name__ == "__main__":
unittest.main()