Compare commits

...

8 Commits

Author SHA1 Message Date
Derek ce1e237d99 Minor fixes
The quart one is sorta wip but whatever
2022-07-10 06:06:55 -04:00
Derek ecb72a1fb2 [Core] Remove broken watchdog #WIP 2022-06-12 20:25:07 -04:00
Derek 1143111d7d [Core] Fix UI webserver borking the exit condition 2022-06-12 20:24:03 -04:00
Derek d8830f8834 Create tts dir if it doesnt exist 2022-06-12 16:26:08 -04:00
Derek 1a39574cc8 Better file management 2022-06-12 15:48:51 -04:00
Derek 9773afab10 Proper built-in TTS!!!
Being able to load the model once makes things wayyyy faster
2022-06-12 15:41:22 -04:00
Derek 8c9b45705e Silly temp tts cuda patch! 2022-06-12 11:58:16 -04:00
Derek c3b847bf78 Silly temp tts (using external pipenv until coqui 3.10 compat) 2022-04-30 03:14:13 -04:00
7 changed files with 922 additions and 195 deletions

View File

@ -27,6 +27,7 @@ librosa = "*"
pytsmod = "*"
quart = "*"
aioscheduler = "*"
TTS = { git="https://github.com/coqui-ai/TTS.git" }
[requires]
python_version = "3.10"

981
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,7 @@ class TwitchIRC:
self.users = shared.users
self._reply_buffer_maxlen = 1000
self._reply_buffer = OrderedDict()
self._group_gifts = {}
def connect(self):
self._ws = NonBlockingWebsocket(WEBSOCKET_ADDRESS)

View File

@ -1,41 +1,17 @@
from multiprocessing import Event
import click
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from ovtk_audiencekit.core import MainProcess
from .group import cli
class ConfigChangeHandler(FileSystemEventHandler):
def __init__(self, reload_event, *args, **kwargs):
super().__init__(*args, **kwargs)
self.reload_event = reload_event
def on_modified(self, fs_event):
self.reload_event.set()
@cli.command()
@click.argument('config_file', type=click.Path('r'), default='config.kdl')
@click.option('--port', default='8080')
@click.option('--bind', default='127.0.0.1')
@click.option('--watch/--no-watch', default=True, help="Automatically reload on config changes")
def start(config_file, watch=True, port=None, bind=None):
def start(config_file, port=None, bind=None):
"""Start audiencekit server"""
reload_event = Event()
if watch:
handler = ConfigChangeHandler(reload_event)
observer_thread = Observer()
observer_thread.schedule(handler, config_file)
observer_thread.start()
main = MainProcess(config_file, reload_event, port, bind)
main = MainProcess(config_file, port, bind)
main.start()
main.join()
if watch:
observer_thread.stop()
observer_thread.join()

View File

@ -36,10 +36,9 @@ def parse_kdl_deep(path, relativeto=None):
class MainProcess(Process):
def __init__(self, config_path, reload_event, port, bind):
def __init__(self, config_path, port, bind):
super().__init__()
self.config_path = config_path
self.reload_event = reload_event
self.port = port
self.bind = bind
@ -55,7 +54,6 @@ class MainProcess(Process):
raise ValueError(f"Multiple nodes named {instance_name}, please specify unique names as the second argument")
else:
raise ValueError(f"Multiple definitions of {instance_name} exist, please specify unique names as the second argument")
return module_name, instance_name
async def handle_events(self):
@ -172,14 +170,23 @@ class MainProcess(Process):
# Do initial setup
self.setup()
self._skehdule = TimedScheduler()
# HACK: what the fuck. there has got to be a better way to write that
loop = asyncio.get_event_loop()
plugin_tick_task = loop.create_task(self.tick_plugins())
chat_event_task = loop.create_task(self.handle_events())
async def start_scheduler():
self._skehdule.start()
asyncio.get_event_loop().create_task(start_scheduler())
skehduler_task = loop.create_task(start_scheduler())
asyncio.get_event_loop().create_task(self.tick_plugins())
asyncio.get_event_loop().create_task(self.handle_events())
asyncio.get_event_loop().create_task(self.webserver.run_task())
async def start_uiwebserver():
try:
# HACK: eats the KeyboardInterrupt - maybe others too
await self.webserver.run_task(use_reloader=False)
finally:
raise ValueError('Quart webserver maybe stole KeyboardInterrupt or maybe is funky fresh :shrug:')
ui_task = loop.create_task(start_uiwebserver())
event_ready = asyncio.Event()
def get_event(pipe):
@ -187,15 +194,19 @@ class MainProcess(Process):
self.event_queue.put_nowait(event)
for pipe in self.pipes:
# REVIEW: This does not work on windows!!!!
asyncio.get_event_loop().add_reader(pipe.fileno(), lambda pipe=pipe: get_event(pipe))
loop.add_reader(pipe.fileno(), lambda pipe=pipe: get_event(pipe))
asyncio.get_event_loop().run_forever()
loop.run_forever()
except KeyboardInterrupt:
pass
except Exception as e:
logger.critical(f'Failure in core process - {e}')
logger.debug(''.join(format_exception(None, e, e.__traceback__)))
finally:
skehduler_task.cancel()
plugin_tick_task.cancel()
chat_event_task.cancel()
ui_task.cancel()
for process in self.chat_processes.values():
process.control_pipe.send(ShutdownRequest('root'))
process.join(5)

View File

@ -0,0 +1,74 @@
import uuid
import os
from TTS.utils.synthesizer import Synthesizer
from TTS.utils.manage import ModelManager
from TTS.config import load_config
from ovtk_audiencekit.plugins import PluginBase
from ovtk_audiencekit.plugins.AudioAlert import Clip
from ovtk_audiencekit.events import Message, SysMessage
from ovtk_audiencekit.core.Config import CACHE_DIR
class TextToSpeechPlugin(PluginBase):
def __init__(self, *args, output=None, cuda=None, engine="tts_models/en/ljspeech/tacotron2-DDC", _children=None, **kwargs):
super().__init__(*args, _children=_children)
self._output_index = Clip.find_output_index(output)
conf_overrides = {k[2:]: v for k, v in kwargs.items() if k.startswith('o_')}
self._cache = os.path.join(CACHE_DIR, 'tts')
os.makedirs(os.path.dirname(self._cache), exist_ok=True)
if isinstance(cuda, int):
self.cuda = cuda
elif cuda == True:
self.cuda = 0
else:
self.cuda = None
manager = ModelManager(output_prefix=CACHE_DIR) # HACK: coqui automatically adds 'tts' subdir
model_path, config_path, model_item = manager.download_model(engine)
vocoder_path, vocoder_config_path, _ = manager.download_model(model_item["default_vocoder"])
if conf_overrides:
override_conf_path = os.path.join(self._cache, f'{self._name}_override.json')
config = load_config(config_path)
for key, value in conf_overrides.items():
config[key] = value
config.save_json(override_conf_path)
config_path = override_conf_path
self.synthesizer = Synthesizer(
model_path,
config_path,
None, # speakers_file_path
None, # language_ids_file_path
vocoder_path,
vocoder_config_path,
None, # encoder_path
None, # encoder_config_path
self.cuda is not None,
)
def run(self, text, *args, _ctx={}, **kwargs):
super().run(*args, **kwargs)
filename = os.path.join(self._cache, f'{uuid.uuid1()}.wav')
try:
wav = self.synthesizer.tts(text)
# TODO: Play direct from memory
self.synthesizer.save_wav(wav, filename)
clip = Clip(filename, self._output_index)
clip.play()
except Exception as e:
if source_event := _ctx.get('event'):
msg = SysMessage(self._name, 'Failed to make speech from input!!')
if isinstance(source_event, Message):
msg.replies_to = source_event
self.chats[source_event.via].send(msg)

View File

@ -0,0 +1 @@
from .TTS import TextToSpeechPlugin as Plugin