Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled
Add complete FunASR codebase including models, runtime, and documentation.
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie < 11
|
||||
@@ -0,0 +1,7 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
fonts
|
||||
*.md
|
||||
*.woff
|
||||
*.ttf
|
||||
public
|
||||
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ['plugin:vue/essential', '@vue/standard'],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'quote-props': 'off',
|
||||
indent: 'off',
|
||||
// "vue/script-indent": [
|
||||
// "error",
|
||||
// 4,
|
||||
// {
|
||||
// baseIndent: 1,
|
||||
// },
|
||||
// ],
|
||||
'vue/order-in-components': 'error'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset'],
|
||||
plugins: [
|
||||
[
|
||||
'import',
|
||||
{
|
||||
libraryName: 'ant-design-vue',
|
||||
libraryDirectory: 'es',
|
||||
style: 'css'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
import getUserInfo from './user/getUserInfo.js'
|
||||
import getMenuList from './user/getMenuList.js'
|
||||
Mock.setup({
|
||||
timeout: 500
|
||||
})
|
||||
|
||||
console.log('启动mock请求数据')
|
||||
getUserInfo(Mock)
|
||||
getMenuList(Mock)
|
||||
@@ -0,0 +1 @@
|
||||
export default Mock => {}
|
||||
@@ -0,0 +1 @@
|
||||
export default Mock => {}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function randomNum (n, m) {
|
||||
return Math.floor(Math.random() * (m - n + 1) + n)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "template-vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve --env VUE_APP_frontendConfigUrl=/template-vue/frontend-config-dev.json",
|
||||
"devmock": "vue-cli-service serve --env VUE_APP_frontendConfigUrl=/template-vue/frontend-config-devmock.json --env VUE_APP_env=devmock",
|
||||
"example": "vue-cli-service build --env VUE_APP_frontendConfigUrl=/template-vue/frontend-config-dev.json --env NODE_ENV=production"
|
||||
},
|
||||
"dependencies": {
|
||||
"ant-design-vue": "1.7.5",
|
||||
"@liveqing/liveplayer": "2.7.10",
|
||||
"axios": "0.19.2",
|
||||
"core-js": "3.22.5",
|
||||
"mockjs": "1.1.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-router": "3.5.3",
|
||||
"vuex": "3.6.2",
|
||||
"swiper": "5.4.5",
|
||||
"vuex-persistedstate": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.10",
|
||||
"@babel/eslint-parser": "7.17.0",
|
||||
"@vue/cli-plugin-babel": "5.0.4",
|
||||
"@vue/cli-plugin-eslint": "5.0.4",
|
||||
"@vue/cli-plugin-router": "5.0.4",
|
||||
"@vue/cli-plugin-vuex": "5.0.4",
|
||||
"@vue/cli-service": "5.0.4",
|
||||
"@vue/eslint-config-standard": "6.1.0",
|
||||
"babel-plugin-component": "1.1.1",
|
||||
"babel-plugin-import": "1.12.2",
|
||||
"compression-webpack-plugin": "3.1.0",
|
||||
"css-unicode-loader": "1.0.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "5.2.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"sass": "1.51.0",
|
||||
"sass-loader": "12.6.0",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"rkCsdjBackendUrl": "http://27.17.3.62:20629/rk-czrk-backend",
|
||||
"rkKstbBackendUrl": "http://27.17.3.62:20626/rk-kstb-backend",
|
||||
"sysId": "387f1f93-b5cb-46e4-9cb4-29146afa643a",
|
||||
"axiosDefaultsBaseURL": "http://27.17.3.62:20626/rk-kstb-backend",
|
||||
"ssoProxyLogoutPathname": "/kstb-nm/#/logout",
|
||||
"ssoServerLoginUrl": "http://27.17.3.62:20601/sso-orcl/index.html#/nmgsj",
|
||||
"rkSjdbBackendUrl": "http://27.17.3.62:20628/rk-sjdb-backend",
|
||||
"kernelSupportBackendUrl": "http://27.17.3.62:20615/kernel-support-backend",
|
||||
"rkCommonBackendUrl": "http://27.17.3.62:20625/rk-common-backend",
|
||||
"ssoServerTokenUrl": "http://27.17.3.62:20612/sso-backend/token",
|
||||
"rkHadoopDownLoadUrl": "http://27.17.3.62:20617/dfs/download",
|
||||
"ssoServerLogoutUrl": "http://27.17.3.62:20601/sso-orcl/#/logout",
|
||||
"rkConfigBackendUrl": "http://27.17.3.62:20624/rk-config-backend"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"": "(必选)axios默认基础路径",
|
||||
"axiosDefaultsBaseURL": "/api",
|
||||
"": "(必选)单点登录服务前端的登录页面",
|
||||
"ssoServerLoginUrl": "http://27.17.3.62:20201/user-center/biz/login/zfjd/login.html",
|
||||
"": "(可选)单点登录服务前端的登出页面",
|
||||
"ssoServerLogoutUrl": "http://27.17.3.62:20201/user-center/logout",
|
||||
"": "(必选)单点登录服务后端的令牌地址",
|
||||
"ssoServerTokenUrl": "http://27.17.3.62:20201/user-center/token",
|
||||
"": "(可选)单点登录代理前端的登出页面",
|
||||
"ssoProxyLogoutPathname": "/template-vue/#/logout",
|
||||
"sysId": "387f1f93-b5cb-46e4-9cb4-29146afa643a"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"": "(必选)axios默认基础路径",
|
||||
"axiosDefaultsBaseURL": "http://10.73.93.232:8081/zfjfz-backend",
|
||||
"": "(必选)单点登录服务前端的登录页面",
|
||||
"ssoServerLoginUrl": "http://10.73.94.72/user-center/biz/login/zfjd/login.html",
|
||||
"": "(可选)单点登录服务前端的登出页面",
|
||||
"ssoServerLogoutUrl": "http://10.73.94.72/user-center/logout",
|
||||
"": "(必选)单点登录服务后端的令牌地址",
|
||||
"ssoServerTokenUrl": "http://10.73.94.72/user-center/token",
|
||||
"": "(可选)单点登录代理前端的登出页面",
|
||||
"ssoProxyLogoutPathname": "/template-vue/#/logout",
|
||||
"sysId": "387f1f93-b5cb-46e4-9cb4-29146afa643a"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<script>
|
||||
var now = new Date().getTime()
|
||||
var head = document.getElementsByTagName('head')[0]
|
||||
|
||||
var linkList = []
|
||||
for (var i = 0; i < linkList.length; i++) {
|
||||
var link = document.createElement('link')
|
||||
link.href = linkList[i] + '?_t=' + now
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
head.appendChild(link)
|
||||
}
|
||||
</script>
|
||||
<script src="./jessibuca.js"></script>
|
||||
<script src="./static/liveplayer//liveplayer-lib.min.js"></script>
|
||||
<link rel="icon" href="<%= BASE_URL %>logo.png" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong
|
||||
>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>语音识别</title>
|
||||
|
||||
</head>
|
||||
<body style="margin-left: 3%">
|
||||
<script src="recorder-core.js" charset="UTF-8"></script>
|
||||
<script src="wav.js" charset="UTF-8"></script>
|
||||
<script src="pcm.js" charset="UTF-8"></script>
|
||||
|
||||
|
||||
|
||||
<h1>FunASR Demo</h1>
|
||||
<h3>这里是FunASR开源项目体验demo,集成了VAD、ASR与标点等工业级别的模型,支持长音频离线文件转写,实时语音识别等,开源项目地址:https://github.com/modelscope/FunASR</h3>
|
||||
|
||||
<div class="div_class_topArea">
|
||||
|
||||
<div class="div_class_recordControl">
|
||||
asr服务器地址(必填):
|
||||
<br>
|
||||
<input id="wssip" type="text" onchange="addresschange()" style=" width: 100%;height:100%" value="wss://127.0.0.1:10095/"/>
|
||||
<br>
|
||||
<a id="wsslink" href="#" onclick="window.open('https://127.0.0.1:10095/', '_blank')"><div id="info_wslink">点此处手工授权wss://127.0.0.1:10095/</div></a>
|
||||
<br>
|
||||
<br>
|
||||
<div style="border:2px solid #ccc;">
|
||||
选择录音模式:<br/>
|
||||
|
||||
<label><input name="recoder_mode" onclick="on_recoder_mode_change()" type="radio" value="mic" checked="true"/>麦克风 </label>
|
||||
<label><input name="recoder_mode" onclick="on_recoder_mode_change()" type="radio" value="file" />文件 </label>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div id="mic_mode_div" style="border:2px solid #ccc;display:block;">
|
||||
选择asr模型模式:<br/>
|
||||
|
||||
<label><input name="asr_mode" type="radio" value="2pass" checked="true"/>2pass </label>
|
||||
<label><input name="asr_mode" type="radio" value="online" />online </label>
|
||||
<label><input name="asr_mode" type="radio" value="offline" />offline </label>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="rec_mode_div" style="border:2px solid #ccc;display:none;">
|
||||
|
||||
|
||||
<input type="file" id="upfile">
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<div id="use_itn_div" style="border:2px solid #ccc;display:block;">
|
||||
逆文本标准化(ITN):<br/>
|
||||
<label><input name="use_itn" type="radio" value="false" checked="true"/>否 </label>
|
||||
<label><input name="use_itn" type="radio" value="true" />是 </label>
|
||||
</div>
|
||||
<br>
|
||||
<div style="border:2px solid #ccc;">
|
||||
热词设置(一行一个关键字,空格隔开权重,如"阿里巴巴 20"):
|
||||
<br>
|
||||
|
||||
|
||||
<textarea rows="3" id="varHot" style=" width: 100%;height:100%" >阿里巴巴 20 hello world 40</textarea>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
语音识别结果显示:
|
||||
<br>
|
||||
|
||||
<textarea rows="10" id="varArea" readonly="true" style=" width: 100%;height:100%" ></textarea>
|
||||
<br>
|
||||
<div id="info_div">请点击开始</div>
|
||||
<div class="div_class_buttons">
|
||||
<button id="btnConnect">连接</button>
|
||||
<button id="btnStart">开始</button>
|
||||
<button id="btnStop">停止</button>
|
||||
|
||||
</div>
|
||||
|
||||
<audio id="audio_record" type="audio/wav" controls style="margin-top: 12px; width: 100%;"></audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="wsconnecter.js" charset="utf-8"></script>
|
||||
<script src="main.js" charset="utf-8"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,583 @@
|
||||
/**
|
||||
* Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights
|
||||
* Reserved. MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
/* 2022-2023 by zhaoming,mali aihealthx.com */
|
||||
|
||||
|
||||
// 连接; 定义socket连接类对象与语音对象
|
||||
var wsconnecter = new WebSocketConnectMethod({msgHandle:getJsonMessage,stateHandle:getConnState});
|
||||
var audioBlob;
|
||||
|
||||
// 录音; 定义录音对象,wav格式
|
||||
var rec = Recorder({
|
||||
type:"pcm",
|
||||
bitRate:16,
|
||||
sampleRate:16000,
|
||||
onProcess:recProcess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
var sampleBuf=new Int16Array();
|
||||
// 定义按钮响应事件
|
||||
var btnStart = document.getElementById('btnStart');
|
||||
btnStart.onclick = record;
|
||||
var btnStop = document.getElementById('btnStop');
|
||||
btnStop.onclick = stop;
|
||||
btnStop.disabled = true;
|
||||
btnStart.disabled = true;
|
||||
|
||||
btnConnect= document.getElementById('btnConnect');
|
||||
btnConnect.onclick = start;
|
||||
|
||||
var awsslink= document.getElementById('wsslink');
|
||||
|
||||
|
||||
var rec_text=""; // for online rec asr result
|
||||
var offline_text=""; // for offline rec asr result
|
||||
var info_div = document.getElementById('info_div');
|
||||
|
||||
var upfile = document.getElementById('upfile');
|
||||
|
||||
|
||||
|
||||
var isfilemode=false; // if it is in file mode
|
||||
var file_ext="";
|
||||
var file_sample_rate=16000; //for wav file sample rate
|
||||
var file_data_array; // array to save file data
|
||||
|
||||
var totalsend=0;
|
||||
|
||||
|
||||
var now_ipaddress=window.location.href;
|
||||
now_ipaddress=now_ipaddress.replace("https://","wss://");
|
||||
now_ipaddress=now_ipaddress.replace("static/offline/index.html","");
|
||||
var localport=window.location.port;
|
||||
if (localport == '')
|
||||
{
|
||||
now_ipaddress=now_ipaddress.substring(0, now_ipaddress.length - 1) + ':10095' + now_ipaddress.substring(now_ipaddress.length - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
now_ipaddress=now_ipaddress.replace(localport,"10095");
|
||||
}
|
||||
document.getElementById('wssip').value=now_ipaddress;
|
||||
addresschange();
|
||||
function addresschange()
|
||||
{
|
||||
|
||||
var Uri = document.getElementById('wssip').value;
|
||||
document.getElementById('info_wslink').innerHTML="点此处手工授权(IOS手机)";
|
||||
Uri=Uri.replace(/wss/g,"https");
|
||||
console.log("addresschange uri=",Uri);
|
||||
|
||||
awsslink.onclick=function(){
|
||||
window.open(Uri, '_blank');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
upfile.onclick=function()
|
||||
{
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
}
|
||||
|
||||
// from https://github.com/xiangyuecn/Recorder/tree/master
|
||||
var readWavInfo=function(bytes){
|
||||
//读取wav文件头,统一成44字节的头
|
||||
if(bytes.byteLength<44){
|
||||
return null;
|
||||
};
|
||||
var wavView=bytes;
|
||||
var eq=function(p,s){
|
||||
for(var i=0;i<s.length;i++){
|
||||
if(wavView[p+i]!=s.charCodeAt(i)){
|
||||
return false;
|
||||
};
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
if(eq(0,"RIFF")&&eq(8,"WAVEfmt ")){
|
||||
|
||||
var numCh=wavView[22];
|
||||
if(wavView[20]==1 && (numCh==1||numCh==2)){//raw pcm 单或双声道
|
||||
var sampleRate=wavView[24]+(wavView[25]<<8)+(wavView[26]<<16)+(wavView[27]<<24);
|
||||
var bitRate=wavView[34]+(wavView[35]<<8);
|
||||
var heads=[wavView.subarray(0,12)],headSize=12;//head只保留必要的块
|
||||
//搜索data块的位置
|
||||
var dataPos=0; // 44 或有更多块
|
||||
for(var i=12,iL=wavView.length-8;i<iL;){
|
||||
if(wavView[i]==100&&wavView[i+1]==97&&wavView[i+2]==116&&wavView[i+3]==97){//eq(i,"data")
|
||||
heads.push(wavView.subarray(i,i+8));
|
||||
headSize+=8;
|
||||
dataPos=i+8;break;
|
||||
}
|
||||
var i0=i;
|
||||
i+=4;
|
||||
i+=4+wavView[i]+(wavView[i+1]<<8)+(wavView[i+2]<<16)+(wavView[i+3]<<24);
|
||||
if(i0==12){//fmt
|
||||
heads.push(wavView.subarray(i0,i));
|
||||
headSize+=i-i0;
|
||||
}
|
||||
}
|
||||
if(dataPos){
|
||||
var wavHead=new Uint8Array(headSize);
|
||||
for(var i=0,n=0;i<heads.length;i++){
|
||||
wavHead.set(heads[i],n);n+=heads[i].length;
|
||||
}
|
||||
return {
|
||||
sampleRate:sampleRate
|
||||
,bitRate:bitRate
|
||||
,numChannels:numCh
|
||||
,wavHead44:wavHead
|
||||
,dataPos:dataPos
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
upfile.onchange = function () {
|
||||
var len = this.files.length;
|
||||
for(let i = 0; i < len; i++) {
|
||||
|
||||
let fileAudio = new FileReader();
|
||||
fileAudio.readAsArrayBuffer(this.files[i]);
|
||||
|
||||
file_ext=this.files[i].name.split('.').pop().toLowerCase();
|
||||
var audioblob;
|
||||
fileAudio.onload = function() {
|
||||
audioblob = fileAudio.result;
|
||||
|
||||
|
||||
file_data_array=audioblob;
|
||||
|
||||
|
||||
info_div.innerHTML='请点击连接进行识别';
|
||||
|
||||
}
|
||||
|
||||
fileAudio.onerror = function(e) {
|
||||
console.log('error' + e);
|
||||
}
|
||||
}
|
||||
// for wav file, we get the sample rate
|
||||
if(file_ext=="wav")
|
||||
for(let i = 0; i < len; i++) {
|
||||
|
||||
let fileAudio = new FileReader();
|
||||
fileAudio.readAsArrayBuffer(this.files[i]);
|
||||
fileAudio.onload = function() {
|
||||
audioblob = new Uint8Array(fileAudio.result);
|
||||
|
||||
// for wav file, we can get the sample rate
|
||||
var info=readWavInfo(audioblob);
|
||||
console.log(info);
|
||||
file_sample_rate=info.sampleRate;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function play_file()
|
||||
{
|
||||
var audioblob=new Blob( [ new Uint8Array(file_data_array)] , {type :"audio/wav"});
|
||||
var audio_record = document.getElementById('audio_record');
|
||||
audio_record.src = (window.URL||webkitURL).createObjectURL(audioblob);
|
||||
audio_record.controls=true;
|
||||
//audio_record.play(); //not auto play
|
||||
}
|
||||
function start_file_send()
|
||||
{
|
||||
sampleBuf=new Uint8Array( file_data_array );
|
||||
|
||||
var chunk_size=960; // for asr chunk_size [5, 10, 5]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
while(sampleBuf.length>=chunk_size){
|
||||
|
||||
sendBuf=sampleBuf.slice(0,chunk_size);
|
||||
totalsend=totalsend+sampleBuf.length;
|
||||
sampleBuf=sampleBuf.slice(chunk_size,sampleBuf.length);
|
||||
wsconnecter.wsSend(sendBuf);
|
||||
|
||||
|
||||
}
|
||||
|
||||
stop();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function on_recoder_mode_change()
|
||||
{
|
||||
var item = null;
|
||||
var obj = document.getElementsByName("recoder_mode");
|
||||
for (var i = 0; i < obj.length; i++) { //遍历Radio
|
||||
if (obj[i].checked) {
|
||||
item = obj[i].value;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(item=="mic")
|
||||
{
|
||||
document.getElementById("mic_mode_div").style.display = 'block';
|
||||
document.getElementById("rec_mode_div").style.display = 'none';
|
||||
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
isfilemode=false;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("mic_mode_div").style.display = 'none';
|
||||
document.getElementById("rec_mode_div").style.display = 'block';
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
isfilemode=true;
|
||||
info_div.innerHTML='请点击选择文件';
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getHotwords(){
|
||||
|
||||
var obj = document.getElementById("varHot");
|
||||
|
||||
if(typeof(obj) == 'undefined' || obj==null || obj.value.length<=0){
|
||||
return null;
|
||||
}
|
||||
let val = obj.value.toString();
|
||||
|
||||
console.log("hotwords="+val);
|
||||
let items = val.split(/[(\r\n)\r\n]+/); //split by \r\n
|
||||
var jsonresult = {};
|
||||
const regexNum = /^[0-9]*$/; // test number
|
||||
for (item of items) {
|
||||
|
||||
let result = item.split(" ");
|
||||
if(result.length>=2 && regexNum.test(result[result.length-1]))
|
||||
{
|
||||
var wordstr="";
|
||||
for(var i=0;i<result.length-1;i++)
|
||||
wordstr=wordstr+result[i]+" ";
|
||||
|
||||
jsonresult[wordstr.trim()]= parseInt(result[result.length-1]);
|
||||
}
|
||||
}
|
||||
console.log("jsonresult="+JSON.stringify(jsonresult));
|
||||
return JSON.stringify(jsonresult);
|
||||
|
||||
}
|
||||
function getAsrMode(){
|
||||
|
||||
var item = null;
|
||||
var obj = document.getElementsByName("asr_mode");
|
||||
for (var i = 0; i < obj.length; i++) { //遍历Radio
|
||||
if (obj[i].checked) {
|
||||
item = obj[i].value;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(isfilemode)
|
||||
{
|
||||
item= "offline";
|
||||
}
|
||||
console.log("asr mode"+item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function handleWithTimestamp(tmptext,tmptime)
|
||||
{
|
||||
console.log( "tmptext: " + tmptext);
|
||||
console.log( "tmptime: " + tmptime);
|
||||
if(tmptime==null || tmptime=="undefined" || tmptext.length<=0)
|
||||
{
|
||||
return tmptext;
|
||||
}
|
||||
tmptext=tmptext.replace(/。|?|,|、|\?|\.|\ /g, ","); // in case there are a lot of "。"
|
||||
var words=tmptext.split(","); // split to chinese sentence or english words
|
||||
var jsontime=JSON.parse(tmptime); //JSON.parse(tmptime.replace(/\]\]\[\[/g, "],[")); // in case there are a lot segments by VAD
|
||||
var char_index=0; // index for timestamp
|
||||
var text_withtime="";
|
||||
for(var i=0;i<words.length;i++)
|
||||
{
|
||||
if(words[i]=="undefined" || words[i].length<=0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
console.log("words===",words[i]);
|
||||
console.log( "words: " + words[i]+",time="+jsontime[char_index][0]/1000);
|
||||
if (/^[a-zA-Z]+$/.test(words[i]))
|
||||
{ // if it is english
|
||||
text_withtime=text_withtime+jsontime[char_index][0]/1000+":"+words[i]+"\n";
|
||||
char_index=char_index+1; //for english, timestamp unit is about a word
|
||||
}
|
||||
else{
|
||||
// if it is chinese
|
||||
text_withtime=text_withtime+jsontime[char_index][0]/1000+":"+words[i]+"\n";
|
||||
char_index=char_index+words[i].length; //for chinese, timestamp unit is about a char
|
||||
}
|
||||
}
|
||||
return text_withtime;
|
||||
|
||||
|
||||
}
|
||||
// 语音识别结果; 对jsonMsg数据解析,将识别结果附加到编辑框中
|
||||
function getJsonMessage( jsonMsg ) {
|
||||
//console.log(jsonMsg);
|
||||
console.log( "message: " + JSON.parse(jsonMsg.data)['text'] );
|
||||
var rectxt=""+JSON.parse(jsonMsg.data)['text'];
|
||||
var asrmodel=JSON.parse(jsonMsg.data)['mode'];
|
||||
var is_final=JSON.parse(jsonMsg.data)['is_final'];
|
||||
var timestamp=JSON.parse(jsonMsg.data)['timestamp'];
|
||||
if(asrmodel=="2pass-offline" || asrmodel=="offline")
|
||||
{
|
||||
|
||||
offline_text=offline_text+handleWithTimestamp(rectxt,timestamp); //rectxt; //.replace(/ +/g,"");
|
||||
rec_text=offline_text;
|
||||
}
|
||||
else
|
||||
{
|
||||
rec_text=rec_text+rectxt; //.replace(/ +/g,"");
|
||||
}
|
||||
var varArea=document.getElementById('varArea');
|
||||
|
||||
varArea.value=rec_text;
|
||||
console.log( "offline_text: " + asrmodel+","+offline_text);
|
||||
console.log( "rec_text: " + rec_text);
|
||||
if (isfilemode==true && is_final==true){
|
||||
console.log("call stop ws!");
|
||||
play_file();
|
||||
wsconnecter.wsStop();
|
||||
|
||||
info_div.innerHTML="请点击连接";
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 连接状态响应
|
||||
function getConnState( connState ) {
|
||||
if ( connState === 0 ) { //on open
|
||||
|
||||
|
||||
info_div.innerHTML='连接成功!请点击开始';
|
||||
if (isfilemode==true){
|
||||
info_div.innerHTML='请耐心等待,大文件等待时间更长';
|
||||
start_file_send();
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStart.disabled = false;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
}
|
||||
} else if ( connState === 1 ) {
|
||||
//stop();
|
||||
} else if ( connState === 2 ) {
|
||||
stop();
|
||||
console.log( 'connecttion error' );
|
||||
|
||||
alert("连接地址"+document.getElementById('wssip').value+"失败,请检查asr地址和端口。或试试界面上手动授权,再连接。");
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
|
||||
info_div.innerHTML='请点击连接';
|
||||
}
|
||||
}
|
||||
|
||||
function record()
|
||||
{
|
||||
|
||||
rec.open( function(){
|
||||
rec.start();
|
||||
console.log("开始");
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = false;
|
||||
btnConnect.disabled=true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 识别启动、停止、清空操作
|
||||
function start() {
|
||||
|
||||
// 清除显示
|
||||
clear();
|
||||
//控件状态更新
|
||||
console.log("isfilemode"+isfilemode);
|
||||
|
||||
//启动连接
|
||||
var ret=wsconnecter.wsStart();
|
||||
// 1 is ok, 0 is error
|
||||
if(ret==1){
|
||||
info_div.innerHTML="正在连接asr服务器,请等待...";
|
||||
isRec = true;
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
info_div.innerHTML="请点击开始";
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stop() {
|
||||
var chunk_size = new Array( 5, 10, 5 );
|
||||
var request = {
|
||||
"chunk_size": chunk_size,
|
||||
"wav_name": "h5",
|
||||
"is_speaking": false,
|
||||
"chunk_interval":10,
|
||||
"mode":getAsrMode(),
|
||||
};
|
||||
console.log(request);
|
||||
if(sampleBuf.length>0){
|
||||
wsconnecter.wsSend(sampleBuf);
|
||||
console.log("sampleBuf.length"+sampleBuf.length);
|
||||
sampleBuf=new Int16Array();
|
||||
}
|
||||
wsconnecter.wsSend( JSON.stringify(request) );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 控件状态更新
|
||||
|
||||
isRec = false;
|
||||
info_div.innerHTML="发送完数据,请等候,正在识别...";
|
||||
|
||||
if(isfilemode==false){
|
||||
btnStop.disabled = true;
|
||||
btnStart.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
//wait 3s for asr result
|
||||
setTimeout(function(){
|
||||
console.log("call stop ws!");
|
||||
wsconnecter.wsStop();
|
||||
btnConnect.disabled=false;
|
||||
info_div.innerHTML="请点击连接";}, 3000 );
|
||||
|
||||
|
||||
|
||||
rec.stop(function(blob,duration){
|
||||
|
||||
console.log(blob);
|
||||
var audioBlob = Recorder.pcm2wav(data = {sampleRate:16000, bitRate:16, blob:blob},
|
||||
function(theblob,duration){
|
||||
console.log(theblob);
|
||||
var audio_record = document.getElementById('audio_record');
|
||||
audio_record.src = (window.URL||webkitURL).createObjectURL(theblob);
|
||||
audio_record.controls=true;
|
||||
//audio_record.play();
|
||||
|
||||
|
||||
} ,function(msg){
|
||||
console.log(msg);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
},function(errMsg){
|
||||
console.log("errMsg: " + errMsg);
|
||||
});
|
||||
}
|
||||
// 停止连接
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
||||
var varArea=document.getElementById('varArea');
|
||||
|
||||
varArea.value="";
|
||||
rec_text="";
|
||||
offline_text="";
|
||||
|
||||
}
|
||||
|
||||
|
||||
function recProcess( buffer, powerLevel, bufferDuration, bufferSampleRate,newBufferIdx,asyncEnd ) {
|
||||
if ( isRec === true ) {
|
||||
var data_48k = buffer[buffer.length-1];
|
||||
|
||||
var array_48k = new Array(data_48k);
|
||||
var data_16k=Recorder.SampleData(array_48k,bufferSampleRate,16000).data;
|
||||
|
||||
sampleBuf = Int16Array.from([...sampleBuf, ...data_16k]);
|
||||
var chunk_size=960; // for asr chunk_size [5, 10, 5]
|
||||
info_div.innerHTML=""+bufferDuration/1000+"s";
|
||||
while(sampleBuf.length>=chunk_size){
|
||||
sendBuf=sampleBuf.slice(0,chunk_size);
|
||||
sampleBuf=sampleBuf.slice(chunk_size,sampleBuf.length);
|
||||
wsconnecter.wsSend(sendBuf);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function getUseITN() {
|
||||
var obj = document.getElementsByName("use_itn");
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
if (obj[i].checked) {
|
||||
return obj[i].value === "true";
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
pcm编码器+编码引擎
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
编码原理:本编码器输出的pcm格式数据其实就是Recorder中的buffers原始数据(经过了重新采样),16位时为LE小端模式(Little Endian),并未经过任何编码处理
|
||||
|
||||
编码的代码和wav.js区别不大,pcm加上一个44字节wav头即成wav文件;所以要播放pcm就很简单了,直接转成wav文件来播放,已提供转换函数 Recorder.pcm2wav
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
Recorder.prototype.enc_pcm={
|
||||
stable:true
|
||||
,testmsg:"pcm为未封装的原始音频数据,pcm数据文件无法直接播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
};
|
||||
Recorder.prototype.pcm=function(res,True,False){
|
||||
var This=this,set=This.set
|
||||
,size=res.length
|
||||
,bitRate=set.bitRate==8?8:16;
|
||||
|
||||
var buffer=new ArrayBuffer(size*(bitRate/8));
|
||||
var data=new DataView(buffer);
|
||||
var offset=0;
|
||||
|
||||
// 写入采样数据
|
||||
if(bitRate==8) {
|
||||
for(var i=0;i<size;i++,offset++) {
|
||||
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点,虽然都有明显杂音
|
||||
var val=(res[i]>>8)+128;
|
||||
data.setInt8(offset,val,true);
|
||||
};
|
||||
}else{
|
||||
for (var i=0;i<size;i++,offset+=2){
|
||||
data.setInt16(offset,res[i],true);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
True(new Blob([data.buffer],{type:"audio/pcm"}));
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**pcm直接转码成wav,可以直接用来播放;需同时引入wav.js
|
||||
data: {
|
||||
sampleRate:16000 pcm的采样率
|
||||
bitRate:16 pcm的位数 取值:8 或 16
|
||||
blob:blob对象
|
||||
}
|
||||
data如果直接提供的blob将默认使用16位16khz的配置,仅用于测试
|
||||
True(wavBlob,duration)
|
||||
False(msg)
|
||||
**/
|
||||
Recorder.pcm2wav=function(data,True,False){
|
||||
if(data.slice && data.type!=null){//Blob 测试用
|
||||
data={blob:data};
|
||||
};
|
||||
var sampleRate=data.sampleRate||16000,bitRate=data.bitRate||16;
|
||||
if(!data.sampleRate || !data.bitRate){
|
||||
console.warn("pcm2wav必须提供sampleRate和bitRate");
|
||||
};
|
||||
if(!Recorder.prototype.wav){
|
||||
False("pcm2wav必须先加载wav编码器wav.js");
|
||||
return;
|
||||
};
|
||||
|
||||
var reader=new FileReader();
|
||||
reader.onloadend=function(){
|
||||
var pcm;
|
||||
if(bitRate==8){
|
||||
//8位转成16位
|
||||
var u8arr=new Uint8Array(reader.result);
|
||||
pcm=new Int16Array(u8arr.length);
|
||||
for(var j=0;j<u8arr.length;j++){
|
||||
pcm[j]=(u8arr[j]-128)<<8;
|
||||
};
|
||||
}else{
|
||||
pcm=new Int16Array(reader.result);
|
||||
};
|
||||
|
||||
Recorder({
|
||||
type:"wav"
|
||||
,sampleRate:sampleRate
|
||||
,bitRate:bitRate
|
||||
}).mock(pcm,sampleRate).stop(function(wavBlob,duration){
|
||||
True(wavBlob,duration);
|
||||
},False);
|
||||
};
|
||||
reader.readAsArrayBuffer(data.blob);
|
||||
};
|
||||
|
||||
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
wav编码器+编码引擎
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
当然最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式
|
||||
浏览器支持情况
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
|
||||
编码原理:给pcm数据加上一个44直接的wav头即成wav文件;pcm数据就是Recorder中的buffers原始数据(重新采样),16位时为LE小端模式(Little Endian),实质上是未经过任何编码处理
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
Recorder.prototype.enc_wav={
|
||||
stable:true
|
||||
,testmsg:"支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
};
|
||||
Recorder.prototype.wav=function(res,True,False){
|
||||
var This=this,set=This.set
|
||||
,size=res.length
|
||||
,sampleRate=set.sampleRate
|
||||
,bitRate=set.bitRate==8?8:16;
|
||||
|
||||
//编码数据 https://github.com/mattdiamond/Recorderjs https://www.cnblogs.com/blqw/p/3782420.html https://www.cnblogs.com/xiaoqi/p/6993912.html
|
||||
var dataLength=size*(bitRate/8);
|
||||
var buffer=new ArrayBuffer(44+dataLength);
|
||||
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+dataLength);
|
||||
/* RIFF type */
|
||||
writeString('WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString('fmt ');
|
||||
/* format chunk length */
|
||||
write32(16);
|
||||
/* sample format (raw) */
|
||||
write16(1);
|
||||
/* channel count */
|
||||
write16(1);
|
||||
/* sample rate */
|
||||
write32(sampleRate);
|
||||
/* byte rate (sample rate * block align) */
|
||||
write32(sampleRate*(bitRate/8));// *1 声道
|
||||
/* block align (channel count * bytes per sample) */
|
||||
write16(bitRate/8);// *1 声道
|
||||
/* bits per sample */
|
||||
write16(bitRate);
|
||||
/* data chunk identifier */
|
||||
writeString('data');
|
||||
/* data chunk length */
|
||||
write32(dataLength);
|
||||
// 写入采样数据
|
||||
if(bitRate==8) {
|
||||
for(var i=0;i<size;i++,offset++) {
|
||||
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点,虽然都有明显杂音
|
||||
var val=(res[i]>>8)+128;
|
||||
data.setInt8(offset,val,true);
|
||||
};
|
||||
}else{
|
||||
for (var i=0;i<size;i++,offset+=2){
|
||||
data.setInt16(offset,res[i],true);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
True(new Blob([data.buffer],{type:"audio/wav"}));
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights
|
||||
* Reserved. MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
/* 2021-2023 by zhaoming,mali aihealthx.com */
|
||||
|
||||
function WebSocketConnectMethod( config ) { //定义socket连接方法类
|
||||
|
||||
|
||||
var speechSokt;
|
||||
var connKeeperID;
|
||||
|
||||
var msgHandle = config.msgHandle;
|
||||
var stateHandle = config.stateHandle;
|
||||
|
||||
this.wsStart = function () {
|
||||
var Uri = document.getElementById('wssip').value; //"wss://111.205.137.58:5821/wss/" //设置wss asr online接口地址 如 wss://X.X.X.X:port/wss/
|
||||
if(Uri.match(/wss:\S*|ws:\S*/))
|
||||
{
|
||||
console.log("Uri"+Uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert("请检查wss地址正确性");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( 'WebSocket' in window ) {
|
||||
speechSokt = new WebSocket( Uri ); // 定义socket连接对象
|
||||
speechSokt.onopen = function(e){onOpen(e);}; // 定义响应函数
|
||||
speechSokt.onclose = function(e){
|
||||
console.log("onclose ws!");
|
||||
//speechSokt.close();
|
||||
onClose(e);
|
||||
};
|
||||
speechSokt.onmessage = function(e){onMessage(e);};
|
||||
speechSokt.onerror = function(e){onError(e);};
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
alert('当前浏览器不支持 WebSocket');
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义停止与发送函数
|
||||
this.wsStop = function () {
|
||||
if(speechSokt != undefined) {
|
||||
console.log("stop ws!");
|
||||
speechSokt.close();
|
||||
}
|
||||
};
|
||||
|
||||
this.wsSend = function ( oneData ) {
|
||||
|
||||
if(speechSokt == undefined) return;
|
||||
if ( speechSokt.readyState === 1 ) { // 0:CONNECTING, 1:OPEN, 2:CLOSING, 3:CLOSED
|
||||
|
||||
speechSokt.send( oneData );
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// SOCEKT连接中的消息与状态响应
|
||||
function onOpen( e ) {
|
||||
// 发送json
|
||||
var chunk_size = new Array( 5, 10, 5 );
|
||||
var request = {
|
||||
"chunk_size": chunk_size,
|
||||
"wav_name": "h5",
|
||||
"is_speaking": true,
|
||||
"chunk_interval":10,
|
||||
"itn":getUseITN(),
|
||||
"mode":getAsrMode(),
|
||||
|
||||
};
|
||||
if(isfilemode)
|
||||
{
|
||||
request.wav_format=file_ext;
|
||||
if(file_ext=="wav")
|
||||
{
|
||||
request.wav_format="PCM";
|
||||
request.audio_fs=file_sample_rate;
|
||||
}
|
||||
}
|
||||
|
||||
var hotwords=getHotwords();
|
||||
|
||||
if(hotwords!=null )
|
||||
{
|
||||
request.hotwords=hotwords;
|
||||
}
|
||||
console.log(JSON.stringify(request));
|
||||
speechSokt.send(JSON.stringify(request));
|
||||
console.log("连接成功");
|
||||
stateHandle(0);
|
||||
|
||||
}
|
||||
|
||||
function onClose( e ) {
|
||||
stateHandle(1);
|
||||
}
|
||||
|
||||
function onMessage( e ) {
|
||||
|
||||
msgHandle( e );
|
||||
}
|
||||
|
||||
function onError( e ) {
|
||||
|
||||
info_div.innerHTML="连接"+e;
|
||||
console.log(e);
|
||||
stateHandle(2);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>语音识别</title>
|
||||
|
||||
</head>
|
||||
<body style="margin-left: 3%">
|
||||
<script src="recorder-core.js" charset="UTF-8"></script>
|
||||
<script src="wav.js" charset="UTF-8"></script>
|
||||
<script src="pcm.js" charset="UTF-8"></script>
|
||||
|
||||
|
||||
|
||||
<h1>FunASR Demo</h1>
|
||||
<h3>这里是FunASR开源项目体验demo,集成了VAD、ASR与标点等工业级别的模型,支持长音频离线文件转写,实时语音识别等,开源项目地址:https://github.com/modelscope/FunASR</h3>
|
||||
|
||||
<div class="div_class_topArea">
|
||||
|
||||
<div class="div_class_recordControl">
|
||||
asr服务器地址(必填):
|
||||
<br>
|
||||
<input id="wssip" type="text" onchange="addresschange()" style=" width: 100%;height:100%" value="wss://127.0.0.1:10095/"/>
|
||||
<br>
|
||||
<a id="wsslink" href="#" onclick="window.open('https://127.0.0.1:10095/', '_blank')"><div id="info_wslink">点此处手工授权wss://127.0.0.1:10095/</div></a>
|
||||
<br>
|
||||
<br>
|
||||
<div style="border:2px solid #ccc;">
|
||||
选择录音模式:<br/>
|
||||
|
||||
<label><input name="recoder_mode" onclick="on_recoder_mode_change()" type="radio" value="mic" checked="true"/>麦克风 </label>
|
||||
<label><input name="recoder_mode" onclick="on_recoder_mode_change()" type="radio" value="file" />文件 </label>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div id="mic_mode_div" style="border:2px solid #ccc;display:block;">
|
||||
选择asr模型模式:<br/>
|
||||
|
||||
<label><input name="asr_mode" type="radio" value="2pass" checked="true"/>2pass </label>
|
||||
<label><input name="asr_mode" type="radio" value="online" />online </label>
|
||||
<label><input name="asr_mode" type="radio" value="offline" />offline </label>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="rec_mode_div" style="border:2px solid #ccc;display:none;">
|
||||
|
||||
|
||||
<input type="file" id="upfile">
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<div id="use_itn_div" style="border:2px solid #ccc;display:block;">
|
||||
逆文本标准化(ITN):<br/>
|
||||
<label><input name="use_itn" type="radio" value="false" checked="true"/>否 </label>
|
||||
<label><input name="use_itn" type="radio" value="true" />是 </label>
|
||||
</div>
|
||||
<br>
|
||||
<div style="border:2px solid #ccc;">
|
||||
热词设置(一行一个关键字,空格隔开权重,如"阿里巴巴 20"):
|
||||
<br>
|
||||
|
||||
|
||||
<textarea rows="3" id="varHot" style=" width: 100%;height:100%" >阿里巴巴 20 hello world 40</textarea>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
语音识别结果显示:
|
||||
<br>
|
||||
|
||||
<textarea rows="10" id="varArea" readonly="true" style=" width: 100%;height:100%" ></textarea>
|
||||
<br>
|
||||
<div id="info_div">请点击开始</div>
|
||||
<div class="div_class_buttons">
|
||||
<button id="btnConnect">连接</button>
|
||||
<button id="btnStart">开始</button>
|
||||
<button id="btnStop">停止</button>
|
||||
|
||||
</div>
|
||||
|
||||
<audio id="audio_record" type="audio/wav" controls style="margin-top: 12px; width: 100%;"></audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="wsconnecter.js" charset="utf-8"></script>
|
||||
<script src="main.js" charset="utf-8"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,583 @@
|
||||
/**
|
||||
* Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights
|
||||
* Reserved. MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
/* 2022-2023 by zhaoming,mali aihealthx.com */
|
||||
|
||||
|
||||
// 连接; 定义socket连接类对象与语音对象
|
||||
var wsconnecter = new WebSocketConnectMethod({msgHandle:getJsonMessage,stateHandle:getConnState});
|
||||
var audioBlob;
|
||||
|
||||
// 录音; 定义录音对象,wav格式
|
||||
var rec = Recorder({
|
||||
type:"pcm",
|
||||
bitRate:16,
|
||||
sampleRate:16000,
|
||||
onProcess:recProcess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
var sampleBuf=new Int16Array();
|
||||
// 定义按钮响应事件
|
||||
var btnStart = document.getElementById('btnStart');
|
||||
btnStart.onclick = record;
|
||||
var btnStop = document.getElementById('btnStop');
|
||||
btnStop.onclick = stop;
|
||||
btnStop.disabled = true;
|
||||
btnStart.disabled = true;
|
||||
|
||||
btnConnect= document.getElementById('btnConnect');
|
||||
btnConnect.onclick = start;
|
||||
|
||||
var awsslink= document.getElementById('wsslink');
|
||||
|
||||
|
||||
var rec_text=""; // for online rec asr result
|
||||
var offline_text=""; // for offline rec asr result
|
||||
var info_div = document.getElementById('info_div');
|
||||
|
||||
var upfile = document.getElementById('upfile');
|
||||
|
||||
|
||||
|
||||
var isfilemode=false; // if it is in file mode
|
||||
var file_ext="";
|
||||
var file_sample_rate=16000; //for wav file sample rate
|
||||
var file_data_array; // array to save file data
|
||||
|
||||
var totalsend=0;
|
||||
|
||||
|
||||
var now_ipaddress=window.location.href;
|
||||
now_ipaddress=now_ipaddress.replace("https://","wss://");
|
||||
now_ipaddress=now_ipaddress.replace("static/online/index.html","");
|
||||
var localport=window.location.port;
|
||||
if (localport == '')
|
||||
{
|
||||
now_ipaddress=now_ipaddress.substring(0, now_ipaddress.length - 1) + ':10096' + now_ipaddress.substring(now_ipaddress.length - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
now_ipaddress=now_ipaddress.replace(localport,"10096");
|
||||
}
|
||||
document.getElementById('wssip').value=now_ipaddress;
|
||||
addresschange();
|
||||
function addresschange()
|
||||
{
|
||||
|
||||
var Uri = document.getElementById('wssip').value;
|
||||
document.getElementById('info_wslink').innerHTML="点此处手工授权(IOS手机)";
|
||||
Uri=Uri.replace(/wss/g,"https");
|
||||
console.log("addresschange uri=",Uri);
|
||||
|
||||
awsslink.onclick=function(){
|
||||
window.open(Uri, '_blank');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
upfile.onclick=function()
|
||||
{
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
}
|
||||
|
||||
// from https://github.com/xiangyuecn/Recorder/tree/master
|
||||
var readWavInfo=function(bytes){
|
||||
//读取wav文件头,统一成44字节的头
|
||||
if(bytes.byteLength<44){
|
||||
return null;
|
||||
};
|
||||
var wavView=bytes;
|
||||
var eq=function(p,s){
|
||||
for(var i=0;i<s.length;i++){
|
||||
if(wavView[p+i]!=s.charCodeAt(i)){
|
||||
return false;
|
||||
};
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
if(eq(0,"RIFF")&&eq(8,"WAVEfmt ")){
|
||||
|
||||
var numCh=wavView[22];
|
||||
if(wavView[20]==1 && (numCh==1||numCh==2)){//raw pcm 单或双声道
|
||||
var sampleRate=wavView[24]+(wavView[25]<<8)+(wavView[26]<<16)+(wavView[27]<<24);
|
||||
var bitRate=wavView[34]+(wavView[35]<<8);
|
||||
var heads=[wavView.subarray(0,12)],headSize=12;//head只保留必要的块
|
||||
//搜索data块的位置
|
||||
var dataPos=0; // 44 或有更多块
|
||||
for(var i=12,iL=wavView.length-8;i<iL;){
|
||||
if(wavView[i]==100&&wavView[i+1]==97&&wavView[i+2]==116&&wavView[i+3]==97){//eq(i,"data")
|
||||
heads.push(wavView.subarray(i,i+8));
|
||||
headSize+=8;
|
||||
dataPos=i+8;break;
|
||||
}
|
||||
var i0=i;
|
||||
i+=4;
|
||||
i+=4+wavView[i]+(wavView[i+1]<<8)+(wavView[i+2]<<16)+(wavView[i+3]<<24);
|
||||
if(i0==12){//fmt
|
||||
heads.push(wavView.subarray(i0,i));
|
||||
headSize+=i-i0;
|
||||
}
|
||||
}
|
||||
if(dataPos){
|
||||
var wavHead=new Uint8Array(headSize);
|
||||
for(var i=0,n=0;i<heads.length;i++){
|
||||
wavHead.set(heads[i],n);n+=heads[i].length;
|
||||
}
|
||||
return {
|
||||
sampleRate:sampleRate
|
||||
,bitRate:bitRate
|
||||
,numChannels:numCh
|
||||
,wavHead44:wavHead
|
||||
,dataPos:dataPos
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
upfile.onchange = function () {
|
||||
var len = this.files.length;
|
||||
for(let i = 0; i < len; i++) {
|
||||
|
||||
let fileAudio = new FileReader();
|
||||
fileAudio.readAsArrayBuffer(this.files[i]);
|
||||
|
||||
file_ext=this.files[i].name.split('.').pop().toLowerCase();
|
||||
var audioblob;
|
||||
fileAudio.onload = function() {
|
||||
audioblob = fileAudio.result;
|
||||
|
||||
|
||||
file_data_array=audioblob;
|
||||
|
||||
|
||||
info_div.innerHTML='请点击连接进行识别';
|
||||
|
||||
}
|
||||
|
||||
fileAudio.onerror = function(e) {
|
||||
console.log('error' + e);
|
||||
}
|
||||
}
|
||||
// for wav file, we get the sample rate
|
||||
if(file_ext=="wav")
|
||||
for(let i = 0; i < len; i++) {
|
||||
|
||||
let fileAudio = new FileReader();
|
||||
fileAudio.readAsArrayBuffer(this.files[i]);
|
||||
fileAudio.onload = function() {
|
||||
audioblob = new Uint8Array(fileAudio.result);
|
||||
|
||||
// for wav file, we can get the sample rate
|
||||
var info=readWavInfo(audioblob);
|
||||
console.log(info);
|
||||
file_sample_rate=info.sampleRate;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function play_file()
|
||||
{
|
||||
var audioblob=new Blob( [ new Uint8Array(file_data_array)] , {type :"audio/wav"});
|
||||
var audio_record = document.getElementById('audio_record');
|
||||
audio_record.src = (window.URL||webkitURL).createObjectURL(audioblob);
|
||||
audio_record.controls=true;
|
||||
//audio_record.play(); //not auto play
|
||||
}
|
||||
function start_file_send()
|
||||
{
|
||||
sampleBuf=new Uint8Array( file_data_array );
|
||||
|
||||
var chunk_size=960; // for asr chunk_size [5, 10, 5]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
while(sampleBuf.length>=chunk_size){
|
||||
|
||||
sendBuf=sampleBuf.slice(0,chunk_size);
|
||||
totalsend=totalsend+sampleBuf.length;
|
||||
sampleBuf=sampleBuf.slice(chunk_size,sampleBuf.length);
|
||||
wsconnecter.wsSend(sendBuf);
|
||||
|
||||
|
||||
}
|
||||
|
||||
stop();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function on_recoder_mode_change()
|
||||
{
|
||||
var item = null;
|
||||
var obj = document.getElementsByName("recoder_mode");
|
||||
for (var i = 0; i < obj.length; i++) { //遍历Radio
|
||||
if (obj[i].checked) {
|
||||
item = obj[i].value;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(item=="mic")
|
||||
{
|
||||
document.getElementById("mic_mode_div").style.display = 'block';
|
||||
document.getElementById("rec_mode_div").style.display = 'none';
|
||||
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
isfilemode=false;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("mic_mode_div").style.display = 'none';
|
||||
document.getElementById("rec_mode_div").style.display = 'block';
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
isfilemode=true;
|
||||
info_div.innerHTML='请点击选择文件';
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getHotwords(){
|
||||
|
||||
var obj = document.getElementById("varHot");
|
||||
|
||||
if(typeof(obj) == 'undefined' || obj==null || obj.value.length<=0){
|
||||
return null;
|
||||
}
|
||||
let val = obj.value.toString();
|
||||
|
||||
console.log("hotwords="+val);
|
||||
let items = val.split(/[(\r\n)\r\n]+/); //split by \r\n
|
||||
var jsonresult = {};
|
||||
const regexNum = /^[0-9]*$/; // test number
|
||||
for (item of items) {
|
||||
|
||||
let result = item.split(" ");
|
||||
if(result.length>=2 && regexNum.test(result[result.length-1]))
|
||||
{
|
||||
var wordstr="";
|
||||
for(var i=0;i<result.length-1;i++)
|
||||
wordstr=wordstr+result[i]+" ";
|
||||
|
||||
jsonresult[wordstr.trim()]= parseInt(result[result.length-1]);
|
||||
}
|
||||
}
|
||||
console.log("jsonresult="+JSON.stringify(jsonresult));
|
||||
return JSON.stringify(jsonresult);
|
||||
|
||||
}
|
||||
function getAsrMode(){
|
||||
|
||||
var item = null;
|
||||
var obj = document.getElementsByName("asr_mode");
|
||||
for (var i = 0; i < obj.length; i++) { //遍历Radio
|
||||
if (obj[i].checked) {
|
||||
item = obj[i].value;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(isfilemode)
|
||||
{
|
||||
item= "offline";
|
||||
}
|
||||
console.log("asr mode"+item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function handleWithTimestamp(tmptext,tmptime)
|
||||
{
|
||||
console.log( "tmptext: " + tmptext);
|
||||
console.log( "tmptime: " + tmptime);
|
||||
if(tmptime==null || tmptime=="undefined" || tmptext.length<=0)
|
||||
{
|
||||
return tmptext;
|
||||
}
|
||||
tmptext=tmptext.replace(/。|?|,|、|\?|\.|\ /g, ","); // in case there are a lot of "。"
|
||||
var words=tmptext.split(","); // split to chinese sentence or english words
|
||||
var jsontime=JSON.parse(tmptime); //JSON.parse(tmptime.replace(/\]\]\[\[/g, "],[")); // in case there are a lot segments by VAD
|
||||
var char_index=0; // index for timestamp
|
||||
var text_withtime="";
|
||||
for(var i=0;i<words.length;i++)
|
||||
{
|
||||
if(words[i]=="undefined" || words[i].length<=0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
console.log("words===",words[i]);
|
||||
console.log( "words: " + words[i]+",time="+jsontime[char_index][0]/1000);
|
||||
if (/^[a-zA-Z]+$/.test(words[i]))
|
||||
{ // if it is english
|
||||
text_withtime=text_withtime+jsontime[char_index][0]/1000+":"+words[i]+"\n";
|
||||
char_index=char_index+1; //for english, timestamp unit is about a word
|
||||
}
|
||||
else{
|
||||
// if it is chinese
|
||||
text_withtime=text_withtime+jsontime[char_index][0]/1000+":"+words[i]+"\n";
|
||||
char_index=char_index+words[i].length; //for chinese, timestamp unit is about a char
|
||||
}
|
||||
}
|
||||
return text_withtime;
|
||||
|
||||
|
||||
}
|
||||
// 语音识别结果; 对jsonMsg数据解析,将识别结果附加到编辑框中
|
||||
function getJsonMessage( jsonMsg ) {
|
||||
//console.log(jsonMsg);
|
||||
console.log( "message: " + JSON.parse(jsonMsg.data)['text'] );
|
||||
var rectxt=""+JSON.parse(jsonMsg.data)['text'];
|
||||
var asrmodel=JSON.parse(jsonMsg.data)['mode'];
|
||||
var is_final=JSON.parse(jsonMsg.data)['is_final'];
|
||||
var timestamp=JSON.parse(jsonMsg.data)['timestamp'];
|
||||
if(asrmodel=="2pass-offline" || asrmodel=="offline")
|
||||
{
|
||||
|
||||
offline_text=offline_text+handleWithTimestamp(rectxt,timestamp); //rectxt; //.replace(/ +/g,"");
|
||||
rec_text=offline_text;
|
||||
}
|
||||
else
|
||||
{
|
||||
rec_text=rec_text+rectxt; //.replace(/ +/g,"");
|
||||
}
|
||||
var varArea=document.getElementById('varArea');
|
||||
|
||||
varArea.value=rec_text;
|
||||
console.log( "offline_text: " + asrmodel+","+offline_text);
|
||||
console.log( "rec_text: " + rec_text);
|
||||
if (isfilemode==true && is_final==true){
|
||||
console.log("call stop ws!");
|
||||
play_file();
|
||||
wsconnecter.wsStop();
|
||||
|
||||
info_div.innerHTML="请点击连接";
|
||||
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 连接状态响应
|
||||
function getConnState( connState ) {
|
||||
if ( connState === 0 ) { //on open
|
||||
|
||||
|
||||
info_div.innerHTML='连接成功!请点击开始';
|
||||
if (isfilemode==true){
|
||||
info_div.innerHTML='请耐心等待,大文件等待时间更长';
|
||||
start_file_send();
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStart.disabled = false;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
}
|
||||
} else if ( connState === 1 ) {
|
||||
//stop();
|
||||
} else if ( connState === 2 ) {
|
||||
stop();
|
||||
console.log( 'connecttion error' );
|
||||
|
||||
alert("连接地址"+document.getElementById('wssip').value+"失败,请检查asr地址和端口。或试试界面上手动授权,再连接。");
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
|
||||
info_div.innerHTML='请点击连接';
|
||||
}
|
||||
}
|
||||
|
||||
function record()
|
||||
{
|
||||
|
||||
rec.open( function(){
|
||||
rec.start();
|
||||
console.log("开始");
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = false;
|
||||
btnConnect.disabled=true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 识别启动、停止、清空操作
|
||||
function start() {
|
||||
|
||||
// 清除显示
|
||||
clear();
|
||||
//控件状态更新
|
||||
console.log("isfilemode"+isfilemode);
|
||||
|
||||
//启动连接
|
||||
var ret=wsconnecter.wsStart();
|
||||
// 1 is ok, 0 is error
|
||||
if(ret==1){
|
||||
info_div.innerHTML="正在连接asr服务器,请等待...";
|
||||
isRec = true;
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
info_div.innerHTML="请点击开始";
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
btnConnect.disabled=false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stop() {
|
||||
var chunk_size = new Array( 5, 10, 5 );
|
||||
var request = {
|
||||
"chunk_size": chunk_size,
|
||||
"wav_name": "h5",
|
||||
"is_speaking": false,
|
||||
"chunk_interval":10,
|
||||
"mode":getAsrMode(),
|
||||
};
|
||||
console.log(request);
|
||||
if(sampleBuf.length>0){
|
||||
wsconnecter.wsSend(sampleBuf);
|
||||
console.log("sampleBuf.length"+sampleBuf.length);
|
||||
sampleBuf=new Int16Array();
|
||||
}
|
||||
wsconnecter.wsSend( JSON.stringify(request) );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 控件状态更新
|
||||
|
||||
isRec = false;
|
||||
info_div.innerHTML="发送完数据,请等候,正在识别...";
|
||||
|
||||
if(isfilemode==false){
|
||||
btnStop.disabled = true;
|
||||
btnStart.disabled = true;
|
||||
btnConnect.disabled=true;
|
||||
//wait 3s for asr result
|
||||
setTimeout(function(){
|
||||
console.log("call stop ws!");
|
||||
wsconnecter.wsStop();
|
||||
btnConnect.disabled=false;
|
||||
info_div.innerHTML="请点击连接";}, 3000 );
|
||||
|
||||
|
||||
|
||||
rec.stop(function(blob,duration){
|
||||
|
||||
console.log(blob);
|
||||
var audioBlob = Recorder.pcm2wav(data = {sampleRate:16000, bitRate:16, blob:blob},
|
||||
function(theblob,duration){
|
||||
console.log(theblob);
|
||||
var audio_record = document.getElementById('audio_record');
|
||||
audio_record.src = (window.URL||webkitURL).createObjectURL(theblob);
|
||||
audio_record.controls=true;
|
||||
//audio_record.play();
|
||||
|
||||
|
||||
} ,function(msg){
|
||||
console.log(msg);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
},function(errMsg){
|
||||
console.log("errMsg: " + errMsg);
|
||||
});
|
||||
}
|
||||
// 停止连接
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
||||
var varArea=document.getElementById('varArea');
|
||||
|
||||
varArea.value="";
|
||||
rec_text="";
|
||||
offline_text="";
|
||||
|
||||
}
|
||||
|
||||
|
||||
function recProcess( buffer, powerLevel, bufferDuration, bufferSampleRate,newBufferIdx,asyncEnd ) {
|
||||
if ( isRec === true ) {
|
||||
var data_48k = buffer[buffer.length-1];
|
||||
|
||||
var array_48k = new Array(data_48k);
|
||||
var data_16k=Recorder.SampleData(array_48k,bufferSampleRate,16000).data;
|
||||
|
||||
sampleBuf = Int16Array.from([...sampleBuf, ...data_16k]);
|
||||
var chunk_size=960; // for asr chunk_size [5, 10, 5]
|
||||
info_div.innerHTML=""+bufferDuration/1000+"s";
|
||||
while(sampleBuf.length>=chunk_size){
|
||||
sendBuf=sampleBuf.slice(0,chunk_size);
|
||||
sampleBuf=sampleBuf.slice(chunk_size,sampleBuf.length);
|
||||
wsconnecter.wsSend(sendBuf);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function getUseITN() {
|
||||
var obj = document.getElementsByName("use_itn");
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
if (obj[i].checked) {
|
||||
return obj[i].value === "true";
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
pcm编码器+编码引擎
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
编码原理:本编码器输出的pcm格式数据其实就是Recorder中的buffers原始数据(经过了重新采样),16位时为LE小端模式(Little Endian),并未经过任何编码处理
|
||||
|
||||
编码的代码和wav.js区别不大,pcm加上一个44字节wav头即成wav文件;所以要播放pcm就很简单了,直接转成wav文件来播放,已提供转换函数 Recorder.pcm2wav
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
Recorder.prototype.enc_pcm={
|
||||
stable:true
|
||||
,testmsg:"pcm为未封装的原始音频数据,pcm数据文件无法直接播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
};
|
||||
Recorder.prototype.pcm=function(res,True,False){
|
||||
var This=this,set=This.set
|
||||
,size=res.length
|
||||
,bitRate=set.bitRate==8?8:16;
|
||||
|
||||
var buffer=new ArrayBuffer(size*(bitRate/8));
|
||||
var data=new DataView(buffer);
|
||||
var offset=0;
|
||||
|
||||
// 写入采样数据
|
||||
if(bitRate==8) {
|
||||
for(var i=0;i<size;i++,offset++) {
|
||||
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点,虽然都有明显杂音
|
||||
var val=(res[i]>>8)+128;
|
||||
data.setInt8(offset,val,true);
|
||||
};
|
||||
}else{
|
||||
for (var i=0;i<size;i++,offset+=2){
|
||||
data.setInt16(offset,res[i],true);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
True(new Blob([data.buffer],{type:"audio/pcm"}));
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**pcm直接转码成wav,可以直接用来播放;需同时引入wav.js
|
||||
data: {
|
||||
sampleRate:16000 pcm的采样率
|
||||
bitRate:16 pcm的位数 取值:8 或 16
|
||||
blob:blob对象
|
||||
}
|
||||
data如果直接提供的blob将默认使用16位16khz的配置,仅用于测试
|
||||
True(wavBlob,duration)
|
||||
False(msg)
|
||||
**/
|
||||
Recorder.pcm2wav=function(data,True,False){
|
||||
if(data.slice && data.type!=null){//Blob 测试用
|
||||
data={blob:data};
|
||||
};
|
||||
var sampleRate=data.sampleRate||16000,bitRate=data.bitRate||16;
|
||||
if(!data.sampleRate || !data.bitRate){
|
||||
console.warn("pcm2wav必须提供sampleRate和bitRate");
|
||||
};
|
||||
if(!Recorder.prototype.wav){
|
||||
False("pcm2wav必须先加载wav编码器wav.js");
|
||||
return;
|
||||
};
|
||||
|
||||
var reader=new FileReader();
|
||||
reader.onloadend=function(){
|
||||
var pcm;
|
||||
if(bitRate==8){
|
||||
//8位转成16位
|
||||
var u8arr=new Uint8Array(reader.result);
|
||||
pcm=new Int16Array(u8arr.length);
|
||||
for(var j=0;j<u8arr.length;j++){
|
||||
pcm[j]=(u8arr[j]-128)<<8;
|
||||
};
|
||||
}else{
|
||||
pcm=new Int16Array(reader.result);
|
||||
};
|
||||
|
||||
Recorder({
|
||||
type:"wav"
|
||||
,sampleRate:sampleRate
|
||||
,bitRate:bitRate
|
||||
}).mock(pcm,sampleRate).stop(function(wavBlob,duration){
|
||||
True(wavBlob,duration);
|
||||
},False);
|
||||
};
|
||||
reader.readAsArrayBuffer(data.blob);
|
||||
};
|
||||
|
||||
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
wav编码器+编码引擎
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
当然最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式
|
||||
浏览器支持情况
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
|
||||
编码原理:给pcm数据加上一个44直接的wav头即成wav文件;pcm数据就是Recorder中的buffers原始数据(重新采样),16位时为LE小端模式(Little Endian),实质上是未经过任何编码处理
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
Recorder.prototype.enc_wav={
|
||||
stable:true
|
||||
,testmsg:"支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
};
|
||||
Recorder.prototype.wav=function(res,True,False){
|
||||
var This=this,set=This.set
|
||||
,size=res.length
|
||||
,sampleRate=set.sampleRate
|
||||
,bitRate=set.bitRate==8?8:16;
|
||||
|
||||
//编码数据 https://github.com/mattdiamond/Recorderjs https://www.cnblogs.com/blqw/p/3782420.html https://www.cnblogs.com/xiaoqi/p/6993912.html
|
||||
var dataLength=size*(bitRate/8);
|
||||
var buffer=new ArrayBuffer(44+dataLength);
|
||||
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+dataLength);
|
||||
/* RIFF type */
|
||||
writeString('WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString('fmt ');
|
||||
/* format chunk length */
|
||||
write32(16);
|
||||
/* sample format (raw) */
|
||||
write16(1);
|
||||
/* channel count */
|
||||
write16(1);
|
||||
/* sample rate */
|
||||
write32(sampleRate);
|
||||
/* byte rate (sample rate * block align) */
|
||||
write32(sampleRate*(bitRate/8));// *1 声道
|
||||
/* block align (channel count * bytes per sample) */
|
||||
write16(bitRate/8);// *1 声道
|
||||
/* bits per sample */
|
||||
write16(bitRate);
|
||||
/* data chunk identifier */
|
||||
writeString('data');
|
||||
/* data chunk length */
|
||||
write32(dataLength);
|
||||
// 写入采样数据
|
||||
if(bitRate==8) {
|
||||
for(var i=0;i<size;i++,offset++) {
|
||||
//16转8据说是雷霄骅的 https://blog.csdn.net/sevennight1989/article/details/85376149 细节比blqw的按比例的算法清晰点,虽然都有明显杂音
|
||||
var val=(res[i]>>8)+128;
|
||||
data.setInt8(offset,val,true);
|
||||
};
|
||||
}else{
|
||||
for (var i=0;i<size;i++,offset+=2){
|
||||
data.setInt16(offset,res[i],true);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
True(new Blob([data.buffer],{type:"audio/wav"}));
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights
|
||||
* Reserved. MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
/* 2021-2023 by zhaoming,mali aihealthx.com */
|
||||
|
||||
function WebSocketConnectMethod( config ) { //定义socket连接方法类
|
||||
|
||||
|
||||
var speechSokt;
|
||||
var connKeeperID;
|
||||
|
||||
var msgHandle = config.msgHandle;
|
||||
var stateHandle = config.stateHandle;
|
||||
|
||||
this.wsStart = function () {
|
||||
var Uri = document.getElementById('wssip').value; //"wss://111.205.137.58:5821/wss/" //设置wss asr online接口地址 如 wss://X.X.X.X:port/wss/
|
||||
if(Uri.match(/wss:\S*|ws:\S*/))
|
||||
{
|
||||
console.log("Uri"+Uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert("请检查wss地址正确性");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( 'WebSocket' in window ) {
|
||||
speechSokt = new WebSocket( Uri ); // 定义socket连接对象
|
||||
speechSokt.onopen = function(e){onOpen(e);}; // 定义响应函数
|
||||
speechSokt.onclose = function(e){
|
||||
console.log("onclose ws!");
|
||||
//speechSokt.close();
|
||||
onClose(e);
|
||||
};
|
||||
speechSokt.onmessage = function(e){onMessage(e);};
|
||||
speechSokt.onerror = function(e){onError(e);};
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
alert('当前浏览器不支持 WebSocket');
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义停止与发送函数
|
||||
this.wsStop = function () {
|
||||
if(speechSokt != undefined) {
|
||||
console.log("stop ws!");
|
||||
speechSokt.close();
|
||||
}
|
||||
};
|
||||
|
||||
this.wsSend = function ( oneData ) {
|
||||
|
||||
if(speechSokt == undefined) return;
|
||||
if ( speechSokt.readyState === 1 ) { // 0:CONNECTING, 1:OPEN, 2:CLOSING, 3:CLOSED
|
||||
|
||||
speechSokt.send( oneData );
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// SOCEKT连接中的消息与状态响应
|
||||
function onOpen( e ) {
|
||||
// 发送json
|
||||
var chunk_size = new Array( 5, 10, 5 );
|
||||
var request = {
|
||||
"chunk_size": chunk_size,
|
||||
"wav_name": "h5",
|
||||
"is_speaking": true,
|
||||
"chunk_interval":10,
|
||||
"itn":getUseITN(),
|
||||
"mode":getAsrMode(),
|
||||
|
||||
};
|
||||
if(isfilemode)
|
||||
{
|
||||
request.wav_format=file_ext;
|
||||
if(file_ext=="wav")
|
||||
{
|
||||
request.wav_format="PCM";
|
||||
request.audio_fs=file_sample_rate;
|
||||
}
|
||||
}
|
||||
|
||||
var hotwords=getHotwords();
|
||||
|
||||
if(hotwords!=null )
|
||||
{
|
||||
request.hotwords=hotwords;
|
||||
}
|
||||
console.log(JSON.stringify(request));
|
||||
speechSokt.send(JSON.stringify(request));
|
||||
console.log("连接成功");
|
||||
stateHandle(0);
|
||||
|
||||
}
|
||||
|
||||
function onClose( e ) {
|
||||
stateHandle(1);
|
||||
}
|
||||
|
||||
function onMessage( e ) {
|
||||
|
||||
msgHandle( e );
|
||||
}
|
||||
|
||||
function onError( e ) {
|
||||
|
||||
info_div.innerHTML="连接"+e;
|
||||
console.log(e);
|
||||
stateHandle(2);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
此目录是为了建设www.funasr.com网站,目前已经有了域名与公网服务器,欢迎社区用户参与建设,主要从3方面:
|
||||
|
||||
- http服务器搭建
|
||||
- html网页设计
|
||||
- funasr相关材料
|
||||
|
||||
其中,http服务器,不限制编程语言,推荐python-http(可维护性好?)?
|
||||
|
||||
html设计可以参考whisper( https://openai.com/research/whisper )
|
||||
|
||||
需要展示内容:
|
||||
|
||||
1、funasr介绍
|
||||
|
||||
可以AIGC生成个语音相关的炫酷配图
|
||||
|
||||
配上文字介绍
|
||||
|
||||
FunASR希望在语音识别的学术研究和工业应用之间架起一座桥梁。通过发布工业级语音识别模型的训练和微调,研究人员和开发人员可以更方便地进行语音识别模型的研究和生产,并推动语音识别生态的发展。让语音识别更有趣!
|
||||
|
||||
2、核心功能
|
||||
|
||||
FunASR是一个基础语音识别工具包,提供多种功能,包括语音识别(ASR)、语音端点检测(VAD)、标点恢复、语言模型、说话人验证、说话人分离和多人对话语音识别等。
|
||||
|
||||
3、Paraformer模型介绍
|
||||
|
||||
Paraformer模型结构图(已有)
|
||||
|
||||
文字描述:
|
||||
|
||||
Paraformer是一种非自回归端到端语音识别模型。非自回归模型相比于目前主流的自回归模型,可以并行的对整条句子输出目标文字,特别适合利用GPU进行并行推理。Paraformer是目前已知的首个在工业大数据上可以获得和自回归端到端模型相同性能的非自回归模型。配合GPU推理,可以将推理效率提升10倍,从而将语音识别云服务的机器成本降低接近10倍。
|
||||
|
||||
4、离线文件转写服务
|
||||
|
||||
原理图
|
||||
|
||||
文字介绍:
|
||||
|
||||
FunASR离线文件转写软件包,提供了一款功能强大的语音离线文件转写服务。拥有完整的语音识别链路,结合了语音端点检测、语音识别、标点等模型,可以将几十个小时的长音频与视频识别成带标点的文字,而且支持上百路请求同时进行转写。输出为带标点的文字,含有字级别时间戳,支持ITN与用户自定义热词等。服务端集成有ffmpeg,支持各种音视频格式输入。软件包提供有html、python、c++、java与c#等多种编程语言客户端,用户可以直接使用与进一步开发。
|
||||
|
||||
在线体验:
|
||||
https://121.43.113.106:1335/static/index.html
|
||||
|
||||
安装:
|
||||
|
||||
https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_offline_zh.md
|
||||
|
||||
使用:
|
||||
|
||||
https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gz
|
||||
|
||||
|
||||
视频教程链接:
|
||||
|
||||
https://www.bilibili.com/video/BV11a4y1U72y/?share_source=copy_web&vd_source=f6576748261a1b738a71ad618d380438
|
||||
|
||||
5、实时听写
|
||||
|
||||
原理图
|
||||
|
||||
文字介绍:
|
||||
|
||||
FunASR实时语音听写软件包,集成了实时版本的语音端点检测模型、语音识别、语音识别、标点预测模型等。采用多模型协同,既可以实时的进行语音转文字,也可以在说话句尾用高精度转写文字修正输出,输出文字带有标点,支持多路请求。依据使用者场景不同,支持实时语音听写服务(online)、非实时一句话转写(offline)与实时与非实时一体化协同(2pass)3种服务模式。软件包提供有html、python、c++、java与c#等多种编程语言客户端,用户可以直接使用与进一步开发。
|
||||
|
||||
在线体验:
|
||||
https://121.43.113.106:1336/static/index.html
|
||||
|
||||
安装:
|
||||
|
||||
https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md
|
||||
|
||||
使用:
|
||||
|
||||
https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gz
|
||||
|
||||
视频教程链接:
|
||||
|
||||
https://www.bilibili.com/video/BV1Yw411K7LY/?share_source=copy_web&vd_source=f6576748261a1b738a71ad618d380438
|
||||
|
||||
|
||||
|
||||
|
||||
6、github
|
||||
|
||||
https://github.com/modelscope/FunASR
|
||||
|
||||
7、社区交流
|
||||
|
||||
|
||||
# 部署
|
||||
git clone https://github.com/modelscope/FunASR.git
|
||||
cd FunASR/web-pages
|
||||
npm install
|
||||
# 开发模式
|
||||
npm run dev
|
||||
# 产品模式
|
||||
npm run example
|
||||
@@ -0,0 +1,5 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@function p($px) {
|
||||
@return #{math.div($px, 19.2) + 'rem'};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
// 全局第三方和公司组件样式覆盖
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.ui-jessibuca {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="ui-jessibuca"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ui-jessibuca',
|
||||
props: {
|
||||
videoUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBandwidth: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
fullscreenFlag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
screenshotFlag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
playFlag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
audioFlag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
jessibuca: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
videoUrl: {
|
||||
immediate: true,
|
||||
handler (newVal, oldVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
setTimeout(() => {
|
||||
this.jessibuca.play(newVal)
|
||||
}, 100)
|
||||
} else if (this.jessibuca) {
|
||||
this.jessibuca.destroy()
|
||||
this.jessibuca = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {},
|
||||
mounted () {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.jessibuca && this.jessibuca.destroy()
|
||||
},
|
||||
methods: {
|
||||
init () {
|
||||
const that = this
|
||||
if (!this.jessibuca && this.$el) {
|
||||
this.jessibuca = new window.Jessibuca({
|
||||
container: this.$el,
|
||||
videoBuffer: 0.2, // 缓存时长
|
||||
isResize: false,
|
||||
text: '',
|
||||
loadingText: '加载中',
|
||||
debug: false,
|
||||
useMSE: true, // 开启硬编码
|
||||
showBandwidth: this.showBandwidth, // 显示网速
|
||||
controlAutoHide: true,
|
||||
operateBtns: {
|
||||
fullscreen: this.fullscreenFlag,
|
||||
screenshot: this.screenshotFlag,
|
||||
play: this.playFlag,
|
||||
audio: this.audioFlag
|
||||
},
|
||||
forceNoOffscreen: true,
|
||||
isNotMute: false
|
||||
})
|
||||
this.jessibuca.on('error', function (error) {
|
||||
console.log('error:', error)
|
||||
})
|
||||
|
||||
this.jessibuca.on('fullscreen', function (flag) {
|
||||
console.log('is fullscreen', flag)
|
||||
that.$emit('fullscreen', flag)
|
||||
})
|
||||
|
||||
this.jessibuca.on('webFullscreen', function (flag) {
|
||||
console.log('is webFullscreen', flag)
|
||||
that.$emit('webFullscreen', flag)
|
||||
})
|
||||
}
|
||||
},
|
||||
setFullscreen (flag) {
|
||||
this.jessibuca.setFullscreen(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/index.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,72 @@
|
||||
.ui-scrollbar {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.child-over-width {
|
||||
.ui-scrollbar__view {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active > .ui-scrollbar__bar,
|
||||
&:focus > .ui-scrollbar__bar,
|
||||
&:hover > .ui-scrollbar__bar {
|
||||
opacity: 1;
|
||||
transition: opacity 0.34s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-scrollbar__wrap {
|
||||
overflow: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-scrollbar__view {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ui-scrollbar__bar {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 120ms ease-out;
|
||||
transition: opacity 120ms ease-out;
|
||||
|
||||
&.is-horizontal {
|
||||
height: 6px;
|
||||
left: 2px;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-vertical {
|
||||
width: 6px;
|
||||
top: 2px;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-scrollbar__thumb {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
cursor: pointer;
|
||||
border-radius: inherit;
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
-webkit-transition: 0.3s background-color;
|
||||
transition: 0.3s background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(220, 4%, 58%, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<script>
|
||||
import { on, off, renderThumbStyle, BAR_MAP } from './util'
|
||||
|
||||
export default {
|
||||
name: 'scrollbar-Bar',
|
||||
props: {
|
||||
vertical: Boolean,
|
||||
size: String,
|
||||
move: Number
|
||||
},
|
||||
computed: {
|
||||
bar () {
|
||||
return BAR_MAP[this.vertical ? 'vertical' : 'horizontal']
|
||||
},
|
||||
|
||||
wrap () {
|
||||
return this.$parent.wrap
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
off(document, 'mouseup', this.mouseUpDocumentHandler)
|
||||
},
|
||||
methods: {
|
||||
clickThumbHandler (e) {
|
||||
// prevent click event of right button
|
||||
if (e.ctrlKey || e.button === 2) {
|
||||
return
|
||||
}
|
||||
this.startDrag(e)
|
||||
this[this.bar.axis] =
|
||||
e.currentTarget[this.bar.offset] -
|
||||
(e[this.bar.client] -
|
||||
e.currentTarget.getBoundingClientRect()[this.bar.direction])
|
||||
},
|
||||
|
||||
clickTrackHandler (e) {
|
||||
const offset = Math.abs(
|
||||
e.target.getBoundingClientRect()[this.bar.direction] -
|
||||
e[this.bar.client]
|
||||
)
|
||||
const thumbHalf = this.$refs.thumb[this.bar.offset] / 2
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbHalf) * 100) / this.$el[this.bar.offset]
|
||||
|
||||
this.wrap[this.bar.scroll] =
|
||||
(thumbPositionPercentage * this.wrap[this.bar.scrollSize]) /
|
||||
100
|
||||
},
|
||||
|
||||
startDrag (e) {
|
||||
e.stopImmediatePropagation()
|
||||
this.cursorDown = true
|
||||
|
||||
on(document, 'mousemove', this.mouseMoveDocumentHandler)
|
||||
on(document, 'mouseup', this.mouseUpDocumentHandler)
|
||||
document.onselectstart = () => false
|
||||
},
|
||||
|
||||
mouseMoveDocumentHandler (e) {
|
||||
if (this.cursorDown === false) return
|
||||
const prevPage = this[this.bar.axis]
|
||||
|
||||
if (!prevPage) return
|
||||
|
||||
const offset =
|
||||
(this.$el.getBoundingClientRect()[this.bar.direction] -
|
||||
e[this.bar.client]) *
|
||||
-1
|
||||
const thumbClickPosition =
|
||||
this.$refs.thumb[this.bar.offset] - prevPage
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbClickPosition) * 100) /
|
||||
this.$el[this.bar.offset]
|
||||
|
||||
this.wrap[this.bar.scroll] =
|
||||
(thumbPositionPercentage * this.wrap[this.bar.scrollSize]) /
|
||||
100
|
||||
},
|
||||
|
||||
mouseUpDocumentHandler (e) {
|
||||
this.cursorDown = false
|
||||
this[this.bar.axis] = 0
|
||||
off(document, 'mousemove', this.mouseMoveDocumentHandler)
|
||||
document.onselectstart = null
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
const { size, move, bar } = this
|
||||
return (
|
||||
<div
|
||||
class={['ui-scrollbar__bar', 'is-' + bar.key]}
|
||||
onMousedown={this.clickTrackHandler}
|
||||
>
|
||||
<div
|
||||
ref="thumb"
|
||||
class="ui-scrollbar__thumb"
|
||||
onMousedown={this.clickThumbHandler}
|
||||
style={renderThumbStyle({ size, move, bar })}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,174 @@
|
||||
|
||||
<script>
|
||||
import { addResizeListener, removeResizeListener, toObject } from './util'
|
||||
import scrollbarWidth from './scrollbar-width'
|
||||
import Bar from './bar.vue'
|
||||
require('./assets/css/index.scss')
|
||||
|
||||
export default {
|
||||
name: 'ui-scrollbar',
|
||||
components: { Bar },
|
||||
props: {
|
||||
native: Boolean,
|
||||
childOverWidth: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
wrapStyle: {},
|
||||
wrapClass: {},
|
||||
viewClass: {},
|
||||
viewStyle: {},
|
||||
noresize: Boolean, // 如果 container 尺寸不会发生变化,最好设置它可以优化性能
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
sizeWidth: '0',
|
||||
sizeHeight: '0',
|
||||
moveX: 0,
|
||||
moveY: 0,
|
||||
scrollbarMiddle: 0
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
wrap () {
|
||||
return this.$refs.wrap
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.native) return
|
||||
this.$nextTick(this.update)
|
||||
!this.noresize && addResizeListener(this.$refs.resize, this.update)
|
||||
!this.noresize && addResizeListener(this.$refs.wrap, this.update)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
if (this.native) return
|
||||
!this.noresize && removeResizeListener(this.$refs.resize, this.update)
|
||||
!this.noresize && removeResizeListener(this.$refs.wrap, this.update)
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleScroll () {
|
||||
const wrap = this.wrap
|
||||
|
||||
this.moveY = (wrap.scrollTop * 100) / wrap.clientHeight
|
||||
this.moveX = (wrap.scrollLeft * 100) / wrap.clientWidth
|
||||
if (wrap.scrollTop + wrap.clientHeight === wrap.scrollHeight) {
|
||||
this.$emit('scrollbottom')
|
||||
}
|
||||
if (wrap.scrollTop === 0) {
|
||||
this.$emit('scrolltop')
|
||||
}
|
||||
this.$emit('scrollMove', {
|
||||
moveX: wrap.scrollLeft,
|
||||
moveY: wrap.scrollTop,
|
||||
clientX: wrap.clientWidth,
|
||||
clientY: wrap.clientHeight,
|
||||
scrollHeight: wrap.scrollHeight,
|
||||
scrollWidth: wrap.scrollWidth
|
||||
})
|
||||
},
|
||||
|
||||
update () {
|
||||
let heightPercentage = null
|
||||
let widthPercentage = null
|
||||
const wrap = this.wrap
|
||||
if (!wrap) return
|
||||
|
||||
heightPercentage = (wrap.clientHeight * 100) / wrap.scrollHeight
|
||||
widthPercentage = (wrap.clientWidth * 100) / wrap.scrollWidth
|
||||
|
||||
this.sizeHeight = heightPercentage < 100 ? heightPercentage + '%' : ''
|
||||
this.sizeWidth = widthPercentage < 100 ? widthPercentage + '%' : ''
|
||||
},
|
||||
continueScroll (rowNum) {
|
||||
const wrap = this.wrap
|
||||
if (this.scrollbarMiddle === 0) {
|
||||
this.scrollbarMiddle = wrap.scrollTop / 2 + wrap.clientHeight / 2 - wrap.clientHeight / rowNum
|
||||
}
|
||||
wrap.scrollTop = this.scrollbarMiddle
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const gutter = scrollbarWidth()
|
||||
let style = this.wrapStyle
|
||||
|
||||
if (gutter) {
|
||||
const gutterWith = `-${gutter}px`
|
||||
let gutterStyle = ''
|
||||
|
||||
let overFlowXStr = ''
|
||||
let overflowYStr = ''
|
||||
if (style && style.length > 0) {
|
||||
style.forEach((styleItem) => {
|
||||
if (styleItem['overflow-x'] || styleItem.overflowX) {
|
||||
overFlowXStr = styleItem['overflow-x'] || styleItem.overflowX
|
||||
}
|
||||
if (styleItem['overflow-y'] || styleItem.overflowY) {
|
||||
overflowYStr = styleItem['overflow-y'] || styleItem.overflowY
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (overFlowXStr === 'hidden') {
|
||||
gutterStyle = `margin-right: ${gutterWith};`
|
||||
} else {
|
||||
gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`
|
||||
}
|
||||
|
||||
if (Array.isArray(this.wrapStyle)) {
|
||||
style = toObject(this.wrapStyle)
|
||||
if (overFlowXStr !== 'hidden') {
|
||||
style.marginBottom = gutterWith
|
||||
}
|
||||
if (overflowYStr !== 'hidden') {
|
||||
style.marginRight = gutterWith
|
||||
}
|
||||
} else if (typeof this.wrapStyle === 'string') {
|
||||
style += gutterStyle
|
||||
} else {
|
||||
style = gutterStyle
|
||||
}
|
||||
}
|
||||
const view = h(
|
||||
this.tag,
|
||||
{
|
||||
class: ['ui-scrollbar__view', this.viewClass],
|
||||
style: this.viewStyle,
|
||||
ref: 'resize'
|
||||
},
|
||||
this.$slots.default
|
||||
)
|
||||
const wrap = (
|
||||
<div
|
||||
ref="wrap"
|
||||
style={style}
|
||||
onScroll={this.handleScroll}
|
||||
class={[this.wrapClass, 'ui-scrollbar__wrap', gutter ? '' : 'ui-scrollbar__wrap--hidden-default']}
|
||||
>{[view]}</div>
|
||||
)
|
||||
let nodes
|
||||
|
||||
if (!this.native) {
|
||||
nodes = [
|
||||
wrap,
|
||||
<Bar move={this.moveX} size={this.sizeWidth}></Bar>,
|
||||
<Bar vertical move={this.moveY} size={this.sizeHeight}></Bar>
|
||||
]
|
||||
} else {
|
||||
nodes = [
|
||||
<div ref="wrap" class={[this.wrapClass, 'ui-scrollbar__wrap']} style={style}>{[view]}</div>
|
||||
]
|
||||
}
|
||||
return h('div', { class: ['ui-scrollbar', { 'child-over-width': !this.childOverWidth }] }, nodes)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
let scrollBarWidth
|
||||
|
||||
export default function () {
|
||||
if (Vue.prototype.$isServer) return 0
|
||||
if (scrollBarWidth !== undefined) return scrollBarWidth
|
||||
|
||||
const outer = document.createElement('div')
|
||||
outer.className = 'ui-scrollbar__wrap'
|
||||
outer.style.visibility = 'hidden'
|
||||
outer.style.width = '100px'
|
||||
outer.style.position = 'absolute'
|
||||
outer.style.top = '-9999px'
|
||||
document.body.appendChild(outer)
|
||||
|
||||
const widthNoScroll = outer.offsetWidth
|
||||
outer.style.overflow = 'scroll'
|
||||
|
||||
const inner = document.createElement('div')
|
||||
inner.style.width = '100%'
|
||||
outer.appendChild(inner)
|
||||
|
||||
const widthWithScroll = inner.offsetWidth
|
||||
outer.parentNode.removeChild(outer)
|
||||
scrollBarWidth = widthNoScroll - widthWithScroll
|
||||
|
||||
return scrollBarWidth
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
|
||||
export const BAR_MAP = {
|
||||
vertical: {
|
||||
offset: 'offsetHeight',
|
||||
scroll: 'scrollTop',
|
||||
scrollSize: 'scrollHeight',
|
||||
size: 'height',
|
||||
key: 'vertical',
|
||||
axis: 'Y',
|
||||
client: 'clientY',
|
||||
direction: 'top'
|
||||
},
|
||||
horizontal: {
|
||||
offset: 'offsetWidth',
|
||||
scroll: 'scrollLeft',
|
||||
scrollSize: 'scrollWidth',
|
||||
size: 'width',
|
||||
key: 'horizontal',
|
||||
axis: 'X',
|
||||
client: 'clientX',
|
||||
direction: 'left'
|
||||
}
|
||||
}
|
||||
|
||||
export function renderThumbStyle ({ move, size, bar }) {
|
||||
const style = {}
|
||||
const translate = `translate${bar.axis}(${move}%)`
|
||||
|
||||
style[bar.size] = size
|
||||
style.transform = translate
|
||||
style.msTransform = translate
|
||||
style.webkitTransform = translate
|
||||
|
||||
return style
|
||||
};
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
|
||||
/* istanbul ignore next */
|
||||
const resizeHandler = function (entries) {
|
||||
for (const entry of entries) {
|
||||
const listeners = entry.target.__resizeListeners__ || []
|
||||
if (listeners.length) {
|
||||
listeners.forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const addResizeListener = function (element, fn) {
|
||||
if (isServer) return
|
||||
if (!element.__resizeListeners__) {
|
||||
element.__resizeListeners__ = []
|
||||
element.__ro__ = new ResizeObserver(resizeHandler)
|
||||
element.__ro__.observe(element)
|
||||
}
|
||||
element.__resizeListeners__.push(fn)
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const removeResizeListener = function (element, fn) {
|
||||
if (!element || !element.__resizeListeners__) return
|
||||
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
|
||||
if (!element.__resizeListeners__.length) {
|
||||
element.__ro__.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
function extend (to, _from) {
|
||||
for (const key in _from) {
|
||||
to[key] = _from[key]
|
||||
}
|
||||
return to
|
||||
};
|
||||
|
||||
export function toObject (arr) {
|
||||
const res = {}
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i]) {
|
||||
extend(res, arr[i])
|
||||
}
|
||||
}
|
||||
return res
|
||||
};
|
||||
|
||||
export const on = (function () {
|
||||
if (!isServer && document.addEventListener) {
|
||||
return function (element, event, handler) {
|
||||
if (element && event && handler) {
|
||||
element.addEventListener(event, handler, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return function (element, event, handler) {
|
||||
if (element && event && handler) {
|
||||
element.attachEvent('on' + event, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const off = (function () {
|
||||
if (!isServer && document.removeEventListener) {
|
||||
return function (element, event, handler) {
|
||||
if (element && event) {
|
||||
element.removeEventListener(event, handler, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return function (element, event, handler) {
|
||||
if (element && event) {
|
||||
element.detachEvent('on' + event, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
@@ -0,0 +1,19 @@
|
||||
// 组件
|
||||
import uiJessibuca from '@/components/ui-jessibuca/index.vue'
|
||||
import uiScrollbar from '@/components/ui-scrollbar/index.vue'
|
||||
|
||||
const components = {
|
||||
// 组件
|
||||
uiJessibuca,
|
||||
uiScrollbar
|
||||
}
|
||||
|
||||
const install = function (Vue, opts = {}) {
|
||||
Object.keys(components).forEach((name) => {
|
||||
Vue.component(name, components[name])
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
install
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
install (Vue) {
|
||||
// px 转 rem
|
||||
Vue.prototype.pxTorem = (px, needUnit) => {
|
||||
if (needUnit) {
|
||||
return px / 19.2 + 'rem'
|
||||
} else {
|
||||
return px / 19.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import Vue from 'vue'
|
||||
// import queryString from 'query-string'
|
||||
import app from '@/views/app/index.vue'
|
||||
import { router } from '@/router/index.js'
|
||||
import store from '@/store/index.js'
|
||||
// 请求对象
|
||||
import Axios from 'axios'
|
||||
// 项目本地组件全局注册
|
||||
import globalComponents from './globalComponents'
|
||||
// 项目本地全局方法
|
||||
import globalFunctions from './globalFunctions'
|
||||
|
||||
// 第三方组件
|
||||
// ant-design-vue 组件 按需加载
|
||||
import { ConfigProvider, Input } from 'ant-design-vue'
|
||||
|
||||
Vue.use(globalComponents)
|
||||
Vue.use(globalFunctions)
|
||||
|
||||
Vue.use(ConfigProvider)
|
||||
Vue.use(Input)
|
||||
|
||||
const axiosInstance = Axios.create({
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
// 加载项目基础样式文件
|
||||
require('@/assets/css/normalize.css')
|
||||
// 加载项目组件覆盖样式,全局模块样式
|
||||
require('@/assets/css/index.scss')
|
||||
// 轮播插件swiper样式
|
||||
require('swiper/css/swiper.min.css')
|
||||
|
||||
// 设置为 false 以阻止 vue 在启动时生成生产提示
|
||||
Vue.config.productionTip = false
|
||||
process.env.VUE_APP_env === 'devmock' && require('../mock')
|
||||
|
||||
Vue.prototype.$axios = axiosInstance
|
||||
|
||||
// axios 配置 请求和响应拦截
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 禁用令牌
|
||||
if (
|
||||
typeof config.headers.disabletoken !== 'undefined' &&
|
||||
config.headers.disabletoken === true
|
||||
) {
|
||||
delete config.headers.disabletoken
|
||||
return config
|
||||
}
|
||||
|
||||
if (
|
||||
typeof config.headers.token === 'undefined' &&
|
||||
localStorage.getItem('token') !== null
|
||||
) {
|
||||
config.headers.token = localStorage.getItem('token')
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
// 异常处理
|
||||
axiosInstance.interceptors.response.use(
|
||||
(config) => {
|
||||
if (typeof config.headers.token !== 'undefined') {
|
||||
localStorage.setItem('token', config.headers.token)
|
||||
}
|
||||
if (
|
||||
config.data &&
|
||||
config.data.statusCode &&
|
||||
config.data.statusCode !== '00'
|
||||
) {
|
||||
// 业务异常处理
|
||||
let msg = 'biz error, statusCode: ' + config.data.statusCode
|
||||
if (config.data.statusMsg) {
|
||||
msg = msg + ', statusMsg: ' + config.data.statusMsg
|
||||
}
|
||||
|
||||
// 打印异常信息
|
||||
console.log(msg)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 打印异常信息
|
||||
console.log(error)
|
||||
|
||||
if (
|
||||
typeof error.response !== 'undefined' &&
|
||||
error.response.status === 401
|
||||
) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 获取必要的数据
|
||||
const checkNecessaryData = (callbackFun) => {
|
||||
const promiseList = []
|
||||
if (promiseList.length > 0) {
|
||||
Promise.all(promiseList)
|
||||
.then((res) => {
|
||||
localStorage.removeItem('getInitDataErrorCount')
|
||||
callbackFun && callbackFun()
|
||||
})
|
||||
.catch((res) => {
|
||||
const errorCount = localStorage.getItem(
|
||||
'getInitDataErrorCount'
|
||||
)
|
||||
if (errorCount) {
|
||||
let count = Number.parseInt(errorCount)
|
||||
if (count <= 3) {
|
||||
localStorage.setItem('getInitDataErrorCount', ++count)
|
||||
// 菜单或者用户角色列表数据请求失败,就刷新页面
|
||||
window.location.reload(window.location.href)
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('getInitDataErrorCount', 1)
|
||||
// 菜单或者用户角色列表数据请求失败,就刷新页面
|
||||
window.location.reload(window.location.href)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
callbackFun && callbackFun()
|
||||
}
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
checkNecessaryData(() => {
|
||||
next()
|
||||
})
|
||||
})
|
||||
|
||||
new Vue({
|
||||
render: (h) => h(app),
|
||||
router: router,
|
||||
store: store
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,32 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
const routerPush = Router.prototype.push
|
||||
Router.prototype.push = function push (location) {
|
||||
return routerPush.call(this, location).catch(error => error)
|
||||
}
|
||||
|
||||
const routerList = [
|
||||
{
|
||||
path: '/',
|
||||
component: resolve => require(['@/views/home/index.vue'], resolve),
|
||||
meta: {}
|
||||
}
|
||||
]
|
||||
|
||||
const routerListMap = {}
|
||||
routerList.forEach((item) => {
|
||||
routerListMap[item.path] = item
|
||||
})
|
||||
|
||||
const createRouter = () => {
|
||||
return new Router({
|
||||
routes: routerList
|
||||
})
|
||||
}
|
||||
|
||||
const router = createRouter()
|
||||
|
||||
export { router, routerListMap }
|
||||
@@ -0,0 +1,4 @@
|
||||
const getters = {
|
||||
|
||||
}
|
||||
export default getters
|
||||
@@ -0,0 +1,24 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
|
||||
import common from './modules/common'
|
||||
import getters from './getters'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production'
|
||||
const createPersisted = createPersistedState({
|
||||
storage: window.sessionStorage,
|
||||
paths: ['user']
|
||||
})
|
||||
|
||||
const store = new Vuex.Store({
|
||||
plugins: debug ? [createPersisted] : [createPersisted],
|
||||
modules: {
|
||||
common
|
||||
},
|
||||
getters
|
||||
})
|
||||
|
||||
export default store
|
||||
@@ -0,0 +1,7 @@
|
||||
const common = {
|
||||
state: {},
|
||||
|
||||
mutations: {}
|
||||
}
|
||||
|
||||
export default common
|
||||
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
const user = {
|
||||
state: {},
|
||||
|
||||
mutations: {},
|
||||
|
||||
actions: {}
|
||||
}
|
||||
|
||||
export default user
|
||||
@@ -0,0 +1,91 @@
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
html > body {
|
||||
overflow: hidden;
|
||||
min-width: 1280px;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.xmgl-icon {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #eff2f6;
|
||||
.app-content{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.page-container {
|
||||
width: 100%;
|
||||
padding: p(15);
|
||||
|
||||
.page-title {
|
||||
padding-top: p(45);
|
||||
padding-bottom: p(35);
|
||||
font-size: p(40);
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.page-content {
|
||||
width: p(1200);
|
||||
background-color: #ffffff;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 1px 0 0 rgba(178, 188, 198, 0.51);
|
||||
}
|
||||
.page-list-content {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 0 0 rgba(178, 188, 198, 0.51);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<div
|
||||
class="app-container"
|
||||
>
|
||||
<div class="app-content" ref="app-content">
|
||||
<ui-scrollbar class="app-content-scrollbar" style="height: 100%;" :childOverWidth="false" :wrapStyle="[{ overflowX: 'hidden' }]">
|
||||
<router-view :style="{ 'min-height': contentMinHeight + 'px' }" :contentMinHeight=" contentMinHeight" />
|
||||
</ui-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {},
|
||||
props: { },
|
||||
data () {
|
||||
return {
|
||||
locale: zhCN,
|
||||
contentMinHeight: 0
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
beforeCreate () {
|
||||
const setHtmlFontSize = function () {
|
||||
const deviceWidth =
|
||||
document.documentElement.clientWidth > 1280
|
||||
? document.documentElement.clientWidth <= 1920
|
||||
? document.documentElement.clientWidth
|
||||
: 1920
|
||||
: 1280
|
||||
document.getElementsByTagName('html')[0].style.cssText = 'font-size:' + deviceWidth / 100 + 'px !important'
|
||||
}
|
||||
let timerId
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
function () {
|
||||
if (timerId) {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
timerId = window.setTimeout(function () {
|
||||
this.getContentMinHeight()
|
||||
}, 300)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
setHtmlFontSize()
|
||||
},
|
||||
created () {},
|
||||
mounted () {
|
||||
this.getContentMinHeight()
|
||||
},
|
||||
methods: {
|
||||
getContentMinHeight () {
|
||||
this.contentMinHeight = this.$refs['app-content'].clientHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/index.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,144 @@
|
||||
.banner-comp {
|
||||
width: 100%;
|
||||
height: p(942);
|
||||
.swiper-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.swiper-pagination {
|
||||
left: p(80);
|
||||
width: p(15) !important;
|
||||
top: 48% !important;
|
||||
// overflow: initial;
|
||||
.swiper-pagination-bullet {
|
||||
width: p(15);
|
||||
height: p(15) !important;
|
||||
border-radius: p(3);
|
||||
background: #1664ff !important;
|
||||
opacity: 1;
|
||||
}
|
||||
.swiper-pagination-bullet-active-next {
|
||||
transform: scale(0.33);
|
||||
}
|
||||
.swiper-pagination-bullet-active-prev {
|
||||
transform: scale(0.33);
|
||||
}
|
||||
}
|
||||
.hiddenPagination {
|
||||
.swiper-pagination {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.content {
|
||||
top: p(330);
|
||||
left: p(216);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.yjqd {
|
||||
letter-spacing: 3px;
|
||||
color: #ffffff;
|
||||
font-size: p(68);
|
||||
}
|
||||
.text {
|
||||
font-size: p(17);
|
||||
letter-spacing: 1px;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
.lxwm {
|
||||
color: #ffffff;
|
||||
font-size: p(16);
|
||||
width: p(124);
|
||||
height: p(38);
|
||||
background-color: #165dff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
line-height: p(38);
|
||||
margin-top: p(33);
|
||||
cursor: pointer;
|
||||
}
|
||||
.lxwm:hover {
|
||||
background-color: #316fff;
|
||||
}
|
||||
|
||||
.jzmd-wrap {
|
||||
margin-top: p(63);
|
||||
.jzmd-title {
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
padding-bottom: p(15);
|
||||
margin-bottom: p(21);
|
||||
font-size: p(22);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2em;
|
||||
height: p(2);
|
||||
background-color: #aeaeb7;
|
||||
border-radius: p(2);
|
||||
}
|
||||
}
|
||||
.jzmd-content {
|
||||
width: p(294);
|
||||
height: p(207);
|
||||
margin-left: 0;
|
||||
|
||||
.jzmd-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.jzmd-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: p(118);
|
||||
line-height: 1;
|
||||
margin-bottom: p(26);
|
||||
.name {
|
||||
font-size: p(16);
|
||||
color: #ffffff;
|
||||
}
|
||||
.num-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: baseline;
|
||||
.text {
|
||||
font-size: p(18);
|
||||
color: #ffffff;
|
||||
}
|
||||
.unit {
|
||||
font-size: p(14);
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
width: p(118);
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.hxgn-comp{
|
||||
width: 100%;
|
||||
img{
|
||||
height: p(639);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
.page-home {
|
||||
padding-top: p(62);
|
||||
// background: url('../funasr首页3.png') center no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
background-color: #000000;
|
||||
.page-home-header{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: p(62);
|
||||
align-items: center;
|
||||
border-bottom: solid 1px #202020;
|
||||
background-color: #000000;
|
||||
|
||||
.logo-img{
|
||||
width: p(110);
|
||||
height: p(48);
|
||||
margin-left: p(38);
|
||||
margin-right: p(20);
|
||||
}
|
||||
ul{
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
li{
|
||||
color: #ffffff;
|
||||
font-size: p(16);
|
||||
margin-right: p(20);
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: p(0) p(10);
|
||||
}
|
||||
li:hover{
|
||||
background-color: rgb(22, 22, 22);
|
||||
}
|
||||
|
||||
.line{
|
||||
position: absolute;
|
||||
background-color: #1664ff;
|
||||
width: p(83);
|
||||
height: p(4);
|
||||
bottom: p(-1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
.search-box{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: p(440);
|
||||
height: p(62);
|
||||
background-color: #292929;
|
||||
border: solid 1px #464646;
|
||||
transition: all 0.3s ease-out;
|
||||
img{
|
||||
width: p(16);
|
||||
height: p(16);
|
||||
margin: p(0) p(21);
|
||||
}
|
||||
.ant-input{
|
||||
border: none;
|
||||
flex: 1;
|
||||
height: p(60);
|
||||
background-color: #292929;
|
||||
color: #fff;
|
||||
font-size: p(16);
|
||||
input::-webkit-input-placeholder{
|
||||
color: #cccccc;
|
||||
font-size: p(16);
|
||||
line-height: p(60);
|
||||
}
|
||||
}
|
||||
.ant-input:focus{
|
||||
box-shadow: none !important;
|
||||
}
|
||||
&.search-box:hover{
|
||||
background-color: #444444;
|
||||
.ant-input{
|
||||
background-color: #444444;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.language-box{
|
||||
color: #ffffff;
|
||||
font-size: p(16);
|
||||
width: p(193);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.page-home-footer{
|
||||
background-color: #1b1c20;
|
||||
.gywm{
|
||||
display: flex;
|
||||
padding: p(0) p(350);
|
||||
padding-top: p(38);
|
||||
img{
|
||||
width: p(200);
|
||||
height: p(85);
|
||||
mix-blend-mode: lighten;
|
||||
margin-right: p(50);
|
||||
}
|
||||
ul{
|
||||
// flex: 1;
|
||||
h3{
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
font-size: p(20);
|
||||
margin-bottom: p(12);
|
||||
}
|
||||
li{
|
||||
color: #999999;
|
||||
font-size: p(16);
|
||||
margin-bottom: p(12);
|
||||
cursor: pointer;
|
||||
}
|
||||
li:hover{
|
||||
color: #c9c9c9;
|
||||
}
|
||||
}
|
||||
ul:nth-of-type(1){
|
||||
margin-right: p(75);
|
||||
}
|
||||
ul:nth-of-type(2){
|
||||
margin-right: p(85);
|
||||
}
|
||||
ul:nth-of-type(3){
|
||||
margin-right: p(85);
|
||||
}
|
||||
ul:nth-of-type(4){
|
||||
margin-right: p(95);
|
||||
}
|
||||
}
|
||||
|
||||
.line-box{
|
||||
margin-top: p(50);
|
||||
padding: p(0) p(350);
|
||||
div{
|
||||
height: 2px;
|
||||
background-color: rgba(113, 113, 113, 0.5);
|
||||
}
|
||||
}
|
||||
.ba-box{
|
||||
padding: p(26) p(350) p(46);
|
||||
ul{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
li{
|
||||
color: #fff;
|
||||
color: #999999;
|
||||
font-size: p(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
.lxwjzxfw-comp{
|
||||
width: 100%;
|
||||
height: p(1015);
|
||||
background: url('../images/lxwjzxfw-bg.png') center no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.lxwjzxfw-plan {
|
||||
padding: p(0) p(350);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
h3 {
|
||||
color: #fff;
|
||||
font-size: p(22);
|
||||
position: relative;
|
||||
line-height: p(46);
|
||||
div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: p(45);
|
||||
height: 2px;
|
||||
background-color: #aeaeb7;
|
||||
}
|
||||
}
|
||||
|
||||
.zxfl-lc{
|
||||
margin-top: p(45);
|
||||
.box{
|
||||
flex: 0.21;
|
||||
.icon-plan{
|
||||
position: relative;
|
||||
}
|
||||
.lc-plan{
|
||||
width: p(180);
|
||||
height: p(82);
|
||||
background-image: linear-gradient(140deg,
|
||||
#e0e4f0 0%,
|
||||
#b3bad1 100%);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
p:nth-of-type(1) {
|
||||
font-size: p(20);
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
}
|
||||
p:nth-of-type(2) {
|
||||
font-size: p(16);
|
||||
color: #515c73;
|
||||
}
|
||||
}
|
||||
}
|
||||
.box-5{
|
||||
flex: 0.16;
|
||||
}
|
||||
|
||||
.blue-box{
|
||||
.lc-plan{
|
||||
background-image: linear-gradient(0deg,
|
||||
#3ebbff 0%,
|
||||
#3d7df8 100%);
|
||||
p{
|
||||
color: #fff !important;
|
||||
}
|
||||
p:nth-of-type(1) {
|
||||
font-size: p(20);
|
||||
}
|
||||
p:nth-of-type(2) {
|
||||
font-size: p(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lc-top{
|
||||
display: flex;
|
||||
.box-5{
|
||||
.icon-plan{
|
||||
.text-img{
|
||||
position: absolute;
|
||||
width: p(55);
|
||||
height: p(61);
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.box-3{
|
||||
.lc-plan{
|
||||
margin-top: p(39);
|
||||
}
|
||||
.icon-plan{
|
||||
img{
|
||||
position: absolute;
|
||||
width: p(195);
|
||||
height: p(138);
|
||||
top: p(0);
|
||||
left: p(80);
|
||||
}
|
||||
}
|
||||
}
|
||||
.box-4{
|
||||
.lc-plan{
|
||||
width: p(154);
|
||||
height: p(70);
|
||||
margin-top: p(152);
|
||||
margin-left: p(15);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-plan{
|
||||
width: p(180);
|
||||
margin: p(10) 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
img{
|
||||
height: p(176);
|
||||
width: p(18);
|
||||
}
|
||||
&.xxld{
|
||||
width: p(180);
|
||||
background: url('../images//lxwj-xx.png') top no-repeat;
|
||||
background-size: 100% p(127);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lc-bottom{
|
||||
display: flex;
|
||||
.box{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-plan{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
img{
|
||||
margin: auto;
|
||||
width: p(57);
|
||||
height: p(18);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lxwj-text{
|
||||
color: #ffffff;
|
||||
font-size: p(18);
|
||||
margin-top: p(40);
|
||||
}
|
||||
}
|
||||
|
||||
.xq-box{
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
.video-box{
|
||||
width: p(517);
|
||||
height: p(290);
|
||||
// background-color: #165dff;
|
||||
margin-right: p(188);
|
||||
iframe{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-box{
|
||||
margin-top: p(80);
|
||||
li{
|
||||
width: p(190);
|
||||
height: p(55);
|
||||
background-color: #165dff;
|
||||
border-radius: p(4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: p(40);
|
||||
cursor: pointer;
|
||||
img{
|
||||
width: p(26);
|
||||
}
|
||||
p{
|
||||
color: #ffffff;
|
||||
font-size: p(24);
|
||||
}
|
||||
|
||||
}
|
||||
li:last-child {
|
||||
margin-bottom: p(15);
|
||||
}
|
||||
li:hover{
|
||||
background-color: #3f77fc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.mxjs-comp {
|
||||
background: url('../images/mxjs-bg.png') center no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.mxjs-plan {
|
||||
padding: p(0) p(350);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
h3 {
|
||||
color: #24242f;
|
||||
font-size: p(22);
|
||||
position: relative;
|
||||
line-height: p(46);
|
||||
div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: p(45);
|
||||
height: 2px;
|
||||
background-color: #aeaeb7;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: p(888);
|
||||
height: p(613);
|
||||
margin: auto;
|
||||
}
|
||||
p {
|
||||
color: #333333;
|
||||
font-size: p(18);
|
||||
margin-top: p(10);
|
||||
padding-bottom: p(83);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
.sstx-comp{
|
||||
width: 100%;
|
||||
height: p(1100);
|
||||
background: url('../images/sstx-bg.png') center no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
.lxwjzxfw-plan{
|
||||
padding: p(0) p(350);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sstx-plan{
|
||||
// padding: p(0) p(350);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
h3 {
|
||||
color: #fff;
|
||||
font-size: p(22);
|
||||
position: relative;
|
||||
line-height: p(46);
|
||||
div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: p(45);
|
||||
height: 2px;
|
||||
background-color: #aeaeb7;
|
||||
}
|
||||
}
|
||||
|
||||
.sstx-lc{
|
||||
padding-top: p(36);
|
||||
.box{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
.lc-plan{
|
||||
width: p(250);
|
||||
height: p(84);
|
||||
background-image: linear-gradient(140deg,
|
||||
#e0e4f0 0%,
|
||||
#b3bad1 100%);
|
||||
border-radius: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
p:nth-of-type(1) {
|
||||
font-size: p(20);
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
}
|
||||
p:nth-of-type(2) {
|
||||
font-size: p(16);
|
||||
color: #515c73;
|
||||
}
|
||||
}
|
||||
.icon-plan{
|
||||
|
||||
}
|
||||
}
|
||||
.box-min{
|
||||
.lc-plan{
|
||||
width: p(180);
|
||||
height: p(70);
|
||||
}
|
||||
}
|
||||
.blue-box{
|
||||
.lc-plan{
|
||||
background-image: linear-gradient(140deg,
|
||||
rgba(22, 92, 255, 0.3) 0%,
|
||||
rgba(22, 92, 255, 0.3) 100%);
|
||||
border-radius: 6px;
|
||||
border: dashed 2px rgba(22, 92, 255, 0.8);
|
||||
|
||||
p{
|
||||
color: #fff !important;
|
||||
}
|
||||
p:nth-of-type(1) {
|
||||
font-size: p(20);
|
||||
}
|
||||
p:nth-of-type(2) {
|
||||
font-size: p(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lc-top{
|
||||
display: flex;
|
||||
.lc-plan{
|
||||
|
||||
}
|
||||
.icon-plan{
|
||||
|
||||
}
|
||||
.box-1{
|
||||
.icon-plan{
|
||||
position: absolute;
|
||||
bottom: p(-48);
|
||||
img{
|
||||
width: p(18);
|
||||
}
|
||||
p{
|
||||
position: absolute;
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
top: 10%;
|
||||
right: 50%;
|
||||
transform: translate(60%, 0);
|
||||
width: max-content;
|
||||
letter-spacing: p(15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lc-center{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: p(115);
|
||||
background-color: rgba(179, 186, 209, 0.25);
|
||||
border-radius: 4px;
|
||||
border: solid 1px rgba(179, 186, 209, 0.8);
|
||||
margin-top: p(30);
|
||||
margin-bottom: p(20);
|
||||
.box-1,.box-2{
|
||||
|
||||
.icon-plan,.icon-plan1{
|
||||
position: absolute;
|
||||
// bottom: p(-48);
|
||||
right: p(-60);
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
img{
|
||||
width: p(140);
|
||||
height: p(18);
|
||||
}
|
||||
div{
|
||||
position: absolute;
|
||||
top: p(-12);
|
||||
width: p(140);
|
||||
p{
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.box-2{
|
||||
.icon-plan1{
|
||||
right: p(-200);
|
||||
img{
|
||||
width: p(286);
|
||||
}
|
||||
div{
|
||||
width: p(286);
|
||||
}
|
||||
}
|
||||
.icon-plan2{
|
||||
position: absolute;
|
||||
bottom: p(-80);
|
||||
img{
|
||||
width: p(18);
|
||||
height: p(90);
|
||||
}
|
||||
p{
|
||||
position: absolute;
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
top: 60%;
|
||||
right: 50%;
|
||||
transform: translate(50%, 0);
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lc-bottom{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: p(20);
|
||||
height: p(160);
|
||||
background-color: rgba(179, 186, 209, 0.25);
|
||||
border-radius: 4px;
|
||||
border: solid 1px rgba(179, 186, 209, 0.8);
|
||||
.box-1{
|
||||
.icon-plan1{
|
||||
position: absolute;
|
||||
top: p(-90);
|
||||
img{
|
||||
width: p(18);
|
||||
height: p(90);
|
||||
}
|
||||
p{
|
||||
position: absolute;
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
top: 60%;
|
||||
right: 50%;
|
||||
transform: translate(50%, 0);
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
.icon-plan2{
|
||||
position: absolute;
|
||||
right: p(-65);
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
img{
|
||||
width: p(140);
|
||||
height: p(18);
|
||||
}
|
||||
div{
|
||||
position: absolute;
|
||||
top: p(-12);
|
||||
width: p(140);
|
||||
p{
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-2{
|
||||
.icon-plan{
|
||||
position: absolute;
|
||||
right: p(-30);
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
img{
|
||||
width: p(140);
|
||||
height: p(18);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-3{
|
||||
.icon-plan{
|
||||
position: absolute;
|
||||
top: p(-210);
|
||||
img{
|
||||
width: p(18);
|
||||
height: p(210);
|
||||
}
|
||||
p{
|
||||
position: absolute;
|
||||
font-size: p(18);
|
||||
color: #fff;
|
||||
bottom: 2%;
|
||||
right: 50%;
|
||||
transform: translate(65%, 0);
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lxwj-text{
|
||||
color: #ffffff;
|
||||
font-size: p(18);
|
||||
margin-top: p(45);
|
||||
}
|
||||
}
|
||||
|
||||
.xq-box{
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
.video-box{
|
||||
width: p(517);
|
||||
height: p(290);
|
||||
// background-color: #165dff;
|
||||
margin-right: p(188);
|
||||
iframe{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-box{
|
||||
margin-top: p(80);
|
||||
li{
|
||||
width: p(190);
|
||||
height: p(55);
|
||||
background-color: #165dff;
|
||||
border-radius: p(4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: p(40);
|
||||
cursor: pointer;
|
||||
img{
|
||||
width: p(26);
|
||||
}
|
||||
p{
|
||||
color: #ffffff;
|
||||
font-size: p(24);
|
||||
}
|
||||
|
||||
}
|
||||
li:last-child {
|
||||
margin-bottom: p(15);
|
||||
}
|
||||
li:hover{
|
||||
background-color: #3f77fc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 524 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 368 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 631 B |
|
After Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 420 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 363 B |
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="banner-comp">
|
||||
<div
|
||||
class="swiper-container"
|
||||
ref="swiper-container"
|
||||
:class="{ hiddenPagination: bannerList && bannerList.length < 2 }"
|
||||
>
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(item, index) in bannerList" :key="index">
|
||||
<div class="item">
|
||||
<img class="banner-bg" :src="item.url" alt />
|
||||
<div v-if="item.flag === 1" class="content">
|
||||
<h3 class="yjqd">一键启动FUNASR</h3>
|
||||
<p class="text">
|
||||
FUNASR希望在语音识别的学术研究和工业应用之间架起一座桥梁。通过发布工业级
|
||||
<br />语音识别模型的训练和微调,研究人员和开发人员可以更方便地进行语音识别模型的
|
||||
<br />研究和生产,并推动语音识别生态的发展。让语音识别更有趣!
|
||||
</p>
|
||||
|
||||
<div class="lxwm">联系我们</div>
|
||||
<div class="jzmd-wrap">
|
||||
<div class="jzmd-title">捐赠名单</div>
|
||||
<div class="jzmd-content swiper-container" ref="jzmd-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div
|
||||
class="jzmd-row swiper-slide"
|
||||
v-for="(item,index) in jzmdRows"
|
||||
:key="index"
|
||||
>
|
||||
<div class="jzmd-item" v-if="jzmdList[2 * (item -1)]">
|
||||
<div class="name">{{ jzmdList[2 * (item -1)].name }}</div>
|
||||
<div class="num-text">
|
||||
<span class="text">{{ jzmdList[2 * (item -1)].num }}</span>
|
||||
<span class="unit">元</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jzmd-item" v-if="jzmdList[2 * (item -1) + 1]">
|
||||
<div class="name">{{ jzmdList[2 * (item -1) + 1].name }}</div>
|
||||
<div class="num-text">
|
||||
<span class="text">{{ jzmdList[2 * (item -1) + 1].num }}</span>
|
||||
<span class="unit">元</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Swiper from 'swiper'
|
||||
export default {
|
||||
name: 'banner-comp',
|
||||
data () {
|
||||
return {
|
||||
bannerList: [
|
||||
{
|
||||
flag: 1,
|
||||
url: require('./assets/images/banner.png')
|
||||
}
|
||||
],
|
||||
swiperObj: null,
|
||||
jzmdSwiperObj: null,
|
||||
jzmdList: [
|
||||
{
|
||||
name: '刘连响',
|
||||
num: 300
|
||||
},
|
||||
{
|
||||
name: '程俊陶',
|
||||
num: 300
|
||||
},
|
||||
{
|
||||
name: '黄明',
|
||||
num: 236
|
||||
},
|
||||
{
|
||||
name: '高志付',
|
||||
num: 235
|
||||
},
|
||||
{
|
||||
name: '陈翔',
|
||||
num: 200
|
||||
},
|
||||
{
|
||||
name: '夏侯杰',
|
||||
num: 200
|
||||
},
|
||||
{
|
||||
name: '黄药师',
|
||||
num: 198
|
||||
},
|
||||
{
|
||||
name: '肖炜',
|
||||
num: 100
|
||||
},
|
||||
{
|
||||
name: '刘柱',
|
||||
num: 100
|
||||
},
|
||||
{
|
||||
name: '黄水杯',
|
||||
num: 85
|
||||
},
|
||||
{
|
||||
name: '子龙',
|
||||
num: 85
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jzmdRows () {
|
||||
return Math.ceil(this.jzmdList.length / 2)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.initSwiper()
|
||||
})
|
||||
this.initJzmdSwiper()
|
||||
},
|
||||
methods: {
|
||||
initSwiper () {
|
||||
if (this.swiperObj) {
|
||||
this.swiperObj.destroy()
|
||||
}
|
||||
// const that = this
|
||||
this.swiperObj = new Swiper(this.$refs['swiper-container'], {
|
||||
slidesPerView: 1,
|
||||
direction: 'vertical',
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
dynamicBullets: true
|
||||
},
|
||||
on: {
|
||||
slideChangeTransitionEnd: function () {},
|
||||
init: function () {}
|
||||
}
|
||||
})
|
||||
},
|
||||
initJzmdSwiper () {
|
||||
if (this.jzmdSwiperObj) {
|
||||
this.jzmdSwiperObj.destroy()
|
||||
}
|
||||
// const that = this
|
||||
this.jzmdSwiperObj = new Swiper(this.$refs['jzmd-swiper'], {
|
||||
direction: 'vertical',
|
||||
autoplay: {
|
||||
delay: 2000,
|
||||
stopOnLastSlide: false,
|
||||
disableOnInteraction: false
|
||||
},
|
||||
slidesPerView: 5,
|
||||
slidesPerGroup: 1,
|
||||
loop: true,
|
||||
loopedSlides: 7
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/banner.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="hxgn-comp">
|
||||
<img src="./assets/images/hxgn.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'hxgn-comp',
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/hxgn.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="page page-home">
|
||||
<header class="page-home-header">
|
||||
<img class="logo-img" src="./assets/images/logo.png" alt="">
|
||||
<ul>
|
||||
<li :ref="item.ref" v-for="(item, index) in headerList" :key="index" @click="anchorTo(item)">
|
||||
<p>{{item.name}}</p>
|
||||
</li>
|
||||
<li @click="toPage({link: 'https://github.com/alibaba-damo-academy/FunASR'})">
|
||||
Github
|
||||
</li>
|
||||
<li>
|
||||
社区交流
|
||||
</li>
|
||||
<div :style="{width: lineWidth + 'px', left: lineLeft + 'px'}" class="line"></div>
|
||||
</ul>
|
||||
<div class="search-box">
|
||||
<img src="./assets/images/ss.png" alt="">
|
||||
<a-input placeholder="请输入关键字" />
|
||||
</div>
|
||||
<div class="language-box">
|
||||
中文/EN
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<banner-comp ref="funasrJs" />
|
||||
|
||||
<hxgn-comp ref="hxgn" />
|
||||
|
||||
<mxjs-comp ref="mxjs" />
|
||||
|
||||
<lxwjzxfw-comp ref="lxwj" />
|
||||
|
||||
<sstx-comp ref="sstx" />
|
||||
|
||||
<footer class="page-home-footer">
|
||||
<div class="gywm">
|
||||
<img src="./assets/images/footer-logo.png" alt="">
|
||||
<ul v-for="(item, index) in footerList" :key="index">
|
||||
<h3>{{item.title}}</h3>
|
||||
<li @click="toPage(cItem)" v-for="(cItem, cIndex) in item.childer" :key="cIndex">
|
||||
{{cItem.name}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bannerComp from './banner.vue'
|
||||
import hxgnComp from './hxgn.vue'
|
||||
import mxjsComp from './mxjs.vue'
|
||||
import lxwjzxfwComp from './lxwjzxfw.vue'
|
||||
import sstxComp from './sstx.vue'
|
||||
export default {
|
||||
name: 'page-home',
|
||||
components: {
|
||||
bannerComp,
|
||||
hxgnComp,
|
||||
mxjsComp,
|
||||
lxwjzxfwComp,
|
||||
sstxComp
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
headerList: [
|
||||
{
|
||||
name: 'funasr介绍',
|
||||
ref: 'h-funasrJs',
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
name: '核心功能',
|
||||
ref: 'h-hxgn',
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
name: 'Paraformer模型介绍',
|
||||
ref: 'h-mxjs',
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
name: '离线文件转写服务',
|
||||
ref: 'h-lxwj',
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
name: '实时听写',
|
||||
ref: 'h-sstx',
|
||||
isAnchor: true
|
||||
}
|
||||
// {
|
||||
// name: 'Github',
|
||||
// ref: 'h-github'
|
||||
// },
|
||||
// {
|
||||
// name: '社区交流',
|
||||
// ref: 'h-sqjl'
|
||||
// }
|
||||
],
|
||||
footerList: [
|
||||
{
|
||||
title: 'Funasr介绍',
|
||||
childer: [
|
||||
{
|
||||
name: '关于我们',
|
||||
link: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '核心功能',
|
||||
childer: [
|
||||
{
|
||||
name: '核心介绍',
|
||||
link: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Paraformer模型介绍',
|
||||
childer: [
|
||||
{
|
||||
name: '模型结构固',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '模型介绍',
|
||||
link: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '离线文件转写服务',
|
||||
childer: [
|
||||
{
|
||||
name: '原理图',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '文字介绍',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '安装',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '使用',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '视频教程链接',
|
||||
link: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '实时听写',
|
||||
childer: [
|
||||
{
|
||||
name: '原理图',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '文字介绍',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '安装',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '使用',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
name: '视频教程链接',
|
||||
link: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
headerActive: 'h-funasrJs',
|
||||
lineWidth: 0,
|
||||
lineLeft: 0
|
||||
}
|
||||
},
|
||||
created () {},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.getLineWidth()
|
||||
this.handlerNodeScroll()
|
||||
this.getAnchorDetails()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
// 底部跳转
|
||||
toPage (item) {
|
||||
if (item.link) window.open(item.link)
|
||||
},
|
||||
// 获取元素的高度,距离顶部距离
|
||||
getAnchorDetails () {
|
||||
for (let i = 0; i < this.headerList.length; i++) {
|
||||
const item = this.headerList[i]
|
||||
if (item.isAnchor) {
|
||||
const refName = item.ref.split('-')[1]
|
||||
const dom = this.$refs[refName].$el
|
||||
item.height = dom.offsetHeight
|
||||
item.top = dom.offsetTop
|
||||
}
|
||||
}
|
||||
},
|
||||
// 头部锚点按钮
|
||||
anchorTo (item) {
|
||||
this.headerActive = item.ref
|
||||
this.getLineWidth()
|
||||
if (item.isAnchor) {
|
||||
const refName = item.ref.split('-')[1]
|
||||
this.scrollToFn(refName)
|
||||
}
|
||||
},
|
||||
// 设置滚动距离
|
||||
scrollToFn (refName) {
|
||||
const dom = this.$refs[refName].$el
|
||||
const top = dom.offsetTop
|
||||
|
||||
const appContentScrollbar = document.getElementsByClassName('app-content-scrollbar')[0]
|
||||
const uiModalWrap = appContentScrollbar.getElementsByClassName('ui-scrollbar__wrap')[0]
|
||||
|
||||
uiModalWrap.scrollTo({
|
||||
top: top - 70,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
// 获取头部选中样式
|
||||
getLineWidth () {
|
||||
const dom = this.$refs[this.headerActive][0]
|
||||
if (!dom) return
|
||||
this.lineWidth = dom.offsetWidth
|
||||
this.lineLeft = dom.offsetLeft
|
||||
},
|
||||
// 监听元素滚动
|
||||
handlerNodeScroll () {
|
||||
const self = this
|
||||
// 函数防抖
|
||||
const fun = self.debounce(e => {
|
||||
// 距顶部
|
||||
const scrollTop = e.target.scrollTop
|
||||
let toActive = null
|
||||
for (let i = 0; i < self.headerList.length; i++) {
|
||||
const item = self.headerList[i]
|
||||
if (scrollTop > (item.top - 90)) {
|
||||
toActive = item
|
||||
}
|
||||
}
|
||||
if (toActive && toActive.top) {
|
||||
self.headerActive = toActive.ref
|
||||
self.getLineWidth()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// 监听滚动
|
||||
const appContentScrollbar = document.getElementsByClassName('app-content-scrollbar')[0]
|
||||
const uiModalWrap = appContentScrollbar.getElementsByClassName('ui-scrollbar__wrap')[0]
|
||||
uiModalWrap.addEventListener('scroll', function (e) {
|
||||
fun(e)
|
||||
})
|
||||
},
|
||||
// 函数防抖
|
||||
debounce (handle, delay) {
|
||||
let timer = null
|
||||
return function () {
|
||||
const _self = this
|
||||
const _args = arguments
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(function () {
|
||||
handle.apply(_self, _args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/index.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="lxwjzxfw-comp">
|
||||
<div class="lxwjzxfw-plan">
|
||||
<h3>
|
||||
离线文件转写服务
|
||||
<div></div>
|
||||
</h3>
|
||||
|
||||
<div class="zxfl-lc">
|
||||
<div class="lc-top">
|
||||
<div class="box box-1">
|
||||
<div class="lc-plan">
|
||||
<p>消息队列</p>
|
||||
<p>(从客户端接收)</p>
|
||||
</div>
|
||||
<div class="icon-plan xxld">
|
||||
<img src="./assets/images/lxwj-to-bottom.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-2">
|
||||
|
||||
</div>
|
||||
<div class="box box-3 blue-box">
|
||||
<div class="lc-plan">
|
||||
<p>语言模型</p>
|
||||
<p>Ngram/Token/Lex</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-yymx.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-4 blue-box">
|
||||
<div class="lc-plan">
|
||||
<p>消息队列</p>
|
||||
<p>(从客户端接收)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-5">
|
||||
<div class="lc-plan">
|
||||
<p>消息队列</p>
|
||||
<p>(从客户端接收)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-to-top.png" alt="">
|
||||
<img class="text-img" src="./assets/images/lxwj-text.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lc-bottom">
|
||||
<div class="box box-1">
|
||||
<div class="lc-plan">
|
||||
<p>语音端点检测</p>
|
||||
<p>FSMN-VAD</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-to-right.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-2">
|
||||
<div class="lc-plan">
|
||||
<p>声学模型</p>
|
||||
<p>Paraformer</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-to-right.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-3 blue-box">
|
||||
<div class="lc-plan">
|
||||
<p>解码器</p>
|
||||
<p>Wfst decoder</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-to-right.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-4">
|
||||
<div class="lc-plan">
|
||||
<p>标点预测</p>
|
||||
<p>CT-Transformer</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/lxwj-to-right.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-5">
|
||||
<div class="lc-plan">
|
||||
<p>逆文本正则化</p>
|
||||
<p>ITN</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="lxwj-text">
|
||||
FunASR离线文件转写软件包,提供了一款功能强大的语音离线文件转写服务。拥有完整的语音识别链路,结合了语音端点检测、语音识别、标点等模型,可以将几十个小时的长音频与视频识别成带标点的文字,而且支持上百路请求同时进行转写。输出为带标点的文字,含有字级别时间戳,支持ITN与用户自定义热词等。服务端集成有ffmpeg,支持各种音视频格式输入。软件包提供有html、python、c++、java与c#等多种编程语言客户端,用户可以直接使用与进一步开发。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="xq-box">
|
||||
<div class="video-box">
|
||||
<iframe src="//player.bilibili.com/player.html?aid=663707374&bvid=BV11a4y1U72y&cid=1338069747&p=1&autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="false"> </iframe>
|
||||
<!-- <LivePlayer
|
||||
:autoplay="false"
|
||||
:videoUrl="videoSrc"
|
||||
></LivePlayer> -->
|
||||
</div>
|
||||
|
||||
<div class="btn-box">
|
||||
<li v-for="(item, index) in btnList" :key="index" @click="toPage(item)">
|
||||
<img :src="item.icon" alt="">
|
||||
<p>{{item.title}}</p>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import LivePlayer from '@liveqing/liveplayer'
|
||||
export default {
|
||||
name: 'lxwjzxfw-comp',
|
||||
// components: {
|
||||
// LivePlayer
|
||||
// },
|
||||
data () {
|
||||
return {
|
||||
btnList: [
|
||||
{
|
||||
icon: require('./assets/images/lxwj-zxty.png'),
|
||||
title: '在线体验',
|
||||
link: 'https://www.funasr.com/static/offline/index.html'
|
||||
},
|
||||
{
|
||||
icon: require('./assets/images/lxwj-az.png'),
|
||||
title: '安 装',
|
||||
link: 'https://github.com/alibaba-damo-academy/FunASR/blob/main/runtime/docs/SDK_advanced_guide_offline_zh.md'
|
||||
},
|
||||
{
|
||||
icon: require('./assets/images/lxwj-sy.png'),
|
||||
title: '使 用',
|
||||
link: 'https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gz'
|
||||
}
|
||||
],
|
||||
videoSrc: 'https://www.bilibili.com/video/BV11a4y1U72y/?share_source=copy_web&vd_source=f6576748261a1b738a71ad618d380438'
|
||||
// videoSrc: 'https://www.bilibili.com/video/BV11a4y1U72y/?share_source=copy_web&vd_source=f6576748261a1b738a71ad618d380438'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toPage (item) {
|
||||
if (item.link) window.open(item.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/lxwjzxfw.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="mxjs-comp">
|
||||
<div class="mxjs-plan">
|
||||
<h3>
|
||||
Paraformer模型介绍
|
||||
<div></div>
|
||||
</h3>
|
||||
<img src="./assets/images/mxjs.png" alt="">
|
||||
<p>
|
||||
Paraformer是一种非自回归端到端语音识别模型。非自回归模型相比于目前主流的自回归模型,可以并行的对整条句子输出目标文字,特别适合利用GPU进行并行推理。Paraformer是目前已知的首个在工业大数据上可以获得和自回归端到端模型相同性能的非自回归模型。配合GPU推理,可以将推理效率提升10倍,从而将语音识别云服务的机器成本降低接近10倍。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'mxjs-comp',
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/mxjs.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="sstx-comp">
|
||||
<div class="lxwjzxfw-plan">
|
||||
<div class="sstx-plan">
|
||||
<h3>
|
||||
实时听写
|
||||
<div></div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="sstx-lc">
|
||||
<div class="lc-top">
|
||||
<div class="box box-1 box-min">
|
||||
<div class="lc-plan">
|
||||
<p>消息队列</p>
|
||||
<p>(从客户端接收)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/sstx-yp.png" alt="">
|
||||
<p>音频</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-2">
|
||||
|
||||
</div>
|
||||
<div class="box box-3 box-min">
|
||||
<div class="lc-plan">
|
||||
<p>消息队列</p>
|
||||
<p>(发送到客户端)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lc-center">
|
||||
<div class="box box-1">
|
||||
<div class="lc-plan">
|
||||
<p>FSMN-VAD-realtime</p>
|
||||
<p>(端点检测实时)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/sstx-fjyd.png" alt="">
|
||||
<div>
|
||||
<p>非静音段</p>
|
||||
<p>间隔600ms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-2">
|
||||
<div class="lc-plan">
|
||||
<p>Paraformmer-online</p>
|
||||
<p>(语音识别实时)</p>
|
||||
</div>
|
||||
<div class="icon-plan1">
|
||||
<img src="./assets/images/sstx-sbwz-max.png" alt="">
|
||||
<div>
|
||||
<p>识别文字</p>
|
||||
<p>间隔600ms</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-plan2">
|
||||
<img src="./assets/images/sstx-vad.png" alt="">
|
||||
<p>VAD 尾点</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-3">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="lc-bottom">
|
||||
<div class="box box-1 blue-box">
|
||||
<div class="lc-plan">
|
||||
<p>FSMN-VAD-realtime</p>
|
||||
<p>(语音识别非实时)</p>
|
||||
</div>
|
||||
<div class="icon-plan1">
|
||||
<img src="./assets/images/sstx-vad.png" alt="">
|
||||
<p>VAD 尾点</p>
|
||||
</div>
|
||||
<div class="icon-plan2">
|
||||
<img src="./assets/images/sstx-sbwz.png" alt="">
|
||||
<div>
|
||||
<p>识别文字</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-2">
|
||||
<div class="lc-plan">
|
||||
<p>CT-Transformer</p>
|
||||
<p>(标点预测)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/sstx-fjyd.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-3">
|
||||
<div class="lc-plan">
|
||||
<p>ITN</p>
|
||||
<p>(逆文本正则化)</p>
|
||||
</div>
|
||||
<div class="icon-plan">
|
||||
<img src="./assets/images/sstx-xz.png" alt="">
|
||||
<div>
|
||||
<p>修正 识别文字</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="lxwj-text">
|
||||
FunASR实时语音听写软件包,集成了实时版本的语音端点检测模型、语音识别、语音识别、标点预测模型等。采用多模型协同,既可以实时的进行语音转文字,也可以在说话句尾用高精度转写文字修正输出,输出文字带有标点,支持多路请求。依据使用者场景不同,支持实时语音听写服务(online)、非实时一句话转写(offline)与实时与非实时一体化协同(2pass)3种服务模式。软件包提供有html、python、c++、java与c#等多种编程语言客户端,用户可以直接使用与进一步开发。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="xq-box">
|
||||
<div class="video-box">
|
||||
<iframe src="//player.bilibili.com/player.html?aid=278639821&bvid=BV1Yw411K7LY&cid=1338073559&p=1&autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" > </iframe>
|
||||
<!-- <LivePlayer
|
||||
:autoplay="false"
|
||||
:videoUrl="videoSrc"
|
||||
></LivePlayer> -->
|
||||
</div>
|
||||
|
||||
<div class="btn-box">
|
||||
<li v-for="(item, index) in btnList" :key="index" @click="toPage(item)">
|
||||
<img :src="item.icon" alt="">
|
||||
<p>{{item.title}}</p>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import LivePlayer from '@liveqing/liveplayer'
|
||||
export default {
|
||||
name: 'sstx-comp',
|
||||
// components: {
|
||||
// LivePlayer
|
||||
// },
|
||||
data () {
|
||||
return {
|
||||
btnList: [
|
||||
{
|
||||
icon: require('./assets/images/lxwj-zxty.png'),
|
||||
title: '在线体验',
|
||||
link: 'https://www.funasr.com/static/online/index.html'
|
||||
},
|
||||
{
|
||||
icon: require('./assets/images/lxwj-az.png'),
|
||||
title: '安 装',
|
||||
link: 'https://github.com/alibaba-damo-academy/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md'
|
||||
},
|
||||
{
|
||||
icon: require('./assets/images/lxwj-sy.png'),
|
||||
title: '使 用',
|
||||
link: 'https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gz'
|
||||
}
|
||||
],
|
||||
videoSrc: 'https://www.bilibili.com/video/BV1Yw411K7LY/?share_source=copy_web&vd_source=f6576748261a1b738a71ad618d380438'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toPage (item) {
|
||||
if (item.link) window.open(item.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assets/css/sstx.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,130 @@
|
||||
const webpack = require('webpack')
|
||||
// gzip压缩插件
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
const projectTitle = 'funasr'
|
||||
|
||||
// node命令行参数--env xxx=xxx保存至node的环境变量process.env.xxx=xxx
|
||||
for (const arg of process.argv) {
|
||||
if (arg.indexOf('=') > 0) {
|
||||
process.env[arg.split('=')[0]] = arg.split('=')[1]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pages: {
|
||||
index: {
|
||||
// 本地化的入口,格式/src/main-<配置>.js,当配置对应的入口文件不存在时,使用默认的入口文件./src/main.js
|
||||
entry: process.argv.indexOf('--mode') !== -1 && require('fs').existsSync('./src/main-' + process.argv[process.argv.indexOf('--mode') + 1] + '.js') ? './src/main-' + process.argv[process.argv.indexOf('--mode') + 1] + '.js' : './src/main.js',
|
||||
// 模板来源
|
||||
template: 'public/index.html',
|
||||
// 在 dist/index.html 的输出
|
||||
filename: 'index.html',
|
||||
// 当使用 title 选项时
|
||||
// template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
|
||||
title: projectTitle
|
||||
}
|
||||
},
|
||||
// 根据环境配置项目名
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/funasr-vue',
|
||||
// 保存时是否eslint检查
|
||||
lintOnSave: true,
|
||||
productionSourceMap: false,
|
||||
devServer: {
|
||||
// host: '127.0.0.1',
|
||||
port: 9018,
|
||||
// https: false, // https:{type:Boolean}
|
||||
// open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.1.57:20611',
|
||||
// ws: true,
|
||||
changeOrigin: true,
|
||||
// logLevel: 'debug',
|
||||
pathRewrite: {
|
||||
'/api': 'http://192.168.1.57:20611/sso-backend'
|
||||
}
|
||||
}
|
||||
},
|
||||
client: {
|
||||
overlay: false
|
||||
}
|
||||
},
|
||||
css: {
|
||||
loaderOptions: {
|
||||
// 给 sass-loader 传递选项
|
||||
sass: {
|
||||
// @/ 是 src/ 的别名
|
||||
// 所以这里假设你有 `src/variables.scss` 这个文件
|
||||
additionalData: '@import "~@/assets/css/common.scss";'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 配置babel-loader 转译 node_modules 里面的文件
|
||||
transpileDependencies: [
|
||||
/[/\\]node_modules[/\\](.+?)?plugin-/ // 匹配编译plugin-开头的组件
|
||||
],
|
||||
configureWebpack: (config) => {
|
||||
const plugins = []
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// 开发环境配置
|
||||
config.devtool = 'eval-source-map'
|
||||
} else {
|
||||
// 生产环境配置
|
||||
// 添加gzip压缩
|
||||
plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
filename: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
config.optimization.minimizer[0].options.minimizer.options.compress.drop_console = true
|
||||
config.optimization.minimizer[0].options.minimizer.options.compress.drop_debugger = true
|
||||
}
|
||||
// 配置打包文件git信息
|
||||
plugins.push(
|
||||
new webpack.BannerPlugin({
|
||||
banner: (() => {
|
||||
try {
|
||||
const execSync = require('child_process').execSync
|
||||
return JSON.stringify({
|
||||
'remote-origin-url':
|
||||
'' + execSync('git remote get-url origin'),
|
||||
branch: '' + execSync('git rev-parse --abbrev-ref HEAD'),
|
||||
commithash: '' + execSync('git rev-parse HEAD')
|
||||
})
|
||||
} catch (e) {
|
||||
return 'not a git repository'
|
||||
}
|
||||
})()
|
||||
})
|
||||
)
|
||||
|
||||
config.plugins = [...config.plugins, ...plugins]
|
||||
|
||||
const sassLoader = require.resolve('sass-loader')
|
||||
config.module.rules.filter(rule => {
|
||||
return rule.test.toString().indexOf('scss') !== -1
|
||||
}).forEach(rule => {
|
||||
rule.oneOf.forEach(oneOfRule => {
|
||||
const sassLoaderIndex = oneOfRule.use.findIndex(item => item.loader === sassLoader)
|
||||
oneOfRule.use.splice(sassLoaderIndex, 0, { loader: require.resolve('css-unicode-loader') })
|
||||
})
|
||||
})
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
// 为了补删除换行而加的配置
|
||||
// config.module
|
||||
// .rule('vue')
|
||||
// .use('vue-loader')
|
||||
// .loader('vue-loader')
|
||||
// .tap((options) => {
|
||||
// // modify the options...
|
||||
// options.compilerOptions.preserveWhitespace = false
|
||||
// return options
|
||||
// })
|
||||
}
|
||||
}
|
||||