Various features and bug fixes

pull/1873/head
KyleBoyer 2 years ago
parent b47334f4c6
commit e04883563e

@ -1 +1,5 @@
Dockerfile Dockerfile
out
results
assets

@ -1,16 +1,19 @@
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 ffmpeg python3-pip rubberband-cli espeak python3-pyaudio -y
RUN apt install python3-pip -y
RUN python3 -m pip install --upgrade pip
RUN mkdir /app RUN mkdir /app
ADD . /app
WORKDIR /app WORKDIR /app
ADD requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
RUN python3 -m spacy download en_core_web_sm
ADD . /app
# tricks for pytube : https://github.com/elebumm/RedditVideoMakerBot/issues/142 # tricks for pytube : https://github.com/elebumm/RedditVideoMakerBot/issues/142
# (NOTE : This is no longer useful since pytube was removed from the dependencies) # (NOTE : This is no longer useful since pytube was removed from the dependencies)
# RUN sed -i 's/re.compile(r"^\\w+\\W")/re.compile(r"^\\$*\\w+\\W")/' /usr/local/lib/python3.8/dist-packages/pytube/cipher.py # RUN sed -i 's/re.compile(r"^\\w+\\W")/re.compile(r"^\\$*\\w+\\W")/' /usr/local/lib/python3.8/dist-packages/pytube/cipher.py
RUN echo ZmluZCAvIC1wYXRoICcqL21vdmllcHkvdG9vbHMucHknIC1wcmludDAgfCB3aGlsZSByZWFkIC1kICQnXDAnIGZpbGU7IGRvCiAgc2VkIC1pIC1lICdzLyciJyInTW92aWVweSAtIFJ1bm5pbmc6XFxuPj4+ICIrICIgIi5qb2luKGNtZCknIiciJy8iTW92aWVweSAtIFJ1bm5pbmc6XFxuPj4+ICIgKyAiICIuam9pbihjbWQpL2cnICIkZmlsZSIKZG9uZQ== | base64 --decode | bash
CMD ["python3", "main.py"] CMD ["python3", "main.py"]

@ -10,7 +10,7 @@ class GTTS:
self.max_chars = 5000 self.max_chars = 5000
self.voices = [] self.voices = []
def run(self, text, filepath): def run(self, text, filepath, random_voice):
tts = gTTS( tts = gTTS(
text=text, text=text,
lang=settings.config["reddit"]["thread"]["post_lang"] or "en", lang=settings.config["reddit"]["thread"]["post_lang"] or "en",

@ -1,9 +1,9 @@
# documentation for tiktok api: https://github.com/oscie57/tiktok-voice/wiki # documentation for tiktok api: https://github.com/oscie57/tiktok-voice/wiki
import base64 import base64
import random import random
import time import time
from typing import Optional, Final from typing import Optional, Final
import requests import requests
from utils import settings from utils import settings
@ -34,7 +34,6 @@ eng_voices: Final[tuple] = (
"en_us_009", # English US - Male 3 "en_us_009", # English US - Male 3
"en_us_010", # English US - Male 4 "en_us_010", # English US - Male 4
"en_male_narration", # Narrator "en_male_narration", # Narrator
"en_male_funny", # Funny
"en_female_emotional", # Peaceful "en_female_emotional", # Peaceful
"en_male_cody", # Serious "en_male_cody", # Serious
) )
@ -86,7 +85,9 @@ class TikTok:
"Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}", "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}",
} }
self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/" self.URI_BASE = (
"https://tiktok-tts.weilnet.workers.dev/api/generation"
)
self.max_chars = 200 self.max_chars = 200
self._session = requests.Session() self._session = requests.Session()
@ -102,15 +103,12 @@ class TikTok:
# get the audio from the TikTok API # get the audio from the TikTok API
data = self.get_voices(voice=voice, text=text) data = self.get_voices(voice=voice, text=text)
# check if there was an error in the request # check if there was an error in the request
status_code = data["status_code"] status_code = data["error"]
if status_code != 0:
raise TikTokTTSException(status_code, data["message"])
# decode data from base64 to binary # decode data from base64 to binary
try: try:
raw_voices = data["data"]["v_str"] raw_voices = data["data"]
except: except:
print( print(
"The TikTok TTS returned an invalid response. Please try again later, and report this bug." "The TikTok TTS returned an invalid response. Please try again later, and report this bug."
@ -128,17 +126,16 @@ class TikTok:
text = text.replace("+", "plus").replace("&", "and").replace("r/", "") text = text.replace("+", "plus").replace("&", "and").replace("r/", "")
# prepare url request # prepare url request
params = {"req_text": text, "speaker_map_type": 0, "aid": 1233} params = {"text": text,"voice": voice}
if voice is not None: if voice is not None:
params["text_speaker"] = voice params["voice"] = voice
# send request # send request
try: try:
response = self._session.post(self.URI_BASE, params=params) response = self._session.post(self.URI_BASE, json=params)
except ConnectionError: except ConnectionError:
time.sleep(random.randrange(1, 7)) time.sleep(random.randrange(1, 7))
response = self._session.post(self.URI_BASE, params=params) response = self._session.post(self.URI_BASE, json=params)
return response.json() return response.json()
@ -148,18 +145,10 @@ class TikTok:
class TikTokTTSException(Exception): class TikTokTTSException(Exception):
def __init__(self, code: int, message: str): def __init__(self, status_code, message):
self._code = code self.status_code = status_code
self._message = message self.message = message
super().__init__(self.message)
def __str__(self) -> str:
if self._code == 1:
return f"Code: {self._code}, reason: probably the aid value isn't correct, message: {self._message}"
if self._code == 2:
return f"Code: {self._code}, reason: the text is too long, message: {self._message}"
if self._code == 4:
return f"Code: {self._code}, reason: the speaker doesn't exist, message: {self._message}"
return f"Code: {self._message}, reason: unknown, message: {self._message}" def __str__(self):
return f"Code: {self.status_code}, Message: {self.message}"

@ -1,5 +1,6 @@
import os import os
import re import re
import ffmpeg
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple
@ -14,6 +15,8 @@ from utils import settings
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from utils.voice import sanitize_text from utils.voice import sanitize_text
from pydub import AudioSegment
DEFAULT_MAX_LENGTH: int = ( DEFAULT_MAX_LENGTH: int = (
50 # Video length variable, edit this on your own risk. It should work, but it's not supported 50 # Video length variable, edit this on your own risk. It should work, but it's not supported
@ -84,11 +87,11 @@ class TTSEngine:
else: else:
self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) self.call_tts("postaudio", process_text(self.reddit_object["thread_post"]))
elif settings.config["settings"]["storymodemethod"] == 1: elif settings.config["settings"]["storymodemethod"] == 1:
for idx, text in track(enumerate(self.reddit_object["thread_post"])): for idx, text in track(enumerate(self.reddit_object["thread_post"]), total=len(self.reddit_object["thread_post"])):
self.call_tts(f"postaudio-{idx}", process_text(text)) self.call_tts(f"postaudio-{idx}", process_text(text))
else: else:
for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."): for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving...", total=len(self.reddit_object["comments"])):
# ! Stop creating mp3 files if the length is greater than max length. # ! Stop creating mp3 files if the length is greater than max length.
if self.length > self.max_length and idx > 1: if self.length > self.max_length and idx > 1:
self.length -= self.last_clip_length self.length -= self.last_clip_length
@ -124,19 +127,22 @@ class TTSEngine:
continue continue
else: else:
self.call_tts(f"{idx}-{idy}.part", newtext) self.call_tts(f"{idx}-{idy}.part", newtext)
with open(f"{self.path}/list.txt", "w") as f: concat_parts=[]
for idz in range(0, len(split_text)): # with open(f"{self.path}/list.txt", "w") as f:
f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") for idz in range(0, len(split_text)):
split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3")) # f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n")
f.write("file " + f"'silence.mp3'" + "\n") concat_parts.append(ffmpeg.input(f"{idx}-{idz}.part.mp3"))
split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3"))
os.system( # f.write("file " + f"'silence.mp3'" + "\n")
"ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " concat_parts.append('silence.mp3')
+ "-i " ffmpeg.concat(*concat_parts).output(f"{self.path}/{idx}.mp3").overwrite_output().global_args('-y -hide_banner -loglevel panic -safe 0').run(quiet=True)
+ f"{self.path}/list.txt " # os.system(
+ "-c copy " # "ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 "
+ f"{self.path}/{idx}.mp3" # + "-i "
) # + f"{self.path}/list.txt "
# + "-c copy "
# + f"{self.path}/{idx}.mp3"
# )
try: try:
for i in range(0, len(split_files)): for i in range(0, len(split_files)):
os.unlink(split_files[i]) os.unlink(split_files[i])
@ -146,20 +152,31 @@ class TTSEngine:
print("OSError") print("OSError")
def call_tts(self, filename: str, text: str): def call_tts(self, filename: str, text: str):
mp3_filepath = f"{self.path}/{filename}.mp3"
audio_speed = settings.config["settings"]["tts"]["speed"]
mp3_speed_changed_filepath = f"{self.path}/{filename}-speed-{audio_speed}.mp3"
self.tts_module.run( self.tts_module.run(
text, text,
filepath=f"{self.path}/{filename}.mp3", filepath=mp3_filepath,
random_voice=settings.config["settings"]["tts"]["random_voice"], random_voice=settings.config["settings"]["tts"]["random_voice"],
) )
if audio_speed != 1:
ffmpeg.input(mp3_filepath).filter("atempo", audio_speed).output(mp3_speed_changed_filepath).overwrite_output().run(quiet=True)
os.replace(mp3_speed_changed_filepath, mp3_filepath)
# try: # try:
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length # self.length += MP3(mp3_filepath).info.length
# except (MutagenError, HeaderNotFoundError): # except (MutagenError, HeaderNotFoundError):
# self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") # self.length += sox.file_info.duration(mp3_filepath)
try: try:
clip = AudioFileClip(f"{self.path}/{filename}.mp3") clip = AudioSegment.from_mp3(mp3_filepath)
self.last_clip_length = clip.duration self.last_clip_length = clip.duration_seconds
self.length += clip.duration self.length += clip.duration_seconds
clip.close() # clip = AudioFileClip(mp3_filepath)
# self.last_clip_length = clip.duration
# self.length += clip.duration
# clip.close()
except: except:
self.length = 0 self.length = 0

@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
docker build -t rvmt . sudo docker build -t rvmt .

@ -67,11 +67,11 @@ def run_many(times) -> None:
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.... ) # correct 1st 2nd 3rd 4th 5th....
main() main()
Popen("cls" if name == "nt" else "clear", shell=True).wait() # Popen("cls" if name == "nt" else "clear", shell=True).wait()
def shutdown() -> NoReturn: def shutdown() -> NoReturn:
if "redditid" in globals(): if "redditid" in globals() and bool(settings.config["settings"]["delete_temp_files"]):
print_markdown("## Clearing temp files") print_markdown("## Clearing temp files")
cleanup(redditid) cleanup(redditid)
@ -109,7 +109,7 @@ if __name__ == "__main__":
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) main(post_id)
Popen("cls" if name == "nt" else "clear", shell=True).wait() # Popen("cls" if name == "nt" else "clear", shell=True).wait()
elif config["settings"]["times_to_run"]: elif config["settings"]["times_to_run"]:
run_many(config["settings"]["times_to_run"]) run_many(config["settings"]["times_to_run"])
else: else:

@ -1,4 +1,5 @@
import re import re
import math
from prawcore.exceptions import ResponseException from prawcore.exceptions import ResponseException
@ -8,13 +9,13 @@ from praw.models import MoreComments
from prawcore.exceptions import ResponseException from prawcore.exceptions import ResponseException
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from utils.openai import ai_rewrite_story
from utils.subreddit import get_subreddit_undone from utils.subreddit import get_subreddit_undone
from utils.videos import check_done from utils.videos import check_done
from utils.voice import sanitize_text from utils.voice import sanitize_text
from utils.posttextparser import posttextparser from utils.posttextparser import posttextparser
from utils.ai_methods import sort_by_similarity from utils.ai_methods import sort_by_similarity
def get_subreddit_threads(POST_ID: str): def get_subreddit_threads(POST_ID: str):
""" """
Returns a list of threads from the AskReddit subreddit. Returns a list of threads from the AskReddit subreddit.
@ -125,10 +126,13 @@ def get_subreddit_threads(POST_ID: str):
content["is_nsfw"] = submission.over_18 content["is_nsfw"] = submission.over_18
content["comments"] = [] content["comments"] = []
if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymode"]:
ai_selftext = submission.selftext
if settings.config["ai"]["openai_rewrite"]:
ai_selftext=ai_rewrite_story(submission.selftext)
if settings.config["settings"]["storymodemethod"] == 1: if settings.config["settings"]["storymodemethod"] == 1:
content["thread_post"] = posttextparser(submission.selftext) content["thread_post"] = posttextparser(ai_selftext)
else: else:
content["thread_post"] = submission.selftext content["thread_post"] = ai_selftext
else: else:
for top_level_comment in submission.comments: for top_level_comment in submission.comments:
if isinstance(top_level_comment, MoreComments): if isinstance(top_level_comment, MoreComments):

@ -20,4 +20,8 @@ torch==2.0.1
transformers==4.29.2 transformers==4.29.2
ffmpeg-python==0.2.0 ffmpeg-python==0.2.0
elevenlabs==0.2.17 elevenlabs==0.2.17
yt-dlp==2023.7.6 yt-dlp==2023.7.6
pydub==0.25.1
openai==1.2.4
tiktoken==0.5.1
humanfriendly==10.0

@ -1,2 +1,3 @@
#!/bin/sh #!/bin/sh
docker run -v $(pwd)/out/:/app/assets -v $(pwd)/.env:/app/.env -it rvmt sudo docker run -v $(pwd)/config.toml:/app/config.toml -v $(pwd)/out/:/app/assets -v $(pwd)/.env:/app/.env -v $(pwd)/results:/app/results -it rvmt

@ -10,7 +10,7 @@ password = { optional = false, nmin = 8, explanation = "The password of your red
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" } 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. You can have multiple subreddits, add an + with no spaces.", example = "AskReddit+Redditdev", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } 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. You can have multiple subreddits, add an + with no spaces.", example = "AskReddit+Redditdev", 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" } 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" } max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 100000, 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" }
min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" } min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" }
post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr", options = ['','af', 'ak', 'am', 'ar', 'as', 'ay', 'az', 'be', 'bg', 'bho', 'bm', 'bn', 'bs', 'ca', 'ceb', 'ckb', 'co', 'cs', 'cy', 'da', 'de', 'doi', 'dv', 'ee', 'el', 'en', 'en-US', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'ha', 'haw', 'hi', 'hmn', 'hr', 'ht', 'hu', 'hy', 'id', 'ig', 'ilo', 'is', 'it', 'iw', 'ja', 'jw', 'ka', 'kk', 'km', 'kn', 'ko', 'kri', 'ku', 'ky', 'la', 'lb', 'lg', 'ln', 'lo', 'lt', 'lus', 'lv', 'mai', 'mg', 'mi', 'mk', 'ml', 'mn', 'mni-Mtei', 'mr', 'ms', 'mt', 'my', 'ne', 'nl', 'no', 'nso', 'ny', 'om', 'or', 'pa', 'pl', 'ps', 'pt', 'qu', 'ro', 'ru', 'rw', 'sa', 'sd', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tr', 'ts', 'tt', 'ug', 'uk', 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh-CN', 'zh-TW', 'zu'] } post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr", options = ['','af', 'ak', 'am', 'ar', 'as', 'ay', 'az', 'be', 'bg', 'bho', 'bm', 'bn', 'bs', 'ca', 'ceb', 'ckb', 'co', 'cs', 'cy', 'da', 'de', 'doi', 'dv', 'ee', 'el', 'en', 'en-US', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'ha', 'haw', 'hi', 'hmn', 'hr', 'ht', 'hu', 'hy', 'id', 'ig', 'ilo', 'is', 'it', 'iw', 'ja', 'jw', 'ka', 'kk', 'km', 'kn', 'ko', 'kri', 'ku', 'ky', 'la', 'lb', 'lg', 'ln', 'lo', 'lt', 'lus', 'lv', 'mai', 'mg', 'mi', 'mk', 'ml', 'mn', 'mni-Mtei', 'mr', 'ms', 'mt', 'my', 'ne', 'nl', 'no', 'nso', 'ny', 'om', 'or', 'pa', 'pl', 'ps', 'pt', 'qu', 'ro', 'ru', 'rw', 'sa', 'sd', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tr', 'ts', 'tt', 'ug', 'uk', 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh-CN', 'zh-TW', 'zu'] }
min_comments = { default = 20, optional = false, nmin = 10, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } min_comments = { default = 20, optional = false, nmin = 10, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" }
@ -18,6 +18,14 @@ min_comments = { default = 20, optional = false, nmin = 10, type = "int", explan
[ai] [ai]
ai_similarity_enabled = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Threads read from Reddit are sorted based on their similarity to the keywords given below"} ai_similarity_enabled = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Threads read from Reddit are sorted based on their similarity to the keywords given below"}
ai_similarity_keywords = {optional = true, type="str", example= 'Elon Musk, Twitter, Stocks', explanation = "Every keyword or even sentence, seperated with comma, is used to sort the reddit threads based on similarity"} ai_similarity_keywords = {optional = true, type="str", example= 'Elon Musk, Twitter, Stocks', explanation = "Every keyword or even sentence, seperated with comma, is used to sort the reddit threads based on similarity"}
openai_api_key = {optional = true, type="str", example= 'sk-', explanation = "OpenAI API Key"}
openai_model = {optional = true, type="str", example= 'gpt-3.5-turbo', explanation = "OpenAI model name", default="gpt-3.5-turbo"}
openai_rewrite = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Use OpenAI to rewrite the original story"}
openai_api_base = {optional = true, type="str", example='http://localhost:4891/v1', explanation = "OpenAI API Base URL", default="https://api.openai.com/v1"}
openai_rewrite_retries = { optional = true, type = "int", default = 5, example = 5, explanation = "Number of retries if the OpenAI response is too short or did not succeed" }
openai_rewrite_chunk_max_tokens = { optional = true, type = "int", default = 2000, example = 1000, explanation = "Number of tokens to split the story at, when rewording, in order to stay under max tokens for the model." }
openai_rewrite_length = { optional = true, default = 1, example = 1.1, explanation = "When rewriting the story with AI, aim for this length, as a percentage, compared to the original story", type = "float" }
openai_retry_fail_error = {optional = true, option = [true, false], default = false, type = "bool", explanation = "Use true to error if AI failed to rewrite, otherwise the original content is kept"}
[settings] [settings]
allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false, ], explanation = "Whether to allow NSFW content, True or False" } allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false, ], explanation = "Whether to allow NSFW content, True or False" }
@ -33,8 +41,8 @@ resolution_h = { optional = false, default = 1920, example = 2560, explantation
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" } 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_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_video = { optional = true, default = "minecraft", example = "rocket-league", options = ["custom", "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 = { optional = true, default = "lofi", example = "chill-summer", options = ["custom", "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"} 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"} 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.)" }
@ -55,3 +63,4 @@ python_voice = { optional = false, default = "1", example = "1", explanation = "
py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" } py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" }
silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" } silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }
no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" } no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" }
speed = { optional = true, type = "float", nmin = 0.1, default = 1.0, example = 1.5, explanation="Sets the speed of the voice", oob_error = "The speed has to be equal to, or greater than 0.1", input_error = "The speed HAS to be a float number greater than 0.1"}

@ -14,5 +14,15 @@
"https://www.youtube.com/watch?v=EZE8JagnBI8", "https://www.youtube.com/watch?v=EZE8JagnBI8",
"chill-summer.mp3", "chill-summer.mp3",
"Mellow Vibes Radio" "Mellow Vibes Radio"
],
"old_custom": [
"https://www.youtube.com/watch?v=_fVRA6XnvYc",
"custom.mp4",
"lofi geek"
],
"custom": [
"https://www.youtube.com/watch?v=5oF5lS0sajs",
"custom.mp4",
"BreakingCopyright"
] ]
} }

@ -59,5 +59,47 @@
"steep.mp4", "steep.mp4",
"joel", "joel",
"center" "center"
],
"custom_randy_bad_render": [
"https://www.youtube.com/watch?v=iAK7oisgAk0",
"l.mp4",
"iStackSlow",
"center"
],
"custom": [
"https://www.youtube.com/watch?v=qXkTf1C_v0Y",
"deathstar_g1.mp4",
"iStackSlow",
"center"
],
"custom_red_room": [
"https://www.youtube.com/watch?v=qXkTf1C_v0Y",
"crimson.mp4",
"iStackSlow",
"center"
],
"custom_randy_hopping": [
"https://www.youtube.com/watch?v=7RmbJOWc9IQ",
"boy_b1.mp4",
"iStackSlow",
"center"
],
"custom_randy_surf": [
"https://www.youtube.com/watch?v=FaeJBS51bH8",
"surf_forsaken.mp4",
"iStackSlow",
"center"
],
"custom_scary": [
"https://www.youtube.com/watch?v=cIO95Xh5qLs",
"custom.mp4",
"GenericBomb",
"center"
],
"custom1": [
"https://www.youtube.com/watch?v=GW_riy_jHRU",
"custom.mp4",
"KazzaGamesTV",
"center"
] ]
} }

@ -13,8 +13,9 @@ def cleanup(reddit_id) -> int:
Returns: Returns:
int: How many files were deleted int: How many files were deleted
""" """
directory = f"../assets/temp/{reddit_id}/" directory = f"./assets/temp/{reddit_id}/"
if exists(directory): if exists(directory):
shutil.rmtree(directory) shutil.rmtree(directory)
return 1 return 1
return 0

@ -0,0 +1,28 @@
import ffmpeg
from pydub import AudioSegment
from tqdm import tqdm
from utils.ffmpeg_progress import ProgressFfmpeg
def get_duration(filename):
if filename.lower().endswith('.mp3'):
return float(AudioSegment.from_mp3(filename).duration_seconds)
probe_info=ffmpeg.probe(filename)
return float(probe_info["format"]["duration"])
def ffmpeg_progress_run(ffmpeg_cmd, length):
pbar = tqdm(total=100, desc="Progress: ", bar_format="{l_bar}{bar}", unit=" %", dynamic_ncols=True, leave=False)
def progress_tracker(progress) -> None:
status = round(progress * 100, 2)
old_percentage = pbar.n
pbar.update(status - old_percentage)
with ProgressFfmpeg(length, progress_tracker) as progress:
ffmpeg_cmd.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()

@ -0,0 +1,42 @@
import threading
import tempfile
import time
class ProgressFfmpeg(threading.Thread):
def __init__(self, vid_duration_seconds, progress_update_callback):
threading.Thread.__init__(self, name="ProgressFfmpeg")
self.stop_event = threading.Event()
self.output_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
self.vid_duration_seconds = vid_duration_seconds
self.progress_update_callback = progress_update_callback
def run(self):
while not self.stop_event.is_set():
latest_progress = self.get_latest_ms_progress()
completed_percent = latest_progress / self.vid_duration_seconds
self.progress_update_callback(completed_percent)
time.sleep(1)
def get_latest_ms_progress(self):
lines = self.output_file.readlines()
if lines:
for line in lines:
if "out_time_ms" in line:
out_time_ms_str = line.split("=")[1].strip()
if out_time_ms_str.isnumeric():
return float(out_time_ms_str) / 1000000.0
else:
# Handle the case when "N/A" is encountered
return 0 # None
return 0 # None
def stop(self):
self.stop_event.set()
def __enter__(self):
self.start()
return self
def __exit__(self, *args, **kwargs):
self.stop()

@ -18,10 +18,11 @@ def load_text_replacements():
def perform_text_replacements(text): def perform_text_replacements(text):
updated_text = text updated_text = text
for replacement in text_replacements['text-and-audio']: for replacement in text_replacements['text-and-audio']:
compiled = re.compile(re.escape(replacement[0]), re.IGNORECASE) regex_escaped_word=re.escape(replacement[0])
compiled = re.compile(r"\b{}\b".format(regex_escaped_word), re.IGNORECASE)
updated_text = compiled.sub(replacement[1], updated_text) updated_text = compiled.sub(replacement[1], updated_text)
for replacement in text_replacements['text-only']: for replacement in text_replacements['text-only']:
compiled = re.compile(re.escape(replacement[0]), re.IGNORECASE) compiled = re.compile(r"\b{}\b".format(regex_escaped_word), re.IGNORECASE)
updated_text = compiled.sub(replacement[1], updated_text) updated_text = compiled.sub(replacement[1], updated_text)
return updated_text return updated_text
@ -93,7 +94,7 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) ->
image.save(f"assets/temp/{id}/png/title.png") image.save(f"assets/temp/{id}/png/title.png")
for idx, text in track(enumerate(texts), "Rendering Image"): for idx, text in track(enumerate(texts), "Rendering Image", total=len(texts)):
image = Image.new("RGBA", size, theme) image = Image.new("RGBA", size, theme)
text = process_text(text, False) text = process_text(text, False)
draw_multiple_line_text(image, perform_text_replacements(text), font, txtclr, padding, wrap=30, transparent=transparent) draw_multiple_line_text(image, perform_text_replacements(text), font, txtclr, padding, wrap=30, transparent=transparent)

@ -0,0 +1,149 @@
from openai import OpenAI
from utils import settings
from utils.console import print_step, print_substep
from utils.posttextparser import posttextparser
from utils.timeout import timeout
# import math
import sys
import tiktoken
def is_valid_ai_response(string: str) -> bool:
lower_string = string.lower()
return not (
lower_string.startswith('i can\'t') or
lower_string.startswith("i cannot") or
lower_string.startswith("i can not") or
lower_string.startswith("sorry") or
lower_string.startswith("i\'m sorry") or
lower_string.startswith("i am sorry") or
lower_string.startswith("i apologize")
) and len(lower_string) > 150
def remove_ai_extras(string: str) -> str:
lines=string.splitlines()
first_line=lines[0].lower().strip()
if "here" in first_line and first_line.endswith('story:'):
return "\n".join(lines[1:]).strip()
return string
def num_tokens_from_string(string: str, encoding_name: str) -> int:
encoding = tiktoken.encoding_for_model(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens
def ai_rewrite_story(story_text):
MAX_RETRIES=int(settings.config['ai']['openai_rewrite_retries'])
MAX_PART_REWORD_TOKENS=int(settings.config['ai']['openai_rewrite_chunk_max_tokens'])
longer_or_shorter = "longer" if float(settings.config['ai']['openai_rewrite_length']) > 1.0 else "shorter"
print_step(
f"Using OpenAI {settings.config['ai']['openai_model']} to rewrite the content..."
)
client = OpenAI(
base_url=settings.config["ai"]["openai_api_base"],
api_key=settings.config["ai"]["openai_api_key"],
timeout=90
)
model_name = settings.config["ai"]["openai_model"]
ai_system_message = f"The user will present you with a story. You must rewrite the story in first person. Rewrite the story to the same or {longer_or_shorter} length, keeping the same details, but make it extremely addictive to listen to, with many cliffhangers. Use language and words that the average social media user can understand. Avoid using swear words and words that may be unallowed, like: gun, drug, shoot, kill, suicide, etc."
ai_user_message = "Rewrite this story to be more addictive. Your output will be published, so make sure to only output the new story."
ai_assistant_message = "Sure! What is the story?"
base_tokens = num_tokens_from_string(ai_system_message+ai_user_message+ai_assistant_message, model_name)
parts=posttextparser(story_text)
rewritten_parts=[]
while len(parts) > 0:
tmp_part_list=[]
tmp_tokens = base_tokens
while tmp_tokens < MAX_PART_REWORD_TOKENS and len(parts) > 0:
next_part=parts.pop(0)
tmp_tokens+=num_tokens_from_string(next_part, model_name)
tmp_part_list.append(next_part)
if len(tmp_part_list) > 0:
joined_part_list=" ".join(tmp_part_list)
part_chat_history = [
{"role":"system", "content":ai_system_message},
{"role":"user", "content":ai_user_message},
{"role":"assistant", "content":ai_assistant_message},
{"role":"user", "content":joined_part_list}
]
joined_part_list_tokens=num_tokens_from_string(joined_part_list, model_name) * float(settings.config['ai']['openai_rewrite_length'])
ai_part_message=''
part_retry_num=0
while part_retry_num <= MAX_RETRIES and num_tokens_from_string(ai_part_message, model_name) < joined_part_list_tokens:
part_retry_text = '' if part_retry_num <= 0 else f"[Retry #{part_retry_num}]"
try:
with timeout(seconds=60):
part_log_message = f"{part_retry_text} Making request to OpenAI to make the portion of the story longer...".strip() if ai_part_message != '' else f"{part_retry_text} Making request to OpenAI to reword a portion of the story...".strip()
print_substep(part_log_message)
# print(part_chat_history)
ai_part_response = client.chat.completions.create(
model=model_name,
messages=part_chat_history,
temperature=0.9, # very creative
timeout=60
# max_tokens=math.ceil(num_tokens_from_string(ai_selftext, model_name)*2.5) # 2.5 because it counts all the messages in history
)
ai_part_message_updated=remove_ai_extras(ai_part_response.choices[0].message.content)
old_part_tokens = num_tokens_from_string(ai_part_message, model_name)
new_part_tokens = num_tokens_from_string(ai_part_message_updated, model_name)
if new_part_tokens > old_part_tokens and is_valid_ai_response(ai_part_message_updated):
ai_part_message = ai_part_message_updated
print_substep(f"Got AI response: {ai_part_message}")
part_chat_history.append({"role":"assistant", "content":ai_part_message})
part_chat_history.append({"role":"user", "content":"Make the story longer/more detailed"})
except KeyboardInterrupt:
sys.exit(1)
except Exception as e:
print_substep(str(e), style="bold red")
pass
part_retry_num+=1
if not bool(ai_part_message):
if bool(settings.config['ai']['openai_retry_fail_error']):
raise ValueError('AI rewrite failed')
else:
ai_part_message = joined_part_list
rewritten_parts.append(ai_part_message)
tmp_part_list.clear()
try:
joined_rewritten_parts=" ".join(rewritten_parts)
chat_history=[
{"role":"system", "content":"The user will present you with a story. You must output the same story with any issues fixed, and possibly expand the story to be longer. Your goal is to output a story that can be read to an audience. This story must make sense and have a lot of cliffhangers, to keep the audience interested. Keep the same story details and possibly add more. Avoid using swear words and words that may be unallowed, like: gun, drug, shoot, kill, suicide, etc. Make your story about 5 minutes in spoken length."},
{"role":"user", "content":"I have a story for you to review. Your output will be published, so make sure to only output the story. Do NOT include any extra information in your response besides the story."},
{"role":"assistant", "content":ai_assistant_message},
{"role":"user", "content":" ".join(rewritten_parts)}
]
joined_rewritten_parts_tokens=num_tokens_from_string(joined_rewritten_parts, model_name)
ai_message=''
retry_num=0
while retry_num <= MAX_RETRIES and num_tokens_from_string(ai_message, model_name) < joined_rewritten_parts_tokens:
retry_text = '' if retry_num <= 0 else f"[Retry #{retry_num}]"
try:
with timeout(seconds=90):
log_message = f"{retry_text} Making request to OpenAI to make the whole story longer...".strip() if ai_message != '' else f"{retry_text} Making request to OpenAI to finalize the whole story...".strip()
print_substep(log_message)
# print(chat_history)
ai_response = client.chat.completions.create(
model=model_name,
messages=chat_history,
temperature=0.9, # very creative
timeout=90
# max_tokens=math.ceil(num_tokens_from_string(ai_selftext, model_name)*2.5) # 2.5 because it counts all the messages in history
)
ai_message_updated=remove_ai_extras(ai_response.choices[0].message.content)
old_tokens = num_tokens_from_string(ai_message, model_name)
new_tokens = num_tokens_from_string(ai_message_updated, model_name)
if new_tokens > old_tokens and is_valid_ai_response(ai_message_updated):
ai_message = ai_message_updated
print_substep(f"Got AI response: {ai_message}")
chat_history.append({"role":"assistant", "content":ai_message})
chat_history.append({"role":"user", "content":"Make the story longer/more detailed"})
except KeyboardInterrupt:
sys.exit(1)
except Exception as e:
print_substep(str(e), style="bold red")
pass
retry_num+=1
return ai_message if ai_message else joined_rewritten_parts
except:
return " ".join(rewritten_parts)

@ -5,6 +5,7 @@
["killing", "unaliving"], ["killing", "unaliving"],
["kill", "unalive"], ["kill", "unalive"],
["dead", "unalive"], ["dead", "unalive"],
["drugged", "out of it, caused by substances,"],
["drug", "substance"], ["drug", "substance"],
["gun", "boom stick"], ["gun", "boom stick"],
["nude", "without clothes"], ["nude", "without clothes"],
@ -13,10 +14,15 @@
["shit", "poo"], ["shit", "poo"],
["weed", "oui'd"], ["weed", "oui'd"],
["shoot", "hit"], ["shoot", "hit"],
["shot", "hit"] ["shot", "hit"],
["reddit", "tiktok"]
], ],
"text-only": [], "text-only": [],
"audio-only": [ "audio-only": [
["mic", "mike"] ["mic", "mike"],
["ai", "artificial intelligence"],
["a.i.", "artificial intelligence"],
["a.i", "artificial intelligence"],
["mkultra", "em kay ultra"]
] ]
} }

@ -0,0 +1,13 @@
import signal
class timeout:
def __init__(self, seconds=1, error_message='Timeout'):
self.seconds = seconds
self.error_message = error_message
def handle_timeout(self, signum, frame):
raise TimeoutError(self.error_message)
def __enter__(self):
signal.signal(signal.SIGALRM, self.handle_timeout)
signal.alarm(self.seconds)
def __exit__(self, type, value, traceback):
signal.alarm(0)

@ -24,10 +24,11 @@ def load_text_replacements():
def perform_text_replacements(text): def perform_text_replacements(text):
updated_text = text updated_text = text
for replacement in text_replacements['text-and-audio']: for replacement in text_replacements['text-and-audio']:
compiled = re.compile(re.escape(replacement[0]), re.IGNORECASE) regex_escaped_word=re.escape(replacement[0])
compiled = re.compile(r"\b{}\b".format(regex_escaped_word), re.IGNORECASE)
updated_text = compiled.sub(replacement[1], updated_text) updated_text = compiled.sub(replacement[1], updated_text)
for replacement in text_replacements['audio-only']: for replacement in text_replacements['text-only']:
compiled = re.compile(re.escape(replacement[0]), re.IGNORECASE) compiled = re.compile(r"\b{}\b".format(regex_escaped_word), re.IGNORECASE)
updated_text = compiled.sub(replacement[1], updated_text) updated_text = compiled.sub(replacement[1], updated_text)
return updated_text return updated_text

@ -1,6 +1,7 @@
import json import json
import random import random
import re import re
import math
from pathlib import Path from pathlib import Path
from random import randrange from random import randrange
from typing import Any, Tuple, Dict from typing import Any, Tuple, Dict
@ -10,6 +11,7 @@ from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
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 import yt_dlp
import ffmpeg
def load_background_options(): def load_background_options():
@ -131,34 +133,74 @@ def chop_background(background_config: Dict[str, Tuple], video_length: int, redd
if settings.config["settings"]["background"][f"background_audio_volume"] == 0: if settings.config["settings"]["background"][f"background_audio_volume"] == 0:
print_step("Volume was set to 0. Skipping background audio creation . . .") print_step("Volume was set to 0. Skipping background audio creation . . .")
else: else:
print_step("Finding a spot in the backgrounds audio to chop...✂️")
audio_choice = f"{background_config['audio'][2]}-{background_config['audio'][1]}" audio_choice = f"{background_config['audio'][2]}-{background_config['audio'][1]}"
background_audio = AudioFileClip(f"assets/backgrounds/audio/{audio_choice}") audio_file_path=f"assets/backgrounds/audio/{audio_choice}"
if bool(settings.config["settings"]["background"][f"background_audio_loop"]):
background_looped_audio_file_path = f"assets/backgrounds/audio/looped-{audio_choice}"
background_audio_duration = float(ffmpeg.probe(audio_file_path)["format"]["duration"])
background_audio_loops = math.ceil(video_length / background_audio_duration)
if background_audio_loops > 1:
print_step(f"Looping background audio {background_audio_loops} times...🔁")
background_audio_loop_input = ffmpeg.input(
audio_file_path,
stream_loop=background_audio_loops
)
ffmpeg.output(
background_audio_loop_input,
background_looped_audio_file_path,
vcodec="copy",
acodec="copy"
).overwrite_output().run(quiet=True)
audio_file_path = background_looped_audio_file_path
print_step("Finding a spot in the background audio to chop...✂️")
background_audio = AudioFileClip(audio_file_path)
start_time_audio, end_time_audio = get_start_and_end_times( start_time_audio, end_time_audio = get_start_and_end_times(
video_length, background_audio.duration video_length, background_audio.duration
) )
background_audio = background_audio.subclip(start_time_audio, end_time_audio) background_audio = background_audio.subclip(start_time_audio, end_time_audio)
background_audio.write_audiofile(f"assets/temp/{id}/background.mp3") background_audio.write_audiofile(f"assets/temp/{id}/background.mp3")
background_audio.close()
print_step("Finding a spot in the backgrounds video to chop...✂️")
video_choice = f"{background_config['video'][2]}-{background_config['video'][1]}" video_choice = f"{background_config['video'][2]}-{background_config['video'][1]}"
background_video = VideoFileClip(f"assets/backgrounds/video/{video_choice}") video_file_path = f"assets/backgrounds/video/{video_choice}"
if bool(settings.config["settings"]["background"][f"background_video_loop"]):
background_looped_video_file_path = f"assets/backgrounds/video/looped-{video_choice}"
background_video_duration = float(ffmpeg.probe(video_file_path)["format"]["duration"])
background_video_loops = math.ceil(video_length / background_video_duration)
if background_video_loops > 1:
print_step(f"Looping background video {background_video_loops} times...🔁")
background_video_loop_input = ffmpeg.input(
video_file_path,
stream_loop=background_video_loops
)
ffmpeg.output(
background_video_loop_input,
background_looped_video_file_path,
vcodec="copy",
acodec="copy"
).overwrite_output().run(quiet=True)
video_file_path = background_looped_video_file_path
print_step("Finding a spot in the background video to chop...✂️")
background_video = VideoFileClip(video_file_path)
start_time_video, end_time_video = get_start_and_end_times( start_time_video, end_time_video = get_start_and_end_times(
video_length, background_video.duration video_length, background_video.duration
) )
background_video.close()
# Extract video subclip # Extract video subclip
try: try:
ffmpeg_extract_subclip( ffmpeg_extract_subclip(
f"assets/backgrounds/video/{video_choice}", video_file_path,
start_time_video, start_time_video,
end_time_video, 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/video/{video_choice}") as video: video=VideoFileClip(video_file_path)
new = video.subclip(start_time_video, end_time_video) 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")
video.close()
print_substep("Background video chopped successfully!", style="bold green") print_substep("Background video chopped successfully!", style="bold green")
return background_config["video"][2] return background_config["video"][2]

@ -13,58 +13,15 @@ from rich.progress import track
from utils.cleanup import cleanup from utils.cleanup import cleanup
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from utils.ffmpeg import ffmpeg_progress_run, get_duration
from utils.thumbnail import create_thumbnail from utils.thumbnail import create_thumbnail
from utils.videos import save_data from utils.videos import save_data
from utils import settings from utils import settings
from humanfriendly import format_size, format_timespan
import tempfile
import threading
import time
console = Console() console = Console()
class ProgressFfmpeg(threading.Thread):
def __init__(self, vid_duration_seconds, progress_update_callback):
threading.Thread.__init__(self, name="ProgressFfmpeg")
self.stop_event = threading.Event()
self.output_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
self.vid_duration_seconds = vid_duration_seconds
self.progress_update_callback = progress_update_callback
def run(self):
while not self.stop_event.is_set():
latest_progress = self.get_latest_ms_progress()
if latest_progress is not None:
completed_percent = latest_progress / self.vid_duration_seconds
self.progress_update_callback(completed_percent)
time.sleep(1)
def get_latest_ms_progress(self):
lines = self.output_file.readlines()
if lines:
for line in lines:
if "out_time_ms" in line:
out_time_ms_str = line.split("=")[1].strip()
if out_time_ms_str.isnumeric():
return float(out_time_ms_str) / 1000000.0
else:
# Handle the case when "N/A" is encountered
return None
return None
def stop(self):
self.stop_event.set()
def __enter__(self):
self.start()
return self
def __exit__(self, *args, **kwargs):
self.stop()
def name_normalize(name: str) -> str: def name_normalize(name: str) -> str:
name = re.sub(r'[?\\"%*:|<>]', "", name) name = re.sub(r'[?\\"%*:|<>]', "", name)
name = re.sub(r"( [w,W]\s?\/\s?[o,O,0])", r" without", name) name = re.sub(r"( [w,W]\s?\/\s?[o,O,0])", r" without", name)
@ -83,9 +40,12 @@ def name_normalize(name: str) -> str:
def prepare_background(reddit_id: str, W: int, H: int) -> str: def prepare_background(reddit_id: str, W: int, H: int) -> str:
print_substep('Preparing the background video...')
output_path = f"assets/temp/{reddit_id}/background_noaudio.mp4" output_path = f"assets/temp/{reddit_id}/background_noaudio.mp4"
input_path = f"assets/temp/{reddit_id}/background.mp4"
input_duration=get_duration(input_path)
output = ( output = (
ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4") ffmpeg.input(input_path)
.filter("crop", f"ih*({W}/{H})", "ih") .filter("crop", f"ih*({W}/{H})", "ih")
.output( .output(
output_path, output_path,
@ -100,7 +60,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str:
.overwrite_output() .overwrite_output()
) )
try: try:
output.run(quiet=True) ffmpeg_progress_run(output, input_duration)
except ffmpeg.Error as e: except ffmpeg.Error as e:
print(e.stderr.decode("utf8")) print(e.stderr.decode("utf8"))
exit(1) exit(1)
@ -182,19 +142,19 @@ def make_final_video(
audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3")) audio_clips.insert(0, ffmpeg.input(f"assets/temp/{reddit_id}/mp3/title.mp3"))
audio_clips_durations = [ audio_clips_durations = [
float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/{i}.mp3")["format"]["duration"]) get_duration(f"assets/temp/{reddit_id}/mp3/{i}.mp3")
for i in range(number_of_clips) for i in range(number_of_clips)
] ]
audio_clips_durations.insert( audio_clips_durations.insert(
0, 0,
float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), get_duration(f"assets/temp/{reddit_id}/mp3/title.mp3")
) )
audio_concat = ffmpeg.concat(*audio_clips, a=1, v=0) audio_concat = ffmpeg.concat(*audio_clips, a=1, v=0)
ffmpeg.output( ffmpeg.output(
audio_concat, f"assets/temp/{reddit_id}/audio.mp3", **{"b:a": "192k"} audio_concat, f"assets/temp/{reddit_id}/audio.mp3", **{"b:a": "192k"}
).overwrite_output().run(quiet=True) ).overwrite_output().run(quiet=True)
console.log(f"[bold green] Video Will Be: {length} Seconds Long") print_substep(f"Video will be: {format_timespan(length)}", style="bold green")
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")
@ -212,14 +172,12 @@ def make_final_video(
current_time = 0 current_time = 0
if settings.config["settings"]["storymode"]: if settings.config["settings"]["storymode"]:
audio_clips_durations = [ audio_clips_durations = [
float( get_duration(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")
ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"]
)
for i in range(number_of_clips) for i in range(number_of_clips)
] ]
audio_clips_durations.insert( audio_clips_durations.insert(
0, 0,
float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), get_duration(f"assets/temp/{reddit_id}/mp3/title.mp3")
) )
if settings.config["settings"]["storymodemethod"] == 0: if settings.config["settings"]["storymodemethod"] == 0:
image_clips.insert( image_clips.insert(
@ -228,8 +186,9 @@ def make_final_video(
"scale", screenshot_width, -1 "scale", screenshot_width, -1
), ),
) )
image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity)
background_clip = background_clip.overlay( background_clip = background_clip.overlay(
image_clips[0], image_overlay,
enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})",
x="(main_w-overlay_w)/2", x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2", y="(main_h-overlay_h)/2",
@ -242,13 +201,26 @@ def make_final_video(
"scale", screenshot_width, -1 "scale", screenshot_width, -1
) )
) )
image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity)
background_clip = background_clip.overlay( background_clip = background_clip.overlay(
image_clips[i], image_overlay,
enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})", enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})",
x="(main_w-overlay_w)/2", x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2", y="(main_h-overlay_h)/2",
) )
current_time += audio_clips_durations[i] current_time += audio_clips_durations[i]
#Code to grab final image and add it to the video.
final_audio_duration = get_duration(f"assets/temp/{reddit_id}/mp3/postaudio-{number_of_clips}.mp3")
final_image = ffmpeg.input(f"assets/temp/{reddit_id}/png/img{number_of_clips}.png")["v"].filter(
"scale", screenshot_width, -1
)
background_clip = background_clip.overlay(
final_image,
enable=f"between(t,{current_time},{current_time + final_audio_duration})",
x="(main_w-overlay_w)/2",
y="(main_h-overlay_h)/2",
)
current_time += final_audio_duration
else: else:
for i in range(0, number_of_clips + 1): for i in range(0, number_of_clips + 1):
image_clips.append( image_clips.append(
@ -327,22 +299,14 @@ def make_final_video(
) )
background_clip = background_clip.filter("scale", W, H) background_clip = background_clip.filter("scale", W, H)
print_step("Rendering the video 🎥") print_step("Rendering the video 🎥")
from tqdm import tqdm
pbar = tqdm(total=100, desc="Progress: ", bar_format="{l_bar}{bar}", unit=" %")
def on_update_example(progress) -> None:
status = round(progress * 100, 2)
old_percentage = pbar.n
pbar.update(status - old_percentage)
defaultPath = f"results/{subreddit}" defaultPath = f"results/{subreddit}"
with ProgressFfmpeg(length, on_update_example) as progress: path = defaultPath + f"/{filename}"
path = defaultPath + f"/{filename}" path = (
path = ( path[:251] + ".mp4"
path[:251] + ".mp4" ) # Prevent a error by limiting the path length, do not change this.
) # Prevent a error by limiting the path length, do not change this. try:
try: ffmpeg_progress_run(
ffmpeg.output( ffmpeg.output(
background_clip, background_clip,
final_audio, final_audio,
@ -354,25 +318,20 @@ def make_final_video(
"b:a": "192k", "b:a": "192k",
"threads": multiprocessing.cpu_count(), "threads": multiprocessing.cpu_count(),
}, },
).overwrite_output().global_args("-progress", progress.output_file.name).run( ).overwrite_output(),
quiet=True, length
overwrite_output=True, )
capture_stdout=False, except ffmpeg.Error as e:
capture_stderr=False, print(e.stderr.decode("utf8"))
) exit(1)
except ffmpeg.Error as e:
print(e.stderr.decode("utf8"))
exit(1)
old_percentage = pbar.n
pbar.update(100 - old_percentage)
if allowOnlyTTSFolder: if allowOnlyTTSFolder:
path = defaultPath + f"/OnlyTTS/{filename}" path = defaultPath + f"/OnlyTTS/{filename}"
path = ( path = (
path[:251] + ".mp4" path[:251] + ".mp4"
) # Prevent a error by limiting the path length, do not change this. ) # Prevent a error by limiting the path length, do not change this.
print_step("Rendering the Only TTS Video 🎥") print_step("Rendering the Only TTS Video 🎥")
with ProgressFfmpeg(length, on_update_example) as progress: try:
try: ffmpeg_progress_run(
ffmpeg.output( ffmpeg.output(
background_clip, background_clip,
audio, audio,
@ -384,21 +343,18 @@ def make_final_video(
"b:a": "192k", "b:a": "192k",
"threads": multiprocessing.cpu_count(), "threads": multiprocessing.cpu_count(),
}, },
).overwrite_output().global_args("-progress", progress.output_file.name).run( ).overwrite_output(),
quiet=True, length
overwrite_output=True, )
capture_stdout=False, except ffmpeg.Error as e:
capture_stderr=False, print(e.stderr.decode("utf8"))
) exit(1)
except ffmpeg.Error as e: save_filename = filename + ".mp4"
print(e.stderr.decode("utf8")) save_data(subreddit, save_filename, title, idx, background_config["video"][2])
exit(1) if bool(settings.config["settings"]["delete_temp_files"]):
print_step("Removing temporary files 🗑")
old_percentage = pbar.n cleanups = cleanup(reddit_id)
pbar.update(100 - old_percentage) print_substep(f"Removed {cleanups} temporary files 🗑")
pbar.close() file_size=os.stat(f"{defaultPath}/{save_filename}").st_size
save_data(subreddit, filename + ".mp4", title, idx, background_config["video"][2]) file_size_human_readable=format_size(file_size)
print_step("Removing temporary files 🗑") print_step(f"Done! 🎉 The {file_size_human_readable} video is in the results folder 📁")
cleanups = cleanup(reddit_id)
print_substep(f"Removed {cleanups} temporary files 🗑")
print_step("Done! 🎉 The video is in the results folder 📁")

Loading…
Cancel
Save