This commit is contained in:
2025-08-31 12:41:05 +08:00
parent fd9d498e3c
commit 67feea9443
6 changed files with 150 additions and 25 deletions

View File

@@ -187,16 +187,37 @@ async def streaming_chat(websocket: WebSocket):
logger.error(f"发送音频块失败: {str(e)}")
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:
await websocket.send_bytes(audio_chunk)
logger.info("音频块发送成功")
except Exception as e:
logger.error(f"发送音频块失败: {str(e)}")
raise
# 实时获取LLM流式输出并处理
logger.info("开始LLM流式处理和TTS合成...")
try:
text_stream = stream_and_split_text(asr_result['text'])
# 直接将LLM流式响应接入TTS
llm_stream = get_xueban_response_async(asr_result['text'], stream=True)
text_stream = stream_and_split_text(llm_stream) # 异步函数调用
# 初始化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合成完成")
except Exception as e:
logger.error(f"TTS合成失败: {str(e)}")

View File

@@ -51,7 +51,37 @@ def stream_and_split_text(query_text):
if i+1 < len(sentences):
sentence = sentences[i] + sentences[i+1]
yield sentence
# 保留不完整的部分
buffer = sentences[-1]
# 处理最后剩余的部分
if 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]
@@ -85,15 +115,10 @@ class StreamingVolcanoTTS:
text_stream: 文本流生成器
audio_callback: 音频数据回调函数,接收音频片段
"""
# 每个文本片段创建一个WebSocket连接但限制并发数
tasks = []
for text in text_stream:
if text.strip(): # 忽略空文本
task = asyncio.create_task(self._synthesize_single_with_semaphore(text, audio_callback))
tasks.append(task)
# 等待所有任务完成
await asyncio.gather(*tasks)
# 实时处理每个文本片段删除任务列表和gather
async for text in text_stream:
if text.strip():
await self._synthesize_single_with_semaphore(text, audio_callback)
async def _synthesize_single_with_semaphore(self, text, audio_callback):
"""使用信号量控制并发数的单个文本合成"""

View File

@@ -14,7 +14,10 @@ const AudioState = {
playback: {
audioElement: null,
isPlaying: false,
audioChunks: [] // 存储接收到的音频块
audioChunks: [], // 存储接收到的音频块
audioQueue: [], // 音频队列,用于流式播放
isStreamPlaying: false, // 是否正在流式播放
currentAudioIndex: 0 // 当前播放的音频索引
},
websocket: {
connection: null,
@@ -178,12 +181,13 @@ const WebSocketManager = {
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
// 合并所有音频块并播放
if (AudioState.playback.audioChunks.length > 0) {
console.log('开始合并和播放音频');
this.combineAndPlayAudio();
} else {
console.warn('没有收到音频数据,无法播放');
// 标记流式播放结束
AudioState.playback.isStreamPlaying = false;
// 如果有音频数据但尚未开始播放,则开始播放
if (AudioState.playback.audioQueue.length > 0 && !AudioState.playback.isPlaying) {
console.log('开始播放队列中的音频');
AudioPlayer.processAudioQueue();
}
break;
@@ -192,6 +196,9 @@ const WebSocketManager = {
console.error('收到错误消息:', data.message);
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
// 重置流式播放状态
AudioState.playback.isStreamPlaying = false;
AudioState.playback.audioQueue = [];
alert('处理失败: ' + data.message);
break;
@@ -206,8 +213,20 @@ const WebSocketManager = {
// 二进制音频数据
console.log('收到音频数据,大小:', event.data.size);
console.log('音频数据类型:', event.data.type);
// 保存到原始音频块数组(保持原有逻辑)
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 duration = AudioState.playback.audioElement.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() {
console.log('开始初始化学伴录音功能...');
// 初始化流式播放器
AudioPlayer.initStreamPlayer();
// 检查DOM是否已就绪
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {