Skip to content

Commit 53bd853

Browse files
committed
Added a new protocol at /l2d for live2d model customization.
1 parent d3adb27 commit 53bd853

30 files changed

Lines changed: 814 additions & 41 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ core_config.txt
77
# 忽略 memory/store 下所有内容(含子目录和文件)
88
memory/store/
99
config/api.py
10+
config/*.json
1011

1112
# 忽略临时文件
1213
*.tmp

README.MD

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
---
2424

2525
# 项目简介
26-
本项目的目的是打造一个新手友好、开箱即用的,具有听觉、视觉、工具调用和多端同步功能的AI~~猫娘~~伙伴。本项目在设计时有三个核心目标
26+
Lanlan(兰兰)是一个新手友好、开箱即用的,具有听觉、视觉、工具调用和多端同步功能的AI~~猫娘~~伙伴。本项目有三个核心目标
2727

28-
1. **降低语音延迟**。本项目的用户界面以语音交互为主,一切系统级设计皆须优先确保**降低对话延迟**,且任何服务不得阻塞对话进程。
28+
1. **极致的低延迟**。本项目的用户界面以语音交互为主,一切系统级设计皆须优先确保**降低语音延迟**,且任何服务不得阻塞对话进程。
2929

3030
1. **全场景同步**。猫娘可以在手机、电脑和智能眼镜上同时存在,且**同一只猫娘**在不同终端同时存在时,**行为应当完全同步**。 (假想场景:如果家中有多个显示器,每一个显示器上都放置着猫娘,那么我们希望无论走到哪里都是在跟同一只猫娘对话,实现全方位环绕式体验。)
3131

@@ -37,8 +37,8 @@
3737

3838
# 运行
3939

40-
1. **获取阿里云API**。在阿里云的百炼平台[官网](https://bailian.console.aliyun.com/)注册账号。新用户实名认证后可以获取大量免费额度,记得留意页面上的"新人福利"广告。注册完成后,请访问[控制台](https://bailian.console.aliyun.com/api-key?tab=model#/api-key)获取API Key。将API Key填入`core_config.txt`中的`"coreApiKey": `后的引号内。
41-
> *注:本项目提供的都是官方链接,不含任何推广,本人无法从中获取任何收益。阿里的官网目前做的很烂,请忍耐orz*
40+
1. **获取阿里云API**。在阿里云的百炼平台[官网](https://bailian.console.aliyun.com/)注册账号。新用户实名认证后可以获取大量免费额度,记得留意页面上的"新人福利"广告。注册完成后,请访问[控制台](https://bailian.console.aliyun.com/api-key?tab=model#/api-key)获取API Key。将API Key填入`core_config.txt`中的`"coreApiKey": `后的引号内。
41+
> *注:本项目提供的都是官方链接,不含任何推广,本人无法从中获取任何收益。阿里的官网目前做的很烂,请忍耐orz*
4242
4343
1. **体验网页版**。对于一键包,填写好API KEY后,运行`启动网页版.bat`即可打开网页版。**首次启动请耐心等待网页刷新**
4444

main_server.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from main_helper import core as core, cross_server as cross_server
1313
from fastapi.templating import Jinja2Templates
1414
from fastapi.responses import HTMLResponse
15+
from utils.preferences import load_user_preferences, update_model_preferences, validate_model_preferences, get_model_preferences, get_preferred_model_path, move_model_to_top
16+
from utils.frontend_utils import find_models
1517
templates = Jinja2Templates(directory="./")
1618
from config import LANLAN_PROMPT, MASTER_NAME, her_name, MAIN_SERVER_PORT
1719

@@ -74,17 +76,61 @@ async def get_default_index(request: Request): # 这个接口在直播版代码
7476
return templates.TemplateResponse("templates/index.html", {
7577
"request": request,
7678
"lanlan_name": her_name,
77-
"model_path": f"/static/live2d/mao_pro.model3.json"
79+
"model_path": f"/static/mao_pro/mao_pro.model3.json"
7880
})
7981

80-
@app.get("/{lanlan_name}", response_class=HTMLResponse)
81-
async def get_index(request: Request, lanlan_name: str):
82-
# Point FileResponse to the correct path relative to where server.py is run
83-
return templates.TemplateResponse("templates/index.html", {
84-
"request": request,
85-
"lanlan_name": lanlan_name,
86-
"model_path": f"/static/live2d/mao_pro.model3.json" # TODO: 根据lanlan_name动态加载模型. 实现起来很简单,但是用户需要手动配置、还需要调整大小和位置,当前版本先不增加复杂度
87-
})
82+
@app.get("/api/preferences")
83+
async def get_preferences():
84+
"""获取用户偏好设置"""
85+
preferences = load_user_preferences()
86+
return preferences
87+
88+
@app.post("/api/preferences")
89+
async def save_preferences(request: Request):
90+
"""保存用户偏好设置"""
91+
try:
92+
data = await request.json()
93+
if not data:
94+
return {"success": False, "error": "无效的数据"}
95+
96+
# 验证偏好数据
97+
if not validate_model_preferences(data):
98+
return {"success": False, "error": "偏好数据格式无效"}
99+
100+
# 更新偏好
101+
if update_model_preferences(data['model_path'], data['position'], data['scale']):
102+
return {"success": True, "message": "偏好设置已保存"}
103+
else:
104+
return {"success": False, "error": "保存失败"}
105+
106+
except Exception as e:
107+
return {"success": False, "error": str(e)}
108+
109+
110+
@app.get("/api/models")
111+
async def get_models():
112+
"""
113+
API接口,调用扫描函数并以JSON格式返回找到的模型列表。
114+
"""
115+
models = find_models()
116+
return models
117+
118+
@app.post("/api/preferences/set-preferred")
119+
async def set_preferred_model(request: Request):
120+
"""设置首选模型"""
121+
try:
122+
data = await request.json()
123+
if not data or 'model_path' not in data:
124+
return {"success": False, "error": "无效的数据"}
125+
126+
if move_model_to_top(data['model_path']):
127+
return {"success": True, "message": "首选模型已更新"}
128+
else:
129+
return {"success": False, "error": "模型不存在或更新失败"}
130+
131+
except Exception as e:
132+
return {"success": False, "error": str(e)}
133+
88134

89135
@app.on_event("startup")
90136
async def startup_event():
@@ -171,6 +217,26 @@ async def websocket_endpoint(websocket: WebSocket, lanlan_name: str):
171217
logger.info(f"Cleaning up WebSocket resources: {websocket.client}")
172218
await session_manager[lanlan_name].cleanup()
173219

220+
@app.get("/l2d", response_class=HTMLResponse)
221+
async def get_l2d_manager(request: Request):
222+
"""渲染Live2D模型管理器页面"""
223+
return templates.TemplateResponse("templates/l2d_manager.html", {
224+
"request": request
225+
})
226+
227+
@app.get("/{lanlan_name}", response_class=HTMLResponse)
228+
async def get_index(request: Request, lanlan_name: str):
229+
# 获取首选模型路径
230+
model_path = get_preferred_model_path() or f"/static/mao_pro/mao_pro.model3.json"
231+
232+
# Point FileResponse to the correct path relative to where server.py is run
233+
return templates.TemplateResponse("templates/index.html", {
234+
"request": request,
235+
"lanlan_name": lanlan_name,
236+
"model_path": model_path
237+
})
238+
239+
174240
# --- Run the Server ---
175241
# (Keep your existing __main__ block)
176242
if __name__ == "__main__":

manager.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
from flask import Flask, render_template, jsonify, request
3+
from utils.preferences import load_user_preferences, update_user_preferences, validate_preferences
4+
5+
# 初始化Flask应用
6+
app = Flask(__name__)
7+
8+
# 修改点:将扫描的根目录直接设置为 'static' 文件夹
9+
SEARCH_ROOT_DIR = app.static_folder or 'static'
10+
11+
def find_models():
12+
"""
13+
递归扫描整个 'static' 文件夹,查找所有包含 '.model3.json' 文件的子目录。
14+
"""
15+
found_models = []
16+
if not os.path.exists(SEARCH_ROOT_DIR):
17+
print(f"警告:指定的静态文件夹路径不存在: {SEARCH_ROOT_DIR}")
18+
return []
19+
20+
# os.walk会遍历指定的根目录下的所有文件夹和文件
21+
for root, dirs, files in os.walk(SEARCH_ROOT_DIR):
22+
for file in files:
23+
if file.endswith('.model3.json'):
24+
# 获取模型名称 (使用其所在的文件夹名,更加直观)
25+
model_name = os.path.basename(root)
26+
27+
# 构建可被浏览器访问的URL路径
28+
# 1. 计算文件相对于 static_folder 的路径
29+
relative_path = os.path.relpath(os.path.join(root, file), app.static_folder)
30+
# 2. 将本地路径分隔符 (如'\') 替换为URL分隔符 ('/')
31+
model_path = relative_path.replace(os.path.sep, '/')
32+
33+
found_models.append({
34+
"name": model_name,
35+
"path": f"/static/{model_path}"
36+
})
37+
38+
# 优化:一旦在某个目录找到模型json,就无需再继续深入该目录的子目录
39+
dirs[:] = []
40+
break
41+
42+
return found_models
43+
44+
@app.route('/')
45+
def l2d_manager():
46+
"""渲染主控制页面"""
47+
return render_template('l2d_manager.html')
48+
49+
@app.route('/api/models')
50+
def get_models():
51+
"""
52+
API接口,调用扫描函数并以JSON格式返回找到的模型列表。
53+
"""
54+
models = find_models()
55+
return jsonify(models)
56+
57+
@app.route('/api/preferences', methods=['GET'])
58+
def get_preferences():
59+
"""获取用户偏好设置"""
60+
preferences = load_user_preferences()
61+
return jsonify(preferences)
62+
63+
@app.route('/api/preferences', methods=['POST'])
64+
def save_preferences():
65+
"""保存用户偏好设置"""
66+
try:
67+
data = request.get_json()
68+
if not data:
69+
return jsonify({"success": False, "error": "无效的数据"}), 400
70+
71+
# 验证偏好数据
72+
if not validate_preferences(data):
73+
return jsonify({"success": False, "error": "偏好数据格式无效"}), 400
74+
75+
# 更新偏好
76+
if update_user_preferences(data):
77+
return jsonify({"success": True, "message": "偏好设置已保存"})
78+
else:
79+
return jsonify({"success": False, "error": "保存失败"}), 500
80+
81+
except Exception as e:
82+
return jsonify({"success": False, "error": str(e)}), 500
83+
84+
if __name__ == '__main__':
85+
app.run(host='0.0.0.0', port=5000, debug=True)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)