diff --git a/configs/config.yaml b/configs/config.yaml index 12e9c93..211e527 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -16,6 +16,7 @@ tts: segment_threshold: 300 min_sentence_length: 200 max_sentence_length: 300 + api_key: 'zuoban' # OpenAI 到微软 TTS 中文语音的映射 voice_mapping: diff --git a/internal/config/config.go b/internal/config/config.go index 7725bbe..37c39a5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type ServerConfig struct { // TTSConfig 包含Microsoft TTS API配置 type TTSConfig struct { + ApiKey string `mapstructure:"api_key"` Region string `mapstructure:"region"` DefaultVoice string `mapstructure:"default_voice"` DefaultRate string `mapstructure:"default_rate"` diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go index 5f43d59..6cd5725 100644 --- a/internal/http/middleware/auth.go +++ b/internal/http/middleware/auth.go @@ -38,3 +38,21 @@ func OpenAIAuth(apiToken string, next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +// TTSAuth 是用于验证 TTS API 接口的中间件 +func TTSAuth(apiKey string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 从查询参数中获取 api_key + queryKey := r.URL.Query().Get("api_key") + + // 如果 apiKey 配置为空字符串,表示不需要验证 + if apiKey != "" && queryKey != apiKey { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("未授权访问: 无效的 API 密钥")) + return + } + + // 验证通过,继续处理请求 + next.ServeHTTP(w, r) + }) +} diff --git a/internal/http/server/routes.go b/internal/http/server/routes.go index 0bad8ff..77f9b5a 100644 --- a/internal/http/server/routes.go +++ b/internal/http/server/routes.go @@ -30,8 +30,10 @@ func SetupRoutes(cfg *config.Config, ttsService tts.Service) (http.Handler, erro // 设置API文档路由 mux.HandleFunc("/api-doc", pagesHandler.HandleAPIDoc) - // 设置TTS API路由 - mux.HandleFunc("/tts", ttsHandler.HandleTTS) + // 设置TTS API路由 - 添加认证中间件 + ttsHandlerFunc := http.HandlerFunc(ttsHandler.HandleTTS) + authenticatedTTSHandler := middleware.TTSAuth(cfg.TTS.ApiKey, ttsHandlerFunc) + mux.Handle("/tts", authenticatedTTSHandler) // 设置语音列表API路由 mux.HandleFunc("/voices", voicesHandler.HandleVoices) diff --git a/web/static/css/style.css b/web/static/css/style.css index 5970b68..3de7152 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -116,6 +116,46 @@ textarea:focus { color: #7f8c8d; } +/* API Key 输入框样式 */ +#api-key-group { + margin-bottom: 15px; + background-color: #f8f9fa; + padding: 12px 15px; + border-radius: 6px; + border-left: 4px solid #3498db; +} + +.api-key-container { + display: flex; + align-items: center; +} + +.api-key-container label { + width: 80px; /* 增加标签宽度 */ + flex-shrink: 0; /* 防止标签被压缩 */ + margin-bottom: 0; /* 覆盖默认的底部边距 */ +} + +#api-key { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 5px; + font-family: inherit; + font-size: 1rem; +} + +#api-key:focus { + outline: none; + border-color: #3498db; + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +#api-key::placeholder { + color: #aaa; + font-size: 0.9rem; +} + /* 设置区域 */ .settings { display: grid; @@ -260,12 +300,24 @@ footer a:hover { .settings { grid-template-columns: 1fr; } - + header h1 { font-size: 2rem; } - + .card { padding: 15px; } +} + +.settings-row { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 20px; + width: 100%; +} + +.half-width { + width: 48%; } \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js index fee7971..736e044 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -7,6 +7,8 @@ document.addEventListener('DOMContentLoaded', function() { const rateValue = document.getElementById('rateValue'); const pitchInput = document.getElementById('pitch'); const pitchValue = document.getElementById('pitchValue'); + const apiKeyInput = document.getElementById('api-key'); + const apiKeyGroup = document.getElementById('api-key-group'); const speakButton = document.getElementById('speak'); const downloadButton = document.getElementById('download'); const copyLinkButton = document.getElementById('copyLink'); @@ -168,9 +170,15 @@ document.addEventListener('DOMContentLoaded', function() { const style = styleSelect.value; const rate = rateInput.value; const pitch = pitchInput.value; + const apiKey = apiKeyInput.value.trim(); // 构建HttpTTS链接 - const httpTtsLink = `${window.location.origin}${config.basePath}/tts?t={{java.encodeURI(speakText)}}&v=${voice}&r={{speakSpeed*4}}&p=${pitch}&s=${style}`; + let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t={{java.encodeURI(speakText)}}&v=${voice}&r={{speakSpeed*4}}&p=${pitch}&s=${style}`; + + // 添加API Key参数(如果有) + if (apiKey) { + httpTtsLink += `&api_key=${apiKey}`; + } copyToClipboard(httpTtsLink); }); @@ -212,6 +220,7 @@ document.addEventListener('DOMContentLoaded', function() { const style = styleSelect.value; const rate = rateInput.value; const pitch = pitchInput.value; + const apiKey = apiKeyInput.value.trim(); // 禁用按钮,显示加载状态 speakButton.disabled = true; @@ -227,11 +236,34 @@ document.addEventListener('DOMContentLoaded', function() { p: pitch }); + // 添加API Key参数(如果有) + if (apiKey) { + params.append('api_key', apiKey); + } + const url = `${config.basePath}/tts?${params.toString()}`; + // 使用fetch发送请求以便捕获HTTP状态码 + const response = await fetch(url); + + if (response.status === 401) { + // 显示API Key输入框 + apiKeyGroup.style.display = 'flex'; + alert('请输入有效的API Key以继续操作'); + throw new Error('需要API Key授权'); + } + + if (!response.ok) { + throw new Error(`HTTP错误: ${response.status}`); + } + + // 获取音频blob + const blob = await response.blob(); + const audioUrl = URL.createObjectURL(blob); + // 更新音频播放器 - audioPlayer.src = url; - lastAudioUrl = url; + audioPlayer.src = audioUrl; + lastAudioUrl = url; // 保存原始URL用于下载和复制链接 // 显示结果区域 resultSection.style.display = 'block'; @@ -240,7 +272,9 @@ document.addEventListener('DOMContentLoaded', function() { audioPlayer.play(); } catch (error) { console.error('生成语音失败:', error); - alert('生成语音失败,请重试'); + if (error.message !== '需要API Key授权') { + alert('生成语音失败,请重试'); + } } finally { // 恢复按钮状态 speakButton.disabled = false; diff --git a/web/templates/index.html b/web/templates/index.html index 51b01ed..fb1be44 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -27,6 +27,11 @@