Initial commit 添加MJ功能
This commit is contained in:
@@ -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])
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user