微信小程序

更新时间:
复制 MD 格式

智能语音交互微信小程序 SDK 支持实时识别小程序录制的语音,通过回调返回中间结果、句子结果和任务完成消息。本文介绍 SDK 接入方式、代码示例和 API。

前提条件

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

  • 已安装微信开发者工具并配置小程序工程,获得录音权限,按微信要求配置隐私保护说明。

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

服务端协议、音频要求和识别参数,请参见实时语音识别接口说明。

使用说明

发送的音频格式和采样率须与识别参数一致。示例使用单声道、16 kHz 的 PCM 音频。微信录音器单次最长录音时长为 10 分钟。

重要

应用需处理 failed、closed 回调并设置超时,异常时停止录音、清理连接和页面状态。shutdown() 只断开连接;正常结束识别应发送完音频后调用 close()。

下载安装

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

获取 Token

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

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

代码示例

示例先启动识别,再录音并发送音频帧。手动或自动停止录音时,等待末帧和录音停止事件,再调用 close()。示例中的超时由应用设置,可按业务需要调整。

使用前替换 APPKEY 和 TOKEN_URL,并在 getToken() 请求中加入业务服务端要求的身份认证信息。TOKEN_URL 是自建接口,不是 NLS OpenAPI;示例约定返回 token(字符串)和 expireTime(Unix 时间戳,单位为秒)。每次开始识别前重新向业务服务端请求有效 Token。

在 app.json 中注册 pages/st/st 页面。以下 JavaScript 和 WXML 共同组成页面。

pages/st/st.js

const SpeechTranscription = require("../../utils/st");
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", () => {
      const session = this.session;
      if (!session) { this.recorder.stop(); return; }
      clearTimeout(session.startTimer);
      this.setData({ recording: true });
    });
    listen("onFrameRecorded", (frame) => {
      const session = this.session;
      if (!session || !session.st || session.closing) return;
      try {
        if (frame.frameBuffer.byteLength && !session.st.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;
      clearTimeout(session.startTimer);
      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 onStStart() {
    if (this.session || this.unloaded) return;
    if (this.recorderBusy) {
      this.setData({ error: "Recorder has not stopped. Reopen this page before retrying." });
      return;
    }
    const session = { sentences: {} };
    this.session = session;
    this.setData({ busy: true, recording: false, result: "", error: "" });
    try {
      const token = await getToken();
      if (this.session !== session) return;
      const st = new SpeechTranscription({ url: GATEWAY, appkey: APPKEY, token });
      session.st = st;
      const showResult = (message) => {
        if (this.session !== session) return;
        const payload = JSON.parse(message).payload;
        session.sentences[payload.index] = payload.result || "";
        const result = Object.keys(session.sentences)
          .sort((a, b) => Number(a) - Number(b))
          .map(index => session.sentences[index]).join("\n");
        this.setData({ result });
      };
      st.on("changed", showResult);
      st.on("end", showResult);
      st.on("completed", (message) => {
        if (this.session !== session) return;
        session.completed = true;
        console.log("task_id:", JSON.parse(message).header.task_id);
      });
      st.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.");
      });
      st.on("closed", () => {
        if (this.session === session && !session.completed) {
          this.abort("Connection closed before recognition completed.");
        }
      });
      await withTimeout(st.start(st.defaultStartParams()), 10000);
      if (this.session !== session) return;
      this.recorderBusy = true;
      session.startTimer = setTimeout(() => {
        if (this.session === session) this.abort("Timed out while starting recording.");
      }, 10000);
      try {
        this.recorder.start({
          duration: 600000,
          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.");
    }
  },

  onStStop() {
    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.st.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);
      clearTimeout(session.startTimer);
      if (session.st) session.st.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/st/st.wxml

<view>
  <button bindtap="onStStart" disabled="{{busy}}">开始识别</button>
  <button bindtap="onStStop" disabled="{{!recording}}">停止识别</button>
  <text>{{result}}</text>
  <text>{{error}}</text>
</view>

result 按句子编号更新识别文本,不重复追加中间结果。error 显示异常提示;控制台记录服务端错误码和任务 ID,不记录 Token。

API 参考

SpeechTranscription

通过 new SpeechTranscription(config) 创建实时语音识别对象,config 为连接配置对象。

config 字段

参数

类型

说明

url

String

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

token

String

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

appkey

String

智能语音交互项目的 Appkey。

defaultStartParams()

不接收参数,返回默认识别参数对象:PCM、16 kHz,并开启中间结果、标点预测和 ITN。可根据实时语音识别接口说明修改返回的对象。

{
  "format": "pcm",
  "sample_rate": 16000,
  "enable_intermediate_result": true,
  "enable_punctuation_prediction": true,
  "enable_inverse_text_normalization": true
}

on(which, handler)

设置事件回调。同一事件再次调用 on 会替换已有回调。返回值:无。

参数

类型

说明

which

String

事件名称。

handler

Function

回调函数。

事件

说明

参数个数

回调参数

started

实时语音识别开始。

1

String,开始消息。

changed

句子中间结果。

1

String,中间结果消息。

begin

检测到句子开始。

1

String,句子开始消息。

end

句子结束,返回该句最终结果。

1

String,句子结果消息。

completed

识别任务完成。

1

String,任务完成消息。

closed

连接关闭。

0

无。

failed

收到服务端任务错误。

1

String,错误消息。

String 类型的消息是 JSON 字符串,可通过 JSON.parse 读取。end 是句子结束,completed 是任务完成;没有有效语音时,任务可以完成而不返回句子结果。

async start(param)

启动实时语音识别。param 为 Object,可使用 defaultStartParams() 的返回值并按需修改。返回 Promise,收到 started 时 resolve,值为开始消息字符串。

连接建立失败可触发 reject。启动阶段收到 TaskFailed 时,通过 failed 回调报告错误,start() 返回的 Promise 仍可能保持 pending。应用需监听 failed、closed 并设置超时。识别参数详情请参见实时语音识别接口说明。

async close(param)

发送实时语音识别结束请求。param 为可选 Object,示例不传该参数。返回 Promise,收到任务完成消息后触发 completed、关闭连接并 resolve,值为完成消息字符串。

客户端为空或等待期间收到 TaskFailed 时会 reject。等待期间连接关闭时,通过 closed 回调处理,Promise 可能继续保持 pending,应用需设置超时。调用前应发送完音频,不要用 shutdown() 代替正常收尾。

shutdown()

强制断开连接,不等待任务完成消息。不接收参数,无返回值。适用于异常清理或页面退出。

sendAudio(data)

发送音频。data 为 ArrayBuffer,包含二进制音频,格式和采样率须与开始识别时的参数一致。

返回 Boolean:客户端对象存在时返回 true,不存在时返回 false。该返回值不表示服务端接收状态。