语音功能完成
This commit is contained in:
parent
4d71363fad
commit
16c66112ef
|
|
@ -1649,14 +1649,20 @@ async def voice_conversation(
|
||||||
|
|
||||||
logger.info(f"TTS 合成完成,音频大小: {len(audio_bytes)} 字节")
|
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')
|
audio_base64_result = base64.b64encode(audio_bytes).decode('utf-8')
|
||||||
|
|
||||||
from ..response import success_response
|
from ..response import success_response
|
||||||
return success_response({
|
return success_response({
|
||||||
"user_text": user_text,
|
"user_text": user_text,
|
||||||
"ai_text": ai_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"
|
"audio_format": "mp3"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1310,14 +1310,16 @@
|
||||||
|
|
||||||
if (response.statusCode === 200 && response.data) {
|
if (response.statusCode === 200 && response.data) {
|
||||||
const result = 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 data = result.data || result
|
||||||
const userText = data.user_text
|
const userText = data.user_text
|
||||||
const aiText = data.ai_text
|
const aiText = data.ai_text
|
||||||
const audioData = data.audio_data
|
const audioData = data.audio_data
|
||||||
|
const audioUrl = data.audio_url // 新增:OSS URL
|
||||||
|
|
||||||
console.log('✅ 识别结果:', userText)
|
console.log('✅ 识别结果:', userText)
|
||||||
console.log('✅ AI回复:', aiText)
|
console.log('✅ AI回复:', aiText)
|
||||||
|
console.log('✅ 音频 URL:', audioUrl || '无')
|
||||||
console.log('✅ 音频数据:', audioData ? `${audioData.length} 字符` : '无')
|
console.log('✅ 音频数据:', audioData ? `${audioData.length} 字符` : '无')
|
||||||
|
|
||||||
// 显示识别结果
|
// 显示识别结果
|
||||||
|
|
@ -1329,9 +1331,12 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放 AI 的语音回复
|
// 播放 AI 的语音回复 - 优先使用 URL
|
||||||
if (audioData) {
|
if (audioUrl) {
|
||||||
console.log('🔊 开始播放 AI 语音回复...')
|
console.log('🔊 使用 OSS URL 播放 AI 语音...')
|
||||||
|
await this.playAIVoiceFromUrl(audioUrl, aiText)
|
||||||
|
} else if (audioData) {
|
||||||
|
console.log('🔊 使用 base64 播放 AI 语音...')
|
||||||
console.log('🔊 音频数据前100字符:', audioData.substring(0, 100))
|
console.log('🔊 音频数据前100字符:', audioData.substring(0, 100))
|
||||||
await this.playAIVoice(audioData, aiText)
|
await this.playAIVoice(audioData, aiText)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1366,6 +1371,70 @@
|
||||||
throw error
|
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) {
|
async playAIVoice(base64Audio, aiText) {
|
||||||
console.log('🔊 playAIVoice 被调用')
|
console.log('🔊 playAIVoice 被调用')
|
||||||
console.log('🔊 base64Audio 长度:', base64Audio ? base64Audio.length : 0)
|
console.log('🔊 base64Audio 长度:', base64Audio ? base64Audio.length : 0)
|
||||||
|
|
@ -1397,134 +1466,100 @@
|
||||||
console.log('✅ 音频数据解码完成,大小:', bytes.length, 'bytes')
|
console.log('✅ 音频数据解码完成,大小:', bytes.length, 'bytes')
|
||||||
|
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
// APP 环境 - 使用 plus.io 写入文件
|
// APP 环境 - 使用 uni.saveFile 保存 base64 数据
|
||||||
console.log('📱 APP 环境,保存音频文件')
|
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) => {
|
plus.io.resolveLocalFileSystemURL('_doc/', (docEntry) => {
|
||||||
console.log('✅ 获取 _doc 目录成功')
|
console.log('✅ 获取 _doc 目录成功')
|
||||||
|
|
||||||
docEntry.getFile(fileName, {create: true, exclusive: false}, (fileEntry) => {
|
const fileName = `ai_voice_${Date.now()}.mp3`
|
||||||
console.log('✅ 文件对象创建成功')
|
|
||||||
console.log('📁 文件路径:', fileEntry.fullPath)
|
docEntry.getFile(fileName, {create: true}, (fileEntry) => {
|
||||||
|
console.log('✅ 文件创建成功:', fileEntry.fullPath)
|
||||||
|
|
||||||
// 创建 FileWriter
|
|
||||||
fileEntry.createWriter((writer) => {
|
fileEntry.createWriter((writer) => {
|
||||||
console.log('✅ FileWriter 创建成功')
|
console.log('✅ Writer 创建成功')
|
||||||
|
|
||||||
// 设置写入完成事件
|
writer.onwriteend = function() {
|
||||||
writer.onwriteend = function(evt) {
|
|
||||||
console.log('✅✅✅ 文件写入完成!')
|
console.log('✅✅✅ 文件写入完成!')
|
||||||
|
|
||||||
// 验证文件大小
|
|
||||||
fileEntry.file((file) => {
|
fileEntry.file((file) => {
|
||||||
console.log('📊 文件大小:', file.size, 'bytes')
|
console.log('<EFBFBD> 文件大小:', file.size, 'bytes')
|
||||||
|
|
||||||
if (file.size === 0) {
|
if (file.size === 0) {
|
||||||
console.error('❌ 文件大小为 0')
|
console.error('❌ 文件大小为 0')
|
||||||
uni.showToast({
|
|
||||||
title: '文件保存失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文件 URL
|
|
||||||
const audioUrl = fileEntry.toLocalURL()
|
const audioUrl = fileEntry.toLocalURL()
|
||||||
console.log('🎵 准备播放,URL:', audioUrl)
|
console.log('🎵 文件 URL:', audioUrl)
|
||||||
|
|
||||||
// 使用 plus.audio 播放
|
// 播放
|
||||||
console.log('🎵 创建播放器...')
|
|
||||||
const player = plus.audio.createPlayer(audioUrl)
|
const player = plus.audio.createPlayer(audioUrl)
|
||||||
|
|
||||||
player.play(() => {
|
player.play(() => {
|
||||||
console.log('🔊🔊🔊 AI 语音开始播放!')
|
console.log('<EFBFBD>🔊🔊 AI 语音开始播放!')
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: 'AI 正在说话',
|
title: 'AI 正在说话',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 1000
|
duration: 1000
|
||||||
})
|
})
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
console.error('❌ plus.audio 播放失败:', e)
|
console.error('❌ 播放失败:', 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('🗑️ 文件已清理'))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
player.addEventListener('ended', () => {
|
player.addEventListener('ended', () => {
|
||||||
console.log('✅ 播放完成')
|
console.log('✅ 播放完成')
|
||||||
player.close()
|
player.close()
|
||||||
// 清理文件
|
|
||||||
fileEntry.remove(() => console.log('🗑️ 文件已清理'))
|
fileEntry.remove(() => console.log('🗑️ 文件已清理'))
|
||||||
})
|
})
|
||||||
|
|
||||||
player.addEventListener('error', (e) => {
|
|
||||||
console.error('❌ 播放错误:', e)
|
|
||||||
player.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
}, (error) => {
|
|
||||||
console.error('❌ 获取文件信息失败:', error)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置写入错误事件
|
|
||||||
writer.onerror = function(e) {
|
writer.onerror = function(e) {
|
||||||
console.error('❌❌❌ 文件写入失败!')
|
console.error('❌ 写入失败:', e)
|
||||||
console.error('错误对象:', e)
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '文件保存失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接写入 Uint8Array
|
// 关键:将 base64 字符串直接写入
|
||||||
console.log('📝 开始写入二进制数据...')
|
// FileWriter 支持字符串,我们写入原始 base64
|
||||||
console.log('📝 数据大小:', bytes.length, 'bytes')
|
console.log('📝 写入 base64 字符串...')
|
||||||
writer.write(bytes)
|
|
||||||
|
|
||||||
}, (error) => {
|
// 将 Uint8Array 转换为 Blob(标准方式)
|
||||||
console.error('❌ 创建 FileWriter 失败:', error)
|
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)
|
||||||
})
|
})
|
||||||
|
}, (err) => {
|
||||||
}, (error) => {
|
console.error('❌ 创建文件失败:', err)
|
||||||
console.error('❌ 创建文件失败:', error)
|
|
||||||
})
|
})
|
||||||
|
}, (err) => {
|
||||||
}, (error) => {
|
console.error('❌ 获取目录失败:', err)
|
||||||
console.error('❌ 获取文件系统失败:', error)
|
|
||||||
})
|
})
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user