# 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"}]}'`