修改bug

This commit is contained in:
yangjun 2025-01-13 09:29:09 +08:00
parent 21cecfa1c6
commit 378695b19a
17 changed files with 1775 additions and 134 deletions

View File

@ -55,12 +55,12 @@
<Icon icon="ant-design:aliwangwang-outlined" /> <Icon icon="ant-design:aliwangwang-outlined" />
<span >讨论区</span> <span >讨论区</span>
</a-menu-item> </a-menu-item>
<!-- <a-menu-item key="sub10"> <a-menu-item key="sub10" @click="getGzt('jxdg')">
<template #icon> <template #icon>
<SettingOutlined /> <SettingOutlined />
</template> </template>
<span @click="getGzt('jxdg')">教学大纲</span> <span>教学大纲</span>
</a-menu-item > --> </a-menu-item >
<!-- <!--
<a-menu-item key="sub9"> <a-menu-item key="sub9">
<BlockOutlined /> <BlockOutlined />

View File

@ -53,12 +53,20 @@
<span>教学单元</span> <span>教学单元</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="sub10"> <a-menu-item key="sub10" @click="getGzt('tlq')">
<template #icon> <template #icon>
<Icon icon="ant-design:aliwangwang-outlined" /> <Icon icon="ant-design:aliwangwang-outlined" />
</template> </template>
<span @click="getGzt('tlq')">讨论区</span> <span >讨论区</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="sub11" @click="getGzt('jxdg')">
<template #icon>
<Icon icon="ant-design:aliwangwang-outlined" />
</template>
<span >教学大纲</span>
</a-menu-item>
</a-menu> </a-menu>
</a-col> </a-col>
<a-col :lg="0" :xs="{ span: 24 }" style="text-align: right;"> <a-col :lg="0" :xs="{ span: 24 }" style="text-align: right;">

View File

@ -1,137 +1,260 @@
<template> <template>
<a-alert :message="`当前时间: ${tday}`" />
<a-calendar v-model:value="value" @panelChange="onPanelChange"> <div>
<template #dateCellRender="{ current }"> <!-- <MyTemplate></MyTemplate> -->
<a-row> </div>
<a-col :xs="0" :md="24">
<ul class="events" > <div>
<li v-for="item in getListData(current)" :key="item.content" @click="handleKecheng(item)" :title="item.kcmc+`-`+item.skjs"> <a-button @click="startConverting" v-if="yysbBoolean">开始转写</a-button>
<a-badge status="success" :text="item.kcmc" /> <a-button @click="endConverting" v-else>结束转写</a-button>
</li> <div v-if="resultText">
</ul> {{ resultText }}
</a-col> </div>
<a-col :xs="24" :md="0"> </div>
<a-popover title="课程信息" v-if="getSfyk(current)">
<template #content>
<ul class="events" >
<li v-for="item in getListData(current)" :key="item.content" @click="handleKecheng(item)" :title="item.kcmc+`-`+item.skjs">
<a-badge status="success" :text="item.kcmc" />
</li>
</ul>
</template>
<a-avatar style="background-color: #1d9b64;width:20px;height: 20px;line-height: 20px;"></a-avatar>
</a-popover>
</a-col>
</a-row>
</template> <a-alert :message="`当前时间: ${tday}`" />
<!-- 去掉头部年月切换 --> <a-calendar v-model:value="value" @panelChange="onPanelChange">
<template #headerRender="{ current }"> <template #dateCellRender="{ current }">
</template> <a-row>
</a-calendar> <a-col :xs="0" :md="24">
</template> <ul class="events" >
<li v-for="item in getListData(current)" :key="item.content" @click="handleKecheng(item)" :title="item.kcmc+`-`+item.skjs">
<a-badge status="success" :text="item.kcmc" />
</li>
</ul>
</a-col>
<a-col :xs="24" :md="0">
<a-popover title="课程信息" v-if="getSfyk(current)">
<template #content>
<ul class="events" >
<li v-for="item in getListData(current)" :key="item.content" @click="handleKecheng(item)" :title="item.kcmc+`-`+item.skjs">
<a-badge status="success" :text="item.kcmc" />
</li>
</ul>
</template>
<a-avatar style="background-color: #1d9b64;width:20px;height: 20px;line-height: 20px;"></a-avatar>
</a-popover>
</a-col>
</a-row>
<script lang="ts" setup>
import { defineComponent, ref, onMounted } from 'vue';
import { Dayjs } from 'dayjs';
import { defHttp } from '/@/utils/http/axios';
import { dateFormat } from '/@/utils/common/compUtils';
const value = ref<Dayjs>(); </template>
const tday = ref<string>(''); <!-- 去掉头部年月切换 -->
const studentKclist = ref<any>([]); <template #headerRender="{ current }">
const getListData = (value: Dayjs) => { </template>
//yyyy-MM-dd </a-calendar>
const formattedDate = value.format('YYYY-MM-DD'); </template>
if (formattedDate.substring(0, 7) == tday.value) {
return studentKclist.value.filter(item => item.skrq === formattedDate); <script lang="ts" setup>
} else { import { defineComponent, ref, onMounted } from 'vue';
return []; import { Dayjs } from 'dayjs';
import { defHttp } from '/@/utils/http/axios';
import { dateFormat } from '/@/utils/common/compUtils';
import { SpeechRecognizer } from '/@/views/site/studentWdkc/yysbUtils/app/speechrecognizer';
import WebAudioSpeechRecognizer from '/@/views/site/studentWdkc/yysbUtils/app/webaudiospeechrecognizer';
import * as ASR from '/@/views/site/studentWdkc/yysbUtils/examples/trtc/asr.esm';
let config = {
secretKey: 'QcbMitrb7QVMyNaZJp7E7mESSFrGUWKz',
secretId: 'AKIDJfpnZ9EF5i7II4cw3xfARVMyNYeljU3Q',
appId: 1255881128,
}
const params = {
signCallback: signCallback, // 使
//
secretid: config.secretId,
secretkey: config.secretKey,
appid: config.appId,
//
// token: config.token,
//
engine_model_type : '16k_zh', // WebRecorder16k engineModelType 16k '16k_zh'
//
// voice_format : 1,
// hotword_id : '08003a00000000000000000000000000',
// needvad: 1,
// filter_dirty: 1,
// filter_modal: 2,
// filter_punc: 0,
// convert_num_mode : 1,
// word_info: 2
} }
};
// const resultText = ref<string>('');
const getSfyk = (value: Dayjs) => { const yysbBoolean = ref<boolean>(true);
//yyyy-MM-dd
const formattedDate = value.format('YYYY-MM-DD');
if (formattedDate.substring(0, 7) == tday.value) { /** 获取签名 start */
var listSfyk = studentKclist.value.filter(item => item.skrq === formattedDate)
if(listSfyk.length>0){ function toUint8Array(wordArray) {
return true; // Shortcuts
}else{ const words = wordArray.words;
const sigBytes = wordArray.sigBytes;
// Convert
const u8 = new Uint8Array(sigBytes);
for (let i = 0; i < sigBytes; i++) {
u8[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
return u8;
}
function Uint8ArrayToString(fileData){
let dataString = '';
for (let i = 0; i < fileData.length; i++) {
dataString += String.fromCharCode(fileData[i]);
}
return dataString;
}
//
function signCallback(signStr) {
const secretKey = config.secretKey;
const hash = window.CryptoJSTest.HmacSHA1(signStr, secretKey);
const bytes = Uint8ArrayToString(toUint8Array(hash));
return window.btoa(bytes);
}
/** 获取签名 end */
let webAudioSpeechRecognizer = null;
function startConverting() {
var t = ASR.guid();
console.log(t);
webAudioSpeechRecognizer = new WebAudioSpeechRecognizer(params);
console.log("🚀 ~ 1111 ~ con:", webAudioSpeechRecognizer)
//
webAudioSpeechRecognizer.OnRecognitionStart = (res) => {
console.log('开始识别', res);
};
//
webAudioSpeechRecognizer.OnRecognitionResultChange = (res) => {
console.log('识别变化时', res);
const currentText = res.result.voice_text_str;
console.log("🚀 ~ startConverting ~ currentText:", currentText)
resultText.value = currentText;
};
//
webAudioSpeechRecognizer.OnRecognitionComplete = (res) => {
console.log('识别结束', res);
};
webAudioSpeechRecognizer.start();
yysbBoolean.value = false;
}
function endConverting(){
webAudioSpeechRecognizer.stop();
//
webAudioSpeechRecognizer.OnRecognitionComplete = (res) => {
console.log('识别结束', res);
};
yysbBoolean.value = true;
webAudioSpeechRecognizer = null;
}
const value = ref<Dayjs>();
const tday = ref<string>('');
const studentKclist = ref<any>([]);
const getListData = (value: Dayjs) => {
//yyyy-MM-dd
const formattedDate = value.format('YYYY-MM-DD');
if (formattedDate.substring(0, 7) == tday.value) {
return studentKclist.value.filter(item => item.skrq === formattedDate);
} else {
return [];
}
};
//
const getSfyk = (value: Dayjs) => {
//yyyy-MM-dd
const formattedDate = value.format('YYYY-MM-DD');
if (formattedDate.substring(0, 7) == tday.value) {
var listSfyk = studentKclist.value.filter(item => item.skrq === formattedDate)
if(listSfyk.length>0){
return true;
}else{
return false;
}
} else {
return false; return false;
} }
} else { };
return false;
const onPanelChange = (value: Dayjs) => {
tday.value = dateFormat(value, "yyyy-MM");
getRiliList(tday.value)
};
//
function handleKecheng(record) {
var jgh = record.jgh.split(",")[0];
// defHttp.post({ url: '/zyDbtx/zyDbtx/deleteByRwbhCreate', params: { rwbh: record.rwbh, fbr: jgh } }).then((res) => {
// loaddata();
// });
var url = '/stuzy/StudentGonggaoList?rwbh=' + record.rwbh + '&xqxn=' + record.xnxq + "&teano=" + record.jgh;
window.open(url, '_blank');
} }
}; //
function getRiliList(skrq){
const onPanelChange = (value: Dayjs) => { studentKclist.value = []
tday.value = dateFormat(value, "yyyy-MM"); defHttp.get({ url: '/ktgl/kcKetangbiao/getStudentRiliKclist' ,params:{skrq}}).then((res) => {
getRiliList(tday.value) res.forEach(element => {
}; studentKclist.value.push(element)
// });
function handleKecheng(record) {
var jgh = record.jgh.split(",")[0];
// defHttp.post({ url: '/zyDbtx/zyDbtx/deleteByRwbhCreate', params: { rwbh: record.rwbh, fbr: jgh } }).then((res) => {
// loaddata();
// });
var url = '/stuzy/StudentGonggaoList?rwbh=' + record.rwbh + '&xqxn=' + record.xnxq + "&teano=" + record.jgh;
window.open(url, '_blank');
}
//
function getRiliList(skrq){
studentKclist.value = []
defHttp.get({ url: '/ktgl/kcKetangbiao/getStudentRiliKclist' ,params:{skrq}}).then((res) => {
res.forEach(element => {
studentKclist.value.push(element)
}); });
}
//
onMounted(() => {
tday.value = dateFormat(new Date(), "yyyy-MM");
getRiliList(tday.value)
}); });
}
// </script>
onMounted(() => { <style scoped>
tday.value = dateFormat(new Date(), "yyyy-MM"); .events {
getRiliList(tday.value) list-style: none;
}); margin: 0;
padding: 0;
}
</script> .events li {
<style scoped> border-radius: 2px;
.events { cursor: pointer;
list-style: none; /* 鼠标指针变为小手 */
margin: 0; transition: background-color 0.3s;
padding: 0; /* 平滑的背景颜色变化 */
} }
.events li { .events li:hover {
border-radius: 2px; background-color: #a3a3a3;
cursor: pointer; /* 深灰色背景 */
/* 鼠标指针变为小手 */ color: white;
transition: background-color 0.3s; /* 如果需要的话,可以改变文字颜色以保证可读性 */
/* 平滑的背景颜色变化 */ }
}
.events li:hover { .events .ant-badge-status {
background-color: #a3a3a3; overflow: hidden;
/* 深灰色背景 */ white-space: nowrap;
color: white; width: 100%;
/* 如果需要的话,可以改变文字颜色以保证可读性 */ text-overflow: ellipsis;
} font-size: 12px;
}
.events .ant-badge-status { .notes-month {
overflow: hidden; text-align: center;
white-space: nowrap; font-size: 28px;
width: 100%; }
text-overflow: ellipsis;
font-size: 12px;
}
.notes-month { .notes-month section {
text-align: center; font-size: 28px;
font-size: 28px; }
} </style>
.notes-month section {
font-size: 28px;
}
</style>

View File

@ -0,0 +1,267 @@
import '../examples/lib/cryptojs.js';
// 识别需要过滤的参数
const needFiltrationParams = ['appid', 'secretkey', 'signCallback', 'echoCancellation'];
function formatSignString(query, params){
let strParam = "";
let signStr = "asr.cloud.tencent.com/asr/v2/";
if(query['appid']){
signStr += query['appid'];
}
const keys = Object.keys(params);
keys.sort();
for (let i = 0, len = keys.length; i < len; i++) {
strParam += `&${keys[i]}=${params[keys[i]]}`;
}
return `${signStr}?${strParam.slice(1)}`;
}
async function createQuery(query){
let params = {};
const time = new Date().getTime();
async function getServerTime(){
return new Promise((resolve, reject)=>{
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", 'https://asr.cloud.tencent.com/server_time', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(xhr.responseText);
}
}
} catch (error) {
reject(error);
}
})
}
const serverTime = await getServerTime();
params['secretid'] = query.secretid || '';
params['engine_model_type'] = query.engine_model_type || '16k_zh';
params['timestamp'] = parseInt(serverTime) || Math.round(time / 1000);
params['expired'] = Math.round(time / 1000) + 24 * 60 * 60;
params['nonce'] = Math.round(time / 100000);
params['voice_id'] = guid();
params['voice_format'] = query.voice_format || 1;
const tempQuery = { ...query };
for (let i = 0, len = needFiltrationParams.length; i < len; i++) {
if (tempQuery.hasOwnProperty(needFiltrationParams[i])) {
delete tempQuery[needFiltrationParams[i]];
}
}
params = {
...tempQuery,
...params,
};
return params;
}
export const guid = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
// 获取签名原文
async function getUrl(self, params) {
if (!params.appid || !params.secretid) {
self.isLog && console.log(self.requestId, '请确认是否填入账号信息', TAG);
self.OnError('请确认是否填入账号信息');
return false;
}
const urlQuery = await createQuery(params);
const queryStr = formatSignString(params, urlQuery);
let signature = '';
if (params.signCallback) {
signature = params.signCallback(queryStr);
} else {
signature = signCallback(params.secretkey, queryStr);
}
return `wss://${queryStr}&signature=${encodeURIComponent(signature)}`;
}
/** 获取签名 start */
function toUint8Array(wordArray) {
// Shortcuts
const words = wordArray.words;
const sigBytes = wordArray.sigBytes;
// Convert
const u8 = new Uint8Array(sigBytes);
for (let i = 0; i < sigBytes; i++) {
u8[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
return u8;
}
function Uint8ArrayToString(fileData){
let dataString = '';
for (let i = 0; i < fileData.length; i++) {
dataString += String.fromCharCode(fileData[i]);
}
return dataString;
}
// 签名函数示例
function signCallback(secretKey, signStr) {
const hash = window.CryptoJSTest.HmacSHA1(signStr, secretKey);
const bytes = Uint8ArrayToString(toUint8Array(hash));
return window.btoa(bytes);
}
/** 获取签名 end */
const TAG = 'SpeechRecognizer';
export class SpeechRecognizer {
constructor(params, requestId, isLog) {
this.socket = null;
this.isSignSuccess = false; // 是否鉴权成功
this.isSentenceBegin = false; // 是否一句话开始
this.query = {
...params
};
this.isRecognizeComplete = false; // 当前是否识别结束
this.requestId = requestId;
this.isLog = isLog;
this.sendCount = 0;
this.getMessageList = [];
}
// 暂停识别,关闭连接
stop() {
if (this.socket && this.socket.readyState === 1) {
this.socket.send(JSON.stringify({type: 'end'}));
this.isRecognizeComplete = true;
} else {
// this.OnError({ code : 6003, message: '连接未建立或连接已关闭' });
if (this.socket && this.socket.readyState === 1) {
this.socket.close();
}
}
}
// 建立websocket链接 data 为用户收集的音频数据
async start(){
this.socket = null;
this.getMessageList = [];
const url = await getUrl(this, this.query);
if (!url) {
this.isLog && console.log(this.requestId, '鉴权失败', TAG);
this.OnError('鉴权失败');
return
}
this.isLog && console.log(this.requestId, 'get ws url', url, TAG);
if ('WebSocket' in window) {
this.socket = new WebSocket(url);
} else if ('MozWebSocket' in window) {
this.socket = new MozWebSocket(url);
} else {
this.isLog && console.log(this.requestId, '浏览器不支持WebSocket', TAG);
this.OnError('浏览器不支持WebSocket');
return
}
this.socket.onopen = (e) => { // 连接建立时触发
this.isLog && console.log(this.requestId, '连接建立', e, TAG);
};
this.socket.onmessage = async (e) => { // 连接建立时触发
try {
this.getMessageList.push(JSON.stringify(e));
const response = JSON.parse(e.data);
if (response.code !== 0) {
if (this.socket.readyState === 1) {
this.socket.close();
}
this.isLog && console.log(this.requestId, JSON.stringify(response), TAG);
this.OnError(response);
} else {
if (!this.isSignSuccess) {
this.OnRecognitionStart(response);
this.isSignSuccess = true;
}
if (response.final === 1) {
this.OnRecognitionComplete(response);
return;
}
if (response.result) {
if (response.result.slice_type === 0) {
this.OnSentenceBegin(response);
this.isSentenceBegin = true;
} else if (response.result.slice_type === 2) {
if (!this.isSentenceBegin) {
this.OnSentenceBegin(response);
}
this.OnSentenceEnd(response);
} else {
this.OnRecognitionResultChange(response);
}
}
this.isLog && console.log(this.requestId, response, TAG);
}
} catch (e) {
this.isLog && console.log(this.requestId, 'socket.onmessage catch error', JSON.stringify(e), TAG);
}
};
this.socket.onerror = (e) => { // 通信发生错误时触发
this.isLog && console.log(this.requestId, 'socket error callback', e, TAG);
this.socket.close();
this.OnError(e);
}
this.socket.onclose = (event) => {
try {
if (!this.isRecognizeComplete) {
this.isLog && console.log(this.requestId, 'socket is close and error', JSON.stringify(event), TAG);
this.OnError(event);
}
} catch (e) {
this.isLog && console.log(this.requestId, 'socket is onclose catch' + this.sendCount, JSON.stringify(e), TAG);
}
}
}
close() {
this.socket && this.socket.readyState === 1 && this.socket.close(1000);
}
// 发送数据
write(data) {
try {
if (!this.socket || String(this.socket.readyState) !== '1') {
setTimeout(() => {
if (this.socket && this.socket.readyState === 1) {
this.socket.send(data);
}
}, 100);
return false;
}
this.sendCount += 1;
this.socket.send(data);
} catch (e) {
this.isLog && console.log(this.requestId , '发送数据 error catch', e, TAG);
}
};
// 开始识别的时候
OnRecognitionStart(res) {
}
// 一句话开始的时候
OnSentenceBegin(res) {
}
// 识别结果发生变化的时候
OnRecognitionResultChange() {
}
// 一句话结束的时候
OnSentenceEnd() {
}
// 识别结束的时候
OnRecognitionComplete() {
}
// 识别失败
OnError() {
}
}
typeof window !== 'undefined' && (window.SpeechRecognizer = SpeechRecognizer);

View File

@ -0,0 +1,110 @@
import WebRecorder from "./webrecorder.js";
import { SpeechRecognizer, guid } from "./speechrecognizer.js";
export default class WebAudioSpeechRecognizer {
constructor(params, isLog) {
this.params = params;
this.recorder = null;
this.speechRecognizer = null;
this.isCanSendData = false;
this.isNormalEndStop = false;
this.audioData = [];
this.isLog = isLog;
this.requestId = null;
}
start() {
try {
this.isLog && console.log('start function is click');
this.requestId = guid();
this.recorder = new WebRecorder(this.requestId, this.params, this.isLog);
this.recorder.OnReceivedData = (data) => {
if (this.isCanSendData) {
this.speechRecognizer && this.speechRecognizer.write(data);
}
};
// 录音失败时
this.recorder.OnError = (err) => {
this.speechRecognizer && this.speechRecognizer.close();
this.stop();
this.OnError(err);
}
this.recorder.OnStop = (res) => {
if (this.speechRecognizer) {
this.speechRecognizer.stop();
// this.speechRecognizer = null;
}
this.OnRecorderStop(res);
}
this.recorder.start();
if (!this.speechRecognizer) {
this.speechRecognizer = new SpeechRecognizer(this.params, this.requestId, this.isLog);
}
// 开始识别
this.speechRecognizer.OnRecognitionStart = (res) => {
if (this.recorder) { // 录音正常
this.OnRecognitionStart(res);
this.isCanSendData = true;
} else {
this.speechRecognizer && this.speechRecognizer.close();
}
};
// 一句话开始
this.speechRecognizer.OnSentenceBegin = (res) => {
this.OnSentenceBegin(res);
};
// 识别变化时
this.speechRecognizer.OnRecognitionResultChange = (res) => {
this.OnRecognitionResultChange(res);
};
// 一句话结束
this.speechRecognizer.OnSentenceEnd = (res) => {
this.OnSentenceEnd(res);
};
// 识别结束
this.speechRecognizer.OnRecognitionComplete = (res) => {
this.OnRecognitionComplete(res);
this.isCanSendData = false;
this.isNormalEndStop = true;
};
// 识别错误
this.speechRecognizer.OnError = (res) => {
if (this.speechRecognizer && !this.isNormalEndStop) {
this.OnError(res);
}
this.speechRecognizer = null;
this.recorder && this.recorder.stop();
this.isCanSendData = false;
};
// 建立连接
this.speechRecognizer.start();
} catch (e) {
console.log(e);
}
}
stop() {
this.isLog && console.log('stop function is click');
if (this.recorder) {
this.recorder.stop();
}
}
destroyStream() {
this.isLog && console.log('destroyStream function is click', this.recorder);
if (this.recorder) {
this.recorder.destroyStream();
}
}
// 开始识别的时候
OnRecognitionStart(res) {}
// 一句话开始的时候
OnSentenceBegin(res) {}
// 识别结果发生变化的时候
OnRecognitionResultChange(res) {}
// 一句话结束的时候
OnSentenceEnd(res) {}
// 识别结束的时候
OnRecognitionComplete(res) {}
// 识别失败
OnError() {}
OnRecorderStop() {}
};
typeof window !== 'undefined' && (window.WebAudioSpeechRecognizer = WebAudioSpeechRecognizer);

View File

@ -0,0 +1,286 @@
export function to16BitPCM(input) {
const dataLength = input.length * (16 / 8);
const dataBuffer = new ArrayBuffer(dataLength);
const dataView = new DataView(dataBuffer);
let offset = 0;
for (let i = 0; i < input.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, input[i]));
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return dataView;
}
export function to16kHz(audioData, sampleRate= 44100) {
const data = new Float32Array(audioData);
const fitCount = Math.round(data.length * (16000 / sampleRate));
const newData = new Float32Array(fitCount);
const springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];
for (let i = 1; i < fitCount - 1; i++) {
const tmp = i * springFactor;
const before = Math.floor(tmp).toFixed();
const after = Math.ceil(tmp).toFixed();
const atPoint = tmp - before;
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
newData[fitCount - 1] = data[data.length - 1];
return newData;
}
const audioWorkletCode = `
class MyProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.audioData = [];
this.sampleCount = 0;
this.bitCount = 0;
this.preTime = 0;
}
process(inputs) {
// 去处理音频数据
// eslint-disable-next-line no-undef
if (inputs[0][0]) {
const output = ${to16kHz}(inputs[0][0], sampleRate);
this.sampleCount += 1;
const audioData = ${to16BitPCM}(output);
this.bitCount += 1;
const data = [...new Int8Array(audioData.buffer)];
this.audioData = this.audioData.concat(data);
if (new Date().getTime() - this.preTime > 100) {
this.port.postMessage({
audioData: new Int8Array(this.audioData),
sampleCount: this.sampleCount,
bitCount: this.bitCount
});
this.preTime = new Date().getTime();
this.audioData = [];
}
return true;
}
}
}
registerProcessor('my-processor', MyProcessor);
`;
const TAG = 'WebRecorder';
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia || navigator.msGetUserMedia;
export default class WebRecorder {
constructor(requestId, params, isLog) {
this.audioData = [];
this.allAudioData = [];
this.stream = null;
this.audioContext = null;
this.requestId = requestId;
this.frameTime = [];
this.frameCount = 0;
this.sampleCount = 0;
this.bitCount = 0;
this.mediaStreamSource = null;
this.isLog = isLog;
this.params = params;
}
static isSupportMediaDevicesMedia() {
return !!(navigator.getUserMedia || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
}
static isSupportUserMediaMedia() {
return !!navigator.getUserMedia;
}
static isSupportAudioContext() {
return typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined';
}
static isSupportMediaStreamSource(requestId, audioContext) {
return typeof audioContext.createMediaStreamSource === 'function';
}
static isSupportAudioWorklet(audioContext) {
return audioContext.audioWorklet && typeof audioContext.audioWorklet.addModule === 'function'
&& typeof AudioWorkletNode !== 'undefined';
}
static isSupportCreateScriptProcessor(requestId, audioContext) {
return typeof audioContext.createScriptProcessor === 'function';
}
start() {
this.frameTime = [];
this.frameCount = 0;
this.allAudioData = [];
this.audioData = [];
this.sampleCount = 0;
this.bitCount = 0;
this.getDataCount = 0;
this.audioContext = null;
this.mediaStreamSource = null;
this.stream = null;
this.preTime = 0;
try {
if (WebRecorder.isSupportAudioContext()) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
} else {
this.isLog && console.log(this.requestId, '浏览器不支持AudioContext', TAG);
this.OnError('浏览器不支持AudioContext');
}
} catch (e) {
this.isLog && console.log(this.requestId, '浏览器不支持webAudioApi相关接口', e, TAG);
this.OnError('浏览器不支持webAudioApi相关接口');
}
this.getUserMedia(this.requestId, this.getAudioSuccess, this.getAudioFail);
}
stop() {
if (!(/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent))){
this.audioContext && this.audioContext.suspend();
}
this.audioContext && this.audioContext.suspend();
this.isLog && console.log(this.requestId, `webRecorder stop ${this.sampleCount}/${this.bitCount}/${this.getDataCount}` , JSON.stringify(this.frameTime), TAG);
this.OnStop(this.allAudioData);
}
destroyStream() {
// 关闭通道
if (this.stream) {
this.stream.getTracks().map((val) => {
val.stop();
});
this.stream = null;
}
}
async getUserMedia(requestId, getStreamAudioSuccess, getStreamAudioFail) {
let audioOption = {
echoCancellation: true,
};
if (this.params && String(this.params.echoCancellation) === 'false') { // 关闭回声消除
audioOption = {
echoCancellation: false,
};
}
const mediaOption = {
audio: audioOption,
video: false,
};
// 获取用户的麦克风
if (WebRecorder.isSupportMediaDevicesMedia()) {
navigator.mediaDevices
.getUserMedia(mediaOption)
.then(stream => {
this.stream = stream;
getStreamAudioSuccess.call(this, requestId, stream);
})
.catch(e => {
getStreamAudioFail.call(this, requestId, e);
});
} else if (WebRecorder.isSupportUserMediaMedia()) {
navigator.getUserMedia(mediaOption,
stream => {
this.stream = stream;
getStreamAudioSuccess.call(this, requestId, stream);
},
function(err) {
getStreamAudioFail.call(this, requestId, err);
}
);
} else {
if (navigator.userAgent.toLowerCase().match(/chrome/) && location.origin.indexOf('https://') < 0) {
this.isLog && console.log(this.requestId, 'chrome下获取浏览器录音功能因为安全性问题需要在localhost或127.0.0.1或https下才能获取权限', TAG);
this.OnError('chrome下获取浏览器录音功能因为安全性问题需要在localhost或127.0.0.1或https下才能获取权限');
} else {
this.isLog && console.log(this.requestId, '无法获取浏览器录音功能请升级浏览器或使用chrome', TAG);
this.OnError('无法获取浏览器录音功能请升级浏览器或使用chrome');
}
this.audioContext && this.audioContext.close();
}
}
async getAudioSuccess(requestId, stream) {
if (!this.audioContext) {
return false;
}
if (this.mediaStreamSource) {
this.mediaStreamSource.disconnect();
this.mediaStreamSource = null;
}
this.audioTrack = stream.getAudioTracks()[0];
const mediaStream = new MediaStream();
mediaStream.addTrack(this.audioTrack);
this.mediaStreamSource = this.audioContext.createMediaStreamSource(mediaStream);
if (WebRecorder.isSupportMediaStreamSource(requestId, this.audioContext)) {
if (WebRecorder.isSupportAudioWorklet(this.audioContext)) { // 不支持 AudioWorklet 降级
this.audioWorkletNodeDealAudioData(this.mediaStreamSource, requestId);
} else {
this.scriptNodeDealAudioData(this.mediaStreamSource, requestId);
}
} else { // 不支持 MediaStreamSource
this.isLog && console.log(this.requestId, '不支持MediaStreamSource', TAG);
this.OnError('不支持MediaStreamSource');
}
}
getAudioFail(requestId, err) {
if (err && err.err && err.err.name === 'NotAllowedError') {
this.isLog && console.log(requestId,'授权失败', JSON.stringify(err.err), TAG);
}
this.isLog && console.log(this.requestId, 'getAudioFail', JSON.stringify(err), TAG);
this.OnError(err);
this.stop();
}
scriptNodeDealAudioData(mediaStreamSource, requestId) {
if (WebRecorder.isSupportCreateScriptProcessor(requestId, this.audioContext)) {
// 创建一个音频分析对象采样的缓冲区大小为0自动适配输入和输出都是单声道
const scriptProcessor = this.audioContext.createScriptProcessor(1024, 1, 1);
// 连接
this.mediaStreamSource && this.mediaStreamSource.connect(scriptProcessor);
scriptProcessor && scriptProcessor.connect(this.audioContext.destination);
scriptProcessor.onaudioprocess = (e) => {
this.getDataCount += 1;
// 去处理音频数据
const inputData = e.inputBuffer.getChannelData(0);
const output = to16kHz(inputData, this.audioContext.sampleRate);
const audioData = to16BitPCM(output);
this.audioData.push(...new Int8Array(audioData.buffer));
this.allAudioData.push(...new Int8Array(audioData.buffer));
if (new Date().getTime() - this.preTime > 100) {
this.frameTime.push(`${Date.now()}-${this.frameCount}`);
this.frameCount += 1;
this.preTime = new Date().getTime();
const audioDataArray = new Int8Array(this.audioData);
this.OnReceivedData(audioDataArray);
this.audioData = [];
this.sampleCount += 1;
this.bitCount += 1;
}
};
} else { // 不支持
this.isLog && console.log(this.requestId, '不支持createScriptProcessor', TAG);
}
}
async audioWorkletNodeDealAudioData(mediaStreamSource, requestId) {
try {
const audioWorkletBlobURL = window.URL.createObjectURL(new Blob([audioWorkletCode], { type: 'text/javascript' }));
await this.audioContext.audioWorklet.addModule(audioWorkletBlobURL);
const myNode = new AudioWorkletNode(this.audioContext, 'my-processor', { numberOfInputs: 1, numberOfOutputs: 1, channelCount: 1 });
myNode.onprocessorerror = (event) => {
// 降级
this.scriptNodeDealAudioData(mediaStreamSource, this.requestId);
return false;
}
myNode.port.onmessage = (event) => {
this.frameTime.push(`${Date.now()}-${this.frameCount}`);
this.OnReceivedData(event.data.audioData);
this.frameCount += 1;
this.allAudioData.push(...event.data.audioData);
this.sampleCount = event.data.sampleCount;
this.bitCount = event.data.bitCount;
};
myNode.port.onmessageerror = (event) => {
// 降级
this.scriptNodeDealAudioData(mediaStreamSource, requestId);
return false;
}
mediaStreamSource &&mediaStreamSource.connect(myNode).connect(this.audioContext.destination);
} catch (e) {
this.isLog && console.log(this.requestId, 'audioWorkletNodeDealAudioData catch error', JSON.stringify(e), TAG);
this.OnError(e);
}
}
// 获取音频数据
OnReceivedData(data) {}
OnError(res) {}
OnStop(res) {}
}
typeof window !== 'undefined' && (window.WebRecorder = WebRecorder);

View File

@ -0,0 +1,31 @@
/** 获取签名 start */
function toUint8Array(wordArray) {
// Shortcuts
const words = wordArray.words;
const sigBytes = wordArray.sigBytes;
// Convert
const u8 = new Uint8Array(sigBytes);
for (let i = 0; i < sigBytes; i++) {
u8[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
return u8;
}
function Uint8ArrayToString(fileData){
let dataString = '';
for (let i = 0; i < fileData.length; i++) {
dataString += String.fromCharCode(fileData[i]);
}
return dataString;
}
// 签名函数示例
function signCallback(signStr) {
const secretKey = config.secretKey;
const hash = window.CryptoJSTest.HmacSHA1(signStr, secretKey);
const bytes = Uint8ArrayToString(toUint8Array(hash));
return window.btoa(bytes);
}
/** 获取签名 end */

View File

@ -0,0 +1,7 @@
let config = {
secretKey: 'QcbMitrb7QVMyNaZJp7E7mESSFrGUWKz',
secretId: 'AKIDJfpnZ9EF5i7II4cw3xfARVMyNYeljU3Q',
appId: 1255881128,
}
window.config = config

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>测试 demo</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="content-box">
<div class="content">
<div class="content-item">
<button class="button" id="start">开始识别</button>
<div class="connecting display-none" id="connecting">建立连接中...</div>
<span class="recognizing display-none" id="recognizing">识别中...</span>
<button class="button end-btn" id="end">结束识别</button>
</div>
<div class="content-item">
<div id="recognizeText" class="recognize-content"></div>
</div>
</div>
</div>
<script src="lib/jquery.js"></script>
<script src="config.js"></script>
<script src="asrauthentication.js"></script>
<script src="../dist/speechrecognizer.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>

View File

@ -0,0 +1,51 @@
.content-box .content{
width: 100%;
overflow: hidden;
margin: 100px auto;
}
.content .content-item {
width: 50%;
float: left;
text-align: center;
}
.left-content {
width: 100%;
height: 300px;
}
.connecting {
color: #999;
margin: 50px 0;
}
.recognizing {
color: #999;
margin: 0 10px;
}
.content .content-item .button {
background: #187cff;
border: 1px solid #478eea;
color: #fff;
text-align: center;
border-radius: 3px;
cursor: pointer;
width: 160px;
height: 40px;
margin: 50px 0;
}
.button.end-btn {
display: none;
}
.recognize-content {
height: 200px;
width: 360px;
background-color: #eee;
border: 1px solid #eee;
border-radius: 5px;
margin: auto;
text-align: left;
padding: 10px;
box-sizing: border-box;
overflow: scroll;
}
.display-none {
display: none;
}

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>测试 demo</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="content-box">
<div class="content">
<div class="content-item left-content">
<button class="button" id="start">开始识别</button>
<div class="connecting display-none" id="connecting">建立连接中...</div>
<span class="recognizing display-none" id="recognizing">识别中...</span>
<button class="button end-btn" id="end">结束识别</button>
</div>
<div class="content-item">
<div id="recognizeText" class="recognize-content"></div>
</div>
</div>
</div>
<script src="lib/jquery.js"></script>
<script src="../dist/speechrecognizer.js"></script>
<script src="config.js"></script>
<script src="asrauthentication.js"></script>
<script type="module" src="index.js"></script>
</body>
</html>

View File

@ -0,0 +1,77 @@
let webAudioSpeechRecognizer;
let isCanStop;
$(function () {
const params = {
signCallback: signCallback, // 鉴权函数,若直接使用默认鉴权函数。可不传此参数
// 用户参数
secretid: config.secretId,
secretkey: config.secretKey,
appid: config.appId,
// 临时密钥参数,非必填
// token: config.token,
// 实时识别接口参数
engine_model_type : '16k_zh', // 因为内置WebRecorder采样16k的数据所以参数 engineModelType 需要选择16k的引擎为 '16k_zh'
// 以下为非必填参数,可跟据业务自行修改
// voice_format : 1,
// hotword_id : '08003a00000000000000000000000000',
// needvad: 1,
// filter_dirty: 1,
// filter_modal: 2,
// filter_punc: 0,
// convert_num_mode : 1,
// word_info: 2
}
$('#start').on('click', function () {
webAudioSpeechRecognizer = new WebAudioSpeechRecognizer(params);
const areaDom = $('#recognizeText');
areaDom.text('');
let resultText = '';
$(this).hide();
$('#connecting').show();
// 开始识别
webAudioSpeechRecognizer.OnRecognitionStart = (res) => {
console.log('开始识别', res);
};
// 一句话开始
webAudioSpeechRecognizer.OnSentenceBegin = (res) => {
console.log('一句话开始', res);
isCanStop = true;
$('#end').show();
$('#recognizing').show();
$('#connecting').hide();
};
// 识别变化时
webAudioSpeechRecognizer.OnRecognitionResultChange = (res) => {
console.log('识别变化时', res);
const currentText = `${resultText}${res.result.voice_text_str}`;
areaDom.text(currentText);
};
// 一句话结束
webAudioSpeechRecognizer.OnSentenceEnd = (res) => {
console.log('一句话结束', res);
resultText += res.result.voice_text_str;
areaDom.text(resultText);
};
// 识别结束
webAudioSpeechRecognizer.OnRecognitionComplete = (res) => {
console.log('识别结束', res);
};
// 识别错误
webAudioSpeechRecognizer.OnError = (res) => {
console.log('识别失败', res)
$('#end').hide();
$('#recognizing').hide();
$('#start').show();
$('#connecting').hide();
};
webAudioSpeechRecognizer.start();
});
$('#end').on('click', function () {
$(this).hide();
$('#recognizing').hide();
$('#start').show();
if (isCanStop) {
webAudioSpeechRecognizer.stop();
}
});
});

View File

@ -0,0 +1,272 @@
/*
* [js-sha1]
*
* @version 0.6.0
* @copyright H, J-C 2018-9-28
* @license MIT
*/
var CryptoJS = CryptoJS || function (g, l) {
var e = {}, d = e.lib = {}, m = function () { }, k = d.Base = {
extend: function (a) {
m.prototype = this;
var c = new m;
a && c.mixIn(a);
c.hasOwnProperty("init") || (c.init = function () {
c.$super.init.apply(this, arguments)
});
c.init.prototype = c;
c.$super = this;
return c
},
create: function () {
var a = this.extend();
a.init.apply(a, arguments);
return a
},
init: function () { },
mixIn: function (a) {
for (var c in a) a.hasOwnProperty(c) && (this[c] = a[c]);
a.hasOwnProperty("toString") && (this.toString = a.toString)
},
clone: function () {
return this.init.prototype.extend(this)
}
},
p = d.WordArray = k.extend({
init: function (a, c) {
a = this.words = a || [];
this.sigBytes = c != l ? c : 4 * a.length
},
toString: function (a) {
return (a || n).stringify(this)
},
concat: function (a) {
var c = this.words,
q = a.words,
f = this.sigBytes;
a = a.sigBytes;
this.clamp();
if (f % 4)
for (var b = 0; b < a; b++) c[f + b >>> 2] |= (q[b >>> 2] >>> 24 - 8 * (b % 4) & 255) << 24 - 8 * ((f + b) % 4);
else if (65535 < q.length)
for (b = 0; b < a; b += 4) c[f + b >>> 2] = q[b >>> 2];
else c.push.apply(c, q);
this.sigBytes += a;
return this
},
clamp: function () {
var a = this.words,
c = this.sigBytes;
a[c >>> 2] &= 4294967295 << 32 - 8 * (c % 4);
a.length = g.ceil(c / 4)
},
clone: function () {
var a = k.clone.call(this);
a.words = this.words.slice(0);
return a
},
random: function (a) {
for (var c = [], b = 0; b < a; b += 4) c.push(4294967296 * g.random() | 0);
return new p.init(c, a)
}
}),
b = e.enc = {}, n = b.Hex = {
stringify: function (a) {
var c = a.words;
a = a.sigBytes;
for (var b = [], f = 0; f < a; f++) {
var d = c[f >>> 2] >>> 24 - 8 * (f % 4) & 255;
b.push((d >>> 4).toString(16));
b.push((d & 15).toString(16))
}
return b.join("")
},
parse: function (a) {
for (var c = a.length, b = [], f = 0; f < c; f += 2) b[f >>> 3] |= parseInt(a.substr(f, 2), 16) << 24 - 4 * (f % 8);
return new p.init(b, c / 2)
}
}, j = b.Latin1 = {
stringify: function (a) {
var c = a.words;
a = a.sigBytes;
for (var b = [], f = 0; f < a; f++) b.push(String.fromCharCode(c[f >>> 2] >>> 24 - 8 * (f % 4) & 255));
return b.join("")
},
parse: function (a) {
for (var c = a.length, b = [], f = 0; f < c; f++) b[f >>> 2] |= (a.charCodeAt(f) & 255) << 24 - 8 * (f % 4);
return new p.init(b, c)
}
}, h = b.Utf8 = {
stringify: function (a) {
try {
return decodeURIComponent(escape(j.stringify(a)))
} catch (c) {
throw Error("Malformed UTF-8 data");
}
},
parse: function (a) {
return j.parse(unescape(encodeURIComponent(a)))
}
},
r = d.BufferedBlockAlgorithm = k.extend({
reset: function () {
this._data = new p.init;
this._nDataBytes = 0
},
_append: function (a) {
"string" == typeof a && (a = h.parse(a));
this._data.concat(a);
this._nDataBytes += a.sigBytes
},
_process: function (a) {
var c = this._data,
b = c.words,
f = c.sigBytes,
d = this.blockSize,
e = f / (4 * d),
e = a ? g.ceil(e) : g.max((e | 0) - this._minBufferSize, 0);
a = e * d;
f = g.min(4 * a, f);
if (a) {
for (var k = 0; k < a; k += d) this._doProcessBlock(b, k);
k = b.splice(0, a);
c.sigBytes -= f
}
return new p.init(k, f)
},
clone: function () {
var a = k.clone.call(this);
a._data = this._data.clone();
return a
},
_minBufferSize: 0
});
d.Hasher = r.extend({
cfg: k.extend(),
init: function (a) {
this.cfg = this.cfg.extend(a);
this.reset()
},
reset: function () {
r.reset.call(this);
this._doReset()
},
update: function (a) {
this._append(a);
this._process();
return this
},
finalize: function (a) {
a && this._append(a);
return this._doFinalize()
},
blockSize: 16,
_createHelper: function (a) {
return function (b, d) {
return (new a.init(d)).finalize(b)
}
},
_createHmacHelper: function (a) {
return function (b, d) {
return (new s.HMAC.init(a, d)).finalize(b)
}
}
});
var s = e.algo = {};
return e
}(Math);
(function () {
var g = CryptoJS,
l = g.lib,
e = l.WordArray,
d = l.Hasher,
m = [],
l = g.algo.SHA1 = d.extend({
_doReset: function () {
this._hash = new e.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520])
},
_doProcessBlock: function (d, e) {
for (var b = this._hash.words, n = b[0], j = b[1], h = b[2], g = b[3], l = b[4], a = 0; 80 > a; a++) {
if (16 > a) m[a] = d[e + a] | 0;
else {
var c = m[a - 3] ^ m[a - 8] ^ m[a - 14] ^ m[a - 16];
m[a] = c << 1 | c >>> 31
}
c = (n << 5 | n >>> 27) + l + m[a];
c = 20 > a ? c + ((j & h | ~j & g) + 1518500249) : 40 > a ? c + ((j ^ h ^ g) + 1859775393) : 60 > a ? c + ((j & h | j & g | h & g) - 1894007588) : c + ((j ^ h ^ g) - 899497514);
l = g;
g = h;
h = j << 30 | j >>> 2;
j = n;
n = c
}
b[0] = b[0] + n | 0;
b[1] = b[1] + j | 0;
b[2] = b[2] + h | 0;
b[3] = b[3] + g | 0;
b[4] = b[4] + l | 0
},
_doFinalize: function () {
var d = this._data,
e = d.words,
b = 8 * this._nDataBytes,
g = 8 * d.sigBytes;
e[g >>> 5] |= 128 << 24 - g % 32;
e[(g + 64 >>> 9 << 4) + 14] = Math.floor(b / 4294967296);
e[(g + 64 >>> 9 << 4) + 15] = b;
d.sigBytes = 4 * e.length;
this._process();
return this._hash
},
clone: function () {
var e = d.clone.call(this);
e._hash = this._hash.clone();
return e
}
});
g.SHA1 = d._createHelper(l);
g.HmacSHA1 = d._createHmacHelper(l)
})();
(function () {
var g = CryptoJS,
l = g.enc.Utf8;
g.algo.HMAC = g.lib.Base.extend({
init: function (e, d) {
e = this._hasher = new e.init;
"string" == typeof d && (d = l.parse(d));
var g = e.blockSize,
k = 4 * g;
d.sigBytes > k && (d = e.finalize(d));
d.clamp();
for (var p = this._oKey = d.clone(), b = this._iKey = d.clone(), n = p.words, j = b.words, h = 0; h < g; h++) n[h] ^= 1549556828, j[h] ^= 909522486;
p.sigBytes = b.sigBytes = k;
this.reset()
},
reset: function () {
var e = this._hasher;
e.reset();
e.update(this._iKey)
},
update: function (e) {
this._hasher.update(e);
return this
},
finalize: function (e) {
var d = this._hasher;
e = d.finalize(e);
d.reset();
return d.finalize(this._oKey.clone().concat(e))
}
})
})();
//使用算法
// var key = "f7205fffe445421fdssdfsdfdsfs"
// var sha1_result = CryptoJS.HmacSHA1("helloword", key)
// console.log('-------',sha1_result.toString())
window && (window.CryptoJSTest = CryptoJS);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,105 @@
let recorder;
let speechRecognizer;
let isCanSendData = false;
let isCanStop;
$(function () {
const params = {
signCallback: signCallback, // 鉴权函数 用户提供鉴权函数不传则为null
// 用户参数
secretid: config.secretId,
appid: config.appId,
// 实时识别接口参数
engine_model_type : '16k_zh', // 引擎
voice_format : 1,
// 以下为非必填参数,可跟据业务自行修改
hotword_id : '08003a00000000000000000000000000',
needvad: 1,
filter_dirty: 1,
filter_modal: 1,
filter_punc: 1,
convert_num_mode : 1,
word_info: 2
}
$('#start').on('click', function () {
const areaDom = $('#recognizeText');
let resultText = '';
$(this).hide();
$('#connecting').show();
speechRecognizer = null;
isCanSendData = false;
// 获取录音数据
recorder = new WebRecorder();
recorder.OnReceivedData = (res) => {
// console.log(res) // res 为采集到浏览器数据
if (isCanSendData) {
// 发送数据
speechRecognizer.write(res);
}
};
// 录音失败时
recorder.OnError = (err) => {
console.log(err);
recorder.stop();
};
recorder.start();
if (!speechRecognizer) {
speechRecognizer = new SpeechRecognizer(params);
}
// 开始识别
speechRecognizer.OnRecognitionStart = (res) => {
console.log('开始识别', res);
isCanSendData = true;
isCanStop = true;
$('#connecting').hide();
$('#end').show();
$('#recognizing').show();
};
// 一句话开始
speechRecognizer.OnSentenceBegin = (res) => {
console.log('一句话开始', res);
};
// 识别变化时
speechRecognizer.OnRecognitionResultChange = (res) => {
console.log('识别变化时', res);
const currentText = `${resultText}${res.result.voice_text_str}`;
areaDom.text(currentText);
};
// 一句话结束
speechRecognizer.OnSentenceEnd = (res) => {
console.log('一句话结束', res);
resultText += res.result.voice_text_str;
areaDom.text(resultText);
};
// 识别结束
speechRecognizer.OnRecognitionComplete = (res) => {
console.log('识别结束', res);
isCanSendData = false;
};
// 识别错误
speechRecognizer.OnError = (res) => {
console.log('识别失败', res);
isCanSendData = false;
$('#end').hide();
$('#recognizing').hide();
$('#connecting').hide();
$('#start').show();
};
// 建立连接
speechRecognizer.start();
});
$('#end').on('click', function () {
$(this).hide();
$('#start').show();
$('#recognizing').hide();
recorder.stop();
if (isCanStop) {
speechRecognizer.stop();
}
});
});

View File

@ -0,0 +1,212 @@
import { SpeechRecognizer } from '../../app/speechrecognizer';
export const guid = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
const audioWorkletCode = `
class MyProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.audioData = [];
this.preTime = 0;
}
process(inputs) {
// 去处理音频数据
// eslint-disable-next-line no-undef
if (inputs[0][0]) {
const output = ${to16kHz}(inputs[0][0], sampleRate);
const audioData = ${to16BitPCM}(output);
const data = [...new Int8Array(audioData.buffer)];
this.audioData = this.audioData.concat(data);
if (new Date().getTime() - this.preTime > 100) {
this.port.postMessage({
audioData: new Int8Array(this.audioData)
});
this.preTime = new Date().getTime();
this.audioData = [];
}
return true;
}
}
}
registerProcessor('my-processor', MyProcessor);
`;
const audioWorkletBlobURL = window.URL.createObjectURL(new Blob([audioWorkletCode], { type: 'text/javascript' }));
const needFiltrationParams = ['appId', 'secretKey', 'secretId', 'audioTrack'];
class ASR {
constructor(options, isLog) {
this.audioTrack = options.audioTrack;
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.speechRecognizer = null;
this.isCanSendData = false;
this.audioData = [];
this.secretkey = options.secretKey;
this.params = {
...options,
secretid: options.secretId,
appid: options.appId,
};
this.isLog = isLog;
this.OnRecognitionStart = function () {};
this.OnSentenceBegin = function () {};
this.OnRecognitionResultChange = function () {};
this.OnSentenceEnd = function () {};
this.OnRecognitionComplete = function () {};
this.OnError = function () {};
this.OnChange = function () {};
}
signCallback(signStr) {
const secretKey = this.secretkey;
const hash = window.CryptoJSTest.HmacSHA1(signStr, secretKey);
const bytes = Uint8ArrayToString(toUint8Array(hash));
return window.btoa(bytes);
}
start() {
if (!this.speechRecognizer) {
const tempQuery = { ...this.params };
for (let i = 0, len = needFiltrationParams.length; i < len; i++) {
if (tempQuery.hasOwnProperty(needFiltrationParams[i])) {
delete tempQuery[needFiltrationParams[i]];
}
}
const params = {
// 用户参数
signCallback: this.signCallback.bind(this),
...tempQuery,
};
this.speechRecognizer = new SpeechRecognizer(params, guid(), this.isLog);
}
// 开始识别
this.speechRecognizer.OnRecognitionStart = (res) => {
this.isCanSendData = true;
this.OnRecognitionStart(res);
};
// 一句话开始
this.speechRecognizer.OnSentenceBegin = (res) => {
this.OnSentenceBegin(res);
this.OnChange(res);
};
// 识别变化时
this.speechRecognizer.OnRecognitionResultChange = (res) => {
this.OnRecognitionResultChange(res);
this.OnChange(res);
};
// 一句话结束
this.speechRecognizer.OnSentenceEnd = (res) => {
this.OnSentenceEnd(res);
this.OnChange(res);
};
// 识别结束
this.speechRecognizer.OnRecognitionComplete = (res) => {
this.OnRecognitionComplete(res);
};
// 识别错误
this.speechRecognizer.OnError = (res) => {
this.isCanSendData = false;
this.OnError(res);
};
// 建立连接
this.speechRecognizer.start();
this.getAudioData();
}
getAudioData() {
const mediaStream = new MediaStream();
mediaStream.addTrack(this.audioTrack);
const mediaStreamSource = this.audioContext.createMediaStreamSource(mediaStream); // 将声音对象输入这个对象
if (this.audioContext.audioWorklet) {
this.audioContext.audioWorklet.addModule(audioWorkletBlobURL).then(() => {
const myNode = new AudioWorkletNode(this.audioContext, 'my-processor', { numberOfInputs: 1, numberOfOutputs: 1, channelCount: 1 });
myNode.port.onmessage = (event) => {
if (this.isCanSendData) {
this.speechRecognizer.write(event.data.audioData);
}
};
mediaStreamSource.connect(myNode).connect(this.audioContext.destination);
})
.catch(console.error);
} else {
// 创建一个音频分析对象采样的缓冲区大小为0自动适配输入和输出都是单声道
const scriptProcessor = this.audioContext.createScriptProcessor(0, 1, 1);
scriptProcessor.onaudioprocess = (e) => {
// 去处理音频数据
const inputData = e.inputBuffer.getChannelData(0);
const output = to16kHz(inputData, this.audioContext.sampleRate);
const audioData = to16BitPCM(output);
this.audioData.push(...new Int8Array(audioData.buffer));
if (new Date().getTime() - this.preTime > 100) {
if (this.isCanSendData) {
const audioDataArray = new Int8Array(this.audioData);
this.speechRecognizer.write(audioDataArray);
this.preTime = new Date().getTime();
this.audioData = [];
}
}
};
// 连接
mediaStreamSource.connect(scriptProcessor);
scriptProcessor.connect(this.audioContext.destination);
}
}
stop() {
this.speechRecognizer.stop();
this.audioContext && this.audioContext.suspend();
}
}
window && (window.ASR = ASR);
function toUint8Array(wordArray) {
// Shortcuts
const { words } = wordArray;
const { sigBytes } = wordArray;
// Convert
const u8 = new Uint8Array(sigBytes);
for (let i = 0; i < sigBytes; i++) {
u8[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
}
return u8;
}
function Uint8ArrayToString(fileData) {
let dataString = '';
for (let i = 0; i < fileData.length; i++) {
dataString += String.fromCharCode(fileData[i]);
}
return dataString;
}
function to16BitPCM(input) {
const dataLength = input.length * (16 / 8);
const dataBuffer = new ArrayBuffer(dataLength);
const dataView = new DataView(dataBuffer);
let offset = 0;
for (let i = 0; i < input.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, input[i]));
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return dataView;
}
function to16kHz(audioData, sampleRate = 44100) {
const data = new Float32Array(audioData);
const fitCount = Math.round(data.length * (16000 / sampleRate));
const newData = new Float32Array(fitCount);
const springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];
for (let i = 1; i < fitCount - 1; i++) {
const tmp = i * springFactor;
const before = Math.floor(tmp).toFixed();
const after = Math.ceil(tmp).toFixed();
const atPoint = tmp - before;
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
newData[fitCount - 1] = data[data.length - 1];
return newData;
}

File diff suppressed because one or more lines are too long