'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)}")
|
||||
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)}")
|
||||
|
Binary file not shown.
@@ -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):
|
||||
"""使用信号量控制并发数的单个文本合成"""
|
||||
|
Binary file not shown.
@@ -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', () => {
|
||||
|
Reference in New Issue
Block a user