import os
import sys
import atexit
import asyncio
import argparse
import subprocess
from pathlib import Path
import tomli
import uvicorn
from loguru import logger
from upgrade_codes.upgrade_manager import UpgradeManager
from src.open_llm_vtuber.server import WebSocketServer
from src.open_llm_vtuber.config_manager import Config, read_yaml, validate_config
os.environ["HF_HOME"] = str(Path(__file__).parent / "models")
os.environ["MODELSCOPE_CACHE"] = str(Path(__file__).parent / "models")
upgrade_manager = UpgradeManager()
def load_env_file(env_path: str = ".env") -> None:
"""Load key-value pairs from a .env file into environment variables.
Existing environment variables are preserved and not overridden.
"""
path = Path(env_path)
if not path.exists():
return
loaded_count = 0
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
continue
if (
len(value) >= 2
and value[0] == value[-1]
and value[0] in {'"', "'"}
):
value = value[1:-1]
if key not in os.environ:
os.environ[key] = value
loaded_count += 1
logger.info(f"Loaded {loaded_count} variables from {path}")
def get_version() -> str:
with open("pyproject.toml", "rb") as f:
pyproject = tomli.load(f)
return pyproject["project"]["version"]
def init_logger(console_log_level: str = "INFO") -> None:
logger.remove()
# Console output
logger.add(
sys.stderr,
level=console_log_level,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}",
colorize=True,
)
# File output
logger.add(
"logs/debug_{time:YYYY-MM-DD}.log",
rotation="10 MB",
retention="30 days",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message} | {extra}",
backtrace=True,
diagnose=True,
)
def check_frontend_submodule(lang=None):
"""
Check if the frontend submodule is initialized. If not, attempt to initialize it.
If initialization fails, log an error message.
"""
if lang is None:
lang = upgrade_manager.lang
frontend_path = Path(__file__).parent / "frontend" / "index.html"
if not frontend_path.exists():
if lang == "id":
logger.warning("Tidak ditemukan submodul front-end. Mencoba menginisialisasi submodul...")
else:
logger.warning(
"Frontend submodule not found, attempting to initialize submodules..."
)
try:
subprocess.run(
["git", "submodule", "update", "--init", "--recursive"], check=True
)
if frontend_path.exists():
if lang == "id":
logger.info("👍 Submodul front-end (dan submodul lainnya) berhasil diinisialisasi.")
else:
logger.info(
"👍 Frontend submodule (and other submodules) initialized successfully."
)
else:
if lang == "id":
logger.critical(
'Gagal menginisialisasi submodul.\nAnda mungkin akan melihat pesan kesalahan {{"detail":"Not Found"}} di browser. Silakan periksa panduan cepat memulai dan halaman masalah umum untuk informasi lebih lanjut.'
)
logger.error(
"Did you manually change or delete the `frontend` folder? \n"
+ "It's a Git submodule — you shouldn't modify it directly. \n"
+ "If you did, discard your changes with `git restore frontend`, then try again.\n"
)
else:
logger.critical(
'Failed to initialize submodules. \nYou might see {{"detail":"Not Found"}} in your browser. Please check our quick start guide and common issues page from our documentation.'
)
logger.error(
"Frontend files are still missing after submodule initialization.\n"
+ "Did you manually change or delete the `frontend` folder? \n"
+ "It's a Git submodule — you shouldn't modify it directly. \n"
+ "If you did, discard your changes with `git restore frontend`, then try again.\n"
)
except Exception as e:
if lang == "id":
logger.critical(
f'Gagal menginisialisasi submodul: {e}。\nAnda mungkin mengalami masalah jaringan dengan GitHub. Anda mungkin akan melihat pesan kesalahan {{"detail":"Not Found"}} di browser. Silakan periksa panduan cepat memulai dan halaman masalah umum untuk informasi lebih lanjut.\n'
)
else:
logger.critical(
f'Failed to initialize submodules: {e}. \nYou might see {{"detail":"Not Found"}} in your browser. Please check our quick start guide and common issues page from our documentation.\n'
)
def parse_args():
parser = argparse.ArgumentParser(description="Open-LLM-VTuber Server")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
parser.add_argument(
"--hf_mirror", action="store_true", help="Use Hugging Face mirror"
)
return parser.parse_args()
@logger.catch
def run(console_log_level: str):
init_logger(console_log_level)
logger.info(f"Open-LLM-VTuber, version v{get_version()}")
# Get selected language
lang = upgrade_manager.lang
# Check if the frontend submodule is initialized
check_frontend_submodule(lang)
# Sync user config with default config
try:
upgrade_manager.sync_user_config()
except Exception as e:
logger.error(f"Error syncing user config: {e}")
atexit.register(WebSocketServer.clean_cache)
# Load configurations from yaml file
config: Config = validate_config(read_yaml("conf.yaml"))
server_config = config.system_config
if server_config.enable_proxy:
logger.info("Proxy mode enabled - /proxy-ws endpoint will be available")
# Initialize the WebSocket server (synchronous part)
server = WebSocketServer(config=config)
# Perform asynchronous initialization (loading context, etc.)
logger.info("Initializing server context...")
try:
asyncio.run(server.initialize())
logger.info("Server context initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize server context: {e}")
sys.exit(1) # Exit if initialization fails
# Run the Uvicorn server
logger.info(f"Starting server on {server_config.host}:{server_config.port}")
uvicorn.run(
app=server.app,
host=server_config.host,
port=server_config.port,
log_level=console_log_level.lower(),
)
if __name__ == "__main__":
load_env_file()
args = parse_args()
console_log_level = "DEBUG" if args.verbose else "INFO"
if args.verbose:
logger.info("Running in verbose mode")
else:
logger.info(
"Running in standard mode. For detailed debug logs, use: uv run run_server.py --verbose"
)
if args.hf_mirror:
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
run(console_log_level=console_log_level)