Python/短视频合成自动化/export.py

575 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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
# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes
class JianYingExport:
"""
封装 pyJianYingDraft.JianyingController库支持
1、初始化素材文件夹内所有素材
2、初始化工作流和工作配置
3、导出草稿
"""
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
def __init__(
self,
materials_folder_path: str,
program_path: str = "E:\\JianYingPro\\5.9.0.11632\\JianYingPro.exe", # 仅可在windows运行该脚本
drafts_folder_path: str = "E:\\JianYingPro Drafts",
draft_counts: int = 10,
video_width: int = 1080,
video_height: int = 1920,
video_fps: int = 30,
):
"""
初始化
:param program_path: 剪映程序路径
:param drafts_folder_path: 剪映草稿文件夹路径
:param materials_folder_path: 素材文件夹路径
:param draft_counts: 草稿数,默认为 10
:param video_width: 视频宽度,默认为 1080像素
:param video_height: 视频高度,默认为 1920像素
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
"""
try:
self.program_path = Path(program_path)
if not self.program_path.exists():
raise RuntimeError("剪映程序路径不存在")
# 初始化剪映专业版进程
self.jianying_process = None
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")
)
self.exports_folder_path.mkdir() # 若导出文件夹存在则抛出异常,需手动处理
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)}") 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:
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 export(self, batch_draft_counts: int = 1):
"""
导出草稿
:param batch_draft_counts: 每批次导出草稿数
"""
# 按照工作流和工作配置拼接素材,生成草稿
self._generate()
# 批次导出
for batch_start in range(0, self.draft_counts, batch_draft_counts):
# 当前批次所有草稿名称
batch_draft_names = self.draft_names[
batch_start : batch_start + batch_draft_counts
]
# 启动剪映专业版进程
self._start_process()
time.sleep(2)
# 初始化剪映控制器
jianying_controller = pyJianYingDraft.JianyingController()
for draft_name in batch_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()
time.sleep(2)
for draft_name in batch_draft_names:
# 就已导出草稿删除
self.drafts_folder.remove(draft_name=draft_name)
time.sleep(2)
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(
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 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 _start_process(self, timeout: int = 60) -> None:
"""
启动剪映专业版进程
:param timeout: 最大等待时间(单位为秒),默认为 60
:return: 无
"""
try:
# 关闭剪映专业版进程
self._close_process()
# 非堵塞方法
self.jianying_process = subprocess.Popen(
args=self.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)}"
) from 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:
# 请求关闭剪映程序窗口
# pylint: disable=c-extension-no-member
win32gui.SendMessage(window_handle, win32con.WM_CLOSE, 0, 0)
start_time = time.time()
while time.time() - start_time < timeout:
# pylint: disable=c-extension-no-member
if not win32gui.IsWindow(window_handle):
print("已关闭剪映专业版进程")
return
time.sleep(2)
raise RuntimeError("关闭超时")
except Exception as exception:
raise RuntimeError(
f"关闭剪映专业版进程发生异常:{str(exception)}"
) from exception
@staticmethod
def _locate_window() -> Optional[int]:
"""
定位剪映程序窗口
:return: 剪映程序窗口句柄
"""
window_handle = None
def callback(handle, _):
"""
遍历所有窗口的回调函数
"""
# 初始化窗口句柄
nonlocal window_handle
# 获取窗口标题
# pylint: disable=c-extension-no-member
window_text = win32gui.GetWindowText(handle)
# 检查窗口是否可见且窗口标题为剪映专业版
# pylint: disable=c-extension-no-member
if win32gui.IsWindowVisible(handle) and window_text == "剪映专业版":
window_handle = handle
return False
return True
# 遍历所有顶层窗口
# pylint: disable=c-extension-no-member
win32gui.EnumWindows(callback, None)
return window_handle