Merge pull request #1039 from nanayashiki1215/IFlytek/IFlytekVoice

添加语音模块
This commit is contained in:
Laurence Rotolo 2023-08-10 15:43:54 +08:00 committed by GitHub
commit 3db73d458b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1661 additions and 5 deletions

View File

@ -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",

View File

@ -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>

View File

@ -64,6 +64,7 @@ export default {
resetUserInfo: 'Reset UserInfo',
chatHistory: 'ChatHistory',
theme: 'Theme',
audiosetting: 'AudioSet',
language: 'Language',
api: 'API',
reverseProxy: 'Reverse Proxy',

View File

@ -65,6 +65,7 @@ export default {
chatHistory: '채팅 기록',
theme: '테마',
language: '언어',
audiosetting: '음성설정',
api: 'API',
reverseProxy: '리버스 프록시',
timeout: '타임아웃',

View File

@ -64,6 +64,7 @@ export default {
resetUserInfo: 'Сбросить информацию о пользователе',
chatHistory: 'История чата',
theme: 'Тема',
audiosetting: 'Настройка',
language: 'Язык',
api: 'API',
reverseProxy: 'Обратный прокси-сервер',

View File

@ -64,6 +64,7 @@ export default {
resetUserInfo: '重置用户信息',
chatHistory: '聊天记录',
theme: '主题',
audiosetting: '语音设置',
language: '语言',
api: 'API',
reverseProxy: '反向代理',

View File

@ -64,6 +64,7 @@ export default {
resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄',
theme: '主題',
audiosetting: '語音設定',
language: '語言',
api: 'API',
reverseProxy: '反向代理',

View File

@ -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 {

View File

@ -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)
},

View File

@ -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>

View File

@ -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

View File

@ -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
}

View File

@ -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)
},
}

View File

@ -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
* APPIDAPISecretAPIKey在控制台--使
*/
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
// lamemp3 (当aue=lame时需传参sfl=1)
// speex-org-wb;7 标准开源speexfor speex_wideband即16k数字代表指定压缩等级默认等级为8
// speex-org-nb;7 标准开源speexfor speex_narrowband即8k数字代表指定压缩等级默认等级为8
// speex;7压缩格式压缩等级1~10默认为78k讯飞定制speex
// speex-wb;7压缩格式压缩等级1~10默认为716k讯飞定制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;

View File

@ -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

View File

@ -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
* APPIDAPISecretAPIKey在控制台--使
*/
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

View File

@ -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
},
}