效果图
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
}
五、感谢
感谢这位博主的分享
努力挣钱的小鑫