| """
|
| Open-LLM-VTuber Server
|
| ========================
|
| This module contains the WebSocket server for Open-LLM-VTuber, which handles
|
| the WebSocket connections, serves static files, and manages the web tool.
|
| It uses FastAPI for the server and Starlette for static file serving.
|
| """
|
|
|
| import os
|
| import shutil
|
|
|
| from fastapi import FastAPI
|
| from starlette.middleware.cors import CORSMiddleware
|
| from starlette.responses import Response
|
| from starlette.staticfiles import StaticFiles as StarletteStaticFiles
|
|
|
| from .routes import init_client_ws_route, init_webtool_routes, init_proxy_route
|
| from .service_context import ServiceContext
|
| from .config_manager.utils import Config
|
| from .openllm_vtuber_main import OpenLLMVTuberMain
|
|
|
|
|
| class CORSStaticFiles(StarletteStaticFiles):
|
| """
|
| Static files handler that adds CORS headers to all responses.
|
| Needed because Starlette StaticFiles might bypass standard middleware.
|
| """
|
|
|
| async def get_response(self, path: str, scope):
|
| response = await super().get_response(path, scope)
|
|
|
|
|
| response.headers["Access-Control-Allow-Origin"] = "*"
|
| response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
| response.headers["Access-Control-Allow-Headers"] = "*"
|
|
|
| if path.endswith(".js"):
|
| response.headers["Content-Type"] = "application/javascript"
|
|
|
| return response
|
|
|
|
|
| class AvatarStaticFiles(CORSStaticFiles):
|
| """
|
| Avatar files handler with security restrictions and CORS headers
|
| """
|
|
|
| async def get_response(self, path: str, scope):
|
| allowed_extensions = (".jpg", ".jpeg", ".png", ".gif", ".svg")
|
| if not any(path.lower().endswith(ext) for ext in allowed_extensions):
|
| return Response("Forbidden file type", status_code=403)
|
| response = await super().get_response(path, scope)
|
| return response
|
|
|
|
|
| class WebSocketServer:
|
| """
|
| API server for Open-LLM-VTuber. This contains the websocket endpoint for the client, hosts the web tool, and serves static files.
|
|
|
| Creates and configures a FastAPI app, registers all routes
|
| (WebSocket, web tools, proxy) and mounts static assets with CORS.
|
|
|
| Args:
|
| config (Config): Application configuration containing system settings.
|
| default_context_cache (ServiceContext, optional):
|
| Pre‑initialized service context for sessions' service context to reference to.
|
| **If omitted, `initialize()` method needs to be called to load service context.**
|
|
|
| Notes:
|
| - If default_context_cache is omitted, call `await initialize()` to load service context cache.
|
| - Use `clean_cache()` to clear and recreate the local cache directory.
|
| """
|
|
|
| def __init__(self, config: Config, default_context_cache: ServiceContext = None):
|
| self.app = FastAPI(title="Open-LLM-VTuber Server")
|
| self.config = config
|
| self.vtuber_main = None
|
| self.is_ready = False
|
| self.default_context_cache = (
|
| default_context_cache or ServiceContext()
|
| )
|
|
|
|
|
|
|
| self.app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
|
|
| self.app.include_router(
|
| init_client_ws_route(default_context_cache=self.default_context_cache, server_instance=self)
|
| )
|
|
|
|
|
| system_config = config.system_config
|
| if hasattr(system_config, "enable_proxy") and system_config.enable_proxy:
|
|
|
| host = system_config.host
|
| port = system_config.port
|
| server_url = f"ws://{host}:{port}/client-ws"
|
| self.app.include_router(
|
| init_proxy_route(server_url=server_url),
|
| )
|
|
|
|
|
| if not os.path.exists("cache"):
|
| os.makedirs("cache")
|
|
|
| os.makedirs("sing/original", exist_ok=True)
|
| os.makedirs("sing/tracks", exist_ok=True)
|
| self.app.mount(
|
| "/cache",
|
| CORSStaticFiles(directory="cache"),
|
| name="cache",
|
| )
|
|
|
|
|
| self.app.mount(
|
| "/live2d-models",
|
| CORSStaticFiles(directory="live2d-models"),
|
| name="live2d-models",
|
| )
|
| self.app.mount(
|
| "/bg",
|
| CORSStaticFiles(directory="backgrounds"),
|
| name="backgrounds",
|
| )
|
| self.app.mount(
|
| "/avatars",
|
| AvatarStaticFiles(directory="avatars"),
|
| name="avatars",
|
| )
|
| self.app.mount(
|
| "/sing/tracks",
|
| CORSStaticFiles(directory="sing/tracks"),
|
| name="sing_tracks"
|
| )
|
|
|
|
|
| self.app.mount(
|
| "/web-tool",
|
| CORSStaticFiles(directory="web_tool", html=True),
|
| name="web_tool",
|
| )
|
|
|
|
|
| self.app.mount(
|
| "/",
|
| CORSStaticFiles(directory="frontend", html=True),
|
| name="frontend",
|
| )
|
| self.is_ready = False
|
|
|
| async def initialize(self):
|
| """Asynchronously load the service context and VTuber Main logic."""
|
| import asyncio
|
| from loguru import logger
|
| import traceback
|
|
|
| await self.default_context_cache.load_from_config(self.config)
|
|
|
| try:
|
| configs_dict = self.config.model_dump()
|
| except:
|
| configs_dict = vars(self.config)
|
|
|
|
|
| if isinstance(configs_dict, dict):
|
| char_cfg = configs_dict.get('character_config', {})
|
|
|
|
|
| if char_cfg.get('system_prompt') is None:
|
| char_cfg['system_prompt'] = char_cfg.get('persona_prompt', "")
|
|
|
|
|
| keys_to_fix = [
|
| 'system_prompt', 'personality', 'instruction',
|
| 'system_with_tools', 'name', 'background'
|
| ]
|
| for key in keys_to_fix:
|
| if key in char_cfg and char_cfg[key] is None:
|
| char_cfg[key] = ""
|
|
|
| if key in configs_dict and configs_dict[key] is None:
|
| configs_dict[key] = ""
|
|
|
|
|
| try:
|
| self.vtuber_main = OpenLLMVTuberMain(
|
| configs=configs_dict,
|
| loop=asyncio.get_running_loop()
|
| )
|
| self.is_ready = True
|
| logger.info("✅ VTuber Main Logic initialized and Ready.")
|
| except Exception as e:
|
| self.is_ready = False
|
| logger.error(f"❌ Failed to initialize VTuber Main: {e}")
|
| logger.error(traceback.format_exc())
|
|
|
| @staticmethod
|
| def clean_cache():
|
| """Clean the cache directory by removing and recreating it."""
|
| cache_dir = "cache"
|
| if os.path.exists(cache_dir):
|
| shutil.rmtree(cache_dir)
|
| os.makedirs(cache_dir)
|
|
|