Initial commit 添加MJ功能

This commit is contained in:
2024-05-15 12:57:15 +08:00
commit 74009113fa
602 changed files with 43708 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
import io
import json
import os
import sys
import clip
import getgrame
import Push_back_Prompt
import public_tools
import shotSplit
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
# sys.argv = ["C:\\Users\\27698\\Desktop\\LAITool\\resources\\scripts\\Lai.exe","-c","D:/父爱如山倒/scripts/output_crop_00001.json"]
print(sys.argv)
if len(sys.argv) < 2:
print("Params: <runtime-config.json>")
exit(0)
if getattr(sys, "frozen", False):
cript_directory = os.path.dirname(sys.executable)
elif __file__:
cript_directory = os.path.dirname(__file__)
def set_ffmpeg_env():
# 根据你的ffmpeg路径替换这个
ffmpeg_path = os.path.join(
cript_directory, "../package/ffmpeg-2023-12-07-git-f89cff96d0-full_build/bin"
)
if sys.platform == "win32":
ffmpeg_path = ffmpeg_path.replace("/", "\\")
# 检查环境变量中是否已经设置
if "PATH" in os.environ:
path_values = os.environ["PATH"]
if ffmpeg_path not in path_values:
os.environ["PATH"] += f";{ffmpeg_path}"
else:
os.environ["PATH"] = ffmpeg_path
set_ffmpeg_env()
# 执行剪辑的方法
if sys.argv[1] == "-c":
clip = clip.Clip(cript_directory, sys.argv[2])
clip.MergeVideosAndClip()
pass
# 获取字体
elif sys.argv[1] == "-f":
# 获取本地已安装的字幕。然后返回
public_tools = public_tools.PublicTools()
font_list = public_tools.get_installed_fonts()
font_obj_list = []
for font_name in font_list:
obj = {"label": font_name, "value": font_name}
font_obj_list.append(obj)
with open(sys.argv[2], "r", encoding="utf-8") as file:
data = json.load(file)
data["font_name_list"] = font_obj_list
with open(sys.argv[2], "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
# 反推提示词
elif sys.argv[1] == "-p":
Push_back_Prompt.init(sys.argv[2], sys.argv[3], sys.argv[4])
pass
# 剪映抽帧
elif sys.argv[1] == "-k":
# print("")
getgrame.init(sys.argv[2], sys.argv[3], sys.argv[4])
pass
# 智能分镜。字幕识别
elif sys.argv[1] == "-a":
print("开始算法分镜:" + sys.argv[2] + " -- 输出文件夹:" + sys.argv[3])
shotSplit.init(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
+43
View File
@@ -0,0 +1,43 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.building.datastruct import Tree
from PyInstaller.utils.hooks import get_package_paths
PACKAGE_DIRECTORY = get_package_paths('faster_whisper')[1]
datas = [(PACKAGE_DIRECTORY, 'faster_whisper')]
a = Analysis(
['Lai.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='Lai',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+297
View File
@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
import io
import os
import re
import subprocess
import sys
import pandas as pd
import numpy as np
from typing import Tuple, List, Dict
import cv2
from PIL import Image
from pathlib import Path
import public_tools
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
TAG_MODE_ACTION_COMMON = "action_common"
TAG_MODE_ACTION = "action"
if getattr(sys, "frozen", False):
cript_directory = os.path.dirname(sys.executable)
elif __file__:
cript_directory = os.path.dirname(__file__)
def make_square(img, target_size):
old_size = img.shape[:2]
desired_size = max(old_size)
desired_size = max(desired_size, target_size)
delta_w = desired_size - old_size[1]
delta_h = desired_size - old_size[0]
top, bottom = delta_h // 2, delta_h - (delta_h // 2)
left, right = delta_w // 2, delta_w - (delta_w // 2)
color = [255, 255, 255]
new_im = cv2.copyMakeBorder(
img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color
)
return new_im
def smart_resize(img, size):
# 假设图像已经经过 make_square 处理
if img.shape[0] > size:
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
elif img.shape[0] < size:
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_CUBIC)
return img
# 调用gpu,很难成功,环境要求太高
use_cpu = False
if use_cpu:
tf_device_name = "/cpu:0"
else:
tf_device_name = "/gpu:0"
class Interrogator:
@staticmethod
def postprocess_tags(
tags: Dict[str, float],
threshold=0.35, # 阈值强度,默认0.35
additional_tags: List[str] = [],
exclude_tags: List[str] = [],
sort_by_alphabetical_order=False,
add_confident_as_weight=False,
replace_underscore=False,
replace_underscore_excludes: List[str] = [],
escape_tag=False,
) -> Dict[str, float]:
for t in additional_tags:
tags[t] = 1.0
tags = {
t: c
# 按标签名称或置信度排序
for t, c in sorted(
tags.items(),
key=lambda i: i[0 if sort_by_alphabetical_order else 1],
reverse=not sort_by_alphabetical_order,
)
# 筛选大于阈值的标签
if (c >= threshold and t not in exclude_tags)
}
new_tags = []
for tag in list(tags):
new_tag = tag
if replace_underscore and tag not in replace_underscore_excludes:
new_tag = new_tag.replace("_", " ")
"""
if escape_tag:
new_tag = tag_escape_pattern.sub(r'\\\1', new_tag)
"""
if add_confident_as_weight:
new_tag = f"({new_tag}:{tags[tag]})"
new_tags.append((new_tag, tags[tag]))
tags = dict(new_tags)
return tags
def __init__(self, name: str) -> None:
self.name = name
def load(self):
raise NotImplementedError()
def unload(self) -> bool:
unloaded = False
if hasattr(self, "model") and self.model is not None:
del self.model
unloaded = True
print(f"Unloaded {self.name}")
if hasattr(self, "tags"):
del self.tags
return unloaded
def interrogate(self, image: Image) -> Tuple[Dict[str, float], Dict[str, float]]:
raise NotImplementedError()
class WaifuDiffusionInterrogator(Interrogator):
def __init__(
self,
name: str,
model_path="model.onnx",
tags_path="selected_tags.csv",
**kwargs,
) -> None:
super().__init__(name)
self.model_path = model_path
self.tags_path = tags_path
self.kwargs = kwargs
def interrogate(
self, image: Image
) -> Tuple[
Dict[str, float], Dict[str, float] # rating confidents # tag confidents
]:
# init model
if not hasattr(self, 'model') or self.model is None:
model_path = os.path.join(cript_directory, "model/tag/model.onnx")
tags_path = os.path.join(cript_directory, "model/tag/selected_tags.csv")
from onnxruntime import InferenceSession
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
self.model = InferenceSession(str(model_path), providers=providers)
print(f"{model_path} 读取 {self.name}模型")
self.tags = pd.read_csv(tags_path)
_, height, _, _ = self.model.get_inputs()[0].shape
# 透明转换成白色
image = image.convert("RGBA")
new_image = Image.new("RGBA", image.size, "WHITE")
new_image.paste(image, mask=image)
image = new_image.convert("RGB")
image = np.asarray(image)
# RGB格式转换
image = image[:, :, ::-1]
image = make_square(image, height)
image = smart_resize(image, height)
image = image.astype(np.float32)
image = np.expand_dims(image, 0)
# 验证一下模型
input_name = self.model.get_inputs()[0].name
label_name = self.model.get_outputs()[0].name
confidents = self.model.run([label_name], {input_name: image})[0]
tags = self.tags[:][["name"]]
tags["confidents"] = confidents[0]
# 前4项标签用于评定模型(一般、敏感、可疑、明确)
ratings = dict(tags[:4].values)
# 其他的是常规标签
tags = dict(tags[4:].values)
return ratings, tags
def getTags(model, img_path):
img = Image.open(img_path)
ratings, tags = model.interrogate(img)
img.close()
tags = model.postprocess_tags(tags)
return ",".join(tags.keys())
pattern_word_split = re.compile(r"\W+")
def is_tag_in_list(tag, rule_list):
words = pattern_word_split.split(tag)
for word in words:
if word in rule_list:
return True
return False
def filter_action(tag_actions: [], tags: []):
action_tags = []
other_tags = []
for tag in tags:
if public_tools.is_empty(tag):
continue
if is_tag_in_list(tag, tag_actions):
action_tags.append(tag)
else:
other_tags.append(tag)
return action_tags, other_tags
def init(sd_setting,m,project_path):
try:
setting_json = public_tools.read_config(sd_setting, webui=False)
except Exception as e:
print("Error: read config", e)
exit(0)
setting_config = public_tools.SettingConfig(setting_json, project_path)
# workspace path config
workspace = setting_config.get_workspace_config()
if not os.path.exists(workspace.input_tag):
os.makedirs(workspace.input_tag)
# 可选功能
if setting_config.enable_tag():
# load model
model = WaifuDiffusionInterrogator(
"wd14-convnextv2-v2",
repo_id="SmilingWolf/wd-v1-4-convnextv2-tagger-v2",
revision="v2.0",
)
tag_mode = setting_config.get_tag_mode()
tag_actions = setting_config.get_tag_actions()
# 轮询开始输出
frame_files = [
f for f in os.listdir(workspace.input_crop) if f.endswith(".png")
]
frame_files.sort()
common_tags = dict()
for frame in frame_files:
frame_file = os.path.join(workspace.input_crop, frame)
txt = getTags(model, frame_file)
tags = txt.split(",")
if tag_mode == TAG_MODE_ACTION:
actions, others = filter_action(tag_actions, tags)
# 替换 txt 为 action txt
txt = ",".join(actions) if len(actions) > 0 else ""
elif tag_mode == TAG_MODE_ACTION_COMMON:
actions, others = filter_action(tag_actions, tags)
txt = ",".join(actions) if len(actions) > 0 else ""
# tag 计数
for tag in others:
if tag in common_tags:
common_tags[tag] = common_tags[tag] + 1
else:
common_tags[tag] = 1
# save tag
txt_file = os.path.join(workspace.input_tag, f"{Path(frame_file).stem}.txt")
with open(txt_file, "w", encoding="utf-8") as tags:
tags.write(txt)
print(f"{frame} 提示词反推完成")
sys.stdout.flush()
# 过滤出现次数 > 30% 的 tags 作为 common tags
threshold_count = max(int(len(frame_files) * 0.3), 1)
common_tag_list = []
for tag in common_tags:
if common_tags[tag] > threshold_count:
common_tag_list.append(tag)
# save common tag
# txt_file = os.path.join(workspace.input_tag, f'common.txt')
# with open(txt_file, 'w', encoding='utf-8') as tags:
# txt = ",".join(common_tag_list) if len(common_tag_list) > 0 else ""
# tags.write(txt)
Binary file not shown.
+548
View File
@@ -0,0 +1,548 @@
# -*- 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
View File
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
import io
import sys
import public_tools
import json
import subprocess
import os
import shutil
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
def init(draft_path, out_dir, package_path):
class TimeObject:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# 剪映草稿目录
draft_folder = draft_path
# 调用函数并获取选定的文件夹
json_path = os.path.join(draft_folder, "draft_content.json")
with open(json_path, "r", encoding="utf-8") as file:
json_data = json.load(file)
if public_tools.check_if_folder_exists(out_dir, "tmp"):
try:
shutil.rmtree(os.path.join(out_dir, "tmp"))
print("文件夹 tmp 删除成功。")
except OSError as e:
print(f"删除文件夹失败:{e}")
try:
os.mkdir(os.path.join(out_dir, "tmp"))
os.mkdir(os.path.join(out_dir + "/tmp", "input_crop"))
print("文件夹创建成功。")
except OSError as e:
print(f"创建文件夹失败:{e}")
# 获取轨道
def find_node(nodes, type, val):
for node in nodes:
if node[type] == val:
return node
# 获取剪映的主轨道
video_nodes = find_node(json_data["tracks"], "type", "video")["segments"]
# 先获取所有的场景的开始结束持续时间
# 获取分割数据
num = 1
time_list = []
for video_node in video_nodes:
# 开始时间
start_time = video_node["target_timerange"]["start"]
# 结束时间
end_time = (
video_node["target_timerange"]["start"]
+ video_node["target_timerange"]["duration"]
)
# 持续时间
duration_time = end_time - start_time
# 中间帧时间
middle_time = (end_time - start_time) / 2
middle_time = start_time + middle_time
# 获取文件地址
video_id = video_node["material_id"]
materials_node = find_node(json_data["materials"]["videos"], "id", video_id)
video_path = materials_node["path"]
time_list.append(
TimeObject(
start_time=start_time,
end_time=end_time,
duration_time=duration_time,
middle_time=middle_time,
video_path=video_path,
)
)
num += 1
# 检查开始时间和结束时间是不是满足包含条件
def is_within_range(video_obj, text_start_time, text_end_time):
return (
text_start_time >= video_obj.start_time
and text_end_time <= video_obj.end_time
)
# 处理文案
text_nodes = find_node(json_data["tracks"], "type", "text")["segments"]
num = 0
text_value_list = []
temp_text_value = ""
for text_node in text_nodes:
text_start_time = text_node["target_timerange"]["start"]
text_end_time = (
text_node["target_timerange"]["start"]
+ text_node["target_timerange"]["duration"]
)
text_material_id = text_node["material_id"]
text_content = find_node(
json_data["materials"]["texts"], "id", text_material_id
)["content"]
text_content_json = json.loads(text_content)
text_value = "".join(text_content_json["text"]) + ""
if is_within_range(time_list[num], text_start_time, text_end_time):
temp_text_value += text_value
else:
text_value_list.append(temp_text_value)
temp_text_value = text_value
num += 1
text_value_list.append(temp_text_value)
print("场景数:" + str(len(text_value_list)))
# 写入txt文件
with open(os.path.join(out_dir, "文案.txt"), "w", encoding="utf-8") as file:
for line in text_value_list:
# 判断是不是最后一行,最后一行不需要换行
if text_value_list.index(line) == len(text_value_list) - 1:
file.write(line)
else:
file.write(line + "\n")
ffmpeg_path = os.path.join(
package_path, "ffmpeg-2023-12-07-git-f89cff96d0-full_build/bin/ffmpeg"
)
# 抽取关键帧
num = 1
for video_list in time_list:
output_file = os.path.join(
out_dir, "tmp/input_crop/" + str(num).zfill(5) + ".png"
)
# FFmpeg命令
ffmpeg_command = [
ffmpeg_path, # 指定FFmpeg可执行文件的路径
"-ss",
str(
public_tools.convert_to_seconds(video_list.middle_time, 1)
), # 开始时间为1.2秒
"-i",
video_list.video_path, # 输入文件
"-frames:v",
"1", # 抽取1帧
output_file, # 输出文件
"-loglevel",
"error",
]
result = subprocess.run(
ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# 检查返回代码
if result.returncode == 0:
print("抽取成功,结果保存在", output_file)
else:
print("抽取失败,错误信息:", result.stderr.decode("utf-8"))
num += 1
sys.stdout.flush() # 强制立即刷新输出缓冲区
print(f"抽取完成,结果保存在 {output_file}")
+473
View File
@@ -0,0 +1,473 @@
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import os
import glob
import public_tools
import subprocess
import json
class ImageToVideo:
def __init__(self) -> None:
self.frames = 0
self.public_tools = public_tools.PublicTools()
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"
)
pass
def create_video_from_image_with_center_offset(
self,
image_path,
duration,
start_offset,
end_offset,
fps=60,
video_size=(1440, 1080),
offest_type="KFTypePositionY",
):
"""
将图片合并成视频,并添加关键帧
"""
# 使用Python的open函数以二进制模式读取图片文件
with open(image_path, "rb") as file:
img_bytes = file.read()
# 将字节流解码成图片
img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
img_resized = self.get_scaled_image(
img, video_size, offest_type, start_offset, end_offset
)
if offest_type == "KFTypePositionY":
video_name = self.create_video_from_image_Y(
image_path,
fps,
video_size,
duration,
start_offset,
end_offset,
img_resized,
)
elif offest_type == "KFTypePositionX":
video_name = self.create_video_from_image_X(
image_path,
fps,
video_size,
duration,
start_offset,
end_offset,
img_resized,
)
elif offest_type == "KFTypeScale":
video_name = self.create_video_from_image_scale(
image_path,
fps,
video_size,
duration,
start_offset,
end_offset,
img,
)
else:
return ValueError("关键帧没有设置正确的参数")
return video_name
def create_video_from_image_scale(
self,
image_path,
fps,
video_size,
duration,
start_scale,
end_scale,
img,
):
"""
缩放关键帧生成视频
"""
scale_width = video_size[0] / img.shape[1]
scale_height = video_size[1] / img.shape[0]
default_scale = max(scale_width, scale_height)
# 创建视频写入器
video_name = f"{image_path.split('/')[-1].split('.')[0]}.mp4"
out = cv2.VideoWriter(
video_name, cv2.VideoWriter_fourcc(*"mp4v"), fps, video_size
)
total_frames = round(duration * fps)
self.frames += total_frames
# 计算偏移变化率
offset_change_per_frame = float(end_scale - start_scale) / total_frames // 2
if start_scale < 0:
start_scale = 0
current_scale = start_scale
for _ in range(int(duration * fps)):
# 创建一个空白画布
canvas = np.zeros((video_size[1], video_size[0], 3), dtype=np.uint8)
# 根据当前的缩放比例调整图片大小
img_resized = cv2.resize(
img,
None,
fx=default_scale + current_scale,
fy=default_scale + current_scale,
)
center_x, center_y = video_size[0] // 2, video_size[1] // 2
# 计算图片在画布上的绘制位置
start_x = center_x - img_resized.shape[1] // 2
start_y = center_y - img_resized.shape[0] // 2
# 安全检查,确保不会复制超出边界的区域
src_x1 = max(-start_x, 0)
dst_x1 = max(start_x, 0)
copy_width = min(img_resized.shape[1] - src_x1, video_size[0] - dst_x1)
src_y1 = max(-start_y, 0)
dst_y1 = max(start_y, 0)
copy_height = min(img_resized.shape[0] - src_y1, video_size[1] - dst_y1)
if copy_height > 0 and copy_width > 0:
canvas[dst_y1 : dst_y1 + copy_height, dst_x1 : dst_x1 + copy_width] = (
img_resized[
src_y1 : src_y1 + copy_height, src_x1 : src_x1 + copy_width
]
)
# 更新偏移量
current_scale += offset_change_per_frame
out.write(canvas)
out.release()
return video_name
def create_video_from_image_X(
self,
image_path,
fps,
video_size,
duration,
start_offset,
end_offset,
img_resized,
):
"""
左右关键帧生成视频
"""
# 创建视频写入器
video_name = f"{image_path.split('/')[-1].split('.')[0]}.mp4"
out = cv2.VideoWriter(
video_name, cv2.VideoWriter_fourcc(*"mp4v"), fps, video_size
)
total_frames = round(duration * fps)
self.frames += total_frames
# 计算偏移变化率
offset_change_per_frame = float(end_offset - start_offset) / total_frames
current_offset = start_offset
for _ in range(int(duration * fps)):
# 创建一个空白画布
canvas = np.zeros((video_size[1], video_size[0], 3), dtype=np.uint8)
center_x, center_y = video_size[0] // 2, video_size[1] // 2
# 计算当前帧的图片中心偏移位置
# offset_y = int(current_offset * scale) # 根据放大比例调整偏移量
offset_x = int(current_offset * 1) # 根据放大比例调整偏移量
# offset_y = int(((new_height - img.shape[0]) // 2) * 1)
# start_y = center_y - img_resized.shape[0] // 2 + offset_y
# 计算图片在画布上的绘制位置
start_x = center_x - img_resized.shape[1] // 2 + offset_x
start_y = center_y - img_resized.shape[0] // 2
# 安全检查,确保不会复制超出边界的区域
src_x1 = max(-start_x, 0)
dst_x1 = max(start_x, 0)
copy_height = min(img_resized.shape[0] - src_x1, video_size[1] - dst_x1)
# copy_width = min(video_size[0], img_resized.shape[1])
# 调整图片复制区域的计算
src_y1 = max(-start_y, 0)
dst_y1 = max(start_y, 0)
copy_width = min(img_resized.shape[1] - src_y1, video_size[0] - dst_y1)
if copy_height > 0 and copy_width > 0:
canvas[dst_y1 : dst_y1 + copy_height, dst_x1 : dst_x1 + copy_width] = (
img_resized[
src_y1 : src_y1 + copy_height, src_x1 : src_x1 + copy_width
]
)
# 更新偏移量
current_offset += offset_change_per_frame
out.write(canvas)
out.release()
return video_name
def create_video_from_image_Y(
self,
image_path,
fps,
video_size,
duration,
start_offset,
end_offset,
img_resized,
):
"""
上下关键帧生成视频
"""
# 创建视频写入器
video_name = f"{image_path.split('/')[-1].split('.')[0]}.mp4"
out = cv2.VideoWriter(
video_name, cv2.VideoWriter_fourcc(*"mp4v"), fps, video_size
)
total_frames = round(duration * fps)
self.frames += total_frames
# 计算偏移变化率
offset_change_per_frame = float(end_offset - start_offset) / total_frames
current_offset = start_offset
for _ in range(int(duration * fps)):
# 创建一个空白画布
canvas = np.zeros((video_size[1], video_size[0], 3), dtype=np.uint8)
center_x, center_y = video_size[0] // 2, video_size[1] // 2
# 计算当前帧的图片中心偏移位置
# offset_y = int(current_offset * scale) # 根据放大比例调整偏移量
offset_y = int(current_offset * 1) # 根据放大比例调整偏移量
# offset_x = int(((new_width - img.shape[1]) // 2) * 1)
# start_x = center_x - img_resized.shape[1] // 2 + offset_x
# 计算图片在画布上的绘制位置
start_x = center_x - img_resized.shape[1] // 2
start_y = center_y - img_resized.shape[0] // 2 + offset_y
# 安全检查,确保不会复制超出边界的区域
src_y1 = max(-start_y, 0)
dst_y1 = max(start_y, 0)
copy_height = min(img_resized.shape[0] - src_y1, video_size[1] - dst_y1)
# copy_width = min(video_size[0], img_resized.shape[1])
# 调整图片复制区域的计算
src_x1 = max(-start_x, 0)
dst_x1 = max(start_x, 0)
copy_width = min(img_resized.shape[1] - src_x1, video_size[0] - dst_x1)
if copy_height > 0 and copy_width > 0:
canvas[dst_y1 : dst_y1 + copy_height, dst_x1 : dst_x1 + copy_width] = (
img_resized[
src_y1 : src_y1 + copy_height, src_x1 : src_x1 + copy_width
]
)
# 更新偏移量
current_offset += offset_change_per_frame
out.write(canvas)
out.release()
return video_name
def get_sorted_images(self, folder_path, image_extensions=[".jpg", ".png"]):
"""
获取图片,排序
"""
# 构建一个匹配所有指定扩展名的模式
patterns = [os.path.join(folder_path, "*" + ext) for ext in image_extensions]
# 列表用于存储找到的图片文件
image_files = []
# 遍历所有模式,匹配文件
for pattern in patterns:
image_files.extend(glob.glob(pattern))
# 按文件名排序
image_files.sort()
return image_files
def get_scaled_image(self, img, video_size, offest_type, start_offest, end_offest):
"""
根据关键帧类型。获取当前图片的放大比例
"""
scale_width = video_size[0] / img.shape[1]
scale_height = video_size[1] / img.shape[0]
scale = max(scale_width, scale_height)
if offest_type == "KFTypePositionY":
# 检查最大偏移量是否大于图片高度
all_offset = abs(start_offest) + abs(end_offest) + video_size[1]
if all_offset > img.shape[0] * scale:
# if all_offset > img.shape[0]:
scale = max(scale, all_offset / img.shape[0])
max_offset = max(abs(start_offest), abs(end_offest))
if max_offset > img.shape[0]:
# 如果最大偏移量大于图片高度,则进一步放大图像
scale = max(scale, video_size[1] / (img.shape[0] - max_offset))
elif offest_type == "KFTypePositionX":
# 检查最大偏移量是否大于图片宽度
all_offset = abs(start_offest) + abs(end_offest) + video_size[0]
# 判断最大高度和当前图片当前放大倍率之间的大小
if all_offset > img.shape[1] * scale:
# if all_offset > img.shape[0]:
scale = max(scale, all_offset / img.shape[1])
max_offset = max(abs(start_offest), abs(end_offest))
if max_offset > img.shape[1]:
# 如果最大偏移量大于图片高度,则进一步放大图像
scale = max(scale, video_size[0] / (img.shape[1] - max_offset))
elif offest_type == "KFTypeScale":
pass
else:
return ValueError("关键帧没有设置正确的参数")
new_width = int(img.shape[1] * scale)
new_height = int(img.shape[0] * scale)
img_resized = cv2.resize(
img, (new_width, new_height), interpolation=cv2.INTER_LINEAR
)
return img_resized
def GenerateVideoAllImage(self, image_dir, offset, config_json):
"""
生成所有的图片
"""
config_data = config_json["srt_time_information"]
isDirection = False
sort_images = self.get_sorted_images(image_dir)
# 生成所有的图片视频
for image_file in sort_images:
filename = os.path.splitext(os.path.basename(image_file))[
0
] # 获取文件名,不包括扩展名
number = int(filename.split("_")[-1])
if number == 188:
print(number)
filtered_data = [item for item in config_data if item["no"] == number]
# 判断是不是空,空的话就跳过
if len(filtered_data) == 0:
return ValueError("没有找到对应的关键帧")
print(filtered_data)
video_arr = []
# 计算当前图片的偏移量,以3200像素为基准
with open(image_file, "rb") as file:
img_bytes = file.read()
# 将字节流解码成图片
img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
img_height, img_width = img.shape[:2]
proportion_height = img_height / 3200
proportion_width = img_height / 3200
if offset["name"] == "KFTypePositionY":
offsetValue = offset["up_down"] * proportion_height
elif offset["name"] == "KFTypePositionX":
offsetValue = offset["left_right"] * proportion_width
elif offset["name"] == "KFTypeScale":
offsetValue = offset["scale"]
else:
return ValueError("关键帧没有设置正确的参数")
# offsetValue = offset
if isDirection:
start_offset = offsetValue
end_offset = -offsetValue
isDirection = False
else:
start_offset = -offsetValue
end_offset = offsetValue
isDirection = True
video_path = self.create_video_from_image_with_center_offset(
image_file,
(filtered_data[0]["end_time"] - filtered_data[0]["start_time"]) / 1000,
start_offset,
end_offset,
self.fps,
self.video_size,
offset["name"],
)
video_arr.append(video_path)
print(video_path)
# 微调所有的视频
mp4_folder = self.public_tools.list_files_by_extension(image_dir, ".mp4")
for mp4_path in mp4_folder:
filename = os.path.splitext(os.path.basename(mp4_path))[
0
] # 获取文件名,不包括扩展名
number = int(filename.split("_")[-1])
if number == 188:
print(number)
filtered_data = [item for item in config_data if item["no"] == number]
# print(filtered_data)
cmd = [
self.ffprobe_path,
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=duration",
"-of",
"json",
mp4_path,
]
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
duration_sec = json.loads(result.stdout)["streams"][0]["duration"]
duration_ms = int(float(duration_sec) * 1000) # 将秒转换为毫秒
print(
duration_ms,
(filtered_data[0]["end_time"] - filtered_data[0]["start_time"]),
)
temp_mp4_path = os.path.join(image_dir, "temp_" + str(number) + ".mp4")
# 开始微调
cmd = [
self.ffmpeg_path,
"-i",
mp4_path,
"-filter:v",
"setpts=PTS*"
+ str(
(filtered_data[0]["end_time"] - filtered_data[0]["start_time"])
/ duration_ms
),
"-c:v",
"h264_nvenc",
"-preset",
"fast",
"-rc:v",
"cbr",
"-b:v",
str(self.bitRate) + "k",
temp_mp4_path,
"-loglevel",
"error",
"-an",
]
subprocess.run(cmd, check=True)
os.remove(mp4_path)
os.rename(temp_mp4_path, mp4_path)
print(self.frames)
+351
View File
@@ -0,0 +1,351 @@
# 读取文件的方法
import json
import os
import win32api
import win32con
import pywintypes
import shutil
import re
class PublicTools:
"""
一些公用的基础方法
"""
def delete_path(self, path):
"""
删除指定路径的文件或者是文件夹
"""
# 检查路径是否存在
if not os.path.exists(path):
return
# 检查路径是文件还是文件夹
if os.path.isfile(path):
# 是文件,执行删除
try:
os.remove(path)
except Exception as e:
raise e
elif os.path.isdir(path):
# 是文件夹,执行删除
try:
shutil.rmtree(path)
except Exception as e:
raise e
else:
raise
def list_files_by_extension(self, folder_path, extension):
"""
读取指定文件夹下面的所有的指定拓展文件命的文件列表
"""
file_list = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.endswith(extension):
file_list.append(os.path.join(root, file))
elif file.endswith(extension.upper()):
file_list.append(os.path.join(root, file))
return file_list
def get_fonts_from_registry(self, key_path):
"""
获取注册表中安装的字体文件
"""
font_names = []
try:
key = win32api.RegOpenKeyEx(
(
win32con.HKEY_LOCAL_MACHINE
if "HKEY_LOCAL_MACHINE" in key_path
else win32con.HKEY_CURRENT_USER
),
key_path.split("\\", 1)[1],
0,
win32con.KEY_READ,
)
i = 0
while True:
try:
value = win32api.RegEnumValue(key, i)
font_name = value[0]
# 使用正则表达式移除括号及其内容
font_name = re.sub(r"\s*\([^)]*\)$", "", font_name)
font_names.append(font_name)
i += 1
except pywintypes.error as e:
if e.winerror == 259: # 没有更多的数据
break
else:
raise
finally:
try:
win32api.RegCloseKey(key)
except:
pass
return font_names
def get_installed_fonts(self):
"""
获取字体文件名称并返回
"""
system_fonts = self.get_fonts_from_registry(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"
)
user_fonts = self.get_fonts_from_registry(
"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"
)
all_fonts = list(set(system_fonts + user_fonts)) # 合并并去重
return all_fonts
# 将RRGGBB转换为BBGGRR
def convert_rrggbb_to_bbggrr(self, rrggbb):
"""
将RRGGBB转换为BBGGRR
"""
if len(rrggbb) == 7:
rr = rrggbb[1:3]
gg = rrggbb[3:5]
bb = rrggbb[5:7]
return bb + gg + rr
else:
return "Invalid input"
def write_to_file(self, arr, filename):
with open(filename, "w",encoding='utf-8') as f:
for item in arr:
f.write("%s\n" % item)
# 读取文件
def read_file(fileType):
txt_path = input(f"输入{fileType}文件路径:")
txt_path = remove_prefix_and_suffix(txt_path, '"', '"')
while txt_path.strip() == "":
txt_path = input(f"输入{fileType}文件路径:")
while os.path.exists(txt_path) == False:
print("文件路径不存在错误:")
txt_path = input(f"输入{fileType}文件路径:")
txt_path = remove_prefix_and_suffix(txt_path, '"', '"')
return txt_path
def format_time_ms(milliseconds):
"""
时间转换将ms->小时:分钟:秒.毫秒格式
"""
seconds = milliseconds / 1000
# 计算小时、分钟和秒
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = seconds % 60
# 格式化字符串
# 使用`%02d`确保小时和分钟总是显示为两位数,`%.2f`确保秒数显示两位小数
formatted_time = f"{hours}:{minutes:02d}:{seconds:05.2f}"
return formatted_time
# 删除满足条件的开头和结尾
def remove_prefix_and_suffix(input_str, prefix_to_remove, suffix_to_remove):
if input_str.startswith(prefix_to_remove):
# 删除开头
input_str = input_str[len(prefix_to_remove) :]
if input_str.endswith(suffix_to_remove):
# 删除结尾
input_str = input_str[: -len(suffix_to_remove)]
return input_str
# 判断文件夹下面是不是有特定的文件夹
def check_if_folder_exists(parent_folder, target_folder_name):
# 获取文件夹列表
subfolders = [f.name for f in os.scandir(parent_folder) if f.is_dir()]
# 检查特定文件夹是否存在
if target_folder_name in subfolders:
return True
else:
return False
# 检查指定文件夹中是否存在特定文件。
def file_exists_in_folder(folder_path: str, file_name: str) -> bool:
# 构建完整的文件路径
file_path = os.path.join(folder_path, file_name)
# 返回文件是否存在
return os.path.isfile(file_path)
# 秒数转换,保留一位小数
def convert_to_seconds(number, count):
seconds = number / 1000000
rounded_number = round(seconds, count)
return rounded_number
def is_empty(obj):
if obj is None:
return True
elif isinstance(obj, str):
return len(obj) == 0
elif isinstance(obj, list):
return len(obj) == 0
elif isinstance(obj, dict):
return len(obj) == 0
return False
def opt_dict(obj, key, default=None):
if obj is None:
return default
if key in obj:
v = obj[key]
if not is_empty(v):
return v
return default
def read_config(path, webui=True):
with open(path, "r", encoding="utf-8") as f:
runtime_config = json.load(f)
if "config" not in runtime_config:
print("no filed 'config' in json")
return None
config = runtime_config["config"]
if "webui" not in config:
print("no filed 'webui' in 'config'")
return None
setting_config_path = config["setting"]
if not os.path.exists(setting_config_path):
setting_config_path = "config/" + setting_config_path
if not os.path.exists(setting_config_path):
setting_config_path = "../" + setting_config_path
# read config
with open(setting_config_path, "r", encoding="utf-8") as f:
setting_config = json.load(f)
# set workspace parent:根目录
if "workspace" in setting_config:
setting_config["workspace"]["parent"] = runtime_config["workspace"]
else:
setting_config["workspace"] = {"parent": runtime_config["workspace"]}
setting_config["video"] = opt_dict(runtime_config, "video")
# merge setting config
if "setting" in config:
setting_config.update(runtime_config["setting"])
# webui config
if webui:
webui_config_path = config["webui"]
if not os.path.exists(webui_config_path):
webui_config_path = "config/webui/" + webui_config_path
if not os.path.exists(webui_config_path):
webui_config_path = "../" + webui_config_path
with open(webui_config_path, "r", encoding="utf-8") as f:
webui_config = json.load(f)
# merge webui config
if "webui" in runtime_config:
webui_config.update(runtime_config["webui"])
return webui_config, setting_config
return setting_config
TAG_MODE_NONE = ""
# 工作路径
class Workspace:
def __init__(
self,
root: str,
input: str,
output: str,
input_crop: str,
output_crop: str,
input_tag: str,
input_mask: str,
input_crop_mask: str,
crop_info: str,
):
self.root = root
self.input = input
self.output = output
self.input_crop = input_crop
self.output_crop = output_crop
self.input_tag = input_tag
self.input_mask = input_mask
self.input_crop_mask = input_crop_mask
self.crop_info = crop_info
# 定义一个倍数函数
def round_up(num, mul):
return (num // mul + 1) * mul
class SettingConfig:
def __init__(self, config: dict, workParent):
self.config = config
self.webui_work_api = None
self.workParent = workParent
def to_dict(self):
return self.__dict__
def get_tag_mode(self):
tag_cfg = opt_dict(self.config, "tag")
return opt_dict(tag_cfg, "mode", TAG_MODE_NONE)
def get_tag_actions(self):
tag_cfg = opt_dict(self.config, "tag")
return opt_dict(tag_cfg, "actions", [])
def get_workspace_config(self) -> Workspace:
workspace_config = opt_dict(self.config, "workspace")
tmp_config = opt_dict(workspace_config, "tmp")
input = opt_dict(workspace_config, "input", "input")
output = opt_dict(workspace_config, "output", "output")
workspace_parent = self.workParent
tmp_parent = opt_dict(tmp_config, "parent", "tmp")
input_crop = opt_dict(tmp_config, "input_crop", "input_crop")
output_crop = opt_dict(tmp_config, "output_crop", "output_crop")
input_tag = opt_dict(tmp_config, "input_tag", "input_crop")
input_mask = opt_dict(tmp_config, "input_mask", "input_mask")
input_crop_mask = opt_dict(tmp_config, "input_crop_mask", "input_crop_mask")
crop_info = opt_dict(tmp_config, "crop_info", "crop_info.txt")
tmp_path = os.path.join(workspace_parent, tmp_parent)
return Workspace(
workspace_parent,
os.path.join(workspace_parent, input),
os.path.join(workspace_parent, output),
os.path.join(tmp_path, input_crop),
os.path.join(tmp_path, output_crop),
os.path.join(tmp_path, input_tag),
os.path.join(tmp_path, input_mask),
os.path.join(tmp_path, input_crop_mask),
os.path.join(tmp_path, crop_info),
)
def enable_tag(self):
tag_cfg = opt_dict(self.config, "tag")
return opt_dict(tag_cfg, "enable", True)
+229
View File
@@ -0,0 +1,229 @@
# pip install scenedetect opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple
from scenedetect.video_manager import VideoManager
from scenedetect.scene_manager import SceneManager
from scenedetect.stats_manager import StatsManager
from scenedetect.detectors.content_detector import ContentDetector
import os
import sys
import subprocess
from huggingface_hub import hf_hub_download
from faster_whisper import WhisperModel
import public_tools
from pathlib import Path
# 获取智能画面分割的时间或者秒数
def find_scenes(video_path, sensitivity):
print(
"正在计算分镜数据" + "sensitivity" + str(sensitivity) + "path : " + video_path
)
sys.stdout.flush()
video_manager = VideoManager([video_path])
stats_manager = StatsManager()
scene_manager = SceneManager(stats_manager)
# 使用contect-detector
scene_manager.add_detector(ContentDetector(threshold=float(sensitivity)))
shijian_list = []
try:
video_manager.set_downscale_factor()
video_manager.start()
scene_manager.detect_scenes(frame_source=video_manager)
scene_list = scene_manager.get_scene_list()
print("分镜数据列表:")
sys.stdout.flush()
for i, scene in enumerate(scene_list):
shijian_list.append([scene[0].get_timecode(), scene[1].get_timecode()])
print(
"Scene %2d: Start %s / Frame %d, End %s / Frame %d"
% (
i + 1,
scene[0].get_timecode(),
scene[0].get_frames(),
scene[1].get_timecode(),
scene[1].get_frames(),
)
)
sys.stdout.flush()
finally:
video_manager.release()
return shijian_list
# 如果不存在就创建
def createDir(file_dir):
# 如果不存在文件夹,就创建
if not os.path.isdir(file_dir):
os.mkdir(file_dir)
# 切分一个视频
def ClipVideo(video_path, out_folder, image_out_folder, sensitivity):
shijian_list = find_scenes(video_path, sensitivity) # 多组时间列表
shijian_list_len = len(shijian_list)
print("总共有%s个场景" % str(shijian_list_len))
sys.stdout.flush()
video_list = []
for i in range(0, shijian_list_len):
start_time_str = shijian_list[i][0]
end_time_str = shijian_list[i][1]
print("开始输出第" + str(i + 1) + "个分镜")
video_name = "{:05d}".format(i + 1)
out_video_file = os.path.join(out_folder, video_name + ".mp4")
sys.stdout.flush()
video_list.append(
{
"start_time_str": start_time_str,
"end_time_str": end_time_str,
"out_video_file": out_video_file,
"video_name": video_name,
}
)
# 使用 ffmpeg 裁剪视频
subprocess.run(
[
"ffmpeg",
"-i",
video_path,
"-ss",
start_time_str,
"-to",
end_time_str,
"-c:v",
"h264_nvenc",
"-preset",
"fast",
"-c:a",
"copy",
out_video_file,
"-loglevel",
"error",
],
check=True,
stderr=subprocess.PIPE,
)
print("分镜输出完成。开始抽帧")
sys.stdout.flush()
for vi in video_list:
h, m, s = vi["start_time_str"].split(":")
start_seconds = int(h) * 3600 + int(m) * 60 + float(s)
h, m, s = vi["end_time_str"].split(":")
end_seconds = int(h) * 3600 + int(m) * 60 + float(s)
print("正在抽帧:" + vi["video_name"])
sys.stdout.flush()
subprocess.run(
[
"ffmpeg",
"-ss",
str((end_seconds - start_seconds) / 2),
"-i",
vi["out_video_file"],
"-frames:v",
"1",
os.path.join(image_out_folder, vi["video_name"] + ".png"),
"-loglevel",
"error",
]
)
print("抽帧完成,开始识别文案")
sys.stdout.flush()
return video_list
def SplitAudio(video_out_folder, video_list):
# ffmpeg -i input_file.mp4 -vn -ab 128k output_file.mp3
print("正在分离音频!!")
mp3_list = []
sys.stdout.flush()
for v in video_list:
mp3_path = os.path.join(video_out_folder, v["video_name"] + ".mp3")
mp3_list.append(mp3_path)
subprocess.run(
[
"ffmpeg",
"-i",
v["out_video_file"],
"-vn",
"-ab",
"128k",
mp3_path,
"-loglevel",
"error",
],
check=True,
)
return mp3_list
def GetText(out_folder, mp3_list):
text = []
# 先获取模型
print("正在下载或加载模型")
sys.stdout.flush()
model_path = Path(
hf_hub_download(repo_id="Systran/faster-whisper-large-v3", filename="model.bin")
)
hf_hub_download(
repo_id="Systran/faster-whisper-large-v3",
filename="config.json",
)
hf_hub_download(
repo_id="Systran/faster-whisper-large-v3",
filename="preprocessor_config.json",
)
hf_hub_download(
repo_id="Systran/faster-whisper-large-v3",
filename="tokenizer.json",
)
hf_hub_download(
repo_id="Systran/faster-whisper-large-v3",
filename="vocabulary.json",
)
model = WhisperModel(
model_size_or_path=os.path.dirname(model_path),
device="auto",
local_files_only=True,
)
print("模型加载成功,开始识别")
sys.stdout.flush()
for mp in mp3_list:
segments, info = model.transcribe(
mp,
beam_size=5,
language="zh",
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=1000),
)
tmp_text = ""
for segment in segments:
tmp_text += segment.text + ""
print(mp + "识别完成")
sys.stdout.flush()
text.append(tmp_text)
# 数据写出
print("文本全部识别成功,正在写出")
sys.stdout.flush()
tools = public_tools.PublicTools()
tools.write_to_file(text, os.path.join(out_folder, "文案.txt"))
print("写出完成")
sys.stdout.flush()
def init(video_path, video_out_folder, image_out_folder, sensitivity):
v_l = ClipVideo(video_path, video_out_folder, image_out_folder, sensitivity)
# 开始分离音频
m_l = SplitAudio(video_out_folder, v_l)
# 开始识别字幕
GetText(os.path.dirname(video_out_folder), m_l)