@ -1,33 +1,67 @@
import multiprocessing
import os
import re
import multiprocessing
from os . path import exists
from typing import Tuple , Any , Final
import translators as ts
import shutil
from os . path import exists
from typing import Final
from typing import Tuple , Any
from PIL import Image
from moviepy . audio . AudioClip import concatenate_audioclips , CompositeAudioClip
from moviepy . audio . io . AudioFileClip import AudioFileClip
from moviepy . video . VideoClip import ImageClip
from moviepy . video . compositing . CompositeVideoClip import CompositeVideoClip
from moviepy . video . compositing . concatenate import concatenate_videoclips
from moviepy . video . io . VideoFileClip import VideoFileClip
from moviepy . video . io . ffmpeg_tools import ffmpeg_extract_subclip
import ffmpeg
import translators as ts
from PIL import Image
from rich . console import Console
from rich . progress import track
from utils import settings
from utils . cleanup import cleanup
from utils . console import print_step , print_substep
from utils . video import Video
from utils . videos import save_data
from utils . thumbnail import create_thumbnail
from utils import settings
from utils . thumbnail import create_thumbnail
from utils . videos import save_data
console = Console ( )
import tempfile
import threading
import time
class ProgressFfmpeg ( threading . Thread ) :
def __init__ ( self , vid_duration_seconds , progress_update_callback ) :
threading . Thread . __init__ ( self , name = ' ProgressFfmpeg ' )
self . stop_event = threading . Event ( )
self . output_file = tempfile . NamedTemporaryFile ( mode = ' w+ ' , delete = False )
self . vid_duration_seconds = vid_duration_seconds
self . progress_update_callback = progress_update_callback
def run ( self ) :
while not self . stop_event . is_set ( ) :
latest_progress = self . get_latest_ms_progress ( )
if latest_progress is not None :
completed_percent = latest_progress / self . vid_duration_seconds
self . progress_update_callback ( completed_percent )
time . sleep ( 1 )
def get_latest_ms_progress ( self ) :
lines = self . output_file . readlines ( )
if lines :
for line in lines :
if ' out_time_ms ' in line :
out_time_ms = line . split ( ' = ' ) [ 1 ]
return int ( out_time_ms ) / 1000000.0
return None
def stop ( self ) :
self . stop_event . set ( )
def __enter__ ( self ) :
self . start ( )
return self
def __exit__ ( self , * args , * * kwargs ) :
self . stop ( )
def name_normalize ( name : str ) - > str :
name = re . sub ( r ' [? \\ " % *:|<>] ' , " " , name )
@ -46,22 +80,13 @@ def name_normalize(name: str) -> str:
return name
def prepare_background ( reddit_id : str , W : int , H : int ) - > VideoFileClip :
clip = (
VideoFileClip ( f " assets/temp/ { reddit_id } /background.mp4 " )
. without_audio ( )
. resize ( height = H )
)
# calculate the center of the background clip
c = clip . w / / 2
# calculate the coordinates where to crop
half_w = W / / 2
x1 = c - half_w
x2 = c + half_w
return clip . crop ( x1 = x1 , y1 = 0 , x2 = x2 , y2 = H )
def prepare_background ( reddit_id : str , W : int , H : int ) - > str :
output_path = f " assets/temp/ { reddit_id } /background_noaudio.mp4 "
output = ffmpeg . input ( f " assets/temp/ { reddit_id } /background.mp4 " ) . filter ( ' crop ' , f " ih*( { W } / { H } ) " , " ih " ) . output (
output_path , an = None ,
* * { " c:v " : " h264 " , " b:v " : " 20M " , " b:a " : " 192k " , " threads " : multiprocessing . cpu_count ( ) } ) . overwrite_output ( )
output . run ( quiet = True )
return output_path
def make_final_video (
@ -81,103 +106,93 @@ def make_final_video(
W : Final [ int ] = int ( settings . config [ " settings " ] [ " resolution_w " ] )
H : Final [ int ] = int ( settings . config [ " settings " ] [ " resolution_h " ] )
# try: # if it isn't found (i.e you just updated and copied over config.toml) it will throw an error
# VOLUME_MULTIPLIER = settings.config["settings"]['background']["background_audio_volume"]
# except (TypeError, KeyError):
# print('No background audio volume found in config.toml. Using default value of 1.')
# VOLUME_MULTIPLIER = 1
reddit_id = re . sub ( r " [^ \ w \ s-] " , " " , reddit_obj [ " thread_id " ] )
print_step ( " Creating the final video 🎥 " )
VideoFileClip . reW = lambda clip : clip . resize ( width = W )
VideoFileClip . reH = lambda clip : clip . resize ( width = H )
opacity = settings . config [ " settings " ] [ " opacity " ]
transition = settings . config [ " settings " ] [ " transition " ]
background_clip = prepare_background ( reddit_id , W = W , H = H )
background_clip = ffmpeg . input ( prepare_background ( reddit_id , W = W , H = H ) )
# Gather all audio clips
audio_clips = list ( )
if settings . config [ " settings " ] [ " storymode " ] :
if settings . config [ " settings " ] [ " storymodemethod " ] == 0 :
audio_clips = [ AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) ]
audio_clips . insert ( 1 , AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/postaudio.mp3 " ) )
audio_clips = [ ffmpeg . input ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) ]
audio_clips . insert ( 1 , ffmpeg. input ( f " assets/temp/ { reddit_id } /mp3/postaudio.mp3 " ) )
elif settings . config [ " settings " ] [ " storymodemethod " ] == 1 :
audio_clips = [
AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/postaudio- { i } .mp3 " )
ffmpeg. input ( f " assets/temp/ { reddit_id } /mp3/postaudio- { i } .mp3 " )
for i in track (
range ( number_of_clips + 1 ) , " Collecting the audio files... "
)
]
audio_clips . insert ( 0 , AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) )
audio_clips . insert ( 0 , ffmpeg. input ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) )
else :
audio_clips = [
AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/ { i } .mp3 " )
for i in range ( number_of_clips )
]
audio_clips . insert ( 0 , AudioFileClip ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) )
audio_concat = concatenate_audioclips ( audio_clips )
audio_composite = CompositeAudioClip ( [ audio_concat ] )
audio_clips = [ ffmpeg . input ( f " assets/temp/ { reddit_id } /mp3/ { i } .mp3 " ) for i in range ( number_of_clips ) ]
audio_clips . insert ( 0 , ffmpeg . input ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) )
audio_clips_durations = [ float ( ffmpeg . probe ( f " assets/temp/ { reddit_id } /mp3/ { i } .mp3 " ) [ ' format ' ] [ ' duration ' ] ) for i
in
range ( number_of_clips ) ]
audio_clips_durations . insert ( 0 , float (
ffmpeg . probe ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) [ ' format ' ] [ ' duration ' ] ) )
audio_concat = ffmpeg . concat ( * audio_clips , a = 1 , v = 0 )
ffmpeg . output ( audio_concat , f " assets/temp/ { reddit_id } /audio.mp3 " , * * { " b:a " : " 192k " } ) . overwrite_output ( ) . run (
quiet = True )
console . log ( f " [bold green] Video Will Be: { length } Seconds Long " )
# add title to video
image_clips = [ ]
# Gather all images
new_opacity = 1 if opacity is None or float ( opacity ) > = 1 else float ( opacity )
new_transition = (
0 if transition is None or float ( transition ) > 2 else float ( transition )
)
screenshot_width = int ( ( W * 90 ) / / 100 )
# Create a screenshot_width variable to scale the screenshots to the correct size, the calculation is int((W * 90) // 100)
# Convert it to a ffmpeg one with iw-
screenshot_width = int ( ( W * 45 ) / / 100 )
audio = ffmpeg . input ( f " assets/temp/ { reddit_id } /audio.mp3 " )
image_clips = list ( )
image_clips . insert (
0 ,
ImageClip ( f " assets/temp/ { reddit_id } /png/title.png " )
. set_duration ( audio_clips [ 0 ] . duration )
. resize ( width = screenshot_width )
. set_opacity ( new_opacity )
. crossfadein ( new_transition )
. crossfadeout ( new_transition ) ,
ffmpeg . input ( f " assets/temp/ { reddit_id } /png/title.png " ) [ ' v ' ]
. filter ( ' scale ' , screenshot_width , - 1 )
)
current_time = 0
if settings . config [ " settings " ] [ " storymode " ] :
audio_clips_durations = [
float ( ffmpeg . probe ( f " assets/temp/ { reddit_id } /mp3/postaudio- { i } .mp3 " ) [ ' format ' ] [ ' duration ' ] ) for i in
range ( number_of_clips ) ]
audio_clips_durations . insert ( 0 , float (
ffmpeg . probe ( f " assets/temp/ { reddit_id } /mp3/title.mp3 " ) [ ' format ' ] [ ' duration ' ] ) )
if settings . config [ " settings " ] [ " storymodemethod " ] == 0 :
image_clips . insert (
1 ,
ImageClip ( f " assets/temp/ { reddit_id } /png/story_content.png " )
. set_duration ( audio_clips [ 1 ] . duration )
. set_position ( " center " )
. resize ( width = screenshot_width )
. set_opacity ( float ( opacity ) ) ,
ffmpeg . input ( f " assets/temp/ { reddit_id } /png/story_content.png " )
. filter ( ' scale ' , screenshot_width , - 1 )
)
background_clip = background_clip . overlay ( image_clips [ 1 ] ,
enable = f ' between(t, { current_time } , { current_time + audio_clips_durations [ 1 ] } ) ' ,
x = ' (main_w-overlay_w)/2 ' , y = ' (main_h-overlay_h)/2 ' )
current_time + = audio_clips_durations [ 1 ]
elif settings . config [ " settings " ] [ " storymodemethod " ] == 1 :
for i in track (
range ( 0 , number_of_clips + 1 ) , " Collecting the image files... "
) :
image_clips . append (
ImageClip ( f " assets/temp/ { reddit_id } /png/img { i } .png " )
. set_duration ( audio_clips [ i + 1 ] . duration )
. resize ( width = screenshot_width )
. set_opacity ( new_opacity )
# .crossfadein(new_transition)
# .crossfadeout(new_transition)
ffmpeg . input ( f " assets/temp/ { reddit_id } /png/img { i } .png " ) [ ' v ' ]
. filter ( ' scale ' , 1080 , - 1 )
)
background_clip = background_clip . overlay ( image_clips [ i ] ,
enable = f ' between(t, { current_time } , { current_time + audio_clips_durations [ i ] } ) ' ,
x = ' (main_w-overlay_w)/2 ' , y = ' (main_h-overlay_h)/2 ' )
current_time + = audio_clips_durations [ i ]
else :
for i in range ( 0 , number_of_clips ) :
for i in range ( 0 , number_of_clips + 1 ) :
image_clips . append (
ImageClip ( f " assets/temp/ { reddit_id } /png/comment_ { i } .png " )
. set_duration ( audio_clips [ i + 1 ] . duration )
. resize ( width = screenshot_width )
. set_opacity ( new_opacity )
. crossfadein ( new_transition )
. crossfadeout ( new_transition )
ffmpeg . input ( f " assets/temp/ { reddit_id } /png/comment_ { i } .png " ) [ ' v ' ]
. filter ( ' scale ' , screenshot_width , - 1 )
)
background_clip = background_clip . overlay ( image_clips [ i ] ,
enable = f ' between(t, { current_time } , { current_time + audio_clips_durations [ i ] } ) ' ,
x = ' (main_w-overlay_w)/2 ' , y = ' (main_h-overlay_h)/2 ' )
current_time + = audio_clips_durations [ i ]
img_clip_pos = background_config [ 3 ]
image_concat = concatenate_videoclips ( image_clips ) . set_position (
img_clip_pos
) # note transition kwarg for delay in imgs
image_concat . audio = audio_composite
final = CompositeVideoClip ( [ background_clip , image_concat ] )
title = re . sub ( r " [^ \ w \ s-] " , " " , reddit_obj [ " thread_title " ] )
idx = re . sub ( r " [^ \ w \ s-] " , " " , reddit_obj [ " thread_id " ] )
title_thumb = reddit_obj [ " thread_title " ]
@ -189,7 +204,7 @@ def make_final_video(
print_substep ( " The results folder didn ' t exist so I made it " )
os . makedirs ( f " ./results/ { subreddit } " )
# create a t umbnail for the video
# create a t h umbnail for the video
settingsbackground = settings . config [ " settings " ] [ " background " ]
if settingsbackground [ " background_thumbnail " ] :
@ -209,7 +224,7 @@ def make_final_video(
if first_image is None :
print_substep ( " No png files found in assets/backgrounds " , " red " )
if settingsbackground [ " background_thumbnail " ] and first_imag e:
els e:
font_family = settingsbackground [ " background_thumbnail_font_family " ]
font_size = settingsbackground [ " background_thumbnail_font_size " ]
font_color = settingsbackground [ " background_thumbnail_font_color " ]
@ -219,8 +234,37 @@ def make_final_video(
thumbnailSave . save ( f " ./assets/temp/ { reddit_id } /thumbnail.png " )
print_substep ( f " Thumbnail - Building Thumbnail in assets/temp/ { reddit_id } /thumbnail.png " )
# create a tumbnail for the video
settingsbackground = settings . config [ " settings " ] [ " background " ]
text = f " Background by { background_config [ 2 ] } "
background_clip = ffmpeg . drawtext ( background_clip ,
text = text ,
x = f ' (w-text_w) ' , y = f ' (h-text_h) ' ,
fontsize = 12 ,
fontcolor = " White " ,
fontfile = os . path . join ( " fonts " , " Roboto-Regular.ttf " ) )
print_step ( " Rendering the video 🎥 " )
from tqdm import tqdm
pbar = tqdm ( total = 100 , desc = " Progress: " , bar_format = " {l_bar} {bar} " , unit = " % " )
def on_update_example ( progress ) :
status = round ( progress * 100 , 2 )
old_percentage = pbar . n
pbar . update ( status - old_percentage )
position = W / / 2 , H - 20
ffmpeg . filter ( filter_name = ' drawtext ' , stream_spec = background_clip , text = text , fontfile = ' fonts/Roboto-Regular.ttf ' , fontsize = 12 ,
fontcolor = ' white ' , x = position [ 0 ] , y = position [ 1 ] , box = 1 )
with ProgressFfmpeg ( length , on_update_example ) as progress :
ffmpeg . output ( background_clip , audio , f " results/ { subreddit } / { filename } .mp4 " , f = ' mp4 ' ,
* * { " c:v " : " h264 " , " b:v " : " 20M " , " b:a " : " 192k " ,
" threads " : multiprocessing . cpu_count ( ) } ) . overwrite_output ( ) . global_args ( ' -progress ' ,
progress . output_file . name ) . run (
quiet = True , overwrite_output = True , capture_stdout = False , capture_stderr = False )
old_percentage = pbar . n
pbar . update ( 100 - old_percentage )
pbar . close ( )
if settingsbackground [ " background_thumbnail " ] :
if not exists ( f " ./results/ { subreddit } /thumbnails " ) :
@ -249,37 +293,9 @@ def make_final_video(
thumbnailSave . save ( f " ./assets/temp/ { reddit_id } /thumbnail.png " )
print_substep ( f " Thumbnail - Building Thumbnail in assets/temp/ { reddit_id } /thumbnail.png " )
# if settings.config["settings"]['background']["background_audio"] and exists(f"assets/backgrounds/background.mp3"):
# audioclip = mpe.AudioFileClip(f"assets/backgrounds/background.mp3").set_duration(final.duration)
# audioclip = audioclip.fx( volumex, 0.2)
# final_audio = mpe.CompositeAudioClip([final.audio, audioclip])
# # lowered_audio = audio_background.multiply_volume( # todo get this to work
# # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx
# final.set_audio(final_audio)
final = Video ( final ) . add_watermark (
text = f " Background credit: { background_config [ 2 ] } " ,
opacity = 0.4 ,
redditid = reddit_obj ,
)
final . write_videofile (
f " assets/temp/ { reddit_id } /temp.mp4 " ,
fps = int ( settings . config [ " settings " ] [ " fps " ] ) ,
audio_codec = " aac " ,
audio_bitrate = " 192k " ,
verbose = False ,
threads = multiprocessing . cpu_count ( ) ,
#preset="ultrafast", # for testing purposes
)
ffmpeg_extract_subclip (
f " assets/temp/ { reddit_id } /temp.mp4 " ,
0 ,
length ,
targetname = f " results/ { subreddit } / { filename } .mp4 " ,
)
# get the thumbnail image from assets/temp/id/thumbnail.png and save it in results/subreddit/thumbnails
if settingsbackground [ " background_thumbnail " ] and exists ( f " assets/temp/ { id} /thumbnail.png " ) :
shutil . move ( f " assets/temp/ { id} /thumbnail.png " , f " ./results/ { subreddit } /thumbnails/ { filename } .png " )
if settingsbackground [ " background_thumbnail " ] and exists ( f " assets/temp/ { reddit_id } /thumbnail.png " ) :
shutil . move ( f " assets/temp/ { reddit_id } /thumbnail.png " , f " ./results/ { subreddit } /thumbnails/ { filename } .png " )
save_data ( subreddit , filename + " .mp4 " , title , idx , background_config [ 2 ] )
print_step ( " Removing temporary files 🗑 " )