# -*- coding: utf-8 -*- """ 剪映草稿管理器模块 """ # 列举导入模块 import hashlib import json from pathlib import Path from random import choice import sys from pyJianYingDraft import DraftFolder, VideoMaterial from caches import Caches from drafts import Drafts sys.path.append(Path(__file__).parent.parent.as_posix()) DRAFT_GENERATE_SCHEMES = { "存量_纯图_竖版": [ [ ( "按钮", { "clip_settings": [ { "scale_x": 1.0, "scale_y": 1.0, }, ], # 剪辑设置 }, ) ], [ ( "底纹", { "clip_settings": [ { "scale_x": 1.0, "scale_y": 1.0, }, ], # 剪辑设置 }, ) ], [ ( "箭头", { "clip_settings": [ { "scale_x": 0.35, "scale_y": 0.35, "transform_x": 0, "transform_y": -0.46, }, ], # 剪辑设置 }, ) ], [ ( "提示语", { "volume": [1.0], # 播放音量 "clip_settings": [ { "scale_x": 1.0, "scale_y": 1.0, "transform_x": 0, "transform_y": 0.05, }, ], # 剪辑设置 }, ) ], ], "存量_纯图_横版": [ [ ( "按钮", { "clip_settings": [ { "scale_x": 1.0, "scale_y": 1.0, }, ], # 剪辑设置 }, ) ], [ ( "底纹", { "clip_settings": [ { "scale_x": 1.0, "scale_y": 1.0, }, ], # 剪辑设置 }, ) ], ], } class DraftsManager: """ 剪映草稿管理器 """ def __init__( self, project_name: str, drafts_folder_path: str = r"E:\JianYingPro Drafts", ): """ 初始化剪映草稿管理器 :param project_name: 项目名称 :param drafts_folder_path: 剪映草稿文件夹路径,默认为 E:\\JianYingPro Drafts """ # 项目名称 self.project_name = project_name # 草稿生成方案 self.draft_generate_scheme: list[list[tuple[str, dict]]] = ( DRAFT_GENERATE_SCHEMES.get(self.project_name, []) ) if not self.draft_generate_scheme: raise RuntimeError(f"未就 {self.project_name} 配置草稿生成方案") # 构建此项目素材仓库路径 self.material_vault_path = Path(rf"E:\jianying\materials\{project_name}") # 检查此项目素材仓库是否存在,若不存在则抛出异常 if not self.material_vault_path.exists(): raise RuntimeError("素材仓库不存在") self.materials: dict[str, list[Path]] = ( {} ) # 键为素材文件夹名称,值为素材路径列表 # 初始化项目素材 self.init_materials() # 构建草稿文件夹路径 self.drafts_folder_path = Path(drafts_folder_path) # 检查草稿文件夹是否存在,若不存在则抛出异常 if not self.drafts_folder_path.exists(): raise RuntimeError("草稿文件夹不存在") # 初始化草稿文件夹管理器 self.drafts_folder = DraftFolder(self.drafts_folder_path.as_posix()) # 实例化缓存(用于检查草稿是否存在缓存,若存在则重新生成) self.caches = Caches() def init_materials(self) -> None: """ 初始化项目素材 :return: None """ # 草稿生成方案第一层为轨道元信息,第二层为片段元信息。其中,轨道元信息包括若干片段元信息;片段元信息包括素材文件夹名称与配置元信息 for track_metadata in self.draft_generate_scheme: for segment_metadata in track_metadata: material_folder_name, _ = segment_metadata # 构建素材文件夹路径 material_folder_path = self.material_vault_path / material_folder_name # 若素材文件夹不存在则抛出异常 if not material_folder_path.exists(): raise RuntimeError(f"素材文件夹 {material_folder_name} 不存在") # 遍历素材文件夹内所有素材路径 self.materials[material_folder_name] = [ material_path for material_path in material_folder_path.glob( "*" ) # 不递归遍历素材文件夹内文件夹 if material_path.is_file() and not material_path.is_symlink() and material_path.suffix.lower() in {".mp4", ".mov", ".png"} ] # 暂仅支持素材路径后缀如上 for material_folder_name in self.materials.keys(): # 统计素材文件夹内素材路径后缀数 suffix_counts = len( {p.suffix.lower() for p in self.materials[material_folder_name]} ) if suffix_counts > 1: raise RuntimeError( f"素材文件夹 {material_folder_name} 内存在多种素材路径后缀" ) def batch_create( self, draft_counts: int, ) -> None: """ 批量创建草稿 :param draft_counts: 草稿数 :param video_fps: 视频帧率(单位为帧/秒),默认为 30 :return: 无 """ index = 1 # 草稿索引 while True: draft_generate_scheme: list[dict] = [] # 草稿生成方案 video_duration = 0 # 视频时长 for track_idx, track_metadata in enumerate(self.draft_generate_scheme): # 生成轨道名称 track_name = f"track{track_idx:02d}" for segment_metadata in track_metadata: # 获取素材文件夹名称、配置元信息 material_folder_name, config_metadata = segment_metadata # 随机选择素材路径 material_path = choice(self.materials[material_folder_name]) # 初始化片段生成方案 segment_generate_scheme = { "track_name": track_name, "material_path": material_path, **{k: choice(v) for k, v in config_metadata.items()}, } # 素材路径后缀 material_path_suffix = material_path.suffix.lower() # 若为第一轨道则将所有素材的时长作为视频时长,其他轨道则将第一片段的目标时长设置为视频时长 if track_idx == 0: if material_path_suffix not in [".mp4", ".mov", ".png"]: raise RuntimeError( f"第一轨道中素材 {material_path.name} 的类型必须为 mp4, mov, png" ) video_duration += VideoMaterial( path=material_path.as_posix() ).duration else: segment_generate_scheme["target_timerange"] = ( 0, video_duration, ) # 暂仅考虑一个轨道包括一个片段 # 将片段生成方案添加至草稿生成方案 draft_generate_scheme.append(segment_generate_scheme) # 生成剪映草稿名称 draft_name = ( hashlib.md5( json.dumps( obj=draft_generate_scheme, cls=self.caches.JSONEncoder, sort_keys=True, ensure_ascii=False, ).encode("utf-8") ) .hexdigest() .upper() ) # MD5哈希值的大写十六进制作为草稿名称 # 若存在相同草稿名称的缓存则跳过 if self.caches.has_same_draft_name(draft_name): continue print(f"正在创建草稿 {draft_name}({index}/{draft_counts})...") if "横版" in self.project_name: video_width, video_height = 1920, 1080 else: video_width, video_height = 1080, 1920 # 创建剪映草稿 draft = Drafts( materials_vault_path=self.material_vault_path, drafts_folder=self.drafts_folder, draft_name=draft_name, video_width=video_width, video_height=video_height, video_fps=30, # 视频帧率(单位为帧/秒),默认为 30 video_duration=video_duration, ) for segment_generate_scheme in draft_generate_scheme: # 获取素材路径后缀 material_path_suffix = segment_generate_scheme[ "material_path" ].suffix.lower() # 根据素材路径后缀匹配具体操作 match material_path_suffix: case ".mp4" | ".mov" | ".png": print("-> 正在添加视频片段...", end="") draft.add_video_segment(**segment_generate_scheme) print("已完成") print("-> 正在保存草稿...", end="") draft.save() print("已完成") # 缓存草稿名称和所有片段配置 self.caches.update( draft_name, draft_generate_scheme, ) print("已完成") print() index += 1 if index > draft_counts: break