761 lines
36 KiB
Python
761 lines
36 KiB
Python
import os
|
||
import modal
|
||
import httpx
|
||
import asyncio
|
||
from typing import List, Annotated, Tuple, Any, Optional
|
||
from modal import current_function_call_id
|
||
from modal.call_graph import InputStatus
|
||
from loguru import logger
|
||
from fastapi import FastAPI, Response, Depends, Header, Request
|
||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||
from fastapi.responses import JSONResponse, RedirectResponse
|
||
from fastapi.exceptions import HTTPException
|
||
from starlette import status
|
||
from scalar_fastapi import get_scalar_api_reference
|
||
import sentry_sdk
|
||
from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels
|
||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||
from .utils.KVCache import KVCache
|
||
from .utils.SentryUtils import SentryUtils
|
||
from .models.media_model import (MediaSource,
|
||
MediaSources,
|
||
MediaCacheStatus,
|
||
CacheResult,
|
||
DownloadResult)
|
||
from .models.web_model import (TaskStatus,
|
||
ErrorCode,
|
||
SentryTransactionInfo,
|
||
SentryTransactionHeader,
|
||
ModalTaskResponse,
|
||
FFMPEGSliceRequest,
|
||
FFMPEGConcatRequest,
|
||
FFMPEGExtractAudioRequest,
|
||
FFMPEGCornerMirrorRequest,
|
||
FFMPEGOverlayGifRequest,
|
||
FFMPEGZoomLoopRequest,
|
||
FFMPEGSubtitleOverlayRequest,
|
||
FFMPEGMixBgmWithNoiseReduceRequest,
|
||
FFMPEGSliceTaskStatusResponse,
|
||
FFMPEGConcatTaskStatusResponse,
|
||
FFMPEGExtractAudioTaskStatusResponse,
|
||
FFMPEGCornerMirrorTaskStatusResponse,
|
||
FFMPEGOverlayGifTaskStatusResponse,
|
||
FFMPEGSubtitleTaskStatusResponse,
|
||
FFMPEGZoomLoopTaskStatusResponse,
|
||
FFMPEGMixBgmWithNoiseReduceStatusResponse,
|
||
ComfyTaskRequest,
|
||
ComfyTaskStatusResponse
|
||
)
|
||
from .config import WorkerConfig
|
||
|
||
bearer_scheme = HTTPBearer()
|
||
|
||
config = WorkerConfig()
|
||
|
||
web_app = FastAPI(title="Modal worker API",
|
||
summary="Modal Worker的API, 包括缓存视频, 发起生产任务等",
|
||
servers=[
|
||
{'url': f'https://bowongai-dev--{config.modal_app_name}-fastapi-webapp.modal.run',
|
||
'description': 'modal 开发环境服务'},
|
||
{'url': 'https://modal-dev.bowong.cc',
|
||
'description': 'modal 开发环境服务独立域名'},
|
||
{'url': f'https://bowongai-test--{config.modal_app_name}-fastapi-webapp.modal.run',
|
||
'description': 'modal 测试环境服务'},
|
||
{'url': f'https://bowongai-main--{config.modal_app_name}-fastapi-webapp.modal.run',
|
||
'description': 'modal 生产环境服务'
|
||
}
|
||
])
|
||
|
||
|
||
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
|
||
token = credentials.credentials
|
||
# 在这里实现你的token验证逻辑
|
||
if not is_valid_token(token):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Invalid authentication credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
return token
|
||
|
||
|
||
def is_valid_token(token: str) -> bool:
|
||
# 这里实现具体的token验证逻辑
|
||
# 例如:验证JWT token,检查数据库中的token等
|
||
return token == "bowong7777"
|
||
|
||
|
||
sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2",
|
||
send_default_pii=True,
|
||
traces_sample_rate=1.0,
|
||
profiles_sample_rate=1.0,
|
||
add_full_stack=True,
|
||
environment=config.modal_environment,
|
||
integrations=[
|
||
LoguruIntegration(level=LoggingLevels.INFO.value, event_level=LoggingLevels.ERROR.value),
|
||
FastApiIntegration()
|
||
]
|
||
)
|
||
modal_kv_cache = KVCache(kv_name=config.modal_kv_name, environment=config.modal_environment)
|
||
cf_account_id = os.environ.get("CF_ACCOUNT_ID")
|
||
cf_kv_api_token = os.environ.get("CF_KV_API_TOKEN")
|
||
cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID")
|
||
|
||
sentry_header_schema = {
|
||
"x-trace-id": {
|
||
"description": "Sentry Transaction ID",
|
||
"schema": {
|
||
"type": "string",
|
||
}
|
||
},
|
||
"x-baggage": {
|
||
"description": "Sentry Transaction baggage",
|
||
"schema": {
|
||
"type": "string",
|
||
}
|
||
}
|
||
}
|
||
|
||
ALIAS_MAP = {
|
||
"/comfyui": "/comfyui/v2",
|
||
}
|
||
|
||
|
||
@web_app.middleware("http")
|
||
@web_app.middleware("https")
|
||
async def alias_middleware(request: Request, call_next):
|
||
if request.url.path in ALIAS_MAP.keys():
|
||
request.scope["path"] = ALIAS_MAP[request.url.path]
|
||
return await call_next(request)
|
||
|
||
|
||
@sentry_sdk.trace
|
||
def batch_update_cloudflare_kv(caches: List[MediaSource]):
|
||
with httpx.Client() as client:
|
||
try:
|
||
response = client.put(
|
||
f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk",
|
||
headers={"Authorization": f"Bearer {cf_kv_api_token}"},
|
||
json=[
|
||
{
|
||
"based64": False,
|
||
"key": cache.urn,
|
||
"value": cache.model_dump_json(),
|
||
}
|
||
for cache in caches
|
||
]
|
||
)
|
||
response.raise_for_status()
|
||
except httpx.RequestError as e:
|
||
logger.error(f"An error occurred while put kv to cloudflare")
|
||
raise e
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}")
|
||
raise e
|
||
except Exception as e:
|
||
logger.error(f"An unexpected error occurred: {str(e)}")
|
||
raise e
|
||
|
||
|
||
@sentry_sdk.trace
|
||
def batch_remove_cloudflare_kv(keys: List[str]):
|
||
with httpx.Client() as client:
|
||
try:
|
||
response = client.post(
|
||
f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk/delete",
|
||
headers={"Authorization": f"Bearer {cf_kv_api_token}"},
|
||
json=keys
|
||
)
|
||
response.raise_for_status()
|
||
except httpx.RequestError as e:
|
||
logger.error(f"An error occurred while put kv to cloudflare")
|
||
raise e
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}")
|
||
raise e
|
||
except Exception as e:
|
||
logger.error(f"An unexpected error occurred: {str(e)}")
|
||
raise e
|
||
|
||
|
||
@sentry_sdk.trace
|
||
async def get_modal_task_status(task_id: str) -> Tuple[
|
||
TaskStatus, Optional[int], Optional[str], Optional[Any], Optional[SentryTransactionInfo]]:
|
||
"""
|
||
使用modal任务id查看任务运行状态和结果
|
||
:param task_id: modal 任务id
|
||
:return: (TaskStaus 任务运行状态, errorCode 错误代码, errorReason 错误原因, results 任务结果, sentryTransactionInfo Sentry跟踪信息)
|
||
"""
|
||
try:
|
||
fn_task = modal.FunctionCall.from_id(task_id)
|
||
call_graph = fn_task.get_call_graph()
|
||
root = call_graph[0]
|
||
if len(root.children) == 0:
|
||
return TaskStatus.expired, ErrorCode.NOT_FOUND.value, "NOT_FOUND", None, None
|
||
task = root.children[0]
|
||
if not task.function_call_id == task_id:
|
||
return TaskStatus.expired, ErrorCode.NOT_FOUND.value, "NOT_FOUND", None, None
|
||
logger.info(task)
|
||
logger.info(f"Task function call status: {task.status}")
|
||
match task.status:
|
||
case InputStatus.PENDING:
|
||
return TaskStatus.running, None, None, None, None
|
||
case InputStatus.SUCCESS:
|
||
try:
|
||
results, sentry_trace = fn_task.get(timeout=2)
|
||
return TaskStatus.success, ErrorCode.SUCCESS.value, None, results, sentry_trace
|
||
except modal.exception.OutputExpiredError:
|
||
return TaskStatus.expired, ErrorCode.NOT_FOUND.value, "EXPIRED", None, None
|
||
except TimeoutError:
|
||
return TaskStatus.running, None, None, None, None
|
||
except Exception as e:
|
||
logger.exception(e)
|
||
return TaskStatus.failed, ErrorCode.SYSTEM_ERROR.value, 'FAILURE', None, None
|
||
case _:
|
||
error = 'FAILURE'
|
||
match task.status:
|
||
case InputStatus.INIT_FAILURE:
|
||
error = 'INIT_FAILURE'
|
||
case InputStatus.TERMINATED:
|
||
error = 'TERMINATED'
|
||
case InputStatus.TIMEOUT:
|
||
error = 'TIMEOUT'
|
||
return TaskStatus.failed, ErrorCode.SYSTEM_ERROR.value, error, None, None
|
||
except Exception as e:
|
||
logger.exception(e)
|
||
return TaskStatus.failed, ErrorCode.NOT_FOUND.value, "NOT_FOUND", None, None
|
||
|
||
|
||
@web_app.get("/scalar", include_in_schema=False)
|
||
async def scalar():
|
||
return get_scalar_api_reference(openapi_url=web_app.openapi_schema, title="Modal worker web endpoint")
|
||
|
||
|
||
@web_app.post("/cache",
|
||
tags=["缓存"],
|
||
summary="缓存视频文件",
|
||
description="异步缓存视频文件到S3存储桶和Modal Dict(KV)",
|
||
dependencies=[Depends(verify_token)])
|
||
@sentry_sdk.trace
|
||
async def cache(medias: MediaSources) -> CacheResult:
|
||
fn_id = current_function_call_id()
|
||
caches: MediaSources
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
||
x_baggage=sentry_sdk.get_baggage())
|
||
|
||
@SentryUtils.sentry_tracker(name="同步视频缓存", op="cache.get", fn_id=fn_id,
|
||
sentry_trace_id=None, sentry_baggage=None)
|
||
async def cache_handler(media: MediaSource):
|
||
cache_span = sentry_sdk.get_current_span()
|
||
cache_span.set_data("runner_id", fn_id)
|
||
cache_span.set_data("cache.key", [media.urn])
|
||
cached_media = modal_kv_cache.get_cache(media.urn)
|
||
cache_hit: bool = False
|
||
if not cached_media:
|
||
# start new download task
|
||
with cache_span.start_child(name="视频缓存任务入队",
|
||
op="queue.publish") as queue_publish_span:
|
||
fn = modal.Function.from_name(config.modal_app_name, 'cache_submit')
|
||
fn_task = fn.spawn(media, sentry_trace)
|
||
queue_publish_span.set_data("cache.key", media.urn)
|
||
queue_publish_span.set_data("messaging.message.id", fn_task.object_id)
|
||
queue_publish_span.set_data("messaging.destination.name",
|
||
"video-downloader.cache_submit")
|
||
queue_publish_span.set_data("messaging.message.body.size", 0)
|
||
# video_cache = MediaCache(status=MediaCacheStatus.downloading,
|
||
# downloader_id=fn_task.object_id)
|
||
media.status = MediaCacheStatus.downloading
|
||
media.downloader_id = fn_task.object_id
|
||
# video_cache_status_json = video_cache.model_dump_json()
|
||
modal_kv_cache.set_cache(media)
|
||
else:
|
||
media = cached_media
|
||
# video_cache = MediaCache.model_validate_json(video_cache_status_json)
|
||
match media.status:
|
||
case MediaCacheStatus.ready:
|
||
cache_hit = True
|
||
case MediaCacheStatus.downloading: # 下载任务已经在进行
|
||
cache_hit = True
|
||
case _:
|
||
# start new download task
|
||
with cache_span.start_child(name="视频缓存任务入队",
|
||
op="queue.publish") as queue_publish_span:
|
||
fn = modal.Function.from_name(config.modal_app_name, 'cache_submit')
|
||
fn_task = fn.spawn(media, sentry_trace)
|
||
queue_publish_span.set_data("cache.key", media.urn)
|
||
queue_publish_span.set_data("messaging.message.id", fn_task.object_id)
|
||
queue_publish_span.set_data("messaging.destination.name",
|
||
"video-downloader.cache_submit")
|
||
queue_publish_span.set_data("messaging.message.body.size", 0)
|
||
media.status = MediaCacheStatus.downloading
|
||
media.downloader_id = fn_task.object_id
|
||
# video_cache = MediaCache(status=MediaCacheStatus.downloading,
|
||
# downloader_id=fn_task.object_id)
|
||
# video_cache_status_json = video_cache.model_dump_json()
|
||
modal_kv_cache.set_cache(media)
|
||
cache_hit = False
|
||
|
||
# caches[media.urn] = video_cache
|
||
# logger.info(f"Media cache hit ? {cache_hit}")
|
||
cache_span.set_data("cache.hit", cache_hit)
|
||
return media
|
||
|
||
async with asyncio.TaskGroup() as group:
|
||
tasks = [group.create_task(cache_handler(media)) for media in medias.inputs]
|
||
|
||
cache_task_result = [task.result() for task in tasks]
|
||
|
||
batch_update_cloudflare_kv(cache_task_result)
|
||
return CacheResult(caches={media.urn: media for media in cache_task_result})
|
||
|
||
|
||
@web_app.post("/cache/download",
|
||
tags=["缓存"],
|
||
summary="批量获取下载地址",
|
||
description="获取已缓存的视频下载地址",
|
||
dependencies=[Depends(verify_token)])
|
||
@sentry_sdk.trace
|
||
async def download_caches(medias: MediaSources) -> DownloadResult:
|
||
cdn_endpoint = config.S3_cdn_endpoint
|
||
urls = []
|
||
for media in medias.inputs:
|
||
urls.append(f"{cdn_endpoint}/{media.get_cdn_url()}")
|
||
return DownloadResult(urls=urls)
|
||
|
||
|
||
@web_app.get("/cache/download",
|
||
tags=["缓存"],
|
||
summary="下载已缓存的视频",
|
||
description="通过CDN下载已缓存的视频文件")
|
||
@sentry_sdk.trace
|
||
async def download_cache(media: str) -> RedirectResponse:
|
||
cdn_endpoint = config.S3_cdn_endpoint
|
||
media = MediaSource.from_str(media)
|
||
return RedirectResponse(url=f"{cdn_endpoint}/{media.get_cdn_url()}", status_code=status.HTTP_302_FOUND)
|
||
|
||
|
||
@web_app.delete("/cache/kv",
|
||
tags=["缓存"],
|
||
summary="清除KV记录",
|
||
description="清除当前环境下KV缓存过的所有数据(S3存储桶内的文件会保留)",
|
||
dependencies=[Depends(verify_token)])
|
||
async def purge_kv_all():
|
||
parent = sentry_sdk.get_current_span()
|
||
span = parent.start_child(name="清除缓存KV", op="purge.flush")
|
||
modal_kv_cache.clear()
|
||
span.set_data("cache.success", True)
|
||
span.finish()
|
||
return JSONResponse(content={"success": True})
|
||
|
||
|
||
@web_app.post("/cache/kv",
|
||
tags=["缓存"],
|
||
summary="删除对应的KV记录",
|
||
description="删除请求中对应的视频缓存记录",
|
||
dependencies=[Depends(verify_token)])
|
||
async def purge_kv(medias: MediaSources):
|
||
try:
|
||
for media in medias.inputs:
|
||
modal_kv_cache.pop(media.urn)
|
||
keys = [media.urn for media in medias.inputs]
|
||
batch_remove_cloudflare_kv(keys)
|
||
return JSONResponse(content={"success": True, "keys": keys})
|
||
except Exception as e:
|
||
return JSONResponse(content={"success": False, "error": str(e)})
|
||
|
||
|
||
@web_app.post("/cache/media",
|
||
tags=["缓存"],
|
||
summary="清除指定的所有缓存",
|
||
description="清除指定的所有缓存(包括KV记录和S3存储文件)",
|
||
dependencies=[Depends(verify_token)])
|
||
async def purge_media(medias: MediaSources):
|
||
fn_id = current_function_call_id()
|
||
fn = modal.Function.from_name(config.modal_app_name, "cache_delete", environment_name=config.modal_environment)
|
||
|
||
@SentryUtils.sentry_tracker(name="清除媒体源缓存", op="cache.purge", fn_id=fn_id,
|
||
sentry_trace_id=None, sentry_baggage=None)
|
||
async def purge_handle(media: MediaSource):
|
||
cache_media = modal_kv_cache.pop(media.urn)
|
||
if cache_media:
|
||
deleted_cache: MediaSource = await fn.remote.aio(cache_media)
|
||
return deleted_cache.urn
|
||
return None
|
||
|
||
async with asyncio.TaskGroup() as group:
|
||
tasks = [group.create_task(purge_handle(media)) for media in medias.inputs]
|
||
|
||
keys = [task.result() for task in tasks]
|
||
batch_remove_cloudflare_kv(keys)
|
||
return JSONResponse(content={"success": True, "keys": keys})
|
||
|
||
|
||
@web_app.post("/ffmpeg/slice",
|
||
tags=["发起任务"],
|
||
summary="发起切割任务",
|
||
description="依据打点信息切出多个片段",
|
||
dependencies=[Depends(verify_token)])
|
||
async def slice_media(request: FFMPEGSliceRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_slice_media",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=request.media, markers=request.markers, sentry_trace=sentry_trace, webhook=request.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/slice/{task_id}",
|
||
tags=["查询任务"],
|
||
summary="查询切割任务状态/结果",
|
||
description="根据任务Id查询运行状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def slice_media(task_id: str, response: Response) -> FFMPEGSliceTaskStatusResponse:
|
||
task_status, code, reason, results, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGSliceTaskStatusResponse(taskId=task_id, status=task_status, code=code, error=reason,
|
||
result=results)
|
||
|
||
|
||
@web_app.post("/ffmpeg/concat",
|
||
tags=["发起任务"],
|
||
summary="发起合并任务",
|
||
description="依据AI分析的结果发起合并任务",
|
||
dependencies=[Depends(verify_token)])
|
||
async def concat_media(body: FFMPEGConcatRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
medias = body.medias
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_concat_medias",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(medias, sentry_trace, webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/concat/{task_id}",
|
||
tags=["查询任务"],
|
||
summary="获取合并任务结果",
|
||
description="获取合并任务的处理状态和结果",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def concat_media_status(task_id: str, response: Response) -> FFMPEGConcatTaskStatusResponse:
|
||
task_status, code, reason, results, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGConcatTaskStatusResponse(taskId=task_id, status=task_status,
|
||
code=code, error=reason,
|
||
result=results)
|
||
|
||
|
||
@web_app.post("/ffmpeg/extract-audio",
|
||
tags=["发起任务"],
|
||
summary="发起音频提取任务",
|
||
description="提取视频文件的音频",
|
||
dependencies=[Depends(verify_token)]
|
||
)
|
||
async def extract_audio(body: FFMPEGExtractAudioRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
media = body.media
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_extract_audio",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media, sentry_trace, webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/extract-audio/{task_id}",
|
||
tags=["查询任务"],
|
||
summary="查询音频提取任务状态",
|
||
description="查询音频提取任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def extract_audio_status(task_id: str, response: Response) -> FFMPEGExtractAudioTaskStatusResponse:
|
||
task_status, code, reason, results, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
|
||
return FFMPEGExtractAudioTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=results)
|
||
|
||
|
||
@web_app.post("/ffmpeg/corner-mirror", tags=["发起任务"], summary="发起镜像小窗去重任务",
|
||
description="发起镜像小窗去重任务", dependencies=[Depends(verify_token)])
|
||
async def corner_mirror(body: FFMPEGCornerMirrorRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
media = body.media
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_corner_mirror",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=media, sentry_trace=sentry_trace, webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/corner-mirror/{task_id}",
|
||
tags=["查询任务"],
|
||
summary="查询镜像小窗去重任务状态",
|
||
description="查询镜像小窗去重任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def corner_mirror_status(task_id: str, response: Response) -> FFMPEGCornerMirrorTaskStatusResponse:
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGCornerMirrorTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=result)
|
||
|
||
|
||
@web_app.post("/ffmpeg/overlay-gif", tags=["发起任务"], summary="发起叠加gif特效任务",
|
||
description="发起叠加gif特效任务", dependencies=[Depends(verify_token)])
|
||
async def overlay_gif(body: FFMPEGOverlayGifRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_overlay_gif",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=body.media, gif=body.gif, sentry_trace=sentry_trace, webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/overlay-gif/{task_id}", tags=["查询任务"],
|
||
summary="查询叠加gif特效任务状态", description="查询叠加gif特效任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def overlay_gif_status(task_id: str, response: Response) -> FFMPEGOverlayGifTaskStatusResponse:
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGOverlayGifTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=result)
|
||
|
||
|
||
@web_app.post("/ffmpeg/zoom-loop", tags=["发起任务"], summary="发起放大缩小循环去重任务",
|
||
description="发起放大缩小循环去重任务", dependencies=[Depends(verify_token)])
|
||
async def zoom_loop(body: FFMPEGZoomLoopRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_zoom_loop", environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=body.media, duration=body.duration, zoom=body.zoom, sentry_trace=sentry_trace,
|
||
webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/zoom-loop/{task_id}", tags=["查询任务"],
|
||
summary="查询放大缩小循环去重任务状态", description="查询放大缩小循环去重任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def zoom_loop_status(task_id: str, response: Response) -> FFMPEGZoomLoopTaskStatusResponse:
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGZoomLoopTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=result)
|
||
|
||
|
||
@web_app.post("/ffmpeg/subtitle-apply", tags=["发起任务"], summary="发起渲染字幕任务",
|
||
description="发起渲染字幕任务", dependencies=[Depends(verify_token)])
|
||
async def subtitle_apply(body: FFMPEGSubtitleOverlayRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_subtitle_apply",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=body.media, subtitle=body.subtitle, fonts=body.fonts, sentry_trace=sentry_trace,
|
||
webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/subtitle-apply/{task_id}", tags=["查询任务"],
|
||
summary="查询渲染字幕任务状态", description="查询渲染字幕任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def subtitle_apply_status(task_id: str, response: Response) -> FFMPEGSubtitleTaskStatusResponse:
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGSubtitleTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=result)
|
||
|
||
|
||
@web_app.post("/ffmpeg/bgm-nosie-reduce", tags=["发起任务"], summary="发起混合BGM并降噪任务",
|
||
description="发起混合BGM并降噪任务", dependencies=[Depends(verify_token)])
|
||
async def bgm_nosie_reduce(body: FFMPEGMixBgmWithNoiseReduceRequest,
|
||
headers: Annotated[SentryTransactionHeader, Header()]) -> ModalTaskResponse:
|
||
fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_bgm_nosie_reduce",
|
||
environment_name=config.modal_environment)
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
fn_call = fn.spawn(media=body.media, bgm=body.bgm, video_volume=body.video_volume, music_volume=body.music_volume,
|
||
noise_sample=body.noise_sample, sentry_trace=sentry_trace, webhook=body.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/ffmpeg/bgm-nosie-reduce/{task_id}", tags=["查询任务"],
|
||
summary="查询混合BGM并降噪任务状态", description="查询混合BGM并降噪任务状态",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)])
|
||
async def bgm_nosie_reduce_status(task_id: str, response: Response) -> FFMPEGMixBgmWithNoiseReduceStatusResponse:
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
return FFMPEGMixBgmWithNoiseReduceStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=result)
|
||
|
||
|
||
@web_app.post("/comfyui/v1", tags=["ComfyUI"], summary="发起ComfyUIBase任务", description="发起ComfyUIBase任务",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)]
|
||
)
|
||
async def comfyui_v1(item: ComfyTaskRequest, headers: Annotated[SentryTransactionHeader, Header()]):
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
cls = modal.Cls.from_name(config.modal_app_name, "ComfyUI", environment_name=config.modal_environment)
|
||
fn_call = cls().api.spawn(item.video_path.path, item.start_time, item.filename_prefix, item.tts_text1,
|
||
item.tts_text2, item.tts_text3, item.tts_text4, item.anchor_id, item.speed, sentry_trace, item.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/comfyui/v1/{task_id}", tags=["ComfyUI"], summary="查询ComfyUIBase任务",
|
||
description="查询ComfyUIBase任务",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)]
|
||
)
|
||
async def comfyui_v1_status(task_id: str, response: Response):
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
media = None
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
if task_status == "success":
|
||
task_status = result["status"]
|
||
if task_status != "success":
|
||
reason = result["msg"]
|
||
else:
|
||
media = MediaSource.from_str("s3://" + "/".join(
|
||
[config.S3_region, config.S3_bucket_name, config.comfyui_s3_output, result["file_name"]]))
|
||
return ComfyTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason, result=media.urn if media else None)
|
||
|
||
|
||
@web_app.post("/comfyui/v2", tags=["ComfyUI"], summary="发起ComfyUILatentSync1.5任务",
|
||
description="发起ComfyUILatentSync1.5任务",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)]
|
||
)
|
||
async def comfyui_v2(item: ComfyTaskRequest, headers: Annotated[SentryTransactionHeader, Header()]):
|
||
sentry_trace = None
|
||
if headers.x_trace_id and headers.x_baggage:
|
||
sentry_trace = SentryTransactionInfo(x_trace_id=headers.x_trace_id, x_baggage=headers.x_baggage)
|
||
cls = modal.Cls.from_name(config.modal_app_name, "ComfyUILatentSync15", environment_name=config.modal_environment)
|
||
fn_call = cls().api.spawn(item.video_path.path, item.start_time, item.filename_prefix, item.tts_text1,
|
||
item.tts_text2, item.tts_text3, item.tts_text4, item.anchor_id, item.speed, sentry_trace, item.webhook)
|
||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||
|
||
|
||
@web_app.get("/comfyui/v2/{task_id}", tags=["ComfyUI"], summary="查询ComfyUILatentSync1.5任务",
|
||
description="查询ComfyUILatentSync1.5任务",
|
||
responses={
|
||
status.HTTP_200_OK: {
|
||
"description": "",
|
||
"headers": sentry_header_schema
|
||
},
|
||
},
|
||
dependencies=[Depends(verify_token)]
|
||
)
|
||
async def comfyui_v2_status(task_id: str, response: Response):
|
||
task_status, code, reason, result, transaction = await get_modal_task_status(task_id)
|
||
media = None
|
||
|
||
if transaction:
|
||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||
response.headers["x-baggage"] = transaction.x_baggage
|
||
if task_status == "success":
|
||
task_status = result["status"]
|
||
if task_status != "success":
|
||
reason = result["msg"]
|
||
else:
|
||
media = MediaSource.from_str("s3://" + "/".join(
|
||
[config.S3_region, config.S3_bucket_name, config.comfyui_s3_output, result["file_name"]]))
|
||
return ComfyTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||
error=reason,
|
||
result=media.urn if media else "")
|
||
|
||
|