Version 3.2

The RedditVideoMakerBot has been updated to version 3.2, bringing exciting new features and bug fixes. Introducing **ElevenLabs TTS** for high-quality audio. Enjoy **background audio** and **random voices per comment**. Run the bot with one click using the **bat file**. Use the **zoom setting** for bigger text. Bug fixes include video downloader improvements, random story mode fix, updated dependencies, code optimizations, text size adjustments, enhanced Reddit credentials validation, and translator fixes. Enjoy the new features and **thanks to all contributors**!
This commit is contained in:
Simon authored and GitHub committed 2023-05-28 16:05:41 +02:00
1 parent f2d89a10dd
commit 0ea74e43f1
24 files changed
+539 -222

No files matched your search

+105 -42
View File
@@ -3,29 +3,36 @@ import random
import re
from pathlib import Path
from random import randrange
from typing import Any, Tuple
from typing import Any, Tuple, Dict
from moviepy.editor import VideoFileClip
from moviepy.editor import VideoFileClip, AudioFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from pytube import YouTube
from pytube.cli import on_progress
from utils import settings
from utils.console import print_step, print_substep
import yt_dlp
# Load background videos
with open("./utils/backgrounds.json") as json_file:
background_options = json.load(json_file)
# Remove "__comment" from backgrounds
background_options.pop("__comment", None)
def load_background_options():
background_options = {}
# Load background videos
with open("./utils/background_videos.json") as json_file:
background_options["video"] = json.load(json_file)
# Add position lambda function
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
for name in list(background_options.keys()):
pos = background_options[name][3]
# Load background audios
with open("./utils/background_audios.json") as json_file:
background_options["audio"] = json.load(json_file)
if pos != "center":
background_options[name][3] = lambda t: ("center", pos + t)
# Remove "__comment" from backgrounds
del background_options["video"]["__comment"]
del background_options["audio"]["__comment"]
for name in list(background_options["video"].keys()):
pos = background_options["video"][name][3]
if pos != "center":
background_options["video"][name][3] = lambda t: ("center", pos + t)
return background_options
def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]:
@@ -38,15 +45,22 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int
Returns:
tuple[int,int]: Start and end time of the randomized interval
"""
random_time = randrange(180, int(length_of_clip) - int(video_length))
initialValue = 180
# Issue #1649 - Ensures that will be a valid interval in the video
while int(length_of_clip) <= int(video_length + initialValue):
if initialValue == initialValue // 2:
raise Exception("Your background is too short for this video length")
else:
initialValue //= 2 # Divides the initial value by 2 until reach 0
random_time = randrange(initialValue, int(length_of_clip) - int(video_length))
return random_time, random_time + video_length
def get_background_config():
def get_background_config(mode: str):
"""Fetch the background/s configuration"""
try:
choice = str(
settings.config["settings"]["background"]["background_choice"]
settings.config["settings"]["background"][f"background_{mode}"]
).casefold()
except AttributeError:
print_substep("No background selected. Picking random background'")
@@ -54,57 +68,106 @@ def get_background_config():
# Handle default / not supported background using default option.
# Default : pick random from supported background.
if not choice or choice not in background_options:
choice = random.choice(list(background_options.keys()))
if not choice or choice not in background_options[mode]:
choice = random.choice(list(background_options[mode].keys()))
return background_options[choice]
return background_options[mode][choice]
def download_background(background_config: Tuple[str, str, str, Any]):
def download_background_video(background_config: Tuple[str, str, str, Any]):
"""Downloads the background/s video from YouTube."""
Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True)
Path("./assets/backgrounds/video/").mkdir(parents=True, exist_ok=True)
# note: make sure the file name doesn't include an - in it
uri, filename, credit, _ = background_config
if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
if Path(f"assets/backgrounds/video/{credit}-{filename}").is_file():
return
print_step(
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
)
print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
print_substep(f"Downloading {filename} from {uri}")
YouTube(uri, on_progress_callback=on_progress).streams.filter(
res="1080p"
).first().download("assets/backgrounds", filename=f"{credit}-{filename}")
ydl_opts = {
"format": "bestvideo[height<=1080][ext=mp4]",
"outtmpl": f"assets/backgrounds/video/{credit}-{filename}",
"retries": 10,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download(uri)
print_substep("Background video downloaded successfully! 🎉", style="bold green")
def chop_background_video(
background_config: Tuple[str, str, str, Any], video_length: int, reddit_object: dict
def download_background_audio(background_config: Tuple[str, str, str]):
"""Downloads the background/s audio from YouTube."""
Path("./assets/backgrounds/audio/").mkdir(parents=True, exist_ok=True)
# note: make sure the file name doesn't include an - in it
uri, filename, credit = background_config
if Path(f"assets/backgrounds/audio/{credit}-{filename}").is_file():
return
print_step(
"We need to download the backgrounds audio. they are fairly large but it's only done once. 😎"
)
print_substep("Downloading the backgrounds audio... please be patient 🙏 ")
print_substep(f"Downloading {filename} from {uri}")
ydl_opts = {
"outtmpl": f"./assets/backgrounds/audio/{credit}-{filename}",
"format": "bestaudio/best",
"extract_audio": True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([uri])
print_substep("Background audio downloaded successfully! 🎉", style="bold green")
def chop_background(
background_config: Dict[str, Tuple], video_length: int, reddit_object: dict
):
"""Generates the background footage to be used in the video and writes it to assets/temp/background.mp4
"""Generates the background audio and footage to be used in the video and writes it to assets/temp/background.mp3 and assets/temp/background.mp4
Args:
background_config (Tuple[str, str, str, Any]) : Current background configuration
background_config (Dict[str,Tuple]]) : Current background configuration
video_length (int): Length of the clip where the background footage is to be taken out of
"""
id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
if settings.config["settings"]["background"][f"background_audio_volume"] == 0:
print_step("Volume was set to 0. Skipping background audio creation . . .")
else:
print_step("Finding a spot in the backgrounds audio to chop...✂️")
audio_choice = (
f"{background_config['audio'][2]}-{background_config['audio'][1]}"
)
background_audio = AudioFileClip(f"assets/backgrounds/audio/{audio_choice}")
start_time_audio, end_time_audio = get_start_and_end_times(
video_length, background_audio.duration
)
background_audio = background_audio.subclip(start_time_audio, end_time_audio)
background_audio.write_audiofile(f"assets/temp/{id}/background.mp3")
print_step("Finding a spot in the backgrounds video to chop...✂️")
choice = f"{background_config[2]}-{background_config[1]}"
id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
background = VideoFileClip(f"assets/backgrounds/{choice}")
start_time, end_time = get_start_and_end_times(video_length, background.duration)
video_choice = f"{background_config['video'][2]}-{background_config['video'][1]}"
background_video = VideoFileClip(f"assets/backgrounds/video/{video_choice}")
start_time_video, end_time_video = get_start_and_end_times(
video_length, background_video.duration
)
# Extract video subclip
try:
ffmpeg_extract_subclip(
f"assets/backgrounds/{choice}",
start_time,
end_time,
f"assets/backgrounds/video/{video_choice}",
start_time_video,
end_time_video,
targetname=f"assets/temp/{id}/background.mp4",
)
except (OSError, IOError): # ffmpeg issue see #348
print_substep("FFMPEG issue. Trying again...")
with VideoFileClip(f"assets/backgrounds/{choice}") as video:
new = video.subclip(start_time, end_time)
with VideoFileClip(f"assets/backgrounds/video/{video_choice}") as video:
new = video.subclip(start_time_video, end_time_video)
new.write_videofile(f"assets/temp/{id}/background.mp4")
print_substep("Background video chopped successfully!", style="bold green")
return background_config[2]
return background_config["video"][2]
# Create a tuple for downloads background (background_audio_options, background_video_options)
background_options = load_background_options()
+103 -26
View File
@@ -1,29 +1,28 @@
import multiprocessing
import os
import re
import shutil
from os.path import exists # Needs to be imported specifically
from os.path import exists # Needs to be imported specifically
from typing import Final
from typing import Tuple, Any
from typing import Tuple, Any, Dict
import ffmpeg
import translators as ts
import translators
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.thumbnail import create_thumbnail
from utils.videos import save_data
console = Console()
from utils import settings
import tempfile
import threading
import time
console = Console()
class ProgressFfmpeg(threading.Thread):
def __init__(self, vid_duration_seconds, progress_update_callback):
@@ -73,7 +72,7 @@ def name_normalize(name: str) -> str:
lang = settings.config["reddit"]["thread"]["post_lang"]
if lang:
print_substep("Translating filename...")
translated_name = ts.google(name, to_language=lang)
translated_name = translators.google(name, to_language=lang)
return translated_name
else:
return name
@@ -98,17 +97,39 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
)
try:
output.run(quiet=True)
except Exception as e:
print(e)
exit()
except ffmpeg.Error as e:
print(e.stderr.decode("utf8"))
exit(1)
return output_path
def merge_background_audio(audio: ffmpeg, reddit_id: str):
"""Gather an audio and merge with assets/backgrounds/background.mp3
Args:
audio (ffmpeg): The TTS final audio but without background.
reddit_id (str): The ID of subreddit
"""
background_audio_volume = settings.config["settings"]["background"][
"background_audio_volume"
]
if background_audio_volume == 0:
return audio # Return the original audio
else:
# sets volume to config
bg_audio = ffmpeg.input(f"assets/temp/{reddit_id}/background.mp3").filter(
"volume",
background_audio_volume,
)
# Merges audio and background_audio
merged_audio = ffmpeg.filter([audio, bg_audio], "amix", duration="longest")
return merged_audio # Return merged audio
def make_final_video(
number_of_clips: int,
length: int,
reddit_obj: dict,
background_config: Tuple[str, str, str, Any],
background_config: Dict[str, Tuple],
):
"""Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp
Args:
@@ -121,13 +142,26 @@ def make_final_video(
W: Final[int] = int(settings.config["settings"]["resolution_w"])
H: Final[int] = int(settings.config["settings"]["resolution_h"])
opacity = settings.config["settings"]["opacity"]
reddit_id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
allowOnlyTTSFolder: bool = (
settings.config["settings"]["background"]["enable_extra_audio"]
and settings.config["settings"]["background"]["background_audio_volume"] != 0
)
print_step("Creating the final video 🎥")
background_clip = ffmpeg.input(prepare_background(reddit_id, W=W, H=H))
# Gather all audio clips
audio_clips = list()
if number_of_clips == 0 and settings.config["settings"]["storymode"] == "false":
print(
"No audio clips to gather. Please use a different TTS or post."
) # This is to fix the TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
exit()
if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 0:
audio_clips = [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")]
@@ -177,6 +211,7 @@ def make_final_video(
screenshot_width = int((W * 45) // 100)
audio = ffmpeg.input(f"assets/temp/{reddit_id}/audio.mp3")
final_audio = merge_background_audio(audio, reddit_id)
image_clips = list()
@@ -242,8 +277,9 @@ def make_final_video(
"v"
].filter("scale", screenshot_width, -1)
)
image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity)
background_clip = background_clip.overlay(
image_clips[i],
image_overlay,
enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})",
x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2",
@@ -258,15 +294,25 @@ def make_final_video(
subreddit = settings.config["reddit"]["thread"]["subreddit"]
if not exists(f"./results/{subreddit}"):
print_substep("The results folder didn't exist so I made it")
print_substep(
"The 'results' folder could not be found so it was automatically created."
)
os.makedirs(f"./results/{subreddit}")
if not exists(f"./results/{subreddit}/OnlyTTS") and allowOnlyTTSFolder:
print_substep(
"The 'OnlyTTS' folder could not be found so it was automatically created."
)
os.makedirs(f"./results/{subreddit}/OnlyTTS")
# create a thumbnail for the video
settingsbackground = settings.config["settings"]["background"]
if settingsbackground["background_thumbnail"]:
if not exists(f"./results/{subreddit}/thumbnails"):
print_substep("The results/thumbnails folder didn't exist so I made it")
print_substep(
"The 'results/thumbnails' folder could not be found so it was automatically created."
)
os.makedirs(f"./results/{subreddit}/thumbnails")
# get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail
first_image = next(
@@ -300,34 +346,36 @@ def make_final_video(
f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png"
)
text = f"Background by {background_config[2]}"
text = f"Background by {background_config['video'][2]}"
background_clip = ffmpeg.drawtext(
background_clip,
text=text,
x=f"(w-text_w)",
y=f"(h-text_h)",
fontsize=12,
fontsize=5,
fontcolor="White",
fontfile=os.path.join("fonts", "Roboto-Regular.ttf"),
)
background_clip = background_clip.filter("scale", W, H)
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):
def on_update_example(progress) -> None:
status = round(progress * 100, 2)
old_percentage = pbar.n
pbar.update(status - old_percentage)
path = f"results/{subreddit}/{filename}"
path = path[:251]
path = path + ".mp4"
defaultPath = f"results/{subreddit}"
with ProgressFfmpeg(length, on_update_example) as progress:
path = defaultPath + f"/{filename}"
path = (
path[:251] + ".mp4"
) # Prevent a error by limiting the path length, do not change this.
ffmpeg.output(
background_clip,
audio,
final_audio,
path,
f="mp4",
**{
@@ -342,12 +390,41 @@ def make_final_video(
capture_stdout=False,
capture_stderr=False,
)
old_percentage = pbar.n
pbar.update(100 - old_percentage)
pbar.close()
if allowOnlyTTSFolder:
path = defaultPath + f"/OnlyTTS/{filename}"
path = (
path[:251] + ".mp4"
) # Prevent a error by limiting the path length, do not change this.
print_step("Rendering the Only TTS Video 🎥")
with ProgressFfmpeg(length, on_update_example) as progress:
try:
ffmpeg.output(
background_clip,
audio,
path,
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,
)
except ffmpeg.Error as e:
print(e.stderr.decode("utf8"))
exit(1)
save_data(subreddit, filename + ".mp4", title, idx, background_config[2])
old_percentage = pbar.n
pbar.update(100 - old_percentage)
pbar.close()
save_data(subreddit, filename + ".mp4", title, idx, background_config["video"][2])
print_step("Removing temporary files 🗑")
cleanups = cleanup(reddit_id)
print_substep(f"Removed {cleanups} temporary files 🗑")
+67 -11
View File
@@ -3,7 +3,7 @@ import re
from pathlib import Path
from typing import Dict, Final
import translators as ts
import translators
from playwright.async_api import async_playwright # pylint: disable=unused-import
from playwright.sync_api import ViewportSize, sync_playwright
from rich.progress import track
@@ -11,6 +11,7 @@ from rich.progress import track
from utils import settings
from utils.console import print_step, print_substep
from utils.imagenarator import imagemaker
from utils.playwright import clear_cookie_by_name
from utils.videos import save_data
@@ -116,7 +117,30 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
page.locator("button[class$='m-full-width']").click()
page.wait_for_timeout(5000)
login_error_div = page.locator(".AnimatedForm__errorMessage").first
if login_error_div.is_visible():
login_error_message = login_error_div.inner_text()
if login_error_message.strip() == "":
# The div element is empty, no error
pass
else:
# The div contains an error message
print_substep(
"Your reddit credentials are incorrect! Please modify them accordingly in the config.toml file.",
style="red",
)
exit()
else:
pass
page.wait_for_load_state()
# Handle the redesign
# Check if the redesign optout cookie is set
if page.locator("#redesign-beta-optin-btn").is_visible():
# Clear the redesign optout cookie
clear_cookie_by_name(context, "redesign_optout")
# Reload the page for the redesign to take effect
page.reload()
# Get the thread screenshot
page.goto(reddit_object["thread_url"], timeout=0)
page.set_viewport_size(ViewportSize(width=W, height=H))
@@ -144,13 +168,13 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
if lang:
print_substep("Translating post...")
texts_in_tl = ts.google(
texts_in_tl = translators.google(
reddit_object["thread_title"],
to_language=lang,
)
page.evaluate(
"tl_content => document.querySelector('[data-test-id=\"post-content\"] > div:nth-child(3) > div > div').textContent = tl_content",
"tl_content => document.querySelector('[data-adclicklocation=\"title\"] > div > div > h1').textContent = tl_content",
texts_in_tl,
)
else:
@@ -158,9 +182,20 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
postcontentpath = f"assets/temp/{reddit_id}/png/title.png"
try:
page.locator('[data-test-id="post-content"]').screenshot(
path=postcontentpath
)
if settings.config["settings"]["zoom"] != 1:
# store zoom settings
zoom = settings.config["settings"]["zoom"]
# zoom the body of the page
page.evaluate("document.body.style.zoom=" + str(zoom))
# as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom
location = page.locator('[data-test-id="post-content"]').bounding_box()
for i in location:
location[i] = float("{:.2f}".format(location[i] * zoom))
page.screenshot(clip=location, path=postcontentpath)
else:
page.locator('[data-test-id="post-content"]').screenshot(
path=postcontentpath
)
except Exception as e:
print_substep("Something went wrong!", style="red")
resp = input(
@@ -202,10 +237,10 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
page.goto(f'https://reddit.com{comment["comment_url"]}', timeout=0)
# translate code
# translate code
if settings.config["reddit"]["thread"]["post_lang"]:
comment_tl = ts.google(
comment_tl = translators.google(
comment["comment_body"],
to_language=settings.config["reddit"]["thread"]["post_lang"],
)
@@ -214,9 +249,29 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
[comment_tl, comment["comment_id"]],
)
try:
page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png"
)
if settings.config["settings"]["zoom"] != 1:
# store zoom settings
zoom = settings.config["settings"]["zoom"]
# zoom the body of the page
page.evaluate("document.body.style.zoom=" + str(zoom))
# scroll comment into view
page.locator(
f"#t1_{comment['comment_id']}"
).scroll_into_view_if_needed()
# as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom
location = page.locator(
f"#t1_{comment['comment_id']}"
).bounding_box()
for i in location:
location[i] = float("{:.2f}".format(location[i] * zoom))
page.screenshot(
clip=location,
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png",
)
else:
page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png"
)
except TimeoutError:
del reddit_object["comments"]
screenshot_num += 1
@@ -227,3 +282,4 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
browser.close()
print_substep("Screenshots downloaded Successfully.", style="bold green")
+2
View File
@@ -7,6 +7,7 @@ from TTS.TikTok import TikTok
from TTS.aws_polly import AWSPolly
from TTS.engine_wrapper import TTSEngine
from TTS.pyttsx import pyttsx
from TTS.elevenlabs import elevenlabs
from TTS.streamlabs_polly import StreamlabsPolly
from utils import settings
from utils.console import print_table, print_step
@@ -19,6 +20,7 @@ TTSProviders = {
"StreamlabsPolly": StreamlabsPolly,
"TikTok": TikTok,
"pyttsx": pyttsx,
"ElevenLabs": elevenlabs,
}