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 = "*"
|
pytsmod = "*"
|
||||||
quart = "*"
|
quart = "*"
|
||||||
aioscheduler = "*"
|
aioscheduler = "*"
|
||||||
|
TTS = { git="https://github.com/coqui-ai/TTS.git" }
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.10"
|
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.users = shared.users
|
||||||
self._reply_buffer_maxlen = 1000
|
self._reply_buffer_maxlen = 1000
|
||||||
self._reply_buffer = OrderedDict()
|
self._reply_buffer = OrderedDict()
|
||||||
|
self._group_gifts = {}
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self._ws = NonBlockingWebsocket(WEBSOCKET_ADDRESS)
|
self._ws = NonBlockingWebsocket(WEBSOCKET_ADDRESS)
|
||||||
|
|
|
@ -1,41 +1,17 @@
|
||||||
from multiprocessing import Event
|
from multiprocessing import Event
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
from ovtk_audiencekit.core import MainProcess
|
from ovtk_audiencekit.core import MainProcess
|
||||||
|
|
||||||
from .group import cli
|
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()
|
@cli.command()
|
||||||
@click.argument('config_file', type=click.Path('r'), default='config.kdl')
|
@click.argument('config_file', type=click.Path('r'), default='config.kdl')
|
||||||
@click.option('--port', default='8080')
|
@click.option('--port', default='8080')
|
||||||
@click.option('--bind', default='127.0.0.1')
|
@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, port=None, bind=None):
|
||||||
def start(config_file, watch=True, port=None, bind=None):
|
|
||||||
"""Start audiencekit server"""
|
"""Start audiencekit server"""
|
||||||
reload_event = Event()
|
main = MainProcess(config_file, port, bind)
|
||||||
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.start()
|
main.start()
|
||||||
main.join()
|
main.join()
|
||||||
|
|
||||||
if watch:
|
|
||||||
observer_thread.stop()
|
|
||||||
observer_thread.join()
|
|
||||||
|
|
|
@ -36,10 +36,9 @@ def parse_kdl_deep(path, relativeto=None):
|
||||||
|
|
||||||
|
|
||||||
class MainProcess(Process):
|
class MainProcess(Process):
|
||||||
def __init__(self, config_path, reload_event, port, bind):
|
def __init__(self, config_path, port, bind):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
self.reload_event = reload_event
|
|
||||||
self.port = port
|
self.port = port
|
||||||
self.bind = bind
|
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")
|
raise ValueError(f"Multiple nodes named {instance_name}, please specify unique names as the second argument")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Multiple definitions of {instance_name} exist, please specify unique names as the second argument")
|
raise ValueError(f"Multiple definitions of {instance_name} exist, please specify unique names as the second argument")
|
||||||
|
|
||||||
return module_name, instance_name
|
return module_name, instance_name
|
||||||
|
|
||||||
async def handle_events(self):
|
async def handle_events(self):
|
||||||
|
@ -172,14 +170,23 @@ class MainProcess(Process):
|
||||||
# Do initial setup
|
# Do initial setup
|
||||||
self.setup()
|
self.setup()
|
||||||
self._skehdule = TimedScheduler()
|
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():
|
async def start_scheduler():
|
||||||
self._skehdule.start()
|
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())
|
async def start_uiwebserver():
|
||||||
asyncio.get_event_loop().create_task(self.handle_events())
|
try:
|
||||||
asyncio.get_event_loop().create_task(self.webserver.run_task())
|
# 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()
|
event_ready = asyncio.Event()
|
||||||
def get_event(pipe):
|
def get_event(pipe):
|
||||||
|
@ -187,15 +194,19 @@ class MainProcess(Process):
|
||||||
self.event_queue.put_nowait(event)
|
self.event_queue.put_nowait(event)
|
||||||
for pipe in self.pipes:
|
for pipe in self.pipes:
|
||||||
# REVIEW: This does not work on windows!!!!
|
# 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:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(f'Failure in core process - {e}')
|
logger.critical(f'Failure in core process - {e}')
|
||||||
logger.debug(''.join(format_exception(None, e, e.__traceback__)))
|
logger.debug(''.join(format_exception(None, e, e.__traceback__)))
|
||||||
finally:
|
finally:
|
||||||
|
skehduler_task.cancel()
|
||||||
|
plugin_tick_task.cancel()
|
||||||
|
chat_event_task.cancel()
|
||||||
|
ui_task.cancel()
|
||||||
for process in self.chat_processes.values():
|
for process in self.chat_processes.values():
|
||||||
process.control_pipe.send(ShutdownRequest('root'))
|
process.control_pipe.send(ShutdownRequest('root'))
|
||||||
process.join(5)
|
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