【剪映小助手源码精讲】22_图片添加服务

58 阅读15分钟

第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
处理流程
  1. 参数验证:验证草稿URL和缓存状态
  2. 目录创建:创建图片资源存储目录
  3. 数据解析:解析和验证图片信息JSON
  4. 轨道创建:添加新的视频轨道(图片使用视频轨道)
  5. 批量添加:遍历添加每个图片到轨道
  6. 草稿保存:持久化草稿更改
  7. 信息返回:返回轨道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_urlstring--图片文件URL
widthint->0图片宽度(像素)
heightint->0图片高度(像素)
startint-≥0显示开始时间(微秒)
endint->start显示结束时间(微秒)
in_animationstringNone-入场动画类型
out_animationstringNone-出场动画类型
loop_animationstringNone-循环动画类型
in_animation_durationintNone-入场动画时长(微秒)
out_animation_durationintNone-出场动画时长(微秒)
loop_animation_durationintNone-循环动画时长(微秒)
transitionstringNone-转场效果类型
transition_durationint500000[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 总结

图片添加服务提供了完整的图片处理解决方案,具有以下特点:

  1. 功能完整:支持批量图片添加、单个图片处理、动画效果和转场效果
  2. 参数灵活:支持透明度、缩放、位置偏移、动画和转场等多种参数配置
  3. 坐标智能:实现了像素坐标到画布坐标的智能转换
  4. 效果丰富:预留了入场动画、出场动画、循环动画和转场效果的接口
  5. 错误处理:完善的异常处理和错误恢复机制
  6. 性能优化:批量处理、异步下载和缓存优化
  7. 扩展性强:插件式动画效果设计和灵活的参数结构
  8. 安全可靠:输入验证和文件安全保护

该服务为剪映小助手提供了强大的图片处理能力,是视频编辑功能的重要组成部分,特别是在制作相册视频、图片展示等场景中发挥重要作用。


相关资源