diff --git a/.dockerignore b/.dockerignore index 1d1fe94..5f754db 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,5 @@ -Dockerfile \ No newline at end of file +Dockerfile +out +results +assets + diff --git a/Dockerfile b/Dockerfile index 4cf2a71..ce0d7ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,19 @@ FROM python:3.10.9-slim RUN apt update -RUN apt-get install -y ffmpeg -RUN apt install python3-pip -y +RUN apt install ffmpeg python3-pip rubberband-cli espeak python3-pyaudio -y + +RUN python3 -m pip install --upgrade pip RUN mkdir /app -ADD . /app WORKDIR /app +ADD requirements.txt /app/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 # (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 echo ZmluZCAvIC1wYXRoICcqL21vdmllcHkvdG9vbHMucHknIC1wcmludDAgfCB3aGlsZSByZWFkIC1kICQnXDAnIGZpbGU7IGRvCiAgc2VkIC1pIC1lICdzLyciJyInTW92aWVweSAtIFJ1bm5pbmc6XFxuPj4+ICIrICIgIi5qb2luKGNtZCknIiciJy8iTW92aWVweSAtIFJ1bm5pbmc6XFxuPj4+ICIgKyAiICIuam9pbihjbWQpL2cnICIkZmlsZSIKZG9uZQ== | base64 --decode | bash CMD ["python3", "main.py"] diff --git a/TTS/GTTS.py b/TTS/GTTS.py index bff100f..217d546 100644 --- a/TTS/GTTS.py +++ b/TTS/GTTS.py @@ -10,7 +10,7 @@ class GTTS: self.max_chars = 5000 self.voices = [] - def run(self, text, filepath): + def run(self, text, filepath, random_voice): tts = gTTS( text=text, lang=settings.config["reddit"]["thread"]["post_lang"] or "en", diff --git a/TTS/TikTok.py b/TTS/TikTok.py index 29542e2..9049bb7 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -1,9 +1,9 @@ # documentation for tiktok api: https://github.com/oscie57/tiktok-voice/wiki + import base64 import random import time from typing import Optional, Final - import requests from utils import settings @@ -34,7 +34,6 @@ eng_voices: Final[tuple] = ( "en_us_009", # English US - Male 3 "en_us_010", # English US - Male 4 "en_male_narration", # Narrator - "en_male_funny", # Funny "en_female_emotional", # Peaceful "en_male_cody", # Serious ) @@ -86,7 +85,9 @@ class TikTok: "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._session = requests.Session() @@ -102,15 +103,12 @@ class TikTok: # get the audio from the TikTok API data = self.get_voices(voice=voice, text=text) - # check if there was an error in the request - status_code = data["status_code"] - if status_code != 0: - raise TikTokTTSException(status_code, data["message"]) + status_code = data["error"] # decode data from base64 to binary try: - raw_voices = data["data"]["v_str"] + raw_voices = data["data"] except: print( "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/", "") # prepare url request - params = {"req_text": text, "speaker_map_type": 0, "aid": 1233} - + params = {"text": text,"voice": voice} if voice is not None: - params["text_speaker"] = voice + params["voice"] = voice # send request try: - response = self._session.post(self.URI_BASE, params=params) + response = self._session.post(self.URI_BASE, json=params) except ConnectionError: 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() @@ -148,18 +145,10 @@ class TikTok: class TikTokTTSException(Exception): - def __init__(self, code: int, message: str): - self._code = code - self._message = 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}" + def __init__(self, status_code, message): + self.status_code = status_code + self.message = message + super().__init__(self.message) - return f"Code: {self._message}, reason: unknown, message: {self._message}" + def __str__(self): + return f"Code: {self.status_code}, Message: {self.message}" diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 7a73d61..60730ec 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -1,5 +1,6 @@ import os import re +import ffmpeg from pathlib import Path from typing import Tuple @@ -14,6 +15,8 @@ from utils import settings from utils.console import print_step, print_substep from utils.voice import sanitize_text +from pydub import AudioSegment + DEFAULT_MAX_LENGTH: int = ( 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: self.call_tts("postaudio", process_text(self.reddit_object["thread_post"])) 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)) 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. if self.length > self.max_length and idx > 1: self.length -= self.last_clip_length @@ -124,19 +127,22 @@ class TTSEngine: continue else: self.call_tts(f"{idx}-{idy}.part", newtext) - with open(f"{self.path}/list.txt", "w") as f: - for idz in range(0, len(split_text)): - f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") - split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3")) - f.write("file " + f"'silence.mp3'" + "\n") - - os.system( - "ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " - + "-i " - + f"{self.path}/list.txt " - + "-c copy " - + f"{self.path}/{idx}.mp3" - ) + concat_parts=[] + # with open(f"{self.path}/list.txt", "w") as f: + for idz in range(0, len(split_text)): + # f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n") + concat_parts.append(ffmpeg.input(f"{idx}-{idz}.part.mp3")) + split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3")) + # f.write("file " + f"'silence.mp3'" + "\n") + concat_parts.append('silence.mp3') + ffmpeg.concat(*concat_parts).output(f"{self.path}/{idx}.mp3").overwrite_output().global_args('-y -hide_banner -loglevel panic -safe 0').run(quiet=True) + # os.system( + # "ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " + # + "-i " + # + f"{self.path}/list.txt " + # + "-c copy " + # + f"{self.path}/{idx}.mp3" + # ) try: for i in range(0, len(split_files)): os.unlink(split_files[i]) @@ -146,20 +152,31 @@ class TTSEngine: print("OSError") 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( text, - filepath=f"{self.path}/{filename}.mp3", + filepath=mp3_filepath, 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: - # self.length += MP3(f"{self.path}/{filename}.mp3").info.length + # self.length += MP3(mp3_filepath).info.length # except (MutagenError, HeaderNotFoundError): - # self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3") + # self.length += sox.file_info.duration(mp3_filepath) try: - clip = AudioFileClip(f"{self.path}/{filename}.mp3") - self.last_clip_length = clip.duration - self.length += clip.duration - clip.close() + clip = AudioSegment.from_mp3(mp3_filepath) + self.last_clip_length = clip.duration_seconds + self.length += clip.duration_seconds + # clip = AudioFileClip(mp3_filepath) + # self.last_clip_length = clip.duration + # self.length += clip.duration + # clip.close() + except: self.length = 0 diff --git a/build.sh b/build.sh index 45ebd33..b71ef6c 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,2 @@ #!/bin/sh -docker build -t rvmt . +sudo docker build -t rvmt . diff --git a/main.py b/main.py index efaa51c..8c62b24 100755 --- a/main.py +++ b/main.py @@ -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}' ) # correct 1st 2nd 3rd 4th 5th.... main() - Popen("cls" if name == "nt" else "clear", shell=True).wait() + # Popen("cls" if name == "nt" else "clear", shell=True).wait() def shutdown() -> NoReturn: - if "redditid" in globals(): + if "redditid" in globals() and bool(settings.config["settings"]["delete_temp_files"]): print_markdown("## Clearing temp files") 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("+"))}' ) 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"]: run_many(config["settings"]["times_to_run"]) else: diff --git a/reddit/subreddit.py b/reddit/subreddit.py index e1def23..0700573 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -1,4 +1,5 @@ import re +import math from prawcore.exceptions import ResponseException @@ -8,13 +9,13 @@ from praw.models import MoreComments from prawcore.exceptions import ResponseException from utils.console import print_step, print_substep +from utils.openai import ai_rewrite_story from utils.subreddit import get_subreddit_undone from utils.videos import check_done from utils.voice import sanitize_text from utils.posttextparser import posttextparser from utils.ai_methods import sort_by_similarity - def get_subreddit_threads(POST_ID: str): """ Returns a list of threads from the AskReddit subreddit. @@ -125,10 +126,13 @@ def get_subreddit_threads(POST_ID: str): content["is_nsfw"] = submission.over_18 content["comments"] = [] 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: - content["thread_post"] = posttextparser(submission.selftext) + content["thread_post"] = posttextparser(ai_selftext) else: - content["thread_post"] = submission.selftext + content["thread_post"] = ai_selftext else: for top_level_comment in submission.comments: if isinstance(top_level_comment, MoreComments): diff --git a/requirements.txt b/requirements.txt index c9abc85..d10b8d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,8 @@ torch==2.0.1 transformers==4.29.2 ffmpeg-python==0.2.0 elevenlabs==0.2.17 -yt-dlp==2023.7.6 \ No newline at end of file +yt-dlp==2023.7.6 +pydub==0.25.1 +openai==1.2.4 +tiktoken==0.5.1 +humanfriendly==10.0 \ No newline at end of file diff --git a/run.sh b/run.sh index 1769e21..6010ab7 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,3 @@ #!/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 + diff --git a/utils/.config.template.toml b/utils/.config.template.toml index accf86d..cbdee5b 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -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" } 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" } -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" } 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" } @@ -18,6 +18,14 @@ min_comments = { default = 20, optional = false, nmin = 10, type = "int", explan [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_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] 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" } [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_audio = { optional = true, default = "lofi", example = "chill-summer", options = ["lofi","lofi-2","chill-summer",""], explanation = "Sets the background audio for the video" } +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 = ["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"} 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.)" } @@ -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)" } 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" } +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"} \ No newline at end of file diff --git a/utils/background_audios.json b/utils/background_audios.json index 752436d..7f9e70e 100644 --- a/utils/background_audios.json +++ b/utils/background_audios.json @@ -14,5 +14,15 @@ "https://www.youtube.com/watch?v=EZE8JagnBI8", "chill-summer.mp3", "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" ] } diff --git a/utils/background_videos.json b/utils/background_videos.json index 6e00992..8ab2e02 100644 --- a/utils/background_videos.json +++ b/utils/background_videos.json @@ -59,5 +59,47 @@ "steep.mp4", "joel", "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" ] } diff --git a/utils/cleanup.py b/utils/cleanup.py index 6e00d4c..98375ea 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -13,8 +13,9 @@ def cleanup(reddit_id) -> int: Returns: int: How many files were deleted """ - directory = f"../assets/temp/{reddit_id}/" + directory = f"./assets/temp/{reddit_id}/" if exists(directory): shutil.rmtree(directory) return 1 + return 0 diff --git a/utils/ffmpeg.py b/utils/ffmpeg.py new file mode 100644 index 0000000..f5c1948 --- /dev/null +++ b/utils/ffmpeg.py @@ -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() \ No newline at end of file diff --git a/utils/ffmpeg_progress.py b/utils/ffmpeg_progress.py new file mode 100644 index 0000000..56af3af --- /dev/null +++ b/utils/ffmpeg_progress.py @@ -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() \ No newline at end of file diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 48e15b8..e7392a6 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -18,10 +18,11 @@ def load_text_replacements(): def perform_text_replacements(text): updated_text = text 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) 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) 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") - 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) text = process_text(text, False) draw_multiple_line_text(image, perform_text_replacements(text), font, txtclr, padding, wrap=30, transparent=transparent) diff --git a/utils/openai.py b/utils/openai.py new file mode 100644 index 0000000..1d6db3f --- /dev/null +++ b/utils/openai.py @@ -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) + \ No newline at end of file diff --git a/utils/text_replacements.json b/utils/text_replacements.json index a020c2f..a68183e 100644 --- a/utils/text_replacements.json +++ b/utils/text_replacements.json @@ -5,6 +5,7 @@ ["killing", "unaliving"], ["kill", "unalive"], ["dead", "unalive"], + ["drugged", "out of it, caused by substances,"], ["drug", "substance"], ["gun", "boom stick"], ["nude", "without clothes"], @@ -13,10 +14,15 @@ ["shit", "poo"], ["weed", "oui'd"], ["shoot", "hit"], - ["shot", "hit"] + ["shot", "hit"], + ["reddit", "tiktok"] ], "text-only": [], "audio-only": [ - ["mic", "mike"] + ["mic", "mike"], + ["ai", "artificial intelligence"], + ["a.i.", "artificial intelligence"], + ["a.i", "artificial intelligence"], + ["mkultra", "em kay ultra"] ] } diff --git a/utils/timeout.py b/utils/timeout.py new file mode 100644 index 0000000..9aa1dff --- /dev/null +++ b/utils/timeout.py @@ -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) \ No newline at end of file diff --git a/utils/voice.py b/utils/voice.py index 880cf10..45f7536 100644 --- a/utils/voice.py +++ b/utils/voice.py @@ -24,10 +24,11 @@ def load_text_replacements(): def perform_text_replacements(text): updated_text = text 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) - for replacement in text_replacements['audio-only']: - compiled = re.compile(re.escape(replacement[0]), re.IGNORECASE) + for replacement in text_replacements['text-only']: + compiled = re.compile(r"\b{}\b".format(regex_escaped_word), re.IGNORECASE) updated_text = compiled.sub(replacement[1], updated_text) return updated_text diff --git a/video_creation/background.py b/video_creation/background.py index 8c15b40..e7a72a7 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -1,6 +1,7 @@ import json import random import re +import math from pathlib import Path from random import randrange 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.console import print_step, print_substep import yt_dlp +import ffmpeg 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: 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}") + 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( 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") + 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]}" - 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( video_length, background_video.duration ) + background_video.close() # Extract video subclip try: ffmpeg_extract_subclip( - f"assets/backgrounds/video/{video_choice}", + video_file_path, 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/video/{video_choice}") as video: - new = video.subclip(start_time_video, end_time_video) - new.write_videofile(f"assets/temp/{id}/background.mp4") + video=VideoFileClip(video_file_path) + new = video.subclip(start_time_video, end_time_video) + new.write_videofile(f"assets/temp/{id}/background.mp4") + video.close() print_substep("Background video chopped successfully!", style="bold green") return background_config["video"][2] diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 84ca249..6903855 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -13,58 +13,15 @@ from rich.progress import track from utils.cleanup import cleanup 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.videos import save_data from utils import settings - -import tempfile -import threading -import time +from humanfriendly import format_size, format_timespan 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: name = re.sub(r'[?\\"%*:|<>]', "", 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: + print_substep('Preparing the background video...') 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 = ( - ffmpeg.input(f"assets/temp/{reddit_id}/background.mp4") + ffmpeg.input(input_path) .filter("crop", f"ih*({W}/{H})", "ih") .output( output_path, @@ -100,7 +60,7 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: .overwrite_output() ) try: - output.run(quiet=True) + ffmpeg_progress_run(output, input_duration) except ffmpeg.Error as e: print(e.stderr.decode("utf8")) 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_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) ] audio_clips_durations.insert( 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) ffmpeg.output( audio_concat, f"assets/temp/{reddit_id}/audio.mp3", **{"b:a": "192k"} ).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) audio = ffmpeg.input(f"assets/temp/{reddit_id}/audio.mp3") @@ -212,14 +172,12 @@ def make_final_video( current_time = 0 if settings.config["settings"]["storymode"]: audio_clips_durations = [ - float( - ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3")["format"]["duration"] - ) + get_duration(f"assets/temp/{reddit_id}/mp3/postaudio-{i}.mp3") for i in range(number_of_clips) ] audio_clips_durations.insert( 0, - float(ffmpeg.probe(f"assets/temp/{reddit_id}/mp3/title.mp3")["format"]["duration"]), + get_duration(f"assets/temp/{reddit_id}/mp3/title.mp3") ) if settings.config["settings"]["storymodemethod"] == 0: image_clips.insert( @@ -228,8 +186,9 @@ def make_final_video( "scale", screenshot_width, -1 ), ) + image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( - image_clips[0], + image_overlay, enable=f"between(t,{current_time},{current_time + audio_clips_durations[0]})", x="(main_w-overlay_w)/2", y="(main_h-overlay_h)/2", @@ -242,13 +201,26 @@ def make_final_video( "scale", screenshot_width, -1 ) ) + image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) background_clip = background_clip.overlay( - image_clips[i], + image_overlay, enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})", x="(main_w-overlay_w)/2", y="(main_h-overlay_h)/2", ) 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: for i in range(0, number_of_clips + 1): image_clips.append( @@ -327,22 +299,14 @@ def make_final_video( ) background_clip = background_clip.filter("scale", W, H) 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}" - 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. - try: + path = defaultPath + f"/{filename}" + path = ( + path[:251] + ".mp4" + ) # Prevent a error by limiting the path length, do not change this. + try: + ffmpeg_progress_run( ffmpeg.output( background_clip, final_audio, @@ -354,25 +318,20 @@ def make_final_video( "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, - ) - except ffmpeg.Error as e: - print(e.stderr.decode("utf8")) - exit(1) - old_percentage = pbar.n - pbar.update(100 - old_percentage) + ).overwrite_output(), + length + ) + except ffmpeg.Error as e: + print(e.stderr.decode("utf8")) + exit(1) 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: - try: + try: + ffmpeg_progress_run( ffmpeg.output( background_clip, audio, @@ -384,21 +343,18 @@ def make_final_video( "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, - ) - except ffmpeg.Error as e: - print(e.stderr.decode("utf8")) - exit(1) - - old_percentage = pbar.n - pbar.update(100 - old_percentage) - pbar.close() - 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 📁") + ).overwrite_output(), + length + ) + except ffmpeg.Error as e: + print(e.stderr.decode("utf8")) + exit(1) + save_filename = filename + ".mp4" + save_data(subreddit, save_filename, title, idx, background_config["video"][2]) + if bool(settings.config["settings"]["delete_temp_files"]): + print_step("Removing temporary files 🗑") + cleanups = cleanup(reddit_id) + print_substep(f"Removed {cleanups} temporary files 🗑") + file_size=os.stat(f"{defaultPath}/{save_filename}").st_size + file_size_human_readable=format_size(file_size) + print_step(f"Done! 🎉 The {file_size_human_readable} video is in the results folder 📁")