From 17e1a092bee17d3a198ce410663e3d7c304bc293 Mon Sep 17 00:00:00 2001 From: dtookey Date: Sun, 20 Sep 2020 13:35:53 -0400 Subject: [PATCH] updated tts.py support both gtts and aws polly (we borrow from the streamlabs api) moved some configuration stuff around added one test method for creating file names, but it doesn't have any assertions, so it's useless added enums for different configuration properties like tts engine, file naming, and poly voices --- config.py | 80 ++++++++++++++++++++++++++++++ requirements.txt | 3 +- tests/test_tts.py | 12 +++++ tests/test_twitch_script_class.py | 2 +- tts.py | 82 ++++++++++++++++++++++++++----- twitch_script_class.py | 24 +++++---- 6 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 tests/test_tts.py diff --git a/config.py b/config.py index 84c2c11..e4899e4 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,83 @@ +from enum import Enum + + +class Speaker(Enum): + GOOGLE_TEXT_TO_SPEECH = 1 + STREAMLABS_API = 2 + + +class FileNameStrategy(Enum): + TIME_BASED = 1 + CONTENT_BASED = 2 + + +class PollyVoices(Enum): + Aditi = "Aditi" + Amy = "Amy" + Astrid = "Astrid" + Bianca = "Bianca" + Brian = "Brian" + Camila = "Camila" + Carla = "Carla" + Carmen = "Carmen" + Celine = "Celine" + Chantal = "Chantal" + Conchita = "Conchita" + Cristiano = "Cristiano" + Dora = "Dora" + Emma = "Emma" + Enrique = "Enrique" + Ewa = "Ewa" + Filiz = "Filiz" + Geraint = "Geraint" + Giorgio = "Giorgio" + Gwyneth = "Gwyneth" + Hans = "Hans" + Ines = "Ines" + Ivy = "Ivy" + Jacek = "Jacek" + Jan = "Jan" + Joanna = "Joanna" + Joey = "Joey" + Justin = "Justin" + Karl = "Karl" + Kendra = "Kendra" + Kimberly = "Kimberly" + Lea = "Lea" + Liv = "Liv" + Lotte = "Lotte" + Lucia = "Lucia" + Lupe = "Lupe" + Mads = "Mads" + Maja = "Maja" + Marlene = "Marlene" + Mathieu = "Mathieu" + Matthew = "Matthew" + Maxim = "Maxim" + Mia = "Mia" + Miguel = "Miguel" + Mizuki = "Mizuki" + Naja = "Naja" + Nicole = "Nicole" + Penelope = "Penelope" + Raveena = "Raveena" + Ricardo = "Ricardo" + Ruben = "Ruben" + Russell = "Russell" + Salli = "Salli" + Seoyeon = "Seoyeon" + Takumi = "Takumi" + Tatyana = "Tatyana" + Vicki = "Vicki" + Vitoria = "Vitoria" + Zeina = "Zeina" + Zhiyu = "Zhiyu" + + botList = ("Nightbot", "StreamElements", "Moobot", "praxis_bot") slurList = ("fag", "faggot", "niga", "nigga", "nigger", "retard", "tard", "rtard", "coon") + +currentSpeaker = Speaker.STREAMLABS_API +fileNameStrategy = FileNameStrategy.CONTENT_BASED +streamlabsVoice = PollyVoices.Justin diff --git a/requirements.txt b/requirements.txt index d052841..0e8c8c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ SQLAlchemy pandas numpy gTTS -playsound \ No newline at end of file +playsound +pyglet \ No newline at end of file diff --git a/tests/test_tts.py b/tests/test_tts.py new file mode 100644 index 0000000..faf992e --- /dev/null +++ b/tests/test_tts.py @@ -0,0 +1,12 @@ +import unittest + +import tts + +class TTSTest(unittest.TestCase): + + def test_file_name(self): + tts.create_file_name("test", "mp3") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_twitch_script_class.py b/tests/test_twitch_script_class.py index eba485c..63a6f52 100644 --- a/tests/test_twitch_script_class.py +++ b/tests/test_twitch_script_class.py @@ -4,7 +4,7 @@ import twitch testValidUrls = ['https://shady.ru', 'http://stolencards.zn', 'https://i.imgur.com/FL6slHd.jpg'] -testInvalidUrls = ['this is just a sentence. With a period', 'gotta have some other stuff', 'bad punctuation produces false positives'] +testInvalidUrls = ['this is just a sentence. With a period', 'gotta have some other stuff', 'bad punctuation.does not produces false positives'] class TwitchBotTest(unittest.TestCase): diff --git a/tts.py b/tts.py index 7eff716..b4d3bf5 100644 --- a/tts.py +++ b/tts.py @@ -1,31 +1,89 @@ +import datetime +import hashlib import os -import datetime - +import requests +from gtts import gTTS from playsound import playsound +import config + +streamLabsUrl = "https://streamlabs.com/polly/speak" def tts(inputText: str, *args): - destPath = os.getcwd() + "\\tts\\" - time = datetime.datetime.now() - fileName: str = time.strftime("%m-%d-%Y_%H-%M-%S") + "_tts.mp3" + outpath = create_speech_file(inputText) + playsound(outpath) - if len(args) == 1: - fileName = args[0] + "_tts.mp3" - # tts = gTTS(text=inputText, lang='en') - # tts.save(destPath + fileName) +def create_speech_gtts(input_text: str): + path = os.path.join(get_tts_dir(), create_file_name(input_text, "mp3")) + if not os.path.exists(path): + sound_digest = gTTS(text=input_text, lang='en') + sound_digest.save(path) + return path - playsound(destPath + fileName) - # os.system(filename) +def create_speech_streamlabs(text: str): + path = os.path.join(get_tts_dir(), create_file_name(text, "ogg")) + if not os.path.exists(path): + body = {"voice": config.streamlabsVoice.value, "text": text} + resp = requests.post(streamLabsUrl, data=body).json() + sound_file_url = resp["speak_url"] + if sound_file_url is not None: + sound_bytes = requests.get(sound_file_url, stream=True) + f = open(path, "+wb") + f.write(sound_bytes.content) + f.close() + return path + + +speechCreationFunctions = { # this is a mapping of the Speaker enum to function pointers + config.Speaker.STREAMLABS_API: create_speech_streamlabs, + config.Speaker.GOOGLE_TEXT_TO_SPEECH: create_speech_gtts +} + + +def create_speech_file(text: str): + text_creation_function = speechCreationFunctions.get(config.currentSpeaker) + output_path = text_creation_function(text) + return output_path + + +def create_file_name(text: str, ext: str): + """ + :param text: the content of the message. using the CONTENT_BASED FileNameStrategy, this will (ostensibly) produce a + unique file name based on the content of the message. Two messages of equal content will produce the same name + :param ext: the desired file extension i.e. mp3, ogg, wav, etc... + :return: returns the formatted filename i.e. 01-01-20_01-01-01_tts.mp3 + """ + if config.fileNameStrategy == config.FileNameStrategy.CONTENT_BASED: + unique_id = hashlib.md5(bytes(text, 'utf-8')).hexdigest() + return "%s_tts.%s" % (unique_id, ext) + + elif config.fileNameStrategy == config.FileNameStrategy.CONTENT_BASED: + time = datetime.datetime.now() + return "%s_tts.%s" % (time.strftime("%m-%d-%Y_%H-%M-%S"), ext) + + else: + return "unconfigured_tts.%s" % ext def play_speech(fileName): - destPath = os.getcwd() + "\\tts\\" + destPath = get_tts_dir() playsound(destPath + fileName) +def get_tts_dir(): + """ + Checks for the tts directory, and will create it if it does not exist + :return: the relative file path of the tts dir + """ + dir = os.path.join(os.getcwd(), "tts") # this is platform-agnostic + if not os.path.exists(dir): + os.mkdir(dir) + return dir + + if __name__ == "__main__": print("Enter Text: ") textInput = str(input()) diff --git a/twitch_script_class.py b/twitch_script_class.py index 1590311..fe6cd5f 100644 --- a/twitch_script_class.py +++ b/twitch_script_class.py @@ -1,13 +1,13 @@ import random +import re + import twitch import twitch.chat -import twitch_cred as twitch_credentials import config as config -import tts - import db -import re +import tts +import twitch_cred as twitch_credentials class Twitch_Module(): @@ -19,7 +19,8 @@ class Twitch_Module(): self.links_allowed: bool = True self.whitelisted_users: list = ["thecuriousnerd", "theredpoint", "lakotor"] # don't freak out, this is *merely* a regex for matching urls that will hit just about everything - self._urlMatcher = re.compile("(https?:(/{1,3}|[a-z0-9%])|[a-z0-9.-]+[.](com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))") + self._urlMatcher = re.compile( + "(https?:(/{1,3}|[a-z0-9%])|[a-z0-9.-]+[.](com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))") def join_channel(self, channel_name): channel_name = "#" + channel_name @@ -81,14 +82,17 @@ class Twitch_Module(): print("{something went wrong}") def tts_message(self, message: twitch.chat.Message): - if self.contains_slur(message) == False: - if self.tts_enabled == True: - if message.text.startswith('!') == False: + if not self.contains_slur(message): + if self.tts_enabled: + if not message.text.startswith('!'): + text_to_say: str = "%s says, %s" % (message.sender, message.text) + channel_text = "%s user msg" % message.channel + if message.sender.lower() == message.channel: - tts.tts(message.sender + " says, " + message.text) + tts.tts(text_to_say) else: # tts.tts(message.sender + " says, " + message.text) - tts.tts(message.sender + " says, " + message.text, message.channel + " user msg") + tts.tts(text_to_say, channel_text) def contains_url(self, message: twitch.chat.Message): containsURL = re.search(self._urlMatcher, message.text.lower()) is not None