From 43c8dd3a7cf0006e0031a1ff4c33769b2bedec50 Mon Sep 17 00:00:00 2001 From: liubiren Date: Wed, 11 Mar 2026 17:55:48 +0800 Subject: [PATCH] 1 --- 短视频AI生成/main.py | 6 +- 短视频合成自动化/caches.db | Bin 16384 -> 675840 bytes 短视频合成自动化/caches.py | 115 ++++ 短视频合成自动化/{draft.py => drafts.py} | 282 +++++---- 短视频合成自动化/edgetts.py | 11 +- 短视频合成自动化/export.py | 693 ----------------------- 短视频合成自动化/jiangying_manager.py | 570 +++++++++++++++++++ 短视频合成自动化/main.py | 14 +- 8 files changed, 841 insertions(+), 850 deletions(-) create mode 100644 短视频合成自动化/caches.py rename 短视频合成自动化/{draft.py => drafts.py} (63%) delete mode 100644 短视频合成自动化/export.py create mode 100644 短视频合成自动化/jiangying_manager.py diff --git a/短视频AI生成/main.py b/短视频AI生成/main.py index db588c7..6006d5c 100644 --- a/短视频AI生成/main.py +++ b/短视频AI生成/main.py @@ -38,10 +38,10 @@ ark_client = Ark( request_client = Request() -def get_brand_words() -> List[str]: +def get_product_image() -> List[str]: """ - 获取品牌词 - :return: 品牌词 + 获取产品图片 + :return: 产品图片 """ try: with open( diff --git a/短视频合成自动化/caches.db b/短视频合成自动化/caches.db index 34265e12cdacf8c0bbd9405885e67a647958f70e..437603ba2bed64fa5b0e82d3a37a288948b87f93 100644 GIT binary patch literal 675840 zcmeFa37j28xi;QswlmwA!@kLq3}G@+NUEx9Yqr?k)y*P1VPC>zAqiw7nFK<}GBb!A z*%1U;L~#LSa{&bOMo{D`qSveUA|U!HkQfmC6vgX)zo+_SI)sap|Npz!&iQ@aQ7U

ul$gO3r)7jdPsRcOA$16#g6i)r3E^j1u_0 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 delta 226 zcmZozpxMyCI6;byfr){Efn}nCod}Sn*CoQs|AT> Optional[Dict[str, Any]]: + """ + 查询并返回单条缓存 + :param draft_name: 草稿名称 + :return: 缓存 + """ + try: + with self: + result = self.query_one( + sql=""" + SELECT configurations + FROM caches + WHERE draft_name = ? AND timestamp >= ? + """, + parameters=(draft_name, time.time() - self.cache_ttl), + ) + return None if result is None else json.loads(result["configurations"]) + except Exception as exception: + raise RuntimeError( + f"查询并获取单条缓存发生异常:{str(exception)}" + ) from exception + + def update( + self, draft_name: str, configurations: Dict[str, Dict[str, Any]] + ) -> Optional[bool]: + """ + 新增或更新单条缓存 + :param draft_name: 草稿名称 + :param configurations: 节点配置 + :return: 成功返回True,失败返回False + """ + try: + with self: + return self.execute( + sql=""" + INSERT OR REPLACE INTO caches (draft_name, configurations, timestamp) VALUES (?, ?, ?) + """, + parameters=( + draft_name, + json.dumps( + obj=configurations, + cls=self.JSONEncoder, + sort_keys=True, + ensure_ascii=False, + ), + time.time(), + ), + ) + except Exception as exception: + raise RuntimeError("新增或更新缓存发生异常") from exception diff --git a/短视频合成自动化/draft.py b/短视频合成自动化/drafts.py similarity index 63% rename from 短视频合成自动化/draft.py rename to 短视频合成自动化/drafts.py index 128f2fe..dd7af32 100644 --- a/短视频合成自动化/draft.py +++ b/短视频合成自动化/drafts.py @@ -1,62 +1,85 @@ # -*- coding: utf-8 -*- """ -生成草稿模块 +剪映草稿模块 """ +# 列举导入模块 from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -import pyJianYingDraft +from pyJianYingDraft import ( + AudioMaterial, + AudioSegment, + ClipSettings, + ClipSettings, + DraftFolder, + FontType, + KeyframeProperty, + StickerSegment, + TextBackground, + TextBorder, + TextSegment, + TextStyle, + TrackType, + VideoMaterial, + VideoSegment, + trange, +) +from pyJianYingDraft.time_util import tim + from edgetts import EdgeTTS -class JianYingDraft: +class Drafts: """ - 封装 pyJianYing中生成草稿的相关功能,支持: - 1、向指定文本轨道添加文本片段 - 2、向指定音频轨道添加音频片段 - 3、向指定视频轨道添加视频或图片片段 - 4、向指定贴纸轨道添加贴纸片段 - 5、根据文本逐段合成语音,生成文本和语音字幕 - 6、将草稿保存至剪映草稿文件夹内 + 剪映草稿,支持: + 1 添加文本片段 + 2 添加音频片段 + 3 添加视频或图片片段 + 4 添加贴纸片段 + 5 生成字幕 + 6 保存剪映草稿 """ def __init__( self, - drafts_folder: pyJianYingDraft.DraftFolder, - draft_name: str, materials_folder_path: Path, - allow_replace: bool = True, + drafts_folder: DraftFolder, + draft_name: str, video_width: int = 1080, video_height: int = 1920, video_fps: int = 30, + video_duration: int = 0, ): """ 初始化 + :param materials_folder_path: 素材文件夹路径 :param drafts_folder: 剪映草稿文件夹管理器 :param draft_name: 草稿名称 - :param allow_replace: 是否允许覆盖同名草稿(若不允许覆盖同名草稿需初始化剪映草稿文件夹) - :param video_width: 视频宽度,默认为 1080像素 - :param video_height: 视频高度,默认为 1920像素 + :param video_width: 视频宽度(单位为像素),默认为 1080 + :param video_height: 视频高度(单位为像素),默认为 1920 :param video_fps: 视频帧率(单位为帧/秒),默认为 30 - :param materials_folder_path: 素材文件夹路径 + :param video_duration: 视频持续时长(单位为微秒),默认为 0 """ try: - # 新建草稿 + # 初始化素材文件夹路径 + self.materials_folder_path = materials_folder_path + if not self.materials_folder_path.exists(): + raise FileNotFoundError(f"素材文件夹路径不存在") + + # 创建剪映草稿 self.draft = drafts_folder.create_draft( draft_name=draft_name, - allow_replace=allow_replace, + allow_replace=True, # 允许覆盖与 draft_name 重名的剪映草稿 width=video_width, height=video_height, fps=video_fps, ) - # 草稿持续时长(单位为毫秒) - self.draft_duration = 0 - - self.materials_folder_path = materials_folder_path + # 初始化视频持续时长 + self.video_duration = video_duration except Exception as exception: - raise RuntimeError(f"发生异常:{str(exception)}") from exception + raise RuntimeError(f"创建剪映草稿发生异常:{str(exception)}") from exception def add_text_segment( self, @@ -74,7 +97,7 @@ class JianYingDraft: animation: Optional[Dict[str, Any]] = None, ) -> None: """ - 向指定文本轨道添加文本片段 + 添加文本片段 :param track_name: 轨道名称 :param add_track: 添加文本轨道,默认为是 :param text: 文本 @@ -93,44 +116,36 @@ class JianYingDraft: if add_track: # 添加文本轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.text, + track_type=TrackType.text, track_name=track_name, ) # 构建文本片段 - text_segment = pyJianYingDraft.TextSegment( + text_segment = TextSegment( text=text.replace("\\n", "\n"), - timerange=pyJianYingDraft.trange( - *(timerange if timerange else (0, self.draft_duration)) - ), - 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 + timerange=trange( + *(timerange if timerange else (0, self.video_duration)) ), + font=FontType(font) if font else None, + style=TextStyle(**style) if style else None, + border=TextBorder(**border) if border else None, + background=(TextBackground(**background) if background else None), clip_settings=( - pyJianYingDraft.ClipSettings(**clip_settings) - if clip_settings - else None + 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(segment=text_segment, track_name=track_name) - except Exception as exception: raise RuntimeError(str(exception)) from exception @@ -146,7 +161,7 @@ class JianYingDraft: fade: Optional[Tuple[str, str]] = None, ) -> None: """ - 向指定音频轨道添加音频片段 + 添加音频片段 :param track_name: 轨道名称 :param add_track: 添加音频轨道,默认为是 :param material_path: 音频素材路径 @@ -159,40 +174,35 @@ class JianYingDraft: """ try: # 音频素材 - audio_material = pyJianYingDraft.AudioMaterial( - path=material_path.as_posix() - ) + audio_material = AudioMaterial(path=material_path.as_posix()) # 音频素材的持续时长 audio_material_duration = audio_material.duration - # 若草稿持续时长为0,则将第一个音频素材持续时长作为草稿持续时长 - - # 获取持续时间 - target_duration = pyJianYingDraft.time_util.tim( - (target_timerange if target_timerange else (0, self.draft_duration))[1] + # 目标持续时间 + target_duration = tim( + (target_timerange if target_timerange else (0, self.video_duration))[1] ) if add_track: # 添加音频轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.audio, + track_type=TrackType.audio, track_name=track_name, ) - duration = 0 # 已添加音频素材的持续时长 - while duration < target_duration: + cumulative_duration = 0 # 累计持续时长 + while cumulative_duration < target_duration: # 构建音频片段 - audio_segment = pyJianYingDraft.AudioSegment( + audio_segment = AudioSegment( material=audio_material, - target_timerange=pyJianYingDraft.trange( - start=duration, + target_timerange=trange( + start=cumulative_duration, duration=min( - (target_duration - duration), audio_material_duration + (target_duration - cumulative_duration), + audio_material_duration, ), ), source_timerange=( - pyJianYingDraft.trange(*source_timerange) - if source_timerange - else None + trange(*source_timerange) if source_timerange else None ), speed=speed, volume=volume, @@ -200,12 +210,10 @@ class JianYingDraft: # 添加淡入淡出 if fade: audio_segment.add_fade(*fade) - - # 向指定音频轨道添加音频片段 + # 向指定轨道添加音频片段 self.draft.add_segment(segment=audio_segment, track_name=track_name) - duration += audio_material_duration - + cumulative_duration += audio_material_duration except Exception as exception: raise RuntimeError(str(exception)) from exception @@ -213,20 +221,20 @@ class JianYingDraft: self, track_name: str, material_path: Path, - target_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None, - source_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None, + target_timerange: Optional[Tuple[int, Optional[int]]] = None, + source_timerange: Optional[Tuple[int, int]] = None, speed: float = 1.0, volume: float = 1.0, clip_settings: Optional[Dict[str, Any]] = None, keyframes: Optional[ - List[Tuple[pyJianYingDraft.KeyframeProperty, Union[str, int], float]] + List[Tuple[KeyframeProperty, Union[str, int], float]] ] = None, animation: Optional[Dict[str, Any]] = None, transition: Optional[Dict[str, Any]] = None, background_filling: Optional[Dict[str, Any]] = None, ) -> None: """ - 向指定视频轨道添加视频或图片片段 + 添加视频或图片片段 :param track_name: 轨道名称 :param material_path: 视频或图片素材路径 :param target_timerange: 视频或图片素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长 @@ -242,74 +250,65 @@ class JianYingDraft: """ try: # 视频素材 - video_material = pyJianYingDraft.VideoMaterial( - path=material_path.as_posix() - ) + video_material = VideoMaterial(path=material_path.as_posix()) # 视频素材的持续时长 video_material_duration = video_material.duration - # 若草稿持续时长为0,则将第一个视频素材持续时长作为草稿持续时长 - relative_index = 0 - if not self.draft_duration: - relative_index = 1 # 视频轨道相对索引 - self.draft_duration = video_material_duration - # 获取持续时间 - target_duration = pyJianYingDraft.time_util.tim( - (target_timerange if target_timerange else (0, self.draft_duration))[1] - ) + # 目标持续时间 + target_duration = ( + tim(target_duration) + if (target_timerange and (target_duration := target_timerange[1])) + else ( + video_material_duration if target_timerange else self.video_duration + ) + ) # 若视频或图片素材在轨道上的范围为空则将视频素材持续时长作为目标持续时长,若视频或图片素材在轨道上的范围中持续时长为空则将视频素材持续时长作为目标持续时长 # 添加视频轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.video, + track_type=TrackType.video, track_name=track_name, - relative_index=relative_index, ) - duration = 0 # 已添加视频素材的持续时长 - while duration < target_duration: + cumulative_duration = 0 # 累计持续时长 + while cumulative_duration < target_duration: # 构建视频或图片片段 - video_segment = pyJianYingDraft.VideoSegment( + video_segment = VideoSegment( material=video_material, - target_timerange=pyJianYingDraft.trange( - start=duration, + target_timerange=trange( + start=cumulative_duration + + (target_timerange[0] if target_timerange else 0), duration=min( - (target_duration - duration), video_material_duration + (target_duration - cumulative_duration), + video_material_duration, ), ), source_timerange=( - pyJianYingDraft.trange(*source_timerange) - if source_timerange - else None + trange(*source_timerange) if source_timerange else None ), speed=speed, volume=volume, clip_settings=( - pyJianYingDraft.ClipSettings(**clip_settings) - if clip_settings - else None + ClipSettings(**clip_settings) if clip_settings else None ), ) # 添加关键帧 if keyframes: - for _property, offset, value in keyframes: - video_segment.add_keyframe(_property, offset, value) - + for keyframe_property, keyframe_offset, keyframe_value in keyframes: + video_segment.add_keyframe( + keyframe_property, keyframe_offset, keyframe_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 - + cumulative_duration += video_material_duration except Exception as exception: raise RuntimeError(str(exception)) from exception @@ -331,25 +330,23 @@ class JianYingDraft: try: # 添加贴纸轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.sticker, + track_type=TrackType.sticker, track_name=track_name, ) # 构建贴纸 # 将贴纸保存为我的预设后在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取 - sticker_segment = pyJianYingDraft.StickerSegment( + sticker_segment = StickerSegment( resource_id=resource_id, - target_timerange=pyJianYingDraft.trange( + target_timerange=trange( *( target_timerange if target_timerange - else (0, self.draft_duration) + else (0, self.video_duration) ) ), clip_settings=( - pyJianYingDraft.ClipSettings(**clip_settings) - if clip_settings - else None + ClipSettings(**clip_settings) if clip_settings else None ), ) @@ -359,10 +356,10 @@ class JianYingDraft: except Exception as exception: raise RuntimeError(str(exception)) from exception - def add_subtitles( + def generate_subtitle( self, - text: str, - timbre: str = "女声-晓晓", + texts: str, + timbre: str = "zh-CN-XiaoxiaoNeural", rate: str = "+25%", volume: str = "+0%", font: Optional[str] = None, @@ -371,12 +368,12 @@ class JianYingDraft: effect: Optional[Dict[str, Any]] = None, ): """ - 添加字幕 - :param text: 文本 + 根据字幕文本合成字幕音频并生成字幕 + :param texts: 字幕文本 :param timbre: 声音音色,默认为女声-晓晓 :param rate: 语速,默认为 +25% :param volume: 音量,默认为 +0% - :param font: 字体,默认为系统 + :param font: 字体,默认为系统默认字体 :param style: 文本样式,默认为字号 12,对齐方式 水平居中 :param clip_settings: 图像调节设置,默认为移动至 (0, -0.5) :param effect: 花字设置,默认为无 @@ -384,33 +381,34 @@ class JianYingDraft: """ # 添加文本轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.text, - track_name=(text_track_name := "subtitles(text)"), + track_type=TrackType.text, + track_name=(text_track_name := "subtitle(text)"), ) # 添加音频轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.audio, - track_name=(audio_track_name := "subtitles(audio)"), + track_type=TrackType.audio, + track_name=(audio_track_name := "subtitle(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) + # 构建字幕音频文件夹路径 + subtitle_folder_path = self.materials_folder_path / "字幕音频" + subtitle_folder_path.mkdir(exist_ok=True) - start = 0 - for paragraph in text.split(","): - # 根据文本合成语音并将语音文件保存至指定文件夹内 - file_path, duration = edge_tts.synthetize( - text=paragraph, timbre=timbre, rate=rate, volume=volume + # 实例化 EdgeTTS + edge_tts = EdgeTTS(folder_path=subtitle_folder_path) + + cumulative_duration = 0 # 累计持续时长 + for text in texts.split(","): + # 根据字幕文本片段合成语音并将语音文件保存至指定文件夹内 + subtitle_audio_path, duration = edge_tts.synthetize( + text=text, timbre=timbre, rate=rate, volume=volume ) # 向指定文本轨道添加文本片段 self.add_text_segment( track_name=text_track_name, - add_track=False, - text=paragraph, - timerange=(start, duration), + add_track=False, # 不添加文本轨道 + text=text, + timerange=(cumulative_duration, duration), font=font, style={ "size": 12.0, @@ -426,20 +424,18 @@ class JianYingDraft: # 向指定音频轨道添加音频片段 self.add_audio_segment( track_name=audio_track_name, - add_track=False, - material_path=file_path, - target_timerange=(start, duration), - volume=1.5, + add_track=False, # 不添加音频轨道 + material_path=subtitle_audio_path, + target_timerange=(cumulative_duration, duration), ) - start += duration + cumulative_duration += duration - # 更新草稿持续时长 - self.draft_duration = start + # 以累计持续时长作为视频持续时长 + self.video_duration = cumulative_duration def save(self) -> None: """将草稿保存至剪映草稿文件夹内""" try: self.draft.save() - except Exception as exception: raise RuntimeError(str(exception)) from exception diff --git a/短视频合成自动化/edgetts.py b/短视频合成自动化/edgetts.py index d60540b..9bee8a4 100644 --- a/短视频合成自动化/edgetts.py +++ b/短视频合成自动化/edgetts.py @@ -3,6 +3,7 @@ 合成语音模块 """ +# 列举导入模块 import asyncio from hashlib import md5 from pathlib import Path @@ -54,17 +55,19 @@ class EdgeTTS: .hexdigest() .upper()}.mp3" # 构造语音文件路径 - file_path = self.folder_path / file_name + audio_path = self.folder_path / file_name communicator = edge_tts.Communicate( text=text.replace("\n", ""), voice=timbre, rate=rate, volume=volume, ) - await communicator.save(file_path.as_posix()) + await communicator.save(audio_path.as_posix()) # 持续时长(单位为微秒) - duration = int(round(MP3(file_path.as_posix()).info.length * 1_000_000)) - return file_path, duration + duration = int( + round(MP3(audio_path.as_posix()).info.length * 1_000_000) + ) + return audio_path, duration return asyncio.run(_async_synthetize()) except Exception as exception: diff --git a/短视频合成自动化/export.py b/短视频合成自动化/export.py deleted file mode 100644 index 0c28031..0000000 --- a/短视频合成自动化/export.py +++ /dev/null @@ -1,693 +0,0 @@ -# -*- coding: utf-8 -*- -""" -导出草稿模块 -""" - -import hashlib -import json -from pathlib import Path -from pathlib import WindowsPath -import random -import re -import shutil -import sys -import time -from typing import Any, Dict, List, Optional - -import pyJianYingDraft - -from draft import JianYingDraft -from utils.sqlite import SQLite - -sys.path.append(Path(__file__).parent.parent.as_posix()) - - -# 自定义JSON编码器 -class JSONEncoder(json.JSONEncoder): - def default(self, o): - # 若为WindowsPath对象则转为字符串路径 - if isinstance(o, WindowsPath): - return o.as_posix() - return super().default(o) - - -class Caches(SQLite): - """ - 缓存客户端,支持: - query:查询并返回单条缓存 - update:新增或更新单条缓存 - """ - - def __init__(self, cache_ttl: int = 30 * 86400): - """ - 初始化 - :param cache_ttl: 缓存生存时间,单位为秒,默认为30天 - """ - # 初始化 - super().__init__(database=Path(__file__).parent.resolve() / "caches.db") - self.cache_ttl = cache_ttl - - # 初始化缓存表(不清理过期缓存) - try: - with self: - self.execute( - sql=""" - CREATE TABLE IF NOT EXISTS caches - ( - --草稿名称 - draft_name TEXT PRIMARY KEY, - --工作流配置 - workflow_configurations TEXT NOT NULL, - --创建时间戳 - timestamp REAL NOT NULL - ) - """ - ) - self.execute( - sql=""" - CREATE INDEX IF NOT EXISTS idx_timestamp ON caches(timestamp) - """ - ) - except Exception as exception: - raise RuntimeError(f"初始化缓存发生异常:{str(exception)}") from exception - - def query(self, draft_name: str) -> Optional[Dict[str, Any]]: - """ - 查询并返回单条缓存 - :param draft_name: 草稿名称 - :return: 缓存 - """ - try: - with self: - result = self.query_one( - sql=""" - SELECT workflow_configurations - FROM caches - WHERE draft_name = ? AND timestamp >= ? - """, - parameters=(draft_name, time.time() - self.cache_ttl), - ) - return ( - None - if result is None - else json.loads(result["workflow_configurations"]) - ) - except Exception as exception: - raise RuntimeError( - f"查询并获取单条缓存发生异常:{str(exception)}" - ) from exception - - def update( - self, draft_name: str, workflow_configurations: List[Dict[str, Any]] - ) -> Optional[bool]: - """ - 新增或更新单条缓存 - :param draft_name: 草稿名称 - :param workflow_configurations: 工作流配置 - :return: 成功返回True,失败返回False - """ - try: - with self: - return self.execute( - sql=""" - INSERT OR REPLACE INTO caches (draft_name, workflow_configurations, timestamp) VALUES (?, ?, ?) - """, - parameters=( - draft_name, - json.dumps( - obj=workflow_configurations, - cls=JSONEncoder, - sort_keys=True, - ensure_ascii=False, - ), - time.time(), - ), - ) - except Exception as exception: - raise RuntimeError("新增或更新缓存发生异常") from exception - - -class JianYingExport: - """ - 封装 pyJianYingDraft.JianyingController库,支持: - 1、初始化素材文件夹内所有素材 - 2、初始化工作流和工作配置 - 3、导出草稿 - """ - - def __init__( - self, - materials_folder_path: str, - drafts_folder_path: str = r"E:\JianYingPro Drafts", - video_width: int = 1080, - video_height: int = 1920, - video_fps: int = 30, - ): - """ - 初始化 - :param drafts_folder_path: 剪映草稿文件夹路径 - :param materials_folder_path: 素材文件夹路径 - :param video_width: 视频宽度,默认为 1080像素 - :param video_height: 视频高度,默认为 1920像素 - :param video_fps: 视频帧率(单位为帧/秒),默认为 30 - """ - try: - # 初始化剪映草稿文件夹路径 - self.drafts_folder_path = Path(drafts_folder_path) - if not self.drafts_folder_path.exists(): - raise RuntimeError("剪映草稿文件夹路径不存在") - - # 初始化剪映草稿文件夹管理器 - self.drafts_folder = pyJianYingDraft.DraftFolder( - folder_path=self.drafts_folder_path.as_posix() - ) - - # 初始化素材文件夹路径 - 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") - ) - # 若导出文件夹存在则删除,再创建导出文件夹 - if self.exports_folder_path.exists(): - shutil.rmtree(self.exports_folder_path) - self.exports_folder_path.mkdir() - - self.materials = {} - # 初始化素材文件夹内所有素材 - self._init_materials() - - # 初始化所有工作流 - self.workflows = { - "默认": [ - "add_subtitles", - "add_background_video", - "add_statement", - "add_sticker1", - "add_sticker2", - ], # 默认工作流,先根据脚本合成音频,再叠加背景视频、声明视频、贴纸1视频和贴纸2视频 - "淘宝闪购": [ - "add_subtitles_video", # 以此作为草稿持续时长 - "add_background_video", - "add_background_audio", - "add_statement_video", - ], # 适用于淘宝闪购、存量抽手机 - "视频号": [ - "add_subtitles_video", # 以此作为草稿持续时长 - "add_background_video", - "add_background_audio", - "add_logo_video", - "add_statement_video", - ], # 适用于视频号 - } - - # 初始化所有节点配置 - self.configurations = { - "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}, - ], # 字体样式 - "keywords": [ - "瑞幸", - ], # 关键词 - "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_subtitles_video": { - "material_path": self.materials["subtitles_video_material_path"], - "volume": [1.0], # 播放音量 - "clip_settings": [ - None, - ], # 图像调节设置 - }, # 添加字幕视频工作配置 - "add_background_video": { - "material_path": self.materials["background_video_material_path"], - "volume": [1.0], # 播放音量 - "clip_settings": [ - { - "scale_x": 1.5, - "scale_y": 1.5, - }, - ], # 图像调节设置 - }, # 添加背景视频工作配置 - "add_background_audio": { - "material_path": self.materials["background_audio_material_path"], - "volume": [0.6], # 播放音量 - }, # 添加背景音频工作配置 - "add_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_logo_video": { - "material_path": self.materials["logo_video_material_path"], - "volume": [1.0], # 播放音量 - "clip_settings": [ - None, - ], # 图像调节设置 - }, # 添加标识视频工作配置 - "add_statement": { - "text": self.materials["statement_text"], - "style": [ - {"size": 5.0, "align": 1, "vertical": True}, - {"size": 6.0, "align": 1, "vertical": True}, - {"size": 7.0, "align": 1, "vertical": True}, - ], # 文本样式 - "border": [ - {"width": 35.0}, - {"width": 40.0}, - {"width": 45.0}, - {"width": 50.0}, - {"width": 55.0}, - ], # 描边宽度 - "clip_settings": [ - { - "transform_x": -0.80, - }, - { - "transform_x": -0.82, - }, - { - "transform_x": -0.84, - }, - ], # 图像调节设置 - }, # 添加声明工作配置 - "add_statement_video": { - "material_path": self.materials["statement_video_material_path"], - "volume": [1.0], # 播放音量 - "clip_settings": [ - None, - ], # 图像调节设置 - }, # 添加声明视频工作配置 - "add_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": { - "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.caches = Caches() - - except Exception as exception: - raise RuntimeError(f"发生异常:{str(exception)}") from 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: - self.materials["subtitles_text"] = [] - - # 字幕(视频) - subtitles_video_folder_path = self.materials_folder_path / "字幕视频" - if ( - subtitles_video_folder_path.exists() - and subtitles_video_folder_path.is_dir() - ): - self.materials["subtitles_video_material_path"] = [ - file_path - for file_path in subtitles_video_folder_path.rglob("*.mov") - if file_path.is_file() - ] - else: - self.materials["subtitles_video_material_path"] = [] - - # 背景视频 - background_video_folder_path = self.materials_folder_path / "背景视频" - if ( - background_video_folder_path.exists() - and background_video_folder_path.is_dir() - ): - self.materials["background_video_material_path"] = [ - file_path - for file_path in background_video_folder_path.rglob("*.mp4") - if file_path.is_file() - ] - else: - self.materials["background_video_material_path"] = [] - - # 背景音频 - background_audio_folder_path = self.materials_folder_path / "背景音频" - if ( - background_audio_folder_path.exists() - and background_audio_folder_path.is_dir() - ): - self.materials["background_audio_material_path"] = [ - file_path - for file_path in background_audio_folder_path.rglob("*.mp3") - if file_path.is_file() - ] - else: - self.materials["background_audio_material_path"] = [] - - # 标识 - logo_path = self.materials_folder_path / "标识.png" - if logo_path.exists() and logo_path.is_file(): - self.materials["logo_material_path"] = [logo_path] # 有且只有一张标识 - else: - self.materials["logo_material_path"] = [] - - # 标识视频 - logo_video_folder_path = self.materials_folder_path / "标识视频" - if logo_video_folder_path.exists() and logo_video_folder_path.is_dir(): - self.materials["logo_video_material_path"] = [ - file_path - for file_path in logo_video_folder_path.rglob("*.mov") - if file_path.is_file() - ] - else: - self.materials["logo_video_material_path"] = [] - - # 声明文本 - 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: - self.materials["statement_text"] = [] - - # 声明视频 - statement_video_folder_path = self.materials_folder_path / "声明视频" - if ( - statement_video_folder_path.exists() - and statement_video_folder_path.is_dir() - ): - self.materials["statement_video_material_path"] = [ - file_path - for file_path in statement_video_folder_path.rglob("*.mov") - if file_path.is_file() - ] - else: - self.materials["statement_video_material_path"] = [] - - def export_videos(self, workflow_name: str, draft_counts: int): - """ - 导出视频 - :param workflow_name: 工作流名称 - :param draft_counts: 每批次导出草稿数 - """ - if workflow_name not in self.workflows: - raise RuntimeError(f"该工作流 {workflow_name} 未配置") - workflow = self.workflows[workflow_name] - - # 若工作流包含添加背景音频,则在添加背景视频节点配置的播放音量设置为0 - if "add_background_audio" in workflow: - self.configurations["add_background_video"]["volume"] = [0.0] - - # 批量生成草稿 - self._batch_generate_drafts( - workflow_name=workflow_name, - draft_counts=draft_counts, - ) - - def _batch_generate_drafts( - self, - workflow_name: str, - draft_counts: int, - ) -> None: - """ - 批量生成草稿 - :param workflow_name: 工作流名称 - :param draft_counts: 草稿数 - :return: 无 - """ - draft_index = 1 # 草稿索引 - while True: - # 获取工作流配置 - workflow_configurations = self._get_workflow_configurations( - workflow_name=workflow_name - ) - - # 根据工作流配置生成草稿名称 - draft_name = self._generate_draft_name( - workflow_configurations=workflow_configurations, - ) - # 若已缓存则跳过 - if self.caches.query(draft_name=draft_name): - continue - - print(f"正在生成草稿 {draft_name}...") - - # 实例化 JianYingDraft - draft = JianYingDraft( - drafts_folder=self.drafts_folder, - 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 node in workflow_configurations: - match node["node_name"]: - # 添加字幕 - case "add_subtitles": - print("-> 正在添加字幕...", end="") - draft.add_subtitles(**node["configurations"]) - print("已完成") - # 添加字幕视频 - case "add_subtitles_video": - print("-> 正在添加字幕视频...", end="") - draft.add_video_segment(**node["configurations"]) - print("已完成") - # 添加背景视频 - case "add_background_video": - print("-> 正在添加背景视频...", end="") - draft.add_video_segment(**node["configurations"]) - print("已完成") - # 添加背景音频 - case "add_background_audio": - print("-> 正在添加背景音频...", end="") - draft.add_audio_segment(**node["configurations"]) - print("已完成") - # 添加标识 - case "add_logo": - print("-> 正在添加标识...", end="") - draft.add_video_segment(**node["configurations"]) - print("已完成") - # 添加标识视频 - case "add_logo_video": - print("-> 正在添加标识视频...", end="") - draft.add_video_segment(**node["configurations"]) - print("已完成") - # 添加声明文本 - case "add_statement": - print("-> 正在添加声明...", end="") - draft.add_text_segment(**node["configurations"]) - print("已完成") - # 添加声明视频 - case "add_statement_video": - print("-> 正在添加声明视频...", end="") - draft.add_video_segment(**node["configurations"]) - print("已完成") - # 添加贴纸 - case _ if node["node_name"].startswith("add_sticker"): - print("-> 正在添加贴纸...", end="") - draft.add_sticker(**node["configurations"]) - print("已完成") - # 将草稿保存至剪映草稿文件夹内 - case "save": - print("-> 正在将草稿保存至剪映草稿文件夹内...", end="") - draft.save() - print("已完成") - - # 缓存 - self.caches.update( - draft_name=draft_name, - workflow_configurations=workflow_configurations, - ) - - print("已完成") - print() - - draft_index += 1 - if draft_index > draft_counts: - break - - def _get_workflow_configurations( - self, - workflow_name: str, - ) -> List[Dict[str, Any]]: - """ - 获取工作流配置 - :param workflow_name: 工作流名称 - :return: 工作流配置 - """ - # 初始化工作流配置 - workflow_configurations = [] - for node_name in self.workflows[workflow_name]: - # 根据节点名称获取节点配置 - configurations = { - key: random.choice(value) - for key, value in self.configurations[node_name].items() - } - # 若非添加字幕则在工作流配置添加轨道名称 - if node_name not in ["add_subtitles"]: - configurations["track_name"] = ( - matched.group("track_name") - if ( - matched := re.match( - pattern=r"^.*?_(?P.*)$", - string=node_name, - ) - ) - else node_name - ) - - workflow_configurations.append( - { - "node_name": node_name, - "configurations": configurations, - } - ) - - # 添加保存节点 - workflow_configurations.append( - { - "node_name": "save", - "configurations": {}, - } - ) - return workflow_configurations - - def _generate_draft_name( - self, - workflow_configurations: List[Dict[str, Any]], - ) -> str: - """ - 根据工作流配置生成草稿名称 - :param workflow_configurations: 工作流配置 - :return: 草稿名称 - """ - return ( - hashlib.md5( - json.dumps( - obj=workflow_configurations, - cls=JSONEncoder, - sort_keys=True, - ensure_ascii=False, - ).encode("utf-8") - ) # 将工作流配置序列化 - .hexdigest() - .upper() # MD5哈希值的大写十六进制作为草稿名称 - ) diff --git a/短视频合成自动化/jiangying_manager.py b/短视频合成自动化/jiangying_manager.py new file mode 100644 index 0000000..9e8db27 --- /dev/null +++ b/短视频合成自动化/jiangying_manager.py @@ -0,0 +1,570 @@ +# -*- coding: utf-8 -*- +""" +剪映草稿管理器模块 +""" + +# 列举导入模块 +import hashlib +import json +from pathlib import Path +import random +import re +import shutil +import sys +from typing import Any, Dict, List + +from pyJianYingDraft import DraftFolder, VideoMaterial + +from caches import Caches +from drafts import Drafts + +sys.path.append(Path(__file__).parent.parent.as_posix()) + + +class JianYingManager: + """ + 剪映草稿管理器 + """ + + def __init__( + self, + materials_folder_path: str, + drafts_folder_path: str = r"E:\JianYingPro Drafts", + ): + """ + 初始化 + :param materials_folder_path: 素材文件夹路径。其中,文件夹名称默认为工作流名称 + :param drafts_folder_path: 剪映草稿文件夹路径,默认为 E:\\JianYingPro Drafts + """ + try: + # 初始化素材文件夹路径 + self.materials_folder_path = Path(materials_folder_path) + if not self.materials_folder_path.exists(): + raise RuntimeError("素材文件夹路径不存在") + + # 初始化所有素材 + self.materials = self._init_materials() + + # 初始化成品文件夹路径 + self.products_folder_path = Path( + materials_folder_path.replace("materials", "products") + ) + # 若成品文件夹存路径已存在则先删除 + if self.products_folder_path.exists(): + shutil.rmtree(self.products_folder_path) + self.products_folder_path.mkdir(parents=True) + + # 初始化剪映草稿文件夹路径 + self.drafts_folder_path = Path(drafts_folder_path) + if not self.drafts_folder_path.exists(): + raise RuntimeError("剪映草稿文件夹路径不存在") + + # 初始化节点配置 + self.configurations = self._init_configurations() + + # 初始化剪映草稿文件夹管理器 + self.drafts_folder = DraftFolder(folder_path=drafts_folder_path) + + # 实例化缓存 + self.caches = Caches() + except Exception as exception: + raise RuntimeError(f"发生异常:{str(exception)}") from exception + + def _init_materials(self) -> Dict[str, List[Any]]: + """ + 初始化所有素材 + :return: 所有素材 + """ + materials = {} + # 构建字幕文本路径 + subtitle_text_path = self.materials_folder_path / "字幕文本.txt" + if subtitle_text_path.exists() and subtitle_text_path.is_file(): + with open(subtitle_text_path, "r", encoding="utf-8") as file: + # 字幕文本列表 + subtitle_texts = file.readlines() + if not subtitle_texts: + raise RuntimeError("字幕文本为空") + materials["subtitle_texts"] = subtitle_texts + else: + materials["subtitle_texts"] = [] + + # 构建字幕视频文件夹路径 + subtitle_video_folder_path = self.materials_folder_path / "字幕视频" + if subtitle_video_folder_path.exists() and subtitle_video_folder_path.is_dir(): + materials["subtitle_video_paths"] = [ + subtitle_video_path + for subtitle_video_path in subtitle_video_folder_path.rglob("*.mov") + ] + else: + materials["subtitle_video_paths"] = [] + + # 构建背景视频文件夹路径 + background_video_folder_path = self.materials_folder_path / "背景视频" + if ( + background_video_folder_path.exists() + and background_video_folder_path.is_dir() + ): + materials["background_video_paths"] = [ + background_video_path + for background_video_path in background_video_folder_path.rglob("*.mp4") + ] + else: + materials["background_video_paths"] = [] + + # 构建达人视频文件夹路径 + kol_video_folder_path = self.materials_folder_path / "达人视频" + if kol_video_folder_path.exists() and kol_video_folder_path.is_dir(): + materials["kol_video_paths"] = [ + kol_video_path + for kol_video_path in kol_video_folder_path.rglob("*.mp4") + ] + else: + materials["kol_video_paths"] = [] + + # 构建背景音频文件夹路径 + background_audio_folder_path = self.materials_folder_path / "背景音频" + if ( + background_audio_folder_path.exists() + and background_audio_folder_path.is_dir() + ): + materials["background_audio_paths"] = [ + background_audio_path + for background_audio_path in background_audio_folder_path.rglob("*.mp3") + ] + else: + materials["background_audio_paths"] = [] + + # 构建标识图片路径 + logo_image_path = self.materials_folder_path / "标识图片.png" + if logo_image_path.exists() and logo_image_path.is_file(): + materials["logo_image_path"] = [logo_image_path] # 有且只有一张标识 + else: + materials["logo_image_path"] = [] + + # 构建标识视频文件夹路径 + logo_video_folder_path = self.materials_folder_path / "标识视频" + if logo_video_folder_path.exists() and logo_video_folder_path.is_dir(): + materials["logo_video_path"] = [ + file_path for file_path in logo_video_folder_path.rglob("*.mov") + ] + else: + materials["logo_video_path"] = [] + + # 构建声明文本路径 + statement_text_path = self.materials_folder_path / "声明文本.txt" + if statement_text_path.exists() and statement_text_path.is_file(): + with open(statement_text_path, "r", encoding="utf-8") as file: + # 声明文本列表 + statement_texts = file.readlines() + if not statement_texts: + raise RuntimeError("声明文本为空") + materials["statement_texts"] = statement_texts + else: + materials["statement_texts"] = [] + + # 构建声明视频文件夹路径 + statement_video_folder_path = self.materials_folder_path / "声明视频" + if ( + statement_video_folder_path.exists() + and statement_video_folder_path.is_dir() + ): + materials["statement_video_path"] = [ + statement_video_path + for statement_video_path in statement_video_folder_path.rglob("*.mov") + ] + else: + materials["statement_video_path"] = [] + + return materials + + def _init_configurations( + self, + ) -> Dict[str, Any]: + """ + 初始化节点配置 + :return: 节点配置 + """ + # 已配置工作流 + workflows = { + "默认工作流": [ + "generate_subtitle", + "add_background_video", + "add_statement", + "add_sticker", + "add_sticker_arrow", + ], # 默认工作流,先根据字幕文本合成字幕音频并生成字幕,再叠加背景视频、声明视频、非箭头贴纸视频和箭头贴纸视频 + "淘宝闪购": [ + "add_subtitle_video", + "add_background_video", + "add_background_audio", + "add_statement_video", + ], # 淘宝闪购,先根据字幕视频获取其持续时长并作为成品持续时长,再叠加背景视频、背景音频和生成视频 + "淘宝闪购_达人": [ + "add_background_video", + "add_background_audio", + "add_subtitle_video", + "add_kol_video", + "add_statement_video", + ], # 淘宝闪购_达人,第一段:先根据字幕视频获取前5秒作为持续时长,再叠加背景视频、背景音频;第二段:背景视频(达人);拼接第一段和第二段再叠加声明视频 + } + # 默认以素材文件夹名称为工作流名称 + workflow_name = self.materials_folder_path.stem + # 工作流 + workflow = workflows.get(workflow_name) + if not workflow: + raise RuntimeError(f"未配置该工作流 {workflow_name}") + + # 节点配置模板 + configurations = { + "generate_subtitle": { + "texts": self.materials["subtitle_texts"], + "style": [ + {"size": 10.0}, + ], # 字体样式 + "effect": [ + {"effect_id": "7127561998556089631"}, + {"effect_id": "7166467215410187552"}, + {"effect_id": "6896138122774498567"}, + {"effect_id": "7166469374507765031"}, + {"effect_id": "6896137924853763336"}, + ], # 花字设置 + }, # 生成字幕工作配置 + "add_subtitle_video": { + "material_path": self.materials["subtitle_video_paths"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + None, + ], # 图像调节设置 + }, # 添加字幕视频工作配置 + "add_background_video": { + "material_path": self.materials["background_video_paths"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + { + "scale_x": 1.0, + "scale_y": 1.0, + }, + ], # 图像调节设置 + }, # 添加背景视频工作配置 + "add_kol_video": { + "material_path": self.materials["kol_video_paths"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + { + "scale_x": 1.0, + "scale_y": 1.0, + }, + ], # 图像调节设置 + }, # 添加达人视频工作配置 + "add_background_audio": { + "material_path": self.materials["background_audio_paths"], + "volume": [0.6], # 播放音量 + }, # 添加背景音频工作配置 + "add_logo_image": { + "material_path": self.materials["logo_image_path"], + "clip_settings": [ + { + "scale_x": 0.2, + "scale_y": 0.2, + "transform_x": -0.78, + "transform_y": 0.82, + }, + ], + }, # 添加标识工作配置 + "add_logo_video": { + "material_path": self.materials["logo_video_path"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + None, + ], # 图像调节设置 + }, # 添加标识视频工作配置 + "add_statement": { + "text": self.materials["statement_texts"], + "style": [ + {"size": 6.0, "align": 1, "vertical": True}, + ], # 文本样式 + "border": [ + {"width": 35.0}, + {"width": 40.0}, + {"width": 45.0}, + ], # 描边宽度 + "clip_settings": [ + { + "transform_x": -0.82, + }, + ], # 图像调节设置 + }, # 添加声明工作配置 + "add_statement_video": { + "material_path": self.materials["statement_video_path"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + None, + ], # 图像调节设置 + }, # 添加声明视频工作配置 + "add_sticker": { + "resource_id": [ + "7110124379568098568", + "7019687632804334861", + "6895933678262750478", + "7010558788675652900", + "7026858083393588487", + ], + "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, + }, + ], # 图像调节设置 + }, # 添加非箭头贴纸工作配置 + "add_sticker_arrow": { + "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, + }, + ], # 图像调节设置 + }, # 添加箭头贴纸工作配置 + } + # 达人视频特殊处理:字幕视频、背景视频和背景音频素材持续时长为 5秒,叠加达人视频 + if workflow_name == "淘宝闪购_达人": + configurations.update( + { + node: { + **configurations[node], + "target_timerange": [ + (0, 5_000_000), + ], + } + for node in [ + "add_subtitle_video", + "add_background_video", + "add_background_audio", + ] + } + ) + configurations.update( + { + node: { + **configurations[node], + "target_timerange": [ + (5_000_000, None), + ], + } + for node in [ + "add_kol_video", + ] + } + ) + + # 若包含添加背景音频节点则在添加背景视频时其播放音量设置为 0 + if "add_background_audio" in workflow: + configurations["add_background_video"]["volume"] = [0.0] + + return {node_name: configurations[node_name] for node_name in workflow} + + def batch_create( + self, + draft_counts: int, + video_width: int = 1080, + video_height: int = 1920, + video_fps: int = 30, + ) -> None: + """ + 批量创建草稿 + :param draft_counts: 草稿数 + :param video_width: 视频宽度(单位为像素),默认为 1080 + :param video_height: 视频高度(单位为像素),默认为 1920 + :param video_fps: 视频帧率(单位为帧/秒),默认为 30 + :return: 无 + """ + draft_index = 1 # 草稿索引 + while True: + # 获取节点配置 + configurations = self._get_configurations() + + video_duration = VideoMaterial( + path=configurations["add_background_video"]["material_path"].as_posix() + ).duration # 默认将背景视频素材持续时长作为视频持续时长(单位为微秒) + # 达人视频特殊处理:固定 5秒加上达人视频素材持续时长 + if "add_kol_video" in configurations: + video_duration = ( + 5_000_000 + + VideoMaterial( + path=configurations["add_kol_video"]["material_path"].as_posix() + ).duration + ) + + # 生成剪映草稿名称 + draft_name = self._generate_draft_name( + configurations=configurations, + ) + # 若草稿名称已缓存则跳过 + if self.caches.query(draft_name=draft_name): + continue + + print(f"正在创建草稿 {draft_name}({draft_index}/{draft_counts})...") + + # 创建剪映草稿 + draft = Drafts( + materials_folder_path=self.materials_folder_path, + drafts_folder=self.drafts_folder, + draft_name=draft_name, + video_width=video_width, + video_height=video_height, + video_fps=video_fps, + video_duration=video_duration, + ) + + for node_name in configurations: + match node_name: + # 添加字幕 + case "generate_subtitle": + print("-> 正在根据字幕文本合成字幕音频并生成字幕...", end="") + draft.generate_subtitle(**configurations[node_name]) + print("已完成") + # 添加字幕视频 + case "add_subtitle_video": + print("-> 正在添加字幕视频...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加背景视频 + case "add_background_video": + print("-> 正在添加背景视频...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加达人视频 + case "add_kol_video": + print("-> 正在添加达人视频...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加背景音频 + case "add_background_audio": + print("-> 正在添加背景音频...", end="") + draft.add_audio_segment(**configurations[node_name]) + print("已完成") + # 添加标识 + case "add_logo_image": + print("-> 正在添加标识图片...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加标识视频 + case "add_logo_video": + print("-> 正在添加标识视频...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加声明文本 + case "add_statement": + print("-> 正在添加声明文本...", end="") + draft.add_text_segment(**configurations[node_name]) + print("已完成") + # 添加声明视频 + case "add_statement_video": + print("-> 正在添加声明视频...", end="") + draft.add_video_segment(**configurations[node_name]) + print("已完成") + # 添加贴纸 + case _ if node_name.startswith("add_sticker"): + print("-> 正在添加贴纸...", end="") + draft.add_sticker(**configurations[node_name]) + print("已完成") + # 保存草稿 + case "save": + print("-> 正在保存草稿...", end="") + draft.save() + print("已完成") + + # 缓存草稿名称和所有节点配置 + self.caches.update( + draft_name=draft_name, + configurations=configurations, + ) + + print("已完成") + print() + + draft_index += 1 + if draft_index > draft_counts: + break + + def _get_configurations( + self, + ) -> Dict[str, Any]: + """ + 获取节点配置 + :return: 节点配置 + """ + configurations = {} + for node_name in self.configurations: + # 根据节点名称获取节点配置 + configurations.update( + { + node_name: { + key: random.choice(value) + for key, value in self.configurations[node_name].items() + } + } + ) + # 若非生成字幕则在工作流配置添加轨道名称 + if node_name != "generate_subtitle": + configurations[node_name]["track_name"] = ( + matched.group("track_name") + if ( + matched := re.match( + pattern=r"^.+_(?P.+_.+)$", + string=node_name, + ) + ) + else node_name + ) + + # 添加保存节点 + configurations.update( + { + "save": {}, + } + ) + return configurations + + def _generate_draft_name( + self, + configurations: Dict[str, Any], + ) -> str: + """ + 生成剪映草稿名称 + :param configurations: 指定工作流所有节点配置 + :return: 草稿名称 + """ + return ( + hashlib.md5( + json.dumps( + obj=configurations, + cls=self.caches.JSONEncoder, + sort_keys=True, + ensure_ascii=False, + ).encode("utf-8") + ) # 将工作流配置序列化 + .hexdigest() + .upper() # MD5哈希值的大写十六进制作为草稿名称 + ) diff --git a/短视频合成自动化/main.py b/短视频合成自动化/main.py index d2946b0..fc6dbcd 100644 --- a/短视频合成自动化/main.py +++ b/短视频合成自动化/main.py @@ -3,16 +3,16 @@ 主模块 """ -from export import JianYingExport +# 列举导入模块 +from jiangying_manager import JianYingManager if __name__ == "__main__": - # 实例化 JianYingExport - jianying_export = JianYingExport( - materials_folder_path=r"E:\jianying\materials\淘宝闪购模版001", + # 实例化 JianYingManager + jianying_manager = JianYingManager( + materials_folder_path=r"E:\jianying\materials\淘宝闪购_达人", ) # 导出视频 - jianying_export.export_videos( - workflow_name="0001", - draft_counts=10, + jianying_manager.batch_create( + draft_counts=1, )