Merge branch 'lloydy-changes' into develop

pull/1838/head
Anthony Lloyd 2 years ago
commit 15ff6f6a8c

@ -43,7 +43,7 @@ def index():
@app.route("/backgrounds", methods=["GET"]) @app.route("/backgrounds", methods=["GET"])
def backgrounds(): def backgrounds():
return render_template("backgrounds.html", file="backgrounds.json") return render_template("backgrounds.html", file="background_videos.json")
@app.route("/background/add", methods=["POST"]) @app.route("/background/add", methods=["POST"])
@ -93,10 +93,10 @@ def videos_json():
return send_from_directory("video_creation/data", "videos.json") return send_from_directory("video_creation/data", "videos.json")
# Make backgrounds.json accessible # Make background_videos.json accessible
@app.route("/backgrounds.json") @app.route("/background_videos.json")
def backgrounds_json(): def backgrounds_json():
return send_from_directory("utils", "backgrounds.json") return send_from_directory("utils", "background_videos.json")
# Make videos in results folder accessible # Make videos in results folder accessible

@ -125,7 +125,7 @@
// Show background videos // Show background videos
$(document).ready(function () { $(document).ready(function () {
$.getJSON("backgrounds.json", $.getJSON("background_videos.json",
function (data) { function (data) {
delete data["__comment"]; delete data["__comment"];
var background = ''; var background = '';

@ -200,9 +200,9 @@
</div> </div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<label for="background_choice" class="col-4">Background Choice</label> <label for="background_video" class="col-4">Background Video</label>
<div class="col-8"> <div class="col-8">
<select name="background_choice" class="form-select" data-toggle="tooltip" <select name="background_video" class="form-select" data-toggle="tooltip"
data-original-title='Sets the background of the video'> data-original-title='Sets the background of the video'>
<option value=" ">Random Video</option> <option value=" ">Random Video</option>
{% for background in checks["background_video"]["options"][1:] %} {% for background in checks["background_video"]["options"][1:] %}

@ -4,13 +4,15 @@ from gtts import gTTS
from utils import settings from utils import settings
from typing import Optional
class GTTS: class GTTS:
def __init__(self): def __init__(self):
self.max_chars = 5000 self.max_chars = 5000
self.voices = [] self.voices = []
def run(self, text, filepath): def run(self, text, filepath, random_voice: bool = False, voice: Optional[str] = None):
tts = gTTS( tts = gTTS(
text=text, text=text,
lang=settings.config["reddit"]["thread"]["post_lang"] or "en", lang=settings.config["reddit"]["thread"]["post_lang"] or "en",
@ -18,5 +20,5 @@ class GTTS:
) )
tts.save(filepath) tts.save(filepath)
def randomvoice(self): def random_voice(self):
return random.choice(self.voices) return random.choice(self.voices)

@ -75,6 +75,8 @@ vocals: Final[tuple] = (
"en_female_ht_f08_wonderful_world", # Dramatic "en_female_ht_f08_wonderful_world", # Dramatic
) )
all_voices: Final[tuple] = disney_voices + eng_voices + non_eng_voices + vocals
# comment out a voice to make it unavailable to be randomly selected
class TikTok: class TikTok:
"""TikTok Text-to-Speech Wrapper""" """TikTok Text-to-Speech Wrapper"""
@ -95,12 +97,13 @@ class TikTok:
# set the headers to the session, so we don't have to do it for every request # set the headers to the session, so we don't have to do it for every request
self._session.headers = headers self._session.headers = headers
def run(self, text: str, filepath: str, random_voice: bool = False): def run(self, text: str, filepath: str, random_voice: bool = False, voice: Optional[str] = None):
if random_voice: if not voice:
voice = self.random_voice() if random_voice:
else: voice = self.random_voice()
# if tiktok_voice is not set in the config file, then use a random voice else:
voice = settings.config["settings"]["tts"].get("tiktok_voice", None) # if tiktok_voice is not set in the config file, then use a random voice
voice = settings.config["settings"]["tts"].get("tiktok_voice", self.random_voice())
# get the audio from the TikTok API # get the audio from the TikTok API
data = self.get_voices(voice=voice, text=text) data = self.get_voices(voice=voice, text=text)

@ -30,13 +30,16 @@ class AWSPolly:
self.max_chars = 3000 self.max_chars = 3000
self.voices = voices self.voices = voices
def run(self, text, filepath, random_voice: bool = False): def run(self, text, filepath, random_voice: bool = False, voice: str = None):
try: try:
session = Session(profile_name="polly") session = Session(profile_name="polly")
polly = session.client("polly") polly = session.client("polly")
if random_voice:
voice = self.randomvoice() if voice: # If voice is explicitly provided, use it
else: voice = voice.capitalize()
elif random_voice: # Else if random_voice is set to True, pick a random voice
voice = self.random_voice()
else: # If none of the above, use the voice from the settings
if not settings.config["settings"]["tts"]["aws_polly_voice"]: if not settings.config["settings"]["tts"]["aws_polly_voice"]:
raise ValueError( raise ValueError(
f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}" f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}"
@ -56,11 +59,8 @@ class AWSPolly:
# Access the audio stream from the response # Access the audio stream from the response
if "AudioStream" in response: if "AudioStream" in response:
file = open(filepath, "wb") with open(filepath, "wb") as file:
file.write(response["AudioStream"].read()) file.write(response["AudioStream"].read())
file.close()
# print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green")
else: else:
# The response didn't contain audio data, exit gracefully # The response didn't contain audio data, exit gracefully
print("Could not stream audio") print("Could not stream audio")
@ -75,5 +75,5 @@ class AWSPolly:
) )
sys.exit(-1) sys.exit(-1)
def randomvoice(self): def random_voice(self):
return random.choice(self.voices) return random.choice(self.voices)

@ -22,13 +22,13 @@ class elevenlabs:
self.max_chars = 2500 self.max_chars = 2500
self.voices = voices self.voices = voices
def run(self, text, filepath, random_voice: bool = False): def run(self, text, filepath, voice=None, random_voice: bool = False):
if random_voice:
voice = self.randomvoice() if not voice: # If voice is not provided directly
else: if random_voice:
voice = str( voice = self.random_voice()
settings.config["settings"]["tts"]["elevenlabs_voice_name"] else:
).capitalize() voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize()
if settings.config["settings"]["tts"]["elevenlabs_api_key"]: if settings.config["settings"]["tts"]["elevenlabs_api_key"]:
api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"]
@ -42,5 +42,5 @@ class elevenlabs:
) )
save(audio=audio, filename=filepath) save(audio=audio, filename=filepath)
def randomvoice(self): def random_voice(self):
return random.choice(self.voices) return random.choice(self.voices)

@ -1,7 +1,7 @@
import os import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple, Optional
import numpy as np import numpy as np
import translators import translators
@ -72,42 +72,38 @@ class TTSEngine:
print_step("Saving Text to MP3 files...") print_step("Saving Text to MP3 files...")
self.add_periods() self.add_periods()
self.call_tts("title", process_text(self.reddit_object["thread_title"])) # Select a voice for the title and thread body if in story mode
# processed_text = ##self.reddit_object["thread_post"] != "" voice = self.tts_module.random_voice() if settings.config["settings"]["tts"]["random_voice"] else None
self.call_tts("title", process_text(self.reddit_object["thread_title"]), voice)
idx = 0 idx = 0
if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 0: if settings.config["settings"]["storymodemethod"] == 0:
if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars: if len(self.reddit_object["thread_post"]) > self.tts_module.max_chars:
self.split_post(self.reddit_object["thread_post"], "postaudio") self.split_post(self.reddit_object["thread_post"], "postaudio", voice)
else: else:
self.call_tts( self.call_tts("postaudio", process_text(self.reddit_object["thread_post"]), voice)
"postaudio", process_text(self.reddit_object["thread_post"])
)
elif settings.config["settings"]["storymodemethod"] == 1: elif settings.config["settings"]["storymodemethod"] == 1:
for idx, text in track(enumerate(self.reddit_object["thread_post"])): for idx, text in track(enumerate(self.reddit_object["thread_post"])):
self.call_tts(f"postaudio-{idx}", process_text(text)) self.call_tts(f"postaudio-{idx}", process_text(text), voice)
else: else:
for idx, comment in track( for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."):
enumerate(self.reddit_object["comments"]), "Saving..." comment_voice = self.tts_module.random_voice() if settings.config["settings"]["tts"]["random_voice"] else None
):
# ! Stop creating mp3 files if the length is greater than max length. # ! Stop creating mp3 files if the length is greater than max length.
if self.length > self.max_length and idx > 1: if self.length > self.max_length and idx > 1:
self.length -= self.last_clip_length self.length -= self.last_clip_length
idx -= 1 idx -= 1
break break
if ( if (len(comment["comment_body"]) > self.tts_module.max_chars): # Split the comment if it is too long
len(comment["comment_body"]) > self.tts_module.max_chars self.split_post(comment["comment_body"], idx, comment_voice) # Split the comment
): # 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 else: # If the comment is not too long, just call the tts engine
self.call_tts(f"{idx}", process_text(comment["comment_body"])) self.call_tts(f"{idx}", process_text(comment["comment_body"]), comment_voice)
print_substep("Saved Text to MP3 files successfully.", style="bold green") print_substep("Saved Text to MP3 files successfully.", style="bold green")
return self.length, idx return self.length, idx
def split_post(self, text: str, idx): def split_post(self, text: str, idx, voice: Optional[str] = None):
split_files = [] split_files = []
split_text = [ split_text = [
x.group().strip() x.group().strip()
@ -126,7 +122,7 @@ class TTSEngine:
print("newtext was blank because sanitized split text resulted in none") print("newtext was blank because sanitized split text resulted in none")
continue continue
else: else:
self.call_tts(f"{idx}-{idy}.part", newtext) self.call_tts(f"{idx}-{idy}.part", newtext, voice)
with open(f"{self.path}/list.txt", "w") as f: with open(f"{self.path}/list.txt", "w") as f:
for idz in range(0, len(split_text)): for idz in range(0, len(split_text)):
f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n")
@ -148,11 +144,12 @@ class TTSEngine:
except OSError: except OSError:
print("OSError") print("OSError")
def call_tts(self, filename: str, text: str): def call_tts(self, filename: str, text: str, voice: Optional[str] = None):
self.tts_module.run( self.tts_module.run(
text, text,
filepath=f"{self.path}/{filename}.mp3", filepath=f"{self.path}/{filename}.mp3",
random_voice=settings.config["settings"]["tts"]["random_voice"], random_voice=settings.config["settings"]["tts"]["random_voice"],
voice=voice
) )
# try: # try:
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length # self.length += MP3(f"{self.path}/{filename}.mp3").info.length
@ -188,4 +185,4 @@ def process_text(text: str, clean: bool = True):
text, translator="google", to_language=lang text, translator="google", to_language=lang
) )
new_text = sanitize_text(translated_text) new_text = sanitize_text(translated_text)
return new_text return new_text

@ -4,6 +4,8 @@ import pyttsx3
from utils import settings from utils import settings
from typing import Optional
class pyttsx: class pyttsx:
def __init__(self): def __init__(self):
@ -15,9 +17,17 @@ class pyttsx:
text: str, text: str,
filepath: str, filepath: str,
random_voice=False, random_voice=False,
voice: Optional[str] = None,
): ):
voice_id = settings.config["settings"]["tts"]["python_voice"] voice_id = settings.config["settings"]["tts"]["python_voice"]
voice_num = settings.config["settings"]["tts"]["py_voice_num"] voice_num = settings.config["settings"]["tts"]["py_voice_num"]
if not voice:
if random_voice:
voice = self.random_voice()
else:
# if pyTTS is not set in the config file, then use a random voice
voice = settings.config["settings"]["tts"].get("python_voice", self.random_voice())
if voice_id == "" or voice_num == "": if voice_id == "" or voice_num == "":
voice_id = 2 voice_id = 2
voice_num = 3 voice_num = 3
@ -33,12 +43,8 @@ class pyttsx:
if random_voice: if random_voice:
voice_id = self.randomvoice() voice_id = self.randomvoice()
engine = pyttsx3.init() 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.save_to_file(text, f"{filepath}")
engine.runAndWait() engine.runAndWait()
def randomvoice(self): def random_voice(self):
return random.choice(self.voices) return random.choice(self.voices)

@ -36,7 +36,7 @@ class StreamlabsPolly:
def run(self, text, filepath, random_voice: bool = False): def run(self, text, filepath, random_voice: bool = False):
if random_voice: if random_voice:
voice = self.randomvoice() voice = self.random_voice()
else: else:
if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]: if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]:
raise ValueError( raise ValueError(
@ -62,5 +62,5 @@ class StreamlabsPolly:
except (KeyError, JSONDecodeError): except (KeyError, JSONDecodeError):
print("Error occurred calling Streamlabs Polly") print("Error occurred calling Streamlabs Polly")
def randomvoice(self): def random_voice(self):
return random.choice(self.voices) return random.choice(self.voices)

@ -1,5 +1,4 @@
import re import re
import praw import praw
from praw.models import MoreComments from praw.models import MoreComments
from prawcore.exceptions import ResponseException from prawcore.exceptions import ResponseException
@ -14,9 +13,7 @@ from utils.voice import sanitize_text
def get_subreddit_threads(POST_ID: str): def get_subreddit_threads(POST_ID: str):
""" """Returns a list of threads from the AskReddit subreddit."""
Returns a list of threads from the AskReddit subreddit.
"""
print_substep("Logging into Reddit.") print_substep("Logging into Reddit.")
@ -40,7 +37,7 @@ def get_subreddit_threads(POST_ID: str):
client_secret=settings.config["reddit"]["creds"]["client_secret"], client_secret=settings.config["reddit"]["creds"]["client_secret"],
user_agent="Accessing Reddit threads", user_agent="Accessing Reddit threads",
username=username, username=username,
passkey=passkey, password=passkey,
check_for_async=False, check_for_async=False,
) )
except ResponseException as e: except ResponseException as e:
@ -49,7 +46,10 @@ def get_subreddit_threads(POST_ID: str):
except: except:
print("Something went wrong...") print("Something went wrong...")
# Ask user for subreddit input min_upvotes = settings.config["reddit"]["thread"]["min_upvotes"]
sort_type = settings.config["reddit"]["thread"]["sort_type"]
subreddit_choice = settings.config["reddit"]["thread"]["subreddit"].lstrip('r/')
print_step("Getting subreddit threads...") print_step("Getting subreddit threads...")
similarity_score = 0 similarity_score = 0
if not settings.config["reddit"]["thread"][ if not settings.config["reddit"]["thread"][
@ -99,9 +99,10 @@ def get_subreddit_threads(POST_ID: str):
threads, subreddit, similarity_scores=similarity_scores threads, subreddit, similarity_scores=similarity_scores
) )
else: else:
threads = subreddit.hot(limit=25) threads = list(getattr(subreddit, str(sort_type))(limit=25))
submission = get_subreddit_undone(threads, subreddit) submission = get_subreddit_undone(threads, subreddit)
# Check submission
if submission is None: if submission is None:
return get_subreddit_threads(POST_ID) # submission already done. rerun return get_subreddit_threads(POST_ID) # submission already done. rerun
@ -129,45 +130,43 @@ def get_subreddit_threads(POST_ID: str):
f"Thread has a similarity score up to {round(similarity_score * 100)}%", f"Thread has a similarity score up to {round(similarity_score * 100)}%",
style="bold blue", style="bold blue",
) )
if submission.score < min_upvotes:
print_substep(
f"Thread has {submission.score} upvotes which is below the minimum threshold of {min_upvotes}. Skipping.")
return get_subreddit_threads(POST_ID)
content["thread_url"] = threadurl content["thread_url"] = threadurl
content["thread_title"] = submission.title content["thread_title"] = submission.title
content["thread_id"] = submission.id content["thread_id"] = submission.id
content["is_nsfw"] = submission.over_18 content["is_nsfw"] = submission.over_18
content["comments"] = [] content["comments"] = []
content["thread_post"] = "" # Default value
if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 1: if settings.config["settings"]["storymodemethod"] == 1:
content["thread_post"] = posttextparser(submission.selftext) content["thread_post"] = posttextparser(submission.selftext)
else: else:
content["thread_post"] = submission.selftext content["thread_post"] = submission.selftext
else: else:
for top_level_comment in submission.comments: # Process comments
if isinstance(top_level_comment, MoreComments): min_comment_length = int(str(settings.config["reddit"]["thread"]["min_comment_length"]))
max_comment_length = int(str(settings.config["reddit"]["thread"]["max_comment_length"]))
min_comment_upvotes = int(str(settings.config["reddit"]["thread"]["min_comment_upvotes"]))
for comment in submission.comments:
if isinstance(comment, MoreComments) or comment.body in ["[removed]", "[deleted]"] or comment.stickied:
continue
sanitised = sanitize_text(comment.body)
if not sanitised or sanitised == " ":
continue continue
if top_level_comment.body in ["[removed]", "[deleted]"]: if min_comment_length <= len(comment.body) <= max_comment_length and comment.score >= min_comment_upvotes:
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78 if comment.author and sanitised:
if not top_level_comment.stickied: content["comments"].append({
sanitised = sanitize_text(top_level_comment.body) "comment_body": comment.body,
if not sanitised or sanitised == " ": "comment_url": comment.permalink,
continue "comment_id": comment.id,
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"]["min_comment_length"]
):
if (
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(
{
"comment_body": top_level_comment.body,
"comment_url": top_level_comment.permalink,
"comment_id": top_level_comment.id,
}
)
print_substep("Received subreddit threads Successfully.", style="bold green") print_substep("Received subreddit threads Successfully.", style="bold green")
return content return content

@ -14,6 +14,8 @@ max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000,
min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" } min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" }
post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr", options = ['','af', 'ak', 'am', 'ar', 'as', 'ay', 'az', 'be', 'bg', 'bho', 'bm', 'bn', 'bs', 'ca', 'ceb', 'ckb', 'co', 'cs', 'cy', 'da', 'de', 'doi', 'dv', 'ee', 'el', 'en', 'en-US', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'ha', 'haw', 'hi', 'hmn', 'hr', 'ht', 'hu', 'hy', 'id', 'ig', 'ilo', 'is', 'it', 'iw', 'ja', 'jw', 'ka', 'kk', 'km', 'kn', 'ko', 'kri', 'ku', 'ky', 'la', 'lb', 'lg', 'ln', 'lo', 'lt', 'lus', 'lv', 'mai', 'mg', 'mi', 'mk', 'ml', 'mn', 'mni-Mtei', 'mr', 'ms', 'mt', 'my', 'ne', 'nl', 'no', 'nso', 'ny', 'om', 'or', 'pa', 'pl', 'ps', 'pt', 'qu', 'ro', 'ru', 'rw', 'sa', 'sd', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tr', 'ts', 'tt', 'ug', 'uk', 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh-CN', 'zh-TW', 'zu'] } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr", options = ['','af', 'ak', 'am', 'ar', 'as', 'ay', 'az', 'be', 'bg', 'bho', 'bm', 'bn', 'bs', 'ca', 'ceb', 'ckb', 'co', 'cs', 'cy', 'da', 'de', 'doi', 'dv', 'ee', 'el', 'en', 'en-US', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'ha', 'haw', 'hi', 'hmn', 'hr', 'ht', 'hu', 'hy', 'id', 'ig', 'ilo', 'is', 'it', 'iw', 'ja', 'jw', 'ka', 'kk', 'km', 'kn', 'ko', 'kri', 'ku', 'ky', 'la', 'lb', 'lg', 'ln', 'lo', 'lt', 'lus', 'lv', 'mai', 'mg', 'mi', 'mk', 'ml', 'mn', 'mni-Mtei', 'mr', 'ms', 'mt', 'my', 'ne', 'nl', 'no', 'nso', 'ny', 'om', 'or', 'pa', 'pl', 'ps', 'pt', 'qu', 'ro', 'ru', 'rw', 'sa', 'sd', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tr', 'ts', 'tt', 'ug', 'uk', 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh-CN', 'zh-TW', 'zu'] }
min_comments = { default = 20, optional = false, nmin = 10, 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" } min_comments = { default = 20, optional = false, nmin = 10, 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" }
min_upvotes = { default = 20, optional = false, nmin = 1, type = "int", explanation = "The minimum number of upvotes 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" }
sort_type = { default = "hot", optional = false, options = ["hot", "top", ], explanation = "the way to sort the reddit threads", example = "top" , oob_error = "the sort type needs to be hot, top, etc." }
[ai] [ai]
ai_similarity_enabled = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Threads read from Reddit are sorted based on their similarity to the keywords given below"} ai_similarity_enabled = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Threads read from Reddit are sorted based on their similarity to the keywords given below"}
@ -55,3 +57,10 @@ python_voice = { optional = false, default = "1", example = "1", explanation = "
py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" } 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" } silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }
no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" } no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" }
[settings.codecs]
VCODEC = { optional = false, default = "h264", example = "h264_nvenc", explanation = "The codec used to encode the videos. to find a list of avaliable codecs run ffmpeg " }
VCODECPRESET = { optional = false, default = "slow", example = "fast", explanation = "The codec preset used to encode the videos. to find a list of avaliable codecs run ffmpeg " }
VIDEO_BITRATE = { optional = false, default = "20M", example = "12M", explanation = "The bitrate of the video in megabytes per second ie 50M " }
AUDIO_BITRATE = { optional = false, default = "192k", example = "256k", explanation = "The bitrate of the audio in kilobytes per second ie 256k " }
CPUTHREADS = { optional = false, default = "4", example = "8", type = "int", explanation = "The amount of CPU threads to use during encoding " }

@ -3,18 +3,33 @@ import shutil
from os.path import exists from os.path import exists
def _listdir(d): # listdir with full path def count_items_in_directory(directory):
return [os.path.join(d, f) for f in os.listdir(d)] """Count all items (files and subdirectories) in a directory."""
return sum([len(files) for _, _, files in os.walk(directory)])
def cleanup(reddit_id) -> int: def cleanup(reddit_id) -> int:
"""Deletes all temporary assets in assets/temp """Deletes all temporary assets in temp/
Returns: Returns:
int: How many files were deleted int: How many files were deleted
""" """
directory = f"../assets/temp/{reddit_id}/" # Check current working directory
if exists(directory): cwd = os.getcwd()
shutil.rmtree(directory) print("Current working directory:", cwd)
directory = os.path.join(cwd, "assets", "temp", reddit_id)
print("Target directory:", directory)
return 1 if not exists(directory):
print("Directory does not exist!")
return 0
count_before_delete = count_items_in_directory(directory)
try:
shutil.rmtree(directory)
print(f"Successfully deleted the directory with {count_before_delete} items!")
return count_before_delete
except Exception as e:
print(f"Error encountered while deleting: {e}")
return 0

@ -137,21 +137,21 @@ def modify_settings(data: dict, config_load, checks: dict):
# Delete background video # Delete background video
def delete_background(key): def delete_background(key):
# Read backgrounds.json # Read background_videos.json
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos:
data = json.load(backgrounds) data = json.load(background_videos)
# Remove background from backgrounds.json # Remove background from background_videos.json
with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds: with open("utils/background_videos.json", "w", encoding="utf-8") as background_videos:
if data.pop(key, None): if data.pop(key, None):
json.dump(data, backgrounds, ensure_ascii=False, indent=4) json.dump(data, background_videos, ensure_ascii=False, indent=4)
else: else:
flash("Couldn't find this background. Try refreshing the page.", "error") flash("Couldn't find this background. Try refreshing the page.", "error")
return return
# Remove background video from ".config.template.toml" # Remove background video from ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].remove(key) config["settings"]["background"]["background_video"]["options"].remove(key)
with Path("utils/.config.template.toml").open("w") as toml_file: with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config)) toml_file.write(tomlkit.dumps(config))
@ -193,8 +193,8 @@ def add_background(youtube_uri, filename, citation, position):
filename = filename.replace(" ", "_") filename = filename.replace(" ", "_")
# Check if background doesn't already exist # Check if background doesn't already exist
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds: with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos:
data = json.load(backgrounds) data = json.load(background_videos)
# Check if key isn't already taken # Check if key isn't already taken
if filename in list(data.keys()): if filename in list(data.keys()):
@ -207,7 +207,7 @@ def add_background(youtube_uri, filename, citation, position):
return return
# Add background video to json file # Add background video to json file
with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds: with open("utils/background_videos.json", "r+", encoding="utf-8") as backgrounds:
data = json.load(backgrounds) data = json.load(backgrounds)
data[filename] = [youtube_uri, filename + ".mp4", citation, position] data[filename] = [youtube_uri, filename + ".mp4", citation, position]
@ -216,7 +216,7 @@ def add_background(youtube_uri, filename, citation, position):
# Add background video to ".config.template.toml" # Add background video to ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text()) config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].append(filename) config["settings"]["background"]["background_video"]["options"].append(filename)
with Path("utils/.config.template.toml").open("w") as toml_file: with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config)) toml_file.write(tomlkit.dumps(config))

@ -65,12 +65,14 @@ class ProgressFfmpeg(threading.Thread):
def name_normalize(name: str) -> str: def name_normalize(name: str) -> str:
name = re.sub(r'[?\\"%*:|<>]', "", name) name = re.sub(r'[?\"%*:|<>]', "", name)
name = re.sub(r"( [w,W]\s?\/\s?[o,O,0])", r" without", name) # Note: I've changed [w,W] to [wW] and [o,O,0] to [oO0]
name = re.sub(r"( [w,W]\s?\/)", r" with", name) name = re.sub(r"([wW])\s?/\s?([oO0])", r"without", name)
name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name) name = re.sub(r"([wW])\s?/", r"with", name)
name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name) name = re.sub(r"(\d+)\s?/\s?(\d+)", r"\1 of \2", name)
name = re.sub(r"\/", r"", name) # This one handles "this/that" as "this or that". The words won't overlap with the earlier "w/ or w/o"
name = re.sub(r"(\w+)\s?/\s?(\w+)", r"\1 or \2", name)
name = re.sub(r"/", "", name)
lang = settings.config["reddit"]["thread"]["post_lang"] lang = settings.config["reddit"]["thread"]["post_lang"]
if lang: if lang:
@ -83,7 +85,7 @@ def name_normalize(name: str) -> str:
return name return name
def prepare_background(reddit_id: str, W: int, H: int) -> str: def prepare_background(reddit_id: str, W: int, H: int) -> Tuple[str, float]:
output_path = f"assets/temp/{reddit_id}/background_noaudio.mp4" output_path = f"assets/temp/{reddit_id}/background_noaudio.mp4"
output = ( output = (
ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4") ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4")
@ -92,10 +94,15 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
output_path, output_path,
an=None, an=None,
**{ **{
"c:v": "h264", # these settings improve the encoding a lot and reduce visual errors in the video
"b:v": "20M", # plus its now super easy to configure your codec settings
"b:a": "192k", "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
"threads": multiprocessing.cpu_count(), "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]),
"b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]),
"b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]),
"threads": int(str(settings.config["settings"]["codecs"]["CPUTHREADS"])),
"force_key_frames": "expr:gte(t,n_forced*1)",
"g": 250, # GOP size
}, },
) )
.overwrite_output() .overwrite_output()
@ -144,8 +151,8 @@ def make_final_video(
background_config (Tuple[str, str, str, Any]): The background config to use. background_config (Tuple[str, str, str, Any]): The background config to use.
""" """
# settings values # settings values
W: Final[int] = int(settings.config["settings"]["resolution_w"]) W: Final[int] = int(str(settings.config["settings"]["resolution_w"]))
H: Final[int] = int(settings.config["settings"]["resolution_h"]) H: Final[int] = int(str(settings.config["settings"]["resolution_h"]))
opacity = settings.config["settings"]["opacity"] opacity = settings.config["settings"]["opacity"]
@ -162,33 +169,28 @@ def make_final_video(
# Gather all audio clips # Gather all audio clips
audio_clips = list() audio_clips = list()
if number_of_clips == 0 and settings.config["settings"]["storymode"] == "false": audio_clips_durations = []
print( storymode = settings.config.get("settings", {}).get("storymode")
"No audio clips to gather. Please use a different TTS or post." storymodemethod = settings.config.get("settings", {}).get("storymodemethod")
) # This is to fix the TypeError: unsupported operand type(s) for +: 'int' and 'NoneType' if number_of_clips == 0 and not storymode:
print("No audio clips to gather. Please use a different TTS or post.")
exit() exit()
if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 0: if storymode:
if storymodemethod == 0:
audio_clips = [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")] audio_clips = [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")]
audio_clips.insert( audio_clips.insert(1, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio.mp3"))
1, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio.mp3")
) elif storymodemethod == 1:
elif settings.config["settings"]["storymodemethod"] == 1: #i find it weird that I have to increase it by one to make it work, it kind of makes sense but not really
audio_clips = [ number_of_clips += 1
ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") audio_clips.extend(
for i in track( [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") for i in track(range(number_of_clips))])
range(number_of_clips + 1), "Collecting the audio files..." audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3"))
)
]
audio_clips.insert(
0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")
)
else: else:
audio_clips = [ audio_clips.extend([ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_clips + 1)])
ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3")
for i in range(number_of_clips)
]
audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3"))
audio_clips_durations = [ audio_clips_durations = [
@ -228,24 +230,19 @@ def make_final_video(
) )
current_time = 0 current_time = 0
if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymode"]:
audio_clips_durations = [ #that logic didnt actually work seeing as the mp3 names are way different for each mode
float(
ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")[
"format"
]["duration"]
)
for i in range(number_of_clips)
]
audio_clips_durations.insert(
0,
float(
ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"][
"duration"
]
),
)
if settings.config["settings"]["storymodemethod"] == 0: if settings.config["settings"]["storymodemethod"] == 0:
audio_clips_durations = [
float(
ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio.mp3")["format"]["duration"]
)
]
audio_clips_durations.insert(
0,
float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]),
)
image_clips.insert( image_clips.insert(
1, 1,
ffmpeg.input(f"assets/temp/{reddit_id}/png/story_content.png").filter( ffmpeg.input(f"assets/temp/{reddit_id}/png/story_content.png").filter(
@ -254,15 +251,27 @@ def make_final_video(
) )
background_clip = background_clip.overlay( background_clip = background_clip.overlay(
image_clips[0], image_clips[0],
enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})",
x="(main_w-overlay_w)/2", x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2", y="(main_h-overlay_h)/2",
) )
current_time += audio_clips_durations[0] current_time += audio_clips_durations[0]
elif settings.config["settings"]["storymodemethod"] == 1: elif settings.config["settings"]["storymodemethod"] == 1:
for i in track(
range(0, number_of_clips + 1), "Collecting the image files..." audio_clips_durations = [
): float(
ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"]
)
for i in range(number_of_clips)
]
audio_clips_durations.insert(
0,
float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]),
)
print("the number of clips originally is: " + str(int(number_of_clips)))
for i in track(range(number_of_clips + 1)):
print("appending image: " + str(i) + ".png")
print("at time :" + str(current_time))
image_clips.append( image_clips.append(
ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter( ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter(
"scale", screenshot_width, -1 "scale", screenshot_width, -1
@ -274,6 +283,7 @@ def make_final_video(
x="(main_w-overlay_w)/2", x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2", y="(main_h-overlay_h)/2",
) )
print("Total image clips: " + str(len(image_clips)))
current_time += audio_clips_durations[i] current_time += audio_clips_durations[i]
else: else:
for i in range(0, number_of_clips + 1): for i in range(0, number_of_clips + 1):
@ -291,6 +301,11 @@ def make_final_video(
) )
current_time += audio_clips_durations[i] current_time += audio_clips_durations[i]
#fade and cut video at appropriate time
total_audio_duration = sum(audio_clips_durations)
background_clip = background_clip.filter('tpad', stop_duration=1)
background_clip = background_clip.filter('fade', type='out', start_time=total_audio_duration, duration=1)
title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"])
idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
title_thumb = reddit_obj["thread_title"] title_thumb = reddit_obj["thread_title"]
@ -346,10 +361,8 @@ def make_final_video(
height, height,
title_thumb, title_thumb,
) )
thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") thumbnailSave.save(f"results/{subreddit}/thumbnails/{filename}.png")
print_substep( print_substep(f"Thumbnail - Building Thumbnail in results/{subreddit}/thumbnails/{filename}.png")
f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png"
)
text = f"Background by {background_config['video'][2]}" text = f"Background by {background_config['video'][2]}"
background_clip = ffmpeg.drawtext( background_clip = ffmpeg.drawtext(
@ -385,10 +398,13 @@ def make_final_video(
path, path,
f="mp4", f="mp4",
**{ **{
"c:v": "h264", "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
"b:v": "20M", "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]),
"b:a": "192k", "b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]),
"threads": multiprocessing.cpu_count(), "b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]),
"threads": int(settings.config["settings"]["codecs"]["CPUTHREADS"]),
"g": 250, # GOP size
"force_key_frames": "expr:gte(t,n_forced*1)",
}, },
).overwrite_output().global_args( ).overwrite_output().global_args(
"-progress", progress.output_file.name "-progress", progress.output_file.name
@ -417,10 +433,13 @@ def make_final_video(
path, path,
f="mp4", f="mp4",
**{ **{
"c:v": "h264", "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
"b:v": "20M", "preset": str(settings.config["settings"]["codecs"]["VCODECPRESET"]),
"b:a": "192k", "b:v": str(settings.config["settings"]["codecs"]["VIDEO_BITRATE"]),
"threads": multiprocessing.cpu_count(), "b:a": str(settings.config["settings"]["codecs"]["AUDIO_BITRATE"]),
"threads": int(settings.config["settings"]["codecs"]["CPUTHREADS"]),
"g": 250, # GOP size
"force_key_frames": "expr:gte(t,n_forced*1)",
}, },
).overwrite_output().global_args( ).overwrite_output().global_args(
"-progress", progress.output_file.name "-progress", progress.output_file.name

@ -16,6 +16,37 @@ from utils.videos import save_data
__all__ = ["download_screenshots_of_reddit_posts"] __all__ = ["download_screenshots_of_reddit_posts"]
def translate_comment_body(page, comment: dict, lang: str):
"""Translate a Reddit comment and update its content on the page.
Args:
page: The Playwright page object.
comment (dict): The Reddit comment data.
lang (str): The target language to which the comment should be translated.
"""
# Check if language setting is present
if not lang:
return
# Translate the comment body
comment_tl = translators.translate_text(
comment["comment_body"],
translator="google",
to_language=lang
)
# Update the content on the page with the translated text
page.evaluate(
'''(tl_content, tl_id) => {
const commentElement = document.querySelector(`#t1_${tl_id} > div:nth-child(2) > div > div[data-testid="comment"] > div`);
if (commentElement) {
commentElement.textContent = tl_content;
}
}''',
comment_tl, comment["comment_id"]
)
def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
"""Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png """Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png
@ -26,8 +57,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# settings values # settings values
W: Final[int] = int(settings.config["settings"]["resolution_w"]) W: Final[int] = int(settings.config["settings"]["resolution_w"])
H: Final[int] = int(settings.config["settings"]["resolution_h"]) H: Final[int] = int(settings.config["settings"]["resolution_h"])
lang: Final[str] = settings.config["reddit"]["thread"]["post_lang"] lang: Final[str] = str(settings.config["reddit"]["thread"]["post_lang"])
storymode: Final[bool] = settings.config["settings"]["storymode"] storymode: Final[bool] = bool(settings.config["settings"]["storymode"])
print_step("Downloading screenshots of reddit posts...") print_step("Downloading screenshots of reddit posts...")
reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
@ -106,12 +137,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
page.set_viewport_size(ViewportSize(width=1920, height=1080)) page.set_viewport_size(ViewportSize(width=1920, height=1080))
page.wait_for_load_state() page.wait_for_load_state()
page.locator('[name="username"]').fill( page.locator('[name="username"]').fill(str(settings.config["reddit"]["creds"]["username"]))
settings.config["reddit"]["creds"]["username"] page.locator('[name="password"]').fill(str(settings.config["reddit"]["creds"]["password"]))
)
page.locator('[name="password"]').fill(
settings.config["reddit"]["creds"]["password"]
)
page.locator("button[class$='m-full-width']").click() page.locator("button[class$='m-full-width']").click()
page.wait_for_timeout(5000) page.wait_for_timeout(5000)
@ -189,14 +216,15 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom # as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom
location = page.locator('[data-test-id="post-content"]').bounding_box() location = page.locator('[data-test-id="post-content"]').bounding_box()
for i in location: for i in location:
location[i] = float("{:.2f}".format(location[i] * zoom)) location[i] = float("{:.2f}".format(location[i] * float(zoom)))
page.screenshot(clip=location, path=postcontentpath) page.screenshot(clip=location, path=postcontentpath)
else: else:
page.locator('[data-test-id="post-content"]').screenshot( page.locator('[data-test-id="post-content"]').screenshot(
path=postcontentpath path=postcontentpath
) )
except Exception as e: except Exception as e:
print_substep("Something went wrong!", style="red") print_substep("Something went wrong inside screenshot_downloader!", style="red")
print_substep(f"Error: {str(e)}", style="red")
resp = input( resp = input(
"Something went wrong with making the screenshots! Do you want to skip the post? (y/n) " "Something went wrong with making the screenshots! Do you want to skip the post? (y/n) "
) )
@ -238,16 +266,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# translate code # translate code
if settings.config["reddit"]["thread"]["post_lang"]: translate_comment_body(page, comment, settings.config["reddit"]["thread"]["post_lang"])
comment_tl = translators.translate_text(
comment["comment_body"],
translator="google",
to_language=settings.config["reddit"]["thread"]["post_lang"],
)
page.evaluate(
'([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"]],
)
try: try:
if settings.config["settings"]["zoom"] != 1: if settings.config["settings"]["zoom"] != 1:
# store zoom settings # store zoom settings
@ -263,7 +283,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
f"#t1_{comment['comment_id']}" f"#t1_{comment['comment_id']}"
).bounding_box() ).bounding_box()
for i in location: for i in location:
location[i] = float("{:.2f}".format(location[i] * zoom)) location[i] = float("{:.2f}".format(location[i] * float(zoom)))
page.screenshot( page.screenshot(
clip=location, clip=location,
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png", path=f"assets/temp/{reddit_id}/png/comment_{idx}.png",

Loading…
Cancel
Save