智能语音交互微信小程序 SDK 可将小程序录制的短语音转换为文字,并通过回调返回中间结果和最终结果。
前提条件
使用限制
单次一句话识别的音频时长不超过 60 秒。发送的音频格式、采样率必须与识别参数一致。示例使用单声道、16 kHz 的 PCM 音频,并在 55 秒后停止录音。
本文下载附件中的 SDK 在 VAD 自动结束识别时,未向应用触发 completed 回调。需要通过该回调获取最终结果时,将 enable_voice_detection 设置为 false,发送完音频后调用 close()。shutdown() 仅断开连接,不能代替正常结束识别。
下载安装
从 GitHub 仓库下载 SDK,或直接下载alibabacloud-nls-wx-sdk-master.zip。将 SDK 的 utils 目录放入小程序工程,再根据文件相对路径使用 require 导入所需模块。
获取 Token
由业务服务端获取 NLS Token,再通过经过身份校验的 HTTPS 接口向小程序提供 Token。获取方法请参见获取 Token。
AccessKey ID 和 AccessKey Secret 只保存在服务端,不写入小程序代码或本地缓存。服务端可根据 Token 的过期时间进行缓存,并在过期前重新获取,避免频繁请求 Token 接口。
代码示例
示例在识别开始后录音并发送音频帧;停止录音时,等待最后一帧和录音停止事件,再请求最终结果。开始、结束和录音收尾操作均设置应用侧超时;这些超时时间可按业务需要调整,不是服务端限制。
使用前替换 APPKEY 和 TOKEN_URL,并在 getToken() 的请求中加入业务服务端要求的身份认证信息。TOKEN_URL 是自建接口,不是 NLS OpenAPI,示例约定其返回 token(字符串)和 expireTime(Unix 时间戳,单位为秒)。
在工程的 app.json 中注册 pages/sr/sr 页面,并按微信小程序要求配置录音用途及隐私保护说明。以下 JavaScript 和 WXML 共同组成该页面。
页面逻辑:pages/sr/sr.js
const SpeechRecognition = require("../../utils/sr");
const APPKEY = "YOUR_APPKEY";
const TOKEN_URL = "https://your-server.example.com/nls/token";
const GATEWAY = "wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1";
function withTimeout(promise, milliseconds) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error("Operation timed out")), milliseconds);
})
]).finally(() => clearTimeout(timer));
}
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("No valid token returned"));
}
},
fail: reject
});
});
}
Page({
data: { busy: false, recording: false, result: "", error: "" },
onLoad() {
this.recorder = wx.getRecorderManager();
const listen = (name, handler) => {
this[name] = (...args) => {
if (!this.unloaded) handler(...args);
};
this.recorder[name](this[name]);
};
listen("onStart", () => {
if (!this.session) {
this.recorder.stop();
return;
}
this.setData({ recording: true });
});
listen("onFrameRecorded", (frame) => {
const session = this.session;
if (!session || !session.sr || session.closing) return;
try {
if (frame.frameBuffer.byteLength && !session.sr.sendAudio(frame.frameBuffer)) {
throw new Error("Audio connection unavailable");
}
if (frame.isLastFrame) session.lastFrame = true;
this.finishRecording(session);
} catch (error) {
this.abort(error.message);
}
});
listen("onStop", () => {
this.recorderBusy = false;
const session = this.session;
if (!session) return;
session.stopped = true;
this.setData({ recording: false });
this.waitForLastFrame(session);
this.finishRecording(session);
});
listen("onError", () => {
this.recorderBusy = false;
this.abort("Recording failed. Check microphone permission.");
});
},
async onSrStart() {
if (this.session || this.recorderBusy) return;
const session = {};
this.session = session;
this.setData({ busy: true, recording: false, result: "", error: "" });
try {
const token = await getToken();
if (this.session !== session) return;
const sr = new SpeechRecognition({ url: GATEWAY, appkey: APPKEY, token });
session.sr = sr;
sr.on("changed", (message) => {
if (this.session === session) {
this.setData({ result: JSON.parse(message).payload.result || "" });
}
});
sr.on("completed", (message) => {
if (this.session !== session) return;
session.completed = true;
const response = JSON.parse(message);
console.log("task_id:", response.header.task_id);
this.setData({ result: response.payload.result || "" });
});
sr.on("failed", (message) => {
if (this.session !== session) return;
const header = JSON.parse(message).header;
console.error("status:", header.status, "task_id:", header.task_id);
this.abort("Recognition failed. Check the status code.");
});
sr.on("closed", () => {
if (this.session === session && !session.completed) {
this.abort("Connection closed before recognition completed.");
}
});
const params = sr.defaultStartParams();
params.enable_voice_detection = false;
await withTimeout(sr.start(params), 10000);
if (this.session !== session) return;
this.recorderBusy = true;
try {
this.recorder.start({
duration: 55000,
numberOfChannels: 1,
sampleRate: 16000,
format: "PCM",
frameSize: 4
});
} catch (error) {
this.recorderBusy = false;
throw error;
}
} catch (error) {
if (this.session === session) this.abort(error.message || "Unable to start recognition.");
}
},
onSrStop() {
const session = this.session;
if (!session || !this.data.recording || session.stopRequested) return;
session.stopRequested = true;
this.setData({ recording: false });
this.waitForLastFrame(session);
this.recorder.stop();
},
waitForLastFrame(session) {
if (session.timer) return;
session.timer = setTimeout(() => {
if (this.session === session) this.abort("Timed out while stopping recording.");
}, 5000);
},
async finishRecording(session) {
if (!session.stopped || !session.lastFrame || session.closing) return;
session.closing = true;
clearTimeout(session.timer);
try {
await withTimeout(session.sr.close(), 10000);
if (this.session === session) {
this.session = null;
this.setData({ busy: false, recording: false });
}
} catch (error) {
if (this.session === session) this.abort(error.message || "Unable to finish recognition.");
}
},
abort(message) {
const session = this.session;
this.session = null;
if (session) {
clearTimeout(session.timer);
if (session.sr) session.sr.shutdown();
}
if (this.recorderBusy) this.recorder.stop();
if (!this.unloaded) {
this.setData({ busy: false, recording: false, error: message || "" });
}
},
onHide() { this.abort(); },
onUnload() {
this.unloaded = true;
this.abort();
for (const name of ["onStart", "onFrameRecorded", "onStop", "onError"]) {
const off = this.recorder["off" + name.slice(2)];
if (typeof off === "function") off.call(this.recorder, this[name]);
}
}
});
页面布局:pages/sr/sr.wxml
<view>
<button bindtap="onSrStart" disabled="{{busy}}">开始识别</button>
<button bindtap="onSrStop" disabled="{{!recording}}">停止识别</button>
<text>{{result}}</text>
<text>{{error}}</text>
</view>
result 显示识别文字,error 显示异常提示。发生服务端错误时,示例在控制台记录状态码和任务 ID,不记录 Token。
接口说明
SpeechRecognition
通过 new SpeechRecognition(config) 创建一句话识别对象。config 为连接配置对象,包含以下字段。
|
参数 |
类型 |
说明 |
|
url |
String |
WebSocket 服务地址,示例使用 |
|
token |
String |
由业务服务端提供的有效 NLS Token。 |
|
appkey |
String |
智能语音交互项目的 Appkey。 |
defaultStartParams()
不接收参数,返回默认识别参数对象。下载附件中的 SDK 返回以下字段;示例会显式关闭其中的 VAD。
{
"format": "pcm",
"sample_rate": 16000,
"enable_intermediate_result": true,
"enable_punctuation_prediction": true,
"enable_inverse_text_normalization": true,
"enable_voice_detection": true,
"max_end_silence": 2000
}
可以修改返回对象后传给 start()。中间结果、标点预测和逆文本正则化(ITN)默认开启。启用 VAD 后,可通过 max_start_silence 和 max_end_silence 调整静音检测;max_end_silence 的取值范围为 200~6000 毫秒。参数的完整说明请参见一句话识别接口说明。
on(which, handler)
注册事件回调,无返回值。对同一事件再次调用 on() 会替换之前的回调。
|
参数 |
类型 |
说明 |
|
which |
String |
事件名称。 |
|
handler |
Function |
回调函数。 |
|
事件 |
回调参数 |
说明 |
|
started |
String |
识别开始消息,JSON 字符串。 |
|
changed |
String |
中间识别结果,JSON 字符串;结果位于 |
|
completed |
String |
调用 |
|
closed |
无 |
WebSocket 连接关闭,不代表识别成功。 |
|
failed |
String |
服务端错误消息,JSON 字符串;可读取 |
消息中的 header.task_id 标识本次识别任务,header.message_id 标识消息。识别任务结束后不再处理后续音频;开始下一次识别前需重新调用 start()。
start(param)
发起一次识别,param 为 Object 类型的识别参数。返回 Promise,在收到识别开始消息时 resolve,值为该消息的 JSON 字符串。等待开始成功后再发送音频。
建立连接失败等异常会使 Promise reject,但服务端返回 TaskFailed 时,SDK 可能只触发 failed 回调而不结束该 Promise。因此,还需处理 failed、closed 并设置应用侧超时,不能只依赖 try/catch。
close(param)
发送停止识别请求。param 为可选的 Object 类型结束参数,普通调用可省略。必须先发送完所有音频帧,再调用此方法。
返回 Promise,在收到识别完成消息时 resolve,值为该消息的 JSON 字符串,同时触发 completed。客户端不存在或等待期间收到 TaskFailed 时 reject。连接关闭不保证结束该 Promise,应用应设置超时并处理连接关闭事件。
shutdown()
强制断开连接,不接收参数,无返回值。适用于取消操作、页面退出或异常清理;不等待最终识别结果。
sendAudio(data)
发送 ArrayBuffer 类型的二进制音频数据。音频格式、采样率必须与 start() 的参数一致。
客户端不存在时返回 false,调用客户端发送方法后返回 true。该布尔值不表示服务端已确认接收;识别结果及错误通过事件回调获取。
相关文档
需要持续识别较长的语音时,请参见实时语音识别微信小程序 SDK。