feat: add reader endpoint and update TTS link generation with display name
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 添加通知函数
|
||||
|
||||
Reference in New Issue
Block a user