| 1 | import sys |
| 2 | import unittest |
| 3 | from pathlib import Path |
| 4 | from unittest.mock import patch |
| 5 | |
| 6 | sys.path.insert(0, str(Path(__file__).parent.parent.parent)) |
| 7 | |
| 8 | import cli |
| 9 | |
| 10 | |
| 11 | class TestCli(unittest.TestCase): |
| 12 | def test_build_video_params_with_local_materials(self): |
| 13 | args = cli.parse_args( |
| 14 | [ |
| 15 | "--video-subject", |
| 16 | "测试主题", |
| 17 | "--video-source", |
| 18 | "local", |
| 19 | "--video-materials", |
| 20 | "a.mp4, ,b.jpg,", |
| 21 | "--video-terms", |
| 22 | "foo, bar", |
| 23 | ] |
| 24 | ) |
| 25 | |
| 26 | params = cli.build_video_params(args) |
| 27 | materials = params.video_materials |
| 28 | |
| 29 | self.assertEqual(params.video_subject, "测试主题") |
| 30 | self.assertEqual(params.video_source, "local") |
| 31 | self.assertEqual([m.url for m in materials], ["a.mp4", "b.jpg"]) |
| 32 | self.assertTrue(all(m.provider == "local" for m in materials)) |
| 33 | self.assertEqual(params.video_terms, ["foo", "bar"]) |
| 34 | |
| 35 | def test_run_cli_dispatches_task_start(self): |
| 36 | with patch.object(cli.tm, "start", return_value={"script": "ok"}) as start, patch.object( |
| 37 | cli.utils, "get_uuid", return_value="task-123" |
| 38 | ), patch("builtins.print") as print_mock: |
| 39 | code = cli.run_cli(["--video-subject", "命令行测试", "--stop-at", "script"]) |
| 40 | |
| 41 | self.assertEqual(code, 0) |
| 42 | self.assertTrue(start.called) |
| 43 | kwargs = start.call_args.kwargs |
| 44 | self.assertEqual(kwargs["task_id"], "task-123") |
| 45 | self.assertEqual(kwargs["stop_at"], "script") |
| 46 | self.assertEqual(kwargs["params"].video_subject, "命令行测试") |
| 47 | print_mock.assert_called_once() |
| 48 | |
| 49 | def test_run_cli_returns_error_when_task_fails(self): |
| 50 | with patch.object(cli.tm, "start", return_value=None), patch.object( |
| 51 | cli.utils, "get_uuid", return_value="task-456" |
| 52 | ), patch.object(cli.logger, "error") as log_error: |
| 53 | code = cli.run_cli(["--video-subject", "失败场景"]) |
| 54 | |
| 55 | self.assertEqual(code, 1) |
| 56 | log_error.assert_called_once() |
| 57 | |
| 58 | def test_subtitle_enabled_by_default(self): |
| 59 | args = cli.parse_args(["--video-subject", "test"]) |
| 60 | params = cli.build_video_params(args) |
| 61 | self.assertTrue(params.subtitle_enabled) |
| 62 | |
| 63 | def test_subtitle_disabled_with_no_flag(self): |
| 64 | args = cli.parse_args(["--video-subject", "test", "--no-subtitle-enabled"]) |
| 65 | params = cli.build_video_params(args) |
| 66 | self.assertFalse(params.subtitle_enabled) |
| 67 | |
| 68 | def test_coverr_video_source_accepted(self): |
| 69 | args = cli.parse_args(["--video-subject", "test", "--video-source", "coverr"]) |
| 70 | params = cli.build_video_params(args) |
| 71 | self.assertEqual(params.video_source, "coverr") |
| 72 | |
| 73 | def test_build_video_params_with_script_video_and_audio_options(self): |
| 74 | args = cli.parse_args( |
| 75 | [ |
| 76 | "--video-subject", |
| 77 | "test", |
| 78 | "--video-language", |
| 79 | "en", |
| 80 | "--paragraph-number", |
| 81 | "3", |
| 82 | "--video-script-prompt", |
| 83 | "use a lighter tone", |
| 84 | "--custom-system-prompt", |
| 85 | "write concise short-form scripts", |
| 86 | "--video-concat-mode", |
| 87 | "sequential", |
| 88 | "--video-transition-mode", |
| 89 | "fade-in", |
| 90 | "--video-clip-duration", |
| 91 | "4", |
| 92 | "--match-materials-to-script", |
| 93 | "--voice-volume", |
| 94 | "1.2", |
| 95 | "--voice-rate", |
| 96 | "1.1", |
| 97 | "--bgm-type", |
| 98 | "custom", |
| 99 | "--bgm-file", |
| 100 | "output001.mp3", |
| 101 | "--bgm-volume", |
| 102 | "0.3", |
| 103 | ] |
| 104 | ) |
| 105 | |
| 106 | params = cli.build_video_params(args) |
| 107 | |
| 108 | self.assertEqual(params.video_language, "en") |
| 109 | self.assertEqual(params.paragraph_number, 3) |
| 110 | self.assertEqual(params.video_script_prompt, "use a lighter tone") |
| 111 | self.assertEqual(params.custom_system_prompt, "write concise short-form scripts") |
| 112 | self.assertEqual(params.video_concat_mode, "sequential") |
| 113 | self.assertEqual(params.video_transition_mode, "FadeIn") |
| 114 | self.assertEqual(params.video_clip_duration, 4) |
| 115 | self.assertTrue(params.match_materials_to_script) |
| 116 | self.assertEqual(params.voice_volume, 1.2) |
| 117 | self.assertEqual(params.voice_rate, 1.1) |
| 118 | self.assertEqual(params.bgm_type, "custom") |
| 119 | self.assertEqual(params.bgm_file, "output001.mp3") |
| 120 | self.assertEqual(params.bgm_volume, 0.3) |
| 121 | |
| 122 | def test_build_video_params_with_subtitle_style_options(self): |
| 123 | args = cli.parse_args( |
| 124 | [ |
| 125 | "--video-subject", |
| 126 | "test", |
| 127 | "--font-name", |
| 128 | "MicrosoftYaHeiBold.ttc", |
| 129 | "--subtitle-position", |
| 130 | "custom", |
| 131 | "--custom-position", |
| 132 | "42.5", |
| 133 | "--text-fore-color", |
| 134 | "#AABBCC", |
| 135 | "--font-size", |
| 136 | "72", |
| 137 | "--stroke-color", |
| 138 | "#112233", |
| 139 | "--stroke-width", |
| 140 | "2.5", |
| 141 | "--subtitle-background-color", |
| 142 | "#000001", |
| 143 | "--rounded-subtitle-background", |
| 144 | ] |
| 145 | ) |
| 146 | |
| 147 | params = cli.build_video_params(args) |
| 148 | |
| 149 | self.assertEqual(params.font_name, "MicrosoftYaHeiBold.ttc") |
| 150 | self.assertEqual(params.subtitle_position, "custom") |
| 151 | self.assertEqual(params.custom_position, 42.5) |
| 152 | self.assertEqual(params.text_fore_color, "#AABBCC") |
| 153 | self.assertEqual(params.font_size, 72) |
| 154 | self.assertEqual(params.stroke_color, "#112233") |
| 155 | self.assertEqual(params.stroke_width, 2.5) |
| 156 | self.assertEqual(params.text_background_color, "#000001") |
| 157 | self.assertTrue(params.rounded_subtitle_background) |
| 158 | |
| 159 | def test_subtitle_background_can_be_disabled_from_cli(self): |
| 160 | args = cli.parse_args( |
| 161 | [ |
| 162 | "--video-subject", |
| 163 | "test", |
| 164 | "--no-subtitle-background-enabled", |
| 165 | "--rounded-subtitle-background", |
| 166 | ] |
| 167 | ) |
| 168 | |
| 169 | params = cli.build_video_params(args) |
| 170 | |
| 171 | self.assertFalse(params.text_background_color) |
| 172 | self.assertFalse(params.rounded_subtitle_background) |
| 173 | |
| 174 | def test_bgm_type_none_maps_to_disabled_background_music(self): |
| 175 | args = cli.parse_args(["--video-subject", "test", "--bgm-type", "none"]) |
| 176 | params = cli.build_video_params(args) |
| 177 | self.assertEqual(params.bgm_type, "") |
| 178 | |
| 179 | def test_local_material_filename_resolved_to_absolute_path(self): |
| 180 | """After preprocess_video, material.url should be an absolute path, not a bare filename.""" |
| 181 | import os |
| 182 | import tempfile |
| 183 | from app.utils import utils |
| 184 | from app.services import video as vd |
| 185 | from app.models.schema import MaterialInfo |
| 186 | |
| 187 | local_videos_dir = utils.storage_dir("local_videos", create=True) |
| 188 | # Create a minimal valid video file for testing |
| 189 | test_filename = "_cli_test_resolve.mp4" |
| 190 | test_filepath = os.path.join(local_videos_dir, test_filename) |
| 191 | # We need a real video file; use a tiny one via moviepy |
| 192 | try: |
| 193 | from moviepy import ColorClip |
| 194 | clip = ColorClip(size=(640, 640), color=(0, 0, 0), duration=1) |
| 195 | clip.write_videofile(test_filepath, fps=1, logger=None) |
| 196 | clip.close() |
| 197 | except Exception: |
| 198 | self.skipTest("moviepy not available for creating test video") |
| 199 | |
| 200 | try: |
| 201 | materials = [MaterialInfo(provider="local", url=test_filename, duration=0)] |
| 202 | result = vd.preprocess_video(materials=materials, clip_duration=4) |
| 203 | self.assertTrue(len(result) > 0, "preprocess_video should return valid materials") |
| 204 | self.assertTrue( |
| 205 | os.path.isabs(result[0].url), |
| 206 | f"material url should be absolute path, got: {result[0].url}", |
| 207 | ) |
| 208 | self.assertEqual(result[0].url, test_filepath) |
| 209 | finally: |
| 210 | if os.path.exists(test_filepath): |
| 211 | os.remove(test_filepath) |
| 212 | |
| 213 | |
| 214 | def test_local_source_requires_video_materials(self): |
| 215 | with self.assertRaises(SystemExit) as cm: |
| 216 | cli.parse_args(["--video-subject", "test", "--video-source", "local"]) |
| 217 | self.assertNotEqual(cm.exception.code, 0) |
| 218 | |
| 219 | def test_local_source_stop_at_terms_rejected(self): |
| 220 | with self.assertRaises(SystemExit) as cm: |
| 221 | cli.parse_args([ |
| 222 | "--video-subject", "test", |
| 223 | "--video-source", "local", |
| 224 | "--video-materials", "a.mp4", |
| 225 | "--stop-at", "terms", |
| 226 | ]) |
| 227 | self.assertNotEqual(cm.exception.code, 0) |
| 228 | |
| 229 | |
| 230 | if __name__ == "__main__": |
| 231 | unittest.main() |
| 232 |