diff --git a/api/handler/admin/chat_model_handler.go b/api/handler/admin/chat_model_handler.go index 65b6649f..2c92089b 100644 --- a/api/handler/admin/chat_model_handler.go +++ b/api/handler/admin/chat_model_handler.go @@ -30,20 +30,21 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { func (h *ChatModelHandler) Save(c *gin.Context) { var data struct { - Id uint `json:"id"` - Name string `json:"name"` - Value string `json:"value"` - Enabled bool `json:"enabled"` - SortNum int `json:"sort_num"` - Open bool `json:"open"` - Platform string `json:"platform"` - Power int `json:"power"` - MaxTokens int `json:"max_tokens"` // 最大响应长度 - MaxContext int `json:"max_context"` // 最大上下文长度 - Temperature float32 `json:"temperature"` // 模型温度 - KeyId int `json:"key_id,omitempty"` - CreatedAt int64 `json:"created_at"` - Type string `json:"type"` + Id uint `json:"id"` + Name string `json:"name"` + Value string `json:"value"` + Enabled bool `json:"enabled"` + SortNum int `json:"sort_num"` + Open bool `json:"open"` + Platform string `json:"platform"` + Power int `json:"power"` + MaxTokens int `json:"max_tokens"` // 最大响应长度 + MaxContext int `json:"max_context"` // 最大上下文长度 + Temperature float32 `json:"temperature"` // 模型温度 + KeyId int `json:"key_id,omitempty"` + CreatedAt int64 `json:"created_at"` + Type string `json:"type"` + Options map[string]string `json:"options"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -59,7 +60,6 @@ func (h *ChatModelHandler) Save(c *gin.Context) { item.Name = data.Name item.Value = data.Value item.Enabled = data.Enabled - item.SortNum = data.SortNum item.Open = data.Open item.Power = data.Power item.MaxTokens = data.MaxTokens @@ -67,6 +67,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) { item.Temperature = data.Temperature item.KeyId = data.KeyId item.Type = data.Type + item.Options = utils.JsonEncode(data.Options) var res *gorm.DB if data.Id > 0 { res = h.DB.Save(&item) diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go index 8b171182..678dd182 100644 --- a/api/handler/chat_handler.go +++ b/api/handler/chat_handler.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "net/url" + "os" "strings" "time" "unicode/utf8" @@ -505,47 +506,90 @@ func (h *ChatHandler) saveChatHistory( // 文本生成语音 func (h *ChatHandler) TextToSpeech(c *gin.Context) { var data struct { - Text string `json:"text"` + ModelId int `json:"model_id"` + Text string `json:"text"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } - // 调用 DeepSeek 的 API 接口 - var apiKey model.ApiKey - h.DB.Where("type", "chat").Where("enabled", true).First(&apiKey) - if apiKey.Id == 0 { - resp.ERROR(c, "no available key, please import key") + textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text)) + audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir) + if _, err := os.Stat(audioFile); err != nil { + os.MkdirAll(audioFile, 0755) + } + audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash) + if _, err := os.Stat(audioFile); err == nil { + // 设置响应头 + c.Header("Content-Type", "audio/mpeg") + c.Header("Content-Disposition", "attachment; filename=speech.mp3") + c.File(audioFile) return } + // 查询模型 + var chatModel model.ChatModel + err := h.DB.Where("id", data.ModelId).First(&chatModel).Error + if err != nil { + resp.ERROR(c, "找不到语音模型") + return + } + + // 调用 DeepSeek 的 API 接口 + var apiKey model.ApiKey + if chatModel.KeyId > 0 { + h.DB.Where("id", chatModel.KeyId).First(&apiKey) + } + if apiKey.Id == 0 { + h.DB.Where("type", "tts").Where("enabled", true).First(&apiKey) + } + if apiKey.Id == 0 { + resp.ERROR(c, "no TTS API key, please import key") + return + } + + logger.Debugf("chatModel: %+v, apiKey: %+v", chatModel, apiKey) + // 调用 openai tts api config := openai.DefaultConfig(apiKey.Value) - config.BaseURL = apiKey.ApiURL + config.BaseURL = apiKey.ApiURL + "/v1" client := openai.NewClientWithConfig(config) + voice := openai.VoiceAlloy + var options map[string]string + err = utils.JsonDecode(chatModel.Options, &options) + if err == nil { + voice = openai.SpeechVoice(options["voice"]) + } req := openai.CreateSpeechRequest{ - Model: openai.TTSModel1, + Model: openai.SpeechModel(chatModel.Value), Input: data.Text, - Voice: openai.VoiceAlloy, + Voice: voice, } audioData, err := client.CreateSpeech(context.Background(), req) if err != nil { - logger.Error("failed to create speech: ", err) - resp.ERROR(c, "failed to create speech") + resp.ERROR(c, err.Error()) return } + // 先将音频数据读取到内存 + audioBytes, err := io.ReadAll(audioData) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 保存到音频文件 + err = os.WriteFile(audioFile, audioBytes, 0644) + if err != nil { + logger.Error("failed to save audio file: ", err) + } + // 设置响应头 c.Header("Content-Type", "audio/mpeg") c.Header("Content-Disposition", "attachment; filename=speech.mp3") - // 将音频数据写入响应 - _, err = io.Copy(c.Writer, audioData) - if err != nil { - logger.Error("failed to write audio data: ", err) - resp.ERROR(c, "failed to write audio data") - return - } + // 直接写入完整的音频数据到响应 + c.Writer.Write(audioBytes) } diff --git a/api/handler/chat_model_handler.go b/api/handler/chat_model_handler.go index 18ef5277..e7aef862 100644 --- a/api/handler/chat_model_handler.go +++ b/api/handler/chat_model_handler.go @@ -30,14 +30,17 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { func (h *ChatModelHandler) List(c *gin.Context) { var items []model.ChatModel var chatModels = make([]vo.ChatModel, 0) - session := h.DB.Session(&gorm.Session{}).Where("type", "chat").Where("enabled", true) + session := h.DB.Session(&gorm.Session{}).Where("enabled", true) t := c.Query("type") + logger.Info("type: ", t) if t != "" { session = session.Where("type", t) + } else { + session = session.Where("type", "chat") } session = session.Where("open", true) - if h.IsLogin(c) { + if h.IsLogin(c) && t == "chat" { user, _ := h.GetLoginUser(c) var models []int err := utils.JsonDecode(user.ChatModels, &models) @@ -48,7 +51,7 @@ func (h *ChatModelHandler) List(c *gin.Context) { } - res := session.Order("sort_num ASC").Find(&items) + res := session.Debug().Order("sort_num ASC").Find(&items) if res.Error == nil { for _, item := range items { var cm vo.ChatModel diff --git a/api/static/audio/2a22a7d227c444f7b69710bd9327acee43457b460cf362331a06474a1b9cc82e.mp3 b/api/static/audio/2a22a7d227c444f7b69710bd9327acee43457b460cf362331a06474a1b9cc82e.mp3 new file mode 100644 index 00000000..22beda80 Binary files /dev/null and b/api/static/audio/2a22a7d227c444f7b69710bd9327acee43457b460cf362331a06474a1b9cc82e.mp3 differ diff --git a/api/static/audio/5b17e861d45e57d6bb49a7268115e20b07d35c527d6db45b911707a4d985251e.mp3 b/api/static/audio/5b17e861d45e57d6bb49a7268115e20b07d35c527d6db45b911707a4d985251e.mp3 new file mode 100644 index 00000000..9982d073 Binary files /dev/null and b/api/static/audio/5b17e861d45e57d6bb49a7268115e20b07d35c527d6db45b911707a4d985251e.mp3 differ diff --git a/api/static/audio/66f89a741101a0a523ca6c6eb596c3ded47c723be27ba6acba076449fd4203ed.mp3 b/api/static/audio/66f89a741101a0a523ca6c6eb596c3ded47c723be27ba6acba076449fd4203ed.mp3 new file mode 100644 index 00000000..a133079b Binary files /dev/null and b/api/static/audio/66f89a741101a0a523ca6c6eb596c3ded47c723be27ba6acba076449fd4203ed.mp3 differ diff --git a/api/static/audio/b2ffb7b1f51c48cb5744554ece5be09049b05f2ac8f23a7c25e1d2ddb337ea07.mp3 b/api/static/audio/b2ffb7b1f51c48cb5744554ece5be09049b05f2ac8f23a7c25e1d2ddb337ea07.mp3 new file mode 100644 index 00000000..737c8676 Binary files /dev/null and b/api/static/audio/b2ffb7b1f51c48cb5744554ece5be09049b05f2ac8f23a7c25e1d2ddb337ea07.mp3 differ diff --git a/api/static/audio/c423597a2cb4ca7faa2962c10c547a8f07c200f8282229c1506c96e922504e91.mp3 b/api/static/audio/c423597a2cb4ca7faa2962c10c547a8f07c200f8282229c1506c96e922504e91.mp3 new file mode 100644 index 00000000..40f8e0a7 Binary files /dev/null and b/api/static/audio/c423597a2cb4ca7faa2962c10c547a8f07c200f8282229c1506c96e922504e91.mp3 differ diff --git a/api/static/audio/ea3a312e1b03d3833aa631af8b4b5c74e24e8ee5b38f0ae946e73e9125a905a0.mp3 b/api/static/audio/ea3a312e1b03d3833aa631af8b4b5c74e24e8ee5b38f0ae946e73e9125a905a0.mp3 new file mode 100644 index 00000000..eccc7c5c Binary files /dev/null and b/api/static/audio/ea3a312e1b03d3833aa631af8b4b5c74e24e8ee5b38f0ae946e73e9125a905a0.mp3 differ diff --git a/api/static/audio/eee981117cf345a51f08049d6976b46c04442045c785fea6ede43e8c5fde0733.mp3 b/api/static/audio/eee981117cf345a51f08049d6976b46c04442045c785fea6ede43e8c5fde0733.mp3 new file mode 100644 index 00000000..9bbd9878 Binary files /dev/null and b/api/static/audio/eee981117cf345a51f08049d6976b46c04442045c785fea6ede43e8c5fde0733.mp3 differ diff --git a/api/store/model/chat_model.go b/api/store/model/chat_model.go index 1505e7f3..20c8c87b 100644 --- a/api/store/model/chat_model.go +++ b/api/store/model/chat_model.go @@ -13,4 +13,5 @@ type ChatModel struct { Temperature float32 // 模型温度 KeyId int // 绑定 API KEY ID Type string // 模型类型 + Options string // 模型选项 } diff --git a/api/store/vo/chat_model.go b/api/store/vo/chat_model.go index 4e4f0f04..50196263 100644 --- a/api/store/vo/chat_model.go +++ b/api/store/vo/chat_model.go @@ -2,16 +2,17 @@ package vo type ChatModel struct { BaseVo - Name string `json:"name"` - Value string `json:"value"` - Enabled bool `json:"enabled"` - SortNum int `json:"sort_num"` - Power int `json:"power"` - Open bool `json:"open"` - MaxTokens int `json:"max_tokens"` // 最大响应长度 - MaxContext int `json:"max_context"` // 最大上下文长度 - Temperature float32 `json:"temperature"` // 模型温度 - KeyId int `json:"key_id,omitempty"` - KeyName string `json:"key_name"` - Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + Enabled bool `json:"enabled"` + SortNum int `json:"sort_num"` + Power int `json:"power"` + Open bool `json:"open"` + MaxTokens int `json:"max_tokens"` // 最大响应长度 + MaxContext int `json:"max_context"` // 最大上下文长度 + Temperature float32 `json:"temperature"` // 模型温度 + KeyId int `json:"key_id,omitempty"` + KeyName string `json:"key_name"` + Options map[string]string `json:"options"` + Type string `json:"type"` } diff --git a/database/update-v4.2.2.sql b/database/update-v4.2.2.sql new file mode 100644 index 00000000..b9600634 --- /dev/null +++ b/database/update-v4.2.2.sql @@ -0,0 +1 @@ +ALTER TABLE `chatgpt_chat_models` ADD `options` TEXT NOT NULL COMMENT '模型自定义选项' AFTER `key_id`; \ No newline at end of file diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue index b9aeaa5d..7c1ad7af 100644 --- a/web/src/components/ChatPrompt.vue +++ b/web/src/components/ChatPrompt.vue @@ -198,7 +198,7 @@ const isExternalImg = (link, files) => { width 100% padding-bottom: 1.5rem; padding-top: 1.5rem; - border-bottom: 0.5px solid var(--el-border-color); + // border-bottom: 0.5px solid var(--el-border-color); .chat-line-inner { display flex; diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index 7922cf62..35ffb43f 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -1,4 +1,5 @@