/* eslint-disable */
<template>
<div class="container">
<div
v-if="showCtrlProcessWaveStatus"
style="height:100px;width:100%;box-sizing: border-box;display:inline-block;vertical-align:bottom"
class="ctrlProcessWave"
/>
<!-- 放一个 <audio ></audio> 播放器,标签名字大写,阻止uniapp里面乱编译 -->
<AUDIO ref="LogAudioPlayer" :class="isPlay?'visible':'hidden'" style="width:100%" />
<el-input
v-model="text"
:class="isShowBase64?'visible':'hidden'"
type="textarea"
placeholder="语音Base64内容"
rows="10"
show-word-limit
readonly
/>
<el-input
v-model="identifyTexts"
:class="isShowIdentifyText?'visible':'hidden'"
type="textarea"
rows="10"
show-word-limit
/>
<div class="buttonContainer">
<el-button :class="isOpen?'hidden':'visible'" type="success" size="small" @click="openVoice">
开始录音
</el-button>
<el-button :class="isOpen?'visible':'hidden'" type="primary" size="small" @click="closeVoice">
{{ isLoading?'编译中...': `结束录音` }}
</el-button>
<el-button type="primary" size="small" @click="sureVoice">确定</el-button>
</div>
</div>
</template>
<script>
/* eslint-disable */
import * as api from '@/api/system/sluggish-sales-manage'
import Recorder from 'recorder-core'
import 'recorder-core/src/engine/mp3.js'
import 'recorder-core/src/engine/mp3-engine.js'
// import 'recorder-core/recorder.mp3.min.js'
import 'recorder-core/src/extensions/waveview'
import CryptoJS from 'crypto-js'
const APPID = 'xxxxxxxx'
const API_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
const API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
export default {
props: {
openStatus: {
type: Boolean,
default: false
},
playStatus: {
type: Boolean,
default: false
},
showCtrlProcessWaveStatus: {
type: Boolean,
default: false
},
showBase64Status: {
type: Boolean,
default: false
},
downStatus: {
type: Boolean,
default: false
},
identifyStatus: {
type: Boolean,
default: false
},
showIdentifyTextStatus: {
type: Boolean,
default: false
},
skc: {
type: String,
default: ''
},
skuInfoObj: {
type: Object,
default: {}
}
},
data() {
return {
isOpen: this.openStatus,//是否正在录音
isPlay: this.playStatus,//是否正在播放音频
isShowCtrlProcessWave: this.showCtrlProcessWaveStatus,
isShowBase64: this.showBase64Status,
isDown: this.downStatus,
isIdentify: this.identifyStatus,
isShowIdentifyText: this.showIdentifyTextStatus,
audio: null, // 音频
text: null, // 存放转换后的文本内容(用于存储 Base64 编码的音频数据)
identifyTexts: '', // 存放转换后的文本内容(用于存储 文件流 编码的音频数据)
audios: [],
base64s: [],
type: 'mp3',
bitRate: 16,
sampleRate: 16000,
duration: 0,
durationTxt: '0',
powerLevel: 0,
logs: [],
sendInterval: 0, // 发送间隔时长(毫秒),mp3 chunk数据会缓冲,当pcm的累积时长达到这个时长,就会传输发送。这个值在takeoffEncodeChunk实现下,使用0也不会有性能上的影响。
realTimeSendTryTime: 0,
realTimeSendTryNumber: null,
realTimeSendTryBytesChunks: null,
realTimeSendTryClearPrevBufferIdx: null,
realTimeSendTryBuffers: null,
transferUploadNumberMax: null,
recLogLast: {},
pcmData: [], // 用于保存 PCM 数据
isLoading:false,
}
},
mounted() {
if (this.$props.openStatus) {
this.openVoice()
}
},
watch: {
skuInfoObj: {
handler(newVal, oldVal) {
if (newVal) {
console.log('newVal',newVal)
this.identifyTexts=newVal.skcRemark
}
},
immediate: true,
deep: true
}
},
methods: {
sureVoice() {
let t = this
let req = {
skcNo: t.skc,
remark:t.identifyTexts
}
api.saveRemark(req).then(res => {
this.$emit('closeRemarkDialog')
return t.$message.success('保存成功')
})
},
openVoice: function() {
var This = this
This.pcmData = []
This.isLoading = false
var rec = this.rec = Recorder({//初始化 Recorder 实例
type: This.type,//mp3格式
bitRate: +This.bitRate,//指定采样率hz
sampleRate: +This.sampleRate,//比特率kbps
//接收到录音数据时的回调函数
onProcess: function (buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) {
//buffers=[[Int16,...],...]:缓冲的PCM数据块(16位小端LE),为从开始录音到现在的所有pcm片段,每次回调可能增加0-n个不定量的pcm片段
//powerLevel:当前缓冲的音量级别0-100
//duration:已缓冲时长
//sampleRate(Worker)时,此采样率和设置的采样率相同,否则不一定相同)
//newBufferIdx:本次回调新增的buffer起始索引
//asyncEnd:fn() 如果onProcess是异步的(返回值为true时),处理完成时需要调用此回调,如果不是异步的请忽略此参数,此方法回调时必须是真异步(不能真异步时需用setTimeout包裹)
This.realTimeOnProcessClear(buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd)// 实时数据处理,清理内存
This.duration = duration
This.durationTxt = This.formatMs(duration, 1)
This.powerLevel = powerLevel
// 将 PCM 数据添加到 pcmData 数组中
for (var i = 0; i < buffers.length; i++) {
if (buffers[i]) {
This.pcmData.push(...buffers[i]);
}
}
//录音的时候,绘制波形语音段
if (This.isShowCtrlProcessWave) This.wave.input(buffers[buffers.length - 1], powerLevel, sampleRate)
},
//实时编码环境下接管编码器输出,当编码器实时编码出一块有效的二进制音频数据时实时回调此方法;参数为二进制的Uint8Array
//当提供此回调方法时,将接管编码器的数据输出,编码器内部将放弃存储生成的音频数据;如果当前编码器或环境不支持实时编码处理,将在open时直接走fail逻辑
//因此提供此回调后调用stop方法将无法获得有效的音频数据,因为编码器内没有音频数据,因此stop时返回的blob将是一个字节长度为0的blob
//大部分录音格式编码器都支持实时编码(边录边转码),比如mp3格式:会实时的将编码出来的mp3片段通过此方法回调,所有的chunkBytes拼接到一起即为完整的mp3,此种拼接的结果比mock方法实时生成的音质更加,因为天然避免了首尾的静默
//不支持实时编码的录音格式不可以提供此回调(wav格式不支持,因为wav文件头中需要提供文件最终长度),提供了将在open时直接走fail逻辑
//用于处理编码的音频数据
takeoffEncodeChunk: function (chunkBytes) {
//chunkBytes:就是编码出来的音频数据片段,所有的chunkBytes拼接在一起即为完整音频
//当你不断说话的时候,就会一直执行下方的逻辑,类似于setTimeOut
// 大于等于60秒自动关闭
if (This.duration >= 5 *60000) {
This.closeVoice()
} else {
// 接管实时转码,推入实时处理(将编码的音频数据发送到 WebSocket 服务器进行实时语音识别)
This.realTimeSendTry(chunkBytes, false)
}
}
})
rec.open(function() {
This.reclog('已打开:' + This.type + ' ' + This.sampleRate + 'hz ' + This.bitRate + 'kbps', 2)
This.recStart()
This.isOpen = true
if (This.isShowCtrlProcessWave) This.wave = Recorder.WaveView({ elem: '.ctrlProcessWave' })
if (This.$refs && This.$refs.LogAudioPlayer) {
var audio = This.$refs.LogAudioPlayer
audio.pause()
audio = null
This.audio = audio
}
This.text = null
This.identifyText = ''
This.base64s = []
}, function (msg, isUserNotAllow) {
if (isUserNotAllow) {
// 用户没有给予录音权限,或者没有可用的录音设备
// 在这里添加你的提醒代码
This.$message.error("无法录音:请检查你的麦克风设备是否正常,或者是否已经给予录音权限");
} else {
// 其他原因导致的打开录音设备失败
This.reclog('打开失败:' + msg, 1);
This.$message.error('打开失败:' + msg);
}
})
},
recStart: function() {
if (!this.rec || !Recorder.IsOpen()) {
this.reclog('未打开录音', 1)
this.isOpen = false
return
}
this.rec.start()
this.realTimeSendTryReset()
var set = this.rec.set
this.reclog('录制中:' + set.type + ' ' + set.sampleRate + 'hz ' + set.bitRate + 'kbps')
},
closeVoice: function() {
this.realTimeSendTry(null, true)
this.rec.close()
},
recLast: function() {
if (!this.recLogLast) {
this.reclog('请先录音,然后停止后再播放', 1)
return
}
if (this.isPlay) this.recplay(this.recLogLast.idx)
if (this.isShowBase64) this.recdown64(this.recLogLast.idx)
if (this.isDown) this.recdown(this.recLogLast.idx)
},
//播放音频
recplay: function(idx) {
var This = this
var o = this.logs[this.logs.length - idx - 1]
o.play = (o.play || 0) + 1
var logmsg = function(msg) {
o.playMsg = '<span style="color:green">' + o.play + '</span> ' + This.getTime() + ' ' + msg
}
logmsg('')
var audio = this.$refs.LogAudioPlayer
audio.controls = true
if (!(audio.ended || audio.paused)) {
audio.pause()
}
audio.onerror = function(e) {
logmsg('<span style="color:red">播放失败[' + audio.error.code + ']' + audio.error.message + '</span>')
}
audio.src = (window.URL || window.webkitURL).createObjectURL(o.res.blob)
audio.play()
This.audio = audio
},
//下载 Base64 编码的音频数据
recdown64: function(idx) {
var This = this
var o = this.logs[this.logs.length - idx - 1]
var reader = new FileReader()
reader.readAsDataURL(o.res.blob)
reader.onloadend = function() {
var base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]
This.text = base64
}
},
//下载音频文件
recdown: function(idx) {
var This = this
var o = this.logs[this.logs.length - idx - 1]
o.down = (o.down || 0) + 1
o = o.res
var name = 'rec-' + o.duration + 'ms-' + (o.rec.set.bitRate || '-') + 'kbps-' + (o.rec.set.sampleRate || '-') + 'hz.' + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || 'unknown')
var downA = document.createElement('A')
downA.href = (window.URL || window.webkitURL).createObjectURL(o.blob)
downA.download = name
downA.click()
},
//毫秒数转换为时分秒,当 all为true时,会显示所有的时间单位,否则只显示非零的时间单位
formatMs: function(ms, all) {
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 t = (h ? h + ':' : '') +
(all || h + m ? ('0' + m).substr(-2) + ':' : '') +
(all || h + m + s ? ('0' + s).substr(-2) + '″' : '') +
('00' + ss).substr(-3)
return t
},
//获取当前时间,格式:HH:MM:SS
getTime: function() {
var now = new Date()
var t = ('0' + now.getHours()).substr(-2) +
':' + ('0' + now.getMinutes()).substr(-2) +
':' + ('0' + now.getSeconds()).substr(-2)
return t
},
//在录音过程中记录和打印日志
reclog: function (msg, color, res) {
//msg:日志消息
//color:是用于在控制台输出时区分不同类型日志的颜色代码
//res:包含音频数据和相关信息的对象
//playMsg:播放消息
//down:下载次数
//down64Val:Base64编码的音频数据
var obj = {
idx: this.logs.length,
msg: msg,
color: color,
res: res,
playMsg: '',
down: 0,
down64Val: ''
}
if (res && res.blob) {//如果 res 对象存在并包含音频数据 blob
this.recLogLast = obj//用于记录最后一次的音频数据
}
this.logs.splice(0, 0, obj)
},
realTimeSendTryReset() {
this.realTimeSendTryTime = 0
},
realTimeOnProcessClear(buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) {//清空,开始录音的时候,this.realTimeSendTryTime的值为0
if (this.realTimeSendTryTime == 0) {
this.realTimeSendTryTime = Date.now()
this.realTimeSendTryNumber = 0
this.transferUploadNumberMax = 0
this.realTimeSendTryBytesChunks = []
this.realTimeSendTryClearPrevBufferIdx = 0
this.realTimeSendTryBuffers = []
}
// 清理PCM缓冲数据,最后完成录音时不能调用stop,因为数据已经被清掉了
// 这里进行了延迟操作(必须要的操作),只清理上次到现在的buffer
for (var i = this.realTimeSendTryClearPrevBufferIdx; i < newBufferIdx; i++) {
buffers[i] = null
}
this.realTimeSendTryClearPrevBufferIdx = newBufferIdx
// 备份一下方便后面生成wav
for (var i = newBufferIdx; i < buffers.length; i++) {
this.realTimeSendTryBuffers.push(buffers[i])
}
},
realTimeSendTry(chunkBytes, isClose) {
console.log('123')
if (chunkBytes) { // 推入缓冲再说
this.realTimeSendTryBytesChunks.push(chunkBytes)
}
var t1 = Date.now()
if (!isClose && t1 - this.realTimeSendTryTime < this.sendInterval) {//边说边翻译,当说话间隔设置小于你设置的间隔,就不进行传输
console.log('return掉了')
return// 控制缓冲达到指定间隔才进行传输
}
this.realTimeSendTryTime = t1
var number = ++this.realTimeSendTryNumber
// 缓冲的chunk拼接成一个更长点的
var len = 0
for (var i = 0; i < this.realTimeSendTryBytesChunks.length; i++) {
len += this.realTimeSendTryBytesChunks[i].length
}
var chunkData = new Uint8Array(len)
for (var i = 0, idx = 0; i < this.realTimeSendTryBytesChunks.length; i++) {
var chunk = this.realTimeSendTryBytesChunks[i]
chunkData.set(chunk, idx)
idx += chunk.length
}
this.realTimeSendTryBytesChunks = []
// 推入传输
var blob = null; var meta = {}
if (chunkData.length > 0) { // 不是空的
blob = new Blob([chunkData], { type: 'audio/' + this.type })
meta = Recorder.mp3ReadMeta([chunkData.buffer], chunkData.length) || {}// 读取出这个片段信息
}
this.transferUpload(number
, blob
, meta.duration || 0
, {
set: {
type: this.type,
sampleRate: meta.sampleRate,
bitRate: meta.bitRate
}
}
, isClose
)
var recMock = Recorder({
type: this.type,
sampleRate: this.sampleRate,
bitRate: this.bitRate
})
if (this.realTimeSendTryBuffers.length > 0) {
var chunk = Recorder.SampleData(this.realTimeSendTryBuffers, this.sampleRate, this.sampleRate)
recMock.mock(chunk.data, this.sampleRate)
var This = this
recMock.stop(function(blob, duration) {
var item = {
blob: blob,
duration: duration,
durationTxt: This.formatMs(duration),
rec: recMock
}
This.audios.push(item)
})
}
this.realTimeSendTryBuffers = []
},
//音频数据转换为 Base64 编码,并通过 WebSocket 发送到服务器
transferUpload(number, blobOrNull, c, blobRec, isClose) {
this.transferUploadNumberMax = Math.max(this.transferUploadNumberMax, number)
var This = this
if (blobOrNull) {
var blob = blobOrNull
//* ********发送:Base64文本发送***************
var reader = new FileReader()
reader.readAsDataURL(blob)
reader.onloadend = function() {
var base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]
This.base64s.push(base64)
//创建了一个 WebSocket
This.connectWebSocket()
}
}
if (isClose) {
This.isLoading = true
// 将 PCM 数据转换为 Blob 对象
var pcmBlob = new Blob([new Int16Array(This.pcmData)], { type: 'audio/pcm' });
// 创建 FormData 对象,并添加 Blob 对象
const formData = new FormData();
formData.append('file', pcmBlob);
// 发送 FormData 对象到服务器
api.voice(formData).then(res => {
This.identifyTexts = This.identifyTexts + res.data
setTimeout(() => {
This.isOpen = false
}, 200);
}).catch(err => {
console.log(6666,err);
})
// this.mergeAll()
}
},
mergeAll() {
var bitRate = this.bitRate;
var idx = -1 + 1;
var files = [];
var This = this;
var read = function() {
var audios = This.audios;
var audiosItem = audios[idx++];
if (idx >= audios.length) {
if (!files.length) {
return;
}
var rec = audiosItem.rec;
This.mp3Merge(files, bitRate, function(fileBytes, duration) {
var blob = new Blob([fileBytes.buffer], { type: 'audio/' + This.type });
This.reclog('已录制:', '', {
blob: blob,
duration: duration,
durationTxt: This.formatMs(duration),
rec: rec
});
// 创建 FormData 对象,并添加 Blob 对象
const formData = new FormData();
formData.append('file', blob);
api.voice(formData).then(res => {
// 处理响应
// 创建隐藏的可下载链接
var element = document.createElement('a');
element.setAttribute('href', window.URL.createObjectURL(blob));
element.setAttribute('download', 'response.mp3'); // or any other extension
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}).catch(err => {
console.log(6666,err);
});
This.recLast();
})
This.audios = [];
return;
}
var reader = new FileReader();
reader.onloadend = function() {
files.push(new Uint8Array(reader.result));
read();
}
console.log('audiosItem.blob:', audiosItem.blob);
reader.readAsArrayBuffer(audiosItem.blob);
}
read();
},
mp3Merge(fileBytesList, bitRate, True) {
// 计算所有文件总长度
var size = 0
for (var i = 0; i < fileBytesList.length; i++) {
size += fileBytesList[i].byteLength
}
// 全部直接拼接到一起
var fileBytes = new Uint8Array(size)
var pos = 0
for (var i = 0; i < fileBytesList.length; i++) {
var bytes = fileBytesList[i]
fileBytes.set(bytes, pos)
pos += bytes.byteLength
}
// 计算合并后的总时长
var duration = Math.round(size * 8 / bitRate)
True(fileBytes, duration)
},
// 连接websocket
connectWebSocket() {
if (!this.isIdentify) return
console.log('连接websocket')
return this.getWebSocketUrl().then(url => {
if (this.webSocket && this.webSocket.readyState === this.webSocket.OPEN) {
return
} else {
if ('WebSocket' in window) {
console.log('创建WebSocket')
this.webSocket = new WebSocket(url)
} else if ('MozWebSocket' in window) {
console.log('MozWebSocket')
this.webSocket = new MozWebSocket(url)
} else {
return
}
//用于连接成功后发送数据
this.webSocket.onopen = e => {
this.webSocketSend()
}
//用于接收服务器返回的识别结果
this.webSocket.onmessage = e => {
this.result(e.data)
}
this.webSocket.onerror = e => {
}
this.webSocket.onclose = e => {
}
}
})
},
getWebSocketUrl() {
return new Promise((resolve) => {
// 请求地址根据语种不同变化
var url = 'wss://iat-api.xfyun.cn/v2/iat'
var host = 'iat-api.xfyun.cn'
var apiKey = API_KEY
var apiSecret = API_SECRET
var date = new Date().toGMTString()
var algorithm = 'hmac-sha256'
var headers = 'host date request-line'
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
var signature = CryptoJS.enc.Base64.stringify(signatureSha)
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
var authorization = btoa(authorizationOrigin)
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`
resolve(url)
})
},
// 向webSocket发送数据
webSocketSend() {
console.log('webSocketSend:', JSON.stringify(this.webSocket))
if (this.webSocket.readyState !== this.webSocket.OPEN) {
return
}
if (this.base64s.length == 0) return
this.identifyText = ''
var index = 0
var params
params =
{
common: {
app_id: APPID
},
business: {
language: 'zh_cn', // 小语种可在控制台--语音听写(流式)--方言/语种处添加试用
domain: 'iat',
accent: 'mandarin' // 中文方言可在控制台--语音听写(流式)--方言/语种处添加试用
},
data: {
status: 0,
format: 'audio/L16;rate=16000',
encoding: 'lame',
audio: this.base64s[index]
}
}
console.log('首帧', this.base64s.length, index)
// console.log("首帧", JSON.stringify(params));
this.webSocket.send(JSON.stringify(params))
this.handlerInterval = setInterval(() => {
// websocket未连接
if (this.webSocket.readyState !== this.webSocket.OPEN) {
console.log('websocket未连接:', this.base64s.length, index)
this.closeVoice()
clearInterval(this.handlerInterval)
return
}
if (this.base64s.length === index) {
params =
{
data: {
status: 2,
format: 'audio/L16;rate=16000',
encoding: 'lame',
audio: ''
}
}
console.log('尾帧', this.base64s.length, index)
// console.log("尾帧", JSON.stringify(params));
this.webSocket.send(JSON.stringify(params))
clearInterval(this.handlerInterval)
return false
}
// 中间帧
index++
params =
{
data: {
status: 1,
format: 'audio/L16;rate=16000',
encoding: 'lame',
audio: this.base64s[index]
}
}
console.log('中间帧', this.base64s.length, index)
// console.log("中间帧",this.base64s.length,index, JSON.stringify(params));
this.webSocket.send(JSON.stringify(params))
}, 40)
},
result(resultData) {
console.log('resultData:', resultData)
// 识别结束
const jsonData = JSON.parse(resultData)
if (jsonData.data && jsonData.data.result) {
const data = jsonData.data.result
const status = jsonData.data.status
let str = ''
const ws = data.ws
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w
}
console.log('识别的结果为' + this.isOpen + '===>' + status + ':', str)
if (status === 0) {
this.identifyText = str
}
if (status === 1) {
this.identifyText += str
}
if (status === 2) {
this.identifyText += str
}
}
if (jsonData.code === 0 && jsonData.data.status === 2) {
this.webSocket.close()
}
if (jsonData.code !== 0) {
this.webSocket.close()
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: fit-content;
.buttonContainer {
margin-top: 20px;
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
}
/* 隐藏 */
.hidden {
display: none;
}
/* 显示 */
.visible {
display: block;
}
</style>
外部组件
<!-- 语音转文字录入 -->
<el-dialog title="单款分析" width="1000px" :visible.sync="txtRemainVisible">
<voice :identify-status="true" :show-identify-text-status="true" :skc="skc" :sku-info-obj="skuInfoObj" @closeRemarkDialog="closeRemarkDialog" />
</el-dialog>
阿里-语音转文字开发联调,学习记录
最新推荐文章于 2026-04-04 13:21:07 发布

1万+

被折叠的 条评论
为什么被折叠?



