diff --git a/src/BowongModalFunctions/api.py b/src/BowongModalFunctions/api.py
index f77ad21..1159bad 100644
--- a/src/BowongModalFunctions/api.py
+++ b/src/BowongModalFunctions/api.py
@@ -7,8 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware
from .utils.KVCache import MediaSourceKVCache
from .router import ffmpeg, cache, comfyui, google, task
-from Douyin_TikTok_Download_API.app.api import router as tikhub
-from .config import WorkerConfig
+from .models.settings.cluster import WorkerConfig
config = WorkerConfig()
@@ -23,14 +22,9 @@ web_app = FastAPI(title="Modal worker API",
])
web_app_tikhub = FastAPI(title="Modal Tikhub API",
- version="1.0.0",
- summary="Modal Tikhub API, 包含抖音Web API",
- servers=[
- {
- 'url': f'https://bowongai-{config.modal_environment}--{config.modal_app_name}-fastapi-webapp-tikhub.modal.run',
- 'description': '当前Modal Tikhub API接口endpoint'
- }
- ])
+ version="1.0.0",
+ summary="Modal Tikhub API, 包含抖音Web API",
+ )
sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2",
send_default_pii=True,
@@ -73,14 +67,6 @@ web_app.add_middleware(
allow_headers=["*"],
)
-web_app_tikhub.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
@web_app.middleware("http")
@web_app.middleware("https")
@@ -94,10 +80,6 @@ async def alias_middleware(request: Request, call_next):
async def scalar():
return get_scalar_api_reference(openapi_url='/openapi.json', title="Modal worker web endpoint")
-@web_app_tikhub.get("/scalar", include_in_schema=False)
-async def scalar():
- return get_scalar_api_reference(openapi_url='/openapi.json', title="Modal worker web endpoint")
-
web_app.include_router(ffmpeg.router)
web_app.include_router(cache.router)
@@ -106,6 +88,3 @@ web_app.include_router(cache.router)
web_app.include_router(google.router)
web_app.include_router(task.router)
-
-# Tikhub API
-web_app_tikhub.include_router(tikhub.router)
diff --git a/src/BowongModalFunctions/middleware/authorization.py b/src/BowongModalFunctions/middleware/authorization.py
index 9c1edc0..934d732 100644
--- a/src/BowongModalFunctions/middleware/authorization.py
+++ b/src/BowongModalFunctions/middleware/authorization.py
@@ -1,7 +1,14 @@
+import httpx
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+from pydantic import EmailStr
from starlette import status
+from ..models.responses.models import NakamaJWTResponse
+from ..models.settings.cluster import WorkerConfig
+
+config = WorkerConfig()
+
bearer_scheme = HTTPBearer(scheme_name="bearer", description="固定API Token的鉴权方式", auto_error=True)
@@ -18,6 +25,17 @@ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(beare
def is_valid_token(token: str) -> bool:
- # 这里实现具体的token验证逻辑
- # 例如:验证JWT token,检查数据库中的token等
- return token == "bowong7777"
+ #
+ if token == config.api_server_token:
+ return True
+ else:
+ response = httpx.get(url=f"{config.nakama_client_endpoint}/v2/account",
+ headers={"Authorization": f"Bearer {token}"})
+ return response.is_success
+
+
+def nakama_login(email: EmailStr, password: str) -> NakamaJWTResponse:
+ response = httpx.post(url=f"{config.nakama_client_endpoint}/v2/account/authenticate/email",
+ json={"email": email.__str__(), "password": password, })
+ response.raise_for_status()
+ return NakamaJWTResponse.model_validate_json(response.text)
diff --git a/src/BowongModalFunctions/models/cache_tasks/__init__.py b/src/BowongModalFunctions/models/cache_tasks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/models/media_model.py b/src/BowongModalFunctions/models/cache_tasks/models.py
similarity index 57%
rename from src/BowongModalFunctions/models/media_model.py
rename to src/BowongModalFunctions/models/cache_tasks/models.py
index 36b73e4..886fb9e 100644
--- a/src/BowongModalFunctions/models/media_model.py
+++ b/src/BowongModalFunctions/models/cache_tasks/models.py
@@ -1,41 +1,22 @@
+import json
import os
import re
from datetime import datetime
-from enum import Enum
from functools import cached_property
-from typing import List, Union, Optional, Any, Dict
+from typing import Optional, Union, Any, List
from urllib.parse import urlparse
-from pydantic import (BaseModel, Field, field_validator, ValidationError,
- field_serializer, SerializationInfo, computed_field, FileUrl, Base64Str, Base64Bytes, ConfigDict)
+
+from pydantic import BaseModel, Field, field_validator, model_validator, ValidationError, computed_field, \
+ field_serializer, Base64Bytes, RootModel
from pydantic.json_schema import JsonSchemaValue
-from ..config import WorkerConfig
-from ..utils.TimeUtils import TimeDelta
-from ..utils.VideoUtils import VideoMetadata
+from pydantic_core.core_schema import SerializationInfo
+
+from ..enums.models import MediaProtocol, MediaCacheStatus, CacheOperationType
+from ..settings.cluster import WorkerConfig
+from ..ffmpeg_tasks.models import VideoMetadata
config = WorkerConfig()
-s3_region = config.S3_region
-s3_bucket_name = config.S3_bucket_name
-s3_mount_point = config.S3_mount_dir
-
-
-class MediaProtocol(str, Enum):
- http = "http"
- s3 = "s3"
- vod = "vod"
- cos = "cos"
- hls = "hls"
- gs = "gs"
-
-
-class MediaCacheStatus(str, Enum):
- downloading = "downloading"
- failed = "failed"
- ready = "ready"
- deleted = "deleted"
- missing = "missing"
- unknown = "unknown"
-
class MediaSource(BaseModel):
path: str = Field(description="媒体源的路径")
@@ -63,7 +44,7 @@ class MediaSource(BaseModel):
elif media_url.startswith('s3://'):
pattern = r"^s3://[a-z]{2}-[a-z]+-\d/.*$"
if not re.match(pattern, media_url):
- media_url = media_url.replace("s3://", f"s3://{s3_region}/")
+ media_url = media_url.replace("s3://", f"s3://{config.S3_region}/")
# s3://{endpoint}/{bucket}/{url}
paths = media_url[5:].split('/')
@@ -139,7 +120,7 @@ class MediaSource(BaseModel):
match self.protocol:
case MediaProtocol.s3:
# 本地挂载缓存
- if self.protocol == MediaProtocol.s3 and self.endpoint == s3_region and self.bucket == s3_bucket_name:
+ if self.protocol == MediaProtocol.s3 and self.endpoint == config.S3_region and self.bucket == config.S3_bucket_name:
return f"{self.path}"
else:
return f"{self.protocol.value}/{self.endpoint}/{self.bucket}/{self.path}"
@@ -169,7 +150,7 @@ class MediaSource(BaseModel):
@computed_field(description="本地挂载地址")
@property
def local_mount_path(self) -> str:
- return f"{s3_mount_point}/{self.cache_filepath}"
+ return f"{config.S3_mount_dir}/{self.cache_filepath}"
@field_serializer('expired_at')
def serialize_datetime(self, value: Optional[datetime], info: SerializationInfo) -> Optional[str]:
@@ -200,59 +181,56 @@ class MediaSource(BaseModel):
return f"{self.protocol.value}://{self.endpoint}/{self.bucket}/{self.path}"
-class MediaSources(BaseModel):
- inputs: List[MediaSource] = Field(examples=[
- [
- "vod://ap-shanghai/1500034234/1397757910405340824.mp4",
- "vod://ap-shanghai/1500034234/1397757910403699452.mp4",
- "s3://ap-northeast-2/modal-media-cache/concat/outputs/fc-01JTPV5FCNA74CKX3N3214XJPD/output.mp4"
- ]
- ], description="支持多种协议['vod://', 's3://'], 计划支持['cos://', http://]")
+class CacheTask(BaseModel):
+ type: CacheOperationType = Field(description="操作类型")
+ source: MediaSource = Field(description="源媒体URN")
+ target: Optional[MediaSource] = Field(description="目标媒体URN")
- @field_validator('inputs', mode='before')
+ @field_validator('source', mode='before')
@classmethod
- def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
- if not v:
- raise ValidationError([
- {
- 'loc': ('inputs',),
- 'msg': "inputs为空",
- 'type': 'value_error',
- 'input': v
- }
- ], MediaSources)
- result = []
- for item in v:
- if isinstance(item, str):
- result.append(MediaSource.from_str(item))
- elif isinstance(item, MediaSource):
- result.append(item)
+ def parse_source(cls, v: Union[str, MediaSource]) -> MediaSource:
+ if isinstance(v, str):
+ media_source = MediaSource.from_str(v)
+ if media_source.protocol == MediaProtocol.s3:
+ return media_source
else:
- raise ValidationError([
- {
- 'loc': ('inputs',),
- 'msg': "inputs元素类型错误: 必须是字符串",
- 'type': 'value_error',
- 'input': v
- }
- ], MediaSources)
- return result
+ raise ValidationError('media只支持s3格式的urn')
+ elif isinstance(v, MediaSource):
+ return v
+ else:
+ raise ValidationError("media格式读取失败")
- model_config = {
- "arbitrary_types_allowed": True
- }
+ @field_validator('target', mode='before')
+ @classmethod
+ def parse_target(cls, v: Union[str, MediaSource]) -> Optional[MediaSource]:
+ if v is None:
+ return None
+ if isinstance(v, str):
+ media_source = MediaSource.from_str(v)
+ if media_source.protocol == MediaProtocol.s3:
+ return media_source
+ else:
+ raise ValidationError('media只支持s3格式的urn')
+ elif isinstance(v, MediaSource):
+ return v
+ else:
+ raise ValidationError("media格式读取失败")
+
+ @model_validator(mode="after")
+ def parse_model(self):
+ match self.type:
+ case CacheOperationType.copy:
+ if self.target is None:
+ raise ValidationError("使用copy行为时必填target URN")
+ case CacheOperationType.move:
+ if self.target is None:
+ raise ValidationError("使用move行为时必填target URN")
+ case _:
+ return self
-class CacheResult(BaseModel):
- caches: Dict[str, MediaSource] = Field(description="Cache ID")
-
-
-class DownloadResult(BaseModel):
- urls: List[str] = Field(description="下载链接")
-
-
-class UploadResultResponse(BaseModel):
- media: MediaSource = Field(description="上传完成的媒体资源")
+class CacheTaskResult(CacheTask):
+ success: bool = Field(default=False, description="执行成功")
class Base64File(BaseModel):
@@ -260,49 +238,103 @@ class Base64File(BaseModel):
filename: str = Field(description="文件名, 包含文件类型后缀")
content_type: str = Field(description="文件数据类型")
- # @computed_field(description="Base64解码后的二进制内容")
- # @property
- # def content(self) -> Base64Bytes:
- # return base64.b64decode(self.raw_content)
-
@computed_field(description="文件字节大小")
@property
def size(self) -> int:
- # b64_str = self.raw_content.strip().replace('\n', '').replace('\r', '')
- # padding = b64_str.count('=', -2) # 只统计末尾的 '='
- # size = len(b64_str)
return len(self.raw_content)
-class UploadBase64Request(BaseModel):
- file: Base64File = Field(description="上传的文件")
- prefix: Optional[str] = Field(description="文件存在的前缀目录", default=None)
+class LiveProduct(BaseModel):
+ title: str = Field(default="", description="商品标题")
+ leaf_category: str = Field(default="", description="商品分类")
+ shop_id: int = Field(default=0, description="店铺ID")
+ product_id: str = Field(default="", description="商品ID")
+ cover: str = Field(default="", description="商品封面图链接")
+ detail_url: str = Field(default="", description="商品详情链接")
-class UploadPresignRequest(BaseModel):
- key: str = Field(description="上传文件的key", examples=['123/456/abc.mp4'])
- content_type: str = Field(description="上传对象的文件类型", examples=['video/mp4'])
+class LiveProductCaches(BaseModel):
+ room_id: str = Field(default="", description="直播间room_id/Room room_id")
+ author_id: str = Field(default="", description="作者id/Author id")
+ update_time: str = Field(default=0, description="商品列表更新时间")
+ count: int = Field(default=0, description="缓存直播间商品数量")
+ product_list: List[LiveProduct] = Field(default=None, description="缓存直播间商品列表")
-class UploadPresignResponse(BaseModel):
- url: str = Field(description="就近加速的PUT上传地址")
- urn: str = Field(description="上传成功后获得的对应资源URN")
- expired_at: datetime = Field(description="上传地址签名过期时间戳")
+class VisualFeatures(BaseModel):
+ """
+ 代表产品的视觉特征
+ """
+ color: str = Field(
+ description="商品详细颜色配色等色彩特征"
+ )
+ pattern: str = Field(
+ description="商品详细图案纹理布料等材质特征, 材料可以根据产品名称和图像确定"
+ )
+ style: str = Field(
+ default=None,
+ description="商品详细款式风格设计等有辨识度的款式特征"
+ )
-class UploadMultipartPresignRequest(UploadPresignRequest):
- parts_count: int = Field(description="分片数量")
+class ProductInfo(BaseModel):
+ """
+ 表示从图像中提取单个产品的信息
+ """
+ image_order: int = Field(
+ description="图像出现的顺序"
+ )
+ image_name: str = Field(
+ description="图像上显示的原始文本"
+ )
+ matched_product: str = Field(
+ description="匹配的标准产品名称(必须与产品列表中的标准产品名称保持一致),或者如果没有匹配,则空"
+ )
+ match_confidence: int = Field(
+ ge=0,
+ le=100,
+ description="产品匹配的置信度范围从0到100"
+ )
+ visual_features: VisualFeatures = Field(
+ description="产品的详细视觉特征"
+ )
-class UploadMultipartPresignResponse(BaseModel):
- urls: List[str] = Field(description="就近加速的PUT分片上传地址")
- list_url: str = Field(description="用于确认分片上传状态的请求地址")
- complete_url: str = Field(description="用于确认完成分片上传的请求地址")
- urn: str = Field(description="上传成功后获得的对应资源URN")
- expired_at: datetime = Field(description="上传地址签名过期时间戳")
+class GeminiFirstStageResponseModel(RootModel):
+ """
+ 从图像中提取的产品信息列表
+ """
+ root: List[ProductInfo]
-class MediaCopyRequest(BaseModel):
- class MediaCopyTask(BaseModel):
- source: MediaSource = Field(description="源媒体")
- destination: MediaSource = Field(description="")
+class ProductTimeline(BaseModel):
+ """
+ 表示单个商品及其在视频时间线中的出现信息。
+ """
+ product: str = Field(description="标准商品名称")
+ timeline: List[str] = Field(
+ description="该商品在视频中出现的时间段列表。每个字符串表示一个时间段,格式为 '开始时间 - 结束时间 (描述)'"
+ )
+
+
+class GeminiFirstStagePromptVariables(BaseModel):
+ product_list: List[str] = Field(description="商品名列表")
+
+ @computed_field(description="xml格式排列的商品列表")
+ @property
+ def product_list_xml(self) -> str:
+ xml_items = [f" {product}" for product in self.product_list]
+ xml_string = "\n".join(xml_items)
+ return f"\n{xml_string}\n "
+
+
+class GeminiSecondStagePromptVariables(BaseModel):
+ product_json_list: List[dict] = Field(description="识别出的商品JSON列表")
+
+ @computed_field(description="xml格式排列的识别出的商品JSON列表")
+ @property
+ def product_json_list_xml(self) -> str:
+ xml_items = [f" {json.dumps(product, ensure_ascii=False)}" for product in
+ self.product_json_list]
+ xml_string = "\n".join(xml_items)
+ return f"\n{xml_string}\n "
diff --git a/src/BowongModalFunctions/models/enums/__init__.py b/src/BowongModalFunctions/models/enums/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/models/enums/models.py b/src/BowongModalFunctions/models/enums/models.py
new file mode 100644
index 0000000..d31d994
--- /dev/null
+++ b/src/BowongModalFunctions/models/enums/models.py
@@ -0,0 +1,47 @@
+from enum import Enum
+
+
+class MediaProtocol(str, Enum):
+ http = "http"
+ s3 = "s3"
+ vod = "vod"
+ cos = "cos"
+ hls = "hls"
+ gs = "gs"
+
+
+class MediaCacheStatus(str, Enum):
+ downloading = "downloading"
+ failed = "failed"
+ ready = "ready"
+ deleted = "deleted"
+ missing = "missing"
+ unknown = "unknown"
+
+
+class TaskStatus(str, Enum):
+ running = "running"
+ failed = "failed"
+ success = "success"
+ expired = "expired"
+
+
+class CacheOperationType(str, Enum):
+ copy = "copy"
+ move = "move"
+ delete = "delete"
+
+
+class ErrorCode(int, Enum):
+ SUCCESS = 0
+ PARAM_ERROR = 10001
+ NOT_FOUND = 10002
+ UNAUTHORIZED = 10003
+ FORBIDDEN = 10004
+ BUSINESS_ERROR = 10005
+ SYSTEM_ERROR = 99999
+
+
+class WebhookMethodEnum(str, Enum):
+ GET = "get"
+ POST = "post"
diff --git a/src/BowongModalFunctions/models/ffmpeg_tasks/__init__.py b/src/BowongModalFunctions/models/ffmpeg_tasks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/models/ffmpeg_tasks/models.py b/src/BowongModalFunctions/models/ffmpeg_tasks/models.py
new file mode 100644
index 0000000..8a4201a
--- /dev/null
+++ b/src/BowongModalFunctions/models/ffmpeg_tasks/models.py
@@ -0,0 +1,310 @@
+import json
+from typing import Union, Any, Optional, List, Dict
+
+from loguru import logger
+from pydantic import BaseModel, Field, computed_field, field_validator, model_validator, ConfigDict, HttpUrl
+from pydantic.json_schema import JsonSchemaValue
+
+from BowongModalFunctions.models.enums.models import WebhookMethodEnum
+from BowongModalFunctions.models.settings.cluster import WorkerConfig
+from BowongModalFunctions.utils.TimeUtils import TimeDelta
+
+config = WorkerConfig()
+
+
+class FFMpegSliceSegment(BaseModel):
+ start: TimeDelta = Field(
+ description="视频切割的开始时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者为标准格式的时间戳")
+ end: TimeDelta = Field(
+ description="视频切割的结束时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者标准格式的时间戳")
+
+ @computed_field
+ @property
+ def duration(self) -> TimeDelta:
+ return self.end - self.start
+
+ @field_validator('start', mode='before')
+ @classmethod
+ def parse_start(cls, v: Union[float, str, TimeDelta]):
+ if isinstance(v, float):
+ if v < 0.0:
+ raise ValueError("开始时间点不能小于0")
+ return TimeDelta(seconds=v)
+ elif isinstance(v, int):
+ if v < 0:
+ raise ValueError("开始时间点不能小于0")
+ return TimeDelta(seconds=v)
+ elif isinstance(v, str):
+ timedelta = TimeDelta.from_format_string(v)
+ if timedelta.total_seconds() < 0.0:
+ raise ValueError("开始时间点不能小于0")
+ return timedelta
+ elif isinstance(v, TimeDelta):
+ if v.total_seconds() < 0.0:
+ raise ValueError("开始时间点不能小于0")
+ return v
+ else:
+ raise TypeError(v)
+
+ @field_validator('end', mode='before')
+ @classmethod
+ def parse_end(cls, v: Union[float, TimeDelta]):
+ if isinstance(v, float):
+ if v < 0.0:
+ raise ValueError("结束时间点不能小于0")
+ return TimeDelta(seconds=v)
+ elif isinstance(v, int):
+ if v < 0:
+ raise ValueError("结束时间点不能小于0")
+ return TimeDelta(seconds=v)
+ elif isinstance(v, str):
+ timedelta = TimeDelta.from_format_string(v)
+ if timedelta.total_seconds() < 0.0:
+ raise ValueError("结束时间点不能小于0")
+ return timedelta
+ elif isinstance(v, TimeDelta):
+ if v.total_seconds() < 0.0:
+ raise ValueError("结束时间点不能小于0")
+ return v
+ else:
+ raise TypeError(v)
+
+ @model_validator(mode='after')
+ def validate_end_after_start(self) -> 'FFMpegSliceSegment':
+ if self.end <= self.start:
+ raise ValueError("end time must be greater than start time")
+ return self
+
+ @classmethod
+ def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> JsonSchemaValue:
+ # Override the schema to represent it as a string
+ return {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number",
+ "examples": [5, 10.5, '00:00:10.500'],
+ },
+ "end": {
+ "type": "number",
+ "examples": [8, 12.5, '00:00:12.500'],
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ]
+ }
+
+ model_config = {
+ "arbitrary_types_allowed": True
+ }
+
+
+class FFMPEGSliceOptions(BaseModel):
+ limit_size: Optional[int] = Field(default=None, description="不超过指定文件(字节)大小, 默认为空不限制输出大小")
+ bit_rate: Optional[int] = Field(default=None, description="指定输出视频的比特率, 不能与limit_size同时设置")
+
+ crf: int = Field(default=16, description="输出视频的质量")
+ fps: int = Field(default=30, description="输出视频的FPS")
+ width: int = Field(default=None, description="输出视频的宽(像素)")
+ height: int = Field(default=None, description="输出视频的高(像素)")
+
+ @computed_field(description="解析为字符表达式的文件大小")
+ @property
+ def pretty_limit_size(self) -> Optional[str]:
+ if self.limit_size is not None:
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
+ if self.limit_size < 1024.0:
+ return f"{self.limit_size:.2f}{unit}"
+ self.limit_size /= 1024.0
+ return f"{self.limit_size:.2f}PB"
+ else:
+ return None
+
+ @computed_field(description="解析为字符表达式的比特率")
+ @property
+ def pretty_bit_rate(self) -> Optional[str]:
+ if self.bit_rate is not None:
+ return f"{self.bit_rate}k"
+
+
+class MediaStream(BaseModel):
+ duration: float = Field(0, description="时长")
+ codec_name: str
+ tags: Optional[Any] = Field(None)
+
+ model_config = ConfigDict(extra='allow')
+
+
+class HLSMediaVideoStream(BaseModel):
+ stream_type: str = "video"
+ codec_name: str
+ codec_type: str
+ width: int
+ height: int
+ avg_frame_rate: str
+ tags: Optional[Any] = Field(None)
+ duration: Optional[float] = Field(None)
+
+ @computed_field
+ @property
+ def video_frame_rate(self) -> float:
+ numerator, denominator = map(int, self.avg_frame_rate.split('/'))
+ if denominator != 0:
+ return numerator / denominator
+ return 0
+
+
+class HLSMediaAudioStream(BaseModel):
+ stream_type: str = "audio"
+ sample_rate: str
+ channels: int
+ channel_layout: str
+ start_time: str
+ tags: Optional[Any] = Field(None)
+
+
+class AudioStream(MediaStream):
+ stream_type: str = Field("audio")
+ codec_type: str
+ sample_rate: str
+ channels: int
+ tags: Optional[Any] = Field(None)
+
+ model_config = ConfigDict(extra='allow')
+
+
+class VideoStream(MediaStream):
+ stream_type: str = "video"
+ width: int
+ height: int
+ bit_rate: int
+ avg_frame_rate: str
+
+ @computed_field
+ @property
+ def video_bitrate(self) -> str:
+ return str(int(self.bit_rate / 1000)) + 'k'
+
+ @computed_field
+ @property
+ def video_frame_rate(self) -> float:
+ numerator, denominator = map(int, self.avg_frame_rate.split('/'))
+ if denominator != 0:
+ return numerator / denominator
+ return 0
+
+ model_config = ConfigDict(extra='allow')
+
+
+class ImageStream(MediaStream):
+ stream_type: str = "image"
+ width: int
+ height: int
+
+ model_config = ConfigDict(extra='allow')
+
+
+class SubtitleStreamTags(BaseModel):
+ language: str = Field(description="内嵌字幕的语言")
+
+ model_config = ConfigDict(extra='allow')
+
+
+class SubtitleStream(MediaStream):
+ stream_type: str = "subtitle"
+ tags: SubtitleStreamTags
+
+ model_config = ConfigDict(extra='allow')
+
+
+class VideoFormat(BaseModel):
+ filename: str
+ format_name: str
+ start_time: float = Field(0, description="起始时间")
+ size: int
+ bit_rate: Optional[int] = Field(None, description="文件比特率")
+ duration: float = Field(3600 * 12, description="文件时长")
+
+
+class VideoMetadata(BaseModel):
+ streams: List[
+ Union[ImageStream, AudioStream, VideoStream, HLSMediaAudioStream, HLSMediaVideoStream, SubtitleStream]] = Field(
+ description="媒体包含的数据轨道")
+ format: Optional[VideoFormat] = Field(None)
+
+ @field_validator('streams', mode='before')
+ def parse_streams(cls, value):
+ streams = []
+ if isinstance(value, List):
+ for stream in value:
+ if isinstance(stream, Dict):
+ logger.info(f"Parsing stream : {json.dumps(stream, ensure_ascii=False)}")
+ if stream.get("codec_type") == 'audio':
+ if stream.get("duration") is None:
+ logger.info("Parsing audio stream")
+ hls_audio = HLSMediaAudioStream.model_validate(stream)
+ streams.append(hls_audio)
+ else:
+ logger.info("Parsing hls audio stream")
+ audio = AudioStream.model_validate(stream)
+ streams.append(audio)
+ elif stream.get("codec_type") == 'video':
+ if stream.get("codec_name") in ("gif", "png", "mjpg", "mjpeg", "webp"):
+ logger.info("Parsing image stream")
+ image = ImageStream.model_validate(stream)
+ streams.append(image)
+ else:
+ if stream.get("duration") is None:
+ logger.info("Parsing hls video stream")
+ hls_video = HLSMediaVideoStream.model_validate(stream)
+ streams.append(hls_video)
+ else:
+ logger.info("Parsing video stream")
+ video = VideoStream.model_validate(stream)
+ streams.append(video)
+ elif stream.get("codec_type") == 'subtitle':
+ logger.info("Parsing subtitle stream")
+ subtitle_stream = SubtitleStream.model_validate(stream)
+ streams.append(subtitle_stream)
+ return streams
+ else:
+ raise TypeError
+
+ model_config = ConfigDict(extra='allow')
+
+
+class SentryTransactionHeader(BaseModel):
+ x_trace_id: Optional[str] = Field(description="Sentry Transaction ID", default=None)
+ x_baggage: Optional[str] = Field(description="Sentry Transaction baggage", default=None)
+
+
+class SentryTransactionInfo(BaseModel):
+ x_trace_id: str = Field(description="Sentry Transaction ID")
+ x_baggage: str = Field(description="Sentry Transaction baggage")
+
+
+class WebhookNotify(BaseModel):
+ endpoint: HttpUrl = Field(description="Webhook回调端点", examples=["https://webhook.example.com?query=123"])
+ method: WebhookMethodEnum = Field(
+ description="Webhook回调请求方法, 使用POST方法时body与查询ffmpeg任务所获得的json body格式一致")
+ headers: Optional[Dict[str, str]] = Field(description="Webhook回调附带的Headers", default=None)
+
+ model_config = ConfigDict(json_schema_extra={
+ "description": "Webhook返回值与查询ffmpeg任务所获得的json格式一致"
+ })
+
+
+class FFMPEGResult(BaseModel):
+ urn: str = Field(description="FFMPEG任务结果urn")
+ content_length: int = Field(description="媒体资源文件字节大小(Byte)")
+ metadata: VideoMetadata = Field(description="媒体元数据")
+
+ @computed_field(description="可通过CDN访问的资源链接")
+ @property
+ def url(self) -> str:
+ prefix = f"s3://{config.S3_region}/{config.S3_bucket_name}"
+ if not self.urn.startswith(prefix):
+ raise ValueError("无法转换非s3前缀协议和非当前挂载点的s3协议")
+ return self.urn.replace(prefix, f"{config.S3_cdn_endpoint}")
diff --git a/src/BowongModalFunctions/models/ffmpeg_worker_model.py b/src/BowongModalFunctions/models/ffmpeg_worker_model.py
deleted file mode 100644
index ed28886..0000000
--- a/src/BowongModalFunctions/models/ffmpeg_worker_model.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from typing import Union, Any
-from pydantic import BaseModel, Field, computed_field, field_validator, model_validator
-from pydantic.json_schema import JsonSchemaValue
-from ..utils.TimeUtils import TimeDelta
-
-
-class FFMpegSliceSegment(BaseModel):
- start: TimeDelta = Field(
- description="视频切割的开始时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者为标准格式的时间戳")
- end: TimeDelta = Field(
- description="视频切割的结束时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者标准格式的时间戳")
-
- @computed_field
- @property
- def duration(self) -> TimeDelta:
- return self.end - self.start
-
- @field_validator('start', mode='before')
- @classmethod
- def parse_start(cls, v: Union[float, str, TimeDelta]):
- if isinstance(v, float):
- if v < 0.0:
- raise ValueError("开始时间点不能小于0")
- return TimeDelta(seconds=v)
- elif isinstance(v, int):
- if v < 0:
- raise ValueError("开始时间点不能小于0")
- return TimeDelta(seconds=v)
- elif isinstance(v, str):
- timedelta = TimeDelta.from_format_string(v)
- if timedelta.total_seconds() < 0.0:
- raise ValueError("开始时间点不能小于0")
- return timedelta
- elif isinstance(v, TimeDelta):
- if v.total_seconds() < 0.0:
- raise ValueError("开始时间点不能小于0")
- return v
- else:
- raise TypeError(v)
-
- @field_validator('end', mode='before')
- @classmethod
- def parse_end(cls, v: Union[float, TimeDelta]):
- if isinstance(v, float):
- if v < 0.0:
- raise ValueError("结束时间点不能小于0")
- return TimeDelta(seconds=v)
- elif isinstance(v, int):
- if v < 0:
- raise ValueError("结束时间点不能小于0")
- return TimeDelta(seconds=v)
- elif isinstance(v, str):
- timedelta = TimeDelta.from_format_string(v)
- if timedelta.total_seconds() < 0.0:
- raise ValueError("结束时间点不能小于0")
- return timedelta
- elif isinstance(v, TimeDelta):
- if v.total_seconds() < 0.0:
- raise ValueError("结束时间点不能小于0")
- return v
- else:
- raise TypeError(v)
-
- @model_validator(mode='after')
- def validate_end_after_start(self) -> 'FFMpegSliceSegment':
- if self.end <= self.start:
- raise ValueError("end time must be greater than start time")
- return self
-
- @classmethod
- def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> JsonSchemaValue:
- # Override the schema to represent it as a string
- return {
- "type": "object",
- "properties": {
- "start": {
- "type": "number",
- "examples": [5, 10.5, '00:00:10.500'],
- },
- "end": {
- "type": "number",
- "examples": [8, 12.5, '00:00:12.500'],
- }
- },
- "required": [
- "start",
- "end"
- ]
- }
-
- model_config = {
- "arbitrary_types_allowed": True
- }
diff --git a/src/BowongModalFunctions/models/requests/__init__.py b/src/BowongModalFunctions/models/requests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/models/web_model.py b/src/BowongModalFunctions/models/requests/models.py
similarity index 52%
rename from src/BowongModalFunctions/models/web_model.py
rename to src/BowongModalFunctions/models/requests/models.py
index c9e2f5e..9c16611 100644
--- a/src/BowongModalFunctions/models/web_model.py
+++ b/src/BowongModalFunctions/models/requests/models.py
@@ -1,160 +1,14 @@
-import json
import uuid
-from datetime import datetime
-from enum import Enum
-from typing import List, Union, Optional, Dict, Any
+from typing import List, Union, Optional, Dict
import pydantic
-from pydantic import BaseModel, Field, field_validator, ConfigDict, HttpUrl, computed_field, RootModel, root_validator, \
- model_validator
+from pydantic import BaseModel, Field, ConfigDict, field_validator, ValidationError, model_validator, computed_field, \
+ EmailStr
-from .ffmpeg_worker_model import FFMpegSliceSegment
-from .media_model import MediaSource, MediaSources, MediaProtocol
-from ..config import WorkerConfig
-from ..utils.TimeUtils import TimeDelta
-from ..utils.VideoUtils import VideoMetadata, FFMPEGSliceOptions
-
-config = WorkerConfig()
-
-
-class TaskStatus(str, Enum):
- running = "running"
- failed = "failed"
- success = "success"
- expired = "expired"
-
-
-class CacheOperationType(str, Enum):
- copy = "copy"
- move = "move"
- delete = "delete"
-
-
-class ErrorCode(int, Enum):
- SUCCESS = 0
- PARAM_ERROR = 10001
- NOT_FOUND = 10002
- UNAUTHORIZED = 10003
- FORBIDDEN = 10004
- BUSINESS_ERROR = 10005
- SYSTEM_ERROR = 99999
-
-
-class WebhookMethodEnum(str, Enum):
- GET = "get"
- POST = "post"
-
-
-class SentryTransactionHeader(BaseModel):
- x_trace_id: Optional[str] = Field(description="Sentry Transaction ID", default=None)
- x_baggage: Optional[str] = Field(description="Sentry Transaction baggage", default=None)
-
-
-class SentryTransactionInfo(BaseModel):
- x_trace_id: str = Field(description="Sentry Transaction ID")
- x_baggage: str = Field(description="Sentry Transaction baggage")
-
-
-class FFMPEGSliceTaskStatusRequest(BaseModel):
- taskId: str = Field(description="任务Id")
-
-
-class ModalTaskResponse(BaseModel):
- success: bool = Field(description="任务接受成功")
- taskId: str = Field(description="任务Id")
-
-
-class CacheDeleteTaskResponse(BaseModel):
- success: bool = Field(description="运行成功")
- keys: List[str] = Field(description="成功从KV和S3删除掉的URN")
- non_kv_keys: List[str] = Field(alias="nonKVKeys", serialization_alias="nonKVKeys",
- description="成功从S3删除的URN, 不存在于KV中")
- not_found_keys: List[str] = Field(alias="notFoundKeys", serialization_alias="notFoundKeys",
- description="不存在的URN")
-
-
-class RecordingTaskResponse(BaseModel):
- success: bool = Field(description="任务接受成功")
- taskId: str = Field(description="任务Id")
- manifest: str = Field(description="播放地址")
- manifest_urn: str = Field(description="播放列表URN")
-
-
-class WebhookNotify(BaseModel):
- endpoint: HttpUrl = Field(description="Webhook回调端点", examples=["https://webhook.example.com?query=123"])
- method: WebhookMethodEnum = Field(
- description="Webhook回调请求方法, 使用POST方法时body与查询ffmpeg任务所获得的json body格式一致")
- headers: Optional[Dict[str, str]] = Field(description="Webhook回调附带的Headers", default=None)
-
- model_config = ConfigDict(json_schema_extra={
- "description": "Webhook返回值与查询ffmpeg任务所获得的json格式一致"
- })
-
-
-class FFMPEGResult(BaseModel):
- urn: str = Field(description="FFMPEG任务结果urn")
- content_length: int = Field(description="媒体资源文件字节大小(Byte)")
- metadata: VideoMetadata = Field(description="媒体元数据")
-
- @computed_field(description="可通过CDN访问的资源链接")
- @property
- def url(self) -> str:
- prefix = f"s3://{config.S3_region}/{config.S3_bucket_name}"
- if not self.urn.startswith(prefix):
- raise ValueError("无法转换非s3前缀协议和非当前挂载点的s3协议")
- return self.urn.replace(prefix, f"{config.S3_cdn_endpoint}")
-
-
-class CacheTask(BaseModel):
- type: CacheOperationType = Field(description="操作类型")
- source: MediaSource = Field(description="源媒体URN")
- target: Optional[MediaSource] = Field(description="目标媒体URN")
-
- @field_validator('source', mode='before')
- @classmethod
- def parse_source(cls, v: Union[str, MediaSource]) -> MediaSource:
- if isinstance(v, str):
- media_source = MediaSource.from_str(v)
- if media_source.protocol == MediaProtocol.s3:
- return media_source
- else:
- raise pydantic.ValidationError('media只支持s3格式的urn')
- elif isinstance(v, MediaSource):
- return v
- else:
- raise pydantic.ValidationError("media格式读取失败")
-
- @field_validator('target', mode='before')
- @classmethod
- def parse_target(cls, v: Union[str, MediaSource]) -> Optional[MediaSource]:
- if v is None:
- return None
- if isinstance(v, str):
- media_source = MediaSource.from_str(v)
- if media_source.protocol == MediaProtocol.s3:
- return media_source
- else:
- raise pydantic.ValidationError('media只支持s3格式的urn')
- elif isinstance(v, MediaSource):
- return v
- else:
- raise pydantic.ValidationError("media格式读取失败")
-
- @model_validator(mode="after")
- def parse_model(self):
- match self.type:
- case CacheOperationType.copy:
- if self.target is None:
- raise pydantic.ValidationError("使用copy行为时必填target URN")
- case CacheOperationType.move:
- if self.target is None:
- raise pydantic.ValidationError("使用move行为时必填target URN")
- case _:
- return self
-
-
-class CacheTaskResult(CacheTask):
- success: bool = Field(default=False, description="执行成功")
+from ..cache_tasks.models import CacheTask, MediaSource, Base64File
+from ..enums.models import MediaProtocol
+from ..ffmpeg_tasks.models import WebhookNotify, FFMpegSliceSegment, FFMPEGSliceOptions
+from ...utils.TimeUtils import TimeDelta
class ClusterCacheBatchRequest(BaseModel):
@@ -163,48 +17,77 @@ class ClusterCacheBatchRequest(BaseModel):
model_config = ConfigDict()
-class ClusterCacheBatchResponse(BaseModel):
- results: List[CacheTaskResult] = Field(description="批量操作任务结果")
+class MediaSourcesRequest(BaseModel):
+ inputs: List[MediaSource] = Field(examples=[
+ [
+ "vod://ap-shanghai/1500034234/1397757910405340824.mp4",
+ "vod://ap-shanghai/1500034234/1397757910403699452.mp4",
+ "s3://ap-northeast-2/modal-media-cache/concat/outputs/fc-01JTPV5FCNA74CKX3N3214XJPD/output.mp4"
+ ]
+ ], description="支持多种协议['vod://', 's3://'], 计划支持['cos://', http://]")
+
+ @field_validator('inputs', mode='before')
+ @classmethod
+ def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
+ if not v:
+ raise ValidationError([
+ {
+ 'loc': ('inputs',),
+ 'msg': "inputs为空",
+ 'type': 'value_error',
+ 'input': v
+ }
+ ], MediaSourcesRequest)
+ result = []
+ for item in v:
+ if isinstance(item, str):
+ result.append(MediaSource.from_str(item))
+ elif isinstance(item, MediaSource):
+ result.append(item)
+ else:
+ raise ValidationError([
+ {
+ 'loc': ('inputs',),
+ 'msg': "inputs元素类型错误: 必须是字符串",
+ 'type': 'value_error',
+ 'input': v
+ }
+ ], MediaSourcesRequest)
+ return result
+
+ model_config = {
+ "arbitrary_types_allowed": True
+ }
+
+
+class UploadBase64Request(BaseModel):
+ file: Base64File = Field(description="上传的文件")
+ prefix: Optional[str] = Field(description="文件存在的前缀目录", default=None)
+
+
+class UploadPresignRequest(BaseModel):
+ key: str = Field(description="上传文件的key", examples=['123/456/abc.mp4'])
+ content_type: str = Field(description="上传对象的文件类型", examples=['video/mp4'])
+
+
+class UploadMultipartPresignRequest(UploadPresignRequest):
+ parts_count: int = Field(description="分片数量")
+
+
+class MediaCopyRequest(BaseModel):
+ class MediaCopyTask(BaseModel):
+ source: MediaSource = Field(description="源媒体")
+ destination: MediaSource = Field(description="")
+
+
+class FFMPEGSliceTaskStatusRequest(BaseModel):
+ taskId: str = Field(description="任务Id")
class BaseFFMPEGTaskRequest(BaseModel):
webhook: Optional[WebhookNotify] = Field(description="Task webhook", default=None)
-class BaseFFMPEGTaskStatusResponse(BaseModel):
- taskId: str = Field(description="任务Id")
- status: TaskStatus = Field(description="任务运行状态")
- error: Optional[str] = Field(description="任务错误原因", default=None)
- code: Optional[int] = Field(description="任务错误原因代码", default=None)
- task_type: str = Field(description="任务类型", default="unknown")
- results: Optional[List[Union[FFMPEGResult, Any]]] = Field(description="任务运行结果", default=None)
-
- model_config = ConfigDict(extra='ignore')
-
-
-class FFMPEGConvertStreamRequest(BaseFFMPEGTaskRequest):
- media: MediaSource = Field(description="待转换的媒体源")
- options: FFMPEGSliceOptions = Field(default_factory=FFMPEGSliceOptions, description="输出质量选项")
-
- @field_validator('media', mode='before')
- @classmethod
- def parse_inputs(cls, v: Union[str, MediaSource]):
- if isinstance(v, str):
- media_source = MediaSource.from_str(v)
- if media_source.protocol == MediaProtocol.hls:
- return media_source
- else:
- raise pydantic.ValidationError('media只支持hls格式的urn')
- elif isinstance(v, MediaSource):
- return v
- else:
- raise pydantic.ValidationError("media格式读取失败")
-
-
-class FFMPEGConvertStreamResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="任务运行结果")
-
-
class FFMPEGSliceRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="待切割的媒体源")
markers: List[FFMpegSliceSegment] = Field(description="按照时间顺序排序过的切割标记数组")
@@ -221,16 +104,8 @@ class FFMPEGSliceRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGSliceTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[List[str]] = Field(default=None, description="任务运行结果")
-
-
class FFMPEGConcatRequest(BaseFFMPEGTaskRequest):
- medias: MediaSources = Field(description="待合并的媒体源")
-
-
-class FFMPEGConcatTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="任务运行结果")
+ medias: MediaSourcesRequest = Field(description="待合并的媒体源")
class FFMPEGExtractAudioRequest(BaseFFMPEGTaskRequest):
@@ -247,10 +122,6 @@ class FFMPEGExtractAudioRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGExtractAudioTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="任务运行结果")
-
-
class FFMPEGCornerMirrorRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
@@ -265,10 +136,6 @@ class FFMPEGCornerMirrorRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGCornerMirrorTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="任务运行结果")
-
-
class FFMPEGBgmRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
bgm_media: MediaSource = Field(description="添加的BGM媒体源", alias="bgmMedia")
@@ -296,10 +163,6 @@ class FFMPEGBgmRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGBgmTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGZoomLoopRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
duration: float = Field(description="放大缩小一个循环的持续时间秒数", default=6.0)
@@ -316,10 +179,6 @@ class FFMPEGZoomLoopRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGZoomLoopTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGOverlayGifRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
gif: MediaSource = Field(description="叠加的特效gif")
@@ -348,10 +207,6 @@ class FFMPEGOverlayGifRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGOverlayGifTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
subtitle: Optional[MediaSource] = Field(default=None, description="需要叠加的字幕文件")
@@ -422,10 +277,6 @@ class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
return self
-class FFMPEGSubtitleTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
media: MediaSource = Field(description="需要处理的媒体源")
bgm: MediaSource = Field(description="需要添加的BGM媒体源")
@@ -467,19 +318,11 @@ class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
raise TypeError(v)
-class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGVideoLoopFillAudioRequest(BaseFFMPEGTaskRequest):
video: MediaSource = Field(description="用来填充的视频素材")
audio: MediaSource = Field(description="被填充的音频素材")
-class FFMPEGVideoLoopFillAudioResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
class FFMPEGExtractFrameRequest(BaseFFMPEGTaskRequest):
video: MediaSource = Field(description="提取帧画面的来源")
seek_time: Optional[Union[str, int, float]] = Field(default=None, description="先跳转到视频对应时间再取首帧",
@@ -515,23 +358,6 @@ class FFMPEGExtractFrameRequest(BaseFFMPEGTaskRequest):
}
-class FFMPEGExtractFrameStatusResponse(BaseFFMPEGTaskStatusResponse):
- result: Optional[str] = Field(default=None, description="生成结果的URN")
-
-
-class ModalTaskCancelResponse(BaseModel):
- success: bool = Field(description="成功取消任务")
- error: Optional[str] = Field(default=None, description="失败原因")
-
-
-class ComfyTaskStatusResponse(BaseModel):
- taskId: str = Field(description="任务Id")
- status: TaskStatus = Field(description="任务运行状态")
- error: Optional[str] = Field(description="任务错误原因", default=None)
- code: Optional[int] = Field(description="任务错误原因代码", default=None)
- result: Optional[str] = Field(description="任务运行结果", default=None)
-
-
class ComfyTaskRequest(BaseFFMPEGTaskRequest):
video_path: MediaSource = Field(
default=None, description="视频源")
@@ -590,40 +416,12 @@ class GeminiRequest(BaseFFMPEGTaskRequest):
raise pydantic.ValidationError("media格式读取失败")
-class GeminiResultResponse(BaseFFMPEGTaskStatusResponse):
- result: str = Field(default="", description="推理出的json")
-
-
-class LiveProduct(BaseModel):
- title: str = Field(default="", description="商品标题")
- leaf_category: str = Field(default="", description="商品分类")
- shop_id: int = Field(default=0, description="店铺ID")
- product_id: str = Field(default="", description="商品ID")
- cover: str = Field(default="", description="商品封面图链接")
- detail_url: str = Field(default="", description="商品详情链接")
-
-
-class LiveProductCaches(BaseModel):
- room_id: str = Field(default="", description="直播间room_id/Room room_id")
- author_id: str = Field(default="", description="作者id/Author id")
- update_time: str = Field(default=0, description="商品列表更新时间")
- count: int = Field(default=0, description="缓存直播间商品数量")
- product_list: List[LiveProduct] = Field(default=None, description="缓存直播间商品列表")
-
-
class MonitorLiveRoomProductRequest(BaseModel):
cookie: str = Field(default="YOUR_COOKIE", description="用户网页版抖音Cookie/Your web version of Douyin Cookie")
room_id: str = Field(default="", description="直播间room_id/Room room_id")
author_id: str = Field(default="", description="作者id/Author id")
-class LiveRoomProductCachesResponse(BaseModel):
- status: int = Field(default=None,
- description="缓存状态/0-正常返回 1-直播已结束 2-IP风控 3-请求Tikhub API错误 4-内部错误")
- message: str = Field(default="", description="错误信息")
- cache_json: str = Field(default="", description="缓存内容/Json文本")
-
-
class MakeGridGeminiRequest(BaseFFMPEGTaskRequest):
pic_info_list: List[Dict[str, str]] = Field(default=[],
description="包含图片信息的字典列表,每个字典包含 \"title\" 和 \"cover\" 键")
@@ -634,115 +432,25 @@ class MakeGridGeminiRequest(BaseFFMPEGTaskRequest):
separator: int = Field(default=12, description="分割线宽度/像素")
-class GoogleUploadResponse(BaseModel):
- kind: str
- id: str
- self_link: HttpUrl = Field(alias='selfLink')
- media_link: HttpUrl = Field(alias='mediaLink')
- name: str
- bucket: str
- generation: str
- metageneration: str
- content_type: str = Field(alias='contentType')
- storage_class: str = Field(alias='storageClass')
- size: int
- md5_hash: str = Field(alias='md5Hash')
- crc32c: str
- etag: str
- time_created: datetime = Field(alias='timeCreated')
- updated: datetime
- time_storage_class_updated: datetime = Field(alias='timeStorageClassUpdated')
- time_finalized: datetime = Field(alias='timeFinalized')
+class FFMPEGConvertStreamRequest(BaseFFMPEGTaskRequest):
+ media: MediaSource = Field(description="待转换的媒体源")
+ options: FFMPEGSliceOptions = Field(default_factory=FFMPEGSliceOptions, description="输出质量选项")
- @computed_field(description="适用于Google内部服务的URN")
- @property
- def urn(self) -> str:
- return ("gs://" + self.id).replace(f"/{self.generation}", "")
-
- model_config = ConfigDict(populate_by_name=True)
+ @field_validator('media', mode='before')
+ @classmethod
+ def parse_inputs(cls, v: Union[str, MediaSource]):
+ if isinstance(v, str):
+ media_source = MediaSource.from_str(v)
+ if media_source.protocol == MediaProtocol.hls:
+ return media_source
+ else:
+ raise pydantic.ValidationError('media只支持hls格式的urn')
+ elif isinstance(v, MediaSource):
+ return v
+ else:
+ raise pydantic.ValidationError("media格式读取失败")
-class VisualFeatures(BaseModel):
- """
- 代表产品的视觉特征
- """
- color: str = Field(
- description="商品详细颜色配色等色彩特征"
- )
- pattern: str = Field(
- description="商品详细图案纹理布料等材质特征, 材料可以根据产品名称和图像确定"
- )
- style: str = Field(
- default=None,
- description="商品详细款式风格设计等有辨识度的款式特征"
- )
-
-
-class ProductInfo(BaseModel):
- """
- 表示从图像中提取单个产品的信息
- """
- image_order: int = Field(
- description="图像出现的顺序"
- )
- image_name: str = Field(
- description="图像上显示的原始文本"
- )
- matched_product: str = Field(
- description="匹配的标准产品名称(必须与产品列表中的标准产品名称保持一致),或者如果没有匹配,则空"
- )
- match_confidence: int = Field(
- ge=0,
- le=100,
- description="产品匹配的置信度范围从0到100"
- )
- visual_features: VisualFeatures = Field(
- description="产品的详细视觉特征"
- )
-
-
-class GeminiFirstStageResponseModel(RootModel):
- """
- 从图像中提取的产品信息列表
- """
- root: List[ProductInfo]
-
-
-class ProductTimeline(BaseModel):
- """
- 表示单个商品及其在视频时间线中的出现信息。
- """
- product: str = Field(description="标准商品名称")
- timeline: List[str] = Field(
- description="该商品在视频中出现的时间段列表。每个字符串表示一个时间段,格式为 '开始时间 - 结束时间 (描述)'"
- )
-
-
-class GeminiSecondStageResponseModel(RootModel):
- """
- 表示一个包含多个商品及其时间线信息的列表。
- """
- root: List[ProductTimeline]
-
-
-class GeminiFirstStagePromptVariables(BaseModel):
- product_list: List[str] = Field(description="商品名列表")
-
- @computed_field(description="xml格式排列的商品列表")
- @property
- def product_list_xml(self) -> str:
- xml_items = [f" {product}" for product in self.product_list]
- xml_string = "\n".join(xml_items)
- return f"\n{xml_string}\n "
-
-
-class GeminiSecondStagePromptVariables(BaseModel):
- product_json_list: List[dict] = Field(description="识别出的商品JSON列表")
-
- @computed_field(description="xml格式排列的识别出的商品JSON列表")
- @property
- def product_json_list_xml(self) -> str:
- xml_items = [f" {json.dumps(product, ensure_ascii=False)}" for product in
- self.product_json_list]
- xml_string = "\n".join(xml_items)
- return f"\n{xml_string}\n "
+class NakamaLogin(BaseModel):
+ email: EmailStr = Field(description="Bowong Echo(Nakama)的登录账号")
+ password: str = Field(description="Bowong Echo(Nakama)的登录密码")
diff --git a/src/BowongModalFunctions/models/responses/__init__.py b/src/BowongModalFunctions/models/responses/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/models/responses/models.py b/src/BowongModalFunctions/models/responses/models.py
new file mode 100644
index 0000000..9b0e4b3
--- /dev/null
+++ b/src/BowongModalFunctions/models/responses/models.py
@@ -0,0 +1,182 @@
+from datetime import datetime
+from typing import List, Dict, Optional, Union, Any
+
+from pydantic import BaseModel, Field, ConfigDict, HttpUrl, computed_field, RootModel
+
+from ..cache_tasks.models import MediaSource, CacheTaskResult, ProductTimeline
+from ..enums.models import TaskStatus
+from ..ffmpeg_tasks.models import FFMPEGResult
+
+
+class DownloadResult(BaseModel):
+ urls: List[str] = Field(description="下载链接")
+
+
+class CacheResultResponse(BaseModel):
+ caches: Dict[str, MediaSource] = Field(description="Cache ID")
+
+
+class UploadResultResponse(BaseModel):
+ media: MediaSource = Field(description="上传完成的媒体资源")
+
+
+class UploadPresignResponse(BaseModel):
+ url: str = Field(description="就近加速的PUT上传地址")
+ urn: str = Field(description="上传成功后获得的对应资源URN")
+ expired_at: datetime = Field(description="上传地址签名过期时间戳")
+
+
+class UploadMultipartPresignResponse(BaseModel):
+ urls: List[str] = Field(description="就近加速的PUT分片上传地址")
+ list_url: str = Field(description="用于确认分片上传状态的请求地址")
+ complete_url: str = Field(description="用于确认完成分片上传的请求地址")
+ urn: str = Field(description="上传成功后获得的对应资源URN")
+ expired_at: datetime = Field(description="上传地址签名过期时间戳")
+
+
+class ModalTaskResponse(BaseModel):
+ success: bool = Field(description="任务接受成功")
+ taskId: str = Field(description="任务Id")
+
+
+class CacheDeleteTaskResponse(BaseModel):
+ success: bool = Field(description="运行成功")
+ keys: List[str] = Field(description="成功从KV和S3删除掉的URN")
+ non_kv_keys: List[str] = Field(alias="nonKVKeys", serialization_alias="nonKVKeys",
+ description="成功从S3删除的URN, 不存在于KV中")
+ not_found_keys: List[str] = Field(alias="notFoundKeys", serialization_alias="notFoundKeys",
+ description="不存在的URN")
+
+
+class RecordingTaskResponse(BaseModel):
+ success: bool = Field(description="任务接受成功")
+ taskId: str = Field(description="任务Id")
+ manifest: str = Field(description="播放地址")
+ manifest_urn: str = Field(description="播放列表URN")
+
+
+class ClusterCacheBatchResponse(BaseModel):
+ results: List[CacheTaskResult] = Field(description="批量操作任务结果")
+
+
+class BaseFFMPEGTaskStatusResponse(BaseModel):
+ taskId: str = Field(description="任务Id")
+ status: TaskStatus = Field(description="任务运行状态")
+ error: Optional[str] = Field(description="任务错误原因", default=None)
+ code: Optional[int] = Field(description="任务错误原因代码", default=None)
+ task_type: str = Field(description="任务类型", default="unknown")
+ results: Optional[List[Union[FFMPEGResult, Any]]] = Field(description="任务运行结果", default=None)
+
+ model_config = ConfigDict(extra='ignore')
+
+
+class FFMPEGConvertStreamResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="任务运行结果")
+
+
+class FFMPEGSliceTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[List[str]] = Field(default=None, description="任务运行结果")
+
+
+class FFMPEGConcatTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="任务运行结果")
+
+
+class FFMPEGExtractAudioTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="任务运行结果")
+
+
+class FFMPEGCornerMirrorTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="任务运行结果")
+
+
+class FFMPEGBgmTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGZoomLoopTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGOverlayGifTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGSubtitleTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGVideoLoopFillAudioResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class FFMPEGExtractFrameStatusResponse(BaseFFMPEGTaskStatusResponse):
+ result: Optional[str] = Field(default=None, description="生成结果的URN")
+
+
+class ModalTaskCancelResponse(BaseModel):
+ success: bool = Field(description="成功取消任务")
+ error: Optional[str] = Field(default=None, description="失败原因")
+
+
+class ComfyTaskStatusResponse(BaseModel):
+ taskId: str = Field(description="任务Id")
+ status: TaskStatus = Field(description="任务运行状态")
+ error: Optional[str] = Field(description="任务错误原因", default=None)
+ code: Optional[int] = Field(description="任务错误原因代码", default=None)
+ result: Optional[str] = Field(description="任务运行结果", default=None)
+
+
+class GeminiResultResponse(BaseFFMPEGTaskStatusResponse):
+ result: str = Field(default="", description="推理出的json")
+
+
+class LiveRoomProductCachesResponse(BaseModel):
+ status: int = Field(default=None,
+ description="缓存状态/0-正常返回 1-直播已结束 2-IP风控 3-请求Tikhub API错误 4-内部错误")
+ message: str = Field(default="", description="错误信息")
+ cache_json: str = Field(default="", description="缓存内容/Json文本")
+
+
+class GoogleUploadResponse(BaseModel):
+ kind: str
+ id: str
+ self_link: HttpUrl = Field(alias='selfLink')
+ media_link: HttpUrl = Field(alias='mediaLink')
+ name: str
+ bucket: str
+ generation: str
+ metageneration: str
+ content_type: str = Field(alias='contentType')
+ storage_class: str = Field(alias='storageClass')
+ size: int
+ md5_hash: str = Field(alias='md5Hash')
+ crc32c: str
+ etag: str
+ time_created: datetime = Field(alias='timeCreated')
+ updated: datetime
+ time_storage_class_updated: datetime = Field(alias='timeStorageClassUpdated')
+ time_finalized: datetime = Field(alias='timeFinalized')
+
+ @computed_field(description="适用于Google内部服务的URN")
+ @property
+ def urn(self) -> str:
+ return ("gs://" + self.id).replace(f"/{self.generation}", "")
+
+ model_config = ConfigDict(populate_by_name=True)
+
+
+class GeminiSecondStageResponseModel(RootModel):
+ """
+ 表示一个包含多个商品及其时间线信息的列表。
+ """
+ root: List[ProductTimeline]
+
+
+class NakamaJWTResponse(BaseModel):
+ token: str = Field(description="Nakama JWT Token")
+ refresh_token: str = Field(description="Nakama JWT Refresh Token")
diff --git a/src/BowongModalFunctions/models/settings/__init__.py b/src/BowongModalFunctions/models/settings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/BowongModalFunctions/config.py b/src/BowongModalFunctions/models/settings/cluster.py
similarity index 92%
rename from src/BowongModalFunctions/config.py
rename to src/BowongModalFunctions/models/settings/cluster.py
index c167d32..aebe21f 100644
--- a/src/BowongModalFunctions/config.py
+++ b/src/BowongModalFunctions/models/settings/cluster.py
@@ -1,4 +1,5 @@
from typing import Optional, Any
+
from pydantic import Field, HttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -20,7 +21,6 @@ class WorkerConfig(BaseSettings):
modal_product_kv_name: str = Field(default='live-product-cache', description="Modal抖音直播间商品缓存KV库")
modal_environment: str = Field(default="dev", description="Modal worker运行环境")
modal_app_name: str = Field(default='bowong-ai-video', description="Modal App集群名称")
- modal_is_local: bool = Field(default=False, description="本地开发环境")
comfyui_s3_input: Optional[str] = Field(default="comfyui-input", description="ComfyUI input S3文件夹名")
comfyui_s3_output: Optional[str] = Field(default="comfyui-output", description="ComfyUI output S3文件夹名")
@@ -29,8 +29,10 @@ class WorkerConfig(BaseSettings):
description="Langfuse public key")
langfuse_secret_key: str = Field(default="sk-lf-dd20cb0b-ef2e-49f6-80f0-b2d9cff1bb11",
description="Langfuse secret key")
+ nakama_client_endpoint: str = Field(default="http://43.143.58.201:7350", description="Nakama client endpoint")
api_version: str = Field(default="0.1.7", description="API接口版本")
+ api_server_token: str = Field(default="bowong7777", description="固定的API调用Bearer Token")
modal_config: Any = SettingsConfigDict(json_schema_extra={
"description": "可通过本地环境变量加载对应Field, 不区分大小写, Modal创建App的Image时可通过dotenv加载指定.env文件写入到Docker Image的系统变量",
diff --git a/src/BowongModalFunctions/router/cache.py b/src/BowongModalFunctions/router/cache.py
index f2adddd..2f648b7 100644
--- a/src/BowongModalFunctions/router/cache.py
+++ b/src/BowongModalFunctions/router/cache.py
@@ -13,22 +13,17 @@ from fastapi.responses import JSONResponse, RedirectResponse
from starlette import status
import boto3
from botocore.config import Config
-from typing_inspection.typing_objects import target
-from ..config import WorkerConfig
+from ..models.settings.cluster import WorkerConfig
from ..middleware.authorization import verify_token
-from ..models.media_model import (MediaSources,
- CacheResult,
- MediaSource,
- MediaCacheStatus,
- DownloadResult,
- UploadResultResponse,
- UploadBase64Request, UploadPresignRequest, UploadPresignResponse,
- UploadMultipartPresignRequest, UploadMultipartPresignResponse, MediaProtocol
- )
-from ..models.web_model import SentryTransactionInfo, MonitorLiveRoomProductRequest, ModalTaskResponse, \
- LiveRoomProductCachesResponse, CacheDeleteTaskResponse, ClusterCacheBatchRequest, CacheOperationType, \
- ClusterCacheBatchResponse, CacheTaskResult
+from ..models.responses.models import DownloadResult, CacheResultResponse, UploadResultResponse, UploadPresignResponse, \
+ UploadMultipartPresignResponse, CacheDeleteTaskResponse, ClusterCacheBatchResponse, LiveRoomProductCachesResponse
+from ..models.enums.models import MediaCacheStatus, CacheOperationType
+from ..models.ffmpeg_tasks.models import SentryTransactionInfo
+from ..models.requests.models import ClusterCacheBatchRequest, MediaSourcesRequest, UploadBase64Request, \
+ UploadPresignRequest, \
+ UploadMultipartPresignRequest, MonitorLiveRoomProductRequest
+from ..models.cache_tasks.models import CacheTaskResult, MediaSource
from ..utils.KVCache import MediaSourceKVCache, LiveProductKVCache
from ..utils.SentryUtils import SentryUtils
@@ -57,9 +52,9 @@ modal_kv_product_cache = LiveProductKVCache(kv_name=config.modal_product_kv_name
summary="缓存视频文件",
description="异步缓存视频文件到S3存储桶和Modal Dict(KV)",
dependencies=[Depends(verify_token)])
-async def cache(medias: MediaSources) -> CacheResult:
+async def cache(medias: MediaSourcesRequest) -> CacheResultResponse:
fn_id = current_function_call_id()
- caches: MediaSources
+ caches: MediaSourcesRequest
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
x_baggage=sentry_sdk.get_baggage())
@@ -123,14 +118,14 @@ async def cache(medias: MediaSources) -> CacheResult:
cache_task_result_list.append(result)
modal_kv_cache.batch_update_cloudflare_kv(cache_task_result_dict)
- return CacheResult(caches={media.urn: media for media in cache_task_result_list})
+ return CacheResultResponse(caches={media.urn: media for media in cache_task_result_list})
@router.delete("/",
summary="清除指定的所有缓存",
description="清除指定的所有缓存(包括KV记录和S3存储文件)",
dependencies=[Depends(verify_token)])
-async def purge_media_kv_file(medias: MediaSources) -> CacheDeleteTaskResponse:
+async def purge_media_kv_file(medias: MediaSourcesRequest) -> CacheDeleteTaskResponse:
fn_id = current_function_call_id()
fn = modal.Function.from_name(config.modal_app_name, "cache_delete", environment_name=config.modal_environment)
@@ -182,7 +177,7 @@ async def purge_media_kv_file(medias: MediaSources) -> CacheDeleteTaskResponse:
description="获取已缓存的视频下载地址",
dependencies=[Depends(verify_token)])
@sentry_sdk.trace
-async def download_caches(medias: MediaSources) -> DownloadResult:
+async def download_caches(medias: MediaSourcesRequest) -> DownloadResult:
cdn_endpoint = config.S3_cdn_endpoint
urls = []
for media in medias.inputs:
@@ -218,7 +213,7 @@ async def purge_kv_all():
summary="删除对应的KV记录",
description="删除请求中对应的视频缓存记录",
dependencies=[Depends(verify_token)])
-async def purge_kv(medias: MediaSources):
+async def purge_kv(medias: MediaSourcesRequest):
try:
for media in medias.inputs:
modal_kv_cache.pop(media.urn)
diff --git a/src/BowongModalFunctions/router/comfyui.py b/src/BowongModalFunctions/router/comfyui.py
index e107ae5..e7fa5d5 100644
--- a/src/BowongModalFunctions/router/comfyui.py
+++ b/src/BowongModalFunctions/router/comfyui.py
@@ -5,13 +5,14 @@ from fastapi import APIRouter, Depends, Header
from fastapi.responses import Response
from starlette import status
-from ..config import WorkerConfig
+from ..models.settings.cluster import WorkerConfig
from ..middleware.authorization import verify_token
-from ..models.media_model import MediaSource
+from ..models.cache_tasks.models import MediaSource
from ..utils.ModalUtils import ModalUtils
-from ..models.web_model import (SentryTransactionHeader,
- ModalTaskResponse, SentryTransactionInfo,
- ComfyTaskRequest, ComfyTaskStatusResponse, TaskStatus)
+from ..models.responses.models import ModalTaskResponse, ComfyTaskStatusResponse
+from ..models.requests.models import ComfyTaskRequest
+from ..models.ffmpeg_tasks.models import SentryTransactionHeader, SentryTransactionInfo
+from ..models.enums.models import TaskStatus
config = WorkerConfig()
diff --git a/src/BowongModalFunctions/router/ffmpeg.py b/src/BowongModalFunctions/router/ffmpeg.py
index 8b37f33..672ad98 100644
--- a/src/BowongModalFunctions/router/ffmpeg.py
+++ b/src/BowongModalFunctions/router/ffmpeg.py
@@ -6,24 +6,15 @@ from fastapi.responses import Response
from loguru import logger
from starlette import status
-from ..config import WorkerConfig
+from ..models.settings.cluster import WorkerConfig
from ..middleware.authorization import verify_token
from ..utils.ModalUtils import ModalUtils
-from ..models.web_model import (FFMPEGSliceRequest, SentryTransactionHeader,
- ModalTaskResponse, SentryTransactionInfo,
- FFMPEGConcatRequest,
- FFMPEGExtractAudioRequest,
- FFMPEGCornerMirrorRequest,
- FFMPEGOverlayGifRequest,
- FFMPEGZoomLoopRequest,
- FFMPEGSubtitleOverlayRequest,
- FFMPEGMixBgmWithNoiseReduceRequest,
- FFMPEGVideoLoopFillAudioRequest,
- FFMPEGConvertStreamRequest,
- FFMPEGExtractFrameRequest,
- BaseFFMPEGTaskStatusResponse,
- FFMPEGStreamRecordRequest,
- RecordingTaskResponse)
+from ..models.responses.models import ModalTaskResponse, RecordingTaskResponse, BaseFFMPEGTaskStatusResponse
+from ..models.requests.models import FFMPEGSliceRequest, FFMPEGConcatRequest, FFMPEGExtractAudioRequest, \
+ FFMPEGCornerMirrorRequest, FFMPEGZoomLoopRequest, FFMPEGOverlayGifRequest, FFMPEGSubtitleOverlayRequest, \
+ FFMPEGMixBgmWithNoiseReduceRequest, FFMPEGVideoLoopFillAudioRequest, FFMPEGExtractFrameRequest, \
+ FFMPEGStreamRecordRequest, FFMPEGConvertStreamRequest
+from ..models.ffmpeg_tasks.models import SentryTransactionHeader, SentryTransactionInfo
config = WorkerConfig()
diff --git a/src/BowongModalFunctions/router/google.py b/src/BowongModalFunctions/router/google.py
index 62b681c..6c66109 100644
--- a/src/BowongModalFunctions/router/google.py
+++ b/src/BowongModalFunctions/router/google.py
@@ -13,12 +13,13 @@ from pydantic import computed_field
from starlette import status
from starlette.responses import JSONResponse
-from BowongModalFunctions.config import WorkerConfig
-from BowongModalFunctions.middleware.authorization import verify_token
-from BowongModalFunctions.models.web_model import SentryTransactionInfo, GeminiResultResponse, GeminiRequest, \
- ModalTaskResponse, MakeGridGeminiRequest, GoogleUploadResponse
-from BowongModalFunctions.utils.ModalUtils import ModalUtils
-from BowongModalFunctions.utils.HTTPUtils import GoogleAuthUtils
+from ..models.settings.cluster import WorkerConfig
+from ..middleware.authorization import verify_token
+from ..models.responses.models import ModalTaskResponse, GeminiResultResponse, GoogleUploadResponse
+from ..models.requests.models import GeminiRequest, MakeGridGeminiRequest
+from ..models.ffmpeg_tasks.models import SentryTransactionInfo
+from ..utils.ModalUtils import ModalUtils
+from ..utils.HTTPUtils import GoogleAuthUtils
config = WorkerConfig()
@@ -191,10 +192,7 @@ async def make_grid_gemini_upload(data: MakeGridGeminiRequest, headers: Annotate
@router.post('/inference_gemini', summary="使用Gemini推理hls视频流指定时间段的打点情况")
-async def inference_gemini(
- data: GeminiRequest,
- headers: Annotated[BundleHeaders, Header()],
-) -> ModalTaskResponse:
+async def inference_gemini(data: GeminiRequest, headers: Annotated[BundleHeaders, Header()], ) -> ModalTaskResponse:
google_api_key = headers.x_google_api_key or os.environ.get("GOOGLE_API_KEY")
if not google_api_key:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing Google API Key")
diff --git a/src/BowongModalFunctions/router/task.py b/src/BowongModalFunctions/router/task.py
index 16b577a..a4445ee 100644
--- a/src/BowongModalFunctions/router/task.py
+++ b/src/BowongModalFunctions/router/task.py
@@ -1,14 +1,18 @@
import modal
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
+from starlette import status
-from ..middleware.authorization import verify_token
-from ..models.web_model import ModalTaskCancelResponse
+from ..middleware.authorization import verify_token, nakama_login
+from ..models.requests.models import NakamaLogin
+from ..models.responses.models import ModalTaskCancelResponse, NakamaJWTResponse
-router = APIRouter(prefix="/task", tags=["tasks"], dependencies=[Depends(verify_token)])
+router = APIRouter(prefix="/task", tags=["tasks"])
-@router.get("/cancel/{task_id}", summary="终止任务", description="终止任务, 无论是正在排队还是真正运行")
+@router.get("/cancel/{task_id}", summary="终止任务",
+ description="终止任务, 无论是正在排队还是真正运行",
+ dependencies=[Depends(verify_token)])
async def task_cancel(task_id: str) -> ModalTaskCancelResponse:
try:
fn_call = modal.FunctionCall.from_id(task_id)
@@ -18,3 +22,12 @@ async def task_cancel(task_id: str) -> ModalTaskCancelResponse:
logger.exception(e)
return ModalTaskCancelResponse(success=False,
error=e.message if hasattr(e, 'message') else str(e))
+
+
+@router.post("/oauth", summary="登录以获取Nakama JWT")
+async def nakama_oauth(body: NakamaLogin) -> NakamaJWTResponse:
+ try:
+ return nakama_login(body.email, body.password)
+ except Exception as e:
+ logger.exception(e)
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账号无法登录")
diff --git a/src/BowongModalFunctions/utils/HTTPUtils.py b/src/BowongModalFunctions/utils/HTTPUtils.py
index c01d2a6..1d9fdfe 100644
--- a/src/BowongModalFunctions/utils/HTTPUtils.py
+++ b/src/BowongModalFunctions/utils/HTTPUtils.py
@@ -12,7 +12,7 @@ from loguru import logger
import aiofiles
from pathlib import Path
from pydantic import BaseModel, computed_field, Field, field_validator
-from BowongModalFunctions.models.web_model import GoogleUploadResponse
+from ..models.responses.models import GoogleUploadResponse
class HTTPDownloadUtils:
@@ -192,7 +192,8 @@ class GoogleAuthUtils:
return GoogleAuthUtils.GoogleAuthResponse.model_validate_json(response.text)
@staticmethod
- async def google_upload_file(file_stream: Union[BinaryIO ,bytes], content_type: str, google_api_key: str, bucket_name: str,
+ async def google_upload_file(file_stream: Union[BinaryIO, bytes], content_type: str, google_api_key: str,
+ bucket_name: str,
filename: str) -> GoogleUploadResponse:
safe_filename = quote(filename)
diff --git a/src/BowongModalFunctions/utils/KVCache.py b/src/BowongModalFunctions/utils/KVCache.py
index db6b2eb..375da42 100644
--- a/src/BowongModalFunctions/utils/KVCache.py
+++ b/src/BowongModalFunctions/utils/KVCache.py
@@ -4,8 +4,7 @@ import httpx
import modal
from loguru import logger
from .VideoUtils import VideoUtils
-from ..models.media_model import MediaSource
-from ..models.web_model import LiveProductCaches
+from ..models.cache_tasks.models import MediaSource, LiveProductCaches
class KVCache:
diff --git a/src/BowongModalFunctions/utils/ModalUtils.py b/src/BowongModalFunctions/utils/ModalUtils.py
index 0a8ae13..caf151c 100644
--- a/src/BowongModalFunctions/utils/ModalUtils.py
+++ b/src/BowongModalFunctions/utils/ModalUtils.py
@@ -1,11 +1,12 @@
-from typing import Tuple, Optional, Any, List
+from typing import Optional, Any, List
import modal
from loguru import logger
-from modal.call_graph import InputStatus
+from modal.call_graph import InputStatus, InputInfo
from pydantic import BaseModel, Field
-from BowongModalFunctions.models.web_model import TaskStatus, SentryTransactionInfo, ErrorCode
+from ..models.ffmpeg_tasks.models import SentryTransactionInfo
+from ..models.enums.models import TaskStatus, ErrorCode
class ModalTaskInfo(BaseModel):
@@ -25,16 +26,16 @@ class ModalUtils:
:param task_id: modal 任务id
:return: (TaskStaus 任务运行状态, function_name 任务名字, errorCode 错误代码, errorReason 错误原因, results 任务结果, sentryTransactionInfo Sentry跟踪信息)
"""
- task = None
+ task: Optional[InputInfo] = None
try:
fn_task = modal.FunctionCall.from_id(task_id)
-
call_graph = fn_task.get_call_graph()
root = call_graph[0]
if len(root.children) == 0:
return ModalTaskInfo(function_name="unknown", error_code=ErrorCode.NOT_FOUND,
status=TaskStatus.expired, error_reason="NOT_FOUND")
- task = root.children[0]
+ # task: Optional[InputInfo] = root.children[0]
+ task: Optional[InputInfo] = next(iter(root.children), None)
if not task.function_call_id == task_id:
return ModalTaskInfo(function_name=task.function_name, status=TaskStatus.expired,
error_code=ErrorCode.NOT_FOUND, error_reason="NOT_FOUND")
diff --git a/src/BowongModalFunctions/utils/SentryUtils.py b/src/BowongModalFunctions/utils/SentryUtils.py
index 5e2703b..1816c6d 100644
--- a/src/BowongModalFunctions/utils/SentryUtils.py
+++ b/src/BowongModalFunctions/utils/SentryUtils.py
@@ -8,8 +8,9 @@ import sentry_sdk
from loguru import logger
import functools
-from BowongModalFunctions.models.web_model import WebhookNotify, WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, \
- TaskStatus, ErrorCode, FFMPEGResult
+from ..models.responses.models import BaseFFMPEGTaskStatusResponse
+from ..models.ffmpeg_tasks.models import WebhookNotify, FFMPEGResult
+from ..models.enums.models import TaskStatus, ErrorCode, WebhookMethodEnum
class SentryUtils:
diff --git a/src/BowongModalFunctions/utils/TimeUtils.py b/src/BowongModalFunctions/utils/TimeUtils.py
index 4ed99bb..51ed5fe 100644
--- a/src/BowongModalFunctions/utils/TimeUtils.py
+++ b/src/BowongModalFunctions/utils/TimeUtils.py
@@ -1,8 +1,6 @@
import re
from datetime import timedelta, datetime
-from loguru import logger
-
class TimeDelta(timedelta):
@classmethod
@@ -23,38 +21,38 @@ def parse_time(time_str):
"""解析时间字符串为datetime对象"""
parsed_time = None
- if re.match(r"^\d{2}:\d{2}:\d{2}\.\d{3}$",time_str):
+ if re.match(r"^\d{2}:\d{2}:\d{2}\.\d{3}$", time_str):
# 先尝试完整格式 HH:MM:SS.fff
parsed_time = datetime.strptime(time_str, '%H:%M:%S.%f')
- elif re.match(r"^\d{2}:\d{2}:\d{2}:\d{3}$",time_str):
+ elif re.match(r"^\d{2}:\d{2}:\d{2}:\d{3}$", time_str):
# 如果失败,尝试 HH:MM:SS:fff 格式
parsed_time = datetime.strptime(time_str, '%H:%M:%S:%f')
- elif re.match(r"^\d{2}:\d{2}\.\d{3}$",time_str):
+ elif re.match(r"^\d{2}:\d{2}\.\d{3}$", time_str):
# 如果失败,尝试 MM:SS.fff 格式
dt = datetime.strptime(time_str, '%M:%S.%f')
# 将小时设为0,只保留分钟和秒
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
- elif re.match(r"^\d{2}\.\d{2}:\d{3}$",time_str):
+ elif re.match(r"^\d{2}\.\d{2}:\d{3}$", time_str):
# 如果失败,尝试 MM.SS:fff 格式
dt = datetime.strptime(time_str, '%M.%S:%f')
# 将小时设为0,只保留分钟和秒
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
- elif re.match(r"^\d{2}\.\d{2}\.\d{3}$",time_str):
+ elif re.match(r"^\d{2}\.\d{2}\.\d{3}$", time_str):
# 如果失败,尝试 MM.SS.fff 格式
dt = datetime.strptime(time_str, '%M.%S.%f')
# 将小时设为0,只保留分钟和秒
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
- elif re.match(r"^\d{2}:\d{2}:\d{3}$",time_str):
+ elif re.match(r"^\d{2}:\d{2}:\d{3}$", time_str):
# 如果失败,尝试 MM:SS:fff 格式
dt = datetime.strptime(time_str, '%M:%S:%f')
# 将小时设为0,只保留分钟和秒
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
- elif re.match(r"^\d{2}:\d{2}$",time_str):
+ elif re.match(r"^\d{2}:\d{2}$", time_str):
# 如果失败,尝试 MM:SS:fff 格式
dt = datetime.strptime(time_str, '%M:%S')
# 将小时设为0,只保留分钟和秒
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
- elif re.match(r"^\d{2}\.\d{2}$",time_str):
+ elif re.match(r"^\d{2}\.\d{2}$", time_str):
# 如果失败,尝试 MM:SS:fff 格式
dt = datetime.strptime(time_str, '%M.%S')
# 将小时设为0,只保留分钟和秒
@@ -136,7 +134,7 @@ def merge_product_data(data, start_time_str, end_time_str, merge_diff=5):
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
microseconds = duration.microseconds
- max_time_str = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}.{microseconds//1000:03d}"
+ max_time_str = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}.{microseconds // 1000:03d}"
product_dict = {}
@@ -155,7 +153,7 @@ def merge_product_data(data, start_time_str, end_time_str, merge_diff=5):
start, end = parse_timeline_item(item)
# 比较起始时间与时间差
start_str = format_time(start)
- if (start - datetime.strptime("00:00:00.000",'%H:%M:%S.%f')) > duration and not start_str.startswith("00"):
+ if (start - datetime.strptime("00:00:00.000", '%H:%M:%S.%f')) > duration and not start_str.startswith("00"):
new_start_str = convert_time(start_str)
else:
new_start_str = start_str
@@ -163,7 +161,7 @@ def merge_product_data(data, start_time_str, end_time_str, merge_diff=5):
new_start_str = max_time_str
end_str = format_time(end)
- if (end - datetime.strptime("00:00:00.000",'%H:%M:%S.%f')) > duration and not end_str.startswith("00"):
+ if (end - datetime.strptime("00:00:00.000", '%H:%M:%S.%f')) > duration and not end_str.startswith("00"):
new_end_str = convert_time(end_str)
else:
new_end_str = end_str
diff --git a/src/BowongModalFunctions/utils/VideoUtils.py b/src/BowongModalFunctions/utils/VideoUtils.py
index 24cbaca..4c0ef4d 100644
--- a/src/BowongModalFunctions/utils/VideoUtils.py
+++ b/src/BowongModalFunctions/utils/VideoUtils.py
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
import aiofiles
import aiohttp
-from typing import Union, List, Tuple, Optional, Dict, Any
+from typing import List, Tuple, Optional, Any
import m3u8
import numpy as np
@@ -14,7 +14,6 @@ import math
from aiohttp import ClientTimeout
from .TimeUtils import TimeDelta
-from pydantic import BaseModel, ConfigDict, computed_field, Field, field_validator
from ffmpeg import FFmpeg
from ffmpeg.asyncio import FFmpeg as AsyncFFmpeg
import soundfile as sf
@@ -35,181 +34,7 @@ from pedalboard.io import AudioFile
from loguru import logger
from .PathUtils import FileUtils
-from ..models.ffmpeg_worker_model import FFMpegSliceSegment
-
-
-class FFMPEGSliceOptions(BaseModel):
- limit_size: Optional[int] = Field(default=None, description="不超过指定文件(字节)大小, 默认为空不限制输出大小")
- bit_rate: Optional[int] = Field(default=None, description="指定输出视频的比特率, 不能与limit_size同时设置")
-
- crf: int = Field(default=16, description="输出视频的质量")
- fps: int = Field(default=30, description="输出视频的FPS")
- width: int = Field(default=None, description="输出视频的宽(像素)")
- height: int = Field(default=None, description="输出视频的高(像素)")
-
- @computed_field(description="解析为字符表达式的文件大小")
- @property
- def pretty_limit_size(self) -> Optional[str]:
- if self.limit_size is not None:
- for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
- if self.limit_size < 1024.0:
- return f"{self.limit_size:.2f}{unit}"
- self.limit_size /= 1024.0
- return f"{self.limit_size:.2f}PB"
- else:
- return None
-
- @computed_field(description="解析为字符表达式的比特率")
- @property
- def pretty_bit_rate(self) -> Optional[str]:
- if self.bit_rate is not None:
- return f"{self.bit_rate}k"
-
-
-class MediaStream(BaseModel):
- duration: float = Field(0, description="时长")
- codec_name: str
- tags: Optional[Any] = Field(None)
-
- model_config = ConfigDict(extra='allow')
-
-
-class HLSMediaVideoStream(BaseModel):
- stream_type: str = "video"
- codec_name: str
- codec_type: str
- width: int
- height: int
- avg_frame_rate: str
- tags: Optional[Any] = Field(None)
- duration: Optional[float] = Field(None)
-
- @computed_field
- @property
- def video_frame_rate(self) -> float:
- numerator, denominator = map(int, self.avg_frame_rate.split('/'))
- if denominator != 0:
- return numerator / denominator
- return 0
-
-
-class HLSMediaAudioStream(BaseModel):
- stream_type: str = "audio"
- sample_rate: str
- channels: int
- channel_layout: str
- start_time: str
- tags: Optional[Any] = Field(None)
-
-
-class AudioStream(MediaStream):
- stream_type: str = Field("audio")
- codec_type: str
- sample_rate: str
- channels: int
- tags: Optional[Any] = Field(None)
-
- model_config = ConfigDict(extra='allow')
-
-
-class VideoStream(MediaStream):
- stream_type: str = "video"
- width: int
- height: int
- bit_rate: int
- avg_frame_rate: str
-
- @computed_field
- @property
- def video_bitrate(self) -> str:
- return str(int(self.bit_rate / 1000)) + 'k'
-
- @computed_field
- @property
- def video_frame_rate(self) -> float:
- numerator, denominator = map(int, self.avg_frame_rate.split('/'))
- if denominator != 0:
- return numerator / denominator
- return 0
-
- model_config = ConfigDict(extra='allow')
-
-
-class ImageStream(MediaStream):
- stream_type: str = "image"
- width: int
- height: int
-
- model_config = ConfigDict(extra='allow')
-
-
-class SubtitleStreamTags(BaseModel):
- language: str = Field(description="内嵌字幕的语言")
-
- model_config = ConfigDict(extra='allow')
-
-
-class SubtitleStream(MediaStream):
- stream_type: str = "subtitle"
- tags: SubtitleStreamTags
-
- model_config = ConfigDict(extra='allow')
-
-
-class VideoFormat(BaseModel):
- filename: str
- format_name: str
- start_time: float = Field(0, description="起始时间")
- size: int
- bit_rate: Optional[int] = Field(None, description="文件比特率")
- duration: float = Field(3600 * 12, description="文件时长")
-
-
-class VideoMetadata(BaseModel):
- streams: List[
- Union[ImageStream, AudioStream, VideoStream, HLSMediaAudioStream, HLSMediaVideoStream, SubtitleStream]] = Field(
- description="媒体包含的数据轨道")
- format: Optional[VideoFormat] = Field(None)
-
- @field_validator('streams', mode='before')
- def parse_streams(cls, value):
- streams = []
- if isinstance(value, List):
- for stream in value:
- if isinstance(stream, Dict):
- logger.info(f"Parsing stream : {json.dumps(stream, ensure_ascii=False)}")
- if stream.get("codec_type") == 'audio':
- if stream.get("duration") is None:
- logger.info("Parsing audio stream")
- hls_audio = HLSMediaAudioStream.model_validate(stream)
- streams.append(hls_audio)
- else:
- logger.info("Parsing hls audio stream")
- audio = AudioStream.model_validate(stream)
- streams.append(audio)
- elif stream.get("codec_type") == 'video':
- if stream.get("codec_name") in ("gif", "png", "mjpg", "mjpeg", "webp"):
- logger.info("Parsing image stream")
- image = ImageStream.model_validate(stream)
- streams.append(image)
- else:
- if stream.get("duration") is None:
- logger.info("Parsing hls video stream")
- hls_video = HLSMediaVideoStream.model_validate(stream)
- streams.append(hls_video)
- else:
- logger.info("Parsing video stream")
- video = VideoStream.model_validate(stream)
- streams.append(video)
- elif stream.get("codec_type") == 'subtitle':
- logger.info("Parsing subtitle stream")
- subtitle_stream = SubtitleStream.model_validate(stream)
- streams.append(subtitle_stream)
- return streams
- else:
- raise TypeError
-
- model_config = ConfigDict(extra='allow')
+from ..models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, VideoStream, VideoMetadata
class VideoUtils:
diff --git a/src/Douyin_TikTok_Download_API/tikhub_api.py b/src/Douyin_TikTok_Download_API/tikhub_api.py
new file mode 100644
index 0000000..2d0101e
--- /dev/null
+++ b/src/Douyin_TikTok_Download_API/tikhub_api.py
@@ -0,0 +1,22 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from scalar_fastapi import get_scalar_api_reference
+from Douyin_TikTok_Download_API.app.api import router
+web_app_tikhub = FastAPI(title="Modal Tikhub API",
+ version="1.0.0",
+ summary="Modal Tikhub API, 包含抖音Web API",
+ )
+
+web_app_tikhub.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+@web_app_tikhub.get("/scalar", include_in_schema=False)
+async def scalar():
+ return get_scalar_api_reference(openapi_url='/openapi.json', title="Modal worker web endpoint")
+
+web_app_tikhub.include_router(router.router)
\ No newline at end of file
diff --git a/src/cluster/app.py b/src/cluster/app.py
index e1365bd..ae35187 100644
--- a/src/cluster/app.py
+++ b/src/cluster/app.py
@@ -1,5 +1,5 @@
import modal
-from BowongModalFunctions.config import WorkerConfig
+from BowongModalFunctions.models.settings.cluster import WorkerConfig
from .video import app as media_app
from .web import app as web_app
from .ffmpeg_app import app as ffmpeg_app
diff --git a/src/cluster/comfyui_v1.py b/src/cluster/comfyui_v1.py
index 27e2d50..548af29 100644
--- a/src/cluster/comfyui_v1.py
+++ b/src/cluster/comfyui_v1.py
@@ -75,8 +75,8 @@ with comfyui_image.imports():
from typing import Tuple, Any, Dict, Optional
import sentry_sdk
from modal import current_function_call_id
- from BowongModalFunctions.config import WorkerConfig
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify
+ from BowongModalFunctions.models.settings.cluster import WorkerConfig
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from sentry_sdk.integrations.loguru import LoguruIntegration
diff --git a/src/cluster/comfyui_v2.py b/src/cluster/comfyui_v2.py
index d445b5f..ee5558e 100644
--- a/src/cluster/comfyui_v2.py
+++ b/src/cluster/comfyui_v2.py
@@ -56,10 +56,10 @@ with comfyui_latentsync_1_5_image.imports():
import sentry_sdk
import psutil
from loguru import logger
- from BowongModalFunctions.config import WorkerConfig
+ from BowongModalFunctions.models.settings.cluster import WorkerConfig
from modal import current_function_call_id
from sentry_sdk.integrations.loguru import LoguruIntegration
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify
from BowongModalFunctions.utils.SentryUtils import SentryUtils
config = WorkerConfig()
diff --git a/src/cluster/ffmpeg_app.py b/src/cluster/ffmpeg_app.py
index 2e73cc6..1a15c82 100644
--- a/src/cluster/ffmpeg_app.py
+++ b/src/cluster/ffmpeg_app.py
@@ -19,7 +19,7 @@ with ffmpeg_worker_image.imports():
import shutil, backoff, sentry_sdk
from typing import List
from loguru import logger
- from BowongModalFunctions.config import WorkerConfig
+ from BowongModalFunctions.models.settings.cluster import WorkerConfig
config = WorkerConfig()
diff --git a/src/cluster/ffmpeg_apps/bgm_nosie_reduce.py b/src/cluster/ffmpeg_apps/bgm_nosie_reduce.py
index 797907c..603b83f 100644
--- a/src/cluster/ffmpeg_apps/bgm_nosie_reduce.py
+++ b/src/cluster/ffmpeg_apps/bgm_nosie_reduce.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/concat_medias.py b/src/cluster/ffmpeg_apps/concat_medias.py
index 85fa93b..6226e0e 100644
--- a/src/cluster/ffmpeg_apps/concat_medias.py
+++ b/src/cluster/ffmpeg_apps/concat_medias.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSources
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.requests.models import MediaSourcesRequest
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
@@ -26,7 +26,7 @@ with ffmpeg_worker_image.imports():
),
}, )
@modal.concurrent(max_inputs=1)
- async def ffmpeg_concat_medias(medias: MediaSources,
+ async def ffmpeg_concat_medias(medias: MediaSourcesRequest,
sentry_trace: Optional[SentryTransactionInfo] = None,
webhook: Optional[WebhookNotify] = None) -> Tuple[
FFMPEGResult, Optional[SentryTransactionInfo]]:
@@ -36,7 +36,7 @@ with ffmpeg_worker_image.imports():
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
- async def ffmpeg_process(media_sources: MediaSources, output_filepath: str) -> FFMPEGResult:
+ async def ffmpeg_process(media_sources: MediaSourcesRequest, output_filepath: str) -> FFMPEGResult:
input_videos = [f"{s3_mount}/{media_source.cache_filepath}" for media_source in media_sources.inputs]
local_output_path, metadata = await VideoUtils.ffmpeg_concat_medias(media_paths=input_videos,
output_path=output_filepath)
diff --git a/src/cluster/ffmpeg_apps/convert_stream.py b/src/cluster/ffmpeg_apps/convert_stream.py
index a7653f3..ab620e1 100644
--- a/src/cluster/ffmpeg_apps/convert_stream.py
+++ b/src/cluster/ffmpeg_apps/convert_stream.py
@@ -3,11 +3,13 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource, MediaProtocol
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.enums.models import MediaProtocol
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
- from BowongModalFunctions.utils.VideoUtils import VideoUtils, FFMPEGSliceOptions
+ from BowongModalFunctions.utils.VideoUtils import VideoUtils
+ from BowongModalFunctions.models.ffmpeg_tasks.models import FFMPEGSliceOptions, SentryTransactionInfo, WebhookNotify, \
+ FFMPEGResult
import sentry_sdk
from typing import Optional, Tuple
from modal import current_function_call_id
diff --git a/src/cluster/ffmpeg_apps/corner_mirror.py b/src/cluster/ffmpeg_apps/corner_mirror.py
index 8a2957f..af4d87c 100644
--- a/src/cluster/ffmpeg_apps/corner_mirror.py
+++ b/src/cluster/ffmpeg_apps/corner_mirror.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/extract_audio.py b/src/cluster/ffmpeg_apps/extract_audio.py
index f98bac6..a2402a1 100644
--- a/src/cluster/ffmpeg_apps/extract_audio.py
+++ b/src/cluster/ffmpeg_apps/extract_audio.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/extract_frame.py b/src/cluster/ffmpeg_apps/extract_frame.py
index 73bbfc4..ee7d138 100644
--- a/src/cluster/ffmpeg_apps/extract_frame.py
+++ b/src/cluster/ffmpeg_apps/extract_frame.py
@@ -4,8 +4,9 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource, MediaProtocol
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.enums.models import MediaProtocol
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/loop_fill.py b/src/cluster/ffmpeg_apps/loop_fill.py
index b4c8453..7f7a922 100644
--- a/src/cluster/ffmpeg_apps/loop_fill.py
+++ b/src/cluster/ffmpeg_apps/loop_fill.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/overlay_gif.py b/src/cluster/ffmpeg_apps/overlay_gif.py
index 1d61b5f..5922aae 100644
--- a/src/cluster/ffmpeg_apps/overlay_gif.py
+++ b/src/cluster/ffmpeg_apps/overlay_gif.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/slice_media.py b/src/cluster/ffmpeg_apps/slice_media.py
index eccb2a8..dcfcd01 100644
--- a/src/cluster/ffmpeg_apps/slice_media.py
+++ b/src/cluster/ffmpeg_apps/slice_media.py
@@ -3,12 +3,14 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
- from BowongModalFunctions.models.media_model import MediaSources, MediaProtocol, MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, \
+ SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.requests.models import MediaSourcesRequest
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.enums.models import MediaProtocol
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
- from BowongModalFunctions.utils.VideoUtils import VideoUtils, FFMPEGSliceOptions
+ from BowongModalFunctions.utils.VideoUtils import VideoUtils
import sentry_sdk
from loguru import logger
from typing import Optional, Tuple, List
diff --git a/src/cluster/ffmpeg_apps/stream_record_as_hls.py b/src/cluster/ffmpeg_apps/stream_record_as_hls.py
index 7bc91e9..255cea0 100644
--- a/src/cluster/ffmpeg_apps/stream_record_as_hls.py
+++ b/src/cluster/ffmpeg_apps/stream_record_as_hls.py
@@ -6,8 +6,9 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult, \
- WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, TaskStatus
+ from BowongModalFunctions.models.responses.models import BaseFFMPEGTaskStatusResponse
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.enums.models import TaskStatus, WebhookMethodEnum
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
import sentry_sdk
diff --git a/src/cluster/ffmpeg_apps/subtitle_apply.py b/src/cluster/ffmpeg_apps/subtitle_apply.py
index 2666565..f53b0c3 100644
--- a/src/cluster/ffmpeg_apps/subtitle_apply.py
+++ b/src/cluster/ffmpeg_apps/subtitle_apply.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/ffmpeg_apps/zoom_loop.py b/src/cluster/ffmpeg_apps/zoom_loop.py
index 49a83f7..c95d759 100644
--- a/src/cluster/ffmpeg_apps/zoom_loop.py
+++ b/src/cluster/ffmpeg_apps/zoom_loop.py
@@ -3,8 +3,8 @@ import modal
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
with ffmpeg_worker_image.imports():
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
from BowongModalFunctions.utils.PathUtils import FileUtils
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.utils.VideoUtils import VideoUtils
diff --git a/src/cluster/video.py b/src/cluster/video.py
index 1468fe3..06b70c7 100644
--- a/src/cluster/video.py
+++ b/src/cluster/video.py
@@ -28,9 +28,9 @@ with downloader_image.imports():
from typing import List
from loguru import logger
- from BowongModalFunctions.config import WorkerConfig
+ from BowongModalFunctions.models.settings.cluster import WorkerConfig
from BowongModalFunctions.utils.KVCache import MediaSourceKVCache, LiveProductKVCache
- from BowongModalFunctions.models.media_model import MediaSource
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
config = WorkerConfig()
diff --git a/src/cluster/video_apps/cache_delete.py b/src/cluster/video_apps/cache_delete.py
index 4ceb408..bf0919b 100644
--- a/src/cluster/video_apps/cache_delete.py
+++ b/src/cluster/video_apps/cache_delete.py
@@ -4,7 +4,8 @@ from ..video import downloader_image, app, config
with downloader_image.imports():
import os
- from BowongModalFunctions.models.media_model import MediaSource, MediaCacheStatus
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.enums.models import MediaCacheStatus
@app.function(cpu=1, timeout=300,
diff --git a/src/cluster/video_apps/cache_submit.py b/src/cluster/video_apps/cache_submit.py
index 6100f88..fa1594c 100644
--- a/src/cluster/video_apps/cache_submit.py
+++ b/src/cluster/video_apps/cache_submit.py
@@ -15,8 +15,9 @@ with downloader_image.imports():
from tencentcloud.vod.v20180717.vod_client import VodClient
from tencentcloud.vod.v20180717 import models as vod_request_models
- from BowongModalFunctions.models.media_model import MediaSource, MediaCacheStatus, MediaProtocol
- from BowongModalFunctions.models.web_model import SentryTransactionInfo
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource
+ from BowongModalFunctions.models.enums.models import MediaProtocol, MediaCacheStatus
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo
@app.function(cpu=1, timeout=1800,
diff --git a/src/cluster/video_apps/hls_slice_inference.py b/src/cluster/video_apps/hls_slice_inference.py
index e6d9959..0770657 100644
--- a/src/cluster/video_apps/hls_slice_inference.py
+++ b/src/cluster/video_apps/hls_slice_inference.py
@@ -18,12 +18,13 @@ with downloader_image.imports():
from fastapi import HTTPException
from starlette import status
from BowongModalFunctions.utils.SentryUtils import SentryUtils
- from BowongModalFunctions.models.media_model import MediaSource
- from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, GeminiFirstStagePromptVariables, \
- GeminiSecondStagePromptVariables, GeminiFirstStageResponseModel, GeminiSecondStageResponseModel
- from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
+ from BowongModalFunctions.models.cache_tasks.models import MediaSource, GeminiFirstStageResponseModel, \
+ GeminiFirstStagePromptVariables, GeminiSecondStagePromptVariables
+ from BowongModalFunctions.models.responses.models import GeminiSecondStageResponseModel
+ from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, \
+ SentryTransactionInfo, WebhookNotify
from BowongModalFunctions.utils.TimeUtils import TimeDelta, merge_product_data
- from BowongModalFunctions.utils.VideoUtils import FFMPEGSliceOptions, VideoUtils
+ from BowongModalFunctions.utils.VideoUtils import VideoUtils
@app.function(cpu=(0.5, 64), timeout=1800,
diff --git a/src/cluster/video_apps/make_grid_upload.py b/src/cluster/video_apps/make_grid_upload.py
index 6742f35..bb0a6b3 100644
--- a/src/cluster/video_apps/make_grid_upload.py
+++ b/src/cluster/video_apps/make_grid_upload.py
@@ -18,7 +18,8 @@ with downloader_image.imports():
from modal import current_function_call_id
from BowongModalFunctions.utils.SentryUtils import SentryUtils
- from BowongModalFunctions.models.web_model import SentryTransactionInfo
+ from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo
+
@app.function(cpu=(0.5, 16),
max_containers=config.video_downloader_concurrency,
diff --git a/src/cluster/video_apps/monitor_live_room_product_trigger.py b/src/cluster/video_apps/monitor_live_room_product_trigger.py
index c19890a..9543dbf 100644
--- a/src/cluster/video_apps/monitor_live_room_product_trigger.py
+++ b/src/cluster/video_apps/monitor_live_room_product_trigger.py
@@ -8,7 +8,8 @@ with downloader_image.imports():
from datetime import datetime, timedelta, timezone
from httpx import Timeout
- from BowongModalFunctions.models.web_model import LiveProduct, LiveProductCaches
+ from BowongModalFunctions.models.cache_tasks.models import LiveProduct, LiveProductCaches
+
@app.function(max_containers=config.video_downloader_concurrency, timeout=130)
@modal.concurrent(max_inputs=50)
diff --git a/src/cluster/web.py b/src/cluster/web.py
index e61bbf9..2276955 100644
--- a/src/cluster/web.py
+++ b/src/cluster/web.py
@@ -22,8 +22,9 @@ app = modal.App(
include_source=False)
with fastapi_image.imports():
- from BowongModalFunctions.api import web_app, web_app_tikhub
- from BowongModalFunctions.config import WorkerConfig
+ from BowongModalFunctions.api import web_app
+ from BowongModalFunctions.models.settings.cluster import WorkerConfig
+ from Douyin_TikTok_Download_API.tikhub_api import web_app_tikhub
config = WorkerConfig()
@@ -44,11 +45,10 @@ with fastapi_image.imports():
def fastapi_webapp():
return web_app
+
# 短保持时间保证IP不停更换
@app.function(scaledown_window=2,
- secrets=[
- modal.Secret.from_name("cf-kv-secret"),
- ],
+ secrets=[modal.Secret.from_name("cf-kv-secret"), ],
cloud="aws",
volumes={
config.S3_mount_dir: modal.CloudBucketMount(
diff --git a/tests/ffmpeg_test_case.py b/tests/ffmpeg_test_case.py
index 41e73bd..113c5fa 100644
--- a/tests/ffmpeg_test_case.py
+++ b/tests/ffmpeg_test_case.py
@@ -5,8 +5,8 @@ from typing import List, Optional
import httpx
from loguru import logger
from tqdm import tqdm
-from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
-from BowongModalFunctions.utils.VideoUtils import VideoUtils, FFMPEGSliceOptions
+from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions
+from BowongModalFunctions.utils.VideoUtils import VideoUtils
class FFMPEGTestCase(unittest.IsolatedAsyncioTestCase):
diff --git a/tests/pydantic_model_validation_test.py b/tests/pydantic_model_validation_test.py
index 87b2149..5808b56 100644
--- a/tests/pydantic_model_validation_test.py
+++ b/tests/pydantic_model_validation_test.py
@@ -3,7 +3,7 @@ import unittest
import httpx
from loguru import logger
-from BowongModalFunctions.models.web_model import FFMPEGSubtitleOverlayRequest
+from BowongModalFunctions.models.requests.models import FFMPEGSubtitleOverlayRequest
class PydanticModelTestCase(unittest.TestCase):