From 7ec634e33b21f0f83a8dedf9c36cdad0a483349e Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 24 Dec 2025 19:43:07 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E5=89=AA?= =?UTF-8?q?=E8=BE=91mcp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/vod/mcp_server.py | 62 +- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 639 +++++++++++++++++- .../src/vod/mcp_tools/subtitle_processing.py | 100 ++- .../src/vod/mcp_tools/upload.py | 2 +- .../src/vod/mcp_tools/video_enhancement.py | 2 +- .../src/vod/mcp_tools/video_play.py | 166 ++++- 6 files changed, 954 insertions(+), 17 deletions(-) diff --git a/server/mcp_server_vod/src/vod/mcp_server.py b/server/mcp_server_vod/src/vod/mcp_server.py index b4036ef1..4bd9b33e 100644 --- a/server/mcp_server_vod/src/vod/mcp_server.py +++ b/server/mcp_server_vod/src/vod/mcp_server.py @@ -41,13 +41,16 @@ "intelligent_slicing", # 智能抠图相关tools "intelligent_matting", - # 字幕处理相关tools - "subtitle_processing", + # # 字幕处理相关tools + # "subtitle_processing", # 音频处理相关tools "audio_processing", # 视频增强相关tools "video_enhancement", } + +ALL_GROUPS = 'all' + def create_mcp_server(groups: list[str] = None, mcp: FastMCP = None): ## init api client service = VodAPI() @@ -57,19 +60,25 @@ def create_mcp_server(groups: list[str] = None, mcp: FastMCP = None): ## init tool groups current_tool_groups = [] env_type = os.getenv("MCP_TOOL_GROUPS") - + if groups is not None: - current_tool_groups = groups + if ALL_GROUPS in groups: + current_tool_groups = AVAILABLE_GROUPS + else: + current_tool_groups = groups elif env_type is not None: + env_grops= [group.strip() for group in env_type.split(",") if group.strip()] try: - current_tool_groups = [group.strip() for group in env_type.split(",") if group.strip()] + if ALL_GROUPS in env_grops: + current_tool_groups = AVAILABLE_GROUPS + else: + current_tool_groups = env_grops print(f"[MCP] Loaded tool groups from environment: {current_tool_groups}") except Exception as e: print(f"[MCP] Error parsing MCP_TOOL_GROUPS environment variable: {e}") current_tool_groups = DEFAULT_GROUPS else: current_tool_groups = DEFAULT_GROUPS - ## update media publish status @@ -103,7 +112,14 @@ def get_play_video_info (vid: str, SpaceName: str, OutputType: str = 'CDN') -> Returns: - PlayURL (str): 视频播放地址. - - Duration (int): 视频时长,单位秒. + - Duration (float): 视频时长,单位秒. + - FormatName (str): 容器名称. + - Size (float): 大小,单位为字节. + - BitRate (str): 码率,单位为 bps. + - CodecName (str): 编码器名称. + - AvgFrameRate (str): 视频平均帧率,单位为 fps. + - Width (int): 视频宽,单位为 px. + - Height (int): 视频高,单位为 px. """ reqs = service.mcp_get("McpGetVideoPlayInfo", { @@ -114,6 +130,13 @@ def get_play_video_info (vid: str, SpaceName: str, OutputType: str = 'CDN') -> },json.dumps({})) url = None duration = 0 + formatName = "" + size = 0 + bitRate = "" + codecName = "" + avgFrameRate = "" + width = 0 + height = 0 if isinstance(reqs, str): reqs = json.loads(reqs) @@ -121,7 +144,18 @@ def get_play_video_info (vid: str, SpaceName: str, OutputType: str = 'CDN') -> videoDetail = result.get("VideoDetail", {}) videoDetailInfo = videoDetail.get("VideoDetailInfo", {}) playInfo = videoDetailInfo.get("PlayInfo", {}) - duration = videoDetailInfo.get("Duration", 0) + durationValue = videoDetailInfo.get("Duration") + duration = float(durationValue) if durationValue is not None else 0 + + # 提取完整的视频信息 + formatName = videoDetailInfo.get("Format", "") + sizeValue = videoDetailInfo.get("Size") + size = float(sizeValue) if sizeValue is not None else 0 + bitRate = str(videoDetailInfo.get("Bitrate", "")) if videoDetailInfo.get("Bitrate") else "" + codecName = videoDetailInfo.get("Codec", "") + avgFrameRate = str(videoDetailInfo.get("Fps", "")) if videoDetailInfo.get("Fps") else "" + width = int(videoDetailInfo.get("Width", 0)) if videoDetailInfo.get("Width") else 0 + height = int(videoDetailInfo.get("Height", 0)) if videoDetailInfo.get("Height") else 0 if videoDetailInfo.get("PublishStatus") == 'Published': url = playInfo.get("MainPlayURL", None) or playInfo.get("BackupPlayUrl", None) @@ -139,7 +173,17 @@ def get_play_video_info (vid: str, SpaceName: str, OutputType: str = 'CDN') -> raise Exception("update publish status failed:", reqs, publishStatus) if url is None: raise Exception("%s: get publish url failed" % vid) - return json.dumps({"PlayURL": url, "Duration": duration}) + return json.dumps({ + "PlayURL": url, + "Duration": duration, + "FormatName": formatName, + "Size": size, + "BitRate": bitRate, + "CodecName": codecName, + "AvgFrameRate": avgFrameRate, + "Width": width, + "Height": height, + }) public_methods["update_media_publish_status"] = update_media_publish_status public_methods["get_play_video_info"] = get_play_video_info diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index d1a6b4a8..68e7b28c 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -1,5 +1,30 @@ import json from src.vod.api.api import VodAPI + + +def _format_source(type: str, source: str) -> str: + """根据 type 自动添加前缀到 source + Args: + type: 文件类型,vid、directurl、http + source: 文件信息 + Returns: + 格式化后的 source + """ + if not source: + return source + + # 如果已经包含协议前缀,直接返回 + if source.startswith(('vid://', 'directurl://', 'http://', 'https://')): + return source + + # 根据 type 添加前缀 + if type == 'vid': + return f'vid://{source}' + elif type == 'directurl': + return f'directurl://{source}' + # http 类型不需要添加前缀,直接返回 + return source + def create_mcp_server(mcp,public_methods: dict, service: VodAPI, ): @mcp.tool() @@ -64,9 +89,21 @@ def audio_video_stitching(type: str, SpaceName: str, videos: list = None, audios raise TypeError("audio_video_stitching: params['audios'] must be a list") if not params["audios"]: raise ValueError("audio_video_stitching: params['audios'] must contain at least one audio") + # 格式化音频列表 + formattedAudios = [] + for audio in params.get("audios", []): + if isinstance(audio, str): + # 如果已经是完整格式,直接使用 + if audio.startswith(('vid://', 'directurl://', 'http://', 'https://')): + formattedAudios.append(audio) + else: + # 默认使用 vid 格式 + formattedAudios.append(_format_source("vid", audio)) + else: + formattedAudios.append(audio) ParamObj = { "space_name": params["SpaceName"], - "audios": params.get("audios", []), + "audios": formattedAudios, } WorkflowId = "loki://158487089" else: @@ -76,9 +113,21 @@ def audio_video_stitching(type: str, SpaceName: str, videos: list = None, audios raise Exception("audio_video_stitching: params['videos'] must be a list", params) if not params["videos"]: raise Exception("audio_video_stitching: params['videos'] must contain at least one video", params) + # 格式化视频列表 + formattedVideos = [] + for video in params.get("videos", []): + if isinstance(video, str): + # 如果已经是完整格式,直接使用 + if video.startswith(('vid://', 'directurl://', 'http://', 'https://')): + formattedVideos.append(video) + else: + # 默认使用 vid 格式 + formattedVideos.append(_format_source("vid", video)) + else: + formattedVideos.append(video) ParamObj = { "space_name": params["SpaceName"], - "videos": params.get("videos", []), + "videos": formattedVideos, "transitions": params.get("transitions", []), } WorkflowId = "loki://154775772" @@ -145,9 +194,13 @@ def audio_video_clipping(type: str, SpaceName: str, source: str, start_time: flo startTime = params.get("start_time", 0) endTime = params.get("end_time", startTime + 1) + sourceType = params.get("type", "video") + sourceValue = params.get("source", "") + # 根据 type 自动添加前缀 + formattedSource = _format_source(sourceType, sourceValue) if sourceType in ["vid", "directurl"] else sourceValue ParamObj = { "space_name": params["SpaceName"], - "source": params.get("source", ""), + "source": formattedSource, "end_time": endTime, "start_time": startTime, } @@ -263,4 +316,582 @@ def get_v_creative_task_result(VCreativeId: str, SpaceName: str) -> dict: return json.dumps(reqsTmp) else: return reqs - + + @mcp.tool() + def flip_video(type: str, source: str, space_name: str, flip_x: bool = False, flip_y: bool = False) -> dict: + """Video rotation capability is supported, allowing for `vertical and horizontal flipping of the video`. ** Default: No flipping ** + Args: + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - flip_x(bool): ** 非必选字段 **, 是否对视频进行上下翻转。Boolean 类型,默认值为 false,表示不翻转。 + - flip_y(bool): ** 非必选字段 **, 是否对视频进行左右翻转。Boolean 类型,默认值为 false,表示不翻转。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"type": type, "source": source, "space_name": space_name, "flip_x": flip_x, "flip_y": flip_y} + if "space_name" not in params: + raise ValueError("flip_video: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("flip_video: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("flip_video: params['space_name'] cannot be empty") + if "source" not in params: + raise ValueError("flip_video: params must contain source") + + formattedSource = _format_source(params.get("type", "vid"), params.get("source", "")) + ParamObj = { + "space_name": params["space_name"], + "source": formattedSource, + "flip_x": params.get("flip_x", False), + "flip_y": params.get("flip_y", False), + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://165221855", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("flip_video: %s" % e, params) + except Exception as e: + raise Exception("flip_video: %s" % e, params) + + @mcp.tool() + def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) -> dict: + """Adjust the speed multiplier of the video, of type Float, with a range from 0.1 to 4. + Args: + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - speed(float): ** 非必选字段 **, 调整速度的倍数,Float类型,取值范围为** 0.1~4 **。参考如下: + - 0.1:放慢至原速的 0.1 倍。 + - 1(默认值):原速。 + - 4:加速至原速的 4 倍。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"type": type, "source": source, "space_name": space_name, "speed": speed} + if "space_name" not in params: + raise ValueError("speedup_video: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("speedup_video: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("speedup_video: params['space_name'] cannot be empty") + if "source" not in params: + raise ValueError("speedup_video: params must contain source") + + speedValue = params.get("speed", 1.0) + if speedValue < 0.1 or speedValue > 4: + raise ValueError("speedup_video: speed must be between 0.1 and 4") + + formattedSource = _format_source(params.get("type", "vid"), params.get("source", "")) + ParamObj = { + "space_name": params["space_name"], + "source": formattedSource, + "speed": speedValue, + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://165223469", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("speedup_video: %s" % e, params) + except Exception as e: + raise Exception("speedup_video: %s" % e, params) + + @mcp.tool() + def image_to_video(images: list, space_name: str, transitions: list = None) -> dict: + """The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition ** + Args: + - images(list[dict]): ** 必选字段 **,待合成的图片列表,子类型取值如下 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 图片文件信息 + - duration(float): ** 非必选字段 **, 图片播放时长,`默认值:3,单位:秒,支持 2 位小数`。 + - animation_type(str): ** 非必选字段 **, 图片的动画类型,选填,不填时无动画效果,取值如下: + - move_up:向上移动 + - move_down:向下移动 + - move_left:向左移动 + - move_right:向右移动 + - zoom_in:缩小 + - zoom_out:放大 + - animation_in(float): ** 非必选字段 **, 动画结束时间,支持2位小数。默认为图片展示时长,表示动画随图片播放同时结束,单位:秒。 + - animation_out(float): ** 非必选字段 **, 动画结束时间,支持2位小数。默认为图片展示时长,表示动画随图片播放同时结束,单位:秒。 + - transitions(list[str]): 非必选字段,转场效果 ID:例如 ["1182359"]。concat_videos 工具支持非交叠转场的效果, + - 可用于非交叠转场的动画 + - 分类:交替出场,ID:1182359 + - 分类:旋转放大,ID:1182360 + - 分类:泛开,ID:1182358 + - 分类:六角形,ID:1182365 + - 分类:故障转换,ID:1182367 + - 分类:飞眼,ID:1182368 + - 分类:梦幻放大,ID:1182369 + - 分类:开门展现,ID:1182370 + - 分类:立方转换,ID:1182373 + - 分类:透镜变换,ID:1182374 + - 分类:晚霞转场,ID:1182375 + - 分类:圆形交替,ID:1182378 + - 注意:如果不提供,则没有转场 + - 当视频数量超过转场数量 2 个及以上时,系统将自动循环使用转场。例如有 10 个视频,2 种转场效果,那么在 9 处拼接点上,这 2 种转场效果将被依次循环使用。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"images": images, "space_name": space_name, "transitions": transitions} + if "space_name" not in params: + raise ValueError("image_to_video: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("image_to_video: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("image_to_video: params['space_name'] cannot be empty") + if "images" not in params: + raise ValueError("image_to_video: params must contain images") + if not isinstance(params["images"], list): + raise TypeError("image_to_video: params['images'] must be a list") + if not params["images"]: + raise ValueError("image_to_video: params['images'] must contain at least one image") + + # 格式化图片列表 + formattedImages = [] + for image in params.get("images", []): + if isinstance(image, dict): + imgType = image.get("type", "vid") + imgSource = image.get("source", "") + formattedSource = _format_source(imgType, imgSource) if imgType in ["vid", "directurl"] else imgSource + formattedImage = { + "type": imgType, + "source": formattedSource, + } + if "duration" in image: + formattedImage["duration"] = image["duration"] + if "animation_type" in image: + formattedImage["animation_type"] = image["animation_type"] + if "animation_in" in image: + formattedImage["animation_in"] = image["animation_in"] + if "animation_out" in image: + formattedImage["animation_out"] = image["animation_out"] + formattedImages.append(formattedImage) + else: + formattedImages.append(image) + + ParamObj = { + "space_name": params["space_name"], + "images": formattedImages, + "transitions": params.get("transitions", []), + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://167979998", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("image_to_video: %s" % e, params) + except Exception as e: + raise Exception("image_to_video: %s" % e, params) + + @mcp.tool() + def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: + """The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing. + Args: + - video(dict): ** 必选字段 **,视频信息 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - audio(dict): ** 必选字段 **,音频信息 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 音频文件信息 + - is_audio_reserve(bool): ** 非必选字段 **, 是否保留原视频流中的音频,取值参考如下: + - true:保留。默认值; + - false:不保留。 + - is_video_audio_sync(bool): ** 非必选字段 **, 是否对齐音频和视频时长,取值参考如下: + - true:通过 output_sync 配置,对齐音频和视频时长。 + - false(默认值):保持原样输出,不做音视频对齐。最终合成的视频时长,以较长的流为准。 + - sync_mode(str): ** 非必选字段 **, ** 设置 is_video_audio_sync 为 true 时生效 **;当音频和视频时长不相等时,可指定对齐基准,可选项:video、audio。 + - video:【默认值】以视频的时长为准。 + - audio:以音频的时长为准。 + - sync_method(str): ** 非必选字段 **, **设置 is_video_audio_sync 为 true 时生效**;指定对齐方式,支持通过裁剪或加速的方式,对齐音频和视频的时长。可选项:speed、trim。 + - speed:通过加快音频或视频的速度,对齐音频和视频的时长。 + - trim:【默认值】通过裁剪音频或视频,对齐音频和视频的时长。从头开始计算并裁剪。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"video": video, "audio": audio, "space_name": space_name, "is_audio_reserve": is_audio_reserve, "is_video_audio_sync": is_video_audio_sync, "sync_mode": sync_mode, "sync_method": sync_method} + if "space_name" not in params: + raise ValueError("compile_video_audio: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("compile_video_audio: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("compile_video_audio: params['space_name'] cannot be empty") + if "video" not in params: + raise ValueError("compile_video_audio: params must contain video") + if not isinstance(params["video"], dict): + raise TypeError("compile_video_audio: params['video'] must be a dict") + if "audio" not in params: + raise ValueError("compile_video_audio: params must contain audio") + if not isinstance(params["audio"], dict): + raise TypeError("compile_video_audio: params['audio'] must be a dict") + + # 格式化视频和音频 + videoType = params["video"].get("type", "vid") + videoSource = params["video"].get("source", "") + formattedVideoSource = _format_source(videoType, videoSource) if videoType in ["vid", "directurl"] else videoSource + + audioType = params["audio"].get("type", "vid") + audioSource = params["audio"].get("source", "") + formattedAudioSource = _format_source(audioType, audioSource) if audioType in ["vid", "directurl"] else audioSource + + ParamObj = { + "space_name": params["space_name"], + "video": formattedVideoSource, + "audio": formattedAudioSource, + "is_audio_reserve": params.get("is_audio_reserve", True), + "is_video_audio_sync": params.get("is_video_audio_sync", False), + } + + if params.get("is_video_audio_sync", False): + ParamObj["sync_mode"] = params.get("sync_mode", "video") + ParamObj["sync_method"] = params.get("sync_method", "trim") + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://167984726", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("compile_video_audio: %s" % e, params) + except Exception as e: + raise Exception("compile_video_audio: %s" % e, params) + + @mcp.tool() + def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") -> dict: + """Audio extraction, outputting the audio format. Supports mp3 and m4a formats. Default is m4a. + Args: + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - format(str): ** 非必选字段 **, 输出音频的格式,支持 mp3、m4a 格式。默认 m4a + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"type": type, "source": source, "space_name": space_name, "format": format} + if "space_name" not in params: + raise ValueError("extract_audio: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("extract_audio: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("extract_audio: params['space_name'] cannot be empty") + if "source" not in params: + raise ValueError("extract_audio: params must contain source") + + formatValue = params.get("format", "m4a") + if formatValue not in ["mp3", "m4a"]: + raise ValueError("extract_audio: format must be mp3 or m4a") + + formattedSource = _format_source(params.get("type", "vid"), params.get("source", "")) + ParamObj = { + "space_name": params["space_name"], + "source": formattedSource, + "format": formatValue, + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://167986559", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("extract_audio: %s" % e, params) + except Exception as e: + raise Exception("extract_audio: %s" % e, params) + + @mcp.tool() + def mix_audios(audios: list, space_name: str) -> dict: + """Mix audios + Args: + - audios(list[dict]): ** 必选字段 **,叠加的音频列表 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 音频文件信息 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"audios": audios, "space_name": space_name} + if "space_name" not in params: + raise ValueError("mix_audios: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("mix_audios: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("mix_audios: params['space_name'] cannot be empty") + if "audios" not in params: + raise ValueError("mix_audios: params must contain audios") + if not isinstance(params["audios"], list): + raise TypeError("mix_audios: params['audios'] must be a list") + if not params["audios"]: + raise ValueError("mix_audios: params['audios'] must contain at least one audio") + + # 格式化音频列表 + formattedAudios = [] + for audio in params.get("audios", []): + if isinstance(audio, dict): + audioType = audio.get("type", "vid") + audioSource = audio.get("source", "") + formattedSource = _format_source(audioType, audioSource) if audioType in ["vid", "directurl"] else audioSource + formattedAudios.append(formattedSource) + else: + formattedAudios.append(audio) + + ParamObj = { + "space_name": params["space_name"], + "audios": formattedAudios, + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://167987532", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("mix_audios: %s" % e, params) + except Exception as e: + raise Exception("mix_audios: %s" % e, params) + + @mcp.tool() + def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: dict = None) -> dict: + """`水印贴片`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. + Note: + - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 + - pos_x 像素 + - pos_y 像素 + Args: + - video(dict): ** 必选字段 **,视频信息 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - sub_video(dict): ** 必选字段 **, 水印贴片信息 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - sub_options(dict): ** 非必选字段 **,水印贴片叠加选项信息,包含如下字段: + - height(str): 水印的高度,支持设置为百分比(相对于视频高度)或具体像素值,例如 100% 或 100。 + - width(str): 水印的宽度,支持设置为百分比(相对于视频高度)或具体像素值,String 类型,例如 100% 或 100。 + - pos_x(str): 水印在水平方向(X 轴)的位置,以视频左上角为原点,单位:像素。例如值为 0 时,表示水印处于水平方向的最左侧;值为 100 时,表示水印相对原点向右移动 100 像素。 + - pos_y(str): 水印在垂直方向(Y 轴)的位置,以视频左上角为原点,单位:像素,例如值为 0 时,表示水印在垂直方向的最上侧;值为 100 时,表示水印相对原点向下移动 100 像素。 + - start_time(float): 水印的开始时间,单位:秒。 + - end_time(float): 水印的结束时间,单位:秒。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"video": video, "sub_video": sub_video, "space_name": space_name, "sub_options": sub_options} + if "space_name" not in params: + raise ValueError("add_sub_video: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("add_sub_video: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("add_sub_video: params['space_name'] cannot be empty") + if "video" not in params: + raise ValueError("add_sub_video: params must contain video") + if not isinstance(params["video"], dict): + raise TypeError("add_sub_video: params['video'] must be a dict") + if "sub_video" not in params: + raise ValueError("add_sub_video: params must contain sub_video") + if not isinstance(params["sub_video"], dict): + raise TypeError("add_sub_video: params['sub_video'] must be a dict") + + # 格式化视频和水印视频 + videoType = params["video"].get("type", "vid") + videoSource = params["video"].get("source", "") + formattedVideoSource = _format_source(videoType, videoSource) if videoType in ["vid", "directurl"] else videoSource + + subVideoType = params["sub_video"].get("type", "vid") + subVideoSource = params["sub_video"].get("source", "") + formattedSubVideoSource = _format_source(subVideoType, subVideoSource) if subVideoType in ["vid", "directurl"] else subVideoSource + + ParamObj = { + "space_name": params["space_name"], + "video": formattedVideoSource, + "sub_video": formattedSubVideoSource, + } + + if params.get("sub_options"): + ParamObj["sub_options"] = params["sub_options"] + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://168021310", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("add_sub_video: %s" % e, params) + except Exception as e: + raise Exception("add_sub_video: %s" % e, params) + + diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index 512aad9e..f265fb34 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -1,6 +1,8 @@ from typing import Any +from src.vod.mcp_tools.edit import _format_source +import json -def create_mcp_server(mcp, public_methods: dict): +def create_mcp_server(mcp, public_methods: dict,service): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] @@ -113,6 +115,102 @@ def video_subtitles_removal_task(type: str, video: str, spaceName: str) -> Any: } return _start_execution(params) + @mcp.tool() + def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_list: list = None, subtitle_config: dict = None) -> dict: + """Add subtitle functionality, supporting both subtitle file (subtitle_url) and subtitle list (text_list) methods. However, subtitle_url and text_list must specify that subtitle_url has a higher priority. + Note: + - subtitle_url 和 text_list 必须指定一个,subtitle_url 优先级更高 + Args: + - video(dict): ** 必选字段 **,视频信息 + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - subtitle_url(str): ** 非必选字段 **, 字幕文件 URL、filename,常见的字幕文件为 SRT、VTT、ASS 等格式。` subtitle_url和text_list必须指定一个subtitle_url优先级更高 ` + - text_list(list[dict]): ** 非必选字段 **, 字幕列表 + - text(str): 输入文本 + - start_time(float): 文本开始时间,单位s + - end_time(float): 文本结束时间,单位s + - subtitle_config(dict): ** 非必选字段 **,字幕配置信息,包含如下字段: + - font_size(int): 字幕的字体大小,Integer 类型,单位:像素。默认200 + - font_type(str): 字幕的字体 ID,String 类型,详情请参考字体 ID。默认 SY_BLACK(思源黑体) + - font_color(str): 字幕的字体颜色,String 类型,RGBA 格式,默认白色 #FFFFFFFF。 + - background_border_width(number): 字幕背景的边框宽度,Number 类型,单位:像素。 + - background_color(str): 字幕的背景颜色,String 类型,RGBA 格式,默认 #00000000。 + - border_color(str): 字幕内容描边的颜色,String 类型,RGBA 格式,默认 #00000000。 + - border_width(int): 字幕内容描边的的宽度,Integer 类型,单位:像素。默认0。 + - font_pos_config(dict): 字幕位置配置 + - width(str): 字幕的宽度,支持设置为百分比(相对于视频宽度)或具体像素值,String 类型,例如 100% 或 1000。 + - height(str): 字幕的高度,支持设置为百分比(相对于视频高度)或具体像素值,String 类型,例如 10% 或 100。 + - pos_x(str): 字幕在水平方向(X 轴)的位置,以视频正上方居中位置为原点,单位:像素,String 类型。例如值为 0 时,表示字幕在水平位置居中;值为 - 100 时,表示字幕向左移动 100 像素;值为 100 时,表示字幕向右移动 100 像素。 + - pos_y(str): 水印在垂直方向(Y 轴)的位置,以视频正上方居中位置为原点,单位:像素,String 类型。例如值为 0 时,表示字幕在视频顶部;值为 100 时,表示字幕向下移动 100 像素。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"video": video, "space_name": space_name, "subtitle_url": subtitle_url, "text_list": text_list, "subtitle_config": subtitle_config} + if "space_name" not in params: + raise ValueError("add_subtitle: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("add_subtitle: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("add_subtitle: params['space_name'] cannot be empty") + if "video" not in params: + raise ValueError("add_subtitle: params must contain video") + if not isinstance(params["video"], dict): + raise TypeError("add_subtitle: params['video'] must be a dict") + if not params.get("subtitle_url") and not params.get("text_list"): + raise ValueError("add_subtitle: params must contain either subtitle_url or text_list") + + # 格式化视频 + videoType = params["video"].get("type", "vid") + videoSource = params["video"].get("source", "") + formattedVideoSource = _format_source(videoType, videoSource) if videoType in ["vid", "directurl"] else videoSource + + ParamObj = { + "space_name": params["space_name"], + "video": formattedVideoSource, + } + + if params.get("subtitle_url"): + ParamObj["subtitle_url"] = params["subtitle_url"] + elif params.get("text_list"): + ParamObj["text_list"] = params["text_list"] + + if params.get("subtitle_config"): + ParamObj["subtitle_config"] = params["subtitle_config"] + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://168214785", + } + + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("add_subtitle: %s" % e, params) + except Exception as e: + raise Exception("add_subtitle: %s" % e, params) + diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index 921f99d9..3eb42f1a 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -5,7 +5,7 @@ def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] @mcp.tool() def video_batch_upload(space_name: str, urls: list[dict[str, any]] = None, ) -> dict: - """ Batch retrieval and upload of URLs upload video to specified space via synchronous upload + """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 - 本接口为异步上传接口。上传任务成功提交后,系统会生成异步执行的任务,排队执行,不保证时效性。 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py index ddcc7159..20089f44 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py @@ -126,7 +126,7 @@ def video_interlacing_task(type: str, video: str, spaceName: str, Fps: float) -> Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,输入 type 为 `videSuperResolution` """ - if not isinstance(Fps, (int, float)) or Fps <= 0 or Fps >= 120: + if not isinstance(Fps, (int, float)) or Fps <= 0 or Fps > 120: raise ValueError("Fps must be > 0 and <= 120") media_input = _build_media_input(type, video, spaceName) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 779f32da..5118d81d 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -5,10 +5,12 @@ from typing import Any, Dict from datetime import datetime, timezone, timedelta import hashlib -from urllib.parse import quote +from urllib.parse import quote, urlparse, urlencode, parse_qs, urlunparse import secrets import json import re +import urllib.request +import urllib.error def register_video_play_methods(service: VodAPI, public_methods: dict,): def str_to_number(s, default=None): @@ -219,6 +221,92 @@ def get_play_directurl(spaceName: str, fileName: str, expired_minutes: int = 60) return urlPath public_methods["get_play_url"] = get_play_directurl + def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: + """通过 directurl 模式获取音视频元数据 + Args: + spaceName: 空间名称 + source: 文件名 + Returns: + 音视频元数据字典 + """ + # 获取播放地址 + playUrl = get_play_directurl(spaceName, source, 60) + if not playUrl: + raise Exception("get_video_audio_info: failed to get play url") + + # 在链接上拼接 x-vod-process=video/info + parsed = urlparse(playUrl) + query_params = parse_qs(parsed.query) + query_params['x-vod-process'] = ['video/info'] + new_query = urlencode(query_params, doseq=True) + info_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, new_query, parsed.fragment)) + + # 发起 GET 请求 + try: + req = urllib.request.Request(info_url) + with urllib.request.urlopen(req, timeout=30) as response: + result_data = json.loads(response.read().decode('utf-8')) + except urllib.error.URLError as e: + raise Exception(f"get_video_audio_info: failed to fetch video info: {e}") + except json.JSONDecodeError as e: + raise Exception(f"get_video_audio_info: failed to parse JSON response: {e}") + + # 解析结果 + format_info = result_data.get("format", {}) + streams = result_data.get("streams", []) + + # 提取视频流信息 + video_stream = None + audio_stream = None + for stream in streams: + codec_type = stream.get("codec_type", "") + if codec_type == "video" and video_stream is None: + video_stream = stream + elif codec_type == "audio" and audio_stream is None: + audio_stream = stream + + # 构建返回结果 + durationValue = format_info.get("duration") + duration = float(durationValue) if durationValue is not None else 0 + sizeValue = format_info.get("size") + size = float(sizeValue) if sizeValue is not None else 0 + + result = { + "FormatName": format_info.get("format_name", ""), + "Duration": duration, + "Size": size, + "BitRate": format_info.get("bit_rate", ""), + "CodecName": "", + "AvgFrameRate": "", + "Width": 0, + "Height": 0, + "Channels": 0, + "SampleRate": "", + "BitsPerSample": "", + "PlayURL": playUrl, + } + + # 填充视频信息 + if video_stream: + result["CodecName"] = video_stream.get("codec_name", "") + result["AvgFrameRate"] = video_stream.get("avg_frame_rate", "") + result["Width"] = int(video_stream.get("width", 0)) if video_stream.get("width") else 0 + result["Height"] = int(video_stream.get("height", 0)) if video_stream.get("height") else 0 + if not result["BitRate"]: + result["BitRate"] = str(video_stream.get("bit_rate", "")) + + # 填充音频信息 + if audio_stream: + result["Channels"] = int(audio_stream.get("channels", 0)) if audio_stream.get("channels") else 0 + result["SampleRate"] = str(audio_stream.get("sample_rate", "")) + bits_per_sample = audio_stream.get("bits_per_sample") + if bits_per_sample: + result["BitsPerSample"] = str(bits_per_sample) + + return result + + public_methods["get_video_audio_info_directurl"] = get_video_audio_info_directurl + def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): @mcp.tool() @@ -234,4 +322,80 @@ def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> st """ return public_methods["get_play_url"](spaceName, fileName, expired_minutes) + @mcp.tool() + def get_video_audio_info(type: str, source: str, space_name: str) -> dict: + """Obtaining audio and video metadata + Note: + - ** directurl 模式:仅支持点播存储 ** + - ** vid 模式:通过 get_play_video_info 获取数据 ** + Args: + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl:仅仅支持点播存储 + - vid + - source(str): 文件信息 + - space_name(str): ** 必选字段 ** , 点播空间 + Returns: + - FormatName(str): 容器名称。 + - Duration(float): 时长,单位为秒。 + - Size(float): 大小,单位为字节。 + - BitRate(str): 码率,单位为 bps + - CodecName(str): 编码器名称。 + - AvgFrameRate(str): 视频平均帧率,单位为 fps。 + - Width(int): 视频宽,单位为 px。 + - Height(int): 视频高,单位为 px。 + - Channels(int): 音频通道数 + - SampleRate(str): 音频采样率,单位 Hz。 + - BitsPerSample(str): 音频采样码率,单位 bit。 + - PlayURL(str): 播放地址 + """ + try: + params = {"type": type, "source": source, "space_name": space_name} + if "space_name" not in params: + raise ValueError("get_video_audio_info: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("get_video_audio_info: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("get_video_audio_info: params['space_name'] cannot be empty") + if "source" not in params: + raise ValueError("get_video_audio_info: params must contain source") + + sourceType = params.get("type", "vid") + sourceValue = params.get("source", "") + + if sourceType == "directurl": + # directurl 模式 + result = public_methods["get_video_audio_info_directurl"](params["space_name"], sourceValue) + return json.dumps(result) + elif sourceType == "vid": + # vid 模式 - 直接使用 get_play_video_info 的返回结果 + videoInfo = public_methods["get_play_video_info"](sourceValue, params["space_name"]) + if isinstance(videoInfo, str): + videoInfo = json.loads(videoInfo) + + durationValue = videoInfo.get("Duration") + duration = float(durationValue) if durationValue is not None else 0 + sizeValue = videoInfo.get("Size") + size = float(sizeValue) if sizeValue is not None else 0 + + result = { + "FormatName": videoInfo.get("FormatName", ""), + "Duration": duration, + "Size": size, + "BitRate": videoInfo.get("BitRate", ""), + "CodecName": videoInfo.get("CodecName", ""), + "AvgFrameRate": videoInfo.get("AvgFrameRate", ""), + "Width": int(videoInfo.get("Width", 0)) if videoInfo.get("Width") else 0, + "Height": int(videoInfo.get("Height", 0)) if videoInfo.get("Height") else 0, + "Channels": 0, + "SampleRate": "", + "BitsPerSample": "", + "PlayURL": videoInfo.get("PlayURL", ""), + } + + return json.dumps(result) + else: + raise ValueError(f"get_video_audio_info: unsupported type: {sourceType}") + except Exception as e: + raise Exception("get_video_audio_info: %s" % e, params) + From 7af2774a7becd859b12cbdb1a31e05fe74d90c29 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 24 Dec 2025 22:32:45 +0800 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E5=89=AA?= =?UTF-8?q?=E8=BE=91mcp=20=E5=A2=9E=E5=8A=A0host=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp_server_vod/src/base/base_service.py | 2 ++ .../mcp_server_vod/src/vod/mcp_tools/edit.py | 11 +++++++--- .../src/vod/mcp_tools/subtitle_processing.py | 22 +++++++++++++++++-- .../src/vod/mcp_tools/video_play.py | 1 + 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_service.py b/server/mcp_server_vod/src/base/base_service.py index 02e722ea..b5443376 100644 --- a/server/mcp_server_vod/src/base/base_service.py +++ b/server/mcp_server_vod/src/base/base_service.py @@ -18,6 +18,8 @@ def __init__(self): self.set_sk(credentials.secret_access_key) self.set_session_token(credentials.session_token) self.service_info.header["x-tt-mcp"] = 'volc' + if os.getenv("VOLCENGINE_HOST"): + self.set_host(os.getenv("VOLCENGINE_HOST")) self.mcp_state = {} @staticmethod diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index 68e7b28c..72199936 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -804,8 +804,6 @@ def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: di """`水印贴片`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. Note: - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 - - pos_x 像素 - - pos_y 像素 Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -866,7 +864,14 @@ def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: di } if params.get("sub_options"): - ParamObj["sub_options"] = params["sub_options"] + + ParamObj["sub_options"] = { + "width": params["sub_options"].get("width", "20%"), + "height": params["sub_options"].get("height", "20%"), + "pos_x": params["sub_options"].get("pos_x", "0"), + "pos_y": params["sub_options"].get("pos_y", "0"), + **params["sub_options"], + } audioVideoStitchingParams = { "ParamObj": ParamObj, diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index f265fb34..71d8efdc 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -2,6 +2,13 @@ from src.vod.mcp_tools.edit import _format_source import json +def_font_pos_config = { + "height": "20%", + "pos_x": "5%", + "pos_y": "68%", + "width": "90%" +} + def create_mcp_server(mcp, public_methods: dict,service): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] @@ -183,8 +190,19 @@ def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_li ParamObj["text_list"] = params["text_list"] if params.get("subtitle_config"): - ParamObj["subtitle_config"] = params["subtitle_config"] - + # 获取用户提供的 font_pos_config,如果不存在则使用空字典 + user_font_pos_config = params["subtitle_config"].get("font_pos_config", {}) + # 合并默认配置和用户配置,用户配置优先级更高 + merged_font_pos_config = {**def_font_pos_config, **user_font_pos_config} + ParamObj["subtitle_config"] = { + **params["subtitle_config"], + "font_pos_config": merged_font_pos_config + } + else: + ParamObj["subtitle_config"] = { + "font_pos_config": def_font_pos_config + } + audioVideoStitchingParams = { "ParamObj": ParamObj, "Uploader": params["space_name"], diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 5118d81d..9d0001dd 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -328,6 +328,7 @@ def get_video_audio_info(type: str, source: str, space_name: str) -> dict: Note: - ** directurl 模式:仅支持点播存储 ** - ** vid 模式:通过 get_play_video_info 获取数据 ** + - ** 不支持 http 模式** Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl:仅仅支持点播存储 From 2fe39cea49e25f3dc1fcb04418e064e3b50efea0 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Sun, 28 Dec 2025 14:25:40 +0800 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E5=92=8C=E5=88=86=E7=BB=84=E8=8E=B7=E5=8F=96=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 313 ++++++++++++++++++ .../mcp_server_vod/src/base/base_service.py | 64 +++- server/mcp_server_vod/src/base/constant.py | 17 + server/mcp_server_vod/src/base/credential.py | 115 ++++++- server/mcp_server_vod/src/vod/mcp_server.py | 91 +++-- .../src/vod/mcp_tools/video_play.py | 18 +- server/mcp_server_vod/src/vod/server.py | 42 ++- 7 files changed, 600 insertions(+), 60 deletions(-) create mode 100644 server/mcp_server_vod/src/base/base_mcp.py create mode 100644 server/mcp_server_vod/src/base/constant.py diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py new file mode 100644 index 00000000..8c94a86d --- /dev/null +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -0,0 +1,313 @@ +from requests import get +from mcp.server.fastmcp import FastMCP +import logging +from typing import Dict, Any,Optional +from mcp.server.fastmcp import Context +from mcp.server.session import ServerSession +from starlette.requests import Request +from src.base.constant import ( + VOLCENGINE_ACCESS_KEY_HEADER, + VOLCENGINE_SECRET_KEY_HEADER, + VOLCENGINE_SESSION_TOKEN_HEADER, + VOLCENGINE_HOST_HEADER, + VOLCENGINE_REGION_HEADER, + VOLCENGINE_TOOLS_SOURCE_HEADER, + VOLCENGINE_TOOLS_TYPE_HEADER, + + VOLCENGINE_ACCESS_KEY_ENV, + VOLCENGINE_SECRET_KEY_ENV, + VOLCENGINE_SESSION_TOKEN_ENV, + VOLCENGINE_HOST_ENV, + VOLCENGINE_REGION_ENV, + VOLCENGINE_TOOLS_SOURCE_ENV, + VOLCENGINE_TOOLS_TYPE_ENV +) + +from os import environ + +logger = logging.getLogger(__name__) + +AVAILABLE_GROUPS = [ +# 视频剪辑相关tools +"edit", +# 智能切片相关tools +"intelligent_slicing", +# 智能抠图相关tools +"intelligent_matting", +# 字幕处理相关tools +"subtitle_processing", +# 音频处理相关tools +"audio_processing", +# 视频增强相关tools +"video_enhancement", +# 上传相关 +'upload', +# 视频播放相关 +"video_play" +] + + +DEFAULT_GROUPS = [ + "edit", + "video_play" +] + +DEFAULT_TOOL_LIST = [ + "audio_video_stitching", + "audio_video_clipping", + "get_v_creative_task_result", + "get_play_url", +] + +TRANSCODE_GROUPS = { + # 智能切片相关tools + "intelligent_slicing", + # 智能抠图相关tools + "intelligent_matting", + # # 字幕处理相关tools + # "subtitle_processing", + # 音频处理相关tools + "audio_processing", + # 视频增强相关tools + "video_enhancement", +} + +ALL_GROUPS = 'all' + +# 工具分组到工具列表的映射 +# 所有分组统一为列表格式,用于在 list_tools 时动态过滤 +TOOL_GROUP_MAP = { + # edit 分组 + "edit": [ + "audio_video_stitching", + "audio_video_clipping", + "get_v_creative_task_result", + "image_to_video", + "compile_video_audio", + "extract_audio", + "mix_audios", + "add_sub_video", + "add_subtitle", + "flip_video", + "speedup_video" + ], + # video_play 分组 + "video_play": [ + "get_play_url", + "get_video_audio_info" + ], + # upload 分组 + "upload": [ + "video_batch_upload", + "query_batch_upload_task_info" + ], + # intelligent_slicing 分组 + "intelligent_slicing": [ + "intelligent_slicing_task" + ], + # intelligent_matting 分组 + "intelligent_matting": [ + "portrait_image_retouching_task", + "green_screen_task" + ], + # subtitle_processing 分组 + "subtitle_processing": [ + "subtitles_removal_task", + "ocr_task", + "asr_task" + ], + # audio_processing 分组 + "audio_processing": [ + "voice_separation_task", + "audio_noise_reduction_task" + ], + # video_enhancement 分组 + "video_enhancement": [ + "video_interlacing_task", + "video_super_resolution_task", + "video_quality_enhancement_task" + ], + # media_tasks 分组(通用) + "media_tasks": [ + "get_media_execution_task_result" + ], +} + +# 工具名到工具分组的反向映射,用于快速查找工具所属分组 +TOOL_NAME_TO_GROUP_MAP = {} +for group_name, tool_list in TOOL_GROUP_MAP.items(): + for tool_name in tool_list: + TOOL_NAME_TO_GROUP_MAP[tool_name] = group_name + + + + +class BaseMCP(FastMCP): + def __init__(self, + name: str | None = None, # + instructions: str | None = None, + host: str = "127.0.0.1", # + port: int = 8000, # + streamable_http_path: str = "/mcp", # + stateless_http: bool = False, # + ): + super().__init__( + name=name, + host=host, + port=port, + stateless_http=stateless_http, + streamable_http_path=streamable_http_path, + instructions=instructions + ) + self._base_mcp_store = {} + + def set_base_mcp_store(self, store: Dict[Any, Any]) -> Dict: + """设置 base_mcp_store + + Args: + store: 要存储的字典 + + Returns: + Dict: 更新后的 _base_mcp_store + """ + self._base_mcp_store = { + **self._base_mcp_store, + **store + } + return self._base_mcp_store + + def get_base_mcp_store(self, key: Any) -> Any: + """获取 base_mcp_store 中的值 + + Args: + key: 要获取的键 + + Returns: + Any: 对应的值,如果 key 为 None 或不存在则返回 None + """ + if key is None: + return None + return self._base_mcp_store.get(key) + def get_request_ctx(self) -> Optional[Request]: + """获取当前请求的 Request 对象 + + Returns: + Optional[Request]: 当前请求的 Request 对象,如果无法获取则返回 None + """ + try: + ctx: Optional[Context[ServerSession, object]] = self.get_context() + if ctx and hasattr(ctx, 'request_context') and ctx.request_context: + raw_request: Optional[Request] = ctx.request_context.request + return raw_request + except Exception as e: + logger.debug(f"Failed to get request context: {e}") + return None + + def _handle_display_tools(self) -> tuple[Optional[str], Optional[list[str]]]: + """处理要显示的工具列表或分组列表 + + 优先级:header > 环境变量 > _base_mcp_store + + Returns: + tuple[Optional[str], Optional[list[str]]]: (tools_type, tools/sources列表),如果未配置则返回 (None, None) + """ + tools_type = None + tools_source = None + + # 优先级1: 从 header 获取 + try: + raw_request: Optional[Request] = self.get_request_ctx() + if raw_request: + headers = raw_request.headers + tools_type_h = headers.get(VOLCENGINE_TOOLS_TYPE_HEADER) + tools_source_h = headers.get(VOLCENGINE_TOOLS_SOURCE_HEADER) + + if tools_type_h and tools_source_h: + if tools_type_h in ['tools', 'groups']: + tools_type = tools_type_h + tools_source = [item.strip() for item in tools_source_h.split(',') if item.strip()] + logger.debug(f"Got tools_type and source from header: {tools_type}, {tools_source}") + return (tools_type, tools_source) + except Exception as e: + logger.debug(f"Failed to get tools from header: {e}") + + # 优先级2: 从环境变量获取 + tool_type_e = environ.get(VOLCENGINE_TOOLS_TYPE_ENV) + tools_source_e = environ.get(VOLCENGINE_TOOLS_SOURCE_ENV) + + if tool_type_e and tools_source_e: + if tool_type_e in ['tools', 'groups']: + tools_type = tool_type_e + tools_source = [item.strip() for item in tools_source_e.split(',') if item.strip()] + logger.debug(f"Got tools_type and source from environment: {tools_type}, {tools_source}") + return (tools_type, tools_source) + + # 优先级3: 从 _base_mcp_store 获取 + tools_type_store = self.get_base_mcp_store('tools_type') + source_store = self.get_base_mcp_store('source') + + if tools_type_store and source_store: + if tools_type_store in ['tools', 'groups']: + # 如果 source_store 是字符串,需要分割 + if isinstance(source_store, str): + tools_source = [item.strip() for item in source_store.split(',') if item.strip()] + elif isinstance(source_store, list): + tools_source = [str(item).strip() for item in source_store if item] + else: + tools_source = None + + if tools_source: + tools_type = tools_type_store + logger.debug(f"Got tools_type and source from _base_mcp_store: {tools_type}, {tools_source}") + return (tools_type, tools_source) + + # 如果都没有配置,返回默认工具列表 + logger.debug("No tools configuration found, using DEFAULT_TOOL_LIST") + return ('tools', DEFAULT_TOOL_LIST) + async def list_tools(self): + """Get the list of tools available for this MCP instance. + Returns: + list[Tool]: A list of Tool objects. + """ + try: + res = await super().list_tools() + + # 获取要显示的工具列表或分组列表 + # _handle_display_tools 返回 (tools_type, tools/sources列表) + tools_type, display_config = self._handle_display_tools() + + if tools_type is None or display_config is None: + # 如果没有配置,返回所有工具 + logger.info(f"BaseMCP.list_tools: no filter, returning all {len(res)} tools") + return res + + # 获取允许的工具列表 + allowed_tools = set() + + if tools_type == 'tools': + # 如果指定的是工具列表,直接使用 + allowed_tools = set(display_config) + elif tools_type == 'groups': + # 如果指定的是分组列表,从 TOOL_GROUP_MAP 中获取对应的工具 + for group_name in display_config: + if group_name == ALL_GROUPS: + # 如果指定了 "all",返回所有工具 + logger.info(f"BaseMCP.list_tools: returning all {len(res)} tools (all groups)") + return res + if group_name in TOOL_GROUP_MAP: + allowed_tools.update(TOOL_GROUP_MAP[group_name]) + + # 过滤工具 + filtered_tools = [] + for tool in res: + if tool.name in allowed_tools: + filtered_tools.append(tool) + + logger.info(f"BaseMCP.list_tools: filtered to {len(filtered_tools)}/{len(res)} tools based on {tools_type}: {display_config}") + return filtered_tools + except Exception as e: + logger.error(f"BaseMCP.list_tools failed with error: {e}") + import traceback + logger.error(traceback.format_exc()) + return [] + diff --git a/server/mcp_server_vod/src/base/base_service.py b/server/mcp_server_vod/src/base/base_service.py index b5443376..b4186415 100644 --- a/server/mcp_server_vod/src/base/base_service.py +++ b/server/mcp_server_vod/src/base/base_service.py @@ -1,8 +1,11 @@ from volcengine.vod.VodService import VodService import json import os -from typing import Dict, Any -from src.base.credential import get_volcengine_credentials_base +import logging +from typing import Dict, Any, Optional +from src.base.credential import get_volcengine_credentials_base, get_volcengine_credentials_from_context +from mcp.server.fastmcp import Context +from mcp.server.session import ServerSession class BaseService(VodService): @@ -21,6 +24,25 @@ def __init__(self): if os.getenv("VOLCENGINE_HOST"): self.set_host(os.getenv("VOLCENGINE_HOST")) self.mcp_state = {} + self._mcp_instance = None # Store MCP instance for context access + + def update_credentials_from_mcp(self): + """Update credentials from MCP context if mcp instance is available.""" + if self._mcp_instance is None: + logging.debug("update_credentials_from_mcp: _mcp_instance is None, skipping") + return + + try: + ctx = self._mcp_instance.get_context() + if ctx: + logging.debug("update_credentials_from_mcp: got context, updating credentials") + self.update_credentials_from_context(ctx) + else: + logging.debug("update_credentials_from_mcp: get_context() returned None") + except Exception as e: + logging.debug(f"update_credentials_from_mcp: Failed to get context from MCP instance: {e}") + import traceback + logging.debug(traceback.format_exc()) @staticmethod def get_api_info(): @@ -30,6 +52,7 @@ def set_api_info(self, api_info): return def mcp_get(self, action, params={}, doseq=0): + self.update_credentials_from_mcp() res = self.get(action, params, doseq) if res == '': raise Exception("%s: empty response" % action) @@ -37,6 +60,7 @@ def mcp_get(self, action, params={}, doseq=0): return res_json def mcp_post(self, action, params={}, body={}): + self.update_credentials_from_mcp() res = self.json(action, params, body) if res == '': raise Exception("%s: empty response" % action) @@ -49,3 +73,39 @@ def set_state(self, state: Dict[str, Any] = {}): } def get_state(self): return self.mcp_state + + def update_credentials_from_context(self, ctx: Optional[Context[ServerSession, object]] = None): + """Update credentials from MCP context headers. + + Args: + ctx: MCP context object. If None, will try to get from current context. + """ + if ctx is None: + return + + try: + # 从上下文获取凭证信息 + context_cred = get_volcengine_credentials_from_context(ctx) + if context_cred: + # 更新凭证 + if context_cred.get("access_key_id"): + self.set_ak(context_cred["access_key_id"]) + if context_cred.get("secret_access_key"): + self.set_sk(context_cred["secret_access_key"]) + if context_cred.get("session_token"): + self.set_session_token(context_cred["session_token"]) + + # 更新 host + if context_cred.get("host"): + self.set_host(context_cred["host"]) + + # 更新 region + if context_cred.get("region"): + # 注意:VodService 的 region 在初始化时设置,可能需要重新初始化 + # 但为了兼容性,我们只更新 service_info 中的 region + if hasattr(self, 'service_info') and hasattr(self.service_info, 'region'): + self.service_info.region = context_cred["region"] + + logging.debug("Credentials updated from MCP context") + except Exception as e: + logging.warning(f"Failed to update credentials from context: {e}") diff --git a/server/mcp_server_vod/src/base/constant.py b/server/mcp_server_vod/src/base/constant.py new file mode 100644 index 00000000..659b9455 --- /dev/null +++ b/server/mcp_server_vod/src/base/constant.py @@ -0,0 +1,17 @@ +VOLCENGINE_ACCESS_KEY_HEADER = 'x-tt-access-key' +VOLCENGINE_SECRET_KEY_HEADER = 'x-tt-secret-key' +VOLCENGINE_SESSION_TOKEN_HEADER = 'x-tt-session-token' +VOLCENGINE_HOST_HEADER = 'x-tt-host' +VOLCENGINE_REGION_HEADER = 'x-tt-region' +VOLCENGINE_TOOLS_SOURCE_HEADER = 'x-tt-tools-source' +VOLCENGINE_TOOLS_TYPE_HEADER = 'x-tt-tools-type' + + + +VOLCENGINE_ACCESS_KEY_ENV = 'VOLCENGINE_ACCESS_KEY' +VOLCENGINE_SECRET_KEY_ENV = 'VOLCENGINE_SECRET_KEY' +VOLCENGINE_SESSION_TOKEN_ENV = 'VOLCENGINE_SESSION_TOKEN' +VOLCENGINE_HOST_ENV = 'VOLCENGINE_HOST' +VOLCENGINE_REGION_ENV = 'VOLCENGINE_REGION' +VOLCENGINE_TOOLS_SOURCE_ENV = 'MCP_TOOLS_SOURCE' +VOLCENGINE_TOOLS_TYPE_ENV = 'MCP_TOOL_TYPE' \ No newline at end of file diff --git a/server/mcp_server_vod/src/base/credential.py b/server/mcp_server_vod/src/base/credential.py index 7d29f15c..875326c4 100644 --- a/server/mcp_server_vod/src/base/credential.py +++ b/server/mcp_server_vod/src/base/credential.py @@ -1,8 +1,26 @@ import json +import logging from pathlib import Path from os import environ from pydantic import BaseModel +from typing import Optional, Dict, Any +from mcp.server.fastmcp import Context +from mcp.server.session import ServerSession +from starlette.requests import Request +from src.base.constant import ( + VOLCENGINE_ACCESS_KEY_HEADER, + VOLCENGINE_SECRET_KEY_HEADER, + VOLCENGINE_SESSION_TOKEN_HEADER, + VOLCENGINE_HOST_HEADER, + VOLCENGINE_REGION_HEADER, + + VOLCENGINE_ACCESS_KEY_ENV, + VOLCENGINE_SECRET_KEY_ENV, + VOLCENGINE_SESSION_TOKEN_ENV, + VOLCENGINE_HOST_ENV, + VOLCENGINE_REGION_ENV, +) VEFAAS_IAM_CRIDENTIAL_PATH = "/var/run/secrets/iam/credential" @@ -11,13 +29,96 @@ class VeIAMCredential(BaseModel): secret_access_key: str session_token: str -VOLCENGINE_ACCESS_KEY_ENV = 'VOLCENGINE_ACCESS_KEY' -VOLCENGINE_SECRET_KEY_ENV = 'VOLCENGINE_SECRET_KEY' -VOLCENGINE_SESSION_TOKEN_ENV = 'VOLCENGINE_SESSION_TOKEN' +# VOLCENGINE_ACCESS_KEY_ENV = 'VOLCENGINE_ACCESS_KEY' +# VOLCENGINE_SECRET_KEY_ENV = 'VOLCENGINE_SECRET_KEY' +# VOLCENGINE_SESSION_TOKEN_ENV = 'VOLCENGINE_SESSION_TOKEN' +# VOLCENGINE_HOST_ENV = 'VOLCENGINE_HOST' +# VOLCENGINE_REGION_ENV = 'VOLCENGINE_REGION' -def get_volcengine_credentials_base() -> VeIAMCredential: - """Get Volcengine credentials from environment variables.""" - # 优先从环境变量读取 +def get_volcengine_credentials_from_context(ctx: Optional[Context[ServerSession, object]] = None) -> Optional[Dict[str, Any]]: + """Get Volcengine credentials from MCP context headers. + + Args: + ctx: MCP context object + + Returns: + Dict containing credentials and config if found in headers, None otherwise. + Keys: access_key_id, secret_access_key, session_token, host, region + """ + print("get_volcengine_credentials_from_context: ctx", ctx.request_context.request) + if ctx is None: + return None + + try: + raw_request: Optional[Request] = ctx.request_context.request + if not raw_request: + return None + + headers = raw_request.headers + + # 从 header 中读取凭证信息(使用与环境变量一致的命名,同时支持连字符和下划线格式) + # 优先使用下划线格式(与环境变量一致),也支持连字符格式和 x- 前缀 + access_key = ( + headers.get(VOLCENGINE_ACCESS_KEY_HEADER) or + None + ) + secret_key = ( + headers.get(VOLCENGINE_SECRET_KEY_HEADER) or + None + ) + session_token = ( + headers.get(VOLCENGINE_SESSION_TOKEN_HEADER) or + None + ) + host = ( + headers.get(VOLCENGINE_HOST_HEADER) or + None + ) + region = ( + headers.get(VOLCENGINE_REGION_HEADER) or + None + ) + + # 如果 header 中有 ak 和 sk,返回凭证信息 + if access_key and secret_key: + result = { + "access_key_id": access_key, + "secret_access_key": secret_key, + "session_token": session_token, + } + if host: + result["host"] = host + if region: + result["region"] = region + return result + + return None + except Exception as e: + logging.warning(f"Failed to get credentials from context: {e}") + return None + + +def get_volcengine_credentials_base(ctx: Optional[Context[ServerSession, object]] = None) -> VeIAMCredential: + """Get Volcengine credentials from context headers, environment variables, or VeFaaS IAM. + + Args: + ctx: Optional MCP context object + + Returns: + VeIAMCredential object + """ + # 优先从 MCP 上下文 header 读取 + if ctx: + context_cred = get_volcengine_credentials_from_context(ctx) + print("context_cred",context_cred) + if context_cred: + return VeIAMCredential( + access_key_id=context_cred.get("access_key_id", ""), + secret_access_key=context_cred.get("secret_access_key", ""), + session_token=context_cred.get("session_token", ""), + ) + + # 如果上下文没有,则从环境变量读取 access_key = environ.get(VOLCENGINE_ACCESS_KEY_ENV, '') secret_key = environ.get(VOLCENGINE_SECRET_KEY_ENV, '') session_token = environ.get(VOLCENGINE_SESSION_TOKEN_ENV, '') @@ -31,7 +132,7 @@ def get_volcengine_credentials_base() -> VeIAMCredential: # 如果仍未获取到有效凭证,则抛出异常 if not (access_key and secret_key): - raise RuntimeError("无法获取有效的 Volcengine 凭证,请检查环境变量或 VeFaaS IAM 配置") + logging.error("无法获取有效的 Volcengine 凭证,请检查环境变量或 VeFaaS IAM 配置") return VeIAMCredential( access_key_id=access_key, secret_access_key=secret_key, diff --git a/server/mcp_server_vod/src/vod/mcp_server.py b/server/mcp_server_vod/src/vod/mcp_server.py index 4bd9b33e..6c2792b3 100644 --- a/server/mcp_server_vod/src/vod/mcp_server.py +++ b/server/mcp_server_vod/src/vod/mcp_server.py @@ -1,15 +1,18 @@ import importlib +from tokenize import group +from typing import Optional from src.vod.api.api import VodAPI from src.vod.mcp_tools.media_tasks import create_transcode_result_server from src.vod.utils.transcode import register_transcode_base_fn from src.vod.mcp_tools.video_play import register_video_play_methods from mcp.server.fastmcp import FastMCP from pathlib import Path +from src.base.base_mcp import TOOL_NAME_TO_GROUP_MAP, TOOL_GROUP_MAP -from urllib.parse import quote import json import os +import logging AVAILABLE_GROUPS = [ # 视频剪辑相关tools @@ -51,35 +54,60 @@ ALL_GROUPS = 'all' -def create_mcp_server(groups: list[str] = None, mcp: FastMCP = None): +# 工具名到工具分组的映射从 base_mcp 导入 +# TOOL_NAME_TO_GROUP_MAP: 工具名 -> 分组名 (用于根据工具名查找分组) +# TOOL_GROUP_MAP: 分组名 -> 工具列表 (用于根据分组名获取工具列表) + + +def create_mcp_server(mcp: FastMCP = None): ## init api client service = VodAPI() ## init public methods public_methods = {} - - ## init tool groups - current_tool_groups = [] - env_type = os.getenv("MCP_TOOL_GROUPS") - - if groups is not None: - if ALL_GROUPS in groups: - current_tool_groups = AVAILABLE_GROUPS - else: - current_tool_groups = groups - elif env_type is not None: - env_grops= [group.strip() for group in env_type.split(",") if group.strip()] - try: - if ALL_GROUPS in env_grops: - current_tool_groups = AVAILABLE_GROUPS - else: - current_tool_groups = env_grops - print(f"[MCP] Loaded tool groups from environment: {current_tool_groups}") - except Exception as e: - print(f"[MCP] Error parsing MCP_TOOL_GROUPS environment variable: {e}") - current_tool_groups = DEFAULT_GROUPS - else: - current_tool_groups = DEFAULT_GROUPS - + + # 确保 mcp 不为 None + if mcp is None: + raise ValueError("mcp parameter cannot be None") + + mcp.set_base_mcp_store({ + 'apiRequestInstance': service + }) + # ## init tool groups + # # 优先级:1. groups 参数 2. MCP header 3. 环境变量 4. 默认值 + # current_tool_groups = [] + + # if groups is not None: + # if ALL_GROUPS in groups: + # current_tool_groups = AVAILABLE_GROUPS + # else: + # current_tool_groups = groups + # print(f"[MCP] Loaded tool groups from parameter: {current_tool_groups}") + # else: + # # 尝试从 MCP 上下文 header 获取 + # header_groups = get_tool_groups_from_context(mcp) + # if header_groups: + # if ALL_GROUPS in header_groups: + # current_tool_groups = AVAILABLE_GROUPS + # else: + # current_tool_groups = header_groups + # print(f"[MCP] Loaded tool groups from header: {current_tool_groups}") + # else: + # # 从环境变量获取 + # env_type = os.getenv("MCP_TOOL_GROUPS") + # if env_type is not None: + # env_grops = [group.strip() for group in env_type.split(",") if group.strip()] + # try: + # if ALL_GROUPS in env_grops: + # current_tool_groups = AVAILABLE_GROUPS + # else: + # current_tool_groups = env_grops + # print(f"[MCP] Loaded tool groups from environment: {current_tool_groups}") + # except Exception as e: + # print(f"[MCP] Error parsing MCP_TOOL_GROUPS environment variable: {e}") + # current_tool_groups = DEFAULT_GROUPS + # else: + # current_tool_groups = DEFAULT_GROUPS + # print(f"[MCP] Using default tool groups: {current_tool_groups}") ## update media publish status def update_media_publish_status (vid: str, SpaceName: str, PublishStatus: str) -> str: @@ -220,5 +248,14 @@ def _load_tools(groupList: list[str]): module.create_mcp_server(mcp, public_methods, service) print(f"[MCP] Loaded tool: {module_name}") - _load_tools(current_tool_groups) + # Store mcp instance in service for context access + service._mcp_instance = mcp + logging.info(f"Set _mcp_instance on service: {mcp is not None}") + + # 注册所有工具(启动时注册所有工具,在 streamable HTTP 建联时会根据 header 动态过滤) + # 注意:这里注册所有工具,而不是只注册 current_tool_groups + # 因为 streamable HTTP 模式下,每次请求的 header 可能不同 + _load_tools(AVAILABLE_GROUPS) + + return mcp diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 9d0001dd..0f7def58 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -11,6 +11,10 @@ import re import urllib.request import urllib.error +import logging +import inspect + + def register_video_play_methods(service: VodAPI, public_methods: dict,): def str_to_number(s, default=None): @@ -312,7 +316,7 @@ def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): @mcp.tool() def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> str: """ - Obtain the video playback link through `fileName` + Obtain the video playback link through `fileName`, 通过 fileName 获取视频播放地址 Args: - spaceName: **必选字段** 空间名称 - fileName: **必选字段** 文件名 @@ -320,11 +324,12 @@ def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> st Returns: - 播放地址 """ + return public_methods["get_play_url"](spaceName, fileName, expired_minutes) @mcp.tool() def get_video_audio_info(type: str, source: str, space_name: str) -> dict: - """Obtaining audio and video metadata + """Obtaining audio and video metadata, 获取音视频播放信息 Note: - ** directurl 模式:仅支持点播存储 ** - ** vid 模式:通过 get_play_video_info 获取数据 ** @@ -349,6 +354,15 @@ def get_video_audio_info(type: str, source: str, space_name: str) -> dict: - BitsPerSample(str): 音频采样码率,单位 bit。 - PlayURL(str): 播放地址 """ + frame = inspect.currentframe() + arguments = inspect.getargvalues(frame).locals + ctx = mcp.get_context() + raw_request: Request = ctx.request_context.request.headers + logging.info(f"get_play_url: {space_name} {source} {type}") + logging.info(f"get_play_url_ctx: {raw_request.get('ak')}") + logging.info(f"get_play_url_ctx: {raw_request.get('sk')}") + print(f"get_play_urframe: {arguments}") + try: params = {"type": type, "source": source, "space_name": space_name} if "space_name" not in params: diff --git a/server/mcp_server_vod/src/vod/server.py b/server/mcp_server_vod/src/vod/server.py index 0443adea..1249109c 100644 --- a/server/mcp_server_vod/src/vod/server.py +++ b/server/mcp_server_vod/src/vod/server.py @@ -2,6 +2,7 @@ from src.vod.mcp_server import create_mcp_server from mcp.server.fastmcp import FastMCP +from src.base.base_mcp import BaseMCP from dotenv import load_dotenv import os import asyncio @@ -10,6 +11,22 @@ load_dotenv() +mcp = BaseMCP( + name="VOD MCP", + host=os.getenv("MCP_SERVER_HOST", "0.0.0.0"), + port=int(os.getenv("MCP_SERVER_PORT", "8000")), + stateless_http=os.getenv("STATLESS_HTTP", "true").lower() == "true", + streamable_http_path=os.getenv("STREAMABLE_HTTP_PATH", "/mcp"), + instructions=""" + ## VOD MCP is the Volcengine VOD MCP Server. + ### Before using the VOD service, please note: + - `SpaceName` is the name of the VOD space. + - `Vid` is the video ID, 示例:v02399g100***2qpj9aljht4nmunv9ng. + - `DirectUrl` 指定资源的 FileName。示例:test.mp3 + """, + ) +def init_mcp () -> FastMCP: + return mcp def main(): try: @@ -21,29 +38,10 @@ def main(): default="stdio", help="Transport protocol to use (streamable-http or stdio)", ) - parser.add_argument("--groups", type=str, help="Comma-separated tool groups") args = parser.parse_args() - print(args.transport) - print(args.groups) - if args.groups: - groups = args.groups.split(',') - else: - groups = None - mcp = FastMCP( - name="VOD MCP", - host=os.getenv("MCP_SERVER_HOST", "0.0.0.0"), - port=int(os.getenv("MCP_SERVER_PORT", "8000")), - stateless_http=os.getenv("STATLESS_HTTP", "true").lower() == "true", - streamable_http_path=os.getenv("STREAMABLE_HTTP_PATH", "/mcp"), - instructions=""" - ## VOD MCP is the Volcengine VOD MCP Server. - ### Before using the VOD service, please note: - - `SpaceName` is the name of the VOD space. - - `Vid` is the video ID, 示例:v02399g100***2qpj9aljht4nmunv9ng. - - `DirectUrl` 指定资源的 FileName。示例:test.mp3 - """, - ) - mcp_instance = create_mcp_server(groups, mcp) + + mcp_instance = create_mcp_server(mcp) + asyncio.run(mcp_instance.run(transport=args.transport)) except Exception as e: print(f"Error occurred while starting the server: {e}", file=sys.stderr) From 2682c5861e55160b322afc0003078cba0398ff4c Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 10:44:21 +0800 Subject: [PATCH 04/26] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=87=AD=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/credential.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/mcp_server_vod/src/base/credential.py b/server/mcp_server_vod/src/base/credential.py index 875326c4..53daba4c 100644 --- a/server/mcp_server_vod/src/base/credential.py +++ b/server/mcp_server_vod/src/base/credential.py @@ -130,9 +130,9 @@ def get_volcengine_credentials_base(ctx: Optional[Context[ServerSession, object] secret_key = vefaas_cred.secret_access_key session_token = vefaas_cred.session_token - # 如果仍未获取到有效凭证,则抛出异常 + # 如果仍未获取到有效凭证,仅打印警告(支持通过 Header 动态传递凭证) if not (access_key and secret_key): - logging.error("无法获取有效的 Volcengine 凭证,请检查环境变量或 VeFaaS IAM 配置") + logging.warning("未检测到 Volcengine 凭证(环境变量/IAM),服务将以无凭证模式启动。请确保在请求 Header 中传递凭证。") return VeIAMCredential( access_key_id=access_key, secret_access_key=secret_key, From 0a0e8fdd8a75196d3d7903587d03021f73a58bd2 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 11:03:52 +0800 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=91=BD=E5=90=8D=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/constant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mcp_server_vod/src/base/constant.py b/server/mcp_server_vod/src/base/constant.py index 659b9455..00b09888 100644 --- a/server/mcp_server_vod/src/base/constant.py +++ b/server/mcp_server_vod/src/base/constant.py @@ -14,4 +14,4 @@ VOLCENGINE_HOST_ENV = 'VOLCENGINE_HOST' VOLCENGINE_REGION_ENV = 'VOLCENGINE_REGION' VOLCENGINE_TOOLS_SOURCE_ENV = 'MCP_TOOLS_SOURCE' -VOLCENGINE_TOOLS_TYPE_ENV = 'MCP_TOOL_TYPE' \ No newline at end of file +VOLCENGINE_TOOLS_TYPE_ENV = 'MCP_TOOLS_TYPE' \ No newline at end of file From 4b63975c0ed5f2d6e04068a16827d5dc462d9364 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 11:16:42 +0800 Subject: [PATCH 06/26] =?UTF-8?q?feat:=20=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=91=BD=E5=90=8D=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/credential.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/mcp_server_vod/src/base/credential.py b/server/mcp_server_vod/src/base/credential.py index 53daba4c..db04a5a4 100644 --- a/server/mcp_server_vod/src/base/credential.py +++ b/server/mcp_server_vod/src/base/credential.py @@ -18,8 +18,8 @@ VOLCENGINE_ACCESS_KEY_ENV, VOLCENGINE_SECRET_KEY_ENV, VOLCENGINE_SESSION_TOKEN_ENV, - VOLCENGINE_HOST_ENV, - VOLCENGINE_REGION_ENV, + # VOLCENGINE_HOST_ENV, + # VOLCENGINE_REGION_ENV, ) VEFAAS_IAM_CRIDENTIAL_PATH = "/var/run/secrets/iam/credential" @@ -35,7 +35,7 @@ class VeIAMCredential(BaseModel): # VOLCENGINE_HOST_ENV = 'VOLCENGINE_HOST' # VOLCENGINE_REGION_ENV = 'VOLCENGINE_REGION' -def get_volcengine_credentials_from_context(ctx: Optional[Context[ServerSession, object]] = None) -> Optional[Dict[str, Any]]: +def get_volcengine_credentials_from_context(ctx: Optional[Context[ServerSession, object, Any]] = None) -> Optional[Dict[str, Any]]: """Get Volcengine credentials from MCP context headers. Args: From d05dfd7716aba9843d8dc601b25aac216373c44f Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 11:21:17 +0800 Subject: [PATCH 07/26] =?UTF-8?q?feat:=20=E7=B1=BB=E5=9E=8B=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/credential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mcp_server_vod/src/base/credential.py b/server/mcp_server_vod/src/base/credential.py index db04a5a4..072ec78b 100644 --- a/server/mcp_server_vod/src/base/credential.py +++ b/server/mcp_server_vod/src/base/credential.py @@ -98,7 +98,7 @@ def get_volcengine_credentials_from_context(ctx: Optional[Context[ServerSession, return None -def get_volcengine_credentials_base(ctx: Optional[Context[ServerSession, object]] = None) -> VeIAMCredential: +def get_volcengine_credentials_base(ctx: Optional[Context[ServerSession, object, Any]] = None) -> VeIAMCredential: """Get Volcengine credentials from context headers, environment variables, or VeFaaS IAM. Args: From 564552a4b8f06064622d9bf186034ddd78254941 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 11:23:04 +0800 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20=E6=97=A0=E7=94=A8=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=BC=95=E5=85=A5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 20 +++++++++---------- .../mcp_server_vod/src/base/base_service.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index 8c94a86d..08709559 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -6,19 +6,19 @@ from mcp.server.session import ServerSession from starlette.requests import Request from src.base.constant import ( - VOLCENGINE_ACCESS_KEY_HEADER, - VOLCENGINE_SECRET_KEY_HEADER, - VOLCENGINE_SESSION_TOKEN_HEADER, - VOLCENGINE_HOST_HEADER, - VOLCENGINE_REGION_HEADER, + # VOLCENGINE_ACCESS_KEY_HEADER, + # VOLCENGINE_SECRET_KEY_HEADER, + # VOLCENGINE_SESSION_TOKEN_HEADER, + # VOLCENGINE_HOST_HEADER, + # VOLCENGINE_REGION_HEADER, VOLCENGINE_TOOLS_SOURCE_HEADER, VOLCENGINE_TOOLS_TYPE_HEADER, - VOLCENGINE_ACCESS_KEY_ENV, - VOLCENGINE_SECRET_KEY_ENV, - VOLCENGINE_SESSION_TOKEN_ENV, - VOLCENGINE_HOST_ENV, - VOLCENGINE_REGION_ENV, + # VOLCENGINE_ACCESS_KEY_ENV, + # VOLCENGINE_SECRET_KEY_ENV, + # VOLCENGINE_SESSION_TOKEN_ENV, + # VOLCENGINE_HOST_ENV, + # VOLCENGINE_REGION_ENV, VOLCENGINE_TOOLS_SOURCE_ENV, VOLCENGINE_TOOLS_TYPE_ENV ) diff --git a/server/mcp_server_vod/src/base/base_service.py b/server/mcp_server_vod/src/base/base_service.py index b4186415..17bd0677 100644 --- a/server/mcp_server_vod/src/base/base_service.py +++ b/server/mcp_server_vod/src/base/base_service.py @@ -74,7 +74,7 @@ def set_state(self, state: Dict[str, Any] = {}): def get_state(self): return self.mcp_state - def update_credentials_from_context(self, ctx: Optional[Context[ServerSession, object]] = None): + def update_credentials_from_context(self, ctx: Optional[Context[ServerSession, object, Any]] = None): """Update credentials from MCP context headers. Args: From c8ec0ecf662d43f9989e25d0ed0115cf20cbfd02 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 29 Dec 2025 11:30:26 +0800 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20=E8=BF=94=E5=9B=9E=E5=80=BC?= =?UTF-8?q?=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/vod/mcp_tools/video_play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 0f7def58..d4a1e54c 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -314,7 +314,7 @@ def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): @mcp.tool() - def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> str: + def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> Any: """ Obtain the video playback link through `fileName`, 通过 fileName 获取视频播放地址 Args: From c57d2351569b3a4dd44fedc467734ce6dd235465 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Tue, 30 Dec 2025 11:47:29 +0800 Subject: [PATCH 10/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 117 +++++++++++++----- .../src/vod/mcp_tools/upload.py | 10 +- .../mcp_server_vod/src/vod/models/__init__.py | 0 .../src/vod/models/request/__init__.py | 0 .../src/vod/models/request/request_models.py | 19 +++ 5 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 server/mcp_server_vod/src/vod/models/__init__.py create mode 100644 server/mcp_server_vod/src/vod/models/request/__init__.py create mode 100644 server/mcp_server_vod/src/vod/models/request/request_models.py diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index 72199936..eae12a92 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -1,5 +1,7 @@ import json from src.vod.api.api import VodAPI +from typing import List, Optional +from src.vod.models.request.request_models import InputSource,addSubVideoOptions def _format_source(type: str, source: str) -> str: @@ -28,7 +30,7 @@ def _format_source(type: str, source: str) -> str: def create_mcp_server(mcp,public_methods: dict, service: VodAPI, ): @mcp.tool() - def audio_video_stitching(type: str, SpaceName: str, videos: list = None, audios: list = None, transitions: list = None) -> dict: + def audio_video_stitching(type: str, SpaceName: str, videos: List[str] = None, audios: List[str] = None, transitions: List[str] = None) -> dict: """ Carry out video stitching, audio stitching, and support for transitions and other capabilities,需要参考 Note 中的要求。 Note: - **audio splicing does not support transitions. ** @@ -449,7 +451,7 @@ def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) - raise Exception("speedup_video: %s" % e, params) @mcp.tool() - def image_to_video(images: list, space_name: str, transitions: list = None) -> dict: + def image_to_video(images: List[InputSource], space_name: str, transitions: List[str] = None) -> dict: """The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition ** Args: - images(list[dict]): ** 必选字段 **,待合成的图片列表,子类型取值如下 @@ -526,6 +528,16 @@ def image_to_video(images: list, space_name: str, transitions: list = None) -> d if "animation_out" in image: formattedImage["animation_out"] = image["animation_out"] formattedImages.append(formattedImage) + elif hasattr(image, 'type') and hasattr(image, 'source'): + # InputSource 对象 + imgType = image.type or "vid" + imgSource = image.source or "" + formattedSource = _format_source(imgType, imgSource) if imgType in ["vid", "directurl"] else imgSource + formattedImage = { + "type": imgType, + "source": formattedSource, + } + formattedImages.append(formattedImage) else: formattedImages.append(image) @@ -562,7 +574,7 @@ def image_to_video(images: list, space_name: str, transitions: list = None) -> d raise Exception("image_to_video: %s" % e, params) @mcp.tool() - def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: + def compile_video_audio(video: InputSource, audio: InputSource, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: """The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing. Args: - video(dict): ** 必选字段 **,视频信息 @@ -606,20 +618,28 @@ def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_rese raise ValueError("compile_video_audio: params['space_name'] cannot be empty") if "video" not in params: raise ValueError("compile_video_audio: params must contain video") - if not isinstance(params["video"], dict): - raise TypeError("compile_video_audio: params['video'] must be a dict") + if not isinstance(params["video"], dict) and not hasattr(params["video"], 'type'): + raise TypeError("compile_video_audio: params['video'] must be a dict or InputSource object") if "audio" not in params: raise ValueError("compile_video_audio: params must contain audio") - if not isinstance(params["audio"], dict): - raise TypeError("compile_video_audio: params['audio'] must be a dict") + if not isinstance(params["audio"], dict) and not hasattr(params["audio"], 'type'): + raise TypeError("compile_video_audio: params['audio'] must be a dict or InputSource object") # 格式化视频和音频 - videoType = params["video"].get("type", "vid") - videoSource = params["video"].get("source", "") + if isinstance(params["video"], dict): + videoType = params["video"].get("type", "vid") + videoSource = params["video"].get("source", "") + else: + videoType = params["video"].type or "vid" + videoSource = params["video"].source or "" formattedVideoSource = _format_source(videoType, videoSource) if videoType in ["vid", "directurl"] else videoSource - audioType = params["audio"].get("type", "vid") - audioSource = params["audio"].get("source", "") + if isinstance(params["audio"], dict): + audioType = params["audio"].get("type", "vid") + audioSource = params["audio"].get("source", "") + else: + audioType = params["audio"].type or "vid" + audioSource = params["audio"].source or "" formattedAudioSource = _format_source(audioType, audioSource) if audioType in ["vid", "directurl"] else audioSource ParamObj = { @@ -726,7 +746,7 @@ def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") raise Exception("extract_audio: %s" % e, params) @mcp.tool() - def mix_audios(audios: list, space_name: str) -> dict: + def mix_audios(audios: List[InputSource], space_name: str) -> dict: """Mix audios Args: - audios(list[dict]): ** 必选字段 **,叠加的音频列表 @@ -765,6 +785,12 @@ def mix_audios(audios: list, space_name: str) -> dict: audioSource = audio.get("source", "") formattedSource = _format_source(audioType, audioSource) if audioType in ["vid", "directurl"] else audioSource formattedAudios.append(formattedSource) + elif hasattr(audio, 'type') and hasattr(audio, 'source'): + # InputSource 对象 + audioType = audio.type or "vid" + audioSource = audio.source or "" + formattedSource = _format_source(audioType, audioSource) if audioType in ["vid", "directurl"] else audioSource + formattedAudios.append(formattedSource) else: formattedAudios.append(audio) @@ -800,7 +826,7 @@ def mix_audios(audios: list, space_name: str) -> dict: raise Exception("mix_audios: %s" % e, params) @mcp.tool() - def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: dict = None) -> dict: + def add_sub_video(video: InputSource, sub_video: InputSource, space_name: str, sub_options: Optional[addSubVideoOptions] = None) -> dict: """`水印贴片`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. Note: - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 @@ -841,20 +867,28 @@ def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: di raise ValueError("add_sub_video: params['space_name'] cannot be empty") if "video" not in params: raise ValueError("add_sub_video: params must contain video") - if not isinstance(params["video"], dict): - raise TypeError("add_sub_video: params['video'] must be a dict") + if not isinstance(params["video"], dict) and not hasattr(params["video"], 'type'): + raise TypeError("add_sub_video: params['video'] must be a dict or InputSource object") if "sub_video" not in params: raise ValueError("add_sub_video: params must contain sub_video") - if not isinstance(params["sub_video"], dict): - raise TypeError("add_sub_video: params['sub_video'] must be a dict") + if not isinstance(params["sub_video"], dict) and not hasattr(params["sub_video"], 'type'): + raise TypeError("add_sub_video: params['sub_video'] must be a dict or InputSource object") # 格式化视频和水印视频 - videoType = params["video"].get("type", "vid") - videoSource = params["video"].get("source", "") + if isinstance(params["video"], dict): + videoType = params["video"].get("type", "vid") + videoSource = params["video"].get("source", "") + else: + videoType = params["video"].type or "vid" + videoSource = params["video"].source or "" formattedVideoSource = _format_source(videoType, videoSource) if videoType in ["vid", "directurl"] else videoSource - subVideoType = params["sub_video"].get("type", "vid") - subVideoSource = params["sub_video"].get("source", "") + if isinstance(params["sub_video"], dict): + subVideoType = params["sub_video"].get("type", "vid") + subVideoSource = params["sub_video"].get("source", "") + else: + subVideoType = params["sub_video"].type or "vid" + subVideoSource = params["sub_video"].source or "" formattedSubVideoSource = _format_source(subVideoType, subVideoSource) if subVideoType in ["vid", "directurl"] else subVideoSource ParamObj = { @@ -864,14 +898,39 @@ def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: di } if params.get("sub_options"): - - ParamObj["sub_options"] = { - "width": params["sub_options"].get("width", "20%"), - "height": params["sub_options"].get("height", "20%"), - "pos_x": params["sub_options"].get("pos_x", "0"), - "pos_y": params["sub_options"].get("pos_y", "0"), - **params["sub_options"], - } + if isinstance(params["sub_options"], dict): + ParamObj["sub_options"] = { + "width": params["sub_options"].get("width", "20%"), + "height": params["sub_options"].get("height", "20%"), + "pos_x": params["sub_options"].get("pos_x", "0"), + "pos_y": params["sub_options"].get("pos_y", "0"), + **params["sub_options"], + } + else: + # addSubVideoOptions 对象 + sub_options_dict = {} + if hasattr(params["sub_options"], 'width') and params["sub_options"].width is not None: + sub_options_dict["width"] = params["sub_options"].width + if hasattr(params["sub_options"], 'height') and params["sub_options"].height is not None: + sub_options_dict["height"] = params["sub_options"].height + if hasattr(params["sub_options"], 'pos_x') and params["sub_options"].pos_x is not None: + sub_options_dict["pos_x"] = params["sub_options"].pos_x + if hasattr(params["sub_options"], 'pos_y') and params["sub_options"].pos_y is not None: + sub_options_dict["pos_y"] = params["sub_options"].pos_y + if hasattr(params["sub_options"], 'start_time') and params["sub_options"].start_time is not None: + sub_options_dict["start_time"] = params["sub_options"].start_time + if hasattr(params["sub_options"], 'end_time') and params["sub_options"].end_time is not None: + sub_options_dict["end_time"] = params["sub_options"].end_time + # 设置默认值 + if "width" not in sub_options_dict: + sub_options_dict["width"] = "20%" + if "height" not in sub_options_dict: + sub_options_dict["height"] = "20%" + if "pos_x" not in sub_options_dict: + sub_options_dict["pos_x"] = "0" + if "pos_y" not in sub_options_dict: + sub_options_dict["pos_y"] = "0" + ParamObj["sub_options"] = sub_options_dict audioVideoStitchingParams = { "ParamObj": ParamObj, diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index 3eb42f1a..3ad88609 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -1,10 +1,12 @@ import json from src.vod.api.api import VodAPI from volcengine.vod.models.request.request_vod_pb2 import VodUrlUploadRequest +from src.vod.models.request.request_models import BatchUploadUrlItem +from typing import List def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] @mcp.tool() - def video_batch_upload(space_name: str, urls: list[dict[str, any]] = None, ) -> dict: + def video_batch_upload(space_name: str, urls: List[BatchUploadUrlItem] = None, ) -> dict: """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 @@ -14,7 +16,7 @@ def video_batch_upload(space_name: str, urls: list[dict[str, any]] = None, ) -> - space_name:** 必选字段 ** 空间名称 - urls(list[dict[str, any]]): ** 必选字段 ** 资源URL列表,每个元素是一个包含URL信息的字典 - SourceUrl (str):** 必选字段 ** 源文件 URL。 - - FileExtension(str):** 必选字段 ** 文件件后缀,即点播存储中文件的类型 + - FileExtension(str):** 必选字段 ** 文件后缀,即点播存储中文件的类型 - 文件后缀必须以 . 开头,不超过 8 位。 - 当您传入 FileExtension 时,视频点播将生成 32 位随机字符串,和您传入的 FileExtension 共同拼接成文件路径。 Returns: @@ -25,8 +27,8 @@ def video_batch_upload(space_name: str, urls: list[dict[str, any]] = None, ) -> req.SpaceName = space_name for video_info in urls: url_set = req.URLSets.add() - url_set.SourceUrl = video_info['SourceUrl'] - url_set.FileExtension = video_info['FileExtension'] + url_set.SourceUrl = video_info.SourceUrl + url_set.FileExtension = video_info.FileExtension resp = service.upload_media_by_url(req) except Exception as e: raise Exception(f'video_batch_upload failed, space_name: {space_name}, urls: {urls}, error: {e}') diff --git a/server/mcp_server_vod/src/vod/models/__init__.py b/server/mcp_server_vod/src/vod/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/mcp_server_vod/src/vod/models/request/__init__.py b/server/mcp_server_vod/src/vod/models/request/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/mcp_server_vod/src/vod/models/request/request_models.py b/server/mcp_server_vod/src/vod/models/request/request_models.py new file mode 100644 index 00000000..7cec0f89 --- /dev/null +++ b/server/mcp_server_vod/src/vod/models/request/request_models.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, Field +from typing import Optional, Literal + +class BatchUploadUrlItem(BaseModel): + SourceUrl: str + FileExtension: str = Field(description="文件后缀,即点播存储中文件的类型,-必须以 . 开头,不超过 8 位。;当您传入 FileExtension 时,视频点播将生成 32 位随机字符串,和您传入的 FileExtension 共同拼接成文件路径") + +class InputSource(BaseModel): + type: Optional[Literal["directurl", "http", "vid"]] = Field(description="文件类型,vid、directurl、http") + source: str = Field(description="文件信息") + + +class addSubVideoOptions(BaseModel): + height: Optional[str] = Field(description="水印的高度,支持设置为百分比(相对于视频高度)或具体像素值,例如 100% 或 100") + width: Optional[str] = Field(description="水印的宽度,支持设置为百分比(相对于视频高度)或具体像素值,String 类型,例如 100% 或 100") + pos_x: Optional[str] = Field(description="水印在水平方向(X 轴)的位置,以视频左上角为原点,单位:像素。例如值为 0 时,表示水印处于水平方向的最左侧;值为 100 时,表示水印相对原点向右移动 100 像素") + pos_y: Optional[str] = Field(description="水印在垂直方向(Y 轴)的位置,以视频左上角为原点,单位:像素,例如值为 0 时,表示水印在垂直方向的最上侧;值为 100 时,表示水印相对原点向下移动 100 像素") + start_time: Optional[float] = Field(description="水印的开始时间,单位:秒") + end_time: Optional[float] = Field(description="水印的结束时间,单位:秒") From f2db0344870e0ed4f319e8967e9b0019453ea3c2 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Tue, 30 Dec 2025 12:56:10 +0800 Subject: [PATCH 11/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 39 ++++--------------- .../src/vod/mcp_tools/upload.py | 8 ++-- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index eae12a92..d3cc3a3f 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -1,6 +1,6 @@ import json from src.vod.api.api import VodAPI -from typing import List, Optional +from typing import List, Optional,Dict from src.vod.models.request.request_models import InputSource,addSubVideoOptions @@ -451,7 +451,7 @@ def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) - raise Exception("speedup_video: %s" % e, params) @mcp.tool() - def image_to_video(images: List[InputSource], space_name: str, transitions: List[str] = None) -> dict: + def image_to_video(images: List[dict], space_name: str, transitions: List[str] = None) -> dict: """The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition ** Args: - images(list[dict]): ** 必选字段 **,待合成的图片列表,子类型取值如下 @@ -574,7 +574,7 @@ def image_to_video(images: List[InputSource], space_name: str, transitions: List raise Exception("image_to_video: %s" % e, params) @mcp.tool() - def compile_video_audio(video: InputSource, audio: InputSource, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: + def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: """The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing. Args: - video(dict): ** 必选字段 **,视频信息 @@ -746,7 +746,7 @@ def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") raise Exception("extract_audio: %s" % e, params) @mcp.tool() - def mix_audios(audios: List[InputSource], space_name: str) -> dict: + def mix_audios(audios: List[dict], space_name: str) -> dict: """Mix audios Args: - audios(list[dict]): ** 必选字段 **,叠加的音频列表 @@ -826,7 +826,7 @@ def mix_audios(audios: List[InputSource], space_name: str) -> dict: raise Exception("mix_audios: %s" % e, params) @mcp.tool() - def add_sub_video(video: InputSource, sub_video: InputSource, space_name: str, sub_options: Optional[addSubVideoOptions] = None) -> dict: + def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Optional[dict] = None) -> dict: """`水印贴片`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. Note: - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 @@ -897,8 +897,8 @@ def add_sub_video(video: InputSource, sub_video: InputSource, space_name: str, s "sub_video": formattedSubVideoSource, } - if params.get("sub_options"): - if isinstance(params["sub_options"], dict): + if params.get("sub_options") and isinstance(params["sub_options"], dict): + ParamObj["sub_options"] = { "width": params["sub_options"].get("width", "20%"), "height": params["sub_options"].get("height", "20%"), @@ -906,31 +906,6 @@ def add_sub_video(video: InputSource, sub_video: InputSource, space_name: str, s "pos_y": params["sub_options"].get("pos_y", "0"), **params["sub_options"], } - else: - # addSubVideoOptions 对象 - sub_options_dict = {} - if hasattr(params["sub_options"], 'width') and params["sub_options"].width is not None: - sub_options_dict["width"] = params["sub_options"].width - if hasattr(params["sub_options"], 'height') and params["sub_options"].height is not None: - sub_options_dict["height"] = params["sub_options"].height - if hasattr(params["sub_options"], 'pos_x') and params["sub_options"].pos_x is not None: - sub_options_dict["pos_x"] = params["sub_options"].pos_x - if hasattr(params["sub_options"], 'pos_y') and params["sub_options"].pos_y is not None: - sub_options_dict["pos_y"] = params["sub_options"].pos_y - if hasattr(params["sub_options"], 'start_time') and params["sub_options"].start_time is not None: - sub_options_dict["start_time"] = params["sub_options"].start_time - if hasattr(params["sub_options"], 'end_time') and params["sub_options"].end_time is not None: - sub_options_dict["end_time"] = params["sub_options"].end_time - # 设置默认值 - if "width" not in sub_options_dict: - sub_options_dict["width"] = "20%" - if "height" not in sub_options_dict: - sub_options_dict["height"] = "20%" - if "pos_x" not in sub_options_dict: - sub_options_dict["pos_x"] = "0" - if "pos_y" not in sub_options_dict: - sub_options_dict["pos_y"] = "0" - ParamObj["sub_options"] = sub_options_dict audioVideoStitchingParams = { "ParamObj": ParamObj, diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index 3ad88609..f2b2100d 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -6,7 +6,7 @@ def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] @mcp.tool() - def video_batch_upload(space_name: str, urls: List[BatchUploadUrlItem] = None, ) -> dict: + def video_batch_upload(space_name: str, urls: List[dict[str, str]] = None, ) -> dict: """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 @@ -14,7 +14,7 @@ def video_batch_upload(space_name: str, urls: List[BatchUploadUrlItem] = None, ) - SourceUrl 必须是可公网直接访问的文件 URL,而非包含视频的网页 URL。 Args: - space_name:** 必选字段 ** 空间名称 - - urls(list[dict[str, any]]): ** 必选字段 ** 资源URL列表,每个元素是一个包含URL信息的字典 + - urls(list[dict[str, str]]): ** 必选字段 ** 资源URL列表,每个元素是一个包含URL信息的字典 - SourceUrl (str):** 必选字段 ** 源文件 URL。 - FileExtension(str):** 必选字段 ** 文件后缀,即点播存储中文件的类型 - 文件后缀必须以 . 开头,不超过 8 位。 @@ -27,8 +27,8 @@ def video_batch_upload(space_name: str, urls: List[BatchUploadUrlItem] = None, ) req.SpaceName = space_name for video_info in urls: url_set = req.URLSets.add() - url_set.SourceUrl = video_info.SourceUrl - url_set.FileExtension = video_info.FileExtension + url_set.SourceUrl = video_info.get('SourceUrl', '') + url_set.FileExtension = video_info.get('FileExtension', '') resp = service.upload_media_by_url(req) except Exception as e: raise Exception(f'video_batch_upload failed, space_name: {space_name}, urls: {urls}, error: {e}') From 3a56f1726f9cf1d9c8e349483bf4832171684ba0 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Tue, 30 Dec 2025 13:10:04 +0800 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/vod/mcp_tools/upload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index f2b2100d..036477c8 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -6,7 +6,7 @@ def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] @mcp.tool() - def video_batch_upload(space_name: str, urls: List[dict[str, str]] = None, ) -> dict: + def video_batch_upload(space_name: str, urls: List[dict] = None, ) -> dict: """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 From 97d29b2166519fca1a597cfa715368227d218f4b Mon Sep 17 00:00:00 2001 From: tiehongji Date: Tue, 30 Dec 2025 18:09:01 +0800 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E5=AD=97?= =?UTF-8?q?=E5=B9=95=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/vod/mcp_tools/subtitle_processing.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index 71d8efdc..07052195 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -2,12 +2,6 @@ from src.vod.mcp_tools.edit import _format_source import json -def_font_pos_config = { - "height": "20%", - "pos_x": "5%", - "pos_y": "68%", - "width": "90%" -} def create_mcp_server(mcp, public_methods: dict,service): _build_media_input = public_methods["_build_media_input"] @@ -191,17 +185,9 @@ def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_li if params.get("subtitle_config"): # 获取用户提供的 font_pos_config,如果不存在则使用空字典 - user_font_pos_config = params["subtitle_config"].get("font_pos_config", {}) # 合并默认配置和用户配置,用户配置优先级更高 - merged_font_pos_config = {**def_font_pos_config, **user_font_pos_config} - ParamObj["subtitle_config"] = { - **params["subtitle_config"], - "font_pos_config": merged_font_pos_config - } - else: - ParamObj["subtitle_config"] = { - "font_pos_config": def_font_pos_config - } + ParamObj["subtitle_config"] = params["subtitle_config"] + audioVideoStitchingParams = { "ParamObj": ParamObj, From ccba4f7369ea63bbb70e8a73a579c529137ac15b Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 31 Dec 2025 10:58:29 +0800 Subject: [PATCH 14/26] =?UTF-8?q?feat:=20=E8=A7=A3=E5=86=B3=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_service.py | 1 + server/mcp_server_vod/src/base/credential.py | 3 +++ .../mcp_server_vod/src/vod/mcp_tools/video_play.py | 12 +----------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_service.py b/server/mcp_server_vod/src/base/base_service.py index 17bd0677..22ab0cda 100644 --- a/server/mcp_server_vod/src/base/base_service.py +++ b/server/mcp_server_vod/src/base/base_service.py @@ -53,6 +53,7 @@ def set_api_info(self, api_info): def mcp_get(self, action, params={}, doseq=0): self.update_credentials_from_mcp() + res = self.get(action, params, doseq) if res == '': raise Exception("%s: empty response" % action) diff --git a/server/mcp_server_vod/src/base/credential.py b/server/mcp_server_vod/src/base/credential.py index 072ec78b..c9e42083 100644 --- a/server/mcp_server_vod/src/base/credential.py +++ b/server/mcp_server_vod/src/base/credential.py @@ -55,6 +55,9 @@ def get_volcengine_credentials_from_context(ctx: Optional[Context[ServerSession, return None headers = raw_request.headers + print("headers", headers) + if not headers: + return None # 从 header 中读取凭证信息(使用与环境变量一致的命名,同时支持连字符和下划线格式) # 优先使用下划线格式(与环境变量一致),也支持连字符格式和 x- 前缀 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index d4a1e54c..af59a450 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -11,8 +11,6 @@ import re import urllib.request import urllib.error -import logging -import inspect @@ -354,14 +352,6 @@ def get_video_audio_info(type: str, source: str, space_name: str) -> dict: - BitsPerSample(str): 音频采样码率,单位 bit。 - PlayURL(str): 播放地址 """ - frame = inspect.currentframe() - arguments = inspect.getargvalues(frame).locals - ctx = mcp.get_context() - raw_request: Request = ctx.request_context.request.headers - logging.info(f"get_play_url: {space_name} {source} {type}") - logging.info(f"get_play_url_ctx: {raw_request.get('ak')}") - logging.info(f"get_play_url_ctx: {raw_request.get('sk')}") - print(f"get_play_urframe: {arguments}") try: params = {"type": type, "source": source, "space_name": space_name} @@ -411,6 +401,6 @@ def get_video_audio_info(type: str, source: str, space_name: str) -> dict: else: raise ValueError(f"get_video_audio_info: unsupported type: {sourceType}") except Exception as e: - raise Exception("get_video_audio_info: %s" % e, params) + raise Exception("get_video_audio_info-1: %s" % e, params) From 5393f2d534ade71b6fbe0bb40c44cf806e9a8117 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 31 Dec 2025 11:52:27 +0800 Subject: [PATCH 15/26] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=BB?= =?UTF-8?q?=E4=B8=AD=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 14 ++++++++------ server/mcp_server_vod/src/vod/mcp_tools/edit.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index 08709559..02ceee64 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -82,14 +82,15 @@ "audio_video_stitching", "audio_video_clipping", "get_v_creative_task_result", + "flip_video", + "speedup_video" "image_to_video", "compile_video_audio", "extract_audio", "mix_audios", "add_sub_video", - "add_subtitle", - "flip_video", - "speedup_video" + + ], # video_play 分组 "video_play": [ @@ -112,9 +113,10 @@ ], # subtitle_processing 分组 "subtitle_processing": [ - "subtitles_removal_task", - "ocr_task", - "asr_task" + "asr_speech_to_text_task", + "ocr_text_to_subtitles_task", + "video_subtitles_removal_task" + "add_subtitle", ], # audio_processing 分组 "audio_processing": [ diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index d3cc3a3f..51ed8f36 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -827,7 +827,7 @@ def mix_audios(audios: List[dict], space_name: str) -> dict: @mcp.tool() def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Optional[dict] = None) -> dict: - """`水印贴片`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. + """`水印贴片`, `画中画`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. Note: - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 Args: From 9c3762f9a1d72a80fda58ba1a4db34aab81263d3 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 31 Dec 2025 12:01:38 +0800 Subject: [PATCH 16/26] =?UTF-8?q?feat:=20=E5=88=86=E7=BB=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index 02ceee64..92f2bcc8 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -115,8 +115,8 @@ "subtitle_processing": [ "asr_speech_to_text_task", "ocr_text_to_subtitles_task", - "video_subtitles_removal_task" - "add_subtitle", + "video_subtitles_removal_task", + "add_subtitle", ], # audio_processing 分组 "audio_processing": [ @@ -297,6 +297,7 @@ async def list_tools(self): logger.info(f"BaseMCP.list_tools: returning all {len(res)} tools (all groups)") return res if group_name in TOOL_GROUP_MAP: + logger.info(f"TOOL_GROUP_MAP {TOOL_GROUP_MAP[group_name]} tools in group {group_name}") allowed_tools.update(TOOL_GROUP_MAP[group_name]) # 过滤工具 From 22cc18660eb736ed584dca03192021e6429d4432 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 31 Dec 2025 15:27:31 +0800 Subject: [PATCH 17/26] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/vod/mcp_tools/subtitle_processing.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index 07052195..3f0012ec 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -136,6 +136,17 @@ def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_li - subtitle_config(dict): ** 非必选字段 **,字幕配置信息,包含如下字段: - font_size(int): 字幕的字体大小,Integer 类型,单位:像素。默认200 - font_type(str): 字幕的字体 ID,String 类型,详情请参考字体 ID。默认 SY_BLACK(思源黑体) + - 字体名称:站酷意大利体,字体id:1187225 + - 字体名称:站酷仓耳渔阳体,字体id:1187223 + - 字体名称:站酷高端黑,字体id:1187221 + - 字体名称:站酷酷黑体,字体id:1187219 + - 字体名称:站酷快乐体,字体id:1187217 + - 字体名称:站酷文艺体,字体id:1187213 + - 字体名称:站酷小薇 LOGO 体,字体id:1187211 + - 字体名称:思源黑体,字体id:SY_Black + - 字体名称:阿里巴巴普惠体,字体id:ALi_PuHui + - 字体名称:庞门正道标题体,字体id:PM_ZhengDao + (注:站酷意大利体标注“不支持中文”,但按要求仅提取分类与ID) - font_color(str): 字幕的字体颜色,String 类型,RGBA 格式,默认白色 #FFFFFFFF。 - background_border_width(number): 字幕背景的边框宽度,Number 类型,单位:像素。 - background_color(str): 字幕的背景颜色,String 类型,RGBA 格式,默认 #00000000。 From c6f8ff4bcdbe81e606f39b8f12106392d6ab67aa Mon Sep 17 00:00:00 2001 From: tiehongji Date: Tue, 6 Jan 2026 15:36:10 +0800 Subject: [PATCH 18/26] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E6=B0=B4?= =?UTF-8?q?=E5=8D=B0=E8=B4=B4=E7=89=87=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/vod/mcp_tools/edit.py | 4 ---- server/mcp_server_vod/src/vod/mcp_tools/video_play.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index 51ed8f36..0acf40f5 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -900,10 +900,6 @@ def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Op if params.get("sub_options") and isinstance(params["sub_options"], dict): ParamObj["sub_options"] = { - "width": params["sub_options"].get("width", "20%"), - "height": params["sub_options"].get("height", "20%"), - "pos_x": params["sub_options"].get("pos_x", "0"), - "pos_y": params["sub_options"].get("pos_y", "0"), **params["sub_options"], } diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index af59a450..80296817 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -302,7 +302,7 @@ def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: result["Channels"] = int(audio_stream.get("channels", 0)) if audio_stream.get("channels") else 0 result["SampleRate"] = str(audio_stream.get("sample_rate", "")) bits_per_sample = audio_stream.get("bits_per_sample") - if bits_per_sample: + if bits_per_sample is not None and bits_per_sample != 0: result["BitsPerSample"] = str(bits_per_sample) return result From 474a0af8bbbe17b88696635cb4c809c8fce03771 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 7 Jan 2026 17:47:04 +0800 Subject: [PATCH 19/26] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E5=80=8D=E9=80=9F=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 5 +- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index 92f2bcc8..df95df16 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -82,8 +82,9 @@ "audio_video_stitching", "audio_video_clipping", "get_v_creative_task_result", - "flip_video", - "speedup_video" + "flip_video", + "speedup_video", + "speedup_audio", "image_to_video", "compile_video_audio", "extract_audio", diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index 0acf40f5..f11f06f8 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -430,6 +430,73 @@ def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) - "WorkflowId": "loki://165223469", } + reqs = None + try: + reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) + if isinstance(reqs, str): + reqs = json.loads(reqs) + reqsTmp = reqs.get('Result', {}) + BaseResp = reqsTmp.get("BaseResp", {}) + return json.dumps({ + "VCreativeId": reqsTmp.get("VCreativeId", ""), + "Code": reqsTmp.get("Code"), + "StatusMessage": BaseResp.get("StatusMessage", ""), + "StatusCode": BaseResp.get("StatusCode", 0), + }) + else: + return reqs + except Exception as e: + raise Exception("speedup_video: %s" % e, params) + except Exception as e: + raise Exception("speedup_video: %s" % e, params) + @mcp.tool() + def speedup_audio(type: str, source: str, space_name: str, speed: float = 1.0) -> dict: + """Adjust the speed multiplier of the audio, of type Float, with a range from 0.1 to 4. + Args: + - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 + - directurl + - http + - vid + - source(str): ** 必选字段 **, 视频文件信息 + - speed(float): ** 非必选字段 **, 调整速度的倍数,Float类型,取值范围为** 0.1~4 **。参考如下: + - 0.1:放慢至原速的 0.1 倍。 + - 1(默认值):原速。 + - 4:加速至原速的 4 倍。 + - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + Returns: + - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 + - Code(int): 任务状态码。为 0 表示任务执行成功。 + - StatusMessage(str): 接口请求的状态信息。当 StatusCode 为 0 时,此时该字段返回 success,表示成功;其他状态码时,该字段会返回具体的错误信息。 + - StatusCode(int): 接口请求的状态码。0 表示成功,其他值表示不同的错误状态。 + """ + try: + params = {"type": type, "source": source, "space_name": space_name, "speed": speed} + if "space_name" not in params: + raise ValueError("speedup_video: params must contain space_name") + if not isinstance(params["space_name"], str): + raise TypeError("speedup_video: params['space_name'] must be a string") + if not params["space_name"].strip(): + raise ValueError("speedup_video: params['space_name'] cannot be empty") + if "source" not in params: + raise ValueError("speedup_video: params must contain source") + + speedValue = params.get("speed", 1.0) + if speedValue < 0.1 or speedValue > 4: + raise ValueError("speedup_video: speed must be between 0.1 and 4") + + formattedSource = _format_source(params.get("type", "vid"), params.get("source", "")) + ParamObj = { + "space_name": params["space_name"], + "source": formattedSource, + "speed": speedValue, + } + + audioVideoStitchingParams = { + "ParamObj": ParamObj, + "Uploader": params["space_name"], + "WorkflowId": "loki://174663067", + } + reqs = None try: reqs = service.mcp_post("McpAsyncVCreativeTask", {}, json.dumps(audioVideoStitchingParams)) From fecfa9fdabb4697cc78c8d9f3213e0a6eae82a66 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 7 Jan 2026 21:57:19 +0800 Subject: [PATCH 20/26] =?UTF-8?q?feat:=20=E8=8E=B7=E5=8F=96=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=93=BE=E6=8E=A5=E5=A2=9E=E5=8A=A0vid=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/vod/mcp_server.py | 36 ------------------- .../src/vod/mcp_tools/video_play.py | 24 +++++++++---- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/server/mcp_server_vod/src/vod/mcp_server.py b/server/mcp_server_vod/src/vod/mcp_server.py index 6c2792b3..efccb26a 100644 --- a/server/mcp_server_vod/src/vod/mcp_server.py +++ b/server/mcp_server_vod/src/vod/mcp_server.py @@ -72,42 +72,6 @@ def create_mcp_server(mcp: FastMCP = None): mcp.set_base_mcp_store({ 'apiRequestInstance': service }) - # ## init tool groups - # # 优先级:1. groups 参数 2. MCP header 3. 环境变量 4. 默认值 - # current_tool_groups = [] - - # if groups is not None: - # if ALL_GROUPS in groups: - # current_tool_groups = AVAILABLE_GROUPS - # else: - # current_tool_groups = groups - # print(f"[MCP] Loaded tool groups from parameter: {current_tool_groups}") - # else: - # # 尝试从 MCP 上下文 header 获取 - # header_groups = get_tool_groups_from_context(mcp) - # if header_groups: - # if ALL_GROUPS in header_groups: - # current_tool_groups = AVAILABLE_GROUPS - # else: - # current_tool_groups = header_groups - # print(f"[MCP] Loaded tool groups from header: {current_tool_groups}") - # else: - # # 从环境变量获取 - # env_type = os.getenv("MCP_TOOL_GROUPS") - # if env_type is not None: - # env_grops = [group.strip() for group in env_type.split(",") if group.strip()] - # try: - # if ALL_GROUPS in env_grops: - # current_tool_groups = AVAILABLE_GROUPS - # else: - # current_tool_groups = env_grops - # print(f"[MCP] Loaded tool groups from environment: {current_tool_groups}") - # except Exception as e: - # print(f"[MCP] Error parsing MCP_TOOL_GROUPS environment variable: {e}") - # current_tool_groups = DEFAULT_GROUPS - # else: - # current_tool_groups = DEFAULT_GROUPS - # print(f"[MCP] Using default tool groups: {current_tool_groups}") ## update media publish status def update_media_publish_status (vid: str, SpaceName: str, PublishStatus: str) -> str: diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 80296817..536036e8 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -312,18 +312,30 @@ def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): @mcp.tool() - def get_play_url(spaceName: str, fileName: str, expired_minutes: int = 60) -> Any: + def get_play_url( type: str, source: str, spaceName: str, expired_minutes: int = 60) -> Any: """ - Obtain the video playback link through `fileName`, 通过 fileName 获取视频播放地址 + Obtain the video playback link through `directurl` or `vid`, 通过 directurl or vid 获取视频播放地址, + Note: + expired_minutes 仅在 directurl 模式下生效 Args: - spaceName: **必选字段** 空间名称 - - fileName: **必选字段** 文件名 - - expired_minutes: **可选字段** 过期时间,默认60分钟 + - source: **必选字段** 文件名 or vid + - 文件名:直接传入文件名,例如 `test.mp4` + - vid:直接传入 vid + - type: **必选字段** 文件类型,默认值为 `directurl` 。字段取值如下 + - directurl:仅仅支持点播存储 + - vid + - expired_minutes: **可选字段** 过期时间,默认60分钟, vid 模式下不生效 Returns: - 播放地址 """ - - return public_methods["get_play_url"](spaceName, fileName, expired_minutes) + if type == "directurl": + return public_methods["get_play_url"](spaceName, source, expired_minutes) + elif type == "vid": + videoInfo = public_methods["get_play_video_info"](source, spaceName) + if isinstance(videoInfo, str): + videoInfo = json.loads(videoInfo) + return videoInfo.get("PlayURL", "") @mcp.tool() def get_video_audio_info(type: str, source: str, space_name: str) -> dict: From 5df9daf667544d886f7a4ebdebce0eaa00ba6bdc Mon Sep 17 00:00:00 2001 From: tiehongji Date: Wed, 14 Jan 2026 16:23:06 +0800 Subject: [PATCH 21/26] =?UTF-8?q?feat:=20=E5=85=BC=E5=AE=B9groups=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index df95df16..eb2231f5 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -237,7 +237,13 @@ def _handle_display_tools(self) -> tuple[Optional[str], Optional[list[str]]]: # 优先级2: 从环境变量获取 tool_type_e = environ.get(VOLCENGINE_TOOLS_TYPE_ENV) tools_source_e = environ.get(VOLCENGINE_TOOLS_SOURCE_ENV) - + + if tool_type_e is None and tools_source_e is None: + source_groups = environ.get("MCP_TOOL_GROUPS") + if source_groups: + tool_type_e = 'groups' + tools_source_e = source_groups + if tool_type_e and tools_source_e: if tool_type_e in ['tools', 'groups']: tools_type = tool_type_e @@ -245,6 +251,7 @@ def _handle_display_tools(self) -> tuple[Optional[str], Optional[list[str]]]: logger.debug(f"Got tools_type and source from environment: {tools_type}, {tools_source}") return (tools_type, tools_source) + # 优先级3: 从 _base_mcp_store 获取 tools_type_store = self.get_base_mcp_store('tools_type') source_store = self.get_base_mcp_store('source') From 9398332dde2a1e236d5d6a2b734debf6d6ad8ca6 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Fri, 23 Jan 2026 16:35:40 +0800 Subject: [PATCH 22/26] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 58 ++++- .../mcp_server_vod/src/base/base_service.py | 4 +- server/mcp_server_vod/src/base/constant.py | 4 +- server/mcp_server_vod/src/vod/mcp_server.py | 4 +- .../src/vod/mcp_tools/audio_processing.py | 30 ++- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 223 ++++++++++++------ .../src/vod/mcp_tools/intelligent_matting.py | 47 ++-- .../src/vod/mcp_tools/intelligent_slicing.py | 16 +- .../src/vod/mcp_tools/media_tasks.py | 16 +- .../src/vod/mcp_tools/subtitle_processing.py | 68 ++++-- .../src/vod/mcp_tools/upload.py | 22 +- .../src/vod/mcp_tools/video_enhancement.py | 51 ++-- .../src/vod/mcp_tools/video_play.py | 28 ++- 13 files changed, 391 insertions(+), 180 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index eb2231f5..df32e053 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -1,8 +1,14 @@ -from requests import get from mcp.server.fastmcp import FastMCP +from mcp.shared.context import LifespanContextT, RequestT import logging from typing import Dict, Any,Optional -from mcp.server.fastmcp import Context +from collections.abc import ( + Sequence, +) + +from mcp.server.fastmcp.server import Context +from mcp.server.session import ServerSessionT +from mcp.types import ContentBlock from mcp.server.session import ServerSession from starlette.requests import Request from src.base.constant import ( @@ -13,6 +19,8 @@ # VOLCENGINE_REGION_HEADER, VOLCENGINE_TOOLS_SOURCE_HEADER, VOLCENGINE_TOOLS_TYPE_HEADER, + VOLCENGINE_SPACE_NAME_HEADER, + VOLCENGINE_SPACE_NAME_ENV, # VOLCENGINE_ACCESS_KEY_ENV, # VOLCENGINE_SECRET_KEY_ENV, @@ -90,8 +98,7 @@ "extract_audio", "mix_audios", "add_sub_video", - - + "wait_for_v_creative_task_result", ], # video_play 分组 "video_play": [ @@ -321,4 +328,47 @@ async def list_tools(self): import traceback logger.error(traceback.format_exc()) return [] + async def call_tool(self, + name: str, + arguments: dict[str, Any], + ) -> Sequence[ContentBlock] | dict[str, Any]: + """Call a tool by name with arguments.""" + try: + context: Optional[Request] = self.get_request_ctx() + ctx: Optional[Context[ServerSession, object]] = self.get_context() + arguments['ctx'] = ctx + if self._base_mcp_store.get('apiRequestInstance'): + apiRequestInstance = self._base_mcp_store['apiRequestInstance'] + if hasattr(apiRequestInstance, 'set_headers') and name: + apiRequestInstance.set_headers('x-tt-tools-name', name) + # 兼容处理 space_name, spaceName, space + space_name_value = None + for key in ['space_name', 'spaceName', 'space']: + if key in arguments and arguments[key] and isinstance(arguments[key], str) and arguments[key].strip(): + space_name_value = arguments[key].strip() + break + + if space_name_value: + arguments['space_name'] = space_name_value + else: + space_name_env = environ.get(VOLCENGINE_SPACE_NAME_ENV) + if context and hasattr(context, 'headers'): + headers = context.headers + space_name_h = headers.get(VOLCENGINE_SPACE_NAME_HEADER) + if space_name_h: + arguments['space_name'] = space_name_h.strip() + elif space_name_env: + arguments['space_name'] = space_name_env.strip() + elif space_name_env: + arguments['space_name'] = space_name_env.strip() + space_name = arguments.get('space_name') + if not space_name or not isinstance(space_name, str) or not space_name.strip(): + raise Exception('space_name is required') + print(f"space_name: {space_name}",arguments) + return await super().call_tool(name, arguments) + except Exception as e: + logger.error(f"BaseMCP.call_tool failed with error: {e}") + import traceback + logger.error(traceback.format_exc()) + raise e diff --git a/server/mcp_server_vod/src/base/base_service.py b/server/mcp_server_vod/src/base/base_service.py index 22ab0cda..1d920644 100644 --- a/server/mcp_server_vod/src/base/base_service.py +++ b/server/mcp_server_vod/src/base/base_service.py @@ -50,7 +50,8 @@ def get_api_info(): def set_api_info(self, api_info): self.api_info = {**self.api_info, **api_info} return - + def set_headers(self, key: str, value: str): + self.service_info.header[key] = value def mcp_get(self, action, params={}, doseq=0): self.update_credentials_from_mcp() @@ -110,3 +111,4 @@ def update_credentials_from_context(self, ctx: Optional[Context[ServerSession, o logging.debug("Credentials updated from MCP context") except Exception as e: logging.warning(f"Failed to update credentials from context: {e}") + diff --git a/server/mcp_server_vod/src/base/constant.py b/server/mcp_server_vod/src/base/constant.py index 00b09888..d0cbe942 100644 --- a/server/mcp_server_vod/src/base/constant.py +++ b/server/mcp_server_vod/src/base/constant.py @@ -5,6 +5,7 @@ VOLCENGINE_REGION_HEADER = 'x-tt-region' VOLCENGINE_TOOLS_SOURCE_HEADER = 'x-tt-tools-source' VOLCENGINE_TOOLS_TYPE_HEADER = 'x-tt-tools-type' +VOLCENGINE_SPACE_NAME_HEADER = 'x-tt-space-name' @@ -14,4 +15,5 @@ VOLCENGINE_HOST_ENV = 'VOLCENGINE_HOST' VOLCENGINE_REGION_ENV = 'VOLCENGINE_REGION' VOLCENGINE_TOOLS_SOURCE_ENV = 'MCP_TOOLS_SOURCE' -VOLCENGINE_TOOLS_TYPE_ENV = 'MCP_TOOLS_TYPE' \ No newline at end of file +VOLCENGINE_TOOLS_TYPE_ENV = 'MCP_TOOLS_TYPE' +VOLCENGINE_SPACE_NAME_ENV = 'MCP_SPACE_NAME' \ No newline at end of file diff --git a/server/mcp_server_vod/src/vod/mcp_server.py b/server/mcp_server_vod/src/vod/mcp_server.py index efccb26a..ffb44c5f 100644 --- a/server/mcp_server_vod/src/vod/mcp_server.py +++ b/server/mcp_server_vod/src/vod/mcp_server.py @@ -1,14 +1,12 @@ import importlib -from tokenize import group -from typing import Optional from src.vod.api.api import VodAPI from src.vod.mcp_tools.media_tasks import create_transcode_result_server from src.vod.utils.transcode import register_transcode_base_fn from src.vod.mcp_tools.video_play import register_video_play_methods from mcp.server.fastmcp import FastMCP from pathlib import Path -from src.base.base_mcp import TOOL_NAME_TO_GROUP_MAP, TOOL_GROUP_MAP + import json import os diff --git a/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py index 75183550..8ba50bac 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py @@ -5,22 +5,26 @@ def create_mcp_server(mcp, public_methods: dict): _start_execution = public_methods["_start_execution"] # audio noise reduction - @mcp.tool() - def audio_noise_reduction_task(type: str, audio: str, spaceName: str) -> Any: - """ Audio noise reduction, supporting two input modes: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Audio noise reduction, supporting two input modes: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def audio_noise_reduction_task(type: str, audio: str, space_name: str = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - audio: ** 必选字段 **, 音频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `audioNoiseReduction` """ - media_input = _build_media_input(type, audio, spaceName) + media_input = _build_media_input(type, audio, space_name) params = { "Input": media_input, "Operation": { @@ -34,22 +38,26 @@ def audio_noise_reduction_task(type: str, audio: str, spaceName: str) -> Any: return _start_execution(params) # voice separation - @mcp.tool() - def voice_separation_task(type: str, video: str, spaceName: str) -> Any: - """Voice separation is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Voice separation is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def voice_separation_task(type: str, video: str, space_name: str = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `voiceSeparation` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index f11f06f8..7463eb88 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -1,7 +1,10 @@ import json +import asyncio +from venv import logger from src.vod.api.api import VodAPI -from typing import List, Optional,Dict -from src.vod.models.request.request_models import InputSource,addSubVideoOptions +from mcp.server.fastmcp import Context +from typing import List, Optional, Callable, Any + def _format_source(type: str, source: str) -> str: @@ -29,17 +32,21 @@ def _format_source(type: str, source: str) -> str: def create_mcp_server(mcp,public_methods: dict, service: VodAPI, ): - @mcp.tool() - def audio_video_stitching(type: str, SpaceName: str, videos: List[str] = None, audios: List[str] = None, transitions: List[str] = None) -> dict: - """ Carry out video stitching, audio stitching, and support for transitions and other capabilities,需要参考 Note 中的要求。 - Note: - - **audio splicing does not support transitions. ** - - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** - - ** directurl://{fileName} 格式指定资源的 FileName。示例:directurl://test.mp3** - - ** http(s):// 格式指定资源的 URL。示例:http://example.com/test.mp4** + @mcp.tool( + description=""" + Carry out video stitching, audio stitching, and support for transitions and other capabilities,需要参考 Note 中的要求。 + Note: + - **audio splicing does not support transitions. ** + - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** + - ** directurl://{fileName} 格式指定资源的 FileName。示例:directurl://test.mp3** + - ** http(s):// 格式指定资源的 URL。示例:http://example.com/test.mp4** + """, + ) + def audio_video_stitching(type: str, space_name: str = None, videos: List[str] = None, audios: List[str] = None, transitions: List[str] = None) -> dict: + """ Args: - type(str): ** 必选字段 ** , 拼接类型。 `audio` | `video` - - SpaceName(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 - videos(List[str]): **视频下必选字段,音频下不传递 ** - 待拼接的视频列表:支持 ** vid:// ** 、 ** http:// ** 格式,** directUrl:// ** 格式 *** 视频要求: *** @@ -72,8 +79,10 @@ def audio_video_stitching(type: str, SpaceName: str, videos: List[str] = None, a - StatusCode(int): 接口请求的状态码。0表示成功,其他值表示不同的错误状态。 """ + print(f"audio_video_stitchingspace_name: {space_name}") + try: - params = {"type": type, "SpaceName": SpaceName, "videos": videos, "audios": audios, "transitions": transitions} + params = {"type": type, "SpaceName": space_name, "videos": videos, "audios": audios, "transitions": transitions} if "SpaceName" not in params: raise ValueError("audio_video_stitching: params must contain SpaceName") if not isinstance(params["SpaceName"], str): @@ -159,8 +168,17 @@ def audio_video_stitching(type: str, SpaceName: str, videos: List[str] = None, a except Exception as e: raise Exception("audio_video_stitching: %s" % e, params) - @mcp.tool() - def audio_video_clipping(type: str, SpaceName: str, source: str, start_time: float, end_time: float) -> dict: + @mcp.tool( + description=""" + Invoke the current tools to complete the cropping of audio and video,需要参考 Note 中的要求。 + Note: + - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** + - ** directurl://{fileName} 格式指定资源的 FileName。示例:directurl://test.mp3** + - ** http(s):// 格式指定资源的 URL。示例:http://example.com/test.mp4** + - `start_time` 和 `end_time` 必须同时指定,且 `end_time` 必须大于 `start_time` + """, + ) + def audio_video_clipping(type: str, source: str, start_time: float, end_time: float, space_name: str = None) -> dict: """ Invoke the current tools to complete the cropping of audio and video,需要参考 Note 中的要求。 Note: - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** @@ -169,7 +187,7 @@ def audio_video_clipping(type: str, SpaceName: str, source: str, start_time: flo - `start_time` 和 `end_time` 必须同时指定,且 `end_time` 必须大于 `start_time` Args: - type(str): ** 必选字段 ** , 拼接类型。 `audio` | `video` - - SpaceName(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 - source(str): ** 必选字段 ** - 输入视频:支持 ** vid:// ** 、 ** http:// ** 格式,** directUrl:// ** 格式 - end_time(float): ** 必选字段 ** @@ -184,7 +202,7 @@ def audio_video_clipping(type: str, SpaceName: str, source: str, start_time: flo """ try: - params = {"type": type, "SpaceName": SpaceName, "source": source, "start_time": start_time, "end_time": end_time} + params = {"type": type, "SpaceName": space_name, "source": source, "start_time": start_time, "end_time": end_time} if "SpaceName" not in params: raise Exception("audio_video_clipping: params must contain SpaceName", params) if not isinstance(params["SpaceName"], str): @@ -238,27 +256,8 @@ def audio_video_clipping(type: str, SpaceName: str, source: str, start_time: flo except Exception as e: raise Exception("audio_video_clipping: %s" % e) - @mcp.tool() - def get_v_creative_task_result(VCreativeId: str, SpaceName: str) -> dict: - """ Query the execution status and results of video stitching, audio stitching, and audio-video cropping by using the `VCreativeId`. - Note: - - **audio splicing does not support transitions. ** - Args: - - VCreativeId(str): `String type`, ID for AI intelligent trimming task. - - SpaceName(str): `String type`, space name. - Returns: - - Status(str): 任务的当前处理状态。 - - running: 执行中 - - success: 执行成功 - - failed_run: 执行失败 - - OutputJson(dict[str,Any]): 任务的输出结果 - - vid: 输出的点播空间vid - - resolution: 分辨率 - - filename: 文件名 - - url: 产物链接,仅当任务成功时返回 - - duration: 时长, 单位:秒(s) - """ - params={"VCreativeId": VCreativeId, "SpaceName": SpaceName} + def _get_v_creative_task_result_impl(VCreativeId: str, space_name: str = None) -> dict: + params={"VCreativeId": VCreativeId, "SpaceName": space_name} if "VCreativeId" not in params: raise ValueError("get_v_creative_task_result: params must contain VCreativeId") if not isinstance(params["VCreativeId"], str): @@ -319,9 +318,83 @@ def get_v_creative_task_result(VCreativeId: str, SpaceName: str) -> dict: else: return reqs - @mcp.tool() - def flip_video(type: str, source: str, space_name: str, flip_x: bool = False, flip_y: bool = False) -> dict: - """Video rotation capability is supported, allowing for `vertical and horizontal flipping of the video`. ** Default: No flipping ** + # @mcp.tool( + # description=""" + # Query the execution status and results of video stitching, audio stitching, and audio-video cropping by using the `VCreativeId`. + # Note: + # - **audio splicing does not support transitions. ** + # """, + # ) + # def get_v_creative_task_result(VCreativeId: str, space_name: str = None) -> dict: + # """ + # Args: + # - VCreativeId(str): `String type`, ID for AI intelligent trimming task. + # - space_name(str): `String type`, space name. ** 非必选字段 ** + # Returns: + # - Status(str): 任务的当前处理状态。 + # - running: 执行中 + # - success: 执行成功 + # - failed_run: 执行失败 + # - OutputJson(dict[str,Any]): 任务的输出结果 + # - vid: 输出的点播空间vid + # - resolution: 分辨率 + # - filename: 文件名 + # - url: 产物链接,仅当任务成功时返回 + # - duration: 时长, 单位:秒(s) + # """ + # return _get_v_creative_task_result_impl(VCreativeId, space_name) + + @mcp.tool( + description="Poll the execution status and results of video stitching, audio stitching, and audio-video cropping by using the `VCreativeId` until success or timeout.", + ) + async def get_v_creative_task_result(VCreativeId: str,space_name: str = None, interval: float = 2.0, max_retries: int = 10, ctx: Context = None) -> dict: + """ + Args: + - VCreativeId(str): `String type`, ID for AI intelligent trimming task. + - space_name(str): `String type`, space name. ** 非必选字段 ** + - interval(float): Polling interval in seconds. Default is 2.0. + - max_retries(int): Maximum number of retries. Default is 30. + Returns: + - Status(str): 任务的当前处理状态。 + - running: 执行中 + - success: 执行成功 + - failed_run: 执行失败 + - OutputJson(dict[str,Any]): 任务的输出结果 + - vid: 输出的点播空间vid + - resolution: 分辨率 + - filename: 文件名 + - url: 产物链接,仅当任务成功时返回 + - duration: 时长, 单位:秒(s) + """ + result_json = None + for i in range(max_retries): + try: + logger.info(f"wait_for_v_creative_task_result: VCreativeId={VCreativeId}, space_name={space_name}, i={i}, ctx=",ctx.report_progress) + result_json = _get_v_creative_task_result_impl(VCreativeId, space_name) + result = json.loads(result_json) if isinstance(result_json, str) else result_json + status = result.get("Status") + if status == "success" or status == "failed_run": + if ctx: + await ctx.report_progress(progress=100, total=100) + return result_json + if status == "running": + if ctx: + await ctx.report_progress(progress=50, total=100) + return result_json + except Exception as e: + raise e + + await asyncio.sleep(interval) + + return result_json + + + + @mcp.tool( + description="Video rotation capability is supported, allowing for `vertical and horizontal flipping of the video`. ** Default: No flipping **", + ) + def flip_video(type: str, source: str, space_name: str = None, flip_x: bool = False, flip_y: bool = False) -> dict: + """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -330,7 +403,7 @@ def flip_video(type: str, source: str, space_name: str, flip_x: bool = False, fl - source(str): ** 必选字段 **, 视频文件信息 - flip_x(bool): ** 非必选字段 **, 是否对视频进行上下翻转。Boolean 类型,默认值为 false,表示不翻转。 - flip_y(bool): ** 非必选字段 **, 是否对视频进行左右翻转。Boolean 类型,默认值为 false,表示不翻转。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -382,9 +455,11 @@ def flip_video(type: str, source: str, space_name: str, flip_x: bool = False, fl except Exception as e: raise Exception("flip_video: %s" % e, params) - @mcp.tool() - def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) -> dict: - """Adjust the speed multiplier of the video, of type Float, with a range from 0.1 to 4. + @mcp.tool( + description="Adjust the speed multiplier of the video, of type Float, with a range from 0.1 to 4.", + ) + def speedup_video(type: str, source: str, space_name: str = None, speed: float = 1.0) -> dict: + """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -395,7 +470,7 @@ def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) - - 0.1:放慢至原速的 0.1 倍。 - 1(默认值):原速。 - 4:加速至原速的 4 倍。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -449,9 +524,11 @@ def speedup_video(type: str, source: str, space_name: str, speed: float = 1.0) - raise Exception("speedup_video: %s" % e, params) except Exception as e: raise Exception("speedup_video: %s" % e, params) - @mcp.tool() - def speedup_audio(type: str, source: str, space_name: str, speed: float = 1.0) -> dict: - """Adjust the speed multiplier of the audio, of type Float, with a range from 0.1 to 4. + @mcp.tool( + description="Adjust the speed multiplier of the audio, of type Float, with a range from 0.1 to 4.", + ) + def speedup_audio(type: str, source: str, space_name: str = None, speed: float = 1.0) -> dict: + """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -462,7 +539,7 @@ def speedup_audio(type: str, source: str, space_name: str, speed: float = 1.0) - - 0.1:放慢至原速的 0.1 倍。 - 1(默认值):原速。 - 4:加速至原速的 4 倍。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -517,8 +594,10 @@ def speedup_audio(type: str, source: str, space_name: str, speed: float = 1.0) - except Exception as e: raise Exception("speedup_video: %s" % e, params) - @mcp.tool() - def image_to_video(images: List[dict], space_name: str, transitions: List[str] = None) -> dict: + @mcp.tool( + description="The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition **", + ) + def image_to_video(images: List[dict], space_name: str = None, transitions: List[str] = None) -> dict: """The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition ** Args: - images(list[dict]): ** 必选字段 **,待合成的图片列表,子类型取值如下 @@ -553,7 +632,7 @@ def image_to_video(images: List[dict], space_name: str, transitions: List[str] = - 分类:圆形交替,ID:1182378 - 注意:如果不提供,则没有转场 - 当视频数量超过转场数量 2 个及以上时,系统将自动循环使用转场。例如有 10 个视频,2 种转场效果,那么在 9 处拼接点上,这 2 种转场效果将被依次循环使用。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -640,9 +719,11 @@ def image_to_video(images: List[dict], space_name: str, transitions: List[str] = except Exception as e: raise Exception("image_to_video: %s" % e, params) - @mcp.tool() - def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: - """The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing. + @mcp.tool( + description="The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing.", + ) + def compile_video_audio(video: dict, audio: dict, space_name: str = None, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: + """ Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -668,7 +749,7 @@ def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_rese - sync_method(str): ** 非必选字段 **, **设置 is_video_audio_sync 为 true 时生效**;指定对齐方式,支持通过裁剪或加速的方式,对齐音频和视频的时长。可选项:speed、trim。 - speed:通过加快音频或视频的速度,对齐音频和视频的时长。 - trim:【默认值】通过裁剪音频或视频,对齐音频和视频的时长。从头开始计算并裁剪。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -747,9 +828,11 @@ def compile_video_audio(video: dict, audio: dict, space_name: str, is_audio_rese except Exception as e: raise Exception("compile_video_audio: %s" % e, params) - @mcp.tool() - def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") -> dict: - """Audio extraction, outputting the audio format. Supports mp3 and m4a formats. Default is m4a. + @mcp.tool( + description="Audio extraction, outputting the audio format. Supports mp3 and m4a formats. Default is m4a.", + ) + def extract_audio(type: str, source: str, space_name: str = None, format: str = "m4a") -> dict: + """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -757,7 +840,7 @@ def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") - vid - source(str): ** 必选字段 **, 视频文件信息 - format(str): ** 非必选字段 **, 输出音频的格式,支持 mp3、m4a 格式。默认 m4a - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -812,9 +895,11 @@ def extract_audio(type: str, source: str, space_name: str, format: str = "m4a") except Exception as e: raise Exception("extract_audio: %s" % e, params) - @mcp.tool() - def mix_audios(audios: List[dict], space_name: str) -> dict: - """Mix audios + @mcp.tool( + description="Mix audios", + ) + def mix_audios(audios: List[dict], space_name: str = None) -> dict: + """ Args: - audios(list[dict]): ** 必选字段 **,叠加的音频列表 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -822,7 +907,7 @@ def mix_audios(audios: List[dict], space_name: str) -> dict: - http - vid - source(str): ** 必选字段 **, 音频文件信息 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 @@ -892,11 +977,15 @@ def mix_audios(audios: List[dict], space_name: str) -> dict: except Exception as e: raise Exception("mix_audios: %s" % e, params) - @mcp.tool() - def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Optional[dict] = None) -> dict: - """`水印贴片`, `画中画`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. + @mcp.tool( + description=""" + `水印贴片`, `画中画`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. Note: - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 + """ + ) + def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Optional[dict] = None) -> dict: + """ Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py index b331a7ba..8a70e196 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py @@ -4,30 +4,35 @@ def create_mcp_server(mcp, public_methods: dict): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] # green screen - @mcp.tool() - def green_screen_task(type: str, video: str, spaceName: str, outputFormat: str = "WEBM") -> Any: - """Green Screen (绿幕抠图) is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Green Screen (绿幕抠图) is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def green_screen_task(type: str, video: str, space_name: str = None, output_format: str = "WEBM") -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName - - outputFormat: ** 必选字段 **, 输出视频的封装格式,默认 `WEBM`,支持的取值如下: + - output_format ** 必选字段 **, 输出视频的封装格式,默认 `WEBM`,支持的取值如下: - MOV - WEBM Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `greenScreen` """ valid_formats = {"MOV", "WEBM"} - fmt = outputFormat.upper() + fmt = output_format.upper() if fmt not in valid_formats: - raise ValueError(f"outputFormat must be one of {sorted(valid_formats)}, but got {outputFormat}") + raise ValueError(f"output_format must be one of {sorted(valid_formats)}, but got {output_format}") + - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { @@ -45,32 +50,36 @@ def green_screen_task(type: str, video: str, spaceName: str, outputFormat: str = return _start_execution(params) # portrait image retouching - @mcp.tool() - def portrait_image_retouching_task( - type: str, video: str, spaceName: str, outputFormat: str = "WEBM" - ) -> Any: - """Portrait image retouching is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Portrait image retouching is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def portrait_image_retouching_task( + type: str, video: str, space_name: str = None, output_format: str = "WEBM" + ) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName - - outputFormat: ** 必选字段 **, 输出视频的封装格式,默认 `WEBM`,支持的取值如下: + - output_format ** 必选字段 **, 输出视频的封装格式,默认 `WEBM`,支持的取值如下: - MOV - WEBM Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `portraitImageRetouching` """ valid_formats = {"MOV", "WEBM"} - fmt = outputFormat.upper() + fmt = output_format.upper() if fmt not in valid_formats: - raise ValueError(f"outputFormat must be one of {sorted(valid_formats)}, but got {outputFormat}") + raise ValueError(f"output_format must be one of {sorted(valid_formats)}, but got {output_format}") - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { diff --git a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py index 73c30946..027b2fc1 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py @@ -8,22 +8,26 @@ def create_mcp_server(mcp, public_methods: dict): _start_execution = public_methods["_start_execution"] # intelligent slicing - @mcp.tool() - def intelligent_slicing_task(type: str, video: str, spaceName: str) -> Any: - """ Intelligent slicing is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Intelligent slicing is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def intelligent_slicing_task(type: str, video: str, space_name: str = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `intelligentSlicing` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { diff --git a/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py b/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py index 7287caf3..b12110cb 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py @@ -4,11 +4,15 @@ def create_transcode_result_server(mcp, public_methods: dict,): """Register all VOD media MCP tools.""" _get_media_execution_task_result = public_methods["_get_media_execution_task_result"] - @mcp.tool() - def get_media_execution_task_result(type: str, runId: str) -> Any: - """Obtain the query results of the media processing task, 场景区分, 仅仅支持单任务模式 + @mcp.tool( + description=""" + Obtain the query results of the media processing task, 场景区分, 仅仅支持单任务模式 + """, + ) + def get_media_execution_task_result(type: str, run_id: str) -> Any: + """ Args: - - runId(str): ** 必选字段 **, 执行 ID。用于唯一指示当前这次媒体处理任务。 + - run_id: ** 必选字段 **, 执行 ID。用于唯一指示当前这次媒体处理任务。 - type(str): ** 必选字段 **, 场景类型 ,取值如下: - portraitImageRetouching:人像抠图 - greenScreen: 绿幕抠图 @@ -64,11 +68,11 @@ def get_media_execution_task_result(type: str, runId: str) -> Any: ] if type not in valid_types: raise ValueError(f"type must be one of {sorted(valid_types)}, but got {type}") - if not runId or not runId.strip(): + if not run_id or not run_id.strip(): raise ValueError("runId must be provided") # query result - response = _get_media_execution_task_result(runId, type) + response = _get_media_execution_task_result(run_id, type) return response diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index 3f0012ec..3dfca13e 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -7,18 +7,22 @@ def create_mcp_server(mcp, public_methods: dict,service): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] - @mcp.tool() - def asr_speech_to_text_task(type: str, video: str, spaceName: str, language: str = None) -> Any: - """ASR speech-to-text captioning is supported, with two input modes available: `Vid` and `DirectUrl`. - Note: - - `language`: ** 可选字段 **, 不传会探测, 仅是在 语言较相似的情况下传递 来提高识别效果 - - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - Args: + @mcp.tool( + description=""" + ASR speech-to-text captioning is supported, with two input modes available: `Vid` and `DirectUrl`. + Note: + - `language`: ** 可选字段 **, 不传会探测, 仅是在 语言较相似的情况下传递 来提高识别效果 + - `Vid`: vid 模式下不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def asr_speech_to_text_task(type: str, video: str, space_name: str = None, language: str = None) -> Any: + """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName - language(str): ** 可选字段 **, 识别提示语言,取值如下: - cmn-Hans-CN: 简体中文 @@ -47,7 +51,7 @@ def asr_speech_to_text_task(type: str, video: str, spaceName: str, language: str Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `asr` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) ask = { "WithSpeakerInfo": True, } @@ -60,22 +64,26 @@ def asr_speech_to_text_task(type: str, video: str, spaceName: str, language: str return _start_execution(params) # OCR - @mcp.tool() - def ocr_text_to_subtitles_task(type: str, video: str, spaceName: str) -> Any: - """OCR text to subtitles is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + OCR text to subtitles is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def ocr_text_to_subtitles_task(type: str, video: str, space_name: str = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `ocr` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": {"Type": "Task", "Task": {"Type": "Ocr", "Ocr": {}}}, @@ -83,22 +91,26 @@ def ocr_text_to_subtitles_task(type: str, video: str, spaceName: str) -> Any: return _start_execution(params) # subtitle removal - @mcp.tool() - def video_subtitles_removal_task(type: str, video: str, spaceName: str) -> Any: - """Video subtitles removal is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Video subtitles removal is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def video_subtitles_removal_task(type: str, video: str, space_name: str) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,type 为 `subtitlesRemoval` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { @@ -116,11 +128,15 @@ def video_subtitles_removal_task(type: str, video: str, spaceName: str) -> Any: } return _start_execution(params) - @mcp.tool() - def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_list: list = None, subtitle_config: dict = None) -> dict: - """Add subtitle functionality, supporting both subtitle file (subtitle_url) and subtitle list (text_list) methods. However, subtitle_url and text_list must specify that subtitle_url has a higher priority. + @mcp.tool( + description=""" + Add subtitle functionality, supporting both subtitle file (subtitle_url) and subtitle list (text_list) methods. However, subtitle_url and text_list must specify that subtitle_url has a higher priority. Note: - subtitle_url 和 text_list 必须指定一个,subtitle_url 优先级更高 + """, + ) + def add_subtitle(video: dict, space_name: str = None, subtitle_url: str = None, text_list: list = None, subtitle_config: dict = None) -> dict: + """ Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -157,7 +173,7 @@ def add_subtitle(video: dict, space_name: str, subtitle_url: str = None, text_li - height(str): 字幕的高度,支持设置为百分比(相对于视频高度)或具体像素值,String 类型,例如 10% 或 100。 - pos_x(str): 字幕在水平方向(X 轴)的位置,以视频正上方居中位置为原点,单位:像素,String 类型。例如值为 0 时,表示字幕在水平位置居中;值为 - 100 时,表示字幕向左移动 100 像素;值为 100 时,表示字幕向右移动 100 像素。 - pos_y(str): 水印在垂直方向(Y 轴)的位置,以视频正上方居中位置为原点,单位:像素,String 类型。例如值为 0 时,表示字幕在视频顶部;值为 100 时,表示字幕向下移动 100 像素。 - - space_name(str): ** 必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 + - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 Returns: - VCreativeId(str): AI 智剪任务 ID,用于查询任务状态。可以通过调用 `get_v_creative_task_result` 接口查询任务状态。 - Code(int): 任务状态码。为 0 表示任务执行成功。 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index 036477c8..a94a6480 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -1,19 +1,23 @@ import json from src.vod.api.api import VodAPI from volcengine.vod.models.request.request_vod_pb2 import VodUrlUploadRequest -from src.vod.models.request.request_models import BatchUploadUrlItem +# from src.vod.models.request.request_models import BatchUploadUrlItem from typing import List def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] - @mcp.tool() - def video_batch_upload(space_name: str, urls: List[dict] = None, ) -> dict: - """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload + @mcp.tool( + description=""" + Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 - 本接口为异步上传接口。上传任务成功提交后,系统会生成异步执行的任务,排队执行,不保证时效性。 - SourceUrl 必须是可公网直接访问的文件 URL,而非包含视频的网页 URL。 + """, + ) + def video_batch_upload(space_name: str = None, urls: List[dict] = None, ) -> dict: + """ Args: - - space_name:** 必选字段 ** 空间名称 + - space_name:** 非必选字段 ** 空间名称 - urls(list[dict[str, str]]): ** 必选字段 ** 资源URL列表,每个元素是一个包含URL信息的字典 - SourceUrl (str):** 必选字段 ** 源文件 URL。 - FileExtension(str):** 必选字段 ** 文件后缀,即点播存储中文件的类型 @@ -40,9 +44,13 @@ def video_batch_upload(space_name: str, urls: List[dict] = None, ) -> dict: else: raise Exception(resp.ResponseMetadata) - @mcp.tool() + @mcp.tool( + description=""" + Obtain the query results of media processing tasks, Obtain the query results of batch upload tasks + """, + ) def query_batch_upload_task_info(job_ids: str) -> dict: - """ Obtain the query results of media processing tasks Obtain the query results of batch upload tasks + """ Args: - job_ids(str): ** 必选字段 ** ,每个 URL 对应的任务 ID。查询多个以 , 逗号分隔,最多 ** 20 条 **。 Returns: diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py index 20089f44..70e186c6 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py @@ -5,23 +5,27 @@ def create_mcp_server(mcp, public_methods: dict): """Register all VOD media MCP tools.""" # video quality enhancement - @mcp.tool() - def video_quality_enhancement_task(type: str, video: str, spaceName: str) -> Any: - """ Video quality enhancement is supported, with two input modes available: `Vid` and `DirectUrl`.。 + @mcp.tool( + description=""" + Video quality enhancement is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """, + ) + def video_quality_enhancement_task(type: str, video: str, space_name: str = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询, 输入 type 为 `enhanceVideo` """ - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { @@ -44,21 +48,26 @@ def video_quality_enhancement_task(type: str, video: str, spaceName: str) -> Any return _start_execution(params) # video super-resolution - @mcp.tool() + @mcp.tool( + description=""" + Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. + Note: + - `Res` 和 `ResLimit` ** 不能同时指定,否则会返回错误 **。 + - `Vid`: vid 模式下不需要进行任何处理 + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """ + + ) def video_super_resolution_task( - type: str, video: str, spaceName: str, Res: str = None, ResLimit: int = None + type: str, video: str, space_name: str, Res: str = None, ResLimit: int = None ) -> Any: - """ Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. - Note: - - `Res` 和 `ResLimit` ** 不能同时指定,否则会返回错误 **。 - - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - video: ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 必选字段 **, 视频空间名称 - Res(str): ** 必选字段 ** 目标分辨率。支持的取值如下所示。 - 240p - 360p @@ -78,7 +87,7 @@ def video_super_resolution_task( if ResLimit is not None and (not isinstance(ResLimit, int) or ResLimit < 64 or ResLimit > 2160): raise ValueError("ResLimit must be an int in [64, 2160]") - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) target = {} if ResLimit is not None: @@ -113,15 +122,19 @@ def video_super_resolution_task( return _start_execution(params) # video interlacing - @mcp.tool() - def video_interlacing_task(type: str, video: str, spaceName: str, Fps: float) -> Any: - """ Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. + @mcp.tool( + description=""" + Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. + """ + ) + def video_interlacing_task(type: str, video: str, space_name: str = None, Fps: float = None) -> Any: + """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid - DirectUrl - video(str): ** 必选字段 **, 视频文件信息, 当 type 为 `Vid` 时, video 为视频文件 ID;当 type 为 `DirectUrl` 时, video 为 FileName - - spaceName(str): ** 必选字段 **, 视频空间名称 + - space_name(str): ** 非必选字段 **, 视频空间名称 - Fps(Float): 目标帧率,单位为 fps。取值范围为 (0, 120]。 Returns - RunId(str): 媒体处理任务执行 ID, 可通过 `get_media_execution_task_result` 方法进行结果查询,输入 type 为 `videSuperResolution` @@ -129,7 +142,7 @@ def video_interlacing_task(type: str, video: str, spaceName: str, Fps: float) -> if not isinstance(Fps, (int, float)) or Fps <= 0 or Fps > 120: raise ValueError("Fps must be > 0 and <= 120") - media_input = _build_media_input(type, video, spaceName) + media_input = _build_media_input(type, video, space_name) params = { "Input": media_input, "Operation": { diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 536036e8..5efdb3ce 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -311,14 +311,17 @@ def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): - @mcp.tool() - def get_play_url( type: str, source: str, spaceName: str, expired_minutes: int = 60) -> Any: - """ + @mcp.tool( + description=""" Obtain the video playback link through `directurl` or `vid`, 通过 directurl or vid 获取视频播放地址, Note: expired_minutes 仅在 directurl 模式下生效 + """ + ) + def get_play_url( type: str, source: str, space_name: str = None, expired_minutes: int = 60) -> Any: + """ Args: - - spaceName: **必选字段** 空间名称 + - space_name: **非必选字段** 空间名称 - source: **必选字段** 文件名 or vid - 文件名:直接传入文件名,例如 `test.mp4` - vid:直接传入 vid @@ -329,27 +332,32 @@ def get_play_url( type: str, source: str, spaceName: str, expired_minutes: int Returns: - 播放地址 """ + print("get_play_urlget_play_urlget_play_urlget_play_url",type, source, space_name, expired_minutes) if type == "directurl": - return public_methods["get_play_url"](spaceName, source, expired_minutes) + return public_methods["get_play_url"](space_name, source, expired_minutes) elif type == "vid": - videoInfo = public_methods["get_play_video_info"](source, spaceName) + videoInfo = public_methods["get_play_video_info"](source, space_name) if isinstance(videoInfo, str): videoInfo = json.loads(videoInfo) return videoInfo.get("PlayURL", "") - @mcp.tool() - def get_video_audio_info(type: str, source: str, space_name: str) -> dict: - """Obtaining audio and video metadata, 获取音视频播放信息 + @mcp.tool( + description=""" + Obtaining audio and video metadata, 获取音视频播放信息 Note: - ** directurl 模式:仅支持点播存储 ** - ** vid 模式:通过 get_play_video_info 获取数据 ** - ** 不支持 http 模式** + """ + ) + def get_video_audio_info(type: str, source: str, space_name: str = None) -> dict: + """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl:仅仅支持点播存储 - vid - source(str): 文件信息 - - space_name(str): ** 必选字段 ** , 点播空间 + - space_name(str): ** 非必选字段 ** , 点播空间 Returns: - FormatName(str): 容器名称。 - Duration(float): 时长,单位为秒。 From d00121f13baeab77c5c49a93828a386cb7d1c784 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Mon, 26 Jan 2026 11:38:53 +0800 Subject: [PATCH 23/26] =?UTF-8?q?feat:=20=E6=B3=A8=E9=87=8A=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/vod/mcp_tools/audio_processing.py | 21 ++---- .../mcp_server_vod/src/vod/mcp_tools/edit.py | 72 ++++++------------- .../src/vod/mcp_tools/intelligent_matting.py | 24 +++---- .../src/vod/mcp_tools/intelligent_slicing.py | 9 +-- .../src/vod/mcp_tools/media_tasks.py | 7 +- .../src/vod/mcp_tools/subtitle_processing.py | 39 ++++------ .../src/vod/mcp_tools/upload.py | 18 ++--- .../src/vod/mcp_tools/video_enhancement.py | 32 +++------ .../src/vod/mcp_tools/video_play.py | 18 ++--- .../mcp_server_vod/src/vod/models/__init__.py | 0 .../src/vod/models/request/__init__.py | 0 .../src/vod/models/request/request_models.py | 19 ----- 12 files changed, 83 insertions(+), 176 deletions(-) delete mode 100644 server/mcp_server_vod/src/vod/models/__init__.py delete mode 100644 server/mcp_server_vod/src/vod/models/request/__init__.py delete mode 100644 server/mcp_server_vod/src/vod/models/request/request_models.py diff --git a/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py index 8ba50bac..357d8754 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/audio_processing.py @@ -3,18 +3,14 @@ def create_mcp_server(mcp, public_methods: dict): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] - # audio noise reduction - @mcp.tool( - description=""" + @mcp.tool() + def audio_noise_reduction_task(type: str, audio: str, space_name: str = None) -> Any: + """ Audio noise reduction, supporting two input modes: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def audio_noise_reduction_task(type: str, audio: str, space_name: str = None) -> Any: - """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -38,16 +34,13 @@ def audio_noise_reduction_task(type: str, audio: str, space_name: str = None) -> return _start_execution(params) # voice separation - @mcp.tool( - description=""" + @mcp.tool() + def voice_separation_task(type: str, video: str, space_name: str = None) -> Any: + """ Voice separation is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def voice_separation_task(type: str, video: str, space_name: str = None) -> Any: - """ + - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid diff --git a/server/mcp_server_vod/src/vod/mcp_tools/edit.py b/server/mcp_server_vod/src/vod/mcp_tools/edit.py index 7463eb88..11243fde 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/edit.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/edit.py @@ -32,18 +32,15 @@ def _format_source(type: str, source: str) -> str: def create_mcp_server(mcp,public_methods: dict, service: VodAPI, ): - @mcp.tool( - description=""" + @mcp.tool() + def audio_video_stitching(type: str, space_name: str = None, videos: List[str] = None, audios: List[str] = None, transitions: List[str] = None) -> dict: + """ Carry out video stitching, audio stitching, and support for transitions and other capabilities,需要参考 Note 中的要求。 Note: - **audio splicing does not support transitions. ** - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** - ** directurl://{fileName} 格式指定资源的 FileName。示例:directurl://test.mp3** - ** http(s):// 格式指定资源的 URL。示例:http://example.com/test.mp4** - """, - ) - def audio_video_stitching(type: str, space_name: str = None, videos: List[str] = None, audios: List[str] = None, transitions: List[str] = None) -> dict: - """ Args: - type(str): ** 必选字段 ** , 拼接类型。 `audio` | `video` - space_name(str): ** 非必选字段 ** , 任务产物的上传空间。AI 处理生成的视频将被上传至此点播空间。 @@ -168,16 +165,7 @@ def audio_video_stitching(type: str, space_name: str = None, videos: List[str] = except Exception as e: raise Exception("audio_video_stitching: %s" % e, params) - @mcp.tool( - description=""" - Invoke the current tools to complete the cropping of audio and video,需要参考 Note 中的要求。 - Note: - - ** vid 模式下需要增加 vid:// 前缀, 示例:vid://123456 ** - - ** directurl://{fileName} 格式指定资源的 FileName。示例:directurl://test.mp3** - - ** http(s):// 格式指定资源的 URL。示例:http://example.com/test.mp4** - - `start_time` 和 `end_time` 必须同时指定,且 `end_time` 必须大于 `start_time` - """, - ) + @mcp.tool() def audio_video_clipping(type: str, source: str, start_time: float, end_time: float, space_name: str = None) -> dict: """ Invoke the current tools to complete the cropping of audio and video,需要参考 Note 中的要求。 Note: @@ -344,11 +332,10 @@ def _get_v_creative_task_result_impl(VCreativeId: str, space_name: str = None) - # """ # return _get_v_creative_task_result_impl(VCreativeId, space_name) - @mcp.tool( - description="Poll the execution status and results of video stitching, audio stitching, and audio-video cropping by using the `VCreativeId` until success or timeout.", - ) + @mcp.tool() async def get_v_creative_task_result(VCreativeId: str,space_name: str = None, interval: float = 2.0, max_retries: int = 10, ctx: Context = None) -> dict: """ + Poll the execution status and results of video stitching, audio stitching, and audio-video cropping by using the `VCreativeId` until success or timeout. Args: - VCreativeId(str): `String type`, ID for AI intelligent trimming task. - space_name(str): `String type`, space name. ** 非必选字段 ** @@ -390,11 +377,10 @@ async def get_v_creative_task_result(VCreativeId: str,space_name: str = None, in - @mcp.tool( - description="Video rotation capability is supported, allowing for `vertical and horizontal flipping of the video`. ** Default: No flipping **", - ) + @mcp.tool() def flip_video(type: str, source: str, space_name: str = None, flip_x: bool = False, flip_y: bool = False) -> dict: """ + Video rotation capability is supported, allowing for `vertical and horizontal flipping of the video`. ** Default: No flipping ** Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -455,11 +441,10 @@ def flip_video(type: str, source: str, space_name: str = None, flip_x: bool = Fa except Exception as e: raise Exception("flip_video: %s" % e, params) - @mcp.tool( - description="Adjust the speed multiplier of the video, of type Float, with a range from 0.1 to 4.", - ) + @mcp.tool() def speedup_video(type: str, source: str, space_name: str = None, speed: float = 1.0) -> dict: """ + Adjust the speed multiplier of the video, of type Float, with a range from 0.1 to 4. Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -524,11 +509,10 @@ def speedup_video(type: str, source: str, space_name: str = None, speed: float = raise Exception("speedup_video: %s" % e, params) except Exception as e: raise Exception("speedup_video: %s" % e, params) - @mcp.tool( - description="Adjust the speed multiplier of the audio, of type Float, with a range from 0.1 to 4.", - ) + @mcp.tool( ) def speedup_audio(type: str, source: str, space_name: str = None, speed: float = 1.0) -> dict: """ + Adjust the speed multiplier of the audio, of type Float, with a range from 0.1 to 4. Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -594,9 +578,7 @@ def speedup_audio(type: str, source: str, space_name: str = None, speed: float = except Exception as e: raise Exception("speedup_video: %s" % e, params) - @mcp.tool( - description="The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition **", - ) + @mcp.tool() def image_to_video(images: List[dict], space_name: str = None, transitions: List[str] = None) -> dict: """The image-to-video conversion function supports non-overlapping transition effects. When the number of videos exceeds the number of transitions by 2 or more, the system will automatically cycle through the transitions. ** Default: No transition ** Args: @@ -719,11 +701,10 @@ def image_to_video(images: List[dict], space_name: str = None, transitions: List except Exception as e: raise Exception("image_to_video: %s" % e, params) - @mcp.tool( - description="The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing.", - ) + @mcp.tool() def compile_video_audio(video: dict, audio: dict, space_name: str = None, is_audio_reserve: bool = True, is_video_audio_sync: bool = False, sync_mode: str = "video", sync_method: str = "trim") -> dict: """ + The compilation of video and audio capabilities require the transmission of both ** audio and video resources ** for processing. Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -828,11 +809,9 @@ def compile_video_audio(video: dict, audio: dict, space_name: str = None, is_aud except Exception as e: raise Exception("compile_video_audio: %s" % e, params) - @mcp.tool( - description="Audio extraction, outputting the audio format. Supports mp3 and m4a formats. Default is m4a.", - ) + @mcp.tool() def extract_audio(type: str, source: str, space_name: str = None, format: str = "m4a") -> dict: - """ + """"Audio extraction, outputting the audio format. Supports mp3 and m4a formats. Default is m4a. Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl @@ -895,11 +874,9 @@ def extract_audio(type: str, source: str, space_name: str = None, format: str = except Exception as e: raise Exception("extract_audio: %s" % e, params) - @mcp.tool( - description="Mix audios", - ) + @mcp.tool() def mix_audios(audios: List[dict], space_name: str = None) -> dict: - """ + """Mix audios, 混音 Args: - audios(list[dict]): ** 必选字段 **,叠加的音频列表 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 @@ -977,15 +954,12 @@ def mix_audios(audios: List[dict], space_name: str = None) -> dict: except Exception as e: raise Exception("mix_audios: %s" % e, params) - @mcp.tool( - description=""" - `水印贴片`, `画中画`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. - Note: - - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 - """ - ) + @mcp.tool() def add_sub_video(video: dict, sub_video: dict, space_name: str, sub_options: Optional[dict] = None) -> dict: """ + `水印贴片`, `画中画`,Add the capability of video watermarking, support adjusting the width and height of the watermark, as well as the position in the horizontal or vertical direction, and determine the timing of the watermark's appearance in the original video by setting start_time and end_time,. + Note: + - 如果设置的水印开始时间、结束时间超出原始视频时长,那么输出视频的长度将以水印的结束时间为准,超出原始视频部分将以黑屏形式延续。例如原始视频为 20 秒,设置 end_time 为 30,那么输出时长为 30 秒 Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py index 8a70e196..846a762c 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_matting.py @@ -4,16 +4,14 @@ def create_mcp_server(mcp, public_methods: dict): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] # green screen - @mcp.tool( - description=""" + @mcp.tool() + def green_screen_task(type: str, video: str, space_name: str = None, output_format: str = "WEBM") -> Any: + """ Green Screen (绿幕抠图) is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def green_screen_task(type: str, video: str, space_name: str = None, output_format: str = "WEBM") -> Any: - """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -50,18 +48,16 @@ def green_screen_task(type: str, video: str, space_name: str = None, output_form return _start_execution(params) # portrait image retouching - @mcp.tool( - description=""" + @mcp.tool() + def portrait_image_retouching_task( + type: str, video: str, space_name: str = None, output_format: str = "WEBM" + ) -> Any: + """ Portrait image retouching is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def portrait_image_retouching_task( - type: str, video: str, space_name: str = None, output_format: str = "WEBM" - ) -> Any: - """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid diff --git a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py index 027b2fc1..446e513d 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/intelligent_slicing.py @@ -8,16 +8,13 @@ def create_mcp_server(mcp, public_methods: dict): _start_execution = public_methods["_start_execution"] # intelligent slicing - @mcp.tool( - description=""" + @mcp.tool() + def intelligent_slicing_task(type: str, video: str, space_name: str = None) -> Any: + """ Intelligent slicing is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def intelligent_slicing_task(type: str, video: str, space_name: str = None) -> Any: - """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid diff --git a/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py b/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py index b12110cb..8401de56 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/media_tasks.py @@ -4,13 +4,10 @@ def create_transcode_result_server(mcp, public_methods: dict,): """Register all VOD media MCP tools.""" _get_media_execution_task_result = public_methods["_get_media_execution_task_result"] - @mcp.tool( - description=""" - Obtain the query results of the media processing task, 场景区分, 仅仅支持单任务模式 - """, - ) + @mcp.tool() def get_media_execution_task_result(type: str, run_id: str) -> Any: """ + Obtain the query results of the media processing task, 场景区分, 仅仅支持单任务模式 Args: - run_id: ** 必选字段 **, 执行 ID。用于唯一指示当前这次媒体处理任务。 - type(str): ** 必选字段 **, 场景类型 ,取值如下: diff --git a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py index 3dfca13e..0133a159 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/subtitle_processing.py @@ -7,17 +7,15 @@ def create_mcp_server(mcp, public_methods: dict,service): _build_media_input = public_methods["_build_media_input"] _start_execution = public_methods["_start_execution"] - @mcp.tool( - description=""" + @mcp.tool() + def asr_speech_to_text_task(type: str, video: str, space_name: str = None, language: str = None) -> Any: + """ ASR speech-to-text captioning is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `language`: ** 可选字段 **, 不传会探测, 仅是在 语言较相似的情况下传递 来提高识别效果 - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def asr_speech_to_text_task(type: str, video: str, space_name: str = None, language: str = None) -> Any: - """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -64,16 +62,14 @@ def asr_speech_to_text_task(type: str, video: str, space_name: str = None, langu return _start_execution(params) # OCR - @mcp.tool( - description=""" + @mcp.tool() + def ocr_text_to_subtitles_task(type: str, video: str, space_name: str = None) -> Any: + """ OCR text to subtitles is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def ocr_text_to_subtitles_task(type: str, video: str, space_name: str = None) -> Any: - """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -91,16 +87,14 @@ def ocr_text_to_subtitles_task(type: str, video: str, space_name: str = None) -> return _start_execution(params) # subtitle removal - @mcp.tool( - description=""" + @mcp.tool() + def video_subtitles_removal_task(type: str, video: str, space_name: str) -> Any: + """ Video subtitles removal is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def video_subtitles_removal_task(type: str, video: str, space_name: str) -> Any: - """ + Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -128,15 +122,12 @@ def video_subtitles_removal_task(type: str, video: str, space_name: str) -> Any: } return _start_execution(params) - @mcp.tool( - description=""" + @mcp.tool() + def add_subtitle(video: dict, space_name: str = None, subtitle_url: str = None, text_list: list = None, subtitle_config: dict = None) -> dict: + """ Add subtitle functionality, supporting both subtitle file (subtitle_url) and subtitle list (text_list) methods. However, subtitle_url and text_list must specify that subtitle_url has a higher priority. Note: - subtitle_url 和 text_list 必须指定一个,subtitle_url 优先级更高 - """, - ) - def add_subtitle(video: dict, space_name: str = None, subtitle_url: str = None, text_list: list = None, subtitle_config: dict = None) -> dict: - """ Args: - video(dict): ** 必选字段 **,视频信息 - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 diff --git a/server/mcp_server_vod/src/vod/mcp_tools/upload.py b/server/mcp_server_vod/src/vod/mcp_tools/upload.py index a94a6480..d092c0c9 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/upload.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/upload.py @@ -5,17 +5,14 @@ from typing import List def create_mcp_server(mcp, public_methods: dict, service: VodAPI,): get_play_url = public_methods['get_play_url'] - @mcp.tool( - description=""" + @mcp.tool() + def video_batch_upload(space_name: str = None, urls: List[dict] = None, ) -> dict: + """ Batch retrieval and upload of URLs upload video、 audio to specified space via synchronous upload Note: - 本接口主要适用于文件没有存储在本地服务器或终端,需要通过公网访问的 URL 地址上传的场景。源文件 URL 支持 HTTP 和 HTTPS。 - 本接口为异步上传接口。上传任务成功提交后,系统会生成异步执行的任务,排队执行,不保证时效性。 - SourceUrl 必须是可公网直接访问的文件 URL,而非包含视频的网页 URL。 - """, - ) - def video_batch_upload(space_name: str = None, urls: List[dict] = None, ) -> dict: - """ Args: - space_name:** 非必选字段 ** 空间名称 - urls(list[dict[str, str]]): ** 必选字段 ** 资源URL列表,每个元素是一个包含URL信息的字典 @@ -44,13 +41,10 @@ def video_batch_upload(space_name: str = None, urls: List[dict] = None, ) -> dic else: raise Exception(resp.ResponseMetadata) - @mcp.tool( - description=""" - Obtain the query results of media processing tasks, Obtain the query results of batch upload tasks - """, - ) + @mcp.tool() def query_batch_upload_task_info(job_ids: str) -> dict: - """ + """ + Obtain the query results of media processing tasks, Obtain the query results of batch upload tasks Args: - job_ids(str): ** 必选字段 ** ,每个 URL 对应的任务 ID。查询多个以 , 逗号分隔,最多 ** 20 条 **。 Returns: diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py index 70e186c6..7eef3ba3 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_enhancement.py @@ -5,16 +5,13 @@ def create_mcp_server(mcp, public_methods: dict): """Register all VOD media MCP tools.""" # video quality enhancement - @mcp.tool( - description=""" + @mcp.tool() + def video_quality_enhancement_task(type: str, video: str, space_name: str = None) -> Any: + """ Video quality enhancement is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """, - ) - def video_quality_enhancement_task(type: str, video: str, space_name: str = None) -> Any: - """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -48,20 +45,16 @@ def video_quality_enhancement_task(type: str, video: str, space_name: str = None return _start_execution(params) # video super-resolution - @mcp.tool( - description=""" + @mcp.tool() + def video_super_resolution_task( + type: str, video: str, space_name: str, Res: str = None, ResLimit: int = None + ) -> Any: + """ Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. Note: - `Res` 和 `ResLimit` ** 不能同时指定,否则会返回错误 **。 - `Vid`: vid 模式下不需要进行任何处理 - `DirectUrl`: directurl 模式下需要传递 FileName,不需要进行任何处理 - """ - - ) - def video_super_resolution_task( - type: str, video: str, space_name: str, Res: str = None, ResLimit: int = None - ) -> Any: - """ Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid @@ -122,13 +115,10 @@ def video_super_resolution_task( return _start_execution(params) # video interlacing - @mcp.tool( - description=""" - Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. - """ - ) + @mcp.tool() def video_interlacing_task(type: str, video: str, space_name: str = None, Fps: float = None) -> Any: - """ + """ + Video Super-Resolution is supported, with two input modes available: `Vid` and `DirectUrl`. Args: - type(str):** 必选字段 **,文件类型,默认值为 `Vid` 。字段取值如下 - Vid diff --git a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py index 5efdb3ce..b8c90a74 100644 --- a/server/mcp_server_vod/src/vod/mcp_tools/video_play.py +++ b/server/mcp_server_vod/src/vod/mcp_tools/video_play.py @@ -311,15 +311,12 @@ def get_video_audio_info_directurl(spaceName: str, source: str) -> dict: def create_mcp_server(mcp: FastMCP, public_methods: dict, service: VodAPI): - @mcp.tool( - description=""" + @mcp.tool() + def get_play_url( type: str, source: str, space_name: str = None, expired_minutes: int = 60) -> Any: + """ Obtain the video playback link through `directurl` or `vid`, 通过 directurl or vid 获取视频播放地址, Note: expired_minutes 仅在 directurl 模式下生效 - """ - ) - def get_play_url( type: str, source: str, space_name: str = None, expired_minutes: int = 60) -> Any: - """ Args: - space_name: **非必选字段** 空间名称 - source: **必选字段** 文件名 or vid @@ -341,17 +338,14 @@ def get_play_url( type: str, source: str, space_name: str = None, expired_minut videoInfo = json.loads(videoInfo) return videoInfo.get("PlayURL", "") - @mcp.tool( - description=""" + @mcp.tool() + def get_video_audio_info(type: str, source: str, space_name: str = None) -> dict: + """ Obtaining audio and video metadata, 获取音视频播放信息 Note: - ** directurl 模式:仅支持点播存储 ** - ** vid 模式:通过 get_play_video_info 获取数据 ** - ** 不支持 http 模式** - """ - ) - def get_video_audio_info(type: str, source: str, space_name: str = None) -> dict: - """ Args: - type(str): ** 必选字段 **,文件类型,默认值为 `vid` 。字段取值如下 - directurl:仅仅支持点播存储 diff --git a/server/mcp_server_vod/src/vod/models/__init__.py b/server/mcp_server_vod/src/vod/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/server/mcp_server_vod/src/vod/models/request/__init__.py b/server/mcp_server_vod/src/vod/models/request/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/server/mcp_server_vod/src/vod/models/request/request_models.py b/server/mcp_server_vod/src/vod/models/request/request_models.py deleted file mode 100644 index 7cec0f89..00000000 --- a/server/mcp_server_vod/src/vod/models/request/request_models.py +++ /dev/null @@ -1,19 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, Literal - -class BatchUploadUrlItem(BaseModel): - SourceUrl: str - FileExtension: str = Field(description="文件后缀,即点播存储中文件的类型,-必须以 . 开头,不超过 8 位。;当您传入 FileExtension 时,视频点播将生成 32 位随机字符串,和您传入的 FileExtension 共同拼接成文件路径") - -class InputSource(BaseModel): - type: Optional[Literal["directurl", "http", "vid"]] = Field(description="文件类型,vid、directurl、http") - source: str = Field(description="文件信息") - - -class addSubVideoOptions(BaseModel): - height: Optional[str] = Field(description="水印的高度,支持设置为百分比(相对于视频高度)或具体像素值,例如 100% 或 100") - width: Optional[str] = Field(description="水印的宽度,支持设置为百分比(相对于视频高度)或具体像素值,String 类型,例如 100% 或 100") - pos_x: Optional[str] = Field(description="水印在水平方向(X 轴)的位置,以视频左上角为原点,单位:像素。例如值为 0 时,表示水印处于水平方向的最左侧;值为 100 时,表示水印相对原点向右移动 100 像素") - pos_y: Optional[str] = Field(description="水印在垂直方向(Y 轴)的位置,以视频左上角为原点,单位:像素,例如值为 0 时,表示水印在垂直方向的最上侧;值为 100 时,表示水印相对原点向下移动 100 像素") - start_time: Optional[float] = Field(description="水印的开始时间,单位:秒") - end_time: Optional[float] = Field(description="水印的结束时间,单位:秒") From e93f05f3b9bb4c546e21d5813148b035aee4de6f Mon Sep 17 00:00:00 2001 From: tiehongji Date: Fri, 30 Jan 2026 14:14:15 +0800 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=BF=85=E9=A1=BB=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/src/base/base_mcp.py | 3 +-- server/mcp_server_vod/src/vod/server.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/mcp_server_vod/src/base/base_mcp.py b/server/mcp_server_vod/src/base/base_mcp.py index df32e053..fd956e1b 100644 --- a/server/mcp_server_vod/src/base/base_mcp.py +++ b/server/mcp_server_vod/src/base/base_mcp.py @@ -364,8 +364,7 @@ async def call_tool(self, arguments['space_name'] = space_name_env.strip() space_name = arguments.get('space_name') if not space_name or not isinstance(space_name, str) or not space_name.strip(): - raise Exception('space_name is required') - print(f"space_name: {space_name}",arguments) + logger.error(f"space_name is required") return await super().call_tool(name, arguments) except Exception as e: logger.error(f"BaseMCP.call_tool failed with error: {e}") diff --git a/server/mcp_server_vod/src/vod/server.py b/server/mcp_server_vod/src/vod/server.py index 1249109c..68a4108d 100644 --- a/server/mcp_server_vod/src/vod/server.py +++ b/server/mcp_server_vod/src/vod/server.py @@ -25,6 +25,8 @@ - `DirectUrl` 指定资源的 FileName。示例:test.mp3 """, ) + + def init_mcp () -> FastMCP: return mcp From 377f05b8ae10dabc55fd75a051a9c4b1fbe7eb8d Mon Sep 17 00:00:00 2001 From: tiehongji Date: Fri, 30 Jan 2026 14:51:01 +0800 Subject: [PATCH 25/26] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0readme=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/README.md | 215 +++++++++++++++++++++++++++-- server/mcp_server_vod/README_zh.md | 199 +++++++++++++++++++++++++- 2 files changed, 404 insertions(+), 10 deletions(-) diff --git a/server/mcp_server_vod/README.md b/server/mcp_server_vod/README.md index c211efe5..1f0d9ab4 100644 --- a/server/mcp_server_vod/README.md +++ b/server/mcp_server_vod/README.md @@ -178,30 +178,140 @@ Query media processing task results, including task status, processing progress, Query the media processing result for the portrait cutout task with runId xxx in space space1. +### Tool 18 flip_video + +#### Description + +Video flip (vertical and horizontal). Supports vid, http url, and directurl input modes. + +#### Trigger Example + +Flip the video vertically and horizontally; video is vid1, space is space1. + +### Tool 19 speedup_video + +#### Description + +Video speed adjustment. Supports vid, http url, and directurl input modes. + +#### Trigger Example + +Adjust video vid1 to 2x speed; space is space1. + +### Tool 20 speedup_audio + +#### Description + +Audio speed adjustment. Supports vid, http url, and directurl input modes. + +#### Trigger Example + +Adjust audio vid1 to 2x speed; space is space1. + +### Tool 21 image_to_video + +#### Description + +Image-to-video conversion. Supports vid, http url, and directurl input modes. + +#### Trigger Example + +Apply zoom and random transitions to images; image resources are vid1, vid2, vid3; space is space1. + +### Tool 22 compile_video_audio + +#### Description + +Audio-video composition: strip original audio from video, align audio with video duration, and related capabilities. + +#### Trigger Example + +Merge audio and video, strip original audio from the video, use video duration as reference; video is vid1, audio is vid2; space is space1. + +### Tool 23 extract_audio + +#### Description + +Extract audio from video. Supports configuring audio output format. + +#### Trigger Example + +Extract audio from vid1; space is space1. + +### Tool 24 mix_audios + +#### Description + +Audio mixing (e.g. adding background music to audio). + +#### Trigger Example + +Mix audios vid1 and vid2; space is space1. + +### Tool 25 add_subtitle + +#### Description + +Add subtitles to video. Typical flow: generate narration with a model, generate voice + subtitles, then composite into the video. + +#### Trigger Example + +Add subtitle `https:****.srt` to video vid1; outline color red, font size 70, outline width 10; space is space1. + +### Tool 26 add_sub_video + +#### Description + +Picture-in-picture, add image to video, and video watermarking. + +#### Trigger Example + +Add watermark overlay: main video vid1, overlay video vid2; overlay at top-right, size 100×100; space is space1. + +### Tool 27 get_video_audio_info + +#### Description + +Retrieve video/audio metadata. + +#### Trigger Example + +Get playback info for vid1; space is space1. + +### Tool 28 get_play_url + +#### Description + +Get audio/video playback URL. + +#### Trigger Example + +Get playback URL for vid1; space is space1. + ## Supported Platforms -Ark, Cursor, Trae etc. +Ark, Cursor, Trae, etc. -## Service Activation Link (Full Product) +## Product Console -[Volcano Engine-Video on Demand-Console](https://www.volcengine.com/product/vod) +[Volcano Engine - Video on Demand - Console](https://www.volcengine.com/product/vod) -## Authentication Method +## Authentication -Please apply for VOLCENGINE_ACCESS_KEY, VOLCENGINE_SECRET_KEY at [Volcano Engine-Video on Demand-Console](https://www.volcengine.com/product/vod) +Apply for VOLCENGINE_ACCESS_KEY and VOLCENGINE_SECRET_KEY at [Volcano Engine - Video on Demand - Console](https://www.volcengine.com/product/vod). ## Installation -### Environment Requirements +### Requirements - Python 3.13+ - Volcano Engine account and AccessKey/SecretKey ## Deployment -### Integration in MCP Client +### Integrate in MCP Client -Configure MCP service in mcp client, MCP JSON configuration: +Configure the MCP service in your MCP client. Example MCP JSON: ```json { @@ -216,13 +326,100 @@ Configure MCP service in mcp client, MCP JSON configuration: "env": { "VOLCENGINE_ACCESS_KEY": "Your Volcengine AK", "VOLCENGINE_SECRET_KEY": "Your Volcengine SK", - "MCP_TOOL_GROUPS": "YOUR_TOOL_GROUPS" + "MCP_TOOLS_TYPE": "Your Source", // groups | tools + // - In groups mode, MCP_TOOLS_SOURCE is the group name + // - In tools mode, MCP_TOOLS_SOURCE is the tool name + "MCP_TOOLS_SOURCE": "Your Source", + } } } } ``` +### Cloud Deployment + +**Dynamic authentication and grouping are supported. Header configuration:** + +- To enable all tools: + - x-tt-tools-type: groups + - x-tt-tools-source: all +- You can switch MCP tools dynamically via x-tt-tools-source to reduce noise and token usage for scenario-specific agents. +- x-tt-tools-type: loading mode; values: groups | tools +- In groups mode, x-tt-tools-source is the group name; use all for all groups. +- In tools mode, x-tt-tools-source is the tool name. + +### Group Configuration + +**Default exposed tools:** + +- get_play_url: Get playback URL +- audio_video_stitching: Audio/video stitching +- audio_video_clipping: Audio/video clipping +- get_v_creative_task_result: Query edit task result + +**Full group mapping:** + +```json +{ + # edit group + "edit": [ # group name + "audio_video_stitching", + "audio_video_clipping", + "get_v_creative_task_result", + "flip_video", + "speedup_video", + "speedup_audio", + "image_to_video", + "compile_video_audio", + "extract_audio", + "mix_audios", + "add_sub_video", + ], + # video_play group + "video_play": [ + "get_play_url", + "get_video_audio_info" + ], + # upload group + "upload": [ + "video_batch_upload", + "query_batch_upload_task_info" + ], + # intelligent_slicing group + "intelligent_slicing": [ + "intelligent_slicing_task" + ], + # intelligent_matting group + "intelligent_matting": [ + "portrait_image_retouching_task", + "green_screen_task" + ], + # subtitle_processing group + "subtitle_processing": [ + "asr_speech_to_text_task", + "ocr_text_to_subtitles_task", + "video_subtitles_removal_task", + "add_subtitle", + ], + # audio_processing group + "audio_processing": [ + "voice_separation_task", + "audio_noise_reduction_task" + ], + # video_enhancement group + "video_enhancement": [ + "video_interlacing_task", + "video_super_resolution_task", + "video_quality_enhancement_task" + ], + # media_tasks group (common) + "media_tasks": [ + "get_media_execution_task_result" + ], +} +``` + ## License MIT diff --git a/server/mcp_server_vod/README_zh.md b/server/mcp_server_vod/README_zh.md index f7b24ece..5072fc3b 100644 --- a/server/mcp_server_vod/README_zh.md +++ b/server/mcp_server_vod/README_zh.md @@ -182,6 +182,116 @@ OCR 文字转字幕,支持 Vid 和 DirectUrl 两种输入模式。 查询人像抠图任务的视频处理任务的处理结果,runId 为 xxx,空间为 space1 +### Tool 18 flip_video + +#### 功能描述 + +视频上下翻转、左右翻转能力,支持 vid、http url、directurl 三种模式输入 + +#### 最容易被唤起的 Prompt 示例 + +把视频上下翻转和左右翻转,视频为 vid1 空间 为 space1 + +### Tool 19 speedup_video + +#### 功能描述 + +视频倍速能力,支持 vid、http url、directurl 三种模式输入 + +#### 最容易被唤起的 Prompt 示例 + +把视频 vid1 进行调整至 2 倍速, 空间 为 space1 + +### Tool 20 speedup_audio + +#### 功能描述 + +音频倍速能力,支持 vid、http url、directurl 三种模式输入 + +#### 最容易被唤起的 Prompt 示例 + +把音频 vid1 进行调整至 2 倍速, 空间 为 space1 + +### Tool 21 image_to_video + +#### 功能描述 + +图片转视频能力,支持 vid、http url、directurl 三种模式输入 + +#### 最容易被唤起的 Prompt 示例 + +对图片进行渐变放大,并增加随机转场, 图片资源为 vid1, vid2、vid3, 空间 为 space1 + +### Tool 22 compile_video_audio + +#### 功能描述 + +音视频合成能力,支持擦除原视频流中的音频、对齐音频和视频时长等能力 + +#### 最容易被唤起的 Prompt 示例 + +进行音视频合并 ,擦除视频原始音效,以视频时长为准,视频为 vid1, 音频为 vid2, 空间 为 space1 + +### Tool 23 extract_audio + +#### 功能描述 + +视频提取音频能力,支持设置音频输出格式 + +#### 最容易被唤起的 Prompt 示例 + +提取音频,音频为 vid1, 空间 为 space1 + +### Tool 24 mix_audios + +#### 功能描述 + +音频叠加能力,常用场景为音频添加背景音乐 + +#### 最容易被唤起的 Prompt 示例 + +帮我进行音频合并操作 vid1, vid2 空间 为 space1 + +### Tool 25 add_subtitle + +#### 功能描述 + +视频添加字幕能力,通常流程:大模型生成旁白,使用配音能力生成语音+字幕;再合成到视频中。 + +#### 最容易被唤起的 Prompt 示例 + +为视频 vid1 增加字幕 `https:****.srt` ,描边的颜色 为 红色,字体大小为 70,描边宽度为 10 ,描边的颜色 为 红色,字体大小为 70,描边宽度为 10 空间 为 space1 + +### Tool 26 add_sub_video + +#### 功能描述 + +视频画中画、视频添加图片、视频水印能力 + +#### 最容易被唤起的 Prompt 示例 + +进行水印贴片,主视频为 vid1 , 贴片视频为 vid2;贴片位于右上角,贴片宽高为 100\* 100。 空间 为 space1 + +### Tool 27 get_video_audio_info + +#### 功能描述 + +视频元信息获取能力 + +#### 最容易被唤起的 Prompt 示例 + +帮我获取 vid1 的播放信息, 空间 为 space1 + +### Tool 28 get_play_url + +#### 功能描述 + +音视频播放链接获取 + +#### 最容易被唤起的 Prompt 示例 + +帮我获取 vid1 的播放链接, 空间 为 space1 + ## 可适配平台 方舟,Cursor,Trae 等 @@ -220,13 +330,100 @@ OCR 文字转字幕,支持 Vid 和 DirectUrl 两种输入模式。 "env": { "VOLCENGINE_ACCESS_KEY": "Your Volcengine AK", "VOLCENGINE_SECRET_KEY": "Your Volcengine SK", - "MCP_TOOL_GROUPS": "YOUR_TOOL_GROUPS" + "MCP_TOOLS_TYPE": "Your Source" // groups | tools + // - groups 模式下 为 MCP_TOOLS_SOURCE 值需要输入分组值; + // - tools: 模式下 MCP_TOOLS_SOURCE 值为 tool.name + "MCP_TOOLS_SOURCE": "Your Source", + } } } } ``` +### 云部署 + +** 支持动态鉴权以及分组,header 信息如下 ** + +- 全选设置为 + - x-tt-tools-type: groups + - x-tt-tools-source: all +- 本期支持了可以通过配置 x-tt-tools-source 来动态切换 mcp tool,可以针对场景化智能体,减少不必要的干扰以及 token 消耗 +- x-tt-tools-type: 动态加载类型的模式,支持: groups | tools 两种 +- groups 模式下 为 x-tt-tools-source 值需要输入分组值 、 all 为 全部设置 +- tools: 模式下 x-tt-tools-source 值为 tool.name + +### 分组信息 + +** 默认输出内容为 ** + +- get_play_url: 获取播放链接 +- audio_video_stitching: 音视频拼接 +- audio_video_clipping: 音视频剪切 +- get_v_creative_task_result: 查询剪辑结果 + +** 详细分组信息如下: ** + +```json +{ + # edit 分组 + "edit": [ #分组名称 + "audio_video_stitching", + "audio_video_clipping", + "get_v_creative_task_result", + "flip_video", + "speedup_video", + "speedup_audio", + "image_to_video", + "compile_video_audio", + "extract_audio", + "mix_audios", + "add_sub_video", + ], + # video_play 分组 + "video_play": [ + "get_play_url", + "get_video_audio_info" + ], + # upload 分组 + "upload": [ + "video_batch_upload", + "query_batch_upload_task_info" + ], + # intelligent_slicing 分组 + "intelligent_slicing": [ + "intelligent_slicing_task" + ], + # intelligent_matting 分组 + "intelligent_matting": [ + "portrait_image_retouching_task", + "green_screen_task" + ], + # subtitle_processing 分组 + "subtitle_processing": [ + "asr_speech_to_text_task", + "ocr_text_to_subtitles_task", + "video_subtitles_removal_task", + "add_subtitle", + ], + # audio_processing 分组 + "audio_processing": [ + "voice_separation_task", + "audio_noise_reduction_task" + ], + # video_enhancement 分组 + "video_enhancement": [ + "video_interlacing_task", + "video_super_resolution_task", + "video_quality_enhancement_task" + ], + # media_tasks 分组(通用) + "media_tasks": [ + "get_media_execution_task_result" + ], +} +``` + ## License MIT From 74a33a3fc6a867b01f1ff1d31ea7cdf150ac62a1 Mon Sep 17 00:00:00 2001 From: tiehongji Date: Fri, 30 Jan 2026 14:52:14 +0800 Subject: [PATCH 26/26] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0readme=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp_server_vod/README.md | 11 +++++------ server/mcp_server_vod/README_zh.md | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/server/mcp_server_vod/README.md b/server/mcp_server_vod/README.md index 1f0d9ab4..db645ed2 100644 --- a/server/mcp_server_vod/README.md +++ b/server/mcp_server_vod/README.md @@ -262,7 +262,7 @@ Add subtitle `https:****.srt` to video vid1; outline color red, font size 70, ou #### Description -Picture-in-picture, add image to video, and video watermarking. +Image to video, and video watermarking. #### Trigger Example @@ -326,11 +326,10 @@ Configure the MCP service in your MCP client. Example MCP JSON: "env": { "VOLCENGINE_ACCESS_KEY": "Your Volcengine AK", "VOLCENGINE_SECRET_KEY": "Your Volcengine SK", - "MCP_TOOLS_TYPE": "Your Source", // groups | tools - // - In groups mode, MCP_TOOLS_SOURCE is the group name - // - In tools mode, MCP_TOOLS_SOURCE is the tool name - "MCP_TOOLS_SOURCE": "Your Source", - + "MCP_TOOLS_TYPE": "Your Source", // groups | tools + // - In groups mode, MCP_TOOLS_SOURCE is the group name + // - In tools mode, MCP_TOOLS_SOURCE is the tool name + "MCP_TOOLS_SOURCE": "Your Source" } } } diff --git a/server/mcp_server_vod/README_zh.md b/server/mcp_server_vod/README_zh.md index 5072fc3b..a89dd7fb 100644 --- a/server/mcp_server_vod/README_zh.md +++ b/server/mcp_server_vod/README_zh.md @@ -266,7 +266,7 @@ OCR 文字转字幕,支持 Vid 和 DirectUrl 两种输入模式。 #### 功能描述 -视频画中画、视频添加图片、视频水印能力 +视频画中画、视频水印能力 #### 最容易被唤起的 Prompt 示例