优谷·雅言SDK 文档2.0.0

iOS 声通平替

STKouyuEngine 是优谷雅言语音评测 SDK 2.0 的 iOS 声通平替包。已经接入声通 STKouyuEngine.framework 的应用,用这个 SwiftPM 包替换声通的 framework 与 libskegn.a,换上优谷雅言的 appKey 与 secretKey,业务代码不改,评测改由优谷雅言云端完成。类名,属性,方法签名,枚举值,常量与代理协议都与声通公开头文件一致,回调里的结果 JSON 与错误 JSON 与安卓平替层一致。

查看 Markdown

版本与环境#

项 值
包地址 https://open.shengzhiai.com/git/stkouyu-ios-compat.git
版本 2.1.2
产品与模块 STKouyuEngine,Objective-C 实现
最低系统 iOS 12,边录边传与朗读进度需要 iOS 13,另支持 macOS 10.15 供测试使用
工具链 Xcode 15 及更新版本,swift-tools-version:5.9
依赖 只用系统框架 Foundation,AVFoundation,CoreGraphics 与 UIKit
许可 Apache-2.0

接入步骤#

需要移除的声通文件#

  1. 在 Xcode 工程里删除 STKouyuEngine.framework 与 libskegn.a,同时删除 Build Phases 里对二者的链接与嵌入设置。
  2. 用 CocoaPods 接入声通的工程,在 Podfile 里删掉 pod '17kouyu-tt' 一行,再执行 pod install。
  3. 工程里自带的 skegn.h 副本可以保留,声明与本包相同。

SwiftPM 依赖#

在 Xcode 菜单 File > Add Package Dependencies 填入包地址 https://open.shengzhiai.com/git/stkouyu-ios-compat.git,规则选 Up to Next Major Version,起始版本 2.1.2,把产品 STKouyuEngine 加到应用 target。

用 Package.swift 管理依赖的工程这样写:

Swift
dependencies: [
    .package(url: "https://open.shengzhiai.com/git/stkouyu-ios-compat.git", from: "2.1.2"),
],
targets: [
    .target(name: "App", dependencies: [
        .product(name: "STKouyuEngine", package: "stkouyu-ios-compat"),
    ]),
]

代码与密钥#

导入写法不变:

Objective-C
#import <STKouyuEngine/STKouyuEngine.h>
#import <STKouyuEngine/KYTestEngine.h>
@import STKouyuEngine;
Swift
import STKouyuEngine
  1. KYStartEngineConfig 的 appKey 与 secretKey 换成优谷雅言控制台的密钥。联调期间用沙箱密钥,说明见优谷雅言 SDK 仓库根目录的 SANDBOX.md。
  2. server 留空或保留声通地址时,请求发往 https://open.shengzhiai.com。私有部署或联调环境写 https:// 开头的完整地址。
  3. 应用原有的 NSMicrophoneUsageDescription 继续生效,平替包不需要新增权限声明。

生命周期示例#

Objective-C:

Objective-C
#import <STKouyuEngine/KYTestEngine.h>

@interface ReadingViewController () <KYTestEngineDelegate>
@end

@implementation ReadingViewController

- (void)setUpEngine {
    KYStartEngineConfig *config = [[KYStartEngineConfig alloc] init];
    config.appKey = @"优谷雅言 appKey";
    config.secretKey = @"优谷雅言 secretKey";
    config.vadEnable = YES;
    [KYTestEngine sharedInstance].delegate = self;
    [[KYTestEngine sharedInstance] initEngine:KY_CloudEngine
                            startEngineConfig:config
                                  finishBlock:^(BOOL isSuccess, NSString *str) {
        NSLog(@"init %d %@", isSuccess, str);
    }];
}

- (void)startReading {
    KYTestConfig *test = [[KYTestConfig alloc] init];
    test.coreType = KYTestType_Sentence_Cn;
    test.refText = @"今天天气很好";
    test.duration = 10000;
    NSString *tokenId = [[KYTestEngine sharedInstance] startEngineWithTestConfig:test result:^(NSString *testResult) {
        NSData *data = [testResult dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
        if (json[@"errId"]) {
            NSLog(@"errId %@ %@", json[@"errId"], json[@"error"]);
        } else {
            NSLog(@"overall %@", json[@"result"][@"overall"]);
        }
    } finishBlock:^(BOOL isSuccess, NSString *str) {
        NSLog(@"start %d %@", isSuccess, str);
    }];
    NSLog(@"tokenId %@", tokenId);
}

- (void)stopReading {
    [[KYTestEngine sharedInstance] stopEngine];   // 结束录音并评测,结果回到 result block
}

- (void)leavePage {
    [[KYTestEngine sharedInstance] cancelEngine]; // 放弃本次评测,不再回调
}

- (void)shutDown {
    [[KYTestEngine sharedInstance] deleteEngine]; // 释放引擎,可重复调用,之后需要重新 initEngine
}

@end

Swift:

Swift
let engine = KYTestEngine.sharedInstance()!
let config = KYStartEngineConfig()
config.appKey = "优谷雅言 appKey"
config.secretKey = "优谷雅言 secretKey"
engine.initEngine(KY_CloudEngine, startEngineConfig: config) { ok, message in print(ok, message ?? "") }

let test = KYTestConfig()
test.coreTypeNS = "sent.eval"
test.refText = "How are you"
let tokenId = engine.start(with: test, result: { json in print(json ?? "") }) { ok, str in print(ok, str ?? "") }
engine.stop()
engine.delete()

Swift 里的方法名由编译器按头文件推导,与使用声通 framework 时相同。编译器去掉方法名里与类名 KYTestEngine 重复的 Engine,也去掉参数名末尾的 Block,常用方法的 Swift 写法如下:

Objective-C Swift
initEngine:startEngineConfig:finishBlock: initEngine(_:startEngineConfig:finish:)
startEngineWithTestConfig:result:finishBlock: start(with:result:finish:)
带录音过程回调的 startEngineWithTestConfig: start(with:onStart:onStartFail:onPause:onTick:onRecording:onRecordEnd:onScoreBlock:finish:)
stopEngine,cancelEngine,deleteEngine stop(),cancel(),delete()
getEngineStatus getStatus()

onScoreBlock 保留 Block,是编译器的推导结果。上面的示例与全部方法的 Swift 写法都写在 Tests/STKouyuEngineTests 里,随测试一起编译。

评测内核#

coreTypeNS 不为空时优先于 coreType。

coreType 对应的 KYTestType 说明
word.eval KYTestType_Word 英文单词
word.eval.pro KYTestType_Word_Pro 英文单词,自适应年龄段
sent.eval KYTestType_Sentence 英文句子
sent.eval.pro KYTestType_Sentence_Pro 英文句子,自适应年龄段
para.eval KYTestType_Paragraph 英文段落
word.eval.cn KYTestType_Word_Cn 中文字词
sent.eval.cn KYTestType_Sentence_Cn 中文句子
para.eval.cn KYTestType_Paragraph_Cn 中文段落
alpha.eval 只能用 coreTypeNS 英文字母
pinyin 只能用 coreTypeNS 拼音题,refPinyin 必填
open.eval.cn 只能用 coreTypeNS 中文开放题,见下方中文开放题一节,2.1.2 起支持

其余内核在本地回调 errId 60003,不发网络请求,包括 open.eval,choice.rec,asr.rec,asr.eval,align.eval,法语,日语与韩语内核,KYTestType_Wordspell,以及声通内核以外的名字。

refText 为空时回调 errId 60006。中文内核与 pinyin 设置了 refPinyin 时可以不设 refText,open.eval.cn 除外。

中文开放题#

2.1.2 起支持 open.eval.cn。KYTestType 没有这个内核的成员,要用 coreTypeNS 指定。平台按原生开放题评分,看内容,语言运用与表达三项,语言固定为中文。

KYTestConfig 属性 用法
refText 必填,与声通相同,填一条或多条参考答案,多条用竖线隔开,写法见下方示例。也可以填题目或任务提示,由评分模型判断是哪一种。是参考答案时,作答与任意一条意思一致就算答对,不要求逐字相同,作答简短不扣分。refPinyin 不能代替 refText
qType KYQType_LookAndSay 即 5 按看图说话评,KYQType_SituationalReply 即 6 按情景问答评,其他取值时按自由表达评。兼容接口不收图片,看图说话按自由表达的细则评分
keypoints,keywords 采分点,keypoints 为字符串数组,keywords 用竖线隔开,两项合计最多 20 个。评分时参考,也用来提高这些词的识别率。参考答案不用来提高识别率
negative_keypoints,keypoints_weight,mode 照常发送,开放题评分不使用
Objective-C
KYTestConfig *test = [[KYTestConfig alloc] init];
test.coreTypeNS = @"open.eval.cn";
test.refText = @"我是学生|我不是学生";
test.qType = KYQType_SituationalReply;

结果 JSON 的 result 含原生开放题的全部字段:overall 总分,content 内容,languageUse 语言运用,delivery 表达,transcript 识别文本,feedback 点评,含义见仓库根目录 RESULTS.md 的开放题一节。openTaskAudit.refKind 为 answers 时评分模型把 refText 当参考答案,为 question 时当题目。另有与声通相同的扁平字段 pronunciation,fluency,integrity,coherence 与 speed,speed 为每分钟字数。结果里没有逐字的 words 与 sentences。录音同样边录边传,realtime_feedback 为 YES 时中间结果只有已收音频的字节数 bytes,没有朗读进度。

参数映射#

评测请求为 POST {平台地址}/{coreType},multipart/form-data,文本字段不带文件名,音频为 audio 文件字段。签名覆盖全部非空文本字段。

KYTestConfig 属性 表单字段 说明
coreTypeNS 与 coreType 请求路径 /{coreType} 见评测内核
refText refText 非空时发送
refPinyin refPinyin 非空时发送
ageGroup agegroup 设为 1,2 或 3 时发送
scale scale 非 0 时发送,整数不带小数点
precision precision 非 0 时发送
slack slack 非 0 时发送
isParagraphNeedWordScore paragraph_need_word_score para.eval 与 para.eval.cn 一律发送 1,customParams 也改不掉。其他内核 YES 时发送 1
phoneme_output phoneme_output 默认 YES,YES 时发送 1
attachAudioUrl attachAudioUrl YES 时发送 1,平台在结果 JSON 末尾返回录音的下载地址 audioUrl,地址保留 7 天,到期删除。录音同时保存在本机,路径用 getLastRecordPath 获取
phonemeOption dict_type 设置后发送 CMU,KK 或 IPA88
dict_dialect dict_dialect 原样发送
customized_lexicon customized_lexicon 字典序列化为 JSON 文本
customized_pron customized_pron 字典序列化为 JSON 文本
readtype_diagnosis readtype_diagnosis 非 0 时发送,结果的 words 另含增读与重复读,默认只含参考文本里的字词
output_rawtext output_rawtext YES 时发送 1
punctuate punctuate YES 时发送 1
itn itn 非 0 时发送
detect_nonscorable detect_nonscorable YES 时发送 1
vad_detection vad_detction YES 时发送 1,字段名与安卓平替层相同
keywords keywords 原样发送,open.eval.cn 用作采分点
keypoints keypoints 数组序列化为 JSON 文本,open.eval.cn 用作采分点
keypoints_weight keypoints_weight 非 0 时发送
negative_keypoints negative_keypoints 数组序列化为 JSON 文本
negativeReftext negativeReftext 原样发送
mode mode KYModeType_Home 时发送 home
qType qType 非 0 时发送数值,open.eval.cn 据此选看图说话或情景问答
customParams 键名即字段名 每个键值一个字段,与映射字段同名时覆盖映射值,空值不发送
request request 字典序列化为 JSON 文本,排在最后,平台合并其中的标量字段
realtime_feedback 不是表单字段 YES 时参数帧带 realtime_feedback,评测过程中回调朗读进度,见朗读进度一节。录音与 feed 模式不论本项都边录边传,上表的字段放进参数帧,见边录边传一节

customParams 的值转成文本的规则:字符串原样,布尔值为 true 或 false,整数不带小数点,小数取最短形式,数组与字典为 JSON 文本。

只在本机生效的属性:

属性 作用
audioPath 评测已有的音频文件,开始后立即上传
isStream 不录音,由 feedAudioData:audioLength: 送入音频。16 kHz 16 位单声道 PCM 边送边传,stopEngine 后只上传剩余部分,其他格式在 stopEngine 后整段上传
audioType,sampleRate,channel,sampleBytes feed 与 audioPath 的音频格式。wav 类型的 PCM 加上 WAV 头后上传,其他类型原样上传
recordPath,recordName 录音文件位置,默认 Documents/record/<tokenId>.wav
duration,durationInterval 最长录音时长与倒计时回调间隔,单位毫秒
seek,ref_length VAD 的说话结束静音时长与最短说话时长,单位 10 毫秒
soundIntensityEnable VAD 回调带上音强
recordCallbackInterval VAD 回调的最小间隔,单位毫秒
forceRecord 只有 stopEngine 才结束本次评测
autoRetry,errIds 自动重评
getParam 结果 JSON 带上 params
serverTimeout 本次请求的超时,单位秒
userId 写入结果 JSON 的 userId

照收但不生效的属性:

属性 说明
coreProvideType 一律云端评测,值为 native 时打印警告日志
compress,quality,complexity,vbr,max_ogg_delay 上传无损 WAV,不做 Speex 压缩
customized_sig,customized_sig_url 按 appKey 与 secretKey 签名
refAudio align.eval 平台不支持
phoneme_diagnosis,blend_phoneme_enable 不在映射表里,需要时经 customParams 发送
serialNumber,protocol 不使用

初始化参数#

KYStartEngineConfig 属性 平替包里的作用
appKey,secretKey 必填,用于请求签名,为空时初始化失败,secretKey 不写入日志
server 为空或主机在 stkouyu.com 下时用 https://open.shengzhiai.com。其他 http,https,ws,wss 开头的地址作为平台地址,ws 换成 http,wss 换成 https。没有这四种前缀或没有主机名的地址使初始化失败
connectTimeout,serverTimeout 单位秒,默认 20 与 60,单次请求的超时取二者中较大的值
vadEnable,seek 本机能量 VAD,seek 默认 60
sdkLogEnable,logLevel,sdkLogPath,isOutputLog 日志,见日志一节
customized_avaudiosession YES 时平替包不设置 AVAudioSession,由应用自己设置
证书与离线参数 isUseOnlineProvison,isUpdateProvison,provison,native,native_db_path,native_cn,ailocalAddress 照收,不生效
其余参数 enable,serverList,sdkCfgAddr,autoDetectNetwork 照收,不生效

KY_NativeEngine 与 KY_MultiEngine 照收,按云端评测,打印警告日志。KY_CloudServer_Release 与 KY_CloudServer_Gray 为声通文档里的两个地址,作为 server 时都改用 https://open.shengzhiai.com。

回调规则#

全部回调在主线程,按发生顺序回调。一次评测在结果回调里收到一份结果 JSON 或错误 JSON,realtime_feedback 为 YES 时在此之前还会收到若干份 eof 为 0 的朗读进度 JSON,见朗读进度一节。调用 cancelEngine 或 deleteEngine 之后这次评测不再有任何回调。结果回调指 testResultBlock 或 onScoreBlock,同时回调代理的 kyTestEngineDidScore:。

开始成功时 finishBlock 收到 YES 与 tokenId,录音与 feed 模式随后回调 onStartBlock 与 kyTestEngineDidRecordStart。开始失败时 finishBlock 收到 NO 与错误 JSON,其余回调与安卓平替层一致:

情况 errId 其余回调
引擎未初始化 60007 结果回调
上一次评测还没有结果 60008 onStartFailBlock 收到 engine is busy,代理不回调,正在进行的评测不受影响
coreType 不支持 60003 结果回调
refText 为空 60006 结果回调
麦克风不可用或没有录音权限 60004 onStartFailBlock 与 kyTestEngineDidRecordStartFail:

录音过程的回调:

  1. 录音结束时回调 onRecordEndBlock,kyTestEngineDidRecordEnd 与 kyTestEngineDidRecordWriteAudioResult:。音频已在录音过程中上传,随后发出剩余音频与结束帧,等待评测结果。YuguCompat.streamUpload 为 NO 时此时整段上传,见边录边传一节。
  2. 设置 duration 后每隔 durationInterval 回调剩余毫秒与剩余百分比,百分比取值 0 到 100,到时结束录音。
  3. vadEnable 为 YES 时回调 vad_status 与 sound_intensity。vad_status 为 0 表示没开始说话,1 表示说话中,2 表示说话结束。说话后静音达到 seek 乘以 10 毫秒即判定结束,录音模式随即结束录音。soundIntensityEnable 为 NO 时 sound_intensity 为 0,为 YES 时取 0 到 100。
  4. forceRecord 为 YES 时 VAD 与 duration 都不结束评测。到达 duration 时停止录音,保留音频,调用 stopEngine 后才上传。
  5. 电话等系统打断录音时回调 onPauseBlock,打断结束后继续录音。
  6. audioPath 模式开始后立即上传,文件不可读与音频不合格都以错误 JSON 回到结果回调。

其他接口:

接口 行为
stopEngine 结束录音或 feed,上传剩余音频后评测,有结果回调
cancelEngine 停止录音与上传,没有结果回调
deleteEngine 取消评测,释放录音器与播放器,恢复为未初始化,可以重复调用
getEngineStatus 录音或接收 feed 期间返回 YES
getLastRecordPath 最近一次录音文件路径,没有录音时为空字符串
feedAudioData:audioLength: 没有可接收音频的评测时回调 kyTestEngineDidRecordFeedFail:
playback,playWithPath: 与带结束回调的两个方法 回调 kyTestEngineDidPlayStart,kyTestEngineDidPlayStartFail: 与 kyTestEngineDidPlayEnd,播放结束时调用传入的 block
stopPlay 停止播放,回调播放结束
activeAudioSession 把音频会话设为 PlayAndRecord 后激活
updateProvision 三个方法 返回 YES
inquireProvision 两个方法 返回 YES,回调 {"provision":"cloud","message":"cloud mode, no provision file needed"}

结果 JSON#

JSON
{"tokenId":"3f0b6a1c9d2e4f5a8b7c6d5e4f3a2b1c","recordId":"eval_77c081e19541","applicationId":"优谷雅言 appKey",
 "userId":"u1","refText":"今天天气很好","eof":1,"dtLastResponse":"2026-10-08 16:21:32:255",
 "result":{"overall":94.6,"pronunciation":100,"fluency":96,"integrity":100,"words":[]},
 "params":{"app":{"applicationId":"优谷雅言 appKey","userId":"u1","timestamp":"1791447692"},
           "audio":{"audioType":"wav","sampleRate":16000,"channel":1,"sampleBytes":2},
           "request":{"coreType":"sent.eval.cn","tokenId":"3f0b6a1c9d2e4f5a8b7c6d5e4f3a2b1c","refText":"今天天气很好","phoneme_output":"1"}}}
  1. result 为平台兼容接口返回的评测结果,原样转交,常用取分字段与声通同名。只有一处补充:平台把逐字得分放在 sentences[].details[].scores 里,声通示例直接读取 details[].overall,所以 details[] 里缺少 overall 的条目在末尾补上取自 scores.overall 的 overall,条目没有 pronunciation 时一并补上取自 scores.pronunciation 的 pronunciation。原有的键不改不删,其余内容逐字节不变。
  2. recordId 为平台评测记录号,平台没有返回时不出现。
  3. params 只在 getParam 为 YES 时出现。audioUrl 只在 attachAudioUrl 为 YES 时出现,位于 JSON 末尾,取值为平台返回的录音下载地址,原样转交。平台保留录音 7 天,到期删除,需要长期保存的录音在 7 天内下载,或用 getLastRecordPath 取本机录音。同一 tokenId 重复提交时平台返回同一个地址。
  4. para.eval 与 para.eval.cn 一律请求逐字得分,结果总带 result.sentences[].details[]。
  5. dtLastResponse 为本机时区收到结果的时刻,格式 yyyy-MM-dd HH:mm:ss:SSS。

边录边传#

录音模式与 feed 模式默认边录边传:音频在录音过程中经声通兼容实时评测接口 wss://{平台地址}/{coreType} 上传,stopEngine 时只剩最后一小段音频与结束帧,平台随即评测,不再等录音结束后整段上传。录音文件照旧写入 recordPath,getLastRecordPath 与回放不变。

  1. feed 模式要求 audioType 为 wav 或 pcm,sampleRate 为 16000,channel 为 1,sampleBytes 为 2,其他格式在 stopEngine 之后整段上传。audioPath 模式总是整段上传。
  2. 需要 iOS 13 或更新版本,iOS 12 上照旧整段上传,日志级别为 info 时打印一行原因。
  3. 不打开 realtime_feedback 时参数帧不带 realtime_feedback,平台不下发进度,结果回调只收到终评 JSON,与整段上传时相同。打开时回调朗读进度,见朗读进度一节。
  4. 结束帧发出之前连接失败,例如握手被拒,断网或心跳超时,stopEngine 之后用同一个 tokenId 整段上传评测,日志级别为 warn 时打印失败原因。结束帧发出之后连接失败时按朗读进度一节第 8 条重新连接。同一个 tokenId 只评测一次,只计费一次。
  5. YuguCompat.streamUpload 设为 NO 之后开始的评测与 2.1.0 相同,录音结束后整段上传。realtime_feedback 为 YES 的评测仍然边录边传,回调朗读进度。
Objective-C
#import <STKouyuEngine/YuguCompat.h>

YuguCompat.streamUpload = NO;   // 录音结束后整段上传,与 2.1.0 相同,默认 YES
Swift
import STKouyuEngine.YuguCompat

YuguCompat.streamUpload = false

压缩#

  1. 整段上传的评测结果:NSURLSession 自动在请求头带上 Accept-Encoding: gzip, deflate, br,回调之前自动解压,平台对评测结果做 gzip 压缩,1.5 MB 的段落结果在网络上约 120 KB。平替包不改这个请求头。
  2. 边录边传的终评帧:平替包使用系统的 NSURLSessionWebSocketTask,由系统自行在握手里提出 permessage-deflate,平台接受后终评帧压缩传输,系统解压之后交给平替包。系统同时压缩上行的音频帧。平替包不另写 WebSocket 实现,也不改握手里的这个请求头,系统没有提出时终评帧不压缩传输。
  3. 是否提出由系统版本决定,Apple 的接口文档没有说明。Apple 开发者论坛上 Apple 技术支持工程师的答复与抓包显示 URLSession 的 WebSocket 握手带 Sec-WebSocket-Extensions: permessage-deflate,出处与核对情况见验证状态一节。
  4. 分段耗时日志里 deflate 表示这一次协商成功,gzip 表示整段上传的结果经 gzip 压缩,wire 为结果在网络上的字节数。系统不报告终评帧在网络上的字节数,边录边传时 wire 记为 -。

朗读进度#

KYTestConfig.realtime_feedback 设为 YES 时,录音与 feed 模式的评测经声通兼容实时评测接口 wss://{平台地址}/{coreType} 边读边上传音频,参数帧带 realtime_feedback。平台在收音过程中回报参考文本里读到了哪里,平替包把进度以 eof 为 0 的 JSON 交给结果回调,与终评使用同一个回调,结构同声通 realtime_feedback 的中间结果。界面可以据此让参考文本跟随朗读位置。终评 JSON 与不打开时相同,同一个 tokenId 只评测一次,只计费一次。

JSON
{"tokenId":"3f0b6a1c9d2e4f5a8b7c6d5e4f3a2b1c","recordId":"","applicationId":"优谷雅言 appKey","userId":"u1",
 "refText":"北京你好","eof":0,"dtLastResponse":"2026-10-12 14:03:21:456",
 "result":{"kernel_version":"1.0.0","resource_version":"1.0.0",
  "words":[{"charType":0,"scores":{"pronunciation":100,"skipped":0,"tone":100,"overall":100,"overall_pron":100},
            "word":"京","word_parts":[{"beginIndex":0,"charType":0,"endIndex":0,"part":"京"}]}],
  "bytes":48000,"readUnits":2,"matchedUnits":2,"totalUnits":4,
  "progress":[{"word":"北","read":true,"matched":true},{"word":"京","read":true,"matched":true},
              {"word":"你","read":false,"matched":false},{"word":"好","read":false,"matched":false}]}}
字段 说明
eof 0 为朗读进度,终评与错误 JSON 为 1
recordId 空串,评测记录编号在终评中给出
result.words 上一次进度回调之后新读到的字或词,按参考文本顺序。各次回调的 words 依次拼起来就是已读的部分,每个字或词只回调一次
result.words[].scores 只表示识别结果与参考文本是否一致:一致时各项为 100,skipped 为 0,不一致时各项为 0,skipped 为 1,英文的 tone 为 0。回调之后不再更新,发音与声调的得分以终评为准
result.words[].charType 0 为汉字,1 为英文
result.progress 参考文本全部的字或词,read 为朗读已越过,matched 为识别结果一致,每次回调都是最新状态。一次评测之内两者只会由 false 变为 true,matched 为 true 时 read 也为 true
result.readUnits,result.matchedUnits,result.totalUnits 已读的个数,识别一致的个数,总数。readUnits 即当前朗读位置
result.bytes 平台这一轮已收到的音频字节数
result.kernel_version,result.resource_version 与终评相同
  1. 中文按字,去掉标点与空白。英文按空白分词,去掉词首词尾的标点,词内的撇号与连字符保留。coreType 以 .cn 结尾或为 pinyin 时按字,其余按词,customParams 带 language 时按 language 判断。
  2. 读到新的字或词时平台立即下发进度,此外大约每 0.5 秒音频下发一次。没有新读到内容的进度不回调,只有字节数的进度也不回调。进度一般比朗读晚 1 到 2 秒,随网络情况变化。
  3. stopEngine 发出结束帧之后,平台对最后一段音频再识别一次,句末的几个字以进度回调送达,然后才是终评。终评之后不再有进度回调。
  4. 朗读进度只对 16 kHz 16 位单声道 PCM 生效。录音模式总是满足,feed 模式要求 audioType 为 wav 或 pcm,sampleRate 为 16000,channel 为 1,sampleBytes 为 2。audioPath 模式与其他音频照旧整段评测,不回调进度。
  5. 需要 iOS 13 或更新版本。iOS 12 上照旧整段评测,不回调进度,日志级别为 info 时打印一行说明。
  6. 参考文本为空或平台当时无法跟踪进度时,这一轮没有进度回调,终评不受影响。
  7. 结束帧发出之前连接失败,例如握手被拒,断网或心跳超时,平替包关闭连接,不再回调进度,录音照常进行,stopEngine 之后用同一个 tokenId 整段评测,与 YuguCompat.streamUpload 为 NO 时相同。平台没有收到结束帧时不评测,不计费。
  8. 结束帧发出之后连接失败或终评超时,平替包用同一个 tokenId 重新连接,重发参数帧,全部音频与结束帧。重连次数与整段评测的重试次数相同,默认 2 次,用尽后回调与整段评测相同的错误 JSON,errId 为 20009。平台按 tokenId 只评测一次,重放期间平台从第一个字重新计算进度,已经回调过的字或词按位置去掉,不会重复回调。
  9. 握手的 query 带 appKey,timestamp,nonce,idempotencyKey 与 signature,签名算法与整段评测相同,参数帧为参数映射一节的表单字段加 realtime_feedback,日志级别为 info 或 debug 时另加 timing,最后是 idempotencyKey。结束帧发出之前每 15 秒发一次协议层 ping,30 秒收不到 pong 视为断线。

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

Objective-C
#import <STKouyuEngine/KYTestEngine.h>
#import <UIKit/UIKit.h>

/// 按 result.progress 给参考文本着色:已读且对上为绿色,已读未对上为橙色,未读为灰色
static NSAttributedString *ColoredRefText(NSString *refText, NSArray<NSDictionary *> *progress) {
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc]
        initWithString:refText attributes:@{NSForegroundColorAttributeName : UIColor.grayColor}];
    NSUInteger from = 0;
    for (NSDictionary *unit in progress) {
        NSRange r = [refText rangeOfString:unit[@"word"] options:0 range:NSMakeRange(from, refText.length - from)];
        if (r.location == NSNotFound) break;
        from = NSMaxRange(r);
        if (![unit[@"read"] boolValue]) continue;
        [text addAttribute:NSForegroundColorAttributeName
                     value:[unit[@"matched"] boolValue] ? UIColor.greenColor : UIColor.orangeColor
                     range:r];
    }
    return text;
}

static void StartReadingWithProgress(UILabel *label) {
    KYTestConfig *test = [[KYTestConfig alloc] init];
    test.coreType = KYTestType_Sentence_Cn;
    test.refText = @"我是中国人,我是中国人。我们都是中国人。中华民族是一家人。";
    test.realtime_feedback = YES;
    label.attributedText = ColoredRefText(test.refText, @[]);
    [[KYTestEngine sharedInstance] startEngineWithTestConfig:test
        result:^(NSString *testResult) {
            NSData *data = [testResult dataUsingEncoding:NSUTF8StringEncoding];
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
            if ([json[@"eof"] integerValue] == 0) {
                label.attributedText = ColoredRefText(test.refText, json[@"result"][@"progress"]);
            } else if (json[@"errId"]) {
                NSLog(@"errId %@ %@", json[@"errId"], json[@"error"]);
            } else {
                NSLog(@"overall %@", json[@"result"][@"overall"]);
            }
        }
        finishBlock:nil];
}

result.progress 每次都是全部字或词的最新状态,整段重新着色即可。只需要跟随朗读位置时,取 result.readUnits,或者累加各次回调 result.words 的个数。示例与测试一起编译,副本在 Tests/STKouyuEngineObjCSupport 里。

错误 JSON 与 errId#

JSON
{"tokenId":"3f0b6a1c9d2e4f5a8b7c6d5e4f3a2b1c","errId":20009,"error":"网络或服务端临时故障,可重评,autoRetry 默认重评此码: HTTP 503 code=50000 system busy (attempts 3)","eof":1,"applicationId":"优谷雅言 appKey"}
errId 含义 来源
20009 网络或服务端临时故障,重试后仍失败 网络错误,超时,HTTP 408,425,429,500,502,503,504,以及平台可重试错误码
平台错误码 平台业务错误,例如 40001,40100,40300,2003,2010,40902 错误响应体的 code
60001 音频文件不存在或不可读 audioPath
60002 音频为空 本机检查,不上传
60003 coreType 不支持 本机检查,不发请求
60004 麦克风不可用或没有录音权限 录音开始
60005 音频短于 1 秒 本机检查 WAV 与 PCM 时长,不上传
60006 refText 为空 本机检查,不发请求
60007 引擎未初始化 本机检查
60008 引擎正忙,上一次评测未结束 本机检查
60009 音频大于 50 MB 或长于 300 秒 本机检查文件大小与 WAV 和 PCM 的时长,不上传
90005 平台响应无法解析 成功响应里没有 result
90011 证书校验失败 TLS 错误,或 App Transport Security 拦截了 http 地址

错误响应体没有错误码时按 HTTP 状态取码:401 为 40100,403 为 40300,404 为 40400,409 为 40900,501 为 50010,其他 4xx 为 40001,其他 5xx 为 50000,其余状态为 90005。完整错误码表见优谷雅言 SDK 仓库根目录的 ERRORS.md。

error 字段:errId 20009 时依次为 20009 的含义,HTTP 状态与错误码,平台消息与尝试次数,其他 errId 为平台消息或本机检查的说明。

重试与自动重评#

  1. 每次评测的 tokenId 同时作为请求头 Idempotency-Key,重试与自动重评都用同一个 tokenId。平台对同一个 tokenId 只计费一次,重复提交得到第一次的结果。
  2. 网络错误,超时,可重试的 HTTP 状态与可重试的错误码自动重试,一次提交最多 3 次请求,两次重试的间隔约 200 毫秒与 400 毫秒,上下浮动 30%。响应带 Retry-After 时至少等待该时长,最多 30 秒。重试与等待限定在一次评测 300 秒的总时限内,下一次重试会越过总时限时不再重试,直接回调错误 JSON。
  3. 400 等不可重试的错误不重试,直接回调错误 JSON。
  4. autoRetry 为 YES 时,错误 JSON 的 errId 落在 errIds 里的评测用同一个 tokenId 重新提交,最多 2 次。errIds 默认为 @[@"20009"],元素可以是字符串或数字。
  5. YuguCompat 可以调整重试次数与总时限。

与声通 SDK 的行为差异#

项 声通 SDK 平替包
引擎 在线,离线,双引擎 只有云端评测
内核 多语种多题型 中文与英文的字词,句子,段落,字母与拼音题
中间结果 realtime_feedback 返回中间结果 realtime_feedback 为 YES 时同样以 eof 为 0 的 JSON 回调新读到的字或词,结构同声通,分数只表示是否读对,需要 iOS 13,见朗读进度一节
上传音频 可用 Speex 压缩 录音与 PCM feed 默认边录边传 16 kHz 单声道 16 位 PCM,streamUpload 为 NO 时录音结束后整段上传 WAV,audioPath 整段上传
录音文件 WAV 或 MP3 一律 WAV 编码,recordName 写 .mp3 时保留原文件名
证书 离线证书 不需要证书,证书相关方法返回 YES
音频地址 attachAudioUrl 返回录音的下载地址 同样返回,字段为结果 JSON 末尾的 audioUrl,平台保留 7 天,到期删除
鉴权 可用 customized_sig 一律按 appKey 与 secretKey 签名,secretKey 必填
结果 JSON 声通评测字段 外层字段一致,result 为优谷雅言评测结果,details[] 条目补上 overall 与 pronunciation
并发 由引擎决定 一个引擎同一时间只做一次评测,上一次结果回调之前开始新评测回调 60008
录音时长 由引擎决定 录音与 PCM 流到 300 秒时自动结束,随即评测,音频最大 50 MB,短于 1 秒不上传

C 接口#

skegn.h 的声明与声通公开的 skegn.h 相同,由本包实现,替换 libskegn.a。直接调用 C 接口的代码不用改,#include "skegn.h" 可以继续使用工程里的副本,也可以改为 #import <STKouyuEngine/skegn.h>。Swift 用 import STKouyuEngine.skegn。

函数 平替包里的行为
skegn_new 读取 cfg JSON 的 appKey,secretKey 与 cloud.server,缺少密钥时返回 NULL
skegn_start 读取 param JSON 的 app.userId,audio 与 request,request 的成员作为表单字段发送,id 收到 tokenId
skegn_feed 收集音频
skegn_stop 上传评测,结果 JSON 经 callback 以 SKEGN_MESSAGE_TYPE_JSON 回调,回调在内部队列上执行,不在主线程
skegn_cancel 取消,不回调
skegn_delete 取消评测,释放引擎
skegn_opt 版本,模块,流量与证书查询写入文本,返回文本长度
skegn_get_device_id 本机首次生成后保存的设备标识
skegn_update_provision,skegn_inquire_provision 返回 0,查询时回调云端说明
skegn_get_last_error 最近一次错误码,数值与声通 skegn_errno.h 相同

request 成员转成文本的规则:字符串原样,JSON 布尔值为 1 或 0,数字取最短形式,数组与对象为 JSON 文本。request.getParam 为真时结果带上 params,request.autoRetry 为真时按 errId 20009 自动重评。回调里可以调用 skegn_cancel 与 skegn_start。

朗读进度与边录边传只在 KYTestEngine 上提供。C 接口照旧在 skegn_stop 之后整段评测,request 里的 realtime_feedback 与其他成员一样作为表单字段发送,不回调进度。日志级别为 info 时同样每次评测打印一行分段耗时。

扩展配置#

声通接口以外的配置集中在 YuguCompat.h,不改变原有声明。

Objective-C
#import <STKouyuEngine/YuguCompat.h>

YuguCompat.baseURL = @"https://sandbox.example.com";   // 优先于 KYStartEngineConfig.server,nil 取消
[YuguCompat setLogLevel:YuguCompatLogLevelDebug];      // 优先于 KYStartEngineConfig.logLevel
[YuguCompat setLogHandler:^(NSInteger level, NSString *message) { /* 写入应用自己的日志 */ }];
YuguCompat.maxRetries = 2;      // 一次提交内的重试次数,0 到 5
YuguCompat.totalTimeout = 300;  // 一次评测的总时限,单位秒
YuguCompat.streamUpload = YES;  // 录音与 feed 边录边传,默认 YES,见边录边传一节
NSLog(@"%@ %@", YuguCompat.sdkVersion, YuguCompat.effectiveBaseURL);

Swift 用 import STKouyuEngine.YuguCompat。

日志#

  1. 级别 0 为 error,1 为 warn,2 为 info,3 为 debug,默认 warn,与 KYLOG_ERROR,KYLOG_WARN,KYLOG_INFO,KYLOG_DEBUG 的取值相同。
  2. isOutputLog 为 YES 时写控制台,sdkLogEnable 为 YES 时另外写入 sdkLogPath,默认 Documents/sdkLog.txt。
  3. 日志不含 secretKey,签名与音频内容,appKey 只记前 4 位。
  4. 应用调用 KYLog 的内容写入同一日志。
  5. warn 级别下评测成功时不打印日志。需要看评测各步骤耗时时打开 info 级别,见分段耗时一节。

打开日志#

声通文档里的写法照样生效:sdkLogEnable 设为 YES,logLevel 设为 2,日志级别为 info,日志写入控制台与 sdkLogPath。

Objective-C
KYStartEngineConfig *config = [[KYStartEngineConfig alloc] init];
config.sdkLogEnable = YES;   // 写入日志文件
config.logLevel = 2;         // info,每次评测打印一行分段耗时
config.sdkLogPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"sdkLog.txt"]; // 可选,默认 Documents/sdkLog.txt
  1. 日志文件默认在应用沙盒的 Documents/sdkLog.txt,在 Xcode 的 Devices and Simulators 窗口下载应用容器即可取出,模拟器上直接在 xcrun simctl get_app_container booted <bundle id> data 给出的目录里查看。
  2. [YuguCompat setLogLevel:YuguCompatLogLevelInfo] 覆盖 logLevel,setLogHandler: 把日志交给应用自己的日志系统,交给 handler 时不再写控制台,文件照写。
  3. C 接口在 skegn_new 的 cfg 里写 "sdkLog":{"enable":1,"level":2,"output":"<日志文件路径>"},level 也接受 error,warn,info,debug 这几个名称。

分段耗时#

日志级别为 info 或 debug 时,每次到达平台的评测在结果回调开始之后打印一行 timing,字段与安卓平替层相同。此时请求另带 timing=1,实时评测的参数帧另带 "timing":1,平台在结果里附上平台侧耗时,平替包读取之后从结果中去掉,应用收到的结果 JSON 与 warn 级别时相同。warn,error 与关闭日志时不请求,不打印。

文本
timing tokenId=3f0b6a1c9d2e4f5a8b7c6d5e4f3a2b1c mode=ws audio=31.20s afterStop=1830ms upload=120ms wait=1610ms download=- parse=40ms deliver=3ms handshake=85ms queued=6400B final=1532118B wire=- deflate server.total=1321ms server.engine=1100ms server.platform=221ms
timing tokenId=9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d mode=rest audio=12.30s afterStop=6150ms upload=1884ms wait=4120ms download=85ms parse=40ms deliver=10ms attempts=1 sent=394012B body=1532118B wire=122880B gzip server.total=3910ms server.engine=3700ms server.platform=210ms
字段 含义
mode ws 为边录边传,rest 为录音结束后整段上传或 audioPath 模式,ws>rest 为边录边传在结束帧之前失败后整段上传,行末 fallback 为失败原因
audio 音频时长,单位秒,压缩格式的音频为 -
afterStop 从 stopEngine 或 VAD,duration,300 秒上限结束录音到结果回调开始,audioPath 模式从开始评测算起
upload 从结束录音到最后一个字节发出:边录边传为结束帧写出,整段上传为最后一次请求的请求体发完
wait 从上传完成到收到结果的第一个字节。边录边传时系统只交出完整的终评帧,算到终评帧收齐,含下载
download 结果从第一个字节到最后一个字节,边录边传为 -
parse 解析平台结果,组装回调 JSON
deliver 从组装完成到主线程开始执行结果回调
handshake 边录边传时从发起连接到握手完成
queued 结束录音时还没有写出到网络的音频字节数
final,body 结果的字节数,按解压之后计
wire 结果在网络上的字节数,压缩时小于 body,系统不报告时为 -
deflate,gzip 终评帧协商了 permessage-deflate,整段上传的结果经 gzip 压缩
attempts,connections 整段上传的请求次数,边录边传的连接次数,只连接一次时不打印
sent 整段上传的请求体字节数
server.total,server.engine,server.platform 平台从开始处理到结果备好的总时长,其中调用评测引擎的时长,以及平台其余部分的时长。整段上传取自响应头 Server-Timing,边录边传取自终评里的平台耗时,平台没有给出时不打印
replayed,errId 平台按 tokenId 重放了已有的结果,评测失败时的 errId
  1. 不明的值记为 -。
  2. debug 级别另外打印两行:phases 为各阶段相对第一个阶段的时刻,engineStages 为平台评测引擎各环节的耗时与平台收到请求的时刻。
  3. 判断慢在哪一步:afterStop 接近 server.total 时时间花在平台评测上,server.engine 占大头时花在评测引擎上。upload 大说明上行慢,结束录音时积压的音频看 queued。wait 减去 server.total 约为网络往返与终评帧的下载。整段上传时 download 大说明下行慢。

头文件一致性核对#

tools/headers-diff.py 随包发布,接入方可以用手上的声通 framework 核对两边的公开接口是否逐项一致。只需要 Python 3.7 或更新版本,不依赖第三方库。

Shell
# 克隆本包后在包目录执行,第二个参数默认为本包的头文件目录
python3 tools/headers-diff.py /path/to/STKouyuEngine.framework/Headers
# 连同 skegn.h 一起比对,参数名不同也算差异
python3 tools/headers-diff.py --original /path/to/STKouyuEngine.framework/Headers \
    --original /path/to/skegn.h --strict --json headers-diff.json

比对内容:类与父类,协议与继承关系,属性及其修饰符与类型,方法的选择子,返回类型与参数类型,协议方法的 @optional 与 @required,枚举的成员与取值,typedef 与 block 类型,结构体,外部常量与函数及其属性宏,宏的取值,头文件保护宏,以及 framework 内部头文件之间的导入。注释,空白与换行不影响结果,UIKit 与 Foundation 等系统头文件的导入不比对。

退出码:0 表示一致,1 表示有差异,2 表示参数错误或无法解析。只存在于本包的 YuguCompat.h 列出但不比对。

验证状态#

Linux 上的结果与沙箱用例截至 2026-10-13,对应 2.1.2。macOS 构建机上的结果截至 2026-10-13,对应 2.1.1,GitHub Actions 运行 38347556835 的 11 个作业全部通过。2.1.2 的 Objective-C 层只改了版本号与头文件注释:

部分 状态
C 核心 签名,参数映射,multipart,WAV,VAD,重试判断,错误映射与结果 JSON 组装都在 Sources/STKouyuEngine/core。2.1.0 的握手地址与签名,参数帧,进度帧解析,eof 为 0 的 JSON 组装,按位置去重,以及实时评测的连接,心跳,回退整段评测与结束后重连的判断也在这里。2.1.1 的参数帧选项,Server-Timing 与平台耗时字段的解析,分段耗时日志的格式化也在这里,平台耗时字段不进结果 JSON 有专门的用例。2.1.2 的内核表放开 open.eval.cn,开放题的 refText 不能由 refPinyin 代替,也有专门的用例。Linux 上用 gcc 13 按 C99 编译零警告,92 组单元测试 13634 项检查全部通过,AddressSanitizer 与 UndefinedBehaviorSanitizer 下同样通过,行覆盖率 98.91%,实时评测与分段耗时部分 100%。2026-10-08 在 macOS 上用 Xcode 15.4 的 clang 跑过 2.0.0 的同样步骤,结果一致。按 2026-10-08 录下的平台返回核对,平台返回的 audioUrl 原样进入结果 JSON 末尾,单句段落与多句段落的 details 补充方式相同。按 2026-10-12 录下的四份进度帧与共享签名向量核对朗读进度
集成测试 C 核心按 KYTestEngine 的请求方式连接平台模拟服务 tools/mock-server/server.mjs,39 个场景 798 项检查在 Linux 上通过,其中 2.0.0 的 21 个场景 2026-10-08 在 macOS 上也通过。2.1.0 的 12 个场景经测试用的 WebSocket 客户端跑实时评测:模拟服务按同一算法校验握手签名,进度回调的 words 依次拼起来不重不漏,握手被拒,结束前断线与心跳超时时用同一个 tokenId 整段评测,结束时断线,结束后出错与终评超时时重连重放,只计费一次,重试用尽得到 20009,进度不一致与没有进度两种故障,以及短于 1 秒的录音不发结束帧。2.0.0 的场景覆盖 500 与 429 重试,读超时与平台处理中两种情形下同一 tokenId 的多次提交只计费一次,409 40901 等待后重试,每次请求换新的 nonce,段落内核的逐字得分与 details 补充,attachAudioUrl 作为表单字段发送,400 不重试,autoRetry,签名错误,以及本机错误不发请求。2.1.1 的 5 个场景:不带 realtime_feedback 的默认边录边传,参数帧不带 realtime_feedback,握手被拒时用同一个 tokenId 整段评测,info 级别时请求带 timing=1 与 "timing":1,模拟服务照常验签,以及连接测试用服务 core-tests/integration/timing_server.py 的整段评测与实时评测,核对 Server-Timing,平台耗时字段不进结果 JSON 与分段耗时日志的各字段。2.1.2 的 1 个场景:open.eval.cn 走自己的路径,refText 与 qType 照原样发送,refPinyin 不能代替开放题的 refText,结果原样进入结果 JSON,实时评测不回调朗读进度
沙箱测试 设置 YUGU_SANDBOX_APPKEY 与 YUGU_SANDBOX_SECRET 后,ci/ios-stcompat.sh 的 sandbox 步骤让同一套 C 核心经 HTTPS 连接真实平台,校验证书与主机名,依次跑八个用例。sent.eval.cn 句子评测得到数值型 result.overall,结果 JSON 不带 audioUrl,para.eval.cn 段落评测的 details 每项带 overall 与 pronunciation,带 attachAudioUrl 的句子评测在结果 JSON 末尾得到平台返回的 https 下载地址 audioUrl,平台不认识的 appKey 得到鉴权类 errId,pinyin 题缺 refPinyin 得到 40001,两个错误用例都只发一次请求,不触发评测。实时评测用例按实时速度经 wss 上传客户例句,核对进度回调与终评。每次运行最多 8 次平台调用,密钥只经环境变量传给测试程序,不出现在命令行与日志里。没有密钥时这一步记为跳过。2026-10-08 平台更新后实测五个用例 59 项检查全部通过,共 5 次平台调用。2026-10-12 实测实时评测用例 77 项检查通过,平台接受握手签名与参数帧,7 次进度回调依次覆盖 25 个字,每个字一次,终评 78 分,1 次平台调用。2.1.1 起评测用例按 info 级别请求平台耗时,另有一个段落按实时速度边录边传的用例。2026-10-13 实测七个用例 165 项检查全部通过,共 7 次平台调用:整段评测的分段耗时一行带 Server-Timing 给出的平台耗时,边录边传的段落参数帧不带 realtime_feedback,终评带平台耗时字段与 wsFirstAudioAt,交给应用的结果 JSON 不带平台耗时字段。Tests/STKouyuEngineTests/SandboxTests.swift 经 Objective-C 接口跑其中四个用例与 2.1.1 的边录边传段落用例,不含 attachAudioUrl 用例,2.1.0 的四个用例已在 macOS 与 iOS 模拟器上编译,GitHub 的推送构建不注入沙箱密钥,这些用例在 macOS 上尚未运行。2.1.2 新增中文开放题用例,两条参考答案“我是学生”与“我不是学生”用竖线隔开,作答音频为 zh_open_answer.wav,核对 openTaskAudit.refKind 为 answers,总分高于原先的短答封顶,带声通的扁平字段,不带 words。2026-10-13 单独实测这个用例 15 项检查通过,1 次平台调用,总分 96
头文件 与声通公开头文件比对 5 个文件 227 项声明,差异为 0,参数名也一致
包清单 Linux 上用 Swift 6.0.3 加载 Package.swift,swift package describe 识别出全部目标与源文件,没有警告。macOS 上 Swift 5.10 按同一份清单构建全部目标
Objective-C 层 2026-10-12 在 GitHub Actions 的 macos-14 构建机上用 Xcode 15.4 编译,链接,运行 2.1.0。swift build 构建 macOS 版,xcodebuild 构建 iOS 真机版与模拟器版,真机版部署目标为 iOS 12,构建日志零警告。Linux 上另用 libclang 18 按 Objective-C ARC 解析全部 .m 文件,对照手写的 Apple 接口桩声明做类型检查,22 个编译单元零错误零警告。接口桩为 WebSocket 接口带上 iOS 13 的可用性标注,iOS 12 目标下可用性判断之外的调用会报警告,现为零警告。2.1.1 的改动经同样的检查,22 个编译单元零错误零警告,接口桩新增 NSURLSessionTaskMetrics 与会话代理的声明,2026-10-13 在 GitHub Actions 运行 38347556835 中于 macOS 上编译通过。2.1.2 同样 22 个编译单元零错误零警告
XCTest Tests/STKouyuEngineTests 的 Swift 测试与其调用的 Objective-C 检查 Tests/STKouyuEngineObjCSupport。macOS 上 swift test 执行 21 项,20 项通过,沙箱用例 1 项跳过,iOS 模拟器上 xcodebuild test 结果相同,连接平台模拟服务的文件评测,流式评测,取消,skegn 接口与七个实时评测用例在两处都通过。七个实时评测用例 RealtimeFeedbackTests 经 Objective-C 接口与 NSURLSessionWebSocketTask 连接平台模拟服务,覆盖进度回调,握手被拒与结束前断线时整段评测,结束后出错时重连重放,进度不一致,没有进度与取消。2026-10-08 测得 Sources/STKouyuEngine 的行覆盖率在 macOS 上为 58.50%,在模拟器上为 57.76%,录音器 YGSTRecorder.m 需要麦克风,测试没有覆盖。2.1.1 新增 StreamUploadTests 五项,连接平台模拟服务核对默认边录边传,streamUpload 关闭,握手被拒时整段评测,关闭时 realtime_feedback 照常边录边传,以及 sdkLogEnable 与 logLevel 为 2 时日志文件里的分段耗时。另新增 TimingAndCompressionTests 三项,只在 macOS 上运行,由测试启动 timing_server.py,核对 NSURLSessionWebSocketTask 的压缩协商与解压,NSURLSession 的 gzip 与 NSURLSessionTaskMetrics 分段。2026-10-13 在 GitHub Actions 运行 38347556835 中,swift test 29 项与 iOS 模拟器 26 项全部通过
macOS 构建 ci/ios-stcompat-macos.sh 2026-10-12 在 GitHub Actions 的 macos-14 构建机上全部步骤通过:在 macOS 上重跑 C 核心的各步骤,swift build,iOS 真机与模拟器的 xcodebuild,连接平台模拟服务的 swift test,以及 iOS 模拟器上的 xcodebuild test。设置沙箱密钥时 swift test 另跑沙箱用例。2.1.1 于 2026-10-13 在运行 38347556835 中全部通过
压缩 整段评测的 gzip 由 NSURLSession 自动协商与解压。边录边传的 permessage-deflate 由 NSURLSessionWebSocketTask 自行提出,出处为 Apple 开发者论坛 https://developer.apple.com/forums/thread/775190 里 Apple 技术支持工程师 2025 年 2 月的答复与抓包,URLSession 的握手带 Sec-WebSocket-Extensions: permessage-deflate,系统按压缩上下文解压,应用另设这个请求头时系统不再改写,另见 https://developer.apple.com/forums/thread/654362 。2026-10-13 用同样的握手请求实测平台,1 次平台调用:平台回 Sec-WebSocket-Extensions: permessage-deflate,接受压缩的参数帧,音频帧与结束帧,289756 字节的终评帧压缩为 14586 字节,分两帧发送,首帧 8192 字节带 RSV1,续帧不带,沿用压缩上下文。timing_server.py 按同样的方式发送终评帧,Linux 上用 Node.js 的 ws 客户端核对过。系统自身的协商与解压由 macOS 上的 TimingAndCompressionTests 核对,2026-10-13 在运行 38347556835 中通过,URLSessionWebSocketTask 自行协商 permessage-deflate。沙箱段落用例在 macOS 上尚未运行
真机 录音,回放,系统打断与音频路由切换尚未在真机验证,构建机上的测试不录音,2.1.1 的边录边传同样尚未在真机验证

常见问题#

skegn 符号重复的链接错误#

工程里还留着 libskegn.a,删除即可。本包已经实现全部 skegn_* 函数。

开始评测时的 60008 回调#

上一次评测还没有回调结果就开始了新评测。等结果回调之后再开始,或者先调用 cancelEngine。

结果里的声通字段#

外层字段与声通一致。result 内为优谷雅言评测结果,常用取分字段 overall,pronunciation,fluency,integrity,words 与声通同名,其余字段以优谷雅言接口文档为准。

私有部署与联调地址#

server 写 https:// 开头的地址,或设置 YuguCompat.baseURL。用 http:// 地址时应用需要配置 App Transport Security 例外,否则回调 errId 90011。

扩展名为 mp3 的 WAV 录音文件#

平替包不带 MP3 编码器。recordName 写 .mp3 时保留文件名,内容为 WAV,用 playback 或 AVAudioPlayer 都能正常播放。

应用自管的音频会话#

把 customized_avaudiosession 设为 YES,平替包不再设置 AVAudioSession。录音前需要应用把类别设为可录音的类别。

重复提交与计费#

同一次评测的重试与自动重评共用一个 tokenId,只计费一次。每次 startEngine 都生成新的 tokenId,算作新的评测。

收不到朗读进度#

依次核对:realtime_feedback 为 YES,系统为 iOS 13 或更新版本,评测为录音模式或 16 kHz 16 位单声道 PCM 的 feed 模式,参考文本不为空。条件不满足时平替包照旧整段评测,日志级别为 info 时打印原因。实时连接在结束帧之前失败时进度停止,日志级别为 warn 时打印原因,stopEngine 之后照常拿到终评。

停止录音之后等待时间长#

把日志级别设为 info,按日志一节的分段耗时一行判断慢在哪一步。mode 为 rest 时核对 YuguCompat.streamUpload 是否被设为 NO,系统是否为 iOS 13 或更新版本,feed 的音频是否为 16 kHz 16 位单声道 PCM。mode 为 ws>rest 时行末的 fallback 给出边录边传失败的原因。

许可#

Apache License 2.0,全文见 LICENSE,另见 NOTICE。声通与 17kouyu 是其各自权利人的名称,此处仅用于说明接口兼容关系。本包只按公开头文件重新实现,不含声通的实现代码,不分发声通的二进制文件。

更新于 2026-10-13