第24章:关键帧添加服务
24.1 概述
关键帧添加服务是剪映小助手的核心功能模块,负责将关键帧动画添加到剪映草稿的视觉片段中。该服务支持批量关键帧添加,提供了完整的关键帧处理流程,包括动画属性设置、时间偏移计算、片段查找和关键帧应用等功能。
关键帧添加服务采用模块化设计,将复杂的关键帧处理逻辑封装成简单的API调用,用户只需要提供目标片段ID、动画属性类型、时间偏移和属性值,系统就能自动完成关键帧的创建和应用操作。
24.2 核心功能
24.2.1 批量关键帧添加
add_keyframes函数是关键帧添加服务的主入口,负责处理批量关键帧添加的完整流程。
核心实现
def add_keyframes(
draft_url: str,
keyframes: str
) -> Tuple[str, int, List[str]]:
"""
添加关键帧到剪映草稿的业务逻辑
Args:
draft_url: 草稿URL
keyframes: 关键帧信息列表的JSON字符串,格式如下:
[
{
"segment_id": "d62994b4-25fe-422a-a123-87ef05038558", # 目标片段的唯一标识ID
"property": "KFTypePositionX", # 动画属性类型
"offset": 0.5, # 关键帧在片段中的时间偏移(0-1范围)
"value": -0.1 # 属性在该时间点的值
}
]
Returns:
draft_url: 草稿URL
keyframes_added: 添加的关键帧数量
affected_segments: 受影响的片段ID列表
Raises:
CustomException: 关键帧添加失败
"""
logger.info(f"add_keyframes started, draft_url: {draft_url}, keyframes: {keyframes}")
# 1. 提取草稿ID
draft_id = helper.get_url_param(draft_url, "draft_id")
if (not draft_id) or (draft_id not in DRAFT_CACHE):
logger.error(f"Invalid draft_url or draft not found in cache: {draft_url}")
raise CustomException(CustomError.INVALID_DRAFT_URL)
# 2. 解析关键帧信息
keyframe_items = parse_keyframes_data(json_str=keyframes)
if len(keyframe_items) == 0:
logger.info(f"No keyframe info provided, draft_id: {draft_id}")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO)
logger.info(f"Parsed {len(keyframe_items)} keyframe items")
# 3. 从缓存中获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
# 4. 处理每个关键帧
keyframes_added = 0
affected_segments: List[str] = []
for i, keyframe_item in enumerate(keyframe_items):
try:
logger.info(f"Processing keyframe {i+1}/{len(keyframe_items)}, segment_id: {keyframe_item['segment_id']}, property: {keyframe_item['property']}")
# 查找片段
segment = find_segment_by_id(script, keyframe_item['segment_id'])
if segment is None:
logger.error(f"Segment not found: {keyframe_item['segment_id']}")
raise CustomException(CustomError.SEGMENT_NOT_FOUND)
# 验证片段类型
if not isinstance(segment, VisualSegment):
logger.error(f"Segment {keyframe_item['segment_id']} is not a visual segment, cannot add keyframes")
raise CustomException(CustomError.INVALID_SEGMENT_TYPE)
# 验证动画属性类型
try:
property_enum = KeyframeProperty(keyframe_item['property'])
except ValueError:
logger.error(f"Invalid property type: {keyframe_item['property']}")
raise CustomException(CustomError.INVALID_KEYFRAME_PROPERTY)
# 计算时间偏移(将相对位置转换为微秒)
segment_duration = segment.duration
time_offset = int(keyframe_item['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']}")
# 添加关键帧
segment.add_keyframe(property_enum, time_offset, keyframe_item['value'])
keyframes_added += 1
if keyframe_item['segment_id'] not in affected_segments:
affected_segments.append(keyframe_item['segment_id'])
logger.info(f"Successfully added keyframe {i+1}, total added: {keyframes_added}")
except CustomException:
logger.error(f"Failed to add keyframe {i+1}: {keyframe_item}")
raise
except Exception as e:
logger.error(f"Failed to add keyframe {i+1}, error: {str(e)}")
raise CustomException(CustomError.KEYFRAME_ADD_FAILED)
# 5. 保存草稿
try:
script.save()
logger.info(f"Draft saved successfully, keyframes_added: {keyframes_added}")
except Exception as e:
logger.error(f"Failed to save draft: {str(e)}")
raise CustomException(CustomError.KEYFRAME_ADD_FAILED)
logger.info(f"add_keyframes completed successfully - draft_id: {draft_id}, keyframes_added: {keyframes_added}, affected_segments: {affected_segments}")
return draft_url, keyframes_added, affected_segments
处理流程
- 参数验证:验证草稿URL和缓存状态
- 数据解析:解析和验证关键帧信息JSON
- 草稿获取:从缓存中获取草稿对象
- 关键帧处理:遍历处理每个关键帧
- 片段查找:根据segment_id查找目标片段
- 类型验证:验证片段类型是否为视觉片段
- 属性验证:验证动画属性类型的有效性
- 时间计算:将相对时间偏移转换为绝对时间(微秒)
- 关键帧添加:调用片段的add_keyframe方法添加关键帧
- 草稿保存:持久化草稿更改
- 信息返回:返回添加的关键帧数量和受影响的片段列表
24.2.2 片段查找功能
find_segment_by_id函数负责在草稿中查找指定ID的片段。
核心实现
def find_segment_by_id(script: ScriptFile, segment_id: str) -> Optional[VisualSegment]:
"""
通过segment_id在草稿中查找对应的片段
Args:
script: 草稿文件对象
segment_id: 片段ID
Returns:
找到的片段对象,如果未找到则返回None
"""
logger.info(f"Searching for segment with id: {segment_id}")
# 遍历所有轨道
for track_name, track in script.tracks.items():
logger.info(f"Searching in track: {track_name}, segments count: {len(track.segments)}")
# 遍历轨道中的所有片段
for segment in track.segments:
if segment.segment_id == segment_id:
logger.info(f"Found segment {segment_id} in track {track_name}")
return segment
logger.warning(f"Segment {segment_id} not found in any track")
return None
24.2.3 关键帧数据解析
parse_keyframes_data函数负责解析和验证关键帧数据的JSON字符串,处理可选字段的默认值。
核心实现
def parse_keyframes_data(json_str: str) -> List[Dict[str, Any]]:
"""
解析关键帧数据的JSON字符串,验证必选字段和数值范围
Args:
json_str: 包含关键帧数据的JSON字符串,格式如下:
[
{
"segment_id": "d62994b4-25fe-422a-a123-87ef05038558", # [必选] 目标片段的唯一标识ID
"property": "KFTypePositionX", # [必选] 动画属性类型
"offset": 0.5, # [必选] 关键帧在片段中的时间偏移(0-1范围)
"value": -0.1 # [必选] 属性在该时间点的值
}
]
Returns:
包含关键帧对象的数组,每个对象都验证过格式和范围
Raises:
CustomException: 当JSON格式错误或缺少必选字段时抛出
"""
try:
# 解析JSON字符串
data = json.loads(json_str)
except json.JSONDecodeError as e:
logger.error(f"JSON parse error: {e.msg}")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"JSON parse error: {e.msg}")
# 确保输入是列表
if not isinstance(data, list):
logger.error("keyframes should be a list")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, "keyframes should be a list")
result = []
# 支持的动画属性类型
supported_properties = {
"KFTypePositionX", "KFTypePositionY", "KFTypeScaleX",
"KFTypeScaleY", "KFTypeRotation", "KFTypeAlpha"
}
for i, item in enumerate(data):
if not isinstance(item, dict):
logger.error(f"the {i}th item should be a dict")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item should be a dict")
# 检查必选字段
required_fields = ["segment_id", "property", "offset", "value"]
missing_fields = [field for field in required_fields if field not in item]
if missing_fields:
logger.error(f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
# 验证动画属性类型
if item["property"] not in supported_properties:
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']}")
# 验证value是数字类型
if not isinstance(item["value"], (int, float)):
logger.error(f"the {i}th item has invalid value type: {type(item['value'])}, must be a number")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item has invalid value type: {type(item['value'])}")
# 创建处理后的对象
processed_item = {
"segment_id": str(item["segment_id"]),
"property": item["property"],
"offset": float(item["offset"]),
"value": float(item["value"])
}
result.append(processed_item)
logger.info(f"Successfully parsed {len(result)} keyframe items")
return result
24.3 数据模型设计
24.3.1 请求响应模型
关键帧添加服务定义了清晰的数据模型:
class AddKeyframesRequest(BaseModel):
"""添加关键帧请求参数"""
draft_url: str = Field(default="", description="草稿URL")
keyframes: str = Field(default="", description="关键帧信息列表, 用JSON字符串表示")
class KeyframeItem(BaseModel):
"""单个关键帧信息"""
segment_id: str = Field(..., description="目标片段的唯一标识ID")
property: str = Field(..., description="动画属性类型 (KFTypePositionX, KFTypePositionY, KFTypeScaleX, KFTypeScaleY, KFTypeRotation, KFTypeAlpha)")
offset: float = Field(..., ge=0.0, le=1.0, description="关键帧在片段中的时间偏移 (0-1范围)")
value: float = Field(..., description="属性在该时间点的值")
class AddKeyframesResponse(BaseModel):
"""添加关键帧响应参数"""
draft_url: str = Field(default="", description="草稿URL")
keyframes_added: int = Field(default=0, description="添加的关键帧数量")
affected_segments: List[str] = Field(default=[], description="受影响的片段ID列表")
24.3.2 关键帧参数配置
关键帧添加服务支持以下动画属性类型:
| 属性名 | 类型 | 取值范围 | 说明 |
|---|---|---|---|
| KFTypePositionX | float | -1.0 ~ 1.0 | X轴位置(相对于画布中心) |
| KFTypePositionY | float | -1.0 ~ 1.0 | Y轴位置(相对于画布中心) |
| KFTypeScaleX | float | 0.0 ~ 10.0 | X轴缩放比例 |
| KFTypeScaleY | float | 0.0 ~ 10.0 | Y轴缩放比例 |
| KFTypeRotation | float | -360.0 ~ 360.0 | 旋转角度(度) |
| KFTypeAlpha | float | 0.0 ~ 1.0 | 透明度(0完全透明,1完全不透明) |
24.3.3 时间偏移机制
关键帧的时间偏移采用相对时间机制:
# 计算时间偏移(将相对位置转换为微秒)
segment_duration = segment.duration
time_offset = int(keyframe_item['offset'] * segment_duration)
这种设计使得关键帧的时间位置与片段的实际长度无关,提高了关键帧的通用性和可复用性。
24.4 关键帧处理特性
24.4.1 智能片段查找
系统实现了高效的片段查找机制:
def find_segment_by_id(script: ScriptFile, segment_id: str) -> Optional[VisualSegment]:
# 遍历所有轨道
for track_name, track in script.tracks.items():
# 遍历轨道中的所有片段
for segment in track.segments:
if segment.segment_id == segment_id:
return segment
return None
24.4.2 类型安全验证
系统对片段类型进行严格验证:
# 验证片段类型
if not isinstance(segment, VisualSegment):
logger.error(f"Segment {keyframe_item['segment_id']} is not a visual segment, cannot add keyframes")
raise CustomException(CustomError.INVALID_SEGMENT_TYPE)
24.4.3 属性枚举验证
系统使用枚举类型确保动画属性的有效性:
# 验证动画属性类型
try:
property_enum = KeyframeProperty(keyframe_item['property'])
except ValueError:
logger.error(f"Invalid property type: {keyframe_item['property']}")
raise CustomException(CustomError.INVALID_KEYFRAME_PROPERTY)
24.4.4 时间计算精度
系统采用微秒级精度进行时间计算:
# 计算时间偏移(将相对位置转换为微秒)
segment_duration = segment.duration
time_offset = int(keyframe_item['offset'] * segment_duration)
24.5 缓存集成
关键帧添加服务深度集成了草稿缓存机制:
# 从缓存获取草稿对象
script: ScriptFile = DRAFT_CACHE[draft_id]
# 操作完成后更新缓存
script.save()
24.6 错误处理
关键帧添加服务实现了完善的错误处理机制:
try:
# 关键帧添加逻辑
segment.add_keyframe(property_enum, time_offset, keyframe_item['value'])
keyframes_added += 1
if keyframe_item['segment_id'] not in affected_segments:
affected_segments.append(keyframe_item['segment_id'])
logger.info(f"Successfully added keyframe {i+1}, total added: {keyframes_added}")
except CustomException:
logger.error(f"Failed to add keyframe {i+1}: {keyframe_item}")
raise
except Exception as e:
logger.error(f"Failed to add keyframe {i+1}, error: {str(e)}")
raise CustomException(CustomError.KEYFRAME_ADD_FAILED)
24.7 日志记录
关键帧添加服务提供了详细的日志记录:
logger.info(f"add_keyframes started, draft_url: {draft_url}, keyframes: {keyframes}")
logger.info(f"Parsed {len(keyframe_items)} keyframe items")
logger.info(f"Processing keyframe {i+1}/{len(keyframe_items)}, segment_id: {keyframe_item['segment_id']}, property: {keyframe_item['property']}")
logger.info(f"Adding keyframe to segment {keyframe_item['segment_id']}: property={property_enum.value}, time_offset={time_offset}, value={keyframe_item['value']}")
logger.info(f"Successfully added keyframe {i+1}, total added: {keyframes_added}")
logger.info(f"Draft saved successfully, keyframes_added: {keyframes_added}")
logger.info(f"add_keyframes completed successfully - draft_id: {draft_id}, keyframes_added: {keyframes_added}, affected_segments: {affected_segments}")
24.8 性能优化
24.8.1 批量处理
关键帧添加服务支持批量处理,减少I/O操作次数:
# 批量添加关键帧
for i, keyframe_item in enumerate(keyframe_items):
# 查找片段
segment = find_segment_by_id(script, keyframe_item['segment_id'])
# 验证动画属性类型
property_enum = KeyframeProperty(keyframe_item['property'])
# 计算时间偏移
segment_duration = segment.duration
time_offset = int(keyframe_item['offset'] * segment_duration)
# 添加关键帧
segment.add_keyframe(property_enum, time_offset, keyframe_item['value'])
keyframes_added += 1
24.8.2 缓存优化
利用草稿缓存机制,避免重复加载:
# 从缓存获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
24.8.3 片段查找优化
采用高效的片段查找算法:
def find_segment_by_id(script: ScriptFile, segment_id: str) -> Optional[VisualSegment]:
# 遍历所有轨道
for track_name, track in script.tracks.items():
# 遍历轨道中的所有片段
for segment in track.segments:
if segment.segment_id == segment_id:
return segment
return None
24.9 安全性考虑
24.9.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']}")
# 验证属性值类型
if not isinstance(item["value"], (int, float)):
logger.error(f"the {i}th item has invalid value type: {type(item['value'])}, must be a number")
raise CustomException(CustomError.INVALID_KEYFRAME_INFO, f"the {i}th item has invalid value type: {type(item['value'])}")
# 验证动画属性类型
if item["property"] not in supported_properties:
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']}")
24.9.2 类型安全
对片段类型进行严格验证:
# 验证片段类型
if not isinstance(segment, VisualSegment):
logger.error(f"Segment {keyframe_item['segment_id']} is not a visual segment, cannot add keyframes")
raise CustomException(CustomError.INVALID_SEGMENT_TYPE)
24.9.3 枚举验证
使用枚举类型确保动画属性的有效性:
# 验证动画属性类型
try:
property_enum = KeyframeProperty(keyframe_item['property'])
except ValueError:
logger.error(f"Invalid property type: {keyframe_item['property']}")
raise CustomException(CustomError.INVALID_KEYFRAME_PROPERTY)
24.10 扩展性设计
24.10.1 动画属性扩展
关键帧属性采用枚举类型设计,便于扩展:
# 支持的动画属性类型
supported_properties = {
"KFTypePositionX", "KFTypePositionY", "KFTypeScaleX",
"KFTypeScaleY", "KFTypeRotation", "KFTypeAlpha"
}
24.10.2 片段类型扩展
系统支持多种片段类型,易于扩展:
# 验证片段类型
if not isinstance(segment, VisualSegment):
logger.error(f"Segment {keyframe_item['segment_id']} is not a visual segment, cannot add keyframes")
raise CustomException(CustomError.INVALID_SEGMENT_TYPE)
24.10.3 时间机制扩展
相关资源
- GitHub代码仓库: https://github.com/Hommy-master/capcut-mate
- Gitee代码仓库: https://gitee.com/taohongmin-gitee/capcut-mate
- API文档地址: https://docs.jcaigc.cn
时间偏移机制采用相对时间设计,便于扩展到不同的片段类型和动画场景。
相关资源
代码仓库地址
- GitHub:
https://github.com/Hommy-master/capcut-mate - Gitee:
https://gitee.com/taohongmin-gitee/capcut-mate
接口文档地址
- API文档地址:
https://docs.jcaigc.cn
时间偏移采用相对时间机制,便于扩展:
# 计算时间偏移(将相对位置转换为微秒)
segment_duration = segment.duration
time_offset = int(keyframe_item['offset'] * segment_duration)
24.11 总结
关键帧添加服务提供了完整的关键帧动画解决方案,具有以下特点:
- 功能完整:支持批量关键帧添加、单个关键帧处理和动画属性设置
- 属性丰富:支持位置、缩放、旋转、透明度等多种动画属性
- 时间智能:采用相对时间偏移机制,提高关键帧的通用性和可复用性
- 片段查找:实现了高效的片段查找机制,支持跨轨道查找
- 类型安全:严格的片段类型验证和动画属性枚举验证
- 精度控制:采用微秒级精度进行时间计算,确保动画的精确性
- 错误处理:完善的异常处理和错误恢复机制
- 性能优化:批量处理、缓存优化和高效的查找算法
- 扩展性强:枚举式动画属性设计和灵活的参数结构
- 安全可靠:输入验证、类型安全和枚举验证保护
该服务为剪映小助手提供了强大的关键帧动画能力,是视频编辑功能的重要组成部分,特别是在制作复杂动画、转场效果、动态特效等场景中发挥重要作用。关键帧系统的引入使得视频编辑更加灵活和精确,为用户提供了专业级的动画控制能力。
2543

被折叠的 条评论
为什么被折叠?



