优谷·雅言SDK 文档2.0.0

安卓 SDK

优谷雅言语音评测的 Android 客户端,Kotlin 编写,Java 可直接调用。SDK 是接入平台的推荐方式,没有 SDK 的语言可按 CONTRACT.md 直连接口。

查看 Markdown
项 内容
坐标 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 仓库:

Groovy
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url "https://open.shengzhiai.com/maven/" }
    }
}

在模块的 build.gradle 里声明依赖:

Groovy
dependencies {
    implementation "com.shengzhiai.yugu:yugu-android-sdk:2.1.0"
}

Kotlin DSL 写法:

Kotlin
// 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,构建时自动合并。使用录音器时,宿主应用需要声明录音权限:

XML
<uses-permission android:name="android.permission.RECORD_AUDIO" />

Android 6.0 起录音权限需要在运行时申请:

Kotlin
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。

五分钟快速开始#

第一步,创建客户端。整个应用共用一个实例即可。

Kotlin
val client = YuguClient.builder()
    .auth(Auth.appKey("YOUR_APP_KEY", "YOUR_SECRET_KEY"))
    .build()

第二步,准备一段 16 kHz,16 位,单声道的 WAV 音频,调用整段评测。REST 方法是阻塞调用,放在后台线程执行:

Kotlin
thread {
    val result = client.evaluate(
        EvaluateConfig(CoreType.SENTENCE, "今天天气很好", Language.ZH_CN),
        File(filesDir, "sample.wav"),
    )
    Log.i("yugu", "总分 ${result.overall},发音 ${result.dimensions.pronunciation}")
}

也可以使用异步方法,回调默认在主线程执行,可以直接更新界面:

Kotlin
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}"
        }
    },
)

第三步,应用退出或不再评测时关闭客户端:

Kotlin
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 段不带该字段。

看图说话需要上传图片:

Kotlin
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。

Kotlin
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 内部重试与实时评测重连共用同一个判定函数,规则按顺序第一条命中为准:

  1. 本地错误码 90001,90002,90007 可重试,其余本地错误码不可重试。
  2. 有平台错误码时取错误码表里的可重试标记,例如 40901,42900,42901,50000,50200 可重试。
  3. 没有错误码或错误码不认识时,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。取消令牌在任何线程都可以调用:

Kotlin
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 里开始录音的代码不会被重复执行。

示例#

Kotlin
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 不解析,需要时从这里读取

按朗读进度给参考文本着色:

Kotlin
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() 停止录音,释放麦克风,注销监听器

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

Kotlin
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。自定义输出:

Kotlin
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。

测试与覆盖率#

Shell
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。

Shell
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。

更新于 2026-10-12