Merge pull request #1039 from nanayashiki1215/IFlytek/IFlytekVoice
添加语音模块
This commit is contained in:
commit
3db73d458b
|
|
@ -23,24 +23,36 @@
|
|||
"common:prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@efox/emp-vuett": "^1.0.0",
|
||||
"@rollup/plugin-inject": "^5.0.3",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"axios": "^1.3.5",
|
||||
"crypto-es": "^1.2.7",
|
||||
"fast-xml-parser": "^4.2.5",
|
||||
"highlight.js": "^11.7.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jquery": "3.6.2",
|
||||
"js-base64": "^3.7.3",
|
||||
"katex": "^0.16.4",
|
||||
"markdown-it": "^13.0.1",
|
||||
"naive-ui": "^2.34.3",
|
||||
"pinia": "^2.0.33",
|
||||
"qs": "^6.11.1",
|
||||
"vconsole": "^3.15.1",
|
||||
"vite-plugin-commonjs": "^0.6.2",
|
||||
"voice-input-button2": "^1.1.9",
|
||||
"vue": "^3.2.47",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.2.2",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.35.3",
|
||||
"@commitlint/cli": "^17.4.4",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@iconify/vue": "^4.1.0",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
|
|
@ -56,9 +68,11 @@
|
|||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.1.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"node-sass": "^9.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.21",
|
||||
"rimraf": "^4.2.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "~4.9.5",
|
||||
"vite": "^4.2.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed/* , ref */ } from 'vue'
|
||||
import { NButton, NPopconfirm, NSelect, useMessage } from 'naive-ui'
|
||||
import type { Language, Theme } from '@/store/modules/app/helper'
|
||||
import type { Language, Theme, AudioSettings} from '@/store/modules/app/helper'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAppStore/* , useUserStore */ } from '@/store'
|
||||
/* import type { UserInfo } from '@/store/modules/user/helper' */
|
||||
|
|
@ -35,6 +35,15 @@ const language = computed({
|
|||
},
|
||||
})
|
||||
|
||||
const audioSettings = computed({
|
||||
get() {
|
||||
return appStore.audioSettings
|
||||
},
|
||||
set(value: AudioSettings) {
|
||||
appStore.setAudio(value)
|
||||
},
|
||||
})
|
||||
|
||||
const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
||||
/* {
|
||||
label: 'Auto',
|
||||
|
|
@ -53,6 +62,10 @@ const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
|||
},
|
||||
]
|
||||
|
||||
// export type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ko-KR' | 'ru-RU'
|
||||
|
||||
// export type AudioSettings = 'input' | 'send' | 'close'
|
||||
|
||||
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
||||
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
|
||||
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
|
||||
|
|
@ -61,6 +74,13 @@ const languageOptions: { label: string; key: Language; value: Language }[] = [
|
|||
{ label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
|
||||
]
|
||||
|
||||
const audioOptions: { label: string; key: AudioSettings; value: AudioSettings }[] = [
|
||||
{ label: '语音识别后显示在对话框', key: 'input', value: 'input' },
|
||||
{ label: '语音识别后直接发送', key: 'send', value: 'send' },
|
||||
{ label: '关闭语音识别功能', key: 'close', value: 'close' },
|
||||
{ label: '关闭语音识别与合成功能', key: 'closeAll', value: 'closeAll' },
|
||||
]
|
||||
|
||||
/* function updateUserInfo(options: Partial<UserInfo>) {
|
||||
userStore.updateUserInfo(options)
|
||||
ms.success(t('common.success'))
|
||||
|
|
@ -213,6 +233,17 @@ function handleImportButtonClick(): void {
|
|||
@update-value="value => appStore.setLanguage(value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.audiosetting') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<NSelect
|
||||
style="width: 210px"
|
||||
:value="audioSettings"
|
||||
:options="audioOptions"
|
||||
@update-value="value => appStore.setAudio(value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default {
|
|||
resetUserInfo: 'Reset UserInfo',
|
||||
chatHistory: 'ChatHistory',
|
||||
theme: 'Theme',
|
||||
audiosetting: 'AudioSet',
|
||||
language: 'Language',
|
||||
api: 'API',
|
||||
reverseProxy: 'Reverse Proxy',
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export default {
|
|||
chatHistory: '채팅 기록',
|
||||
theme: '테마',
|
||||
language: '언어',
|
||||
audiosetting: '음성설정',
|
||||
api: 'API',
|
||||
reverseProxy: '리버스 프록시',
|
||||
timeout: '타임아웃',
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default {
|
|||
resetUserInfo: 'Сбросить информацию о пользователе',
|
||||
chatHistory: 'История чата',
|
||||
theme: 'Тема',
|
||||
audiosetting: 'Настройка',
|
||||
language: 'Язык',
|
||||
api: 'API',
|
||||
reverseProxy: 'Обратный прокси-сервер',
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default {
|
|||
resetUserInfo: '重置用户信息',
|
||||
chatHistory: '聊天记录',
|
||||
theme: '主题',
|
||||
audiosetting: '语音设置',
|
||||
language: '语言',
|
||||
api: 'API',
|
||||
reverseProxy: '反向代理',
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default {
|
|||
resetUserInfo: '重設使用者資訊',
|
||||
chatHistory: '紀錄',
|
||||
theme: '主題',
|
||||
audiosetting: '語音設定',
|
||||
language: '語言',
|
||||
api: 'API',
|
||||
reverseProxy: '反向代理',
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ export type Theme = 'light' | 'dark' | 'auto'
|
|||
|
||||
export type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ko-KR' | 'ru-RU'
|
||||
|
||||
export type AudioSettings = 'input' | 'send' | 'close' | 'closeAll'
|
||||
|
||||
export interface AppState {
|
||||
siderCollapsed: boolean
|
||||
theme: Theme
|
||||
language: Language
|
||||
audioSettings: AudioSettings
|
||||
}
|
||||
|
||||
export function defaultSetting(): AppState {
|
||||
return { siderCollapsed: false, theme: 'light', language: 'zh-CN' }
|
||||
return { siderCollapsed: false, theme: 'light', audioSettings: 'input', language: 'zh-CN' }
|
||||
}
|
||||
|
||||
export function getLocalSetting(): AppState {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import type { AppState, Language, Theme } from './helper'
|
||||
import type { AppState, Language, Theme, AudioSettings} from './helper'
|
||||
import { getLocalSetting, setLocalSetting } from './helper'
|
||||
|
||||
import { store } from '@/store'
|
||||
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
|
|
@ -23,6 +24,11 @@ export const useAppStore = defineStore('app-store', {
|
|||
}
|
||||
},
|
||||
|
||||
setAudio(audioSettings: AudioSettings) {
|
||||
this.audioSettings = audioSettings
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
recordState() {
|
||||
setLocalSetting(this.$state)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang='ts'>
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref , onBeforeUnmount , watchEffect , getCurrentInstance} from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { NAutoComplete, NButton, NDropdown, NInput, NRadioButton, NRadioGroup, useDialog, useMessage } from 'naive-ui'
|
||||
|
|
@ -17,6 +17,14 @@ import { useChatStore, usePromptStore } from '@/store'
|
|||
import { t } from '@/locales'
|
||||
import { bing_search, chat, chatfile } from '@/api/chat'
|
||||
import { idStore } from '@/store/modules/knowledgebaseid/id'
|
||||
// 添加语音识别
|
||||
import VConsole from "vconsole";
|
||||
import CryptoES from "crypto-es"; // 科大讯飞
|
||||
import { Base64 } from "js-base64";
|
||||
import IatRecorder from "./voicekeda/until/iatRecorder"; // 科大讯飞
|
||||
import TTSRecorder from "./voicekeda/kedatts/tts-ws";
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
let controller = new AbortController()
|
||||
const { iconRender } = useIconRender()
|
||||
// const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
|
||||
|
|
@ -57,6 +65,116 @@ dataSources.value.forEach((item, index) => {
|
|||
updateChatSome(+uuid, index, { loading: false })
|
||||
})
|
||||
|
||||
|
||||
// ======================科大讯飞语音识别模块=============================
|
||||
|
||||
let iatRecorder = new IatRecorder()
|
||||
var vConsole = new VConsole()
|
||||
let countInterval
|
||||
const showTimer = ref(false)
|
||||
const isRecording = ref(false)
|
||||
const seconds = ref(0)
|
||||
const buttonText = ref('开始识别')
|
||||
const currentIcon = ref('icon-park-solid:voice')
|
||||
|
||||
// 状态改变时触发
|
||||
iatRecorder.onWillStatusChange = function (oldStatus, status) {
|
||||
// 可以在这里进行页面中一些交互逻辑处理:倒计时,录音的动画,按钮交互等
|
||||
// let text = {
|
||||
// null: "开始识别", // 最开始状态
|
||||
// init: "开始识别", // 初始化状态
|
||||
// ing: "结束识别", // 正在录音状态
|
||||
// end: "开始识别", // 结束状态
|
||||
// };
|
||||
// let senconds = 0;
|
||||
if (status === 'ing') {
|
||||
currentIcon.value = 'mingcute:voice-line'
|
||||
showTimer.value = true
|
||||
countInterval = setInterval(() => {
|
||||
seconds.value++
|
||||
}, 1000)
|
||||
} else if (status === 'init') {
|
||||
currentIcon.value = 'icon-park-solid:voice'
|
||||
showTimer.value = true
|
||||
seconds.value = 0
|
||||
}else if (status === 'end') {
|
||||
currentIcon.value = 'icon-park-solid:voice'
|
||||
showTimer.value = false
|
||||
clearInterval(countInterval)
|
||||
const appStore = useAppStore()
|
||||
const audioSetting = computed(() => appStore.audioSettings);
|
||||
if(audioSetting.value == "send"){
|
||||
onConversation()
|
||||
}
|
||||
}else {
|
||||
currentIcon.value = 'icon-park-solid:voice'
|
||||
showTimer.value = false
|
||||
clearInterval(countInterval)
|
||||
}
|
||||
};
|
||||
|
||||
// 监听识别结果的变化
|
||||
iatRecorder.onTextChange = function (text) {
|
||||
console.log(text, "text")
|
||||
const appStore = useAppStore()
|
||||
const audioSetting = computed(() => appStore.audioSettings);
|
||||
if (audioSetting.value != "close" && audioSetting.value != "closeall") {
|
||||
prompt.value = text
|
||||
}
|
||||
};
|
||||
|
||||
async function distinguish() {
|
||||
if (iatRecorder.status === "ing") {
|
||||
iatRecorder.stop()
|
||||
} else {
|
||||
iatRecorder.start()
|
||||
}
|
||||
}
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return {
|
||||
'status-init': !isRecording.value,
|
||||
'status-ing': isRecording.value,
|
||||
}
|
||||
})
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
const minutes = Math.floor(seconds.value / 60)
|
||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||
const formattedSeconds = (seconds.value % 60).toString().padStart(2, '0');
|
||||
// return `${formattedMinutes}:${formattedSeconds}`;
|
||||
return `${formattedSeconds}`
|
||||
})
|
||||
|
||||
// ======================end==========================
|
||||
|
||||
// ======================科大讯飞语音合成模块=============================
|
||||
|
||||
let ttsRecorder = new TTSRecorder()
|
||||
|
||||
function playTTS(string) {
|
||||
// ttsRecorder初始化配置
|
||||
ttsRecorder.setParams({
|
||||
voiceName: 'aisxping',
|
||||
text: string
|
||||
})
|
||||
ttsRecorder.start()
|
||||
}
|
||||
|
||||
ttsRecorder.onWillStatusChange = function(oldStatus, status) {
|
||||
// 可以在这里的回调中进行页面中一些交互逻辑处理:按钮交互等
|
||||
// 按钮中的状态
|
||||
let btnState = {
|
||||
init: '立即合成',
|
||||
ttsing: '正在合成',
|
||||
play: '停止播放',
|
||||
endPlay: '重新播放',
|
||||
errorTTS: '合成失败',
|
||||
}
|
||||
}
|
||||
|
||||
// ======================end==========================
|
||||
|
||||
async function handleSubmit() {
|
||||
if (search.value === 'Bing搜索') {
|
||||
loading.value = true
|
||||
|
|
@ -194,6 +312,14 @@ async function onConversation() {
|
|||
requestOptions: { prompt: message, options: { ...options } },
|
||||
},
|
||||
)
|
||||
|
||||
// 语音合成
|
||||
const appStore = useAppStore()
|
||||
const audioSetting = computed(() => appStore.audioSettings);
|
||||
if (audioSetting.value != "closeall") {
|
||||
playTTS(result);
|
||||
}
|
||||
|
||||
scrollToBottomIfAtBottom()
|
||||
loading.value = false
|
||||
/* await fetchChatAPIProcess<Chat.ConversationResponse>({
|
||||
|
|
@ -690,6 +816,16 @@ function searchfun() {
|
|||
/>
|
||||
</template>
|
||||
</NAutoComplete>
|
||||
<NButton type="info" @click="distinguish" :class="buttonClass" style="background-color:rgb(88, 88, 242);">
|
||||
<template #icon>
|
||||
<span class="dark:text-black">
|
||||
<SvgIcon :icon="currentIcon" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="showTimer" class="time-box">
|
||||
<span class="used-time">{{ formattedTime }}</span>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
|
||||
<template #icon>
|
||||
<span class="dark:text-black">
|
||||
|
|
@ -725,4 +861,13 @@ function searchfun() {
|
|||
box-shadow: 0 12px 40px 0 rgba(148,186,215,.2);
|
||||
border: 1px solid ;
|
||||
}
|
||||
|
||||
.time-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.used-time {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* base64.js
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License.
|
||||
* http://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* References:
|
||||
* http://en.wikipedia.org/wiki/Base64
|
||||
*/
|
||||
;(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined'
|
||||
? module.exports = factory(global)
|
||||
: typeof define === 'function' && define.amd
|
||||
? define(factory) : factory(global)
|
||||
}((
|
||||
typeof self !== 'undefined' ? self
|
||||
: typeof window !== 'undefined' ? window
|
||||
: typeof global !== 'undefined' ? global
|
||||
: this
|
||||
), function(global) {
|
||||
'use strict';
|
||||
// existing version for noConflict()
|
||||
global = global || {};
|
||||
var _Base64 = global.Base64;
|
||||
var version = "2.5.1";
|
||||
// if node.js and NOT React Native, we use Buffer
|
||||
var buffer;
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
try {
|
||||
buffer = eval("require('buffer').Buffer");
|
||||
} catch (err) {
|
||||
buffer = undefined;
|
||||
}
|
||||
}
|
||||
// constants
|
||||
var b64chars
|
||||
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
var b64tab = function(bin) {
|
||||
var t = {};
|
||||
for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i;
|
||||
return t;
|
||||
}(b64chars);
|
||||
var fromCharCode = String.fromCharCode;
|
||||
// encoder stuff
|
||||
var cb_utob = function(c) {
|
||||
if (c.length < 2) {
|
||||
var cc = c.charCodeAt(0);
|
||||
return cc < 0x80 ? c
|
||||
: cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
|
||||
+ fromCharCode(0x80 | (cc & 0x3f)))
|
||||
: (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
} else {
|
||||
var cc = 0x10000
|
||||
+ (c.charCodeAt(0) - 0xD800) * 0x400
|
||||
+ (c.charCodeAt(1) - 0xDC00);
|
||||
return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
|
||||
+ fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
}
|
||||
};
|
||||
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
|
||||
var utob = function(u) {
|
||||
return u.replace(re_utob, cb_utob);
|
||||
};
|
||||
var cb_encode = function(ccc) {
|
||||
var padlen = [0, 2, 1][ccc.length % 3],
|
||||
ord = ccc.charCodeAt(0) << 16
|
||||
| ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
|
||||
| ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
|
||||
chars = [
|
||||
b64chars.charAt( ord >>> 18),
|
||||
b64chars.charAt((ord >>> 12) & 63),
|
||||
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
|
||||
padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
|
||||
];
|
||||
return chars.join('');
|
||||
};
|
||||
var btoa = global.btoa ? function(b) {
|
||||
return global.btoa(b);
|
||||
} : function(b) {
|
||||
return b.replace(/[\s\S]{1,3}/g, cb_encode);
|
||||
};
|
||||
var _encode = buffer ?
|
||||
buffer.from && Uint8Array && buffer.from !== Uint8Array.from
|
||||
? function (u) {
|
||||
return (u.constructor === buffer.constructor ? u : buffer.from(u))
|
||||
.toString('base64')
|
||||
}
|
||||
: function (u) {
|
||||
return (u.constructor === buffer.constructor ? u : new buffer(u))
|
||||
.toString('base64')
|
||||
}
|
||||
: function (u) { return btoa(utob(u)) }
|
||||
;
|
||||
var encode = function(u, urisafe) {
|
||||
return !urisafe
|
||||
? _encode(String(u))
|
||||
: _encode(String(u)).replace(/[+\/]/g, function(m0) {
|
||||
return m0 == '+' ? '-' : '_';
|
||||
}).replace(/=/g, '');
|
||||
};
|
||||
var encodeURI = function(u) { return encode(u, true) };
|
||||
// decoder stuff
|
||||
var re_btou = new RegExp([
|
||||
'[\xC0-\xDF][\x80-\xBF]',
|
||||
'[\xE0-\xEF][\x80-\xBF]{2}',
|
||||
'[\xF0-\xF7][\x80-\xBF]{3}'
|
||||
].join('|'), 'g');
|
||||
var cb_btou = function(cccc) {
|
||||
switch(cccc.length) {
|
||||
case 4:
|
||||
var cp = ((0x07 & cccc.charCodeAt(0)) << 18)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(2)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(3)),
|
||||
offset = cp - 0x10000;
|
||||
return (fromCharCode((offset >>> 10) + 0xD800)
|
||||
+ fromCharCode((offset & 0x3FF) + 0xDC00));
|
||||
case 3:
|
||||
return fromCharCode(
|
||||
((0x0f & cccc.charCodeAt(0)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(2))
|
||||
);
|
||||
default:
|
||||
return fromCharCode(
|
||||
((0x1f & cccc.charCodeAt(0)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(1))
|
||||
);
|
||||
}
|
||||
};
|
||||
var btou = function(b) {
|
||||
return b.replace(re_btou, cb_btou);
|
||||
};
|
||||
var cb_decode = function(cccc) {
|
||||
var len = cccc.length,
|
||||
padlen = len % 4,
|
||||
n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
|
||||
| (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
|
||||
| (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
|
||||
| (len > 3 ? b64tab[cccc.charAt(3)] : 0),
|
||||
chars = [
|
||||
fromCharCode( n >>> 16),
|
||||
fromCharCode((n >>> 8) & 0xff),
|
||||
fromCharCode( n & 0xff)
|
||||
];
|
||||
chars.length -= [0, 0, 2, 1][padlen];
|
||||
return chars.join('');
|
||||
};
|
||||
var _atob = global.atob ? function(a) {
|
||||
return global.atob(a);
|
||||
} : function(a){
|
||||
return a.replace(/\S{1,4}/g, cb_decode);
|
||||
};
|
||||
var atob = function(a) {
|
||||
return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
|
||||
};
|
||||
var _decode = buffer ?
|
||||
buffer.from && Uint8Array && buffer.from !== Uint8Array.from
|
||||
? function(a) {
|
||||
return (a.constructor === buffer.constructor
|
||||
? a : buffer.from(a, 'base64')).toString();
|
||||
}
|
||||
: function(a) {
|
||||
return (a.constructor === buffer.constructor
|
||||
? a : new buffer(a, 'base64')).toString();
|
||||
}
|
||||
: function(a) { return btou(_atob(a)) };
|
||||
var decode = function(a){
|
||||
return _decode(
|
||||
String(a).replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' })
|
||||
.replace(/[^A-Za-z0-9\+\/]/g, '')
|
||||
);
|
||||
};
|
||||
var noConflict = function() {
|
||||
var Base64 = global.Base64;
|
||||
global.Base64 = _Base64;
|
||||
return Base64;
|
||||
};
|
||||
// export Base64
|
||||
global.Base64 = {
|
||||
VERSION: version,
|
||||
atob: atob,
|
||||
btoa: btoa,
|
||||
fromBase64: decode,
|
||||
toBase64: encode,
|
||||
utob: utob,
|
||||
encode: encode,
|
||||
encodeURI: encodeURI,
|
||||
btou: btou,
|
||||
decode: decode,
|
||||
noConflict: noConflict,
|
||||
__buffer__: buffer
|
||||
};
|
||||
// if ES5 is available, make Base64.extendString() available
|
||||
if (typeof Object.defineProperty === 'function') {
|
||||
var noEnum = function(v){
|
||||
return {value:v,enumerable:false,writable:true,configurable:true};
|
||||
};
|
||||
global.Base64.extendString = function () {
|
||||
Object.defineProperty(
|
||||
String.prototype, 'fromBase64', noEnum(function () {
|
||||
return decode(this)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64', noEnum(function (urisafe) {
|
||||
return encode(this, urisafe)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64URI', noEnum(function () {
|
||||
return encode(this, true)
|
||||
}));
|
||||
};
|
||||
}
|
||||
//
|
||||
// export Base64 to the namespace
|
||||
//
|
||||
if (global['Meteor']) { // Meteor.js
|
||||
Base64 = global.Base64;
|
||||
}
|
||||
// module.exports and AMD are mutually exclusive.
|
||||
// module.exports has precedence.
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports.Base64 = global.Base64;
|
||||
}
|
||||
else if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define([], function(){ return global.Base64 });
|
||||
}
|
||||
// that's it!
|
||||
return {Base64: global.Base64}
|
||||
}));
|
||||
|
||||
|
||||
|
||||
//////////////////
|
||||
// WEBPACK FOOTER
|
||||
// ./node_modules/_js-base64@2.5.1@js-base64/base64.js
|
||||
// module id = 57
|
||||
// module chunks = 2
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* @Autor: lycheng
|
||||
* @Date: 2019-12-27 15:21:38
|
||||
* @Description:
|
||||
*/
|
||||
function writeString(data, offset, str) {
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
data.setUint8(offset + i, str.charCodeAt(i))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加wav头
|
||||
* @param {音频arrayBuffer} bytes
|
||||
* @param {采样率} sampleRate
|
||||
* @param {声道数} numChannels
|
||||
* @param {sampleBits} oututSampleBits
|
||||
* @param {小端字节} littleEdian
|
||||
*/
|
||||
function encodeWAV(
|
||||
bytes,
|
||||
sampleRate,
|
||||
numChannels,
|
||||
oututSampleBits,
|
||||
littleEdian = true
|
||||
) {
|
||||
let sampleBits = oututSampleBits
|
||||
let buffer = new ArrayBuffer(44 + bytes.byteLength)
|
||||
let data = new DataView(buffer)
|
||||
let channelCount = numChannels
|
||||
let offset = 0
|
||||
// 资源交换文件标识符
|
||||
writeString(data, offset, 'RIFF')
|
||||
offset += 4
|
||||
// 下个地址开始到文件尾总字节数,即文件大小-8
|
||||
data.setUint32(offset, 36 + bytes.byteLength, true)
|
||||
offset += 4
|
||||
// WAV文件标志
|
||||
writeString(data, offset, 'WAVE')
|
||||
offset += 4
|
||||
// 波形格式标志
|
||||
writeString(data, offset, 'fmt ')
|
||||
offset += 4
|
||||
// 过滤字节,一般为 0x10 = 16
|
||||
data.setUint32(offset, 16, true)
|
||||
offset += 4
|
||||
// 格式类别 (PCM形式采样数据)
|
||||
data.setUint16(offset, 1, true)
|
||||
offset += 2
|
||||
// 通道数
|
||||
data.setUint16(offset, channelCount, true)
|
||||
offset += 2
|
||||
// 采样率,每秒样本数,表示每个通道的播放速度
|
||||
data.setUint32(offset, sampleRate, true)
|
||||
offset += 4
|
||||
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
|
||||
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true)
|
||||
offset += 4
|
||||
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
|
||||
data.setUint16(offset, channelCount * (sampleBits / 8), true)
|
||||
offset += 2
|
||||
// 每样本数据位数
|
||||
data.setUint16(offset, sampleBits, true)
|
||||
offset += 2
|
||||
// 数据标识符
|
||||
writeString(data, offset, 'data')
|
||||
offset += 4
|
||||
// 采样数据总数,即数据总大小-44
|
||||
data.setUint32(offset, bytes.byteLength, true)
|
||||
offset += 4
|
||||
|
||||
// 给wav头增加pcm体
|
||||
for (let i = 0; i < bytes.byteLength; ) {
|
||||
data.setUint8(offset, bytes.getUint8(i), true)
|
||||
offset++
|
||||
i++
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function downloadWAV(audioData, sampleRate, oututSampleBits) {
|
||||
let wavData = encodeWAV(audioData, sampleRate||44100, 1, oututSampleBits||16)
|
||||
let blob = new Blob([wavData], {
|
||||
type: 'audio/wav',
|
||||
})
|
||||
let defaultName = new Date().getTime()
|
||||
let node = document.createElement('a')
|
||||
node.href = window.URL.createObjectURL(blob)
|
||||
node.download = `${defaultName}.wav`
|
||||
node.click()
|
||||
node.remove()
|
||||
}
|
||||
|
||||
function downloadPCM(audioData) {
|
||||
let blob = new Blob([audioData], {
|
||||
type: 'audio/pcm',
|
||||
})
|
||||
let defaultName = new Date().getTime()
|
||||
let node = document.createElement('a')
|
||||
node.href = window.URL.createObjectURL(blob)
|
||||
node.download = `${defaultName}.pcm`
|
||||
node.click()
|
||||
node.remove()
|
||||
}
|
||||
export {
|
||||
downloadWAV,
|
||||
downloadPCM
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
let minSampleRate = 22050
|
||||
self.onmessage = function(e) {
|
||||
transcode.transToAudioData(e.data)
|
||||
}
|
||||
var transcode = {
|
||||
transToAudioData: function(audioDataStr, fromRate = 16000, toRate = 22505) {
|
||||
let outputS16 = transcode.base64ToS16(audioDataStr)
|
||||
let output = transcode.transS16ToF32(outputS16)
|
||||
output = transcode.transSamplingRate(output, fromRate, toRate)
|
||||
output = Array.from(output)
|
||||
self.postMessage({
|
||||
data: output,
|
||||
rawAudioData: Array.from(outputS16)
|
||||
})
|
||||
},
|
||||
transSamplingRate: function(data, fromRate = 44100, toRate = 16000) {
|
||||
var fitCount = Math.round(data.length * (toRate / fromRate))
|
||||
var newData = new Float32Array(fitCount)
|
||||
var springFactor = (data.length - 1) / (fitCount - 1)
|
||||
newData[0] = data[0]
|
||||
for (let i = 1; i < fitCount - 1; i++) {
|
||||
var tmp = i * springFactor
|
||||
var before = Math.floor(tmp).toFixed()
|
||||
var after = Math.ceil(tmp).toFixed()
|
||||
var atPoint = tmp - before
|
||||
newData[i] = data[before] + (data[after] - data[before]) * atPoint
|
||||
}
|
||||
newData[fitCount - 1] = data[data.length - 1]
|
||||
return newData
|
||||
},
|
||||
transS16ToF32: function(input) {
|
||||
var tmpData = []
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
var d = input[i] < 0 ? input[i] / 0x8000 : input[i] / 0x7fff
|
||||
tmpData.push(d)
|
||||
}
|
||||
return new Float32Array(tmpData)
|
||||
},
|
||||
base64ToS16: function(base64AudioData) {
|
||||
base64AudioData = atob(base64AudioData)
|
||||
const outputArray = new Uint8Array(base64AudioData.length)
|
||||
for (let i = 0; i < base64AudioData.length; ++i) {
|
||||
outputArray[i] = base64AudioData.charCodeAt(i)
|
||||
}
|
||||
return new Int16Array(new DataView(outputArray.buffer).buffer)
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
//
|
||||
// tts-ws.ts
|
||||
// 科大讯飞语音合成
|
||||
//
|
||||
// Created by panhong on 2023/07/20.
|
||||
//
|
||||
import { downloadPCM, downloadWAV } from './download.js';
|
||||
import CryptoES from "crypto-es"; // 科大讯飞
|
||||
import Enc from 'enc';
|
||||
const transWorker = new Worker(
|
||||
new URL("../kedatts/transcode.worker.js", import.meta.url)
|
||||
);
|
||||
import VConsole from 'vconsole';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
/**
|
||||
* 获取websocket url
|
||||
* APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取,正式使用需要后端配置返回避免泄露
|
||||
*/
|
||||
const APPID = "";
|
||||
const API_SECRET = "";
|
||||
const API_KEY = "";
|
||||
|
||||
function getWebsocketUrl(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
var apiKey = API_KEY;
|
||||
var apiSecret = API_SECRET;
|
||||
var url = 'wss://tts-api.xfyun.cn/v2/tts';
|
||||
var host = location.host;
|
||||
var date = new Date().toGMTString();
|
||||
var algorithm = 'hmac-sha256';
|
||||
var headers = 'host date request-line';
|
||||
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`;
|
||||
var signatureSha = CryptoES.HmacSHA256(signatureOrigin, apiSecret);
|
||||
var signature = CryptoES.enc.Base64.stringify(signatureSha);
|
||||
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
|
||||
var authorization = btoa(authorizationOrigin);
|
||||
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
|
||||
resolve(url);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class TTSRecorder {
|
||||
private speed: number;
|
||||
private voice: number;
|
||||
private pitch: number;
|
||||
private voiceName: string;
|
||||
private text: string;
|
||||
private tte: string;
|
||||
private defaultText: string;
|
||||
private appId: string;
|
||||
private audioData: number[];
|
||||
private rawAudioData: number[];
|
||||
private audioDataOffset: number;
|
||||
private status: string;
|
||||
private ttsWS: WebSocket;
|
||||
private playTimeout: ReturnType<typeof setTimeout> | null;
|
||||
private audioContext: AudioContext | null;
|
||||
private bufferSource: AudioBufferSourceNode | null;
|
||||
|
||||
constructor({
|
||||
speed = 50,
|
||||
voice = 50,
|
||||
pitch = 50,
|
||||
voiceName = 'xiaoyan',
|
||||
appId = APPID,
|
||||
text = '',
|
||||
tte = 'UTF8',
|
||||
defaultText = '请输入您要合成的文本',
|
||||
}: {
|
||||
speed?: number;
|
||||
voice?: number;
|
||||
pitch?: number;
|
||||
voiceName?: string;
|
||||
appId?: string;
|
||||
text?: string;
|
||||
tte?: string;
|
||||
defaultText?: string;
|
||||
} = {}) {
|
||||
this.speed = speed;
|
||||
this.voice = voice;
|
||||
this.pitch = pitch;
|
||||
this.voiceName = voiceName;
|
||||
this.text = text;
|
||||
this.tte = tte;
|
||||
this.defaultText = defaultText;
|
||||
this.appId = appId;
|
||||
this.audioData = [];
|
||||
this.rawAudioData = [];
|
||||
this.audioDataOffset = 0;
|
||||
this.status = 'init';
|
||||
this.ttsWS = null;
|
||||
this.playTimeout = null;
|
||||
this.audioContext = null;
|
||||
this.bufferSource = null;
|
||||
transWorker.onmessage = (e: MessageEvent) => {
|
||||
// if(e.data != undefined && e.data.length > 0) {
|
||||
this.audioData.push(...e.data.data);
|
||||
this.rawAudioData.push(...e.data.rawAudioData);
|
||||
// }
|
||||
};
|
||||
}
|
||||
|
||||
// 修改录音听写状态
|
||||
private setStatus(status: string) {
|
||||
if (this.onWillStatusChange) {
|
||||
this.onWillStatusChange(this.status, status);
|
||||
}
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// 设置合成相关参数
|
||||
public setParams({ speed, voice, pitch, text, voiceName, tte }: {
|
||||
speed?: number;
|
||||
voice?: number;
|
||||
pitch?: number;
|
||||
text?: string;
|
||||
voiceName?: string;
|
||||
tte?: string;
|
||||
}) {
|
||||
speed !== undefined && (this.speed = speed);
|
||||
voice !== undefined && (this.voice = voice);
|
||||
pitch !== undefined && (this.pitch = pitch);
|
||||
text && (this.text = text);
|
||||
tte && (this.tte = tte);
|
||||
voiceName && (this.voiceName = voiceName);
|
||||
this.resetAudio();
|
||||
}
|
||||
|
||||
// 连接websocket
|
||||
private connectWebSocket() {
|
||||
this.setStatus('ttsing');
|
||||
return getWebsocketUrl().then(url => {
|
||||
let ttsWS: WebSocket;
|
||||
if ('WebSocket' in window) {
|
||||
ttsWS = new WebSocket(url);
|
||||
} else if ('MozWebSocket' in window) {
|
||||
ttsWS = new MozWebSocket(url);
|
||||
} else {
|
||||
alert('浏览器不支持WebSocket');
|
||||
return;
|
||||
}
|
||||
this.ttsWS = ttsWS;
|
||||
ttsWS.onopen = e => {
|
||||
this.webSocketSend();
|
||||
this.playTimeout = setTimeout(() => {
|
||||
this.audioPlay();
|
||||
}, 1000);
|
||||
};
|
||||
ttsWS.onmessage = e => {
|
||||
this.result(e.data);
|
||||
};
|
||||
ttsWS.onerror = e => {
|
||||
clearTimeout(this.playTimeout);
|
||||
this.setStatus('errorTTS');
|
||||
alert('WebSocket报错,请f12查看详情');
|
||||
console.error(`详情查看:${encodeURI(url.replace('wss:', 'https:'))}`);
|
||||
};
|
||||
ttsWS.onclose = e => {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 处理音频数据
|
||||
private transToAudioData(audioData: number[]) {
|
||||
// Implement your logic here
|
||||
}
|
||||
|
||||
// websocket发送数据
|
||||
// bgs有背景音 0:无背景音(默认值) 1:有背景音
|
||||
// auf 音频采样率,可选值:
|
||||
// audio/L16;rate=8000:合成8K 的音频
|
||||
// audio/L16;rate=16000:合成16K 的音频
|
||||
// auf不传值:合成16K 的音频
|
||||
// aue:音频编码,可选值:
|
||||
// raw:未压缩的pcm
|
||||
// lame:mp3 (当aue=lame时需传参sfl=1)
|
||||
// speex-org-wb;7: 标准开源speex(for speex_wideband,即16k)数字代表指定压缩等级(默认等级为8)
|
||||
// speex-org-nb;7: 标准开源speex(for speex_narrowband,即8k)数字代表指定压缩等级(默认等级为8)
|
||||
// speex;7:压缩格式,压缩等级1~10,默认为7(8k讯飞定制speex)
|
||||
// speex-wb;7:压缩格式,压缩等级1~10,默认为7(16k讯飞定制speex)
|
||||
private webSocketSend() {
|
||||
var params = {
|
||||
common: {
|
||||
app_id: this.appId, // APPID
|
||||
},
|
||||
business: {
|
||||
aue: 'raw',
|
||||
auf: 'audio/L16;rate=16000',
|
||||
vcn: this.voiceName,
|
||||
speed: this.speed,
|
||||
volume: this.voice,
|
||||
pitch: this.pitch,
|
||||
bgs: 0,
|
||||
tte: this.tte,
|
||||
},
|
||||
data: {
|
||||
status: 2,
|
||||
text: this.encodeText(
|
||||
this.text || this.defaultText,
|
||||
this.tte === 'unicode' ? 'base64&utf16le' : ''
|
||||
),
|
||||
},
|
||||
};
|
||||
this.ttsWS.send(JSON.stringify(params));
|
||||
}
|
||||
|
||||
private encodeText(text: string, encoding: string): ArrayBuffer | string {
|
||||
switch (encoding) {
|
||||
case 'utf16le': {
|
||||
let buf = new ArrayBuffer(text.length * 4);
|
||||
let bufView = new Uint16Array(buf);
|
||||
for (let i = 0, strlen = text.length; i < strlen; i++) {
|
||||
bufView[i] = text.charCodeAt(i);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
case 'buffer2Base64': {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(text);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
case 'base64&utf16le': {
|
||||
return this.encodeText(
|
||||
this.encodeText(text, 'utf16le') as string,
|
||||
'buffer2Base64'
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return Base64.encode(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// websocket接收数据的处理
|
||||
private result(resultData: string) {
|
||||
let jsonData = JSON.parse(resultData);
|
||||
// 合成失败
|
||||
if (jsonData.code !== 0) {
|
||||
alert(`合成失败: ${jsonData.code}:${jsonData.message}`);
|
||||
console.error(`${jsonData.code}:${jsonData.message}`);
|
||||
this.resetAudio();
|
||||
return;
|
||||
}
|
||||
transWorker.postMessage(jsonData.data.audio);
|
||||
|
||||
if (jsonData.code === 0 && jsonData.data.status === 2) {
|
||||
this.ttsWS.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 重置音频数据
|
||||
private resetAudio() {
|
||||
this.audioStop();
|
||||
this.setStatus('init');
|
||||
this.audioDataOffset = 0;
|
||||
this.audioData = [];
|
||||
this.rawAudioData = [];
|
||||
this.ttsWS && this.ttsWS.close();
|
||||
clearTimeout(this.playTimeout);
|
||||
}
|
||||
|
||||
// 音频初始化
|
||||
private audioInit() {
|
||||
let AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
this.audioContext.resume();
|
||||
this.audioDataOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频播放
|
||||
private audioPlay() {
|
||||
this.setStatus('play');
|
||||
let audioData = this.audioData.slice(this.audioDataOffset);
|
||||
this.audioDataOffset += audioData.length;
|
||||
let audioBuffer = this.audioContext.createBuffer(1, audioData.length, 22050);
|
||||
let nowBuffering = audioBuffer.getChannelData(0);
|
||||
if (audioBuffer.copyToChannel) {
|
||||
audioBuffer.copyToChannel(
|
||||
new Float32Array(audioData),
|
||||
0,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < audioData.length; i++) {
|
||||
nowBuffering[i] = audioData[i];
|
||||
}
|
||||
}
|
||||
let bufferSource = this.bufferSource = this.audioContext.createBufferSource();
|
||||
bufferSource.buffer = audioBuffer;
|
||||
bufferSource.connect(this.audioContext.destination);
|
||||
bufferSource.start();
|
||||
bufferSource.onended = event => {
|
||||
if (this.status !== 'play') {
|
||||
return;
|
||||
}
|
||||
if (this.audioDataOffset < this.audioData.length) {
|
||||
this.audioPlay();
|
||||
} else {
|
||||
this.audioStop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 音频播放结束
|
||||
private audioStop() {
|
||||
this.setStatus('endPlay');
|
||||
clearTimeout(this.playTimeout);
|
||||
this.audioDataOffset = 0;
|
||||
if (this.bufferSource) {
|
||||
try {
|
||||
this.bufferSource.stop();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.audioData.length) {
|
||||
this.audioPlay();
|
||||
} else {
|
||||
if (!this.audioContext) {
|
||||
this.audioInit();
|
||||
}
|
||||
if (!this.audioContext) {
|
||||
alert('该浏览器不支持webAudioApi相关接口');
|
||||
return;
|
||||
}
|
||||
this.connectWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.audioStop();
|
||||
}
|
||||
}
|
||||
|
||||
export default TTSRecorder;
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* base64.js
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License.
|
||||
* http://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* References:
|
||||
* http://en.wikipedia.org/wiki/Base64
|
||||
*/
|
||||
;(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined'
|
||||
? module.exports = factory(global)
|
||||
: typeof define === 'function' && define.amd
|
||||
? define(factory) : factory(global)
|
||||
}((
|
||||
typeof self !== 'undefined' ? self
|
||||
: typeof window !== 'undefined' ? window
|
||||
: typeof global !== 'undefined' ? global
|
||||
: this
|
||||
), function(global) {
|
||||
'use strict';
|
||||
// existing version for noConflict()
|
||||
global = global || {};
|
||||
var _Base64 = global.Base64;
|
||||
var version = "2.5.1";
|
||||
// if node.js and NOT React Native, we use Buffer
|
||||
var buffer;
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
try {
|
||||
buffer = eval("require('buffer').Buffer");
|
||||
} catch (err) {
|
||||
buffer = undefined;
|
||||
}
|
||||
}
|
||||
// constants
|
||||
var b64chars
|
||||
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
var b64tab = function(bin) {
|
||||
var t = {};
|
||||
for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i;
|
||||
return t;
|
||||
}(b64chars);
|
||||
var fromCharCode = String.fromCharCode;
|
||||
// encoder stuff
|
||||
var cb_utob = function(c) {
|
||||
if (c.length < 2) {
|
||||
var cc = c.charCodeAt(0);
|
||||
return cc < 0x80 ? c
|
||||
: cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
|
||||
+ fromCharCode(0x80 | (cc & 0x3f)))
|
||||
: (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
} else {
|
||||
var cc = 0x10000
|
||||
+ (c.charCodeAt(0) - 0xD800) * 0x400
|
||||
+ (c.charCodeAt(1) - 0xDC00);
|
||||
return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
|
||||
+ fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
}
|
||||
};
|
||||
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
|
||||
var utob = function(u) {
|
||||
return u.replace(re_utob, cb_utob);
|
||||
};
|
||||
var cb_encode = function(ccc) {
|
||||
var padlen = [0, 2, 1][ccc.length % 3],
|
||||
ord = ccc.charCodeAt(0) << 16
|
||||
| ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
|
||||
| ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
|
||||
chars = [
|
||||
b64chars.charAt( ord >>> 18),
|
||||
b64chars.charAt((ord >>> 12) & 63),
|
||||
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
|
||||
padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
|
||||
];
|
||||
return chars.join('');
|
||||
};
|
||||
var btoa = global.btoa ? function(b) {
|
||||
return global.btoa(b);
|
||||
} : function(b) {
|
||||
return b.replace(/[\s\S]{1,3}/g, cb_encode);
|
||||
};
|
||||
var _encode = buffer ?
|
||||
buffer.from && Uint8Array && buffer.from !== Uint8Array.from
|
||||
? function (u) {
|
||||
return (u.constructor === buffer.constructor ? u : buffer.from(u))
|
||||
.toString('base64')
|
||||
}
|
||||
: function (u) {
|
||||
return (u.constructor === buffer.constructor ? u : new buffer(u))
|
||||
.toString('base64')
|
||||
}
|
||||
: function (u) { return btoa(utob(u)) }
|
||||
;
|
||||
var encode = function(u, urisafe) {
|
||||
return !urisafe
|
||||
? _encode(String(u))
|
||||
: _encode(String(u)).replace(/[+\/]/g, function(m0) {
|
||||
return m0 == '+' ? '-' : '_';
|
||||
}).replace(/=/g, '');
|
||||
};
|
||||
var encodeURI = function(u) { return encode(u, true) };
|
||||
// decoder stuff
|
||||
var re_btou = new RegExp([
|
||||
'[\xC0-\xDF][\x80-\xBF]',
|
||||
'[\xE0-\xEF][\x80-\xBF]{2}',
|
||||
'[\xF0-\xF7][\x80-\xBF]{3}'
|
||||
].join('|'), 'g');
|
||||
var cb_btou = function(cccc) {
|
||||
switch(cccc.length) {
|
||||
case 4:
|
||||
var cp = ((0x07 & cccc.charCodeAt(0)) << 18)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(2)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(3)),
|
||||
offset = cp - 0x10000;
|
||||
return (fromCharCode((offset >>> 10) + 0xD800)
|
||||
+ fromCharCode((offset & 0x3FF) + 0xDC00));
|
||||
case 3:
|
||||
return fromCharCode(
|
||||
((0x0f & cccc.charCodeAt(0)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(2))
|
||||
);
|
||||
default:
|
||||
return fromCharCode(
|
||||
((0x1f & cccc.charCodeAt(0)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(1))
|
||||
);
|
||||
}
|
||||
};
|
||||
var btou = function(b) {
|
||||
return b.replace(re_btou, cb_btou);
|
||||
};
|
||||
var cb_decode = function(cccc) {
|
||||
var len = cccc.length,
|
||||
padlen = len % 4,
|
||||
n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
|
||||
| (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
|
||||
| (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
|
||||
| (len > 3 ? b64tab[cccc.charAt(3)] : 0),
|
||||
chars = [
|
||||
fromCharCode( n >>> 16),
|
||||
fromCharCode((n >>> 8) & 0xff),
|
||||
fromCharCode( n & 0xff)
|
||||
];
|
||||
chars.length -= [0, 0, 2, 1][padlen];
|
||||
return chars.join('');
|
||||
};
|
||||
var _atob = global.atob ? function(a) {
|
||||
return global.atob(a);
|
||||
} : function(a){
|
||||
return a.replace(/\S{1,4}/g, cb_decode);
|
||||
};
|
||||
var atob = function(a) {
|
||||
return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
|
||||
};
|
||||
var _decode = buffer ?
|
||||
buffer.from && Uint8Array && buffer.from !== Uint8Array.from
|
||||
? function(a) {
|
||||
return (a.constructor === buffer.constructor
|
||||
? a : buffer.from(a, 'base64')).toString();
|
||||
}
|
||||
: function(a) {
|
||||
return (a.constructor === buffer.constructor
|
||||
? a : new buffer(a, 'base64')).toString();
|
||||
}
|
||||
: function(a) { return btou(_atob(a)) };
|
||||
var decode = function(a){
|
||||
return _decode(
|
||||
String(a).replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' })
|
||||
.replace(/[^A-Za-z0-9\+\/]/g, '')
|
||||
);
|
||||
};
|
||||
var noConflict = function() {
|
||||
var Base64 = global.Base64;
|
||||
global.Base64 = _Base64;
|
||||
return Base64;
|
||||
};
|
||||
// export Base64
|
||||
global.Base64 = {
|
||||
VERSION: version,
|
||||
atob: atob,
|
||||
btoa: btoa,
|
||||
fromBase64: decode,
|
||||
toBase64: encode,
|
||||
utob: utob,
|
||||
encode: encode,
|
||||
encodeURI: encodeURI,
|
||||
btou: btou,
|
||||
decode: decode,
|
||||
noConflict: noConflict,
|
||||
__buffer__: buffer
|
||||
};
|
||||
// if ES5 is available, make Base64.extendString() available
|
||||
if (typeof Object.defineProperty === 'function') {
|
||||
var noEnum = function(v){
|
||||
return {value:v,enumerable:false,writable:true,configurable:true};
|
||||
};
|
||||
global.Base64.extendString = function () {
|
||||
Object.defineProperty(
|
||||
String.prototype, 'fromBase64', noEnum(function () {
|
||||
return decode(this)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64', noEnum(function (urisafe) {
|
||||
return encode(this, urisafe)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64URI', noEnum(function () {
|
||||
return encode(this, true)
|
||||
}));
|
||||
};
|
||||
}
|
||||
//
|
||||
// export Base64 to the namespace
|
||||
//
|
||||
if (global['Meteor']) { // Meteor.js
|
||||
Base64 = global.Base64;
|
||||
}
|
||||
// module.exports and AMD are mutually exclusive.
|
||||
// module.exports has precedence.
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports.Base64 = global.Base64;
|
||||
}
|
||||
else if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define([], function(){ return global.Base64 });
|
||||
}
|
||||
// that's it!
|
||||
return {Base64: global.Base64}
|
||||
}));
|
||||
|
||||
|
||||
|
||||
//////////////////
|
||||
// WEBPACK FOOTER
|
||||
// ./node_modules/_js-base64@2.5.1@js-base64/base64.js
|
||||
// module id = 57
|
||||
// module chunks = 2
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,362 @@
|
|||
//
|
||||
// iatRecorder.ts
|
||||
// 科大讯飞语音识别
|
||||
//
|
||||
// Created by panhong on 2023/07/20.
|
||||
//
|
||||
import CryptoES from "crypto-es"; // 科大讯飞
|
||||
const transWorker = new Worker(
|
||||
new URL("../until/transcode.worker.js", import.meta.url)
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取websocket url
|
||||
* APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取,正式使用需要后端配置返回避免泄露
|
||||
*/
|
||||
const APPID = "";
|
||||
const API_SECRET = "";
|
||||
const API_KEY = "";
|
||||
|
||||
const getWebSocketUrl = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 请求地址根据语种不同变化
|
||||
var url = "wss://iat-api.xfyun.cn/v2/iat";
|
||||
var host = "iat-api.xfyun.cn";
|
||||
var apiKey = API_KEY;
|
||||
var apiSecret = API_SECRET;
|
||||
var date = new Date().toGMTString();
|
||||
var algorithm = "hmac-sha256";
|
||||
var headers = "host date request-line";
|
||||
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
|
||||
var signatureSha = CryptoES.HmacSHA256(signatureOrigin, apiSecret);
|
||||
var signature = CryptoES.enc.Base64.stringify(signatureSha);
|
||||
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
|
||||
var authorization = btoa(authorizationOrigin);
|
||||
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
|
||||
resolve(url);
|
||||
});
|
||||
};
|
||||
|
||||
class IatRecorder {
|
||||
public status: string;
|
||||
public accent: string;
|
||||
public language: string;
|
||||
public appId: string;
|
||||
public audioData: any[];
|
||||
public resultText: string;
|
||||
public resultTextTemp: string;
|
||||
public onWillStatusChange?: (
|
||||
prevStatus: string,
|
||||
nextStatus: string
|
||||
) => void;
|
||||
public onTextChange?: (text: string) => void;
|
||||
constructor(
|
||||
{ language, accent, appId } = {} as {
|
||||
language?: string;
|
||||
accent?: string;
|
||||
appId?: string;
|
||||
}
|
||||
) {
|
||||
let self = this;
|
||||
this.status = "null";
|
||||
this.language = language || "zh_cn";
|
||||
this.accent = accent || "mandarin";
|
||||
this.appId = appId || APPID;
|
||||
// 记录音频数据
|
||||
this.audioData = [];
|
||||
// 记录听写结果
|
||||
this.resultText = "";
|
||||
// wpgs下的听写结果需要中间状态辅助记录
|
||||
this.resultTextTemp = "";
|
||||
transWorker.onmessage = function (event) {
|
||||
self.audioData.push(...event.data);
|
||||
};
|
||||
}
|
||||
// 修改录音听写状态
|
||||
setStatus(status:string) {
|
||||
this.onWillStatusChange && this.status !== status && this.onWillStatusChange(this.status, status)
|
||||
this.status = status
|
||||
}
|
||||
// setStatus({ status, resultText, resultTextTemp }:{ string; resultText?: string; resultTextTemp?: string}) {
|
||||
// this.onWillStatusChange && this.status !== status && this.onWillStatusChange(this.status, status, resultTextTemp || resultText || '')
|
||||
// this.status = status
|
||||
// resultText !== undefined && (this.resultText = resultText)
|
||||
// resultTextTemp !== undefined && (this.resultTextTemp = resultTextTemp)
|
||||
// }
|
||||
setResultText({ resultText, resultTextTemp }:{ resultText?: string; resultTextTemp?: string } = {}) {
|
||||
this.onTextChange && this.onTextChange(resultTextTemp || resultText || '')
|
||||
resultText !== undefined && (this.resultText = resultText)
|
||||
resultTextTemp !== undefined && (this.resultTextTemp = resultTextTemp)
|
||||
}
|
||||
|
||||
// 修改听写参数
|
||||
setParams({ language, accent }:{language?:string; accent?:string} = {}) {
|
||||
language && (this.language = language)
|
||||
accent && (this.accent = accent)
|
||||
}
|
||||
|
||||
// 连接websocket
|
||||
connectWebSocket() {
|
||||
return getWebSocketUrl().then(url => {
|
||||
let iatWS
|
||||
if ('WebSocket' in window) {
|
||||
iatWS = new WebSocket(url)
|
||||
} else if ('MozWebSocket' in window) {
|
||||
iatWS = new MozWebSocket(url)
|
||||
} else {
|
||||
alert('浏览器不支持WebSocket')
|
||||
return
|
||||
}
|
||||
this.webSocket = iatWS
|
||||
this.setStatus('init')
|
||||
iatWS.onopen = e => {
|
||||
this.setStatus('ing')
|
||||
// 重新开始录音
|
||||
setTimeout(() => {
|
||||
this.webSocketSend()
|
||||
}, 500)
|
||||
}
|
||||
iatWS.onmessage = e => {
|
||||
this.result(e.data)
|
||||
}
|
||||
iatWS.onerror = e => {
|
||||
console.log(`${e.code}`,'onerroronerroronerror')
|
||||
this.recorderStop()
|
||||
}
|
||||
iatWS.onclose = e => {
|
||||
console.log(`${e.code}`,'oncloseonclose')
|
||||
this.recorderStop()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化浏览器录音
|
||||
recorderInit() {
|
||||
navigator.getUserMedia =
|
||||
navigator.getUserMedia ||
|
||||
navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia
|
||||
|
||||
// 创建音频环境
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
|
||||
this.audioContext.resume()
|
||||
if (!this.audioContext) {
|
||||
alert('浏览器不支持webAudioApi相关接口')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.audioContext) {
|
||||
alert('浏览器不支持webAudioApi相关接口')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取浏览器录音权限
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({
|
||||
audio: true,
|
||||
video: false,
|
||||
})
|
||||
.then(stream => {
|
||||
getMediaSuccess(stream)
|
||||
})
|
||||
.catch(e => {
|
||||
getMediaFail(e)
|
||||
})
|
||||
} else if (navigator.getUserMedia) {
|
||||
navigator.getUserMedia(
|
||||
{
|
||||
audio: true,
|
||||
video: false,
|
||||
},
|
||||
stream => {
|
||||
getMediaSuccess(stream)
|
||||
},
|
||||
function (e) {
|
||||
getMediaFail(e)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (navigator.userAgent.toLowerCase().match(/chrome/) && location.origin.indexOf('https://') < 0) {
|
||||
alert('chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限')
|
||||
} else {
|
||||
alert('无法获取浏览器录音功能,请升级浏览器或使用chrome')
|
||||
}
|
||||
this.audioContext && this.audioContext.close()
|
||||
return
|
||||
}
|
||||
// 获取浏览器录音权限成功的回调
|
||||
let getMediaSuccess = stream => {
|
||||
console.log('getMediaSuccess')
|
||||
// 创建一个用于通过JavaScript直接处理音频
|
||||
this.scriptProcessor = this.audioContext.createScriptProcessor(0, 1, 1)
|
||||
this.scriptProcessor.onaudioprocess = e => {
|
||||
// 去处理音频数据
|
||||
if (this.status === 'ing') {
|
||||
transWorker.postMessage(e.inputBuffer.getChannelData(0))
|
||||
}
|
||||
}
|
||||
// 创建一个新的MediaStreamAudioSourceNode 对象,使来自MediaStream的音频可以被播放和操作
|
||||
this.mediaSource = this.audioContext.createMediaStreamSource(stream)
|
||||
// 连接
|
||||
this.mediaSource.connect(this.scriptProcessor)
|
||||
this.scriptProcessor.connect(this.audioContext.destination)
|
||||
this.connectWebSocket()
|
||||
}
|
||||
|
||||
let getMediaFail = (e) => {
|
||||
alert('请求麦克风失败')
|
||||
console.log(e)
|
||||
this.audioContext && this.audioContext.close()
|
||||
this.audioContext = undefined
|
||||
// 关闭websocket
|
||||
if (this.webSocket && this.webSocket.readyState === 1) {
|
||||
this.webSocket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
recorderStart() {
|
||||
if (!this.audioContext) {
|
||||
this.recorderInit()
|
||||
} else {
|
||||
this.audioContext.resume()
|
||||
this.connectWebSocket()
|
||||
}
|
||||
}
|
||||
// 暂停录音
|
||||
recorderStop() {
|
||||
// safari下suspend后再次resume录音内容将是空白,设置safari下不做suspend
|
||||
if (!(/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgen))) {
|
||||
this.audioContext && this.audioContext.suspend()
|
||||
}
|
||||
this.setStatus('end')
|
||||
}
|
||||
// 处理音频数据
|
||||
// transAudioData(audioData) {
|
||||
// audioData = transAudioData.transaction(audioData)
|
||||
// this.audioData.push(...audioData)
|
||||
// }
|
||||
// 对处理后的音频数据进行base64编码,
|
||||
toBase64(buffer) {
|
||||
var binary = ''
|
||||
var bytes = new Uint8Array(buffer)
|
||||
var len = bytes.byteLength
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return window.btoa(binary)
|
||||
}
|
||||
// 向webSocket发送数据
|
||||
webSocketSend() {
|
||||
if (this.webSocket.readyState !== 1) {
|
||||
return
|
||||
}
|
||||
let audioData = this.audioData.splice(0, 1280)
|
||||
var params = {
|
||||
common: {
|
||||
app_id: this.appId,
|
||||
},
|
||||
business: {
|
||||
language: this.language, //小语种可在控制台--语音听写(流式)--方言/语种处添加试用
|
||||
domain: 'iat',
|
||||
accent: this.accent, //中文方言可在控制台--语音听写(流式)--方言/语种处添加试用
|
||||
vad_eos: 5000,
|
||||
dwa: 'wpgs', //为使该功能生效,需到控制台开通动态修正功能(该功能免费)
|
||||
},
|
||||
data: {
|
||||
status: 0,
|
||||
format: 'audio/L16;rate=16000',
|
||||
encoding: 'raw',
|
||||
audio: this.toBase64(audioData),
|
||||
},
|
||||
}
|
||||
console.log(audioData, 'audioData')
|
||||
this.webSocket.send(JSON.stringify(params))
|
||||
this.handlerInterval = setInterval(() => {
|
||||
// websocket未连接
|
||||
if (this.webSocket.readyState !== 1) {
|
||||
this.audioData = []
|
||||
clearInterval(this.handlerInterval)
|
||||
return
|
||||
}
|
||||
if (this.audioData.length === 0) {
|
||||
if (this.status === 'end') {
|
||||
this.webSocket.send(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
status: 2,
|
||||
format: 'audio/L16;rate=16000',
|
||||
encoding: 'raw',
|
||||
audio: '',
|
||||
},
|
||||
})
|
||||
)
|
||||
this.audioData = []
|
||||
clearInterval(this.handlerInterval)
|
||||
}
|
||||
return false
|
||||
}
|
||||
audioData = this.audioData.splice(0, 1280)
|
||||
// 中间帧
|
||||
this.webSocket.send(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
status: 1,
|
||||
format: 'audio/L16;rate=16000',
|
||||
encoding: 'raw',
|
||||
audio: this.toBase64(audioData),
|
||||
},
|
||||
})
|
||||
)
|
||||
}, 40)
|
||||
}
|
||||
result(resultData) {
|
||||
// 识别结束
|
||||
let jsonData = JSON.parse(resultData)
|
||||
if (jsonData.data && jsonData.data.result) {
|
||||
let data = jsonData.data.result
|
||||
let str = ''
|
||||
let resultStr = ''
|
||||
let ws = data.ws
|
||||
for (let i = 0; i < ws.length; i++) {
|
||||
str = str + ws[i].cw[0].w
|
||||
}
|
||||
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
|
||||
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
|
||||
if (data.pgs) {
|
||||
if (data.pgs === 'apd') {
|
||||
// 将resultTextTemp同步给resultText
|
||||
this.setResultText({
|
||||
resultText: this.resultTextTemp,
|
||||
})
|
||||
}
|
||||
// 将结果存储在resultTextTemp中
|
||||
this.setResultText({
|
||||
resultTextTemp: this.resultText + str,
|
||||
})
|
||||
} else {
|
||||
this.setResultText({
|
||||
resultText: this.resultText + str,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (jsonData.code === 0 && jsonData.data.status === 2) {
|
||||
this.webSocket.close()
|
||||
}
|
||||
if (jsonData.code !== 0) {
|
||||
this.webSocket.close()
|
||||
console.log(`${jsonData.code}:${jsonData.message}`)
|
||||
}
|
||||
}
|
||||
start() {
|
||||
this.recorderStart()
|
||||
this.setResultText({ resultText: '', resultTextTemp: '' })
|
||||
}
|
||||
stop() {
|
||||
this.recorderStop()
|
||||
}
|
||||
}
|
||||
export default IatRecorder
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
|
||||
self.onmessage = function(e){
|
||||
transAudioData.transcode(e.data)
|
||||
}
|
||||
let transAudioData = {
|
||||
transcode(audioData) {
|
||||
let output = transAudioData.to16kHz(audioData)
|
||||
output = transAudioData.to16BitPCM(output)
|
||||
output = Array.from(new Uint8Array(output.buffer))
|
||||
self.postMessage(output)
|
||||
// return output
|
||||
},
|
||||
to16kHz(audioData) {
|
||||
var data = new Float32Array(audioData)
|
||||
var fitCount = Math.round(data.length * (16000 / 44100))
|
||||
var newData = new Float32Array(fitCount)
|
||||
var springFactor = (data.length - 1) / (fitCount - 1)
|
||||
newData[0] = data[0]
|
||||
for (let i = 1; i < fitCount - 1; i++) {
|
||||
var tmp = i * springFactor
|
||||
var before = Math.floor(tmp).toFixed()
|
||||
var after = Math.ceil(tmp).toFixed()
|
||||
var atPoint = tmp - before
|
||||
newData[i] = data[before] + (data[after] - data[before]) * atPoint
|
||||
}
|
||||
newData[fitCount - 1] = data[data.length - 1]
|
||||
return newData
|
||||
},
|
||||
to16BitPCM(input) {
|
||||
var dataLength = input.length * (16 / 8)
|
||||
var dataBuffer = new ArrayBuffer(dataLength)
|
||||
var dataView = new DataView(dataBuffer)
|
||||
var offset = 0
|
||||
for (var i = 0; i < input.length; i++, offset += 2) {
|
||||
var s = Math.max(-1, Math.min(1, input[i]))
|
||||
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
|
||||
}
|
||||
return dataView
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue