语音功能完成
This commit is contained in:
parent
4d71363fad
commit
16c66112ef
|
|
@ -1649,14 +1649,20 @@ async def voice_conversation(
|
|||
|
||||
logger.info(f"TTS 合成完成,音频大小: {len(audio_bytes)} 字节")
|
||||
|
||||
# 5. 返回结果
|
||||
# 5. 上传音频到 OSS 并返回 URL
|
||||
logger.info("上传 TTS 音频到 OSS...")
|
||||
audio_url = upload_audio_file(audio_bytes, 'mp3')
|
||||
logger.info(f"TTS 音频已上传: {audio_url}")
|
||||
|
||||
# 同时返回 base64(兼容旧版本)
|
||||
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_data": audio_base64_result, # 保留 base64(兼容)
|
||||
"audio_url": audio_url, # 新增:OSS URL
|
||||
"audio_format": "mp3"
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1310,14 +1310,16 @@
|
|||
|
||||
if (response.statusCode === 200 && response.data) {
|
||||
const result = response.data
|
||||
// 后端返回格式: {code: 1, msg: "ok", data: {user_text, ai_text, audio_data}}
|
||||
// 后端返回格式: {code: 1, msg: "ok", data: {user_text, ai_text, audio_data, audio_url}}
|
||||
const data = result.data || result
|
||||
const userText = data.user_text
|
||||
const aiText = data.ai_text
|
||||
const audioData = data.audio_data
|
||||
const audioUrl = data.audio_url // 新增:OSS URL
|
||||
|
||||
console.log('✅ 识别结果:', userText)
|
||||
console.log('✅ AI回复:', aiText)
|
||||
console.log('✅ 音频 URL:', audioUrl || '无')
|
||||
console.log('✅ 音频数据:', audioData ? `${audioData.length} 字符` : '无')
|
||||
|
||||
// 显示识别结果
|
||||
|
|
@ -1329,9 +1331,12 @@
|
|||
})
|
||||
}
|
||||
|
||||
// 播放 AI 的语音回复
|
||||
if (audioData) {
|
||||
console.log('🔊 开始播放 AI 语音回复...')
|
||||
// 播放 AI 的语音回复 - 优先使用 URL
|
||||
if (audioUrl) {
|
||||
console.log('🔊 使用 OSS URL 播放 AI 语音...')
|
||||
await this.playAIVoiceFromUrl(audioUrl, aiText)
|
||||
} else if (audioData) {
|
||||
console.log('🔊 使用 base64 播放 AI 语音...')
|
||||
console.log('🔊 音频数据前100字符:', audioData.substring(0, 100))
|
||||
await this.playAIVoice(audioData, aiText)
|
||||
} else {
|
||||
|
|
@ -1366,6 +1371,70 @@
|
|||
throw error
|
||||
}
|
||||
},
|
||||
async playAIVoiceFromUrl(audioUrl, aiText) {
|
||||
console.log('🔊 playAIVoiceFromUrl 被调用')
|
||||
console.log('🔊 音频 URL:', audioUrl)
|
||||
console.log('🔊 aiText:', aiText)
|
||||
|
||||
try {
|
||||
// 显示 AI 回复文字
|
||||
if (aiText) {
|
||||
uni.showToast({
|
||||
title: `AI: ${aiText.substring(0, 20)}...`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
|
||||
// 直接使用 URL 播放
|
||||
const audioContext = uni.createInnerAudioContext()
|
||||
audioContext.src = audioUrl
|
||||
audioContext.autoplay = true
|
||||
audioContext.volume = 1.0
|
||||
|
||||
console.log('🎵 音频上下文已创建,开始播放...')
|
||||
|
||||
audioContext.onPlay(() => {
|
||||
console.log('🔊🔊🔊 AI 语音开始播放!')
|
||||
uni.showToast({
|
||||
title: 'AI 正在说话',
|
||||
icon: 'none',
|
||||
duration: 1000
|
||||
})
|
||||
})
|
||||
|
||||
audioContext.onEnded(() => {
|
||||
console.log('✅ AI 语音播放完成')
|
||||
audioContext.destroy()
|
||||
})
|
||||
|
||||
audioContext.onError((error) => {
|
||||
console.error('❌ 播放失败:', error)
|
||||
console.error('错误码:', error.errCode)
|
||||
console.error('错误信息:', error.errMsg)
|
||||
|
||||
uni.showToast({
|
||||
title: '播放失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
audioContext.destroy()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 播放 AI 语音失败:', error)
|
||||
console.error('错误类型:', error.constructor.name)
|
||||
console.error('错误消息:', error.message)
|
||||
console.error('错误堆栈:', error.stack)
|
||||
|
||||
uni.showToast({
|
||||
title: '播放失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
async playAIVoice(base64Audio, aiText) {
|
||||
console.log('🔊 playAIVoice 被调用')
|
||||
console.log('🔊 base64Audio 长度:', base64Audio ? base64Audio.length : 0)
|
||||
|
|
@ -1397,134 +1466,100 @@
|
|||
console.log('✅ 音频数据解码完成,大小:', bytes.length, 'bytes')
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP 环境 - 使用 plus.io 写入文件
|
||||
// APP 环境 - 使用 uni.saveFile 保存 base64 数据
|
||||
console.log('📱 APP 环境,保存音频文件')
|
||||
console.log('📊 音频数据大小:', bytes.length, 'bytes')
|
||||
console.log('📊 base64 数据大小:', base64Audio.length, '字符')
|
||||
|
||||
const fileName = `ai_voice_${Date.now()}.mp3`
|
||||
// 先将 base64 转换为临时文件路径
|
||||
// 使用 uni.base64ToArrayBuffer 和 uni.arrayBufferToBase64
|
||||
const tempFileName = `temp_audio_${Date.now()}.mp3`
|
||||
const tempPath = `_doc/${tempFileName}`
|
||||
|
||||
// 使用 plus.io 保存文件
|
||||
console.log('<27> 临时文件路径:', tempPath)
|
||||
|
||||
// 方法:使用 XMLHttpRequest 将 base64 转换为 Blob,然后保存
|
||||
// 创建一个 data URL
|
||||
const dataUrl = 'data:audio/mp3;base64,' + base64Audio
|
||||
|
||||
// 使用 fetch API(如果可用)或 XMLHttpRequest
|
||||
console.log('🔄 开始转换 base64 为文件...')
|
||||
|
||||
// 使用 plus.io 直接写入
|
||||
plus.io.resolveLocalFileSystemURL('_doc/', (docEntry) => {
|
||||
console.log('✅ 获取 _doc 目录成功')
|
||||
|
||||
docEntry.getFile(fileName, {create: true, exclusive: false}, (fileEntry) => {
|
||||
console.log('✅ 文件对象创建成功')
|
||||
console.log('📁 文件路径:', fileEntry.fullPath)
|
||||
const fileName = `ai_voice_${Date.now()}.mp3`
|
||||
|
||||
docEntry.getFile(fileName, {create: true}, (fileEntry) => {
|
||||
console.log('✅ 文件创建成功:', fileEntry.fullPath)
|
||||
|
||||
// 创建 FileWriter
|
||||
fileEntry.createWriter((writer) => {
|
||||
console.log('✅ FileWriter 创建成功')
|
||||
console.log('✅ Writer 创建成功')
|
||||
|
||||
// 设置写入完成事件
|
||||
writer.onwriteend = function(evt) {
|
||||
writer.onwriteend = function() {
|
||||
console.log('✅✅✅ 文件写入完成!')
|
||||
|
||||
// 验证文件大小
|
||||
fileEntry.file((file) => {
|
||||
console.log('📊 文件大小:', file.size, 'bytes')
|
||||
console.log('<EFBFBD> 文件大小:', file.size, 'bytes')
|
||||
|
||||
if (file.size === 0) {
|
||||
console.error('❌ 文件大小为 0')
|
||||
uni.showToast({
|
||||
title: '文件保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件 URL
|
||||
const audioUrl = fileEntry.toLocalURL()
|
||||
console.log('🎵 准备播放,URL:', audioUrl)
|
||||
console.log('🎵 文件 URL:', audioUrl)
|
||||
|
||||
// 使用 plus.audio 播放
|
||||
console.log('🎵 创建播放器...')
|
||||
// 播放
|
||||
const player = plus.audio.createPlayer(audioUrl)
|
||||
|
||||
player.play(() => {
|
||||
console.log('🔊🔊🔊 AI 语音开始播放!')
|
||||
console.log('<EFBFBD>🔊🔊 AI 语音开始播放!')
|
||||
uni.showToast({
|
||||
title: 'AI 正在说话',
|
||||
icon: 'none',
|
||||
duration: 1000
|
||||
})
|
||||
}, (e) => {
|
||||
console.error('❌ plus.audio 播放失败:', e)
|
||||
console.error('错误详情:', JSON.stringify(e))
|
||||
|
||||
// 如果 plus.audio 失败,尝试 uni.createInnerAudioContext
|
||||
console.log('🔄 尝试使用 InnerAudioContext')
|
||||
const ctx = uni.createInnerAudioContext()
|
||||
ctx.src = audioUrl
|
||||
ctx.autoplay = true
|
||||
|
||||
ctx.onPlay(() => {
|
||||
console.log('🔊 InnerAudioContext 播放成功')
|
||||
uni.showToast({
|
||||
title: 'AI 正在说话',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
|
||||
ctx.onError((err) => {
|
||||
console.error('❌ InnerAudioContext 也失败:', err)
|
||||
uni.showToast({
|
||||
title: '播放失败',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
})
|
||||
|
||||
ctx.onEnded(() => {
|
||||
console.log('✅ 播放完成')
|
||||
ctx.destroy()
|
||||
// 清理文件
|
||||
fileEntry.remove(() => console.log('🗑️ 文件已清理'))
|
||||
})
|
||||
console.error('❌ 播放失败:', e)
|
||||
})
|
||||
|
||||
player.addEventListener('ended', () => {
|
||||
console.log('✅ 播放完成')
|
||||
player.close()
|
||||
// 清理文件
|
||||
fileEntry.remove(() => console.log('🗑️ 文件已清理'))
|
||||
})
|
||||
|
||||
player.addEventListener('error', (e) => {
|
||||
console.error('❌ 播放错误:', e)
|
||||
player.close()
|
||||
})
|
||||
|
||||
}, (error) => {
|
||||
console.error('❌ 获取文件信息失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
// 设置写入错误事件
|
||||
writer.onerror = function(e) {
|
||||
console.error('❌❌❌ 文件写入失败!')
|
||||
console.error('错误对象:', e)
|
||||
|
||||
uni.showToast({
|
||||
title: '文件保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
console.error('❌ 写入失败:', e)
|
||||
}
|
||||
|
||||
// 直接写入 Uint8Array
|
||||
console.log('📝 开始写入二进制数据...')
|
||||
console.log('📝 数据大小:', bytes.length, 'bytes')
|
||||
writer.write(bytes)
|
||||
// 关键:将 base64 字符串直接写入
|
||||
// FileWriter 支持字符串,我们写入原始 base64
|
||||
console.log('📝 写入 base64 字符串...')
|
||||
|
||||
}, (error) => {
|
||||
console.error('❌ 创建 FileWriter 失败:', error)
|
||||
// 将 Uint8Array 转换为 Blob(标准方式)
|
||||
try {
|
||||
// 尝试创建 Blob
|
||||
const blob = new Blob([bytes.buffer], {type: 'audio/mpeg'})
|
||||
console.log('✅ Blob 创建成功,大小:', blob.size)
|
||||
writer.write(blob)
|
||||
} catch (err) {
|
||||
console.error('❌ Blob 创建失败:', err)
|
||||
// 降级:写入 base64 字符串
|
||||
console.log('🔄 降级:写入 base64 字符串')
|
||||
writer.write(base64Audio)
|
||||
}
|
||||
|
||||
}, (err) => {
|
||||
console.error('❌ 创建 Writer 失败:', err)
|
||||
})
|
||||
|
||||
}, (error) => {
|
||||
console.error('❌ 创建文件失败:', error)
|
||||
}, (err) => {
|
||||
console.error('❌ 创建文件失败:', err)
|
||||
})
|
||||
|
||||
}, (error) => {
|
||||
console.error('❌ 获取文件系统失败:', error)
|
||||
}, (err) => {
|
||||
console.error('❌ 获取目录失败:', err)
|
||||
})
|
||||
// #endif
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user