语音功能完成

This commit is contained in:
Lilixu007 2026-03-06 09:23:43 +08:00
parent 4d71363fad
commit 16c66112ef
2 changed files with 131 additions and 90 deletions

View File

@ -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"
}) })

View File

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