Compare commits

..

29 Commits
master ... prod

Author SHA1 Message Date
Song367
53159ca24c 修改api配置
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
2025-09-18 15:57:37 +08:00
Song367
fe13751624 增加未达到字数处理
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2025-08-18 19:15:59 +08:00
Song367
9aee63f624 添加15个字,末尾标点符号处理
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2025-08-14 18:27:27 +08:00
Song367
f299cf0145 英文冒号不设置为停顿符
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 55s
2025-07-08 17:31:35 +08:00
Song367
bbf7843a66 更改配置线路
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-07-08 15:44:57 +08:00
Song367
dee9dd7b08 延长判断字符15个
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2025-06-25 14:47:19 +08:00
Song367
0781e8bd86 文本返回读取内容
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-20 15:33:46 +08:00
Song367
602bb1fce2 音色配置修改
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-19 16:50:14 +08:00
Song367
a9230fcf81 去除日志
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-19 16:22:49 +08:00
Song367
ebd2f6fabb local
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-19 15:20:38 +08:00
Song367
16417c09a9 增加打印
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-19 11:47:05 +08:00
Song367
b73c2d2412 修改公证api token
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-19 11:27:03 +08:00
Song367
d52a8064db 新增匹配
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 20:29:37 +08:00
Song367
f4b35f4596 jijo
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-18 19:14:11 +08:00
Song367
7f89c584c0 audio
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 18:18:57 +08:00
Song367
ef15f5894e 删除无效输出
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-18 18:11:25 +08:00
Song367
99d6cb9f9f 增加文本长度
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 54s
2025-06-18 17:53:47 +08:00
Song367
1534a0228e 修复音频裁剪问题
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2025-06-18 17:22:26 +08:00
Song367
962b9d785a 阈值
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-18 17:17:19 +08:00
Song367
68919732db 修改阈值
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2025-06-18 16:44:14 +08:00
Song367
0c708da80d 修改音频阈值
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 16:23:42 +08:00
Song367
112a7dd70d 修改audio文件命名
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2s
2025-06-18 16:15:28 +08:00
Song367
927406b6c8 修改URL 配置
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 54s
2025-06-18 15:06:47 +08:00
wangxvdong
76ac16eb13 * ) 增加挂载宿主机目录配置。
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 15:02:32 +08:00
wangxvdong
2dfb2cc519 * ) 增加挂载宿主机目录配置。
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 52s
2025-06-18 15:00:50 +08:00
Song367
a4d2cfe3e1 dockerfile copy .env
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 13:51:50 +08:00
Song367
1585a4491c 添加控制台输出
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 52s
2025-06-18 13:48:16 +08:00
Song367
d35dfba895 dockerfile 添加代理
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 53s
2025-06-18 13:40:53 +08:00
Song367
a632bc7332 修改dockerfile
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 4m45s
2025-06-18 13:23:08 +08:00
7 changed files with 434 additions and 67 deletions

14
.env
View File

@ -1,11 +1,15 @@
# LLM API Configuration
LLM_API_URL=http://tianchat.zenithsafe.com:5001/v1
LLM_API_KEY=app-k9WhnUvAPCVcSoPDEYVUxXgC
LLM_API_URL=http://101.133.149.116:8777/v1
LLM_API_KEY=app-7mh1IAGueaBodwdMflz8Omqv
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
APP_KEY=b4839cb2-cb81-4472-a2c1-2abf31e4bb27
APP_ID=1364966010532270080
APP_KEY=a72c98fa-cbe3-449e-b004-36523437bc5d
SIG_EXP=3600
FILE_URL=http://172.17.0.1:6200/
FILE_URL=http://14.103.170.252:6200/
# Server Configuration
PORT=8080

View File

@ -39,8 +39,7 @@ jobs:
fi
docker run -d --rm --name gong-zheng-api \
-v /usr/share/fonts/opentype/noto:/usr/share/fonts \
-v $(pwd)/audio:/app/audio \
-v /root/files:/usr/src/app/audio \
-p 6211:8080 \
-p 6212:8000 \
gong-zheng-api:${{ gitea.run_id }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@ -1,56 +1,31 @@
# 构建 Go 服务
FROM golang:1.21-alpine AS go-builder
WORKDIR /app
WORKDIR /usr/src/app
# 安装必要的构建工具
RUN apk add --no-cache gcc musl-dev
# 设置 GOPROXY 环境变量
ENV GOPROXY=https://goproxy.cn,direct
# 复制 Go 项目文件
COPY . .
# 构建 Go 服务
RUN go build -o main ./main.go
# 构建 Python 服务
FROM python:3.11-slim
# 运行阶段
FROM alpine:latest
WORKDIR /app
WORKDIR /usr/src/app
# 安装必要的系统依赖
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# 从构建阶段复制编译好的二进制文件和配置文件
COPY --from=go-builder /usr/src/app/main .
COPY --from=go-builder /usr/src/app/.env .
# 创建音频目录
RUN mkdir -p /app/audio
# 暴露端口(根据你的 API 服务端口修改)
EXPOSE 8080
# 复制 Python 文件服务器
COPY file_server.py .
# 从 go-builder 阶段复制编译好的 Go 服务
COPY --from=go-builder /app/main .
# 复制配置文件(如果有的话)
COPY --from=go-builder /app/config.yaml .
# 设置环境变量
ENV PORT=8000
ENV GO_PORT=8080
# 创建启动脚本
RUN echo '#!/bin/bash\n\
# 启动 Go 服务\n\
./main &\n\
# 启动 Python 文件服务器\n\
python file_server.py -p $PORT\n\
' > /app/start.sh && chmod +x /app/start.sh
# 暴露端口
EXPOSE 8000 8080
# 设置工作目录
WORKDIR /app
# 启动服务
CMD ["/app/start.sh"]
# 运行服务
CMD ["./main"]

View File

@ -4,11 +4,9 @@ services:
app:
build: .
ports:
- "8000:8000" # Python 文件服务器端口
- "8080:8080" # Go 服务端口
volumes:
- ./audio:/app/audio # 挂载音频目录
environment:
- PORT=8000
- GO_PORT=8080
restart: unless-stopped

View File

@ -62,7 +62,6 @@ class FileHandler(http.server.SimpleHTTPRequestHandler):
with open(file_path, 'rb') as f:
self.send_response(200)
self.send_header('Content-type', content_type)
self.send_header('Content-Disposition', f'attachment; filename="{os.path.basename(file_path)}"')
self.end_headers()
self.wfile.write(f.read())
except Exception as e:

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"log"
"os"
"strconv"
@ -25,8 +26,12 @@ 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)
// Get token configuration from environment variables
sigExp, err := strconv.Atoi(os.Getenv("SIG_EXP"))
if err != nil {

View File

@ -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)
@ -138,12 +154,51 @@ func (s *LLMService) CallLLMAPI(data map[string]interface{}) (interface{}, error
return nil, fmt.Errorf("error marshaling payload: %v", err)
}
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,135 @@ 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
}
fmt.Println("触发音频", speechResp)
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 s_msg, 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)
@ -167,6 +351,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
}
messageChan := make(chan Message, 100) // Buffered channel for better performance
all_message := ""
initialSessage := ""
go func() {
defer resp.Body.Close()
@ -200,6 +385,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
switch event {
case "message":
answer := getString(jsonData, "answer")
fmt.Println("源文本:", answer)
var audio string
// 定义标点符号map
@ -209,7 +395,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
"!": true, "": true, // 感叹号
"?": true, "": true, // 问号
";": true, "": true, // 分号
":": true, "": true, // 冒号
"": true, // 冒号
"、": true,
}
@ -256,6 +442,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
}
new_message := ""
initialSessage += answer
all_message += answer
if containsPunctuation(initialSessage) {
segments := splitByPunctuation(initialSessage)
// fmt.Printf("原始文本: %s\n", initialSessage)
@ -264,11 +451,32 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
// fmt.Printf("片段 %d: %s\n", i+1, segment)
// }
if len(segments) > 1 {
initialSessage = segments[len(segments)-1]
new_message = strings.Join(segments[:len(segments)-1], "")
format_message := strings.Join(segments[:len(segments)-1], "")
// 检查initialSessage的字符长度是否超过15个
if utf8.RuneCountInString(format_message) > 15 {
initialSessage = segments[len(segments)-1]
// 如果超过10个字符将其添加到new_message中并清空initialSessage
new_message = strings.Join(segments[:len(segments)-1], "")
// initialSessage = ""
} else {
if containsPunctuation(format_message) && utf8.RuneCountInString(format_message) > 10 {
initialSessage = segments[len(segments)-1]
new_message = strings.Join(segments[:len(segments)-1], "")
} else {
continue
}
}
} else {
new_message = initialSessage
initialSessage = ""
if utf8.RuneCountInString(initialSessage) > 15 {
new_message = initialSessage
initialSessage = ""
} else {
continue
}
}
// fmt.Printf("新消息: %s\n", new_message)
// fmt.Printf("剩余文本: %s\n", initialSessage)
@ -280,8 +488,8 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
s_msg := strings.TrimSpace(new_message)
// Trim punctuation from the message
new_message = trimPunctuation(s_msg)
// fmt.Println("new_message", new_message)
fmt.Println("new_message", new_message)
// println(new_message)
// 最多重试一次
for i := 0; i < 1; i++ {
speechResp, err := s.SynthesizeSpeech(new_message, audio_type)
@ -303,7 +511,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
fmt.Printf("Error reading audio data: %v\n", err)
} else {
// Save original audio first
originalPath := fmt.Sprintf("audio/original_%d.wav", time.Now().Unix())
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)
}
@ -315,7 +523,7 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
fmt.Printf("Error trimming audio silence: %v\n", err)
} else {
// Save the trimmed audio as WAV file
audio_path := fmt.Sprintf("trimmed_%d.wav", time.Now().Unix())
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)
@ -326,12 +534,12 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
}
break // 获取到音频就退出
}
fmt.Println("audio is empty, retry", speechResp)
// fmt.Println("audio is empty, retry", speechResp)
// time.Sleep(1 * time.Second)
}
fmt.Println("所有消息:", all_message)
messageChan <- Message{
Answer: new_message,
Answer: s_msg,
IsEnd: false,
ConversationID: getString(jsonData, "conversation_id"),
TaskID: getString(jsonData, "task_id"),
@ -339,6 +547,96 @@ func (s *LLMService) handleStreamingResponse(req *http.Request, data map[string]
AudioData: audio, // Update to use the correct path to audio data
}
case "message_end":
// 在流结束前,处理剩余的文本生成音频
if initialSessage != "" {
// 不管文本长度,直接生成音频
s_msg := strings.TrimSpace(initialSessage)
// 定义标点符号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
}
new_message := trimPunctuation(s_msg)
fmt.Println("最后一段文本生成音频:", new_message)
// 生成语音
var audio string
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
}
fmt.Println("语音:", speechResp)
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
}
}
// 发送最后一段文本的消息
messageChan <- Message{
Answer: s_msg,
IsEnd: false,
ConversationID: getString(jsonData, "conversation_id"),
TaskID: getString(jsonData, "task_id"),
ClientID: getString(data, "conversation_id"),
AudioData: audio,
}
// 清空剩余文本
initialSessage = ""
}
// 发送结束消息
messageChan <- Message{
Answer: "",
IsEnd: true,
@ -353,6 +651,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)
@ -424,7 +811,7 @@ func (s *LLMService) DeleteConversation(conversationID, user string) (map[string
// SynthesizeSpeech converts text to speech
func (s *LLMService) SynthesizeSpeech(text string, audio string) (*SpeechResponse, error) {
payload := SpeechRequest{
Model: "speech-02-turbo",
Model: "speech-02-hd",
Text: text,
Stream: false,
LanguageBoost: "auto",
@ -433,8 +820,8 @@ func (s *LLMService) SynthesizeSpeech(text string, audio string) (*SpeechRespons
VoiceID: audio,
Speed: 1,
Vol: 1,
Pitch: 0,
Emotion: "happy",
Pitch: -1,
Emotion: "neutral",
},
AudioSetting: AudioSetting{
SampleRate: 32000,