diff --git a/剪映脚本生成自动化/main.py b/剪映脚本生成自动化/main.py deleted file mode 100644 index 230af03..0000000 --- a/剪映脚本生成自动化/main.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -剪映脚本生成自动化 -""" - -from capcut import GenerateDraft - - -# 编导方案1 -def direct(): - """生成剪映草稿""" - # 实例化 - draft = GenerateDraft( - name="demo2", - ) # 需要 - - # 根据脚本生成文本和音频字幕 - draft.add_subtitle( - script="所有人今天准备狂点外卖,是真的0.1元起一杯的霸王茶姬,还外卖到家怎么能不来一杯呢,现在淘宝闪购给大家发福利,最高22元无门槛红包,官方链接就在下方,奶茶脑袋快冲" - ) - # 为背景视频添加视频轨道并添加视频片段 - draft.add_video( - track_name="background", - name="background.mp4", - clip_settings={"scale_x": 2.5, "scale_y": 2.5}, - ) - # 为logo添加视频轨道并添加图片片段 - draft.add_video( - track_name="logo", - name="logo.png", - ) - # 为免责声明添加视频轨道并添加图片片段 - draft.add_text( - track_name="disclaimer", - content="支付需谨慎谨防诈骗\n仅限支付宝用户,详情以活动为准", - ) - - draft.add_sticker(track_name="sticker", resource_id="7026858083393588487") - - # 保存草稿 - draft.save() - - -if __name__ == "__main__": - execute_workflow() diff --git a/剪映脚本生成自动化/workflow.py b/剪映脚本生成自动化/workflow.py deleted file mode 100644 index 886189a..0000000 --- a/剪映脚本生成自动化/workflow.py +++ /dev/null @@ -1,287 +0,0 @@ -# -*- 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) diff --git a/剪映脚本生成自动化/capcut.py b/短视频合成自动化/draft.py similarity index 79% rename from 剪映脚本生成自动化/capcut.py rename to 短视频合成自动化/draft.py index ce32275..31a6068 100644 --- a/剪映脚本生成自动化/capcut.py +++ b/短视频合成自动化/draft.py @@ -3,32 +3,28 @@ 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 +import pyJianYingDraft from edgetts import EdgeTTS -class CapCutDraft: +class JianYingDraft: """ - 封装pyCapCut,支持: + 封装 pyJianYing中生成草稿的相关功能,支持: 1、向指定文本轨道添加文本片段 2、向指定音频轨道添加音频片段 3、向指定视频轨道添加视频或图片片段 4、向指定贴纸轨道添加贴纸片段 5、根据文本逐段合成语音,生成文本和语音字幕 - 6、将草稿保存至 CapCut草稿文件夹内 - (导出尚未支持) + 6、将草稿保存至剪映草稿文件夹内 """ # 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", + materials_folder_path: Path, + drafts_folder_path: str = r"E:\JianyingPro Drafts", allow_replace: bool = True, video_width: int = 1080, video_height: int = 1920, @@ -36,7 +32,7 @@ class CapCutDraft: ): """ 初始化 - :param capcut_folder_path: CapCut草稿文件夹路径 + :param drafts_folder_path: 剪映草稿文件夹路径 :param draft_name: 草稿名称 :param allow_replace: 是否允许覆盖同名草稿 :param video_width: 视频宽度,默认为 1080像素 @@ -44,33 +40,24 @@ class CapCutDraft: :param video_fps: 视频帧率(单位为帧/秒),默认为 30 :param materials_folder_path: 素材文件夹路径 """ - print("正在初始化 CapCutDraft...", end="") # noinspection PyBroadException try: # 初始化草稿文件夹管理器 - self.draft_folder = capcut.DraftFolder(capcut_folder_path) + self.draft_folder = pyJianYingDraft.DraftFolder(drafts_folder_path) # 新建草稿 self.draft = self.draft_folder.create_draft( draft_name=draft_name, + allow_replace=allow_replace, 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("素材文件夹路径不存在") + self.materials_folder_path = materials_folder_path - print("已完成") except Exception as exception: raise RuntimeError(f"发生异常:{str(exception)}") @@ -105,27 +92,30 @@ class CapCutDraft: :param animation: 动画设置,默认为无 :return: 无 """ - print(f"正在向 {track_name}文本轨道添加文本片段...", end="") try: if add_track: # 添加文本轨道 self.draft.add_track( - track_type=capcut.TrackType.text, + track_type=pyJianYingDraft.TrackType.text, track_name=track_name, ) # 构建文本片段 - text_segment = capcut.TextSegment( + text_segment = pyJianYingDraft.TextSegment( text=text.replace("\\n", "\n"), - timerange=trange( + timerange=pyJianYingDraft.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, + font=pyJianYingDraft.FontType(font) if font else None, + style=pyJianYingDraft.TextStyle(**style) if style else None, + border=pyJianYingDraft.TextBorder(**border) if border else None, + background=( + pyJianYingDraft.TextBackground(**background) if background else None + ), clip_settings=( - capcut.ClipSettings(**clip_settings) if clip_settings else None + pyJianYingDraft.ClipSettings(**clip_settings) + if clip_settings + else None ), ) # 添加气泡 @@ -144,8 +134,7 @@ class CapCutDraft: # 向指定文本轨道添加文本片段 self.draft.add_segment(segment=text_segment, track_name=track_name) - print("已完成") - return + except Exception: raise @@ -174,19 +163,18 @@ class CapCutDraft: :param fade: 淡入淡出设置,默认为无 :return: 无 """ - print(f"正在向 {track_name}音频轨道添加音频片段...", end="") try: if add_track: # 添加音频轨道 self.draft.add_track( - track_type=capcut.TrackType.video, + track_type=pyJianYingDraft.TrackType.video, track_name=track_name, ) # 构建音频片段 - audio_segment = capcut.AudioSegment( + audio_segment = pyJianYingDraft.AudioSegment( material=material_path.as_posix(), - target_timerange=trange( + target_timerange=pyJianYingDraft.trange( *( target_timerange if target_timerange @@ -194,7 +182,9 @@ class CapCutDraft: ) ), source_timerange=( - trange(*source_timerange) if source_timerange else None + pyJianYingDraft.trange(*source_timerange) + if source_timerange + else None ), speed=speed, volume=volume, @@ -205,8 +195,7 @@ class CapCutDraft: # 向指定音频轨道添加音频片段 self.draft.add_segment(segment=audio_segment, track_name=track_name) - print("已完成") - return + except Exception: raise @@ -223,7 +212,9 @@ class CapCutDraft: 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, + keyframes: Optional[ + List[Tuple[pyJianYingDraft.keyframe, Union[str, int], float]] + ] = None, animation: Optional[Dict[str, Any]] = None, transition: Optional[Dict[str, Any]] = None, background_filling: Optional[Tuple[str, Any]] = None, @@ -243,11 +234,10 @@ class CapCutDraft: :param background_filling: 背景填充设置,默认为无 :return: 无 """ - print(f"正在向 {track_name}视频轨道添加视频/图片片段...", end="") try: # 添加视频轨道 self.draft.add_track( - track_type=capcut.TrackType.video, + track_type=pyJianYingDraft.TrackType.video, track_name=track_name, ) @@ -257,35 +247,41 @@ class CapCutDraft: ) # 视频素材 - video_material = capcut.VideoMaterial(path=material_path.as_posix()) + video_material = pyJianYingDraft.VideoMaterial( + path=material_path.as_posix() + ) # 视频素材的持续时长 video_material_duration = video_material.duration duration = 0 # 已添加视频素材的持续时长 while duration < target_duration: # 构建视频或图片片段 - video_segment = capcut.VideoSegment( + video_segment = pyJianYingDraft.VideoSegment( material=video_material, - target_timerange=trange( + target_timerange=pyJianYingDraft.trange( start=duration, duration=min( (target_duration - duration), video_material_duration ), ), source_timerange=( - trange(*source_timerange) if source_timerange else None + pyJianYingDraft.trange(*source_timerange) + if source_timerange + else None ), speed=speed, volume=volume, clip_settings=( - ClipSettings(**clip_settings) if clip_settings else None + pyJianYingDraft.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) + for property, offset, value in keyframes: + video_segment.add_keyframe(property, offset, value) # 添加动画 if animation: @@ -304,8 +300,6 @@ class CapCutDraft: duration += video_material_duration - print("已完成") - return except Exception: raise @@ -326,18 +320,17 @@ class CapCutDraft: :param clip_settings: 图像调节设置,默认为无 :return: 无 """ - print(f"正在向 {track_name}贴纸轨道添加贴纸片段...", end="") try: # 添加贴纸轨道 self.draft.add_track( - track_type=capcut.TrackType.sticker, + track_type=pyJianYingDraft.TrackType.sticker, track_name=track_name, ) # 构建贴纸 - sticker_segment = capcut.StickerSegment( + sticker_segment = pyJianYingDraft.StickerSegment( resource_id=resource_id, # 可先将贴纸保存为我的预设,再在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取 - target_timerange=trange( + target_timerange=pyJianYingDraft.trange( *( target_timerange if target_timerange @@ -345,13 +338,15 @@ class CapCutDraft: ) ), clip_settings=( - capcut.ClipSettings(**clip_settings) if clip_settings else None + pyJianYingDraft.ClipSettings(**clip_settings) + if clip_settings + else None ), ) # 向指定贴纸轨道添加贴纸片段 self.draft.add_segment(segment=sticker_segment, track_name=track_name) - print("已完成") + except Exception: raise @@ -378,15 +373,14 @@ class CapCutDraft: :param effect: 花字设置,默认为无 :return: 无 """ - print("正在添加字幕...") # 添加文本轨道 self.draft.add_track( - track_type=capcut.TrackType.text, + track_type=pyJianYingDraft.TrackType.text, track_name=(text_track_name := "subtitles(text)"), ) # 添加音频轨道 self.draft.add_track( - track_type=capcut.TrackType.audio, + track_type=pyJianYingDraft.TrackType.audio, track_name=(audio_track_name := "subtitles(audio)"), ) @@ -415,7 +409,7 @@ class CapCutDraft: **(style or {}), }, clip_settings={ - "transform_y": -0.5, + "transform_y": -0.75, **(clip_settings or {}), }, effect=effect, @@ -426,20 +420,17 @@ class CapCutDraft: add_track=False, material_path=file_path, target_timerange=(start, duration), + volume=1.5, ) 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/剪映脚本生成自动化/edgetts.py b/短视频合成自动化/edgetts.py similarity index 100% rename from 剪映脚本生成自动化/edgetts.py rename to 短视频合成自动化/edgetts.py diff --git a/短视频合成自动化/export.py b/短视频合成自动化/export.py new file mode 100644 index 0000000..ddc0a02 --- /dev/null +++ b/短视频合成自动化/export.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- + +import random +import subprocess +import time +from pathlib import Path +from typing import Any, Dict, Optional + +import pyJianYingDraft +import win32con +import win32gui + +from draft import JianYingDraft + + +class JianYingExport: + """ + 封装 pyJianYing中导出草稿的相关功能,支持: + 1、初始化素材文件夹内所有素材 + 2、就工作流添加工作 + 3、基于工作流生成草稿 + """ + + # noinspection PyShadowingNames + def __init__( + self, + materials_folder_path: str, + jianying_program_path: str = r"E:\JianyingPro\5.9.0.11632\JianyingPro.exe", + draft_counts: int = 10, + video_width: int = 1080, + video_height: int = 1920, + video_fps: int = 30, + ): + """ + 初始化 + :param jianying_program_path: 剪映执行程序路径 + :param materials_folder_path: 素材文件夹路径 + :param draft_counts: 草稿数,默认为 10 + :param video_width: 视频宽度,默认为 1080像素 + :param video_height: 视频高度,默认为 1920像素 + :param video_fps: 视频帧率(单位为帧/秒),默认为 30 + """ + # noinspection PyBroadException + try: + self.jianying_program_path = Path(jianying_program_path) + if not self.jianying_program_path.exists(): + raise RuntimeError("剪映执行程序路径不存在") + + # 初始化剪映专业版进程 + self.jianying_process = None + + self.materials_folder_path = Path(materials_folder_path) + if not self.materials_folder_path.exists(): + raise RuntimeError("素材文件夹路径不存在") + + # 初始化导出文件夹路径 + self.exports_folder_path = Path( + self.materials_folder_path.as_posix().replace("materials", "exports") + ) + self.exports_folder_path.mkdir(exist_ok=True) + + self.materials = {} + # 初始化素材文件夹内所有素材 + self._init_materials() + + # 构建项目名称 + self.project_name = self.materials_folder_path.stem + # 初始化工作流,其目的是按照工作流和工作配置拼接素材,先生成草稿再导出 + self.workflow = [ + "add_subtitles", + "add_background_video", + "add_statement", + "add_sticker1", + "add_sticker2", + "save", + ] + # 初始化工作配置 + self.configuration = { + "add_subtitles": { + "text": self.materials["subtitles_text"], + "timbre": [ + "zh-CN-XiaoxiaoNeural", + "zh-CN-XiaoyiNeural", + "zh-CN-YunjianNeural", + "zh-CN-YunxiNeural", + "zh-CN-YunxiaNeural", + "zh-CN-YunyangNeural", + ], # 音色 + "style": [ + {"size": 9.0}, + {"size": 10.0}, + {"size": 11.0}, + ], # 字体样式 + "effect": [ + {"effect_id": "7127561998556089631"}, + {"effect_id": "7166467215410187552"}, + {"effect_id": "6896138122774498567"}, + {"effect_id": "7166469374507765031"}, + {"effect_id": "6896137924853763336"}, + {"effect_id": "6896137990788091143"}, + {"effect_id": "7127614731187211551"}, + {"effect_id": "7127823362356743461"}, + {"effect_id": "7127653467555990821"}, + {"effect_id": "7127828216647011592"}, + ], # 花字设置 + }, # 添加字幕工作配置 + "add_background_video": { + "track_name": ["background_video"], + "material_path": self.materials["background_video_material_path"], + "volume": [0.3, 0.4, 0.5], + "clip_settings": [ + None, + { + "transform_x": 0.1, + }, + { + "transform_x": 0.2, + }, + { + "transform_x": -0.1, + }, + { + "transform_x": -0.2, + }, + { + "transform_y": 0.1, + }, + { + "transform_y": 0.2, + }, + { + "transform_y": -0.1, + }, + { + "transform_y": -0.2, + }, + { + "transform_x": 0.1, + "transform_y": 0.1, + }, + { + "transform_x": 0.1, + "transform_y": -0.1, + }, + { + "transform_x": -0.1, + "transform_y": 0.1, + }, + { + "transform_x": -0.1, + "transform_y": -0.1, + }, + { + "transform_x": 0.2, + "transform_y": 0.2, + }, + { + "transform_x": 0.2, + "transform_y": -0.2, + }, + { + "transform_x": -0.2, + "transform_y": 0.2, + }, + { + "transform_x": -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.78, + "transform_y": 0.82, + }, + { + "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, + }, + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": 0.78, + "transform_y": 0.82, + }, + ], + }, # 添加标识工作配置 + "add_statement": { + "track_name": ["statement"], + "text": self.materials["statement_text"], + "style": [ + {"size": 6.0, "align": 1, "vertical": True}, + {"size": 7.0, "align": 1, "vertical": True}, + {"size": 8.0, "align": 1, "vertical": True}, + ], # 文本样式 + "border": [ + {"width": 40.0}, + {"width": 44.0}, + {"width": 50.0}, + {"width": 55.0}, + {"width": 60.0}, + ], # 描边宽度 + "clip_settings": [ + { + "transform_x": -0.80, + }, + { + "transform_x": -0.82, + }, + { + "transform_x": -0.84, + }, + ], # 图像调节设置 + }, # 添加声明工作配置 + "add_sticker1": { + "track_name": ["sticker1"], + "resource_id": [ + "7110124379568098568", + "7019687632804334861", + "6895933678262750478", + "7010558788675652900", + "7026858083393588487", + "7222940306558209336", + "7120543009489341727", + "6939830545673227557", + "6939826722451754271", + "7210221631132749093", + "7138432572488453408", + "7137700067338620192", + "6895924436822674696", + "7134644683506044163", + "7062539853430279437", + ], + "clip_settings": [ + { + "scale_x": 0.75, + "scale_y": 0.75, + "transform_x": -0.75, + "transform_y": 0.75, + }, + { + "scale_x": 0.75, + "scale_y": 0.75, + "transform_y": 0.75, + }, + { + "scale_x": 0.75, + "scale_y": 0.75, + "transform_x": 0.75, + "transform_y": 0.75, + }, + ], # 图像调节设置 + }, # 添加贴纸工作配置1(不包含箭头类) + "add_sticker2": { + "track_name": ["sticker2"], + "resource_id": [ + "7143078914989018379", + "7142870400358255905", + "7185568038027103544", + "7024342011440319781", + "7205042602184363322", + ], + "clip_settings": [ + { + "scale_x": 0.75, + "scale_y": 0.75, + "transform_x": -0.8, + "transform_y": -0.62, + }, + ], # 图像调节设置 + }, # 添加贴纸工作配置2(箭头类) + } + + self.video_width, self.video_height = video_width, video_height + self.video_fps = video_fps + + self.draft_counts = draft_counts + # 初始化批量生成草稿的所有草稿名称,用于批量导出 + self.draft_names = [] + + 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 _generate( + self, + ) -> None: + """ + 按照工作流和工作配置拼接素材,生成草稿 + :return: 无 + """ + for idx in range(self.draft_counts): + # 构建草稿名称 + draft_name = self.project_name + f"{idx + 1:03d}" + print(f"正在合成短视频 {draft_name}, {idx + 1}/{self.draft_counts}...") + + # 实例化 JianYingDraft + draft = JianYingDraft( + 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": + print("-> 正在添加字幕...", end="") + draft.add_subtitles(**self._random(work=work)) + print("已完成") + # 添加背景视频 + case "add_background_video": + print("-> 正在添加背景视频...", end="") + draft.add_video_segment(**self._random(work=work)) + print("已完成") + # 添加标识 + case "add_logo": + print("-> 正在添加标识...", end="") + draft.add_video_segment(**self._random(work=work)) + print("已完成") + # 添加声明 + case "add_statement": + print("-> 正在添加声明...", end="") + draft.add_text_segment(**self._random(work=work)) + print("已完成") + # 添加贴纸 + case _ if work.startswith("add_sticker"): + print("-> 正在添加贴纸...", end="") + draft.add_sticker(**self._random(work=work)) + print("已完成") + # 将草稿保存至剪映草稿文件夹内 + case "save": + print("-> 正在将草稿保存至剪映草稿文件夹内...", end="") + draft.save() + print("已完成") + + self.draft_names.append(draft_name) + + print("已完成") + print() + + # 按照草稿名称倒叙排序 + self.draft_names.sort(reverse=True) + + def _random( + self, + work: str, + ) -> Dict[str, Any]: + """ + 随机化工作配置项 + :param work: 工作,包括添加字幕、添加背景视频、添加标识、添加声明和添加贴纸 + :return: 工作配置 + """ + return { + key: random.choice(value) for key, value in self.configuration[work].items() + } + + def export(self): + """ + 导出草稿 + """ + # 按照工作流和工作配置拼接素材,生成草稿 + self._generate() + + # 启动剪映专业版进程 + self._start_process() + time.sleep(10) + + # 初始化剪映控制器 + jianying_controller = pyJianYingDraft.JianyingController() + + for draft_name in self.draft_names: + print(f"正在导出 {draft_name}...") + if (self.exports_folder_path / f"{draft_name}.mp4").is_file(): + print("存在相同名称的草稿,跳过") + continue + jianying_controller.export_draft( + draft_name=draft_name, output_path=self.exports_folder_path.as_posix() + ) + print("已完成") + print() + + # 关闭剪映专业版进程 + self._close_process() + + def _start_process(self, timeout: int = 60) -> None: + """ + 启动剪映专业版进程 + :param timeout: 最大等待时间(单位为秒),默认为 60 + :return: 无 + """ + try: + # 关闭剪映专业版进程 + self._close_process() + + # 非堵塞方法 + self.jianying_process = subprocess.Popen( + args=self.jianying_program_path.as_posix(), + shell=True, # 适配 Windows路径中的空格 + stdout=subprocess.DEVNULL, # 重定向 + stderr=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NEW_CONSOLE, + ) + + start_time = time.time() + while time.time() - start_time < timeout: + # 定位剪映执行程序窗口 + if self._locate_window() is not None: + print(f"已启动剪映专业版进程,PID {self.jianying_process.pid}") + return + time.sleep(2) + raise RuntimeError("启动超时") + + except Exception as exception: + raise RuntimeError(f"启动剪映专业版进程发生异常:{str(exception)}") + + def _close_process(self, timeout: int = 60) -> None: + """ + 关闭剪映专业版进程 + :param timeout: 最大等待时间(单位为秒),默认为 60 + :return: 无 + """ + try: + # 定位剪映执行程序窗口 + window_handle = self._locate_window() + if window_handle is not None: + # 请求关闭剪映执行程序窗口 + win32gui.SendMessage(window_handle, win32con.WM_CLOSE, 0, 0) + + start_time = time.time() + while time.time() - start_time < timeout: + if not win32gui.IsWindow(window_handle): + print("已关闭剪映专业版进程") + return + else: + time.sleep(2) + raise RuntimeError("关闭超时") + + except Exception as exception: + raise RuntimeError(f"关闭剪映专业版进程发生异常:{str(exception)}") + + @staticmethod + def _locate_window() -> Optional[int]: + """ + 定位剪映执行程序窗口 + :return: 剪映执行程序窗口句柄 + """ + window_handle = None + + def callback(handle, _): + """ + 遍历所有窗口的回调函数 + """ + # 初始化窗口句柄 + nonlocal window_handle + # 获取窗口标题 + window_text = win32gui.GetWindowText(handle) + # 检查窗口是否可见且窗口标题为剪映专业版 + if win32gui.IsWindowVisible(handle) and window_text == "剪映专业版": + window_handle = handle + return False + return True + + # 遍历所有顶层窗口 + win32gui.EnumWindows(callback, None) + return window_handle diff --git a/短视频合成自动化/main.py b/短视频合成自动化/main.py new file mode 100644 index 0000000..dfe3a5c --- /dev/null +++ b/短视频合成自动化/main.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +""" +短视频合成自动化 +""" + +from export import JianYingExport + +if __name__ == "__main__": + # 实例化 JianYingExport + jianying_export = JianYingExport( + materials_folder_path=r"E:\jianying\materials\260104", + draft_counts=1, + ) + + # 导出草稿 + jianying_export.export()