vue项目中集合科大讯飞语音识别功能,web端,语音听写(流式版)WebAPI 文档
然后还要准备CryptoJS这个第三方的包import CryptoJS from 'crypto-js';,直接npm下载就行。最后是封装了一个组件,来使用。这个项目使用的是ant的组件库。先在public文件下加入了iat,也就是demo文件中的dist文件夹。这次主要是将demo改写成了vue语法,其他的都没变。
·
科大讯飞官方文档:语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心
这是demo
这次主要是将demo改写成了vue语法,其他的都没变。
先在public文件下加入了iat,也就是demo文件中的dist文件夹
然后还要准备CryptoJS这个第三方的包import CryptoJS from 'crypto-js';,直接npm下载就行。
最后是封装了一个组件,来使用。这个项目使用的是ant的组件库。可自行换成别的。
<script>
import { Close, Voice } from '@icon-park/vue-next';
import CryptoJS from 'crypto-js';
import { message } from 'ant-design-vue';
export default {
name: 'XfIat',
components: { Close, Voice },
data() {
return {
// 控制录音弹窗
voiceOpen: false,
// 是否开始录音
startVoiceStatus: false,
// 识别中状态
identifyStatus: false,
recorder: null,
transcription: '',
btnStatus: '',
resultText: '',
resultTextTemp: '',
countdownInterval: null,
iatWS: null,
recognition: null,
};
},
emits: ['sendMsg'],
props: {
buttonDisabled: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
xfIatKeys: {
default: {
APPID: '',
APIKey: '',
APISecret: '',
},
},
},
methods: {
/**
* 打开录音
* @returns {Promise<void>}
*/
async startVoice() {
if (this.loading) {
return;
}
this.voiceOpen = true;
await this.playIatVoice();
},
async playIatVoice() {
if (this.loading) {
return;
}
this.startVoiceStatus = !this.startVoiceStatus;
// 浏览器自带的识别
if (this.recognition) {
if (this.startVoiceStatus) {
this.recognition.start();
} else {
this.recognition.stop();
}
return;
}
if (this.startVoiceStatus) {
this.connectWebSocket();
} else {
this.recorder.stop();
}
},
/**
* 关闭录音弹窗
*/
closeVoiceOpen() {
this.voiceOpen = false;
this.startVoiceStatus = false;
if (this.recorder) {
this.recorder.stop();
}
if (this.recognition) {
this.recognition.stop();
}
this.transcription = '';
},
renderResult(resultData) {
// 识别结束
const jsonData = JSON.parse(resultData);
if (jsonData.data && jsonData.data.result) {
const data = jsonData.data.result;
let str = '';
const { ws } = data;
for (let i = 0; i < ws.length; i += 1) {
str += ws[i].cw[0].w;
}
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
if (data.pgs) {
if (data.pgs === 'apd') {
// 将resultTextTemp同步给resultText
this.resultText = this.resultTextTemp;
}
// 将结果存储在resultTextTemp中
this.resultTextTemp = this.resultText + str;
} else {
this.resultText += str;
}
this.transcription = this.resultTextTemp || this.resultText || '';
}
if (jsonData.code === 0 && jsonData.data.status === 2) {
this.iatWS.close();
}
if (jsonData.code !== 0) {
this.iatWS.close();
console.error(jsonData);
}
},
connectWebSocket() {
const websocketUrl = this.getWebSocketUrl();
if ('WebSocket' in window) {
this.iatWS = new window.WebSocket(websocketUrl);
} else if ('MozWebSocket' in window) {
this.iatWS = new window.MozWebSocket(websocketUrl);
} else {
message.error('浏览器不支持WebSocket');
return;
}
this.changeBtnStatus('CONNECTING');
this.iatWS.onopen = e => {
console.log('iatWS.onopen', e);
// 开始录音
this.recorder.start({
sampleRate: 16000,
frameSize: 1280,
});
const params = {
common: {
app_id: this.xfIatKeys.APPID,
},
business: {
language: 'zh_cn',
domain: 'iat',
accent: 'mandarin',
vad_eos: 5000,
dwa: 'wpgs',
nbest: 1,
wbest: 1,
},
data: {
status: 0,
format: 'audio/L16;rate=16000',
encoding: 'raw',
},
};
this.iatWS.send(JSON.stringify(params));
};
this.iatWS.onmessage = e => {
this.renderResult(e.data);
};
this.iatWS.onerror = e => {
console.error(e);
this.recorder.stop();
this.changeBtnStatus('CLOSED');
};
this.iatWS.onclose = e => {
console.log(e);
this.recorder.stop();
this.changeBtnStatus('CLOSED');
};
},
getWebSocketUrl() {
const { APIKey, APISecret } = this.xfIatKeys;
if (!APIKey) {
message.error('语音识别配置未生效');
return null;
}
// 请求地址根据语种不同变化
let url = 'wss://iat-api.xfyun.cn/v2/iat';
const host = 'iat-api.xfyun.cn';
const apiKey = APIKey;
const apiSecret = APISecret;
const date = new Date().toGMTString();
const algorithm = 'hmac-sha256';
const headers = 'host date request-line';
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
const signature = CryptoJS.enc.Base64.stringify(signatureSha);
const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
const authorization = btoa(authorizationOrigin);
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
return url;
},
countdown() {
let seconds = 60;
console.log(`录音中(${seconds}s)`);
this.countdownInterval = setInterval(() => {
seconds -= 1;
if (seconds <= 0) {
clearInterval(this.countdownInterval);
this.recorder.stop();
} else {
console.log(`录音中(${seconds}s)`);
}
}, 1000);
},
changeBtnStatus(status) {
this.btnStatus = status;
if (status === 'CONNECTING') {
console.log('建立连接中');
this.resultText = '';
this.resultTextTemp = '';
} else if (status === 'OPEN') {
if (this.recorder) {
this.countdown();
}
} else if (status === 'CLOSING') {
console.log('关闭连接中');
} else if (status === 'CLOSED') {
console.log('开始录音');
}
},
toBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
},
sendMsg(text) {
this.$emit('sendMsg', text);
this.transcription = '';
},
},
created() {
// 浏览器自带的识别
/* if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = true; // 连续识别
this.recognition.interimResults = true; // 显示中间结果
this.recognition.lang = 'zh-CN'; // 设置语言为中文(简体)
// 识别结果
this.recognition.onresult = event => {
this.transcription = Array.from(event.results)
.map(result => result[0].transcript)
.join('');
};
this.recognition.onerror = event => {
console.error('识别错误:', event.error);
};
this.recognition.onend = () => {
console.log('录音停止了');
this.startVoiceStatus = false;
};
this.recognition.onstart = () => {
this.changeBtnStatus('OPEN');
};
return;
} */
this.recorder = new window.RecorderManager('/iat');
this.recorder.onStart = () => {
this.changeBtnStatus('OPEN');
};
this.recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
if (this.iatWS.readyState === this.iatWS.OPEN) {
this.iatWS.send(
JSON.stringify({
data: {
status: isLastFrame ? 2 : 1,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: this.toBase64(frameBuffer),
},
}),
);
if (isLastFrame) {
this.changeBtnStatus('CLOSING');
}
}
};
this.recorder.onStop = () => {
console.log('录音结束,停止定时器');
clearInterval(this.countdownInterval);
this.startVoiceStatus = false;
};
},
};
</script>
<template>
<a-tooltip placement="top">
<template #title>
<span style="font-size: 12px;font-weight: 300">语音输入</span>
</template>
<div
@click="startVoice"
:class="['send-btn',buttonDisabled ? 'inactive' : 'active']">
<voice
theme="outline"
size="24"
:strokeWidth="3"
class="icon"/>
</div>
</a-tooltip>
<div class="voice-box" v-if="voiceOpen">
<div class="voice-body">
<div class="close-box">
<close theme="outline" class="close-icon" size="16" @click.stop="closeVoiceOpen"/>
</div>
<div class="textarea-box">
<a-textarea
v-model:value="transcription"
:rows="2"
:bordered="false"
style="padding: 0;resize: none;"
class="custom-textarea-wrapper"
/>
<div class="text-center-box">
<div class="font-medium">{{ identifyStatus ? '识别中' : '想问什么,说来听听...' }}</div>
<div class="tip-text">点击下方语音图标可{{ startVoiceStatus ? '停止' : '开始' }}录音</div>
</div>
</div>
<div class="action-box">
<a-button type="text" :disabled="!transcription||startVoiceStatus" @click="transcription=''">清空</a-button>
<div class="start-voice-box" :style="loading?'cursor: not-allowed':''">
<voice
@click.stop="playIatVoice"
theme="outline"
size="20"
:strokeWidth="3"
class="start-voice"/>
</div>
<a-button type="text" :disabled="!transcription||startVoiceStatus" @click="sendMsg(transcription)">发送</a-button>
<div class="water-ripples" :style="{opacity:startVoiceStatus?1:0}">
<div class="circle"></div>
<div class="circle circle1"></div>
<div class="circle circle2"></div>
<div class="circle circle3"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.send-btn {
//margin-left: 1rem;
height: 2.25rem;
width: 2.25rem;
cursor: pointer;
border-radius: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 0.15s;
&:hover {
background-color: #f2f3f5;
.icon {
margin: 2px 2px 0 0;
color: #7f5df6;
}
}
}
.inactive {
.icon {
color: #b4becf;
margin: 2px 2px 0 0;
}
}
.active {
.icon {
margin: 2px 2px 0 0;
color: #7f5df6;
}
}
.voice-box {
width: 100%;
position: fixed;
bottom: 5px;
left: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
.voice-body {
position: absolute;
bottom: 0;
width: 64%;
height: 11rem;
border-radius: 1rem;
padding: 1rem 1.25rem;
font-size: .875rem;
line-height: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, .16);
box-sizing: border-box;
background-color: #fff;
border: 1px solid #e4e7ed;
color: #303133;
display: flex;
flex-direction: column;
justify-content: space-between;
.close-box {
position: absolute;
right: 1rem;
top: 1rem;
.close-icon {
color: #596780;
float: right;
cursor: pointer;
}
}
.textarea-box {
padding-right: 1.25rem;
.custom-textarea-wrapper {
max-height: 90px; /* 设置最大高度以激活滚动条 */
overflow: auto; /* 启用滚动条 */
}
/* WebKit 浏览器的滚动条样式 */
.custom-textarea-wrapper ::-webkit-scrollbar {
width: 8px; /* 设置滚动条的宽度 */
}
.custom-textarea-wrapper ::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道背景 */
}
.custom-textarea-wrapper ::-webkit-scrollbar-thumb {
background: #f2f3f5; /* 滚动条滑块的颜色 */
border-radius: 4px; /* 滚动条滑块的圆角 */
}
.custom-textarea-wrapper ::-webkit-scrollbar-thumb:hover {
background: #555; /* 滚动条滑块在悬停时的颜色 */
}
/* Firefox 的滚动条样式 */
.custom-textarea-wrapper {
scrollbar-width: thin; /* 滚动条宽度: thin/auto */
scrollbar-color: #f2f3f5 transparent; /* 滚动条颜色 (滑块 背景) */
}
.text-center-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.font-medium {
font-weight: 500;
}
.tip-text {
font-size: .75rem;
line-height: 1rem;
margin-top: 0.35rem;
color: #9DA3AF;
}
}
}
.action-box {
display: flex;
align-items: center;
justify-content: center;
position: relative;
gap: 2.5rem;
.start-voice-box {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(124 92 252);
border-radius: 50%;
cursor: pointer;
z-index: 3;
.start-voice {
color: #fff;
}
}
.start-voice-box:hover {
opacity: 0.8;
}
.water-ripples {
z-index: 2;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 28px;
height: 28px;
.circle {
width: 28px;
height: 28px;
background: rgb(124, 92, 252);
border-radius: 100%;
position: absolute;
}
.circle1 {
animation-delay: 0s;
animation-name: waterCircle;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
.circle2 {
animation-delay: 1s;
animation-name: waterCircle;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
.circle3 {
animation-delay: 2s;
animation-name: waterCircle;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes waterCircle {
0% {
transform: scale(1);
opacity: .5;
}
25% {
transform: scale(1.25);
opacity: .375;
}
50% {
transform: scale(1.5);
opacity: .25;
}
75% {
transform: scale(1.75);
opacity: .125;
}
100% {
transform: scale(2);
opacity: .05;
}
}
}
.ant-btn-text {
color: #909399;
}
.ant-btn-text:disabled {
background-color: transparent;
border-color: transparent;
color: #c8c9cc;
}
.ant-btn-text:not(:disabled):hover {
background-color: transparent;
border-color: transparent;
color: #c8c9cc;
}
}
}
}
@media (max-width: 1279px) {
.voice-body{
width: 76% !important;
}
}
@media (max-width: 1023px) {
.voice-body{
width: calc(100% - 1rem) !important;
}
}
</style>
更多推荐
所有评论(0)