Skip to content
Leo's Log
搜索 K
Main Navigation 主页目录关于
AI网站
实用网站
工具网站

Appearance

Sidebar Navigation

容器化

应用安装

kibana应用

Elasticsearch 应用

Jenkins

部署

构建

介绍

Docker

Docker-compose的常用命令

Docker镜像源

Docker 介绍

单点登录

系统对接

群辉NAS服务器

契约锁

MaxKey

单点登录方案

maxkey 部署

maxkey 同步器

单点登录介绍

单点注销方案

介绍

Jwt

Jeecgboot使用jwt跨系统交互

Jwt验证原理

Jeecgboot

Jeecgboot对接单点登录流程

企业微信

消息推送

群聊机器人

数据与智能专区

3.管理企业知识集API

2.专区程序开发指引

1.专区程序接入指引

Python

libreoffice

conda

pages

关于我

闲聊

tools

PicTools

ExcelTools

recommendation

工具网页

实用网页

AI工具

Nginx

SSL

自签证书

Maxkey配置

APP

Flutter

API

CSDN

获取用户CSDN文章列表

AI

项目

AI简历分析

AI合同审查

语音识别

阿里云语音识别

uniapp接入阿里云语音识别

知识库系统

知识库系统

流式接口

CozeAPI流式接口

Rag

rag介绍

LangChain4j

LangChain4j 介绍

火山引擎调用示例

rag

Elasticsearch 介绍

rag介绍

Dify

私有化部署

AI+BI问数系统

AI+BI问数系统

文章目录

语音识别插件 ​

阅读时间:

18

min
文章字数:

4.4k

字
发布日期:

2025-11-24

最近更新:

2025-12-09

阅读量:

-

当前文档以uni-app作为示例进行接入,以VUE3语法进行编写

  • 支持vue2、vue3、nvue
  • 支持编译成:H5、Android App、iOS App、微信小程序
  • 支持已有的大部分录音格式:mp3、wav、pcm、amr、ogg、g711a、g711u等
  • 支持实时处理,包括变速变调、实时上传、ASR语音转文字
  • 支持可视化波形显示;可配置回声消除、降噪;注意:不支持通话时录音
  • 支持PCM音频流式播放、完整播放,App端用原生插件边录音边播放更流畅
  • 支持离线使用,本组件和配套原生插件均不依赖网络
  • App端有配套的原生录音插件可供搭配使用,兼容性和体验更好

Recorder-UniCore插件链接阿里云语音识别

集成项目中 ​

安装依赖 ​

安装recorder-core

shell
npm install recorder-core --registry=https://registry.npmmirror.com/

引入组件 ​

导入Recorder-UniCore组件:直接复制本目录下的uni_modules/Recorder-UniCore组件到你项目中,或者到DCloud 插件市场下载此组件

配置录音权限 ​

在调用RecordApp.RequestPermission的时候,Recorder-UniCore组件会自动处理好App的系统录音权限,只需要在uni-app项目的 manifest.json 中配置好Android和iOS的录音权限声明。

//Android需要勾选的权限,第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音,需要启用后台录音保活服务,否则锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音(renderjs中H5录音、原生插件录音均受影响),请参考下面的`androidNotifyService`

//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置,不然App切到后台后立马会停止录音

按需引入js ​

ts

<script setup lang="ts">
    /** 先引入Recorder ( 需先 npm install recorder-core )**/
    import Recorder from 'recorder-core';
    Recorder.a=1
    /** H5、小程序环境中:引入需要的格式编码器、可视化插件,App环境中在renderjs中引入 **/
    // #ifdef H5 || MP-WEIXIN
    //按需引入需要的录音格式编码器,用不到的不需要引入,减少程序体积;H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入,免得这些文件太大
    import 'recorder-core/src/engine/wav.js'
    
    //可选引入可视化插件
    import 'recorder-core/src/extensions/waveview.js'
    // #endif
    /** 引入RecordApp **/
    import RecordApp from 'recorder-core/src/app-support/app.js'
    //【所有平台必须引入】uni-app支持文件
    import '../../uni_modules/Recorder-UniCore/app-uni-support.js'
    
    // #ifdef MP-WEIXIN
    //可选引入微信小程序支持文件
    import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
    // #endif
    
    //引入阿里云语音识别插件
    import 'recorder-core/src/extensions/asr.aliyun.short.js'
    var vue3This=getCurrentInstance().proxy; //必须定义到最外面,getCurrentInstance得到的就是当前实例this
    
    // ASR 相关响应式数据
    const asrTokenApi = ref('')
    const asrLang = ref('普通话')
    const asrTime = ref('')
    const asrTxt = ref('')
    const SyncID = ref(0) // 同步操作,如果同时操作多次,之前的操作全部终止
    const recpowerx = ref(0)
    const recpowert = ref('')
    const reclogs = ref<Array<{txt: string, color?: string}>>([])
    const asr = ref<any>(null) // 语音识别对象
    const waveView = ref<any>(null) // 音频可视化对象
    const playerRef = ref<any>(null) // 播放器引用
    
    const emit = defineEmits(['close'])
    const userStore = useUserStore()
    const showPopup = ref(false)
    
    // 获取状态栏高度(APP端)
    const statusBarHeight = ref(0)
</script>

额外新增一个renderjs模块 ​

照抄下面这段代码放到vue文件末尾

ts
<!-- #ifdef APP -->
<script module="testMainVue" lang="renderjs"> //这地方就别用组合式api了,可能不能import vue
/**============= App中在renderjs中引入RecordApp,这样App中也能使用H5录音、音频可视化 =============**/
/** 先引入Recorder **/
import Recorder from 'recorder-core';

//按需引入需要的录音格式编码器,用不到的不需要引入,减少程序体积;H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入,免得这些文件太大
import 'recorder-core/src/engine/mp3.js'
import 'recorder-core/src/engine/mp3-engine.js'
import 'recorder-core/src/engine/wav.js'
//可选引入可视化插件
import 'recorder-core/src/extensions/waveview.js'
import 'recorder-core/src/extensions/frequency.histogram.view.js'
import 'recorder-core/src/extensions/lib.fft.js'
/** 引入RecordApp **/
import RecordApp from 'recorder-core/src/app-support/app.js'
//【必须引入】uni-app支持文件
import '../../uni_modules/Recorder-UniCore/app-uni-support.js'

export default {
	mounted(){
		//App的renderjs必须调用的函数,传入当前模块this
		RecordApp.UniRenderjsRegister(this);
		//测试用
		rjsThis=this;
	},
	methods: {
		//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
		//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用,二进制数据需转成base64来传递
		testCall(val){
			this.$ownerInstance.callMethod("reclog",'逻辑层调用renderjs中的testCall结果:'+val);
		}
	}
}

//测试用,打印this里面的对象
var rjsThis;
window.traceThis__vue3_capi=function(){
	var obj=rjsThis;
	var str="renderjs可用:<pre style='white-space:pre-wrap'>";
	var trace=(val)=>{
		if(/func/i.test(typeof val))val="[Func]";
		try{ val=""+val;}catch(e){val="[?"+(typeof val)+"]"}
		return '<span style="color:#bbb">='+val.substr(0,50)+'</span>';
	}
	for(var k in obj){
		str+='\n	this.'+k+trace(obj[k]);
	}
	for(var k in obj.$ownerInstance){
		str+='\n	this.$ownerInstance.'+k+trace(obj.$ownerInstance[k]);
	}
	for(var k in obj.$ownerInstance.$vm){
		str+='\n	this.$ownerInstance.$vm.'+k+trace(obj.$ownerInstance.$vm[k]);
	}
	for(var k in uni){
		str+='\n	uni.'+k+trace(uni[k]);
	}
	str+='</pre>';
	var el=document.querySelector('.testPerfRJsLogs');
	el.innerHTML+=str;
}
</script>
<!-- #endif -->

调用录音 ​

ts
<script setup>
import { ref, getCurrentInstance, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app'  
  
var vue3This=getCurrentInstance().proxy; //必须定义到最外面,getCurrentInstance得到的就是当前实例this

onShow(()=>{
  if(vue3This.isMounted) RecordApp.UniPageOnShow(vue3This); //onShow可能比mounted先执行,页面可能还未准备好
});

onMounted(() => {
  vue3This.isMounted=true; RecordApp.UniPageOnShow(vue3This); //onShow可能比mounted先执行,页面准备好了时再执行一次
  // 设置当前时间
  currentTime.value = getCurrentTime()

  // 页面加载时显示popup
  setTimeout(() => {
    showPopup.value = true
  }, 100)
  
  
})

</script>

录音逻辑代码 ​

ts

/*******************下面的接口实现代码可以直接copy到你的项目里面使用**********************/

/**实现apiRequest接口,tokenApi的请求实现方法**/
var uni_ApiRequest=function(url,args,success,fail){
  uni.setStorageSync("page_asr_asrTokenApi", url); //测试用的存起来

  //如果你已经获得了token数据,直接success回调即可,不需要发起api请求
  if(/^\s*\{.*\}\s*$/.test(url)){ //这里是输入框里面填的json数据解析直接返回
    var data; try{ data=JSON.parse(url); }catch(e){};
    if(!data || !data.appkey || !data.token){
      fail("填写的json数据"+(!data?"解析失败":"中缺少appkey或token"));
    }else{
      success({ appkey:data.appkey, token:data.token });
    }
    return;
  }

  //请求url获得token数据,然后通过success返回结果
  uni.request({
    url:url, data:args, method:"POST", dataType:"text"
    ,header:{"content-type":"application/x-www-form-urlencoded"}
    ,success:(e)=>{
      if(e.statusCode!=200){
        fail("请求出错["+e.statusCode+"]");
        return;
      }
      try{
        var data=JSON.parse(e.data);
      }catch(e){
        fail("请求结果不是json格式:"+e.data);
        return;
      }

      //【自行修改】根据自己的接口格式提取出数据并回调
      if(data.c!==0){
        fail("接口调用错误:"+data.m);
        return;
      }
      data=data.v;
      success({ appkey:data.appkey, token:data.token });
    }
    ,fail:(e)=>{
      fail(e.errMsg||"请求出错");
    }
  });
};

/**实现compatibleWebSocket接口**/
var uni_WebSocket=function(url){
  //事件回调
  var ws={
    onopen:()=>{}
    ,onerror:(event)=>{}
    ,onclose:(event)=>{}
    ,onmessage:(event)=>{}
  };
  var store=ws.storeData={};

  //发送数据,data为字符串或者arraybuffer
  ws.send=(data)=>{
    store.wsTask.send({ data:data });
  };
  //进行连接
  ws.connect=()=>{
    var wsTask=store.wsTask=uni.connectSocket({
      url:url
      // protocols:['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjYwMDE3NTIiLCJleHAiOjE3NjM2ODAyNTB9.5bs8MMWWro6DMVu8jzpejIjOZG0n9M4NO89UiDNz1bc']
      ,success:()=>{ }
      ,fail:(res)=>{
        if(store.isError)return; store.isError=1;
        ws.onerror({message:"创建连接出现错误:"+res.errMsg});
      }
    });
    wsTask.onOpen(()=>{
      if(store.isOpen)return; store.isOpen=1;
      ws.onopen();
    });
    wsTask.onClose((e)=>{
      if(store.isClose)return; store.isClose=1;
      ws.onclose({ code:e.code||-1, reason:e.reason||"" });
    });
    wsTask.onError((e)=>{
      if(store.isError)return; store.isError=1;
      ws.onerror({ message:e.errMsg||"未知错误" });
    });
    wsTask.onMessage((e)=>{ ws.onmessage({data:e.data}); });
  };
  //关闭连接
  ws.close=(code,reason)=>{
    var obj={};
    if(code!=null)obj.code=code;
    if(reason!=null)obj.reason=reason;
    store.wsTask.close(obj);
  };
  return ws;
};

// 获取当前录音键标签
const currentKeyTag = () => {
  if (!RecordApp.Current) return "[?]";

  // #ifdef APP
  var tag2 = "Renderjs+H5";
  if (RecordApp.UniNativeUtsPlugin) {
    tag2 = RecordApp.UniNativeUtsPlugin.nativePlugin ? "NativePlugin" : "UtsPlugin";
  }
  return RecordApp.Current.Key + "(" + tag2 + ")";
  // #endif

  return RecordApp.Current.Key;
}

// 开始录音,然后开始语音识别
const recStart = async () => {


  var sid = ++SyncID.value;

  if (!asrTokenApi.value) {
    reclog("需要提供TokenApi", "1");
    return;
  }

  if (asr.value) {
    reclog("上次asr未关闭", "1");
    return;
  }

  reclog("正在请求录音权限...");
  console.log('开始语音录制')
  // APP端开始录音时震动反馈
  try {
    // 检查是否支持 plus API
    if (typeof plus !== 'undefined' && plus.device && typeof plus.device.vibrate === 'function') {
      // 使用plus.device.vibrate,增加震动时长到100毫秒(更明显)
      plus.device.vibrate(80)
      console.log('震动反馈已触发(plus.device,100ms)')
    } else if (typeof uni !== 'undefined' && uni.vibrateShort) {
      // 备用方案:使用 uni API
      uni.vibrateShort({
        success: () => {
          console.log('震动反馈成功(uni API)')
        },
        fail: (err) => {
          console.warn('震动反馈失败:', err)
        }
      })
    } else {
      console.warn('震动功能不可用')
    }
  } catch (error) {
    console.warn('震动功能异常:', error)
  }
  console.log('UniWebViewActivate 1 ')
  // vue3This.$refs.player.setPlayBytes(null);

  RecordApp.UniWebViewActivate(vue3This); // App环境下必须先切换成当前页面WebView
  console.log('UniWebViewActivate')
  RecordApp.UniAppUseLicense='我已获得UniAppID=****的商用授权';
  <!-- #ifdef H5 -->
  RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置,在h5(H5、App+renderjs)中必须提前配置,因为h5中RequestPermission会直接打开录音
  <!-- #endif -->
  RecordApp.RequestPermission(() => {
    reclog(currentKeyTag() + " 已获得录音权限", "2");
    recStart__asrStart(sid);
  }, (msg: string, isUserNotAllow?: boolean) => {
    if (isUserNotAllow) {
      // 用户拒绝了录音权限
      // 这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
      requestRecordPermission()
    }
    reclog(currentKeyTag() + " "
      + (isUserNotAllow ? "isUserNotAllow," : "") + "请求录音权限失败:" + msg, "1");
  });
  isRecording.value = true
  recordFilePath.value = ''
  recordStartTime.value = Date.now() // 记录录音开始时间

}

// 开始录音
const recStart__2 = (sid: number) => {
  if (sid != SyncID.value) {
    reclog("sync cancel recStart__2", "#f60");
    return;
  }

  reclog(currentKeyTag() + " 正在打开录音...");
  RecordApp.UniWebViewActivate(vue3This); // App环境下必须先切换成当前页面WebView
  RecordApp.Start({
    type: "wav",
    bitRate: 16,
    sampleRate: 16000,
    onProcess: (buffers: any[], powerLevel: number, duration: number, sampleRate: number, newBufferIdx: number, asyncEnd: any) => {
      if (sid != SyncID.value) return;

      if (asr.value) { // 已打开实时语音识别
        asr.value.input(buffers, sampleRate, newBufferIdx);
      }

      recpowerx.value = powerLevel;
      recpowert.value = formatTime(duration, 1) + " / " + powerLevel;

      // H5、小程序等可视化图形绘制,直接运行在逻辑层;App里面需要在onProcess_renderjs中进行这些操作
      // #ifdef H5 || MP-WEIXIN
      if (waveView.value) {
        waveView.value.input(buffers[buffers.length - 1], powerLevel, sampleRate);
      }
      // #endif
    },
    onProcess_renderjs: `function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
			//App中是在renderjs中进行的可视化图形绘制
			if(this.waveView){
				this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
			}
		}`,
    stop_renderjs: `function(aBuf,duration,mime){
			//App中可以放一个函数,在Stop成功时renderjs中会先调用这里的代码,this是renderjs模块的this(也可以用This变量)
			this.audioData=aBuf; //留着给Stop时进行转码成wav播放
		}`
  }, () => {
    reclog(currentKeyTag() + " 已开始录音,请讲话(asrProcess中已限制最多识别60*2-5*(2-1)=115秒)...", "2");

    // 创建音频可视化图形绘制,App环境下是在renderjs中绘制,H5、小程序等是在逻辑层中绘制,因此需要提供两段相同的代码(宽高值需要和canvas style的宽高一致)
    // RecordApp.UniFindCanvas(null, [".recwave-WaveView"], `
    // 	this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
    // `, (canvas1: any) => {
    // 	waveView.value = Recorder.WaveView({ compatibleCanvas: canvas1, width: 300, height: 100 });
    // });

    RecordApp.UniFindCanvas(null,[".recwave-Histogram3"],`
						this.waveView=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
							,lineCount:20,position:0,minHeight:4,fallDuration:400,stripeEnable:false,mirrorEnable:true
							,linear:[0,"#ffffff",1,"#ffffff"]});
					`,(canvas1:any)=>{
      waveView.value=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
        ,lineCount:20,position:0,minHeight:4,fallDuration:400,stripeEnable:false,mirrorEnable:true
        ,linear:[0,"#ffffff",1,"#ffffff"]});
    });

  }, (msg: string) => {
    reclog(currentKeyTag() + " 开始录音失败:" + msg, "1");
    recCancel("开始录音失败"); // 立即结束语音识别
  });
}

// 开始语音识别
const recStart__asrStart = (sid: number) => {
  if (sid != SyncID.value) {
    reclog("sync cancel recStart__asrStart", "#f60");
    return;
  }

  // 创建语音识别对象,每次识别都要新建,asr不能共用
  var asrInstance = Recorder.ASR_Aliyun_Short({
    tokenApi: asrTokenApi.value,
    apiArgs: {
      lang: asrLang.value
    },
    apiRequest: uni_ApiRequest, // tokenApi的请求实现方法
    compatibleWebSocket: uni_WebSocket, // 返回兼容WebSocket的对象
    asrProcess: (text: string, nextDuration: number, abortMsg?: string) => {
      /***实时识别结果,必须返回true才能继续识别,否则立即超时停止识别***/
      // 检查是否已被取消
      if (isRecordingCancelled.value) {
        reclog("[asrProcess回调]录音已取消", "1");
        recCancel("录音已取消");
        isRecordingCancelled.value = false
        recordStartTime.value = 0
        isRecording.value = false
        return false;
      }

      if (abortMsg) {
        // 语音识别中途出错
        reclog("[asrProcess回调]被终止:" + abortMsg, "1");
        recCancel("语音识别出错"); // 立即结束录音,就算继续录音也不会识别
        return false;
      }

      asrTxt.value = text;
      asrTime.value = ("识别时长: " + formatTime(asrInstance.asrDuration())
        + " 已发送数据时长: " + formatTime(asrInstance.sendDuration()));
      return nextDuration <= 2 * 60 * 1000; // 允许识别2分钟的识别时长(比录音时长小5秒)
    },
    log: (msg: string, color?: string) => {
      reclog(msg, color == "1" ? "#faa" : "#aaa");
    }
  });

  asr.value = asrInstance;

  reclog("语言:" + asrInstance.set.apiArgs.lang + ",tokenApi:" + asrInstance.set.tokenApi + ",正在打开语音识别...");

  // 打开语音识别,建议先打开asr,成功后再开始录音
  asrInstance.start(() => {
    // 无需特殊处理start和stop的关系,只要调用了stop,会阻止未完成的start,不会执行回调
    reclog("已开始语音识别", "2");
    recStart__2(sid);
  }, (errMsg: string) => {
    reclog("语音识别开始失败,请重试:" + errMsg, "1");
    recCancel("语音识别开始失败");
  });
}

// 停止录音,结束语音识别
const recStop = () => {
  ++SyncID.value;

  // 保存当前状态,防止在异步操作中被重置
  const currentAction = recordingAction.value
  const isCurrentlyRecording = isRecording.value

  console.log('recStop 被调用,recordingAction:', currentAction, 'isRecording:', isCurrentlyRecording)

  // 如果滑动到取消区域,执行取消操作
  if (currentAction === 'cancel' && isCurrentlyRecording) {
    console.log('滑动到取消区域,执行取消操作')
    // 先设置取消标志,防止后续操作
    isRecordingCancelled.value = true
    // 先取消录音并关闭语音层(同步操作)
    cancelVoiceRecording()
    // 然后中断语音识别(异步操作)
    recCancel('用户取消录音')
    return
  }

  // 如果滑动到转文字区域,停止录音和语音识别但不关闭遮罩层
  if (currentAction === 'text' && isCurrentlyRecording) {
    console.log('滑动到转文字区域,停止录音和语音识别但不关闭遮罩层')
    // 停止录音但不关闭遮罩层
    stopVoiceRecordingForText()
    // 停止语音识别但不关闭遮罩层
    recCancelForText()
    // 标记转文字模式已就绪,按钮将变为发送按钮
    isTextModeReady.value = true
    return
  }

  // 正常停止录音
  recCancel();
}

const recCancel = (cancelMsg?: string) => {
  reclog("正在停止...");

  var asr2 = asr.value;
  asr.value = null; // 先干掉asr,防止重复stop

  if (!asr2) {
    reclog("未开始识别", "1");
  } else {
    // asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
    asr2.stop((text: string, abortMsg?: string) => {
      if (abortMsg) {
        abortMsg = "发现识别中途被终止(一般无需特别处理):" + abortMsg;
      }
      reclog("语音识别完成" + (abortMsg ? "," + abortMsg : ""), abortMsg ? "#f60" : "2");
      reclog("识别最终结果:" + text, "2");

      // 检查是否从取消按钮松开(需要同时检查 recordingAction 和 isRecordingCancelled)
      const isCanceled = cancelMsg || isRecordingCancelled.value || recordingAction.value === 'cancel'

      // 默认解析语音识别的文字并发送出去
      // 如果识别结果不为空且未被取消,则自动发送
      if (text && text.trim() && !isCanceled && !isWaitingForResponse.value) {
        // 设置识别结果到输入框
        inputMessage.value = text.trim()
        nextTick(() => {
          sendMessage()
        })
      } else if (isCanceled) {
        // 如果是取消操作,不发送识别结果,也不弹出任何提示
        reclog("录音已取消,不发送识别结果", "1");
      } else if (!text || !text.trim()) {
        // 只有在非取消状态下,识别结果为空时才弹出提示
        reclog("识别结果为空,不发送", "1");
        uni.showToast({
          title: '未识别出有效内容',
          icon: 'none',
          duration: 2000
        })
      } else if (isWaitingForResponse.value) {
        reclog("AI正在回答中,不发送识别结果", "1");
      }
    }, (errMsg: string) => {
      reclog("语音识别" + (cancelMsg ? "被取消" : "结束失败") + ":" + errMsg, "1");
    });
  }

  RecordApp.Stop((aBuf: ArrayBuffer, duration: number, mime: string) => {
    // 得到录音数据,可以试听参考
    var recSet = (RecordApp.GetCurrentRecOrNull() || { set: { type: "wav" } }).set;
    reclog("已录制[" + mime + "]:" + formatTime(duration, 1) + " " + aBuf.byteLength + "字节 "
      + recSet.sampleRate + "hz " + recSet.bitRate + "kbps", "2");

    var aBuf_renderjs = "this.audioData";

    // 播放,部分格式会转码成wav播放
    if (playerRef.value) {
      playerRef.value.setPlayBytes(aBuf, aBuf_renderjs, duration, mime, recSet, Recorder);
    }
  }, (msg: string) => {
    reclog("结束录音失败:" + msg, "1");
    // 如果是取消操作,不弹出提示
    if (!cancelMsg && !isRecordingCancelled.value && recordingAction.value !== 'cancel') {
      uni.showToast({
        title: '未识别出有效内容',
        icon: 'none',
        duration: 2000
      })
    }
  });

  // 关闭语音层(隐藏录音遮罩层)
  isRecording.value = false
  recordingAction.value = 'none'
  isSlidingToAction.value = false
  isRecordingCancelled.value = false
  recordStartTime.value = 0

}

// 停止语音识别但不关闭遮罩层(用于转文字功能)
const recCancelForText = () => {
  reclog("正在停止语音识别(转文字模式)...");

  var asr2 = asr.value;
  asr.value = null; // 先干掉asr,防止重复stop

  if (!asr2) {
    reclog("未开始识别", "1");
  } else {
    // asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
    asr2.stop((text: string, abortMsg?: string) => {
      if (abortMsg) {
        abortMsg = "发现识别中途被终止(一般无需特别处理):" + abortMsg;
      }
      reclog("语音识别完成" + (abortMsg ? "," + abortMsg : ""), abortMsg ? "#f60" : "2");
      reclog("识别最终结果:" + text, "2");

      // 转文字模式:将识别结果显示在遮罩层的文字气泡中
      if (text && text.trim()) {
        asrTxt.value = text.trim()
        reclog("识别结果已显示在文字气泡中", "2");
      } else {
        // 转文字模式下,即使识别结果为空也不弹出提示,让用户自己决定是否取消
        reclog("识别结果为空", "1");
        asrTxt.value = '' // 清空文字气泡
      }
    }, (errMsg: string) => {
      reclog("语音识别结束失败:" + errMsg, "1");
    });
  }

  RecordApp.Stop((aBuf: ArrayBuffer, duration: number, mime: string) => {
    // 得到录音数据,可以试听参考
    var recSet = (RecordApp.GetCurrentRecOrNull() || { set: { type: "wav" } }).set;
    reclog("已录制[" + mime + "]:" + formatTime(duration, 1) + " " + aBuf.byteLength + "字节 "
      + recSet.sampleRate + "hz " + recSet.bitRate + "kbps", "2");

    var aBuf_renderjs = "this.audioData";

    // 播放,部分格式会转码成wav播放
    if (playerRef.value) {
      playerRef.value.setPlayBytes(aBuf, aBuf_renderjs, duration, mime, recSet, Recorder);
    }
  }, (msg: string) => {
    reclog("结束录音失败:" + msg, "1");
    // 转文字模式下,即使录音失败也不弹出提示,让用户自己决定是否取消
    // 如果是取消操作,也不弹出提示
    if (!isRecordingCancelled.value && recordingAction.value !== 'cancel') {
      uni.showToast({
        title: '未识别出有效内容',
        icon: 'none',
        duration: 2000
      })
    }
  });

  // 注意:不关闭遮罩层,保持 isRecording.value = true,以便显示识别文字
  // 不重置 recordingAction,保持 'text' 状态
  // 不重置 isSlidingToAction,保持滑动状态
  recordStartTime.value = 0
}

// 记录日志
const reclog = (msg: string, color?: string) => {
  var now = new Date();
  var t = ("0" + now.getHours()).substr(-2)
    + ":" + ("0" + now.getMinutes()).substr(-2)
    + ":" + ("0" + now.getSeconds()).substr(-2);
  var txt = "[" + t + "]" + msg;
  console.log(txt);
  reclogs.value.splice(0, 0, { txt: txt, color: color });
}

// 格式化时间
const formatTime = (ms: number, showSS?: number) => {
  var ss = ms % 1000;
  ms = (ms - ss) / 1000;
  var s = ms % 60;
  ms = (ms - s) / 60;
  var m = ms % 60;
  ms = (ms - m) / 60;
  var h = ms;
  var v = "";

  if (h > 0) v += (h < 10 ? "0" : "") + h + ":";
  v += (m < 10 ? "0" : "") + m + ":";
  v += (s < 10 ? "0" : "") + s;
  if (showSS) v += "″" + ("00" + ss).substr(-3);

  return v;
}
Pager
上一篇AI合同审查
下一篇知识库系统
本站总访问量 - 次 本站总访客数 - 人
Copyright © 2025 leolog  湘ICP备2025138984号-1 湘公网安备43138202000191号