britto224 commited on
Commit
50ce359
·
verified ·
1 Parent(s): 79592ba

Update src/open_llm_vtuber/server.py

Browse files
Files changed (1) hide show
  1. src/open_llm_vtuber/server.py +211 -210
src/open_llm_vtuber/server.py CHANGED
@@ -1,210 +1,211 @@
1
- """
2
- Open-LLM-VTuber Server
3
- ========================
4
- This module contains the WebSocket server for Open-LLM-VTuber, which handles
5
- the WebSocket connections, serves static files, and manages the web tool.
6
- It uses FastAPI for the server and Starlette for static file serving.
7
- """
8
-
9
- import os
10
- import shutil
11
-
12
- from fastapi import FastAPI
13
- from starlette.middleware.cors import CORSMiddleware
14
- from starlette.responses import Response
15
- from starlette.staticfiles import StaticFiles as StarletteStaticFiles
16
-
17
- from .routes import init_client_ws_route, init_webtool_routes, init_proxy_route
18
- from .service_context import ServiceContext
19
- from .config_manager.utils import Config
20
- from .openllm_vtuber_main import OpenLLMVTuberMain
21
-
22
- # Create a custom StaticFiles class that adds CORS headers
23
- class CORSStaticFiles(StarletteStaticFiles):
24
- """
25
- Static files handler that adds CORS headers to all responses.
26
- Needed because Starlette StaticFiles might bypass standard middleware.
27
- """
28
-
29
- async def get_response(self, path: str, scope):
30
- response = await super().get_response(path, scope)
31
-
32
- # Add CORS headers to all responses
33
- response.headers["Access-Control-Allow-Origin"] = "*"
34
- response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
35
- response.headers["Access-Control-Allow-Headers"] = "*"
36
-
37
- if path.endswith(".js"):
38
- response.headers["Content-Type"] = "application/javascript"
39
-
40
- return response
41
-
42
-
43
- class AvatarStaticFiles(CORSStaticFiles):
44
- """
45
- Avatar files handler with security restrictions and CORS headers
46
- """
47
-
48
- async def get_response(self, path: str, scope):
49
- allowed_extensions = (".jpg", ".jpeg", ".png", ".gif", ".svg")
50
- if not any(path.lower().endswith(ext) for ext in allowed_extensions):
51
- return Response("Forbidden file type", status_code=403)
52
- response = await super().get_response(path, scope)
53
- return response
54
-
55
-
56
- class WebSocketServer:
57
- """
58
- API server for Open-LLM-VTuber. This contains the websocket endpoint for the client, hosts the web tool, and serves static files.
59
-
60
- Creates and configures a FastAPI app, registers all routes
61
- (WebSocket, web tools, proxy) and mounts static assets with CORS.
62
-
63
- Args:
64
- config (Config): Application configuration containing system settings.
65
- default_context_cache (ServiceContext, optional):
66
- Pre‑initialized service context for sessions' service context to reference to.
67
- **If omitted, `initialize()` method needs to be called to load service context.**
68
-
69
- Notes:
70
- - If default_context_cache is omitted, call `await initialize()` to load service context cache.
71
- - Use `clean_cache()` to clear and recreate the local cache directory.
72
- """
73
-
74
- def __init__(self, config: Config, default_context_cache: ServiceContext = None):
75
- self.app = FastAPI(title="Open-LLM-VTuber Server") # Added title for clarity
76
- self.config = config
77
- self.vtuber_main = None
78
- self.is_ready = False
79
- self.default_context_cache = (
80
- default_context_cache or ServiceContext()
81
- ) # Use provided context or initialize a new empty one waiting to be loaded
82
- # It will be populated during the initialize method call
83
-
84
- # Add global CORS middleware
85
- self.app.add_middleware(
86
- CORSMiddleware,
87
- allow_origins=["*"],
88
- allow_credentials=True,
89
- allow_methods=["*"],
90
- allow_headers=["*"],
91
- )
92
-
93
-
94
- # The context will be populated during the initialize step
95
- self.app.include_router(
96
- init_client_ws_route(default_context_cache=self.default_context_cache, server_instance=self)
97
- )
98
-
99
- # Initialize and include proxy routes if proxy is enabled
100
- system_config = config.system_config
101
- if hasattr(system_config, "enable_proxy") and system_config.enable_proxy:
102
- # Construct the server URL for the proxy
103
- host = system_config.host
104
- port = system_config.port
105
- server_url = f"ws://{host}:{port}/client-ws"
106
- self.app.include_router(
107
- init_proxy_route(server_url=server_url),
108
- )
109
-
110
- # Mount cache directory first (to ensure audio file access)
111
- if not os.path.exists("cache"):
112
- os.makedirs("cache")
113
- if not os.path.exists("sing"):
114
- os.makedirs("sing/original", exist_ok=True)
115
- self.app.mount(
116
- "/cache",
117
- CORSStaticFiles(directory="cache"),
118
- name="cache",
119
- )
120
-
121
- # Mount static files with CORS-enabled handlers
122
- self.app.mount(
123
- "/live2d-models",
124
- CORSStaticFiles(directory="live2d-models"),
125
- name="live2d-models",
126
- )
127
- self.app.mount(
128
- "/bg",
129
- CORSStaticFiles(directory="backgrounds"),
130
- name="backgrounds",
131
- )
132
- self.app.mount(
133
- "/avatars",
134
- AvatarStaticFiles(directory="avatars"),
135
- name="avatars",
136
- )
137
- self.app.mount(
138
- "/sing/tracks",
139
- CORSStaticFiles(directory="sing/tracks"),
140
- name="sing_tracks"
141
- )
142
-
143
- # Mount web tool directory separately from frontend
144
- self.app.mount(
145
- "/web-tool",
146
- CORSStaticFiles(directory="web_tool", html=True),
147
- name="web_tool",
148
- )
149
-
150
- # Mount main frontend last (as catch-all)
151
- self.app.mount(
152
- "/",
153
- CORSStaticFiles(directory="frontend", html=True),
154
- name="frontend",
155
- )
156
- self.is_ready = False
157
-
158
- async def initialize(self):
159
- """Asynchronously load the service context and VTuber Main logic."""
160
- import asyncio
161
- from loguru import logger
162
- import traceback
163
-
164
- await self.default_context_cache.load_from_config(self.config)
165
-
166
- try:
167
- configs_dict = self.config.model_dump()
168
- except:
169
- configs_dict = vars(self.config)
170
-
171
- # --- ĐOẠN SỬA LỖI: Tự động map persona_prompt sang system_prompt ---
172
- if isinstance(configs_dict, dict):
173
- char_cfg = configs_dict.get('character_config', {})
174
-
175
- # 1. Nếu system_prompt trống nhưng persona_prompt có dữ liệu, thì lấy persona_prompt
176
- if char_cfg.get('system_prompt') is None:
177
- char_cfg['system_prompt'] = char_cfg.get('persona_prompt', "")
178
-
179
- # 2. Đảm bảo các chuỗi quan trọng không bao giờ là None để tránh lỗi +=
180
- keys_to_fix = [
181
- 'system_prompt', 'personality', 'instruction',
182
- 'system_with_tools', 'name', 'background'
183
- ]
184
- for key in keys_to_fix:
185
- if key in char_cfg and char_cfg[key] is None:
186
- char_cfg[key] = ""
187
- # Kiểm tra cả ở cấp ngoài nếu có
188
- if key in configs_dict and configs_dict[key] is None:
189
- configs_dict[key] = ""
190
- # -----------------------------------------------------------------
191
-
192
- try:
193
- self.vtuber_main = OpenLLMVTuberMain(
194
- configs=configs_dict,
195
- loop=asyncio.get_running_loop()
196
- )
197
- self.is_ready = True
198
- logger.info("✅ VTuber Main Logic initialized and Ready.")
199
- except Exception as e:
200
- self.is_ready = False
201
- logger.error(f"❌ Failed to initialize VTuber Main: {e}")
202
- logger.error(traceback.format_exc())
203
-
204
- @staticmethod
205
- def clean_cache():
206
- """Clean the cache directory by removing and recreating it."""
207
- cache_dir = "cache"
208
- if os.path.exists(cache_dir):
209
- shutil.rmtree(cache_dir)
210
- os.makedirs(cache_dir)
 
 
1
+ """
2
+ Open-LLM-VTuber Server
3
+ ========================
4
+ This module contains the WebSocket server for Open-LLM-VTuber, which handles
5
+ the WebSocket connections, serves static files, and manages the web tool.
6
+ It uses FastAPI for the server and Starlette for static file serving.
7
+ """
8
+
9
+ import os
10
+ import shutil
11
+
12
+ from fastapi import FastAPI
13
+ from starlette.middleware.cors import CORSMiddleware
14
+ from starlette.responses import Response
15
+ from starlette.staticfiles import StaticFiles as StarletteStaticFiles
16
+
17
+ from .routes import init_client_ws_route, init_webtool_routes, init_proxy_route
18
+ from .service_context import ServiceContext
19
+ from .config_manager.utils import Config
20
+ from .openllm_vtuber_main import OpenLLMVTuberMain
21
+
22
+ # Create a custom StaticFiles class that adds CORS headers
23
+ class CORSStaticFiles(StarletteStaticFiles):
24
+ """
25
+ Static files handler that adds CORS headers to all responses.
26
+ Needed because Starlette StaticFiles might bypass standard middleware.
27
+ """
28
+
29
+ async def get_response(self, path: str, scope):
30
+ response = await super().get_response(path, scope)
31
+
32
+ # Add CORS headers to all responses
33
+ response.headers["Access-Control-Allow-Origin"] = "*"
34
+ response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
35
+ response.headers["Access-Control-Allow-Headers"] = "*"
36
+
37
+ if path.endswith(".js"):
38
+ response.headers["Content-Type"] = "application/javascript"
39
+
40
+ return response
41
+
42
+
43
+ class AvatarStaticFiles(CORSStaticFiles):
44
+ """
45
+ Avatar files handler with security restrictions and CORS headers
46
+ """
47
+
48
+ async def get_response(self, path: str, scope):
49
+ allowed_extensions = (".jpg", ".jpeg", ".png", ".gif", ".svg")
50
+ if not any(path.lower().endswith(ext) for ext in allowed_extensions):
51
+ return Response("Forbidden file type", status_code=403)
52
+ response = await super().get_response(path, scope)
53
+ return response
54
+
55
+
56
+ class WebSocketServer:
57
+ """
58
+ API server for Open-LLM-VTuber. This contains the websocket endpoint for the client, hosts the web tool, and serves static files.
59
+
60
+ Creates and configures a FastAPI app, registers all routes
61
+ (WebSocket, web tools, proxy) and mounts static assets with CORS.
62
+
63
+ Args:
64
+ config (Config): Application configuration containing system settings.
65
+ default_context_cache (ServiceContext, optional):
66
+ Pre‑initialized service context for sessions' service context to reference to.
67
+ **If omitted, `initialize()` method needs to be called to load service context.**
68
+
69
+ Notes:
70
+ - If default_context_cache is omitted, call `await initialize()` to load service context cache.
71
+ - Use `clean_cache()` to clear and recreate the local cache directory.
72
+ """
73
+
74
+ def __init__(self, config: Config, default_context_cache: ServiceContext = None):
75
+ self.app = FastAPI(title="Open-LLM-VTuber Server") # Added title for clarity
76
+ self.config = config
77
+ self.vtuber_main = None
78
+ self.is_ready = False
79
+ self.default_context_cache = (
80
+ default_context_cache or ServiceContext()
81
+ ) # Use provided context or initialize a new empty one waiting to be loaded
82
+ # It will be populated during the initialize method call
83
+
84
+ # Add global CORS middleware
85
+ self.app.add_middleware(
86
+ CORSMiddleware,
87
+ allow_origins=["*"],
88
+ allow_credentials=True,
89
+ allow_methods=["*"],
90
+ allow_headers=["*"],
91
+ )
92
+
93
+
94
+ # The context will be populated during the initialize step
95
+ self.app.include_router(
96
+ init_client_ws_route(default_context_cache=self.default_context_cache, server_instance=self)
97
+ )
98
+
99
+ # Initialize and include proxy routes if proxy is enabled
100
+ system_config = config.system_config
101
+ if hasattr(system_config, "enable_proxy") and system_config.enable_proxy:
102
+ # Construct the server URL for the proxy
103
+ host = system_config.host
104
+ port = system_config.port
105
+ server_url = f"ws://{host}:{port}/client-ws"
106
+ self.app.include_router(
107
+ init_proxy_route(server_url=server_url),
108
+ )
109
+
110
+ # Mount cache directory first (to ensure audio file access)
111
+ if not os.path.exists("cache"):
112
+ os.makedirs("cache")
113
+ # Always ensure all required sing subdirectories exist
114
+ os.makedirs("sing/original", exist_ok=True)
115
+ os.makedirs("sing/tracks", exist_ok=True)
116
+ self.app.mount(
117
+ "/cache",
118
+ CORSStaticFiles(directory="cache"),
119
+ name="cache",
120
+ )
121
+
122
+ # Mount static files with CORS-enabled handlers
123
+ self.app.mount(
124
+ "/live2d-models",
125
+ CORSStaticFiles(directory="live2d-models"),
126
+ name="live2d-models",
127
+ )
128
+ self.app.mount(
129
+ "/bg",
130
+ CORSStaticFiles(directory="backgrounds"),
131
+ name="backgrounds",
132
+ )
133
+ self.app.mount(
134
+ "/avatars",
135
+ AvatarStaticFiles(directory="avatars"),
136
+ name="avatars",
137
+ )
138
+ self.app.mount(
139
+ "/sing/tracks",
140
+ CORSStaticFiles(directory="sing/tracks"),
141
+ name="sing_tracks"
142
+ )
143
+
144
+ # Mount web tool directory separately from frontend
145
+ self.app.mount(
146
+ "/web-tool",
147
+ CORSStaticFiles(directory="web_tool", html=True),
148
+ name="web_tool",
149
+ )
150
+
151
+ # Mount main frontend last (as catch-all)
152
+ self.app.mount(
153
+ "/",
154
+ CORSStaticFiles(directory="frontend", html=True),
155
+ name="frontend",
156
+ )
157
+ self.is_ready = False
158
+
159
+ async def initialize(self):
160
+ """Asynchronously load the service context and VTuber Main logic."""
161
+ import asyncio
162
+ from loguru import logger
163
+ import traceback
164
+
165
+ await self.default_context_cache.load_from_config(self.config)
166
+
167
+ try:
168
+ configs_dict = self.config.model_dump()
169
+ except:
170
+ configs_dict = vars(self.config)
171
+
172
+ # --- ĐOẠN SỬA LỖI: Tự động map persona_prompt sang system_prompt ---
173
+ if isinstance(configs_dict, dict):
174
+ char_cfg = configs_dict.get('character_config', {})
175
+
176
+ # 1. Nếu system_prompt trống nhưng persona_prompt có dữ liệu, thì lấy persona_prompt
177
+ if char_cfg.get('system_prompt') is None:
178
+ char_cfg['system_prompt'] = char_cfg.get('persona_prompt', "")
179
+
180
+ # 2. Đảm bảo các chuỗi quan trọng không bao giờ là None để tránh lỗi +=
181
+ keys_to_fix = [
182
+ 'system_prompt', 'personality', 'instruction',
183
+ 'system_with_tools', 'name', 'background'
184
+ ]
185
+ for key in keys_to_fix:
186
+ if key in char_cfg and char_cfg[key] is None:
187
+ char_cfg[key] = ""
188
+ # Kiểm tra cả cấp ngoài nếu có
189
+ if key in configs_dict and configs_dict[key] is None:
190
+ configs_dict[key] = ""
191
+ # -----------------------------------------------------------------
192
+
193
+ try:
194
+ self.vtuber_main = OpenLLMVTuberMain(
195
+ configs=configs_dict,
196
+ loop=asyncio.get_running_loop()
197
+ )
198
+ self.is_ready = True
199
+ logger.info("✅ VTuber Main Logic initialized and Ready.")
200
+ except Exception as e:
201
+ self.is_ready = False
202
+ logger.error(f"❌ Failed to initialize VTuber Main: {e}")
203
+ logger.error(traceback.format_exc())
204
+
205
+ @staticmethod
206
+ def clean_cache():
207
+ """Clean the cache directory by removing and recreating it."""
208
+ cache_dir = "cache"
209
+ if os.path.exists(cache_dir):
210
+ shutil.rmtree(cache_dir)
211
+ os.makedirs(cache_dir)