From 20e9d79499ccfbe55a864302309a4853c6a935b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Jul 2022 00:27:00 +0000 Subject: [PATCH 01/63] Bump translators from 5.2.2 to 5.3.1 Bumps [translators](https://github.com/uliontse/translators) from 5.2.2 to 5.3.1. - [Release notes](https://github.com/uliontse/translators/releases) - [Commits](https://github.com/uliontse/translators/commits) --- updated-dependencies: - dependency-name: translators dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4cc217c..c33fd05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ python-dotenv==0.20.0 pytube==12.1.0 requests==2.28.1 rich==12.4.4 -translators==5.2.2 +translators==5.3.1 From 1a4edfafca88a708f5e6e431c4e4ffc7d02b7683 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 12 Jul 2022 12:41:42 -0400 Subject: [PATCH 02/63] Watermark implementation THIS IS IN BETA --- main.py | 4 +-- requirements.txt | 2 ++ utils/video.py | 55 ++++++++++++++++++++++++++++++++ video_creation/final_video.py | 60 +++++++++++------------------------ 4 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 utils/video.py diff --git a/main.py b/main.py index 10ab3c1..653703c 100755 --- a/main.py +++ b/main.py @@ -16,8 +16,8 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -__VERSION__ = "2.3" -__BRANCH__ = "master" +__VERSION__ = "2.3.1" +__BRANCH__ = "develop" print( """ diff --git a/requirements.txt b/requirements.txt index e57a37d..7bccd0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 + +Pillow~=9.1.1 diff --git a/utils/video.py b/utils/video.py new file mode 100644 index 0000000..f49e58f --- /dev/null +++ b/utils/video.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from moviepy.video.VideoClip import VideoClip, ImageClip +from moviepy.video.io.VideoFileClip import VideoFileClip +from PIL import Image, ImageDraw, ImageFont, ImageEnhance + +class Video: + def __init__(self, video: VideoClip | VideoFileClip, *args, **kwargs): + self.video: VideoClip = video + self.fps = self.video.fps + self.duration = self.video.duration + + @staticmethod + def _create_watermark(text, path, fontsize=15, opacity=0.5): + + width = 500 + height = 200 + + white = (255, 255, 255) + transparent = (0, 0, 0, 0) + + font = ImageFont.load_default() + wm = Image.new('RGBA', (width, height), transparent) + im = Image.new('RGBA', (width, height), transparent) # Change this line too. + + draw = ImageDraw.Draw(wm) + w, h = draw.textsize(text, font) + draw.text(((width - w) / 2, (height - h) / 2), text, white, font) + en = ImageEnhance.Brightness(wm) # todo alow it to use the fontsize + mask = en.enhance(1 - opacity) + im.paste(wm, (25, 25), mask) + im.save(path) + + def add_watermark(self, text, opacity=0.5): + # add a watermark to the video clip with the given text and opacity without importing a new library + from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + from moviepy.video.compositing.concatenate import concatenate_videoclips + path = './assets/temp/png/watermark.png' + self._create_watermark(text, path, opacity=opacity) + image_clips = [] + image_clips.insert( + 0, + ImageClip(path) + .set_duration(self.video.duration) + #.resize(width=W - 100) + ) + image_concat = concatenate_videoclips(image_clips).set_position((0.1, 0.1)) + self.video = CompositeVideoClip([self.video, image_concat]) + return self.video + + + + +if __name__ == '__main__': # todo delete + Video._create_watermark('Background Video by Jason(example)', '../assets/temp/png/watermark.png') diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8524051..9d40825 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -15,6 +15,7 @@ from rich.console import Console from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.video import Video from utils.videos import save_data from utils import settings @@ -42,12 +43,8 @@ def name_normalize(name: str) -> str: return name -def make_final_video( - number_of_clips: int, - length: int, - reddit_obj: dict, - background_config: Tuple[str, str, str, Any], -): +def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, + background_config: Tuple[str, str, str, Any], ): """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp Args: number_of_clips (int): Index to end at when going through the screenshots' @@ -65,11 +62,8 @@ def make_final_video( VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] background_clip = ( - VideoFileClip("assets/temp/background.mp4") - .without_audio() - .resize(height=H) - .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) - ) + VideoFileClip("assets/temp/background.mp4").without_audio().resize(height=H).crop(x1=1166.6, y1=0, x2=2246.6, + y2=1920)) # Gather all audio clips audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] @@ -82,21 +76,15 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - image_clips.insert( - 0, - ImageClip("assets/temp/png/title.png") - .set_duration(audio_clips[0].duration) - .resize(width=W - 100) - .set_opacity(new_opacity), - ) + image_clips.insert(0, + ImageClip("assets/temp/png/title.png").set_duration(audio_clips[0].duration).resize( + width=W - 100).set_opacity( + new_opacity), ) for i in range(0, number_of_clips): image_clips.append( - ImageClip(f"assets/temp/png/comment_{i}.png") - .set_duration(audio_clips[i + 1].duration) - .resize(width=W - 100) - .set_opacity(new_opacity) - ) + ImageClip(f"assets/temp/png/comment_{i}.png").set_duration(audio_clips[i + 1].duration).resize( + width=W - 100).set_opacity(new_opacity)) # if os.path.exists("assets/mp3/posttext.mp3"): # image_clips.insert( @@ -109,7 +97,8 @@ def make_final_video( # ) # else: story mode stuff img_clip_pos = background_config[3] - image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) + image_concat = concatenate_videoclips(image_clips).set_position( + img_clip_pos) # note transition kwarg for delay in imgs image_concat.audio = audio_composite final = CompositeVideoClip([background_clip, image_concat]) title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) @@ -129,27 +118,14 @@ 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.write_videofile( - "assets/temp/temp.mp4", - fps=30, - audio_codec="aac", - audio_bitrate="192k", - verbose=False, - threads=multiprocessing.cpu_count(), - ) - ffmpeg_extract_subclip( - "assets/temp/temp.mp4", - 0, - final.duration, - targetname=f"results/{subreddit}/{filename}", - ) + final = Video(final).add_watermark(text=f'Background credit: {background_config[2]}', opacity=0.4) + final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k", verbose=False, + threads=multiprocessing.cpu_count(), ) + ffmpeg_extract_subclip("assets/temp/temp.mp4", 0, final.duration, targetname=f"results/{subreddit}/{filename}", ) save_data(subreddit, filename, title, idx, background_config[2]) print_step("Removing temporary files 🗑") cleanups = cleanup() 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]}') From d9d62e252c2f2eef46963db5eb1437ddca1cd0f2 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 12 Jul 2022 13:11:33 -0400 Subject: [PATCH 03/63] backup but I will be reverting this. --- utils/video.py | 54 ++++++++++++-------------------------------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/utils/video.py b/utils/video.py index f49e58f..2cceae1 100644 --- a/utils/video.py +++ b/utils/video.py @@ -1,8 +1,11 @@ from __future__ import annotations -from moviepy.video.VideoClip import VideoClip, ImageClip +from typing import Tuple + +from moviepy.video.VideoClip import VideoClip, TextClip from moviepy.video.io.VideoFileClip import VideoFileClip -from PIL import Image, ImageDraw, ImageFont, ImageEnhance +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + class Video: def __init__(self, video: VideoClip | VideoFileClip, *args, **kwargs): @@ -11,45 +14,14 @@ class Video: self.duration = self.video.duration @staticmethod - def _create_watermark(text, path, fontsize=15, opacity=0.5): + def _create_watermark(text, fontsize, opacity=0.5): + txt_clip = TextClip(text, fontsize=fontsize, color='black').set_opacity(opacity) + return txt_clip - width = 500 - height = 200 + def add_watermark(self, text, opacity=0.5, position: Tuple = (0.95, 0.95), fontsize=15): + txt_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) + txt_clip = txt_clip.set_pos(position).set_duration(10) - white = (255, 255, 255) - transparent = (0, 0, 0, 0) - - font = ImageFont.load_default() - wm = Image.new('RGBA', (width, height), transparent) - im = Image.new('RGBA', (width, height), transparent) # Change this line too. - - draw = ImageDraw.Draw(wm) - w, h = draw.textsize(text, font) - draw.text(((width - w) / 2, (height - h) / 2), text, white, font) - en = ImageEnhance.Brightness(wm) # todo alow it to use the fontsize - mask = en.enhance(1 - opacity) - im.paste(wm, (25, 25), mask) - im.save(path) - - def add_watermark(self, text, opacity=0.5): - # add a watermark to the video clip with the given text and opacity without importing a new library - from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip - from moviepy.video.compositing.concatenate import concatenate_videoclips - path = './assets/temp/png/watermark.png' - self._create_watermark(text, path, opacity=opacity) - image_clips = [] - image_clips.insert( - 0, - ImageClip(path) - .set_duration(self.video.duration) - #.resize(width=W - 100) - ) - image_concat = concatenate_videoclips(image_clips).set_position((0.1, 0.1)) - self.video = CompositeVideoClip([self.video, image_concat]) + # Overlay the text clip on the first video clip + self.video = CompositeVideoClip([self.video, txt_clip]) return self.video - - - - -if __name__ == '__main__': # todo delete - Video._create_watermark('Background Video by Jason(example)', '../assets/temp/png/watermark.png') From 1bd39db6f20fa52528059a69c645c1a38571c3f7 Mon Sep 17 00:00:00 2001 From: napuh Date: Wed, 13 Jul 2022 19:47:33 +0200 Subject: [PATCH 04/63] Fix: cutting long file names to avoid errors when saving --- video_creation/final_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8524051..c7e7fc3 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -29,11 +29,11 @@ def name_normalize(name: str) -> str: name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name) name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name) name = re.sub(r"\/", r"", name) + name[:70] lang = settings.config["reddit"]["thread"]["post_lang"] if lang: import translators as ts - print_substep("Translating filename...") translated_name = ts.google(name, to_language=lang) return translated_name From b19b0fd91b1a6f1c4a7023c02b4e981691244a06 Mon Sep 17 00:00:00 2001 From: RiveN000 <61630074+RiveN000@users.noreply.github.com> Date: Thu, 14 Jul 2022 00:04:32 +0200 Subject: [PATCH 05/63] Added two new background videos Added two cool copyright-free background videos often used in shorts. --- .config.template.toml | 2 +- video_creation/background.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.config.template.toml b/.config.template.toml index 18f1b00..9fd779e 100644 --- a/.config.template.toml +++ b/.config.template.toml @@ -31,7 +31,7 @@ storymode = { optional = true, type = "bool", default = false, example = false, ], explanation = "not yet implemented" } [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } +background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } #background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, # false, #], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } diff --git a/video_creation/background.py b/video_creation/background.py index be0f46c..6054592 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -44,6 +44,18 @@ background_options = { "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), + ), } From d5db0ce0cfeca27056ddf998c46fb0c08597ea4f Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 18:13:57 -0400 Subject: [PATCH 06/63] refactored final_video.py updated requirements.txt simplified cleanup.py --- requirements.txt | 2 -- utils/cleanup.py | 18 ++++++----- video_creation/final_video.py | 61 ++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7bccd0d..e57a37d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,3 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 - -Pillow~=9.1.1 diff --git a/utils/cleanup.py b/utils/cleanup.py index ef4fc44..75074b7 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -2,6 +2,10 @@ import os from os.path import exists +def _listdir(d): # listdir with full path + return [os.path.join(d, f) for f in os.listdir(d)] + + def cleanup() -> int: """Deletes all temporary assets in assets/temp @@ -14,14 +18,12 @@ def cleanup() -> int: count += len(files) for f in files: os.remove(f) - try: - for file in os.listdir("./assets/temp/mp4"): + REMOVE_DIRS = ["./assets/temp/mp3/", "./assets/temp/png/"] + files_to_remove = list(map(_listdir, REMOVE_DIRS)) + for directory in files_to_remove: + for file in directory: count += 1 - os.remove("./assets/temp/mp4/" + file) - except FileNotFoundError: - pass - for file in os.listdir("./assets/temp/mp3"): - count += 1 - os.remove("./assets/temp/mp3/" + file) + os.remove(file) return count + return 0 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 9d40825..a6d5d2f 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -43,8 +43,12 @@ def name_normalize(name: str) -> str: return name -def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, - background_config: Tuple[str, str, str, Any], ): +def make_final_video( + number_of_clips: int, + length: int, + reddit_obj: dict, + background_config: Tuple[str, str, str, Any], +): """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp Args: number_of_clips (int): Index to end at when going through the screenshots' @@ -62,8 +66,11 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] background_clip = ( - VideoFileClip("assets/temp/background.mp4").without_audio().resize(height=H).crop(x1=1166.6, y1=0, x2=2246.6, - y2=1920)) + VideoFileClip("assets/temp/background.mp4") + .without_audio() + .resize(height=H) + .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) + ) # Gather all audio clips audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] @@ -76,15 +83,21 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - image_clips.insert(0, - ImageClip("assets/temp/png/title.png").set_duration(audio_clips[0].duration).resize( - width=W - 100).set_opacity( - new_opacity), ) + image_clips.insert( + 0, + ImageClip("assets/temp/png/title.png") + .set_duration(audio_clips[0].duration) + .resize(width=W - 100) + .set_opacity(new_opacity), + ) for i in range(0, number_of_clips): image_clips.append( - ImageClip(f"assets/temp/png/comment_{i}.png").set_duration(audio_clips[i + 1].duration).resize( - width=W - 100).set_opacity(new_opacity)) + ImageClip(f"assets/temp/png/comment_{i}.png") + .set_duration(audio_clips[i + 1].duration) + .resize(width=W - 100) + .set_opacity(new_opacity) + ) # if os.path.exists("assets/mp3/posttext.mp3"): # image_clips.insert( @@ -98,7 +111,8 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, # 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 + 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"]) @@ -118,14 +132,29 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, # # lowered_audio = audio_background.multiply_volume( # todo get this to work # # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx # final.set_audio(final_audio) - final = Video(final).add_watermark(text=f'Background credit: {background_config[2]}', opacity=0.4) - final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k", verbose=False, - threads=multiprocessing.cpu_count(), ) - ffmpeg_extract_subclip("assets/temp/temp.mp4", 0, final.duration, targetname=f"results/{subreddit}/{filename}", ) + final = Video(final).add_watermark( + text=f"Background credit: {background_config[2]}", opacity=0.4 + ) + final.write_videofile( + "assets/temp/temp.mp4", + fps=30, + audio_codec="aac", + audio_bitrate="192k", + verbose=False, + threads=multiprocessing.cpu_count(), + ) + ffmpeg_extract_subclip( + "assets/temp/temp.mp4", + 0, + final.duration, + targetname=f"results/{subreddit}/{filename}", + ) save_data(subreddit, filename, title, idx, background_config[2]) print_step("Removing temporary files 🗑") cleanups = cleanup() 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]}' + ) From 9b01aa9f271f176999e5d5b989052660530db440 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 18:15:29 -0400 Subject: [PATCH 07/63] typo: fixed typo in engine_wrapper.py --- TTS/engine_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index c5ed714..213defb 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFUALT_MAX_LENGTH: int = 50 # video length variable +DEFAULT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: @@ -35,7 +35,7 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFUALT_MAX_LENGTH, + max_length: int = DEFAULT_MAX_LENGTH, ): self.tts_module = tts_module() self.reddit_object = reddit_object From d7419cf8e3c3524910c6a33c2fc4c0c2d3ae2dee Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 19:07:42 -0400 Subject: [PATCH 08/63] Update final_video.py --- video_creation/final_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index c7e7fc3..eb42247 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -29,7 +29,7 @@ def name_normalize(name: str) -> str: name = re.sub(r"(\d+)\s?\/\s?(\d+)", r"\1 of \2", name) name = re.sub(r"(\w+)\s?\/\s?(\w+)", r"\1 or \2", name) name = re.sub(r"\/", r"", name) - name[:70] + name[:30] lang = settings.config["reddit"]["thread"]["post_lang"] if lang: From a210011db7b1fd63741666d128fa64dcb76a0ba4 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 21:55:32 -0400 Subject: [PATCH 09/63] refactor: moved backgrounds to CONSTANTS.py --- .config.template.toml | 2 +- utils/CONSTANTS.py | 46 ++++++++++++++++++++++++++++++++++++ video_creation/background.py | 35 +-------------------------- 3 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 utils/CONSTANTS.py diff --git a/.config.template.toml b/.config.template.toml index 18f1b00..9fd779e 100644 --- a/.config.template.toml +++ b/.config.template.toml @@ -31,7 +31,7 @@ storymode = { optional = true, type = "bool", default = false, example = false, ], explanation = "not yet implemented" } [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } +background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } #background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, # false, #], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py new file mode 100644 index 0000000..76d2ee4 --- /dev/null +++ b/utils/CONSTANTS.py @@ -0,0 +1,46 @@ + +# 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/video_creation/background.py b/video_creation/background.py index be0f46c..bef0c53 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -10,42 +10,9 @@ from pytube import YouTube from pytube.cli import on_progress from utils import settings +from utils.CONSTANTS import background_options from utils.console import print_step, print_substep -# Supported Background. Can add/remove background video here.... -# - : key -> used as keyword for TOML file. value -> background configuration -# Format (value): -# 1. Youtube URI -# 2. filename -# 3. Citation (owner of the video) -# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) -background_options = { - "motor-gta": ( # Motor-GTA Racing - "https://www.youtube.com/watch?v=vw5L4xCPy9Q", - "bike-parkour-gta.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), - "rocket-league": ( # Rocket League - "https://www.youtube.com/watch?v=2X9QGY__0II", - "rocket_league.mp4", - "Orbital Gameplay", - lambda t: ("center", 200 + t), - ), - "minecraft": ( # Minecraft parkour - "https://www.youtube.com/watch?v=n_Dv4JMiwK8", - "parkour.mp4", - "bbswitzer", - "center", - ), - "gta": ( # GTA Stunt Race - "https://www.youtube.com/watch?v=qGa9kWREOnE", - "gta-stunt-race.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), -} - def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: """Generates a random interval of time to be used as the background of the video. From 1e93121aede538f4807ab73737a884260e388211 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 12 Jul 2022 12:41:42 -0400 Subject: [PATCH 10/63] Watermark implementation THIS IS IN BETA --- main.py | 4 +-- requirements.txt | 2 ++ utils/video.py | 55 ++++++++++++++++++++++++++++++++ video_creation/final_video.py | 60 +++++++++++------------------------ 4 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 utils/video.py diff --git a/main.py b/main.py index 10ab3c1..653703c 100755 --- a/main.py +++ b/main.py @@ -16,8 +16,8 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -__VERSION__ = "2.3" -__BRANCH__ = "master" +__VERSION__ = "2.3.1" +__BRANCH__ = "develop" print( """ diff --git a/requirements.txt b/requirements.txt index e57a37d..7bccd0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 + +Pillow~=9.1.1 diff --git a/utils/video.py b/utils/video.py new file mode 100644 index 0000000..f49e58f --- /dev/null +++ b/utils/video.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from moviepy.video.VideoClip import VideoClip, ImageClip +from moviepy.video.io.VideoFileClip import VideoFileClip +from PIL import Image, ImageDraw, ImageFont, ImageEnhance + +class Video: + def __init__(self, video: VideoClip | VideoFileClip, *args, **kwargs): + self.video: VideoClip = video + self.fps = self.video.fps + self.duration = self.video.duration + + @staticmethod + def _create_watermark(text, path, fontsize=15, opacity=0.5): + + width = 500 + height = 200 + + white = (255, 255, 255) + transparent = (0, 0, 0, 0) + + font = ImageFont.load_default() + wm = Image.new('RGBA', (width, height), transparent) + im = Image.new('RGBA', (width, height), transparent) # Change this line too. + + draw = ImageDraw.Draw(wm) + w, h = draw.textsize(text, font) + draw.text(((width - w) / 2, (height - h) / 2), text, white, font) + en = ImageEnhance.Brightness(wm) # todo alow it to use the fontsize + mask = en.enhance(1 - opacity) + im.paste(wm, (25, 25), mask) + im.save(path) + + def add_watermark(self, text, opacity=0.5): + # add a watermark to the video clip with the given text and opacity without importing a new library + from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + from moviepy.video.compositing.concatenate import concatenate_videoclips + path = './assets/temp/png/watermark.png' + self._create_watermark(text, path, opacity=opacity) + image_clips = [] + image_clips.insert( + 0, + ImageClip(path) + .set_duration(self.video.duration) + #.resize(width=W - 100) + ) + image_concat = concatenate_videoclips(image_clips).set_position((0.1, 0.1)) + self.video = CompositeVideoClip([self.video, image_concat]) + return self.video + + + + +if __name__ == '__main__': # todo delete + Video._create_watermark('Background Video by Jason(example)', '../assets/temp/png/watermark.png') diff --git a/video_creation/final_video.py b/video_creation/final_video.py index eb42247..5a52348 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -15,6 +15,7 @@ from rich.console import Console from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.video import Video from utils.videos import save_data from utils import settings @@ -42,12 +43,8 @@ def name_normalize(name: str) -> str: return name -def make_final_video( - number_of_clips: int, - length: int, - reddit_obj: dict, - background_config: Tuple[str, str, str, Any], -): +def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, + background_config: Tuple[str, str, str, Any], ): """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp Args: number_of_clips (int): Index to end at when going through the screenshots' @@ -65,11 +62,8 @@ def make_final_video( VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] background_clip = ( - VideoFileClip("assets/temp/background.mp4") - .without_audio() - .resize(height=H) - .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) - ) + VideoFileClip("assets/temp/background.mp4").without_audio().resize(height=H).crop(x1=1166.6, y1=0, x2=2246.6, + y2=1920)) # Gather all audio clips audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] @@ -82,21 +76,15 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - image_clips.insert( - 0, - ImageClip("assets/temp/png/title.png") - .set_duration(audio_clips[0].duration) - .resize(width=W - 100) - .set_opacity(new_opacity), - ) + image_clips.insert(0, + ImageClip("assets/temp/png/title.png").set_duration(audio_clips[0].duration).resize( + width=W - 100).set_opacity( + new_opacity), ) for i in range(0, number_of_clips): image_clips.append( - ImageClip(f"assets/temp/png/comment_{i}.png") - .set_duration(audio_clips[i + 1].duration) - .resize(width=W - 100) - .set_opacity(new_opacity) - ) + ImageClip(f"assets/temp/png/comment_{i}.png").set_duration(audio_clips[i + 1].duration).resize( + width=W - 100).set_opacity(new_opacity)) # if os.path.exists("assets/mp3/posttext.mp3"): # image_clips.insert( @@ -109,7 +97,8 @@ def make_final_video( # ) # else: story mode stuff img_clip_pos = background_config[3] - image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) + image_concat = concatenate_videoclips(image_clips).set_position( + img_clip_pos) # note transition kwarg for delay in imgs image_concat.audio = audio_composite final = CompositeVideoClip([background_clip, image_concat]) title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) @@ -129,27 +118,14 @@ 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.write_videofile( - "assets/temp/temp.mp4", - fps=30, - audio_codec="aac", - audio_bitrate="192k", - verbose=False, - threads=multiprocessing.cpu_count(), - ) - ffmpeg_extract_subclip( - "assets/temp/temp.mp4", - 0, - final.duration, - targetname=f"results/{subreddit}/{filename}", - ) + final = Video(final).add_watermark(text=f'Background credit: {background_config[2]}', opacity=0.4) + final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k", verbose=False, + threads=multiprocessing.cpu_count(), ) + ffmpeg_extract_subclip("assets/temp/temp.mp4", 0, final.duration, targetname=f"results/{subreddit}/{filename}", ) save_data(subreddit, filename, title, idx, background_config[2]) print_step("Removing temporary files 🗑") cleanups = cleanup() 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]}') From 7dcac10d1ef1b6e1c6379545a8a1a3d4d7e54463 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 12 Jul 2022 13:11:33 -0400 Subject: [PATCH 11/63] backup but I will be reverting this. --- utils/video.py | 54 ++++++++++++-------------------------------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/utils/video.py b/utils/video.py index f49e58f..2cceae1 100644 --- a/utils/video.py +++ b/utils/video.py @@ -1,8 +1,11 @@ from __future__ import annotations -from moviepy.video.VideoClip import VideoClip, ImageClip +from typing import Tuple + +from moviepy.video.VideoClip import VideoClip, TextClip from moviepy.video.io.VideoFileClip import VideoFileClip -from PIL import Image, ImageDraw, ImageFont, ImageEnhance +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + class Video: def __init__(self, video: VideoClip | VideoFileClip, *args, **kwargs): @@ -11,45 +14,14 @@ class Video: self.duration = self.video.duration @staticmethod - def _create_watermark(text, path, fontsize=15, opacity=0.5): + def _create_watermark(text, fontsize, opacity=0.5): + txt_clip = TextClip(text, fontsize=fontsize, color='black').set_opacity(opacity) + return txt_clip - width = 500 - height = 200 + def add_watermark(self, text, opacity=0.5, position: Tuple = (0.95, 0.95), fontsize=15): + txt_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) + txt_clip = txt_clip.set_pos(position).set_duration(10) - white = (255, 255, 255) - transparent = (0, 0, 0, 0) - - font = ImageFont.load_default() - wm = Image.new('RGBA', (width, height), transparent) - im = Image.new('RGBA', (width, height), transparent) # Change this line too. - - draw = ImageDraw.Draw(wm) - w, h = draw.textsize(text, font) - draw.text(((width - w) / 2, (height - h) / 2), text, white, font) - en = ImageEnhance.Brightness(wm) # todo alow it to use the fontsize - mask = en.enhance(1 - opacity) - im.paste(wm, (25, 25), mask) - im.save(path) - - def add_watermark(self, text, opacity=0.5): - # add a watermark to the video clip with the given text and opacity without importing a new library - from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip - from moviepy.video.compositing.concatenate import concatenate_videoclips - path = './assets/temp/png/watermark.png' - self._create_watermark(text, path, opacity=opacity) - image_clips = [] - image_clips.insert( - 0, - ImageClip(path) - .set_duration(self.video.duration) - #.resize(width=W - 100) - ) - image_concat = concatenate_videoclips(image_clips).set_position((0.1, 0.1)) - self.video = CompositeVideoClip([self.video, image_concat]) + # Overlay the text clip on the first video clip + self.video = CompositeVideoClip([self.video, txt_clip]) return self.video - - - - -if __name__ == '__main__': # todo delete - Video._create_watermark('Background Video by Jason(example)', '../assets/temp/png/watermark.png') From 36aa2566687e2d1935f50c713d00e330fbd7f8ba Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 18:13:57 -0400 Subject: [PATCH 12/63] refactored final_video.py updated requirements.txt simplified cleanup.py --- requirements.txt | 2 -- utils/cleanup.py | 18 ++++++----- video_creation/final_video.py | 61 ++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7bccd0d..e57a37d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,3 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 - -Pillow~=9.1.1 diff --git a/utils/cleanup.py b/utils/cleanup.py index ef4fc44..75074b7 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -2,6 +2,10 @@ import os from os.path import exists +def _listdir(d): # listdir with full path + return [os.path.join(d, f) for f in os.listdir(d)] + + def cleanup() -> int: """Deletes all temporary assets in assets/temp @@ -14,14 +18,12 @@ def cleanup() -> int: count += len(files) for f in files: os.remove(f) - try: - for file in os.listdir("./assets/temp/mp4"): + REMOVE_DIRS = ["./assets/temp/mp3/", "./assets/temp/png/"] + files_to_remove = list(map(_listdir, REMOVE_DIRS)) + for directory in files_to_remove: + for file in directory: count += 1 - os.remove("./assets/temp/mp4/" + file) - except FileNotFoundError: - pass - for file in os.listdir("./assets/temp/mp3"): - count += 1 - os.remove("./assets/temp/mp3/" + file) + os.remove(file) return count + return 0 diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 5a52348..3157296 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -43,8 +43,12 @@ def name_normalize(name: str) -> str: return name -def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, - background_config: Tuple[str, str, str, Any], ): +def make_final_video( + number_of_clips: int, + length: int, + reddit_obj: dict, + background_config: Tuple[str, str, str, Any], +): """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp Args: number_of_clips (int): Index to end at when going through the screenshots' @@ -62,8 +66,11 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] background_clip = ( - VideoFileClip("assets/temp/background.mp4").without_audio().resize(height=H).crop(x1=1166.6, y1=0, x2=2246.6, - y2=1920)) + VideoFileClip("assets/temp/background.mp4") + .without_audio() + .resize(height=H) + .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) + ) # Gather all audio clips audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] @@ -76,15 +83,21 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - image_clips.insert(0, - ImageClip("assets/temp/png/title.png").set_duration(audio_clips[0].duration).resize( - width=W - 100).set_opacity( - new_opacity), ) + image_clips.insert( + 0, + ImageClip("assets/temp/png/title.png") + .set_duration(audio_clips[0].duration) + .resize(width=W - 100) + .set_opacity(new_opacity), + ) for i in range(0, number_of_clips): image_clips.append( - ImageClip(f"assets/temp/png/comment_{i}.png").set_duration(audio_clips[i + 1].duration).resize( - width=W - 100).set_opacity(new_opacity)) + ImageClip(f"assets/temp/png/comment_{i}.png") + .set_duration(audio_clips[i + 1].duration) + .resize(width=W - 100) + .set_opacity(new_opacity) + ) # if os.path.exists("assets/mp3/posttext.mp3"): # image_clips.insert( @@ -98,7 +111,8 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, # 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 + 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"]) @@ -118,14 +132,29 @@ def make_final_video(number_of_clips: int, length: int, reddit_obj: dict, # # lowered_audio = audio_background.multiply_volume( # todo get this to work # # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx # final.set_audio(final_audio) - final = Video(final).add_watermark(text=f'Background credit: {background_config[2]}', opacity=0.4) - final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k", verbose=False, - threads=multiprocessing.cpu_count(), ) - ffmpeg_extract_subclip("assets/temp/temp.mp4", 0, final.duration, targetname=f"results/{subreddit}/{filename}", ) + final = Video(final).add_watermark( + text=f"Background credit: {background_config[2]}", opacity=0.4 + ) + final.write_videofile( + "assets/temp/temp.mp4", + fps=30, + audio_codec="aac", + audio_bitrate="192k", + verbose=False, + threads=multiprocessing.cpu_count(), + ) + ffmpeg_extract_subclip( + "assets/temp/temp.mp4", + 0, + final.duration, + targetname=f"results/{subreddit}/{filename}", + ) save_data(subreddit, filename, title, idx, background_config[2]) print_step("Removing temporary files 🗑") cleanups = cleanup() 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]}' + ) From 2f3efb03a9a7e01e786be121e9aff7f45f1a1c52 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 18:15:29 -0400 Subject: [PATCH 13/63] typo: fixed typo in engine_wrapper.py --- TTS/engine_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index c5ed714..213defb 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFUALT_MAX_LENGTH: int = 50 # video length variable +DEFAULT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: @@ -35,7 +35,7 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFUALT_MAX_LENGTH, + max_length: int = DEFAULT_MAX_LENGTH, ): self.tts_module = tts_module() self.reddit_object = reddit_object From 891c9945fd02b7003dd7cf9633b9516d0490fe81 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 21:55:32 -0400 Subject: [PATCH 14/63] refactor: moved backgrounds to CONSTANTS.py --- .config.template.toml | 2 +- utils/CONSTANTS.py | 46 ++++++++++++++++++++++++++++++++++++ video_creation/background.py | 35 +-------------------------- 3 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 utils/CONSTANTS.py diff --git a/.config.template.toml b/.config.template.toml index 18f1b00..9fd779e 100644 --- a/.config.template.toml +++ b/.config.template.toml @@ -31,7 +31,7 @@ storymode = { optional = true, type = "bool", default = false, example = false, ], explanation = "not yet implemented" } [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } +background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } #background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, # false, #], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py new file mode 100644 index 0000000..76d2ee4 --- /dev/null +++ b/utils/CONSTANTS.py @@ -0,0 +1,46 @@ + +# 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/video_creation/background.py b/video_creation/background.py index be0f46c..bef0c53 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -10,42 +10,9 @@ from pytube import YouTube from pytube.cli import on_progress from utils import settings +from utils.CONSTANTS import background_options from utils.console import print_step, print_substep -# Supported Background. Can add/remove background video here.... -# - : key -> used as keyword for TOML file. value -> background configuration -# Format (value): -# 1. Youtube URI -# 2. filename -# 3. Citation (owner of the video) -# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) -background_options = { - "motor-gta": ( # Motor-GTA Racing - "https://www.youtube.com/watch?v=vw5L4xCPy9Q", - "bike-parkour-gta.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), - "rocket-league": ( # Rocket League - "https://www.youtube.com/watch?v=2X9QGY__0II", - "rocket_league.mp4", - "Orbital Gameplay", - lambda t: ("center", 200 + t), - ), - "minecraft": ( # Minecraft parkour - "https://www.youtube.com/watch?v=n_Dv4JMiwK8", - "parkour.mp4", - "bbswitzer", - "center", - ), - "gta": ( # GTA Stunt Race - "https://www.youtube.com/watch?v=qGa9kWREOnE", - "gta-stunt-race.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), -} - def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: """Generates a random interval of time to be used as the background of the video. From 3b5ab95720160298b275210e1ead357cf8fdc123 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 22:15:35 -0400 Subject: [PATCH 15/63] potentially fixes #978 --- video_creation/screenshot_downloader.py | 44 ++++++++++--------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 6fb9ef4..8010862 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -49,36 +49,29 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() - page.locator( - '[data-click-id="text"] button' - ).click() # Remove "Click to see nsfw" Button in Screenshot + + if page.locator('[data-click-id="text"] button').is_visible(): + page.locator('[data-click-id="text"] button').click() # Remove "Click to see nsfw" Button in Screenshot # translate code if settings.config["reddit"]["thread"]["post_lang"]: print_substep("Translating post...") - texts_in_tl = ts.google( - reddit_object["thread_title"], - to_language=settings.config["reddit"]["thread"]["post_lang"], - ) + texts_in_tl = ts.google(reddit_object["thread_title"], + to_language=settings.config["reddit"]["thread"]["post_lang"], ) page.evaluate( "tl_content => document.querySelector('[data-test-id=\"post-content\"] > div:nth-child(3) > div > div').textContent = tl_content", - texts_in_tl, - ) + texts_in_tl, ) else: print_substep("Skipping translation...") page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png") if storymode: - page.locator('[data-click-id="text"]').screenshot( - path="assets/temp/png/story_content.png" - ) + page.locator('[data-click-id="text"]').screenshot(path="assets/temp/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 @@ -91,17 +84,16 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in # translate code if settings.config["reddit"]["thread"]["post_lang"]: - comment_tl = ts.google( - comment["comment_body"], - to_language=settings.config["reddit"]["thread"]["post_lang"], - ) + comment_tl = ts.google(comment["comment_body"], + 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"]], - ) - - page.locator(f"#t1_{comment['comment_id']}").screenshot( - path=f"assets/temp/png/comment_{idx}.png" - ) - + [comment_tl, comment["comment_id"]], ) + try: + page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/png/comment_{idx}.png") + except TimeoutError: + del reddit_object["comments"] + screenshot_num += 1 + print('TimeoutError: Skipping screenshot...') + continue print_substep("Screenshots downloaded Successfully.", style="bold green") From 78ff605c42ce058d71304710822b481ee1afd61c Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Jul 2022 22:20:17 -0400 Subject: [PATCH 16/63] everything works except for changes the location --- TTS/engine_wrapper.py | 2 +- requirements.txt | 2 ++ utils/video.py | 38 ++++++++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 213defb..93da092 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ 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 +DEFAULT_MAX_LENGTH: int = 10 # video length variable class TTSEngine: diff --git a/requirements.txt b/requirements.txt index e57a37d..7bccd0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ requests==2.28.1 rich==12.5.1 toml==0.10.2 translators==5.3.1 + +Pillow~=9.1.1 diff --git a/utils/video.py b/utils/video.py index 2cceae1..61dc41c 100644 --- a/utils/video.py +++ b/utils/video.py @@ -2,26 +2,44 @@ from __future__ import annotations from typing import Tuple -from moviepy.video.VideoClip import VideoClip, TextClip -from moviepy.video.io.VideoFileClip import VideoFileClip +from PIL import ImageFont, Image, ImageDraw, ImageEnhance +from moviepy.video.VideoClip import VideoClip, ImageClip from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip class Video: - def __init__(self, video: VideoClip | VideoFileClip, *args, **kwargs): + def __init__(self, video: VideoClip, *args, **kwargs): self.video: VideoClip = video self.fps = self.video.fps self.duration = self.video.duration @staticmethod def _create_watermark(text, fontsize, opacity=0.5): - txt_clip = TextClip(text, fontsize=fontsize, color='black').set_opacity(opacity) - return txt_clip + path = "./assets/temp/png/watermark.png" + width = int(fontsize * len(text)) + height = int(fontsize * len(text) / 2) - def add_watermark(self, text, opacity=0.5, position: Tuple = (0.95, 0.95), fontsize=15): - txt_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) - txt_clip = txt_clip.set_pos(position).set_duration(10) + white = (255, 255, 255) + transparent = (0, 0, 0, 0) - # Overlay the text clip on the first video clip - self.video = CompositeVideoClip([self.video, txt_clip]) + font = ImageFont.load_default() + wm = Image.new("RGBA", (width, height), transparent) + im = Image.new("RGBA", (width, height), transparent) # Change this line too. + + draw = ImageDraw.Draw(wm) + w, h = draw.textsize(text, font) + draw.text(((width - w) / 2, (height - h) / 2), text, white, font) + en = ImageEnhance.Brightness(wm) # todo allow it to use the fontsize + mask = en.enhance(1 - opacity) + im.paste(wm, (25, 25), mask) + im.save(path) + return ImageClip(path) + + def add_watermark(self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (1, 100), fontsize=15): + img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) + img_clip = img_clip.set_opacity(opacity).set_duration(duration) + img_clip = img_clip.set_position(("center","bottom")) # set position doesn't work for some reason # todo fix + + # Overlay the img clip on the first video clip + self.video = CompositeVideoClip([self.video, img_clip]) return self.video From cbfdfc3ce762d6a5546e57c010a7861bbf92b6cd Mon Sep 17 00:00:00 2001 From: final Date: Thu, 14 Jul 2022 15:00:39 -0500 Subject: [PATCH 17/63] Fixed video being too long error. --- TTS/engine_wrapper.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index c5ed714..c5cc86c 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -36,12 +36,14 @@ class TTSEngine: reddit_object: dict, path: str = "assets/temp/mp3", max_length: int = DEFUALT_MAX_LENGTH, + last_clip_length: int = 0, ): self.tts_module = tts_module() self.reddit_object = reddit_object self.path = path self.max_length = max_length self.length = 0 + self.last_clip_length = last_clip_length def run(self) -> Tuple[int, int]: @@ -63,9 +65,13 @@ class TTSEngine: self.call_tts("posttext", self.reddit_object["thread_post"]) idx = None - for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + for idx, comment in track( + enumerate(self.reddit_object["comments"]), "Saving..." + ): # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length: + self.length -= self.last_clip_length + idx -= 1 break if ( len(comment["comment_body"]) > self.tts_module.max_chars @@ -93,7 +99,9 @@ class TTSEngine: continue self.call_tts(f"{idx}-{idy - offset}.part", text_cut) - split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3")) + 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 @@ -110,13 +118,17 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run(text=process_text(text), filepath=f"{self.path}/{filename}.mp3") + self.tts_module.run( + text=process_text(text), filepath=f"{self.path}/{filename}.mp3" + ) # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") try: clip = AudioFileClip(f"{self.path}/{filename}.mp3") + if clip.duration + self.length < self.max_length: + self.last_clip_length = clip.duration self.length += clip.duration clip.close() except: From 7ef9bb2747ce592a01416a219ea55e0d75d02acd Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 18:49:47 -0400 Subject: [PATCH 18/63] sugarcoated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a9e205..0530b2b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5- ## Contributing & Ways to improve 📈 -In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made. +In its current state, this bot does exactly what it needs to do. However, improvements can always be made! I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute! From 839eb6fc2ffc08149145fdc800fbd0f87fa5128d Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 19:36:35 -0400 Subject: [PATCH 19/63] added a potential bypass in case of absolute mad lads --- utils/subreddit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/utils/subreddit.py b/utils/subreddit.py index 4eb0108..5638f1e 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -5,7 +5,7 @@ from utils import settings from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0): """_summary_ Args: @@ -41,9 +41,13 @@ def get_subreddit_undone(submissions: list, subreddit): continue return submission print("all submissions have been done going by top submission order") + VALID_TIME_FILTERS = ["day", "hour", "month", "week", "year", 'all'] # set doesn't have __getitem__ + index = times_checked + 1 + if index == len(VALID_TIME_FILTERS): + print("all time filters have been checked you absolute madlad ") + return get_subreddit_undone( - subreddit.top(time_filter="hour"), subreddit - ) # all the videos in hot have already been done + 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 def already_done(done_videos: list, submission) -> bool: From bc215f8b3218a7ef1025f5c5dd1ad17c21307ec8 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 22:35:41 -0400 Subject: [PATCH 20/63] Stable implementation of the watermark NOT DESIGNED FOR PRODUCTION USE --- TTS/engine_wrapper.py | 2 +- main.py | 27 ++++++++++++++----- .../.config.template.toml | 0 utils/settings.py | 2 +- utils/video.py | 10 ++++--- 5 files changed, 30 insertions(+), 11 deletions(-) rename .config.template.toml => utils/.config.template.toml (100%) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 93da092..e22c96b 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFAULT_MAX_LENGTH: int = 10 # video length variable +DEFAULT_MAX_LENGTH: int = 5 # video length variable todo change class TTSEngine: diff --git a/main.py b/main.py index 653703c..adc478d 100755 --- a/main.py +++ b/main.py @@ -2,6 +2,9 @@ import math from subprocess import Popen from os import name + +from prawcore import ResponseException + from reddit.subreddit import get_subreddit_threads from utils.cleanup import cleanup from utils.console import print_markdown, print_step @@ -51,14 +54,20 @@ def main(POST_ID=None): def run_many(times): for x in range(1, times + 1): print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th","th", "th")[x%10]} iteration of {times}' + f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' ) # correct 1st 2nd 3rd 4th 5th.... main() Popen("cls" if name == "nt" else "clear", shell=True).wait() +def shutdown(): + print_markdown("## Clearing temp files") + cleanup() + exit() + + if __name__ == "__main__": - config = settings.check_toml(".config.template.toml", "config.toml") + config = settings.check_toml("utils/.config.template.toml", "config.toml") config is False and exit() try: if config["settings"]["times_to_run"]: @@ -68,13 +77,19 @@ if __name__ == "__main__": for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( - f'on the {index}{("st" if index%10 == 1 else ("nd" if index%10 == 2 else ("rd" if index%10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' + f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' ) main(post_id) Popen("cls" if name == "nt" else "clear", shell=True).wait() else: main() except KeyboardInterrupt: - print_markdown("## Clearing temp files") - cleanup() - exit() + shutdown() + except ResponseException: + # error for invalid credentials + print_markdown("## Invalid credentials") + print_markdown("Please check your credentials in the config.toml file") + + shutdown() + + # todo error diff --git a/.config.template.toml b/utils/.config.template.toml similarity index 100% rename from .config.template.toml rename to utils/.config.template.toml diff --git a/utils/settings.py b/utils/settings.py index a36f63e..a9d7726 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -167,4 +167,4 @@ If you see any prompts, that means that you have unset/incorrectly set variables if __name__ == "__main__": - check_toml(".config.template.toml", "config.toml") + check_toml("utils/.config.template.toml", "config.toml") diff --git a/utils/video.py b/utils/video.py index 61dc41c..5c89e3b 100644 --- a/utils/video.py +++ b/utils/video.py @@ -18,7 +18,6 @@ class Video: path = "./assets/temp/png/watermark.png" width = int(fontsize * len(text)) height = int(fontsize * len(text) / 2) - white = (255, 255, 255) transparent = (0, 0, 0, 0) @@ -35,10 +34,15 @@ class Video: im.save(path) return ImageClip(path) - def add_watermark(self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (1, 100), fontsize=15): + def add_watermark(self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15): + print(len(text)) + compensation = round((position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), ndigits=2) + position = (compensation, position[1]) + print(f'{compensation=}') + print(f'{position=}') img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) img_clip = img_clip.set_opacity(opacity).set_duration(duration) - img_clip = img_clip.set_position(("center","bottom")) # set position doesn't work for some reason # todo fix + img_clip = img_clip.set_position(position, relative=True) # set position doesn't work for some reason # fixme # Overlay the img clip on the first video clip self.video = CompositeVideoClip([self.video, img_clip]) From d5d8cb17219272c62a42714675a2c27b386d267f Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 22:42:04 -0400 Subject: [PATCH 21/63] fix: typo --- video_creation/background.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video_creation/background.py b/video_creation/background.py index 19ded06..7a577e6 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -58,7 +58,7 @@ def download_background(background_config: Tuple[str, str, str, Any]): YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( "assets/backgrounds", filename=f"{credit}-{filename}" ) - print_substep("Background videos downloaded successfully! 🎉", style="bold green") + print_substep("Background video downloaded successfully! 🎉", style="bold green") def chop_background_video(background_config: Tuple[str, str, str, Any], video_length: int): From 420bac9a5a16d7d5ea22e4f24dfd5a8dabf1eeef Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 22:44:13 -0400 Subject: [PATCH 22/63] fix: typo reverted temp change --- TTS/engine_wrapper.py | 2 +- utils/video.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index e22c96b..213defb 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFAULT_MAX_LENGTH: int = 5 # video length variable todo change +DEFAULT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: diff --git a/utils/video.py b/utils/video.py index 5c89e3b..03ed88c 100644 --- a/utils/video.py +++ b/utils/video.py @@ -35,14 +35,13 @@ class Video: return ImageClip(path) def add_watermark(self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15): - print(len(text)) compensation = round((position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), ndigits=2) position = (compensation, position[1]) - print(f'{compensation=}') - print(f'{position=}') + #print(f'{compensation=}') + #print(f'{position=}') img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) img_clip = img_clip.set_opacity(opacity).set_duration(duration) - img_clip = img_clip.set_position(position, relative=True) # set position doesn't work for some reason # fixme + img_clip = img_clip.set_position(position, relative=True) # todo get dara from utils/CONSTANTS.py and adapt position accordingly # Overlay the img clip on the first video clip self.video = CompositeVideoClip([self.video, img_clip]) From 4abf80317039b11be240e7aa0c91b01c1c9ed42b Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 22:45:21 -0400 Subject: [PATCH 23/63] reformatted code --- utils/CONSTANTS.py | 1 - utils/subreddit.py | 16 +++++++++-- utils/video.py | 17 ++++++++---- video_creation/background.py | 1 + video_creation/final_video.py | 1 + video_creation/screenshot_downloader.py | 36 +++++++++++++++++-------- 6 files changed, 53 insertions(+), 19 deletions(-) diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py index 76d2ee4..e46ebbc 100644 --- a/utils/CONSTANTS.py +++ b/utils/CONSTANTS.py @@ -1,4 +1,3 @@ - # Supported Background. Can add/remove background video here.... # - : key -> used as keyword for TOML file. value -> background configuration # Format (value): diff --git a/utils/subreddit.py b/utils/subreddit.py index 5638f1e..c386868 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -41,13 +41,25 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): continue return submission print("all submissions have been done going by top submission order") - VALID_TIME_FILTERS = ["day", "hour", "month", "week", "year", 'all'] # set doesn't have __getitem__ + VALID_TIME_FILTERS = [ + "day", + "hour", + "month", + "week", + "year", + "all", + ] # set doesn't have __getitem__ index = times_checked + 1 if index == len(VALID_TIME_FILTERS): print("all time filters have been checked you absolute madlad ") return get_subreddit_undone( - subreddit.top(time_filter=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 + 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 def already_done(done_videos: list, submission) -> bool: diff --git a/utils/video.py b/utils/video.py index 03ed88c..63dc170 100644 --- a/utils/video.py +++ b/utils/video.py @@ -34,14 +34,21 @@ class Video: im.save(path) return ImageClip(path) - def add_watermark(self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15): - compensation = round((position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), ndigits=2) + def add_watermark( + self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 + ): + compensation = round( + (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), + ndigits=2, + ) position = (compensation, position[1]) - #print(f'{compensation=}') - #print(f'{position=}') + # print(f'{compensation=}') + # print(f'{position=}') img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) img_clip = img_clip.set_opacity(opacity).set_duration(duration) - img_clip = img_clip.set_position(position, relative=True) # todo get dara from utils/CONSTANTS.py and adapt position accordingly + img_clip = img_clip.set_position( + position, relative=True + ) # todo get dara from utils/CONSTANTS.py and adapt position accordingly # Overlay the img clip on the first video clip self.video = CompositeVideoClip([self.video, img_clip]) diff --git a/video_creation/background.py b/video_creation/background.py index 7a577e6..6e656fa 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -13,6 +13,7 @@ from utils import settings from utils.CONSTANTS import background_options from utils.console import print_step, print_substep + 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. diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 3157296..8d12afe 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -35,6 +35,7 @@ def name_normalize(name: str) -> str: lang = settings.config["reddit"]["thread"]["post_lang"] if lang: import translators as ts + print_substep("Translating filename...") translated_name = ts.google(name, to_language=lang) return translated_name diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 8010862..1f28c74 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -51,27 +51,36 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in page.locator('[data-testid="content-gate"] button').click() 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 if settings.config["reddit"]["thread"]["post_lang"]: print_substep("Translating post...") - texts_in_tl = ts.google(reddit_object["thread_title"], - to_language=settings.config["reddit"]["thread"]["post_lang"], ) + texts_in_tl = ts.google( + reddit_object["thread_title"], + to_language=settings.config["reddit"]["thread"]["post_lang"], + ) page.evaluate( "tl_content => document.querySelector('[data-test-id=\"post-content\"] > div:nth-child(3) > div > div').textContent = tl_content", - texts_in_tl, ) + texts_in_tl, + ) else: print_substep("Skipping translation...") page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png") if storymode: - page.locator('[data-click-id="text"]').screenshot(path="assets/temp/png/story_content.png") + page.locator('[data-click-id="text"]').screenshot( + path="assets/temp/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 @@ -84,16 +93,21 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in # translate code if settings.config["reddit"]["thread"]["post_lang"]: - comment_tl = ts.google(comment["comment_body"], - to_language=settings.config["reddit"]["thread"]["post_lang"], ) + comment_tl = ts.google( + comment["comment_body"], + 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"]], ) + [comment_tl, comment["comment_id"]], + ) try: - page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/png/comment_{idx}.png") + page.locator(f"#t1_{comment['comment_id']}").screenshot( + path=f"assets/temp/png/comment_{idx}.png" + ) except TimeoutError: del reddit_object["comments"] screenshot_num += 1 - print('TimeoutError: Skipping screenshot...') + print("TimeoutError: Skipping screenshot...") continue print_substep("Screenshots downloaded Successfully.", style="bold green") From 45c840158933ea8e6fb23b47641256eb3b0565b2 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 14 Jul 2022 22:57:26 -0400 Subject: [PATCH 24/63] fix: typo left in causing name error --- TTS/engine_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index bad4495..267f47a 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -35,7 +35,7 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFUALT_MAX_LENGTH, + max_length: int = DEFAULT_MAX_LENGTH, last_clip_length: int = 0, ): self.tts_module = tts_module() From f0e1bdc48a209a712b387c0b8dfef964fdebf663 Mon Sep 17 00:00:00 2001 From: RiveN000 <61630074+RiveN000@users.noreply.github.com> Date: Fri, 15 Jul 2022 12:59:21 +0200 Subject: [PATCH 25/63] Fix: Hide the "Click to see NSFW" button Fixed this issue by making Playwright wait for page to fully load before clicking the button. --- video_creation/screenshot_downloader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 1f28c74..7898e8d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -49,6 +49,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() + page.wait_for_load_state() # Wait for page to fully load if page.locator('[data-click-id="text"] button').is_visible(): page.locator( From 5920f0d77304e7bde3d325d16c75225390ede4fb Mon Sep 17 00:00:00 2001 From: RocketNinja15 <50027436+RocketNinja15@users.noreply.github.com> Date: Fri, 15 Jul 2022 10:29:36 -0700 Subject: [PATCH 26/63] Documentation for Reddit Script Instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a9e205..db91958 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ On MacOS and Linux (debian, arch, fedora and centos, and based on those), you ca This can also be used to update the installation 4. Run `python main.py` -5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". +5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:localhost:8080 6. The bot will ask you to fill in your details to connect to the Reddit API, and configure the bot to your liking 7. Enjoy 😎 8. If you need to reconfigure the bot, simply open the `config.toml` file and delete the lines that need to be changed. On the next run of the bot, it will help you reconfigure those options. From 18e5727859cac0a6c06fe5b3ba318f497ba4db84 Mon Sep 17 00:00:00 2001 From: RocketNinja15 <50027436+RocketNinja15@users.noreply.github.com> Date: Sat, 16 Jul 2022 01:45:11 -0700 Subject: [PATCH 27/63] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db91958..f819920 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ On MacOS and Linux (debian, arch, fedora and centos, and based on those), you ca This can also be used to update the installation 4. Run `python main.py` -5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:localhost:8080 +5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:google.com 6. The bot will ask you to fill in your details to connect to the Reddit API, and configure the bot to your liking 7. Enjoy 😎 8. If you need to reconfigure the bot, simply open the `config.toml` file and delete the lines that need to be changed. On the next run of the bot, it will help you reconfigure those options. From 2f5b0ca490090c1fdfa6a6f539f396ee7bcd48dd Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Mon, 18 Jul 2022 16:43:39 +0200 Subject: [PATCH 28/63] fix AWS_Polly TTS max_chars previously aws TTS did not read out comments correctly and only made weird sounds with super short mp3s. This fixes the problem and allows the full comment to be read. --- TTS/aws_polly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index efd762b..1f20ed4 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -26,7 +26,7 @@ voices = [ class AWSPolly: def __init__(self): - self.max_chars = 0 + self.max_chars = 100000 self.voices = voices def run(self, text, filepath, random_voice: bool = False): From e347c21c10926860524ced184b115e938ae2beb9 Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Mon, 18 Jul 2022 16:47:24 +0200 Subject: [PATCH 29/63] allow numbers in reddit post_id --- utils/.config.template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 9fd779e..67160c9 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -13,7 +13,7 @@ random = { optional = true, options = [true, false, ], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } -post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } +post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z0-9])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr" } min_comments = { default = 20, optional = false, nmin = 15, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } From 4882feb9195326aa7aaa977fb688e82092f118bb Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Jul 2022 12:48:23 -0400 Subject: [PATCH 30/63] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8a9e205..54f387d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +# I am now pushing directly to https://github.com/elebumm/RedditVideoMakerBot GO THERE. + + # Reddit Video Maker Bot 🎥 All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨. From 771452544ba0be3d0bb71f3cecd7fe491fff5717 Mon Sep 17 00:00:00 2001 From: PatatjeMC Date: Tue, 19 Jul 2022 16:16:37 +0200 Subject: [PATCH 31/63] Fixed glitchy ending Fixed the issue where the ending freezes for multiple seconds (again cause someone broke my previous fix) --- video_creation/final_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8524051..6a53c98 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -141,7 +141,7 @@ def make_final_video( ffmpeg_extract_subclip( "assets/temp/temp.mp4", 0, - final.duration, + length, targetname=f"results/{subreddit}/{filename}", ) save_data(subreddit, filename, title, idx, background_config[2]) From 36851dd07b35426d47a58637adc3d0c6ffecfb05 Mon Sep 17 00:00:00 2001 From: PatatjeMC Date: Tue, 19 Jul 2022 16:20:10 +0200 Subject: [PATCH 32/63] Revert "Merge branch 'master' of https://github.com/PatatjeMC/RedditVideoMakerBot" This reverts commit 99656fad6532139b25c661d959c627d83737a056, reversing changes made to 771452544ba0be3d0bb71f3cecd7fe491fff5717. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 54f387d..8a9e205 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -# I am now pushing directly to https://github.com/elebumm/RedditVideoMakerBot GO THERE. - - # Reddit Video Maker Bot 🎥 All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨. From 85beecae20c1c0f874ffb90fac63d43c50ccbdde Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Tue, 19 Jul 2022 17:47:25 +0200 Subject: [PATCH 33/63] use correct max_chars for synchronous api call --- TTS/aws_polly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index 1f20ed4..eac5884 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -26,7 +26,7 @@ voices = [ class AWSPolly: def __init__(self): - self.max_chars = 100000 + self.max_chars = 3000 self.voices = voices def run(self, text, filepath, random_voice: bool = False): From 3e084cd8bc4fd94ee18b472fd76c6a9d1d649233 Mon Sep 17 00:00:00 2001 From: RiveN000 <61630074+RiveN000@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:31:53 +0200 Subject: [PATCH 34/63] Created option to add transition between comments Created an option to add a transition (fadein and fadeout) between the images of comments. It makes videos look much more pleasant. --- utils/.config.template.toml | 1 + video_creation/final_video.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 9fd779e..102a813 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -26,6 +26,7 @@ theme = { optional = false, default = "dark", example = "light", options = ["dar ], 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 = "not yet implemented" } diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8d12afe..502c8fc 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -66,6 +66,7 @@ def make_final_video( VideoFileClip.reW = lambda clip: clip.resize(width=W) VideoFileClip.reH = lambda clip: clip.resize(width=H) opacity = settings.config["settings"]["opacity"] + transition = settings.config["settings"]["transition"] background_clip = ( VideoFileClip("assets/temp/background.mp4") .without_audio() @@ -84,12 +85,15 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) + new_transition = 0 if transition is None or float(transition) > 2 else float(transition) image_clips.insert( 0, ImageClip("assets/temp/png/title.png") .set_duration(audio_clips[0].duration) .resize(width=W - 100) - .set_opacity(new_opacity), + .set_opacity(new_opacity) + .crossfadein(new_transition) + .crossfadeout(new_transition), ) for i in range(0, number_of_clips): @@ -98,6 +102,8 @@ def make_final_video( .set_duration(audio_clips[i + 1].duration) .resize(width=W - 100) .set_opacity(new_opacity) + .crossfadein(new_transition) + .crossfadeout(new_transition) ) # if os.path.exists("assets/mp3/posttext.mp3"): From 8e50522c8046ff0568c9a3773cf36f78cbf2daef Mon Sep 17 00:00:00 2001 From: micziz Date: Tue, 19 Jul 2022 22:06:55 +0200 Subject: [PATCH 35/63] Change the install script 1 --- install.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index 782069d..53577de 100644 --- a/install.sh +++ b/install.sh @@ -92,7 +92,10 @@ function install_centos(){ function get_the_bot(){ echo "Downloading the bot" - git clone https://github.com/elebumm/RedditVideoMakerBot.git + rm -rf RedditVideoMakerBot-master + curl -sL https://github.com/elebumm/RedditVideoMakerBot/archive/refs/heads/master.zip -o master.zip + unzip master.zip + rm -rf master.zip } #install python dependencies @@ -100,7 +103,7 @@ function install_python_dep(){ # tell the user that the script is going to install the python dependencies echo "Installing python dependencies" # cd into the directory - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master # install the dependencies pip3 install -r requirements.txt # cd out @@ -112,7 +115,7 @@ function install_playwright(){ # tell the user that the script is going to install playwright echo "Installing playwright" # cd into the directory where the script is downloaded - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master # run the install script python3 -m playwright install python3 -m playwright install-deps @@ -198,9 +201,9 @@ function install_main(){ install_playwright fi - DIR="./RedditVideoMakerBot" + DIR="./RedditVideoMakerBot-master" if [ -d "$DIR" ]; then - printf "\nThe bot is already installed, want to run it?" + printf "\nThe bot is installed, want to run it?" # if -y (assume yes) continue if [[ ASSUME_YES -eq 1 ]]; then echo "Assuming yes" @@ -214,7 +217,7 @@ function install_main(){ exit 1 fi fi - cd RedditVideoMakerBot + cd RedditVideoMakerBot-master python3 main.py fi } From 38fc12ed5c5af33b68add5a0c4ff1736de20f421 Mon Sep 17 00:00:00 2001 From: micziz Date: Tue, 19 Jul 2022 22:07:50 +0200 Subject: [PATCH 36/63] Change the install script 2 --- install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 53577de..f85c80b 100644 --- a/install.sh +++ b/install.sh @@ -68,26 +68,26 @@ function install_macos(){ # Function to install for arch (and other forks like manjaro) function install_arch(){ echo "Installing required packages" - sudo pacman -S --needed python3 tk git && python3 -m ensurepip || install_fail + sudo pacman -S --needed python3 tk git && python3 -m ensurepip unzip || install_fail } # Function to install for debian (and ubuntu) function install_deb(){ echo "Installing required packages" - sudo apt install python3 python3-dev python3-tk python3-pip git || install_fail + sudo apt install python3 python3-dev python3-tk python3-pip unzip || install_fail } # Function to install for fedora (and other forks) function install_fedora(){ echo "Installing required packages" - sudo dnf install python3 python3-tkinter python3-pip git python3-devel || install_fail + sudo dnf install python3 python3-tkinter python3-pip python3-devel unzip || install_fail } # Function to install for centos (and other forks based on it) function install_centos(){ echo "Installing required packages" sudo yum install -y python3 || install_fail - sudo yum install -y python3-tkinter epel-release python3-pip git || install_fail + sudo yum install -y python3-tkinter epel-release python3-pip unzip|| install_fail } function get_the_bot(){ From 1685b55e6c0fa2538c7dbd29e41b02d18238040c Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Wed, 20 Jul 2022 09:39:54 +0200 Subject: [PATCH 37/63] add max_chars to GTTS, fixing #1036 --- TTS/GTTS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/GTTS.py b/TTS/GTTS.py index 31e29df..cef1b24 100644 --- a/TTS/GTTS.py +++ b/TTS/GTTS.py @@ -8,7 +8,7 @@ max_chars = 0 class GTTS: def __init__(self): - self.max_chars = 0 + self.max_chars = 5000 self.voices = [] def run(self, text, filepath): From b0fa6616248ac9df93b533c3e50f127f328ec29b Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Wed, 20 Jul 2022 12:57:52 +0200 Subject: [PATCH 38/63] prevent last comment being cut offin the middle --- TTS/engine_wrapper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 267f47a..65c1453 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -127,8 +127,7 @@ class TTSEngine: # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") try: clip = AudioFileClip(f"{self.path}/{filename}.mp3") - if clip.duration + self.length < self.max_length: - self.last_clip_length = clip.duration + self.last_clip_length = clip.duration self.length += clip.duration clip.close() except: From ffda658790004809473c04ded167e94cf0185bb4 Mon Sep 17 00:00:00 2001 From: Ketan Chowdhury Date: Thu, 21 Jul 2022 10:26:08 +0530 Subject: [PATCH 39/63] Edited main.py and CONTRIBUTING.md Added a line to code to the shutdown function to inform the user of exiting the program upon calling the function. And, Also wrote a section of CONTRIBUTING.md to help beginner developers to setup their fork of the repo as many beginners to github might not know how to setup this up. --- CONTRIBUTING.md | 21 ++++++++++++++++++++- main.py | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca1c7cb..7f59dfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,25 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/elebum You development environment should follow the requirements stated in the [README file](README.md). If you are not using the specified versions, **please reference this in your pull request**, so reviewers can test your code on both versions. +#### Setting up your development repository + +These steps are only specified for beginner developers trying to contribute to this repository. +If you know how to make a fork and clone, you can skip these steps. + +Before contributing, follow these steps (if you are a beginner) + +- Create a fork of this repository to your personal account +- Clone the repo to your computer +- Make sure that you have all dependencies installed +- Run `python3 main.py` to make sure that the program is working +- Now, you are all setup to contribute your own features to this repo! + +Even if you are a beginner to working with python or contributing to open source software, +don't worry! You can still try contributing even to the documentation! + +("Setting up your development repository" was written by a beginner developer themselves!) + + #### Making your first PR When making your PR, follow these guidelines: @@ -114,7 +133,7 @@ When making your PR, follow these guidelines: - You link any issues that are resolved or fixed by your changes. (this is done by typing "Fixes #\") in your pull request - Where possible, you have used `git pull --rebase`, to avoid creating unnecessary merge commits - You have meaningful commits, and if possible, follow the commit style guide of `type: explanation` -- Here are the commit types: +- Here are the commit types: - **feat** - a new feature - **fix** - a bug fix - **docs** - a change to documentation / commenting diff --git a/main.py b/main.py index adc478d..a419e45 100755 --- a/main.py +++ b/main.py @@ -62,6 +62,8 @@ def run_many(times): def shutdown(): print_markdown("## Clearing temp files") + # Informs the user of exiting the program + print("Exiting...") cleanup() exit() From 81b20f5e13ad990d11b090a832b28cd20c140239 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 21 Jul 2022 21:23:41 -0400 Subject: [PATCH 40/63] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f59dfd..8fc7d3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ + # Contributing to Reddit Video Maker Bot 🎥 Thanks for taking the time to contribute! ❤️ @@ -115,7 +116,7 @@ Before contributing, follow these steps (if you are a beginner) - Create a fork of this repository to your personal account - Clone the repo to your computer - Make sure that you have all dependencies installed -- Run `python3 main.py` to make sure that the program is working +- Run `python main.py` to make sure that the program is working - Now, you are all setup to contribute your own features to this repo! Even if you are a beginner to working with python or contributing to open source software, From 43f4af9b06c7bf8396bde00b3cccb7ffb171fe8f Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 22 Jul 2022 08:56:51 -0400 Subject: [PATCH 41/63] Update main.py Put the format correctly --- main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.py b/main.py index a419e45..6725540 100755 --- a/main.py +++ b/main.py @@ -62,9 +62,8 @@ def run_many(times): def shutdown(): print_markdown("## Clearing temp files") - # Informs the user of exiting the program - print("Exiting...") cleanup() + print("Exiting...") exit() From 571c78202168eece080597e366d0347580caff7b Mon Sep 17 00:00:00 2001 From: Josh Greenwood <31244208+joshgreenwood2003@users.noreply.github.com> Date: Sat, 23 Jul 2022 17:00:34 +0100 Subject: [PATCH 42/63] process split text before checking if it is empty --- TTS/engine_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 267f47a..354f467 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -94,7 +94,7 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - if not text_cut or text_cut.isspace(): + if not process_text(text_cut) or process_text(text_cut.isspace()): offset += 1 continue From 9065f563f17a5d5a0cd8372d7eb3bce9c2da2cb9 Mon Sep 17 00:00:00 2001 From: Josh Greenwood <31244208+joshgreenwood2003@users.noreply.github.com> Date: Sat, 23 Jul 2022 23:33:18 +0100 Subject: [PATCH 43/63] improved efficiency of empty text check --- TTS/engine_wrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 354f467..a415680 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -94,11 +94,12 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - if not process_text(text_cut) or process_text(text_cut.isspace()): + new_text = process_text(text_cut) + if not new_text or new_text.isspace(): offset += 1 continue - self.call_tts(f"{idx}-{idy - offset}.part", text_cut) + self.call_tts(f"{idx}-{idy - offset}.part", new_text) split_files.append( AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3") ) @@ -119,7 +120,7 @@ class TTSEngine: def call_tts(self, filename: str, text: str): self.tts_module.run( - text=process_text(text), filepath=f"{self.path}/{filename}.mp3" + text, filepath=f"{self.path}/{filename}.mp3" ) # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length From 53986c72ee9fb6154ac69784f4b3e4f1286b8fca Mon Sep 17 00:00:00 2001 From: Josh Greenwood <31244208+joshgreenwood2003@users.noreply.github.com> Date: Sun, 24 Jul 2022 01:37:10 +0100 Subject: [PATCH 44/63] process text before all calls to call_tts Process_text now called before call_tts to ensure it is called minimally while allowing for verification that text is not empty --- TTS/engine_wrapper.py | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index a415680..7eb0501 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ 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 +DEFUALT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: @@ -35,15 +35,13 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFAULT_MAX_LENGTH, - last_clip_length: int = 0, + max_length: int = DEFUALT_MAX_LENGTH, ): self.tts_module = tts_module() self.reddit_object = reddit_object self.path = path self.max_length = max_length self.length = 0 - self.last_clip_length = last_clip_length def run(self) -> Tuple[int, int]: @@ -57,28 +55,25 @@ class TTSEngine: print_step("Saving Text to MP3 files...") - self.call_tts("title", self.reddit_object["thread_title"]) + self.call_tts("title", process_text(self.reddit_object["thread_title"])) + processed_text = process_text(self.reddit_object["thread_post"]) if ( - self.reddit_object["thread_post"] != "" + processed_text != "" and settings.config["settings"]["storymode"] == True ): - self.call_tts("posttext", self.reddit_object["thread_post"]) + self.call_tts("posttext", processed_text) idx = None - for idx, comment in track( - enumerate(self.reddit_object["comments"]), "Saving..." - ): + for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length: - self.length -= self.last_clip_length - idx -= 1 break if ( len(comment["comment_body"]) > self.tts_module.max_chars ): # Split the comment if it is too long self.split_post(comment["comment_body"], idx) # Split the comment else: # If the comment is not too long, just call the tts engine - self.call_tts(f"{idx}", comment["comment_body"]) + self.call_tts(f"{idx}", process_text(comment["comment_body"])) print_substep("Saved Text to MP3 files successfully.", style="bold green") return self.length, idx @@ -94,15 +89,13 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - new_text = process_text(text_cut) + 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") - ) + 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 @@ -119,17 +112,13 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run( - text, filepath=f"{self.path}/{filename}.mp3" - ) + self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") try: clip = AudioFileClip(f"{self.path}/{filename}.mp3") - if clip.duration + self.length < self.max_length: - self.last_clip_length = clip.duration self.length += clip.duration clip.close() except: From 7df0671b70d76837da1b31c198023f88bf90f854 Mon Sep 17 00:00:00 2001 From: Josh Greenwood <31244208+joshgreenwood2003@users.noreply.github.com> Date: Sun, 24 Jul 2022 01:47:14 +0100 Subject: [PATCH 45/63] formatting --- TTS/engine_wrapper.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 7eb0501..6d09c7a 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -13,7 +13,7 @@ from utils.console import print_step, print_substep from utils.voice import sanitize_text from utils import settings -DEFUALT_MAX_LENGTH: int = 50 # video length variable +DEFAULT_MAX_LENGTH: int = 50 # video length variable class TTSEngine: @@ -35,13 +35,15 @@ class TTSEngine: tts_module, reddit_object: dict, path: str = "assets/temp/mp3", - max_length: int = DEFUALT_MAX_LENGTH, + max_length: int = DEFAULT_MAX_LENGTH, + last_clip_length: int = 0, ): self.tts_module = tts_module() self.reddit_object = reddit_object self.path = path self.max_length = max_length self.length = 0 + self.last_clip_length = last_clip_length def run(self) -> Tuple[int, int]: @@ -64,9 +66,13 @@ class TTSEngine: self.call_tts("posttext", processed_text) idx = None - for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): + for idx, comment in track( + enumerate(self.reddit_object["comments"]), "Saving..." + ): # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length: + self.length -= self.last_clip_length + idx -= 1 break if ( len(comment["comment_body"]) > self.tts_module.max_chars @@ -95,7 +101,9 @@ class TTSEngine: continue self.call_tts(f"{idx}-{idy - offset}.part", new_text) - split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3")) + 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 @@ -112,13 +120,17 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") + self.tts_module.run( + text, filepath=f"{self.path}/{filename}.mp3" + ) # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") try: clip = AudioFileClip(f"{self.path}/{filename}.mp3") + if clip.duration + self.length < self.max_length: + self.last_clip_length = clip.duration self.length += clip.duration clip.close() except: From c29f408fb6ca2b85e079eb768c5b45598c59b5c1 Mon Sep 17 00:00:00 2001 From: RiveN000 <61630074+RiveN000@users.noreply.github.com> Date: Tue, 26 Jul 2022 20:39:49 +0200 Subject: [PATCH 46/63] Fixed merge conflicts from #982 Co-Authored-By: Jacob Cuomo --- utils/.config.template.toml | 43 +++++++++++++++---------------------- video_creation/voices.py | 2 +- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index d783349..d49ffa3 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -1,46 +1,37 @@ [reddit.creds] -client_id = { optional = false, nmin = 12, nmax = 30, explanation = "the ID of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The ID should be over 12 and under 30 characters, double check your input." } -client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "the SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." } -username = { optional = false, nmin = 3, nmax = 20, explanation = "the username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" } -password = { optional = false, nmin = 8, explanation = "the password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" } -2fa = { optional = true, type = "bool", options = [true, - false, -], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true } +client_id = { optional = false, nmin = 12, nmax = 30, explanation = "The ID of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The ID should be over 12 and under 30 characters, double check your input." } +client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "The SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." } +username = { optional = false, nmin = 3, nmax = 20, explanation = "The username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" } +password = { optional = false, nmin = 8, explanation = "The password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" } +2fa = { optional = true, type = "bool", options = [true, false,], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true } [reddit.thread] -random = { optional = true, options = [true, - false, -], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } -subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } +random = { optional = true, options = [true, false,], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } +subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "What subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z0-9])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr" } min_comments = { default = 20, optional = false, nmin = 15, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } + + [settings] -allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, - false, -], explanation = "Whether to allow NSFW content, True or False" } -theme = { optional = false, default = "dark", example = "light", options = ["dark", - "light", -], explanation = "sets the Reddit theme, either LIGHT or DARK" } -times_to_run = { optional = false, default = 1, example = 2, explanation = "used if you want to run multiple times. set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } +allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to allow NSFW content, True or False" } +theme = { optional = false, default = "dark", example = "light", options = ["dark", "light",], explanation = "Sets the Reddit theme, either LIGHT or DARK" } +times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } 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 = "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 = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } -#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, -# false, -#], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } +background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video based on game name" } +#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } #background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" } [settings.tts] -choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], example = "streamlabspolly", explanation = "The backend used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } +voice_choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], 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" } diff --git a/video_creation/voices.py b/video_creation/voices.py index ac33dd7..066921c 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -33,7 +33,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: tuple[int,int]: (total length of the audio, the number of comments audio was generated for) """ - voice = settings.config["settings"]["tts"]["choice"] + voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: From 4b581127dd37f71a6307d9b46b1bb3047b44c69e Mon Sep 17 00:00:00 2001 From: RiveN000 <61630074+RiveN000@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:17:15 +0200 Subject: [PATCH 47/63] Changed background_choice example --- utils/.config.template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index d49ffa3..010a29b 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -25,7 +25,7 @@ storymode = { optional = true, type = "bool", default = false, example = false, [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", 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" } From c5fda9d3fe96b74d294d96c278f452abdb93fedc Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:41:02 +0530 Subject: [PATCH 48/63] added pyttsx option --- TTS/pyttsx.py | 38 +++++++++++++++++++++++++ ptt.py | 9 ++++++ utils/.config.template.toml | 4 ++- video_creation/data/videos.json | 19 ++++++++++++- video_creation/screenshot_downloader.py | 1 + video_creation/voices.py | 3 ++ 6 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 TTS/pyttsx.py create mode 100644 ptt.py diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py new file mode 100644 index 0000000..0e6d3bb --- /dev/null +++ b/TTS/pyttsx.py @@ -0,0 +1,38 @@ +import pyttsx3 +from utils import settings +import random + +max_chars = 0 + +#Uses the system voices, significantly faster than other tts + +class pyttsx: + + def __init__(self): + self.max_chars = 0 + self.voices = [] + + def run( + self, + text: str = "Python Text to Speech", + filepath: str = "assets/temp/mp3", + random_voice=False, + censor=False, + ): + voice_id = int(settings.config["settings"]["tts"]["python_voice"]) + voice_num = int(settings.config["settings"]["tts"]["python_voice_number"]) + for i in range(voice_num): + self.voices.append(i) + i=+1 + if random_voice: + voice_id = self.randomvoice() + else: + voice = voice_id + engine = pyttsx3.init() + voices = engine.getProperty('voices') + engine.setProperty('voice', voices[voice].id) #changing index changes voices but ony 0 and 1 are working here + engine.save_to_file(text, f"{filepath}") + engine.runAndWait() + + def randomvoice(self): + return random.choice(self.voices) \ No newline at end of file diff --git a/ptt.py b/ptt.py new file mode 100644 index 0000000..b82c8e0 --- /dev/null +++ b/ptt.py @@ -0,0 +1,9 @@ +import pyttsx3 +engine = pyttsx3.init() +voices = engine.getProperty('voices') +for voice in voices: + print(voice, voice.id) + engine.setProperty('voice', voice.id) + engine.say("Hello World!") + engine.runAndWait() + engine.stop() \ No newline at end of file diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 010a29b..20a163a 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -31,7 +31,9 @@ background_choice = { optional = true, default = "minecraft", example = "rocket- [settings.tts] -voice_choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], 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 = "", 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)"} +python_voice_number = {optional = false, default = "2", example = "2", explanation= "the number of system voices(2 are pre-installed in windows)"} diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json index fe51488..b851d3e 100644 --- a/video_creation/data/videos.json +++ b/video_creation/data/videos.json @@ -1 +1,18 @@ -[] +[ + { + "subreddit": "tifu", + "id": "w97htn", + "time": "1658942748", + "background_credit": "Achy Gaming", + "reddit_title": "TIFU by refusing to give tea to a Chinese senator", + "filename": "TIFU by refusing to give tea to a Chinese senator.mp4" + }, + { + "subreddit": "tifu", + "id": "w9iqby", + "time": "1658945240", + "background_credit": "Achy Gaming", + "reddit_title": "TIFU by giving my boss a RedBull", + "filename": "TIFU by giving my boss a RedBull.mp4" + } +] \ No newline at end of file diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 7898e8d..4574675 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -1,3 +1,4 @@ +from distutils.command.config import config import json from pathlib import Path diff --git a/video_creation/voices.py b/video_creation/voices.py index 066921c..2a82b27 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -3,12 +3,14 @@ from typing import Dict, Tuple from rich.console import Console +from TTS import pyttsx 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.pyttsx import pyttsx from utils import settings from utils.console import print_table, print_step @@ -20,6 +22,7 @@ TTSProviders = { "AWSPolly": AWSPolly, "StreamlabsPolly": StreamlabsPolly, "TikTok": TikTok, + "pyttsx" : pyttsx, } From 6ca3223ca3ba72f809c7e5e4116f8d18a2e8ed76 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:45:35 +0530 Subject: [PATCH 49/63] small mistake in voices.py --- TTS/pyttsx.py | 2 +- utils/.config.template.toml | 2 +- video_creation/data/videos.json | 19 +------------------ video_creation/voices.py | 1 - 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index 0e6d3bb..ae5d3a9 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -21,7 +21,7 @@ class pyttsx: ): voice_id = int(settings.config["settings"]["tts"]["python_voice"]) voice_num = int(settings.config["settings"]["tts"]["python_voice_number"]) - for i in range(voice_num): + for i in range(voice_num-1): self.voices.append(i) i=+1 if random_voice: diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 20a163a..fe377fe 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -35,5 +35,5 @@ voice_choice = { optional = false, default = "", options = ["streamlabspolly", " 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)"} +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)"} python_voice_number = {optional = false, default = "2", example = "2", explanation= "the number of system voices(2 are pre-installed in windows)"} diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json index b851d3e..0637a08 100644 --- a/video_creation/data/videos.json +++ b/video_creation/data/videos.json @@ -1,18 +1 @@ -[ - { - "subreddit": "tifu", - "id": "w97htn", - "time": "1658942748", - "background_credit": "Achy Gaming", - "reddit_title": "TIFU by refusing to give tea to a Chinese senator", - "filename": "TIFU by refusing to give tea to a Chinese senator.mp4" - }, - { - "subreddit": "tifu", - "id": "w9iqby", - "time": "1658945240", - "background_credit": "Achy Gaming", - "reddit_title": "TIFU by giving my boss a RedBull", - "filename": "TIFU by giving my boss a RedBull.mp4" - } -] \ No newline at end of file +[] \ No newline at end of file diff --git a/video_creation/voices.py b/video_creation/voices.py index 2a82b27..b4e6216 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -3,7 +3,6 @@ from typing import Dict, Tuple from rich.console import Console -from TTS import pyttsx from TTS.engine_wrapper import TTSEngine from TTS.GTTS import GTTS From ae41127a91f447055792a228740df5111aab3349 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Thu, 28 Jul 2022 00:01:12 +0530 Subject: [PATCH 50/63] fixed a error(self-made), added default values --- TTS/TikTok.py | 12 +++++---- TTS/aws_polly.py | 4 ++- TTS/engine_wrapper.py | 11 +++----- TTS/pyttsx.py | 30 ++++++++++++---------- TTS/streamlabs_polly.py | 4 ++- main.py | 4 ++- ptt.py | 7 +++--- reddit/subreddit.py | 16 +++++++++--- tempTEMP_MPY_wvf_snd.mp4 | Bin 0 -> 64754 bytes utils/cleanup.py | 4 ++- utils/console.py | 17 ++++++++++--- utils/settings.py | 32 +++++++++++++++++++----- utils/subreddit.py | 11 +++++--- utils/video.py | 12 +++++++-- utils/videos.py | 8 ++++-- utils/voice.py | 4 ++- video_creation/background.py | 14 +++++++---- video_creation/final_video.py | 8 ++++-- video_creation/screenshot_downloader.py | 14 ++++++++--- video_creation/voices.py | 18 +++++++++---- 20 files changed, 160 insertions(+), 70 deletions(-) create mode 100644 tempTEMP_MPY_wvf_snd.mp4 diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 743118c..6a116d7 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -62,9 +62,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} @@ -81,7 +79,9 @@ class TikTok: # TikTok Text-to-Speech Wrapper ) ) try: - r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") + r = requests.post( + f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0" + ) except requests.exceptions.SSLError: # https://stackoverflow.com/a/47475019/18516611 session = requests.Session() @@ -89,7 +89,9 @@ class TikTok: # TikTok Text-to-Speech Wrapper adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) - r = session.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") + r = session.post( + f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0" + ) # print(r.text) vstr = [r.json()["data"]["v_str"]][0] b64d = base64.b64decode(vstr) diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index eac5884..e7763dd 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -40,7 +40,9 @@ class AWSPolly: 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() + voice = str( + settings.config["settings"]["tts"]["aws_polly_voice"] + ).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6d09c7a..6c2e5ed 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -59,10 +59,7 @@ class TTSEngine: self.call_tts("title", process_text(self.reddit_object["thread_title"])) processed_text = process_text(self.reddit_object["thread_post"]) - if ( - processed_text != "" - and settings.config["settings"]["storymode"] == True - ): + if processed_text != "" and settings.config["settings"]["storymode"] == True: self.call_tts("posttext", processed_text) idx = None @@ -95,7 +92,7 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - new_text = process_text(text_cut) + new_text = process_text(text_cut) if not new_text or new_text.isspace(): offset += 1 continue @@ -120,9 +117,7 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run( - text, filepath=f"{self.path}/{filename}.mp3" - ) + self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index ae5d3a9..7e0fad1 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -1,13 +1,12 @@ +import random import pyttsx3 from utils import settings -import random max_chars = 0 +# Uses the system voices, significantly faster than other tts -#Uses the system voices, significantly faster than other tts - -class pyttsx: +class Pyttsx: def __init__(self): self.max_chars = 0 self.voices = [] @@ -17,22 +16,27 @@ class pyttsx: text: str = "Python Text to Speech", filepath: str = "assets/temp/mp3", random_voice=False, - censor=False, ): - voice_id = int(settings.config["settings"]["tts"]["python_voice"]) - voice_num = int(settings.config["settings"]["tts"]["python_voice_number"]) - for i in range(voice_num-1): + if (settings.config["settings"]["tts"]["python_voice"] == '' or settings.config["settings"]["tts"]["python_voice"] == ""): + voice_id = 0 + voice_num = 3 + else: + voice_id = int(settings.config["settings"]["tts"]["python_voice"]) + voice_num = int(settings.config["settings"]["tts"]["python_voice"]) + for i in range(voice_num - 1): self.voices.append(i) - i=+1 + i = +1 if random_voice: voice_id = self.randomvoice() else: voice = voice_id engine = pyttsx3.init() - voices = engine.getProperty('voices') - engine.setProperty('voice', voices[voice].id) #changing index changes voices but ony 0 and 1 are working here + voices = engine.getProperty("voices") + engine.setProperty( + "voice", voices[voice].id + ) # changing index changes voices but ony 0 and 1 are working here engine.save_to_file(text, f"{filepath}") engine.runAndWait() - + def randomvoice(self): - return random.choice(self.voices) \ No newline at end of file + return random.choice(self.voices) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 75c4f49..60cf9e0 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -40,7 +40,9 @@ class StreamlabsPolly: 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() + voice = str( + settings.config["settings"]["tts"]["streamlabs_polly_voice"] + ).capitalize() body = {"voice": voice, "text": text, "service": "polly"} response = requests.post(self.url, data=body) if not check_ratelimit(response): diff --git a/main.py b/main.py index 6725540..be0606c 100755 --- a/main.py +++ b/main.py @@ -75,7 +75,9 @@ if __name__ == "__main__": run_many(config["settings"]["times_to_run"]) elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1: - for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): + for index, post_id in enumerate( + config["reddit"]["thread"]["post_id"].split("+") + ): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/ptt.py b/ptt.py index b82c8e0..6b49ef6 100644 --- a/ptt.py +++ b/ptt.py @@ -1,9 +1,10 @@ import pyttsx3 + engine = pyttsx3.init() -voices = engine.getProperty('voices') +voices = engine.getProperty("voices") for voice in voices: print(voice, voice.id) - engine.setProperty('voice', voice.id) + engine.setProperty("voice", voice.id) engine.say("Hello World!") engine.runAndWait() - engine.stop() \ No newline at end of file + engine.stop() diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 716a7fa..486c3ca 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -19,7 +19,9 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print("\nEnter your two-factor authentication code from your authenticator app.\n") + print( + "\nEnter your two-factor authentication code from your authenticator app.\n" + ) code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -45,7 +47,9 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) + re.sub( + r"r\/", "", input("What subreddit would you like to pull from? ") + ) # removes the r/ from the input ) except ValueError: @@ -55,7 +59,9 @@ def get_subreddit_threads(POST_ID: str): sub = settings.config["reddit"]["thread"]["subreddit"] print_substep(f"Using subreddit: r/{sub} from TOML config") subreddit_choice = sub - if str(subreddit_choice).casefold().startswith("r/"): # removes the r/ from the input + if ( + str(subreddit_choice).casefold().startswith("r/") + ): # removes the r/ from the input subreddit_choice = subreddit_choice[2:] subreddit = reddit.subreddit( subreddit_choice @@ -67,7 +73,9 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) + submission = reddit.submission( + id=settings.config["reddit"]["thread"]["post_id"] + ) else: threads = subreddit.hot(limit=25) submission = get_subreddit_undone(threads, subreddit) diff --git a/tempTEMP_MPY_wvf_snd.mp4 b/tempTEMP_MPY_wvf_snd.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e453248b627c59db64380f23539dca9d7c32d7f0 GIT binary patch literal 64754 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 OK!B4+kn=t__x}Or;(%xX literal 0 HcmV?d00001 diff --git a/utils/cleanup.py b/utils/cleanup.py index 75074b7..e2f25f9 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -14,7 +14,9 @@ def cleanup() -> int: """ if exists("./assets/temp"): count = 0 - files = [f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()] + files = [ + f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower() + ] count += len(files) for f in files: os.remove(f) diff --git a/utils/console.py b/utils/console.py index 6f99a41..1ffa11c 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,7 +49,10 @@ def handle_input( optional=False, ): if optional: - console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") + console.print( + message + + "\n[green]This is an optional value. Do you want to skip it? (y/n)" + ) if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -83,7 +86,11 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") + console.print( + "[red]" + + err_message + + "\nAre you absolutely sure it's correct?(y/n)" + ) if input().casefold().startswith("y"): break continue @@ -116,5 +123,9 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + "[red bold]" + + err_message + + "\nValid options are: " + + ", ".join(map(str, options)) + + "." ) diff --git a/utils/settings.py b/utils/settings.py index a9d7726..2bd4db9 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -54,7 +54,11 @@ def check(value, checks, name): 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"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -62,8 +66,16 @@ def check(value, checks, name): 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"]) + ( + "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 @@ -71,9 +83,15 @@ def check(value, checks, name): if incorrect: value = handle_input( message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + ( + ("[blue]Example: " + str(checks["example"]) + "\n") + if "example" in checks + else "" + ) + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] + + ("Non-optional ", "Optional ")[ + "optional" in checks and checks["optional"] is True + ] ) + "[#C0CAF5 bold]" + str(name) @@ -114,7 +132,9 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") + console.print( + f"[red bold]Encountered error when trying to to load {template_file}: {error}" + ) return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index c386868..1113ebe 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -19,7 +19,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for submission in submissions: if already_done(done_videos, submission): @@ -34,7 +36,9 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): if submission.stickied: print_substep("This post was pinned by moderators. Skipping...") continue - if submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]): + if submission.num_comments <= int( + settings.config["reddit"]["thread"]["min_comments"] + ): print_substep( f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) @@ -55,7 +59,8 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): return get_subreddit_undone( subreddit.top( - time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50) + time_filter=VALID_TIME_FILTERS[index], + limit=(50 if int(index) == 0 else index + 1 * 50), ), subreddit, times_checked=index, diff --git a/utils/video.py b/utils/video.py index 63dc170..b373d04 100644 --- a/utils/video.py +++ b/utils/video.py @@ -35,10 +35,18 @@ class Video: return ImageClip(path) def add_watermark( - self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 + self, + text, + opacity=0.5, + duration: int | float = 5, + position: Tuple = (0.7, 0.9), + fontsize=15, ): compensation = round( - (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), + ( + position[0] + / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0]) + ), ndigits=2, ) position = (compensation, position[1]) diff --git a/utils/videos.py b/utils/videos.py index 4a91e8c..7f064f7 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -20,7 +20,9 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -34,7 +36,9 @@ def check_done( return redditobj -def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): +def save_data( + subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str +): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index a0709fa..aab6d24 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -40,7 +40,9 @@ def sleep_until(time): if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + zoneDiff = ( + pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() + ) end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index 6e656fa..fde8e5e 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -31,7 +31,9 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(): """Fetch the background/s configuration""" try: - choice = str(settings.config["settings"]["background"]["background_choice"]).casefold() + choice = str( + settings.config["settings"]["background"]["background_choice"] + ).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -56,13 +58,15 @@ def download_background(background_config: Tuple[str, str, str, Any]): ) 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( - "assets/backgrounds", filename=f"{credit}-{filename}" - ) + YouTube(uri, on_progress_callback=on_progress).streams.filter( + res="1080p" + ).first().download("assets/backgrounds", filename=f"{credit}-{filename}") print_substep("Background video downloaded successfully! 🎉", style="bold green") -def chop_background_video(background_config: Tuple[str, str, str, Any], video_length: int): +def chop_background_video( + background_config: Tuple[str, str, str, Any], video_length: int +): """Generates the background footage to be used in the video and writes it to assets/temp/background.mp4 Args: diff --git a/video_creation/final_video.py b/video_creation/final_video.py index fd12642..7877e0d 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -75,7 +75,9 @@ def make_final_video( ) # Gather all audio clips - audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] + audio_clips = [ + AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips) + ] audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3")) audio_concat = concatenate_audioclips(audio_clips) audio_composite = CompositeAudioClip([audio_concat]) @@ -85,7 +87,9 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - new_transition = 0 if transition is None or float(transition) > 2 else float(transition) + new_transition = ( + 0 if transition is None or float(transition) > 2 else float(transition) + ) image_clips.insert( 0, ImageClip("assets/temp/png/title.png") diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 4574675..169ff4a 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -36,9 +36,13 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in context = browser.new_context() if settings.config["settings"]["theme"] == "dark": - cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" + ) else: - cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") + cookie_file = open( + "./video_creation/data/cookie-light-mode.json", encoding="utf-8" + ) cookies = json.load(cookie_file) context.add_cookies(cookies) # load preference cookies # Get the thread screenshot @@ -50,7 +54,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() - page.wait_for_load_state() # Wait for page to fully load + page.wait_for_load_state() # Wait for page to fully load if page.locator('[data-click-id="text"] button').is_visible(): page.locator( @@ -73,7 +77,9 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in else: print_substep("Skipping translation...") - page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png") + page.locator('[data-test-id="post-content"]').screenshot( + path="assets/temp/png/title.png" + ) if storymode: page.locator('[data-click-id="text"]').screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index b4e6216..80a83ad 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -9,7 +9,7 @@ from TTS.GTTS import GTTS from TTS.streamlabs_polly import StreamlabsPolly from TTS.aws_polly import AWSPolly from TTS.TikTok import TikTok -from TTS.pyttsx import pyttsx +from TTS.pyttsx import Pyttsx from utils import settings from utils.console import print_table, print_step @@ -21,7 +21,7 @@ TTSProviders = { "AWSPolly": AWSPolly, "StreamlabsPolly": StreamlabsPolly, "TikTok": TikTok, - "pyttsx" : pyttsx, + "pyttsx": Pyttsx, } @@ -37,7 +37,9 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, voice), reddit_obj + ) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -46,12 +48,18 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) + text_to_mp3 = TTSEngine( + get_case_insensitive_key_value(TTSProviders, choice), reddit_obj + ) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), + ( + value + for dict_key, value in input_dict.items() + if dict_key.lower() == key.lower() + ), None, ) From 3da1debf834786bfff19215151b5dc317d1cb097 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Thu, 28 Jul 2022 14:41:41 +0530 Subject: [PATCH 51/63] added full pyttsx functionality --- TTS/pyttsx.py | 38 ++++++++++++++++-------------------- tempTEMP_MPY_wvf_snd.mp4 | Bin 64754 -> 0 bytes utils/.config.template.toml | 2 +- video_creation/voices.py | 4 ++-- 4 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 tempTEMP_MPY_wvf_snd.mp4 diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index 7e0fad1..2be3b53 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -2,41 +2,37 @@ import random import pyttsx3 from utils import settings -max_chars = 0 -# Uses the system voices, significantly faster than other tts +class pyttsx: - -class Pyttsx: def __init__(self): - self.max_chars = 0 + self.max_chars = 5000 self.voices = [] def run( self, - text: str = "Python Text to Speech", - filepath: str = "assets/temp/mp3", + text: str , + filepath: str, random_voice=False, ): - if (settings.config["settings"]["tts"]["python_voice"] == '' or settings.config["settings"]["tts"]["python_voice"] == ""): - voice_id = 0 + 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(settings.config["settings"]["tts"]["python_voice"]) - voice_num = int(settings.config["settings"]["tts"]["python_voice"]) - for i in range(voice_num - 1): + voice_id = int(voice_id) + voice_num = int(voice_num) + for i in range(voice_num): self.voices.append(i) - i = +1 + i=+1 if random_voice: - voice_id = self.randomvoice() - else: - voice = voice_id + voice_id = self.randomvoice() engine = pyttsx3.init() - voices = engine.getProperty("voices") - engine.setProperty( - "voice", voices[voice].id - ) # changing index changes voices but ony 0 and 1 are working here + voices = engine.getProperty('voices') + engine.setProperty('voice', voices[voice_id].id) #changing index changes voices but ony 0 and 1 are working here engine.save_to_file(text, f"{filepath}") engine.runAndWait() - + def randomvoice(self): return random.choice(self.voices) diff --git a/tempTEMP_MPY_wvf_snd.mp4 b/tempTEMP_MPY_wvf_snd.mp4 deleted file mode 100644 index e453248b627c59db64380f23539dca9d7c32d7f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64754 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 OK!B4+kn=t__x}Or;(%xX diff --git a/utils/.config.template.toml b/utils/.config.template.toml index fe377fe..0ec28c9 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -36,4 +36,4 @@ aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", 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)"} -python_voice_number = {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)"} diff --git a/video_creation/voices.py b/video_creation/voices.py index 80a83ad..29c2b57 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -9,7 +9,7 @@ from TTS.GTTS import GTTS from TTS.streamlabs_polly import StreamlabsPolly from TTS.aws_polly import AWSPolly from TTS.TikTok import TikTok -from TTS.pyttsx import Pyttsx +from TTS.pyttsx import pyttsx from utils import settings from utils.console import print_table, print_step @@ -21,7 +21,7 @@ TTSProviders = { "AWSPolly": AWSPolly, "StreamlabsPolly": StreamlabsPolly, "TikTok": TikTok, - "pyttsx": Pyttsx, + "pyttsx": pyttsx, } From fec2c22f442900105474e2134f1ec5651c7076d6 Mon Sep 17 00:00:00 2001 From: Callum Leslie Date: Fri, 29 Jul 2022 18:18:40 +0000 Subject: [PATCH 52/63] chore: prepare release --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 6725540..916c54a 100755 --- a/main.py +++ b/main.py @@ -20,7 +20,7 @@ from video_creation.screenshot_downloader import download_screenshots_of_reddit_ from video_creation.voices import save_text_to_mp3 __VERSION__ = "2.3.1" -__BRANCH__ = "develop" +__BRANCH__ = "master" print( """ From 9140af9c02430b294fae9d70beef6a8aca593ac7 Mon Sep 17 00:00:00 2001 From: Callum Leslie Date: Fri, 29 Jul 2022 19:23:01 +0100 Subject: [PATCH 53/63] style: formatted with black --- TTS/engine_wrapper.py | 19 +++++-------------- video_creation/screenshot_downloader.py | 2 +- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6d09c7a..8fb9a41 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -59,16 +59,11 @@ class TTSEngine: self.call_tts("title", process_text(self.reddit_object["thread_title"])) processed_text = process_text(self.reddit_object["thread_post"]) - if ( - processed_text != "" - and settings.config["settings"]["storymode"] == True - ): + if processed_text != "" and settings.config["settings"]["storymode"] == True: self.call_tts("posttext", processed_text) idx = None - for idx, comment in track( - enumerate(self.reddit_object["comments"]), "Saving..." - ): + for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): # ! Stop creating mp3 files if the length is greater than max length. if self.length > self.max_length: self.length -= self.last_clip_length @@ -95,15 +90,13 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - new_text = process_text(text_cut) + 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") - ) + 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 @@ -120,9 +113,7 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run( - text, filepath=f"{self.path}/{filename}.mp3" - ) + self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 7898e8d..51645e4 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -49,7 +49,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() - page.wait_for_load_state() # Wait for page to fully load + page.wait_for_load_state() # Wait for page to fully load if page.locator('[data-click-id="text"] button').is_visible(): page.locator( From ca97087ac86c24f55630bbe0f482489e36ea677a Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Sat, 30 Jul 2022 00:05:41 +0530 Subject: [PATCH 54/63] fixed formatting --- TTS/TikTok.py | 12 ++++------ TTS/aws_polly.py | 4 +--- TTS/engine_wrapper.py | 11 ++++++--- TTS/pyttsx.py | 26 +++++++++++--------- TTS/streamlabs_polly.py | 4 +--- main.py | 4 +--- reddit/subreddit.py | 16 ++++--------- utils/cleanup.py | 4 +--- utils/console.py | 19 ++++----------- utils/settings.py | 32 +++++-------------------- utils/subreddit.py | 11 +++------ utils/video.py | 12 ++-------- utils/videos.py | 8 ++----- utils/voice.py | 4 +--- video_creation/background.py | 14 ++++------- video_creation/final_video.py | 8 ++----- video_creation/screenshot_downloader.py | 15 ++++-------- video_creation/voices.py | 14 +++-------- 18 files changed, 68 insertions(+), 150 deletions(-) diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 6a116d7..743118c 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -62,7 +62,9 @@ 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} @@ -79,9 +81,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper ) ) try: - r = requests.post( - f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0" - ) + r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") except requests.exceptions.SSLError: # https://stackoverflow.com/a/47475019/18516611 session = requests.Session() @@ -89,9 +89,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) - r = session.post( - f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0" - ) + r = session.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") # print(r.text) vstr = [r.json()["data"]["v_str"]][0] b64d = base64.b64decode(vstr) diff --git a/TTS/aws_polly.py b/TTS/aws_polly.py index e7763dd..eac5884 100644 --- a/TTS/aws_polly.py +++ b/TTS/aws_polly.py @@ -40,9 +40,7 @@ class AWSPolly: 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() + voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() try: # Request speech synthesis response = polly.synthesize_speech( diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 6c2e5ed..02ab37b 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -59,7 +59,10 @@ class TTSEngine: self.call_tts("title", process_text(self.reddit_object["thread_title"])) processed_text = process_text(self.reddit_object["thread_post"]) - if processed_text != "" and settings.config["settings"]["storymode"] == True: + if ( + processed_text != "" + and settings.config["settings"]["storymode"] == True + ): self.call_tts("posttext", processed_text) idx = None @@ -92,7 +95,7 @@ class TTSEngine: offset = 0 for idy, text_cut in enumerate(split_text): # print(f"{idx}-{idy}: {text_cut}\n") - new_text = process_text(text_cut) + new_text = process_text(text_cut) if not new_text or new_text.isspace(): offset += 1 continue @@ -117,7 +120,9 @@ class TTSEngine: # Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() def call_tts(self, filename: str, text: str): - self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3") + self.tts_module.run( + text, filepath=f"{self.path}/{filename}.mp3" + ) # try: # self.length += MP3(f"{self.path}/{filename}.mp3").info.length # except (MutagenError, HeaderNotFoundError): diff --git a/TTS/pyttsx.py b/TTS/pyttsx.py index 2be3b53..06d27c5 100644 --- a/TTS/pyttsx.py +++ b/TTS/pyttsx.py @@ -2,37 +2,41 @@ import random import pyttsx3 from utils import settings -class pyttsx: +class pyttsx: def __init__(self): self.max_chars = 5000 self.voices = [] def run( self, - text: str , + 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 = 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") + 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 + i = +1 if random_voice: - voice_id = self.randomvoice() + 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 + voices = engine.getProperty("voices") + engine.setProperty( + "voice", voices[voice_id].id + ) # changing index changes voices but ony 0 and 1 are working here engine.save_to_file(text, f"{filepath}") engine.runAndWait() - + def randomvoice(self): return random.choice(self.voices) diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 60cf9e0..75c4f49 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -40,9 +40,7 @@ class StreamlabsPolly: 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() + voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() body = {"voice": voice, "text": text, "service": "polly"} response = requests.post(self.url, data=body) if not check_ratelimit(response): diff --git a/main.py b/main.py index be0606c..6725540 100755 --- a/main.py +++ b/main.py @@ -75,9 +75,7 @@ if __name__ == "__main__": run_many(config["settings"]["times_to_run"]) elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1: - for index, post_id in enumerate( - config["reddit"]["thread"]["post_id"].split("+") - ): + for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): index += 1 print_step( f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 486c3ca..9a675a6 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -19,9 +19,7 @@ def get_subreddit_threads(POST_ID: str): content = {} if settings.config["reddit"]["creds"]["2fa"]: - print( - "\nEnter your two-factor authentication code from your authenticator app.\n" - ) + print("\nEnter your two-factor authentication code from your authenticator app.\n") code = input("> ") print() pw = settings.config["reddit"]["creds"]["password"] @@ -47,9 +45,7 @@ def get_subreddit_threads(POST_ID: str): ]: # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") try: subreddit = reddit.subreddit( - re.sub( - r"r\/", "", input("What subreddit would you like to pull from? ") - ) + re.sub(r"r\/", "", input("What subreddit would you like to pull from? ")) # removes the r/ from the input ) except ValueError: @@ -59,9 +55,7 @@ def get_subreddit_threads(POST_ID: str): sub = settings.config["reddit"]["thread"]["subreddit"] print_substep(f"Using subreddit: r/{sub} from TOML config") subreddit_choice = sub - if ( - str(subreddit_choice).casefold().startswith("r/") - ): # removes the r/ from the input + if (str(subreddit_choice).casefold().startswith("r/")): # removes the r/ from the input subreddit_choice = subreddit_choice[2:] subreddit = reddit.subreddit( subreddit_choice @@ -73,9 +67,7 @@ def get_subreddit_threads(POST_ID: str): settings.config["reddit"]["thread"]["post_id"] and len(str(settings.config["reddit"]["thread"]["post_id"]).split("+")) == 1 ): - submission = reddit.submission( - id=settings.config["reddit"]["thread"]["post_id"] - ) + submission = reddit.submission(id=settings.config["reddit"]["thread"]["post_id"]) else: threads = subreddit.hot(limit=25) submission = get_subreddit_undone(threads, subreddit) diff --git a/utils/cleanup.py b/utils/cleanup.py index e2f25f9..75074b7 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -14,9 +14,7 @@ def cleanup() -> int: """ if exists("./assets/temp"): count = 0 - files = [ - f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower() - ] + files = [f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()] count += len(files) for f in files: os.remove(f) diff --git a/utils/console.py b/utils/console.py index 1ffa11c..c557b2a 100644 --- a/utils/console.py +++ b/utils/console.py @@ -49,10 +49,7 @@ def handle_input( optional=False, ): if optional: - console.print( - message - + "\n[green]This is an optional value. Do you want to skip it? (y/n)" - ) + console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") if input().casefold().startswith("y"): return default if default is not NotImplemented else "" if default is not NotImplemented: @@ -86,11 +83,7 @@ def handle_input( console.print("[red]" + err_message) continue elif match != "" and re.match(match, user_input) is None: - console.print( - "[red]" - + err_message - + "\nAre you absolutely sure it's correct?(y/n)" - ) + console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") if input().casefold().startswith("y"): break continue @@ -123,9 +116,5 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" - + err_message - + "\nValid options are: " - + ", ".join(map(str, options)) - + "." - ) + "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + ) \ No newline at end of file diff --git a/utils/settings.py b/utils/settings.py index 2bd4db9..a9d7726 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -54,11 +54,7 @@ def check(value, checks, name): 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"] - ) + or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): incorrect = True @@ -66,16 +62,8 @@ def check(value, checks, name): 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"] - ) + ("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 @@ -83,15 +71,9 @@ def check(value, checks, name): if incorrect: value = handle_input( message=( - ( - ("[blue]Example: " + str(checks["example"]) + "\n") - if "example" in checks - else "" - ) + (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + "[red]" - + ("Non-optional ", "Optional ")[ - "optional" in checks and checks["optional"] is True - ] + + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] ) + "[#C0CAF5 bold]" + str(name) @@ -132,9 +114,7 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]: try: template = toml.load(template_file) except Exception as error: - console.print( - f"[red bold]Encountered error when trying to to load {template_file}: {error}" - ) + console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") return False try: config = toml.load(config_file) diff --git a/utils/subreddit.py b/utils/subreddit.py index 1113ebe..3ffafd9 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -19,9 +19,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): if not exists("./video_creation/data/videos.json"): with open("./video_creation/data/videos.json", "w+") as f: json.dump([], f) - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for submission in submissions: if already_done(done_videos, submission): @@ -36,9 +34,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): if submission.stickied: print_substep("This post was pinned by moderators. Skipping...") continue - if submission.num_comments <= int( - settings.config["reddit"]["thread"]["min_comments"] - ): + if submission.num_comments <= int(settings.config["reddit"]["thread"]["min_comments"]): print_substep( f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) @@ -59,8 +55,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): return get_subreddit_undone( subreddit.top( - time_filter=VALID_TIME_FILTERS[index], - limit=(50 if int(index) == 0 else index + 1 * 50), + time_filter=VALID_TIME_FILTERS[index],limit=(50 if int(index) == 0 else index + 1 * 50), ), subreddit, times_checked=index, diff --git a/utils/video.py b/utils/video.py index b373d04..d892eb9 100644 --- a/utils/video.py +++ b/utils/video.py @@ -35,18 +35,10 @@ class Video: return ImageClip(path) def add_watermark( - self, - text, - opacity=0.5, - duration: int | float = 5, - position: Tuple = (0.7, 0.9), - fontsize=15, + self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15, ): compensation = round( - ( - position[0] - / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0]) - ), + (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), ndigits=2, ) position = (compensation, position[1]) diff --git a/utils/videos.py b/utils/videos.py index 7f064f7..4a91e8c 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -20,9 +20,7 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open( - "./video_creation/data/videos.json", "r", encoding="utf-8" - ) as done_vids_raw: + with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -36,9 +34,7 @@ def check_done( return redditobj -def save_data( - subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str -): +def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: diff --git a/utils/voice.py b/utils/voice.py index aab6d24..a0709fa 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -40,9 +40,7 @@ def sleep_until(time): if sys.version_info[0] >= 3 and time.tzinfo: end = time.astimezone(timezone.utc).timestamp() else: - zoneDiff = ( - pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() - ) + zoneDiff = pytime.time() - (datetime.now() - datetime(1970, 1, 1)).total_seconds() end = (time - datetime(1970, 1, 1)).total_seconds() + zoneDiff # Type check diff --git a/video_creation/background.py b/video_creation/background.py index fde8e5e..6e656fa 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -31,9 +31,7 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int def get_background_config(): """Fetch the background/s configuration""" try: - choice = str( - settings.config["settings"]["background"]["background_choice"] - ).casefold() + choice = str(settings.config["settings"]["background"]["background_choice"]).casefold() except AttributeError: print_substep("No background selected. Picking random background'") choice = None @@ -58,15 +56,13 @@ def download_background(background_config: Tuple[str, str, str, Any]): ) 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("assets/backgrounds", filename=f"{credit}-{filename}") + YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( + "assets/backgrounds", filename=f"{credit}-{filename}" + ) print_substep("Background video downloaded successfully! 🎉", style="bold green") -def chop_background_video( - background_config: Tuple[str, str, str, Any], video_length: int -): +def chop_background_video(background_config: Tuple[str, str, str, Any], video_length: int): """Generates the background footage to be used in the video and writes it to assets/temp/background.mp4 Args: diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 7877e0d..eb7508b 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -75,9 +75,7 @@ def make_final_video( ) # Gather all audio clips - audio_clips = [ - AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips) - ] + audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)] audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3")) audio_concat = concatenate_audioclips(audio_clips) audio_composite = CompositeAudioClip([audio_concat]) @@ -87,9 +85,7 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - new_transition = ( - 0 if transition is None or float(transition) > 2 else float(transition) - ) + new_transition = (0 if transition is None or float(transition) > 2 else float(transition)) image_clips.insert( 0, ImageClip("assets/temp/png/title.png") diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 169ff4a..7898e8d 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -1,4 +1,3 @@ -from distutils.command.config import config import json from pathlib import Path @@ -36,13 +35,9 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in context = browser.new_context() if settings.config["settings"]["theme"] == "dark": - cookie_file = open( - "./video_creation/data/cookie-dark-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") else: - cookie_file = open( - "./video_creation/data/cookie-light-mode.json", encoding="utf-8" - ) + cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") cookies = json.load(cookie_file) context.add_cookies(cookies) # load preference cookies # Get the thread screenshot @@ -54,7 +49,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in print_substep("Post is NSFW. You are spicy...") page.locator('[data-testid="content-gate"] button').click() - page.wait_for_load_state() # Wait for page to fully load + page.wait_for_load_state() # Wait for page to fully load if page.locator('[data-click-id="text"] button').is_visible(): page.locator( @@ -77,9 +72,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in else: print_substep("Skipping translation...") - page.locator('[data-test-id="post-content"]').screenshot( - path="assets/temp/png/title.png" - ) + page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png") if storymode: page.locator('[data-click-id="text"]').screenshot( diff --git a/video_creation/voices.py b/video_creation/voices.py index 29c2b57..f49514d 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -37,9 +37,7 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: voice = settings.config["settings"]["tts"]["voice_choice"] if str(voice).casefold() in map(lambda _: _.casefold(), TTSProviders): - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, voice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, voice), reddit_obj) else: while True: print_step("Please choose one of the following TTS providers: ") @@ -48,18 +46,12 @@ def save_text_to_mp3(reddit_obj) -> Tuple[int, int]: if choice.casefold() in map(lambda _: _.casefold(), TTSProviders): break print("Unknown Choice") - text_to_mp3 = TTSEngine( - get_case_insensitive_key_value(TTSProviders, choice), reddit_obj - ) + text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj) return text_to_mp3.run() def get_case_insensitive_key_value(input_dict, key): return next( - ( - value - for dict_key, value in input_dict.items() - if dict_key.lower() == key.lower() - ), + (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), None, ) From 5d0172a46394bbe43d76158c37f1e47ce39907c5 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Sat, 30 Jul 2022 00:09:36 +0530 Subject: [PATCH 55/63] fixed formatting changes --- TTS/engine_wrapper.py | 6 +++--- reddit/subreddit.py | 2 +- utils/console.py | 3 ++- utils/subreddit.py | 2 +- utils/video.py | 2 +- video_creation/final_video.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 02ab37b..6d09c7a 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -60,9 +60,9 @@ class TTSEngine: self.call_tts("title", process_text(self.reddit_object["thread_title"])) processed_text = process_text(self.reddit_object["thread_post"]) if ( - processed_text != "" + processed_text != "" and settings.config["settings"]["storymode"] == True - ): + ): self.call_tts("posttext", processed_text) idx = None @@ -122,7 +122,7 @@ class TTSEngine: 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): diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 9a675a6..716a7fa 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -55,7 +55,7 @@ def get_subreddit_threads(POST_ID: str): sub = settings.config["reddit"]["thread"]["subreddit"] print_substep(f"Using subreddit: r/{sub} from TOML config") subreddit_choice = sub - if (str(subreddit_choice).casefold().startswith("r/")): # removes the r/ from the input + if str(subreddit_choice).casefold().startswith("r/"): # removes the r/ from the input subreddit_choice = subreddit_choice[2:] subreddit = reddit.subreddit( subreddit_choice diff --git a/utils/console.py b/utils/console.py index c557b2a..647e131 100644 --- a/utils/console.py +++ b/utils/console.py @@ -117,4 +117,5 @@ def handle_input( return user_input console.print( "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." - ) \ No newline at end of file + ) + \ No newline at end of file diff --git a/utils/subreddit.py b/utils/subreddit.py index 3ffafd9..fae4631 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -55,7 +55,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): return get_subreddit_undone( subreddit.top( - time_filter=VALID_TIME_FILTERS[index],limit=(50 if int(index) == 0 else index + 1 * 50), + time_filter=VALID_TIME_FILTERS[index],limit=(50 if int(index) == 0 else index + 1 * 50) ), subreddit, times_checked=index, diff --git a/utils/video.py b/utils/video.py index d892eb9..63dc170 100644 --- a/utils/video.py +++ b/utils/video.py @@ -35,7 +35,7 @@ class Video: return ImageClip(path) def add_watermark( - self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15, + self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 ): compensation = round( (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), diff --git a/video_creation/final_video.py b/video_creation/final_video.py index eb7508b..fd12642 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -85,7 +85,7 @@ def make_final_video( image_clips = [] # Gather all images new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity) - new_transition = (0 if transition is None or float(transition) > 2 else float(transition)) + new_transition = 0 if transition is None or float(transition) > 2 else float(transition) image_clips.insert( 0, ImageClip("assets/temp/png/title.png") From 3de02c9c6da6a215cbc9c4bef364df3171601107 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Sat, 30 Jul 2022 00:10:55 +0530 Subject: [PATCH 56/63] formatting changes --- utils/console.py | 3 +-- utils/subreddit.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/utils/console.py b/utils/console.py index 647e131..c3617e3 100644 --- a/utils/console.py +++ b/utils/console.py @@ -117,5 +117,4 @@ def handle_input( return user_input console.print( "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." - ) - \ No newline at end of file + ) \ No newline at end of file diff --git a/utils/subreddit.py b/utils/subreddit.py index fae4631..c386868 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -55,7 +55,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0): return get_subreddit_undone( subreddit.top( - time_filter=VALID_TIME_FILTERS[index],limit=(50 if int(index) == 0 else index + 1 * 50) + time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50) ), subreddit, times_checked=index, From a51e85a053dfdecbc47a7b9cd13cc720be7dcb51 Mon Sep 17 00:00:00 2001 From: Shorya Agarwal <60816807+Supreme-hub@users.noreply.github.com> Date: Sat, 30 Jul 2022 00:13:31 +0530 Subject: [PATCH 57/63] formatting --- utils/console.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/console.py b/utils/console.py index c3617e3..1518c27 100644 --- a/utils/console.py +++ b/utils/console.py @@ -117,4 +117,5 @@ def handle_input( return user_input console.print( "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." - ) \ No newline at end of file + ) + From 5bf38dbbf0ae04dd52897f46532fd204bb9b7a64 Mon Sep 17 00:00:00 2001 From: Supreme-hub <60816807+Supreme-hub@users.noreply.github.com> Date: Sat, 30 Jul 2022 06:20:46 +0530 Subject: [PATCH 58/63] added trailing commas --- utils/.config.template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 0ec28c9..31ae9aa 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -31,7 +31,7 @@ background_choice = { optional = true, default = "minecraft", example = "rocket- [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 = "", 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" } From 31d5f421ebc1ba6058ef35040eb2b416d37954fd Mon Sep 17 00:00:00 2001 From: Luka Hietala <95122845+LukaHietala@users.noreply.github.com> Date: Sat, 30 Jul 2022 17:35:28 +0300 Subject: [PATCH 59/63] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8a9e205..e314251 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ 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/) + ## Video https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4 From 4eaf699a1e1052a3031a7d6359347461ec554de2 Mon Sep 17 00:00:00 2001 From: Simon <65854503+StopmotionSimonYT@users.noreply.github.com> Date: Sun, 31 Jul 2022 22:36:55 +0200 Subject: [PATCH 60/63] Add files via upload --- GUI.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 GUI.py diff --git a/GUI.py b/GUI.py new file mode 100644 index 0000000..90e37ae --- /dev/null +++ b/GUI.py @@ -0,0 +1,30 @@ +# Import the server module +import http.server +import webbrowser +# Set the hostname +HOST = "localhost" +# Set the port number +PORT = 4000 + +# Define class to display the index page of the web server +class PythonServer(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == '/GUI': + self.path = 'index.html' + return http.server.SimpleHTTPRequestHandler.do_GET(self) + +# Declare object of the class +webServer = http.server.HTTPServer((HOST, PORT), PythonServer) +# Print the URL of the webserver, new =2 opens in a new tab +print(f"Server started at http://{HOST}:{PORT}/GUI/") +webbrowser.open(f'http://{HOST}:{PORT}/GUI/', new = 2) +print("Website opened in new tab") +print("Press Ctrl+C to quit") +try: + # Run the web server + webServer.serve_forever() +except KeyboardInterrupt: + # Stop the web server + webServer.server_close() + print("The server is stopped.") + exit() \ No newline at end of file From 0f1fccb1a3b7f520c8bb6549e1327d5aa362862f Mon Sep 17 00:00:00 2001 From: Simon <65854503+StopmotionSimonYT@users.noreply.github.com> Date: Sun, 31 Jul 2022 22:37:33 +0200 Subject: [PATCH 61/63] Changed URL of video download --- GUI/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/index.html b/GUI/index.html index 807c9e7..c771adc 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -149,7 +149,7 @@ video += '
'; video += '
'; video += 'View'; - video += 'Download'; + video += 'Download'; video += '
'; video += '
'; video += ''; From 7132bd60ca7c8522c0e63303f3ecaa249c53c36d Mon Sep 17 00:00:00 2001 From: Simon Woker Date: Fri, 5 Aug 2022 21:56:51 +0200 Subject: [PATCH 62/63] limit filename to 255 chars max length for filenames on windows --- video_creation/final_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video_creation/final_video.py b/video_creation/final_video.py index fd12642..d78863a 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -125,7 +125,7 @@ def make_final_video( title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) - filename = f"{name_normalize(title)}.mp4" + filename = f"{name_normalize(title)[:251]}.mp4" subreddit = settings.config["reddit"]["thread"]["subreddit"] if not exists(f"./results/{subreddit}"): From 8463794556051740061b0556541044b38b53d50c Mon Sep 17 00:00:00 2001 From: Callum Leslie Date: Mon, 8 Aug 2022 22:00:15 +0100 Subject: [PATCH 63/63] chore: bump version to 2.4 for release --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 916c54a..bef8ff8 100755 --- a/main.py +++ b/main.py @@ -19,7 +19,7 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.voices import save_text_to_mp3 -__VERSION__ = "2.3.1" +__VERSION__ = "2.4" __BRANCH__ = "master" print(