Python/短视频自动合成_新/drafts_manager.py

310 lines
11 KiB
Python

# -*- 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