日常更新

from NUC
This commit is contained in:
liubiren 2025-12-31 22:02:04 +08:00
parent e5330d4195
commit e8a0f5d414
7 changed files with 771 additions and 586 deletions

View File

@ -0,0 +1,445 @@
# -*- coding: utf-8 -*-
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
from edgetts import EdgeTTS
class CapCutDraft:
"""
封装pyCapCut支持
1向指定文本轨道添加文本片段
2向指定音频轨道添加音频片段
3向指定视频轨道添加视频或图片片段
4向指定贴纸轨道添加贴纸片段
5根据文本逐段合成语音生成文本和语音字幕
6将草稿保存至 CapCut草稿文件夹内
导出尚未支持
"""
# 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",
allow_replace: bool = True,
video_width: int = 1080,
video_height: int = 1920,
video_fps: int = 30,
):
"""
初始化
:param capcut_folder_path: CapCut草稿文件夹路径
:param draft_name: 草稿名称
:param allow_replace: 是否允许覆盖同名草稿
:param video_width: 视频宽度默认为 1080像素
:param video_height: 视频高度默认为 1920像素
:param video_fps: 视频帧率单位为帧/默认为 30
:param materials_folder_path: 素材文件夹路径
"""
print("正在初始化 CapCutDraft...", end="")
# noinspection PyBroadException
try:
# 初始化草稿文件夹管理器
self.draft_folder = capcut.DraftFolder(capcut_folder_path)
# 新建草稿
self.draft = self.draft_folder.create_draft(
draft_name=draft_name,
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("素材文件夹路径不存在")
print("已完成")
except Exception as exception:
raise RuntimeError(f"发生异常:{str(exception)}")
def add_text_segment(
self,
track_name: str,
text: str,
add_track: bool = True,
timerange: Optional[Tuple[Optional[int, str], Optional[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:
"""
print(f"正在向 {track_name}文本轨道添加文本片段...", end="")
try:
if add_track:
# 添加文本轨道
self.draft.add_track(
track_type=capcut.TrackType.text,
track_name=track_name,
)
# 构建文本片段
text_segment = capcut.TextSegment(
text=text.replace("\\n", "\n"),
timerange=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,
clip_settings=(
capcut.ClipSettings(**clip_settings) if clip_settings else None
),
)
# 添加气泡
if bubble:
text_segment.add_bubble(**bubble)
# 添加花字
if effect:
text_segment.add_effect(
**effect
) # 可先将花字保存预设再在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id
# 添加动画
if animation:
text_segment.add_animation(**animation)
# 向指定文本轨道添加文本片段
self.draft.add_segment(segment=text_segment, track_name=track_name)
print("已完成")
return
except Exception:
raise
def add_audio_segment(
self,
track_name: str,
material_path: Path,
add_track: bool = True,
target_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]]
] = None,
source_timerange: Optional[Tuple[str, str]] = None,
speed: Optional[float] = 1.0,
volume: Optional[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:
"""
print(f"正在向 {track_name}音频轨道添加音频片段...", end="")
try:
if add_track:
# 添加音频轨道
self.draft.add_track(
track_type=capcut.TrackType.video,
track_name=track_name,
)
# 构建音频片段
audio_segment = capcut.AudioSegment(
material=material_path.as_posix(),
target_timerange=trange(
*(
target_timerange
if target_timerange
else (0, self.draft_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)
print("已完成")
return
except Exception:
raise
def add_video_segment(
self,
track_name: str,
material_path: Path,
target_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]]
] = None,
source_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]],
] = None,
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,
animation: Optional[Dict[str, Any]] = None,
transition: Optional[Dict[str, Any]] = None,
background_filling: Optional[Tuple[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:
"""
print(f"正在向 {track_name}视频轨道添加视频/图片片段...", end="")
try:
# 添加视频轨道
self.draft.add_track(
track_type=capcut.TrackType.video,
track_name=track_name,
)
# 解析开始时间和持续时间
target_start, target_duration = (
target_timerange if target_timerange else (0, self.draft_duration)
)
# 视频素材
video_material = capcut.VideoMaterial(path=material_path.as_posix())
# 视频素材的持续时长
video_material_duration = video_material.duration
duration = 0 # 已添加视频素材的持续时长
while duration < target_duration:
# 构建视频或图片片段
video_segment = capcut.VideoSegment(
material=video_material,
target_timerange=trange(
start=duration,
duration=min(
(target_duration - 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:
# noinspection PyShadowingBuiltins
for property, time, value in keyframes:
video_segment.add_keyframe(property, time, 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
print("已完成")
return
except Exception:
raise
def add_sticker(
self,
track_name: str,
resource_id: str,
target_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]]
] = None,
clip_settings: Optional[Dict[str, Any]] = None,
) -> None:
"""
向指定贴纸轨道添加贴纸片段
:param track_name: 轨道名称
:param resource_id: 贴纸资源标识
:param target_timerange: 贴纸在轨道上的范围包括开始时间和持续时长默认为草稿持续时长
:param clip_settings: 图像调节设置默认为无
:return:
"""
print(f"正在向 {track_name}贴纸轨道添加贴纸片段...", end="")
try:
# 添加贴纸轨道
self.draft.add_track(
track_type=capcut.TrackType.sticker,
track_name=track_name,
)
# 构建贴纸
sticker_segment = capcut.StickerSegment(
resource_id=resource_id, # 可先将贴纸保存为我的预设再在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取
target_timerange=trange(
*(
target_timerange
if target_timerange
else (0, self.draft_duration)
)
),
clip_settings=(
capcut.ClipSettings(**clip_settings) if clip_settings else None
),
)
# 向指定贴纸轨道添加贴纸片段
self.draft.add_segment(segment=sticker_segment, track_name=track_name)
print("已完成")
except Exception:
raise
def add_subtitles(
self,
text: str,
timbre: Optional[str] = "女声-晓晓",
rate: str = "+25%",
volume: str = "+0%",
font: Optional[str] = None,
style: Optional[Dict[str, Any]] = None,
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:
"""
print("正在添加字幕...")
# 添加文本轨道
self.draft.add_track(
track_type=capcut.TrackType.text,
track_name=(text_track_name := "subtitles(text)"),
)
# 添加音频轨道
self.draft.add_track(
track_type=capcut.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.5,
**(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),
)
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

View File

@ -1,544 +0,0 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import pycapcut as capcut
from pycapcut import trange
from pycapcut.keyframe import KeyframeProperty
from pycapcut.segment import ClipSettings
from edgetts import EdgeTTS
class GenerateDraft:
"""剪映脚本生成器"""
# noinspection PyShadowingNames
def __init__(
self,
name: str,
video_width: int = 1080,
video_height: int = 1920,
video_fps: int = 30,
drafts_path: str = r"C:\Users\admin\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft",
materials_path: str = r"C:\Users\admin\PycharmProjects\Python\剪映脚本生成自动化\materials",
):
"""
初始化生成剪映脚本
:param name: 草稿名称
:param video_width: 视频宽度
:param video_height: 视频高度
:param video_fps: 视频帧率
:param drafts_path: 剪映草稿文件夹路径
:param materials_path: 素材文件夹路径
"""
print("正在初始化剪映脚本生成器...", end="")
# noinspection PyBroadException
try:
# 草稿名称
self.name = name
# 视频宽度和高度
self.video_width, self.video_height = video_width, video_height
# 视频帧率
self.video_fps = video_fps
# 剪映草稿文件夹路径
self.drafts_path = drafts_path
# 素材文件夹路径
self.materials_path = Path(materials_path) # 转为path对象
# 检查素材文件夹是否存在,若不存在则抛出异常
if not self.materials_path.exists():
raise FileNotFoundError(f"素材文件夹不存在")
# 初始化草稿文件夹管理器
self.draft_folder = capcut.DraftFolder(self.drafts_path)
# 新建草稿
self.draft = self.draft_folder.create_draft(
draft_name=self.name,
width=self.video_width,
height=self.video_height,
fps=self.video_fps,
allow_replace=True, # 允许覆盖
)
# 脚本持续时长
self.duration = 0
# 实例化EdgeTTS
self.synthesizer = EdgeTTS(self.materials_path)
print("已完成")
except Exception as exception:
print(f"发生异常:{str(exception)}")
raise
def _get_material(self, name: str) -> str:
"""
获取素材
:param name: 素材名称
:return 素材路径
"""
# 素材路径
material_path = self.materials_path / name
if not material_path.exists():
raise FileNotFoundError(f"素材文件不存在")
return material_path.as_posix()
def _add_audio(
self,
track_name: str,
name: str,
target_timerange: Tuple[Optional[int, str], Optional[int, str]],
source_timerange: Optional[Tuple[str, str]] = None,
speed: Optional[float] = None,
volume: float = 1.0,
fade: Optional[Tuple[str, str]] = None,
) -> None:
"""
添加音频片段
:param track_name: 轨道名称
:param name: 音频素材名称
:param target_timerange: 音频素材在轨道上的范围包括开始时间和持续时长
:param source_timerange: 截取音频素材范围包括开始时间和持续时长默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分
:param speed: 播放速度
:param volume: 播放音量
:param fade: 淡入淡出设置
:return:
"""
try:
# 构建音频片段
audio_segment = capcut.AudioSegment(
material=self._get_material(name),
target_timerange=trange(*target_timerange),
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)
return
except Exception:
raise
def _add_video(
self,
track_name: str,
name: str,
target_timerange: Tuple[Optional[int, str], Optional[int, str]],
source_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]],
] = None,
speed: Optional[float] = None,
volume: float = 1.0,
clip_settings: Optional[Dict[str, Any]] = None,
keyframes: Optional[List[Tuple[KeyframeProperty, str, float]]] = None,
animation: Optional[Dict[str, Any]] = None,
transition: Optional[Dict[str, Any]] = None,
background_filling: Optional[Tuple[str, Any]] = None,
) -> None:
"""
添加视频/图片片段
:param track_name: 轨道名称
:param name: 视频/图片素材名称
:param target_timerange: 视频素材在轨道上的范围包括开始时间和持续时长
:param source_timerange: 截取视频素材范围包括开始时间和持续时长
:param speed: 播放速度
:param volume: 播放音量
:param clip_settings: 图像调节设置
:param keyframes: 关键帧设置
:param animation: 动画设置
:param transition: 转场设置
:param background_filling: 背景填充设置
:param track_name 轨道名称
:return:
"""
try:
# 构建视频/图片片段
video_segment = capcut.VideoSegment(
material=self._get_material(name),
target_timerange=trange(*target_timerange),
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:
# noinspection PyShadowingBuiltins
for property, time, value in keyframes:
video_segment.add_keyframe(property, time, 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)
return
except Exception:
raise
def _add_text(
self,
track_name: str,
content: str,
timerange: Tuple[Optional[int, str], Optional[int, str]],
border: Optional[Dict[str, Any]] = None,
background: Optional[Dict[str, Any]] = None,
font: Optional[str] = None,
style: 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 content: 文本内容
:param timerange: 文本素材在轨道上的范围包括开始时间和持续时长
:param border: 文本描边设置
:param background: 文本背景设置
:param font: 字体类型
:param style: 字体样式
:param clip_settings: 文本调节设置
:param bubble: 气泡设置
:param effect: 花字设置
:param animation: 动画设置
:return:
"""
try:
# 构建文本片段
text_segment = capcut.TextSegment(
text=content,
timerange=trange(*timerange),
border=capcut.TextBorder(**border) if border else None,
background=capcut.TextBackground(**background) if background else None,
font=capcut.FontType(font) if font else None,
style=capcut.TextStyle(**style) if style else None,
clip_settings=(
capcut.ClipSettings(**clip_settings) if clip_settings else None
),
)
# 添加气泡
if bubble:
text_segment.add_bubble(**bubble)
# 添加花字
if effect:
text_segment.add_effect(
**effect
) # 可先将花字保存预设再在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id
# 添加动画
if animation:
text_segment.add_animation(**animation)
# 向指定轨道添加文本片段
self.draft.add_segment(segment=text_segment, track_name=track_name)
return
except Exception:
raise
def _add_sticker(
self,
track_name: str,
resource_id: str,
target_timerange: Tuple[Optional[int, str], Optional[int, str]],
clip_settings: Optional[Dict[str, Any]] = None,
) -> None:
"""
添加贴纸片段
:param track_name: 轨道名称
:param resource_id: 贴纸 resource_id
:param target_timerange: 贴纸在轨道上的范围包括开始时间和持续时长
:param clip_settings: 文本调节设置
:return:
"""
try:
# 构建贴纸
sticker_segment = capcut.StickerSegment(
resource_id=resource_id, # 可先将贴纸保存为我的预设再在C:\Users\admin\AppData\Local\JianyingPro\User Data\Presets\Combination\Presets\我的预设?\preset_draft获取
target_timerange=trange(*target_timerange),
clip_settings=(
capcut.ClipSettings(**clip_settings) if clip_settings else None
),
)
# 向指定轨道添加贴纸
self.draft.add_segment(segment=sticker_segment, track_name=track_name)
except Exception:
raise
def save(self) -> None:
"""保存草稿"""
try:
self.draft.save()
except Exception:
raise
def add_subtitle(
self,
script: str,
track_name: str = "subtitle",
timbre: Optional[str] = "女声-晓晓",
rate: str = "+25%",
volume: str = "+0%",
):
"""
根据脚本生成文本和音频字幕
:param track_name: 轨道名称
:param script: 脚本
:param timbre: 声音音色
:param rate: 语速
:param volume: 音量
:return:
"""
print("正在根据脚本生成文本和音频字幕...", end="")
# 添加文本轨道
self.draft.add_track(
track_type=capcut.TrackType.text,
track_name=(text_track_name := f"{track_name}:text"),
)
# 添加音频轨道
self.draft.add_track(
track_type=capcut.TrackType.audio,
track_name=(audio_track_name := f"{track_name}:audio"),
)
start = 0
for content in script.split(""):
# 根据文本内容合成语音并返回音频素材名称
name, duration = self.synthesizer.generate_audio(
content, timbre, rate, volume
)
# 添加文本片段
self._add_text(
track_name=text_track_name,
content=content,
timerange=(start, duration),
style={"size": 12.0, "align": 1}, # 字号为12对齐方式为水平居中
clip_settings={"transform_y": -0.5}, # 垂直位移
effect={"effect_id": "6896137858998930701"}, # 第二行第三列花字
)
# 添加音频片段
self._add_audio(
track_name=audio_track_name,
name=name,
target_timerange=(start, duration),
)
start += duration
# 更新脚本持续时长
self.duration = start
print("已完成")
def add_video(
self,
track_name: str,
name: str,
target_timerange: Tuple[Optional[int, str], Optional[int, str]] = None,
source_timerange: Optional[
Tuple[Optional[int, str], Optional[int, str]],
] = None,
speed: Optional[float] = None,
volume: float = 1.0,
clip_settings: Optional[Dict[str, Any]] = None,
keyframes: Optional[List[Tuple[KeyframeProperty, str, float]]] = None,
animation: Optional[Dict[str, Any]] = None,
transition: Optional[Dict[str, Any]] = None,
background_filling: Optional[Tuple[str, Any]] = None,
):
"""
向指定轨道添加视频/图片片段
:param track_name: 轨道名称
:param name: 视频/图片素材名称
:param target_timerange: 视频素材在轨道上的范围包括开始时间和持续时长
:param source_timerange: 截取视频素材范围包括开始时间和持续时长
:param speed: 播放速度
:param volume: 播放音量
:param clip_settings: 图像调节设置
:param keyframes: 关键帧设置
:param animation: 动画设置
:param transition: 转场设置
:param background_filling: 背景填充设置
:return:
"""
# 预设图像调节设置
CLIPSETTINGS = {
"logo": {
"scale_x": 0.2,
"scale_y": 0.2,
"transform_x": -0.68,
"transform_y": 0.82,
} # 等比缩放至20%,移动至左上角
}
# 根据轨道名称获取预设文本调节设置
if track_name in CLIPSETTINGS and not clip_settings:
clip_settings = CLIPSETTINGS.get(track_name)
print(f"正在向轨道 {track_name} 添加视频/图片片段...", end="")
track_name = f"{track_name}:video"
# 添加视频轨道
self.draft.add_track(track_type=capcut.TrackType.video, track_name=track_name)
# 添加视频片段
self._add_video(
track_name=track_name,
name=name,
target_timerange=(
target_timerange if target_timerange else (0, self.duration)
),
source_timerange=source_timerange,
speed=speed,
volume=volume,
clip_settings=clip_settings,
keyframes=keyframes,
animation=animation,
transition=transition,
background_filling=background_filling,
)
print("已完成")
def add_text(
self,
track_name: str,
content: str,
timerange: Tuple[Optional[int, str], Optional[int, str]] = None,
border: Optional[Dict[str, Any]] = None,
background: Optional[Dict[str, Any]] = None,
font: Optional[str] = None,
style: Optional[Dict[str, Any]] = None,
clip_settings: Optional[Dict[str, Any]] = None,
bubble: Optional[Dict[str, Any]] = None,
effect: Optional[str] = None,
animation: Optional[Dict[str, Any]] = None,
):
"""
向指定轨道添加文本片段
:param track_name: 轨道名称
:param content: 文本内容
:param timerange: 文本素材在轨道上的范围包括开始时间和持续时长
:param border: 文本描边设置
:param background: 文本背景设置
:param font: 字体类型
:param style: 字体样式
:param clip_settings: 文本调节设置
:param bubble: 气泡设置
:param effect: 花字设置
:param animation: 动画设置
:return:
"""
# 预设文本描边设置
BORDER = {
"disclaimer": {
"width": 60.0,
} # 描边宽度为60
}
# 根据轨道名称获取预设文本描边设置
if track_name in BORDER and not border:
border = BORDER.get(track_name)
# 预设字体样式
STYLE = {
"disclaimer": {
"size": 8.0,
"align": 1,
} # 字号为8对齐方式为水平居中
}
# 根据轨道名称获取预设字体样式
if track_name in STYLE and not style:
style = STYLE.get(track_name)
# 预设文本调节设置
CLIPSETTINGS = {
"disclaimer": {
"transform_y": -0.8,
} # 垂直位移
}
# 根据轨道名称获取预设字体样式
if track_name in CLIPSETTINGS and not clip_settings:
clip_settings = CLIPSETTINGS.get(track_name)
print(f"正在向轨道 {track_name} 添加文本片段...", end="")
track_name = f"{track_name}:text"
# 添加文本轨道
self.draft.add_track(track_type=capcut.TrackType.text, track_name=track_name)
# 添加文本片段
self._add_text(
track_name=track_name,
content=content,
timerange=(timerange if timerange else (0, self.duration)),
border=border,
background=background,
font=font,
style=style,
clip_settings=clip_settings,
bubble=bubble,
effect=effect,
animation=animation,
)
print("已完成")
def add_sticker(
self,
track_name: str,
resource_id: str,
target_timerange: Tuple[Optional[int, str], Optional[int, str]] = None,
clip_settings: Optional[Dict[str, Any]] = None,
):
"""
向指定轨道添加贴纸
:param track_name: 轨道名称
:param resource_id: 贴纸 resource_id可先将贴纸保存为我的预设
:param target_timerange: 贴纸在轨道上的范围包括开始时间和持续时长
:param clip_settings: 文本调节设置
:return:
"""
# 预设文本描边设置
RESOURCEID = {
"7026858083393588487": {
"scale_x": 0.2,
"scale_y": 0.2,
"transform_x": -0.75,
"transform_y": -0.78,
} # 等比缩放至20%,移动至左上角
}
# 根据轨道名称获取预设文本描边设置
if resource_id in RESOURCEID and not clip_settings:
clip_settings = RESOURCEID.get(resource_id)
print(f"正在向轨道 {track_name} 添加贴纸...", end="")
track_name = f"{track_name}:video"
# 添加贴纸轨道
self.draft.add_track(track_type=capcut.TrackType.sticker, track_name=track_name)
# 添加贴纸
self._add_sticker(
track_name=track_name,
resource_id=resource_id,
target_timerange=(
target_timerange if target_timerange else (0, self.duration)
),
clip_settings=clip_settings,
)
print("已完成")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
import asyncio
import uuid
from _md5 import md5
from pathlib import Path
from typing import Optional, Tuple
from typing import Tuple, Union
import edge_tts
from mutagen.mp3 import MP3
@ -11,65 +11,62 @@ from mutagen.mp3 import MP3
class EdgeTTS:
"""
edge在线语音合成
封装EdgeTTS支持
1根据文本合成语音并将语音文件保存至指定文件夹内
"""
# 中文音色
TIMBRES = {
"女声-晓晓": "zh-CN-XiaoxiaoNeural",
"女声-晓辰": "zh-CN-XiaochenNeural",
"女声-晓倩": "zh-CN-XiaoqianNeural",
}
def __init__(
self,
materials_path: Path,
folder_path: Union[str, Path],
):
"""
初始化语音合成器
:param materials_path: 素材文件夹路径
初始化
:param folder_path: 语音文件保存文件夹路径
"""
# 素材文件夹路径
self.materials_path = materials_path
# 指定文件夹路径path对象
self.folder_path = (
folder_path if isinstance(folder_path, Path) else Path(folder_path)
)
def generate_audio(
def synthetize(
self,
content: str,
timbre: Optional[str] = "女声-晓晓",
rate: str = "+0%",
volume: str = "+0%",
) -> Tuple[str, int]:
text: str,
timbre: str,
rate: str,
volume: str,
) -> Tuple[Path, int]:
"""
根据文本内容合成语音并返回音频素材名称
:param content: 文本内容
:param timbre: 音色名称例如女声-晓晓
根据文本合成语音并将语音文件保存至指定文件夹内
:param text: 文本
:param timbre: 音色名称
:param rate: 语速
:param volume: 音量
:return 音频素材名称和持续时长
:return 语音文件路径path对象和持续时长单位为微秒
"""
# noinspection PyBroadException
try:
# 异步处理:根据文本内容合成语音并保存为音频素材
async def _async_generate_audio():
# 实例化Communicate
# 异步处理方法
async def _async_synthetize():
# 构造语音文件名称
file_name = f"{md5((text + timbre + rate + volume).encode("utf-8"))
.hexdigest()
.upper()}.mp3"
# 构造语音文件路径
file_path = self.folder_path / file_name
communicator = edge_tts.Communicate(
text=content,
voice=self.TIMBRES[timbre],
text=text.replace("\n", ""),
voice=timbre,
rate=rate,
volume=volume,
)
# 音频素材名称
name = f"{uuid.uuid4().hex[-16:].upper()}.mp3"
# 音频素材路径
audio_path = self.materials_path / name
await communicator.save(audio_path := audio_path.as_posix())
# 音频持续时长(单位为微妙)
duration = int(round(MP3(audio_path).info.length * 1000000))
return name, duration
await communicator.save(file_path.as_posix())
# 持续时长(单位为微秒)
duration = int(round(MP3(file_path.as_posix()).info.length * 1000000))
return file_path, duration
# 同步调用异步逻辑,对外暴露纯同步接口
return asyncio.run(_async_generate_audio())
return asyncio.run(_async_synthetize())
except Exception as exception:
raise RuntimeError(
f"根据文本内容合成语音并保存为音频素材发声异常:{str(exception)}"
f"根据文本合成语音并将语音文件保存至指定文件夹内发生异常:{str(exception)}"
)

View File

@ -4,7 +4,7 @@
剪映脚本生成自动化
"""
from draft import GenerateDraft
from capcut import GenerateDraft
# 编导方案1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,287 @@
# -*- 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)