# -*- coding: utf-8 -*- from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import pycapcut as capcut from pycapcut import trange from pycapcut.keyframe import KeyframeProperty from pycapcut.segment import ClipSettings from edgetts import EdgeTTS class GenerateDraft: """剪映脚本生成器""" # noinspection PyShadowingNames def __init__( self, name: str, video_width: int = 1080, video_height: int = 1920, video_fps: int = 30, drafts_path: str = r"C:\Users\admin\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft", materials_path: str = r"C:\Users\admin\PycharmProjects\Python\剪映脚本生成自动化\materials", ): """ 初始化生成剪映脚本 :param name: 草稿名称 :param video_width: 视频宽度 :param video_height: 视频高度 :param video_fps: 视频帧率 :param drafts_path: 剪映草稿文件夹路径 :param materials_path: 素材文件夹路径 """ print("正在初始化剪映脚本生成器...", end="") # noinspection PyBroadException try: # 草稿名称 self.name = name # 视频宽度和高度 self.video_width, self.video_height = video_width, video_height # 视频帧率 self.video_fps = video_fps # 剪映草稿文件夹路径 self.drafts_path = drafts_path # 素材文件夹路径 self.materials_path = Path(materials_path) # 转为path对象 # 检查素材文件夹是否存在,若不存在则抛出异常 if not self.materials_path.exists(): raise FileNotFoundError(f"素材文件夹不存在") # 初始化草稿文件夹管理器 self.draft_folder = capcut.DraftFolder(self.drafts_path) # 新建草稿 self.draft = self.draft_folder.create_draft( draft_name=self.name, width=self.video_width, height=self.video_height, fps=self.video_fps, allow_replace=True, # 允许覆盖 ) # 脚本持续时长 self.duration = 0 # 实例化EdgeTTS self.synthesizer = EdgeTTS(self.materials_path) print("已完成") except Exception as exception: print(f"发生异常:{str(exception)}") raise def _get_material(self, name: str) -> str: """ 获取素材 :param name: 素材名称 :return 素材路径 """ # 素材路径 material_path = self.materials_path / name if not material_path.exists(): raise FileNotFoundError(f"素材文件不存在") return material_path.as_posix() def _add_audio( self, track_name: str, name: str, target_timerange: Tuple[Optional[int, str], Optional[int, str]], source_timerange: Optional[Tuple[str, str]] = None, speed: Optional[float] = None, volume: float = 1.0, fade: Optional[Tuple[str, str]] = None, ) -> None: """ 添加音频片段 :param track_name: 轨道名称 :param name: 音频素材名称 :param target_timerange: 音频素材在轨道上的范围,包括开始时间和持续时长 :param source_timerange: 截取音频素材范围,包括开始时间和持续时长,默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分 :param speed: 播放速度 :param volume: 播放音量 :param fade: 淡入淡出设置 :return: 无 """ try: # 构建音频片段 audio_segment = capcut.AudioSegment( material=self._get_material(name), target_timerange=trange(*target_timerange), source_timerange=( trange(*source_timerange) if source_timerange else None ), speed=speed, volume=volume, ) # 添加淡入淡出效果 if fade: audio_segment.add_fade(*fade) # 向指定轨道添加音频片段 self.draft.add_segment(segment=audio_segment, track_name=track_name) return except Exception: raise def _add_video( self, track_name: str, name: str, target_timerange: Tuple[Optional[int, str], Optional[int, str]], source_timerange: Optional[ Tuple[Optional[int, str], Optional[int, str]], ] = None, speed: Optional[float] = None, volume: float = 1.0, clip_settings: Optional[Dict[str, Any]] = None, keyframes: Optional[List[Tuple[KeyframeProperty, str, float]]] = None, animation: Optional[Dict[str, Any]] = None, transition: Optional[Dict[str, Any]] = None, background_filling: Optional[Tuple[str, Any]] = None, ) -> None: """ 添加视频/图片片段 :param track_name: 轨道名称 :param name: 视频/图片素材名称 :param target_timerange: 视频素材在轨道上的范围,包括开始时间和持续时长 :param source_timerange: 截取视频素材范围,包括开始时间和持续时长 :param speed: 播放速度 :param volume: 播放音量 :param clip_settings: 图像调节设置 :param keyframes: 关键帧设置 :param animation: 动画设置 :param transition: 转场设置 :param background_filling: 背景填充设置 :param track_name: 轨道名称 :return: 无 """ try: # 构建视频/图片片段 video_segment = capcut.VideoSegment( material=self._get_material(name), target_timerange=trange(*target_timerange), source_timerange=( trange(*source_timerange) if source_timerange else None ), speed=speed, volume=volume, clip_settings=ClipSettings(**clip_settings) if clip_settings else None, ) # (视频素材)添加关键帧 if keyframes: # noinspection PyShadowingBuiltins for property, time, value in keyframes: video_segment.add_keyframe(property, time, value) # (视频素材)添加动画 if animation: video_segment.add_animation(**animation) # (视频素材)添加转场 if transition: video_segment.add_transition(**transition) # (图片素材)添加背景填充 if background_filling: video_segment.add_background_filling(*background_filling) # 向指定轨道添加视频/图片片段 self.draft.add_segment(segment=video_segment, track_name=track_name) return except Exception: raise def _add_text( self, track_name: str, content: str, timerange: Tuple[Optional[int, str], Optional[int, str]], border: Optional[Dict[str, Any]] = None, background: Optional[Dict[str, Any]] = None, font: Optional[str] = None, style: Optional[Dict[str, Any]] = None, clip_settings: Optional[Dict[str, Any]] = None, bubble: Optional[Dict[str, Any]] = None, effect: Optional[Dict[str, Any]] = None, animation: Optional[Dict[str, Any]] = None, ) -> None: """ 添加文本片段 :param track_name: 轨道名称 :param content: 文本内容 :param timerange: 文本素材在轨道上的范围,包括开始时间和持续时长 :param border: 文本描边设置 :param background: 文本背景设置 :param font: 字体类型 :param style: 字体样式 :param clip_settings: 文本调节设置 :param bubble: 气泡设置 :param effect: 花字设置 :param animation: 动画设置 :return: 无 """ try: # 构建文本片段 text_segment = capcut.TextSegment( text=content, timerange=trange(*timerange), border=capcut.TextBorder(**border) if border else None, background=capcut.TextBackground(**background) if background else None, font=capcut.FontType(font) if font else None, style=capcut.TextStyle(**style) if style else None, clip_settings=( capcut.ClipSettings(**clip_settings) if clip_settings else None ), ) # 添加气泡 if bubble: text_segment.add_bubble(**bubble) # 添加花字 if effect: text_segment.add_effect( **effect ) # 可先将花字保存预设,再在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id # 添加动画 if animation: text_segment.add_animation(**animation) # 向指定轨道添加文本片段 self.draft.add_segment(segment=text_segment, track_name=track_name) return except Exception: raise def _add_sticker( self, track_name: str, resource_id: str, target_timerange: Tuple[Optional[int, str], Optional[int, str]], clip_settings: Optional[Dict[str, Any]] = None, ) -> None: """ 添加贴纸片段 :param track_name: 轨道名称 :param resource_id: 贴纸 resource_id :param target_timerange: 贴纸在轨道上的范围,包括开始时间和持续时长 :param clip_settings: 文本调节设置 :return: 无 """ try: # 构建贴纸 sticker_segment = capcut.StickerSegment( resource_id=resource_id, # 可先将贴纸保存为我的预设,再在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取 target_timerange=trange(*target_timerange), clip_settings=( capcut.ClipSettings(**clip_settings) if clip_settings else None ), ) # 向指定轨道添加贴纸 self.draft.add_segment(segment=sticker_segment, track_name=track_name) except Exception: raise def save(self) -> None: """保存草稿""" try: self.draft.save() except Exception: raise def add_subtitle( self, script: str, track_name: str = "subtitle", timbre: Optional[str] = "女声-晓晓", rate: str = "+25%", volume: str = "+0%", ): """ 根据脚本生成文本和音频字幕 :param track_name: 轨道名称 :param script: 脚本 :param timbre: 声音音色 :param rate: 语速 :param volume: 音量 :return: 无 """ print("正在根据脚本生成文本和音频字幕...", end="") # 添加文本轨道 self.draft.add_track( track_type=capcut.TrackType.text, track_name=(text_track_name := f"{track_name}:text"), ) # 添加音频轨道 self.draft.add_track( track_type=capcut.TrackType.audio, track_name=(audio_track_name := f"{track_name}:audio"), ) start = 0 for content in script.split(","): # 根据文本内容合成语音并返回音频素材名称 name, duration = self.synthesizer.generate_audio( content, timbre, rate, volume ) # 添加文本片段 self._add_text( track_name=text_track_name, content=content, timerange=(start, duration), style={"size": 12.0, "align": 1}, # 字号为12,对齐方式为水平居中 clip_settings={"transform_y": -0.5}, # 垂直位移 effect={"effect_id": "6896137858998930701"}, # 第二行第三列花字 ) # 添加音频片段 self._add_audio( track_name=audio_track_name, name=name, target_timerange=(start, duration), ) start += duration # 更新脚本持续时长 self.duration = start print("已完成") def add_video( self, track_name: str, name: str, target_timerange: Tuple[Optional[int, str], Optional[int, str]] = None, source_timerange: Optional[ Tuple[Optional[int, str], Optional[int, str]], ] = None, speed: Optional[float] = None, volume: float = 1.0, clip_settings: Optional[Dict[str, Any]] = None, keyframes: Optional[List[Tuple[KeyframeProperty, str, float]]] = None, animation: Optional[Dict[str, Any]] = None, transition: Optional[Dict[str, Any]] = None, background_filling: Optional[Tuple[str, Any]] = None, ): """ 向指定轨道添加视频/图片片段 :param track_name: 轨道名称 :param name: 视频/图片素材名称 :param target_timerange: 视频素材在轨道上的范围,包括开始时间和持续时长 :param source_timerange: 截取视频素材范围,包括开始时间和持续时长 :param speed: 播放速度 :param volume: 播放音量 :param clip_settings: 图像调节设置 :param keyframes: 关键帧设置 :param animation: 动画设置 :param transition: 转场设置 :param background_filling: 背景填充设置 :return: 无 """ # 预设图像调节设置 CLIPSETTINGS = { "logo": { "scale_x": 0.2, "scale_y": 0.2, "transform_x": -0.68, "transform_y": 0.82, } # 等比缩放至20%,移动至左上角 } # 根据轨道名称获取预设文本调节设置 if track_name in CLIPSETTINGS and not clip_settings: clip_settings = CLIPSETTINGS.get(track_name) print(f"正在向轨道 {track_name} 添加视频/图片片段...", end="") track_name = f"{track_name}:video" # 添加视频轨道 self.draft.add_track(track_type=capcut.TrackType.video, track_name=track_name) # 添加视频片段 self._add_video( track_name=track_name, name=name, target_timerange=( target_timerange if target_timerange else (0, self.duration) ), source_timerange=source_timerange, speed=speed, volume=volume, clip_settings=clip_settings, keyframes=keyframes, animation=animation, transition=transition, background_filling=background_filling, ) print("已完成") def add_text( self, track_name: str, content: str, timerange: Tuple[Optional[int, str], Optional[int, str]] = None, border: Optional[Dict[str, Any]] = None, background: Optional[Dict[str, Any]] = None, font: Optional[str] = None, style: Optional[Dict[str, Any]] = None, clip_settings: Optional[Dict[str, Any]] = None, bubble: Optional[Dict[str, Any]] = None, effect: Optional[str] = None, animation: Optional[Dict[str, Any]] = None, ): """ 向指定轨道添加文本片段 :param track_name: 轨道名称 :param content: 文本内容 :param timerange: 文本素材在轨道上的范围,包括开始时间和持续时长 :param border: 文本描边设置 :param background: 文本背景设置 :param font: 字体类型 :param style: 字体样式 :param clip_settings: 文本调节设置 :param bubble: 气泡设置 :param effect: 花字设置 :param animation: 动画设置 :return: 无 """ # 预设文本描边设置 BORDER = { "disclaimer": { "width": 60.0, } # 描边宽度为60 } # 根据轨道名称获取预设文本描边设置 if track_name in BORDER and not border: border = BORDER.get(track_name) # 预设字体样式 STYLE = { "disclaimer": { "size": 8.0, "align": 1, } # 字号为8,对齐方式为水平居中 } # 根据轨道名称获取预设字体样式 if track_name in STYLE and not style: style = STYLE.get(track_name) # 预设文本调节设置 CLIPSETTINGS = { "disclaimer": { "transform_y": -0.8, } # 垂直位移 } # 根据轨道名称获取预设字体样式 if track_name in CLIPSETTINGS and not clip_settings: clip_settings = CLIPSETTINGS.get(track_name) print(f"正在向轨道 {track_name} 添加文本片段...", end="") track_name = f"{track_name}:text" # 添加文本轨道 self.draft.add_track(track_type=capcut.TrackType.text, track_name=track_name) # 添加文本片段 self._add_text( track_name=track_name, content=content, timerange=(timerange if timerange else (0, self.duration)), border=border, background=background, font=font, style=style, clip_settings=clip_settings, bubble=bubble, effect=effect, animation=animation, ) print("已完成") def add_sticker( self, track_name: str, resource_id: str, target_timerange: Tuple[Optional[int, str], Optional[int, str]] = None, clip_settings: Optional[Dict[str, Any]] = None, ): """ 向指定轨道添加贴纸 :param track_name: 轨道名称 :param resource_id: 贴纸 resource_id,可先将贴纸保存为我的预设 :param target_timerange: 贴纸在轨道上的范围,包括开始时间和持续时长 :param clip_settings: 文本调节设置 :return: 无 """ # 预设文本描边设置 RESOURCEID = { "7026858083393588487": { "scale_x": 0.2, "scale_y": 0.2, "transform_x": -0.75, "transform_y": -0.78, } # 等比缩放至20%,移动至左上角 } # 根据轨道名称获取预设文本描边设置 if resource_id in RESOURCEID and not clip_settings: clip_settings = RESOURCEID.get(resource_id) print(f"正在向轨道 {track_name} 添加贴纸...", end="") track_name = f"{track_name}:video" # 添加贴纸轨道 self.draft.add_track(track_type=capcut.TrackType.sticker, track_name=track_name) # 添加贴纸 self._add_sticker( track_name=track_name, resource_id=resource_id, target_timerange=( target_timerange if target_timerange else (0, self.duration) ), clip_settings=clip_settings, ) print("已完成")