优谷·雅言SDK 文档2.0.0

iOS SDK

优谷雅言语音评测服务的 iOS 与 macOS SDK,提供整段评测,实时评测,语音合成,报告查询,自动重试,幂等键,断线重连,错误分类,音频预检与录音。

查看 Markdown
项 内容
版本 2.1.0,2026-10-12
仓库 https://open.shengzhiai.com/git/yugu-ios-sdk.git,匿名只读
产品 YuguCore,YuguSDK
平台 iOS 13 起,macOS 11 起,YuguCore 另支持 Linux
工具链 Swift 5.9 起,Xcode 15 起
第三方依赖 无
许可 Apache-2.0

产品#

产品 内容 平台
YuguCore 整段评测,实时评测,语音合成,报告查询,重试,幂等,签名,错误分类,音频预检,只依赖 Foundation iOS,macOS,Linux
YuguSDK YuguCore 的全部能力加录音器 YuguRecorder,写 import YuguSDK 即可使用全部类型 iOS,macOS

App 一般选 YuguSDK。服务端或命令行工具只做评测时选 YuguCore。

安装#

Xcode 添加依赖#

  1. 在 Xcode 菜单 File 中选择 Add Package Dependencies。
  2. 在搜索框输入 https://open.shengzhiai.com/git/yugu-ios-sdk.git。
  3. Dependency Rule 选择 Up to Next Major Version,版本填 2.1.0。
  4. 产品勾选 YuguSDK,点击 Add Package。

Package.swift 声明#

Swift
// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v13)],
    dependencies: [
        .package(url: "https://open.shengzhiai.com/git/yugu-ios-sdk.git", from: "2.1.0"),
    ],
    targets: [
        .target(name: "MyApp", dependencies: [
            .product(name: "YuguSDK", package: "yugu-ios-sdk"),
        ]),
    ]
)

包标识取仓库地址的最后一段,所以 package: 参数写 yugu-ios-sdk。

版本固定与回退#

每个版本对应仓库里的一个 Git 标签,已发布的标签不会改动。固定在某个版本写 .package(url: "https://open.shengzhiai.com/git/yugu-ios-sdk.git", exact: "2.1.0"),回退时把版本号改成更早的标签,然后在 Xcode 中执行 File 菜单的 Packages 下的 Reset Package Caches。

CocoaPods 方式#

Ruby
pod 'YuguSDK', :git => 'https://open.shengzhiai.com/git/yugu-ios-sdk.git', :tag => '2.1.0'

CocoaPods 把两个产品编进同一个模块 YuguSDK,代码里写 import YuguSDK。

仓库访问#

仓库地址为 HTTPS,匿名只读,不需要账号,令牌与证书配置。沙箱环境与测试密钥的申请方式见 SDK 总仓库的 SANDBOX.md。

麦克风权限#

使用录音器的 App 必须在 Info.plist 中声明麦克风用途,缺少这一项时 iOS 在首次录音时终止 App。

XML
<key>NSMicrophoneUsageDescription</key>
<string>录制朗读音频,用于语音评测</string>

macOS App 开启了沙盒时,另需在 entitlements 中打开 com.apple.security.device.audio-input。只评测已有音频时不需要麦克风权限。

五分钟上手#

Swift
import YuguSDK

// 创建客户端,App 内复用同一个实例
let client = YuguClient(options: YuguClientOptions(
    auth: .appKey("<appKey>", secretKey: "<secretKey>")))

// 评测一段 16 kHz 单声道 WAV
let config = EvaluateConfig(coreType: .sentence, referenceText: "今天天气很好", language: "zh-CN")
let result = try await client.evaluate(audio: .file(wavURL), config: config)

// 读取分数
print("总分", result.overall ?? 0)
print("流利度", result.dimensions.fluency ?? 0, "完整度", result.dimensions.integrity ?? 0)
for word in result.words {
    print(word.word ?? "", word.overall ?? 0)
}

// 不再使用时释放
client.close()

不使用 async 时用完成回调写法,回调在 callbackQueue 上执行,缺省为主队列:

Swift
client.evaluate(audio: .file(wavURL), config: config) { outcome in
    switch outcome {
    case .success(let result): print(result.overall ?? 0)
    case .failure(let error): print(error.category, error.code, error.message)
    }
}

用麦克风录音再评测:

Swift
let recorder = YuguRecorder()
YuguRecorder.requestPermission { granted in
    guard granted else { return }
    try? recorder.start()
}
// 用户读完后
if let recording = recorder.stop() {
    client.evaluate(audio: recording.audioInput, config: config) { outcome in
        // 处理结果
    }
}
recorder.release()

客户端选项#

YuguClientOptions 的缺省值与其他各端 SDK 相同。

选项 缺省值 含义
baseUrl https://open.shengzhiai.com REST 基址
wsBaseUrl 由 baseUrl 推出,缺省为 wss://open.shengzhiai.com WebSocket 基址
auth 必填 .appKey(appKey, secretKey:) 或 .token(jwt)
connectTimeoutMs 10000 REST 连接超时,也是 WebSocket 握手超时
readTimeoutMs 120000 单次 REST 尝试的超时
totalTimeoutMs 300000 一次逻辑调用的总时限,含重试与等待
retry RetryPolicy.default 重试策略
autoIdempotencyKey true 写操作自动生成幂等键
logLevel .warn 日志级别
logger ConsoleLogger 日志输出
eventListener 无 指标回调
audioPrecheck .warn 音频预检方式
strictAudio false 平台报 1001 时改为返回音频错误
userAgent yugu-ios-sdk/2.1.0 请求头 User-Agent
reconnect ReconnectPolicy.default 实时评测重连策略
audioBufferPolicy .replay 重连时的音频缓冲策略
pingIntervalMs 15000 协议层心跳间隔
pongTimeoutMs 30000 心跳应答超时
resultTimeoutMs 300000 结束后等待终评的时限
callbackQueue 主队列 完成回调与会话回调所在队列
urlSessionConfiguration 无 自定义 URLSession 配置,例如代理
httpTransport 无 替换 HTTP 层,例如证书锁定
webSocketTransportFactory 无 替换 WebSocket 层

单次调用用 RequestOptions 覆盖:idempotencyKey,timeoutMs,readTimeoutMs,retry,cancellationToken,audioPrecheck。实时会话用 StreamOptions 覆盖:idempotencyKey,reconnect,audioBufferPolicy,connectTimeoutMs,resultTimeoutMs,pingIntervalMs,pongTimeoutMs,audioPrecheck。

接口一览#

方法 作用
evaluate(audio:config:image:options:) 原生整段评测 POST /api/v1/evaluate
evaluateCompat(coreType:audio:params:options:) 声通兼容整段评测 POST /{coreType}
tts(_:options:) 语音合成 POST /api/v1/tts/generate
getReport(recordId:options:) 报告查询 GET /api/v1/report/{recordId}
streamEvaluate(config:options:listener:) 原生实时评测会话
streamEvaluateCompat(coreType:params:options:listener:) 声通兼容实时评测会话
close() 释放客户端,可重复调用

REST 方法都有两种写法,async throws 写法跟随任务取消,完成回调写法返回 YuguCall,调用 cancel() 取消。客户端的全部方法线程安全。

评测参数在 EvaluateConfig 中设置,常用字段为 coreType,referenceText,language,slack,scale,precision,agegroup,includeReport,includeAsrText,refPinyin,taskType,paragraphNeedWordScore,平台新增的字段放进 extra。config 分片的 JSON 文本由 SDK 按固定顺序生成,整数值的小数写成整数,发出的文本与签名的文本一字不差。realtimeFeedback 只用于原生实时评测,打开朗读进度,见实时评测的朗读进度一节,整段评测不发送这个字段。

结果字段#

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

属性 来源 内容
overall result.overall,连读题取 result.connected_overall 总分
dimensions result 的各维度 完整度,准确度,发音,流利度,声调,韵律,朗读技巧,情感,语速
words result.words 逐字逐词得分,拼音,读音状态,音素,时间
sentences result.sentences 分句得分,段落题带 details 逐词详情
yuguScores result.yuguScores 五维结构化分数
connected 连读题 连读,失爆,弱读,节奏,各连接处的实现程度
openTask 开放题 内容,语言运用,表达,识别文本,点评,审计标记
report report 报告摘要,建议,维度分
asrText asrText 识别文本与逐字对齐
warnings warnings 与 result.warning 平台的音频质量警告
localWarnings 本地预检 本地预检警告
idempotencyKey,replayed,attempts 调用信息 幂等键,是否为平台重放的结果,尝试次数
raw,resultJSON 原始 JSON 未建模的字段从这里读取

各评测模式的取分字段逐项见 SDK 总仓库的 RESULTS.md。raw 是 JSONValue,可用下标读取,也可用 decode(_:) 解码为自定义类型。

错误处理#

所有失败都以 YuguError 返回。

属性 含义
category 错误类别,共 16 类
code 平台错误码,警告码或 SDK 本地错误码,未知时为 0
name 错误码在码表中的名称
httpStatus HTTP 状态,没有时为 0
message 说明
retryable 用同一个幂等键再次调用能否成功
idempotencyKey 本次调用的幂等键
recordId 平台已生成的记录号
attempts 已尝试次数
rawBody 响应原文,最多 4 KB
cause 底层错误,例如 URLError

类别与便捷属性一一对应,按类别分支即可:

类别 便捷属性 典型处置
network,timeout isNetworkError,isTimeout 提示网络状况,可重试
auth,permission isAuthError,isPermissionDenied 检查密钥与授权
invalidParam,notFound isInvalidParameter,isNotFound 修正参数
conflict isConflict 幂等键冲突,见重试与幂等一节
rateLimit,quota isRateLimited,isQuotaExceeded 稍后重试或充值
server,upstream isServerError 可重试,持续失败时联系平台
audio isAudioQuality 提示用户重录
state isIllegalState 检查调用顺序
cancelled isCancelled 调用方已取消
protocol isProtocolViolation 响应无法解析,联系平台
Swift
do {
    let result = try await client.evaluate(audio: .file(wavURL), config: config)
    show(result)
} catch let error as YuguError {
    switch error.category {
    case .audio: askToRecordAgain()
    case .auth, .permission: checkCredentials()
    default:
        if error.retryable { retryLater(idempotencyKey: error.idempotencyKey) }
    }
}

本地错误码:

错误码 含义
90001 网络错误,可重试
90002 连接或读取超时,可重试
90003 调用方取消
90004 客户端已关闭
90005 响应或帧无法解析
90006 实时评测重连次数用尽
90007 结束后等待终评超时,可重试
90008 重连重放缓冲超过 10 MB
90009 当前会话状态不允许该操作
90010 调用参数不合法,在任何网络请求之前报出
90011 证书校验失败
90101 到 90105 音频预检,见音频预检一节
90201 到 90203 录音器,依次为没有权限,麦克风不可用,录音出错

平台错误码的完整码表内置在 ErrorTable 中,说明见 SDK 总仓库的 ERRORS.md。1004 与 1005 在错误响应里是账户状态错误,在评测结果的 warning 里是音频警告,YuguErrors.fromCode 按错误响应查表,YuguErrors.fromWarning 按警告查表。YuguErrors.isRetryable(_:) 与 SDK 自身的重试判断使用同一套规则。

重试与幂等#

幂等键#

evaluate,evaluateCompat,tts 与两种实时会话都带幂等键。调用方没有指定时,SDK 为每次逻辑调用生成一个 32 位小写十六进制的键,重试与重连沿用同一个键。指定的键须为 1 到 200 个可打印 ASCII 字符,不含空格,不合法时在发出请求之前报 90010。

平台对幂等键的处理:

情形 平台行为
同一个键,同一个请求,首次已成功 直接返回首次结果,响应头带 Idempotency-Replayed: true,result.replayed 为 true,不再计费
首次请求仍在处理 等待最多 30 秒后返回首次结果,仍未完成时返回 409 与错误码 40901,可重试
同一个键,不同的请求 返回 409 与错误码 40903,不可重试
首次请求失败 释放这个键,再次提交按新请求处理

键的作用域为 appKey,使用 token 时为用户,有效期 24 小时。业务上需要“同一题只计一次费”时,自行指定键,例如用题目号与用户号组成的字符串,失败后用 error.idempotencyKey 再次提交。

Swift
let options = RequestOptions(idempotencyKey: "exam-42-user-7-q3")
let result = try await client.evaluate(audio: .file(wavURL), config: config, options: options)

重试策略#

缺省最多重试 2 次,共 3 次尝试。第 n 次重试前等待 min(maxDelayMs, initialDelayMs 乘 multiplier 的 n-1 次方),再乘以 0.7 到 1.3 之间的随机系数,缺省约为 200 毫秒与 400 毫秒。响应带 Retry-After 时等待时间不少于该值,该值按 30 秒封顶。等待后会超过 totalTimeoutMs 时不再重试,直接返回最后一次的错误。

重试的错误:网络错误,超时,HTTP 408,425,429,500,502,503,504,以及码表标为可重试的错误码,例如 40901,42900,42901,50000,50200。参数错误,鉴权失败,额度不足等不重试。没有幂等键的写操作不重试,autoIdempotencyKey 为 false,调用时也未指定键,就属于这种情况。报告查询是 GET 请求,不带键也会重试。

每次重试触发 eventListener 的 onRetry,同时写一条 WARN 日志,形如 retry 1/2 in 231 ms: HTTP 503 code=50200。

Swift
var options = YuguClientOptions(auth: .appKey("<appKey>", secretKey: "<secretKey>"))
options.retry = RetryPolicy(maxRetries: 3, initialDelayMs: 300)
options.totalTimeoutMs = 60_000

实时评测#

会话流程#

Swift
let handlers = YuguStreamHandlers()
handlers.onStateChanged = { from, to in print(from, "->", to) }
handlers.onReconnecting = { attempt, delayMs, cause in showBanner("网络中断,正在重连") }
handlers.onReconnected = { attempt, droppedBytes in hideBanner() }
handlers.onResult = { result in show(result) }
handlers.onError = { error in showError(error) }
handlers.onClosed = { code, reason in releaseUI() }

let session = try client.streamEvaluate(config: config, listener: handlers)
try session.start()
// 每帧 640 字节,16 kHz 单声道 16 位小端 PCM
try session.sendAudio(frame)
// 读完
try session.end()

start() 之后即可送音频,服务端就绪之前送来的音频由 SDK 暂存后按顺序发出。单次送入超过 32000 字节时,SDK 拆成不超过 32000 字节的帧发送,平台会以 1009 关闭超过 128 KB 的帧。session.state 随时可查。cancel() 与 close() 可重复调用。

状态#

文本
IDLE -> CONNECTING -> CONNECTED -> STARTED -> ENDING -> COMPLETED -> CLOSED
CONNECTING | CONNECTED | STARTED | ENDING -> RECONNECTING -> CONNECTING
任一非终止状态 -> FAILED -> CLOSED
任一非终止状态 -> CANCELLED -> CLOSED
回调 时机
onStateChanged 每次状态变化
onConnected 首次连接建立
onStarted 服务端首次接受参数,此后送入的音频直接发出
onPartial 进度帧,原生会话与声通兼容会话打开 realtimeFeedback 后收到,end() 之后到终评之前也会收到,见朗读进度一节
onReconnecting 连接中断,第几次重连,等待多久,原因
onReconnected 重连成功,DROP 策略下附带丢弃的字节数
onWarning end() 时的本地预检警告
onResult 终评结果
onError 失败
onClosed 会话结束,总是最后一个回调

回调的约定:每个会话只会收到 onResult 与 onError 中的一个,调用过 cancel() 的会话两者都不会收到,onClosed 总是最后一个回调,同一会话的回调不会同时执行,session.state 与最近一次 onStateChanged 报告的状态一致。

断线重连#

触发重连的情况:连接失败,连接被异常关闭,心跳超时,握手超时,终评超时,以及带可重试错误码的错误帧,例如 40901,42901,50200。不可重试的错误帧,鉴权失败的握手,证书错误直接进入 FAILED。服务端以 1000 正常关闭却没有给出结果时,会话以 90005 失败。服务端以 1002,1003,1007,1008,1009,1010 或 4000 到 4999 之间的代码关闭连接时,新连接也会被同样拒绝,会话直接以 90005 失败,不重连。

ReconnectPolicy 缺省最多连续重连 8 次,间隔约为 0.5 秒,1 秒,2 秒,4 秒,之后每次 4 秒,合计约 23 秒,各有正负 30% 的随机抖动,足以撑过 10 秒的断网。重连成功后次数从零重新计算,连续失败次数用尽时会话以 90006 失败,cause 为最后一次的原因。整个会话的重连总数不超过 maxAttempts 的 3 倍,缺省为 24 次,服务端反复接受连接又断开时,到达上限后同样以 90006 失败。

平台没有服务端续传,重连等于用同样的参数与同一个幂等键开一个新的服务端会话。音频缓冲策略 AudioBufferPolicy 决定音频怎么处理:

策略 行为
.replay,缺省 保留本次会话已送的全部音频,上限 10 MB。重连后重发开始帧,按原顺序重放全部音频,接着发出重连期间送入的音频,end() 已调用时再发结束帧。同一个幂等键使平台只评测一次,只计费一次。音频超过 10 MB 后,下一次需要重连时会话以 90008 失败
.drop 不保留音频,重连期间送入的音频丢弃,onReconnected 给出丢弃的字节数,新会话只评测重连之后送入的音频。end() 已发出后断线时,会话直接失败
.fail 不重连,连接中断即失败

关闭自动幂等键,也未指定键时,会话不重连。

心跳与超时#

会话进入 STARTED 后每 15 秒发一次协议层 ping,30 秒收不到 pong 视为连接中断。connectTimeoutMs 限定从发起连接到服务端接受参数的时间,resultTimeoutMs 限定 end() 之后等待终评的时间。任何状态都有时限,会话不会无回调地停住。

朗读进度#

原生会话在 EvaluateConfig 中设 realtimeFeedback: true,声通兼容会话在 CompatParams 中设 realtimeFeedback: true,平台在收音过程中下发进度帧,按参考文本逐字或逐词给出已经读到的位置,onPartial 回调 StreamPartial。按进度给参考文本着色的写法:

Swift
let refText = "我是中国人,我是中国人。我们都是中国人。中华民族是一家人。"
let config = EvaluateConfig(coreType: .sentence, referenceText: refText, language: "zh-CN", realtimeFeedback: true)

let handlers = YuguStreamHandlers()
handlers.onPartial = { partial in
    guard partial.hasProgress else { return }
    label.attributedText = colored(refText, partial.progress)
}
handlers.onResult = { result in show(result) }
handlers.onError = { error in showError(error) }
let session = try client.streamEvaluate(config: config, listener: handlers)

// 已对上的字为绿色,读过但没对上的为橙色,还没读到的与标点为灰色
func colored(_ refText: String, _ progress: [ReadingWord]) -> NSAttributedString {
    let text = NSMutableAttributedString(string: refText, attributes: [.foregroundColor: UIColor.systemGray])
    let ns = refText as NSString
    var from = 0
    for w in progress {
        let range = ns.range(of: w.word, options: [], range: NSRange(location: from, length: ns.length - from))
        if range.location == NSNotFound { break }
        let color: UIColor = w.matched ? .systemGreen : (w.read ? .systemOrange : .systemGray)
        text.addAttribute(.foregroundColor, value: color, range: range)
        from = range.location + range.length
    }
    return text
}
属性 内容
bytes 服务端在当前连接上已收到的音频字节数,重连后从零重新累计
progress 参考文本的全部单位,按原文顺序排列,取自进度帧的 result.progress,元素为 ReadingWord,没有进度时为空数组。中文按字,英文按空白分词,标点与空白不算单位
readUnits read 为 true 的单位数,没有进度时为 -1
matchedUnits matched 为 true 的单位数,没有进度时为 -1
totalUnits 单位总数,等于 progress 的长度,没有进度时为 -1
hasProgress 进度帧带 result.progress 时为 true
raw 进度帧的原始 JSON

ReadingWord 有三个属性:word 是取自参考文本的字或词,read 表示朗读位置已越过这个单位,matched 表示识别结果与这个单位一致。一轮之内 read 与 matched 只会由 false 变为 true,matched 为 true 时 read 也为 true。read 为 true 而 matched 为 false,表示朗读已越过这个位置但识别结果与参考文本不一致,可能是读错,漏读或识别误差,以终评为准。

进度变化时平台立即下发一帧,此外每收约 0.5 秒音频下发一帧,进度一般比朗读晚 1 到 2 秒,随网络情况变化。end() 之后会话处于 ENDING,平台对最后一段音频的识别结果仍以进度帧送达,onResult 之后不再回调 onPartial。进度帧不含发音评分,打开朗读进度不改变终评,也不额外计费。

进度只对 16 kHz 16 位单声道 PCM 生效,一轮开头带 WAV 头的同样生效。参考文本为空,音频为其他格式,或平台当时无法跟踪进度时,进度帧只有 bytes,hasProgress 为 false,终评不受影响。

声通兼容会话的进度帧另带声通格式的 result.words,只列出上一帧之后新读到的单位,覆盖第 readUnits - words.count 到第 readUnits - 1 个单位,顶层 recordId 为空字符串,记录号只在终评里给出。words 里的分数只表示识别结果与参考文本是否一致,发音与声调的实际得分以终评为准。逐字状态以 progress 为准,SDK 不读取 words,需要声通格式时从 raw 读取。重连后平台从第一个单位重新下发 words,拼接时按单位位置去重。原生会话的进度帧没有 words。

断线重连后,服务端在新的会话上从零重新计算进度。缓冲策略为 .replay 时,重放音频期间进度逐步追上,会话记住最近一次回调的 readUnits 与 matchedUnits,只回调两者都不小于上一次的进度帧,界面上的进度不会倒退,没有回调的进度帧各写一条 DEBUG 日志。缓冲策略为 .drop 时,新的服务端会话只评测重连之后送入的音频,重连成功时会话清除记住的进度,此后的进度按新的服务端会话重新开始,可能小于重连之前。不带进度的帧照常回调。每个会话单独记录,新的会话从零开始。

realtimeFeedback 缺省为 false,此时开始帧与 2.0.0 一字不差。声通兼容会话的 CompatParams.realtimeFeedback 发送的仍是 realtime_feedback。

生命周期#

客户端,会话,录音器都有显式释放接口,重复调用不会报错。

Swift
final class ReadingViewController: UIViewController {
    private let client = YuguClient(options: YuguClientOptions(
        auth: .appKey("<appKey>", secretKey: "<secretKey>")))
    private let recorder = YuguRecorder()
    private let recorderHandlers = YuguRecorderHandlers()
    private var session: YuguStreamSession?

    func startReading(text: String) throws {
        let handlers = YuguStreamHandlers()
        handlers.onResult = { [weak self] result in self?.show(result) }
        handlers.onError = { [weak self] error in self?.show(error) }
        let session = try client.streamEvaluate(
            config: EvaluateConfig(coreType: .sentence, referenceText: text, language: "zh-CN"),
            listener: handlers)
        self.session = session
        recorderHandlers.onFrame = { frame in try? session.sendAudio(frame) }
        recorder.setListener(recorderHandlers)
        try session.start()
        try recorder.start()
    }

    func finishReading() {
        recorder.stop()
        try? session?.end()
    }

    deinit {
        session?.cancel()
        recorder.release()
        client.close()
    }
}
对象 释放接口 行为
YuguClient close() 取消进行中的 REST 调用,这些调用以 90004 结束,取消全部会话,释放网络资源。关闭后的调用返回 90004
YuguStreamSession cancel(),close() 关闭连接,不再回调结果,最后回调 onClosed,随后释放监听器
YuguRecorder stop(),release() 停止采集,销毁音频引擎,释放麦克风,release() 之后丢弃监听器

录音器的状态为 IDLE,RECORDING,PAUSED,STOPPED,RELEASED,用 recorder.state 查询,pause() 与 resume() 用于暂停与继续。音频引擎在 start() 时创建,在 stop(),release() 与任何错误路径上销毁,麦克风不会在录音结束后继续占用。来电等音频中断开始时录音器转为 PAUSED,中断结束时回调 interruptionEndedShouldResume,由 App 决定是否调用 resume()。耳机插拔等路由变化时录音器重新连接输入继续录音,无法继续时以 90203 结束。

App 被系统终止时,进程内的连接与音频引擎随进程一起释放。平台只在收到结束帧后评测,没有发出结束帧的实时会话不评测,不计费。结束帧或整段评测请求已经到达平台时,平台照常评测与计费,用同一个幂等键与同样的音频再次提交即可取回结果,不再计费。

音频预检#

整段评测上传前与实时评测 end() 时,SDK 在本地检查 WAV 与 PCM 音频。

错误码 条件 WARN 模式 REJECT 模式
90101 时长不足 1 秒 警告 上传前失败
90102 时长超过 300 秒,整段上传大于 50 MB,或一次实时会话的音频大于 10 MB 警告 上传前失败
90103 全程静音,峰值低于 200,均方根低于 30 警告 上传前失败
90104 音量过低,均方根低于 -45 dBFS 警告 警告
90105 不是 16 位 PCM,或采样率低于 16000 警告 上传前失败

缺省为 WARN,警告放在结果的 localWarnings 中,实时会话另回调 onWarning。静音阈值与平台的静音判定一致,本地拒绝的音频上传后也只会得 0 分。REJECT 模式下实时会话不发结束帧,平台不评测,不计费。MP3 等其他格式只检查大小,整段上传的上限为平台限制的 50 MB。AudioPrecheck.check(_:) 可单独调用。

外部音频#

评测不依赖内置录音器,音频可以来自任何来源。

写法 用途
.data(bytes) 内存中的 WAV,MP3 等编码音频,类型由文件头识别
.file(url) 本地音频文件
.pcm16(pcm, sampleRate:, channels:) 16 位小端 PCM,SDK 加上 WAV 头后上传

实时会话用 sendAudio(_:) 送入外部采集的 16 kHz 单声道 16 位 PCM,每次 640 字节,即 20 毫秒,最为合适。YuguWAV.wrap 与 YuguWAV.parse 用于 WAV 封装与解析。

日志与指标#

日志级别为 .off,.error,.warn,.info,.debug,缺省为 .warn。日志不含 secretKey,签名,token 与音频数据,appKey 只保留前 4 个字符。自定义输出实现 YuguLogger:

Swift
final class AppLogger: YuguLogger {
    func log(level: YuguLogLevel, tag: String, message: String, error: Error?) {
        os_log("%{public}@ %{public}@", tag, message)
    }
}
options.logger = AppLogger()
options.logLevel = .info

YuguEventListener 的方法都是可选的:

方法 时机
onRequestStart(op:method:path:attempt:) 每次 REST 尝试开始
onRequestEnd(op:httpStatus:latencyMs:attempts:error:) 一次逻辑调用结束,耗时含全部重试
onRetry(op:attempt:delayMs:error:) 安排一次重试
onSessionStateChanged(sessionId:from:to:) 实时会话状态变化
onReconnect(sessionId:attempt:succeeded:) 一次重连结束

指标回调在 SDK 内部队列上执行,需要尽快返回。

测试#

Shell
swift test
swift test --enable-code-coverage

测试覆盖签名的全部跨端向量,错误码表的每一项,各评测模式的真实返回,线上录制的朗读进度帧,multipart 字节布局,退避计算,幂等键复用,实时会话状态机与三种缓冲策略,重连后的进度过滤,生命周期与音频预检。在 SDK 总仓库中执行时,测试还会启动 tools/mock-server 的平台模拟服务,验证重试,计费只计一次,两条通道的朗读进度,以及实时会话在服务端断开,网络中断 10 秒,连接重置时的行为。没有 Node.js,单独克隆本仓库,或在不能启动进程的 iOS 模拟器上时,这部分测试自动跳过。持续集成脚本为总仓库的 ci/ios.sh 与 ci/ios-macos.sh。

2026-10-12 在 GitHub Actions 的 macos-14 构建机上用 Xcode 15.4 跑 ci/ios-macos.sh:macOS 上 216 项测试 212 项通过,4 项沙箱用例跳过。iOS 模拟器上 216 项测试 169 项通过,需要启动模拟服务的用例与 4 项沙箱用例共 47 项跳过。iOS 真机版,命令行演示与演示 App 均构建通过。录音器 YuguRecorder 只经过编译,没有单元测试,录音与音频中断尚未在真机验证。

设置 YUGU_SANDBOX_APPKEY 与 YUGU_SANDBOX_SECRET 后,测试另外连接沙箱跑四条端到端用例,未设置时这四条自动跳过,不连接平台。基址缺省为 https://open.shengzhiai.com,可用 YUGU_SANDBOX_BASE 修改。一次运行在沙箱上产生 5 次评测调用,第二次整段评测是重放,不占当日次数。

用例 内容
原生整段评测 同一个幂等键提交两次,第二次为重放,记录号相同
声通兼容整段评测 sent.eval.cn
原生实时评测 完整会话,核对回调顺序与总分,Linux 上经测试用的 WebSocket 客户端与本地转发连接沙箱
原生实时评测朗读进度 打开 realtimeFeedback,按实际语速送入 25 个字的中文例句,核对进度帧逐字给出,进度不倒退,终评带总分
Shell
export YUGU_SANDBOX_APPKEY='<appKey>'
export YUGU_SANDBOX_SECRET='<secretKey>'
swift test --filter SandboxTests

常见问题#

模拟器录音#

iOS 模拟器使用 Mac 的麦克风,首次录音时 macOS 会询问是否允许 Xcode 使用麦克风。模拟器没有输入设备时 start() 报 90202。

Linux 上的实时评测#

Linux 版 Foundation 的 WebSocket 依赖 libcurl 的 ws 支持,Ubuntu 24.04 自带的 libcurl 不包含这项支持,会话会以 90010 失败。Linux 上的整段评测,语音合成与报告查询不受影响。需要在 Linux 上做实时评测时,实现 YuguWebSocketTransport 后通过 webSocketTransportFactory 传入。

签名失败#

鉴权错误码 2003 表示签名不匹配,常见原因是 secretKey 填错,或设备时间与标准时间相差超过 300 秒。SDK 每次尝试都重新生成时间戳与随机串,签名的就是实际发出的文本,不需要自行签名。

重复计费#

写操作与实时会话都带幂等键,重试与重连沿用同一个键,平台对同一个键只计费一次。业务层再次提交同一次作答时,传入首次的 idempotencyKey。

明文 HTTP 地址#

App Transport Security 默认拦截 http:// 地址,此时报 90010。联调本地服务时在 Info.plist 中为该域名添加 ATS 例外,生产地址为 HTTPS,不需要配置。

回调线程#

完成回调与会话回调在 callbackQueue 上执行,缺省为主队列,可直接更新界面。async 写法不经过 callbackQueue。录音器回调在 RecorderOptions.callbackQueue 上执行,缺省同样为主队列,在回调里调用 sendAudio 是线程安全的。

许可#

Apache-2.0,见 LICENSE 与 NOTICE。

更新于 2026-10-12