uniapp在app上使用科大讯飞实现语音识别(上)

语音识别可以识别录制好的MP3,但是无法实时转换,基于app录音不支持分片问题,参考组件科大讯飞实时语音识别

1.在uniapp 在app上不支持录音分片为了解决这个问题,底层使用了renderjs 使用了web相关的技术。

测试demo

<template>
    <view class="content">
        <view placeholder="转文字" class="inputarea">
            {{ msg }}
        </view>
        <view class="down-ui" v-if="downed" :style="{ backgroundColor: downtime == -1 ? '#e43d33' : '#1acf3b' }">
            <!-- 效果显示 -->
            <view v-if="downtime == -1">
                建立连接中
            </view>
            <view v-else>
                语音倒计时
                <text style="color: red;">{{ downtime }} </text>
            </view>
        </view>

        <button class="btn-bottom" :disabled="disabled" @touchstart.stop="start" @touchend.stop="end">按下说话</button>

        <yue-asr-xf ref="yueAsrRefs" :options="optionsxf" @countDown="countDown" @result="resultMsg" @onStop="onStop"
            @onOpen="onOpen" @change="change"></yue-asr-xf>
    </view>
</template>

<script>
    export default {
        data() {

            const second = 60;
            return {
                title: 'Hello',
                msg: '转文字',
                optionsxf: {
                    receordingDuration: second,
                    APPID: '221111', // 请替换为实际值 
                    API_SECRET: 'NmzMjcyMjQyMWFmZmNmM2M0YTBj', // 请替换为实际值
                    API_KEY: '926adea190f4a146bbc3c' // 请替换为实际值
                },
                downtime: -1, //默认-1
                downed: false,
                disabled: false,
                second,
            };
        },
        onLoad() {
            // #ifdef APP
            //对于android应用,基于安全权限,请在调用时动态申请危险权限
            plus.android.requestPermissions(["android.permission.RECORD_AUDIO"], (e) => {}, (e) => {})
            // #endif
        },
        methods: {

            resumeUi() {
                this.downed = false;
                this.downtime = -1;
                this.disabled = false;
                this.downtime = this.second;
            },

            start() {
                if (this.disabled) {
                    return;
                }
                console.log("开始")
                this.downed = true;
                this.$refs.yueAsrRefs.start();
                this.disabled = true;
                // 建立连接
            },
            end() {
                console.log("结束")
                this.$refs.yueAsrRefs.end();
            },
            countDown(e) {
                console.log('countDown', e);
                this.downtime = e;
            },
            onStop(e) {
                console.log('onStop', e);
                this.resumeUi();
            },
            onOpen(e) {
                console.log('onOpen', e);
            },
            change(e) {
                console.log('change', e);
            },
            resultMsg(e) {
                this.msg = e
                console.log('resultMsg', e);
            }
        }
    };
</script>

<style>
    .btn-bottom {
        width: 100vw;
        position: absolute;
        bottom: 0px;
    }

    .inputarea {
        text-align: left;
        color: red;
        height: 200rpx;
        border: 1px solid #ccc;
        margin: 10rpx;
        border-radius: 10rpx;
        padding: 5rpx;
        overflow-y: scroll;
    }

    .down-ui {
        height: 100px;
        width: 100%;
        position: absolute;
        bottom: 50px;
        text-align: center;
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>

2.manifest.json

"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />"

3.static下增加文件,将包里的dist复制过来

image.png

4.uni_modules

image.png

5.下载地址

https://p.dcloud.net.cn/plugin?id=21811&token=18363f5143222daa710fb756c3fe8cca

6.优化后的代码

e1a0b2fd46170bd72571d5de5f48f0b3.jpg
<template>
    <view class="container">
        <!-- 聊天消息区域 -->
        <scroll-view ref="chatScroll" class="chat-area" scroll-y="true" scroll-with-animation>
            <view v-for="(item, index) in messages" :key="index" class="message-item" :id="item.id"
                :class="item.type === 'user' ? 'user-message' : 'ai-message'">
                <image v-if="item.type === 'ai'" class="avatar" :src="aiAvatar"></image>
                <view class="message-content">
                    {{ item.content }}
                </view>
                <image v-if="item.type === 'user'" class="avatar" src="/src/static/images/ai/user.png"></image>
            </view>
        </scroll-view>

        <!-- 文本输入区域 -->
        <view class="text-input-container" v-if="inputMode === 'text'">
            <image class="mode-switch-btn" :src="'/static/images/ai/voice.png'" @click="inputMode = 'voice'"
                mode="aspectFit" />
            <input v-model="inputText" placeholder="输入消息..." @confirm="sendTextMessage" />
            <button @click="sendTextMessage">发送</button>
        </view>

        <!-- 语音输入区域 -->
        <view class="voice-input-container" v-if="inputMode === 'voice'">
            <view class="voice-preview" v-if="isRecording" :class="{ canceling: isCanceling }">
                <view class="voice-controls">
                    <text :style="{ color: isCanceling ? '#e43d33' : '#666' }">
                        {{ isCanceling ? '取消发送' : '松开发送' }}
                    </text>
                    <view class="slide-hint" v-show="!isCanceling">
                        <text>↑ 上滑取消</text>
                    </view>
                    <view class="cancel-hint" v-show="isCanceling">
                        <text style="color: #e43d33; font-weight: bold;">松开手指取消发送</text>
                    </view>
                </view>
                <view class="voice-text-preview">
                    {{ inputText || '正在聆听...' }}
                </view>
            </view>
            <view style="display: flex;">
                <image class="mode-switch-btn" :src="'/static/images/ai/keyboard.png'" @click="inputMode = 'text'"
                    mode="aspectFit" />
                <button class="voice-btn" @touchstart="startVoiceInput" @touchend="endVoiceInput"
                    @touchmove="handleTouchMove">
                    {{ isRecording ? '松开结束' : '按住说话' }}
                </button>
            </view>
        </view>

        <yue-asr-xf ref="yueAsrRefs" :options="optionsxf" @countDown="countDown" @result="resultMsg" @onStop="onStop"
            @onOpen="onOpen" @change="change"></yue-asr-xf>
    </view>
</template>

<script>
    export default {
        data() {
            const second = 60;
            return {
                title: 'Hello',
                msg: '转文字',
                messages: [], // 聊天消息列表
                lastMsgId: '', // 最后一条消息ID
                inputText: '', // 输入框文本
                aiAvatar: '/static/tabbar/intelligence.gif', // AI头像
                userAvatar: '/src/static/images/ai/user.png', // 用户头像
                inputMode: 'voice', // 输入模式:voice/text
                isRecording: false, // 是否正在录音
                isCanceling: false, // 是否正在取消
                optionsxf: {
                    receordingDuration: second,
                    APPID: 'dd27c773', // 请替换为实际值
                    API_SECRET: 'NmZhMjczMjcyMjQyMWFmZmNmM2M0YTBj', // 请替换为实际值
                    API_KEY: '926ab0bbb1fb226dea190f4a146bbc3c' // 请替换为实际值
                },
                downtime: -1, // 默认-1
                downed: false,
                disabled: false,
                second,
            };
        },
        methods: {
            resumeUi() {
                this.downed = false;
                this.downtime = -1;
                this.disabled = false;
                this.downtime = this.second;
            },

            start() {
                if (this.disabled) {
                    return;
                }
                console.log("开始")
                this.downed = true;
                this.$refs.yueAsrRefs.start();
                this.disabled = true;
            },
            end() {
                console.log("结束")
                this.$refs.yueAsrRefs.end();
            },
            countDown(e) {
                console.log('countDown', e);
                this.downtime = e;
            },
            onOpen(e) {
                console.log('onOpen', e);
            },
            change(e) {
                console.log('change', e);
            },
            resultMsg(e) {
                this.inputText = e;
                console.log('resultMsg', e);
            },
            onStop(e) {
                console.log('onStop', e);
                this.resumeUi();
                if (this.inputText.trim()) {
                    this.sendTextMessage();
                }
            },
            sendTextMessage() {
                if (!this.inputText.trim()) return;

                const userMsg = {
                    type: 'user',
                    content: this.inputText,
                    id: 'msg_' + Date.now()
                };
                this.messages.push(userMsg);
                this.lastMsgId = userMsg.id;
                this.$nextTick(() => {
                    this.scrollToBottom();
                });

                this.inputText = '';

                setTimeout(() => {
                    const aiMsg = {
                        type: 'ai',
                        content: '这是AI的模拟回复',
                        id: 'msg_' + Date.now()
                    };
                    this.messages.push(aiMsg);
                    this.lastMsgId = aiMsg.id;
                    this.$nextTick(() => {
                        this.scrollToBottom();
                    });
                }, 1000);
            },
            scrollToBottom() {
                this.$nextTick(() => {
                    if (this.$refs.chatScroll) {
                        this.$refs.chatScroll.scrollTo({
                            top: 999999, // 足够大的值确保滚动到底部
                            duration: 300
                        });
                        console.log('执行滚动')
                    }
                });
            },
        endVoiceInput() {
            // 确保停止录音
            this.$refs.yueAsrRefs.end();
            
            if (this.isCanceling) {
                this.inputText = '';
            } else if (this.inputText.trim()) {
                this.sendTextMessage();
            }
            
            // 重置所有状态
            this.isRecording = false;
            this.isCanceling = false;
            this.startY = null;
        },
            handleTouchMove(e) {
                if (!this.isRecording) return;

                const touch = e.touches[0];
                if (!this.startY) {
                    this.startY = touch.pageY;
                    return;
                }

                const currentY = touch.pageY;
                const moveDistance = this.startY - currentY;

                // console.log('触摸移动距离:', moveDistance, 'startY:', this.startY, 'currentY:', currentY);

                if (moveDistance > 50) {
                    if (!this.isCanceling) {
                        this.$set(this, 'isCanceling', true);
                        // console.log('触发取消状态');
                    }
                } else if (this.isCanceling) {
                    this.$set(this, 'isCanceling', false);
                    // console.log('取消状态已重置');
                }
            },
            startVoiceInput() {
                this.startY = null; // 重置起始坐标
                this.isRecording = true;
                this.isCanceling = false;
                this.$refs.yueAsrRefs.start();
            },
            focusInput() {
                // #ifdef H5
                this.$refs.input.focus();
                // #endif
            }
        }
    };
</script>

<style lang="scss">
    .container {
        display: flex;
        flex-direction: column;
        height: 100vh;
        background-color: #f5f5f5;
    }

    .chat-area {
        flex: 1;
        padding: 20rpx;
        overflow-y: auto;
    }

    .message-item {
        display: flex;
        margin-bottom: 30rpx;
        align-items: flex-start;
        margin-right: 40rpx;

        .avatar {
            width: 80rpx;
            height: 80rpx;
            border-radius: 50%;
        }

        .message-content {
            max-width: 70%;
            padding: 20rpx;
            border-radius: 10rpx;
            font-size: 28rpx;
            line-height: 1.5;
        }
    }

    .user-message {
        justify-content: flex-end;

        .message-content {
            background-color: #1989fa;
            color: white;
            margin-left: 20rpx;
        }
    }

    .ai-message {
        justify-content: flex-start;

        .message-content {
            background-color: white;
            color: #333;
            margin-right: 20rpx;
        }
    }

    .text-input-container {
        display: flex;
        padding: 20rpx;
        background-color: white;
        border-top: 1rpx solid #eee;

        input {
            flex: 1;
            padding: 20rpx;
            border: 1rpx solid #ddd;
            border-radius: 50rpx;
            font-size: 28rpx;
            margin-right: 20rpx;
        }

        button {
            padding: 0 40rpx;
            background-color: #1989fa;
            color: white;
            border: none;
            border-radius: 50rpx;
        }
    }

    .voice-input-container {
        background-color: white;
        padding: 20rpx;
        border-top: 1rpx solid #eee;

        .voice-preview {
            background-color: #f9f9f9;
            border-radius: 10rpx;
            padding: 20rpx;
            margin-bottom: 20rpx;
            transition: all 0.3s;

            &.canceling {
                background-color: #ffeeee;

                .voice-controls {
                    color: #e43d33;
                }
            }

            .voice-controls {
                text-align: center;
                font-size: 28rpx;
                margin-bottom: 20rpx;

                .slide-hint {
                    font-size: 24rpx;
                    color: #999;
                    margin-top: 10rpx;
                }

                .cancel-hint {
                    font-size: 24rpx;
                    margin-top: 10rpx;
                }
            }

            .voice-text-preview {
                min-height: 80rpx;
                padding: 20rpx;
                background-color: white;
                border-radius: 10rpx;
                color: #333;
            }
        }

        .voice-btn {
            width: 100%;
            background-color: #1989fa;
            color: white;
            border: none;
            border-radius: 10rpx;
            font-size: 32rpx;
            text-align: center;
        }
    }

    .mode-switch-btn {
        width: 70rpx;
        height: 70rpx;
        margin: 10rpx;
    }
</style>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容