4 Commits
6 changed files with 64 additions and 6 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+34 -1
View File
@@ -4,14 +4,18 @@
import os import os
import uvicorn import uvicorn
import aiohttp
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
# 导入后端服务 # 导入后端服务
from server import app as api_app from server import app as api_app
# 确保音频缓存目录存在
os.makedirs("audio_cache", exist_ok=True)
# 主应用 # 主应用
app = FastAPI(title="Voice Chat Web") app = FastAPI(title="Voice Chat Web")
@@ -24,6 +28,35 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# ChatTTS 音频代理(解决 HTTPS 页面访问 HTTP 资源问题)
@app.get("/chattts/audio/{filename}")
async def proxy_chattts_audio(filename: str):
"""代理 ChatTTS 音频文件"""
chattts_url = os.getenv("CHATTTS_URL", "http://192.168.2.5:12002")
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{chattts_url}/audio/{filename}",
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
if resp.status != 200:
return Response(content=b'{"detail":"Audio not found"}', status_code=404, media_type="application/json")
audio_data = await resp.read()
return Response(
content=audio_data,
media_type="audio/wav",
headers={"Cache-Control": "public, max-age=3600"}
)
except Exception as e:
return Response(content=f'{"detail":"{str(e)}"}'.encode(), status_code=500, media_type="application/json")
# 挂载音频文件目录(Edge TTS 生成的 MP3
app.mount("/audio", StaticFiles(directory="audio_cache"), name="audio")
# 挂载 API # 挂载 API
app.mount("/api", api_app) app.mount("/api", api_app)
+30 -5
View File
@@ -453,6 +453,9 @@
<!-- TTS 设置 --> <!-- TTS 设置 -->
<div class="tts-section"> <div class="tts-section">
<h3>🔊 TTS语音合成设置</h3> <h3>🔊 TTS语音合成设置</h3>
<div class="browser-tip" id="browserTip" style="display: none; background: #fff3cd; padding: 8px 12px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; color: #856404;">
⚠️ 当前浏览器可能限制音频播放,建议使用 Chrome/Firefox,或点击页面任意位置解锁播放
</div>
<div class="tts-options" id="ttsOptions"> <div class="tts-options" id="ttsOptions">
<div class="tts-option" data-provider="none"> <div class="tts-option" data-provider="none">
<div class="name">❌ 无 TTS</div> <div class="name">❌ 无 TTS</div>
@@ -545,6 +548,16 @@
let currentVoice = 'zh-CN-XiaoxiaoNeural'; let currentVoice = 'zh-CN-XiaoxiaoNeural';
let autoPlay = true; // 自动播放开关 let autoPlay = true; // 自动播放开关
let volumeLevel = 1.5; // 音量倍率 let volumeLevel = 1.5; // 音量倍率
let userInteracted = false; // 用户是否已交互
// 用户点击页面解锁音频播放能力
document.addEventListener('click', () => {
userInteracted = true;
}, { once: true });
document.addEventListener('touchstart', () => {
userInteracted = true;
}, { once: true });
// 元素 // 元素
const statusDot = document.getElementById('statusDot'); const statusDot = document.getElementById('statusDot');
@@ -569,10 +582,25 @@
await checkStatus(); await checkStatus();
await loadTTSProviders(); await loadTTSProviders();
// 检测特殊浏览器(小米浏览器等)
checkBrowser();
// 定时检查状态 // 定时检查状态
setInterval(checkStatus, 10000); setInterval(checkStatus, 10000);
} }
// 检测浏览器兼容性
function checkBrowser() {
const ua = navigator.userAgent.toLowerCase();
const browserTip = document.getElementById('browserTip');
// 小米浏览器、华为浏览器等国产浏览器UA特征
if (ua.includes('miui') || ua.includes('xiaomi') || ua.includes('huawei') ||
ua.includes('micromessenger') || ua.includes('quark') || ua.includes('ucbrowser')) {
browserTip.style.display = 'block';
}
}
// 检查服务状态 // 检查服务状态
async function checkStatus() { async function checkStatus() {
try { try {
@@ -913,7 +941,7 @@
chatSection.scrollTop = chatSection.scrollHeight; chatSection.scrollTop = chatSection.scrollHeight;
// 自动播放(在元素添加到DOM后) // 自动播放(在元素添加到DOM后)
if (role === 'assistant' && audioData && autoPlay && audioBtnId) { if (role === 'assistant' && audioData && autoPlay && audioBtnId && userInteracted) {
setTimeout(() => { setTimeout(() => {
const btn = document.getElementById(audioBtnId); const btn = document.getElementById(audioBtnId);
if (btn) { if (btn) {
@@ -928,16 +956,13 @@
const audio = new Audio(url); const audio = new Audio(url);
const icon = btn.querySelector('.play-icon'); const icon = btn.querySelector('.play-icon');
// 应用音量倍率
audio.volume = Math.min(volumeLevel, 2); // 最大不超过2
audio.onplay = () => { audio.onplay = () => {
icon.textContent = '🔊'; icon.textContent = '🔊';
btn.classList.add('playing'); btn.classList.add('playing');
}; };
audio.onended = () => { audio.onended = () => {
icon.textContent = url.startsWith('/audio') || url.startsWith('http') ? '🔊' : '▶️'; icon.textContent = url.startsWith('/audio') || url.startsWith('http') || url.startsWith('blob:') ? '🔊' : '▶️';
btn.classList.remove('playing'); btn.classList.remove('playing');
}; };