新增Nakama登录接口,同时支持使用Nakama JWT调用API
* 新增Nakama登录接口,同时支持使用Nakama JWT调用API * 将tikhub_api和api分离 * 拆分models为按功能划分,以便管理依赖 --------- Merge request URL: https://g-ldyi2063.coding.net/p/dev/d/modalDeploy/git/merge/4865?initial=true Co-authored-by: shuohigh@gmail.com
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>{product}</product>" for product in self.product_list]
|
||||
xml_string = "\n".join(xml_items)
|
||||
return f"<products>\n{xml_string}\n </products>"
|
||||
|
||||
|
||||
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" <product>{json.dumps(product, ensure_ascii=False)}</product>" for product in
|
||||
self.product_json_list]
|
||||
xml_string = "\n".join(xml_items)
|
||||
return f"<products>\n{xml_string}\n </products>"
|
||||
0
src/BowongModalFunctions/models/enums/__init__.py
Normal file
0
src/BowongModalFunctions/models/enums/__init__.py
Normal file
47
src/BowongModalFunctions/models/enums/models.py
Normal file
47
src/BowongModalFunctions/models/enums/models.py
Normal file
@@ -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"
|
||||
310
src/BowongModalFunctions/models/ffmpeg_tasks/models.py
Normal file
310
src/BowongModalFunctions/models/ffmpeg_tasks/models.py
Normal file
@@ -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}")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>{product}</product>" for product in self.product_list]
|
||||
xml_string = "\n".join(xml_items)
|
||||
return f"<products>\n{xml_string}\n </products>"
|
||||
|
||||
|
||||
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" <product>{json.dumps(product, ensure_ascii=False)}</product>" for product in
|
||||
self.product_json_list]
|
||||
xml_string = "\n".join(xml_items)
|
||||
return f"<products>\n{xml_string}\n </products>"
|
||||
class NakamaLogin(BaseModel):
|
||||
email: EmailStr = Field(description="Bowong Echo(Nakama)的登录账号")
|
||||
password: str = Field(description="Bowong Echo(Nakama)的登录密码")
|
||||
182
src/BowongModalFunctions/models/responses/models.py
Normal file
182
src/BowongModalFunctions/models/responses/models.py
Normal file
@@ -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")
|
||||
@@ -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的系统变量",
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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="账号无法登录")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
22
src/Douyin_TikTok_Download_API/tikhub_api.py
Normal file
22
src/Douyin_TikTok_Download_API/tikhub_api.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user