diff --git a/config/action_rules.csv b/config/action_rules.csv new file mode 100644 index 0000000..de98a09 --- /dev/null +++ b/config/action_rules.csv @@ -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,难过|可惜|遗憾|糟糕|失败了|伤心 diff --git a/core/action_signal.py b/core/action_signal.py new file mode 100644 index 0000000..0fb8006 --- /dev/null +++ b/core/action_signal.py @@ -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, + } diff --git a/core/fay_core.py b/core/fay_core.py index f767b8c..6c78746 100644 --- a/core/fay_core.py +++ b/core/fay_core.py @@ -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 diff --git a/core/wsa_server.py b/core/wsa_server.py index f2d5feb..e166efc 100644 --- a/core/wsa_server.py +++ b/core/wsa_server.py @@ -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() diff --git a/docs/Fay侧标准动作改造说明.md b/docs/Fay侧标准动作改造说明.md new file mode 100644 index 0000000..1a6d60d --- /dev/null +++ b/docs/Fay侧标准动作改造说明.md @@ -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 + } + } +} +``` + +即可。 + +后续具体怎么动,由各前端或驱动端通过配置表自己决定。 diff --git a/docs/Live2D模型制作要求.md b/docs/Live2D模型制作要求.md new file mode 100644 index 0000000..c1858c2 --- /dev/null +++ b/docs/Live2D模型制作要求.md @@ -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 等) | +| 渲染目标 | 浏览器端 WebGL(Cubism 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 中正确声明 diff --git a/docs/Live2D模型制作要求(简化版).md b/docs/Live2D模型制作要求(简化版).md new file mode 100644 index 0000000..480e3b3 --- /dev/null +++ b/docs/Live2D模型制作要求(简化版).md @@ -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 不能漏**:没有这两个声明,自动眨眼和口型同步不会生效 diff --git a/readme/wechat.jpg b/readme/wechat.jpg new file mode 100644 index 0000000..88c9557 Binary files /dev/null and b/readme/wechat.jpg differ diff --git a/readme/wechat.png b/readme/wechat.png deleted file mode 100644 index ac923e8..0000000 Binary files a/readme/wechat.png and /dev/null differ