返回 MoneyPrinterTurbo
cli.py
根目录 / cli.py
1 import argparse
2 import json
3 import re
4 from typing import Sequence
5
6 from loguru import logger
7
8 from app.models.schema import MaterialInfo, VideoParams
9 from app.services import task as tm
10 from app.utils import utils
11
12
13 def _positive_int(value: str) -> int:
14 parsed = int(value)
15 if parsed < 1:
16 raise argparse.ArgumentTypeError(f"value must be >= 1, got {parsed}")
17 return parsed
18
19
20 def _paragraph_count(value: str) -> int:
21 parsed = int(value)
22 if parsed < 1 or parsed > 10:
23 raise argparse.ArgumentTypeError(
24 f"paragraph-number must be between 1 and 10, got {parsed}"
25 )
26 return parsed
27
28
29 def _non_negative_float(value: str) -> float:
30 parsed = float(value)
31 if parsed < 0:
32 raise argparse.ArgumentTypeError(f"value must be >= 0, got {parsed}")
33 return parsed
34
35
36 def _percent_position(value: str) -> float:
37 parsed = float(value)
38 if parsed < 0 or parsed > 100:
39 raise argparse.ArgumentTypeError(
40 f"custom-position must be between 0 and 100, got {parsed}"
41 )
42 return parsed
43
44
45 def _hex_color(value: str) -> str:
46 if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
47 raise argparse.ArgumentTypeError(
48 f"color must use #RRGGBB format, got {value!r}"
49 )
50 return value
51
52
53 _TRANSITION_MODE_VALUES = {
54 "none": None,
55 "shuffle": "Shuffle",
56 "fade-in": "FadeIn",
57 "fade-out": "FadeOut",
58 "slide-in": "SlideIn",
59 "slide-out": "SlideOut",
60 }
61
62
63 def _transition_mode(value: str) -> str | None:
64 normalized = value.strip().lower()
65 if normalized not in _TRANSITION_MODE_VALUES:
66 allowed = ", ".join(_TRANSITION_MODE_VALUES)
67 raise argparse.ArgumentTypeError(
68 f"video-transition-mode must be one of: {allowed}"
69 )
70 return _TRANSITION_MODE_VALUES[normalized]
71
72
73 def _bgm_type(value: str) -> str:
74 normalized = value.strip().lower()
75 if normalized == "none":
76 return ""
77 if normalized in {"", "random", "custom"}:
78 return normalized
79 raise argparse.ArgumentTypeError("bgm-type must be one of: none, random, custom")
80
81
82 def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
83 parser = argparse.ArgumentParser(
84 description="MoneyPrinterTurbo command line video generation"
85 )
86 parser.add_argument("--video-subject", required=True, help="video subject")
87 parser.add_argument("--video-script", default="", help="custom script")
88 parser.add_argument("--video-terms", default=None, help="comma-separated terms")
89 parser.add_argument(
90 "--video-language",
91 default=None,
92 help="script generation language code (default: auto detect)",
93 )
94 parser.add_argument(
95 "--paragraph-number",
96 type=_paragraph_count,
97 default=None,
98 help="script paragraph count, 1-10",
99 )
100 parser.add_argument(
101 "--video-script-prompt",
102 default=None,
103 help="custom script requirements prompt",
104 )
105 parser.add_argument(
106 "--custom-system-prompt",
107 default=None,
108 help="custom system prompt for script generation",
109 )
110 parser.add_argument(
111 "--video-source",
112 default="pexels",
113 choices=["pexels", "pixabay", "coverr", "local"],
114 help="video material source",
115 )
116 parser.add_argument(
117 "--video-materials",
118 default="",
119 help="comma-separated local material paths",
120 )
121 parser.add_argument(
122 "--stop-at",
123 default="video",
124 choices=["script", "terms", "audio", "subtitle", "materials", "video"],
125 help="pipeline stop stage",
126 )
127 parser.add_argument(
128 "--video-count", type=_positive_int, default=1, help="output video count (>=1)"
129 )
130 parser.add_argument("--video-aspect", default="9:16", help="video aspect ratio")
131 parser.add_argument(
132 "--video-concat-mode",
133 choices=["random", "sequential"],
134 default=None,
135 help="video concatenation mode",
136 )
137 parser.add_argument(
138 "--video-transition-mode",
139 type=_transition_mode,
140 default=None,
141 metavar="{none,shuffle,fade-in,fade-out,slide-in,slide-out}",
142 help="video transition mode",
143 )
144 parser.add_argument(
145 "--video-clip-duration",
146 type=_positive_int,
147 default=None,
148 help="maximum duration of each source clip in seconds",
149 )
150 parser.add_argument(
151 "--match-materials-to-script",
152 default=None,
153 action=argparse.BooleanOptionalAction,
154 help="match generated/search materials to script order",
155 )
156 parser.add_argument("--voice-name", default="", help="tts voice name")
157 parser.add_argument(
158 "--voice-volume",
159 type=_non_negative_float,
160 default=None,
161 help="speech volume multiplier",
162 )
163 parser.add_argument(
164 "--voice-rate",
165 type=_non_negative_float,
166 default=None,
167 help="speech rate multiplier",
168 )
169 parser.add_argument(
170 "--bgm-type",
171 type=_bgm_type,
172 default=None,
173 metavar="{none,random,custom}",
174 help="background music mode",
175 )
176 parser.add_argument("--bgm-file", default=None, help="custom background music file")
177 parser.add_argument(
178 "--bgm-volume",
179 type=_non_negative_float,
180 default=None,
181 help="background music volume multiplier",
182 )
183 parser.add_argument(
184 "--subtitle-enabled",
185 default=True,
186 action=argparse.BooleanOptionalAction,
187 help="enable subtitles (default: enabled, use --no-subtitle-enabled to disable)",
188 )
189 parser.add_argument("--font-name", default=None, help="subtitle font file name")
190 parser.add_argument(
191 "--subtitle-position",
192 choices=["top", "center", "bottom", "custom"],
193 default=None,
194 help="subtitle position",
195 )
196 parser.add_argument(
197 "--custom-position",
198 type=_percent_position,
199 default=None,
200 help="custom subtitle position as percent from top, 0-100",
201 )
202 parser.add_argument(
203 "--text-fore-color",
204 type=_hex_color,
205 default=None,
206 help="subtitle text color in #RRGGBB format",
207 )
208 parser.add_argument(
209 "--font-size", type=_positive_int, default=None, help="subtitle font size"
210 )
211 parser.add_argument(
212 "--stroke-color",
213 type=_hex_color,
214 default=None,
215 help="subtitle outline color in #RRGGBB format",
216 )
217 parser.add_argument(
218 "--stroke-width",
219 type=_non_negative_float,
220 default=None,
221 help="subtitle outline width",
222 )
223 parser.add_argument(
224 "--subtitle-background-enabled",
225 default=None,
226 action=argparse.BooleanOptionalAction,
227 help="enable subtitle background",
228 )
229 parser.add_argument(
230 "--subtitle-background-color",
231 type=_hex_color,
232 default=None,
233 help="subtitle background color in #RRGGBB format",
234 )
235 parser.add_argument(
236 "--rounded-subtitle-background",
237 default=None,
238 action=argparse.BooleanOptionalAction,
239 help="enable rounded translucent subtitle background",
240 )
241 parser.add_argument("--task-id", default="", help="custom task id")
242 args = parser.parse_args(argv)
243
244 if args.video_source == "local" and not (args.video_materials or "").strip():
245 parser.error("--video-materials is required when --video-source is local")
246
247 if args.video_source == "local" and args.stop_at == "terms":
248 parser.error(
249 "--stop-at terms has no effect with --video-source local "
250 "(search terms are not generated for local sources)"
251 )
252
253 return args
254
255
256 def build_video_params(args: argparse.Namespace) -> VideoParams:
257 video_terms = args.video_terms
258 if video_terms:
259 video_terms = [term.strip() for term in video_terms.split(",") if term.strip()]
260
261 video_materials = None
262 materials_arg = args.video_materials or ""
263 if materials_arg.strip():
264 video_materials = [
265 # Actual duration will be detected during video processing; use 0 as placeholder.
266 MaterialInfo(provider="local", url=item.strip(), duration=0)
267 for item in materials_arg.split(",")
268 if item.strip()
269 ]
270
271 params_kwargs = {
272 "video_subject": args.video_subject,
273 "video_script": args.video_script,
274 "video_terms": video_terms,
275 "video_source": args.video_source,
276 "video_materials": video_materials,
277 "video_count": args.video_count,
278 "video_aspect": args.video_aspect,
279 "voice_name": args.voice_name,
280 "subtitle_enabled": args.subtitle_enabled,
281 }
282
283 optional_arg_names = [
284 "video_language",
285 "paragraph_number",
286 "video_script_prompt",
287 "custom_system_prompt",
288 "video_concat_mode",
289 "video_transition_mode",
290 "video_clip_duration",
291 "match_materials_to_script",
292 "voice_volume",
293 "voice_rate",
294 "bgm_type",
295 "bgm_file",
296 "bgm_volume",
297 "font_name",
298 "subtitle_position",
299 "custom_position",
300 "text_fore_color",
301 "font_size",
302 "stroke_color",
303 "stroke_width",
304 "rounded_subtitle_background",
305 ]
306 for name in optional_arg_names:
307 value = getattr(args, name)
308 if value is not None:
309 params_kwargs[name] = value
310
311 if args.subtitle_background_enabled is False:
312 params_kwargs["text_background_color"] = False
313 params_kwargs["rounded_subtitle_background"] = False
314 elif args.subtitle_background_color is not None:
315 params_kwargs["text_background_color"] = args.subtitle_background_color
316 elif args.subtitle_background_enabled is True:
317 params_kwargs["text_background_color"] = True
318
319 return VideoParams(**params_kwargs)
320
321
322 def run_cli(argv: Sequence[str] | None = None) -> int:
323 args = parse_args(argv)
324 params = build_video_params(args)
325 task_id = args.task_id or utils.get_uuid()
326 logger.info(f"start cli task: {task_id}, stop_at: {args.stop_at}")
327 result = tm.start(task_id=task_id, params=params, stop_at=args.stop_at)
328 if not result:
329 logger.error("video generation failed")
330 return 1
331
332 print(json.dumps({"task_id": task_id, "result": result}, ensure_ascii=False))
333 return 0
334
335
336 if __name__ == "__main__":
337 raise SystemExit(run_cli())
338
338 lines PYTHON