From 9ca1ae8476acb3232b267c50d485142fab78b460 Mon Sep 17 00:00:00 2001 From: Jo <42.jochang@gmail.com> Date: Fri, 1 Sep 2023 10:16:46 -0700 Subject: [PATCH 01/60] fix: Fixes #1812, random_voice True/False acceptance --- utils/.config.template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index accf86d51..211cc97d1 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -44,7 +44,7 @@ background_thumbnail_font_color = { optional = true, default = "255,255,255", ex [settings.tts] voice_choice = { optional = false, default = "tiktok", options = ["elevenlabs", "streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. " } -random_voice = { optional = false, default = true, example = true, options = [true, false,], explanation = "Randomizes the voice used for each comment" } +random_voice = { optional = false, type = "bool", default = true, example = true, options = [true, false,], explanation = "Randomizes the voice used for each comment" } elevenlabs_voice_name = { optional = false, default = "Bella", example = "Bella", explanation = "The voice used for elevenlabs", options = ["Adam", "Antoni", "Arnold", "Bella", "Domi", "Elli", "Josh", "Rachel", "Sam", ] } elevenlabs_api_key = { optional = true, example = "21f13f91f54d741e2ae27d2ab1b99d59", explanation = "Elevenlabs API key" } aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } From ed97ae4ebbb53b1a18fb68a2eb416549b2ae8b27 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 3 Sep 2023 17:22:55 -0400 Subject: [PATCH 02/60] fix: fixed the GUI chore: reformatted and optimized imports. Co-authored-by: Jan Tumpa --- GUI.py | 4 +- GUI/settings.html | 4 +- TTS/TikTok.py | 4 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 8 ++- TTS/engine_wrapper.py | 25 ++++--- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 9 ++- reddit/subreddit.py | 33 ++++++--- utils/ai_methods.py | 22 ++++-- utils/cleanup.py | 2 +- utils/console.py | 17 ++++- utils/ffmpeg_install.py | 23 ++++--- utils/gui_utils.py | 22 ++++-- utils/imagenarator.py | 19 +++-- utils/playwright.py | 4 +- utils/settings.py | 35 ++++++++-- utils/subreddit.py | 21 ++++-- utils/thumbnail.py | 12 +++- utils/videos.py | 8 ++- utils/voice.py | 6 +- video_creation/background.py | 15 ++-- video_creation/final_video.py | 92 ++++++++++++++++++------- video_creation/screenshot_downloader.py | 42 +++++++---- video_creation/voices.py | 16 +++-- 26 files changed, 329 insertions(+), 126 deletions(-) diff --git a/GUI.py b/GUI.py index 4588083dd..47dfc25c4 100644 --- a/GUI.py +++ b/GUI.py @@ -82,7 +82,9 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template("settings.html", file="config.toml", data=config, checks=checks) + return render_template( + "settings.html", file="config.toml", data=config, checks=checks + ) # Make videos.json accessible diff --git a/GUI/settings.html b/GUI/settings.html index 1f0ef2ea6..f3f175195 100644 --- a/GUI/settings.html +++ b/GUI/settings.html @@ -205,7 +205,7 @@ @@ -618,4 +618,4 @@ }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 29542e2fe..3c83e9a02 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -86,7 +86,9 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + self.URI_BASE = ( + "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + ) self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 4d55860bc..58323f96f 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,7 +41,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["aws_polly_voice"] + ).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index e18bba9e4..ab6dbb027 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -26,7 +26,9 @@ 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() + 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"] @@ -35,7 +37,9 @@ def run(self, text, filepath, random_voice: bool = False): "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) - audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") + audio = generate( + api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1" + ) save(audio=audio, filename=filepath) def randomvoice(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 7a73d6172..a78aef732 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,10 +14,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text - -DEFAULT_MAX_LENGTH: int = ( - 50 # Video length variable, edit this on your own risk. It should work, but it's not supported -) +DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported class TTSEngine: @@ -60,7 +57,9 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) + comment["comment_body"] = re.sub( + r"\bAGI\b", "A.G.I", comment["comment_body"] + ) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -82,13 +81,17 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) + self.call_tts( + "postaudio", process_text(self.reddit_object["thread_post"]) + ) 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)) else: - for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + for idx, comment in track( + enumerate(self.reddit_object["comments"]), "Saving..." + ): # ! 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 @@ -171,7 +174,9 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) + silence.write_audiofile( + f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None + ) def process_text(text: str, clean: bool = True): @@ -179,6 +184,8 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text(text, translator="google", to_language=lang) + translated_text = translators.translate_text( + text, translator="google", to_language=lang + ) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index bf47601d8..a80bf2d9c 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,7 +21,9 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError("set pyttsx values to a valid value, switching to defaults") + raise ValueError( + "set pyttsx values to a valid value, switching to defaults" + ) else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index dc80dc9cd..721dd7840 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,7 +42,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["streamlabs_polly_voice"] + ).capitalize() body = {"voice": voice, "text": text, "service": "polly"} response = requests.post(self.url, data=body) if not check_ratelimit(response): diff --git a/main.py b/main.py index efaa51cba..8eff30809 100755 --- a/main.py +++ b/main.py @@ -7,11 +7,13 @@ from typing import NoReturn from prawcore import ResponseException -from utils.console import print_substep + from reddit.subreddit import get_subreddit_threads from utils import settings from utils.cleanup import cleanup from utils.console import print_markdown, print_step +from utils.console import print_substep +from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion from video_creation.background import ( @@ -23,7 +25,6 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -from utils.ffmpeg_install import ffmpeg_install __VERSION__ = "3.2.1" @@ -103,7 +104,9 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): + for index, post_id in enumerate( + config["reddit"]["thread"]["post_id"].split("+") + ): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index e1def2377..f2f8419df 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -1,18 +1,16 @@ 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): @@ -24,7 +22,9 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print("\nEnter your two-factor authentication code from your authenticator app.\n") + print( + "\nEnter your two-factor authentication code from your authenticator app.\n" + ) code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -57,7 +57,9 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) + re.sub( + r"r\/", "", input("What subreddit would you like to pull from? ") + ) # removes the r/ from the input ) except ValueError: @@ -67,7 +69,9 @@ 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 str(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) @@ -78,8 +82,12 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) - elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison + submission = reddit.submission( + id=settings.config["reddit"]["thread"]["post_id"] + ) + elif settings.config["ai"][ + "ai_similarity_enabled" + ]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -97,7 +105,10 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": + elif ( + not submission.num_comments + and settings.config["settings"]["storymode"] == "false" + ): print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index ed1d55982..7f6457060 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -1,12 +1,16 @@ import numpy as np -from transformers import AutoTokenizer, AutoModel import torch +from transformers import AutoTokenizer, AutoModel # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[0] # First element of model_output contains all token embeddings - input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + token_embeddings = model_output[ + 0 + ] # First element of model_output contains all token embeddings + input_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -32,13 +36,19 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) + threads_embeddings = mean_pooling( + threads_embeddings, encoded_threads["attention_mask"] + ) # Keywords inference - encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") + encoded_keywords = tokenizer( + keywords, padding=True, truncation=True, return_tensors="pt" + ) with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) + keywords_embeddings = mean_pooling( + keywords_embeddings, encoded_keywords["attention_mask"] + ) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/cleanup.py b/utils/cleanup.py index 6e00d4c3d..8c73b15f4 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -1,6 +1,6 @@ import os -from os.path import exists import shutil +from os.path import exists def _listdir(d): # listdir with full path diff --git a/utils/console.py b/utils/console.py index 18c3248b5..7ac8a7035 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,7 +49,10 @@ def handle_input( optional=False, ): if optional: - console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") + console.print( + message + + "\n[green]This is an optional value. Do you want to skip it? (y/n)" + ) if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -83,7 +86,11 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") + console.print( + "[red]" + + err_message + + "\nAre you absolutely sure it's correct?(y/n)" + ) if input().casefold().startswith("y"): break continue @@ -116,5 +123,9 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + "[red bold]" + + err_message + + "\nValid options are: " + + ", ".join(map(str, options)) + + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index 7d5b3adda..b29301ffa 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -1,14 +1,13 @@ -import zipfile -import requests import os import subprocess +import zipfile + +import requests def ffmpeg_install_windows(): try: - ffmpeg_url = ( - "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" - ) + ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -39,7 +38,10 @@ def ffmpeg_install_windows(): # Rename and move files os.rename(f"{ffmpeg_extracted_folder}-6.0-full_build", ffmpeg_extracted_folder) for file in os.listdir(os.path.join(ffmpeg_extracted_folder, "bin")): - os.rename(os.path.join(ffmpeg_extracted_folder, "bin", file), os.path.join(".", file)) + os.rename( + os.path.join(ffmpeg_extracted_folder, "bin", file), + os.path.join(".", file), + ) os.rmdir(os.path.join(ffmpeg_extracted_folder, "bin")) for file in os.listdir(os.path.join(ffmpeg_extracted_folder, "doc")): os.remove(os.path.join(ffmpeg_extracted_folder, "doc", file)) @@ -101,7 +103,10 @@ def ffmpeg_install(): try: # Try to run the FFmpeg command subprocess.run( - ["ffmpeg", "-version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ["ffmpeg", "-version"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) except FileNotFoundError as e: # Check if there's ffmpeg.exe in the current directory @@ -122,7 +127,9 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print("Your OS is not supported. Please install FFmpeg manually and try again.") + print( + "Your OS is not supported. Please install FFmpeg manually and try again." + ) exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/gui_utils.py b/utils/gui_utils.py index f683adfec..9d644d887 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,7 +67,11 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -76,8 +80,16 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -150,7 +162,9 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( + youtube_uri + ) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 935654015..06bbd690f 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -1,9 +1,10 @@ +import os import re import textwrap -import os from PIL import Image, ImageDraw, ImageFont from rich.progress import track + from TTS.engine_wrapper import process_text @@ -17,7 +18,9 @@ def draw_multiple_line_text( Fontperm = font.getsize(text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + y = (image_height / 2) - ( + ((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2 + ) for line in lines: line_width, line_height = font.getsize(line) if transparent: @@ -63,19 +66,25 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title + tfont = ImageFont.truetype( + os.path.join("fonts", "Roboto-Bold.ttf"), 100 + ) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) # for title - draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) + draw_multiple_line_text( + image, title, tfont, txtclr, padding, wrap=30, transparent=transparent + ) image.save(f"assets/temp/{id}/png/title.png") for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) + draw_multiple_line_text( + image, text, font, txtclr, padding, wrap=30, transparent=transparent + ) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index 9672f03d1..be046e6b3 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,5 +1,7 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] + filtered_cookies = [ + cookie for cookie in cookies if cookie["name"] != cookie_cleared_name + ] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index 60efedbd0..e2db50877 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,6 +1,7 @@ import re -from typing import Tuple, Dict from pathlib import Path +from typing import Tuple, Dict + import toml from rich.console import Console @@ -52,7 +53,11 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -60,8 +65,16 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -69,9 +82,15 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + ( + ("[blue]Example: " + str(checks["example"]) + "\n") + if "example" in checks + else "" + ) + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] + + ("Non-optional ", "Optional ")[ + "optional" in checks and checks["optional"] is True + ] ) + "[#C0CAF5 bold]" + str(name) @@ -112,7 +131,9 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") + console.print( + f"[red bold]Encountered error when trying to to load {template_file}: {error}" + ) return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index f8e60edbf..a3732f620 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -2,11 +2,13 @@ from os.path import exists from utils import settings -from utils.console import print_substep from utils.ai_methods import sort_by_similarity +from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): +def get_subreddit_undone( + submissions: list, subreddit, times_checked=0, similarity_scores=None +): """_summary_ Args: @@ -18,7 +20,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print("Sorting based on similarity for a different date filter and thread limit..") + print( + "Sorting based on similarity for a different date filter and thread limit.." + ) submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -27,7 +31,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -43,7 +49,8 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments + <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -52,7 +59,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep("You are trying to use story mode on post with no post text") + print_substep( + "You are trying to use story mode on post with no post text" + ) continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index 172b4543c..aeb82b4c3 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,11 +1,15 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): +def create_thumbnail( + thumbnail, font_family, font_size, font_color, width, height, title +): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round( + Xaxis / sizeLetterXaxis + ) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -30,6 +34,8 @@ def create_thumbnail(thumbnail, font_family, font_size, font_color, width, heigh # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) + draw.text( + (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font + ) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index 7c756fc61..c30cb2c0f 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,7 +19,9 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -33,7 +35,9 @@ def check_done( return redditobj -def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): +def save_data( + subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str +): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 37933ae6e..9bc09d87c 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -4,10 +4,10 @@ from datetime import datetime from time import sleep +from cleantext import clean from requests import Response from utils import settings -from cleantext import clean if sys.version_info[0] >= 3: from datetime import timezone @@ -43,7 +43,9 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + zoneDiff = ( + pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + ) end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 8c15b401f..3683bb5d6 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -5,11 +5,12 @@ from random import randrange from typing import Any, Tuple, Dict +import yt_dlp from moviepy.editor import VideoFileClip, AudioFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip + from utils import settings from utils.console import print_step, print_substep -import yt_dlp def load_background_options(): @@ -59,7 +60,9 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() + choice = str( + settings.config["settings"]["background"][f"background_{mode}"] + ).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -119,7 +122,9 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): +def chop_background( + background_config: Dict[str, Tuple], video_length: int, reddit_object: dict +): """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: @@ -132,7 +137,9 @@ def chop_background(background_config: Dict[str, Tuple], video_length: int, redd 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]}" + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 84ca24957..0739df83a 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -1,9 +1,12 @@ import multiprocessing import os import re +import tempfile +import threading +import time from os.path import exists # Needs to be imported specifically from typing import Final -from typing import Tuple, Any, Dict +from typing import Tuple, Dict import ffmpeg import translators @@ -11,15 +14,11 @@ 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 -from utils import settings - -import tempfile -import threading -import time console = Console() @@ -76,7 +75,9 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text(name, translator="google", to_language=lang) + translated_name = translators.translate_text( + name, translator="google", to_language=lang + ) return translated_name else: return name @@ -113,7 +114,9 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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"] + background_audio_volume = settings.config["settings"]["background"][ + "background_audio_volume" + ] if background_audio_volume == 0: return audio # Return the original audio else: @@ -167,27 +170,42 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + 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...") + for i in track( + range(number_of_clips + 1), "Collecting the audio files..." + ) ] - audio_clips.insert(0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_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"]) + 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"]), + 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( @@ -213,13 +231,19 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"] + 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"]), + float( + ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"][ + "duration" + ] + ), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -236,7 +260,9 @@ def make_final_video( ) 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..."): + for i in track( + range(0, number_of_clips + 1), "Collecting the image files..." + ): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -252,9 +278,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( - "scale", screenshot_width, -1 - ) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ + "v" + ].filter("scale", screenshot_width, -1) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( @@ -273,11 +299,15 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep("The 'results' folder could not be found so it was automatically created.") + 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.") + 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 @@ -291,7 +321,11 @@ def make_final_video( 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( - (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), + ( + file + for file in os.listdir("assets/backgrounds") + if file.endswith(".png") + ), None, ) if first_image is None: @@ -313,7 +347,9 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") + print_substep( + f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" + ) text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -354,7 +390,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -384,7 +422,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdc8d611a..6226adf86 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -4,7 +4,6 @@ from typing import Dict, Final 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 @@ -12,7 +11,6 @@ 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 __all__ = ["download_screenshots_of_reddit_posts"] @@ -38,7 +36,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -48,15 +48,21 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) else: # Switch to dark theme - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-light-mode.json", encoding="utf-8" + ) bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -100,8 +106,12 @@ 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( + settings.config["reddit"]["creds"]["username"] + ) + page.locator('[name="password"]').fill( + settings.config["reddit"]["creds"]["password"] + ) page.locator("button[class$='m-full-width']").click() page.wait_for_timeout(5000) @@ -182,7 +192,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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) + page.locator('[data-test-id="post-content"]').screenshot( + path=postcontentpath + ) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -196,7 +208,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input("Do you want the error traceback for debugging purposes? (y/n)") + resp = input( + "Do you want the error traceback for debugging purposes? (y/n)" + ) if not resp.casefold().startswith("y"): exit() @@ -241,9 +255,13 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator( + f"#t1_{comment['comment_id']}" + ).bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 4d8495b3c..4b29657b7 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -5,9 +5,9 @@ from TTS.GTTS import GTTS from TTS.TikTok import TikTok from TTS.aws_polly import AWSPolly +from TTS.elevenlabs import elevenlabs 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 @@ -36,7 +36,9 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, voice), reddit_obj + ) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -45,12 +47,18 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, choice), reddit_obj + ) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), + ( + value + for dict_key, value in input_dict.items() + if dict_key.lower() == key.lower() + ), None, ) From 48abe251ec1af92f189232bacc949376f43825db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:02:55 +0000 Subject: [PATCH 03/60] Bump unidecode from 1.3.6 to 1.3.8 Bumps [unidecode](https://github.com/kmike/text-unidecode) from 1.3.6 to 1.3.8. - [Release notes](https://github.com/kmike/text-unidecode/releases) - [Commits](https://github.com/kmike/text-unidecode/commits) --- updated-dependencies: - dependency-name: unidecode dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9abc854f..aada5cc4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ Pillow==9.5.0 tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 -unidecode==1.3.6 +unidecode==1.3.8 spacy==3.5.3 torch==2.0.1 transformers==4.29.2 From 6d19bd1f5c8bf1a334aa75e5f1727de224d8d56b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:42:08 +0000 Subject: [PATCH 04/60] Bump pillow from 9.5.0 to 10.2.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.2.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aada5cc4e..fd8b51bbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ rich==13.4.1 toml==0.10.2 translators==5.7.6 pyttsx3==2.90 -Pillow==9.5.0 +Pillow==10.2.0 tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 From 3f40fc9e1d3e2d35677849b5672d65b72ce08477 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:19:14 +0000 Subject: [PATCH 05/60] Bump transformers from 4.29.2 to 4.37.1 Bumps [transformers](https://github.com/huggingface/transformers) from 4.29.2 to 4.37.1. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.29.2...v4.37.1) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aada5cc4e..209020bf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.0.1 -transformers==4.29.2 +transformers==4.37.1 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From 425286fe90b233e55e28992fd7653ce7c7205943 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:00:12 +0000 Subject: [PATCH 06/60] fixup: Format Python code with Black --- GUI.py | 4 +- TTS/TikTok.py | 4 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 8 +-- TTS/engine_wrapper.py | 25 +++----- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 4 +- reddit/subreddit.py | 25 ++------ utils/ai_methods.py | 20 ++---- utils/console.py | 17 +----- utils/ffmpeg_install.py | 8 +-- utils/gui_utils.py | 22 ++----- utils/imagenarator.py | 16 ++--- utils/playwright.py | 4 +- utils/settings.py | 32 ++-------- utils/subreddit.py | 19 ++---- utils/thumbnail.py | 12 +--- utils/videos.py | 8 +-- utils/voice.py | 4 +- video_creation/background.py | 12 +--- video_creation/final_video.py | 81 ++++++------------------- video_creation/screenshot_downloader.py | 40 +++--------- video_creation/voices.py | 14 +---- 24 files changed, 96 insertions(+), 295 deletions(-) diff --git a/GUI.py b/GUI.py index 47dfc25c4..4588083dd 100644 --- a/GUI.py +++ b/GUI.py @@ -82,9 +82,7 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template( - "settings.html", file="config.toml", data=config, checks=checks - ) + return render_template("settings.html", file="config.toml", data=config, checks=checks) # Make videos.json accessible diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 3c83e9a02..29542e2fe 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -86,9 +86,7 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = ( - "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" - ) + self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 58323f96f..4d55860bc 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,9 +41,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index ab6dbb027..e18bba9e4 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -26,9 +26,7 @@ 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() + 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"] @@ -37,9 +35,7 @@ def run(self, text, filepath, random_voice: bool = False): "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) - audio = generate( - api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1" - ) + audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") save(audio=audio, filename=filepath) def randomvoice(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index a78aef732..6d498d278 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,11 +14,12 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text -DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +DEFAULT_MAX_LENGTH: int = ( + 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +) class TTSEngine: - """Calls the given TTS engine to reduce code duplication and allow multiple TTS engines. Args: @@ -57,9 +58,7 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub( - r"\bAGI\b", "A.G.I", comment["comment_body"] - ) + comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -81,17 +80,13 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts( - "postaudio", process_text(self.reddit_object["thread_post"]) - ) + self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) 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)) else: - for idx, comment in track( - enumerate(self.reddit_object["comments"]), "Saving..." - ): + for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): # ! 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 @@ -174,9 +169,7 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile( - f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None - ) + silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) def process_text(text: str, clean: bool = True): @@ -184,8 +177,6 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text( - text, translator="google", to_language=lang - ) + translated_text = translators.translate_text(text, translator="google", to_language=lang) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index a80bf2d9c..bf47601d8 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,9 +21,7 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError( - "set pyttsx values to a valid value, switching to defaults" - ) + raise ValueError("set pyttsx values to a valid value, switching to defaults") else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 721dd7840..dc80dc9cd 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,9 +42,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} response = requests.post(self.url, data=body) if not check_ratelimit(response): diff --git a/main.py b/main.py index 8eff30809..0a401c352 100755 --- a/main.py +++ b/main.py @@ -104,9 +104,7 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate( - config["reddit"]["thread"]["post_id"].split("+") - ): + for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index f2f8419df..8646b4892 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -22,9 +22,7 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print( - "\nEnter your two-factor authentication code from your authenticator app.\n" - ) + print("\nEnter your two-factor authentication code from your authenticator app.\n") code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -57,9 +55,7 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub( - r"r\/", "", input("What subreddit would you like to pull from? ") - ) + re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) # removes the r/ from the input ) except ValueError: @@ -69,9 +65,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 ( - str(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) @@ -82,12 +76,8 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission( - id=settings.config["reddit"]["thread"]["post_id"] - ) - elif settings.config["ai"][ - "ai_similarity_enabled" - ]: # ai sorting based on comparison + submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) + elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -105,10 +95,7 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif ( - not submission.num_comments - and settings.config["settings"]["storymode"] == "false" - ): + elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index 7f6457060..eb6e73ee9 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -5,12 +5,8 @@ # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[ - 0 - ] # First element of model_output contains all token embeddings - input_mask_expanded = ( - attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() - ) + token_embeddings = model_output[0] # First element of model_output contains all token embeddings + input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -36,19 +32,13 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling( - threads_embeddings, encoded_threads["attention_mask"] - ) + threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) # Keywords inference - encoded_keywords = tokenizer( - keywords, padding=True, truncation=True, return_tensors="pt" - ) + encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling( - keywords_embeddings, encoded_keywords["attention_mask"] - ) + keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/console.py b/utils/console.py index 7ac8a7035..18c3248b5 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,10 +49,7 @@ def handle_input( optional=False, ): if optional: - console.print( - message - + "\n[green]This is an optional value. Do you want to skip it? (y/n)" - ) + console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -86,11 +83,7 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print( - "[red]" - + err_message - + "\nAre you absolutely sure it's correct?(y/n)" - ) + console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") if input().casefold().startswith("y"): break continue @@ -123,9 +116,5 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" - + err_message - + "\nValid options are: " - + ", ".join(map(str, options)) - + "." + "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index b29301ffa..b2c673d1d 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -7,7 +7,9 @@ def ffmpeg_install_windows(): try: - ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ffmpeg_url = ( + "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ) ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -127,9 +129,7 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print( - "Your OS is not supported. Please install FFmpeg manually and try again." - ) + print("Your OS is not supported. Please install FFmpeg manually and try again.") exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/gui_utils.py b/utils/gui_utils.py index 9d644d887..f683adfec 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,11 +67,7 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -80,16 +76,8 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -162,9 +150,7 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( - youtube_uri - ) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 06bbd690f..151b0e6fe 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -18,9 +18,7 @@ def draw_multiple_line_text( Fontperm = font.getsize(text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - ( - ((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2 - ) + y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) for line in lines: line_width, line_height = font.getsize(line) if transparent: @@ -66,25 +64,19 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype( - os.path.join("fonts", "Roboto-Bold.ttf"), 100 - ) # for title + tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) # for title - draw_multiple_line_text( - image, title, tfont, txtclr, padding, wrap=30, transparent=transparent - ) + draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) image.save(f"assets/temp/{id}/png/title.png") for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text( - image, text, font, txtclr, padding, wrap=30, transparent=transparent - ) + draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index be046e6b3..9672f03d1 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,7 +1,5 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [ - cookie for cookie in cookies if cookie["name"] != cookie_cleared_name - ] + filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index e2db50877..8187e9a87 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -53,11 +53,7 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -65,16 +61,8 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -82,15 +70,9 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - ( - ("[blue]Example: " + str(checks["example"]) + "\n") - if "example" in checks - else "" - ) + (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + "[red]" - + ("Non-optional ", "Optional ")[ - "optional" in checks and checks["optional"] is True - ] + + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] ) + "[#C0CAF5 bold]" + str(name) @@ -131,9 +113,7 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print( - f"[red bold]Encountered error when trying to to load {template_file}: {error}" - ) + console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index a3732f620..403b6d36f 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -6,9 +6,7 @@ from utils.console import print_substep -def get_subreddit_undone( - submissions: list, subreddit, times_checked=0, similarity_scores=None -): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): """_summary_ Args: @@ -20,9 +18,7 @@ def get_subreddit_undone( """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print( - "Sorting based on similarity for a different date filter and thread limit.." - ) + print("Sorting based on similarity for a different date filter and thread limit..") submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -31,9 +27,7 @@ def get_subreddit_undone( if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -49,8 +43,7 @@ def get_subreddit_undone( print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments - <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -59,9 +52,7 @@ def get_subreddit_undone( continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep( - "You are trying to use story mode on post with no post text" - ) + print_substep("You are trying to use story mode on post with no post text") continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index aeb82b4c3..172b4543c 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,15 +1,11 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail( - thumbnail, font_family, font_size, font_color, width, height, title -): +def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round( - Xaxis / sizeLetterXaxis - ) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -34,8 +30,6 @@ def create_thumbnail( # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text( - (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font - ) + draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index c30cb2c0f..7c756fc61 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,9 +19,7 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -35,9 +33,7 @@ def check_done( return redditobj -def save_data( - subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str -): +def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 9bc09d87c..56595fca5 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -43,9 +43,7 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = ( - pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() - ) + zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 3683bb5d6..2ec981258 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -60,9 +60,7 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str( - settings.config["settings"]["background"][f"background_{mode}"] - ).casefold() + choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -122,9 +120,7 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background( - background_config: Dict[str, Tuple], video_length: int, reddit_object: dict -): +def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): """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: @@ -137,9 +133,7 @@ def chop_background( 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]}" - ) + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 0739df83a..5069474ea 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -75,9 +75,7 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text( - name, translator="google", to_language=lang - ) + translated_name = translators.translate_text(name, translator="google", to_language=lang) return translated_name else: return name @@ -114,9 +112,7 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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" - ] + background_audio_volume = settings.config["settings"]["background"]["background_audio_volume"] if background_audio_volume == 0: return audio # Return the original audio else: @@ -170,42 +166,27 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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") - ) + 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..." - ) + for i in track(range(number_of_clips + 1), "Collecting the audio files...") ] - audio_clips.insert( - 0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") - for i in range(number_of_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" - ] - ) + 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" - ] - ), + 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( @@ -231,19 +212,13 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")[ - "format" - ]["duration"] + 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" - ] - ), + float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -260,9 +235,7 @@ def make_final_video( ) 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..." - ): + for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -278,9 +251,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ - "v" - ].filter("scale", screenshot_width, -1) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( + "scale", screenshot_width, -1 + ) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( @@ -299,15 +272,11 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep( - "The 'results' folder could not be found so it was automatically created." - ) + 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." - ) + 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 @@ -321,11 +290,7 @@ def make_final_video( 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( - ( - file - for file in os.listdir("assets/backgrounds") - if file.endswith(".png") - ), + (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), None, ) if first_image is None: @@ -347,9 +312,7 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep( - f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" - ) + print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -390,9 +353,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -422,9 +383,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 6226adf86..cdcf8ef1d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -36,9 +36,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -48,21 +46,15 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") else: # Switch to dark theme - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open( - "./video_creation/data/cookie-light-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -106,12 +98,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(settings.config["reddit"]["creds"]["username"]) + page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) page.locator("button[class$='m-full-width']").click() page.wait_for_timeout(5000) @@ -192,9 +180,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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 - ) + page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -208,9 +194,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input( - "Do you want the error traceback for debugging purposes? (y/n)" - ) + resp = input("Do you want the error traceback for debugging purposes? (y/n)") if not resp.casefold().startswith("y"): exit() @@ -255,13 +239,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator(f"#t1_{comment['comment_id']}").bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 4b29657b7..8495f8d1c 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -36,9 +36,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, voice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -47,18 +45,12 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, choice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - ( - value - for dict_key, value in input_dict.items() - if dict_key.lower() == key.lower() - ), + (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), None, ) From fb6457844e6339626056ad18f772328f1bc4c74c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:12:47 +0000 Subject: [PATCH 07/60] Bump gtts from 2.3.2 to 2.5.1 Bumps [gtts](https://github.com/pndurette/gTTS) from 2.3.2 to 2.5.1. - [Release notes](https://github.com/pndurette/gTTS/releases) - [Changelog](https://github.com/pndurette/gTTS/blob/main/CHANGELOG.md) - [Commits](https://github.com/pndurette/gTTS/compare/v2.3.2...v2.5.1) --- updated-dependencies: - dependency-name: gtts dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2e13965f..ef05d516f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ boto3==1.26.142 botocore==1.29.142 -gTTS==2.3.2 +gTTS==2.5.1 moviepy==1.0.3 playwright==1.34.0 praw==7.7.0 From e05bafd9854fa3c0436a32c8de1ff63461823c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:12:08 +0000 Subject: [PATCH 08/60] Bump torch from 2.0.1 to 2.2.0 Bumps [torch](https://github.com/pytorch/pytorch) from 2.0.1 to 2.2.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.0.1...v2.2.0) --- updated-dependencies: - dependency-name: torch dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2e13965f..0de532347 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Flask==2.3.3 clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 -torch==2.0.1 +torch==2.2.0 transformers==4.37.1 ffmpeg-python==0.2.0 elevenlabs==0.2.17 From 49ddc36b447f9c191bf1b0cf4529a8199e605db3 Mon Sep 17 00:00:00 2001 From: dieperdev <140034404+dieperdev@users.noreply.github.com> Date: Wed, 31 Jan 2024 06:39:43 -0500 Subject: [PATCH 09/60] Update streamlabs_polly.py --- TTS/streamlabs_polly.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index dc80dc9cd..aca1fe33d 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -43,8 +43,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + headers = {"referer": "https://streamlabs.com"} body = {"voice": voice, "text": text, "service": "polly"} - response = requests.post(self.url, data=body) + response = requests.post(self.url, headers=headers, data=body) if not check_ratelimit(response): self.run(text, filepath, random_voice) From f2a5291d1ae1223447f3a65b449f69f980ba3209 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:22:29 +0000 Subject: [PATCH 10/60] Bump transformers from 4.37.1 to 4.37.2 Bumps [transformers](https://github.com/huggingface/transformers) from 4.37.1 to 4.37.2. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.37.1...v4.37.2) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 46591d1c8..f80402967 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.2.0 -transformers==4.37.1 +transformers==4.37.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From d1bb2d1a8af9a0d48e3f2370a2b7c52868266010 Mon Sep 17 00:00:00 2001 From: Kristian Date: Wed, 20 Mar 2024 14:03:16 +0100 Subject: [PATCH 11/60] Fix for new UI by changing domain --- TTS/streamlabs_polly.py | 2 +- reddit/subreddit.py | 2 +- video_creation/data/videos.json | 51 ++++++++++++++++++++++++- video_creation/screenshot_downloader.py | 12 +++--- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index dddeedccc..9ecabf43a 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -45,7 +45,7 @@ def run(self, text, filepath, random_voice: bool = False): voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer" : "https://streamlabs.com/" } - response = requests.post(self.url, headers=headers,data=body) + response = requests.post(self.url, headers=headers, data=body) if not check_ratelimit(response): self.run(text, filepath, random_voice) diff --git a/reddit/subreddit.py b/reddit/subreddit.py index e1def2377..419cf796a 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -106,7 +106,7 @@ def get_subreddit_threads(POST_ID: str): upvotes = submission.score ratio = submission.upvote_ratio * 100 num_comments = submission.num_comments - threadurl = f"https://reddit.com{submission.permalink}" + threadurl = f"https://new.reddit.com/{submission.permalink}" print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green") print_substep(f"Thread url is: {threadurl} :thumbsup:", style="bold green") diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json index fe51488c7..674d67bce 100644 --- a/video_creation/data/videos.json +++ b/video_creation/data/videos.json @@ -1 +1,50 @@ -[] +[ + { + "subreddit": "", + "id": "1bhrs61", + "time": "1710787283", + "background_credit": "", + "reddit_title": "skipped", + "filename": "" + }, + { + "subreddit": "", + "id": "1bhk4f3", + "time": "1710787512", + "background_credit": "", + "reddit_title": "skipped", + "filename": "" + }, + { + "subreddit": "", + "id": "1bhwnnw", + "time": "1710791048", + "background_credit": "", + "reddit_title": "skipped", + "filename": "" + }, + { + "subreddit": "askreddit", + "id": "1bi9nhh", + "time": "1710859130", + "background_credit": "bbswitzer", + "reddit_title": "What is something presently happening that people dont realize could have huge consequences", + "filename": "What is something presently happening that people dont realize could have huge consequences.mp4" + }, + { + "subreddit": "askreddit", + "id": "1bimyrg", + "time": "1710869538", + "background_credit": "bbswitzer", + "reddit_title": "What do you think the USA will be most known for 100 years from now", + "filename": "What do you think the USA will be most known for 100 years from now.mp4" + }, + { + "subreddit": "", + "id": "1bilpoz", + "time": "1710870790", + "background_credit": "", + "reddit_title": "skipped", + "filename": "" + } +] \ No newline at end of file diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdc8d611a..fb60bfccd 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -75,7 +75,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Launching Headless Browser...") browser = p.chromium.launch( - headless=True + headless=False ) # headless=False will show the browser for debugging purposes # Device scale factor (or dsf for short) allows us to increase the resolution of the screenshots # When the dsf is 1, the width of the screenshot is 600 pixels @@ -100,9 +100,9 @@ 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("button[class$='m-full-width']").click() + page.locator(f'input[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) + page.locator(f'input[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) + page.get_by_role("button", name="Log In").click() page.wait_for_timeout(5000) login_error_div = page.locator(".AnimatedForm__errorMessage").first @@ -220,7 +220,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): if page.locator('[data-testid="content-gate"]').is_visible(): page.locator('[data-testid="content-gate"] button').click() - page.goto(f'https://reddit.com{comment["comment_url"]}', timeout=0) + page.goto(f"https://new.reddit.com/{comment['comment_url']}") # translate code @@ -263,4 +263,4 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # close browser instance when we are done using it browser.close() - print_substep("Screenshots downloaded Successfully.", style="bold green") + print_substep("Screenshots downloaded Successfully.", style="bold green") \ No newline at end of file From f1cce1165f02d8d520fdc4b7d560f8a5e156fad5 Mon Sep 17 00:00:00 2001 From: Cyteon <129582290+Cyteon@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:05:42 +0100 Subject: [PATCH 12/60] Delete video_creation/data/videos.json --- video_creation/data/videos.json | 50 --------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 video_creation/data/videos.json diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json deleted file mode 100644 index 674d67bce..000000000 --- a/video_creation/data/videos.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "subreddit": "", - "id": "1bhrs61", - "time": "1710787283", - "background_credit": "", - "reddit_title": "skipped", - "filename": "" - }, - { - "subreddit": "", - "id": "1bhk4f3", - "time": "1710787512", - "background_credit": "", - "reddit_title": "skipped", - "filename": "" - }, - { - "subreddit": "", - "id": "1bhwnnw", - "time": "1710791048", - "background_credit": "", - "reddit_title": "skipped", - "filename": "" - }, - { - "subreddit": "askreddit", - "id": "1bi9nhh", - "time": "1710859130", - "background_credit": "bbswitzer", - "reddit_title": "What is something presently happening that people dont realize could have huge consequences", - "filename": "What is something presently happening that people dont realize could have huge consequences.mp4" - }, - { - "subreddit": "askreddit", - "id": "1bimyrg", - "time": "1710869538", - "background_credit": "bbswitzer", - "reddit_title": "What do you think the USA will be most known for 100 years from now", - "filename": "What do you think the USA will be most known for 100 years from now.mp4" - }, - { - "subreddit": "", - "id": "1bilpoz", - "time": "1710870790", - "background_credit": "", - "reddit_title": "skipped", - "filename": "" - } -] \ No newline at end of file From b508f2af7353e15070d64bdaa8b9e2aa303bc2ea Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 22 Mar 2024 17:41:40 +0100 Subject: [PATCH 13/60] meme support --- TTS/engine_wrapper.py | 2 +- main.py | 52 ++++++++++++++++++++++--- utils/.config.template.toml | 2 + utils/subreddit.py | 6 +-- video_creation/final_video.py | 45 ++++++++++++--------- video_creation/screenshot_downloader.py | 8 +++- 6 files changed, 84 insertions(+), 31 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6d498d278..cf5faa90c 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -75,7 +75,7 @@ def run(self) -> Tuple[int, int]: # processed_text = ##self.reddit_object["thread_post"] != "" idx = 0 - if settings.config["settings"]["storymode"]: + if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: 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") diff --git a/main.py b/main.py index 0a401c352..f01fa949b 100755 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import math import sys +import os from os import name from pathlib import Path from subprocess import Popen @@ -26,6 +27,8 @@ from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 +from moviepy.editor import VideoFileClip, concatenate_videoclips + __VERSION__ = "3.2.1" print( @@ -63,13 +66,50 @@ def main(POST_ID=None) -> None: def run_many(times) -> None: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() + if not settings.config["settings"]["mememode"]: + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() + else: + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() + + make_meme_video() + +def make_meme_video(): + if not os.path.exists("./clipped"): + os.mkdir("./clipped") + directory = f'./results/{settings.config["reddit"]["thread"]["subreddit"]}' + + print(directory) + + # Get a list of all MP4 files in the directory + mp4_files = [f for f in os.listdir(directory) if f.endswith('.mp4')] + + # Create a list of VideoFileClip objects + clips = [VideoFileClip(os.path.join(directory, f)) for f in mp4_files] + + # Concatenate the clips into a single video + final_clip = concatenate_videoclips(clips) + + # Write the final video to a file + output_file = './clipped/output.mp4' + final_clip.write_videofile(output_file) + + # Close the video clips + for clip in clips: + clip.close() + # Delete the individual MP4 files + for f in mp4_files: + os.remove(os.path.join(directory, f)) def shutdown() -> NoReturn: if "redditid" in globals(): diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 211cc97d1..0f6165e9e 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -25,6 +25,8 @@ theme = { optional = false, default = "dark", example = "light", options = ["dar times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } #transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" } +mememode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only show post content, and multipile of them (no comments)" } +memes_per_vid = { optional = true, default = 10, example = 10, explanation = "Number of memes per video", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, great for subreddits with stories" } storymodemethod= { optional = true, default = 1, example = 1, explanation = "Style that's used for the storymode. Set to 0 for single picture display in whole video, set to 1 for fancy looking video ", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once.", options = [0, 1] } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } diff --git a/utils/subreddit.py b/utils/subreddit.py index 403b6d36f..c5818d24b 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -50,7 +50,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) continue - if settings.config["settings"]["storymode"]: + if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: if not submission.selftext: print_substep("You are trying to use story mode on post with no post text") continue @@ -63,9 +63,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f"Post is too long ({len(submission.selftext)}), try with a different post. ({settings.config['settings']['storymode_max_length']} character limit)" ) continue - elif len(submission.selftext) < 30: + elif len(submission.selftext) < 30: continue - if settings.config["settings"]["storymode"] and not submission.is_self: + if settings.config["settings"]["storymode"] and not submission.is_self and not settings.config["settings"]["mememode"]: continue if similarity_scores is not None: return submission, similarity_scores[i].item() diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 5069474ea..48404eb88 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -99,7 +99,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: .overwrite_output() ) try: - output.run(quiet=True) + output.run(quiet=False) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) exit(1) @@ -166,12 +166,13 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + #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...") - ] + if not settings.config["settings"]["mememode"]: + 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...") + ] audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) else: @@ -191,7 +192,7 @@ def make_final_video( 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) + ).overwrite_output().run(quiet=False) console.log(f"[bold green] Video Will Be: {length} Seconds Long") @@ -204,18 +205,20 @@ def make_final_video( image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( - "scale", screenshot_width, -1 + "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 = [] + if not settings.config["settings"]["mememode"]: + 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"]), @@ -227,6 +230,8 @@ def make_final_video( "scale", screenshot_width, -1 ), ) + if settings.config["settings"]["mememode"]: audio_clips_durations[0] += 2 + background_clip = background_clip.overlay( image_clips[0], enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", @@ -234,7 +239,7 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[0] - elif settings.config["settings"]["storymodemethod"] == 1: + elif settings.config["settings"]["storymodemethod"] == 1 and not settings.config["settings"]["mememode"]: for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( @@ -248,6 +253,8 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[i] + elif settings.config["settings"]["mememode"]: + pass else: for i in range(0, number_of_clips + 1): image_clips.append( @@ -354,10 +361,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=True, + quiet=False, overwrite_output=True, capture_stdout=False, - capture_stderr=False, + capture_stderr=True, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) @@ -384,10 +391,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=True, + quiet=False, overwrite_output=True, capture_stdout=False, - capture_stderr=False, + capture_stderr=True, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index d688caf9c..e5a83ab44 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -28,6 +28,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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"] + mememode: Final[bool] = settings.config["settings"]["mememode"] print_step("Downloading screenshots of reddit posts...") reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) @@ -168,6 +169,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Skipping translation...") postcontentpath = f"assets/temp/{reddit_id}/png/title.png" + + try: if settings.config["settings"]["zoom"] != 1: # store zoom settings @@ -181,6 +184,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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( @@ -200,11 +204,11 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): raise e - if storymode: + if storymode and not mememode: page.locator('[data-click-id="text"]').first.screenshot( path=f"assets/temp/{reddit_id}/png/story_content.png" ) - else: + elif not mememode: for idx, comment in enumerate( track( reddit_object["comments"][:screenshot_num], From 410f44297632363b095859b178b6f242cda0c597 Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 22 Mar 2024 17:48:14 +0100 Subject: [PATCH 14/60] Revert "meme support" This reverts commit b508f2af7353e15070d64bdaa8b9e2aa303bc2ea. --- TTS/engine_wrapper.py | 2 +- main.py | 52 +++---------------------- utils/.config.template.toml | 2 - utils/subreddit.py | 6 +-- video_creation/final_video.py | 45 +++++++++------------ video_creation/screenshot_downloader.py | 8 +--- 6 files changed, 31 insertions(+), 84 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index cf5faa90c..6d498d278 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -75,7 +75,7 @@ def run(self) -> Tuple[int, int]: # processed_text = ##self.reddit_object["thread_post"] != "" idx = 0 - if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: + 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") diff --git a/main.py b/main.py index f01fa949b..0a401c352 100755 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import math import sys -import os from os import name from pathlib import Path from subprocess import Popen @@ -27,8 +26,6 @@ from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -from moviepy.editor import VideoFileClip, concatenate_videoclips - __VERSION__ = "3.2.1" print( @@ -66,50 +63,13 @@ def main(POST_ID=None) -> None: def run_many(times) -> None: - if not settings.config["settings"]["mememode"]: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() - else: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() - - make_meme_video() - -def make_meme_video(): - if not os.path.exists("./clipped"): - os.mkdir("./clipped") - directory = f'./results/{settings.config["reddit"]["thread"]["subreddit"]}' - - print(directory) - - # Get a list of all MP4 files in the directory - mp4_files = [f for f in os.listdir(directory) if f.endswith('.mp4')] - - # Create a list of VideoFileClip objects - clips = [VideoFileClip(os.path.join(directory, f)) for f in mp4_files] - - # Concatenate the clips into a single video - final_clip = concatenate_videoclips(clips) - - # Write the final video to a file - output_file = './clipped/output.mp4' - final_clip.write_videofile(output_file) - - # Close the video clips - for clip in clips: - clip.close() + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() - # Delete the individual MP4 files - for f in mp4_files: - os.remove(os.path.join(directory, f)) def shutdown() -> NoReturn: if "redditid" in globals(): diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 0f6165e9e..211cc97d1 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -25,8 +25,6 @@ theme = { optional = false, default = "dark", example = "light", options = ["dar times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } #transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" } -mememode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only show post content, and multipile of them (no comments)" } -memes_per_vid = { optional = true, default = 10, example = 10, explanation = "Number of memes per video", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, great for subreddits with stories" } storymodemethod= { optional = true, default = 1, example = 1, explanation = "Style that's used for the storymode. Set to 0 for single picture display in whole video, set to 1 for fancy looking video ", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once.", options = [0, 1] } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } diff --git a/utils/subreddit.py b/utils/subreddit.py index c5818d24b..403b6d36f 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -50,7 +50,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) continue - if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: + if settings.config["settings"]["storymode"]: if not submission.selftext: print_substep("You are trying to use story mode on post with no post text") continue @@ -63,9 +63,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f"Post is too long ({len(submission.selftext)}), try with a different post. ({settings.config['settings']['storymode_max_length']} character limit)" ) continue - elif len(submission.selftext) < 30: + elif len(submission.selftext) < 30: continue - if settings.config["settings"]["storymode"] and not submission.is_self and not settings.config["settings"]["mememode"]: + if settings.config["settings"]["storymode"] and not submission.is_self: continue if similarity_scores is not None: return submission, similarity_scores[i].item() diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 48404eb88..5069474ea 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -99,7 +99,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: .overwrite_output() ) try: - output.run(quiet=False) + output.run(quiet=True) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) exit(1) @@ -166,13 +166,12 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + audio_clips.insert(1, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio.mp3")) elif settings.config["settings"]["storymodemethod"] == 1: - if not settings.config["settings"]["mememode"]: - 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...") - ] + 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...") + ] audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) else: @@ -192,7 +191,7 @@ def make_final_video( 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=False) + ).overwrite_output().run(quiet=True) console.log(f"[bold green] Video Will Be: {length} Seconds Long") @@ -205,20 +204,18 @@ def make_final_video( image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( - "scale", screenshot_width, -1, + "scale", screenshot_width, -1 ), ) current_time = 0 if settings.config["settings"]["storymode"]: - audio_clips_durations = [] - if not settings.config["settings"]["mememode"]: - 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 = [ + 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"]), @@ -230,8 +227,6 @@ def make_final_video( "scale", screenshot_width, -1 ), ) - if settings.config["settings"]["mememode"]: audio_clips_durations[0] += 2 - background_clip = background_clip.overlay( image_clips[0], enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", @@ -239,7 +234,7 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[0] - elif settings.config["settings"]["storymodemethod"] == 1 and not settings.config["settings"]["mememode"]: + elif settings.config["settings"]["storymodemethod"] == 1: for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( @@ -253,8 +248,6 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[i] - elif settings.config["settings"]["mememode"]: - pass else: for i in range(0, number_of_clips + 1): image_clips.append( @@ -361,10 +354,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=False, + quiet=True, overwrite_output=True, capture_stdout=False, - capture_stderr=True, + capture_stderr=False, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) @@ -391,10 +384,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=False, + quiet=True, overwrite_output=True, capture_stdout=False, - capture_stderr=True, + capture_stderr=False, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index e5a83ab44..d688caf9c 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -28,7 +28,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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"] - mememode: Final[bool] = settings.config["settings"]["mememode"] print_step("Downloading screenshots of reddit posts...") reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) @@ -169,8 +168,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Skipping translation...") postcontentpath = f"assets/temp/{reddit_id}/png/title.png" - - try: if settings.config["settings"]["zoom"] != 1: # store zoom settings @@ -184,7 +181,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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( @@ -204,11 +200,11 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): raise e - if storymode and not mememode: + if storymode: page.locator('[data-click-id="text"]').first.screenshot( path=f"assets/temp/{reddit_id}/png/story_content.png" ) - elif not mememode: + else: for idx, comment in enumerate( track( reddit_object["comments"][:screenshot_num], From 005cfaa4fe315ab262a4df45ce6c2282d11cc529 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:39:44 +0000 Subject: [PATCH 15/60] Bump torch from 2.2.0 to 2.2.2 Bumps [torch](https://github.com/pytorch/pytorch) from 2.2.0 to 2.2.2. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.2.0...v2.2.2) --- updated-dependencies: - dependency-name: torch dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f80402967..c022e5d82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Flask==2.3.3 clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 -torch==2.2.0 +torch==2.2.2 transformers==4.37.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 From 3e9b099c4606b380ac7231353d409881e2ffbfbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:42:06 +0000 Subject: [PATCH 16/60] Bump transformers from 4.37.2 to 4.39.2 Bumps [transformers](https://github.com/huggingface/transformers) from 4.37.2 to 4.39.2. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.37.2...v4.39.2) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c022e5d82..7a66debb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.2.2 -transformers==4.37.2 +transformers==4.39.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From d52c443ff56133ec1fe4fb625b46e4c4ed8199b6 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Mon, 1 Apr 2024 12:52:09 -0400 Subject: [PATCH 17/60] Update streamlabs_polly.py --- TTS/streamlabs_polly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index aca1fe33d..9ecabf43a 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -43,8 +43,8 @@ def run(self, text, filepath, random_voice: bool = False): 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() - headers = {"referer": "https://streamlabs.com"} body = {"voice": voice, "text": text, "service": "polly"} + headers = {"Referer" : "https://streamlabs.com/" } response = requests.post(self.url, headers=headers, data=body) if not check_ratelimit(response): self.run(text, filepath, random_voice) From a9ea4a077ff5731d1b5bfce2efe864dd45aa0bd1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:52:25 +0000 Subject: [PATCH 18/60] fixup: Format Python code with Black --- TTS/streamlabs_polly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 9ecabf43a..3f0610db6 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -44,7 +44,7 @@ def run(self, text, filepath, random_voice: bool = False): ) voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} - headers = {"Referer" : "https://streamlabs.com/" } + headers = {"Referer": "https://streamlabs.com/"} response = requests.post(self.url, headers=headers, data=body) if not check_ratelimit(response): self.run(text, filepath, random_voice) From 11156f13c94da7f91b2b9da33b8875e7c0a1a16b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:19:26 +0000 Subject: [PATCH 19/60] Bump pillow from 10.2.0 to 10.3.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a66debb7..2b8246e6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ rich==13.4.1 toml==0.10.2 translators==5.7.6 pyttsx3==2.90 -Pillow==10.2.0 +Pillow==10.3.0 tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 From bdde04fa04f98ca771770c767879a1db56e51144 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:19:34 +0000 Subject: [PATCH 20/60] Bump transformers from 4.39.2 to 4.39.3 Bumps [transformers](https://github.com/huggingface/transformers) from 4.39.2 to 4.39.3. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.39.2...v4.39.3) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a66debb7..d8cbd192b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.2.2 -transformers==4.39.2 +transformers==4.39.3 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From 49b2321099a89bbc08afffef381e634bb4487b36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:30:26 +0000 Subject: [PATCH 21/60] Bump translators from 5.7.6 to 5.9.1 Bumps [translators](https://github.com/UlionTse/translators) from 5.7.6 to 5.9.1. - [Changelog](https://github.com/UlionTse/translators/blob/master/README_history.md) - [Commits](https://github.com/UlionTse/translators/commits) --- updated-dependencies: - dependency-name: translators dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a66debb7..6488a165e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ prawcore~=2.3.0 requests==2.31.0 rich==13.4.1 toml==0.10.2 -translators==5.7.6 +translators==5.9.1 pyttsx3==2.90 Pillow==10.2.0 tomlkit==0.11.8 From 2dfbb71a26c829c239b72187325170e25582fcb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:17:22 +0000 Subject: [PATCH 22/60] Bump torch from 2.2.2 to 2.3.0 Bumps [torch](https://github.com/pytorch/pytorch) from 2.2.2 to 2.3.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.2.2...v2.3.0) --- updated-dependencies: - dependency-name: torch dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c85f8fd9d..92bc26f1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Flask==2.3.3 clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 -torch==2.2.2 +torch==2.3.0 transformers==4.39.3 ffmpeg-python==0.2.0 elevenlabs==0.2.17 From 1b146ab1f1832953d73aec89634f2208c7618ed5 Mon Sep 17 00:00:00 2001 From: Jo <42.jochang@gmail.com> Date: Fri, 1 Sep 2023 10:16:46 -0700 Subject: [PATCH 23/60] fix: Fixes #1812, random_voice True/False acceptance --- utils/.config.template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index e132ea8cf..211cc97d1 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -44,7 +44,7 @@ background_thumbnail_font_color = { optional = true, default = "255,255,255", ex [settings.tts] voice_choice = { optional = false, default = "tiktok", options = ["elevenlabs", "streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. " } -random_voice = { optional = false, default = true, example = true, type = "bool", options = [true, false,], explanation = "Randomizes the voice used for each comment" } +random_voice = { optional = false, type = "bool", default = true, example = true, options = [true, false,], explanation = "Randomizes the voice used for each comment" } elevenlabs_voice_name = { optional = false, default = "Bella", example = "Bella", explanation = "The voice used for elevenlabs", options = ["Adam", "Antoni", "Arnold", "Bella", "Domi", "Elli", "Josh", "Rachel", "Sam", ] } elevenlabs_api_key = { optional = true, example = "21f13f91f54d741e2ae27d2ab1b99d59", explanation = "Elevenlabs API key" } aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } From 35fac14447aba71e5ca95142a0d1b4164f185575 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 3 Sep 2023 17:22:55 -0400 Subject: [PATCH 24/60] fix: fixed the GUI chore: reformatted and optimized imports. Co-authored-by: Jan Tumpa --- GUI.py | 4 +- GUI/settings.html | 4 +- TTS/TikTok.py | 4 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 8 ++- TTS/engine_wrapper.py | 25 ++++--- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 9 ++- reddit/subreddit.py | 33 ++++++--- utils/ai_methods.py | 22 ++++-- utils/cleanup.py | 2 +- utils/console.py | 17 ++++- utils/ffmpeg_install.py | 23 ++++--- utils/gui_utils.py | 22 ++++-- utils/imagenarator.py | 19 +++-- utils/playwright.py | 4 +- utils/settings.py | 35 ++++++++-- utils/subreddit.py | 21 ++++-- utils/thumbnail.py | 12 +++- utils/videos.py | 8 ++- utils/voice.py | 6 +- video_creation/background.py | 15 ++-- video_creation/final_video.py | 92 ++++++++++++++++++------- video_creation/screenshot_downloader.py | 42 +++++++---- video_creation/voices.py | 16 +++-- 26 files changed, 329 insertions(+), 126 deletions(-) diff --git a/GUI.py b/GUI.py index 4588083dd..47dfc25c4 100644 --- a/GUI.py +++ b/GUI.py @@ -82,7 +82,9 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template("settings.html", file="config.toml", data=config, checks=checks) + return render_template( + "settings.html", file="config.toml", data=config, checks=checks + ) # Make videos.json accessible diff --git a/GUI/settings.html b/GUI/settings.html index 1f0ef2ea6..f3f175195 100644 --- a/GUI/settings.html +++ b/GUI/settings.html @@ -205,7 +205,7 @@ @@ -618,4 +618,4 @@ }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 29542e2fe..3c83e9a02 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -86,7 +86,9 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + self.URI_BASE = ( + "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + ) self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 4d55860bc..58323f96f 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,7 +41,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["aws_polly_voice"] + ).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index e18bba9e4..ab6dbb027 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -26,7 +26,9 @@ 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() + 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"] @@ -35,7 +37,9 @@ def run(self, text, filepath, random_voice: bool = False): "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) - audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") + audio = generate( + api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1" + ) save(audio=audio, filename=filepath) def randomvoice(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 7a73d6172..a78aef732 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,10 +14,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text - -DEFAULT_MAX_LENGTH: int = ( - 50 # Video length variable, edit this on your own risk. It should work, but it's not supported -) +DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported class TTSEngine: @@ -60,7 +57,9 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) + comment["comment_body"] = re.sub( + r"\bAGI\b", "A.G.I", comment["comment_body"] + ) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -82,13 +81,17 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) + self.call_tts( + "postaudio", process_text(self.reddit_object["thread_post"]) + ) 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)) else: - for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + for idx, comment in track( + enumerate(self.reddit_object["comments"]), "Saving..." + ): # ! 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 @@ -171,7 +174,9 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) + silence.write_audiofile( + f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None + ) def process_text(text: str, clean: bool = True): @@ -179,6 +184,8 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text(text, translator="google", to_language=lang) + translated_text = translators.translate_text( + text, translator="google", to_language=lang + ) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index bf47601d8..a80bf2d9c 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,7 +21,9 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError("set pyttsx values to a valid value, switching to defaults") + raise ValueError( + "set pyttsx values to a valid value, switching to defaults" + ) else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 3f0610db6..aded32304 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,7 +42,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["streamlabs_polly_voice"] + ).capitalize() body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer": "https://streamlabs.com/"} response = requests.post(self.url, headers=headers, data=body) diff --git a/main.py b/main.py index 6b581ef27..4714e8651 100755 --- a/main.py +++ b/main.py @@ -7,11 +7,13 @@ from typing import NoReturn from prawcore import ResponseException -from utils.console import print_substep + from reddit.subreddit import get_subreddit_threads from utils import settings from utils.cleanup import cleanup from utils.console import print_markdown, print_step +from utils.console import print_substep +from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion from video_creation.background import ( @@ -23,7 +25,6 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -from utils.ffmpeg_install import ffmpeg_install __VERSION__ = "3.2.1" @@ -103,7 +104,9 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): + for index, post_id in enumerate( + config["reddit"]["thread"]["post_id"].split("+") + ): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index e1def2377..f2f8419df 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -1,18 +1,16 @@ 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): @@ -24,7 +22,9 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print("\nEnter your two-factor authentication code from your authenticator app.\n") + print( + "\nEnter your two-factor authentication code from your authenticator app.\n" + ) code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -57,7 +57,9 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) + re.sub( + r"r\/", "", input("What subreddit would you like to pull from? ") + ) # removes the r/ from the input ) except ValueError: @@ -67,7 +69,9 @@ 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 str(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) @@ -78,8 +82,12 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) - elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison + submission = reddit.submission( + id=settings.config["reddit"]["thread"]["post_id"] + ) + elif settings.config["ai"][ + "ai_similarity_enabled" + ]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -97,7 +105,10 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": + elif ( + not submission.num_comments + and settings.config["settings"]["storymode"] == "false" + ): print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index ed1d55982..7f6457060 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -1,12 +1,16 @@ import numpy as np -from transformers import AutoTokenizer, AutoModel import torch +from transformers import AutoTokenizer, AutoModel # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[0] # First element of model_output contains all token embeddings - input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + token_embeddings = model_output[ + 0 + ] # First element of model_output contains all token embeddings + input_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -32,13 +36,19 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) + threads_embeddings = mean_pooling( + threads_embeddings, encoded_threads["attention_mask"] + ) # Keywords inference - encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") + encoded_keywords = tokenizer( + keywords, padding=True, truncation=True, return_tensors="pt" + ) with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) + keywords_embeddings = mean_pooling( + keywords_embeddings, encoded_keywords["attention_mask"] + ) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/cleanup.py b/utils/cleanup.py index 6e00d4c3d..8c73b15f4 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -1,6 +1,6 @@ import os -from os.path import exists import shutil +from os.path import exists def _listdir(d): # listdir with full path diff --git a/utils/console.py b/utils/console.py index 18c3248b5..7ac8a7035 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,7 +49,10 @@ def handle_input( optional=False, ): if optional: - console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") + console.print( + message + + "\n[green]This is an optional value. Do you want to skip it? (y/n)" + ) if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -83,7 +86,11 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") + console.print( + "[red]" + + err_message + + "\nAre you absolutely sure it's correct?(y/n)" + ) if input().casefold().startswith("y"): break continue @@ -116,5 +123,9 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + "[red bold]" + + err_message + + "\nValid options are: " + + ", ".join(map(str, options)) + + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index 7d5b3adda..b29301ffa 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -1,14 +1,13 @@ -import zipfile -import requests import os import subprocess +import zipfile + +import requests def ffmpeg_install_windows(): try: - ffmpeg_url = ( - "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" - ) + ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -39,7 +38,10 @@ def ffmpeg_install_windows(): # Rename and move files os.rename(f"{ffmpeg_extracted_folder}-6.0-full_build", ffmpeg_extracted_folder) for file in os.listdir(os.path.join(ffmpeg_extracted_folder, "bin")): - os.rename(os.path.join(ffmpeg_extracted_folder, "bin", file), os.path.join(".", file)) + os.rename( + os.path.join(ffmpeg_extracted_folder, "bin", file), + os.path.join(".", file), + ) os.rmdir(os.path.join(ffmpeg_extracted_folder, "bin")) for file in os.listdir(os.path.join(ffmpeg_extracted_folder, "doc")): os.remove(os.path.join(ffmpeg_extracted_folder, "doc", file)) @@ -101,7 +103,10 @@ def ffmpeg_install(): try: # Try to run the FFmpeg command subprocess.run( - ["ffmpeg", "-version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ["ffmpeg", "-version"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) except FileNotFoundError as e: # Check if there's ffmpeg.exe in the current directory @@ -122,7 +127,9 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print("Your OS is not supported. Please install FFmpeg manually and try again.") + print( + "Your OS is not supported. Please install FFmpeg manually and try again." + ) exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/gui_utils.py b/utils/gui_utils.py index f683adfec..9d644d887 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,7 +67,11 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -76,8 +80,16 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -150,7 +162,9 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( + youtube_uri + ) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 935654015..06bbd690f 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -1,9 +1,10 @@ +import os import re import textwrap -import os from PIL import Image, ImageDraw, ImageFont from rich.progress import track + from TTS.engine_wrapper import process_text @@ -17,7 +18,9 @@ def draw_multiple_line_text( Fontperm = font.getsize(text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + y = (image_height / 2) - ( + ((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2 + ) for line in lines: line_width, line_height = font.getsize(line) if transparent: @@ -63,19 +66,25 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title + tfont = ImageFont.truetype( + os.path.join("fonts", "Roboto-Bold.ttf"), 100 + ) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) # for title - draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) + draw_multiple_line_text( + image, title, tfont, txtclr, padding, wrap=30, transparent=transparent + ) image.save(f"assets/temp/{id}/png/title.png") for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) + draw_multiple_line_text( + image, text, font, txtclr, padding, wrap=30, transparent=transparent + ) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index 9672f03d1..be046e6b3 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,5 +1,7 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] + filtered_cookies = [ + cookie for cookie in cookies if cookie["name"] != cookie_cleared_name + ] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index 60efedbd0..e2db50877 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,6 +1,7 @@ import re -from typing import Tuple, Dict from pathlib import Path +from typing import Tuple, Dict + import toml from rich.console import Console @@ -52,7 +53,11 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -60,8 +65,16 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -69,9 +82,15 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + ( + ("[blue]Example: " + str(checks["example"]) + "\n") + if "example" in checks + else "" + ) + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] + + ("Non-optional ", "Optional ")[ + "optional" in checks and checks["optional"] is True + ] ) + "[#C0CAF5 bold]" + str(name) @@ -112,7 +131,9 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") + console.print( + f"[red bold]Encountered error when trying to to load {template_file}: {error}" + ) return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index f8e60edbf..a3732f620 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -2,11 +2,13 @@ from os.path import exists from utils import settings -from utils.console import print_substep from utils.ai_methods import sort_by_similarity +from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): +def get_subreddit_undone( + submissions: list, subreddit, times_checked=0, similarity_scores=None +): """_summary_ Args: @@ -18,7 +20,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print("Sorting based on similarity for a different date filter and thread limit..") + print( + "Sorting based on similarity for a different date filter and thread limit.." + ) submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -27,7 +31,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -43,7 +49,8 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments + <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -52,7 +59,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep("You are trying to use story mode on post with no post text") + print_substep( + "You are trying to use story mode on post with no post text" + ) continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index 172b4543c..aeb82b4c3 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,11 +1,15 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): +def create_thumbnail( + thumbnail, font_family, font_size, font_color, width, height, title +): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round( + Xaxis / sizeLetterXaxis + ) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -30,6 +34,8 @@ def create_thumbnail(thumbnail, font_family, font_size, font_color, width, heigh # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) + draw.text( + (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font + ) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index 7c756fc61..c30cb2c0f 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,7 +19,9 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -33,7 +35,9 @@ def check_done( return redditobj -def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): +def save_data( + subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str +): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 37933ae6e..9bc09d87c 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -4,10 +4,10 @@ from datetime import datetime from time import sleep +from cleantext import clean from requests import Response from utils import settings -from cleantext import clean if sys.version_info[0] >= 3: from datetime import timezone @@ -43,7 +43,9 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + zoneDiff = ( + pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + ) end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 8c15b401f..3683bb5d6 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -5,11 +5,12 @@ from random import randrange from typing import Any, Tuple, Dict +import yt_dlp from moviepy.editor import VideoFileClip, AudioFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip + from utils import settings from utils.console import print_step, print_substep -import yt_dlp def load_background_options(): @@ -59,7 +60,9 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() + choice = str( + settings.config["settings"]["background"][f"background_{mode}"] + ).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -119,7 +122,9 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): +def chop_background( + background_config: Dict[str, Tuple], video_length: int, reddit_object: dict +): """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: @@ -132,7 +137,9 @@ def chop_background(background_config: Dict[str, Tuple], video_length: int, redd 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]}" + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 84ca24957..0739df83a 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -1,9 +1,12 @@ import multiprocessing import os import re +import tempfile +import threading +import time from os.path import exists # Needs to be imported specifically from typing import Final -from typing import Tuple, Any, Dict +from typing import Tuple, Dict import ffmpeg import translators @@ -11,15 +14,11 @@ 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 -from utils import settings - -import tempfile -import threading -import time console = Console() @@ -76,7 +75,9 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text(name, translator="google", to_language=lang) + translated_name = translators.translate_text( + name, translator="google", to_language=lang + ) return translated_name else: return name @@ -113,7 +114,9 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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"] + background_audio_volume = settings.config["settings"]["background"][ + "background_audio_volume" + ] if background_audio_volume == 0: return audio # Return the original audio else: @@ -167,27 +170,42 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + 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...") + for i in track( + range(number_of_clips + 1), "Collecting the audio files..." + ) ] - audio_clips.insert(0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_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"]) + 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"]), + 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( @@ -213,13 +231,19 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"] + 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"]), + float( + ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"][ + "duration" + ] + ), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -236,7 +260,9 @@ def make_final_video( ) 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..."): + for i in track( + range(0, number_of_clips + 1), "Collecting the image files..." + ): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -252,9 +278,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( - "scale", screenshot_width, -1 - ) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ + "v" + ].filter("scale", screenshot_width, -1) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( @@ -273,11 +299,15 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep("The 'results' folder could not be found so it was automatically created.") + 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.") + 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 @@ -291,7 +321,11 @@ def make_final_video( 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( - (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), + ( + file + for file in os.listdir("assets/backgrounds") + if file.endswith(".png") + ), None, ) if first_image is None: @@ -313,7 +347,9 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") + print_substep( + f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" + ) text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -354,7 +390,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -384,7 +422,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdc8d611a..6226adf86 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -4,7 +4,6 @@ from typing import Dict, Final 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 @@ -12,7 +11,6 @@ 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 __all__ = ["download_screenshots_of_reddit_posts"] @@ -38,7 +36,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -48,15 +48,21 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) else: # Switch to dark theme - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-light-mode.json", encoding="utf-8" + ) bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -100,8 +106,12 @@ 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( + settings.config["reddit"]["creds"]["username"] + ) + page.locator('[name="password"]').fill( + settings.config["reddit"]["creds"]["password"] + ) page.locator("button[class$='m-full-width']").click() page.wait_for_timeout(5000) @@ -182,7 +192,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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) + page.locator('[data-test-id="post-content"]').screenshot( + path=postcontentpath + ) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -196,7 +208,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input("Do you want the error traceback for debugging purposes? (y/n)") + resp = input( + "Do you want the error traceback for debugging purposes? (y/n)" + ) if not resp.casefold().startswith("y"): exit() @@ -241,9 +255,13 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator( + f"#t1_{comment['comment_id']}" + ).bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 4d8495b3c..4b29657b7 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -5,9 +5,9 @@ from TTS.GTTS import GTTS from TTS.TikTok import TikTok from TTS.aws_polly import AWSPolly +from TTS.elevenlabs import elevenlabs 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 @@ -36,7 +36,9 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, voice), reddit_obj + ) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -45,12 +47,18 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, choice), reddit_obj + ) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), + ( + value + for dict_key, value in input_dict.items() + if dict_key.lower() == key.lower() + ), None, ) From fb49acdc73928fea62b53068f6221f30a7b82afd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:02:55 +0000 Subject: [PATCH 25/60] Bump unidecode from 1.3.6 to 1.3.8 Bumps [unidecode](https://github.com/kmike/text-unidecode) from 1.3.6 to 1.3.8. - [Release notes](https://github.com/kmike/text-unidecode/releases) - [Commits](https://github.com/kmike/text-unidecode/commits) --- updated-dependencies: - dependency-name: unidecode dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9abc854f..aada5cc4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ Pillow==9.5.0 tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 -unidecode==1.3.6 +unidecode==1.3.8 spacy==3.5.3 torch==2.0.1 transformers==4.29.2 From c84e34e60848d46d4ee8a84d4859534d3328081a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:19:14 +0000 Subject: [PATCH 26/60] Bump transformers from 4.29.2 to 4.37.1 Bumps [transformers](https://github.com/huggingface/transformers) from 4.29.2 to 4.37.1. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.29.2...v4.37.1) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aada5cc4e..209020bf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.0.1 -transformers==4.29.2 +transformers==4.37.1 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From bbf5a8265d2badaf9673c16011ced4c6104192d1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:00:12 +0000 Subject: [PATCH 27/60] fixup: Format Python code with Black --- GUI.py | 4 +- TTS/TikTok.py | 4 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 8 +-- TTS/engine_wrapper.py | 25 +++----- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 4 +- reddit/subreddit.py | 25 ++------ utils/ai_methods.py | 20 ++---- utils/console.py | 17 +----- utils/ffmpeg_install.py | 8 +-- utils/gui_utils.py | 22 ++----- utils/imagenarator.py | 16 ++--- utils/playwright.py | 4 +- utils/settings.py | 32 ++-------- utils/subreddit.py | 19 ++---- utils/thumbnail.py | 12 +--- utils/videos.py | 8 +-- utils/voice.py | 4 +- video_creation/background.py | 12 +--- video_creation/final_video.py | 81 ++++++------------------- video_creation/screenshot_downloader.py | 40 +++--------- video_creation/voices.py | 14 +---- 24 files changed, 96 insertions(+), 295 deletions(-) diff --git a/GUI.py b/GUI.py index 47dfc25c4..4588083dd 100644 --- a/GUI.py +++ b/GUI.py @@ -82,9 +82,7 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template( - "settings.html", file="config.toml", data=config, checks=checks - ) + return render_template("settings.html", file="config.toml", data=config, checks=checks) # Make videos.json accessible diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 3c83e9a02..29542e2fe 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -86,9 +86,7 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = ( - "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" - ) + self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 58323f96f..4d55860bc 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,9 +41,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index ab6dbb027..e18bba9e4 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -26,9 +26,7 @@ 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() + 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"] @@ -37,9 +35,7 @@ def run(self, text, filepath, random_voice: bool = False): "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) - audio = generate( - api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1" - ) + audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") save(audio=audio, filename=filepath) def randomvoice(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index a78aef732..6d498d278 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,11 +14,12 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text -DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +DEFAULT_MAX_LENGTH: int = ( + 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +) class TTSEngine: - """Calls the given TTS engine to reduce code duplication and allow multiple TTS engines. Args: @@ -57,9 +58,7 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub( - r"\bAGI\b", "A.G.I", comment["comment_body"] - ) + comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -81,17 +80,13 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts( - "postaudio", process_text(self.reddit_object["thread_post"]) - ) + self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) 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)) else: - for idx, comment in track( - enumerate(self.reddit_object["comments"]), "Saving..." - ): + for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): # ! 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 @@ -174,9 +169,7 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile( - f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None - ) + silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) def process_text(text: str, clean: bool = True): @@ -184,8 +177,6 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text( - text, translator="google", to_language=lang - ) + translated_text = translators.translate_text(text, translator="google", to_language=lang) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index a80bf2d9c..bf47601d8 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,9 +21,7 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError( - "set pyttsx values to a valid value, switching to defaults" - ) + raise ValueError("set pyttsx values to a valid value, switching to defaults") else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index aded32304..3f0610db6 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,9 +42,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer": "https://streamlabs.com/"} response = requests.post(self.url, headers=headers, data=body) diff --git a/main.py b/main.py index 4714e8651..abedeebd5 100755 --- a/main.py +++ b/main.py @@ -104,9 +104,7 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate( - config["reddit"]["thread"]["post_id"].split("+") - ): + for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index f2f8419df..8646b4892 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -22,9 +22,7 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print( - "\nEnter your two-factor authentication code from your authenticator app.\n" - ) + print("\nEnter your two-factor authentication code from your authenticator app.\n") code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -57,9 +55,7 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub( - r"r\/", "", input("What subreddit would you like to pull from? ") - ) + re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) # removes the r/ from the input ) except ValueError: @@ -69,9 +65,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 ( - str(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) @@ -82,12 +76,8 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission( - id=settings.config["reddit"]["thread"]["post_id"] - ) - elif settings.config["ai"][ - "ai_similarity_enabled" - ]: # ai sorting based on comparison + submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) + elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -105,10 +95,7 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif ( - not submission.num_comments - and settings.config["settings"]["storymode"] == "false" - ): + elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index 7f6457060..eb6e73ee9 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -5,12 +5,8 @@ # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[ - 0 - ] # First element of model_output contains all token embeddings - input_mask_expanded = ( - attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() - ) + token_embeddings = model_output[0] # First element of model_output contains all token embeddings + input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -36,19 +32,13 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling( - threads_embeddings, encoded_threads["attention_mask"] - ) + threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) # Keywords inference - encoded_keywords = tokenizer( - keywords, padding=True, truncation=True, return_tensors="pt" - ) + encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling( - keywords_embeddings, encoded_keywords["attention_mask"] - ) + keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/console.py b/utils/console.py index 7ac8a7035..18c3248b5 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,10 +49,7 @@ def handle_input( optional=False, ): if optional: - console.print( - message - + "\n[green]This is an optional value. Do you want to skip it? (y/n)" - ) + console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -86,11 +83,7 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print( - "[red]" - + err_message - + "\nAre you absolutely sure it's correct?(y/n)" - ) + console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") if input().casefold().startswith("y"): break continue @@ -123,9 +116,5 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" - + err_message - + "\nValid options are: " - + ", ".join(map(str, options)) - + "." + "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index b29301ffa..b2c673d1d 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -7,7 +7,9 @@ def ffmpeg_install_windows(): try: - ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ffmpeg_url = ( + "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ) ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -127,9 +129,7 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print( - "Your OS is not supported. Please install FFmpeg manually and try again." - ) + print("Your OS is not supported. Please install FFmpeg manually and try again.") exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/gui_utils.py b/utils/gui_utils.py index 9d644d887..f683adfec 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,11 +67,7 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -80,16 +76,8 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -162,9 +150,7 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( - youtube_uri - ) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 06bbd690f..151b0e6fe 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -18,9 +18,7 @@ def draw_multiple_line_text( Fontperm = font.getsize(text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - ( - ((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2 - ) + y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) for line in lines: line_width, line_height = font.getsize(line) if transparent: @@ -66,25 +64,19 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype( - os.path.join("fonts", "Roboto-Bold.ttf"), 100 - ) # for title + tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) # for title - draw_multiple_line_text( - image, title, tfont, txtclr, padding, wrap=30, transparent=transparent - ) + draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) image.save(f"assets/temp/{id}/png/title.png") for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text( - image, text, font, txtclr, padding, wrap=30, transparent=transparent - ) + draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index be046e6b3..9672f03d1 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,7 +1,5 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [ - cookie for cookie in cookies if cookie["name"] != cookie_cleared_name - ] + filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index e2db50877..8187e9a87 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -53,11 +53,7 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -65,16 +61,8 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -82,15 +70,9 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - ( - ("[blue]Example: " + str(checks["example"]) + "\n") - if "example" in checks - else "" - ) + (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + "[red]" - + ("Non-optional ", "Optional ")[ - "optional" in checks and checks["optional"] is True - ] + + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] ) + "[#C0CAF5 bold]" + str(name) @@ -131,9 +113,7 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print( - f"[red bold]Encountered error when trying to to load {template_file}: {error}" - ) + console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index a3732f620..403b6d36f 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -6,9 +6,7 @@ from utils.console import print_substep -def get_subreddit_undone( - submissions: list, subreddit, times_checked=0, similarity_scores=None -): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): """_summary_ Args: @@ -20,9 +18,7 @@ def get_subreddit_undone( """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print( - "Sorting based on similarity for a different date filter and thread limit.." - ) + print("Sorting based on similarity for a different date filter and thread limit..") submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -31,9 +27,7 @@ def get_subreddit_undone( if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -49,8 +43,7 @@ def get_subreddit_undone( print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments - <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -59,9 +52,7 @@ def get_subreddit_undone( continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep( - "You are trying to use story mode on post with no post text" - ) + print_substep("You are trying to use story mode on post with no post text") continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index aeb82b4c3..172b4543c 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,15 +1,11 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail( - thumbnail, font_family, font_size, font_color, width, height, title -): +def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round( - Xaxis / sizeLetterXaxis - ) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -34,8 +30,6 @@ def create_thumbnail( # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text( - (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font - ) + draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index c30cb2c0f..7c756fc61 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,9 +19,7 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -35,9 +33,7 @@ def check_done( return redditobj -def save_data( - subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str -): +def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 9bc09d87c..56595fca5 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -43,9 +43,7 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = ( - pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() - ) + zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 3683bb5d6..2ec981258 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -60,9 +60,7 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str( - settings.config["settings"]["background"][f"background_{mode}"] - ).casefold() + choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -122,9 +120,7 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background( - background_config: Dict[str, Tuple], video_length: int, reddit_object: dict -): +def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): """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: @@ -137,9 +133,7 @@ def chop_background( 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]}" - ) + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 0739df83a..5069474ea 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -75,9 +75,7 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text( - name, translator="google", to_language=lang - ) + translated_name = translators.translate_text(name, translator="google", to_language=lang) return translated_name else: return name @@ -114,9 +112,7 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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" - ] + background_audio_volume = settings.config["settings"]["background"]["background_audio_volume"] if background_audio_volume == 0: return audio # Return the original audio else: @@ -170,42 +166,27 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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") - ) + 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..." - ) + for i in track(range(number_of_clips + 1), "Collecting the audio files...") ] - audio_clips.insert( - 0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") - for i in range(number_of_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" - ] - ) + 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" - ] - ), + 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( @@ -231,19 +212,13 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")[ - "format" - ]["duration"] + 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" - ] - ), + float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -260,9 +235,7 @@ def make_final_video( ) 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..." - ): + for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -278,9 +251,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ - "v" - ].filter("scale", screenshot_width, -1) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( + "scale", screenshot_width, -1 + ) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( @@ -299,15 +272,11 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep( - "The 'results' folder could not be found so it was automatically created." - ) + 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." - ) + 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 @@ -321,11 +290,7 @@ def make_final_video( 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( - ( - file - for file in os.listdir("assets/backgrounds") - if file.endswith(".png") - ), + (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), None, ) if first_image is None: @@ -347,9 +312,7 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep( - f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" - ) + print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -390,9 +353,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -422,9 +383,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 6226adf86..cdcf8ef1d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -36,9 +36,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -48,21 +46,15 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") else: # Switch to dark theme - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open( - "./video_creation/data/cookie-light-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -106,12 +98,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(settings.config["reddit"]["creds"]["username"]) + page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) page.locator("button[class$='m-full-width']").click() page.wait_for_timeout(5000) @@ -192,9 +180,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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 - ) + page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -208,9 +194,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input( - "Do you want the error traceback for debugging purposes? (y/n)" - ) + resp = input("Do you want the error traceback for debugging purposes? (y/n)") if not resp.casefold().startswith("y"): exit() @@ -255,13 +239,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator(f"#t1_{comment['comment_id']}").bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 4b29657b7..8495f8d1c 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -36,9 +36,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, voice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -47,18 +45,12 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, choice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - ( - value - for dict_key, value in input_dict.items() - if dict_key.lower() == key.lower() - ), + (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), None, ) From f827f9faba70fcc2ffe2e15b3cf3f560155ae5a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:42:08 +0000 Subject: [PATCH 28/60] Bump pillow from 9.5.0 to 10.2.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.2.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 209020bf2..f2e13965f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ rich==13.4.1 toml==0.10.2 translators==5.7.6 pyttsx3==2.90 -Pillow==9.5.0 +Pillow==10.2.0 tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 From 37faf2a57235907240238cdd5120b12761590833 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:12:08 +0000 Subject: [PATCH 29/60] Bump torch from 2.0.1 to 2.2.0 Bumps [torch](https://github.com/pytorch/pytorch) from 2.0.1 to 2.2.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.0.1...v2.2.0) --- updated-dependencies: - dependency-name: torch dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2e13965f..0de532347 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Flask==2.3.3 clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 -torch==2.0.1 +torch==2.2.0 transformers==4.37.1 ffmpeg-python==0.2.0 elevenlabs==0.2.17 From d15740de09819ed58612c9fa1bd3a1a70d189c1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:12:47 +0000 Subject: [PATCH 30/60] Bump gtts from 2.3.2 to 2.5.1 Bumps [gtts](https://github.com/pndurette/gTTS) from 2.3.2 to 2.5.1. - [Release notes](https://github.com/pndurette/gTTS/releases) - [Changelog](https://github.com/pndurette/gTTS/blob/main/CHANGELOG.md) - [Commits](https://github.com/pndurette/gTTS/compare/v2.3.2...v2.5.1) --- updated-dependencies: - dependency-name: gtts dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0de532347..46591d1c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ boto3==1.26.142 botocore==1.29.142 -gTTS==2.3.2 +gTTS==2.5.1 moviepy==1.0.3 playwright==1.34.0 praw==7.7.0 From cdf8ad264a5e4385b2ec9c956a8613045eb1fcd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:22:29 +0000 Subject: [PATCH 31/60] Bump transformers from 4.37.1 to 4.37.2 Bumps [transformers](https://github.com/huggingface/transformers) from 4.37.1 to 4.37.2. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.37.1...v4.37.2) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 46591d1c8..f80402967 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.2.0 -transformers==4.37.1 +transformers==4.37.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From d42ff50a3540fe0e579fa144704fba61f598fb94 Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 22 Mar 2024 17:41:40 +0100 Subject: [PATCH 32/60] meme support --- TTS/engine_wrapper.py | 2 +- main.py | 52 ++++++++++++++++++++++--- utils/.config.template.toml | 2 + utils/subreddit.py | 6 +-- video_creation/final_video.py | 45 ++++++++++++--------- video_creation/screenshot_downloader.py | 8 +++- 6 files changed, 84 insertions(+), 31 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6d498d278..cf5faa90c 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -75,7 +75,7 @@ def run(self) -> Tuple[int, int]: # processed_text = ##self.reddit_object["thread_post"] != "" idx = 0 - if settings.config["settings"]["storymode"]: + if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: 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") diff --git a/main.py b/main.py index abedeebd5..3d99a5e37 100755 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import math import sys +import os from os import name from pathlib import Path from subprocess import Popen @@ -26,6 +27,8 @@ from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 +from moviepy.editor import VideoFileClip, concatenate_videoclips + __VERSION__ = "3.2.1" print( @@ -63,13 +66,50 @@ def main(POST_ID=None) -> None: def run_many(times) -> None: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() + if not settings.config["settings"]["mememode"]: + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() + else: + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() + + make_meme_video() + +def make_meme_video(): + if not os.path.exists("./clipped"): + os.mkdir("./clipped") + directory = f'./results/{settings.config["reddit"]["thread"]["subreddit"]}' + + print(directory) + + # Get a list of all MP4 files in the directory + mp4_files = [f for f in os.listdir(directory) if f.endswith('.mp4')] + + # Create a list of VideoFileClip objects + clips = [VideoFileClip(os.path.join(directory, f)) for f in mp4_files] + + # Concatenate the clips into a single video + final_clip = concatenate_videoclips(clips) + + # Write the final video to a file + output_file = './clipped/output.mp4' + final_clip.write_videofile(output_file) + + # Close the video clips + for clip in clips: + clip.close() + # Delete the individual MP4 files + for f in mp4_files: + os.remove(os.path.join(directory, f)) def shutdown() -> NoReturn: if "redditid" in globals(): diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 211cc97d1..0f6165e9e 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -25,6 +25,8 @@ theme = { optional = false, default = "dark", example = "light", options = ["dar times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } #transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" } +mememode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only show post content, and multipile of them (no comments)" } +memes_per_vid = { optional = true, default = 10, example = 10, explanation = "Number of memes per video", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, great for subreddits with stories" } storymodemethod= { optional = true, default = 1, example = 1, explanation = "Style that's used for the storymode. Set to 0 for single picture display in whole video, set to 1 for fancy looking video ", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once.", options = [0, 1] } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } diff --git a/utils/subreddit.py b/utils/subreddit.py index 403b6d36f..c5818d24b 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -50,7 +50,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) continue - if settings.config["settings"]["storymode"]: + if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: if not submission.selftext: print_substep("You are trying to use story mode on post with no post text") continue @@ -63,9 +63,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f"Post is too long ({len(submission.selftext)}), try with a different post. ({settings.config['settings']['storymode_max_length']} character limit)" ) continue - elif len(submission.selftext) < 30: + elif len(submission.selftext) < 30: continue - if settings.config["settings"]["storymode"] and not submission.is_self: + if settings.config["settings"]["storymode"] and not submission.is_self and not settings.config["settings"]["mememode"]: continue if similarity_scores is not None: return submission, similarity_scores[i].item() diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 5069474ea..48404eb88 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -99,7 +99,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: .overwrite_output() ) try: - output.run(quiet=True) + output.run(quiet=False) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) exit(1) @@ -166,12 +166,13 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + #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...") - ] + if not settings.config["settings"]["mememode"]: + 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...") + ] audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) else: @@ -191,7 +192,7 @@ def make_final_video( 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) + ).overwrite_output().run(quiet=False) console.log(f"[bold green] Video Will Be: {length} Seconds Long") @@ -204,18 +205,20 @@ def make_final_video( image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( - "scale", screenshot_width, -1 + "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 = [] + if not settings.config["settings"]["mememode"]: + 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"]), @@ -227,6 +230,8 @@ def make_final_video( "scale", screenshot_width, -1 ), ) + if settings.config["settings"]["mememode"]: audio_clips_durations[0] += 2 + background_clip = background_clip.overlay( image_clips[0], enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", @@ -234,7 +239,7 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[0] - elif settings.config["settings"]["storymodemethod"] == 1: + elif settings.config["settings"]["storymodemethod"] == 1 and not settings.config["settings"]["mememode"]: for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( @@ -248,6 +253,8 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[i] + elif settings.config["settings"]["mememode"]: + pass else: for i in range(0, number_of_clips + 1): image_clips.append( @@ -354,10 +361,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=True, + quiet=False, overwrite_output=True, capture_stdout=False, - capture_stderr=False, + capture_stderr=True, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) @@ -384,10 +391,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=True, + quiet=False, overwrite_output=True, capture_stdout=False, - capture_stderr=False, + capture_stderr=True, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdcf8ef1d..d62b2d205 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -28,6 +28,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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"] + mememode: Final[bool] = settings.config["settings"]["mememode"] print_step("Downloading screenshots of reddit posts...") reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) @@ -168,6 +169,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Skipping translation...") postcontentpath = f"assets/temp/{reddit_id}/png/title.png" + + try: if settings.config["settings"]["zoom"] != 1: # store zoom settings @@ -181,6 +184,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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( @@ -200,11 +204,11 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): raise e - if storymode: + if storymode and not mememode: page.locator('[data-click-id="text"]').first.screenshot( path=f"assets/temp/{reddit_id}/png/story_content.png" ) - else: + elif not mememode: for idx, comment in enumerate( track( reddit_object["comments"][:screenshot_num], From 903081fca3ba6e9ddd7bd1830c5710e8977a2546 Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 22 Mar 2024 17:48:14 +0100 Subject: [PATCH 33/60] Revert "meme support" This reverts commit b508f2af7353e15070d64bdaa8b9e2aa303bc2ea. --- TTS/engine_wrapper.py | 2 +- main.py | 52 +++---------------------- utils/.config.template.toml | 2 - utils/subreddit.py | 6 +-- video_creation/final_video.py | 45 +++++++++------------ video_creation/screenshot_downloader.py | 8 +--- 6 files changed, 31 insertions(+), 84 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index cf5faa90c..6d498d278 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -75,7 +75,7 @@ def run(self) -> Tuple[int, int]: # processed_text = ##self.reddit_object["thread_post"] != "" idx = 0 - if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: + 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") diff --git a/main.py b/main.py index 3d99a5e37..abedeebd5 100755 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import math import sys -import os from os import name from pathlib import Path from subprocess import Popen @@ -27,8 +26,6 @@ from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -from moviepy.editor import VideoFileClip, concatenate_videoclips - __VERSION__ = "3.2.1" print( @@ -66,50 +63,13 @@ def main(POST_ID=None) -> None: def run_many(times) -> None: - if not settings.config["settings"]["mememode"]: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() - else: - for x in range(1, times + 1): - print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' - ) # correct 1st 2nd 3rd 4th 5th.... - main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() - - make_meme_video() - -def make_meme_video(): - if not os.path.exists("./clipped"): - os.mkdir("./clipped") - directory = f'./results/{settings.config["reddit"]["thread"]["subreddit"]}' - - print(directory) - - # Get a list of all MP4 files in the directory - mp4_files = [f for f in os.listdir(directory) if f.endswith('.mp4')] - - # Create a list of VideoFileClip objects - clips = [VideoFileClip(os.path.join(directory, f)) for f in mp4_files] - - # Concatenate the clips into a single video - final_clip = concatenate_videoclips(clips) - - # Write the final video to a file - output_file = './clipped/output.mp4' - final_clip.write_videofile(output_file) - - # Close the video clips - for clip in clips: - clip.close() + for x in range(1, times + 1): + print_step( + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + ) # correct 1st 2nd 3rd 4th 5th.... + main() + Popen("cls" if name == "nt" else "clear", shell=True).wait() - # Delete the individual MP4 files - for f in mp4_files: - os.remove(os.path.join(directory, f)) def shutdown() -> NoReturn: if "redditid" in globals(): diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 0f6165e9e..211cc97d1 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -25,8 +25,6 @@ theme = { optional = false, default = "dark", example = "light", options = ["dar times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } #transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" } -mememode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only show post content, and multipile of them (no comments)" } -memes_per_vid = { optional = true, default = 10, example = 10, explanation = "Number of memes per video", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, great for subreddits with stories" } storymodemethod= { optional = true, default = 1, example = 1, explanation = "Style that's used for the storymode. Set to 0 for single picture display in whole video, set to 1 for fancy looking video ", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once.", options = [0, 1] } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } diff --git a/utils/subreddit.py b/utils/subreddit.py index c5818d24b..403b6d36f 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -50,7 +50,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) continue - if settings.config["settings"]["storymode"] and not settings.config["settings"]["mememode"]: + if settings.config["settings"]["storymode"]: if not submission.selftext: print_substep("You are trying to use story mode on post with no post text") continue @@ -63,9 +63,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari f"Post is too long ({len(submission.selftext)}), try with a different post. ({settings.config['settings']['storymode_max_length']} character limit)" ) continue - elif len(submission.selftext) < 30: + elif len(submission.selftext) < 30: continue - if settings.config["settings"]["storymode"] and not submission.is_self and not settings.config["settings"]["mememode"]: + if settings.config["settings"]["storymode"] and not submission.is_self: continue if similarity_scores is not None: return submission, similarity_scores[i].item() diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 48404eb88..5069474ea 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -99,7 +99,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: .overwrite_output() ) try: - output.run(quiet=False) + output.run(quiet=True) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) exit(1) @@ -166,13 +166,12 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + audio_clips.insert(1, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio.mp3")) elif settings.config["settings"]["storymodemethod"] == 1: - if not settings.config["settings"]["mememode"]: - 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...") - ] + 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...") + ] audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) else: @@ -192,7 +191,7 @@ def make_final_video( 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=False) + ).overwrite_output().run(quiet=True) console.log(f"[bold green] Video Will Be: {length} Seconds Long") @@ -205,20 +204,18 @@ def make_final_video( image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( - "scale", screenshot_width, -1, + "scale", screenshot_width, -1 ), ) current_time = 0 if settings.config["settings"]["storymode"]: - audio_clips_durations = [] - if not settings.config["settings"]["mememode"]: - 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 = [ + 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"]), @@ -230,8 +227,6 @@ def make_final_video( "scale", screenshot_width, -1 ), ) - if settings.config["settings"]["mememode"]: audio_clips_durations[0] += 2 - background_clip = background_clip.overlay( image_clips[0], enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", @@ -239,7 +234,7 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[0] - elif settings.config["settings"]["storymodemethod"] == 1 and not settings.config["settings"]["mememode"]: + elif settings.config["settings"]["storymodemethod"] == 1: for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( @@ -253,8 +248,6 @@ def make_final_video( y="(main_h-overlay_h)/2", ) current_time += audio_clips_durations[i] - elif settings.config["settings"]["mememode"]: - pass else: for i in range(0, number_of_clips + 1): image_clips.append( @@ -361,10 +354,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=False, + quiet=True, overwrite_output=True, capture_stdout=False, - capture_stderr=True, + capture_stderr=False, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) @@ -391,10 +384,10 @@ def on_update_example(progress) -> None: "threads": multiprocessing.cpu_count(), }, ).overwrite_output().global_args("-progress", progress.output_file.name).run( - quiet=False, + quiet=True, overwrite_output=True, capture_stdout=False, - capture_stderr=True, + capture_stderr=False, ) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index d62b2d205..cdcf8ef1d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -28,7 +28,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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"] - mememode: Final[bool] = settings.config["settings"]["mememode"] print_step("Downloading screenshots of reddit posts...") reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) @@ -169,8 +168,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Skipping translation...") postcontentpath = f"assets/temp/{reddit_id}/png/title.png" - - try: if settings.config["settings"]["zoom"] != 1: # store zoom settings @@ -184,7 +181,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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( @@ -204,11 +200,11 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): raise e - if storymode and not mememode: + if storymode: page.locator('[data-click-id="text"]').first.screenshot( path=f"assets/temp/{reddit_id}/png/story_content.png" ) - elif not mememode: + else: for idx, comment in enumerate( track( reddit_object["comments"][:screenshot_num], From 64f2322ba049cdda8773f1f73b5cfde149d31ef1 Mon Sep 17 00:00:00 2001 From: Cyteon <129582290+Cyteon@users.noreply.github.com> Date: Sat, 27 Apr 2024 11:14:53 +0200 Subject: [PATCH 34/60] Git merge conflict was not actually resolved --- TTS/streamlabs_polly.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index ef0505686..1bbbc2f87 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -43,13 +43,11 @@ def run(self, text, filepath, random_voice: bool = False): 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() - headers = {"referer": "https://streamlabs.com"} + body = {"voice": voice, "text": text, "service": "polly"} -<<<<<<< HEAD headers = {"Referer": "https://streamlabs.com/"} -======= ->>>>>>> elebumm-master response = requests.post(self.url, headers=headers, data=body) + if not check_ratelimit(response): self.run(text, filepath, random_voice) From 133e400686205f7eafa112d3749837b8b87c862c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 18:53:04 +0000 Subject: [PATCH 35/60] Bump transformers from 4.39.3 to 4.40.2 Bumps [transformers](https://github.com/huggingface/transformers) from 4.39.3 to 4.40.2. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.39.3...v4.40.2) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92bc26f1c..80208a5a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ clean-text==0.6.0 unidecode==1.3.8 spacy==3.5.3 torch==2.3.0 -transformers==4.39.3 +transformers==4.40.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 yt-dlp==2023.7.6 \ No newline at end of file From 209630832d89bcf6655a057c33dae4489467cd74 Mon Sep 17 00:00:00 2001 From: cyteon Date: Sun, 19 May 2024 20:37:38 +0200 Subject: [PATCH 36/60] fancy title screen --- .gitignore | 5 +- assets/title_template.png | Bin 0 -> 51539 bytes video_creation/final_video.py | 62 +++++++++++++++++++++++- video_creation/screenshot_downloader.py | 4 ++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 assets/title_template.png diff --git a/.gitignore b/.gitignore index 41bdd5e06..cc6bd1884 100644 --- a/.gitignore +++ b/.gitignore @@ -231,7 +231,8 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -assets/ +assets/temp +assets/backgrounds /.vscode out .DS_Store @@ -244,4 +245,4 @@ video_creation/data/videos.json video_creation/data/envvars.txt config.toml -*.exe \ No newline at end of file +*.exe diff --git a/assets/title_template.png b/assets/title_template.png new file mode 100644 index 0000000000000000000000000000000000000000..d726d4ff7188b178dc518609ebdec0a75e67fe48 GIT binary patch literal 51539 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 str: exit(1) return output_path +def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): + print_step(f"Creating fancy thumbnail for: {text}") + font_title_size = 47 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + image_width, image_height = image.size + lines = textwrap.wrap(text, width=wrap) + y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 + draw = ImageDraw.Draw(image) + + username_font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 30) + draw.text((205, 825), settings.config["settings"]["channel_name"], font=username_font, fill=text_color, align="left") + + if len(lines) == 3: + lines = textwrap.wrap(text, width=wrap+10) + font_title_size = 40 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 35 + elif len(lines) == 4: + lines = textwrap.wrap(text, width=wrap+10) + font_title_size = 35 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 40 + elif len(lines) > 4: + lines = textwrap.wrap(text, width=wrap+10) + font_title_size = 30 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 + + for line in lines: + _, line_height = font.getsize(line) + draw.text((120, y), line, font=font, fill=text_color, align="left") + y += line_height + padding + + return image + def merge_background_audio(audio: ffmpeg, reddit_id: str): """Gather an audio and merge with assets/backgrounds/background.mp3 @@ -201,6 +239,28 @@ def make_final_video( image_clips = list() + Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) + + # Copyright 2024 beingbored (aka. Tim), MIT License, permission granted to use, copy, modify, and distribute. + # get the title_template image and draw a text in the middle part of it with the title of the thread + title_template = Image.open("assets/title_template.png") + + title = reddit_obj["thread_title"] + + title = name_normalize(title) + + font_color = "#000000" + padding = 5 + + # create_fancy_thumbnail(image, text, text_color, padding + title_img = create_fancy_thumbnail( + title_template, title, font_color, padding + ) + + title_img.save(f"assets/temp/{reddit_id}/png/title.png") + + # Copyright end + image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index d688caf9c..d314e6d87 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -67,6 +67,10 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): txtclr=txtcolor, transparent=transparent, ) + + if settings.config["settings"]["storymodemethod"] == 1: + print_substep("Storymodemethod 1 does not need screnshoots") + return screenshot_num: int with sync_playwright() as p: From 0522d195daf29259d2c682fe76d116595e038968 Mon Sep 17 00:00:00 2001 From: cyteon Date: Sun, 19 May 2024 20:49:10 +0200 Subject: [PATCH 37/60] some fixes --- utils/.config.template.toml | 1 + utils/imagenarator.py | 8 -------- video_creation/screenshot_downloader.py | 5 +---- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 211cc97d1..f4a3af0cb 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -31,6 +31,7 @@ storymode_max_length = { optional = true, default = 1000, example = 1000, explan resolution_w = { optional = false, default = 1080, example = 1440, explantation = "Sets the width in pixels of the final video" } resolution_h = { optional = false, default = 1920, example = 2560, explantation = "Sets the height in pixels of the final video" } zoom = { optional = true, default = 1, example = 1.1, explanation = "Sets the browser zoom level. Useful if you want the text larger.", type = "float", nmin = 0.1, nmax = 2, oob_error = "The text is really difficult to read at a zoom level higher than 2" } +channel_name = { optional = true, default = "Reddit Tales", example = "Reddit Stories", explanation = "Sets the channel name for the video" } [settings.background] background_video = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", "minecraft-2","multiversus","fall-guys","steep", ""], explanation = "Sets the background for the video based on game name" } diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 151b0e6fe..6ed22cff0 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -56,25 +56,17 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> """ Render Images for video """ - title = process_text(reddit_obj["thread_title"], False) texts = reddit_obj["thread_post"] id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) if transparent: font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) - # for title - draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) - - image.save(f"assets/temp/{id}/png/title.png") - for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index d314e6d87..da7e6e0d9 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -58,6 +58,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False + if storymode and settings.config["settings"]["storymodemethod"] == 1: # for idx,item in enumerate(reddit_object["thread_post"]): print_substep("Generating images...") @@ -67,10 +68,6 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): txtclr=txtcolor, transparent=transparent, ) - - if settings.config["settings"]["storymodemethod"] == 1: - print_substep("Storymodemethod 1 does not need screnshoots") - return screenshot_num: int with sync_playwright() as p: From 128953c69a247f31759b21250542edaad8466d35 Mon Sep 17 00:00:00 2001 From: Cyteon <129582290+Cyteon@users.noreply.github.com> Date: Sat, 25 May 2024 20:46:52 +0200 Subject: [PATCH 38/60] fix invite link, and add myself as maintainer --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 143100ecd..6f92026ae 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ I have tried to simplify the code so anyone can read it and start contributing a Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information. -### For any questions or support join the [Discord](https://discord.gg/WBQT52RrHV) server +### For any questions or support join the [Discord](https://discord.gg/qfQSx45xCV) server ## Developers and maintainers. @@ -101,6 +101,8 @@ Freebiell (Freebie#3263) - https://github.com/FreebieII Aman Raza (electro199#8130) - https://github.com/electro199 +Cyteon (cyteon) - https://github.com/cyteon + ## LICENSE [Roboto Fonts](https://fonts.google.com/specimen/Roboto/about) are licensed under [Apache License V2](https://www.apache.org/licenses/LICENSE-2.0) From 31312a30d4825d961516ef975ae29f693ab52555 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 May 2024 09:39:02 +0000 Subject: [PATCH 39/60] fixup: Format Python code with Black --- TTS/streamlabs_polly.py | 2 +- video_creation/final_video.py | 45 ++++++++++++++++++------- video_creation/screenshot_downloader.py | 4 +-- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 1bbbc2f87..1541fac2f 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -47,7 +47,7 @@ def run(self, text, filepath, random_voice: bool = False): body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer": "https://streamlabs.com/"} response = requests.post(self.url, headers=headers, data=body) - + if not check_ratelimit(response): self.run(text, filepath, random_voice) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 0ebd39bcb..cad622747 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -108,33 +108,56 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: exit(1) return output_path + def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): print_step(f"Creating fancy thumbnail for: {text}") font_title_size = 47 font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 + y = ( + (image_height / 2) + - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) draw = ImageDraw.Draw(image) username_font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 30) - draw.text((205, 825), settings.config["settings"]["channel_name"], font=username_font, fill=text_color, align="left") + draw.text( + (205, 825), + settings.config["settings"]["channel_name"], + font=username_font, + fill=text_color, + align="left", + ) if len(lines) == 3: - lines = textwrap.wrap(text, width=wrap+10) + lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 40 font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 35 + y = ( + (image_height / 2) + - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 35 + ) elif len(lines) == 4: - lines = textwrap.wrap(text, width=wrap+10) + lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 35 font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 40 + y = ( + (image_height / 2) + - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 40 + ) elif len(lines) > 4: - lines = textwrap.wrap(text, width=wrap+10) + lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 30 font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = (image_height / 2) - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 + y = ( + (image_height / 2) + - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) for line in lines: _, line_height = font.getsize(line) @@ -240,7 +263,7 @@ def make_final_video( image_clips = list() Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) - + # Copyright 2024 beingbored (aka. Tim), MIT License, permission granted to use, copy, modify, and distribute. # get the title_template image and draw a text in the middle part of it with the title of the thread title_template = Image.open("assets/title_template.png") @@ -253,9 +276,7 @@ def make_final_video( padding = 5 # create_fancy_thumbnail(image, text, text_color, padding - title_img = create_fancy_thumbnail( - title_template, title, font_color, padding - ) + title_img = create_fancy_thumbnail(title_template, title, font_color, padding) title_img.save(f"assets/temp/{reddit_id}/png/title.png") diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index da7e6e0d9..f7c66ee44 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -58,7 +58,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False - + if storymode and settings.config["settings"]["storymodemethod"] == 1: # for idx,item in enumerate(reddit_object["thread_post"]): print_substep("Generating images...") @@ -262,4 +262,4 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # close browser instance when we are done using it browser.close() - print_substep("Screenshots downloaded Successfully.", style="bold green") \ No newline at end of file + print_substep("Screenshots downloaded Successfully.", style="bold green") From c922ebcf1e883e7aae0ebc0d8b87aab61663c65a Mon Sep 17 00:00:00 2001 From: Cyteon <129582290+Cyteon@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:58:07 +0200 Subject: [PATCH 40/60] changed copyright to credit --- video_creation/final_video.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index cad622747..2011f0443 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -264,7 +264,7 @@ def make_final_video( Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) - # Copyright 2024 beingbored (aka. Tim), MIT License, permission granted to use, copy, modify, and distribute. + # Credits to tim (beingbored) # get the title_template image and draw a text in the middle part of it with the title of the thread title_template = Image.open("assets/title_template.png") @@ -279,9 +279,6 @@ def make_final_video( title_img = create_fancy_thumbnail(title_template, title, font_color, padding) title_img.save(f"assets/temp/{reddit_id}/png/title.png") - - # Copyright end - image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( From 5d2b6e0b29ecfc5e220fd36ab6b31d5944ceaf1f Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 16:43:58 -0400 Subject: [PATCH 41/60] CHORE: removed some unused comments + Shameless plugging. --- Dockerfile | 4 ---- README.md | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4cf2a712a..add8e246c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,4 @@ ADD . /app WORKDIR /app RUN pip install -r requirements.txt -# tricks for pytube : https://github.com/elebumm/RedditVideoMakerBot/issues/142 -# (NOTE : This is no longer useful since pytube was removed from the dependencies) -# RUN sed -i 's/re.compile(r"^\\w+\\W")/re.compile(r"^\\$*\\w+\\W")/' /usr/local/lib/python3.8/dist-packages/pytube/cipher.py - CMD ["python3", "main.py"] diff --git a/README.md b/README.md index 6f92026ae..e4b7973e5 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ On macOS and Linux (debian, arch, fedora and centos, and based on those), you ca This can also be used to update the installation 4. Run `python main.py` -5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:google.com +5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:`https://jasoncameron.dev` 6. The bot will ask you to fill in your details to connect to the Reddit API, and configure the bot to your liking 7. Enjoy 😎 8. If you need to reconfigure the bot, simply open the `config.toml` file and delete the lines that need to be changed. On the next run of the bot, it will help you reconfigure those options. From 901ad75e0b8e4daf7299551c39eb68e5c665e052 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 16:44:47 -0400 Subject: [PATCH 42/60] Fixed installation error by specifying non 2.0 numpy + incorrect dunder all --- requirements.txt | 5 +++-- video_creation/screenshot_downloader.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 80208a5a3..cd571f566 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,9 +15,10 @@ tomlkit==0.11.8 Flask==2.3.3 clean-text==0.6.0 unidecode==1.3.8 -spacy==3.5.3 +spacy==3.7.5 torch==2.3.0 transformers==4.40.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 -yt-dlp==2023.7.6 \ No newline at end of file +yt-dlp==2023.7.6 +numpy==1.26.4 diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index f7c66ee44..507fb5103 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -13,7 +13,7 @@ from utils.playwright import clear_cookie_by_name from utils.videos import save_data -__all__ = ["download_screenshots_of_reddit_posts"] +__all__ = ["get_screenshots_of_reddit_posts"] def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): From a4f0022a5aa5706df24d1c3e5a0eb775d9ecea70 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:01:11 -0400 Subject: [PATCH 43/60] Fix: AttributeError: 'FreeTypeFont' object has no attribute 'getsize' --- utils/fonts.py | 12 ++++++++++++ utils/imagenarator.py | 7 ++++--- video_creation/final_video.py | 12 ++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 utils/fonts.py diff --git a/utils/fonts.py b/utils/fonts.py new file mode 100644 index 000000000..dc851a2e0 --- /dev/null +++ b/utils/fonts.py @@ -0,0 +1,12 @@ +from PIL.ImageFont import ImageFont, FreeTypeFont + + +def getsize(font: ImageFont | FreeTypeFont, text: str): + left, top, right, bottom = font.getbbox(text) + width = right - left + height = bottom - top + return width, height + +def getheight(font: ImageFont | FreeTypeFont, text: str): + _, height = getsize(font, text) + return height diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 6ed22cff0..14def21cd 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -6,6 +6,7 @@ from rich.progress import track from TTS.engine_wrapper import process_text +from utils.fonts import getsize, getheight def draw_multiple_line_text( @@ -15,12 +16,12 @@ def draw_multiple_line_text( Draw multiline text over given image """ draw = ImageDraw.Draw(image) - Fontperm = font.getsize(text) + font_height = getheight(font, text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + y = (image_height / 2) - (((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2) for line in lines: - line_width, line_height = font.getsize(line) + line_width, line_height = getsize(font, line) if transparent: shadowcolor = "black" for i in range(1, 5): diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 2011f0443..70f39c4cf 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -20,6 +20,7 @@ from utils import settings from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.fonts import getheight from utils.thumbnail import create_thumbnail from utils.videos import save_data @@ -117,7 +118,7 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): lines = textwrap.wrap(text, width=wrap) y = ( (image_height / 2) - - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 ) draw = ImageDraw.Draw(image) @@ -137,7 +138,7 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 35 ) elif len(lines) == 4: @@ -146,7 +147,7 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 40 ) elif len(lines) > 4: @@ -155,14 +156,13 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - (((font.getsize(text)[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 ) for line in lines: - _, line_height = font.getsize(line) draw.text((120, y), line, font=font, fill=text_color, align="left") - y += line_height + padding + y += getheight(font, line) + padding return image From 953be5a36b6f4cf9305daa3af8c6165819c3b699 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:02:32 +0000 Subject: [PATCH 44/60] fixup: Format Python code with Black --- utils/fonts.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/utils/fonts.py b/utils/fonts.py index dc851a2e0..97235b984 100644 --- a/utils/fonts.py +++ b/utils/fonts.py @@ -2,11 +2,12 @@ def getsize(font: ImageFont | FreeTypeFont, text: str): - left, top, right, bottom = font.getbbox(text) - width = right - left - height = bottom - top - return width, height + left, top, right, bottom = font.getbbox(text) + width = right - left + height = bottom - top + return width, height + def getheight(font: ImageFont | FreeTypeFont, text: str): - _, height = getsize(font, text) - return height + _, height = getsize(font, text) + return height From adf1e784136cdc4b8a7c7add6fe9ec61bdf6e0f4 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:20:59 -0400 Subject: [PATCH 45/60] Update elevenlabs to 1.0 --- TTS/elevenlabs.py | 66 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index e18bba9e4..a81779ee0 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -1,42 +1,38 @@ import random -from elevenlabs import generate, save +from elevenlabs import save +from elevenlabs.client import ElevenLabs from utils import settings -voices = [ - "Adam", - "Antoni", - "Arnold", - "Bella", - "Domi", - "Elli", - "Josh", - "Rachel", - "Sam", -] - class elevenlabs: - def __init__(self): - 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() - - if settings.config["settings"]["tts"]["elevenlabs_api_key"]: - api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] - else: - raise ValueError( - "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." - ) - - audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") - save(audio=audio, filename=filepath) - - def randomvoice(self): - return random.choice(self.voices) + def __init__(self): + self.max_chars = 2500 + self.client: ElevenLabs = None + + def run(self, text, filepath, random_voice: bool = False): + if self.client is None: + self.initialize() + if random_voice: + voice = self.randomvoice() + else: + voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() + + audio = self.client.generate(text=text, voice=voice, model="eleven_multilingual_v1") + save(audio=audio, filename=filepath) + + def initialize(self): + if settings.config["settings"]["tts"]["elevenlabs_api_key"]: + api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] + else: + raise ValueError( + "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." + ) + + self.client = ElevenLabs(api_key=api_key) + + def randomvoice(self): + if self.client is None: + self.initialize() + return random.choice(self.client.voices.get_all().voices).voice_name From 59c77007159e825284f2340ac18c8a6d2c208b64 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:27:57 -0400 Subject: [PATCH 46/60] Updated deps --- requirements.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index cd571f566..e6e2e7bb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,24 @@ -boto3==1.26.142 -botocore==1.29.142 +boto3==1.34.127 +botocore==1.34.127 gTTS==2.5.1 moviepy==1.0.3 -playwright==1.34.0 -praw==7.7.0 +playwright==1.44.0 +praw==7.7.1 prawcore~=2.3.0 -requests==2.31.0 -rich==13.4.1 +requests==2.32.3 +rich==13.7.1 toml==0.10.2 -translators==5.9.1 +translators==5.9.2 pyttsx3==2.90 Pillow==10.3.0 -tomlkit==0.11.8 -Flask==2.3.3 +tomlkit==0.12.5 +Flask==3.0.3 clean-text==0.6.0 unidecode==1.3.8 spacy==3.7.5 -torch==2.3.0 -transformers==4.40.2 +torch==2.3.1 +transformers==4.41.2 ffmpeg-python==0.2.0 -elevenlabs==0.2.17 -yt-dlp==2023.7.6 +elevenlabs==1.3.0 +yt-dlp==2024.5.27 numpy==1.26.4 From fe383a794c606497c74904e37b39b07910a964fc Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:38:24 -0400 Subject: [PATCH 47/60] Possible bug location. --- video_creation/final_video.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 70f39c4cf..437528707 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -334,6 +334,9 @@ def make_final_video( ) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) + assert ( + audio_clips_durations is not None + ), "Please make a GitHub issue if you see this. Ping @JasonLovesDoggo on GitHub." background_clip = background_clip.overlay( image_overlay, enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})", From 9e60d8358061259554c1794e4358ba903231c8e0 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:38:51 -0400 Subject: [PATCH 48/60] Fmt using black & isort --- GUI.py | 14 +-- TTS/TikTok.py | 6 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 62 +++++------ TTS/engine_wrapper.py | 24 +++-- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 21 ++-- reddit/subreddit.py | 25 +++-- utils/ai_methods.py | 22 ++-- utils/console.py | 17 ++- utils/ffmpeg_install.py | 8 +- utils/fonts.py | 15 +-- utils/gui_utils.py | 22 +++- utils/imagenarator.py | 10 +- utils/playwright.py | 4 +- utils/settings.py | 34 ++++-- utils/subreddit.py | 19 +++- utils/thumbnail.py | 12 ++- utils/videos.py | 8 +- utils/voice.py | 4 +- video_creation/background.py | 16 ++- video_creation/final_video.py | 132 ++++++++++++++++++------ video_creation/screenshot_downloader.py | 40 +++++-- video_creation/voices.py | 20 ++-- 25 files changed, 379 insertions(+), 168 deletions(-) diff --git a/GUI.py b/GUI.py index 4588083dd..c230f6c80 100644 --- a/GUI.py +++ b/GUI.py @@ -3,14 +3,8 @@ # Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" import tomlkit -from flask import ( - Flask, - redirect, - render_template, - request, - send_from_directory, - url_for, -) +from flask import (Flask, redirect, render_template, request, + send_from_directory, url_for) import utils.gui_utils as gui @@ -82,7 +76,9 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template("settings.html", file="config.toml", data=config, checks=checks) + return render_template( + "settings.html", file="config.toml", data=config, checks=checks + ) # Make videos.json accessible diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 29542e2fe..f9477329e 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -2,7 +2,7 @@ import base64 import random import time -from typing import Optional, Final +from typing import Final, Optional import requests @@ -86,7 +86,9 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + self.URI_BASE = ( + "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" + ) self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 4d55860bc..58323f96f 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,7 +41,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["aws_polly_voice"] + ).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index a81779ee0..4b3c2d6c6 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -7,32 +7,36 @@ class elevenlabs: - def __init__(self): - self.max_chars = 2500 - self.client: ElevenLabs = None - - def run(self, text, filepath, random_voice: bool = False): - if self.client is None: - self.initialize() - if random_voice: - voice = self.randomvoice() - else: - voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() - - audio = self.client.generate(text=text, voice=voice, model="eleven_multilingual_v1") - save(audio=audio, filename=filepath) - - def initialize(self): - if settings.config["settings"]["tts"]["elevenlabs_api_key"]: - api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] - else: - raise ValueError( - "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." - ) - - self.client = ElevenLabs(api_key=api_key) - - def randomvoice(self): - if self.client is None: - self.initialize() - return random.choice(self.client.voices.get_all().voices).voice_name + def __init__(self): + self.max_chars = 2500 + self.client: ElevenLabs = None + + def run(self, text, filepath, random_voice: bool = False): + if self.client is None: + self.initialize() + if random_voice: + voice = self.randomvoice() + else: + voice = str( + settings.config["settings"]["tts"]["elevenlabs_voice_name"] + ).capitalize() + + audio = self.client.generate( + text=text, voice=voice, model="eleven_multilingual_v1" + ) + save(audio=audio, filename=filepath) + + def initialize(self): + if settings.config["settings"]["tts"]["elevenlabs_api_key"]: + api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] + else: + raise ValueError( + "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." + ) + + self.client = ElevenLabs(api_key=api_key) + + def randomvoice(self): + if self.client is None: + self.initialize() + return random.choice(self.client.voices.get_all().voices).voice_name diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6d498d278..f7a08f527 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,9 +14,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text -DEFAULT_MAX_LENGTH: int = ( - 50 # Video length variable, edit this on your own risk. It should work, but it's not supported -) +DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported class TTSEngine: @@ -58,7 +56,9 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) + comment["comment_body"] = re.sub( + r"\bAGI\b", "A.G.I", comment["comment_body"] + ) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -80,13 +80,17 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) + self.call_tts( + "postaudio", process_text(self.reddit_object["thread_post"]) + ) 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)) else: - for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + for idx, comment in track( + enumerate(self.reddit_object["comments"]), "Saving..." + ): # ! 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 @@ -169,7 +173,9 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) + silence.write_audiofile( + f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None + ) def process_text(text: str, clean: bool = True): @@ -177,6 +183,8 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text(text, translator="google", to_language=lang) + translated_text = translators.translate_text( + text, translator="google", to_language=lang + ) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index bf47601d8..a80bf2d9c 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,7 +21,9 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError("set pyttsx values to a valid value, switching to defaults") + raise ValueError( + "set pyttsx values to a valid value, switching to defaults" + ) else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 1541fac2f..f59626d5a 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,7 +42,9 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str( + settings.config["settings"]["tts"]["streamlabs_polly_voice"] + ).capitalize() body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer": "https://streamlabs.com/"} diff --git a/main.py b/main.py index abedeebd5..3470fbc82 100755 --- a/main.py +++ b/main.py @@ -11,19 +11,17 @@ from reddit.subreddit import get_subreddit_threads from utils import settings from utils.cleanup import cleanup -from utils.console import print_markdown, print_step -from utils.console import print_substep +from utils.console import print_markdown, print_step, print_substep from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion -from video_creation.background import ( - download_background_video, - download_background_audio, - chop_background, - get_background_config, -) +from video_creation.background import (chop_background, + download_background_audio, + download_background_video, + get_background_config) from video_creation.final_video import make_final_video -from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts +from video_creation.screenshot_downloader import \ + get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 __VERSION__ = "3.2.1" @@ -38,7 +36,6 @@ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ """ ) -# Modified by JasonLovesDoggo print_markdown( "### Thanks for using this tool! Feel free to contribute to this project on GitHub! If you have any questions, feel free to join my Discord server or submit a GitHub issue. You can find solutions to many common problems in the documentation: https://reddit-video-maker-bot.netlify.app/" ) @@ -104,7 +101,9 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): + for index, post_id in enumerate( + config["reddit"]["thread"]["post_id"].split("+") + ): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 5f2ac5f34..fcf85f312 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -22,7 +22,9 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print("\nEnter your two-factor authentication code from your authenticator app.\n") + print( + "\nEnter your two-factor authentication code from your authenticator app.\n" + ) code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -55,7 +57,9 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) + re.sub( + r"r\/", "", input("What subreddit would you like to pull from? ") + ) # removes the r/ from the input ) except ValueError: @@ -65,7 +69,9 @@ 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 str(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) @@ -76,8 +82,12 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) - elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison + submission = reddit.submission( + id=settings.config["reddit"]["thread"]["post_id"] + ) + elif settings.config["ai"][ + "ai_similarity_enabled" + ]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -95,7 +105,10 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": + elif ( + not submission.num_comments + and settings.config["settings"]["storymode"] == "false" + ): print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index eb6e73ee9..8c3fbf13a 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -1,12 +1,16 @@ import numpy as np import torch -from transformers import AutoTokenizer, AutoModel +from transformers import AutoModel, AutoTokenizer # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[0] # First element of model_output contains all token embeddings - input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + token_embeddings = model_output[ + 0 + ] # First element of model_output contains all token embeddings + input_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -32,13 +36,19 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) + threads_embeddings = mean_pooling( + threads_embeddings, encoded_threads["attention_mask"] + ) # Keywords inference - encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") + encoded_keywords = tokenizer( + keywords, padding=True, truncation=True, return_tensors="pt" + ) with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) + keywords_embeddings = mean_pooling( + keywords_embeddings, encoded_keywords["attention_mask"] + ) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/console.py b/utils/console.py index 18c3248b5..7ac8a7035 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,7 +49,10 @@ def handle_input( optional=False, ): if optional: - console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") + console.print( + message + + "\n[green]This is an optional value. Do you want to skip it? (y/n)" + ) if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -83,7 +86,11 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") + console.print( + "[red]" + + err_message + + "\nAre you absolutely sure it's correct?(y/n)" + ) if input().casefold().startswith("y"): break continue @@ -116,5 +123,9 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + "[red bold]" + + err_message + + "\nValid options are: " + + ", ".join(map(str, options)) + + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index b2c673d1d..b29301ffa 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -7,9 +7,7 @@ def ffmpeg_install_windows(): try: - ffmpeg_url = ( - "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" - ) + ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -129,7 +127,9 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print("Your OS is not supported. Please install FFmpeg manually and try again.") + print( + "Your OS is not supported. Please install FFmpeg manually and try again." + ) exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/fonts.py b/utils/fonts.py index dc851a2e0..4980f6aa0 100644 --- a/utils/fonts.py +++ b/utils/fonts.py @@ -1,12 +1,13 @@ -from PIL.ImageFont import ImageFont, FreeTypeFont +from PIL.ImageFont import FreeTypeFont, ImageFont def getsize(font: ImageFont | FreeTypeFont, text: str): - left, top, right, bottom = font.getbbox(text) - width = right - left - height = bottom - top - return width, height + left, top, right, bottom = font.getbbox(text) + width = right - left + height = bottom - top + return width, height + def getheight(font: ImageFont | FreeTypeFont, text: str): - _, height = getsize(font, text) - return height + _, height = getsize(font, text) + return height diff --git a/utils/gui_utils.py b/utils/gui_utils.py index f683adfec..9d644d887 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,7 +67,11 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -76,8 +80,16 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -150,7 +162,9 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( + youtube_uri + ) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 14def21cd..e26f9e7c7 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -6,7 +6,7 @@ from rich.progress import track from TTS.engine_wrapper import process_text -from utils.fonts import getsize, getheight +from utils.fonts import getheight, getsize def draw_multiple_line_text( @@ -19,7 +19,9 @@ def draw_multiple_line_text( font_height = getheight(font, text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + y = (image_height / 2) - ( + ((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2 + ) for line in lines: line_width, line_height = getsize(font, line) if transparent: @@ -71,5 +73,7 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) + draw_multiple_line_text( + image, text, font, txtclr, padding, wrap=30, transparent=transparent + ) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index 9672f03d1..be046e6b3 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,5 +1,7 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] + filtered_cookies = [ + cookie for cookie in cookies if cookie["name"] != cookie_cleared_name + ] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index 8187e9a87..2a917232b 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import Tuple, Dict +from typing import Dict, Tuple import toml from rich.console import Console @@ -53,7 +53,11 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -61,8 +65,16 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -70,9 +82,15 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + ( + ("[blue]Example: " + str(checks["example"]) + "\n") + if "example" in checks + else "" + ) + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] + + ("Non-optional ", "Optional ")[ + "optional" in checks and checks["optional"] is True + ] ) + "[#C0CAF5 bold]" + str(name) @@ -113,7 +131,9 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") + console.print( + f"[red bold]Encountered error when trying to to load {template_file}: {error}" + ) return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index 403b6d36f..a3732f620 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -6,7 +6,9 @@ from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): +def get_subreddit_undone( + submissions: list, subreddit, times_checked=0, similarity_scores=None +): """_summary_ Args: @@ -18,7 +20,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print("Sorting based on similarity for a different date filter and thread limit..") + print( + "Sorting based on similarity for a different date filter and thread limit.." + ) submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -27,7 +31,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -43,7 +49,8 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments + <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -52,7 +59,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similari continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep("You are trying to use story mode on post with no post text") + print_substep( + "You are trying to use story mode on post with no post text" + ) continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index 172b4543c..aeb82b4c3 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,11 +1,15 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): +def create_thumbnail( + thumbnail, font_family, font_size, font_color, width, height, title +): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round( + Xaxis / sizeLetterXaxis + ) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -30,6 +34,8 @@ def create_thumbnail(thumbnail, font_family, font_size, font_color, width, heigh # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) + draw.text( + (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font + ) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index 7c756fc61..c30cb2c0f 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,7 +19,9 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -33,7 +35,9 @@ def check_done( return redditobj -def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): +def save_data( + subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str +): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 56595fca5..9bc09d87c 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -43,7 +43,9 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + zoneDiff = ( + pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + ) end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 2ec981258..dc26395b0 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -3,10 +3,10 @@ import re from pathlib import Path from random import randrange -from typing import Any, Tuple, Dict +from typing import Any, Dict, Tuple import yt_dlp -from moviepy.editor import VideoFileClip, AudioFileClip +from moviepy.editor import AudioFileClip, VideoFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from utils import settings @@ -60,7 +60,9 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() + choice = str( + settings.config["settings"]["background"][f"background_{mode}"] + ).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -120,7 +122,9 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): +def chop_background( + background_config: Dict[str, Tuple], video_length: int, reddit_object: dict +): """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: @@ -133,7 +137,9 @@ def chop_background(background_config: Dict[str, Tuple], video_length: int, redd 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]}" + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 437528707..d071d00cb 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -2,18 +2,16 @@ import os import re import tempfile -import threading import textwrap +import threading import time - -from pathlib import Path from os.path import exists # Needs to be imported specifically -from typing import Final -from typing import Tuple, Dict +from pathlib import Path +from typing import Dict, Final, Tuple import ffmpeg import translators -from PIL import ImageDraw, ImageFont, Image +from PIL import Image, ImageDraw, ImageFont from rich.console import Console from rich.progress import track @@ -79,7 +77,9 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text(name, translator="google", to_language=lang) + translated_name = translators.translate_text( + name, translator="google", to_language=lang + ) return translated_name else: return name @@ -118,7 +118,10 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): lines = textwrap.wrap(text, width=wrap) y = ( (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - ( + ((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) + / 2 + ) + 30 ) draw = ImageDraw.Draw(image) @@ -135,28 +138,52 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): if len(lines) == 3: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 40 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + font = ImageFont.truetype( + os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size + ) y = ( (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - ( + ( + (getheight(font, text) + (len(lines) * padding) / len(lines)) + * len(lines) + ) + / 2 + ) + 35 ) elif len(lines) == 4: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 35 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + font = ImageFont.truetype( + os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size + ) y = ( (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - ( + ( + (getheight(font, text) + (len(lines) * padding) / len(lines)) + * len(lines) + ) + / 2 + ) + 40 ) elif len(lines) > 4: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 30 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + font = ImageFont.truetype( + os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size + ) y = ( (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + - ( + ( + (getheight(font, text) + (len(lines) * padding) / len(lines)) + * len(lines) + ) + / 2 + ) + 30 ) @@ -173,7 +200,9 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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"] + background_audio_volume = settings.config["settings"]["background"][ + "background_audio_volume" + ] if background_audio_volume == 0: return audio # Return the original audio else: @@ -227,27 +256,42 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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")) + 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...") + for i in track( + range(number_of_clips + 1), "Collecting the audio files..." + ) ] - audio_clips.insert(0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_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"]) + 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"]), + 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( @@ -290,13 +334,19 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"] + 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"]), + float( + ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"][ + "duration" + ] + ), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -313,7 +363,9 @@ def make_final_video( ) 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..."): + for i in track( + range(0, number_of_clips + 1), "Collecting the image files..." + ): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -329,9 +381,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( - "scale", screenshot_width, -1 - ) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ + "v" + ].filter("scale", screenshot_width, -1) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) assert ( @@ -353,11 +405,15 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep("The 'results' folder could not be found so it was automatically created.") + 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.") + 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 @@ -371,7 +427,11 @@ def make_final_video( 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( - (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), + ( + file + for file in os.listdir("assets/backgrounds") + if file.endswith(".png") + ), None, ) if first_image is None: @@ -393,7 +453,9 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") + print_substep( + f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" + ) text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -434,7 +496,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -464,7 +528,9 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args("-progress", progress.output_file.name).run( + ).overwrite_output().global_args( + "-progress", progress.output_file.name + ).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 507fb5103..161c1fe52 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -36,7 +36,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -46,15 +48,21 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) else: # Switch to dark theme - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-light-mode.json", encoding="utf-8" + ) bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -99,8 +107,12 @@ 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(f'input[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) - page.locator(f'input[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) + page.locator(f'input[name="username"]').fill( + settings.config["reddit"]["creds"]["username"] + ) + page.locator(f'input[name="password"]').fill( + settings.config["reddit"]["creds"]["password"] + ) page.get_by_role("button", name="Log In").click() page.wait_for_timeout(5000) @@ -181,7 +193,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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) + page.locator('[data-test-id="post-content"]').screenshot( + path=postcontentpath + ) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -195,7 +209,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input("Do you want the error traceback for debugging purposes? (y/n)") + resp = input( + "Do you want the error traceback for debugging purposes? (y/n)" + ) if not resp.casefold().startswith("y"): exit() @@ -240,9 +256,13 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator( + f"#t1_{comment['comment_id']}" + ).bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 8495f8d1c..fa29c0172 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -2,15 +2,15 @@ from rich.console import Console -from TTS.GTTS import GTTS -from TTS.TikTok import TikTok from TTS.aws_polly import AWSPolly from TTS.elevenlabs import elevenlabs from TTS.engine_wrapper import TTSEngine +from TTS.GTTS import GTTS from TTS.pyttsx import pyttsx from TTS.streamlabs_polly import StreamlabsPolly +from TTS.TikTok import TikTok from utils import settings -from utils.console import print_table, print_step +from utils.console import print_step, print_table console = Console() @@ -36,7 +36,9 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, voice), reddit_obj + ) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -45,12 +47,18 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, choice), reddit_obj + ) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), + ( + value + for dict_key, value in input_dict.items() + if dict_key.lower() == key.lower() + ), None, ) From 53db79ab2922a469177866adf460a66966c62254 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:39:18 +0000 Subject: [PATCH 49/60] fixup: Format Python code with Black --- GUI.py | 7 +- TTS/TikTok.py | 4 +- TTS/aws_polly.py | 4 +- TTS/elevenlabs.py | 8 +- TTS/engine_wrapper.py | 24 ++--- TTS/pyttsx.py | 4 +- TTS/streamlabs_polly.py | 4 +- main.py | 17 ++-- reddit/subreddit.py | 25 ++--- utils/ai_methods.py | 20 +--- utils/console.py | 17 +--- utils/ffmpeg_install.py | 8 +- utils/gui_utils.py | 22 +---- utils/imagenarator.py | 8 +- utils/playwright.py | 4 +- utils/settings.py | 32 ++----- utils/subreddit.py | 19 +--- utils/thumbnail.py | 12 +-- utils/videos.py | 8 +- utils/voice.py | 4 +- video_creation/background.py | 12 +-- video_creation/final_video.py | 122 ++++++------------------ video_creation/screenshot_downloader.py | 40 ++------ video_creation/voices.py | 14 +-- 24 files changed, 109 insertions(+), 330 deletions(-) diff --git a/GUI.py b/GUI.py index c230f6c80..01fe90d78 100644 --- a/GUI.py +++ b/GUI.py @@ -3,8 +3,7 @@ # Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" import tomlkit -from flask import (Flask, redirect, render_template, request, - send_from_directory, url_for) +from flask import Flask, redirect, render_template, request, send_from_directory, url_for import utils.gui_utils as gui @@ -76,9 +75,7 @@ def settings(): # Change settings config = gui.modify_settings(data, config_load, checks) - return render_template( - "settings.html", file="config.toml", data=config, checks=checks - ) + return render_template("settings.html", file="config.toml", data=config, checks=checks) # Make videos.json accessible diff --git a/TTS/TikTok.py b/TTS/TikTok.py index f9477329e..23d291844 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -86,9 +86,7 @@ def __init__(self): "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", } - self.URI_BASE = ( - "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" - ) + self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" self.max_chars = 200 self._session = requests.Session() diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 58323f96f..4d55860bc 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -41,9 +41,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index 4b3c2d6c6..e896621b3 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -17,13 +17,9 @@ 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() + voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() - audio = self.client.generate( - text=text, voice=voice, model="eleven_multilingual_v1" - ) + audio = self.client.generate(text=text, voice=voice, model="eleven_multilingual_v1") save(audio=audio, filename=filepath) def initialize(self): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index f7a08f527..6d498d278 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -14,7 +14,9 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text -DEFAULT_MAX_LENGTH: int = 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +DEFAULT_MAX_LENGTH: int = ( + 50 # Video length variable, edit this on your own risk. It should work, but it's not supported +) class TTSEngine: @@ -56,9 +58,7 @@ def add_periods( comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"]) comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = re.sub(r"\bAI\b", "A.I", comment["comment_body"]) - comment["comment_body"] = re.sub( - r"\bAGI\b", "A.G.I", comment["comment_body"] - ) + comment["comment_body"] = re.sub(r"\bAGI\b", "A.G.I", comment["comment_body"]) if comment["comment_body"][-1] != ".": comment["comment_body"] += "." comment["comment_body"] = comment["comment_body"].replace(". . .", ".") @@ -80,17 +80,13 @@ def run(self) -> Tuple[int, int]: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: self.split_post(self.reddit_object["thread_post"], "postaudio") else: - self.call_tts( - "postaudio", process_text(self.reddit_object["thread_post"]) - ) + self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) 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)) else: - for idx, comment in track( - enumerate(self.reddit_object["comments"]), "Saving..." - ): + for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): # ! 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 @@ -173,9 +169,7 @@ def create_silence_mp3(self): fps=44100, ) silence = volumex(silence, 0) - silence.write_audiofile( - f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None - ) + silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) def process_text(text: str, clean: bool = True): @@ -183,8 +177,6 @@ def process_text(text: str, clean: bool = True): new_text = sanitize_text(text) if clean else text if lang: print_substep("Translating Text...") - translated_text = translators.translate_text( - text, translator="google", to_language=lang - ) + translated_text = translators.translate_text(text, translator="google", to_language=lang) new_text = sanitize_text(translated_text) return new_text diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index a80bf2d9c..bf47601d8 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,9 +21,7 @@ def run( if voice_id == "" or voice_num == "": voice_id = 2 voice_num = 3 - raise ValueError( - "set pyttsx values to a valid value, switching to defaults" - ) + raise ValueError("set pyttsx values to a valid value, switching to defaults") else: voice_id = int(voice_id) voice_num = int(voice_num) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index f59626d5a..1541fac2f 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -42,9 +42,7 @@ def run(self, text, filepath, random_voice: bool = False): 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() + voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} headers = {"Referer": "https://streamlabs.com/"} diff --git a/main.py b/main.py index 3470fbc82..a2abddf24 100755 --- a/main.py +++ b/main.py @@ -15,13 +15,14 @@ from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion -from video_creation.background import (chop_background, - download_background_audio, - download_background_video, - get_background_config) +from video_creation.background import ( + chop_background, + download_background_audio, + download_background_video, + get_background_config, +) from video_creation.final_video import make_final_video -from video_creation.screenshot_downloader import \ - get_screenshots_of_reddit_posts +from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 __VERSION__ = "3.2.1" @@ -101,9 +102,7 @@ def shutdown() -> NoReturn: sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate( - config["reddit"]["thread"]["post_id"].split("+") - ): + for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index fcf85f312..5f2ac5f34 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -22,9 +22,7 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print( - "\nEnter your two-factor authentication code from your authenticator app.\n" - ) + print("\nEnter your two-factor authentication code from your authenticator app.\n") code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -57,9 +55,7 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub( - r"r\/", "", input("What subreddit would you like to pull from? ") - ) + re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) # removes the r/ from the input ) except ValueError: @@ -69,9 +65,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 ( - str(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) @@ -82,12 +76,8 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission( - id=settings.config["reddit"]["thread"]["post_id"] - ) - elif settings.config["ai"][ - "ai_similarity_enabled" - ]: # ai sorting based on comparison + submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) + elif settings.config["ai"]["ai_similarity_enabled"]: # ai sorting based on comparison threads = subreddit.hot(limit=50) keywords = settings.config["ai"]["ai_similarity_keywords"].split(",") keywords = [keyword.strip() for keyword in keywords] @@ -105,10 +95,7 @@ def get_subreddit_threads(POST_ID: str): if submission is None: return get_subreddit_threads(POST_ID) # submission already done. rerun - elif ( - not submission.num_comments - and settings.config["settings"]["storymode"] == "false" - ): + elif not submission.num_comments and settings.config["settings"]["storymode"] == "false": print_substep("No comments found. Skipping.") exit() diff --git a/utils/ai_methods.py b/utils/ai_methods.py index 8c3fbf13a..e62894263 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -5,12 +5,8 @@ # Mean Pooling - Take attention mask into account for correct averaging def mean_pooling(model_output, attention_mask): - token_embeddings = model_output[ - 0 - ] # First element of model_output contains all token embeddings - input_mask_expanded = ( - attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() - ) + token_embeddings = model_output[0] # First element of model_output contains all token embeddings + input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( input_mask_expanded.sum(1), min=1e-9 ) @@ -36,19 +32,13 @@ def sort_by_similarity(thread_objects, keywords): ) with torch.no_grad(): threads_embeddings = model(**encoded_threads) - threads_embeddings = mean_pooling( - threads_embeddings, encoded_threads["attention_mask"] - ) + threads_embeddings = mean_pooling(threads_embeddings, encoded_threads["attention_mask"]) # Keywords inference - encoded_keywords = tokenizer( - keywords, padding=True, truncation=True, return_tensors="pt" - ) + encoded_keywords = tokenizer(keywords, padding=True, truncation=True, return_tensors="pt") with torch.no_grad(): keywords_embeddings = model(**encoded_keywords) - keywords_embeddings = mean_pooling( - keywords_embeddings, encoded_keywords["attention_mask"] - ) + keywords_embeddings = mean_pooling(keywords_embeddings, encoded_keywords["attention_mask"]) # Compare every keyword w/ every thread embedding threads_embeddings_tensor = torch.tensor(threads_embeddings) diff --git a/utils/console.py b/utils/console.py index 7ac8a7035..18c3248b5 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,10 +49,7 @@ def handle_input( optional=False, ): if optional: - console.print( - message - + "\n[green]This is an optional value. Do you want to skip it? (y/n)" - ) + console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -86,11 +83,7 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print( - "[red]" - + err_message - + "\nAre you absolutely sure it's correct?(y/n)" - ) + console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") if input().casefold().startswith("y"): break continue @@ -123,9 +116,5 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" - + err_message - + "\nValid options are: " - + ", ".join(map(str, options)) - + "." + "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." ) diff --git a/utils/ffmpeg_install.py b/utils/ffmpeg_install.py index b29301ffa..b2c673d1d 100644 --- a/utils/ffmpeg_install.py +++ b/utils/ffmpeg_install.py @@ -7,7 +7,9 @@ def ffmpeg_install_windows(): try: - ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ffmpeg_url = ( + "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip" + ) ffmpeg_zip_filename = "ffmpeg.zip" ffmpeg_extracted_folder = "ffmpeg" @@ -127,9 +129,7 @@ def ffmpeg_install(): elif os.name == "mac": ffmpeg_install_mac() else: - print( - "Your OS is not supported. Please install FFmpeg manually and try again." - ) + print("Your OS is not supported. Please install FFmpeg manually and try again.") exit() else: print("Please install FFmpeg manually and try again.") diff --git a/utils/gui_utils.py b/utils/gui_utils.py index 9d644d887..f683adfec 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -67,11 +67,7 @@ def check(value, checks): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -80,16 +76,8 @@ def check(value, checks): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -162,9 +150,7 @@ def delete_background(key): # Add background video def add_background(youtube_uri, filename, citation, position): # Validate YouTube URI - regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search( - youtube_uri - ) + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(youtube_uri) if not regex: flash("YouTube URI is invalid!", "error") diff --git a/utils/imagenarator.py b/utils/imagenarator.py index e26f9e7c7..509882d76 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -19,9 +19,7 @@ def draw_multiple_line_text( font_height = getheight(font, text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - ( - ((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2 - ) + y = (image_height / 2) - (((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2) for line in lines: line_width, line_height = getsize(font, line) if transparent: @@ -73,7 +71,5 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) - draw_multiple_line_text( - image, text, font, txtclr, padding, wrap=30, transparent=transparent - ) + draw_multiple_line_text(image, text, font, txtclr, padding, wrap=30, transparent=transparent) image.save(f"assets/temp/{id}/png/img{idx}.png") diff --git a/utils/playwright.py b/utils/playwright.py index be046e6b3..9672f03d1 100644 --- a/utils/playwright.py +++ b/utils/playwright.py @@ -1,7 +1,5 @@ def clear_cookie_by_name(context, cookie_cleared_name): cookies = context.cookies() - filtered_cookies = [ - cookie for cookie in cookies if cookie["name"] != cookie_cleared_name - ] + filtered_cookies = [cookie for cookie in cookies if cookie["name"] != cookie_cleared_name] context.clear_cookies() context.add_cookies(filtered_cookies) diff --git a/utils/settings.py b/utils/settings.py index 2a917232b..2ebaef3af 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -53,11 +53,7 @@ def get_check_value(key, default_result): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ( - "nmax" in checks - and checks["nmax"] is not None - and value > checks["nmax"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -65,16 +61,8 @@ def get_check_value(key, default_result): not incorrect and hasattr(value, "__iter__") and ( - ( - "nmin" in checks - and checks["nmin"] is not None - and len(value) < checks["nmin"] - ) - or ( - "nmax" in checks - and checks["nmax"] is not None - and len(value) > checks["nmax"] - ) + ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) + or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): incorrect = True @@ -82,15 +70,9 @@ def get_check_value(key, default_result): if incorrect: value = handle_input( message=( - ( - ("[blue]Example: " + str(checks["example"]) + "\n") - if "example" in checks - else "" - ) + (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + "[red]" - + ("Non-optional ", "Optional ")[ - "optional" in checks and checks["optional"] is True - ] + + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] ) + "[#C0CAF5 bold]" + str(name) @@ -131,9 +113,7 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print( - f"[red bold]Encountered error when trying to to load {template_file}: {error}" - ) + console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index a3732f620..403b6d36f 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -6,9 +6,7 @@ from utils.console import print_substep -def get_subreddit_undone( - submissions: list, subreddit, times_checked=0, similarity_scores=None -): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0, similarity_scores=None): """_summary_ Args: @@ -20,9 +18,7 @@ def get_subreddit_undone( """ # Second try of getting a valid Submission if times_checked and settings.config["ai"]["ai_similarity_enabled"]: - print( - "Sorting based on similarity for a different date filter and thread limit.." - ) + print("Sorting based on similarity for a different date filter and thread limit..") submissions = sort_by_similarity( submissions, keywords=settings.config["ai"]["ai_similarity_enabled"] ) @@ -31,9 +27,7 @@ def get_subreddit_undone( if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for i, submission in enumerate(submissions): if already_done(done_videos, submission): @@ -49,8 +43,7 @@ def get_subreddit_undone( print_substep("This post was pinned by moderators. Skipping...") continue if ( - submission.num_comments - <= int(settings.config["reddit"]["thread"]["min_comments"]) + submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]) and not settings.config["settings"]["storymode"] ): print_substep( @@ -59,9 +52,7 @@ def get_subreddit_undone( continue if settings.config["settings"]["storymode"]: if not submission.selftext: - print_substep( - "You are trying to use story mode on post with no post text" - ) + print_substep("You are trying to use story mode on post with no post text") continue else: # Check for the length of the post text diff --git a/utils/thumbnail.py b/utils/thumbnail.py index aeb82b4c3..172b4543c 100644 --- a/utils/thumbnail.py +++ b/utils/thumbnail.py @@ -1,15 +1,11 @@ from PIL import ImageDraw, ImageFont -def create_thumbnail( - thumbnail, font_family, font_size, font_color, width, height, title -): +def create_thumbnail(thumbnail, font_family, font_size, font_color, width, height, title): font = ImageFont.truetype(font_family + ".ttf", font_size) Xaxis = width - (width * 0.2) # 20% of the width sizeLetterXaxis = font_size * 0.5 # 50% of the font size - XaxisLetterQty = round( - Xaxis / sizeLetterXaxis - ) # Quantity of letters that can fit in the X axis + XaxisLetterQty = round(Xaxis / sizeLetterXaxis) # Quantity of letters that can fit in the X axis MarginYaxis = height * 0.12 # 12% of the height MarginXaxis = width * 0.05 # 5% of the width # 1.1 rem @@ -34,8 +30,6 @@ def create_thumbnail( # loop for put the title in the thumbnail for i in range(0, len(arrayTitle)): # 1.1 rem - draw.text( - (MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font - ) + draw.text((MarginXaxis, MarginYaxis + (LineHeight * i)), arrayTitle[i], rgb, font=font) return thumbnail diff --git a/utils/videos.py b/utils/videos.py index c30cb2c0f..7c756fc61 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,9 +19,7 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -35,9 +33,7 @@ def check_done( return redditobj -def save_data( - subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str -): +def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index 9bc09d87c..56595fca5 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -43,9 +43,7 @@ def sleep_until(time) -> None: if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = ( - pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() - ) + zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index dc26395b0..43be69ac0 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -60,9 +60,7 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(mode: str): """Fetch the background/s configuration""" try: - choice = str( - settings.config["settings"]["background"][f"background_{mode}"] - ).casefold() + choice = str(settings.config["settings"]["background"][f"background_{mode}"]).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -122,9 +120,7 @@ def download_background_audio(background_config: Tuple[str, str, str]): print_substep("Background audio downloaded successfully! 🎉", style="bold green") -def chop_background( - background_config: Dict[str, Tuple], video_length: int, reddit_object: dict -): +def chop_background(background_config: Dict[str, Tuple], video_length: int, reddit_object: dict): """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: @@ -137,9 +133,7 @@ def chop_background( 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]}" - ) + 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 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index d071d00cb..101d0f774 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -77,9 +77,7 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: print_substep("Translating filename...") - translated_name = translators.translate_text( - name, translator="google", to_language=lang - ) + translated_name = translators.translate_text(name, translator="google", to_language=lang) return translated_name else: return name @@ -118,10 +116,7 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): lines = textwrap.wrap(text, width=wrap) y = ( (image_height / 2) - - ( - ((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) - / 2 - ) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 ) draw = ImageDraw.Draw(image) @@ -138,52 +133,28 @@ def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): if len(lines) == 3: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 40 - font = ImageFont.truetype( - os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size - ) + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - ( - ( - (getheight(font, text) + (len(lines) * padding) / len(lines)) - * len(lines) - ) - / 2 - ) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 35 ) elif len(lines) == 4: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 35 - font = ImageFont.truetype( - os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size - ) + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - ( - ( - (getheight(font, text) + (len(lines) * padding) / len(lines)) - * len(lines) - ) - / 2 - ) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 40 ) elif len(lines) > 4: lines = textwrap.wrap(text, width=wrap + 10) font_title_size = 30 - font = ImageFont.truetype( - os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size - ) + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) y = ( (image_height / 2) - - ( - ( - (getheight(font, text) + (len(lines) * padding) / len(lines)) - * len(lines) - ) - / 2 - ) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + 30 ) @@ -200,9 +171,7 @@ def merge_background_audio(audio: ffmpeg, reddit_id: str): 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" - ] + background_audio_volume = settings.config["settings"]["background"]["background_audio_volume"] if background_audio_volume == 0: return audio # Return the original audio else: @@ -256,42 +225,27 @@ def make_final_video( if settings.config["settings"]["storymode"]: if settings.config["settings"]["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") - ) + 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..." - ) + for i in track(range(number_of_clips + 1), "Collecting the audio files...") ] - audio_clips.insert( - 0, ffmpeg.input(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 = [ - ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") - for i in range(number_of_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" - ] - ) + 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" - ] - ), + 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( @@ -334,19 +288,13 @@ def make_final_video( if settings.config["settings"]["storymode"]: audio_clips_durations = [ float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")[ - "format" - ]["duration"] + 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" - ] - ), + float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -363,9 +311,7 @@ def make_final_video( ) 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..." - ): + for i in track(range(0, number_of_clips + 1), "Collecting the image files..."): image_clips.append( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( "scale", screenshot_width, -1 @@ -381,9 +327,9 @@ def make_final_video( else: for i in range(0, number_of_clips + 1): image_clips.append( - ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")[ - "v" - ].filter("scale", screenshot_width, -1) + ffmpeg.input(f"assets/temp/{reddit_id}/png/comment_{i}.png")["v"].filter( + "scale", screenshot_width, -1 + ) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) assert ( @@ -405,15 +351,11 @@ def make_final_video( subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): - print_substep( - "The 'results' folder could not be found so it was automatically created." - ) + 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." - ) + 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 @@ -427,11 +369,7 @@ def make_final_video( 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( - ( - file - for file in os.listdir("assets/backgrounds") - if file.endswith(".png") - ), + (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), None, ) if first_image is None: @@ -453,9 +391,7 @@ def make_final_video( title_thumb, ) thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep( - f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" - ) + print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( @@ -496,9 +432,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, @@ -528,9 +462,7 @@ def on_update_example(progress) -> None: "b:a": "192k", "threads": multiprocessing.cpu_count(), }, - ).overwrite_output().global_args( - "-progress", progress.output_file.name - ).run( + ).overwrite_output().global_args("-progress", progress.output_file.name).run( quiet=True, overwrite_output=True, capture_stdout=False, diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 161c1fe52..507fb5103 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -36,9 +36,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # set the theme and disable non-essential cookies if settings.config["settings"]["theme"] == "dark": - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False @@ -48,21 +46,15 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (0, 0, 0, 0) txtcolor = (255, 255, 255) transparent = True - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") else: # Switch to dark theme - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") bgcolor = (33, 33, 36, 255) txtcolor = (240, 240, 240) transparent = False else: - cookie_file = open( - "./video_creation/data/cookie-light-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False @@ -107,12 +99,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(f'input[name="username"]').fill( - settings.config["reddit"]["creds"]["username"] - ) - page.locator(f'input[name="password"]').fill( - settings.config["reddit"]["creds"]["password"] - ) + page.locator(f'input[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) + page.locator(f'input[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) page.get_by_role("button", name="Log In").click() page.wait_for_timeout(5000) @@ -193,9 +181,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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 - ) + page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) except Exception as e: print_substep("Something went wrong!", style="red") resp = input( @@ -209,9 +195,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): "green", ) - resp = input( - "Do you want the error traceback for debugging purposes? (y/n)" - ) + resp = input("Do you want the error traceback for debugging purposes? (y/n)") if not resp.casefold().startswith("y"): exit() @@ -256,13 +240,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): # 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() + 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() + location = page.locator(f"#t1_{comment['comment_id']}").bounding_box() for i in location: location[i] = float("{:.2f}".format(location[i] * zoom)) page.screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index fa29c0172..ad94a140c 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -36,9 +36,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, voice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -47,18 +45,12 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, choice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - ( - value - for dict_key, value in input_dict.items() - if dict_key.lower() == key.lower() - ), + (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), None, ) From 260c05b0d69afc4ee90d4241a80387861f96d142 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:43:41 -0400 Subject: [PATCH 50/60] Update actions --- .github/workflows/autoblack.yml | 17 ++++++++++++----- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/lint.yml | 5 ++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/autoblack.yml b/.github/workflows/autoblack.yml index 8e367fed2..7c786873c 100644 --- a/.github/workflows/autoblack.yml +++ b/.github/workflows/autoblack.yml @@ -3,7 +3,7 @@ # Othewrwise, Black is run and its changes are committed back to the incoming pull request. # https://github.com/cclauss/autoblack -name: autoblack +name: fmt on: push: branches: ["develop"] @@ -11,11 +11,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.10 - name: Install Black run: pip install black - name: Run black --check . @@ -30,3 +30,10 @@ jobs: git checkout $GITHUB_HEAD_REF git commit -am "fixup: Format Python code with Black" git push origin HEAD:develop + + - uses: isort/isort-action@v1 + with: + requirement-files: "requirements.txt" + + + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ec78b1a34..0f392ff8a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f95531f91..d758192fd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: psf/black@stable with: options: "--line-length 101" + - uses: isort/isort-action@v1 + with: + requirement-files: "requirements.txt" From 6ade93390cc76ed90ada478e884e6f8c7540c578 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:48:19 -0400 Subject: [PATCH 51/60] Fix actions + update dockerfile python v --- .github/workflows/{autoblack.yml => fmt.yml} | 4 ++-- .github/workflows/lint.yml | 2 +- Dockerfile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{autoblack.yml => fmt.yml} (93%) diff --git a/.github/workflows/autoblack.yml b/.github/workflows/fmt.yml similarity index 93% rename from .github/workflows/autoblack.yml rename to .github/workflows/fmt.yml index 7c786873c..abe385425 100644 --- a/.github/workflows/autoblack.yml +++ b/.github/workflows/fmt.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: 3.10 + python-version: 3.10.14 - name: Install Black run: pip install black - name: Run black --check . @@ -33,7 +33,7 @@ jobs: - uses: isort/isort-action@v1 with: - requirement-files: "requirements.txt" + requirements-files: "requirements.txt" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d758192fd..55479578b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,4 +12,4 @@ jobs: options: "--line-length 101" - uses: isort/isort-action@v1 with: - requirement-files: "requirements.txt" + requirements-files: "requirements.txt" diff --git a/Dockerfile b/Dockerfile index add8e246c..3f53adae7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.9-slim +FROM python:3.10.14-slim RUN apt update RUN apt-get install -y ffmpeg From a2b094eadc1722a90364dde2b58d46f21749fd1e Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:49:38 -0400 Subject: [PATCH 52/60] fix black run cmd --- .github/workflows/fmt.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index abe385425..eceba030b 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -18,8 +18,8 @@ jobs: python-version: 3.10.14 - name: Install Black run: pip install black - - name: Run black --check . - run: black --check . + - name: Run black check + run: black --check . --line-length 101 - name: If needed, commit black changes to the pull request if: failure() run: | From 2113543ad9d36d2c5ac46a90fb06023978e0af6e Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:53:55 -0400 Subject: [PATCH 53/60] intentially leave GUI.py unformatted to test GHA --- .github/workflows/fmt.yml | 12 +++++------- .github/workflows/lint.yml | 2 -- main.py | 13 ++++++------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index eceba030b..d9a625dfc 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -8,7 +8,7 @@ on: push: branches: ["develop"] jobs: - build: + format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -16,6 +16,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.10.14 + - uses: isort/isort-action@v1 + with: + check-only: false - name: Install Black run: pip install black - name: Run black check @@ -31,9 +34,4 @@ jobs: git commit -am "fixup: Format Python code with Black" git push origin HEAD:develop - - uses: isort/isort-action@v1 - with: - requirements-files: "requirements.txt" - - - + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 55479578b..b944a9e30 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,5 +11,3 @@ jobs: with: options: "--line-length 101" - uses: isort/isort-action@v1 - with: - requirements-files: "requirements.txt" diff --git a/main.py b/main.py index a2abddf24..ba9b6f18b 100755 --- a/main.py +++ b/main.py @@ -15,14 +15,13 @@ from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion -from video_creation.background import ( - chop_background, - download_background_audio, - download_background_video, - get_background_config, -) +from video_creation.background import (chop_background, + download_background_audio, + download_background_video, + get_background_config) from video_creation.final_video import make_final_video -from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts +from video_creation.screenshot_downloader import \ + get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 __VERSION__ = "3.2.1" From 1bf0352e7866e59ddd64a08e96f775f4fbb2cab5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:54:58 -0400 Subject: [PATCH 54/60] remove an invalid check-only flag --- .github/workflows/fmt.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index d9a625dfc..927ffd817 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -17,8 +17,6 @@ jobs: with: python-version: 3.10.14 - uses: isort/isort-action@v1 - with: - check-only: false - name: Install Black run: pip install black - name: Run black check From 79e1346a06ee866a8fe61c5f3eb12b61fa2e7d79 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:57:22 -0400 Subject: [PATCH 55/60] remove an invalid check-only flag --- .github/workflows/fmt.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 927ffd817..2df6338a0 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -16,15 +16,17 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.10.14 - - uses: isort/isort-action@v1 - - name: Install Black - run: pip install black + - name: Install Black & isort + run: pip install black isort - name: Run black check run: black --check . --line-length 101 + - name: Run isort check + run: isort . --check-only - name: If needed, commit black changes to the pull request if: failure() run: | black . --line-length 101 + isort . git config --global user.name github-actions git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY From 25e7979715241e568e8b6777df427f2580910ecd Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:57:37 +0000 Subject: [PATCH 56/60] fixup: Format Python code with Black --- GUI.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GUI.py b/GUI.py index 01fe90d78..299dcfe66 100644 --- a/GUI.py +++ b/GUI.py @@ -3,7 +3,8 @@ # Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" import tomlkit -from flask import Flask, redirect, render_template, request, send_from_directory, url_for +from flask import (Flask, redirect, render_template, request, + send_from_directory, url_for) import utils.gui_utils as gui From fd9b0a936a2817095365e6878dba9f2a6ad440cd Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 17:58:26 -0400 Subject: [PATCH 57/60] change comment (last push) --- .github/workflows/fmt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 2df6338a0..9919fd46e 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -22,7 +22,7 @@ jobs: run: black --check . --line-length 101 - name: Run isort check run: isort . --check-only - - name: If needed, commit black changes to the pull request + - name: If needed, commit changes to the pull request if: failure() run: | black . --line-length 101 From b4fadbcf210dabfdd7c5cf68b3d0cc330c81b2d3 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 18:01:56 -0400 Subject: [PATCH 58/60] fix isort profile --- .github/workflows/fmt.yml | 4 ++-- .github/workflows/lint.yml | 2 ++ GUI.py | 10 ++++++++-- main.py | 13 +++++++------ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 9919fd46e..c09814a3d 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -21,12 +21,12 @@ jobs: - name: Run black check run: black --check . --line-length 101 - name: Run isort check - run: isort . --check-only + run: isort . --check-only --diff --profile black - name: If needed, commit changes to the pull request if: failure() run: | black . --line-length 101 - isort . + isort . --profile black git config --global user.name github-actions git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b944a9e30..e93afee05 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,3 +11,5 @@ jobs: with: options: "--line-length 101" - uses: isort/isort-action@v1 + with: + configuration: "--check-only --diff --profile black" diff --git a/GUI.py b/GUI.py index 299dcfe66..4588083dd 100644 --- a/GUI.py +++ b/GUI.py @@ -3,8 +3,14 @@ # Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" import tomlkit -from flask import (Flask, redirect, render_template, request, - send_from_directory, url_for) +from flask import ( + Flask, + redirect, + render_template, + request, + send_from_directory, + url_for, +) import utils.gui_utils as gui diff --git a/main.py b/main.py index ba9b6f18b..a2abddf24 100755 --- a/main.py +++ b/main.py @@ -15,13 +15,14 @@ from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion -from video_creation.background import (chop_background, - download_background_audio, - download_background_video, - get_background_config) +from video_creation.background import ( + chop_background, + download_background_audio, + download_background_video, + get_background_config, +) from video_creation.final_video import make_final_video -from video_creation.screenshot_downloader import \ - get_screenshots_of_reddit_posts +from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 __VERSION__ = "3.2.1" From 6318b398d1906c8098def38adf2e7441e43c945c Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 18:11:11 -0400 Subject: [PATCH 59/60] Fix headless issues. closes #2024 --- video_creation/screenshot_downloader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 507fb5103..6b56e9993 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -74,7 +74,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): print_substep("Launching Headless Browser...") browser = p.chromium.launch( - headless=False + headless=True ) # headless=False will show the browser for debugging purposes # Device scale factor (or dsf for short) allows us to increase the resolution of the screenshots # When the dsf is 1, the width of the screenshot is 600 pixels @@ -86,6 +86,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): color_scheme="dark", viewport=ViewportSize(width=W, height=H), device_scale_factor=dsf, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", ) cookies = json.load(cookie_file) cookie_file.close() From 6b474b4b50cf1abacc7520e04b22c09d133ce11f Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 16 Jun 2024 18:13:14 -0400 Subject: [PATCH 60/60] CHORE: UPDATE VERSION --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index a2abddf24..849663d0b 100755 --- a/main.py +++ b/main.py @@ -25,7 +25,7 @@ from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -__VERSION__ = "3.2.1" +__VERSION__ = "3.3.0" print( """