Skip to content

Commit be1877a

Browse files
committed
Fixed minor issue with l2d expression profile. All set for v0.3.2.
1 parent ba7ecff commit be1877a

2 files changed

Lines changed: 133 additions & 41 deletions

File tree

main_server.py

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,8 +1027,45 @@ async def get_emotion_mapping(model_name: str):
10271027
with open(model_json_path, 'r', encoding='utf-8') as f:
10281028
config_data = json.load(f)
10291029

1030-
emotion_mapping = config_data.get('EmotionMapping', {})
1031-
1030+
# 优先使用 EmotionMapping;若不存在则从 FileReferences 推导
1031+
emotion_mapping = config_data.get('EmotionMapping')
1032+
if not emotion_mapping:
1033+
derived_mapping = {"motions": {}, "expressions": {}}
1034+
file_refs = config_data.get('FileReferences', {}) or {}
1035+
1036+
# 从标准 Motions 结构推导
1037+
motions = file_refs.get('Motions', {}) or {}
1038+
for group_name, items in motions.items():
1039+
files = []
1040+
for item in items or []:
1041+
try:
1042+
file_path = item.get('File') if isinstance(item, dict) else None
1043+
if file_path:
1044+
files.append(file_path.replace('\\', '/'))
1045+
except Exception:
1046+
continue
1047+
derived_mapping["motions"][group_name] = files
1048+
1049+
# 从标准 Expressions 结构推导(按 Name 的前缀进行分组,如 happy_xxx)
1050+
expressions = file_refs.get('Expressions', []) or []
1051+
for item in expressions:
1052+
if not isinstance(item, dict):
1053+
continue
1054+
name = item.get('Name') or ''
1055+
file_path = item.get('File') or ''
1056+
if not file_path:
1057+
continue
1058+
file_path = file_path.replace('\\', '/')
1059+
# 根据第一个下划线拆分分组
1060+
if '_' in name:
1061+
group = name.split('_', 1)[0]
1062+
else:
1063+
# 无前缀的归入 neutral 组,避免丢失
1064+
group = 'neutral'
1065+
derived_mapping["expressions"].setdefault(group, []).append(file_path)
1066+
1067+
emotion_mapping = derived_mapping
1068+
10321069
return {"success": True, "config": emotion_mapping}
10331070
except Exception as e:
10341071
logger.error(f"获取情绪映射配置失败: {e}")
@@ -1061,14 +1098,59 @@ async def update_emotion_mapping(model_name: str, request: Request):
10611098
with open(model_json_path, 'r', encoding='utf-8') as f:
10621099
config_data = json.load(f)
10631100

1064-
# 添加或更新 EmotionMapping
1101+
# 统一写入到标准 Cubism 结构(FileReferences.Motions / FileReferences.Expressions)
1102+
file_refs = config_data.setdefault('FileReferences', {})
1103+
1104+
# 处理 motions: data 结构为 { motions: { emotion: ["motions/xxx.motion3.json", ...] }, expressions: {...} }
1105+
motions_input = (data.get('motions') if isinstance(data, dict) else None) or {}
1106+
motions_output = {}
1107+
for group_name, files in motions_input.items():
1108+
items = []
1109+
for file_path in files or []:
1110+
if not isinstance(file_path, str):
1111+
continue
1112+
normalized = file_path.replace('\\', '/').lstrip('./')
1113+
items.append({"File": normalized})
1114+
motions_output[group_name] = items
1115+
file_refs['Motions'] = motions_output
1116+
1117+
# 处理 expressions: 将按 emotion 前缀生成扁平列表,Name 采用 "{emotion}_{basename}" 的约定
1118+
expressions_input = (data.get('expressions') if isinstance(data, dict) else None) or {}
1119+
1120+
# 先保留不属于我们情感前缀的原始表达(避免覆盖用户自定义)
1121+
existing_expressions = file_refs.get('Expressions', []) or []
1122+
emotion_prefixes = set(expressions_input.keys())
1123+
preserved_expressions = []
1124+
for item in existing_expressions:
1125+
try:
1126+
name = (item.get('Name') or '') if isinstance(item, dict) else ''
1127+
prefix = name.split('_', 1)[0] if '_' in name else None
1128+
if not prefix or prefix not in emotion_prefixes:
1129+
preserved_expressions.append(item)
1130+
except Exception:
1131+
preserved_expressions.append(item)
1132+
1133+
new_expressions = []
1134+
for emotion, files in expressions_input.items():
1135+
for file_path in files or []:
1136+
if not isinstance(file_path, str):
1137+
continue
1138+
normalized = file_path.replace('\\', '/').lstrip('./')
1139+
base = os.path.basename(normalized)
1140+
base_no_ext = base.replace('.exp3.json', '')
1141+
name = f"{emotion}_{base_no_ext}"
1142+
new_expressions.append({"Name": name, "File": normalized})
1143+
1144+
file_refs['Expressions'] = preserved_expressions + new_expressions
1145+
1146+
# 同时保留一份 EmotionMapping(供管理器读取与向后兼容)
10651147
config_data['EmotionMapping'] = data
1066-
1148+
10671149
# 保存配置到文件
10681150
with open(model_json_path, 'w', encoding='utf-8') as f:
10691151
json.dump(config_data, f, ensure_ascii=False, indent=2)
1070-
1071-
logger.info(f"模型 {model_name} 的情绪映射配置已更新")
1152+
1153+
logger.info(f"模型 {model_name} 的情绪映射配置已更新(已同步到 FileReferences)")
10721154
return {"success": True, "message": "情绪映射配置已保存"}
10731155
except Exception as e:
10741156
logger.error(f"更新情绪映射配置失败: {e}")

static/live2d.js

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ class Live2DManager {
110110
this.isLocked = true;
111111
this.onModelLoaded = null;
112112
this.onStatusUpdate = null;
113+
this.modelName = null; // 记录当前模型目录名
114+
this.modelRootPath = null; // 记录当前模型根路径,如 /static/<modelName>
113115
}
114116

115117
// 初始化 PIXI 应用
@@ -201,41 +203,31 @@ class Live2DManager {
201203
return;
202204
}
203205

204-
const expressions = this.emotionMapping.Expressions.filter(e => e.Name.startsWith(emotion));
206+
const expressions = this.emotionMapping.Expressions.filter(e => (e.Name || '').startsWith(emotion));
205207
if (!expressions || expressions.length === 0) {
206208
console.log(`未找到情感 ${emotion} 对应的表情,将跳过表情播放`);
207209
return; // Gracefully exit if no expression is found
208210
}
209211

210-
const expressionFile = this.getRandomElement(expressions).File.split('/').pop();
211-
if (!expressionFile) return;
212+
const choice = this.getRandomElement(expressions);
213+
if (!choice || !choice.File) return;
212214

213215
try {
214-
// 获取模型名称(从模型路径中提取)
215-
let modelName = 'mao_pro'; // 默认模型名称
216-
217-
// 尝试从模型路径中提取模型名称
218-
if (this.currentModel.internalModel && this.currentModel.internalModel.settings && this.currentModel.internalModel.settings.model) {
219-
const modelPath = this.currentModel.internalModel.settings.model;
220-
const pathParts = modelPath.split('/');
221-
modelName = pathParts[pathParts.length - 2] || pathParts[pathParts.length - 1].replace('.model3.json', '');
222-
}
223-
224-
// 加载表情文件并应用参数
225-
const expressionPath = `/static/${modelName}/expressions/${expressionFile}`;
216+
// 计算表达文件路径(相对模型根目录)
217+
const expressionPath = this.resolveAssetPath(choice.File);
226218
const response = await fetch(expressionPath);
227219
if (!response.ok) {
228220
throw new Error(`Failed to load expression: ${response.statusText}`);
229221
}
230222

231223
const expressionData = await response.json();
232-
console.log(`加载表情文件: ${expressionFile}`, expressionData);
224+
console.log(`加载表情文件: ${choice.File}`, expressionData);
233225

234226
// 方法1: 尝试使用原生expression API
235227
if (this.currentModel.expression) {
236228
try {
237-
// 从文件名中提取expression名称(去掉.exp3.json后缀
238-
const expressionName = expressionFile.replace('.exp3.json', '');
229+
// 直接使用配置中的 Name(我们保存时已包含情感前缀
230+
const expressionName = choice.Name || choice.File.replace('.exp3.json', '');
239231
console.log(`尝试使用原生API播放expression: ${expressionName}`);
240232

241233
const expression = await this.currentModel.expression(expressionName);
@@ -286,20 +278,10 @@ class Live2DManager {
286278
return;
287279
}
288280

289-
const motionFile = this.getRandomElement(motions).File.split('/').pop();
290-
if (!motionFile) return;
281+
const choice = this.getRandomElement(motions);
282+
if (!choice || !choice.File) return;
291283

292284
try {
293-
// 获取模型名称(从模型路径中提取)
294-
let modelName = 'mao_pro'; // 默认模型名称
295-
296-
// 尝试从模型路径中提取模型名称
297-
if (this.currentModel.internalModel && this.currentModel.internalModel.settings && this.currentModel.internalModel.settings.model) {
298-
const modelPath = this.currentModel.internalModel.settings.model;
299-
const pathParts = modelPath.split('/');
300-
modelName = pathParts[pathParts.length - 2] || pathParts[pathParts.length - 1].replace('.model3.json', '');
301-
}
302-
303285
// 清除之前的动作定时器
304286
if (this.motionTimer) {
305287
console.log('检测到前一个motion正在播放,正在停止...');
@@ -326,14 +308,14 @@ class Live2DManager {
326308

327309
// 尝试使用Live2D模型的原生motion播放功能
328310
try {
329-
// 构建完整的motion路径
330-
const motionPath = `/static/${modelName}/motions/${motionFile}`;
311+
// 构建完整的motion路径(相对模型根目录)
312+
const motionPath = this.resolveAssetPath(choice.File);
331313
console.log(`尝试播放motion: ${motionPath}`);
332314

333315
// 方法1: 直接使用模型的motion播放功能
334316
if (this.currentModel.motion) {
335317
try {
336-
console.log(`尝试播放motion: ${motionFile}`);
318+
console.log(`尝试播放motion: ${choice.File}`);
337319

338320
// 使用情感名称作为motion组名,这样可以确保播放正确的motion
339321
console.log(`尝试使用情感组播放motion: ${emotion}`);
@@ -363,7 +345,7 @@ class Live2DManager {
363345

364346
// 设置定时器在motion结束后清理
365347
this.motionTimer = setTimeout(() => {
366-
console.log(`motion播放完成(预期文件: ${motionFile})`);
348+
console.log(`motion播放完成(预期文件: ${choice.File})`);
367349
this.motionTimer = null;
368350
this.clearEmotionEffects();
369351
}, motionDuration);
@@ -387,7 +369,7 @@ class Live2DManager {
387369
}
388370

389371
// 如果所有方法都失败,回退到简单动作
390-
console.warn(`无法播放motion: ${motionFile},回退到简单动作`);
372+
console.warn(`无法播放motion: ${choice.File},回退到简单动作`);
391373
this.playSimpleMotion(emotion);
392374

393375
} catch (error) {
@@ -605,6 +587,21 @@ class Live2DManager {
605587
const model = await Live2DModel.from(modelPath, { autoInteract: false });
606588
this.currentModel = model;
607589

590+
// 解析模型目录名与根路径,供资源解析使用
591+
try {
592+
const cleanPath = (modelPath || '').split('#')[0].split('?')[0];
593+
const lastSlash = cleanPath.lastIndexOf('/');
594+
const rootDir = lastSlash >= 0 ? cleanPath.substring(0, lastSlash) : '/static';
595+
this.modelRootPath = rootDir; // e.g. /static/mao_pro or /static/some/deeper/dir
596+
const parts = rootDir.split('/').filter(Boolean);
597+
this.modelName = parts.length > 0 ? parts[parts.length - 1] : null;
598+
console.log('模型根路径解析:', { modelPath, modelName: this.modelName, modelRootPath: this.modelRootPath });
599+
} catch (e) {
600+
console.warn('解析模型根路径失败,将使用默认值', e);
601+
this.modelRootPath = '/static';
602+
this.modelName = null;
603+
}
604+
608605
// 配置渲染纹理数量以支持更多蒙版
609606
if (model.internalModel && model.internalModel.renderer && model.internalModel.renderer._clippingManager) {
610607
model.internalModel.renderer._clippingManager._renderTextureCount = 3;
@@ -666,6 +663,19 @@ class Live2DManager {
666663
}
667664
}
668665

666+
// 解析资源相对路径(基于当前模型根目录)
667+
resolveAssetPath(relativePath) {
668+
if (!relativePath) return '';
669+
let rel = String(relativePath).replace(/^[\\/]+/, '');
670+
if (rel.startsWith('static/')) {
671+
return `/${rel}`;
672+
}
673+
if (rel.startsWith('/static/')) {
674+
return rel;
675+
}
676+
return `${this.modelRootPath}/${rel}`;
677+
}
678+
669679
// 应用模型设置
670680
applyModelSettings(model, options) {
671681
const { preferences, isMobile = false } = options;

0 commit comments

Comments
 (0)