更新记录
1.0.0(2026-08-14)
阿里云跟读测评
平台兼容性
uni-app(4.75)
| Vue2 |
Vue2插件版本 |
Vue3 |
Vue3插件版本 |
Chrome |
Safari |
app-vue |
app-vue插件版本 |
app-nvue |
app-nvue插件版本 |
Android |
Android插件版本 |
iOS |
鸿蒙 |
| √ |
1.0.0 |
√ |
1.0.0 |
- |
- |
√ |
1.0.0 |
√ |
1.0.0 |
10.0 |
1.0.0 |
× |
× |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
小红书小程序 |
快应用-华为 |
快应用-联盟 |
| × |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
uni-app x(4.75)
| Chrome |
Safari |
Android |
iOS |
鸿蒙 |
微信小程序 |
| - |
- |
- |
- |
- |
- |
yao-oral-eval
基于先声智能(SingSound)ssound-android 3.0.2 SDK 的口语测评 UTS 插件,目前支持 Android 端。
功能
- 录音权限自动请求
- 引擎初始化(含试用凭证自动获取)
- 开始/停止/取消测评
- 实时评测结果回调
- 录音回放
使用
import { initSDK, start, stop, cancel, destroy } from "@/uni_modules/yao-oral-eval";
export default {
data() {
return {
// 试用凭证(先声测评平台测试账号)
showCredentials: true,
appKey: 'xxx',
secretKey: 'xxx',
userId: 'test_user_001',
// 引擎状态
initing: false,
engineReady: false,
// 测评配置
coreTypes: [
{ name: '英文句子', value: 'en.sent.score' },
{ name: '英文单词', value: 'en.word.score' },
{ name: '英文段落', value: 'en.pred.score' },
{ name: '中文句子', value: 'cn.sent.score' },
{ name: '中文单词', value: 'cn.word.score' },
],
coreTypeIndex: 0,
refText: "Hello, how are you today?",
realTime: false,
// 录音状态
recording: false,
volume: 0,
// 状态显示
statusText: '未初始化',
statusLevel: 'idle',
// 结果
realTimeResult: '',
finalResult: '',
// 错误
errorCode: 0,
errorMsg: '',
ip:'12:12:13:15'
}
},
onLoad() {},
onUnload() {
destroy();
},
methods: {
onCoreTypeChange(e) {
this.coreTypeIndex = e.detail.value;
},
onRealTimeChange(e) {
this.realTime = e.detail.value;
},
updateStatus(text, level) {
this.statusText = text;
this.statusLevel = level;
},
onInit() {
this.doInit();
},
doInit() {
this.initing = true;
this.errorMsg = '';
this.updateStatus('初始化中...', 'initing');
initSDK(
this.appKey,
this.secretKey,
this.userId || '111',
this.ip,
() => {
// onReady
this.initing = false;
this.engineReady = true;
this.updateStatus('引擎就绪,可以开始测评', 'ready');
uni.showToast({ title: '引擎初始化成功', icon: 'success' });
},
() => {
//
this.recording = true;
this.finalResult = '';
this.realTimeResult = '';
this.updateStatus('正在录音测评...', 'recording');
},
(result) => {
// onResult
console.log("onResult",result);
this.finalResult = this.formatJson(result);
this.updateStatus('测评完成', 'done');
},
(result) => {
// onRealTimeEval
this.realTimeResult = this.formatJson(result);
},
(code, msg) => {
// onEnd
this.recording = false;
this.volume = 0;
if (code === 0) {
this.updateStatus('测评结束', 'done');
} else {
this.updateStatus('结束: ' + msg + ' (' + code + ')', 'error');
}
},
(vol) => {
// onVolume
this.volume = Math.min(100, vol);
},
() => {
// onFrontVadTimeout
this.recording = false;
this.volume = 0;
this.updateStatus('前端超时:未检测到说话', 'warn');
uni.showToast({ title: '未检测到说话', icon: 'none' });
},
() => {
// onBackVadTimeout
this.recording = false;
this.volume = 0;
this.updateStatus('后端超时:说话后静音', 'warn');
},
(code, msg) => {
// onError
this.initing = false;
this.recording = false;
this.volume = 0;
this.errorCode = code;
this.errorMsg = msg;
this.updateStatus('错误: ' + msg, 'error');
uni.showToast({ title: msg, icon: 'none' });
console.log(code, msg)
}
);
},
onStart() {
if (!this.engineReady) {
uni.showToast({ title: '请先初始化引擎', icon: 'none' });
return;
}
const coreType = this.coreTypes[this.coreTypeIndex].value;
start(coreType, this.refText, 100, this.realTime);
this.updateStatus('正在录音...', 'recording');
},
onStop() {
stop();
this.updateStatus('正在获取结果...', 'processing');
},
onCancel() {
cancel();
this.recording = false;
this.volume = 0;
this.updateStatus('已取消', 'idle');
},
onPlayback() {
const path = getRecordPath();
if (!path) {
uni.showToast({ title: '没有录音文件', icon: 'none' });
return;
}
const audio = uni.createInnerAudioContext();
audio.src = path;
audio.play();
uni.showToast({ title: '播放录音中', icon: 'none' });
},
formatJson(str) {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (e) {
return str;
}
},
}
}
<view class="container">
<!-- 标题 -->
<view class="header">
<text class="title">口语测评 Demo</text>
<text class="subtitle">yao-oral-eval</text>
</view>
<!-- 凭证配置 -->
<view class="card">
<view class="card-header" @click="showCredentials = !showCredentials">
<text class="card-title">凭证配置(试用)</text>
<text class="arrow">{{ showCredentials ? '▼' : '▶' }}</text>
</view>
<view v-if="showCredentials" class="card-body">
<view class="form-item">
<text class="label">AppKey</text>
<input class="input" v-model="appKey" placeholder="appKey" />
</view>
<view class="form-item">
<text class="label">SecretKey</text>
<input class="input" v-model="secretKey" placeholder="secretKey" />
</view>
<view class="form-item">
<text class="label">UserID</text>
<input class="input" v-model="userId" placeholder="userId" />
</view>
<button class="btn btn-primary" @click="onInit" :disabled="engineReady || initing">
{{ initing ? '初始化中...' : (engineReady ? '引擎已就绪 ✓' : '初始化引擎') }}
</button>
</view>
</view>
<!-- 测评配置 -->
<view class="card">
<view class="card-header"><text class="card-title">测评配置</text></view>
<view class="card-body">
<view class="form-item">
<text class="label">测评类型</text>
<picker :range="coreTypes" range-key="name" @change="onCoreTypeChange">
<view class="picker-value">{{ coreTypes[coreTypeIndex].name }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">参考文本</text>
<textarea class="textarea" v-model="refText" placeholder="请输入参考文本" />
</view>
<view class="form-item form-row">
<text class="label">实时评测</text>
<switch :checked="realTime" @change="onRealTimeChange" />
</view>
</view>
</view>
<!-- 录音控制 -->
<view class="card">
<view class="card-header"><text class="card-title">录音测评</text></view>
<view class="card-body">
<view class="status-bar">
<text class="status-label">状态:</text>
<text class="status-value" :class="'status-' + statusLevel">{{ statusText }}</text>
</view>
<view class="volume-bar" v-if="recording">
<view class="volume-fill" :style="{ width: volume + '%' }"></view>
</view>
<view class="btn-group">
<button class="btn btn-record" @click="onStart" :disabled="!engineReady || recording">开始测评</button>
<button class="btn btn-stop" @click="onStop" :disabled="!recording">停止</button>
</view>
<view class="btn-group">
<button class="btn btn-secondary" @click="onCancel" :disabled="!recording">取消</button>
<button class="btn btn-secondary" @click="onPlayback" :disabled="!engineReady">播放录音</button>
</view>
</view>
</view>
<!-- 实时结果 -->
<view class="card" v-if="realTimeResult">
<view class="card-header"><text class="card-title">实时结果</text></view>
<view class="card-body">
<text class="result-text" selectable>{{ realTimeResult }}</text>
</view>
</view>
<!-- 最终结果 -->
<view class="card" v-if="finalResult">
<view class="card-header"><text class="card-title">最终结果</text></view>
<view class="card-body">
<text class="result-text" selectable>{{ finalResult }}</text>
</view>
</view>
<!-- 错误信息 -->
<view class="card card-error" v-if="errorMsg">
<view class="card-body">
<text class="error-text">[{{ errorCode }}] {{ errorMsg }}</text>
</view>
</view>
</view>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
text-align: center;
padding: 30rpx 0;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
display: block;
}
.subtitle {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
display: block;
}
.card {
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.arrow {
font-size: 24rpx;
color: #ccc;
}
.card-body {
padding: 20rpx 30rpx;
}
.form-item {
margin-bottom: 20rpx;
}
.form-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.label {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
display: block;
}
.form-row .label {
margin-bottom: 0;
}
.input {
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 16rpx 20rpx;
font-size: 28rpx;
background: #fafafa;
}
.textarea {
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 16rpx 20rpx;
font-size: 28rpx;
background: #fafafa;
width: 100%;
height: 120rpx;
box-sizing: border-box;
}
.picker-value {
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 16rpx 20rpx;
font-size: 28rpx;
background: #fafafa;
color: #333;
}
.btn {
border-radius: 12rpx;
font-size: 30rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
margin: 0;
}
.btn::after {
border: none;
}
.btn[disabled] {
opacity: 0.5;
}
.btn-primary {
background: #007aff;
color: #fff;
width: 100%;
margin-top: 10rpx;
}
.btn-record {
background: #ff4d4f;
color: #fff;
flex: 1;
}
.btn-stop {
background: #52c41a;
color: #fff;
flex: 1;
margin-left: 20rpx;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
flex: 1;
font-size: 28rpx;
}
.btn-secondary:last-child {
margin-left: 20rpx;
}
.btn-group {
display: flex;
margin-top: 20rpx;
}
.status-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.status-label {
font-size: 26rpx;
color: #999;
}
.status-value {
font-size: 28rpx;
font-weight: bold;
margin-left: 10rpx;
}
.status-idle { color: #999; }
.status-initing { color: #faad14; }
.status-ready { color: #52c41a; }
.status-recording { color: #ff4d4f; }
.status-processing { color: #1677ff; }
.status-done { color: #52c41a; }
.status-warn { color: #faad14; }
.status-error { color: #ff4d4f; }
.volume-bar {
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.volume-fill {
height: 100%;
background: #52c41a;
}
.result-text {
font-size: 24rpx;
color: #333;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.6;
}
.card-error {
border: 2rpx solid #ff4d4f;
}
.error-text {
font-size: 26rpx;
color: #ff4d4f;
}
支持的测评类型(coreType)
| coreType |
说明 |
en.sent.score |
英文句子 |
en.word.score |
英文单词 |
en.pred.score |
英文段落 |
cn.sent.score |
中文句子 |
cn.word.score |
中文单词 |
生产环境鉴权
试用凭证接口仅用于测试。生产环境应从自有后台获取 warrantId,然后调用:
开发文档