diff --git a/GUI.py b/GUI.py
index 4588083..7b8b23c 100644
--- a/GUI.py
+++ b/GUI.py
@@ -43,7 +43,7 @@ def index():
@app.route("/backgrounds", methods=["GET"])
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"])
@@ -91,10 +91,10 @@ def videos_json():
return send_from_directory("video_creation/data", "videos.json")
-# Make backgrounds.json accessible
-@app.route("/backgrounds.json")
+# Make background_videos.json accessible
+@app.route("/background_videos.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
diff --git a/GUI/backgrounds.html b/GUI/backgrounds.html
index 541e39f..438aa90 100644
--- a/GUI/backgrounds.html
+++ b/GUI/backgrounds.html
@@ -125,7 +125,7 @@
// Show background videos
$(document).ready(function () {
- $.getJSON("backgrounds.json",
+ $.getJSON("background_videos.json",
function (data) {
delete data["__comment"];
var background = '';
diff --git a/GUI/settings.html b/GUI/settings.html
index 1f0ef2e..854e10d 100644
--- a/GUI/settings.html
+++ b/GUI/settings.html
@@ -200,12 +200,12 @@
-
+
-
diff --git a/TTS/GTTS.py b/TTS/GTTS.py
index bff100f..53f78dc 100644
--- a/TTS/GTTS.py
+++ b/TTS/GTTS.py
@@ -4,13 +4,15 @@ from gtts import gTTS
from utils import settings
+from typing import Optional
+
class GTTS:
def __init__(self):
self.max_chars = 5000
self.voices = []
- def run(self, text, filepath):
+ def run(self, text, filepath, random_voice: bool = False, voice: Optional[str] = None):
tts = gTTS(
text=text,
lang=settings.config["reddit"]["thread"]["post_lang"] or "en",
@@ -18,5 +20,5 @@ class GTTS:
)
tts.save(filepath)
- def randomvoice(self):
- return random.choice(self.voices)
+ def random_voice(self):
+ return random.choice(self.voices)
\ No newline at end of file
diff --git a/TTS/TikTok.py b/TTS/TikTok.py
index 29542e2..efe77b4 100644
--- a/TTS/TikTok.py
+++ b/TTS/TikTok.py
@@ -75,6 +75,8 @@ vocals: Final[tuple] = (
"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:
"""TikTok Text-to-Speech Wrapper"""
@@ -93,12 +95,13 @@ class TikTok:
# set the headers to the session, so we don't have to do it for every request
self._session.headers = headers
- def run(self, text: str, filepath: str, random_voice: bool = False):
- if random_voice:
- voice = self.random_voice()
- else:
- # if tiktok_voice is not set in the config file, then use a random voice
- voice = settings.config["settings"]["tts"].get("tiktok_voice", None)
+ def run(self, text: str, filepath: str, random_voice: bool = False, voice: Optional[str] = None):
+ if not voice:
+ if random_voice:
+ voice = self.random_voice()
+ else:
+ # 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
data = self.get_voices(voice=voice, text=text)
diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py
index 4d55860..a11cb65 100644
--- a/TTS/aws_polly.py
+++ b/TTS/aws_polly.py
@@ -30,13 +30,16 @@ class AWSPolly:
self.max_chars = 3000
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:
session = Session(profile_name="polly")
polly = session.client("polly")
- if random_voice:
- voice = self.randomvoice()
- else:
+
+ if voice: # If voice is explicitly provided, use it
+ 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"]:
raise ValueError(
f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}"
@@ -54,11 +57,8 @@ class AWSPolly:
# Access the audio stream from the response
if "AudioStream" in response:
- file = open(filepath, "wb")
- file.write(response["AudioStream"].read())
- file.close()
- # print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green")
-
+ with open(filepath, "wb") as file:
+ file.write(response["AudioStream"].read())
else:
# The response didn't contain audio data, exit gracefully
print("Could not stream audio")
@@ -73,5 +73,5 @@ class AWSPolly:
)
sys.exit(-1)
- def randomvoice(self):
+ def random_voice(self):
return random.choice(self.voices)
diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py
index e18bba9..9638bde 100644
--- a/TTS/elevenlabs.py
+++ b/TTS/elevenlabs.py
@@ -22,11 +22,13 @@ class elevenlabs:
self.max_chars = 2500
self.voices = voices
- def run(self, text, filepath, random_voice: bool = False):
- if random_voice:
- voice = self.randomvoice()
- else:
- voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize()
+ def run(self, text, filepath, voice=None, random_voice: bool = False):
+
+ if not voice: # If voice is not provided directly
+ if random_voice:
+ voice = self.random_voice()
+ else:
+ voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize()
if settings.config["settings"]["tts"]["elevenlabs_api_key"]:
api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"]
@@ -38,5 +40,5 @@ class elevenlabs:
audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1")
save(audio=audio, filename=filepath)
- def randomvoice(self):
+ def random_voice(self):
return random.choice(self.voices)
diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py
index 7a73d61..6d8c8ec 100644
--- a/TTS/engine_wrapper.py
+++ b/TTS/engine_wrapper.py
@@ -1,7 +1,7 @@
import os
import re
from pathlib import Path
-from typing import Tuple
+from typing import Tuple, Optional
import numpy as np
import translators
@@ -73,38 +73,38 @@ class TTSEngine:
print_step("Saving Text to MP3 files...")
self.add_periods()
- self.call_tts("title", process_text(self.reddit_object["thread_title"]))
- # processed_text = ##self.reddit_object["thread_post"] != ""
+ # Select a voice for the title and thread body if in story mode
+ 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
if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 0:
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:
- self.call_tts("postaudio", process_text(self.reddit_object["thread_post"]))
+ self.call_tts("postaudio", process_text(self.reddit_object["thread_post"]), voice)
elif settings.config["settings"]["storymodemethod"] == 1:
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:
for idx, comment in track(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.
if self.length > self.max_length and idx > 1:
self.length -= self.last_clip_length
idx -= 1
break
- if (
- len(comment["comment_body"]) > self.tts_module.max_chars
- ): # Split the comment if it is too long
- self.split_post(comment["comment_body"], idx) # Split the comment
+ 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, comment_voice) # 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"]))
+ self.call_tts(f"{idx}", process_text(comment["comment_body"]), comment_voice)
print_substep("Saved Text to MP3 files successfully.", style="bold green")
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_text = [
x.group().strip()
@@ -123,7 +123,7 @@ class TTSEngine:
print("newtext was blank because sanitized split text resulted in none")
continue
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:
for idz in range(0, len(split_text)):
f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n")
@@ -145,11 +145,12 @@ class TTSEngine:
except 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(
text,
filepath=f"{self.path}/{filename}.mp3",
random_voice=settings.config["settings"]["tts"]["random_voice"],
+ voice=voice
)
# try:
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length
@@ -181,4 +182,4 @@ def process_text(text: str, clean: bool = True):
print_substep("Translating Text...")
translated_text = translators.translate_text(text, translator="google", to_language=lang)
new_text = sanitize_text(translated_text)
- return new_text
+ return new_text
\ No newline at end of file
diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py
index bf47601..6070f06 100644
--- a/TTS/pyttsx.py
+++ b/TTS/pyttsx.py
@@ -9,34 +9,29 @@ class pyttsx:
def __init__(self):
self.max_chars = 5000
self.voices = []
+ engine = pyttsx3.init()
+ self.voices = [voice.id for voice in engine.getProperty("voices")]
+
+ def run(self, text: str, filepath: str, random_voice=False, voice=None):
+ voice_id = voice or settings.config["settings"]["tts"]["python_voice"]
+
+ if not voice_id:
+ voice_id = self.random_voice()
+
+ if not voice_id.isdigit():
+ raise ValueError("Invalid voice ID provided")
+ voice_id = int(voice_id)
+
+ if voice_id >= len(self.voices):
+ raise ValueError(f"Voice ID out of range. Valid IDs are 0 to {len(self.voices) - 1}")
- def run(
- self,
- text: str,
- filepath: str,
- random_voice=False,
- ):
- voice_id = settings.config["settings"]["tts"]["python_voice"]
- voice_num = settings.config["settings"]["tts"]["py_voice_num"]
- if voice_id == "" or voice_num == "":
- voice_id = 2
- voice_num = 3
- raise ValueError("set pyttsx values to a valid value, switching to defaults")
- else:
- voice_id = int(voice_id)
- voice_num = int(voice_num)
- for i in range(voice_num):
- self.voices.append(i)
- i = +1
if random_voice:
- voice_id = self.randomvoice()
+ voice_id = self.random_voice()
+
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", self.voices[voice_id])
engine.save_to_file(text, f"{filepath}")
engine.runAndWait()
- def randomvoice(self):
- return random.choice(self.voices)
+ def random_voice(self):
+ return random.choice(self.voices)
\ No newline at end of file
diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py
index dc80dc9..1e3b40b 100644
--- a/TTS/streamlabs_polly.py
+++ b/TTS/streamlabs_polly.py
@@ -36,7 +36,7 @@ class StreamlabsPolly:
def run(self, text, filepath, random_voice: bool = False):
if random_voice:
- voice = self.randomvoice()
+ voice = self.random_voice()
else:
if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]:
raise ValueError(
@@ -60,5 +60,5 @@ class StreamlabsPolly:
except (KeyError, JSONDecodeError):
print("Error occurred calling Streamlabs Polly")
- def randomvoice(self):
+ def random_voice(self):
return random.choice(self.voices)
diff --git a/reddit/subreddit.py b/reddit/subreddit.py
index e1def23..6b81437 100644
--- a/reddit/subreddit.py
+++ b/reddit/subreddit.py
@@ -1,24 +1,19 @@
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.ai_methods import sort_by_similarity
from utils.console import print_step, print_substep
+from utils.posttextparser import posttextparser
from utils.subreddit import get_subreddit_undone
from utils.videos import check_done
from utils.voice import sanitize_text
-from utils.posttextparser import posttextparser
-from utils.ai_methods import sort_by_similarity
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.")
@@ -40,7 +35,7 @@ def get_subreddit_threads(POST_ID: str):
client_secret=settings.config["reddit"]["creds"]["client_secret"],
user_agent="Accessing Reddit threads",
username=username,
- passkey=passkey,
+ password=passkey,
check_for_async=False,
)
except ResponseException as e:
@@ -49,8 +44,12 @@ def get_subreddit_threads(POST_ID: str):
except:
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...")
+
similarity_score = 0
if not settings.config["reddit"]["thread"][
"subreddit"
@@ -91,9 +90,10 @@ def get_subreddit_threads(POST_ID: str):
threads, subreddit, similarity_scores=similarity_scores
)
else:
- threads = subreddit.hot(limit=25)
+ threads = list(getattr(subreddit, str(sort_type))(limit=25))
submission = get_subreddit_undone(threads, subreddit)
+ # Check submission
if submission is None:
return get_subreddit_threads(POST_ID) # submission already done. rerun
@@ -118,45 +118,43 @@ def get_subreddit_threads(POST_ID: str):
f"Thread has a similarity score up to {round(similarity_score * 100)}%",
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_title"] = submission.title
content["thread_id"] = submission.id
content["is_nsfw"] = submission.over_18
content["comments"] = []
+ content["thread_post"] = "" # Default value
if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 1:
content["thread_post"] = posttextparser(submission.selftext)
else:
content["thread_post"] = submission.selftext
else:
- for top_level_comment in submission.comments:
- if isinstance(top_level_comment, MoreComments):
+ # Process comments
+ 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
- if top_level_comment.body in ["[removed]", "[deleted]"]:
- continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
- if not top_level_comment.stickied:
- 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"]["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,
- }
- )
+ if min_comment_length <= len(comment.body) <= max_comment_length and comment.score >= min_comment_upvotes:
+ if comment.author and sanitised:
+ content["comments"].append({
+ "comment_body": comment.body,
+ "comment_url": comment.permalink,
+ "comment_id": comment.id,
+ })
print_substep("Received subreddit threads Successfully.", style="bold green")
return content
diff --git a/utils/.config.template.toml b/utils/.config.template.toml
index accf86d..a93852e 100644
--- a/utils/.config.template.toml
+++ b/utils/.config.template.toml
@@ -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" }
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_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_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)" }
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" }
+
+[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 " }
\ No newline at end of file
diff --git a/utils/cleanup.py b/utils/cleanup.py
index 6e00d4c..7b2bfbb 100644
--- a/utils/cleanup.py
+++ b/utils/cleanup.py
@@ -3,18 +3,33 @@ from os.path import exists
import shutil
-def _listdir(d): # listdir with full path
- return [os.path.join(d, f) for f in os.listdir(d)]
+def count_items_in_directory(directory):
+ """Count all items (files and subdirectories) in a directory."""
+ return sum([len(files) for _, _, files in os.walk(directory)])
def cleanup(reddit_id) -> int:
- """Deletes all temporary assets in assets/temp
+ """Deletes all temporary assets in temp/
Returns:
int: How many files were deleted
"""
- directory = f"../assets/temp/{reddit_id}/"
- if exists(directory):
- shutil.rmtree(directory)
+ # Check current working directory
+ cwd = os.getcwd()
+ 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
diff --git a/utils/gui_utils.py b/utils/gui_utils.py
index f683adf..1975a15 100644
--- a/utils/gui_utils.py
+++ b/utils/gui_utils.py
@@ -125,21 +125,21 @@ def modify_settings(data: dict, config_load, checks: dict):
# 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)
+ # Read background_videos.json
+ with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos:
+ data = json.load(background_videos)
- # Remove background from backgrounds.json
- with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds:
+ # Remove background from background_videos.json
+ with open("utils/background_videos.json", "w", encoding="utf-8") as background_videos:
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:
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)
+ config["settings"]["background"]["background_video"]["options"].remove(key)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
@@ -179,8 +179,8 @@ def add_background(youtube_uri, filename, citation, position):
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)
+ with open("utils/background_videos.json", "r", encoding="utf-8") as background_videos:
+ data = json.load(background_videos)
# Check if key isn't already taken
if filename in list(data.keys()):
@@ -193,7 +193,7 @@ def add_background(youtube_uri, filename, citation, position):
return
# 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[filename] = [youtube_uri, filename + ".mp4", citation, position]
@@ -202,7 +202,7 @@ def add_background(youtube_uri, filename, citation, position):
# 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)
+ config["settings"]["background"]["background_video"]["options"].append(filename)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
diff --git a/video_creation/final_video.py b/video_creation/final_video.py
index 84ca249..fd53096 100644
--- a/video_creation/final_video.py
+++ b/video_creation/final_video.py
@@ -66,12 +66,14 @@ class ProgressFfmpeg(threading.Thread):
def name_normalize(name: str) -> str:
- name = re.sub(r'[?\\"%*:|<>]', "", name)
- name = re.sub(r"( [w,W]\s?\/\s?[o,O,0])", r" without", name)
- name = re.sub(r"( [w,W]\s?\/)", r" with", name)
- name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name)
- name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name)
- name = re.sub(r"\/", r"", name)
+ name = re.sub(r'[?\"%*:|<>]', "", name)
+ # Note: I've changed [w,W] to [wW] and [o,O,0] to [oO0]
+ name = re.sub(r"([wW])\s?/\s?([oO0])", r"without", name)
+ name = re.sub(r"([wW])\s?/", r"with", name)
+ name = re.sub(r"(\d+)\s?/\s?(\d+)", r"\1 of \2", 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"]
if lang:
@@ -82,7 +84,7 @@ def name_normalize(name: str) -> str:
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 = (
ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4")
@@ -91,10 +93,15 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
output_path,
an=None,
**{
- "c:v": "h264",
- "b:v": "20M",
- "b:a": "192k",
- "threads": multiprocessing.cpu_count(),
+ # these settings improve the encoding a lot and reduce visual errors in the video
+ # plus its now super easy to configure your codec settings
+ "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
+ "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()
@@ -141,8 +148,8 @@ def make_final_video(
background_config (Tuple[str, str, str, Any]): The background config to use.
"""
# settings values
- W: Final[int] = int(settings.config["settings"]["resolution_w"])
- H: Final[int] = int(settings.config["settings"]["resolution_h"])
+ W: Final[int] = int(str(settings.config["settings"]["resolution_w"]))
+ H: Final[int] = int(str(settings.config["settings"]["resolution_h"]))
opacity = settings.config["settings"]["opacity"]
@@ -159,26 +166,28 @@ def make_final_video(
# Gather all audio clips
audio_clips = list()
- if number_of_clips == 0 and settings.config["settings"]["storymode"] == "false":
- print(
- "No audio clips to gather. Please use a different TTS or post."
- ) # This is to fix the TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
+ audio_clips_durations = []
+ storymode = settings.config.get("settings", {}).get("storymode")
+ storymodemethod = settings.config.get("settings", {}).get("storymodemethod")
+ if number_of_clips == 0 and not storymode:
+ print("No audio clips to gather. Please use a different TTS or post.")
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.insert(1, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio.mp3"))
- elif settings.config["settings"]["storymodemethod"] == 1:
- audio_clips = [
- ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")
- for i in track(range(number_of_clips + 1), "Collecting the audio files...")
- ]
+
+ elif 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
+ number_of_clips += 1
+ audio_clips.extend(
+ [ffmpeg.input(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") for i in track(range(number_of_clips))])
audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3"))
else:
- audio_clips = [
- ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_clips)
- ]
+ audio_clips.extend([ffmpeg.input(f"assets/temp/{reddit_id}/mp3/{i}.mp3") for i in range(number_of_clips + 1)])
audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3"))
audio_clips_durations = [
@@ -210,18 +219,19 @@ def make_final_video(
)
current_time = 0
+
if settings.config["settings"]["storymode"]:
- 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"]),
- )
+ #that logic didnt actually work seeing as the mp3 names are way different for each mode
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(
1,
ffmpeg.input(f"assets/temp/{reddit_id}/png/story_content.png").filter(
@@ -230,13 +240,27 @@ def make_final_video(
)
background_clip = background_clip.overlay(
image_clips[0],
- enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})",
x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2",
)
current_time += audio_clips_durations[0]
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(
ffmpeg.input(f"assets/temp/{reddit_id}/png/img{i}.png")["v"].filter(
"scale", screenshot_width, -1
@@ -248,6 +272,7 @@ def make_final_video(
x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2",
)
+ print("Total image clips: " + str(len(image_clips)))
current_time += audio_clips_durations[i]
else:
for i in range(0, number_of_clips + 1):
@@ -265,6 +290,11 @@ def make_final_video(
)
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"])
idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
title_thumb = reddit_obj["thread_title"]
@@ -312,8 +342,8 @@ def make_final_video(
height,
title_thumb,
)
- thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png")
- print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png")
+ thumbnailSave.save(f"results/{subreddit}/thumbnails/{filename}.png")
+ print_substep(f"Thumbnail - Building Thumbnail in results/{subreddit}/thumbnails/{filename}.png")
text = f"Background by {background_config['video'][2]}"
background_clip = ffmpeg.drawtext(
@@ -349,10 +379,13 @@ def make_final_video(
path,
f="mp4",
**{
- "c:v": "h264",
- "b:v": "20M",
- "b:a": "192k",
- "threads": multiprocessing.cpu_count(),
+ "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
+ "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(settings.config["settings"]["codecs"]["CPUTHREADS"]),
+ "g": 250, # GOP size
+ "force_key_frames": "expr:gte(t,n_forced*1)",
},
).overwrite_output().global_args("-progress", progress.output_file.name).run(
quiet=True,
@@ -379,10 +412,13 @@ def make_final_video(
path,
f="mp4",
**{
- "c:v": "h264",
- "b:v": "20M",
- "b:a": "192k",
- "threads": multiprocessing.cpu_count(),
+ "c:v": str(settings.config["settings"]["codecs"]["VCODEC"]),
+ "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(settings.config["settings"]["codecs"]["CPUTHREADS"]),
+ "g": 250, # GOP size
+ "force_key_frames": "expr:gte(t,n_forced*1)",
},
).overwrite_output().global_args("-progress", progress.output_file.name).run(
quiet=True,
diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py
index cdc8d61..e87143d 100644
--- a/video_creation/screenshot_downloader.py
+++ b/video_creation/screenshot_downloader.py
@@ -18,6 +18,37 @@ from utils.videos import save_data
__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):
"""Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png
@@ -28,8 +59,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# settings values
W: Final[int] = int(settings.config["settings"]["resolution_w"])
H: Final[int] = int(settings.config["settings"]["resolution_h"])
- lang: Final[str] = settings.config["reddit"]["thread"]["post_lang"]
- storymode: Final[bool] = settings.config["settings"]["storymode"]
+ lang: Final[str] = str(settings.config["reddit"]["thread"]["post_lang"])
+ storymode: Final[bool] = bool(settings.config["settings"]["storymode"])
print_step("Downloading screenshots of reddit posts...")
reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
@@ -100,8 +131,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
page.set_viewport_size(ViewportSize(width=1920, height=1080))
page.wait_for_load_state()
- page.locator('[name="username"]').fill(settings.config["reddit"]["creds"]["username"])
- page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"])
+ page.locator('[name="username"]').fill(str(settings.config["reddit"]["creds"]["username"]))
+ page.locator('[name="password"]').fill(str(settings.config["reddit"]["creds"]["password"]))
page.locator("button[class$='m-full-width']").click()
page.wait_for_timeout(5000)
@@ -179,12 +210,13 @@ 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
location = page.locator('[data-test-id="post-content"]').bounding_box()
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)
else:
page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath)
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(
"Something went wrong with making the screenshots! Do you want to skip the post? (y/n) "
)
@@ -224,16 +256,8 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# translate code
- if 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"]],
- )
+ translate_comment_body(page, comment, settings.config["reddit"]["thread"]["post_lang"])
+
try:
if settings.config["settings"]["zoom"] != 1:
# store zoom settings
@@ -245,7 +269,7 @@ 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
location = page.locator(f"#t1_{comment['comment_id']}").bounding_box()
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=f"assets/temp/{reddit_id}/png/comment_{idx}.png",