parent
e8a0f5d414
commit
e6bb9efb14
|
|
@ -1,46 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
剪映脚本生成自动化
|
||||
"""
|
||||
|
||||
from capcut import GenerateDraft
|
||||
|
||||
|
||||
# 编导方案1
|
||||
def direct():
|
||||
"""生成剪映草稿"""
|
||||
# 实例化
|
||||
draft = GenerateDraft(
|
||||
name="demo2",
|
||||
) # 需要
|
||||
|
||||
# 根据脚本生成文本和音频字幕
|
||||
draft.add_subtitle(
|
||||
script="所有人今天准备狂点外卖,是真的0.1元起一杯的霸王茶姬,还外卖到家怎么能不来一杯呢,现在淘宝闪购给大家发福利,最高22元无门槛红包,官方链接就在下方,奶茶脑袋快冲"
|
||||
)
|
||||
# 为背景视频添加视频轨道并添加视频片段
|
||||
draft.add_video(
|
||||
track_name="background",
|
||||
name="background.mp4",
|
||||
clip_settings={"scale_x": 2.5, "scale_y": 2.5},
|
||||
)
|
||||
# 为logo添加视频轨道并添加图片片段
|
||||
draft.add_video(
|
||||
track_name="logo",
|
||||
name="logo.png",
|
||||
)
|
||||
# 为免责声明添加视频轨道并添加图片片段
|
||||
draft.add_text(
|
||||
track_name="disclaimer",
|
||||
content="支付需谨慎谨防诈骗\n仅限支付宝用户,详情以活动为准",
|
||||
)
|
||||
|
||||
draft.add_sticker(track_name="sticker", resource_id="7026858083393588487")
|
||||
|
||||
# 保存草稿
|
||||
draft.save()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
execute_workflow()
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Literal
|
||||
|
||||
from capcut import CapCutDraft
|
||||
|
||||
|
||||
class WorkFlow:
|
||||
"""
|
||||
生成 CapCut草稿的工作流,支持:
|
||||
1、初始化素材文件夹内所有素材
|
||||
2、就工作流添加工作
|
||||
3、基于工作流生成草稿
|
||||
"""
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def __init__(
|
||||
self,
|
||||
materials_folder_path: str,
|
||||
video_width: int = 1080,
|
||||
video_height: int = 1920,
|
||||
video_fps: int = 30,
|
||||
):
|
||||
"""
|
||||
初始化
|
||||
:param materials_folder_path: 素材文件夹路径
|
||||
:param video_width: 视频宽度,默认为 1080像素
|
||||
:param video_height: 视频高度,默认为 1920像素
|
||||
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
|
||||
"""
|
||||
print("正在初始化 DraftsGenerator...", end="")
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.materials_folder_path = Path(materials_folder_path)
|
||||
if not self.materials_folder_path.exists():
|
||||
raise RuntimeError("素材文件夹路径不存在")
|
||||
|
||||
# 构建项目名称
|
||||
self.project_name = self.materials_folder_path.stem
|
||||
|
||||
# 初始化工作流
|
||||
self.workflow = [
|
||||
"add_subtitles",
|
||||
"add_background_video",
|
||||
"add_logo",
|
||||
"add_statement",
|
||||
"add_sticker",
|
||||
"save",
|
||||
]
|
||||
|
||||
self.video_width = video_width
|
||||
self.video_height = video_height
|
||||
self.video_fps = video_fps
|
||||
|
||||
# 初始化素材文件夹内所有素材
|
||||
self.materials = {}
|
||||
self._init_materials()
|
||||
|
||||
# 初始化所有工作配置
|
||||
self.configurations = {
|
||||
"add_subtitles": {
|
||||
"text": self.materials["subtitles_text"],
|
||||
"timbre": [
|
||||
"zh-CN-XiaoxiaoNeural",
|
||||
"zh-CN-XiaoyiNeural",
|
||||
"zh-CN-YunjianNeural",
|
||||
"zh-CN-YunxiNeural",
|
||||
],
|
||||
"style": [{"size": 8}, {"size": 10}],
|
||||
"effect": [
|
||||
{"effect_id": "7127561998556089631"},
|
||||
{"effect_id": "7166467215410187552"},
|
||||
{"effect_id": "6896138122774498567"},
|
||||
],
|
||||
},
|
||||
"add_background_video": {
|
||||
"track_name": ["background_video"],
|
||||
"material_path": self.materials["background_video_material_path"],
|
||||
"clip_settings": [
|
||||
None,
|
||||
{
|
||||
"transform_x": 0.2,
|
||||
},
|
||||
{
|
||||
"transform_x": -0.2,
|
||||
},
|
||||
{
|
||||
"transform_y": 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.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,
|
||||
},
|
||||
],
|
||||
},
|
||||
"add_statement": {
|
||||
"track_name": ["statement"],
|
||||
"text": self.materials["statement_text"],
|
||||
"border": [
|
||||
{"width": 40.0},
|
||||
{"width": 50.0},
|
||||
{"width": 60.0},
|
||||
], # 描边宽度
|
||||
"style": [{"size": 8.0, "align": 1}], # 文本样式
|
||||
"clip_settings": [
|
||||
{
|
||||
"transform_y": -0.8,
|
||||
}
|
||||
], # 图像调节设置
|
||||
},
|
||||
"add_sticker": {
|
||||
"track_name": ["sticker"],
|
||||
"resource_id": ["7026858083393588487"],
|
||||
"clip_settings": [
|
||||
{
|
||||
"scale_x": 0.2,
|
||||
"scale_y": 0.2,
|
||||
"transform_x": -0.75,
|
||||
"transform_y": -0.78,
|
||||
}
|
||||
], # 图像调节设置
|
||||
},
|
||||
}
|
||||
|
||||
print("已完成")
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"发生异常:{str(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 add_work(
|
||||
self,
|
||||
work: Literal[
|
||||
"add_subtitles",
|
||||
"add_background_video",
|
||||
"add_logo",
|
||||
"add_statement",
|
||||
"add_sticker",
|
||||
"save",
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
就工作流添加工作
|
||||
:param work: 工作,目前仅支持添加字幕、添加背景视频、添加标识、添加声明、添加贴纸和保存
|
||||
:return: 无
|
||||
"""
|
||||
self.workflow.append(work)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
counts: int = 1,
|
||||
) -> None:
|
||||
"""
|
||||
基于工作流生成草稿
|
||||
:param counts: 生成 CapCut草稿数
|
||||
:return: 无
|
||||
"""
|
||||
for idx in range(counts):
|
||||
draft_name = self.project_name + f"{idx + 1:03d}"
|
||||
# 实例化 CapCutDraft
|
||||
draft = CapCutDraft(
|
||||
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":
|
||||
draft.add_subtitles(**self._random(work=work))
|
||||
# 添加背景视频
|
||||
case "add_background_video":
|
||||
draft.add_video_segment(**self._random(work=work))
|
||||
# 添加标识
|
||||
case "add_logo":
|
||||
draft.add_video_segment(**self._random(work=work))
|
||||
# 添加声明
|
||||
case "add_statement":
|
||||
draft.add_text_segment(**self._random(work=work))
|
||||
# 添加贴纸
|
||||
case "add_sticker":
|
||||
draft.add_sticker(**self._random(work=work))
|
||||
# 将草稿保存至 CapCut草稿文件夹内
|
||||
case "save":
|
||||
draft.save()
|
||||
|
||||
def _random(
|
||||
self,
|
||||
work: Literal[
|
||||
"add_subtitles",
|
||||
"add_background_video",
|
||||
"add_logo",
|
||||
"add_statement",
|
||||
"add_sticker",
|
||||
],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
随机获取工作配置
|
||||
:param work: 工作,包括添加字幕、添加背景视频、添加标识、添加声明和添加贴纸
|
||||
:return: 工作配置
|
||||
"""
|
||||
|
||||
return {
|
||||
key: random.choice(value)
|
||||
for key, value in self.configurations[work].items()
|
||||
}
|
||||
|
||||
|
||||
a = WorkFlow(materials_folder_path=r"E:\projects\251225")
|
||||
|
||||
|
||||
a.generate(counts=5)
|
||||
|
|
@ -3,32 +3,28 @@
|
|||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import pycapcut as capcut
|
||||
from pycapcut import trange
|
||||
from pycapcut.keyframe import KeyframeProperty
|
||||
from pycapcut.segment import ClipSettings
|
||||
import pyJianYingDraft
|
||||
|
||||
from edgetts import EdgeTTS
|
||||
|
||||
|
||||
class CapCutDraft:
|
||||
class JianYingDraft:
|
||||
"""
|
||||
封装pyCapCut,支持:
|
||||
封装 pyJianYing中生成草稿的相关功能,支持:
|
||||
1、向指定文本轨道添加文本片段
|
||||
2、向指定音频轨道添加音频片段
|
||||
3、向指定视频轨道添加视频或图片片段
|
||||
4、向指定贴纸轨道添加贴纸片段
|
||||
5、根据文本逐段合成语音,生成文本和语音字幕
|
||||
6、将草稿保存至 CapCut草稿文件夹内
|
||||
(导出尚未支持)
|
||||
6、将草稿保存至剪映草稿文件夹内
|
||||
"""
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def __init__(
|
||||
self,
|
||||
draft_name: str,
|
||||
materials_folder_path: Union[str, Path],
|
||||
capcut_folder_path: str = r"C:\Users\admin\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft",
|
||||
materials_folder_path: Path,
|
||||
drafts_folder_path: str = r"E:\JianyingPro Drafts",
|
||||
allow_replace: bool = True,
|
||||
video_width: int = 1080,
|
||||
video_height: int = 1920,
|
||||
|
|
@ -36,7 +32,7 @@ class CapCutDraft:
|
|||
):
|
||||
"""
|
||||
初始化
|
||||
:param capcut_folder_path: CapCut草稿文件夹路径
|
||||
:param drafts_folder_path: 剪映草稿文件夹路径
|
||||
:param draft_name: 草稿名称
|
||||
:param allow_replace: 是否允许覆盖同名草稿
|
||||
:param video_width: 视频宽度,默认为 1080像素
|
||||
|
|
@ -44,33 +40,24 @@ class CapCutDraft:
|
|||
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
|
||||
:param materials_folder_path: 素材文件夹路径
|
||||
"""
|
||||
print("正在初始化 CapCutDraft...", end="")
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
# 初始化草稿文件夹管理器
|
||||
self.draft_folder = capcut.DraftFolder(capcut_folder_path)
|
||||
self.draft_folder = pyJianYingDraft.DraftFolder(drafts_folder_path)
|
||||
|
||||
# 新建草稿
|
||||
self.draft = self.draft_folder.create_draft(
|
||||
draft_name=draft_name,
|
||||
allow_replace=allow_replace,
|
||||
width=video_width,
|
||||
height=video_height,
|
||||
fps=video_fps,
|
||||
allow_replace=allow_replace,
|
||||
)
|
||||
|
||||
# 草稿持续时长
|
||||
# 草稿持续时长(单位为毫秒)
|
||||
self.draft_duration = 0
|
||||
|
||||
self.materials_folder_path = (
|
||||
materials_folder_path
|
||||
if isinstance(materials_folder_path, Path)
|
||||
else Path(materials_folder_path)
|
||||
)
|
||||
if not self.materials_folder_path.exists():
|
||||
raise RuntimeError("素材文件夹路径不存在")
|
||||
self.materials_folder_path = materials_folder_path
|
||||
|
||||
print("已完成")
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"发生异常:{str(exception)}")
|
||||
|
||||
|
|
@ -105,27 +92,30 @@ class CapCutDraft:
|
|||
:param animation: 动画设置,默认为无
|
||||
:return: 无
|
||||
"""
|
||||
print(f"正在向 {track_name}文本轨道添加文本片段...", end="")
|
||||
try:
|
||||
if add_track:
|
||||
# 添加文本轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.text,
|
||||
track_type=pyJianYingDraft.TrackType.text,
|
||||
track_name=track_name,
|
||||
)
|
||||
|
||||
# 构建文本片段
|
||||
text_segment = capcut.TextSegment(
|
||||
text_segment = pyJianYingDraft.TextSegment(
|
||||
text=text.replace("\\n", "\n"),
|
||||
timerange=trange(
|
||||
timerange=pyJianYingDraft.trange(
|
||||
*(timerange if timerange else (0, self.draft_duration))
|
||||
),
|
||||
font=capcut.FontType(font) if font else None,
|
||||
style=capcut.TextStyle(**style) if style else None,
|
||||
border=capcut.TextBorder(**border) if border else None,
|
||||
background=capcut.TextBackground(**background) if background else None,
|
||||
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
|
||||
),
|
||||
clip_settings=(
|
||||
capcut.ClipSettings(**clip_settings) if clip_settings else None
|
||||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||||
if clip_settings
|
||||
else None
|
||||
),
|
||||
)
|
||||
# 添加气泡
|
||||
|
|
@ -144,8 +134,7 @@ class CapCutDraft:
|
|||
|
||||
# 向指定文本轨道添加文本片段
|
||||
self.draft.add_segment(segment=text_segment, track_name=track_name)
|
||||
print("已完成")
|
||||
return
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
|
@ -174,19 +163,18 @@ class CapCutDraft:
|
|||
:param fade: 淡入淡出设置,默认为无
|
||||
:return: 无
|
||||
"""
|
||||
print(f"正在向 {track_name}音频轨道添加音频片段...", end="")
|
||||
try:
|
||||
if add_track:
|
||||
# 添加音频轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.video,
|
||||
track_type=pyJianYingDraft.TrackType.video,
|
||||
track_name=track_name,
|
||||
)
|
||||
|
||||
# 构建音频片段
|
||||
audio_segment = capcut.AudioSegment(
|
||||
audio_segment = pyJianYingDraft.AudioSegment(
|
||||
material=material_path.as_posix(),
|
||||
target_timerange=trange(
|
||||
target_timerange=pyJianYingDraft.trange(
|
||||
*(
|
||||
target_timerange
|
||||
if target_timerange
|
||||
|
|
@ -194,7 +182,9 @@ class CapCutDraft:
|
|||
)
|
||||
),
|
||||
source_timerange=(
|
||||
trange(*source_timerange) if source_timerange else None
|
||||
pyJianYingDraft.trange(*source_timerange)
|
||||
if source_timerange
|
||||
else None
|
||||
),
|
||||
speed=speed,
|
||||
volume=volume,
|
||||
|
|
@ -205,8 +195,7 @@ class CapCutDraft:
|
|||
|
||||
# 向指定音频轨道添加音频片段
|
||||
self.draft.add_segment(segment=audio_segment, track_name=track_name)
|
||||
print("已完成")
|
||||
return
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
|
@ -223,7 +212,9 @@ class CapCutDraft:
|
|||
speed: Optional[float] = 1.0,
|
||||
volume: Optional[float] = 1.0,
|
||||
clip_settings: Optional[Dict[str, Any]] = None,
|
||||
keyframes: Optional[List[Tuple[KeyframeProperty, str, float]]] = None,
|
||||
keyframes: Optional[
|
||||
List[Tuple[pyJianYingDraft.keyframe, Union[str, int], float]]
|
||||
] = None,
|
||||
animation: Optional[Dict[str, Any]] = None,
|
||||
transition: Optional[Dict[str, Any]] = None,
|
||||
background_filling: Optional[Tuple[str, Any]] = None,
|
||||
|
|
@ -243,11 +234,10 @@ class CapCutDraft:
|
|||
:param background_filling: 背景填充设置,默认为无
|
||||
:return: 无
|
||||
"""
|
||||
print(f"正在向 {track_name}视频轨道添加视频/图片片段...", end="")
|
||||
try:
|
||||
# 添加视频轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.video,
|
||||
track_type=pyJianYingDraft.TrackType.video,
|
||||
track_name=track_name,
|
||||
)
|
||||
|
||||
|
|
@ -257,35 +247,41 @@ class CapCutDraft:
|
|||
)
|
||||
|
||||
# 视频素材
|
||||
video_material = capcut.VideoMaterial(path=material_path.as_posix())
|
||||
video_material = pyJianYingDraft.VideoMaterial(
|
||||
path=material_path.as_posix()
|
||||
)
|
||||
# 视频素材的持续时长
|
||||
video_material_duration = video_material.duration
|
||||
|
||||
duration = 0 # 已添加视频素材的持续时长
|
||||
while duration < target_duration:
|
||||
# 构建视频或图片片段
|
||||
video_segment = capcut.VideoSegment(
|
||||
video_segment = pyJianYingDraft.VideoSegment(
|
||||
material=video_material,
|
||||
target_timerange=trange(
|
||||
target_timerange=pyJianYingDraft.trange(
|
||||
start=duration,
|
||||
duration=min(
|
||||
(target_duration - duration), video_material_duration
|
||||
),
|
||||
),
|
||||
source_timerange=(
|
||||
trange(*source_timerange) if source_timerange else None
|
||||
pyJianYingDraft.trange(*source_timerange)
|
||||
if source_timerange
|
||||
else None
|
||||
),
|
||||
speed=speed,
|
||||
volume=volume,
|
||||
clip_settings=(
|
||||
ClipSettings(**clip_settings) if clip_settings else None
|
||||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||||
if clip_settings
|
||||
else None
|
||||
),
|
||||
)
|
||||
# 添加关键帧
|
||||
if keyframes:
|
||||
# noinspection PyShadowingBuiltins
|
||||
for property, time, value in keyframes:
|
||||
video_segment.add_keyframe(property, time, value)
|
||||
for property, offset, value in keyframes:
|
||||
video_segment.add_keyframe(property, offset, value)
|
||||
|
||||
# 添加动画
|
||||
if animation:
|
||||
|
|
@ -304,8 +300,6 @@ class CapCutDraft:
|
|||
|
||||
duration += video_material_duration
|
||||
|
||||
print("已完成")
|
||||
return
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
|
@ -326,18 +320,17 @@ class CapCutDraft:
|
|||
:param clip_settings: 图像调节设置,默认为无
|
||||
:return: 无
|
||||
"""
|
||||
print(f"正在向 {track_name}贴纸轨道添加贴纸片段...", end="")
|
||||
try:
|
||||
# 添加贴纸轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.sticker,
|
||||
track_type=pyJianYingDraft.TrackType.sticker,
|
||||
track_name=track_name,
|
||||
)
|
||||
|
||||
# 构建贴纸
|
||||
sticker_segment = capcut.StickerSegment(
|
||||
sticker_segment = pyJianYingDraft.StickerSegment(
|
||||
resource_id=resource_id, # 可先将贴纸保存为我的预设,再在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取
|
||||
target_timerange=trange(
|
||||
target_timerange=pyJianYingDraft.trange(
|
||||
*(
|
||||
target_timerange
|
||||
if target_timerange
|
||||
|
|
@ -345,13 +338,15 @@ class CapCutDraft:
|
|||
)
|
||||
),
|
||||
clip_settings=(
|
||||
capcut.ClipSettings(**clip_settings) if clip_settings else None
|
||||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||||
if clip_settings
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
# 向指定贴纸轨道添加贴纸片段
|
||||
self.draft.add_segment(segment=sticker_segment, track_name=track_name)
|
||||
print("已完成")
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
|
@ -378,15 +373,14 @@ class CapCutDraft:
|
|||
:param effect: 花字设置,默认为无
|
||||
:return: 无
|
||||
"""
|
||||
print("正在添加字幕...")
|
||||
# 添加文本轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.text,
|
||||
track_type=pyJianYingDraft.TrackType.text,
|
||||
track_name=(text_track_name := "subtitles(text)"),
|
||||
)
|
||||
# 添加音频轨道
|
||||
self.draft.add_track(
|
||||
track_type=capcut.TrackType.audio,
|
||||
track_type=pyJianYingDraft.TrackType.audio,
|
||||
track_name=(audio_track_name := "subtitles(audio)"),
|
||||
)
|
||||
|
||||
|
|
@ -415,7 +409,7 @@ class CapCutDraft:
|
|||
**(style or {}),
|
||||
},
|
||||
clip_settings={
|
||||
"transform_y": -0.5,
|
||||
"transform_y": -0.75,
|
||||
**(clip_settings or {}),
|
||||
},
|
||||
effect=effect,
|
||||
|
|
@ -426,20 +420,17 @@ class CapCutDraft:
|
|||
add_track=False,
|
||||
material_path=file_path,
|
||||
target_timerange=(start, duration),
|
||||
volume=1.5,
|
||||
)
|
||||
start += duration
|
||||
|
||||
# 更新草稿持续时长
|
||||
self.draft_duration = start
|
||||
print("已完成")
|
||||
return
|
||||
|
||||
def save(self) -> None:
|
||||
"""将草稿保存至 CapCut草稿文件夹内"""
|
||||
print("正在将草稿保存至 CapCut草稿文件夹内...", end="")
|
||||
"""将草稿保存至剪映草稿文件夹内"""
|
||||
try:
|
||||
self.draft.save()
|
||||
print("已完成")
|
||||
return
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
# -*- 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
|
||||
|
||||
|
||||
class JianYingExport:
|
||||
"""
|
||||
封装 pyJianYing中导出草稿的相关功能,支持:
|
||||
1、初始化素材文件夹内所有素材
|
||||
2、就工作流添加工作
|
||||
3、基于工作流生成草稿
|
||||
"""
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def __init__(
|
||||
self,
|
||||
materials_folder_path: str,
|
||||
jianying_program_path: str = r"E:\JianyingPro\5.9.0.11632\JianyingPro.exe",
|
||||
draft_counts: int = 10,
|
||||
video_width: int = 1080,
|
||||
video_height: int = 1920,
|
||||
video_fps: int = 30,
|
||||
):
|
||||
"""
|
||||
初始化
|
||||
:param jianying_program_path: 剪映执行程序路径
|
||||
:param materials_folder_path: 素材文件夹路径
|
||||
:param draft_counts: 草稿数,默认为 10
|
||||
:param video_width: 视频宽度,默认为 1080像素
|
||||
:param video_height: 视频高度,默认为 1920像素
|
||||
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
|
||||
"""
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.jianying_program_path = Path(jianying_program_path)
|
||||
if not self.jianying_program_path.exists():
|
||||
raise RuntimeError("剪映执行程序路径不存在")
|
||||
|
||||
# 初始化剪映专业版进程
|
||||
self.jianying_process = None
|
||||
|
||||
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(exist_ok=True)
|
||||
|
||||
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)}")
|
||||
|
||||
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 _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(
|
||||
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 export(self):
|
||||
"""
|
||||
导出草稿
|
||||
"""
|
||||
# 按照工作流和工作配置拼接素材,生成草稿
|
||||
self._generate()
|
||||
|
||||
# 启动剪映专业版进程
|
||||
self._start_process()
|
||||
time.sleep(10)
|
||||
|
||||
# 初始化剪映控制器
|
||||
jianying_controller = pyJianYingDraft.JianyingController()
|
||||
|
||||
for draft_name in self.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()
|
||||
|
||||
def _start_process(self, timeout: int = 60) -> None:
|
||||
"""
|
||||
启动剪映专业版进程
|
||||
:param timeout: 最大等待时间(单位为秒),默认为 60
|
||||
:return: 无
|
||||
"""
|
||||
try:
|
||||
# 关闭剪映专业版进程
|
||||
self._close_process()
|
||||
|
||||
# 非堵塞方法
|
||||
self.jianying_process = subprocess.Popen(
|
||||
args=self.jianying_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)}")
|
||||
|
||||
def _close_process(self, timeout: int = 60) -> None:
|
||||
"""
|
||||
关闭剪映专业版进程
|
||||
:param timeout: 最大等待时间(单位为秒),默认为 60
|
||||
:return: 无
|
||||
"""
|
||||
try:
|
||||
# 定位剪映执行程序窗口
|
||||
window_handle = self._locate_window()
|
||||
if window_handle is not None:
|
||||
# 请求关闭剪映执行程序窗口
|
||||
win32gui.SendMessage(window_handle, win32con.WM_CLOSE, 0, 0)
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if not win32gui.IsWindow(window_handle):
|
||||
print("已关闭剪映专业版进程")
|
||||
return
|
||||
else:
|
||||
time.sleep(2)
|
||||
raise RuntimeError("关闭超时")
|
||||
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"关闭剪映专业版进程发生异常:{str(exception)}")
|
||||
|
||||
@staticmethod
|
||||
def _locate_window() -> Optional[int]:
|
||||
"""
|
||||
定位剪映执行程序窗口
|
||||
:return: 剪映执行程序窗口句柄
|
||||
"""
|
||||
window_handle = None
|
||||
|
||||
def callback(handle, _):
|
||||
"""
|
||||
遍历所有窗口的回调函数
|
||||
"""
|
||||
# 初始化窗口句柄
|
||||
nonlocal window_handle
|
||||
# 获取窗口标题
|
||||
window_text = win32gui.GetWindowText(handle)
|
||||
# 检查窗口是否可见且窗口标题为剪映专业版
|
||||
if win32gui.IsWindowVisible(handle) and window_text == "剪映专业版":
|
||||
window_handle = handle
|
||||
return False
|
||||
return True
|
||||
|
||||
# 遍历所有顶层窗口
|
||||
win32gui.EnumWindows(callback, None)
|
||||
return window_handle
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
短视频合成自动化
|
||||
"""
|
||||
|
||||
from export import JianYingExport
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 实例化 JianYingExport
|
||||
jianying_export = JianYingExport(
|
||||
materials_folder_path=r"E:\jianying\materials\260104",
|
||||
draft_counts=1,
|
||||
)
|
||||
|
||||
# 导出草稿
|
||||
jianying_export.export()
|
||||
Loading…
Reference in New Issue