'commit'
This commit is contained in:
Binary file not shown.
@@ -187,16 +187,37 @@ async def streaming_chat(websocket: WebSocket):
|
|||||||
logger.error(f"发送音频块失败: {str(e)}")
|
logger.error(f"发送音频块失败: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# 获取LLM流式输出并断句
|
# 获取学伴响应内容(包含题目信息)
|
||||||
logger.info("开始LLM处理和TTS合成...")
|
logger.info("获取学伴响应内容...")
|
||||||
|
llm_chunks = []
|
||||||
|
async for chunk in get_xueban_response_async(asr_result['text'], stream=True):
|
||||||
|
llm_chunks.append(chunk)
|
||||||
|
full_llm_response = ''.join(llm_chunks)
|
||||||
|
logger.info(f"学伴响应内容: {full_llm_response}")
|
||||||
|
|
||||||
|
# 定义音频回调函数,将音频块发送给前端
|
||||||
|
async def audio_callback(audio_chunk):
|
||||||
|
logger.info(f"发送音频块,大小: {len(audio_chunk)}")
|
||||||
try:
|
try:
|
||||||
text_stream = stream_and_split_text(asr_result['text'])
|
await websocket.send_bytes(audio_chunk)
|
||||||
|
logger.info("音频块发送成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送音频块失败: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 实时获取LLM流式输出并处理
|
||||||
|
logger.info("开始LLM流式处理和TTS合成...")
|
||||||
|
try:
|
||||||
|
# 直接将LLM流式响应接入TTS
|
||||||
|
llm_stream = get_xueban_response_async(asr_result['text'], stream=True)
|
||||||
|
text_stream = stream_and_split_text(llm_stream) # 异步函数调用
|
||||||
|
|
||||||
# 初始化TTS处理器
|
# 初始化TTS处理器
|
||||||
tts = StreamingVolcanoTTS(max_concurrency=2)
|
tts = StreamingVolcanoTTS(max_concurrency=1)
|
||||||
|
|
||||||
# 流式处理文本并生成音频
|
# 异步迭代文本流
|
||||||
await tts.synthesize_stream(text_stream, audio_callback)
|
async for text_chunk in text_stream:
|
||||||
|
await tts._synthesize_single_with_semaphore(text_chunk, audio_callback)
|
||||||
logger.info("TTS合成完成")
|
logger.info("TTS合成完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"TTS合成失败: {str(e)}")
|
logger.error(f"TTS合成失败: {str(e)}")
|
||||||
|
Binary file not shown.
@@ -60,6 +60,36 @@ def stream_and_split_text(query_text):
|
|||||||
yield buffer
|
yield buffer
|
||||||
|
|
||||||
|
|
||||||
|
# 修改为
|
||||||
|
async def stream_and_split_text(llm_stream):
|
||||||
|
"""
|
||||||
|
流式获取LLM输出并按句子分割
|
||||||
|
@param llm_stream: LLM流式响应生成器
|
||||||
|
@return: 异步生成器,每次产生一个完整句子
|
||||||
|
"""
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
|
# 直接处理LLM流式输出
|
||||||
|
async for content in llm_stream:
|
||||||
|
buffer += content
|
||||||
|
|
||||||
|
# 使用正则表达式检测句子结束
|
||||||
|
sentences = re.split(r'([。!?.!?])', buffer)
|
||||||
|
if len(sentences) > 1:
|
||||||
|
# 提取完整句子
|
||||||
|
for i in range(0, len(sentences)-1, 2):
|
||||||
|
if i+1 < len(sentences):
|
||||||
|
sentence = sentences[i] + sentences[i+1]
|
||||||
|
yield sentence
|
||||||
|
|
||||||
|
# 保留不完整的部分
|
||||||
|
buffer = sentences[-1]
|
||||||
|
|
||||||
|
# 处理最后剩余的部分
|
||||||
|
if buffer:
|
||||||
|
yield buffer
|
||||||
|
|
||||||
|
|
||||||
class StreamingVolcanoTTS:
|
class StreamingVolcanoTTS:
|
||||||
def __init__(self, voice_type='zh_female_wanwanxiaohe_moon_bigtts', encoding='wav', max_concurrency=2):
|
def __init__(self, voice_type='zh_female_wanwanxiaohe_moon_bigtts', encoding='wav', max_concurrency=2):
|
||||||
self.voice_type = voice_type
|
self.voice_type = voice_type
|
||||||
@@ -85,15 +115,10 @@ class StreamingVolcanoTTS:
|
|||||||
text_stream: 文本流生成器
|
text_stream: 文本流生成器
|
||||||
audio_callback: 音频数据回调函数,接收音频片段
|
audio_callback: 音频数据回调函数,接收音频片段
|
||||||
"""
|
"""
|
||||||
# 为每个文本片段创建一个WebSocket连接,但限制并发数
|
# 实时处理每个文本片段(删除任务列表和gather)
|
||||||
tasks = []
|
async for text in text_stream:
|
||||||
for text in text_stream:
|
if text.strip():
|
||||||
if text.strip(): # 忽略空文本
|
await self._synthesize_single_with_semaphore(text, audio_callback)
|
||||||
task = asyncio.create_task(self._synthesize_single_with_semaphore(text, audio_callback))
|
|
||||||
tasks.append(task)
|
|
||||||
|
|
||||||
# 等待所有任务完成
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _synthesize_single_with_semaphore(self, text, audio_callback):
|
async def _synthesize_single_with_semaphore(self, text, audio_callback):
|
||||||
"""使用信号量控制并发数的单个文本合成"""
|
"""使用信号量控制并发数的单个文本合成"""
|
||||||
|
Binary file not shown.
@@ -14,7 +14,10 @@ const AudioState = {
|
|||||||
playback: {
|
playback: {
|
||||||
audioElement: null,
|
audioElement: null,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
audioChunks: [] // 存储接收到的音频块
|
audioChunks: [], // 存储接收到的音频块
|
||||||
|
audioQueue: [], // 音频队列,用于流式播放
|
||||||
|
isStreamPlaying: false, // 是否正在流式播放
|
||||||
|
currentAudioIndex: 0 // 当前播放的音频索引
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
connection: null,
|
connection: null,
|
||||||
@@ -178,12 +181,13 @@ const WebSocketManager = {
|
|||||||
UIController.toggleElement('thinkingIndicator', false);
|
UIController.toggleElement('thinkingIndicator', false);
|
||||||
UIController.setStartRecordButtonEnabled(true);
|
UIController.setStartRecordButtonEnabled(true);
|
||||||
|
|
||||||
// 合并所有音频块并播放
|
// 标记流式播放结束
|
||||||
if (AudioState.playback.audioChunks.length > 0) {
|
AudioState.playback.isStreamPlaying = false;
|
||||||
console.log('开始合并和播放音频');
|
|
||||||
this.combineAndPlayAudio();
|
// 如果有音频数据但尚未开始播放,则开始播放
|
||||||
} else {
|
if (AudioState.playback.audioQueue.length > 0 && !AudioState.playback.isPlaying) {
|
||||||
console.warn('没有收到音频数据,无法播放');
|
console.log('开始播放队列中的音频');
|
||||||
|
AudioPlayer.processAudioQueue();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -192,6 +196,9 @@ const WebSocketManager = {
|
|||||||
console.error('收到错误消息:', data.message);
|
console.error('收到错误消息:', data.message);
|
||||||
UIController.toggleElement('thinkingIndicator', false);
|
UIController.toggleElement('thinkingIndicator', false);
|
||||||
UIController.setStartRecordButtonEnabled(true);
|
UIController.setStartRecordButtonEnabled(true);
|
||||||
|
// 重置流式播放状态
|
||||||
|
AudioState.playback.isStreamPlaying = false;
|
||||||
|
AudioState.playback.audioQueue = [];
|
||||||
alert('处理失败: ' + data.message);
|
alert('处理失败: ' + data.message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -206,8 +213,20 @@ const WebSocketManager = {
|
|||||||
// 二进制音频数据
|
// 二进制音频数据
|
||||||
console.log('收到音频数据,大小:', event.data.size);
|
console.log('收到音频数据,大小:', event.data.size);
|
||||||
console.log('音频数据类型:', event.data.type);
|
console.log('音频数据类型:', event.data.type);
|
||||||
|
|
||||||
|
// 保存到原始音频块数组(保持原有逻辑)
|
||||||
AudioState.playback.audioChunks.push(event.data);
|
AudioState.playback.audioChunks.push(event.data);
|
||||||
console.log('当前音频块数量:', AudioState.playback.audioChunks.length);
|
|
||||||
|
// 添加到音频队列(用于流式播放)
|
||||||
|
AudioState.playback.audioQueue.push(event.data);
|
||||||
|
console.log('当前音频队列长度:', AudioState.playback.audioQueue.length);
|
||||||
|
|
||||||
|
// 如果尚未开始流式播放,则开始播放
|
||||||
|
if (!AudioState.playback.isStreamPlaying && !AudioState.playback.isPlaying) {
|
||||||
|
console.log('开始流式播放音频');
|
||||||
|
AudioState.playback.isStreamPlaying = true;
|
||||||
|
AudioPlayer.processAudioQueue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -355,6 +374,64 @@ const AudioPlayer = {
|
|||||||
const currentTime = AudioState.playback.audioElement.currentTime;
|
const currentTime = AudioState.playback.audioElement.currentTime;
|
||||||
const duration = AudioState.playback.audioElement.duration;
|
const duration = AudioState.playback.audioElement.duration;
|
||||||
UIController.updateTimeDisplay(currentTime, duration);
|
UIController.updateTimeDisplay(currentTime, duration);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化流式播放器
|
||||||
|
initStreamPlayer() {
|
||||||
|
// 创建新的音频元素用于流式播放
|
||||||
|
if (!AudioState.playback.streamAudioElement) {
|
||||||
|
AudioState.playback.streamAudioElement = new Audio();
|
||||||
|
|
||||||
|
// 监听音频结束事件
|
||||||
|
AudioState.playback.streamAudioElement.addEventListener('ended', () => {
|
||||||
|
// 当前音频播放完毕,处理队列中的下一个音频
|
||||||
|
this.processAudioQueue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误事件
|
||||||
|
AudioState.playback.streamAudioElement.addEventListener('error', (e) => {
|
||||||
|
console.error('流式播放音频错误:', e);
|
||||||
|
// 即使出错,也继续处理队列中的下一个音频
|
||||||
|
this.processAudioQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理音频队列
|
||||||
|
processAudioQueue() {
|
||||||
|
// 如果正在播放或队列为空,则返回
|
||||||
|
if (AudioState.playback.isStreamPlaying || AudioState.playback.audioQueue.length === 0) {
|
||||||
|
AudioState.playback.isStreamPlaying = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置播放状态
|
||||||
|
AudioState.playback.isStreamPlaying = true;
|
||||||
|
|
||||||
|
// 从队列中取出第一个音频块
|
||||||
|
const audioBlob = AudioState.playback.audioQueue.shift();
|
||||||
|
|
||||||
|
// 创建音频URL
|
||||||
|
const audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
|
||||||
|
// 设置音频源并播放
|
||||||
|
AudioState.playback.streamAudioElement.src = audioUrl;
|
||||||
|
AudioState.playback.streamAudioElement.play()
|
||||||
|
.then(() => {
|
||||||
|
console.log('开始播放音频块');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('播放音频块失败:', error);
|
||||||
|
// 播放失败,继续处理下一个
|
||||||
|
AudioState.playback.isStreamPlaying = false;
|
||||||
|
this.processAudioQueue();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// 播放完成后释放URL对象
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(audioUrl);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -589,10 +666,12 @@ const RecordingManager = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 初始化 ====================
|
||||||
// 页面加载完成后初始化
|
|
||||||
function initializeApp() {
|
function initializeApp() {
|
||||||
console.log('开始初始化学伴录音功能...');
|
console.log('开始初始化学伴录音功能...');
|
||||||
|
|
||||||
|
// 初始化流式播放器
|
||||||
|
AudioPlayer.initStreamPlayer();
|
||||||
|
|
||||||
// 检查DOM是否已就绪
|
// 检查DOM是否已就绪
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
Reference in New Issue
Block a user