Compare commits
13 commits
main
...
feat/revit
Author | SHA1 | Date | |
---|---|---|---|
9f33ad634e | |||
3b6a6df0eb | |||
cd636e47d4 | |||
bea110b99e | |||
4521250ac1 | |||
439d9943ab | |||
72dbd2a232 | |||
eabcba5d16 | |||
|
9311245ef0 | ||
|
476552bbf5 | ||
ccb86d4c02 | |||
75e9c3dcc2 | |||
d4597620fe |
39 changed files with 854 additions and 997 deletions
|
@ -27,7 +27,7 @@ trigger event="Raid" {
|
|||
## For developers
|
||||
Extending audiencekit's automation abilities is made as simple as possible. For example, a plugin that automatically changes "simp" to "shrimp" in every message (as seen by those consuming this project's output) is simply:
|
||||
```python
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import Message
|
||||
|
||||
class Plugin(PluginBase):
|
||||
|
|
103
config.kdl
103
config.kdl
|
@ -1,7 +1,7 @@
|
|||
/* Comments surrounded by asterisks and slashes (like this one) are instructions,
|
||||
comments staring with two slashes are example configuration! */
|
||||
comments staring with two slashes are valid example configuration! */
|
||||
|
||||
/* Step 1: Add your credentials */
|
||||
/* Step 1: Give it the rights (to party) */
|
||||
secrets {
|
||||
/* Generate credentials via https://ovtk.skeh.site/twitch/auth
|
||||
and paste them between the curly braces below */
|
||||
|
@ -17,46 +17,52 @@ secrets {
|
|||
}
|
||||
}
|
||||
|
||||
/* Step 2: Import modules
|
||||
|
||||
/* Step 2: Import what you need
|
||||
|
||||
There are two types of modules, chats and plugins:
|
||||
+ Chats are self-explanitory: the event providers - livestream services.
|
||||
+ Plugins are the heart of the system, and can both monitor livestream events
|
||||
and be called by name in your config files to perform actions.
|
||||
and be called on directly to perform actions.
|
||||
|
||||
Some plugins (called "builtins" and always starting with a lowercase character)
|
||||
are always available, but others are not loaded until you ask to conserve
|
||||
resources. Chats are never loaded by default.
|
||||
Some plugins (called "builtins") are always available, but others are not loaded
|
||||
until you ask. Chats are never loaded by default.
|
||||
|
||||
You can load either by using the "chat" or "plugin" node respectively,
|
||||
and supplying the name of the module. You can also rename them by seperating
|
||||
the module name and the new name with a colon (:)
|
||||
the desired name and node name with a colon (:), otherwise they take on the
|
||||
module name and throw an error when two are used at the same time.
|
||||
*/
|
||||
// chat "Twitch" channel_name="MyTwitchChannel"
|
||||
// chat "Twitch:otherguy" channel_name="SomeOtherChannel"
|
||||
// plugin "AudioAlert" output="ALSA:default"
|
||||
// guest:chat "Twitch" channel_name="CollabChannel" readonly=true
|
||||
// aplay:plugin "AudioAlert" output="ALSA:default"
|
||||
|
||||
|
||||
/* Step 3: Kick it
|
||||
/* Step 3: Get silly with it
|
||||
|
||||
Some example automations are provided below to get you started.
|
||||
See the wiki for more details: https://git.skeh.site/skeh/ovtk_audiencekit/wiki
|
||||
Config is always valid KDL (https://kdl.dev/), but has some special quirks.
|
||||
See the wiki for more details, but the jist is:
|
||||
+ A plugin's main routine is run by making a node with its name
|
||||
+ Subroutines can be run by seperating with a dot: `name.routine`
|
||||
+ Returned values can be saved to the "context" (see below) by using a colon: `val:name`
|
||||
+ Config is evaluated top to bottom, outter-most runs by default
|
||||
+ Plugins are free to parse their children (inside braces) however they like, so not all nodes are the same! See `command` and `scene`.
|
||||
+ Custom type `t` (for template) can be used to insert data from the context, builtin `set` can be used to write to it.
|
||||
+ Plugins often use this to share additional data!
|
||||
*/
|
||||
|
||||
/* Self-promo every 10 min */
|
||||
// cue {
|
||||
// every hours=2 {
|
||||
// reply "Like the setup? Run it yourself! https://git.skeh.site/explore/repos?q=ovtk&topic=1"
|
||||
// }
|
||||
/* Self-promo every 2 hours */
|
||||
// cue hours=2 {
|
||||
// reply "Like the setup? Run it yourself! https://git.skeh.site/explore/repos?q=ovtk&topic=1"
|
||||
// }
|
||||
|
||||
|
||||
/* Call an existing shoutout bot on raid */
|
||||
// trigger event="Raid" {
|
||||
// reply (arg)"!so {event.from_channel}"
|
||||
// reply (t)"!so {event.from_channel}"
|
||||
// }
|
||||
|
||||
/* Lurk command */
|
||||
/* Lurk command (!lurk) */
|
||||
// command "lurk" help="Lurke modeo" display=true {
|
||||
// do {
|
||||
// reply "The Twitch algorithm thanks you for the lurk~" display=true
|
||||
|
@ -65,16 +71,57 @@ secrets {
|
|||
|
||||
/* TTS for every donation event */
|
||||
// plugin "TTS"
|
||||
// trigger monitization="0.01-" source="self" {
|
||||
// TTS (arg)"{event.user_name} says: {event.text}"
|
||||
// trigger monitization="0.01-" {
|
||||
// TTS (t)"{event.user_name} says: {event.text}"
|
||||
// }
|
||||
|
||||
/* Voice effect channel redeem (via midi to physical effect rack / DAW) */
|
||||
/* Control OBS from your midi controller - see https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests */
|
||||
// plugin "OBS" password="A Very Secret Example Password"
|
||||
// plugin "Midi"
|
||||
// Midi.listen "control_change" channel=1 control=1 value=127 {
|
||||
// OBS "SetInputMute" inputName="Microphone" inputMuted=true
|
||||
// }
|
||||
// Midi.listen "control_change" channel=1 control=1 value=0 {
|
||||
// OBS "SetInputMute" inputName="Microphone" inputMuted=false
|
||||
// }
|
||||
|
||||
/* Talk to your midi rig too! */
|
||||
// trigger event="ChannelPointRedemption" action="Sound Sussy" {
|
||||
// midi "sysex" data="FunEffect;1"
|
||||
// cue {
|
||||
// after minutes=5 {
|
||||
// midi "sysex" data="FunEffect;0"
|
||||
// }
|
||||
// Midi "program_change" channel=1 program=1
|
||||
// cue minutes=5 {
|
||||
// Midi "program_change" channel=1 program=0
|
||||
// }
|
||||
// }
|
||||
|
||||
/* Control just about anything else, from anywhere (http://localhost:8000/scene/) else! */
|
||||
// plugin "OSC" port=9000
|
||||
// plugin "OBS" password="A Very Secret Example Password"
|
||||
// scene "Brb / starting" {
|
||||
// OBS "SetCurrentProgramScene" sceneName="Starting"
|
||||
// OSC "/track/1/mute" (u8)1
|
||||
//
|
||||
// exit {
|
||||
// OSC "/track/1/mute" (u8)0
|
||||
// }
|
||||
// }
|
||||
// scene "Live" {
|
||||
// OBS "SetCurrentProgramScene" sceneName="Live"
|
||||
// }
|
||||
|
||||
|
||||
/* Step 4: Organize
|
||||
|
||||
Split config into multiple files to keep sane, or even define entirely seperate
|
||||
setups for special occasions and run by providing their path to the `start` command.
|
||||
*/
|
||||
// import "base.kdl"
|
||||
// import "secrets_i_promise_not_to_open_on_air.kdl"
|
||||
|
||||
|
||||
/* Step 5: Reach for the stars!
|
||||
|
||||
Still can't make that stupid idea a reality? Custom plugins are a single file in "src/ovtk_audiencekit/plugins" away~
|
||||
Learn a little Python, hack on any of the other plugins,
|
||||
toss PluginBase.py at ChatGPT and try your luck,
|
||||
or nicely ask your local birdy <3
|
||||
*/
|
||||
|
|
|
@ -9,9 +9,10 @@ dependencies = [
|
|||
"click",
|
||||
"kdl-py",
|
||||
"quart==0.18.*",
|
||||
"werkzeug==2.3.7",
|
||||
"hypercorn",
|
||||
"requests",
|
||||
"websockets",
|
||||
"websockets==11.0.3",
|
||||
"aioprocessing",
|
||||
"aioscheduler",
|
||||
"pyaudio==0.2.*",
|
||||
|
@ -22,10 +23,6 @@ dependencies = [
|
|||
"blessed",
|
||||
"appdirs",
|
||||
"maya",
|
||||
"mido",
|
||||
"python-rtmidi",
|
||||
"simpleobsws",
|
||||
"python-osc>=1.9.0",
|
||||
]
|
||||
requires-python = ">=3.10,<3.11"
|
||||
readme = "README.md"
|
||||
|
@ -39,11 +36,12 @@ tts = [
|
|||
phrasecounter = ["num2words"]
|
||||
jail = ["owoify-py==2.*"]
|
||||
twitch = ["miniirc"]
|
||||
|
||||
[tool.pdm.dev-dependencies]
|
||||
dev = [
|
||||
"pipenv-setup",
|
||||
midi = [
|
||||
"mido",
|
||||
"python-rtmidi",
|
||||
]
|
||||
obs = ["simpleobsws"]
|
||||
osc = ["python-osc>=1.9.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-backend"]
|
||||
|
@ -52,6 +50,7 @@ build-backend = "pdm.backend"
|
|||
[tool.pdm.scripts]
|
||||
start = "python audiencekit.py start"
|
||||
ws = "python audiencekit.py ws"
|
||||
debug = "python audiencekit.py --debug"
|
||||
|
||||
[tool.pdm]
|
||||
[[tool.pdm.source]]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
@ -15,12 +15,14 @@ logger = logging.getLogger(__name__)
|
|||
@click.option('--bus-port', default='8080')
|
||||
@click.option('--web-bind', default='localhost')
|
||||
@click.option('--web-port', default='8000')
|
||||
def start(config_file, bus_bind=None, bus_port=None, web_bind=None, web_port=None):
|
||||
@click.option('--parallel', default=5)
|
||||
def start(config_file, bus_bind=None, bus_port=None, web_bind=None, web_port=None, parallel=None):
|
||||
"""Start audiencekit server"""
|
||||
logger.info('Hewwo!!')
|
||||
main = MainProcess(config_file,
|
||||
bus_conf=(bus_bind, bus_port),
|
||||
web_conf=(web_bind, web_port))
|
||||
web_conf=(web_bind, web_port),
|
||||
max_concurrent=parallel)
|
||||
try:
|
||||
asyncio.run(main.run())
|
||||
except KeyboardInterrupt:
|
||||
|
|
|
@ -76,10 +76,11 @@ class EventCommandFactory(click.MultiCommand):
|
|||
param = click.Option(param_decls=[f'--{field.name}'], type=param_type, default=field.default, show_default=True)
|
||||
params.append(param)
|
||||
|
||||
if target_event.__dict__.get('__cli__'):
|
||||
params.extend(target_event.__cli__())
|
||||
|
||||
command = click.Command(name, callback=mkevent_generic, params=params, help=target_event.__doc__)
|
||||
|
||||
if target_event.__dict__.get('_cli'):
|
||||
command = target_event._cli(command)
|
||||
|
||||
return command
|
||||
|
||||
|
||||
|
|
|
@ -17,11 +17,12 @@ import hypercorn
|
|||
import hypercorn.asyncio
|
||||
import hypercorn.logging
|
||||
|
||||
from ovtk_audiencekit.core import WebsocketServerProcess
|
||||
from ovtk_audiencekit.core.Config import parse_kdl_deep, kdl_reserved, compute_dynamic
|
||||
from ovtk_audiencekit.core.Plugins import PluginError
|
||||
from ovtk_audiencekit.core.WebsocketServerProcess import WebsocketServerProcess
|
||||
from ovtk_audiencekit.events import Event, Delete
|
||||
from ovtk_audiencekit.chats.ChatProcess import ShutdownRequest
|
||||
from ovtk_audiencekit.plugins import builtins, PluginError
|
||||
from ovtk_audiencekit.plugins import builtins
|
||||
from ovtk_audiencekit.utils import format_exception
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -52,11 +53,12 @@ def import_or_reload_mod(module_name, default_package=None, external=False):
|
|||
|
||||
|
||||
class MainProcess:
|
||||
def __init__(self, config_path, bus_conf=(None, None), web_conf=(None, None)):
|
||||
def __init__(self, config_path, bus_conf, web_conf, max_concurrent=5):
|
||||
self._running = False
|
||||
self.config_path = config_path
|
||||
self.bus_conf = bus_conf
|
||||
self.web_conf = web_conf
|
||||
self.max_concurrent = max_concurrent
|
||||
|
||||
self.chat_processes = {}
|
||||
self.plugins = {}
|
||||
|
@ -129,7 +131,6 @@ class MainProcess:
|
|||
if e.fatal:
|
||||
self._unload_plugin(e.source)
|
||||
except Exception as e:
|
||||
self._plugin_error
|
||||
logger.critical(f'Failure when processing {plugin_name} ({e}) - disabling...')
|
||||
logger.debug(format_exception(e))
|
||||
self._unload_plugin(plugin_name)
|
||||
|
@ -237,7 +238,7 @@ class MainProcess:
|
|||
if plugin_module is None:
|
||||
logger.error(f'Unknown plugin: {node.name}')
|
||||
else:
|
||||
await plugin_module._call(node.sub, node.tag, *node.args, **node.props, _ctx=global_ctx, _children=node.nodes)
|
||||
await plugin_module._kdl_call(node, global_ctx)
|
||||
|
||||
async def user_shutdown(self):
|
||||
for process_name, process in list(reversed(self.chat_processes.items())):
|
||||
|
@ -293,7 +294,8 @@ class MainProcess:
|
|||
await self.user_setup()
|
||||
# Start plumbing tasks
|
||||
user_tasks.add(loop.create_task(self.tick_plugins()))
|
||||
user_tasks.add(loop.create_task(self.handle_events()))
|
||||
for i in range(0, self.max_concurrent):
|
||||
user_tasks.add(loop.create_task(self.handle_events()))
|
||||
|
||||
logger.info(f'Ready to rumble! Press Ctrl+C to shut down')
|
||||
reload_task = loop.create_task(self.reload_ev.wait())
|
||||
|
|
|
@ -9,6 +9,8 @@ import kdl
|
|||
import quart
|
||||
|
||||
from ovtk_audiencekit.core.Config import kdl_parse_config, compute_dynamic
|
||||
from ovtk_audiencekit.utils import format_exception
|
||||
|
||||
|
||||
|
||||
class PluginError(Exception):
|
||||
|
@ -30,6 +32,7 @@ class OvtkBlueprint(quart.Blueprint):
|
|||
|
||||
class PluginBase(ABC):
|
||||
plugins = {}
|
||||
hooks = {} # the hookerrrrrrrr
|
||||
|
||||
def __init__(self, chat_processes, event_queue, name, global_ctx, _children=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
@ -56,24 +59,41 @@ class PluginBase(ABC):
|
|||
def __del__(self):
|
||||
if self.plugins.get(self._name) == self:
|
||||
del self.plugins[self._name]
|
||||
if self._name in self.hooks:
|
||||
del self.hooks[self._name]
|
||||
|
||||
async def _kdl_call(self, node, _ctx):
|
||||
args, props = compute_dynamic(node, _ctx=_ctx)
|
||||
subroutine = node.sub
|
||||
|
||||
if subroutine:
|
||||
func = self
|
||||
for accessor in subroutine:
|
||||
func = getattr(func, accessor)
|
||||
else:
|
||||
func = self.run
|
||||
|
||||
for hook in self.hooks.values():
|
||||
try:
|
||||
res = hook(self._name, node, _ctx)
|
||||
if asyncio.iscoroutinefunction(hook):
|
||||
await res
|
||||
except Exception as e:
|
||||
self.logger.warning(f'Failed to run plugin hook: {e}')
|
||||
self.logger.debug(format_exception(e))
|
||||
|
||||
async def _call(self, subroutine, tag, *args, **kwargs):
|
||||
try:
|
||||
if subroutine:
|
||||
func = self
|
||||
for accessor in subroutine:
|
||||
func = getattr(func, accessor)
|
||||
else:
|
||||
func = self.run
|
||||
res = func(*args, **kwargs)
|
||||
result = func(*args, _children=node.nodes, _ctx=_ctx, **props)
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
res = await res
|
||||
return res
|
||||
result = await result
|
||||
except Exception as e:
|
||||
if isinstance(e, KeyboardInterrupt):
|
||||
raise e
|
||||
raise PluginError(self._name, str(e)) from e
|
||||
|
||||
if node.alias:
|
||||
_ctx[node.alias] = result
|
||||
|
||||
async def _tick(self, *args, **kwargs):
|
||||
try:
|
||||
res = self.tick(*args, **kwargs)
|
||||
|
@ -98,6 +118,7 @@ class PluginBase(ABC):
|
|||
raise e
|
||||
raise PluginError(self._name, str(e)) from e
|
||||
|
||||
|
||||
# Base class helpers
|
||||
def broadcast(self, event):
|
||||
"""Send event to every active chat"""
|
||||
|
@ -106,7 +127,10 @@ class PluginBase(ABC):
|
|||
continue
|
||||
proc.control_pipe.send(event)
|
||||
|
||||
async def execute_kdl(self, nodes, *py_args, _ctx={}, **py_props):
|
||||
def register_hook(self, hook):
|
||||
self.hooks[self._name] = hook
|
||||
|
||||
async def execute_kdl(self, nodes, _ctx={}):
|
||||
"""
|
||||
Run other plugins as configured by the passed KDL nodes collection
|
||||
If this was done in response to an event, pass it as 'event' in _ctx!
|
||||
|
@ -114,16 +138,14 @@ class PluginBase(ABC):
|
|||
_ctx = copy.deepcopy({**self._global_ctx, **_ctx})
|
||||
for node in nodes:
|
||||
try:
|
||||
args, props = compute_dynamic(node, _ctx=_ctx)
|
||||
target = self.plugins.get(node.name)
|
||||
if target is None:
|
||||
self.logger.warning(f'Could not find plugin or builtin with name {node.name}')
|
||||
break
|
||||
result = await target._call(node.sub, node.tag, *args, *py_args, **props, _ctx=_ctx, **py_props, _children=node.nodes)
|
||||
if node.alias:
|
||||
_ctx[node.alias] = result
|
||||
await target._kdl_call(node, _ctx)
|
||||
except Exception as e:
|
||||
self.logger.warning(f'Failed to execute defered KDL: {e}')
|
||||
self.logger.debug(format_exception(e))
|
||||
break
|
||||
|
||||
|
||||
|
@ -134,6 +156,7 @@ class PluginBase(ABC):
|
|||
"""
|
||||
self._event_queue.put_nowait(event)
|
||||
|
||||
|
||||
# User-defined
|
||||
async def setup(self, *args, **kwargs):
|
||||
"""Called when plugin is being loaded."""
|
|
@ -1,3 +1,4 @@
|
|||
from .WebsocketServerProcess import WebsocketServerProcess
|
||||
from .Plugins import PluginBase, PluginError
|
||||
from .MainProcess import MainProcess
|
||||
from .Audio import Clip, Stream
|
||||
|
|
|
@ -1,20 +1,47 @@
|
|||
from dataclasses import dataclass, field
|
||||
import json
|
||||
|
||||
import click
|
||||
|
||||
from ovtk_audiencekit.events import Event
|
||||
|
||||
|
||||
@dataclass
|
||||
class Control(Event):
|
||||
"""Generic inter-bus communication"""
|
||||
target: str
|
||||
target: str = None
|
||||
data: dict = field(default_factory=dict)
|
||||
|
||||
def __init__(self, via, target, **kwargs):
|
||||
super().__init__(via)
|
||||
self.target = target
|
||||
self.data = kwargs
|
||||
|
||||
def __repr__(self):
|
||||
return f"Control message : target = {self.target}, data = {self.data}"
|
||||
|
||||
def serialize(self):
|
||||
return json.dumps({
|
||||
'type': [cls.__name__ for cls in self.__class__.__mro__],
|
||||
'data': {**self.data, 'target': self.target},
|
||||
'data': {**self.data, **self.to_dict()},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _cli(cls, cmd):
|
||||
def parse(ctx, param, value):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
|
||||
super_cb = cmd.callback
|
||||
|
||||
@click.pass_context
|
||||
def expand(ctx, *args, json=None, **kwargs):
|
||||
if json:
|
||||
kwargs = {**json, **kwargs}
|
||||
return ctx.invoke(super_cb, *args, **kwargs)
|
||||
|
||||
cmd.params.append(
|
||||
click.Option(['--json', '-d'], callback=parse, help="Add arbitrary data in JSON format")
|
||||
)
|
||||
cmd.callback = expand
|
||||
return cmd
|
||||
|
|
|
@ -43,14 +43,13 @@ class Message(Event):
|
|||
return super().hydrate(user_type=user_type, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def __cli__(cls):
|
||||
def _cli(cls, cmd):
|
||||
def dono(ctx, param, value):
|
||||
if value:
|
||||
return [value, value]
|
||||
|
||||
return [
|
||||
click.Option(['--monitization', '-m'], type=click.FLOAT, callback=dono),
|
||||
]
|
||||
cmd.params.append(click.Option(['--monitization', '-m'], type=click.FLOAT, callback=dono))
|
||||
return cmd
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -28,11 +28,12 @@ class Subscription(Event):
|
|||
return f"Subcription from {self.user_name or 'anonymous'} to {recipent} - tier = {self.tier}"
|
||||
|
||||
@classmethod
|
||||
def __cli__(cls):
|
||||
def _cli(cls, cmd):
|
||||
def userfactory(ctx, param, value):
|
||||
if value:
|
||||
return [User(user_name=name, user_id=name) for name in value]
|
||||
|
||||
return [
|
||||
click.Option(['--gifted_to', '-g'], type=click.STRING, callback=userfactory, multiple=True),
|
||||
]
|
||||
cmd.params.append(
|
||||
click.Option(['--gifted_to', '-g'], type=click.STRING, callback=userfactory, multiple=True)
|
||||
)
|
||||
return cmd
|
||||
|
|
|
@ -7,3 +7,4 @@ from .Subscription import Subscription
|
|||
from .Follow import Follow
|
||||
|
||||
__all__ = ['Event', 'Message', 'SysMessage', 'Delete', 'Control', 'Subscription', 'Follow']
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
|
|
@ -3,7 +3,7 @@ from collections import deque
|
|||
|
||||
import maya
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.core import Clip, Stream
|
||||
|
||||
class AudioAlert(PluginBase):
|
||||
|
@ -24,7 +24,7 @@ class AudioAlert(PluginBase):
|
|||
self.logger.warn('Target audio device does not claim to support common sample rates! Attempting playback at native rate of audio')
|
||||
self.sample_rate = sample_rate
|
||||
|
||||
def run(self, path, speed=1, keep_pitch=False, immediate=True, poly=1, **kwargs):
|
||||
async def run(self, path, speed=1, keep_pitch=False, wait=False, poly=1, **kwargs):
|
||||
poly = int(poly)
|
||||
key = f'{path}@{speed}{"X" if keep_pitch else "x"}'
|
||||
clip = self.clips.get(key, [None, None])[0]
|
||||
|
@ -53,12 +53,12 @@ class AudioAlert(PluginBase):
|
|||
stream_dq.append(stream)
|
||||
|
||||
|
||||
if immediate:
|
||||
task = asyncio.create_task(stream.aplay())
|
||||
task.add_done_callback(self.tasks.remove)
|
||||
self.tasks.add(task)
|
||||
if wait:
|
||||
await stream.aplay()
|
||||
else:
|
||||
stream.play()
|
||||
task = asyncio.create_task(stream.aplay())
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
self.tasks.add(task)
|
||||
|
||||
def close(self):
|
||||
self._cleanup_task.cancel()
|
||||
|
|
|
@ -7,7 +7,7 @@ import maya
|
|||
from requests.exceptions import HTTPError
|
||||
from owoify.owoify import owoify, Owoness
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.core.Data import CACHE_DIR
|
||||
from ovtk_audiencekit.plugins.builtins.Command import Command, CommandTypes
|
||||
from ovtk_audiencekit.events.Message import Message, SysMessage
|
||||
|
@ -87,7 +87,7 @@ class JailPlugin(PluginBase):
|
|||
if isinstance(event, Message):
|
||||
if self.jail_command.invoked(event):
|
||||
try:
|
||||
args = self.jail_command.parse(event.text)
|
||||
args, _ = self.jail_command.parse(event.text)
|
||||
end_date = maya.when(args['length'], prefer_dates_from='future')
|
||||
deets = self.chats[event.via].shared.api.get_user_details(args['username'])
|
||||
if deets is None:
|
||||
|
@ -117,7 +117,7 @@ class JailPlugin(PluginBase):
|
|||
self.send_to_bus(weewoo)
|
||||
elif self.unjail_command.invoked(event):
|
||||
try:
|
||||
args = self.jail_command.parse(event.text)
|
||||
args, _ = self.jail_command.parse(event.text)
|
||||
deets = self.chats[event.via].shared.api.get_user_details(args['username'])
|
||||
if deets is None:
|
||||
raise ValueError()
|
||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
|
||||
import mido
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
|
||||
def matches(msg, attrs):
|
||||
|
@ -14,8 +14,7 @@ def matches(msg, attrs):
|
|||
|
||||
|
||||
class MidiPlugin(PluginBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def setup(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
def callback(msg):
|
||||
asyncio.run_coroutine_threadsafe(self.recv_callback(msg), loop)
|
1
src/ovtk_audiencekit/plugins/Midi/__init__.py
Normal file
1
src/ovtk_audiencekit/plugins/Midi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .Midi import MidiPlugin as Plugin
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
|
||||
import simpleobsws
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
|
||||
class OBSWSPlugin(PluginBase):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from pythonosc.udp_client import SimpleUDPClient
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
|
||||
class OSCPlugin(PluginBase):
|
||||
|
|
|
@ -4,7 +4,7 @@ import json
|
|||
import os
|
||||
|
||||
from ovtk_audiencekit.core.Data import CACHE_DIR
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import Message
|
||||
|
||||
from .Formatter import PhraseCountFormatter
|
||||
|
|
|
@ -2,7 +2,7 @@ from argparse import ArgumentError
|
|||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.plugins.builtins.Command import Command, CommandTypes
|
||||
from ovtk_audiencekit.events.Message import Message, SysMessage, USER_TYPE
|
||||
from ovtk_audiencekit.chats.Twitch import Process as Twitch
|
||||
|
@ -47,7 +47,7 @@ class ShoutoutPlugin(PluginBase):
|
|||
if isinstance(event, Message):
|
||||
if self.command and self.command.invoked(event):
|
||||
try:
|
||||
args = self.command.parse(event.text)
|
||||
args, _ = self.command.parse(event.text)
|
||||
except ArgumentError as e:
|
||||
msg = SysMessage(self._name, str(e), replies_to=event)
|
||||
self.chats[event.via].send(msg)
|
||||
|
|
|
@ -6,7 +6,7 @@ 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.core import PluginBase
|
||||
from ovtk_audiencekit.events import Message, SysMessage
|
||||
from ovtk_audiencekit.core import Clip, Stream
|
||||
from ovtk_audiencekit.core.Data import CACHE_DIR
|
||||
|
@ -58,6 +58,12 @@ class TextToSpeechPlugin(PluginBase):
|
|||
use_cuda=self.cuda,
|
||||
)
|
||||
|
||||
self.tasks = set()
|
||||
|
||||
def close(self):
|
||||
for task in self.tasks:
|
||||
task.cancel()
|
||||
|
||||
def make_tts_wav(self, text, filename=None):
|
||||
if filename is None:
|
||||
filename = os.path.join(self.cache_dir, f'{uuid.uuid1()}.wav')
|
||||
|
@ -70,25 +76,29 @@ class TextToSpeechPlugin(PluginBase):
|
|||
self.synthesizer.save_wav(wav, filename)
|
||||
return filename
|
||||
|
||||
async def run(self, text, *args, _ctx={}, wait=True, **kwargs):
|
||||
async def run(self, text, *args, _ctx={}, wait=False, **kwargs):
|
||||
try:
|
||||
# Force punctuation (keep AI from spinning off into random noises)
|
||||
if not any([text.endswith(punc) for punc in '.!?:']):
|
||||
text += '.'
|
||||
filename = self.make_tts_wav(text)
|
||||
# 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)
|
||||
|
||||
# TODO: Play direct from memory
|
||||
clip = Clip(filename, force_stereo=True, samplerate=self.sample_rate)
|
||||
stream = Stream(clip, self.output_index)
|
||||
if wait:
|
||||
async def play():
|
||||
async def play():
|
||||
try:
|
||||
await stream.aplay()
|
||||
finally:
|
||||
stream.close()
|
||||
os.remove(os.path.join(self.cache_dir, filename))
|
||||
asyncio.create_task(play())
|
||||
else:
|
||||
stream.play()
|
||||
stream.close()
|
||||
os.remove(os.path.join(self.cache_dir, filename))
|
||||
task = asyncio.create_task(play())
|
||||
self.tasks.add(task)
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
|
||||
if wait:
|
||||
await task
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to make speech from input: {e}")
|
||||
if source_event := _ctx.get('event'):
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from ovtk_audiencekit.core.PluginBase import PluginBase, PluginError
|
||||
|
||||
__all__ = ['PluginBase', 'PluginError']
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import random
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
|
||||
class ChancePlugin(PluginBase):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import subprocess
|
||||
import random
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import SysMessage
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import sys
|
|||
|
||||
from multipledispatch import dispatch
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import Message, SysMessage
|
||||
from ovtk_audiencekit.events.Message import USER_TYPE
|
||||
|
||||
|
@ -87,7 +87,7 @@ class Command:
|
|||
args = emoteless.split()[1:]
|
||||
parsed, unknown = self._parser.parse_known_args(args)
|
||||
parsed_asdict = vars(parsed)
|
||||
return parsed_asdict
|
||||
return parsed_asdict, unknown
|
||||
|
||||
|
||||
class CommandPlugin(PluginBase):
|
||||
|
@ -127,9 +127,10 @@ class CommandPlugin(PluginBase):
|
|||
continue
|
||||
if command.invoked(event):
|
||||
try:
|
||||
args = command.parse(event.text)
|
||||
args, unknown = command.parse(event.text)
|
||||
self.logger.debug(f"Parsed args for {command.name}: {args}")
|
||||
ctx = dict(event=event, **args)
|
||||
self.logger.debug(f"Remaining text: {unknown}")
|
||||
ctx = dict(event=event, rest=' '.join(unknown), **args)
|
||||
await self.execute_kdl(actionnode.nodes, _ctx=ctx)
|
||||
except argparse.ArgumentError as e:
|
||||
msg = SysMessage(self._name, f"{e}. See !help {command.name}", replies_to=event)
|
||||
|
@ -138,7 +139,7 @@ class CommandPlugin(PluginBase):
|
|||
|
||||
if self.help_cmd.invoked(event):
|
||||
try:
|
||||
args = self.help_cmd.parse(event.text)
|
||||
args, _ = self.help_cmd.parse(event.text)
|
||||
except argparse.ArgumentError as e:
|
||||
msg = SysMessage(self._name, f"{e}. See !help {self.help_cmd.name}", replies_to=event)
|
||||
self.chats[event.via].send(msg)
|
||||
|
|
|
@ -6,7 +6,7 @@ import uuid
|
|||
import maya
|
||||
import aioscheduler
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.utils import format_exception
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import subprocess
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import SysMessage
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
import logging
|
||||
|
||||
|
|
53
src/ovtk_audiencekit/plugins/builtins/Mkevent.py
Normal file
53
src/ovtk_audiencekit/plugins/builtins/Mkevent.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import dataclasses
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import Event
|
||||
from ovtk_audiencekit.utils import get_subclasses
|
||||
|
||||
|
||||
class EvFactory:
|
||||
def __init__(self, ev_class):
|
||||
self.target_class = ev_class
|
||||
self.arg_convs = []
|
||||
self.kwarg_convs = {}
|
||||
for field in dataclasses.fields(self.target_class):
|
||||
required = all(isinstance(default, dataclasses.MISSING.__class__) for default in [field.default, field.default_factory])
|
||||
if issubclass(field.type, Enum):
|
||||
conv = lambda value: field.type.__members__[value]
|
||||
else:
|
||||
conv = None
|
||||
if required:
|
||||
self.arg_convs.append(conv)
|
||||
else:
|
||||
self.kwarg_convs[field.name] = conv
|
||||
|
||||
def make(self, via, *args, **kwargs):
|
||||
args = [
|
||||
conv(arg) if conv else arg
|
||||
for arg, conv in zip(args, self.arg_convs)
|
||||
]
|
||||
kwargs = {
|
||||
key: self.kwarg_convs[key](value) if self.kwarg_convs.get(key) else value
|
||||
for key, value in kwargs.items()
|
||||
}
|
||||
return self.target_class(via, *args, **kwargs)
|
||||
|
||||
|
||||
class MakeEventPkugin(PluginBase):
|
||||
"""Create a new event and send it to the event bus"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.event_factories = {}
|
||||
for event_class in get_subclasses(Event):
|
||||
event_name = event_class.__name__
|
||||
self.event_factories[event_name] = EvFactory(event_class)
|
||||
|
||||
def run(self, event_name, *args, _children=[], _ctx={}, **kwargs):
|
||||
event_factory = self.event_factories.get(event_name)
|
||||
if event_factory is None:
|
||||
raise ValueError(f'Unknown event type "{event_name}"')
|
||||
ev = event_factory.make('kdl', *args, **kwargs)
|
||||
self.send_to_bus(ev)
|
|
@ -1,4 +1,4 @@
|
|||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import SysMessage, Message
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import json
|
|||
|
||||
import kdl
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.utils import format_exception
|
||||
|
||||
|
||||
|
@ -36,14 +36,14 @@ class ScenePlugin(PluginBase):
|
|||
self.blueprint.add_url_rule('/<name>/<cmd>', 'api-sceneset', self.ui_setscene)
|
||||
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, immediate=True, oneshot=False, **kwargs):
|
||||
if _children is None and active is None:
|
||||
raise UsageError('Either define a new scene or set `--active` to true / false')
|
||||
async def run(self, name, _children=None, _ctx={}, active=None, group=None, oneshot=False, **kwargs):
|
||||
if _children is None:
|
||||
raise UsageError('Empty scene definition! Did you mean scene.set?')
|
||||
|
||||
if _children:
|
||||
await self.define(name, group, _children, default_active=active, oneshot=oneshot, ctx=_ctx)
|
||||
else:
|
||||
await self.switch(name, active, is_immediate=immediate, ctx=_ctx)
|
||||
await self.define(name, group, _children, default_active=active, oneshot=oneshot, ctx=_ctx)
|
||||
|
||||
async def set(self, name, _children=None, _ctx={}, active=True, wait=False):
|
||||
await self.switch(name, active, is_immediate=not wait, ctx=_ctx)
|
||||
|
||||
async def define(self, name, group, children, default_active=False, oneshot=False, ctx={}):
|
||||
if self.scenes.get(name) is not None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.core.Config import compute_dynamic
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import re
|
|||
from dataclasses import dataclass
|
||||
import typing
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import Message
|
||||
|
||||
|
||||
|
|
9
src/ovtk_audiencekit/plugins/builtins/Wait.py
Normal file
9
src/ovtk_audiencekit/plugins/builtins/Wait.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from ovtk_audiencekit.core import PluginBase
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class WaitPlugin(PluginBase):
|
||||
"""Halt execution for a bit"""
|
||||
async def run(self, seconds=0, minutes=0, **kwargs): # If you want `hours`, why??????
|
||||
await asyncio.sleep(seconds + (minutes * 60))
|
|
@ -1,19 +0,0 @@
|
|||
import subprocess
|
||||
|
||||
import websockets
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.events import SysMessage
|
||||
from ovtk_audiencekit.utils import make_sync
|
||||
|
||||
@make_sync
|
||||
async def send(ws, data):
|
||||
async with websockets.connect(ws) as websocket:
|
||||
await websocket.send(data)
|
||||
|
||||
class WebSocketPlugin(PluginBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def run(self, endpoint, data, _ctx={}, **kwargs):
|
||||
send(endpoint, data)
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from ovtk_audiencekit.plugins import PluginBase
|
||||
from ovtk_audiencekit.core import PluginBase
|
||||
from ovtk_audiencekit.events import SysMessage, Message
|
||||
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@ from .Write import WritePlugin as write
|
|||
from .Exec import ExecPlugin as exec
|
||||
from .Chance import ChancePlugin as chance
|
||||
from .Choice import ChoicePlugin as choice
|
||||
from .Midi import MidiPlugin as midi
|
||||
from .WebSocket import WebSocketPlugin as ws
|
||||
from .Set import SetPlugin as set
|
||||
from .Scene import ScenePlugin as scene
|
||||
from .Log import LogPlugin as log
|
||||
from .Mkevent import MakeEventPkugin as mkevent
|
||||
from .Wait import WaitPlugin as wait
|
||||
|
||||
__all__ = ['trigger', 'reply', 'command', 'cue', 'write', 'exec', 'chance', 'choice', 'midi', 'ws', 'set', 'scene', 'log']
|
||||
__all__ = [
|
||||
'trigger', 'reply', 'command', 'cue', 'write', 'exec', 'chance', 'choice',
|
||||
'set', 'scene', 'log', 'mkevent', 'wait',
|
||||
]
|
||||
|
|
Loading…
Add table
Reference in a new issue