diff --git a/GUI.py b/GUI.py index 4588083..7b8b23c 100644 --- a/GUI.py +++ b/GUI.py @@ -43,7 +43,7 @@ def index(): @app.route("/backgrounds", methods=["GET"]) def backgrounds(): - return render_template("backgrounds.html", file="backgrounds.json") + return render_template("backgrounds.html", file="background_videos.json") @app.route("/background/add", methods=["POST"]) @@ -91,10 +91,10 @@ def videos_json(): return send_from_directory("video_creation/data", "videos.json") -# Make backgrounds.json accessible -@app.route("/backgrounds.json") +# Make background_videos.json accessible +@app.route("/background_videos.json") def backgrounds_json(): - return send_from_directory("utils", "backgrounds.json") + return send_from_directory("utils", "background_videos.json") # Make videos in results folder accessible diff --git a/GUI/backgrounds.html b/GUI/backgrounds.html index 541e39f..438aa90 100644 --- a/GUI/backgrounds.html +++ b/GUI/backgrounds.html @@ -125,7 +125,7 @@ // Show background videos $(document).ready(function () { - $.getJSON("backgrounds.json", + $.getJSON("background_videos.json", function (data) { delete data["__comment"]; var background = ''; diff --git a/GUI/settings.html b/GUI/settings.html index 1f0ef2e..854e10d 100644 --- a/GUI/settings.html +++ b/GUI/settings.html @@ -200,12 +200,12 @@
- +
- diff --git a/TTS/GTTS.py b/TTS/GTTS.py index bff100f..53f78dc 100644 --- a/TTS/GTTS.py +++ b/TTS/GTTS.py @@ -4,13 +4,15 @@ from gtts import gTTS from utils import settings +from typing import Optional + class GTTS: def __init__(self): self.max_chars = 5000 self.voices = [] - def run(self, text, filepath): + def run(self, text, filepath, random_voice: bool = False, voice: Optional[str] = None): tts = gTTS( text=text, lang=settings.config["reddit"]["thread"]["post_lang"] or "en", @@ -18,5 +20,5 @@ class GTTS: ) tts.save(filepath) - def randomvoice(self): - return random.choice(self.voices) + def random_voice(self): + return random.choice(self.voices) \ No newline at end of file diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 29542e2..efe77b4 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -75,6 +75,8 @@ vocals: Final[tuple] = ( "en_female_ht_f08_wonderful_world", # Dramatic ) +all_voices: Final[tuple] = disney_voices + eng_voices + non_eng_voices + vocals +# comment out a voice to make it unavailable to be randomly selected class TikTok: """TikTok Text-to-Speech Wrapper""" @@ -93,12 +95,13 @@ class TikTok: # set the headers to the session, so we don't have to do it for every request self._session.headers = headers - def run(self, text: str, filepath: str, random_voice: bool = False): - if random_voice: - voice = self.random_voice() - else: - # if tiktok_voice is not set in the config file, then use a random voice - voice = settings.config["settings"]["tts"].get("tiktok_voice", None) + def run(self, text: str, filepath: str, random_voice: bool = False, voice: Optional[str] = None): + if not voice: + if random_voice: + voice = self.random_voice() + else: + # if tiktok_voice is not set in the config file, then use a random voice + voice = settings.config["settings"]["tts"].get("tiktok_voice", self.random_voice()) # get the audio from the TikTok API data = self.get_voices(voice=voice, text=text) diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 4d55860..a11cb65 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -30,13 +30,16 @@ class AWSPolly: self.max_chars = 3000 self.voices = voices - def run(self, text, filepath, random_voice: bool = False): + def run(self, text, filepath, random_voice: bool = False, voice: str = None): try: session = Session(profile_name="polly") polly = session.client("polly") - if random_voice: - voice = self.randomvoice() - else: + + if voice: # If voice is explicitly provided, use it + voice = voice.capitalize() + elif random_voice: # Else if random_voice is set to True, pick a random voice + voice = self.random_voice() + else: # If none of the above, use the voice from the settings 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}" @@ -54,11 +57,8 @@ class AWSPolly: # 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") - + with open(filepath, "wb") as file: + file.write(response["AudioStream"].read()) else: # The response didn't contain audio data, exit gracefully print("Could not stream audio") @@ -73,5 +73,5 @@ class AWSPolly: ) sys.exit(-1) - def randomvoice(self): + def random_voice(self): return random.choice(self.voices) diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index e18bba9..9638bde 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -22,11 +22,13 @@ class elevenlabs: self.max_chars = 2500 self.voices = voices - def run(self, text, filepath, random_voice: bool = False): - if random_voice: - voice = self.randomvoice() - else: - voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() + def run(self, text, filepath, voice=None, random_voice: bool = False): + + if not voice: # If voice is not provided directly + if random_voice: + voice = self.random_voice() + else: + voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() if settings.config["settings"]["tts"]["elevenlabs_api_key"]: api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] @@ -38,5 +40,5 @@ class elevenlabs: audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") save(audio=audio, filename=filepath) - def randomvoice(self): + def random_voice(self): return random.choice(self.voices) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 7a73d61..6d8c8ec 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -1,7 +1,7 @@ import os import re from pathlib import Path -from typing import Tuple +from typing import Tuple, Optional import numpy as np import translators @@ -73,38 +73,38 @@ class TTSEngine: print_step("Saving Text to MP3 files...") self.add_periods() - self.call_tts("title", process_text(self.reddit_object["thread_title"])) - # processed_text = ##self.reddit_object["thread_post"] != "" + # Select a voice for the title and thread body if in story mode + voice = self.tts_module.random_voice() if settings.config["settings"]["tts"]["random_voice"] else None + self.call_tts("title", process_text(self.reddit_object["thread_title"]), voice) + idx = 0 if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymodemethod"] == 0: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: - self.split_post(self.reddit_object["thread_post"], "postaudio") + self.split_post(self.reddit_object["thread_post"], "postaudio", voice) else: - self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) + self.call_tts("postaudio", process_text(self.reddit_object["thread_post"]), voice) elif settings.config["settings"]["storymodemethod"] == 1: for idx, text in track(enumerate(self.reddit_object["thread_post"])): - self.call_tts(f"postaudio-{idx}", process_text(text)) - + self.call_tts(f"postaudio-{idx}", process_text(text), voice) else: for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + comment_voice = self.tts_module.random_voice() if settings.config["settings"]["tts"]["random_voice"] else None # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length and idx > 1: self.length -= self.last_clip_length idx -= 1 break - 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 + 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, comment_voice) # Split the comment else: # If the comment is not too long, just call the tts engine - self.call_tts(f"{idx}", process_text(comment["comment_body"])) + self.call_tts(f"{idx}", process_text(comment["comment_body"]), comment_voice) print_substep("Saved Text to MP3 files successfully.", style="bold green") return self.length, idx - def split_post(self, text: str, idx): + def split_post(self, text: str, idx, voice: Optional[str] = None): split_files = [] split_text = [ x.group().strip() @@ -123,7 +123,7 @@ class TTSEngine: print("newtext was blank because sanitized split text resulted in none") continue else: - self.call_tts(f"{idx}-{idy}.part", newtext) + self.call_tts(f"{idx}-{idy}.part", newtext, voice) with open(f"{self.path}/list.txt", "w") as f: for idz in range(0, len(split_text)): f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") @@ -145,11 +145,12 @@ class TTSEngine: except OSError: print("OSError") - def call_tts(self, filename: str, text: str): + def call_tts(self, filename: str, text: str, voice: Optional[str] = None): self.tts_module.run( text, filepath=f"{self.path}/{filename}.mp3", random_voice=settings.config["settings"]["tts"]["random_voice"], + voice=voice ) # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length @@ -181,4 +182,4 @@ def process_text(text: str, clean: bool = True): print_substep("Translating Text...") translated_text = translators.translate_text(text, translator="google", to_language=lang) new_text = sanitize_text(translated_text) - return new_text + return new_text \ No newline at end of file diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index bf47601..6070f06 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -9,34 +9,29 @@ class pyttsx: def __init__(self): self.max_chars = 5000 self.voices = [] + engine = pyttsx3.init() + self.voices = [voice.id for voice in engine.getProperty("voices")] + + def run(self, text: str, filepath: str, random_voice=False, voice=None): + voice_id = voice or settings.config["settings"]["tts"]["python_voice"] + + if not voice_id: + voice_id = self.random_voice() + + if not voice_id.isdigit(): + raise ValueError("Invalid voice ID provided") + voice_id = int(voice_id) + + if voice_id >= len(self.voices): + raise ValueError(f"Voice ID out of range. Valid IDs are 0 to {len(self.voices) - 1}") - def run( - self, - text: str, - filepath: str, - random_voice=False, - ): - voice_id = settings.config["settings"]["tts"]["python_voice"] - voice_num = settings.config["settings"]["tts"]["py_voice_num"] - if voice_id == "" or voice_num == "": - voice_id = 2 - voice_num = 3 - raise ValueError("set pyttsx values to a valid value, switching to defaults") - else: - voice_id = int(voice_id) - voice_num = int(voice_num) - for i in range(voice_num): - self.voices.append(i) - i = +1 if random_voice: - voice_id = self.randomvoice() + voice_id = self.random_voice() + engine = pyttsx3.init() - voices = engine.getProperty("voices") - engine.setProperty( - "voice", voices[voice_id].id - ) # changing index changes voices but ony 0 and 1 are working here + engine.setProperty("voice", self.voices[voice_id]) engine.save_to_file(text, f"{filepath}") engine.runAndWait() - def randomvoice(self): - return random.choice(self.voices) + def random_voice(self): + return random.choice(self.voices) \ No newline at end of file diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index dc80dc9..1e3b40b 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -36,7 +36,7 @@ class StreamlabsPolly: def run(self, text, filepath, random_voice: bool = False): if random_voice: - voice = self.randomvoice() + voice = self.random_voice() else: if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]: raise ValueError( @@ -60,5 +60,5 @@ class StreamlabsPolly: except (KeyError, JSONDecodeError): print("Error occurred calling Streamlabs Polly") - def randomvoice(self): + def random_voice(self): return random.choice(self.voices) diff --git a/reddit/subreddit.py b/reddit/subreddit.py index e1def23..6b81437 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -1,24 +1,19 @@ import re - -from prawcore.exceptions import ResponseException - -from utils import settings import praw from praw.models import MoreComments from prawcore.exceptions import ResponseException +from utils import settings +from utils.ai_methods import sort_by_similarity from utils.console import print_step, print_substep +from utils.posttextparser import posttextparser from utils.subreddit import get_subreddit_undone from utils.videos import check_done from utils.voice import sanitize_text -from utils.posttextparser import posttextparser -from utils.ai_methods import sort_by_similarity def get_subreddit_threads(POST_ID: str): - """ - Returns a list of threads from the AskReddit subreddit. - """ + """Returns a list of threads from the AskReddit subreddit.""" print_substep("Logging into Reddit.") @@ -40,7 +35,7 @@ def get_subreddit_threads(POST_ID: str): client_secret=settings.config["reddit"]["creds"]["client_secret"], user_agent="Accessing Reddit threads", username=username, - passkey=passkey, + password=passkey, check_for_async=False, ) except ResponseException as e: @@ -49,8 +44,12 @@ def get_subreddit_threads(POST_ID: str): except: print("Something went wrong...") - # Ask user for subreddit input + min_upvotes = settings.config["reddit"]["thread"]["min_upvotes"] + sort_type = settings.config["reddit"]["thread"]["sort_type"] + subreddit_choice = settings.config["reddit"]["thread"]["subreddit"].lstrip('r/') + print_step("Getting subreddit threads...") + similarity_score = 0 if not settings.config["reddit"]["thread"][ "subreddit" @@ -91,9 +90,10 @@ def get_subreddit_threads(POST_ID: str): threads, subreddit, similarity_scores=similarity_scores ) else: - threads = subreddit.hot(limit=25) + threads = list(getattr(subreddit, str(sort_type))(limit=25)) submission = get_subreddit_undone(threads, subreddit) + # Check submission if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun @@ -118,45 +118,43 @@ def get_subreddit_threads(POST_ID: str): f"Thread has a similarity score up to {round(similarity_score * 100)}%", style="bold blue", ) + if submission.score < min_upvotes: + print_substep( + f"Thread has {submission.score} upvotes which is below the minimum threshold of {min_upvotes}. Skipping.") + return get_subreddit_threads(POST_ID) content["thread_url"] = threadurl content["thread_title"] = submission.title content["thread_id"] = submission.id content["is_nsfw"] = submission.over_18 content["comments"] = [] + content["thread_post"] = "" # Default value if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymodemethod"] == 1: content["thread_post"] = posttextparser(submission.selftext) else: content["thread_post"] = submission.selftext else: - for top_level_comment in submission.comments: - if isinstance(top_level_comment, MoreComments): + # Process comments + min_comment_length = int(str(settings.config["reddit"]["thread"]["min_comment_length"])) + max_comment_length = int(str(settings.config["reddit"]["thread"]["max_comment_length"])) + min_comment_upvotes = int(str(settings.config["reddit"]["thread"]["min_comment_upvotes"])) + + for comment in submission.comments: + if isinstance(comment, MoreComments) or comment.body in ["[removed]", "[deleted]"] or comment.stickied: + continue + + sanitised = sanitize_text(comment.body) + if not sanitised or sanitised == " ": continue - 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 len(top_level_comment.body) >= int( - settings.config["reddit"]["thread"]["min_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( - { - "comment_body": top_level_comment.body, - "comment_url": top_level_comment.permalink, - "comment_id": top_level_comment.id, - } - ) + if min_comment_length <= len(comment.body) <= max_comment_length and comment.score >= min_comment_upvotes: + if comment.author and sanitised: + content["comments"].append({ + "comment_body": comment.body, + "comment_url": comment.permalink, + "comment_id": comment.id, + }) print_substep("Received subreddit threads Successfully.", style="bold green") return content diff --git a/utils/.config.template.toml b/utils/.config.template.toml index accf86d..a93852e 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -14,6 +14,8 @@ max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr", options = ['','af', 'ak', 'am', 'ar', 'as', 'ay', 'az', 'be', 'bg', 'bho', 'bm', 'bn', 'bs', 'ca', 'ceb', 'ckb', 'co', 'cs', 'cy', 'da', 'de', 'doi', 'dv', 'ee', 'el', 'en', 'en-US', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'ha', 'haw', 'hi', 'hmn', 'hr', 'ht', 'hu', 'hy', 'id', 'ig', 'ilo', 'is', 'it', 'iw', 'ja', 'jw', 'ka', 'kk', 'km', 'kn', 'ko', 'kri', 'ku', 'ky', 'la', 'lb', 'lg', 'ln', 'lo', 'lt', 'lus', 'lv', 'mai', 'mg', 'mi', 'mk', 'ml', 'mn', 'mni-Mtei', 'mr', 'ms', 'mt', 'my', 'ne', 'nl', 'no', 'nso', 'ny', 'om', 'or', 'pa', 'pl', 'ps', 'pt', 'qu', 'ro', 'ru', 'rw', 'sa', 'sd', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tr', 'ts', 'tt', 'ug', 'uk', 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh-CN', 'zh-TW', 'zu'] } min_comments = { default = 20, optional = false, nmin = 10, 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" } +min_upvotes = { default = 20, optional = false, nmin = 1, type = "int", explanation = "The minimum number of upvotes 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" } +sort_type = { default = "hot", optional = false, options = ["hot", "top", ], explanation = "the way to sort the reddit threads", example = "top" , oob_error = "the sort type needs to be hot, top, etc." } [ai] ai_similarity_enabled = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Threads read from Reddit are sorted based on their similarity to the keywords given below"} @@ -55,3 +57,10 @@ python_voice = { optional = false, default = "1", example = "1", explanation = " py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" } silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" } no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" } + +[settings.codecs] +VCODEC = { optional = false, default = "h264", example = "h264_nvenc", explanation = "The codec used to encode the videos. to find a list of avaliable codecs run ffmpeg " } +VCODECPRESET = { optional = false, default = "slow", example = "fast", explanation = "The codec preset used to encode the videos. to find a list of avaliable codecs run ffmpeg " } +VIDEO_BITRATE = { optional = false, default = "20M", example = "12M", explanation = "The bitrate of the video in megabytes per second ie 50M " } +AUDIO_BITRATE = { optional = false, default = "192k", example = "256k", explanation = "The bitrate of the audio in kilobytes per second ie 256k " } +CPUTHREADS = { optional = false, default = "4", example = "8", type = "int", explanation = "The amount of CPU threads to use during encoding " } \ No newline at end of file diff --git a/utils/cleanup.py b/utils/cleanup.py index 6e00d4c..7b2bfbb 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -3,18 +3,33 @@ from os.path import exists import shutil -def _listdir(d): # listdir with full path - return [os.path.join(d, f) for f in os.listdir(d)] +def count_items_in_directory(directory): + """Count all items (files and subdirectories) in a directory.""" + return sum([len(files) for _, _, files in os.walk(directory)]) def cleanup(reddit_id) -> int: - """Deletes all temporary assets in assets/temp + """Deletes all temporary assets in temp/ Returns: int: How many files were deleted """ - directory = f"../assets/temp/{reddit_id}/" - if exists(directory): - shutil.rmtree(directory) + # Check current working directory + cwd = os.getcwd() + print("Current working directory:", cwd) + + directory = os.path.join(cwd, "assets", "temp", reddit_id) + print("Target directory:", directory) - return 1 + if not exists(directory): + print("Directory does not exist!") + return 0 + + count_before_delete = count_items_in_directory(directory) + try: + shutil.rmtree(directory) + print(f"Successfully deleted the directory with {count_before_delete} items!") + return count_before_delete + except Exception as e: + print(f"Error encountered while deleting: {e}") + return 0 diff --git a/utils/gui_utils.py b/utils/gui_utils.py index f683adf..1975a15 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -125,21 +125,21 @@ def modify_settings(data: dict, config_load, checks: dict): # Delete background video def delete_background(key): - # Read backgrounds.json - with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: - data = json.load(backgrounds) + # Read background_videos.json + with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos: + data = json.load(background_videos) - # Remove background from backgrounds.json - with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds: + # Remove background from background_videos.json + with open("utils/background_videos.json", "w", encoding="utf-8") as background_videos: if data.pop(key, None): - json.dump(data, backgrounds, ensure_ascii=False, indent=4) + json.dump(data, background_videos, ensure_ascii=False, indent=4) else: flash("Couldn't find this background. Try refreshing the page.", "error") return # Remove background video from ".config.template.toml" config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) - config["settings"]["background"]["background_choice"]["options"].remove(key) + config["settings"]["background"]["background_video"]["options"].remove(key) with Path("utils/.config.template.toml").open("w") as toml_file: toml_file.write(tomlkit.dumps(config)) @@ -179,8 +179,8 @@ def add_background(youtube_uri, filename, citation, position): filename = filename.replace(" ", "_") # Check if background doesn't already exist - with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: - data = json.load(backgrounds) + with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos: + data = json.load(background_videos) # Check if key isn't already taken if filename in list(data.keys()): @@ -193,7 +193,7 @@ def add_background(youtube_uri, filename, citation, position): return # Add background video to json file - with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds: + with open("utils/background_videos.json", "r+", encoding="utf-8") as backgrounds: data = json.load(backgrounds) data[filename] = [youtube_uri, filename + ".mp4", citation, position] @@ -202,7 +202,7 @@ def add_background(youtube_uri, filename, citation, position): # Add background video to ".config.template.toml" config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) - config["settings"]["background"]["background_choice"]["options"].append(filename) + config["settings"]["background"]["background_video"]["options"].append(filename) with Path("utils/.config.template.toml").open("w") as toml_file: toml_file.write(tomlkit.dumps(config)) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 84ca249..fd53096 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -66,12 +66,14 @@ class ProgressFfmpeg(threading.Thread): def name_normalize(name: str) -> str: - name = re.sub(r'[?\\"%*:|<>]', "", name) - name = re.sub(r"( [w,W]\s?\/\s?[o,O,0])", r" without", name) - name = re.sub(r"( [w,W]\s?\/)", r" with", name) - 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 = re.sub(r'[?\"%*:|<>]', "", name) + # Note: I've changed [w,W] to [wW] and [o,O,0] to [oO0] + name = re.sub(r"([wW])\s?/\s?([oO0])", r"without", name) + name = re.sub(r"([wW])\s?/", r"with", name) + name = re.sub(r"(\d+)\s?/\s?(\d+)", r"\1 of \2", name) + # This one handles "this/that" as "this or that". The words won't overlap with the earlier "w/ or w/o" + name = re.sub(r"(\w+)\s?/\s?(\w+)", r"\1 or \2", name) + name = re.sub(r"/", "", name) lang = settings.config["reddit"]["thread"]["post_lang"] if lang: @@ -82,7 +84,7 @@ def name_normalize(name: str) -> str: return name -def prepare_background(reddit_id: str, W: int, H: int) -> str: +def prepare_background(reddit_id: str, W: int, H: int) -> Tuple[str, float]: output_path = f"assets/temp/{reddit_id}/background_noaudio.mp4" output = ( ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4") @@ -91,10 +93,15 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: output_path, an=None, **{ - "c:v": "h264", - "b:v": "20M", - "b:a": "192k", - "threads": multiprocessing.cpu_count(), + # these settings improve the encoding a lot and reduce visual errors in the video + # plus its now super easy to configure your codec settings + "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]), + "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]), + "b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]), + "b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]), + "threads": int(str(settings.config["settings"]["codecs"]["CPUTHREADS"])), + "force_key_frames": "expr:gte(t,n_forced*1)", + "g": 250, # GOP size }, ) .overwrite_output() @@ -141,8 +148,8 @@ def make_final_video( background_config (Tuple[str, str, str, Any]): The background config to use. """ # settings values - W: Final[int] = int(settings.config["settings"]["resolution_w"]) - H: Final[int] = int(settings.config["settings"]["resolution_h"]) + W: Final[int] = int(str(settings.config["settings"]["resolution_w"])) + H: Final[int] = int(str(settings.config["settings"]["resolution_h"])) opacity = settings.config["settings"]["opacity"] @@ -159,26 +166,28 @@ def make_final_video( # 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' + audio_clips_durations = [] + storymode = settings.config.get("settings", {}).get("storymode") + storymodemethod = settings.config.get("settings", {}).get("storymodemethod") + if number_of_clips == 0 and not storymode: + print("No audio clips to gather. Please use a different TTS or post.") exit() - if settings.config["settings"]["storymode"]: - if settings.config["settings"]["storymodemethod"] == 0: + + if storymode: + + if storymodemethod == 0: 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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") - for i in track(range(number_of_clips + 1), "Collecting the audio files...") - ] + + elif storymodemethod == 1: + #i find it weird that I have to increase it by one to make it work, it kind of makes sense but not really + number_of_clips += 1 + audio_clips.extend( + [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") for i in track(range(number_of_clips))]) audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) else: - audio_clips = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_clips) - ] + audio_clips.extend([ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_clips + 1)]) audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) audio_clips_durations = [ @@ -210,18 +219,19 @@ def make_final_video( ) 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"]), - ) + #that logic didnt actually work seeing as the mp3 names are way different for each mode if settings.config["settings"]["storymodemethod"] == 0: + audio_clips_durations = [ + float( + ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio.mp3")["format"]["duration"] + ) + ] + audio_clips_durations.insert( + 0, + float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), + ) image_clips.insert( 1, ffmpeg.input(f"assets/temp/{reddit_id}/png/story_content.png").filter( @@ -230,13 +240,27 @@ def make_final_video( ) background_clip = background_clip.overlay( image_clips[0], - enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", x="(main_w-overlay_w)/2", y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[0] elif settings.config["settings"]["storymodemethod"] == 1: - for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): + + 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"]), + ) + + print("the number of clips originally is: " + str(int(number_of_clips))) + for i in track(range(number_of_clips + 1)): + print("appending image: " + str(i) + ".png") + print("at time :" + str(current_time)) image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -248,6 +272,7 @@ def make_final_video( x="(main_w-overlay_w)/2", y="(main_h-overlay_h)/2", ) + print("Total image clips: " + str(len(image_clips))) current_time += audio_clips_durations[i] else: for i in range(0, number_of_clips + 1): @@ -265,6 +290,11 @@ def make_final_video( ) current_time += audio_clips_durations[i] + #fade and cut video at appropriate time + total_audio_duration = sum(audio_clips_durations) + background_clip = background_clip.filter('tpad', stop_duration=1) + background_clip = background_clip.filter('fade', type='out', start_time=total_audio_duration, duration=1) + 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"] @@ -312,8 +342,8 @@ def make_final_video( height, title_thumb, ) - thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") + thumbnailSave.save(f"results/{subreddit}/thumbnails/{filename}.png") + print_substep(f"Thumbnail - Building Thumbnail in results/{subreddit}/thumbnails/{filename}.png") text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -349,10 +379,13 @@ def make_final_video( path, f="mp4", **{ - "c:v": "h264", - "b:v": "20M", - "b:a": "192k", - "threads": multiprocessing.cpu_count(), + "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]), + "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]), + "b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]), + "b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]), + "threads": int(settings.config["settings"]["codecs"]["CPUTHREADS"]), + "g": 250, # GOP size + "force_key_frames": "expr:gte(t,n_forced*1)", }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, @@ -379,10 +412,13 @@ def make_final_video( path, f="mp4", **{ - "c:v": "h264", - "b:v": "20M", - "b:a": "192k", - "threads": multiprocessing.cpu_count(), + "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]), + "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]), + "b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]), + "b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]), + "threads": int(settings.config["settings"]["codecs"]["CPUTHREADS"]), + "g": 250, # GOP size + "force_key_frames": "expr:gte(t,n_forced*1)", }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdc8d61..e87143d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -18,6 +18,37 @@ from utils.videos import save_data __all__ = ["download_screenshots_of_reddit_posts"] +def translate_comment_body(page, comment: dict, lang: str): + """Translate a Reddit comment and update its content on the page. + + Args: + page: The Playwright page object. + comment (dict): The Reddit comment data. + lang (str): The target language to which the comment should be translated. + """ + # Check if language setting is present + if not lang: + return + + # Translate the comment body + comment_tl = translators.translate_text( + comment["comment_body"], + translator="google", + to_language=lang + ) + + # Update the content on the page with the translated text + page.evaluate( + '''(tl_content, tl_id) => { + const commentElement = document.querySelector(`#t1_${tl_id} > div:nth-child(2) > div > div[data-testid="comment"] > div`); + if (commentElement) { + commentElement.textContent = tl_content; + } + }''', + comment_tl, comment["comment_id"] + ) + + def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): """Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png @@ -28,8 +59,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # settings values W: Final[int] = int(settings.config["settings"]["resolution_w"]) H: Final[int] = int(settings.config["settings"]["resolution_h"]) - lang: Final[str] = settings.config["reddit"]["thread"]["post_lang"] - storymode: Final[bool] = settings.config["settings"]["storymode"] + lang: Final[str] = str(settings.config["reddit"]["thread"]["post_lang"]) + storymode: Final[bool] = bool(settings.config["settings"]["storymode"]) print_step("Downloading screenshots of reddit posts...") reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) @@ -100,8 +131,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): page.set_viewport_size(ViewportSize(width=1920, height=1080)) page.wait_for_load_state() - page.locator('[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) - page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) + page.locator('[name="username"]').fill(str(settings.config["reddit"]["creds"]["username"])) + page.locator('[name="password"]').fill(str(settings.config["reddit"]["creds"]["password"])) page.locator("button[class$='m-full-width']").click() page.wait_for_timeout(5000) @@ -179,12 +210,13 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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)) + location[i] = float("{:.2f}".format(location[i] * float(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") + print_substep("Something went wrong inside screenshot_downloader!", style="red") + print_substep(f"Error: {str(e)}", style="red") resp = input( "Something went wrong with making the screenshots! Do you want to skip the post? (y/n) " ) @@ -224,16 +256,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # translate code - if settings.config["reddit"]["thread"]["post_lang"]: - comment_tl = translators.translate_text( - comment["comment_body"], - translator="google", - to_language=settings.config["reddit"]["thread"]["post_lang"], - ) - page.evaluate( - '([tl_content, tl_id]) => document.querySelector(`#t1_${tl_id} > div:nth-child(2) > div > div[data-testid="comment"] > div`).textContent = tl_content', - [comment_tl, comment["comment_id"]], - ) + translate_comment_body(page, comment, settings.config["reddit"]["thread"]["post_lang"]) + try: if settings.config["settings"]["zoom"] != 1: # store zoom settings @@ -245,7 +269,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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)) + location[i] = float("{:.2f}".format(location[i] * float(zoom))) page.screenshot( clip=location, path=f"assets/temp/{reddit_id}/png/comment_{idx}.png",