Merge pull request #1093 from elebumm/develop

2.4
pull/1136/head 2.4
Callum Leslie 2 years ago committed by GitHub
commit 3eff3e4386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 #\<issue number\>") 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

@ -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()

@ -149,7 +149,7 @@
video += '<div class="d-flex justify-content-between align-items-center">';
video += '<div class="btn-group">';
video += '<a href="https://www.reddit.com/r/'+value.subreddit+'/comments/'+value.id+'/" class="btn btn-sm btn-outline-secondary" target="_blank">View</a>';
video += '<a href="results/'+value.subreddit+'/'+value.filename+'" class="btn btn-sm btn-outline-secondary" download>Download</a>';
video += '<a href="http://localhost:4000/results/'+value.subreddit+'/'+value.filename+'" class="btn btn-sm btn-outline-secondary" download>Download</a>';
video += '</div>';
video += '<div class="btn-group">';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="'+getCopyData(value.subreddit, value.reddit_title, value.filename, value.background_credit)+'"><i class="bi bi-card-text"></i></button>';

@ -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!

@ -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):

@ -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):

@ -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:

@ -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)

@ -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
}

@ -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

@ -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()

@ -9,3 +9,5 @@ requests==2.28.1
rich==12.5.1
toml==0.10.2
translators==5.3.1
Pillow~=9.1.1

@ -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)"}

@ -0,0 +1,45 @@
# Supported Background. Can add/remove background video here....
# <key>-<value> : 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),
),
}

@ -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

@ -118,3 +118,4 @@ def handle_input(
console.print(
"[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "."
)

@ -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")

@ -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

@ -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

@ -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>-<value> : 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):

@ -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])

@ -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")

@ -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:

Loading…
Cancel
Save