diff --git a/.config.template.toml b/.config.template.toml index ddfa293..18f1b00 100644 --- a/.config.template.toml +++ b/.config.template.toml @@ -12,11 +12,11 @@ password = { optional = false, nmin = 8, explanation = "the password of your red random = { optional = true, options = [true, false, ], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } -subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, nmax = 21, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } +subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr" } - +min_comments = { default = 20, optional = false, nmin = 15, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } [settings] allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false, @@ -29,10 +29,15 @@ opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false, ], explanation = "not yet implemented" } + +[settings.background] background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } -background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, - false, -], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } +#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, +# false, +#], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } +#background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" } + + [settings.tts] choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], example = "streamlabspolly", explanation = "The backend used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } diff --git a/GUI/index.html b/GUI/index.html new file mode 100644 index 0000000..807c9e7 --- /dev/null +++ b/GUI/index.html @@ -0,0 +1,281 @@ + + + + + + RedditVideoMakerBot + + + + + + + + + +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + diff --git a/README.md b/README.md index f8b5c02..b86b683 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,5 @@ CallumIO (c.#6837) - https://github.com/CallumIO Verq (Verq#2338) - https://github.com/CordlessCoder LukaHietala (Pix.#0001) - https://github.com/LukaHietala + +Freebiell (Freebie#6429) - https://github.com/FreebieII diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 13eaea1..efd762b 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from boto3 import Session -from botocore.exceptions import BotoCoreError, ClientError +from botocore.exceptions import BotoCoreError, ClientError, ProfileNotFound import sys from utils import settings import random @@ -30,36 +30,46 @@ class AWSPolly: self.voices = voices def run(self, text, filepath, random_voice: bool = False): - session = Session(profile_name="polly") - polly = session.client("polly") - if random_voice: - voice = self.randomvoice() - else: - if not settings.config["settings"]["tts"]["aws_polly_voice"]: - return ValueError( - f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}" - ) - voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: - # Request speech synthesis - response = polly.synthesize_speech( - Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural" - ) - except (BotoCoreError, ClientError) as error: - # The service returned an error, exit gracefully - print(error) - sys.exit(-1) + session = Session(profile_name="polly") + polly = session.client("polly") + if random_voice: + voice = self.randomvoice() + else: + if not settings.config["settings"]["tts"]["aws_polly_voice"]: + raise ValueError( + f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}" + ) + voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() + try: + # Request speech synthesis + response = polly.synthesize_speech( + Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural" + ) + except (BotoCoreError, ClientError) as error: + # The service returned an error, exit gracefully + print(error) + sys.exit(-1) - # Access the audio stream from the response - if "AudioStream" in response: - file = open(filepath, "wb") - file.write(response["AudioStream"].read()) - file.close() - # print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green") + # Access the audio stream from the response + if "AudioStream" in response: + file = open(filepath, "wb") + file.write(response["AudioStream"].read()) + file.close() + # print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green") - else: - # The response didn't contain audio data, exit gracefully - print("Could not stream audio") + else: + # The response didn't contain audio data, exit gracefully + print("Could not stream audio") + sys.exit(-1) + except ProfileNotFound: + print("You need to install the AWS CLI and configure your profile") + print( + """ + Linux: https://docs.aws.amazon.com/polly/latest/dg/setup-aws-cli.html + Windows: https://docs.aws.amazon.com/polly/latest/dg/install-voice-plugin2.html + """ + ) sys.exit(-1) def randomvoice(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index a171db7..c5ed714 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -47,7 +47,7 @@ class TTSEngine: Path(self.path).mkdir(parents=True, exist_ok=True) - # This file needs to be removed in case this post does not use post text, so that it wont appear in the final video + # This file needs to be removed in case this post does not use post text, so that it won't appear in the final video try: Path(f"{self.path}/posttext.mp3").unlink() except OSError: @@ -67,10 +67,12 @@ class TTSEngine: # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length: break - if not self.tts_module.max_chars: + if ( + len(comment["comment_body"]) > self.tts_module.max_chars + ): # Split the comment if it is too long + self.split_post(comment["comment_body"], idx) # Split the comment + else: # If the comment is not too long, just call the tts engine self.call_tts(f"{idx}", comment["comment_body"]) - else: - self.split_post(comment["comment_body"], idx) print_substep("Saved Text to MP3 files successfully.", style="bold green") return self.length, idx @@ -79,14 +81,20 @@ class TTSEngine: split_files = [] split_text = [ x.group().strip() - for x in re.finditer(rf" *((.{{0,{self.tts_module.max_chars}}})(\.|.$))", text) + for x in re.finditer( + r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text + ) ] - - idy = None + offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - self.call_tts(f"{idx}-{idy}.part", text_cut) - split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy}.part.mp3")) + if not text_cut or text_cut.isspace(): + offset += 1 + continue + + self.call_tts(f"{idx}-{idy - offset}.part", text_cut) + split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3")) + CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile( f"{self.path}/{idx}.mp3", fps=44100, verbose=False, logger=None ) @@ -107,9 +115,12 @@ class TTSEngine: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") - clip = AudioFileClip(f"{self.path}/{filename}.mp3") - self.length += clip.duration - clip.close() + try: + clip = AudioFileClip(f"{self.path}/{filename}.mp3") + self.length += clip.duration + clip.close() + except: + self.length = 0 def process_text(text: str): diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index b7365ab..75c4f49 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -37,8 +37,8 @@ class StreamlabsPolly: voice = self.randomvoice() else: if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]: - return ValueError( - f"Please set the config variable STREAMLABS_VOICE to a valid voice. options are: {voices}" + raise ValueError( + f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}" ) voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} diff --git a/main.py b/main.py index 8ce8725..10ab3c1 100755 --- a/main.py +++ b/main.py @@ -7,7 +7,6 @@ from utils.cleanup import cleanup from utils.console import print_markdown, print_step from utils import settings -# from utils.checker import envUpdate from video_creation.background import ( download_background, chop_background_video, @@ -17,7 +16,9 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -VERSION = "2.2.9" +__VERSION__ = "2.3" +__BRANCH__ = "master" + print( """ ██████╗ ███████╗██████╗ ██████╗ ██╗████████╗ ██╗ ██╗██╗██████╗ ███████╗ ██████╗ ███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗ @@ -32,7 +33,7 @@ print( print_markdown( "### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)" ) -print_step(f"You are using V{VERSION} of the bot") +print_step(f"You are using v{__VERSION__} of the bot") def main(POST_ID=None): diff --git a/pyproject.toml b/pyproject.toml index 261a1ba..94e52ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ playwright = "1.23.0" praw = "7.6.0" pytube = "12.1.0" requests = "2.28.1" -rich = "12.4.4" +rich = "12.5.1" toml = "0.10.2" translators = "5.3.1" diff --git a/reddit/subreddit.py b/reddit/subreddit.py index b64a52a..716a7fa 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -7,6 +7,7 @@ from praw.models import MoreComments from utils.console import print_step, print_substep from utils.subreddit import get_subreddit_undone from utils.videos import check_done +from utils.voice import sanitize_text def get_subreddit_threads(POST_ID: str): @@ -17,7 +18,7 @@ def get_subreddit_threads(POST_ID: str): print_substep("Logging into Reddit.") content = {} - if settings.config["reddit"]["creds"]["2fa"] == True: + if settings.config["reddit"]["creds"]["2fa"]: print("\nEnter your two-factor authentication code from your authenticator app.\n") code = input("> ") print() @@ -26,7 +27,7 @@ def get_subreddit_threads(POST_ID: str): else: passkey = settings.config["reddit"]["creds"]["password"] username = settings.config["reddit"]["creds"]["username"] - if username.casefold().startswith("u/"): + if str(username).casefold().startswith("u/"): username = username[2:] reddit = praw.Reddit( client_id=settings.config["reddit"]["creds"]["client_id"], @@ -54,7 +55,7 @@ def get_subreddit_threads(POST_ID: str): sub = settings.config["reddit"]["thread"]["subreddit"] print_substep(f"Using subreddit: r/{sub} from TOML config") subreddit_choice = sub - if subreddit_choice.casefold().startswith("r/"): # removes the r/ from the input + if str(subreddit_choice).casefold().startswith("r/"): # removes the r/ from the input subreddit_choice = subreddit_choice[2:] subreddit = reddit.subreddit( subreddit_choice @@ -64,11 +65,10 @@ def get_subreddit_threads(POST_ID: str): submission = reddit.submission(id=POST_ID) elif ( settings.config["reddit"]["thread"]["post_id"] - and len(settings.config["reddit"]["thread"]["post_id"].split("+")) == 1 + and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) else: - threads = subreddit.hot(limit=25) submission = get_subreddit_undone(threads, subreddit) submission = check_done(submission) # double-checking @@ -95,11 +95,15 @@ def get_subreddit_threads(POST_ID: str): if top_level_comment.body in ["[removed]", "[deleted]"]: continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78 if not top_level_comment.stickied: + sanitised = sanitize_text(top_level_comment.body) + if not sanitised or sanitised == " ": + continue if len(top_level_comment.body) <= int( settings.config["reddit"]["thread"]["max_comment_length"] ): if ( top_level_comment.author is not None + and sanitize_text(top_level_comment.body) is not None ): # if errors occur with this change to if not. content["comments"].append( { diff --git a/utils/subreddit.py b/utils/subreddit.py index 48dceba..4eb0108 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -34,11 +34,16 @@ def get_subreddit_undone(submissions: list, subreddit): if submission.stickied: print_substep("This post was pinned by moderators. Skipping...") continue + if submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]): + print_substep( + f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' + ) + continue return submission print("all submissions have been done going by top submission order") return get_subreddit_undone( subreddit.top(time_filter="hour"), subreddit - ) # all of the videos in hot have already been done + ) # all the videos in hot have already been done def already_done(done_videos: list, submission) -> bool: diff --git a/utils/voice.py b/utils/voice.py index 0272b09..a0709fa 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -36,7 +36,7 @@ def sleep_until(time): # Convert datetime to unix timestamp and adjust for locality if isinstance(time, datetime): - # If we're on Python 3 and the user specified a timezone, convert to UTC and get tje timestamp. + # If we're on Python 3 and the user specified a timezone, convert to UTC and get the timestamp. if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: @@ -81,7 +81,7 @@ def sanitize_text(text: str) -> str: result = re.sub(regex_urls, " ", text) # note: not removing apostrophes - regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]" + regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-–—%“”‘\"%\*/{}\[\]\(\)\\|<>=+]" result = re.sub(regex_expr, " ", result) result = result.replace("+", "plus").replace("&", "and") # remove extra whitespace diff --git a/video_creation/background.py b/video_creation/background.py index 7ef4321..be0f46c 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -30,7 +30,7 @@ background_options = { "https://www.youtube.com/watch?v=2X9QGY__0II", "rocket_league.mp4", "Orbital Gameplay", - "top", + lambda t: ("center", 200 + t), ), "minecraft": ( # Minecraft parkour "https://www.youtube.com/watch?v=n_Dv4JMiwK8", @@ -64,7 +64,7 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(): """Fetch the background/s configuration""" try: - choice = str(settings.config["settings"]["background_choice"]).casefold() + choice = str(settings.config["settings"]["background"]["background_choice"]).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None diff --git a/video_creation/final_video.py b/video_creation/final_video.py index f1e1f96..eb42247 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -3,20 +3,14 @@ import multiprocessing import os import re from os.path import exists -from typing import Dict, Tuple, Any - -import translators as ts - -from moviepy.editor import ( - VideoFileClip, - AudioFileClip, - ImageClip, - concatenate_videoclips, - concatenate_audioclips, - CompositeAudioClip, - CompositeVideoClip, -) -from moviepy.video.io.ffmpeg_tools import ffmpeg_merge_video_audio, ffmpeg_extract_subclip +from typing import Tuple, Any +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 from rich.console import Console from utils.cleanup import cleanup @@ -24,9 +18,7 @@ from utils.console import print_step, print_substep from utils.videos import save_data from utils import settings - console = Console() - W, H = 1080, 1920 @@ -37,9 +29,11 @@ def name_normalize(name: str) -> str: name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name) name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name) name = re.sub(r"\/", r"", name) + name[:30] lang = settings.config["reddit"]["thread"]["post_lang"] if lang: + import translators as ts print_substep("Translating filename...") translated_name = ts.google(name, to_language=lang) return translated_name @@ -49,7 +43,10 @@ def name_normalize(name: str) -> str: def make_final_video( - number_of_clips: int, length: int, reddit_obj: dict, background_config: Tuple[str, str, str, Any] + number_of_clips: int, + length: int, + reddit_obj: dict, + background_config: Tuple[str, str, str, Any], ): """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp Args: @@ -58,6 +55,11 @@ def make_final_video( reddit_obj (dict): The reddit object that contains the posts to read. background_config (Tuple[str, str, str, Any]): The background config to use. """ + # 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 print_step("Creating the final video 🎥") VideoFileClip.reW = lambda clip: clip.resize(width=W) VideoFileClip.reH = lambda clip: clip.resize(width=H) @@ -116,12 +118,18 @@ def make_final_video( filename = f"{name_normalize(title)}.mp4" subreddit = settings.config["reddit"]["thread"]["subreddit"] - save_data(subreddit, filename, title, idx, background_config[2]) - if not exists(f"./results/{subreddit}"): print_substep("The results folder didn't exist so I made it") os.makedirs(f"./results/{subreddit}") + # 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.write_videofile( "assets/temp/temp.mp4", fps=30, @@ -130,38 +138,13 @@ def make_final_video( verbose=False, threads=multiprocessing.cpu_count(), ) - if settings.config["settings"]["background_audio"]: - print("[bold green] Merging background audio with video") - if not exists(f"assets/backgrounds/background.mp3"): - print_substep( - "Cannot find assets/backgrounds/background.mp3 audio file didn't so skipping." - ) - ffmpeg_extract_subclip( - "assets/temp/temp.mp4", - 0, - final.duration, - targetname=f"results/{subreddit}/{filename}", - ) - else: - ffmpeg_merge_video_audio( - "assets/temp/temp.mp4", - "assets/backgrounds/background.mp3", - "assets/temp/temp_audio.mp4", - ) - ffmpeg_extract_subclip( # check if this gets run - "assets/temp/temp_audio.mp4", - 0, - final.duration, - targetname=f"results/{subreddit}/{filename}", - ) - else: - print("debug duck") - ffmpeg_extract_subclip( - "assets/temp/temp.mp4", - 0, - final.duration, - targetname=f"results/{subreddit}/{filename}", - ) + ffmpeg_extract_subclip( + "assets/temp/temp.mp4", + 0, + final.duration, + targetname=f"results/{subreddit}/{filename}", + ) + save_data(subreddit, filename, title, idx, background_config[2]) print_step("Removing temporary files 🗑") cleanups = cleanup() print_substep(f"Removed {cleanups} temporary files 🗑") diff --git a/video_creation/voices.py b/video_creation/voices.py index ffc0898..ac33dd7 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -34,7 +34,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: """ voice = settings.config["settings"]["tts"]["choice"] - if voice.casefold() in map(lambda _: _.casefold(), TTSProviders): + if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: while True: @@ -45,7 +45,6 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: break print("Unknown Choice") text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) - return text_to_mp3.run()