1.数字人接口增加数字人指定动作、表情等输出;

2.更换群二维码。
This commit is contained in:
guo zebin
2026-03-25 22:18:00 +08:00
parent 4e85827068
commit 1d7563209a
9 changed files with 1004 additions and 19 deletions
+21
View File
@@ -0,0 +1,21 @@
code,behavior,affect,intensity,priority,sentimentHint,keywords
greeting.hello,nod,smile,0.35,60,0.4,你好|您好|hello|hi|哈喽
greeting.welcome,invite,warm,0.78,80,0.9,欢迎光临|欢迎回来|欢迎来到|热烈欢迎|欢迎
farewell.goodbye,wave,smile,0.62,72,0.2,再见|拜拜|bye|goodbye|下次见
dialogue.confirm,nod,smile,0.42,58,0.3,是的|对的|没错|当然|收到|明白|了解|可以
dialogue.reject,reject,serious,0.58,70,-0.6,不是|不对|不行|不可以|不能|no
dialogue.think,think,neutral,0.44,74,0.0,让我想想|想一想|考虑一下|这个嘛|容我想想
dialogue.question,question,curious,0.52,55,0.0,为什么|怎么回事|真的吗|是什么|怎么
dialogue.explain,explain,neutral,0.56,63,0.1,也就是说|换句话说|意思是|其实|具体来说|比如|例如
dialogue.recommend,recommend,smile,0.63,66,0.5,建议|推荐|你可以试试|最好|我建议
dialogue.summary,summary,neutral,0.4,57,0.1,总之|综上|所以|最后|总结一下
guidance.invite,invite,warm,0.74,82,0.7,请进|这边请|跟我来|请坐|往这边
guidance.wait,wait,neutral,0.26,65,0.0,稍等|等一下|请稍等|稍后|马上
guidance.remind,remind,neutral,0.48,62,0.1,提醒|记得|别忘了|告诉你
guidance.warn,warn,serious,0.8,84,-0.4,注意|小心|警告|注意安全
social.thanks,thanks,warm,0.46,61,0.4,谢谢|感谢|多谢|thank you
social.apology,apology,sorry,0.6,71,-0.3,抱歉|对不起|不好意思|请原谅
social.care,care,warm,0.5,64,0.2,还好吗|没事吧|别担心|放心
emotion.celebrate,celebrate,excited,0.95,92,1.6,太好了|成功|完成了|真棒|厉害
emotion.surprise,surprise,surprised,0.88,86,1.0,天啊|竟然|不会吧|omg|真的假的
emotion.sad,sad,sad,0.82,83,-1.2,难过|可惜|遗憾|糟糕|失败了|伤心
1 code behavior affect intensity priority sentimentHint keywords
2 greeting.hello nod smile 0.35 60 0.4 你好|您好|hello|hi|哈喽
3 greeting.welcome invite warm 0.78 80 0.9 欢迎光临|欢迎回来|欢迎来到|热烈欢迎|欢迎
4 farewell.goodbye wave smile 0.62 72 0.2 再见|拜拜|bye|goodbye|下次见
5 dialogue.confirm nod smile 0.42 58 0.3 是的|对的|没错|当然|收到|明白|了解|可以
6 dialogue.reject reject serious 0.58 70 -0.6 不是|不对|不行|不可以|不能|no
7 dialogue.think think neutral 0.44 74 0.0 让我想想|想一想|考虑一下|这个嘛|容我想想
8 dialogue.question question curious 0.52 55 0.0 为什么|怎么回事|真的吗|是什么|怎么
9 dialogue.explain explain neutral 0.56 63 0.1 也就是说|换句话说|意思是|其实|具体来说|比如|例如
10 dialogue.recommend recommend smile 0.63 66 0.5 建议|推荐|你可以试试|最好|我建议
11 dialogue.summary summary neutral 0.4 57 0.1 总之|综上|所以|最后|总结一下
12 guidance.invite invite warm 0.74 82 0.7 请进|这边请|跟我来|请坐|往这边
13 guidance.wait wait neutral 0.26 65 0.0 稍等|等一下|请稍等|稍后|马上
14 guidance.remind remind neutral 0.48 62 0.1 提醒|记得|别忘了|告诉你
15 guidance.warn warn serious 0.8 84 -0.4 注意|小心|警告|注意安全
16 social.thanks thanks warm 0.46 61 0.4 谢谢|感谢|多谢|thank you
17 social.apology apology sorry 0.6 71 -0.3 抱歉|对不起|不好意思|请原谅
18 social.care care warm 0.5 64 0.2 还好吗|没事吧|别担心|放心
19 emotion.celebrate celebrate excited 0.95 92 1.6 太好了|成功|完成了|真棒|厉害
20 emotion.surprise surprise surprised 0.88 86 1.0 天啊|竟然|不会吧|omg|真的假的
21 emotion.sad sad sad 0.82 83 -1.2 难过|可惜|遗憾|糟糕|失败了|伤心
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
import csv
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Dict, List, Optional, Tuple
RULES_PATH = Path(__file__).resolve().parents[1] / "config" / "action_rules.csv"
@dataclass(frozen=True)
class ActionRule:
code: str
behavior: str
affect: str
intensity: float
priority: int
sentiment_hint: float
keywords: Tuple[str, ...]
def _normalize_text(text: str) -> str:
return (text or "").strip().lower()
@lru_cache(maxsize=1)
def load_action_rules() -> Tuple[ActionRule, ...]:
try:
with RULES_PATH.open("r", encoding="utf-8", newline="") as file:
reader = csv.DictReader(file)
built_rules: List[ActionRule] = []
for row in reader:
try:
keywords = tuple(
k.strip() for k in row["keywords"].split("|") if k.strip()
)
built_rules.append(ActionRule(
code=row["code"],
behavior=row["behavior"],
affect=row["affect"],
intensity=float(row.get("intensity", 0.5)),
priority=int(row.get("priority", 0)),
sentiment_hint=float(row.get("sentimentHint", 0.0)),
keywords=keywords,
))
except (KeyError, TypeError, ValueError):
continue
except OSError:
return ()
return tuple(built_rules)
def resolve_action_signal(text: str) -> Optional[Dict[str, object]]:
normalized = _normalize_text(text)
if not normalized:
return None
best_rule: Optional[ActionRule] = None
best_matches: List[str] = []
best_score = (-1, -1, -1)
for rule in load_action_rules():
matched_keywords = [
keyword for keyword in rule.keywords if keyword.lower() in normalized
]
if not matched_keywords:
continue
score = (
len(matched_keywords),
max(len(keyword) for keyword in matched_keywords),
rule.priority,
)
if score > best_score:
best_rule = rule
best_matches = matched_keywords
best_score = score
if best_rule is None:
return None
return {
"code": best_rule.code,
"behavior": best_rule.behavior,
"affect": best_rule.affect,
"intensity": best_rule.intensity,
"priority": best_rule.priority,
"matchedKeywords": best_matches,
"sentimentHint": best_rule.sentiment_hint,
}
+1 -1
View File
@@ -47,7 +47,7 @@ import numpy as np
from ai_module import baidu_emotion
from core.live2d_action_standard import resolve_action_signal
from core.action_signal import resolve_action_signal
from core import wsa_server
+18 -18
View File
@@ -37,16 +37,16 @@ class MyServer:
output_setting = data.get("Output")
except json.JSONDecodeError:
pass # Ignore invalid JSON messages
if username is not None or output_setting is not None:
remote_address = websocket.remote_address
unique_id = f"{remote_address[0]}:{remote_address[1]}"
async with self.lock:
for i in range(len(self.__clients)):
if self.__clients[i]["id"] == unique_id:
if username is not None:
self.__clients[i]["username"] = username
if output_setting is not None:
self.__clients[i]["output"] = output_setting
if username is not None or output_setting is not None:
remote_address = websocket.remote_address
unique_id = f"{remote_address[0]}:{remote_address[1]}"
async with self.lock:
for i in range(len(self.__clients)):
if self.__clients[i]["id"] == unique_id:
if username is not None:
self.__clients[i]["username"] = username
if output_setting is not None:
self.__clients[i]["output"] = output_setting
await self.__consumer(message)
except websockets.exceptions.ConnectionClosedError as e:
# 从客户端列表中移除已断开的连接
@@ -195,17 +195,17 @@ class MyServer:
if self.__server:
util.log(1, 'server already exist')
return
self.__server = websockets.serve(self.__handler, self.__host, self.__port)
self.__server = websockets.serve(self.__handler, self.__host, self.__port, ping_interval=10, ping_timeout=5)
asyncio.get_event_loop().run_until_complete(self.__server)
asyncio.get_event_loop().run_forever()
# 往要发送的命令列表中,添加命令
def add_cmd(self, content):
if not self.__running:
return
# keep unicode (emoji/中文) intact for websocket consumers
jsonStr = json.dumps(content, ensure_ascii=False)
self.__listCmd.append(jsonStr)
def add_cmd(self, content):
if not self.__running:
return
# keep unicode (emoji/中文) intact for websocket consumers
jsonStr = json.dumps(content, ensure_ascii=False)
self.__listCmd.append(jsonStr)
# util.log('命令 {}'.format(content))
# 开启服务
@@ -314,4 +314,4 @@ def get_web_instance() -> MyServer:
if __name__ == '__main__':
testServer = TestServer(host='0.0.0.0', port=10000)
testServer.start_server()
testServer.start_server()
+401
View File
@@ -0,0 +1,401 @@
# Fay 侧通用动作语义改造说明
## 目标
让 Fay 输出“通用动作语义”,而不是输出某个渲染引擎或某个模型的具体动作编号。
这样 Fay 可以同时服务于:
- Live2D 数字人
- 3D 数字人
- Unity / Unreal 角色
- 机器人 / 硬件角色
- 仅语音客户端
原则:
1. 保持原版 Fay WebSocket 音频接口结构不变。
2. 在原版 `Data` 中增加两个可选字段:`Action``Sentiment`
3. Fay 不输出 `TapBody``MotionNo``F01` 这类具体实现细节。
4. 各前端或驱动端通过自己的配置表完成映射。
---
## 原版接口
原版接口如下:
```json
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": []
}
}
```
---
## 建议改造方式
在原版 `Data` 中增加可选字段 `Action``Sentiment`
```json
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": [],
"Sentiment": 0.7,
"Action": {
"code": "guidance.invite",
"behavior": "invite",
"affect": "warm",
"intensity": 0.74,
"priority": 82
}
}
}
```
说明:
- 不强制增加 `ActionSchema`
- 第一版先保证字段语义稳定即可
- 如果未来需要版本控制,可以后续再补 `Action.version`
- `Action``Sentiment` 建议同时存在,但都应为可选字段
---
## 为什么需要“动作语义锚点”
这里的“动作语义锚点”,建议用 `behavior` 表示。
它的作用不是描述某个具体模型动作,而是给所有前端一个统一的“动作类别坐标”。
### 它解决的问题
| 问题 | 没有语义锚点时 | 有语义锚点时 |
| --- | --- | --- |
| Fay 与模型耦合 | Fay 需要知道 `TapBody``MotionNo=21` | Fay 只说 `behavior=invite` |
| 多端复用困难 | Live2D、3D、机器人都要单独写逻辑 | 各端都只做 `behavior -> 本地动作` 映射 |
| 话术很多但动作有限 | 每种话术都要单独适配 | 多个话术可归一到同一个锚点 |
| 后续维护成本高 | 每换模型都要改 Fay | 只改前端配置表 |
### 一个简单例子
这些话术:
- “这边请”
- “请进”
- “跟我来”
- “请往这边走”
在 Fay 侧都可以归一成:
```json
{
"code": "guidance.invite",
"behavior": "invite",
"affect": "warm"
}
```
然后不同终端各自解释:
- Live2D:映射到某个引导动作
- 3D:映射到某个招手/邀请动画
- 机器人:映射到某个手臂引导动作
- 纯语音:忽略动作,只保留语气
所以:
- `code` 更偏业务/场景语义
- `behavior` 更偏“身体动作类别”
- `affect` 更偏“表现气质/情绪风格”
---
## 推荐字段定义
| 字段 | 类型 | 是否必填 | 含义 |
| --- | --- | --- | --- |
| `Sentiment` | number | 否 | 连续情绪值,建议范围 `-2 ~ 2``-1 ~ 1`,由接收端自行约定 |
| `Action.code` | string | 是 | 标准动作语义 ID,稳定主键 |
| `Action.behavior` | string | 否 | 动作语义锚点,描述身体动作类别 |
| `Action.affect` | string | 否 | 表现语义锚点,描述表情/语气/情绪风格 |
| `Action.intensity` | number | 否 | 强度,建议范围 `0 ~ 1` |
| `Action.priority` | number | 否 | 优先级,冲突时用于决策 |
---
## 字段职责建议
### `Sentiment`
用于表达”这段话整体情绪倾向”。
它和 `Action` 不冲突,职责不同:
- `Action` 决定”做什么”
- `Sentiment` 决定”情绪偏什么方向”
推荐用途:
-`Action` 存在时,作为微调信号
-`Action` 不存在时,作为回退信号
- 可同时用于表情、语气、动作幅度、TTS 风格
#### `Sentiment` 计算方式
Fay 当前有两种计算方式,按优先级依次尝试:
**方式一:百度情感分析 API(优先)**
如果配置了 `baidu_emotion_api_key``baidu_emotion_secret_key`,调用百度 NLP 的 `sentiment_classify` 接口(`ai_module/baidu_emotion.py`),返回百度原始 sentiment 值(0=消极 / 1=中性 / 2=积极)。
**方式二:本地关键词匹配(兜底)**
未配置百度 API 或调用失败时,走 `fay_core.py` 中的 `__analyze_sentiment_by_keywords()` 方法,逻辑如下:
1. 维护 4 个关键词列表,每个对应一个权重:
| 列表 | 示例关键词 | 权重 |
| --- | --- | --- |
| 非常积极 | 开心、太好了、完美、激动 | +2 |
| 轻微积极 | 好、可以、没问题、欢迎 | +1 |
| 轻微消极 | 不好、失望、烦、担心 | -1 |
| 非常消极 | 痛苦、绝望、崩溃、气死 | -2 |
2. 统计文本中匹配的关键词数量,加权求和
3. 标点修正:``/`!`/`~` 加 0.3`...`/`。。.` 减 0.3
4. 最终 clamp 到 `[-2, +2]` 范围
注意:此计算与 `Action` 中的 `sentimentHint` 字段完全独立。`sentimentHint` 是动作规则自身的情感标注,用于辅助关键词匹配;`Sentiment` 是对整句话的情感极性评估。
### `code`
用于表达“这次动作在业务上的语义是什么”。
例如:
- `greeting.hello`
- `guidance.invite`
- `dialogue.explain`
- `social.thanks`
### `behavior`
用于表达“身体动作属于哪一类”。
例如:
- `nod`
- `invite`
- `wave`
- `think`
- `warn`
它是动作语义锚点。
### `affect`
用于表达“整体表现风格是什么”。
例如:
- `smile`
- `warm`
- `neutral`
- `serious`
- `sad`
- `excited`
这个字段比 `expression` 更通用,因为它不只适用于 Live2D 表情,也可以映射到:
- 3D 面部表情
- TTS 语气风格
- 机器人灯光/屏幕表情
### `intensity`
用于表达“动作做得轻一点还是重一点”。
例如:
- `0.2` 轻微点头
- `0.8` 明显邀请
- `0.95` 强烈庆祝
---
## Fay 侧只负责什么
Fay 侧只负责:
```text
文本 / 意图 / 场景 -> Action
文本 / 情绪分析 -> Sentiment
```
Fay 不负责:
```text
Action -> Live2D动作编号
Action -> 3D动画名
Action -> 机器人舵机动作
```
---
## 不建议 Fay 输出的字段
| 不建议由 Fay 输出 | 原因 |
| --- | --- |
| `MotionGroup` | 属于 конкретe 模型动作组 |
| `MotionNo` | 属于 конкретe 模型动作编号 |
| `TapBody` | 属于 конкретe Live2D 工程命名 |
| `F01/F02/F03` | 属于 конкретe 模型表情资源名 |
这些都应该由前端或驱动端自行配置。
---
## 推荐可配置映射表
Fay 侧维护一张 CSV 配置表(`config/action_rules.csv`),可直接用 Excel 打开编辑:
```text
关键词/意图 -> code/behavior/affect/intensity/priority/sentimentHint
```
CSV 格式说明:
- 第一行为表头:`code,behavior,affect,intensity,priority,sentimentHint,keywords`
- `keywords` 列中多个关键词用 `|` 分隔(避免与 CSV 逗号冲突)
- 文件编码为 UTF-8
不要把这张表写死在代码里。
### 当前规则示例
| 关键词或意图示例 | `code` | `behavior` | `affect` | `intensity` | `priority` |
| --- | --- | --- | --- | --- | --- |
| 你好、您好、hello、hi | `greeting.hello` | `nod` | `smile` | `0.35` | `60` |
| 欢迎、欢迎光临、欢迎来到 | `greeting.welcome` | `invite` | `warm` | `0.78` | `80` |
| 再见、拜拜、goodbye | `farewell.goodbye` | `wave` | `smile` | `0.62` | `72` |
| 是的、没错、收到、明白 | `dialogue.confirm` | `nod` | `smile` | `0.42` | `58` |
| 不是、不行、不能、no | `dialogue.reject` | `reject` | `serious` | `0.58` | `70` |
| 让我想想、考虑一下 | `dialogue.think` | `think` | `neutral` | `0.44` | `74` |
| 为什么、怎么回事、真的吗 | `dialogue.question` | `question` | `curious` | `0.52` | `55` |
| 也就是说、其实、比如 | `dialogue.explain` | `explain` | `neutral` | `0.56` | `63` |
| 建议、推荐、你可以试试 | `dialogue.recommend` | `recommend` | `smile` | `0.63` | `66` |
| 总之、综上、最后总结 | `dialogue.summary` | `summary` | `neutral` | `0.40` | `57` |
| 请进、这边请、跟我来 | `guidance.invite` | `invite` | `warm` | `0.74` | `82` |
| 稍等、等一下、请稍等 | `guidance.wait` | `wait` | `neutral` | `0.26` | `65` |
| 提醒、别忘了、记得 | `guidance.remind` | `remind` | `neutral` | `0.48` | `62` |
| 注意、小心、警告 | `guidance.warn` | `warn` | `serious` | `0.80` | `84` |
| 谢谢、感谢、多谢 | `social.thanks` | `thanks` | `warm` | `0.46` | `61` |
| 抱歉、对不起、不好意思 | `social.apology` | `apology` | `sorry` | `0.60` | `71` |
| 还好吗、没事吧、别担心 | `social.care` | `care` | `warm` | `0.50` | `64` |
| 太好了、成功了、真棒 | `emotion.celebrate` | `celebrate` | `excited` | `0.95` | `92` |
| 天啊、竟然、不会吧 | `emotion.surprise` | `surprise` | `surprised` | `0.88` | `86` |
| 难过、可惜、遗憾、糟糕 | `emotion.sad` | `sad` | `sad` | `0.82` | `83` |
说明:
- 这张表本身就建议“带上表情风格”,也就是 `affect`
- 这样前端不需要只靠 `Sentiment` 猜表情
- `Sentiment` 仍然保留,用于微调和回退
---
## 各端如何消费 `Action`
### Live2D
```text
behavior -> motion
affect -> expression
intensity -> 候选动作强度选择
sentiment -> 表情/动作幅度微调或回退
```
### 3D 数字人
```text
behavior -> 动画状态机
affect -> 面部表情 / blendshape
intensity -> 动作幅度 / 播放权重
sentiment -> 情绪层混合权重
```
### 机器人
```text
behavior -> 舵机动作模板
affect -> 灯光 / 屏幕表情 / 语音风格
intensity -> 动作幅度 / 执行力度
sentiment -> 情绪表现微调
```
### 纯语音客户端
```text
behavior -> 可忽略
affect -> TTS 风格 / 音色 / 情绪
intensity -> 情绪强度
sentiment -> 连续情绪控制
```
---
## 最小结论
Fay 侧建议从:
```json
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": []
}
}
```
升级为:
```json
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": [],
"Sentiment": 0.7,
"Action": {
"code": "guidance.invite",
"behavior": "invite",
"affect": "warm",
"intensity": 0.74,
"priority": 82
}
}
}
```
即可。
后续具体怎么动,由各前端或驱动端通过配置表自己决定。
+283
View File
@@ -0,0 +1,283 @@
# Live2D 模型制作要求
本文档用于指导外包人员制作 Live2D 模型,确保交付后能直接对接我方的数字人系统。
---
## 一、技术规格
| 项目 | 要求 |
| --- | --- |
| Cubism 版本 | **Cubism 5**SDK for Web 5-r.4 |
| model3.json 版本 | Version: 3 |
| 贴图尺寸 | 2048×2048,张数不限 |
| 交付格式 | 完整的 Cubism 导出目录(含 .moc3、贴图、motion3.json、exp3.json、physics3.json、pose3.json 等) |
| 渲染目标 | 浏览器端 WebGLCubism SDK for Web |
---
## 二、必需参数(Parameters
模型必须包含以下标准参数,参数 ID 请严格使用 Cubism 默认命名:
### 口型同步(必需)
| 参数 ID | 用途 | 说明 |
| --- | --- | --- |
| `ParamMouthOpenY` | 嘴巴张合 | **最关键参数**,程序通过此参数驱动实时口型同步 |
程序会在 `model3.json``Groups` 中查找 `LipSync` 组:
```json
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": ["ParamMouthOpenY"]
}
```
### 眨眼(必需)
| 参数 ID | 用途 |
| --- | --- |
| `ParamEyeLOpen` | 左眼开合 |
| `ParamEyeROpen` | 右眼开合 |
程序会在 `Groups` 中查找 `EyeBlink` 组:
```json
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
}
```
### 头部与身体跟随(必需)
| 参数 ID | 用途 |
| --- | --- |
| `ParamAngleX` | 头部左右旋转 |
| `ParamAngleY` | 头部上下旋转 |
| `ParamAngleZ` | 头部倾斜 |
| `ParamBodyAngleX` | 身体左右摇摆 |
| `ParamEyeBallX` | 眼球水平跟随 |
| `ParamEyeBallY` | 眼球垂直跟随 |
这些参数用于呼吸动效和鼠标/视线跟随,程序会自动驱动。
---
## 三、口型同步(LipSync)要求
程序会通过 `ParamMouthOpenY` 参数实时驱动嘴巴开合,模拟说话口型。该参数的值范围是 **0(闭嘴)~ 1(张嘴最大)**
### 对嘴部建模的要求
- **`ParamMouthOpenY` = 0 时**:嘴巴完全闭合,自然状态
- **`ParamMouthOpenY` = 1 时**:嘴巴张到最大
- 中间值(0.2~0.8)应有平滑的过渡,不能出现跳变或形变异常
- 嘴部形变需要在 0~1 全范围内看起来自然,因为程序会输出各种中间值
### 建模细节建议
程序内部使用 OVR LipSync 的 15 种 viseme(口型音素),每种映射到不同的开合度:
| 开合度范围 | 对应口型场景 | 对应 viseme 示例 |
| --- | --- | --- |
| 0.0 | 静音 / 闭嘴 | sil |
| 0.2~0.3 | 轻微张嘴(双唇音、唇齿音) | PP, FF, nn |
| 0.4~0.5 | 中度张嘴(齿音、舌音) | TH, DD, CH, SS, ih |
| 0.6~0.7 | 较大张嘴(元音、圆唇音) | kk, E, oh |
| 0.8~0.9 | 大张嘴(低元音、圆唇突出) | aa, ou |
请确保这些开合度值下嘴型看起来合理自然。
### 重要约束
- 动作(motion)和表情(expression)中**都不要**对 `ParamMouthOpenY` 设置关键帧,否则会与程序的实时口型驱动冲突
- 如果嘴部有多个参数(如 `ParamMouthForm` 控制嘴型宽窄),可以保留,但 `ParamMouthOpenY` 必须留给程序独占
---
## 四、动作(Motions)要求
### 动作组结构
模型必须包含以下两个动作组:
| 动作组名称 | 用途 | 说明 |
| --- | --- | --- |
| `Idle` | 待机动作 | 无对话时循环播放,至少 1 个 |
| `TapBody` | 语义动作 | 对话时由程序调用,按编号索引触发 |
### TapBody 动作清单
`TapBody` 组中的动作按**数组下标**(从 0 开始)索引触发。程序会根据后端传来的语义信号选择对应编号的动作。
需要制作以下 **18 类语义动作**,每类至少 1 个,推荐关键类别提供 2 个变体(用于强度区分):
| 编号 | 语义(behavior) | 动作描述 | 建议变体数 |
| --- | --- | --- | --- |
| 1 | nod(点头) | 轻微点头表示肯定 | 2(轻/重) |
| 2 | invite(邀请) | 伸手引导方向 | 1 |
| 3 | wave(挥手) | 挥手打招呼或告别 | 1 |
| 4 | reject(拒绝) | 摇头或摆手表示否定 | 2(轻/重) |
| 5 | think(思考) | 手托下巴、目光偏移 | 2(轻/重) |
| 6 | question(疑问) | 歪头、挑眉表示好奇 | 1 |
| 7 | explain(解释) | 双手摊开或单手比划说明 | 2(简述/详述) |
| 8 | recommend(推荐) | 单手指向或展示推荐姿态 | 1 |
| 9 | summary(总结) | 双手合拢或归拢手势 | 1 |
| 10 | wait(等待) | 双手交叠、微微前倾 | 1 |
| 11 | remind(提醒) | 食指轻点或举手提示 | 1 |
| 12 | warn(警告) | 严肃摆手或交叉手势 | 1 |
| 13 | thanks(感谢) | 鞠躬或双手合十 | 2(轻/重) |
| 14 | apology(道歉) | 鞠躬或低头表示歉意 | 1 |
| 15 | care(关心) | 微微前倾、温和手势 | 1 |
| 16 | celebrate(庆祝) | 举手欢呼、拍手 | 2(轻/重) |
| 17 | surprise(惊讶) | 后仰、手捂嘴或张大眼 | 1 |
| 18 | sad(悲伤) | 低头、肩膀下沉 | 2(轻/重) |
**关于编号与变体的说明:**
- TapBody 组中的动作按数组顺序排列,程序通过下标调用
- 有 2 个变体的类别:下标靠前的是**轻度版本**(intensity 低时使用),下标靠后的是**强度版本**(intensity 高时使用)
- 最终动作总数取决于变体数量,预计 **18~26 个**
- 交付时请提供一份**动作编号与语义的对照表**,方便我方配置映射
### 动作制作要求
- 每个动作 `FadeInTime``FadeOutTime` 建议设为 `0.5`
- 动作时长建议 2~4 秒,不要过长(对话过程中会频繁切换)
- 动作结束后应能自然过渡回待机姿态
- **动作中不要包含嘴部参数的关键帧**,口型由程序实时驱动,动作中写入嘴部数据会产生冲突
---
## 五、表情(Expressions)要求
表情用于控制面部细节(眉毛、眼睛、嘴角等),与动作独立叠加。
### 必需表情清单
至少制作以下 **6 个表情**,表情名称请严格按下表命名:
| 表情名称 | 语义(affect) | 面部特征描述 |
| --- | --- | --- |
| `F01` | 默认 / 微笑(smile, warm, neutral | 自然微笑,嘴角上扬,眉毛放松。这是最常用的表情 |
| `F02` | 严肃(serious) | 眉毛微蹙,嘴角平或略向下,目光坚定 |
| `F03` | 悲伤 / 歉意(sad, sorry) | 眉毛下垂,眼睛微眯,嘴角下拉 |
| `F04` | 好奇 / 惊讶 / 兴奋(curious, surprised, excited | 眉毛上挑,眼睛睁大 |
| `F05` | 害羞 / 不好意思 | 脸红、目光偏移、嘴角含笑(可选,备用) |
| `F06` | 得意 / 自信 | 眉毛上扬、嘴角上翘、目光明亮(可选,备用) |
**说明:**
- `F01`~`F04` 为**必需**,程序核心逻辑依赖这 4 个表情
- `F05`~`F06` 及更多为**可选**,交付后我方可扩展映射
- 如果能做更多差异化表情更好,但命名请保持 `F01``F02`... 的编号格式
- 表情文件格式为 `.exp3.json`,放在 `expressions/` 目录下
### 表情制作要求
- **表情中不要包含 `ParamMouthOpenY` 参数**,口型由程序实时驱动,表情中写入会产生冲突
- 表情是对基础状态的差值叠加,请确保各表情之间切换自然
- `F01`(微笑)是默认表情,对话结束后会自动恢复到此表情
---
## 六、model3.json 结构要求
最终交付的 `model3.json` 需要包含以下结构(以示例名 `MyModel` 为例):
```json
{
"Version": 3,
"FileReferences": {
"Moc": "MyModel.moc3",
"Textures": ["MyModel.2048/texture_00.png"],
"Physics": "MyModel.physics3.json",
"Pose": "MyModel.pose3.json",
"Expressions": [
{ "Name": "F01", "File": "expressions/F01.exp3.json" },
{ "Name": "F02", "File": "expressions/F02.exp3.json" },
{ "Name": "F03", "File": "expressions/F03.exp3.json" },
{ "Name": "F04", "File": "expressions/F04.exp3.json" }
],
"Motions": {
"Idle": [
{ "File": "motions/idle.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
],
"TapBody": [
{ "File": "motions/m01_nod_light.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
{ "File": "motions/m02_nod_heavy.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
"... 按照对照表顺序排列所有动作"
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
},
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": ["ParamMouthOpenY"]
}
],
"HitAreas": [
{ "Id": "HitArea", "Name": "Head" },
{ "Id": "HitArea2", "Name": "Body" }
]
}
```
---
## 七、目录结构示例
```
MyModel/
├── MyModel.model3.json # 模型配置(入口文件)
├── MyModel.moc3 # 模型数据
├── MyModel.physics3.json # 物理演算
├── MyModel.pose3.json # 姿态配置(可选)
├── MyModel.cdi3.json # 显示信息(可选)
├── MyModel.2048/ # 贴图目录
│ ├── texture_00.png
│ └── texture_01.png # 如有多张
├── expressions/ # 表情文件
│ ├── F01.exp3.json
│ ├── F02.exp3.json
│ ├── F03.exp3.json
│ └── F04.exp3.json
└── motions/ # 动作文件
├── idle.motion3.json
├── m01_nod_light.motion3.json
├── m02_nod_heavy.motion3.json
└── ...
```
---
## 八、交付清单
| 交付物 | 必需 | 说明 |
| --- | --- | --- |
| 完整模型导出目录 | 是 | 包含上述所有文件 |
| 动作编号对照表 | 是 | 说明 TapBody 中每个下标对应的语义和动作描述 |
| Cubism Editor 工程源文件(.cmo3 | 推荐 | 方便后续调整 |
| 动作源文件(.can3) | 推荐 | 方便后续调整动作 |
---
## 九、重要注意事项
1. **口型参数独占**`ParamMouthOpenY` 由程序实时驱动口型同步,动作和表情中**都不要**对此参数设置关键帧
2. **表情命名必须匹配**:表情名称必须是 `F01``F02`... 格式,程序按此名称调用
3. **动作组命名必须匹配**:待机组必须叫 `Idle`,语义动作组必须叫 `TapBody`
4. **TapBody 的顺序很重要**:程序按数组下标索引动作,请务必按对照表排列顺序
5. **动作不宜过长**:对话场景中动作切换频繁,单个动作建议 2~4 秒
6. **Groups 配置不能遗漏**`EyeBlink``LipSync` 两个 Group 必须在 model3.json 中正确声明
@@ -0,0 +1,187 @@
# Live2D 模型制作要求(简化版)
本文档用于指导外包制作 Live2D 数字人模型。本版为最小可用版本,仅满足基础对话场景。
---
## 一、技术规格
| 项目 | 要求 |
| -------------- | --------------------------------------------------- |
| Cubism 版本 | **Cubism 5** |
| model3.json 版本 | Version: 3 |
| 贴图尺寸 | 2048×2048 |
| 渲染目标 | 浏览器端 WebGL |
| 交付格式 | 完整 Cubism 导出目录(.moc3、贴图、motion3.json、physics3.json |
---
## 二、必需参数(Parameters
参数 ID 请严格使用 Cubism 默认命名。
| 参数 ID | 用途 | 说明 |
| ----------------- | ---- | ---------------- |
| `ParamMouthOpenY` | 嘴巴张合 | 程序实时驱动口型,**最关键** |
| `ParamEyeLOpen` | 左眼开合 | 程序驱动自动眨眼 |
| `ParamEyeROpen` | 右眼开合 | 程序驱动自动眨眼 |
| `ParamAngleX` | 头部左右 | 呼吸动效 + 视线跟随 |
| `ParamAngleY` | 头部上下 | 同上 |
| `ParamAngleZ` | 头部倾斜 | 同上 |
| `ParamBodyAngleX` | 身体摇摆 | 呼吸动效 |
| `ParamEyeBallX` | 眼球水平 | 视线跟随 |
| `ParamEyeBallY` | 眼球垂直 | 视线跟随 |
---
## 三、口型同步(LipSync)要求
程序会通过 `ParamMouthOpenY` 参数实时驱动嘴巴开合,模拟说话口型。该参数的值范围是 **0(闭嘴)~ 1(张嘴最大)**
### 对嘴部建模的要求
- **`ParamMouthOpenY` = 0 时**:嘴巴完全闭合,自然状态
- **`ParamMouthOpenY` = 1 时**:嘴巴张到最大
- 中间值(0.2~0.8)应有平滑的过渡,不能出现跳变或形变异常
- 嘴部形变需要在 0~1 全范围内看起来自然,因为程序会输出各种中间值
### 建模细节建议
程序内部会产生以下几种典型开合度,请确保这些值下嘴型看起来合理:
| 开合度范围 | 对应口型场景 |
| --- | --- |
| 0.0 | 静音 / 闭嘴 |
| 0.2~0.3 | 轻微张嘴(双唇音 p/b、唇齿音 f) |
| 0.4~0.5 | 中度张嘴(齿音 d/t、舌音 ch/s) |
| 0.6~0.7 | 较大张嘴(元音 e、圆唇音 o) |
| 0.8~0.9 | 大张嘴(低元音 a、圆唇突出 ou) |
### 重要约束
- 动作(motion)和表情(expression)中**都不要**对 `ParamMouthOpenY` 设置关键帧,否则会与程序的实时口型驱动冲突
- 如果嘴部有多个参数(如 `ParamMouthForm` 控制嘴型宽窄),可以保留,但 `ParamMouthOpenY` 必须留给程序独占
---
## 四、动作(Motions)要求
不需要表情文件,不需要情绪系统。只需要以下动作:
### 动作组结构
| 动作组名称 | 用途 |
| --------- | -------------- |
| `Idle` | 待机,无对话时循环播放 |
| `TapBody` | 对话动作,程序按数组下标调用 |
### Idle 组(至少 1 个)
自然站立的待机循环动画,有轻微呼吸感。
### TapBody 组(共 5 个动作)
程序按 **数组下标(从 0 开始)** 调用,顺序必须严格按下表排列:
| 下标 | 动作名称 | 动作描述 | 使用场景 |
| --- | ---- | ---------------------- | ----------- |
| 0 | 聆听 | 微微前倾、略点头,表现出认真听对方说话的姿态 | 用户正在输入/说话时 |
| 1 | 说话 | 自然的说话姿态,可有轻微手势,身体略有律动 | AI 回复时的默认动作 |
| 2 | 思考 | 手托下巴或目光微偏上方,表现在想问题 | AI 正在生成回复时 |
| 3 | 等待 | 双手自然交叠或轻微晃动,耐心等待的姿态 | 空闲等待用户操作时 |
| 4 | 打招呼 | 挥手或点头致意 | 对话开始时的问候 |
### 动作制作要求
- `FadeInTime``FadeOutTime` 设为 `0.5`
- 动作时长 **2~4 秒**
- 动作结束后能自然过渡回待机姿态
- **动作中不要包含 `ParamMouthOpenY` 的关键帧**,口型由程序实时驱动,写入会冲突
---
## 五、model3.json 结构要求
`MyModel` 为例:
```json
{
"Version": 3,
"FileReferences": {
"Moc": "MyModel.moc3",
"Textures": ["MyModel.2048/texture_00.png"],
"Physics": "MyModel.physics3.json",
"Motions": {
"Idle": [
{ "File": "motions/idle.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
],
"TapBody": [
{ "File": "motions/listen.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
{ "File": "motions/speak.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
{ "File": "motions/think.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
{ "File": "motions/wait.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
{ "File": "motions/greet.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
},
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": ["ParamMouthOpenY"]
}
],
"HitAreas": [
{ "Id": "HitArea", "Name": "Body" }
]
}
```
**关键点:**
- `Groups``EyeBlink``LipSync` 必须声明,程序依赖这两个组实现自动眨眼和口型同步
- `TapBody` 数组中的顺序必须是:聆听、说话、思考、等待、打招呼
---
## 六、目录结构
```
MyModel/
├── MyModel.model3.json
├── MyModel.moc3
├── MyModel.physics3.json
├── MyModel.2048/
│ └── texture_00.png
└── motions/
├── idle.motion3.json
├── listen.motion3.json # 下标 0 - 聆听
├── speak.motion3.json # 下标 1 - 说话
├── think.motion3.json # 下标 2 - 思考
├── wait.motion3.json # 下标 3 - 等待
└── greet.motion3.json # 下标 4 - 打招呼
```
---
## 七、交付清单
| 交付物 | 必需 |
| -------------------------- | --- |
| 完整模型导出目录(上述所有文件) | 是 |
| Cubism Editor 工程源文件(.cmo3 | 推荐 |
| 动作源文件(.can3) | 推荐 |
---
## 八、注意事项
1. **`ParamMouthOpenY` 不要动**:动作中不要对嘴巴参数设关键帧,程序实时驱动口型
2. **动作组必须叫 `Idle` 和 `TapBody`**:程序按这两个名字查找
3. **TapBody 顺序不能变**:程序按下标 0~4 调用,顺序错了动作就对不上
4. **`EyeBlink``LipSync` Groups 不能漏**:没有这两个声明,自动眨眼和口型同步不会生效
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB