diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca1c7cb..8fc7d3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ + # Contributing to Reddit Video Maker Bot 🎥 Thanks for taking the time to contribute! ❤️ @@ -105,6 +106,25 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/elebum You development environment should follow the requirements stated in the [README file](README.md). If you are not using the specified versions, **please reference this in your pull request**, so reviewers can test your code on both versions. +#### Setting up your development repository + +These steps are only specified for beginner developers trying to contribute to this repository. +If you know how to make a fork and clone, you can skip these steps. + +Before contributing, follow these steps (if you are a beginner) + +- Create a fork of this repository to your personal account +- Clone the repo to your computer +- Make sure that you have all dependencies installed +- Run `python main.py` to make sure that the program is working +- Now, you are all setup to contribute your own features to this repo! + +Even if you are a beginner to working with python or contributing to open source software, +don't worry! You can still try contributing even to the documentation! + +("Setting up your development repository" was written by a beginner developer themselves!) + + #### Making your first PR When making your PR, follow these guidelines: @@ -114,7 +134,7 @@ When making your PR, follow these guidelines: - You link any issues that are resolved or fixed by your changes. (this is done by typing "Fixes #\") in your pull request - Where possible, you have used `git pull --rebase`, to avoid creating unnecessary merge commits - You have meaningful commits, and if possible, follow the commit style guide of `type: explanation` -- Here are the commit types: +- Here are the commit types: - **feat** - a new feature - **fix** - a bug fix - **docs** - a change to documentation / commenting diff --git a/GUI.py b/GUI.py new file mode 100644 index 0000000..90e37ae --- /dev/null +++ b/GUI.py @@ -0,0 +1,30 @@ +# Import the server module +import http.server +import webbrowser +# Set the hostname +HOST = "localhost" +# Set the port number +PORT = 4000 + +# Define class to display the index page of the web server +class PythonServer(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == '/GUI': + self.path = 'index.html' + return http.server.SimpleHTTPRequestHandler.do_GET(self) + +# Declare object of the class +webServer = http.server.HTTPServer((HOST, PORT), PythonServer) +# Print the URL of the webserver, new =2 opens in a new tab +print(f"Server started at http://{HOST}:{PORT}/GUI/") +webbrowser.open(f'http://{HOST}:{PORT}/GUI/', new = 2) +print("Website opened in new tab") +print("Press Ctrl+C to quit") +try: + # Run the web server + webServer.serve_forever() +except KeyboardInterrupt: + # Stop the web server + webServer.server_close() + print("The server is stopped.") + exit() \ No newline at end of file diff --git a/GUI/index.html b/GUI/index.html index 807c9e7..c771adc 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -149,7 +149,7 @@ video += '
'; video += '
'; video += 'View'; - video += 'Download'; + video += 'Download'; video += '
'; video += '
'; video += ''; diff --git a/README.md b/README.md index 8a9e205..8d61d9a 100644 --- a/README.md +++ b/README.md @@ -51,20 +51,22 @@ 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". +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 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. (Note if you got an error installing or running the bot try first rerunning the command with a three after the name e.g. python3 or pip3) +If you want to read more detailed guide about the bot, please refer to the [documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/) + ## Video https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4 ## Contributing & Ways to improve 📈 -In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made. +In its current state, this bot does exactly what it needs to do. However, improvements can always be made! I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute! diff --git a/TTS/GTTS.py b/TTS/GTTS.py index 31e29df..cef1b24 100644 --- a/TTS/GTTS.py +++ b/TTS/GTTS.py @@ -8,7 +8,7 @@ max_chars = 0 class GTTS: def __init__(self): - self.max_chars = 0 + self.max_chars = 5000 self.voices = [] def run(self, text, filepath): diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index efd762b..eac5884 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -26,7 +26,7 @@ voices = [ class AWSPolly: def __init__(self): - self.max_chars = 0 + self.max_chars = 3000 self.voices = voices def run(self, text, filepath, random_voice: bool = False): diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index c5ed714..9a64f0e 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFUALT_MAX_LENGTH: int = 50 # video length variable +DEFAULT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: @@ -35,13 +35,15 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFUALT_MAX_LENGTH, + max_length: int = DEFAULT_MAX_LENGTH, + last_clip_length: int = 0, ): self.tts_module = tts_module() self.reddit_object = reddit_object self.path = path self.max_length = max_length self.length = 0 + self.last_clip_length = last_clip_length def run(self) -> Tuple[int, int]: @@ -55,24 +57,24 @@ class TTSEngine: print_step("Saving Text to MP3 files...") - self.call_tts("title", self.reddit_object["thread_title"]) - if ( - self.reddit_object["thread_post"] != "" - and settings.config["settings"]["storymode"] == True - ): - self.call_tts("posttext", self.reddit_object["thread_post"]) + self.call_tts("title", process_text(self.reddit_object["thread_title"])) + processed_text = process_text(self.reddit_object["thread_post"]) + if processed_text != "" and settings.config["settings"]["storymode"] == True: + self.call_tts("posttext", processed_text) idx = None 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: + self.length -= self.last_clip_length + idx -= 1 break if ( len(comment["comment_body"]) > self.tts_module.max_chars ): # Split the comment if it is too long self.split_post(comment["comment_body"], idx) # Split the comment else: # If the comment is not too long, just call the tts engine - self.call_tts(f"{idx}", comment["comment_body"]) + self.call_tts(f"{idx}", process_text(comment["comment_body"])) print_substep("Saved Text to MP3 files successfully.", style="bold green") return self.length, idx @@ -88,11 +90,12 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - if not text_cut or text_cut.isspace(): + new_text = process_text(text_cut) + if not new_text or new_text.isspace(): offset += 1 continue - self.call_tts(f"{idx}-{idy - offset}.part", text_cut) + self.call_tts(f"{idx}-{idy - offset}.part", new_text) split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3")) CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile( @@ -110,13 +113,14 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run(text=process_text(text), filepath=f"{self.path}/{filename}.mp3") + self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") try: clip = AudioFileClip(f"{self.path}/{filename}.mp3") + self.last_clip_length = clip.duration self.length += clip.duration clip.close() except: diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py new file mode 100644 index 0000000..06d27c5 --- /dev/null +++ b/TTS/pyttsx.py @@ -0,0 +1,42 @@ +import random +import pyttsx3 +from utils import settings + + +class pyttsx: + def __init__(self): + self.max_chars = 5000 + self.voices = [] + + def run( + self, + text: str, + filepath: str, + random_voice=False, + ): + voice_id = settings.config["settings"]["tts"]["python_voice"] + voice_num = settings.config["settings"]["tts"]["py_voice_num"] + if voice_id == "" or voice_num == "": + voice_id = 2 + voice_num = 3 + raise ValueError( + "set pyttsx values to a valid value, switching to defaults" + ) + else: + voice_id = int(voice_id) + voice_num = int(voice_num) + for i in range(voice_num): + self.voices.append(i) + i = +1 + if random_voice: + voice_id = self.randomvoice() + engine = pyttsx3.init() + voices = engine.getProperty("voices") + engine.setProperty( + "voice", voices[voice_id].id + ) # changing index changes voices but ony 0 and 1 are working here + engine.save_to_file(text, f"{filepath}") + engine.runAndWait() + + def randomvoice(self): + return random.choice(self.voices) diff --git a/install.sh b/install.sh index 782069d..f85c80b 100644 --- a/install.sh +++ b/install.sh @@ -68,31 +68,34 @@ function install_macos(){ # Function to install for arch (and other forks like manjaro) function install_arch(){ echo "Installing required packages" - sudo pacman -S --needed python3 tk git && python3 -m ensurepip || install_fail + sudo pacman -S --needed python3 tk git && python3 -m ensurepip unzip || install_fail } # Function to install for debian (and ubuntu) function install_deb(){ echo "Installing required packages" - sudo apt install python3 python3-dev python3-tk python3-pip git || install_fail + sudo apt install python3 python3-dev python3-tk python3-pip unzip || install_fail } # Function to install for fedora (and other forks) function install_fedora(){ echo "Installing required packages" - sudo dnf install python3 python3-tkinter python3-pip git python3-devel || install_fail + sudo dnf install python3 python3-tkinter python3-pip python3-devel unzip || install_fail } # Function to install for centos (and other forks based on it) function install_centos(){ echo "Installing required packages" sudo yum install -y python3 || install_fail - sudo yum install -y python3-tkinter epel-release python3-pip git || install_fail + sudo yum install -y python3-tkinter epel-release python3-pip unzip|| install_fail } function get_the_bot(){ echo "Downloading the bot" - git clone https://github.com/elebumm/RedditVideoMakerBot.git + rm -rf RedditVideoMakerBot-master + curl -sL https://github.com/elebumm/RedditVideoMakerBot/archive/refs/heads/master.zip -o master.zip + unzip master.zip + rm -rf master.zip } #install python dependencies @@ -100,7 +103,7 @@ function install_python_dep(){ # tell the user that the script is going to install the python dependencies echo "Installing python dependencies" # cd into the directory - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master # install the dependencies pip3 install -r requirements.txt # cd out @@ -112,7 +115,7 @@ function install_playwright(){ # tell the user that the script is going to install playwright echo "Installing playwright" # cd into the directory where the script is downloaded - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master # run the install script python3 -m playwright install python3 -m playwright install-deps @@ -198,9 +201,9 @@ function install_main(){ install_playwright fi - DIR="./RedditVideoMakerBot" + DIR="./RedditVideoMakerBot-master" if [ -d "$DIR" ]; then - printf "\nThe bot is already installed, want to run it?" + printf "\nThe bot is installed, want to run it?" # if -y (assume yes) continue if [[ ASSUME_YES -eq 1 ]]; then echo "Assuming yes" @@ -214,7 +217,7 @@ function install_main(){ exit 1 fi fi - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master python3 main.py fi } diff --git a/main.py b/main.py index 10ab3c1..bef8ff8 100755 --- a/main.py +++ b/main.py @@ -2,6 +2,9 @@ import math from subprocess import Popen from os import name + +from prawcore import ResponseException + from reddit.subreddit import get_subreddit_threads from utils.cleanup import cleanup from utils.console import print_markdown, print_step @@ -16,7 +19,7 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -__VERSION__ = "2.3" +__VERSION__ = "2.4" __BRANCH__ = "master" print( @@ -51,14 +54,21 @@ def main(POST_ID=None): def run_many(times): 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}' + 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() +def shutdown(): + print_markdown("## Clearing temp files") + cleanup() + print("Exiting...") + exit() + + if __name__ == "__main__": - config = settings.check_toml(".config.template.toml", "config.toml") + config = settings.check_toml("utils/.config.template.toml", "config.toml") config is False and exit() try: if config["settings"]["times_to_run"]: @@ -68,13 +78,19 @@ if __name__ == "__main__": 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("+"))}' + 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("+"))}' ) main(post_id) Popen("cls" if name == "nt" else "clear", shell=True).wait() else: main() except KeyboardInterrupt: - print_markdown("## Clearing temp files") - cleanup() - exit() + shutdown() + except ResponseException: + # error for invalid credentials + print_markdown("## Invalid credentials") + print_markdown("Please check your credentials in the config.toml file") + + shutdown() + + # todo error diff --git a/ptt.py b/ptt.py new file mode 100644 index 0000000..6b49ef6 --- /dev/null +++ b/ptt.py @@ -0,0 +1,10 @@ +import pyttsx3 + +engine = pyttsx3.init() +voices = engine.getProperty("voices") +for voice in voices: + print(voice, voice.id) + engine.setProperty("voice", voice.id) + engine.say("Hello World!") + engine.runAndWait() + engine.stop() diff --git a/requirements.txt b/requirements.txt index e57a37d..7bccd0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 + +Pillow~=9.1.1 diff --git a/.config.template.toml b/utils/.config.template.toml similarity index 55% rename from .config.template.toml rename to utils/.config.template.toml index 18f1b00..31ae9aa 100644 --- a/.config.template.toml +++ b/utils/.config.template.toml @@ -1,45 +1,39 @@ [reddit.creds] -client_id = { optional = false, nmin = 12, nmax = 30, explanation = "the ID of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The ID should be over 12 and under 30 characters, double check your input." } -client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "the SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." } -username = { optional = false, nmin = 3, nmax = 20, explanation = "the username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" } -password = { optional = false, nmin = 8, explanation = "the password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" } -2fa = { optional = true, type = "bool", options = [true, - false, -], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true } +client_id = { optional = false, nmin = 12, nmax = 30, explanation = "The ID of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The ID should be over 12 and under 30 characters, double check your input." } +client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "The SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." } +username = { optional = false, nmin = 3, nmax = 20, explanation = "The username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" } +password = { optional = false, nmin = 8, explanation = "The password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" } +2fa = { optional = true, type = "bool", options = [true, false,], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true } [reddit.thread] -random = { optional = true, options = [true, - false, -], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } -subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } -post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } +random = { optional = true, options = [true, false,], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } +subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "What subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } +post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z0-9])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr" } min_comments = { default = 20, optional = false, nmin = 15, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } + + [settings] -allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, - false, -], explanation = "Whether to allow NSFW content, True or False" } -theme = { optional = false, default = "dark", example = "light", options = ["dark", - "light", -], explanation = "sets the Reddit theme, either LIGHT or DARK" } -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." } +allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to allow NSFW content, True or False" } +theme = { optional = false, default = "dark", example = "light", options = ["dark", "light",], explanation = "Sets the Reddit theme, either LIGHT or DARK" } +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" } -storymode = { optional = true, type = "bool", default = false, example = false, options = [true, - false, -], explanation = "not yet implemented" } +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" } +storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, not yet implemented" } + [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } -#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, -# false, -#], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } +background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video based on game name" } +#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } #background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" } [settings.tts] -choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], example = "streamlabspolly", explanation = "The backend used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } +voice_choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx",], example = "tiktok", explanation = "The voice platform used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" } tiktok_voice = { optional = false, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" } +python_voice = {optional = false, default = "1", example = "1", explanation = "The index of the system tts voices (can be downloaded externally, run ptt.py to find value, start from zero)"} +py_voice_num = {optional = false, default = "2", example = "2", explanation= "the number of system voices(2 are pre-installed in windows)"} diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py new file mode 100644 index 0000000..e46ebbc --- /dev/null +++ b/utils/CONSTANTS.py @@ -0,0 +1,45 @@ +# Supported Background. Can add/remove background video here.... +# - : key -> used as keyword for TOML file. value -> background configuration +# Format (value): +# 1. Youtube URI +# 2. filename +# 3. Citation (owner of the video) +# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) +background_options = { + "motor-gta": ( # Motor-GTA Racing + "https://www.youtube.com/watch?v=vw5L4xCPy9Q", + "bike-parkour-gta.mp4", + "Achy Gaming", + lambda t: ("center", 480 + t), + ), + "rocket-league": ( # Rocket League + "https://www.youtube.com/watch?v=2X9QGY__0II", + "rocket_league.mp4", + "Orbital Gameplay", + lambda t: ("center", 200 + t), + ), + "minecraft": ( # Minecraft parkour + "https://www.youtube.com/watch?v=n_Dv4JMiwK8", + "parkour.mp4", + "bbswitzer", + "center", + ), + "gta": ( # GTA Stunt Race + "https://www.youtube.com/watch?v=qGa9kWREOnE", + "gta-stunt-race.mp4", + "Achy Gaming", + lambda t: ("center", 480 + t), + ), + "csgo-surf": ( # CSGO Surf + "https://www.youtube.com/watch?v=E-8JlyO59Io", + "csgo-surf.mp4", + "Aki", + "center", + ), + "cluster-truck": ( # Cluster Truck Gameplay + "https://www.youtube.com/watch?v=uVKxtdMgJVU", + "cluster_truck.mp4", + "No Copyright Gameplay", + lambda t: ("center", 480 + t), + ), +} diff --git a/utils/cleanup.py b/utils/cleanup.py index ef4fc44..75074b7 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -2,6 +2,10 @@ import os from os.path import exists +def _listdir(d): # listdir with full path + return [os.path.join(d, f) for f in os.listdir(d)] + + def cleanup() -> int: """Deletes all temporary assets in assets/temp @@ -14,14 +18,12 @@ def cleanup() -> int: count += len(files) for f in files: os.remove(f) - try: - for file in os.listdir("./assets/temp/mp4"): + REMOVE_DIRS = ["./assets/temp/mp3/", "./assets/temp/png/"] + files_to_remove = list(map(_listdir, REMOVE_DIRS)) + for directory in files_to_remove: + for file in directory: count += 1 - os.remove("./assets/temp/mp4/" + file) - except FileNotFoundError: - pass - for file in os.listdir("./assets/temp/mp3"): - count += 1 - os.remove("./assets/temp/mp3/" + file) + os.remove(file) return count + return 0 diff --git a/utils/console.py b/utils/console.py index 6f99a41..1518c27 100644 --- a/utils/console.py +++ b/utils/console.py @@ -118,3 +118,4 @@ def handle_input( console.print( "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." ) + diff --git a/utils/settings.py b/utils/settings.py index a36f63e..a9d7726 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -167,4 +167,4 @@ If you see any prompts, that means that you have unset/incorrectly set variables if __name__ == "__main__": - check_toml(".config.template.toml", "config.toml") + check_toml("utils/.config.template.toml", "config.toml") diff --git a/utils/subreddit.py b/utils/subreddit.py index 4eb0108..c386868 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -5,7 +5,7 @@ from utils import settings from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0): """_summary_ Args: @@ -41,8 +41,24 @@ def get_subreddit_undone(submissions: list, subreddit): continue return submission print("all submissions have been done going by top submission order") + VALID_TIME_FILTERS = [ + "day", + "hour", + "month", + "week", + "year", + "all", + ] # set doesn't have __getitem__ + index = times_checked + 1 + if index == len(VALID_TIME_FILTERS): + print("all time filters have been checked you absolute madlad ") + return get_subreddit_undone( - subreddit.top(time_filter="hour"), subreddit + subreddit.top( + time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50) + ), + subreddit, + times_checked=index, ) # all the videos in hot have already been done diff --git a/utils/video.py b/utils/video.py new file mode 100644 index 0000000..63dc170 --- /dev/null +++ b/utils/video.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Tuple + +from PIL import ImageFont, Image, ImageDraw, ImageEnhance +from moviepy.video.VideoClip import VideoClip, ImageClip +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + + +class Video: + def __init__(self, video: VideoClip, *args, **kwargs): + self.video: VideoClip = video + self.fps = self.video.fps + self.duration = self.video.duration + + @staticmethod + def _create_watermark(text, fontsize, opacity=0.5): + path = "./assets/temp/png/watermark.png" + width = int(fontsize * len(text)) + height = int(fontsize * len(text) / 2) + white = (255, 255, 255) + transparent = (0, 0, 0, 0) + + font = ImageFont.load_default() + wm = Image.new("RGBA", (width, height), transparent) + im = Image.new("RGBA", (width, height), transparent) # Change this line too. + + draw = ImageDraw.Draw(wm) + w, h = draw.textsize(text, font) + draw.text(((width - w) / 2, (height - h) / 2), text, white, font) + en = ImageEnhance.Brightness(wm) # todo allow it to use the fontsize + mask = en.enhance(1 - opacity) + im.paste(wm, (25, 25), mask) + im.save(path) + return ImageClip(path) + + def add_watermark( + self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 + ): + compensation = round( + (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), + ndigits=2, + ) + position = (compensation, position[1]) + # print(f'{compensation=}') + # print(f'{position=}') + img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) + img_clip = img_clip.set_opacity(opacity).set_duration(duration) + img_clip = img_clip.set_position( + position, relative=True + ) # todo get dara from utils/CONSTANTS.py and adapt position accordingly + + # Overlay the img clip on the first video clip + self.video = CompositeVideoClip([self.video, img_clip]) + return self.video diff --git a/video_creation/background.py b/video_creation/background.py index be0f46c..6e656fa 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -10,42 +10,9 @@ from pytube import YouTube from pytube.cli import on_progress from utils import settings +from utils.CONSTANTS import background_options from utils.console import print_step, print_substep -# Supported Background. Can add/remove background video here.... -# - : key -> used as keyword for TOML file. value -> background configuration -# Format (value): -# 1. Youtube URI -# 2. filename -# 3. Citation (owner of the video) -# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) -background_options = { - "motor-gta": ( # Motor-GTA Racing - "https://www.youtube.com/watch?v=vw5L4xCPy9Q", - "bike-parkour-gta.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), - "rocket-league": ( # Rocket League - "https://www.youtube.com/watch?v=2X9QGY__0II", - "rocket_league.mp4", - "Orbital Gameplay", - lambda t: ("center", 200 + t), - ), - "minecraft": ( # Minecraft parkour - "https://www.youtube.com/watch?v=n_Dv4JMiwK8", - "parkour.mp4", - "bbswitzer", - "center", - ), - "gta": ( # GTA Stunt Race - "https://www.youtube.com/watch?v=qGa9kWREOnE", - "gta-stunt-race.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), -} - def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: """Generates a random interval of time to be used as the background of the video. @@ -92,7 +59,7 @@ def download_background(background_config: Tuple[str, str, str, Any]): YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( "assets/backgrounds", filename=f"{credit}-{filename}" ) - print_substep("Background videos downloaded successfully! 🎉", style="bold green") + print_substep("Background video downloaded successfully! 🎉", style="bold green") def chop_background_video(background_config: Tuple[str, str, str, Any], video_length: int): diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json index fe51488..0637a08 100644 --- a/video_creation/data/videos.json +++ b/video_creation/data/videos.json @@ -1 +1 @@ -[] +[] \ No newline at end of file diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8524051..d78863a 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -15,6 +15,7 @@ from rich.console import Console from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.video import Video from utils.videos import save_data from utils import settings @@ -29,6 +30,7 @@ def name_normalize(name: str) -> str: name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name) name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name) name = re.sub(r"\/", r"", name) + name[:30] lang = settings.config["reddit"]["thread"]["post_lang"] if lang: @@ -64,6 +66,7 @@ def make_final_video( VideoFileClip.reW = lambda clip: clip.resize(width=W) VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] + transition = settings.config["settings"]["transition"] background_clip = ( VideoFileClip("assets/temp/background.mp4") .without_audio() @@ -82,12 +85,15 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) + new_transition = 0 if transition is None or float(transition) > 2 else float(transition) image_clips.insert( 0, ImageClip("assets/temp/png/title.png") .set_duration(audio_clips[0].duration) .resize(width=W - 100) - .set_opacity(new_opacity), + .set_opacity(new_opacity) + .crossfadein(new_transition) + .crossfadeout(new_transition), ) for i in range(0, number_of_clips): @@ -96,6 +102,8 @@ def make_final_video( .set_duration(audio_clips[i + 1].duration) .resize(width=W - 100) .set_opacity(new_opacity) + .crossfadein(new_transition) + .crossfadeout(new_transition) ) # if os.path.exists("assets/mp3/posttext.mp3"): @@ -109,13 +117,15 @@ def make_final_video( # ) # else: story mode stuff img_clip_pos = background_config[3] - image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) + image_concat = concatenate_videoclips(image_clips).set_position( + img_clip_pos + ) # note transition kwarg for delay in imgs image_concat.audio = audio_composite final = CompositeVideoClip([background_clip, image_concat]) title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) - filename = f"{name_normalize(title)}.mp4" + filename = f"{name_normalize(title)[:251]}.mp4" subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): @@ -129,7 +139,9 @@ def make_final_video( # # lowered_audio = audio_background.multiply_volume( # todo get this to work # # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx # final.set_audio(final_audio) - + final = Video(final).add_watermark( + text=f"Background credit: {background_config[2]}", opacity=0.4 + ) final.write_videofile( "assets/temp/temp.mp4", fps=30, @@ -141,7 +153,7 @@ def make_final_video( ffmpeg_extract_subclip( "assets/temp/temp.mp4", 0, - final.duration, + length, targetname=f"results/{subreddit}/{filename}", ) save_data(subreddit, filename, title, idx, background_config[2]) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 6fb9ef4..51645e4 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -49,9 +49,12 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() - page.locator( - '[data-click-id="text"] button' - ).click() # Remove "Click to see nsfw" Button in Screenshot + page.wait_for_load_state() # Wait for page to fully load + + if page.locator('[data-click-id="text"] button').is_visible(): + page.locator( + '[data-click-id="text"] button' + ).click() # Remove "Click to see nsfw" Button in Screenshot # translate code @@ -99,9 +102,13 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in '([tl_content, tl_id]) => document.querySelector(`#t1_${tl_id} > div:nth-child(2) > div > div[data-testid="comment"] > div`).textContent = tl_content', [comment_tl, comment["comment_id"]], ) - - page.locator(f"#t1_{comment['comment_id']}").screenshot( - path=f"assets/temp/png/comment_{idx}.png" - ) - + try: + page.locator(f"#t1_{comment['comment_id']}").screenshot( + path=f"assets/temp/png/comment_{idx}.png" + ) + except TimeoutError: + del reddit_object["comments"] + screenshot_num += 1 + print("TimeoutError: Skipping screenshot...") + continue print_substep("Screenshots downloaded Successfully.", style="bold green") diff --git a/video_creation/voices.py b/video_creation/voices.py index ac33dd7..f49514d 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -9,6 +9,7 @@ from TTS.GTTS import GTTS from TTS.streamlabs_polly import StreamlabsPolly from TTS.aws_polly import AWSPolly from TTS.TikTok import TikTok +from TTS.pyttsx import pyttsx from utils import settings from utils.console import print_table, print_step @@ -20,6 +21,7 @@ TTSProviders = { "AWSPolly": AWSPolly, "StreamlabsPolly": StreamlabsPolly, "TikTok": TikTok, + "pyttsx": pyttsx, } @@ -33,7 +35,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: tuple[int,int]: (total length of the audio, the number of comments audio was generated for) """ - voice = settings.config["settings"]["tts"]["choice"] + 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) else: