第22章:图片添加服务
22.1 概述
图片添加服务是剪映小助手的重要功能模块,负责将图片素材添加到剪映草稿中。该服务支持批量图片添加,提供了完整的图片处理流程,包括图片下载、格式处理、动画效果、转场效果和轨道管理等功能。
图片添加服务采用模块化设计,将复杂的图片处理逻辑封装成简单的API调用,用户只需要提供图片URL和基本参数,系统就能自动完成图片的下载、处理和添加操作。
22.2 核心功能
22.2.1 批量图片添加
add_images函数是图片添加服务的主入口,负责处理批量图片添加的完整流程。
核心实现
def add_images(
draft_url: str,
image_infos: str,
alpha: float = 1.0,
scale_x: float = 1.0,
scale_y: float = 1.0,
transform_x: int = 0,
transform_y: int = 0
) -> Tuple[str, str, List[str], List[str], List[SegmentInfo]]:
"""
添加图片到剪映草稿的业务逻辑
Args:
draft_url: 草稿URL,必选参数
image_infos: 图片信息JSON字符串,格式如下:
[
{
"image_url": "https://s.coze.cn/t/XpufYwc2_u4/", // [必选] 图片文件URL
"width": 1024, // [必选] 图片宽度(像素)
"height": 1024, // [必选] 图片高度(像素)
"start": 0, // [必选] 显示开始时间(微秒)
"end": 1000000, // [必选] 显示结束时间(微秒)
"in_animation": "", // [可选] 入场动画类型
"out_animation": "", // [可选] 出场动画类型
"loop_animation": "", // [可选] 循环动画类型
"in_animation_duration": "", // [可选] 入场动画时长(微秒)
"out_animation_duration": "", // [可选] 出场动画时长(微秒)
"loop_animation_duration": "", // [可选] 循环动画时长(微秒)
"transition": "", // [可选] 转场效果类型
"transition_duration": 500000 // [可选] 转场效果时长(微秒,范围100000-2500000)
}
]
alpha: 全局透明度[0, 1],默认值为1.0
scale_x: X轴缩放比例,默认值为1.0
scale_y: Y轴缩放比例,默认值为1.0
transform_x: X轴位置偏移(像素),默认值为0
transform_y: Y轴位置偏移(像素),默认值为0
Returns:
draft_url: 草稿URL
track_id: 视频轨道ID
image_ids: 图片ID列表
segment_ids: 片段ID列表
segment_infos: 片段信息列表,包含每个片段的ID、开始时间和结束时间
Raises:
CustomException: 图片批量添加失败
"""
logger.info(f"add_images started, draft_url: {draft_url}, alpha: {alpha}, scale_x: {scale_x}, scale_y: {scale_y}, transform_x: {transform_x}, transform_y: {transform_y}")
# 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_id: {draft_id}")
raise CustomException(CustomError.INVALID_DRAFT_URL)
# 2. 创建保存图片资源的目录
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_image_dir = os.path.join(draft_dir, "assets", "images")
os.makedirs(name=draft_image_dir, exist_ok=True)
logger.info(f"Created image directory: {draft_image_dir}")
# 3. 解析图片信息
images = parse_image_data(json_str=image_infos)
if len(images) == 0:
logger.error(f"No image info provided, draft_id: {draft_id}")
raise CustomException(CustomError.INVALID_IMAGE_INFO)
logger.info(f"Parsed {len(images)} image items")
# 4. 从缓存中获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
# 5. 添加视频轨道(图片使用视频轨道)
track_name = f"image_track_{helper.gen_unique_id()}"
script.add_track(track_type=draft.TrackType.video, track_name=track_name)
logger.info(f"Added image track: {track_name}")
# 6. 遍历图片信息,添加图片到草稿中的指定轨道,收集片段ID和信息
segment_ids = []
segment_infos = []
for i, image in enumerate(images):
try:
segment_id, segment_info = add_image_to_draft(
script, track_name,
draft_image_dir=draft_image_dir,
image=image,
alpha=alpha,
scale_x=scale_x,
scale_y=scale_y,
transform_x=transform_x,
transform_y=transform_y
)
segment_ids.append(segment_id)
segment_infos.append(segment_info)
logger.info(f"Added image {i+1}/{len(images)}, segment_id: {segment_id}")
except Exception as e:
logger.error(f"Failed to add image {i+1}/{len(images)}, error: {str(e)}")
raise
# 7. 保存草稿
script.save()
logger.info(f"Draft saved successfully")
# 8. 获取当前视频轨道ID
track_id = ""
for key in script.tracks.keys():
if script.tracks[key].name == track_name:
track_id = script.tracks[key].track_id
break
logger.info(f"Image track created, draft_id: {draft_id}, track_id: {track_id}")
# 9. 获取当前所有视频资源ID(包括图片)
image_ids = [video.material_id for video in script.materials.videos if video.material_type == "photo"]
logger.info(f"Image track completed, draft_id: {draft_id}, image_ids: {image_ids}")
return draft_url, track_id, image_ids, segment_ids, segment_infos
处理流程
- 参数验证:验证草稿URL和缓存状态
- 目录创建:创建图片资源存储目录
- 数据解析:解析和验证图片信息JSON
- 轨道创建:添加新的视频轨道(图片使用视频轨道)
- 批量添加:遍历添加每个图片到轨道
- 草稿保存:持久化草稿更改
- 信息返回:返回轨道ID、图片ID、片段ID和片段信息列表
22.2.2 单个图片添加
add_image_to_draft函数负责将单个图片添加到指定的轨道中,支持动画和转场效果。
核心实现
def add_image_to_draft(
script: ScriptFile,
track_name: str,
draft_image_dir: str,
image: dict,
alpha: float = 1.0,
scale_x: float = 1.0,
scale_y: float = 1.0,
transform_x: int = 0,
transform_y: int = 0
) -> Tuple[str, SegmentInfo]:
"""
向剪映草稿中添加单个图片
Args:
script: 草稿文件对象
track_name: 视频轨道名称
draft_image_dir: 图片资源目录
image: 图片信息字典,包含以下字段:
image_url: 图片URL
width: 图片宽度(像素)
height: 图片高度(像素)
start: 显示开始时间(微秒)
end: 显示结束时间(微秒)
in_animation: 入场动画类型(可选)
out_animation: 出场动画类型(可选)
loop_animation: 循环动画类型(可选)
in_animation_duration: 入场动画时长(微秒,可选)
out_animation_duration: 出场动画时长(微秒,可选)
loop_animation_duration: 循环动画时长(微秒,可选)
transition: 转场效果类型(可选)
transition_duration: 转场效果时长(微秒,可选)
alpha: 图片透明度
scale_x: 横向缩放
scale_y: 纵向缩放
transform_x: X轴位置偏移(像素)
transform_y: Y轴位置偏移(像素)
Returns:
segment_id: 片段ID
segment_info: 片段信息字典,包含id、start、end
Raises:
CustomException: 添加图片失败
"""
try:
# 1. 下载图片文件
image_path = helper.download(url=image['image_url'], save_dir=draft_image_dir)
logger.info(f"Downloaded image from {image['image_url']} to {image_path}")
# 2. 创建图片素材并添加到草稿
segment_duration = image['end'] - image['start']
# 创建图像调节设置
clip_settings = draft.ClipSettings(
alpha=alpha,
scale_x=scale_x,
scale_y=scale_y,
transform_x=transform_x / (image['width'] / 2), # 转换为半画布宽单位
transform_y=transform_y / (image['height'] / 2) # 转换为半画布高单位
)
# 创建视频片段(图片使用VideoSegment)
video_segment = draft.VideoSegment(
material=image_path,
target_timerange=trange(start=image['start'], duration=segment_duration),
clip_settings=clip_settings
)
# 3. 添加动画效果(如果指定了)
# 注意:由于动画相关的枚举类型较复杂,这里先预留接口
if image.get('in_animation'):
try:
logger.info(f"In animation '{image['in_animation']}' specified but not implemented yet")
# 这里可以根据需要添加具体的入场动画
# 例如:video_segment.add_animation(IntroType.XXX, duration=image.get('in_animation_duration'))
except Exception as e:
logger.warning(f"Failed to add in animation '{image['in_animation']}': {str(e)}")
if image.get('out_animation'):
try:
logger.info(f"Out animation '{image['out_animation']}' specified but not implemented yet")
# 这里可以根据需要添加具体的出场动画
# 例如:video_segment.add_animation(OutroType.XXX, duration=image.get('out_animation_duration'))
except Exception as e:
logger.warning(f"Failed to add out animation '{image['out_animation']}': {str(e)}")
if image.get('loop_animation'):
try:
logger.info(f"Loop animation '{image['loop_animation']}' specified but not implemented yet")
# 循环动画可能需要特殊处理
except Exception as e:
logger.warning(f"Failed to add loop animation '{image['loop_animation']}': {str(e)}")
# 4. 添加转场效果(如果指定了)
if image.get('transition'):
try:
logger.info(f"Transition '{image['transition']}' specified but not implemented yet")
# 例如:video_segment.add_transition(TransitionType.XXX, duration=image.get('transition_duration'))
except Exception as e:
logger.warning(f"Failed to add transition '{image['transition']}': {str(e)}")
logger.info(f"Created image segment, material_id: {video_segment.material_instance.material_id}")
logger.info(f"Image segment details - start: {image['start']}, duration: {segment_duration}, size: {image['width']}x{image['height']}")
# 5. 向指定轨道添加片段
script.add_segment(video_segment, track_name)
# 6. 构造片段信息
segment_info = SegmentInfo(
id=video_segment.segment_id,
start=image['start'],
end=image['end']
)
return video_segment.segment_id, segment_info
except CustomException:
logger.error(f"Add image to draft failed, draft_image_dir: {draft_image_dir}, image: {image}")
raise
except Exception as e:
logger.error(f"Add image to draft failed, error: {str(e)}")
raise CustomException(err=CustomError.IMAGE_ADD_FAILED)
22.2.3 图片数据解析
parse_image_data函数负责解析和验证图片数据的JSON字符串,处理可选字段的默认值。
核心实现
def parse_image_data(json_str: str) -> List[Dict[str, Any]]:
"""
解析图片数据的JSON字符串,处理可选字段的默认值
Args:
json_str: 包含图片数据的JSON字符串,格式如下:
[
{
"image_url": "https://s.coze.cn/t/XpufYwc2_u4/", // [必选] 图片文件URL
"width": 1024, // [必选] 图片宽度(像素)
"height": 1024, // [必选] 图片高度(像素)
"start": 0, // [必选] 显示开始时间(微秒)
"end": 1000000, // [必选] 显示结束时间(微秒)
"in_animation": "", // [可选] 入场动画类型
"out_animation": "", // [可选] 出场动画类型
"loop_animation": "", // [可选] 循环动画类型
"in_animation_duration": "", // [可选] 入场动画时长(微秒)
"out_animation_duration": "", // [可选] 出场动画时长(微秒)
"loop_animation_duration": "", // [可选] 循环动画时长(微秒)
"transition": "", // [可选] 转场效果类型
"transition_duration": 500000 // [可选] 转场效果时长(微秒,范围100000-2500000)
}
]
Returns:
包含图片对象的数组,每个对象都处理了默认值
Raises:
CustomException: 当JSON格式错误或缺少必选字段时抛出
"""
try:
# 解析JSON字符串
data = json.loads(json_str)
logger.info(f"Successfully parsed JSON with {len(data) if isinstance(data, list) else 1} items")
except json.JSONDecodeError as e:
logger.error(f"JSON parse error: {e.msg}")
raise CustomException(CustomError.INVALID_IMAGE_INFO, f"JSON parse error: {e.msg}")
# 确保输入是列表
if not isinstance(data, list):
logger.error("Image infos should be a list")
raise CustomException(CustomError.INVALID_IMAGE_INFO, "image_infos should be a list")
result = []
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_IMAGE_INFO, f"the {i}th item should be a dict")
# 检查必选字段
required_fields = ["image_url", "width", "height", "start", "end"]
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_IMAGE_INFO, f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
# 创建处理后的对象,设置默认值
processed_item = {
"image_url": item["image_url"],
"width": item["width"],
"height": item["height"],
"start": item["start"],
"end": item["end"],
"in_animation": item.get("in_animation", None), # 默认无入场动画
"out_animation": item.get("out_animation", None), # 默认无出场动画
"loop_animation": item.get("loop_animation", None), # 默认无循环动画
"in_animation_duration": item.get("in_animation_duration", None), # 默认无入场动画时长
"out_animation_duration": item.get("out_animation_duration", None), # 默认无出场动画时长
"loop_animation_duration": item.get("loop_animation_duration", None), # 默认无循环动画时长
"transition": item.get("transition", None), # 默认无转场
"transition_duration": item.get("transition_duration", 500000) # 默认转场时长500000微秒
}
# 验证数值范围
if processed_item["width"] <= 0 or processed_item["height"] <= 0:
logger.error(f"Invalid image dimensions: width={processed_item['width']}, height={processed_item['height']}")
raise CustomException(CustomError.INVALID_IMAGE_INFO, f"the {i}th item has invalid image dimensions")
if processed_item["start"] < 0 or processed_item["end"] <= processed_item["start"]:
logger.error(f"Invalid time range: start={processed_item['start']}, end={processed_item['end']}")
raise CustomException(CustomError.INVALID_IMAGE_INFO, f"the {i}th item has invalid time range")
# 验证转场时长范围
if processed_item["transition_duration"] < 100000 or processed_item["transition_duration"] > 2500000:
logger.warning(f"Transition duration {processed_item['transition_duration']} out of range [100000, 2500000], using default 500000")
processed_item["transition_duration"] = 500000
result.append(processed_item)
logger.debug(f"Processed image item {i+1}: {processed_item}")
return result
22.3 数据模型设计
22.3.1 请求响应模型
图片添加服务定义了清晰的数据模型:
class AddImagesRequest(BaseModel):
"""批量添加图片请求参数"""
draft_url: str = Field(..., description="草稿URL")
image_infos: str = Field(..., description="图片信息列表, 用JSON字符串表示")
alpha: float = Field(default=1.0, description="全局透明度[0, 1]")
scale_x: float = Field(default=1.0, description="X轴缩放比例")
scale_y: float = Field(default=1.0, description="Y轴缩放比例")
transform_x: int = Field(default=0, description="X轴位置偏移(像素)")
transform_y: int = Field(default=0, description="Y轴位置偏移(像素)")
class SegmentInfo(BaseModel):
"""片段信息"""
id: str = Field(..., description="片段ID")
start: int = Field(..., description="开始时间(微秒)")
end: int = Field(..., description="结束时间(微秒)")
class AddImagesResponse(BaseModel):
"""添加图片响应参数"""
draft_url: str = Field(default="", description="草稿URL")
track_id: str = Field(default="", description="视频轨道ID")
image_ids: List[str] = Field(default=[], description="图片ID列表")
segment_ids: List[str] = Field(default=[], description="片段ID列表")
segment_infos: List[SegmentInfo] = Field(default=[], description="片段信息列表")
22.3.2 图片参数配置
图片添加服务支持以下参数配置:
| 参数名 | 类型 | 必选 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|---|---|
| image_url | string | 是 | - | - | 图片文件URL |
| width | int | 是 | - | >0 | 图片宽度(像素) |
| height | int | 是 | - | >0 | 图片高度(像素) |
| start | int | 是 | - | ≥0 | 显示开始时间(微秒) |
| end | int | 是 | - | >start | 显示结束时间(微秒) |
| in_animation | string | 否 | None | - | 入场动画类型 |
| out_animation | string | 否 | None | - | 出场动画类型 |
| loop_animation | string | 否 | None | - | 循环动画类型 |
| in_animation_duration | int | 否 | None | - | 入场动画时长(微秒) |
| out_animation_duration | int | 否 | None | - | 出场动画时长(微秒) |
| loop_animation_duration | int | 否 | None | - | 循环动画时长(微秒) |
| transition | string | 否 | None | - | 转场效果类型 |
| transition_duration | int | 否 | 500000 | [100000, 2500000] | 转场效果时长(微秒) |
22.4 图片处理特性
22.4.1 坐标转换系统
图片添加服务实现了智能的坐标转换系统:
# 创建图像调节设置
clip_settings = draft.ClipSettings(
alpha=alpha,
scale_x=scale_x,
scale_y=scale_y,
transform_x=transform_x / (image['width'] / 2), # 转换为半画布宽单位
transform_y=transform_y / (image['height'] / 2) # 转换为半画布高单位
)
22.4.2 动画效果框架
系统预留了丰富的动画效果接口:
# 添加入场动画
---
## 相关资源
- **GitHub代码仓库**: https://github.com/Hommy-master/capcut-mate
- **Gitee代码仓库**: https://gitee.com/taohongmin-gitee/capcut-mate
- **API文档地址**: https://docs.jcaigc.cn
if image.get('in_animation'):
try:
logger.info(f"In animation specified but not implemented yet")
# 例如:video_segment.add_animation(IntroType.XXX, duration=image.get('in_animation_duration'))
except Exception as e:
logger.warning(f"Failed to add in animation: {str(e)}")
# 添加出场动画
if image.get('out_animation'):
try:
logger.info(f"Out animation specified but not implemented yet")
# 例如:video_segment.add_animation(OutroType.XXX, duration=image.get('out_animation_duration'))
except Exception as e:
logger.warning(f"Failed to add out animation: {str(e)}")
# 添加循环动画
if image.get('loop_animation'):
try:
logger.info(f"Loop animation specified but not implemented yet")
# 循环动画可能需要特殊处理
except Exception as e:
logger.warning(f"Failed to add loop animation: {str(e)}")
22.4.3 转场效果支持
系统支持多种转场效果:
# 添加转场效果
if image.get('transition'):
try:
logger.info(f"Transition specified but not implemented yet")
# 例如:video_segment.add_transition(TransitionType.XXX, duration=image.get('transition_duration'))
except Exception as e:
logger.warning(f"Failed to add transition: {str(e)}")
22.5 缓存集成
图片添加服务深度集成了草稿缓存机制:
# 从缓存获取草稿对象
script: ScriptFile = DRAFT_CACHE[draft_id]
# 操作完成后更新缓存
script.save()
22.6 错误处理
图片添加服务实现了完善的错误处理机制:
try:
# 图片添加逻辑
segment_id, segment_info = add_image_to_draft(
script, track_name, draft_image_dir=draft_image_dir,
image=image, alpha=alpha, scale_x=scale_x, scale_y=scale_y,
transform_x=transform_x, transform_y=transform_y
)
except CustomException:
logger.error(f"Add image to draft failed")
raise
except Exception as e:
logger.error(f"Add image to draft failed, error: {str(e)}")
raise CustomException(err=CustomError.IMAGE_ADD_FAILED)
22.7 日志记录
图片添加服务提供了详细的日志记录:
logger.info(f"add_images started, draft_url: {draft_url}, alpha: {alpha}, scale_x: {scale_x}, scale_y: {scale_y}, transform_x: {transform_x}, transform_y: {transform_y}")
logger.info(f"Created image directory: {draft_image_dir}")
logger.info(f"Parsed {len(images)} image items")
logger.info(f"Added image track: {track_name}")
logger.info(f"Added image {i+1}/{len(images)}, segment_id: {segment_id}")
logger.info(f"Draft saved successfully")
logger.info(f"Image track created, draft_id: {draft_id}, track_id: {track_id}")
logger.info(f"Image track completed, draft_id: {draft_id}, image_ids: {image_ids}")
22.8 性能优化
22.8.1 批量处理
图片添加服务支持批量处理,减少I/O操作次数:
# 批量添加图片
for i, image in enumerate(images):
segment_id, segment_info = add_image_to_draft(
script, track_name, draft_image_dir=draft_image_dir,
image=image, alpha=alpha, scale_x=scale_x, scale_y=scale_y,
transform_x=transform_x, transform_y=transform_y
)
segment_ids.append(segment_id)
segment_infos.append(segment_info)
22.8.2 异步下载
图片下载采用异步方式,提高处理效率:
# 下载图片文件
image_path = helper.download(url=image['image_url'], save_dir=draft_image_dir)
22.8.3 缓存优化
利用草稿缓存机制,避免重复加载:
# 从缓存获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
22.9 安全性考虑
22.9.1 输入验证
对所有输入参数进行严格验证:
# 验证图片尺寸
if processed_item["width"] <= 0 or processed_item["height"] <= 0:
logger.error(f"Invalid image dimensions")
raise CustomException(CustomError.INVALID_IMAGE_INFO, "invalid image dimensions")
# 验证时间范围
if processed_item["start"] < 0 or processed_item["end"] <= processed_item["start"]:
logger.error(f"Invalid time range")
raise CustomException(CustomError.INVALID_IMAGE_INFO, "invalid time range")
# 验证转场时长范围
if processed_item["transition_duration"] < 100000 or processed_item["transition_duration"] > 2500000:
logger.warning(f"Transition duration out of range, using default 500000")
processed_item["transition_duration"] = 500000
22.9.2 文件安全
图片文件下载到指定目录,避免路径遍历:
draft_image_dir = os.path.join(draft_dir, "assets", "images")
os.makedirs(name=draft_image_dir, exist_ok=True)
22.10 扩展性设计
22.10.1 动画效果扩展
动画效果采用插件式设计,便于扩展:
# 添加入场动画(预留接口)
if image.get('in_animation'):
try:
logger.info(f"In animation specified but not implemented yet")
# 例如:video_segment.add_animation(IntroType.XXX, duration=image.get('in_animation_duration'))
except Exception as e:
logger.warning(f"Failed to add in animation: {str(e)}")
22.10.2 转场效果扩展
转场效果支持多种类型,易于扩展:
# 添加转场效果(预留接口)
if image.get('transition'):
try:
logger.info(f"Transition specified but not implemented yet")
# 例如:video_segment.add_transition(TransitionType.XXX, duration=image.get('transition_duration'))
except Exception as e:
logger.warning(f"Failed to add transition: {str(e)}")
22.10.3 参数扩展
图片参数采用字典结构,便于添加新参数:
processed_item = {
"image_url": item["image_url"],
"width": item["width"],
"height": item["height"],
"start": item["start"],
"end": item["end"],
"in_animation": item.get("in_animation", None),
"out_animation": item.get("out_animation", None),
"loop_animation": item.get("loop_animation", None),
# 可以轻松添加新参数
}
22.11 总结
图片添加服务提供了完整的图片处理解决方案,具有以下特点:
- 功能完整:支持批量图片添加、单个图片处理、动画效果和转场效果
- 参数灵活:支持透明度、缩放、位置偏移、动画和转场等多种参数配置
- 坐标智能:实现了像素坐标到画布坐标的智能转换
- 效果丰富:预留了入场动画、出场动画、循环动画和转场效果的接口
- 错误处理:完善的异常处理和错误恢复机制
- 性能优化:批量处理、异步下载和缓存优化
- 扩展性强:插件式动画效果设计和灵活的参数结构
- 安全可靠:输入验证和文件安全保护
该服务为剪映小助手提供了强大的图片处理能力,是视频编辑功能的重要组成部分,特别是在制作相册视频、图片展示等场景中发挥重要作用。
相关资源
- GitHub代码仓库: github.com/Hommy-maste…
- Gitee代码仓库: gitee.com/taohongmin-…
- API文档地址: docs.jcaigc.cn