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
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"]

@ -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",

@ -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}"

@ -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

@ -1,2 +1,2 @@
#!/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}'
) # 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:

@ -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):

@ -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
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
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" }
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"}

@ -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"
]
}

@ -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"
]
}

@ -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

@ -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):
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)

@ -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"],
["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"]
]
}

@ -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):
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

@ -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]

@ -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 📁")

Loading…
Cancel
Save