generated from template/vite-react-template
temp
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
// import Recorder from 'recorder-core/recorder.mp3.min'; //已包含recorder-core和mp3格式支持
|
||||
// import 'recorder-core/src/extensions/waveview'
|
||||
|
||||
// Recorder.a = 1;
|
||||
// @ts-ignore
|
||||
const Recorder = window.Recorder;
|
||||
export { Recorder };
|
||||
|
||||
10
src/main.tsx
10
src/main.tsx
@@ -1,10 +1,4 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App, AppRoute } from './pages/App.tsx';
|
||||
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
|
||||
import { RecordInfo } from './pages/record';
|
||||
|
||||
console.log('cu',)
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<CustomThemeProvider>
|
||||
<AppRoute />
|
||||
</CustomThemeProvider>,
|
||||
);
|
||||
createRoot(document.getElementById('root')!).render(<RecordInfo />);
|
||||
|
||||
82
src/pages/record/index.tsx
Normal file
82
src/pages/record/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MyRecorder } from './module/MyRecorder';
|
||||
import { AliAsr } from './module/AliAsr';
|
||||
export const RecordInfo = () => {
|
||||
const recorderRef = useRef<MyRecorder | null>(null);
|
||||
const asrRef = useRef<AliAsr | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
const init = async () => {
|
||||
recorderRef.current = new MyRecorder();
|
||||
asrRef.current = new AliAsr();
|
||||
asrRef.current?.init();
|
||||
const open = await recorderRef.current?.init();
|
||||
if (open) {
|
||||
setMounted(true);
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
const startRecord = () => {
|
||||
recorderRef.current?.startRecord();
|
||||
};
|
||||
// asrRef.current?.start(startRecord, (msg) => {
|
||||
// console.log('start fail', msg);
|
||||
// });
|
||||
startRecord();
|
||||
};
|
||||
const stop = async () => {
|
||||
const stopRecord = () => {
|
||||
recorderRef.current?.stopRecord();
|
||||
};
|
||||
|
||||
const blob = await recorderRef.current?.stopRecord();
|
||||
if (blob !== false) {
|
||||
asrRef.current?.asr?.audioToText(
|
||||
blob,
|
||||
(text) => {
|
||||
console.log('text', text);
|
||||
setText(text);
|
||||
},
|
||||
(msg) => {
|
||||
console.log('text fail', msg);
|
||||
},
|
||||
);
|
||||
}
|
||||
console.log('stop');
|
||||
// asrRef.current?.stop(
|
||||
// (text, abortMsg) => {
|
||||
// console.log('stop', text, abortMsg);
|
||||
// setText(text);
|
||||
// stopRecord();
|
||||
// },
|
||||
// (msg) => {
|
||||
// console.log('stop fail', msg);
|
||||
// stopRecord();
|
||||
// },
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-5'>
|
||||
<div className='flex py-4'>
|
||||
<button
|
||||
disabled={!mounted}
|
||||
onClick={start}
|
||||
className='btn btn-primary px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 active:bg-blue-700 transition-colors duration-200 text-white font-medium mr-4 cursor-pointer'>
|
||||
start
|
||||
</button>
|
||||
<button
|
||||
disabled={!mounted}
|
||||
onClick={stop}
|
||||
className='btn btn-danger px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 active:bg-red-700 transition-colors duration-200 text-white font-medium cursor-pointer'>
|
||||
stop
|
||||
</button>
|
||||
</div>
|
||||
<div className='mt-4'>{text}</div>
|
||||
<div className='recwave w-[100px] h-[100px] border p-2'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
95
src/pages/record/module/AliAsr.ts
Normal file
95
src/pages/record/module/AliAsr.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Recorder } from '../../../app';
|
||||
|
||||
/** Token的请求结果 */
|
||||
export type ApiShortResult = {
|
||||
c: number;
|
||||
m: string;
|
||||
v: {
|
||||
appkey: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** 阿里云一句话识别配置 */
|
||||
export class AliAsr {
|
||||
asr?: any;
|
||||
constructor() {}
|
||||
init() {
|
||||
this.asr = this.create();
|
||||
}
|
||||
create() {
|
||||
const asr = Recorder.ASR_Aliyun_Short({
|
||||
tokenApi: '/token',
|
||||
apiArgs: {
|
||||
//请求tokenApi时要传的参数
|
||||
action: 'token',
|
||||
lang: '普通话', //语言模型设置(具体取值取决于tokenApi支持了哪些语言)
|
||||
},
|
||||
compatibleWebSocket: null,
|
||||
//高级选项
|
||||
fileSpeed: 6, //单个文件识别发送速度控制,取值1-n;1:为按播放速率发送,最慢,识别精度完美;6:按六倍播放速度发送,花10秒识别60秒文件比较快,精度还行;再快测试发现似乎会缺失内容,可能是发送太快底层识别不过来导致返回的结果缺失。
|
||||
log: (msg, error) => {
|
||||
console.log('AliAsr log', msg, error);
|
||||
},
|
||||
});
|
||||
this.asr = asr;
|
||||
return asr;
|
||||
}
|
||||
/**
|
||||
* 获取输入的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getInputDuration() {
|
||||
return this.asr?.inputDuration();
|
||||
}
|
||||
/**
|
||||
* 获取已发送识别的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getSendDuration() {
|
||||
return this.asr?.sendDuration();
|
||||
}
|
||||
/**
|
||||
* 获取已识别的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getAsrDuration() {
|
||||
return this.asr?.asrDuration();
|
||||
}
|
||||
/**
|
||||
* 获取实时结果文本
|
||||
* @returns
|
||||
*/
|
||||
getText() {
|
||||
return this.asr?.getText();
|
||||
}
|
||||
/**
|
||||
* 一次性将单个完整音频Blob文件转成文字
|
||||
* @param audioBlob
|
||||
* @returns
|
||||
*/
|
||||
audioToText(audioBlob: Blob) {
|
||||
return this.asr?.audioToText(audioBlob);
|
||||
}
|
||||
/**
|
||||
* 一次性将单个完整PCM音频数据转成文字
|
||||
* @param buffer
|
||||
* @param sampleRate
|
||||
* @returns
|
||||
*/
|
||||
pcmToText(buffer: ArrayBuffer, sampleRate: number) {
|
||||
return this.asr?.pcmToText(buffer, sampleRate);
|
||||
}
|
||||
/**
|
||||
* 开始识别
|
||||
*/
|
||||
start(recordStartFn: () => void, fail?: (msg?: string) => void) {
|
||||
this.asr?.start(recordStartFn, fail);
|
||||
}
|
||||
/**
|
||||
* 停止识别
|
||||
*/
|
||||
stop(success?: (text: string, abortMsg?: string) => void, fail?: (msg?: string) => void) {
|
||||
this.asr?.stop(success, fail);
|
||||
}
|
||||
}
|
||||
111
src/pages/record/module/MyRecorder.ts
Normal file
111
src/pages/record/module/MyRecorder.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Recorder } from '../../../app';
|
||||
import { AliAsr } from './AliAsr';
|
||||
|
||||
export class MyRecorder {
|
||||
private recorder?: typeof Recorder;
|
||||
private processTime = 0;
|
||||
private wave?: any;
|
||||
asr?: AliAsr;
|
||||
blob?: Blob;
|
||||
constructor() {}
|
||||
async init(asr?: AliAsr) {
|
||||
const that = this;
|
||||
this.asr = asr;
|
||||
this.recorder = new Recorder({
|
||||
type: 'mp3',
|
||||
sampleRate: 16000,
|
||||
bitRate: 16,
|
||||
onProcess: function (buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
|
||||
//录音实时回调,大约1秒调用12次本回调,buffers为开始到现在的所有录音pcm数据块(16位小端LE)
|
||||
//可利用extensions/sonic.js插件实时变速变调,此插件计算量巨大,onProcess需要返回true开启异步模式
|
||||
//可实时上传(发送)数据,配合Recorder.SampleData方法,将buffers中的新数据连续的转换成pcm上传,或使用mock方法将新数据连续的转码成其他格式上传,可以参考文档里面的:Demo片段列表 -> 实时转码并上传-通用版;基于本功能可以做到:实时转发数据、实时保存数据、实时语音识别(ASR)等
|
||||
that.processTime = Date.now();
|
||||
//可实时绘制波形(extensions目录内的waveview.js、wavesurfer.view.js、frequency.histogram.view.js插件功能)
|
||||
that.wave && that.wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
|
||||
if (asr) {
|
||||
asr.asr?.input(buffers, bufferSampleRate, 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
const isOpen = await this.open();
|
||||
console.log('open recorder', isOpen);
|
||||
return isOpen;
|
||||
}
|
||||
async open() {
|
||||
const that = this;
|
||||
return new Promise((resolve) => {
|
||||
that.recorder?.open(
|
||||
function () {
|
||||
if (Recorder.WaveView) that.wave = Recorder.WaveView({ elem: '.recwave' });
|
||||
console.log('open success');
|
||||
resolve(true);
|
||||
},
|
||||
function (msg, isUserNotAllow) {
|
||||
//用户拒绝未授权或不支持
|
||||
console.log('open fail', msg, isUserNotAllow);
|
||||
console.log((isUserNotAllow ? 'UserNotAllow,' : '') + '无法录音:' + msg);
|
||||
resolve(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
async startRecord() {
|
||||
let that = this;
|
||||
if (!this.recorder) {
|
||||
await this.init();
|
||||
}
|
||||
this.recorder?.start();
|
||||
this.processTime = 0;
|
||||
//【稳如老狗WDT】可选的,监控是否在正常录音有onProcess回调,如果长时间没有回调就代表录音不正常
|
||||
let rec = this.recorder;
|
||||
console.log(rec);
|
||||
rec.wdtPauseT = 0;
|
||||
const startTime = Date.now();
|
||||
const wdt = setInterval(function () {
|
||||
const processTime = that.processTime;
|
||||
console.log(rec, wdt, 'processTime', processTime);
|
||||
if (!rec || wdt != rec.watchDogTimer) {
|
||||
clearInterval(wdt);
|
||||
return;
|
||||
} //sync
|
||||
if (Date.now() < rec.wdtPauseT) return; //如果暂停录音了就不检测:puase时赋值rec.wdtPauseT=Date.now()*2(永不监控),resume时赋值rec.wdtPauseT=Date.now()+1000(1秒后再监控)
|
||||
if (Date.now() - (processTime || startTime) > 1500) {
|
||||
clearInterval(wdt);
|
||||
console.error(processTime ? '录音被中断' : '录音未能正常开始');
|
||||
// ... 错误处理,关闭录音,提醒用户
|
||||
}
|
||||
}, 1000);
|
||||
rec.watchDogTimer = wdt;
|
||||
}
|
||||
async stopRecord() {
|
||||
const that = this;
|
||||
let rec = this.recorder;
|
||||
rec.watchDogTimer = 0; //停止监控onProcess超时
|
||||
return new Promise((resolve) =>
|
||||
this.recorder?.stop(
|
||||
function (blob, duration) {
|
||||
const localUrl = (window.URL || webkitURL).createObjectURL(blob);
|
||||
console.log(blob, localUrl, '时长:' + duration + 'ms');
|
||||
rec.close(); //释放录音资源,当然可以不释放,后面可以连续调用start;但不释放时系统或浏览器会一直提示在录音,最佳操作是录完就close掉
|
||||
that.recorder = null;
|
||||
//已经拿到blob文件对象想干嘛就干嘛:立即播放、上传、下载保存
|
||||
that.blob = blob;
|
||||
/*** 【立即播放例子】 ***/
|
||||
const audio = document.createElement('audio');
|
||||
document.body.prepend(audio);
|
||||
audio.controls = true;
|
||||
audio.src = localUrl;
|
||||
audio.play();
|
||||
resolve(blob);
|
||||
},
|
||||
function (msg) {
|
||||
console.log('录音失败:' + msg);
|
||||
rec.close(); //可以通过stop方法的第3个参数来自动调用close
|
||||
rec = null;
|
||||
that.recorder = null;
|
||||
resolve(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user