按功能模块拆分API路由到不同文件
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
MODAL_ENVIRONMENT=dev
|
MODAL_ENVIRONMENT=dev
|
||||||
modal_app_name=cluster-test
|
modal_app_name=bowong-ai-video
|
||||||
S3_mount_dir=/mntS3
|
S3_mount_dir=/mntS3
|
||||||
S3_bucket_name=modal-media-cache
|
S3_bucket_name=modal-media-cache
|
||||||
S3_region=ap-northeast-2
|
S3_region=ap-northeast-2
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
modal deploy -m cluster.app
|
modal deploy -m cluster.app
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 本地验证fastapi部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn BowongModalFunctions.api:web_app --host 0.0.0.0 --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
# Pip
|
# Pip
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ license = "MIT"
|
|||||||
license-files = ["LICEN[CS]E*"]
|
license-files = ["LICEN[CS]E*"]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
cli = ["twine>=6.1.0"]
|
cli = [
|
||||||
|
"gunicorn>=23.0.0",
|
||||||
|
"twine>=6.1.0",
|
||||||
|
"uvicorn[standard]>=0.34.2",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling >= 1.26"]
|
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 modal
|
||||||
import httpx
|
from typing import Annotated
|
||||||
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 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 starlette import status
|
||||||
from scalar_fastapi import get_scalar_api_reference
|
from scalar_fastapi import get_scalar_api_reference
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels
|
from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels
|
||||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
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.KVCache import KVCache
|
||||||
from .utils.SentryUtils import SentryUtils
|
from .utils.ModalUtils import ModalUtils
|
||||||
from .models.media_model import (MediaSource,
|
from .models.media_model import MediaSource
|
||||||
MediaSources,
|
from .models.web_model import (SentryTransactionInfo,
|
||||||
MediaCacheStatus,
|
|
||||||
CacheResult,
|
|
||||||
DownloadResult)
|
|
||||||
from .models.web_model import (TaskStatus,
|
|
||||||
ErrorCode,
|
|
||||||
SentryTransactionInfo,
|
|
||||||
SentryTransactionHeader,
|
SentryTransactionHeader,
|
||||||
ModalTaskResponse,
|
ModalTaskResponse,
|
||||||
FFMPEGSliceRequest,
|
|
||||||
FFMPEGConcatRequest,
|
|
||||||
FFMPEGExtractAudioRequest,
|
|
||||||
FFMPEGCornerMirrorRequest,
|
|
||||||
FFMPEGOverlayGifRequest,
|
|
||||||
FFMPEGZoomLoopRequest,
|
|
||||||
FFMPEGSubtitleOverlayRequest,
|
|
||||||
FFMPEGMixBgmWithNoiseReduceRequest,
|
|
||||||
FFMPEGSliceTaskStatusResponse,
|
|
||||||
FFMPEGConcatTaskStatusResponse,
|
|
||||||
FFMPEGExtractAudioTaskStatusResponse,
|
|
||||||
FFMPEGCornerMirrorTaskStatusResponse,
|
|
||||||
FFMPEGOverlayGifTaskStatusResponse,
|
|
||||||
FFMPEGSubtitleTaskStatusResponse,
|
|
||||||
FFMPEGZoomLoopTaskStatusResponse,
|
|
||||||
FFMPEGMixBgmWithNoiseReduceStatusResponse,
|
|
||||||
ComfyTaskRequest,
|
ComfyTaskRequest,
|
||||||
ComfyTaskStatusResponse
|
ComfyTaskStatusResponse
|
||||||
)
|
)
|
||||||
|
from .router import ffmpeg, cache
|
||||||
from .config import WorkerConfig
|
from .config import WorkerConfig
|
||||||
|
|
||||||
bearer_scheme = HTTPBearer()
|
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
web_app = FastAPI(title="Modal worker API",
|
web_app = FastAPI(title="Modal worker API",
|
||||||
|
version="0.1.2",
|
||||||
summary="Modal Worker的API, 包括缓存视频, 发起生产任务等",
|
summary="Modal Worker的API, 包括缓存视频, 发起生产任务等",
|
||||||
servers=[
|
servers=[
|
||||||
{'url': f'https://bowongai-dev--{config.modal_app_name}-fastapi-webapp.modal.run',
|
{'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",
|
sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2",
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
traces_sample_rate=1.0,
|
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)
|
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 = {
|
sentry_header_schema = {
|
||||||
"x-trace-id": {
|
"x-trace-id": {
|
||||||
@@ -120,6 +69,14 @@ ALIAS_MAP = {
|
|||||||
"/comfyui": "/comfyui/v2",
|
"/comfyui": "/comfyui/v2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
web_app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@web_app.middleware("http")
|
@web_app.middleware("http")
|
||||||
@web_app.middleware("https")
|
@web_app.middleware("https")
|
||||||
@@ -129,538 +86,13 @@ async def alias_middleware(request: Request, call_next):
|
|||||||
return await call_next(request)
|
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)
|
@web_app.get("/scalar", include_in_schema=False)
|
||||||
async def scalar():
|
async def scalar():
|
||||||
return get_scalar_api_reference(openapi_url=web_app.openapi_schema, title="Modal worker web endpoint")
|
return get_scalar_api_reference(openapi_url=web_app.openapi_schema, title="Modal worker web endpoint")
|
||||||
|
|
||||||
|
|
||||||
@web_app.post("/cache",
|
web_app.include_router(ffmpeg.router)
|
||||||
tags=["缓存"],
|
web_app.include_router(cache.router)
|
||||||
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任务",
|
@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)
|
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)
|
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,
|
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)
|
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)]
|
dependencies=[Depends(verify_token)]
|
||||||
)
|
)
|
||||||
async def comfyui_v1_status(task_id: str, response: Response):
|
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
|
media = None
|
||||||
if transaction:
|
if transaction:
|
||||||
response.headers["x-trace-id"] = transaction.x_trace_id
|
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)
|
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)
|
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,
|
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)
|
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)]
|
dependencies=[Depends(verify_token)]
|
||||||
)
|
)
|
||||||
async def comfyui_v2_status(task_id: str, response: Response):
|
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
|
media = None
|
||||||
|
|
||||||
if transaction:
|
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,
|
return ComfyTaskStatusResponse(taskId=task_id, status=task_status, code=code,
|
||||||
error=reason,
|
error=reason,
|
||||||
result=media.urn if media else "")
|
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)")
|
bucket: Optional[str] = Field(description="媒体源所使用的存储桶(s3)/SubAppId(vod)")
|
||||||
urn: Optional[str] = Field(description="媒体源的唯一指定标识")
|
urn: Optional[str] = Field(description="媒体源的唯一指定标识")
|
||||||
status: MediaCacheStatus = Field(default=MediaCacheStatus.unknown, description="媒体源在Modal集群挂载的缓存状态")
|
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)
|
downloader_id: Optional[str] = Field(description="正在处理下载的Downloader ID", default=None)
|
||||||
progress: int = Field(description="缓存进度", default=0)
|
progress: int = Field(description="缓存进度", default=0)
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ class MediaSources(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
||||||
if not v:
|
if not v:
|
||||||
return v
|
raise ValidationError("inputs为空")
|
||||||
result = []
|
result = []
|
||||||
for item in v:
|
for item in v:
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class WebhookNotify(BaseModel):
|
|||||||
examples=["Bearer 123456"], default=None)
|
examples=["Bearer 123456"], default=None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFFMPEGTaskRequest(BaseModel):
|
class BaseFFMPEGTaskRequest(BaseModel):
|
||||||
webhook: Optional[WebhookNotify] = Field(description="Task webhook", default=None)
|
webhook: Optional[WebhookNotify] = Field(description="Task webhook", default=None)
|
||||||
|
|
||||||
@@ -249,7 +248,7 @@ class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
||||||
if not v:
|
if not v:
|
||||||
return v
|
raise pydantic.ValidationError("fonts输入为空")
|
||||||
result = []
|
result = []
|
||||||
for item in v:
|
for item in v:
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
@@ -309,6 +308,7 @@ class FFMPEGMixBgmWithNoiseReduceRequest(BaseFFMPEGTaskRequest):
|
|||||||
class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
|
class FFMPEGMixBgmWithNoiseReduceStatusResponse(BaseFFMPEGTaskStatusResponse):
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
||||||
|
|
||||||
|
|
||||||
class ComfyTaskStatusResponse(BaseModel):
|
class ComfyTaskStatusResponse(BaseModel):
|
||||||
taskId: str = Field(description="任务Id")
|
taskId: str = Field(description="任务Id")
|
||||||
status: TaskStatus = Field(description="任务运行状态")
|
status: TaskStatus = Field(description="任务运行状态")
|
||||||
@@ -316,16 +316,18 @@ class ComfyTaskStatusResponse(BaseModel):
|
|||||||
code: Optional[int] = Field(description="任务错误原因代码", default=None)
|
code: Optional[int] = Field(description="任务错误原因代码", default=None)
|
||||||
result: Optional[str] = Field(description="任务运行结果", default=None)
|
result: Optional[str] = Field(description="任务运行结果", default=None)
|
||||||
|
|
||||||
|
|
||||||
class ComfyTaskRequest(BaseFFMPEGTaskRequest):
|
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="开始时间")
|
start_time: str = Field(default="00:00:01.600", description="开始时间")
|
||||||
filename_prefix:str = Field(default=str(uuid.uuid4()), description="生成文件名前缀")
|
filename_prefix: str = Field(default=str(uuid.uuid4()), description="生成文件名前缀")
|
||||||
tts_text1:str = Field(default="好好看,这是我们家专门为女生定制的背心,", description="tts文本1")
|
tts_text1: str = Field(default="好好看,这是我们家专门为女生定制的背心,", description="tts文本1")
|
||||||
tts_text2:str = Field(default="", description="tts文本2")
|
tts_text2: str = Field(default="", description="tts文本2")
|
||||||
tts_text3:str = Field(default="", description="tts文本3")
|
tts_text3: str = Field(default="", description="tts文本3")
|
||||||
tts_text4:str = Field(default="", description="tts文本4")
|
tts_text4: str = Field(default="", description="tts文本4")
|
||||||
anchor_id: str = Field(default="dawan", description="讲话人ID")
|
anchor_id: str = Field(default="dawan", description="讲话人ID")
|
||||||
speed:float = Field(default=1, description="讲话语速")
|
speed: float = Field(default=1, description="讲话语速")
|
||||||
|
|
||||||
@field_validator('video_path', mode='before')
|
@field_validator('video_path', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -335,4 +337,4 @@ class ComfyTaskRequest(BaseFFMPEGTaskRequest):
|
|||||||
elif isinstance(v, MediaSource):
|
elif isinstance(v, MediaSource):
|
||||||
return v
|
return v
|
||||||
else:
|
else:
|
||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|||||||
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
|
import modal
|
||||||
|
from loguru import logger
|
||||||
from ..models.media_model import MediaSource
|
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:
|
class KVCache:
|
||||||
@@ -31,3 +36,50 @@ class KVCache:
|
|||||||
raise KeyError("URN错误,资源不存在")
|
raise KeyError("URN错误,资源不存在")
|
||||||
return None
|
return None
|
||||||
return MediaSource.model_validate_json(cache_json)
|
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
|
||||||
from typing import Optional, Tuple, Any, Coroutine
|
|
||||||
import httpx
|
import httpx
|
||||||
import psutil
|
import psutil
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
@@ -17,7 +16,7 @@ class SentryUtils:
|
|||||||
cpu_count = psutil.cpu_count()
|
cpu_count = psutil.cpu_count()
|
||||||
mem = psutil.virtual_memory()
|
mem = psutil.virtual_memory()
|
||||||
|
|
||||||
return (cpu_freq, cpu_count, mem)
|
return cpu_freq, cpu_count, mem
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sentry_tracker(sentry_trace_id: Optional[str], sentry_baggage: Optional[str],
|
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 numpy as np
|
||||||
import json, os
|
import json, os
|
||||||
import math
|
import math
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
|
||||||
from .TimeUtils import TimeDelta
|
from .TimeUtils import TimeDelta
|
||||||
from pydantic import BaseModel, ConfigDict, computed_field
|
from pydantic import BaseModel, ConfigDict, computed_field
|
||||||
from ffmpeg import FFmpeg
|
from ffmpeg import FFmpeg
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ app = modal.App(config.modal_app_name,
|
|||||||
app.include(media_app)
|
app.include(media_app)
|
||||||
app.include(ffmpeg_app)
|
app.include(ffmpeg_app)
|
||||||
app.include(web_app)
|
app.include(web_app)
|
||||||
app.include(comfyui_v1_app)
|
# app.include(comfyui_v1_app)
|
||||||
app.include(comfyui_v2_app)
|
# app.include(comfyui_v2_app)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ ffmpeg_worker_image = (
|
|||||||
modal.Image.debian_slim(python_version="3.11")
|
modal.Image.debian_slim(python_version="3.11")
|
||||||
.apt_install('ffmpeg')
|
.apt_install('ffmpeg')
|
||||||
.pip_install_from_pyproject("../pyproject.toml")
|
.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('cluster')
|
||||||
.add_local_python_source('BowongModalFunctions')
|
.add_local_python_source('BowongModalFunctions')
|
||||||
)
|
)
|
||||||
@@ -16,8 +16,8 @@ app = modal.App(
|
|||||||
include_source=False)
|
include_source=False)
|
||||||
|
|
||||||
with ffmpeg_worker_image.imports():
|
with ffmpeg_worker_image.imports():
|
||||||
import shutil, psutil, os, backoff, sentry_sdk
|
import shutil, os, backoff, sentry_sdk
|
||||||
from typing import List, Optional, Tuple, Any
|
from typing import List, Optional, Tuple
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from modal import current_function_call_id
|
from modal import current_function_call_id
|
||||||
from ffmpeg.asyncio import FFmpeg
|
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:
|
def ffmpeg_init(use_ffprobe: bool = False) -> FFmpeg:
|
||||||
if use_ffprobe:
|
if use_ffprobe:
|
||||||
ffmpeg_cmd = FFmpeg('ffprobe')
|
ffmpeg_cmd = FFmpeg('ffprobe')
|
||||||
@@ -455,7 +446,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
output_path=output_path)
|
output_path=output_path)
|
||||||
|
|
||||||
s3_outputs = local_copy_to_s3([local_output_filepath])
|
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,
|
result = await ffmpeg_process(video=media, bgm=bgm, video_volume=video_volume,
|
||||||
music_volume=music_volume, noise_sample=noise_sample)
|
music_volume=music_volume, noise_sample=noise_sample)
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ with downloader_image.imports():
|
|||||||
file_extension = media_info.Type
|
file_extension = media_info.Type
|
||||||
cache_dir = f"/{config.S3_mount_dir}/{media.protocol.value}/{media.endpoint}/{media.bucket}"
|
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}"
|
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:
|
else:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"FileId : {media.path} not found in SubAppId: {media.bucket} at {media.endpoint}")
|
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]
|
[package.optional-dependencies]
|
||||||
cli = [
|
cli = [
|
||||||
|
{ name = "gunicorn" },
|
||||||
{ name = "twine" },
|
{ name = "twine" },
|
||||||
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -207,6 +209,7 @@ requires-dist = [
|
|||||||
{ name = "cos-python-sdk-v5", specifier = ">=1.9.36" },
|
{ name = "cos-python-sdk-v5", specifier = ">=1.9.36" },
|
||||||
{ name = "crcmod", specifier = ">=1.7" },
|
{ name = "crcmod", specifier = ">=1.7" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||||
|
{ name = "gunicorn", marker = "extra == 'cli'", specifier = ">=23.0.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "loguru", specifier = ">=0.7.3" },
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "modal", specifier = ">=0.76.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-common", specifier = ">=3.0.1363" },
|
||||||
{ name = "tencentcloud-sdk-python-vod", specifier = ">=3.0.1363" },
|
{ name = "tencentcloud-sdk-python-vod", specifier = ">=3.0.1363" },
|
||||||
{ name = "twine", marker = "extra == 'cli'", specifier = ">=6.1.0" },
|
{ 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" },
|
{ name = "webvtt-py", specifier = ">=0.5.1" },
|
||||||
]
|
]
|
||||||
provides-extras = ["cli"]
|
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 }
|
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]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user