Implemented experimental real production ready voice chat, relegated old flow to voice debug mode. New Web UI panel for Voice Chat.

This commit is contained in:
2026-01-20 23:06:17 +02:00
parent 362108f4b0
commit 2934efba22
31 changed files with 5408 additions and 357 deletions

View File

@@ -87,6 +87,13 @@ def get_current_gpu_url():
app = FastAPI()
# ========== Global Exception Handler ==========
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Catch all unhandled exceptions and log them properly."""
logger.error(f"Unhandled exception on {request.method} {request.url.path}: {exc}", exc_info=True)
return {"success": False, "error": "Internal server error"}
# ========== Logging Middleware ==========
@app.middleware("http")
async def log_requests(request: Request, call_next):
@@ -2522,6 +2529,217 @@ async def get_log_file(component: str, lines: int = 100):
logger.error(f"Failed to read log file for {component}: {e}")
return {"success": False, "error": str(e)}
# ============================================================================
# Voice Call Management
# ============================================================================
@app.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 warmup
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
await dm_logger.log_message(
user_id=user.id,
user_name=user.name,
message_content=dm_message,
direction="outgoing",
message_id=sent_message.id,
attachments=[],
response_type="voice_call_invite"
)
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: 'VoiceSession', 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
await dm_logger.log_message(
user_id=user.id,
user_name=user.name,
message_content=timeout_message,
direction="outgoing",
message_id=sent_message.id,
attachments=[],
response_type="voice_call_timeout"
)
except:
pass
except asyncio.CancelledError:
# User joined in time, normal operation
pass
@app.get("/voice/debug-mode")
def get_voice_debug_mode():
"""Get current voice debug mode status"""
return {
"debug_mode": globals.VOICE_DEBUG_MODE
}
@app.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}")
return {
"status": "ok",
"debug_mode": enabled,
"message": f"Voice debug mode {'enabled' if enabled else 'disabled'}"
}
def start_api():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3939)