更新记录

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,然后调用:

开发文档

隐私、权限声明

1. 本插件需要申请的系统权限列表:

录音(RECORD_AUDIO)、网络访问(INTERNET)、获取网络状态(ACCESS_NETWORK_STATE)

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件在测评过程中会录制用户语音音频并发送至先声测评云服务进行评分

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。