# -*- coding: utf-8 -*- """ 剪映草稿管理器模块 """ # 列举导入模块 import hashlib import json from pathlib import Path from random import choice import shutil 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": [ { "rotation": 90, }, ], # 剪辑设置 }, ) ], [ ( "底纹", { "clip_settings": [ { "rotation": 90, }, ], # 剪辑设置 }, ) ], ], } 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, 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: 无 """ 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})...") # 创建剪映草稿 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=video_fps, 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