diff --git a/backend/app.py b/backend/app.py index bba4d57..f20f0c1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,13 +4,15 @@ AI Chat App - 后台管理服务 端口: 19020 (与前端同一端口) """ -from flask import Flask, jsonify, request, send_from_directory +from flask import Flask, jsonify, request, send_from_directory, Response from flask_cors import CORS import os import json import sqlite3 from datetime import datetime import hashlib +import asyncio +import edge_tts app = Flask(__name__, static_folder='../www') CORS(app) @@ -244,6 +246,8 @@ def init_db(): ('guest_chat_messages', '20', '游客每日对话消息限制'), ('guest_agent_messages', '20', '游客每日智能体消息限制'), ('admin_password', 'admin123', '管理员密码'), + ('tts_provider', 'edge', 'TTS方案'), + ('tts_voice', 'zh-CN-XiaoxiaoNeural', 'TTS语音'), ] for key, value, desc in default_configs: cursor.execute('INSERT INTO system_configs (key, value, description) VALUES (?, ?, ?)', (key, value, desc)) @@ -996,13 +1000,63 @@ def get_frontend_config(): 'chatSessions': int(system.get('guest_chat_sessions', '1')), 'chatMessages': int(system.get('guest_chat_messages', '20')), 'agentMessages': int(system.get('guest_agent_messages', '20')), - } + }, + 'ttsProvider': system.get('tts_provider', 'edge'), + 'ttsVoice': system.get('tts_voice', 'zh-CN-XiaoxiaoNeural'), } } return jsonify(config) +# ==================== TTS 语音合成 ==================== + +@app.route('/api/tts', methods=['POST']) +def generate_tts(): + """使用 Edge TTS 生成语音""" + data = request.json + text = data.get('text', '') + voice = data.get('voice', 'zh-CN-XiaoxiaoNeural') # 默认中文女声 + + if not text: + return jsonify({'error': '缺少文本内容'}), 400 + + try: + # 使用 asyncio 运行 edge_tts + async def generate_audio(): + communicate = edge_tts.Communicate(text, voice) + audio_data = b'' + for chunk in communicate.stream_sync(): + if chunk['type'] == 'audio': + audio_data += chunk['data'] + return audio_data + + audio_data = asyncio.run(generate_audio()) + + # 返回音频数据(MP3格式) + return Response(audio_data, mimetype='audio/mpeg') + + except Exception as e: + return jsonify({'error': f'TTS生成失败: {str(e)}'}), 500 + + +@app.route('/api/tts/voices', methods=['GET']) +def get_tts_voices(): + """获取可用的 TTS 语音列表""" + try: + voices = asyncio.run(edge_tts.list_voices()) + # 过滤中文语音 + chinese_voices = [v for v in voices if v['Locale'].startswith('zh-')] + voice_list = [{ + 'name': v['ShortName'], + 'gender': v['Gender'], + 'locale': v['Locale'] + } for v in chinese_voices] + return jsonify({'voices': voice_list}) + except Exception as e: + return jsonify({'error': f'获取语音列表失败: {str(e)}'}), 500 + + # ==================== 启动 ==================== if __name__ == '__main__': diff --git a/backend/requirements.txt b/backend/requirements.txt index 1a72c35..d90c687 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,3 @@ flask>=2.0.0 -flask-cors>=3.0.0 \ No newline at end of file +flask-cors>=3.0.0 +edge-tts>=6.0.0 \ No newline at end of file diff --git a/www/admin.js b/www/admin.js index a215d65..6dba838 100644 --- a/www/admin.js +++ b/www/admin.js @@ -1264,6 +1264,25 @@ async function loadSystemPage(content) { +

TTS语音配置

+ +
+ + +
+ +
+ + +
+ @@ -1282,7 +1301,9 @@ async function saveSystemConfig() { guest_chat_sessions: document.getElementById('guestChatSessions').value, guest_chat_messages: document.getElementById('guestChatMessages').value, guest_agent_messages: document.getElementById('guestAgentMessages').value, - admin_password: document.getElementById('adminPassword').value + admin_password: document.getElementById('adminPassword').value, + tts_provider: document.getElementById('ttsProvider').value, + tts_voice: document.getElementById('ttsVoice').value }; await fetchAPI('/api/admin/system', 'POST', data); diff --git a/www/app.js b/www/app.js index 0ce4852..233680c 100644 --- a/www/app.js +++ b/www/app.js @@ -123,6 +123,11 @@ async function loadBackendConfig() { console.log('LLM能力: 思考模式=', llmCapabilities.thinking, '视觉=', llmCapabilities.vision); } + // 加载 TTS 配置 + if (backendConfig.system) { + ttsVoice = backendConfig.system.ttsVoice || 'zh-CN-XiaoxiaoNeural'; + } + updateAgentsDisplay(); console.log('后台配置已加载', backendConfig); } catch (e) { @@ -296,6 +301,13 @@ let llmCapabilities = { vision: false // 是否支持视觉能力 }; +// TTS 语音播放状态 +let enableTTS = false; // 是否启用语音播放(新建对话默认关闭) +let currentPlayingAudio = null; // 当前播放的音频对象 +let ttsVoice = 'zh-CN-XiaoxiaoNeural'; // TTS 语音 +let ttsQueue = []; // TTS 待播放队列 +let isTTSPlaying = false; // 是否正在播放队列 + // DOM 元素(初始为 null,在 openConversation 时重新获取) let appContainer = null; let messagesContainer = null; @@ -2915,6 +2927,9 @@ function showAgentChatPage() {

${currentAgent.desc}

+
@@ -3033,6 +3048,21 @@ function showAgentChatPage() { }); } + // 绑定 TTS 开关按钮(智能体对话) + const ttsBtn = document.getElementById('ttsBtn'); + if (ttsBtn) { + ttsBtn.addEventListener('click', () => { + enableTTS = !enableTTS; + ttsBtn.classList.toggle('active', enableTTS); + showToast(enableTTS ? '语音播放已开启' : '语音播放已关闭'); + // 如果关闭,停止当前播放 + if (!enableTTS && currentPlayingAudio) { + currentPlayingAudio.pause(); + currentPlayingAudio = null; + } + }); + } + // 绑定输入事件 userInput.addEventListener('keydown', handleKeyDown); userInput.addEventListener('input', (e) => autoResize(e.target)); @@ -3380,8 +3410,8 @@ function openConversation(id) {

${escapeHtml(currentConversation.title)}

- @@ -3496,6 +3526,21 @@ function openConversation(id) { }); } + // 绑定 TTS 开关按钮(普通对话) + const ttsBtn = document.getElementById('ttsBtn'); + if (ttsBtn) { + ttsBtn.addEventListener('click', () => { + enableTTS = !enableTTS; + ttsBtn.classList.toggle('active', enableTTS); + showToast(enableTTS ? '语音播放已开启' : '语音播放已关闭'); + // 如果关闭,停止当前播放 + if (!enableTTS && currentPlayingAudio) { + currentPlayingAudio.pause(); + currentPlayingAudio = null; + } + }); + } + // 绑定置顶置底按钮事件 const scrollTopBtn = document.getElementById('scrollTopBtn'); const scrollBottomBtn = document.getElementById('scrollBottomBtn'); @@ -4158,6 +4203,8 @@ function renderMessages() { const copyIcon = ``; + const playIcon = ``; + const actions = isUser ? `
@@ -4166,6 +4213,7 @@ function renderMessages() {
` : `
+