From a8617338786e27e7c740c16456477d709fed5875 Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Mon, 15 Dec 2025 23:38:30 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=92=8C=E6=97=A5=E5=BF=97=EF=BC=8C=E6=8F=90=E9=AB=98=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/add_videos.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/service/add_videos.py b/src/service/add_videos.py index 3943179..302f463 100644 --- a/src/service/add_videos.py +++ b/src/service/add_videos.py @@ -154,10 +154,10 @@ def add_video_to_draft( # 0. 下载视频 video_path = download(url=video['video_url'], save_dir=draft_video_dir) - # 1. 使用指定的duration或计算值 - specified_duration = video.get('duration', video['end'] - video['start']) + # 1. 获取视频播放时长(target duration) + target_duration = video.get('duration', video['end'] - video['start']) - # 创建图像调节设置 + # 2. 创建图像调节设置 clip_settings = draft.ClipSettings( alpha=alpha, scale_x=scale_x, @@ -166,25 +166,24 @@ def add_video_to_draft( transform_y=transform_y / video['height'] # 转换为半画布高单位 ) - # 计算在素材中的源时间范围 - source_duration = video['end'] - video['start'] + # 3. 计算在时间轴上的显示时长(source duration) + display_duration = video['end'] - video['start'] - # 创建视频素材 + # 4. 创建视频素材 video_material = draft.VideoMaterial(video_path) + # 5. 创建视频片段 video_segment = draft.VideoSegment( material=video_material, - target_timerange=trange(start=video['start'], duration=source_duration), - source_timerange=trange(start=0, duration=min(video_material.duration, source_duration)), + target_timerange=trange(start=video['start'], duration=display_duration), + source_timerange=trange(start=0, duration=min(video_material.duration, display_duration)), speed=1.0, # 保持原始速度 - volume=video['volume'], + volume=video.get('volume', 1.0), clip_settings=clip_settings ) - - logger.info(f"material_id: {video_segment.material_instance.material_id}") - logger.info(f"video_path: {video_path}, start: {video['start']}, duration: {source_duration}, volume: {video['volume']}") + logger.info(f"video_path: {video_path}, start: {video['start']}, target_duration: {target_duration}, display_duration: {display_duration}, volume: {video.get('volume', 1.0)}") - # 2. 添加转场效果(如果指定了) + # 6. 添加转场效果(如果指定了) transition_name = video.get('transition') if transition_name: transition_type = find_transition_type_by_name(transition_name) @@ -198,10 +197,10 @@ def add_video_to_draft( else: logger.warning(f"Transition type not found for name: {transition_name}") - # 3. 向指定轨道添加片段, + # 7. 向指定轨道添加片段 script.add_segment(video_segment, track_name) - # 修复:返回 segment_id 而不是 material_id + # 8. 返回片段ID(注意:是segment_id而不是material_id) return video_segment.segment_id except CustomException: logger.info(f"Add video to draft failed, draft_video_dir: {draft_video_dir}, video: {video}") @@ -210,6 +209,7 @@ def add_video_to_draft( logger.error(f"Add video to draft failed, error: {str(e)}") raise CustomException(err=CustomError.VIDEO_ADD_FAILED) + def find_transition_type_by_name(transition_name: str) -> Optional[draft.TransitionType]: """ 根据转场名称查找对应的转场类型 From c9ba242e920572ca952b6f1b91b29a400e406c0b Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Tue, 16 Dec 2025 10:44:56 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AD=97=E5=B9=95?= =?UTF-8?q?=E9=98=B4=E5=BD=B1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/add_captions.md | 37 +++++++++++++++++++++++++++++++++++- src/router/v1.py | 3 ++- src/schemas/add_captions.py | 10 ++++++++++ src/service/add_captions.py | 38 +++++++++++++++++++++++++++---------- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/docs/add_captions.md b/docs/add_captions.md index 7922cc0..73a90f7 100644 --- a/docs/add_captions.md +++ b/docs/add_captions.md @@ -55,6 +55,7 @@ POST /openapi/capcut-mate/v1/add_captions | transform_x | number | ❌ | 0.0 | X轴位置偏移(像素) | | transform_y | number | ❌ | 0.0 | Y轴位置偏移(像素) | | style_text | boolean | ❌ | false | 是否使用样式文本 | +| shadow_info | object | ❌ | null | 文本阴影参数 | ### captions字段详细说明 @@ -128,6 +129,18 @@ captions是一个JSON字符串,包含字幕数组,每个字幕对象包含 - 以画布中心为原点 - 实际存储时会转换为半画布高单位(假设画布高度1080,即除以540) +#### 文本阴影参数 + +`shadow_info` 是一个对象,包含以下字段: + +| 字段名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| alpha | number | ❌ | 1.0 | 阴影不透明度,取值范围为[0, 1] | +| color | string | ❌ | "#000000" | 阴影颜色(十六进制) | +| diffuse | number | ❌ | 15.0 | 阴影扩散程度,取值范围为[0, 100] | +| distance | number | ❌ | 5.0 | 阴影距离,取值范围为[0, 100] | +| angle | number | ❌ | -45.0 | 阴影角度,取值范围为[-180, 180] | + ## 响应格式 ### 成功响应 (200) @@ -212,7 +225,7 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ -H "Content-Type: application/json" \ -d '{ "draft_url": "YOUR_DRAFT_URL", - "captions": "[{\"start\":0,\"end\":5000000,\"text\":\"你好,剪映\",\"keyword\":\"好\",\"keyword_color\":\"#ff0000\"}]", + "captions": "[{\"start\":0,\"end\":5000000,\"text\":\"你好,剪映\",\"keyword\":\"好\",\"keyword_color\":\"#ff0000\"]", "text_color": "#ffffff", "alignment": 1, "alpha": 1.0, @@ -224,6 +237,28 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ }' ``` +#### 4. 带文本阴影的字幕 + +```bash +curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ + -H "Content-Type: application/json" \ + -d '{ + "draft_url": "YOUR_DRAFT_URL", + "captions": "[{\"start\":0,\"end\":5000000,\"text\":\"你好,剪映\"}]", + "text_color": "#ffffff", + "alignment": 1, + "alpha": 1.0, + "font_size": 20, + "shadow_info": { + "alpha": 0.8, + "color": "#000000", + "diffuse": 20.0, + "distance": 10.0, + "angle": -45.0 + } + }' +``` + ## 错误码说明 | 错误码 | 错误信息 | 说明 | 解决方案 | diff --git a/src/router/v1.py b/src/router/v1.py index b7e485c..25468b4 100644 --- a/src/router/v1.py +++ b/src/router/v1.py @@ -208,7 +208,8 @@ def add_captions(acr: AddCaptionsRequest) -> AddCaptionsResponse: style_text=acr.style_text, underline=acr.underline, italic=acr.italic, - bold=acr.bold + bold=acr.bold, + shadow_info=acr.shadow_info ) return AddCaptionsResponse( diff --git a/src/schemas/add_captions.py b/src/schemas/add_captions.py index ca0bec8..bee8d0e 100644 --- a/src/schemas/add_captions.py +++ b/src/schemas/add_captions.py @@ -2,6 +2,15 @@ from pydantic import BaseModel, Field from typing import List, Optional +class ShadowInfo(BaseModel): + """文本阴影参数""" + alpha: float = Field(default=1.0, ge=0.0, le=1.0, description="阴影不透明度, 取值范围为[0, 1]") + color: str = Field(default="#000000", description="阴影颜色(十六进制)") + diffuse: float = Field(default=15.0, ge=0.0, le=100.0, description="阴影扩散程度, 取值范围为[0, 100]") + distance: float = Field(default=5.0, ge=0.0, le=100.0, description="阴影距离, 取值范围为[0, 100]") + angle: float = Field(default=-45.0, ge=-180.0, le=180.0, description="阴影角度, 取值范围为[-180, 180]") + + class AddCaptionsRequest(BaseModel): """批量添加字幕请求参数""" draft_url: str = Field(default="", description="草稿URL") @@ -22,6 +31,7 @@ class AddCaptionsRequest(BaseModel): underline: bool = Field(default=False, description="文字下划线开关") italic: bool = Field(default=False, description="文本斜体开关") bold: bool = Field(default=False, description="文本加粗开关") + shadow_info: Optional[ShadowInfo] = Field(default=None, description="文本阴影参数") class CaptionItem(BaseModel): diff --git a/src/service/add_captions.py b/src/service/add_captions.py index 6f6e4d7..a9bb695 100644 --- a/src/service/add_captions.py +++ b/src/service/add_captions.py @@ -2,10 +2,11 @@ import json from typing import List, Dict, Any, Tuple, Optional, Literal from src.utils.logger import logger -from src.pyJianYingDraft import ScriptFile, TrackType, TextSegment, TextStyle, ClipSettings, Timerange, FontType, TextBorder +from src.pyJianYingDraft import ScriptFile, TrackType, TextSegment, TextStyle, ClipSettings, Timerange, FontType, TextBorder, TextShadow from src.utils.draft_cache import DRAFT_CACHE from exceptions import CustomException, CustomError from src.utils import helper +from src.schemas.add_captions import ShadowInfo def add_captions( @@ -26,7 +27,8 @@ def add_captions( style_text: bool = False, underline: bool = False, italic: bool = False, - bold: bool = False + bold: bool = False, + shadow_info: Optional[ShadowInfo] = None ) -> Tuple[str, str, List[str], List[str], List[dict]]: """ 批量添加字幕到剪映草稿的业务逻辑 @@ -83,7 +85,7 @@ def add_captions( logger.debug(f"Function parameters - text_color: {text_color}, border_color: {border_color}, " f"alignment: {alignment}, alpha: {alpha}, font: {font}, font_size: {font_size}, " f"scale_x: {scale_x}, scale_y: {scale_y}, transform_x: {transform_x}, transform_y: {transform_y}, " - f"style_text: {style_text}, underline: {underline}, italic: {italic}, bold: {bold}") + f"style_text: {style_text}, underline: {underline}, italic: {italic}, bold: {bold}, shadow_info: {shadow_info}") try: # 1. 提取草稿ID @@ -134,7 +136,8 @@ def add_captions( style_text=style_text, underline=underline, italic=italic, - bold=bold + bold=bold, + shadow_info=shadow_info ) segment_ids.append(segment_id) text_ids.append(text_id) @@ -188,7 +191,8 @@ def add_caption_to_draft( style_text: bool = False, underline: bool = False, italic: bool = False, - bold: bool = False + bold: bool = False, + shadow_info: Optional[ShadowInfo] = None ) -> Tuple[str, str, dict]: """ 向剪映草稿中添加单个字幕 @@ -210,6 +214,7 @@ def add_caption_to_draft( in_animation_duration: 入场动画时长,可选 out_animation_duration: 出场动画时长,可选 loop_animation_duration: 循环动画时长,可选 + shadow_info: 文本阴影参数 其他参数:字幕样式设置 Returns: @@ -283,20 +288,33 @@ def add_caption_to_draft( transform_y=transform_y / script.height # 转换为画布高度单位 ) - # 7. 创建文本片段 + # 7. 创建文本阴影(如果提供了shadow_info) + text_shadow = None + if shadow_info: + shadow_rgb_color = hex_to_rgb(shadow_info.color) + text_shadow = TextShadow( + alpha=shadow_info.alpha, + color=shadow_rgb_color, + diffuse=shadow_info.diffuse, + distance=shadow_info.distance, + angle=shadow_info.angle + ) + + # 8. 创建文本片段 text_segment = TextSegment( text=caption['text'], timerange=timerange, style=text_style, border=text_border, # 添加边框 font=font_type, # 添加字体 + shadow=text_shadow, # 添加阴影 clip_settings=clip_settings ) logger.info(f"Created text segment, material_id: {text_segment.material_id}") logger.info(f"Text segment details - start: {caption['start']}, duration: {caption_duration}, text: {caption['text'][:50]}") - # 8. 处理关键词高亮 + # 9. 处理关键词高亮 if caption.get('keyword'): keyword_color = caption.get('keyword_color', '#ff7100') # 默认橙色 keyword_rgb_color = hex_to_rgb(keyword_color) @@ -305,7 +323,7 @@ def add_caption_to_draft( apply_keyword_highlight(text_segment, caption['keyword'], keyword_rgb_color, keyword_font_size) logger.info(f"Applied keyword highlighting: {caption['keyword']} with color {keyword_color} and font size {keyword_font_size}") - # 9. TODO: 处理动画效果(需要导入相应的动画类型) + # 10. TODO: 处理动画效果(需要导入相应的动画类型) if caption.get('in_animation'): logger.info(f"In animation specified but not implemented yet: {caption['in_animation']}") if caption.get('out_animation'): @@ -313,10 +331,10 @@ def add_caption_to_draft( if caption.get('loop_animation'): logger.info(f"Loop animation specified but not implemented yet: {caption['loop_animation']}") - # 10. 向指定轨道添加片段 + # 11. 向指定轨道添加片段 script.add_segment(text_segment, track_name) - # 11. 构造片段信息 + # 12. 构造片段信息 segment_info = { "id": text_segment.segment_id, "start": caption['start'], From 1cc27ccf47eb741571603469474718979120e6d4 Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Tue, 16 Dec 2025 10:49:09 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/add_captions.md | 20 ++++++++++---------- src/schemas/add_captions.py | 10 +++++----- src/service/add_captions.py | 10 +++++----- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/add_captions.md b/docs/add_captions.md index 73a90f7..bfe1bff 100644 --- a/docs/add_captions.md +++ b/docs/add_captions.md @@ -135,11 +135,11 @@ captions是一个JSON字符串,包含字幕数组,每个字幕对象包含 | 字段名 | 类型 | 必填 | 默认值 | 说明 | |--------|------|------|--------|------| -| alpha | number | ❌ | 1.0 | 阴影不透明度,取值范围为[0, 1] | -| color | string | ❌ | "#000000" | 阴影颜色(十六进制) | -| diffuse | number | ❌ | 15.0 | 阴影扩散程度,取值范围为[0, 100] | -| distance | number | ❌ | 5.0 | 阴影距离,取值范围为[0, 100] | -| angle | number | ❌ | -45.0 | 阴影角度,取值范围为[-180, 180] | +| shadow_alpha | number | ❌ | 1.0 | 阴影不透明度,取值范围为[0, 1] | +| shadow_color | string | ❌ | "#000000" | 阴影颜色(十六进制) | +| shadow_diffuse | number | ❌ | 15.0 | 阴影扩散程度,取值范围为[0, 100] | +| shadow_distance | number | ❌ | 5.0 | 阴影距离,取值范围为[0, 100] | +| shadow_angle | number | ❌ | -45.0 | 阴影角度,取值范围为[-180, 180] | ## 响应格式 @@ -250,11 +250,11 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ "alpha": 1.0, "font_size": 20, "shadow_info": { - "alpha": 0.8, - "color": "#000000", - "diffuse": 20.0, - "distance": 10.0, - "angle": -45.0 + "shadow_alpha": 0.8, + "shadow_color": "#000000", + "shadow_diffuse": 20.0, + "shadow_distance": 10.0, + "shadow_angle": -45.0 } }' ``` diff --git a/src/schemas/add_captions.py b/src/schemas/add_captions.py index bee8d0e..1907b70 100644 --- a/src/schemas/add_captions.py +++ b/src/schemas/add_captions.py @@ -4,11 +4,11 @@ from typing import List, Optional class ShadowInfo(BaseModel): """文本阴影参数""" - alpha: float = Field(default=1.0, ge=0.0, le=1.0, description="阴影不透明度, 取值范围为[0, 1]") - color: str = Field(default="#000000", description="阴影颜色(十六进制)") - diffuse: float = Field(default=15.0, ge=0.0, le=100.0, description="阴影扩散程度, 取值范围为[0, 100]") - distance: float = Field(default=5.0, ge=0.0, le=100.0, description="阴影距离, 取值范围为[0, 100]") - angle: float = Field(default=-45.0, ge=-180.0, le=180.0, description="阴影角度, 取值范围为[-180, 180]") + shadow_alpha: float = Field(default=1.0, ge=0.0, le=1.0, description="阴影不透明度, 取值范围为[0, 1]") + shadow_color: str = Field(default="#000000", description="阴影颜色(十六进制)") + shadow_diffuse: float = Field(default=15.0, ge=0.0, le=100.0, description="阴影扩散程度, 取值范围为[0, 100]") + shadow_distance: float = Field(default=5.0, ge=0.0, le=100.0, description="阴影距离, 取值范围为[0, 100]") + shadow_angle: float = Field(default=-45.0, ge=-180.0, le=180.0, description="阴影角度, 取值范围为[-180, 180]") class AddCaptionsRequest(BaseModel): diff --git a/src/service/add_captions.py b/src/service/add_captions.py index a9bb695..902d6e7 100644 --- a/src/service/add_captions.py +++ b/src/service/add_captions.py @@ -291,13 +291,13 @@ def add_caption_to_draft( # 7. 创建文本阴影(如果提供了shadow_info) text_shadow = None if shadow_info: - shadow_rgb_color = hex_to_rgb(shadow_info.color) + shadow_rgb_color = hex_to_rgb(shadow_info.shadow_color) text_shadow = TextShadow( - alpha=shadow_info.alpha, + alpha=shadow_info.shadow_alpha, color=shadow_rgb_color, - diffuse=shadow_info.diffuse, - distance=shadow_info.distance, - angle=shadow_info.angle + diffuse=shadow_info.shadow_diffuse, + distance=shadow_info.shadow_distance, + angle=shadow_info.shadow_angle ) # 8. 创建文本片段 From 2cd6f72e4d8312b4471644108d6e67cd2f88a605 Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Tue, 16 Dec 2025 10:55:38 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E5=AD=97?= =?UTF-8?q?=E9=98=B4=E5=BD=B1=E5=BC=80=E5=85=B3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/add_captions.md | 30 ++++++++++++++++++++++++++++ src/router/v1.py | 1 + src/schemas/add_captions.py | 1 + src/service/add_captions.py | 39 ++++++++++++++++++++++++++----------- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/docs/add_captions.md b/docs/add_captions.md index bfe1bff..e535c3b 100644 --- a/docs/add_captions.md +++ b/docs/add_captions.md @@ -55,6 +55,7 @@ POST /openapi/capcut-mate/v1/add_captions | transform_x | number | ❌ | 0.0 | X轴位置偏移(像素) | | transform_y | number | ❌ | 0.0 | Y轴位置偏移(像素) | | style_text | boolean | ❌ | false | 是否使用样式文本 | +| has_shadow | boolean | ❌ | false | 是否启用文本阴影 | | shadow_info | object | ❌ | null | 文本阴影参数 | ### captions字段详细说明 @@ -141,6 +142,18 @@ captions是一个JSON字符串,包含字幕数组,每个字幕对象包含 | shadow_distance | number | ❌ | 5.0 | 阴影距离,取值范围为[0, 100] | | shadow_angle | number | ❌ | -45.0 | 阴影角度,取值范围为[-180, 180] | +当 `has_shadow` 设置为 `true` 但未提供 `shadow_info` 时,系统将使用以下默认阴影配置: + +```json +{ + "shadow_color": "#000000", + "shadow_alpha": 0.9, + "shadow_diffuse": 15, + "shadow_distance": 5, + "shadow_angle": -45 +} +``` + ## 响应格式 ### 成功响应 (200) @@ -249,6 +262,7 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ "alignment": 1, "alpha": 1.0, "font_size": 20, + "has_shadow": true, "shadow_info": { "shadow_alpha": 0.8, "shadow_color": "#000000", @@ -259,6 +273,22 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ }' ``` +#### 5. 使用默认文本阴影的字幕 + +```bash +curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_captions \ + -H "Content-Type: application/json" \ + -d '{ + "draft_url": "YOUR_DRAFT_URL", + "captions": "[{\"start\":0,\"end\":5000000,\"text\":\"你好,剪映\"}]", + "text_color": "#ffffff", + "alignment": 1, + "alpha": 1.0, + "font_size": 20, + "has_shadow": true + }' +``` + ## 错误码说明 | 错误码 | 错误信息 | 说明 | 解决方案 | diff --git a/src/router/v1.py b/src/router/v1.py index 25468b4..d1f1c4a 100644 --- a/src/router/v1.py +++ b/src/router/v1.py @@ -209,6 +209,7 @@ def add_captions(acr: AddCaptionsRequest) -> AddCaptionsResponse: underline=acr.underline, italic=acr.italic, bold=acr.bold, + has_shadow=acr.has_shadow, shadow_info=acr.shadow_info ) diff --git a/src/schemas/add_captions.py b/src/schemas/add_captions.py index 1907b70..5ce685f 100644 --- a/src/schemas/add_captions.py +++ b/src/schemas/add_captions.py @@ -31,6 +31,7 @@ class AddCaptionsRequest(BaseModel): underline: bool = Field(default=False, description="文字下划线开关") italic: bool = Field(default=False, description="文本斜体开关") bold: bool = Field(default=False, description="文本加粗开关") + has_shadow: bool = Field(default=False, description="是否启用文本阴影") shadow_info: Optional[ShadowInfo] = Field(default=None, description="文本阴影参数") diff --git a/src/service/add_captions.py b/src/service/add_captions.py index 902d6e7..67dcb0c 100644 --- a/src/service/add_captions.py +++ b/src/service/add_captions.py @@ -28,6 +28,7 @@ def add_captions( underline: bool = False, italic: bool = False, bold: bool = False, + has_shadow: bool = False, shadow_info: Optional[ShadowInfo] = None ) -> Tuple[str, str, List[str], List[str], List[dict]]: """ @@ -85,7 +86,7 @@ def add_captions( logger.debug(f"Function parameters - text_color: {text_color}, border_color: {border_color}, " f"alignment: {alignment}, alpha: {alpha}, font: {font}, font_size: {font_size}, " f"scale_x: {scale_x}, scale_y: {scale_y}, transform_x: {transform_x}, transform_y: {transform_y}, " - f"style_text: {style_text}, underline: {underline}, italic: {italic}, bold: {bold}, shadow_info: {shadow_info}") + f"style_text: {style_text}, underline: {underline}, italic: {italic}, bold: {bold}, has_shadow: {has_shadow}, shadow_info: {shadow_info}") try: # 1. 提取草稿ID @@ -137,6 +138,7 @@ def add_captions( underline=underline, italic=italic, bold=bold, + has_shadow=has_shadow, shadow_info=shadow_info ) segment_ids.append(segment_id) @@ -192,6 +194,7 @@ def add_caption_to_draft( underline: bool = False, italic: bool = False, bold: bool = False, + has_shadow: bool = False, shadow_info: Optional[ShadowInfo] = None ) -> Tuple[str, str, dict]: """ @@ -214,6 +217,7 @@ def add_caption_to_draft( in_animation_duration: 入场动画时长,可选 out_animation_duration: 出场动画时长,可选 loop_animation_duration: 循环动画时长,可选 + has_shadow: 是否启用文本阴影 shadow_info: 文本阴影参数 其他参数:字幕样式设置 @@ -288,17 +292,30 @@ def add_caption_to_draft( transform_y=transform_y / script.height # 转换为画布高度单位 ) - # 7. 创建文本阴影(如果提供了shadow_info) + # 7. 创建文本阴影(如果启用了阴影) text_shadow = None - if shadow_info: - shadow_rgb_color = hex_to_rgb(shadow_info.shadow_color) - text_shadow = TextShadow( - alpha=shadow_info.shadow_alpha, - color=shadow_rgb_color, - diffuse=shadow_info.shadow_diffuse, - distance=shadow_info.shadow_distance, - angle=shadow_info.shadow_angle - ) + if has_shadow: + # 如果启用了阴影但没有提供shadow_info,则使用默认值 + if shadow_info is None: + # 创建默认的阴影配置 + shadow_rgb_color = hex_to_rgb("#000000") + text_shadow = TextShadow( + alpha=0.9, + color=shadow_rgb_color, + diffuse=15.0, + distance=5.0, + angle=-45.0 + ) + else: + # 使用提供的shadow_info配置 + shadow_rgb_color = hex_to_rgb(shadow_info.shadow_color) + text_shadow = TextShadow( + alpha=shadow_info.shadow_alpha, + color=shadow_rgb_color, + diffuse=shadow_info.shadow_diffuse, + distance=shadow_info.shadow_distance, + angle=shadow_info.shadow_angle + ) # 8. 创建文本片段 text_segment = TextSegment( From bce99a202425f7e45275d7387895121d407cdd41 Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Tue, 16 Dec 2025 13:27:00 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E5=B8=A7=E6=94=AF=E6=8C=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/add_keyframes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/service/add_keyframes.py b/src/service/add_keyframes.py index 95ef74e..db98d6d 100644 --- a/src/service/add_keyframes.py +++ b/src/service/add_keyframes.py @@ -184,7 +184,8 @@ def parse_keyframes_data(json_str: str) -> List[Dict[str, Any]]: # 支持的动画属性类型 supported_properties = { "KFTypePositionX", "KFTypePositionY", "KFTypeScaleX", - "KFTypeScaleY", "KFTypeRotation", "KFTypeAlpha" + "KFTypeScaleY", "KFTypeRotation", "KFTypeAlpha", "UNIFORM_SCALE", + "KFTypeSaturation", "KFTypeContrast", "KFTypeBrightness", "KFTypeVolume" } for i, item in enumerate(data): From 875b56bb88df4c2baecb5bbe70ae5adddd45d4ab Mon Sep 17 00:00:00 2001 From: Hommy <16620803786@163.com> Date: Tue, 16 Dec 2025 13:38:53 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=B8=A7=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/add_keyframes.md | 23 +++++++++++++++++------ src/service/add_keyframes.py | 29 +++++++++++++++++++---------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/add_keyframes.md b/docs/add_keyframes.md index 8886b88..52d5439 100644 --- a/docs/add_keyframes.md +++ b/docs/add_keyframes.md @@ -19,7 +19,7 @@ POST /openapi/capcut-mate/v1/add_keyframes ```json { "draft_url": "https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/get_draft?draft_id=2025092811473036584258", - "keyframes": "[{\"segment_id\":\"d62994b4-25fe-422a-a123-87ef05038558\",\"property\":\"KFTypePositionX\",\"offset\":0.5,\"value\":-0.1}]" + "keyframes": "[{\"segment_id\":\"d62994b4-25fe-422a-a123-87ef05038558\",\"property\":\"KFTypePositionX\",\"offset\":5000000,\"value\":-0.1}]" } ``` @@ -39,9 +39,15 @@ keyframes 是一个JSON字符串,包含关键帧数组,每个关键帧对象 |--------|------|------|------| | segment_id | string | ✅ | 目标片段的唯一标识ID | | property | string | ✅ | 动画属性类型,支持的类型见下表 | -| offset | number | ✅ | 关键帧在片段中的时间偏移(0-1范围,0表示开始,1表示结束) | +| offset | number | ✅ | 关键帧在片段中的时间偏移(微秒绝对时间) | | value | number | ✅ | 属性在该时间点的值 | +#### offset 参数说明 + +offset 参数只支持微秒绝对时间格式: +- 以微秒为单位的整数,如 5000000 表示片段开始后5秒的位置 +- 系统会自动将微秒值转换为相对时间比例 + #### 支持的动画属性类型 | 属性类型 | 描述 | 值范围 | 示例 | @@ -52,6 +58,11 @@ keyframes 是一个JSON字符串,包含关键帧数组,每个关键帧对象 | KFTypeScaleY | Y轴缩放 | 0.1 到 10.0 | 1.0 (原始), 0.5 (缩小), 2.0 (放大) | | KFTypeRotation | 旋转角度 | -360 到 360 | 0 (无旋转), 90 (顺时针90度) | | KFTypeAlpha | 透明度 | 0.0 到 1.0 | 1.0 (不透明), 0.5 (半透明), 0.0 (透明) | +| UNIFORM_SCALE | 统一缩放 | 0.1 到 10.0 | 1.0 (原始), 0.5 (缩小), 2.0 (放大) | +| KFTypeSaturation | 饱和度 | -1.0 到 1.0 | 0.0 (原始), -0.5 (降低), 0.5 (增强) | +| KFTypeContrast | 对比度 | -1.0 到 1.0 | 0.0 (原始), -0.5 (降低), 0.5 (增强) | +| KFTypeBrightness | 亮度 | -1.0 到 1.0 | 0.0 (原始), -0.5 (变暗), 0.5 (变亮) | +| KFTypeVolume | 音量 | 0.0 到 2.0 | 1.0 (原始), 0.5 (降低), 2.0 (增强) | ## 响应格式 @@ -85,14 +96,14 @@ keyframes 是一个JSON字符串,包含关键帧数组,每个关键帧对象 ### cURL 示例 -#### 1. 基本关键帧添加 +#### 1. 基本关键帧添加(使用微秒时间) ```bash curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_keyframes \ -H "Content-Type: application/json" \ -d '{ "draft_url": "YOUR_DRAFT_URL", - "keyframes": "[{\"segment_id\":\"d62994b4-25fe-422a-a123-87ef05038558\",\"property\":\"KFTypePositionX\",\"offset\":0,\"value\":0},{\"segment_id\":\"d62994b4-25fe-422a-a123-87ef05038558\",\"property\":\"KFTypePositionX\",\"offset\":1,\"value\":-0.5}]" + "keyframes": "[{\"segment_id\":\"segment-id\",\"property\":\"UNIFORM_SCALE\",\"offset\":0,\"value\":1},{\"segment_id\":\"segment-id\",\"property\":\"UNIFORM_SCALE\",\"offset\":5000000,\"value\":1.3}]" }' ``` @@ -103,7 +114,7 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_keyframes -H "Content-Type: application/json" \ -d '{ "draft_url": "YOUR_DRAFT_URL", - "keyframes": "[{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypePositionX\",\"offset\":0,\"value\":0},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypePositionY\",\"offset\":0,\"value\":0},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypeRotation\",\"offset\":0.5,\"value\":90},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypeAlpha\",\"offset\":1,\"value\":0}]" + "keyframes": "[{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypePositionX\",\"offset\":0,\"value\":0},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypePositionY\",\"offset\":0,\"value\":0},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypeRotation\",\"offset\":2500000,\"value\":90},{\"segment_id\":\"segment-uuid\",\"property\":\"KFTypeAlpha\",\"offset\":5000000,\"value\":0}]" }' ``` @@ -124,7 +135,7 @@ curl -X POST https://capcut-mate.jcaigc.cn/openapi/capcut-mate/v1/add_keyframes 1. **片段ID验证**: segment_id 必须是草稿中存在的有效片段ID 2. **片段类型限制**: 只有视觉片段(视频、图片、贴纸、文本)支持关键帧 -3. **时间偏移范围**: offset 值必须在 0.0-1.0 范围内 +3. **时间偏移范围**: offset 值必须是非负整数(微秒) 4. **属性值范围**: 不同的属性类型有不同的值范围限制 5. **重复关键帧**: 相同片段相同属性的关键帧会被累加,不会覆盖 6. **性能考虑**: 单次请求建议不超过100个关键帧 diff --git a/src/service/add_keyframes.py b/src/service/add_keyframes.py index db98d6d..f3bd999 100644 --- a/src/service/add_keyframes.py +++ b/src/service/add_keyframes.py @@ -70,7 +70,7 @@ def add_keyframes( if segment is None: logger.error(f"Segment not found: {keyframe_item['segment_id']}, skipping this keyframe") failed_keyframes += 1 - continue # 继续处理下一个关键帧,而不是抛出异常 + continue # 继续处理下一个关键帧 # 验证片段类型 if not isinstance(segment, VisualSegment): @@ -86,9 +86,18 @@ def add_keyframes( failed_keyframes += 1 continue # 继续处理下一个关键帧 - # 计算时间偏移(将相对位置转换为微秒) + # 处理offset值:只支持微秒绝对时间,需要转换为相对时间 segment_duration = segment.duration - time_offset = int(keyframe_item['offset'] * segment_duration) + offset_value = keyframe_item['offset'] + + # 将微秒绝对时间转换为相对时间(0-1范围) + relative_offset = offset_value / segment_duration + # 确保相对时间在有效范围内 + relative_offset = max(0.0, min(1.0, relative_offset)) + logger.info(f"Converting absolute offset {offset_value} microseconds to relative offset {relative_offset} for segment duration {segment_duration}") + + # 计算时间偏移(将相对位置转换为微秒) + time_offset = int(relative_offset * segment_duration) logger.info(f"Adding keyframe to segment {keyframe_item['segment_id']}: property={property_enum.value}, time_offset={time_offset}, value={keyframe_item['value']}") @@ -156,7 +165,7 @@ def parse_keyframes_data(json_str: str) -> List[Dict[str, Any]]: { "segment_id": "d62994b4-25fe-422a-a123-87ef05038558", # [必选] 目标片段的唯一标识ID "property": "KFTypePositionX", # [必选] 动画属性类型 - "offset": 0.5, # [必选] 关键帧在片段中的时间偏移(0-1范围) + "offset": 5000000, # [必选] 关键帧在片段中的时间偏移(微秒绝对时间) "value": -0.1 # [必选] 属性在该时间点的值 } ] @@ -206,10 +215,10 @@ def parse_keyframes_data(json_str: str) -> List[Dict[str, Any]]: logger.error(f"the {i}th item has unsupported property type: {item['property']}") raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item has unsupported property type: {item['property']}") - # 验证offset范围(0-1) - if not isinstance(item["offset"], (int, float)) or item["offset"] < 0.0 or item["offset"] > 1.0: - logger.error(f"the {i}th item has invalid offset value: {item['offset']}, must be between 0.0 and 1.0") - raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item has invalid offset value: {item['offset']}") + # 验证offset是数字类型且为非负数 + if not isinstance(item["offset"], (int, float)) or item["offset"] < 0: + logger.error(f"the {i}th item has invalid offset type or value: {item['offset']}, must be a non-negative number") + raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item has invalid offset type or value: {item['offset']}") # 验证value是数字类型 if not isinstance(item["value"], (int, float)): @@ -220,11 +229,11 @@ def parse_keyframes_data(json_str: str) -> List[Dict[str, Any]]: processed_item = { "segment_id": str(item["segment_id"]), "property": item["property"], - "offset": float(item["offset"]), + "offset": float(item["offset"]), # 保持原始微秒值,在主函数中转换为相对时间 "value": float(item["value"]) } result.append(processed_item) logger.info(f"Successfully parsed {len(result)} keyframe items") - return result \ No newline at end of file + return result