| 1 | import warnings |
| 2 | from enum import Enum |
| 3 | from typing import Any, List, Optional, Union |
| 4 | |
| 5 | import pydantic |
| 6 | from pydantic import BaseModel, Field |
| 7 | |
| 8 | from app.config import config |
| 9 | |
| 10 | # 忽略 Pydantic 的特定警告 |
| 11 | warnings.filterwarnings( |
| 12 | "ignore", |
| 13 | category=UserWarning, |
| 14 | message="Field name.*shadows an attribute in parent.*", |
| 15 | ) |
| 16 | |
| 17 | |
| 18 | class VideoConcatMode(str, Enum): |
| 19 | random = "random" |
| 20 | sequential = "sequential" |
| 21 | |
| 22 | |
| 23 | class VideoTransitionMode(str, Enum): |
| 24 | none = None |
| 25 | shuffle = "Shuffle" |
| 26 | fade_in = "FadeIn" |
| 27 | fade_out = "FadeOut" |
| 28 | slide_in = "SlideIn" |
| 29 | slide_out = "SlideOut" |
| 30 | |
| 31 | |
| 32 | class VideoAspect(str, Enum): |
| 33 | landscape = "16:9" |
| 34 | portrait = "9:16" |
| 35 | square = "1:1" |
| 36 | |
| 37 | def to_resolution(self): |
| 38 | if self == VideoAspect.landscape: |
| 39 | return 1920, 1080 |
| 40 | elif self == VideoAspect.portrait: |
| 41 | return 1080, 1920 |
| 42 | elif self == VideoAspect.square: |
| 43 | return 1080, 1080 |
| 44 | raise ValueError(f"unsupported video aspect: {self}") |
| 45 | |
| 46 | |
| 47 | class _Config: |
| 48 | arbitrary_types_allowed = True |
| 49 | |
| 50 | |
| 51 | @pydantic.dataclasses.dataclass(config=_Config) |
| 52 | class MaterialInfo: |
| 53 | provider: str = "pexels" |
| 54 | url: str = "" |
| 55 | duration: int = 0 |
| 56 | |
| 57 | |
| 58 | class VideoParams(BaseModel): |
| 59 | """ |
| 60 | { |
| 61 | "video_subject": "", |
| 62 | "video_aspect": "横屏 16:9(西瓜视频)", |
| 63 | "voice_name": "女生-晓晓", |
| 64 | "bgm_name": "random", |
| 65 | "font_name": "STHeitiMedium 黑体-中", |
| 66 | "text_color": "#FFFFFF", |
| 67 | "font_size": 60, |
| 68 | "stroke_color": "#000000", |
| 69 | "stroke_width": 1.5 |
| 70 | } |
| 71 | """ |
| 72 | |
| 73 | video_subject: str |
| 74 | video_script: str = "" # Script used to generate the video |
| 75 | video_terms: Optional[str | list] = None # Keywords used to generate the video |
| 76 | video_aspect: Optional[VideoAspect] = VideoAspect.portrait.value |
| 77 | video_concat_mode: Optional[VideoConcatMode] = VideoConcatMode.random.value |
| 78 | video_transition_mode: Optional[VideoTransitionMode] = None |
| 79 | video_clip_duration: Optional[int] = 5 |
| 80 | match_materials_to_script: bool = False |
| 81 | video_count: Optional[int] = 1 |
| 82 | |
| 83 | video_source: Optional[str] = "pexels" |
| 84 | video_materials: Optional[List[MaterialInfo]] = ( |
| 85 | None # Materials used to generate the video |
| 86 | ) |
| 87 | |
| 88 | custom_audio_file: Optional[str] = None # Custom audio file path, will ignore TTS and can still use Whisper subtitles |
| 89 | video_language: Optional[str] = "" # auto detect |
| 90 | |
| 91 | voice_name: Optional[str] = "" |
| 92 | voice_volume: Optional[float] = 1.0 |
| 93 | voice_rate: Optional[float] = 1.0 |
| 94 | bgm_type: Optional[str] = "random" |
| 95 | bgm_file: Optional[str] = "" |
| 96 | bgm_volume: Optional[float] = 0.2 |
| 97 | |
| 98 | subtitle_enabled: Optional[bool] = True |
| 99 | subtitle_position: Optional[str] = config.ui.get("subtitle_position", "bottom") # top, bottom, center, custom |
| 100 | custom_position: float = config.ui.get("custom_position", 70.0) |
| 101 | font_name: Optional[str] = "STHeitiMedium.ttc" |
| 102 | text_fore_color: Optional[str] = "#FFFFFF" |
| 103 | text_background_color: Union[bool, str] = True |
| 104 | rounded_subtitle_background: bool = False |
| 105 | |
| 106 | font_size: int = 60 |
| 107 | stroke_color: Optional[str] = "#000000" |
| 108 | stroke_width: float = 1.5 |
| 109 | n_threads: Optional[int] = 2 |
| 110 | paragraph_number: int = Field(default=1, ge=1, le=10) |
| 111 | video_script_prompt: str = Field(default="", max_length=2000) |
| 112 | custom_system_prompt: str = Field(default="", max_length=8000) |
| 113 | |
| 114 | |
| 115 | class SubtitleRequest(BaseModel): |
| 116 | video_script: str |
| 117 | video_language: Optional[str] = "" |
| 118 | voice_name: Optional[str] = "zh-CN-XiaoxiaoNeural-Female" |
| 119 | voice_volume: Optional[float] = 1.0 |
| 120 | voice_rate: Optional[float] = 1.2 |
| 121 | bgm_type: Optional[str] = "random" |
| 122 | bgm_file: Optional[str] = "" |
| 123 | bgm_volume: Optional[float] = 0.2 |
| 124 | subtitle_position: Optional[str] = config.ui.get("subtitle_position", "bottom") |
| 125 | font_name: Optional[str] = "STHeitiMedium.ttc" |
| 126 | text_fore_color: Optional[str] = "#FFFFFF" |
| 127 | text_background_color: Union[bool, str] = True |
| 128 | rounded_subtitle_background: bool = False |
| 129 | font_size: int = 60 |
| 130 | stroke_color: Optional[str] = "#000000" |
| 131 | stroke_width: float = 1.5 |
| 132 | video_source: Optional[str] = "local" |
| 133 | subtitle_enabled: Optional[str] = "true" |
| 134 | |
| 135 | |
| 136 | class AudioRequest(BaseModel): |
| 137 | video_script: str |
| 138 | video_language: Optional[str] = "" |
| 139 | voice_name: Optional[str] = "zh-CN-XiaoxiaoNeural-Female" |
| 140 | voice_volume: Optional[float] = 1.0 |
| 141 | voice_rate: Optional[float] = 1.2 |
| 142 | bgm_type: Optional[str] = "random" |
| 143 | bgm_file: Optional[str] = "" |
| 144 | bgm_volume: Optional[float] = 0.2 |
| 145 | video_source: Optional[str] = "local" |
| 146 | |
| 147 | |
| 148 | class VideoScriptParams: |
| 149 | """ |
| 150 | { |
| 151 | "video_subject": "春天的花海", |
| 152 | "video_language": "", |
| 153 | "paragraph_number": 1, |
| 154 | "video_script_prompt": "", |
| 155 | "custom_system_prompt": "" |
| 156 | } |
| 157 | """ |
| 158 | |
| 159 | video_subject: Optional[str] = "春天的花海" |
| 160 | video_language: Optional[str] = "" |
| 161 | paragraph_number: int = Field(default=1, ge=1, le=10) |
| 162 | video_script_prompt: str = Field(default="", max_length=2000) |
| 163 | custom_system_prompt: str = Field(default="", max_length=8000) |
| 164 | |
| 165 | |
| 166 | class VideoTermsParams: |
| 167 | """ |
| 168 | { |
| 169 | "video_subject": "", |
| 170 | "video_script": "", |
| 171 | "amount": 5, |
| 172 | "match_materials_to_script": false |
| 173 | } |
| 174 | """ |
| 175 | |
| 176 | video_subject: Optional[str] = "春天的花海" |
| 177 | video_script: Optional[str] = ( |
| 178 | "春天的花海,如诗如画般展现在眼前。万物复苏的季节里,大地披上了一袭绚丽多彩的盛装。金黄的迎春、粉嫩的樱花、洁白的梨花、艳丽的郁金香……" |
| 179 | ) |
| 180 | amount: Optional[int] = 5 |
| 181 | match_materials_to_script: bool = False |
| 182 | |
| 183 | |
| 184 | class VideoSocialMetadataParams: |
| 185 | """ |
| 186 | { |
| 187 | "video_subject": "A day in Shanghai", |
| 188 | "video_script": "", |
| 189 | "language": "auto", |
| 190 | "platform": "tiktok" |
| 191 | } |
| 192 | """ |
| 193 | |
| 194 | video_subject: Optional[str] = Field(default="A day in Shanghai", max_length=500) |
| 195 | video_script: Optional[str] = Field(default="", max_length=8000) |
| 196 | language: Optional[str] = Field(default="auto", max_length=64) |
| 197 | platform: Optional[str] = Field(default="tiktok", max_length=64) |
| 198 | |
| 199 | |
| 200 | class BaseResponse(BaseModel): |
| 201 | status: int = 200 |
| 202 | message: Optional[str] = "success" |
| 203 | data: Any = None |
| 204 | |
| 205 | |
| 206 | class TaskVideoRequest(VideoParams, BaseModel): |
| 207 | pass |
| 208 | |
| 209 | |
| 210 | class TaskQueryRequest(BaseModel): |
| 211 | pass |
| 212 | |
| 213 | |
| 214 | class VideoScriptRequest(VideoScriptParams, BaseModel): |
| 215 | pass |
| 216 | |
| 217 | |
| 218 | class VideoTermsRequest(VideoTermsParams, BaseModel): |
| 219 | pass |
| 220 | |
| 221 | |
| 222 | class VideoSocialMetadataRequest(VideoSocialMetadataParams, BaseModel): |
| 223 | pass |
| 224 | |
| 225 | |
| 226 | ###################################################################################################### |
| 227 | ###################################################################################################### |
| 228 | ###################################################################################################### |
| 229 | ###################################################################################################### |
| 230 | class TaskResponse(BaseResponse): |
| 231 | class TaskResponseData(BaseModel): |
| 232 | task_id: str |
| 233 | |
| 234 | data: TaskResponseData |
| 235 | |
| 236 | class Config: |
| 237 | json_schema_extra = { |
| 238 | "example": { |
| 239 | "status": 200, |
| 240 | "message": "success", |
| 241 | "data": {"task_id": "6c85c8cc-a77a-42b9-bc30-947815aa0558"}, |
| 242 | }, |
| 243 | } |
| 244 | |
| 245 | |
| 246 | class TaskQueryResponse(BaseResponse): |
| 247 | class Config: |
| 248 | json_schema_extra = { |
| 249 | "example": { |
| 250 | "status": 200, |
| 251 | "message": "success", |
| 252 | "data": { |
| 253 | "state": 1, |
| 254 | "progress": 100, |
| 255 | "videos": [ |
| 256 | "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/final-1.mp4" |
| 257 | ], |
| 258 | "combined_videos": [ |
| 259 | "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/combined-1.mp4" |
| 260 | ], |
| 261 | }, |
| 262 | }, |
| 263 | } |
| 264 | |
| 265 | |
| 266 | class TaskDeletionResponse(BaseResponse): |
| 267 | class Config: |
| 268 | json_schema_extra = { |
| 269 | "example": { |
| 270 | "status": 200, |
| 271 | "message": "success", |
| 272 | "data": { |
| 273 | "state": 1, |
| 274 | "progress": 100, |
| 275 | "videos": [ |
| 276 | "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/final-1.mp4" |
| 277 | ], |
| 278 | "combined_videos": [ |
| 279 | "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/combined-1.mp4" |
| 280 | ], |
| 281 | }, |
| 282 | }, |
| 283 | } |
| 284 | |
| 285 | |
| 286 | class VideoScriptResponse(BaseResponse): |
| 287 | class Config: |
| 288 | json_schema_extra = { |
| 289 | "example": { |
| 290 | "status": 200, |
| 291 | "message": "success", |
| 292 | "data": { |
| 293 | "video_script": "春天的花海,是大自然的一幅美丽画卷。在这个季节里,大地复苏,万物生长,花朵争相绽放,形成了一片五彩斑斓的花海..." |
| 294 | }, |
| 295 | }, |
| 296 | } |
| 297 | |
| 298 | |
| 299 | class VideoTermsResponse(BaseResponse): |
| 300 | class Config: |
| 301 | json_schema_extra = { |
| 302 | "example": { |
| 303 | "status": 200, |
| 304 | "message": "success", |
| 305 | "data": {"video_terms": ["sky", "tree"]}, |
| 306 | }, |
| 307 | } |
| 308 | |
| 309 | |
| 310 | class VideoSocialMetadataResponse(BaseResponse): |
| 311 | class Config: |
| 312 | json_schema_extra = { |
| 313 | "example": { |
| 314 | "status": 200, |
| 315 | "message": "success", |
| 316 | "data": { |
| 317 | "title": "A Day in Shanghai You Should Not Miss", |
| 318 | "caption": "Save this quick Shanghai inspiration and follow for more short travel ideas.", |
| 319 | "hashtags": ["#shorts", "#travel", "#shanghai", "#viral", "#fyp"], |
| 320 | }, |
| 321 | }, |
| 322 | } |
| 323 | |
| 324 | |
| 325 | class BgmRetrieveResponse(BaseResponse): |
| 326 | class Config: |
| 327 | json_schema_extra = { |
| 328 | "example": { |
| 329 | "status": 200, |
| 330 | "message": "success", |
| 331 | "data": { |
| 332 | "files": [ |
| 333 | { |
| 334 | "name": "output013.mp3", |
| 335 | "size": 1891269, |
| 336 | "file": "/MoneyPrinterTurbo/resource/songs/output013.mp3", |
| 337 | } |
| 338 | ] |
| 339 | }, |
| 340 | }, |
| 341 | } |
| 342 | |
| 343 | |
| 344 | class BgmUploadResponse(BaseResponse): |
| 345 | class Config: |
| 346 | json_schema_extra = { |
| 347 | "example": { |
| 348 | "status": 200, |
| 349 | "message": "success", |
| 350 | "data": {"file": "/MoneyPrinterTurbo/resource/songs/example.mp3"}, |
| 351 | }, |
| 352 | } |
| 353 | |
| 354 | class VideoMaterialRetrieveResponse(BaseResponse): |
| 355 | class Config: |
| 356 | json_schema_extra = { |
| 357 | "example": { |
| 358 | "status": 200, |
| 359 | "message": "success", |
| 360 | "data": { |
| 361 | "files": [ |
| 362 | { |
| 363 | "name": "example.mp4", |
| 364 | "size": 12345678, |
| 365 | "file": "/MoneyPrinterTurbo/resource/videos/example.mp4", |
| 366 | } |
| 367 | ] |
| 368 | }, |
| 369 | }, |
| 370 | } |
| 371 | |
| 372 | class VideoMaterialUploadResponse(BaseResponse): |
| 373 | class Config: |
| 374 | json_schema_extra = { |
| 375 | "example": { |
| 376 | "status": 200, |
| 377 | "message": "success", |
| 378 | "data": { |
| 379 | "file": "/MoneyPrinterTurbo/resource/videos/example.mp4", |
| 380 | }, |
| 381 | }, |
| 382 | } |
| 383 |