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