Merge pull request #15 from Hommy-master/dev - 解决关键帧参数不兼容的问题。

解决关键帧参数不兼容的问题。
This commit is contained in:
Hommy
2025-12-16 13:42:04 +08:00
committed by GitHub
7 changed files with 178 additions and 44 deletions
+66 -1
View File
@@ -55,6 +55,8 @@ 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字段详细说明
@@ -128,6 +130,30 @@ captions是一个JSON字符串,包含字幕数组,每个字幕对象包含
- 以画布中心为原点
- 实际存储时会转换为半画布高单位(假设画布高度1080,即除以540)
#### 文本阴影参数
`shadow_info` 是一个对象,包含以下字段:
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| 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] |
`has_shadow` 设置为 `true` 但未提供 `shadow_info` 时,系统将使用以下默认阴影配置:
```json
{
"shadow_color": "#000000",
"shadow_alpha": 0.9,
"shadow_diffuse": 15,
"shadow_distance": 5,
"shadow_angle": -45
}
```
## 响应格式
### 成功响应 (200)
@@ -212,7 +238,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 +250,45 @@ 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,
"has_shadow": true,
"shadow_info": {
"shadow_alpha": 0.8,
"shadow_color": "#000000",
"shadow_diffuse": 20.0,
"shadow_distance": 10.0,
"shadow_angle": -45.0
}
}'
```
#### 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
}'
```
## 错误码说明
| 错误码 | 错误信息 | 说明 | 解决方案 |
+17 -6
View File
@@ -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个关键帧
+3 -1
View File
@@ -208,7 +208,9 @@ def add_captions(acr: AddCaptionsRequest) -> AddCaptionsResponse:
style_text=acr.style_text,
underline=acr.underline,
italic=acr.italic,
bold=acr.bold
bold=acr.bold,
has_shadow=acr.has_shadow,
shadow_info=acr.shadow_info
)
return AddCaptionsResponse(
+11
View File
@@ -2,6 +2,15 @@ from pydantic import BaseModel, Field
from typing import List, Optional
class ShadowInfo(BaseModel):
"""文本阴影参数"""
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):
"""批量添加字幕请求参数"""
draft_url: str = Field(default="", description="草稿URL")
@@ -22,6 +31,8 @@ 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="文本阴影参数")
class CaptionItem(BaseModel):
+45 -10
View File
@@ -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,9 @@ def add_captions(
style_text: bool = False,
underline: bool = False,
italic: bool = False,
bold: bool = False
bold: bool = False,
has_shadow: bool = False,
shadow_info: Optional[ShadowInfo] = None
) -> Tuple[str, str, List[str], List[str], List[dict]]:
"""
批量添加字幕到剪映草稿的业务逻辑
@@ -83,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}")
f"style_text: {style_text}, underline: {underline}, italic: {italic}, bold: {bold}, has_shadow: {has_shadow}, shadow_info: {shadow_info}")
try:
# 1. 提取草稿ID
@@ -134,7 +137,9 @@ def add_captions(
style_text=style_text,
underline=underline,
italic=italic,
bold=bold
bold=bold,
has_shadow=has_shadow,
shadow_info=shadow_info
)
segment_ids.append(segment_id)
text_ids.append(text_id)
@@ -188,7 +193,9 @@ def add_caption_to_draft(
style_text: bool = False,
underline: bool = False,
italic: bool = False,
bold: bool = False
bold: bool = False,
has_shadow: bool = False,
shadow_info: Optional[ShadowInfo] = None
) -> Tuple[str, str, dict]:
"""
向剪映草稿中添加单个字幕
@@ -210,6 +217,8 @@ def add_caption_to_draft(
in_animation_duration: 入场动画时长,可选
out_animation_duration: 出场动画时长,可选
loop_animation_duration: 循环动画时长,可选
has_shadow: 是否启用文本阴影
shadow_info: 文本阴影参数
其他参数:字幕样式设置
Returns:
@@ -283,20 +292,46 @@ def add_caption_to_draft(
transform_y=transform_y / script.height # 转换为画布高度单位
)
# 7. 创建文本片段
# 7. 创建文本阴影(如果启用了阴影)
text_shadow = None
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(
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 +340,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 +348,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'],
+21 -11
View File
@@ -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 # [必选] 属性在该时间点的值
}
]
@@ -184,7 +193,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):
@@ -205,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)):
@@ -219,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
return result
+15 -15
View File
@@ -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]:
"""
根据转场名称查找对应的转场类型