Compare commits

..

6 commits

Author SHA1 Message Date
1a1dfc7d2a Fix module defined event ingest 2025-03-07 02:09:34 -05:00
65d527bdfa Fix some plugin requests -> httpx imports 2025-03-07 01:39:07 -05:00
e67b31daf8 Fix missing await 2025-03-07 01:38:19 -05:00
7ebf0b48a4 Move websocket bus to asyncio operation 2025-02-17 23:25:50 -05:00
7685170714 Move chats to asyncio operation 2025-02-17 22:18:57 -05:00
1bc693a4eb Remove peertube chat module
This feature was rejected upstream aaaages ago, and i dont maintain the 
fork with it in it anymore
2025-02-17 18:36:35 -05:00
11 changed files with 468 additions and 1629 deletions

2000
pdm.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,20 +8,21 @@ authors = [
dependencies = [ dependencies = [
"click", "click",
"kdl-py", "kdl-py",
"quart", "quart==0.18.*",
"werkzeug", "werkzeug==2.3.7",
"hypercorn", "hypercorn",
"websockets", "websockets==11.0.3",
"aioprocessing",
"aioscheduler", "aioscheduler",
"pyaudio", "pyaudio==0.2.*",
"librosa", "librosa==0.8.*",
"pytsmod", "pytsmod",
"numpy", "numpy",
"multipledispatch", "multipledispatch",
"blessed", "blessed",
"appdirs", "appdirs",
"maya", "maya",
"httpx", "httpx>=0.28.1",
] ]
requires-python = ">=3.10,<3.11" requires-python = ">=3.10,<3.11"
readme = "README.md" readme = "README.md"
@ -29,17 +30,18 @@ license = {text = "GPLv2"}
[project.optional-dependencies] [project.optional-dependencies]
tts = [ tts = [
"coqui-tts", "TTS==0.9.*",
"torch==1.13.*",
] ]
phrasecounter = ["num2words"] phrasecounter = ["num2words"]
jail = ["owoify-py"] jail = ["owoify-py==2.*"]
twitch = ["miniirc"] twitch = ["miniirc"]
midi = [ midi = [
"mido", "mido",
"python-rtmidi", "python-rtmidi",
] ]
obs = ["simpleobsws"] obs = ["simpleobsws"]
osc = ["python-osc"] osc = ["python-osc>=1.9.0"]
yt-dlp = ["yt-dlp"] yt-dlp = ["yt-dlp"]
[build-system] [build-system]

View file

@ -61,10 +61,6 @@ def cli(loglevel, show_time=False):
logging.getLogger('hypercorn.access').setLevel(logging.WARN) logging.getLogger('hypercorn.access').setLevel(logging.WARN)
logging.getLogger('httpx').setLevel(logging.WARN) logging.getLogger('httpx').setLevel(logging.WARN)
logging.getLogger('httpcore').setLevel(logging.INFO) logging.getLogger('httpcore').setLevel(logging.INFO)
logging.getLogger('torio._extension.utils').setLevel(logging.WARN)
logging.getLogger('matplotlib').setLevel(logging.INFO)
logging.getLogger('fsspec').setLevel(logging.INFO)
logging.getLogger('TTS').setLevel(logging.INFO if loglevel == logging.DEBUG else logging.WARN)
# Quiet warnings # Quiet warnings
if loglevel > logging.DEBUG: if loglevel > logging.DEBUG:
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")

View file

@ -8,6 +8,7 @@ import pyaudio as pya
import librosa import librosa
import pytsmod as tsm import pytsmod as tsm
import soundfile import soundfile
from aioprocessing import AioEvent
# HACK: Redirect stderr to /dev/null to silence portaudio boot # HACK: Redirect stderr to /dev/null to silence portaudio boot
devnull = os.open(os.devnull, os.O_WRONLY) devnull = os.open(os.devnull, os.O_WRONLY)
@ -52,9 +53,9 @@ class Clip:
def stretch(self, speed, keep_pitch=True): def stretch(self, speed, keep_pitch=True):
if keep_pitch: if keep_pitch:
stretched = tsm.wsola(self._stereo_transpose(self.raw), 1 / speed) stretched = tsm.wsola(self._stereo_transpose(self.raw), speed)
else: else:
stretched = librosa.resample(self._stereo_transpose(self.raw), self.samplerate * speed, self.samplerate, fix=False, scale=True) stretched = librosa.resample(self._stereo_transpose(self.raw), self.samplerate * (1 / speed), self.samplerate, fix=False, scale=True)
self.raw = np.ascontiguousarray(self._stereo_transpose(stretched), dtype='float32') self.raw = np.ascontiguousarray(self._stereo_transpose(stretched), dtype='float32')
def save(self, filename): def save(self, filename):
@ -66,8 +67,7 @@ class Stream:
self.clip = clip self.clip = clip
self.pos = 0 self.pos = 0
self.playing = False self.playing = False
self.loop = asyncio.get_event_loop() self._end_event = AioEvent()
self._end_event = asyncio.Event()
self._stream = pyaudio.open( self._stream = pyaudio.open(
output_device_index=output_index, output_device_index=output_index,
format=pya.paFloat32, format=pya.paFloat32,
@ -85,11 +85,16 @@ class Stream:
if not self._stream.is_active(): if not self._stream.is_active():
self._stream.start_stream() self._stream.start_stream()
async def play(self): def play(self):
self._end_event.clear()
self._play()
self._end_event.wait(timeout=self.clip.length)
async def aplay(self):
self._end_event.clear() self._end_event.clear()
self._play() self._play()
try: try:
await self._end_event.wait() await self._end_event.coro_wait(timeout=self.clip.length)
except asyncio.CancelledError: except asyncio.CancelledError:
self.playing = False self.playing = False
self._stream.stop_stream() self._stream.stop_stream()
@ -112,7 +117,7 @@ class Stream:
if self.pos >= self.clip.raw.shape[0]: if self.pos >= self.clip.raw.shape[0]:
self.playing = False self.playing = False
self.loop.call_soon_threadsafe(self._end_event.set) self._end_event.set()
return buffer, pya.paContinue return buffer, pya.paContinue

View file

@ -139,7 +139,7 @@ def parse_kdl_deep(path, relativeto=None):
if relativeto: if relativeto:
path = os.path.normpath(os.path.join(relativeto, path)) path = os.path.normpath(os.path.join(relativeto, path))
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r') as f:
try: try:
config = kdl.parse(f.read(), kdl_parse_config) config = kdl.parse(f.read(), kdl_parse_config)
for node in config.nodes: for node in config.nodes:

View file

@ -3,6 +3,7 @@ import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
import os import os
import os.path
import pathlib import pathlib
import sys import sys
import signal import signal
@ -66,9 +67,6 @@ class MainProcess:
# Save sys.path since some config will clobber it # Save sys.path since some config will clobber it
self._initial_syspath = sys.path self._initial_syspath = sys.path
if os.name == 'nt':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
def _unload_plugin(self, plugin_name): def _unload_plugin(self, plugin_name):
plugin = self.plugins[plugin_name] plugin = self.plugins[plugin_name]
plugin.close() plugin.close()
@ -246,15 +244,12 @@ class MainProcess:
sys.path = self._initial_syspath sys.path = self._initial_syspath
async def _discount_repl(self): async def _discount_repl(self):
loop = asyncio.get_event_loop()
# REVIEW: Not a good UX at the moment (as new logs clobber the terminal entry) # REVIEW: Not a good UX at the moment (as new logs clobber the terminal entry)
while True: async for line in reader:
line = await loop.run_in_executor(None, sys.stdin.readline)
line = line.strip() line = line.strip()
logger.debug(f'Got terminal input: {line}') if line == b'reload':
if line == 'reload':
self.reload_ev.set() self.reload_ev.set()
elif line == 'quit': elif line == b'quit':
self.shutdown_ev.set() self.shutdown_ev.set()
async def run(self): async def run(self):
@ -266,6 +261,8 @@ class MainProcess:
try: try:
# System setup # System setup
## Make stdin handler ## Make stdin handler
reader = asyncio.StreamReader()
await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(reader), sys.stdin)
self.cli_task = loop.create_task(self._discount_repl()) self.cli_task = loop.create_task(self._discount_repl())
## Init websocket server (external end of the event bus) ## Init websocket server (external end of the event bus)
self.bus_server = WebsocketServerProcess(self.event_queue, *self.bus_conf) self.bus_server = WebsocketServerProcess(self.event_queue, *self.bus_conf)

View file

@ -29,17 +29,6 @@ class OvtkBlueprint(quart.Blueprint):
endpoint = self.name + endpoint endpoint = self.name + endpoint
return quart.url_for(endpoint, *args, **kwargs) return quart.url_for(endpoint, *args, **kwargs)
def render(self, name, **kwargs):
"""render_template that prefers the plugin-specific templates"""
full = self.template_folder / name
if os.path.exists(full):
template_string = None
with open(full, 'r') as template_file:
template_string = template_file.read()
return quart.render_template_string(template_string, **kwargs)
else:
return quart.render_template(name, **kwargs)
class PluginBase(ABC): class PluginBase(ABC):
plugins = {} plugins = {}

View file

@ -54,9 +54,9 @@ class AudioAlert(PluginBase):
if wait: if wait:
await stream.play() await stream.aplay()
else: else:
task = asyncio.create_task(stream.play()) task = asyncio.create_task(stream.aplay())
task.add_done_callback(self.tasks.discard) task.add_done_callback(self.tasks.discard)
self.tasks.add(task) self.tasks.add(task)

View file

@ -52,8 +52,8 @@ class TextToSpeechPlugin(PluginBase):
config_path = override_conf_path config_path = override_conf_path
self.synthesizer = Synthesizer( self.synthesizer = Synthesizer(
tts_checkpoint=model_path, model_path,
tts_config_path=config_path, config_path,
vocoder_checkpoint=vocoder_path, vocoder_checkpoint=vocoder_path,
vocoder_config=vocoder_config_path, vocoder_config=vocoder_config_path,
use_cuda=self.cuda, use_cuda=self.cuda,
@ -66,26 +66,22 @@ class TextToSpeechPlugin(PluginBase):
task.cancel() task.cancel()
def make_tts_wav(self, text, filename=None): def make_tts_wav(self, text, filename=None):
# Force punctuation (keeps the models from acting unpredictably)
text = text.strip()
if not any([text.endswith(punc) for punc in '.!?:']):
text += '.'
if filename is None: if filename is None:
filename = os.path.join(self.cache_dir, f'{uuid.uuid1()}.wav') filename = os.path.join(self.cache_dir, f'{uuid.uuid1()}.wav')
self.logger.info(f'Generating TTS "{text}"...')
if self.speaker_wav: if self.speaker_wav:
wav = self.synthesizer.tts(text, None, 'en', self.speaker_wav) wav = self.synthesizer.tts(text, None, 'en', self.speaker_wav)
else: else:
wav = self.synthesizer.tts(text) wav = self.synthesizer.tts(text)
self.synthesizer.save_wav(wav, filename) self.synthesizer.save_wav(wav, filename)
self.logger.info(f'Done - saved as {filename}')
return filename return filename
async def run(self, text, *args, _ctx={}, wait=False, **kwargs): async def run(self, text, *args, _ctx={}, wait=False, **kwargs):
try: try:
# Force punctuation (keep AI from spinning off into random noises)
if not any([text.endswith(punc) for punc in '.!?:']):
text += '.'
# Do TTS processing in a thread to avoid blocking main loop # Do TTS processing in a thread to avoid blocking main loop
filename = await asyncio.get_running_loop().run_in_executor(None, self.make_tts_wav, text) filename = await asyncio.get_running_loop().run_in_executor(None, self.make_tts_wav, text)
@ -94,7 +90,7 @@ class TextToSpeechPlugin(PluginBase):
stream = Stream(clip, self.output_index) stream = Stream(clip, self.output_index)
async def play(): async def play():
try: try:
await stream.play() await stream.aplay()
finally: finally:
stream.close() stream.close()
os.remove(os.path.join(self.cache_dir, filename)) os.remove(os.path.join(self.cache_dir, filename))

View file

@ -34,7 +34,7 @@ class ScenePlugin(PluginBase):
self.blueprint.add_url_rule('/', 'ctrlpanel', self.ui_ctrlpanel) self.blueprint.add_url_rule('/', 'ctrlpanel', self.ui_ctrlpanel)
self.blueprint.add_url_rule('/<name>/<cmd>', 'api-sceneset', self.ui_setscene) self.blueprint.add_url_rule('/<name>/<cmd>', 'api-sceneset', self.ui_setscene)
self.blueprint.add_url_rule('/monitor', 'monitor', self.ui_monitor_ws, websocket=True) self.blueprint.add_url_rule('/monitor', 'monitor', self.ui_monitor_ws, is_websocket=True)
async def run(self, name, _children=None, _ctx={}, active=None, group=None, oneshot=False, **kwargs): async def run(self, name, _children=None, _ctx={}, active=None, group=None, oneshot=False, **kwargs):
if _children is None: if _children is None:
@ -138,7 +138,7 @@ class ScenePlugin(PluginBase):
async def ui_ctrlpanel(self): async def ui_ctrlpanel(self):
groups = self._get_state() groups = self._get_state()
return await self.blueprint.render('index.html', init_state=json.dumps(groups)) return await quart.render_template('index.html', init_state=json.dumps(groups))
async def ui_setscene(self, name=None, cmd=None): async def ui_setscene(self, name=None, cmd=None):
active = cmd == 'activate' active = cmd == 'activate'

View file

@ -2,7 +2,7 @@
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Scene control</title> <title>Test page</title>
<script type="importmap"> <script type="importmap">
{ {
"imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" } "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" }