Merge pull request #653 from elebumm/feat/better-tts-changer
Better TTS Changer
This commit is contained in:
19 files changed
+384
-318
No files matched your search
+11
-3
@@ -72,10 +72,18 @@ POSTLANG=""
|
||||
VOICE="Matthew" # e.g. en_us_002
|
||||
#EXPLANATION sets the voice the TTS uses
|
||||
|
||||
TTsChoice="polly" # todo add docs
|
||||
#EXPLANATION the backend used for TTS, default is polly
|
||||
# IMPORTANT NOTE: if you use translate, you need to set this gtts or set tiktok and use custom voice in your language
|
||||
TTSCHOICE="Polly"
|
||||
#EXPLANATION the backend used for TTS. Without anything specified, the user will be prompted to choose one.
|
||||
# IMPORTANT NOTE: if you use translate, you need to set this to googletranslate or tiktok and use custom voice in your language
|
||||
|
||||
STREAMLABS_VOICE="Joanna"
|
||||
#EXPLANATION Sets the voice for the Streamlabs Polly TTS Engine. Check the file for more information on different voices.
|
||||
|
||||
AWS_VOICE="Joanna"
|
||||
#EXPLANATION Sets the voice for the AWS Polly TTS Engine. Check the file for more information on different voices.
|
||||
|
||||
TIKTOK_VOICE="en_us_006"
|
||||
#EXPLANATION Sets the voice for the TikTok TTS Engine. Check the file for more information on different voices.
|
||||
|
||||
#OPTIONAL
|
||||
STORYMODE="False"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -149,7 +149,7 @@ disable=raw-checker-failed,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead
|
||||
use-symbolic-message-instead,
|
||||
attribute-defined-outside-init,
|
||||
invalid-name,
|
||||
missing-docstring,
|
||||
|
||||
@@ -34,7 +34,6 @@ The only original thing being done is the editing and gathering of all materials
|
||||
|
||||
- Python 3.6+
|
||||
- Playwright (this should install automatically in installation)
|
||||
- Sox
|
||||
|
||||
## Installation 👩💻
|
||||
|
||||
@@ -43,21 +42,17 @@ The only original thing being done is the editing and gathering of all materials
|
||||
|
||||
2b **Manual Install**: Rename `.env.template` to `.env` and replace all values with the appropriate fields. To get Reddit keys (**required**), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script". Copy your keys into the `.env` file, along with whether your account uses two-factor authentication.
|
||||
|
||||
3. Install [SoX](https://sourceforge.net/projects/sox/files/sox/)
|
||||
3. Run `pip install -r requirements.txt`
|
||||
|
||||
4. Add the directory of your SoX installation to the Path system variables (see https://github.com/elebumm/RedditVideoMakerBot/issues/613)
|
||||
and move the added directory all the way to the top of the list of directories.
|
||||
|
||||
5. Run `pip install -r requirements.txt`
|
||||
4. Run `playwright install` and `playwright install-deps`. (if this fails try adding python -m to the front of the command)
|
||||
|
||||
6. Run `playwright install` and `playwright install-deps`. (if this fails try adding python -m to the front of the command)
|
||||
|
||||
7. Run `python main.py` (unless you chose automatic install, then the installer will automatically run main.py)
|
||||
5. Run `python main.py` (unless you chose automatic install, then the installer will automatically run main.py)
|
||||
required\*\*), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script".
|
||||
Copy your keys into the `.env` file, along with whether your account uses two-factor authentication.
|
||||
8. Enjoy 😎
|
||||
6. Enjoy 😎
|
||||
|
||||
(Note if you got an error installing or running the bot try first rerunning the command with a three after the name e.g. python3 or pip3)
|
||||
|
||||
## Video
|
||||
|
||||
https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4
|
||||
|
||||
+16
-10
@@ -1,13 +1,19 @@
|
||||
from gtts import gTTS
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import os
|
||||
from gtts import gTTS
|
||||
|
||||
max_chars = 0
|
||||
|
||||
|
||||
class GTTS:
|
||||
def tts(
|
||||
self,
|
||||
req_text: str = "Google Text To Speech",
|
||||
filename: str = "title.mp3",
|
||||
random_speaker=False,
|
||||
censor=False,
|
||||
):
|
||||
tts = gTTS(text=req_text, lang=os.getenv("POSTLANG") or "en", slow=False)
|
||||
tts.save(f"{filename}")
|
||||
def __init__(self):
|
||||
self.max_chars = 0
|
||||
self.voices = []
|
||||
|
||||
def run(self, text, filepath):
|
||||
tts = gTTS(text=text, lang=os.getenv("POSTLANG") or "en", slow=False)
|
||||
tts.save(filepath)
|
||||
|
||||
def randomvoice(self):
|
||||
return random.choice(self.voices)
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
import requests
|
||||
import sox
|
||||
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
|
||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
||||
from requests.exceptions import JSONDecodeError
|
||||
|
||||
voices = [
|
||||
"Brian",
|
||||
"Emma",
|
||||
"Russell",
|
||||
"Joey",
|
||||
"Matthew",
|
||||
"Joanna",
|
||||
"Kimberly",
|
||||
"Amy",
|
||||
"Geraint",
|
||||
"Nicole",
|
||||
"Justin",
|
||||
"Ivy",
|
||||
"Kendra",
|
||||
"Salli",
|
||||
"Raveena",
|
||||
]
|
||||
|
||||
|
||||
# valid voices https://lazypy.ro/tts/
|
||||
|
||||
|
||||
class POLLY:
|
||||
def __init__(self):
|
||||
self.url = "https://streamlabs.com/polly/speak"
|
||||
|
||||
def tts(
|
||||
self,
|
||||
req_text: str = "Amazon Text To Speech",
|
||||
filename: str = "title.mp3",
|
||||
random_speaker=False,
|
||||
censor=False,
|
||||
):
|
||||
if random_speaker:
|
||||
voice = self.randomvoice()
|
||||
else:
|
||||
if not os.getenv("VOICE"):
|
||||
return ValueError(
|
||||
"Please set the environment variable VOICE to a valid voice. options are: {}".format(
|
||||
voices
|
||||
)
|
||||
)
|
||||
voice = str(os.getenv("VOICE")).capitalize()
|
||||
body = {"voice": voice, "text": req_text, "service": "polly"}
|
||||
response = requests.post(self.url, data=body)
|
||||
try:
|
||||
voice_data = requests.get(response.json()["speak_url"])
|
||||
with open(filename, "wb") as f:
|
||||
f.write(voice_data.content)
|
||||
except (KeyError, JSONDecodeError):
|
||||
if response.json()["error"] == "Text length is too long!":
|
||||
chunks = [m.group().strip() for m in re.finditer(r" *((.{0,499})(\.|.$))", req_text)]
|
||||
|
||||
audio_clips = []
|
||||
cbn = sox.Combiner()
|
||||
|
||||
chunkId = 0
|
||||
for chunk in chunks:
|
||||
body = {"voice": voice, "text": chunk, "service": "polly"}
|
||||
resp = requests.post(self.url, data=body)
|
||||
voice_data = requests.get(resp.json()["speak_url"])
|
||||
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
|
||||
out.write(voice_data.content)
|
||||
|
||||
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
|
||||
|
||||
chunkId = chunkId + 1
|
||||
try:
|
||||
if len(audio_clips) > 1:
|
||||
cbn.convert(samplerate=44100, n_channels=2)
|
||||
cbn.build(audio_clips, filename, "concatenate")
|
||||
else:
|
||||
os.rename(audio_clips[0], filename)
|
||||
except (
|
||||
sox.core.SoxError,
|
||||
FileNotFoundError,
|
||||
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
|
||||
for clip in audio_clips:
|
||||
i = audio_clips.index(clip) # get the index of the clip
|
||||
audio_clips = (
|
||||
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
|
||||
) # replace the clip with an AudioFileClip
|
||||
audio_concat = concatenate_audioclips(audio_clips)
|
||||
audio_composite = CompositeAudioClip([audio_concat])
|
||||
audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
|
||||
|
||||
def make_readable(self, text):
|
||||
"""
|
||||
Amazon Polly fails to read some symbols properly such as '& (and)'.
|
||||
So we normalize input text before passing it to the service
|
||||
"""
|
||||
text = text.replace("&", "and")
|
||||
return text
|
||||
|
||||
def randomvoice(self):
|
||||
return random.choice(voices)
|
||||
+31
-72
@@ -1,12 +1,7 @@
|
||||
import base64
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
import requests
|
||||
import sox
|
||||
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
|
||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
# from profanity_filter import ProfanityFilter
|
||||
@@ -67,75 +62,39 @@ noneng = [
|
||||
|
||||
class TikTok: # TikTok Text-to-Speech Wrapper
|
||||
def __init__(self):
|
||||
self.URI_BASE = (
|
||||
"https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
|
||||
)
|
||||
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
|
||||
self.max_chars = 300
|
||||
self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng}
|
||||
|
||||
def tts(
|
||||
self,
|
||||
req_text: str = "TikTok Text To Speech",
|
||||
filename: str = "title.mp3",
|
||||
random_speaker: bool = False,
|
||||
censor=False,
|
||||
):
|
||||
req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and")
|
||||
if censor:
|
||||
# req_text = pf.censor(req_text)
|
||||
pass
|
||||
def run(self, text, filepath, random_voice: bool = False):
|
||||
# if censor:
|
||||
# req_text = pf.censor(req_text)
|
||||
# pass
|
||||
voice = (
|
||||
self.randomvoice() if random_speaker else (os.getenv("VOICE") or random.choice(human))
|
||||
self.randomvoice()
|
||||
if random_voice
|
||||
else (os.getenv("TIKTOK_VOICE") or random.choice(self.voices["human"]))
|
||||
)
|
||||
|
||||
chunks = [m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)]
|
||||
|
||||
audio_clips = []
|
||||
cbn = sox.Combiner()
|
||||
# cbn.set_input_format(file_type=["mp3" for _ in chunks])
|
||||
|
||||
chunkId = 0
|
||||
for chunk in chunks:
|
||||
try:
|
||||
r = requests.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
|
||||
except requests.exceptions.SSLError:
|
||||
# https://stackoverflow.com/a/47475019/18516611
|
||||
session = requests.Session()
|
||||
retry = Retry(connect=3, backoff_factor=0.5)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
r = session.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
|
||||
print(r.text)
|
||||
vstr = [r.json()["data"]["v_str"]][0]
|
||||
b64d = base64.b64decode(vstr)
|
||||
|
||||
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
|
||||
out.write(b64d)
|
||||
|
||||
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
|
||||
|
||||
chunkId = chunkId + 1
|
||||
try:
|
||||
if len(audio_clips) > 1:
|
||||
cbn.convert(samplerate=44100, n_channels=2)
|
||||
cbn.build(audio_clips, filename, "concatenate")
|
||||
else:
|
||||
os.rename(audio_clips[0], filename)
|
||||
except (
|
||||
sox.core.SoxError,
|
||||
FileNotFoundError,
|
||||
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
|
||||
for clip in audio_clips:
|
||||
i = audio_clips.index(clip) # get the index of the clip
|
||||
audio_clips = (
|
||||
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
|
||||
) # replace the clip with an AudioFileClip
|
||||
audio_concat = concatenate_audioclips(audio_clips)
|
||||
audio_composite = CompositeAudioClip([audio_concat])
|
||||
audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
|
||||
r = requests.post(
|
||||
f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0"
|
||||
)
|
||||
except requests.exceptions.SSLError:
|
||||
# https://stackoverflow.com/a/47475019/18516611
|
||||
session = requests.Session()
|
||||
retry = Retry(connect=3, backoff_factor=0.5)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
r = session.post(
|
||||
f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0"
|
||||
)
|
||||
# print(r.text)
|
||||
vstr = [r.json()["data"]["v_str"]][0]
|
||||
b64d = base64.b64decode(vstr)
|
||||
|
||||
@staticmethod
|
||||
def randomvoice():
|
||||
ok_or_good = random.randrange(1, 10)
|
||||
if ok_or_good == 1: # 1/10 chance of ok voice
|
||||
return random.choice(voices)
|
||||
return random.choice(human) # 9/10 chance of good voice
|
||||
with open(filepath, "wb") as out:
|
||||
out.write(b64d)
|
||||
|
||||
def randomvoice(self):
|
||||
return random.choice(self.voices["human"])
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
from boto3 import Session
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
import sys
|
||||
import os
|
||||
import random
|
||||
|
||||
voices = [
|
||||
"Brian",
|
||||
"Emma",
|
||||
"Russell",
|
||||
"Joey",
|
||||
"Matthew",
|
||||
"Joanna",
|
||||
"Kimberly",
|
||||
"Amy",
|
||||
"Geraint",
|
||||
"Nicole",
|
||||
"Justin",
|
||||
"Ivy",
|
||||
"Kendra",
|
||||
"Salli",
|
||||
"Raveena",
|
||||
]
|
||||
|
||||
|
||||
class AWSPolly:
|
||||
def __init__(self):
|
||||
self.max_chars = 0
|
||||
self.voices = voices
|
||||
|
||||
def run(self, text, filepath, random_voice: bool = False):
|
||||
session = Session(profile_name="polly")
|
||||
polly = session.client("polly")
|
||||
if random_voice:
|
||||
voice = self.randomvoice()
|
||||
else:
|
||||
if not os.getenv("VOICE"):
|
||||
return ValueError(
|
||||
f"Please set the environment variable VOICE to a valid voice. options are: {voices}"
|
||||
)
|
||||
voice = str(os.getenv("AWS_VOICE")).capitalize()
|
||||
try:
|
||||
# Request speech synthesis
|
||||
response = polly.synthesize_speech(
|
||||
Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural"
|
||||
)
|
||||
except (BotoCoreError, ClientError) as error:
|
||||
# The service returned an error, exit gracefully
|
||||
print(error)
|
||||
sys.exit(-1)
|
||||
|
||||
# Access the audio stream from the response
|
||||
if "AudioStream" in response:
|
||||
file = open(filepath, "wb")
|
||||
file.write(response["AudioStream"].read())
|
||||
file.close()
|
||||
# print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green")
|
||||
|
||||
else:
|
||||
# The response didn't contain audio data, exit gracefully
|
||||
print("Could not stream audio")
|
||||
sys.exit(-1)
|
||||
|
||||
def randomvoice(self):
|
||||
return random.choice(self.voices)
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
import re
|
||||
from os import getenv
|
||||
from mutagen.mp3 import MP3
|
||||
import translators as ts
|
||||
from rich.progress import track
|
||||
from moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips
|
||||
from utils.console import print_step, print_substep
|
||||
from utils.voice import sanitize_text
|
||||
|
||||
|
||||
class TTSEngine:
|
||||
|
||||
"""Calls the given TTS engine to reduce code duplication and allow multiple TTS engines.
|
||||
|
||||
Args:
|
||||
tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method.
|
||||
reddit_object : The reddit object that contains the posts to read.
|
||||
path (Optional) : The unix style path to save the mp3 files to. This must not have leading or trailing slashes.
|
||||
max_length (Optional) : The maximum length of the mp3 files in total.
|
||||
|
||||
Notes:
|
||||
tts_module must take the arguments text and filepath.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tts_module,
|
||||
reddit_object: dict,
|
||||
path: str = "assets/temp/mp3",
|
||||
max_length: int = 50,
|
||||
):
|
||||
self.tts_module = tts_module()
|
||||
self.reddit_object = reddit_object
|
||||
self.path = path
|
||||
self.max_length = max_length
|
||||
self.length = 0
|
||||
|
||||
def run(self) -> Tuple[int, int]:
|
||||
|
||||
Path(self.path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# This file needs to be removed in case this post does not use post text, so that it wont appear in the final video
|
||||
try:
|
||||
Path(f"{self.path}/posttext.mp3").unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print_step("Saving Text to MP3 files...")
|
||||
|
||||
self.call_tts("title", self.reddit_object["thread_title"])
|
||||
if (
|
||||
self.reddit_object["thread_post"] != ""
|
||||
and getenv("STORYMODE", "").casefold() == "true"
|
||||
):
|
||||
self.call_tts("posttext", self.reddit_object["thread_post"])
|
||||
|
||||
idx = None
|
||||
for idx, comment in track(
|
||||
enumerate(self.reddit_object["comments"]), "Saving..."
|
||||
):
|
||||
# ! Stop creating mp3 files if the length is greater than max length.
|
||||
if self.length > self.max_length:
|
||||
break
|
||||
if not self.tts_module.max_chars:
|
||||
self.call_tts(f"{idx}", comment["comment_body"])
|
||||
else:
|
||||
self.split_post(comment["comment_body"], idx)
|
||||
|
||||
print_substep("Saved Text to MP3 files successfully.", style="bold green")
|
||||
return self.length, idx
|
||||
|
||||
def split_post(self, text: str, idx: int) -> str:
|
||||
split_files = []
|
||||
split_text = [
|
||||
x.group().strip()
|
||||
for x in re.finditer(
|
||||
rf" *((.{{0,{self.tts_module.max_chars}}})(\.|.$))", text
|
||||
)
|
||||
]
|
||||
|
||||
idy = None
|
||||
for idy, text_cut in enumerate(split_text):
|
||||
# print(f"{idx}-{idy}: {text_cut}\n")
|
||||
self.call_tts(f"{idx}-{idy}.part", text_cut)
|
||||
split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy}.part.mp3"))
|
||||
CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile(
|
||||
f"{self.path}/{idx}.mp3", fps=44100, verbose=False, logger=None
|
||||
)
|
||||
|
||||
for i in range(0, idy + 1):
|
||||
# print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3")
|
||||
Path(f"{self.path}/{idx}-{i}.part.mp3").unlink()
|
||||
|
||||
def call_tts(self, filename: str, text: str):
|
||||
self.tts_module.run(
|
||||
text=process_text(text), filepath=f"{self.path}/{filename}.mp3"
|
||||
)
|
||||
self.length += MP3(f"{self.path}/{filename}.mp3").info.length
|
||||
|
||||
|
||||
def process_text(text: str):
|
||||
lang = getenv("POSTLANG", "")
|
||||
new_text = sanitize_text(text)
|
||||
if lang:
|
||||
print_substep("Translating Text...")
|
||||
new_text = ts.google(text, to_language=lang)
|
||||
return new_text
|
||||
@@ -0,0 +1,53 @@
|
||||
import random
|
||||
import os
|
||||
import requests
|
||||
from requests.exceptions import JSONDecodeError
|
||||
|
||||
voices = [
|
||||
"Brian",
|
||||
"Emma",
|
||||
"Russell",
|
||||
"Joey",
|
||||
"Matthew",
|
||||
"Joanna",
|
||||
"Kimberly",
|
||||
"Amy",
|
||||
"Geraint",
|
||||
"Nicole",
|
||||
"Justin",
|
||||
"Ivy",
|
||||
"Kendra",
|
||||
"Salli",
|
||||
"Raveena",
|
||||
]
|
||||
|
||||
|
||||
# valid voices https://lazypy.ro/tts/
|
||||
|
||||
|
||||
class StreamlabsPolly:
|
||||
def __init__(self):
|
||||
self.url = "https://streamlabs.com/polly/speak"
|
||||
self.max_chars = 550
|
||||
self.voices = voices
|
||||
|
||||
def run(self, text, filepath, random_voice: bool = False):
|
||||
if random_voice:
|
||||
voice = self.randomvoice()
|
||||
else:
|
||||
if not os.getenv("VOICE"):
|
||||
return ValueError(
|
||||
f"Please set the environment variable VOICE to a valid voice. options are: {voices}"
|
||||
)
|
||||
voice = str(os.getenv("STREAMLABS_VOICE")).capitalize()
|
||||
body = {"voice": voice, "text": text, "service": "polly"}
|
||||
response = requests.post(self.url, data=body)
|
||||
try:
|
||||
voice_data = requests.get(response.json()["speak_url"])
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(voice_data.content)
|
||||
except (KeyError, JSONDecodeError):
|
||||
print("Error occured calling Streamlabs Polly")
|
||||
|
||||
def randomvoice(self):
|
||||
return random.choice(self.voices)
|
||||
@@ -1,24 +0,0 @@
|
||||
from os import getenv
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from TTS.GTTS import GTTS
|
||||
from TTS.POLLY import POLLY
|
||||
from TTS.TikTok import TikTok
|
||||
from utils.console import print_substep
|
||||
|
||||
CHOICE_DIR = {"tiktok": TikTok, "gtts": GTTS, "polly": POLLY}
|
||||
|
||||
|
||||
class TTS:
|
||||
def __new__(cls):
|
||||
load_dotenv()
|
||||
try:
|
||||
CHOICE = getenv("TTsChoice").casefold()
|
||||
except AttributeError:
|
||||
print_substep("None defined. Defaulting to 'polly.'")
|
||||
CHOICE = "polly"
|
||||
valid_keys = [key.lower() for key in CHOICE_DIR.keys()]
|
||||
if CHOICE not in valid_keys:
|
||||
raise ValueError(f"{CHOICE} is not valid. Please use one of these {valid_keys} options")
|
||||
return CHOICE_DIR.get(CHOICE)()
|
||||
+13
-5
@@ -31,7 +31,9 @@ def get_subreddit_threads():
|
||||
|
||||
content = {}
|
||||
if str(getenv("REDDIT_2FA")).casefold() == "yes":
|
||||
print("\nEnter your two-factor authentication code from your authenticator app.\n")
|
||||
print(
|
||||
"\nEnter your two-factor authentication code from your authenticator app.\n"
|
||||
)
|
||||
code = input("> ")
|
||||
print()
|
||||
pw = getenv("REDDIT_PASSWORD")
|
||||
@@ -55,14 +57,18 @@ def get_subreddit_threads():
|
||||
): # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
|
||||
try:
|
||||
subreddit = reddit.subreddit(
|
||||
re.sub(r"r\/", "", input("What subreddit would you like to pull from? "))
|
||||
re.sub(
|
||||
r"r\/", "", input("What subreddit would you like to pull from? ")
|
||||
)
|
||||
# removes the r/ from the input
|
||||
)
|
||||
except ValueError:
|
||||
subreddit = reddit.subreddit("askreddit")
|
||||
print_substep("Subreddit not defined. Using AskReddit.")
|
||||
else:
|
||||
print_substep(f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config")
|
||||
print_substep(
|
||||
f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config"
|
||||
)
|
||||
subreddit = reddit.subreddit(
|
||||
getenv("SUBREDDIT")
|
||||
) # Allows you to specify in .env. Done for automation purposes.
|
||||
@@ -83,12 +89,14 @@ def get_subreddit_threads():
|
||||
print_substep(f"Thread has {upvotes} upvotes", style="bold blue")
|
||||
print_substep(f"Thread has a upvote ratio of {ratio}%", style="bold blue")
|
||||
print_substep(f"Thread has {num_comments} comments", style="bold blue")
|
||||
environ["VIDEO_TITLE"] = str(textify(submission.title)) # todo use global instend of env vars
|
||||
environ["VIDEO_TITLE"] = str(
|
||||
textify(submission.title)
|
||||
) # todo use global instend of env vars
|
||||
environ["VIDEO_ID"] = str(textify(submission.id))
|
||||
|
||||
content["thread_url"] = f"https://reddit.com{submission.permalink}"
|
||||
content["thread_title"] = submission.title
|
||||
# content["thread_content"] = submission.content
|
||||
content["thread_post"] = submission.selftext
|
||||
content["comments"] = []
|
||||
for top_level_comment in submission.comments:
|
||||
if isinstance(top_level_comment, MoreComments):
|
||||
|
||||
+2
-1
@@ -1,3 +1,5 @@
|
||||
boto3==1.24.12
|
||||
botocore==1.27.12
|
||||
gTTS==2.2.4
|
||||
moviepy==1.0.3
|
||||
mutagen==1.45.1
|
||||
@@ -7,5 +9,4 @@ python-dotenv==0.20.0
|
||||
pytube==12.1.0
|
||||
requests==2.28.0
|
||||
rich==12.4.4
|
||||
sox==1.4.1
|
||||
translators==5.2.2
|
||||
@@ -4,6 +4,7 @@ from rich.markdown import Markdown
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich.columns import Columns
|
||||
import re
|
||||
|
||||
console = Console()
|
||||
@@ -28,6 +29,12 @@ def print_substep(text, style=""):
|
||||
console.print(text, style=style)
|
||||
|
||||
|
||||
def print_table(items):
|
||||
"""Prints items in a table."""
|
||||
|
||||
console.print(Columns([Panel(f"[yellow]{item}", expand=True) for item in items]))
|
||||
|
||||
|
||||
def handle_input(
|
||||
message: str = "",
|
||||
check_type=False,
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@ def sanitize_text(text):
|
||||
# note: not removing apostrophes
|
||||
regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
|
||||
result = re.sub(regex_expr, " ", result)
|
||||
|
||||
result = result.replace("+", "plus").replace("&", "and")
|
||||
# remove extra whitespace
|
||||
return " ".join(result.split())
|
||||
@@ -40,7 +40,9 @@ def download_background():
|
||||
"assets/backgrounds", filename=f"{credit}-{filename}"
|
||||
)
|
||||
|
||||
print_substep("Background videos downloaded successfully! 🎉", style="bold green")
|
||||
print_substep(
|
||||
"Background videos downloaded successfully! 🎉", style="bold green"
|
||||
)
|
||||
|
||||
|
||||
def chop_background_video(video_length):
|
||||
|
||||
@@ -55,7 +55,9 @@ def make_final_video(number_of_clips, length):
|
||||
# add title to video
|
||||
image_clips = []
|
||||
# Gather all images
|
||||
if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
|
||||
if (
|
||||
opacity is None or float(opacity) >= 1
|
||||
): # opacity not set or is set to one OR MORE
|
||||
image_clips.insert(
|
||||
0,
|
||||
ImageClip("assets/temp/png/title.png")
|
||||
@@ -74,7 +76,9 @@ def make_final_video(number_of_clips, length):
|
||||
)
|
||||
|
||||
for i in range(0, number_of_clips):
|
||||
if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
|
||||
if (
|
||||
opacity is None or float(opacity) >= 1
|
||||
): # opacity not set or is set to one OR MORE
|
||||
image_clips.append(
|
||||
ImageClip(f"assets/temp/png/comment_{i}.png")
|
||||
.set_duration(audio_clips[i + 1].duration)
|
||||
@@ -100,7 +104,9 @@ def make_final_video(number_of_clips, length):
|
||||
# .set_opacity(float(opacity)),
|
||||
# )
|
||||
# else:
|
||||
image_concat = concatenate_videoclips(image_clips).set_position(("center", "center"))
|
||||
image_concat = concatenate_videoclips(image_clips).set_position(
|
||||
("center", "center")
|
||||
)
|
||||
image_concat.audio = audio_composite
|
||||
final = CompositeVideoClip([background_clip, image_concat])
|
||||
|
||||
@@ -134,7 +140,13 @@ def make_final_video(number_of_clips, length):
|
||||
print_substep("the results folder didn't exist so I made it")
|
||||
os.mkdir("./results")
|
||||
|
||||
final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k")
|
||||
final.write_videofile(
|
||||
"assets/temp/temp.mp4",
|
||||
fps=30,
|
||||
audio_codec="aac",
|
||||
audio_bitrate="192k",
|
||||
verbose=False,
|
||||
)
|
||||
ffmpeg_tools.ffmpeg_extract_subclip(
|
||||
"assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}"
|
||||
)
|
||||
|
||||
+45
-76
@@ -1,22 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
import sox
|
||||
from mutagen import MutagenError
|
||||
from mutagen.mp3 import MP3, HeaderNotFoundError
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
|
||||
from TTS.swapper import TTS
|
||||
from TTS.engine_wrapper import TTSEngine
|
||||
from TTS.GTTS import GTTS
|
||||
from TTS.streamlabs_polly import StreamlabsPolly
|
||||
from TTS.aws_polly import AWSPolly
|
||||
from TTS.TikTok import TikTok
|
||||
|
||||
from utils.console import print_table, print_step
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
from utils.console import print_step, print_substep
|
||||
from utils.voice import sanitize_text
|
||||
|
||||
import translators as ts
|
||||
import os
|
||||
TTSProviders = {
|
||||
"GoogleTranslate": GTTS,
|
||||
"AWSPolly": AWSPolly,
|
||||
"StreamlabsPolly": StreamlabsPolly,
|
||||
"TikTok": TikTok,
|
||||
}
|
||||
|
||||
VIDEO_LENGTH: int = 40 # secs
|
||||
|
||||
@@ -26,68 +30,33 @@ def save_text_to_mp3(reddit_obj):
|
||||
Args:
|
||||
reddit_obj : The reddit object you received from the reddit API in the askreddit.py file.
|
||||
"""
|
||||
print_step("Saving Text to MP3 files...")
|
||||
length = 0
|
||||
|
||||
# Create a folder for the mp3 files.
|
||||
Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if os.getenv("POSTLANG"):
|
||||
print_substep("Translating Texts...")
|
||||
tl_title = ts.google(reddit_obj["thread_title"], to_language=os.getenv("POSTLANG"))
|
||||
env = os.getenv("TTSCHOICE", "")
|
||||
if env.casefold() in map(lambda _: _.casefold(), TTSProviders):
|
||||
text_to_mp3 = TTSEngine(
|
||||
get_case_insensitive_key_value(TTSProviders, env), reddit_obj
|
||||
)
|
||||
else:
|
||||
print_substep("Skipping Translation...")
|
||||
tl_title = reddit_obj["thread_title"]
|
||||
|
||||
TextToSpeech = TTS()
|
||||
TextToSpeech.tts(
|
||||
sanitize_text(tl_title),
|
||||
filename=f"assets/temp/mp3/title.mp3",
|
||||
random_speaker=False,
|
||||
choice = ""
|
||||
while True:
|
||||
print_step("Please choose one of the following TTS providers: ")
|
||||
print_table(TTSProviders)
|
||||
choice = input("\n")
|
||||
if choice.casefold() in map(lambda _: _.casefold(), TTSProviders):
|
||||
break
|
||||
print("Unknown Choice")
|
||||
text_to_mp3 = TTSEngine(
|
||||
get_case_insensitive_key_value(TTSProviders, choice), reddit_obj
|
||||
)
|
||||
|
||||
return text_to_mp3.run()
|
||||
|
||||
|
||||
def get_case_insensitive_key_value(input_dict, key):
|
||||
return next(
|
||||
(
|
||||
value
|
||||
for dict_key, value in input_dict.items()
|
||||
if dict_key.lower() == key.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
try:
|
||||
length += MP3(f"assets/temp/mp3/title.mp3").info.length
|
||||
except HeaderNotFoundError: # note to self AudioFileClip
|
||||
length += sox.file_info.duration(f"assets/temp/mp3/title.mp3")
|
||||
if getenv("STORYMODE").casefold() == "true":
|
||||
TextToSpeech.tts(
|
||||
sanitize_text(reddit_obj["thread_content"]),
|
||||
filename=f"assets/temp/mp3/story_content.mp3",
|
||||
random_speaker=False,
|
||||
)
|
||||
# 'story_content'
|
||||
com = 0
|
||||
for comment in track((reddit_obj["comments"]), "Saving..."):
|
||||
# ! Stop creating mp3 files if the length is greater than VIDEO_LENGTH seconds. This can be longer, but this is just a good_voices starting point
|
||||
if length > VIDEO_LENGTH:
|
||||
break
|
||||
|
||||
if os.getenv("POSTLANG"):
|
||||
tl_comment = ts.google(comment["comment_body"], to_language=os.getenv("POSTLANG"))
|
||||
else:
|
||||
tl_comment = comment["comment_body"]
|
||||
|
||||
TextToSpeech.tts(
|
||||
sanitize_text(tl_comment),
|
||||
filename=f"assets/temp/mp3/{com}.mp3",
|
||||
random_speaker=False,
|
||||
)
|
||||
try:
|
||||
length += MP3(f"assets/temp/mp3/{com}.mp3").info.length
|
||||
com += 1
|
||||
except (HeaderNotFoundError, MutagenError, Exception):
|
||||
try:
|
||||
length += sox.file_info.duration(f"assets/temp/mp3/{com}.mp3")
|
||||
com += 1
|
||||
except (OSError, IOError):
|
||||
print(
|
||||
"would have removed"
|
||||
f"assets/temp/mp3/{com}.mp3"
|
||||
f"assets/temp/png/comment_{com}.png"
|
||||
)
|
||||
# remove(f"assets/temp/mp3/{com}.mp3")
|
||||
# remove(f"assets/temp/png/comment_{com}.png")# todo might cause odd un-syncing
|
||||
|
||||
print_substep("Saved Text to MP3 files Successfully.", style="bold green")
|
||||
# ! Return the index, so we know how many screenshots of comments we need to make.
|
||||
return length, com
|
||||
Reference in new issue
Block a user