From ebd2f6fabb1562072856fcb06921a5c7ccedbb6a Mon Sep 17 00:00:00 2001 From: Song367 <601337784@qq.com> Date: Thu, 19 Jun 2025 15:20:38 +0800 Subject: [PATCH] local --- .env | 4 + main.go | 2 + service/llm_service.go | 280 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 282 insertions(+), 4 deletions(-) diff --git a/.env b/.env index b7e08ee..1ed5a3b 100644 --- a/.env +++ b/.env @@ -1,6 +1,10 @@ # LLM API Configuration LLM_API_URL=http://tianchat.zenithsafe.com:5001/v1 LLM_API_KEY=app-WVRIlrX75YnR3GsAKLlxiOEa + +LLMOurApiUrl=https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions +LLMOurApiKey=e999a241-6bf3-4ee0-99a8-e4de9b617f28 + MiniMaxApiKey=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLkuIrmtbfpopzpgJTnp5HmioDmnInpmZDlhazlj7giLCJVc2VyTmFtZSI6IuadqOmqpSIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxNzI4NzEyMzI0OTc5NjI2ODM5IiwiUGhvbmUiOiIxMzM4MTU1OTYxOCIsIkdyb3VwSUQiOiIxNzI4NzEyMzI0OTcxMjM4MjMxIiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDYtMTYgMTY6Mjk6NTkiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.D_JF0-nO89NdMZCYq4ocEyqxtZ9SeEdtMvbeSkZTWspt0XfX2QpPAVh-DI3MCPZTeSmjNWLf4fA_Th2zpVrj4UxWMbGKBeLZWLulNpwAHGMUTdqenuih3daCDPCzs0duhlFyQnZgGcEOGQ476HL72N2klujP8BUy_vfAh_Zv0po-aujQa5RxardDSOsbs49NTPEw0SQEXwaJ5bVmiZ5s-ysJ9pZWSEiyJ6SX9z3JeZHKj9DxHdOw5roZR8izo54e4IoqyLlzEfhOMW7P15-ffDH3M6HGiEmeBaGRYGAIciELjZS19ONNMKsTj-wXNGWtKG-sjAB1uuqkkT5Ul9Dunw MiniMaxApiURL=https://api.minimaxi.com/v1/t2a_v2 APP_ID=1364994890450210816 diff --git a/main.go b/main.go index 8fd868d..fce96da 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,8 @@ func main() { MiniMaxApiKey: os.Getenv("MiniMaxApiKey"), MiniMaxApiURL: os.Getenv("MiniMaxApiURL"), FILE_URL: os.Getenv("FILE_URL"), + LLMOurApiUrl: os.Getenv("LLMOurApiUrl"), + LLMOurApiKey: os.Getenv("LLMOurApiKey"), }) fmt.Println("config: ", llmService) diff --git a/service/llm_service.go b/service/llm_service.go index d689b15..d8a5119 100644 --- a/service/llm_service.go +++ b/service/llm_service.go @@ -24,6 +24,8 @@ type Config struct { MiniMaxApiKey string MiniMaxApiURL string FILE_URL string + LLMOurApiUrl string + LLMOurApiKey string } // LLMService handles communication with the LLM API @@ -51,6 +53,7 @@ type RequestPayload struct { ConversationID string `json:"conversation_id"` Files []interface{} `json:"files"` Audio string `json:"audio"` + LlmType string `json:"llm_type"` } // VoiceSetting represents voice configuration @@ -112,6 +115,18 @@ type SpeechResponse struct { BaseResp BaseResponse `json:"base_resp"` } +type LLMOurMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type LLMOurRequestPayload struct { + Model string `json:"model"` + Stream bool `json:"stream"` + StreamOptions map[string]interface{} `json:"stream_options"` + Messages []LLMOurMessage `json:"messages"` +} + // NewLLMService creates a new instance of LLMService func NewLLMService(config Config) *LLMService { return &LLMService{ @@ -130,6 +145,7 @@ func (s *LLMService) CallLLMAPI(data map[string]interface{}) (interface{}, error ConversationID: getString(data, "conversation_id"), Files: make([]interface{}, 0), Audio: getString(data, "audio"), + LlmType: getString(data, "llm_type"), } fmt.Printf("前端传来的数据:%+v\n", payload) @@ -137,13 +153,52 @@ func (s *LLMService) CallLLMAPI(data map[string]interface{}) (interface{}, error if err != nil { return nil, fmt.Errorf("error marshaling payload: %v", err) } - fmt.Println(s.config.LLMApiURL + "/chat-messages") - req, err := http.NewRequest("POST", s.config.LLMApiURL+"/chat-messages", bytes.NewBuffer(jsonData)) - // req, err := http.NewRequest("GET", "http://localhost:8080/stream-text", nil) + + currentUrl := s.config.LLMApiURL + "/chat-messages" + fmt.Println(currentUrl) + req := &http.Request{} + if payload.LlmType == "ours" { + // 动态构造 messages + var messages []LLMOurMessage + if msgs, ok := data["messages"]; ok { + if arr, ok := msgs.([]interface{}); ok { + for _, m := range arr { + if mMap, ok := m.(map[string]interface{}); ok { + role, _ := mMap["role"].(string) + content, _ := mMap["content"].(string) + messages = append(messages, LLMOurMessage{Role: role, Content: content}) + } + } + } + } + // fallback: 如果没有 messages,则用 query 作为 user 消息 + if len(messages) == 0 && payload.Query != "" { + messages = append(messages, LLMOurMessage{Role: "user", Content: payload.Query}) + } + ourPayload := LLMOurRequestPayload{ + Model: "bot-20250522162100-44785", // 可根据 data 或配置传入 + Stream: true, + StreamOptions: map[string]interface{}{"include_usage": true}, + Messages: messages, + } + jsonData, err = json.Marshal(ourPayload) + if err != nil { + return nil, fmt.Errorf("error marshaling ourPayload: %v", err) + } + currentUrl = s.config.LLMOurApiUrl + req, err = http.NewRequest("POST", currentUrl, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + req.Header.Set("Authorization", "Bearer "+s.config.LLMOurApiKey) + req.Header.Set("Content-Type", "application/json") + return s.handleStreamingResponseV2(req, data, payload.Audio) + } + + req, err = http.NewRequest("POST", currentUrl, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } - req.Header.Set("Authorization", "Bearer "+s.config.LLMApiKey) req.Header.Set("Content-Type", "application/json") @@ -155,6 +210,134 @@ func (s *LLMService) CallLLMAPI(data map[string]interface{}) (interface{}, error return s.handleNonStreamingResponse(req) } +// processStreamSegment 处理流式文本分段、语音合成等逻辑,返回 new_message、audio、是否需要发送 +func (s *LLMService) processStreamSegment(initialSessage *string, all_message *string, answer string, audio_type string) (string, string, bool) { + // 定义标点符号map + punctuations := map[string]bool{ + ",": true, ",": true, // 逗号 + ".": true, "。": true, // 句号 + "!": true, "!": true, // 感叹号 + "?": true, "?": true, // 问号 + ";": true, ";": true, // 分号 + ":": true, ":": true, // 冒号 + "、": true, + } + + // 删除字符串前后的标点符号 + trimPunctuation := func(s string) string { + if len(s) > 0 { + lastRune, size := utf8.DecodeLastRuneInString(s) + if punctuations[string(lastRune)] { + s = s[:len(s)-size] + } + } + return s + } + + // 判断字符串是否包含标点符号 + containsPunctuation := func(s string) bool { + for _, char := range s { + if punctuations[string(char)] { + return true + } + } + return false + } + + // 按标点符号分割文本 + splitByPunctuation := func(s string) []string { + var result []string + var current string + for _, char := range s { + if punctuations[string(char)] { + if current != "" { + result = append(result, current+string(char)) + current = "" + } + } else { + current += string(char) + } + } + if current != "" { + result = append(result, current) + } + return result + } + + *initialSessage += answer + *all_message += answer + new_message := "" + if containsPunctuation(*initialSessage) { + segments := splitByPunctuation(*initialSessage) + if len(segments) > 1 { + format_message := strings.Join(segments[:len(segments)-1], "") + if utf8.RuneCountInString(format_message) > 10 { + *initialSessage = segments[len(segments)-1] + new_message = strings.Join(segments[:len(segments)-1], "") + } else { + return "", "", false + } + } else { + if utf8.RuneCountInString(*initialSessage) > 10 { + new_message = *initialSessage + *initialSessage = "" + } else if utf8.RuneCountInString(*initialSessage) <= 10 && strings.HasSuffix(*initialSessage, "。") { + new_message = *initialSessage + *initialSessage = "" + } else { + return "", "", false + } + } + } + + if new_message == "" { + return "", "", false + } + s_msg := strings.TrimSpace(new_message) + new_message = trimPunctuation(s_msg) + + audio := "" + for i := 0; i < 1; i++ { + speechResp, err := s.SynthesizeSpeech(new_message, audio_type) + if err != nil { + fmt.Printf("Error synthesizing speech: %v\n", err) + break + } + audio = speechResp.Data.Audio + if audio != "" { + resp, err := http.Get(audio) + if err != nil { + fmt.Printf("Error downloading audio: %v\n", err) + } else { + defer resp.Body.Close() + audioBytes, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("Error reading audio data: %v\n", err) + } else { + originalPath := fmt.Sprintf("audio/original_%d.wav", time.Now().UnixNano()) + if err := os.WriteFile(originalPath, audioBytes, 0644); err != nil { + fmt.Printf("Error saving original audio: %v\n", err) + } + audioBase64 := base64.StdEncoding.EncodeToString(audioBytes) + trimmedAudio, err := s.TrimAudioSilence(audioBase64) + if err != nil { + fmt.Printf("Error trimming audio silence: %v\n", err) + } else { + audio_path := fmt.Sprintf("trimmed_%d.wav", time.Now().UnixNano()) + outputPath := "audio/" + audio_path + if err := s.SaveBase64AsWAV(trimmedAudio, outputPath); err != nil { + fmt.Printf("Error saving trimmed WAV file: %v\n", err) + } + audio = s.config.FILE_URL + audio_path + } + } + } + break + } + } + return new_message, audio, true +} + // handleStreamingResponse processes streaming responses func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]interface{}, audio_type string) (chan Message, error) { resp, err := s.client.Do(req) @@ -374,6 +557,95 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string] return messageChan, nil } +// handleStreamingResponseV2 适配新流式返回格式 +func (s *LLMService) handleStreamingResponseV2(req *http.Request, data map[string]interface{}, audio_type string) (chan Message, error) { + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + messageChan := make(chan Message, 100) + all_message := "" + initialSessage := "" + go func() { + defer resp.Body.Close() + defer close(messageChan) + reader := bufio.NewReader(resp.Body) + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + fmt.Printf("Error reading line: %v\n", err) + continue + } + + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // line = strings.TrimSpace(line) + if strings.HasPrefix(line, "data:") { + line = strings.TrimSpace(strings.TrimPrefix(line, "data:")) + } + + fmt.Println("line: ", line) + + if line == "[DONE]" { + messageChan <- Message{ + Answer: "", + IsEnd: true, + ConversationID: getString(data, "conversation_id"), + TaskID: getString(data, "task_id"), + } + return + } + + var jsonData map[string]interface{} + if err := json.Unmarshal([]byte(line), &jsonData); err != nil { + fmt.Printf("Error unmarshaling JSON: %v\n", err) + continue + } + choices, ok := jsonData["choices"].([]interface{}) + if !ok || len(choices) == 0 { + continue + } + choice, ok := choices[0].(map[string]interface{}) + if !ok { + continue + } + delta, ok := choice["delta"].(map[string]interface{}) + if !ok { + continue + } + content, _ := delta["content"].(string) + if content == "" { + continue + } + + new_message, audio, needSend := s.processStreamSegment(&initialSessage, &all_message, content, audio_type) + if !needSend { + continue + } + messageChan <- Message{ + Answer: new_message, + IsEnd: false, + ConversationID: getString(data, "conversation_id"), + TaskID: getString(data, "task_id"), + ClientID: getString(data, "conversation_id"), + AudioData: audio, + } + } + }() + return messageChan, nil +} + // handleNonStreamingResponse processes non-streaming responses func (s *LLMService) handleNonStreamingResponse(req *http.Request) (map[string]interface{}, error) { resp, err := s.client.Do(req)