Merge branch 'develop' into elevenlabs
This commit is contained in:
15 files changed
+299
-100
No files matched your search
@@ -29,4 +29,4 @@ jobs:
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY
|
||||
git checkout $GITHUB_HEAD_REF
|
||||
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
|
||||
|
||||
RUN apt update
|
||||
RUN apt-get install -y ffmpeg
|
||||
RUN apt install python3-pip -y
|
||||
|
||||
RUN mkdir /app
|
||||
|
||||
+14
-1
@@ -56,6 +56,13 @@
|
||||
.tooltip-inner {
|
||||
max-width: 500px !important;
|
||||
}
|
||||
#hard-reload {
|
||||
cursor: pointer;
|
||||
color: darkblue;
|
||||
}
|
||||
#hard-reload:hover {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -132,11 +139,17 @@
|
||||
Theme by © Bootstrap. <a
|
||||
href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers"
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
document.getElementById("hard-reload").addEventListener("click", function () {
|
||||
window.location.reload(true);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</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!
|
||||
|
||||
- [ ] 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 background that is picked instead of the Minecraft one.
|
||||
- [x] Allowing users to choose between any subreddit.
|
||||
|
||||
+12
-6
@@ -3,11 +3,8 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
# import sox
|
||||
# from mutagen import MutagenError
|
||||
# from mutagen.mp3 import MP3, HeaderNotFoundError
|
||||
import numpy as np
|
||||
import translators as ts
|
||||
import translators
|
||||
from moviepy.audio.AudioClip import AudioClip
|
||||
from moviepy.audio.fx.volumex import volumex
|
||||
from moviepy.editor import AudioFileClip
|
||||
@@ -55,9 +52,18 @@ class TTSEngine:
|
||||
self,
|
||||
): # 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"]:
|
||||
# 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"] = 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] != ".":
|
||||
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]:
|
||||
Path(self.path).mkdir(parents=True, exist_ok=True)
|
||||
@@ -141,7 +147,7 @@ class TTSEngine:
|
||||
print("OSError")
|
||||
|
||||
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:
|
||||
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length
|
||||
# except (MutagenError, HeaderNotFoundError):
|
||||
@@ -172,6 +178,6 @@ def process_text(text: str, clean: bool = True):
|
||||
new_text = sanitize_text(text) if clean else text
|
||||
if lang:
|
||||
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)
|
||||
return new_text
|
||||
@@ -6,6 +6,7 @@ from os import name
|
||||
from pathlib import Path
|
||||
from subprocess import Popen
|
||||
|
||||
import ffmpeg
|
||||
from prawcore import ResponseException
|
||||
from utils.console import print_substep
|
||||
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.version import checkversion
|
||||
from video_creation.background import (
|
||||
download_background,
|
||||
chop_background_video,
|
||||
download_background_video,
|
||||
download_background_audio,
|
||||
chop_background,
|
||||
get_background_config,
|
||||
)
|
||||
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 = math.ceil(length)
|
||||
get_screenshots_of_reddit_posts(reddit_object, number_of_comments)
|
||||
bg_config = get_background_config()
|
||||
download_background(bg_config)
|
||||
chop_background_video(bg_config, length, reddit_object)
|
||||
make_final_video(number_of_comments, length, reddit_object, bg_config)
|
||||
bg_config = {
|
||||
"video": get_background_config("video"),
|
||||
"audio": get_background_config("audio"),
|
||||
}
|
||||
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:
|
||||
@@ -81,6 +91,7 @@ def shutdown():
|
||||
if __name__ == "__main__":
|
||||
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.")
|
||||
exit()
|
||||
ffmpeg_install() # install ffmpeg if not installed
|
||||
directory = Path().absolute()
|
||||
config = settings.check_toml(
|
||||
@@ -128,4 +139,4 @@ if __name__ == "__main__":
|
||||
f"Error: {err} \n"
|
||||
f'Config: {config["settings"]}'
|
||||
)
|
||||
raise err
|
||||
raise err
|
||||
+5
-5
@@ -5,19 +5,19 @@ moviepy==1.0.3
|
||||
playwright==1.23.0
|
||||
praw==7.6.1
|
||||
prawcore~=2.3.0
|
||||
pytube==12.1.0
|
||||
requests==2.28.1
|
||||
rich==13.3.1
|
||||
rich==13.3.5
|
||||
toml==0.10.2
|
||||
translators==5.3.1
|
||||
pyttsx3==2.90
|
||||
Pillow~=9.4.0
|
||||
tomlkit==0.11.4
|
||||
Flask==2.2.2
|
||||
tomlkit==0.11.8
|
||||
Flask==2.3.2
|
||||
clean-text==0.6.0
|
||||
unidecode==1.3.2
|
||||
spacy==3.4.1
|
||||
torch==1.12.1
|
||||
transformers==4.25.1
|
||||
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." }
|
||||
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" }
|
||||
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]
|
||||
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_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" }
|
||||
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, 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", 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_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" }
|
||||
@@ -43,12 +45,13 @@ background_thumbnail_font_color = { optional = true, default = "255,255,255", ex
|
||||
|
||||
[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. " }
|
||||
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_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" }
|
||||
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_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)" }
|
||||
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" }
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
File renamed without changes.
@@ -23,7 +23,7 @@ def ffmpeg_install_windows():
|
||||
os.remove(f"ffmpeg/doc/{file}")
|
||||
os.rmdir("ffmpeg/doc")
|
||||
# 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.")
|
||||
exit()
|
||||
except Exception as e:
|
||||
@@ -81,4 +81,4 @@ def ffmpeg_install():
|
||||
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(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)
|
||||
else:
|
||||
tfont = ImageFont.truetype(
|
||||
os.path.join("fonts", "Roboto-Bold.ttf"), 35
|
||||
os.path.join("fonts", "Roboto-Bold.ttf"), 100
|
||||
) # 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)
|
||||
|
||||
image = Image.new("RGBA", size, theme)
|
||||
|
||||
+100
-43
@@ -3,29 +3,38 @@ import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
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 pytube import YouTube
|
||||
from pytube.cli import on_progress
|
||||
from utils import settings
|
||||
from utils.console import print_step, print_substep
|
||||
import yt_dlp
|
||||
|
||||
# Load background videos
|
||||
with open("./utils/backgrounds.json") as json_file:
|
||||
background_options = json.load(json_file)
|
||||
def load_background_options():
|
||||
background_options = {}
|
||||
# Load background videos
|
||||
with open("./utils/background_videos.json") as json_file:
|
||||
background_options["video"] = json.load(json_file)
|
||||
|
||||
# Remove "__comment" from backgrounds
|
||||
background_options.pop("__comment", None)
|
||||
# Load background audios
|
||||
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
|
||||
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
|
||||
for name in list(background_options.keys()):
|
||||
pos = background_options[name][3]
|
||||
if pos != "center":
|
||||
background_options["video"][name][3] = lambda t: ("center", pos + t)
|
||||
|
||||
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]:
|
||||
@@ -38,15 +47,22 @@ def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int
|
||||
Returns:
|
||||
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
|
||||
|
||||
|
||||
def get_background_config():
|
||||
def get_background_config(mode: str):
|
||||
"""Fetch the background/s configuration"""
|
||||
try:
|
||||
choice = str(
|
||||
settings.config["settings"]["background"]["background_choice"]
|
||||
settings.config["settings"]["background"][f"background_{mode}"]
|
||||
).casefold()
|
||||
except AttributeError:
|
||||
print_substep("No background selected. Picking random background'")
|
||||
@@ -54,57 +70,98 @@ def get_background_config():
|
||||
|
||||
# Handle default / not supported background using default option.
|
||||
# Default : pick random from supported background.
|
||||
if not choice or choice not in background_options:
|
||||
choice = random.choice(list(background_options.keys()))
|
||||
if not choice or choice not in background_options[mode]:
|
||||
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."""
|
||||
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
|
||||
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
|
||||
print_step(
|
||||
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
|
||||
)
|
||||
print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
|
||||
print_substep(f"Downloading {filename} from {uri}")
|
||||
YouTube(uri, on_progress_callback=on_progress).streams.filter(
|
||||
res="1080p"
|
||||
).first().download("assets/backgrounds", filename=f"{credit}-{filename}")
|
||||
ydl_opts = {
|
||||
'format': "bestvideo[height<=1080][ext=mp4]",
|
||||
"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")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
def chop_background_video(
|
||||
background_config: Tuple[str, str, str, Any], video_length: int, reddit_object: dict
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([uri])
|
||||
|
||||
print_substep("Background audio downloaded successfully! 🎉", style="bold green")
|
||||
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
"""
|
||||
id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"])
|
||||
|
||||
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...✂️")
|
||||
choice = f"{background_config[2]}-{background_config[1]}"
|
||||
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)
|
||||
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:
|
||||
ffmpeg_extract_subclip(
|
||||
f"assets/backgrounds/{choice}",
|
||||
start_time,
|
||||
end_time,
|
||||
f"assets/backgrounds/video/{video_choice}",
|
||||
start_time_video,
|
||||
end_time_video,
|
||||
targetname=f"assets/temp/{id}/background.mp4",
|
||||
)
|
||||
except (OSError, IOError): # ffmpeg issue see #348
|
||||
print_substep("FFMPEG issue. Trying again...")
|
||||
with VideoFileClip(f"assets/backgrounds/{choice}") as video:
|
||||
new = video.subclip(start_time, end_time)
|
||||
with VideoFileClip(f"assets/backgrounds/video/{video_choice}") as video:
|
||||
new = video.subclip(start_time_video, end_time_video)
|
||||
new.write_videofile(f"assets/temp/{id}/background.mp4")
|
||||
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 os
|
||||
import re
|
||||
import shutil
|
||||
from os.path import exists # Needs to be imported specifically
|
||||
from typing import Final
|
||||
from typing import Tuple, Any
|
||||
from typing import Tuple, Any, Dict
|
||||
|
||||
import ffmpeg
|
||||
import translators as ts
|
||||
import translators
|
||||
from PIL import Image
|
||||
from rich.console import Console
|
||||
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.videos import save_data
|
||||
|
||||
console = Console()
|
||||
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class ProgressFfmpeg(threading.Thread):
|
||||
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"]
|
||||
if lang:
|
||||
print_substep("Translating filename...")
|
||||
translated_name = ts.google(name, to_language=lang)
|
||||
translated_name = translators.google(name, to_language=lang)
|
||||
return translated_name
|
||||
else:
|
||||
return name
|
||||
@@ -104,11 +103,34 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
|
||||
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(
|
||||
number_of_clips: int,
|
||||
length: int,
|
||||
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
|
||||
Args:
|
||||
@@ -122,6 +144,10 @@ def make_final_video(
|
||||
H: Final[int] = int(settings.config["settings"]["resolution_h"])
|
||||
|
||||
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 🎥")
|
||||
|
||||
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)
|
||||
audio = ffmpeg.input(f"assets/temp/{reddit_id}/audio.mp3")
|
||||
final_audio = merge_background_audio(audio,reddit_id)
|
||||
|
||||
image_clips = list()
|
||||
|
||||
@@ -258,15 +285,19 @@ def make_final_video(
|
||||
subreddit = settings.config["reddit"]["thread"]["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}")
|
||||
|
||||
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
|
||||
settingsbackground = settings.config["settings"]["background"]
|
||||
|
||||
if settingsbackground["background_thumbnail"]:
|
||||
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")
|
||||
# get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail
|
||||
first_image = next(
|
||||
@@ -300,7 +331,7 @@ def make_final_video(
|
||||
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,
|
||||
text=text,
|
||||
@@ -320,15 +351,14 @@ def make_final_video(
|
||||
old_percentage = pbar.n
|
||||
pbar.update(status - old_percentage)
|
||||
|
||||
path = f"results/{subreddit}/{filename}"
|
||||
path = path[:251]
|
||||
path = path + ".mp4"
|
||||
|
||||
defaultPath = f"results/{subreddit}"
|
||||
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(
|
||||
background_clip,
|
||||
audio,
|
||||
path,
|
||||
final_audio,
|
||||
path,
|
||||
f="mp4",
|
||||
**{
|
||||
"c:v": "h264",
|
||||
@@ -342,13 +372,36 @@ def make_final_video(
|
||||
capture_stdout=False,
|
||||
capture_stderr=False,
|
||||
)
|
||||
|
||||
old_percentage = pbar.n
|
||||
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()
|
||||
|
||||
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 🗑")
|
||||
cleanups = cleanup(reddit_id)
|
||||
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 typing import Dict, Final
|
||||
|
||||
import translators as ts
|
||||
import translators
|
||||
from playwright.async_api import async_playwright # pylint: disable=unused-import
|
||||
from playwright.sync_api import ViewportSize, sync_playwright
|
||||
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.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()
|
||||
# Get the thread screenshot
|
||||
@@ -144,7 +157,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
|
||||
|
||||
if lang:
|
||||
print_substep("Translating post...")
|
||||
texts_in_tl = ts.google(
|
||||
texts_in_tl = translators.google(
|
||||
reddit_object["thread_title"],
|
||||
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"
|
||||
try:
|
||||
page.locator('[data-test-id="post-content"]').screenshot(
|
||||
path=postcontentpath
|
||||
)
|
||||
if settings.config["settings"]["zoom"] != 1:
|
||||
# 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:
|
||||
print_substep("Something went wrong!", style="red")
|
||||
resp = input(
|
||||
@@ -205,7 +229,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
|
||||
# translate code
|
||||
|
||||
if settings.config["reddit"]["thread"]["post_lang"]:
|
||||
comment_tl = ts.google(
|
||||
comment_tl = translators.google(
|
||||
comment["comment_body"],
|
||||
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"]],
|
||||
)
|
||||
try:
|
||||
page.locator(f"#t1_{comment['comment_id']}").screenshot(
|
||||
path=f"assets/temp/{reddit_id}/png/comment_{idx}.png"
|
||||
)
|
||||
if settings.config["settings"]["zoom"] != 1:
|
||||
# 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:
|
||||
del reddit_object["comments"]
|
||||
screenshot_num += 1
|
||||
|
||||
Reference in new issue
Block a user