Compare commits
8 Commits
9937951e69
...
ce1e237d99
Author | SHA1 | Date |
---|---|---|
Derek | ce1e237d99 | |
Derek | ecb72a1fb2 | |
Derek | 1143111d7d | |
Derek | d8830f8834 | |
Derek | 1a39574cc8 | |
Derek | 9773afab10 | |
Derek | 8c9b45705e | |
Derek | c3b847bf78 |
1
Pipfile
1
Pipfile
|
@ -27,6 +27,7 @@ librosa = "*"
|
|||
pytsmod = "*"
|
||||
quart = "*"
|
||||
aioscheduler = "*"
|
||||
TTS = { git="https://github.com/coqui-ai/TTS.git" }
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
from .TTS import TextToSpeechPlugin as Plugin
|
Loading…
Reference in New Issue