feat: add reader endpoint and update TTS link generation with display name
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +20,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cfg = config.Get()
|
||||||
|
|
||||||
// truncateForLog 截断文本用于日志显示,同时显示开头和结尾
|
// truncateForLog 截断文本用于日志显示,同时显示开头和结尾
|
||||||
func truncateForLog(text string, maxLength int) string {
|
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)))
|
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 将文本按句子分割
|
// splitTextBySentences 将文本按句子分割
|
||||||
func splitTextBySentences(text string) []string {
|
func splitTextBySentences(text string) []string {
|
||||||
// 如果文本过短,直接作为一个句子返回
|
// 如果文本过短,直接作为一个句子返回
|
||||||
@@ -457,9 +511,8 @@ func splitTextBySentences(text string) []string {
|
|||||||
return []string{text}
|
return []string{text}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.Get().TTS
|
maxLen := cfg.TTS.MaxSentenceLength
|
||||||
maxLen := cfg.MaxSentenceLength
|
minLen := cfg.TTS.MinSentenceLength
|
||||||
minLen := cfg.MinSentenceLength
|
|
||||||
|
|
||||||
// 第一次分割:按标点和长度限制分割
|
// 第一次分割:按标点和长度限制分割
|
||||||
sentences := utils.SplitAndFilterEmptyLines(text)
|
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.POST("/tts", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleTTS)
|
||||||
baseRouter.GET("/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路由
|
// 设置语音列表API路由
|
||||||
baseRouter.GET("/voices", voicesHandler.HandleVoices)
|
baseRouter.GET("/voices", voicesHandler.HandleVoices)
|
||||||
|
|||||||
@@ -23,3 +23,10 @@ type OpenAIRequest struct {
|
|||||||
Voice string `json:"voice"`
|
Voice string `json:"voice"`
|
||||||
Speed float64 `json:"speed"`
|
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/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -157,3 +158,28 @@ func MergeStringsWithLimit(strs []string, minLen int, maxLen int) []string {
|
|||||||
|
|
||||||
return result
|
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 () {
|
copyHttpTtsLinkButton.addEventListener('click', function () {
|
||||||
const text = "{{java.encodeURI(speakText)}}";
|
const text = "{{java.encodeURI(speakText)}}";
|
||||||
const voice = voiceSelect.value;
|
const voice = voiceSelect.value;
|
||||||
|
const displayName = voiceSelect.options[voiceSelect.selectedIndex].text;
|
||||||
const style = styleSelect.value;
|
const style = styleSelect.value;
|
||||||
const rate = "{{speakSpeed*4}}"
|
const rate = "{{speakSpeed*4}}"
|
||||||
const pitch = pitchInput.value;
|
const pitch = pitchInput.value;
|
||||||
const apiKey = apiKeyInput.value.trim();
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
|
||||||
// 构建HttpTTS链接
|
// 构建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不为空时才添加
|
// 只有当style不为空时才添加
|
||||||
if (style) {
|
if (style) {
|
||||||
@@ -229,7 +230,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
httpTtsLink += `&api_key=${apiKey}`;
|
httpTtsLink += `&api_key=${apiKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard(httpTtsLink);
|
window.open(httpTtsLink, '_blank')
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示/隐藏API Key区域的按钮事件
|
// 显示/隐藏API Key区域的按钮事件
|
||||||
@@ -421,8 +422,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// 复制内容到剪贴板的通用函数
|
// 复制内容到剪贴板的通用函数
|
||||||
function copyToClipboard(text) {
|
function copyToClipboard(text) {
|
||||||
|
let success = false;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
showCustomAlert('链接已复制到剪贴板', 'success');
|
showCustomAlert('链接已复制到剪贴板', 'success');
|
||||||
|
success = true;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('复制失败:', err);
|
console.error('复制失败:', err);
|
||||||
// 兼容处理
|
// 兼容处理
|
||||||
@@ -435,12 +438,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
showCustomAlert('链接已复制到剪贴板', 'success');
|
showCustomAlert('链接已复制到剪贴板', 'success');
|
||||||
|
success = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err);
|
console.error('复制失败:', err);
|
||||||
|
shwoCustomAlert('复制失败', 'error');
|
||||||
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
});
|
});
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加通知函数
|
// 添加通知函数
|
||||||
|
|||||||
Reference in New Issue
Block a user