跳转到主要内容
Sambert

WebSocket 接入指南

本文介绍通过WebSocket连接访问Sambert实时语音合成服务的交互流程、接口地址和请求头。

DashScope SDK目前仅支持Java和Python。使用其他编程语言时,可通过WebSocket连接与服务进行通信。 **用户指南:**关于模型介绍和选型建议请参见语音合成。

接口地址

WebSocket 接口地址固定为:wss://maas.qianwenaiapi.com/api-ws/v1/inference
URL 必须使用 wss:// 协议,且固定不变。Authorization 在请求头中设置(参见请求头)。

请求头

请求头中需添加如下信息:
参数类型是否必选说明
Authorizationstring是鉴权令牌,格式为 Bearer $DASHSCOPE_API_KEY,将 $DASHSCOPE_API_KEY 替换为实际的 API Key。
user-agentstring否客户端标识,便于服务端追踪来源。
X-DashScope-WorkSpacestring否千问AI平台业务空间ID。
X-DashScope-DataInspectionstring否是否启用数据合规检测功能。默认不传或设为enable。如非必要,请勿启用该参数。
Authorization 鉴权在 WebSocket 握手阶段验证。如果 API Key 无效或缺失,握手将失败并返回 HTTP 401/403 错误。

交互流程

image
客户端事件和服务端事件的详细说明,请参见客户端事件和服务端事件。 按时间顺序,客户端与服务端的交互流程如下:
  1. 建立连接:客户端与服务端建立WebSocket连接。
  2. 开启任务:客户端发送run-task事件以开启任务。Sambert在run-task中一次性发送全部待合成文本。
  3. 等待确认:客户端收到服务端返回的task-started事件,标志着任务已成功开启。
  4. 接收音频:客户端通过 binary 通道接收服务端持续返回的音频流,同时收到result-generated事件(携带时间戳等附加信息)。
  5. 任务结束:客户端收到服务端返回的task-finished事件,标志着任务结束。
  6. 关闭连接:客户端关闭WebSocket连接。
为提高资源利用率,建议复用 WebSocket 连接处理多个任务,而非为每个任务建立新连接。详细操作请参见连接复用(WebSocket)。
Sambert 不支持流式输入(streaming 为 out 而非 duplex),所有待合成文本必须在 run-task 事件中一次性发送。不支持 continue-task 和 finish-task 指令。
通过 Java SDK 使用对象池并发调用 Sambert 的方法和完整示例,请参见语音合成高并发最佳实践。直接使用 WebSocket 协议时,可按上述流程复用连接。

示例代码

以下示例使用通用 WebSocket 库直接发送协议事件,使用 sambert-zhichu-v1。先获取 API Key并配置 DASHSCOPE_API_KEY 环境变量。
  • Go
  • C#
  • PHP
  • Node.js
go mod init tts-example
go get github.com/gorilla/websocket@v1.5.3 github.com/google/uuid@v1.6.0
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

const (
	wsURL      = "wss://maas.qianwenaiapi.com/api-ws/v1/inference" // WebSocket服务器地址
	outputFile = "output.mp3"                                        // 输出文件路径
)

func main() {
	// 若没有将API Key配置到环境变量,可将下行替换为:apiKey := "your_api_key"。不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// 检查并清空输出文件
	if err := clearOutputFile(outputFile); err != nil {
		fmt.Println("清空输出文件失败:", err)
		return
	}

	// 连接WebSocket服务
	conn, err := connectWebSocket(apiKey)
	if err != nil {
		fmt.Println("连接WebSocket失败:", err)
		return
	}
	defer closeConnection(conn)

	// 创建一个通道用于接收任务完成的通知
	done := make(chan struct{})

	// 启动异步接收消息的goroutine
	go receiveMessage(conn, done)

	// 发送run-task指令
	if err := sendRunTaskMsg(conn); err != nil {
		fmt.Println("发送run-task指令失败:", err)
		return
	}

	// 等待任务完成或超时
	select {
	case <-done:
		fmt.Println("任务结束")
	case <-time.After(5 * time.Minute):
		fmt.Println("任务超时")
	}
}

// 定义消息结构体
type Message struct {
	Header  Header  `json:"header"`
	Payload Payload `json:"payload"`
}

// 定义头部信息
type Header struct {
	Action       string                 `json:"action,omitempty"`
	TaskID       string                 `json:"task_id"`
	Streaming    string                 `json:"streaming,omitempty"`
	Event        string                 `json:"event,omitempty"`
	ErrorCode    string                 `json:"error_code,omitempty"`
	ErrorMessage string                 `json:"error_message,omitempty"`
	Attributes   map[string]interface{} `json:"attributes"`
}

// 定义负载信息
type Payload struct {
	Model      string     `json:"model,omitempty"`
	TaskGroup  string     `json:"task_group,omitempty"`
	Task       string     `json:"task,omitempty"`
	Function   string     `json:"function,omitempty"`
	Input      Input      `json:"input,omitempty"`
	Parameters Parameters `json:"parameters,omitempty"`
	Output     Output     `json:"output,omitempty"`
	Usage      Usage      `json:"usage,omitempty"`
}

// 定义输入信息
type Input struct {
	Text string `json:"text"`
}

// 定义参数信息
type Parameters struct {
	TextType                string  `json:"text_type"`
	Format                  string  `json:"format"`
	SampleRate              int     `json:"sample_rate"`
	Volume                  int     `json:"volume"`
	Rate                    float64 `json:"rate"`
	Pitch                   float64 `json:"pitch"`
	WordTimestampEnabled    bool    `json:"word_timestamp_enabled"`
	PhonemeTimestampEnabled bool    `json:"phoneme_timestamp_enabled"`
}

// 定义输出信息
type Output struct {
	Sentence Sentence `json:"sentence"`
}

// 定义句子信息
type Sentence struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Words     []Word `json:"words"`
}

// 定义单词信息
type Word struct {
	Text      string    `json:"text"`
	BeginTime int       `json:"begin_time"`
	EndTime   int       `json:"end_time"`
	Phonemes  []Phoneme `json:"phonemes"`
}

// 定义音素信息
type Phoneme struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Text      string `json:"text"`
	Tone      int    `json:"tone"`
}

// 定义使用信息
type Usage struct {
	Characters int `json:"characters"`
}

func receiveMessage(conn *websocket.Conn, done chan struct{}) {
	for {
		msgType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("解析服务器消息失败:", err)
			close(done)
			break
		}

		if msgType == websocket.BinaryMessage {
			// 处理二进制音频流
			if err := writeBinaryDataToFile(message, outputFile); err != nil {
				fmt.Println("写入二进制数据失败:", err)
				close(done)
				break
			}
			fmt.Println("音频片段已写入本地文件")
		} else {
			// 处理文本消息
			var msg Message
			if err := json.Unmarshal(message, &msg); err != nil {
				fmt.Println("解析事件失败:", err)
				continue
			}
			if handleMessage(conn, msg, done) {
				break
			}
		}
	}
}

func handleMessage(conn *websocket.Conn, msg Message, done chan struct{}) bool {
	switch msg.Header.Event {
	case "task-started":
		fmt.Println("任务已启动")

	case "result-generated":
	// 如需获取附加消息,可在此处添加相应代码

	case "task-finished":
		fmt.Println("任务已完成")
		close(done)
		return true

	case "task-failed":
		if msg.Header.ErrorMessage != "" {
			fmt.Printf("任务失败:%s\n", msg.Header.ErrorMessage)
		} else {
			fmt.Println("未知原因导致任务失败")
		}
		close(done)
		return true

	default:
		fmt.Printf("预料之外的事件:%v\n", msg)
		close(done)
	}

	return false
}

func sendRunTaskMsg(conn *websocket.Conn) error {
	runTaskMsg, err := generateRunTaskMsg()
	if err != nil {
		return err
	}
	if err := conn.WriteMessage(websocket.TextMessage, []byte(runTaskMsg)); err != nil {
		return err
	}
	return nil
}

func generateRunTaskMsg() (string, error) {
	runTaskMessage := Message{
		Header: Header{
			Action:    "run-task",
			TaskID:    uuid.New().String(),
			Streaming: "out",
		},
		Payload: Payload{
			Model:     "sambert-zhichu-v1",
			TaskGroup: "audio",
			Task:      "tts",
			Function:  "SpeechSynthesizer",
			Input: Input{
				Text: "白日依山尽,黄河入海流。欲穷千里目,更上一层楼。",
			},
			Parameters: Parameters{
				TextType:                "PlainText",
				Format:                  "mp3",
				SampleRate:              16000,
				Volume:                  50,
				Rate:                    1.0,
				Pitch:                   1.0,
				WordTimestampEnabled:    true,
				PhonemeTimestampEnabled: true,
			},
		},
	}

	runTaskMsgJSON, err := json.Marshal(runTaskMessage)
	return string(runTaskMsgJSON), err
}

func connectWebSocket(apiKey string) (*websocket.Conn, error) {
	header := make(http.Header)
	header.Add("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		fmt.Println("连接WebSocket失败:", err)
		return nil, err
	}
	return conn, nil
}

func writeBinaryDataToFile(data []byte, filePath string) error {
	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.Write(data)
	return err
}

func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

func clearOutputFile(filePath string) error {
	file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}