添加webhook回调处理装饰器
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
modal deploy -e dev ./src/cluster/app.py
|
modal deploy -m cluster.app
|
||||||
```
|
```
|
||||||
|
|
||||||
# Pip
|
# Pip
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="modal deploy" type="PythonConfigurationType" factoryName="Python">
|
<configuration default="false" name="modal deploy" type="PythonConfigurationType" factoryName="Python">
|
||||||
<module name="modalDeploy" />
|
<module name="modalDeploy" />
|
||||||
<option name="ENV_FILES" value="$PROJECT_DIR$/.env" />
|
<option name="ENV_FILES" value="$PROJECT_DIR$/.runtime.env" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
<envs>
|
<envs>
|
||||||
@@ -30,6 +30,7 @@ dependencies = [
|
|||||||
"scalar-fastapi>=1.0.3",
|
"scalar-fastapi>=1.0.3",
|
||||||
"modal>=0.76.3",
|
"modal>=0.76.3",
|
||||||
"python-dotenv>=1.1.0",
|
"python-dotenv>=1.1.0",
|
||||||
|
"python-multipart>=0.0.20",
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import List, Annotated, Tuple, Any, Optional
|
|||||||
from modal import current_function_call_id
|
from modal import current_function_call_id
|
||||||
from modal.call_graph import InputStatus
|
from modal.call_graph import InputStatus
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from fastapi import FastAPI, Response, Depends, Header
|
from fastapi import FastAPI, Response, Depends, Header, UploadFile, File
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
@@ -167,9 +167,9 @@ def batch_remove_cloudflare_kv(keys: List[str]):
|
|||||||
async def get_modal_task_status(task_id: str) -> Tuple[
|
async def get_modal_task_status(task_id: str) -> Tuple[
|
||||||
TaskStatus, Optional[int], Optional[str], Optional[Any], Optional[SentryTransactionInfo]]:
|
TaskStatus, Optional[int], Optional[str], Optional[Any], Optional[SentryTransactionInfo]]:
|
||||||
"""
|
"""
|
||||||
|
使用modal任务id查看任务运行状态和结果
|
||||||
:param task_id:
|
:param task_id: modal 任务id
|
||||||
:return: (TaskStaus, errorCode, errorReason, results, sentryTransactionInfo)
|
:return: (TaskStaus 任务运行状态, errorCode 错误代码, errorReason 错误原因, results 任务结果, sentryTransactionInfo Sentry跟踪信息)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fn_task = modal.FunctionCall.from_id(task_id)
|
fn_task = modal.FunctionCall.from_id(task_id)
|
||||||
@@ -180,7 +180,6 @@ async def get_modal_task_status(task_id: str) -> Tuple[
|
|||||||
task = root.children[0]
|
task = root.children[0]
|
||||||
if not task.function_call_id == task_id:
|
if not task.function_call_id == task_id:
|
||||||
return TaskStatus.expired, ErrorCode.NOT_FOUND.value, "NOT_FOUND", None, None
|
return TaskStatus.expired, ErrorCode.NOT_FOUND.value, "NOT_FOUND", None, None
|
||||||
|
|
||||||
logger.info(task)
|
logger.info(task)
|
||||||
logger.info(f"Task function call status: {task.status}")
|
logger.info(f"Task function call status: {task.status}")
|
||||||
match task.status:
|
match task.status:
|
||||||
@@ -388,7 +387,7 @@ async def slice_media(request: FFMPEGSliceRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(request.media, request.markers, sentry_trace)
|
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)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -425,7 +424,7 @@ async def concat_media(body: FFMPEGConcatRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(medias, sentry_trace)
|
fn_call = fn.spawn(medias, sentry_trace, webhook=body.webhook)
|
||||||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -464,7 +463,7 @@ async def extract_audio(body: FFMPEGExtractAudioRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media, sentry_trace)
|
fn_call = fn.spawn(media, sentry_trace, webhook=body.webhook)
|
||||||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -499,7 +498,7 @@ async def corner_mirror(body: FFMPEGCornerMirrorRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media=media, sentry_trace=sentry_trace)
|
fn_call = fn.spawn(media=media, sentry_trace=sentry_trace, webhook=body.webhook)
|
||||||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -532,7 +531,7 @@ async def overlay_gif(body: FFMPEGOverlayGifRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media=body.media, gif=body.gif, sentry_trace=sentry_trace)
|
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)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -562,7 +561,8 @@ async def zoom_loop(body: FFMPEGZoomLoopRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media=body.media, duration=body.duration, zoom=body.zoom, sentry_trace=sentry_trace)
|
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)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -593,7 +593,8 @@ async def subtitle_apply(body: FFMPEGSubtitleOverlayRequest,
|
|||||||
sentry_trace = None
|
sentry_trace = None
|
||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media=body.media, subtitle=body.subtitle, sentry_trace=sentry_trace)
|
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)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -625,11 +626,11 @@ async def bgm_nosie_reduce(body: FFMPEGMixBgmWithNoiseReduceRequest,
|
|||||||
if headers.x_trace_id and headers.x_baggage:
|
if headers.x_trace_id and headers.x_baggage:
|
||||||
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)
|
||||||
fn_call = fn.spawn(media=body.media, bgm=body.bgm, video_volume=body.video_volume, music_volume=body.music_volume,
|
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)
|
noise_sample=body.noise_sample, sentry_trace=sentry_trace, webhook=body.webhook)
|
||||||
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
return ModalTaskResponse(success=True, taskId=fn_call.object_id)
|
||||||
|
|
||||||
|
|
||||||
@web_app.get("/ffmpeg/subtitle-apply/{task_id}", tags=["查询任务"],
|
@web_app.get("/ffmpeg/bgm-nosie-reduce/{task_id}", tags=["查询任务"],
|
||||||
summary="查询混合BGM并降噪任务状态", description="查询混合BGM并降噪任务状态",
|
summary="查询混合BGM并降噪任务状态", description="查询混合BGM并降噪任务状态",
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_200_OK: {
|
status.HTTP_200_OK: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
class WorkerConfig(BaseSettings):
|
class WorkerConfig(BaseSettings):
|
||||||
video_downloader_concurrency: int = Field(default=10, description="处理缓存任务的并行数")
|
video_downloader_concurrency: int = Field(default=10, description="处理缓存任务的并行数")
|
||||||
ffmpeg_worker_concurrency: int = Field(default=10, description="处理视频合成任务的并行数")
|
ffmpeg_worker_concurrency: int = Field(default=10, description="处理视频合成任务的并行数")
|
||||||
|
local_output_mount: str = Field(default="/mnt/outputs", description="本地结果挂载路径前缀")
|
||||||
|
|
||||||
S3_region: str = Field(default='ap-northeast-2', description="S3挂载桶的地域")
|
S3_region: str = Field(default='ap-northeast-2', description="S3挂载桶的地域")
|
||||||
S3_bucket_name: str = Field(default='modal-media-cache', description="集群挂载的S3存储桶")
|
S3_bucket_name: str = Field(default='modal-media-cache', description="集群挂载的S3存储桶")
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ from urllib.parse import urlparse
|
|||||||
from pydantic import (BaseModel, Field, field_validator, ValidationError,
|
from pydantic import (BaseModel, Field, field_validator, ValidationError,
|
||||||
field_serializer, SerializationInfo, computed_field)
|
field_serializer, SerializationInfo, computed_field)
|
||||||
from pydantic.json_schema import JsonSchemaValue
|
from pydantic.json_schema import JsonSchemaValue
|
||||||
|
from ..config import WorkerConfig
|
||||||
|
|
||||||
# todo: 临时解决方案,需要想个办法获取本地挂载点配置
|
config = WorkerConfig()
|
||||||
s3_region = 'ap-northeast-2'
|
|
||||||
s3_bucket_name = 'modal-media-cache'
|
s3_region = config.S3_region
|
||||||
|
s3_bucket_name = config.S3_bucket_name
|
||||||
|
|
||||||
|
|
||||||
class MediaProtocol(str, Enum):
|
class MediaProtocol(str, Enum):
|
||||||
@@ -53,6 +55,8 @@ class MediaSource(BaseModel):
|
|||||||
urn=media_url)
|
urn=media_url)
|
||||||
elif media_url.startswith('s3://'): # s3://{endpoint}/{bucket}/{url}
|
elif media_url.startswith('s3://'): # s3://{endpoint}/{bucket}/{url}
|
||||||
paths = media_url[5:].split('/')
|
paths = media_url[5:].split('/')
|
||||||
|
if len(paths) < 3:
|
||||||
|
raise ValidationError("URN-s3 格式错误")
|
||||||
return MediaSource(path='/'.join(paths[2:]),
|
return MediaSource(path='/'.join(paths[2:]),
|
||||||
protocol=MediaProtocol.s3,
|
protocol=MediaProtocol.s3,
|
||||||
endpoint=paths[0],
|
endpoint=paths[0],
|
||||||
@@ -60,6 +64,8 @@ class MediaSource(BaseModel):
|
|||||||
urn=media_url)
|
urn=media_url)
|
||||||
elif media_url.startswith('vod://'): # vod://{endpoint}/{subAppId}/{fileId}
|
elif media_url.startswith('vod://'): # vod://{endpoint}/{subAppId}/{fileId}
|
||||||
paths = media_url[6:].split('/')
|
paths = media_url[6:].split('/')
|
||||||
|
if len(paths) < 3:
|
||||||
|
raise ValidationError("URN-vod 格式错误")
|
||||||
# 兼容有文件类型后缀和没有文件类型后缀的格式
|
# 兼容有文件类型后缀和没有文件类型后缀的格式
|
||||||
url = paths[2] if '.' in os.path.basename(paths[2]) else paths[2] + ".mp4"
|
url = paths[2] if '.' in os.path.basename(paths[2]) else paths[2] + ".mp4"
|
||||||
return MediaSource(path=url,
|
return MediaSource(path=url,
|
||||||
@@ -103,7 +109,7 @@ class MediaSource(BaseModel):
|
|||||||
|
|
||||||
@computed_field(description="s3挂载路径下的缓存相对路径")
|
@computed_field(description="s3挂载路径下的缓存相对路径")
|
||||||
@property
|
@property
|
||||||
def cache_filepath(self, local_mount: bool = False) -> str:
|
def cache_filepath(self) -> str:
|
||||||
match self.protocol:
|
match self.protocol:
|
||||||
case MediaProtocol.s3:
|
case MediaProtocol.s3:
|
||||||
# 本地挂载缓存
|
# 本地挂载缓存
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class WebhookNotify(BaseModel):
|
|||||||
endpoint: HttpUrl = Field(description="Webhook回调端点", examples=["https://webhook.example.com"])
|
endpoint: HttpUrl = Field(description="Webhook回调端点", examples=["https://webhook.example.com"])
|
||||||
method: WebhookMethodEnum = Field(description="Webhook回调请求方法", examples=["get", "post"])
|
method: WebhookMethodEnum = Field(description="Webhook回调请求方法", examples=["get", "post"])
|
||||||
authentication: Optional[str] = Field(description="Webhook authentication回调请求降权token,如不需要鉴权则不填",
|
authentication: Optional[str] = Field(description="Webhook authentication回调请求降权token,如不需要鉴权则不填",
|
||||||
examples=["Bearer 123456"])
|
examples=["Bearer 123456"], default=None)
|
||||||
|
|
||||||
|
|
||||||
class BaseFFMPEGTaskRequest(BaseModel):
|
class BaseFFMPEGTaskRequest(BaseModel):
|
||||||
@@ -64,6 +64,7 @@ class BaseFFMPEGTaskStatusResponse(BaseModel):
|
|||||||
status: TaskStatus = Field(description="任务运行状态")
|
status: TaskStatus = Field(description="任务运行状态")
|
||||||
error: Optional[str] = Field(description="任务错误原因", default=None)
|
error: Optional[str] = Field(description="任务错误原因", default=None)
|
||||||
code: Optional[int] = Field(description="任务错误原因代码", default=None)
|
code: Optional[int] = Field(description="任务错误原因代码", default=None)
|
||||||
|
results: Optional[List[str]] = Field(description="任务运行结果", default=None)
|
||||||
|
|
||||||
model_config = ConfigDict(extra='ignore')
|
model_config = ConfigDict(extra='ignore')
|
||||||
|
|
||||||
@@ -217,6 +218,7 @@ class FFMPEGOverlayGifTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
|||||||
class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
||||||
media: MediaSource = Field(description="需要处理的媒体源")
|
media: MediaSource = Field(description="需要处理的媒体源")
|
||||||
subtitle: MediaSource = Field(description="需要叠加的字幕文件")
|
subtitle: MediaSource = Field(description="需要叠加的字幕文件")
|
||||||
|
fonts: List[MediaSource] = Field(description="字幕文件内使用到的字体文件")
|
||||||
|
|
||||||
@field_validator('media', mode='before')
|
@field_validator('media', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -241,6 +243,21 @@ class FFMPEGSubtitleOverlayRequest(BaseFFMPEGTaskRequest):
|
|||||||
else:
|
else:
|
||||||
raise TypeError(v)
|
raise TypeError(v)
|
||||||
|
|
||||||
|
@field_validator('fonts', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]:
|
||||||
|
if not v:
|
||||||
|
return v
|
||||||
|
result = []
|
||||||
|
for item in v:
|
||||||
|
if isinstance(item, str):
|
||||||
|
result.append(MediaSource.from_str(item))
|
||||||
|
elif isinstance(item, MediaSource):
|
||||||
|
result.append(item)
|
||||||
|
else:
|
||||||
|
raise pydantic.ValidationError("fonts元素类型错误: 必须是字符串")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGSubtitleTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
class FFMPEGSubtitleTaskStatusResponse(BaseFFMPEGTaskStatusResponse):
|
||||||
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
result: Optional[str] = Field(default=None, description="生成结果的URN")
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from typing import Optional, Tuple, Any
|
import asyncio
|
||||||
|
from typing import Optional, Tuple, Any, Coroutine
|
||||||
|
import httpx
|
||||||
import psutil
|
import psutil
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
from loguru import logger
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from BowongModalFunctions.models.web_model import WebhookNotify, WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, \
|
||||||
|
TaskStatus, ErrorCode
|
||||||
|
|
||||||
|
|
||||||
class SentryUtils:
|
class SentryUtils:
|
||||||
@@ -16,6 +23,7 @@ class SentryUtils:
|
|||||||
def sentry_tracker(sentry_trace_id: Optional[str], sentry_baggage: Optional[str],
|
def sentry_tracker(sentry_trace_id: Optional[str], sentry_baggage: Optional[str],
|
||||||
op: Optional[str], name: Optional[str], fn_id: str):
|
op: Optional[str], name: Optional[str], fn_id: str):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if sentry_trace_id and sentry_baggage:
|
if sentry_trace_id and sentry_baggage:
|
||||||
transaction = sentry_sdk.continue_trace(environ_or_headers={"sentry-trace": sentry_trace_id,
|
transaction = sentry_sdk.continue_trace(environ_or_headers={"sentry-trace": sentry_trace_id,
|
||||||
@@ -38,3 +46,39 @@ class SentryUtils:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def webhook_handler(webhook: WebhookNotify, func_id: str):
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def async_wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
status = TaskStatus.success
|
||||||
|
error = None
|
||||||
|
code = ErrorCode.SUCCESS.value
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
result = None
|
||||||
|
status = TaskStatus.failed
|
||||||
|
error = e.message if hasattr(e, 'message') else str(e)
|
||||||
|
code = ErrorCode.SYSTEM_ERROR.value
|
||||||
|
logger.info(f"webhook = {webhook}")
|
||||||
|
if webhook:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
match webhook.method:
|
||||||
|
case WebhookMethodEnum.POST:
|
||||||
|
response = client.post(url=webhook.endpoint.__str__(),
|
||||||
|
json=BaseFFMPEGTaskStatusResponse(
|
||||||
|
taskId=func_id, status=status, error=error,
|
||||||
|
code=code,
|
||||||
|
results=[result] if isinstance(result, str) else result,
|
||||||
|
).model_dump())
|
||||||
|
logger.info(f"webhook response = {response}")
|
||||||
|
case WebhookMethodEnum.GET:
|
||||||
|
response = client.post(url=webhook.endpoint.__str__())
|
||||||
|
response.raise_for_status()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return async_wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|||||||
@@ -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, json, sentry_sdk
|
import shutil, psutil, os, backoff, sentry_sdk
|
||||||
from typing import List, Optional, Tuple, Dict, Any
|
from typing import List, Optional, Tuple, Any
|
||||||
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
|
||||||
@@ -26,13 +26,13 @@ with ffmpeg_worker_image.imports():
|
|||||||
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
from BowongModalFunctions.utils.VideoUtils import VideoUtils
|
||||||
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
||||||
from BowongModalFunctions.models.media_model import MediaSources, MediaSource, MediaProtocol
|
from BowongModalFunctions.models.media_model import MediaSources, MediaSource, MediaProtocol
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo
|
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify
|
||||||
from BowongModalFunctions.config import WorkerConfig
|
from BowongModalFunctions.config import WorkerConfig
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
|
|
||||||
s3_mount = config.S3_mount_dir
|
s3_mount = config.S3_mount_dir
|
||||||
output_path_prefix = "/mnt/outputs"
|
output_path_prefix = config.local_output_mount
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn="https://75cca970bfcc3d45d24361e7c0f1833c@sentry.bowongai.com/4",
|
dsn="https://75cca970bfcc3d45d24361e7c0f1833c@sentry.bowongai.com/4",
|
||||||
@@ -111,7 +111,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
# region='ap-northeast',
|
# region='ap-northeast',
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -119,7 +119,8 @@ with ffmpeg_worker_image.imports():
|
|||||||
}, )
|
}, )
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_concat_medias(medias: MediaSources,
|
async def ffmpeg_concat_medias(medias: MediaSources,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None) -> Tuple[
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None) -> Tuple[
|
||||||
str, Optional[SentryTransactionInfo]]:
|
str, Optional[SentryTransactionInfo]]:
|
||||||
|
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
@@ -127,19 +128,20 @@ with ffmpeg_worker_image.imports():
|
|||||||
@SentryUtils.sentry_tracker(name="视频合并任务", op="ffmpeg.concat", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频合并任务", op="ffmpeg.concat", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(media_sources: MediaSources, output_filepath: str) -> str:
|
async def ffmpeg_process(media_sources: MediaSources, output_filepath: str) -> str:
|
||||||
input_videos = [f"{s3_mount}/{media_source.cache_filepath}" for media_source in media_sources.inputs]
|
input_videos = [f"{s3_mount}/{media_source.cache_filepath}" for media_source in media_sources.inputs]
|
||||||
local_output_path = await VideoUtils.ffmpeg_concat_medias(media_paths=input_videos,
|
local_output_path = await VideoUtils.ffmpeg_concat_medias(media_paths=input_videos,
|
||||||
output_path=output_filepath)
|
output_path=output_filepath)
|
||||||
return local_output_path
|
s3_outputs = local_copy_to_s3([local_output_path])
|
||||||
|
return s3_outputs[0]
|
||||||
|
|
||||||
output_path = f"{output_path_prefix}/concat/outputs/{fn_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/concat/outputs/{fn_id}/output.mp4"
|
||||||
local_output = await ffmpeg_process(media_sources=medias, output_filepath=output_path)
|
s3_output = await ffmpeg_process(media_sources=medias, output_filepath=output_path)
|
||||||
s3_outputs = local_copy_to_s3([local_output])
|
|
||||||
if not sentry_trace:
|
if not sentry_trace:
|
||||||
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
||||||
x_baggage=sentry_sdk.get_baggage())
|
x_baggage=sentry_sdk.get_baggage())
|
||||||
return s3_outputs[0], sentry_trace
|
return s3_output, sentry_trace
|
||||||
|
|
||||||
|
|
||||||
@app.function(
|
@app.function(
|
||||||
@@ -147,7 +149,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
cloud="aws",
|
cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -157,14 +159,15 @@ with ffmpeg_worker_image.imports():
|
|||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_slice_media(media: MediaSource,
|
async def ffmpeg_slice_media(media: MediaSource,
|
||||||
markers: List[FFMpegSliceSegment],
|
markers: List[FFMpegSliceSegment],
|
||||||
webhook: Optional[str], # todo : handle webhook callback
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None) -> Tuple[
|
webhook: Optional[WebhookNotify] = None) -> Tuple[
|
||||||
List[str], Optional[SentryTransactionInfo]]:
|
List[str], Optional[SentryTransactionInfo]]:
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频切割任务", op="ffmpeg.slice", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频切割任务", op="ffmpeg.slice", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_slice_process(media_source: MediaSource, media_markers: List[FFMpegSliceSegment],
|
async def ffmpeg_slice_process(media_source: MediaSource, media_markers: List[FFMpegSliceSegment],
|
||||||
fn_id: str) -> List[str]:
|
fn_id: str) -> List[str]:
|
||||||
cache_filepath = f"{s3_mount}/{media_source.cache_filepath}"
|
cache_filepath = f"{s3_mount}/{media_source.cache_filepath}"
|
||||||
@@ -268,7 +271,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -277,8 +280,8 @@ with ffmpeg_worker_image.imports():
|
|||||||
)
|
)
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_extract_audio(media_source: MediaSource,
|
async def ffmpeg_extract_audio(media_source: MediaSource,
|
||||||
webhook: Optional[str], # todo handle webhook callback
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None) -> Tuple[
|
webhook: Optional[WebhookNotify] = None) -> Tuple[
|
||||||
Optional[str], Optional[SentryTransactionInfo]]:
|
Optional[str], Optional[SentryTransactionInfo]]:
|
||||||
|
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
@@ -286,9 +289,10 @@ with ffmpeg_worker_image.imports():
|
|||||||
@SentryUtils.sentry_tracker(name="视频切割任务", op="ffmpeg.extract.audio", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频切割任务", op="ffmpeg.extract.audio", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(media: MediaSource, fn_id: str) -> str:
|
async def ffmpeg_process(media: MediaSource, fn_id: str) -> str:
|
||||||
cache_filepath = f"{s3_mount}/{media.cache_filepath}"
|
cache_filepath = f"{s3_mount}/{media.cache_filepath}"
|
||||||
output_path = f"{output_path_prefix}/extract_audio/outputs/{fn_id}/output.wav"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/extract_audio/outputs/{fn_id}/output.wav"
|
||||||
await VideoUtils.ffmpeg_extract_audio_async(cache_filepath, output_path)
|
await VideoUtils.ffmpeg_extract_audio_async(cache_filepath, output_path)
|
||||||
s3_outputs = local_copy_to_s3([output_path])
|
s3_outputs = local_copy_to_s3([output_path])
|
||||||
return s3_outputs[0]
|
return s3_outputs[0]
|
||||||
@@ -304,7 +308,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -315,17 +319,19 @@ with ffmpeg_worker_image.imports():
|
|||||||
mirror_scale_down_size: int = 6,
|
mirror_scale_down_size: int = 6,
|
||||||
mirror_from_right: bool = True,
|
mirror_from_right: bool = True,
|
||||||
mirror_position: tuple[float, float] = (40, 40),
|
mirror_position: tuple[float, float] = (40, 40),
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None):
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None):
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频镜像去重", op="ffmpeg.mirror", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频镜像去重", op="ffmpeg.mirror", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(media: MediaSource, func_id: str, mirror_scale_down_size: int = 6,
|
async def ffmpeg_process(media: MediaSource, func_id: str, mirror_scale_down_size: int = 6,
|
||||||
mirror_from_right: bool = True,
|
mirror_from_right: bool = True,
|
||||||
mirror_position: tuple[float, float] = (40, 40)) -> str:
|
mirror_position: tuple[float, float] = (40, 40)) -> str:
|
||||||
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
||||||
output_path = f"{output_path_prefix}/corner_mirror/outputs/{func_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/corner_mirror/outputs/{func_id}/output.mp4"
|
||||||
local_output_filepath = await VideoUtils.ffmpeg_corner_mirror(media_path=media_filepath,
|
local_output_filepath = await VideoUtils.ffmpeg_corner_mirror(media_path=media_filepath,
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
mirror_from_right=mirror_from_right,
|
mirror_from_right=mirror_from_right,
|
||||||
@@ -346,7 +352,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -354,16 +360,18 @@ with ffmpeg_worker_image.imports():
|
|||||||
}, )
|
}, )
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_overlay_gif(media: MediaSource, gif: MediaSource,
|
async def ffmpeg_overlay_gif(media: MediaSource, gif: MediaSource,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None):
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None):
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频叠加GIF特效", op="ffmpeg.overlay", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频叠加GIF特效", op="ffmpeg.overlay", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(media: MediaSource, func_id: str, gif: MediaSource) -> str:
|
async def ffmpeg_process(media: MediaSource, func_id: str, gif: MediaSource) -> str:
|
||||||
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
||||||
gif_filepath = f"{s3_mount}/{gif.cache_filepath}"
|
gif_filepath = f"{s3_mount}/{gif.cache_filepath}"
|
||||||
output_path = f"{output_path_prefix}/overlay/outputs/{func_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/overlay/outputs/{func_id}/output.mp4"
|
||||||
local_output_filepath = await VideoUtils.ffmpeg_overlay_gif(media_path=media_filepath,
|
local_output_filepath = await VideoUtils.ffmpeg_overlay_gif(media_path=media_filepath,
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
overlay_gif_path=gif_filepath)
|
overlay_gif_path=gif_filepath)
|
||||||
@@ -380,7 +388,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -388,15 +396,17 @@ with ffmpeg_worker_image.imports():
|
|||||||
}, )
|
}, )
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_zoom_loop(media: MediaSource, duration: int = 6, zoom: float = 0.1,
|
async def ffmpeg_zoom_loop(media: MediaSource, duration: int = 6, zoom: float = 0.1,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None):
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None):
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频放大缩小去重", op="ffmpeg.zoom.loop", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频放大缩小去重", op="ffmpeg.zoom.loop", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(media: MediaSource, func_id: str, duration: int = 6, zoom: float = 0.1) -> str:
|
async def ffmpeg_process(media: MediaSource, func_id: str, duration: int = 6, zoom: float = 0.1) -> str:
|
||||||
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
media_filepath = f"{s3_mount}/{media.cache_filepath}"
|
||||||
output_path = f"{output_path_prefix}/zoom_loop/outputs/{func_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/zoom_loop/outputs/{func_id}/output.mp4"
|
||||||
local_output_filepath = await VideoUtils.ffmpeg_zoom_loop(media_path=media_filepath,
|
local_output_filepath = await VideoUtils.ffmpeg_zoom_loop(media_path=media_filepath,
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
@@ -414,7 +424,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -423,18 +433,20 @@ with ffmpeg_worker_image.imports():
|
|||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_bgm_nosie_reduce(media: MediaSource, bgm: MediaSource, noise_sample: Optional[MediaSource] = None,
|
async def ffmpeg_bgm_nosie_reduce(media: MediaSource, bgm: MediaSource, noise_sample: Optional[MediaSource] = None,
|
||||||
video_volume: float = 1.4, music_volume: float = 0.1,
|
video_volume: float = 1.4, music_volume: float = 0.1,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None):
|
sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None):
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频混合BGM降噪", op="ffmpeg.bgm.nosie.reduce", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频混合BGM降噪", op="ffmpeg.bgm.nosie.reduce", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
async def ffmpeg_process(video: MediaSource, bgm: MediaSource, func_id: str,
|
async def ffmpeg_process(video: MediaSource, bgm: MediaSource, func_id: str,
|
||||||
noise_sample: Optional[MediaSource] = None) -> str:
|
noise_sample: Optional[MediaSource] = None) -> str:
|
||||||
local_input_filepath = f"{s3_mount}/{video.cache_filepath}"
|
local_input_filepath = f"{s3_mount}/{video.cache_filepath}"
|
||||||
bgm_filepath = f"{s3_mount}/{bgm.cache_filepath}"
|
bgm_filepath = f"{s3_mount}/{bgm.cache_filepath}"
|
||||||
noise_sample_path = f"{s3_mount}/{noise_sample.cache_filepath}" if noise_sample else None
|
noise_sample_path = f"{s3_mount}/{noise_sample.cache_filepath}" if noise_sample else None
|
||||||
output_path = f"{output_path_prefix}/bgm_nosie_reduce/outputs/{func_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/bgm_nosie_reduce/outputs/{func_id}/output.mp4"
|
||||||
local_output_filepath = await VideoUtils.ffmpeg_mix_bgm_with_noise_reduce(media_path=local_input_filepath,
|
local_output_filepath = await VideoUtils.ffmpeg_mix_bgm_with_noise_reduce(media_path=local_input_filepath,
|
||||||
bgm_audio_path=bgm_filepath,
|
bgm_audio_path=bgm_filepath,
|
||||||
video_volume=video_volume,
|
video_volume=video_volume,
|
||||||
@@ -457,7 +469,7 @@ with ffmpeg_worker_image.imports():
|
|||||||
@app.function(timeout=600, cloud="aws",
|
@app.function(timeout=600, cloud="aws",
|
||||||
max_containers=config.ffmpeg_worker_concurrency,
|
max_containers=config.ffmpeg_worker_concurrency,
|
||||||
volumes={
|
volumes={
|
||||||
f"/mntS3": modal.CloudBucketMount(
|
s3_mount: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
secret=modal.Secret.from_name("aws-s3-secret",
|
secret=modal.Secret.from_name("aws-s3-secret",
|
||||||
environment_name=config.modal_environment),
|
environment_name=config.modal_environment),
|
||||||
@@ -465,26 +477,31 @@ with ffmpeg_worker_image.imports():
|
|||||||
}, )
|
}, )
|
||||||
@modal.concurrent(max_inputs=1)
|
@modal.concurrent(max_inputs=1)
|
||||||
async def ffmpeg_subtitle_apply(media: MediaSource, subtitle: MediaSource,
|
async def ffmpeg_subtitle_apply(media: MediaSource, subtitle: MediaSource,
|
||||||
sentry_trace: Optional[SentryTransactionInfo] = None):
|
fonts: List[MediaSource], sentry_trace: Optional[SentryTransactionInfo] = None,
|
||||||
|
webhook: Optional[WebhookNotify] = None):
|
||||||
fn_id = current_function_call_id()
|
fn_id = current_function_call_id()
|
||||||
|
|
||||||
@SentryUtils.sentry_tracker(name="视频叠加字幕", op="ffmpeg.subtitle.apply", fn_id=fn_id,
|
@SentryUtils.sentry_tracker(name="视频叠加字幕", op="ffmpeg.subtitle.apply", fn_id=fn_id,
|
||||||
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
|
||||||
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
|
||||||
async def ffmpeg_process(video: MediaSource, subtitle: MediaSource, func_id: str, font_dir: str) -> str:
|
@SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
|
||||||
|
async def ffmpeg_process(video: MediaSource, subtitle: MediaSource,
|
||||||
|
fonts: List[MediaSource], func_id: str) -> str:
|
||||||
media_path = f"{s3_mount}/{video.cache_filepath}"
|
media_path = f"{s3_mount}/{video.cache_filepath}"
|
||||||
subtitle_path = f"{s3_mount}/{subtitle.cache_filepath}"
|
subtitle_path = f"{s3_mount}/{subtitle.cache_filepath}"
|
||||||
output_path = f"{output_path_prefix}/subtitle_apply/{func_id}/output.mp4"
|
output_path = f"{output_path_prefix}/{config.modal_environment}/subtitle_apply/{func_id}/output.mp4"
|
||||||
|
font_dir = f"{output_path_prefix}/{config.modal_environment}/subtitle_apply/{func_id}/fonts"
|
||||||
|
os.makedirs(font_dir, exist_ok=True)
|
||||||
|
local_fonts = [f"{s3_mount}/{font.cache_filepath}" for font in fonts]
|
||||||
|
for font in local_fonts:
|
||||||
|
font_filename = os.path.basename(font)
|
||||||
|
shutil.copy(font, f"{font_dir}/{font_filename}")
|
||||||
local_output = await VideoUtils.ffmpeg_subtitle_apply(media_path=media_path, subtitle_path=subtitle_path,
|
local_output = await VideoUtils.ffmpeg_subtitle_apply(media_path=media_path, subtitle_path=subtitle_path,
|
||||||
font_dir=font_dir, output_path=output_path)
|
font_dir=font_dir, output_path=output_path)
|
||||||
|
|
||||||
s3_outputs = local_copy_to_s3([local_output])
|
s3_outputs = local_copy_to_s3([local_output])
|
||||||
return s3_outputs[0]
|
return s3_outputs[0]
|
||||||
|
|
||||||
# todo : check if subtitle use fonts exist in fonts dir
|
result = await ffmpeg_process(video=media, subtitle=subtitle, fonts=fonts, func_id=fn_id)
|
||||||
font_dir = f"{s3_mount}/fonts"
|
|
||||||
result = await ffmpeg_process(video=media, subtitle=subtitle, func_id=fn_id, font_dir=font_dir)
|
|
||||||
if not sentry_trace:
|
if not sentry_trace:
|
||||||
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
|
||||||
x_baggage=sentry_sdk.get_baggage())
|
x_baggage=sentry_sdk.get_baggage())
|
||||||
|
|||||||
@@ -38,15 +38,6 @@ with downloader_image.imports():
|
|||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo
|
from BowongModalFunctions.models.web_model import SentryTransactionInfo
|
||||||
|
|
||||||
config = WorkerConfig()
|
config = WorkerConfig()
|
||||||
# config = WorkerConfig(
|
|
||||||
# modal_app_name="bowong-ai-video-test",
|
|
||||||
# video_downloader_concurrency=10,
|
|
||||||
# ffmpeg_worker_concurrency=10,
|
|
||||||
# modal_kv_name="media-cache",
|
|
||||||
# S3_region='ap-northeast-2',
|
|
||||||
# S3_bucket_name="modal-media-cache",
|
|
||||||
# modal_environment="dev"
|
|
||||||
# )
|
|
||||||
|
|
||||||
sentry_sdk.init(dsn="https://85632fdcd62f699c2f88af6ca489e9ec@sentry.bowongai.com/3",
|
sentry_sdk.init(dsn="https://85632fdcd62f699c2f88af6ca489e9ec@sentry.bowongai.com/3",
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -186,6 +186,7 @@ dependencies = [
|
|||||||
{ name = "pyloudnorm" },
|
{ name = "pyloudnorm" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "python-ffmpeg" },
|
{ name = "python-ffmpeg" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
{ name = "scalar-fastapi" },
|
{ name = "scalar-fastapi" },
|
||||||
{ name = "sentry-sdk", extra = ["loguru"] },
|
{ name = "sentry-sdk", extra = ["loguru"] },
|
||||||
{ name = "soundfile" },
|
{ name = "soundfile" },
|
||||||
@@ -217,6 +218,7 @@ requires-dist = [
|
|||||||
{ name = "pyloudnorm", specifier = ">=0.1.1" },
|
{ name = "pyloudnorm", specifier = ">=0.1.1" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||||
{ name = "python-ffmpeg", specifier = ">=2.0.12" },
|
{ name = "python-ffmpeg", specifier = ">=2.0.12" },
|
||||||
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
{ name = "scalar-fastapi", specifier = ">=1.0.3" },
|
{ name = "scalar-fastapi", specifier = ">=1.0.3" },
|
||||||
{ name = "sentry-sdk", extras = ["loguru"], specifier = ">=2.26.1" },
|
{ name = "sentry-sdk", extras = ["loguru"], specifier = ">=2.26.1" },
|
||||||
{ name = "soundfile", specifier = ">=0.13.1" },
|
{ name = "soundfile", specifier = ">=0.13.1" },
|
||||||
|
|||||||
Reference in New Issue
Block a user