微信小程序

更新时间:
复制 MD 格式

智能语音交互微信小程序 SDK 可将小程序录制的短语音转换为文字,并通过回调返回中间结果和最终结果。

前提条件

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

  • 已安装微信开发者工具并完成小程序工程配置。微信基础库要求 2.4.4 及以上版本。

  • 小程序已获得录音权限。在小程序后台配置服务器域名:request 合法域名填写提供 Token 的业务服务端 HTTPS 域名;socket 合法域名填写 wss://nls-gateway-cn-shanghai.aliyuncs.com。

使用限制

单次一句话识别的音频时长不超过 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 服务地址,示例使用 wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1。

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 字符串;结果位于 payload.result。

completed

String

调用 close() 后接收到的识别完成消息,JSON 字符串;最终结果位于 payload.result。

closed

无

WebSocket 连接关闭,不代表识别成功。

failed

String

服务端错误消息,JSON 字符串;可读取 header.status 和 header.task_id 排查问题。

消息中的 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。该布尔值不表示服务端已确认接收;识别结果及错误通过事件回调获取。

相关文档