按功能模块拆分API路由到不同文件
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
MODAL_ENVIRONMENT=dev
|
||||
modal_app_name=cluster-test
|
||||
modal_app_name=bowong-ai-video
|
||||
S3_mount_dir=/mntS3
|
||||
S3_bucket_name=modal-media-cache
|
||||
S3_region=ap-northeast-2
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
modal deploy -m cluster.app
|
||||
```
|
||||
|
||||
## 本地验证fastapi部署
|
||||
|
||||
```bash
|
||||
uvicorn BowongModalFunctions.api:web_app --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
# Pip
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -41,7 +41,11 @@ license = "MIT"
|
||||
license-files = ["LICEN[CS]E*"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
cli = ["twine>=6.1.0"]
|
||||
cli = [
|
||||
"gunicorn>=23.0.0",
|
||||
"twine>=6.1.0",
|
||||
"uvicorn[standard]>=0.34.2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling >= 1.26"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.1-pre3"
|
||||
__version__: str = "0.1.2"
|
||||
@@ -1,58 +1,29 @@
|
||||
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 typing import Annotated
|
||||
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 fastapi.middleware.cors import CORSMiddleware
|
||||
from .middleware.authorization import verify_token
|
||||
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,
|
||||
from .utils.ModalUtils import ModalUtils
|
||||
from .models.media_model import MediaSource
|
||||
from .models.web_model import (SentryTransactionInfo,
|
||||
SentryTransactionHeader,
|
||||
ModalTaskResponse,
|
||||
FFMPEGSliceRequest,
|
||||
FFMPEGConcatRequest,
|
||||
FFMPEGExtractAudioRequest,
|
||||
FFMPEGCornerMirrorRequest,
|
||||
FFMPEGOverlayGifRequest,
|
||||
FFMPEGZoomLoopRequest,
|
||||
FFMPEGSubtitleOverlayRequest,
|
||||
FFMPEGMixBgmWithNoiseReduceRequest,
|
||||
FFMPEGSliceTaskStatusResponse,
|
||||
FFMPEGConcatTaskStatusResponse,
|
||||
FFMPEGExtractAudioTaskStatusResponse,
|
||||
FFMPEGCornerMirrorTaskStatusResponse,
|
||||
FFMPEGOverlayGifTaskStatusResponse,
|
||||
FFMPEGSubtitleTaskStatusResponse,
|
||||
FFMPEGZoomLoopTaskStatusResponse,
|
||||
FFMPEGMixBgmWithNoiseReduceStatusResponse,
|
||||
ComfyTaskRequest,
|
||||
ComfyTaskStatusResponse
|
||||
)
|
||||
from .router import ffmpeg, cache
|
||||
from .config import WorkerConfig
|
||||
|
||||
bearer_scheme = HTTPBearer()
|
||||
|
||||
config = WorkerConfig()
|
||||
|
||||
web_app = FastAPI(title="Modal worker API",
|
||||
version="0.1.2",
|
||||
summary="Modal Worker的API, 包括缓存视频, 发起生产任务等",
|
||||
servers=[
|
||||
{'url': f'https://bowongai-dev--{config.modal_app_name}-fastapi-webapp.modal.run',
|
||||
@@ -66,25 +37,6 @@ web_app = FastAPI(title="Modal worker API",
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
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,
|
||||
@@ -97,9 +49,6 @@ sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.co
|
||||
]
|
||||
)
|
||||
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": {
|
||||
@@ -120,6 +69,14 @@ ALIAS_MAP = {
|
||||
"/comfyui": "/comfyui/v2",
|
||||
}
|
||||
|
||||
web_app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@web_app.middleware("http")
|
||||
@web_app.middleware("https")
|
||||
@@ -129,538 +86,13 @@ async def alias_middleware(request: Request, call_next):
|
||||
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.include_router(ffmpeg.router)
|
||||
web_app.include_router(cache.router)
|
||||
|
||||
|
||||
@web_app.post("/comfyui/v1", tags=["ComfyUI"], summary="发起ComfyUIBase任务", description="发起ComfyUIBase任务",
|
||||
@@ -678,7 +110,8 @@ async def comfyui_v1(item: ComfyTaskRequest, headers: Annotated[SentryTransactio
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -693,7 +126,7 @@ async def comfyui_v1(item: ComfyTaskRequest, headers: Annotated[SentryTransactio
|
||||
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)
|
||||
task_status, code, reason, result, transaction = await ModalUtils.get_modal_task_status(task_id)
|
||||
media = None
|
||||
if transaction:
|
||||
response.headers["x-trace-id"] = transaction.x_trace_id
|
||||
@@ -725,7 +158,8 @@ async def comfyui_v2(item: ComfyTaskRequest, headers: Annotated[SentryTransactio
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -740,7 +174,7 @@ async def comfyui_v2(item: ComfyTaskRequest, headers: Annotated[SentryTransactio
|
||||
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)
|
||||
task_status, code, reason, result, transaction = await ModalUtils.get_modal_task_status(task_id)
|
||||
media = None
|
||||
|
||||
if transaction:
|
||||
@@ -756,5 +190,3 @@ async def comfyui_v2_status(task_id: str, response: Response):
|
||||
return ComfyTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||||
error=reason,
|
||||
result=media.urn if media else "")
|
||||
|
||||
|
||||
|
||||
0
src/BowongModalFunctions/middleware/__init__.py
Normal file
0
src/BowongModalFunctions/middleware/__init__.py
Normal file
23
src/BowongModalFunctions/middleware/authorization.py
Normal file
23
src/BowongModalFunctions/middleware/authorization.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from starlette import status
|
||||
|
||||
bearer_scheme = HTTPBearer()
|
||||
|
||||
|
||||
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"
|
||||
@@ -39,7 +39,7 @@ class MediaSource(BaseModel):
|
||||
bucket: Optional[str] = Field(description="媒体源所使用的存储桶(s3)/SubAppId(vod)")
|
||||
urn: Optional[str] = Field(description="媒体源的唯一指定标识")
|
||||
status: MediaCacheStatus = Field(default=MediaCacheStatus.unknown, description="媒体源在Modal集群挂载的缓存状态")
|
||||
expired_at: Optional[datetime] = Field(description="缓存过期时间点", default=None)
|
||||
expired_at: Optional[datetime] = Field(description="缓存过期时间点, 为None时不会过期(过期处理WIP)", default=None)
|
||||
downloader_id: Optional[str] = Field(description="正在处理下载的Downloader ID", default=None)
|
||||
progress: int = Field(description="缓存进度", default=0)
|
||||
|
||||
@@ -165,7 +165,7 @@ class MediaSources(BaseModel):
|
||||
@classmethod
|
||||
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
||||
if not v:
|
||||
return v
|
||||
raise ValidationError("inputs为空")
|
||||
result = []
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
|
||||
@@ -56,7 +56,6 @@ class WebhookNotify(BaseModel):
|
||||
examples=["Bearer 123456"], default=None)
|
||||
|
||||
|
||||
|
||||
class BaseFFMPEGTaskRequest(BaseModel):
|
||||
webhook: Optional[WebhookNotify] = Field(description="Task webhook", default=None)
|
||||
|
||||
@@ -249,7 +248,7 @@ class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
||||
@classmethod
|
||||
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
||||
if not v:
|
||||
return v
|
||||
raise pydantic.ValidationError("fonts输入为空")
|
||||
result = []
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
@@ -309,6 +308,7 @@ class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
|
||||
class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
|
||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
||||
|
||||
|
||||
class ComfyTaskStatusResponse(BaseModel):
|
||||
taskId: str = Field(description="任务Id")
|
||||
status: TaskStatus = Field(description="任务运行状态")
|
||||
@@ -316,8 +316,10 @@ class ComfyTaskStatusResponse(BaseModel):
|
||||
code: Optional[int] = Field(description="任务错误原因代码", default=None)
|
||||
result: Optional[str] = Field(description="任务运行结果", default=None)
|
||||
|
||||
|
||||
class ComfyTaskRequest(BaseFFMPEGTaskRequest):
|
||||
video_path: MediaSource = Field(default=MediaSource.from_str("s3://modal-media-cache/comfyui-input/markers/1016.mp4"), description="视频源")
|
||||
video_path: MediaSource = Field(
|
||||
default=MediaSource.from_str("s3://modal-media-cache/comfyui-input/markers/1016.mp4"), description="视频源")
|
||||
start_time: str = Field(default="00:00:01.600", description="开始时间")
|
||||
filename_prefix: str = Field(default=str(uuid.uuid4()), description="生成文件名前缀")
|
||||
tts_text1: str = Field(default="好好看,这是我们家专门为女生定制的背心,", description="tts文本1")
|
||||
|
||||
0
src/BowongModalFunctions/router/__init__.py
Normal file
0
src/BowongModalFunctions/router/__init__.py
Normal file
180
src/BowongModalFunctions/router/cache.py
Normal file
180
src/BowongModalFunctions/router/cache.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import asyncio
|
||||
|
||||
import modal
|
||||
from modal import current_function_call_id
|
||||
import sentry_sdk
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette import status
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
from ..config import WorkerConfig
|
||||
from ..middleware.authorization import verify_token
|
||||
from ..models.media_model import MediaSources, CacheResult, MediaSource, MediaCacheStatus, DownloadResult
|
||||
|
||||
from ..models.web_model import SentryTransactionInfo
|
||||
from ..utils.KVCache import KVCache
|
||||
from ..utils.SentryUtils import SentryUtils
|
||||
|
||||
config = WorkerConfig()
|
||||
|
||||
router = APIRouter(prefix="/cache")
|
||||
modal_kv_cache = KVCache(kv_name=config.modal_kv_name, environment=config.modal_environment)
|
||||
|
||||
|
||||
@router.post("/",
|
||||
tags=["缓存"],
|
||||
summary="缓存视频文件",
|
||||
description="异步缓存视频文件到S3存储桶和Modal Dict(KV)",
|
||||
dependencies=[Depends(verify_token)])
|
||||
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]
|
||||
|
||||
KVCache.batch_update_cloudflare_kv(cache_task_result)
|
||||
return CacheResult(caches={media.urn: media for media in cache_task_result})
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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)
|
||||
|
||||
|
||||
@router.delete("/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})
|
||||
|
||||
|
||||
@router.post("/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]
|
||||
KVCache.batch_remove_cloudflare_kv(keys)
|
||||
return JSONResponse(content={"success": True, "keys": keys})
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"success": False, "error": str(e)})
|
||||
|
||||
|
||||
@router.post("/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]
|
||||
KVCache.batch_remove_cloudflare_kv(keys)
|
||||
return JSONResponse(content={"success": True, "keys": keys})
|
||||
319
src/BowongModalFunctions/router/ffmpeg.py
Normal file
319
src/BowongModalFunctions/router/ffmpeg.py
Normal file
@@ -0,0 +1,319 @@
|
||||
from typing import Annotated
|
||||
|
||||
import modal
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
from fastapi.responses import Response
|
||||
from starlette import status
|
||||
|
||||
from ..config import WorkerConfig
|
||||
from ..middleware.authorization import verify_token
|
||||
from ..utils.ModalUtils import ModalUtils
|
||||
from ..models.web_model import (FFMPEGSliceRequest, SentryTransactionHeader,
|
||||
ModalTaskResponse, SentryTransactionInfo,
|
||||
FFMPEGConcatRequest,
|
||||
FFMPEGExtractAudioRequest,
|
||||
FFMPEGCornerMirrorRequest,
|
||||
FFMPEGOverlayGifRequest,
|
||||
FFMPEGZoomLoopRequest,
|
||||
FFMPEGSubtitleOverlayRequest,
|
||||
FFMPEGMixBgmWithNoiseReduceRequest,
|
||||
FFMPEGSliceTaskStatusResponse,
|
||||
FFMPEGConcatTaskStatusResponse,
|
||||
FFMPEGExtractAudioTaskStatusResponse,
|
||||
FFMPEGCornerMirrorTaskStatusResponse,
|
||||
FFMPEGOverlayGifTaskStatusResponse,
|
||||
FFMPEGSubtitleTaskStatusResponse,
|
||||
FFMPEGZoomLoopTaskStatusResponse,
|
||||
FFMPEGMixBgmWithNoiseReduceStatusResponse)
|
||||
|
||||
config = WorkerConfig()
|
||||
|
||||
router = APIRouter(prefix="/ffmpeg")
|
||||
|
||||
sentry_header_schema = {
|
||||
"x-trace-id": {
|
||||
"description": "Sentry Transaction ID",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"x-baggage": {
|
||||
"description": "Sentry Transaction baggage",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
|
||||
|
||||
@router.post("/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)
|
||||
|
||||
|
||||
@router.get("/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 ModalUtils.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)
|
||||
@@ -1,8 +1,13 @@
|
||||
from typing import Optional
|
||||
import os
|
||||
from typing import Optional, List
|
||||
|
||||
import httpx
|
||||
import modal
|
||||
|
||||
from loguru import logger
|
||||
from ..models.media_model import MediaSource
|
||||
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")
|
||||
|
||||
|
||||
class KVCache:
|
||||
@@ -31,3 +36,50 @@ class KVCache:
|
||||
raise KeyError("URN错误,资源不存在")
|
||||
return None
|
||||
return MediaSource.model_validate_json(cache_json)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
56
src/BowongModalFunctions/utils/ModalUtils.py
Normal file
56
src/BowongModalFunctions/utils/ModalUtils.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import Tuple, Optional, Any
|
||||
|
||||
import modal
|
||||
from loguru import logger
|
||||
from modal.call_graph import InputStatus
|
||||
|
||||
from BowongModalFunctions.models.web_model import TaskStatus, SentryTransactionInfo, ErrorCode
|
||||
|
||||
|
||||
class ModalUtils:
|
||||
@staticmethod
|
||||
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
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
from typing import Optional, Tuple, Any, Coroutine
|
||||
from typing import Optional, Tuple, Any
|
||||
import httpx
|
||||
import psutil
|
||||
import sentry_sdk
|
||||
@@ -17,7 +16,7 @@ class SentryUtils:
|
||||
cpu_count = psutil.cpu_count()
|
||||
mem = psutil.virtual_memory()
|
||||
|
||||
return (cpu_freq, cpu_count, mem)
|
||||
return cpu_freq, cpu_count, mem
|
||||
|
||||
@staticmethod
|
||||
def sentry_tracker(sentry_trace_id: Optional[str], sentry_baggage: Optional[str],
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Union, List, Tuple, Optional
|
||||
import numpy as np
|
||||
import json, os
|
||||
import math
|
||||
from BowongModalFunctions.models.media_model import MediaSource
|
||||
from .TimeUtils import TimeDelta
|
||||
from pydantic import BaseModel, ConfigDict, computed_field
|
||||
from ffmpeg import FFmpeg
|
||||
|
||||
@@ -16,5 +16,5 @@ app = modal.App(config.modal_app_name,
|
||||
app.include(media_app)
|
||||
app.include(ffmpeg_app)
|
||||
app.include(web_app)
|
||||
app.include(comfyui_v1_app)
|
||||
app.include(comfyui_v2_app)
|
||||
# app.include(comfyui_v1_app)
|
||||
# app.include(comfyui_v2_app)
|
||||
|
||||
@@ -5,7 +5,7 @@ ffmpeg_worker_image = (
|
||||
modal.Image.debian_slim(python_version="3.11")
|
||||
.apt_install('ffmpeg')
|
||||
.pip_install_from_pyproject("../pyproject.toml")
|
||||
.env(dotenv_values("../.runtime.env"))
|
||||
.env(dotenv_values("../../.runtime.env"))
|
||||
.add_local_python_source('cluster')
|
||||
.add_local_python_source('BowongModalFunctions')
|
||||
)
|
||||
@@ -16,8 +16,8 @@ app = modal.App(
|
||||
include_source=False)
|
||||
|
||||
with ffmpeg_worker_image.imports():
|
||||
import shutil, psutil, os, backoff, sentry_sdk
|
||||
from typing import List, Optional, Tuple, Any
|
||||
import shutil, os, backoff, sentry_sdk
|
||||
from typing import List, Optional, Tuple
|
||||
from loguru import logger
|
||||
from modal import current_function_call_id
|
||||
from ffmpeg.asyncio import FFmpeg
|
||||
@@ -44,15 +44,6 @@ with ffmpeg_worker_image.imports():
|
||||
)
|
||||
|
||||
|
||||
@sentry_sdk.trace
|
||||
def capture_hardware_info() -> Tuple[Any, int, Any]:
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
cpu_count = psutil.cpu_count()
|
||||
mem = psutil.virtual_memory()
|
||||
|
||||
return (cpu_freq, cpu_count, mem)
|
||||
|
||||
|
||||
def ffmpeg_init(use_ffprobe: bool = False) -> FFmpeg:
|
||||
if use_ffprobe:
|
||||
ffmpeg_cmd = FFmpeg('ffprobe')
|
||||
@@ -455,7 +446,7 @@ with ffmpeg_worker_image.imports():
|
||||
output_path=output_path)
|
||||
|
||||
s3_outputs = local_copy_to_s3([local_output_filepath])
|
||||
return s3_outputs
|
||||
return s3_outputs[0]
|
||||
|
||||
result = await ffmpeg_process(video=media, bgm=bgm, video_volume=video_volume,
|
||||
music_volume=music_volume, noise_sample=noise_sample)
|
||||
|
||||
@@ -136,7 +136,7 @@ with downloader_image.imports():
|
||||
file_extension = media_info.Type
|
||||
cache_dir = f"/{config.S3_mount_dir}/{media.protocol.value}/{media.endpoint}/{media.bucket}"
|
||||
cache_file = media.path if '.' in media.path else f"{media.path}.{file_extension}"
|
||||
return (cache_dir, cache_file, media_info.MediaUrl)
|
||||
return cache_dir, cache_file, media_info.MediaUrl
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"FileId : {media.path} not found in SubAppId: {media.bucket} at {media.endpoint}")
|
||||
|
||||
16
uv.lock
generated
16
uv.lock
generated
@@ -197,7 +197,9 @@ dependencies = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "gunicorn" },
|
||||
{ name = "twine" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -207,6 +209,7 @@ requires-dist = [
|
||||
{ name = "cos-python-sdk-v5", specifier = ">=1.9.36" },
|
||||
{ name = "crcmod", specifier = ">=1.7" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||
{ name = "gunicorn", marker = "extra == 'cli'", specifier = ">=23.0.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "modal", specifier = ">=0.76.3" },
|
||||
@@ -225,6 +228,7 @@ requires-dist = [
|
||||
{ name = "tencentcloud-sdk-python-common", specifier = ">=3.0.1363" },
|
||||
{ name = "tencentcloud-sdk-python-vod", specifier = ">=3.0.1363" },
|
||||
{ name = "twine", marker = "extra == 'cli'", specifier = ">=6.1.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'cli'", specifier = ">=0.34.2" },
|
||||
{ name = "webvtt-py", specifier = ">=0.5.1" },
|
||||
]
|
||||
provides-extras = ["cli"]
|
||||
@@ -670,6 +674,18 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/b9/55936e462a5925190d7427e880b3033601d1effd13809b483d13a926061a/grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3", size = 61254 }
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "23.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
|
||||
Reference in New Issue
Block a user