From 28677fe08c771f8b2c1eb77a30911553ba6ca032 Mon Sep 17 00:00:00 2001 From: Lilixu007 <1273914445@qq.com> Date: Thu, 5 Mar 2026 17:18:04 +0800 Subject: [PATCH] LLM --- lover/routers/voice_call.py | 205 +++++++++++++++++++ test_voice_conversation.py | 130 ++++++++++++ xuniYou/pages/chat/phone.vue | 245 ++++++++++++++++++++-- 完整启动指南.md | 329 ++++++++++++++++++++++++++++++ 快速参考.md | 186 +++++++++++++++++ 语音对话功能说明.md | 383 +++++++++++++++++++++++++++++++++++ 语音对话快速开始.md | 285 ++++++++++++++++++++++++++ 语音播放问题排查.md | 286 ++++++++++++++++++++++++++ 8 files changed, 2034 insertions(+), 15 deletions(-) create mode 100644 test_voice_conversation.py create mode 100644 完整启动指南.md create mode 100644 快速参考.md create mode 100644 语音对话功能说明.md create mode 100644 语音对话快速开始.md create mode 100644 语音播放问题排查.md diff --git a/lover/routers/voice_call.py b/lover/routers/voice_call.py index d3d6779..ce9af09 100644 --- a/lover/routers/voice_call.py +++ b/lover/routers/voice_call.py @@ -1460,3 +1460,208 @@ async def voice_call(websocket: WebSocket): pass finally: await session.close() + + +@router.post("/call/conversation") +async def voice_conversation( + request: dict, + user: AuthedUser = Depends(get_current_user) +): + """ + 完整的语音对话流程: + 1. 接收音频数据(base64) + 2. ASR 识别为文字 + 3. LLM 生成回复 + 4. TTS 合成语音 + 5. 返回语音数据(base64) + """ + try: + # 1. 接收并解码音频数据 + if 'audio_data' not in request: + raise HTTPException(status_code=400, detail="缺少 audio_data 字段") + + audio_base64 = request['audio_data'] + audio_format = request.get('format', 'wav') + + logger.info(f"收到语音对话请求,用户: {user.id}, 格式: {audio_format}") + + # 解码音频 + import base64 + audio_data = base64.b64decode(audio_base64) + logger.info(f"音频数据大小: {len(audio_data)} 字节") + + # 2. ASR 识别 + logger.info("开始 ASR 识别...") + from dashscope.audio.asr import Transcription + from ..oss_utils import upload_audio_file, delete_audio_file + + # 上传到 OSS + file_url = upload_audio_file(audio_data, audio_format) + logger.info(f"音频已上传: {file_url}") + + try: + # 调用 ASR + task_response = Transcription.async_call( + model='paraformer-v2', + file_urls=[file_url], + parameters={ + 'format': audio_format, + 'sample_rate': 16000, + 'enable_words': False + } + ) + + if task_response.status_code != 200: + raise Exception(f"ASR 任务创建失败") + + task_id = task_response.output.task_id + logger.info(f"ASR 任务创建: {task_id}") + + # 等待识别结果 + import time + max_wait = 30 + start_time = time.time() + user_text = None + + while time.time() - start_time < max_wait: + result = Transcription.wait(task=task_id) + + if result.status_code == 200: + if result.output.task_status == "SUCCEEDED": + # 解析识别结果 + if hasattr(result.output, 'results') and result.output.results: + for item in result.output.results: + if isinstance(item, dict) and 'transcription_url' in item: + import requests + resp = requests.get(item['transcription_url'], timeout=10) + if resp.status_code == 200: + data = resp.json() + if 'transcripts' in data: + for transcript in data['transcripts']: + if 'text' in transcript: + user_text = transcript['text'].strip() + break + if user_text: + break + break + elif result.output.task_status == "FAILED": + error_code = getattr(result.output, 'code', 'Unknown') + logger.error(f"ASR 失败: {error_code}") + break + + time.sleep(2) + + if not user_text: + logger.warning("ASR 未识别到文本") + from ..response import success_response + return success_response({ + "user_text": "", + "ai_text": "抱歉,我没有听清楚,请再说一遍", + "audio_data": None + }) + + logger.info(f"ASR 识别结果: {user_text}") + + finally: + # 清理 OSS 文件 + try: + delete_audio_file(file_url) + except: + pass + + # 3. LLM 生成回复 + logger.info("开始 LLM 对话生成...") + + # 获取用户的恋人信息 + db = SessionLocal() + try: + lover = db.query(Lover).filter(Lover.user_id == user.id).first() + + # 构建系统提示 + system_prompt = f"你是用户 {user.nickname or '用户'} 的虚拟恋人,请用亲密、温暖、口语化的短句聊天。" + if lover and lover.personality_prompt: + system_prompt += f"\n人格设定:{lover.personality_prompt}" + + # 构建对话历史 + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_text} + ] + + # 调用 LLM + from ..llm import chat_completion + llm_result = chat_completion(messages) + ai_text = llm_result.content + + logger.info(f"LLM 回复: {ai_text}") + + finally: + db.close() + + # 4. TTS 合成语音 + logger.info("开始 TTS 语音合成...") + + # 清理文本(去除 Markdown 等) + clean_text = re.sub(r"\*\*(.*?)\*\*", r"\1", ai_text) + clean_text = re.sub(r"`([^`]*)`", r"\1", clean_text) + clean_text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", clean_text) + clean_text = re.sub(r"\*[^\*]{0,80}\*", "", clean_text) + clean_text = re.sub(r"[~~]+", "", clean_text) + clean_text = clean_text.replace("*", "") + clean_text = re.sub(r"\s+", " ", clean_text).strip() + + # 获取音色配置 + db = SessionLocal() + try: + voice_code = None + lover = db.query(Lover).filter(Lover.user_id == user.id).first() + + if lover and lover.voice_id: + voice = db.query(VoiceLibrary).filter(VoiceLibrary.id == lover.voice_id).first() + if voice and voice.voice_code: + voice_code = voice.voice_code + + if not voice_code: + # 使用默认音色 + gender = "female" if (user.gender or 0) == 1 else "male" + voice = db.query(VoiceLibrary).filter( + VoiceLibrary.gender == gender, + VoiceLibrary.is_default.is_(True) + ).first() + if voice and voice.voice_code: + voice_code = voice.voice_code + else: + voice_code = settings.VOICE_CALL_TTS_VOICE or "longxiaochun_v2" + + finally: + db.close() + + # 调用 TTS + model = settings.VOICE_CALL_TTS_MODEL or "cosyvoice-v2" + audio_format_enum = AudioFormat.MP3_22050HZ_MONO_256KBPS + + audio_bytes, _ = synthesize( + clean_text, + model=model, + voice=voice_code, + audio_format=audio_format_enum + ) + + logger.info(f"TTS 合成完成,音频大小: {len(audio_bytes)} 字节") + + # 5. 返回结果 + audio_base64_result = base64.b64encode(audio_bytes).decode('utf-8') + + from ..response import success_response + return success_response({ + "user_text": user_text, + "ai_text": ai_text, + "audio_data": audio_base64_result, + "audio_format": "mp3" + }) + + except HTTPException: + raise + except Exception as e: + logger.error(f"语音对话处理失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"语音对话处理失败: {str(e)}") diff --git a/test_voice_conversation.py b/test_voice_conversation.py new file mode 100644 index 0000000..78d9daf --- /dev/null +++ b/test_voice_conversation.py @@ -0,0 +1,130 @@ +""" +测试完整的语音对话流程 +ASR → LLM → TTS +""" +import os +import sys +import base64 +import logging + +# 添加 lover 目录到路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lover')) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def create_test_audio(): + """创建测试音频(2秒的正弦波)""" + import struct + import math + import wave + import tempfile + + sample_rate = 16000 + duration = 2 + frequency = 440 + + samples = [] + for i in range(sample_rate * duration): + value = int(32767 * 0.3 * math.sin(2 * math.pi * frequency * i / sample_rate)) + samples.append(struct.pack(' { + uni.showToast({ + title: `AI: ${aiText}`, + icon: 'none', + duration: 3000 + }) + }, 2000) + } + } return result } else { - throw new Error(`ASR请求失败: ${response.statusCode}`) + throw new Error(`对话请求失败: ${response.statusCode}`) } } catch (error) { - console.error('❌ ASR请求失败:', error) + console.error('❌ 对话请求失败:', error) // 隐藏加载提示 uni.hideLoading() uni.showToast({ - title: 'ASR识别失败', + title: '对话处理失败', icon: 'none', duration: 2000 }) throw error } }, + async playAIVoice(base64Audio, aiText) { + console.log('🔊 playAIVoice 被调用') + console.log('🔊 base64Audio 长度:', base64Audio ? base64Audio.length : 0) + console.log('🔊 aiText:', aiText) + + if (!base64Audio) { + console.error('❌ 没有音频数据') + return + } + + try { + // 显示 AI 回复文字 + if (aiText) { + uni.showToast({ + title: `AI: ${aiText.substring(0, 20)}...`, + icon: 'none', + duration: 3000 + }) + } + + // 解码 base64 为二进制数据 + console.log('📦 开始解码 base64...') + const binaryString = atob(base64Audio) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + console.log('✅ 音频数据解码完成,大小:', bytes.length, 'bytes') + + // #ifdef APP-PLUS + // APP 环境 - 简化实现 + console.log('📱 APP 环境,保存并播放音频') + + const fileName = `ai_voice_${Date.now()}.mp3` + const filePath = `_doc/${fileName}` + + // 使用 plus.io 保存文件 + plus.io.resolveLocalFileSystemURL('_doc/', (entry) => { + console.log('✅ 获取 _doc 目录成功') + + entry.getFile(fileName, {create: true}, (fileEntry) => { + console.log('✅ 创建文件成功:', fileName) + + fileEntry.createWriter((writer) => { + writer.onwrite = () => { + console.log('✅ 文件写入成功') + console.log('📁 文件路径:', fileEntry.fullPath) + + // 播放音频 + console.log('🎵 创建音频上下文...') + const audioContext = uni.createInnerAudioContext() + + // 使用完整路径 + const fullPath = fileEntry.fullPath + console.log('🎵 设置音频源:', fullPath) + audioContext.src = fullPath + audioContext.autoplay = true + + audioContext.onPlay(() => { + console.log('🔊 AI 语音开始播放') + }) + + audioContext.onEnded(() => { + console.log('✅ AI 语音播放完成') + // 清理 + audioContext.destroy() + fileEntry.remove(() => { + console.log('🗑️ 临时文件已清理') + }) + }) + + audioContext.onError((error) => { + console.error('❌ 播放失败:', error) + console.error('错误详情:', JSON.stringify(error)) + uni.showToast({ + title: '播放失败', + icon: 'none' + }) + audioContext.destroy() + }) + } + + writer.onerror = (error) => { + console.error('❌ 文件写入失败:', error) + uni.showToast({ + title: '文件保存失败', + icon: 'none' + }) + } + + // 写入数据 + console.log('📝 开始写入文件...') + const blob = new Blob([bytes.buffer], {type: 'audio/mp3'}) + writer.write(blob) + }, (error) => { + console.error('❌ 创建 writer 失败:', error) + }) + }, (error) => { + console.error('❌ 创建文件失败:', error) + console.error('错误详情:', JSON.stringify(error)) + }) + }, (error) => { + console.error('❌ 获取文件系统失败:', error) + console.error('错误详情:', JSON.stringify(error)) + }) + // #endif + + // #ifdef MP-WEIXIN + // 微信小程序 + console.log('📱 微信小程序环境') + const fs = uni.getFileSystemManager() + const tempFilePath = `${wx.env.USER_DATA_PATH}/ai_voice_${Date.now()}.mp3` + + fs.writeFileSync(tempFilePath, bytes.buffer, 'binary') + console.log('✅ 临时文件已保存:', tempFilePath) + + const innerAudioContext = uni.createInnerAudioContext() + innerAudioContext.src = tempFilePath + innerAudioContext.autoplay = true + + innerAudioContext.onPlay(() => { + console.log('🔊 AI 语音开始播放') + }) + + innerAudioContext.onEnded(() => { + console.log('✅ AI 语音播放完成') + try { + fs.unlinkSync(tempFilePath) + console.log('🗑️ 临时文件已清理') + } catch (e) { + console.warn('清理临时文件失败:', e) + } + }) + + innerAudioContext.onError((error) => { + console.error('❌ AI 语音播放失败:', error) + uni.showToast({ + title: '语音播放失败', + icon: 'none' + }) + }) + // #endif + + // #ifdef H5 + // H5 环境 + console.log('🌐 H5 环境,使用 Blob URL') + + const blob = new Blob([bytes.buffer], {type: 'audio/mp3'}) + const blobUrl = URL.createObjectURL(blob) + console.log('✅ Blob URL 创建成功:', blobUrl) + + const innerAudioContext = uni.createInnerAudioContext() + innerAudioContext.src = blobUrl + innerAudioContext.autoplay = true + + innerAudioContext.onPlay(() => { + console.log('🔊 AI 语音开始播放') + }) + + innerAudioContext.onEnded(() => { + console.log('✅ AI 语音播放完成') + URL.revokeObjectURL(blobUrl) + console.log('🗑️ Blob URL 已释放') + }) + + innerAudioContext.onError((error) => { + console.error('❌ AI 语音播放失败:', error) + uni.showToast({ + title: '语音播放失败', + icon: 'none' + }) + }) + // #endif + + } catch (error) { + console.error('❌ 播放 AI 语音失败:', error) + console.error('错误类型:', error.name) + console.error('错误消息:', error.message) + console.error('错误堆栈:', error.stack) + uni.showToast({ + title: '语音播放失败', + icon: 'none' + }) + } + }, goRecharge() { uni.showToast({ title: '充值功能开发中', diff --git a/完整启动指南.md b/完整启动指南.md new file mode 100644 index 0000000..73897ec --- /dev/null +++ b/完整启动指南.md @@ -0,0 +1,329 @@ +# AI 女友项目 - 完整启动指南 + +## 📋 项目架构 + +``` +AI 女友项目 +├── PHP 后端 (FastAdmin) - 端口 30100 +│ ├── 用户管理 +│ ├── 后台管理 +│ └── 基础 API +│ +├── Python 后端 (FastAPI) - 端口 30101 +│ ├── AI 对话 (LLM) +│ ├── 语音识别 (ASR) +│ ├── 语音合成 (TTS) +│ ├── 图像生成 +│ ├── 视频生成 +│ └── 其他 AI 功能 +│ +└── 前端 (uni-app) + ├── 小程序 + └── APP +``` + +## 🚀 启动步骤 + +### 方法 1:使用启动脚本(推荐) + +1. **双击运行** `启动项目.bat` + +2. **等待启动完成** + - 会自动打开两个命令行窗口 + - PHP 服务器窗口 + - Python 后端窗口 + +3. **验证启动成功** + - 浏览器会自动打开 + - PHP 后台:http://127.0.0.1:30100 + - Python API 文档:http://127.0.0.1:30101/docs + +### 方法 2:手动启动 + +#### 启动 PHP 服务器 + +```bash +cd xunifriend_RaeeC/public +php -S 0.0.0.0:30100 router.php +``` + +#### 启动 Python 后端 + +```bash +# 在项目根目录 +python -m uvicorn lover.main:app --host 0.0.0.0 --port 30101 --reload +``` + +## ✅ 验证服务状态 + +### 1. 检查端口占用 + +```bash +# Windows +netstat -ano | findstr :30100 +netstat -ano | findstr :30101 +``` + +应该看到: +``` +TCP 0.0.0.0:30100 0.0.0.0:0 LISTENING [PID] +TCP 0.0.0.0:30101 0.0.0.0:0 LISTENING [PID] +``` + +### 2. 访问 API 文档 + +打开浏览器访问: + +**Python API 文档**:http://127.0.0.1:30101/docs + +应该看到 FastAPI 的 Swagger 文档界面,包含: +- `/voice/call/asr` - ASR 语音识别 +- `/voice/call/conversation` - 完整语音对话 +- `/chat/send` - 文字聊天 +- 其他 AI 功能端点 + +**PHP 后台**:http://127.0.0.1:30100/admin + +### 3. 测试健康检查 + +```bash +# Python 后端健康检查 +curl http://127.0.0.1:30101/health +``` + +应该返回: +```json +{ + "code": 1, + "msg": "ok", + "data": { + "status": "ok" + } +} +``` + +## 🎯 测试语音对话功能 + +### 1. 在前端测试 + +1. 打开 uni-app 应用(小程序或 APP) +2. 进入语音通话页面 +3. 按住"按住说话"按钮 +4. 说话 2-3 秒:"你好,今天天气怎么样?" +5. 松开按钮 +6. 等待处理(约 5-10 秒) +7. 看到提示:"你说: 你好,今天天气怎么样?" +8. 听到 AI 的语音回复 + +### 2. 查看日志 + +**Python 后端窗口**应该显示: +``` +INFO - 收到语音对话请求,用户: XXX, 格式: wav +INFO - 音频数据大小: XXX 字节 +INFO - 开始 ASR 识别... +INFO - 音频已上传: https://... +INFO - ASR 识别结果: 你好,今天天气怎么样? +INFO - 开始 LLM 对话生成... +INFO - LLM 回复: 今天天气很好哦... +INFO - 开始 TTS 语音合成... +INFO - TTS 合成完成,音频大小: XXX 字节 +``` + +## 🔧 常见问题 + +### 问题 1:端口被占用 + +**症状**: +``` +[错误] 端口 30100 或 30101 已被占用 +``` + +**解决方案**: + +1. 使用启动脚本(会自动清理端口) +2. 或手动清理: + +```bash +# 查找占用端口的进程 +netstat -ano | findstr :30100 +netstat -ano | findstr :30101 + +# 终止进程(替换 [PID] 为实际的进程 ID) +taskkill /F /PID [PID] +``` + +### 问题 2:Python 依赖缺失 + +**症状**: +``` +ModuleNotFoundError: No module named 'xxx' +``` + +**解决方案**: + +```bash +cd lover +pip install -r requirements.txt +``` + +### 问题 3:PHP 路径错误 + +**症状**: +``` +[错误] PHP 未找到 +``` + +**解决方案**: + +编辑 `启动项目.bat`,修改 PHP 路径: +```bat +set PHP_PATH=D:\你的PHP路径\php.exe +``` + +### 问题 4:前端连接失败 + +**症状**: +前端显示"连接失败"或"网络错误" + +**解决方案**: + +1. 检查前端配置 `xuniYou/utils/request.js`: +```javascript +export const baseURL = 'http://192.168.1.141:30100' // PHP +export const baseURLPy = 'http://192.168.1.141:30101' // Python +``` + +2. 确保 IP 地址正确(局域网 IP 或 127.0.0.1) + +3. 如果使用真机测试,确保手机和电脑在同一局域网 + +## 📱 前端配置 + +### 本地开发(电脑测试) + +```javascript +// xuniYou/utils/request.js +export const baseURL = 'http://127.0.0.1:30100' +export const baseURLPy = 'http://127.0.0.1:30101' +``` + +### 局域网测试(手机测试) + +```javascript +// xuniYou/utils/request.js +export const baseURL = 'http://192.168.1.141:30100' // 替换为你的电脑 IP +export const baseURLPy = 'http://192.168.1.141:30101' +``` + +### 生产环境 + +```javascript +// xuniYou/utils/request.js +export const baseURL = 'http://你的域名:30100' +export const baseURLPy = 'http://你的域名:30101' +``` + +## 🔒 安全提示 + +### 开发环境 + +- ✅ 使用 `0.0.0.0` 监听所有网络接口 +- ✅ 允许局域网访问 +- ✅ 开启 CORS +- ✅ 开启调试日志 + +### 生产环境 + +- ⚠️ 使用 Nginx 反向代理 +- ⚠️ 配置 HTTPS +- ⚠️ 限制 CORS 来源 +- ⚠️ 关闭调试日志 +- ⚠️ 使用环境变量管理敏感信息 + +## 📊 服务监控 + +### 查看服务状态 + +```bash +# Windows +tasklist | findstr php.exe +tasklist | findstr python.exe + +# 查看端口 +netstat -ano | findstr :30100 +netstat -ano | findstr :30101 +``` + +### 查看日志 + +- **PHP 日志**:在 PHP 服务器窗口查看 +- **Python 日志**:在 Python 后端窗口查看 +- **前端日志**:在浏览器控制台或 HBuilderX 控制台查看 + +## 🛑 停止服务 + +### 使用启动脚本启动的 + +直接关闭对应的命令行窗口即可: +- 关闭"PHP 服务器"窗口 → 停止 PHP 服务 +- 关闭"Python 后端"窗口 → 停止 Python 服务 + +### 手动启动的 + +在对应的命令行窗口按 `Ctrl + C` + +### 强制停止 + +```bash +# 停止所有 PHP 进程 +taskkill /F /IM php.exe + +# 停止所有 Python 进程(谨慎使用) +taskkill /F /IM python.exe +``` + +## 📝 启动检查清单 + +启动前确认: + +- [ ] PHP 已安装并配置正确 +- [ ] Python 已安装并配置正确 +- [ ] Python 依赖已安装(`pip install -r lover/requirements.txt`) +- [ ] 环境变量已配置(`.env` 文件) +- [ ] 数据库已配置并可连接 +- [ ] OSS 已配置(如需使用语音功能) +- [ ] DashScope API Key 已配置(如需使用 AI 功能) + +启动后验证: + +- [ ] PHP 服务器正常运行(端口 30100) +- [ ] Python 后端正常运行(端口 30101) +- [ ] 可以访问 API 文档(http://127.0.0.1:30101/docs) +- [ ] 健康检查通过(http://127.0.0.1:30101/health) +- [ ] 前端可以连接后端 + +## 🎉 启动成功! + +如果以上步骤都完成,你应该看到: + +``` +╔════════════════════════════════════╗ +║ 启动成功! ║ +╚════════════════════════════════════╝ + +[PHP 服务器] ✓ 已启动 +→ http://127.0.0.1:30100 +→ http://127.0.0.1:30100/admin + +[Python 后端] ✓ 已启动 +→ http://127.0.0.1:30101 +→ http://127.0.0.1:30101/docs +``` + +现在可以开始使用 AI 女友的所有功能了!💕 + +--- + +**最后更新**: 2026-03-05 +**版本**: v2.0 diff --git a/快速参考.md b/快速参考.md new file mode 100644 index 0000000..16bec5b --- /dev/null +++ b/快速参考.md @@ -0,0 +1,186 @@ +# AI 女友项目 - 快速参考 + +## 🚀 启动命令 + +```bash +# 方法 1:使用启动脚本(推荐) +双击 "启动项目.bat" + +# 方法 2:手动启动 +# PHP 服务器 +cd xunifriend_RaeeC/public +php -S 0.0.0.0:30100 router.php + +# Python 后端 +python -m uvicorn lover.main:app --host 0.0.0.0 --port 30101 --reload +``` + +## 🌐 服务地址 + +| 服务 | 地址 | 说明 | +|------|------|------| +| PHP 后台 | http://127.0.0.1:30100/admin | 用户管理、后台管理 | +| Python API | http://127.0.0.1:30101/docs | AI 功能、API 文档 | +| 健康检查 | http://127.0.0.1:30101/health | 服务状态检查 | + +## 📡 API 端点 + +### 语音对话 + +| 端点 | 方法 | 功能 | 响应时间 | +|------|------|------|---------| +| `/voice/call/asr` | POST | 仅 ASR 识别 | 3-5秒 | +| `/voice/call/conversation` | POST | 完整对话(ASR+LLM+TTS) | 7-15秒 | + +### 请求格式 + +```json +{ + "audio_data": "base64编码的音频", + "format": "wav" +} +``` + +### 响应格式 + +**ASR 模式**: +```json +{ + "code": 1, + "msg": "ok", + "data": { + "text": "识别的文字" + } +} +``` + +**对话模式**: +```json +{ + "code": 1, + "msg": "ok", + "data": { + "user_text": "用户说的话", + "ai_text": "AI的回复", + "audio_data": "base64编码的AI语音", + "audio_format": "mp3" + } +} +``` + +## 🔧 前端配置 + +```javascript +// xuniYou/utils/request.js + +// 本地开发 +export const baseURL = 'http://127.0.0.1:30100' +export const baseURLPy = 'http://127.0.0.1:30101' + +// 局域网测试(替换为你的电脑 IP) +export const baseURL = 'http://192.168.1.141:30100' +export const baseURLPy = 'http://192.168.1.141:30101' +``` + +## 🔍 调试命令 + +```bash +# 检查端口占用 +netstat -ano | findstr :30100 +netstat -ano | findstr :30101 + +# 测试健康检查 +curl http://127.0.0.1:30101/health + +# 查看进程 +tasklist | findstr php.exe +tasklist | findstr python.exe + +# 停止进程 +taskkill /F /IM php.exe +taskkill /F /IM python.exe +``` + +## 📋 环境变量 + +```bash +# .env 文件 + +# DashScope API Key(必需) +DASHSCOPE_API_KEY=sk-xxx + +# OSS 配置(语音功能必需) +ALIYUN_OSS_ACCESS_KEY_ID=xxx +ALIYUN_OSS_ACCESS_KEY_SECRET=xxx +ALIYUN_OSS_BUCKET_NAME=xxx +ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com +ALIYUN_OSS_CDN_DOMAIN=https://xxx.oss-cn-hangzhou.aliyuncs.com + +# 数据库 +DATABASE_URL=mysql+pymysql://root:password@localhost:3306/fastadmin + +# LLM 配置 +LLM_MODEL=qwen-plus +LLM_TEMPERATURE=0.8 +LLM_MAX_TOKENS=2000 + +# TTS 配置 +VOICE_CALL_TTS_MODEL=cosyvoice-v2 +VOICE_CALL_TTS_VOICE=longxiaochun_v2 + +# ASR 配置 +VOICE_CALL_ASR_MODEL=paraformer-v2 +VOICE_CALL_ASR_SAMPLE_RATE=16000 +``` + +## 🐛 常见错误 + +| 错误 | 原因 | 解决方案 | +|------|------|---------| +| 端口被占用 | 服务已在运行 | 使用启动脚本自动清理 | +| ModuleNotFoundError | Python 依赖缺失 | `pip install -r lover/requirements.txt` | +| 连接失败 | 前端配置错误 | 检查 `request.js` 中的 IP 和端口 | +| ASR 识别失败 | 音频格式错误 | 使用 WAV 格式,16kHz,单声道 | +| OSS 上传失败 | OSS 配置错误 | 检查 `.env` 中的 OSS 配置 | + +## 📊 性能指标 + +| 步骤 | 时间 | +|------|------| +| 录音 | 2-5秒 | +| ASR 识别 | 2-5秒 | +| LLM 生成 | 1-3秒 | +| TTS 合成 | 1-2秒 | +| **总计** | **7-15秒** | + +## 🎯 测试流程 + +1. 双击 `启动项目.bat` +2. 等待两个窗口启动完成 +3. 访问 http://127.0.0.1:30101/docs 验证 +4. 打开前端应用 +5. 进入语音通话页面 +6. 按住说话按钮 +7. 说话 2-3 秒 +8. 松开按钮 +9. 等待 AI 回复 +10. 听到 AI 语音 + +## 📞 技术支持 + +遇到问题时提供: +1. 前端控制台日志 +2. Python 后端窗口日志 +3. PHP 服务器窗口日志 +4. 环境配置信息 +5. 错误截图 + +--- + +**快速链接**: +- [完整启动指南](完整启动指南.md) +- [语音对话功能说明](语音对话功能说明.md) +- [语音对话快速开始](语音对话快速开始.md) +- [ASR问题修复总结](ASR问题修复总结.md) + +**最后更新**: 2026-03-05 diff --git a/语音对话功能说明.md b/语音对话功能说明.md new file mode 100644 index 0000000..3107854 --- /dev/null +++ b/语音对话功能说明.md @@ -0,0 +1,383 @@ +# 语音对话功能说明 + +## 🎯 功能概述 + +完整的语音对话流程已实现,包括: +1. **ASR(语音识别)** - 用户语音 → 文字 +2. **LLM(对话生成)** - 文字 → AI 回复文字 +3. **TTS(语音合成)** - AI 回复文字 → 语音 +4. **前端播放** - 播放 AI 的语音回复 + +## 📋 API 端点 + +### 1. 仅 ASR 识别(已有) +``` +POST /voice/call/asr +``` + +**请求**: +```json +{ + "audio_data": "base64编码的音频数据", + "format": "wav" +} +``` + +**响应**: +```json +{ + "code": 1, + "msg": "ok", + "data": { + "text": "识别的文字" + } +} +``` + +### 2. 完整语音对话(新增) +``` +POST /voice/call/conversation +``` + +**请求**: +```json +{ + "audio_data": "base64编码的音频数据", + "format": "wav" +} +``` + +**响应**: +```json +{ + "code": 1, + "msg": "ok", + "data": { + "user_text": "用户说的话", + "ai_text": "AI的回复文字", + "audio_data": "base64编码的AI语音", + "audio_format": "mp3" + } +} +``` + +## 🔄 完整流程 + +### 后端处理流程 + +``` +用户录音 + ↓ +1. 接收 base64 音频数据 + ↓ +2. 解码为二进制 + ↓ +3. 上传到 OSS + ↓ +4. 调用 DashScope ASR + ↓ +5. 等待识别结果 + ↓ +6. 获取用户文字 + ↓ +7. 构建对话上下文(包含恋人人格) + ↓ +8. 调用 LLM 生成回复 + ↓ +9. 清理回复文本(去除 Markdown) + ↓ +10. 获取用户的音色配置 + ↓ +11. 调用 TTS 合成语音 + ↓ +12. 将语音编码为 base64 + ↓ +13. 返回完整结果 +``` + +### 前端处理流程 + +``` +用户按住说话按钮 + ↓ +1. 开始录音 + ↓ +2. 松开按钮,停止录音 + ↓ +3. 读取录音文件 + ↓ +4. 转换为 base64 + ↓ +5. 调用 /voice/call/conversation + ↓ +6. 显示"对话处理中..." + ↓ +7. 接收响应 + ↓ +8. 显示用户说的话 + ↓ +9. 解码 AI 语音数据 + ↓ +10. 保存为临时文件 + ↓ +11. 显示 AI 回复文字 + ↓ +12. 播放 AI 语音 + ↓ +13. 播放完成后清理临时文件 +``` + +## 🎨 前端代码示例 + +### 发送语音并接收回复 + +```javascript +async sendAudioToASR(audioBytes) { + // 1. 转换为 base64 + let base64Audio = '' + for (let i = 0; i < audioBytes.length; i++) { + base64Audio += String.fromCharCode(audioBytes[i]) + } + base64Audio = btoa(base64Audio) + + // 2. 调用 API + const response = await uni.request({ + url: this.baseURLPy + '/voice/call/conversation', + method: 'POST', + header: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + uni.getStorageSync("token") + }, + data: { + audio_data: base64Audio, + format: 'wav' + } + }) + + // 3. 处理响应 + const data = response.data.data + const userText = data.user_text + const aiText = data.ai_text + const audioData = data.audio_data + + // 4. 显示识别结果 + uni.showToast({ + title: `你说: ${userText}`, + icon: 'none' + }) + + // 5. 播放 AI 语音 + await this.playAIVoice(audioData, aiText) +} +``` + +### 播放 AI 语音 + +```javascript +async playAIVoice(base64Audio, aiText) { + // 1. 解码 base64 + const binaryString = atob(base64Audio) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + // 2. 保存为临时文件 + const fs = uni.getFileSystemManager() + const tempFilePath = `${wx.env.USER_DATA_PATH}/ai_voice_${Date.now()}.mp3` + fs.writeFileSync(tempFilePath, bytes.buffer, 'binary') + + // 3. 显示 AI 回复文字 + uni.showToast({ + title: `AI: ${aiText}`, + icon: 'none', + duration: 3000 + }) + + // 4. 播放音频 + const innerAudioContext = uni.createInnerAudioContext() + innerAudioContext.src = tempFilePath + innerAudioContext.autoplay = true + + innerAudioContext.onEnded(() => { + // 清理临时文件 + fs.unlinkSync(tempFilePath) + }) +} +``` + +## 🔧 配置要求 + +### 环境变量 + +```bash +# DashScope API Key(ASR + LLM + TTS) +DASHSCOPE_API_KEY=sk-xxx + +# OSS 配置(存储音频文件) +ALIYUN_OSS_ACCESS_KEY_ID=xxx +ALIYUN_OSS_ACCESS_KEY_SECRET=xxx +ALIYUN_OSS_BUCKET_NAME=xxx +ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com +ALIYUN_OSS_CDN_DOMAIN=https://xxx.oss-cn-hangzhou.aliyuncs.com + +# LLM 配置 +LLM_MODEL=qwen-plus +LLM_TEMPERATURE=0.8 +LLM_MAX_TOKENS=2000 + +# TTS 配置 +VOICE_CALL_TTS_MODEL=cosyvoice-v2 +VOICE_CALL_TTS_VOICE=longxiaochun_v2 +VOICE_CALL_TTS_FORMAT=mp3 + +# ASR 配置 +VOICE_CALL_ASR_MODEL=paraformer-v2 +VOICE_CALL_ASR_SAMPLE_RATE=16000 +``` + +### 数据库表 + +需要以下表: +- `fa_lover` - 恋人信息(personality_prompt, voice_id) +- `fa_voice_library` - 音色库(voice_code, gender, is_default) +- `fa_user` - 用户信息(nickname, gender) + +## 📊 性能指标 + +### 预期处理时间 + +| 步骤 | 时间 | 说明 | +|------|------|------| +| 录音 | 2-5秒 | 用户说话时间 | +| 上传 OSS | 0.5-1秒 | 取决于网络 | +| ASR 识别 | 2-5秒 | DashScope 处理 | +| LLM 生成 | 1-3秒 | 取决于回复长度 | +| TTS 合成 | 1-2秒 | 取决于文本长度 | +| 下载播放 | 0.5-1秒 | 取决于网络 | +| **总计** | **7-17秒** | 完整对话周期 | + +### 优化建议 + +1. **流式处理** - LLM 和 TTS 可以流式返回,边生成边播放 +2. **缓存** - 常见回复可以预先合成并缓存 +3. **并行处理** - 某些步骤可以并行执行 +4. **CDN 加速** - 使用 CDN 加速音频传输 + +## 🎯 用户体验流程 + +### 理想的对话体验 + +``` +用户: [按住按钮] "今天天气怎么样?" [松开] + ↓ +界面: "对话处理中..." + ↓ (3-5秒) +界面: "你说: 今天天气怎么样?" + ↓ (1秒) +界面: "AI: 今天天气很好哦,阳光明媚,适合出去走走~" + ↓ +[播放 AI 语音] + ↓ +用户: [听完后继续对话] +``` + +## 🐛 常见问题 + +### 1. 识别结果为空 + +**原因**: +- 录音时间太短 +- 环境噪音太大 +- 没有说话内容 + +**解决**: +- 确保录音时长 > 1 秒 +- 在安静环境测试 +- 靠近麦克风说话 + +### 2. AI 回复太慢 + +**原因**: +- 网络延迟 +- LLM 生成时间长 +- TTS 合成时间长 + +**解决**: +- 优化网络连接 +- 使用更快的模型(qwen-flash) +- 限制回复长度 + +### 3. 语音播放失败 + +**原因**: +- 音频格式不支持 +- 临时文件写入失败 +- 权限问题 + +**解决**: +- 使用标准 MP3 格式 +- 检查文件系统权限 +- 添加错误处理 + +## 🚀 下一步优化 + +### 短期优化(1-2周) + +1. **流式 TTS** - 边生成边播放,减少等待时间 +2. **对话历史** - 保存最近几轮对话,提供上下文 +3. **打断功能** - 用户可以打断 AI 的回复 +4. **情感识别** - 根据用户语气调整 AI 回复 + +### 中期优化(1-2月) + +1. **多轮对话** - 完整的对话管理系统 +2. **个性化** - 根据用户习惯调整回复风格 +3. **语音克隆** - 支持用户自定义音色 +4. **实时对话** - WebSocket 实时双向通信 + +### 长期优化(3-6月) + +1. **多模态** - 结合图像、视频的对话 +2. **情感分析** - 深度理解用户情绪 +3. **主动对话** - AI 主动发起话题 +4. **场景化** - 不同场景的专属对话模式 + +## 📝 测试清单 + +### 功能测试 + +- [ ] 录音功能正常 +- [ ] ASR 识别准确 +- [ ] LLM 回复合理 +- [ ] TTS 语音自然 +- [ ] 播放功能正常 +- [ ] 错误处理完善 + +### 性能测试 + +- [ ] 响应时间 < 15秒 +- [ ] 音频质量良好 +- [ ] 内存占用合理 +- [ ] 网络流量可控 + +### 兼容性测试 + +- [ ] iOS 设备 +- [ ] Android 设备 +- [ ] 不同网络环境 +- [ ] 不同音色 + +## 📞 技术支持 + +如遇问题,请提供: +1. 完整的前端控制台日志 +2. 后端服务器日志 +3. 录音文件信息 +4. 网络环境 +5. 设备信息 + +--- + +**最后更新**: 2026-03-05 +**版本**: v2.0 - 完整语音对话 diff --git a/语音对话快速开始.md b/语音对话快速开始.md new file mode 100644 index 0000000..fd4b692 --- /dev/null +++ b/语音对话快速开始.md @@ -0,0 +1,285 @@ +# 语音对话功能 - 快速开始 + +## ✅ 已完成的功能 + +1. ✅ **ASR 语音识别** - 用户语音转文字 +2. ✅ **LLM 对话生成** - AI 智能回复 +3. ✅ **TTS 语音合成** - AI 回复转语音 +4. ✅ **前端播放** - 播放 AI 语音 + +## 🚀 快速测试 + +### 1. 启动后端服务 + +**双击运行**:`启动项目.bat` + +这会自动启动: +- PHP 服务器:http://127.0.0.1:30100 +- Python 后端:http://127.0.0.1:30101 + +应该看到两个窗口打开,显示: +``` +[PHP 服务器] 正在启动... +[PHP 服务器] 端口: 30100 + +[Python 后端] 正在启动... +[Python 后端] 端口: 30101 +INFO: Uvicorn running on http://0.0.0.0:30101 +``` + +### 2. 测试完整对话流程 + +在前端应用中: + +1. 打开语音通话页面 +2. 按住"按住说话"按钮 +3. 说话 2-3 秒:"你好,今天天气怎么样?" +4. 松开按钮 +5. 等待处理(约 5-10 秒) +6. 看到提示:"你说: 你好,今天天气怎么样?" +7. 听到 AI 的语音回复 + +### 3. 查看日志 + +**前端控制台**: +``` +📤 开始发送音频进行语音对话 +📊 音频数据大小: XXX bytes +📤 发送语音对话请求... +✅ 对话响应: ... +✅ 识别结果: 你好,今天天气怎么样? +✅ AI回复: 今天天气很好哦... +🔊 开始播放 AI 语音回复... +``` + +**后端日志**: +``` +INFO - 收到语音对话请求,用户: XXX, 格式: wav +INFO - 音频数据大小: XXX 字节 +INFO - 开始 ASR 识别... +INFO - 音频已上传: https://... +INFO - ASR 任务创建: xxx +INFO - ASR 识别结果: 你好,今天天气怎么样? +INFO - 开始 LLM 对话生成... +INFO - LLM 回复: 今天天气很好哦... +INFO - 开始 TTS 语音合成... +INFO - TTS 合成完成,音频大小: XXX 字节 +``` + +### 2. 验证服务运行 + +打开浏览器访问: +- Python API 文档:http://127.0.0.1:30101/docs +- PHP 后台:http://127.0.0.1:30100/admin + +如果能正常访问,说明服务启动成功。 + +## 📋 对比:两种模式 + +### 模式 1: 仅 ASR 识别 + +**端点**: `/voice/call/asr` + +**流程**: +``` +用户录音 → ASR → 返回文字 +``` + +**用途**: +- 快速测试 ASR 功能 +- 仅需要语音转文字 +- 调试识别准确度 + +**响应时间**: 3-5 秒 + +### 模式 2: 完整语音对话(推荐) + +**端点**: `/voice/call/conversation` + +**流程**: +``` +用户录音 → ASR → LLM → TTS → 返回文字+语音 +``` + +**用途**: +- 完整的语音对话体验 +- AI 智能回复 +- 自然的语音交互 + +**响应时间**: 7-15 秒 + +## 🎯 使用建议 + +### 什么时候用 ASR 模式? + +- ✅ 测试语音识别准确度 +- ✅ 调试录音功能 +- ✅ 快速验证音频质量 +- ✅ 不需要 AI 回复 + +### 什么时候用对话模式? + +- ✅ 正式的语音通话功能 +- ✅ 需要 AI 智能回复 +- ✅ 完整的用户体验 +- ✅ 生产环境使用 + +## 🔧 切换模式 + +### 前端代码修改 + +**使用 ASR 模式**: +```javascript +const response = await uni.request({ + url: this.baseURLPy + '/voice/call/asr', + method: 'POST', + data: { + audio_data: base64Audio, + format: 'wav' + } +}) + +// 只返回识别文字 +const text = response.data.data.text +``` + +**使用对话模式**(当前默认): +```javascript +const response = await uni.request({ + url: this.baseURLPy + '/voice/call/conversation', + method: 'POST', + data: { + audio_data: base64Audio, + format: 'wav' + } +}) + +// 返回识别文字 + AI回复 + 语音 +const userText = response.data.data.user_text +const aiText = response.data.data.ai_text +const audioData = response.data.data.audio_data +``` + +## 📊 功能对比表 + +| 功能 | ASR 模式 | 对话模式 | +|------|---------|---------| +| 语音识别 | ✅ | ✅ | +| AI 回复 | ❌ | ✅ | +| 语音合成 | ❌ | ✅ | +| 响应时间 | 3-5秒 | 7-15秒 | +| 网络流量 | 小 | 中 | +| 用户体验 | 基础 | 完整 | +| 适用场景 | 测试 | 生产 | + +## 🎨 用户体验对比 + +### ASR 模式体验 + +``` +用户: [按住] "你好" [松开] + ↓ (3秒) +界面: "识别: 你好" + ↓ +[结束,等待用户下一次操作] +``` + +### 对话模式体验(推荐) + +``` +用户: [按住] "你好" [松开] + ↓ (3秒) +界面: "你说: 你好" + ↓ (2秒) +界面: "AI: 你好呀,很高兴见到你~" + ↓ +[播放 AI 语音] + ↓ +[自然的对话体验] +``` + +## 🚀 下一步 + +### 立即可以做的 + +1. ✅ 测试完整对话流程 +2. ✅ 调整 AI 回复风格(修改 personality_prompt) +3. ✅ 更换音色(修改 voice_id) +4. ✅ 优化录音时长 + +### 需要开发的 + +1. ⏳ 对话历史记录 +2. ⏳ 多轮对话上下文 +3. ⏳ 流式 TTS(边生成边播放) +4. ⏳ 打断功能 + +## 💡 优化建议 + +### 提升响应速度 + +1. **使用更快的模型** + ```bash + LLM_MODEL=qwen-flash # 更快但质量略低 + ``` + +2. **限制回复长度** + ```bash + LLM_MAX_TOKENS=500 # 减少生成时间 + ``` + +3. **优化网络** + - 使用 CDN 加速 + - 选择就近的 OSS 区域 + +### 提升对话质量 + +1. **完善人格设定** + - 在数据库中设置详细的 personality_prompt + - 包含性格、说话风格、兴趣爱好等 + +2. **添加对话历史** + - 保存最近 5-10 轮对话 + - 提供更连贯的上下文 + +3. **情感调节** + - 根据用户语气调整回复 + - 使用不同的 TTS 参数 + +## 📝 测试检查清单 + +### 基础功能 +- [ ] 录音功能正常 +- [ ] ASR 识别准确(准确率 > 90%) +- [ ] LLM 回复合理 +- [ ] TTS 语音自然 +- [ ] 播放功能正常 + +### 用户体验 +- [ ] 响应时间可接受(< 15秒) +- [ ] 提示信息清晰 +- [ ] 错误处理友好 +- [ ] 音质清晰 + +### 边界情况 +- [ ] 录音时间太短 +- [ ] 录音时间太长 +- [ ] 网络中断 +- [ ] 识别失败 +- [ ] 合成失败 + +## 🎉 恭喜! + +你已经完成了完整的语音对话功能!现在可以: + +1. ✅ 用户说话,AI 听懂 +2. ✅ AI 智能回复 +3. ✅ AI 用语音回答 +4. ✅ 自然的对话体验 + +享受与 AI 恋人的语音对话吧!💕 + +--- + +**最后更新**: 2026-03-05 +**版本**: v2.0 diff --git a/语音播放问题排查.md b/语音播放问题排查.md new file mode 100644 index 0000000..fce9cd3 --- /dev/null +++ b/语音播放问题排查.md @@ -0,0 +1,286 @@ +# 语音播放问题排查指南 + +## 🔍 问题现象 + +后端成功生成了 AI 语音(120463 字节),但前端没有播放出来。 + +## 📋 排查步骤 + +### 1. 检查前端是否收到音频数据 + +重新编译并运行,查看前端控制台日志: + +``` +✅ 对话响应: ... +✅ 识别结果: ghost~来啦? +✅ AI回复: ghost~来啦?刚在有《夏日友人帐》... +✅ 音频数据: XXXXX 字符 <-- 应该看到这行 +🔊 开始播放 AI 语音回复... +🔊 音频数据前100字符: ... +``` + +**如果没有看到"音频数据"**: +- 问题在后端返回数据格式 +- 检查后端响应结构 + +**如果看到了"音频数据"**: +- 继续下一步 + +### 2. 检查 playAIVoice 是否被调用 + +查看日志: + +``` +🔊 playAIVoice 被调用 +🔊 base64Audio 长度: XXXXX +🔊 aiText: ... +📦 开始解码 base64... +✅ 音频数据解码完成,大小: XXXXX bytes +📱 APP 环境,保存并播放音频 +``` + +**如果没有看到这些日志**: +- `playAIVoice` 没有被调用 +- 检查 `audioData` 是否为空 + +**如果看到了这些日志**: +- 继续下一步 + +### 3. 检查文件保存是否成功 + +查看日志: + +``` +✅ 获取 _doc 目录成功 +✅ 创建文件成功: ai_voice_xxx.mp3 +✅ 文件写入成功 +📁 文件路径: /storage/emulated/0/Android/data/... +``` + +**如果看到错误**: +- 文件系统权限问题 +- 检查 APP 存储权限 + +**如果文件保存成功**: +- 继续下一步 + +### 4. 检查音频播放 + +查看日志: + +``` +🎵 创建音频上下文... +🎵 设置音频源: /storage/emulated/0/Android/data/.../ai_voice_xxx.mp3 +🔊 AI 语音开始播放 <-- 应该看到这行 +``` + +**如果看到播放错误**: +- 音频格式问题 +- 音频文件损坏 +- 播放器不支持 + +## 🔧 常见问题和解决方案 + +### 问题 1:没有收到音频数据 + +**症状**: +``` +✅ AI回复: ... +⚠️ 没有收到音频数据 +``` + +**原因**: +- 后端返回数据格式不正确 +- `audio_data` 字段缺失 + +**解决方案**: + +检查后端响应: +```bash +# 使用 curl 测试 +curl -X POST http://127.0.0.1:30101/voice/call/conversation \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"audio_data":"...","format":"wav"}' \ + | jq '.data.audio_data' | head -c 100 +``` + +应该看到 base64 字符串。 + +### 问题 2:base64 解码失败 + +**症状**: +``` +❌ 播放 AI 语音失败 +错误类型: InvalidCharacterError +``` + +**原因**: +- base64 字符串包含非法字符 +- 字符串被截断 + +**解决方案**: + +检查 base64 字符串: +```javascript +console.log('base64 前10字符:', audioData.substring(0, 10)) +console.log('base64 后10字符:', audioData.substring(audioData.length - 10)) +console.log('是否包含非法字符:', /[^A-Za-z0-9+/=]/.test(audioData)) +``` + +### 问题 3:文件保存失败 + +**症状**: +``` +❌ 获取文件系统失败 +❌ 创建文件失败 +``` + +**原因**: +- APP 没有存储权限 +- 文件路径不存在 + +**解决方案**: + +1. 检查 APP 权限: +```javascript +// 在 manifest.json 中添加 +"permissions": { + "android.permission.WRITE_EXTERNAL_STORAGE": {}, + "android.permission.READ_EXTERNAL_STORAGE": {} +} +``` + +2. 使用其他目录: +```javascript +// 尝试使用 _downloads 目录 +plus.io.resolveLocalFileSystemURL('_downloads/', ...) +``` + +### 问题 4:音频播放失败 + +**症状**: +``` +✅ 文件写入成功 +🎵 设置音频源: ... +❌ 播放失败: {errMsg: "..."} +``` + +**原因**: +- 音频格式不支持 +- 文件路径错误 +- 音频文件损坏 + +**解决方案**: + +1. **验证音频文件**: +```javascript +// 检查文件是否存在 +plus.io.resolveLocalFileSystemURL(filePath, (entry) => { + entry.file((file) => { + console.log('文件大小:', file.size) + console.log('文件类型:', file.type) + }) +}) +``` + +2. **使用绝对路径**: +```javascript +// 使用 fileEntry.toLocalURL() +const audioUrl = fileEntry.toLocalURL() +audioContext.src = audioUrl +``` + +3. **测试音频文件**: +```javascript +// 保存到相册,手动播放测试 +plus.gallery.save(filePath, () => { + console.log('已保存到相册,请手动播放测试') +}) +``` + +## 🎯 调试技巧 + +### 1. 保存音频文件到相册 + +```javascript +// 在 playAIVoice 中添加 +writer.onwrite = () => { + console.log('✅ 文件写入成功') + + // 保存到相册以便测试 + plus.gallery.save(fileEntry.fullPath, () => { + console.log('✅ 已保存到相册') + uni.showToast({ + title: '音频已保存到相册', + icon: 'none' + }) + }, (error) => { + console.error('保存到相册失败:', error) + }) + + // 继续播放... +} +``` + +### 2. 使用系统播放器测试 + +```javascript +// 使用系统播放器打开 +plus.runtime.openFile(fileEntry.fullPath, {}, (error) => { + console.error('打开文件失败:', error) +}) +``` + +### 3. 对比文件大小 + +```javascript +console.log('原始 base64 长度:', base64Audio.length) +console.log('解码后字节数:', bytes.length) +console.log('预期字节数:', Math.ceil(base64Audio.length * 3 / 4)) +console.log('后端返回大小:', 120463) // 从后端日志获取 +``` + +应该接近 120463 字节。 + +## 📝 完整的调试日志示例 + +**成功的日志**: +``` +📤 发送语音对话请求... +✅ 对话响应: {statusCode: 200, data: {...}} +✅ 识别结果: ghost~来啦? +✅ AI回复: ghost~来啦?刚在有《夏日友人帐》... +✅ 音频数据: 160616 字符 +🔊 开始播放 AI 语音回复... +🔊 音频数据前100字符: SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYxLjEuMTAwAAAAAAAAAAAAAAD/+5DEAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAA... +🔊 playAIVoice 被调用 +🔊 base64Audio 长度: 160616 +🔊 aiText: ghost~来啦?刚在有《夏日友人帐》... +📦 开始解码 base64... +✅ 音频数据解码完成,大小: 120463 bytes +📱 APP 环境,保存并播放音频 +✅ 获取 _doc 目录成功 +✅ 创建文件成功: ai_voice_1709636448745.mp3 +📝 开始写入文件... +✅ 文件写入成功 +📁 文件路径: /storage/emulated/0/Android/data/io.dcloud.HBuilder/apps/HBuilder/doc/ai_voice_1709636448745.mp3 +🎵 创建音频上下文... +🎵 设置音频源: /storage/emulated/0/Android/data/io.dcloud.HBuilder/apps/HBuilder/doc/ai_voice_1709636448745.mp3 +🔊 AI 语音开始播放 +✅ AI 语音播放完成 +🗑️ 临时文件已清理 +``` + +## 🚀 下一步 + +如果以上步骤都无法解决问题,请提供: + +1. 完整的前端控制台日志 +2. 后端日志(TTS 合成部分) +3. 设备信息(Android 版本、APP 版本) +4. 是否有其他音频播放功能正常工作 + +--- + +**最后更新**: 2026-03-05