442 lines
17 KiB
Python
442 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
剪映草稿模块
|
||
"""
|
||
|
||
# 列举导入模块
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||
|
||
from pyJianYingDraft import (
|
||
AudioMaterial,
|
||
AudioSegment,
|
||
ClipSettings,
|
||
ClipSettings,
|
||
DraftFolder,
|
||
FontType,
|
||
KeyframeProperty,
|
||
StickerSegment,
|
||
TextBackground,
|
||
TextBorder,
|
||
TextSegment,
|
||
TextStyle,
|
||
TrackType,
|
||
VideoMaterial,
|
||
VideoSegment,
|
||
trange,
|
||
)
|
||
from pyJianYingDraft.time_util import tim
|
||
|
||
from edgetts import EdgeTTS
|
||
|
||
|
||
class Drafts:
|
||
"""
|
||
剪映草稿,支持:
|
||
1 添加文本片段
|
||
2 添加音频片段
|
||
3 添加视频或图片片段
|
||
4 添加贴纸片段
|
||
5 生成字幕
|
||
6 保存剪映草稿
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
materials_folder_path: Path,
|
||
drafts_folder: DraftFolder,
|
||
draft_name: str,
|
||
video_width: int = 1080,
|
||
video_height: int = 1920,
|
||
video_fps: int = 30,
|
||
video_duration: int = 0,
|
||
):
|
||
"""
|
||
初始化
|
||
:param materials_folder_path: 素材文件夹路径
|
||
:param drafts_folder: 剪映草稿文件夹管理器
|
||
:param draft_name: 草稿名称
|
||
:param video_width: 视频宽度(单位为像素),默认为 1080
|
||
:param video_height: 视频高度(单位为像素),默认为 1920
|
||
:param video_fps: 视频帧率(单位为帧/秒),默认为 30
|
||
:param video_duration: 视频持续时长(单位为微秒),默认为 0
|
||
"""
|
||
try:
|
||
# 初始化素材文件夹路径
|
||
self.materials_folder_path = materials_folder_path
|
||
if not self.materials_folder_path.exists():
|
||
raise FileNotFoundError(f"素材文件夹路径不存在")
|
||
|
||
# 创建剪映草稿
|
||
self.draft = drafts_folder.create_draft(
|
||
draft_name=draft_name,
|
||
allow_replace=True, # 允许覆盖与 draft_name 重名的剪映草稿
|
||
width=video_width,
|
||
height=video_height,
|
||
fps=video_fps,
|
||
)
|
||
|
||
# 初始化视频持续时长
|
||
self.video_duration = video_duration
|
||
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=TrackType.text,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 构建文本片段
|
||
text_segment = TextSegment(
|
||
text=text.replace("\\n", "\n"),
|
||
timerange=trange(
|
||
*(timerange if timerange else (0, self.video_duration))
|
||
),
|
||
font=FontType(font) if font else None,
|
||
style=TextStyle(**style) if style else None,
|
||
border=TextBorder(**border) if border else None,
|
||
background=(TextBackground(**background) if background else None),
|
||
clip_settings=(
|
||
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:
|
||
# 音频素材
|
||
audio_material = AudioMaterial(path=material_path.as_posix())
|
||
# 音频素材的持续时长
|
||
audio_material_duration = audio_material.duration
|
||
# 目标持续时间
|
||
target_duration = tim(
|
||
(target_timerange if target_timerange else (0, self.video_duration))[1]
|
||
)
|
||
|
||
if add_track:
|
||
# 添加音频轨道
|
||
self.draft.add_track(
|
||
track_type=TrackType.audio,
|
||
track_name=track_name,
|
||
)
|
||
|
||
cumulative_duration = 0 # 累计持续时长
|
||
while cumulative_duration < target_duration:
|
||
# 构建音频片段
|
||
audio_segment = AudioSegment(
|
||
material=audio_material,
|
||
target_timerange=trange(
|
||
start=cumulative_duration,
|
||
duration=min(
|
||
(target_duration - cumulative_duration),
|
||
audio_material_duration,
|
||
),
|
||
),
|
||
source_timerange=(
|
||
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)
|
||
|
||
cumulative_duration += audio_material_duration
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|
||
|
||
def add_video_segment(
|
||
self,
|
||
track_name: str,
|
||
material_path: Path,
|
||
target_timerange: Optional[Tuple[int, Optional[int]]] = None,
|
||
source_timerange: Optional[Tuple[int, int]] = None,
|
||
speed: float = 1.0,
|
||
volume: float = 1.0,
|
||
clip_settings: Optional[Dict[str, Any]] = None,
|
||
keyframes: Optional[
|
||
List[Tuple[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:
|
||
# 视频素材
|
||
video_material = VideoMaterial(path=material_path.as_posix())
|
||
# 视频素材的持续时长
|
||
video_material_duration = video_material.duration
|
||
# 目标持续时间
|
||
target_duration = (
|
||
tim(target_duration)
|
||
if (target_timerange and (target_duration := target_timerange[1]))
|
||
else (
|
||
video_material_duration if target_timerange else self.video_duration
|
||
)
|
||
) # 若视频或图片素材在轨道上的范围为空则将视频素材持续时长作为目标持续时长,若视频或图片素材在轨道上的范围中持续时长为空则将视频素材持续时长作为目标持续时长
|
||
|
||
# 添加视频轨道
|
||
self.draft.add_track(
|
||
track_type=TrackType.video,
|
||
track_name=track_name,
|
||
)
|
||
|
||
cumulative_duration = 0 # 累计持续时长
|
||
while cumulative_duration < target_duration:
|
||
# 构建视频或图片片段
|
||
video_segment = VideoSegment(
|
||
material=video_material,
|
||
target_timerange=trange(
|
||
start=cumulative_duration
|
||
+ (target_timerange[0] if target_timerange else 0),
|
||
duration=min(
|
||
(target_duration - cumulative_duration),
|
||
video_material_duration,
|
||
),
|
||
),
|
||
source_timerange=(
|
||
trange(*source_timerange) if source_timerange else None
|
||
),
|
||
speed=speed,
|
||
volume=volume,
|
||
clip_settings=(
|
||
ClipSettings(**clip_settings) if clip_settings else None
|
||
),
|
||
)
|
||
# 添加关键帧
|
||
if keyframes:
|
||
for keyframe_property, keyframe_offset, keyframe_value in keyframes:
|
||
video_segment.add_keyframe(
|
||
keyframe_property, keyframe_offset, keyframe_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)
|
||
|
||
cumulative_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=TrackType.sticker,
|
||
track_name=track_name,
|
||
)
|
||
|
||
# 构建贴纸
|
||
# 将贴纸保存为我的预设后在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取
|
||
sticker_segment = StickerSegment(
|
||
resource_id=resource_id,
|
||
target_timerange=trange(
|
||
*(
|
||
target_timerange
|
||
if target_timerange
|
||
else (0, self.video_duration)
|
||
)
|
||
),
|
||
clip_settings=(
|
||
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 generate_subtitle(
|
||
self,
|
||
texts: str,
|
||
timbre: str = "zh-CN-XiaoxiaoNeural",
|
||
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 texts: 字幕文本
|
||
: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=TrackType.text,
|
||
track_name=(text_track_name := "subtitle(text)"),
|
||
)
|
||
# 添加音频轨道
|
||
self.draft.add_track(
|
||
track_type=TrackType.audio,
|
||
track_name=(audio_track_name := "subtitle(audio)"),
|
||
)
|
||
|
||
# 构建字幕音频文件夹路径
|
||
subtitle_folder_path = self.materials_folder_path / "字幕音频"
|
||
subtitle_folder_path.mkdir(exist_ok=True)
|
||
|
||
# 实例化 EdgeTTS
|
||
edge_tts = EdgeTTS(folder_path=subtitle_folder_path)
|
||
|
||
cumulative_duration = 0 # 累计持续时长
|
||
for text in texts.split(","):
|
||
# 根据字幕文本片段合成语音并将语音文件保存至指定文件夹内
|
||
subtitle_audio_path, duration = edge_tts.synthetize(
|
||
text=text, timbre=timbre, rate=rate, volume=volume
|
||
)
|
||
# 向指定文本轨道添加文本片段
|
||
self.add_text_segment(
|
||
track_name=text_track_name,
|
||
add_track=False, # 不添加文本轨道
|
||
text=text,
|
||
timerange=(cumulative_duration, 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=subtitle_audio_path,
|
||
target_timerange=(cumulative_duration, duration),
|
||
)
|
||
cumulative_duration += duration
|
||
|
||
# 以累计持续时长作为视频持续时长
|
||
self.video_duration = cumulative_duration
|
||
|
||
def save(self) -> None:
|
||
"""将草稿保存至剪映草稿文件夹内"""
|
||
try:
|
||
self.draft.save()
|
||
except Exception as exception:
|
||
raise RuntimeError(str(exception)) from exception
|