diff --git a/internal/http/handlers/tts.go b/internal/http/handlers/tts.go index dd7c1d6..8df234a 100644 --- a/internal/http/handlers/tts.go +++ b/internal/http/handlers/tts.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "fmt" "log" "net/http" @@ -19,6 +20,8 @@ import ( "github.com/gin-gonic/gin" ) +var cfg = config.Get() + // truncateForLog 截断文本用于日志显示,同时显示开头和结尾 func truncateForLog(text string, maxLength int) string { // 先去除换行符 @@ -450,6 +453,57 @@ func (h *TTSHandler) handleSegmentedTTS(c *gin.Context, req models.TTSRequest) { totalTime, splitTime, synthesisTime, writeTime, formatFileSize(len(audioData))) } +// HandleReader 返回 reader 可导入的格式 +func (h *TTSHandler) HandleReader(context *gin.Context) { + // 从URL参数获取 + req := models.TTSRequest{ + Text: context.Query("t"), + Voice: context.Query("v"), + Rate: context.Query("r"), + Pitch: context.Query("p"), + Style: context.Query("s"), + } + displayName := context.Query("n") + + baseUrl := utils.GetBaseURL(context) + basePath, err := utils.JoinURL(baseUrl, cfg.Server.BasePath) + if err != nil { + context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 构建基本URL + urlParams := []string{"t={{java.encodeURI(speakText)}}", "r={{speakSpeed*4}}"} + + // 只有有值的参数才添加 + if req.Voice != "" { + urlParams = append(urlParams, fmt.Sprintf("v=%s", req.Voice)) + } + + if req.Pitch != "" { + urlParams = append(urlParams, fmt.Sprintf("p=%s", req.Pitch)) + } + + if req.Style != "" { + urlParams = append(urlParams, fmt.Sprintf("s=%s", req.Style)) + } + + if cfg.TTS.ApiKey != "" { + urlParams = append(urlParams, fmt.Sprintf("api_key=%s", cfg.TTS.ApiKey)) + } + + url := fmt.Sprintf("%s/tts?%s", basePath, strings.Join(urlParams, "&")) + + encoder := json.NewEncoder(context.Writer) + encoder.SetEscapeHTML(false) + context.Status(http.StatusOK) + encoder.Encode(models.ReaderResponse{ + Id: time.Now().Unix(), + Name: displayName, + Url: url, + }) +} + // splitTextBySentences 将文本按句子分割 func splitTextBySentences(text string) []string { // 如果文本过短,直接作为一个句子返回 @@ -457,9 +511,8 @@ func splitTextBySentences(text string) []string { return []string{text} } - cfg := config.Get().TTS - maxLen := cfg.MaxSentenceLength - minLen := cfg.MinSentenceLength + maxLen := cfg.TTS.MaxSentenceLength + minLen := cfg.TTS.MinSentenceLength // 第一次分割:按标点和长度限制分割 sentences := utils.SplitAndFilterEmptyLines(text) diff --git a/internal/http/routes/routes.go b/internal/http/routes/routes.go index 98cde2c..301a328 100644 --- a/internal/http/routes/routes.go +++ b/internal/http/routes/routes.go @@ -50,6 +50,7 @@ func SetupRoutes(cfg *config.Config, ttsService tts.Service) (*gin.Engine, error baseRouter.POST("/tts", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleTTS) baseRouter.GET("/tts", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleTTS) + baseRouter.GET("/reader.json", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleReader) // 设置语音列表API路由 baseRouter.GET("/voices", voicesHandler.HandleVoices) diff --git a/internal/models/tts.go b/internal/models/tts.go index 7b42496..6b5b811 100644 --- a/internal/models/tts.go +++ b/internal/models/tts.go @@ -23,3 +23,10 @@ type OpenAIRequest struct { Voice string `json:"voice"` Speed float64 `json:"speed"` } + +// ReaderResponse reader 响应结构体 +type ReaderResponse struct { + Id int64 `json:"id"` + Name string `json:"name"` + Url string `json:"url"` +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 7979612..0c77899 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/gin-gonic/gin" "net/http" "net/url" "strings" @@ -157,3 +158,28 @@ func MergeStringsWithLimit(strs []string, minLen int, maxLen int) []string { return result } + +// GetBaseURL 返回基础 URL,包括方案和主机,但不包括路径和查询参数 +func GetBaseURL(c *gin.Context) string { + scheme := "http" + if c.Request.TLS != nil || c.Request.Header.Get("X-Forwarded-Proto") == "https" { + scheme = "https" + } + + return fmt.Sprintf("%s://%s", scheme, c.Request.Host) +} + +// JoinURL 安全地拼接基础 URL 和相对路径 +func JoinURL(baseURL, relativePath string) (string, error) { + base, err := url.Parse(baseURL) + if err != nil { + return "", err + } + + rel, err := url.Parse(relativePath) + if err != nil { + return "", err + } + + return base.ResolveReference(rel).String(), nil +} diff --git a/web/static/js/app.js b/web/static/js/app.js index af1868d..d3c1dfa 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -211,13 +211,14 @@ document.addEventListener('DOMContentLoaded', function () { copyHttpTtsLinkButton.addEventListener('click', function () { const text = "{{java.encodeURI(speakText)}}"; const voice = voiceSelect.value; + const displayName = voiceSelect.options[voiceSelect.selectedIndex].text; const style = styleSelect.value; const rate = "{{speakSpeed*4}}" const pitch = pitchInput.value; const apiKey = apiKeyInput.value.trim(); // 构建HttpTTS链接 - let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t=${text}&v=${voice}&r=${rate}&p=${pitch}`; + let httpTtsLink = `${window.location.origin}${config.basePath}/reader.json?&v=${voice}&r=${rate}&p=${pitch}&n=${displayName}`; // 只有当style不为空时才添加 if (style) { @@ -229,7 +230,7 @@ document.addEventListener('DOMContentLoaded', function () { httpTtsLink += `&api_key=${apiKey}`; } - copyToClipboard(httpTtsLink); + window.open(httpTtsLink, '_blank') }); // 显示/隐藏API Key区域的按钮事件 @@ -421,8 +422,10 @@ document.addEventListener('DOMContentLoaded', function () { // 复制内容到剪贴板的通用函数 function copyToClipboard(text) { + let success = false; navigator.clipboard.writeText(text).then(() => { showCustomAlert('链接已复制到剪贴板', 'success'); + success = true; }).catch(err => { console.error('复制失败:', err); // 兼容处理 @@ -435,12 +438,16 @@ document.addEventListener('DOMContentLoaded', function () { try { document.execCommand('copy'); showCustomAlert('链接已复制到剪贴板', 'success'); + success = true; } catch (err) { console.error('复制失败:', err); + shwoCustomAlert('复制失败', 'error'); + success = false; } document.body.removeChild(textArea); }); + return success; } // 添加通知函数