LLM
This commit is contained in:
parent
503ae8a364
commit
28677fe08c
|
|
@ -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)}")
|
||||
|
|
|
|||
130
test_voice_conversation.py
Normal file
130
test_voice_conversation.py
Normal 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)
|
||||
|
|
@ -1272,12 +1272,12 @@
|
|||
},
|
||||
// 发送音频到ASR端点进行处理
|
||||
async sendAudioToASR(audioBytes) {
|
||||
console.log('📤 开始发送音频到ASR端点')
|
||||
console.log('📤 开始发送音频进行语音对话')
|
||||
console.log('📊 音频数据大小:', audioBytes.length, 'bytes')
|
||||
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '语音识别中...',
|
||||
title: '对话处理中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
|
|
@ -1289,9 +1289,9 @@
|
|||
}
|
||||
base64Audio = btoa(base64Audio)
|
||||
|
||||
console.log('📤 发送ASR请求...')
|
||||
console.log('📤 发送语音对话请求...')
|
||||
const response = await uni.request({
|
||||
url: this.baseURLPy + '/voice/call/asr',
|
||||
url: this.baseURLPy + '/voice/call/conversation',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -1299,44 +1299,259 @@
|
|||
},
|
||||
data: {
|
||||
audio_data: base64Audio,
|
||||
format: 'wav' // 改用 WAV 格式
|
||||
format: 'wav'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ ASR响应:', response)
|
||||
console.log('✅ 对话响应:', response)
|
||||
|
||||
// 隐藏加载提示
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.statusCode === 200 && 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({
|
||||
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',
|
||||
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: '充值功能开发中',
|
||||
|
|
|
|||
329
完整启动指南.md
Normal file
329
完整启动指南.md
Normal 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]
|
||||
```
|
||||
|
||||
### 问题 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
|
||||
186
快速参考.md
Normal file
186
快速参考.md
Normal 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
383
语音对话功能说明.md
Normal 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 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 - 完整语音对话
|
||||
285
语音对话快速开始.md
Normal file
285
语音对话快速开始.md
Normal 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
286
语音播放问题排查.md
Normal 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 字符串。
|
||||
|
||||
### 问题 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
|
||||
Loading…
Reference in New Issue
Block a user