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
|
## 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:
|
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
|
```python
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.events import Message
|
from ovtk_audiencekit.events import Message
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
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 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 {
|
secrets {
|
||||||
/* Generate credentials via https://ovtk.skeh.site/twitch/auth
|
/* Generate credentials via https://ovtk.skeh.site/twitch/auth
|
||||||
and paste them between the curly braces below */
|
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:
|
There are two types of modules, chats and plugins:
|
||||||
+ Chats are self-explanitory: the event providers - livestream services.
|
+ Chats are self-explanitory: the event providers - livestream services.
|
||||||
+ Plugins are the heart of the system, and can both monitor livestream events
|
+ 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)
|
Some plugins (called "builtins") are always available, but others are not loaded
|
||||||
are always available, but others are not loaded until you ask to conserve
|
until you ask. Chats are never loaded by default.
|
||||||
resources. Chats are never loaded by default.
|
|
||||||
|
|
||||||
You can load either by using the "chat" or "plugin" node respectively,
|
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
|
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" channel_name="MyTwitchChannel"
|
||||||
// chat "Twitch:otherguy" channel_name="SomeOtherChannel"
|
// guest:chat "Twitch" channel_name="CollabChannel" readonly=true
|
||||||
// plugin "AudioAlert" output="ALSA:default"
|
// 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.
|
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 */
|
/* Self-promo every 2 hours */
|
||||||
// cue {
|
// cue hours=2 {
|
||||||
// every hours=2 {
|
// reply "Like the setup? Run it yourself! https://git.skeh.site/explore/repos?q=ovtk&topic=1"
|
||||||
// reply "Like the setup? Run it yourself! https://git.skeh.site/explore/repos?q=ovtk&topic=1"
|
|
||||||
// }
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
/* Call an existing shoutout bot on raid */
|
/* Call an existing shoutout bot on raid */
|
||||||
// trigger event="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 {
|
// command "lurk" help="Lurke modeo" display=true {
|
||||||
// do {
|
// do {
|
||||||
// reply "The Twitch algorithm thanks you for the lurk~" display=true
|
// reply "The Twitch algorithm thanks you for the lurk~" display=true
|
||||||
|
@ -65,16 +71,57 @@ secrets {
|
||||||
|
|
||||||
/* TTS for every donation event */
|
/* TTS for every donation event */
|
||||||
// plugin "TTS"
|
// plugin "TTS"
|
||||||
// trigger monitization="0.01-" source="self" {
|
// trigger monitization="0.01-" {
|
||||||
// TTS (arg)"{event.user_name} says: {event.text}"
|
// 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" {
|
// trigger event="ChannelPointRedemption" action="Sound Sussy" {
|
||||||
// midi "sysex" data="FunEffect;1"
|
// Midi "program_change" channel=1 program=1
|
||||||
// cue {
|
// cue minutes=5 {
|
||||||
// after minutes=5 {
|
// Midi "program_change" channel=1 program=0
|
||||||
// midi "sysex" data="FunEffect;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",
|
"click",
|
||||||
"kdl-py",
|
"kdl-py",
|
||||||
"quart==0.18.*",
|
"quart==0.18.*",
|
||||||
|
"werkzeug==2.3.7",
|
||||||
"hypercorn",
|
"hypercorn",
|
||||||
"requests",
|
"requests",
|
||||||
"websockets",
|
"websockets==11.0.3",
|
||||||
"aioprocessing",
|
"aioprocessing",
|
||||||
"aioscheduler",
|
"aioscheduler",
|
||||||
"pyaudio==0.2.*",
|
"pyaudio==0.2.*",
|
||||||
|
@ -22,10 +23,6 @@ dependencies = [
|
||||||
"blessed",
|
"blessed",
|
||||||
"appdirs",
|
"appdirs",
|
||||||
"maya",
|
"maya",
|
||||||
"mido",
|
|
||||||
"python-rtmidi",
|
|
||||||
"simpleobsws",
|
|
||||||
"python-osc>=1.9.0",
|
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10,<3.11"
|
requires-python = ">=3.10,<3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -39,11 +36,12 @@ tts = [
|
||||||
phrasecounter = ["num2words"]
|
phrasecounter = ["num2words"]
|
||||||
jail = ["owoify-py==2.*"]
|
jail = ["owoify-py==2.*"]
|
||||||
twitch = ["miniirc"]
|
twitch = ["miniirc"]
|
||||||
|
midi = [
|
||||||
[tool.pdm.dev-dependencies]
|
"mido",
|
||||||
dev = [
|
"python-rtmidi",
|
||||||
"pipenv-setup",
|
|
||||||
]
|
]
|
||||||
|
obs = ["simpleobsws"]
|
||||||
|
osc = ["python-osc>=1.9.0"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["pdm-backend"]
|
requires = ["pdm-backend"]
|
||||||
|
@ -52,6 +50,7 @@ build-backend = "pdm.backend"
|
||||||
[tool.pdm.scripts]
|
[tool.pdm.scripts]
|
||||||
start = "python audiencekit.py start"
|
start = "python audiencekit.py start"
|
||||||
ws = "python audiencekit.py ws"
|
ws = "python audiencekit.py ws"
|
||||||
|
debug = "python audiencekit.py --debug"
|
||||||
|
|
||||||
[tool.pdm]
|
[tool.pdm]
|
||||||
[[tool.pdm.source]]
|
[[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('--bus-port', default='8080')
|
||||||
@click.option('--web-bind', default='localhost')
|
@click.option('--web-bind', default='localhost')
|
||||||
@click.option('--web-port', default='8000')
|
@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"""
|
"""Start audiencekit server"""
|
||||||
logger.info('Hewwo!!')
|
logger.info('Hewwo!!')
|
||||||
main = MainProcess(config_file,
|
main = MainProcess(config_file,
|
||||||
bus_conf=(bus_bind, bus_port),
|
bus_conf=(bus_bind, bus_port),
|
||||||
web_conf=(web_bind, web_port))
|
web_conf=(web_bind, web_port),
|
||||||
|
max_concurrent=parallel)
|
||||||
try:
|
try:
|
||||||
asyncio.run(main.run())
|
asyncio.run(main.run())
|
||||||
except KeyboardInterrupt:
|
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)
|
param = click.Option(param_decls=[f'--{field.name}'], type=param_type, default=field.default, show_default=True)
|
||||||
params.append(param)
|
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__)
|
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
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,12 @@ import hypercorn
|
||||||
import hypercorn.asyncio
|
import hypercorn.asyncio
|
||||||
import hypercorn.logging
|
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.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.events import Event, Delete
|
||||||
from ovtk_audiencekit.chats.ChatProcess import ShutdownRequest
|
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
|
from ovtk_audiencekit.utils import format_exception
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -52,11 +53,12 @@ def import_or_reload_mod(module_name, default_package=None, external=False):
|
||||||
|
|
||||||
|
|
||||||
class MainProcess:
|
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._running = False
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
self.bus_conf = bus_conf
|
self.bus_conf = bus_conf
|
||||||
self.web_conf = web_conf
|
self.web_conf = web_conf
|
||||||
|
self.max_concurrent = max_concurrent
|
||||||
|
|
||||||
self.chat_processes = {}
|
self.chat_processes = {}
|
||||||
self.plugins = {}
|
self.plugins = {}
|
||||||
|
@ -129,7 +131,6 @@ class MainProcess:
|
||||||
if e.fatal:
|
if e.fatal:
|
||||||
self._unload_plugin(e.source)
|
self._unload_plugin(e.source)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._plugin_error
|
|
||||||
logger.critical(f'Failure when processing {plugin_name} ({e}) - disabling...')
|
logger.critical(f'Failure when processing {plugin_name} ({e}) - disabling...')
|
||||||
logger.debug(format_exception(e))
|
logger.debug(format_exception(e))
|
||||||
self._unload_plugin(plugin_name)
|
self._unload_plugin(plugin_name)
|
||||||
|
@ -237,7 +238,7 @@ class MainProcess:
|
||||||
if plugin_module is None:
|
if plugin_module is None:
|
||||||
logger.error(f'Unknown plugin: {node.name}')
|
logger.error(f'Unknown plugin: {node.name}')
|
||||||
else:
|
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):
|
async def user_shutdown(self):
|
||||||
for process_name, process in list(reversed(self.chat_processes.items())):
|
for process_name, process in list(reversed(self.chat_processes.items())):
|
||||||
|
@ -293,7 +294,8 @@ class MainProcess:
|
||||||
await self.user_setup()
|
await self.user_setup()
|
||||||
# Start plumbing tasks
|
# Start plumbing tasks
|
||||||
user_tasks.add(loop.create_task(self.tick_plugins()))
|
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')
|
logger.info(f'Ready to rumble! Press Ctrl+C to shut down')
|
||||||
reload_task = loop.create_task(self.reload_ev.wait())
|
reload_task = loop.create_task(self.reload_ev.wait())
|
||||||
|
|
|
@ -9,6 +9,8 @@ import kdl
|
||||||
import quart
|
import quart
|
||||||
|
|
||||||
from ovtk_audiencekit.core.Config import kdl_parse_config, compute_dynamic
|
from ovtk_audiencekit.core.Config import kdl_parse_config, compute_dynamic
|
||||||
|
from ovtk_audiencekit.utils import format_exception
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PluginError(Exception):
|
class PluginError(Exception):
|
||||||
|
@ -30,6 +32,7 @@ class OvtkBlueprint(quart.Blueprint):
|
||||||
|
|
||||||
class PluginBase(ABC):
|
class PluginBase(ABC):
|
||||||
plugins = {}
|
plugins = {}
|
||||||
|
hooks = {} # the hookerrrrrrrr
|
||||||
|
|
||||||
def __init__(self, chat_processes, event_queue, name, global_ctx, _children=None, **kwargs):
|
def __init__(self, chat_processes, event_queue, name, global_ctx, _children=None, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -56,24 +59,41 @@ class PluginBase(ABC):
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self.plugins.get(self._name) == self:
|
if self.plugins.get(self._name) == self:
|
||||||
del self.plugins[self._name]
|
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:
|
try:
|
||||||
if subroutine:
|
result = func(*args, _children=node.nodes, _ctx=_ctx, **props)
|
||||||
func = self
|
|
||||||
for accessor in subroutine:
|
|
||||||
func = getattr(func, accessor)
|
|
||||||
else:
|
|
||||||
func = self.run
|
|
||||||
res = func(*args, **kwargs)
|
|
||||||
if asyncio.iscoroutinefunction(func):
|
if asyncio.iscoroutinefunction(func):
|
||||||
res = await res
|
result = await result
|
||||||
return res
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, KeyboardInterrupt):
|
if isinstance(e, KeyboardInterrupt):
|
||||||
raise e
|
raise e
|
||||||
raise PluginError(self._name, str(e)) from e
|
raise PluginError(self._name, str(e)) from e
|
||||||
|
|
||||||
|
if node.alias:
|
||||||
|
_ctx[node.alias] = result
|
||||||
|
|
||||||
async def _tick(self, *args, **kwargs):
|
async def _tick(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
res = self.tick(*args, **kwargs)
|
res = self.tick(*args, **kwargs)
|
||||||
|
@ -98,6 +118,7 @@ class PluginBase(ABC):
|
||||||
raise e
|
raise e
|
||||||
raise PluginError(self._name, str(e)) from e
|
raise PluginError(self._name, str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
# Base class helpers
|
# Base class helpers
|
||||||
def broadcast(self, event):
|
def broadcast(self, event):
|
||||||
"""Send event to every active chat"""
|
"""Send event to every active chat"""
|
||||||
|
@ -106,7 +127,10 @@ class PluginBase(ABC):
|
||||||
continue
|
continue
|
||||||
proc.control_pipe.send(event)
|
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
|
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!
|
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})
|
_ctx = copy.deepcopy({**self._global_ctx, **_ctx})
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
try:
|
try:
|
||||||
args, props = compute_dynamic(node, _ctx=_ctx)
|
|
||||||
target = self.plugins.get(node.name)
|
target = self.plugins.get(node.name)
|
||||||
if target is None:
|
if target is None:
|
||||||
self.logger.warning(f'Could not find plugin or builtin with name {node.name}')
|
self.logger.warning(f'Could not find plugin or builtin with name {node.name}')
|
||||||
break
|
break
|
||||||
result = await target._call(node.sub, node.tag, *args, *py_args, **props, _ctx=_ctx, **py_props, _children=node.nodes)
|
await target._kdl_call(node, _ctx)
|
||||||
if node.alias:
|
|
||||||
_ctx[node.alias] = result
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f'Failed to execute defered KDL: {e}')
|
self.logger.warning(f'Failed to execute defered KDL: {e}')
|
||||||
|
self.logger.debug(format_exception(e))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +156,7 @@ class PluginBase(ABC):
|
||||||
"""
|
"""
|
||||||
self._event_queue.put_nowait(event)
|
self._event_queue.put_nowait(event)
|
||||||
|
|
||||||
|
|
||||||
# User-defined
|
# User-defined
|
||||||
async def setup(self, *args, **kwargs):
|
async def setup(self, *args, **kwargs):
|
||||||
"""Called when plugin is being loaded."""
|
"""Called when plugin is being loaded."""
|
|
@ -1,3 +1,4 @@
|
||||||
from .WebsocketServerProcess import WebsocketServerProcess
|
from .WebsocketServerProcess import WebsocketServerProcess
|
||||||
|
from .Plugins import PluginBase, PluginError
|
||||||
from .MainProcess import MainProcess
|
from .MainProcess import MainProcess
|
||||||
from .Audio import Clip, Stream
|
from .Audio import Clip, Stream
|
||||||
|
|
|
@ -1,20 +1,47 @@
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
from ovtk_audiencekit.events import Event
|
from ovtk_audiencekit.events import Event
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Control(Event):
|
class Control(Event):
|
||||||
"""Generic inter-bus communication"""
|
"""Generic inter-bus communication"""
|
||||||
target: str
|
target: str = None
|
||||||
data: dict = field(default_factory=dict)
|
data: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __init__(self, via, target, **kwargs):
|
||||||
|
super().__init__(via)
|
||||||
|
self.target = target
|
||||||
|
self.data = kwargs
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Control message : target = {self.target}, data = {self.data}"
|
return f"Control message : target = {self.target}, data = {self.data}"
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
'type': [cls.__name__ for cls in self.__class__.__mro__],
|
'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)
|
return super().hydrate(user_type=user_type, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __cli__(cls):
|
def _cli(cls, cmd):
|
||||||
def dono(ctx, param, value):
|
def dono(ctx, param, value):
|
||||||
if value:
|
if value:
|
||||||
return [value, value]
|
return [value, value]
|
||||||
|
|
||||||
return [
|
cmd.params.append(click.Option(['--monitization', '-m'], type=click.FLOAT, callback=dono))
|
||||||
click.Option(['--monitization', '-m'], type=click.FLOAT, callback=dono),
|
return cmd
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -28,11 +28,12 @@ class Subscription(Event):
|
||||||
return f"Subcription from {self.user_name or 'anonymous'} to {recipent} - tier = {self.tier}"
|
return f"Subcription from {self.user_name or 'anonymous'} to {recipent} - tier = {self.tier}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __cli__(cls):
|
def _cli(cls, cmd):
|
||||||
def userfactory(ctx, param, value):
|
def userfactory(ctx, param, value):
|
||||||
if value:
|
if value:
|
||||||
return [User(user_name=name, user_id=name) for name in value]
|
return [User(user_name=name, user_id=name) for name in value]
|
||||||
|
|
||||||
return [
|
cmd.params.append(
|
||||||
click.Option(['--gifted_to', '-g'], type=click.STRING, callback=userfactory, multiple=True),
|
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
|
from .Follow import Follow
|
||||||
|
|
||||||
__all__ = ['Event', 'Message', 'SysMessage', 'Delete', 'Control', 'Subscription', '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
|
import maya
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.core import Clip, Stream
|
from ovtk_audiencekit.core import Clip, Stream
|
||||||
|
|
||||||
class AudioAlert(PluginBase):
|
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.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
|
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)
|
poly = int(poly)
|
||||||
key = f'{path}@{speed}{"X" if keep_pitch else "x"}'
|
key = f'{path}@{speed}{"X" if keep_pitch else "x"}'
|
||||||
clip = self.clips.get(key, [None, None])[0]
|
clip = self.clips.get(key, [None, None])[0]
|
||||||
|
@ -53,12 +53,12 @@ class AudioAlert(PluginBase):
|
||||||
stream_dq.append(stream)
|
stream_dq.append(stream)
|
||||||
|
|
||||||
|
|
||||||
if immediate:
|
if wait:
|
||||||
task = asyncio.create_task(stream.aplay())
|
await stream.aplay()
|
||||||
task.add_done_callback(self.tasks.remove)
|
|
||||||
self.tasks.add(task)
|
|
||||||
else:
|
else:
|
||||||
stream.play()
|
task = asyncio.create_task(stream.aplay())
|
||||||
|
task.add_done_callback(self.tasks.discard)
|
||||||
|
self.tasks.add(task)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self._cleanup_task.cancel()
|
self._cleanup_task.cancel()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import maya
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from owoify.owoify import owoify, Owoness
|
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.core.Data import CACHE_DIR
|
||||||
from ovtk_audiencekit.plugins.builtins.Command import Command, CommandTypes
|
from ovtk_audiencekit.plugins.builtins.Command import Command, CommandTypes
|
||||||
from ovtk_audiencekit.events.Message import Message, SysMessage
|
from ovtk_audiencekit.events.Message import Message, SysMessage
|
||||||
|
@ -87,7 +87,7 @@ class JailPlugin(PluginBase):
|
||||||
if isinstance(event, Message):
|
if isinstance(event, Message):
|
||||||
if self.jail_command.invoked(event):
|
if self.jail_command.invoked(event):
|
||||||
try:
|
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')
|
end_date = maya.when(args['length'], prefer_dates_from='future')
|
||||||
deets = self.chats[event.via].shared.api.get_user_details(args['username'])
|
deets = self.chats[event.via].shared.api.get_user_details(args['username'])
|
||||||
if deets is None:
|
if deets is None:
|
||||||
|
@ -117,7 +117,7 @@ class JailPlugin(PluginBase):
|
||||||
self.send_to_bus(weewoo)
|
self.send_to_bus(weewoo)
|
||||||
elif self.unjail_command.invoked(event):
|
elif self.unjail_command.invoked(event):
|
||||||
try:
|
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'])
|
deets = self.chats[event.via].shared.api.get_user_details(args['username'])
|
||||||
if deets is None:
|
if deets is None:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
|
|
||||||
import mido
|
import mido
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
|
|
||||||
|
|
||||||
def matches(msg, attrs):
|
def matches(msg, attrs):
|
||||||
|
@ -14,8 +14,7 @@ def matches(msg, attrs):
|
||||||
|
|
||||||
|
|
||||||
class MidiPlugin(PluginBase):
|
class MidiPlugin(PluginBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def setup(self):
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
def callback(msg):
|
def callback(msg):
|
||||||
asyncio.run_coroutine_threadsafe(self.recv_callback(msg), loop)
|
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
|
import simpleobsws
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
|
|
||||||
|
|
||||||
class OBSWSPlugin(PluginBase):
|
class OBSWSPlugin(PluginBase):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from pythonosc.udp_client import SimpleUDPClient
|
from pythonosc.udp_client import SimpleUDPClient
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
|
|
||||||
|
|
||||||
class OSCPlugin(PluginBase):
|
class OSCPlugin(PluginBase):
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ovtk_audiencekit.core.Data import CACHE_DIR
|
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 ovtk_audiencekit.events import Message
|
||||||
|
|
||||||
from .Formatter import PhraseCountFormatter
|
from .Formatter import PhraseCountFormatter
|
||||||
|
|
|
@ -2,7 +2,7 @@ from argparse import ArgumentError
|
||||||
|
|
||||||
from requests.exceptions import HTTPError
|
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.plugins.builtins.Command import Command, CommandTypes
|
||||||
from ovtk_audiencekit.events.Message import Message, SysMessage, USER_TYPE
|
from ovtk_audiencekit.events.Message import Message, SysMessage, USER_TYPE
|
||||||
from ovtk_audiencekit.chats.Twitch import Process as Twitch
|
from ovtk_audiencekit.chats.Twitch import Process as Twitch
|
||||||
|
@ -47,7 +47,7 @@ class ShoutoutPlugin(PluginBase):
|
||||||
if isinstance(event, Message):
|
if isinstance(event, Message):
|
||||||
if self.command and self.command.invoked(event):
|
if self.command and self.command.invoked(event):
|
||||||
try:
|
try:
|
||||||
args = self.command.parse(event.text)
|
args, _ = self.command.parse(event.text)
|
||||||
except ArgumentError as e:
|
except ArgumentError as e:
|
||||||
msg = SysMessage(self._name, str(e), replies_to=event)
|
msg = SysMessage(self._name, str(e), replies_to=event)
|
||||||
self.chats[event.via].send(msg)
|
self.chats[event.via].send(msg)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from TTS.utils.synthesizer import Synthesizer
|
||||||
from TTS.utils.manage import ModelManager
|
from TTS.utils.manage import ModelManager
|
||||||
from TTS.config import load_config
|
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.events import Message, SysMessage
|
||||||
from ovtk_audiencekit.core import Clip, Stream
|
from ovtk_audiencekit.core import Clip, Stream
|
||||||
from ovtk_audiencekit.core.Data import CACHE_DIR
|
from ovtk_audiencekit.core.Data import CACHE_DIR
|
||||||
|
@ -58,6 +58,12 @@ class TextToSpeechPlugin(PluginBase):
|
||||||
use_cuda=self.cuda,
|
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):
|
def make_tts_wav(self, text, filename=None):
|
||||||
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')
|
||||||
|
@ -70,25 +76,29 @@ class TextToSpeechPlugin(PluginBase):
|
||||||
self.synthesizer.save_wav(wav, filename)
|
self.synthesizer.save_wav(wav, filename)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
async def run(self, text, *args, _ctx={}, wait=True, **kwargs):
|
async def run(self, text, *args, _ctx={}, wait=False, **kwargs):
|
||||||
try:
|
try:
|
||||||
# Force punctuation (keep AI from spinning off into random noises)
|
# Force punctuation (keep AI from spinning off into random noises)
|
||||||
if not any([text.endswith(punc) for punc in '.!?:']):
|
if not any([text.endswith(punc) for punc in '.!?:']):
|
||||||
text += '.'
|
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
|
# TODO: Play direct from memory
|
||||||
clip = Clip(filename, force_stereo=True, samplerate=self.sample_rate)
|
clip = Clip(filename, force_stereo=True, samplerate=self.sample_rate)
|
||||||
stream = Stream(clip, self.output_index)
|
stream = Stream(clip, self.output_index)
|
||||||
if wait:
|
async def play():
|
||||||
async def play():
|
try:
|
||||||
await stream.aplay()
|
await stream.aplay()
|
||||||
|
finally:
|
||||||
stream.close()
|
stream.close()
|
||||||
os.remove(os.path.join(self.cache_dir, filename))
|
os.remove(os.path.join(self.cache_dir, filename))
|
||||||
asyncio.create_task(play())
|
task = asyncio.create_task(play())
|
||||||
else:
|
self.tasks.add(task)
|
||||||
stream.play()
|
task.add_done_callback(self.tasks.discard)
|
||||||
stream.close()
|
|
||||||
os.remove(os.path.join(self.cache_dir, filename))
|
if wait:
|
||||||
|
await task
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to make speech from input: {e}")
|
self.logger.error(f"Failed to make speech from input: {e}")
|
||||||
if source_event := _ctx.get('event'):
|
if source_event := _ctx.get('event'):
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from ovtk_audiencekit.core.PluginBase import PluginBase, PluginError
|
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||||
|
|
||||||
__all__ = ['PluginBase', 'PluginError']
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
|
|
||||||
|
|
||||||
class ChancePlugin(PluginBase):
|
class ChancePlugin(PluginBase):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import subprocess
|
import subprocess
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.events import SysMessage
|
from ovtk_audiencekit.events import SysMessage
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import sys
|
||||||
|
|
||||||
from multipledispatch import dispatch
|
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 import Message, SysMessage
|
||||||
from ovtk_audiencekit.events.Message import USER_TYPE
|
from ovtk_audiencekit.events.Message import USER_TYPE
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class Command:
|
||||||
args = emoteless.split()[1:]
|
args = emoteless.split()[1:]
|
||||||
parsed, unknown = self._parser.parse_known_args(args)
|
parsed, unknown = self._parser.parse_known_args(args)
|
||||||
parsed_asdict = vars(parsed)
|
parsed_asdict = vars(parsed)
|
||||||
return parsed_asdict
|
return parsed_asdict, unknown
|
||||||
|
|
||||||
|
|
||||||
class CommandPlugin(PluginBase):
|
class CommandPlugin(PluginBase):
|
||||||
|
@ -127,9 +127,10 @@ class CommandPlugin(PluginBase):
|
||||||
continue
|
continue
|
||||||
if command.invoked(event):
|
if command.invoked(event):
|
||||||
try:
|
try:
|
||||||
args = command.parse(event.text)
|
args, unknown = command.parse(event.text)
|
||||||
self.logger.debug(f"Parsed args for {command.name}: {args}")
|
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)
|
await self.execute_kdl(actionnode.nodes, _ctx=ctx)
|
||||||
except argparse.ArgumentError as e:
|
except argparse.ArgumentError as e:
|
||||||
msg = SysMessage(self._name, f"{e}. See !help {command.name}", replies_to=event)
|
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):
|
if self.help_cmd.invoked(event):
|
||||||
try:
|
try:
|
||||||
args = self.help_cmd.parse(event.text)
|
args, _ = self.help_cmd.parse(event.text)
|
||||||
except argparse.ArgumentError as e:
|
except argparse.ArgumentError as e:
|
||||||
msg = SysMessage(self._name, f"{e}. See !help {self.help_cmd.name}", replies_to=event)
|
msg = SysMessage(self._name, f"{e}. See !help {self.help_cmd.name}", replies_to=event)
|
||||||
self.chats[event.via].send(msg)
|
self.chats[event.via].send(msg)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import uuid
|
||||||
import maya
|
import maya
|
||||||
import aioscheduler
|
import aioscheduler
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.utils import format_exception
|
from ovtk_audiencekit.utils import format_exception
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.events import SysMessage
|
from ovtk_audiencekit.events import SysMessage
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
|
|
||||||
import logging
|
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
|
from ovtk_audiencekit.events import SysMessage, Message
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import json
|
||||||
|
|
||||||
import kdl
|
import kdl
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.utils import format_exception
|
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('/<name>/<cmd>', 'api-sceneset', self.ui_setscene)
|
||||||
self.blueprint.add_url_rule('/monitor', 'monitor', self.ui_monitor_ws, is_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, immediate=True, oneshot=False, **kwargs):
|
async def run(self, name, _children=None, _ctx={}, active=None, group=None, oneshot=False, **kwargs):
|
||||||
if _children is None and active is None:
|
if _children is None:
|
||||||
raise UsageError('Either define a new scene or set `--active` to true / false')
|
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)
|
||||||
await self.define(name, group, _children, default_active=active, oneshot=oneshot, ctx=_ctx)
|
|
||||||
else:
|
async def set(self, name, _children=None, _ctx={}, active=True, wait=False):
|
||||||
await self.switch(name, active, is_immediate=immediate, ctx=_ctx)
|
await self.switch(name, active, is_immediate=not wait, ctx=_ctx)
|
||||||
|
|
||||||
async def define(self, name, group, children, default_active=False, oneshot=False, ctx={}):
|
async def define(self, name, group, children, default_active=False, oneshot=False, ctx={}):
|
||||||
if self.scenes.get(name) is not None:
|
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
|
from ovtk_audiencekit.core.Config import compute_dynamic
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.events import Message
|
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
|
import os
|
||||||
|
|
||||||
from ovtk_audiencekit.plugins import PluginBase
|
from ovtk_audiencekit.core import PluginBase
|
||||||
from ovtk_audiencekit.events import SysMessage, Message
|
from ovtk_audiencekit.events import SysMessage, Message
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,13 @@ from .Write import WritePlugin as write
|
||||||
from .Exec import ExecPlugin as exec
|
from .Exec import ExecPlugin as exec
|
||||||
from .Chance import ChancePlugin as chance
|
from .Chance import ChancePlugin as chance
|
||||||
from .Choice import ChoicePlugin as choice
|
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 .Set import SetPlugin as set
|
||||||
from .Scene import ScenePlugin as scene
|
from .Scene import ScenePlugin as scene
|
||||||
from .Log import LogPlugin as log
|
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