智能语音交互微信小程序 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 字段
|
字段 |
类型 |
说明 |
|
|
String |
WebSocket 服务地址,无 SDK 默认值。示例使用 |
|
|
String |
业务服务端提供的有效 NLS Token。 |
|
|
String |
智能语音交互项目的 Appkey。 |
合成参数
start(param) 接收合成参数对象,可通过以下任一方式创建。defaultStartParams(voice) 不包含 text,需另行设置。
|
参数 |
类型 |
必填 |
说明 |
|
|
String |
是 |
待合成文本,使用 UTF-8 编码,最多 300 个字符。 |
|
|
String |
否 |
音色。服务端默认值为 |
|
|
String |
否 |
音频格式: |
|
|
Integer |
否 |
采样率,单位为 Hz。默认值:16000。 |
|
|
Integer |
否 |
音量,取值范围:0~100。默认值:50。 |
|
|
Integer |
否 |
语速,取值范围:-500~500。默认值:0。倍速换算见下文。 |
|
|
Integer |
否 |
语调,取值范围:-500~500。默认值:0。 |
|
|
Boolean |
否 |
是否开启字级别时间戳。SDK 默认值: |
使用音色的多情感功能时,在 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 不传参数外,其余事件均向回调传入一个参数。
|
事件 |
回调参数类型 |
说明 |
|
|
String |
字幕消息。开启并支持字级别时间戳时返回。 |
|
|
ArrayBuffer |
合成音频的二进制分片,通过 |
|
|
String |
合成完成消息。 |
|
|
无 |
连接已关闭。 |
|
|
String |
服务端任务错误消息。 |
String 类型的消息是 JSON 字符串,可通过 JSON.parse 读取。
async start(param)
根据 param(Object)启动语音合成,并通过已注册的回调返回音频和消息。收到 SynthesisCompleted 后关闭连接、触发 completed,返回的 Promise 以完成消息字符串 resolve。连接建立失败或收到 TaskFailed 时 reject;TaskFailed 同时触发 failed。
普通连接关闭不会自动结束 start() 返回的 Promise。需处理合成完成前的 closed 事件并设置应用侧超时,异常时清理连接、文件和页面状态。
shutdown()
强制关闭连接,不等待合成完成。不接收参数,无返回值。可在取消任务、发生异常或页面卸载时调用;同时需由应用结束等待并清理本地资源。