package main import ( "context" "encoding/json" "fmt" "strings" "sync" "time" "github.com/gorilla/websocket" ) type ttsCfg struct { Model string `json:"model"` Voice string `json:"voice"` Instruction string `json:"instruction"` Rate float64 `json:"rate"` Pitch float64 `json:"pitch"` Volume int `json:"volume"` } func defaultTTS(cfg agentCfg) ttsCfg { return ttsCfg{Model: cfg.TTSModel, Voice: cfg.TTSVoice, Instruction: cfg.TTSInstruction, Rate: 1, Pitch: 1, Volume: 50} } func normalizeTTS(in *ttsCfg, cfg agentCfg) (ttsCfg, error) { if in == nil { return defaultTTS(cfg), nil } t := *in t.Model, t.Voice, t.Instruction = strings.TrimSpace(t.Model), strings.TrimSpace(t.Voice), strings.TrimSpace(t.Instruction) if t.Model == "" { t.Model = cfg.TTSModel } if t.Voice == "" { t.Voice = cfg.TTSVoice } if t.Voice == "" { return t, fmt.Errorf("请填写音色或复刻音色 ID") } if len(t.Model) > 100 || len(t.Voice) > 200 || len([]rune(t.Instruction)) > 100 { return t, fmt.Errorf("TTS 配置文本过长") } if t.Rate < 0.5 || t.Rate > 2 || t.Pitch < 0.5 || t.Pitch > 2 || t.Volume < 0 || t.Volume > 100 { return t, fmt.Errorf("TTS 参数超出范围:语速/语调 0.5~2.0,音量 0~100") } return t, nil } // ttsSpeak: 百炼 CosyVoice 流式合成,音频块实时回调 onAudio。 func ttsSpeak(ctx context.Context, cfg agentCfg, tts ttsCfg, text string, onAudio func([]byte)) error { cctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() conn, _, err := websocket.DefaultDialer.DialContext(cctx, cfg.BailianWssBaseURL, map[string][]string{"Authorization": {"Bearer " + cfg.BailianKey}}) if err != nil { return fmt.Errorf("连接 TTS: %w", err) } defer conn.Close() taskID := uuid() parameters := map[string]any{ "text_type": "PlainText", "voice": tts.Voice, "format": "pcm", "sample_rate": 16000, "volume": tts.Volume, "rate": tts.Rate, "pitch": tts.Pitch, } if tts.Instruction != "" { parameters["instruction"] = tts.Instruction } err = conn.WriteJSON(map[string]any{ "header": map[string]string{"action": "run-task", "task_id": taskID, "streaming": "duplex"}, "payload": map[string]any{ "task_group": "audio", "task": "tts", "function": "SpeechSynthesizer", "model": tts.Model, "parameters": parameters, "input": map[string]any{}, }, }) if err != nil { return fmt.Errorf("TTS run-task: %w", err) } var once sync.Once done := make(chan error, 1) go func() { for { mt, data, err := conn.ReadMessage() if err != nil { select { case done <- err: default: } return } if mt == websocket.BinaryMessage { onAudio(data) continue } var ev struct { Header struct { Event string `json:"event"` ErrorMessage string `json:"error_message"` } `json:"header"` } if json.Unmarshal(data, &ev) != nil { continue } switch ev.Header.Event { case "task-started": once.Do(func() { // task-started 到达后才允许发文本 if err := conn.WriteJSON(map[string]any{ "header": map[string]string{"action": "continue-task", "task_id": taskID, "streaming": "duplex"}, "payload": map[string]any{"input": map[string]string{"text": text}}, }); err != nil { select { case done <- err: default: } } conn.WriteJSON(map[string]any{ "header": map[string]string{"action": "finish-task", "task_id": taskID, "streaming": "duplex"}, "payload": map[string]any{"input": map[string]any{}}, }) }) case "task-finished": select { case done <- nil: default: } return case "task-failed": select { case done <- fmt.Errorf("TTS 失败: %s", ev.Header.ErrorMessage): default: } return } } }() select { case err := <-done: return err case <-cctx.Done(): conn.Close() return cctx.Err() } }