Files
voice_test/tts.go
T

150 lines
3.9 KiB
Go

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()
}
}