| 项 | 内容 |
|---|---|
| 坐标 | com.shengzhiai.yugu:yugu-android-sdk:2.1.0 |
| 仓库 | https://open.shengzhiai.com/maven/,公开读取,不需要账号 |
| 包名 | com.shengzhiai.yugu |
| 最低系统 | Android 5.0,minSdk 21 |
| 字节码 | Java 8,Kotlin 1.9 |
| 运行时依赖 | OkHttp 4.12,Kotlin 标准库 |
| 许可 | Apache License 2.0 |
能力:整段评测,声通兼容整段评测,语音合成,报告查询,原生实时评测,声通兼容实时评测,实时评测的朗读进度,录音器。
安装#
在 settings.gradle 里加入优谷雅言 Maven 仓库:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url "https://open.shengzhiai.com/maven/" }
}
}在模块的 build.gradle 里声明依赖:
dependencies {
implementation "com.shengzhiai.yugu:yugu-android-sdk:2.1.0"
}Kotlin DSL 写法:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://open.shengzhiai.com/maven/") }
}
}
// app/build.gradle.kts
dependencies {
implementation("com.shengzhiai.yugu:yugu-android-sdk:2.1.0")
}工程仍在顶层 build.gradle 的 allprojects 里声明仓库时,把同一行 maven { url "https://open.shengzhiai.com/maven/" } 加进那里。
仓库里的每个版本发布后不再改动,依赖写成具体版本号即可锁定,需要回退时改回历史版本号。每个文件都附带 .md5,.sha1,.sha256,.sha512 校验和,另有源码包 -sources.jar 与接口文档包 -javadoc.jar。1.x 版本没有发布到 Maven 仓库,源码包仍可在 https://open.shengzhiai.com/sdk/ 下载,已停止维护。
SDK 自带 R8 混淆保留规则,接入方不需要额外配置。
权限#
SDK 的清单文件已声明 INTERNET,构建时自动合并。使用录音器时,宿主应用需要声明录音权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />Android 6.0 起录音权限需要在运行时申请:
if (Build.VERSION.SDK_INT >= 23 &&
checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 1001)
}把 Context 传给 Recorder 后,SDK 在开始录音前检查权限,没有权限时抛 PermissionException,错误码 90201。
五分钟快速开始#
第一步,创建客户端。整个应用共用一个实例即可。
val client = YuguClient.builder()
.auth(Auth.appKey("YOUR_APP_KEY", "YOUR_SECRET_KEY"))
.build()第二步,准备一段 16 kHz,16 位,单声道的 WAV 音频,调用整段评测。REST 方法是阻塞调用,放在后台线程执行:
thread {
val result = client.evaluate(
EvaluateConfig(CoreType.SENTENCE, "今天天气很好", Language.ZH_CN),
File(filesDir, "sample.wav"),
)
Log.i("yugu", "总分 ${result.overall},发音 ${result.dimensions.pronunciation}")
}也可以使用异步方法,回调默认在主线程执行,可以直接更新界面:
client.evaluateAsync(
EvaluateConfig(CoreType.SENTENCE, "今天天气很好", Language.ZH_CN),
AudioInput.fromFile(File(filesDir, "sample.wav")),
object : YuguCallback<EvalResult> {
override fun onSuccess(result: EvalResult) {
scoreView.text = "总分 ${result.overall}"
}
override fun onFailure(error: YuguException) {
scoreView.text = "评测失败 ${error.code} ${error.message}"
}
},
)第三步,应用退出或不再评测时关闭客户端:
client.close()完整的可运行工程见 demos/android-demo,包含录音评测,示例音频评测与实时评测。
客户端选项#
| 选项 | 默认值 | 含义 |
|---|---|---|
baseUrl |
https://open.shengzhiai.com |
REST 基址 |
wsBaseUrl |
wss://open.shengzhiai.com |
实时评测基址 |
auth |
必填 | Auth.appKey(appKey, secretKey) 或 Auth.token(jwt) |
connectTimeoutMs |
10000 | 每个地址的 TCP 与 TLS 建连超时,实时评测连接打开后等待开始帧的时限,实时评测建连与升级以 3 倍为上限 |
readTimeoutMs |
120000 | 单次 REST 尝试的读取超时 |
totalTimeoutMs |
300000 | 一次逻辑调用的总时限,含全部重试与等待 |
retryPolicy |
见重试一节 | REST 重试策略 |
autoIdempotencyKey |
true | 调用方未指定幂等键时自动生成 |
logLevel |
WARN |
日志级别 |
logger |
写入 Logcat | 日志输出 |
eventListener |
无 | 指标回调 |
audioPrecheck |
WARN |
本地音频预检方式 |
strictAudio |
false | 整段评测结果带 1001 警告时抛异常 |
userAgent |
yugu-android-sdk/2.1.0 |
请求头 User-Agent |
reconnectPolicy |
见重连一节 | 实时评测重连策略 |
audioBufferPolicy |
REPLAY |
实时评测断线期间的音频处理方式 |
resultTimeoutMs |
300000 | 实时评测调用 end() 后等待终评的时限 |
pingIntervalMs |
15000 | 实时评测心跳间隔 |
maxReplayBytes |
10 MB | 重放缓冲上限 |
callbackExecutor |
主线程 | 实时评测与异步方法的回调线程 |
okHttpClient |
SDK 共用实例 | 自定义代理与证书时传入,SDK 在其基础上派生超时设置 |
单次调用的选项用 RequestOptions 传入:idempotencyKey 幂等键,totalTimeoutMs 总时限,readTimeoutMs 读取超时,retryPolicy 重试策略,cancellationToken 取消令牌。实时会话的选项用 SessionOptions 传入:idempotencyKey,reconnectPolicy,audioBufferPolicy,resultTimeoutMs,extraQuery。
接口概览#
| 方法 | 接口 | 说明 |
|---|---|---|
evaluate(config, audio, options, image) |
POST /api/v1/evaluate |
原生整段评测,音频可为 AudioInput,ByteArray,File,InputStream |
evaluateCompat(config, audio, options) |
POST /{coreType} |
声通兼容整段评测,coreType 取声通命名 |
tts(request, options) |
POST /api/v1/tts/generate |
语音合成,返回可直接播放的 resolvedUrl |
getReport(recordId, options) |
GET /api/v1/report/{recordId} |
报告查询 |
streamEvaluate(config, listener, options) |
WS /api/v1/ws/evaluate |
原生实时评测,返回 StreamSession,可开启朗读进度 |
streamEvaluateCompat(config, listener, options) |
WS /{coreType} |
声通兼容实时评测,可开启朗读进度 |
evaluateAsync,evaluateCompatAsync,ttsAsync,getReportAsync |
同上 | 异步版本,返回可取消的 YuguCall |
close() |
无 | 关闭客户端,可重复调用 |
原生 coreType:word,sentence,passage,connected,open,alpha,pinyin。声通兼容 coreType:word.eval,word.eval.pro,sent.eval,sent.eval.pro,para.eval,alpha.eval,word.eval.cn,sent.eval.cn,para.eval.cn,pinyin。
EvaluateConfig 字段与平台 config 段一一对应:coreType,referenceText,language,includeReport,includeStandardAudio,includeAsrText,slack,scale,precision,agegroup,toneWeight,refPinyin,phonemeOutput,taskType,paragraphNeedWordScore。平台新增而 SDK 尚未列出的字段放进 extra。值为 null 的字段不发送,由平台取默认值。另有 realtimeFeedback,默认 false,只对原生实时评测有效,为 true 时开始帧带 "realtimeFeedback": true,打开朗读进度,整段评测的 config 段不带该字段。
看图说话需要上传图片:
client.evaluate(
EvaluateConfig(CoreType.OPEN, "描述这张图片", Language.ZH_CN, taskType = TaskType.PICTURE),
AudioInput.fromFile(wavFile),
RequestOptions.DEFAULT,
ImageInput.fromFile(jpgFile),
)各评测模式的取分字段#
结果统一解析为 EvalResult。raw 为整个响应,resultRaw 为 result 节点,rawJson 为响应原文,SDK 未建模的字段可从这三处读取。平台各字段的含义见 评测结果字段。
| 模式 | 总分 | 分项 | 明细 |
|---|---|---|---|
word,sentence,alpha |
overall |
dimensions.pronunciation,fluency,integrity,rhythm,accuracy,readingSkill |
words,sentences,asrText |
中文 sentence,pinyin |
overall |
同上,另有 tone 声调与 emotion |
words 带 pinyin,symbolPinyin,tone,toneScore |
passage |
overall |
同 sentence |
sentences 按句给分,配置 paragraphNeedWordScore = 1 时每句带逐字 words |
connected |
overall,取 result.connected_overall |
connected.linking,rhythm,elision,reduction |
connected.boundaries 每个词间边界的 between,tags,realized,startMs,endMs |
open |
overall |
open.content,open.languageUse,open.delivery |
open.transcript,open.feedback,open.audit |
| 声通兼容 | overall |
同原生对应模式 | words,sentences |
其余公共字段:recordId 评测记录号,speed 语速,rearTone 句末调型,duration 与 durationSeconds 时长,coreType 与 language 平台回传的模式与语种,report 评测报告,standardAudio 示范音,warnings 平台警告,localWarnings 本地预检警告,idempotencyKey 本次使用的幂等键,replayed 是否为幂等重放。
report.dimensionScores 里值为 null 的维度表示本次不评,不等于零分。时间单位:span 为 10 毫秒,boundaries 为毫秒。
错误处理#
全部错误都是 YuguException 的子类,带 category 类别,code 错误码,httpStatus,message,retryable,idempotencyKey,recordId,attempts 尝试次数,rawBody 原始响应体,最多 4 KB。错误码含义见 ERRORS.md,SDK 内置同一份表,常量在 com.shengzhiai.yugu.errors.ErrorCodes。
| 异常类型 | 类别 | 典型错误码 |
|---|---|---|
NetworkException |
NETWORK |
90001 网络错误,90006 重连用尽,90011 证书校验失败 |
RequestTimeoutException |
TIMEOUT |
90002 超时,90007 等待终评超时,HTTP 408 |
AuthException |
AUTH |
40100,2001 到 2011 |
PermissionException |
PERMISSION |
40300,1004,1005,1012,1013,90201 |
InvalidParameterException |
INVALID_PARAM |
40001,90010 |
NotFoundException |
NOT_FOUND |
40400 |
ConflictException |
CONFLICT |
40900,40901,40903,1311 |
RateLimitException |
RATE_LIMIT |
42900,42901,3001 到 3003 |
QuotaExceededException |
QUOTA |
40902,42902,42903,2012,1310 |
ServerException |
SERVER 或 UPSTREAM |
50000,50010,50200 |
AudioQualityException |
AUDIO |
90101 到 90105,开启 strictAudio 后的 1001 |
IllegalSessionStateException |
STATE |
90004 客户端已关闭,90008,90009,90202,90203 |
RequestCancelledException |
CANCELLED |
90003 |
ProtocolViolationException |
PROTOCOL |
90005 |
映射顺序:SDK 本地错误,响应体里的非零 code 查错误码表,FastAPI 形态 detail 里的 [2001] 前缀,最后按 HTTP 状态归类。错误码不在错误码表里时保留原码,类别与可重试按 HTTP 状态判定。HTTP 状态不在 ERRORS.md 的兜底表里时按类别归类:其余 4xx 为 INVALID_PARAM,不重试,其余 5xx 为 SERVER,只有列在可重试状态里的才重试,其他状态为 UNKNOWN。
try {
client.evaluate(config, wavFile)
} catch (e: AuthException) {
// 凭据错误,重试无效,提示检查 appKey 与 secretKey
} catch (e: AudioQualityException) {
// 音频不合格,提示重录
} catch (e: YuguException) {
if (YuguErrors.isRetryable(e)) {
// SDK 已按策略重试过,仍失败,可稍后再试
}
Log.w("yugu", e.toString())
}平台的音频质量警告 1001 到 1005 与 1009 不抛异常,列在 result.warnings,WarningCode 枚举这些码。需要把警告当异常处理时,用 YuguErrors.fromWarning(code) 构造 AudioQualityException。开启 strictAudio 后,整段评测结果带 1001 未检测到有效音频时直接抛出。
错误码按场景查表,不只看数字:错误响应体与错误帧里的 code,以及 YuguErrors.fromCode,查错误码表,结果里的警告与 YuguErrors.fromWarning 查警告码表,90000 起的本地码查本地码表。1004 与 1005 因此在错误响应里是账户状态,在结果的警告列表里是音频问题。只出现在警告码表里的码,例如 1001,fromCode 同样构造 AudioQualityException。
重试与幂等#
重试策略#
REST 调用失败后,SDK 按 RetryPolicy 自动重试:
| 字段 | 默认值 |
|---|---|
maxRetries |
2,最多 3 次尝试 |
initialDelayMs |
200 |
multiplier |
2.0 |
maxDelayMs |
4000 |
jitter |
0.3 |
respectRetryAfter |
true |
maxRetryAfterMs |
30000 |
第 n 次重试前的等待时长:
delay(n) = min(maxDelayMs, initialDelayMs * multiplier^(n-1)) * (1 + U(-jitter, +jitter))
带 Retry-After 时 delay = max(delay, min(retryAfterMs, maxRetryAfterMs))
now + delay 超过总时限时停止重试,抛出最后一次的错误默认值给出约 200 毫秒与 400 毫秒两次重试,各自上下浮动 30%。
是否重试由 YuguErrors.isRetryable 判定,SDK 内部重试与实时评测重连共用同一个判定函数,规则按顺序第一条命中为准:
- 本地错误码 90001,90002,90007 可重试,其余本地错误码不可重试。
- 有平台错误码时取错误码表里的可重试标记,例如 40901,42900,42901,50000,50200 可重试。
- 没有错误码或错误码不认识时,HTTP 408,425,429,500,502,503,504 可重试。
参数错误,鉴权失败,额度不足等不会重试。每次重试都会回调 eventListener.onRetry,同时写一条 WARN 日志,例如 retry 1/2 in 231 ms: HTTP 503 code=50200。
一次尝试指一次发出的请求。连接域名的某个地址失败时,请求还没有发出,SDK 在同一次尝试内改连下一个地址,例如 IPv6 不通时改走 IPv4,不计入重试次数。请求开始发送之后的重发只由 SDK 按策略执行,带同一个幂等键与新的 X-Nonce。
幂等键#
写操作 evaluate,evaluateCompat,tts 带请求头 Idempotency-Key,实时评测在握手参数与开始帧里带 idempotencyKey。
- 调用方不指定时,SDK 为每次逻辑调用生成 32 位小写十六进制键。
- 同一次调用的全部重试与实时评测的全部重连都复用同一个键,重试循环里不会生成新键。
- 调用方可通过
RequestOptions(idempotencyKey = "...")或SessionOptions(idempotencyKey = "...")指定键,键须为 1 到 200 个可见 ASCII 字符,不符合时在发请求前抛InvalidParameterException,错误码 90010。 - 作用域为 appKey,使用 token 鉴权时为用户。有效期 24 小时。
- 平台语义:同一个键加同一份请求,重放首次成功的结果,不再计费,结果的
replayed为 true。首次请求仍在处理时,平台最多等待 30 秒后重放,或返回 409 错误码 40901,同时带Retry-After,SDK 会按同一个键重试。同一个键用于另一份不同的请求时返回 40903,不可重试。首次请求失败时键被释放,可以用同一个键重新提交。 - 关闭
autoIdempotencyKey后,未指定键的写请求不重试,实时会话不重连,避免重复计费,DEBUG 日志会说明原因。getReport是只读请求,不带幂等键,按同样的规则重试。
取消与总时限#
totalTimeoutMs 限制一次逻辑调用的总耗时,包括全部重试与等待,超时抛 RequestTimeoutException。取消令牌在任何线程都可以调用:
val token = CancellationToken()
val options = RequestOptions(cancellationToken = token)
// 另一个线程
token.cancel() // 调用方收到 RequestCancelledException,错误码 90003实时评测与重连#
会话状态#
StreamSession.getState() 返回 SessionState:
| 状态 | 含义 |
|---|---|
IDLE |
刚创建 |
CONNECTING |
正在建立连接 |
CONNECTED |
收到服务端 connected,已发出开始帧 |
STARTED |
收到服务端 started,音频实时发送 |
ENDING |
已发出结束帧,等待终评 |
RECONNECTING |
连接中断,等待重连 |
COMPLETED |
收到终评结果 |
FAILED |
出现不可恢复的错误或重连用尽 |
CANCELLED |
调用方调用了 cancel() |
CLOSED |
连接已关闭,终态 |
正常流程的回调顺序:
onStateChanged(IDLE, CONNECTING)
onStateChanged(CONNECTING, CONNECTED) onConnected()
onStateChanged(CONNECTED, STARTED) onStarted()
onStateChanged(STARTED, ENDING)
onStateChanged(ENDING, COMPLETED) onResult(result)
onStateChanged(COMPLETED, CLOSED) onClosed(1000, "completed")回调保证:
- 每个会话最终回调一次
onResult或一次onError,先调用cancel()时两者都不回调。 onClosed总是最后一个回调。- 同一会话的回调不会并发执行,默认在主线程执行。
- 回调里
getState()的返回值等于最近一次onStateChanged通知的新状态,界面可以随时查询状态。 onConnected与onStarted只在首次连接时回调,之后的重连只回调onReconnecting与onReconnected,在onStarted里开始录音的代码不会被重复执行。
示例#
val session = client.streamEvaluate(
EvaluateConfig(CoreType.SENTENCE, "今天天气很好", Language.ZH_CN),
object : StreamListener {
override fun onStateChanged(oldState: SessionState, newState: SessionState) {
stateView.text = newState.name
}
override fun onReconnecting(attempt: Int, delayMs: Long, cause: YuguException) {
stateView.text = "网络中断,第 $attempt 次重连"
}
override fun onResult(result: EvalResult) {
scoreView.text = "总分 ${result.overall}"
}
override fun onError(error: YuguException) {
scoreView.text = "评测失败 ${error.code}"
}
},
)
recorder.start(session) // 每 20 毫秒送一帧 640 字节的 PCM
// 读完以后
recorder.stop()
session.end()声通兼容实时评测用 streamEvaluateCompat(CompatConfig(...), listener)。两种实时评测都可以打开朗读进度,原生实时评测在 EvaluateConfig 里设 realtimeFeedback = true,声通兼容实时评测在 CompatConfig 里设 realtimeFeedback = true,服务端在收音过程中逐字或逐词告知参考文本读到了哪里,通过 onPartial 回调,详见朗读进度一节。
朗读进度#
打开朗读进度后,onPartial(partial) 在收音过程中多次回调,PartialResult 的字段:
| 字段 | 含义 |
|---|---|
hasProgress |
中间帧带朗读进度时为 true,为 false 时只有 bytes |
progress |
ReadingWord 列表,参考文本按原文顺序逐字或逐词列出,中文按字,英文按词,去掉标点,没有进度时为空列表 |
progress[i].word |
字或词,取自参考文本 |
progress[i].read |
朗读位置已越过这个字或词 |
progress[i].matched |
识别结果与这个字或词一致,为 true 时 read 也为 true |
readUnits |
read 为 true 的单位数,没有进度时为 -1 |
matchedUnits |
matched 为 true 的单位数,没有进度时为 -1 |
totalUnits |
单位总数,等于 progress 的长度,没有进度时为 -1 |
bytes |
服务端这一轮已收到的音频字节数 |
raw |
整个中间帧。声通兼容通道的进度帧另带声通格式的增量 result.words,只列出上一帧之后新读过的字或词,断线重连后从第一个字或词重新下发,SDK 不解析,需要时从这里读取 |
按朗读进度给参考文本着色:
val refText = "我是中国人,我是中国人。我们都是中国人。中华民族是一家人。"
val session = client.streamEvaluate(
EvaluateConfig(CoreType.SENTENCE, refText, Language.ZH_CN, realtimeFeedback = true),
object : StreamListener {
override fun onPartial(partial: PartialResult) {
if (!partial.hasProgress) return
val text = SpannableString(refText)
var from = 0
for (w in partial.progress) {
val start = refText.indexOf(w.word, from) // progress 去掉了标点,按顺序在原文里找
if (start < 0) break
from = start + w.word.length
val color = when {
w.matched -> Color.rgb(46, 125, 50) // 已读,识别一致
w.read -> Color.rgb(198, 40, 40) // 已读,识别不一致
else -> Color.GRAY // 未读
}
text.setSpan(ForegroundColorSpan(color), start, from, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
refTextView.text = text
}
override fun onResult(result: EvalResult) {
scoreView.text = "总分 ${result.overall}"
}
override fun onError(error: YuguException) {
scoreView.text = "评测失败 ${error.code}"
}
},
)- 一轮之内
read与matched只会由 false 变为 true。read为 true 而matched为 false,表示朗读已越过这个位置,但识别结果与参考文本不一致,可能是读错,漏读或识别误差,以终评为准。 - 进度一般比朗读晚 1 到 2 秒。调用
end()后,最后一段音频的进度仍会在ENDING状态下回调,终评是最后一个结果,onResult之后不再回调onPartial。 - 进度只对 16 kHz,16 位,单声道 PCM 生效,录音器默认采集这种格式的音频。参考文本为空,音频是其他格式,或服务端暂时无法跟踪进度时,
hasProgress为 false,中间帧只有bytes,终评不受影响。 - 断线重连后,服务端在新会话上从零重新计算进度。
REPLAY策略下重放音频期间进度会低于重连前,会话记住上一次回调的readUnits与matchedUnits,只回调两者都不小于上一次的进度帧,其余的丢弃,写一条 DEBUG 日志,界面上的进度不会倒退。DROP策略下新会话只评测重连之后的音频,每次重连成功后,新会话的进度从第一帧起照常回调,可能低于重连前。不带进度的中间帧照常回调。新的会话从零开始。 - 进度帧不含发音评分。终评与不打开朗读进度时相同,不额外计费。
重连策略#
| 字段 | 默认值 |
|---|---|
enabled |
true |
maxAttempts |
8,连续重连失败次数的上限,重连成功后从零算起 |
initialDelayMs |
500 |
multiplier |
2.0 |
maxDelayMs |
4000 |
jitter |
0.3 |
默认依次等待约 0.5,1,2,4,4,4,4,4 秒,合计约 23 秒,可以扛过 10 秒左右的断网。连续失败满 maxAttempts 次时会话以 90006 结束。为保证会话一定结束,一次会话的重连总次数另有上限,为 maxAttempts 的 3 倍,默认 24 次,服务端反复接受又断开连接时会话以 90006 结束,不会无限重连。
触发重连的情况:传输层故障,连接以 1001,1011 等非协议类关闭码断开,心跳超时,握手超时,等待终评超时,服务端错误帧的错误码可重试,例如 40901,42901,50200。直接进入 FAILED 不重连的情况:不可重试的错误帧,鉴权失败的握手,服务端在终评前以 1000 关闭,关闭码为 1002,1003,1007,1008,1009,1010 或 4000 到 4999,后两类的错误码为 90005。
平台不支持会话续传,重连语义如下:重连后是一个新的服务端会话,SDK 用同一组参数与同一个幂等键重新握手,重发开始帧,再按缓冲策略补发音频。同一个幂等键保证重连不会重复计费。
断线期间的音频#
| 策略 | 行为 |
|---|---|
REPLAY |
默认。保留本次会话已送入的全部音频,上限 10 MB。重连后先重放缓冲,再补发断线期间送入的音频,已调用 end() 时再发结束帧。缓冲超过上限后,下一次需要重连时会话以 90008 失败 |
DROP |
不缓冲。断线期间送入的音频丢弃,新会话只评测重连后送入的音频,onReconnected 的 droppedBytes 给出丢弃的字节数。已发出结束帧后断线,会话直接失败 |
FAIL |
不重连,传输层故障直接进入 FAILED |
心跳与超时#
- 心跳:每 15 秒发一次 WebSocket ping,到下一次发 ping 时仍未收到上一次的 pong 即判定连接失效,最长 30 秒,随后按重连策略处理。
- 握手:建连与升级须在
connectTimeoutMs的 3 倍内完成,一个地址连不上时改连下一个地址,例如 IPv6 不通时改走 IPv4。连接打开后须在connectTimeoutMs内收到started。 - 终评:调用
end()后resultTimeoutMs内未收到结果,按 90007 处理,可重连时用同一个幂等键重放音频取回结果。
区分中断与结束#
| 现象 | 回调 |
|---|---|
| 重连中 | onReconnecting(attempt, delayMs, cause),状态为 RECONNECTING |
| 重连成功 | onReconnected(attempt, droppedBytes) |
| 重连失败 | onError,错误码 90006,cause 为最后一次的原因 |
| 评测正常结束 | onResult,状态为 COMPLETED |
| 连接关闭 | onClosed(code, reason),正常结束与取消为 1000,传输层故障为 1006,服务端主动关闭时为其关闭码 |
生命周期#
客户端,会话与录音器都有显式的释放方法,全部可以重复调用:
| 对象 | 释放 | 释放后 |
|---|---|---|
YuguClient |
close() |
取消进行中的会话与请求,再调用任何方法抛 IllegalSessionStateException,错误码 90004 |
StreamSession |
cancel() 或 close() |
进入 CANCELLED 与 CLOSED,不再回调 onResult 与 onError |
Recorder |
release() 或 close() |
停止录音,释放麦克风,注销监听器 |
创建,使用,释放的完整示例:
class EvalActivity : Activity() {
private lateinit var client: YuguClient
private lateinit var recorder: Recorder
private var session: StreamSession? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 创建
client = YuguClient.builder().auth(Auth.token(tokenFromYourServer)).build()
recorder = Recorder(this)
recorder.setListener(object : Recorder.Listener {
override fun onLevel(level: Int) {
runOnUiThread { volumeBar.progress = level }
}
override fun onError(error: YuguException) {
session?.cancel()
}
})
}
// 使用:每道题一个会话
fun startQuestion(refText: String) {
val s = client.streamEvaluate(EvaluateConfig(CoreType.SENTENCE, refText, Language.ZH_CN), listener)
session = s
recorder.start(s)
}
fun finishQuestion() {
recorder.stop()
session?.end()
}
override fun onDestroy() {
// 释放
recorder.release()
session?.cancel()
client.close()
super.onDestroy()
}
}录音器的状态为 IDLE,RECORDING,PAUSED,STOPPED,RELEASED,可用 getState() 查询。stop(),release() 与每一条出错路径都会释放麦克风:初始化失败抛 90202,没有权限抛 90201,录音中读取失败时状态回到 STOPPED,回调 onError,错误码 90203。pause() 停止采集,resume() 继续。录到的音频用 pcm(),toWav(),writeWav(file) 读取,默认最长 300 秒,到时自动停止。setListener(null) 或 removeListener() 注销监听器。
异常退出时的清理:客户端与会话占用的线程,连接与定时器都随 close() 与 cancel() 释放,SDK 的后台线程空闲时自动退出。进程被系统回收时麦克风与连接由系统回收,下次启动重新创建客户端即可。连续创建与释放客户端 100 次,线程与内存不会累积增长,单元测试覆盖了这一场景。
音频预检#
整段评测上传前与实时评测调用 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 | 警告 | 抛异常 |
默认为 WARN,警告写进 result.localWarnings,实时评测另外回调 onWarning。REJECT 模式在上传前抛 AudioQualityException,不产生计费。静音阈值与平台的静音判定一致,本地判为静音的音频,平台也会给零分。OFF 关闭预检。
外部音频#
评测接口接受调用方自有的音频,录音器可以不用:
| 来源 | 写法 |
|---|---|
| 字节数组 | AudioInput.fromBytes(bytes) 或 client.evaluate(config, bytes) |
| 文件 | AudioInput.fromFile(file) 或 client.evaluate(config, file) |
| 输入流 | AudioInput.fromStream(stream),读到结尾,流由调用方关闭 |
| 裸 PCM | AudioInput.fromPcm(pcm, 16000, 1),上传前自动加 WAV 头 |
| 实时音频 | session.sendAudio(pcm),16 kHz,16 位,单声道,推荐每帧 640 字节,超过 32000 字节的块自动拆成多帧发送 |
文件名决定上传时的 Content-Type,例如 .wav 为 audio/wav,.mp3 为 audio/mpeg,也可以在构造时传入。平台支持 WAV 与 MP3,采样率不低于 16 kHz。直播流与第三方采集的 PCM 可以直接用 sendAudio 送进实时会话。
签名规则#
使用 appKey 与 secretKey 鉴权时,SDK 自动计算签名:业务参数丢弃空值,按参数名字典序拼成 k1=v1&k2=v2,不做 URL 编码,签名值为 Base64(HMAC_SHA256(payload, secretKey))。各接口的业务参数:
| 接口 | 参与签名的参数 |
|---|---|
| 原生整段评测 | {config: config 段原文},config 段不带文件名,Content-Type: application/json; charset=utf-8 |
| 声通兼容整段评测 | 实际发送的文本表单字段,音频不参与 |
| 语音合成 | 请求体顶层非空标量字段,值与请求体文本一致,数字写成不带指数的小数 |
| 报告查询 | 空集 |
| 实时评测握手 | 除 signature 以外的全部 query 参数,含 appKey,timestamp,nonce,idempotencyKey |
请求头 X-Timestamp 与 X-Nonce 每次尝试重新生成,签名对应的业务参数不变。Signer 公开可用,共享签名向量 spec/fixtures/sign/vectors.json 由单元测试逐条验证。
日志与指标#
日志级别为 OFF,ERROR,WARN,INFO,DEBUG,默认 WARN,默认输出到 Logcat,标签为 YuguSDK。自定义输出:
val client = YuguClient.builder()
.auth(auth)
.logLevel(LogLevel.DEBUG)
.logger { level, tag, message, error -> println("$level $tag $message ${error ?: ""}") }
.build()日志不包含 secretKey,签名值,token 与音频字节,appKey 只输出前 4 位加 ***,地址里的 signature 与 token 参数显示为 ***。
指标回调 YuguEventListener 的方法全部可选:
| 方法 | 时机 |
|---|---|
onRequestStart(op, method, path, attempt) |
REST 每次尝试发出前 |
onRequestEnd(op, httpStatus, latencyMs, attempts, error) |
REST 每个逻辑调用结束时,latencyMs 含重试,attempts 为总尝试次数 |
onRetry(op, attempt, delayMs, error) |
每次重试前 |
onSessionStateChanged(sessionId, oldState, newState) |
实时会话状态变化 |
onReconnect(sessionId, attempt, succeeded) |
实时会话每次重连的结果 |
op 取值:evaluate,evaluateCompat,tts,getReport。
测试与覆盖率#
cd android
./gradlew test
./gradlew :yugu-android-sdk:jacocoTestReport测试包括签名向量,参数序列化与校验,multipart 组装,各模式结果解析,朗读进度帧解析,全部错误码与警告码的映射,幂等键生成与复用,重试判定与退避计算,实时会话状态机,重连后的进度过滤,三种缓冲策略,生命周期,录音器与音频预检。集成测试自动启动平台模拟服务 tools/mock-server,需要 Node.js 20,覆盖幂等计费,重试,服务端断开,异常关闭,服务端无响应,客户端断网 10 秒,网络切换与两条通道的朗读进度。覆盖率报告在 yugu-android-sdk/build/reports/jacoco/jacocoTestReport/html/index.html,行覆盖率门槛为 70%。持续集成脚本为 ci/android.sh。
源码构建与本地发布#
需要 JDK 17 与 Android SDK Platform 34,ANDROID_HOME 指向 SDK 目录,或在 local.properties 里写 sdk.dir。
cd android
./gradlew :yugu-android-sdk:assembleRelease
./gradlew :yugu-android-sdk:publishToYuguDir -PyuguPublishDir=/absolute/path/to/maven-repo发布目录按 Maven 仓库布局写入 AAR,POM,Gradle 模块元数据,源码包,接口文档包与各自的校验和,可直接合并到 Maven 仓库,也可用 maven { url "file:///absolute/path/to/maven-repo" } 引用。
常见问题#
主线程调用阻塞方法#
evaluate,evaluateCompat,tts,getReport 会发起网络请求,在主线程调用会被系统拒绝。在后台线程调用,或改用对应的 Async 方法,Kotlin 协程中可写 withContext(Dispatchers.IO) { client.evaluate(config, file) }。
secretKey 放在客户端的风险#
secretKey 写进安装包后可能被反编译取出。正式应用建议由自己的服务端签发 token,客户端使用 Auth.token(jwt),appKey 与 secretKey 只在集成与测试阶段放在客户端。
回调所在的线程#
实时评测回调与异步方法回调默认在主线程,可以直接更新界面。录音器的 onFrame 与 onLevel 在采集线程上回调,更新界面需要切回主线程。需要其他线程时设置 callbackExecutor。
网络抖动导致的重复计费#
SDK 的重试与重连都复用同一个幂等键,平台对同一个键只计费一次。业务层自己重新提交同一段音频时,传入同一个 idempotencyKey 也能避免重复计费。
录音器启动失败#
错误码 90201 表示没有录音权限,90202 表示麦克风被其他应用占用或设备不支持 16 kHz 单声道录音,都不会占住麦克风。
IPv6 不通的网络#
部分网络分配了 IPv6 地址,实际不通。连接失败的地址会被跳过,同一次尝试内改连域名的下一个地址,调用方不需要处理。IPv6 地址拒绝连接时改连立即完成,丢包时每次连接先等满一次 connectTimeoutMs,这类网络里可以适当调小 connectTimeoutMs。
代理与自定义证书#
通过 okHttpClient(client) 传入配置好代理或证书的 OkHttpClient,SDK 在其基础上派生超时与心跳设置,不会关闭传入的实例。证书校验失败抛 NetworkException,错误码 90011,不会重试。
Java 调用#
客户端与配置类都提供构建器,例如 EvaluateConfig.builder(CoreType.SENTENCE, "今天天气很好").language(Language.ZH_CN).build(),打开朗读进度再加 .realtimeFeedback(true)。监听器接口的可选方法是 Java 默认方法,只需实现 onResult 与 onError。朗读进度在 Java 里写作 partial.hasProgress(),partial.getProgress(),word.getRead(),word.getMatched()。异常类型都继承 RuntimeException,按需捕获 YuguException。
沙箱与测试密钥#
沙箱环境,测试密钥的申请方式与每日调用额度见 SANDBOX.md。
从 1.x 升级#
包名由 tech.dragonai.yugu 改为 com.shengzhiai.yugu,默认基址改为 https://open.shengzhiai.com,详见 变更记录 与 升级到 2.0。
版本兼容#
各端版本的完整对照见 版本兼容。
| SDK 版本 | 接口版本 | Android | 编译环境 |
|---|---|---|---|
2.1.0 |
API v1,契约 v2.1 | 5.0 及以上,minSdk 21 |
Java 8 字节码,Kotlin 1.9,OkHttp 4.12 |
2.0.0 |
API v1,契约 v2 | 5.0 及以上,minSdk 21 |
Java 8 字节码,Kotlin 1.9,OkHttp 4.12 |
1.0.0 |
API v1 | 7.0 及以上,minSdk 24 |
源码包,已停止维护 |
许可#
按 Apache License 2.0 授权,全文见 LICENSE,第三方名称说明见 NOTICE。