需求:需要录音的文本可分为多段和单个,我们可以进行上传录音和在线录音的形式生成最终的录音。
在这里用到了自定义Audio,Audio代码在我的博客里有,博客地址是:https://blog.csdn.net/qq_40657321/article/details/120329405
其中在线录音用到了js-audio-recorder包,详情链接:https://www.npmjs.com/package/js-audio-recorder/v/0.2.3
在线录音参考文章:
1.在线录音演示地址:https://recorder.zhuyuntao.cn/
2.在线录音代码:https://github.com/2fps/recorder
3.在线录音实现:https://www.jb51.net/article/159849.htm
上传录音
1.1 上传录音(单个)
上传之前
上传后:生成录音并且可删除
flag是表示文件是否上传的标志
{
!detailItem?.voiceItems[0]?.ossPath ?
<div className={
styles.upload_btn}>
//上传文件
<Upload
action={
`url 上传接口`}
headers={
{
_security_token:
((token || Cookie.get('token的key')) as string) ||
getState().global.token,
}}
data={
() => ({
// 除了file额外的参数
id: detail?.id,
type: 1,
})}
showUploadList={
false} // 不显示上传后的文件
onChange={
(data: any) => handleUpload(data, 0)}
maxCount={
1} // 最大上传个数1个
>
<Button className={
styles.btn}>上传文件</Button>
</Upload>
<div className={
styles.tit}>
<span>支持拓展名:mp3/wav,大小不要超过10M</span>
</div>
</div> : detailItem?.voiceItems[0]?.flag ? <div className={
styles.upload_btn}>
//上传后
//自定义Audio,代码在文章开头的链接里
<Audio
src={
path} // 文件路径
id={
detailItem.id + '12'}
type={
'delete'}
onClick={
() => deleteRecord(0)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
</div>
1.2 上传录音(多个)
部分上传成功:生成录音并且可删除,其他未上传文件可继续上传
全部上传后:生成录音并且可逐个删除
在线录音
2.1 在线录音(单个)
录音初始状态:点击按钮可开始录音
录音中:点击按钮暂停录音并且生成录音
录音结束后:生成录音并且可删除
<div className={
styles.online_btn}>
{
(!detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => startRecording(0)}>
<img src={
play} className={
styles.btn_img} />
开始录音
</Button>
</div> : (detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => stopRecording(0)}>
<img src={
pause} className={
styles.btn_img} ></img>
{
detailItem?.voiceItems[0].timeValue || '00:00'}
</Button>
</div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
styles.upload_btn}>
<Audio
src={
detailItem?.voiceItems[0]?.ossPath}
id={
detailItem.id + '11'}
type={
'delete'}
onClick={
() => deleteRecord(0)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
</div>
2.2 在线录音(多个)
部分录音状态:点击按钮可开始录音,点击删除可删除录音,重新录音
全部录音完成:点击删除按钮,删除当前录音,可重新开始录音
多个分段(数字转汉字)
拿到的数据是一个数组,第0个录音数据,是第一段,依次递增展示。
所以需要把下标+1,并且转成汉字。例子如下:
1=》一
12=》一十二
上代码:
//将小数部分的数字转换为字符串的方法:
var chnNumChar = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
var chnUnitSection = ["", "万", "亿", "万亿", "亿亿"];
var chnUnitChar = ["", "十", "百", "千"];
//定义在每个小节的内部进行转化的方法,其他部分则与小节内部转化方法相同
const sectionToChinese = (section: number) => {
var str = '', chnstr = '', zero = false, count = 0; //zero为是否进行补零, 第一次进行取余由于为个位数,默认不补零
while (section > 0) {
var v = section % 10; //对数字取余10,得到的数即为个位数
if (v == 0) {
//如果数字为零,则对字符串进行补零
if (zero) {
zero = false; //如果遇到连续多次取余都是0,那么只需补一个零即可
chnstr = chnNumChar[v] + chnstr;
}
} else {
zero = true; //第一次取余之后,如果再次取余为零,则需要补零
str = chnNumChar[v];
str += chnUnitChar[count];
chnstr = str + chnstr;
}
count++;
section = Math.floor(section / 10);
}
return chnstr;
}
//定义整个数字全部转换的方法,需要依次对数字进行10000为单位的取余,然后分成小节,按小节计算,当每个小节的数不足1000时,则需要进行补零
const TransformToChinese = (num: number) => {
num = Math.floor(num);
var unitPos = 0;
var strIns = '', chnStr = '';
var needZero = false;
if (num === 0) {
return chnNumChar[0];
}
while (num > 0) {
var section = num % 10000;
if (needZero) {
chnStr = chnNumChar[0] + chnStr;
}
strIns = sectionToChinese(section);
strIns += (section !== 0) ? chnUnitSection[unitPos] : chnUnitSection[0];
chnStr = strIns + chnStr;
needZero = (section < 1000) && (section > 0);
num = Math.floor(num / 10000);
unitPos++;
}
return chnStr;
}
完整代码
import React, {
useEffect, useState } from 'react';
import {
Modal, Form, Button, Upload, message } from 'antd';
import {
getState, } from '@@/store';
import {
CONFIG } from '@/services';
import Cookie from 'js-cookie';
import util from '@souche-f2e/souche-util';
import {
TVoiceSave, TConfigDetail } from '../types';
import styles from './index.less'
import pause from '@/assets/images/pause.png';
import play from '@/assets/images/play.png';
// import { default as Recorder } from '@/utils/record';
// import { Recorder } from '@/utils';
import Recorder from 'js-audio-recorder';
import Audio from '@/components/Audio';
type ISyncConfigModalProps = {
visible: boolean;
detail: TConfigDetail | null;
loading: boolean;
uploadType: string;
onCancel: () => void;
onConfirm: (data: TVoiceSave) => void;
};
const token = util.getParams().token;
let recorder: any = null;
//将小数部分的数字转换为字符串的方法:
var chnNumChar = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
var chnUnitSection = ["", "万", "亿", "万亿", "亿亿"];
var chnUnitChar = ["", "十", "百", "千"];
const SyncConfigModal: React.FC<ISyncConfigModalProps> = ({
visible,
loading,
onCancel,
onConfirm,
detail,
uploadType
}) => {
const [form] = Form.useForm();
const [detailItem, setDetailItem] = useState<TConfigDetail | null>(null);
const [lastIndex, setLastIndex] = useState<any>(null);
useEffect(() => {
if (!visible) {
form.resetFields();
setDetailItem(null);
} else {
setLastIndex(null);
if (visible && detail && detail?.voiceItems?.length > 1) {
detail.voiceItems = detail?.voiceItems?.filter(v => !v.paramName)
setDetailItem(detail);
} else {
setDetailItem(detail);
}
}
}, [visible]);
// const handleFormSubmit = (values: { groupId: string; syncType: '1' | '2' }) => {
// onConfirm({
// ...values,
// });
// };
//定义在每个小节的内部进行转化的方法,其他部分则与小节内部转化方法相同
const sectionToChinese = (section: number) => {
var str = '', chnstr = '', zero = false, count = 0; //zero为是否进行补零, 第一次进行取余由于为个位数,默认不补零
while (section > 0) {
var v = section % 10; //对数字取余10,得到的数即为个位数
if (v == 0) {
//如果数字为零,则对字符串进行补零
if (zero) {
zero = false; //如果遇到连续多次取余都是0,那么只需补一个零即可
chnstr = chnNumChar[v] + chnstr;
}
} else {
zero = true; //第一次取余之后,如果再次取余为零,则需要补零
str = chnNumChar[v];
str += chnUnitChar[count];
chnstr = str + chnstr;
}
count++;
section = Math.floor(section / 10);
}
return chnstr;
}
//定义整个数字全部转换的方法,需要依次对数字进行10000为单位的取余,然后分成小节,按小节计算,当每个小节的数不足1000时,则需要进行补零
const TransformToChinese = (num: number) => {
num = Math.floor(num);
var unitPos = 0;
var strIns = '', chnStr = '';
var needZero = false;
if (num === 0) {
return chnNumChar[0];
}
while (num > 0) {
var section = num % 10000;
if (needZero) {
chnStr = chnNumChar[0] + chnStr;
}
strIns = sectionToChinese(section);
strIns += (section !== 0) ? chnUnitSection[unitPos] : chnUnitSection[0];
chnStr = strIns + chnStr;
needZero = (section < 1000) && (section > 0);
num = Math.floor(num / 10000);
unitPos++;
}
return chnStr;
}
const handleUpload = (data: any, index: number) => {
if (data.fileList[0].status === 'done') {
if (data.file.response.code === '200' && detailItem) {
let list = {
...detailItem };
const item = {
...list.voiceItems[index] }
console.log(item, 'item---')
list.voiceItems[index] = {
...item, ...data.file.response.data }|| {
};
console.log(list.voiceItems[index], 'list.voiceItems[index]---')
setDetailItem(list);
}
}
};
const transferTime = (time: number) => {
let min: number | string = Math.floor(time / 60);
if (min < 10) {
min = `0${
min}`;
}
let sec: number | string = Math.floor((time) % 60);
if (sec < 10) {
sec = `0${
sec}`;
}
return `${
min}:${
sec}`;
}
// 开始录音
const startRecording = (index: number) => {
if (detailItem && detailItem?.voiceItems) {
let list = {
...detailItem };
list.voiceItems[index].audioFlag = true;
// 播放其他的停止播放
if (lastIndex !== null && lastIndex !== index) {
console.log(lastIndex)
list.voiceItems[lastIndex].audioFlag = false;
list.voiceItems[lastIndex].timeValue = "00:00"; // 时间重置
setDetailItem(list);
stopRecording(lastIndex); // 销毁
}
create(index);
}
setLastIndex(index);
}
const create = (index: number) => {
recorder = new Recorder({
sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
numChannels: 1,
})
recorder.start().then(() => {
// 开始录音
recorder.onprogress = function (params: any) {
if (detailItem && detailItem?.voiceItems) {
let list = {
...detailItem };
list.voiceItems[index].timeValue = transferTime(params?.duration);
setDetailItem(list);
}
}
}, (error: any) => {
// 出错了
console.log(`${
error.name} : ${
error.message}`);
});
}
// 停止录音
const stopRecording = (index: number) => {
recorder && recorder.stop()
if (detailItem && detailItem?.voiceItems) {
let list = {
...detailItem };
list.voiceItems[index].audioFlag = false;
setDetailItem(list);
}
createDownloadLink(index)
}
// 删除录音
const deleteRecord = (index: number) => {
if (detailItem && detailItem?.voiceItems) {
let list = {
...detailItem };
list.voiceItems[index].audioFlag = false;
list.voiceItems[index].ossPath = '';
setDetailItem(list);
}
}
// 生成文件
const createDownloadLink = async (index: number) => {
const blob = recorder.getWAVBlob()
console.log('b;ol', blob);
let formDate = new FormData();
formDate.append("file", blob, "blob.wav");
formDate.append("serviceId", detailItem?.id || '');
formDate.append("serviceModel", "1");
try {
const res = await CONFIG.uploadFile(formDate);
if (res) {
if (detailItem && detailItem?.voiceItems) {
let list = {
...detailItem };
const item = {
...list.voiceItems[index] }
console.log(item, 'item---')
list.voiceItems[index] = {
...item, ...res, duration: undefined } || {
};
console.log(list.voiceItems[index], 'list.voiceItems[index]---')
setDetailItem(list);
}
}
} catch (e) {
}
}
// 保存
const handleOk = async () => {
const params = {
id: detailItem?.id || '',
voiceItems: detailItem?.voiceItems || [],
// voiceType: uploadType === 'upload'? "UPLOAD" : 'ONLINE'
}
onConfirm(params);
}
return (
<Modal
visible={
visible}
title={
uploadType === 'upload' ? "上传录音" : "在线录音"}
onOk={
handleOk}
onCancel={
onCancel}
confirmLoading={
loading}
width={
560}
className={
styles.modal_height}
>
{
detailItem?.voiceItems?.length === 1 ?
<div className={
styles.upload_one}>
<h4>话术内容</h4>
<p>{
detailItem?.voiceItems[0]?.text}</p>
{
uploadType === 'upload' ? <div>
{
!detailItem?.voiceItems[0]?.ossPath ? <div className={
styles.upload_btn}>
<Upload
action={
`${
process.env.MUJI_APP_CUSTOMER_SYSTEM_SERVER}/speechVoiceController/uploadFile.json`}
headers={
{
_security_token:
((token || Cookie.get('_security_token_ai')) as string) ||
getState().global.token,
}}
data={
() => ({
serviceId: detail?.id,
serviceModel: 1,
})}
showUploadList={
false}
onChange={
(data: any) => handleUpload(data, 0)}
maxCount={
1}
>
<Button className={
styles.btn}>上传文件</Button>
</Upload>
<div className={
styles.tit}>
<span>支持拓展名:mp3/wav,大小不要超过10M</span>
</div>
</div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
styles.upload_btn}>
<Audio
src={
detailItem?.voiceItems[0]?.ossPath}
id={
detailItem.id + '12'}
type={
'delete'}
onClick={
() => deleteRecord(0)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
</div> :
<div className={
styles.online_btn}>
{
(!detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => startRecording(0)}>
<img src={
play} className={
styles.btn_img} />
开始录音
</Button>
</div> : (detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => stopRecording(0)}>
<img src={
pause} className={
styles.btn_img} ></img>
{
detailItem?.voiceItems[0].timeValue || '00:00'}
</Button>
</div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
styles.upload_btn}>
<Audio
src={
detailItem?.voiceItems[0]?.ossPath}
id={
detailItem.id + '11'}
type={
'delete'}
onClick={
() => deleteRecord(0)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
</div>
}
</div> :
<div>
{
detailItem?.voiceItems?.map((v: any, i: number) => {
return (
<div key={
i}>
{
<div className={
styles.upload_one} key={
i} >
<h4>第{
TransformToChinese(i + 1)}段</h4>
<p>{
v?.text}</p>
{
(uploadType === 'upload' && !v?.ossPath) ? <div className={
styles.upload_btn}>
<Upload
action={
`${
process.env.MUJI_APP_CUSTOMER_SYSTEM_SERVER}/speechVoiceController/uploadFile.json`}
headers={
{
_security_token:
((token || Cookie.get('_security_token_ai')) as string) ||
getState().global.token,
}}
data={
() => ({
serviceId: detail?.id,
serviceModel: 1,
})}
showUploadList={
false}
onChange={
(data: any) => handleUpload(data, i)}
maxCount={
1}
>
<Button className={
styles.btn}>上传文件</Button>
</Upload>
<div className={
styles.tit}>
<p>含有变量的话术需要分段录入</p>
<p> 支持拓展名:mp3/wav,大小不要超过10M</p>
</div>
</div> : (uploadType === 'upload' && v?.ossPath) ? <div className={
styles.upload_btn}>
<Audio
src={
v?.ossPath}
id={
i}
type={
'delete'}
onClick={
() => deleteRecord(i)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
{
(uploadType === 'online' && !v?.audioFlag && !v?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => startRecording(i)}>
<img src={
play} className={
styles.btn_img} />
开始录音
</Button>
<div className={
styles.tit}>
<div>含有变量的话术需要分段录入</div>
</div>
</div> : (uploadType === 'online' && v?.audioFlag && !v?.ossPath) ? <div className={
styles.upload_btn}>
<Button className={
styles.btn} onClick={
() => stopRecording(i)}>
<img src={
pause} className={
styles.btn_img} ></img>
{
v.timeValue || '00:00'}
</Button>
<div className={
styles.tit}>
<div>含有变量的话术需要分段录入</div>
</div>
</div> :
(uploadType === 'online' && v?.ossPath) ? <div className={
styles.upload_btn}>
<Audio
src={
v?.ossPath}
id={
i}
type={
'delete'}
onClick={
() => deleteRecord(i)}
bg={
'#F2F3F5'}
/>
</div> : ''
}
</div>
}
</div>
)
})
}
</div>
}
</Modal >
);
};
export default SyncConfigModal;