11# -*- coding: utf-8 -*-
2+ import sys , os
3+ sys .path .insert (0 , os .path .dirname (os .path .abspath (__file__ )))
24import asyncio
35import json
46import traceback
5- import sys
67import uuid
78import logging
89from datetime import datetime
10+ import webbrowser
911
1012from fastapi import FastAPI , WebSocket , WebSocketDisconnect , Request , File , UploadFile , Form
1113from fastapi .staticfiles import StaticFiles
1517from utils .preferences import load_user_preferences , update_model_preferences , validate_model_preferences , move_model_to_top
1618from utils .frontend_utils import find_models , load_characters , save_characters
1719from multiprocessing import Process , Queue , Event
18- import os
1920import atexit
2021import dashscope
2122from dashscope .audio .tts_v2 import VoiceEnrollmentService
@@ -77,6 +78,20 @@ def cleanup():
7778# relative to where the server is running (gemini-live-app/).
7879app .mount ("/static" , StaticFiles (directory = "static" ), name = "static" )
7980
81+ # 使用 FastAPI 的 app.state 来管理启动配置
82+ def get_start_config ():
83+ """从 app.state 获取启动配置"""
84+ if hasattr (app .state , 'start_config' ):
85+ return app .state .start_config
86+ return {
87+ "browser_mode_enabled" : False ,
88+ "browser_page" : "chara_manager" ,
89+ 'server' : None
90+ }
91+
92+ def set_start_config (config ):
93+ """设置启动配置到 app.state"""
94+ app .state .start_config = config
8095
8196# *** CORRECTED ROOT PATH TO SERVE index.html ***
8297@app .get ("/" , response_class = HTMLResponse )
@@ -147,6 +162,61 @@ async def set_preferred_model(request: Request):
147162 except Exception as e :
148163 return {"success" : False , "error" : str (e )}
149164
165+ @app .get ("/api/config/core_api" )
166+ async def get_core_config ():
167+ """获取核心配置(API Key)"""
168+ try :
169+ # 尝试从core_config.json读取
170+ try :
171+ with open ('./config/core_config.json' , 'r' , encoding = 'utf-8' ) as f :
172+ core_cfg = json .load (f )
173+ api_key = core_cfg .get ('coreApiKey' , '' )
174+ except FileNotFoundError :
175+ # 如果文件不存在,返回当前内存中的CORE_API_KEY
176+ api_key = CORE_API_KEY
177+
178+ return {
179+ "api_key" : api_key ,
180+ "success" : True
181+ }
182+ except Exception as e :
183+ return {
184+ "success" : False ,
185+ "error" : str (e )
186+ }
187+
188+
189+ @app .post ("/api/config/core_api" )
190+ async def update_core_config (request : Request ):
191+ """更新核心配置(API Key)"""
192+ try :
193+ data = await request .json ()
194+ if not data :
195+ return {"success" : False , "error" : "无效的数据" }
196+
197+ if 'coreApiKey' not in data :
198+ return {"success" : False , "error" : "缺少coreApiKey字段" }
199+
200+ api_key = data ['coreApiKey' ]
201+ if api_key is None :
202+ return {"success" : False , "error" : "API Key不能为null" }
203+
204+ if not isinstance (api_key , str ):
205+ return {"success" : False , "error" : "API Key必须是字符串类型" }
206+
207+ api_key = api_key .strip ()
208+ if not api_key :
209+ return {"success" : False , "error" : "API Key不能为空" }
210+
211+ # 保存到core_config.json
212+ core_cfg = {"coreApiKey" : api_key }
213+ with open ('./config/core_config.json' , 'w' , encoding = 'utf-8' ) as f :
214+ json .dump (core_cfg , f , indent = 2 , ensure_ascii = False )
215+
216+ return {"success" : True , "message" : "API Key已保存" }
217+ except Exception as e :
218+ return {"success" : False , "error" : str (e )}
219+
150220
151221@app .on_event ("startup" )
152222async def startup_event ():
@@ -161,6 +231,29 @@ async def startup_event():
161231 )
162232 sync_process [k ].start ()
163233 logger .info (f"同步连接器进程已启动 (PID: { sync_process [k ].pid } )" )
234+
235+ # 如果启用了浏览器模式,在服务器启动完成后打开浏览器
236+ current_config = get_start_config ()
237+ print (f"启动配置: { current_config } " )
238+ if current_config ['browser_mode_enabled' ]:
239+ import threading
240+
241+ def launch_browser_delayed ():
242+ # 等待一小段时间确保服务器完全启动
243+ import time
244+ time .sleep (1 )
245+ # 从 app.state 获取配置
246+ config = get_start_config ()
247+ url = f"http://127.0.0.1:{ MAIN_SERVER_PORT } /{ config ['browser_page' ]} "
248+ try :
249+ webbrowser .open (url )
250+ logger .info (f"服务器启动完成,已打开浏览器访问: { url } " )
251+ except Exception as e :
252+ logger .error (f"打开浏览器失败: { e } " )
253+
254+ # 在独立线程中启动浏览器
255+ t = threading .Thread (target = launch_browser_delayed , daemon = True )
256+ t .start ()
164257
165258
166259@app .on_event ("shutdown" )
@@ -175,6 +268,19 @@ async def shutdown_event():
175268 if sync_process [k ].is_alive ():
176269 sync_process [k ].terminate () # 如果超时,强制终止
177270 logger .info ("同步连接器进程已停止" )
271+
272+ # 向memory_server发送关闭信号
273+ try :
274+ import requests
275+ from config import MEMORY_SERVER_PORT
276+ shutdown_url = f"http://localhost:{ MEMORY_SERVER_PORT } /shutdown"
277+ response = requests .post (shutdown_url , timeout = 2 )
278+ if response .status_code == 200 :
279+ logger .info ("已向memory_server发送关闭信号" )
280+ else :
281+ logger .warning (f"向memory_server发送关闭信号失败,状态码: { response .status_code } " )
282+ except Exception as e :
283+ logger .warning (f"向memory_server发送关闭信号时出错: { e } " )
178284
179285
180286@app .websocket ("/ws/{lanlan_name}" )
@@ -250,6 +356,12 @@ async def chara_manager(request: Request):
250356async def voice_clone_page (request : Request , lanlan_name : str = "" ):
251357 return templates .TemplateResponse ("templates/voice_clone.html" , {"request" : request , "lanlan_name" : lanlan_name })
252358
359+ @app .get ("/api_key" , response_class = HTMLResponse )
360+ async def api_key_settings (request : Request ):
361+ """API Key 设置页面"""
362+ return templates .TemplateResponse ("templates/api_key_settings.html" , {
363+ "request" : request
364+ })
253365
254366@app .get ('/api/characters' )
255367async def get_characters ():
@@ -324,6 +436,33 @@ async def update_catgirl_voice_id(name: str, request: Request):
324436 save_characters (characters )
325437 return {"success" : True }
326438
439+ @app .post ('/api/characters/clear_voice_ids' )
440+ async def clear_voice_ids ():
441+ """清除所有角色的本地Voice ID记录"""
442+ try :
443+ characters = load_characters ()
444+ cleared_count = 0
445+
446+ # 清除所有猫娘的voice_id
447+ if '猫娘' in characters :
448+ for name in characters ['猫娘' ]:
449+ if 'voice_id' in characters ['猫娘' ][name ] and characters ['猫娘' ][name ]['voice_id' ]:
450+ characters ['猫娘' ][name ]['voice_id' ] = ''
451+ cleared_count += 1
452+
453+ save_characters (characters )
454+
455+ return JSONResponse ({
456+ 'success' : True ,
457+ 'message' : f'已清除 { cleared_count } 个角色的Voice ID记录' ,
458+ 'cleared_count' : cleared_count
459+ })
460+ except Exception as e :
461+ return JSONResponse ({
462+ 'success' : False ,
463+ 'error' : f'清除Voice ID记录时出错: { str (e )} '
464+ }, status_code = 500 )
465+
327466@app .post ('/api/tmpfiles_voice_clone' )
328467async def tmpfiles_voice_clone (file : UploadFile = File (...), prefix : str = Form (...)):
329468 import os
@@ -374,6 +513,49 @@ async def delete_catgirl(name: str):
374513 save_characters (characters )
375514 return {"success" : True }
376515
516+ @app .post ('/api/beacon/shutdown' )
517+ async def beacon_shutdown ():
518+ """Beacon API for graceful server shutdown"""
519+ try :
520+ # 从 app.state 获取配置
521+ current_config = get_start_config ()
522+ # Only respond to beacon if server was started with --open-browser
523+ if current_config ['browser_mode_enabled' ]:
524+ logger .info ("收到beacon信号,准备关闭服务器..." )
525+ # Schedule server shutdown
526+ asyncio .create_task (shutdown_server_async ())
527+ return {"success" : True , "message" : "服务器关闭信号已接收" }
528+ except Exception as e :
529+ logger .error (f"Beacon处理错误: { e } " )
530+ return {"success" : False , "error" : str (e )}
531+
532+ async def shutdown_server_async ():
533+ """异步关闭服务器"""
534+ try :
535+ # Give a small delay to allow the beacon response to be sent
536+ await asyncio .sleep (0.5 )
537+ logger .info ("正在关闭服务器..." )
538+
539+ # 向memory_server发送关闭信号
540+ try :
541+ import requests
542+ from config import MEMORY_SERVER_PORT
543+ shutdown_url = f"http://localhost:{ MEMORY_SERVER_PORT } /shutdown"
544+ response = requests .post (shutdown_url , timeout = 1 )
545+ if response .status_code == 200 :
546+ logger .info ("已向memory_server发送关闭信号" )
547+ else :
548+ logger .warning (f"向memory_server发送关闭信号失败,状态码: { response .status_code } " )
549+ except Exception as e :
550+ logger .warning (f"向memory_server发送关闭信号时出错: { e } " )
551+
552+ # Signal the server to stop
553+ current_config = get_start_config ()
554+ if current_config ['server' ] is not None :
555+ current_config ['server' ].should_exit = True
556+ except Exception as e :
557+ logger .error (f"关闭服务器时出错: { e } " )
558+
377559@app .post ('/api/characters/catgirl/{old_name}/rename' )
378560async def rename_catgirl (old_name : str , request : Request ):
379561 data = await request .json ()
@@ -408,15 +590,68 @@ async def get_index(request: Request, lanlan_name: str):
408590
409591
410592# --- Run the Server ---
411- # (Keep your existing __main__ block)
412593if __name__ == "__main__" :
413594 import uvicorn
595+ import argparse
596+ import os
597+ import signal
598+
599+ parser = argparse .ArgumentParser ()
600+ parser .add_argument ("--open-browser" , action = "store_true" ,
601+ help = "启动后是否打开浏览器并监控它" )
602+ parser .add_argument ("--page" , type = str , default = "" ,
603+ choices = ["index" , "chara_manager" , "api_key" ],
604+ help = "要打开的页面路由(不含域名和端口)" )
605+ args = parser .parse_args ()
414606
415607 logger .info ("--- Starting FastAPI Server ---" )
416608 # Use os.path.abspath to show full path clearly
417609 logger .info (f"Serving static files from: { os .path .abspath ('static' )} " )
418610 logger .info (f"Serving index.html from: { os .path .abspath ('templates/index.html' )} " )
419611 logger .info (f"Access UI at: http://127.0.0.1:{ MAIN_SERVER_PORT } (or your network IP:{ MAIN_SERVER_PORT } )" )
420612 logger .info ("-----------------------------" )
421- # Run from the directory containing server.py (gemini-live-app/)
422- uvicorn .run ("main_server:app" , host = "0.0.0.0" , port = MAIN_SERVER_PORT , reload = False )
613+
614+ # 1) 配置 UVicorn
615+ config = uvicorn .Config (
616+ app = app ,
617+ host = "0.0.0.0" ,
618+ port = MAIN_SERVER_PORT ,
619+ log_level = "info" ,
620+ loop = "asyncio" ,
621+ reload = False ,
622+ )
623+ server = uvicorn .Server (config )
624+
625+ # Set browser mode flag if --open-browser is used
626+ if args .open_browser :
627+ # 使用 FastAPI 的 app.state 来管理配置
628+ start_config = {
629+ "browser_mode_enabled" : True ,
630+ "browser_page" : args .page if args .page != 'index' else '' ,
631+ 'server' : server
632+ }
633+ set_start_config (start_config )
634+ else :
635+ # 设置默认配置
636+ start_config = {
637+ "browser_mode_enabled" : False ,
638+ "browser_page" : "" ,
639+ 'server' : server
640+ }
641+ set_start_config (start_config )
642+
643+ print (f"启动配置: { get_start_config ()} " )
644+
645+ # 2) 定义服务器关闭回调
646+ def shutdown_server ():
647+ logger .info ("收到浏览器关闭信号,正在关闭服务器..." )
648+ os .kill (os .getpid (), signal .SIGTERM )
649+
650+ # 4) 启动服务器(阻塞,直到 server.should_exit=True)
651+ logger .info ("--- Starting FastAPI Server ---" )
652+ logger .info (f"Access UI at: http://127.0.0.1:{ MAIN_SERVER_PORT } /{ args .page } " )
653+
654+ try :
655+ server .run ()
656+ finally :
657+ logger .info ("服务器已关闭" )
0 commit comments