客户端线程安全,一个应用创建一个实例即可。客户端,实时会话与录音器都有显式的释放方法,全部可以重复调用,异常路径同样会关闭连接,取消定时器。
创建,使用与释放#
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() 自动调用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()
}
}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()
}
}JavaScript
import { YuguClient, YuguRecorder } from '@shengzhiai/yugu-web-sdk';
// 创建
const client = new YuguClient({ token: () => fetch('/api/yugu-token').then((r) => r.text()) });
const recorder = new YuguRecorder({
listener: {
onStateChanged: (_old, state) => console.log('录音器', state),
onLevel: (level) => meter.value = level,
onError: (e) => console.warn('录音出错', e.code, e.message),
},
});
// 使用
async function readOnce(referenceText) {
const session = client.streamEvaluate(
{ coreType: 'sentence', referenceText, language: 'zh-CN' },
{
onStateChanged: (_old, state) => (statusLabel.textContent = state),
onReconnecting: (attempt, delayMs) => console.log('重连中', attempt, delayMs),
onResult: (result) => console.log('总分', result.overall),
onError: (error) => console.warn('评测失败', error.code, error.message),
},
);
await recorder.start({ session });
await waitForUserToFinish();
await recorder.stop();
session.end();
return session.waitForResult();
}
// 释放
window.addEventListener('pagehide', () => {
recorder.release();
client.close();
});JavaScript
const { YuguClient, RecorderState } = require('@shengzhiai/yugu-miniprogram-sdk');
Page({
data: { state: 'IDLE', overall: null },
onLoad() {
this.client = new YuguClient({ auth: { token: getApp().globalData.token } });
this.recorder = this.client.createRecorder({ sampleRate: 16000, numberOfChannels: 1, format: 'PCM' });
this.recorder.setListener({
onError: (err) => wx.showToast({ title: '录音失败 ' + err.code, icon: 'none' }),
});
},
start() {
this.session = this.client.streamEvaluate(
{ coreType: 'sentence', referenceText: '今天天气很好', language: 'zh-CN' },
{
onStateChanged: (oldState, newState) => this.setData({ state: newState }),
onReconnecting: (attempt, delayMs) => console.log('重连', attempt, delayMs),
onResult: (result) => this.setData({ overall: result.overall }),
onError: (err) => wx.showToast({ title: err.message, icon: 'none' }),
onClosed: () => console.log('会话结束', this.session.getState()),
},
);
this.recorder.pipeTo(this.session);
this.recorder.start();
},
stop() {
if (this.recorder.getState() === RecorderState.RECORDING) this.recorder.stop();
},
onUnload() {
if (this.session) this.session.close();
this.recorder.release();
this.client.close();
},
});释放规则#
| 对象 | 释放 | 释放后 |
|---|---|---|
| 客户端 | close() |
未结束的实时会话依次进入 CANCELLED 与 CLOSED,进行中的请求以 90004 结束,之后的调用以 90004 失败。小程序同时释放 createRecorder 创建的录音器 |
| 实时会话 | cancel() 或 close() |
进入 CANCELLED 与 CLOSED,不再回调终评与错误,连接关闭回调照常触发 |
| 录音器 | release() |
停止录音,释放麦克风,注销监听器 |
Java 客户端实现 AutoCloseable,可以用 try-with-resources 管理,SDK 线程都是守护线程,不阻止 JVM 退出。示例里安卓在 onDestroy 释放,iOS 在 deinit 里释放,网页在 pagehide 事件里释放,小程序在页面 onUnload 里释放。
状态查询#
| 对象 | 查询 |
|---|---|
| 客户端 | 网页与小程序 isClosed(),Java getOpenSessionCount() 返回尚未关闭的会话数 |
| 实时会话 | getState() 或 state,isActive() 在终态之前为 true |
| 录音器 | 安卓,iOS,网页与小程序的录音器状态都是 IDLE,RECORDING,PAUSED,STOPPED,RELEASED,iOS 用 recorder.state 查询,其余用 getState() |
声通平替层#
Java
// 创建:进入评测页面时
SkEgnManager manager = SkEgnManager.getInstance(context);
EngineSetting es = EngineSetting.getInstance(context).setOnInitEngineListener(new OnInitEngineListener() {
@Override public void onStartInitEngine() { }
@Override public void onInitEngineSuccess() { /* 可以开始评测 */ }
@Override public void onInitEngineFailed(String reason) { /* appKey 为空,地址不合法等 */ }
});
manager.initEngine(appKey, secretKey, userId, es);
// 使用:每道题一次
RecordSetting rs = new RecordSetting(CoreType.CN_SENT_EVAL, "今天天气很好");
rs.setDuration(15000);
manager.startRecord(rs, new OnRecorderListener() {
@Override public void onStart() { }
@Override public void onStartRecordFail(String reason) { }
@Override public void onPause() { }
@Override public void onTick(long millisUntilFinished, double percentUntilFinished) { }
@Override public void onRecordEnd() { }
@Override public void onRecording(int vadStatus, int soundIntensity) { }
@Override public void onScore(String json) {
// 结果 JSON 或错误 JSON,见“结果 JSON”与“错误 JSON”两节
}
});
manager.stopRecord();
// 释放:离开页面时,可以重复调用
manager.clearActivityListener();
manager.recycle();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平替层沿用声通的引擎单例与回调写法,引擎初始化一次,每道题调用一次开始与结束,页面退出时销毁引擎。