mirror of
https://github.com/harry0703/MoneyPrinterTurbo.git
synced 2026-02-21 16:37:21 +08:00
247 lines
9.2 KiB
Python
247 lines
9.2 KiB
Python
import glob
|
||
import random
|
||
from typing import List
|
||
from PIL import ImageFont
|
||
from loguru import logger
|
||
from moviepy.editor import *
|
||
from moviepy.video.fx.crop import crop
|
||
from moviepy.video.tools.subtitles import SubtitlesClip
|
||
|
||
from app.models.schema import VideoAspect
|
||
from app.utils import utils
|
||
|
||
|
||
def get_bgm_file(bgm_name: str = "random"):
|
||
if not bgm_name:
|
||
return ""
|
||
if bgm_name == "random":
|
||
suffix = "*.mp3"
|
||
song_dir = utils.song_dir()
|
||
# 使用glob.glob获取指定扩展名的文件列表
|
||
files = glob.glob(os.path.join(song_dir, suffix))
|
||
# 使用random.choice从列表中随机选择一个文件
|
||
return random.choice(files)
|
||
|
||
file = os.path.join(utils.song_dir(), bgm_name)
|
||
if os.path.exists(file):
|
||
return file
|
||
return ""
|
||
|
||
|
||
def combine_videos(combined_video_path: str,
|
||
video_paths: List[str],
|
||
audio_file: str,
|
||
video_aspect: VideoAspect = VideoAspect.portrait,
|
||
max_clip_duration: int = 5,
|
||
threads: int = 2,
|
||
) -> str:
|
||
logger.info(f"combining {len(video_paths)} videos into one file: {combined_video_path}")
|
||
audio_clip = AudioFileClip(audio_file)
|
||
max_duration = audio_clip.duration
|
||
logger.info(f"max duration of audio: {max_duration} seconds")
|
||
# Required duration of each clip
|
||
req_dur = max_duration / len(video_paths)
|
||
logger.info(f"each clip will be maximum {req_dur} seconds long")
|
||
|
||
aspect = VideoAspect(video_aspect)
|
||
video_width, video_height = aspect.to_resolution()
|
||
|
||
clips = []
|
||
tot_dur = 0
|
||
# Add downloaded clips over and over until the duration of the audio (max_duration) has been reached
|
||
while tot_dur < max_duration:
|
||
for video_path in video_paths:
|
||
clip = VideoFileClip(video_path)
|
||
clip = clip.without_audio()
|
||
# Check if clip is longer than the remaining audio
|
||
if (max_duration - tot_dur) < clip.duration:
|
||
clip = clip.subclip(0, (max_duration - tot_dur))
|
||
# Only shorten clips if the calculated clip length (req_dur) is shorter than the actual clip to prevent still image
|
||
elif req_dur < clip.duration:
|
||
clip = clip.subclip(0, req_dur)
|
||
clip = clip.set_fps(30)
|
||
|
||
# Not all videos are same size, so we need to resize them
|
||
# logger.info(f"{video_path}: size is {clip.w} x {clip.h}, expected {video_width} x {video_height}")
|
||
if clip.w != video_width or clip.h != video_height:
|
||
if round((clip.w / clip.h), 4) < 0.5625:
|
||
clip = crop(clip,
|
||
width=clip.w,
|
||
height=round(clip.w / 0.5625),
|
||
x_center=clip.w / 2,
|
||
y_center=clip.h / 2
|
||
)
|
||
else:
|
||
clip = crop(clip,
|
||
width=round(0.5625 * clip.h),
|
||
height=clip.h,
|
||
x_center=clip.w / 2,
|
||
y_center=clip.h / 2
|
||
)
|
||
logger.info(f"resizing video to {video_width} x {video_height}")
|
||
clip = clip.resize((video_width, video_height))
|
||
|
||
if clip.duration > max_clip_duration:
|
||
clip = clip.subclip(0, max_clip_duration)
|
||
|
||
clips.append(clip)
|
||
tot_dur += clip.duration
|
||
|
||
final_clip = concatenate_videoclips(clips)
|
||
final_clip = final_clip.set_fps(30)
|
||
logger.info(f"writing")
|
||
final_clip.write_videofile(combined_video_path, threads=threads)
|
||
logger.success(f"completed")
|
||
return combined_video_path
|
||
|
||
|
||
def wrap_text(text, max_width, font='Arial', fontsize=60):
|
||
# 创建字体对象
|
||
font = ImageFont.truetype(font, fontsize)
|
||
|
||
def get_text_size(inner_text):
|
||
left, top, right, bottom = font.getbbox(inner_text)
|
||
return right - left, bottom - top
|
||
|
||
width, height = get_text_size(text)
|
||
if width <= max_width:
|
||
return text
|
||
|
||
logger.warning(f"wrapping text, max_width: {max_width}, text_width: {width}, text: {text}")
|
||
_wrapped_lines_ = []
|
||
# 使用textwrap尝试分行,然后检查每行是否符合宽度限制
|
||
|
||
chars = list(text)
|
||
_txt_ = ''
|
||
for char in chars:
|
||
_txt_ += char
|
||
_width, _height = get_text_size(_txt_)
|
||
if _width <= max_width:
|
||
continue
|
||
else:
|
||
_wrapped_lines_.append(_txt_)
|
||
_txt_ = ''
|
||
_wrapped_lines_.append(_txt_)
|
||
return '\n'.join(_wrapped_lines_)
|
||
|
||
|
||
def generate_video(video_path: str,
|
||
audio_path: str,
|
||
subtitle_path: str,
|
||
output_file: str,
|
||
video_aspect: VideoAspect = VideoAspect.portrait,
|
||
|
||
threads: int = 2,
|
||
|
||
font_name: str = "",
|
||
fontsize: int = 60,
|
||
stroke_color: str = "#000000",
|
||
stroke_width: float = 1.5,
|
||
text_fore_color: str = "white",
|
||
text_background_color: str = "transparent",
|
||
|
||
bgm_file: str = "",
|
||
):
|
||
aspect = VideoAspect(video_aspect)
|
||
video_width, video_height = aspect.to_resolution()
|
||
|
||
logger.info(f"start, video size: {video_width} x {video_height}")
|
||
logger.info(f" ① video: {video_path}")
|
||
logger.info(f" ② audio: {audio_path}")
|
||
logger.info(f" ③ subtitle: {subtitle_path}")
|
||
logger.info(f" ④ output: {output_file}")
|
||
|
||
if not font_name:
|
||
font_name = "STHeitiMedium.ttc"
|
||
font_path = os.path.join(utils.font_dir(), font_name)
|
||
logger.info(f"using font: {font_path}")
|
||
|
||
# 自定义的生成器函数,包含换行逻辑
|
||
def generator(txt):
|
||
# 应用自动换行
|
||
wrapped_txt = wrap_text(txt, max_width=video_width - 100,
|
||
font=font_path,
|
||
fontsize=fontsize) # 调整max_width以适应你的视频
|
||
return TextClip(
|
||
wrapped_txt,
|
||
font=font_path,
|
||
fontsize=fontsize,
|
||
color=text_fore_color,
|
||
bg_color=text_background_color,
|
||
stroke_color=stroke_color,
|
||
stroke_width=stroke_width,
|
||
print_cmd=False,
|
||
)
|
||
|
||
position_height = video_height - 200
|
||
if video_aspect == VideoAspect.landscape:
|
||
position_height = video_height - 100
|
||
|
||
clips = [
|
||
VideoFileClip(video_path),
|
||
# subtitles.set_position(lambda _t: ('center', position_height))
|
||
]
|
||
# Burn the subtitles into the video
|
||
if subtitle_path and os.path.exists(subtitle_path):
|
||
subtitles = SubtitlesClip(subtitle_path, generator)
|
||
clips.append(subtitles.set_position(lambda _t: ('center', position_height)))
|
||
|
||
result = CompositeVideoClip(clips)
|
||
|
||
# Add the audio
|
||
audio = AudioFileClip(audio_path)
|
||
result = result.set_audio(audio)
|
||
|
||
temp_output_file = f"{output_file}.temp.mp4"
|
||
logger.info(f"writing to temp file: {temp_output_file}")
|
||
result.write_videofile(temp_output_file, threads=threads or 2)
|
||
|
||
video_clip = VideoFileClip(temp_output_file)
|
||
if bgm_file:
|
||
logger.info(f"adding background music: {bgm_file}")
|
||
# Add song to video at 30% volume using moviepy
|
||
original_duration = video_clip.duration
|
||
original_audio = video_clip.audio
|
||
song_clip = AudioFileClip(bgm_file).set_fps(44100)
|
||
# Set the volume of the song to 10% of the original volume
|
||
song_clip = song_clip.volumex(0.2).set_fps(44100)
|
||
# Add the song to the video
|
||
comp_audio = CompositeAudioClip([original_audio, song_clip])
|
||
video_clip = video_clip.set_audio(comp_audio)
|
||
video_clip = video_clip.set_fps(30)
|
||
video_clip = video_clip.set_duration(original_duration)
|
||
# 编码为aac,否则iPhone里面无法播放
|
||
logger.info(f"encoding audio codec to aac")
|
||
video_clip.write_videofile(output_file, audio_codec="aac", threads=threads)
|
||
# delete the temp file
|
||
os.remove(temp_output_file)
|
||
logger.success(f"completed")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
txt = "hello 幸福经常被描述为最终人生目标和人类追求的核心 但它通常涉及对个人生活中意义和目的的深刻感悟"
|
||
font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
|
||
t = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
|
||
print(t)
|
||
|
||
task_id = "c12fd1e6-4b0a-4d65-a075-c87abe35a072"
|
||
task_dir = utils.task_dir(task_id)
|
||
video_file = f"{task_dir}/combined.mp4"
|
||
audio_file = f"{task_dir}/audio.mp3"
|
||
subtitle_file = f"{task_dir}/subtitle.srt"
|
||
output_file = f"{task_dir}/final.mp4"
|
||
generate_video(video_path=video_file,
|
||
audio_path=audio_file,
|
||
subtitle_path=subtitle_file,
|
||
output_file=output_file,
|
||
video_aspect=VideoAspect.portrait,
|
||
threads=2,
|
||
font_name="STHeitiMedium.ttc",
|
||
fontsize=60,
|
||
stroke_color="#000000",
|
||
stroke_width=1.5,
|
||
text_fore_color="white",
|
||
text_background_color="transparent",
|
||
bgm_file=""
|
||
)
|