feat: chat

This commit is contained in:
wangdan-fit2cloud 2025-06-11 19:15:17 +08:00
parent 569d16d735
commit babd6d5bde
125 changed files with 77179 additions and 286 deletions

19
node_modules/.package-lock.json generated vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "MaxKB",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/recorder-core": {
"version": "1.3.25011100",
"resolved": "https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz",
"integrity": "sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==",
"license": "MIT"
},
"node_modules/vue3-menus": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vue3-menus/-/vue3-menus-1.1.2.tgz",
"integrity": "sha512-MoX87TH25fbKmmE8PwC+c2kJOSGJheP4pBR2we0RkOrfUDQg7sK+akAZSmQU8o+7dF+xVF2NfKPhoVHOhlX9wQ==",
"license": "MIT"
}
}
}

240
node_modules/recorder-core/README.md generated vendored Normal file
View File

@ -0,0 +1,240 @@
# Recorderrecorder-core 用于html5录音
GitHub: [https://github.com/xiangyuecn/Recorder](https://github.com/xiangyuecn/Recorder)
Gitee: [https://gitee.com/xiangyuecn/Recorder](https://gitee.com/xiangyuecn/Recorder)
文档和详细使用方法请参考上面两个Recorder仓库。npm recorder这个名字已被使用因此在Recorder基础上增加后缀-core就命名为recorder-core和Recorder核心文件同名。
# 如何使用
## 使用npm安装
```
npm install recorder-core
```
## 引入Recorder库
可以使用`import`、`require`、`html script`等你适合的方式来引入js文件下面的以import为主要参考其他引入方式根据文件路径自行调整一下就可以了。
``` javascript
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行recorder-core会自动往window浏览器环境或Object非浏览器环境下挂载名称为Recorder对象全局可调用Recorder
import Recorder from 'recorder-core' //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//import './你clone的目录/src/recorder-core.js' //clone源码可以按这个方式引入下同
//require('./你clone的目录/src/recorder-core.js') //clone源码可以按这个方式引入下同
//<script src="你clone的目录/src/recorder-core.js"> //htmlscript
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js的话必须要加上
//以上三个也可以合并使用压缩好的recorder.xxx.min.js
//比如 import 'recorder-core/recorder.mp3.min' //已包含recorder-core和mp3格式支持
//比如 <script src="你clone的目录/recorder.mp3.min.js">
//可选的插件支持项,把需要的插件按需引入进来即可
import 'recorder-core/src/extensions/waveview'
/****以上均为Recorder的相关文件下面是RecordApp需要的支持文件****/
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js。注意app.js会自动往window浏览器环境或Object非浏览器环境下挂载名称为RecordApp对象全局可调用RecordApp
import RecordApp from 'recorder-core/src/app-support/app'
//引入特定平台环境下的支持文件(也可以统统引入进来,非对应的环境下运行时会忽略掉)
//import 'recorder-core/src/app-support/app-native-support.js' //App下的原生录音支持文件App中未提供原生支持时可以不提供统统走H5录音
//import 'recorder-core/src/app-support/app-miniProgram-wx-support.js' //微信小程序下的录音支持文件
//import '@/uni_modules/Recorder-UniCore/app-uni-support.js' //uni-app下的支持文件请参考本文档目录下的demo_UniApp测试项目
//ts import 提示npm包内已自带了.d.ts声明文件不过是any类型
```
## Recorder调用录音
这里假设只录3秒录完后立即播放[在线编辑运行此代码>>](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?idf=self_base_demo)。录音结束后得到的是Blob二进制文件对象可以下载保存成文件、用`FileReader`读取成`ArrayBuffer`或者`Base64`给js处理或者参考下一节上传示例直接上传。
``` javascript
//简单控制台直接测试方法:在任意(无CSP限制)页面内加载需要的js加载成功后再执行一次本代码立即会有效果
//①加载Recorder+mp3await import("https://unpkg.com/recorder-core/recorder.mp3.min.js"); console.log("import ok")
//②可视化插件和显示await import("https://unpkg.com/recorder-core/src/extensions/waveview.js"); console.log("import ok"); div=document.createElement("div");div.innerHTML='<div style="height:100px;width:300px;" class="recwave"></div>';document.body.prepend(div);
var rec,processTime,wave;
/**调用open打开录音请求好录音权限**/
var recOpen=function(success){//一般在显示出录音按钮或相关的录音界面时进行此方法调用,后面用户点击开始录音时就能畅通无阻了
rec=Recorder({ //本配置参数请参考下面的文档,有详细介绍
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
,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
processTime=Date.now();
//可实时绘制波形extensions目录内的waveview.js、wavesurfer.view.js、frequency.histogram.view.js插件功能
wave&&wave.input(buffers[buffers.length-1],powerLevel,bufferSampleRate);
}
});
rec.open(function(){//打开麦克风授权获得相关资源
//rec.start() 此处可以立即开始录音但不建议这样编写因为open是一个延迟漫长的操作通过两次用户操作来分别调用open和start是推荐的最佳流程
//创建可视化指定一个要显示的div
if(Recorder.WaveView)wave=Recorder.WaveView({elem:".recwave"});
success&&success();
},function(msg,isUserNotAllow){//用户拒绝未授权或不支持
console.log((isUserNotAllow?"UserNotAllow":"")+"无法录音:"+msg);
});
};
/**开始录音**/
function recStart(){//打开了录音后才能进行start、stop调用
rec.start();
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
var wdt=rec.watchDogTimer=setInterval(function(){
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);
var startTime=Date.now(); rec.wdtPauseT=0; processTime=0;
};
/**结束录音**/
function recStop(){
rec.watchDogTimer=0; //停止监控onProcess超时
rec.stop(function(blob,duration){
//简单利用URL生成本地文件地址注意不用了时需要revokeObjectURL否则霸占内存
//此地址只能本地使用比如赋值给audio.src进行播放赋值给a.href然后a.click()进行下载a需提供download="xxx.mp3"属性)
var localUrl=(window.URL||webkitURL).createObjectURL(blob);
console.log(blob,localUrl,"时长:"+duration+"ms");
rec.close();//释放录音资源当然可以不释放后面可以连续调用start但不释放时系统或浏览器会一直提示在录音最佳操作是录完就close掉
rec=null;
//已经拿到blob文件对象想干嘛就干嘛立即播放、上传、下载保存
/*** 【立即播放例子】 ***/
var audio=document.createElement("audio");
document.body.prepend(audio);
audio.controls=true;
audio.src=localUrl;
audio.play();
},function(msg){
console.log("录音失败:"+msg);
rec.close();//可以通过stop方法的第3个参数来自动调用close
rec=null;
});
};
//这里假设立即运行只录3秒录完后立即播放本段代码copy到控制台内可直接运行
recOpen(function(){
recStart();
setTimeout(recStop,3000);
});
```
## RecordApp调用录音
RecordApp的基础调用方式在所有平台环境下是通用的但不同环境下可能会提供更多的方法、或配置参数以供使用多出来的请参考对应的平台环境支持说明。
``` javascript
/**请求录音权限Start调用前至少要调用一次RequestPermission**/
var recReq=function(success){
//RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置在h5中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.RequestPermission(function(){
//注意有使用到H5录音时为了获得最佳兼容性建议RequestPermission、Start至少有一个应当在用户操作触摸、点击等下进行调用
success&&success();
},function(msg,isUserNotAllow){//用户拒绝未授权或不支持
console.log((isUserNotAllow?"UserNotAllow":"")+"无法录音:"+msg);
});
};
/**开始录音**/
var recStart=function(success){
var processTime=0;
//开始录音的参数和Recorder的初始化参数大部分相同
RecordApp.Start({
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除打开后声音可能会从听筒播放部分环境下如小程序、uni-app原生接口可调用接口切换成扬声器外放
//注意H5中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd){
//录音实时回调大约1秒调用12次本回调buffers为开始到现在的所有录音pcm数据块(16位小端LE)
//可实时上传发送数据可实时绘制波形ASR语音识别使用可参考Recorder
processTime=Date.now();
}
//... 不同环境的专有配置,根据文档按需配置
},function(){
//开始录音成功
success&&success();
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
var this_= RecordApp; //有this就用this没有就用一个全局对象
if(RecordApp.Current.CanProcess()){
var wdt=this_.watchDogTimer=setInterval(function(){
if(wdt!=this_.watchDogTimer){ clearInterval(wdt); return } //sync
if(Date.now()<this_.wdtPauseT) return; //如果暂停录音了就不检测puase时赋值this_.wdtPauseT=Date.now()*2永不监控resume时赋值this_.wdtPauseT=Date.now()+10001秒后再监控
if(Date.now()-(processTime||startTime)>1500){ clearInterval(wdt);
console.error(processTime?"录音被中断":"录音未能正常开始");
// ... 错误处理,关闭录音,提醒用户
}
},1000);
}else{
console.warn("当前环境不支持onProcess回调不启用watchDogTimer"); //目前都支持回调
}
var startTime=Date.now(); this_.wdtPauseT=0;
},function(msg){
console.log("开始录音失败:"+msg);
});
};
//暂停录音
var recPause=function(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Pause();
var this_=RecordApp;this_.wdtPauseT=Date.now()*2; //永不监控onProcess超时
console.log("已暂停");
}
};
//继续录音
var recResume=function(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Resume();
var this_=RecordApp;this_.wdtPauseT=Date.now()+1000; //1秒后再监控onProcess超时
console.log("继续录音中...");
}
};
/**停止录音,清理资源**/
var recStop=function(){
var this_=RecordApp;this_.watchDogTimer=0; //停止监控onProcess超时
RecordApp.Stop(function(arrayBuffer,duration,mime){
//arrayBuffer就是录音文件的二进制数据不同平台环境下均可进行播放、上传
console.log(arrayBuffer,mime,"时长:"+duration+"ms");
//如果当前环境支持Blob也可以直接构造成Blob文件对象和Recorder使用一致
if(typeof(Blob)!="undefined" && typeof(window)=="object"){
var blob=new Blob([arrayBuffer],{type:mime});
console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
}
},function(msg){
console.log("录音失败:"+msg);
});
};
//这里假设立即运行只录3秒
recReq(function(){
recStart(function(){
setTimeout(recStop,3000);
});
});
```

2
node_modules/recorder-core/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,2 @@
declare let Recorder : any;
export default Recorder;

32
node_modules/recorder-core/package.json generated vendored Normal file
View File

@ -0,0 +1,32 @@
{
"name": "recorder-core",
"version": "1.3.25011100",
"description": "Recorder库: html5 js 录音 mp3 wav ogg webm amr g711a g711u 格式支持pc和Android、iOS部分浏览器、Hybrid App提供Android iOS App源码、微信提供ASR语音识别转文字 H5版语音通话聊天示例 DTMF编码解码",
"homepage": "https://github.com/xiangyuecn/Recorder",
"main": "src/recorder-core.js",
"keywords": [
"recorder",
"recordapp",
"record",
"html录音",
"h5录音",
"html5",
"mp3",
"wav",
"ASR",
"语音识别",
"语音转文字",
"DTMF",
"recording",
"webrtc"
],
"repository": {
"type": "git",
"url": "https://github.com/xiangyuecn/Recorder.git"
},
"bugs": {
"url": "https://github.com/xiangyuecn/Recorder/issues"
},
"author": "xiangyuecn",
"license": "MIT"
}

2
node_modules/recorder-core/recorder.mp3.min.d.ts generated vendored Normal file
View File

@ -0,0 +1,2 @@
declare let Recorder : any;
export default Recorder;

6
node_modules/recorder-core/recorder.mp3.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

2
node_modules/recorder-core/recorder.wav.min.d.ts generated vendored Normal file
View File

@ -0,0 +1,2 @@
declare let Recorder : any;
export default Recorder;

6
node_modules/recorder-core/recorder.wav.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,549 @@
/*
录音 RecordApp: 微信小程序支持文件支持在微信小程序环境中使用
https://github.com/xiangyuecn/Recorder
录音功能由微信小程序的RecorderManager录音接口提供已屏蔽10分钟录音限制因为js层已加载Recorder和相应的js编码引擎所以Recorder支持的录音格式小程序内均可以做到支持
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var IsWx=typeof wx=="object" && !!wx.getRecorderManager;
var App=Recorder.RecordApp;
var CLog=App.CLog;
var platform={
Support:function(call){
if(IsWx && isBrowser){ //有的h5里面有wx对象又有的wx里面有window对象
var win=window,doc=win.document,loc=win.location,body=doc.body;
if(loc && loc.href && loc.reload && body && body.appendChild){
CLog("识别是浏览器但又检测到wx",3);
call(false); return; //多判断一些稳妥一点
}
}
call(IsWx);
}
,CanProcess:function(){
return true;//支持实时回调
}
};
App.RegisterPlatform("miniProgram-wx",platform);
//当使用到录音的页面onShow时进行一次调用用于恢复被暂停的录音比如按了home键会暂停录音
App.MiniProgramWx_onShow=function(){
recOnShow();
};
/*******实现统一接口*******/
platform.RequestPermission=function(sid,success,fail){
requestPermission(success,fail);
};
platform.Start=function(sid,set,success,fail){
onRecFn.param=set;
var rec=Recorder(set);
rec.set.disableEnvInFix=true; //不要音频输入丢失补偿
rec.dataType="arraybuffer";
onRecFn.rec=rec;//等待第一个数据到来再调用rec.start
App.__Rec=rec;//App需要暴露出使用到的rec实例
recStart(success,fail);
};
platform.Stop=function(sid,success,fail){
clearCurMg();
var failCall=function(msg){
if(App.__Sync(sid)){
onRecFn.rec=null;
}
fail(msg);
};
var rec=onRecFn.rec;
onRecFn.rec=null;
var clearMsg=success?"":App.__StopOnlyClearMsg();
if(!rec){
failCall("未开始录音"+(clearMsg?" ("+clearMsg+")":""));
return;
};
CLog("rec encode: pcm:"+rec.recSize+" srcSR:"+rec.srcSampleRate+" set:"+JSON.stringify(onRecFn.param));
var end=function(){
if(App.__Sync(sid)){
//把可能变更的配置写回去
for(var k in rec.set){
onRecFn.param[k]=rec.set[k];
};
};
};
if(!success){
end();
failCall(clearMsg);
return;
};
rec.stop(function(arrBuf,duration,mime){
end();
success(arrBuf,duration,mime);
},function(msg){
end();
failCall(msg);
});
};
var onRecFn=function(pcm,sampleRate){
var rec=onRecFn.rec;
if(!rec){
CLog("未开始录音但收到wx PCM数据",3);
return;
};
if(!rec._appStart){
rec.envStart({
envName:platform.Key,canProcess:platform.CanProcess()
},sampleRate);
};
rec._appStart=1;
var sum=0;
for(var i=0;i<pcm.length;i++){
sum+=Math.abs(pcm[i]);
}
rec.envIn(pcm,sum);
};
/*******微信小程序录音接口调用*******/
var hasPermission=false;
var requestPermission=function(success,fail){
clearCurMg();
initSys();
if(hasPermission){
success(); return;
}
var mg=wx.getRecorderManager(),next=1;
mg.onStart(function(){
hasPermission=true;
if(next){ next=0;
stopMg(mg);
success();
}
});
mg.onError(function(res){
var msg="请求录音权限出现错误:"+res.errMsg;
CLog(msg+"。"+UserPermissionMsg,1,res);
if(next){ next=0;
stopMg(mg);
fail(msg,true);
}
});
newStart("req",mg);
};
var UserPermissionMsg="请自行检查wx.getSetting中的scope.record录音权限如果用户拒绝了权限请引导用户到小程序设置中授予录音权限。";
var curMg,mgStime=0;
var clearCurMg=function(){
var old=curMg;curMg=null;
if(old){ stopMg(old) }
};
var stopMg=function(mg){
mgStime=Date.now();
mg.stop();
};
var newStart=function(tag,mg){ //统一参数进行start调用不然开发工具上热更新参数不一样直接卡死
var obj={
duration:600000
,sampleRate:48000 //pc端无效
,encodeBitRate:320000
,numberOfChannels:1
,format:"PCM"
,frameSize:isDev?1:4 //4=48/12
};
var set=onRecFn.param||{},aec=(set.audioTrackSet||{}).echoCancellation;
if(sys.platform=="android"){ //Android指定麦克风源 MediaRecorder.AudioSource0 DEFAULT 默认音频源1 MIC 主麦克风5 CAMCORDER 相机方向的麦6 VOICE_RECOGNITION 语音识别7 VOICE_COMMUNICATION 语音通信(带回声消除)
var source=set.android_audioSource,asVal="";
if(source==null && aec) source=7;
if(source==null) source=App.Default_Android_AudioSource;
if(source==1) asVal="mic";
if(source==5) asVal="camcorder";
if(source==6) asVal="voice_recognition";
if(source==7) asVal="voice_communication";
if(asVal)obj.audioSource=asVal;
};
if(aec){
CLog("mg注意iOS下无法配置回声消除Android无此问题建议都启用听筒播放避免回声wx.setInnerAudioOption({speakerOn:false})",3);
};
CLog("["+tag+"]mg.start obj",obj);
mg.start(obj);
};
var recOnShow=function(){
if(curMg && curMg.__pause){
CLog("mg onShow 录音开始恢复...",3);
curMg.resume();
}
};
var recStart=function(success,fail){
clearCurMg();
initSys();
devWebMInfo={};
if(isDev){
CLog("RecorderManager.onFrameRecorded 在开发工具中测试返回的是webm格式音频将会尝试进行解码。开发工具中录音偶尔会非常卡建议使用真机测试各种奇奇怪怪的毛病就都正常了",3);
}
var startIsEnd=false,startCount=1;
var startEnd=function(err){
if(startIsEnd)return; startIsEnd=true;
if(err){
clearCurMg();
fail(err);
}else{
success();
};
};
var mg=curMg=wx.getRecorderManager();
mg.onInterruptionEnd(function(){
if(mg!=curMg)return;
CLog("mg onInterruptionEnd 录音开始恢复...",3);
mg.resume();
});
mg.onPause(function(){
if(mg!=curMg)return;
mg.__pause=Date.now();
CLog("mg onPause 录音被打断",3);
});
mg.onResume(function(){
if(mg!=curMg)return;
var t=mg.__pause?Date.now()-mg.__pause:0,t2=0;
mg.__pause=0;
if(t>300){//填充最多1秒的静默
t2=Math.min(1000,t);
onRecFn(new Int16Array(48000/1000*t2),48000);
}
CLog("mg onResume 恢复录音,填充了"+t2+"ms静默",3);
});
mg.onError(function(res){
if(mg!=curMg)return;
var msg=res.errMsg,tag="mg onError 开始录音出错:";
if(!startIsEnd && !mg._srt && /fail.+is.+recording/i.test(msg)){
var st=600-(Date.now()-mgStime); //距离上次停止未超过600毫秒重试
if(st>0){ st=Math.max(100,st);
CLog(tag+"等待"+st+"ms重试",3,res);
setTimeout(function(){
if(mg!=curMg)return; mg._srt=1;
CLog(tag+"正在重试",3);
newStart("retry start",mg);
}, st);
return;
};
};
CLog(startCount>1?tag+"可能无法继续录音["+startCount+"]。"+msg
:tag+msg+"。"+UserPermissionMsg,1,res);
startEnd("开始录音出错:"+msg);
});
mg.onStart(function(){
if(mg!=curMg)return;
CLog("mg onStart 已开始录音");
mg._srt=0; //下次开始失败可以重试
mg._st=Date.now();
startEnd();
});
mg.onStop(function(res){
CLog("mg onStop 请勿尝试使用此原始结果中的文件路径此原始文件的格式、采样率等和录音配置不相同如需本地文件可在RecordApp.Stop回调中将得到的ArrayBuffer二进制音频数据用RecordApp.MiniProgramWx_WriteLocalFile接口保存到本地即可得到有效路径。res:",res);
if(mg!=curMg)return;
if(!mg._st || Date.now()-mg._st<600){ CLog("mg onStop但已忽略",3); return }
CLog("mg onStop 已停止录音,正在重新开始录音...");
startCount++;
mg._st=0;
newStart("restart",mg);
});
var start0=function(){
mg.onFrameRecorded(function(res){
if(mg!=curMg)return;
if(!startIsEnd)CLog("mg onStart未触发但收到了onFrameRecorded",3);
startEnd();
var aBuf=res.frameBuffer;
if(!aBuf || !aBuf.byteLength){
return;
}
if(isDev){
devWebmDecode(new Uint8Array(aBuf));
}else{
onRecFn(new Int16Array(aBuf),48000);
};
});
newStart("start",mg);
};
var st=600-(Date.now()-mgStime); //距离上次停止未超过600毫秒等待一会一般是第一次请求权限后立马开始录音造成的录音参数不一样不共享同一个mg
if(st>0){ st=Math.max(100,st);
CLog("mg.start距stop太近需等待"+st+"ms",3);
setTimeout(function(){ if(mg!=curMg)return; start0(); }, st);
}else{
start0();
};
};
//保存文件到本地提供文件名或set和arrayBufferTrue(savePath)False(errMsg)
App.MiniProgramWx_WriteLocalFile=function(fileName,buffer,True,False){
var set=fileName; if(typeof(set)=="string") set={fileName:fileName};
fileName=set.fileName;
var append=set.append; //追加写入到文件结尾
var seek_=set.seekOffset, seek=+seek_||0; //覆盖写入到指定位置
if(!seek_ && seek_!==0) seek=-1;
var EnvUsr=wx.env.USER_DATA_PATH, savePath=fileName;
if(fileName.indexOf(EnvUsr)==-1) savePath=EnvUsr+"/"+fileName;
//如果上次还在写入,就等待,保证顺序写入
var tasks=writeTasks[savePath]=writeTasks[savePath]||[];
var tk0=tasks[0], tk={a:set,b:buffer,c:True,d:False};
if(tk0 && tk0._r){ //还在写入,等待
CLog("wx文件等待写入"+savePath,3);
set._tk=1; tasks.push(tk); return;
}
if(set._tk) CLog("wx文件继续写入"+savePath);
tasks.splice(0,0,tk); tk._r=1; //阻塞后续写入
var mg=wx.getFileSystemManager(), fd=0;
var endCall=function(){ //操作完成 清理环境,延迟一下等操作完全结束
if(fd) mg.close({ fd:fd });
setTimeout(function(){
tasks.shift(); var tk=tasks.shift();
if(tk){ //继续写入等待的
App.MiniProgramWx_WriteLocalFile(tk.a,tk.b,tk.c,tk.d);
}
});
};
var okCall=function(){ endCall(); True&&True(savePath) };
var failCall=function(e){ endCall();
var msg=e.errMsg||"-";
CLog("wx文件"+savePath+"写入出错:"+msg,1);
False&&False(msg);
};
if(seek>-1 || append){
mg.open({
filePath:savePath ,flag:seek>-1?"r+":"a"
,success:function(res){
fd=res.fd;
var opt={ fd:fd, data:buffer, success:okCall, fail:failCall };
if(seek>-1) opt.position=seek;
mg.write(opt);
}
,fail:failCall
});
}else{
mg.writeFile({
filePath:savePath, encoding:"binary", data:buffer
,success:okCall, fail:failCall
});
}
};
var writeTasks={};
//删除已保存到本地的文件savePath必须是WriteLocalFile得到的路径 True() False(errMsg)
App.MiniProgramWx_DeleteLocalFile=function(savePath,True,False){
wx.getFileSystemManager().unlink({
filePath:savePath
,success:function(){ True&&True() }
,fail:function(e){ False&&False(e.errMsg||"-") }
});
};
var isDev,sys;
var initSys=function(){
if(sys)return;
sys=wx.getSystemInfoSync();
isDev=sys.platform=="devtools"?1:0;
if(isDev){
devWebCtx=wx.createWebAudioContext();
}
};
/****开发工具内录音返回的webm数据解码成pcm方便测试****/
var devWebCtx,devWebMInfo;
//=======从WebM字节流中提取pcm数据=====
var devWebmDecode=function(inBytes){
var scope=devWebMInfo;
if(!scope.pos){
scope.pos=[0]; scope.tracks={}; scope.bytes=[];
};
var tracks=scope.tracks, position=[scope.pos[0]];
var endPos=function(){ scope.pos[0]=position[0] };
var sBL=scope.bytes.length;
var bytes=new Uint8Array(sBL+inBytes.length);
bytes.set(scope.bytes); bytes.set(inBytes,sBL);
scope.bytes=bytes;
//检测到不是webm当做pcm直接返回
var returnPCM=function(){
scope.bytes=[];
onRecFn(new Int16Array(bytes),48000);
};
if(scope.isNotWebM){
returnPCM(); return;
};
//先读取文件头和Track信息
if(!scope._ht){
//暴力搜索EBML Header开头数据可能存在上次录音结尾数据
var headPos0=0;
for(var i=0;i<bytes.length;i++){
if(bytes[i]==0x1A && bytes[i+1]==0x45 && bytes[i+2]==0xDF && bytes[i+3]==0xA3){
headPos0=i;
position[0]=i+4; break;
}
}
if(!position[0]){
if(bytes.length>5*1024){
CLog("未识别到WebM数据开发工具可能已支持PCM",3);
scope.isNotWebM=true;
returnPCM();
};
return;//未识别到EBML Header
}
readMatroskaBlock(bytes, position);//跳过EBML Header内容
if(!BytesEq(readMatroskaVInt(bytes, position), [0x18,0x53,0x80,0x67])){
return;//未识别到Segment
}
readMatroskaVInt(bytes, position);//跳过Segment长度值
while(position[0]<bytes.length){
var eid0=readMatroskaVInt(bytes, position);
var bytes0=readMatroskaBlock(bytes, position);
var pos0=[0],audioIdx=0;
if(!bytes0)return;//数据不全,等待缓冲
//Track完整数据循环读取TrackEntry
if(BytesEq(eid0, [0x16,0x54,0xAE,0x6B])){
scope._ht=bytes.slice(headPos0,position[0]);
CLog("WebM Tracks",tracks);
endPos();
break;
}
}
}
//循环读取Cluster内的SimpleBlock
var datas=[],dataLen=0;
while(position[0]<bytes.length){
var p0=position[0];
var eid1=readMatroskaVInt(bytes, position);
var p1=position[0];
var bytes1=readMatroskaBlock(bytes, position);
if(!bytes1)break;//数据不全,等待缓冲
if(BytesEq(eid1, [0xA3])){//SimpleBlock完整数据
var arr=bytes.slice(p0,position[0]);
dataLen+=arr.length;
datas.push(arr);
}
endPos();
}
if(!dataLen){
return;
}
var more=new Uint8Array(bytes.length-scope.pos[0]);
more.set(bytes.subarray(scope.pos[0]));
scope.bytes=more; //清理已读取了的缓冲数据
scope.pos[0]=0;
//和头一起拼接成新的webm
var add=[0x1F,0x43,0xB6,0x75,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF];//Cluster
add.push(0xE7,0x81,0x00);
dataLen+=add.length;
datas.splice(0,0,add);
dataLen+=scope._ht.length;
datas.splice(0,0,scope._ht);
var u8arr=new Uint8Array(dataLen); //已获取的音频数据
for(var i=0,i2=0;i<datas.length;i++){
u8arr.set(datas[i],i2);
i2+=datas[i].length;
}
devWebCtx.decodeAudioData(u8arr.buffer, function(raw){
var src=raw.getChannelData(0);
var pcm=new Int16Array(src.length);
for(var i=0;i<src.length;i++){
var s=Math.max(-1,Math.min(1,src[i]));
s=s<0?s*0x8000:s*0x7FFF;
pcm[i]=s;
};
onRecFn(pcm,raw.sampleRate);
},function(){
CLog("WebM解码失败",1);
});
};
//两个字节数组内容是否相同
var BytesEq=function(bytes1,bytes2){
if(!bytes1 || bytes1.length!=bytes2.length) return false;
if(bytes1.length==1) return bytes1[0]==bytes2[0];
for(var i=0;i<bytes1.length;i++){
if(bytes1[i]!=bytes2[i]) return false;
}
return true;
};
//字节数组BE转成int数字
var BytesInt=function(bytes){
var s="";//0-8字节js位运算只支持4字节
for(var i=0;i<bytes.length;i++){var n=bytes[i];s+=(n<16?"0":"")+n.toString(16)};
return parseInt(s,16)||0;
};
//读取一个可变长数值字节数组
var readMatroskaVInt=function(arr,pos,trim){
var i=pos[0];
if(i>=arr.length)return;
var b0=arr[i],b2=("0000000"+b0.toString(2)).substr(-8);
var m=/^(0*1)(\d*)$/.exec(b2);
if(!m)return;
var len=m[1].length, val=[];
if(i+len>arr.length)return;
for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
if(trim) val[0]=parseInt(m[2]||'0',2);
pos[0]=i;
return val;
};
//读取一个自带长度的内容字节数组
var readMatroskaBlock=function(arr,pos){
var lenVal=readMatroskaVInt(arr,pos,1);
if(!lenVal)return;
var len=BytesInt(lenVal);
var i=pos[0], val=[];
if(len<0x7FFFFFFF){ //超大值代表没有长度
if(i+len>arr.length)return;
for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
}
pos[0]=i;
return val;
};
//=====End WebM读取=====
}));

View File

@ -0,0 +1,209 @@
/*
录音 RecordApp: App Native支持文件支持在浏览器环境中使用Hybrid App各种适配后的js运行环境中使用非浏览器环境
https://github.com/xiangyuecn/Recorder
特别注明本文件涉及的功能需要iOSAndroid等App端提供的原生支持如果你不能修改App的源码并且坚决要使用本文件那将会很困难
如果是在App内置的浏览器中进行录音Hybrid App应当首选使用Recorder H5进行录音RecordApp+Native也可以在非浏览器环境中使用比如只有js运行时的app环境nodejs环境
录音功能由原生App(Native)代码实现通过JsBridge和js进行交互Native层需要提供请求权限开始录音结束录音定时回调PCM[Int16]片段 等功能和接口因为js层已加载Recorder和相应的js编码引擎所以Native层无需进行编码可大大简化App的逻辑
录音必须是单声道的因为这个库从头到尾就没有打算支持双声道
JsBridge可以是自己实现的交互方式 别人提供的框架因为不知道具体使用的桥接方式对应的请求已抽象成了4个方法在Native.Config中需自行实现
注意此文件并非拿来就能用的需要改动需实现标注的地方也可以不改动此文件使用另外的初始化配置文件来进行配置可参考app-support-sample目录内的配置文件另外这个目录内还有Android和iOS的demo项目copy源码改改就能用
如果是App内置的浏览器中使用时H5支持在iframe中使用但如果是跨域要特殊处理
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var App=Recorder.RecordApp;
var CLog=App.CLog;
var platform={
Support:function(call){
if(!App.AlwaysAppUseH5){
config.IsApp(call);
return;
};
//不支持app原生录音
call(false);
}
,CanProcess:function(){
return true;//支持实时回调
}
,Config:{
IsApp:function(call){
//如需打开原生App支持此方法【需实现】此方法用来判断1. 判断app是否是在环境中 2. app支持录音
NeedConfigMsg("IsApp");
call(false);//默认实现不支持app原生录音支持就回调call(true)
}
,JsBridgeRequestPermission:function(success,fail){
/*App
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 出错回调
*/
fail(NeedConfigMsg("JsBridgeRequestPermission"));
}
,JsBridgeStart:function(set,success,fail){
/*AppappPCMJsBridge jsset.onProcessJsBridgeAlive10appstop
set:RecordApp.Start的set参数
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
*/
fail(NeedConfigMsg("JsBridgeStart"));
}
,JsBridgeStop:function(success,fail){
/*App
success:fn() 结束录音时回调
fail:fn(errMsg) 结束录音出错时回调
*/
fail(NeedConfigMsg("JsBridgeStop"));
}
}
};
App.RegisterPlatform("Native",platform);
var config=platform.Config;
var NeedConfigMsg=function(fn){
var msg=$T("WWoj::{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法",0,"RecordApp.Platforms.Native.Config",fn,"app-native-support.js");
CLog(msg,3);
return msg;
};
/*******App Native层在录音时定时回调本js方法*******/
/*
pcmDataBase64: base64<Int16[]>字符串 当前单声道录音缓冲PCM片段正常情况下为上次回调本接口开始到现在的录音数据Int16[]二进制数组需要编码成base64字符串或者直接传一个Int16Array对象
sampleRate123456 录制音频实际的采样率
*/
var onRecFn=function(pcmDataBase64,sampleRate){
var rec=onRecFn.rec;
if(!rec){
CLog($T("rCAM::未开始录音但收到Native PCM数据"),3);
return;
};
if(!rec._appStart){
rec.envStart({
envName:platform.Key,canProcess:platform.CanProcess()
},sampleRate);
};
rec._appStart=1;
var sum=0;
if(pcmDataBase64 instanceof Int16Array){
var pcm=new Int16Array(pcmDataBase64);
for(var i=0;i<pcm.length;i++){
sum+=Math.abs(pcm[i]);
}
}else{
var bstr=atob(pcmDataBase64),n=bstr.length;
var pcm=new Int16Array(n/2);
for(var idx=0,s,i=0;i+2<=n;idx++,i+=2){
s=((bstr.charCodeAt(i)|(bstr.charCodeAt(i+1)<<8))<<16)>>16;
pcm[idx]=s;
sum+=Math.abs(s);
};
}
rec.envIn(pcm,sum);
};
if(!isBrowser){
App.NativeRecordReceivePCM=onRecFn;
};
//尝试注入顶层window用于接收Native回调数据此处特殊处理一下省得跨域的iframe无权限
if(isBrowser){
window.NativeRecordReceivePCM=onRecFn;
try{
window.top.NativeRecordReceivePCM=onRecFn;
}catch(e){
var tipsFn=function(){
CLog($T("t2OF::检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听postMessage转发兼容传输数据请自行实现将top层接收到数据转发到本iframe不限层不然无法接收到录音数据"),3);
};
setTimeout(tipsFn,8000);
tipsFn();
addEventListener("message",function(e){//纯天然无需考虑origin
var data=e.data;//{type:"",data:{pcmDataBase64:"",sampleRate:16000}}
if(data&&data.type=="NativeRecordReceivePCM"){
data=data.data;
onRecFn(data.pcmDataBase64, data.sampleRate);
};
});
};
};
/*******实现统一接口*******/
platform.RequestPermission=function(sid,success,fail){
config.JsBridgeRequestPermission(success,fail);
};
platform.Start=function(sid,set,success,fail){
onRecFn.param=set;
var rec=Recorder(set);
rec.set.disableEnvInFix=true; //不要音频输入丢失补偿
rec.dataType="arraybuffer";
onRecFn.rec=rec;//等待第一个数据到来再调用rec.start
App.__Rec=rec;//App需要暴露出使用到的rec实例
config.JsBridgeStart(set,success,fail);
};
platform.Stop=function(sid,success,fail){
var failCall=function(msg){
if(App.__Sync(sid)){
onRecFn.rec=null;
}
fail(msg);
};
config.JsBridgeStop(function(){
if(!App.__Sync(sid)){
failCall("Incorrect sync status");
return;
};
var rec=onRecFn.rec;
onRecFn.rec=null;
var clearMsg=success?"":App.__StopOnlyClearMsg();
if(!rec){
failCall($T("Z2y2::未开始录音")
+(clearMsg?" ("+clearMsg+")":""));
return;
};
CLog("rec encode: pcm:"+rec.recSize+" srcSR:"+rec.srcSampleRate+" set:"+JSON.stringify(onRecFn.param));
var end=function(){
if(App.__Sync(sid)){
//把可能变更的配置写回去
for(var k in rec.set){
onRecFn.param[k]=rec.set[k];
};
};
};
if(!success){
end();
failCall(clearMsg);
return;
};
rec.stop(function(arrBuf,duration,mime){
end();
success(arrBuf,duration,mime);
},function(msg){
end();
failCall(msg);
});
},failCall);
};
}));

2
node_modules/recorder-core/src/app-support/app.d.ts generated vendored Normal file
View File

@ -0,0 +1,2 @@
declare let RecordApp : any;
export default RecordApp;

450
node_modules/recorder-core/src/app-support/app.js generated vendored Normal file
View File

@ -0,0 +1,450 @@
/*
RecordApp基于Recorder的跨平台录音支持在浏览器环境中使用H5各种使用js来构建的程序中使用App小程序UniAppElectronNodeJs
https://github.com/xiangyuecn/Recorder
示例demo请参考根目录内的app-support-sample目录
使用时需先引入recorder-core和需要的编码器再引入本js再根据不同平台引入相应的app-xxx-support.js支持文件如果引入的支持文件需要进行额外的配置可参考app-support-sample目录内对应的配置文件
可以仅使用RecordApp作为入口可以不关心recorder-core中的方法因为RecordApp已经对他进行了一次封装并且屏蔽了非通用的功能
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(win,rec,ni,ni.$T,browser);
//umd returnExports.js
if(typeof(define)=='function' && define.amd){
define(function(){
return win.RecordApp;
});
};
if(typeof(module)=='object' && module.exports){
module.exports=win.RecordApp;
};
}(function(Export,Recorder,i18n,$T,isBrowser){
"use strict";
var App={
LM:"2024-04-09 19:22"
//当前使用的平台实现
,Current:0
//已注册的平台实现
,Platforms:{}
};
var Platforms=App.Platforms;
var AppTxt="RecordApp";
var ReqTxt="RequestPermission";
var RegTxt="RegisterPlatform";
var WApp2=Export[AppTxt];//重复加载js
if(WApp2&&WApp2.LM==App.LM){
WApp2.CLog($T("uXtA::重复导入{1}",0,AppTxt),3);
return;
};
Export[AppTxt]=App;
Recorder[AppTxt]=App;
App.__SID_=0;//同步操作,防止同时多次调用
var SID=App.__SID=function(){ return ++App.__SID_; };
var Sync=App.__Sync=function(sid,tag,err){
if(App.__SID_!=sid){
if(tag){
CLog($T("kIBu::注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调",0,tag)+(err?", error: "+err:""),3);
}
return false;
}
return true;
};
var CLog=function(){
var v=arguments; v[0]="["+(CLog.Tag||AppTxt)+"]["+(App.Current&&App.Current.Key||"?")+"]"+v[0];
Recorder.CLog.apply(null,v);
};
App.CLog=CLog;
/**
注册一个平台的实现对应的都会有一个app-xxx-support.js支持文件(Default-H5除外)config中提供统一的实现接口
{
Support: fn( call(canUse) ) 判断此平台是否支持或开启如果平台可用需回调call(true)选择使用这个平台并忽略其他平台
CanProcess: fn() 此平台是否支持实时回调返回true代表支持
Install: fn( success(),fail(errMsg) ) 可选平台初始化安装当使用此平台时会执行一次本方法同一时间只会有一次调用没有并发调用问题
Pause: fn() 可选暂停录音实现如果返回false将执行默认暂停操作
Resume: fn() 可选继续录音实现如果返回false将执行默认继续操作
下面的方法中sid用于同步操作在异步回调中用App.__Sync判断此sid是否处于同步状态
实现中使用到的Recorder实例需赋值给App.__RecStop结束后会自动清理并赋值为null
RequestPermission:fn(sid,success,fail) 实现录音权限请求通过回调函数返回结果
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 没有权限或者不能录音时回调如果是用户主动拒绝的录音权限除了有错误消息外isUserNotAllow=true方便程序中做不同的提示提升用户主动授权概率
Start:fn(sid,set,success,fail) 实现开始录音
set:{} 和Recorder的set大部分参数相同
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
Start_Check:fn(set) 可选调用本实现的Start前执行环境检查返回检查错误文本如果返回false将执行默认检查操作
AllStart_Clean:fn(set) 可选任何实现的Start前执行本配置清理set里面可能为了兼容不同平台环境会传入额外的参数可以进行清理无返回值
Stop:fn(sid,success,fail) 实现结束录音返回结果success参数=null时仅清理资源
success:fn(arrayBuffer,duration,mime) 成功完成录音回调参数可能为null
arrayBuffer:ArrayBuffer 录音数据
duration:123 //录音数据持续时间
mime:"audio/mp3" 录音数据格式
fail:fn(errMsg) 录音出错时回调
}
**/
App[RegTxt]=function(key,config){ //App.RegisterPlatform=function()
config.Key=key;
if(Platforms[key]){
CLog($T("ha2K::重复注册{1}",0,key),3);
}
Platforms[key]=config;
};
App.__StopOnlyClearMsg=function(){
return $T("wpTL::仅清理资源");
};
/****实现默认的H5统一接口*****/
var KeyH5="Default-H5",H5OpenSet=ReqTxt+"_H5OpenSet";
(function(){
var impl={
Support:function(call){
//默认的始终要支持
call(true);
}
,CanProcess:function(){
return true;//支持实时回调
}
};
App[RegTxt](KeyH5,impl);
impl[ReqTxt]=function(sid,success,fail){ //impl.RequestPermission=function()
var old=App.__Rec;
if(old){
old.close();
App.__Rec=null;
};
h5Kill();
//h5会提前打开录音open时需要的配置只能单独配置
var h5Set=App[H5OpenSet]; App[H5OpenSet]=null;
var rec=Recorder(h5Set||{});
rec.open(function(){
//rec.close(); keep stream Stop时再关免得Start再次请求权限
success();
},fail);
};
var h5Kill=function(){ //释放检测权限时已打开的录音
if(Recorder.IsOpen()){
CLog("kill open...");
var rec=Recorder();
rec.open();
rec.close();
};
};
impl.Start=function(sid,set,success,fail){
var appRec=App.__Rec;
if(appRec!=null){
appRec.stop();//未stop的stop掉
};
App.__Rec=appRec=Recorder(set);
appRec.appSet=set;
appRec.dataType="arraybuffer";
appRec.open(function(){
if(Sync(sid)){
appRec.start();
};
success();
},fail);
};
impl.Stop=function(sid,success,fail){
var appRec=App.__Rec;
var clearMsg=success?"":App.__StopOnlyClearMsg();
if(!appRec){
h5Kill(); //释放检测权限时已打开的录音
fail($T("bpvP::未开始录音")
+(clearMsg?" ("+clearMsg+")":""));
return;
};
var end=function(){
if(Sync(sid)){
appRec.close();
//把可能变更的配置写回去
for(var k in appRec.set){
appRec.appSet[k]=appRec.set[k];
};
};
};
var stopFail=function(msg){
end();
fail(msg);
};
if(!success){
stopFail(clearMsg);
return;
};
appRec.stop(function(arrBuf,duration,mime){
end();
success(arrBuf,duration,mime);
},stopFail);
};
})();
/***
获取底层平台录音过程中会使用用来处理实时数据的Recorder对象实例rec如果底层录音过程中不使用Recorder进行数据的实时处理目前没有将返回nullStart调用前和Stop调用后均会返回null
rec中的方法不一定都能使用主要用来获取内部缓冲用的比如实时清理缓冲
***/
App.GetCurrentRecOrNull=function(){
return App.__Rec||null;
};
/**暂停录音**/
App.Pause=function(){
var cur=App.Current,key="Pause";
if(cur&&cur[key]){
if(cur[key]()!==false)return;
}
var rec=App.__Rec;
if(rec && canProc(key)){
rec.pause();
}
};
/**恢复录音**/
App.Resume=function(){
var cur=App.Current,key="Resume";
if(cur&&cur[key]){
if(cur[key]()!==false)return;
}
var rec=App.__Rec;
if(rec && canProc(key)){
rec.resume();
}
};
var canProc=function(tag){
var cur=App.Current;
if(cur&&cur.CanProcess()) return 1;
CLog($T("fLJD::当前环境不支持实时回调,无法进行{1}",0,tag),3);
};
/***
初始化安装可反复调用
success: fn() 初始化成功
fail: fn(msg) 初始化失败
***/
App.Install=function(success,fail){
var cur=App.Current;
if(cur){ success&&success(); return; }
//因为此操作是异步的为了避免竞争Current资源此代码保证得到结果前只会发起一次调用
var reqs=App.__reqs||(App.__reqs=[]);
reqs.push({s:success,f:fail});
success=function(){ call("s",arguments) };
fail=function(){ call("f",arguments) };
var call=function(fn,args){
var arr=[].concat(reqs); reqs.length=0;
for(var i=0;i<arr.length;i++){
var f=arr[i][fn];
f&&f.apply(null,args);
};
};
if(reqs.length>1) return;
var keys=[KeyH5],key;
for(var k in Platforms){
if(k!=KeyH5)keys.push(k);
}
keys.reverse();
var initCur=function(idx){
key=keys[idx];
cur=Platforms[key];
cur.Support(function(canUse){
if(!canUse){
return initCur(idx+1);
};
if(cur.Install){
cur.Install(initOk,fail);
}else{
initOk();
};
});
};
var initOk=function(){
App.Current=cur;
CLog("Install platform: "+key);
success();
};
initCur(0);
};
/***
请求录音权限如果当前环境不支持录音或用户拒绝将调用错误回调调用start前需先至少调用一次此方法请求权限后如果不使用了不管有没有调用Start至少要调用一次Stop来清理可能持有的资源
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 没有权限或者不能录音时回调如果是用户主动拒绝的录音权限除了有错误消息外isUserNotAllow=true方便程序中做不同的提示提升用户主动授权概率
***/
App[ReqTxt]=function(success,fail){ //App.RequestPermission=function(success,fail){
var sid=SID(),tag=AppTxt+"."+ReqTxt;
var failCall=function(errMsg,isUserNotAllow){
isUserNotAllow=!!isUserNotAllow;
var msg=errMsg+", isUserNotAllow:"+isUserNotAllow;
if(!Sync(sid,tag,msg))return;
CLog($T("YnzX::录音权限请求失败:")+msg,1);
fail&&fail(errMsg,isUserNotAllow);
};
CLog(ReqTxt+"...");
App.Install(function(){
if(!Sync(sid,tag))return;
var checkMsg=CheckH5();
if(checkMsg){ failCall(checkMsg); return; };
App.Current[ReqTxt](sid,function(){ //App.Current.RequestPermission()
if(!Sync(sid,tag))return;
CLog(ReqTxt+" Success");
success&&success();
},failCall);
},failCall);
};
var NeedReqMsg=function(){
return $T("nwKR::需先调用{1}",0,ReqTxt);
};
var CheckH5=function(){
var msg="";
if(App.Current.Key==KeyH5 && !isBrowser){
msg=$T("citA::当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入",0,"src/app-support/app-xxx-support.js",AppTxt+"."+RegTxt);
};
return msg;
};
/***
开始录音需先调用RequestPermission获得录音权限
set设置默认值{
type:"mp3"//最佳输出格式,如果底层实现能够支持就应当优先返回此格式
sampleRate:16000//最佳采样率hz
bitRate:16//最佳比特率kbps
onProcess:NOOP //如果当前环境支持实时回调RecordApp.Current.CanProcess()接收到录音数据时的回调函数fn(buffers,powerLevel,bufferDuration,bufferSampleRate)
takeoffEncodeChunk:NOOP //fn(chunkBytes)
} 注意此对象会被修改因为平台实现时需要把实际使用的值存入此对象
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
***/
App.Start=function(set,success,fail){
var sid=SID(),tag=AppTxt+".Start";
var failCall=function(msg){
if(!Sync(sid,tag,msg))return;
CLog($T("ecp9::开始录音失败:")+msg,1);
fail&&fail(msg);
};
CLog("Start...");
var cur=App.Current;
if(!cur){
failCall(NeedReqMsg());
return;
};
set||(set={});
var obj={
type:"mp3"
,sampleRate:16000
,bitRate:16
,onProcess:function(){}
};
for(var k in obj){
set[k]||(set[k]=obj[k]);
};
//对配置项进行清理set里面可能为了兼容不同平台环境会传入额外的参数可以进行清理
for(var k in Platforms){
var pf=Platforms[k];
if(pf.AllStart_Clean){
pf.AllStart_Clean(set);
}
};
//先执行一下环境配置检查
var checkMsg=false;
if(cur.Start_Check){
checkMsg=cur.Start_Check(set);
};
if(checkMsg===false){
var checkRec=Recorder(set);
checkMsg=checkRec.envCheck({envName:cur.Key,canProcess:cur.CanProcess()});
if(!checkMsg) checkMsg=CheckH5();
}
if(checkMsg){
failCall($T("EKmS::不能录音:")+checkMsg);
return;
};
//重置Stop时的rec
App._SRec=0;
cur.Start(sid,set,function(){
if(!Sync(sid,tag))return;
CLog($T("k7Qo::已开始录音"),set);
App._STime=Date.now();
success&&success();
},failCall);
};
/***
结束录音
success:fn(arrayBuffer,duration,mime) 结束录音时回调
arrayBuffer:ArrayBuffer 录音二进制数据
duration:123 录音时长单位毫秒
mime:"auido/mp3" 录音格式类型
fail:fn(errMsg) 录音出错时回调
本方法可以用来清理持有的资源如果不提供success参数=null时将不会进行音频编码操作只进行清理完可能持有的资源后走fail回调
***/
App.Stop=function(success,fail){
var sid=SID(),tag=AppTxt+".Stop";
var failCall=function(msg){
if(!Sync(sid,tag,msg))return;
CLog($T("Douz::结束录音失败:")+msg,success?1:0);
try{
fail&&fail(msg);
}finally{ clear() }
};
var clear=function(){
App._SRec=App.__Rec;
App.__Rec=null;
};
CLog("Stop... "+$T("wqSH::和Start时差{1}ms",0,App._STime?Date.now()-App._STime:-1)+" Recorder.LM:"+Recorder.LM+" "+AppTxt+".LM:"+App.LM);
var t1=Date.now();
var cur=App.Current;
if(!cur){
failCall(NeedReqMsg());
return;
};
cur.Stop(sid, !success?null:function(arrayBuffer,duration,mime){
if(!Sync(sid,tag))return;
CLog($T("g3VX::结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}",0,Date.now()-t1,duration,arrayBuffer.byteLength,mime));
try{
success(arrayBuffer,duration,mime);
}finally{ clear() }
},failCall);
};
}));

15598
node_modules/recorder-core/src/engine/beta-amr-engine.js generated vendored Normal file

File diff suppressed because one or more lines are too long

359
node_modules/recorder-core/src/engine/beta-amr.js generated vendored Normal file
View File

@ -0,0 +1,359 @@
/*
amr编码器beta版需带上src/engine/amr-engine.js引擎使用如果需要播放amr音频需要额外带上wav.js引擎来调用Recorder.amr2wav把amr转成wav播放
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
FFmpeg转码
[ wav->AMR-NB] ffmpeg.exe -i test.wav -ar 8000 -ab 6.7k -ac 1 amr-6.7.amr
[ wav->AMR-NB] ffmpeg.exe -i test.wav -ar 8000 -ab 12.2k -ac 1 amr-12.2.amr
[ wav->AMR-WB] ffmpeg.exe -i test.wav -acodec libvo_amrwbenc -ar 16000 -ab 23.85k -ac 1 amr-23.85.amr
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var BitS="4.75, 5.15, 5.9, 6.7, 7.4, 7.95, 10.2, 12.2";
Recorder.prototype.enc_amr={
stable:true,takeEC:"full"
,getTestMsg:function(){
return $T("b2mN::AMR-NB(NarrowBand)采样率设置无效只提供8000hz比特率范围{1}默认12.2kbps一帧20ms、{2}字节浏览器一般不支持播放amr格式可用Recorder.amr2wav()转码成wav播放",0,BitS,"Math.ceil(bitRate/8*20)+1");
}
};
var NormalizeSet=function(set){
var bS=set.bitRate,b=Recorder.AMR.BitRate(bS);
var sS=set.sampleRate,s=8000;
if(bS!=b || sS!=s) Recorder.CLog($T("tQBv::AMR Info: 和设置的不匹配{1},已更新成{2}",0,"set:"+bS+"kbps "+sS+"hz","set:"+b+"kbps "+s+"hz"),3);
set.bitRate=b;
set.sampleRate=s;
};
var ImportEngineErr=function(){
return $T.G("NeedImport-2",["beta-amr.js","src/engine/beta-amr-engine.js"]);
};
//是否支持web worker
var HasWebWorker=isBrowser && typeof Worker=="function";
/**amrwavsrc/engine/wav.js
amrBlob: amr音频文件blob对象 ArrayBuffer回调也将返回ArrayBuffer
True(wavBlob,duration,mime)
False(msg)
**/
Recorder.amr2wav=function(amrBlob,True,False){
if(!Recorder.AMR){
False(ImportEngineErr()); return;
};
if(!Recorder.prototype.wav){
False($T.G("NeedImport-2",["amr2wav","src/engine/wav.js"]));
return;
};
var loadOk=function(arrB,dArrB){
var amr=new Uint8Array(arrB);
Recorder.AMR.decode(amr,function(pcm){
var rec=Recorder({type:"wav"});
if(dArrB)rec.dataType="arraybuffer";
rec.mock(pcm,8000).stop(function(wavBlob,duration,mime){
True(wavBlob,duration,mime);
},False);
},False);
};
if(amrBlob instanceof ArrayBuffer){
loadOk(amrBlob,1);
}else{
var reader=new FileReader();
reader.onloadend=function(){
loadOk(reader.result);
};
reader.readAsArrayBuffer(amrBlob);
};
};
//*******标准UI线程转码支持函数************
Recorder.prototype.amr=function(res,True,False){
var This=this,set=This.set,srcSampleRate=set.sampleRate,sampleRate=8000;
if(!Recorder.AMR){
False(ImportEngineErr()); return;
};
//必须先处理好采样率
NormalizeSet(set);
if(srcSampleRate>sampleRate){
res=Recorder.SampleData([res],srcSampleRate,sampleRate).data;
}else if(srcSampleRate<sampleRate){
False($T("q12D::数据采样率低于{1}",0,sampleRate)); return;
};
//优先采用worker编码非worker时用老方法提供兼容
if(HasWebWorker){
var ctx=This.amr_start(set);
if(ctx){
if(ctx.isW){
This.amr_encode(ctx,res);
This.amr_complete(ctx,True,False,1);
return;
}
This.amr_stop(ctx);
};
};
Recorder.AMR.encode(res,function(data){
True(data.buffer,"audio/amr");
},False,set.bitRate);
};
//********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码*********
//全局共享一个Worker后台串行执行
var amrWorker;
Recorder.BindDestroy("amrWorker",function(){
if(amrWorker){
Recorder.CLog("amrWorker Destroy");
amrWorker.terminate();
amrWorker=null;
};
});
Recorder.prototype.amr_envCheck=function(envInfo,set){//检查环境下配置是否可用
var errMsg="";
//需要实时编码返回数据,此时需要检查是否可实时编码
if(set.takeoffEncodeChunk){
if(!newContext()){//浏览器不能创建实时编码环境
errMsg=$T("TxjV::当前浏览器版本太低,无法实时处理");
};
};
if(!errMsg && !Recorder.AMR){
errMsg=ImportEngineErr();
};
return errMsg;
};
Recorder.prototype.amr_start=function(set){//如果返回null代表不支持
return newContext(set);
};
var openList={id:0};
var newContext=function(setOrNull,_badW){
//独立运行的函数scope.wkScope worker.onmessage 字符串会被替换
var run=function(e){
var ed=e.data;
var wk_ctxs=scope.wkScope.wk_ctxs;
var wk_AMR=scope.wkScope.wk_AMR;
var cur=wk_ctxs[ed.id];
if(ed.action=="init"){
wk_ctxs[ed.id]={
takeoff:ed.takeoff
,memory:new Uint8Array(500000), mOffset:0
,encObj:wk_AMR.GetEncoder(ed.bitRate)
};
}else if(!cur){
return;
};
var addBytes=function(buf){
var bufLen=buf.length;
if(cur.mOffset+bufLen>cur.memory.length){
var tmp=new Uint8Array(cur.memory.length+Math.max(500000,bufLen));
tmp.set(cur.memory.subarray(0, cur.mOffset));
cur.memory=tmp;
}
cur.memory.set(buf,cur.mOffset);
cur.mOffset+=bufLen;
};
switch(ed.action){
case "stop":
if(!cur.isCp) try{ cur.encObj.flush() }catch(e){ console.error(e) }
cur.encObj=null;
delete wk_ctxs[ed.id];
break;
case "encode":
if(cur.isCp)break;
try{
var buf=cur.encObj.encode(ed.pcm);
}catch(e){ //精简代码调用了abort
cur.err=e;
console.error(e);
break;
};
if(!cur._h){//添加AMR头
cur._h=1;
var head=wk_AMR.GetHeader();
var buf2=new Uint8Array(head.length+buf.length);
buf2.set(head);
buf2.set(buf,head.length);
buf=buf2;
}
if(buf.length>0){
if(cur.takeoff){
worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
addBytes(buf);
};
};
break;
case "complete":
cur.isCp=1;
try{ cur.encObj.flush() }catch(e){ console.error(e) }; //flush没有结果只做释放
if(cur.err){
worker.onmessage({action:ed.action,id:ed.id
,err:"AMR Encoder: "+cur.err.message});
break;
};
worker.onmessage({
action:ed.action
,id:ed.id
,blob:cur.memory.buffer.slice(0,cur.mOffset)
});
break;
};
};
var initOnMsg=function(isW){
worker.onmessage=function(e){
var data=e; if(isW)data=e.data;
var ctx=openList[data.id];
if(ctx){
if(data.action=="takeoff"){
//取走实时生成的amr数据
ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer));
}else{
//complete
ctx.call&&ctx.call(data);
ctx.call=null;
};
};
};
};
var initCtx=function(){
var ctx={worker:worker,set:setOrNull};
if(setOrNull){
ctx.id=++openList.id;
openList[ctx.id]=ctx;
NormalizeSet(setOrNull);
var takeoff=!!setOrNull.takeoffEncodeChunk;
if(takeoff){
Recorder.CLog($T("Q7p7::takeoffEncodeChunk接管AMR编码器输出的二进制数据只有首次回调数据首帧包含AMR头在合并成AMR文件时如果没有把首帧数据包含进去则必须在文件开头添加上AMR头Recorder.AMR.AMR_HEADER转成二进制否则无法播放"),3);
};
worker.postMessage({
action:"init"
,id:ctx.id
,sampleRate:setOrNull.sampleRate
,bitRate:setOrNull.bitRate
,takeoff:takeoff
,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
}else{
worker.postMessage({
x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
};
return ctx;
};
var scope,worker=amrWorker;
//非浏览器不支持worker或者开启失败使用UI线程处理
if(_badW || !HasWebWorker){
Recorder.CLog($T("6o9Z::当前环境不支持Web Workeramr实时编码器运行在主线程中"),3);
worker={ postMessage:function(ed){ run({data:ed}); } };
scope={wkScope:{
wk_ctxs:{}, wk_AMR:Recorder.AMR
}};
initOnMsg();
return initCtx();
};
try{
if(!worker){
//创建一个新Worker
var onmsg=(run+"").replace(/[\w\$]+\.onmessage/g,"self.postMessage");
onmsg=onmsg.replace(/[\w\$]+\.wkScope/g,"wkScope");
var jsCode=");self.onmessage="+onmsg;
jsCode+=";var wkScope={ wk_ctxs:{},wk_AMR:Create() }";
var engineCode=Recorder.AMR.Create.toString();
var url=(window.URL||webkitURL).createObjectURL(new Blob(["var Create=(",engineCode,jsCode], {type:"text/javascript"}));
worker=new Worker(url);
setTimeout(function(){
(window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
},10000);//chrome 83 file协议下如果直接释放将会使WebWorker无法启动
initOnMsg(1);
};
var ctx=initCtx(); ctx.isW=1;
amrWorker=worker;
return ctx;
}catch(e){//出错了就不要提供了
worker&&worker.terminate();
console.error(e);
return newContext(setOrNull, 1);//切换到UI线程处理
};
};
Recorder.prototype.amr_stop=function(startCtx){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"stop"
,id:startCtx.id
});
startCtx.worker=null;
delete openList[startCtx.id];
//疑似泄露检测 排除id
var opens=-1;
for(var k in openList){
opens++;
};
if(opens){
Recorder.CLog($T("yYWs::amr worker剩{1}个未stop",0,opens),3);
};
};
};
Recorder.prototype.amr_encode=function(startCtx,pcm){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"encode"
,id:startCtx.id
,pcm:pcm
});
};
};
Recorder.prototype.amr_complete=function(startCtx,True,False,autoStop){
var This=this;
if(startCtx&&startCtx.worker){
startCtx.call=function(data){
if(autoStop){
This.amr_stop(startCtx);
};
if(data.err){
False(data.err);
}else{
True(data.blob,"audio/amr");
};
};
startCtx.worker.postMessage({
action:"complete"
,id:startCtx.id
});
}else{
False($T("jOi8::amr编码器未start"));
};
};
}));

30158
node_modules/recorder-core/src/engine/beta-ogg-engine.js generated vendored Normal file

File diff suppressed because one or more lines are too long

367
node_modules/recorder-core/src/engine/beta-ogg.js generated vendored Normal file
View File

@ -0,0 +1,367 @@
/*
ogg编码器beta版需带上src/engine/beta-ogg-engine.js引擎使用
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
Recorder.prototype.enc_ogg={
stable:true,takeEC:"slow"
,getTestMsg:function(){
return $T("O8Gn::Ogg Vorbis比特率取值16-100kbps采样率取值无限制");
}
};
var ImportEngineErr=function(){
return $T.G("NeedImport-2",["beta-ogg.js","src/engine/beta-ogg-engine.js"]);
};
//是否支持web worker
var HasWebWorker=isBrowser && typeof Worker=="function";
//*******标准UI线程转码支持函数************
Recorder.prototype.ogg=function(res,True,False){
var This=this,set=This.set,size=res.length,bitRate=set.bitRate;
if(!Recorder.OggVorbisEncoder){
False(ImportEngineErr()); return;
};
//优先采用worker编码非worker时用老方法提供兼容
if(HasWebWorker){
var ctx=This.ogg_start(set);
if(ctx){
if(ctx.isW){
This.ogg_encode(ctx,res);
This.ogg_complete(ctx,True,False,1);
return;
}
This.ogg_stop(ctx);
};
};
var bitV=GetBitRate(bitRate);
set.bitRate=bitV.bitRate;
var ogg = new Recorder.OggVorbisEncoder(set.sampleRate, 1, bitV.val);
var blockSize=set.sampleRate;
var memory=new Uint8Array(500000), mOffset=0;
var idx=0,isFlush=0;
var run=function(){
try{
if(idx<size){
var buf=ogg.encode([res.subarray(idx,idx+blockSize)]);
}else{
isFlush=1;
var buf=ogg.flush();
};
}catch(e){ //精简代码调用了abort
console.error(e);
if(!isFlush) try{ ogg.flush() }catch(r){ console.error(r) }
False("Ogg Encoder: "+e.message);
return;
};
if(buf.length>0){
var bufLen=buf.length;
if(mOffset+bufLen>memory.length){
var tmp=new Uint8Array(memory.length+Math.max(500000,bufLen));
tmp.set(memory.subarray(0, mOffset));
memory=tmp;
}
memory.set(buf,mOffset);
mOffset+=bufLen;
};
if(idx<size){
idx+=blockSize;
setTimeout(run);//尽量避免卡ui
}else{
True(memory.buffer.slice(0,mOffset),"audio/ogg");
};
};
run();
}
//********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码*********
//全局共享一个Worker后台串行执行。如果每次都开一个新的编码速度可能会慢很多可能是浏览器运行缓存的因素并且可能瞬间产生多个并行操作占用大量cpu
var oggWorker;
Recorder.BindDestroy("oggWorker",function(){
if(oggWorker){
Recorder.CLog("oggWorker Destroy");
oggWorker.terminate();
oggWorker=null;
};
});
Recorder.prototype.ogg_envCheck=function(envInfo,set){//检查环境下配置是否可用
var errMsg="";
//需要实时编码返回数据,此时需要检查是否可实时编码
if(set.takeoffEncodeChunk){
if(!newContext()){//浏览器不能创建实时编码环境
errMsg=$T("5si6::当前浏览器版本太低,无法实时处理");
};
};
if(!errMsg && !Recorder.OggVorbisEncoder){
errMsg=ImportEngineErr();
};
return errMsg;
};
Recorder.prototype.ogg_start=function(set){//如果返回null代表不支持
return newContext(set);
};
var openList={id:0};
var newContext=function(setOrNull,_badW){
//独立运行的函数scope.wkScope worker.onmessage 字符串会被替换
var run=function(e){
var ed=e.data;
var wk_ctxs=scope.wkScope.wk_ctxs;
var wk_OggEnc=scope.wkScope.wk_OggEnc;
var cur=wk_ctxs[ed.id];
if(ed.action=="init"){
wk_ctxs[ed.id]={
takeoff:ed.takeoff
,memory:new Uint8Array(500000), mOffset:0
,encObj:new wk_OggEnc(ed.sampleRate, 1, ed.bitVv)
};
}else if(!cur){
return;
};
var addBytes=function(buf){
var bufLen=buf.length;
if(cur.mOffset+bufLen>cur.memory.length){
var tmp=new Uint8Array(cur.memory.length+Math.max(500000,bufLen));
tmp.set(cur.memory.subarray(0, cur.mOffset));
cur.memory=tmp;
}
cur.memory.set(buf,cur.mOffset);
cur.mOffset+=bufLen;
};
switch(ed.action){
case "stop":
if(!cur.isCp) try{ cur.encObj.flush() }catch(e){ console.error(e) }
cur.encObj=null;
delete wk_ctxs[ed.id];
break;
case "encode":
if(cur.isCp)break;
try{
var buf=cur.encObj.encode([ed.pcm]);
}catch(e){ //精简代码调用了abort
cur.err=e;
console.error(e);
};
if(buf && buf.length>0){
if(cur.takeoff){
worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
addBytes(buf);
};
};
break;
case "complete":
cur.isCp=1;
try{
var buf=cur.encObj.flush();
}catch(e){ //精简代码调用了abort
cur.err=e;
console.error(e);
};
if(buf && buf.length>0){
if(cur.takeoff){
worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
addBytes(buf);
};
};
if(cur.err){
worker.onmessage({action:ed.action,id:ed.id
,err:"Ogg Encoder: "+cur.err.message});
break;
};
worker.onmessage({
action:ed.action
,id:ed.id
,blob:cur.memory.buffer.slice(0,cur.mOffset)
});
break;
};
};
var initOnMsg=function(isW){
worker.onmessage=function(e){
var data=e; if(isW)data=e.data;
var ctx=openList[data.id];
if(ctx){
if(data.action=="takeoff"){
//取走实时生成的ogg数据
ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer));
}else{
//complete
ctx.call&&ctx.call(data);
ctx.call=null;
};
};
};
};
var initCtx=function(){
var ctx={worker:worker,set:setOrNull};
if(setOrNull){
ctx.id=++openList.id;
openList[ctx.id]=ctx;
var bitV=GetBitRate(setOrNull.bitRate);
setOrNull.bitRate=bitV.bitRate;
var takeoff=!!setOrNull.takeoffEncodeChunk;
if(takeoff){
Recorder.CLog($T("R8yz::takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据Ogg由数据页组成一页包含多帧音频数据含几秒的音频一页数据无法单独解码和播放此编码器每次输出都是完整的一页数据因此实时性会比较低在合并成完整ogg文件时必须将输出的所有数据合并到一起否则可能无法播放不支持截取中间一部分单独解码和播放"),3);
};
worker.postMessage({
action:"init"
,id:ctx.id
,sampleRate:setOrNull.sampleRate
,bitRate:setOrNull.bitRate, bitVv:bitV.val
,takeoff:takeoff
,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
}else{
worker.postMessage({
x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
};
return ctx;
};
var scope,worker=oggWorker;
//非浏览器不支持worker或者开启失败使用UI线程处理
if(_badW || !HasWebWorker){
Recorder.CLog($T("hB9D::当前环境不支持Web WorkerOggVorbis实时编码器运行在主线程中"),3);
worker={ postMessage:function(ed){ run({data:ed}); } };
scope={wkScope:{
wk_ctxs:{}, wk_OggEnc:Recorder.OggVorbisEncoder
}};
initOnMsg();
return initCtx();
};
try{
if(!worker){
//创建一个新Worker
var onmsg=(run+"").replace(/[\w\$]+\.onmessage/g,"self.postMessage");
onmsg=onmsg.replace(/[\w\$]+\.wkScope/g,"wkScope");
var jsCode=");self.onmessage="+onmsg;
jsCode+=";var wkScope={ wk_ctxs:{},wk_OggEnc:Create() };";
if(Recorder.OggVorbisEncoder.Module.StaticSeed){
jsCode+="wkScope.wk_OggEnc.Module.StaticSeed=true;";
};
var engineCode=Recorder.OggVorbisEncoder.Create.toString();
var url=(window.URL||webkitURL).createObjectURL(new Blob(["var Create=(",engineCode,jsCode], {type:"text/javascript"}));
worker=new Worker(url);
setTimeout(function(){
(window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
},10000);//chrome 83 file协议下如果直接释放将会使WebWorker无法启动
initOnMsg(1);
};
var ctx=initCtx(); ctx.isW=1;
oggWorker=worker;
return ctx;
}catch(e){//出错了就不要提供了
worker&&worker.terminate();
console.error(e);
return newContext(setOrNull, 1);//切换到UI线程处理
};
};
Recorder.prototype.ogg_stop=function(startCtx){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"stop"
,id:startCtx.id
});
startCtx.worker=null;
delete openList[startCtx.id];
//疑似泄露检测 排除id
var opens=-1;
for(var k in openList){
opens++;
};
if(opens){
Recorder.CLog($T("oTiy::ogg worker剩{1}个未stop",0,opens),3);
};
};
};
Recorder.prototype.ogg_encode=function(startCtx,pcm){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"encode"
,id:startCtx.id
,pcm:pcm
});
};
};
Recorder.prototype.ogg_complete=function(startCtx,True,False,autoStop){
var This=this;
if(startCtx&&startCtx.worker){
startCtx.call=function(data){
if(autoStop){
This.ogg_stop(startCtx);
};
if(data.err){
False(data.err);
}else{
True(data.blob,"audio/ogg");
};
};
startCtx.worker.postMessage({
action:"complete"
,id:startCtx.id
});
}else{
False($T("dIpw::ogg编码器未start"));
};
};
/*
var ogg = new Recorder.OggVorbisEncoder(16000, 1, -0.1);
for(var i=0;i<10*10;i++){v=ogg.encode([new Array(1600).fill().map(a=>~~(Math.random()*0x7fff))]).length; if(v)console.log(i,v)}
console.log("flush");ogg.flush().length;
*/
//转换比特率成质量数值
var GetBitRate=function(bitRate){
bitRate=Math.min(Math.max(bitRate,16),100);
//取值-0.1-1实际输出16-100kbps
var val=Math.max(1.1*(bitRate-16)/(100-16)-0.1, -0.1);
return {bitRate:bitRate,val:val};
};
}));

91
node_modules/recorder-core/src/engine/beta-webm.js generated vendored Normal file
View File

@ -0,0 +1,91 @@
/*
webm编码器beta版
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var mime="audio/webm";
var support=isBrowser&&window.MediaRecorder&&MediaRecorder.isTypeSupported(mime);
Recorder.prototype.enc_webm={
stable:false
,getTestMsg:function(){
if(!support) return $T("L49q::此浏览器不支持进行webm编码未实现MediaRecorder");
return $T("tsTW::只有比较新的浏览器支持压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法只能按照类似边播放边收听形式把数据导入到MediaRecorder有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小但音频文件中的比特率并非设定比特率采样率由于是我们自己采样的到这个编码器随他怎么搞");
}
};
Recorder.prototype.webm=function(res,True,False){
//https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder
//https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamAudioDestinationNode
if(!isBrowser){
False($T.G("NonBrowser-1",["webm encoder"]));
return;
};
if(!support){
False($T("aG4z::此浏览器不支持把录音转成webm格式"));
return;
};
var This=this, set=This.set,size=res.length,sampleRate=set.sampleRate;
var ctx=Recorder.GetContext(true);
var endCall=function(){
Recorder.CloseNewCtx(ctx);
};
var dest=ctx.createMediaStreamDestination();
dest.channelCount=1;
//录音啦
var recorder = new MediaRecorder(dest.stream,{
mimeType:mime
,bitsPerSecond:set.bitRate*1000
});
var chunks = [];
recorder.ondataavailable=function(e) {
chunks.push(e.data);
};
recorder.onstop=function(e) {
var blob=new Blob(chunks,{type:mime});
var reader=new FileReader();
reader.onloadend=function(){
endCall();
True(reader.result,mime);
};
reader.readAsArrayBuffer(blob);
};
recorder.onerror=function(e){
endCall();
False($T("PIX0::转码webm出错{1}",0,e.message));
};
recorder.start();
//声音源
var buffer=ctx.createBuffer(1,size,sampleRate);
var buffer0=buffer.getChannelData(0);
for(var j=0;j<size;j++){
var s=res[j];
s=s<0?s/0x8000:s/0x7FFF;
buffer0[j]=s;
};
var source=ctx.createBufferSource();
source.channelCount=1;
source.buffer=buffer;
source.connect(dest);
if(source.start){source.start()}else{source.noteOn(0)};
source.onended=function(){
recorder.stop();
};
}
}));

236
node_modules/recorder-core/src/engine/g711x.js generated vendored Normal file
View File

@ -0,0 +1,236 @@
/*
g711x编码器+解码器
https://github.com/xiangyuecn/Recorder
可用type
g711a: G.711 A-law (pcma)
g711u: G.711 μ-law (pcmumu-law)
编解码源码移植自https://github.com/twstx1/codec-for-audio-in-G72X-G711-G723-G726-G729/blob/master/G711_G721_G723/g711.c
移植相关测试代码FFmpeg转码播放命令assets/runtime-codes/test.g7xx.engine.js
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var regEngine=function(key,desc,enc,dec){
Recorder.prototype["enc_"+key]={
stable:true,fast:true
,getTestMsg:function(){
return $T("d8YX::{1}{2}音频文件无法直接播放可用Recorder.{2}2wav()转码成wav播放采样率比特率设置无效固定为8000hz采样率、16位每个采样压缩成8位存储音频文件大小为8000字节/秒如需任意采样率支持请使用Recorder.{2}_encode()方法",0,desc,key);
}
};
//*******标准UI线程转码支持函数************
Recorder.prototype[key]=function(res,True,False){
var This=this,set=This.set,srcSampleRate=set.sampleRate,sampleRate=8000;
set.bitRate=16;
set.sampleRate=sampleRate;
if(srcSampleRate>sampleRate){
res=Recorder.SampleData([res],srcSampleRate,sampleRate).data;
}else if(srcSampleRate<sampleRate){
False($T("29UK::数据采样率低于{1}",0,sampleRate)); return;
};
var bytes=enc(res);
True(bytes.buffer,"audio/"+key);
};
/**pcmg711x
pcm: Int16Array任意采样率pcm数据标准采样率为8000
返回Uint8Arrayg711x二进制数据采样率为pcm的采样率
**/
Recorder[key+"_encode"]=function(pcm){
return enc(pcm);
};
/**g711xpcm
bytes: Uint8Arrayg711x二进制数据采样率一般是8000
返回Int16Array为g711x的采样率16位的pcm数据
**/
Recorder[key+"_decode"]=function(bytes){
return dec(bytes);
};
/**g711xwavsrc/engine/wav.js
g711xBlob: g711x音频文件blob对象 ArrayBuffer回调也将返回ArrayBuffer采样率只支持8000
True(wavBlob,duration,mime)
False(msg)
**/
Recorder[key+"2wav"]=function(g711xBlob,True,False){
if(!Recorder.prototype.wav){
False($T.G("NeedImport-2",[key+"2wav","src/engine/wav.js"]));
return;
};
var loadOk=function(arrB,dArrB){
var bytes=new Uint8Array(arrB);
var pcm=dec(bytes);
var rec=Recorder({
type:"wav",sampleRate:8000,bitRate:16
});
if(dArrB)rec.dataType="arraybuffer";
rec.mock(pcm,8000).stop(function(wavBlob,duration,mime){
True(wavBlob,duration,mime);
},False);
};
if(g711xBlob instanceof ArrayBuffer){
loadOk(g711xBlob,1);
}else{
var reader=new FileReader();
reader.onloadend=function(){
loadOk(reader.result);
};
reader.readAsArrayBuffer(g711xBlob);
};
};
//********边录边转码支持函数g711转码超快因此也是工作在UI线程非Worker*********
Recorder.prototype[key+"_envCheck"]=function(envInfo,set){//检查环境下配置是否可用
return ""; //没有需要检查的内容
};
Recorder.prototype[key+"_start"]=function(set){//如果返回null代表不支持
set.bitRate=16;
set.sampleRate=8000;
return {set:set, memory:new Uint8Array(500000), mOffset:0};
};
var addBytes=function(cur,buf){
var bufLen=buf.length;
if(cur.mOffset+bufLen>cur.memory.length){
var tmp=new Uint8Array(cur.memory.length+Math.max(500000,bufLen));
tmp.set(cur.memory.subarray(0, cur.mOffset));
cur.memory=tmp;
}
cur.memory.set(buf,cur.mOffset);
cur.mOffset+=bufLen;
};
Recorder.prototype[key+"_stop"]=function(startCtx){
if(startCtx&&startCtx.memory){
startCtx.memory=null;
}
};
Recorder.prototype[key+"_encode"]=function(startCtx,pcm){
if(startCtx&&startCtx.memory){
var set=startCtx.set;
var bytes=enc(pcm);
if(set.takeoffEncodeChunk){
set.takeoffEncodeChunk(bytes);
}else{
addBytes(startCtx, bytes);
};
};
};
Recorder.prototype[key+"_complete"]=function(startCtx,True,False,autoStop){
if(startCtx&&startCtx.memory){
if(autoStop){
this[key+"_stop"](startCtx);
};
var buffer=startCtx.memory.buffer.slice(0,startCtx.mOffset);
True(buffer,"audio/"+key);
}else{
False($T("quVJ::{1}编码器未start",0,key));
};
};
};
var Tab=[1,2,3,3,4,4,4,4,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7];
regEngine("g711a","G.711 A-law (pcma)"
,function(pcm){//编码
var buffer=new Uint8Array(pcm.length);
for(var i=0;i<pcm.length;i++){
var pcm_val=pcm[i],mask;
if (pcm_val >= 0) {
mask = 0xD5; /* sign (7th) bit = 1 */
} else {
mask = 0x55; /* sign bit = 0 */
pcm_val = -pcm_val - 1;
}
/* Convert the scaled magnitude to segment number. */
var seg = (Tab[pcm_val>>8&0x7F]||8)-1;
/* Combine the sign, segment, and quantization bits. */
var aval = seg << 4;
if (seg < 2)
aval |= (pcm_val >> 4) & 15;
else
aval |= (pcm_val >> (seg + 3)) & 15;
buffer[i] = (aval ^ mask);
}
return buffer;
}
,function(bytes){//解码
var buffer=new Int16Array(bytes.length);
for(var i=0;i<bytes.length;i++){
var a_val=bytes[i]^0x55;
var t = (a_val & 15) << 4;
var seg = (a_val & 0x70) >> 4;
switch (seg) {
case 0:
t += 8; break;
case 1:
t += 0x108; break;
default:
t += 0x108;
t <<= seg - 1;
}
buffer[i] = ((a_val & 0x80) ? t : -t);
}
return buffer;
});
regEngine("g711u","G.711 μ-law (pcmu、mu-law)"
,function(pcm){//编码
var buffer=new Uint8Array(pcm.length);
for(var i=0;i<pcm.length;i++){
var pcm_val=pcm[i],mask;
/* Get the sign and the magnitude of the value. */
if (pcm_val < 0) {
pcm_val = 0x84 - pcm_val;
mask = 0x7F;
} else {
pcm_val += 0x84;
mask = 0xFF;
}
/* Convert the scaled magnitude to segment number. */
var seg = (Tab[pcm_val>>8&0x7F]||8)-1;
var uval = (seg << 4) | ((pcm_val >> (seg + 3)) & 0xF);
buffer[i] = (uval ^ mask);
}
return buffer;
}
,function(bytes){//解码
var buffer=new Int16Array(bytes.length);
for(var i=0;i<bytes.length;i++){
var u_val= ~bytes[i];
var t = ((u_val & 15) << 3) + 0x84;
t <<= (u_val & 0x70) >> 4;
buffer[i] = ((u_val & 0x80) ? (0x84 - t) : (t - 0x84));
}
return buffer;
});
}));

11401
node_modules/recorder-core/src/engine/mp3-engine.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

533
node_modules/recorder-core/src/engine/mp3.js generated vendored Normal file
View File

@ -0,0 +1,533 @@
/*
mp3编码器需带上src/engine/mp3-engine.js引擎使用
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var SampleS="48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000";
var BitS="8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320";
Recorder.prototype.enc_mp3={
stable:true,takeEC:"full"
,getTestMsg:function(){
return $T("Zm7L::采样率范围:{1};比特率范围:{2}不同比特率支持的采样率范围不同小于32kbps时采样率需小于32000",0,SampleS,BitS);
}
};
var NormalizeSet=function(set){
var bS=set.bitRate, sS=set.sampleRate,s=sS;
if((" "+BitS+",").indexOf(" "+bS+",")==-1){
Recorder.CLog($T("eGB9::{1}不在mp3支持的取值范围{2}",0,"bitRate="+bS,BitS),3);
}
if((" "+SampleS+",").indexOf(" "+sS+",")==-1){//engine SmpFrqIndex函数会检测
var arr=SampleS.split(", "),vs=[];
for(var i=0;i<arr.length;i++) vs.push({v:+arr[i],s:Math.abs(arr[i]-sS)});
vs.sort(function(a,b){return a.s-b.s}); s=vs[0].v;
set.sampleRate=s;
Recorder.CLog($T("zLTa::sampleRate已更新为{1},因为{2}不在mp3支持的取值范围{3}",0,s,sS,SampleS),3);
}
};
var ImportEngineErr=function(){
return $T.G("NeedImport-2",["mp3.js","src/engine/mp3-engine.js"]);
};
//是否支持web worker
var HasWebWorker=isBrowser && typeof Worker=="function";
//*******标准UI线程转码支持函数************
Recorder.prototype.mp3=function(res,True,False){
var This=this,set=This.set,size=res.length;
if(!Recorder.lamejs){
False(ImportEngineErr()); return;
};
//优先采用worker编码非worker时用老方法提供兼容
if(HasWebWorker){
var ctx=This.mp3_start(set);
if(ctx){
if(ctx.isW){
This.mp3_encode(ctx,res);
This.mp3_complete(ctx,True,False,1);
return;
}
This.mp3_stop(ctx);
};
};
NormalizeSet(set);
//https://github.com/wangpengfei15975/recorder.js
//https://github.com/zhuker/lamejs bug:采样率必须和源一致不然8k时没有声音有问题fixhttps://github.com/zhuker/lamejs/pull/11
var mp3=new Recorder.lamejs.Mp3Encoder(1,set.sampleRate,set.bitRate);
var blockSize=57600;
var memory=new Int8Array(500000), mOffset=0;
var idx=0,isFlush=0;
var run=function(){
try{
if(idx<size){
var buf=mp3.encodeBuffer(res.subarray(idx,idx+blockSize));
}else{
isFlush=1;
var buf=mp3.flush();
};
}catch(e){ //精简代码调用了abort
console.error(e);
if(!isFlush) try{ mp3.flush() }catch(r){ console.error(r) }
False("MP3 Encoder: "+e.message);
return;
};
var bufLen=buf.length;
if(bufLen>0){
if(mOffset+bufLen>memory.length){
var tmp=new Int8Array(memory.length+Math.max(500000,bufLen));
tmp.set(memory.subarray(0, mOffset));
memory=tmp;
}
memory.set(buf,mOffset);
mOffset+=bufLen;
};
if(idx<size){
idx+=blockSize;
setTimeout(run);//尽量避免卡ui
}else{
var data=[memory.buffer.slice(0,mOffset)];
//去掉开头的标记信息帧
var meta=mp3TrimFix.fn(data,mOffset,size,set.sampleRate);
mp3TrimFixSetMeta(meta,set);
True(data[0]||new ArrayBuffer(0),"audio/mp3");
};
};
run();
}
//********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码*********
//全局共享一个Worker后台串行执行。如果每次都开一个新的编码速度可能会慢很多可能是浏览器运行缓存的因素并且可能瞬间产生多个并行操作占用大量cpu
var mp3Worker;
Recorder.BindDestroy("mp3Worker",function(){
if(mp3Worker){
Recorder.CLog("mp3Worker Destroy");
mp3Worker.terminate();
mp3Worker=null;
};
});
Recorder.prototype.mp3_envCheck=function(envInfo,set){//检查环境下配置是否可用
var errMsg="";
//需要实时编码返回数据,此时需要检查是否可实时编码
if(set.takeoffEncodeChunk){
if(!newContext()){//浏览器不能创建实时编码环境
errMsg=$T("yhUs::当前浏览器版本太低,无法实时处理");
};
};
if(!errMsg && !Recorder.lamejs){
errMsg=ImportEngineErr();
};
return errMsg;
};
Recorder.prototype.mp3_start=function(set){//如果返回null代表不支持
return newContext(set);
};
var openList={id:0};
var newContext=function(setOrNull,_badW){
//独立运行的函数scope.wkScope worker.onmessage 字符串会被替换
var run=function(e){
var ed=e.data;
var wk_ctxs=scope.wkScope.wk_ctxs;
var wk_lame=scope.wkScope.wk_lame;
var wk_mp3TrimFix=scope.wkScope.wk_mp3TrimFix;
var cur=wk_ctxs[ed.id];
if(ed.action=="init"){
wk_ctxs[ed.id]={
sampleRate:ed.sampleRate
,bitRate:ed.bitRate
,takeoff:ed.takeoff
,pcmSize:0
,memory:new Int8Array(500000), mOffset:0
,encObj:new wk_lame.Mp3Encoder(1,ed.sampleRate,ed.bitRate)
};
}else if(!cur){
return;
};
var addBytes=function(buf){
var bufLen=buf.length;
if(cur.mOffset+bufLen>cur.memory.length){
var tmp=new Int8Array(cur.memory.length+Math.max(500000,bufLen));
tmp.set(cur.memory.subarray(0, cur.mOffset));
cur.memory=tmp;
}
cur.memory.set(buf,cur.mOffset);
cur.mOffset+=bufLen;
};
switch(ed.action){
case "stop":
if(!cur.isCp) try{ cur.encObj.flush() }catch(e){ console.error(e) }
cur.encObj=null;
delete wk_ctxs[ed.id];
break;
case "encode":
if(cur.isCp)break;
cur.pcmSize+=ed.pcm.length;
try{
var buf=cur.encObj.encodeBuffer(ed.pcm);
}catch(e){ //精简代码调用了abort
cur.err=e;
console.error(e);
};
if(buf && buf.length>0){
if(cur.takeoff){
worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
addBytes(buf);
};
};
break;
case "complete":
cur.isCp=1;
try{
var buf=cur.encObj.flush();
}catch(e){ //精简代码调用了abort
cur.err=e;
console.error(e);
};
if(buf && buf.length>0){
if(cur.takeoff){
worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
addBytes(buf);
};
};
if(cur.err){
worker.onmessage({action:ed.action,id:ed.id
,err:"MP3 Encoder: "+cur.err.message});
break;
};
var data=[cur.memory.buffer.slice(0,cur.mOffset)];
//去掉开头的标记信息帧
var meta=wk_mp3TrimFix.fn(data,cur.mOffset,cur.pcmSize,cur.sampleRate);
worker.onmessage({
action:ed.action
,id:ed.id
,blob:data[0]||new ArrayBuffer(0)
,meta:meta
});
break;
};
};
var initOnMsg=function(isW){
worker.onmessage=function(e){
var data=e; if(isW)data=e.data;
var ctx=openList[data.id];
if(ctx){
if(data.action=="takeoff"){
//取走实时生成的mp3数据
ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer));
}else{
//complete
ctx.call&&ctx.call(data);
ctx.call=null;
};
};
};
};
var initCtx=function(){
var ctx={worker:worker,set:setOrNull};
if(setOrNull){
ctx.id=++openList.id;
openList[ctx.id]=ctx;
NormalizeSet(setOrNull);
worker.postMessage({
action:"init"
,id:ctx.id
,sampleRate:setOrNull.sampleRate
,bitRate:setOrNull.bitRate
,takeoff:!!setOrNull.takeoffEncodeChunk
,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
}else{
worker.postMessage({
x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
};
return ctx;
};
var scope,worker=mp3Worker;
//非浏览器不支持worker或者开启失败使用UI线程处理
if(_badW || !HasWebWorker){
Recorder.CLog($T("k9PT::当前环境不支持Web Workermp3实时编码器运行在主线程中"),3);
worker={ postMessage:function(ed){ run({data:ed}); } };
scope={wkScope:{
wk_ctxs:{}, wk_lame:Recorder.lamejs, wk_mp3TrimFix:mp3TrimFix
}};
initOnMsg();
return initCtx();
};
try{
if(!worker){
//创建一个新Worker
var onmsg=(run+"").replace(/[\w\$]+\.onmessage/g,"self.postMessage");
onmsg=onmsg.replace(/[\w\$]+\.wkScope/g,"wkScope");
var jsCode=");wk_lame();self.onmessage="+onmsg;
jsCode+=";var wkScope={ wk_ctxs:{},wk_lame:wk_lame";
jsCode+=",wk_mp3TrimFix:{rm:"+mp3TrimFix.rm+",fn:"+mp3TrimFix.fn+"} }";
var lamejsCode=Recorder.lamejs.toString();
var url=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_lame=(",lamejsCode,jsCode], {type:"text/javascript"}));
worker=new Worker(url);
setTimeout(function(){
(window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
},10000);//chrome 83 file协议下如果直接释放将会使WebWorker无法启动
initOnMsg(1);
};
var ctx=initCtx(); ctx.isW=1;
mp3Worker=worker;
return ctx;
}catch(e){//出错了就不要提供了
worker&&worker.terminate();
console.error(e);
return newContext(setOrNull, 1);//切换到UI线程处理
};
};
Recorder.prototype.mp3_stop=function(startCtx){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"stop"
,id:startCtx.id
});
startCtx.worker=null;
delete openList[startCtx.id];
//疑似泄露检测 排除id
var opens=-1;
for(var k in openList){
opens++;
};
if(opens){
Recorder.CLog($T("fT6M::mp3 worker剩{1}个未stop",0,opens),3);
};
};
};
Recorder.prototype.mp3_encode=function(startCtx,pcm){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"encode"
,id:startCtx.id
,pcm:pcm
});
};
};
Recorder.prototype.mp3_complete=function(startCtx,True,False,autoStop){
var This=this;
if(startCtx&&startCtx.worker){
startCtx.call=function(data){
if(autoStop){
This.mp3_stop(startCtx);
};
if(data.err){
False(data.err);
}else{
mp3TrimFixSetMeta(data.meta,startCtx.set);
True(data.blob,"audio/mp3");
};
};
startCtx.worker.postMessage({
action:"complete"
,id:startCtx.id
});
}else{
False($T("mPxH::mp3编码器未start"));
};
};
//*******辅助函数************
/*lamejsmp3null
mp3Buffers=[ArrayBuffer,...]
length=mp3Buffers的数据二进制总长度
*/
Recorder.mp3ReadMeta=function(mp3Buffers,length){
//kill babel-polyfill ES6 Number.parseInt 不然放到Worker里面找不到方法也不能用typeof(x)==object 会被替换成 _typeof
var parseInt_ES3=typeof(window)!="undefined"&&window.parseInt||typeof(self)!="undefined"&&self.parseInt||parseInt;
var u8arr0=new Uint8Array(mp3Buffers[0]||[]);
if(u8arr0.length<4){
return null;
};
var byteAt=function(idx,u8){
return ("0000000"+((u8||u8arr0)[idx]||0).toString(2)).substr(-8);
};
var b2=byteAt(0)+byteAt(1);
var b4=byteAt(2)+byteAt(3);
if(!/^1{11}/.test(b2)){//未发现帧同步
return null;
};
var version=({"00":2.5,"10":2,"11":1})[b2.substr(11,2)];
var layer=({"01":3})[b2.substr(13,2)];//仅支持Layer3
var sampleRate=({ //lamejs -> Tables.samplerate_table
"1":[44100, 48000, 32000]
,"2":[22050, 24000, 16000]
,"2.5":[11025, 12000, 8000]
})[version];
sampleRate&&(sampleRate=sampleRate[parseInt_ES3(b4.substr(4,2),2)]);
var bitRate=[ //lamejs -> Tables.bitrate_table
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] //MPEG 2 2.5
,[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]//MPEG 1
][version==1?1:0][parseInt_ES3(b4.substr(0,4),2)];
if(!version || !layer || !bitRate || !sampleRate){
return null;
};
var duration=Math.round(length*8/bitRate);
var frame=layer==1?384:layer==2?1152:version==1?1152:576;
var frameDurationFloat=frame/sampleRate*1000;
var frameSize=Math.floor((frame*bitRate)/8/sampleRate*1000);
//检测是否存在Layer3帧填充1字节。这里只获取第二帧的填充信息首帧永远没有填充。其他帧可能隔一帧出现一个填充或者隔很多帧出现一个填充目测是取决于frameSize未舍入时的小数部分因为有些采样率的frameSize会出现小数11025、22050、44100 典型的除不尽),然后字节数无法表示这种小数,就通过一定步长来填充弥补小数部分丢失
var hasPadding=0,seek=0;
for(var i=0;i<mp3Buffers.length;i++){
//寻找第二帧
var buf=mp3Buffers[i];
seek+=buf.byteLength;
if(seek>=frameSize+3){
var buf8=new Uint8Array(buf);
var idx=buf.byteLength-(seek-(frameSize+3)+1);
var ib4=byteAt(idx,buf8);
hasPadding=ib4.charAt(6)=="1";
break;
};
};
if(hasPadding){
frameSize++;
};
return {
version:version //1 2 2.5 -> MPEG1 MPEG2 MPEG2.5
,layer:layer//3 -> Layer3
,sampleRate:sampleRate //采样率 hz
,bitRate:bitRate //比特率 kbps
,duration:duration //音频时长 ms
,size:length //总长度 byte
,hasPadding:hasPadding //是否存在1字节填充首帧永远没有这个值其实代表的第二帧是否有填充并不代表其他帧的
,frameSize:frameSize //每帧最大长度含可能存在的1字节padding byte
,frameDurationFloat:frameDurationFloat //每帧时长,含小数 ms
};
};
//去掉lamejs开头的标记信息帧免得mp3解码出来的时长比pcm的长太多
var mp3TrimFix={//minfiy keep name
rm:Recorder.mp3ReadMeta
,fn:function(mp3Buffers,length,pcmLength,pcmSampleRate){
var meta=this.rm(mp3Buffers,length);
if(!meta){
return {size:length, err:"mp3 unknown format"};
};
var pcmDuration=Math.round(pcmLength/pcmSampleRate*1000);
//开头多出这么多帧移除掉正常情况下最多为2帧
var num=Math.floor((meta.duration-pcmDuration)/meta.frameDurationFloat);
if(num>0){
var size=num*meta.frameSize-(meta.hasPadding?1:0);//首帧没有填充第二帧可能有填充这里假设最多为2帧测试并未出现3帧以上情况其他帧不管就算出现了并且导致了错误后面自动容错
length-=size;
var arr0=0,arrs=[];
for(var i=0;i<mp3Buffers.length;i++){
var arr=mp3Buffers[i];
if(size<=0){
break;
};
if(size>=arr.byteLength){
size-=arr.byteLength;
arrs.push(arr);
mp3Buffers.splice(i,1);
i--;
}else{
mp3Buffers[i]=arr.slice(size);
arr0=arr;
size=0;
};
};
var checkMeta=this.rm(mp3Buffers,length);
if(!checkMeta){
//还原变更,应该不太可能会出现
arr0&&(mp3Buffers[0]=arr0);
for(var i=0;i<arrs.length;i++){
mp3Buffers.splice(i,0,arrs[i]);
};
meta.err="mp3 fix error: 已还原,错误原因不明"; //worker里面没$T翻译
};
var fix=meta.trimFix={};
fix.remove=num;
fix.removeDuration=Math.round(num*meta.frameDurationFloat);
fix.duration=Math.round(length*8/meta.bitRate);
};
return meta;
}
};
var mp3TrimFixSetMeta=function(meta,set){
var tag="MP3 Info: ";
if(meta.sampleRate&&meta.sampleRate!=set.sampleRate || meta.bitRate&&meta.bitRate!=set.bitRate){
Recorder.CLog(tag+$T("uY9i::和设置的不匹配{1},已更新成{2}",0,"set:"+set.bitRate+"kbps "+set.sampleRate+"hz","set:"+meta.bitRate+"kbps "+meta.sampleRate+"hz"),3,set);
set.sampleRate=meta.sampleRate;
set.bitRate=meta.bitRate;
};
var trimFix=meta.trimFix;
if(trimFix){
tag+=$T("iMSm::Fix移除{1}帧",0,trimFix.remove)+" "+trimFix.removeDuration+"ms -> "+trimFix.duration+"ms";
if(trimFix.remove>2){
meta.err=(meta.err?meta.err+", ":"")+$T("b9zm::移除帧数过多");
};
}else{
tag+=(meta.duration||"-")+"ms";
};
if(meta.err){
Recorder.CLog(tag,meta.size?1:0,meta.err,meta);
}else{
Recorder.CLog(tag,meta);
};
};
}));

174
node_modules/recorder-core/src/engine/pcm.js generated vendored Normal file
View File

@ -0,0 +1,174 @@
/*
pcm编码器+编码引擎
https://github.com/xiangyuecn/Recorder
编码原理本编码器输出的pcm格式数据其实就是Recorder中的buffers原始数据经过了重新采样16位时为LE小端模式Little Endian并未经过任何编码处理
编码的代码和wav.js区别不大pcm加上一个44字节wav头即成wav文件所以要播放pcm就很简单了直接转成wav文件来播放已提供转换函数 Recorder.pcm2wav
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
Recorder.prototype.enc_pcm={
stable:true,fast:true
,getTestMsg:function(){
return $T("fWsN::pcm为未封装的原始音频数据pcm音频文件无法直接播放可用Recorder.pcm2wav()转码成wav播放支持位数8位、16位填在比特率里面采样率取值无限制");
}
};
var NormalizeSet=function(set){
var bS=set.bitRate,b=bS==8?8:16;
if(bS!=b) Recorder.CLog($T("uMUJ::PCM Info: 不支持{1}位,已更新成{2}位",0,bS,b),3);
set.bitRate=b;
};
//*******标准UI线程转码支持函数************
Recorder.prototype.pcm=function(res,True,False){
var set=this.set;
NormalizeSet(set);
var bytes=PcmEncode(res,set.bitRate);
True(bytes.buffer,"audio/pcm");
};
var PcmEncode=function(pcm,bitRate){
if(bitRate==8) {
var size=pcm.length;
var bytes=new Uint8Array(size);
for(var i=0;i<size;i++){
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点
var val=(pcm[i]>>8)+128;
bytes[i]=val;
};
}else{
pcm=new Int16Array(pcm); //复制一份
var bytes=new Uint8Array(pcm.buffer);
};
return bytes;
};
/**pcmwavsrc/engine/wav.js
data: {
sampleRate:16000 pcm的采样率
bitRate:16 pcm的位数 取值8 16
blob:blob对象 ArrayBuffer回调也将返回ArrayBuffer
}
data如果直接提供的blob将默认使用16位16khz的配置仅用于测试
True(wavBlob,duration,mime)
False(msg)
**/
Recorder.pcm2wav=function(data,True,False){
if(!data.blob){//Blob 测试用
data={blob:data};
};
var blob=data.blob;
var sampleRate=data.sampleRate||16000,bitRate=data.bitRate||16;
if(!data.sampleRate || !data.bitRate){
Recorder.CLog($T("KmRz::pcm2wav必须提供sampleRate和bitRate"),3);
};
if(!Recorder.prototype.wav){
False($T.G("NeedImport-2",["pcm2wav","src/engine/wav.js"]));
return;
};
var loadOk=function(arrB,dArrB){
var pcm;
if(bitRate==8){
//8位转成16位
var u8arr=new Uint8Array(arrB);
pcm=new Int16Array(u8arr.length);
for(var j=0;j<u8arr.length;j++){
pcm[j]=(u8arr[j]-128)<<8;
};
}else{
pcm=new Int16Array(arrB);
};
var rec=Recorder({
type:"wav"
,sampleRate:sampleRate
,bitRate:bitRate
});
if(dArrB)rec.dataType="arraybuffer";
rec.mock(pcm,sampleRate).stop(function(wavBlob,duration,mime){
True(wavBlob,duration,mime);
},False);
};
if(blob instanceof ArrayBuffer){
loadOk(blob,1);
}else{
var reader=new FileReader();
reader.onloadend=function(){
loadOk(reader.result);
};
reader.readAsArrayBuffer(blob);
};
};
//********边录边转码支持函数pcm转码超快因此也是工作在UI线程非Worker*********
Recorder.prototype.pcm_envCheck=function(envInfo,set){//检查环境下配置是否可用
return ""; //没有需要检查的内容
};
Recorder.prototype.pcm_start=function(set){//如果返回null代表不支持
NormalizeSet(set);
return {set:set, memory:new Uint8Array(500000), mOffset:0};
};
var addBytes=function(cur,buf){
var bufLen=buf.length;
if(cur.mOffset+bufLen>cur.memory.length){
var tmp=new Uint8Array(cur.memory.length+Math.max(500000,bufLen));
tmp.set(cur.memory.subarray(0, cur.mOffset));
cur.memory=tmp;
}
cur.memory.set(buf,cur.mOffset);
cur.mOffset+=bufLen;
};
Recorder.prototype.pcm_stop=function(startCtx){
if(startCtx&&startCtx.memory){
startCtx.memory=null;
}
};
Recorder.prototype.pcm_encode=function(startCtx,pcm){
if(startCtx&&startCtx.memory){
var set=startCtx.set;
var bytes=PcmEncode(pcm, set.bitRate);
if(set.takeoffEncodeChunk){
set.takeoffEncodeChunk(bytes);
}else{
addBytes(startCtx, bytes);
};
};
};
Recorder.prototype.pcm_complete=function(startCtx,True,False,autoStop){
if(startCtx&&startCtx.memory){
if(autoStop){
this.pcm_stop(startCtx);
};
var buffer=startCtx.memory.buffer.slice(0,startCtx.mOffset);
True(buffer,"audio/pcm");
}else{
False($T("sDkA::pcm编码器未start"));
};
};
}));

122
node_modules/recorder-core/src/engine/wav.js generated vendored Normal file
View File

@ -0,0 +1,122 @@
/*
wav编码器+编码引擎
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
编码原理给pcm数据加上一个44字节的wav头即成wav文件pcm数据就是Recorder中的buffers原始数据重新采样16位时为LE小端模式Little Endian实质上是未经过任何编码处理
注意其他wav编码器可能不是44字节的头要从任意wav文件中提取pcm数据请参考assets/runtime-codes/fragment.decode.wav.js
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
Recorder.prototype.enc_wav={
stable:true,fast:true
,getTestMsg:function(){
return $T("gPSE::支持位数8位、16位填在比特率里面采样率取值无限制此编码器仅在pcm数据前加了一个44字节的wav头编码出来的16位wav文件去掉开头的44字节即可得到pcm其他wav编码器可能不是44字节");
}
};
var NormalizeSet=function(set){
var bS=set.bitRate,b=bS==8?8:16;
if(bS!=b) Recorder.CLog($T("wyw9::WAV Info: 不支持{1}位,已更新成{2}位",0,bS,b),3);
set.bitRate=b;
};
Recorder.prototype.wav=function(res,True,False){
var This=this,set=This.set;
NormalizeSet(set);
var size=res.length,sampleRate=set.sampleRate,bitRate=set.bitRate;
var dataLength=size*(bitRate/8);
//生成wav头
var header=Recorder.wav_header(1,1,sampleRate,bitRate,dataLength);
var offset=header.length;
var bytes=new Uint8Array(offset+dataLength);
bytes.set(header);
// 写入采样数据
if(bitRate==8) {
for(var i=0;i<size;i++) {
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点
var val=(res[i]>>8)+128;
bytes[offset++]=val;
};
}else{
bytes=new Int16Array(bytes.buffer);//长度一定是偶数
bytes.set(res,offset/2);
};
True(bytes.buffer,"audio/wav");
};
/**
根据参数生成wav文件头返回Uint8Arrayformat=1时固定返回44字节其他返回46字节
format: 1 (raw pcm) 2 (ADPCM) 3 (IEEE Float) 6 (g711a) 7 (g711u)
numCh: 声道数
dataLength: wav中的音频数据二进制长度
**/
Recorder.wav_header=function(format,numCh,sampleRate,bitRate,dataLength){
//文件头 http://soundfile.sapp.org/doc/WaveFormat/ https://www.jianshu.com/p/63d7aa88582b https://github.com/mattdiamond/Recorderjs https://www.cnblogs.com/blqw/p/3782420.html https://www.cnblogs.com/xiaoqi/p/6993912.html
var extSize=format==1?0:2;
var buffer=new ArrayBuffer(44+extSize);
var data=new DataView(buffer);
var offset=0;
var writeString=function(str){
for (var i=0;i<str.length;i++,offset++) {
data.setUint8(offset,str.charCodeAt(i));
};
};
var write16=function(v){
data.setUint16(offset,v,true);
offset+=2;
};
var write32=function(v){
data.setUint32(offset,v,true);
offset+=4;
};
/* RIFF identifier */
writeString('RIFF');
/* RIFF chunk length */
write32(36+extSize+dataLength);
/* RIFF type */
writeString('WAVE');
/* format chunk identifier */
writeString('fmt ');
/* format chunk length */
write32(16+extSize);
/* audio format */
write16(format);
/* channel count */
write16(numCh);
/* sample rate */
write32(sampleRate);
/* byte rate (sample rate * block align) */
write32(sampleRate*(numCh*bitRate/8));// *1 声道
/* block align (channel count * bytes per sample) */
write16(numCh*bitRate/8);// *1 声道
/* bits per sample */
write16(bitRate);
if(format!=1){// ExtraParamSize 0
write16(0);
}
/* data chunk identifier */
writeString('data');
/* data chunk length */
write32(dataLength);
return new Uint8Array(buffer);
};
}));

View File

@ -0,0 +1,910 @@
/*
录音 Recorder扩展ASR阿里云语音识别语音转文字支持实时语音识别单个音频文件转文字
https://github.com/xiangyuecn/Recorder
- 本扩展通过调用 阿里云-智能语音交互-一句话识别 接口来进行语音识别无时长限制
- 识别过程中采用WebSocket直连阿里云语音数据无需经过自己服务器
- 自己服务器仅需提供一个Token生成接口即可本库已实现一个本地测试NodeJs后端程序 /assets/demo-asr/NodeJsServer_asr.aliyun.short.js
本扩展单次语音识别时虽长无限制最佳使用场景还是1-5分钟内的语音识别60分钟以上的语音识别本扩展也能胜任需自行进行重试容错处理但太长的识别场景不太适合使用阿里云一句话识别阿里云单次一句话识别最长60秒本扩展自带拼接过程所以无时长限制为什么采用一句话识别因为便宜
对接流程
1. 到阿里云开通 一句话识别 服务可试用一段时间正式使用时应当开通商用版很便宜得到AccessKeySecret参考https://help.aliyun.com/document_detail/324194.html
2. 到阿里云智能语音交互控制台创建相应的语音识别项目并配置好项目得到Appkey每个项目可以设置一种语言模型要支持多种语言就创建多个项目
3. 需要后端提供一个Token生成接口用到上面的Key和Secret可直接参考或本地运行此NodeJs后端测试程序/assets/demo-asr/NodeJsServer_asr.aliyun.short.js配置好代码里的阿里云账号后在目录内直接命令行执行`node NodeJsServer_asr.aliyun.short.js`即可运行提供本地测试接口
4. 前端调用ASR_Aliyun_Short传入tokenApi即可很简单的实现语音识别功能
在线测试例子
https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.asr.aliyun.short
调用示例
var rec=Recorder(recSet);rec.open(...) //进行语音识别前,先打开录音,获得录音权限
var asr=Recorder.ASR_Aliyun_Short(set); //创建asr对象参数详情请参考下面的源码
//asr创建好后随时调用strat开始进行语音识别
asr.start(function(){
rec.start();//一般在start成功之后调用rec.start()开始录音,此时可以通知用户讲话了
},fail);
//实时处理输入音频数据一般是在rec.set.onProcess中调用本方法输入实时录制的音频数据输入的数据将会发送语音识别不管有没有start都可以调用本方法start前输入的数据会缓冲起来等到start后进行识别
asr.input([[Int16,...],...],48000,0);
//话讲完后调用stop结束语音识别得到识别到的内容文本
asr.stop(function(text,abortMsg){
//text为识别到的最终完整内容如果存在abortMsg代表识别中途被某种错误停止了text是停止前的内容识别到的完整内容一般早在asrProcess中会收到abort事件然后要停止录音
},fail);
更多的方法
asr.inputDuration() 获取input已输入的音频数据总时长单位ms
asr.sendDuration() 获取已发送识别的音频数据总时长存在重发重叠部分因此比inputDuration长
asr.asrDuration() 获取已识别的音频数据总时长去除了sendDuration的重叠部分<=inputDuration
asr.getText() 获取实时结果文本如果已stop返回的就是最终文本一般无需调用此方法因为回调中都提供了此方法的返回值
//一次性将单个完整音频Blob文件转成文字无需start、stop创建好asr后直接调用本方法即可
asr.audioToText(audioBlob,success,fail)
//一次性的将单个完整PCM音频数据转成文字无需start、stop创建好asr后直接调用本方法即可
asr.pcmToText(buffer,sampleRate,success,fail)
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var ASR_Aliyun_Short=function(set){
return new fn(set);
};
var ASR_Aliyun_ShortTxt="ASR_Aliyun_Short";
var fn=function(set){
var This=this;
var o={
tokenApi:"" /*必填调用阿里云一句话识别需要的token获取api地址
接口实现请参考本地测试NodeJs后端程序/assets/demo-asr/NodeJsServer_asr.aliyun.short.js
此接口默认需要返回数据格式
{
c:0 //code0接口调用正常其他数值接口调用出错
,m:"" //message接口调用出错时的错误消息
,v:{ //value接口成功调用返回的结果【结果中必须包含下面两个值】
appkey:"aaaa" //lang语言模型对应的项目appkey
,token:"bbbb" //语音识别Access Token
}
}
如果不是返回的这个格式的数据必须提供apiRequest配置自行请求api*/
,apiArgs:{ //请求tokenApi时要传的参数
action:"token"
,lang:"普通话" //语言模型设置具体取值取决于tokenApi支持了哪些语言
}
,apiRequest:null /*tokenApi的请求实现方法默认使用简单的ajax实现
如果你接口返回的数据格式和默认格式不一致必须提供一个函数来自行请求api
方法参数fn(url,args,success,fail)
url:"" == tokenApi
args:{} == apiArgs
success:fn(value) 接口调用成功回调value={appkey:"", token:""}
fail:fn(errMsg) 接口调用出错回调errMsg="错误消息"
*/
,compatibleWebSocket:null /*提供一个函数返回兼容WebSocket的对象一般也需要提供apiRequest
如果你使用的环境不支持WebSocket需要提供一个函数来返回一个兼容实现对象
方法参数fn(url) url为连接地址返回一个对象需支持的回调和方法{
onopen:fn() 连接成功回调
onerror:fn({message}) 连接失败回调
onclose:fn({code, reason}) 连接关闭回调
onmessage:fn({data}) 收到消息回调
connect:fn() 进行连接
close:fn(code,reason) 关闭连接
send:fn(data) 发送数据data为字符串或者arraybuffer
}
binaryType固定使用arraybuffer类型
*/
//,asrProcess:null //fn(text,nextDuration,abortMsg) 当实时接收到语音识别结果时的回调函数(对单个完整音频文件的识别也有效)
//此方法需要返回true才会继续识别否则立即当做识别超时处理你应当通过nextDuration来决定是否继续识别避免无限制的识别大量消耗阿里云资源额度如果不提供本回调默认1分钟超时后终止识别(因为没有绑定回调,你不知道已经被终止了)
//text为中间识别到的内容并非已有录音片段的最终结果后续可能会根据语境修整
//nextDuration 为当前回调时下次即将进行识别的总时长单位毫秒通过这个参数来限制识别总时长超过时长就返回false终止识别第二分钟开始每分钟会多识别前一分钟结尾的5秒数据用于两分钟之间的拼接相当于第二分钟最多识别55秒的新内容
//abortMsg如不为空代表识别中途因为某种原因终止了识别比如超时、接口调用失败收到此信息时应当立即调用asr的stop方法得到最终结果并且终止录音
,log:NOOP //fn(msg,color)提供一个日志输出接口默认只会输出到控制台color 1:红色2绿色不为空时为颜色字符串
//高级选项
,fileSpeed:6 //单个文件识别发送速度控制取值1-n1为按播放速率发送最慢识别精度完美6按六倍播放速度发送花10秒识别60秒文件比较快精度还行再快测试发现似乎会缺失内容可能是发送太快底层识别不过来导致返回的结果缺失。
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
This.state=0;//0 未start1 start2 stop
This.started=0;
This.sampleRate=16000;//发送的采样率
//This.tokenData
This.pcmBuffers=[];//等待发送的缓冲数据
This.pcmTotal=0;//输入的总量
This.pcmOffset=0;//缓冲[0]的已发送位置
This.pcmSend=0;//发送的总量,不会重复计算重发的量
This.joinBuffers=[];//下一分钟左移5秒和上一分钟重叠5秒
This.joinSize=0;//左移的数据量
This.joinSend=0;//单次已发送量
This.joinOffset=-1;//左移[0]的已发送位置,-1代表可以进行整理buffers
This.joinIsOpen=0;//是否开始发送
This.joinSendTotal=0;//已发送重叠的总量
This.sendCurSize=0;//单个wss发送量不能超过1分钟的量
This.sendTotal=0;//总计的发送量,存在重发重叠部分
//This.stopWait=null
//This.sendWait=0
//This.sendAbort=false
//This.sendAbortMsg=""
//This.wsCur 当前的wss
//This.wsLock 新的一分钟wss准备
This.resTxts=[];//每分钟结果列表 resTxt object: {tempTxt:"efg",okTxt:"efgh",fullTxt:"abcdefgh"}
if(!set.asrProcess){
This.log("未绑定asrProcess回调无法感知到abort事件",3);
};
};
var CLog=function(){
var v=arguments; v[0]="["+ASR_Aliyun_ShortTxt+"]"+v[0];
Recorder.CLog.apply(null,v);
};
fn.prototype=ASR_Aliyun_Short.prototype={
log:function(msg,color){
CLog(msg,typeof color=="number"?color:0);
this.set.log("["+ASR_Aliyun_ShortTxt+"]"+msg,color==3?"#f60":color);
}
//input已输入的音频数据总时长
,inputDuration:function(){
return Math.round(this.pcmTotal/this.sampleRate*1000);
}
//已发送识别的音频数据总时长存在重发重叠部分因此比inputDuration长
,sendDuration:function(add){
var size=this.sendTotal;
size+=add||0;
return Math.round(size/this.sampleRate*1000);
}
//已识别的音频数据总时长去除了sendDuration的重叠部分值<=inputDuration
,asrDuration:function(){
return this.sendDuration(-this.joinSendTotal);
}
/**,mp3wavPCM -> start -> input ... input -> stop
blob:Blob 音频文件Blob对象rec.stop得到的录音结果file input选择的文件XMLHttpRequest的blob结果new Blob([TypedArray])创建的blob
success fn(text,abortMsg) text为识别到的完整内容,abortMsg参考stop
fail:fn(errMsg)
**/
,audioToText:function(blob,success,fail){
var This=this;
var failCall=function(err){
This.log(err,1);
fail&&fail(err);
};
if(!Recorder.GetContext()){//强制激活Recorder.Ctx 不支持大概率也不支持解码
failCall("浏览器不支持音频解码");
return;
};
var reader=new FileReader();
reader.onloadend=function(){
var ctx=Recorder.Ctx;
ctx.decodeAudioData(reader.result,function(raw){
var src=raw.getChannelData(0);
var sampleRate=raw.sampleRate;
var pcm=new Int16Array(src.length);
for(var i=0;i<src.length;i++){//floatTo16BitPCM
var s=Math.max(-1,Math.min(1,src[i]));
s=s<0?s*0x8000:s*0x7FFF;
pcm[i]=s;
};
This.pcmToText(pcm,sampleRate,success,fail);
},function(e){
failCall("音频解码失败["+blob.type+"]:"+e.message);
});
};
reader.readAsArrayBuffer(blob);
}
/**:start -> input ... input -> stop
buffer:[Int16,...] 16位单声道音频pcm数据一维数组
sampleRate pcm的采样率
success fn(text,abortMsg) text为识别到的完整内容,abortMsg参考stop
fail:fn(errMsg)
**/
,pcmToText:function(buffer,sampleRate,success,fail){
var This=this;
This.start(function(){
This.log("单个文件"+Math.round(buffer.length/sampleRate*1000)+"ms转文字");
This.sendSpeed=This.set.fileSpeed;
This.input([buffer],sampleRate);
This.stop(success,fail);
},fail);
}
/**inputstopstartinputstart
建议在success回调中开始录音即rec.start当然asr.start和rec.start同时进行调用或者任意一个先调用都是允许的不过当出现fail时需要处理好asr和rec各自的状态
无需特殊处理start和stop的关系只要调用了stop会阻止未完成的start不会执行回调
success:fn()
fail:fn(errMsg)
**/
,start:function(success,fail){
var This=this,set=This.set;
var failCall=function(err){
This.sendAbortMsg=err;
fail&&fail(err);
};
if(!set.compatibleWebSocket){
if(!isBrowser){
failCall("非浏览器环境请提供compatibleWebSocket配置来返回一个兼容的WebSocket");
return;
};
};
if(This.state!=0){
failCall("ASR对象不可重复start");
return;
};
This.state=1;
var stopCancel=function(){
This.log("ASR start被stop中断",1);
This._send();//调用了再说,不管什么状态
};
This._token(function(){
if(This.state!=1){
stopCancel();
}else{
This.log("OK start",2);
This.started=1;
success&&success();
This._send();//调用了再说,不管什么状态
};
},function(err){
err="语音识别token接口出错"+err;
This.log(err,1);
if(This.state!=1){
stopCancel();
}else{
failCall(err);
This._send();//调用了再说,不管什么状态
};
});
}
/**rec.stop
success:fn(text,abortMsg) text为识别到的最终完整内容如果存在abortMsg代表识别中途被某种错误停止了text是停止前的内容识别到的完整内容一般早在asrProcess中会收到abort事件然后要停止录音
fail:fn(errMsg)
**/
,stop:function(success,fail){
success=success||NOOP;
fail=fail||NOOP;
var This=this;
var failCall=function(err){
err="语音识别stop出错"+err;
This.log(err,1);
fail(err);
};
if(This.state==2){
failCall("ASR对象不可重复stop");
return;
};
This.state=2;
This.stopWait=function(){
This.stopWait=null;
if(!This.started){
fail(This.sendAbortMsg||"未开始语音识别");
return;
};
var txt=This.getText();
if(!txt && This.sendAbortMsg){
fail(This.sendAbortMsg);//仅没有内容时,才走异常
}else{
success(txt, This.sendAbortMsg||"");//尽力返回已有内容
};
};
//等待数据发送完
This._send();
}
/**startstartstart
buffers:[[Int16...],...] pcm片段列表为二维数组第一维数组内存放1个或多个pcm数据比如可以是rec.buffersonProcess中的buffers截取的一段新二维数组
sampleRate:48000 buffers中pcm的采样率
buffersOffset:0 可选默认0从buffers第一维的这个位置开始识别方便rec的onProcess中使用
**/
,input:function(buffers,sampleRate ,buffersOffset){
var This=this;
if(This.state==2){//已停止,停止输入数据
This._send();
return;
};
var msg="input输入的采样率低于"+This.sampleRate;
if(sampleRate<This.sampleRate){
CLog(msg+",数据已丢弃",3);
if(!This.pcmTotal){
This.sendAbortMsg=msg;
};
This._send();
return;
};
if(This.sendAbortMsg==msg){
This.sendAbortMsg="";
};
if(buffersOffset){
var newBuffers=[];
for(var idx=buffersOffset;idx<buffers.length;idx++){
newBuffers.push(buffers[idx]);
};
buffers=newBuffers;
};
var pcm=Recorder.SampleData(buffers,sampleRate,This.sampleRate).data;
This.pcmTotal+=pcm.length;
This.pcmBuffers.push(pcm);
This._send();
}
,_send:function(){
var This=this,set=This.set;
if(This.sendWait){
//阻塞中
return;
};
var tryStopEnd=function(){
This.stopWait&&This.stopWait();
};
if(This.state==2 && (!This.started || !This.stopWait)){
//已经stop了并且未ok开始 或者 未在等待结果
tryStopEnd();
return;
};
if(This.sendAbort){
//已异常中断了
tryStopEnd();
return;
};
//异常提前终止
var abort=function(err){
if(!This.sendAbort){
This.sendAbort=1;
This.sendAbortMsg=err||"-";
processCall(0,1);//abort后只调用最后一次
};
This._send();
};
var processCall=function(addSize,abortLast){
if(!abortLast && This.sendAbort){
return false;
};
addSize=addSize||0;
if(!set.asrProcess){
//默认超过1分钟自动停止
return This.sendTotal+addSize<=size60s;
};
//实时回调
var val=set.asrProcess(This.getText()
,This.sendDuration(addSize)
,This.sendAbort?This.sendAbortMsg:"");
if(!This._prsw && typeof(val)!="boolean"){
CLog("asrProcess返回值必须是boolean类型true才能继续识别否则立即超时",1);
};
This._prsw=1;
return val;
};
var size5s=This.sampleRate*5;
var size60s=This.sampleRate*60;
//建立ws连接
var ws=This.wsCur;
if(!ws){
if(This.started){//已start才创建ws
var resTxt={};
This.resTxts.push(resTxt);
ws=This.wsCur=This._wsNew(
This.tokenData
,"ws:"+This.resTxts.length
,resTxt
,function(){
processCall();
}
,function(){
This._send();
}
,function(err){
//异常中断
if(ws==This.wsCur){
abort(err);
};
}
);
};
return;
};
//正在新建新1分钟连接等着
if(This.wsLock){
return;
};
//已有ok的连接直接陆续将所有缓冲分段发送完
if(ws._s!=2 || ws.isStop){
//正在关闭或者其他状态不管,等着
return;
};
//没有数据了
if(This.pcmSend>=This.pcmTotal){
if(This.state==1){
//缓冲数据已发送完,等待新数据
return;
};
//已stop结束识别得到最终结果
ws.stopWs(function(){
tryStopEnd();
},function(err){
abort(err);
});
return;
};
//准备本次发送数据块
var minSize=This.sampleRate/1000*50;//最小发送量50ms ≈1.6k
var maxSize=This.sampleRate;//最大发送量1000ms ≈32k
//速度控制1取决于网速
if((ws.bufferedAmount||0)/2>maxSize*3){
//传输太慢,阻塞一会再发送
This.sendWait=setTimeout(function(){
This.sendWait=0;
This._send();
},100);
return;
};
//速度控制2取决于已发送时长单个文件才会被控制速率
if(This.sendSpeed){
var spMaxMs=(Date.now()-ws.okTime)*This.sendSpeed;
var nextMs=(This.sendCurSize+maxSize/3)/This.sampleRate*1000;
var delay=Math.floor((nextMs-spMaxMs)/This.sendSpeed);
if(delay>0){
//传输太快,怕底层识别不过来,降低发送速度
CLog("[ASR]延迟"+delay+"ms发送");
This.sendWait=setTimeout(function(){
This.sendWait=0;
This._send();
},delay);
return;
};
};
var needSend=1;
var copyBuffers=function(offset,buffers,dist){
var size=dist.length;
for(var i=0,idx=0;idx<size&&i<buffers.length;){
var pcm=buffers[i];
if(pcm.length-offset<=size-idx){
dist.set(offset==0?pcm:pcm.subarray(offset),idx);
idx+=pcm.length-offset;
offset=0;
buffers.splice(i,1);
}else{
dist.set(pcm.subarray(offset,offset+(size-idx)),idx);
offset+=size-idx;
break;
};
};
return offset;
};
if(This.joinIsOpen){
//发送新1分钟的开头重叠5秒数据
if(This.joinOffset==-1){
//精准定位5秒
This.joinSend=0;
This.joinOffset=0;
This.log("发送上1分钟结尾5秒数据...");
var total=0;
for(var i=This.joinBuffers.length-1;i>=0;i--){
total+=This.joinBuffers[i].length;
if(total>=size5s){
This.joinBuffers.splice(0, i);
This.joinSize=total;
This.joinOffset=total-size5s;
break;
};
};
};
var buffersSize=This.joinSize-This.joinOffset;//缓冲余量
var size=Math.min(maxSize,buffersSize);
if(size<=0){
//重叠5秒数据发送完毕
This.log("发送新1分钟数据(重叠"+Math.round(This.joinSend/This.sampleRate*1000)+"ms)...");
This.joinBuffers=[];
This.joinSize=0;
This.joinOffset=-1;
This.joinIsOpen=0;
This._send();
return;
};
//创建块数据消耗掉buffers
var chunk=new Int16Array(size);
This.joinSend+=size;
This.joinSendTotal+=size;
This.joinOffset=copyBuffers(This.joinOffset,This.joinBuffers,chunk);
This.joinSize=0;
for(var i=0;i<This.joinBuffers.length;i++){
This.joinSize+=This.joinBuffers[i].length;
};
}else{
var buffersSize=This.pcmTotal-This.pcmSend;//缓冲余量
var buffersDur=Math.round(buffersSize/This.sampleRate*1000);
var curHasSize=size60s-This.sendCurSize;//当前连接剩余能发送的量
var sizeNext=Math.min(maxSize,buffersSize);//不管连接剩余数时本应当发送的数量
var size=Math.min(sizeNext,curHasSize);
if(This.state==1 && size<Math.min(minSize,curHasSize)){
//不够发送一次的,等待新数据
return;
};
var needNew=0;
if(curHasSize<=0){
//当前连接一分钟已消耗完
if(This.state==2 && buffersSize<This.sampleRate*1.2){
//剩余的量太少并且已stop没必要再新建连接直接丢弃
size=buffersSize;
This.log("丢弃结尾"+buffersDur+"ms数据","#999");
needSend=0;
}else{
//开始新1分钟的连接等到实时回调后再看要不要新建
needNew=true;
};
};
//回调看看是否要超时终止掉
if(needSend && !processCall(sizeNext)){//用本应当的发送量来计算
//超时,终止识别
var durS=Math.round(This.asrDuration()/1000);
This.log("已主动超时,共识别"+durS+"秒,丢弃缓冲"+buffersDur+"ms正在终止...");
This.wsLock=1;//阻塞住后续调用
ws.stopWs(function(){
abort("已主动超时,共识别"+durS+"秒,终止识别");
},function(err){
abort(err);
});
return;
};
//开始新1分钟的连接
if(needNew){
CLog("[ASR]新1分钟接续当前缓冲"+buffersDur+"ms...");
This.wsLock=1;//阻塞住后续调用
ws.stopWs(function(){
This._token(function(){
This.log("新1分钟接续OK当前缓冲"+buffersDur+"ms",2);
This.wsLock=0;
This.wsCur=0;//重置当前连接
This.sendCurSize=0;
This.joinIsOpen=1;//新1分钟先发重叠的5秒数据
This.joinOffset=-1;
This._send();
},function(err){
abort("语音识别新1分钟token接口出错"+err);
});
},function(err){
abort(err);
});
return;
};
//创建块数据消耗掉buffers
var chunk=new Int16Array(size);
This.pcmOffset=copyBuffers(This.pcmOffset,This.pcmBuffers,chunk);
This.pcmSend+=size;
//写入到下一分钟的头5秒重叠区域中不管写了多少写就完了
This.joinBuffers.push(chunk);
This.joinSize+=size;
};
This.sendCurSize+=chunk.length;
This.sendTotal+=chunk.length;
if(needSend){
try{
ws.send(chunk.buffer);
}catch(e){CLog("ws.send",1,e);};
};
//不要停
This.sendWait=setTimeout(function(){
This.sendWait=0;
This._send();
});//仅退出调用堆栈
}
/**返回实时结果文本如果已stop返回的就是最终文本**/
,getText:function(){
var arr=this.resTxts;
var txt="";
for(var i=0;i<arr.length;i++){
var obj=arr[i];
if(obj.fullTxt){
txt=obj.fullTxt;
}else{
var tmp=obj.tempTxt||"";
if(obj.okTxt){
tmp=obj.okTxt;
};
//5秒重叠进行模糊拼接
if(!txt){
txt=tmp;
}else{
var left=txt.substr(-20);//240字/分
var finds=[];
for(var x=0,max=Math.min(17,tmp.length-3);x<=max;x++){
for(var i0=0;i0<17;i0++){
if(left[i0]==tmp[x]){
var n=1;
for(;n<17;n++){
if(left[i0+n]!=tmp[x+n]){
break;
};
};
if(n>=3){//3字相同即匹配
finds.push({x:x,i0:i0,n:n});
};
};
};
};
finds.sort(function(a,b){
var v=b.n-a.n;
return v!=0?v:b.i0-a.i0;//越长越好,越靠后越好
});
var f0=finds[0];
if(f0){
txt=txt.substr(0,txt.length-left.length+f0.i0);
txt+=tmp.substr(f0.x);
}else{
txt+=tmp;
};
};
//存起来
if(obj.okTxt!=null && tmp==obj.okTxt){
obj.fullTxt=txt;
};
};
};
return txt;
}
//创建新的wss连接
,_wsNew:function(sData,id,resTxt,process,connOk,connFail){
var uuid=function(){
var s=[];
for(var i=0,r;i<32;i++){
r=Math.floor(Math.random()*16);
s.push(String.fromCharCode(r<10?r+48:r-10+97));
};
return s.join("");
};
var This=this,set=This.set;
CLog("[ASR "+id+"]正在连接...");
var url="wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1?token="+sData.token;
if(set.compatibleWebSocket){
var ws=set.compatibleWebSocket(url);
}else{
var ws=new WebSocket(url);
}
//ws._s=0 0连接中 1opening 2openOK 3stoping 4closeing -1closed
//ws.isStop=0 1已停止识别
ws.onclose=function(){
if(ws._s==-1)return;
var isFail=ws._s!=4;
ws._s=-1;
This.log("["+id+"]close");
isFail&&connFail(ws._err||"连接"+id+"已关闭");
};
ws.onerror=function(e){
if(ws._s==-1)return;
var msg="网络连接错误";
ws._err||(ws._err=msg);
This.log("["+id+"]"+msg,1);
ws.onclose();
};
ws.onopen=function(){
if(ws._s==-1)return;
ws._s=1;
CLog("[ASR "+id+"]open");
ws._task=uuid();
ws.send(JSON.stringify({
header:{
message_id:uuid()
,task_id:ws._task
,appkey:sData.appkey
,namespace:"SpeechRecognizer"
,name:"StartRecognition"
}
,payload:{
format:"pcm"
,sample_rate:This.sampleRate
,enable_intermediate_result:true //返回中间识别结果
,enable_punctuation_prediction:true //添加标点
,enable_inverse_text_normalization:true //后处理中将数值处理
}
,context:{ }
}));
};
ws.onmessage=function(e){
var data=e.data;
var logMsg=true;
if(typeof(data)=="string" && data[0]=="{"){
data=JSON.parse(data);
var header=data.header||{};
var payload=data.payload||{};
var name=header.name||"";
var status=header.status||0;
var isFail=name=="TaskFailed";
var errMsg="";
//init
if(ws._s==1 && (name=="RecognitionStarted" || isFail)){
if(isFail){
errMsg="连接"+id+"失败["+status+"]"+header.status_text;
}else{
ws._s=2;
This.log("["+id+"]连接OK");
ws.okTime=Date.now();
connOk();
};
};
//中间结果
if(ws._s==2 && (name=="RecognitionResultChanged" || isFail)){
if(isFail){
errMsg="识别出现错误["+status+"]"+header.status_text;
}else{
logMsg=!ws._clmsg;
ws._clmsg=1;
resTxt.tempTxt=payload.result||"";
process();
};
};
//stop
if(ws._s==3 && (name=="RecognitionCompleted" || isFail)){
var txt="";
if(isFail){
errMsg="停止识别出现错误["+status+"]"+header.status_text;
}else{
txt=payload.result||"";
This.log("["+id+"]最终识别结果:"+txt);
};
ws.stopCall&&ws.stopCall(txt,errMsg);
};
if(errMsg){
This.log("["+id+"]"+errMsg,1);
ws._err||(ws._err=errMsg);
};
};
if(logMsg){
CLog("[ASR "+id+"]msg",data);
};
};
ws.stopWs=function(True,False){
if(ws._s!=2){
False(id+"状态不正确["+ws._s+"]");
return;
};
ws._s=3;
ws.isStop=1;
ws.stopCall=function(txt,err){
clearTimeout(ws.stopInt);
ws.stopCall=0;
ws._s=4;
ws.close();
resTxt.okTxt=txt;
process();
if(err){
False(err);
}else{
True();
};
};
ws.stopInt=setTimeout(function(){
ws.stopCall&&ws.stopCall("","停止识别返回结果超时");
},10000);
CLog("[ASR "+id+"]send stop");
ws.send(JSON.stringify({
header:{
message_id:uuid()
,task_id:ws._task
,appkey:sData.appkey
,namespace:"SpeechRecognizer"
,name:"StopRecognition"
}
}));
};
if(ws.connect)ws.connect(); //兼容时会有这个方法
return ws;
}
//获得开始识别的token信息
,_token:function(True,False){
var This=this,set=This.set;
if(!set.tokenApi){
False("未配置tokenApi");return;
};
(set.apiRequest||DefaultPost)(set.tokenApi,set.apiArgs||{},function(data){
if(!data || !data.appkey || !data.token){
False("apiRequest回调的数据格式不正确");return;
};
This.tokenData=data;
True();
},False);
}
};
//手撸一个ajax
function DefaultPost(url,args,success,fail){
var xhr=new XMLHttpRequest();
xhr.timeout=20000;
xhr.open("POST",url);
xhr.onreadystatechange=function(){
if(xhr.readyState==4){
if(xhr.status==200){
try{
var o=JSON.parse(xhr.responseText);
}catch(e){};
if(o.c!==0 || !o.v){
fail(o.m||"接口返回非预定义json数据");
return;
};
success(o.v);
}else{
fail("请求失败["+xhr.status+"]");
}
}
};
var arr=[];
for(var k in args){
arr.push(k+"="+encodeURIComponent(args[k]));
};
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(arr.join("&"));
};
function NOOP(){};
Recorder[ASR_Aliyun_ShortTxt]=ASR_Aliyun_Short;
}));

View File

@ -0,0 +1,887 @@
/*
录音 Recorder扩展实时播放录音片段文件把片段文件转换成MediaStream流
https://github.com/xiangyuecn/Recorder
BufferStreamPlayer可以通过input方法一次性输入整个音频文件或者实时输入音频片段文件然后播放出来输入支持格式pcmwavmp3等浏览器支持的音频格式非pcm格式会自动解码成pcm播放音质效果比pcmwav格式差点输入前输入后都可进行处理要播放的音频比如混音变速变调输入的音频会写入到内部的MediaStream流中完成将连续的音频片段文件转换成流
BufferStreamPlayer可以用于
1. Recorder onProcess等实时处理中将实时处理好的音频片段转直接换成MediaStream此流可以作为WebRTC的local流发送到对方或播放出来
2. 接收到的音频片段文件的实时播放比如WebSocket接收到的录音片段文件播放WebRTC remote流Recorder支持对这种流进行实时处理实时处理后的播放
3. 单个音频文件的实时播放处理比如播放一段音频并同时进行可视化绘制其实自己解码+播放绘制比直接调用这个更有趣但这个省事配套功能多点
在线测试例子
https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.decode_buffer_stream_player
调用示例
var stream=Recorder.BufferStreamPlayer(set)
//创建好后第一件事就是start打开流打开后就会开始播放input输入的音频set具体配置看下面源码注意start需要在用户操作(触摸、点击等)时进行调用原因参考runningContext配置
stream.start(()=>{
stream.currentTime;//当前已播放的时长单位ms数值变化时会有onUpdateTime事件
stream.duration;//已输入的全部数据总时长单位ms数值变化时会有onUpdateTime事件实时模式下意义不大会比实际播放的长因为实时播放时卡了就会丢弃部分数据不播放
stream.isStop;//是否已停止调用了stop方法时会设为true
stream.isPause;//是否已暂停调用了pause方法时会设为true
stream.isPlayEnd;//已输入的数据是否播放到了结尾没有可播放的数据了input后又会变成false可代表正在缓冲中或播放结束状态变更时会有onPlayEnd事件
//如果不要默认的播放可以设置set.play为false这种情况下只拿到MediaStream来用
stream.getMediaStream() //通过getMediaStream方法得到MediaStream流此流可以作为WebRTC的local流发送到对方或者直接拿来赋值给audio.srcObject来播放和赋值audio.src作用一致未start时调用此方法将会抛异常
stream.getAudioSrc() //【已过时】超低版本浏览器中得到MediaStream流的字符串播放地址可赋值给audio标签的src直接播放音频未start时调用此方法将会抛异常新版本浏览器已停止支持将MediaStream转换成url字符串调用本方法新浏览器会抛异常因此在不需要兼容不支持srcObject的超低版本浏览器时请直接使用getMediaStream然后赋值给auido.srcObject来播放
},(errMsg)=>{
//start失败无法播放
});
//随时都能调用input会等到start成功后播放出来不停的调用input就能持续的播放出声音了需要暂停播放就不要调用input就行了
stream.input(anyData); //anyData数据格式 和更多说明请阅读下面的input方法源码注释
stream.clearInput(keepDuration); //清除已输入但还未播放的数据一般用于非实时模式打断老的播放返回清除的音频时长默认会从总时长duration中减去此时长keepDuration=true时不减去
//暂停播放暂停后实时模式下会丢弃所有input输入的数据resume时只播放新input的数据非实时模式下所有input输入的数据会保留到resume时继续播放
stream.pause();
//恢复播放实时模式下只会从最新input的数据开始播放非实时模式下会从暂停的位置继续播放
stream.resume();
//不要播放了就调用stop停止播放关闭所有资源
stream.stop();
注意已知Firefox的AudioBuffer没法动态修改数据所以对于带有这种特性的浏览器将采用先缓冲后再播放类似assets/runtime-codes/fragment.playbuffer.js音质会相对差一点其他浏览器测试AndroidIOSChrome无此问题start方法中有一大段代码给浏览器做了特性检测并进行兼容处理
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var BufferStreamPlayer=function(set){
return new fn(set);
};
var BufferStreamPlayerTxt="BufferStreamPlayer";
var fn=function(set){
var This=this;
var o={
play:true //要播放声音设为false不播放只提供MediaStream
,realtime:true /*默认为true实时模式设为false为非实时模式
实时模式设为 true {maxDelay:300,discardAll:false}配置对象
如果有新的input输入数据但之前输入的数据还未播放完的时长不超过maxDelay时缓冲播放延迟默认限制在300ms内如果积压的数据量过大则积压的数据将会被直接丢弃少量积压会和新数据一起加速播放最终达到尽快播放新输入的数据的目的这在网络不流畅卡顿时会发挥很大作用可有效降低播放延迟出现加速播放时声音听起来会比较怪异可配置discardAll=true来关闭此特性少量积压的数据也直接丢弃不会加速播放如果你的音频数据块超过200ms需要调大maxDelay取值100-800ms
非实时模式设为 false
连续完整的播放完所有input输入的数据之前输入的还未播放完又有新input输入会加入队列排队播放比如用于一次性同时输入几段音频完整播放
*/
//,onInputError:fn(errMsg, inputIndex) //当input输入出错时回调参数为input第几次调用和错误消息
//,onUpdateTime:fn() //已播放时长、总时长更新回调stop、pause、resume后一定会回调this.currentTime为已播放时长this.duration为已输入的全部数据总时长实时模式下意义不大会比实际播放的长单位都是ms
//,onPlayEnd:fn() //没有可播放的数据时回调stop后一定会回调已输入的数据已全部播放完了可代表正在缓冲中或播放结束之后如果继续input输入了新数据播放完后会再次回调因此会多次回调非实时模式一次性输入了数据时此回调相当于播放完成可以stop掉重新创建对象来input数据可达到循环播放效果
//,decode:false //input输入的数据在调用transform之前是否要进行一次音频解码成pcm [Int16,...]
//mp3、wav等都可以设为true、或设为{fadeInOut:true}配置对象会自动解码成pcm默认会开启fadeInOut对解码的pcm首尾进行淡入淡出处理减少爆音wav等解码后和原始pcm一致的音频可以把fadeInOut设为false
//transform:fn(inputData,sampleRate,True,False)
//将input输入的data如果开启了decode将是解码后的pcm转换处理成要播放的pcm数据如果没有解码也没有提供本方法input的data必须是[Int16,...]并且设置set.sampleRate
//inputData:any input方法输入的任意格式数据只要这个转换函数支持处理如果开启了decode此数据为input输入的数据解码后的pcm [Int16,...]
//sampleRate:123 如果设置了decode为解码后的采样率否则为set.sampleRate || null
//True(pcm,sampleRate) 回调处理好的pcm数据([Int16,...])和pcm的采样率
//False(errMsg) 处理失败回调
//sampleRate:16000 //可选input输入的数据默认的采样率当没有设置解码也没有提供transform时应当明确设置采样率
//runningContext:AudioContext //可选提供一个state为running状态的AudioContext对象(ctx)默认会在start时自动创建一个新的ctx这个配置的作用请参阅Recorder的runningContext配置
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
if(!set.onInputError){
set.onInputError=function(err,n){ CLog(err,1); };
}
};
fn.prototype=BufferStreamPlayer.prototype={
/**【已过时】获取MediaStream的audio播放地址新版浏览器、未start将会抛异常**/
getAudioSrc:function(){
CLog($T("0XYC::getAudioSrc方法已过时请直接使用getMediaStream然后赋值给audio.srcObject仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"),3);
if(!this._src){
//新版chrome调用createObjectURL会直接抛异常了 https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#using_object_urls_for_media_streams
this._src=(window.URL||webkitURL).createObjectURL(this.getMediaStream());
}
return this._src;
}
/**获取MediaStream流对象未start将会抛异常**/
,getMediaStream:function(){
if(!this._dest){
throw new Error(NoStartMsg());
}
return this._dest.stream;
}
/**inputstart()runningContext
* True() 打开成功回调
* False(errMsg) 打开失败回调**/
,start:function(True,False){
var falseCall=function(msg,noClear){
var next=!checkStop();
if(!noClear)This._clear();
CLog(msg,1);
next&&False&&False(msg);
};
var checkStop=function(){
if(This.isStop){
CLog($T("6DDt::start被stop终止"),3);
return true;
};
};
var This=this,set=This.set,__abTest=This.__abTest;
if(This._Tc!=null){
falseCall($T("I4h4::{1}多次start",0,BufferStreamPlayerTxt),1);
return;
}
if(!isBrowser){
falseCall($T.G("NonBrowser-1",[BufferStreamPlayerTxt]));
return;
}
This._Tc=0;//currentTime 对应的采样数
This._Td=0;//duration 对应的采样数
This.currentTime=0;//当前已播放的时长单位ms
This.duration=0;//已输入的全部数据总时长单位ms实时模式下意义不大会比实际播放的长因为实时播放时卡了就会丢弃部分数据不播放
This.isStop=0;//是否已停止
This.isPause=0;//是否已暂停
This.isPlayEnd=0;//已输入的数据是否播放到了结尾没有可播放的数据了input后又会变成false可代表正在缓冲中或播放结束
This.inputN=0;//第n次调用input
This.inputQueueIdx=0;//input调用队列当前已处理到的位置
This.inputQueue=[];//input调用队列用于纠正执行顺序
This.bufferSampleRate=0;//audioBuffer的采样率首次input后就会固定下来
This.audioBuffer=0;
This.pcmBuffer=[[],[]];//未推入audioBuffer的pcm数据缓冲
var fail=function(msg){
falseCall($T("P6Gs::浏览器不支持打开{1}",0,BufferStreamPlayerTxt)+(msg?": "+msg:""));
};
var ctx=set.runningContext || Recorder.GetContext(true); This._ctx=ctx;
var sVal=ctx.state,spEnd=Recorder.CtxSpEnd(sVal);
!__abTest&&CLog("start... ctx.state="+sVal+(
spEnd?$T("JwDm::注意ctx不是running状态start需要在用户操作(触摸、点击等)时进行调用否则会尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"):""
));
var support=1;
if(!ctx || !ctx.createMediaStreamDestination){
support=0;
}else{
var source=ctx.createBufferSource();
if(!source.start || source.onended===undefined){
support=0;//createBufferSource版本太低难兼容
}
};
if(!support){
fail("");
return;
};
var end=function(){
if(checkStop())return;
//创建MediaStream
var dest=ctx.createMediaStreamDestination();
dest.channelCount=1;
This._dest=dest;
!__abTest&&CLog("start ok");
True&&True();
This._inputProcess();//处理未完成start前的input调用
This._updateTime();//更新时间
//定时在没有input输入时将未写入buffer的数据写进去
if(!badAB){
This._writeInt=setInterval(function(){
This._writeBuffer();
},100);
}else{
CLog($T("qx6X::此浏览器的AudioBuffer实现不支持动态特性采用兼容模式"),3);
This._writeInt=setInterval(function(){
This._writeBad();
},10);//定时调用进行数据写入播放
}
};
var abTest=function(){
//浏览器实现检测已知Firefox的AudioBuffer没法在_writeBuffer中动态修改数据检测方法直接新开一个输入一段测试数据看看能不能拿到流中的数据
var testStream=BufferStreamPlayer({ play:false,sampleRate:8000,runningContext:ctx });
testStream.__abTest=1; var testRec;
testStream.start(function(){
testRec=Recorder({
type:"unknown"
,sourceStream:testStream.getMediaStream()
,runningContext:ctx
,onProcess:function(buffers){
var bf=buffers[buffers.length-1],all0=1;
for(var i=0;i<bf.length;i++){
if(bf[i]!=0){ all0=0; break; }
}
if(all0 && buffers.length<5){
return;//再等等看最长约等500ms
}
testRec.close();
testStream.stop();
if(testInt){ clearTimeout(testInt); testInt=0;
//全部是0就是浏览器不行要缓冲一次性播放进行兼容
badAB=all0;
BufferStreamPlayer.BadAudioBuffer=badAB;
end();
}
}
});
testRec.open(function(){
testRec.start();
},function(msg){
testStream.stop(); fail(msg);
});
},fail);
//超时没有回调
var testInt=setTimeout(function(){
testInt=0; testStream.stop(); testRec&&testRec.close();
fail($T("cdOx::环境检测超时"));
},1500);
//随机生成1秒的数据rec有一次回调即可
var data=new Int16Array(8000);
for(var i=0;i<8000;i++){
data[i]=~~(Math.random()*0x7fff*2-0x7fff);
}
testStream.input(data);
};
var badAB=BufferStreamPlayer.BadAudioBuffer;
var ctxNext=function(){
if(__abTest || badAB!=null){
setTimeout(end); //应当setTimeout一下强转成异步统一调用代码时的行为
}else{
abTest();
};
};
var tag="AudioContext resume: ";
Recorder.ResumeCtx(ctx,function(runC){
runC&&CLog(tag+"wait...");
return !This.isStop;
},function(runC){
runC&&CLog(tag+ctx.state);
ctxNext();
},function(err){ //比较少见,可能没有影响
CLog(tag+ctx.state+" "+$T("S2Bu::可能无法播放:{1}",0,err),1);
ctxNext();
});
}
,_clear:function(){
var This=this;
This.isStop=1;
clearInterval(This._writeInt);
This.inputQueue=0;
if(This._src){
(window.URL||webkitURL).revokeObjectURL(This._src);
This._src=0;
}
if(This._dest){
Recorder.StopS_(This._dest.stream);
This._dest=0;
}
if(!This.set.runningContext && This._ctx){
Recorder.CloseNewCtx(This._ctx);
}
This._ctx=0;
var source=This.bufferSource;
if(source){
source.disconnect();
source.stop();
}
This.bufferSource=0;
This.audioBuffer=0;
}
/**停止播放,关闭所有资源**/
,stop:function(){
var This=this;
This._clear();
!This.__abTest&&CLog("stop");
This._playEnd(1);
}
/**暂停播放暂停后实时模式下会丢弃所有input输入的数据resume时只播放新input的数据非实时模式下所有input输入的数据会保留到resume时继续播放**/
,pause:function(){
CLog("pause");
this.isPause=1;
this._updateTime(1);
}
/**恢复播放实时模式下只会从最新input的数据开始播放非实时模式下会从暂停的位置继续播放**/
,resume:function(){
var This=this,tag="resume",tag3=tag+"(wait ctx)";
CLog(tag);
This.isPause=0;
This._updateTime(1);
var ctx=This._ctx;
if(ctx){ //AudioContext如果被暂停尽量恢复
Recorder.ResumeCtx(ctx,function(runC){
runC&&CLog(tag3+"...");
return !This.isStop && !This.isPause;
},function(runC){
runC&&CLog(tag3+ctx.state);
},function(err){
CLog(tag3+ctx.state+"[err]"+err,1);
});
};
}
//当前输入的数据播放到结尾时触发回调stop时永远会触发回调
,_playEnd:function(stop){
var This=this,startTime=This._PNs,call=This.set.onPlayEnd;
if(stop || !This.isPause){//暂停播到结尾不算
if(stop || !This.isPlayEnd){
if(stop || (startTime && Date.now()-startTime>500)){//已停止或者延迟确认成功
This._PNs=0;
This.isPlayEnd=1;
call&&call();
This._updateTime(1);
}else if(!startTime){//刚检测到的没有数据了,开始延迟确认
This._PNs=Date.now();
};
};
};
}
//有数据播放时,取消已到结尾状态
,_playLive:function(){
var This=this;
This.isPlayEnd=0;
This._PNs=0;
}
//时间更新时触发回调,没有更新时不会触发回调
,_updateTime:function(must){
var This=this,sampleRate=This.bufferSampleRate||9e9,call=This.set.onUpdateTime;
This.currentTime=Math.round(This._Tc/sampleRate*1000);
This.duration=Math.round(This._Td/sampleRate*1000);
var s=""+This.currentTime+This.duration;
if(must || This._UTs!=s){
This._UTs=s;
call&&call();
}
}
/**startstart
anyData: any 具体类型取决于
set.decode为false时:
未提供set.transform数据必须是pcm[Int16,...]此时的set必须提供sampleRate
提供了set.transform数据为transform方法支持的任意格式
set.decode为true时:
数据必须是ArrayBuffer会自动解码成pcm[Int16,...]注意输入的每一片数据都应该是完整的一个音频片段文件否则可能会解码失败注意ArrayBuffer对象是Transferable object参与解码后此对象将不可用因为内存数据已被转移到了解码线程可通过 stream.input(arrayBuffer.slice(0)) 形式复制一份再解码就没有这个问题了
关于anyData的二进制长度
如果是提供的pcmwav格式数据数据长度对播放无太大影响很短的数据也能很好的连续播放
如果是提供的mp3这种必须解码才能获得pcm的数据数据应当尽量长点测试发现片段有300ms以上解码后能很好的连续播放低于100ms解码后可能会有明显的杂音更低的可能会解码失败当片段确实太小时可以将本来会多次input调用的数据缓冲起来等数据量达到了300ms再来调用一次input能比较显著的改善播放音质
**/
,input:function(anyData){
var This=this,set=This.set;
var inputN=++This.inputN;
if(!This.inputQueue){
throw new Error(NoStartMsg());
}
var decSet=set.decode;
if(decSet){
//先解码
DecodeAudio(anyData, function(data){
if(!This.inputQueue)return;//stop了
if(decSet.fadeInOut==null || decSet.fadeInOut){
FadeInOut(data.data, data.sampleRate);//解码后的数据进行一下淡入淡出处理,减少爆音
}
This._input2(inputN, data.data, data.sampleRate);
},function(err){
This._inputErr(err, inputN);
});
}else{
This._input2(inputN, anyData, set.sampleRate);
}
}
//transform处理
,_input2:function(inputN, anyData, sampleRate){
var This=this,set=This.set;
if(set.transform){
set.transform(anyData, sampleRate, function(pcm, sampleRate2){
if(!This.inputQueue)return;//stop了
sampleRate=sampleRate2||sampleRate;
This._input3(inputN, pcm, sampleRate);
},function(err){
This._inputErr(err, inputN);
});
}else{
This._input3(inputN, anyData, sampleRate);
}
}
//转换好的pcm加入input队列纠正调用顺序未start时等待
,_input3:function(inputN, pcm, sampleRate){
var This=this;
if(!pcm || !pcm.subarray){
This._inputErr($T("ZfGG::input调用失败非pcm[Int16,...]输入时必须解码或者使用transform转换"), inputN);
return;
}
if(!sampleRate){
This._inputErr($T("N4ke::input调用失败未提供sampleRate"), inputN);
return;
}
if(This.bufferSampleRate && This.bufferSampleRate!=sampleRate){
This._inputErr($T("IHZd::input调用失败data的sampleRate={1}和之前的={2}不同",0,sampleRate,This.bufferSampleRate), inputN);
return;
}
if(!This.bufferSampleRate){
This.bufferSampleRate=sampleRate;//首次处理后,固定下来,后续的每次输入都是相同的
}
//加入队列纠正input执行顺序解码、transform均有可能会导致顺序不一致
if(inputN>This.inputQueueIdx){ //clearInput移动了队列位置的丢弃
This.inputQueue[inputN]=pcm;
}
if(This._dest){//已start可以开始处理队列
This._inputProcess();
}
}
,_inputErr:function(errMsg, inputN){
if(!this.inputQueue) return;//stop了
this.inputQueue[inputN]=1;//出错了,队列里面也要占个位
this.set.onInputError(errMsg, inputN);
}
//处理input队列
,_inputProcess:function(){
var This=this;
if(!This.bufferSampleRate){
return;
}
var queue=This.inputQueue;
for(var i=This.inputQueueIdx+1;i<queue.length;i++){ //inputN是从1开始所以+1
var pcm=queue[i];
if(pcm==1){
This.inputQueueIdx=i;//跳过出错的input
continue;
}
if(!pcm){
return;//之前的input还未进入本方法退出等待
}
This.inputQueueIdx=i;
queue[i]=null;
//推入缓冲,最多两个元素 [堆积的,新的]
var pcms=This.pcmBuffer;
var pcm0=pcms[0],pcm1=pcms[1];
if(pcm0.length){
if(pcm1.length){
var tmp=new Int16Array(pcm0.length+pcm1.length);
tmp.set(pcm0);
tmp.set(pcm1,pcm0.length);
pcms[0]=tmp;
}
}else{
pcms[0]=pcm1;
}
pcms[1]=pcm;
This._Td+=pcm.length;//更新已输入总时长
This._updateTime();
This._playLive();//有播放数据了
}
if(!BufferStreamPlayer.BadAudioBuffer){
if(!This.audioBuffer){
This._createBuffer(true);
}else{
This._writeBuffer();
}
}else{
This._writeBad();
}
}
/**清除已输入但还未播放的数据一般用于非实时模式打断老的播放返回清除的音频时长默认会从总时长duration中减去此时长keepDuration时不减去*/
,clearInput:function(keepDuration){
var This=this, sampleRate=This.bufferSampleRate, size=0;
if(This.inputQueue){//未stop
This.inputQueueIdx=This.inputN;//队列位置移到结尾
var pcms=This.pcmBuffer;
size=pcms[0].length+pcms[1].length;
This._subClear();
if(!keepDuration) This._Td-=size;//减掉已输入总时长
This._updateTime(1);
}
var dur = size? Math.round(size/sampleRate*1000) : 0;
CLog("clearInput "+dur+"ms "+size);
return dur;
}
/****************正常的播放处理****************/
//创建播放buffer
,_createBuffer:function(init){
var This=this,set=This.set;
if(!init && !This.audioBuffer){
return;
}
var ctx=This._ctx;
var sampleRate=This.bufferSampleRate;
var bufferSize=sampleRate*(set.bufferSecond||60);//建一个可以持续播放60秒的buffer循环写入数据播放大点好简单省事
var buffer=ctx.createBuffer(1, bufferSize,sampleRate);
var source=ctx.createBufferSource();
source.channelCount=1;
source.buffer=buffer;
source.connect(This._dest);
if(set.play){//播放出声音
source.connect(ctx.destination);
}
source.onended=function(){
source.disconnect();
source.stop();
This._createBuffer();//重新创建buffer
};
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
This.bufferSource=source;
This.audioBuffer=buffer;
This.audioBufferIdx=0;
This._createBufferTime=Date.now();
This._writeBuffer();
}
,_writeBuffer:function(){
var This=this,set=This.set;
var buffer=This.audioBuffer;
var sampleRate=This.bufferSampleRate;
var oldAudioBufferIdx=This.audioBufferIdx;
if(!buffer){
return;
}
//计算已播放的量,可能已播放过头了,卡了没有数据
var playSize=Math.floor((Date.now()-This._createBufferTime)/1000*sampleRate);
if(This.audioBufferIdx+0.005*sampleRate<playSize){//5ms动态区间
This.audioBufferIdx=playSize;//将写入位置修正到当前播放位置
}
//写进去了,但还未被播放的量
var wnSize=Math.max(0, This.audioBufferIdx-playSize);
//这次最大能写入多少限制到800ms包括写入了还未播放的
var maxSize=buffer.length-This.audioBufferIdx;
maxSize=Math.min(maxSize, ~~(0.8*sampleRate)-wnSize);
if(maxSize<1){//写不下了,退出
return;
}
if(This._subPause()){//暂停了,不消费缓冲数据
return;
};
var pcms=This.pcmBuffer;
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
if(pcm0.length+pcm1Len==0){//无可用数据,退出
This._playEnd();//无可播放数据回调
return;
};
This._playLive();//有播放数据了
var pcmSize=0,speed=1;
var realMode=set.realtime;
while(realMode){
//************实时模式************
//尽量同步播放避免过大延迟但始终保持延迟150ms播放新数据这样每次添加进新数据都是接到还未播放到的最后面减少引入的杂音减少网络波动的影响
var delaySecond=0.15;
//计算当前堆积的量
var dSize=wnSize+pcm0.length;
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
//堆积的在300ms内按正常播放
if(dSize<dMax){
//至少要延迟播放新数据
var d150Size=Math.floor(delaySecond*sampleRate-dSize-pcm1Len);
if(oldAudioBufferIdx==0 && d150Size>0){
//开头加上少了的延迟
This.audioBufferIdx=Math.max(This.audioBufferIdx, d150Size);
}
realMode=false;//切换成顺序播放
break;
}
//堆积的太多,配置为全丢弃
if(realMode.discardAll){
if(dSize>dMax*1.333){//超过400ms取200ms正常播放300ms中位数
pcm0=This._cutPcm0(Math.round(dMax*0.666-wnSize-pcm1Len));
}
realMode=false;//切换成顺序播放
break;
}
//堆积的太多要加速播放了最多播放积压最后3秒的量超过的直接丢弃
pcm0=This._cutPcm0(3*sampleRate-wnSize-pcm1Len);
speed=1.6;//倍速,重采样
//计算要截取出来量
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
break;
}
if(!realMode){
//*******按顺序取数据播放*********
//计算要截取出来量
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
}
if(!pcmSize){
return;
}
//截取数据并写入到audioBuffer中
This.audioBufferIdx=This._subWrite(buffer,pcmSize,This.audioBufferIdx,speed);
}
/****************兼容播放处理,播放音质略微差点****************/
,_writeBad:function(){
var This=this,set=This.set;
var buffer=This.audioBuffer;
var sampleRate=This.bufferSampleRate;
var ctx=This._ctx;
//正在播放5ms不能结束就等待播放完定时器是10ms
if(buffer){
var ms=buffer.length/sampleRate*1000;
if(Date.now()-This._createBufferTime<ms-5){
return;
}
}
//这次最大能写入多少限制到800ms
var maxSize=~~(0.8*sampleRate);
var st=set.PlayBufferDisable?0:sampleRate/1000*300;//缓冲播放,不然间隔太短接续爆音明显
if(This._subPause()){//暂停了,不消费缓冲数据
return;
};
var pcms=This.pcmBuffer;
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
var allSize=pcm0.length+pcm1Len;
if(allSize==0 || allSize<st){//无可用数据 不够缓冲量,退出
This._playEnd();//无可播放数据回调,最后一丁点会始终等缓冲满导致卡住
return;
};
This._playLive();//有播放数据了
var pcmSize=0,speed=1;
var realMode=set.realtime;
while(realMode){
//************实时模式************
//计算当前堆积的量
var dSize=pcm0.length;
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
//堆积的在300ms内按正常播放
if(dSize<dMax){
realMode=false;//切换成顺序播放
break;
}
//堆积的太多,配置为全丢弃
if(realMode.discardAll){
if(dSize>dMax*1.333){//超过400ms取200ms正常播放300ms中位数
pcm0=This._cutPcm0(Math.round(dMax*0.666-pcm1Len));
}
realMode=false;//切换成顺序播放
break;
}
//堆积的太多要加速播放了最多播放积压最后3秒的量超过的直接丢弃
pcm0=This._cutPcm0(3*sampleRate-pcm1Len);
speed=1.6;//倍速,重采样
//计算要截取出来量
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
break;
}
if(!realMode){
//*******按顺序取数据播放*********
//计算要截取出来量
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
}
if(!pcmSize){
return;
}
//新建buffer一次性完整播放当前的数据
buffer=ctx.createBuffer(1,pcmSize,sampleRate);
//截取数据并写入到audioBuffer中
This._subWrite(buffer,pcmSize,0,speed);
//首尾进行1ms的淡入淡出 大幅减弱爆音
FadeInOut(buffer.getChannelData(0), sampleRate);
var source=ctx.createBufferSource();
source.channelCount=1;
source.buffer=buffer;
source.connect(This._dest);
if(set.play){//播放出声音
source.connect(ctx.destination);
}
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
This.bufferSource=source;
This.audioBuffer=buffer;
This._createBufferTime=Date.now();
}
,_cutPcm0:function(pcmNs){//保留堆积的数据到指定的时长数量
var pcms=this.pcmBuffer,pcm0=pcms[0];
if(pcmNs<0)pcmNs=0;
if(pcm0.length>pcmNs){//丢弃超过秒数的
var size=pcm0.length-pcmNs, dur=Math.round(size/this.bufferSampleRate*1000);
pcm0=pcm0.subarray(size);
pcms[0]=pcm0;
CLog($T("L8sC::延迟过大,已丢弃{1}ms {2}",0,dur,size),3);
}
return pcm0;
}
,_subPause:function(){//暂停了就不要消费掉缓冲数据了等待resume再来消费
var This=this;
if(!This.isPause){
return 0;
};
if(This.set.realtime){//实时模式丢弃所有未消费的数据resume时从最新input的数据开始播放
This._subClear();
};
return 1;
}
,_subClear:function(){ //清除缓冲数据
this.pcmBuffer=[[],[]];
}
,_subWrite:function(buffer, pcmSize, offset, speed){
var This=this;
var pcms=This.pcmBuffer;
var pcm0=pcms[0],pcm1=pcms[1];
//截取数据
var pcm=new Int16Array(pcmSize);
var i=0,n=0;
for(var j=0;n<pcmSize && j<pcm0.length;){//简单重采样
pcm[n++]=pcm0[i];
j+=speed; i=Math.round(j);
}
if(i>=pcm0.length){//堆积的消耗完了
pcm0=new Int16Array(0);
for(j=0,i=0;n<pcmSize && j<pcm1.length;){
pcm[n++]=pcm1[i];
j+=speed; i=Math.round(j);
}
if(i>=pcm1.length){
pcm1=new Int16Array(0);
}else{
pcm1=pcm1.subarray(i);
}
pcms[1]=pcm1;
}else{
pcm0=pcm0.subarray(i);
}
pcms[0]=pcm0;
//写入到audioBuffer中
var channel=buffer.getChannelData(0);
for(var i=0;i<pcmSize;i++,offset++){
channel[offset]=pcm[i]/0x7FFF;
}
This._Tc+=pcmSize;//更新已播放时长
This._updateTime();
return offset;
}
};
var NoStartMsg=function(){
return $T("TZPq::{1}未调用start方法",0,BufferStreamPlayerTxt);
};
/**pcm数据进行首尾1ms淡入淡出处理播放时可以大幅减弱爆音**/
var FadeInOut=BufferStreamPlayer.FadeInOut=function(arr,sampleRate){
var sd=sampleRate/1000*1;//浮点数arr是Int16或者Float32
for(var i=0;i<sd;i++){
arr[i]*=i/sd;
}
for(var l=arr.length,i=~~(l-sd);i<l;i++){
arr[i]*=(l-i)/sd;
}
};
/**解码音频文件成pcm**/
var DecodeAudio=BufferStreamPlayer.DecodeAudio=function(arrayBuffer,True,False){
var ctx=Recorder.GetContext();
if(!ctx){//强制激活Recorder.Ctx 不支持大概率也不支持解码
False&&False($T("iCFC::浏览器不支持音频解码"));
return;
};
if(!arrayBuffer || !(arrayBuffer instanceof ArrayBuffer)){
False&&False($T("wE2k::音频解码数据必须是ArrayBuffer"));
return;//非ArrayBuffer 有日志但不抛异常 不会走回调
};
ctx.decodeAudioData(arrayBuffer,function(raw){
var src=raw.getChannelData(0);
var sampleRate=raw.sampleRate;
var pcm=new Int16Array(src.length);
for(var i=0;i<src.length;i++){//floatTo16BitPCM
var s=Math.max(-1,Math.min(1,src[i]));
s=s<0?s*0x8000:s*0x7FFF;
pcm[i]=s;
};
True&&True({
sampleRate:sampleRate
,duration:Math.round(src.length/sampleRate*1000)
,data:pcm
});
},function(e){
False&&False($T("mOaT::音频解码失败:{1}",0,e&&e.message||"-"));
});
};
var CLog=function(){
var v=arguments; v[0]="["+BufferStreamPlayerTxt+"]"+v[0];
Recorder.CLog.apply(null,v);
};
Recorder[BufferStreamPlayerTxt]=BufferStreamPlayer;
}));

View File

@ -0,0 +1,372 @@
/***
简单用 正弦波方波锯齿波三角波 函数生成一段音乐简谱的pcm数据主要用于测试时提供音频数据本可音频生成插件可以移植到其他语言环境如需定制可联系作者
https://github.com/xiangyuecn/Recorder
此插件在线生成测试assets/runtime-codes/test.create-audio.nmn2pcm.js
var pcmData=Recorder.NMN2PCM(set);
set配置{
texts:""|["",""] 简谱格式化文本如果格式不符合要求将会抛异常
sampleRate: 生成pcm的采样率默认48000取值不能过低否则会削除高音
timbre: 音色默认2.0使用音符对应频率的一个倍频取值>=1.0
meterDuration: 一拍时长毫秒默认600ms
muteDuration: 音符之间的静默毫秒0时无静默默认meterDur/4最大50ms
beginDuration: 开头的静默时长毫秒0时无静默默认为200ms
endDuration: 结尾的静默时长毫秒0时无静默默认为200ms
volume: 音量默认0.3取值范围0.0-1.0最大值1
waveType: 波形发生器类型默认"sine"取值sine(正弦波)square(方波volume应当减半)sawtooth(锯齿波)triangle(三角波)
}
texts格式单个文本或文本数组
- 四分音符(一拍)低音: 1.-7. 中音: 1-7 高音: 1'-7' 休止符(静音)0
- 音符后面用 "." 表示低音尽量改用"."".." 倍低音"..." 超低音
- 音符后面用 "'" 表示高音尽量改用"'""''" 倍高音"'''" 超高音
- 音符之间用 "|" " " 分隔一拍
- 一拍里面多个音符用 "," 分隔每个音按权重分配这一拍的时长占比6,7为一拍67各占1/2相当于八分音符
- 音符后面用 "-" 表示二分音符简单计算为1+1=2拍时长几个-就加几拍
- 音符后面用 "_" 表示八分音符两两在一拍里面的音符可以免写_自动会按1/2分配一拍里面只有一个音时这拍会被简单计算为1/2=0.5其他情况计算会按权重分配这一拍的时长(复杂)6,7_为1/2+1/2/2=0.756*,7_才是(1+0.5)/2+1/2/2=1其中6权重1分配1/2=0.57权重0.5分配1/2/2=0.25多加一个"_"就多除个26_,7_是1/2+1/2=1等同于6,7可免写_6__,7__是1/2/2+1/2/2=0.5只要权重加起来是整数就算作完整的1拍
- 音符后面用 "*" 表示1+0.5=1.5多出来的1/2计算和_相同(复杂)"**"两个表示加0.25
- 可以使用 "S"(sine) "Q"(square) "A"(sawtooth) "T"(triangle) 来切换后续波形发生器类型按一拍来书写但不占用时长类型后面可以接 "(2.0)" 来设置音色 "[0.5]" 来设置音量为set.volume*0.5特殊值 "R"(reset) 可重置类型成set配置值如果R后面没有接音色或音量也会被重置比如"1 2|A(4.0)[0.6] 3 4 R|5 6"其中12 56使用set配置的类型和音色音量34使用锯齿波音色4.0音量0.18=0.3*0.6
- 如果同时有多个音必须提供数组格式每个音单独提供一个完整简谱必须同步对齐
返回结果{
pcm: Int16Arraypcm数据
duration: 123 pcm的时长单位毫秒
set: {...} 使用的set配置
warns: [] 不适合抛异常的提示消息
}
Recorder.NMN2PCM.GetExamples() 可获取内置的简谱
***/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var NMN2PCM=function(set){
var texts=set.texts||[]; if(typeof(texts)=="string") texts=[texts];
var setSR=set.sampleRate, sampleRate=setSR; if(!sampleRate || sampleRate<1)sampleRate=48000;
var meterDur=set.meterDuration||600;
var timbre=set.timbre||2; if(timbre<1)timbre=1;
var volume=set.volume; if(volume==null)volume=0.3;
volume=Math.max(0,volume); volume=Math.min(1,volume);
var waveType=set.waveType||"";
if(",sine,square,sawtooth,triangle,".indexOf(","+waveType+",")==-1)waveType="";
waveType=waveType||"sine";
var muteDur=set.muteDuration;
if(muteDur==null || muteDur<0){
muteDur=meterDur/4; if(muteDur>50)muteDur=50;
}
var mute0=new Int16Array(sampleRate*muteDur/1000);
var beginDur=set.beginDuration;
if(beginDur==null || beginDur<0) beginDur=200;
var beginMute=new Int16Array(sampleRate*beginDur/1000);
var endDur=set.endDuration;
if(endDur==null || endDur<0) endDur=200;
var endMute=new Int16Array(sampleRate*endDur/1000);
//生成C调频率 A=440 国际标准音
var s=function(s){ return 440/Math.pow(2,s/12) };
var Freqs=[s(9),s(7),s(5),s(4),s(2),s(0),s(-2)];
var FreqMP={};
for(var i=1;i<=7;i++){
var v=Freqs[i-1];
FreqMP[i+"..."]=v/8;
FreqMP[i+".."]=v/4;
FreqMP[i+"."]=v/2;
FreqMP[i]=v;
FreqMP[i+"'"]=v*2;
FreqMP[i+"''"]=v*4;
FreqMP[i+"'''"]=v*8;
}
var tracks=[],freqMax=0,freqMin=90000;
for(var iT=0;setSR!=-1 && iT<texts.length;iT++){
var meters=texts[iT].split(/[\s\|]+/);
var buffers=[],size=0,wType=waveType,wTimbre=timbre,wVol=volume;
for(var i0=0;i0<meters.length;i0++){
var txt0=meters[i0]; if(!txt0)continue;
var v0=txt0.charCodeAt(0);
if(v0<48 || v0>55){//不是0-7切换波形或音色
var m=/^(\w)(?:\((.+)\)|\[(.+)\])*$/.exec(txt0)||[],mT=m[1];
var m=/\((.+)\)/.exec(txt0)||[],mTb=m[1];
var m=/\[(.+)\]/.exec(txt0)||[],mVol=m[1];
if(mT=="R"){ wType=waveType;wTimbre=timbre;wVol=volume; }
else if(mT=="S") wType="sine";
else if(mT=="Q") wType="square";
else if(mT=="A") wType="sawtooth";
else if(mT=="T") wType="triangle";
else mT="";
if(!mT||mTb&&!+mTb||mVol&&!+mVol)throw new Error("Invalid: "+txt0);
if(mTb)wTimbre=+mTb;
if(mVol)wVol=volume*mVol;
continue;
}
var ys=txt0.split(",");//一拍里面的音符
var durTotal=meterDur; //一拍的时长,如果里面有+,代表多拍
var bTotal=0,hasG=0,hasX=0;
for(var i2=0;i2<ys.length;i2++){//先计算出每个音符的占用时长比例
var vs=ys[i2].split("");
var o={ y:vs[0],b:1,t:wType,tb:wTimbre,vol:wVol }; ys[i2]=o;
for(var i3=1;i3<vs.length;i3++){
var v=vs[i3];
if(v=="'") o.y+="'";
else if(v==".") o.y+=".";
else if(v=="-"){ o.b+=1; durTotal+=meterDur; }
else if(v=="_"){ o.b/=2; hasG=1; }
else if(v=="*" && !hasX){ o.b+=0.5; hasX=0.5;
if(vs[i3+1]=="*"){ o.b-=0.25; hasX=0.25; i3++; } }
else throw new Error($T("3RBa::符号[{1}]无效:{2}",0,v,txt0));
}
bTotal+=o.b;
}
if(bTotal%1>0){
if(hasG){//"_"不够数量,减掉时间
durTotal*=bTotal/Math.ceil(bTotal);
}else if(hasX){//"*"加上1/2|1/4拍的时间
durTotal+=meterDur*hasX;
}
}
durTotal-=ys.length*muteDur;//减掉中间的静默
for(var i2=0;i2<ys.length;i2++){//生成每个音符的pcm
var o=ys[i2],wType=o.t,wTimbre=o.tb,wVol=o.vol,freq=FreqMP[o.y]||0;
if(!freq && o.y!="0") throw new Error($T("U212::音符[{1}]无效:{2}",0,o.y,txt0));
freq=freq*wTimbre;
var dur=durTotal*o.b/bTotal;
var pcm=new Int16Array(Math.round(dur/1000*sampleRate));
if(freq){
freqMax=Math.max(freqMax,freq);
freqMin=Math.min(freqMin,freq);
//不同波形算法取自 https://github.com/cristovao-trevisan/wave-generator/blob/master/index.js
if(wType=="sine"){//正弦波
var V=(2 * Math.PI) * freq / sampleRate;
for(var i=0;i<pcm.length;i++){
var v=wVol*Math.sin(V * i);
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
}
}else if(wType=="square"){//方波
var V=sampleRate / freq;
for(var i=0;i<pcm.length;i++){
var v=wVol*((i % V) < (V / 2) ? 1 : -1);
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
}
}else if(wType=="sawtooth"){//锯齿波
var V=sampleRate / freq;
for(var i=0;i<pcm.length;i++){
var v=wVol*(-1 + 2 * (i % V) / V);
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
}
}else if(wType=="triangle"){//三角波
var V=sampleRate / freq;
for(var i=0;i<pcm.length;i++){
var Vi = (i + V / 4) % V;
var v=wVol*(Vi<V/2?(-1+4*Vi/V):(3-4*Vi/V));
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
}
}
var pcmDur4=~~(pcm.length/sampleRate*1000/4)||1;
FadeInOut(pcm,sampleRate,Math.min(pcmDur4, 10));
}
var mute=mute0; if(!buffers.length)mute=beginMute;
buffers.push(mute); size+=mute.length;
buffers.push(pcm); size+=pcm.length;
}
}
if(size>0){
buffers.push(endMute); size+=endMute.length;
tracks.push({buffers:buffers,size:size});
}
}
tracks.sort(function(a,b){return b.size-a.size});
var pcm=new Int16Array(tracks[0]&&tracks[0].size||0);
for(var iT=0;iT<tracks.length;iT++){
var o=tracks[iT],buffers=o.buffers,size=o.size;
if(iT==0){
for(var i=0,offset=0;i<buffers.length;i++){
var buf=buffers[i];
pcm.set(buf,offset);
offset+=buf.length;
}
}else{
var diffMs=(pcm.length-size)/sampleRate*1000;
if(diffMs>10){//10毫秒误差
throw new Error($T("7qAD::多个音时必须对齐,相差{1}ms",0,diffMs));
};
for(var i=0,offset=0;i<buffers.length;i++){
var buf=buffers[i];
for(var j=0;j<buf.length;j++){
var data_mix,data1=pcm[offset],data2=buf[j];
//简单混音算法 https://blog.csdn.net/dancing_night/article/details/53080819
if(data1<0 && data2<0){
data_mix = data1+data2 - (data1 * data2 / -0x7FFF);
}else{
data_mix = data1+data2 - (data1 * data2 / 0x7FFF);
};
pcm[offset++]=data_mix;
}
}
}
}
var dur=Math.round(pcm.length/sampleRate*1000);
var Warns=[],minSR=~~(freqMax*2);
if(freqMax && sampleRate<minSR){
var msg="sampleRate["+sampleRate+"] should be greater than "+minSR;
Warns.push(msg); Recorder.CLog("NMN2PCM: "+msg,3);
}
return {pcm:pcm, duration:dur, warns:Warns, set:{
texts:texts, sampleRate:sampleRate, timbre:timbre, meterDuration:meterDur
,muteDuration:muteDur, beginDuration:beginDur, endDuration:endDur
,volume:volume,waveType:waveType
}};
};
/**pcm数据进行首尾1ms淡入淡出处理播放时可以大幅减弱爆音**/
var FadeInOut=NMN2PCM.FadeInOut=function(arr,sampleRate,dur){
var sd=sampleRate/1000*(dur||1);//浮点数arr是Int16或者Float32
for(var i=0;i<sd;i++){
arr[i]*=i/sd;
}
for(var l=arr.length,i=~~(l-sd);i<l;i++){
arr[i]*=(l-i)/sd;
}
};
/***内置部分简谱*****/
NMN2PCM.GetExamples=function(){ return {
DFH:{//前3句https://www.hnchemeng.com/liux/201807/68393.html
name:"东方红"
,get:function(sampleRate){
return NMN2PCM({ //https://www.bilibili.com/video/BV1VW4y1v7nY?p=2
sampleRate:sampleRate
,meterDuration:1000
,timbre:3
,texts:"5 5,6|2-|1 1,6.|2-|5 5|6,1' 6,5|1 1,6.|2-"
});
}
}
,HappyBirthday:{//4句https://www.zaoxu.com/jjsh/bkdq/310228.html
name:$T("QGsW::祝你生日快乐")
,get:function(sampleRate){
return NMN2PCM({
sampleRate:sampleRate
,meterDuration:450
,timbre:4
,waveType:"triangle", volume:0.15
,texts:"5.,5. 6. 5.|1 7.-|5.,5. 6. 5.|2 1-|5.,5. 5 3|1 7. 6.|4*,4_ 3 1|2 1-"
});
}
}
,LHC:{//节选一段https://www.qinyipu.com/jianpu/jianpudaquan/41703.html
name:"兰花草(洒水版)"
,get:function(sampleRate){
return NMN2PCM({
sampleRate:sampleRate
,meterDuration:650
,timbre:4
,texts:"6.,3 3,3|3* 2_|1*,2_ 1,7.|6.-|6,6 6,6|6* 5_|3_,5_,5 5,4|3-|3,3_,6_ 6,5|3* 2_|1*,2_ 1,7.|6. 3.|3.,1 1,7.|6.* 2__,3__|2*,1_ 7._,7._,5.|6.-"
});
}
}
,ForElise:{//节选一段https://www.qinyipu.com/jianpu/chunyinle/3023.html
name:$T("emJR::致爱丽丝")
,get:function(sampleRate){
return NMN2PCM({
sampleRate:sampleRate
,meterDuration:550
,muteDuration:20
,timbre:6
,texts:"3',2'|3',2' 3',7 2',1'|"
+"6 0,1 3,6|7 0,3 5,7|1' 0 3',2'|"
+"3',2' 3',7 2',1'|6 0,1 3,6|7 0,3 1',7|"
+"6 0,7 1',2'|3' 0,5 4',3'|2' 0,4 3',2'|1' 0,3 2',1'|"
+"7"
});
}
}
,Canon_Right:{//节选一段https://www.cangqiang.com.cn/d/32153.html
name:$T("GsYy::卡农-右手简谱")
,get:function(sampleRate){
return NMN2PCM({
sampleRate:sampleRate
,meterDuration:700
,texts:"1',7 1',3 5 6,7|"
+"1' 3' 5',3' 5',6'|4',3' 2',4' 3',2' 1',7| 7 1',2'|"
+"5',3'_,4'_ 5',3'_,4'_ 5',5,6,7 1',2',3',4'|3',1'_,2'_ 3',3_,4_ 5,6,5,4 5,1',7,1'|"
+"6,1'_,7_ 6,5_,4_ 5,4,3,4 5,6,7,1'|6,1'_,7_ 1',7_,6_ 7,6,7,1' 2'_,1'_,7|1'-"
});
}
}
,Canon:{//开头一段https://www.kanpula.com/jianpu/21316.html
name:$T("bSFZ::卡农")
,get:function(sampleRate){
var txt1="",txt2="",txt3="",txt4="";
//(1)
txt1+="3'---|2'---|1'---|7---|";
txt2+="1'---|7---|6---|5---|";
txt3+="5---|5---|3---|3---|";
txt4+="R[0.3] 1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
//(5)
txt1+="6---|5---|6---|7---|";
txt2+="4---|3---|4---|5---|";
txt3+="1---|1---|1---|2---|";
txt4+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|";
//(9)
txt1+="3'---|2'---|1'---|7---|";
txt2+="1'---|7---|6---|5---|";
txt3+="5---|5---|3---|3-- 5'|";
txt4+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
//(13)
txt1+="4' 3' 2' 4'|3' 2' 1' 5|6- 6 1'|7 1' 2'-|";
txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|";
txt3+="0---|0---|0---|0---|";
txt4+="0---|0---|0---|0---|";
//(17)
txt1+="3',5 1'_ 5' 5_ 3'|3' 4' 3' 2'|1',3 6_ 3' 3_ 1'|1' 2' 1' 7|";
txt2+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
txt3+="0---|0---|0---|0---|";
txt4+="0---|0---|0---|0---|";
//(21)
txt1+="6,1 4_ 1' 1_ 6|5,1 3_ 1' 1_ 5|6,1 4_ 1' 1_ 6|7 7 1' 2'|";
txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5..,5. 5..,5. 6..,6. 6..,6.|";
txt3+="0---|0---|0---|0---|";
txt4+="0---|0---|0---|0---|";
return NMN2PCM({
sampleRate:sampleRate
,meterDuration:500
,texts:[txt1,txt2,txt3,txt4]
});
}
}
}
};
Recorder.NMN2PCM=NMN2PCM;
}));

View File

@ -0,0 +1,268 @@
/*
录音 Recorder扩展DTMF电话拨号按键信号解码器解码得到按键值
使用本扩展需要引入lib.fft.js支持
本扩展识别DTMF按键准确度高误识别率低支持识别120ms以上按键间隔+30ms以上的按键音纯js实现易于移植
使用场景电话录音软解软电话实时提取DTMF按键信号等
https://github.com/xiangyuecn/Recorder
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
/*
参数
pcmData:[Int16,...] pcm一维数组原则上一次处理的数据量不要超过10秒太长的数据应当分段延时处理
sampleRate: 123 pcm的采样率
prevChunk: null || {} 上次的返回值用于连续识别
返回:
chunk:{
keys:[keyItem,...] 识别到的按键如果未识别到数组长度为0
keyItem:{
key:"" //按键值 0-9 #*
time:123 //所在的时间位置ms
}
//以下用于下次接续识别
lastIs:"" "":mute {}:match 结尾处是什么
lastCheckCount:0 结尾如果是key此时的检查次数
prevIs:"" "":null {}:match 上次疑似检测到了什么
totalLen:0 总采样数相对4khz
pcm:[Int16,...] 4khz pcm数据
checkFactor:3 信号检查因子取值123默认为3不支持低于32ms的按键音检测当需要检测时可以设为2当信号更恶劣时设为1这样将会减少检查的次数导致错误识别率变高
debug:false 是否开启调试日志
}
*/
Recorder.DTMF_Decode=function(pcmData,sampleRate,prevChunk){
prevChunk||(prevChunk={});
var lastIs=prevChunk.lastIs||"";
var lastCheckCount=prevChunk.lastCheckCount==null?99:prevChunk.lastCheckCount;
var prevIs=prevChunk.prevIs||"";
var totalLen=prevChunk.totalLen||0;
var prevPcm=prevChunk.pcm;
var checkFactor=prevChunk.checkFactor||0;
var debug=prevChunk.debug;
var keys=[];
if(!Recorder.LibFFT){
throw new Error($T.G("NeedImport-2",["DTMF_Decode","src/extensions/lib.fft.js"]));
};
var bufferSize=256;//小一点每次处理的时长不会太长,也不要太小影响分辨率
var fft=Recorder.LibFFT(bufferSize);
/****初始值计算****/
var windowSize=bufferSize/4;//滑动窗口大小取值为4的原因64/4=16ms16ms*(3-1)=32ms保证3次取值判断有效性
var checkCount=checkFactor||3;//只有3次连续窗口内计算结果相同判定为有效信号或间隔
var muteCount=3;//两个信号间的最小间隔3个窗口大小
var startTotal=totalLen;
/****将采样率降低到4khz单次fft处理1000/(4000/256)=64ms分辨率4000/256=15.625hz允许连续dtmf信号间隔128ms****/
var stepFloat=sampleRate/4000;
var newSize=Math.floor(pcmData.length/stepFloat);
totalLen+=newSize;
var pos=0;
if(prevPcm&&prevPcm.length>bufferSize){//接上上次的数据,继续滑动
pos=windowSize*(checkCount+1);
newSize+=pos;
startTotal-=pos;
};
var arr=new Int16Array(newSize);
if(pos){
arr.set(prevPcm.subarray(prevPcm.length-pos));//接上上次的数据,继续滑动
};
for(var idxFloat=0;idxFloat<pcmData.length;pos++,idxFloat+=stepFloat){
//简单抽样
arr[pos]=pcmData[Math.round(idxFloat)];
};
pcmData=arr;
sampleRate=4000;
var freqStep=sampleRate/bufferSize;//分辨率
var logMin=20;//粗略计算信号强度最小值此值是先给0再根据下面的Math.log(fv)多次【测试】(下面一个log)出来的
/****循环处理所有数据,识别出所有按键信号****/
for(var i0=0; i0+bufferSize<=pcmData.length; i0+=windowSize){
var arr=pcmData.subarray(i0,i0+bufferSize);
var freqs=fft.transform(arr);
var freqPs=[];
var fv0=0,p0=0,v0=0,vi0=0, fv1=0,p1=0,v1=0,vi1=0;//查找高群和低群
for(var i2=0;i2<freqs.length;i2++){
var fv=freqs[i2];
var p=Math.log(fv);//粗略计算信号强度
freqPs.push(p);
var v=(i2+1)*freqStep;
if(p>logMin){
if(fv>fv0 && v<1050){
fv0=fv;
p0=p;
v0=v;
vi0=i2;
}else if(fv>fv1 && v>1050){
fv1=fv;
p1=p;
v1=v;
vi1=i2;
};
};
};
var pv0 =-1, pv1=-1;
if(v0>600 && v1<1700 && Math.abs(p0-p1)<2.5){//高低频的幅度相差不能太大,此值是先给个大值再多次【测试】(下面一个log)得出来的
//波形匹配度两个峰值之间应当是深V型曲线如果出现大幅杂波可以直接排除掉
var isV=1;
//先找出谷底
var pMin=p0,minI=0;
for(var i2=vi0;i2<vi1;i2++){
var v=freqPs[i2];
if(v && v<pMin){//0不作数
pMin=v;
minI=i2;
};
};
var xMax=(p0-pMin)*0.5//允许幅度变化最大值
//V左侧下降段
var curMin=p0;
for(var i2=vi0;isV&&i2<minI;i2++){
var v=freqPs[i2];
if(v<=curMin){
curMin=v;
}else if(v-curMin>xMax){
isV=0;//下降段检测到过度上升
};
};
//V右侧上升段
var curMax=pMin;
for(var i2=minI;isV&&i2<vi1;i2++){
var v=freqPs[i2];
if(v>=curMax){
curMax=v;
}else if(curMax-v>xMax){
isV=0;//上升段检测到过度下降
};
};
if(isV){
pv0=FindIndex(v0, DTMF_Freqs[0], freqStep);
pv1=FindIndex(v1, DTMF_Freqs[1], freqStep);
};
};
var key="";
if (pv0 >= 0 && pv1 >= 0) {
key = DTMF_Chars[pv0][pv1];
if(debug)console.log(key,Math.round((startTotal+i0)/sampleRate*1000),p0.toFixed(2),p1.toFixed(2),Math.abs(p0-p1).toFixed(2)); //【测试】得出数值
if(lastIs){
if(lastIs.key==key){//有效,增加校验次数
lastCheckCount++;
}else{//异常数据,恢复间隔计数
key="";
lastCheckCount=lastIs.old+lastCheckCount;
};
}else{
//没有连续的信号检查是否在100ms内有检测到信号当中间是断开的那种
if(prevIs && prevIs.old2 && prevIs.key==key){
if(startTotal+i0-prevIs.start<100*sampleRate/1000){
lastIs=prevIs;
lastCheckCount=prevIs.old2+1;
if(debug)console.warn("接续了开叉的信号"+lastCheckCount);
};
};
if(!lastIs){
if(lastCheckCount>=muteCount){//间隔够了,开始按键识别计数
lastIs={key:key,old:lastCheckCount,old2:lastCheckCount,start:startTotal+i0,pcms:[],use:0};
lastCheckCount=1;
}else{//上次识别以来间隔不够,重置间隔计数
key="";
lastCheckCount=0;
};
};
};
}else{
if(lastIs){//下一个,恢复间隔计数
lastIs.old2=lastCheckCount;
lastCheckCount=lastIs.old+lastCheckCount;
};
};
if(key){
if(debug)lastIs.pcms.push(arr);
//按键有效并且未push过
if(lastCheckCount>=checkCount && !lastIs.use){
lastIs.use=1;
keys.push({
key:key
,time:Math.round(lastIs.start/sampleRate*1000)
});
};
//重置间隔数据
if(lastIs.use){
if(debug)console.log(key+"有效按键",lastIs);
lastIs.old=0;
lastIs.old2=0;
lastCheckCount=0;
};
}else{
//未发现按键
if(lastIs){
if(debug)console.log(lastIs) //测试输出疑似key
prevIs=lastIs;
};
lastIs="";
lastCheckCount++;
};
};
return {
keys:keys
,lastIs:lastIs
,lastCheckCount:lastCheckCount
,prevIs:prevIs
,totalLen:totalLen
,pcm:pcmData
,checkFactor:checkFactor
,debug:debug
};
};
var DTMF_Freqs = [
[697, 770, 852, 941],
[1209, 1336, 1477, 1633]
];
var DTMF_Chars = [
["1", "2", "3", "A"],
["4", "5", "6", "B"],
["7", "8", "9", "C"],
["*", "0", "#", "D"],
];
var FindIndex=function(freq, freqs, freqStep){
var idx=-1,idxb=1000;
for(var i=0;i<freqs.length;i++){
var xb=Math.abs(freqs[i]-freq);
if(idxb>xb){
idxb=xb;
if(xb<freqStep*2){//最多2个分辨率内误差
idx=i;
};
};
};
return idx;
};
}));

View File

@ -0,0 +1,196 @@
/*
录音 Recorder扩展DTMF电话拨号按键信号编码生成器生成按键对应的音频PCM信号
本扩展分两个功能
DTMF_Encode
DTMF_EncodeMix
本扩展生成信号代码原理简单粗暴纯js实现易于移植0依赖
使用场景DTMF按键信号生成软电话实时发送DTMF按键信号等
https://github.com/xiangyuecn/Recorder
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
/**
本方法用来生成单个按键信号pcm数据属于底层方法要混合多个按键信号到别的pcm中请用封装好的DTMF_EncodeMix方法
参数
key: 单个按键0-9#*
sampleRate:123 要生成的pcm采样率
duration:100 按键音持续时间
mute:50 按键音前后静音时长
返回
pcm[Int16,...]生成单个按键信号
**/
Recorder.DTMF_Encode=function(key,sampleRate,duration,mute){
var durSize=Math.floor(sampleRate*(duration||100)/1000);
var muteSize=Math.floor(sampleRate*(mute==null?50:mute)/1000);
var pcm0=new Int16Array(durSize+muteSize*2);
var pcm1=new Int16Array(durSize+muteSize*2);
// https://github.com/watilde/node-dtfm/blob/master/encode.js
var f0=DTMF_Freqs[key][0];
var f1=DTMF_Freqs[key][1];
var vol=0.3;
for(var i=0;i<durSize;i++){
var v0=vol*Math.sin((2 * Math.PI) * f0 * (i / sampleRate));
var v1=vol*Math.sin((2 * Math.PI) * f1 * (i / sampleRate));
pcm0[i+muteSize]=Math.max(-1,Math.min(1,v0))*0x7FFF;
pcm1[i+muteSize]=Math.max(-1,Math.min(1,v1))*0x7FFF;
};
//简单叠加 低群 和 高群 信号
Mix(pcm0,0,pcm1,0);
return pcm0;
};
/**返回EncodeMix对象将输入的按键信号混合到持续输入的pcm流中当.mix(inputPcms)提供的太短的pcm会无法完整放下一个完整的按键信号所以需要不停调用.mix(inputPcms)进行混合**/
Recorder.DTMF_EncodeMix=function(set){
return new EncodeMix(set);
};
var EncodeMix=function(set){
var This=this;
This.set={
duration:100 //按键信号持续时间 ms最小值为30ms
,mute:25 //按键音前后静音时长 ms取值为0也是可以的
,interval:200 //两次按键信号间隔时长 ms间隔内包含了duration+mute*2最小值为120ms
};
for(var k in set){
This.set[k]=set[k];
};
This.keys="";
This.idx=0;
This.state={keyIdx:-1,skip:0};
};
EncodeMix.prototype={
/** 添加一个按键或多个按键 "0" "123#*"后面慢慢通过mix方法混合到pcm中无返回值 **/
add:function(keys){
this.keys+=keys;
}
/** pcmpcms:[[Int16,...],...]sampleRatepcmindexpcmspcm
返回混合状态对象
注意调用本方法会修改pcms中的内容因此混合结果就在pcms内 **/
,mix:function(pcms,sampleRate,index){
index||(index=0);
var This=this,set=This.set;
var newEncodes=[];
var state=This.state;
var pcmPos=0;
loop:
for(var i0=index;i0<pcms.length;i0++){
var pcm=pcms[i0];
var key=This.keys.charAt(This.idx);
if(!key){//没有需要处理的按键,把间隔消耗掉
state.skip=Math.max(0, state.skip-pcm.length);
} else while(key){
//按键间隔处理
if(state.skip){
var op=pcm.length-pcmPos;
if(op<=state.skip){
state.skip-=op;
pcmPos=0;
continue loop;
};
pcmPos+=state.skip;
state.skip=0;
};
var keyPcm=state.keyPcm;
//这个key已经混合过看看有没有剩余的信号
if(state.keyIdx==This.idx){
if(state.cur>=keyPcm.length){
state.keyIdx=-1;
};
};
//新的key生成信号
if(state.keyIdx!=This.idx){
keyPcm=Recorder.DTMF_Encode(key,sampleRate,set.duration,set.mute);
state.keyIdx=This.idx;
state.cur=0;
state.keyPcm=keyPcm;
newEncodes.push({
key:key
,data:keyPcm
});
};
//将keyPcm混合到当前pcm中实际是替换逻辑
var res=Mix(pcm,pcmPos,keyPcm,state.cur,true);
state.cur=res.cur;
pcmPos=res.last;
//下一个按键
if(res.cur>=keyPcm.length){
This.idx++;
key=This.keys.charAt(This.idx);
state.skip=Math.floor(sampleRate*(set.interval-set.duration-set.mute*2)/1000);
};
//当前pcm的位置已消耗完
if(res.last>=pcm.length){
pcmPos=0;
continue loop;//下一个pcm
};
};
};
return {
newEncodes:newEncodes //本次混合新生成的按键信号列表 [{key:"*",data:[Int16,...]},...],如果没有产生新信号将为空数组
,hasNext:This.idx<This.keys.length //是否还有未混合完的信号
};
}
};
//teach.realtime.mix_multiple 抄过来的简单混合算法
var Mix=function(buffer,pos1,add,pos2,mute){
for(var j=pos1,cur=pos2;;j++,cur++){
if(j>=buffer.length || cur>=add.length){
return {
last:j
,cur:cur
};
};
if(mute){
buffer[j]=0;//置为0即为静音
};
var data_mix,data1=buffer[j],data2=add[cur];
//简单混音算法 https://blog.csdn.net/dancing_night/article/details/53080819
if(data1<0 && data2<0){
data_mix = data1+data2 - (data1 * data2 / -0x7FFF);
}else{
data_mix = data1+data2 - (data1 * data2 / 0x7FFF);
};
buffer[j]=data_mix;
};
};
var DTMF_Freqs={
'1': [697, 1209] ,'2': [697, 1336] ,'3': [697, 1477] ,'A': [697, 1633]
,'4': [770, 1209] ,'5': [770, 1336] ,'6': [770, 1477] ,'B': [770, 1633]
,'7': [852, 1209] ,'8': [852, 1336] ,'9': [852, 1477] ,'C': [852, 1633]
,'*': [941, 1209] ,'0': [941, 1336] ,'#': [941, 1477] ,'D': [941, 1633]
};
}));

View File

@ -0,0 +1,377 @@
/*
录音 Recorder扩展频率直方图显示
使用本扩展需要引入src/extensions/lib.fft.js支持直方图特意优化主要显示0-5khz语音部分线性其他高频显示区域较小不适合用来展示音乐频谱可通过配置fullFreq来恢复成完整的线性频谱或自行修改源码修改成倍频程频谱伯德图对数频谱本可视化插件可以移植到其他语言环境如需定制可联系作者
https://github.com/xiangyuecn/Recorder
本扩展核心算法主要参考了Java开源库jmp123 版本0.3 的代码
https://www.iteye.com/topic/851459
https://sourceforge.net/projects/jmp123/files/
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var FrequencyHistogramView=function(set){
return new fn(set);
};
var ViewTxt="FrequencyHistogramView";
var fn=function(set){
var This=this;
var o={
/*
elem:"css selector" //自动显示到dom并以此dom大小为显示大小
//或者配置显示大小手动把frequencyObj.elem显示到别的地方
,width:0 //显示宽度
,height:0 //显示高度
H5环境以上配置二选一
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象需支持getContext("2d")支持设置width、height支持drawImage(canvas,...)
,width:0 //canvas显示宽度
,height:0 //canvas显示高度
非H5环境使用以上配置
*/
scale:2 //缩放系数应为正整数使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
,fps:20 //绘制帧率,不可过高
,lineCount:30 //直方图柱子数量数量的多少对性能影响不大密集运算集中在FFT算法中
,widthRatio:0.6 //柱子线条宽度占比为所有柱子占用整个视图宽度的比例剩下的空白区域均匀插入柱子中间默认值也基本相当于一根柱子占0.6一根空白占0.4设为1不留空白当视图不足容下所有柱子时也不留空白
,spaceWidth:0 //柱子间空白固定基础宽度柱子宽度自适应当不为0时widthRatio无效当视图不足容下所有柱子时将不会留空白允许为负数让柱子发生重叠
,minHeight:0 //柱子保留基础高度position不为±1时应该保留点高度
,position:-1 //绘制位置,取值-1到1-1为最底下0为中间1为最顶上小数为百分比
,mirrorEnable:false //是否启用镜像,如果启用,视图宽度会分成左右两块,右边这块进行绘制,左边这块进行镜像(以中间这根柱子的中心进行镜像)
,stripeEnable:true //是否启用柱子顶上的峰值小横条position不是-1时应当关闭否则会很丑
,stripeHeight:3 //峰值小横条基础高度
,stripeMargin:6 //峰值小横条和柱子保持的基础距离
,fallDuration:1000 //柱子从最顶上下降到最底部最长时间ms
,stripeFallDuration:3500 //峰值小横条从最顶上下降到底部最长时间ms
//柱子颜色配置:[位置css颜色...] 位置: 取值0.0-1.0之间
,linear:[0,"rgba(0,187,17,1)",0.5,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"]
//峰值小横条渐变颜色配置取值格式和linear一致留空为柱子的渐变颜色
,stripeLinear:null
,shadowBlur:0 //柱子阴影基础大小设为0不显示阴影如果柱子数量太多时请勿开启非常影响性能
,shadowColor:"#bbb" //柱子阴影颜色
,stripeShadowBlur:-1 //峰值小横条阴影基础大小设为0不显示阴影-1为柱子的大小如果柱子数量太多时请勿开启非常影响性能
,stripeShadowColor:"" //峰值小横条阴影颜色,留空为柱子的阴影颜色
,fullFreq:false //是否要绘制所有频率默认false主要绘制5khz以下的频率高频部分占比很少此时不同的采样率对频谱显示几乎没有影响设为true后不同采样率下显示的频谱是不一样的因为 最大频率=采样率/2 会有差异
//当发生绘制时会回调此方法参数为当前绘制的频率数据和采样率可实现多个直方图同时绘制只消耗一个input输入和计算时间
,onDraw:function(frequencyData,sampleRate){}
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
var cCanvas="compatibleCanvas";
if(set[cCanvas]){
var canvas=This.canvas=set[cCanvas];
}else{
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
var elem=set.elem;
if(elem){
if(typeof(elem)=="string"){
elem=document.querySelector(elem);
}else if(elem.length){
elem=elem[0];
};
};
if(elem){
set.width=elem.offsetWidth;
set.height=elem.offsetHeight;
};
var thisElem=This.elem=document.createElement("div");
thisElem.style.fontSize=0;
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
var canvas=This.canvas=thisElem.querySelector("canvas");
if(elem){
elem.innerHTML="";
elem.appendChild(thisElem);
};
};
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
if(!width || !height){
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
};
canvas.width=width;
canvas.height=height;
var ctx=This.ctx=canvas.getContext("2d");
if(!Recorder.LibFFT){
throw new Error($T.G("NeedImport-2",[ViewTxt,"src/extensions/lib.fft.js"]));
};
This.fft=Recorder.LibFFT(1024);
//柱子所在高度
This.lastH=[];
//峰值小横条所在高度
This.stripesH=[];
};
fn.prototype=FrequencyHistogramView.prototype={
genLinear:function(ctx,colors,from,to){
var rtv=ctx.createLinearGradient(0,from,0,to);
for(var i=0;i<colors.length;){
rtv.addColorStop(colors[i++],colors[i++]);
};
return rtv;
}
,input:function(pcmData,powerLevel,sampleRate){
var This=this;
This.sampleRate=sampleRate;
This.pcmData=pcmData;
This.pcmPos=0;
This.inputTime=Date.now();
This.schedule();
}
,schedule:function(){
var This=this,set=This.set;
var interval=Math.floor(1000/set.fps);
if(!This.timer){
This.timer=setInterval(function(){
This.schedule();
},interval);
};
var now=Date.now();
var drawTime=This.drawTime||0;
if(now-This.inputTime>set.stripeFallDuration*1.3){
//超时没有输入,顶部横条已全部落下,干掉定时器
clearInterval(This.timer);
This.timer=0;
This.lastH=[];//重置高度再绘制一次,避免定时不准没到底就停了
This.stripesH=[];
This.draw(null,This.sampleRate);
return;
};
if(now-drawTime<interval){
//没到间隔时间,不绘制
return;
};
This.drawTime=now;
//调用FFT计算频率数据
var bufferSize=This.fft.bufferSize;
var pcm=This.pcmData;
var pos=This.pcmPos;
var arr=new Int16Array(bufferSize);
for(var i=0;i<bufferSize&&pos<pcm.length;i++,pos++){
arr[i]=pcm[pos];
};
This.pcmPos=pos;
var frequencyData=This.fft.transform(arr);
//推入绘制
This.draw(frequencyData,This.sampleRate);
}
,draw:function(frequencyData,sampleRate){
var This=this,set=This.set;
var ctx=This.ctx;
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
var lineCount=set.lineCount;
var bufferSize=This.fft.bufferSize;
//计算高度位置
var position=set.position;
var posAbs=Math.abs(set.position);
var originY=position==1?0:height;//y轴原点
var heightY=height;//最高的一边高度
if(posAbs<1){
heightY=heightY/2;
originY=heightY;
heightY=Math.floor(heightY*(1+posAbs));
originY=Math.floor(position>0?originY*(1-posAbs):originY*(1+posAbs));
};
var lastH=This.lastH;
var stripesH=This.stripesH;
var speed=Math.ceil(heightY/(set.fallDuration/(1000/set.fps)));
var stripeSpeed=Math.ceil(heightY/(set.stripeFallDuration/(1000/set.fps)));
var stripeMargin=set.stripeMargin*scale;
var Y0=1 << (Math.round(Math.log(bufferSize)/Math.log(2) + 3) << 1);
var logY0 = Math.log(Y0)/Math.log(10);
var dBmax=20*Math.log(0x7fff)/Math.log(10);
var fftSize=bufferSize/2,fftSize5k=fftSize;
if(!set.fullFreq){//非绘制所有频率时计算5khz所在位置8000采样率及以下最高只有4khz
fftSize5k=Math.min(fftSize,Math.floor(fftSize*5000/(sampleRate/2)));
}
var isFullFreq=fftSize5k==fftSize;
var line80=isFullFreq?lineCount:Math.round(lineCount*0.8);//80%的柱子位置
var fftSizeStep1=fftSize5k/line80;
var fftSizeStep2=isFullFreq?0:(fftSize-fftSize5k)/(lineCount-line80);
var fftIdx=0;
for(var i=0;i<lineCount;i++){
// !fullFreq 时不采用jmp123的非线性划分频段录音语音并不适用于音乐的频率应当弱化高频部分
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
var start=Math.ceil(fftIdx);
if(i<line80){
//5khz以下
fftIdx+=fftSizeStep1;
}else{
//5khz以上
fftIdx+=fftSizeStep2;
};
var end=Math.ceil(fftIdx); if(end==start)end++;
end=Math.min(end,fftSize);
//参考AudioGUI.java .drawHistogram方法
//查找当前频段的最大"幅值"
var maxAmp=0;
if(frequencyData){
for (var j=start; j<end; j++) {
maxAmp=Math.max(maxAmp,Math.abs(frequencyData[j]));
};
};
//计算音量
var dB= (maxAmp > Y0) ? Math.floor((Math.log(maxAmp)/Math.log(10) - logY0) * 17) : 0;
var h=heightY*Math.min(dB/dBmax,1);
//使柱子匀速下降
lastH[i]=(lastH[i]||0)-speed;
if(h<lastH[i]){h=lastH[i];};
if(h<0){h=0;};
lastH[i]=h;
var shi=stripesH[i]||0;
if(h&&h+stripeMargin>shi) {
stripesH[i]=h+stripeMargin;
}else{
//使峰值小横条匀速度下落
var sh =shi-stripeSpeed;
if(sh < 0){sh = 0;};
stripesH[i] = sh;
};
};
//开始绘制图形
ctx.clearRect(0,0,width,height);
var linear1=This.genLinear(ctx,set.linear,originY,originY-heightY);//上半部分的填充
var stripeLinear1=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY-heightY)||linear1;//上半部分的峰值小横条填充
var linear2=This.genLinear(ctx,set.linear,originY,originY+heightY);//下半部分的填充
var stripeLinear2=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY+heightY)||linear2;//上半部分的峰值小横条填充
//计算柱子间距
var mirrorEnable=set.mirrorEnable;
var mirrorCount=mirrorEnable?lineCount*2-1:lineCount;//镜像柱子数量翻一倍-1根
var widthRatio=set.widthRatio;
var spaceWidth=set.spaceWidth*scale;
if(spaceWidth!=0){
widthRatio=(width-spaceWidth*(mirrorCount+1))/width;
};
for(var i=0;i<2;i++){
var lineFloat=Math.max(1*scale,(width*widthRatio)/mirrorCount);//柱子宽度至少1个单位
var lineWN=Math.floor(lineFloat),lineWF=lineFloat-lineWN;//提取出小数部分
var spaceFloat=(width-mirrorCount*lineFloat)/(mirrorCount+1);//均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
if(spaceFloat>0 && spaceFloat<1){
widthRatio=1; spaceFloat=0; //不够一个像素,丢弃不绘制间隔,重新计算
}else break;
};
//绘制
var minHeight=set.minHeight*scale;
var XFloat=mirrorEnable?(width-lineWN)/2-spaceFloat:0;//镜像时,中间柱子位于正中心
for(var iMirror=0;iMirror<2;iMirror++){
if(iMirror){ ctx.save(); ctx.scale(-1,1); }
var xMirror=iMirror?width:0; //绘制镜像部分不用drawImage(canvas)进行镜像绘制提升兼容性iOS微信小程序bug https://developers.weixin.qq.com/community/develop/doc/000aaca2148dc8a235a0fb8c66b000
//绘制柱子
ctx.shadowBlur=set.shadowBlur*scale;
ctx.shadowColor=set.shadowColor;
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
xFloat+=spaceFloat;
x=Math.floor(xFloat)-xMirror;
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
h=Math.max(lastH[i],minHeight);
//绘制上半部分
if(originY!=0){
y=originY-h;
ctx.fillStyle=linear1;
ctx.fillRect(x, y, w, h);
};
//绘制下半部分
if(originY!=height){
ctx.fillStyle=linear2;
ctx.fillRect(x, originY, w, h);
};
xFloat+=w;
};
//绘制柱子顶上峰值小横条
if(set.stripeEnable){
var stripeShadowBlur=set.stripeShadowBlur;
ctx.shadowBlur=(stripeShadowBlur==-1?set.shadowBlur:stripeShadowBlur)*scale;
ctx.shadowColor=set.stripeShadowColor||set.shadowColor;
var stripeHeight=set.stripeHeight*scale;
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
xFloat+=spaceFloat;
x=Math.floor(xFloat)-xMirror;
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
h=stripesH[i];
//绘制上半部分
if(originY!=0){
y=originY-h-stripeHeight;
if(y<0){y=0;};
ctx.fillStyle=stripeLinear1;
ctx.fillRect(x, y, w, stripeHeight);
};
//绘制下半部分
if(originY!=height){
y=originY+h;
if(y+stripeHeight>height){
y=height-stripeHeight;
};
ctx.fillStyle=stripeLinear2;
ctx.fillRect(x, y, w, stripeHeight);
};
xFloat+=w;
};
};
if(iMirror){ ctx.restore(); }
if(!mirrorEnable) break;
};
if(frequencyData){
set.onDraw(frequencyData,sampleRate);
};
}
};
Recorder[ViewTxt]=FrequencyHistogramView;
}));

118
node_modules/recorder-core/src/extensions/lib.fft.js generated vendored Normal file
View File

@ -0,0 +1,118 @@
/*
时域转频域快速傅里叶变换(FFT)
https://github.com/xiangyuecn/Recorder
var fft=Recorder.LibFFT(bufferSize)
bufferSize取值2的n次方
fft.bufferSize 实际采用的bufferSize
fft.transform(inBuffer)
inBuffer:[Int16,...] 数组长度必须是bufferSize
返回[Float64(Long),...]长度为bufferSize/2
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
/*
从FFT.java 移植Java开源库jmp123 版本0.3
https://www.iteye.com/topic/851459
https://sourceforge.net/projects/jmp123/files/
*/
Recorder.LibFFT=function(bufferSize){
var FFT_N_LOG,FFT_N,MINY;
var real, imag, sintable, costable;
var bitReverse;
var FFT_Fn=function(bufferSize) {//bufferSize只能取值2的n次方
FFT_N_LOG=Math.round(Math.log(bufferSize)/Math.log(2));
FFT_N = 1 << FFT_N_LOG;
MINY = ((FFT_N << 2) * Math.sqrt(2));
real = [];
imag = [];
sintable = [0];
costable = [0];
bitReverse = [];
var i, j, k, reve;
for (i = 0; i < FFT_N; i++) {
k = i;
for (j = 0, reve = 0; j != FFT_N_LOG; j++) {
reve <<= 1;
reve |= (k & 1);
k >>>= 1;
}
bitReverse[i] = reve;
}
var theta, dt = 2 * Math.PI / FFT_N;
for (i = (FFT_N >> 1) - 1; i > 0; i--) {
theta = i * dt;
costable[i] = Math.cos(theta);
sintable[i] = Math.sin(theta);
}
}
/*
用于频谱显示的快速傅里叶变换
inBuffer 输入FFT_N个实数返回 FFT_N/2个输出值(复数模的平方)
*/
var getModulus=function(inBuffer) {
var i, j, k, ir, j0 = 1, idx = FFT_N_LOG - 1;
var cosv, sinv, tmpr, tmpi;
for (i = 0; i != FFT_N; i++) {
real[i] = inBuffer[bitReverse[i]];
imag[i] = 0;
}
for (i = FFT_N_LOG; i != 0; i--) {
for (j = 0; j != j0; j++) {
cosv = costable[j << idx];
sinv = sintable[j << idx];
for (k = j; k < FFT_N; k += j0 << 1) {
ir = k + j0;
tmpr = cosv * real[ir] - sinv * imag[ir];
tmpi = cosv * imag[ir] + sinv * real[ir];
real[ir] = real[k] - tmpr;
imag[ir] = imag[k] - tmpi;
real[k] += tmpr;
imag[k] += tmpi;
}
}
j0 <<= 1;
idx--;
}
j = FFT_N >> 1;
var outBuffer=new Float64Array(j);
/*
* 输出模的平方:
* for(i = 1; i <= j; i++)
* inBuffer[i-1] = real[i] * real[i] + imag[i] * imag[i];
*
* 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值
* 和Spectrum.Y0,Spectrum.logY0对应.
*/
sinv = MINY;
cosv = -MINY;
for (i = j; i != 0; i--) {
tmpr = real[i];
tmpi = imag[i];
if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)
outBuffer[i - 1] = 0;
else
outBuffer[i - 1] = Math.round(tmpr * tmpr + tmpi * tmpi);
}
return outBuffer;
}
FFT_Fn(bufferSize);
return {transform:getModulus,bufferSize:FFT_N};
};
}));

1155
node_modules/recorder-core/src/extensions/sonic.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,278 @@
/*
录音 Recorder扩展音频可视化波形显示
https://github.com/xiangyuecn/Recorder
外观和名称来源于
https://github.com/katspaugh/wavesurfer.js https://github.com/collab-project/videojs-record
本扩展的波形绘制直接简单的使用PCM的采样数值大小来进行线条的绘制同一段音频绘制出的波形和Audition内显示的波形外观上几乎没有差异
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var WaveSurferView=function(set){
return new fn(set);
};
var ViewTxt="WaveSurferView";
var fn=function(set){
var This=this;
var o={
/*
elem:"css selector" //自动显示到dom并以此dom大小为显示大小
//或者配置显示大小手动把surferObj.elem显示到别的地方
,width:0 //显示宽度
,height:0 //显示高度
H5环境以上配置二选一
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象需支持getContext("2d")支持设置width、height支持drawImage(canvas,...)
,compatibleCanvas_2x: CanvasObject //提供一个宽度是compatibleCanvas的2倍canvas对象
,width:0 //canvas显示宽度
,height:0 //canvas显示高度
非H5环境使用以上配置
*/
scale:2 //缩放系数应为正整数使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
,fps:50 //绘制帧率不可过高50-60fps运动性质动画明显会流畅舒适实际显示帧率达不到这个值也并无太大影响
,duration:2500 //当前视图窗口内最大绘制的波形的持续时间,此处决定了移动速率
,direction:1 //波形前进方向取值1由左往右-1由右往左
,position:0 //绘制位置,取值-1到1-1为最底下0为中间1为最顶上小数为百分比
,centerHeight:1 //中线基础粗细如果为0不绘制中线position=±1时应当设为0
//波形颜色配置:[位置css颜色...] 位置: 取值0.0-1.0之间
,linear:[0,"rgba(0,187,17,1)",0.7,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"]
,centerColor:"" //中线css颜色留空取波形第一个渐变颜色
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
var cCanvas="compatibleCanvas";
if(set[cCanvas]){
var canvas=This.canvas=set[cCanvas];
var canvas2=This.canvas2=set[cCanvas+"_2x"];
}else{
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
var elem=set.elem;
if(elem){
if(typeof(elem)=="string"){
elem=document.querySelector(elem);
}else if(elem.length){
elem=elem[0];
};
};
if(elem){
set.width=elem.offsetWidth;
set.height=elem.offsetHeight;
};
var thisElem=This.elem=document.createElement("div");
thisElem.style.fontSize=0;
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
var canvas=This.canvas=thisElem.querySelector("canvas");
var canvas2=This.canvas2=document.createElement("canvas");
if(elem){
elem.innerHTML="";
elem.appendChild(thisElem);
};
};
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
if(!width || !height){
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
};
canvas.width=width;
canvas.height=height;
var ctx=This.ctx=canvas.getContext("2d");
canvas2.width=width*2;//卷轴,后台绘制画布能容纳两块窗口内容,进行无缝滚动
canvas2.height=height;
var ctx2=This.ctx2=canvas2.getContext("2d");
This.x=0;
};
fn.prototype=WaveSurferView.prototype={
genLinear:function(ctx,colors,from,to){
var rtv=ctx.createLinearGradient(0,from,0,to);
for(var i=0;i<colors.length;){
rtv.addColorStop(colors[i++],colors[i++]);
};
return rtv;
}
,input:function(pcmData,powerLevel,sampleRate){
var This=this;
This.sampleRate=sampleRate;
This.pcmData=pcmData;
This.pcmPos=0;
This.inputTime=Date.now();
This.schedule();
}
,schedule:function(){
var This=this,set=This.set;
var interval=Math.floor(1000/set.fps);
if(!This.timer){
This.timer=setInterval(function(){
This.schedule();
},interval);
};
var now=Date.now();
var drawTime=This.drawTime||0;
if(now-drawTime<interval){
//没到间隔时间,不绘制
return;
};
This.drawTime=now;
//切分当前需要的绘制数据
var bufferSize=This.sampleRate/set.fps;
var pcm=This.pcmData;
var pos=This.pcmPos;
var arr=new Int16Array(Math.min(bufferSize,pcm.length-pos));
for(var i=0;i<arr.length;i++,pos++){
arr[i]=pcm[pos];
};
This.pcmPos=pos;
//推入绘制
if(arr.length){
This.draw(arr,This.sampleRate);
}else{
if(now-This.inputTime>1300){
//超时没有输入,干掉定时器
clearInterval(This.timer);
This.timer=0;
};
};
}
,draw:function(pcmData,sampleRate){
var This=this,set=This.set;
var ctx=This.ctx2;
var scale=set.scale;
var width=set.width*scale;
var width2=width*2;
var height=set.height*scale;
var lineWidth=1*scale;//一条线占用1个单位长度
//计算高度位置
var position=set.position;
var posAbs=Math.abs(set.position);
var originY=position==1?0:height;//y轴原点
var heightY=height;//最高的一边高度
if(posAbs<1){
heightY=heightY/2;
originY=heightY;
heightY=Math.floor(heightY*(1+posAbs));
originY=Math.floor(position>0?originY*(1-posAbs):originY*(1+posAbs));
};
//计算绘制占用长度
var pcmDuration=pcmData.length*1000/sampleRate;
var pcmWidth=pcmDuration*width/set.duration;
pcmWidth+=This.drawLoss||0;
var pointCount=0;
if(pcmWidth<lineWidth){
This.drawLoss=pcmWidth;
//pointCount=0; 不够一根不绘制
}else{
This.drawLoss=0;
pointCount=Math.floor(pcmWidth/lineWidth);
};
//***后台卷轴连续绘制***
var linear1=This.genLinear(ctx,set.linear,originY,originY-heightY);//上半部分的填充
var linear2=This.genLinear(ctx,set.linear,originY,originY+heightY);//下半部分的填充
var x=This.x;
var step=pcmData.length/pointCount;
for(var i=0,idx=0;i<pointCount;i++){
var j=Math.floor(idx);
var end=Math.floor(idx+step);
idx+=step;
//寻找区间内最大值
var max=0;
for(;j<end;j++){
max=Math.max(max,Math.abs(pcmData[j]));
};
//计算高度
var h=heightY*Math.min(1,max/0x7fff);
//绘制当前线条不管方向从x:0往x:max方向画就是了
//绘制上半部分
if(originY!=0){
ctx.fillStyle=linear1;
ctx.fillRect(x, originY-h, lineWidth, h);
};
//绘制下半部分
if(originY!=height){
ctx.fillStyle=linear2;
ctx.fillRect(x, originY, lineWidth, h);
};
x+=lineWidth;
//超过卷轴宽度,移动画布第二个窗口内容到第一个窗口
if(x>=width2){
ctx.clearRect(0,0,width,height);
ctx.drawImage(This.canvas2,width,0,width,height,0,0,width,height);
ctx.clearRect(width,0,width,height);
x=width;
};
};
This.x=x;
//***画回到显示区域***
ctx=This.ctx;
ctx.clearRect(0,0,width,height);
//绘制一条中线
var centerHeight=set.centerHeight*scale;
if(centerHeight){
var y=originY-Math.floor(centerHeight/2);
y=Math.max(y,0);
y=Math.min(y,height-centerHeight);
ctx.fillStyle=set.centerColor||set.linear[1];
ctx.fillRect(0, y, width, centerHeight);
};
//画回画布
var srcX=0,srcW=x,destX=0;
if(srcW>width){
srcX=srcW-width;
srcW=width;
}else{
destX=width-srcW;
};
var direction=set.direction;
if(direction==-1){//由右往左
ctx.drawImage(This.canvas2,srcX,0,srcW,height,destX,0,srcW,height);
}else{//由左往右
ctx.save();
ctx.scale(-1,1);
ctx.drawImage(This.canvas2,srcX,0,srcW,height,-width+destX,0,srcW,height);
ctx.restore();
};
}
};
Recorder[ViewTxt]=WaveSurferView;
}));

229
node_modules/recorder-core/src/extensions/waveview.js generated vendored Normal file
View File

@ -0,0 +1,229 @@
/*
录音 Recorder扩展动态波形显示
https://github.com/xiangyuecn/Recorder
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
var WaveView=function(set){
return new fn(set);
};
var ViewTxt="WaveView";
var fn=function(set){
var This=this;
var o={
/*
elem:"css selector" //自动显示到dom并以此dom大小为显示大小
//或者配置显示大小手动把waveviewObj.elem显示到别的地方
,width:0 //显示宽度
,height:0 //显示高度
H5环境以上配置二选一
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象需支持getContext("2d")支持设置width、height支持drawImage(canvas,...)
,width:0 //canvas显示宽度
,height:0 //canvas显示高度
非H5环境使用以上配置
*/
scale:2 //缩放系数应为正整数使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
,speed:9 //移动速度系数,越大越快
,phase:21.8 //相位,调整了速度后,调整这个值得到一个看起来舒服的波形
,fps:20 //绘制帧率调整后也需调整phase值
,keep:true //当停止了input输入时是否保持波形设为false停止后将变成一条线
,lineWidth:3 //线条基础粗细
//渐变色配置:[位置css颜色...] 位置: 取值0.0-1.0之间
,linear1:[0,"rgba(150,96,238,1)",0.2,"rgba(170,79,249,1)",1,"rgba(53,199,253,1)"] //线条渐变色1从左到右
,linear2:[0,"rgba(209,130,255,0.6)",1,"rgba(53,199,255,0.6)"] //线条渐变色2从左到右
,linearBg:[0,"rgba(255,255,255,0.2)",1,"rgba(54,197,252,0.2)"] //背景渐变色,从上到下
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
var cCanvas="compatibleCanvas";
if(set[cCanvas]){
var canvas=This.canvas=set[cCanvas];
}else{
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
var elem=set.elem;
if(elem){
if(typeof(elem)=="string"){
elem=document.querySelector(elem);
}else if(elem.length){
elem=elem[0];
};
};
if(elem){
set.width=elem.offsetWidth;
set.height=elem.offsetHeight;
};
var thisElem=This.elem=document.createElement("div");
thisElem.style.fontSize=0;
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
var canvas=This.canvas=thisElem.querySelector("canvas");
if(elem){
elem.innerHTML="";
elem.appendChild(thisElem);
};
};
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
if(!width || !height){
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
};
canvas.width=width;
canvas.height=height;
var ctx=This.ctx=canvas.getContext("2d");
This.linear1=This.genLinear(ctx,width,set.linear1);
This.linear2=This.genLinear(ctx,width,set.linear2);
This.linearBg=This.genLinear(ctx,height,set.linearBg,true);
This._phase=0;
};
fn.prototype=WaveView.prototype={
genLinear:function(ctx,size,colors,top){
var rtv=ctx.createLinearGradient(0,0,top?0:size,top?size:0);
for(var i=0;i<colors.length;){
rtv.addColorStop(colors[i++],colors[i++]);
};
return rtv;
}
,genPath:function(frequency,amplitude,phase){
//曲线生成算法参考 https://github.com/HaloMartin/MCVoiceWave/blob/f6dc28975fbe0f7fc6cc4dbc2e61b0aa5574e9bc/MCVoiceWave/MCVoiceWaveView.m#L268
var rtv=[];
var This=this,set=This.set;
var scale=set.scale;
var width=set.width*scale;
var maxAmplitude=set.height*scale/2;
for(var x=0;x<=width;x+=scale) {
var scaling=(1+Math.cos(Math.PI+(x/width)*2*Math.PI))/2;
var y=scaling*maxAmplitude*amplitude*Math.sin(2*Math.PI*(x/width)*frequency+phase)+maxAmplitude;
rtv.push(y);
}
return rtv;
}
,input:function(pcmData,powerLevel,sampleRate){
var This=this;
This.sampleRate=sampleRate;
This.pcmData=pcmData;
This.pcmPos=0;
This.inputTime=Date.now();
This.schedule();
}
,schedule:function(){
var This=this,set=This.set;
var interval=Math.floor(1000/set.fps);
if(!This.timer){
This.timer=setInterval(function(){
This.schedule();
},interval);
};
var now=Date.now();
var drawTime=This.drawTime||0;
if(now-drawTime<interval){
//没到间隔时间,不绘制
return;
};
This.drawTime=now;
//切分当前需要的绘制数据
var bufferSize=This.sampleRate/set.fps;
var pcm=This.pcmData;
var pos=This.pcmPos;
var len=Math.max(0, Math.min(bufferSize,pcm.length-pos));
var sum=0;
for(var i=0;i<len;i++,pos++){
sum+=Math.abs(pcm[pos]);
};
This.pcmPos=pos;
//推入绘制
if(len || !set.keep){
This.draw(Recorder.PowerLevel(sum, len));
}
if(!len && now-This.inputTime>1300){
//超时没有输入,干掉定时器
clearInterval(This.timer);
This.timer=0;
}
}
,draw:function(powerLevel){
var This=this,set=This.set;
var ctx=This.ctx;
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
var speedx=set.speed/set.fps;
var phase=This._phase-=speedx;//位移速度
var phase2=phase+speedx*set.phase;
var amplitude=powerLevel/100;
var path1=This.genPath(2,amplitude,phase);
var path2=This.genPath(1.8,amplitude,phase2);
//开始绘制图形
ctx.clearRect(0,0,width,height);
//绘制包围背景
ctx.beginPath();
for(var i=0,x=0;x<=width;i++,x+=scale) {
if (x==0) {
ctx.moveTo(x,path1[i]);
}else {
ctx.lineTo(x,path1[i]);
};
};
i--;
for(var x=width-1;x>=0;i--,x-=scale) {
ctx.lineTo(x,path2[i]);
};
ctx.closePath();
ctx.fillStyle=This.linearBg;
ctx.fill();
//绘制线
This.drawPath(path2,This.linear2);
This.drawPath(path1,This.linear1);
}
,drawPath:function(path,linear){
var This=this,set=This.set;
var ctx=This.ctx;
var scale=set.scale;
var width=set.width*scale;
ctx.beginPath();
for(var i=0,x=0;x<=width;i++,x+=scale) {
if (x==0) {
ctx.moveTo(x,path[i]);
}else {
ctx.lineTo(x,path[i]);
};
};
ctx.lineWidth=set.lineWidth*scale;
ctx.strokeStyle=linear;
ctx.stroke();
}
};
Recorder[ViewTxt]=WaveView;
}));

935
node_modules/recorder-core/src/i18n/Template.js generated vendored Normal file
View File

@ -0,0 +1,935 @@
/*
Recorder i18n/Template.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="Your-Language-Name" or "your-language"
Desc: This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件复制并改名后将文本翻译成对应语言即可
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="Your-Language-Name";
Recorder.CLog('Import Recorder i18n lang="Your-Language-Name"');
i18n.alias["Your-Language-Name"]="your-language";
var putSet={lang:"your-language"};
i18n.data["rtl$your-language"]=false;
i18n.data["desc$your-language"]="This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件,复制并改名后,将文本翻译成对应语言即可。";
//@@Exec i18n.GenerateDisplayEnglish=true;
//*************** Begin srcFile=recorder-core.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"K8zP:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="剩{1}个GetContext未close"
//@@en="There are {1} GetContext unclosed"
,"mSxV:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="注意ctx不是running状态rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用否则将在rec.start时尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
,"nMIy:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="Stream的采样率{1}不等于{2}将进行采样率转换注意音质不会变好甚至可能变差主要在移动端未禁用回声消除时会产生此现象浏览器有回声消除时可能只会返回16k采样率的音频数据"
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
,"eS8i:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="。由于{1}内部1秒375次回调在移动端可能会有性能问题导致回调丢失录音变短PC端无影响暂不建议开启{1}。"
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
,"ZGlf:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="Connect采用老的{1}"
//@@en="Connect uses the old {1}, "
,"7TU0:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="但已设置{1}尝试启用{2}"
//@@en="But {1} is set trying to enable {2}"
,"JwCL:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="可设置{1}尝试启用{2}"
//@@en="Can set {1} try to enable {2}"
,"VGjB:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}未返回任何音频,恢复使用{2}"
//@@en="{1} did not return any audio, reverting to {2}"
,"MxX1:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"XUap:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
,"yOta:"+ //args: {1}-{3}
"" /** TODO: translate to your-language **/
//@@zh="(此浏览器不支持{1}"
//@@en=" (This browser does not support {1}) "
,"VwPd:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}未返回任何音频,降级使用{2}"
//@@en="{1} did not return any audio, downgrade to {2}"
,"vHnb:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"O9P7:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
,"LMEm:"+ //args: {1}-{4}
"" /** TODO: translate to your-language **/
//@@zh="{1}的filter采样率变了重设滤波"
//@@en="The filter sampleRate of {1} has changed, reset the filter"
,"d48C:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}似乎传入了未重置chunk {2}"
//@@en="{1} seems to have passed in an unreset chunk {2}"
,"tlbC:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}和{2}必须是数值"
//@@en="{1} and {2} must be number"
,"VtS4:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="录音open失败"
//@@en="Recording open failed: "
,"5tWi:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="open被取消"
//@@en="open cancelled"
,"dFm8:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="open被中断"
//@@en="open interrupted"
,"VtJO:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="可尝试使用RecordApp解决方案"
//@@en=", you can try to use the RecordApp solution "
,"EMJq:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不能录音:"
//@@en="Cannot record: "
,"A5bm:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不支持此浏览器从流中获取录音"
//@@en="This browser does not support obtaining recordings from stream"
,"1iU7:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="从流中打开录音失败:"
//@@en="Failed to open recording from stream: "
,"BTW2:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无权录音(跨域请尝试给iframe添加麦克风访问策略如{1})"
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
,"Nclz:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh=",无可用麦克风"
//@@en=", no microphone available"
,"jBa9:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="用户拒绝了录音权限"
//@@en="User denied recording permission"
,"gyO5:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="浏览器禁止不安全页面录音可开启https解决"
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
,"oWNo:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="此浏览器不支持录音"
//@@en="This browser does not support recording"
,"COxc:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="发现同时多次调用open"
//@@en="It was found that open was called multiple times at the same time"
,"upb8:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="录音功能无效:无音频流"
//@@en="Invalid recording: no audio stream"
,"Q1GA:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh=",将尝试禁用回声消除后重试"
//@@en=", will try to disable echoCancellation and try again"
,"KxE2:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="请求录音权限错误"
//@@en="Error requesting recording permission"
,"xEQR:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无法录音:"
//@@en="Unable to record: "
,"bDOG:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
,"IjL3:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除移动端未禁用回声消除时可能会降低系统播放音量关闭录音后可恢复和仅提供16k采样率的音频流不需要回声消除时可明确配置成禁用来获得48k高音质的流请参阅文档中{2}配置"
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
,"RiWe:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="close被忽略因为同时open了多个rec只有最后一个会真正close"
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
,"hWVz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="忽略"
//@@en="ignore"
,"UHvm:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不支持{1}架构"
//@@en="{1} architecture not supported"
,"Essp:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
//@@en="{1} type does not support setting takeoffEncodeChunk"
,"2XBl:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="(未加载编码器)"
//@@en="(Encoder not loaded)"
,"LG7e:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="{1}环境不支持实时处理"
//@@en="{1} environment does not support real-time processing"
,"7uMV:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="补偿{1}ms"
//@@en="Compensation {1}ms"
,"4Kfd:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="未补偿{1}ms"
//@@en="Uncompensated {1}ms"
,"bM5i:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="回调出错是不允许的,需保证不会抛异常"
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
,"gFUF:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="低性能,耗时{1}ms"
//@@en="Low performance, took {1}ms"
,"2ghS:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="未进入异步前不能清除buffers"
//@@en="Buffers cannot be cleared before entering async"
,"ufqH:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="start失败未open"
//@@en="start failed: not open"
,"6WmN:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="start 开始录音,"
//@@en="start recording, "
,"kLDN:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="start被中断"
//@@en="start was interrupted"
,"Bp2y:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh=",可能无法录音:"
//@@en=", may fail to record: "
,"upkE:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="stop 和start时差:"
//@@en="Stop and start time difference: "
,"Xq4s:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="补偿:"
//@@en="compensate: "
,"3CQP:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"u8JG:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh=",请设置{1}"
//@@en=", please set {1}"
,"1skY:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
,"Wv7l:"+ //args: {1}-{3}
"" /** TODO: translate to your-language **/
//@@zh="{1}编码器返回的不是{2}"
//@@en="{1} encoder returned not {2}"
,"Vkbd:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
,"QWnr:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="生成的{1}无效"
//@@en="Invalid generated {1}"
,"Sz2H:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="未开始录音"
//@@en="Recording not started"
,"wf9t:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="开始录音前无用户交互导致AudioContext未运行"
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
,"Dl2c:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未采集到录音"
//@@en="Recording not captured"
,"Ltz3:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
,"xGuI:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="录音错误:"
//@@en="Recording error: "
,"AxOH:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="音频buffers被释放"
//@@en="Audio buffers are released"
,"xkKd:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="采样:{1} 花:{2}ms"
//@@en="Sampled: {1}, took: {2}ms"
,"CxeT:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="非浏览器环境,不支持{1}"
//@@en="Non-browser environment, does not support {1}"
,"NonBrowser-1:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="参数错误:{1}"
//@@en="Illegal argument: {1}"
,"IllegalArgs-1:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="调用{1}需要先导入{2}"
//@@en="Calling {1} needs to import {2} first"
,"NeedImport-2:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="不支持:{1}"
//@@en="Not support: {1}"
,"NotSupport-1:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="覆盖导入{1}"
//@@en="Override import {1}"
,"8HO5:"+ //args: {1}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=recorder-core.js ***************
//*************** Begin srcFile=engine/beta-amr.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="AMR-NB(NarrowBand)采样率设置无效只提供8000hz比特率范围{1}默认12.2kbps一帧20ms、{2}字节浏览器一般不支持播放amr格式可用Recorder.amr2wav()转码成wav播放"
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
//@@Put0
"b2mN:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
,"tQBv:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"q12D:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"TxjV:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据只有首次回调数据首帧包含AMR头在合并成AMR文件时如果没有把首帧数据包含进去则必须在文件开头添加上AMR头Recorder.AMR.AMR_HEADER转成二进制否则无法播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
,"Q7p7:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前环境不支持Web Workeramr实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
,"6o9Z:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="amr worker剩{1}个未stop"
//@@en="amr worker left {1} unstopped"
,"yYWs:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="amr编码器未start"
//@@en="amr encoder not started"
,"jOi8:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/beta-amr.js ***************
//*************** Begin srcFile=engine/beta-ogg.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="Ogg Vorbis比特率取值16-100kbps采样率取值无限制"
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
//@@Put0
"O8Gn:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"5si6:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据Ogg由数据页组成一页包含多帧音频数据含几秒的音频一页数据无法单独解码和播放此编码器每次输出都是完整的一页数据因此实时性会比较低在合并成完整ogg文件时必须将输出的所有数据合并到一起否则可能无法播放不支持截取中间一部分单独解码和播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
,"R8yz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前环境不支持Web WorkerOggVorbis实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
,"hB9D:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="ogg worker剩{1}个未stop"
//@@en="There are {1} unstopped ogg workers"
,"oTiy:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="ogg编码器未start"
//@@en="ogg encoder not started"
,"dIpw:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/beta-ogg.js ***************
//*************** Begin srcFile=engine/beta-webm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="此浏览器不支持进行webm编码未实现MediaRecorder"
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
//@@Put0
"L49q:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="只有比较新的浏览器支持压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法只能按照类似边播放边收听形式把数据导入到MediaRecorder有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小但音频文件中的比特率并非设定比特率采样率由于是我们自己采样的到这个编码器随他怎么搞"
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
,"tsTW:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="此浏览器不支持把录音转成webm格式"
//@@en="This browser does not support converting recordings to webm format"
,"aG4z:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="转码webm出错{1}"
//@@en="Error encoding webm: {1}"
,"PIX0:"+ //args: {1}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/beta-webm.js ***************
//*************** Begin srcFile=engine/g711x.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}{2}音频文件无法直接播放可用Recorder.{2}2wav()转码成wav播放采样率比特率设置无效固定为8000hz采样率、16位每个采样压缩成8位存储音频文件大小为8000字节/秒如需任意采样率支持请使用Recorder.{2}_encode()方法"
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
//@@Put0
"d8YX:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"29UK:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}编码器未start"
//@@en="{1} encoder not started"
,"quVJ:"+ //args: {1}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/g711x.js ***************
//*************** Begin srcFile=engine/mp3.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="采样率范围:{1};比特率范围:{2}不同比特率支持的采样率范围不同小于32kbps时采样率需小于32000"
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
//@@Put0
"Zm7L:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}不在mp3支持的取值范围{2}"
//@@en="{1} is not in the value range supported by mp3: {2}"
,"eGB9:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围{3}"
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
,"zLTa:"+ //args: {1}-{3}
"" /** TODO: translate to your-language **/
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"yhUs:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前环境不支持Web Workermp3实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
,"k9PT:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="mp3 worker剩{1}个未stop"
//@@en="There are {1} unstopped mp3 workers left"
,"fT6M:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="mp3编码器未start"
//@@en="mp3 encoder not started"
,"mPxH:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="和设置的不匹配{1},已更新成{2}"
//@@en="Does not match the set {1}, has been updated to {2}"
,"uY9i:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="Fix移除{1}帧"
//@@en="Fix remove {1} frame"
,"iMSm:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="移除帧数过多"
//@@en="Remove too many frames"
,"b9zm:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/mp3.js ***************
//*************** Begin srcFile=engine/pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="pcm为未封装的原始音频数据pcm音频文件无法直接播放可用Recorder.pcm2wav()转码成wav播放支持位数8位、16位填在比特率里面采样率取值无限制"
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
//@@Put0
"fWsN:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
,"uMUJ:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="pcm2wav必须提供sampleRate和bitRate"
//@@en="pcm2wav must provide sampleRate and bitRate"
,"KmRz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="pcm编码器未start"
//@@en="pcm encoder not started"
,"sDkA:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/pcm.js ***************
//*************** Begin srcFile=engine/wav.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="支持位数8位、16位填在比特率里面采样率取值无限制此编码器仅在pcm数据前加了一个44字节的wav头编码出来的16位wav文件去掉开头的44字节即可得到pcm其他wav编码器可能不是44字节"
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
//@@Put0
"gPSE:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
,"wyw9:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=engine/wav.js ***************
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="getAudioSrc方法已过时请直接使用getMediaStream然后赋值给audio.srcObject仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
//@@Put0
"0XYC:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="start被stop终止"
//@@en="start is terminated by stop"
,"6DDt:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="{1}多次start"
//@@en="{1} repeat start"
,"I4h4:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="浏览器不支持打开{1}"
//@@en="The browser does not support opening {1}"
,"P6Gs:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="注意ctx不是running状态start需要在用户操作(触摸、点击等)时进行调用否则会尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
,"JwDm:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="此浏览器的AudioBuffer实现不支持动态特性采用兼容模式"
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
,"qx6X:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="环境检测超时"
//@@en="Environment detection timeout"
,"cdOx:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="可能无法播放:{1}"
//@@en="Could not play: {1}"
,"S2Bu:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="input调用失败非pcm[Int16,...]输入时必须解码或者使用transform转换"
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
,"ZfGG:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="input调用失败未提供sampleRate"
//@@en="input call failed: sampleRate not provided"
,"N4ke:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="input调用失败data的sampleRate={1}和之前的={2}不同"
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
,"IHZd:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="延迟过大,已丢弃{1}ms {2}"
//@@en="The delay is too large, {1}ms has been discarded, {2}"
,"L8sC:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}未调用start方法"
//@@en="{1} did not call the start method"
,"TZPq:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="浏览器不支持音频解码"
//@@en="Browser does not support audio decoding"
,"iCFC:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="音频解码数据必须是ArrayBuffer"
//@@en="Audio decoding data must be ArrayBuffer"
,"wE2k:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="音频解码失败:{1}"
//@@en="Audio decoding failed: {1}"
,"mOaT:"+ //args: {1}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=extensions/buffer_stream.player.js ***************
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="符号[{1}]无效:{2}"
//@@en="Invalid symbol [{1}]: {2}"
//@@Put0
"3RBa:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="音符[{1}]无效:{2}"
//@@en="Invalid note [{1}]: {2}"
,"U212:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="多个音时必须对齐,相差{1}ms"
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
,"7qAD:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="祝你生日快乐"
//@@en="Happy Birthday to You"
,"QGsW:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="致爱丽丝"
//@@en="For Elise"
,"emJR:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="卡农-右手简谱"
//@@en="Canon - Right Hand Notation"
,"GsYy:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="卡农"
//@@en="Canon"
,"bSFZ:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
//*************** Begin srcFile=extensions/sonic.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="当前环境不支持Web Worker不支持调用Sonic.Async"
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
//@@Put0
"Ikdz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="sonic worker剩{1}个未flush"
//@@en="There are {1} unflushed sonic workers left"
,"IC5Y:"+ //args: {1}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=extensions/sonic.js ***************
//*************** Begin srcFile=app-support/app-native-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
//@@Put0
"WWoj:"+ //args: {1}-{3}
"" /** TODO: translate to your-language **/
//@@zh="未开始录音但收到Native PCM数据"
//@@en="Recording does not start, but Native PCM data is received"
,"rCAM:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听postMessage转发兼容传输数据请自行实现将top层接收到数据转发到本iframe不限层不然无法接收到录音数据"
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
,"t2OF:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音"
//@@en="Recording not started"
,"Z2y2:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=app-support/app-native-support.js ***************
//*************** Begin srcFile=app-support/app.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"uXtA:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
,"kIBu:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="重复注册{1}"
//@@en="Duplicate registration {1}"
,"ha2K:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="仅清理资源"
//@@en="Clean resources only"
,"wpTL:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音"
//@@en="Recording not started"
,"bpvP:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前环境不支持实时回调,无法进行{1}"
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
,"fLJD:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="录音权限请求失败:"
//@@en="Recording permission request failed: "
,"YnzX:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需先调用{1}"
//@@en="Need to call {1} first"
,"nwKR:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
,"citA:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="开始录音失败:"
//@@en="Failed to start recording: "
,"ecp9:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不能录音:"
//@@en="Cannot record: "
,"EKmS:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="已开始录音"
//@@en="Recording started"
,"k7Qo:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"Douz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="和Start时差{1}ms"
//@@en="Time difference from Start: {1}ms"
,"wqSH:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
,"g3VX:"+ //args: {1}-{4}
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=app-support/app.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

782
node_modules/recorder-core/src/i18n/en-US.js generated vendored Normal file
View File

@ -0,0 +1,782 @@
/*
Recorder i18n/en-US.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="en-US" or "en"
Desc: English, 英语This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自google翻译+百度翻译由中文翻译成英文
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="en-US";
Recorder.CLog('Import Recorder i18n lang="en-US"');
i18n.alias["en-US"]="en";
var putSet={lang:"en"};
i18n.data["rtl$en"]=false;
i18n.data["desc$en"]="English, 英语。This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自google翻译+百度翻译,由中文翻译成英文。";
//@@Exec i18n.GenerateDisplayEnglish=false;
//*************** Begin srcFile=recorder-core.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@Put0
"K8zP:"+ //args: {1}
"Duplicate import {1}"
//@@zh="剩{1}个GetContext未close"
,"mSxV:"+ //args: {1}
"There are {1} GetContext unclosed"
//@@zh="注意ctx不是running状态rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用否则将在rec.start时尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
,"nMIy:"+ //no args
" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
//@@zh="Stream的采样率{1}不等于{2}将进行采样率转换注意音质不会变好甚至可能变差主要在移动端未禁用回声消除时会产生此现象浏览器有回声消除时可能只会返回16k采样率的音频数据"
,"eS8i:"+ //args: {1}-{2}
"The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
//@@zh="。由于{1}内部1秒375次回调在移动端可能会有性能问题导致回调丢失录音变短PC端无影响暂不建议开启{1}。"
,"ZGlf:"+ //args: {1}
". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
//@@zh="Connect采用老的{1}"
,"7TU0:"+ //args: {1}
"Connect uses the old {1}, "
//@@zh="但已设置{1}尝试启用{2}"
,"JwCL:"+ //args: {1}-{2}
"But {1} is set trying to enable {2}"
//@@zh="可设置{1}尝试启用{2}"
,"VGjB:"+ //args: {1}-{2}
"Can set {1} try to enable {2}"
//@@zh="{1}未返回任何音频,恢复使用{2}"
,"MxX1:"+ //args: {1}-{2}
"{1} did not return any audio, reverting to {2}"
//@@zh="{1}多余回调"
,"XUap:"+ //args: {1}
"{1} redundant callback"
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
,"yOta:"+ //args: {1}-{3}
"Connect uses {1}, set {2} to restore old-fashioned {3}"
//@@zh="(此浏览器不支持{1}"
,"VwPd:"+ //args: {1}
" (This browser does not support {1}) "
//@@zh="{1}未返回任何音频,降级使用{2}"
,"vHnb:"+ //args: {1}-{2}
"{1} did not return any audio, downgrade to {2}"
//@@zh="{1}多余回调"
,"O9P7:"+ //args: {1}
"{1} redundant callback"
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
,"LMEm:"+ //args: {1}-{4}
"Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
//@@zh="{1}的filter采样率变了重设滤波"
,"d48C:"+ //args: {1}
"The filter sampleRate of {1} has changed, reset the filter"
//@@zh="{1}似乎传入了未重置chunk {2}"
,"tlbC:"+ //args: {1}-{2}
"{1} seems to have passed in an unreset chunk {2}"
//@@zh="{1}和{2}必须是数值"
,"VtS4:"+ //args: {1}-{2}
"{1} and {2} must be number"
//@@zh="录音open失败"
,"5tWi:"+ //no args
"Recording open failed: "
//@@zh="open被取消"
,"dFm8:"+ //no args
"open cancelled"
//@@zh="open被中断"
,"VtJO:"+ //no args
"open interrupted"
//@@zh="可尝试使用RecordApp解决方案"
,"EMJq:"+ //no args
", you can try to use the RecordApp solution "
//@@zh="不能录音:"
,"A5bm:"+ //no args
"Cannot record: "
//@@zh="不支持此浏览器从流中获取录音"
,"1iU7:"+ //no args
"This browser does not support obtaining recordings from stream"
//@@zh="从流中打开录音失败:"
,"BTW2:"+ //no args
"Failed to open recording from stream: "
//@@zh="无权录音(跨域请尝试给iframe添加麦克风访问策略如{1})"
,"Nclz:"+ //args: {1}
"No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
//@@zh=",无可用麦克风"
,"jBa9:"+ //no args
", no microphone available"
//@@zh="用户拒绝了录音权限"
,"gyO5:"+ //no args
"User denied recording permission"
//@@zh="浏览器禁止不安全页面录音可开启https解决"
,"oWNo:"+ //no args
"Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
//@@zh="此浏览器不支持录音"
,"COxc:"+ //no args
"This browser does not support recording"
//@@zh="发现同时多次调用open"
,"upb8:"+ //no args
"It was found that open was called multiple times at the same time"
//@@zh="录音功能无效:无音频流"
,"Q1GA:"+ //no args
"Invalid recording: no audio stream"
//@@zh=",将尝试禁用回声消除后重试"
,"KxE2:"+ //no args
", will try to disable echoCancellation and try again"
//@@zh="请求录音权限错误"
,"xEQR:"+ //no args
"Error requesting recording permission"
//@@zh="无法录音:"
,"bDOG:"+ //no args
"Unable to record: "
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
,"IjL3:"+ //args: {1}
"Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除移动端未禁用回声消除时可能会降低系统播放音量关闭录音后可恢复和仅提供16k采样率的音频流不需要回声消除时可明确配置成禁用来获得48k高音质的流请参阅文档中{2}配置"
,"RiWe:"+ //args: {1}-{2}
", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
//@@zh="close被忽略因为同时open了多个rec只有最后一个会真正close"
,"hWVz:"+ //no args
"close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
//@@zh="忽略"
,"UHvm:"+ //no args
"ignore"
//@@zh="不支持{1}架构"
,"Essp:"+ //args: {1}
"{1} architecture not supported"
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
,"2XBl:"+ //args: {1}
"{1} type does not support setting takeoffEncodeChunk"
//@@zh="(未加载编码器)"
,"LG7e:"+ //no args
"(Encoder not loaded)"
//@@zh="{1}环境不支持实时处理"
,"7uMV:"+ //args: {1}
"{1} environment does not support real-time processing"
//@@zh="补偿{1}ms"
,"4Kfd:"+ //args: {1}
"Compensation {1}ms"
//@@zh="未补偿{1}ms"
,"bM5i:"+ //args: {1}
"Uncompensated {1}ms"
//@@zh="回调出错是不允许的,需保证不会抛异常"
,"gFUF:"+ //no args
"Callback error is not allowed, you need to ensure that no exception will be thrown"
//@@zh="低性能,耗时{1}ms"
,"2ghS:"+ //args: {1}
"Low performance, took {1}ms"
//@@zh="未进入异步前不能清除buffers"
,"ufqH:"+ //no args
"Buffers cannot be cleared before entering async"
//@@zh="start失败未open"
,"6WmN:"+ //no args
"start failed: not open"
//@@zh="start 开始录音,"
,"kLDN:"+ //no args
"start recording, "
//@@zh="start被中断"
,"Bp2y:"+ //no args
"start was interrupted"
//@@zh=",可能无法录音:"
,"upkE:"+ //no args
", may fail to record: "
//@@zh="stop 和start时差:"
,"Xq4s:"+ //no args
"Stop and start time difference: "
//@@zh="补偿:"
,"3CQP:"+ //no args
"compensate: "
//@@zh="结束录音失败:"
,"u8JG:"+ //no args
"Failed to stop recording: "
//@@zh=",请设置{1}"
,"1skY:"+ //args: {1}
", please set {1}"
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
,"Wv7l:"+ //args: {1}-{3}
"Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
//@@zh="{1}编码器返回的不是{2}"
,"Vkbd:"+ //args: {1}-{2}
"{1} encoder returned not {2}"
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
,"QWnr:"+ //no args
"After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
//@@zh="生成的{1}无效"
,"Sz2H:"+ //args: {1}
"Invalid generated {1}"
//@@zh="未开始录音"
,"wf9t:"+ //no args
"Recording not started"
//@@zh="开始录音前无用户交互导致AudioContext未运行"
,"Dl2c:"+ //no args
", No user interaction before starting recording, resulting in AudioContext not running"
//@@zh="未采集到录音"
,"Ltz3:"+ //no args
"Recording not captured"
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
,"xGuI:"+ //args: {1}-{2}
"The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
//@@zh="录音错误:"
,"AxOH:"+ //no args
"Recording error: "
//@@zh="音频buffers被释放"
,"xkKd:"+ //no args
"Audio buffers are released"
//@@zh="采样:{1} 花:{2}ms"
,"CxeT:"+ //args: {1}-{2}
"Sampled: {1}, took: {2}ms"
//@@zh="非浏览器环境,不支持{1}"
,"NonBrowser-1:"+ //args: {1}
"Non-browser environment, does not support {1}"
//@@zh="参数错误:{1}"
,"IllegalArgs-1:"+ //args: {1}
"Illegal argument: {1}"
//@@zh="调用{1}需要先导入{2}"
,"NeedImport-2:"+ //args: {1}-{2}
"Calling {1} needs to import {2} first"
//@@zh="不支持:{1}"
,"NotSupport-1:"+ //args: {1}
"Not support: {1}"
//@@zh="覆盖导入{1}"
,"8HO5:"+ //args: {1}
"Override import {1}"
]);
//*************** End srcFile=recorder-core.js ***************
//*************** Begin srcFile=engine/beta-amr.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="AMR-NB(NarrowBand)采样率设置无效只提供8000hz比特率范围{1}默认12.2kbps一帧20ms、{2}字节浏览器一般不支持播放amr格式可用Recorder.amr2wav()转码成wav播放"
//@@Put0
"b2mN:"+ //args: {1}-{2}
"AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
,"tQBv:"+ //args: {1}-{2}
"AMR Info: does not match the set {1}, has been updated to {2}"
//@@zh="数据采样率低于{1}"
,"q12D:"+ //args: {1}
"Data sampleRate lower than {1}"
//@@zh="当前浏览器版本太低,无法实时处理"
,"TxjV:"+ //no args
"The current browser version is too low to process in real time"
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据只有首次回调数据首帧包含AMR头在合并成AMR文件时如果没有把首帧数据包含进去则必须在文件开头添加上AMR头Recorder.AMR.AMR_HEADER转成二进制否则无法播放"
,"Q7p7:"+ //no args
"takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
//@@zh="当前环境不支持Web Workeramr实时编码器运行在主线程中"
,"6o9Z:"+ //no args
"The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
//@@zh="amr worker剩{1}个未stop"
,"yYWs:"+ //args: {1}
"amr worker left {1} unstopped"
//@@zh="amr编码器未start"
,"jOi8:"+ //no args
"amr encoder not started"
]);
//*************** End srcFile=engine/beta-amr.js ***************
//*************** Begin srcFile=engine/beta-ogg.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="Ogg Vorbis比特率取值16-100kbps采样率取值无限制"
//@@Put0
"O8Gn:"+ //no args
"Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
//@@zh="当前浏览器版本太低,无法实时处理"
,"5si6:"+ //no args
"The current browser version is too low to process in real time"
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据Ogg由数据页组成一页包含多帧音频数据含几秒的音频一页数据无法单独解码和播放此编码器每次输出都是完整的一页数据因此实时性会比较低在合并成完整ogg文件时必须将输出的所有数据合并到一起否则可能无法播放不支持截取中间一部分单独解码和播放"
,"R8yz:"+ //no args
"takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
//@@zh="当前环境不支持Web WorkerOggVorbis实时编码器运行在主线程中"
,"hB9D:"+ //no args
"The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
//@@zh="ogg worker剩{1}个未stop"
,"oTiy:"+ //args: {1}
"There are {1} unstopped ogg workers"
//@@zh="ogg编码器未start"
,"dIpw:"+ //no args
"ogg encoder not started"
]);
//*************** End srcFile=engine/beta-ogg.js ***************
//*************** Begin srcFile=engine/beta-webm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="此浏览器不支持进行webm编码未实现MediaRecorder"
//@@Put0
"L49q:"+ //no args
"This browser does not support webm encoding, MediaRecorder is not implemented"
//@@zh="只有比较新的浏览器支持压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法只能按照类似边播放边收听形式把数据导入到MediaRecorder有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小但音频文件中的比特率并非设定比特率采样率由于是我们自己采样的到这个编码器随他怎么搞"
,"tsTW:"+ //no args
"Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
//@@zh="此浏览器不支持把录音转成webm格式"
,"aG4z:"+ //no args
"This browser does not support converting recordings to webm format"
//@@zh="转码webm出错{1}"
,"PIX0:"+ //args: {1}
"Error encoding webm: {1}"
]);
//*************** End srcFile=engine/beta-webm.js ***************
//*************** Begin srcFile=engine/g711x.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}{2}音频文件无法直接播放可用Recorder.{2}2wav()转码成wav播放采样率比特率设置无效固定为8000hz采样率、16位每个采样压缩成8位存储音频文件大小为8000字节/秒如需任意采样率支持请使用Recorder.{2}_encode()方法"
//@@Put0
"d8YX:"+ //args: {1}-{2}
"{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
//@@zh="数据采样率低于{1}"
,"29UK:"+ //args: {1}
"Data sampleRate lower than {1}"
//@@zh="{1}编码器未start"
,"quVJ:"+ //args: {1}
"{1} encoder not started"
]);
//*************** End srcFile=engine/g711x.js ***************
//*************** Begin srcFile=engine/mp3.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="采样率范围:{1};比特率范围:{2}不同比特率支持的采样率范围不同小于32kbps时采样率需小于32000"
//@@Put0
"Zm7L:"+ //args: {1}-{2}
"sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
//@@zh="{1}不在mp3支持的取值范围{2}"
,"eGB9:"+ //args: {1}-{2}
"{1} is not in the value range supported by mp3: {2}"
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围{3}"
,"zLTa:"+ //args: {1}-{3}
"sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
//@@zh="当前浏览器版本太低,无法实时处理"
,"yhUs:"+ //no args
"The current browser version is too low to process in real time"
//@@zh="当前环境不支持Web Workermp3实时编码器运行在主线程中"
,"k9PT:"+ //no args
"The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
//@@zh="mp3 worker剩{1}个未stop"
,"fT6M:"+ //args: {1}
"There are {1} unstopped mp3 workers left"
//@@zh="mp3编码器未start"
,"mPxH:"+ //no args
"mp3 encoder not started"
//@@zh="和设置的不匹配{1},已更新成{2}"
,"uY9i:"+ //args: {1}-{2}
"Does not match the set {1}, has been updated to {2}"
//@@zh="Fix移除{1}帧"
,"iMSm:"+ //args: {1}
"Fix remove {1} frame"
//@@zh="移除帧数过多"
,"b9zm:"+ //no args
"Remove too many frames"
]);
//*************** End srcFile=engine/mp3.js ***************
//*************** Begin srcFile=engine/pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="pcm为未封装的原始音频数据pcm音频文件无法直接播放可用Recorder.pcm2wav()转码成wav播放支持位数8位、16位填在比特率里面采样率取值无限制"
//@@Put0
"fWsN:"+ //no args
"pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
,"uMUJ:"+ //args: {1}-{2}
"PCM Info: {1} bit is not supported, has been updated to {2} bit"
//@@zh="pcm2wav必须提供sampleRate和bitRate"
,"KmRz:"+ //no args
"pcm2wav must provide sampleRate and bitRate"
//@@zh="pcm编码器未start"
,"sDkA:"+ //no args
"pcm encoder not started"
]);
//*************** End srcFile=engine/pcm.js ***************
//*************** Begin srcFile=engine/wav.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="支持位数8位、16位填在比特率里面采样率取值无限制此编码器仅在pcm数据前加了一个44字节的wav头编码出来的16位wav文件去掉开头的44字节即可得到pcm其他wav编码器可能不是44字节"
//@@Put0
"gPSE:"+ //no args
"Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
,"wyw9:"+ //args: {1}-{2}
"WAV Info: {1} bit is not supported, has been updated to {2} bit"
]);
//*************** End srcFile=engine/wav.js ***************
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="getAudioSrc方法已过时请直接使用getMediaStream然后赋值给audio.srcObject仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
//@@Put0
"0XYC:"+ //no args
"The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
//@@zh="start被stop终止"
,"6DDt:"+ //no args
"start is terminated by stop"
//@@zh="{1}多次start"
,"I4h4:"+ //args: {1}
"{1} repeat start"
//@@zh="浏览器不支持打开{1}"
,"P6Gs:"+ //args: {1}
"The browser does not support opening {1}"
//@@zh="注意ctx不是running状态start需要在用户操作(触摸、点击等)时进行调用否则会尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
,"JwDm:"+ //no args
" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
//@@zh="此浏览器的AudioBuffer实现不支持动态特性采用兼容模式"
,"qx6X:"+ //no args
"The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
//@@zh="环境检测超时"
,"cdOx:"+ //no args
"Environment detection timeout"
//@@zh="可能无法播放:{1}"
,"S2Bu:"+ //args: {1}
"Could not play: {1}"
//@@zh="input调用失败非pcm[Int16,...]输入时必须解码或者使用transform转换"
,"ZfGG:"+ //no args
"input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
//@@zh="input调用失败未提供sampleRate"
,"N4ke:"+ //no args
"input call failed: sampleRate not provided"
//@@zh="input调用失败data的sampleRate={1}和之前的={2}不同"
,"IHZd:"+ //args: {1}-{2}
"input call failed: sampleRate={1} of data is different from previous={2}"
//@@zh="延迟过大,已丢弃{1}ms {2}"
,"L8sC:"+ //args: {1}-{2}
"The delay is too large, {1}ms has been discarded, {2}"
//@@zh="{1}未调用start方法"
,"TZPq:"+ //args: {1}
"{1} did not call the start method"
//@@zh="浏览器不支持音频解码"
,"iCFC:"+ //no args
"Browser does not support audio decoding"
//@@zh="音频解码数据必须是ArrayBuffer"
,"wE2k:"+ //no args
"Audio decoding data must be ArrayBuffer"
//@@zh="音频解码失败:{1}"
,"mOaT:"+ //args: {1}
"Audio decoding failed: {1}"
]);
//*************** End srcFile=extensions/buffer_stream.player.js ***************
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="符号[{1}]无效:{2}"
//@@Put0
"3RBa:"+ //args: {1}-{2}
"Invalid symbol [{1}]: {2}"
//@@zh="音符[{1}]无效:{2}"
,"U212:"+ //args: {1}-{2}
"Invalid note [{1}]: {2}"
//@@zh="多个音时必须对齐,相差{1}ms"
,"7qAD:"+ //args: {1}
"Multiple tones must be aligned, with a difference of {1}ms"
//@@zh="祝你生日快乐"
,"QGsW:"+ //no args
"Happy Birthday to You"
//@@zh="致爱丽丝"
,"emJR:"+ //no args
"For Elise"
//@@zh="卡农-右手简谱"
,"GsYy:"+ //no args
"Canon - Right Hand Notation"
//@@zh="卡农"
,"bSFZ:"+ //no args
"Canon"
]);
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
//*************** Begin srcFile=extensions/sonic.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="当前环境不支持Web Worker不支持调用Sonic.Async"
//@@Put0
"Ikdz:"+ //no args
"The current environment does not support Web Worker and does not support calling Sonic.Async"
//@@zh="sonic worker剩{1}个未flush"
,"IC5Y:"+ //args: {1}
"There are {1} unflushed sonic workers left"
]);
//*************** End srcFile=extensions/sonic.js ***************
//*************** Begin srcFile=app-support/app-native-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
//@@Put0
"WWoj:"+ //args: {1}-{3}
"The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
//@@zh="未开始录音但收到Native PCM数据"
,"rCAM:"+ //no args
"Recording does not start, but Native PCM data is received"
//@@zh="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听postMessage转发兼容传输数据请自行实现将top层接收到数据转发到本iframe不限层不然无法接收到录音数据"
,"t2OF:"+ //no args
"A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
//@@zh="未开始录音"
,"Z2y2:"+ //no args
"Recording not started"
]);
//*************** End srcFile=app-support/app-native-support.js ***************
//*************** Begin srcFile=app-support/app.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@Put0
"uXtA:"+ //args: {1}
"Duplicate import {1}"
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
,"kIBu:"+ //args: {1}
"Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
//@@zh="重复注册{1}"
,"ha2K:"+ //args: {1}
"Duplicate registration {1}"
//@@zh="仅清理资源"
,"wpTL:"+ //no args
"Clean resources only"
//@@zh="未开始录音"
,"bpvP:"+ //no args
"Recording not started"
//@@zh="当前环境不支持实时回调,无法进行{1}"
,"fLJD:"+ //args: {1}
"The current environment does not support real-time callback and cannot be performed {1}"
//@@zh="录音权限请求失败:"
,"YnzX:"+ //no args
"Recording permission request failed: "
//@@zh="需先调用{1}"
,"nwKR:"+ //args: {1}
"Need to call {1} first"
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
,"citA:"+ //args: {1}-{2}
"This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
//@@zh="开始录音失败:"
,"ecp9:"+ //no args
"Failed to start recording: "
//@@zh="不能录音:"
,"EKmS:"+ //no args
"Cannot record: "
//@@zh="已开始录音"
,"k7Qo:"+ //no args
"Recording started"
//@@zh="结束录音失败:"
,"Douz:"+ //no args
"Failed to stop recording: "
//@@zh="和Start时差{1}ms"
,"wqSH:"+ //args: {1}
"Time difference from Start: {1}ms"
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
,"g3VX:"+ //args: {1}-{4}
"Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
]);
//*************** End srcFile=app-support/app.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

935
node_modules/recorder-core/src/i18n/es.js generated vendored Normal file
View File

@ -0,0 +1,935 @@
/*
Recorder i18n/es.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="es"
Desc: Spanish, Español, 西班牙语Esta traducción proviene principalmente de: traducción de google + traducción de Baidu, traducida del chino al español. 此翻译主要来自google翻译+百度翻译由中文翻译成西班牙语
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="es";
Recorder.CLog('Import Recorder i18n lang="es"');
//i18n.alias["other-lang-key"]="es";
var putSet={lang:"es"};
i18n.data["rtl$es"]=false;
i18n.data["desc$es"]="Spanish, Español, 西班牙语。Esta traducción proviene principalmente de: traducción de google + traducción de Baidu, traducida del chino al español. 此翻译主要来自google翻译+百度翻译,由中文翻译成西班牙语。";
//@@Exec i18n.GenerateDisplayEnglish=true;
//*************** Begin srcFile=recorder-core.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"K8zP:"+ //args: {1}
"Importación duplicada {1}"
//@@zh="剩{1}个GetContext未close"
//@@en="There are {1} GetContext unclosed"
,"mSxV:"+ //args: {1}
"Los {1} GetContext restantes no han sido close"
//@@zh="注意ctx不是running状态rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用否则将在rec.start时尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
,"nMIy:"+ //no args
" (Nota: ctx no está en estado running. Se debe llamar al menos a uno de rec.open y start durante la operación del usuario (tocar, hacer clic, etc.); de lo contrario, se intentará ctx.resume durante rec.start, lo que puede causar compatibilidad problemas (solo iOS), consulte la configuración de runningContext en la documentación) "
//@@zh="Stream的采样率{1}不等于{2}将进行采样率转换注意音质不会变好甚至可能变差主要在移动端未禁用回声消除时会产生此现象浏览器有回声消除时可能只会返回16k采样率的音频数据"
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
,"eS8i:"+ //args: {1}-{2}
"El sampleRate {1} de la transmisión no es igual a {2} y se realizará la conversión de sampleRate (nota: la calidad del sonido no mejorará o incluso puede empeorar). Este fenómeno ocurre principalmente cuando echoCancellation no está desactivado en el terminal móvil. Cuando el navegador tiene echoCancellation, es posible que solo se devuelvan datos de audio con una frecuencia de muestreo de 16k. "
//@@zh="。由于{1}内部1秒375次回调在移动端可能会有性能问题导致回调丢失录音变短PC端无影响暂不建议开启{1}。"
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
,"ZGlf:"+ //args: {1}
". Debido a las 375 devoluciones de llamadas por segundo dentro de {1}, puede haber problemas de rendimiento en el lado móvil que pueden provocar que se pierdan las devoluciones de llamadas y que la grabación sea más corta. No hay ningún impacto en el lado de la PC. No se recomienda habilitar {1} por el momento."
//@@zh="Connect采用老的{1}"
//@@en="Connect uses the old {1}, "
,"7TU0:"+ //args: {1}
"Connect utiliza el antiguo {1}, "
//@@zh="但已设置{1}尝试启用{2}"
//@@en="But {1} is set trying to enable {2}"
,"JwCL:"+ //args: {1}-{2}
"Pero {1} está configurado para intentar habilitar {2}"
//@@zh="可设置{1}尝试启用{2}"
//@@en="Can set {1} try to enable {2}"
,"VGjB:"+ //args: {1}-{2}
"Puedes configurar {1} para intentar habilitar {2}"
//@@zh="{1}未返回任何音频,恢复使用{2}"
//@@en="{1} did not return any audio, reverting to {2}"
,"MxX1:"+ //args: {1}-{2}
"{1} no devolvió ningún audio, continúe usando {2}"
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"XUap:"+ //args: {1}
"{1} devolución de llamada redundante"
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
,"yOta:"+ //args: {1}-{3}
"Connect usa {1}, configurar {2} puede restaurar el antiguo {3}"
//@@zh="(此浏览器不支持{1}"
//@@en=" (This browser does not support {1}) "
,"VwPd:"+ //args: {1}
" (Este navegador no soporta {1}) "
//@@zh="{1}未返回任何音频,降级使用{2}"
//@@en="{1} did not return any audio, downgrade to {2}"
,"vHnb:"+ //args: {1}-{2}
"{1} no devuelve audio, se ha degradado para usar {2}"
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"O9P7:"+ //args: {1}
"{1} devolución de llamada redundante"
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
,"LMEm:"+ //args: {1}-{4}
"Connect usa {1}, configure {2} para volver a {3} o al antiguo {4}"
//@@zh="{1}的filter采样率变了重设滤波"
//@@en="The filter sampleRate of {1} has changed, reset the filter"
,"d48C:"+ //args: {1}
"La frecuencia de muestreo del filtro de {1} ha cambiado y el filtro se ha restablecido"
//@@zh="{1}似乎传入了未重置chunk {2}"
//@@en="{1} seems to have passed in an unreset chunk {2}"
,"tlbC:"+ //args: {1}-{2}
"{1} parece haber introducido chunk {2} sin restablecer"
//@@zh="{1}和{2}必须是数值"
//@@en="{1} and {2} must be number"
,"VtS4:"+ //args: {1}-{2}
"{1} y {2} deben ser valores numéricos"
//@@zh="录音open失败"
//@@en="Recording open failed: "
,"5tWi:"+ //no args
"Error al grabar open: "
//@@zh="open被取消"
//@@en="open cancelled"
,"dFm8:"+ //no args
"open fue cancelado"
//@@zh="open被中断"
//@@en="open interrupted"
,"VtJO:"+ //no args
"open fue interrumpido"
//@@zh="可尝试使用RecordApp解决方案"
//@@en=", you can try to use the RecordApp solution "
,"EMJq:"+ //no args
", puedes probar la solución RecordApp"
//@@zh="不能录音:"
//@@en="Cannot record: "
,"A5bm:"+ //no args
"No se puede grabar: "
//@@zh="不支持此浏览器从流中获取录音"
//@@en="This browser does not support obtaining recordings from stream"
,"1iU7:"+ //no args
"Este navegador no admite la recuperación de grabaciones de transmisiones"
//@@zh="从流中打开录音失败:"
//@@en="Failed to open recording from stream: "
,"BTW2:"+ //no args
"No se pudo abrir la grabación desde la transmisión: "
//@@zh="无权录音(跨域请尝试给iframe添加麦克风访问策略如{1})"
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
,"Nclz:"+ //args: {1}
"Sin permiso para grabar (entre dominios, intente agregar una política de acceso al micrófono al iframe, como {1})"
//@@zh=",无可用麦克风"
//@@en=", no microphone available"
,"jBa9:"+ //no args
", no hay micrófono disponible"
//@@zh="用户拒绝了录音权限"
//@@en="User denied recording permission"
,"gyO5:"+ //no args
"Usuario denegado permiso de grabación"
//@@zh="浏览器禁止不安全页面录音可开启https解决"
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
,"oWNo:"+ //no args
"El navegador prohíbe el registro de páginas no seguras, lo que se puede solucionar activando https"
//@@zh="此浏览器不支持录音"
//@@en="This browser does not support recording"
,"COxc:"+ //no args
"Este navegador no admite la grabación"
//@@zh="发现同时多次调用open"
//@@en="It was found that open was called multiple times at the same time"
,"upb8:"+ //no args
"Descubrí que se llamó a open varias veces al mismo tiempo"
//@@zh="录音功能无效:无音频流"
//@@en="Invalid recording: no audio stream"
,"Q1GA:"+ //no args
"La función de grabación no funciona: no hay transmisión de audio"
//@@zh=",将尝试禁用回声消除后重试"
//@@en=", will try to disable echoCancellation and try again"
,"KxE2:"+ //no args
", intentaré deshabilitar echoCancellation y volveré a intentarlo"
//@@zh="请求录音权限错误"
//@@en="Error requesting recording permission"
,"xEQR:"+ //no args
"Error al solicitar permiso de grabación"
//@@zh="无法录音:"
//@@en="Unable to record: "
,"bDOG:"+ //no args
"No se puede grabar: "
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
,"IjL3:"+ //args: {1}
"Nota: Se ha configurado el parámetro {1}, lo que puede provocar que el navegador no seleccione correctamente el micrófono, o que el terminal móvil no habilite echoCancellation, etc. "
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除移动端未禁用回声消除时可能会降低系统播放音量关闭录音后可恢复和仅提供16k采样率的音频流不需要回声消除时可明确配置成禁用来获得48k高音质的流请参阅文档中{2}配置"
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
,"RiWe:"+ //args: {1}-{2}
", cuando no se configura {1}, el navegador puede habilitar automáticamente la cancelación de eco. Cuando la cancelación de eco no está deshabilitada en el terminal móvil, el volumen de reproducción del sistema puede reducirse (se puede restaurar después de cerrar la grabación) y solo se proporciona una transmisión de audio con una frecuencia de muestreo de 16 k (cuando no se requiere la cancelación de eco, se puede configurar explícitamente para que se deshabilite y se obtenga una transmisión de alta calidad de 48 k). Consulte la configuración {2} en el documento"
//@@zh="close被忽略因为同时open了多个rec只有最后一个会真正close"
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
,"hWVz:"+ //no args
"close se ignora (debido a que se abren varios recs al mismo tiempo, solo el último será realmente close)"
//@@zh="忽略"
//@@en="ignore"
,"UHvm:"+ //no args
"descuido"
//@@zh="不支持{1}架构"
//@@en="{1} architecture not supported"
,"Essp:"+ //args: {1}
"No es compatible con la arquitectura {1}"
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
//@@en="{1} type does not support setting takeoffEncodeChunk"
,"2XBl:"+ //args: {1}
"El tipo {1} no admite la configuración de takeoffEncodeChunk"
//@@zh="(未加载编码器)"
//@@en="(Encoder not loaded)"
,"LG7e:"+ //no args
"(sin codificador cargado)"
//@@zh="{1}环境不支持实时处理"
//@@en="{1} environment does not support real-time processing"
,"7uMV:"+ //args: {1}
"El entorno {1} no admite el procesamiento en tiempo real"
//@@zh="补偿{1}ms"
//@@en="Compensation {1}ms"
,"4Kfd:"+ //args: {1}
"Compensación {1}ms"
//@@zh="未补偿{1}ms"
//@@en="Uncompensated {1}ms"
,"bM5i:"+ //args: {1}
"{1} ms sin compensar"
//@@zh="回调出错是不允许的,需保证不会抛异常"
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
,"gFUF:"+ //no args
"No se permiten errores en las devoluciones de llamada y se debe garantizar que no se produzcan excepciones"
//@@zh="低性能,耗时{1}ms"
//@@en="Low performance, took {1}ms"
,"2ghS:"+ //args: {1}
"Bajo rendimiento, tarda {1} ms"
//@@zh="未进入异步前不能清除buffers"
//@@en="Buffers cannot be cleared before entering async"
,"ufqH:"+ //no args
"Los buffers no se pueden borrar antes de entrar asíncrono"
//@@zh="start失败未open"
//@@en="start failed: not open"
,"6WmN:"+ //no args
"start falló: no open"
//@@zh="start 开始录音,"
//@@en="start recording, "
,"kLDN:"+ //no args
"start, comenzar a grabar, "
//@@zh="start被中断"
//@@en="start was interrupted"
,"Bp2y:"+ //no args
"start fue interrumpido"
//@@zh=",可能无法录音:"
//@@en=", may fail to record: "
,"upkE:"+ //no args
", es posible que no sea posible grabar: "
//@@zh="stop 和start时差:"
//@@en="Stop and start time difference: "
,"Xq4s:"+ //no args
"stop, diferencia horaria con start: "
//@@zh="补偿:"
//@@en="compensate: "
,"3CQP:"+ //no args
"compensar: "
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"u8JG:"+ //no args
"No se pudo finalizar la grabación: "
//@@zh=",请设置{1}"
//@@en=", please set {1}"
,"1skY:"+ //args: {1}
", por favor establece {1}"
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
,"Wv7l:"+ //args: {1}-{3}
"Finalizar la grabación. La codificación tarda {1} ms. La duración del audio es de {2} ms. El tamaño del archivo es {3}b"
//@@zh="{1}编码器返回的不是{2}"
//@@en="{1} encoder returned not {2}"
,"Vkbd:"+ //args: {1}-{2}
"El codificador {1} no devuelve {2}"
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
,"QWnr:"+ //no args
"Después de habilitar takeoffEncodeChunk, la longitud del blob devuelta por stop es 0 y no se proporcionan datos de audio"
//@@zh="生成的{1}无效"
//@@en="Invalid generated {1}"
,"Sz2H:"+ //args: {1}
"El {1} generado no es válido"
//@@zh="未开始录音"
//@@en="Recording not started"
,"wf9t:"+ //no args
"Grabación no iniciada"
//@@zh="开始录音前无用户交互导致AudioContext未运行"
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
,"Dl2c:"+ //no args
", no hay interacción del usuario antes de comenzar a grabar, lo que hace que AudioContext no se ejecute"
//@@zh="未采集到录音"
//@@en="Recording not captured"
,"Ltz3:"+ //no args
"No se recopiló ninguna grabación"
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
,"xGuI:"+ //args: {1}-{2}
"El codificador de {1} no está cargado. Intente encontrar el codificador de {1} en src/engine de {2} y cárguelo"
//@@zh="录音错误:"
//@@en="Recording error: "
,"AxOH:"+ //no args
"Error de grabación: "
//@@zh="音频buffers被释放"
//@@en="Audio buffers are released"
,"xkKd:"+ //no args
"Se liberan los buffers de audio"
//@@zh="采样:{1} 花:{2}ms"
//@@en="Sampled: {1}, took: {2}ms"
,"CxeT:"+ //args: {1}-{2}
"Muestra: {1} Flor: {2}ms"
//@@zh="非浏览器环境,不支持{1}"
//@@en="Non-browser environment, does not support {1}"
,"NonBrowser-1:"+ //args: {1}
"Entorno sin navegador, no es compatible con {1}"
//@@zh="参数错误:{1}"
//@@en="Illegal argument: {1}"
,"IllegalArgs-1:"+ //args: {1}
"Error de parámetro: {1}"
//@@zh="调用{1}需要先导入{2}"
//@@en="Calling {1} needs to import {2} first"
,"NeedImport-2:"+ //args: {1}-{2}
"Para llamar a {1}, primero debes importar {2}"
//@@zh="不支持:{1}"
//@@en="Not support: {1}"
,"NotSupport-1:"+ //args: {1}
"No compatible: {1}"
//@@zh="覆盖导入{1}"
//@@en="Override import {1}"
,"8HO5:"+ //args: {1}
"Anular importación {1}"
]);
//*************** End srcFile=recorder-core.js ***************
//*************** Begin srcFile=engine/beta-amr.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="AMR-NB(NarrowBand)采样率设置无效只提供8000hz比特率范围{1}默认12.2kbps一帧20ms、{2}字节浏览器一般不支持播放amr格式可用Recorder.amr2wav()转码成wav播放"
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
//@@Put0
"b2mN:"+ //args: {1}-{2}
"AMR-NB (NarrowBand), la configuración sampleRate no es válida (solo se proporcionan 8000 hz), rango bitRate: {1} (predeterminado 12.2 kbps), un cuadro de 20 ms, {2} bytes; los navegadores generalmente no admiten la reproducción en formato amr, disponible Recorder.amr2wav() Transcodifica a wav para reproducción"
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
,"tQBv:"+ //args: {1}-{2}
"AMR Info: no coincide con el conjunto {1}, se ha actualizado a {2}"
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"q12D:"+ //args: {1}
"Los datos sampleRate están por debajo de {1}"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"TxjV:"+ //no args
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据只有首次回调数据首帧包含AMR头在合并成AMR文件时如果没有把首帧数据包含进去则必须在文件开头添加上AMR头Recorder.AMR.AMR_HEADER转成二进制否则无法播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
,"Q7p7:"+ //no args
"takeoffEncodeChunk se hace cargo de la salida de datos binarios del codificador AMR. Solo los primeros datos de devolución de llamada (primer cuadro) contienen el encabezado AMR; al fusionarlos en un archivo AMR, si los datos del primer cuadro no están incluidos, el encabezado AMR debe agregarse en el comienzo del archivo: Recorder.AMR.AMR_HEADER (convertido a binario), de lo contrario no se puede reproducir"
//@@zh="当前环境不支持Web Workeramr实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
,"6o9Z:"+ //no args
"El entorno actual no es compatible con Web Worker y el codificador en tiempo real amr se ejecuta en el hilo principal"
//@@zh="amr worker剩{1}个未stop"
//@@en="amr worker left {1} unstopped"
,"yYWs:"+ //args: {1}
"a amr worker le quedan {1} no stop"
//@@zh="amr编码器未start"
//@@en="amr encoder not started"
,"jOi8:"+ //no args
"codificador amr no start"
]);
//*************** End srcFile=engine/beta-amr.js ***************
//*************** Begin srcFile=engine/beta-ogg.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="Ogg Vorbis比特率取值16-100kbps采样率取值无限制"
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
//@@Put0
"O8Gn:"+ //no args
"Ogg Vorbis, el valor de bitRate es de 16-100 kbps, el valor de sampleRate es ilimitado"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"5si6:"+ //no args
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据Ogg由数据页组成一页包含多帧音频数据含几秒的音频一页数据无法单独解码和播放此编码器每次输出都是完整的一页数据因此实时性会比较低在合并成完整ogg文件时必须将输出的所有数据合并到一起否则可能无法播放不支持截取中间一部分单独解码和播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
,"R8yz:"+ //no args
"takeoffEncodeChunk se hace cargo de la salida de datos binarios del codificador OggVorbis. Ogg se compone de páginas de datos. Una página contiene múltiples fotogramas de datos de audio (incluidos varios segundos de audio. Una página de datos no se puede decodificar ni reproducir por separado). Cada salida de este codificador está completo. Una página de datos, por lo que el rendimiento en tiempo real será relativamente bajo; al fusionar en un archivo ogg completo, todos los datos de salida deben fusionarse; de lo contrario, es posible que no se reproduzca y no se admita intercepte la parte media y decodifíquela y reprodúzcala por separado"
//@@zh="当前环境不支持Web WorkerOggVorbis实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
,"hB9D:"+ //no args
"El entorno actual no admite Web Workers y el codificador en tiempo real OggVorbis se ejecuta en el hilo principal"
//@@zh="ogg worker剩{1}个未stop"
//@@en="There are {1} unstopped ogg workers"
,"oTiy:"+ //args: {1}
"a ogg worker le quedan {1} no stop"
//@@zh="ogg编码器未start"
//@@en="ogg encoder not started"
,"dIpw:"+ //no args
"codificador ogg no start"
]);
//*************** End srcFile=engine/beta-ogg.js ***************
//*************** Begin srcFile=engine/beta-webm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="此浏览器不支持进行webm编码未实现MediaRecorder"
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
//@@Put0
"L49q:"+ //no args
"Este navegador no admite la codificación webm y MediaRecorder no está implementado"
//@@zh="只有比较新的浏览器支持压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法只能按照类似边播放边收听形式把数据导入到MediaRecorder有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小但音频文件中的比特率并非设定比特率采样率由于是我们自己采样的到这个编码器随他怎么搞"
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
,"tsTW:"+ //no args
"Sólo los navegadores más nuevos lo admiten y la tasa de compresión es similar a la de mp3. Dado que no hay forma de codificar rápidamente los datos pcm existentes, los datos sólo se pueden importar a MediaRecorder de forma similar a la reproducción y escucha, y hay que esperar unos segundos. Aunque el tamaño del archivo de audio de salida se puede controlar mediante la velocidad de bits, la velocidad de bits en el archivo de audio no es la velocidad de bits establecida. Dado que la frecuencia de muestreo la probamos nosotros mismos, podemos hacer lo que queramos con este codificador"
//@@zh="此浏览器不支持把录音转成webm格式"
//@@en="This browser does not support converting recordings to webm format"
,"aG4z:"+ //no args
"Este navegador no admite la conversión de grabaciones al formato webm"
//@@zh="转码webm出错{1}"
//@@en="Error encoding webm: {1}"
,"PIX0:"+ //args: {1}
"Error al transcodificar webm: {1}"
]);
//*************** End srcFile=engine/beta-webm.js ***************
//*************** Begin srcFile=engine/g711x.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}{2}音频文件无法直接播放可用Recorder.{2}2wav()转码成wav播放采样率比特率设置无效固定为8000hz采样率、16位每个采样压缩成8位存储音频文件大小为8000字节/秒如需任意采样率支持请使用Recorder.{2}_encode()方法"
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
//@@Put0
"d8YX:"+ //args: {1}-{2}
"{1}; {2} El archivo de audio no se puede reproducir directamente. Puede utilizar Recorder.{2}2wav() para transcodificarlo a wav para su reproducción; la configuración de velocidad de bits de frecuencia de muestreo no es válida y está fijada en 8000 hz de muestreo velocidad, 16 bits, y cada muestra está comprimida. en un almacenamiento de 8 bits, el tamaño del archivo de audio es 8000 bytes/segundo; si necesita compatibilidad con cualquier frecuencia de muestreo, utilice el método Recorder.{2}_encode()"
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"29UK:"+ //args: {1}
"Los datos sampleRate están por debajo de {1}"
//@@zh="{1}编码器未start"
//@@en="{1} encoder not started"
,"quVJ:"+ //args: {1}
"codificador {1} no start"
]);
//*************** End srcFile=engine/g711x.js ***************
//*************** Begin srcFile=engine/mp3.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="采样率范围:{1};比特率范围:{2}不同比特率支持的采样率范围不同小于32kbps时采样率需小于32000"
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
//@@Put0
"Zm7L:"+ //args: {1}-{2}
"rango sampleRate: {1}; rango bitRate: {2} (diferentes bitRate admiten diferentes rangos sampleRate. Cuando es inferior a 32 kbps, sampleRate debe ser inferior a 32000)"
//@@zh="{1}不在mp3支持的取值范围{2}"
//@@en="{1} is not in the value range supported by mp3: {2}"
,"eGB9:"+ //args: {1}-{2}
"{1} no está en el rango de valores soportado por mp3: {2}"
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围{3}"
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
,"zLTa:"+ //args: {1}-{3}
"sampleRate se ha actualizado a {1} porque {2} no está en el rango de valores admitido por mp3: {3}"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"yhUs:"+ //no args
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
//@@zh="当前环境不支持Web Workermp3实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
,"k9PT:"+ //no args
"El entorno actual no es compatible con Web Worker y el codificador en tiempo real mp3 se ejecuta en el hilo principal"
//@@zh="mp3 worker剩{1}个未stop"
//@@en="There are {1} unstopped mp3 workers left"
,"fT6M:"+ //args: {1}
"a mp3 worker le quedan {1} no stop"
//@@zh="mp3编码器未start"
//@@en="mp3 encoder not started"
,"mPxH:"+ //no args
"codificador mp3 no start"
//@@zh="和设置的不匹配{1},已更新成{2}"
//@@en="Does not match the set {1}, has been updated to {2}"
,"uY9i:"+ //args: {1}-{2}
"No coincide con la configuración {1}, se ha actualizado a {2}"
//@@zh="Fix移除{1}帧"
//@@en="Fix remove {1} frame"
,"iMSm:"+ //args: {1}
"Fix elimina {1} fotogramas"
//@@zh="移除帧数过多"
//@@en="Remove too many frames"
,"b9zm:"+ //no args
"Eliminar demasiados fotogramas"
]);
//*************** End srcFile=engine/mp3.js ***************
//*************** Begin srcFile=engine/pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="pcm为未封装的原始音频数据pcm音频文件无法直接播放可用Recorder.pcm2wav()转码成wav播放支持位数8位、16位填在比特率里面采样率取值无限制"
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
//@@Put0
"fWsN:"+ //no args
"pcm son datos de audio originales no encapsulados. Los archivos de audio Pcm no se pueden reproducir directamente. Recorder.pcm2wav() se puede utilizar para transcodificar a wav para su reproducción. Admite dígitos de 8 y 16 bits (rellene bitRate) y el valor de sampleRate es ilimitado"
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
,"uMUJ:"+ //args: {1}-{2}
"PCM Info: El bit {1} no es compatible y se ha actualizado al bit {2}"
//@@zh="pcm2wav必须提供sampleRate和bitRate"
//@@en="pcm2wav must provide sampleRate and bitRate"
,"KmRz:"+ //no args
"pcm2wav debe proporcionar sampleRate y bitRate"
//@@zh="pcm编码器未start"
//@@en="pcm encoder not started"
,"sDkA:"+ //no args
"codificador pcm no start"
]);
//*************** End srcFile=engine/pcm.js ***************
//*************** Begin srcFile=engine/wav.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="支持位数8位、16位填在比特率里面采样率取值无限制此编码器仅在pcm数据前加了一个44字节的wav头编码出来的16位wav文件去掉开头的44字节即可得到pcm其他wav编码器可能不是44字节"
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
//@@Put0
"gPSE:"+ //no args
"Admite dígitos de 8 y 16 bits (completados en bitRate) y el valor de sampleRate es ilimitado; este codificador solo agrega un encabezado wav de 44 bytes antes de los datos pcm, y el archivo wav codificado de 16 bits elimina los 44 bits iniciales. Bytes para obtener pcm (nota: es posible que otros codificadores WAV no tengan 44 bytes)"
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
,"wyw9:"+ //args: {1}-{2}
"WAV Info: El bit {1} no es compatible y se ha actualizado al bit {2}"
]);
//*************** End srcFile=engine/wav.js ***************
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="getAudioSrc方法已过时请直接使用getMediaStream然后赋值给audio.srcObject仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
//@@Put0
"0XYC:"+ //no args
"El método getAudioSrc está obsoleto: utilice getMediaStream directamente y asígnelo a audio.srcObject. Solo se permite llamar a este método en navegadores que no admiten srcObject y asignarlo a audio.src por compatibilidad"
//@@zh="start被stop终止"
//@@en="start is terminated by stop"
,"6DDt:"+ //no args
"start es cancelado por stop"
//@@zh="{1}多次start"
//@@en="{1} repeat start"
,"I4h4:"+ //args: {1}
"{1} se repite start"
//@@zh="浏览器不支持打开{1}"
//@@en="The browser does not support opening {1}"
,"P6Gs:"+ //args: {1}
"El navegador no admite la apertura de {1}"
//@@zh="注意ctx不是running状态start需要在用户操作(触摸、点击等)时进行调用否则会尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
,"JwDm:"+ //no args
" (Nota: ctx no está en estado running. Es necesario llamar a start cuando el usuario opera (tocar, hacer clic, etc.); de lo contrario, se intentará ctx.resume, lo que puede causar problemas de compatibilidad (solo iOS). Consulte la configuración runningContext en el documento) "
//@@zh="此浏览器的AudioBuffer实现不支持动态特性采用兼容模式"
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
,"qx6X:"+ //no args
"La implementación AudioBuffer de este navegador no admite funciones dinámicas y utiliza el modo de compatibilidad"
//@@zh="环境检测超时"
//@@en="Environment detection timeout"
,"cdOx:"+ //no args
"Tiempo de espera de detección del entorno"
//@@zh="可能无法播放:{1}"
//@@en="Could not play: {1}"
,"S2Bu:"+ //args: {1}
"No puede jugar: {1}"
//@@zh="input调用失败非pcm[Int16,...]输入时必须解码或者使用transform转换"
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
,"ZfGG:"+ //no args
"Falló la llamada input: no PCM [int16,...] al ingresar, se debe decodificar o usar la conversión transform"
//@@zh="input调用失败未提供sampleRate"
//@@en="input call failed: sampleRate not provided"
,"N4ke:"+ //no args
"Falló la llamada input: no se proporcionó sampleRate"
//@@zh="input调用失败data的sampleRate={1}和之前的={2}不同"
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
,"IHZd:"+ //args: {1}-{2}
"Falló la llamada a input: sampleRate={1} de los datos es diferente de la anterior ={2}"
//@@zh="延迟过大,已丢弃{1}ms {2}"
//@@en="The delay is too large, {1}ms has been discarded, {2}"
,"L8sC:"+ //args: {1}-{2}
"El retraso es demasiado grande, se han descartado {1}ms, {2}"
//@@zh="{1}未调用start方法"
//@@en="{1} did not call the start method"
,"TZPq:"+ //args: {1}
"{1} no se llama al método start"
//@@zh="浏览器不支持音频解码"
//@@en="Browser does not support audio decoding"
,"iCFC:"+ //no args
"El navegador no admite decodificación de audio"
//@@zh="音频解码数据必须是ArrayBuffer"
//@@en="Audio decoding data must be ArrayBuffer"
,"wE2k:"+ //no args
"Los datos de decodificación de audio deben ser ArrayBuffer"
//@@zh="音频解码失败:{1}"
//@@en="Audio decoding failed: {1}"
,"mOaT:"+ //args: {1}
"Falló la decodificación de audio: {1}"
]);
//*************** End srcFile=extensions/buffer_stream.player.js ***************
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="符号[{1}]无效:{2}"
//@@en="Invalid symbol [{1}]: {2}"
//@@Put0
"3RBa:"+ //args: {1}-{2}
"Símbolo [{1}] no válido: {2}"
//@@zh="音符[{1}]无效:{2}"
//@@en="Invalid note [{1}]: {2}"
,"U212:"+ //args: {1}-{2}
"Nota [{1}] no válido: {2}"
//@@zh="多个音时必须对齐,相差{1}ms"
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
,"7qAD:"+ //args: {1}
"Hay que alinearse a la hora de múltiples sonidos, con diferencias {1}ms"
//@@zh="祝你生日快乐"
//@@en="Happy Birthday to You"
,"QGsW:"+ //no args
"Happy Birthday to You"
//@@zh="致爱丽丝"
//@@en="For Elise"
,"emJR:"+ //no args
"For Elise"
//@@zh="卡农-右手简谱"
//@@en="Canon - Right Hand Notation"
,"GsYy:"+ //no args
"Canon - símbolo de la mano derecha"
//@@zh="卡农"
//@@en="Canon"
,"bSFZ:"+ //no args
"Canon"
]);
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
//*************** Begin srcFile=extensions/sonic.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="当前环境不支持Web Worker不支持调用Sonic.Async"
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
//@@Put0
"Ikdz:"+ //no args
"El entorno actual no admite Web Worker, no admite llamadas a Sonic.Async"
//@@zh="sonic worker剩{1}个未flush"
//@@en="There are {1} unflushed sonic workers left"
,"IC5Y:"+ //args: {1}
"sonic worker deja {1} sin flush"
]);
//*************** End srcFile=extensions/sonic.js ***************
//*************** Begin srcFile=app-support/app-native-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
//@@Put0
"WWoj:"+ //args: {1}-{3}
"El método {2} en {1} no se implementa, por favor implemente este método en el archivo {3} o en el archivo de configuración"
//@@zh="未开始录音但收到Native PCM数据"
//@@en="Recording does not start, but Native PCM data is received"
,"rCAM:"+ //no args
"No se inició la grabación, pero se recibieron los datos de Native PCM"
//@@zh="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听postMessage转发兼容传输数据请自行实现将top层接收到数据转发到本iframe不限层不然无法接收到录音数据"
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
,"t2OF:"+ //no args
"Detectado iframe entre dominios, NativeRecordReceivePCM no se puede inyectar en el nivel superior, se han monitoreado los datos de transmisión compatibles con el reenvío de postMessage, por favor, realice por sí mismo el reenvío de los datos recibidos en la capa superior a este iframe (capa ilimitada), de lo contrario no se pueden recibir los datos de grabación"
//@@zh="未开始录音"
//@@en="Recording not started"
,"Z2y2:"+ //no args
"Grabación no iniciada"
]);
//*************** End srcFile=app-support/app-native-support.js ***************
//*************** Begin srcFile=app-support/app.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"uXtA:"+ //args: {1}
"Importación duplicada {1}"
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
,"kIBu:"+ //args: {1}
"Nota: Debido a que se llaman simultáneamente otros métodos relacionados con la grabación, el resultado de la llamada actual de {1} se ha descartado y no habrá devolución de llamada"
//@@zh="重复注册{1}"
//@@en="Duplicate registration {1}"
,"ha2K:"+ //args: {1}
"Doble registro {1}"
//@@zh="仅清理资源"
//@@en="Clean resources only"
,"wpTL:"+ //no args
"Solo limpiar recursos"
//@@zh="未开始录音"
//@@en="Recording not started"
,"bpvP:"+ //no args
"Grabación no iniciada"
//@@zh="当前环境不支持实时回调,无法进行{1}"
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
,"fLJD:"+ //args: {1}
"El entorno actual no admite devoluciones de llamada en tiempo real y no se puede realizar {1}"
//@@zh="录音权限请求失败:"
//@@en="Recording permission request failed: "
,"YnzX:"+ //no args
"La solicitud de permiso de grabación falló: "
//@@zh="需先调用{1}"
//@@en="Need to call {1} first"
,"nwKR:"+ //args: {1}
"Primero hay que llamar a {1}"
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
,"citA:"+ //args: {1}-{2}
"Actualmente no es un entorno de navegador, es necesario introducir un archivo de soporte para esta plataforma ({1}), o llamar al {2} para lograr su propio acceso."
//@@zh="开始录音失败:"
//@@en="Failed to start recording: "
,"ecp9:"+ //no args
"Falló al iniciar la grabación: "
//@@zh="不能录音:"
//@@en="Cannot record: "
,"EKmS:"+ //no args
"No se puede grabar: "
//@@zh="已开始录音"
//@@en="Recording started"
,"k7Qo:"+ //no args
"Se ha iniciado la grabación"
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"Douz:"+ //no args
"Falló al finalizar la grabación: "
//@@zh="和Start时差{1}ms"
//@@en="Time difference from Start: {1}ms"
,"wqSH:"+ //args: {1}
"Diferencia horaria con start: {1}ms"
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
,"g3VX:"+ //args: {1}-{4}
"Terminar la grabación lleva tiempo {1}ms , duración del audio {2}ms , tamaño del archivo {3}b , {4}"
]);
//*************** End srcFile=app-support/app.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

935
node_modules/recorder-core/src/i18n/fr.js generated vendored Normal file
View File

@ -0,0 +1,935 @@
/*
Recorder i18n/fr.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="fr"
Desc: French, Français, 法语Cette traduction provient principalement de : traduction google + traduction Baidu, traduit du chinois vers le français. 此翻译主要来自google翻译+百度翻译由中文翻译成法语
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="fr";
Recorder.CLog('Import Recorder i18n lang="fr"');
//i18n.alias["other-lang-key"]="fr";
var putSet={lang:"fr"};
i18n.data["rtl$fr"]=false;
i18n.data["desc$fr"]="French, Français, 法语。Cette traduction provient principalement de : traduction google + traduction Baidu, traduit du chinois vers le français. 此翻译主要来自google翻译+百度翻译,由中文翻译成法语。";
//@@Exec i18n.GenerateDisplayEnglish=true;
//*************** Begin srcFile=recorder-core.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"K8zP:"+ //args: {1}
"Répéter l'importation {1}"
//@@zh="剩{1}个GetContext未close"
//@@en="There are {1} GetContext unclosed"
,"mSxV:"+ //args: {1}
"Les {1} GetContext restants n'ont pas été close"
//@@zh="注意ctx不是running状态rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用否则将在rec.start时尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
,"nMIy:"+ //no args
" (Remarque : ctx n'est pas dans l'état running. Au moins l'un des rec.open et start doit être appelé pendant l'opération de l'utilisateur (toucher, cliquer, etc.), sinon ctx.resume sera tenté pendant rec.start, ce qui peut entraîner une compatibilité. problèmes (iOS uniquement), voir la configuration de runningContext dans la documentation) "
//@@zh="Stream的采样率{1}不等于{2}将进行采样率转换注意音质不会变好甚至可能变差主要在移动端未禁用回声消除时会产生此现象浏览器有回声消除时可能只会返回16k采样率的音频数据"
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
,"eS8i:"+ //args: {1}-{2}
"Si le sampleRate {1} du Stream n'est pas égal à {2}, la conversion sampleRate sera effectuée (attention : la qualité du son ne s'améliorera pas ou pourra même se dégrader. Ce phénomène se produit principalement lorsque echoCancellation n'est pas désactivé sur le mobile). terminal Lorsque le navigateur a echoCancellation, seules les données audio avec un taux d'échantillonnage de 16k seront renvoyées. "
//@@zh="。由于{1}内部1秒375次回调在移动端可能会有性能问题导致回调丢失录音变短PC端无影响暂不建议开启{1}。"
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
,"ZGlf:"+ //args: {1}
". En raison des 375 rappels par seconde dans un délai de {1}, il peut y avoir des problèmes de performances du côté mobile qui peuvent entraîner la perte des rappels et un raccourcissement de l'enregistrement. Il n'y a aucun impact du côté PC. Il n'est pas recommandé d'activer {1} pour le moment."
//@@zh="Connect采用老的{1}"
//@@en="Connect uses the old {1}, "
,"7TU0:"+ //args: {1}
"Connect utilise l'ancien {1}, "
//@@zh="但已设置{1}尝试启用{2}"
//@@en="But {1} is set trying to enable {2}"
,"JwCL:"+ //args: {1}-{2}
"Mais {1} est configuré pour essayer d'activer {2}"
//@@zh="可设置{1}尝试启用{2}"
//@@en="Can set {1} try to enable {2}"
,"VGjB:"+ //args: {1}-{2}
"Vous pouvez configurer {1} pour essayer d'activer {2}"
//@@zh="{1}未返回任何音频,恢复使用{2}"
//@@en="{1} did not return any audio, reverting to {2}"
,"MxX1:"+ //args: {1}-{2}
"{1} n'a renvoyé aucun son et a repris l'utilisation de {2}"
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"XUap:"+ //args: {1}
"{1} rappel redondant"
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
,"yOta:"+ //args: {1}-{3}
"Connect utilise {1} et la configuration de {2} peut restaurer l'ancien {3}"
//@@zh="(此浏览器不支持{1}"
//@@en=" (This browser does not support {1}) "
,"VwPd:"+ //args: {1}
" (Ce navigateur ne prend pas en charge {1}) "
//@@zh="{1}未返回任何音频,降级使用{2}"
//@@en="{1} did not return any audio, downgrade to {2}"
,"vHnb:"+ //args: {1}-{2}
"{1} ne renvoie aucun audio, rétrogradez pour utiliser {2}"
//@@zh="{1}多余回调"
//@@en="{1} redundant callback"
,"O9P7:"+ //args: {1}
"{1} rappel redondant"
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
,"LMEm:"+ //args: {1}-{4}
"Connect utilise {1}, la configuration de {2} peut restaurer l'utilisation de {3} ou de l'ancien {4}"
//@@zh="{1}的filter采样率变了重设滤波"
//@@en="The filter sampleRate of {1} has changed, reset the filter"
,"d48C:"+ //args: {1}
"Le taux d'échantillonnage du filtre de {1} a changé, réinitialisez le filtre"
//@@zh="{1}似乎传入了未重置chunk {2}"
//@@en="{1} seems to have passed in an unreset chunk {2}"
,"tlbC:"+ //args: {1}-{2}
"{1} semble passer dans chunk {2} qui n'est pas réinitialisé"
//@@zh="{1}和{2}必须是数值"
//@@en="{1} and {2} must be number"
,"VtS4:"+ //args: {1}-{2}
"{1} et {2} doivent être des valeurs numériques"
//@@zh="录音open失败"
//@@en="Recording open failed: "
,"5tWi:"+ //no args
"L'enregistrement de open a échoué : "
//@@zh="open被取消"
//@@en="open cancelled"
,"dFm8:"+ //no args
"open a été annulé"
//@@zh="open被中断"
//@@en="open interrupted"
,"VtJO:"+ //no args
"open a été interrompu"
//@@zh="可尝试使用RecordApp解决方案"
//@@en=", you can try to use the RecordApp solution "
,"EMJq:"+ //no args
", vous pouvez essayer la solution RecordApp"
//@@zh="不能录音:"
//@@en="Cannot record: "
,"A5bm:"+ //no args
"Impossible d'enregistrer : "
//@@zh="不支持此浏览器从流中获取录音"
//@@en="This browser does not support obtaining recordings from stream"
,"1iU7:"+ //no args
"Ce navigateur ne prend pas en charge la récupération d'enregistrements à partir de flux"
//@@zh="从流中打开录音失败:"
//@@en="Failed to open recording from stream: "
,"BTW2:"+ //no args
"Échec de l'ouverture de l'enregistrement à partir du flux : "
//@@zh="无权录音(跨域请尝试给iframe添加麦克风访问策略如{1})"
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
,"Nclz:"+ //args: {1}
"Aucune autorisation d'enregistrement (inter-domaine, veuillez essayer d'ajouter une stratégie d'accès au microphone à l'iframe, telle que {1})"
//@@zh=",无可用麦克风"
//@@en=", no microphone available"
,"jBa9:"+ //no args
", pas de micro disponible"
//@@zh="用户拒绝了录音权限"
//@@en="User denied recording permission"
,"gyO5:"+ //no args
"L'utilisateur a refusé l'autorisation d'enregistrement"
//@@zh="浏览器禁止不安全页面录音可开启https解决"
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
,"oWNo:"+ //no args
"Le navigateur interdit l'enregistrement des pages dangereuses, ce qui peut être résolu en activant https"
//@@zh="此浏览器不支持录音"
//@@en="This browser does not support recording"
,"COxc:"+ //no args
"Ce navigateur ne prend pas en charge l'enregistrement"
//@@zh="发现同时多次调用open"
//@@en="It was found that open was called multiple times at the same time"
,"upb8:"+ //no args
"J'ai constaté que open était appelé plusieurs fois en même temps"
//@@zh="录音功能无效:无音频流"
//@@en="Invalid recording: no audio stream"
,"Q1GA:"+ //no args
"La fonction d'enregistrement ne fonctionne pas : pas de flux audio"
//@@zh=",将尝试禁用回声消除后重试"
//@@en=", will try to disable echoCancellation and try again"
,"KxE2:"+ //no args
", je vais essayer de désactiver echoCancellation et réessayer"
//@@zh="请求录音权限错误"
//@@en="Error requesting recording permission"
,"xEQR:"+ //no args
"Erreur lors de la demande d'autorisation d'enregistrement"
//@@zh="无法录音:"
//@@en="Unable to record: "
,"bDOG:"+ //no args
"Impossible d'enregistrer : "
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
,"IjL3:"+ //args: {1}
"Remarque : Le paramètre {1} a été configuré, ce qui peut empêcher le navigateur de sélectionner correctement le microphone ou le terminal mobile de ne pas activer echoCancellation, etc. "
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除移动端未禁用回声消除时可能会降低系统播放音量关闭录音后可恢复和仅提供16k采样率的音频流不需要回声消除时可明确配置成禁用来获得48k高音质的流请参阅文档中{2}配置"
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
,"RiWe:"+ //args: {1}-{2}
", lorsque {1} n'est pas configuré, le navigateur peut activer automatiquement echoCancellation. Lorsque echoCancellation n'est pas désactivé sur le terminal mobile, le volume de lecture du système peut être réduit (peut être restauré après la fermeture de l'enregistrement) et seul le flux audio à 16 k de fréquence d'échantillonnage est fourni (lorsque echoCancellation n'est pas requis, il peut être explicitement configuré pour être désactivé afin d'obtenir un flux de haute qualité à 48 k). Veuillez vous référer à la configuration {2} dans le document"
//@@zh="close被忽略因为同时open了多个rec只有最后一个会真正close"
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
,"hWVz:"+ //no args
"close est ignoré (car plusieurs recs sont ouverts en même temps, seul le dernier sera en fait close)"
//@@zh="忽略"
//@@en="ignore"
,"UHvm:"+ //no args
"négligence"
//@@zh="不支持{1}架构"
//@@en="{1} architecture not supported"
,"Essp:"+ //args: {1}
"Ne prend pas en charge l'architecture {1}"
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
//@@en="{1} type does not support setting takeoffEncodeChunk"
,"2XBl:"+ //args: {1}
"Le type {1} ne prend pas en charge le paramètre takeoffEncodeChunk"
//@@zh="(未加载编码器)"
//@@en="(Encoder not loaded)"
,"LG7e:"+ //no args
"(aucun encodeur chargé)"
//@@zh="{1}环境不支持实时处理"
//@@en="{1} environment does not support real-time processing"
,"7uMV:"+ //args: {1}
"L'environnement {1} ne prend pas en charge le traitement en temps réel"
//@@zh="补偿{1}ms"
//@@en="Compensation {1}ms"
,"4Kfd:"+ //args: {1}
"Compensation {1} ms"
//@@zh="未补偿{1}ms"
//@@en="Uncompensated {1}ms"
,"bM5i:"+ //args: {1}
"Non compensé {1} ms"
//@@zh="回调出错是不允许的,需保证不会抛异常"
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
,"gFUF:"+ //no args
"Les erreurs dans les rappels ne sont pas autorisées et doivent être assurées qu'aucune exception n'est levée"
//@@zh="低性能,耗时{1}ms"
//@@en="Low performance, took {1}ms"
,"2ghS:"+ //args: {1}
"Faible performance, prend {1} ms"
//@@zh="未进入异步前不能清除buffers"
//@@en="Buffers cannot be cleared before entering async"
,"ufqH:"+ //no args
"Les buffers ne peuvent pas être effacés avant d'entrer en mode asynchrone"
//@@zh="start失败未open"
//@@en="start failed: not open"
,"6WmN:"+ //no args
"Échec de start: pas open"
//@@zh="start 开始录音,"
//@@en="start recording, "
,"kLDN:"+ //no args
"start, démarre l'enregistrement, "
//@@zh="start被中断"
//@@en="start was interrupted"
,"Bp2y:"+ //no args
"start a été interrompu"
//@@zh=",可能无法录音:"
//@@en=", may fail to record: "
,"upkE:"+ //no args
", l'enregistrement peut ne pas être possible : "
//@@zh="stop 和start时差:"
//@@en="Stop and start time difference: "
,"Xq4s:"+ //no args
"stop, décalage horaire avec start : "
//@@zh="补偿:"
//@@en="compensate: "
,"3CQP:"+ //no args
"compenser: "
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"u8JG:"+ //no args
"Échec de la fin de l'enregistrement : "
//@@zh=",请设置{1}"
//@@en=", please set {1}"
,"1skY:"+ //args: {1}
", veuillez définir {1}"
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
,"Wv7l:"+ //args: {1}-{3}
"Terminer l'enregistrement. L'encodage prend {1} ms. La durée audio est de {2} ms. La taille du fichier est de {3}b"
//@@zh="{1}编码器返回的不是{2}"
//@@en="{1} encoder returned not {2}"
,"Vkbd:"+ //args: {1}-{2}
"L'encodeur {1} ne renvoie pas {2}"
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
,"QWnr:"+ //no args
"Après avoir activé takeoffEncodeChunk, la longueur du blob renvoyée par stop est 0 et aucune donnée audio n'est fournie"
//@@zh="生成的{1}无效"
//@@en="Invalid generated {1}"
,"Sz2H:"+ //args: {1}
"Le {1} généré n'est pas valide"
//@@zh="未开始录音"
//@@en="Recording not started"
,"wf9t:"+ //no args
"L'enregistrement n'a pas démarré"
//@@zh="开始录音前无用户交互导致AudioContext未运行"
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
,"Dl2c:"+ //no args
", il n'y a aucune interaction de l'utilisateur avant de démarrer l'enregistrement, ce qui empêche AudioContext de s'exécuter"
//@@zh="未采集到录音"
//@@en="Recording not captured"
,"Ltz3:"+ //no args
"Aucun enregistrement n'a été collecté"
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
,"xGuI:"+ //args: {1}-{2}
"L'encodeur {1} n'est pas chargé. Veuillez essayer de trouver l'encodeur {1} dans src/engine de {2} et de le charger"
//@@zh="录音错误:"
//@@en="Recording error: "
,"AxOH:"+ //no args
"Erreur d'enregistrement : "
//@@zh="音频buffers被释放"
//@@en="Audio buffers are released"
,"xkKd:"+ //no args
"Les tampons audio sont libérés"
//@@zh="采样:{1} 花:{2}ms"
//@@en="Sampled: {1}, took: {2}ms"
,"CxeT:"+ //args: {1}-{2}
"Échantillon : {1} Fleur : {2}ms"
//@@zh="非浏览器环境,不支持{1}"
//@@en="Non-browser environment, does not support {1}"
,"NonBrowser-1:"+ //args: {1}
"Environnement sans navigateur, ne prend pas en charge {1}"
//@@zh="参数错误:{1}"
//@@en="Illegal argument: {1}"
,"IllegalArgs-1:"+ //args: {1}
"Erreur de paramètre : {1}"
//@@zh="调用{1}需要先导入{2}"
//@@en="Calling {1} needs to import {2} first"
,"NeedImport-2:"+ //args: {1}-{2}
"Pour appeler {1}, vous devez d'abord importer {2}"
//@@zh="不支持:{1}"
//@@en="Not support: {1}"
,"NotSupport-1:"+ //args: {1}
"Non pris en charge : {1}"
//@@zh="覆盖导入{1}"
//@@en="Override import {1}"
,"8HO5:"+ //args: {1}
"Remplacer l'importation {1}"
]);
//*************** End srcFile=recorder-core.js ***************
//*************** Begin srcFile=engine/beta-amr.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="AMR-NB(NarrowBand)采样率设置无效只提供8000hz比特率范围{1}默认12.2kbps一帧20ms、{2}字节浏览器一般不支持播放amr格式可用Recorder.amr2wav()转码成wav播放"
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
//@@Put0
"b2mN:"+ //args: {1}-{2}
"AMR-NB (NarrowBand), le paramètre sampleRate n'est pas valide (seul 8000 Hz est fourni), plage bitRate : {1} (par défaut 12.2 kbps), une image 20 ms, {2} octets ; les navigateurs ne prennent généralement pas en charge la lecture du format amr, disponible Recorder.amr2wav() Transcoder en wav pour la lecture"
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
,"tQBv:"+ //args: {1}-{2}
"AMR Info : ne correspond pas à l'ensemble {1}, a été mis à jour vers {2}"
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"q12D:"+ //args: {1}
"Les données sampleRate sont inférieures à {1}"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"TxjV:"+ //no args
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据只有首次回调数据首帧包含AMR头在合并成AMR文件时如果没有把首帧数据包含进去则必须在文件开头添加上AMR头Recorder.AMR.AMR_HEADER转成二进制否则无法播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
,"Q7p7:"+ //no args
"takeoffEncodeChunk prend en charge les données binaires sorties par l'encodeur AMR. Seules les premières données de rappel (première image) contiennent l'en-tête AMR ; lors de la fusion dans un fichier AMR, si les données de la première image ne sont pas incluses, l'en-tête AMR doit être ajouté à la début du fichier : Recorder.AMR.AMR_HEADER (converti en binaire), sinon il ne peut pas être lu"
//@@zh="当前环境不支持Web Workeramr实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
,"6o9Z:"+ //no args
"L'environnement actuel ne prend pas en charge Web Worker et l'encodeur en temps réel amr s'exécute dans le thread principal"
//@@zh="amr worker剩{1}个未stop"
//@@en="amr worker left {1} unstopped"
,"yYWs:"+ //args: {1}
"amr worker reste {1} non stop"
//@@zh="amr编码器未start"
//@@en="amr encoder not started"
,"jOi8:"+ //no args
"encodeur amr pas start"
]);
//*************** End srcFile=engine/beta-amr.js ***************
//*************** Begin srcFile=engine/beta-ogg.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="Ogg Vorbis比特率取值16-100kbps采样率取值无限制"
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
//@@Put0
"O8Gn:"+ //no args
"Ogg Vorbis, la valeur bitRate est de 16 à 100 kbps, la valeur sampleRate est illimitée"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"5si6:"+ //no args
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据Ogg由数据页组成一页包含多帧音频数据含几秒的音频一页数据无法单独解码和播放此编码器每次输出都是完整的一页数据因此实时性会比较低在合并成完整ogg文件时必须将输出的所有数据合并到一起否则可能无法播放不支持截取中间一部分单独解码和播放"
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
,"R8yz:"+ //no args
"takeoffEncodeChunk prend en charge les données binaires sorties par l'encodeur OggVorbis. Ogg est composé de pages de données. Une page contient plusieurs images de données audio (y compris plusieurs secondes d'audio. Une page de données ne peut pas être décodée et lue séparément). Chaque sortie de ce L'encodeur est complet. Une page de données, donc les performances en temps réel seront relativement faibles ; lors de la fusion dans un fichier ogg complet, toutes les données de sortie doivent être fusionnées ensemble, sinon elles risquent de ne pas être lues et ne sont pas prises en charge. interceptez la partie médiane, décodez-la et jouez-la séparément"
//@@zh="当前环境不支持Web WorkerOggVorbis实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
,"hB9D:"+ //no args
"L'environnement actuel ne prend pas en charge les Web Workers et l'encodeur en temps réel OggVorbis s'exécute dans le thread principal."
//@@zh="ogg worker剩{1}个未stop"
//@@en="There are {1} unstopped ogg workers"
,"oTiy:"+ //args: {1}
"ogg worker reste {1} non stop"
//@@zh="ogg编码器未start"
//@@en="ogg encoder not started"
,"dIpw:"+ //no args
"encodeur ogg pas start"
]);
//*************** End srcFile=engine/beta-ogg.js ***************
//*************** Begin srcFile=engine/beta-webm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="此浏览器不支持进行webm编码未实现MediaRecorder"
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
//@@Put0
"L49q:"+ //no args
"Ce navigateur ne prend pas en charge l'encodage Webm et MediaRecorder n'est pas implémenté"
//@@zh="只有比较新的浏览器支持压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法只能按照类似边播放边收听形式把数据导入到MediaRecorder有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小但音频文件中的比特率并非设定比特率采样率由于是我们自己采样的到这个编码器随他怎么搞"
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
,"tsTW:"+ //no args
"Seuls les navigateurs les plus récents le prennent en charge et le taux de compression est similaire à celui du mp3. Puisqu'il n'existe aucun moyen d'encoder rapidement les données pcm existantes, les données ne peuvent être importées dans MediaRecorder que d'une manière similaire de lecture et d'écoute, et vous devez attendre quelques secondes. Bien que la taille du fichier audio de sortie puisse être contrôlée par le débit binaire, le débit binaire du fichier audio n'est pas le débit binaire défini. Puisque le taux d'échantillonnage est échantillonné par nous-mêmes, nous pouvons faire ce que nous voulons avec cet encodeur"
//@@zh="此浏览器不支持把录音转成webm格式"
//@@en="This browser does not support converting recordings to webm format"
,"aG4z:"+ //no args
"Ce navigateur ne prend pas en charge la conversion des enregistrements au format Webm"
//@@zh="转码webm出错{1}"
//@@en="Error encoding webm: {1}"
,"PIX0:"+ //args: {1}
"Erreur de transcodage Webm : {1}"
]);
//*************** End srcFile=engine/beta-webm.js ***************
//*************** Begin srcFile=engine/g711x.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}{2}音频文件无法直接播放可用Recorder.{2}2wav()转码成wav播放采样率比特率设置无效固定为8000hz采样率、16位每个采样压缩成8位存储音频文件大小为8000字节/秒如需任意采样率支持请使用Recorder.{2}_encode()方法"
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
//@@Put0
"d8YX:"+ //args: {1}-{2}
"{1} ; {2} Le fichier audio ne peut pas être lu directement. Vous pouvez utiliser Recorder.{2}2wav() pour le transcoder en wav pour la lecture ; le paramètre de débit binaire du taux d'échantillonnage n'est pas valide et est fixé à 8 000 hz. taux, 16 bits, et chaque échantillon est compressé. dans un stockage de 8 bits, la taille du fichier audio est de 8 000 octets/seconde ; si vous avez besoin de prise en charge pour un taux d'échantillonnage, veuillez utiliser la méthode Recorder.{2}_encode()"
//@@zh="数据采样率低于{1}"
//@@en="Data sampleRate lower than {1}"
,"29UK:"+ //args: {1}
"Les données sampleRate sont inférieures à {1}"
//@@zh="{1}编码器未start"
//@@en="{1} encoder not started"
,"quVJ:"+ //args: {1}
"encodeur {1} pas start"
]);
//*************** End srcFile=engine/g711x.js ***************
//*************** Begin srcFile=engine/mp3.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="采样率范围:{1};比特率范围:{2}不同比特率支持的采样率范围不同小于32kbps时采样率需小于32000"
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
//@@Put0
"Zm7L:"+ //args: {1}-{2}
"Plage sampleRate : {1} ; plage bitRate : {2} (différents bitRate prennent en charge différentes plages sampleRate. Lorsqu'il est inférieur à 32 kbit/s, le sampleRate doit être inférieur à 32000)"
//@@zh="{1}不在mp3支持的取值范围{2}"
//@@en="{1} is not in the value range supported by mp3: {2}"
,"eGB9:"+ //args: {1}-{2}
"{1} n'est pas dans la plage de valeurs prise en charge par mp3 : {2}"
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围{3}"
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
,"zLTa:"+ //args: {1}-{3}
"sampleRate a été mis à jour à {1} car {2} n'est pas dans la plage de valeurs prise en charge par mp3 : {3}"
//@@zh="当前浏览器版本太低,无法实时处理"
//@@en="The current browser version is too low to process in real time"
,"yhUs:"+ //no args
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
//@@zh="当前环境不支持Web Workermp3实时编码器运行在主线程中"
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
,"k9PT:"+ //no args
"L'environnement actuel ne prend pas en charge Web Worker et l'encodeur en temps réel mp3 s'exécute dans le thread principal"
//@@zh="mp3 worker剩{1}个未stop"
//@@en="There are {1} unstopped mp3 workers left"
,"fT6M:"+ //args: {1}
"mp3 worker left {1} unstopped"
//@@zh="mp3编码器未start"
//@@en="mp3 encoder not started"
,"mPxH:"+ //no args
"encodeur mp3 pas start"
//@@zh="和设置的不匹配{1},已更新成{2}"
//@@en="Does not match the set {1}, has been updated to {2}"
,"uY9i:"+ //args: {1}-{2}
"Ne correspond pas au paramètre {1}, a été mis à jour vers {2}"
//@@zh="Fix移除{1}帧"
//@@en="Fix remove {1} frame"
,"iMSm:"+ //args: {1}
"Fix supprime {1} images"
//@@zh="移除帧数过多"
//@@en="Remove too many frames"
,"b9zm:"+ //no args
"Supprimer trop de cadres"
]);
//*************** End srcFile=engine/mp3.js ***************
//*************** Begin srcFile=engine/pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="pcm为未封装的原始音频数据pcm音频文件无法直接播放可用Recorder.pcm2wav()转码成wav播放支持位数8位、16位填在比特率里面采样率取值无限制"
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
//@@Put0
"fWsN:"+ //no args
"pcm est une donnée audio originale non encapsulée. Les fichiers audio Pcm ne peuvent pas être lus directement. Recorder.pcm2wav() peut être utilisé pour transcoder en wav pour la lecture. Il prend en charge 8 et 16 chiffres (remplis dans bitRate) et la valeur de sampleRate est illimitée."
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
,"uMUJ:"+ //args: {1}-{2}
"PCM Info: Le bit {1} n'est pas pris en charge et a été mis à jour vers le bit {2}"
//@@zh="pcm2wav必须提供sampleRate和bitRate"
//@@en="pcm2wav must provide sampleRate and bitRate"
,"KmRz:"+ //no args
"pcm2wav doit fournir sampleRate et bitRate"
//@@zh="pcm编码器未start"
//@@en="pcm encoder not started"
,"sDkA:"+ //no args
"encodeur pcm pas start"
]);
//*************** End srcFile=engine/pcm.js ***************
//*************** Begin srcFile=engine/wav.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="支持位数8位、16位填在比特率里面采样率取值无限制此编码器仅在pcm数据前加了一个44字节的wav头编码出来的16位wav文件去掉开头的44字节即可得到pcm其他wav编码器可能不是44字节"
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
//@@Put0
"gPSE:"+ //no args
"Prend en charge les chiffres de 8 bits et 16 bits (remplis dans bitRate) et la valeur de sampleRate est illimitée ; cet encodeur ajoute uniquement un en-tête wav de 44 octets avant les données pcm, et le fichier wav codé de 16 bits supprime les 44 premiers bits. Octets pour obtenir pcm (remarque : les autres encodeurs wav peuvent ne pas faire 44 octets)"
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
,"wyw9:"+ //args: {1}-{2}
"WAV Info: Le bit {1} n'est pas pris en charge et a été mis à jour vers le bit {2}"
]);
//*************** End srcFile=engine/wav.js ***************
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="getAudioSrc方法已过时请直接使用getMediaStream然后赋值给audio.srcObject仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
//@@Put0
"0XYC:"+ //no args
"La méthode getAudioSrc est obsolète : veuillez utiliser getMediaStream directement et l'attribuer à audio.srcObject. Cette méthode ne peut être appelée que dans les navigateurs qui ne prennent pas en charge srcObject et attribuée à audio.src pour des raisons de compatibilité."
//@@zh="start被stop终止"
//@@en="start is terminated by stop"
,"6DDt:"+ //no args
"start est terminé par stop"
//@@zh="{1}多次start"
//@@en="{1} repeat start"
,"I4h4:"+ //args: {1}
"Répétition {1} start"
//@@zh="浏览器不支持打开{1}"
//@@en="The browser does not support opening {1}"
,"P6Gs:"+ //args: {1}
"Le navigateur ne prend pas en charge l'ouverture de {1}"
//@@zh="注意ctx不是running状态start需要在用户操作(触摸、点击等)时进行调用否则会尝试进行ctx.resume可能会产生兼容性问题(仅iOS)请参阅文档中runningContext配置"
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
,"JwDm:"+ //no args
"(Remarque : ctx n'est pas dans l'état running. start doit être appelé lorsque l'utilisateur opère (toucher, cliquer, etc.), sinon ctx.resume sera tenté, ce qui peut entraîner des problèmes de compatibilité (iOS uniquement). Veuillez vous référer au configuration runningContext dans le document)"
//@@zh="此浏览器的AudioBuffer实现不支持动态特性采用兼容模式"
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
,"qx6X:"+ //no args
"L'implémentation AudioBuffer de ce navigateur ne prend pas en charge les fonctionnalités dynamiques et utilise le mode de compatibilité"
//@@zh="环境检测超时"
//@@en="Environment detection timeout"
,"cdOx:"+ //no args
"Expiration du délai de détection de l'environnement"
//@@zh="可能无法播放:{1}"
//@@en="Could not play: {1}"
,"S2Bu:"+ //args: {1}
"Peut ne pas jouer: {1}"
//@@zh="input调用失败非pcm[Int16,...]输入时必须解码或者使用transform转换"
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
,"ZfGG:"+ //no args
"L'appel input a échoué: non - PCM [int16,...] en entrée, il doit être décodé ou converti avec transform"
//@@zh="input调用失败未提供sampleRate"
//@@en="input call failed: sampleRate not provided"
,"N4ke:"+ //no args
"L'appel input a échoué: sampleRate n'a pas été fourni"
//@@zh="input调用失败data的sampleRate={1}和之前的={2}不同"
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
,"IHZd:"+ //args: {1}-{2}
"L'appel input a échoué: sampleRate={1} de Data est différent de ={2} précédent"
//@@zh="延迟过大,已丢弃{1}ms {2}"
//@@en="The delay is too large, {1}ms has been discarded, {2}"
,"L8sC:"+ //args: {1}-{2}
"Le délai est trop important, {1}ms ont été ignorées, {2}"
//@@zh="{1}未调用start方法"
//@@en="{1} did not call the start method"
,"TZPq:"+ //args: {1}
"{1} la méthode start n'est pas appelée"
//@@zh="浏览器不支持音频解码"
//@@en="Browser does not support audio decoding"
,"iCFC:"+ //no args
"Le navigateur ne supporte pas le décodage audio"
//@@zh="音频解码数据必须是ArrayBuffer"
//@@en="Audio decoding data must be ArrayBuffer"
,"wE2k:"+ //no args
"Les données de décodage audio doivent être ArrayBuffer"
//@@zh="音频解码失败:{1}"
//@@en="Audio decoding failed: {1}"
,"mOaT:"+ //args: {1}
"Le décodage audio a échoué: {1}"
]);
//*************** End srcFile=extensions/buffer_stream.player.js ***************
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="符号[{1}]无效:{2}"
//@@en="Invalid symbol [{1}]: {2}"
//@@Put0
"3RBa:"+ //args: {1}-{2}
"Le symbole [{1}] est invalide: {2}"
//@@zh="音符[{1}]无效:{2}"
//@@en="Invalid note [{1}]: {2}"
,"U212:"+ //args: {1}-{2}
"Note [{1}] invalide: {2}"
//@@zh="多个音时必须对齐,相差{1}ms"
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
,"7qAD:"+ //args: {1}
"Doit être aligné lorsque plusieurs tonalités, différence {1}ms"
//@@zh="祝你生日快乐"
//@@en="Happy Birthday to You"
,"QGsW:"+ //no args
"Happy Birthday to You"
//@@zh="致爱丽丝"
//@@en="For Elise"
,"emJR:"+ //no args
"For Elise"
//@@zh="卡农-右手简谱"
//@@en="Canon - Right Hand Notation"
,"GsYy:"+ //no args
"Canon - symbole de la main droite"
//@@zh="卡农"
//@@en="Canon"
,"bSFZ:"+ //no args
"Canon"
]);
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
//*************** Begin srcFile=extensions/sonic.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="当前环境不支持Web Worker不支持调用Sonic.Async"
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
//@@Put0
"Ikdz:"+ //no args
"Web Worker n'est pas supporté dans l'environnement actuel, appel Sonic.Async n'est pas supporté"
//@@zh="sonic worker剩{1}个未flush"
//@@en="There are {1} unflushed sonic workers left"
,"IC5Y:"+ //args: {1}
"sonic worker reste {1} non flush"
]);
//*************** End srcFile=extensions/sonic.js ***************
//*************** Begin srcFile=app-support/app-native-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
//@@Put0
"WWoj:"+ //args: {1}-{3}
"La méthode {2} dans {1} n'est pas implémentée, implémentez - la dans un fichier {3} ou dans un fichier de configuration"
//@@zh="未开始录音但收到Native PCM数据"
//@@en="Recording does not start, but Native PCM data is received"
,"rCAM:"+ //no args
"L'enregistrement n'a pas commencé, mais les données Native PCM ont été reçues"
//@@zh="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听postMessage转发兼容传输数据请自行实现将top层接收到数据转发到本iframe不限层不然无法接收到录音数据"
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
,"t2OF:"+ //no args
"Iframe Cross - Domain détecté, NativeRecordReceivePCM ne peut pas être injecté à la couche supérieure, a écouté postMessage pour transmettre des données de transfert compatibles, s'il vous plaît implémenter vous - même pour transmettre les données reçues à la couche supérieure à cette iframe (couche illimitée), sinon les données d'enregistrement ne peuvent pas être reçues"
//@@zh="未开始录音"
//@@en="Recording not started"
,"Z2y2:"+ //no args
"L'enregistrement n'a pas commencé"
]);
//*************** End srcFile=app-support/app-native-support.js ***************
//*************** Begin srcFile=app-support/app.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="重复导入{1}"
//@@en="Duplicate import {1}"
//@@Put0
"uXtA:"+ //args: {1}
"Importation répétée {1}"
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
,"kIBu:"+ //args: {1}
"Remarque : Étant donné que d'autres méthodes liées à l'enregistrement sont appelées simultanément, le résultat de l'appel actuel de {1} a été ignoré et il n'y aura pas de rappel"
//@@zh="重复注册{1}"
//@@en="Duplicate registration {1}"
,"ha2K:"+ //args: {1}
"Enregistrement répété {1}"
//@@zh="仅清理资源"
//@@en="Clean resources only"
,"wpTL:"+ //no args
"Nettoyage des ressources uniquement"
//@@zh="未开始录音"
//@@en="Recording not started"
,"bpvP:"+ //no args
"L'enregistrement n'a pas commencé"
//@@zh="当前环境不支持实时回调,无法进行{1}"
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
,"fLJD:"+ //args: {1}
"L'environnement actuel ne prend pas en charge le Callback en temps réel, impossible de faire {1}"
//@@zh="录音权限请求失败:"
//@@en="Recording permission request failed: "
,"YnzX:"+ //no args
"La demande d'autorisation d'enregistrement a échoué: "
//@@zh="需先调用{1}"
//@@en="Need to call {1} first"
,"nwKR:"+ //args: {1}
"Appelez d'abord {1}"
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
,"citA:"+ //args: {1}-{2}
"Actuellement, ce n'est pas un environnement de navigateur, il est nécessaire d'introduire un fichier de support ({1}) pour cette plate - forme ou d'appeler {2} pour implémenter l'accès par vous - même"
//@@zh="开始录音失败:"
//@@en="Failed to start recording: "
,"ecp9:"+ //no args
"Le début de l'enregistrement échoue: "
//@@zh="不能录音:"
//@@en="Cannot record: "
,"EKmS:"+ //no args
"Ne peut pas enregistrer: "
//@@zh="已开始录音"
//@@en="Recording started"
,"k7Qo:"+ //no args
"Enregistrement commencé"
//@@zh="结束录音失败:"
//@@en="Failed to stop recording: "
,"Douz:"+ //no args
"Fin de l'enregistrement échoué: "
//@@zh="和Start时差{1}ms"
//@@en="Time difference from Start: {1}ms"
,"wqSH:"+ //args: {1}
"Et le décalage horaire de départ: {1}ms"
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
,"g3VX:"+ //args: {1}-{4}
"Fin de l'enregistrement, prend du temps {1}ms Durée audio {2}ms , taille du fichier {3}b , {4}"
]);
//*************** End srcFile=app-support/app.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

41
node_modules/recorder-core/src/i18n/zh-CN.js generated vendored Normal file
View File

@ -0,0 +1,41 @@
/*
Recorder i18n/zh-CN.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="zh-CN" or "zh"
Desc: Simplified Chinese, 简体中文代码内置完整的中文支持无需额外翻译本文件存在的意义是方便查看支持的语言
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="zh-CN";
Recorder.CLog('Import Recorder i18n lang="zh-CN"');
i18n.alias["zh-CN"]="zh";
var putSet={lang:"zh"};
i18n.data["rtl$zh"]=false;
i18n.data["desc$zh"]="Simplified Chinese, 简体中文。代码内置完整的中文支持,无需额外翻译,本文件存在的意义是方便查看支持的语言。";
//@@Exec i18n.GenerateDisplayEnglish=false;
//@@Exec i18n.put(putSet,[]);
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

2005
node_modules/recorder-core/src/recorder-core.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

106
node_modules/vue3-menus/.npmignore generated vendored Normal file
View File

@ -0,0 +1,106 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
yarn.lock

21
node_modules/vue3-menus/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 xfy520
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

385
node_modules/vue3-menus/README.md generated vendored Normal file
View File

@ -0,0 +1,385 @@
# vue3-menus
Vue3.0 自定义右键菜单,支持 Vite2.0[官网](https://doc.wssio.com/opensource/vue3-menus/)
Vue3.0 原生实现完全自定义右键菜单组件, 零依赖,可根据可视区域自动调节显示位置,可支持插槽完全重写每一项菜单
![演示](./example/vue3-menus.png)
## 在线演示
- [完整菜单功能演示](https://codepen.io/xfy520/pen/yLXNqzy)
- [复制粘贴演示](https://codepen.io/xfy520/pen/xxrGJdg)
## 快速安装
### npm 安装
```shell
npm install vue3-menus
```
```shell
yarn add vue3-menus
```
### CDN
```html
<script src="https://unpkg.com/vue3-menus/dist/vue3-menus.umd.min.js">
```
## 使用Vite 情况下同样使用)
CDN引入则不需要 `app.use(Vue3Menus)`
> 样例中使用的是`@ant-design/icons-vue`图标与`@element-plus/icons`图标、图标可以使用`html`代码传入、也可以通过插槽`自定义图标`、也可以`完全重写每一项菜单`
```js
// 全局注册组件、指令、方法
import { createApp } from 'vue';
import Menus from 'vue3-menus';
import App from './App.vue';
const app = createApp(App);
app.use(Menus);
app.mount('#app');
// 单个注册某个,以下三种方式均可在单个文件内使用
import { createApp } from 'vue';
import { directive, menusEvent, Vue3Menus } from 'vue3-menus';
import App from './App.vue';
const app = createApp(App);
app.component('vue3-menus', Vue3Menus); // 只注册组件
app.directive('menus', directive); // 只注册指令
app.config.globalProperties.$menusEvent = menusEvent; // 只绑定方法
app.mount('#app');
```
```html
<template>
<div style="height: 98vh; width: 100%;" v-menus:left="menus">
<div class="div" v-menus:left="menus">指令方式打开菜单</div>
<div class="div" @click.stop @contextmenu="($event) => $menusEvent($event, menus)">事件方式打开菜单</div>
<div class="div" @click.stop @contextmenu="rightClick">组件方式打开菜单</div>
<vue3-menus :open="isOpen" :event="eventVal" :menus="menus.menus">
<template #icon="{menu, activeIndex, index}">{{activeIndex}}</template>
<template #label="{ menu, activeIndex, index }">插槽:{{ menu.label }}</template>
</vue3-menus>
</div>
</template>
<script>
import { defineComponent, nextTick, ref, shallowRef } from "vue";
import { SyncOutlined, WindowsOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
import { Printer } from '@element-plus/icons'
export default defineComponent({
name: "App",
setup() {
const isOpen = ref(false);
const eventVal = ref({});
function rightClick(event) {
isOpen.value = false;
nextTick(() => {
eventVal.value = event;
isOpen.value = true;
})
event.preventDefault();
}
const menus = shallowRef({
menus: [
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back(-1);
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
},
{
label: "前进(F)",
tip: 'Alt+向右箭头',
disabled: true
},
{
label: "重新加载(R)",
tip: 'Ctrl+R',
click: () => location.reload(),
divided: true
},
{
label: "另存为(A)...",
tip: 'Ctrl+S'
},
{
label: "打印(P)...",
tip: 'Ctrl+P',
click: () => window.print(),
},
{
label: "投射(C)...",
divided: true
},
{
label: '发送到你的设备',
children: [
{
label: 'iPhone',
},
{
label: 'iPad'
},
{
label: 'Windows 11'
}
]
},
{
label: "为此页面创建二维码",
divided: true,
},
{
label: "使用网页翻译(F)",
divided: true,
children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
{
label: "百度翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },]
},
{
label: "搜狗翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
{
label: "有道翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
]
},
{
label: "截取网页(R)"
},
{ label: "查看网页源代码(U)", tip: 'Ctrl+U' },
{ label: "检查(N)", tip: 'Ctrl+Shift+I' }
]
})
return { menus, isOpen, rightClick, eventVal }
},
});
</script>
```
```css
.div {
display: inline-block;
background-color: aqua;
margin: 0 20px;
line-height: 200px;
padding: 0 20px;
height: 200px;
}
```
### 指令方式使用
```html
<template>
<div v-menus:left="menus">指令方式打开菜单</div>
</template>
<script>
import { defineComponent, shallowRef } from "vue";
import { directive } from 'vue3-menus';
export default defineComponent({
name: "App",
directives: {
menus: directive
},
setup() {
const menus = shallowRef({
menus: [
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back(-1);
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
}
]
})
return { menus }
},
});
</script>
```
### 方法方式使用
```html
<template>
<div class="div" @click.stop @contextmenu="rightClick">事件方式打开菜单</div>
</template>
<script>
import { defineComponent, shallowRef } from "vue";
import { menusEvent } from 'vue3-menus';
export default defineComponent({
name: "App",
setup() {
const menus = shallowRef({
menus: [
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back(-1);
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
}
]
});
function rightClick(event) {
menusEvent(event, menus.value);
event.preventDefault();
}
return { rightClick }
},
});
</script>
```
### 组件方式使用
```html
<template>
<div class="div" @click.stop @contextmenu="rightClick">组件方式打开菜单</div>
<vue3-menus v-model:open="isOpen" :event="eventVal" :menus="menus" hasIcon>
<template #icon="{menu, activeIndex, index}">{{activeIndex}}</template>
<template #label="{ menu, activeIndex, index}">插槽:{{ menu.label }}</template>
</vue3-menus>
</template>
<script>
import { defineComponent, nextTick, ref, shallowRef } from "vue";
import { Vue3Menus } from 'vue3-menus';
export default defineComponent({
name: "App",
components: {
Vue3Menus
},
setup() {
const isOpen = ref(false);
const eventVal = ref({});
function rightClick(event) {
isOpen.value = false;
nextTick(() => {
eventVal.value = event;
isOpen.value = true;
})
event.preventDefault();
}
const menus = shallowRef([
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back(-1);
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
}
]);
return { menus, isOpen, rightClick, eventVal }
},
});
</script>
```
## 参数说明
### 单个菜单项参数`MenusItemOptions`
| 属性 | 描述 | 类型 | 是否必填 | 默认值 |
| :------: | :----------------------------------------------------------: | :--------------------: | :------: | :---------: |
| label | 菜单项名称 | `string` | `true` | — |
| style | 每一项菜单的自定义样式 | `object` | `false` | `{}` |
| icon | 图标参数内部支持html字符串图标传入组件时需要实现icon插槽 | `string` \| `其他类型` | `false` | `undefined` |
| disabled | 是否禁用菜单项 | `boolean` | `false` | `undefined` |
| divided | 是否显示分割线 | `boolean` | `false` | `undefined` |
| tip | 没项菜单后面的小提示 | `string` | `false` | `''` |
| hidden | 是否隐藏该项 | `boolean` | `false` | `undefined` |
| children | 子菜单列表信息 | `MenusItemOptions[]` | `false` | `undefined` |
| enter | 菜单项移入事件,返回`null`或`false`不展开子菜单 | `Function()` | `false` | `undefined` |
| click | 菜单项点击事件,返回`null`或`false`不关闭菜单 | `Function()` | `false` | `undefined` |
### 指令与方法使用参数
| 属性 | 描述 | 类型 | 是否必填 | 默认值 |
| :-------: | :---------------------------------------------: | :-------------------: | :------: | :---------: |
| menus | 菜单列表信息 | `MenusItemOptions[]` | `true` | [] |
| menusClass | 菜单外层 `div``class` 名 | `string` | `false` | `null` |
| itemClass | 菜单每一项的`class`名 | `string` | `false` | `null` |
| minWidth | 菜单容器最小宽度 | `number` \| `string` | `false` | `none` |
| maxWidth | 菜单容器最打宽度 | `number` \| `string` | `false` | `none` |
| zIndex | 菜单层级 | `number` \| `string` | `false` | `3` |
| direction | 菜单打开方向 | `left` \| `right` | `false` | `right` |
### 组件使用参数
| 属性 | 描述 | 类型 | 是否必填 | 默认值 | 插槽传入值 |
| :-------: | :---------------------------------------------: | :-------------------: | :------------------: | :---------: | :-----------------------------------------------: |
| menus | 菜单列表信息 | `MenusItemOptions[]` | `true` | [] | |
| event | 鼠标事件信息(指令使用时不传) | `Event` | 与`position`必填一项 | {} | |
| menusClass | 菜单外层 `div``class` 名 | `string` | `false` | `null` | |
| itemClass | 菜单每一项的`class`名 | `string` | `false` | `null` | |
| minWidth | 菜单容器最小宽度 | `number` \| `string` | `false` | `none` | |
| maxWidth | 菜单容器最打宽度 | `number` \| `string` | `false` | `none` | |
| zIndex | 菜单层级 | `number` \| `string` | `false` | `3` | |
| direction | 菜单打开方向 | `left` \| `right` | `false` | `right` | |
| open | 控制菜单组件显示 | `boolean` | `true` | `false` | |
| args | 附加参数 | `unknown` | `false` | `undefined` | |
| default | 默认插槽 | `Slot` | `false` | - | `activeIndex`: 当前选中索引, `menu`: 当前菜单项 `MenusItemOptions`, `index`: 当前菜单索引 |
| icon | 图标插槽 | `Slot` | `false` | - | `activeIndex`: 当前选中索引, `menu`: 当前菜单项 `MenusItemOptions`, `index`: 当前菜单索引 |
| label | 菜单标题插槽 | `Slot` | `false` | - | `activeIndex`: 当前选中索引, `menu`: 当前菜单项 `MenusItemOptions`, `index`: 当前菜单索引 |
| suffix | 菜单后缀插槽 | `Slot` | `false` | - | `activeIndex`: 当前选中索引, `menu`: 当前菜单项 `MenusItemOptions`, `index`: 当前菜单索引 |
### 指令使用配置
> 配置参数与方法使用相同
| 指令使用方式 | 描述 | 参数类型 | 参数是否必填 | 默认值 |
| :-----------: | :------------------------: | :-----------: | :----------: | :----: |
| v-menus | 绑定元素右击打开菜单 | `MenuOptions` | `true` | - |
| v-menus:all | 绑定元素左右击均可打开菜单 | `MenuOptions` | `true` | - |
| v-menus:left | 绑定元素左击打开 | `MenuOptions` | `true` | - |
| v-menus:right | 绑定元素右击打开 | `MenuOptions` | `true` | - |

49
node_modules/vue3-menus/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,49 @@
type menusItemType = {
label: string;
style?: {
[key: string]: string | number
}
icon?: string | unknown;
disabled?: boolean;
divided?: boolean;
enter?: (menu: menusItemType, args: unknown) => unknown;
click?: (menu: menusItemType, args: unknown) => unknown;
children?: Array<menusItemType>;
tip?: string;
hidden?: boolean;
}
type menusType = {
menus: Array<menusItemType>;
menusClass?: string;
itemClass?: string;
minWidth?: number | string;
maxWidth?: number | string;
zIndex?: number | string;
direction?: "left" | "right";
}
type componentMenusType = menusType & {
event: MouseEvent;
open: boolean;
args?: unknown
}
declare module 'vue3-menus' {
export const Vue3Menus: import('vue').DefineComponent<componentMenusType, componentMenusType, componentMenusType, componentMenusType, componentMenusType,
componentMenusType, componentMenusType, componentMenusType, componentMenusType, componentMenusType, componentMenusType, componentMenusType>;
export const menusEvent: (event: MouseEvent, menus: menusType | Array<menusItemType>, args: unknown) => void;
export const directive: import('vue').Directive<any, menusType | Array<menusItemType>>;
const install: (app: import('vue').App, options: {
name: string
}) => unknown;
export default install;
}
export {
menusType,
menusItemType
}

58
node_modules/vue3-menus/package.json generated vendored Normal file
View File

@ -0,0 +1,58 @@
{
"name": "vue3-menus",
"version": "1.1.2",
"author": "xufangyi",
"private": false,
"description": "Vue3.0 左右键菜单",
"keywords": [
"vue",
"vue3",
"vue3-menus",
"contextmenu",
"vue-contextmenu",
"vue3-contextmenu"
],
"main": "dist/vue3-menus.js",
"module": "dist/vue3-menus.es.js",
"types": "./index.d.ts",
"files": [
"package.json",
"README.md",
".gitignore",
"LICENSE",
"dist/vue3-menus.es.js",
"dist/vue3-menus.es.min.js",
"dist/vue3-menus.js",
"dist/vue3-menus.min.js",
"src",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/xfy520/vue3-menus.git"
},
"scripts": {
"build": "rollup -c",
"dev": "vite"
},
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.15.5",
"@vitejs/plugin-vue": "^1.10.1",
"@vitejs/plugin-vue-jsx": "^1.3.0",
"@vue/compiler-sfc": "3.0.0",
"rollup": "^2.57.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-postcss": "3.1.8",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-uglify": "^6.0.4",
"rollup-plugin-vue": "^6.0.0",
"vite": "^2.7.0-beta.11",
"vue": "3.0.0"
}
}

353
node_modules/vue3-menus/src/App.vue generated vendored Normal file
View File

@ -0,0 +1,353 @@
<template>
<div class="menus-example" @click="click"></div>
<vue3-menus class="aaa" v-model:open="open" :event="event" :menus="menus"></vue3-menus>
</template>
<script lang="ts">
import { defineComponent, ref, nextTick } from 'vue';
export default defineComponent({
name: 'App',
setup() {
const menus = ref([
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back();
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
},
{
label: "前进(F)",
tip: 'Alt+向右箭头',
disabled: true
},
{
label: "重新加载(R)",
tip: 'Ctrl+R',
icon: '<svg focusable="false" class="anticon-spin" data-icon="sync" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"></path></svg>',
click: () => location.reload(),
divided: true
},
{
label: "另存为(A)...",
tip: 'Ctrl+S'
},
{
label: "打印(P)...",
tip: 'Ctrl+P',
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M256 768H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.056 29.056 0 0 1-12.16-12.096C65.536 746.432 64 741.248 64 727.04V379.072c0-42.816 4.48-58.304 12.8-73.984 8.384-15.616 20.672-27.904 36.288-36.288 15.68-8.32 31.168-12.8 73.984-12.8H256V64h512v192h68.928c42.816 0 58.304 4.48 73.984 12.8 15.616 8.384 27.904 20.672 36.288 36.288 8.32 15.68 12.8 31.168 12.8 73.984v347.904c0 14.272-1.472 19.456-4.288 24.64a29.056 29.056 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H768v192H256V768zm64-192v320h384V576H320zm-64 128V512h512v192h128V379.072c0-29.376-1.408-36.48-5.248-43.776a23.296 23.296 0 0 0-10.048-10.048c-7.232-3.84-14.4-5.248-43.776-5.248H187.072c-29.376 0-36.48 1.408-43.776 5.248a23.296 23.296 0 0 0-10.048 10.048c-3.84 7.232-5.248 14.4-5.248 43.776V704h128zm64-448h384V128H320v128zm-64 128h64v64h-64v-64zm128 0h64v64h-64v-64z"></path></svg>',
click: () => window.print(),
},
{
label: "投射(C)...",
divided: true,
children: [
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back();
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
},
{
label: "前进(F)",
tip: 'Alt+向右箭头',
disabled: true
},
{
label: "重新加载(R)",
tip: 'Ctrl+R',
icon: '<svg focusable="false" class="anticon-spin" data-icon="sync" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"></path></svg>',
click: () => location.reload(),
divided: true
},
{
label: "另存为(A)...",
tip: 'Ctrl+S'
},
{
label: "打印(P)...",
tip: 'Ctrl+P',
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M256 768H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.056 29.056 0 0 1-12.16-12.096C65.536 746.432 64 741.248 64 727.04V379.072c0-42.816 4.48-58.304 12.8-73.984 8.384-15.616 20.672-27.904 36.288-36.288 15.68-8.32 31.168-12.8 73.984-12.8H256V64h512v192h68.928c42.816 0 58.304 4.48 73.984 12.8 15.616 8.384 27.904 20.672 36.288 36.288 8.32 15.68 12.8 31.168 12.8 73.984v347.904c0 14.272-1.472 19.456-4.288 24.64a29.056 29.056 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H768v192H256V768zm64-192v320h384V576H320zm-64 128V512h512v192h128V379.072c0-29.376-1.408-36.48-5.248-43.776a23.296 23.296 0 0 0-10.048-10.048c-7.232-3.84-14.4-5.248-43.776-5.248H187.072c-29.376 0-36.48 1.408-43.776 5.248a23.296 23.296 0 0 0-10.048 10.048c-3.84 7.232-5.248 14.4-5.248 43.776V704h128zm64-448h384V128H320v128zm-64 128h64v64h-64v-64zm128 0h64v64h-64v-64z"></path></svg>',
click: () => window.print(),
},
{
label: "投射(C)...",
divided: true
},
{
label: '发送到你的设备',
icon: '<svg focusable="false" class="" data-icon="windows" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M120.1 770.6L443 823.2V543.8H120.1v226.8zm63.4-163.5h196.2v141.6l-196.2-31.9V607.1zm340.3 226.5l382 62.2v-352h-382v289.8zm63.4-226.5h255.3v214.4l-255.3-41.6V607.1zm-63.4-415.7v288.8h382V128.1l-382 63.3zm318.7 225.5H587.3V245l255.3-42.3v214.2zm-722.4 63.3H443V201.9l-322.9 53.5v224.8zM183.5 309l196.2-32.5v140.4H183.5V309z"></path></svg>',
children: [
{
label: 'iPhone',
},
{
label: 'iPad'
},
{
label: 'Windows 11'
}
]
},
{
label: "为此页面创建二维码",
divided: true,
icon: '<svg focusable="false" class="" data-icon="qrcode" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"></path></svg>',
},
{
label: "使用网页翻译(F)",
divided: true,
children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
{
label: "百度翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },]
},
{
label: "搜狗翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
{
label: "有道翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
]
},
{
label: "截取网页(R)"
},
{ label: "查看网页源代码(U)", tip: 'Ctrl+U' },
{ label: "检查(N)", tip: 'Ctrl+Shift+I' }
]
},
{
label: '发送到你的设备',
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>',
children: [
{
label: 'iPhone',
},
{
label: 'iPad'
},
{
label: 'Windows 11'
}
]
},
{
label: "为此页面创建二维码",
divided: true,
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>'
},
{
label: "使用网页翻译(F)",
divided: true,
children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
{
label: "百度翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },]
},
{
label: "搜狗翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
{
label: "有道翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
]
},
{
label: "截取网页(R)",
children: [
{
label: "返回(B)",
tip: 'Alt+向左箭头',
click: () => {
window.history.back();
}
},
{
label: "点击不关闭菜单",
tip: '不关闭菜单',
click: () => {
return false;
}
},
{
label: "前进(F)",
tip: 'Alt+向右箭头',
disabled: true
},
{
label: "重新加载(R)",
tip: 'Ctrl+R',
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>',
click: () => location.reload(),
divided: true
},
{
label: "另存为(A)...",
tip: 'Ctrl+S'
},
{
label: "打印(P)...",
tip: 'Ctrl+P',
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>',
click: () => window.print(),
},
{
label: "投射(C)...",
divided: true
},
{
label: '发送到你的设备',
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>',
children: [
{
label: 'iPhone',
},
{
label: 'iPad'
},
{
label: 'Windows 11'
}
]
},
{
label: "为此页面创建二维码",
divided: true,
icon: '<svg focusable="false" class="" data-icon="github" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>'
},
{
label: "使用网页翻译(F)",
divided: true,
children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
{
label: "百度翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },]
},
{
label: "搜狗翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
{
label: "有道翻译", children: [
{ label: "翻译成繁体中文" },
{ label: "翻译成繁体中文" },
]
},
]
},
{
label: "截取网页(R)"
},
{ label: "查看网页源代码(U)", tip: 'Ctrl+U' },
{ label: "检查(N)", tip: 'Ctrl+Shift+I' }
]
},
{ label: "查看网页源代码(U)", tip: 'Ctrl+U' },
{ label: "检查(N)", tip: 'Ctrl+Shift+I' }
]);
const open = ref(false);
const event = ref({});
function click(e: any) {
open.value = false;
nextTick(() => {
event.value = e;
open.value = true;
})
e.preventDefault();
}
setTimeout(() => {
menus.value[1].label = '点击关闭菜单'
menus.value[6].children[0].disabled = true
menus.value[6].children[8].label = '123'
menus.value[6].children[9].children[2].children[1].label = '123'
}, 10000);
return {
click,
menus,
open,
event
}
},
});
</script>
<style>
.my-menus-item {
display: flex;
line-height: 2rem;
padding: 0 1rem;
margin: 0;
font-size: 0.8rem;
outline: 0;
align-items: center;
transition: 0.2s;
box-sizing: border-box;
list-style: none;
border-bottom: 1px solid #00000000;
}
.my-menus-item-divided {
border-bottom-color: #ebeef5;
}
.my-menus-item-available {
color: #606266;
cursor: pointer;
}
.my-menus-item-available:hover {
background: #ecf5ff;
color: #409eff;
}
.my-menus-item-active {
background: #ecf5ff;
color: #409eff;
}
.my-menus-item-disabled {
color: #c0c4cc;
cursor: not-allowed;
}
</style>

7
node_modules/vue3-menus/src/main.ts generated vendored Normal file
View File

@ -0,0 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import Vue3Menus from '../package/index'
const app = createApp(App)
app.use(Vue3Menus)
app.mount('#app')

25
package-lock.json generated Normal file
View File

@ -0,0 +1,25 @@
{
"name": "MaxKB",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"recorder-core": "^1.3.25011100",
"vue3-menus": "^1.1.2"
}
},
"node_modules/recorder-core": {
"version": "1.3.25011100",
"resolved": "https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz",
"integrity": "sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==",
"license": "MIT"
},
"node_modules/vue3-menus": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vue3-menus/-/vue3-menus-1.1.2.tgz",
"integrity": "sha512-MoX87TH25fbKmmE8PwC+c2kJOSGJheP4pBR2we0RkOrfUDQg7sK+akAZSmQU8o+7dF+xVF2NfKPhoVHOhlX9wQ==",
"license": "MIT"
}
}
}

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"recorder-core": "^1.3.25011100",
"vue3-menus": "^1.1.2"
}
}

View File

@ -0,0 +1,29 @@
<svg width="89" height="22" viewBox="0 0 89 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11132_142017)">
<path d="M9.58875 8.33325H11.1126V14.0475H9.58875V8.33325Z" fill="#3370FF"/>
<path d="M36.8547 8.33325H38.3785V14.0475H36.8547V8.33325Z" fill="#3370FF"/>
<path d="M59.5767 8.33325H61.1005V14.0475H59.5767V8.33325Z" fill="#3370FF"/>
<path d="M86.843 8.33325H88.3668V14.0475H86.843V8.33325Z" fill="#3370FF"/>
<path d="M41.399 6.80957H42.9229V14.8096H41.399V6.80957Z" fill="#3370FF"/>
<path d="M55.0322 6.04736H56.556V15.5712H55.0322V6.04736Z" fill="#3370FF"/>
<path d="M45.9435 8.71411H47.4673V13.2855H45.9435V8.71411Z" fill="#3370FF"/>
<path d="M50.4879 7.95239H52.0117V14.4286H50.4879V7.95239Z" fill="#3370FF"/>
<path d="M5.04443 6.04736H6.56824V15.5712H5.04443V6.04736Z" fill="#3370FF"/>
<path d="M0.5 8.71411H2.02381V13.2855H0.5V8.71411Z" fill="#3370FF"/>
<path d="M14.1332 6.80957H15.657V15.1905H14.1332V6.80957Z" fill="#3370FF"/>
<path d="M32.3103 6.80957H33.8341V15.1905H32.3103V6.80957Z" fill="#3370FF"/>
<path d="M64.1211 6.80957H65.6449V15.5715H64.1211V6.80957Z" fill="#3370FF"/>
<path d="M82.2986 6.80957H83.8224V15.1905H82.2986V6.80957Z" fill="#3370FF"/>
<path d="M18.6776 6.04736H20.2014V15.9521H18.6776V6.04736Z" fill="#3370FF"/>
<path d="M27.7664 5.6665H29.2902V16.7141H27.7664V5.6665Z" fill="#3370FF"/>
<path d="M68.6654 5.28564H70.1892V16.7142H68.6654V5.28564Z" fill="#3370FF"/>
<path d="M77.7543 5.28564H79.2781V16.7142H77.7543V5.28564Z" fill="#3370FF"/>
<path d="M23.2219 2.6189H24.7457V19.3808H23.2219V2.6189Z" fill="#3370FF"/>
<path d="M73.2098 3.3811H74.7336V18.6192H73.2098V3.3811Z" fill="#3370FF"/>
</g>
<defs>
<clipPath id="clip0_11132_142017">
<rect width="88" height="22" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,29 @@
<svg width="89" height="22" viewBox="0 0 89 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11133_227282)">
<path d="M9.58875 8.33325H11.1126V14.0475H9.58875V8.33325Z" fill="#8F959E"/>
<path d="M36.8547 8.33325H38.3785V14.0475H36.8547V8.33325Z" fill="#8F959E"/>
<path d="M59.5767 8.33325H61.1005V14.0475H59.5767V8.33325Z" fill="#8F959E"/>
<path d="M86.843 8.33325H88.3668V14.0475H86.843V8.33325Z" fill="#8F959E"/>
<path d="M41.399 6.80957H42.9229V14.8096H41.399V6.80957Z" fill="#8F959E"/>
<path d="M55.0322 6.04736H56.556V15.5712H55.0322V6.04736Z" fill="#8F959E"/>
<path d="M45.9435 8.71411H47.4673V13.2855H45.9435V8.71411Z" fill="#8F959E"/>
<path d="M50.4879 7.95239H52.0117V14.4286H50.4879V7.95239Z" fill="#8F959E"/>
<path d="M5.04443 6.04736H6.56824V15.5712H5.04443V6.04736Z" fill="#8F959E"/>
<path d="M0.5 8.71411H2.02381V13.2855H0.5V8.71411Z" fill="#8F959E"/>
<path d="M14.1332 6.80957H15.657V15.1905H14.1332V6.80957Z" fill="#8F959E"/>
<path d="M32.3103 6.80957H33.8341V15.1905H32.3103V6.80957Z" fill="#8F959E"/>
<path d="M64.1211 6.80957H65.6449V15.5715H64.1211V6.80957Z" fill="#8F959E"/>
<path d="M82.2986 6.80957H83.8224V15.1905H82.2986V6.80957Z" fill="#8F959E"/>
<path d="M18.6776 6.04736H20.2014V15.9521H18.6776V6.04736Z" fill="#8F959E"/>
<path d="M27.7664 5.6665H29.2902V16.7141H27.7664V5.6665Z" fill="#8F959E"/>
<path d="M68.6654 5.28564H70.1892V16.7142H68.6654V5.28564Z" fill="#8F959E"/>
<path d="M77.7543 5.28564H79.2781V16.7142H77.7543V5.28564Z" fill="#8F959E"/>
<path d="M23.2219 2.6189H24.7457V19.3808H23.2219V2.6189Z" fill="#8F959E"/>
<path d="M73.2098 3.3811H74.7336V18.6192H73.2098V3.3811Z" fill="#8F959E"/>
</g>
<defs>
<clipPath id="clip0_11133_227282">
<rect width="88" height="22" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.98262 2.70361H5.98155C5.17745 2.70361 4.55609 2.95232 4.11749 3.46751C3.71543 3.9294 3.51441 4.56895 3.51441 5.36838V8.69045C3.51441 9.18787 3.40476 9.56094 3.18545 9.80965C2.97469 10.0292 2.6275 10.1764 2.14389 10.2413C1.95529 10.2666 1.81483 10.4233 1.81482 10.6084V11.4085C1.81481 11.5847 1.94968 11.7334 2.12951 11.7554C2.62062 11.8156 2.97261 11.9537 3.18545 12.1902C3.40476 12.4211 3.51441 12.7942 3.51441 13.3094V16.6492C3.51441 17.4309 3.71543 18.0704 4.11749 18.5323C4.55609 19.0297 5.17745 19.2962 5.98155 19.2962H6.98262C7.20691 19.2962 7.38874 19.1195 7.38874 18.9014V18.252C7.38874 18.034 7.20691 17.8572 6.98262 17.8572H6.3105C5.61605 17.8572 5.2871 17.4664 5.2871 16.7203V13.2205C5.2871 12.1546 4.75712 11.4263 3.71543 10.9999C4.75712 10.6268 5.2871 9.88071 5.2871 8.77927V5.29732C5.2871 4.51565 5.61605 4.14259 6.3105 4.14259H6.98262C7.20691 4.14259 7.38874 3.96584 7.38874 3.74781V3.09839C7.38874 2.88036 7.20691 2.70361 6.98262 2.70361ZM16.0185 2.70361H15.0174C14.7931 2.70361 14.6113 2.88036 14.6113 3.09839V3.74781C14.6113 3.96584 14.7931 4.14259 15.0174 4.14259H15.6895C16.3657 4.14259 16.7129 4.51565 16.7129 5.29732V8.77927C16.7129 9.88071 17.2246 10.6268 18.2846 10.9999C17.2246 11.4263 16.7129 12.1546 16.7129 13.2205V16.7203C16.7129 17.4664 16.3657 17.8572 15.6895 17.8572H15.0174C14.7931 17.8572 14.6113 18.034 14.6113 18.252V18.9014C14.6113 19.1195 14.7931 19.2962 15.0174 19.2962H16.0185C16.8226 19.2962 17.4439 19.0297 17.8825 18.5323C18.2846 18.0704 18.4856 17.4309 18.4856 16.6492V13.3094C18.4856 12.7942 18.5953 12.4211 18.8146 12.1902C19.0209 11.961 19.3579 11.8242 19.8257 11.7612L19.8331 11.7602C20.0347 11.7339 20.1852 11.5667 20.1852 11.3689V10.6149C20.1852 10.4263 20.0421 10.2665 19.8499 10.2405C19.3701 10.1756 19.0277 10.0314 18.8328 9.82741C18.5953 9.5787 18.4856 9.18787 18.4856 8.69045V5.36838C18.4856 4.56895 18.2846 3.9294 17.8825 3.46751C17.4439 2.95232 16.8226 2.70361 16.0185 2.70361Z" fill="white"/>
<path d="M7.44443 8.68878C7.44443 8.49241 7.60362 8.33323 7.79999 8.33323H14.2C14.3964 8.33323 14.5555 8.49241 14.5555 8.68878V9.75545C14.5555 9.95182 14.3964 10.111 14.2 10.111H7.79999C7.60362 10.111 7.44443 9.95182 7.44443 9.75545V8.68878Z" fill="white"/>
<path d="M7.44443 12.2443C7.44443 12.048 7.60362 11.8888 7.79999 11.8888H14.2C14.3964 11.8888 14.5555 12.048 14.5555 12.2443V13.311C14.5555 13.5074 14.3964 13.6666 14.2 13.6666H7.79999C7.60362 13.6666 7.44443 13.5074 7.44443 13.311V12.2443Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.2404 11.9757C14.2022 11.8917 14.1406 11.8206 14.0629 11.7709C13.9853 11.7213 13.8949 11.6951 13.8027 11.6957H12.6518V9.16918C12.6518 7.44347 11.069 6.00118 8.96725 5.65376V1.63242C8.96725 1.37615 8.86544 1.13037 8.68423 0.949156C8.50302 0.767942 8.25724 0.666138 8.00096 0.666138C7.74469 0.666138 7.49891 0.767942 7.3177 0.949156C7.13648 1.13037 7.03468 1.37615 7.03468 1.63242V5.65604C4.93411 6.00233 3.34896 7.44576 3.34896 9.17033V11.6945H2.19811C2.07221 11.6959 1.95172 11.746 1.86184 11.8341C1.77197 11.9223 1.71965 12.0418 1.71582 12.1677V12.1837C1.71582 12.2797 1.74782 12.3779 1.80953 12.4614L3.76496 15.1345C3.80786 15.1932 3.86352 15.2413 3.92775 15.2754C3.99197 15.3094 4.06308 15.3284 4.13572 15.3309C4.20836 15.3334 4.28062 15.3195 4.34705 15.29C4.41349 15.2605 4.47237 15.2163 4.51925 15.1608L6.80153 12.4899C6.86177 12.42 6.9005 12.3341 6.91307 12.2427C6.92564 12.1512 6.91151 12.0581 6.87239 11.9745C6.83423 11.8906 6.7726 11.8195 6.69493 11.7698C6.61726 11.7201 6.52688 11.694 6.43468 11.6945H5.28382V9.16918C5.28382 8.3829 6.39925 7.50747 8.00039 7.50747C9.60153 7.50747 10.7192 8.3829 10.7192 9.16918V11.6922H9.56611C9.47671 11.6918 9.38902 11.7166 9.31303 11.7637C9.23705 11.8108 9.17583 11.8783 9.13639 11.9585C9.10152 12.026 9.08349 12.1009 9.08382 12.1768C9.08444 12.2795 9.11721 12.3794 9.17754 12.4625L11.133 15.1368C11.1759 15.1955 11.2315 15.2436 11.2957 15.2777C11.36 15.3117 11.4311 15.3307 11.5037 15.3332C11.5764 15.3357 11.6486 15.3217 11.7151 15.2923C11.7815 15.2628 11.8404 15.2186 11.8872 15.1631L14.1684 12.4888C14.242 12.404 14.2833 12.2959 14.285 12.1837V12.1688C14.2839 12.1015 14.2683 12.0352 14.2392 11.9745L14.2404 11.9757Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.77756 1.66699C4.28664 1.66699 3.88867 2.06496 3.88867 2.55588V19.4448C3.88867 19.9357 4.28664 20.3337 4.77756 20.3337H17.222C17.7129 20.3337 18.1109 19.9357 18.1109 19.4448V6.3069C18.1109 6.19036 18.0651 6.07847 17.9834 5.99536L13.8597 1.79989C13.7761 1.71488 13.6619 1.66699 13.5427 1.66699H4.77756ZM7.04915 9.02062C7.04915 8.77516 7.24814 8.57617 7.4936 8.57617H14.5059C14.7514 8.57617 14.9504 8.77516 14.9504 9.02062V9.74789C14.9504 9.99335 14.7514 10.1923 14.5059 10.1923H7.4936C7.24814 10.1923 7.04915 9.99335 7.04915 9.74789V9.02062ZM7.04915 13.061C7.04915 12.8156 7.24814 12.6166 7.4936 12.6166H10.5553C10.8008 12.6166 10.9998 12.8156 10.9998 13.061V13.7883C10.9998 14.0338 10.8008 14.2327 10.5553 14.2327H7.4936C7.24814 14.2327 7.04915 14.0338 7.04915 13.7883V13.061Z" fill="white"/>
<path opacity="0.5" d="M13.6665 1.68457C13.7391 1.70561 13.8058 1.74502 13.8597 1.7999L17.9835 5.99537C18.0172 6.02971 18.0449 6.06897 18.0657 6.11145H14.6755C14.1183 6.11145 13.6665 5.6597 13.6665 5.10244V1.68457Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.05704 15.3334H2.00004C1.82323 15.3334 1.65366 15.2632 1.52864 15.1382C1.40361 15.0131 1.33337 14.8436 1.33337 14.6667V1.33341C1.33337 1.1566 1.40361 0.987035 1.52864 0.862011C1.65366 0.736987 1.82323 0.666748 2.00004 0.666748H12.6667C12.8435 0.666748 13.0131 0.736987 13.1381 0.862011C13.2631 0.987035 13.3334 1.1566 13.3334 1.33341V6.05641C13.3334 6.23316 13.2633 6.4027 13.1384 6.52775L4.52871 15.1381C4.46678 15.2 4.39324 15.2492 4.31231 15.2827C4.23138 15.3162 4.14464 15.3334 4.05704 15.3334ZM4.431 7.90245C4.49352 7.96496 4.5783 8.00008 4.66671 8.00008H8.00004C8.08844 8.00008 8.17323 7.96496 8.23574 7.90245C8.29825 7.83994 8.33337 7.75515 8.33337 7.66675V7.00008C8.33337 6.91167 8.29825 6.82689 8.23574 6.76438C8.17323 6.70187 8.08844 6.66675 8.00004 6.66675H4.66671C4.5783 6.66675 4.49352 6.70187 4.431 6.76438C4.36849 6.82689 4.33337 6.91167 4.33337 7.00008V7.66675C4.33337 7.75515 4.36849 7.83994 4.431 7.90245ZM4.431 4.56912C4.49352 4.63163 4.5783 4.66675 4.66671 4.66675H10.3334C10.3771 4.66675 10.4205 4.65813 10.4609 4.64138C10.5014 4.62462 10.5381 4.60007 10.5691 4.56912C10.6 4.53816 10.6246 4.50142 10.6413 4.46098C10.6581 4.42053 10.6667 4.37719 10.6667 4.33341V3.66675C10.6667 3.62297 10.6581 3.57963 10.6413 3.53919C10.6246 3.49874 10.6 3.462 10.5691 3.43105C10.5381 3.40009 10.5014 3.37554 10.4609 3.35879C10.4205 3.34204 10.3771 3.33341 10.3334 3.33341H4.66671C4.5783 3.33341 4.49352 3.36853 4.431 3.43105C4.36849 3.49356 4.33337 3.57834 4.33337 3.66675V4.33341C4.33337 4.42182 4.36849 4.5066 4.431 4.56912Z" fill="white"/>
<g opacity="0.5">
<path d="M13.565 11.518L11.6847 9.6381L7.55305 13.7961L7.33337 15.5777C7.33337 15.6661 7.36849 15.7509 7.431 15.8134C7.49352 15.8759 7.5783 15.911 7.66671 15.911L9.45064 15.627L13.565 11.518Z" fill="white"/>
<path d="M14.0486 8.20917C13.7886 7.94884 13.387 7.92884 13.151 8.16417L12.1543 9.16417L14.0369 11.0468L15.0346 10.0508L15.0662 10.0168C15.2689 9.7781 15.2396 9.4001 14.9912 9.15144L14.0486 8.20917Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1718950836622" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14660" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M366.08 301.269333l-215.552 235.178667L354.133333 771.669333a21.333333 21.333333 0 0 1-0.810666 28.8l-27.904 28.842667a21.333333 21.333333 0 0 1-31.36-0.810667L50.944 550.826667a21.333333 21.333333 0 0 1 0.298667-28.458667L306.773333 243.669333a21.333333 21.333333 0 0 1 31.018667-0.426666l27.904 28.8a21.333333 21.333333 0 0 1 0.426667 29.226666z m513.578667 235.178667l-206.08-235.178667a21.333333 21.333333 0 0 1 0.682666-28.928l27.904-28.8a21.333333 21.333333 0 0 1 31.317334 0.682667l245.674666 277.845333a21.333333 21.333333 0 0 1-0.298666 28.544l-255.402667 278.613334a21.333333 21.333333 0 0 1-31.061333 0.426666l-27.904-28.757333a21.333333 21.333333 0 0 1-0.384-29.269333l215.594666-235.178667z m-324.864-474.88l42.410666 4.906667a21.333333 21.333333 0 0 1 18.730667 23.637333L514.133333 965.12a21.333333 21.333333 0 0 1-23.637333 18.730667l-42.368-4.949334a21.333333 21.333333 0 0 1-18.773333-23.637333L531.2 80.213333a21.333333 21.333333 0 0 1 23.68-18.730666z" p-id="14661" fill="#FF8800"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.78395 12.7399C8.87529 12.6625 8.93678 12.5557 8.9578 12.4378L10.4173 3.95194C10.4294 3.87835 10.4253 3.803 10.4054 3.73113C10.3855 3.65926 10.3502 3.59258 10.3019 3.53572C10.2536 3.47887 10.1936 3.43318 10.1259 3.40185C10.0582 3.37051 9.98452 3.35427 9.90994 3.35425H8.04041C7.92032 3.35426 7.80413 3.39687 7.7125 3.4745C7.62088 3.55212 7.55976 3.65974 7.54002 3.77819L7.01877 6.78751H4.5168L5.01025 3.95194C5.02234 3.87835 5.01828 3.803 4.99836 3.73113C4.97843 3.65926 4.94311 3.59258 4.89485 3.53572C4.84658 3.47887 4.78653 3.43318 4.71885 3.40185C4.65117 3.37051 4.57748 3.35427 4.5029 3.35425H2.63337C2.51366 3.35562 2.39818 3.39872 2.30683 3.47611C2.21549 3.5535 2.154 3.66033 2.13298 3.77819L0.673494 12.264C0.6614 12.3376 0.665456 12.413 0.685383 12.4849C0.70531 12.5567 0.740631 12.6234 0.788895 12.6803C0.837159 12.7371 0.897213 12.7828 0.964892 12.8141C1.03257 12.8455 1.10626 12.8617 1.18084 12.8617H3.02257C3.14266 12.8617 3.25885 12.8191 3.35048 12.7415C3.4421 12.6639 3.50322 12.5563 3.52296 12.4378L4.07201 9.42848H6.57398L6.08053 12.264C6.06844 12.3376 6.0725 12.413 6.09243 12.4849C6.11235 12.5567 6.14767 12.6234 6.19594 12.6803C6.2442 12.7371 6.30425 12.7828 6.37193 12.8141C6.43961 12.8455 6.5133 12.8617 6.58788 12.8617H8.45741C8.57712 12.8604 8.6926 12.8173 8.78395 12.7399Z" fill="white"/>
<path d="M13.686 12.7378C13.7786 12.6617 13.8418 12.5555 13.8644 12.4378L14.6428 7.9134C14.6603 7.83959 14.6613 7.76282 14.6456 7.68859C14.6299 7.61437 14.598 7.54454 14.5522 7.48409C14.5064 7.42364 14.4477 7.37409 14.3805 7.33897C14.3133 7.30385 14.2391 7.28404 14.1633 7.28096H12.2938C12.1741 7.28233 12.0586 7.32543 11.9672 7.40282C11.8759 7.48021 11.8144 7.58704 11.7934 7.7049L10.9872 12.2571C10.9739 12.3303 10.9769 12.4055 10.996 12.4774C11.015 12.5493 11.0497 12.6161 11.0974 12.6731C11.1451 12.7302 11.2048 12.776 11.2723 12.8074C11.3397 12.8388 11.4132 12.855 11.4876 12.8548H13.3571C13.477 12.8553 13.5934 12.814 13.686 12.7378Z" fill="white"/>
<path opacity="0.5" d="M14.891 5.59802C14.9822 5.52202 15.0443 5.41679 15.0667 5.30021L15.3239 3.93802C15.3383 3.86434 15.3362 3.78837 15.3176 3.71561C15.2991 3.64284 15.2647 3.5751 15.2168 3.51725C15.1689 3.45941 15.1088 3.41291 15.0408 3.38112C14.9728 3.34932 14.8986 3.33301 14.8235 3.33338H12.9609C12.8428 3.33281 12.7282 3.37345 12.6369 3.4483C12.5455 3.52315 12.4832 3.62752 12.4605 3.74343L12.1964 5.11256C12.1833 5.18609 12.1863 5.2616 12.2054 5.33384C12.2244 5.40607 12.2589 5.4733 12.3065 5.53084C12.3541 5.58839 12.4137 5.63486 12.4811 5.66705C12.5485 5.69923 12.6221 5.71635 12.6968 5.71721H14.5663C14.6851 5.71614 14.7998 5.67403 14.891 5.59802Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.99967 2C1.63149 2 1.33301 2.29848 1.33301 2.66667V13.3333C1.33301 13.7015 1.63149 14 1.99967 14H13.9997C14.3679 14 14.6663 13.7015 14.6663 13.3333V2.66667C14.6663 2.29848 14.3679 2 13.9997 2H1.99967ZM13.333 3.33328V10.0001L11.9021 8.56907C11.7719 8.43893 11.5608 8.43893 11.4306 8.56907L9.56874 10.431C9.43854 10.5612 9.22747 10.5612 9.09727 10.431L5.56871 6.9024C5.43853 6.77227 5.22748 6.77227 5.09731 6.9024L2.66634 9.3334V3.33328H13.333Z" fill="white"/>
<path opacity="0.5" d="M10.333 5.33333C10.333 5.14924 10.4823 5 10.6663 5H11.6663C11.8504 5 11.9997 5.14924 11.9997 5.33333V6.33333C11.9997 6.51743 11.8504 6.66667 11.6663 6.66667H10.6663C10.4823 6.66667 10.333 6.51743 10.333 6.33333V5.33333Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 832 B

View File

@ -0,0 +1,4 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.8888 3.44443V5.22221H18.9999C19.4908 5.22221 19.8888 5.62018 19.8888 6.1111V9.22221H15.4443V8.33332H13.6666V9.22221H8.33324V8.33332H6.55546V9.22221H2.11102V6.1111C2.11102 5.62018 2.50898 5.22221 2.9999 5.22221H6.11102V3.44443C6.11102 2.95351 6.50898 2.55554 6.9999 2.55554H14.9999C15.4908 2.55554 15.8888 2.95351 15.8888 3.44443ZM7.88879 5.22221H14.111V4.33332H7.88879V5.22221Z" fill="white"/>
<path d="M2.11102 11H6.55546V11.8889H8.33324V11H13.6666V11.8889H15.4443V11H19.8888V18.5555C19.8888 19.0465 19.4908 19.4444 18.9999 19.4444H2.9999C2.50898 19.4444 2.11102 19.0465 2.11102 18.5555V11Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 764 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5892 18.5008L14.1666 15.4259H17.9166C18.3768 15.4259 18.7499 15.0449 18.7499 14.575V2.10096C18.7499 1.63099 18.3768 1.25 17.9166 1.25H1.66658C1.20635 1.25 0.833252 1.63099 0.833252 2.10096V14.575C0.833252 15.0449 1.20635 15.4259 1.66658 15.4259H5.83325L9.41066 18.5008C9.7361 18.8331 10.2637 18.8331 10.5892 18.5008ZM5.41658 7.08333H6.24992C6.71015 7.08333 7.08325 7.45643 7.08325 7.91667V8.75C7.08325 9.21024 6.71015 9.58333 6.24992 9.58333H5.41658C4.95635 9.58333 4.58325 9.21024 4.58325 8.75V7.91667C4.58325 7.45643 4.95635 7.08333 5.41658 7.08333ZM8.74992 7.91667C8.74992 7.45643 9.12301 7.08333 9.58325 7.08333H10.4166C10.8768 7.08333 11.2499 7.45643 11.2499 7.91667V8.75C11.2499 9.21024 10.8768 9.58333 10.4166 9.58333H9.58325C9.12301 9.58333 8.74992 9.21024 8.74992 8.75V7.91667ZM13.7499 7.08333H14.5832C15.0435 7.08333 15.4166 7.45643 15.4166 7.91667V8.75C15.4166 9.21024 15.0435 9.58333 14.5832 9.58333H13.7499C13.2897 9.58333 12.9166 9.21024 12.9166 8.75V7.91667C12.9166 7.45643 13.2897 7.08333 13.7499 7.08333Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,21 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.66676 5.22216C5.66676 4.24034 4.87081 3.44438 3.88898 3.44438C2.90714 3.44438 2.11121 4.24034 2.11121 5.22216C2.11121 6.20398 2.90714 6.99994 3.88898 6.99994C4.87081 6.99994 5.66676 6.20398 5.66676 5.22216Z"
fill="white" />
<path
d="M5.66676 16.7777C5.66676 15.7959 4.87081 14.9999 3.88898 14.9999C2.90714 14.9999 2.11121 15.7959 2.11121 16.7777C2.11121 17.7595 2.90714 18.5555 3.88898 18.5555C4.87081 18.5555 5.66676 17.7595 5.66676 16.7777Z"
fill="white" />
<path
d="M20.7778 10.9999C20.7778 9.52716 19.5839 8.33323 18.1111 8.33323C16.6384 8.33323 15.4445 9.52716 15.4445 10.9999C15.4445 12.4726 16.6384 13.6666 18.1111 13.6666C19.5839 13.6666 20.7778 12.4726 20.7778 10.9999Z"
fill="white" />
<path
d="M6.55562 4.77767C6.55562 3.30494 5.36169 2.111 3.88896 2.111C2.41622 2.111 1.22229 3.30494 1.22229 4.77767C1.22229 6.2504 2.41622 7.44434 3.88896 7.44434C5.36169 7.44434 6.55562 6.2504 6.55562 4.77767Z"
fill="white" />
<path
d="M6.55562 17.2221C6.55562 15.7494 5.36169 14.5555 3.88896 14.5555C2.41622 14.5555 1.22229 15.7494 1.22229 17.2221C1.22229 18.6949 2.41622 19.8888 3.88896 19.8888C5.36169 19.8888 6.55562 18.6949 6.55562 17.2221Z"
fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M4.77783 16.7777C4.77783 16.2868 5.1758 15.8888 5.66672 15.8888H7.00005C7.47797 15.8888 7.9427 15.6735 8.466 15.2097C9.00158 14.735 9.50472 14.0859 10.0733 13.3468C10.0826 13.3347 10.0919 13.3226 10.1012 13.3105C10.6367 12.6143 11.2356 11.8357 11.9104 11.2375C12.0023 11.156 12.0971 11.0765 12.195 10.9999C12.0971 10.9232 12.0023 10.8438 11.9104 10.7623C11.2356 10.1641 10.6367 9.38547 10.1012 8.68927L10.0733 8.65297C9.50472 7.91385 9.00158 7.26481 8.466 6.7901C7.9427 6.32626 7.47797 6.11101 7.00005 6.11101H5.66672C5.1758 6.11101 4.77783 5.71304 4.77783 5.22212C4.77783 4.7312 5.1758 4.33323 5.66672 4.33323H7.00005C8.07769 4.33323 8.94629 4.8402 9.64522 5.4597C10.3201 6.0579 10.919 6.83655 11.4545 7.53275L11.4824 7.56904C12.0509 8.30817 12.5541 8.95721 13.0897 9.43192C13.613 9.89576 14.0777 10.111 14.5556 10.111H16.3334C16.8243 10.111 17.2223 10.509 17.2223 10.9999C17.2223 11.4908 16.8243 11.8888 16.3334 11.8888H14.5556C14.0777 11.8888 13.613 12.104 13.0897 12.5679C12.5541 13.0426 12.0509 13.6916 11.4824 14.4308L11.4545 14.467C10.919 15.1632 10.3201 15.9419 9.64522 16.5401C8.94629 17.1596 8.07769 17.6666 7.00005 17.6666H5.66672C5.1758 17.6666 4.77783 17.2686 4.77783 16.7777Z"
fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.4409 232.4409"><defs><style>.cls-1{fill:#fff;}</style></defs><title>MaxKB</title><path class="cls-1" d="M128.4532,177H98.7785L87.78,187.9985a4.6069,4.6069,0,0,0,3.2576,7.8644h45.1569a4.6069,4.6069,0,0,0,3.2575-7.8644Z"/><path class="cls-1" d="M210.0008,90.7042h-5.85v41.1511h5.85a4.4537,4.4537,0,0,0,4.4537-4.4537V95.1579A4.4537,4.4537,0,0,0,210.0008,90.7042Z"/><path class="cls-1" d="M28.29,90.7042H22.44a4.4538,4.4538,0,0,0-4.4538,4.4537v32.2437a4.4538,4.4538,0,0,0,4.4538,4.4537h5.85Z"/><path class="cls-1" d="M138.8087,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,138.8087,96.1512Z"/><path class="cls-1" d="M95.3622,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,95.3622,96.1512Z"/><path class="cls-1" d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.50959 19.3955C6.84272 20.0622 5.93829 20.4368 4.99527 20.4367C4.05226 20.4366 3.1479 20.0619 2.48114 19.395C1.81439 18.7282 1.43986 17.8238 1.43994 16.8807C1.44002 15.9377 1.81472 15.0334 2.48159 14.3666L4.6407 12.207C3.70381 9.41949 4.34648 6.21772 6.56692 3.99594C8.78914 1.77372 11.9927 1.13238 14.7816 2.07105C14.9607 2.13105 15.1758 2.21994 15.426 2.33683C15.5488 2.39405 15.656 2.48004 15.7385 2.5875C15.821 2.69496 15.8764 2.82073 15.8999 2.95414C15.9235 3.08754 15.9145 3.22467 15.8738 3.35388C15.8331 3.48309 15.7618 3.60057 15.666 3.69638L11.9096 7.45283L14.4238 9.96705L18.1318 6.25905C18.2333 6.15762 18.3578 6.08226 18.4947 6.0394C18.6316 5.99653 18.7768 5.98744 18.918 6.0129C19.0592 6.03836 19.1921 6.09761 19.3054 6.1856C19.4187 6.27358 19.5091 6.38769 19.5687 6.51816C19.6825 6.76616 19.7683 6.97949 19.8265 7.15861C20.7345 9.93283 20.0856 13.1048 17.8807 15.3097C15.6594 17.5306 12.4571 18.1728 9.66914 17.2359L7.50959 19.395V19.3955Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H24C28.4183 0 32 3.58172 32 8V24C32 28.4183 28.4183 32 24 32H8C3.58172 32 0 28.4183 0 24V8Z" fill="#FF8800"/>
<g clip-path="url(#clip0_10330_2372)">
<path d="M22.5715 7.55565C22.7761 7.55565 22.9542 7.69533 23.0031 7.89401L26.3667 21.5792C26.4356 21.8593 26.2235 22.1297 25.9351 22.1297H24.9468C24.7483 22.1297 24.5739 21.9981 24.5194 21.8072L23.4149 17.9355H19.6078L19.0541 19.8045C18.1413 22.4957 15.4533 24.4445 12.2798 24.4445C9.32693 24.4445 6.79442 22.7572 5.71945 20.3549C5.69242 20.2945 5.66068 20.2159 5.62896 20.134C5.53599 19.8941 5.67535 19.6296 5.92345 19.5612C5.96516 19.5497 6.0032 19.5393 6.03397 19.5308C6.35482 19.4424 6.61238 19.3715 6.80666 19.318C7.02895 19.2568 7.25552 19.3785 7.34889 19.5893C8.14894 21.3957 10.0553 22.6668 12.2798 22.6668C14.5329 22.6668 16.4597 21.3628 17.2411 19.5193L17.317 19.3285L20.0773 7.89578C20.1255 7.69624 20.3041 7.55565 20.5094 7.55565H22.5715ZM12.2798 7.11121C14.9799 7.11121 17.1687 9.41058 17.1687 12.247V16.1976C17.1687 19.0341 14.9799 21.3334 12.2798 21.3334C9.57975 21.3334 7.39092 19.0341 7.39092 16.1976V12.247C7.39092 9.41058 9.57975 7.11121 12.2798 7.11121ZM21.5396 9.10632L19.9645 16.0903H23.1147L21.5396 9.10632Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_10330_2372">
<rect width="21.3333" height="21.3333" fill="white" transform="translate(5.33331 5.33337)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.185 14.7097C8.41034 15.273 9.21034 15.2667 9.42667 14.6997L10.8753 10.9044L14.701 9.42671C15.266 9.20838 15.2707 8.41072 14.7083 8.18572L5.28034 4.41438C4.736 4.19672 4.196 4.73705 4.41367 5.28105L8.185 14.7097Z" fill="white"/>
<path opacity="0.5" d="M8.33167 4.559C8.275 2.58367 6.65567 1 4.66667 1C2.64167 1 1 2.64167 1 4.66667C1 6.65467 2.58167 8.273 4.55567 8.33167L3.98233 6.898C3.59753 6.77974 3.24987 6.56394 2.97314 6.27157C2.69642 5.9792 2.50004 5.6202 2.4031 5.22948C2.30617 4.83876 2.31196 4.42961 2.41993 4.0418C2.52791 3.65398 2.73438 3.3007 3.01928 3.01629C3.30418 2.73187 3.65781 2.52601 4.04581 2.4187C4.43381 2.3114 4.84298 2.3063 5.23353 2.40391C5.62408 2.50152 5.98274 2.69851 6.27463 2.97574C6.56653 3.25296 6.78174 3.60099 6.89933 3.986L8.33167 4.559Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.99998 1.33334H6.99998C7.18407 1.33334 7.33331 1.48258 7.33331 1.66668V2.33334C7.33331 2.51744 7.18407 2.66668 6.99998 2.66668H2.66665V6.33334C2.66665 6.51744 2.51741 6.66668 2.33331 6.66668H1.66665C1.48255 6.66668 1.33331 6.51744 1.33331 6.33334V2.00001C1.33331 1.63182 1.63179 1.33334 1.99998 1.33334ZM14 14.6667H9.66665C9.48255 14.6667 9.33331 14.5174 9.33331 14.3333V13.6667C9.33331 13.4826 9.48255 13.3333 9.66665 13.3333H13.3333V9.66668C13.3333 9.48258 13.4826 9.33334 13.6666 9.33334H14.3333C14.5174 9.33334 14.6666 9.48258 14.6666 9.66668V14C14.6666 14.3682 14.3682 14.6667 14 14.6667Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7033 1.48695C10.7776 1.29397 10.9631 1.16666 11.1699 1.16666H11.87C12.0768 1.16666 12.2623 1.29397 12.3366 1.48695L14.6475 7.48695C14.7737 7.81448 14.5319 8.16666 14.1809 8.16666H13.8107C13.5981 8.16666 13.4088 8.0323 13.3387 7.83167L12.7962 6.28018C12.7729 6.21331 12.7098 6.16852 12.6389 6.16852H10.468C10.3995 6.16852 10.3379 6.21049 10.3129 6.27429L9.69462 7.84935C9.61948 8.04076 9.43482 8.16666 9.22919 8.16666H8.85893C8.50794 8.16666 8.26619 7.81448 8.39234 7.48695L10.7033 1.48695ZM11.5194 2.85876L10.7668 4.90148H12.2656L11.5194 2.85876Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.33331 14.3333C1.33331 14.5174 1.48255 14.6667 1.66665 14.6667H7.66665C7.85074 14.6667 7.99998 14.5174 7.99998 14.3333V8.33333C7.99998 8.14924 7.85074 8 7.66665 8H1.66665C1.48255 8 1.33331 8.14924 1.33331 8.33333V14.3333ZM2.83332 9.33333H3.49998C3.59203 9.33333 3.66665 9.40795 3.66665 9.5V10.1667C3.66665 10.2587 3.59203 10.3333 3.49998 10.3333H2.83332C2.74127 10.3333 2.66665 10.2587 2.66665 10.1667V9.5C2.66665 9.40795 2.74127 9.33333 2.83332 9.33333ZM4.04698 13.3294L2.62187 13.326C2.56665 13.3259 2.52198 13.281 2.52211 13.2258C2.52218 13.1993 2.53271 13.174 2.5514 13.1553L3.92901 11.7777C3.98108 11.7256 4.0655 11.7256 4.11757 11.7777L4.75326 12.4134L6.43904 10.7276C6.49111 10.6755 6.57553 10.6755 6.6276 10.7276C6.6526 10.7526 6.66665 10.7865 6.66665 10.8219V13.2333C6.66665 13.2886 6.62188 13.3333 6.56665 13.3333H4.07474C4.06511 13.3333 4.05579 13.332 4.04698 13.3294Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H24C28.4183 0 32 3.58172 32 8V24C32 28.4183 28.4183 32 24 32H8C3.58172 32 0 28.4183 0 24V8Z" fill="#14C0FF"/>
<g clip-path="url(#clip0_10330_2180)">
<path d="M9.42854 7.55565C9.22395 7.55565 9.04578 7.69533 8.99695 7.89401L5.63329 21.5792C5.56444 21.8593 5.77647 22.1297 6.06488 22.1297H7.05321C7.25171 22.1297 7.42615 21.9981 7.4806 21.8072L8.5851 17.9355H12.3922L12.9459 19.8045C13.8587 22.4957 16.5467 24.4445 19.7202 24.4445C22.6731 24.4445 25.2056 22.7572 26.2805 20.3549C26.3076 20.2945 26.3393 20.2159 26.371 20.134C26.464 19.8941 26.3246 19.6296 26.0765 19.5612C26.0348 19.5497 25.9968 19.5393 25.966 19.5308C25.6452 19.4424 25.3876 19.3715 25.1933 19.318C24.9711 19.2568 24.7445 19.3785 24.6511 19.5893C23.8511 21.3957 21.9447 22.6668 19.7202 22.6668C17.4671 22.6668 15.5403 21.3628 14.7589 19.5193L14.683 19.3285L11.9227 7.89578C11.8745 7.69624 11.6959 7.55565 11.4906 7.55565H9.42854ZM19.7202 7.11121C17.0201 7.11121 14.8313 9.41058 14.8313 12.247V16.1976C14.8313 19.0341 17.0201 21.3334 19.7202 21.3334C22.4203 21.3334 24.6091 19.0341 24.6091 16.1976V12.247C24.6091 9.41058 22.4203 7.11121 19.7202 7.11121ZM10.4604 9.10632L12.0355 16.0903H8.88529L10.4604 9.10632Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_10330_2180">
<rect width="21.3333" height="21.3333" fill="white" transform="matrix(-1 0 0 1 26.6667 5.33337)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6260_56998)">
<path d="M8.45989 5.8569L8.57477 5.86299C8.79559 5.87481 8.96834 6.05752 8.96834 6.27866V6.82427C8.96834 7.07408 8.75005 7.26525 8.50052 7.25364C8.42661 7.2502 8.35257 7.24833 8.27834 7.24833C8.06408 7.24833 7.92822 7.30022 7.84948 7.38496C7.77816 7.4623 7.73167 7.61024 7.73167 7.85333V8.13167H8.55167C8.78179 8.13167 8.96834 8.31821 8.96834 8.54833V9.38881C8.96834 9.61893 8.78179 9.80547 8.55167 9.80547H7.73167V13.9133C7.73167 14.1435 7.54512 14.33 7.31501 14.33H6.32501C6.09489 14.33 5.90834 14.1435 5.90834 13.9133V9.80547H5.13859C4.90847 9.80547 4.72192 9.61893 4.72192 9.38881V8.54833C4.72192 8.31821 4.90847 8.13167 5.13859 8.13167H5.90834V7.74833C5.90834 7.19278 6.08218 6.73149 6.43374 6.37176C6.78941 6.00781 7.32033 5.83333 8.01 5.83333C8.15999 5.83333 8.30995 5.84119 8.45989 5.8569Z" fill="white"/>
<path d="M12.4626 9.47701L11.5695 8.15211C11.492 8.03721 11.3625 7.96833 11.224 7.96833H10.1483C9.81198 7.96833 9.61424 8.34622 9.80596 8.62253L11.3934 10.9103L9.61243 13.5148C9.42332 13.7913 9.62135 14.1667 9.95637 14.1667H11.0006C11.1403 14.1667 11.2708 14.0966 11.3479 13.9801L12.4396 12.3325L13.5313 13.9801C13.6085 14.0966 13.7389 14.1667 13.8786 14.1667H14.9522C15.2886 14.1667 15.4864 13.7886 15.2945 13.5123L13.4629 10.8753L15.044 8.62451C15.238 8.34841 15.0405 7.96833 14.7031 7.96833H13.6913C13.552 7.96833 13.4219 8.03796 13.3446 8.15387L12.4626 9.47701Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9226 2.74408C18.7663 2.5878 18.5543 2.5 18.3333 2.5H1.66665C1.44563 2.5 1.23367 2.5878 1.07739 2.74408C0.92111 2.90036 0.833313 3.11232 0.833313 3.33333V16.6667C0.833313 16.8877 0.92111 17.0996 1.07739 17.2559C1.23367 17.4122 1.44563 17.5 1.66665 17.5H18.3333C18.5543 17.5 18.7663 17.4122 18.9226 17.2559C19.0788 17.0996 19.1666 16.8877 19.1666 16.6667V3.33333C19.1666 3.11232 19.0788 2.90036 18.9226 2.74408ZM2.49998 4.16667H17.5V15.8333H2.49998V4.16667Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_6260_56998">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,760 @@
<template>
<el-dialog
class="execution-details-dialog responsive-dialog"
:title="$t('chat.executionDetails.title')"
v-model="dialogVisible"
destroy-on-close
append-to-body
align-center
@click.stop
>
<el-scrollbar>
<div class="execution-details">
<template v-for="(item, index) in arraySort(detail, 'index')" :key="index">
<el-card class="mb-8" shadow="never" style="--el-card-padding: 12px 16px">
<div class="flex-between cursor" @click="current = current === index ? '' : index">
<div class="flex align-center">
<el-icon class="mr-8 arrow-icon" :class="current === index ? 'rotate-90' : ''">
<CaretRight />
</el-icon>
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="24"
:item="item.info"
/>
<h4>{{ item.name }}</h4>
</div>
<div class="flex align-center">
<span
class="mr-16 color-secondary"
v-if="
item.type === WorkflowType.Question ||
item.type === WorkflowType.AiChat ||
item.type === WorkflowType.ImageUnderstandNode ||
item.type === WorkflowType.ImageGenerateNode ||
item.type === WorkflowType.Application
"
>{{ item?.message_tokens + item?.answer_tokens }} tokens</span
>
<span class="mr-16 color-secondary">{{ item?.run_time?.toFixed(2) || 0.0 }} s</span>
<el-icon class="success" :size="16" v-if="item.status === 200">
<CircleCheck />
</el-icon>
<el-icon class="danger" :size="16" v-else>
<CircleClose />
</el-icon>
</div>
</div>
<el-collapse-transition>
<div class="mt-12" v-if="current === index">
<template v-if="item.status === 200">
<!-- 开始 -->
<template
v-if="
item.type === WorkflowType.Start || item.type === WorkflowType.Application
"
>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div class="mb-8">
<span class="color-secondary">
{{ $t('chat.paragraphSource.question') }}:</span
>
{{ item.question || '-' }}
</div>
<div v-for="(f, i) in item.global_fields" :key="i" class="mb-8">
<span class="color-secondary">{{ f.label }}:</span> {{ f.value }}
</div>
<div v-if="item.document_list?.length > 0">
<p class="mb-8 color-secondary">
{{ $t('common.fileUpload.document') }}:
</p>
<el-space wrap>
<template v-for="(f, i) in item.document_list" :key="i">
<el-card
shadow="never"
style="--el-card-padding: 8px"
class="file cursor"
>
<div class="flex align-center">
<img :src="getImgUrl(f && f?.name)" alt="" width="24" />
<div class="ml-4 ellipsis" :title="f && f?.name">
{{ f && f?.name }}
</div>
</div>
</el-card>
</template>
</el-space>
</div>
<div v-if="item.image_list?.length > 0">
<p class="mb-8 color-secondary">{{ $t('common.fileUpload.image') }}:</p>
<el-space wrap>
<template v-for="(f, i) in item.image_list" :key="i">
<el-image
:src="f.url"
alt=""
fit="cover"
style="width: 40px; height: 40px; display: block"
class="border-r-4"
/>
</template>
</el-space>
</div>
<div v-if="item.audio_list?.length > 0">
<p class="mb-8 color-secondary">
{{ $t('chat.executionDetails.audioFile') }}:
</p>
<el-space wrap>
<template v-for="(f, i) in item.audio_list" :key="i">
<audio
:src="f.url"
controls
style="width: 300px; height: 43px"
class="border-r-4"
/>
</template>
</el-space>
</div>
<div v-if="item.other_list?.length > 0">
<p class="mb-8 color-secondary">
{{ $t('common.fileUpload.document') }}:
</p>
<el-space wrap>
<template v-for="(f, i) in item.other_list" :key="i">
<el-card
shadow="never"
style="--el-card-padding: 8px"
class="file cursor"
>
<div class="flex align-center">
<img :src="getImgUrl(f && f?.name)" alt="" width="24" />
<div class="ml-4 ellipsis" :title="f && f?.name">
{{ f && f?.name }}
</div>
</div>
</el-card>
</template>
</el-space>
</div>
</div>
</div>
</template>
<!-- 知识库检索 -->
<template v-if="item.type == WorkflowType.SearchDataset">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.searchContent') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">{{ item.question || '-' }}</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.searchResult') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<template v-if="item.paragraph_list?.length > 0">
<template
v-for="(paragraph, paragraphIndex) in arraySort(
item.paragraph_list,
'similarity',
true
)"
:key="paragraphIndex"
>
<ParagraphCard
:data="paragraph"
:content="paragraph.content"
:index="paragraphIndex"
/>
</template>
</template>
<template v-else> -</template>
</div>
</div>
</template>
<!-- 判断器 -->
<template v-if="item.type == WorkflowType.Condition">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.conditionResult') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
{{ item.branch_name || '-' }}
</div>
</div>
</template>
<!-- AI 对话 / 问题优化-->
<template
v-if="
item.type == WorkflowType.AiChat ||
item.type == WorkflowType.Question ||
item.type == WorkflowType.Application
"
>
<div
class="card-never border-r-4"
v-if="item.type !== WorkflowType.Application"
>
<h5 class="p-8-12">
{{ $t('views.application.applicationForm.form.roleSettings.label') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
{{ item.system || '-' }}
</div>
</div>
<div
class="card-never border-r-4 mt-8"
v-if="item.type !== WorkflowType.Application"
>
<h5 class="p-8-12">{{ $t('chat.history') }}</h5>
<div class="p-8-12 border-t-dashed lighter">
<template v-if="item.history_message?.length > 0">
<p
class="mt-4 mb-4"
v-for="(history, historyIndex) in item.history_message"
:key="historyIndex"
>
<span class="color-secondary mr-4">{{ history.role }}:</span
><span>{{ history.content }}</span>
</p>
</template>
<template v-else> -</template>
</div>
</div>
<div
class="card-never border-r-4 mt-8"
v-if="item.type !== WorkflowType.Application"
>
<h5 class="p-8-12">
{{ $t('chat.executionDetails.currentChat') }}
</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.question || '-' }}
</div>
</div>
<div class="card-never border-r-4 mt-8" v-if="item.type == WorkflowType.AiChat">
<h5 class="p-8-12">
{{ $t('views.applicationWorkflow.nodes.aiChatNode.think') }}
</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.reasoning_content || '-' }}
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{
item.type == WorkflowType.Application
? $t('common.param.outputParam')
: $t('chat.executionDetails.answer')
}}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<MdPreview
v-if="item.answer"
ref="editorRef"
editorId="preview-only"
:modelValue="item.answer"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</div>
</div>
</template>
<!-- 指定回复 -->
<template v-if="item.type === WorkflowType.Reply">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.replyContent') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<el-scrollbar height="150">
<MdPreview
v-if="item.answer"
ref="editorRef"
editorId="preview-only"
:modelValue="item.answer"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</el-scrollbar>
</div>
</div>
</template>
<!-- 文档内容提取 -->
<template v-if="item.type === WorkflowType.DocumentExtractNode">
<div class="card-never border-r-4">
<h5 class="p-8-12 flex align-center">
<span class="mr-4"> {{ $t('common.param.outputParam') }}</span>
<el-tooltip
effect="dark"
:content="$t('chat.executionDetails.paramOutputTooltip')"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</h5>
<div class="p-8-12 border-t-dashed lighter">
<el-scrollbar height="150">
<el-card
shadow="never"
style="--el-card-padding: 8px"
v-for="(file_content, index) in item.content"
:key="index"
class="mb-8"
>
<MdPreview
v-if="file_content"
ref="editorRef"
editorId="preview-only"
:modelValue="file_content"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</el-card>
</el-scrollbar>
</div>
</div>
</template>
<template v-if="item.type === WorkflowType.SpeechToTextNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div class="mb-8">
<div v-if="item.audio_list?.length > 0">
<p class="mb-8 color-secondary">
{{ $t('chat.executionDetails.audioFile') }}:
</p>
<el-space wrap>
<template v-for="(f, i) in item.audio_list" :key="i">
<audio
:src="f.url"
controls
style="width: 300px; height: 43px"
class="border-r-4"
/>
</template>
</el-space>
</div>
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<el-card
shadow="never"
style="--el-card-padding: 8px"
v-for="(file_content, index) in item.content"
:key="index"
class="mb-8"
>
<MdPreview
v-if="file_content"
ref="editorRef"
editorId="preview-only"
:modelValue="file_content"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</el-card>
</div>
</div>
</template>
<template v-if="item.type === WorkflowType.TextToSpeechNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div class="p-8-12 border-t-dashed lighter">
<p class="mb-8 color-secondary">
{{ $t('chat.executionDetails.textContent') }}:
</p>
<div v-if="item.content">
<MdPreview
ref="editorRef"
editorId="preview-only"
:modelValue="item.content"
style="background: none"
noImgZoomIn
/>
</div>
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<p class="mb-8 color-secondary">
{{ $t('chat.executionDetails.audioFile') }}:
</p>
<div v-if="item.answer" v-html="item.answer"></div>
</div>
</div>
</template>
<!-- 函数库 -->
<template
v-if="
item.type === WorkflowType.FunctionLib ||
item.type === WorkflowType.FunctionLibCustom
"
>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">{{ $t('chat.executionDetails.input') }}</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.params || '-' }}
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">{{ $t('chat.executionDetails.output') }}</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.result || '-' }}
</div>
</div>
</template>
<!-- 多路召回 -->
<template v-if="item.type == WorkflowType.RrerankerNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.searchContent') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">{{ item.question || '-' }}</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.rerankerContent') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<template v-if="item.document_list?.length > 0">
<template
v-for="(paragraph, paragraphIndex) in item.document_list"
:key="paragraphIndex"
>
<ParagraphCard
:data="paragraph.metadata"
:content="paragraph.page_content"
:index="paragraphIndex"
/>
</template>
</template>
<template v-else> -</template>
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.rerankerResult') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<template v-if="item.result_list?.length > 0">
<template
v-for="(paragraph, paragraphIndex) in item.result_list"
:key="paragraphIndex"
>
<ParagraphCard
:data="paragraph.metadata"
:content="paragraph.page_content"
:index="paragraphIndex"
:score="paragraph.metadata?.relevance_score"
/>
</template>
</template>
<template v-else> -</template>
</div>
</div>
</template>
<!-- 表单收集 -->
<template v-if="item.type === WorkflowType.FormNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam')
}}<span style="color: #f54a45">{{
item.is_submit ? '' : `(${$t('chat.executionDetails.noSubmit')})`
}}</span>
</h5>
<div class="p-8-12 border-t-dashed lighter">
<DynamicsForm
:disabled="true"
label-position="top"
require-asterisk-position="right"
ref="dynamicsFormRef"
:render_data="item.form_field_list"
label-suffix=":"
v-model="item.form_data"
:model="item.form_data"
></DynamicsForm>
</div>
</div>
</template>
<!-- 图片理解 -->
<template v-if="item.type == WorkflowType.ImageUnderstandNode">
<div
class="card-never border-r-4"
v-if="item.type !== WorkflowType.Application"
>
<h5 class="p-8-12">
{{ $t('views.application.applicationForm.form.roleSettings.label') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
{{ item.system || '-' }}
</div>
</div>
<div
class="card-never border-r-4 mt-8"
v-if="item.type !== WorkflowType.Application"
>
<h5 class="p-8-12">{{ $t('chat.history') }}</h5>
<div class="p-8-12 border-t-dashed lighter">
<template v-if="item.history_message?.length > 0">
<p
class="mt-4 mb-4"
v-for="(history, historyIndex) in item.history_message"
:key="historyIndex"
>
<span class="color-secondary mr-4">{{ history.role }}:</span>
<span v-if="Array.isArray(history.content)">
<template v-for="(h, i) in history.content" :key="i">
<el-image
v-if="h.type === 'image_url'"
:src="h.image_url.url"
alt=""
fit="cover"
style="width: 40px; height: 40px; display: inline-block"
class="border-r-4 mr-8"
/>
<span v-else>{{ h.text }}<br /></span>
</template>
</span>
<span v-else>{{ history.content }}</span>
</p>
</template>
<template v-else> -</template>
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.currentChat') }}
</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
<div v-if="item.image_list?.length > 0">
<el-space wrap>
<template v-for="(f, i) in item.image_list" :key="i">
<el-image
:src="f.url"
alt=""
fit="cover"
style="width: 40px; height: 40px; display: block"
class="border-r-4"
/>
</template>
</el-space>
</div>
<div>
{{ item.question || '-' }}
</div>
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{
item.type == WorkflowType.Application
? $t('common.param.outputParam')
: $t('chat.executionDetails.answer')
}}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<MdPreview
v-if="item.answer"
ref="editorRef"
editorId="preview-only"
:modelValue="item.answer"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</div>
</div>
</template>
<!-- 图片生成 -->
<template v-if="item.type == WorkflowType.ImageGenerateNode">
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{ $t('chat.executionDetails.currentChat') }}
</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.question || '-' }}
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">
{{
item.type == WorkflowType.Application
? $t('common.param.outputParam')
: $t('chat.executionDetails.answer')
}}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<MdPreview
v-if="item.answer"
ref="editorRef"
editorId="preview-only"
:modelValue="item.answer"
style="background: none"
noImgZoomIn
/>
<template v-else> -</template>
</div>
</div>
</template>
<!-- 变量赋值 -->
<template v-if="item.type === WorkflowType.VariableAssignNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(f, i) in item.result_list" :key="i" class="mb-8">
<span class="color-secondary">{{ f.name }}:</span> {{ f.input_value }}
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(f, i) in item.result_list" :key="i" class="mb-8">
<span class="color-secondary">{{ f.name }}:</span> {{ f.output_value }}
</div>
</div>
</div>
</template>
<!-- MCP 节点 -->
<template v-if="item.type === WorkflowType.McpNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div class="mb-8">
<span class="color-secondary"> {{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}: </span> {{ item.mcp_tool }}
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(value, name) in item.tool_params" :key="name" class="mb-8">
<span class="color-secondary">{{ name }}:</span> {{ value }}
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(f, i) in item.result" :key="i" class="mb-8">
<span class="color-secondary">result:</span> {{ f }}
</div>
</div>
</div>
</template>
</template>
<template v-else>
<div class="card-never border-r-4">
<h5 class="p-8-12">{{ $t('chat.executionDetails.errMessage') }}</h5>
<div class="p-8-12 border-t-dashed lighter">{{ item.err_message || '-' }}</div>
</div>
</template>
</div>
</el-collapse-transition>
</el-card>
</template>
</div>
</el-scrollbar>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
import { cloneDeep } from 'lodash'
import ParagraphCard from './component/ParagraphCard.vue'
import { arraySort } from '@/utils/utils'
import { iconComponent } from '@/workflow/icons/utils'
import { WorkflowType } from '@/enums/workflow'
import { getImgUrl } from '@/utils/utils'
import DynamicsForm from '@/components/dynamics-form/index.vue'
const dialogVisible = ref(false)
const detail = ref<any[]>([])
const current = ref<number | string>('')
watch(dialogVisible, (bool) => {
if (!bool) {
detail.value = []
}
})
const open = (data: any) => {
detail.value = cloneDeep(data)
dialogVisible.value = true
}
onBeforeUnmount(() => {
dialogVisible.value = false
})
defineExpose({ open })
</script>
<style lang="scss">
.execution-details-dialog {
.el-dialog__header {
padding-bottom: 16px;
}
.execution-details {
max-height: calc(100vh - 260px);
.arrow-icon {
transition: 0.2s;
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div class="chat-knowledge-source">
<div class="flex align-center mt-16" v-if="!isWorkFlow(props.type)">
<span class="mr-4 color-secondary">{{ $t('chat.KnowledgeSource.title') }}</span>
<el-divider direction="vertical" />
<el-button type="primary" class="mr-8" link @click="openParagraph(data)">
<AppIcon iconName="app-reference-outlined" class="mr-4"></AppIcon>
{{ $t('chat.KnowledgeSource.referenceParagraph') }}
{{ data.paragraph_list?.length || 0 }}</el-button
>
</div>
<div class="mt-8" v-if="!isWorkFlow(props.type)">
<el-row :gutter="8" v-if="uniqueParagraphList?.length">
<template v-for="(item, index) in uniqueParagraphList" :key="index">
<el-col :span="12" class="mb-8">
<el-card shadow="never" class="file-List-card" data-width="40">
<div class="flex-between">
<div class="flex">
<img :src="getImgUrl(item && item?.document_name)" alt="" width="20" />
<div class="ml-4 ellipsis-1" :title="item?.document_name" v-if="!item.source_url">
<p>{{ item && item?.document_name }}</p>
</div>
<div class="ml-8" v-else>
<a
:href="getNormalizedUrl(item?.source_url)"
target="_blank"
class="ellipsis"
:title="item?.document_name?.trim()"
>
<span :title="item?.document_name?.trim()">{{ item?.document_name }}</span>
</a>
</div>
</div>
</div>
</el-card>
</el-col>
</template>
</el-row>
</div>
<div
class="execution-details border-t color-secondary flex-between mt-12"
style="padding-top: 12px; padding-bottom: 8px"
>
<div>
<span class="mr-8">
{{ $t('chat.KnowledgeSource.consume') }}: {{ data?.message_tokens + data?.answer_tokens }}
</span>
<span>
{{ $t('chat.KnowledgeSource.consumeTime') }}: {{ data?.run_time?.toFixed(2) }} s</span
>
</div>
<el-button
v-if="isWorkFlow(props.type)"
type="primary"
link
@click="openExecutionDetail(data.execution_details)"
style="padding: 0;"
>
<el-icon class="mr-4"><Document /></el-icon>
{{ $t('chat.executionDetails.title') }}</el-button
>
</div>
<!-- 知识库引用 dialog -->
<ParagraphSourceDialog ref="ParagraphSourceDialogRef" />
<!-- 执行详情 dialog -->
<ExecutionDetailDialog ref="ExecutionDetailDialogRef" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import ParagraphSourceDialog from './ParagraphSourceDialog.vue'
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
import { isWorkFlow } from '@/utils/application'
import { getImgUrl, getNormalizedUrl } from '@/utils/utils'
const props = defineProps({
data: {
type: Object,
default: () => {}
},
type: {
type: String,
default: ''
}
})
const ParagraphSourceDialogRef = ref()
const ExecutionDetailDialogRef = ref()
function openParagraph(row: any, id?: string) {
ParagraphSourceDialogRef.value.open(row, id)
}
function openExecutionDetail(row: any) {
ExecutionDetailDialogRef.value.open(row)
}
const uniqueParagraphList = computed(() => {
const seen = new Set()
return (
props.data.paragraph_list?.filter((paragraph: any) => {
const key = paragraph.document_name.trim()
if (seen.has(key)) {
return false
}
seen.add(key)
// meta {} json
if (paragraph.meta && typeof paragraph.meta === 'string') {
paragraph.meta = JSON.parse(paragraph.meta)
paragraph.source_url = paragraph.meta.source_url
}
return true
}) || []
)
})
</script>
<style lang="scss" scoped>
@media only screen and (max-width: 420px) {
.chat-knowledge-source {
.execution-details {
display: block;
}
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<el-dialog
class="paragraph-source responsive-dialog"
:title="$t('chat.paragraphSource.title')"
v-model="dialogVisible"
destroy-on-close
append-to-body
align-center
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="mb-8">
<el-scrollbar>
<div class="paragraph-source-height p-16 pb-0">
<el-form label-position="top">
<el-form-item :label="$t('chat.paragraphSource.question')">
<el-input v-model="detail.problem_text" disabled />
</el-form-item>
<el-form-item :label="$t('chat.paragraphSource.optimizationQuestion')">
<el-input v-model="detail.padding_problem_text" disabled />
</el-form-item>
<el-form-item :label="$t('chat.KnowledgeSource.referenceParagraph')">
<div v-if="detail.paragraph_list.length > 0" class="w-full">
<template v-for="(item, index) in detail.paragraph_list" :key="index">
<ParagraphCard :data="item" :content="item.content" :index="index" />
</template>
</div>
<span v-else> - </span>
</el-form-item>
</el-form>
</div>
</el-scrollbar>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
import { cloneDeep } from 'lodash'
import { arraySort } from '@/utils/utils'
import ParagraphCard from './component/ParagraphCard.vue'
const emit = defineEmits(['refresh'])
const dialogVisible = ref(false)
const detail = ref<any>({})
watch(dialogVisible, (bool) => {
if (!bool) {
detail.value = {}
}
})
const open = (data: any, id?: string) => {
detail.value = cloneDeep(data)
detail.value.paragraph_list = id
? detail.value.paragraph_list.filter((v: any) => v.dataset_id === id)
: detail.value.paragraph_list
detail.value.paragraph_list = arraySort(detail.value.paragraph_list, 'similarity', true)
dialogVisible.value = true
}
onBeforeUnmount(() => {
dialogVisible.value = false
})
defineExpose({ open })
</script>
<style lang="scss">
.paragraph-source {
padding: 0;
.el-dialog__header {
padding: 24px 24px 0 24px;
}
.el-dialog__body {
padding: 8px !important;
}
.paragraph-source-height {
max-height: calc(100vh - 260px);
}
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<CardBox
shadow="never"
:title="data.title || '-'"
class="paragraph-source-card cursor mb-8 paragraph-source-card-height"
:class="data.is_active ? '' : 'disabled'"
:showIcon="false"
>
<template #icon>
<AppAvatar class="mr-12 avatar-light" :size="22"> {{ index + 1 + '' }}</AppAvatar>
</template>
<div class="active-button primary">{{ score?.toFixed(3) || data.similarity?.toFixed(3) }}</div>
<template #description>
<el-scrollbar height="150">
<MdPreview ref="editorRef" editorId="preview-only" :modelValue="content" noImgZoomIn />
</el-scrollbar>
</template>
<template #footer>
<div class="footer-content flex-between">
<el-text class="flex align-center item">
<img :src="getImgUrl(data?.document_name?.trim())" alt="" width="20" class="mr-4" />
<template v-if="meta?.source_url">
<a
:href="getNormalizedUrl(meta?.source_url)"
target="_blank"
class="ellipsis-1 break-all"
:title="data?.document_name?.trim()"
>
{{ data?.document_name?.trim() }}
</a>
</template>
<template v-else>
<span class="ellipsis-1 break-all" :title="data?.document_name?.trim()">
{{ data?.document_name?.trim() }}
</span>
</template>
</el-text>
<div class="flex align-center item" style="line-height: 32px">
<AppAvatar class="mr-8 avatar-blue" shape="square" :size="18">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<span class="ellipsis-1 break-all" :title="data?.dataset_name">
{{ data?.dataset_name }}</span
>
</div>
</div>
</template>
</CardBox>
</template>
<script setup lang="ts">
import { getImgUrl, getNormalizedUrl } from '@/utils/utils'
import { computed } from 'vue'
const props = defineProps({
data: {
type: Object,
default: () => {}
},
content: {
type: String,
default: ''
},
index: {
type: Number,
default: 0
},
score: {
type: Number,
default: null
}
})
const isMetaObject = computed(() => typeof props.data.meta === 'object')
const parsedMeta = computed(() => {
try {
return JSON.parse(props.data.meta)
} catch (e) {
return {}
}
})
const meta = computed(() => (isMetaObject.value ? props.data.meta : parsedMeta.value))
</script>
<style lang="scss" scoped>
.paragraph-source-card {
.footer-content {
.item {
max-width: 50%;
}
}
}
.paragraph-source-card-height {
height: 260px;
}
@media only screen and (max-width: 768px) {
.paragraph-source-card-height {
height: 285px;
}
.paragraph-source-card {
.footer-content {
display: block;
.item {
max-width: 100%;
}
}
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<div class="item-content mb-16 lighter">
<template v-for="(answer_text, index) in answer_text_list" :key="index">
<div class="avatar mr-8" v-if="showAvatar">
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px" />
<LogoIcon v-else height="28px" width="28px" />
</div>
<div
class="content"
@mouseup="openControl"
:style="{
'padding-right': showUserAvatar ? 'var(--padding-left)' : '0'
}"
>
<el-card shadow="always" class="mb-8 border-r-8" style="--el-card-padding: 6px 16px">
<MdRenderer
v-if="
(chatRecord.write_ed === undefined || chatRecord.write_ed === true) &&
answer_text.length == 0
"
:source="$t('chat.tip.answerMessage')"
></MdRenderer>
<template v-else-if="answer_text.length > 0">
<MdRenderer
v-for="(answer, index) in answer_text"
:key="index"
:chat_record_id="answer.chat_record_id"
:child_node="answer.child_node"
:runtime_node_id="answer.runtime_node_id"
:reasoning_content="answer.reasoning_content"
:disabled="loading || type == 'log'"
:source="answer.content"
:send-message="chatMessage"
></MdRenderer>
</template>
<p v-else-if="chatRecord.is_stop" shadow="always" style="margin: 0.5rem 0">
{{ $t('chat.tip.stopAnswer') }}
</p>
<p v-else shadow="always" style="margin: 0.5rem 0">
{{ $t('chat.tip.answerLoading') }} <span class="dotting"></span>
</p>
<!-- 知识来源 -->
<KnowledgeSource
:data="chatRecord"
:type="application.type"
v-if="showSource(chatRecord) && index === chatRecord.answer_text_list.length - 1"
/>
</el-card>
</div>
</template>
<div
class="content"
:style="{
'padding-left': showAvatar ? 'var(--padding-left)' : '0',
'padding-right': showUserAvatar ? 'var(--padding-left)' : '0'
}"
>
<OperationButton
:type="type"
:application="application"
:chatRecord="chatRecord"
@update:chatRecord="(event: any) => emit('update:chatRecord', event)"
:loading="loading"
:start-chat="startChat"
:stop-chat="stopChat"
:regenerationChart="regenerationChart"
></OperationButton>
</div>
</div>
</template>
<script setup lang="ts">
import KnowledgeSource from '@/components/ai-chat/KnowledgeSource.vue'
import MdRenderer from '@/components/markdown/MdRenderer.vue'
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
import { type chatType } from '@/api/type/application'
import { computed } from 'vue'
import bus from '@/bus'
import useStore from '@/stores'
const props = defineProps<{
chatRecord: chatType
application: any
loading: boolean
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => Promise<boolean>
chatManagement: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
}>()
const { user } = useStore()
const emit = defineEmits(['update:chatRecord'])
const showAvatar = computed(() => {
return user.isEnterprise() ? props.application.show_avatar : true
})
const showUserAvatar = computed(() => {
return user.isEnterprise() ? props.application.show_user_avatar : true
})
const chatMessage = (question: string, type: 'old' | 'new', other_params_data?: any) => {
if (type === 'old') {
add_answer_text_list(props.chatRecord.answer_text_list)
props.sendMessage(question, other_params_data, props.chatRecord).then(() => {
props.chatManagement.open(props.chatRecord.id)
props.chatManagement.write(props.chatRecord.id)
})
} else {
props.sendMessage(question, other_params_data)
}
}
const add_answer_text_list = (answer_text_list: Array<any>) => {
answer_text_list.push([])
}
const openControl = (event: any) => {
if (props.type !== 'log') {
bus.emit('open-control', event)
}
}
const answer_text_list = computed(() => {
return props.chatRecord.answer_text_list.map((item) => {
if (typeof item == 'string') {
return [
{
content: item,
chat_record_id: undefined,
child_node: undefined,
runtime_node_id: undefined,
reasoning_content: undefined
}
]
} else if (item instanceof Array) {
return item
} else {
return [item]
}
})
})
function showSource(row: any) {
if (props.type === 'log') {
return true
} else if (row.write_ed && 500 !== row.status) {
if (props.type === 'debug-ai-chat' || props.application?.show_source) {
return true
}
}
return false
}
const regenerationChart = (chat: chatType) => {
props.sendMessage(chat.problem_text, { re_chat: true })
}
const stopChat = (chat: chatType) => {
props.chatManagement.stop(chat.id)
}
const startChat = (chat: chatType) => {
props.chatManagement.write(chat.id)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,170 @@
<template>
<div class="touch-chat w-full mr-8">
<el-button
text
bg
class="microphone-button w-full mt-8 ml-8 mb-8"
style="font-size: 1rem; padding: 1.2rem 0 !important; background-color: #eff0f1"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
:disabled="disabled"
>
{{ disabled ? '对话中' : '按住说话' }}
</el-button>
<!-- 使用 custom-class 自定义样式 -->
<transition name="el-fade-in-linear">
<el-card class="custom-speech-card" :class="isTouching ? '' : 'active'" v-if="dialogVisible">
<p>
<el-text type="info" v-if="isTouching"
>00:{{ props.time < 10 ? `0${props.time}` : props.time }}</el-text
>
<span class="lighter" v-else>
{{ message }}
</span>
</p>
<el-avatar :size="isTouching ? 43 : 50" icon="Close" class="close" />
<!-- <div class="close"></div> -->
<p class="lighter" :style="{ visibility: isTouching ? 'visible' : 'hidden' }">
{{ message }}
</p>
<div class="speech-img flex-center border-r-4 mt-16">
<img v-if="isTouching" src="@/assets/chat/acoustic-color.svg" alt="" />
<img v-else src="@/assets/chat/acoustic.svg" alt="" />
</div>
</el-card>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
// import { Close } from '@element-plus/icons-vue'
const props = defineProps({
time: {
type: Number,
default: 0
},
start: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['TouchStart', 'TouchEnd'])
//
const startY = ref(0)
const isTouching = ref(false)
const dialogVisible = ref(false)
const message = ref('按住说话')
watch(
() => [props.time, props.start],
([time, start]) => {
if (start) {
isTouching.value = true
dialogVisible.value = true
message.value = '松开发送,上滑取消'
if (time === 60) {
dialogVisible.value = false
emit('TouchEnd', isTouching.value)
isTouching.value = false
}
} else {
dialogVisible.value = false
isTouching.value = false
}
}
)
watch(
() => props.start,
(val) => {
if (val) {
isTouching.value = true
dialogVisible.value = true
message.value = '松开发送,上滑取消'
} else {
dialogVisible.value = false
isTouching.value = false
}
}
)
function onTouchStart(event: any) {
//
event.preventDefault()
if (props.disabled) {
return
}
emit('TouchStart')
startY.value = event.touches[0].clientY
}
function onTouchMove(event: any) {
if (!isTouching.value) return
//
event.preventDefault()
const currentY = event.touches[0].clientY
const deltaY = currentY - startY.value
//
if (deltaY < -50) {
// -50
message.value = '松开取消发送'
isTouching.value = false
}
}
function onTouchEnd() {
emit('TouchEnd', isTouching.value)
}
</script>
<style lang="scss" scoped>
.custom-speech-card {
position: fixed;
bottom: 10px;
left: 50%; /* 水平居中 */
transform: translateX(-50%);
width: 92%;
background: #ffffff;
border: 1px solid #ffffff;
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
z-index: 999;
text-align: center;
color: var(--app-text-color-secondary);
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.close {
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
border: 1px solid rgba(222, 224, 227, 1);
background: rgba(255, 255, 255, 1);
color: var(--app-text-color-secondary);
font-size: 1.6rem;
margin: 20px 0;
}
.speech-img {
text-align: center;
background: #ebf1ff;
padding: 8px;
img {
height: 25px;
}
}
&.active {
.close {
background: #f54a45;
color: #ffffff;
border: none;
font-size: 2rem;
}
.speech-img {
background: #eff0f1;
}
}
}
</style>

View File

@ -0,0 +1,978 @@
<template>
<div class="ai-chat__operate p-16">
<slot name="operateBefore" />
<div class="operate-textarea">
<el-scrollbar max-height="136">
<div
class="p-8-12"
v-loading="localLoading"
v-if="
uploadDocumentList.length ||
uploadImageList.length ||
uploadAudioList.length ||
uploadVideoList.length ||
uploadOtherList.length
"
>
<el-row :gutter="10">
<el-col
v-for="(item, index) in uploadDocumentList"
:key="index"
:xs="24"
:sm="props.type === 'debug-ai-chat' ? 24 : 12"
:md="props.type === 'debug-ai-chat' ? 24 : 12"
:lg="props.type === 'debug-ai-chat' ? 24 : 12"
:xl="props.type === 'debug-ai-chat' ? 24 : 12"
class="mb-8"
>
<el-card
shadow="never"
style="--el-card-padding: 8px; max-width: 100%"
class="file cursor"
>
<div
class="flex-between align-center"
@mouseenter.stop="mouseenter(item)"
@mouseleave.stop="mouseleave()"
>
<div class="flex align-center">
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
<div class="ml-4 ellipsis-1" :title="item && item?.name">
{{ item && item?.name }}
</div>
</div>
<div
@click="deleteFile(index, 'document')"
class="delete-icon color-secondary"
v-if="showDelete === item.url"
>
<el-icon style="font-size: 16px; top: 2px">
<CircleCloseFilled />
</el-icon>
</div>
</div>
</el-card>
</el-col>
<el-col
v-for="(item, index) in uploadOtherList"
:key="index"
:xs="24"
:sm="props.type === 'debug-ai-chat' ? 24 : 12"
:md="props.type === 'debug-ai-chat' ? 24 : 12"
:lg="props.type === 'debug-ai-chat' ? 24 : 12"
:xl="props.type === 'debug-ai-chat' ? 24 : 12"
class="mb-8"
>
<el-card
shadow="never"
style="--el-card-padding: 8px; max-width: 100%"
class="file cursor"
>
<div
class="flex-between align-center"
@mouseenter.stop="mouseenter(item)"
@mouseleave.stop="mouseleave()"
>
<div class="flex align-center">
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
<div class="ml-4 ellipsis-1" :title="item && item?.name">
{{ item && item?.name }}
</div>
</div>
<div
@click="deleteFile(index, 'other')"
class="delete-icon color-secondary"
v-if="showDelete === item.url"
>
<el-icon style="font-size: 16px; top: 2px">
<CircleCloseFilled />
</el-icon>
</div>
</div>
</el-card>
</el-col>
<el-col
:xs="24"
:sm="props.type === 'debug-ai-chat' ? 24 : 12"
:md="props.type === 'debug-ai-chat' ? 24 : 12"
:lg="props.type === 'debug-ai-chat' ? 24 : 12"
:xl="props.type === 'debug-ai-chat' ? 24 : 12"
class="mb-8"
v-for="(item, index) in uploadAudioList"
:key="index"
>
<el-card shadow="never" style="--el-card-padding: 8px" class="file cursor">
<div
class="flex-between align-center"
@mouseenter.stop="mouseenter(item)"
@mouseleave.stop="mouseleave()"
>
<div class="flex align-center">
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
<div class="ml-4 ellipsis-1" :title="item && item?.name">
{{ item && item?.name }}
</div>
</div>
<div
@click="deleteFile(index, 'audio')"
class="delete-icon color-secondary"
v-if="showDelete === item.url"
>
<el-icon style="font-size: 16px; top: 2px">
<CircleCloseFilled />
</el-icon>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-space wrap>
<template v-for="(item, index) in uploadImageList" :key="index">
<div
class="file file-image cursor border border-r-4"
v-if="item.url"
@mouseenter.stop="mouseenter(item)"
@mouseleave.stop="mouseleave()"
>
<div
@click="deleteFile(index, 'image')"
class="delete-icon color-secondary"
v-if="showDelete === item.url"
>
<el-icon style="font-size: 16px; top: 2px">
<CircleCloseFilled />
</el-icon>
</div>
<el-image
:src="item.url"
alt=""
fit="cover"
style="width: 40px; height: 40px; display: block"
class="border-r-4"
/>
</div>
</template>
</el-space>
</div>
</el-scrollbar>
<div class="flex" :style="{ alignItems: isMicrophone ? 'center' : 'end' }">
<TouchChat
v-if="isMicrophone"
@TouchStart="startRecording"
@TouchEnd="TouchEnd"
:time="recorderTime"
:start="recorderStatus === 'START'"
:disabled="loading"
/>
<el-input
v-else
ref="quickInputRef"
v-model="inputValue"
:placeholder="
recorderStatus === 'START'
? `${$t('chat.inputPlaceholder.speaking')}...`
: recorderStatus === 'TRANSCRIBING'
? `${$t('chat.inputPlaceholder.recorderLoading')}...`
: $t('chat.inputPlaceholder.default')
"
:autosize="{ minRows: 1, maxRows: isMobile ? 4 : 10 }"
type="textarea"
:maxlength="100000"
@keydown.enter="sendChatHandle($event)"
@paste="handlePaste"
@drop="handleDrop"
/>
<div class="operate flex align-center">
<template v-if="props.applicationDetails.stt_model_enable">
<span v-if="mode === 'mobile'">
<el-button text @click="switchMicrophone(!isMicrophone)">
<!-- 键盘 -->
<AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon>
<el-icon v-else>
<!-- 录音 -->
<Microphone />
</el-icon>
</el-button>
</span>
<span class="flex align-center" v-else>
<el-button
:disabled="loading"
text
@click="startRecording"
v-if="recorderStatus === 'STOP'"
>
<el-icon>
<Microphone />
</el-icon>
</el-button>
<div v-else class="operate flex align-center">
<el-text type="info"
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
>
<el-button
text
type="primary"
@click="stopRecording"
:loading="recorderStatus === 'TRANSCRIBING'"
>
<AppIcon iconName="app-video-stop"></AppIcon>
</el-button>
</div>
</span>
</template>
<template v-if="recorderStatus === 'STOP' || mode === 'mobile'">
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4">
<el-upload
action="#"
multiple
:auto-upload="false"
:show-file-list="false"
:accept="getAcceptList()"
:on-change="(file: any, fileList: any) => uploadFile(file, fileList)"
ref="upload"
>
<el-tooltip
:disabled="mode === 'mobile'"
effect="dark"
placement="top"
popper-class="upload-tooltip-width"
>
<template #content>
<div class="break-all pre-wrap">
{{ $t('chat.uploadFile.label') }}{{ $t('chat.uploadFile.most')
}}{{ props.applicationDetails.file_upload_setting.maxFiles
}}{{ $t('chat.uploadFile.limit') }}
{{ props.applicationDetails.file_upload_setting.fileLimit }}MB<br />{{
$t('chat.uploadFile.fileType')
}}{{ getAcceptList().replace(/\./g, '').replace(/,/g, '、').toUpperCase() }}
</div>
</template>
<el-button text :disabled="checkMaxFilesLimit() || loading" class="mt-4">
<el-icon><Paperclip /></el-icon>
</el-button>
</el-tooltip>
</el-upload>
</span>
<el-divider
direction="vertical"
v-if="
props.applicationDetails.file_upload_enable ||
props.applicationDetails.stt_model_enable
"
/>
<el-button
text
class="sent-button"
:disabled="isDisabledChat || loading"
@click="sendChatHandle"
>
<img v-show="isDisabledChat || loading" src="@/assets/icon_send.svg" alt="" />
<SendIcon v-show="!isDisabledChat && !loading" />
</el-button>
</template>
</div>
</div>
</div>
<div class="text-center" v-if="applicationDetails.disclaimer" style="margin-top: 8px">
<el-text type="info" v-if="applicationDetails.disclaimer" style="font-size: 12px">
<auto-tooltip :content="applicationDetails.disclaimer_value">
{{ applicationDetails.disclaimer_value }}
</auto-tooltip>
</el-text>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import Recorder from 'recorder-core'
import TouchChat from './TouchChat.vue'
import applicationApi from '@/api/application/application'
import { MsgAlert } from '@/utils/message'
import { type chatType } from '@/api/type/application'
import { useRoute, useRouter } from 'vue-router'
import { getImgUrl } from '@/utils/utils'
import bus from '@/bus'
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import { MsgWarning } from '@/utils/message'
import { t } from '@/locales'
const router = useRouter()
const route = useRoute()
const {
query: { mode, question }
} = route as any
const quickInputRef = ref()
const props = withDefaults(
defineProps<{
applicationDetails: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
loading: boolean
isMobile: boolean
appId?: string
chatId: string
showUserInput?: boolean
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => void
openChatId: () => Promise<string>
validate: () => Promise<any>
}>(),
{
applicationDetails: () => ({}),
available: true
}
)
const emit = defineEmits(['update:chatId', 'update:loading', 'update:showUserInput'])
const chartOpenId = ref<string>()
const chatId_context = computed({
get: () => {
if (chartOpenId.value) {
return chartOpenId.value
}
return props.chatId
},
set: (v) => {
chartOpenId.value = v
emit('update:chatId', v)
}
})
const localLoading = computed({
get: () => {
return props.loading
},
set: (v) => {
emit('update:loading', v)
}
})
const upload = ref()
const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF', 'BMP']
const documentExtensions = ['PDF', 'DOCX', 'TXT', 'XLS', 'XLSX', 'MD', 'HTML', 'CSV']
const videoExtensions: any = []
const audioExtensions = ['MP3', 'WAV', 'OGG', 'AAC', 'M4A']
let otherExtensions = ['PPT', 'DOC']
const getAcceptList = () => {
const { image, document, audio, video, other } = props.applicationDetails.file_upload_setting
let accepts: any = []
if (image) {
accepts = [...imageExtensions]
}
if (document) {
accepts = [...accepts, ...documentExtensions]
}
if (audio) {
accepts = [...accepts, ...audioExtensions]
}
if (video) {
accepts = [...accepts, ...videoExtensions]
}
if (other) {
//
otherExtensions = props.applicationDetails.file_upload_setting.otherExtensions
accepts = [...accepts, ...otherExtensions]
}
if (accepts.length === 0) {
return `.${t('chat.uploadFile.tipMessage')}`
}
return accepts.map((ext: any) => '.' + ext).join(',')
}
const checkMaxFilesLimit = () => {
return (
props.applicationDetails.file_upload_setting.maxFiles <=
uploadImageList.value.length +
uploadDocumentList.value.length +
uploadAudioList.value.length +
uploadVideoList.value.length +
uploadOtherList.value.length
)
}
const uploadFile = async (file: any, fileList: any) => {
const { maxFiles, fileLimit } = props.applicationDetails.file_upload_setting
//
const file_limit_once =
uploadImageList.value.length +
uploadDocumentList.value.length +
uploadAudioList.value.length +
uploadVideoList.value.length +
uploadOtherList.value.length
if (file_limit_once >= maxFiles) {
MsgWarning(t('chat.uploadFile.limitMessage1') + maxFiles + t('chat.uploadFile.limitMessage2'))
fileList.splice(0, fileList.length)
return
}
if (fileList.filter((f: any) => f.size > fileLimit * 1024 * 1024).length > 0) {
// MB
MsgWarning(t('chat.uploadFile.sizeLimit') + fileLimit + 'MB')
fileList.splice(0, fileList.length)
return
}
const formData = new FormData()
formData.append('file', file.raw, file.name)
//
const extension = file.name.split('.').pop().toUpperCase() //
console.log(documentExtensions)
if (imageExtensions.includes(extension)) {
uploadImageList.value.push(file)
} else if (documentExtensions.includes(extension)) {
uploadDocumentList.value.push(file)
} else if (videoExtensions.includes(extension)) {
uploadVideoList.value.push(file)
} else if (audioExtensions.includes(extension)) {
uploadAudioList.value.push(file)
} else if (otherExtensions.includes(extension)) {
uploadOtherList.value.push(file)
}
if (!chatId_context.value) {
const res = await props.openChatId()
chatId_context.value = res
}
if (props.type === 'debug-ai-chat') {
formData.append('debug', 'true')
} else {
formData.append('debug', 'false')
}
applicationApi
.uploadFile(
props.applicationDetails.id as string,
chatId_context.value as string,
formData,
localLoading
)
.then((response) => {
fileList.splice(0, fileList.length)
uploadImageList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', '')
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadDocumentList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') == file.name.replaceAll(' ', '')
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadAudioList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', '')
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadVideoList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', '')
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadOtherList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', '')
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
if (!inputValue.value && uploadImageList.value.length > 0) {
inputValue.value = t('chat.uploadFile.imageMessage')
}
})
}
//
const handlePaste = (event: ClipboardEvent) => {
if (!props.applicationDetails.file_upload_enable) return
const clipboardData = event.clipboardData
if (!clipboardData) return
//
const files = clipboardData.files
if (files.length === 0) return
// FileList
Array.from(files).forEach((rawFile: File) => {
// el-upload
const elFile = {
uid: Date.now(), // ID
name: rawFile.name,
size: rawFile.size,
raw: rawFile, //
status: 'ready', //
percentage: 0 //
}
// on-change
uploadFile(elFile, [elFile])
})
//
event.preventDefault()
}
//
const handleDrop = (event: DragEvent) => {
if (!props.applicationDetails.file_upload_enable) return
event.preventDefault()
const files = event.dataTransfer?.files
if (!files) return
Array.from(files).forEach((rawFile) => {
const elFile = {
uid: Date.now(),
name: rawFile.name,
size: rawFile.size,
raw: rawFile,
status: 'ready',
percentage: 0
}
uploadFile(elFile, [elFile])
})
}
// id
const intervalId = ref<any | null>(null)
//
const recorderTime = ref(0)
// START: TRANSCRIBING:
const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP')
const inputValue = ref<string>('')
const uploadImageList = ref<Array<any>>([])
const uploadDocumentList = ref<Array<any>>([])
const uploadVideoList = ref<Array<any>>([])
const uploadAudioList = ref<Array<any>>([])
const uploadOtherList = ref<Array<any>>([])
const showDelete = ref('')
const isDisabledChat = computed(
() => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name))
)
//
const isMicrophone = ref(false)
const switchMicrophone = (status: boolean) => {
if (status) {
//
recorderManage.open(() => {
isMicrophone.value = true
})
} else {
//
recorderManage.close()
isMicrophone.value = false
}
}
const TouchEnd = (bool?: Boolean) => {
if (bool) {
stopRecording()
recorderStatus.value = 'STOP'
} else {
stopTimer()
recorderStatus.value = 'STOP'
}
}
//
Recorder.CLog = function () {}
class RecorderManage {
recorder?: any
uploadRecording: (blob: Blob, duration: number) => void
constructor(uploadRecording: (blob: Blob, duration: number) => void) {
this.uploadRecording = uploadRecording
}
open(callback?: () => void) {
const recorder = new Recorder({
type: 'mp3',
bitRate: 128,
sampleRate: 16000
})
if (!this.recorder) {
recorder.open(() => {
this.recorder = recorder
if (callback) {
callback()
}
}, this.errorCallBack)
}
}
start() {
if (this.recorder) {
this.recorder.start()
recorderStatus.value = 'START'
handleTimeChange()
} else {
const recorder = new Recorder({
type: 'mp3',
bitRate: 128,
sampleRate: 16000
})
recorder.open(() => {
this.recorder = recorder
recorder.start()
recorderStatus.value = 'START'
handleTimeChange()
}, this.errorCallBack)
}
}
stop() {
if (this.recorder) {
this.recorder.stop(
(blob: Blob, duration: number) => {
if (mode !== 'mobile') {
this.close()
}
this.uploadRecording(blob, duration)
},
(err: any) => {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
})
}
)
}
}
close() {
if (this.recorder) {
this.recorder.close()
this.recorder = undefined
}
}
private errorCallBack(err: any, isUserNotAllow: boolean) {
if (isUserNotAllow) {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
})
} else {
MsgAlert(
t('common.tip'),
`${err}
<div style="width: 100%;height:1px;border-top:1px var(--el-border-color) var(--el-border-style);margin:10px 0;"></div>
${t('chat.tip.recorderTip')}
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
{
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
}
)
}
}
}
//
const uploadRecording = async (audioBlob: Blob) => {
try {
//
if (!props.applicationDetails.stt_autosend) {
switchMicrophone(false)
}
recorderStatus.value = 'TRANSCRIBING'
const formData = new FormData()
formData.append('file', audioBlob, 'recording.mp3')
if (props.applicationDetails.stt_autosend) {
bus.emit('on:transcribing', true)
}
applicationApi
.postSpeechToText(props.applicationDetails.id as string, formData, localLoading)
.then((response) => {
inputValue.value = typeof response.data === 'string' ? response.data : ''
//
if (props.applicationDetails.stt_autosend) {
nextTick(() => {
autoSendMessage()
})
} else {
switchMicrophone(false)
}
})
.catch((error) => {
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
})
.finally(() => {
recorderStatus.value = 'STOP'
bus.emit('on:transcribing', false)
})
} catch (error) {
recorderStatus.value = 'STOP'
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
}
}
const recorderManage = new RecorderManage(uploadRecording)
//
const startRecording = () => {
recorderManage.start()
}
//
const stopRecording = () => {
recorderManage.stop()
}
const handleTimeChange = () => {
recorderTime.value = 0
if (intervalId.value) {
return
}
intervalId.value = setInterval(() => {
if (recorderStatus.value === 'STOP') {
clearInterval(intervalId.value!)
intervalId.value = null
return
}
recorderTime.value++
if (recorderTime.value === 60) {
if (mode !== 'mobile') {
stopRecording()
clearInterval(intervalId.value!)
intervalId.value = null
recorderStatus.value = 'STOP'
}
}
}, 1000)
}
//
const stopTimer = () => {
if (intervalId.value !== null) {
clearInterval(intervalId.value)
recorderTime.value = 0
intervalId.value = null
}
}
function autoSendMessage() {
props
.validate()
.then(() => {
props.sendMessage(inputValue.value, {
image_list: uploadImageList.value,
document_list: uploadDocumentList.value,
audio_list: uploadAudioList.value,
video_list: uploadVideoList.value,
other_list: uploadOtherList.value
})
inputValue.value = ''
uploadImageList.value = []
uploadDocumentList.value = []
uploadAudioList.value = []
uploadVideoList.value = []
uploadOtherList.value = []
if (quickInputRef.value) {
quickInputRef.value.textareaStyle.height = '45px'
}
})
.catch(() => {
emit('update:showUserInput', true)
})
}
function sendChatHandle(event?: any) {
const isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
//
if ((isMobile || mode === 'mobile') && event?.key === 'Enter') {
//
return
}
if (!event?.ctrlKey && !event?.shiftKey && !event?.altKey && !event?.metaKey) {
//
event?.preventDefault()
if (!isDisabledChat.value && !props.loading && !event?.isComposing) {
if (inputValue.value.trim()) {
autoSendMessage()
}
}
} else {
// ctrl/shift/cmd/opt +enter
insertNewlineAtCursor(event)
}
}
const insertNewlineAtCursor = (event?: any) => {
const textarea = quickInputRef.value.$el.querySelector(
'.el-textarea__inner'
) as HTMLTextAreaElement
const startPos = textarea.selectionStart
const endPos = textarea.selectionEnd
//
event.preventDefault()
//
inputValue.value = inputValue.value.slice(0, startPos) + '\n' + inputValue.value.slice(endPos)
nextTick(() => {
textarea.setSelectionRange(startPos + 1, startPos + 1) //
})
}
function deleteFile(index: number, val: string) {
if (val === 'image') {
uploadImageList.value.splice(index, 1)
} else if (val === 'document') {
uploadDocumentList.value.splice(index, 1)
} else if (val === 'video') {
uploadVideoList.value.splice(index, 1)
} else if (val === 'audio') {
uploadAudioList.value.splice(index, 1)
} else if (val === 'other') {
uploadOtherList.value.splice(index, 1)
}
}
function mouseenter(row: any) {
showDelete.value = row.url
}
function mouseleave() {
showDelete.value = ''
}
onMounted(() => {
bus.on('chat-input', (message: string) => {
inputValue.value = message
})
if (question) {
inputValue.value = decodeURIComponent(question.trim())
sendChatHandle()
setTimeout(() => {
//
const route = router.currentRoute.value
// query
const query = { ...route.query }
//
delete query.question
const newRoute =
Object.entries(query)?.length > 0
? route.path +
'?' +
Object.entries(query)
.map(([key, value]) => `${key}=${value}`)
.join('&')
: route.path
history.pushState(null, '', '/ui' + newRoute)
}, 100)
}
setTimeout(() => {
if (quickInputRef.value && mode === 'embed') {
quickInputRef.value.textarea.style.height = '0'
}
}, 1800)
})
</script>
<style lang="scss" scoped>
.ai-chat {
&__operate {
background: #f3f7f9;
position: relative;
width: 100%;
box-sizing: border-box;
z-index: 10;
&:before {
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
content: '';
position: absolute;
width: 100%;
top: -16px;
left: 0;
height: 16px;
}
:deep(.operate-textarea) {
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #ffffff;
box-sizing: border-box;
&:has(.el-textarea__inner:focus) {
border: 1px solid var(--el-color-primary);
}
.el-textarea__inner {
border-radius: 8px !important;
box-shadow: none;
resize: none;
padding: 13px 16px;
box-sizing: border-box;
}
.operate {
padding: 6px 10px;
.el-icon {
font-size: 20px;
}
.sent-button {
max-height: none;
.el-icon {
font-size: 24px;
}
}
.el-loading-spinner {
margin-top: -15px;
.circular {
width: 31px;
height: 31px;
}
}
}
}
.file-image {
position: relative;
overflow: inherit;
.delete-icon {
position: absolute;
right: -5px;
top: -5px;
z-index: 1;
}
}
.upload-tooltip-width {
width: 300px;
}
}
}
@media only screen and (max-width: 768px) {
.ai-chat {
&__operate {
position: fixed;
bottom: 0;
font-size: 1rem;
.el-icon {
font-size: 1.4rem !important;
}
}
}
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div>
<vue3-menus v-model:open="isOpen" :event="eventVal" :zIndex="9999" :menus="menus" hasIcon>
<template #icon="{ menu }"
><AppIcon v-if="menu.icon" :iconName="menu.icon"></AppIcon
></template>
<template #label="{ menu }"> {{ menu.label }}</template>
</vue3-menus>
</div>
</template>
<script setup lang="ts">
import { Vue3Menus } from 'vue3-menus'
import { MsgSuccess } from '@/utils/message'
import bus from '@/bus'
import { ref, nextTick, onMounted } from 'vue'
import { t } from '@/locales'
const isOpen = ref<boolean>(false)
const eventVal = ref<any>({})
function getSelection() {
const selection = window.getSelection()
if (selection) {
if (selection.rangeCount === 0) return undefined
const range = selection.getRangeAt(0)
const fragment = range.cloneContents() //
const div = document.createElement('div')
div.appendChild(fragment)
if (div.textContent) {
return div.textContent.trim()
}
}
return undefined
}
/**
* 打开控制台
* @param event
*/
const openControl = (event: any) => {
const c = getSelection()
if (c) {
if (!isOpen.value) {
nextTick(() => {
eventVal.value = event
isOpen.value = true
})
} else {
clearSelectedText()
isOpen.value = false
}
event.preventDefault()
} else {
isOpen.value = false
}
}
const menus = ref([
{
label: t('common.copy'),
icon: 'app-copy',
click: () => {
const selectionText = getSelection()
if (selectionText) {
clearSelectedText()
if (
typeof navigator.clipboard === 'undefined' ||
typeof navigator.clipboard.writeText === 'undefined'
) {
const input = document.createElement('input')
input.setAttribute('value', selectionText)
document.body.appendChild(input)
input.select()
try {
if (document.execCommand('copy')) {
MsgSuccess(t('common.copySuccess'))
}
} finally {
document.body.removeChild(input)
}
} else {
navigator.clipboard.writeText(selectionText).then(() => {
MsgSuccess(t('common.copySuccess'))
})
}
}
},
},
{
label: t('chat.quote'),
icon: 'app-quote',
click: () => {
bus.emit('chat-input', getSelection())
clearSelectedText()
},
},
])
/**
* 清除选中文本
*/
const clearSelectedText = () => {
if (window.getSelection) {
var selection = window.getSelection()
if (selection) {
selection.removeAllRanges()
}
}
}
onMounted(() => {
bus.on('open-control', openControl)
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,556 @@
<template>
<div class="chat-operation-button flex-between">
<el-text type="info">
<span class="ml-4" v-if="data.create_time">{{ datetimeFormat(data.create_time) }}</span>
</el-text>
<div>
<!-- 语音播放 -->
<span v-if="tts">
<el-tooltip
v-if="audioManage?.isPlaying()"
effect="dark"
:content="$t('chat.operation.pause')"
placement="top"
>
<el-button
type="primary"
text
:disabled="!data?.write_ed"
@click="audioManage?.pause(true)"
>
<AppIcon iconName="app-video-pause"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('chat.operation.play')" placement="top" v-else>
<el-button
text
:disabled="!data?.write_ed"
@click="
() => {
bus.emit('play:pause', props.data.record_id)
audioManage?.play(props.data.answer_text, true, true)
}
"
>
<AppIcon iconName="app-video-play"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
</span>
<span v-if="type == 'ai-chat' || type == 'log'">
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button text @click="copy(data)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" :content="$t('chat.operation.regeneration')" placement="top">
<el-button :disabled="chat_loading" text @click="regeneration">
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip
effect="dark"
:content="$t('chat.operation.like')"
placement="top"
v-if="buttonData?.vote_status === '-1'"
>
<el-button text @click="voteHandle('0')" :disabled="loading">
<AppIcon iconName="app-like"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('chat.operation.cancelLike')"
placement="top"
v-if="buttonData?.vote_status === '0'"
>
<el-button text @click="voteHandle('-1')" :disabled="loading">
<AppIcon iconName="app-like-color"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" v-if="buttonData?.vote_status === '-1'" />
<el-tooltip
effect="dark"
:content="$t('chat.operation.oppose')"
placement="top"
v-if="buttonData?.vote_status === '-1'"
>
<el-button text @click="voteHandle('1')" :disabled="loading">
<AppIcon iconName="app-oppose"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('chat.operation.cancelOppose')"
placement="top"
v-if="buttonData?.vote_status === '1'"
>
<el-button text @click="voteHandle('-1')" :disabled="loading">
<AppIcon iconName="app-oppose-color"></AppIcon>
</el-button>
</el-tooltip>
</span>
<div ref="audioCiontainer"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import { copyClick } from '@/utils/clipboard'
import applicationApi from '@/api/application/application'
import { datetimeFormat } from '@/utils/time'
import { MsgError } from '@/utils/message'
import bus from '@/bus'
const copy = (data: any) => {
try {
const text = data.answer_text_list
.map((item: Array<any>) => item.map((i) => i.content).join('\n'))
.join('\n\n')
copyClick(removeFormRander(text))
} catch (e: any) {
copyClick(removeFormRander(data?.answer_text.trim()))
}
}
const route = useRoute()
const {
params: { id }
} = route as any
const props = withDefaults(
defineProps<{
data: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
chatId: string
chat_loading: boolean
applicationId: string
tts: boolean
tts_type: string
tts_autoplay: boolean
}>(),
{
data: () => ({}),
type: 'ai-chat'
}
)
const emit = defineEmits(['update:data', 'regeneration'])
const audioPlayer = ref<HTMLAudioElement[] | null>([])
const audioCiontainer = ref<HTMLDivElement>()
const buttonData = ref(props.data)
const loading = ref(false)
const audioList = ref<string[]>([])
function regeneration() {
emit('regeneration')
}
function voteHandle(val: string) {
applicationApi
.putChatVote(props.applicationId, props.chatId, props.data.record_id, val, loading)
.then(() => {
buttonData.value['vote_status'] = val
emit('update:data', buttonData.value)
})
}
function markdownToPlainText(md: string) {
return (
md
// ![alt](url)
.replace(/!\[.*?\]\(.*?\)/g, '')
// [text](url)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// Markdown (#, ##, ###)
.replace(/^#{1,6}\s+/gm, '')
// **text** __text__
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/__(.*?)__/g, '$1')
// *text* _text_
.replace(/\*(.*?)\*/g, '$1')
.replace(/_(.*?)_/g, '$1')
// `code`
.replace(/`(.*?)`/g, '$1')
// ```code```
.replace(/```[\s\S]*?```/g, '')
//
.replace(/\n{2,}/g, '\n')
.trim()
)
}
function removeFormRander(text: string) {
return text.replace(/<form_rander>[\s\S]*?<\/form_rander>/g, '').trim()
}
function getKey(keys: Array<number>, index: number) {
// index
for (let i = keys.length - 1; i >= 0; i--) {
if (keys[i] <= index) {
return keys[i]
}
}
return 0
}
function smartSplit(
str: string,
minLengthConfig: any = {
0: 10,
1: 25,
3: 50,
5: 100
},
is_end = false
) {
// /20
const regex = /([。?\n])|(<audio[^>]*><\/audio>)/g
//
const parts = str.split(regex)
const result = []
const keys = Object.keys(minLengthConfig).map(Number)
let minLength = minLengthConfig[0]
let temp_str = ''
for (let i = 0; i < parts.length; i++) {
const content = parts[i]
if (content == undefined) {
continue
}
if (/^<audio[^>]*><\/audio>$/.test(content)) {
if (temp_str.length > 0) {
result.push(temp_str)
temp_str = ''
}
result.push(content)
continue
}
temp_str += parts[i]
if (temp_str.length > minLength && /[。?\n]$/.test(temp_str)) {
minLength = minLengthConfig[getKey(keys, i)]
result.push(temp_str)
temp_str = ''
}
}
if (temp_str.length > 0 && is_end) {
result.push(temp_str)
}
return result
}
enum AudioStatus {
/**
* 结束
*/
END = 'END',
/**
* 播放中
*/
PLAY_INT = 'PLAY_INT',
/**
* 刚挂载
*/
MOUNTED = 'MOUNTED',
/**
* 就绪
*/
READY = 'READY',
/**
* 错误
*/
ERROR = 'ERROR'
}
class AudioManage {
textList: Array<string>
statusList: Array<AudioStatus>
audioList: Array<HTMLAudioElement | SpeechSynthesisUtterance>
tryList: Array<number>
ttsType: string
root: Element
is_end: boolean
constructor(ttsType: string, root: HTMLDivElement) {
this.textList = []
this.audioList = []
this.statusList = []
this.tryList = []
this.ttsType = ttsType
this.root = root
this.is_end = false
}
appendTextList(textList: Array<string>) {
const newTextList = textList.slice(this.textList.length)
//
if (newTextList.length <= 0) {
return
}
newTextList.forEach((text, index) => {
this.textList.push(text)
this.statusList.push(AudioStatus.MOUNTED)
this.tryList.push(1)
index = this.textList.length - 1
if (this.ttsType === 'TTS') {
const audioElement: HTMLAudioElement = document.createElement('audio')
audioElement.controls = false
audioElement.hidden = true
/**
* 播放结束事件
*/
audioElement.onended = () => {
this.statusList[index] = AudioStatus.END
//
if (this.statusList.every((item) => item === AudioStatus.END) && this.is_end) {
this.statusList = this.statusList.map((item) => AudioStatus.READY)
this.is_end = false
} else {
// next
this.play()
}
}
this.root.appendChild(audioElement)
if (/^<audio[^>]*><\/audio>$/.test(text)) {
audioElement.src = text.match(/src="([^"]*)"/)?.[1] || ''
this.statusList[index] = AudioStatus.READY
} else {
applicationApi
.postTextToSpeech(
(props.applicationId as string) || (id as string),
{ text: text },
loading
)
.then(async (res: any) => {
if (res.type === 'application/json') {
const text = await res.text()
if (this.tryList[index] >= 3) {
MsgError(text)
}
this.statusList[index] = AudioStatus.ERROR
throw ''
}
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
audioElement.src = url
this.statusList[index] = AudioStatus.READY
this.play()
})
.catch((err) => {
this.statusList[index] = AudioStatus.ERROR
this.play()
})
}
this.audioList.push(audioElement)
} else {
const speechSynthesisUtterance: SpeechSynthesisUtterance = new SpeechSynthesisUtterance(
text
)
speechSynthesisUtterance.onend = () => {
this.statusList[index] = AudioStatus.END
//
if (this.statusList.every((item) => item === AudioStatus.END)) {
this.statusList = this.statusList.map((item) => AudioStatus.READY)
} else {
// next
this.play()
}
}
speechSynthesisUtterance.onerror = (e) => {
this.statusList[index] = AudioStatus.READY
}
this.statusList[index] = AudioStatus.READY
this.audioList.push(speechSynthesisUtterance)
this.play()
}
})
}
reTryError() {
this.statusList.forEach((status, index) => {
if (status === AudioStatus.ERROR && this.tryList[index] <= 3) {
this.tryList[index]++
const audioElement = this.audioList[index]
if (audioElement instanceof HTMLAudioElement) {
const text = this.textList[index]
this.statusList[index] = AudioStatus.MOUNTED
applicationApi
.postTextToSpeech(
(props.applicationId as string) || (id as string),
{ text: text },
loading
)
.then(async (res: any) => {
if (res.type === 'application/json') {
const text = await res.text()
if (this.tryList[index] >= 3) {
MsgError(text)
}
throw ''
}
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
audioElement.src = url
this.statusList[index] = AudioStatus.READY
this.play()
})
.catch((err) => {
console.log('err: ', err)
this.statusList[index] = AudioStatus.ERROR
this.play()
})
}
}
})
}
isPlaying() {
return this.statusList.some((item) => [AudioStatus.PLAY_INT].includes(item))
}
play(text?: string, is_end?: boolean, self?: boolean) {
if (is_end) {
this.is_end = true
}
if (self) {
this.tryList = this.tryList.map((item) => 0)
}
if (text) {
const textList = this.getTextList(text, is_end ? true : false)
this.appendTextList(textList)
}
//
if (this.statusList.some((item) => [AudioStatus.PLAY_INT].includes(item))) {
return
}
this.reTryError()
//
const index = this.statusList.findIndex((status) =>
[AudioStatus.MOUNTED, AudioStatus.READY].includes(status)
)
if (index < 0 || this.statusList[index] === AudioStatus.MOUNTED) {
return
}
const audioElement = this.audioList[index]
if (audioElement instanceof HTMLAudioElement) {
//
try {
this.statusList[index] = AudioStatus.PLAY_INT
const play = audioElement.play()
if (play instanceof Promise) {
play.catch((e) => {
this.statusList[index] = AudioStatus.READY
})
}
} catch (e: any) {
this.statusList[index] = AudioStatus.ERROR
}
} else {
if (window.speechSynthesis.paused) {
window.speechSynthesis.resume()
} else {
if (window.speechSynthesis.pending) {
window.speechSynthesis.cancel()
}
speechSynthesis.speak(audioElement)
this.statusList[index] = AudioStatus.PLAY_INT
}
}
}
pause(self?: boolean) {
const index = this.statusList.findIndex((status) => status === AudioStatus.PLAY_INT)
if (index < 0) {
return
}
const audioElement = this.audioList[index]
if (audioElement instanceof HTMLAudioElement) {
if (this.statusList[index] === AudioStatus.PLAY_INT) {
//
this.statusList[index] = AudioStatus.READY
audioElement.pause()
}
} else {
this.statusList[index] = AudioStatus.READY
if (self) {
window.speechSynthesis.pause()
nextTick(() => {
if (!window.speechSynthesis.paused) {
window.speechSynthesis.cancel()
}
})
} else {
window.speechSynthesis.cancel()
}
}
}
getTextList(text: string, is_end: boolean) {
//
text = removeFormRander(text)
// text
text = markdownToPlainText(text)
const split = smartSplit(
text,
{
0: 20,
1: 50,
5: 100
},
is_end
)
return split
}
}
const audioManage = ref<AudioManage>()
onMounted(() => {
if (audioCiontainer.value) {
audioManage.value = new AudioManage(props.tts_type, audioCiontainer.value)
}
bus.on('play:pause', (record_id: string) => {
if (record_id !== props.data.record_id) {
if (audioManage.value) {
audioManage.value?.pause()
}
}
})
bus.on('change:answer', (data: any) => {
const record_id = data.record_id
bus.emit('play:pause', record_id)
if (props.data.record_id == record_id) {
if (props.tts && props.tts_autoplay) {
if (audioManage.value) {
audioManage.value.play(props.data.answer_text, data.is_end)
}
}
}
})
})
onBeforeUnmount(() => {
bus.off('change:answer')
bus.off('play:pause')
if (audioManage.value) {
audioManage.value.pause()
}
if (window.speechSynthesis) {
window.speechSynthesis.cancel()
}
})
</script>
<style lang="scss" scoped>
@media only screen and (max-width: 430px) {
.chat-operation-button {
display: block;
text-align: right;
}
}
</style>

View File

@ -0,0 +1,265 @@
<template>
<div class="flex-between mt-8">
<div>
<el-text type="info">
<span class="ml-4">{{ datetimeFormat(data.create_time) }}</span>
</el-text>
</div>
<div>
<!-- 语音播放 -->
<span v-if="tts">
<el-tooltip effect="dark" :content="$t('chat.operation.play')" placement="top" v-if="!audioPlayerStatus">
<el-button text @click="playAnswerText(data?.answer_text)">
<AppIcon iconName="app-video-play"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip v-else effect="dark" :content="$t('chat.operation.pause')" placement="top">
<el-button type="primary" text @click="pausePlayAnswerText()">
<AppIcon iconName="app-video-pause"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
</span>
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button text @click="copyClick(data?.answer_text)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip
v-if="buttonData.improve_paragraph_id_list.length === 0"
effect="dark"
:content="$t('views.chatLog.editContent')"
placement="top"
>
<el-button text @click="editContent(data)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip v-else effect="dark" :content="$t('views.chatLog.editMark')" placement="top">
<el-button text @click="editMark(data)">
<AppIcon iconName="app-document-active" class="primary"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" v-if="buttonData?.vote_status !== '-1'" />
<el-button text disabled v-if="buttonData?.vote_status === '0'">
<AppIcon iconName="app-like-color"></AppIcon>
</el-button>
<el-button text disabled v-if="buttonData?.vote_status === '1'">
<AppIcon iconName="app-oppose-color"></AppIcon>
</el-button>
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { copyClick } from '@/utils/clipboard'
import EditContentDialog from '@/views/chat-log/component/EditContentDialog.vue'
import EditMarkDialog from '@/views/chat-log/component/EditMarkDialog.vue'
import { datetimeFormat } from '@/utils/time'
import applicationApi from '@/api/application/application'
import { useRoute } from 'vue-router'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route as any
const props = defineProps({
data: {
type: Object,
default: () => {}
},
applicationId: {
type: String,
default: ''
},
tts: Boolean,
tts_type: String
})
const emit = defineEmits(['update:data'])
const audioPlayer = ref<HTMLAudioElement[] | null>(null)
const EditContentDialogRef = ref()
const EditMarkDialogRef = ref()
const buttonData = ref(props.data)
const loading = ref(false)
const utterance = ref<SpeechSynthesisUtterance | null>(null)
const audioList = ref<string[]>([])
const currentAudioIndex = ref(0)
function editContent(data: any) {
EditContentDialogRef.value.open(data)
}
function editMark(data: any) {
EditMarkDialogRef.value.open(data)
}
const audioPlayerStatus = ref(false)
function markdownToPlainText(md: string) {
return (
md
// ![alt](url)
.replace(/!\[.*?\]\(.*?\)/g, '')
// [text](url)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// Markdown (#, ##, ###)
.replace(/^#{1,6}\s+/gm, '')
// **text** __text__
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/__(.*?)__/g, '$1')
// *text* _text_
.replace(/\*(.*?)\*/g, '$1')
.replace(/_(.*?)_/g, '$1')
// `code`
.replace(/`(.*?)`/g, '$1')
// ```code```
.replace(/```[\s\S]*?```/g, '')
//
.replace(/\n{2,}/g, '\n')
.trim()
)
}
function removeFormRander(text: string) {
return text
.replace(/<form_rander>[\s\S]*?<\/form_rander>/g, '')
.trim()
}
const playAnswerText = (text: string) => {
if (!text) {
text = t('chat.tip.answerMessage')
}
//
text = removeFormRander(text)
// text
text = markdownToPlainText(text)
// console.log(text)
audioPlayerStatus.value = true
//
audioList.value = text.split(/(<audio[^>]*><\/audio>)/)
playAnswerTextPart()
}
const playAnswerTextPart = () => {
// console.log(audioList.value, currentAudioIndex.value)
if (currentAudioIndex.value === audioList.value.length) {
audioPlayerStatus.value = false
currentAudioIndex.value = 0
return
}
if (audioList.value[currentAudioIndex.value].includes('<audio')) {
if (audioPlayer.value) {
audioPlayer.value[currentAudioIndex.value].src = audioList.value[currentAudioIndex.value].match(/src="([^"]*)"/)?.[1] || ''
audioPlayer.value[currentAudioIndex.value].play() //
audioPlayer.value[currentAudioIndex.value].onended = () => {
currentAudioIndex.value += 1
playAnswerTextPart()
}
}
} else if (props.tts_type === 'BROWSER') {
if (audioList.value[currentAudioIndex.value] !== utterance.value?.text) {
window.speechSynthesis.cancel()
}
if (window.speechSynthesis.paused && audioList.value[currentAudioIndex.value] === utterance.value?.text) {
window.speechSynthesis.resume()
return
}
// SpeechSynthesisUtterance
utterance.value = new SpeechSynthesisUtterance(audioList.value[currentAudioIndex.value])
utterance.value.onend = () => {
utterance.value = null
currentAudioIndex.value += 1
playAnswerTextPart()
}
utterance.value.onerror = () => {
audioPlayerStatus.value = false
utterance.value = null
}
//
window.speechSynthesis.speak(utterance.value)
} else if (props.tts_type === 'TTS') {
//
if (audioPlayer.value && audioPlayer.value[currentAudioIndex.value]?.src) {
audioPlayer.value[currentAudioIndex.value].play()
return
}
applicationApi
.postTextToSpeech((props.applicationId as string) || (id as string), { text: audioList.value[currentAudioIndex.value] }, loading)
.then(async (res: any) => {
if (res.type === 'application/json') {
const text = await res.text()
MsgError(text)
return
}
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
// blob
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = "abc.mp3"
// link.click()
// audioPlayer DOM
if (audioPlayer.value) {
audioPlayer.value[currentAudioIndex.value].src = url
audioPlayer.value[currentAudioIndex.value].play() //
audioPlayer.value[currentAudioIndex.value].onended = () => {
currentAudioIndex.value += 1
playAnswerTextPart()
}
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
}
const pausePlayAnswerText = () => {
audioPlayerStatus.value = false
if (props.tts_type === 'TTS') {
if (audioPlayer.value) {
audioPlayer.value?.forEach((item) => {
item.pause()
})
}
}
if (props.tts_type === 'BROWSER') {
window.speechSynthesis.pause()
}
}
function refreshMark() {
buttonData.value.improve_paragraph_id_list = []
emit('update:data', buttonData.value)
}
function refreshContent(data: any) {
buttonData.value = data
emit('update:data', buttonData.value)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,57 @@
<template>
<div class="operation-button-container">
<LogOperationButton
v-if="type === 'log'"
v-bind:data="chatRecord"
@update:data="(event: any) => emit('update:chatRecord', event)"
:applicationId="application.id"
:tts="application.tts_model_enable"
:tts_type="application.tts_type"
:type="type"
/>
<div class="mt-8" v-else>
<el-button
type="primary"
v-if="chatRecord.is_stop && !chatRecord.write_ed"
@click="startChat(chatRecord)"
link
>{{ $t('chat.operation.continue') }}
</el-button>
<el-button type="primary" v-else-if="!chatRecord.write_ed" @click="stopChat(chatRecord)" link
>{{ $t('chat.operation.stopChat') }}
</el-button>
</div>
<ChatOperationButton
v-show="chatRecord.write_ed && 500 != chatRecord.status"
:tts="application.tts_model_enable"
:tts_type="application.tts_type"
:tts_autoplay="application.tts_autoplay"
:data="chatRecord"
:type="type"
:applicationId="application.id"
:chatId="chatRecord.chat_id"
:chat_loading="loading"
@regeneration="regenerationChart(chatRecord)"
/>
</div>
</template>
<script setup lang="ts">
import ChatOperationButton from '@/components/ai-chat/component/operation-button/ChatOperationButton.vue'
import LogOperationButton from '@/components/ai-chat/component/operation-button/LogOperationButton.vue'
import { type chatType } from '@/api/type/application'
defineProps<{
type: 'log' | 'ai-chat' | 'debug-ai-chat'
chatRecord: chatType
application: any
loading: boolean
startChat: (chat_record: any) => void
stopChat: (chat_record: any) => void
regenerationChart: (chat_record: any) => void
}>()
const emit = defineEmits(['update:chatRecord'])
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,74 @@
<template>
<!-- 开场白组件 -->
<div class="item-content mb-16">
<div class="avatar mr-8" v-if="prologue && showAvatar">
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px" />
<LogoIcon v-else height="28px" width="28px" />
</div>
<div
class="content"
v-if="prologue"
:style="{
'padding-right': showUserAvatar ? 'var(--padding-left)' : '0'
}"
>
<el-card shadow="always" class="border-r-8" style="--el-card-padding: 10px 16px 12px">
<MdRenderer
:source="prologue"
:send-message="sendMessage"
reasoning_content=""
></MdRenderer>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { type chatType } from '@/api/type/application'
import { computed } from 'vue'
import MdRenderer from '@/components/markdown/MdRenderer.vue'
import { t } from '@/locales'
import useStore from '@/stores'
const props = defineProps<{
application: any
available: boolean
type: 'log' | 'ai-chat' | 'debug-ai-chat'
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => void
}>()
const { user } = useStore()
const showAvatar = computed(() => {
return user.isEnterprise() ? props.application.show_avatar : true
})
const showUserAvatar = computed(() => {
return user.isEnterprise() ? props.application.show_user_avatar : true
})
const toQuickQuestion = (match: string, offset: number, input: string) => {
return `<quick_question>${match.replace('- ', '')}</quick_question>`
}
const prologue = computed(() => {
const temp = props.available ? props.application?.prologue : t('chat.tip.prologueMessage')
if (temp) {
const tag_list = [
/<html_rander>[\d\D]*?<\/html_rander>/g,
/<echarts_rander>[\d\D]*?<\/echarts_rander>/g,
/<quick_question>[\d\D]*?<\/quick_question>/g,
/<form_rander>[\d\D]*?<\/form_rander>/g
]
let _temp = temp
for (const index in tag_list) {
_temp = _temp.replaceAll(tag_list[index], '')
}
const quick_question_list = _temp.match(/-\s.+/g)
let result = temp
for (const index in quick_question_list) {
const quick_question = quick_question_list[index]
result = result.replace(quick_question, toQuickQuestion)
}
return result
}
return ''
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,240 @@
<template>
<!-- 问题内容 -->
<div class="question-content item-content mb-16 lighter">
<div class="content p-12-16 border-r-8" :class="getClassName">
<div class="text break-all pre-wrap">
<div class="mb-8" v-if="document_list.length">
<el-space wrap class="w-full media-file-width">
<template v-for="(item, index) in document_list" :key="index">
<el-card shadow="never" style="--el-card-padding: 8px" class="download-file cursor">
<div class="download-button flex align-center" @click="downloadFile(item)">
<el-icon class="mr-4">
<Download />
</el-icon>
{{ $t('chat.download') }}
</div>
<div class="show flex align-center">
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
<div class="ml-4 ellipsis-1" :title="item && item?.name">
{{ item && item?.name }}
</div>
</div>
</el-card>
</template>
</el-space>
</div>
<div class="mb-8" v-if="image_list.length">
<el-space wrap>
<template v-for="(item, index) in image_list" :key="index">
<div class="file cursor border-r-4" v-if="item.url">
<el-image
:src="item.url"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="getAttrsArray(image_list, 'url')"
:initial-index="index"
alt=""
fit="cover"
style="width: 170px; height: 170px; display: block"
class="border-r-4"
/>
</div>
</template>
</el-space>
</div>
<div class="mb-8" v-if="audio_list.length">
<el-space wrap>
<template v-for="(item, index) in audio_list" :key="index">
<div class="file cursor border-r-4" v-if="item.url">
<audio
:src="item.url"
controls
style="width: 350px; height: 43px"
class="border-r-4"
/>
</div>
</template>
</el-space>
</div>
<div class="mb-8" v-if="other_list.length">
<el-space wrap class="w-full media-file-width">
<template v-for="(item, index) in other_list" :key="index">
<el-card shadow="never" style="--el-card-padding: 8px" class="download-file cursor">
<div class="download-button flex align-center" @click="downloadFile(item)">
<el-icon class="mr-4">
<Download />
</el-icon>
{{ $t('chat.download') }}
</div>
<div class="show flex align-center">
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
<div class="ml-4 ellipsis-1" :title="item && item?.name">
{{ item && item?.name }}
</div>
</div>
</el-card>
</template>
</el-space>
</div>
<span> {{ chatRecord.problem_text }}</span>
</div>
</div>
<div class="avatar ml-8" v-if="showAvatar">
<el-image
v-if="application.user_avatar"
:src="application.user_avatar"
alt=""
fit="cover"
style="width: 28px; height: 28px; display: block"
/>
<AppAvatar v-else>
<img src="@/assets/user-icon.svg" style="width: 50%" alt="" />
</AppAvatar>
</div>
</div>
</template>
<script setup lang="ts">
import { type chatType } from '@/api/type/application'
import { getImgUrl, getAttrsArray, downloadByURL } from '@/utils/utils'
import { onMounted, computed } from 'vue'
import useStore from '@/stores'
const props = defineProps<{
application: any
chatRecord: chatType
type: 'log' | 'ai-chat' | 'debug-ai-chat'
}>()
const { user } = useStore()
const showAvatar = computed(() => {
return user.isEnterprise() ? props.application.show_user_avatar : true
})
const document_list = computed(() => {
if (props.chatRecord?.upload_meta) {
return props.chatRecord.upload_meta?.document_list || []
}
const startNode = props.chatRecord.execution_details?.find(
(detail) => detail.type === 'start-node'
)
return startNode?.document_list || []
})
const image_list = computed(() => {
if (props.chatRecord?.upload_meta) {
return props.chatRecord.upload_meta?.image_list || []
}
const startNode = props.chatRecord.execution_details?.find(
(detail) => detail.type === 'start-node'
)
return startNode?.image_list || []
})
const audio_list = computed(() => {
if (props.chatRecord?.upload_meta) {
return props.chatRecord.upload_meta?.audio_list || []
}
const startNode = props.chatRecord.execution_details?.find(
(detail) => detail.type === 'start-node'
)
return startNode?.audio_list || []
})
const other_list = computed(() => {
if (props.chatRecord?.upload_meta) {
return props.chatRecord.upload_meta?.other_list || []
}
const startNode = props.chatRecord.execution_details?.find(
(detail) => detail.type === 'start-node'
)
return startNode?.other_list || []
})
const getClassName = computed(() => {
return document_list.value.length >= 2 || other_list.value.length >= 2
? 'media_2'
: document_list.value.length
? `media_${document_list.value.length}`
: other_list.value.length
? `media_${other_list.value.length}`
: `media_0`
})
function downloadFile(item: any) {
downloadByURL(item.url, item.name)
}
onMounted(() => {})
</script>
<style lang="scss" scoped>
.question-content {
display: flex;
justify-content: flex-end;
padding-left: var(--padding-left);
width: 100%;
box-sizing: border-box;
.content {
background: #d6e2ff;
padding-left: 16px;
padding-right: 16px;
}
.download-file {
height: 43px;
&:hover {
color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.download-button {
display: block;
text-align: center;
line-height: 26px;
}
.show {
display: none;
}
}
.download-button {
display: none;
}
}
.media-file-width {
:deep(.el-space__item) {
width: 49% !important;
}
}
.media_2 {
flex: 1;
}
.media_0 {
flex: inherit;
}
.media_1 {
width: 50%;
}
}
@media only screen and (max-width: 768px) {
.question-content {
.media-file-width {
:deep(.el-space__item) {
min-width: 100% !important;
}
}
.media_1 {
width: 100%;
}
}
}
.debug-ai-chat {
.question-content {
.media-file-width {
:deep(.el-space__item) {
min-width: 100% !important;
}
}
.media_1 {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<!-- 问题内容 -->
<div class="question-content item-content mb-16 lighter">
<div class="content p-12-16 border-r-8">
<span> {{ text }}</span
><span class="dotting"></span>
</div>
<div class="avatar ml-8" v-if="application.show_user_avatar">
<el-image
v-if="application.user_avatar"
:src="application.user_avatar"
alt=""
fit="cover"
style="width: 28px; height: 28px; display: block"
/>
<AppAvatar v-else>
<img src="@/assets/user-icon.svg" style="width: 50%" alt="" />
</AppAvatar>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
text: string
application: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
}>()
</script>
<style lang="scss" scoped>
.question-content {
display: flex;
justify-content: flex-end;
padding-left: var(--padding-left);
width: 100%;
box-sizing: border-box;
.content {
background: #d6e2ff;
padding-left: 16px;
padding-right: 16px;
}
.download-file {
height: 43px;
&:hover {
color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.download-button {
display: block;
text-align: center;
line-height: 26px;
}
.show {
display: none;
}
}
.download-button {
display: none;
}
}
.media-file-width {
:deep(.el-space__item) {
min-width: 40% !important;
flex-grow: 1;
}
}
.media_2 {
flex: 1;
}
.media_0 {
flex: inherit;
}
.media_1 {
width: 50%;
}
}
@media only screen and (max-width: 768px) {
.question-content {
.media-file-width {
:deep(.el-space__item) {
min-width: 100% !important;
}
}
.media_1 {
width: 100%;
}
}
}
.debug-ai-chat {
.question-content {
.media-file-width {
:deep(.el-space__item) {
min-width: 100% !important;
}
}
.media_1 {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,399 @@
<template>
<div
v-if="
(inputFieldList.length > 0 || (type === 'debug-ai-chat' && apiInputFieldList.length > 0)) &&
type !== 'log'
"
class="user-form-container mb-16 w-full"
>
<el-card shadow="always" class="border-r-8" style="--el-card-padding: 16px 8px">
<div class="flex align-center cursor w-full" style="padding: 0 8px">
<!-- <el-icon class="mr-8 arrow-icon" :class="showUserInput ? 'rotate-90' : ''"
><CaretRight
/></el-icon> -->
<span class="break-all ellipsis-1 mr-16" :title="inputFieldConfig.title">
{{ inputFieldConfig.title }}
</span>
</div>
<el-scrollbar :max-height="first ? 0 : 450">
<el-collapse-transition>
<div
v-show="showUserInput"
class="mt-16"
style="padding: 0 8px; height: calc(100% - 100px)"
>
<DynamicsForm
:key="dynamicsFormRefresh"
v-model="form_data_context"
:model="form_data_context"
label-position="top"
require-asterisk-position="right"
:render_data="inputFieldList"
ref="dynamicsFormRef"
/>
<DynamicsForm
v-if="type === 'debug-ai-chat'"
v-model="api_form_data_context"
:model="api_form_data_context"
label-position="top"
require-asterisk-position="right"
:render_data="apiInputFieldList"
ref="dynamicsFormRef2"
/>
</div>
</el-collapse-transition>
</el-scrollbar>
<div class="text-right mr-8">
<el-button type="primary" v-if="first" @click="confirmHandle">{{
$t('chat.operation.startChat')
}}</el-button>
<el-button v-if="!first" @click="cancelHandle">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" v-if="!first" @click="confirmHandle">{{
$t('common.confirm')
}}</el-button>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, onMounted } from 'vue'
import DynamicsForm from '@/components/dynamics-form/index.vue'
import type { FormField } from '@/components/dynamics-form/type'
import { useRoute } from 'vue-router'
import { MsgWarning } from '@/utils/message'
import { t } from '@/locales'
const route = useRoute()
const {
params: { accessToken }
} = route
const props = defineProps<{
application: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
api_form_data: any
form_data: any
first: boolean
}>()
//
const dynamicsFormRefresh = ref(0)
const inputFieldList = ref<FormField[]>([])
const apiInputFieldList = ref<FormField[]>([])
const inputFieldConfig = ref({ title: t('chat.userInput') })
const showUserInput = ref(true)
const firstMounted = ref(false)
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
const dynamicsFormRef2 = ref<InstanceType<typeof DynamicsForm>>()
const emit = defineEmits(['update:api_form_data', 'update:form_data', 'confirm', 'cancel'])
const api_form_data_context = computed({
get: () => {
return props.api_form_data
},
set: (data) => {
emit('update:api_form_data', data)
}
})
const form_data_context = computed({
get: () => {
return props.form_data
},
set: (data) => {
emit('update:form_data', data)
}
})
watch(
() => props.application,
(data) => {
handleInputFieldList()
}
)
function handleInputFieldList() {
dynamicsFormRefresh.value++
let default_value: any = {}
props.application.work_flow?.nodes
?.filter((v: any) => v.id === 'base-node')
.map((v: any) => {
inputFieldList.value = v.properties.user_input_field_list
? v.properties.user_input_field_list.map((v: any) => {
switch (v.type) {
case 'input':
return {
field: v.variable,
input_type: 'TextInput',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required
}
case 'select':
return {
field: v.variable,
input_type: 'SingleSelect',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
return v
}
})
: v.properties.input_field_list
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'user_input')
.map((v: any) => {
switch (v.type) {
case 'input':
return {
field: v.variable,
input_type: 'TextInput',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required
}
case 'select':
return {
field: v.variable,
input_type: 'SingleSelect',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
break
}
})
: []
apiInputFieldList.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => {
switch (v.type) {
case 'input':
return {
field: v.variable,
input_type: 'TextInput',
label: v.variable,
default_value: v.default_value || default_value[v.variable],
required: v.is_required
}
case 'select':
return {
field: v.variable,
input_type: 'SingleSelect',
label: v.variable,
default_value: v.default_value || default_value[v.variable],
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.variable,
default_value: v.default_value || default_value[v.variable],
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
break
}
})
: v.properties.input_field_list
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
switch (v.type) {
case 'input':
return {
field: v.variable,
input_type: 'TextInput',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required
}
case 'select':
return {
field: v.variable,
input_type: 'SingleSelect',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.name,
default_value: default_value[v.variable],
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
break
}
})
: []
//
inputFieldConfig.value = v.properties.user_input_config?.title
? v.properties.user_input_config
: { title: t('chat.userInput') }
})
}
const getRouteQueryValue = (field: string) => {
let _value = route.query[field]
if (_value != null) {
if (_value instanceof Array) {
_value = _value
.map((item) => {
if (item != null) {
return decodeQuery(item)
}
return null
})
.filter((item) => item != null)
} else {
_value = decodeQuery(_value)
}
return _value
}
return null
}
const validate = () => {
const promise_list = []
if (dynamicsFormRef.value) {
promise_list.push(dynamicsFormRef.value?.validate())
}
if (dynamicsFormRef2.value) {
promise_list.push(dynamicsFormRef2.value?.validate())
}
promise_list.push(validate_query())
return Promise.all(promise_list)
}
const validate_query = () => {
// query
let msg = []
for (let f of apiInputFieldList.value) {
if (f.required && !api_form_data_context.value[f.field]) {
msg.push(f.field)
}
}
if (msg.length > 0) {
MsgWarning(
`${t('chat.tip.inputParamMessage1')} ${msg.join('、')}${t('chat.tip.inputParamMessage2')}`
)
return Promise.reject(false)
}
return Promise.resolve(false)
}
const initRouteQueryValue = () => {
for (let f of apiInputFieldList.value) {
if (!api_form_data_context.value[f.field]) {
let _value = getRouteQueryValue(f.field)
if (_value != null) {
api_form_data_context.value[f.field] = _value
}
}
}
if (!api_form_data_context.value['asker']) {
const asker = getRouteQueryValue('asker')
if (asker) {
api_form_data_context.value['asker'] = getRouteQueryValue('asker')
}
}
}
const decodeQuery = (query: string) => {
try {
return decodeURIComponent(query)
} catch (e) {
return query
}
}
const confirmHandle = () => {
validate().then((ok) => {
localStorage.setItem(`${accessToken}userForm`, JSON.stringify(form_data_context.value))
emit('confirm')
})
}
const cancelHandle = () => {
emit('cancel')
}
const render = (data: any) => {
if (dynamicsFormRef.value) {
dynamicsFormRef.value?.render(inputFieldList.value, data)
}
}
const renderDebugAiChat = (data: any) => {
if (dynamicsFormRef2.value) {
dynamicsFormRef2.value?.render(apiInputFieldList.value, data)
}
}
defineExpose({ validate, render, renderDebugAiChat })
onMounted(() => {
firstMounted.value = true
handleInputFieldList()
initRouteQueryValue()
})
</script>
<style lang="scss" scoped>
.user-form-container {
padding: 0 24px;
}
@media only screen and (max-width: 768px) {
.user-form-container {
max-width: 100%;
}
}
</style>

View File

@ -0,0 +1,49 @@
.ai-chat {
--padding-left: 36px;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
position: relative;
color: var(--app-text-color);
box-sizing: border-box;
&__content {
padding-top: 0;
box-sizing: border-box;
.avatar {
float: left;
}
.content {
// padding-left: var(--padding-left);
// padding-right: var(--padding-left);
:deep(ol) {
margin-left: 16px !important;
}
}
}
}
.chat-width {
max-width: 80%;
margin: 0 auto;
}
@media only screen and (max-width: 1000px) {
.chat-width {
max-width: 100% !important;
margin: 0 auto;
}
}
@media only screen and (max-width: 768px) {
.ai-chat {
height: calc(100% - 106px) !important;
}
}
.chat-mobile {
.el-button.is-text:not(.is-disabled):hover {
background: none;
}
}

View File

@ -0,0 +1,658 @@
<template>
<div
ref="aiChatRef"
class="ai-chat"
:class="type"
:style="{
height: firsUserInput ? '100%' : undefined,
paddingBottom: applicationDetails.disclaimer ? '20px' : 0
}"
>
<div
v-show="showUserInputContent"
:class="firsUserInput ? 'firstUserInput' : 'popperUserInput'"
>
<UserForm
v-model:api_form_data="api_form_data"
v-model:form_data="form_data"
:application="applicationDetails"
:type="type"
:first="firsUserInput"
@confirm="UserFormConfirm"
@cancel="UserFormCancel"
ref="userFormRef"
></UserForm>
</div>
<template v-if="!(isUserInput || isAPIInput) || !firsUserInput || type === 'log'">
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
<div ref="dialogScrollbar" class="ai-chat__content p-16">
<PrologueContent
:type="type"
:application="applicationDetails"
:available="available"
:send-message="sendMessage"
></PrologueContent>
<template v-for="(item, index) in chatList" :key="index">
<!-- 问题 -->
<QuestionContent
:type="type"
:application="applicationDetails"
:chat-record="item"
></QuestionContent>
<!-- 回答 -->
<AnswerContent
:application="applicationDetails"
:loading="loading"
v-model:chat-record="chatList[index]"
:type="type"
:send-message="sendMessage"
:chat-management="ChatManagement"
></AnswerContent>
</template>
<TransitionContent
v-if="transcribing"
:text="t('chat.transcribing')"
:type="type"
:application="applicationDetails"
></TransitionContent>
</div>
</el-scrollbar>
<ChatInputOperate
:app-id="appId"
:application-details="applicationDetails"
:is-mobile="isMobile"
:type="type"
:send-message="sendMessage"
:open-chat-id="openChatId"
:validate="validate"
:chat-management="ChatManagement"
v-model:chat-id="chartOpenId"
v-model:loading="loading"
v-model:show-user-input="showUserInput"
v-if="type !== 'log'"
>
<template #operateBefore>
<div class="flex-between">
<slot name="operateBefore">
<span></span>
</slot>
<el-button
v-if="isUserInput || isAPIInput"
class="user-input-button mb-8"
type="primary"
text
@click="toggleUserInput"
>
<AppIcon iconName="app-user-input"></AppIcon>
</el-button>
</div>
</template>
</ChatInputOperate>
<Control></Control>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application/application'
import chatLogApi from '@/api/application/chat-log'
import { ChatManagement, type chatType } from '@/api/type/application'
import { randomId } from '@/utils/utils'
import useStore from '@/stores'
import { isWorkFlow } from '@/utils/application'
import { debounce } from 'lodash'
import AnswerContent from '@/components/ai-chat/component/answer-content/index.vue'
import QuestionContent from '@/components/ai-chat/component/question-content/index.vue'
import TransitionContent from '@/components/ai-chat/component/transition-content/index.vue'
import ChatInputOperate from '@/components/ai-chat/component/chat-input-operate/index.vue'
import PrologueContent from '@/components/ai-chat/component/prologue-content/index.vue'
import UserForm from '@/components/ai-chat/component/user-form/index.vue'
import Control from '@/components/ai-chat/component/control/index.vue'
import { t } from '@/locales'
import bus from '@/bus'
const transcribing = ref<boolean>(false)
defineOptions({ name: 'AiChat' })
const route = useRoute()
const {
params: { accessToken, id },
query: { mode }
} = route as any
const props = withDefaults(
defineProps<{
applicationDetails: any
type?: 'log' | 'ai-chat' | 'debug-ai-chat'
appId?: string
record?: Array<chatType>
available?: boolean
chatId?: string
}>(),
{
applicationDetails: () => ({}),
available: true,
type: 'ai-chat'
}
)
const emit = defineEmits(['refresh', 'scroll'])
const { application, common } = useStore()
const isMobile = computed(() => {
return common.isMobile() || mode === 'embed' || mode === 'mobile'
})
const aiChatRef = ref()
const scrollDiv = ref()
const dialogScrollbar = ref()
const loading = ref(false)
const inputValue = ref<string>('')
const chartOpenId = ref<string>('')
const chatList = ref<any[]>([])
const form_data = ref<any>({})
const api_form_data = ref<any>({})
const userFormRef = ref<InstanceType<typeof UserForm>>()
//
const firsUserInput = ref(false)
const showUserInput = ref(false)
//
const initialFormData = ref({})
const initialApiFormData = ref({})
const isUserInput = computed(
() =>
props.applicationDetails.work_flow?.nodes?.filter((v: any) => v.id === 'base-node')[0]
.properties.user_input_field_list.length > 0
)
const isAPIInput = computed(
() =>
props.type === 'debug-ai-chat' &&
props.applicationDetails.work_flow?.nodes?.filter((v: any) => v.id === 'base-node')[0]
.properties.api_input_field_list.length > 0
)
const showUserInputContent = computed(() => {
return (
(((isUserInput.value || isAPIInput.value) && firsUserInput.value) || showUserInput.value) &&
props.type !== 'log'
)
})
watch(
() => props.chatId,
(val) => {
if (val && val !== 'new') {
chartOpenId.value = val
firsUserInput.value = false
} else {
chartOpenId.value = ''
if (isUserInput.value) {
firsUserInput.value = true
} else if (props.type == 'debug-ai-chat' && isAPIInput.value) {
firsUserInput.value = true
}
}
},
{ deep: true, immediate: true }
)
watch(
() => props.applicationDetails,
() => {
chartOpenId.value = ''
},
{ deep: true }
)
watch(
() => props.record,
(value) => {
chatList.value = value ? value : []
},
{
immediate: true
}
)
const toggleUserInput = () => {
showUserInput.value = !showUserInput.value
if (showUserInput.value) {
//
initialFormData.value = JSON.parse(JSON.stringify(form_data.value))
initialApiFormData.value = JSON.parse(JSON.stringify(api_form_data.value))
}
}
function UserFormConfirm() {
firsUserInput.value = false
showUserInput.value = false
}
function UserFormCancel() {
//
form_data.value = JSON.parse(JSON.stringify(initialFormData.value))
api_form_data.value = JSON.parse(JSON.stringify(initialApiFormData.value))
userFormRef.value?.render(form_data.value)
showUserInput.value = false
}
const validate = () => {
return userFormRef.value?.validate() || Promise.reject(false)
}
function sendMessage(val: string, other_params_data?: any, chat?: chatType): Promise<boolean> {
if (isUserInput.value) {
if (userFormRef.value) {
return userFormRef.value
?.validate()
.then((ok) => {
let userFormData = JSON.parse(localStorage.getItem(`${accessToken}userForm`) || '{}')
const newData = Object.keys(form_data.value).reduce((result: any, key: string) => {
result[key] = Object.prototype.hasOwnProperty.call(userFormData, key)
? userFormData[key]
: form_data.value[key]
return result
}, {})
localStorage.setItem(`${accessToken}userForm`, JSON.stringify(newData))
showUserInput.value = false
if (!loading.value && props.applicationDetails?.name) {
handleDebounceClick(val, other_params_data, chat)
return true
}
throw 'err: no send'
})
.catch((e) => {
if (isAPIInput.value && props.type !== 'debug-ai-chat') {
showUserInput.value = false
} else {
showUserInput.value = true
}
return false
})
} else {
return Promise.reject(false)
}
} else {
showUserInput.value = false
if (!loading.value && props.applicationDetails?.name) {
handleDebounceClick(val, other_params_data, chat)
return Promise.resolve(true)
}
return Promise.reject(false)
}
}
const handleDebounceClick = debounce((val, other_params_data?: any, chat?: chatType) => {
chatMessage(chat, val, false, other_params_data)
}, 200)
/**
* 打开对话id
*/
const openChatId: () => Promise<string> = () => {
const obj = props.applicationDetails
if (props.appId) {
return applicationApi
.getChatOpen(props.appId)
.then((res) => {
chartOpenId.value = res.data
return res.data
})
.catch((res) => {
if (res.response.status === 403) {
return application.asyncAppAuthentication(accessToken).then(() => {
return openChatId()
})
}
return Promise.reject(res)
})
} else {
if (isWorkFlow(obj.type)) {
const submitObj = {
work_flow: obj.work_flow,
user_id: obj.user
}
return applicationApi.postWorkflowChatOpen(submitObj).then((res) => {
chartOpenId.value = res.data
return res.data
})
} else {
return applicationApi.postChatOpen(obj).then((res) => {
chartOpenId.value = res.data
return res.data
})
}
}
}
/**
* 对话
*/
function getChartOpenId(chat?: any, problem?: string, re_chat?: boolean, other_params_data?: any) {
return openChatId().then(() => {
chatMessage(chat, problem, re_chat, other_params_data)
})
}
/**
* 获取一个递归函数,处理流式数据
* @param chat 每一条对话记录
* @param reader 流数据
* @param stream 是否是流式数据
*/
const getWrite = (chat: any, reader: any, stream: boolean) => {
let tempResult = ''
/**
*
* @param done 是否结束
* @param value
*/
const write_stream = ({ done, value }: { done: boolean; value: any }) => {
try {
if (done) {
ChatManagement.close(chat.id)
return
}
const decoder = new TextDecoder('utf-8')
let str = decoder.decode(value, { stream: true })
// start chunk chunkdata:{xxx}\n\n data:{ -> xxx}\n\n fetchchunkdata: \n\n
tempResult += str
const split = tempResult.match(/data:.*}\n\n/g)
if (split) {
str = split.join('')
tempResult = tempResult.replace(str, '')
} else {
return reader.read().then(write_stream)
}
// end
if (str && str.startsWith('data:')) {
if (split) {
for (const index in split) {
const chunk = JSON?.parse(split[index].replace('data:', ''))
chat.chat_id = chunk.chat_id
chat.record_id = chunk.chat_record_id
if (!chunk.is_end) {
ChatManagement.appendChunk(chat.id, chunk)
}
if (chunk.is_end) {
//
return Promise.resolve()
}
}
}
}
} catch (e) {
return Promise.reject(e)
}
return reader.read().then(write_stream)
}
/**
* 处理 json 响应
* @param param0
*/
const write_json = ({ done, value }: { done: boolean; value: any }) => {
if (done) {
const result_block = JSON.parse(tempResult)
if (result_block.code === 500) {
return Promise.reject(result_block.message)
} else {
if (result_block.content) {
ChatManagement.append(chat.id, result_block.content)
}
}
ChatManagement.close(chat.id)
return
}
if (value) {
const decoder = new TextDecoder('utf-8')
tempResult += decoder.decode(value)
}
return reader.read().then(write_json)
}
return stream ? write_stream : write_json
}
const errorWrite = (chat: any, message?: string) => {
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
ChatManagement.append(chat.id, message || t('chat.tip.error500Message'))
ChatManagement.updateStatus(chat.id, 500)
ChatManagement.close(chat.id)
}
//
function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_params_data?: any) {
loading.value = true
if (!chat) {
chat = reactive({
id: randomId(),
problem_text: problem ? problem : inputValue.value.trim(),
answer_text: '',
answer_text_list: [[]],
buffer: [],
reasoning_content: '',
reasoning_content_buffer: [],
write_ed: false,
is_stop: false,
record_id: '',
chat_id: '',
vote_status: '-1',
status: undefined,
upload_meta: {
image_list:
other_params_data && other_params_data.image_list ? other_params_data.image_list : [],
document_list:
other_params_data && other_params_data.document_list
? other_params_data.document_list
: [],
audio_list:
other_params_data && other_params_data.audio_list ? other_params_data.audio_list : [],
other_list:
other_params_data && other_params_data.other_list ? other_params_data.other_list : []
}
})
chatList.value.push(chat)
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
inputValue.value = ''
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
}
if (chat.run_time) {
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
}
if (!chartOpenId.value) {
getChartOpenId(chat, problem, re_chat, other_params_data).catch(() => {
errorWrite(chat)
})
} else {
const obj = {
message: chat.problem_text,
re_chat: re_chat || false,
...other_params_data,
form_data: {
...form_data.value,
...api_form_data.value
}
}
//
applicationApi
.postChatMessage(chartOpenId.value, obj)
.then((response) => {
if (response.status === 401) {
application
.asyncAppAuthentication(accessToken)
.then(() => {
chatMessage(chat, problem)
})
.catch(() => {
errorWrite(chat)
})
} else if (response.status === 460) {
return Promise.reject(t('chat.tip.errorIdentifyMessage'))
} else if (response.status === 461) {
return Promise.reject(t('chat.tip.errorLimitMessage'))
} else {
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
const reader = response.body.getReader()
//
const write = getWrite(
chat,
reader,
response.headers.get('Content-Type') !== 'application/json'
)
return reader.read().then(write)
}
})
.then(() => {
if (props.chatId === 'new') {
emit('refresh', chartOpenId.value)
}
return (id || props.applicationDetails?.show_source) && getSourceDetail(chat)
})
.finally(() => {
ChatManagement.close(chat.id)
})
.catch((e: any) => {
errorWrite(chat, e + '')
})
}
}
/**
* 获取对话详情
* @param row
*/
function getSourceDetail(row: any) {
if (row.record_id) {
chatLogApi.getRecordDetail(id || props.appId, row.chat_id, row.record_id, loading).then((res) => {
const exclude_keys = ['answer_text', 'id', 'answer_text_list']
Object.keys(res.data).forEach((key) => {
if (!exclude_keys.includes(key)) {
row[key] = res.data[key]
}
})
})
}
return true
}
/**
* 滚动条距离最上面的高度
*/
const scrollTop = ref(0)
const scorll = ref(true)
const getMaxHeight = () => {
return dialogScrollbar.value!.scrollHeight
}
/**
* 滚动滚动条到最上面
* @param $event
*/
const handleScrollTop = ($event: any) => {
scrollTop.value = $event.scrollTop
if (
dialogScrollbar.value.scrollHeight - (scrollTop.value + scrollDiv.value.wrapRef.offsetHeight) <=
40
) {
scorll.value = true
} else {
scorll.value = false
}
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
}
/**
* 处理跟随滚动条
*/
const handleScroll = () => {
if (props.type !== 'log' && scrollDiv.value) {
//
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) {
//
if (scorll.value) {
scrollDiv.value.setScrollTop(getMaxHeight())
}
}
}
}
onMounted(() => {
if (isUserInput.value && localStorage.getItem(`${accessToken}userForm`)) {
let userFormData = JSON.parse(localStorage.getItem(`${accessToken}userForm`) || '{}')
form_data.value = userFormData
}
if (window.speechSynthesis) {
window.speechSynthesis.cancel()
}
window.sendMessage = sendMessage
bus.on('on:transcribing', (status: boolean) => {
transcribing.value = status
nextTick(() => {
if (scorll.value) {
scrollDiv.value.setScrollTop(getMaxHeight())
}
})
})
})
onBeforeUnmount(() => {
window.sendMessage = null
})
function setScrollBottom() {
//
scrollDiv.value.setScrollTop(getMaxHeight())
}
watch(
chatList,
() => {
handleScroll()
},
{ deep: true, immediate: true }
)
defineExpose({
setScrollBottom
})
</script>
<style lang="scss">
@import './index.scss';
.firstUserInput {
height: 100%;
display: flex;
justify-content: center;
overflow: auto;
.user-form-container {
max-width: 70%;
}
}
.debug-ai-chat {
.user-form-container {
max-width: 100%;
}
}
.popperUserInput {
position: absolute;
z-index: 999;
right: 50px;
bottom: 0;
width: calc(100% - 50px);
max-width: 400px;
}
@media only screen and (max-width: 768px) {
.firstUserInput {
.user-form-container {
max-width: 100%;
}
}
}
</style>

View File

@ -14,16 +14,16 @@
</el-icon>
</template>
<script setup lang="ts">
import { iconMap } from './index'
import { computed } from 'vue'
import { iconMap } from '@/components/app-icon/index'
defineOptions({ name: 'AppIcon' })
const props = withDefaults(
defineProps<{
iconName?: string
}>(),
{
iconName: '404'
}
iconName: '404',
},
)
const isIconfont = computed(() => props.iconName?.includes('app-'))

View File

@ -16,6 +16,7 @@
</el-avatar>
</template>
<script setup lang="ts">
defineOptions({ name: 'KnowledgeIcon' })
const props = defineProps({
type: {
type: [String, Number],

View File

@ -18,7 +18,6 @@
</el-card>
</template>
<script setup lang="ts">
import KnowledgeIcon from '@/views/knowledge/component/KnowledgeIcon.vue'
import { computed } from 'vue'
defineOptions({ name: 'CardCheckbox' })
const props = defineProps<{

View File

@ -21,6 +21,8 @@ import MdPreview from './markdown/MdPreview.vue'
import MdEditorMagnify from './markdown/MdEditorMagnify.vue'
import TagEllipsis from './tag-ellipsis/index.vue'
import CardCheckbox from './card-checkbox/index.vue'
import AiChat from './ai-chat/index.vue'
import KnowledgeIcon from './app-icon/KnowledgeIcon.vue'
export default {
install(app: App) {
app.component('LogoFull', LogoFull)
@ -45,5 +47,7 @@ export default {
app.component('MdEditorMagnify', MdEditorMagnify)
app.component('TagEllipsis', TagEllipsis)
app.component('CardCheckbox', CardCheckbox)
app.component('AiChat', AiChat)
app.component('KnowledgeIcon', KnowledgeIcon)
},
}

21
ui/src/enums/workflow.ts Normal file
View File

@ -0,0 +1,21 @@
export enum WorkflowType {
Base = 'base-node',
Start = 'start-node',
AiChat = 'ai-chat-node',
SearchDataset = 'search-dataset-node',
Question = 'question-node',
Condition = 'condition-node',
Reply = 'reply-node',
FunctionLib = 'function-lib-node',
FunctionLibCustom = 'function-node',
RrerankerNode = 'reranker-node',
Application = 'application-node',
DocumentExtractNode = 'document-extract-node',
ImageUnderstandNode = 'image-understand-node',
VariableAssignNode = 'variable-assign-node',
FormNode = 'form-node',
TextToSpeechNode = 'text-to-speech-node',
SpeechToTextNode = 'speech-to-text-node',
ImageGenerateNode = 'image-generate-node',
McpNode = 'mcp-node',
}

View File

@ -27,27 +27,8 @@
class="mr-8"
:size="24"
/>
<KnowledgeIcon v-else-if="isDataset" :type="current?.type" />
<el-avatar
v-else-if="isDataset && current?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-else-if="isDataset && current?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<el-avatar v-else class="mr-8 avatar-blue" shape="square" :size="24">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<div class="ellipsis" :title="current?.name">{{ current?.name }}</div>
</div>
@ -81,26 +62,9 @@
shape="square"
:size="24"
/>
<el-avatar
v-else-if="isDataset && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-else-if="isDataset && item.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<el-avatar v-else class="mr-12 avatar-blue" shape="square" :size="24">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<KnowledgeIcon v-if="isDataset" :type="item.type" />
<span class="ellipsis" :title="item?.name"> {{ item?.name }}</span>
</div>
</el-dropdown-item>

View File

@ -183,38 +183,9 @@
<el-card class="relate-knowledge-card border-r-4" shadow="never">
<div class="flex-between">
<div class="flex align-center" style="width: 80%">
<el-avatar
v-if="relatedObject(knowledgeList, item, 'id')?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img
src="@/assets/knowledge/icon_web.svg"
style="width: 58%"
alt=""
/>
</el-avatar>
<el-avatar
v-else-if="relatedObject(knowledgeList, item, 'id')?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
style="background: none"
>
<img
src="@/assets/knowledge/logo_lark.svg"
style="width: 100%"
alt=""
/>
</el-avatar>
<el-avatar v-else class="mr-8 avatar-blue" shape="square" :size="32">
<img
src="@/assets/knowledge/icon_document.svg"
style="width: 58%"
alt=""
/>
</el-avatar>
<KnowledgeIcon
:type="relatedObject(knowledgeList, item, 'id')?.type"
/>
<span
class="ellipsis cursor"

View File

@ -20,10 +20,10 @@
<template #footer>
<div>
<el-button @click="pre" :disabled="pre_disable || loading">{{
$t('views.log.buttons.prev')
$t('views.chatLog.buttons.prev')
}}</el-button>
<el-button @click="next" :disabled="next_disable || loading">{{
$t('views.log.buttons.next')
$t('views.chatLog.buttons.next')
}}</el-button>
</div>
</template>
@ -36,7 +36,7 @@ import { useRoute } from 'vue-router'
import { type ApplicationFormType, type chatType } from '@/api/type/application'
import useStore from '@/stores'
const AiChatRef = ref()
const { log } = useStore()
const { chatLog } = useStore()
const props = withDefaults(
defineProps<{
/**
@ -87,7 +87,7 @@ function closeHandle() {
}
function getChatRecord() {
return log
return chatLog
.asyncChatRecordLog(id as string, props.chatId, paginationConfig, loading)
.then((res: any) => {
paginationConfig.total = res.data.total

View File

@ -1,6 +1,6 @@
<template>
<el-dialog
:title="$t('views.log.editContent')"
:title="$t('views.chatLog.editContent')"
v-model="dialogVisible"
width="600"
:close-on-click-modal="false"
@ -26,7 +26,7 @@
<el-form-item :label="$t('common.content')" prop="content">
<MdEditor
v-model="form.content"
:placeholder="$t('views.log.form.content.placeholder')"
:placeholder="$t('views.chatLog.form.content.placeholder')"
:maxLength="100000"
:preview="false"
:toolbars="toolbars"
@ -43,56 +43,33 @@
<el-input
show-word-limit
v-model="form.title"
:placeholder="$t('views.log.form.title.placeholder')"
:placeholder="$t('views.chatLog.form.title.placeholder')"
maxlength="256"
>
</el-input>
</el-form-item>
<el-form-item :label="$t('views.log.selectDataset')" prop="dataset_id">
<el-form-item :label="$t('views.chatLog.selectDataset')" prop="dataset_id">
<el-select
v-model="form.dataset_id"
filterable
:placeholder="$t('views.log.selectDatasetPlaceholder')"
:placeholder="$t('views.chatLog.selectDatasetPlaceholder')"
:loading="optionLoading"
@change="changeDataset"
>
<el-option v-for="item in datasetList" :key="item.id" :label="item.name" :value="item.id">
<span class="flex align-center">
<AppAvatar
v-if="!item.dataset_id && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '0'"
class="mr-12 avatar-blue"
shape="square"
:size="24"
>
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<KnowledgeIcon v-if="item.dataset_id" :type="item.type" />
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('views.log.saveToDocument')" prop="document_id">
<el-form-item :label="$t('views.chatLog.saveToDocument')" prop="document_id">
<el-select
v-model="form.document_id"
filterable
:placeholder="$t('views.log.documentPlaceholder')"
:placeholder="$t('views.chatLog.documentPlaceholder')"
:loading="optionLoading"
@change="changeDocument"
>
@ -121,7 +98,7 @@
import { ref, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import logApi from '@/api/log'
import chatLogApi from '@/api/application/chat-log'
import imageApi from '@/api/image'
import useStore from '@/stores'
import { t } from '@/locales'
@ -129,7 +106,7 @@ const { application, document, user } = useStore()
const route = useRoute()
const {
params: { id }
params: { id },
} = route as any
const emit = defineEmits(['refresh'])
@ -162,7 +139,7 @@ const toolbars = [
'=',
'pageFullscreen',
'preview',
'htmlPreview'
'htmlPreview',
] as any[]
const footers = ['markdownTotal', 0, '=', 1, 'scrollSwitch']
@ -177,15 +154,15 @@ const form = ref<any>({
title: '',
content: '',
dataset_id: '',
document_id: ''
document_id: '',
})
const rules = reactive<FormRules>({
content: [{ required: true, message: t('views.log.form.content.placeholder'), trigger: 'blur' }],
content: [{ required: true, message: t('views.chatLog.form.content.placeholder'), trigger: 'blur' }],
dataset_id: [
{ required: true, message: t('views.log.selectDatasetPlaceholder'), trigger: 'change' }
{ required: true, message: t('views.chatLog.selectDatasetPlaceholder'), trigger: 'change' },
],
document_id: [{ required: true, message: t('views.log.documentPlaceholder'), trigger: 'change' }]
document_id: [{ required: true, message: t('views.chatLog.documentPlaceholder'), trigger: 'change' }],
})
const datasetList = ref<any[]>([])
@ -201,7 +178,7 @@ watch(dialogVisible, (bool) => {
title: '',
content: '',
dataset_id: '',
document_id: ''
document_id: '',
}
datasetList.value = []
documentList.value = []
@ -223,7 +200,7 @@ const onUploadImg = async (files: any, callback: any) => {
})
.catch((error) => rej(error))
})
})
}),
)
callback(res.map((item) => item.data))
@ -282,9 +259,9 @@ const submitForm = async (formEl: FormInstance | undefined) => {
const obj = {
title: form.value.title,
content: form.value.content,
problem_text: form.value.problem_text
problem_text: form.value.problem_text,
}
logApi
chatLogApi
.putChatRecordLog(
id,
form.value.chat_id,
@ -292,7 +269,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
form.value.dataset_id,
form.value.document_id,
obj,
loading
loading,
)
.then((res: any) => {
emit('refresh', res.data)

View File

@ -1,6 +1,6 @@
<template>
<el-dialog
:title="$t('views.log.editMark')"
:title="$t('views.chatLog.editMark')"
v-model="dialogVisible"
width="600"
class="edit-mark-dialog"

View File

@ -269,7 +269,6 @@
import { ref, type Ref, onMounted, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import KnowledgeIcon from '@/views/knowledge/component/KnowledgeIcon.vue'
import ChatRecordDrawer from './component/ChatRecordDrawer.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import chatLogApi from '@/api/application/chat-log'

View File

@ -135,7 +135,7 @@ import { hexToRgba } from '@/utils/theme'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
const { user, log } = useStore()
const { user, chatLog } = useStore()
const route = useRoute()
const isPopup = computed(() => {
@ -179,7 +179,7 @@ function editName(val: string, item: any) {
const obj = {
abstract: val
}
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
chatLog.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
const find = chatLogData.value.find((row: any) => row.id === item.id)
if (find) {
find.abstract = val
@ -203,7 +203,7 @@ function mouseenter(row: any) {
mouseId.value = row.id
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
chatLog.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
paginationConfig.current_page = 1
@ -244,7 +244,7 @@ function getChatLog(id: string) {
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLog.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
paginationConfig.current_page = 1
paginationConfig.total = 0
@ -257,7 +257,7 @@ function getChatLog(id: string) {
}
function getChatRecord() {
return log
return chatLog
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,

View File

@ -134,7 +134,7 @@ import { hexToRgba } from '@/utils/theme'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
const { user, log } = useStore()
const { user, chatLog } = useStore()
const AiChatRef = ref()
const loading = ref(false)
@ -175,7 +175,7 @@ function editName(val: string, item: any) {
abstract: val
}
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
chatLog.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
const find = chatLogData.value.find((row: any) => row.id === item.id)
if (find) {
find.abstract = val
@ -199,7 +199,7 @@ function mouseenter(row: any) {
mouseId.value = row.id
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
chatLog.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
paginationConfig.current_page = 1
@ -240,7 +240,7 @@ function getChatLog(id: string) {
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLog.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
paginationConfig.current_page = 1
paginationConfig.total = 0
@ -253,7 +253,7 @@ function getChatLog(id: string) {
}
function getChatRecord() {
return log
return chatLog
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,

View File

@ -172,7 +172,7 @@ import EditTitleDialog from './EditTitleDialog.vue'
import { t } from '@/locales'
useResize()
const { user, log, common } = useStore()
const { user, chatLog, common } = useStore()
const EditTitleDialogRef = ref()
@ -239,7 +239,7 @@ function refreshFieldTitle(chatId: string, abstract: string) {
}
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
chatLog.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
currentChatName.value = t('chat.createChat')
@ -289,7 +289,7 @@ function getChatLog(id: string, refresh?: boolean) {
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLog.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
if (refresh) {
currentChatName.value = chatLogData.value?.[0]?.abstract
@ -307,7 +307,7 @@ function getChatLog(id: string, refresh?: boolean) {
}
function getChatRecord() {
return log
return chatLog
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,

View File

@ -1,6 +1,6 @@
<template>
<el-dialog
:title="$t('views.log.selectDataset')"
:title="$t('views.chatLog.selectDataset')"
v-model="dialogVisible"
width="600"
class="select-dataset-dialog"
@ -9,7 +9,7 @@
>
<template #header="{ titleId, titleClass }">
<div class="my-header flex">
<h4 :id="titleId" :class="titleClass">{{ $t('views.log.selectDataset') }}</h4>
<h4 :id="titleId" :class="titleClass">{{ $t('views.chatLog.selectDataset') }}</h4>
<el-button link class="ml-16" @click="refresh">
<el-icon class="mr-4"><Refresh /></el-icon>{{ $t('common.refresh') }}
</el-button>
@ -24,31 +24,8 @@
<el-card shadow="never" :class="item.id === selectDataset ? 'active' : ''">
<el-radio :value="item.id" size="large">
<div class="flex align-center">
<el-avatar
v-if="item?.type === '0'"
class="mr-8 avatar-blue"
shape="square"
:size="32"
>
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-if="item?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-if="item?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<KnowledgeIcon :type="item.type" />
<span class="ellipsis" :title="item.name">
{{ item.name }}
</span>
@ -80,7 +57,7 @@ import useStore from '@/stores'
const { knowledge } = useStore()
const route = useRoute()
const {
params: { id } // iddatasetID
params: { id }, // iddatasetID
} = route as any
const emit = defineEmits(['refresh'])

View File

@ -277,7 +277,6 @@
<script lang="ts" setup>
import { onMounted, ref, reactive, shallowRef, nextTick } from 'vue'
import KnowledgeIcon from '@/views/knowledge/component/KnowledgeIcon.vue'
import CreateKnowledgeDialog from './create-component/CreateKnowledgeDialog.vue'
import CreateWebKnowledgeDialog from './create-component/CreateWebKnowledgeDialog.vue'
import CreateFolderDialog from '@/components/folder-tree/CreateFolderDialog.vue'

View File

@ -1,6 +1,6 @@
<template>
<el-dialog
:title="`${$t('views.log.selectDataset')}/${$t('common.fileUpload.document')}`"
:title="`${$t('views.chatLog.selectDataset')}/${$t('common.fileUpload.document')}`"
v-model="dialogVisible"
width="500"
:close-on-click-modal="false"
@ -14,51 +14,28 @@
:rules="rules"
@submit.prevent
>
<el-form-item :label="$t('views.log.selectDataset')" prop="dataset_id">
<el-form-item :label="$t('views.chatLog.selectDataset')" prop="dataset_id">
<el-select
v-model="form.dataset_id"
filterable
:placeholder="$t('views.log.selectDatasetPlaceholder')"
:placeholder="$t('views.chatLog.selectDatasetPlaceholder')"
:loading="optionLoading"
@change="changeDataset"
>
<el-option v-for="item in datasetList" :key="item.id" :label="item.name" :value="item.id">
<span class="flex align-center">
<el-avatar
v-if="!item.dataset_id && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-else-if="!item.dataset_id && item.type === '2'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<el-avatar
v-else-if="!item.dataset_id && item.type === '0'"
class="mr-12 avatar-blue"
shape="square"
:size="24"
>
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<KnowledgeIcon v-if="!item.dataset_id" :type="item.type" />
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('views.log.saveToDocument')" prop="document_id">
<el-form-item :label="$t('views.chatLog.saveToDocument')" prop="document_id">
<el-select
v-model="form.document_id"
filterable
:placeholder="$t('views.log.documentPlaceholder')"
:placeholder="$t('views.chatLog.documentPlaceholder')"
:loading="optionLoading"
>
<el-option
@ -93,7 +70,7 @@ const { knowledge, document } = useStore()
const route = useRoute()
const {
params: { id, documentId }
params: { id, documentId },
} = route as any
const emit = defineEmits(['refresh'])
@ -104,14 +81,14 @@ const loading = ref(false)
const form = ref<any>({
dataset_id: '',
document_id: ''
document_id: '',
})
const rules = reactive<FormRules>({
dataset_id: [
{ required: true, message: t('views.log.selectDatasetPlaceholder'), trigger: 'change' }
{ required: true, message: t('views.chatLog.selectDatasetPlaceholder'), trigger: 'change' },
],
document_id: [{ required: true, message: t('views.log.documentPlaceholder'), trigger: 'change' }]
document_id: [{ required: true, message: t('views.chatLog.documentPlaceholder'), trigger: 'change' }],
})
const datasetList = ref<any[]>([])
@ -123,7 +100,7 @@ watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
dataset_id: '',
document_id: ''
document_id: '',
}
datasetList.value = []
documentList.value = []
@ -166,7 +143,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
form.value.dataset_id,
form.value.document_id,
paragraphList.value,
loading
loading,
)
.then(() => {
emit('refresh')

Some files were not shown because too many files have changed in this diff Show More