优谷·雅言SDK 文档2.0.0

微信小程序 SDK

微信小程序端的优谷雅言语音评测 SDK,封装整段评测,声通兼容评测,实时评测,语音合成与报告查询,内置幂等键,自动重试,断线重连,录音器与音频预检。SDK 为零运行时依赖的 CommonJS 包,附完整 TypeScript 声明。

查看 Markdown
项 内容
包名 @shengzhiai/yugu-miniprogram-sdk
当前版本 2.1.0,2026-10-12
npm 源 https://open.shengzhiai.com/npm/,匿名只读
服务基址 https://open.shengzhiai.com,wss://open.shengzhiai.com
基础库 2.20.1 及以上
源码仓库 https://open.shengzhiai.com/git/yugu-sdk.git 的 miniprogram 目录
示例工程 源码仓库的 demos/miniprogram-demo 目录
许可 Apache-2.0

接口契约见仓库根目录的 CONTRACT.md,错误码见 ERRORS.md,各评测模式的取分字段见 RESULTS.md,沙箱见 SANDBOX.md。

安装#

在小程序工程的根目录,即 project.config.json 所在目录,执行:

Shell
echo "@shengzhiai:registry=https://open.shengzhiai.com/npm/" >> .npmrc
npm install @shengzhiai/yugu-miniprogram-sdk@2.1.0

工程里还没有 package.json 时先执行 npm init -y。npm 源不需要账号,已发布的版本不会被覆盖,退回历史版本时把版本号换成目标版本重新安装即可。

安装后在微信开发者工具中构建 npm:

  1. 打开工程,点击菜单“工具”,选择“构建 npm”。
  2. 构建完成后工程里出现 miniprogram_npm/@shengzhiai/yugu-miniprogram-sdk 目录。
  3. 代码中按包名引用:
JavaScript
const { YuguClient } = require('@shengzhiai/yugu-miniprogram-sdk');

project.config.json 设置了 miniprogramRoot 时,package.json 放在该目录下,或者在 setting.packNpmRelationList 里登记 package.json 与 miniprogram_npm 的位置。升级 SDK 后要重新构建 npm。

合法域名#

SDK 只通过 wx.request 与 wx.connectSocket 访问平台,上线前在小程序管理后台的“开发管理”,“开发设置”,“服务器域名”中配置:

类型 域名
request 合法域名 https://open.shengzhiai.com
socket 合法域名 wss://open.shengzhiai.com

SDK 2.0 不再使用 wx.uploadFile,不需要配置 uploadFile 合法域名。开发阶段可以在开发者工具的“详情”,“本地设置”里勾选“不校验合法域名,web-view,TLS 版本以及 HTTPS 证书”,真机预览与发布前要改回。

录音需要麦克风权限。在小程序管理后台的“用户隐私保护指引”中声明使用麦克风,首次录音时微信弹出授权框,用户拒绝后录音器报 PermissionException,错误码 90201。

五分钟上手#

整段评测:录一段音,上传评测,打印总分。

JavaScript
const { YuguClient } = require('@shengzhiai/yugu-miniprogram-sdk');

const client = new YuguClient({
  auth: { token: '<由自有服务端下发的短期 token>' },
});
const recorder = client.createRecorder();

Page({
  onRecordStart() {
    recorder.start();
  },
  async onRecordStop() {
    const recording = await recorder.stop();
    const result = await client.evaluate({
      coreType: 'sentence',
      referenceText: '今天天气很好',
      language: 'zh-CN',
      audio: recording,
    });
    console.log('总分', result.overall, '发音', result.dims.pronunciation, '流利', result.dims.fluency);
  },
  onUnload() {
    client.close();
  },
});

实时评测:录音帧直接推入会话,停止录音时自动结束会话。

JavaScript
const session = client.streamEvaluate(
  { coreType: 'sentence', referenceText: '今天天气很好', language: 'zh-CN' },
  {
    onResult(result) { console.log('总分', result.overall); },
    onError(err) { console.log('评测失败', err.code, err.message); },
  },
);
recorder.pipeTo(session);
recorder.start();
// 用户读完后
recorder.stop();

鉴权#

方式 写法 说明
token auth: { token } 由自有服务端调用开放平台换取短期 JWT 后下发给小程序,正式环境使用这种方式
签名 auth: { appKey, secretKey } 每次请求按 CONTRACT.md 第 2 节签名,适合沙箱联调。声通兼容整段评测 evaluateCompat 要求请求头带 X-App-Key,只能用这种方式

secretKey 写进小程序包就可能被反编译取得,正式环境不要在小程序里使用 secretKey。为兼容 1.x 写法,token,appKey,secretKey 也可以直接写在客户端配置的顶层。签名鉴权时请求头带 X-App-Key,X-Timestamp,X-Nonce,X-Signature,实时评测把签名参数放在握手 query 里。设备时钟与服务器时间相差超过 300 秒时请求会被拒绝,错误归为 AuthException。

接口一览#

接口 说明
new YuguClient(options) 创建客户端,配置见“客户端配置”
client.evaluate(params, options) 原生整段评测 POST /api/v1/evaluate,返回 Promise<EvalResult>
client.evaluateCompat(coreType, params, options) 声通兼容整段评测 POST /{coreType},返回 Promise<EvalResult>,需要签名鉴权,用 token 时在发请求前以错误码 90010 失败
client.tts(params, options) 语音合成,返回 Promise<TtsResult>,fullUrl 可直接交给 InnerAudioContext
client.getReport(recordId, options) 报告查询,返回报告数据
client.streamEvaluate(params, listener, options) 原生实时评测,返回 StreamSession,参数 realtimeFeedback 打开朗读进度
client.streamEvaluateCompat(coreType, params, listener, options) 声通兼容实时评测,返回 StreamSession,参数 realtimeFeedback 打开朗读进度
client.createRecorder(options) 创建录音器,client.close() 时一并释放
client.close() 关闭客户端,可重复调用
client.isClosed() 是否已关闭
session.sendAudio(chunk) 送入 16 位小端 PCM,ArrayBuffer 或类型化数组,大于 32000 字节的块拆成多帧发送,平台单帧上限 128 KB
session.end() 音频结束,等待终评
session.cancel(),session.close() 取消会话,可重复调用
session.getState(),session.isActive() 会话状态查询
session.setListener(listener),session.removeListener() 更换或注销监听器
recorder.start(),pause(),resume(),stop(),release() 录音控制,release() 可重复调用
recorder.getState() 录音器状态查询
recorder.pipeTo(session),recorder.unpipe() 录音帧推入实时评测会话
isRetryable(error) 可重试判定,与 SDK 内部重试共用一套规则
fromCode(code),fromWarningCode(code) 按错误码构造对应类型的错误
precheckAudio(bytes, options) 本地音频预检
generateIdempotencyKey() 生成幂等键
signParams(params, secretKey) 签名,与平台算法一致
createAbortController() 小程序环境的取消控制器
parseEvalResult(json) 把平台原始结果转为 EvalResult

evaluate 的参数为 coreType,referenceText,language,includeReport,includeStandardAudio,includeAsrText,slack,scale,precision,agegroup,toneWeight,refPinyin,phonemeOutput,taskType,paragraphNeedWordScore 与音频 audio,含义见 CONTRACT.md 第 3.1 节。契约之外的新字段放在 extra 里原样透传。看图说话题用 image 传图片。evaluateCompat 用声通参数名 refText,language,refPinyin,其余表单字段放在 fields 里。streamEvaluate 的参数与 evaluate 相同,不带音频,另有 realtimeFeedback,streamEvaluateCompat 的参数为 refText,language,refPinyin,realtimeFeedback 与 fields,realtimeFeedback 的用法见“朗读进度”。

客户端配置#

配置 默认值 说明
auth 必填 { token } 或 { appKey, secretKey }
baseUrl https://open.shengzhiai.com REST 基址
wsBaseUrl wss://open.shengzhiai.com 实时评测基址
connectTimeoutMs 10000 实时评测从建连到服务端会话开始的时限
readTimeoutMs 120000 单次 REST 尝试的时限
totalTimeoutMs 300000 一次逻辑调用的总时限,重试与等待都计入
retry 见“重试与幂等” 部分覆盖重试策略,false 关闭重试
autoIdempotencyKey true 调用方未给幂等键时自动生成
logLevel WARN OFF,ERROR,WARN,INFO,DEBUG
logger 控制台 日志输出函数 (level, tag, message, error)
eventListener 无 指标回调,见“日志与指标”
audioPrecheck WARN 音频预检模式 OFF,WARN,REJECT
strictAudio false 评测结果带警告 1001 时抛出 AudioQualityException
userAgent yugu-miniprogram-sdk/2.1.0 通过请求头 X-Yugu-SDK 发送,小程序不能改写 User-Agent
reconnect 见“实时评测” 部分覆盖重连策略,false 关闭重连
audioBufferPolicy REPLAY 实时评测音频缓冲策略 REPLAY,DROP,FAIL
heartbeatIntervalMs 15000 应用层心跳间隔,0 关闭心跳
heartbeatTimeoutMs 30000 心跳生效后多久收不到任何帧视为断线
resultTimeoutMs 300000 发出结束帧后等待终评的时限
replayBufferLimitBytes 10485760 REPLAY 缓冲上限,即 10 MB

wx.request 只有一个总超时,SDK 用 readTimeoutMs 与剩余总时限中较小的值作为每次尝试的超时。客户端配置里另有 wx 与 random 两项,分别用于在测试中注入 wx 对象与退避抖动的随机数源,业务代码不需要设置。

单次调用选项#

evaluate,evaluateCompat,tts,getReport 的最后一个参数:

选项 说明
idempotencyKey 调用方指定的幂等键,1 到 200 个可见 ASCII 字符,例如业务单号
timeoutMs 覆盖本次调用的 readTimeoutMs
totalTimeoutMs 覆盖本次调用的 totalTimeoutMs
retry 覆盖本次调用的重试策略,false 不重试
signal 取消信号,createAbortController().signal 或任意 AbortSignal 形态的对象
audioPrecheck 覆盖本次调用的预检模式
strictAudio 覆盖本次调用的 strictAudio
JavaScript
const { createAbortController } = require('@shengzhiai/yugu-miniprogram-sdk');
const ctl = createAbortController();
client.evaluate(params, { idempotencyKey: 'order-20261008-42', signal: ctl.signal })
  .catch((err) => console.log(err.name, err.code));
ctl.abort(); // 页面退出时取消,Promise 以 RequestCancelledException 结束,错误码 90003

结果模型#

整段评测与实时评测都返回 EvalResult:

字段 说明
overall 总分,connected 模式取 result.connected_overall
dims 朗读类维度:pronunciation,accuracy,fluency,integrity,tone,rhythm,emotion,未评的维度为 null
words 逐字或逐词详情,字段保持平台原名,见 RESULTS.md
sentences 分句结果,段落题在 paragraphNeedWordScore 为 1 时带 details
speed,rearTone,duration,durationSec 语速,句末语调,时长原值与秒数
connected 连读题的分项,其余题型为 null
open 开放题的分项,其余题型为 null
coreType,language 引擎报告的题型与语种
asrText,report,standardAudio,yuguScores 识别文本,报告,标准示范音,维度细分
warnings 平台音频质量警告 [{ code, message }]
localWarnings 本地预检发现的问题,错误码 90101 到 90105
recordId,eof 记录号与结束标记
idempotencyKey,replayed 本次使用的幂等键,结果是否为同一个键的重放
attempts 整段评测为尝试次数,实时评测为使用的连接数
raw 平台返回的原始 JSON

各模式的取分字段:

原生 coreType 声通兼容 coreType 总分 分项
word word.eval,word.eval.pro,word.eval.cn overall dims,words
sentence sent.eval,sent.eval.pro,sent.eval.cn overall dims,words,sentences
passage para.eval,para.eval.cn overall dims,sentences[].details,words
alpha alpha.eval overall dims,words
pinyin pinyin overall dims,words,引擎按句子题评分,coreType 报告为 sentence
connected 无 overall,即 connected.overall connected.linking,elision,reduction,rhythm,nBoundaries,boundaries[]
open 无 overall open.content,open.languageUse,open.delivery,open.transcript,open.feedback,open.openTaskAudit

connected.boundaries 的每一项给出 between 两个词,tags 应有的连读类型,realized 实际实现程度,start_ms 与 end_ms。open.delivery 含 fluency,pronunciation,speech_rate,speech_rate_label,n_pauses。嵌套对象保持平台原有字段名,原始 JSON 始终可以从 raw 取到。

JavaScript
const r = await client.evaluate({ coreType: 'open', referenceText: '介绍一下自己', language: 'zh-CN', audio: recording });
if (r.open) {
  console.log(r.overall, r.open.content.overall, r.open.delivery.speech_rate_label, r.open.feedback.suggestions);
}

语音合成返回 TtsResult:audioUrl 为平台原值,fullUrl 为可直接播放的完整地址,另有 duration,format,idempotencyKey,replayed。

TypeScript#

包内附 types/index.d.ts,package.json 的 types 字段已指向该文件,引入后字段有补全与编译期检查,开启 strict 与 noImplicitAny 也没有隐式 any。声明只依赖 ES2017 标准库,不依赖 DOM 与 Node 类型,与小程序官方类型 miniprogram-api-typings 可以同时使用。

TypeScript
import { YuguClient, isRetryable, RateLimitException } from '@shengzhiai/yugu-miniprogram-sdk';
import type { EvalResult, StreamListener } from '@shengzhiai/yugu-miniprogram-sdk';

const client = new YuguClient({ auth: { token } });
const listener: StreamListener = {
  onResult(result: EvalResult) { console.log(result.overall); },
  onError(err) { if (err instanceof RateLimitException && isRetryable(err)) console.log(err.retryAfterMs); },
};

错误处理#

SDK 抛出的错误都是 YuguError 或其子类,字段为 category,code,codeNamespace,httpStatus,message,retryable,idempotencyKey,recordId,attempts,rawBody,op,cause。rawBody 截取前 4 KB。codeNamespace 说明错误码来自哪张表:errors 为平台错误码,warnings 为音频质量警告码,local 为 SDK 本地错误码,unknown 为码表之外的码或没有码。

类别 类型 典型错误码
NETWORK NetworkException 90001 网络错误,90006 重连次数用尽,90011 证书校验失败
TIMEOUT RequestTimeoutException 90002 超时,90007 终评超时
AUTH AuthException 40100,2001 到 2011
PERMISSION PermissionException 40300,90201 没有麦克风权限
INVALID_PARAM InvalidParameterException 40001,90010 调用参数不合法
NOT_FOUND NotFoundException 40400
CONFLICT ConflictException 40901 同一幂等键处理中,40903 幂等键用于不同请求
RATE_LIMIT RateLimitException 42900,42901,3001 到 3003
QUOTA QuotaExceededException 40902,42903 沙箱当日次数用尽
SERVER,UPSTREAM ServerException 50000,50200
AUDIO AudioQualityException 90101 到 90105,警告码 1001 到 1005 与 1009
STATE IllegalSessionStateException 90004 客户端已关闭,90008,90009,90202,90203
CANCELLED RequestCancelledException 90003
PROTOCOL ProtocolViolationException 90005 响应或帧无法解析
UNKNOWN YuguError 无法归类的错误

错误归类的顺序:本地错误,响应体里的 code,引擎 detail 中的 [2001] 形式,最后按 HTTP 状态。响应体里的 code 只按平台错误码表解释。HTTP 状态先查 ERRORS.md 的兜底表,表里没有的其余 4xx 归为 INVALID_PARAM,其余 5xx 归为 SERVER,两者都不重试,其他状态归为 UNKNOWN。完整码表见 ERRORS.md,SDK 内置同一份表,导出为 ERROR_TABLE,WARNING_TABLE,LOCAL_TABLE 与 ErrorCodes。

JavaScript
const { isRetryable, AuthException, AudioQualityException, ErrorCodes } = require('@shengzhiai/yugu-miniprogram-sdk');
try {
  await client.evaluate(params);
} catch (err) {
  if (err instanceof AuthException) {
    // 重新获取 token
  } else if (err instanceof AudioQualityException) {
    // 提示重录,err.code 为 90101 到 90105
  } else if (err.code === ErrorCodes.QUOTA_INSUFFICIENT) {
    // 额度不足,提示充值
  } else if (isRetryable(err)) {
    // SDK 已按策略重试过,稍后再提示用户重试
  }
}

平台音频质量警告 1001 到 1005 与 1009 随结果返回,放在 result.warnings,不抛出异常。设置 strictAudio: true 后,警告 1001 即未检测到有效音频会以 AudioQualityException 抛出。错误码 1004 与 1005 在平台错误码与警告码中都存在,按出现的位置解释:错误响应体里是用户禁用与锁定,评测结果的警告里是噪声与音频不完整。fromCode 按平台错误码构造,警告请用 fromWarningCode。

重试与幂等#

写操作 evaluate,evaluateCompat,tts 默认每次逻辑调用生成一个幂等键,放在请求头 Idempotency-Key,重试复用同一个键,平台对同一个键只评测一次,只计费一次。结果与错误都带 idempotencyKey,同一个键的重复提交返回首次结果,replayed 为 true。幂等键按 appKey 或 token 用户隔离,平台保存 24 小时,首次请求失败时释放。同一个键用于不同请求时平台回 409,错误码 40903,不重试。

默认重试策略:

字段 默认值
maxRetries 2,即一共 3 次尝试
initialDelayMs 200
multiplier 2
maxDelayMs 4000
jitter 0.3
respectRetryAfter true
maxRetryAfterMs 30000

第 n 次重试前等待 min(maxDelayMs, initialDelayMs × multiplier^(n-1)) × (1 + U(-jitter, jitter)) 毫秒,默认约为 200 毫秒与 400 毫秒,各上下浮动 30%。响应带 Retry-After 时取两者较大值,上限 maxRetryAfterMs。下一次等待会超出 totalTimeoutMs 时不再重试,直接抛出最后一次的错误,attempts 记录已尝试次数。

可重试的情形:网络错误,超时,HTTP 408,425,429,500,502,503,504,以及错误码表里标为可重试的错误码,例如 40901,42900,42901,50000,50200。参数错误,鉴权失败,权限不足,额度不足等不重试。每次重试都会回调 eventListener.onRetry,同时输出一条 WARN 日志,例如 retry 1/2 in 231 ms: HTTP 503 code=50200。

关闭 autoIdempotencyKey 后,调用时没有给键的写操作不重试,DEBUG 日志会说明原因。报告查询天然幂等,不带键也会重试。

实时评测#

实时评测在一条 WebSocket 连接上完成一轮评测:连接,开始,送音频,结束,终评。streamEvaluate 与 streamEvaluateCompat 返回会话后在下一个微任务开始连接,开始前送入的音频与提前调用的 end() 都会按顺序补发。

会话状态:

状态 含义
IDLE 未连接
CONNECTING 建立连接中
CONNECTED 服务端已发 connected
STARTED 服务端已回 started,可以送音频
ENDING 结束帧已发出,等待终评
RECONNECTING 断线后等待重连
COMPLETED 已收到终评
FAILED 以错误结束
CANCELLED 调用方取消
CLOSED 会话结束,连接已关闭

监听器回调:

回调 时机
onStateChanged(oldState, newState) 每次状态变化
onConnected() 首次连接成功
onStarted() 首次服务端会话开始
onPartial(partial) 打开 realtimeFeedback 后的朗读进度,原生与声通兼容两条通道都有,end() 之后到终评之前仍会回调,不含发音评分,字段见“朗读进度”
onReconnecting(attempt, delayMs, cause) 断线后开始等待重连
onReconnected(attempt, info) 重连成功,info 含 droppedBytes 与 replayedBytes
onWarning(warning) end() 时对这一轮音频的预检发现
onResult(result) 终评,必填
onError(error) 以错误结束,必填
onClosed(code, reason) 会话结束,总在最后

回调保证:每个会话只触发 onResult 与 onError 中的一个,最多一次,调用方先调用 cancel() 时两者都不触发。onClosed 总是最后一个回调。同一个会话的回调不会嵌套执行,回调里调用会话方法引起的新回调排在当前回调返回之后。getState() 返回的值与最近一次 onStateChanged 给出的新状态一致。

断线重连语义:平台不支持会话续传,一个连接就是一个服务端会话。连接意外关闭,心跳超时,连接超时,终评超时,或收到可重试的错误帧时,会话进入 RECONNECTING,按退避等待后新建连接,用同一个幂等键重发开始帧,按缓冲策略处理音频,结束帧已发出时一并重发。平台按幂等键识别重发,首次评测已完成时直接重放首次结果,不重复评测,不重复计费。重连次数用尽时以 NetworkException 结束,错误码 90006,cause 为最后一次的原因。服务端以关闭码 1002,1003,1007,1008,1009,1010 或 4000 到 4999 关闭连接时,重连也会被同样拒绝,会话立即以 ProtocolViolationException 结束,错误码 90005,不重连。服务端以 1000 正常关闭却没有给出终评时同样处理。

缓冲策略 行为
REPLAY 默认 缓冲这一轮的全部音频,上限 10 MB,重连后整段重放,评分覆盖全部音频。超过上限后停止缓冲,之后的重连以错误码 90008 结束
DROP 不缓冲,断开的连接上已发的音频与重连期间送入的音频都不再评分,onReconnected 的 droppedBytes 给出字节数。结束帧发出后断线时没有可评的音频,直接以错误结束
FAIL 不重连,断线即以错误结束

默认重连策略为 enabled true,maxAttempts 8,initialDelayMs 500,multiplier 2,maxDelayMs 4000,jitter 0.3,各次等待约为 0.5 秒,1 秒,2 秒,4 秒,4 秒,4 秒,4 秒,4 秒,合计约 23 秒,按抖动最短也有 16 秒以上,默认设置即可撑过客户端断网 10 秒。maxAttempts 指一次断线内连续失败的次数,重连成功后重新计。一个会话的重连总次数另有上限,为 maxAttempts 的 3 倍,默认 24 次,服务端反复接受后又断开时,会话在达到上限后以错误码 90006 结束,getStats().reconnectAttempts 给出已用的次数。

心跳:小程序不能发送协议层 ping,SDK 使用应用层心跳 {"cmd":"ping"},服务端回 {"event":"pong"}。

  1. 每条连接收到 connected 后先发一个探测 ping,再发开始帧或参数帧,所以不认识 ping 的服务端也不会因此丢失音频。
  2. 服务端回了 pong,心跳才生效:连接后与开始后每 heartbeatIntervalMs 发一次 ping,heartbeatTimeoutMs 内收不到任何帧视为断线,随即重连。
  3. 服务端对探测回 {"event":"error","message":"unknown cmd"},或不回 pong 先回 started 时,视为不支持心跳,本会话关闭心跳,不当作错误,此后依靠连接超时,连接关闭检测与终评超时。
  4. 发出结束帧后服务端在评测,SDK 不再发心跳,改由 resultTimeoutMs 判断,默认 300 秒。

单个会话可以用 streamEvaluate 的第三个参数覆盖 idempotencyKey,reconnect,audioBufferPolicy,heartbeatIntervalMs,heartbeatTimeoutMs,resultTimeoutMs,connectTimeoutMs,replayBufferLimitBytes,audioPrecheck 与附加握手参数 query。end() 之后再调用 sendAudio 会被忽略,返回 false,同时记录一条 WARN 日志。

朗读进度#

学员朗读的过程中,平台按参考文本逐字或逐词报告哪些单位已经读过,界面可以边读边标出进度。原生实时评测在 streamEvaluate 的参数里写 realtimeFeedback: true,开始帧随之带 "realtimeFeedback": true,不写时开始帧与 2.0.0 逐字节相同。声通兼容实时评测在 streamEvaluateCompat 的参数里写 realtimeFeedback: true,参数帧带 "realtime_feedback": true。两条通道的 result.progress 格式相同,都经 onPartial 回调,进度帧不含发音评分,终评与计费不变,帧格式见 CONTRACT.md 5.5 节。

字段 说明
bytes 这一轮服务端已收到的音频字节数,含义与 2.0.0 相同
readUnits read 为 true 的单位数,帧里没有朗读进度时为 -1
matchedUnits matched 为 true 的单位数,帧里没有朗读进度时为 -1
totalUnits 单位总数,等于 progress 的长度,帧里没有朗读进度时为 -1
progress 参考文本的全部单位,取自帧里的 result.progress,按原文顺序排列,每项为 { word, read, matched },帧里没有朗读进度时为空数组
progress[].word 字或词,取自参考文本。中文按字,去掉标点与空白,英文按空白分词,去掉词首词尾的标点
progress[].read 朗读位置已越过这个单位
progress[].matched 识别结果与这个单位一致,为 true 时 read 也为 true
hasProgress 帧里带 result.progress 时为 true,只有 bytes 时为 false
raw 平台发来的原始帧
  • 同一个服务端会话里,read 与 matched 只会由 false 变为 true。read 为 true 而 matched 为 false,表示朗读已越过这个位置,但识别结果与参考文本不一致,可能是读错,漏读或识别误差,以终评为准。
  • 进度变化时平台立即下发一帧,此外每收约 0.5 秒音频下发一帧,进度一般比朗读晚 1 到 2 秒,随网络情况变化。end() 之后会话处于 ENDING,引擎对最后一段音频的识别结果仍以进度回调送达,终评总是这一轮的最后一个回调。
  • 朗读进度只对 16 kHz,16 位,单声道 PCM 生效,录音器的默认配置 sampleRate: 16000,numberOfChannels: 1,format: 'PCM' 配合 pipeTo 即满足要求。参考文本为空,音频不是这种格式,或平台当时无法跟踪进度时,进度帧只有 bytes,hasProgress 为 false,终评不受影响。
  • 声通兼容通道的进度帧另带 "recordId": "",kernel_version,resource_version 与声通格式的 words,words 只含上一帧之后新读过的单位,供按声通文档解析的代码使用。SDK 不解析这几项,逐字状态只取 result.progress,需要时从 raw.result.words 读取。断线重连后服务端从第一个单位重新下发 words,逐字状态以 progress 为准。原生通道的进度帧没有 words。
  • 断线重连:平台不支持会话续传,新的服务端会话在重放音频期间从零重新算出进度。会话记住上一次回调的 readUnits 与 matchedUnits,只回调两个数都不小于上一次的进度帧,其余丢弃,记一条 DEBUG 日志,默认的 REPLAY 策略下界面上的进度不会倒退。只有 bytes 的帧照常回调,每个新会话从零开始记。DROP 策略下新会话只评重连之后的音频,每次重连成功后会话清空记住的进度,之后的进度从新会话算起,可能小于断线前回调过的值。

示例:按 progress 给参考文本逐字着色,未读为灰色,已对上为绿色,读过但没对上为红色。第一个进度帧就列出全部单位,可以直接用来排版参考文本。this.client 与 this.recorder 的创建与释放见“生命周期”。

JavaScript
Page({
  data: { units: [], overall: null },

  start() {
    this.session = this.client.streamEvaluate(
      { coreType: 'sentence', referenceText: '我是中国人,我是中国人。我们都是中国人。中华民族是一家人。', language: 'zh-CN', realtimeFeedback: true },
      {
        onPartial: (p) => {
          if (!p.hasProgress) return;
          this.setData({
            units: p.progress.map((w, i) => ({ i, text: w.word, state: w.matched ? 'matched' : w.read ? 'mismatched' : 'unread' })),
          });
        },
        onResult: (result) => this.setData({ overall: result.overall }),
        onError: (err) => wx.showToast({ title: err.message, icon: 'none' }),
      },
    );
    this.recorder.pipeTo(this.session);
    this.recorder.start();
  },
});
HTML
<view class="ref-text">
  <text wx:for="{{units}}" wx:key="i" class="unit {{item.state}}">{{item.text}}</text>
</view>
文本
.unit.unread { color: #999999; }
.unit.matched { color: #1a7f37; }
.unit.mismatched { color: #cf222e; }

生命周期#

客户端,会话,录音器都有可以重复调用的释放接口:

对象 释放接口 行为
客户端 close() 取消未结束的会话,中止进行中的请求,释放 createRecorder 创建的录音器。之后的调用以错误码 90004 失败
会话 cancel(),close() 关闭连接,不再触发 onResult 与 onError,仍触发 onClosed
录音器 release() 停止录音,释放麦克风,清除监听器与推流关系,之后再调用 start,stop 等方法以错误码 90009 失败

创建,使用,释放的完整示例:

JavaScript
const { YuguClient, RecorderState } = require('@shengzhiai/yugu-miniprogram-sdk');

Page({
  data: { state: 'IDLE', overall: null },

  onLoad() {
    this.client = new YuguClient({ auth: { token: getApp().globalData.token } });
    this.recorder = this.client.createRecorder({ sampleRate: 16000, numberOfChannels: 1, format: 'PCM' });
    this.recorder.setListener({
      onError: (err) => wx.showToast({ title: '录音失败 ' + err.code, icon: 'none' }),
    });
  },

  start() {
    this.session = this.client.streamEvaluate(
      { coreType: 'sentence', referenceText: '今天天气很好', language: 'zh-CN' },
      {
        onStateChanged: (oldState, newState) => this.setData({ state: newState }),
        onReconnecting: (attempt, delayMs) => console.log('重连', attempt, delayMs),
        onResult: (result) => this.setData({ overall: result.overall }),
        onError: (err) => wx.showToast({ title: err.message, icon: 'none' }),
        onClosed: () => console.log('会话结束', this.session.getState()),
      },
    );
    this.recorder.pipeTo(this.session);
    this.recorder.start();
  },

  stop() {
    if (this.recorder.getState() === RecorderState.RECORDING) this.recorder.stop();
  },

  onUnload() {
    if (this.session) this.session.close();
    this.recorder.release();
    this.client.close();
  },
});

异常路径的清理:录音器在 stop(),release() 与每一种错误路径上都释放麦克风,推流中的录音器出错时取消对应会话。会话进入 COMPLETED,FAILED,CANCELLED 后立即关闭连接,清除全部定时器,进入 CLOSED。页面被销毁前没有调用释放接口时,客户端 close() 仍可以在任何时刻补调。

录音器#

配置 默认值 说明
sampleRate 16000 采样率
numberOfChannels 1 声道数
format PCM 录音格式,逐帧回调需要 PCM 或 mp3
frameSize 1 每次帧回调的大小,单位 KB
duration 300000 最长录音时长,单位毫秒,到时自动停止
audioSource 自动 音频输入源

录音器状态为 IDLE,RECORDING,PAUSED,STOPPED,RELEASED。stop() 返回 { tempFilePath, duration, fileSize, format, sampleRate, numberOfChannels },可以直接作为 evaluate 的 audio,SDK 会为 PCM 加上 WAV 头再上传。监听器回调为 onStateChanged,onStart,onPause,onResume,onStop,onFrame(frame, isLastFrame),onError,onInterruptionBegin,onInterruptionEnd,setListener(null) 注销监听器。

pipeTo(session) 把录音帧切成 640 字节一帧,即 16 kHz 单声道 20 毫秒,送入会话,录音停止时补发剩余音频,再调用 session.end()。会话提前结束时录音器自动停止。

wx.getRecorderManager() 返回全局共用的同一个对象,没有注销回调的方法。SDK 只在该对象上注册一次回调,再分发给当前占用麦克风的录音器,所以反复创建与释放录音器不会累积回调。同一时刻只有一个录音器可以录音,另一个录音器开始录音会以错误码 90202 失败。使用 SDK 录音器期间,业务代码不要再给 wx.getRecorderManager() 注册回调,否则会覆盖 SDK 的回调。

音频预检#

整段评测上传前与实时评测 end() 时,SDK 对 WAV 与 PCM 做本地检查,其他格式只检查大小。

错误码 条件 WARN 模式 REJECT 模式
90101 时长短于 1 秒 记为警告 上传前报错
90102 时长超过 300 秒,整段上传大于 50 MB,或实时评测一轮大于 10 MB 记为警告 上传前报错
90103 全程静音:峰值低于 200,均方根也低于 30 记为警告 上传前报错
90104 音量过低:均方根低于 -45 dBFS 记为警告 只记为警告
90105 不是 16 位 PCM WAV,或采样率低于 16000 记为警告 上传前报错

大小上限按用途区分:整段评测上传的上限为平台请求体上限 50 MB,实时评测一轮音频的上限为 10 MB,precheckAudio 默认按上传检查,传 stream: true 时按实时评测检查。90103 的阈值与平台的静音判定一致,被拦下的音频提交上去也只会得 0 分。警告放在结果的 localWarnings 里,实时评测另外回调 onWarning。REJECT 模式下实时评测在 end() 时发现问题,会话以 AudioQualityException 结束,不发结束帧,不计费。界面需要在评测前提示时,可以直接调用 precheckAudio:

JavaScript
const { precheckAudio } = require('@shengzhiai/yugu-miniprogram-sdk');
const report = precheckAudio(arrayBuffer); // 或 precheckAudio(pcm, { format: 'pcm', sampleRate: 16000, stream: true })
console.log(report.durationMs, report.rmsDbfs, report.warnings);

外部音频#

评测接口接受调用方自有的音频,不必使用 SDK 的录音器:

输入 写法
内存中的音频 audio: arrayBuffer 或 audio: uint8Array
本地文件 audio: tempFilePath,也可以是 wxfile:// 或用户目录下的路径,SDK 用 wx.getFileSystemManager().readFile 读取
带格式说明的文件 audio: { tempFilePath, format: 'pcm', sampleRate: 16000 }
原始 PCM 数据 audio: pcmArrayBuffer, audioFormat: 'pcm', sampleRate: 16000
SDK 录音结果 audio: await recorder.stop()

WAV,mp3,m4a,aac,ogg,webm 等容器按文件头识别后原样上传,原始 PCM 自动加 WAV 头。实时评测用 session.sendAudio 送入任意来源的 16 位小端 PCM,例如直播流或第三方采集模块的输出。

日志与指标#

日志级别为 OFF,ERROR,WARN,INFO,DEBUG,默认 WARN,输出到控制台,可以用 logger 改为自有的日志通道。日志里不出现 secretKey,签名,token 与音频数据,appKey 只保留前 4 个字符,实时评测的握手地址在记录前去掉凭据参数。

eventListener 的方法都可以不实现,回调里抛出的异常会被记录,不影响 SDK:

方法 时机
onRequestStart(op, method, path, attempt) 每次 REST 尝试开始
onRequestEnd(op, httpStatus, latencyMs, attempts, error) 一次逻辑调用结束,成功时 error 为空
onRetry(op, attempt, delayMs, error) 每次重试前
onSessionStateChanged(sessionId, oldState, newState) 实时评测会话状态变化
onReconnect(sessionId, attempt, succeeded) 每次重连尝试的结果
JavaScript
const client = new YuguClient({
  auth: { token },
  logLevel: 'INFO',
  logger: (level, tag, message) => wx.getRealtimeLogManager().info(level, tag, message),
  eventListener: {
    onRequestEnd: (op, status, latencyMs, attempts) => report(op, status, latencyMs, attempts),
  },
});

基础库与运行环境#

项 要求
小程序基础库 2.20.1 及以上
用到的接口 wx.request 的 timeout 与 responseType,wx.connectSocket 与 SocketTask,wx.getRecorderManager 的 PCM 录音与 onFrameRecorded,wx.getFileSystemManager
JavaScript 产物为 ES2017 语法的 CommonJS,不依赖 TextEncoder,crypto.subtle,AbortController,globalThis
开发者工具 支持构建 npm 的稳定版,“将 JS 编译成 ES5”打开或关闭都可以

签名所需的 SHA-256,HMAC,Base64 与 UTF-8 编码都由 SDK 用纯 JavaScript 实现。

测试与构建#

Shell
cd miniprogram
npm ci
npm test            # 构建,单元测试与集成测试,c8 行覆盖率门槛 70%
npm run typecheck   # 用 strict 配置检查 TypeScript 声明
bash ../ci/miniprogram.sh

集成测试启动仓库自带的平台模拟服务 tools/mock-server,覆盖整段评测,重试,幂等,实时评测断线与重连,朗读进度,不连接生产环境。覆盖率报告在 coverage/index.html。设置 YUGU_SANDBOX_APPKEY 与 YUGU_SANDBOX_SECRET 后另跑沙箱联调用例,见 SANDBOX.md。

常见问题#

原生整段评测报 HTTP 415#

这是 1.x 版本的缺陷:wx.uploadFile 不能给表单段设置 Content-Type,config 段以 text/plain 到达平台,平台回 415。2.0 自行拼装 multipart 请求体,config 段不带文件名,类型为 application/json; charset=utf-8,用 wx.request 发送,已修复,升级即可。

报 url not in domain list#

域名没有加入合法域名,按“合法域名”一节配置 request 与 socket 两类域名。错误归为 NetworkException,错误码 90001。

签名校验失败,错误码 2003#

检查 appKey 与 secretKey 是否成对,设备时间是否准确。平台要求时间戳与服务器时间相差不超过 300 秒。

录音失败,错误码 90201#

用户拒绝了麦克风授权,或者小程序没有在用户隐私保护指引中声明麦克风。可以用 wx.openSetting 引导用户重新授权。

实时评测一直在重连#

先看 onReconnecting 的 cause:NetworkException 多为网络中断,RequestTimeoutException 多为心跳或终评超时。重连次数用尽后会话以错误码 90006 结束,不会停在中间状态。默认的 8 次约覆盖 23 秒的中断,网络更差的场景可以调大 reconnect.maxAttempts。

同一个幂等键报 40903#

一个幂等键只能用于一个请求,参数或音频变化后要换新键。SDK 自动生成的键每次调用都不同,只有调用方显式传入同一个键时才会出现。

分数为 0#

查看 result.warnings 与 result.localWarnings:1001 或 90103 表示没有有效语音,应提示用户重录。开启 audioPrecheck: 'REJECT' 可以在上传前拦下静音与过短的录音,不产生计费。

打包体积#

miniprogram_dist/index.js 约 130 KB,未压缩,上传代码时开发者工具会再压缩。SDK 没有运行时依赖。

版本与许可#

变更记录见 CHANGELOG.md,各端版本对应关系见仓库根目录的 COMPATIBILITY.md,由 1.x 升级见 MIGRATION-2.0.md。许可为 Apache-2.0,见 LICENSE 与 NOTICE。

更新于 2026-10-12