# affect-chat-frontend-demo
**Repository Path**: haukzero/affect-chat-frontend-demo
## Basic Information
- **Project Name**: affect-chat-frontend-demo
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 2
- **Created**: 2026-05-06
- **Last Updated**: 2026-05-17
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Affect Chat — 情感计算交互原型
一个最小化的前端 Demo,演示 **LLM 流式对话 + 文本情感识别 + 语音合成 + Live2D 表情** 的端到端联动。
- **零依赖前端**:纯 HTML / CSS / 原生 JS(无构建步骤、无 npm)。
- **零依赖后端**:Python 标准库 `http.server`,作为静态文件服务器 + OpenAI 兼容代理 + 桩服务。
- **协议统一**:前端始终讲 OpenAI `/v1/chat/completions` 流式协议。切换 vLLM / DashScope / OpenAI 只需在侧边栏改 Base URL。
## 启动
```bash
python backend/server.py # http://127.0.0.1:8787
```
打开 即可。`--host` / `--port` 可改。
## 切换 LLM 上游
侧边栏三个字段全部留空时使用本地 fake LLM。要接真实模型:
| 字段 | 示例 |
|------------|-------------------------------------------------------------------------|
| Base URL | `http://10.10.111.36:8001` · `https://dashscope.aliyuncs.com/compatible-mode/v1` · `https://api.openai.com/v1` |
| Model | `qwen-turbo` · `gpt-4o-mini` · `/root/share/models/Qwen3-30B-A3B/` |
| API Key | `sk-...`(vLLM 不需要时留空) |
后端会自动把 Base URL 归一化成 `/v1/chat/completions`,并通过本地代理转发以绕开浏览器 CORS 限制。
## 目录结构
```
backend/
server.py HTTP 路由 + 静态文件
proxy.py OpenAI 上游透明代理
services/
fake_llm.py OpenAI 流式协议的占位 LLM
fake_emotion.py 文本→情感分类(随机桩)
emotion2vec_service.py 语音(wav)→情感分类(emotion2vec)
fake_voice.py (text, emotion)→语音元数据(桩)
frontend/
index.html
src/
app.js UI 状态、消息渲染、事件绑定
config.js 全部端点 / prompt 路径 / 上下文参数
styles.css
core/ ↓ 与传输协议直接耦合的客户端
llm-client.js OpenAI 流式 + 非流式
emotion-client.js
voice-client.js
pipeline/ ↓ 业务编排(不感知具体后端)
prompt-store.js 懒加载 + 缓存 prompt 文本文件
think-splitter.js 切分 ...
context-manager.js 滑动窗口 + 摘要压缩
chat-service.js 端到端事件流编排器
live2d/ Live2D 渲染、情绪→参数映射
assets/
prompts/ system / summarize prompt 文件
live2d/ Live2D 模型
vendor/ pixi、cubism4 等第三方库
```
## 设计原则
1. **Prompt 是数据,不是代码**。所有 prompt 都在 `frontend/assets/prompts/*.txt`,运行时 fetch。
2. **协议层 vs 编排层**。`core/` 只关心一次 HTTP 调用;`pipeline/` 编排多次调用、上下文、业务事件。
3. **切换 LLM 只改 URL**。前端发送的请求体始终是标准 OpenAI 格式;本地代理负责转发,不做任何 payload 改写。
4. **失败回合不入历史**。后端报错的对话会从 session 中弹出,不污染下一轮上下文。
## 上下文管理
由 `pipeline/context-manager.js` 实现,参数在 `config.js` 的 `context` 段:
- `maxWindowMessages`:发给模型的最近 N 条消息上限。
- `summaryTriggerMessages`:未摘要消息超过此阈值时触发压缩。
- `summaryBatchSize`:每次压缩折叠的旧消息数。
压缩通过同一个 OpenAI endpoint 调用 `chatOnce` 完成,使用 `summarize.txt` 作为 system prompt。
## Live2D
模型放在 `frontend/assets/live2d//`,路径在 `config.js` 的 `live2d.modelPath` 配置。情感→动作映射见 `live2d.motionMap`。
## 语音输入情绪识别(emotion2vec)
项目已集成 `POST /api/emotion/audio`:
- 前端点击输入框左侧麦克风按钮开始录音(最多约 8 秒)。
- 浏览器端将录音转成 16kHz 单声道 WAV 并 base64 上传。
- 后端通过 `emotion2vec` 推理并返回统一情感契约:
- 若浏览器支持 Web Speech API(`SpeechRecognition`),会并行做语音转写并自动填入输入框(默认自动发送)。
```jsonc
{ "emotion": "joy", "label": "开心", "confidence": 91 }
```
### 安装依赖
```bash
pip install -r requirements.txt
```
### 可选环境变量
- `EMOTION2VEC_MODEL`:默认 `iic/emotion2vec_plus_seed`
- `EMOTION2VEC_HUB`:默认 `hf`(可选 `ms`)
- `EMOTION2VEC_PRELOAD`:默认 `0`,采用懒加载(首次语音请求时再加载模型)
- `EMOTION2VEC_WARMUP`:默认 `0`,不在启动阶段做 warmup 推理
- `EMOTION2VEC_REQUIRED`:默认 `1`,预加载失败将直接退出服务(推荐)
- `EMOTION2VEC_DISABLE_UPDATE`:默认 `1`,关闭 FunASR 启动时更新检查日志
默认是轻启动模式,服务启动更快;首次调用 `/api/emotion/audio` 时才会触发模型加载与依赖检查。
若希望恢复“启动即预加载”的旧行为,可设置:
```bash
set EMOTION2VEC_PRELOAD=1
set EMOTION2VEC_WARMUP=1
```
项目已加入 `imageio-ffmpeg` 兜底,若系统 PATH 中没有 `ffmpeg`,会尝试自动补齐运行时依赖。
示例:
```bash
set EMOTION2VEC_MODEL=iic/emotion2vec_plus_base
set EMOTION2VEC_HUB=hf
python backend/server.py
```
### 语音输入前端开关
`frontend/src/config.js` 的 `voiceInput` 可调:
- `useBrowserSTT`:启用/禁用浏览器 STT(默认 `true`)
- `language`:STT 语言(默认 `zh-CN`)
- `autoSend`:转写后自动发送(默认 `true`)
- `sttWaitMs`:停止录音后等待 STT 收敛的毫秒数(默认 `1500`)
- `prependEmotionHint`:是否在转写前加情绪提示前缀(默认 `false`)
## 集成真实的情感识别 / 语音合成
> 当前 `backend/services/fake_emotion.py` 与 `fake_voice.py` 都是占位实现。前端只与下列两个 HTTP 契约耦合,**只要新实现保持同样的入参与返回字段,前端不需要任何改动**。
>
> ### 契约
>
> **`POST /api/emotion`**
> ```jsonc
> // 请求
> { "text": "今天好开心啊" }
> // 响应
> { "emotion": "joy", "label": "开心", "confidence": 87 }
> ```
> `emotion` 必须是 `config.js` `live2d.motionMap` 里的键之一(默认:`calm` / `joy` / `sadness` / `anger` / `surprise` / `thinking`)。新增标签时同步更新 `motionMap`。
>
> **`POST /api/voice`**
> ```jsonc
> // 请求
> { "text": "...", "emotion": "joy" }
> // 响应
> { "voice": "Bright Companion", "audioUrl": "https://.../xxx.mp3", "duration": 2.4 }
> ```
> `audioUrl` 留 `null` 时前端只展示音色名;返回有效 URL 时前端会自动播放(音频元素绑定在 `app.js`)。
>
> ### 接入步骤
>
> 1. **替换实现文件**,保持模块导出函数签名不变:
> - `backend/services/fake_emotion.py::recognize(text) -> dict`
> - `backend/services/fake_voice.py::synthesize(text, emotion) -> dict`
> 2. 把第三方 SDK / HTTP 客户端的依赖加进环境(按需,例如 `pip install dashscope` / `transformers` / `edge-tts`)。
> 3. 重启 `backend/server.py`,刷新浏览器。
>
> ### 示例骨架
>
> **情感识别 — 调本地 HuggingFace 模型**
> ```python
> # backend/services/fake_emotion.py 直接覆盖
> from transformers import pipeline
> _clf = pipeline("text-classification",
> model="uer/roberta-base-finetuned-jd-binary-chinese")
> _MAP = {"positive": "joy", "negative": "sadness"} # 按你的模型标签映射
>
> def recognize(text: str) -> dict:
> out = _clf(text, truncation=True)[0]
> emo = _MAP.get(out["label"].lower(), "calm")
> return {"emotion": emo, "label": emo, "confidence": int(out["score"] * 100)}
> ```
>
> **语音合成 — 调云端 TTS(以 edge-tts 为例)**
> ```python
> # backend/services/fake_voice.py 直接覆盖
> import asyncio, hashlib, edge_tts
> from pathlib import Path
>
> CACHE = Path(__file__).resolve().parent.parent.parent / "frontend" / "assets" / "tts"
> CACHE.mkdir(exist_ok=True)
> VOICE_BY_EMOTION = {"joy": "zh-CN-XiaoyiNeural", "sadness": "zh-CN-YunxiNeural", ...}
>
> def synthesize(text: str, emotion: str) -> dict:
> voice = VOICE_BY_EMOTION.get(emotion, "zh-CN-XiaoxiaoNeural")
> name = hashlib.md5(f"{voice}|{text}".encode()).hexdigest() + ".mp3"
> path = CACHE / name
> if not path.exists():
> asyncio.run(edge_tts.Communicate(text, voice).save(str(path)))
> return {"voice": voice, "audioUrl": f"/assets/tts/{name}", "duration": None}
> ```
> 由于 `backend/server.py` 已经把整个 `frontend/` 当作静态根目录,写入 `frontend/assets/tts/` 的文件会自动通过 `/assets/tts/` 暴露。
>
> ### 替换 LLM 后端
>
> 真实 LLM 不要改后端。直接在浏览器侧边栏填好 Base URL / Model / API Key,本地后端会通过 `X-LLM-Upstream` 头透明代理,避免任何 CORS / 凭据泄漏问题。
## 开发提示
- 改完前端无需重启服务器,刷新浏览器即可。
- 改 Python 代码后需要重启 `backend/server.py`。
- 验证后端:`curl -N http://127.0.0.1:8787/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"x","stream":true,"messages":[{"role":"user","content":"hi"}]}'`