426 lines
16 KiB
Python
426 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
生成草稿模块
|
||
"""
|
||
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||
|
||
import pyJianYingDraft
|
||
from edgetts import EdgeTTS
|
||
|
||
|
||
class JianYingDraft:
|
||
"""
|
||
封装 pyJianYing中生成草稿的相关功能,支持:
|
||
1、向指定文本轨道添加文本片段
|
||
2、向指定音频轨道添加音频片段
|
||
3、向指定视频轨道添加视频或图片片段
|
||
4、向指定贴纸轨道添加贴纸片段
|
||
5、根据文本逐段合成语音,生成文本和语音字幕
|
||
6、将草稿保存至剪映草稿文件夹内
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
drafts_folder: pyJianYingDraft.DraftFolder,
|
||
draft_name: str,
|
||
materials_folder_path: Path,
|
||
allow_replace: bool = True,
|
||
video_width: int = 1080,
|
||
video_height: int = 1920,
|
||
video_fps: int = 30,
|
||
):
|
||
"""
|
||
初始化
|
||
:param drafts_folder: 剪映草稿文件夹管理器
|
||
:param draft_name: 草稿名称
|
||
:param allow_replace: 是否允许覆盖同名草稿(若不允许覆盖同名草稿需初始化剪映草稿文件夹)
|
||
:param video_width: 视频宽度,默认为 1080像素
|
||
:param video_height: 视频高度,默认为 1920像素
|
||
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
|
||
:param materials_folder_path: 素材文件夹路径
|
||
"""
|
||
try:
|
||
# 新建草稿
|
||
self.draft = drafts_folder.create_draft(
|
||
draft_name=draft_name,
|
||
allow_replace=allow_replace,
|
||
width=video_width,
|
||
height=video_height,
|
||
fps=video_fps,
|
||
)
|
||
# 草稿持续时长(单位为毫秒)
|
||
self.draft_duration = 0
|
||
|
||
self.materials_folder_path = materials_folder_path
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(f"发生异常:{str(exception)}") from exception
|
||
|
||
def add_text_segment(
|
||
self,
|
||
track_name: str,
|
||
text: str,
|
||
add_track: bool = True,
|
||
timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None,
|
||
font: Optional[str] = None,
|
||
style: Optional[Dict[str, Any]] = None,
|
||
border: Optional[Dict[str, Any]] = None,
|
||
background: Optional[Dict[str, Any]] = None,
|
||
clip_settings: Optional[Dict[str, Any]] = None,
|
||
bubble: Optional[Dict[str, Any]] = None,
|
||
effect: Optional[Dict[str, Any]] = None,
|
||
animation: Optional[Dict[str, Any]] = None,
|
||
) -> None:
|
||
"""
|
||
向指定文本轨道添加文本片段
|
||
:param track_name: 轨道名称
|
||
:param add_track: 添加文本轨道,默认为是
|
||
:param text: 文本
|
||
:param timerange: 文本素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长
|
||
:param font: 字体,默认为系统
|
||
:param style: 字体样式,默认为字号 15,对齐方式 左对齐
|
||
:param border: 文本描边设置,默认为无
|
||
:param background: 文本背景设置,默认为无
|
||
:param clip_settings: 图像调节设置,默认为移动至 (0, 0)
|
||
:param bubble: 气泡设置,默认为无
|
||
:param effect: 花字设置,默认为无
|
||
:param animation: 动画设置,默认为无
|
||
:return: 无
|
||
"""
|
||
try:
|
||
if add_track:
|
||
# 添加文本轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.text,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 构建文本片段
|
||
text_segment = pyJianYingDraft.TextSegment(
|
||
text=text.replace("\\n", "\n"),
|
||
timerange=pyJianYingDraft.trange(
|
||
*(timerange if timerange else (0, self.draft_duration))
|
||
),
|
||
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=(
|
||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||
if clip_settings
|
||
else None
|
||
),
|
||
)
|
||
# 添加气泡
|
||
if bubble:
|
||
text_segment.add_bubble(**bubble)
|
||
|
||
# 添加花字
|
||
# 将花字保存预设后在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id
|
||
if effect:
|
||
text_segment.add_effect(**effect)
|
||
|
||
# 添加动画
|
||
if animation:
|
||
text_segment.add_animation(**animation)
|
||
|
||
# 向指定文本轨道添加文本片段
|
||
self.draft.add_segment(segment=text_segment, track_name=track_name)
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|
||
|
||
def add_audio_segment(
|
||
self,
|
||
track_name: str,
|
||
material_path: Path,
|
||
add_track: bool = True,
|
||
target_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None,
|
||
source_timerange: Optional[Tuple[str, str]] = None,
|
||
speed: float = 1.0,
|
||
volume: float = 1.0,
|
||
fade: Optional[Tuple[str, str]] = None,
|
||
) -> None:
|
||
"""
|
||
向指定音频轨道添加音频片段
|
||
:param track_name: 轨道名称
|
||
:param add_track: 添加音频轨道,默认为是
|
||
:param material_path: 音频素材路径
|
||
:param target_timerange: 音频素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长
|
||
:param source_timerange: 截取音频素材范围,包括开始时间和持续时长,默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分
|
||
:param speed: 播放速度,默认为 1.0
|
||
:param volume: 播放音量,默认为 1.0
|
||
:param fade: 淡入淡出设置,默认为无
|
||
:return: 无
|
||
"""
|
||
try:
|
||
if add_track:
|
||
# 添加音频轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.video,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 构建音频片段
|
||
audio_segment = pyJianYingDraft.AudioSegment(
|
||
material=material_path.as_posix(),
|
||
target_timerange=pyJianYingDraft.trange(
|
||
*(
|
||
target_timerange
|
||
if target_timerange
|
||
else (0, self.draft_duration)
|
||
)
|
||
),
|
||
source_timerange=(
|
||
pyJianYingDraft.trange(*source_timerange)
|
||
if source_timerange
|
||
else None
|
||
),
|
||
speed=speed,
|
||
volume=volume,
|
||
)
|
||
# 添加淡入淡出
|
||
if fade:
|
||
audio_segment.add_fade(*fade)
|
||
|
||
# 向指定音频轨道添加音频片段
|
||
self.draft.add_segment(segment=audio_segment, track_name=track_name)
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|
||
|
||
# pylint: disable=too-many-locals
|
||
def add_video_segment(
|
||
self,
|
||
track_name: str,
|
||
material_path: Path,
|
||
target_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None,
|
||
source_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None,
|
||
speed: float = 1.0,
|
||
volume: float = 1.0,
|
||
clip_settings: Optional[Dict[str, Any]] = None,
|
||
keyframes: Optional[
|
||
List[Tuple[pyJianYingDraft.KeyframeProperty, Union[str, int], float]]
|
||
] = None,
|
||
animation: Optional[Dict[str, Any]] = None,
|
||
transition: Optional[Dict[str, Any]] = None,
|
||
background_filling: Optional[Dict[str, Any]] = None,
|
||
) -> None:
|
||
"""
|
||
向指定视频轨道添加视频或图片片段
|
||
:param track_name: 轨道名称
|
||
:param material_path: 视频或图片素材路径
|
||
:param target_timerange: 视频或图片素材在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长
|
||
:param source_timerange: 截取视频或图片素材范围,包括开始时间和持续时长,默认为空
|
||
:param speed: 播放速度,默认为 1.0
|
||
:param volume: 播放音量,默认为 1.0
|
||
:param clip_settings: 图像调节设置,默认为无
|
||
:param keyframes: 关键帧设置,默认为无
|
||
:param animation: 动画设置,默认为无
|
||
:param transition: 转场设置,默认为无
|
||
:param background_filling: 背景填充设置,默认为无
|
||
:return: 无
|
||
"""
|
||
try:
|
||
# 添加视频轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.video,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 获取持续时间
|
||
target_duration = pyJianYingDraft.time_util.tim(
|
||
(target_timerange if target_timerange else (0, self.draft_duration))[1]
|
||
)
|
||
|
||
# 视频素材
|
||
video_material = pyJianYingDraft.VideoMaterial(
|
||
path=material_path.as_posix()
|
||
)
|
||
# 视频素材的持续时长
|
||
video_material_duration = video_material.duration
|
||
|
||
duration = 0 # 已添加视频素材的持续时长
|
||
while duration < target_duration:
|
||
# 构建视频或图片片段
|
||
video_segment = pyJianYingDraft.VideoSegment(
|
||
material=video_material,
|
||
target_timerange=pyJianYingDraft.trange(
|
||
start=duration,
|
||
duration=min(
|
||
(target_duration - duration), video_material_duration
|
||
),
|
||
),
|
||
source_timerange=(
|
||
pyJianYingDraft.trange(*source_timerange)
|
||
if source_timerange
|
||
else None
|
||
),
|
||
speed=speed,
|
||
volume=volume,
|
||
clip_settings=(
|
||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||
if clip_settings
|
||
else None
|
||
),
|
||
)
|
||
# 添加关键帧
|
||
if keyframes:
|
||
for _property, offset, value in keyframes:
|
||
video_segment.add_keyframe(_property, offset, value)
|
||
|
||
# 添加动画
|
||
if animation:
|
||
video_segment.add_animation(**animation)
|
||
|
||
# 添加转场
|
||
if transition:
|
||
video_segment.add_transition(**transition)
|
||
|
||
# 添加背景填充
|
||
if background_filling:
|
||
video_segment.add_background_filling(**background_filling)
|
||
|
||
# 向指定视频轨道添加视频或图片片段
|
||
self.draft.add_segment(segment=video_segment, track_name=track_name)
|
||
|
||
duration += video_material_duration
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|
||
|
||
def add_sticker(
|
||
self,
|
||
track_name: str,
|
||
resource_id: str,
|
||
target_timerange: Optional[Tuple[Union[int, str], Union[int, str]]] = None,
|
||
clip_settings: Optional[Dict[str, Any]] = None,
|
||
) -> None:
|
||
"""
|
||
向指定贴纸轨道添加贴纸片段
|
||
:param track_name: 轨道名称
|
||
:param resource_id: 贴纸资源标识
|
||
:param target_timerange: 贴纸在轨道上的范围,包括开始时间和持续时长,默认为草稿持续时长
|
||
:param clip_settings: 图像调节设置,默认为无
|
||
:return: 无
|
||
"""
|
||
try:
|
||
# 添加贴纸轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.sticker,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 构建贴纸
|
||
# 将贴纸保存为我的预设后在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取
|
||
sticker_segment = pyJianYingDraft.StickerSegment(
|
||
resource_id=resource_id,
|
||
target_timerange=pyJianYingDraft.trange(
|
||
*(
|
||
target_timerange
|
||
if target_timerange
|
||
else (0, self.draft_duration)
|
||
)
|
||
),
|
||
clip_settings=(
|
||
pyJianYingDraft.ClipSettings(**clip_settings)
|
||
if clip_settings
|
||
else None
|
||
),
|
||
)
|
||
|
||
# 向指定贴纸轨道添加贴纸片段
|
||
self.draft.add_segment(segment=sticker_segment, track_name=track_name)
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|
||
|
||
def add_subtitles(
|
||
self,
|
||
text: str,
|
||
timbre: str = "女声-晓晓",
|
||
rate: str = "+25%",
|
||
volume: str = "+0%",
|
||
font: Optional[str] = None,
|
||
style: Optional[Dict[str, Any]] = {"size": 12.0, "align": "center"},
|
||
clip_settings: Optional[Dict[str, Any]] = None,
|
||
effect: Optional[Dict[str, Any]] = None,
|
||
):
|
||
"""
|
||
添加字幕
|
||
:param text: 文本
|
||
:param timbre: 声音音色,默认为女声-晓晓
|
||
:param rate: 语速,默认为 +25%
|
||
:param volume: 音量,默认为 +0%
|
||
:param font: 字体,默认为系统
|
||
:param style: 文本样式,默认为字号 12,对齐方式 水平居中
|
||
:param clip_settings: 图像调节设置,默认为移动至 (0, -0.5)
|
||
:param effect: 花字设置,默认为无
|
||
:return: 无
|
||
"""
|
||
# 添加文本轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.text,
|
||
track_name=(text_track_name := "subtitles(text)"),
|
||
)
|
||
# 添加音频轨道
|
||
self.draft.add_track(
|
||
track_type=pyJianYingDraft.TrackType.audio,
|
||
track_name=(audio_track_name := "subtitles(audio)"),
|
||
)
|
||
|
||
# 构造语音文件保存文件夹路径(path对象)
|
||
subtitles_folder_path = self.materials_folder_path / "subtitles"
|
||
subtitles_folder_path.mkdir(exist_ok=True)
|
||
# 实例化 EdgeTTS
|
||
edge_tts = EdgeTTS(folder_path=subtitles_folder_path)
|
||
|
||
start = 0
|
||
for paragraph in text.split(","):
|
||
# 根据文本合成语音并将语音文件保存至指定文件夹内
|
||
file_path, duration = edge_tts.synthetize(
|
||
text=paragraph, timbre=timbre, rate=rate, volume=volume
|
||
)
|
||
# 向指定文本轨道添加文本片段
|
||
self.add_text_segment(
|
||
track_name=text_track_name,
|
||
add_track=False,
|
||
text=paragraph,
|
||
timerange=(start, duration),
|
||
font=font,
|
||
style={
|
||
"size": 12.0,
|
||
"align": 1,
|
||
**(style or {}),
|
||
},
|
||
clip_settings={
|
||
"transform_y": -0.6,
|
||
**(clip_settings or {}),
|
||
},
|
||
effect=effect,
|
||
)
|
||
# 向指定音频轨道添加音频片段
|
||
self.add_audio_segment(
|
||
track_name=audio_track_name,
|
||
add_track=False,
|
||
material_path=file_path,
|
||
target_timerange=(start, duration),
|
||
volume=1.5,
|
||
)
|
||
start += duration
|
||
|
||
# 更新草稿持续时长
|
||
self.draft_duration = start
|
||
|
||
def save(self) -> None:
|
||
"""将草稿保存至剪映草稿文件夹内"""
|
||
try:
|
||
self.draft.save()
|
||
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|