feat: add API Key input and authentication for TTS requests
This commit is contained in:
@@ -16,6 +16,7 @@ tts:
|
|||||||
segment_threshold: 300
|
segment_threshold: 300
|
||||||
min_sentence_length: 200
|
min_sentence_length: 200
|
||||||
max_sentence_length: 300
|
max_sentence_length: 300
|
||||||
|
api_key: 'zuoban'
|
||||||
|
|
||||||
# OpenAI 到微软 TTS 中文语音的映射
|
# OpenAI 到微软 TTS 中文语音的映射
|
||||||
voice_mapping:
|
voice_mapping:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
// TTSConfig 包含Microsoft TTS API配置
|
// TTSConfig 包含Microsoft TTS API配置
|
||||||
type TTSConfig struct {
|
type TTSConfig struct {
|
||||||
|
ApiKey string `mapstructure:"api_key"`
|
||||||
Region string `mapstructure:"region"`
|
Region string `mapstructure:"region"`
|
||||||
DefaultVoice string `mapstructure:"default_voice"`
|
DefaultVoice string `mapstructure:"default_voice"`
|
||||||
DefaultRate string `mapstructure:"default_rate"`
|
DefaultRate string `mapstructure:"default_rate"`
|
||||||
|
|||||||
@@ -38,3 +38,21 @@ func OpenAIAuth(apiToken string, next http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(w, r)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ func SetupRoutes(cfg *config.Config, ttsService tts.Service) (http.Handler, erro
|
|||||||
// 设置API文档路由
|
// 设置API文档路由
|
||||||
mux.HandleFunc("/api-doc", pagesHandler.HandleAPIDoc)
|
mux.HandleFunc("/api-doc", pagesHandler.HandleAPIDoc)
|
||||||
|
|
||||||
// 设置TTS API路由
|
// 设置TTS API路由 - 添加认证中间件
|
||||||
mux.HandleFunc("/tts", ttsHandler.HandleTTS)
|
ttsHandlerFunc := http.HandlerFunc(ttsHandler.HandleTTS)
|
||||||
|
authenticatedTTSHandler := middleware.TTSAuth(cfg.TTS.ApiKey, ttsHandlerFunc)
|
||||||
|
mux.Handle("/tts", authenticatedTTSHandler)
|
||||||
|
|
||||||
// 设置语音列表API路由
|
// 设置语音列表API路由
|
||||||
mux.HandleFunc("/voices", voicesHandler.HandleVoices)
|
mux.HandleFunc("/voices", voicesHandler.HandleVoices)
|
||||||
|
|||||||
@@ -116,6 +116,46 @@ textarea:focus {
|
|||||||
color: #7f8c8d;
|
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 {
|
.settings {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -269,3 +309,15 @@ footer a:hover {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.half-width {
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const rateValue = document.getElementById('rateValue');
|
const rateValue = document.getElementById('rateValue');
|
||||||
const pitchInput = document.getElementById('pitch');
|
const pitchInput = document.getElementById('pitch');
|
||||||
const pitchValue = document.getElementById('pitchValue');
|
const pitchValue = document.getElementById('pitchValue');
|
||||||
|
const apiKeyInput = document.getElementById('api-key');
|
||||||
|
const apiKeyGroup = document.getElementById('api-key-group');
|
||||||
const speakButton = document.getElementById('speak');
|
const speakButton = document.getElementById('speak');
|
||||||
const downloadButton = document.getElementById('download');
|
const downloadButton = document.getElementById('download');
|
||||||
const copyLinkButton = document.getElementById('copyLink');
|
const copyLinkButton = document.getElementById('copyLink');
|
||||||
@@ -168,9 +170,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const style = styleSelect.value;
|
const style = styleSelect.value;
|
||||||
const rate = rateInput.value;
|
const rate = rateInput.value;
|
||||||
const pitch = pitchInput.value;
|
const pitch = pitchInput.value;
|
||||||
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
|
||||||
// 构建HttpTTS链接
|
// 构建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);
|
copyToClipboard(httpTtsLink);
|
||||||
});
|
});
|
||||||
@@ -212,6 +220,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const style = styleSelect.value;
|
const style = styleSelect.value;
|
||||||
const rate = rateInput.value;
|
const rate = rateInput.value;
|
||||||
const pitch = pitchInput.value;
|
const pitch = pitchInput.value;
|
||||||
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
|
||||||
// 禁用按钮,显示加载状态
|
// 禁用按钮,显示加载状态
|
||||||
speakButton.disabled = true;
|
speakButton.disabled = true;
|
||||||
@@ -227,11 +236,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
p: pitch
|
p: pitch
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加API Key参数(如果有)
|
||||||
|
if (apiKey) {
|
||||||
|
params.append('api_key', apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${config.basePath}/tts?${params.toString()}`;
|
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;
|
audioPlayer.src = audioUrl;
|
||||||
lastAudioUrl = url;
|
lastAudioUrl = url; // 保存原始URL用于下载和复制链接
|
||||||
|
|
||||||
// 显示结果区域
|
// 显示结果区域
|
||||||
resultSection.style.display = 'block';
|
resultSection.style.display = 'block';
|
||||||
@@ -240,7 +272,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
audioPlayer.play();
|
audioPlayer.play();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成语音失败:', error);
|
console.error('生成语音失败:', error);
|
||||||
alert('生成语音失败,请重试');
|
if (error.message !== '需要API Key授权') {
|
||||||
|
alert('生成语音失败,请重试');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 恢复按钮状态
|
// 恢复按钮状态
|
||||||
speakButton.disabled = false;
|
speakButton.disabled = false;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@
|
|||||||
<div class="char-counter"><span id="charCount">0</span>/5000</div>
|
<div class="char-counter"><span id="charCount">0</span>/5000</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="api-key-group" style="display: none;" class="api-key-container">
|
||||||
|
<label for="api-key">API Key:</label>
|
||||||
|
<input type="password" id="api-key" placeholder="请输入有效的API Key以继续操作">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="voice">语音:</label>
|
<label for="voice">语音:</label>
|
||||||
@@ -41,14 +46,16 @@
|
|||||||
<option value="loading">加载中...</option>
|
<option value="loading">加载中...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-group">
|
<div class="settings-row">
|
||||||
|
<div class="setting-group half-width">
|
||||||
<label for="rate">语速:</label>
|
<label for="rate">语速:</label>
|
||||||
<input type="range" id="rate" min="-100" max="100" value="0">
|
<input type="range" id="rate" min="-100" max="100" value="0">
|
||||||
<span id="rateValue">0%</span>
|
<span id="rateValue">0%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-group">
|
<div class="setting-group half-width">
|
||||||
<label for="pitch">语调:</label>
|
<label for="pitch">语调:</label>
|
||||||
<input type="range" id="pitch" min="-100" max="100" value="0">
|
<input type="range" id="pitch" min="-100" max="100" value="0">
|
||||||
<span id="pitchValue">0%</span>
|
<span id="pitchValue">0%</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user