修改bug
This commit is contained in:
parent
21cecfa1c6
commit
378695b19a
|
@ -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 />
|
||||||
|
|
|
@ -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;">
|
||||||
|
|
|
@ -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', // 因为内置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
|
||||||
}
|
}
|
||||||
};
|
|
||||||
//判断是否有课
|
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>
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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 */
|
|
@ -0,0 +1,7 @@
|
||||||
|
let config = {
|
||||||
|
secretKey: 'QcbMitrb7QVMyNaZJp7E7mESSFrGUWKz',
|
||||||
|
secretId: 'AKIDJfpnZ9EF5i7II4cw3xfARVMyNYeljU3Q',
|
||||||
|
appId: 1255881128,
|
||||||
|
}
|
||||||
|
window.config = config
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
Loading…
Reference in New Issue