refactor: split api.py monolith into 19 route modules (Phase B)
Split 3,598-line api.py into thin orchestrator (128 lines) + 19 route modules in bot/routes/: core.py (7 routes), mood.py (10), language.py (3), evil_mode.py (6), bipolar_mode.py (9), gpu.py (2), bot_actions.py (4), autonomous.py (13), profile_picture.py (26), manual_send.py (3), servers.py (6), figurines.py (5), dms.py (18), image_generation.py (4), chat.py (1), config.py (7), logging_config.py (9), voice.py (3), memory.py (10) All 146 routes verified present via test_route_split.py (149 tests). 21/21 regression tests (test_config_state.py) pass. Monolith backup: bot/api_monolith_backup.py (revert: cp it to api.py).
This commit is contained in:
207
bot/routes/voice.py
Normal file
207
bot/routes/voice.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Voice call management routes + helpers."""
|
||||
|
||||
import asyncio
|
||||
from fastapi import APIRouter, Form
|
||||
import discord
|
||||
import globals
|
||||
from utils.dm_logger import dm_logger
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('api')
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/voice/call")
|
||||
async def initiate_voice_call(user_id: str = Form(...), voice_channel_id: str = Form(...)):
|
||||
"""
|
||||
Initiate a voice call to a user.
|
||||
|
||||
Flow:
|
||||
1. Start STT and TTS containers
|
||||
2. Wait for models to load (health check)
|
||||
3. Join voice channel
|
||||
4. Send DM with invite to user
|
||||
5. Wait for user to join (30min timeout)
|
||||
6. Auto-disconnect 45s after user leaves
|
||||
"""
|
||||
logger.info(f"📞 Voice call initiated for user {user_id} in channel {voice_channel_id}")
|
||||
|
||||
# Check if bot is running
|
||||
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
|
||||
return {"success": False, "error": "Bot is not running"}
|
||||
|
||||
# Run the voice call setup in the bot's event loop
|
||||
try:
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
_initiate_voice_call_impl(user_id, voice_channel_id),
|
||||
globals.client.loop
|
||||
)
|
||||
result = future.result(timeout=90) # 90 second timeout for container warmup
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error initiating voice call: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
async def _initiate_voice_call_impl(user_id: str, voice_channel_id: str):
|
||||
"""Implementation of voice call initiation that runs in the bot's event loop."""
|
||||
from utils.container_manager import ContainerManager
|
||||
from utils.voice_manager import VoiceSessionManager
|
||||
|
||||
try:
|
||||
# Convert string IDs to integers for Discord API
|
||||
user_id_int = int(user_id)
|
||||
channel_id_int = int(voice_channel_id)
|
||||
|
||||
# Get user and channel
|
||||
user = await globals.client.fetch_user(user_id_int)
|
||||
if not user:
|
||||
return {"success": False, "error": "User not found"}
|
||||
|
||||
channel = globals.client.get_channel(channel_id_int)
|
||||
if not channel or not isinstance(channel, discord.VoiceChannel):
|
||||
return {"success": False, "error": "Voice channel not found"}
|
||||
|
||||
# Get a text channel for voice operations (use first text channel in guild)
|
||||
text_channel = None
|
||||
for ch in channel.guild.text_channels:
|
||||
if ch.permissions_for(channel.guild.me).send_messages:
|
||||
text_channel = ch
|
||||
break
|
||||
|
||||
if not text_channel:
|
||||
return {"success": False, "error": "No accessible text channel found"}
|
||||
|
||||
# Start containers
|
||||
logger.info("Starting voice containers...")
|
||||
containers_started = await ContainerManager.start_voice_containers()
|
||||
|
||||
if not containers_started:
|
||||
return {"success": False, "error": "Failed to start voice containers"}
|
||||
|
||||
# Start voice session
|
||||
logger.info(f"Starting voice session in {channel.name}")
|
||||
session_manager = VoiceSessionManager()
|
||||
|
||||
try:
|
||||
await session_manager.start_session(channel.guild.id, channel, text_channel)
|
||||
except Exception as e:
|
||||
await ContainerManager.stop_voice_containers()
|
||||
return {"success": False, "error": f"Failed to start voice session: {str(e)}"}
|
||||
|
||||
# Set up voice call tracking (use integer ID)
|
||||
session_manager.active_session.call_user_id = user_id_int
|
||||
|
||||
# Generate invite link
|
||||
invite = await channel.create_invite(
|
||||
max_age=1800, # 30 minutes
|
||||
max_uses=1,
|
||||
reason="Miku voice call"
|
||||
)
|
||||
|
||||
# Send DM to user
|
||||
try:
|
||||
# Get LLM to generate a personalized invitation message
|
||||
from utils.llm import query_llama
|
||||
|
||||
invitation_prompt = f"""You're calling {user.name} in voice chat! Generate a cute, excited message inviting them to join you.
|
||||
Keep it brief (1-2 sentences). Make it feel personal and enthusiastic!"""
|
||||
|
||||
invitation_text = await query_llama(
|
||||
user_prompt=invitation_prompt,
|
||||
user_id=user.id,
|
||||
guild_id=None,
|
||||
response_type="voice_call_invite",
|
||||
author_name=user.name
|
||||
)
|
||||
|
||||
dm_message = f"📞 **Miku is calling you! Very experimental! Speak clearly, loudly and close to the mic! Expect weirdness!** 📞\n\n{invitation_text}\n\n🎤 Join here: {invite.url}"
|
||||
|
||||
sent_message = await user.send(dm_message)
|
||||
|
||||
# Log to DM logger
|
||||
dm_logger.log_user_message(user, sent_message, is_bot_message=True)
|
||||
|
||||
logger.info(f"✓ DM sent to {user.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send DM: {e}")
|
||||
# Don't fail the whole call if DM fails
|
||||
|
||||
# Set up 30min timeout task
|
||||
session_manager.active_session.call_timeout_task = asyncio.create_task(
|
||||
_voice_call_timeout_handler(session_manager.active_session, user, channel)
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user_id,
|
||||
"channel_id": voice_channel_id,
|
||||
"invite_url": invite.url
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in voice call implementation: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
async def _voice_call_timeout_handler(voice_session, user: discord.User, channel: discord.VoiceChannel):
|
||||
"""Handle 30min timeout if user doesn't join."""
|
||||
try:
|
||||
await asyncio.sleep(1800) # 30 minutes
|
||||
|
||||
# Check if user ever joined
|
||||
if not voice_session.user_has_joined:
|
||||
logger.info(f"Voice call timeout - user {user.name} never joined")
|
||||
|
||||
# End the session (which triggers cleanup)
|
||||
from utils.voice_manager import VoiceSessionManager
|
||||
session_manager = VoiceSessionManager()
|
||||
await session_manager.end_session()
|
||||
|
||||
# Stop containers
|
||||
from utils.container_manager import ContainerManager
|
||||
await ContainerManager.stop_voice_containers()
|
||||
|
||||
# Send timeout DM
|
||||
try:
|
||||
timeout_message = "Aww, I guess you couldn't make it to voice chat... Maybe next time! 💙"
|
||||
sent_message = await user.send(timeout_message)
|
||||
|
||||
# Log to DM logger
|
||||
dm_logger.log_user_message(user, sent_message, is_bot_message=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# User joined in time, normal operation
|
||||
pass
|
||||
|
||||
|
||||
@router.get("/voice/debug-mode")
|
||||
def get_voice_debug_mode():
|
||||
"""Get current voice debug mode status"""
|
||||
return {
|
||||
"debug_mode": globals.VOICE_DEBUG_MODE
|
||||
}
|
||||
|
||||
|
||||
@router.post("/voice/debug-mode")
|
||||
def set_voice_debug_mode(enabled: bool = Form(...)):
|
||||
"""Set voice debug mode (shows transcriptions and responses in text channel)"""
|
||||
globals.VOICE_DEBUG_MODE = enabled
|
||||
logger.info(f"Voice debug mode set to: {enabled}")
|
||||
|
||||
# Persist so it survives restarts
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("voice.debug_mode", enabled, persist=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"debug_mode": enabled,
|
||||
"message": f"Voice debug mode {'enabled' if enabled else 'disabled'}"
|
||||
}
|
||||
Reference in New Issue
Block a user