优谷·雅言SDK 文档2.0.0

Java SDK

服务端 Java SDK,封装优谷雅言开放平台的整段评测,声通兼容评测,语音合成,报告查询与实时流式评测。SDK 是推荐的接入方式,直连 REST 与 WebSocket 接口留给没有 SDK 的语言使用。

查看 Markdown
项目 说明
Maven 坐标 com.shengzhiai.yugu:yugu-java-sdk:2.1.0
仓库地址 https://open.shengzhiai.com/maven/,公开读取,无需认证
Java 版本 Java 11 及更高版本
运行时依赖 jackson-databind 2.17,HTTP 与 WebSocket 使用 JDK 自带的 java.net.http
许可 Apache-2.0,见 LICENSE
包名 com.shengzhiai.yugu,子包 model,errors,audio

安装#

Maven 项目在 pom.xml 中加入仓库与依赖:

XML
<repositories>
  <repository>
    <id>shengzhiai</id>
    <url>https://open.shengzhiai.com/maven/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>com.shengzhiai.yugu</groupId>
    <artifactId>yugu-java-sdk</artifactId>
    <version>2.1.0</version>
  </dependency>
</dependencies>

Gradle 项目在 build.gradle 中加入:

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

dependencies {
    implementation 'com.shengzhiai.yugu:yugu-java-sdk:2.1.0'
}

Gradle Kotlin 脚本写法:

Kotlin
repositories {
    mavenCentral()
    maven("https://open.shengzhiai.com/maven/")
}

dependencies {
    implementation("com.shengzhiai.yugu:yugu-java-sdk:2.1.0")
}

jackson-databind 从 Maven Central 获取。仓库中每个版本都保留,改写版本号即可回退到历史版本。每个版本附带 -sources.jar 与 -javadoc.jar,IDE 可直接查看源码与接口文档。

五分钟快速开始#

分三步执行。需要 JDK 11 或更高版本与 Maven 3.6 或更高版本,一段 3 秒左右的 16 kHz,16 位,单声道 WAV 录音,以及开放平台签发的 appKey 与 secretKey,联调期间用沙箱密钥,申请方式见 沙箱环境。

  1. 新建工程目录 yugu-quickstart,在其中新建 pom.xml:
XML
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>yugu-quickstart</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.release>11</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <repositories>
    <repository>
      <id>shengzhiai</id>
      <url>https://open.shengzhiai.com/maven/</url>
    </repository>
  </repositories>
  <dependencies>
    <dependency>
      <groupId>com.shengzhiai.yugu</groupId>
      <artifactId>yugu-java-sdk</artifactId>
      <version>2.1.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.5.0</version>
        <configuration>
          <mainClass>QuickStart</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
  1. 新建 src/main/java/QuickStart.java:
Java
import com.shengzhiai.yugu.YuguClient;
import com.shengzhiai.yugu.errors.YuguException;
import com.shengzhiai.yugu.model.EvalResult;
import com.shengzhiai.yugu.model.EvaluateConfig;

import java.nio.file.Path;

public class QuickStart {
    public static void main(String[] args) {
        try (YuguClient client = YuguClient.builder()
                .apiKey(System.getenv("YUGU_APP_KEY"), System.getenv("YUGU_SECRET_KEY"))
                .build()) {
            EvalResult r = client.evaluate(Path.of("audio.wav"),
                    new EvaluateConfig(EvaluateConfig.CORE_SENTENCE, "今天天气很好", "zh-CN"));
            System.out.println("总分 " + r.getOverall());
            System.out.println("发音 " + r.getDims().getPronunciation());
            System.out.println("流利度 " + r.getDims().getFluency());
            System.out.println("完整度 " + r.getDims().getIntegrity());
            r.getWords().forEach(w -> System.out.println(w.getWord() + " " + w.getScores().getOverall()));
        } catch (YuguException e) {
            System.err.println(e.getCategory() + " " + e.getCode() + " " + e.getRawMessage());
        }
    }
}
  1. 把录音放到工程目录,命名为 audio.wav,设置密钥后运行,控制台输出总分,分项与逐字分数:
Shell
YUGU_APP_KEY=沙箱appKey YUGU_SECRET_KEY=沙箱secretKey mvn -q compile exec:java

demos/java-cli 是同样流程的完整工程,带命令行参数,可直接运行。

接口一览#

方法 平台接口 说明
evaluate(audio, EvaluateConfig) POST /api/v1/evaluate 原生整段评测,audio 可为 byte[],File,Path,InputStream,AudioSource
evaluateCompat(audio, CompatConfig) POST /{coreType} 声通兼容整段评测,coreType 取 CompatConfig.CORE_TYPES 中的值
tts(TtsRequest) POST /api/v1/tts/generate 语音合成,返回音频地址
getReport(recordId) GET /api/v1/report/{recordId} 查询评测记录的报告
streamEvaluate(EvaluateConfig, StreamListener) wss://open.shengzhiai.com/api/v1/ws/evaluate 原生实时评测,返回 StreamSession,可开启朗读进度
streamEvaluateCompat(CompatConfig, StreamListener) wss://open.shengzhiai.com/{coreType} 声通兼容实时评测,可开启朗读进度
close() 无 释放客户端,取消未结束的实时会话

每个 REST 方法都有一个带 RequestOptions 的重载,每个实时方法都有一个带 StreamOptions 的重载。整段评测与实时评测的结果都是 EvalResult:

取值方法 含义
getOverall() 总分,连读题型取 connected_overall
getDims() 维度分:pronunciation,accuracy,fluency,integrity,tone,rhythm,emotion,readingSkill,speed,rearTone
getWords() 逐字或逐词分数,含音素,英文的字母发音与音节,读音状态
getSentences() 逐句分数,段落题开启 paragraphNeedWordScore 后含逐字明细
getConnected() 连读题型的连读,节奏,失爆,弱读与词间边界,其他题型为 null
getOpen() 开放题的内容,语言运用,表达呈现,转写文本,反馈与审计,其他题型为 null
getAsrText() 识别文本与逐字对齐,开启 includeAsrText 后返回
getReport() 详细报告原始 JSON,开启 includeReport 后返回
getWarnings() 平台音频质量警告,元素含 code 与 message
getLocalWarnings() 本地音频预检的提示,码值 90101 到 90105
getIdempotencyKey(),isReplayed(),getAttempts() 本次调用的幂等键,是否为平台重放的首次结果,尝试次数
getRaw() 完整响应 JSON,新增字段可从中读取

鉴权方式#

两种方式二选一,同时配置时使用签名方式。

方式 配置 REST 请求 WebSocket 握手
签名,推荐服务端使用 apiKey(appKey, secretKey) X-App-Key,X-Timestamp,X-Nonce,X-Signature 请求头 query 参数 appKey,timestamp,nonce,signature
令牌 token(jwt) Authorization: Bearer 请求头 query 参数 token

声通兼容整段评测 POST /{coreType} 只接受签名方式,只配置令牌时平台返回 AuthException,错误码 40100。

签名算法与平台一致:业务参数去掉空值后按键名排序,拼成 k1=v1&k2=v2,计算 Base64(HMAC_SHA256(payload, secretKey))。原生评测签名 config 分片的完整 JSON 文本,语音合成签名请求体顶层的标量字段,兼容评测签名表单字段,报告查询签名空集合,WebSocket 握手签名 signature 之外的全部 query 参数。Signer 类公开同一算法,便于自行核对。secretKey 只用于本地计算,不会发送到网络,也不会写入日志。

错误处理#

SDK 的全部错误都是 YuguException 的子类,属于非受检异常,可以按子类分支处理,也可以读取 getCategory():

Java
try {
    EvalResult r = client.evaluate(audio, config);
} catch (InvalidParameterException e) {
    // 参数错误,修改后再提交
} catch (AuthException | PermissionException e) {
    // 凭据或权限问题,告警
} catch (QuotaExceededException e) {
    // 额度或余额不足,提示充值
} catch (AudioQualityException e) {
    // 音频质量问题,提示用户重录
} catch (YuguException e) {
    if (e.isRetryable()) {
        // SDK 已按策略重试过,仍失败,可稍后用同一个幂等键再次提交
    }
    log.warn("code={} http={} key={} attempts={}", e.getCode(), e.getHttpStatus(), e.getIdempotencyKey(), e.getAttempts());
}
类别 异常类 典型错误码 可重试
NETWORK NetworkException 90001 连接失败,90006 重连次数用尽,90011 证书校验失败 90001 可重试
TIMEOUT RequestTimeoutException 90002 连接或读取超时,90007 等待终评超时,HTTP 408 是
AUTH AuthException 40100,2001 到 2011,HTTP 401 否
PERMISSION PermissionException 40300,1004,1005,1012,1013,HTTP 403 否
INVALID_PARAM InvalidParameterException 40001,90010,HTTP 400,413,415,422 否
NOT_FOUND NotFoundException 40400,HTTP 404 否
CONFLICT ConflictException 40901 同一幂等键处理中,40903 幂等键用于其他请求,1311 40901 与 1311 可重试
RATE_LIMIT RateLimitException 42900,42901,3001 到 3003,HTTP 429 是
QUOTA QuotaExceededException 40902,42902,42903,1310,2012 否
SERVER 与 UPSTREAM ServerException 50000,50010,50200,HTTP 500,502,503,504 50010 之外可重试
AUDIO AudioQualityException 90101 到 90105,严格模式下的 1001 否
STATE IllegalSessionStateException 90004 客户端已关闭,90008 重放缓冲超限,90009 状态不允许 否
CANCELLED RequestCancelledException 90003 否
PROTOCOL ProtocolViolationException 90005 响应无法解析 否

异常保留这些字段:getCode() 平台错误码或本地码,getHttpStatus() HTTP 状态,isRetryable() 是否可重试,getIdempotencyKey() 幂等键,getRecordId() 评测记录号,getAttempts() 尝试次数,getRawBody() 原始响应体,截断到 4 KB,getRetryAfterMs() 服务端建议的等待时间。

YuguErrors.isRetryable(Throwable) 是公开的可重试判定,SDK 的重试与重连使用同一个函数。判定顺序:本地码中 90001,90002,90007 可重试,其他本地码不可重试。其次,平台错误码按错误码表的可重试标记判定。错误码缺失或未登记时,HTTP 408,425,429,500,502,503,504 可重试。错误码常量在 ErrorTable 中,例如 ErrorTable.QUOTA_INSUFFICIENT,完整码表见仓库根目录的 ERRORS.md。

音频质量警告 1001 到 1005 与 1009 不是异常,评分照常返回,警告列在 getWarnings() 中,WarningCode 枚举全部警告码。需要把警告当作错误处理时,调用 YuguErrors.fromWarning(code) 构造 AudioQualityException。开启 strictAudio(true) 后,平台返回 1001 时整段评测与实时评测都改为抛出 AudioQualityException,异常的 getResult() 保留原结果。

重试与幂等#

写操作 evaluate,evaluateCompat,tts 默认自动生成幂等键,32 位小写十六进制,通过 Idempotency-Key 请求头发送。同一次调用的每次重试复用同一个键,重试时只更新时间戳,随机串与签名。实时会话把同一个键放进握手参数与开始帧,断线重连沿用该键。

调用方也可以指定幂等键,例如用业务订单号保证同一段音频重复提交时只计费一次:

Java
EvalResult r = client.evaluate(audio, config, RequestOptions.withIdempotencyKey("order-20261008-0001"));
if (r.isReplayed()) {
    // 平台识别出重复提交,返回首次结果,没有再次计费
}

自定义的键由 1 到 200 个可打印 ASCII 字符组成,不含空格,不符合时在发出请求前抛出 InvalidParameterException,错误码 90010。

平台侧的幂等语义:

情形 平台行为
作用范围 按 appKey 隔离,令牌方式按用户隔离
有效期 首次成功结果保留 24 小时
同一个键,同一请求 返回首次成功的结果,响应头 Idempotency-Replayed: true,不再计费
首次请求仍在处理 平台最多等待 30 秒后重放结果,仍未完成时返回 409,错误码 40901,带 Retry-After,SDK 自动等待后重试
同一个键,不同请求 返回 409,错误码 40903,不重试
首次请求失败 释放该键,下次提交重新处理

重试策略 RetryPolicy 的默认值:

字段 默认值 含义
maxRetries 2 首次之外最多重试 2 次,总计 3 次尝试
initialDelayMs 200 第一次退避
multiplier 2.0 退避倍数
maxDelayMs 4000 退避上限
jitter 0.3 随机抖动幅度,正负 30 %
respectRetryAfter true 遵守服务端的 Retry-After
maxRetryAfterMs 30000 Retry-After 的上限

第 n 次重试前的等待:min(maxDelayMs, initialDelayMs * multiplier^(n-1)) * (1 + U(-jitter, +jitter)),默认约为 200 毫秒与 400 毫秒。响应带 Retry-After 时取两者中较大的值。整次调用受 totalTimeoutMs 约束,默认 300 秒,等待会越过期限时不再重试,直接抛出最后一次的错误。

只重试可重试的错误:连接失败,读超时,HTTP 429 与 5xx,以及错误码表标记为可重试的错误码。参数错误,鉴权失败,额度不足不重试。关闭自动幂等键 autoIdempotencyKey(false) 后,没有幂等键的写操作不重试,避免重复计费。报告查询天然幂等,照常重试。每次重试都会在 WARN 级别写一条日志,例如 retry 1/2 in 231 ms: HTTP 503 code=50200,同时回调 EventListener.onRetry。

单次调用可覆盖默认值:

Java
RequestOptions opts = RequestOptions.builder()
        .idempotencyKey("order-20261008-0001")
        .retry(RetryPolicy.builder().maxRetries(4).build())
        .totalTimeoutMs(60_000)
        .readTimeoutMs(30_000)
        .cancellation(token)
        .build();

CancellationToken 可从其他线程取消调用,正在进行的请求立即中断,重试等待随即结束,调用抛出 RequestCancelledException,错误码 90003。

实时流式评测与断线重连#

实时评测接收 16 kHz,16 位,小端,单声道 PCM,建议每帧 640 字节,即 20 毫秒。单次 sendAudio 可以送入任意长度的音频,SDK 按不超过 32000 字节一帧切分发送,平台对超过 128 KB 的帧以 1009 关闭连接。streamEvaluate 立即返回会话对象,连接在后台建立,开始前送入的音频先排队,开始后按顺序发出。

Java
StreamSession session = client.streamEvaluate(
        new EvaluateConfig(EvaluateConfig.CORE_SENTENCE, "今天天气很好", "zh-CN"),
        new StreamListener() {
            @Override public void onStateChanged(SessionState from, SessionState to) { ui.show(to); }
            @Override public void onReconnecting(int attempt, long delayMs, YuguException cause) { ui.showReconnecting(attempt); }
            @Override public void onReconnected(int attempt, long droppedBytes) { ui.showConnected(); }
            @Override public void onResult(EvalResult result) { ui.showScore(result.getOverall()); }
            @Override public void onError(YuguException error) { ui.showError(error); }
            @Override public void onClosed(int code, String reason) { ui.release(); }
        });
for (byte[] frame : pcmFrames) {
    session.sendAudio(frame);
}
session.end();
EvalResult result = session.result().get(60, TimeUnit.SECONDS);

会话状态 SessionState 与转换:

状态 含义
IDLE 会话已创建,尚未连接
CONNECTING 正在建立 WebSocket 连接
CONNECTED 服务端发出 connected,SDK 已发送开始帧
STARTED 服务端发出 started,音频开始传输
ENDING 已发送结束帧,等待终评
RECONNECTING 连接中断,等待重连
COMPLETED 已回调终评结果
FAILED 已回调错误
CANCELLED 调用方取消或客户端关闭
CLOSED 连接已释放,最后一个状态

正常流程为 IDLE,CONNECTING,CONNECTED,STARTED,ENDING,COMPLETED,CLOSED。getState() 随时返回最近一次 onStateChanged 通知的状态,isActive() 在终态之前返回 true。

回调约定:

  1. 每个会话恰有一次 onResult 或 onError,先调用 cancel() 时两者都不回调。
  2. onClosed 总是最后一个回调。
  3. 同一会话的回调按顺序执行,互不并发。
  4. 重连时的回调顺序为 onReconnecting,onConnected,onStarted,onReconnected。

触发重连的情形:传输层故障,结果前收到 1001,1006,1011 等可恢复的关闭码,心跳超时,连接或开始超时,结束后等待终评超时,以及错误码可重试的服务端错误帧,例如 40901,42901,50200。其他错误帧直接进入 FAILED。关闭码 1002,1003,1007,1008,1009,1010 以及 4000 到 4999 表示平台拒收这批数据,重连会被同样拒绝,会话直接进入 FAILED,错误码 90005。结果前收到 1000 同样进入 FAILED。平台不支持会话续传,每次重连都是新的服务端会话,参数与幂等键与原会话一致,平台据此保证同一段音频只计费一次。

重连策略 ReconnectPolicy 默认开启,每次中断最多连续重连 8 次,等待依次约为 0.5,1,2,4,4,4,4,4 秒,合计约 23 秒,抖动正负 30 %。默认设置可以撑过 10 秒左右的断网。次数只累计连续失败的尝试,重连成功后从零开始。一个会话累计最多重连 maxAttempts 的 3 倍,默认 24 次,服务端反复接受又断开连接时不会无限重连。任一上限用尽时回调 onError,错误码 90006,getCause() 为最后一次失败的原因。

重连期间的音频处理由 AudioBufferPolicy 决定:

策略 行为 结果覆盖范围
REPLAY,默认 保留本次会话的全部音频,上限 10 MB。重连后重新发送开始帧,重放全部音频,补发重连期间排队的音频,已调用 end() 时再次发送结束帧。超过 10 MB 后下一次重连以 90008 失败 全部音频
DROP 不缓冲,重连期间送入的音频丢弃。onReconnected 的 droppedBytes 给出未计入结果的字节数。调用 end() 之后断线时直接以原因失败 重连之后送入的音频
FAIL 不重连,传输层故障直接进入 FAILED 无

心跳使用 WebSocket 协议层 ping,默认每 15 秒一次,30 秒内收不到 pong 或任何帧即判定连接中断。发送结束帧后默认最多等待终评 300 秒,超时触发重连,重连关闭时回调 90007。以上时间可在 ClientOptions 中调整,单个会话可用 StreamOptions 覆盖幂等键,重连策略,缓冲策略,终评超时与重放上限。

原生实时评测与声通兼容实时评测都可以开启朗读进度,服务端在收音过程中下发进度帧,SDK 回调 onPartial,说明见朗读进度一节。

朗读进度#

原生实时评测调用 EvaluateConfig.realtimeFeedback(true),声通兼容实时评测调用 CompatConfig.realtimeFeedback(true)。开启后服务端在收音过程中报告参考文本里哪些字或词已经读过,SDK 通过 onPartial 回调 StreamPartial,朗读状态取自进度帧的 result.progress。原生开始帧只在开启时带 "realtimeFeedback": true,未开启时开始帧与 2.0.0 相同。兼容参数帧带 "realtime_feedback": true。该选项只作用于实时评测,整段评测不发送。

取值方法 含义
hasProgress() 这一帧带朗读进度时为 true
getProgress() 参考文本的全部单位,按原文顺序排列,元素为 ReadingWord,没有进度时为空列表
getReadUnits() 已读的单位数,即当前朗读位置,没有进度时为 -1
getMatchedUnits() 识别结果一致的单位数,没有进度时为 -1
getTotalUnits() 单位总数,等于 getProgress() 的长度,没有进度时为 -1
getBytes() 这一轮服务端已收到的音频字节数
getRaw() 原始进度帧,含 SDK 未解析的字段

ReadingWord 的 getWord() 为字或词,取自参考文本。isRead() 为 true 表示朗读位置已越过这个单位,isMatched() 为 true 表示识别结果与这个单位一致。两个 ReadingWord 的三个字段都相同时 equals 为 true。

兼容通道的进度帧沿用声通 realtime_feedback 中间结果的结构,顶层 recordId 为空串,result 另带 kernel_version,resource_version 与声通式增量 words。增量 words 只含本帧新读到的字或词,覆盖第 getReadUnits() - words.length 到第 getReadUnits() - 1 个单位,从 0 计,每项结构同终评的 words。识别一致的单位各项分数为 100,skipped 为 0,不一致的各项分数为 0,skipped 为 1,分数下发之后不再更新,实际得分以终评为准。SDK 不解析这几个字段,需要时从 getRaw() 读取,朗读状态以 getProgress() 为准。断线重连后新会话的 words 从第一个单位重新下发,按位置去掉已经处理过的单位即可。原生通道的进度帧没有这几个字段。

进度规则:

  1. 中文按字划分单位,去掉标点与空白。英文按空白分词,去掉词首词尾的标点,词内的撇号与连字符保留。原生开始帧的 language 以 zh 开头,或兼容 coreType 以 .cn 结尾或为 pinyin 时按字,其余按词。兼容参数帧带 language 时按 language 判断。
  2. 一轮之内 isRead() 与 isMatched() 只会由 false 变为 true,isMatched() 为 true 时 isRead() 也为 true。isRead() 为 true 而 isMatched() 为 false,表示朗读已越过这个位置但识别结果与参考文本不一致,可能是读错,漏读或识别误差,以终评为准。
  3. 进度一般比朗读晚 1 到 2 秒,随网络情况变化。调用 end() 之后会话处于 ENDING,最后一段音频的进度仍通过 onPartial 送达,onResult 总在最后。
  4. 进度只对 16 kHz,16 位,单声道 PCM 生效,一轮开头带 WAV 头的同样生效。参考文本为空,音频不是 PCM,或服务端当时无法跟踪时,hasProgress() 为 false,进度帧只有 getBytes(),终评不受影响。
  5. 断线重连后服务端在新会话上从零重新计算进度。缓冲策略为默认的 REPLAY 时,重放音频期间进度从零追上来,会话记住上一次回调的 getReadUnits() 与 getMatchedUnits(),只回调两者都不小于上一次的进度帧,界面上的进度不会倒退,丢弃的进度帧在 DEBUG 级别记一条日志。缓冲策略为 DROP 时新会话只评重连之后的音频,进度可能追不上重连前的位置,每次重连成功后记录清零,新会话的进度帧从零开始照常回调。没有进度的帧照常回调。每次调用 streamEvaluate 或 streamEvaluateCompat 得到的会话单独记录。
  6. 进度帧不含发音评分,终评与未开启时相同,不额外计费。

按 getProgress() 给参考文本着色的示例,识别一致的字为绿色,已读但识别不一致的字为红色,未读的字为灰色:

Java
StreamSession session = client.streamEvaluate(
        new EvaluateConfig(EvaluateConfig.CORE_SENTENCE, "我是中国人,我是中国人。我们都是中国人。中华民族是一家人。", "zh-CN")
                .realtimeFeedback(true),
        new StreamListener() {
            @Override public void onPartial(StreamPartial p) {
                if (!p.hasProgress()) {
                    return;
                }
                StringBuilder html = new StringBuilder();
                for (ReadingWord w : p.getProgress()) {
                    String color = w.isMatched() ? "green" : w.isRead() ? "red" : "gray";
                    html.append("<span style=\"color:").append(color).append("\">").append(w.getWord()).append("</span>");
                }
                ui.showReference(html.toString(), p.getReadUnits() + "/" + p.getTotalUnits());
            }
            @Override public void onResult(EvalResult result) { ui.showScore(result.getOverall()); }
            @Override public void onError(YuguException error) { ui.showError(error); }
        });

生命周期#

YuguClient 线程安全,一个应用创建一个实例即可。客户端实现 AutoCloseable,建议用 try-with-resources 管理:

Java
try (YuguClient client = YuguClient.builder()
        .apiKey(appKey, secretKey)
        .build()) {                                       // 创建
    EvalResult r = client.evaluate(audio, config);       // 使用
    StreamSession s = client.streamEvaluate(config, listener);
    s.sendAudio(pcm);
    s.end();
    s.result().get(60, TimeUnit.SECONDS);
}                                                         // 释放:close() 自动调用

释放规则:

操作 效果
client.close() 可重复调用。未结束的实时会话依次进入 CANCELLED 与 CLOSED,随后回调 onClosed。进行中的 REST 调用抛出错误码 90004,SDK 线程随即结束
关闭之后再调用 抛出 IllegalSessionStateException,错误码 90004
session.cancel() 可重复调用,不再回调结果与错误,连接以 1000 关闭
session.close() 可重复调用,未结束时等同 cancel()
session.awaitClosed(timeout, unit) 等待会话进入 CLOSED
client.getOpenSessionCount() 尚未关闭的会话数量

异常路径同样会释放资源:会话失败,被取消或客户端关闭时,连接都会关闭,定时器都会取消。SDK 的线程都是守护线程,不会阻止 JVM 退出。

音频预检#

整段评测上传前与实时评测调用 end() 时,SDK 在本地检查音频,避免为无效音频付费。WAV 与原始 PCM 做全部检查,MP3 等其他格式只检查大小。

本地码 条件 WARN 模式,默认 REJECT 模式
90101 时长短于 1 秒 警告 上传前抛出
90102 时长超过 300 秒,或者整段评测大于 50 MB,或者实时评测单轮大于 10 MB 警告 抛出
90103 全程静音:峰值低于 200,RMS 低于 30 警告 抛出
90104 音量过低:RMS 低于 -45 dBFS 警告 警告
90105 不是 16 位 PCM 或采样率低于 16000 警告 抛出

静音阈值与平台的静音判定一致,本地拒绝的文件就是平台会评为 0 分的文件。警告写入 WARN 日志,整段评测放在 getLocalWarnings() 中,实时评测另外回调 onWarning。audioPrecheck(AudioPrecheckMode.OFF) 关闭预检。AudioPrecheck.analyze(bytes) 可单独调用,返回时长,峰值,RMS 与全部检查项。

外部音频#

评测接口接受调用方自有的音频,录音设备,直播流与第三方采集都可接入:

来源 写法
字节数组 client.evaluate(bytes, config)
文件 client.evaluate(new File("a.wav"), config) 或 client.evaluate(Path.of("a.wav"), config)
输入流 client.evaluate(inputStream, config),读到流末尾,不关闭流
原始 PCM client.evaluate(AudioSource.pcm(pcm, 16000), config),SDK 封装为 WAV 后上传
实时流 session.sendAudio(pcm),可分任意大小的块送入

整段评测支持 WAV 与 MP3,平台接受的上限为 50 MB,SDK 读取上限为 64 MB。音频在一次调用内只读取一次,重试上传同一份数据。

日志与指标#

日志级别 LogLevel 从低到高为 OFF,ERROR,WARN,INFO,DEBUG,默认 WARN,写到标准错误输出。logger 选项可接入任意日志框架,例如 SLF4J:

Java
Logger slf = LoggerFactory.getLogger("yugu");
YuguClient client = YuguClient.builder()
        .apiKey(appKey, secretKey)
        .logLevel(LogLevel.INFO)
        .logger((level, tag, message, error) -> {
            if (level == LogLevel.ERROR) slf.error("[{}] {}", tag, message, error);
            else if (level == LogLevel.WARN) slf.warn("[{}] {}", tag, message, error);
            else slf.info("[{}] {}", tag, message, error);
        })
        .build();
级别 内容
ERROR SDK 内部问题,例如回调抛出异常
WARN 重试,重连,预检警告,实时会话失败
INFO 每次调用的状态码,耗时与尝试次数,会话状态变化
DEBUG 每次请求的方法,路径,超时与脱敏后的请求头,WebSocket 握手地址

日志不包含 secretKey,签名,令牌与音频数据,appKey 只保留前 4 个字符,后接 ***。

指标回调 EventListener 的方法全部可选:

方法 时机
onRequestStart(op, method, path, attempt) 每次 HTTP 尝试之前
onRequestEnd(op, httpStatus, latencyMs, attempts, error) 每次逻辑调用结束,含总耗时与尝试次数
onRetry(op, attempt, delayMs, error) 每次重试等待之前
onSessionStateChanged(sessionId, old, new) 实时会话每次状态变化
onReconnect(sessionId, attempt, succeeded) 每次重连尝试的结果

配置项#

选项 默认值 含义
baseUrl https://open.shengzhiai.com REST 地址
wsBaseUrl wss://open.shengzhiai.com WebSocket 地址,只设置 baseUrl 时由其推导
apiKey(appKey, secretKey) 或 token(jwt) 必填 鉴权方式
connectTimeoutMs 10000 建立连接的超时
readTimeoutMs 120000 单次 HTTP 尝试的超时
totalTimeoutMs 300000 一次调用含重试与等待的总期限
retry RetryPolicy.defaults() 重试策略
autoIdempotencyKey true 写操作自动生成幂等键
logLevel 与 logger WARN,标准错误输出 日志
eventListener 无 指标回调
audioPrecheck WARN 音频预检模式
strictAudio false 平台警告 1001 改为抛出异常
userAgent yugu-java-sdk/2.1.0 User-Agent 请求头
reconnect ReconnectPolicy.defaults(),连续 8 次 实时会话重连策略
audioBufferPolicy REPLAY 实时会话断线时的音频处理
pingIntervalMs 与 pongTimeoutMs 15000,30000 心跳
resultTimeoutMs 300000 结束后等待终评的时间

沙箱环境#

开放平台为联调提供沙箱密钥,名称以 sandbox- 开头,每个密钥每天 200 次调用,超出后返回错误码 42903,不重试。沙箱与正式环境使用同一地址,只有密钥不同。申请方式与限制见仓库根目录的 SANDBOX.md。SDK 的集成测试在设置 YUGU_SANDBOX_APPKEY 与 YUGU_SANDBOX_SECRET 两个环境变量后对沙箱运行。

构建与测试#

Shell
cd java
./mvnw verify          # 编译,单元测试,基于平台模拟服务的集成测试,覆盖率门槛
./mvnw verify -Pslow   # 另外运行断网 10 秒的慢速测试

集成测试会启动 tools/mock-server/server.mjs,需要 Node 20 与该目录下的 npm ci。覆盖率报告位于 target/site/jacoco/index.html,行覆盖率低于 70 % 时构建失败。ci/java.sh 是持续集成使用的脚本。

常见问题#

问题:调用一直返回 AuthException,错误码 2003。

解答:签名不匹配。核对 appKey 与 secretKey 是否成对,服务器时钟与标准时间相差是否超过 300 秒。签名方式与令牌方式同时配置时 SDK 使用签名方式。

问题:网络抖动时同一段音频会被计费几次。

解答:一次。SDK 的每次重试与每次重连都复用同一个幂等键,平台对同一键只计费一次。业务层自行重新提交时,把首次使用的键通过 RequestOptions.withIdempotencyKey 传入,平台返回首次结果,isReplayed() 为 true。

问题:错误 40903 是什么意思。

解答:同一个幂等键已经用于另一段音频或另一组参数。每段不同的音频应使用不同的键,自动生成的键天然满足这一点。

问题:实时评测断线后结果是否完整。

解答:默认的 REPLAY 策略重放全部音频,结果覆盖整段音频。DROP 策略只评测重连之后的音频,onReconnected 的 droppedBytes 给出缺失的字节数。

问题:如何区分“正在重连”,“重连失败”与“评测完成”。

解答:分别对应 onReconnecting 回调,错误码 90006 的 onError 回调与 onResult 回调。三者也可以从 onStateChanged 中的 RECONNECTING,FAILED,COMPLETED 状态判断。

问题:断网多久以内实时评测可以自动恢复。

解答:默认设置下连续重连 8 次,等待合计约 23 秒,网络在这段时间内恢复即可继续,默认的 REPLAY 策略重放全部音频,结果完整。超过后回调错误码 90006。ReconnectPolicy.builder().maxAttempts(n) 可调整次数。

问题:回调运行在哪个线程。

解答:运行在 SDK 的守护线程上。同一会话的回调依次执行,不会并发。回调中不宜执行耗时操作,界面更新需切换到界面线程。

问题:为什么评测结果的 getLocalWarnings() 中有 90104。

解答:本地预检发现音量偏低,音频照常上传评测,分数可能偏低,建议提示用户靠近麦克风重录。

问题:如何从 1.0.0 版本迁移。

解答:坐标改为 com.shengzhiai.yugu:yugu-java-sdk:2.1.0,包名由 tech.dragonai.yugu 改为 com.shengzhiai.yugu,YuguException 改为非受检异常,流式接口的参数改为 EvaluateConfig 与 CompatConfig,回调接口的方法有调整。详细说明见 CHANGELOG.md 与仓库根目录的 MIGRATION-2.0.md。

版本与许可#

版本记录见 CHANGELOG.md,各端版本与接口版本的对应关系见仓库根目录的 COMPATIBILITY.md。SDK 按 Apache License 2.0 授权,全文见 LICENSE,第三方说明见 NOTICE。

更新于 2026-10-12