diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4de6e2f..9059b5b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -45,7 +45,7 @@ body: Python version : [e.g. Python 3.6] - App version / Branch [e.g. latest, V2.0, master, develop] + App version / Branch : [e.g. latest, V2.0, master, develop] validations: required: true - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index aca6d9f..3390cfc 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: Ask a question about: Join our discord server to ask questions and discuss with maintainers and contributors. diff --git a/.gitignore b/.gitignore index 793db5d..3da725f 100644 --- a/.gitignore +++ b/.gitignore @@ -243,3 +243,4 @@ video_creation/data/videos.json video_creation/data/envvars.txt config.toml +video_creation/data/videos.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fc7d3d..3811f80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents ## I Have a Question -> If you want to ask a question, we assume that you have read the available [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/). +> If you want to ask a question, we assume that you have read the available [Documentation](https://reddit-video-maker-bot.netlify.app/). Before you ask a question, it is best to search for existing [Issues](https://github.com/elebumm/RedditVideoMakerBot/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. @@ -144,4 +144,4 @@ When making your PR, follow these guidelines: ### Improving The Documentation -All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/reddit-bot-docs) +All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/RedditVideoMakerBot-website) diff --git a/GUI.py b/GUI.py index bc41838..47dfc25 100644 --- a/GUI.py +++ b/GUI.py @@ -1,32 +1,118 @@ -# Import the server module -import http.server import webbrowser +from pathlib import Path + +# Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" +import tomlkit +from flask import ( + Flask, + redirect, + render_template, + request, + send_from_directory, + url_for, +) + +import utils.gui_utils as gui # 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() +# Configure application +app = Flask(__name__, template_folder="GUI") + +# Configure secret key only to use 'flash' +app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + + +# Ensure responses aren't cached +@app.after_request +def after_request(response): + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + response.headers["Expires"] = 0 + response.headers["Pragma"] = "no-cache" + return response + + +# Display index.html +@app.route("/") +def index(): + return render_template("index.html", file="videos.json") + + +@app.route("/backgrounds", methods=["GET"]) +def backgrounds(): + return render_template("backgrounds.html", file="backgrounds.json") + + +@app.route("/background/add", methods=["POST"]) +def background_add(): + # Get form values + youtube_uri = request.form.get("youtube_uri").strip() + filename = request.form.get("filename").strip() + citation = request.form.get("citation").strip() + position = request.form.get("position").strip() + + gui.add_background(youtube_uri, filename, citation, position) + + return redirect(url_for("backgrounds")) + + +@app.route("/background/delete", methods=["POST"]) +def background_delete(): + key = request.form.get("background-key") + gui.delete_background(key) + + return redirect(url_for("backgrounds")) + + +@app.route("/settings", methods=["GET", "POST"]) +def settings(): + config_load = tomlkit.loads(Path("config.toml").read_text()) + config = gui.get_config(config_load) + + # Get checks for all values + checks = gui.get_checks() + + if request.method == "POST": + # Get data from form as dict + data = request.form.to_dict() + + # Change settings + config = gui.modify_settings(data, config_load, checks) + + return render_template( + "settings.html", file="config.toml", data=config, checks=checks + ) + + +# Make videos.json accessible +@app.route("/videos.json") +def videos_json(): + return send_from_directory("video_creation/data", "videos.json") + + +# Make backgrounds.json accessible +@app.route("/backgrounds.json") +def backgrounds_json(): + return send_from_directory("utils", "backgrounds.json") + + +# Make videos in results folder accessible +@app.route("/results/") +def results(name): + return send_from_directory("results", name, as_attachment=True) + + +# Make voices samples in voices folder accessible +@app.route("/voices/") +def voices(name): + return send_from_directory("GUI/voices", name, as_attachment=True) + + +# Run browser and start the app +if __name__ == "__main__": + webbrowser.open(f"http://{HOST}:{PORT}", new=2) + print("Website opened in new tab. Refresh if it didn't load.") + app.run(port=PORT) diff --git a/GUI/backgrounds.html b/GUI/backgrounds.html new file mode 100644 index 0000000..541e39f --- /dev/null +++ b/GUI/backgrounds.html @@ -0,0 +1,263 @@ +{% extends "layout.html" %} +{% block main %} + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/GUI/index.html b/GUI/index.html index c771adc..fe64e36 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -1,229 +1,131 @@ - - - - - - RedditVideoMakerBot - - - +{% extends "layout.html" %} +{% block main %} - - - - - -
- -
- -
-
+
+
-
-
- +
+
+ +
-
-
+
-
+
-
-
- - +
+
- - - - - - - - - + var searchFilter = () => { + const input = document.querySelector(".searchFilter"); + const cards = document.getElementsByClassName("col"); + console.log(cards[1]) + let filter = input.value + for (let i = 0; i < cards.length; i++) { + let title = cards[i].querySelector(".card-text"); + if (title.innerText.toLowerCase().indexOf(filter.toLowerCase()) > -1) { + cards[i].classList.remove("d-none") + } else { + cards[i].classList.add("d-none") + } + } + } + +{% endblock %} \ No newline at end of file diff --git a/GUI/layout.html b/GUI/layout.html new file mode 100644 index 0000000..d56299e --- /dev/null +++ b/GUI/layout.html @@ -0,0 +1,142 @@ + + + + + + + RedditVideoMakerBot + + + + + + + + + + + + + + +
+ {% if get_flashed_messages() %} + {% for category, message in get_flashed_messages(with_categories=true) %} + + {% if category == "error" %} + + + {% else %} + + {% endif %} + {% endfor %} + {% endif %} + +
+ + {% block main %}{% endblock %} + + + + + \ No newline at end of file diff --git a/GUI/settings.html b/GUI/settings.html new file mode 100644 index 0000000..2b6b014 --- /dev/null +++ b/GUI/settings.html @@ -0,0 +1,576 @@ +{% extends "layout.html" %} +{% block main %} + +
+
+
+
+ + +

Reddit Credentials

+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ + +

Reddit Thread

+
+ +
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ Max number of characters a comment can have. +
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ The minimum number of comments a post should have to be + included. +
+
+ + +

General Settings

+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+ Used if you want to create multiple videos. +
+
+
+ +
+
+ +
+ Sets the opacity of the comments when overlayed over the + background. +
+
+
+ +
+
+ +
+ Sets the transition time (in seconds) between the + comments. Set to 0 if you want to disable it. +
+
+
+ +
+ + See all available + backgrounds +
+
+ + +

TTS Settings

+
+ +
+ +
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ Time in seconds between TTS comments. +
+
+
+
+ + +
+
+
+ +
+ + + +{% endblock %} \ No newline at end of file diff --git a/GUI/voices/amy.mp3 b/GUI/voices/amy.mp3 new file mode 100644 index 0000000..29ea64b Binary files /dev/null and b/GUI/voices/amy.mp3 differ diff --git a/GUI/voices/br_001.mp3 b/GUI/voices/br_001.mp3 new file mode 100644 index 0000000..3b343dc Binary files /dev/null and b/GUI/voices/br_001.mp3 differ diff --git a/GUI/voices/br_003.mp3 b/GUI/voices/br_003.mp3 new file mode 100644 index 0000000..9a6b845 Binary files /dev/null and b/GUI/voices/br_003.mp3 differ diff --git a/GUI/voices/br_004.mp3 b/GUI/voices/br_004.mp3 new file mode 100644 index 0000000..47d0010 Binary files /dev/null and b/GUI/voices/br_004.mp3 differ diff --git a/GUI/voices/br_005.mp3 b/GUI/voices/br_005.mp3 new file mode 100644 index 0000000..104cc3a Binary files /dev/null and b/GUI/voices/br_005.mp3 differ diff --git a/GUI/voices/brian.mp3 b/GUI/voices/brian.mp3 new file mode 100644 index 0000000..9304703 Binary files /dev/null and b/GUI/voices/brian.mp3 differ diff --git a/GUI/voices/de_001.mp3 b/GUI/voices/de_001.mp3 new file mode 100644 index 0000000..ef4a1b5 Binary files /dev/null and b/GUI/voices/de_001.mp3 differ diff --git a/GUI/voices/de_002.mp3 b/GUI/voices/de_002.mp3 new file mode 100644 index 0000000..4d7f89f Binary files /dev/null and b/GUI/voices/de_002.mp3 differ diff --git a/GUI/voices/emma.mp3 b/GUI/voices/emma.mp3 new file mode 100644 index 0000000..07895f0 Binary files /dev/null and b/GUI/voices/emma.mp3 differ diff --git a/GUI/voices/en_au_001.mp3 b/GUI/voices/en_au_001.mp3 new file mode 100644 index 0000000..b940e5c Binary files /dev/null and b/GUI/voices/en_au_001.mp3 differ diff --git a/GUI/voices/en_au_002.mp3 b/GUI/voices/en_au_002.mp3 new file mode 100644 index 0000000..a6421bb Binary files /dev/null and b/GUI/voices/en_au_002.mp3 differ diff --git a/GUI/voices/en_uk_001.mp3 b/GUI/voices/en_uk_001.mp3 new file mode 100644 index 0000000..666cac6 Binary files /dev/null and b/GUI/voices/en_uk_001.mp3 differ diff --git a/GUI/voices/en_uk_003.mp3 b/GUI/voices/en_uk_003.mp3 new file mode 100644 index 0000000..eecf6f6 Binary files /dev/null and b/GUI/voices/en_uk_003.mp3 differ diff --git a/GUI/voices/en_us_001.mp3 b/GUI/voices/en_us_001.mp3 new file mode 100644 index 0000000..56f1e01 Binary files /dev/null and b/GUI/voices/en_us_001.mp3 differ diff --git a/GUI/voices/en_us_002.mp3 b/GUI/voices/en_us_002.mp3 new file mode 100644 index 0000000..56f1e01 Binary files /dev/null and b/GUI/voices/en_us_002.mp3 differ diff --git a/GUI/voices/en_us_006.mp3 b/GUI/voices/en_us_006.mp3 new file mode 100644 index 0000000..f980665 Binary files /dev/null and b/GUI/voices/en_us_006.mp3 differ diff --git a/GUI/voices/en_us_007.mp3 b/GUI/voices/en_us_007.mp3 new file mode 100644 index 0000000..446a911 Binary files /dev/null and b/GUI/voices/en_us_007.mp3 differ diff --git a/GUI/voices/en_us_009.mp3 b/GUI/voices/en_us_009.mp3 new file mode 100644 index 0000000..2dd624d Binary files /dev/null and b/GUI/voices/en_us_009.mp3 differ diff --git a/GUI/voices/en_us_010.mp3 b/GUI/voices/en_us_010.mp3 new file mode 100644 index 0000000..34a37fe Binary files /dev/null and b/GUI/voices/en_us_010.mp3 differ diff --git a/GUI/voices/en_us_c3po.mp3 b/GUI/voices/en_us_c3po.mp3 new file mode 100644 index 0000000..eeabfac Binary files /dev/null and b/GUI/voices/en_us_c3po.mp3 differ diff --git a/GUI/voices/en_us_chewbacca.mp3 b/GUI/voices/en_us_chewbacca.mp3 new file mode 100644 index 0000000..6257d10 Binary files /dev/null and b/GUI/voices/en_us_chewbacca.mp3 differ diff --git a/GUI/voices/en_us_ghostface.mp3 b/GUI/voices/en_us_ghostface.mp3 new file mode 100644 index 0000000..0ad75be Binary files /dev/null and b/GUI/voices/en_us_ghostface.mp3 differ diff --git a/GUI/voices/en_us_rocket.mp3 b/GUI/voices/en_us_rocket.mp3 new file mode 100644 index 0000000..66a0356 Binary files /dev/null and b/GUI/voices/en_us_rocket.mp3 differ diff --git a/GUI/voices/en_us_stitch.mp3 b/GUI/voices/en_us_stitch.mp3 new file mode 100644 index 0000000..3c2fcc0 Binary files /dev/null and b/GUI/voices/en_us_stitch.mp3 differ diff --git a/GUI/voices/en_us_stormtrooper.mp3 b/GUI/voices/en_us_stormtrooper.mp3 new file mode 100644 index 0000000..8773ee4 Binary files /dev/null and b/GUI/voices/en_us_stormtrooper.mp3 differ diff --git a/GUI/voices/es_002.mp3 b/GUI/voices/es_002.mp3 new file mode 100644 index 0000000..4fb1d01 Binary files /dev/null and b/GUI/voices/es_002.mp3 differ diff --git a/GUI/voices/es_mx_002.mp3 b/GUI/voices/es_mx_002.mp3 new file mode 100644 index 0000000..18d0dfe Binary files /dev/null and b/GUI/voices/es_mx_002.mp3 differ diff --git a/GUI/voices/fr_001.mp3 b/GUI/voices/fr_001.mp3 new file mode 100644 index 0000000..deec4c8 Binary files /dev/null and b/GUI/voices/fr_001.mp3 differ diff --git a/GUI/voices/fr_002.mp3 b/GUI/voices/fr_002.mp3 new file mode 100644 index 0000000..c2b7512 Binary files /dev/null and b/GUI/voices/fr_002.mp3 differ diff --git a/GUI/voices/geraint.mp3 b/GUI/voices/geraint.mp3 new file mode 100644 index 0000000..403e0b6 Binary files /dev/null and b/GUI/voices/geraint.mp3 differ diff --git a/GUI/voices/id_001.mp3 b/GUI/voices/id_001.mp3 new file mode 100644 index 0000000..19febf9 Binary files /dev/null and b/GUI/voices/id_001.mp3 differ diff --git a/GUI/voices/ivy.mp3 b/GUI/voices/ivy.mp3 new file mode 100644 index 0000000..0d649c3 Binary files /dev/null and b/GUI/voices/ivy.mp3 differ diff --git a/GUI/voices/joanna.mp3 b/GUI/voices/joanna.mp3 new file mode 100644 index 0000000..f53b393 Binary files /dev/null and b/GUI/voices/joanna.mp3 differ diff --git a/GUI/voices/joey.mp3 b/GUI/voices/joey.mp3 new file mode 100644 index 0000000..d83c9d2 Binary files /dev/null and b/GUI/voices/joey.mp3 differ diff --git a/GUI/voices/jp_001.mp3 b/GUI/voices/jp_001.mp3 new file mode 100644 index 0000000..b6505cd Binary files /dev/null and b/GUI/voices/jp_001.mp3 differ diff --git a/GUI/voices/jp_003.mp3 b/GUI/voices/jp_003.mp3 new file mode 100644 index 0000000..1ef5782 Binary files /dev/null and b/GUI/voices/jp_003.mp3 differ diff --git a/GUI/voices/jp_005.mp3 b/GUI/voices/jp_005.mp3 new file mode 100644 index 0000000..5d3d5bf Binary files /dev/null and b/GUI/voices/jp_005.mp3 differ diff --git a/GUI/voices/jp_006.mp3 b/GUI/voices/jp_006.mp3 new file mode 100644 index 0000000..3e493fb Binary files /dev/null and b/GUI/voices/jp_006.mp3 differ diff --git a/GUI/voices/justin.mp3 b/GUI/voices/justin.mp3 new file mode 100644 index 0000000..1275f14 Binary files /dev/null and b/GUI/voices/justin.mp3 differ diff --git a/GUI/voices/kendra.mp3 b/GUI/voices/kendra.mp3 new file mode 100644 index 0000000..2bb35a8 Binary files /dev/null and b/GUI/voices/kendra.mp3 differ diff --git a/GUI/voices/kimberly.mp3 b/GUI/voices/kimberly.mp3 new file mode 100644 index 0000000..e51652c Binary files /dev/null and b/GUI/voices/kimberly.mp3 differ diff --git a/GUI/voices/kr_002.mp3 b/GUI/voices/kr_002.mp3 new file mode 100644 index 0000000..e9ef657 Binary files /dev/null and b/GUI/voices/kr_002.mp3 differ diff --git a/GUI/voices/kr_003.mp3 b/GUI/voices/kr_003.mp3 new file mode 100644 index 0000000..8411cd2 Binary files /dev/null and b/GUI/voices/kr_003.mp3 differ diff --git a/GUI/voices/kr_004.mp3 b/GUI/voices/kr_004.mp3 new file mode 100644 index 0000000..182b407 Binary files /dev/null and b/GUI/voices/kr_004.mp3 differ diff --git a/GUI/voices/matthew.mp3 b/GUI/voices/matthew.mp3 new file mode 100644 index 0000000..cce7967 Binary files /dev/null and b/GUI/voices/matthew.mp3 differ diff --git a/GUI/voices/nicole.mp3 b/GUI/voices/nicole.mp3 new file mode 100644 index 0000000..5e972e2 Binary files /dev/null and b/GUI/voices/nicole.mp3 differ diff --git a/GUI/voices/raveena.mp3 b/GUI/voices/raveena.mp3 new file mode 100644 index 0000000..558403f Binary files /dev/null and b/GUI/voices/raveena.mp3 differ diff --git a/GUI/voices/russell.mp3 b/GUI/voices/russell.mp3 new file mode 100644 index 0000000..b8d8e7a Binary files /dev/null and b/GUI/voices/russell.mp3 differ diff --git a/GUI/voices/salli.mp3 b/GUI/voices/salli.mp3 new file mode 100644 index 0000000..467f98d Binary files /dev/null and b/GUI/voices/salli.mp3 differ diff --git a/README.md b/README.md index 5d2bdc2..9095588 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,11 @@ The only original thing being done is the editing and gathering of all materials 1. Clone this repository 2. Run `pip install -r requirements.txt` - 3. Run `python -m playwright install` and `python -m playwright install-deps` **EXPERIMENTAL!!!!** -On MacOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash) +On macOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash) `bash <(curl -sL https://raw.githubusercontent.com/elebumm/RedditVideoMakerBot/master/install.sh)` @@ -58,7 +57,7 @@ This can also be used to update the installation (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/) +If you want to read more detailed guide about the bot, please refer to the [documentation](https://reddit-video-maker-bot.netlify.app/) ## Video diff --git a/TTS/GTTS.py b/TTS/GTTS.py index cef1b24..3bf8ee3 100644 --- a/TTS/GTTS.py +++ b/TTS/GTTS.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 import random -from utils import settings + from gtts import gTTS -max_chars = 0 +from utils import settings class GTTS: diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 743118c..7f79c81 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -1,9 +1,11 @@ import base64 -from utils import settings import random + import requests from requests.adapters import HTTPAdapter, Retry +from utils import settings + # from profanity_filter import ProfanityFilter # pf = ProfanityFilter() # Code by @JasonLovesDoggo @@ -62,9 +64,7 @@ noneng = [ class TikTok: # TikTok Text-to-Speech Wrapper def __init__(self): - self.URI_BASE = ( - "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=" - ) + self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=" self.max_chars = 300 self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng} @@ -75,10 +75,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper voice = ( self.randomvoice() if random_voice - else ( - settings.config["settings"]["tts"]["tiktok_voice"] - or random.choice(self.voices["human"]) - ) + else (settings.config["settings"]["tts"]["tiktok_voice"] or random.choice(self.voices["human"])) ) try: r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index eac5884..fa02079 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 +import random +import sys + from boto3 import Session from botocore.exceptions import BotoCoreError, ClientError, ProfileNotFound -import sys + from utils import settings -import random voices = [ "Brian", @@ -37,15 +39,11 @@ class AWSPolly: voice = self.randomvoice() else: if not settings.config["settings"]["tts"]["aws_polly_voice"]: - raise ValueError( - f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}" - ) + raise ValueError(f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}") voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: # Request speech synthesis - response = polly.synthesize_speech( - Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural" - ) + response = polly.synthesize_speech(Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural") except (BotoCoreError, ClientError) as error: # The service returned an error, exit gracefully print(error) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 12668df..1324118 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 +import os +import re from pathlib import Path from typing import Tuple -import re # import sox # from mutagen import MutagenError # from mutagen.mp3 import MP3, HeaderNotFoundError +import numpy as np import translators as ts +from moviepy.audio.AudioClip import AudioClip +from moviepy.audio.fx.volumex import volumex +from moviepy.editor import AudioFileClip from rich.progress import track -from moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips + +from utils import settings from utils.console import print_step, print_substep from utils.voice import sanitize_text -from utils import settings DEFAULT_MAX_LENGTH: int = 50 # video length variable @@ -21,7 +26,7 @@ class TTSEngine: """Calls the given TTS engine to reduce code duplication and allow multiple TTS engines. Args: - tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method. + tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method. reddit_object : The reddit object that contains the posts to read. path (Optional) : The unix style path to save the mp3 files to. This must not have leading or trailing slashes. max_length (Optional) : The maximum length of the mp3 files in total. @@ -70,9 +75,7 @@ class TTSEngine: 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 + 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}", process_text(comment["comment_body"])) @@ -83,50 +86,71 @@ class TTSEngine: def split_post(self, text: str, idx: int): split_files = [] split_text = [ - x.group().strip() - for x in re.finditer( - r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text - ) + x.group().strip() for x in re.finditer(r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text) ] - offset = 0 - for idy, text_cut in enumerate(split_text): - # print(f"{idx}-{idy}: {text_cut}\n") - 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", new_text) - split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3")) - - CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile( - f"{self.path}/{idx}.mp3", fps=44100, verbose=False, logger=None - ) + self.create_silence_mp3() - for i in split_files: - name = i.filename - i.close() - Path(name).unlink() - - # for i in range(0, idy + 1): - # print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3") + idy = None + for idy, text_cut in enumerate(split_text): + newtext = process_text(text_cut) + # print(f"{idx}-{idy}: {newtext}\n") - # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() + if not newtext or newtext.isspace(): + print("newtext was blank because sanitized split text resulted in none") + continue + else: + self.call_tts(f"{idx}-{idy}.part", newtext) + with open(f"{self.path}/list.txt", 'w') as f: + for idz in range(0, len(split_text)): + f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") + split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3")) + f.write("file " + f"'silence.mp3'" + "\n") + + os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " + + "-i " + f"{self.path}/list.txt " + + "-c copy " + f"{self.path}/{idx}.mp3") + try: + for i in range(0, len(split_files)): + os.unlink(split_files[i]) + except FileNotFoundError as e: + print("File not found: " + e.filename) + except OSError: + print("OSError") def call_tts(self, filename: str, text: str): - 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: + self.tts_module.run(text, filepath=f"{self.path}/{filename}_no_silence.mp3") + self.create_silence_mp3() + + with open(f"{self.path}/{filename}.txt", 'w') as f: + f.write("file " + f"'{filename}_no_silence.mp3'" + "\n") + f.write("file " + f"'silence.mp3'" + "\n") + f.close() + os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " + + "-i " + f"{self.path}/{filename}.txt " + + "-c copy " + f"{self.path}/{filename}.mp3") clip = AudioFileClip(f"{self.path}/{filename}.mp3") - self.last_clip_length = clip.duration self.length += clip.duration + self.last_clip_length = clip.duration clip.close() + try: + name = [f"{filename}_no_silence.mp3", "silence.mp3", f"{filename}.txt"] + for i in range(0, len(name)): + os.unlink(str(rf"{self.path}/" + name[i])) + except FileNotFoundError as e: + print("File not found: " + e.filename) + except OSError: + print("OSError") except: self.length = 0 + def create_silence_mp3(self): + silence_duration = settings.config["settings"]["tts"]["silence_duration"] + silence = AudioClip(make_frame=lambda t: np.sin(440 * 2 * np.pi * t), duration=silence_duration, fps=44100) + silence = volumex(silence, 0) + silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None) + def process_text(text: str): lang = settings.config["reddit"]["thread"]["post_lang"] diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index b5ffb29..874d573 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -1,5 +1,7 @@ import random + import pyttsx3 + from utils import settings @@ -30,9 +32,7 @@ class pyttsx: 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.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() diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 75c4f49..ce2250b 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -1,6 +1,8 @@ import random + import requests from requests.exceptions import JSONDecodeError + from utils import settings from utils.voice import check_ratelimit @@ -37,9 +39,7 @@ class StreamlabsPolly: voice = self.randomvoice() else: if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]: - raise ValueError( - f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}" - ) + raise ValueError(f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}") voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} response = requests.post(self.url, data=body) diff --git a/install.sh b/install.sh index f85c80b..38c1708 100644 --- a/install.sh +++ b/install.sh @@ -62,7 +62,13 @@ function install_macos(){ fi # Install the required packages echo "Installing required Packages" - brew install python@3.10 tcl-tk python-tk + if [! command --version python3 &> /dev/null ]; then + echo "Installing python3" + brew install python@3.10 + else + echo "python3 already installed." + fi + brew install tcl-tk python-tk } # Function to install for arch (and other forks like manjaro) diff --git a/main.py b/main.py index 8a0755d..02964fc 100755 --- a/main.py +++ b/main.py @@ -1,18 +1,18 @@ #!/usr/bin/env python import math -import re -from subprocess import Popen +import sys from os import name +from subprocess import Popen +from pathlib import Path from prawcore import ResponseException from reddit.subreddit import get_subreddit_threads -from utils.cleanup import cleanup -from utils.console import print_markdown, print_step, print_substep from utils import settings +from utils.cleanup import cleanup +from utils.console import print_markdown, print_step from utils.id import id from utils.version import checkversion - from video_creation.background import ( download_background, chop_background_video, @@ -22,7 +22,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.4.1" +__VERSION__ = "2.4.2" print( """ @@ -36,7 +36,7 @@ print( ) # Modified by JasonLovesDoggo print_markdown( - "### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)" + "### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. You can find solutions to many common problems in the [Documentation](): https://reddit-video-maker-bot.netlify.app/" ) checkversion(__VERSION__) @@ -75,14 +75,14 @@ def shutdown(): print("Exiting...") exit() + if __name__ == "__main__": - config = settings.check_toml("utils/.config.template.toml", "config.toml") + assert sys.version_info >= (3, 9), "Python 3.10 or higher is required" + directory = Path().absolute() + config = settings.check_toml(f"{directory}/utils/.config.template.toml", "config.toml") config is False and exit() try: - if config["settings"]["times_to_run"]: - run_many(config["settings"]["times_to_run"]) - - elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1: + if len(config["reddit"]["thread"]["post_id"].split("+")) > 1: for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( @@ -90,6 +90,8 @@ if __name__ == "__main__": ) main(post_id) Popen("cls" if name == "nt" else "clear", shell=True).wait() + elif config["settings"]["times_to_run"]: + run_many(config["settings"]["times_to_run"]) else: main() except KeyboardInterrupt: diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 11b93af..5114d9a 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -1,11 +1,10 @@ import re -from prawcore.exceptions import ResponseException - -from utils import settings import praw from praw.models import MoreComments +from prawcore.exceptions import ResponseException +from utils import settings from utils.console import print_step, print_substep from utils.subreddit import get_subreddit_undone from utils.videos import check_done @@ -41,9 +40,8 @@ def get_subreddit_threads(POST_ID: str): check_for_async=False, ) except ResponseException as e: - match e.response.status_code: - case 401: - print("Invalid credentials - please check them in config.toml") + if e.response.status_code == 401: + print("Invalid credentials - please check them in config.toml") except: print("Something went wrong...") @@ -105,12 +103,9 @@ def get_subreddit_threads(POST_ID: str): sanitised = sanitize_text(top_level_comment.body) if not sanitised or sanitised == " ": continue - if len(top_level_comment.body) <= int( - settings.config["reddit"]["thread"]["max_comment_length"] - ): + if len(top_level_comment.body) <= int(settings.config["reddit"]["thread"]["max_comment_length"]): if ( - top_level_comment.author is not None - and sanitize_text(top_level_comment.body) is not None + top_level_comment.author is not None and sanitize_text(top_level_comment.body) is not None ): # if errors occur with this change to if not. content["comments"].append( { diff --git a/requirements.txt b/requirements.txt index f5b0e22..a2ccadf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ toml==0.10.2 translators==5.3.1 pyttsx3==2.90 Pillow~=9.1.1 +tomlkit==0.11.4 +Flask==2.2.2 diff --git a/utils/.config.template.toml b/utils/.config.template.toml index adbaed0..50cf413 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -3,11 +3,11 @@ client_id = { optional = false, nmin = 12, nmax = 30, explanation = "The ID of y 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 } +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" } +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. You can have multiple subreddits, add an + with no spaces.", example = "AskReddit+Redditdev", 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" } @@ -16,24 +16,25 @@ min_comments = { default = 20, optional = false, nmin = 15, type = "int", explan [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" } +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" } 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" } +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 = "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_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] -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." } +voice_choice = { optional = false, default = "tiktok", 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)"} +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)" } +silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" } diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py deleted file mode 100644 index e46ebbc..0000000 --- a/utils/CONSTANTS.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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/backgrounds.json b/utils/backgrounds.json new file mode 100644 index 0000000..af800c5 --- /dev/null +++ b/utils/backgrounds.json @@ -0,0 +1,39 @@ +{ + "__comment": "Supported Backgrounds. Can add/remove background video here...", + "motor-gta": [ + "https://www.youtube.com/watch?v=vw5L4xCPy9Q", + "bike-parkour-gta.mp4", + "Achy Gaming", + 480 + ], + "rocket-league": [ + "https://www.youtube.com/watch?v=2X9QGY__0II", + "rocket_league.mp4", + "Orbital Gameplay", + 200 + ], + "minecraft": [ + "https://www.youtube.com/watch?v=n_Dv4JMiwK8", + "parkour.mp4", + "bbswitzer", + "center" + ], + "gta": [ + "https://www.youtube.com/watch?v=qGa9kWREOnE", + "gta-stunt-race.mp4", + "Achy Gaming", + 480 + ], + "csgo-surf": [ + "https://www.youtube.com/watch?v=E-8JlyO59Io", + "csgo-surf.mp4", + "Aki", + "center" + ], + "cluster-truck": [ + "https://www.youtube.com/watch?v=uVKxtdMgJVU", + "cluster_truck.mp4", + "No Copyright Gameplay", + 480 + ] +} \ No newline at end of file diff --git a/utils/console.py b/utils/console.py index 6f99a41..ce1b8a4 100644 --- a/utils/console.py +++ b/utils/console.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 +import re + +from rich.columns import Columns from rich.console import Console from rich.markdown import Markdown from rich.padding import Padding from rich.panel import Panel from rich.text import Text -from rich.columns import Columns -import re console = Console() @@ -54,11 +55,7 @@ def handle_input( return default if default is not NotImplemented else "" if default is not NotImplemented: console.print( - "[green]" - + message - + '\n[blue bold]The default value is "' - + str(default) - + '"\nDo you want to use it?(y/n)' + "[green]" + message + '\n[blue bold]The default value is "' + str(default) + '"\nDo you want to use it?(y/n)' ) if input().casefold().startswith("y"): return default @@ -71,9 +68,7 @@ def handle_input( if check_type is not False: try: user_input = check_type(user_input) - if (nmin is not None and user_input < nmin) or ( - nmax is not None and user_input > nmax - ): + if (nmin is not None and user_input < nmin) or (nmax is not None and user_input > nmax): # FAILSTATE Input out of bounds console.print("[red]" + oob_error) continue @@ -89,9 +84,7 @@ def handle_input( continue else: # FAILSTATE Input STRING out of bounds - if (nmin is not None and len(user_input) < nmin) or ( - nmax is not None and len(user_input) > nmax - ): + if (nmin is not None and len(user_input) < nmin) or (nmax is not None and len(user_input) > nmax): console.print("[red bold]" + oob_error) continue break # SUCCESS Input STRING in bounds @@ -105,16 +98,8 @@ def handle_input( isinstance(eval(user_input), check_type) return check_type(user_input) except: - console.print( - "[red bold]" - + err_message - + "\nValid options are: " - + ", ".join(map(str, options)) - + "." - ) + console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".") continue if user_input in options: return user_input - console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." - ) + console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".") diff --git a/utils/gui_utils.py b/utils/gui_utils.py new file mode 100644 index 0000000..b539ff7 --- /dev/null +++ b/utils/gui_utils.py @@ -0,0 +1,226 @@ +import json +import re +from pathlib import Path + +import toml +import tomlkit +from flask import flash + + +# Get validation checks from template +def get_checks(): + template = toml.load("utils/.config.template.toml") + checks = {} + + def unpack_checks(obj: dict): + for key in obj.keys(): + if "optional" in obj[key].keys(): + checks[key] = obj[key] + else: + unpack_checks(obj[key]) + + unpack_checks(template) + + return checks + + +# Get current config (from config.toml) as dict +def get_config(obj: dict, done={}): + for key in obj.keys(): + if not isinstance(obj[key], dict): + done[key] = obj[key] + else: + get_config(obj[key], done) + + return done + + +# Checks if value is valid +def check(value, checks): + incorrect = False + + if value == "False": + value = "" + + if not incorrect and "type" in checks: + try: + value = eval(checks["type"])(value) + except Exception: + incorrect = True + + if ( + not incorrect and "options" in checks and value not in checks["options"] + ): # FAILSTATE Value is not one of the options + incorrect = True + if ( + not incorrect + and "regex" in checks + and ( + (isinstance(value, str) and re.match(checks["regex"], value) is None) + or not isinstance(value, str) + ) + ): # FAILSTATE Value doesn't match regex, or has regex but is not a string. + incorrect = True + + if ( + not incorrect + and not hasattr(value, "__iter__") + and ( + ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) + ) + ): + incorrect = True + + if ( + not incorrect + and hasattr(value, "__iter__") + and ( + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) + ) + ): + incorrect = True + + if incorrect: + return "Error" + + return value + + +# Modify settings (after form is submitted) +def modify_settings(data: dict, config_load, checks: dict): + # Modify config settings + def modify_config(obj: dict, name: str, value: any): + for key in obj.keys(): + if name == key: + obj[key] = value + elif not isinstance(obj[key], dict): + continue + else: + modify_config(obj[key], name, value) + + # Remove empty/incorrect key-value pairs + data = {key: value for key, value in data.items() if value and key in checks.keys()} + + # Validate values + for name in data.keys(): + value = check(data[name], checks[name]) + + # Value is invalid + if value == "Error": + flash("Some values were incorrect and didn't save!", "error") + else: + # Value is valid + modify_config(config_load, name, value) + + # Save changes in config.toml + with Path("config.toml").open("w") as toml_file: + toml_file.write(tomlkit.dumps(config_load)) + + flash("Settings saved!") + + return get_config(config_load) + + +# Delete background video +def delete_background(key): + # Read backgrounds.json + with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: + data = json.load(backgrounds) + + # Remove background from backgrounds.json + with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds: + if data.pop(key, None): + json.dump(data, backgrounds, ensure_ascii=False, indent=4) + else: + flash("Couldn't find this background. Try refreshing the page.", "error") + return + + # Remove background video from ".config.template.toml" + config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) + config["settings"]["background"]["background_choice"]["options"].remove(key) + + with Path("utils/.config.template.toml").open("w") as toml_file: + toml_file.write(tomlkit.dumps(config)) + + flash(f'Successfully removed "{key}" background!') + + +# Add background video +def add_background(youtube_uri, filename, citation, position): + # Validate YouTube URI + regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)").search( + youtube_uri + ) + + if not regex: + flash("YouTube URI is invalid!", "error") + return + + youtube_uri = f"https://www.youtube.com/watch?v={regex.group(1)}" + + # Check if position is valid + if position == "" or position == "center": + position = "center" + + elif position.isdecimal(): + position = int(position) + + else: + flash('Position is invalid! It can be "center" or decimal number.', "error") + return + + # Sanitize filename + regex = re.compile(r"^([a-zA-Z0-9\s_-]{1,100})$").match(filename) + + if not regex: + flash("Filename is invalid!", "error") + return + + filename = filename.replace(" ", "_") + + # Check if background doesn't already exist + with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: + data = json.load(backgrounds) + + # Check if key isn't already taken + if filename in list(data.keys()): + flash("Background video with this name already exist!", "error") + return + + # Check if the YouTube URI isn't already used under different name + if youtube_uri in [data[i][0] for i in list(data.keys())]: + flash("Background video with this YouTube URI is already added!", "error") + return + + # Add background video to json file + with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds: + data = json.load(backgrounds) + + data[filename] = [youtube_uri, filename + ".mp4", citation, position] + backgrounds.seek(0) + json.dump(data, backgrounds, ensure_ascii=False, indent=4) + + # Add background video to ".config.template.toml" + config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) + config["settings"]["background"]["background_choice"]["options"].append(filename) + + with Path("utils/.config.template.toml").open("w") as toml_file: + toml_file.write(tomlkit.dumps(config)) + + flash(f'Added "{citation}-{filename}.mp4" as a new background video!') + + return diff --git a/utils/id.py b/utils/id.py index c66d77a..3d76593 100644 --- a/utils/id.py +++ b/utils/id.py @@ -1,10 +1,12 @@ import re + from utils.console import print_substep + def id(reddit_obj: dict): """ This function takes a reddit object and returns the post id """ id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) print_substep(f"Thread ID is {id}", style="bold blue") - return id \ No newline at end of file + return id diff --git a/utils/settings.py b/utils/settings.py index a9d7726..d2974d5 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,13 +1,12 @@ #!/usr/bin/env python -import toml -from rich.console import Console import re - from typing import Tuple, Dict +from pathlib import Path +import toml +from rich.console import Console from utils.console import handle_input - console = Console() config = dict # autocomplete @@ -35,17 +34,12 @@ def check(value, checks, name): except: incorrect = True - if ( - not incorrect and "options" in checks and value not in checks["options"] - ): # FAILSTATE Value is not one of the options + if not incorrect and "options" in checks and value not in checks["options"]: # FAILSTATE Value is not one of the options incorrect = True if ( not incorrect and "regex" in checks - and ( - (isinstance(value, str) and re.match(checks["regex"], value) is None) - or not isinstance(value, str) - ) + and ((isinstance(value, str) and re.match(checks["regex"], value) is None) or not isinstance(value, str)) ): # FAILSTATE Value doesn't match regex, or has regex but is not a string. incorrect = True @@ -85,9 +79,7 @@ def check(value, checks, name): err_message=get_check_value("input_error", "Incorrect input"), nmin=get_check_value("nmin", None), nmax=get_check_value("nmax", None), - oob_error=get_check_value( - "oob_error", "Input out of bounds(Value too high/low/long/short)" - ), + oob_error=get_check_value("oob_error", "Input out of bounds(Value too high/low/long/short)"), options=get_check_value("options", None), optional=get_check_value("optional", False), ) @@ -167,4 +159,5 @@ If you see any prompts, that means that you have unset/incorrectly set variables if __name__ == "__main__": - check_toml("utils/.config.template.toml", "config.toml") + directory = Path().absolute() + check_toml(f"{directory}/utils/.config.template.toml", "config.toml") diff --git a/utils/subreddit.py b/utils/subreddit.py index c386868..b0b7ae5 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -54,9 +54,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): print("all time filters have been checked you absolute madlad ") return get_subreddit_undone( - subreddit.top( - time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50) - ), + 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/version.py b/utils/version.py index a8177cc..8cad1d8 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1,11 +1,10 @@ import requests + from utils.console import print_step def checkversion(__VERSION__): - response = requests.get( - "https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest" - ) + response = requests.get("https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest") latestversion = response.json()["tag_name"] if __VERSION__ == latestversion: print_step(f"You are using the newest version ({__VERSION__}) of the bot") diff --git a/utils/video.py b/utils/video.py index 5566cd1..2d212df 100644 --- a/utils/video.py +++ b/utils/video.py @@ -1,7 +1,6 @@ from __future__ import annotations -from ast import Str -import re +import re from typing import Tuple from PIL import ImageFont, Image, ImageDraw, ImageEnhance @@ -37,9 +36,7 @@ class Video: im.save(path) return ImageClip(path) - def add_watermark( - self, text, redditid, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 - ): + def add_watermark(self, text, redditid, 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, diff --git a/utils/videos.py b/utils/videos.py index 4a91e8c..7c756fc 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -1,6 +1,5 @@ import json import time -from typing import Dict from praw.models import Submission diff --git a/utils/voice.py b/utils/voice.py index a0709fa..0ff6b37 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -1,7 +1,7 @@ import re import sys -from datetime import datetime import time as pytime +from datetime import datetime from time import sleep from requests import Response @@ -81,7 +81,7 @@ def sanitize_text(text: str) -> str: result = re.sub(regex_urls, " ", text) # note: not removing apostrophes - regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-–—%“”‘\"%\*/{}\[\]\(\)\\|<>=+]" + regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-%—“”‘\"%\*/{}\[\]\(\)\\|<>=+]" result = re.sub(regex_expr, " ", result) result = result.replace("+", "plus").replace("&", "and") # remove extra whitespace diff --git a/video_creation/background.py b/video_creation/background.py index 1676f97..c451025 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -1,19 +1,32 @@ -from pathlib import Path +import json import random -from random import randrange import re +from pathlib import Path +from random import randrange from typing import Any, Tuple - from moviepy.editor import VideoFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip 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 +# Load background videos +with open("utils/backgrounds.json") as json_file: + background_options = json.load(json_file) + +# Remove "__comment" from backgrounds +background_options.pop("__comment", None) + +# Add position lambda function +# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) +for name in list(background_options.keys()): + pos = background_options[name][3] + + if pos != "center": + background_options[name][3] = lambda t: ("center", pos + 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. @@ -52,9 +65,7 @@ def download_background(background_config: Tuple[str, str, str, Any]): uri, filename, credit, _ = background_config if Path(f"assets/backgrounds/{credit}-{filename}").is_file(): return - print_step( - "We need to download the backgrounds videos. they are fairly large but it's only done once. 😎" - ) + print_step("We need to download the backgrounds videos. they are fairly large but it's only done once. 😎") print_substep("Downloading the backgrounds videos... please be patient 🙏 ") print_substep(f"Downloading {filename} from {uri}") YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json index 0637a08..fe51488 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 743e561..ad675c5 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -4,6 +4,7 @@ import os import re from os.path import exists from typing import Tuple, Any + from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.video.VideoClip import ImageClip @@ -13,11 +14,11 @@ from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from rich.console import Console +from utils import settings 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 console = Console() W, H = 1080, 1920 @@ -118,9 +119,7 @@ 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 - ) # note transition kwarg for delay in imgs + 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"]) @@ -140,9 +139,7 @@ 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, redditid=reddit_obj - ) + final = Video(final).add_watermark(text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj) final.write_videofile( f"assets/temp/{id}/temp.mp4", fps=30, @@ -163,6 +160,4 @@ def make_final_video( print_substep(f"Removed {cleanups} temporary files 🗑") print_substep("See result in the results folder!") - print_step( - f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}' - ) + print_step(f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}') diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 2344fce..ba7835f 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -1,19 +1,17 @@ import json - -from pathlib import Path import re +from pathlib import Path from typing import Dict -from utils import settings -from playwright.async_api import async_playwright # pylint: disable=unused-import - -# do not remove the above line +import translators as ts from playwright.sync_api import sync_playwright, ViewportSize from rich.progress import track -import translators as ts +from utils import settings from utils.console import print_step, print_substep +# do not remove the above line + storymode = False @@ -32,7 +30,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in with sync_playwright() as p: print_substep("Launching Headless Browser...") - browser = p.chromium.launch() + browser = p.chromium.launch(headless=True) # add headless=False for debug context = browser.new_context() if settings.config["settings"]["theme"] == "dark": @@ -53,9 +51,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in 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 + page.locator('[data-click-id="text"] button').click() # Remove "Click to see nsfw" Button in Screenshot # translate code @@ -74,16 +70,12 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Skipping translation...") postcontentpath = f"assets/temp/{id}/png/title.png" - page.locator('[data-test-id="post-content"]').screenshot(path= postcontentpath) + page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) if storymode: - page.locator('[data-click-id="text"]').screenshot( - path=f"assets/temp/{id}/png/story_content.png" - ) + page.locator('[data-click-id="text"]').screenshot(path=f"assets/temp/{id}/png/story_content.png") else: - for idx, comment in enumerate( - track(reddit_object["comments"], "Downloading screenshots...") - ): + for idx, comment in enumerate(track(reddit_object["comments"], "Downloading screenshots...")): # Stop if we have reached the screenshot_num if idx >= screenshot_num: break @@ -105,9 +97,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in [comment_tl, comment["comment_id"]], ) try: - page.locator(f"#t1_{comment['comment_id']}").screenshot( - path=f"assets/temp/{id}/png/comment_{idx}.png" - ) + page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/{id}/png/comment_{idx}.png") except TimeoutError: del reddit_object["comments"] screenshot_num += 1 diff --git a/video_creation/voices.py b/video_creation/voices.py index f49514d..511b7ff 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -1,19 +1,18 @@ #!/usr/bin/env python -from typing import Dict, Tuple +from typing import Tuple from rich.console import Console -from TTS.engine_wrapper import TTSEngine from TTS.GTTS import GTTS -from TTS.streamlabs_polly import StreamlabsPolly -from TTS.aws_polly import AWSPolly from TTS.TikTok import TikTok +from TTS.aws_polly import AWSPolly +from TTS.engine_wrapper import TTSEngine from TTS.pyttsx import pyttsx +from TTS.streamlabs_polly import StreamlabsPolly from utils import settings from utils.console import print_table, print_step - console = Console() TTSProviders = {