549 lines
20 KiB
Python
549 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||
import io
|
||
import os
|
||
import sys
|
||
import public_tools
|
||
import random
|
||
import subprocess
|
||
import uuid
|
||
import json
|
||
from moviepy.editor import AudioFileClip, VideoFileClip
|
||
from pydub import AudioSegment
|
||
import iamge_to_video
|
||
|
||
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||
|
||
MATERIAL_FOLDER = "Y:\\D\\素材\\素材,自选\\做菜(2)"
|
||
|
||
|
||
class Clip:
|
||
"""
|
||
剪辑合并的类
|
||
"""
|
||
|
||
def __init__(self, cript_directory, config_path) -> None:
|
||
self.audio_duration = None
|
||
self.cript_directory = cript_directory
|
||
self.ID = str(uuid.uuid4())
|
||
self.ASS_ID = str(uuid.uuid4())
|
||
self.TEMP_FOLDER = os.path.join(cript_directory, "Temp\\" + self.ID)
|
||
self.ASS_FILE_PATH = os.path.join(
|
||
self.TEMP_FOLDER, self.ASS_ID + ".ass"
|
||
).replace("\\", "/")
|
||
self.config_path = config_path
|
||
self.ffmpeg_path = (
|
||
"../package/ffmpeg-2023-12-07-git-f89cff96d0-full_build/bin/ffmpeg"
|
||
)
|
||
self.ffprobe_path = (
|
||
"../package/ffmpeg-2023-12-07-git-f89cff96d0-full_build/bin/ffprobe"
|
||
)
|
||
self.getInitData()
|
||
pass
|
||
|
||
def getInitData(self):
|
||
"""
|
||
初始化数据的方法
|
||
"""
|
||
self.public_tools = public_tools.PublicTools()
|
||
with open(self.config_path, "r", encoding="utf-8") as file:
|
||
self.config_json = json.load(file)
|
||
self.srt_config = self.config_json["srt_config"]
|
||
self.image_folder = self.config_json["image_folder"]
|
||
self.video_resolution_x = self.config_json["video_resolution_x"]
|
||
self.video_resolution_y = self.config_json["video_resolution_y"]
|
||
self.background_music_folder = self.config_json["background_music_folder"]
|
||
self.srt_file_path = self.config_json["srt_path"]
|
||
self.audio_path = self.config_json["audio_path"]
|
||
self.mp4_file_txt = self.config_json["mp4_file_txt"]
|
||
self.outpue_file = self.config_json["outpue_file"].replace("\\", "/")
|
||
self.srt_style = self.config_json["srt_style"]
|
||
self.friendly_reminder = self.config_json["friendly_reminder"]
|
||
self.audio_sound_size = self.config_json["audio_sound_size"]
|
||
self.background_music_sound_size = self.config_json[
|
||
"background_music_sound_size"
|
||
]
|
||
self.keyFrame = self.config_json["keyFrame"]
|
||
|
||
self.frameRate = (self.config_json["frameRate"],)
|
||
self.bitRate = self.config_json["bitRate"]
|
||
|
||
# 图片数据
|
||
self.iamge_to_video = iamge_to_video.ImageToVideo()
|
||
self.iamge_to_video.fps = self.config_json["frameRate"]
|
||
self.iamge_to_video.video_size = (
|
||
self.video_resolution_x,
|
||
self.video_resolution_y,
|
||
)
|
||
self.iamge_to_video.bitRate = self.bitRate
|
||
|
||
# 随机背景音乐
|
||
if self.background_music_folder == "":
|
||
pass
|
||
else:
|
||
backgroun_music_list1 = self.public_tools.list_files_by_extension(
|
||
self.background_music_folder, ".mp3"
|
||
)
|
||
backgroun_music_list2 = self.public_tools.list_files_by_extension(
|
||
self.background_music_folder, ".wav"
|
||
)
|
||
backgroun_music_list = backgroun_music_list1 + backgroun_music_list2
|
||
self.background_music = random.choice(backgroun_music_list)
|
||
|
||
# 修改状态
|
||
self.config_json["status"] = "generating"
|
||
with open(self.config_path, "w", encoding="utf-8") as file:
|
||
json.dump(self.config_json, file, ensure_ascii=False, indent=4)
|
||
pass
|
||
|
||
# 获取srt配置文件并修改srt数据
|
||
def getConfigJson(self):
|
||
with open(self.srt_config, "r", encoding="utf-8") as file:
|
||
self.config_json = json.load(file)
|
||
# 修改最后的视频的长度
|
||
# 获取音频的时间
|
||
self.GetAudioDuration()
|
||
# print(self.audio_duration)
|
||
# 直接修改
|
||
self.config_json["srt_time_information"][
|
||
len(self.config_json["srt_time_information"]) - 1
|
||
]["end_time"] = self.audio_duration
|
||
|
||
for i in range(len(self.config_json["srt_time_information"])):
|
||
if i == 0:
|
||
self.config_json["srt_time_information"][i]["start_time"] = 0
|
||
elif i == len(self.config_json["srt_time_information"]) - 1:
|
||
pass
|
||
else:
|
||
# 将当前的 end_time 设置为 下一个的 start_time,最后一个数据不设置
|
||
self.config_json["srt_time_information"][i]["end_time"] = (
|
||
self.config_json["srt_time_information"][i + 1]["start_time"]
|
||
)
|
||
|
||
# 将AI生成的图片生成一个个的小视频
|
||
def ImageToOneVideo(self):
|
||
self.getConfigJson()
|
||
self.iamge_to_video.GenerateVideoAllImage(
|
||
self.image_folder, self.keyFrame, self.config_json
|
||
)
|
||
|
||
# 将生成的所有的mp4文件路径写入到txt文件中
|
||
self.mp4_files = self.public_tools.list_files_by_extension(
|
||
self.image_folder, ".mp4"
|
||
)
|
||
self.mp4_files.sort()
|
||
if os.path.exists(self.mp4_file_txt):
|
||
os.remove(self.mp4_file_txt)
|
||
# 打开文件并写入数据
|
||
with open(self.mp4_file_txt, "w", encoding="utf-8") as file:
|
||
for line in self.mp4_files:
|
||
# 写入每行数据,并在每行末尾添加换行符
|
||
file.write("file '" + line + "'\n")
|
||
# print(self.mp4_files)
|
||
|
||
pass
|
||
|
||
# 获取音视频的时间长短
|
||
def GetAudioVideoDuration(self, path):
|
||
try:
|
||
# 获取文件的后缀
|
||
type = os.path.splitext(path)[1]
|
||
if type.upper() == ".MP3":
|
||
mp3_audio = AudioFileClip(path)
|
||
mp3_duration = mp3_audio.duration * 1000
|
||
mp3_audio.close()
|
||
return mp3_duration
|
||
elif type.upper() == ".WAV":
|
||
wav_audio = AudioFileClip(path)
|
||
wav_duration = wav_audio.duration * 1000
|
||
wav_audio.close()
|
||
return wav_duration
|
||
elif type.upper() == ".MP4":
|
||
video = VideoFileClip(path)
|
||
video_duration = video.duration * 1000
|
||
video.close()
|
||
return video_duration
|
||
else:
|
||
raise ValueError("参数类型错误")
|
||
except Exception as e:
|
||
raise ValueError(e)
|
||
|
||
# 创建临时文件夹
|
||
def CreateTempFolder(self):
|
||
try:
|
||
# 判断Temp文件是不是存在
|
||
this_path = self.cript_directory
|
||
isExitTemp = public_tools.check_if_folder_exists(this_path, "Temp")
|
||
temp = os.path.join(this_path, "Temp")
|
||
if not isExitTemp:
|
||
os.makedirs(os.path.join(this_path, "Temp"), exist_ok=True)
|
||
isExitTemp = public_tools.check_if_folder_exists(
|
||
os.path.join(this_path, "Temp"), self.ID
|
||
)
|
||
if not isExitTemp:
|
||
os.makedirs(os.path.join(temp, self.ID), exist_ok=True)
|
||
|
||
except Exception as e:
|
||
raise ValueError(e)
|
||
|
||
# 获取音频的时间
|
||
def GetAudioDuration(self):
|
||
if self.audio_duration:
|
||
pass
|
||
else:
|
||
self.audio_duration = self.GetAudioVideoDuration(self.audio_path)
|
||
|
||
# 匹配视频,返回一个txt文件地址
|
||
def AddVideoToList(self):
|
||
video_duration = 0
|
||
IsContinue = True
|
||
txt_list = []
|
||
# 获取音频的时长
|
||
self.GetAudioDuration()
|
||
# print("音频时长:", self.audio_duration)
|
||
# 创建临时文件夹
|
||
self.CreateTempFolder()
|
||
# 开始匹配时长,从素材文件夹中
|
||
material_list = self.public_tools.list_files_by_extension(
|
||
MATERIAL_FOLDER, ".mp4"
|
||
)
|
||
|
||
# 开始处理素材,随机获取,计算时间,生成tmp和txt文件
|
||
# print(random.choice(material_list))
|
||
i = 0
|
||
while True:
|
||
random_path = random.choice(material_list)
|
||
# print(random_path)
|
||
# 对视频进行处理(加速,去头,去尾,镜像,放大,静音)
|
||
output_file = os.path.join(self.TEMP_FOLDER, str(i) + ".mp4")
|
||
start_time = 5000 # 开头删除5秒
|
||
total_duration = self.GetAudioVideoDuration(
|
||
random_path
|
||
) # 假设视频总时长为60秒
|
||
end_time = 3000 # 结尾删除5秒
|
||
|
||
# 计算出需要保留的部分的长度
|
||
duration = total_duration - start_time - end_time
|
||
if duration <= 0:
|
||
duration = total_duration
|
||
|
||
video_duration += duration
|
||
if video_duration > self.audio_duration:
|
||
duration = duration - (video_duration - self.audio_duration)
|
||
IsContinue = False
|
||
|
||
start_time = f"{int(start_time // 3600000):02d}:{int((start_time % 3600000) // 60000):02d}:{(start_time % 60000) / 1000:.3f}"
|
||
duration = f"{int(duration // 3600000):02d}:{int((duration % 3600000) // 60000):02d}:{(duration % 60000) / 1000:.3f}"
|
||
# 添加txtlist表
|
||
txt_list.append(f"file '{os.path.abspath(output_file)}'")
|
||
# 删除开头,结尾,镜像,加速,放大
|
||
subprocess.run(
|
||
[
|
||
self.ffmpeg_path,
|
||
"-hwaccel",
|
||
"cuda", # 启用 CUDA 硬件加速
|
||
"-c:v",
|
||
"h264_cuvid", # 使用 NVIDIA CUVID 解码器进行解码
|
||
"-i",
|
||
random_path,
|
||
"-ss",
|
||
str(start_time),
|
||
"-t",
|
||
str(duration),
|
||
"-vf",
|
||
"hflip,setpts=1*PTS,format=yuv420p,scale=iw*1.1:ih*1.1,crop=iw/1.1:ih/1.1",
|
||
"-an",
|
||
"-b:v",
|
||
"5000k",
|
||
"-c:v",
|
||
"h264_nvenc",
|
||
"-preset",
|
||
"fast",
|
||
"-loglevel",
|
||
"error",
|
||
output_file,
|
||
],
|
||
check=True,
|
||
stderr=subprocess.PIPE,
|
||
)
|
||
i += 1
|
||
if not IsContinue:
|
||
self.vide_list_txt = os.path.join(
|
||
self.TEMP_FOLDER, str(uuid.uuid4()) + ".txt"
|
||
)
|
||
with open(self.vide_list_txt, "w") as f:
|
||
for item in txt_list:
|
||
f.write(str(item) + "\n")
|
||
break
|
||
|
||
# 处理音频
|
||
def ModifyBackGroundMusic(self):
|
||
self.GetAudioDuration()
|
||
self.CreateTempFolder()
|
||
background_music = []
|
||
# 先处理背景音乐,时间超长裁剪,不够长循环
|
||
background_music1 = self.public_tools.list_files_by_extension(
|
||
self.background_music_folder, ".mp3"
|
||
)
|
||
background_music2 = self.public_tools.list_files_by_extension(
|
||
self.background_music_folder, ".wav"
|
||
)
|
||
background_music = background_music1 + background_music2
|
||
self.background_music_path = random.choice(background_music)
|
||
|
||
# print("背景音乐" + self.background_music_path)
|
||
|
||
# 如果输入文件不是WAV,需要先转换为WAV
|
||
if not self.background_music_path.lower().endswith(".wav"):
|
||
audio = AudioSegment.from_file(self.background_music_path).export(
|
||
format="wav"
|
||
)
|
||
audio = AudioSegment.from_wav(audio)
|
||
else:
|
||
audio = AudioSegment.from_wav(self.background_music_path)
|
||
|
||
# 获取背景音乐的长度
|
||
duration_background_music = len(audio)
|
||
|
||
# 如果音频长度超过指定长度,则裁剪
|
||
if duration_background_music > self.audio_duration:
|
||
audio = audio[: self.audio_duration]
|
||
# 如果音频长度小于指定长度,则循环补齐
|
||
else:
|
||
while duration_background_music < self.audio_duration:
|
||
audio += audio
|
||
duration_background_music = len(audio)
|
||
audio = audio[: self.audio_duration]
|
||
|
||
# 将背景音乐写入到Temp文件中
|
||
self.background_music_path = os.path.join(
|
||
self.TEMP_FOLDER, str(uuid.uuid4()) + ".wav"
|
||
)
|
||
|
||
audio.export(self.background_music_path, format="wav")
|
||
|
||
# 合并两个音乐
|
||
def Merge_Audio(
|
||
self, audio_path1, background_music_path, audio_db, backgroud_music_db
|
||
):
|
||
# 转换格式
|
||
def convert_to_wav(audio_path):
|
||
"""如果音频不是WAV格式,则转换为WAV格式"""
|
||
if not audio_path.lower().endswith(".wav"):
|
||
sound = AudioSegment.from_file(audio_path)
|
||
audio_path = audio_path.rsplit(".", 1)[0] + ".wav" # 创建新WAV文件路径
|
||
sound.export(audio_path, format="wav") # 导出为WAV格式
|
||
return AudioSegment.from_wav(audio_path)
|
||
|
||
self.mix_audio = os.path.join(self.TEMP_FOLDER, str(uuid.uuid4()) + ".wav")
|
||
# 转换音频格式为WAV并加载
|
||
audio1 = convert_to_wav(audio_path1)
|
||
audio2 = convert_to_wav(background_music_path)
|
||
# 调整音频文件的分贝量
|
||
adjusted_audio1 = audio1 + audio_db # 第一个音频的音量增加db_change1分贝
|
||
adjusted_audio2 = (
|
||
audio2 + backgroud_music_db
|
||
) # 第二个音频的音量增加db_change2分贝
|
||
# 合并音频
|
||
combined_audio = adjusted_audio1.overlay(adjusted_audio2)
|
||
|
||
# 导出合并后的音频为WAV格式
|
||
combined_audio.export(self.mix_audio, format="wav")
|
||
|
||
# 处理字幕样式,使用ass
|
||
def ConvertSubtitles(
|
||
self,
|
||
font_name="文悦新青年体 (非商用) W8",
|
||
font_size="80",
|
||
primary_colour="&H0CE0F9",
|
||
alignment="5",
|
||
positionX=0,
|
||
positionY=0,
|
||
):
|
||
# 将srt转换为ass
|
||
subprocess.run(
|
||
[
|
||
self.ffmpeg_path,
|
||
"-i",
|
||
self.srt_file_path,
|
||
self.ASS_FILE_PATH,
|
||
"-loglevel",
|
||
"error",
|
||
],
|
||
check=True,
|
||
stderr=subprocess.PIPE,
|
||
)
|
||
|
||
modified_lines = [] # 创建一个新的列表来保存修改后的行
|
||
|
||
# 修改字幕
|
||
with open(self.ASS_FILE_PATH, "r", encoding="utf-8") as file:
|
||
lines = file.readlines()
|
||
|
||
for line in lines:
|
||
# 修改分辨率
|
||
if line.startswith("PlayResX:"):
|
||
line = "PlayResX: " + str(self.video_resolution_x) + "\n"
|
||
if line.startswith("PlayResY:"):
|
||
line = "PlayResY: " + str(self.video_resolution_y) + "\n"
|
||
if line.startswith("ScaledBorderAndShadow:"):
|
||
line = "ScaledBorderAndShadow: no" + "\n"
|
||
|
||
if line.startswith("Style:"):
|
||
parts = line.split(",")
|
||
# 修改样式设置
|
||
parts[1] = font_name # 字体名字
|
||
parts[2] = str(font_size) # 字体大小
|
||
parts[3] = "&H" + primary_colour # 字体颜色
|
||
parts[4] = "&H" + primary_colour # 第二颜色
|
||
parts[5] = "&H0" # OutlineColour
|
||
parts[6] = "&H0" # BackColour
|
||
parts[7] = "0" # Bold
|
||
parts[8] = "0" # Italic
|
||
parts[9] = "0" # Underline
|
||
parts[10] = "0" # StrikeOut
|
||
parts[11] = "100" # ScaleX 缩放X
|
||
parts[12] = "100" # ScaleY 缩放Y
|
||
parts[13] = "0" # Spacing
|
||
parts[14] = "0" # Angle
|
||
parts[15] = "0" # BorderStyle
|
||
parts[16] = "1" # Outline
|
||
parts[17] = "0" # Shadow
|
||
parts[18] = alignment # Alignment
|
||
parts[19] = "0" # MarginL
|
||
parts[20] = "0" # MarginR
|
||
parts[21] = "0" # MarginV
|
||
parts[22] = "1" # Encoding
|
||
line = ",".join(parts)
|
||
# 添加坐标信息
|
||
if line.startswith("Dialogue:"):
|
||
texts = line.split(",", 9)
|
||
text = texts[9]
|
||
# 检查文本中是否已经包含\pos标签
|
||
if "\\pos(" not in text:
|
||
text = "{\\pos(%d,%d)}" % (positionX, positionY) + text
|
||
texts[9] = text
|
||
line = ",".join(texts)
|
||
|
||
modified_lines.append(line) # 添加修改后的行到新列表
|
||
# 添加全局水印并设置和视频的时长一直
|
||
# Dialogue: 0,0:00:01.00,0:00:05.00,Default,,0,0,0,,{\fs24\an8\alpha80}这是一段示例文本。
|
||
modified_lines.append(
|
||
"Dialogue: 0,0:00:00.00,"
|
||
+ public_tools.format_time_ms(self.audio_duration)
|
||
+ ",Default,,0,0,0,,"
|
||
+ "{\\pos("
|
||
+ str(self.friendly_reminder["positionX"])
|
||
+ ","
|
||
+ str(self.friendly_reminder["positionY"])
|
||
+ ")\\fs"
|
||
+ str(self.friendly_reminder["fontSize"])
|
||
+ "\\alpha&H"
|
||
+ str(self.friendly_reminder["transparent"])
|
||
+ "&\\fn"
|
||
+ self.friendly_reminder["fontName"]
|
||
+ "\\c"
|
||
+ self.public_tools.convert_rrggbb_to_bbggrr(
|
||
self.friendly_reminder["fontColor"]
|
||
)
|
||
+ "}"
|
||
+ self.friendly_reminder["showText"]
|
||
)
|
||
with open(self.ASS_FILE_PATH, "w", encoding="utf-8") as file:
|
||
file.writelines(modified_lines)
|
||
|
||
# 合并视频并添加音乐和字幕
|
||
def MergeVideoAndAudio(self):
|
||
command = [
|
||
self.ffmpeg_path,
|
||
"-f",
|
||
"concat",
|
||
"-safe",
|
||
"0",
|
||
"-i",
|
||
self.mp4_file_txt,
|
||
"-i",
|
||
self.mix_audio,
|
||
"-vf",
|
||
f"subtitles=./Temp/{self.ID}/{self.ASS_ID}.ass",
|
||
# f"subtitles= {ASS_FILE_PATH}",
|
||
"-c:v",
|
||
"h264_nvenc",
|
||
"-preset",
|
||
"fast",
|
||
"-rc:v",
|
||
"cbr",
|
||
"-b:v",
|
||
str(self.bitRate) + "k",
|
||
"-c:a",
|
||
"aac",
|
||
"-strict",
|
||
"-2",
|
||
"-loglevel",
|
||
"error",
|
||
self.outpue_file,
|
||
]
|
||
subprocess.run(command, check=True, stderr=subprocess.PIPE)
|
||
# subprocess.run(command)
|
||
pass
|
||
|
||
def DeleteFile(self):
|
||
"""
|
||
删除已经存在的视频文件
|
||
"""
|
||
# 删除图片目录下面的所有的MP4文件
|
||
out_mp4_list = self.public_tools.list_files_by_extension(
|
||
self.image_folder, ".mp4"
|
||
)
|
||
for mp4 in out_mp4_list:
|
||
self.public_tools.delete_path(mp4)
|
||
|
||
# 删除输出文件
|
||
self.public_tools.delete_path(self.outpue_file)
|
||
pass
|
||
|
||
# 合并视频并添加剪辑
|
||
def MergeVideosAndClip(self):
|
||
# 删除文件
|
||
self.DeleteFile()
|
||
|
||
# 将图片生成视频
|
||
self.ImageToOneVideo()
|
||
|
||
# # 处理音乐
|
||
self.ModifyBackGroundMusic()
|
||
|
||
# # 合并音乐
|
||
self.Merge_Audio(
|
||
self.audio_path,
|
||
self.background_music_path,
|
||
self.audio_sound_size,
|
||
self.background_music_sound_size,
|
||
)
|
||
|
||
# # 素材处理
|
||
# self.AddVideoToList()
|
||
|
||
# 字幕处理
|
||
self.ConvertSubtitles(
|
||
self.srt_style["fontName"],
|
||
str(self.srt_style["fontSize"] * (self.video_resolution_x / 1440)),
|
||
self.public_tools.convert_rrggbb_to_bbggrr(self.srt_style["fontColor"]),
|
||
"5",
|
||
self.srt_style["positionX"] * (self.video_resolution_x / 1440),
|
||
self.srt_style["positionY"] * (self.video_resolution_y / 1080),
|
||
)
|
||
|
||
# 视频合并
|
||
self.MergeVideoAndAudio()
|
||
|
||
# 删除临时文件夹
|
||
self.public_tools.delete_path(self.TEMP_FOLDER)
|
||
# 删除临时的视频
|
||
del_mp4_list = self.public_tools.list_files_by_extension(
|
||
self.image_folder, ".mp4"
|
||
)
|
||
for f in del_mp4_list:
|
||
self.public_tools.delete_path(f)
|
||
|
||
pass
|