generated from template/vite-react-template
temp
This commit is contained in:
5
backend/demo/index.html
Normal file
5
backend/demo/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
<script src="recorder.mp3.min.js"></script> <!--已包含recorder-core和mp3格式支持, CDN不稳定仅供测试: https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/recorder.mp3.min.js-->
|
||||
|
||||
|
||||
<script src="./index.js"></script>
|
||||
393
backend/demo/index.js
Normal file
393
backend/demo/index.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/******************
|
||||
《【教程】【ASR】实时语音识别、音频文件转文字-阿里云版》
|
||||
作者:高坚果
|
||||
时间:2020-7-22 22:37:09
|
||||
|
||||
通过阿里云语音识别(语音转文字)插件 /src/extensions/asr.aliyun.short.js,可实现实时语音识别、单个语音文件转文字。
|
||||
|
||||
只需要后端提供一个Token生成接口,就能进行语音识别,可直接参考或本地运行此NodeJs后端测试程序:/assets/demo-asr/NodeJsServer_asr.aliyun.short.js,配置好代码里的阿里云账号后,在目录内直接命令行执行`node NodeJsServer_asr.aliyun.short.js`即可运行提供本地测试接口。
|
||||
|
||||
目前暂未提供其他版本的语音识别插件,比如腾讯云、讯飞等,搭配使用Recorder的onProcess实时处理,可根据自己的业务需求选择对应厂家自行对接即可,如需定制开发请联系作者。
|
||||
******************/
|
||||
|
||||
var asr;
|
||||
|
||||
/*************单个语音文件转文字例子,你也可以录完音后再一次性进行识别***************/
|
||||
//将录音文件进行语音识别,支持的录音格式取决于浏览器的支持,兼容性mp3最好,wav次之,其他格式不一定能够解码
|
||||
var fileToText=function(audioBlob,fileName){
|
||||
if(asr){
|
||||
Runtime.Log("上次asr未关闭",1);
|
||||
return;
|
||||
};
|
||||
Runtime.Log("开始识别文件:《"+fileName+"》,asrProcess中已限制最多识别前60*3-5*(3-1)=170秒 ...");
|
||||
$(".recAsrTxt").text("");
|
||||
$(".recAsrTime").html("");
|
||||
|
||||
var url=$(".asrTokenApi").val();
|
||||
var urlReq=null;
|
||||
if(/^\s*\{.*\}\s*$/.test(url)){
|
||||
//这里是输入框里面填的json数据,直接success回调即可
|
||||
urlReq=function(url,args,success,fail){
|
||||
var data; try{ data=JSON.parse(url); }catch(e){};
|
||||
if(!data || !data.appkey || !data.token){
|
||||
fail("填写的json数据"+(!data?"解析失败":"中缺少appkey或token"));
|
||||
}else{
|
||||
success({ appkey:data.appkey, token:data.token });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var asr2=asr=Recorder.ASR_Aliyun_Short({
|
||||
tokenApi:url
|
||||
,apiArgs:{
|
||||
lang:$("[name=arsLang]:checked").val()
|
||||
,xxx:"其他请求参数"
|
||||
}
|
||||
,apiRequest:urlReq //如果提供了token数据,可不发起api请求
|
||||
,asrProcess:function(text,nextDuration,abortMsg){
|
||||
/***识别中间结果实回调,必须返回true才能继续识别,否则立即超时停止识别***/
|
||||
if(abortMsg){
|
||||
//语音识别中途出错,单个文件识别无需任何处理,会自动回调结果
|
||||
Runtime.Log("[asrProcess回调]被终止:"+abortMsg,1);
|
||||
return false;
|
||||
};
|
||||
|
||||
$(".recAsrTxt").text(text);
|
||||
$(".recAsrTime").html("识别时长: "+formatTime(asr2.asrDuration())
|
||||
+" 已发送数据时长: "+formatTime(asr2.sendDuration()));
|
||||
return nextDuration<=3*60*1000;//允许识别3分钟的识别时长(比音频时长小5*2秒)
|
||||
}
|
||||
,log:function(msg,color){
|
||||
Runtime.Log('<span style="opacity:0.15">'+msg+'</span>',color);
|
||||
}
|
||||
});
|
||||
Runtime.Log("语言:"+asr.set.apiArgs.lang);
|
||||
//语音文件识别只需调用audioToText即可完成识别,简单快速
|
||||
asr.audioToText(audioBlob,function(text,abortMsg){
|
||||
asr=null;
|
||||
if(abortMsg){
|
||||
Runtime.Log("发现识别中途被终止(一般无需特别处理):"+abortMsg,"#fb8");
|
||||
};
|
||||
Runtime.Log("文件识别最终结果:"+text, 2);
|
||||
},function(errMsg){
|
||||
asr=null;
|
||||
Runtime.Log("文件识别结束失败:"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
/*************单个语音文件转文字例子 END***************/
|
||||
|
||||
|
||||
|
||||
/*************实时语音识别例子*************************/
|
||||
/******界面交互处理、打开录音权限******/
|
||||
//使用长按录音的方式,可有效控制转换时长,避免不必要的资源浪费
|
||||
//长按按钮功能已经封装好了,直接调用 BindTouchButton 即可快速实现长按
|
||||
var bindTouchButton=function(){
|
||||
DemoFragment.BindTouchButton(
|
||||
"recTouchBtn"
|
||||
,"按住进行录音+识别"
|
||||
,"松开结束"
|
||||
,{}
|
||||
,asrOnTouchStart
|
||||
,asrOnTouchEnd
|
||||
);
|
||||
};
|
||||
|
||||
var rec;
|
||||
/**打开录音,先得到录音权限**/
|
||||
function recOpenClick(){
|
||||
$(".recOpenBtn").hide();
|
||||
$(".recCloseBtn").show();
|
||||
var end=function(isOk){
|
||||
if(isOk){
|
||||
$(".asrStartBtns").show();
|
||||
$(".asrStopBtn").show();
|
||||
};
|
||||
};
|
||||
|
||||
rec=Recorder({
|
||||
type:"wav"
|
||||
,sampleRate:16000
|
||||
,bitRate:16
|
||||
,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd){
|
||||
Runtime.Process.apply(null,arguments);
|
||||
|
||||
//实时推入asr处理。asr.input随时都可以调用,就算asr并未start,会缓冲到asr.start完成然后将已input的数据进行识别
|
||||
if(asr){
|
||||
//buffers是从录音开头到现在的缓冲,因此需要提供 buffersOffset=newBufferIdx
|
||||
asr.input(buffers, bufferSampleRate, newBufferIdx);
|
||||
};
|
||||
}
|
||||
});
|
||||
var t=setTimeout(function(){
|
||||
recAsrStatus("无法录音:权限请求被忽略(超时假装手动点击了确认对话框)",1);
|
||||
rec=null;
|
||||
end(false);
|
||||
},8000);
|
||||
|
||||
recAsrStatus("正在打开录音权限,请稍后...");
|
||||
rec.open(function(){//打开麦克风授权获得相关资源
|
||||
clearTimeout(t);
|
||||
recAsrStatus("录音已打开,可以长按录音+识别了",2);
|
||||
end(true);
|
||||
},function(msg,isUserNotAllow){//用户拒绝未授权或不支持
|
||||
clearTimeout(t);
|
||||
recAsrStatus((isUserNotAllow?"UserNotAllow,":"")+"无法录音:"+msg, 1);
|
||||
rec=null;
|
||||
end(false);
|
||||
});
|
||||
};
|
||||
var recCloseClick=function(){
|
||||
$(".recOpenBtn").show();
|
||||
$(".recCloseBtn").hide();
|
||||
$(".asrStartBtns").hide();
|
||||
if(rec){
|
||||
Runtime.Log("已关闭录音");
|
||||
rec.close();
|
||||
rec=null;
|
||||
}else{
|
||||
Runtime.Log("未打开录音",1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//免长按,这里就是调用的长按时的两个状态方法 功能是一样的
|
||||
var asrStartClick_NoTouch=function(){
|
||||
asrOnTouchStart(function(){});
|
||||
};
|
||||
var asrStopClick_NoTouch=function(){
|
||||
if(!asr){
|
||||
Runtime.Log("未开始识别",1);
|
||||
return;
|
||||
};
|
||||
asrOnTouchEnd(false,true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/******核心的长按录音识别******/
|
||||
/**长按开始录音**/
|
||||
var asrOnTouchStart=function(cancel){
|
||||
if(!rec){
|
||||
cancel("未打开录音");
|
||||
recAsrStatus("未打开录音",1);
|
||||
return;
|
||||
};
|
||||
rec.s_isStart=false;
|
||||
if(asr){
|
||||
cancel("上次asr未关闭");
|
||||
recAsrStatus("上次asr未关闭",1);
|
||||
return;
|
||||
};
|
||||
$(".recAsrTxt").text("");
|
||||
$(".recAsrTime").html("");
|
||||
|
||||
var url=$(".asrTokenApi").val();
|
||||
var urlReq=null;
|
||||
if(/^\s*\{.*\}\s*$/.test(url)){
|
||||
//这里是输入框里面填的json数据,直接success回调即可
|
||||
urlReq=function(url,args,success,fail){
|
||||
var data; try{ data=JSON.parse(url); }catch(e){};
|
||||
if(!data || !data.appkey || !data.token){
|
||||
fail("填写的json数据"+(!data?"解析失败":"中缺少appkey或token"));
|
||||
}else{
|
||||
success({ appkey:data.appkey, token:data.token });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//创建语音识别对象,每次识别都要新建,asr不能共用
|
||||
var asr2=asr=Recorder.ASR_Aliyun_Short({
|
||||
tokenApi:url
|
||||
,apiArgs:{
|
||||
lang:$("[name=arsLang]:checked").val()
|
||||
,xxx:"其他请求参数"
|
||||
}
|
||||
,apiRequest:urlReq //如果提供了token数据,可不发起api请求
|
||||
,asrProcess:function(text,nextDuration,abortMsg){
|
||||
/***实时识别结果,必须返回true才能继续识别,否则立即超时停止识别***/
|
||||
if(abortMsg){
|
||||
//语音识别中途出错
|
||||
recAsrStatus("[asrProcess回调]被终止:"+abortMsg,1);
|
||||
cancel("语音识别出错");//立即结束录音,就算继续录音也不会识别
|
||||
return false;
|
||||
};
|
||||
|
||||
$(".recAsrTxt").text(text);
|
||||
$(".recAsrTime").html("识别时长: "+formatTime(asr2.asrDuration())
|
||||
+" 已发送数据时长: "+formatTime(asr2.sendDuration()));
|
||||
return nextDuration<=2*60*1000;//允许识别2分钟的识别时长(比录音时长小5秒)
|
||||
}
|
||||
,log:function(msg,color){
|
||||
Runtime.Log('<span style="opacity:0.15">'+msg+'</span>',color);
|
||||
}
|
||||
});
|
||||
Runtime.Log("语言:"+asr.set.apiArgs.lang);
|
||||
recAsrStatus("连接服务器中,请稍后...");
|
||||
//打开语音识别,建议先打开asr,成功后再开始录音
|
||||
asr.start(function(){//无需特殊处理start和stop的关系,只要调用了stop,会阻止未完成的start,不会执行回调
|
||||
//开始录音
|
||||
Runtime.Log("开始录音...");
|
||||
rec.start();
|
||||
rec.s_isStart=true;
|
||||
|
||||
recAsrStatus("滴~~ 已开始语音识别,请讲话(asrProcess中已限制最多识别60*2-5*(2-1)=115秒)...",2);
|
||||
},function(errMsg){
|
||||
recAsrStatus("语音识别开始失败,请重试:"+errMsg,1);
|
||||
|
||||
cancel("语音识别开始失败");
|
||||
});
|
||||
};
|
||||
|
||||
/**松开停止录音**/
|
||||
var asrOnTouchEnd=function(isCancel,isUser){
|
||||
recAsrStatus(isCancel?"已取消":isUser?"已松开":"长按被取消",isUser?0:1);
|
||||
|
||||
var asr2=asr;asr=null;//先干掉asr,防止重复stop
|
||||
if(!asr2){
|
||||
Runtime.Log("未开始识别",1);
|
||||
}else{
|
||||
//asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
|
||||
asr2.stop(function(text,abortMsg){
|
||||
if(abortMsg){
|
||||
abortMsg="发现识别中途被终止(一般无需特别处理):"+abortMsg;
|
||||
};
|
||||
recAsrStatus("语音识别完成"+(abortMsg?","+abortMsg:""),abortMsg?"#f60":2);
|
||||
Runtime.Log("识别最终结果:"+text, 2);
|
||||
},function(errMsg){
|
||||
recAsrStatus("语音识别"+(!isUser?"被取消":"结束失败")+":"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
|
||||
var rec2=rec;
|
||||
if(rec2.s_isStart){
|
||||
rec2.s_isStart=false;
|
||||
rec2.stop(function(blob,duration){
|
||||
Runtime.LogAudio(blob,duration,rec2);
|
||||
},function(errMsg){
|
||||
Runtime.Log("录音失败:"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**更新状态**/
|
||||
var recAsrStatus=function(html,color){
|
||||
var elem=document.querySelector(".recAsrStatus");
|
||||
elem.style.color=color==1?"red":color==2?"#0b1":(color||null);
|
||||
elem.innerHTML=html;
|
||||
|
||||
Runtime.Log(html,color);
|
||||
};
|
||||
/*************实时语音识别例子 END***************/
|
||||
|
||||
|
||||
|
||||
//破坏环境,测试错误是否被正确处理
|
||||
var killToken=function(){
|
||||
if(!asr){
|
||||
Runtime.Log("未开始语音识别",1);
|
||||
return;
|
||||
}
|
||||
asr.set.apiRequest=function(url,args,success,fail){
|
||||
fail("不让获取Token");
|
||||
}
|
||||
Runtime.Log("已设置ASR的apiRequest,下一分钟将无法获得Token");
|
||||
};
|
||||
var killWs=function(){
|
||||
if(!asr || !asr.wsCur){
|
||||
Runtime.Log("未开始语音识别",1);
|
||||
return;
|
||||
}
|
||||
asr.wsCur.close();
|
||||
Runtime.Log("已强制关闭了ASR的WebSocket连接");
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//=====以下代码无关紧要,音频数据源和界面==================
|
||||
//加载录音框架
|
||||
Runtime.Import([
|
||||
{url:RootFolder+"/src/recorder-core.js",check:function(){return !window.Recorder}}
|
||||
,{url:RootFolder+"/src/engine/wav.js",check:function(){return !Recorder.prototype.wav}}
|
||||
,{url:RootFolder+"/src/extensions/asr.aliyun.short.js",check:function(){return !Recorder.ASR_Aliyun_Short}}
|
||||
|
||||
,{url:RootFolder+"/assets/runtime-codes/fragment.touch_button.js",check:function(){return !window.DemoFragment||!DemoFragment.BindTouchButton}}//引入BindTouchButton
|
||||
]);
|
||||
|
||||
|
||||
//显示控制按钮
|
||||
Runtime.Ctrls([
|
||||
{html:'\
|
||||
<hr/><div class="arsLangs" style="padding:5px 0">识别语言模型:\
|
||||
<style>.arsLangs label{cursor: pointer;margin-right:10px}</style>\
|
||||
<label><input type="radio" name="arsLang" value="普通话" checked>普通话</label>\
|
||||
<label><input type="radio" name="arsLang" value="粤语">粤语</label>\
|
||||
<label><input type="radio" name="arsLang" value="英语">英语</label>\
|
||||
<label><input type="radio" name="arsLang" value="日语">日语</label>\
|
||||
</div>\
|
||||
<hr/><div style="padding:5px 0">Token Api:\
|
||||
<input class="asrTokenApi" value="'+localTokenApi(1).replace(/"/g,""")+'" placeholder="请填写api地址 或 token的json数据" style="width:240px"/>\
|
||||
<div style="font-size:13px;color:#999;word-break:break-all;">\
|
||||
<div>你可以下载Recorder仓库<a href="https://gitee.com/xiangyuecn/Recorder/tree/master/assets/demo-asr" target="_blank">/assets/demo-asr</a>内的nodejs服务器端脚本到电脑上,配置好代码里的阿里云账号,然后运行此服务器端脚本即可提供本地测试接口</div>\
|
||||
<div>如果无法访问此api地址,比如手机上,你可以根据服务器脚本中的提示在电脑上打http地址,手动复制或自行提供 {appkey:"...",token:"..."} ,先删掉上面输入框中的url再粘贴json进去即可使用</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
'}
|
||||
,{html:'<hr/>\
|
||||
<div>\
|
||||
<span style="margin-right:170px">\
|
||||
<button class="mainBtn recOpenBtn" onclick="recOpenClick()">单击此处打开 录音+识别 功能</button>\
|
||||
<button class="mainBtn recCloseBtn" onclick="recCloseClick()" style="display:none;">关闭录音</button>\
|
||||
</span>\
|
||||
\
|
||||
<button class="mainBtn asrStartBtns" style="display:none;" onclick="asrStartClick_NoTouch()">免按住开始录音+识别</button>\
|
||||
<button class="mainBtn asrStopBtn" onclick="asrStopClick_NoTouch()" style="display:none;">结束语音识别</button>\
|
||||
</div>\
|
||||
\
|
||||
<div>\
|
||||
<button class="mainBtn recTouchBtn asrStartBtns" style="width:260px;height:60px;line-height:60px;display:none;"></button>\
|
||||
</div>\
|
||||
\
|
||||
<div class="recAsrStatus"></div>\
|
||||
\
|
||||
<hr/>\
|
||||
<div style="margin:12px 0 6px;font-size:12px">实时识别结果: <span class="recAsrTime"></span></div>\
|
||||
<div class="recAsrTxt" style="padding:15px 10px;min-height:50px;margin-bottom:12px;border:3px dashed #a2a1a1"></div>\
|
||||
'}
|
||||
|
||||
,{name:"破坏ASR配置,下一分钟得不到Token",click:"killToken"}
|
||||
,{name:"强制断开ASR的WebSocket连接",click:"killWs"}
|
||||
,{choiceFile:{
|
||||
multiple:false
|
||||
,name:"音频(已限制最多前3分钟内转成文字)"
|
||||
,mime:"audio/*"
|
||||
,process:function(fileName,arrayBuffer,filesCount,fileIdx,endCall){
|
||||
fileToText(new Blob([arrayBuffer]),fileName);
|
||||
endCall();
|
||||
}
|
||||
}}
|
||||
]);
|
||||
|
||||
bindTouchButton();
|
||||
|
||||
var formatTime=function(n){//格式化毫秒成 分:秒
|
||||
n=Math.round(n/1000);
|
||||
var s=n%60;
|
||||
var m=(n-s)/60;
|
||||
return m+":"+("0"+s).substr(-2);
|
||||
};
|
||||
|
||||
|
||||
function localTokenApi(useSet){
|
||||
var url="http://127.0.0.1:9527/token";
|
||||
if(useSet){
|
||||
url=localStorage["ASR_Aliyun_Short_TokenApi"]||url;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
$(".asrTokenApi").bind("change",function(){
|
||||
localStorage["ASR_Aliyun_Short_TokenApi"]=this.value==localTokenApi()?"":this.value;
|
||||
});
|
||||
6
backend/demo/recorder.mp3.min.js
vendored
Normal file
6
backend/demo/recorder.mp3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
backend/package.json
Normal file
14
backend/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.7.1",
|
||||
"type": "module"
|
||||
}
|
||||
312
backend/src/NodeJsServer_asr.aliyun.short.cjs
Normal file
312
backend/src/NodeJsServer_asr.aliyun.short.cjs
Normal file
@@ -0,0 +1,312 @@
|
||||
/*******************************************
|
||||
ASR 阿里云-智能语音交互-一句话识别 生成token的api接口,本地测试http后端服务
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
【运行】直接在当前文件夹内用命令行执行命令,即可运行:
|
||||
node NodeJsServer_asr.aliyun.short.js
|
||||
|
||||
【运行前先修改以下配置必填项】:
|
||||
*******************************************/
|
||||
global.Config = {
|
||||
//请到阿里云开通 一句话识别 服务(可试用一段时间,正式使用时应当开通商用版,很便宜),得到AccessKey、Secret,参考:https://help.aliyun.com/document_detail/324194.html
|
||||
AccessKey: 'LTAI5tDazeiB3pYYUwnMuLAt', //【必填】根据文档创建RAM用户,分配好权限,生成访问密钥
|
||||
Secret: '2UnGJO6GLZIVXS249B4wgi9uTKknLY', //【必填】
|
||||
|
||||
//请到阿里云智能语音交互控制台创建相应的语音识别项目,每个项目可以设置一种语言模型,要支持多种语言就创建多个项目,然后填写每个项目对应的Appkey到这里,前端调用本接口的时候传入lang参数来获取到对应的Appkey
|
||||
Langs: {
|
||||
普通话: 'xGuvvqhseLDauN2r', //【必填】至少创建一个普通话语言模型的项目用于测试,填对应的Appkey
|
||||
粤语: '',
|
||||
英语: '',
|
||||
日语: '',
|
||||
//...... 按需提供语言对应的项目Appkey
|
||||
},
|
||||
};
|
||||
|
||||
global.Port = 9527; //http服务端口
|
||||
global.SSL = {
|
||||
CertFile: '', //cert.pem 证书路径,不为空将开启https
|
||||
KeyFile: '', //cert.key 密钥路径
|
||||
};
|
||||
const Config = global.Config;
|
||||
|
||||
//如果提供了这个本地文件,就加载过来,可覆盖global下挂载的对象
|
||||
var fs = require('fs');
|
||||
var localFile = 'NodeJsServer_asr.aliyun.short__local.cjs';
|
||||
fs.existsSync(localFile) && require('./' + localFile);
|
||||
|
||||
var GetLangKeys = function () {
|
||||
var keys = [];
|
||||
for (var k in Config.Langs) if (Config.Langs[k]) keys.push(k);
|
||||
return keys;
|
||||
};
|
||||
|
||||
/***************一句话识别Token生成***************/
|
||||
var GetAsrToken = async function (ctx, args) {
|
||||
var lang = args.lang; //unsafe
|
||||
var rtv = {};
|
||||
ctx.setValue(rtv);
|
||||
var errTag = '[GetAsrToken]';
|
||||
|
||||
if (!Config.AccessKey || !Config.Secret) {
|
||||
return ctx.error(errTag + '未配置AccessKey');
|
||||
}
|
||||
console.log('Config', lang);
|
||||
|
||||
//根据前端需要的语言获得appkey,一种语言模型对应一个项目
|
||||
var appkey = Config.Langs[lang];
|
||||
if (!appkey) {
|
||||
var errMsg = 'lang[' + lang + ']未配置Appkey';
|
||||
if (!lang) errMsg = '请求需要提供lang参数';
|
||||
return ctx.error(errTag + errMsg + ',已配置的可用lang值' + JSON.stringify(GetLangKeys()));
|
||||
}
|
||||
rtv.appkey = appkey;
|
||||
|
||||
//获得Access Token,Token有效期内可以重复使用,实际使用时应当全局共享,并做好并发控制
|
||||
var cache = AccessToken_RedisCache; //假装读取Redis缓存数据
|
||||
if (!cache || cache.ExpireTime - Date.now() / 1000 < 60) {
|
||||
//缓存无效
|
||||
/*RedisLock()*/ {
|
||||
//假装加锁,得到锁的允许更新Token
|
||||
cache = AccessToken_RedisCache; //假装得到锁后,二次确认是否抢占到更新机会
|
||||
if (!cache || cache.ExpireTime - Date.now() / 1000 < 60) {
|
||||
cache = await NewAccessToken(ctx);
|
||||
if (ctx.isError()) {
|
||||
return ctx.error(errTag + ctx.getMsg());
|
||||
}
|
||||
console.log('NewAccessToken', cache);
|
||||
AccessToken_RedisCache = cache; //假装写入Redis缓存
|
||||
}
|
||||
}
|
||||
}
|
||||
rtv.token = cache.Id;
|
||||
|
||||
ctx.endLog = function () {
|
||||
console.log(
|
||||
' \x1B[32m' +
|
||||
lang +
|
||||
'token:' +
|
||||
JSON.stringify(rtv) +
|
||||
'\x1B[0m' +
|
||||
'\n 此token可重复使用,有效期到:' +
|
||||
new Date(cache.ExpireTime * 1000).toLocaleString(),
|
||||
);
|
||||
};
|
||||
return ctx;
|
||||
};
|
||||
|
||||
var AccessToken_RedisCache = null;
|
||||
//调用阿里云接口,获得新的Access Token,文档:https://help.aliyun.com/document_detail/113251.html
|
||||
var NewAccessToken = async function (ctx) {
|
||||
var params = {
|
||||
AccessKeyId: Config.AccessKey,
|
||||
Action: 'CreateToken',
|
||||
Version: '2019-02-28',
|
||||
Format: 'JSON',
|
||||
RegionId: 'cn-shanghai',
|
||||
Timestamp: JSON.stringify(new Date()).replace(/"/g, ''),
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
SignatureNonce: RandomHEX(32).replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'),
|
||||
};
|
||||
|
||||
var arr = [];
|
||||
for (var k in params) {
|
||||
arr.push(urlEncodeX(k) + '=' + urlEncodeX(params[k]));
|
||||
}
|
||||
arr.sort();
|
||||
params = arr.join('&');
|
||||
var signStr = 'GET&' + urlEncodeX('/') + '&' + urlEncodeX(params);
|
||||
var sign = HMAC('sha1', Config.Secret + '&', signStr, 'base64');
|
||||
params = 'Signature=' + urlEncodeX(sign) + '&' + params;
|
||||
|
||||
//直接发起get请求
|
||||
var load = LoadRequest('http://nls-meta.cn-shanghai.aliyuncs.com/?' + params);
|
||||
await load.getString(false);
|
||||
var respTxt = load.response.data;
|
||||
|
||||
var data = {};
|
||||
try {
|
||||
data = JSON.parse(respTxt);
|
||||
} catch (e) {}
|
||||
if (!data.Token) {
|
||||
ctx.error('获取Token失败[' + data.Code + ']:' + data.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.Token;
|
||||
};
|
||||
|
||||
//一些函数
|
||||
var LoadRequest = require('./lib.load.cjs');
|
||||
var Crypto = require('crypto');
|
||||
var urlEncodeX = function (val) {
|
||||
return encodeURIComponent(val).replace(/[^\w\-\.\~\%]/g, function (a) {
|
||||
return '%' + ('0' + a.charCodeAt(0).toString(16)).substr(-2).toUpperCase();
|
||||
});
|
||||
};
|
||||
var RandomHEX = function (len) {
|
||||
var s = [];
|
||||
for (var i = 0, r; i < len; i++) {
|
||||
r = Math.floor(Math.random() * 16);
|
||||
if (r < 10) {
|
||||
s.push(String.fromCharCode(r + 48));
|
||||
} else {
|
||||
s.push(String.fromCharCode(r - 10 + 97));
|
||||
}
|
||||
}
|
||||
return s.join('');
|
||||
};
|
||||
var HMAC = function (hash, key, data, resType) {
|
||||
var hmac = Crypto.createHmac(hash, key);
|
||||
hmac.update(data);
|
||||
return hmac.digest(resType === null ? null : resType || 'hex');
|
||||
};
|
||||
|
||||
/***************提供接口***************/
|
||||
var Api = {}; //async method_path0(ctx,args)
|
||||
|
||||
//内容回显
|
||||
Api.GET_ = Api.GET_echo = async function (ctx, args) {
|
||||
ctx.setValue('<h1>阿里云一句话识别TokenApi服务正在运行...</h1>', 'html');
|
||||
};
|
||||
Api.POST_ = Api.POST_echo = async function (ctx, args) {
|
||||
ctx.setValue({ url: ctx.req.url, method: ctx.req.method, headers: ctx.req.headers, args: args });
|
||||
};
|
||||
|
||||
//token接口
|
||||
Api.GET_token = Api.POST_token = async function (ctx, args) {
|
||||
await GetAsrToken(ctx, args);
|
||||
};
|
||||
|
||||
/***开启http服务***/
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var querystring = require('querystring');
|
||||
var reqProcess = function (req, rep) {
|
||||
var ctx = {
|
||||
req: req,
|
||||
rep: req,
|
||||
data: { c: 0, m: '', v: '' },
|
||||
status: 200,
|
||||
setCT: false,
|
||||
contentType: 'text/json; charset=utf-8',
|
||||
};
|
||||
ctx.setValue = function (val, contentType) {
|
||||
if (contentType != null) {
|
||||
ctx.setCT = true;
|
||||
ctx.contentType = !contentType || contentType == 'html' ? 'text/html; charset=utf-8' : contentType;
|
||||
}
|
||||
ctx.data.v = val;
|
||||
};
|
||||
ctx.error = function (msg) {
|
||||
ctx.data.c = 1;
|
||||
ctx.data.m = msg;
|
||||
return ctx;
|
||||
};
|
||||
ctx.isError = function () {
|
||||
return ctx.data.c != 0;
|
||||
};
|
||||
ctx.getMsg = function () {
|
||||
return ctx.data.m;
|
||||
};
|
||||
ctx.endLog = function () {};
|
||||
|
||||
var post = Buffer.alloc(0);
|
||||
req.on('data', function (chunk) {
|
||||
post = Buffer.concat([post, chunk], post.length + chunk.length);
|
||||
});
|
||||
req.on('end', function () {
|
||||
(async function () {
|
||||
if (req.method != 'GET' && req.method != 'POST') {
|
||||
ctx.setValue('Method: ' + req.method, 'html');
|
||||
} else
|
||||
try {
|
||||
//解析url查询参数 和 post表单
|
||||
var params = querystring.parse((/\?(.+)/.exec(req.url) || [])[1] || '');
|
||||
params = Object.assign(params, querystring.parse(post.toString()));
|
||||
|
||||
var path0 = (/^\/([^\/\?]+)/.exec(req.url) || [])[1] || '';
|
||||
var fn = Api[req.method + '_' + path0];
|
||||
if (fn) {
|
||||
await fn(ctx, params);
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
ctx.error(req.method + '路径不存在:' + req.url);
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.error('执行出错:' + e.stack);
|
||||
}
|
||||
if (!ctx.setCT) {
|
||||
try {
|
||||
var sendData = JSON.stringify(ctx.data);
|
||||
} catch (e) {
|
||||
sendData = JSON.stringify({ c: 1, m: '返回数据失败:' + e.stack });
|
||||
}
|
||||
} else {
|
||||
var sendData = ctx.data.v;
|
||||
}
|
||||
|
||||
rep.writeHead(ctx.status, {
|
||||
'Content-Type': ctx.contentType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
rep.end(sendData);
|
||||
|
||||
console.log(
|
||||
'[' +
|
||||
new Date().toLocaleString() +
|
||||
']:' +
|
||||
Port +
|
||||
' ' +
|
||||
req.method +
|
||||
' ' +
|
||||
ctx.status +
|
||||
' ' +
|
||||
Buffer.byteLength(sendData) +
|
||||
' ' +
|
||||
post.length +
|
||||
' ' +
|
||||
req.url,
|
||||
);
|
||||
if (ctx.isError()) {
|
||||
console.log(' \x1B[31m' + ctx.getMsg() + '\x1B[0m');
|
||||
}
|
||||
ctx.endLog();
|
||||
})();
|
||||
});
|
||||
};
|
||||
|
||||
var UrlProtocol = 'http';
|
||||
var UrlPort = Port == 80 ? '' : ':' + Port;
|
||||
if (SSL.CertFile) {
|
||||
UrlProtocol = 'https';
|
||||
UrlPort = Port == 443 ? '' : ':' + Port;
|
||||
}
|
||||
|
||||
console.log('正在创建' + UrlProtocol + '服务(port:' + Port + ')...');
|
||||
if (SSL.CertFile) {
|
||||
https
|
||||
.createServer(
|
||||
{
|
||||
cert: fs.readFileSync(SSL.CertFile),
|
||||
key: fs.readFileSync(SSL.KeyFile),
|
||||
},
|
||||
reqProcess,
|
||||
)
|
||||
.listen(Port);
|
||||
} else {
|
||||
http.createServer(reqProcess).listen(Port);
|
||||
}
|
||||
|
||||
console.log('\x1B[32m%s\x1B[0m', UrlProtocol.toUpperCase() + '服务正在运行...');
|
||||
console.log(`
|
||||
请在语音识别测试页面填写接口地址: \x1B[33m
|
||||
${UrlProtocol}://127.0.0.1${UrlPort}/token
|
||||
${UrlProtocol}://localhost${UrlPort}/token
|
||||
${UrlProtocol}://你的局域网IP${UrlPort}/token
|
||||
${UrlProtocol}://你的域名${UrlPort}/token
|
||||
\x1B[0m
|
||||
如果你的测试页面\x1B[35m无法访问http地址\x1B[0m或\x1B[35m存在跨域问题\x1B[0m时,请在本电脑上直接访问 \x1B[33m${UrlProtocol}://127.0.0.1${UrlPort}/token?lang=你需要lang语言\x1B[0m
|
||||
然后手动复制 \x1B[33m{appkey:'...',token:'...'}\x1B[0m json到页面中使用,复制的token可以重复使用,本控制台中会显示有效期
|
||||
已配置的可用lang语言\x1B[33m${JSON.stringify(GetLangKeys())}\x1B[0m
|
||||
`);
|
||||
199
backend/src/lib.load.cjs
Normal file
199
backend/src/lib.load.cjs
Normal file
@@ -0,0 +1,199 @@
|
||||
//请求数据封装
|
||||
var LoadFn=module.exports=function(url){
|
||||
return new Load(url);
|
||||
};
|
||||
|
||||
var QueryString = require('querystring');
|
||||
var Http=require('http');
|
||||
var Https=require('https');
|
||||
|
||||
var Load=function(url){
|
||||
this.response={//响应数据
|
||||
url:"" //最终响应的url,如果发生了30x跳转为跳转后的地址
|
||||
,status:0
|
||||
,statusDesc:"" //Not Found
|
||||
,headers:[] //["k: v",...] k大小写未知
|
||||
,data:null //string或buffer
|
||||
};
|
||||
|
||||
this.params={};//表单参数 GET或POST表单
|
||||
this.rawPost=null;//POST数据 string或buffer
|
||||
this.headers={};
|
||||
this.contentType="application/x-www-form-urlencoded";
|
||||
this.timeout=15019;//默认GET的,POST为30000
|
||||
this.redirect=true;//是否允许重定向
|
||||
this.debug=false;//调试模式,会忽略证书的校验
|
||||
|
||||
this._url=url;
|
||||
var m=/((\w+:)?\/\/([^\/:]+)(\:\d+)?)(.*)/.exec(url)||[];
|
||||
this._url_left=m[1];
|
||||
this._url_protocol=(m[2]||"http:").toLowerCase();
|
||||
this._url_isHttps=this._url_protocol=="https:";
|
||||
this._url_host=m[3]||"";
|
||||
this._url_port=+(m[4]||":"+(this._url_isHttps?443:80)).substr(1);
|
||||
this._url_pq=m[5]||"";
|
||||
};
|
||||
Load.prototype={
|
||||
|
||||
_redirect:async function(url,checkSuccessStatus,method,encode){
|
||||
if(/(^https?:\/\/)|(^\/)/i.test(url)){
|
||||
if(RegExp.$2){
|
||||
url=this._url_left+url;
|
||||
};
|
||||
}else{
|
||||
throw new Error("未支持的重定向地址格式:"+url);
|
||||
};
|
||||
|
||||
var redirectCount=this.redirectCount||0;
|
||||
if(redirectCount>=10){
|
||||
throw new Error("重定向超过次数");
|
||||
};
|
||||
var load=new Load(url);
|
||||
|
||||
load.params=this.params;
|
||||
load.rawPost=this.rawPost;
|
||||
load.headers=this.headers;
|
||||
load.contentType=this.contentType;
|
||||
load.timeout=this.timeout;
|
||||
load.redirect=this.redirect;
|
||||
load.debug=this.debug;
|
||||
|
||||
|
||||
load.redirectCount=redirectCount+1;
|
||||
await load.send(checkSuccessStatus,method,encode);
|
||||
return load;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,set:function(k,v){
|
||||
this[k]=v;
|
||||
return this;
|
||||
}
|
||||
|
||||
,send:async function(checkSuccessStatus,method,encode){
|
||||
var This=this;
|
||||
return await new Promise(function(resolve, reject){
|
||||
var isGET=method=="GET";
|
||||
var url=This._url_pq;
|
||||
var params=QueryString.stringify(This.params);
|
||||
if(isGET && params){
|
||||
if(url.indexOf("?")+1){
|
||||
url+="&"+params;
|
||||
}else{
|
||||
url+="?"+params;
|
||||
};
|
||||
};
|
||||
if(!isGET){
|
||||
This.headers["Content-Length"]=Buffer.byteLength(This.rawPost||params);
|
||||
if(!This.headers["Content-Type"]){
|
||||
This.headers["Content-Type"]=This.contentType;
|
||||
};
|
||||
};
|
||||
|
||||
var scope=This._url_isHttps?Https:Http;
|
||||
var obj={
|
||||
method:method
|
||||
,headers:This.headers
|
||||
,timeout:This.timeout!=15019?This.timeout:isGET?15000:30000
|
||||
,rejectUnauthorized:This.debug?false:true
|
||||
|
||||
,protocol:This._url_protocol
|
||||
,host:This._url_host
|
||||
,port:This._url_port
|
||||
,path:url
|
||||
};
|
||||
var req=scope.request(obj,function(resp){
|
||||
var status=resp.statusCode;
|
||||
var headers=[],location="";
|
||||
var arr=resp.rawHeaders;
|
||||
for(var i=0;i<arr.length;i+=2){
|
||||
var k=arr[i], v=arr[i+1];
|
||||
headers.push(k+": "+v);
|
||||
|
||||
if(k.toLowerCase()=="location"){
|
||||
location=v;
|
||||
};
|
||||
};
|
||||
This.response={
|
||||
url:This._url
|
||||
,status:status
|
||||
,statusDesc:resp.statusMessage
|
||||
,headers:headers
|
||||
};
|
||||
if((status==301||status==302) && This.redirect){
|
||||
This._redirect(location,checkSuccessStatus,method,encode)
|
||||
.then(function(o){
|
||||
This.response=o.response;
|
||||
resolve(This.response.data);
|
||||
},reject);
|
||||
return;
|
||||
};
|
||||
|
||||
var buffers=[];
|
||||
resp.on('data',function(buffer){
|
||||
buffers.push(buffer);
|
||||
});
|
||||
resp.on('end',function(){
|
||||
if(checkSuccessStatus!==false){
|
||||
if(!( status>=200 && status<300 )){
|
||||
reject(new Error("请求失败["+status+"]"));
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
This.response.data=Buffer.concat(buffers);
|
||||
if(encode!=null){
|
||||
var enc=(encode=="json"?"":encode)||'utf8';
|
||||
var val=This.response.data.toString(enc);
|
||||
if(encode=="json"){
|
||||
try{
|
||||
val=JSON.parse(val);
|
||||
}catch(e){
|
||||
reject(new Error("请求结果解析失败"));
|
||||
return;
|
||||
};
|
||||
};
|
||||
This.response.data=val;
|
||||
};
|
||||
|
||||
resolve(This.response.data);
|
||||
});
|
||||
});
|
||||
req.on("error",function(e){
|
||||
reject(new Error("请求错误:"+e.message));
|
||||
});
|
||||
|
||||
if(!isGET){
|
||||
if(This.rawPost){
|
||||
req.write(This.rawPost);
|
||||
}else if(params){
|
||||
req.write(params);
|
||||
};
|
||||
};
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
,getBytes:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET");
|
||||
}
|
||||
,getString:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET","");
|
||||
}
|
||||
,getJson:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET","json");
|
||||
}
|
||||
|
||||
,postBytes:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST");
|
||||
}
|
||||
,postString:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST","");
|
||||
}
|
||||
,postJson:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST","json");
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user