微信小程序

更新时间:
复制 MD 格式

智能语音交互微信小程序 SDK 将文本合成为音频,通过回调返回音频数据和任务消息。本文介绍 SDK 接入、合成与播放示例,以及主要接口和参数。

前提条件

  • 已开通智能语音交互服务,并在智能语音交互控制台创建项目、获取 Appkey。

  • 已准备微信小程序工程,并在小程序后台配置合法服务器域名:request 域名填写提供 Token 的业务服务端 HTTPS 域名,socket 域名填写 wss://nls-gateway-cn-shanghai.aliyuncs.com。

服务协议、支持的音色和音频参数,请参见语音合成接口说明。

下载安装

从 GitHub 仓库下载 SDK,或直接下载alibabacloud-nls-wx-sdk-master.zip。将 utils 目录放入小程序工程,通过 require("../../utils/tts") 导入 SpeechSynthesizer;相对路径按页面位置调整。

获取 Token

由业务服务端获取 NLS Token,再通过经过身份校验的 HTTPS 接口向小程序提供 Token。获取方法请参见获取 Token。

AccessKey ID 和 AccessKey Secret 只保存在服务端,不写入小程序代码或本地缓存。服务端可根据 Token 的过期时间进行缓存,并在过期前重新获取,避免频繁请求 Token 接口。

代码示例

示例输入文本后合成 WAV 音频,待合成完成后播放。合成和播放期间禁止重复启动,停止操作或页面卸载时关闭连接、销毁播放器并删除本次任务的临时文件。合成超时设置为 60 秒,可按业务需要调整,不是服务端时长限制。

替换 APPKEY 和 TOKEN_URL,并在 getToken() 中补充业务服务端要求的身份认证信息。TOKEN_URL 是自建接口,不是 NLS OpenAPI;示例约定接口返回 token(字符串)和 expireTime(Unix 时间戳,单位为秒),且 Token 剩余有效时间超过 60 秒。

在 app.json 中注册 pages/tts/tts 页面。以下两个文件共同组成页面。

pages/tts/tts.js

const SpeechSynthesizer = require("../../utils/tts");
const APPKEY = "YOUR_APPKEY";
const TOKEN_URL = "https://example.com/api/nls-token";
const URL = "wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1";
const SYNTHESIS_TIMEOUT_MS = 60000;
const fs = wx.getFileSystemManager();
let sequence = 0;

function getToken() {
  return new Promise((resolve, reject) => {
    wx.request({
      url: TOKEN_URL,
      method: "GET",
      timeout: 10000,
      // Add the authentication required by your application server.
      success(res) {
        const value = res.data;
        if (res.statusCode === 200 && value &&
            typeof value.token === "string" && value.token &&
            Number(value.expireTime) > Date.now() / 1000 + 60) {
          resolve(value.token);
        } else {
          reject(new Error("Unable to obtain a valid NLS token."));
        }
      },
      fail() { reject(new Error("Token request failed.")); }
    });
  });
}

Page({
  data: { text: "Hello, welcome to speech synthesis.", busy: false, error: "" },

  onLoad() {
    this.alive = true;
    this.task = null;
  },

  textInput(e) { this.setData({ text: e.detail.value }); },

  async onTtsStart() {
    if (!this.alive || this.task) return;
    const text = this.data.text.trim();
    if (!text) {
      this.setData({ error: "Enter text to synthesize." });
      return;
    }
    const task = {
      path: `${wx.env.USER_DATA_PATH}/tts-${Date.now()}-${++sequence}.wav`,
      phase: "token", bytes: 0, fileCreated: false, disposed: false
    };
    this.task = task;
    this.setData({ busy: true, error: "" });

    try {
      const token = await getToken();
      if (task.disposed) return;
      fs.writeFileSync(task.path, new ArrayBuffer(0));
      task.fileCreated = true;
      task.tts = new SpeechSynthesizer({ url: URL, appkey: APPKEY, token });
      task.phase = "synthesis";

      // Connection closure alone does not settle the SDK's start Promise.
      const interrupted = new Promise((resolve, reject) => {
        task.reject = reject;
        task.timer = setTimeout(() => {
          reject(new Error("Speech synthesis timed out."));
        }, SYNTHESIS_TIMEOUT_MS);
      });
      task.tts.on("data", data => {
        if (task.disposed) return;
        try {
          fs.appendFileSync(task.path, data);
          task.bytes += data.byteLength;
        } catch (_) {
          task.reject(new Error("Unable to save audio."));
        }
      });
      task.tts.on("completed", () => { task.phase = "complete"; });
      task.tts.on("closed", () => {
        if (!task.disposed && task.phase === "synthesis") {
          task.reject(new Error("Connection closed before synthesis completed."));
        }
      });
      task.tts.on("failed", message => {
        // Log only diagnostic fields, not credentials or the full response.
        try {
          const header = JSON.parse(message).header;
          console.error("Synthesis failed", header.status, header.task_id);
        } catch (_) { console.error("Synthesis failed"); }
      });

      const params = task.tts.defaultStartParams("aixia");
      params.text = text;
      params.format = "wav";
      await Promise.race([task.tts.start(params), interrupted]);
      clearTimeout(task.timer);
      if (task.disposed) return;
      if (!task.bytes) throw new Error("No audio was returned.");

      task.phase = "playback";
      const audio = wx.createInnerAudioContext();
      task.audio = audio;
      audio.onEnded(() => this.dispose(task));
      audio.onError(() => this.dispose(task, "Unable to play audio."));
      audio.src = task.path;
      audio.play();
    } catch (_) {
      if (!task.disposed) {
        this.dispose(task, "Synthesis failed. Check the token, network, and audio settings.");
      }
    }
  },

  dispose(task, error = "") {
    if (!task || task.disposed) return;
    task.disposed = true;
    clearTimeout(task.timer);
    if (task.reject) task.reject(new Error("Task cancelled."));
    if (task.tts) task.tts.shutdown();
    if (task.audio) {
      task.audio.stop();
      task.audio.destroy();
    }
    if (task.fileCreated) {
      fs.unlink({
        filePath: task.path,
        fail() { console.warn("Unable to remove the temporary audio file."); }
      });
    }
    if (this.task === task) {
      this.task = null;
      if (this.alive) this.setData({ busy: false, error });
    }
  },

  onStop() { this.dispose(this.task); },

  onUnload() {
    this.alive = false;
    this.dispose(this.task);
  }
});

pages/tts/tts.wxml

<view>
  <textarea bindinput="textInput" value="{{text}}" maxlength="300" disabled="{{busy}}" />
  <button bindtap="onTtsStart" disabled="{{busy}}">合成并播放</button>
  <button bindtap="onStop" disabled="{{!busy}}">停止</button>
  <text>{{error}}</text>
</view>

API 参考

SpeechSynthesizer

通过 new SpeechSynthesizer(config) 创建语音合成对象。config 为 Object,包含以下必填字段。

config 字段

字段

类型

说明

url

String

WebSocket 服务地址,无 SDK 默认值。示例使用 wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1。

token

String

业务服务端提供的有效 NLS Token。

appkey

String

智能语音交互项目的 Appkey。

合成参数

start(param) 接收合成参数对象,可通过以下任一方式创建。defaultStartParams(voice) 不包含 text,需另行设置。

参数

类型

必填

说明

text

String

是

待合成文本,使用 UTF-8 编码,最多 300 个字符。

voice

String

否

音色。服务端默认值为 xiaoyun;defaultStartParams(voice) 使用传入的值。支持的音色请参见语音合成接口说明。

format

String

否

音频格式:pcm、wav 或 mp3。SDK 默认参数对象设为 wav;自定义对象未传该字段时,服务端默认为 pcm。

sample_rate

Integer

否

采样率,单位为 Hz。默认值:16000。

volume

Integer

否

音量,取值范围:0~100。默认值:50。

speech_rate

Integer

否

语速,取值范围:-500~500。默认值:0。倍速换算见下文。

pitch_rate

Integer

否

语调,取值范围:-500~500。默认值:0。

enable_subtitle

Boolean

否

是否开启字级别时间戳。SDK 默认值:false。用法请参见时间戳功能。

使用音色的多情感功能时,在 text 中添加 ssml-emotion 标签。支持的音色和写法请参见SSML 使用说明。

语速倍速换算

speech_rate 的 -500、0、500 分别对应默认语速的 0.5 倍、1 倍、2 倍。默认语速因音色而异,通常约每秒 4 个汉字。倍速小于 1 时,参数值按 (1 - 1 / 倍速) / 0.002 计算;大于 1 时,按 (1 - 1 / 倍速) / 0.001 计算,结果取近似整数。例如,0.8 倍速对应 -125,1.2 倍速对应 166。

使用默认参数对象

以下代码使用已创建的 tts 实例。默认参数对象的音频格式为 WAV、采样率为 16000、音量为 50、语速和语调为 0,enable_subtitle 为 false。

const params = tts.defaultStartParams("aixia");
params.text = "Hello, welcome to speech synthesis.";
params.enable_subtitle = true;

自定义参数对象

显式设置音频格式,使后续保存和播放的文件类型与合成格式一致。

const params = {
  text: "Hello, welcome to speech synthesis.",
  voice: "aixia",
  format: "wav",
  sample_rate: 16000,
  volume: 50,
  speech_rate: 0,
  pitch_rate: 0,
  enable_subtitle: true
};

on(which, handler)

注册本地事件回调:which 为事件名称(String),handler 为回调函数(Function)。同一事件再次注册会替换已有回调,无返回值。除 closed 不传参数外,其余事件均向回调传入一个参数。

事件

回调参数类型

说明

meta

String

字幕消息。开启并支持字级别时间戳时返回。

data

ArrayBuffer

合成音频的二进制分片,通过 byteLength 读取字节数。

completed

String

合成完成消息。

closed

无

连接已关闭。

failed

String

服务端任务错误消息。

String 类型的消息是 JSON 字符串,可通过 JSON.parse 读取。

async start(param)

根据 param(Object)启动语音合成,并通过已注册的回调返回音频和消息。收到 SynthesisCompleted 后关闭连接、触发 completed,返回的 Promise 以完成消息字符串 resolve。连接建立失败或收到 TaskFailed 时 reject;TaskFailed 同时触发 failed。

重要

普通连接关闭不会自动结束 start() 返回的 Promise。需处理合成完成前的 closed 事件并设置应用侧超时,异常时清理连接、文件和页面状态。

shutdown()

强制关闭连接,不等待合成完成。不接收参数,无返回值。可在取消任务、发生异常或页面卸载时调用;同时需由应用结束等待并清理本地资源。