vue3 中使用 Recorder 实现录音并上传,并用Go语言调取讯飞识别录音(Go语言)

ops/2025/3/14 16:59:43/

录音并识别

  • 效果图
  • 一、开启游览器录音权限
  • 二、前端代码
  • 三、Go代码,上传到讯飞识别录音返回到前端

效果图

在这里插入图片描述

recorder-core插件可以在网页中进行录音。录音文件(blob)并可以自定义上传,可以下载录音文件到本地,本文录音过程中会显示可视化波形,插件兼容PC端、Android、和iOS,目前只测试pc端

一、开启游览器录音权限

http://localhost:5173/ 网址为例:
第一步:在浏览器地址栏输入:
edge游览器输入:edge://flags/#unsafely-treat-insecure-origin-as-secure
谷歌游览器输入: chrome://flags/#unsafely-treat-insecure-origin-as-secure
第二步:在 Insecure origins treated as secure 输入栏中输入需要获取麦克风权限的白名单网址。
第三步:将右侧 已禁用 状态改成 已启用。
第四步:点击浏览器右下角 重启 按钮重启浏览器。
在这里插入图片描述

二、前端代码

下载插件

npm install recorder-core
使用的版本号:
“recorder-core”: “^1.3.25011100”,

前端代码使用了antdv UI库,只在button 和switch 上使用了,可以自行删除前缀(a-)

<template><div style="padding: 10px"><div style="display: flex; align-items: center"><!-- 按钮 --><a-button @click="recOpen">打开录音,请求权限</a-button><a-button @click="recStart">开始录音</a-button><a-button @click="recStop">结束录音</a-button><a-button @click="recPlay">本地试听</a-button><div style="display: flex; align-items: center; margin: 0 10px"><span>下载文件到本地</span><a-switch v-model:checked="downloadShow" checked-children="开" un-checked-children="关" /></div><div style="display: flex; align-items: center; margin: 0 10px"><span>录音完成上传识别</span><a-switch v-model:checked="uploadShow" checked-children="开" un-checked-children="关" /></div></div><div style="margin-top: 10px"><div> 波形绘制区域 </div><div style="padding-top: 5px"><div style="border: 1px solid #ccc; display: inline-block; vertical-align: bottom"><div style="height: 100px; width: 300px" ref="recwave"></div></div></div></div><div style="margin: 10px 0"><div class="file"><i class="ico-plus"></i>上传图片<inputtype="file"id="avatar"name="avatar"accept="wav/*"multiplerequired@change="changeFile"/></div></div><div>识别结果:<span style="color: aqua">{{ recordingResult }}</span></div></div>
</template><script lang="ts" setup>import { onMounted, ref } from 'vue';//必须引入的核心import Recorder from 'recorder-core';//引入mp3格式支持文件;如果需要多个格式支持,把这些格式的编码引擎js文件放到后面统统引入进来即可import 'recorder-core/src/engine/mp3';import 'recorder-core/src/engine/mp3-engine';//录制wav格式的用这一句就行import 'recorder-core/src/engine/wav';//可选的插件支持项,这个是波形可视化插件import 'recorder-core/src/extensions/waveview';import { upWav } from '/@/api/service';let rec: any;let recBlob: any;let wave: any;const recwave = ref(null);let recordingResult = ref('');let downloadShow = ref(false);let uploadShow = ref(true);onMounted(() => {recOpen();});// 打开录音function recOpen() {rec = Recorder({type: 'wav',sampleRate: 16000,bitRate: 16,onProcess: (buffers: any,powerLevel: any,bufferDuration: any,bufferSampleRate: any,newBufferIdx: any,asyncEnd: any,) => {if (wave) {wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);}},});rec.open(() => {console.log('录音已打开');if (recwave.value) {wave = Recorder.WaveView({ elem: recwave.value });}},(msg: any, isUserNotAllow: any) => {console.log((isUserNotAllow ? 'UserNotAllow,' : '') + '无法录音:' + msg);},);}// 开始录音async function recStart() {rec?.start();}// 结束录音并保存到本地function recStop() {if (!rec) {console.error('未打开录音');return;}rec.stop((blob: any, duration: any) => {//blob就是我们要的录音文件对象,可以上传,或者本地播放recBlob = blob;//简单利用URL生成本地文件地址,此地址只能本地使用,比如赋值给audio.src进行播放,赋值给a.href然后a.click()进行下载(a需提供download="xxx.mp3"属性)const localUrl = (window.URL || window.webkitURL).createObjectURL(blob);console.log('录音成功', blob, localUrl, '时长:' + duration + 'ms');if (downloadShow.value) {download(blob);}if (uploadShow.value) {upload(blob); //把blob文件上传到服务器}//关闭录音,释放录音资源,当然可以不释放,后面可以连续调用startrec.close();rec = null;},(err: any) => {console.error('结束录音出错:' + err);rec.close();rec = null;},);}// 本地试听(保持原样)function recPlay() {const localUrl = URL.createObjectURL(recBlob);const audio = document.createElement('audio');audio.controls = true;document.body.appendChild(audio);audio.src = localUrl;audio.play();setTimeout(() => {URL.revokeObjectURL(audio.src);}, 5000);}/**下载录音文件到本地*/function download(blob) {// 创建下载链接const url = URL.createObjectURL(blob);const a = document.createElement('a');a.style.display = 'none';a.href = url;a.download = `recording_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.wav`; // 使用时间戳生成唯一文件名document.body.appendChild(a);a.click();// 清理资源setTimeout(() => {URL.revokeObjectURL(url);document.body.removeChild(a);}, 100);}/**上传录音*/function upload(blob: any) {//使用FormData用multipart/form-data表单上传文件//或者将blob文件用FileReader转成base64纯文本编码,使用普通application/x-www-form-urlencoded表单上传// const form = new FormData();// form.append('upfile', blob, 'recorder.mp3'); // 和普通form表单并无二致,后端接收到upfile参数的文件,文件名为recorder.mp3// form.append('key', 'value'); // 其他参数// var xhr = new XMLHttpRequest();// xhr.open('POST', '/upload/xxxx');// xhr.onreadystatechange = () => {//   if (xhr.readyState == 4) {//     if (xhr.status == 200) {//       console.log('上传成功');//     } else {//       console.error('上传失败' + xhr.status);//     }//   }// };// xhr.send(form);// 自己的上传函数uploadService(blob);}/**手动选择文件上传*/async function changeFile(e: any) {// 确保事件对象有效if (!e || !e.target || !e.target.files) {console.error('Invalid event or no files selected');return;}// 获取文件列表const files = e.target.files;console.log('e::::', e);console.log('files::::', files);// 检查文件列表是否为空if (files.length === 0) {console.error('No files selected');return;}// 获取第一个文件const file = files[0];console.log('file::::', file);// 调用 OCR 函数try {let res = await upWav({ file: file });console.log('🚀 ~ changeFile ~ res:', res);} catch (error) {console.error('Error during file processing:', error);}}/**录音完毕,自动上传识别*/async function uploadService(blob) {try {let res: any = await upWav({ file: blob });if (res.data.code == 1) {recordingResult.value = res.data.data;} else {recordingResult.value = res.data.msg;}} catch (error) {console.error('Error during file processing:', error);}}
</script>

三、Go代码,上传到讯飞识别录音返回到前端

使用讯飞的录音文件转写产品

获取前端上传的文件

package ocrimport ("context""fmt""io""io/ioutil""net/http""ocrtext/internal/logic/xfyun""ocrtext/internal/svc""ocrtext/internal/types""os""path/filepath""github.com/zeromicro/go-zero/core/logx"
)type UpWavBySpeechRecognitionLogic struct {logx.Loggerctx    context.ContextsvcCtx *svc.ServiceContextr      *http.Request
}func NewUpWavBySpeechRecognitionLogic(ctx context.Context, svcCtx *svc.ServiceContext, r *http.Request) *UpWavBySpeechRecognitionLogic {return &UpWavBySpeechRecognitionLogic{Logger: logx.WithContext(ctx),ctx:    ctx,svcCtx: svcCtx,r:      r,}
}func (l *UpWavBySpeechRecognitionLogic) UpWavBySpeechRecognition() (resp *types.OcrListResp, err error) {// 获取上传的文件file, handler, err := l.r.FormFile("file")if err != nil {fmt.Println("检索文件错误")return}defer file.Close()// 打印文件信息:handler.Filename//fmt.Printf("Uploaded File: %+v\n", handler.Filename)//fmt.Printf("File Size: %+v\n", handler.Size)//fmt.Printf("MIME Header: %+v\n", handler.Header)// 创建一个临时文件来保存上传的文件内容tempFile, err := ioutil.TempFile(os.TempDir(), "upload-*"+filepath.Ext(handler.Filename))if err != nil {fmt.Println("创建临时文件出错")return}defer tempFile.Close()// 将上传的文件内容复制到临时文件中_, err = io.Copy(tempFile, file)if err != nil {fmt.Println("将文件复制到临时文件错误")return}// 新增语音识别调用api := xfyun.NewRequestApi(tempFile.Name()) // 使用临时文件路径result, err := api.GetResult()if err != nil {return &types.OcrListResp{BaseDataInfo: types.BaseDataInfo{Code: 500,Msg:  "语音识别失败: " + err.Error(),},}, nil}// 解析识别结果(根据实际API响应结构调整)return &types.OcrListResp{BaseDataInfo: types.BaseDataInfo{Code: 1,},Data: result.Result,}, nil
}

上传到讯飞,识别录音返回到前端
讯飞后台地址:https://console.xfyun.cn/services/lfasr
在这里插入图片描述

package xfyunimport ("crypto/hmac""crypto/md5""crypto/sha1""encoding/base64""encoding/hex""encoding/json""fmt""io""net/http""net/url""os""strings""time"
)const (lfasrHost    = "https://raasr.xfyun.cn/v2/api"apiUpload    = "/upload"apiGetResult = "/getResult"appid        = "" // 自行填写secretKey    = "" // 自行填写
)type RequestApi struct {AppID          stringSecretKey      stringUploadFilePath stringts             stringsigna          string
}type UploadResponse struct {Code     string `json:"code"`DescInfo string `json:"descInfo"`Content struct {OrderID          string `json:"orderId"`TaskEstimateTime int    `json:"taskEstimateTime"`} `json:"content"`
}type OrderInfo struct {OrderId          string `json:"orderId"`failType         int    `json:"failType"`Status           int    `json:"status"`OriginalDuration int    `json:"originalDuration"`RealDuration     int    `json:"realDuration"`
}type GetResultResponse struct {Code     string `json:"code"`DescInfo string `json:"descInfo"`Content struct {OrderInfo OrderInfo `json:"orderInfo"`// 根据实际API响应添加字段OrderResult      string `json:"orderResult"`ResultText       string `json:"resultText"`TaskEstimateTime int    `json:"taskEstimateTime"`} `json:"content"`
}
type XFYunResp struct {Result string `json:"result"`
}// 基础结构体定义
type Cw struct {W string `json:"w"`
}type Ws struct {Cw []Cw `json:"cw"`
}type Rt struct {Ws []Ws `json:"ws"`
}type St struct {Rt []Rt `json:"rt"`
}type JSON1Best struct {St St `json:"st"`
}// Lattice项(需要二次解析)
type LatticeItem struct {JSON1Best string `json:"json_1best"`
}// Lattice2项(直接包含结构)
type Lattice2Item struct {JSON1Best JSON1Best `json:"json_1best"`
}
type OrderResult struct {Lattice  []LatticeItem  `json:"lattice"`Lattice2 []Lattice2Item `json:"lattice2"`
}func NewRequestApi(uploadFilePath string) *RequestApi {ts := fmt.Sprintf("%d", time.Now().Unix())return &RequestApi{AppID:          appid,SecretKey:      secretKey,UploadFilePath: uploadFilePath,ts:             ts,signa:          "",}
}// getSigna 获取接口鉴权
func (r *RequestApi) getSigna() string {data := r.AppID + r.tshasher := md5.New()hasher.Write([]byte(data))md5Str := hex.EncodeToString(hasher.Sum(nil))mac := hmac.New(sha1.New, []byte(r.SecretKey))mac.Write([]byte(md5Str))signa := base64.StdEncoding.EncodeToString(mac.Sum(nil))return signa
}// Upload 上传文件函数
func (r *RequestApi) Upload() (*UploadResponse, error) {r.signa = r.getSigna()fileInfo, err := os.Stat(r.UploadFilePath)if err != nil {return nil, err}params := url.Values{}params.Add("appId", r.AppID)params.Add("signa", r.signa)params.Add("ts", r.ts)params.Add("fileSize", fmt.Sprintf("%d", fileInfo.Size()))params.Add("fileName", fileInfo.Name())params.Add("duration", "200")file, err := os.Open(r.UploadFilePath)if err != nil {return nil, err}defer file.Close()reqURL := lfasrHost + apiUpload + "?" + params.Encode()resp, err := http.Post(reqURL, "application/octet-stream", file)if err != nil {return nil, err}defer resp.Body.Close()body, err := io.ReadAll(resp.Body)if err != nil {return nil, err}var uploadResp UploadResponseif err := json.Unmarshal(body, &uploadResp); err != nil {return nil, err}return &uploadResp, nil
}// GetResult 获取结果函数
func (r *RequestApi) GetResult() (*XFYunResp, error) {uploadResp, err := r.Upload()if err != nil {return nil, err}params := url.Values{}params.Add("appId", r.AppID)params.Add("signa", r.signa)params.Add("ts", r.ts)params.Add("orderId", uploadResp.Content.OrderID)params.Add("resultType", "transfer,predict")client := &http.Client{}var resultResp GetResultResponse/**get,post都可*/// 创建请求体缓冲区//var requestBody bytes.Buffer//writer := multipart.NewWriter(&requestBody)添加表单字段//_ = writer.WriteField("appId", r.AppID)//_ = writer.WriteField("signa", r.signa)//_ = writer.WriteField("ts", r.ts)//_ = writer.WriteField("orderId", uploadResp.Content.OrderID)//_ = writer.WriteField("resultType", "transfer,predict")//关闭writer以生成结尾边界//writer.Close()for {fmt.Println("重新请求")reqURL := lfasrHost + apiGetResult + "?" + params.Encode()resp, err := client.Get(reqURL)// 创建HTTP请求//reqURL := lfasrHost + apiGetResult//req, err := http.NewRequest("POST", reqURL, &requestBody)//if err != nil {//	// 处理错误//}设置Content-Type头(必须包含boundary参数)//req.Header.Set("Content-Type", writer.FormDataContentType())//发送请求//client := &http.Client{}//resp, err := client.Do(req)//if err != nil {//	return nil, err//}//defer resp.Body.Close()if err != nil {return nil, err}body, err := io.ReadAll(resp.Body)err = resp.Body.Close()if err != nil {return nil, err}if err := json.Unmarshal(body, &resultResp); err != nil {return nil, err}fmt.Printf("resultResp: %+v\n", resultResp)if resultResp.Code == "000000" && (resultResp.Content.OrderInfo.Status == 4 || resultResp.Content.OrderInfo.Status == -1) {break}if resultResp.Content.OrderInfo.Status == 4 { // 假设4表示完成break}time.Sleep(1 * time.Second)}/**提取文字内容*/ParseOrderResultResp, err := ParseOrderResult(resultResp.Content.OrderResult)if err != nil {return nil, err}return &XFYunResp{Result: ParseOrderResultResp,}, nil
}// ParseOrderResult 解析入口函数
func ParseOrderResult(strData string) (string, error) {var jsonData = []byte(strData)var result OrderResultif err := json.Unmarshal(jsonData, &result); err != nil {return "", err}var words []string// 统一处理两种数据结构processJSON1Best := func(content *JSON1Best) {for _, rt := range content.St.Rt {for _, ws := range rt.Ws {for _, cw := range ws.Cw {if cw.W != "" {words = append(words, cw.W)}}}}}// 解析Latticefor _, item := range result.Lattice {var content JSON1Bestif err := json.Unmarshal([]byte(item.JSON1Best), &content); err == nil {processJSON1Best(&content)}}// 解析Lattice2//for _, item := range result.Lattice2 {//	processJSON1Best(&item.JSON1Best)//}return strings.Join(words, ""), nil
}

五、感谢
感谢这位博主的分享
努力挣钱的小鑫


http://www.ppmy.cn/ops/165715.html

相关文章

赛事|基于SprinBoot+vue的CSGO赛事管理系统(源码+数据库+文档)

CSGO赛事管理系统 目录 基于SprinBootvue的CSGO赛事管理系统 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3参赛战队功能模块 4合作方功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&…

OBJ文件生成PCD文件(python 实现)

代码实现 将 .obj 文件转换为 .pcd&#xff08;点云数据&#xff09; 代码文件。 import open3d as o3d# 加载 .obj 文件 mesh o3d.io.read_triangle_mesh("bunny.obj")# 检查是否成功加载 if not mesh.has_vertices():print("无法加载 .obj 文件&#xff0c…

Manus:成为AI Agent领域的标杆

一、引言 官网&#xff1a;Manus 随着人工智能技术的飞速发展&#xff0c;AI Agent&#xff08;智能体&#xff09;作为人工智能领域的重要分支&#xff0c;正逐渐从概念走向现实&#xff0c;并在各行各业展现出巨大的应用潜力。在众多AI Agent产品中&#xff0c;Manus以其独…

C# net deepseek RAG AI开发 全流程 介绍

deepseek本地部署教程及net开发对接 步骤详解&#xff1a;安装教程及net开发对接全流程介绍 DeepSeekRAG 中的 RAG&#xff0c;全称是 Retrieval-Augmented Generation&#xff08;检索增强生成&#xff09;&#xff0c;是一种结合外部知识库检索与大模型生成能力的技术架构。其…

2025年03月11日Github流行趋势

项目名称&#xff1a;pydoll 项目地址url&#xff1a;https://github.com/thalissonvs/pydoll项目语言&#xff1a;Python历史star数&#xff1a;1372今日star数&#xff1a;148项目维护者&#xff1a;thalissonvs, apps/github-actions, LucasAlvws, CaioWzy, Patolox项目简介…

PHP语言的开源贡献

PHP语言的开源贡献及其影响 引言 在互联网技术飞速发展的今天&#xff0c;开源软件已经成为了软件开发的重要组成部分。它不仅改变了我们开发和使用软件的方式&#xff0c;更在促进技术共享、推动创新和降低开发成本等方面发挥了重要作用。而在众多的开源项目中&#xff0c;P…

golang的Map

Map集合 概述 Map 是一种无序的键值对的集合。 Map 最重要的一点是通过 key 来快速检索数据&#xff0c;key 类似于索引&#xff0c;指向数据的值。 Map 是一种集合&#xff0c;所以我们可以像迭代数组和切片那样迭代它。不过&#xff0c;Map 是无序的&#xff0c;遍历 Map…

leetcode0056. 合并区间 - medium

1 题目&#xff1a;合并区间 官方难度 - 中等 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1…