# 虚拟父母陪伴系统 (parent_companion)

## 📋 项目概述

基于 Web 的智能亲情陪伴应用。用户可以与 AI 扮演的"虚拟父母"进行文字聊天或实时语音通话，获得情感支持与陪伴。AI 会自动发布"家人动态"（类似朋友圈），主动出现在用户生活中。

### 核心特性

- **四种角色**：温柔妈妈、严格妈妈、温柔爸爸、严格爸爸，每种角色配有独立人设、音色与少样本对话示例
- **双模式交互**：文字聊天（支持图片识别）+ 实时语音通话
- **豆包大模型驱动**：火山引擎 Ark 平台语言模型（ep-m-20260401231121-qf7cp）
- **豆包语音服务**：TTS 语音合成 + ASR 大模型流式语音识别，均通过本地 Python 后端代理
- **智能打断检测**：VAD 语音活动检测，用户说话时可即时打断 AI 朗读
- **流式 AI 回复**：语音通话时 AI 边生成边返回文本，整段一次性 TTS 合成，零停顿播放
- **滚动记忆摘要**：超过 8 轮对话自动压缩为记忆摘要，AI 能记住早期聊过的内容
- **家人动态（朋友圈）**：AI 自动生成文案 + GPT-Image-2 生成配图，模拟父母主动发朋友圈
- **响应式设计**：适配桌面端与移动端浏览器

---

## 📁 项目结构

```
parent_companion/
├── chat_app.html                 # 主聊天页面（HTML 骨架，~120 行）
├── family_moments.html           # 家人动态页面（朋友圈风格）
├── role_selection.html           # 角色选择向导（4步配置流程）
├── moments_data.json             # 家人动态数据（自动生成，持久化存储）
├── README.md                     # 本文档
│
├── server/
│   └── tts_server.py             # Python 后端（TTS + ASR + 生图 + 动态生成）
│
└── src/
    ├── css/
    │   └── style.css             # 聊天页面样式
    └── js/
        ├── config.js             # 全局变量、API 地址、常量（VAD 阈值等）
        ├── role.js               # 初始化、角色配置加载、UI 更新、加载记忆摘要
        ├── asr.js                # 豆包 ASR WebSocket 客户端 + PCM 音频推送 + 防重复锁
        ├── tts.js                # TTS 播放（豆包 API）+ 浏览器 speechSynthesis 降级
        ├── vad.js                # VAD 打断检测（能量阈值 + 确认帧）
        ├── process.js            # processVoiceInput（流式 AI → 整段 TTS → 播放）
        ├── voice-call.js         # 语音通话初始化
        ├── call-ui.js            # 通话界面控制（显示/挂断/静音/免提/停止通话）
        ├── stop-call.js          # 停止语音通话
        ├── interrupt.js          # triggerInterrupt + interruptSpeaking（打断处理）
        ├── interrupt-handler.js  # 打断入口（供外部调用）
        ├── api.js                # callDoubaoAPI（非流式）+ callDoubaoAPIStream（流式）+ 记忆摘要注入
        ├── messages.js           # 消息渲染、打字机效果、简易 markdown
        ├── chat.js               # 文字聊天输入、图片上传、快捷建议
        ├── image.js              # 图片选择/预览
        ├── ui-helpers.js         # 状态文字、波形动画、通话计时器
        ├── settings.js           # API Key 配置、角色重置
        ├── history.js            # 对话历史 localStorage 存取（按角色分开）
        ├── history-mem.js        # 滚动记忆摘要（conversationHistory + memorySummary + 自动压缩）
        └── legacy-speech.js      # 浏览器 Web Speech API 封装（safeStart/StopRecognition）
```

---

## 🚀 快速启动

### 环境要求

- Python 3.8+
- Chrome / Edge 浏览器（需支持麦克风权限）
- 网络代理（如 Clash，用于访问 Grsai 生图 API）

### 安装依赖

```bash
pip install fastapi uvicorn websockets pydantic aiohttp
```

### 启动服务

**窗口 1 — 后端服务（8000 端口）：**
```bash
cd C:\trae_project\scan\parent_companion
python server\tts_server.py
```
等待看到 `Uvicorn running on http://0.0.0.0:8000` 和 4 个音色 ✅ 验证通过。

**窗口 2 — 前端静态服务（3000 端口）：**
```bash
cd C:\trae_project\scan\parent_companion
python -m http.server 3000
```

**浏览器访问：**
```
http://localhost:3000/chat_app.html        # 聊天/通话
http://localhost:3000/family_moments.html   # 家人动态
```

### 首次使用

1. 首次打开 `chat_app.html` 会跳转到角色选择页面（role_selection.html）
2. 完成 4 步配置：选择角色 → 选择性格 → 设置称呼/昵称 → 完成
3. 点击页面右上角 ⚙️ 配置火山引擎 Ark API Key
4. 打开 `family_moments.html`，点击"✨ 生成家人动态"初始化动态内容

---

## 🔧 技术架构

### 整体架构

| 层次 | 技术 / 服务 |
|------|------------|
| 前端 | HTML5 + CSS3 + 原生 JavaScript（无框架），20 个 JS 模块 |
| 本地后端 | Python 3 + FastAPI + Uvicorn，运行于 localhost:8000 |
| AI 对话 | 火山引擎 Ark 平台，模型 ep-m-20260401231121-qf7cp（豆包系列）|
| TTS 语音合成 | 豆包 TTS（ws_binary 协议），通过 tts_server.py 代理 |
| ASR 语音识别 | 豆包大模型流式 ASR v3（二进制帧协议），通过 tts_server.py 代理 |
| 图片生成 | Grsai GPT-Image-2 API，通过 tts_server.py 代理（需代理 127.0.0.1:7890）|
| 本地存储 | localStorage（角色配置、API Key、聊天历史、记忆摘要）|
| 动态数据 | moments_data.json（服务端文件存储）|

### 后端接口（server/tts_server.py）

| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/audio/speech` | POST | TTS 合成，接受 `{text, voice_key, rate, volume, role, personality}`，返回 MP3。失败自动重试 1 次 |
| `/asr` | WebSocket | ASR 代理，v3 二进制帧协议，只转发新增的 definite 分句 |
| `/v1/images/generate` | POST | 图片生成代理，接受 `{prompt, size, quality}`，通过 Clash 代理调用 Grsai API |
| `/v1/moments` | GET | 获取所有家人动态（从 moments_data.json 读取）|
| `/v1/moments/generate` | POST | AI 生成一条新动态（文案 + 配图）|
| `/v1/moments/batch-generate` | POST | 批量生成 5 条动态（首次初始化用）|
| `/voices` | GET | 返回可用音色映射表 |
| `/health` | GET | 健康检查 |

### ASR 协议要点（v3 bigmodel 二进制帧）

- 鉴权：HTTP Header `X-Api-App-Key` + `X-Api-Access-Key` + `X-Api-Resource-Id`
- 连接后先发 `full_client_request`（4B header + 4B size + gzip(JSON payload)）
- 持续发送 `audio_only_request`（PCM 音频 gzip 压缩，4096 samples/帧）
- 最后一包设置 flags=0b0010
- 服务端返回 `full_server_response`（gzip(JSON)），包含累积的 `result.text` 和 `utterances[]`
- 后端用 `forwarded_count` 追踪已转发分句数，只发送新增的 `definite=true` 分句
- ASR 连接全程保持，不随 TTS 播放/打断而断开，靠 `_asrProcessing` 锁控制处理时机
- 静音/AI 说话期间发送全零静默帧保持连接活跃

### TTS 协议要点（ws_binary）

- WebSocket `wss://openspeech.bytedance.com/api/v1/tts/ws_binary`
- 发送 gzip 压缩的 JSON payload（含 text、voice_type、rate 等）
- header_size 动态计算：`(raw[0] & 0x0F) * 4`
- `msg_type=0xb` 音频数据帧，`0xe` 最后一包，`0xf` 错误帧
- recv 超时 5 秒，已有数据时正常返回（豆包有时不发结束帧）
- 合成失败自动重试 1 次
- 整段文本一次性合成，不分段

### 语音通话流程（流式版）

```
用户说话
  → ASR 识别到 definite 分句 → processVoiceInput(text)
  → callDoubaoAPIStream()：stream=true，流式接收 AI 全部回复
  → 整段文本一次性 POST 到 /v1/audio/speech
  → Audio 元素播放 MP3（零分段停顿）
  → 播放 2.5 秒后启动 VAD 打断检测
  → 播完 → _unlockASR() 解锁 → 等待下一句
  → 如果用户打断 → triggerInterrupt() → 停 TTS + 解锁 ASR → 继续识别
```

### 打断机制

- VAD 阈值：`baselineAvg + 35`（加性偏移，避免扬声器回声导致阈值飙升）
- 确认帧：连续 6 帧（480ms）超阈值才触发打断
- 打断时：`ttsPlayToken++` 废弃当前 TTS + `_streamToken++` 废弃流式队列 + `_unlockASR()` 解锁
- ASR 连接不断开，打断后立刻可以识别新的语音

### 前端模块说明

| 模块 | 职责 |
|------|------|
| `config.js` | 全局状态变量、VAD 阈值常量（VAD_THRESHOLD=58, VAD_CONFIRM_FRAMES=6）|
| `role.js` | 从 localStorage 加载角色配置，更新头像/标题，加载记忆摘要 |
| `asr.js` | 建立 ASR WebSocket，ScriptProcessorNode 采集 PCM（4096 samples/帧），静音时发全零帧。只处理 `definite===true` 的分句，`_asrProcessing` 锁 + `_lastProcessedASR` 防重复，`_unlockASR()` 暴露解锁接口 |
| `tts.js` | POST 到 `/v1/audio/speech` 获取 MP3，Audio 元素播放，30 秒超时。打断时不降级。仅网络错误才降级到浏览器 speechSynthesis |
| `vad.js` | 采集 480ms 底噪基线，动态阈值 `max(58, baseline+35)`。每 80ms 检测频率能量，连续 6 帧触发打断 |
| `process.js` | 流式 AI 生成 → 整段 TTS 合成 → Audio 播放。使用 `_streamToken` 控制打断废弃 |
| `interrupt.js` | `triggerInterrupt()`：递增 ttsPlayToken + streamToken，清空队列，停 TTS/VAD，解锁 ASR |
| `api.js` | `_buildAPIPayload()`：注入 systemPrompt + 记忆摘要 + 通话模式提示。非流式 `callDoubaoAPI()` + 流式 `callDoubaoAPIStream()`。每轮对话后异步触发 `_tryCompressMemory()` |
| `history-mem.js` | `conversationHistory` 数组 + `memorySummary` 摘要。`RECENT_TURNS=6` 轮发给 AI，超过 `SUMMARY_TRIGGER=8` 轮自动压缩旧对话为 ≤200 字摘要。摘要存 localStorage（按角色隔离）|
| `messages.js` | 聊天消息 DOM 渲染，逐字打字机效果，简易 markdown（加粗+换行）|

---

## 🏠 家人动态系统

### 概述

模拟父母发朋友圈，AI 自动生成文案 + 配图，用户打开页面直接看到。

### 动态类型（5 种，带权重随机）

| 类型 | 权重 | 内容 |
|------|------|------|
| image_text | 3 | AI 生成文字 + GPT-Image-2 生成配图 |
| image_mood | 2 | GPT-Image-2 生成图片 + 心情标签 |
| text_only | 3 | 纯文字（想念/关心/分享生活）|
| music_text | 1 | 文字 + 随机音乐卡片 |
| music_mood | 1 | 心情标签 + 随机音乐卡片 |

### 生成流程

```
触发生成（手动点击 ✨ 或批量生成）
  → 随机选择动态类型（带权重）
  → 调用豆包大模型生成文案（根据时间段、类型、记忆摘要）
  → 如果需要配图 → 调用 Grsai GPT-Image-2 生成（通过 Clash 代理）
  → 如果是音乐类型 → 从预置音乐池随机选一首
  → 存入 moments_data.json（持久化）
  → 前端 GET /v1/moments 加载显示
```

### 音乐池（预置 5 首）

| 标题 | 频道 | 场景 |
|------|------|------|
| 时光慢慢 | 妈妈的电台 | 怀旧 |
| 月光摇篮曲 | 晚安频道 | 睡前 |
| 春风十里 | 午后时光 | 放松 |
| 温暖的风 | 回家的路 | 想念 |
| 星空下的约定 | 深夜电台 | 深夜 |

### 图片生成配置

- API：Grsai GPT-Image-2（`https://api.grsai.com/v1/images/generations`）
- API Key：`sk-70f90b7377ec48f7b564da4f085b44d3`
- 需要网络代理：`http://127.0.0.1:7890`（Clash）
- 图片风格：prompt 追加 `no text, no watermark, realistic phone photography`
- 尺寸：1024x1024，quality: medium

---

## 🎭 四种角色

| 角色键 | 音色 | 温度 | 人设核心 |
|--------|------|------|----------|
| mother_gentle | zh_female_wenroumama_uranus_bigtts | 0.75 | 温柔坚定的职场妈妈，先共情再引导 |
| mother_strict | zh_female_vv_uranus_bigtts | 0.7 | 成功商业女性高管，有格局 |
| father_gentle | zh_male_m191_uranus_bigtts | 0.7 | 温润慈爱的爸爸，重陪伴与情绪守护 |
| father_strict | zh_male_taocheng_uranus_bigtts | 0.7 | 顶级商业精英，以格局引领子女 |

---

## 🧠 记忆系统

### 滚动记忆摘要

| 参数 | 值 | 说明 |
|------|------|------|
| MAX_HISTORY_TURNS | 20 | 内存最大保留轮数 |
| RECENT_TURNS | 6 | 每次发给 AI 的最近对话轮数 |
| SUMMARY_TRIGGER | 8 | 超过此轮数触发摘要压缩 |

### 工作流程

```
第 1-8 轮：正常对话，最近 6 轮作为上下文
第 8 轮时：自动压缩最旧的对话 → 调用豆包生成 ≤200 字摘要
第 9 轮起：systemPrompt 注入【你对这个孩子的记忆】+ 摘要 + 最近 6 轮
第 16 轮时：再次压缩，新信息融合到摘要中
```

### AI 看到的上下文结构

```
[角色人设 systemPrompt]
[记忆摘要：孩子最近工作压力大，喜欢吃巧克力蛋糕...]
[语音通话模式提示（如果在通话中）]
[few-shot 示例对话]
[最近 6 轮对话原文]
[用户新消息]
```

---

## ⚙️ API 凭证汇总

| 服务 | 参数 | 值 |
|------|------|------|
| TTS | AppID | 9975991600 |
| TTS | Token | 5YnN_op5zvSFP-7JrxxyY1pdLdY9UdtI |
| TTS | Cluster | volcano_tts |
| ASR | AppID | 6173189800 |
| ASR | Token | W7Qv23m1cJUHZe1ystl7bgFGtXvjOX7Z |
| ASR | Resource ID | volc.bigasr.sauc.duration |
| 生图 | Grsai Key | sk-70f90b7377ec48f7b564da4f085b44d3 |
| 生图 | 代理 | http://127.0.0.1:7890 |
| AI 对话 | Ark API Key | 9ec9fd5a-4e49-46b3-9c41-e03a48a24f67（默认值）|
| AI 对话 | 模型端点 | ep-m-20260401231121-qf7cp |

> ⚠️ 生产环境应使用环境变量管理凭证，不要硬编码在源码中。

---

## 🔔 已知问题

1. **打断后第一句话**：打断时 ASR 可能收到混合音频，打断后的第一句话可能不完整
2. **ScriptProcessorNode 过时**：W3C deprecated，未来需迁移至 AudioWorkletNode
3. **TTS 偶发空数据**：豆包 TTS 偶尔返回 0 bytes，后端自动重试 1 次
4. **ASR 超时断连**：长时间无语音触发 `45000081`，onclose 自动 2 秒后重连
5. **生图需要代理**：Grsai API 需通过 Clash（127.0.0.1:7890）访问
6. **家人动态生成耗时**：每条含图片的动态需要 30-60 秒（AI 文案 + 图片生成）

---

## 📝 版本历史

| 日期 | 版本 | 变更内容 |
|------|------|----------|
| 2026-05-14 | v1.0 | 初始版本，文字聊天 + 语音通话 |
| 2026-05-18 | v2.0 | ASR 迁移至 v3 bigmodel 二进制协议；Seed ASR 2.0；TTS 分段合成；ASR 防重复 |
| 2026-05-20 | v2.1 | 项目重构为 20 个模块；关闭安全体验模式 |
| 2026-05-21 | v3.0 | TTS 帧解析修复（动态 header_size）；打断机制重写；VAD 阈值改为加性偏移；流式 AI 回复；整段 TTS 零停顿 |
| 2026-05-24 | v3.1 | 滚动记忆摘要；家人动态系统（AI 文案 + GPT-Image-2 生图）；Grsai API 代理接入 |

---

## 🎯 后续优化计划

- [ ] 家人动态定时自动发布（后台定时任务，每天 2-3 条）
- [ ] 动态内容融入用户记忆摘要
- [ ] 图片预生成缓存（零等待）
- [ ] 用户体系（登录/注册，记忆隔离）
- [ ] ScriptProcessorNode → AudioWorkletNode
- [ ] 凭证改为环境变量 / .env
- [ ] 深色模式
- [ ] 向量数据库长期记忆（ChromaDB + embedding）
- [ ] AI 主动赠送音乐功能
- [ ] 妈妈的歌单 / 情绪时间线
