24 files changed
+353
-123
No files matched your search
+21
-1
@@ -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()
|
||||
+1
-1
@@ -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!
|
||||
|
||||
|
||||
+1
-1
@@ -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):
|
||||
|
||||
+1
-1
@@ -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):
|
||||
|
||||
+16
-12
@@ -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)
|
||||
+13
-10
@@ -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),
|
||||
),
|
||||
}
|
||||
+10
-8
@@ -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)) + "."
|
||||
)
|
||||
|
||||
+1
-1
@@ -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")
|
||||
+18
-2
@@ -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):
|
||||
|
||||
@@ -1 +1 @@
|
||||
[]
|
||||
[]
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user