Merge branch 'develop' into elevenlabs

pull/1619/head
Simon 2 years ago committed by GitHub
commit 53ab45bba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,4 +29,4 @@ jobs:
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY
git checkout $GITHUB_HEAD_REF git checkout $GITHUB_HEAD_REF
git commit -am "fixup: Format Python code with Black" git commit -am "fixup: Format Python code with Black"
git push git push origin HEAD:master

@ -1,6 +1,7 @@
FROM python:3.10.9-slim FROM python:3.10.9-slim
RUN apt update RUN apt update
RUN apt-get install -y ffmpeg
RUN apt install python3-pip -y RUN apt install python3-pip -y
RUN mkdir /app RUN mkdir /app

@ -56,6 +56,13 @@
.tooltip-inner { .tooltip-inner {
max-width: 500px !important; max-width: 500px !important;
} }
#hard-reload {
cursor: pointer;
color: darkblue;
}
#hard-reload:hover {
color: blue;
}
</style> </style>
</head> </head>
@ -132,11 +139,17 @@
Theme by &copy; Bootstrap. <a Theme by &copy; Bootstrap. <a
href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers" href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers"
target="_blank">Developers and Maintainers</a></p> target="_blank">Developers and Maintainers</a></p>
<p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) and visit your local <p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) or click <a id="hard-reload">this</a> and visit your local
<strong>{{ file }}</strong> file. <strong>{{ file }}</strong> file.
</p> </p>
</div> </div>
</footer> </footer>
<script>
document.getElementById("hard-reload").addEventListener("click", function () {
window.location.reload(true);
});
</script>
</body> </body>
</html> </html>

@ -70,7 +70,7 @@ In its current state, this bot does exactly what it needs to do. However, improv
I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute! I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute!
- [ ] Creating better documentation and adding a command line interface. - [ ] Creating better documentation and adding a command line interface.
- [ ] Allowing the user to choose background music for their videos. - [x] Allowing the user to choose background music for their videos.
- [x] Allowing users to choose a reddit thread instead of being randomized. - [x] Allowing users to choose a reddit thread instead of being randomized.
- [x] Allowing users to choose a background that is picked instead of the Minecraft one. - [x] Allowing users to choose a background that is picked instead of the Minecraft one.
- [x] Allowing users to choose between any subreddit. - [x] Allowing users to choose between any subreddit.

@ -3,11 +3,8 @@ import re
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple
# import sox
# from mutagen import MutagenError
# from mutagen.mp3 import MP3, HeaderNotFoundError
import numpy as np import numpy as np
import translators as ts import translators
from moviepy.audio.AudioClip import AudioClip from moviepy.audio.AudioClip import AudioClip
from moviepy.audio.fx.volumex import volumex from moviepy.audio.fx.volumex import volumex
from moviepy.editor import AudioFileClip from moviepy.editor import AudioFileClip
@ -55,9 +52,18 @@ class TTSEngine:
self, self,
): # adds periods to the end of paragraphs (where people often forget to put them) so tts doesn't blend sentences ): # adds periods to the end of paragraphs (where people often forget to put them) so tts doesn't blend sentences
for comment in self.reddit_object["comments"]: for comment in self.reddit_object["comments"]:
# remove links
regex_urls = r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
comment["comment_body"] = re.sub(regex_urls, " ", comment["comment_body"])
comment["comment_body"] = comment["comment_body"].replace("\n", ". ") comment["comment_body"] = comment["comment_body"].replace("\n", ". ")
comment["comment_body"] = re.sub(r'\bAI\b', 'A.I', comment["comment_body"])
comment["comment_body"] = re.sub(r'\bAGI\b', 'A.G.I', comment["comment_body"])
if comment["comment_body"][-1] != ".": if comment["comment_body"][-1] != ".":
comment["comment_body"] += "." comment["comment_body"] += "."
comment["comment_body"] = comment["comment_body"].replace(". . .", ".")
comment["comment_body"] = comment["comment_body"].replace(".. . ", ".")
comment["comment_body"] = comment["comment_body"].replace(". . ", ".")
comment["comment_body"] = re.sub(r'\."\.', '".', comment["comment_body"])
def run(self) -> Tuple[int, int]: def run(self) -> Tuple[int, int]:
Path(self.path).mkdir(parents=True, exist_ok=True) Path(self.path).mkdir(parents=True, exist_ok=True)
@ -141,7 +147,7 @@ class TTSEngine:
print("OSError") print("OSError")
def call_tts(self, filename: str, text: str): 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", random_voice=settings.config["settings"]["tts"]["random_voice"])
# try: # try:
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length # self.length += MP3(f"{self.path}/{filename}.mp3").info.length
# except (MutagenError, HeaderNotFoundError): # except (MutagenError, HeaderNotFoundError):
@ -172,6 +178,6 @@ def process_text(text: str, clean: bool = True):
new_text = sanitize_text(text) if clean else text new_text = sanitize_text(text) if clean else text
if lang: if lang:
print_substep("Translating Text...") print_substep("Translating Text...")
translated_text = ts.google(text, to_language=lang) translated_text = translators.google(text, to_language=lang)
new_text = sanitize_text(translated_text) new_text = sanitize_text(translated_text)
return new_text return new_text

@ -6,6 +6,7 @@ from os import name
from pathlib import Path from pathlib import Path
from subprocess import Popen from subprocess import Popen
import ffmpeg
from prawcore import ResponseException from prawcore import ResponseException
from utils.console import print_substep from utils.console import print_substep
from reddit.subreddit import get_subreddit_threads from reddit.subreddit import get_subreddit_threads
@ -15,8 +16,9 @@ from utils.console import print_markdown, print_step
from utils.id import id from utils.id import id
from utils.version import checkversion from utils.version import checkversion
from video_creation.background import ( from video_creation.background import (
download_background, download_background_video,
chop_background_video, download_background_audio,
chop_background,
get_background_config, get_background_config,
) )
from video_creation.final_video import make_final_video from video_creation.final_video import make_final_video
@ -50,10 +52,18 @@ def main(POST_ID=None) -> None:
length, number_of_comments = save_text_to_mp3(reddit_object) length, number_of_comments = save_text_to_mp3(reddit_object)
length = math.ceil(length) length = math.ceil(length)
get_screenshots_of_reddit_posts(reddit_object, number_of_comments) get_screenshots_of_reddit_posts(reddit_object, number_of_comments)
bg_config = get_background_config() bg_config = {
download_background(bg_config) "video": get_background_config("video"),
chop_background_video(bg_config, length, reddit_object) "audio": get_background_config("audio"),
make_final_video(number_of_comments, length, reddit_object, bg_config) }
download_background_video(bg_config["video"])
download_background_audio(bg_config["audio"])
chop_background(bg_config, length, reddit_object)
try:
make_final_video(number_of_comments, length, reddit_object, bg_config)
except ffmpeg.Error as e:
print(e.stderr.decode("utf8"))
exit(1)
def run_many(times) -> None: def run_many(times) -> None:
@ -81,6 +91,7 @@ def shutdown():
if __name__ == "__main__": if __name__ == "__main__":
if sys.version_info.major != 3 or sys.version_info.minor != 10: if sys.version_info.major != 3 or sys.version_info.minor != 10:
print("Hey! Congratulations, you've made it so far (which is pretty rare with no Python 3.10). Unfortunately, this program only works on Python 3.10. Please install Python 3.10 and try again.") print("Hey! Congratulations, you've made it so far (which is pretty rare with no Python 3.10). Unfortunately, this program only works on Python 3.10. Please install Python 3.10 and try again.")
exit()
ffmpeg_install() # install ffmpeg if not installed ffmpeg_install() # install ffmpeg if not installed
directory = Path().absolute() directory = Path().absolute()
config = settings.check_toml( config = settings.check_toml(
@ -128,4 +139,4 @@ if __name__ == "__main__":
f"Error: {err} \n" f"Error: {err} \n"
f'Config: {config["settings"]}' f'Config: {config["settings"]}'
) )
raise err raise err

@ -5,19 +5,19 @@ moviepy==1.0.3
playwright==1.23.0 playwright==1.23.0
praw==7.6.1 praw==7.6.1
prawcore~=2.3.0 prawcore~=2.3.0
pytube==12.1.0
requests==2.28.1 requests==2.28.1
rich==13.3.1 rich==13.3.5
toml==0.10.2 toml==0.10.2
translators==5.3.1 translators==5.3.1
pyttsx3==2.90 pyttsx3==2.90
Pillow~=9.4.0 Pillow~=9.4.0
tomlkit==0.11.4 tomlkit==0.11.8
Flask==2.2.2 Flask==2.3.2
clean-text==0.6.0 clean-text==0.6.0
unidecode==1.3.2 unidecode==1.3.2
spacy==3.4.1 spacy==3.4.1
torch==1.12.1 torch==1.12.1
transformers==4.25.1 transformers==4.25.1
ffmpeg-python==0.2.0 ffmpeg-python==0.2.0
elevenlabs==0.2.10 elevenlabs==0.2.10
yt-dlp==2023.3.4

@ -31,11 +31,13 @@ storymodemethod= { optional = true, default = 1, example = 1, explanation = "Sty
storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." }
resolution_w = { optional = false, default = 1080, example = 1440, explantation = "Sets the width in pixels of the final video" } resolution_w = { optional = false, default = 1080, example = 1440, explantation = "Sets the width in pixels of the final video" }
resolution_h = { optional = false, default = 1920, example = 2560, explantation = "Sets the height in pixels of the final video" } resolution_h = { optional = false, default = 1920, example = 2560, explantation = "Sets the height in pixels of the final video" }
zoom = { optional = true, default = 1, example = 1.1, explanation = "Sets the browser zoom level. Useful if you want the text larger.", type = "float", nmin = 0.1, nmax = 2, oob_error = "The text is really difficult to read at a zoom level higher than 2" }
[settings.background] [settings.background]
background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", "minecraft-2","multiversus","fall-guys","steep", ""], explanation = "Sets the background for the video based on game name" } background_video = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", "minecraft-2","multiversus","fall-guys","steep", ""], 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 = { optional = true, default = "lofi", example = "chill-summer", options = ["lofi","lofi-2","chill-summer",""], explanation = "Sets the background audio for the video" }
#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" } background_audio_volume = { optional = true, type = "float", nmin = 0, nmax = 1, default = 0.15, example = 0.05, explanation="Sets the volume of the background audio. If you don't want background audio, set it to 0.", oob_error = "The volume HAS to be between 0 and 1", input_error = "The volume HAS to be a float number between 0 and 1"}
enable_extra_audio = { optional = true, type = "bool", default = false, example = false, explanation="Used if you want to render another video without background audio in a separate folder", input_error = "The value HAS to be true or false"}
background_thumbnail = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Generate a thumbnail for the video (put a thumbnail.png file in the assets/backgrounds directory.)" } background_thumbnail = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Generate a thumbnail for the video (put a thumbnail.png file in the assets/backgrounds directory.)" }
background_thumbnail_font_family = { optional = true, default = "arial", example = "arial", explanation = "Font family for the thumbnail text" } background_thumbnail_font_family = { optional = true, default = "arial", example = "arial", explanation = "Font family for the thumbnail text" }
background_thumbnail_font_size = { optional = true, type = "int", default = 96, example = 96, explanation = "Font size in pixels for the thumbnail text" } background_thumbnail_font_size = { optional = true, type = "int", default = 96, example = 96, explanation = "Font size in pixels for the thumbnail text" }
@ -43,12 +45,13 @@ background_thumbnail_font_color = { optional = true, default = "255,255,255", ex
[settings.tts] [settings.tts]
voice_choice = { optional = false, default = "tiktok", options = ["elevenlabs", "streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. " } voice_choice = { optional = false, default = "tiktok", options = ["elevenlabs", "streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. " }
random_voice = { optional = false, default = true, example = true, options = [true, false,], explanation = "Randomizes the voice used for each comment" }
elevenlabs_voice_name = { optional = false, default = "Bella", example = "Bella", explanation = "The voice used for elevenlabs", options = ["Adam", "Antoni", "Arnold", "Bella", "Domi", "Elli", "Josh", "Rachel", "Sam", ] } elevenlabs_voice_name = { optional = false, default = "Bella", example = "Bella", explanation = "The voice used for elevenlabs", options = ["Adam", "Antoni", "Arnold", "Bella", "Domi", "Elli", "Josh", "Rachel", "Sam", ] }
elevenlabs_api_key = { optional = true, example = "21f13f91f54d741e2ae27d2ab1b99d59", explanation = "Elevenlabs API key" } elevenlabs_api_key = { optional = true, example = "21f13f91f54d741e2ae27d2ab1b99d59", explanation = "Elevenlabs API key" }
aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } 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" } streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" }
tiktok_voice = { optional = true, default = "en_us_001", example = "en_us_006", explanation = "The voice used for TikTok TTS" } tiktok_voice = { optional = true, default = "en_us_001", example = "en_us_006", explanation = "The voice used for TikTok TTS" }
tiktok_sessionid = { optional = true, example = "c76bcc3a7625abcc27b508c7db457ff1", explanation = "TikTok sessionid needed for the TTS API request. Check documentation if you don't know how to obtain it." } tiktok_sessionid = { optional = true, example = "c76bcc3a7625abcc27b508c7db457ff1", explanation = "TikTok sessionid needed if you're using the TikTok TTS. Check documentation if you don't know how to obtain it." }
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 = { optional = false, default = "1", example = "1", explanation = "The index of the system tts voices (can be downloaded externally, run ptt.py to find value, start from zero)" }
py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" } py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" }
silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" } silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }

@ -0,0 +1,18 @@
{
"__comment": "Supported Backgrounds Audio. Can add/remove background audio here...",
"lofi": [
"https://www.youtube.com/watch?v=LTphVIore3A",
"lofi.mp3",
"Super Lofi World"
],
"lofi-2":[
"https://www.youtube.com/watch?v=BEXL80LS0-I",
"lofi-2.mp3",
"stompsPlaylist"
],
"chill-summer":[
"https://www.youtube.com/watch?v=EZE8JagnBI8",
"chill-summer.mp3",
"Mellow Vibes Radio"
]
}

@ -23,7 +23,7 @@ def ffmpeg_install_windows():
os.remove(f"ffmpeg/doc/{file}") os.remove(f"ffmpeg/doc/{file}")
os.rmdir("ffmpeg/doc") os.rmdir("ffmpeg/doc")
# Add to the path # Add to the path
subprocess.run("setx /M PATH \"%PATH%;%CD%\\ffmpeg\"", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run("setx PATH \"%PATH%;%CD%\\ffmpeg\"", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("FFmpeg installed successfully! Please restart your computer and then re-run the program.") print("FFmpeg installed successfully! Please restart your computer and then re-run the program.")
exit() exit()
except Exception as e: except Exception as e:
@ -81,4 +81,4 @@ def ffmpeg_install():
except Exception as e: except Exception as e:
print("Welcome fellow traveler! You're one of the few who have made it this far. We have no idea how you got at this error, but we're glad you're here. Please report this error to the developer, and we'll try to fix it as soon as possible. Thank you for your patience!") print("Welcome fellow traveler! You're one of the few who have made it this far. We have no idea how you got at this error, but we're glad you're here. Please report this error to the developer, and we'll try to fix it as soon as possible. Thank you for your patience!")
print(e) print(e)
return None return None

@ -68,9 +68,9 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) ->
tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 50) tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 50)
else: else:
tfont = ImageFont.truetype( tfont = ImageFont.truetype(
os.path.join("fonts", "Roboto-Bold.ttf"), 35 os.path.join("fonts", "Roboto-Bold.ttf"), 100
) # for title ) # for title
font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 30) font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 90)
size = (1920, 1080) size = (1920, 1080)
image = Image.new("RGBA", size, theme) image = Image.new("RGBA", size, theme)

@ -3,29 +3,38 @@ import random
import re import re
from pathlib import Path from pathlib import Path
from random import randrange from random import randrange
from typing import Any, Tuple from typing import Any, Tuple,Dict
from moviepy.editor import VideoFileClip from moviepy.editor import VideoFileClip,AudioFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from pytube import YouTube
from pytube.cli import on_progress
from utils import settings from utils import settings
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
import yt_dlp
# Load background videos def load_background_options():
with open("./utils/backgrounds.json") as json_file: background_options = {}
background_options = json.load(json_file) # Load background videos
with open("./utils/background_videos.json") as json_file:
background_options["video"] = json.load(json_file)
# Remove "__comment" from backgrounds # Load background audios
background_options.pop("__comment", None) with open("./utils/background_audios.json") as json_file:
background_options["audio"] = json.load(json_file)
# Remove "__comment" from backgrounds
del background_options["video"]["__comment"]
del background_options["audio"]["__comment"]
# Add position lambda function
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
for name in list(background_options["video"].keys()):
pos = background_options["video"][name][3]
# Add position lambda function if pos != "center":
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) background_options["video"][name][3] = lambda t: ("center", pos + t)
for name in list(background_options.keys()):
pos = background_options[name][3] return background_options
if pos != "center":
background_options[name][3] = lambda t: ("center", pos + t)
def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]:
@ -38,15 +47,22 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int
Returns: Returns:
tuple[int,int]: Start and end time of the randomized interval tuple[int,int]: Start and end time of the randomized interval
""" """
random_time = randrange(180, int(length_of_clip) - int(video_length)) initialValue = 180
# Issue #1649 - Ensures that will be a valid interval in the video
while(int(length_of_clip) <= int(video_length+initialValue)):
if(initialValue == initialValue //2):
raise Exception("Your background is too short for this video length")
else:
initialValue //= 2 #Divides the initial value by 2 until reach 0
random_time = randrange(initialValue, int(length_of_clip) - int(video_length))
return random_time, random_time + video_length return random_time, random_time + video_length
def get_background_config(): def get_background_config(mode: str):
"""Fetch the background/s configuration""" """Fetch the background/s configuration"""
try: try:
choice = str( choice = str(
settings.config["settings"]["background"]["background_choice"] settings.config["settings"]["background"][f"background_{mode}"]
).casefold() ).casefold()
except AttributeError: except AttributeError:
print_substep("No background selected. Picking random background'") print_substep("No background selected. Picking random background'")
@ -54,57 +70,98 @@ def get_background_config():
# Handle default / not supported background using default option. # Handle default / not supported background using default option.
# Default : pick random from supported background. # Default : pick random from supported background.
if not choice or choice not in background_options: if not choice or choice not in background_options[mode]:
choice = random.choice(list(background_options.keys())) choice = random.choice(list(background_options[mode].keys()))
return background_options[choice]
return background_options[mode][choice]
def download_background(background_config: Tuple[str, str, str, Any]): def download_background_video(background_config: Tuple[str, str, str, Any]):
"""Downloads the background/s video from YouTube.""" """Downloads the background/s video from YouTube."""
Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True) Path("./assets/backgrounds/video/").mkdir(parents=True, exist_ok=True)
# note: make sure the file name doesn't include an - in it # note: make sure the file name doesn't include an - in it
uri, filename, credit, _ = background_config uri, filename, credit, _ = background_config
if Path(f"assets/backgrounds/{credit}-{filename}").is_file(): if Path(f"assets/backgrounds/video/{credit}-{filename}").is_file():
return return
print_step( print_step(
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎" "We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
) )
print_substep("Downloading the backgrounds videos... please be patient 🙏 ") print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
print_substep(f"Downloading {filename} from {uri}") print_substep(f"Downloading {filename} from {uri}")
YouTube(uri, on_progress_callback=on_progress).streams.filter( ydl_opts = {
res="1080p" 'format': "bestvideo[height<=1080][ext=mp4]",
).first().download("assets/backgrounds", filename=f"{credit}-{filename}") "outtmpl": f"assets/backgrounds/video/{credit}-{filename}",
"retries": 10,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download(uri)
print_substep("Background video downloaded successfully! 🎉", style="bold green") print_substep("Background video downloaded successfully! 🎉", style="bold green")
def download_background_audio(background_config: Tuple[str, str, str]):
"""Downloads the background/s audio from YouTube."""
Path("./assets/backgrounds/audio/").mkdir(parents=True, exist_ok=True)
# note: make sure the file name doesn't include an - in it
uri, filename, credit = background_config
if Path(f"assets/backgrounds/audio/{credit}-{filename}").is_file():
return
print_step(
"We need to download the backgrounds audio. they are fairly large but it's only done once. 😎"
)
print_substep("Downloading the backgrounds audio... please be patient 🙏 ")
print_substep(f"Downloading {filename} from {uri}")
ydl_opts = {
'outtmpl': f'./assets/backgrounds/audio/{credit}-{filename}',
'format': 'bestaudio/best',
'extract_audio': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([uri])
def chop_background_video( print_substep("Background audio downloaded successfully! 🎉", style="bold green")
background_config: Tuple[str, str, str, Any], video_length: int, reddit_object: dict
def chop_background(
background_config: Dict[str,Tuple], video_length: int, reddit_object: dict
): ):
"""Generates the background footage to be used in the video and writes it to assets/temp/background.mp4 """Generates the background audio and footage to be used in the video and writes it to assets/temp/background.mp3 and assets/temp/background.mp4
Args: Args:
background_config (Tuple[str, str, str, Any]) : Current background configuration background_config (Dict[str,Tuple]]) : Current background configuration
video_length (int): Length of the clip where the background footage is to be taken out of video_length (int): Length of the clip where the background footage is to be taken out of
""" """
print_step("Finding a spot in the backgrounds video to chop...✂️")
choice = f"{background_config[2]}-{background_config[1]}"
id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
background = VideoFileClip(f"assets/backgrounds/{choice}")
start_time, end_time = get_start_and_end_times(video_length, background.duration) if(settings.config["settings"]["background"][f"background_audio_volume"] == 0):
print_step("Volume was set to 0. Skipping background audio creation . . .")
else:
print_step("Finding a spot in the backgrounds audio to chop...✂️")
audio_choice = f"{background_config['audio'][2]}-{background_config['audio'][1]}"
background_audio = AudioFileClip(f"assets/backgrounds/audio/{audio_choice}")
start_time_audio, end_time_audio = get_start_and_end_times(video_length, background_audio.duration)
background_audio = background_audio.subclip(start_time_audio,end_time_audio)
background_audio.write_audiofile(f"assets/temp/{id}/background.mp3")
print_step("Finding a spot in the backgrounds video to chop...✂️")
video_choice = f"{background_config['video'][2]}-{background_config['video'][1]}"
background_video = VideoFileClip(f"assets/backgrounds/video/{video_choice}")
start_time_video, end_time_video = get_start_and_end_times(video_length, background_video.duration)
# Extract video subclip
try: try:
ffmpeg_extract_subclip( ffmpeg_extract_subclip(
f"assets/backgrounds/{choice}", f"assets/backgrounds/video/{video_choice}",
start_time, start_time_video,
end_time, end_time_video,
targetname=f"assets/temp/{id}/background.mp4", targetname=f"assets/temp/{id}/background.mp4",
) )
except (OSError, IOError): # ffmpeg issue see #348 except (OSError, IOError): # ffmpeg issue see #348
print_substep("FFMPEG issue. Trying again...") print_substep("FFMPEG issue. Trying again...")
with VideoFileClip(f"assets/backgrounds/{choice}") as video: with VideoFileClip(f"assets/backgrounds/video/{video_choice}") as video:
new = video.subclip(start_time, end_time) new = video.subclip(start_time_video, end_time_video)
new.write_videofile(f"assets/temp/{id}/background.mp4") new.write_videofile(f"assets/temp/{id}/background.mp4")
print_substep("Background video chopped successfully!", style="bold green") print_substep("Background video chopped successfully!", style="bold green")
return background_config[2] return background_config["video"][2]
# Create a tuple for downloads background (background_audio_options, background_video_options)
background_options = load_background_options()

@ -1,13 +1,12 @@
import multiprocessing import multiprocessing
import os import os
import re import re
import shutil
from os.path import exists # Needs to be imported specifically from os.path import exists # Needs to be imported specifically
from typing import Final from typing import Final
from typing import Tuple, Any from typing import Tuple, Any, Dict
import ffmpeg import ffmpeg
import translators as ts import translators
from PIL import Image from PIL import Image
from rich.console import Console from rich.console import Console
from rich.progress import track from rich.progress import track
@ -18,12 +17,12 @@ from utils.console import print_step, print_substep
from utils.thumbnail import create_thumbnail from utils.thumbnail import create_thumbnail
from utils.videos import save_data from utils.videos import save_data
console = Console()
import tempfile import tempfile
import threading import threading
import time import time
console = Console()
class ProgressFfmpeg(threading.Thread): class ProgressFfmpeg(threading.Thread):
def __init__(self, vid_duration_seconds, progress_update_callback): def __init__(self, vid_duration_seconds, progress_update_callback):
@ -73,7 +72,7 @@ def name_normalize(name: str) -> str:
lang = settings.config["reddit"]["thread"]["post_lang"] lang = settings.config["reddit"]["thread"]["post_lang"]
if lang: if lang:
print_substep("Translating filename...") print_substep("Translating filename...")
translated_name = ts.google(name, to_language=lang) translated_name = translators.google(name, to_language=lang)
return translated_name return translated_name
else: else:
return name return name
@ -104,11 +103,34 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
return output_path return output_path
def merge_background_audio(audio: ffmpeg, reddit_id: str):
"""Gather an audio and merge with assets/backgrounds/background.mp3
Args:
audio (ffmpeg): The TTS final audio but without background.
reddit_id (str): The ID of subreddit
"""
background_audio_volume = settings.config["settings"]["background"]["background_audio_volume"]
if (background_audio_volume == 0):
return audio # Return the original audio
else:
# sets volume to config
bg_audio = (
ffmpeg.input(f"assets/temp/{reddit_id}/background.mp3")
.filter(
"volume",
background_audio_volume,
)
)
# Merges audio and background_audio
merged_audio = ffmpeg.filter([audio, bg_audio], "amix", duration="longest")
return merged_audio # Return merged audio
def make_final_video( def make_final_video(
number_of_clips: int, number_of_clips: int,
length: int, length: int,
reddit_obj: dict, reddit_obj: dict,
background_config: Tuple[str, str, str, Any], background_config: Dict[str,Tuple],
): ):
"""Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp """Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp
Args: Args:
@ -122,6 +144,10 @@ def make_final_video(
H: Final[int] = int(settings.config["settings"]["resolution_h"]) H: Final[int] = int(settings.config["settings"]["resolution_h"])
reddit_id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) reddit_id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
allowOnlyTTSFolder: bool = settings.config["settings"]["background"]["enable_extra_audio"] \
and settings.config["settings"]["background"]["background_audio_volume"] != 0
print_step("Creating the final video 🎥") print_step("Creating the final video 🎥")
background_clip = ffmpeg.input(prepare_background(reddit_id, W=W, H=H)) background_clip = ffmpeg.input(prepare_background(reddit_id, W=W, H=H))
@ -177,6 +203,7 @@ def make_final_video(
screenshot_width = int((W * 45) // 100) screenshot_width = int((W * 45) // 100)
audio = ffmpeg.input(f"assets/temp/{reddit_id}/audio.mp3") audio = ffmpeg.input(f"assets/temp/{reddit_id}/audio.mp3")
final_audio = merge_background_audio(audio,reddit_id)
image_clips = list() image_clips = list()
@ -258,15 +285,19 @@ def make_final_video(
subreddit = settings.config["reddit"]["thread"]["subreddit"] subreddit = settings.config["reddit"]["thread"]["subreddit"]
if not exists(f"./results/{subreddit}"): if not exists(f"./results/{subreddit}"):
print_substep("The results folder didn't exist so I made it") print_substep("The 'results' folder could not be found so it was automatically created.")
os.makedirs(f"./results/{subreddit}") os.makedirs(f"./results/{subreddit}")
if not exists(f"./results/{subreddit}/OnlyTTS") and allowOnlyTTSFolder:
print_substep("The 'OnlyTTS' folder could not be found so it was automatically created.")
os.makedirs(f"./results/{subreddit}/OnlyTTS")
# create a thumbnail for the video # create a thumbnail for the video
settingsbackground = settings.config["settings"]["background"] settingsbackground = settings.config["settings"]["background"]
if settingsbackground["background_thumbnail"]: if settingsbackground["background_thumbnail"]:
if not exists(f"./results/{subreddit}/thumbnails"): if not exists(f"./results/{subreddit}/thumbnails"):
print_substep("The results/thumbnails folder didn't exist so I made it") print_substep("The 'results/thumbnails' folder could not be found so it was automatically created.")
os.makedirs(f"./results/{subreddit}/thumbnails") os.makedirs(f"./results/{subreddit}/thumbnails")
# get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail # get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail
first_image = next( first_image = next(
@ -300,7 +331,7 @@ def make_final_video(
f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png" f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png"
) )
text = f"Background by {background_config[2]}" text = f"Background by {background_config['video'][2]}"
background_clip = ffmpeg.drawtext( background_clip = ffmpeg.drawtext(
background_clip, background_clip,
text=text, text=text,
@ -320,15 +351,14 @@ def make_final_video(
old_percentage = pbar.n old_percentage = pbar.n
pbar.update(status - old_percentage) pbar.update(status - old_percentage)
path = f"results/{subreddit}/{filename}" defaultPath = f"results/{subreddit}"
path = path[:251]
path = path + ".mp4"
with ProgressFfmpeg(length, on_update_example) as progress: with ProgressFfmpeg(length, on_update_example) as progress:
path = defaultPath + f"/{filename}"
path = path[:251] + ".mp4" #Prevent a error by limiting the path length, do not change this.
ffmpeg.output( ffmpeg.output(
background_clip, background_clip,
audio, final_audio,
path, path,
f="mp4", f="mp4",
**{ **{
"c:v": "h264", "c:v": "h264",
@ -342,13 +372,36 @@ def make_final_video(
capture_stdout=False, capture_stdout=False,
capture_stderr=False, capture_stderr=False,
) )
old_percentage = pbar.n old_percentage = pbar.n
pbar.update(100 - old_percentage) pbar.update(100 - old_percentage)
if(allowOnlyTTSFolder):
path = defaultPath + f"/OnlyTTS/{filename}"
path = path[:251] + ".mp4" #Prevent a error by limiting the path length, do not change this.
print_step("Rendering the Only TTS Video 🎥")
with ProgressFfmpeg(length, on_update_example) as progress:
ffmpeg.output(
background_clip,
audio,
path,
f="mp4",
**{
"c:v": "h264",
"b:v": "20M",
"b:a": "192k",
"threads": multiprocessing.cpu_count(),
},
).overwrite_output().global_args("-progress", progress.output_file.name).run(
quiet=True,
overwrite_output=True,
capture_stdout=False,
capture_stderr=False,
)
old_percentage = pbar.n
pbar.update(100 - old_percentage)
pbar.close() pbar.close()
save_data(subreddit, filename + ".mp4", title, idx, background_config[2]) save_data(subreddit, filename + ".mp4", title, idx, background_config['video'][2])
print_step("Removing temporary files 🗑") print_step("Removing temporary files 🗑")
cleanups = cleanup(reddit_id) cleanups = cleanup(reddit_id)
print_substep(f"Removed {cleanups} temporary files 🗑") print_substep(f"Removed {cleanups} temporary files 🗑")
print_step("Done! 🎉 The video is in the results folder 📁") print_step("Done! 🎉 The video is in the results folder 📁")

@ -3,7 +3,7 @@ import re
from pathlib import Path from pathlib import Path
from typing import Dict, Final from typing import Dict, Final
import translators as ts import translators
from playwright.async_api import async_playwright # pylint: disable=unused-import from playwright.async_api import async_playwright # pylint: disable=unused-import
from playwright.sync_api import ViewportSize, sync_playwright from playwright.sync_api import ViewportSize, sync_playwright
from rich.progress import track from rich.progress import track
@ -115,6 +115,19 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
) )
page.locator("button[class$='m-full-width']").click() page.locator("button[class$='m-full-width']").click()
page.wait_for_timeout(5000) page.wait_for_timeout(5000)
login_error_div = page.locator(".AnimatedForm__errorMessage").first
if login_error_div.is_visible():
login_error_message = login_error_div.inner_text()
if login_error_message.strip() == "":
# The div element is empty, no error
pass
else:
# The div contains an error message
print_substep("Your reddit credentials are incorrect! Please modify them accordingly in the config.toml file.", style="red")
exit()
else:
pass
page.wait_for_load_state() page.wait_for_load_state()
# Get the thread screenshot # Get the thread screenshot
@ -144,7 +157,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
if lang: if lang:
print_substep("Translating post...") print_substep("Translating post...")
texts_in_tl = ts.google( texts_in_tl = translators.google(
reddit_object["thread_title"], reddit_object["thread_title"],
to_language=lang, to_language=lang,
) )
@ -158,9 +171,20 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
postcontentpath = f"assets/temp/{reddit_id}/png/title.png" postcontentpath = f"assets/temp/{reddit_id}/png/title.png"
try: try:
page.locator('[data-test-id="post-content"]').screenshot( if settings.config["settings"]["zoom"] != 1:
path=postcontentpath # store zoom settings
) zoom = settings.config["settings"]["zoom"]
# zoom the body of the page
page.evaluate("document.body.style.zoom="+str(zoom))
# as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom
location = page.locator('[data-test-id="post-content"]').bounding_box()
for i in location:
location[i] = float("{:.2f}".format(location[i]*zoom))
page.screenshot(clip=location, path=postcontentpath)
else:
page.locator('[data-test-id="post-content"]').screenshot(
path=postcontentpath
)
except Exception as e: except Exception as e:
print_substep("Something went wrong!", style="red") print_substep("Something went wrong!", style="red")
resp = input( resp = input(
@ -205,7 +229,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# translate code # translate code
if settings.config["reddit"]["thread"]["post_lang"]: if settings.config["reddit"]["thread"]["post_lang"]:
comment_tl = ts.google( comment_tl = translators.google(
comment["comment_body"], comment["comment_body"],
to_language=settings.config["reddit"]["thread"]["post_lang"], to_language=settings.config["reddit"]["thread"]["post_lang"],
) )
@ -214,9 +238,22 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
[comment_tl, comment["comment_id"]], [comment_tl, comment["comment_id"]],
) )
try: try:
page.locator(f"#t1_{comment['comment_id']}").screenshot( if settings.config["settings"]["zoom"] != 1:
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png" # store zoom settings
) zoom = settings.config["settings"]["zoom"]
# zoom the body of the page
page.evaluate("document.body.style.zoom="+str(zoom))
# scroll comment into view
page.locator(f"#t1_{comment['comment_id']}").scroll_into_view_if_needed()
# as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom
location = page.locator(f"#t1_{comment['comment_id']}").bounding_box()
for i in location:
location[i] = float("{:.2f}".format(location[i]*zoom))
page.screenshot(clip=location, path=f"assets/temp/{reddit_id}/png/comment_{idx}.png")
else:
page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png"
)
except TimeoutError: except TimeoutError:
del reddit_object["comments"] del reddit_object["comments"]
screenshot_num += 1 screenshot_num += 1

Loading…
Cancel
Save