新增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 .utils.KVCache import MediaSourceKVCache
|
||||||
from .router import ffmpeg, cache, comfyui, google, task
|
from .router import ffmpeg, cache, comfyui, google, task
|
||||||
from Douyin_TikTok_Download_API.app.api import router as tikhub
|
from .models.settings.cluster import WorkerConfig
|
||||||
from .config import WorkerConfig
|
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
@@ -23,14 +22,9 @@ web_app = FastAPI(title="Modal worker API",
|
|||||||
])
|
])
|
||||||
|
|
||||||
web_app_tikhub = FastAPI(title="Modal Tikhub API",
|
web_app_tikhub = FastAPI(title="Modal Tikhub API",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
summary="Modal Tikhub API, 包含抖音Web API",
|
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'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2",
|
sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2",
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
@@ -73,14 +67,6 @@ web_app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
web_app_tikhub.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.middleware("http")
|
@web_app.middleware("http")
|
||||||
@web_app.middleware("https")
|
@web_app.middleware("https")
|
||||||
@@ -94,10 +80,6 @@ async def alias_middleware(request: Request, call_next):
|
|||||||
async def scalar():
|
async def scalar():
|
||||||
return get_scalar_api_reference(openapi_url='/openapi.json', title="Modal worker web endpoint")
|
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(ffmpeg.router)
|
||||||
web_app.include_router(cache.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(google.router)
|
||||||
web_app.include_router(task.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 import Depends, HTTPException
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
from pydantic import EmailStr
|
||||||
from starlette import status
|
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)
|
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:
|
def is_valid_token(token: str) -> bool:
|
||||||
# 这里实现具体的token验证逻辑
|
#
|
||||||
# 例如:验证JWT token,检查数据库中的token等
|
if token == config.api_server_token:
|
||||||
return token == "bowong7777"
|
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 os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
|
||||||
from functools import cached_property
|
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 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 pydantic.json_schema import JsonSchemaValue
|
||||||
from ..config import WorkerConfig
|
from pydantic_core.core_schema import SerializationInfo
|
||||||
from ..utils.TimeUtils import TimeDelta
|
|
||||||
from ..utils.VideoUtils import VideoMetadata
|
from ..enums.models import MediaProtocol, MediaCacheStatus, CacheOperationType
|
||||||
|
from ..settings.cluster import WorkerConfig
|
||||||
|
from ..ffmpeg_tasks.models import VideoMetadata
|
||||||
|
|
||||||
config = WorkerConfig()
|
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):
|
class MediaSource(BaseModel):
|
||||||
path: str = Field(description="媒体源的路径")
|
path: str = Field(description="媒体源的路径")
|
||||||
@@ -63,7 +44,7 @@ class MediaSource(BaseModel):
|
|||||||
elif media_url.startswith('s3://'):
|
elif media_url.startswith('s3://'):
|
||||||
pattern = r"^s3://[a-z]{2}-[a-z]+-\d/.*$"
|
pattern = r"^s3://[a-z]{2}-[a-z]+-\d/.*$"
|
||||||
if not re.match(pattern, media_url):
|
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}
|
# s3://{endpoint}/{bucket}/{url}
|
||||||
paths = media_url[5:].split('/')
|
paths = media_url[5:].split('/')
|
||||||
|
|
||||||
@@ -139,7 +120,7 @@ class MediaSource(BaseModel):
|
|||||||
match self.protocol:
|
match self.protocol:
|
||||||
case MediaProtocol.s3:
|
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}"
|
return f"{self.path}"
|
||||||
else:
|
else:
|
||||||
return f"{self.protocol.value}/{self.endpoint}/{self.bucket}/{self.path}"
|
return f"{self.protocol.value}/{self.endpoint}/{self.bucket}/{self.path}"
|
||||||
@@ -169,7 +150,7 @@ class MediaSource(BaseModel):
|
|||||||
@computed_field(description="本地挂载地址")
|
@computed_field(description="本地挂载地址")
|
||||||
@property
|
@property
|
||||||
def local_mount_path(self) -> str:
|
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')
|
@field_serializer('expired_at')
|
||||||
def serialize_datetime(self, value: Optional[datetime], info: SerializationInfo) -> Optional[str]:
|
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}"
|
return f"{self.protocol.value}://{self.endpoint}/{self.bucket}/{self.path}"
|
||||||
|
|
||||||
|
|
||||||
class MediaSources(BaseModel):
|
class CacheTask(BaseModel):
|
||||||
inputs: List[MediaSource] = Field(examples=[
|
type: CacheOperationType = Field(description="操作类型")
|
||||||
[
|
source: MediaSource = Field(description="源媒体URN")
|
||||||
"vod://ap-shanghai/1500034234/1397757910405340824.mp4",
|
target: Optional[MediaSource] = Field(description="目标媒体URN")
|
||||||
"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')
|
@field_validator('source', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
def parse_source(cls, v: Union[str, MediaSource]) -> MediaSource:
|
||||||
if not v:
|
if isinstance(v, str):
|
||||||
raise ValidationError([
|
media_source = MediaSource.from_str(v)
|
||||||
{
|
if media_source.protocol == MediaProtocol.s3:
|
||||||
'loc': ('inputs',),
|
return media_source
|
||||||
'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)
|
|
||||||
else:
|
else:
|
||||||
raise ValidationError([
|
raise ValidationError('media只支持s3格式的urn')
|
||||||
{
|
elif isinstance(v, MediaSource):
|
||||||
'loc': ('inputs',),
|
return v
|
||||||
'msg': "inputs元素类型错误: 必须是字符串",
|
else:
|
||||||
'type': 'value_error',
|
raise ValidationError("media格式读取失败")
|
||||||
'input': v
|
|
||||||
}
|
|
||||||
], MediaSources)
|
|
||||||
return result
|
|
||||||
|
|
||||||
model_config = {
|
@field_validator('target', mode='before')
|
||||||
"arbitrary_types_allowed": True
|
@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):
|
class CacheTaskResult(CacheTask):
|
||||||
caches: Dict[str, MediaSource] = Field(description="Cache ID")
|
success: bool = Field(default=False, description="执行成功")
|
||||||
|
|
||||||
|
|
||||||
class DownloadResult(BaseModel):
|
|
||||||
urls: List[str] = Field(description="下载链接")
|
|
||||||
|
|
||||||
|
|
||||||
class UploadResultResponse(BaseModel):
|
|
||||||
media: MediaSource = Field(description="上传完成的媒体资源")
|
|
||||||
|
|
||||||
|
|
||||||
class Base64File(BaseModel):
|
class Base64File(BaseModel):
|
||||||
@@ -260,49 +238,103 @@ class Base64File(BaseModel):
|
|||||||
filename: str = Field(description="文件名, 包含文件类型后缀")
|
filename: str = Field(description="文件名, 包含文件类型后缀")
|
||||||
content_type: 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="文件字节大小")
|
@computed_field(description="文件字节大小")
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
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)
|
return len(self.raw_content)
|
||||||
|
|
||||||
|
|
||||||
class UploadBase64Request(BaseModel):
|
class LiveProduct(BaseModel):
|
||||||
file: Base64File = Field(description="上传的文件")
|
title: str = Field(default="", description="商品标题")
|
||||||
prefix: Optional[str] = Field(description="文件存在的前缀目录", default=None)
|
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):
|
class LiveProductCaches(BaseModel):
|
||||||
key: str = Field(description="上传文件的key", examples=['123/456/abc.mp4'])
|
room_id: str = Field(default="", description="直播间room_id/Room room_id")
|
||||||
content_type: str = Field(description="上传对象的文件类型", examples=['video/mp4'])
|
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):
|
class VisualFeatures(BaseModel):
|
||||||
url: str = Field(description="就近加速的PUT上传地址")
|
"""
|
||||||
urn: str = Field(description="上传成功后获得的对应资源URN")
|
代表产品的视觉特征
|
||||||
expired_at: datetime = Field(description="上传地址签名过期时间戳")
|
"""
|
||||||
|
color: str = Field(
|
||||||
|
description="商品详细颜色配色等色彩特征"
|
||||||
|
)
|
||||||
|
pattern: str = Field(
|
||||||
|
description="商品详细图案纹理布料等材质特征, 材料可以根据产品名称和图像确定"
|
||||||
|
)
|
||||||
|
style: str = Field(
|
||||||
|
default=None,
|
||||||
|
description="商品详细款式风格设计等有辨识度的款式特征"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadMultipartPresignRequest(UploadPresignRequest):
|
class ProductInfo(BaseModel):
|
||||||
parts_count: int = Field(description="分片数量")
|
"""
|
||||||
|
表示从图像中提取单个产品的信息
|
||||||
|
"""
|
||||||
|
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):
|
class GeminiFirstStageResponseModel(RootModel):
|
||||||
urls: List[str] = Field(description="就近加速的PUT分片上传地址")
|
"""
|
||||||
list_url: str = Field(description="用于确认分片上传状态的请求地址")
|
从图像中提取的产品信息列表
|
||||||
complete_url: str = Field(description="用于确认完成分片上传的请求地址")
|
"""
|
||||||
urn: str = Field(description="上传成功后获得的对应资源URN")
|
root: List[ProductInfo]
|
||||||
expired_at: datetime = Field(description="上传地址签名过期时间戳")
|
|
||||||
|
|
||||||
|
|
||||||
class MediaCopyRequest(BaseModel):
|
class ProductTimeline(BaseModel):
|
||||||
class MediaCopyTask(BaseModel):
|
"""
|
||||||
source: MediaSource = Field(description="源媒体")
|
表示单个商品及其在视频时间线中的出现信息。
|
||||||
destination: MediaSource = Field(description="")
|
"""
|
||||||
|
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
|
import uuid
|
||||||
from datetime import datetime
|
from typing import List, Union, Optional, Dict
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Union, Optional, Dict, Any
|
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
from pydantic import BaseModel, Field, field_validator, ConfigDict, HttpUrl, computed_field, RootModel, root_validator, \
|
from pydantic import BaseModel, Field, ConfigDict, field_validator, ValidationError, model_validator, computed_field, \
|
||||||
model_validator
|
EmailStr
|
||||||
|
|
||||||
from .ffmpeg_worker_model import FFMpegSliceSegment
|
from ..cache_tasks.models import CacheTask, MediaSource, Base64File
|
||||||
from .media_model import MediaSource, MediaSources, MediaProtocol
|
from ..enums.models import MediaProtocol
|
||||||
from ..config import WorkerConfig
|
from ..ffmpeg_tasks.models import WebhookNotify, FFMpegSliceSegment, FFMPEGSliceOptions
|
||||||
from ..utils.TimeUtils import TimeDelta
|
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="执行成功")
|
|
||||||
|
|
||||||
|
|
||||||
class ClusterCacheBatchRequest(BaseModel):
|
class ClusterCacheBatchRequest(BaseModel):
|
||||||
@@ -163,48 +17,77 @@ class ClusterCacheBatchRequest(BaseModel):
|
|||||||
model_config = ConfigDict()
|
model_config = ConfigDict()
|
||||||
|
|
||||||
|
|
||||||
class ClusterCacheBatchResponse(BaseModel):
|
class MediaSourcesRequest(BaseModel):
|
||||||
results: List[CacheTaskResult] = Field(description="批量操作任务结果")
|
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):
|
class BaseFFMPEGTaskRequest(BaseModel):
|
||||||
webhook: Optional[WebhookNotify] = Field(description="Task webhook", default=None)
|
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):
|
class FFMPEGSliceRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="待切割的媒体源")
|
media: MediaSource = Field(description="待切割的媒体源")
|
||||||
markers: List[FFMpegSliceSegment] = Field(description="按照时间顺序排序过的切割标记数组")
|
markers: List[FFMpegSliceSegment] = Field(description="按照时间顺序排序过的切割标记数组")
|
||||||
@@ -221,16 +104,8 @@ class FFMPEGSliceRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGSliceTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[List[str]] = Field(default=None, description="任务运行结果")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGConcatRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGConcatRequest(BaseFFMPEGTaskRequest):
|
||||||
medias: MediaSources = Field(description="待合并的媒体源")
|
medias: MediaSourcesRequest = Field(description="待合并的媒体源")
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGConcatTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="任务运行结果")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGExtractAudioRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGExtractAudioRequest(BaseFFMPEGTaskRequest):
|
||||||
@@ -247,10 +122,6 @@ class FFMPEGExtractAudioRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGExtractAudioTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="任务运行结果")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGCornerMirrorRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGCornerMirrorRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
|
|
||||||
@@ -265,10 +136,6 @@ class FFMPEGCornerMirrorRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGCornerMirrorTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="任务运行结果")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGBgmRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGBgmRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
bgm_media: MediaSource = Field(description="添加的BGM媒体源", alias="bgmMedia")
|
bgm_media: MediaSource = Field(description="添加的BGM媒体源", alias="bgmMedia")
|
||||||
@@ -296,10 +163,6 @@ class FFMPEGBgmRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGBgmTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGZoomLoopRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGZoomLoopRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
duration: float = Field(description="放大缩小一个循环的持续时间秒数", default=6.0)
|
duration: float = Field(description="放大缩小一个循环的持续时间秒数", default=6.0)
|
||||||
@@ -316,10 +179,6 @@ class FFMPEGZoomLoopRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGZoomLoopTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGOverlayGifRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGOverlayGifRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
gif: MediaSource = Field(description="叠加的特效gif")
|
gif: MediaSource = Field(description="叠加的特效gif")
|
||||||
@@ -348,10 +207,6 @@ class FFMPEGOverlayGifRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGOverlayGifTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
subtitle: Optional[MediaSource] = Field(default=None, description="需要叠加的字幕文件")
|
subtitle: Optional[MediaSource] = Field(default=None, description="需要叠加的字幕文件")
|
||||||
@@ -422,10 +277,6 @@ class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGSubtitleTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
bgm: MediaSource = Field(description="需要添加的BGM媒体源")
|
bgm: MediaSource = Field(description="需要添加的BGM媒体源")
|
||||||
@@ -467,19 +318,11 @@ class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGVideoLoopFillAudioRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGVideoLoopFillAudioRequest(BaseFFMPEGTaskRequest):
|
||||||
video: MediaSource = Field(description="用来填充的视频素材")
|
video: MediaSource = Field(description="用来填充的视频素材")
|
||||||
audio: MediaSource = Field(description="被填充的音频素材")
|
audio: MediaSource = Field(description="被填充的音频素材")
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGVideoLoopFillAudioResponse(BaseFFMPEGTaskStatusResponse):
|
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGExtractFrameRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGExtractFrameRequest(BaseFFMPEGTaskRequest):
|
||||||
video: MediaSource = Field(description="提取帧画面的来源")
|
video: MediaSource = Field(description="提取帧画面的来源")
|
||||||
seek_time: Optional[Union[str, int, float]] = Field(default=None, 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):
|
class ComfyTaskRequest(BaseFFMPEGTaskRequest):
|
||||||
video_path: MediaSource = Field(
|
video_path: MediaSource = Field(
|
||||||
default=None, description="视频源")
|
default=None, description="视频源")
|
||||||
@@ -590,40 +416,12 @@ class GeminiRequest(BaseFFMPEGTaskRequest):
|
|||||||
raise pydantic.ValidationError("media格式读取失败")
|
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):
|
class MonitorLiveRoomProductRequest(BaseModel):
|
||||||
cookie: str = Field(default="YOUR_COOKIE", description="用户网页版抖音Cookie/Your web version of Douyin Cookie")
|
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")
|
room_id: str = Field(default="", description="直播间room_id/Room room_id")
|
||||||
author_id: str = Field(default="", description="作者id/Author 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):
|
class MakeGridGeminiRequest(BaseFFMPEGTaskRequest):
|
||||||
pic_info_list: List[Dict[str, str]] = Field(default=[],
|
pic_info_list: List[Dict[str, str]] = Field(default=[],
|
||||||
description="包含图片信息的字典列表,每个字典包含 \"title\" 和 \"cover\" 键")
|
description="包含图片信息的字典列表,每个字典包含 \"title\" 和 \"cover\" 键")
|
||||||
@@ -634,115 +432,25 @@ class MakeGridGeminiRequest(BaseFFMPEGTaskRequest):
|
|||||||
separator: int = Field(default=12, description="分割线宽度/像素")
|
separator: int = Field(default=12, description="分割线宽度/像素")
|
||||||
|
|
||||||
|
|
||||||
class GoogleUploadResponse(BaseModel):
|
class FFMPEGConvertStreamRequest(BaseFFMPEGTaskRequest):
|
||||||
kind: str
|
media: MediaSource = Field(description="待转换的媒体源")
|
||||||
id: str
|
options: FFMPEGSliceOptions = Field(default_factory=FFMPEGSliceOptions, description="输出质量选项")
|
||||||
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")
|
@field_validator('media', mode='before')
|
||||||
@property
|
@classmethod
|
||||||
def urn(self) -> str:
|
def parse_inputs(cls, v: Union[str, MediaSource]):
|
||||||
return ("gs://" + self.id).replace(f"/{self.generation}", "")
|
if isinstance(v, str):
|
||||||
|
media_source = MediaSource.from_str(v)
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
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):
|
class NakamaLogin(BaseModel):
|
||||||
"""
|
email: EmailStr = Field(description="Bowong Echo(Nakama)的登录账号")
|
||||||
代表产品的视觉特征
|
password: str = Field(description="Bowong Echo(Nakama)的登录密码")
|
||||||
"""
|
|
||||||
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>"
|
|
||||||
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 typing import Optional, Any
|
||||||
|
|
||||||
from pydantic import Field, HttpUrl
|
from pydantic import Field, HttpUrl
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
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_product_kv_name: str = Field(default='live-product-cache', description="Modal抖音直播间商品缓存KV库")
|
||||||
modal_environment: str = Field(default="dev", description="Modal worker运行环境")
|
modal_environment: str = Field(default="dev", description="Modal worker运行环境")
|
||||||
modal_app_name: str = Field(default='bowong-ai-video', description="Modal App集群名称")
|
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_input: Optional[str] = Field(default="comfyui-input", description="ComfyUI input S3文件夹名")
|
||||||
comfyui_s3_output: Optional[str] = Field(default="comfyui-output", description="ComfyUI output 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")
|
description="Langfuse public key")
|
||||||
langfuse_secret_key: str = Field(default="sk-lf-dd20cb0b-ef2e-49f6-80f0-b2d9cff1bb11",
|
langfuse_secret_key: str = Field(default="sk-lf-dd20cb0b-ef2e-49f6-80f0-b2d9cff1bb11",
|
||||||
description="Langfuse secret key")
|
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_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={
|
modal_config: Any = SettingsConfigDict(json_schema_extra={
|
||||||
"description": "可通过本地环境变量加载对应Field, 不区分大小写, Modal创建App的Image时可通过dotenv加载指定.env文件写入到Docker Image的系统变量",
|
"description": "可通过本地环境变量加载对应Field, 不区分大小写, Modal创建App的Image时可通过dotenv加载指定.env文件写入到Docker Image的系统变量",
|
||||||
@@ -13,22 +13,17 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
|||||||
from starlette import status
|
from starlette import status
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.config import Config
|
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 ..middleware.authorization import verify_token
|
||||||
from ..models.media_model import (MediaSources,
|
from ..models.responses.models import DownloadResult, CacheResultResponse, UploadResultResponse, UploadPresignResponse, \
|
||||||
CacheResult,
|
UploadMultipartPresignResponse, CacheDeleteTaskResponse, ClusterCacheBatchResponse, LiveRoomProductCachesResponse
|
||||||
MediaSource,
|
from ..models.enums.models import MediaCacheStatus, CacheOperationType
|
||||||
MediaCacheStatus,
|
from ..models.ffmpeg_tasks.models import SentryTransactionInfo
|
||||||
DownloadResult,
|
from ..models.requests.models import ClusterCacheBatchRequest, MediaSourcesRequest, UploadBase64Request, \
|
||||||
UploadResultResponse,
|
UploadPresignRequest, \
|
||||||
UploadBase64Request, UploadPresignRequest, UploadPresignResponse,
|
UploadMultipartPresignRequest, MonitorLiveRoomProductRequest
|
||||||
UploadMultipartPresignRequest, UploadMultipartPresignResponse, MediaProtocol
|
from ..models.cache_tasks.models import CacheTaskResult, MediaSource
|
||||||
)
|
|
||||||
from ..models.web_model import SentryTransactionInfo, MonitorLiveRoomProductRequest, ModalTaskResponse, \
|
|
||||||
LiveRoomProductCachesResponse, CacheDeleteTaskResponse, ClusterCacheBatchRequest, CacheOperationType, \
|
|
||||||
ClusterCacheBatchResponse, CacheTaskResult
|
|
||||||
from ..utils.KVCache import MediaSourceKVCache, LiveProductKVCache
|
from ..utils.KVCache import MediaSourceKVCache, LiveProductKVCache
|
||||||
from ..utils.SentryUtils import SentryUtils
|
from ..utils.SentryUtils import SentryUtils
|
||||||
|
|
||||||
@@ -57,9 +52,9 @@ modal_kv_product_cache = LiveProductKVCache(kv_name=config.modal_product_kv_name
|
|||||||
summary="缓存视频文件",
|
summary="缓存视频文件",
|
||||||
description="异步缓存视频文件到S3存储桶和Modal Dict(KV)",
|
description="异步缓存视频文件到S3存储桶和Modal Dict(KV)",
|
||||||
dependencies=[Depends(verify_token)])
|
dependencies=[Depends(verify_token)])
|
||||||
async def cache(medias: MediaSources) -> CacheResult:
|
async def cache(medias: MediaSourcesRequest) -> CacheResultResponse:
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
caches: MediaSources
|
caches: MediaSourcesRequest
|
||||||
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
||||||
x_baggage=sentry_sdk.get_baggage())
|
x_baggage=sentry_sdk.get_baggage())
|
||||||
|
|
||||||
@@ -123,14 +118,14 @@ async def cache(medias: MediaSources) -> CacheResult:
|
|||||||
cache_task_result_list.append(result)
|
cache_task_result_list.append(result)
|
||||||
|
|
||||||
modal_kv_cache.batch_update_cloudflare_kv(cache_task_result_dict)
|
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("/",
|
@router.delete("/",
|
||||||
summary="清除指定的所有缓存",
|
summary="清除指定的所有缓存",
|
||||||
description="清除指定的所有缓存(包括KV记录和S3存储文件)",
|
description="清除指定的所有缓存(包括KV记录和S3存储文件)",
|
||||||
dependencies=[Depends(verify_token)])
|
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_id = current_function_call_id()
|
||||||
fn = modal.Function.from_name(config.modal_app_name, "cache_delete", environment_name=config.modal_environment)
|
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="获取已缓存的视频下载地址",
|
description="获取已缓存的视频下载地址",
|
||||||
dependencies=[Depends(verify_token)])
|
dependencies=[Depends(verify_token)])
|
||||||
@sentry_sdk.trace
|
@sentry_sdk.trace
|
||||||
async def download_caches(medias: MediaSources) -> DownloadResult:
|
async def download_caches(medias: MediaSourcesRequest) -> DownloadResult:
|
||||||
cdn_endpoint = config.S3_cdn_endpoint
|
cdn_endpoint = config.S3_cdn_endpoint
|
||||||
urls = []
|
urls = []
|
||||||
for media in medias.inputs:
|
for media in medias.inputs:
|
||||||
@@ -218,7 +213,7 @@ async def purge_kv_all():
|
|||||||
summary="删除对应的KV记录",
|
summary="删除对应的KV记录",
|
||||||
description="删除请求中对应的视频缓存记录",
|
description="删除请求中对应的视频缓存记录",
|
||||||
dependencies=[Depends(verify_token)])
|
dependencies=[Depends(verify_token)])
|
||||||
async def purge_kv(medias: MediaSources):
|
async def purge_kv(medias: MediaSourcesRequest):
|
||||||
try:
|
try:
|
||||||
for media in medias.inputs:
|
for media in medias.inputs:
|
||||||
modal_kv_cache.pop(media.urn)
|
modal_kv_cache.pop(media.urn)
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ from fastapi import APIRouter, Depends, Header
|
|||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from ..config import WorkerConfig
|
from ..models.settings.cluster import WorkerConfig
|
||||||
from ..middleware.authorization import verify_token
|
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 ..utils.ModalUtils import ModalUtils
|
||||||
from ..models.web_model import (SentryTransactionHeader,
|
from ..models.responses.models import ModalTaskResponse, ComfyTaskStatusResponse
|
||||||
ModalTaskResponse, SentryTransactionInfo,
|
from ..models.requests.models import ComfyTaskRequest
|
||||||
ComfyTaskRequest, ComfyTaskStatusResponse, TaskStatus)
|
from ..models.ffmpeg_tasks.models import SentryTransactionHeader, SentryTransactionInfo
|
||||||
|
from ..models.enums.models import TaskStatus
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,15 @@ from fastapi.responses import Response
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from ..config import WorkerConfig
|
from ..models.settings.cluster import WorkerConfig
|
||||||
from ..middleware.authorization import verify_token
|
from ..middleware.authorization import verify_token
|
||||||
from ..utils.ModalUtils import ModalUtils
|
from ..utils.ModalUtils import ModalUtils
|
||||||
from ..models.web_model import (FFMPEGSliceRequest, SentryTransactionHeader,
|
from ..models.responses.models import ModalTaskResponse, RecordingTaskResponse, BaseFFMPEGTaskStatusResponse
|
||||||
ModalTaskResponse, SentryTransactionInfo,
|
from ..models.requests.models import FFMPEGSliceRequest, FFMPEGConcatRequest, FFMPEGExtractAudioRequest, \
|
||||||
FFMPEGConcatRequest,
|
FFMPEGCornerMirrorRequest, FFMPEGZoomLoopRequest, FFMPEGOverlayGifRequest, FFMPEGSubtitleOverlayRequest, \
|
||||||
FFMPEGExtractAudioRequest,
|
FFMPEGMixBgmWithNoiseReduceRequest, FFMPEGVideoLoopFillAudioRequest, FFMPEGExtractFrameRequest, \
|
||||||
FFMPEGCornerMirrorRequest,
|
FFMPEGStreamRecordRequest, FFMPEGConvertStreamRequest
|
||||||
FFMPEGOverlayGifRequest,
|
from ..models.ffmpeg_tasks.models import SentryTransactionHeader, SentryTransactionInfo
|
||||||
FFMPEGZoomLoopRequest,
|
|
||||||
FFMPEGSubtitleOverlayRequest,
|
|
||||||
FFMPEGMixBgmWithNoiseReduceRequest,
|
|
||||||
FFMPEGVideoLoopFillAudioRequest,
|
|
||||||
FFMPEGConvertStreamRequest,
|
|
||||||
FFMPEGExtractFrameRequest,
|
|
||||||
BaseFFMPEGTaskStatusResponse,
|
|
||||||
FFMPEGStreamRecordRequest,
|
|
||||||
RecordingTaskResponse)
|
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ from pydantic import computed_field
|
|||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from ..models.settings.cluster import WorkerConfig
|
||||||
from BowongModalFunctions.middleware.authorization import verify_token
|
from ..middleware.authorization import verify_token
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, GeminiResultResponse, GeminiRequest, \
|
from ..models.responses.models import ModalTaskResponse, GeminiResultResponse, GoogleUploadResponse
|
||||||
ModalTaskResponse, MakeGridGeminiRequest, GoogleUploadResponse
|
from ..models.requests.models import GeminiRequest, MakeGridGeminiRequest
|
||||||
from BowongModalFunctions.utils.ModalUtils import ModalUtils
|
from ..models.ffmpeg_tasks.models import SentryTransactionInfo
|
||||||
from BowongModalFunctions.utils.HTTPUtils import GoogleAuthUtils
|
from ..utils.ModalUtils import ModalUtils
|
||||||
|
from ..utils.HTTPUtils import GoogleAuthUtils
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
@@ -191,10 +192,7 @@ async def make_grid_gemini_upload(data: MakeGridGeminiRequest, headers: Annotate
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/inference_gemini', summary="使用Gemini推理hls视频流指定时间段的打点情况")
|
@router.post('/inference_gemini', summary="使用Gemini推理hls视频流指定时间段的打点情况")
|
||||||
async def inference_gemini(
|
async def inference_gemini(data: GeminiRequest, headers: Annotated[BundleHeaders, Header()], ) -> ModalTaskResponse:
|
||||||
data: GeminiRequest,
|
|
||||||
headers: Annotated[BundleHeaders, Header()],
|
|
||||||
) -> ModalTaskResponse:
|
|
||||||
google_api_key = headers.x_google_api_key or os.environ.get("GOOGLE_API_KEY")
|
google_api_key = headers.x_google_api_key or os.environ.get("GOOGLE_API_KEY")
|
||||||
if not google_api_key:
|
if not google_api_key:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing Google API Key")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing Google API Key")
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import modal
|
import modal
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from starlette import status
|
||||||
|
|
||||||
from ..middleware.authorization import verify_token
|
from ..middleware.authorization import verify_token, nakama_login
|
||||||
from ..models.web_model import ModalTaskCancelResponse
|
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:
|
async def task_cancel(task_id: str) -> ModalTaskCancelResponse:
|
||||||
try:
|
try:
|
||||||
fn_call = modal.FunctionCall.from_id(task_id)
|
fn_call = modal.FunctionCall.from_id(task_id)
|
||||||
@@ -18,3 +22,12 @@ async def task_cancel(task_id: str) -> ModalTaskCancelResponse:
|
|||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return ModalTaskCancelResponse(success=False,
|
return ModalTaskCancelResponse(success=False,
|
||||||
error=e.message if hasattr(e, 'message') else str(e))
|
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
|
import aiofiles
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pydantic import BaseModel, computed_field, Field, field_validator
|
from pydantic import BaseModel, computed_field, Field, field_validator
|
||||||
from BowongModalFunctions.models.web_model import GoogleUploadResponse
|
from ..models.responses.models import GoogleUploadResponse
|
||||||
|
|
||||||
|
|
||||||
class HTTPDownloadUtils:
|
class HTTPDownloadUtils:
|
||||||
@@ -192,7 +192,8 @@ class GoogleAuthUtils:
|
|||||||
return GoogleAuthUtils.GoogleAuthResponse.model_validate_json(response.text)
|
return GoogleAuthUtils.GoogleAuthResponse.model_validate_json(response.text)
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
filename: str) -> GoogleUploadResponse:
|
||||||
safe_filename = quote(filename)
|
safe_filename = quote(filename)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import httpx
|
|||||||
import modal
|
import modal
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from .VideoUtils import VideoUtils
|
from .VideoUtils import VideoUtils
|
||||||
from ..models.media_model import MediaSource
|
from ..models.cache_tasks.models import MediaSource, LiveProductCaches
|
||||||
from ..models.web_model import LiveProductCaches
|
|
||||||
|
|
||||||
|
|
||||||
class KVCache:
|
class KVCache:
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from typing import Tuple, Optional, Any, List
|
from typing import Optional, Any, List
|
||||||
|
|
||||||
import modal
|
import modal
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from modal.call_graph import InputStatus
|
from modal.call_graph import InputStatus, InputInfo
|
||||||
from pydantic import BaseModel, Field
|
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):
|
class ModalTaskInfo(BaseModel):
|
||||||
@@ -25,16 +26,16 @@ class ModalUtils:
|
|||||||
:param task_id: modal 任务id
|
:param task_id: modal 任务id
|
||||||
:return: (TaskStaus 任务运行状态, function_name 任务名字, errorCode 错误代码, errorReason 错误原因, results 任务结果, sentryTransactionInfo Sentry跟踪信息)
|
:return: (TaskStaus 任务运行状态, function_name 任务名字, errorCode 错误代码, errorReason 错误原因, results 任务结果, sentryTransactionInfo Sentry跟踪信息)
|
||||||
"""
|
"""
|
||||||
task = None
|
task: Optional[InputInfo] = None
|
||||||
try:
|
try:
|
||||||
fn_task = modal.FunctionCall.from_id(task_id)
|
fn_task = modal.FunctionCall.from_id(task_id)
|
||||||
|
|
||||||
call_graph = fn_task.get_call_graph()
|
call_graph = fn_task.get_call_graph()
|
||||||
root = call_graph[0]
|
root = call_graph[0]
|
||||||
if len(root.children) == 0:
|
if len(root.children) == 0:
|
||||||
return ModalTaskInfo(function_name="unknown", error_code=ErrorCode.NOT_FOUND,
|
return ModalTaskInfo(function_name="unknown", error_code=ErrorCode.NOT_FOUND,
|
||||||
status=TaskStatus.expired, error_reason="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:
|
if not task.function_call_id == task_id:
|
||||||
return ModalTaskInfo(function_name=task.function_name, status=TaskStatus.expired,
|
return ModalTaskInfo(function_name=task.function_name, status=TaskStatus.expired,
|
||||||
error_code=ErrorCode.NOT_FOUND, error_reason="NOT_FOUND")
|
error_code=ErrorCode.NOT_FOUND, error_reason="NOT_FOUND")
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import sentry_sdk
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from BowongModalFunctions.models.web_model import WebhookNotify, WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, \
|
from ..models.responses.models import BaseFFMPEGTaskStatusResponse
|
||||||
TaskStatus, ErrorCode, FFMPEGResult
|
from ..models.ffmpeg_tasks.models import WebhookNotify, FFMPEGResult
|
||||||
|
from ..models.enums.models import TaskStatus, ErrorCode, WebhookMethodEnum
|
||||||
|
|
||||||
|
|
||||||
class SentryUtils:
|
class SentryUtils:
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
class TimeDelta(timedelta):
|
class TimeDelta(timedelta):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -23,38 +21,38 @@ def parse_time(time_str):
|
|||||||
"""解析时间字符串为datetime对象"""
|
"""解析时间字符串为datetime对象"""
|
||||||
|
|
||||||
parsed_time = None
|
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
|
# 先尝试完整格式 HH:MM:SS.fff
|
||||||
parsed_time = datetime.strptime(time_str, '%H:%M:%S.%f')
|
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 格式
|
# 如果失败,尝试 HH:MM:SS:fff 格式
|
||||||
parsed_time = datetime.strptime(time_str, '%H:%M:%S:%f')
|
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 格式
|
# 如果失败,尝试 MM:SS.fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M:%S.%f')
|
dt = datetime.strptime(time_str, '%M:%S.%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=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 格式
|
# 如果失败,尝试 MM.SS:fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M.%S:%f')
|
dt = datetime.strptime(time_str, '%M.%S:%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=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 格式
|
# 如果失败,尝试 MM.SS.fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M.%S.%f')
|
dt = datetime.strptime(time_str, '%M.%S.%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=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 格式
|
# 如果失败,尝试 MM:SS:fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M:%S:%f')
|
dt = datetime.strptime(time_str, '%M:%S:%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=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 格式
|
# 如果失败,尝试 MM:SS:fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M:%S')
|
dt = datetime.strptime(time_str, '%M:%S')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=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 格式
|
# 如果失败,尝试 MM:SS:fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M.%S')
|
dt = datetime.strptime(time_str, '%M.%S')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为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)
|
hours, remainder = divmod(total_seconds, 3600)
|
||||||
minutes, seconds = divmod(remainder, 60)
|
minutes, seconds = divmod(remainder, 60)
|
||||||
microseconds = duration.microseconds
|
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 = {}
|
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, end = parse_timeline_item(item)
|
||||||
# 比较起始时间与时间差
|
# 比较起始时间与时间差
|
||||||
start_str = format_time(start)
|
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)
|
new_start_str = convert_time(start_str)
|
||||||
else:
|
else:
|
||||||
new_start_str = start_str
|
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
|
new_start_str = max_time_str
|
||||||
|
|
||||||
end_str = format_time(end)
|
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)
|
new_end_str = convert_time(end_str)
|
||||||
else:
|
else:
|
||||||
new_end_str = end_str
|
new_end_str = end_str
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from typing import Union, List, Tuple, Optional, Dict, Any
|
from typing import List, Tuple, Optional, Any
|
||||||
|
|
||||||
import m3u8
|
import m3u8
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -14,7 +14,6 @@ import math
|
|||||||
from aiohttp import ClientTimeout
|
from aiohttp import ClientTimeout
|
||||||
|
|
||||||
from .TimeUtils import TimeDelta
|
from .TimeUtils import TimeDelta
|
||||||
from pydantic import BaseModel, ConfigDict, computed_field, Field, field_validator
|
|
||||||
from ffmpeg import FFmpeg
|
from ffmpeg import FFmpeg
|
||||||
from ffmpeg.asyncio import FFmpeg as AsyncFFmpeg
|
from ffmpeg.asyncio import FFmpeg as AsyncFFmpeg
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
@@ -35,181 +34,7 @@ from pedalboard.io import AudioFile
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from .PathUtils import FileUtils
|
from .PathUtils import FileUtils
|
||||||
from ..models.ffmpeg_worker_model import FFMpegSliceSegment
|
from ..models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, VideoStream, VideoMetadata
|
||||||
|
|
||||||
|
|
||||||
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 VideoUtils:
|
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
|
import modal
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.models.settings.cluster import WorkerConfig
|
||||||
from .video import app as media_app
|
from .video import app as media_app
|
||||||
from .web import app as web_app
|
from .web import app as web_app
|
||||||
from .ffmpeg_app import app as ffmpeg_app
|
from .ffmpeg_app import app as ffmpeg_app
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ with comfyui_image.imports():
|
|||||||
from typing import Tuple, Any, Dict, Optional
|
from typing import Tuple, Any, Dict, Optional
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from modal import current_function_call_id
|
from modal import current_function_call_id
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.models.settings.cluster import WorkerConfig
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from sentry_sdk.integrations.loguru import LoguruIntegration
|
from sentry_sdk.integrations.loguru import LoguruIntegration
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ with comfyui_latentsync_1_5_image.imports():
|
|||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
import psutil
|
import psutil
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.models.settings.cluster import WorkerConfig
|
||||||
from modal import current_function_call_id
|
from modal import current_function_call_id
|
||||||
from sentry_sdk.integrations.loguru import LoguruIntegration
|
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
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
import shutil, backoff, sentry_sdk
|
import shutil, backoff, sentry_sdk
|
||||||
from typing import List
|
from typing import List
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.models.settings.cluster import WorkerConfig
|
||||||
|
|
||||||
config = 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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSources
|
from BowongModalFunctions.models.requests.models import MediaSourcesRequest
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
@@ -26,7 +26,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
),
|
),
|
||||||
}, )
|
}, )
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_concat_medias(medias: MediaSources,
|
async def ffmpeg_concat_medias(medias: MediaSourcesRequest,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None,
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
webhook: Optional[WebhookNotify] = None) -> Tuple[
|
webhook: Optional[WebhookNotify] = None) -> Tuple[
|
||||||
FFMPEGResult, Optional[SentryTransactionInfo]]:
|
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_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage 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)
|
@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]
|
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,
|
local_output_path, metadata = await VideoUtils.ffmpeg_concat_medias(media_paths=input_videos,
|
||||||
output_path=output_filepath)
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource, MediaProtocol
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.enums.models import MediaProtocol
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
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
|
import sentry_sdk
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from modal import current_function_call_id
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource, MediaProtocol
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
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.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, \
|
||||||
from BowongModalFunctions.models.media_model import MediaSources, MediaProtocol, MediaSource
|
SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.models.web_model import 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.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils, FFMPEGSliceOptions
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from typing import Optional, Tuple, List
|
from typing import Optional, Tuple, List
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import modal
|
|||||||
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult, \
|
from BowongModalFunctions.models.responses.models import BaseFFMPEGTaskStatusResponse
|
||||||
WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, TaskStatus
|
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.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
import sentry_sdk
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
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
|
from ..ffmpeg_app import ffmpeg_worker_image, app, config, s3_mount, local_copy_to_s3, output_path_prefix
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo, WebhookNotify, FFMPEGResult
|
||||||
from BowongModalFunctions.utils.PathUtils import FileUtils
|
from BowongModalFunctions.utils.PathUtils import FileUtils
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ with downloader_image.imports():
|
|||||||
from typing import List
|
from typing import List
|
||||||
from loguru import logger
|
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.utils.KVCache import MediaSourceKVCache, LiveProductKVCache
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from ..video import downloader_image, app, config
|
|||||||
|
|
||||||
with downloader_image.imports():
|
with downloader_image.imports():
|
||||||
import os
|
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,
|
@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.vod_client import VodClient
|
||||||
from tencentcloud.vod.v20180717 import models as vod_request_models
|
from tencentcloud.vod.v20180717 import models as vod_request_models
|
||||||
|
|
||||||
from BowongModalFunctions.models.media_model import MediaSource, MediaCacheStatus, MediaProtocol
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo
|
from BowongModalFunctions.models.enums.models import MediaProtocol, MediaCacheStatus
|
||||||
|
from BowongModalFunctions.models.ffmpeg_tasks.models import SentryTransactionInfo
|
||||||
|
|
||||||
|
|
||||||
@app.function(cpu=1, timeout=1800,
|
@app.function(cpu=1, timeout=1800,
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ with downloader_image.imports():
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.cache_tasks.models import MediaSource, GeminiFirstStageResponseModel, \
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, GeminiFirstStagePromptVariables, \
|
GeminiFirstStagePromptVariables, GeminiSecondStagePromptVariables
|
||||||
GeminiSecondStagePromptVariables, GeminiFirstStageResponseModel, GeminiSecondStageResponseModel
|
from BowongModalFunctions.models.responses.models import GeminiSecondStageResponseModel
|
||||||
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions, \
|
||||||
|
SentryTransactionInfo, WebhookNotify
|
||||||
from BowongModalFunctions.utils.TimeUtils import TimeDelta, merge_product_data
|
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,
|
@app.function(cpu=(0.5, 64), timeout=1800,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ with downloader_image.imports():
|
|||||||
from modal import current_function_call_id
|
from modal import current_function_call_id
|
||||||
|
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
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),
|
@app.function(cpu=(0.5, 16),
|
||||||
max_containers=config.video_downloader_concurrency,
|
max_containers=config.video_downloader_concurrency,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ with downloader_image.imports():
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from httpx import Timeout
|
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)
|
@app.function(max_containers=config.video_downloader_concurrency, timeout=130)
|
||||||
@modal.concurrent(max_inputs=50)
|
@modal.concurrent(max_inputs=50)
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ app = modal.App(
|
|||||||
include_source=False)
|
include_source=False)
|
||||||
|
|
||||||
with fastapi_image.imports():
|
with fastapi_image.imports():
|
||||||
from BowongModalFunctions.api import web_app, web_app_tikhub
|
from BowongModalFunctions.api import web_app
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.models.settings.cluster import WorkerConfig
|
||||||
|
from Douyin_TikTok_Download_API.tikhub_api import web_app_tikhub
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
@@ -44,11 +45,10 @@ with fastapi_image.imports():
|
|||||||
def fastapi_webapp():
|
def fastapi_webapp():
|
||||||
return web_app
|
return web_app
|
||||||
|
|
||||||
|
|
||||||
# 短保持时间保证IP不停更换
|
# 短保持时间保证IP不停更换
|
||||||
@app.function(scaledown_window=2,
|
@app.function(scaledown_window=2,
|
||||||
secrets=[
|
secrets=[modal.Secret.from_name("cf-kv-secret"), ],
|
||||||
modal.Secret.from_name("cf-kv-secret"),
|
|
||||||
],
|
|
||||||
cloud="aws",
|
cloud="aws",
|
||||||
volumes={
|
volumes={
|
||||||
config.S3_mount_dir: modal.CloudBucketMount(
|
config.S3_mount_dir: modal.CloudBucketMount(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from typing import List, Optional
|
|||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
from BowongModalFunctions.models.ffmpeg_tasks.models import FFMpegSliceSegment, FFMPEGSliceOptions
|
||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils, FFMPEGSliceOptions
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGTestCase(unittest.IsolatedAsyncioTestCase):
|
class FFMPEGTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import unittest
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from BowongModalFunctions.models.web_model import FFMPEGSubtitleOverlayRequest
|
from BowongModalFunctions.models.requests.models import FFMPEGSubtitleOverlayRequest
|
||||||
|
|
||||||
|
|
||||||
class PydanticModelTestCase(unittest.TestCase):
|
class PydanticModelTestCase(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user