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:
205
bot/utils/container_manager.py
Normal file
205
bot/utils/container_manager.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# container_manager.py
|
||||
"""
|
||||
Manages Docker containers for STT and TTS services.
|
||||
Handles startup, shutdown, and warmup detection.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import aiohttp
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('container_manager')
|
||||
|
||||
class ContainerManager:
|
||||
"""Manages STT and TTS Docker containers."""
|
||||
|
||||
# Container names from docker-compose.yml
|
||||
STT_CONTAINER = "miku-stt"
|
||||
TTS_CONTAINER = "miku-rvc-api"
|
||||
|
||||
# Warmup check endpoints
|
||||
STT_HEALTH_URL = "http://miku-stt:8767/health" # HTTP health check endpoint
|
||||
TTS_HEALTH_URL = "http://miku-rvc-api:8765/health"
|
||||
|
||||
# Warmup timeouts
|
||||
STT_WARMUP_TIMEOUT = 30 # seconds
|
||||
TTS_WARMUP_TIMEOUT = 60 # seconds (RVC takes longer)
|
||||
|
||||
@classmethod
|
||||
async def start_voice_containers(cls) -> bool:
|
||||
"""
|
||||
Start STT and TTS containers and wait for them to warm up.
|
||||
|
||||
Returns:
|
||||
bool: True if both containers started and warmed up successfully
|
||||
"""
|
||||
logger.info("🚀 Starting voice chat containers...")
|
||||
|
||||
try:
|
||||
# Start STT container using docker start (assumes container exists)
|
||||
logger.info(f"Starting {cls.STT_CONTAINER}...")
|
||||
result = subprocess.run(
|
||||
["docker", "start", cls.STT_CONTAINER],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to start {cls.STT_CONTAINER}: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info(f"✓ {cls.STT_CONTAINER} started")
|
||||
|
||||
# Start TTS container
|
||||
logger.info(f"Starting {cls.TTS_CONTAINER}...")
|
||||
result = subprocess.run(
|
||||
["docker", "start", cls.TTS_CONTAINER],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to start {cls.TTS_CONTAINER}: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info(f"✓ {cls.TTS_CONTAINER} started")
|
||||
|
||||
# Wait for warmup
|
||||
logger.info("⏳ Waiting for containers to warm up...")
|
||||
|
||||
stt_ready = await cls._wait_for_stt_warmup()
|
||||
if not stt_ready:
|
||||
logger.error("STT failed to warm up")
|
||||
return False
|
||||
|
||||
tts_ready = await cls._wait_for_tts_warmup()
|
||||
if not tts_ready:
|
||||
logger.error("TTS failed to warm up")
|
||||
return False
|
||||
|
||||
logger.info("✅ All voice containers ready!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting voice containers: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def stop_voice_containers(cls) -> bool:
|
||||
"""
|
||||
Stop STT and TTS containers.
|
||||
|
||||
Returns:
|
||||
bool: True if containers stopped successfully
|
||||
"""
|
||||
logger.info("🛑 Stopping voice chat containers...")
|
||||
|
||||
try:
|
||||
# Stop both containers
|
||||
result = subprocess.run(
|
||||
["docker", "stop", cls.STT_CONTAINER, cls.TTS_CONTAINER],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to stop containers: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info("✓ Voice containers stopped")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping voice containers: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def _wait_for_stt_warmup(cls) -> bool:
|
||||
"""
|
||||
Wait for STT container to be ready by checking health endpoint.
|
||||
|
||||
Returns:
|
||||
bool: True if STT is ready within timeout
|
||||
"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
while (asyncio.get_event_loop().time() - start_time) < cls.STT_WARMUP_TIMEOUT:
|
||||
try:
|
||||
async with session.get(cls.STT_HEALTH_URL, timeout=aiohttp.ClientTimeout(total=2)) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
if data.get("status") == "ready" and data.get("warmed_up"):
|
||||
logger.info("✓ STT is ready")
|
||||
return True
|
||||
except Exception:
|
||||
# Not ready yet, wait and retry
|
||||
pass
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.error(f"STT warmup timeout ({cls.STT_WARMUP_TIMEOUT}s)")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def _wait_for_tts_warmup(cls) -> bool:
|
||||
"""
|
||||
Wait for TTS container to be ready by checking health endpoint.
|
||||
|
||||
Returns:
|
||||
bool: True if TTS is ready within timeout
|
||||
"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
while (asyncio.get_event_loop().time() - start_time) < cls.TTS_WARMUP_TIMEOUT:
|
||||
try:
|
||||
async with session.get(cls.TTS_HEALTH_URL, timeout=aiohttp.ClientTimeout(total=2)) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
# RVC API returns "status": "healthy", not "ready"
|
||||
status_ok = data.get("status") in ["ready", "healthy"]
|
||||
if status_ok and data.get("warmed_up"):
|
||||
logger.info("✓ TTS is ready")
|
||||
return True
|
||||
except Exception:
|
||||
# Not ready yet, wait and retry
|
||||
pass
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.error(f"TTS warmup timeout ({cls.TTS_WARMUP_TIMEOUT}s)")
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def are_containers_running(cls) -> tuple[bool, bool]:
|
||||
"""
|
||||
Check if STT and TTS containers are currently running.
|
||||
|
||||
Returns:
|
||||
tuple[bool, bool]: (stt_running, tts_running)
|
||||
"""
|
||||
try:
|
||||
# Check STT
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "-f", "{{.State.Running}}", cls.STT_CONTAINER],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
stt_running = result.returncode == 0 and result.stdout.strip() == "true"
|
||||
|
||||
# Check TTS
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "-f", "{{.State.Running}}", cls.TTS_CONTAINER],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
tts_running = result.returncode == 0 and result.stdout.strip() == "true"
|
||||
|
||||
return (stt_running, tts_running)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking container status: {e}")
|
||||
return (False, False)
|
||||
Reference in New Issue
Block a user