diff --git a/剪映脚本生成自动化/capcut.py b/剪映脚本生成自动化/capcut.py new file mode 100644 index 0000000..ce32275 --- /dev/null +++ b/剪映脚本生成自动化/capcut.py @@ -0,0 +1,445 @@ +# -*- coding: utf-8 -*- + +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + +import pycapcut as capcut +from pycapcut import trange +from pycapcut.keyframe import KeyframeProperty +from pycapcut.segment import ClipSettings + +from edgetts import EdgeTTS + + +class CapCutDraft: + """ + 封装pyCapCut,支持: + 1、向指定文本轨道添加文本片段 + 2、向指定音频轨道添加音频片段 + 3、向指定视频轨道添加视频或图片片段 + 4、向指定贴纸轨道添加贴纸片段 + 5、根据文本逐段合成语音,生成文本和语音字幕 + 6、将草稿保存至 CapCut草稿文件夹内 + (导出尚未支持) + """ + + # noinspection PyShadowingNames + def __init__( + self, + draft_name: str, + materials_folder_path: Union[str, Path], + capcut_folder_path: str = r"C:\Users\admin\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft", + allow_replace: bool = True, + video_width: int = 1080, + video_height: int = 1920, + video_fps: int = 30, + ): + """ + 初始化 + :param capcut_folder_path: CapCut草稿文件夹路径 + :param draft_name: 草稿名称 + :param allow_replace: 是否允许覆盖同名草稿 + :param video_width: 视频宽度,默认为 1080像素 + :param video_height: 视频高度,默认为 1920像素 + :param video_fps: 视频帧率(单位为帧/秒),默认为 30 + :param materials_folder_path: 素材文件夹路径 + """ + print("正在初始化 CapCutDraft...", end="") + # noinspection PyBroadException + try: + # 初始化草稿文件夹管理器 + self.draft_folder = capcut.DraftFolder(capcut_folder_path) + + # 新建草稿 + self.draft = self.draft_folder.create_draft( + draft_name=draft_name, + width=video_width, + height=video_height, + fps=video_fps, + allow_replace=allow_replace, + ) + + # 草稿持续时长 + self.draft_duration = 0 + + self.materials_folder_path = ( + materials_folder_path + if isinstance(materials_folder_path, Path) + else Path(materials_folder_path) + ) + if not self.materials_folder_path.exists(): + raise RuntimeError("素材文件夹路径不存在") + + print("已完成") + except Exception as exception: + raise RuntimeError(f"发生异常:{str(exception)}") + + def add_text_segment( + self, + track_name: str, + text: str, + add_track: bool = True, + timerange: Optional[Tuple[Optional[int, str], Optional[int, str]]] = None, + font: Optional[str] = None, + style: Optional[Dict[str, Any]] = None, + border: Optional[Dict[str, Any]] = None, + background: 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 add_track: 添加文本轨道,默认为是 + :param text: 文本 + :param timerange: 文本素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长 + :param font: 字体,默认为系统 + :param style: 字体样式,默认为字号 15,对齐方式 左对齐 + :param border: 文本描边设置,默认为无 + :param background: 文本背景设置,默认为无 + :param clip_settings: 图像调节设置,默认为移动至 (0, 0) + :param bubble: 气泡设置,默认为无 + :param effect: 花字设置,默认为无 + :param animation: 动画设置,默认为无 + :return: 无 + """ + print(f"正在向 {track_name}文本轨道添加文本片段...", end="") + try: + if add_track: + # 添加文本轨道 + self.draft.add_track( + track_type=capcut.TrackType.text, + track_name=track_name, + ) + + # 构建文本片段 + text_segment = capcut.TextSegment( + text=text.replace("\\n", "\n"), + timerange=trange( + *(timerange if timerange else (0, self.draft_duration)) + ), + font=capcut.FontType(font) if font else None, + style=capcut.TextStyle(**style) if style else None, + border=capcut.TextBorder(**border) if border else None, + background=capcut.TextBackground(**background) if background 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) + print("已完成") + return + except Exception: + raise + + def add_audio_segment( + self, + track_name: str, + material_path: Path, + add_track: bool = True, + target_timerange: Optional[ + Tuple[Optional[int, str], Optional[int, str]] + ] = None, + source_timerange: Optional[Tuple[str, str]] = None, + speed: Optional[float] = 1.0, + volume: Optional[float] = 1.0, + fade: Optional[Tuple[str, str]] = None, + ) -> None: + """ + 向指定音频轨道添加音频片段 + :param track_name: 轨道名称 + :param add_track: 添加音频轨道,默认为是 + :param material_path: 音频素材路径 + :param target_timerange: 音频素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长 + :param source_timerange: 截取音频素材范围,包括开始时间和持续时长,默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分 + :param speed: 播放速度,默认为 1.0 + :param volume: 播放音量,默认为 1.0 + :param fade: 淡入淡出设置,默认为无 + :return: 无 + """ + print(f"正在向 {track_name}音频轨道添加音频片段...", end="") + try: + if add_track: + # 添加音频轨道 + self.draft.add_track( + track_type=capcut.TrackType.video, + track_name=track_name, + ) + + # 构建音频片段 + audio_segment = capcut.AudioSegment( + material=material_path.as_posix(), + target_timerange=trange( + *( + target_timerange + if target_timerange + else (0, self.draft_duration) + ) + ), + 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) + print("已完成") + return + except Exception: + raise + + def add_video_segment( + self, + track_name: str, + material_path: Path, + target_timerange: Optional[ + Tuple[Optional[int, str], Optional[int, str]] + ] = None, + source_timerange: Optional[ + Tuple[Optional[int, str], Optional[int, str]], + ] = None, + speed: Optional[float] = 1.0, + volume: Optional[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 material_path: 视频或图片素材路径 + :param target_timerange: 视频或图片素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长 + :param source_timerange: 截取视频或图片素材范围,包括开始时间和持续时长,默认为空 + :param speed: 播放速度,默认为 1.0 + :param volume: 播放音量,默认为 1.0 + :param clip_settings: 图像调节设置,默认为无 + :param keyframes: 关键帧设置,默认为无 + :param animation: 动画设置,默认为无 + :param transition: 转场设置,默认为无 + :param background_filling: 背景填充设置,默认为无 + :return: 无 + """ + print(f"正在向 {track_name}视频轨道添加视频/图片片段...", end="") + try: + # 添加视频轨道 + self.draft.add_track( + track_type=capcut.TrackType.video, + track_name=track_name, + ) + + # 解析开始时间和持续时间 + target_start, target_duration = ( + target_timerange if target_timerange else (0, self.draft_duration) + ) + + # 视频素材 + video_material = capcut.VideoMaterial(path=material_path.as_posix()) + # 视频素材的持续时长 + video_material_duration = video_material.duration + + duration = 0 # 已添加视频素材的持续时长 + while duration < target_duration: + # 构建视频或图片片段 + video_segment = capcut.VideoSegment( + material=video_material, + target_timerange=trange( + start=duration, + duration=min( + (target_duration - duration), video_material_duration + ), + ), + 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) + + duration += video_material_duration + + print("已完成") + return + except Exception: + raise + + def add_sticker( + self, + track_name: str, + resource_id: str, + target_timerange: Optional[ + Tuple[Optional[int, str], Optional[int, str]] + ] = None, + clip_settings: Optional[Dict[str, Any]] = None, + ) -> None: + """ + 向指定贴纸轨道添加贴纸片段 + :param track_name: 轨道名称 + :param resource_id: 贴纸资源标识 + :param target_timerange: 贴纸在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长 + :param clip_settings: 图像调节设置,默认为无 + :return: 无 + """ + print(f"正在向 {track_name}贴纸轨道添加贴纸片段...", end="") + try: + # 添加贴纸轨道 + self.draft.add_track( + track_type=capcut.TrackType.sticker, + track_name=track_name, + ) + + # 构建贴纸 + 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 + if target_timerange + else (0, self.draft_duration) + ) + ), + clip_settings=( + capcut.ClipSettings(**clip_settings) if clip_settings else None + ), + ) + + # 向指定贴纸轨道添加贴纸片段 + self.draft.add_segment(segment=sticker_segment, track_name=track_name) + print("已完成") + except Exception: + raise + + def add_subtitles( + self, + text: str, + timbre: Optional[str] = "女声-晓晓", + rate: str = "+25%", + volume: str = "+0%", + font: Optional[str] = None, + style: Optional[Dict[str, Any]] = None, + clip_settings: Optional[Dict[str, Any]] = None, + effect: Optional[Dict[str, Any]] = None, + ): + """ + 添加字幕 + :param text: 文本 + :param timbre: 声音音色,默认为女声-晓晓 + :param rate: 语速,默认为 +25% + :param volume: 音量,默认为 +0% + :param font: 字体,默认为系统 + :param style: 文本样式,默认为字号 12,对齐方式 水平居中 + :param clip_settings: 图像调节设置,默认为移动至 (0, -0.5) + :param effect: 花字设置,默认为无 + :return: 无 + """ + print("正在添加字幕...") + # 添加文本轨道 + self.draft.add_track( + track_type=capcut.TrackType.text, + track_name=(text_track_name := "subtitles(text)"), + ) + # 添加音频轨道 + self.draft.add_track( + track_type=capcut.TrackType.audio, + track_name=(audio_track_name := "subtitles(audio)"), + ) + + # 构造语音文件保存文件夹路径(path对象) + subtitles_folder_path = self.materials_folder_path / "subtitles" + subtitles_folder_path.mkdir(exist_ok=True) + # 实例化 EdgeTTS + edge_tts = EdgeTTS(folder_path=subtitles_folder_path) + + start = 0 + for paragraph in text.split(","): + # 根据文本合成语音并将语音文件保存至指定文件夹内 + file_path, duration = edge_tts.synthetize( + text=paragraph, timbre=timbre, rate=rate, volume=volume + ) + # 向指定文本轨道添加文本片段 + self.add_text_segment( + track_name=text_track_name, + add_track=False, + text=paragraph, + timerange=(start, duration), + font=font, + style={ + "size": 12.0, + "align": 1, + **(style or {}), + }, + clip_settings={ + "transform_y": -0.5, + **(clip_settings or {}), + }, + effect=effect, + ) + # 向指定音频轨道添加音频片段 + self.add_audio_segment( + track_name=audio_track_name, + add_track=False, + material_path=file_path, + target_timerange=(start, duration), + ) + start += duration + + # 更新草稿持续时长 + self.draft_duration = start + print("已完成") + return + + def save(self) -> None: + """将草稿保存至 CapCut草稿文件夹内""" + print("正在将草稿保存至 CapCut草稿文件夹内...", end="") + try: + self.draft.save() + print("已完成") + return + except Exception: + raise diff --git a/剪映脚本生成自动化/draft.py b/剪映脚本生成自动化/draft.py deleted file mode 100644 index 7118c6e..0000000 --- a/剪映脚本生成自动化/draft.py +++ /dev/null @@ -1,544 +0,0 @@ -# -*- 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("已完成") diff --git a/剪映脚本生成自动化/edgetts.py b/剪映脚本生成自动化/edgetts.py index c2b9d3f..c0ed9b9 100644 --- a/剪映脚本生成自动化/edgetts.py +++ b/剪映脚本生成自动化/edgetts.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import asyncio -import uuid +from _md5 import md5 from pathlib import Path -from typing import Optional, Tuple +from typing import Tuple, Union import edge_tts from mutagen.mp3 import MP3 @@ -11,65 +11,62 @@ from mutagen.mp3 import MP3 class EdgeTTS: """ - edge在线语音合成 + 封装EdgeTTS,支持: + 1、根据文本合成语音并将语音文件保存至指定文件夹内 """ - # 中文音色 - TIMBRES = { - "女声-晓晓": "zh-CN-XiaoxiaoNeural", - "女声-晓辰": "zh-CN-XiaochenNeural", - "女声-晓倩": "zh-CN-XiaoqianNeural", - } - def __init__( self, - materials_path: Path, + folder_path: Union[str, Path], ): """ - 初始化语音合成器 - :param materials_path: 素材文件夹路径 + 初始化 + :param folder_path: 语音文件保存文件夹路径 """ - # 素材文件夹路径 - self.materials_path = materials_path + # 指定文件夹路径(path对象) + self.folder_path = ( + folder_path if isinstance(folder_path, Path) else Path(folder_path) + ) - def generate_audio( + def synthetize( self, - content: str, - timbre: Optional[str] = "女声-晓晓", - rate: str = "+0%", - volume: str = "+0%", - ) -> Tuple[str, int]: + text: str, + timbre: str, + rate: str, + volume: str, + ) -> Tuple[Path, int]: """ - 根据文本内容合成语音并返回音频素材名称 - :param content: 文本内容 - :param timbre: 音色名称,例如女声-晓晓 + 根据文本合成语音并将语音文件保存至指定文件夹内 + :param text: 文本 + :param timbre: 音色名称 :param rate: 语速 :param volume: 音量 - :return 音频素材名称和持续时长 + :return 语音文件路径(path对象)和持续时长(单位为微秒) """ + # noinspection PyBroadException try: - # 异步处理:根据文本内容合成语音并保存为音频素材 - async def _async_generate_audio(): - # 实例化Communicate + # 异步处理方法 + async def _async_synthetize(): + # 构造语音文件名称 + file_name = f"{md5((text + timbre + rate + volume).encode("utf-8")) + .hexdigest() + .upper()}.mp3" + # 构造语音文件路径 + file_path = self.folder_path / file_name communicator = edge_tts.Communicate( - text=content, - voice=self.TIMBRES[timbre], + text=text.replace("\n", ""), + voice=timbre, rate=rate, volume=volume, ) - # 音频素材名称 - name = f"{uuid.uuid4().hex[-16:].upper()}.mp3" - # 音频素材路径 - audio_path = self.materials_path / name - await communicator.save(audio_path := audio_path.as_posix()) - # 音频持续时长(单位为微妙) - duration = int(round(MP3(audio_path).info.length * 1000000)) - return name, duration + await communicator.save(file_path.as_posix()) + # 持续时长(单位为微秒) + duration = int(round(MP3(file_path.as_posix()).info.length * 1000000)) + return file_path, duration - # 同步调用异步逻辑,对外暴露纯同步接口 - return asyncio.run(_async_generate_audio()) + return asyncio.run(_async_synthetize()) except Exception as exception: raise RuntimeError( - f"根据文本内容合成语音并保存为音频素材发声异常:{str(exception)}" + f"根据文本合成语音并将语音文件保存至指定文件夹内发生异常:{str(exception)}" ) diff --git a/剪映脚本生成自动化/main.py b/剪映脚本生成自动化/main.py index d9d9c4d..230af03 100644 --- a/剪映脚本生成自动化/main.py +++ b/剪映脚本生成自动化/main.py @@ -4,7 +4,7 @@ 剪映脚本生成自动化 """ -from draft import GenerateDraft +from capcut import GenerateDraft # 编导方案1 diff --git a/剪映脚本生成自动化/materials/background.mp4 b/剪映脚本生成自动化/materials/background.mp4 deleted file mode 100644 index bea931f..0000000 Binary files a/剪映脚本生成自动化/materials/background.mp4 and /dev/null differ diff --git a/剪映脚本生成自动化/materials/logo.png b/剪映脚本生成自动化/materials/logo.png deleted file mode 100644 index a75c2df..0000000 Binary files a/剪映脚本生成自动化/materials/logo.png and /dev/null differ diff --git a/剪映脚本生成自动化/workflow.py b/剪映脚本生成自动化/workflow.py new file mode 100644 index 0000000..886189a --- /dev/null +++ b/剪映脚本生成自动化/workflow.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- + +import random +from pathlib import Path +from typing import Any, Dict, Literal + +from capcut import CapCutDraft + + +class WorkFlow: + """ + 生成 CapCut草稿的工作流,支持: + 1、初始化素材文件夹内所有素材 + 2、就工作流添加工作 + 3、基于工作流生成草稿 + """ + + # noinspection PyShadowingNames + def __init__( + self, + materials_folder_path: str, + video_width: int = 1080, + video_height: int = 1920, + video_fps: int = 30, + ): + """ + 初始化 + :param materials_folder_path: 素材文件夹路径 + :param video_width: 视频宽度,默认为 1080像素 + :param video_height: 视频高度,默认为 1920像素 + :param video_fps: 视频帧率(单位为帧/秒),默认为 30 + """ + print("正在初始化 DraftsGenerator...", end="") + # noinspection PyBroadException + try: + self.materials_folder_path = Path(materials_folder_path) + if not self.materials_folder_path.exists(): + raise RuntimeError("素材文件夹路径不存在") + + # 构建项目名称 + self.project_name = self.materials_folder_path.stem + + # 初始化工作流 + self.workflow = [ + "add_subtitles", + "add_background_video", + "add_logo", + "add_statement", + "add_sticker", + "save", + ] + + self.video_width = video_width + self.video_height = video_height + self.video_fps = video_fps + + # 初始化素材文件夹内所有素材 + self.materials = {} + self._init_materials() + + # 初始化所有工作配置 + self.configurations = { + "add_subtitles": { + "text": self.materials["subtitles_text"], + "timbre": [ + "zh-CN-XiaoxiaoNeural", + "zh-CN-XiaoyiNeural", + "zh-CN-YunjianNeural", + "zh-CN-YunxiNeural", + ], + "style": [{"size": 8}, {"size": 10}], + "effect": [ + {"effect_id": "7127561998556089631"}, + {"effect_id": "7166467215410187552"}, + {"effect_id": "6896138122774498567"}, + ], + }, + "add_background_video": { + "track_name": ["background_video"], + "material_path": self.materials["background_video_material_path"], + "clip_settings": [ + None, + { + "transform_x": 0.2, + }, + { + "transform_x": -0.2, + }, + { + "transform_y": 0.2, + }, + { + "transform_y": -0.2, + }, + ], + }, + "add_logo": { + "track_name": ["logo"], + "material_path": self.materials["logo_material_path"], + "clip_settings": [ + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": -0.68, + "transform_y": 0.82, + }, + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": 0, + "transform_y": 0.82, + }, + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": 0.68, + "transform_y": 0.82, + }, + ], + }, + "add_statement": { + "track_name": ["statement"], + "text": self.materials["statement_text"], + "border": [ + {"width": 40.0}, + {"width": 50.0}, + {"width": 60.0}, + ], # 描边宽度 + "style": [{"size": 8.0, "align": 1}], # 文本样式 + "clip_settings": [ + { + "transform_y": -0.8, + } + ], # 图像调节设置 + }, + "add_sticker": { + "track_name": ["sticker"], + "resource_id": ["7026858083393588487"], + "clip_settings": [ + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": -0.75, + "transform_y": -0.78, + } + ], # 图像调节设置 + }, + } + + print("已完成") + except Exception as exception: + raise RuntimeError(f"发生异常:{str(exception)}") + + def _init_materials(self) -> None: + """ + 初始化素材文件夹内所有素材 + :return: 无 + """ + # 字幕文本 + subtitles_path = self.materials_folder_path / "字幕文本.txt" + if subtitles_path.exists() and subtitles_path.is_file(): + with open(subtitles_path, "r", encoding="utf-8") as file: + subtitles_text = file.readlines() + if not subtitles_text: + raise RuntimeError("字幕文本为空") + self.materials["subtitles_text"] = subtitles_text + else: + raise RuntimeError("字幕文本不存在") + + # 背景视频 + background_videos_path = self.materials_folder_path / "背景视频" + if background_videos_path.exists() and background_videos_path.is_dir(): + background_video_material_path = [ + file_path + for file_path in background_videos_path.rglob("*.mp4") + if file_path.is_file() + ] + if not background_video_material_path: + raise RuntimeError("背景视频为空") + self.materials["background_video_material_path"] = ( + background_video_material_path + ) + else: + raise RuntimeError("背景视频文件夹不存在") + + # 标识 + logo_path = self.materials_folder_path / "标识.png" + if logo_path.exists() and logo_path.is_file(): + self.materials["logo_material_path"] = [logo_path] # 有且只有一张标识 + else: + raise RuntimeError("标识不存在") + + # 声明文本 + statement_path = self.materials_folder_path / "声明文本.txt" + if statement_path.exists() and statement_path.is_file(): + with open(statement_path, "r", encoding="utf-8") as file: + statement_text = file.readlines() + if not statement_text: + raise RuntimeError("声明文本为空") + self.materials["statement_text"] = statement_text + else: + raise RuntimeError("声明不存在") + + def add_work( + self, + work: Literal[ + "add_subtitles", + "add_background_video", + "add_logo", + "add_statement", + "add_sticker", + "save", + ], + ) -> None: + """ + 就工作流添加工作 + :param work: 工作,目前仅支持添加字幕、添加背景视频、添加标识、添加声明、添加贴纸和保存 + :return: 无 + """ + self.workflow.append(work) + + def generate( + self, + counts: int = 1, + ) -> None: + """ + 基于工作流生成草稿 + :param counts: 生成 CapCut草稿数 + :return: 无 + """ + for idx in range(counts): + draft_name = self.project_name + f"{idx + 1:03d}" + # 实例化 CapCutDraft + draft = CapCutDraft( + draft_name=draft_name, + video_width=self.video_width, + video_height=self.video_height, + video_fps=self.video_fps, + materials_folder_path=self.materials_folder_path, + ) + for work in self.workflow: + match work: + # 添加字幕 + case "add_subtitles": + draft.add_subtitles(**self._random(work=work)) + # 添加背景视频 + case "add_background_video": + draft.add_video_segment(**self._random(work=work)) + # 添加标识 + case "add_logo": + draft.add_video_segment(**self._random(work=work)) + # 添加声明 + case "add_statement": + draft.add_text_segment(**self._random(work=work)) + # 添加贴纸 + case "add_sticker": + draft.add_sticker(**self._random(work=work)) + # 将草稿保存至 CapCut草稿文件夹内 + case "save": + draft.save() + + def _random( + self, + work: Literal[ + "add_subtitles", + "add_background_video", + "add_logo", + "add_statement", + "add_sticker", + ], + ) -> Dict[str, Any]: + """ + 随机获取工作配置 + :param work: 工作,包括添加字幕、添加背景视频、添加标识、添加声明和添加贴纸 + :return: 工作配置 + """ + + return { + key: random.choice(value) + for key, value in self.configurations[work].items() + } + + +a = WorkFlow(materials_folder_path=r"E:\projects\251225") + + +a.generate(counts=5)