From afb919398595b67631d52d1bec6342dd584421d1 Mon Sep 17 00:00:00 2001 From: RockYang Date: Mon, 31 Mar 2025 18:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E6=92=AD=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/go.mod | 1 + api/go.sum | 2 + api/handler/chat_handler.go | 71 ++++++++++++++++++++----------- api/main.go | 1 + web/public/images/voice.gif | Bin 0 -> 2381 bytes web/src/components/ChatReply.vue | 25 ++++++++++- 6 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 web/public/images/voice.gif diff --git a/api/go.mod b/api/go.mod index 293fcbf9..e4f18276 100644 --- a/api/go.mod +++ b/api/go.mod @@ -50,6 +50,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a // indirect github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect + github.com/sashabaranov/go-openai v1.38.1 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/api/go.sum b/api/go.sum index 1de7ee25..38297cab 100644 --- a/api/go.sum +++ b/api/go.sum @@ -203,6 +203,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8= +github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go index f24fd25e..8b171182 100644 --- a/api/handler/chat_handler.go +++ b/api/handler/chat_handler.go @@ -22,15 +22,16 @@ import ( "geekai/utils" "geekai/utils/resp" "html/template" + "io" "net/http" "net/url" - "regexp" "strings" "time" "unicode/utf8" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" + "github.com/sashabaranov/go-openai" "gorm.io/gorm" ) @@ -501,28 +502,50 @@ func (h *ChatHandler) saveChatHistory( } } -// 将AI回复消息中生成的图片链接下载到本地 -func (h *ChatHandler) extractImgUrl(text string) string { - pattern := `!\[([^\]]*)]\(([^)]+)\)` - re := regexp.MustCompile(pattern) - matches := re.FindAllStringSubmatch(text, -1) - - // 下载图片并替换链接地址 - for _, match := range matches { - imageURL := match[2] - logger.Debug(imageURL) - // 对于相同地址的图片,已经被替换了,就不再重复下载了 - if !strings.Contains(text, imageURL) { - continue - } - - newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false) - if err != nil { - logger.Error("error with download image: ", err) - continue - } - - text = strings.ReplaceAll(text, imageURL, newImgURL) +// 文本生成语音 +func (h *ChatHandler) TextToSpeech(c *gin.Context) { + var data struct { + 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") + return + } + + // 调用 openai tts api + config := openai.DefaultConfig(apiKey.Value) + config.BaseURL = apiKey.ApiURL + client := openai.NewClientWithConfig(config) + req := openai.CreateSpeechRequest{ + Model: openai.TTSModel1, + Input: data.Text, + Voice: openai.VoiceAlloy, + } + + audioData, err := client.CreateSpeech(context.Background(), req) + if err != nil { + logger.Error("failed to create speech: ", err) + resp.ERROR(c, "failed to create speech") + return + } + + // 设置响应头 + 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 } - return text } diff --git a/api/main.go b/api/main.go index 60caf25c..0f67adb6 100644 --- a/api/main.go +++ b/api/main.go @@ -251,6 +251,7 @@ func main() { group.GET("clear", h.Clear) group.POST("tokens", h.Tokens) group.GET("stop", h.StopGenerate) + group.POST("tts", h.TextToSpeech) }), fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) { s.Engine.POST("/api/upload", h.Upload) diff --git a/web/public/images/voice.gif b/web/public/images/voice.gif new file mode 100644 index 0000000000000000000000000000000000000000..f6fb2e9e900e5394362ae2937119975078960188 GIT binary patch literal 2381 zcmZ?wbhEHbOkqf2`2L^4#l?k*iOJH^63BqCfm}X5K1oSQAeVuGfrEoXOG^tP0%RK- z8v~_)B0$o@!UD(uia-3#D;~3+Xa-p=6GyebhKN-IP1=djf;=>D>!$_cy3y9aD3Q#de(#w~1;3^w zhb#+OowX;+WOZ0i%gy>mN^PE|FMMWAq{8!a^G)0L*~}_>d1ATj^0+-x zg|82-XFmSTx;p>u?(NT?)$Oj%eZP7Cakl?fG1(ti9iOk}zsn}$^PKaAZu>3orhT1t zJu_|o&e*i?Gwvt0>DOD{OZ_?RdE~PAopGtZr@Rk5X20`ty8nW?pI_fUzkmP#{|syz z4;om+G9EN?s7VM7xnMn)%CK;KJO>BgGuw^_>*LuRS(hAD-?28XCR0--?vBMf#}h%S z&qH*kZajF9&CvL2&t&7^^Q?~IBDqt(_+EAk6b|v7P#SpMEKz)_?SyX*w_OUaU-6x? z-Qhk{BeQ_tWDADJ|0nzpK3B7>_{F7_{_kQUUp+o?+rRw(@hZt0KdveE)_U_TYuOCMbvyztl^^@O1=8G zb}Z^Q-WRT1>3mO2q)LyAV{wz3)D4v;eu#vF#b;TlwWn6C zxZCf>)|t}pwJGxc#E+_yF@5LX{kfyiG(Vl zZyGEecb_hqFzwRQ#AQ65$6q?ltD69a6~No&BhaItKV!oV`eQpnwW@AOg5)j?W*M*JQ_cxAAf4B zY%^irbEckK{BiR4P5))I{EnS6-;r?{TYSbd!58yfnZ-gL1(~Lzf+W#&ml^Nc$NOm#3_v2BC<+c;a z6O12e%&)U{x{^H6Uhm}G1{b@`1*sfzD;G==bJM(69paUt+8WWcA~okvXXw* + @@ -97,6 +98,8 @@ import hl from "highlight.js"; import emoji from "markdown-it-emoji"; import mathjaxPlugin from "markdown-it-mathjax3"; import MarkdownIt from "markdown-it"; +import { httpPost } from "@/utils/http"; +import RippleButton from "./ui/RippleButton.vue"; // eslint-disable-next-line no-undef,no-unused-vars const props = defineProps({ @@ -156,7 +159,26 @@ if (!props.data.icon) { const synthesis = (text) => { console.log(text); - ElMessage.info("语音合成功能暂不可用"); + // 生成语音 + httpPost("/api/chat/tts", { text }, { responseType: 'blob' }).then(response => { + // 创建 Blob 对象,明确指定 MIME 类型 + const blob = new Blob([response], { type: 'audio/wav' }); + // 创建 URL + const audioUrl = URL.createObjectURL(blob); + // 创建音频元素 + const audio = new Audio(audioUrl); + // 播放音频 + audio.play().then(() => { + // 播放完成后释放 URL + URL.revokeObjectURL(audioUrl); + }).catch(err => { + console.error('播放音频失败:', err); + ElMessage.error('音频播放失败,请检查浏览器是否支持该音频格式'); + }); + }).catch(err => { + console.error('语音合成请求失败:', err); + ElMessage.error('语音合成失败,请稍后重试'); + }); }; // 重新生成 @@ -168,6 +190,7 @@ const reGenerate = (prompt) => {