Skip to content

Commit

Permalink
Initial Overlay UI and Flow
Browse files Browse the repository at this point in the history
  • Loading branch information
WolfwithSword committed Jan 9, 2025
1 parent edae251 commit b45a4ee
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/chatdnd/events/session_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from helpers import Event

on_party_update = Event()
3 changes: 3 additions & 0 deletions src/chatdnd/events/web_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from helpers import Event

on_overlay_open = Event()
14 changes: 13 additions & 1 deletion src/chatdnd/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
from data import Session, Member, SessionState
from custom_logger.logger import logger

from chatdnd.events.session_events import on_party_update
from chatdnd.events.web_events import on_overlay_open

class SessionManager():
def __init__(self):
self.session = Session()
on_overlay_open.addListener(self.trigger_update)
on_party_update.trigger([self.session.get_party()])

def join_queue(self, member: Member):
self.session.queue.add(member)
Expand All @@ -17,11 +22,18 @@ def start_session(self, party_size: int = 4) -> bool:
self.session.party.clear()
self.session.party.update(random.sample(sorted(self.session.queue), party_size))
self.session.queue.clear()
on_party_update.trigger([self.session.get_party()])
return True

def end(self):
self.session.clear()
self.session.state = SessionState.NONE
on_party_update.trigger([self.session.get_party()])

def open(self):
self.session.state = SessionState.OPEN
self.session.clear()
self.session.state = SessionState.OPEN
on_party_update.trigger([self.session.get_party()])

def trigger_update(self):
on_party_update.trigger([self.session.get_party()])
3 changes: 3 additions & 0 deletions src/data/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ def clear(self):
self.queue.clear()
self.party.clear()
self.state: SessionState = SessionState.NONE

def get_party(self) -> List[Member]:
return sorted(self.party)
2 changes: 1 addition & 1 deletion src/helpers/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ async def _run_async_func(self, func, *args):
try:
await func(*args)
except Exception as e:
print(f"Async Event Listener error: {e}")
logger.error(f"Async Event Listener error: {e}")
99 changes: 72 additions & 27 deletions src/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
from custom_logger.logger import logger

from chatdnd.events.chat_events import chat_say_command
from chatdnd.events.session_events import on_party_update
from chatdnd.events.web_events import on_overlay_open
from helpers.utils import get_resource_path


STATIC_DIR = get_resource_path("../server/static", from_resources=True)
message_queue = Queue()
members_queue = Queue()
clients = set()
overlay_clients = set()

class ServerApp():
def __init__(self, config: Config):
Expand All @@ -24,58 +28,81 @@ def __init__(self, config: Config):
self.tts = LocalTTS(config) # TODO both local and cloud
self._setup_routes()

self._party: set[Member] = set()

# Setup here temporarily for POC
chat_say_command.addListener(self.chat_say)

on_party_update.addListener(self.send_members)
on_overlay_open.trigger()

def _setup_routes(self):
@self.app.route('/test') # Temp / Testing
async def test():
logger.info("TEST QUART")
return jsonify({"test": "123"})

@self.app.websocket("/tts")
@self.app.websocket("/ws/tts")
async def audio_stream():
clients.add(websocket)
try:
logger.debug("ws opened")
logger.debug("tts ws opened")
await websocket.send_json({"type":"heartbeat"})
while True:
if not message_queue.empty():
member, message = await message_queue.get()
logger.info(f"saying {message} from {member}")

async for chunk in self.tts.get_stream(message, '' if not member else member.preferred_tts):
speech_message = {
"type": "speech",
"name": member.name,
"message": message
}
logger.info(f"saying '{message}' from {member}")
duration = 0
last_chunk_duration = 0
send_bounce = False
async for chunk, _duration in self.tts.get_stream(message, '' if not member else member.preferred_tts):
# TODO: Allow for break / interruption from emergency stuff - also hide stuff. Or yknow, just instruct to hide the browser source.
if not send_bounce:
send_bounce = True
await self.animate_member(member.name, "bounce")
await members_queue.put(speech_message)
await asyncio.wait_for(websocket.send(chunk), timeout=10)
duration += _duration
last_chunk_duration = _duration
await asyncio.sleep(last_chunk_duration)
speech_message = {
"type": "endspeech"
}
await self.animate_member(member.name, "idle")
await asyncio.sleep(0.3)
await members_queue.put(speech_message)

await asyncio.sleep(0.1)
except Exception as e:
logger.error(e)
pass
finally:
logger.debug("ws closed")
logger.debug("tts ws closed")
clients.discard(websocket)

# This POST is *purely* for testing. We want actual triggers to be event driven to put text into the queue.
# Final version, this will not be enabled. We will rely on event processing
@self.app.route('/trigger-tts', methods=['POST']) # doesnt have to be a post
async def trigger_tts():

text = (await request.get_json()).get('text', 'Default message') # Read text from POST body

if not text:
return jsonify({"error": "Text is required"}), 400

if clients:
await message_queue.put((None, text))
return jsonify({"status": "success", "message": "Text sent to WebSocket for TTS."})
else:
return jsonify({"error": "No active WebSocket connection."}), 400
@self.app.websocket("/ws/members")
async def user_overlay_ws():
overlay_clients.add(websocket)
try:
await websocket.send_json({"type":"heartbeat"})
logger.debug('overlay ws opened')
on_overlay_open.trigger()
await asyncio.sleep(0.5)
while True:
while not members_queue.empty():
message = await members_queue.get()
logger.info(f"member msg {message}")
await asyncio.wait_for(websocket.send_json(message), timeout=5)
await asyncio.sleep(0.5)
finally:
logger.debug('overlay ws closed')
overlay_clients.discard(websocket)

@self.app.route('/overlay')
async def overlay():
return await send_from_directory(STATIC_DIR, 'overlay.html')

async def chat_say(self, member: Member, text: str):
logger.info(f"TTS saying '{text}' from {'UNKNOWN' if not member else member.name}")
# If client was connected but dc'd, this can revive connection when it runs. But also, we don't want things to queue up forever... Might not be a problem, needs hard testing later
# But cannot simply do an if check here for clients
await message_queue.put((member, text))
Expand All @@ -84,3 +111,21 @@ async def chat_say(self, member: Member, text: str):
async def run_task(self, host="0.0.0.0", **kwargs):
# TODO on port change, request app restart
await self.app.run_task(host=host, port=self.config.getint(section="SERVER", option="port"), **kwargs)

async def send_members(self, members: list[Member] = []):
user_data = [{"name": member.name, "pfp_url": member.pfp_url} for member in sorted(members)]
if not user_data:
speech_message = {
"type": "endspeech"
}
await members_queue.put(speech_message)
message = {"type": "update_users", "users": user_data}
await members_queue.put(message)

async def animate_member(self, name, anim_type):
message = {
"type": "animate",
"name": name,
"animation": anim_type
}
await members_queue.put(message)
Loading

0 comments on commit b45a4ee

Please sign in to comment.