diff --git a/README.md b/README.md deleted file mode 100644 index c0f1fbe..0000000 --- a/README.md +++ /dev/null @@ -1,156 +0,0 @@ -# OpenAITTS配音台 - -一个简洁高效的文本转语音工具,支持自定义OpenAI API地址,基于Go后端和浏览器前端实现。 - -## 功能特点 - -- ✨ 简洁大方的用户界面 -- 🔧 支持自定义API地址 -- 🎤 多种声音模型选择 -- 📱 响应式设计,适配各种设备 -- 💾 本地存储API配置 -- ⚡ 快速生成和播放语音 -- 📥 支持音频下载 - -## 技术栈 - -- **后端**: Go 1.18+ -- **前端**: HTML5 + CSS3 + JavaScript (ES6+) -- **API**: OpenAI TTS API - -## 配置说明 - -### 1. 配置文件方式 - -复制 `config.example.json` 为 `config.json`,然后修改其中的配置: - -```json -{ - "api_base_url": "https://your-api-address.com", - "port": "8080" -} -``` - -### 2. 环境变量方式 - -你也可以通过环境变量来配置: - -```bash -export OPENAI_API_BASE_URL="https://your-api-address.com" -export PORT="8080" -``` - -### 3. 前端直接配置 - -在浏览器界面中,你可以直接输入API Key和API Base URL,这些配置会自动保存到浏览器本地存储中。 - -## 快速开始 - -### 前提条件 - -- 已安装Go 1.18或更高版本 -- 有可用的OpenAI API Key -- 自定义API地址(如果需要) - -### 启动服务 - -在项目根目录下执行: - -```bash -go run main.go -``` - -或者使用配置文件: - -```bash -go run main.go -config=config.json -``` - -服务启动后,会在控制台输出: - -``` -2024/01/01 12:00:00 Server starting on port 8080... -2024/01/01 12:00:00 Using API base URL: https://api.openai.com -2024/01/01 12:00:00 Open http://localhost:8080 in your browser -``` - -### 使用方法 - -1. 在浏览器中打开 `http://localhost:8080` -2. 在API配置区域输入你的API Key和自定义API地址 -3. 在文本输入区域输入要转换为语音的文本 -4. 选择模型和声音参数 -5. 点击"生成语音"按钮 -6. 生成完成后,可以播放或下载音频 - -## 支持的声音模型 - -- **tts-1**: 标准模型,速度快 -- **tts-1-hd**: 高清模型,质量更高 - -## 支持的声音类型 - -- **alloy**: 清晰自然的声音 -- **echo**: 沉稳有力的声音 -- **fable**: 温暖亲切的声音 -- **onyx**: 成熟稳重的声音 -- **nova**: 活泼开朗的声音 -- **shimmer**: 柔美动听的声音 - -## 项目结构 - -``` -. -├── main.go # Go后端主程序 -├── index.html # 前端HTML页面 -├── styles.css # 前端CSS样式 -├── app.js # 前端JavaScript -├── config.example.json # 配置文件示例 -└── README.md # 项目说明文档 -``` - -## API端点 - -### POST /api/tts - -生成语音的API端点,请求体格式: - -```json -{ - "api_key": "sk-...", - "text": "要转换的文本", - "voice": "alloy", - "model": "tts-1-hd", - "api_base": "https://your-api-address.com" -} -``` - -响应: -- 成功:返回MP3音频文件(Content-Type: audio/mpeg) -- 失败:返回JSON格式的错误信息 - -## 注意事项 - -1. 请确保你的API Key有效且有足够的配额 -2. 自定义API地址需要兼容OpenAI TTS API的接口格式 -3. 文本长度建议不超过4096个字符 -4. 生成的音频文件会自动下载到你的默认下载目录 - -## 浏览器支持 - -- Chrome 60+ -- Firefox 55+ -- Safari 12+ -- Edge 79+ - -## 许可证 - -MIT License - -## 贡献 - -欢迎提交Issue和Pull Request! - -## 联系方式 - -如有问题或建议,请通过GitHub Issues反馈。 diff --git a/app.js b/app.js deleted file mode 100644 index 1cafae0..0000000 --- a/app.js +++ /dev/null @@ -1,221 +0,0 @@ -// DOM元素获取 -const apiKeyInput = document.getElementById('apiKey'); -const apiBaseInput = document.getElementById('apiBase'); -const textInput = document.getElementById('textInput'); -const charCount = document.getElementById('charCount'); -const modelSelect = document.getElementById('model'); -const voiceSelect = document.getElementById('voice'); -const generateBtn = document.getElementById('generateBtn'); -const playBtn = document.getElementById('playBtn'); -const downloadBtn = document.getElementById('downloadBtn'); -const statusDiv = document.getElementById('status'); -const audioPlayer = document.getElementById('audioPlayer'); - -// 当前生成的音频URL -let currentAudioURL = null; - -// 初始化函数 -function init() { - // 加载本地存储的配置 - loadConfig(); - - // 绑定事件监听器 - bindEventListeners(); - - // 初始状态更新 - updateCharCount(); -} - -// 加载本地存储的配置 -function loadConfig() { - const savedAPIKey = localStorage.getItem('openai_api_key'); - const savedAPIBase = localStorage.getItem('openai_api_base'); - - if (savedAPIKey) { - apiKeyInput.value = savedAPIKey; - } - - if (savedAPIBase) { - apiBaseInput.value = savedAPIBase; - } -} - -// 保存配置到本地存储 -function saveConfig() { - localStorage.setItem('openai_api_key', apiKeyInput.value); - localStorage.setItem('openai_api_base', apiBaseInput.value); -} - -// 绑定事件监听器 -function bindEventListeners() { - // 文本输入事件 - 更新字符计数 - textInput.addEventListener('input', updateCharCount); - - // API配置输入事件 - 保存到本地存储 - apiKeyInput.addEventListener('input', saveConfig); - apiBaseInput.addEventListener('input', saveConfig); - - // 生成语音按钮点击事件 - generateBtn.addEventListener('click', generateSpeech); - - // 播放按钮点击事件 - playBtn.addEventListener('click', togglePlay); - - // 下载按钮点击事件 - downloadBtn.addEventListener('click', downloadAudio); - - // 音频播放结束事件 - audioPlayer.addEventListener('ended', onAudioEnded); -} - -// 更新字符计数 -function updateCharCount() { - const count = textInput.value.length; - charCount.textContent = count; -} - -// 生成语音 -async function generateSpeech() { - // 验证输入 - if (!validateInput()) { - return; - } - - // 保存配置 - saveConfig(); - - // 更新状态 - setStatus('loading', '正在生成语音...'); - disableControls(); - - try { - // 构建请求数据 - const requestData = { - api_key: apiKeyInput.value, - text: textInput.value, - voice: voiceSelect.value, - model: modelSelect.value, - api_base: apiBaseInput.value - }; - - // 发送请求到后端API - const response = await fetch('/api/tts', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestData) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({ - error: `HTTP错误: ${response.status} ${response.statusText}` - })); - throw new Error(errorData.error || '生成语音失败'); - } - - // 获取音频Blob - const audioBlob = await response.blob(); - - // 创建音频URL - if (currentAudioURL) { - URL.revokeObjectURL(currentAudioURL); - } - currentAudioURL = URL.createObjectURL(audioBlob); - - // 更新音频播放器 - audioPlayer.src = currentAudioURL; - - // 更新状态 - setStatus('success', '语音生成成功!'); - enableControls(); - - } catch (error) { - // 处理错误 - setStatus('error', `生成失败: ${error.message}`); - disableControls(); - } -} - -// 验证输入 -function validateInput() { - if (!apiKeyInput.value.trim()) { - setStatus('error', '请输入API Key'); - apiKeyInput.focus(); - return false; - } - - if (!textInput.value.trim()) { - setStatus('error', '请输入要转换的文本'); - textInput.focus(); - return false; - } - - return true; -} - -// 切换播放状态 -function togglePlay() { - if (audioPlayer.paused) { - audioPlayer.play(); - playBtn.textContent = '暂停'; - } else { - audioPlayer.pause(); - playBtn.textContent = '播放'; - } -} - -// 音频播放结束 -function onAudioEnded() { - playBtn.textContent = '播放'; -} - -// 下载音频 -function downloadAudio() { - if (!currentAudioURL) { - return; - } - - // 创建下载链接 - const link = document.createElement('a'); - link.href = currentAudioURL; - link.download = 'speech.mp3'; - - // 触发下载 - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} - -// 设置状态 -function setStatus(type, message) { - statusDiv.className = `status ${type}`; - statusDiv.textContent = message; - - // 清除成功状态 - if (type === 'success') { - setTimeout(() => { - statusDiv.className = 'status'; - statusDiv.textContent = ''; - }, 3000); - } -} - -// 禁用控件 -function disableControls() { - generateBtn.disabled = true; - playBtn.disabled = true; - downloadBtn.disabled = true; - generateBtn.classList.add('loading'); -} - -// 启用控件 -function enableControls() { - generateBtn.disabled = false; - playBtn.disabled = false; - downloadBtn.disabled = false; - generateBtn.classList.remove('loading'); -} - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', init); diff --git a/config.example.json b/config.example.json deleted file mode 100644 index a03bb7a..0000000 --- a/config.example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "api_base_url": "https://api.openai.com", - "port": "8080" -} diff --git a/index.html b/index.html deleted file mode 100644 index aae6241..0000000 --- a/index.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - OpenAITTS配音台 - - - -
-
-

OpenAITTS配音台

-

简洁高效的文本转语音工具

-
- -
- -
-

API配置

-
-
- - -
-
- - -
-
-
- - -
-

输入文本

-
- -
- 0 字符 -
-
-
- - -
-

语音参数

-
-
- - -
-
- - -
-
-
- - -
-
- - - -
-
-
- - -
-

音频播放

-
- -
-
-
- - -
- - - - diff --git a/main.go b/main.go deleted file mode 100644 index 3117508..0000000 --- a/main.go +++ /dev/null @@ -1,149 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "io" - "log" - "net/http" - "os" - "strings" -) - -// 配置结构体 -type Config struct { - APIBaseURL string `json:"api_base_url"` - Port string `json:"port"` -} - -// 从配置文件加载配置 -func loadConfig() Config { - config := Config{ - APIBaseURL: "https://api.openai.com", // 默认API地址 - Port: "8080", // 默认端口 - } - - // 尝试读取配置文件 - if _, err := os.Stat("config.json"); err == nil { - data, err := os.ReadFile("config.json") - if err == nil { - json.Unmarshal(data, &config) - } - } - - // 环境变量覆盖配置文件 - if envURL := os.Getenv("OPENAI_API_BASE_URL"); envURL != "" { - config.APIBaseURL = envURL - } - if envPort := os.Getenv("PORT"); envPort != "" { - config.Port = envPort - } - - return config -} - -// TTS请求结构体 -type TTSRequest struct { - APIKey string `json:"api_key"` - Text string `json:"text"` - Voice string `json:"voice"` - Model string `json:"model"` - APIBase string `json:"api_base"` -} - -// OpenAI TTS API请求体 -type openAITTSRequest struct { - Model string `json:"model"` - Input string `json:"input"` - Voice string `json:"voice"` -} - -func main() { - // 命令行参数 - var configPath string - flag.StringVar(&configPath, "config", "config.json", "配置文件路径") - flag.Parse() - - config := loadConfig() - - // 静态文件服务 - http.Handle("/", http.FileServer(http.Dir("."))) - - // TTS API端点 - http.HandleFunc("/api/tts", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 解析请求体 - var req TTSRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - // 使用用户提供的API地址,如果没有则使用配置中的 - apiBaseURL := config.APIBaseURL - if req.APIBase != "" { - apiBaseURL = req.APIBase - } - - // 构建OpenAI TTS API请求 - openAIReq := openAITTSRequest{ - Model: req.Model, - Input: req.Text, - Voice: req.Voice, - } - - reqBody, err := json.Marshal(openAIReq) - if err != nil { - http.Error(w, "Failed to marshal request", http.StatusInternalServerError) - return - } - - // 创建HTTP客户端 - client := &http.Client{} - openAIReq, err := http.NewRequest("POST", apiBaseURL+"/v1/audio/speech", bytes.NewBuffer(reqBody)) - if err != nil { - http.Error(w, "Failed to create request", http.StatusInternalServerError) - return - } - - // 设置请求头 - openAIReq.Header.Set("Content-Type", "application/json") - openAIReq.Header.Set("Authorization", "Bearer "+req.APIKey) - - // 发送请求 - resp, err := client.Do(openAIReq) - if err != nil { - http.Error(w, "Failed to call OpenAI API: "+err.Error(), http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - errBody, _ := io.ReadAll(resp.Body) - http.Error(w, "OpenAI API error: "+string(errBody), resp.StatusCode) - return - } - - // 设置响应头 - w.Header().Set("Content-Type", "audio/mpeg") - w.Header().Set("Content-Disposition", "attachment; filename=speech.mp3") - - // 将响应体写入客户端 - io.Copy(w, resp.Body) - }) - - log.Printf("Server starting on port %s...", config.Port) - log.Printf("Using API base URL: %s", config.APIBaseURL) - log.Printf("Open http://localhost:%s in your browser", config.Port) - - // 启动服务器 - if err := http.ListenAndServe(":"+config.Port, nil); err != nil { - log.Fatal("Server failed to start: ", err) - } -} diff --git a/styles.css b/styles.css deleted file mode 100644 index a59b4f8..0000000 --- a/styles.css +++ /dev/null @@ -1,344 +0,0 @@ -/* 基础样式重置 */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* 全局样式 */ -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - line-height: 1.6; - color: #333; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; -} - -/* 容器样式 */ -.container { - max-width: 800px; - margin: 0 auto; - padding: 20px; - background: rgba(255, 255, 255, 0.95); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - margin-top: 20px; - margin-bottom: 20px; -} - -/* 头部样式 */ -header { - text-align: center; - margin-bottom: 30px; - padding-bottom: 20px; - border-bottom: 2px solid #e0e0e0; -} - -header h1 { - color: #2d3748; - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 10px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - color: #718096; - font-size: 1.1rem; - font-weight: 400; -} - -/* 主要内容区域 */ -main { - display: flex; - flex-direction: column; - gap: 25px; -} - -/* 各部分通用样式 */ -section { - background: #f8fafc; - padding: 25px; - border-radius: 12px; - border: 1px solid #e2e8f0; - transition: all 0.3s ease; -} - -section:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); -} - -section h2 { - color: #2d3748; - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 20px; - display: flex; - align-items: center; - gap: 10px; -} - -section h2::before { - content: ''; - width: 4px; - height: 24px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 2px; -} - -/* 配置网格布局 */ -.config-grid, -.params-grid, -.controls-grid { - display: grid; - grid-template-columns: 1fr; - gap: 20px; -} - -@media (min-width: 600px) { - .config-grid, - .params-grid, - .controls-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -/* 表单组样式 */ -.form-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.form-group label { - font-weight: 500; - color: #4a5568; - font-size: 0.95rem; -} - -/* 表单输入样式 */ -.form-input, -.form-select { - padding: 12px 16px; - border: 2px solid #e2e8f0; - border-radius: 8px; - font-size: 1rem; - transition: all 0.3s ease; - background: white; - width: 100%; -} - -.form-input:focus, -.form-select:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -.form-input::placeholder { - color: #a0aec0; -} - -/* 文本输入区域 */ -.text-input-container { - display: flex; - flex-direction: column; - gap: 10px; -} - -#textInput { - padding: 16px; - border: 2px solid #e2e8f0; - border-radius: 8px; - font-size: 1rem; - font-family: inherit; - resize: vertical; - min-height: 150px; - transition: all 0.3s ease; - background: white; -} - -#textInput:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -.text-info { - text-align: right; - color: #718096; - font-size: 0.9rem; -} - -/* 按钮样式 */ -.btn { - padding: 12px 24px; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - text-decoration: none; -} - -.btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; -} - -.btn-primary:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); -} - -.btn-secondary { - background: #e2e8f0; - color: #4a5568; -} - -.btn-secondary:hover:not(:disabled) { - background: #cbd5e0; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none; -} - -/* 控制区域 */ -.controls-section { - display: flex; - flex-direction: column; - gap: 15px; -} - -.status { - padding: 12px 16px; - border-radius: 8px; - font-size: 0.95rem; - font-weight: 500; - text-align: center; - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; -} - -.status.success { - background: #c6f6d5; - color: #22543d; - border: 1px solid #9ae6b4; -} - -.status.error { - background: #fed7d7; - color: #742a2a; - border: 1px solid #feb2b2; -} - -.status.loading { - background: #ebf8ff; - color: #2a4365; - border: 1px solid #bee3f8; -} - -/* 音频播放区域 */ -.audio-player-container { - display: flex; - justify-content: center; - padding: 20px 0; -} - -.audio-player { - width: 100%; - max-width: 100%; - height: 50px; - background: white; - border-radius: 25px; - border: 2px solid #e2e8f0; - transition: all 0.3s ease; -} - -.audio-player:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -/* 页脚样式 */ -footer { - text-align: center; - margin-top: 30px; - padding-top: 20px; - border-top: 2px solid #e0e0e0; - color: #718096; - font-size: 0.9rem; -} - -/* 响应式设计 */ -@media (max-width: 600px) { - .container { - margin: 10px; - padding: 15px; - } - - header h1 { - font-size: 2rem; - } - - section { - padding: 20px; - } - - .btn { - padding: 10px 20px; - font-size: 0.95rem; - } -} - -/* 动画效果 */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -section { - animation: fadeIn 0.5s ease-out; -} - -/* 加载动画 */ -.loading::after { - content: ''; - display: inline-block; - width: 16px; - height: 16px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - border-top-color: #fff; - animation: spin 1s ease-in-out infinite; - margin-left: 10px; -} - -@keyframes spin { - to { transform: rotate(360deg); } -}