This commit is contained in:
2025-04-20 18:46:48 +08:00
parent e66f7ce00e
commit a696bc3bbe
34 changed files with 9681 additions and 328 deletions

View File

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

View File

@@ -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 />);

View 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>
);
};

View 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-n1为按播放速率发送最慢识别精度完美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);
}
}

View 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()+10001秒后再监控
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);
},
),
);
}
}