This commit is contained in:
Lilixu007 2026-03-05 17:18:04 +08:00
parent 503ae8a364
commit 28677fe08c
8 changed files with 2034 additions and 15 deletions

View File

@ -1460,3 +1460,208 @@ async def voice_call(websocket: WebSocket):
pass pass
finally: finally:
await session.close() 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)}")

130
test_voice_conversation.py Normal file
View File

@ -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('<h', value))
pcm_data = b''.join(samples)
# 转换为 WAV
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_file:
with wave.open(temp_file.name, 'wb') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(16000)
wav_file.writeframes(pcm_data)
temp_file_path = temp_file.name
with open(temp_file_path, 'rb') as f:
wav_data = f.read()
os.unlink(temp_file_path)
return wav_data
def test_conversation():
"""测试语音对话端点"""
print("=" * 60)
print("测试完整语音对话流程")
print("=" * 60)
# 创建测试音频
print("\n🎵 创建测试音频...")
audio_data = create_test_audio()
print(f" 音频大小: {len(audio_data)} 字节")
# 转换为 base64
audio_base64 = base64.b64encode(audio_data).decode('utf-8')
print(f" Base64 长度: {len(audio_base64)}")
# 调用 API
print("\n📤 调用语音对话 API...")
import requests
# 注意:需要有效的 token
token = "test_token" # 替换为实际的 token
try:
response = requests.post(
'http://127.0.0.1:30101/voice/call/conversation',
json={
'audio_data': audio_base64,
'format': 'wav'
},
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
timeout=60
)
print(f"\n📋 响应状态: {response.status_code}")
if response.status_code == 200:
result = response.json()
print(f"\n✅ 对话成功!")
print(f" 用户说: {result.get('data', {}).get('user_text', 'N/A')}")
print(f" AI回复: {result.get('data', {}).get('ai_text', 'N/A')}")
audio_data_result = result.get('data', {}).get('audio_data')
if audio_data_result:
audio_bytes = base64.b64decode(audio_data_result)
print(f" AI语音大小: {len(audio_bytes)} 字节")
# 保存 AI 语音到文件
output_file = 'test_ai_voice.mp3'
with open(output_file, 'wb') as f:
f.write(audio_bytes)
print(f" AI语音已保存: {output_file}")
print("\n" + "=" * 60)
print("🎉 语音对话测试完成!")
print("=" * 60)
return True
else:
print(f"\n❌ 请求失败: {response.status_code}")
print(f" 响应: {response.text}")
return False
except Exception as e:
print(f"\n❌ 测试失败: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
print("\n⚠️ 注意:此测试需要:")
print(" 1. 后端服务运行在 http://127.0.0.1:30101")
print(" 2. 有效的用户 token")
print(" 3. 配置了 DASHSCOPE_API_KEY")
print(" 4. 配置了 OSS")
print("\n由于测试音频是正弦波ASR 可能无法识别")
print("但可以测试 LLM 和 TTS 部分\n")
success = test_conversation()
sys.exit(0 if success else 1)

View File

@ -1272,12 +1272,12 @@
}, },
// ASR // ASR
async sendAudioToASR(audioBytes) { async sendAudioToASR(audioBytes) {
console.log('📤 开始发送音频到ASR端点') console.log('📤 开始发送音频进行语音对话')
console.log('📊 音频数据大小:', audioBytes.length, 'bytes') console.log('📊 音频数据大小:', audioBytes.length, 'bytes')
// //
uni.showLoading({ uni.showLoading({
title: '语音识别中...', title: '对话处理中...',
mask: true mask: true
}) })
@ -1289,9 +1289,9 @@
} }
base64Audio = btoa(base64Audio) base64Audio = btoa(base64Audio)
console.log('📤 发送ASR请求...') console.log('📤 发送语音对话请求...')
const response = await uni.request({ const response = await uni.request({
url: this.baseURLPy + '/voice/call/asr', url: this.baseURLPy + '/voice/call/conversation',
method: 'POST', method: 'POST',
header: { header: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -1299,44 +1299,259 @@
}, },
data: { data: {
audio_data: base64Audio, audio_data: base64Audio,
format: 'wav' // WAV format: 'wav'
} }
}) })
console.log('✅ ASR响应:', response) console.log('✅ 对话响应:', response)
// //
uni.hideLoading() uni.hideLoading()
if (response.statusCode === 200 && response.data) { if (response.statusCode === 200 && response.data) {
const result = response.data const result = response.data
console.log('✅ ASR识别结果:', result.text) // : {code: 1, msg: "ok", data: {user_text, ai_text, audio_data}}
const data = result.data || result
const userText = data.user_text
const aiText = data.ai_text
const audioData = data.audio_data
console.log('✅ 识别结果:', userText)
console.log('✅ AI回复:', aiText)
console.log('✅ 音频数据:', audioData ? `${audioData.length} 字符` : '无')
// //
if (userText) {
uni.showToast({ uni.showToast({
title: `识别: ${result.text}`, title: `你说: ${userText}`,
icon: 'none',
duration: 2000
})
}
// AI
if (audioData) {
console.log('🔊 开始播放 AI 语音回复...')
console.log('🔊 音频数据前100字符:', audioData.substring(0, 100))
await this.playAIVoice(audioData, aiText)
} else {
console.warn('⚠️ 没有收到音频数据')
//
if (aiText) {
setTimeout(() => {
uni.showToast({
title: `AI: ${aiText}`,
icon: 'none', icon: 'none',
duration: 3000 duration: 3000
}) })
}, 2000)
}
}
return result return result
} else { } else {
throw new Error(`ASR请求失败: ${response.statusCode}`) throw new Error(`对话请求失败: ${response.statusCode}`)
} }
} catch (error) { } catch (error) {
console.error('❌ ASR请求失败:', error) console.error('❌ 对话请求失败:', error)
// //
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: 'ASR识别失败', title: '对话处理失败',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}) })
throw error 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() { goRecharge() {
uni.showToast({ uni.showToast({
title: '充值功能开发中', title: '充值功能开发中',

329
完整启动指南.md Normal file
View File

@ -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]
```
### 问题 2Python 依赖缺失
**症状**
```
ModuleNotFoundError: No module named 'xxx'
```
**解决方案**
```bash
cd lover
pip install -r requirements.txt
```
### 问题 3PHP 路径错误
**症状**
```
[错误] 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

186
快速参考.md Normal file
View File

@ -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

383
语音对话功能说明.md Normal file
View File

@ -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 KeyASR + 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 - 完整语音对话

285
语音对话快速开始.md Normal file
View File

@ -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

286
语音播放问题排查.md Normal file
View File

@ -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 字符串。
### 问题 2base64 解码失败
**症状**
```
❌ 播放 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