feat: TTS流式播放+新对话默认关闭

This commit is contained in:
2026-04-28 17:51:47 +08:00
parent 7de13ffc6d
commit c346418d68

View File

@@ -53,9 +53,11 @@ let backendConfig = null; // 从API获取的配置
let currentUser = null; // 当前登录用户 { username, password, registeredAt }
// TTS 语音播放状态
let enableTTS = false; // 是否启用语音播放
let enableTTS = false; // 是否启用语音播放(默认关闭)
let currentPlayingAudio = null; // 当前播放的音频对象
let ttsVoice = 'zh-CN-XiaoxiaoNeural'; // TTS 语音
let ttsQueue = []; // TTS 待播放队列
let isTTSPlaying = false; // 是否正在播放队列
// 每日使用统计(未登录用户)
let dailyUsage = {
@@ -3137,6 +3139,10 @@ async function openAgent(agentId) {
}
}
// 重置 TTS 状态(新对话默认关闭)
enableTTS = false;
stopTTSQueue();
// 创建新对话并设置智能体
const newConv = {
id: backendId || Date.now().toString(),
@@ -3159,6 +3165,9 @@ async function openAgent(agentId) {
function showAgentChatPage() {
if (!currentAgent || !currentConversation) return;
// 停止之前的 TTS 播放
stopTTSQueue();
const chatHtml = `
<div class="chat-page">
<header class="chat-header">
@@ -3616,6 +3625,10 @@ async function createNewConversation() {
return;
}
// 重置 TTS 状态(新对话默认关闭)
enableTTS = false;
stopTTSQueue();
// 如果用户已登录,先在 backend 创建对话
let backendId = null;
if (currentUser && currentUser.id) {
@@ -3653,6 +3666,9 @@ async function createNewConversation() {
// 打开对话
function openConversation(id) {
// 停止之前的 TTS 播放
stopTTSQueue();
currentConversation = conversations.find(c => c.id === id);
if (!currentConversation) {
showConversationList();
@@ -4088,6 +4104,8 @@ async function streamGenerate(userMsgIndex) {
let buffer = '';
let thinkingOutputStarted = false; // 正式内容是否开始输出
let abortController = new AbortController(); // 用于中断流
let ttsAccumulatedText = ''; // TTS 累计文本
let ttsLastPlayedIndex = 0; // 上次播放的文本位置
// 绑定停止按钮事件
const stopBtn = document.getElementById('stopGenerateBtn');
@@ -4157,6 +4175,32 @@ async function streamGenerate(userMsgIndex) {
currentConversation.messages[aiMessageIndex].content += delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
// 流式TTS播放积累文本并播放
if (enableTTS) {
ttsAccumulatedText += delta.content;
// 每80字符播放一段避免太短频繁请求
if (ttsAccumulatedText.length >= 80) {
// 找到合适的断句点(句子结束)
const breakPoints = ['。', '', '', '', '.', '!', '?', ';', '\n'];
let breakIndex = -1;
for (let i = ttsAccumulatedText.length - 1; i >= ttsLastPlayedIndex + 40; i--) {
if (breakPoints.includes(ttsAccumulatedText[i])) {
breakIndex = i;
break;
}
}
// 如果找到断句点或者积累超过120字符强制播放
if (breakIndex >= 0 || ttsAccumulatedText.length - ttsLastPlayedIndex >= 120) {
const textToPlay = ttsAccumulatedText.slice(ttsLastPlayedIndex, breakIndex >= 0 ? breakIndex + 1 : ttsAccumulatedText.length);
if (textToPlay.trim()) {
addToTTSQueue(textToPlay.trim());
}
ttsLastPlayedIndex = breakIndex >= 0 ? breakIndex + 1 : ttsAccumulatedText.length;
}
}
}
scrollToBottom();
}
}
@@ -4170,11 +4214,21 @@ async function streamGenerate(userMsgIndex) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
// 播放剩余未播放的文本流式TTS
if (enableTTS && ttsAccumulatedText && ttsLastPlayedIndex < ttsAccumulatedText.length) {
const remainingText = ttsAccumulatedText.slice(ttsLastPlayedIndex).trim();
if (remainingText) {
addToTTSQueue(remainingText);
}
}
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
// 出错时停止TTS队列
stopTTSQueue();
} finally {
isLoading = false;
sendBtn.disabled = false;
@@ -4187,12 +4241,6 @@ async function streamGenerate(userMsgIndex) {
// 记录统计到 backend
logStatsToBackend('llm_call', currentConversation.agentId || 'chat', 1);
// 自动播放 TTS
const lastMsg = currentConversation.messages[aiMessageIndex];
if (enableTTS && lastMsg && lastMsg.content) {
autoPlayTTS(lastMsg.content);
}
// 自动总结标题第一次对话和每隔5次对话
const totalMessages = currentConversation.messages.length;
// 第一次对话(用户+AI=2条或每5次对话10条
@@ -4740,7 +4788,78 @@ function renderMessages() {
scrollToBottom();
}
// TTS 语音播放
// ==================== TTS 队列播放 ====================
// 添加文本到 TTS 队列
async function addToTTSQueue(text) {
if (!text || !enableTTS) return;
ttsQueue.push(text);
// 如果没有在播放,开始播放队列
if (!isTTSPlaying) {
playTTSQueue();
}
}
// 播放 TTS 队列
async function playTTSQueue() {
if (ttsQueue.length === 0) {
isTTSPlaying = false;
return;
}
isTTSPlaying = true;
while (ttsQueue.length > 0 && enableTTS) {
const text = ttsQueue.shift();
try {
const response = await fetch('/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, voice: ttsVoice })
});
if (!response.ok || !enableTTS) break;
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
// 播放音频
currentPlayingAudio = new Audio(audioUrl);
await currentPlayingAudio.play();
// 等待播放完成
await new Promise(resolve => {
currentPlayingAudio.onended = resolve;
currentPlayingAudio.onerror = resolve;
});
URL.revokeObjectURL(audioUrl);
currentPlayingAudio = null;
} catch (e) {
console.error('TTS播放失败:', e);
break;
}
}
isTTSPlaying = false;
ttsQueue = []; // 清空队列
}
// 停止 TTS 队列播放
function stopTTSQueue() {
ttsQueue = [];
isTTSPlaying = false;
if (currentPlayingAudio) {
currentPlayingAudio.pause();
currentPlayingAudio = null;
}
}
// TTS 手动播放(点击消息上的播放按钮)
async function playTTS(index) {
if (!currentConversation || index < 0) return;
@@ -4753,10 +4872,9 @@ async function playTTS(index) {
return;
}
// 如果正在播放,停止播放
if (currentPlayingAudio) {
currentPlayingAudio.pause();
currentPlayingAudio = null;
// 如果正在播放或队列中有内容,停止
if (currentPlayingAudio || isTTSPlaying) {
stopTTSQueue();
showToast('已停止播放');
return;
}
@@ -4783,7 +4901,7 @@ async function playTTS(index) {
// 播放音频
currentPlayingAudio = new Audio(audioUrl);
currentPlayingAudio.play();
await currentPlayingAudio.play();
showToast('开始播放');
@@ -4799,43 +4917,6 @@ async function playTTS(index) {
}
}
// 自动播放 TTSAI回复完成后
function autoPlayTTS(text) {
if (!enableTTS || !text) return;
// 延迟一点时间,让用户先看到内容
setTimeout(async () => {
try {
const response = await fetch('/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, voice: ttsVoice })
});
if (!response.ok) return;
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
// 停止之前的播放
if (currentPlayingAudio) {
currentPlayingAudio.pause();
}
currentPlayingAudio = new Audio(audioUrl);
currentPlayingAudio.play();
currentPlayingAudio.onended = () => {
URL.revokeObjectURL(audioUrl);
currentPlayingAudio = null;
};
} catch (e) {
console.error('自动TTS播放失败:', e);
}
}, 500);
}
// 折叠/展开思考内容
function toggleThinking(block) {
block.classList.toggle('expanded');