diff --git a/剪映脚本生成自动化/main.py b/剪映脚本生成自动化/main.py new file mode 100644 index 0000000..22c800d --- /dev/null +++ b/剪映脚本生成自动化/main.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- + +""" +剪映脚本生成自动化 +""" + +import asyncio +import uuid +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import edge_tts +import pycapcut as capcut +from mutagen.mp3 import MP3 +from pycapcut import trange +from pycapcut.keyframe import KeyframeProperty +from pycapcut.segment import ClipSettings + + +class EdgeTTS: + """ + edge在线语音合成 + """ + + # 中文音色 + TIMBRES = { + "女声-晓晓": "zh-CN-XiaoxiaoNeural", + "女声-晓辰": "zh-CN-XiaochenNeural", + "女声-晓倩": "zh-CN-XiaoqianNeural", + } + + def __init__( + self, + materials_path: Path, + ): + """ + 初始化语音合成器 + :param materials_path: 素材文件夹路径 + """ + # 素材文件夹路径 + self.materials_path = materials_path + + def generate_audio( + self, + content: str, + timbre: Optional[str] = "女声-晓晓", + rate: str = "+0%", + volume: str = "+0%", + ) -> Tuple[str, int]: + """ + 根据文本内容合成语音并返回音频素材名称 + :param content: 文本内容 + :param timbre: 音色名称,例如女声-晓晓 + :param rate: 语速 + :param volume: 音量 + :return 音频素材名称和持续时长 + """ + # noinspection PyBroadException + try: + # 异步处理:根据文本内容合成语音并保存为音频素材 + async def _async_generate_audio(): + # 实例化Communicate + communicator = edge_tts.Communicate( + text=content, + voice=self.TIMBRES[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 + + # 同步调用异步逻辑,对外暴露纯同步接口 + return asyncio.run(_async_generate_audio()) + except Exception as exception: + raise RuntimeError( + f"根据文本内容合成语音并保存为音频素材发声异常:{str(exception)}" + ) + + +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("正在生成剪映脚本...") + # 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.draft.add_track(track_type=capcut.TrackType.video) + + # 音频持续时长 + self.audio_duration = 0 + + # 实例化EdgeTTS + self.synthesizer = EdgeTTS(self.materials_path) + 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, + 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 name: 音频素材名称 + :param target_timerange: 音频素材在轨道上的范围,包括开始时间和持续时长 + :param source_timerange: 截取音频素材范围,包括开始时间和持续时长,默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分 + :param speed: 播放速度 + :param volume: 播放音量 + :param fade: 淡入淡出设置 + :return: 无 + """ + print(f"正在添加音频片段 {name}...", end="") + 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(audio_segment) + print("已完成") + return + except Exception as exception: + print(f"发生异常:{str(exception)}") + raise + + def _add_video( + self, + 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 name: 视频/图片素材名称 + :param target_timerange: 视频素材在轨道上的范围,包括开始时间和持续时长 + :param source_timerange: 截取视频素材范围,包括开始时间和持续时长 + :param speed: 播放速度 + :param volume: 播放音量 + :param clip_settings: 图像调节设置 + :param keyframes: 关键帧设置 + :param animation: 动画设置 + :param transition: 转场设置 + :param background_filling: 背景填充设置 + :return: 无 + """ + print(f"正在添加视频/图片片段 {name}...", end="") + 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(video_segment) + print("已完成") + return + except Exception as exception: + print(f"发生异常:{str(exception)}") + raise + + def _add_text( + self, + 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[str] = None, + animation: Optional[Dict[str, Any]] = None, + ) -> None: + """ + 构建文本片段 + :param content: 文本内容 + :param timerange: 文本素材在轨道上的范围,包括开始时间和持续时长 + :param border: 文本描边设置 + :param background: 文本背景设置 + :param font: 字体类型 + :param style: 字体样式 + :param clip_settings: 图像调节设置 + :param bubble: 气泡设置 + :param effect: 花字设置 + :param animation: 动画设置 + :return: 不返回 + """ + print(f"正在添加文本片段 {content}...", end="") + try: + # 创建文字片段 + text_segment = capcut.TextSegment( + text=content, + timerange=trange(*timerange), + border=capcut.TextBorder(**border) if border else None, + background=capcut.TextBackground(**background) if border 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) + + # 添加花字,可先保存预设,再在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id + if effect: + text_segment.add_effect(effect) + + # 添加动画 + if animation: + text_segment.add_animation(**animation) + + self.draft.add_segment(text_segment) + print("已完成") + return + except Exception as exception: + print(f"发生异常:{str(exception)}") + raise + + def _save(self) -> None: + """保存草稿""" + try: + self.draft.save() + print("草稿保存成功") + except Exception as exception: + print(f"发生异常:{str(exception)}") + raise + + def create_audio_and_text( + self, + script: str, + timbre: Optional[str] = "女声-晓晓", + rate: str = "+25%", + volume: str = "+0%", + ): + """ + 根据脚本合成音频和文本素材 + :param script: 脚本 + :param timbre: 声音音色 + :param rate: 语速 + :param volume: 音量 + :return: 无 + """ + print("正在根据脚本合成音频和文本素材...") + # 添加音频和文本轨道 + self.draft.add_track(track_type=capcut.TrackType.audio) + self.draft.add_track(track_type=capcut.TrackType.text) + + start = 0 + for content in script.split(","): + # 根据文本内容合成语音并返回音频素材名称 + name, duration = self.synthesizer.generate_audio( + content, timbre, rate, volume + ) + # 构建音频片段 + self._add_audio(name=name, target_timerange=(start, duration)) + # 构建文本片段 + self._add_text( + content=content, + timerange=(start, duration), + style={"size": 12.0, "align": 1}, # 字号为12,对齐方式为水平居中 + clip_settings={"transform_y": -0.6}, # 垂直位移 + effect="6896137858998930701", # 第二行第三列花字 + ) + start += duration + + self.audio_duration = start + + def create_video( + self, + 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, + ): + """ + 根据脚本合成音频和文本素材 + :param script: 脚本 + :param timbre: 声音音色 + :param rate: 语速 + :param volume: 音量 + :return: 无 + """ + print("正在根据脚本合成音频和文本素材...") + # 添加音频和文本轨道 + + +# ======================== 调用示例(使用抽象后的方法) ======================== +def execute_workflow(): + """生成剪映草稿""" + # 实例化 + draft = GenerateDraft( + name="demo2", + ) + + # 根据脚本合成音频和文本素材 + draft.generate_audio_and_text( + script="所有人今天准备狂点外卖,是真的0.1元起一杯的霸王茶姬,还外卖到家怎么能不来一杯呢,现在淘宝闪购给大家发福利,最高22元无门槛红包,官方链接就在下方,奶茶脑袋快冲" + ) + + # 构建背景视频片段 + draft._add_video(name="background.mp4", target_timerange=(0, draft.audio_duration)) + + # draft._add_video(name="arrow.gif", target_timerange=(0, draft.audio_duration)) + + # 保存草稿 + draft._save() + + +if __name__ == "__main__": + execute_workflow() diff --git a/剪映脚本生成自动化/materials/A3C2197E3CDED8B7.mp3 b/剪映脚本生成自动化/materials/A3C2197E3CDED8B7.mp3 new file mode 100644 index 0000000..99a8d2d Binary files /dev/null and b/剪映脚本生成自动化/materials/A3C2197E3CDED8B7.mp3 differ diff --git a/剪映脚本生成自动化/materials/B6236CB9ECFA98A4.mp3 b/剪映脚本生成自动化/materials/B6236CB9ECFA98A4.mp3 new file mode 100644 index 0000000..3a6f85d Binary files /dev/null and b/剪映脚本生成自动化/materials/B6236CB9ECFA98A4.mp3 differ diff --git a/剪映脚本生成自动化/materials/arrow.gif b/剪映脚本生成自动化/materials/arrow.gif new file mode 100644 index 0000000..e5e38e3 Binary files /dev/null and b/剪映脚本生成自动化/materials/arrow.gif differ diff --git a/剪映脚本生成自动化/materials/background.mp3 b/剪映脚本生成自动化/materials/background.mp3 new file mode 100644 index 0000000..b259af8 Binary files /dev/null and b/剪映脚本生成自动化/materials/background.mp3 differ diff --git a/剪映脚本生成自动化/materials/background.mp4 b/剪映脚本生成自动化/materials/background.mp4 new file mode 100644 index 0000000..bea931f Binary files /dev/null and b/剪映脚本生成自动化/materials/background.mp4 differ diff --git a/营销短视频生成自动化/main.py b/营销短视频生成自动化/main.py deleted file mode 100644 index feb8749..0000000 --- a/营销短视频生成自动化/main.py +++ /dev/null @@ -1,290 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -营销短视频生成自动化 -""" - -from pathlib import Path -from typing import Optional, Tuple, Union - -import pycapcut as capcut -from pycapcut import tim, trange - - -class Draft: - """剪映脚本生成器类""" - - # noinspection PyShadowingNames - def __init__( - self, - draft_name: str, - drafts_path: str = r"C:\Users\admin\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft", - stocks_path: Path = Path(__file__).parent / "materials", - video_width: int = 1920, - video_height: int = 1080, - ): - """ - 初始化剪映草稿生成器 - :param drafts_path: 草稿文件夹路径 - :param stocks_path: 素材文件夹路径 - :param draft_name: 草稿名称 - :param video_width: 视频宽度 - :param video_height: 视频高度 - """ - # 基础配置 - self.drafts_path = drafts_path - self.stocks_path = stocks_path - self.draft_name = draft_name - self.video_width, self.video_height = video_width, video_height - - # 尝试创建草稿文件夹和草稿 - # noinspection PyBroadException - try: - self.draft_folder = capcut.DraftFolder(self.drafts_path) - self.draft = self.draft_folder.create_draft( - self.draft_name, self.video_width, self.video_height, allow_replace=True - ) - - # 添加基础轨道:音频、视频、文本(图片/贴纸复用视频轨) - self.draft.add_track(capcut.TrackType.audio) - self.draft.add_track(capcut.TrackType.video) - self.draft.add_track(capcut.TrackType.text) - - except: - raise RuntimeError("创建草稿文件夹和草稿发生异常") - - # 检查素材文件夹是否存在,若不存在则抛出异常 - if not self.stocks_path.exists(): - raise FileNotFoundError(f"素材文件夹不存在") - - def _check_path(self, file_name: str) -> str: - """ - 检查文件是否存在,若存在则返回文件路径 - :param file_name: 文件名称 - :return 文件路径 - """ - file_path = self.stocks_path / file_name - if not file_path.exists(): - raise FileNotFoundError(f"素材文件不存在") - return file_path.as_posix() - - def add_audio( - self, - file_name: str, - start_time: str, - duration: str, - volume: float = 1.0, - fade_in: str = "0s", - fade_out: str = "0s", - ) -> capcut.AudioSegment: - """ - 添加音频片段 - :param file_name: 音频文件名称 - :param start_time: 音频在轨道上的开始时间(如 "0s") - :param duration: 音频持续时长(如 "1s") - :param volume: 音量(如 1.0) - :param fade_in: 淡入时长(如 “0s”) - :param fade_out: 淡出时长(如 “0s”) - Returns: capcut.AudioSegment - """ - try: - # 创建音频片段 - audio_segment = capcut.AudioSegment( - self._check_path(file_name), trange(start_time, duration), volume=volume - ) - # 添加淡入淡出效果 - audio_segment.add_fade(fade_in, fade_out) - - self.draft.add_segment(audio_segment) - return audio_segment - except Exception: - raise RuntimeError("添加音频片段发生异常") - - def add_video( - self, - file_name: str, - start_time: str, - duration: str, - animation: Optional[capcut.IntroType] = None, - transition: Optional[capcut.TransitionType] = None, - keyframes: Optional[list] = None, - ) -> capcut.VideoSegment: - """ - 添加视频片段 - :param file_name: 视频文件名称 - :param start_time: 视频在轨道上的开始时间(如 "0s") - :param duration: 视频持续时长(如 "1s") - :param animation: 动画配置 - :param transition: 转场配置 - :param keyframes: 关键帧配置 - :return capcut.VideoSegment - """ - try: - # 创建视频片段 - video_segment = capcut.VideoSegment( - self._check_path(file_name), trange(start_time, duration) - ) - # 添加动画 - if animation: - video_segment.add_animation(animation) - - # 添加转场 - if transition: - video_segment.add_transition(transition) - - # 添加关键帧 - if keyframes: - # noinspection PyShadowingBuiltins - for property, time, value in keyframes: - video_segment.add_keyframe(property, time, value) - - self.draft.add_segment(video_segment) - return video_segment - except Exception: - raise RuntimeError("添加视频片段发生异常") - - def add_image( - self, - file_name: str, - start_time: Union[str, tim], - duration: Optional[str] = None, - background_filling: Tuple[str, float] = ("blur", 0.0625), - ) -> capcut.VideoSegment: - """ - 添加图片/贴纸 - :param file_name: 图片文件名称 - :param start_time: 图片在轨道上的开始时间(如 "0s") - :param duration: 图片持续时长(如 "1s") - :param background_filling: 背景填充配置 - :return: capcut.VideoSegment - """ - try: - # 创建图片素材 - image_material = capcut.VideoMaterial(self._check_path(file_name)) - # 创建图片片段 - image_segment = capcut.VideoSegment( - image_material, - trange(start_time, duration if duration else image_material.duration), - ) # 若已设置图片持续时长则使用,否则使用视频持续时长 - - # 添加背景填充 - if background_filling: - image_segment.add_background_filling(*background_filling) - - self.draft.add_segment(image_segment) - return image_segment - except Exception: - raise RuntimeError("添加图片/贴纸发生异常") - - def add_text( - self, - content: str, - target_timerange: trange, - font: capcut.FontType = capcut.FontType.悠然体, - color: Tuple[float, float, float] = (1.0, 1.0, 0.0), - position_y: float = -0.8, - outro_animation: Optional[capcut.TextOutro] = capcut.TextOutro.故障闪动, - anim_duration: tim = tim("1s"), - bubble_id: Optional[str] = "7446997603268496646", - effect_id: Optional[str] = "7336825073334078725", - ) -> capcut.TextSegment: - """ - 添加文字片段 - - Args: - content: 文字内容 - target_timerange: 文字显示的时间范围(通常对齐视频片段) - font: 字体类型 - color: 文字颜色(RGB,0-1) - position_y: 文字Y轴位置(-0.8为屏幕下方) - outro_animation: 出场动画(None则不添加) - anim_duration: 动画时长 - bubble_id: 气泡效果ID(None则不添加) - effect_id: 花字效果ID(None则不添加) - - Returns: - capcut.TextSegment: 创建的文字片段对象 - """ - try: - # 创建文字片段 - text_segment = capcut.TextSegment( - content, - target_timerange, - font=font, - style=capcut.TextStyle(color=color), - clip_settings=capcut.ClipSettings(transform_y=position_y), - ) - - # 添加出场动画 - if outro_animation: - text_segment.add_animation(outro_animation, duration=anim_duration) - - # 添加气泡 - if bubble_id: - text_segment.add_bubble(bubble_id, bubble_id) - - # 添加花字 - if effect_id: - text_segment.add_effect(effect_id) - - self.draft.add_segment(text_segment) - return text_segment - except Exception: - raise RuntimeError("添加文字片段发生异常") - - def save(self) -> None: - """保存草稿""" - try: - self.draft.save() - print("草稿保存成功") - except Exception: - raise RuntimeError(f"保存草稿发生异常") - - -# ======================== 调用示例(使用抽象后的方法) ======================== -def main(): - """生成脚本""" - # 实例化 - draft = Draft( - draft_name="demo2", - ) - - # 添加音频 - draft.add_audio( - file_name="audio.mp3", - start_time="0s", - duration="5s", - volume=0.6, - ) - - # 添加视频 - video_segment = draft.add_video( - file_name="video.mp4", - start_time="0s", - duration="4.2s", - keyframes=[ - (capcut.KeyframeProperty.position_x, tim(0), -2), - (capcut.KeyframeProperty.position_x, tim("0.5s"), 0), - ], - ) - - # 添加图片/贴纸 - draft.add_image( - file_name="sticker.gif", - start_time=video_segment.end, # 视频结束位置开始 - background_filling=("blur", 0.0625), - ) - - # 添加文字 - draft.add_text( - content="抽象化后更易扩展!", - target_timerange=video_segment.target_timerange, - position_y=-0.5, # 微调位置 - ) - - # 保存草稿 - draft.save() - - -if __name__ == "__main__": - main() diff --git a/营销短视频生成自动化/materials/audio.mp3 b/营销短视频生成自动化/materials/audio.mp3 deleted file mode 100644 index f79c238..0000000 Binary files a/营销短视频生成自动化/materials/audio.mp3 and /dev/null differ diff --git a/营销短视频生成自动化/materials/sticker.gif b/营销短视频生成自动化/materials/sticker.gif deleted file mode 100644 index 81a28fa..0000000 Binary files a/营销短视频生成自动化/materials/sticker.gif and /dev/null differ diff --git a/营销短视频生成自动化/materials/video.mp4 b/营销短视频生成自动化/materials/video.mp4 deleted file mode 100644 index 8b21113..0000000 Binary files a/营销短视频生成自动化/materials/video.mp4 and /dev/null differ diff --git a/营销短视频生成自动化/materials/示例.mp4 b/营销短视频生成自动化/materials/示例.mp4 deleted file mode 100644 index 2664296..0000000 Binary files a/营销短视频生成自动化/materials/示例.mp4 and /dev/null differ