助手
扣子 AI 帮助与支持
你好,我是 扣子 文档问答助手 🎉 你在阅读当前文档的过程中,无论对文档概念的解释,还是文档内容方面的疑问,都可以随时向我提问,我会全力为你解答
推荐问题
如何快速了解这个空间的核心内容?
有哪些近期更新的重点文档?
我应该从哪些文档开始阅读?
文档反馈
低代码项目
快速开始
智能体
工作流
应用
资源
发布
模型
多人协作
推广与变现

集成语音合成 SDK

更新于: 2026-06-25 19:29:14

语音合成 SDK(简称 WsSpeech SDK)基于双向流式语音合成 WebSocket OpenAPI 封装,提供了完整且高效的语音交互解决方案。它具备实时语音合成、文本播放、灵活的播放控制和丰富的音色选择等功能,适用于智能对话系统、语音导航和有声内容生成等多种应用场景,帮助开发者快速实现高质量的语音交互功能。更多接口详情请参考双向流式语音合成

体验 Demo

扣子提供语音合成 Demo 和 TypeScript 格式的语音合成示例源码,帮助你快速体验语音合成的功能,并根据示例源码快速实现语音合成。

Demo 功能简介

Demo 的主要功能包括:

  • 支持整句播放和流式播放两种模式
  • 实时文本编辑和预览
  • 支持停止或暂停播放

Image

使用 Demo

  1. 配置参数。
    单击右上角的 Settings,配置个人访问令牌和音色 ID,具体如下表所示。

    配置

    说明

    Base WS URL

    保持默认值 wss://ws.coze.cn

    个人访问令牌

    扣子 API & SDK 通过访问令牌进行 API & SDK 请求的鉴权。
    个人访问令牌的获取方式可参考添加个人访问令牌

    说明

    应为令牌授予 createSpeechlistVoice 的权限。

    音色 ID

    设置智能体使用的音色。
    扣子提供一系列系统音色,你可以在系统音色列表查看音色 ID,或通过查看音色列表 API 获取可用的音色列表。如果不指定 voiceId 或值为空,将使用默认的柔美女友音色,音色 ID 为 7426720361733046281。

  2. 在文本框中输入待合成的文本,单击整句播放流式播放

完整示例代码

以下是基于 React 和 Antd 框架开发的语音合成示例代码,具备文本输入、播放控制和状态显示等核心功能,能帮助你快速了解语音合成的实现流程。
使用前需配置个人访问令牌(ACCESS_TOKEN),并安装@coze/apiantdreact等依赖,建议开发时开启调试模式。

import React, { useEffect, useRef, useState } from 'react';
import {
  Button,
  Layout,
  Space,
  Typography,
  message,
  Row,
  Col,
  Card,
  Input,
} from 'antd';
import { WsSpeechClient } from '@coze/api/ws-tools';
import {
  SoundOutlined,
  PauseOutlined,
  PlayCircleOutlined,
  StopOutlined,
} from '@ant-design/icons';

const { Title, Paragraph, Text } = Typography;
const { TextArea } = Input;

// 个人访问令牌
const ACCESS_TOKEN ='pat_Qm47PKJR5dvMOP53v6DyzwCbTtvEZHQc2TVINEveg9v1T3iSYlTdScJ8***';
// 音色ID
const VOICE_ID = ''; // 可选

const SpeechDemo: React.FC = () => {
  const clientRef = useRef<WsSpeechClient>();
  const [transcriptionText, setTranscriptionText] =
    useState('你好,这是一个文本转语音测试。');
  const [isPlaying, setIsPlaying] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [disabled, setDisabled] = useState(false);
  const [hasToken, setHasToken] = useState<boolean>(false);
  const [status, setStatus] = useState<string>('未开始');

  // 检查令牌
  useEffect(() => {
    const checkRequirements = async () => {
      // 检查是否配置了PAT令牌
      const hasConfiguredToken = !!ACCESS_TOKEN;
      setHasToken(hasConfiguredToken);
    };

    checkRequirements();
  }, []);

  // 初始化客户端
  const initClient = () => {
    if (!ACCESS_TOKEN) {
      throw new Error('请先配置个人访问令牌');
    }

    const client = new WsSpeechClient({
      token: ACCESS_TOKEN,
      allowPersonalAccessTokenInBrowser: true,
      debug: false,
    });

    // 语音合成完成事件(含中断)
    client.on('completed', () => {
      setIsPlaying(false);
      setIsPaused(false);
      setStatus('已完成');
      console.log('语音合成完成');
    });

    // 注册所有事件
    client.on('data', event => {
      console.log('收到事件', event);
    });

    clientRef.current = client;
  };

  // 整句播放
  const handleAppendAndComplete = async () => {
    try {
      setDisabled(true); // 操作过程中禁用按钮,防止重复点击

      if (!clientRef.current) {
        initClient();
      }

      await clientRef.current?.connect({
        voiceId: VOICE_ID, // 可以从配置中获取音色ID
      });

      setIsPlaying(true);
      setIsPaused(false);
      setStatus('播放中');

      // 开始语音合成
      clientRef.current?.appendAndComplete(transcriptionText);
    } catch (error) {
      message.error(`操作失败:${error}`);
      console.error(error);
      setStatus('发生错误');
    } finally {
      setDisabled(false);
    }
  };

  // 流式播放
  const handleAppend = async () => {
    try {
      setDisabled(true);

      if (!clientRef.current) {
        initClient();
      }

      await clientRef.current?.connect({
        voiceId: VOICE_ID,
      });

      setIsPlaying(true);
      setIsPaused(false);
      setStatus('流式播放中');

      // 逐字符发送文本
      for (let i = 0; i < transcriptionText.length; i++) {
        clientRef.current?.append(transcriptionText[i]);
        await new Promise(resolve => setTimeout(resolve, 100));
      }

      // 完成文本输入
      clientRef.current?.complete();
    } catch (error) {
      message.error(`操作失败:${error}`);
      console.error(error);
      setStatus('发生错误');
    } finally {
      setDisabled(false);
    }
  };

  // 中断播放
  const handleInterrupt = async () => {
    try {
      await clientRef.current?.interrupt();
      setIsPlaying(false);
      setIsPaused(false);
      setStatus('已中断');
    } catch (error) {
      message.error(`中断失败:${error}`);
    }
  };

  // 暂停播放
  const handlePause = async () => {
    try {
      await clientRef.current?.pause();
      setIsPaused(true);
      setStatus('已暂停');
    } catch (error) {
      message.error(`暂停失败:${error}`);
    }
  };

  // 恢复播放
  const handleResume = async () => {
    try {
      await clientRef.current?.resume();
      setIsPaused(false);
      setStatus('播放中');
    } catch (error) {
      message.error(`恢复失败:${error}`);
    }
  };

  // 设置变更处理
  const handleSettingsChange = () => {
    window.location.reload();
  };

  // 组件卸载时清理资源
  useEffect(() => {
    return () => {
      clientRef.current?.disconnect();
    };
  }, []);

  return (
    <Layout style={{ height: '100%' }}>
      <Layout.Content style={{ background: '#fff', padding: '20px' }}>
        <Title level={2}>语音合成 (TTS) 演示</Title>

        {/* 前置条件检查 */}
        <Card title="前置条件检查" style={{ marginBottom: '20px' }}>
          <Row gutter={[0, 16]}>
            <Col span={24}>
              <Space>
                <Text strong>个人访问令牌 (PAT):</Text>
                {hasToken ? (
                  <Text type="success">已配置</Text>
                ) : (
                  <Text type="danger">未配置 - 请在右上角 Settings 中设置</Text>
                )}
              </Space>
            </Col>
            <Col span={24}>
              <Space>
                <Text strong>音色配置:</Text>
                {VOICE_ID ? (
                  <Text type="success">已配置 (ID: {VOICE_ID})</Text>
                ) : (
                  <Text type="warning">未配置 - 将使用默认音色</Text>
                )}
              </Space>
            </Col>
          </Row>
        </Card>

        {/* 文本输入 */}
        <Card title="文本输入" style={{ marginBottom: '20px' }}>
          <TextArea
            rows={4}
            value={transcriptionText}
            onChange={e => setTranscriptionText(e.target.value)}
            placeholder="请输入要转换为语音的文本"
            style={{ marginBottom: '16px' }}
          />
        </Card>

        {/* 播放控制 */}
        <Card title="播放控制" style={{ marginBottom: '20px' }}>
          <Row justify="start" style={{ marginBottom: '16px' }}>
            <Col>
              <Text>
                当前状态: <Text strong>{status}</Text>
              </Text>
            </Col>
          </Row>
          <Row gutter={16}>
            <Col>
              <Button
                type="primary"
                icon={<SoundOutlined />}
                disabled={disabled || !hasToken || (isPlaying && !isPaused)}
                onClick={handleAppendAndComplete}
                size="large"
              >
                整句播放
              </Button>
            </Col>
            <Col>
              <Button
                type="primary"
                icon={<SoundOutlined />}
                disabled={disabled || !hasToken || (isPlaying && !isPaused)}
                onClick={handleAppend}
                size="large"
              >
                流式播放
              </Button>
            </Col>
            {isPlaying && (
              <>
                <Col>
                  <Button
                    icon={<StopOutlined />}
                    onClick={handleInterrupt}
                    size="large"
                    disabled={!isPlaying}
                  >
                    中断
                  </Button>
                </Col>
                <Col>
                  <Button
                    icon={isPaused ? <PlayCircleOutlined /> : <PauseOutlined />}
                    onClick={isPaused ? handleResume : handlePause}
                    size="large"
                    disabled={!isPlaying}
                  >
                    {isPaused ? '恢复' : '暂停'}
                  </Button>
                </Col>
              </>
            )}
          </Row>
        </Card>

        {/* 使用说明 */}
        <Card title="使用说明" style={{ marginTop: '20px' }}>
          <Paragraph>
            <ol>
              <li>在代码中配置个人访问令牌 (PAT)</li>
              <li>可选:在代码中配置音色 ID</li>
              <li>在"文本输入"区域输入要转换为语音的文本</li>
              <li>
                选择播放方式:
                <ul>
                  <li>
                    <strong>整句播放</strong>:一次性将全部文本转换为语音
                  </li>
                  <li>
                    <strong>流式播放</strong>:逐字符转换,模拟实时生成效果
                  </li>
                </ul>
              </li>
              <li>播放过程中可以使用"暂停"、"恢复"和"中断"按钮控制播放</li>
            </ol>
          </Paragraph>
        </Card>
      </Layout.Content>
    </Layout>
  );
};

export default SpeechDemo;

实现流程

步骤一:安装依赖

运行以下命令安装 WsSpeech SDK 及其依赖项。

npm install @coze/api

步骤二:导入所需模块

在项目中导入 WsSpeechClient 类。

import { WsSpeechClient } from '@coze/api/ws-tools';

步骤三:初始化客户端

创建 WsSpeechClient 实例,并配置 token 等参数以初始化客户端。

// 创建 WsSpeechClient 实例
const client = new WsSpeechClient({
  token: 'pat_Qm47PKJR5dvMOP53v6DyzwCbTtvEZHQc2TVINEveg9v1T3iSYlTdScJ8***', // 替换为你的个人访问令牌
  allowPersonalAccessTokenInBrowser: true, // 在浏览器环境中必需
  debug: true, // 可选,启用调试日志
});

参数说明:

  • token:访问密钥,用于身份认证与鉴权。体验或调试场景可以生成短期的个人访问令牌(PAT),以快速完成 WsChat SDK 的整体流程。个人访问令牌的获取方法请参见添加个人访问令牌。在线上环境中,应使用服务访问令牌(SAT)或 OAuth 鉴权方案,详细说明请参见鉴权方式概述

    说明

    扣子 SDK 封装了多种鉴权方式,能够有效简化鉴权流程,你可以参考鉴权示例代码实现不同方式的 OAuth 认证,以获取和管理访问扣子 API 所需的令牌

  • allowPersonalAccessTokenInBrowser:在浏览器环境中使用个人访问令牌时,必须设置为 true。
  • debug:启用调试日志,便于开发和测试阶段的问题排查。

步骤四:监听事件

在初始化客户端后,通过 client.on 方法注册各种事件监听器。详细的事件说明请参见双向流式语音合成事件

// 注册事件监听器
client.on('completed', () => {
  console.log('语音合成完成');
  // 处理完成事件(例如,启用按钮,更新 UI)
});

// 可选:记录所有事件用于调试
client.on('data', (event) => {
  console.log('收到事件:', event);
});

步骤五:建立连接

调用 client.connect 方法建立客户端和服务端之间的连接,并配置音色。
扣子提供一系列系统音色,你可以在系统音色列表查看音色 ID,或通过查看音色列表 API 获取可用的音色列表。如果不指定 voiceId 或值为空,将使用默认的柔美女友音色,音色 ID 为 7426720361733046281。

// 可以选择指定音色 ID
await client.connect({ voiceId: '7426720361733046281' });

步骤六:将文本转换为语音

WsSpeech SDK 提供了两种语音的播放模式:

  • 整句播放:一次性播放整段文本,适用于短文本播放、固定内容播放以及无需实时交互的场景。
  • 流式播放:逐字符实时播放文本,提供更自然的播放体验,适用于实时对话系统、打字机效果以及需要控制播放节奏的场景。

两种模式的示例代码如下:

  • 整句播放

    // 方法 1:一次性播放整段文本
    client.appendAndComplete('你好,这是一个文本转语音测试。');
    
  • 流式播放(逐字符)

    // 方法 2:逐字符流式播放文本
    const text = '你好,这是一个流式文本转语音测试。';
    
    // 首先建立连接
    await client.connect();
    
    // 逐字符发送文本
    for (let i = 0; i < text.length; i++) {
      client.append(text[i]);
      // 字符之间添加延迟(可选)
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    
    // 通知文本输入完成
    client.complete();
    

步骤七:控制播放

在播放过程中,你可以中断播放、暂停播放、恢复播放。

// 立即中断播放
await client.interrupt();

// 暂停播放
await client.pause();

// 恢复播放
await client.resume();

// 切换播放/暂停
await client.togglePlay();

// 检查是否正在播放
const isPlaying = client.isPlaying();

说明

  • 在执行播放控制操作时,建议添加错误处理逻辑,以确保在操作失败时能够及时处理异常情况。
  • 可以通过 isPlaying 状态更新用户界面,为用户提供更直观的反馈。

步骤八:断开连接

在组件卸载或不再需要语音合成功能时,调用 disconnect 方法断开连接,清理资源,以避免潜在的内存泄漏或其他资源占用问题。

// 完成后断开连接以清理资源
await client.disconnect();