更新记录

1.0.1(2026-10-28)

支持ios、鸿蒙

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 iOS插件版本 鸿蒙 鸿蒙插件版本
√ 1.0.0 √ 1.0.0 × × √ 1.0.0 √ 1.0.0 10.0 1.0.0 13 1.0.1 5.0 1.0.1
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uni-app x(4.75)

Chrome Safari Android Android插件版本 iOS iOS插件版本 鸿蒙 鸿蒙插件版本 微信小程序
× × 10.0 1.0.0 13 1.0.1 16 1.0.1 ×

yao-oral-eval

基于先声智能(SingSound)云评测服务的口语测评 / 跟读测评 UTS 插件。鉴权、录音、评测协议与先声官方 ssound-android 3.0.2 SDK 对齐,无需集成官方 SDK,纯 UTS 实现。

支持平台

平台 状态
Android ✔ 已启用
HarmonyOS ✔ 已启用
iOS ✔ 已启用

功能特性

  • 初始化时自动申请录音权限(Android,仅 Android 端)
  • 两种评测模式:
    • 实时评测:WebSocket 推流,onRealTimeEval 实时返回逐词反馈,onResult 返回最终总分
    • 录音评测:本地录音,stop() 后上传 WAV 走 HTTP 评测
  • 独立录音 API(可脱离评测单独录音,逐帧回调 PCM 数据)
  • 音频文件评测(对已有 WAV 文件直接发起 HTTP 评测)

快速开始

import { initSDK, start, stop, cancel, destroy } from "@/uni_modules/yao-oral-eval";

export default {
  onLoad() {
    initSDK(
      'your_appKey',            // appKey
      'your_secretKey',         // secretKey
      'test_user_001',          // userId
      '12.12.13.15',            // 用户客户端 IP(鉴权签名用)
      () => { /* onReady 引擎就绪 */ },
      () => { /*  开始录音 */ },
      (result) => { /* onResult 最终评测结果(JSON 字符串) */ },
      (result) => { /* onRealTimeEval 实时评测结果 */ },
      (code, msg) => { /* onEnd code=0 正常结束 */ },
      (volume) => { /* onVolume 音量 0-100 */ },
      () => { /* onFrontVadTimeout 前端点超时:未检测到说话 */ },
      () => { /* onBackVadTimeout 后端点超时:说话后静音 */ },
      (code, msg) => { /* onError 错误,见下方错误码表 */ }
    );
  },
  onUnload() {
    destroy();
  },
  methods: {
    // 开始评测:实时模式
    onStartRealTime() {
      start('en.sent.score', 'Hello, how are you today?', 100, true);
    },
    // 开始评测:录音模式(stop 后出结果)
    onStartRecord() {
      start('cn.sent.score', '今天天气真不错', 100, false);
    },
    onStop() {
      stop();
    },
    onCancel() {
      cancel();
    }
  }
}

API 参考

initSDK(appKey, secretKey, userId, ip, onReady, , onResult, onRealTimeEval, onEnd, onVolume, onFrontVadTimeout, onBackVadTimeout, onError)

初始化引擎并鉴权。

  • Android 端会先弹出录音权限申请,授权通过后才发起鉴权;权限被拒绝时通过 onError(-11) 报错,不会发起鉴权。
  • 鉴权流程:先请求正式环境,失败自动回退试用环境;成功后回调 onReady,失败回调 onError(-6)。
  • 所有回调为独立函数参数(UTS 不支持对象接口作为参数),建议对 initSDK 调用方页面使用 @UTSJS.keepAlive 语义的持久回调(插件已内置处理)。
参数 类型 说明
appKey string 先声平台 appid
secretKey string 先声平台 app_secret
userId string 用户唯一标识
ip string 用户客户端 IP(参与鉴权签名)

评测控制

函数 说明
start(coreType, refText, rank, realTime) 开始评测。realTime=true 实时推流模式;false 录音结束后 HTTP 上传。rank 传 100
stop() 结束评测。实时模式:发 end 包等最终结果;录音模式:停录并上传 WAV
cancel() 取消当前评测并停止录音
destroy() 销毁引擎,释放录音与网络资源(页面 onUnload 时务必调用)

其他

函数 说明
setAuth(warrantId, expireAt) 设置第三方鉴权凭证。warrant_id 为一次性凭证,每次评测前插件内部会自动重新鉴权;服务端模式下可由业务层直接下发
startPCMFileEval(coreType, refText, rank, realTime, filePath, sampleRate, channel, id) 文件评测:对已有 WAV/PCM 文件(16k/单声道/16bit 推荐)发起 HTTP 评测。id 任意类型,会原样透传到结果 JSON 的 itemId 字段。realTime 参数忽略,统一走 HTTP
playback() 播放最近一次评测录音
getRecordPath() 返回最近一次评测录音的 WAV 文件路径,无录音返回空字符串
md5(input) MD5 摘要,返回 32 位小写十六进制字符串

独立录音 API(Android / iOS)

与评测解耦的通用录音能力,逐帧回调 PCM 数据(行为对齐鸿蒙端 yao-recorded-frame)。

函数 说明
recordStart(options, callback) 开始录音。options(UTSJSONObject):sampleRate(默认 16000)、numberOfChannels(默认 1)、frameSize(默认 1024,字节)、audioSource。callback(prompt) 返回 "success" 表示成功,否则为错误信息
recordStop(callback) 停止录音并落盘 WAV,callback(path) 返回文件路径
recordPause() / recordResume() 暂停 / 恢复录音
onFrameRecorded(callback) 注册逐帧回调,callback(frame) 返回 PCM 字节数据
getBufferStatus() 返回 BufferStatus | null,未录音时为 null;availableRead 为已累计 PCM 字节数

回调说明

回调 触发时机
onReady 引擎初始化(鉴权)成功
onBegin 开始评测录音
onResult 最终评测结果(JSON 字符串),formatJson 后可解析总分
onRealTimeEval 实时模式的逐词实时反馈
onEnd(code, msg) 评测结束。code === 0 正常,非 0 为异常结束原因
onVolume(volume) 录音音量,0-100
onFrontVadTimeout 前端点超时:开始录音后一直未检测到说话
onBackVadTimeout 后端点超时:说话后持续静音
onError(code, msg) 错误,见错误码表

错误码

错误码 含义
-2 未鉴权(请先调用 initSDK)/ 已有评测进行中 / 录音启动失败
-3 鉴权成功但 warrant_id 为空
-4 无音频数据 / 音频文件路径为空
-5 没有录音文件
-6 鉴权失败 / 鉴权异常 / warrant_id 失效
-7 评测失败(响应解析失败或服务端返回错误)
-9 评测异常(音频文件读取失败等)
-10 WebSocket 连接失败 / 错误
-11 录音权限被拒绝(Android 初始化时申请,勾选"不再询问"需到系统设置手动开启)

支持的测评类型(coreType)

coreType 说明
en.sent.score 英文句子
en.word.score 英文单词
en.pred.score 英文段落
cn.sent.score 中文句子
cn.word.score 中文单词

生产环境鉴权

插件默认在 initSDK 时用 appKey + secretKey 自动鉴权(正式环境失败自动回退试用环境),试用环境仅供调试。

生产环境推荐两种方式:

  1. 插件自动鉴权:继续使用 initSDK 传入正式凭证,插件每次评测前自动重新鉴权(warrant_id 为一次性凭证)。
  2. 服务端下发:由自有后台调用先声鉴权接口获取 warrant_id,业务层调用 setAuth(warrantId, expireAt) 直接下发,避免 secretKey 出现在客户端。

权限说明

Android 端插件已在 AndroidManifest.xml 中声明以下权限,云打包时自动合并:

  • RECORD_AUDIO(录音,initSDK 时动态申请)
  • INTERNET / ACCESS_NETWORK_STATE / ACCESS_WIFI_STATE(网络访问)

试用鉴权接口走 HTTP,插件已声明 usesCleartextTraffic=true 允许明文流量。

完整示例页面

即项目首页 pages/index/index.vue,测评类型切换、实时评分逐词高亮、录音回放等完整功能,可直接复制使用。

<template>
    <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="score && score.overall !== null">
            <view class="card-header"><text class="card-title">评分结果</text></view>
            <view class="card-body">
                <view class="score-main">
                    <text class="score-value" :class="'score-c-' + scoreLevel">{{ score.overall }}</text>
                    <text class="score-label">{{ recording ? '实时评分 · 说完自动出总分' : '总评分' }}</text>
                </view>
                <view class="score-grid">
                    <view class="score-item" v-if="score.accuracy !== null">
                        <text class="score-num">{{ score.accuracy }}</text>
                        <text class="score-name">准确度</text>
                    </view>
                    <view class="score-item" v-if="score.fluency !== null">
                        <text class="score-num">{{ score.fluency }}</text>
                        <text class="score-name">流利度</text>
                    </view>
                    <view class="score-item" v-if="score.integrity !== null">
                        <text class="score-num">{{ score.integrity }}</text>
                        <text class="score-name">完整度</text>
                    </view>
                    <view class="score-item" v-if="score.pron !== null">
                        <text class="score-num">{{ score.pron }}</text>
                        <text class="score-name">发音</text>
                    </view>
                    <view class="score-item" v-if="score.rhythm !== null">
                        <text class="score-num">{{ score.rhythm }}</text>
                        <text class="score-name">韵律</text>
                    </view>
                </view>
                <view class="words-row" v-if="realtimeWords.length">
                    <view class="word-chip" v-for="(w, i) in realtimeWords" :key="i">
                        <text class="word-char">{{ w.char }}</text>
                        <text class="word-score" v-if="w.score !== null"
                            :class="w.score >= 80 ? 'w-good' : (w.score >= 60 ? 'w-mid' : 'w-bad')">{{ w.score }}</text>
                        <text class="word-score w-pending" v-else>·</text>
                    </view>
                </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>
</template>

<script>
    import {
        initSDK,
        start,
        stop,
        cancel,
        destroy,
        getRecordPath
    } from "@/uni_modules/yao-oral-eval";

    export default {
        data() {
            return {
                // 试用凭证(先声测评平台测试账号)
                showCredentials: true,
                appKey: 'your_appKey',
                secretKey: 'your_secretKey',
                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?",
                realTime: false,

                // 录音状态
                recording: false,
                volume: 0,

                // 状态显示
                statusText: '未初始化',
                statusLevel: 'idle',

                // 结果
                realTimeResult: '',
                finalResult: '',
                score: null,
                realtimeWords: [],

                // 错误
                errorCode: 0,
                errorMsg: '',
                ip: '12.12.13.15'
            }
        },
        computed: {
            scoreLevel() {
                const v = this.score && this.score.overall;
                if (v == null) return 'mid';
                return v >= 80 ? 'good' : (v >= 60 ? 'mid' : 'bad');
            }
        },
        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.score = null;
                        this.realtimeWords = [];
                        this.updateStatus('正在录音测评...', 'recording');
                    },
                    (result) => {
                        // onResult
                        this.finalResult = this.formatJson(result);
                        this.parseScore(result);
                        this.updateStatus('测评完成', 'done');
                    },
                    (result) => {
                        // onRealTimeEval
                        this.realTimeResult = this.formatJson(result);
                        this.parseRealtimeScore(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' });
            },

            parseScore(result) {
                try {
                    const data = JSON.parse(result);
                    const r = data.result || data;
                    const r1 = (v) => (typeof v === 'number' ? Math.round(v * 10) / 10 : null);
                    this.score = {
                        overall: r1(r.overall),
                        accuracy: r1(r.accuracy),
                        fluency: r1(r.fluency ? r.fluency.overall : null),
                        integrity: r1(r.integrity),
                        pron: r1(r.pron),
                        rhythm: r1(r.rhythm ? r.rhythm.overall : null)
                    };
                } catch (e) {
                    this.score = null;
                }
            },
            parseRealtimeScore(result) {
                try {
                    const data = JSON.parse(result);
                    const r = data.result;
                    if (!r || !r.realtime_details) return;
                    const words = [];
                    let sum = 0;
                    let count = 0;
                    r.realtime_details.forEach((d) => {
                        // dp_type=0 是已说出的单词(有实时分),dp_type=1 为未说(占位)
                        words.push({
                            char: d.char,
                            score: d.dp_type === 0 ? d.score : null
                        });
                        if (d.dp_type === 0 && typeof d.score === 'number' && d.dur > 0) {
                            sum += d.score;
                            count += 1;
                        }
                    });
                    this.realtimeWords = words;
                    if (count > 0) {
                        const live = Math.round((sum / count) * 10) / 10;
                        if (this.score && this.score.accuracy === null) {
                            this.score.overall = live;
                        } else if (!this.score) {
                            this.score = {
                                overall: live,
                                accuracy: null,
                                fluency: null,
                                integrity: null,
                                pron: null,
                                rhythm: null
                            };
                        }
                    }
                } catch (e) {
                    // 忽略实时包解析失败
                }
            },
            formatJson(str) {
                try {
                    return JSON.stringify(JSON.parse(str), null, 2);
                } catch (e) {
                    return str;
                }
            },
        }
    }
</script>

<style>
    .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;
    }

    .score-main {
        text-align: center;
        margin-bottom: 20rpx;
    }

    .score-value {
        font-size: 100rpx;
        font-weight: bold;
        line-height: 1.2;
        display: block;
    }

    .score-c-good { color: #52c41a; }
    .score-c-mid { color: #faad14; }
    .score-c-bad { color: #ff4d4f; }

    .score-label {
        font-size: 26rpx;
        color: #999;
        display: block;
    }

    .score-grid {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
    }

    .score-item {
        width: 33.33%;
        text-align: center;
        padding: 14rpx 0;
    }

    .score-num {
        font-size: 38rpx;
        font-weight: bold;
        color: #1677ff;
        display: block;
    }

    .score-name {
        font-size: 24rpx;
        color: #999;
        display: block;
        margin-top: 6rpx;
    }

    .words-row {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        margin-top: 16rpx;
        justify-content: center;
    }

    .word-chip {
        display: flex;
        flex-direction: column;
        align-items: center;
        margin: 8rpx 14rpx;
    }

    .word-char {
        font-size: 26rpx;
        color: #333;
    }

    .word-score {
        font-size: 24rpx;
        margin-top: 4rpx;
        font-weight: bold;
    }

    .w-good { color: #52c41a; }
    .w-mid { color: #faad14; }
    .w-bad { color: #ff4d4f; }
    .w-pending { color: #ccc; }

    .error-text {
        font-size: 26rpx;
        color: #ff4d4f;
    }
</style>

隐私、权限声明

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

需要录音权限

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

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

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

无

暂无用户评论。