From 7de13ffc6d2bbce55bec432f23ac9526e3fe6e19 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Tue, 28 Apr 2026 17:28:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=E5=9B=9E=E5=A4=8D=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=8A=9F=E8=83=BD=EF=BC=88Edge=20TTS?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 58 ++++++++++++++++++- www/admin.js | 35 +++++++++++ www/app.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++-- www/style.css | 13 ++++- 4 files changed, 252 insertions(+), 7 deletions(-) diff --git a/backend/app.py b/backend/app.py index 98d7044..3ce8c3e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,7 +4,7 @@ AI Chat App - 后台管理服务 端口: 19021 (与前端同一端口) """ -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 @@ -12,6 +12,8 @@ import sqlite3 from datetime import datetime import hashlib import base64 +import asyncio +import edge_tts app = Flask(__name__, static_folder='../www') CORS(app) @@ -273,6 +275,8 @@ def init_db(): ('app_description', '提供智能对话、多种智能体服务', '应用简介'), ('privacy_policy_url', '', '隐私政策链接'), ('user_agreement_url', '', '用户协议链接'), + ('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)) @@ -285,6 +289,8 @@ def init_db(): ('app_description', '提供智能对话、多种智能体服务', '应用简介'), ('privacy_policy_url', '', '隐私政策链接'), ('user_agreement_url', '', '用户协议链接'), + ('tts_provider', 'edge', 'TTS方案'), + ('tts_voice', 'zh-CN-XiaoxiaoNeural', 'TTS语音'), ] for key, value, desc in default_configs: cursor.execute('SELECT COUNT(*) FROM system_configs WHERE key=?', (key,)) @@ -1386,12 +1392,62 @@ def get_frontend_config(): 'description': system.get('app_description', '提供智能对话、多种智能体服务'), 'privacyPolicyUrl': system.get('privacy_policy_url', ''), 'userAgreementUrl': system.get('user_agreement_url', ''), + '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/www/admin.js b/www/admin.js index f063e84..63c4ee0 100644 --- a/www/admin.js +++ b/www/admin.js @@ -1399,6 +1399,39 @@ async function loadSystemPage(content) { +

TTS语音配置

+ +
+ + + 目前仅支持 Edge TTS,后续将添加更多方案 +
+ +
+ + + 选择AI回复的朗读语音 +
+

链接配置

@@ -1436,6 +1469,8 @@ async function saveSystemConfig() { admin_password: document.getElementById('adminPassword').value, privacy_policy_url: document.getElementById('privacyPolicyUrl').value, user_agreement_url: document.getElementById('userAgreementUrl').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 c8874fe..a3959a5 100644 --- a/www/app.js +++ b/www/app.js @@ -52,6 +52,11 @@ let backendConfig = null; // 从API获取的配置 // 用户状态 let currentUser = null; // 当前登录用户 { username, password, registeredAt } +// TTS 语音播放状态 +let enableTTS = false; // 是否启用语音播放 +let currentPlayingAudio = null; // 当前播放的音频对象 +let ttsVoice = 'zh-CN-XiaoxiaoNeural'; // TTS 语音 + // 每日使用统计(未登录用户) let dailyUsage = { date: null, // 日期 YYYY-MM-DD @@ -127,6 +132,8 @@ async function loadBackendConfig() { // 将后台系统配置赋值到 CONFIG if (backendConfig.system) { CONFIG.system = backendConfig.system; + // 加载 TTS 配置 + ttsVoice = backendConfig.system.ttsVoice || 'zh-CN-XiaoxiaoNeural'; } // 将后台 LLM 配置赋值到 CONFIG @@ -3165,6 +3172,9 @@ function showAgentChatPage() {

${currentAgent.desc}

+
@@ -3257,6 +3267,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; + } + }); + } + // 绑定功能开关按钮事件 if (thinkingBtn) { thinkingBtn.addEventListener('click', () => { @@ -3649,8 +3674,8 @@ function openConversation(id) {

${escapeHtml(currentConversation.title)}

- @@ -3748,8 +3773,20 @@ function openConversation(id) { const backBtn = document.getElementById('backBtn'); if (backBtn) backBtn.addEventListener('click', showConversationList); - const clearBtn = document.getElementById('clearBtn'); - if (clearBtn) clearBtn.addEventListener('click', clearCurrentChat); + // 绑定 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; + } + }); + } // 绑定功能开关按钮事件 if (thinkingBtn) { @@ -4150,6 +4187,12 @@ 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条) @@ -4647,6 +4690,8 @@ function renderMessages() { const copyIcon = ``; + const playIcon = ``; + const actions = isUser ? `
@@ -4655,6 +4700,7 @@ function renderMessages() {
` : `
+