mirror of
https://github.com/babysor/MockingBird.git
synced 2024-03-22 13:11:31 +08:00
523 lines
16 KiB
HTML
523 lines
16 KiB
HTML
<!DOCTYPE HTML>
|
||
<html>
|
||
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
||
<link rel="shortcut icon" type="image/png" href="../static/img/bird-sm.png">
|
||
|
||
<title>MockingBird Web Server</title>
|
||
|
||
<script src="{{ url_for('static',filename='js/recorder-core.js') }}"></script>
|
||
<script src="{{ url_for('static',filename='js/mp3.js') }}"></script>
|
||
<script src="{{ url_for('static',filename='js/wav.js') }}"></script>
|
||
<script src="{{ url_for('static',filename='js/mp3-engine.js') }}"></script>
|
||
<script src="{{ url_for('static',filename='js/frequency.histogram.view.js') }}"></script>
|
||
<script src="{{ url_for('static',filename='js/lib.fft.js') }}"></script>
|
||
|
||
<script src="{{ url_for('static',filename='js/jquery.js') }}"></script>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div class="main">
|
||
|
||
<div class="mainBox">
|
||
<div class="title" >
|
||
<div style="width: 15%;float: left;margin-left: 5%;">
|
||
<img src="../static/img/bird.png" style="width: 100%;border-radius:50%;"></img>
|
||
</div>
|
||
<div style="width: 80% ;height: 15%;; margin-left: 15%;overflow: hidden;">
|
||
<div style="margin-left: 5%;margin-top: 15px;font-size: xx-large;font-weight: bolder;">
|
||
拟声鸟工具箱
|
||
</div>
|
||
<div style="margin-left: 5%;margin-top: 3px;font-size: large;">
|
||
<a href="https://github.com/babysor/MockingBird" target="_blank">https://github.com/babysor/MockingBird</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-left: 5%;margin-top: 50px;width: 90%;">
|
||
<div style="font-size: larger;font-weight: bolder;">1. 请输入中文</div>
|
||
<textarea id="user_input_text"
|
||
style="border:1px solid #ccc; width: 100%; height: 100px; font-size: 15px; margin-top: 10px;"></textarea>
|
||
</div>
|
||
<div class="pd btns" style="margin-left: 5%;margin-top: 20px;width: 90%; ">
|
||
<!-- <div>
|
||
<button onclick="recOpen()" style="margin-right:10px">打开录音,请求权限</button>
|
||
<button onclick="recClose()" style="margin-right:0">关闭录音,释放资源</button>
|
||
</div> -->
|
||
<div style="font-size: larger;font-weight: bolder;">2. 请直接录音,点击停止结束</div>
|
||
<button onclick="recStart()" >录制</button>
|
||
<button onclick="recStop()">停止</button>
|
||
<button onclick="recPlay()" >播放</button>
|
||
</div>
|
||
<div class="pd btns" style="margin-left: 5%;margin-top: 20px;width: 90%; ">
|
||
<div style="font-size: larger;font-weight: bolder;">或上传音频</div>
|
||
<input type="file" id="fileInput" accept=".wav" />
|
||
<label for="fileInput">选择音频</label>
|
||
<div id="audio1"></div>
|
||
</div>
|
||
<div class="pd btns" style="margin-left: 5%;margin-top: 20px;width: 90%; ">
|
||
<div style="font-size: larger;font-weight: bolder;">3. 选择Synthesizer模型</div>
|
||
<span class="box">
|
||
<select id="selectSynt">
|
||
</select>
|
||
</span>
|
||
</div>
|
||
<div class="pd btns" style="margin-left: 5%;margin-top: 20px;width: 90%; ">
|
||
<div style="font-size: larger;font-weight: bolder;">4. 选择Vocoder模型</div>
|
||
<span class="box">
|
||
<select id="selectVocoder">
|
||
<option>WaveRNN</option>
|
||
<option>HifiGAN</option>
|
||
</select>
|
||
</span>
|
||
</div>
|
||
<div class="pd btns" style="margin-left: 5%;margin-top: 20px;width: 90%; text-align:right;">
|
||
<button id="upload" onclick="recUpload()">上传合成</button>
|
||
</div>
|
||
|
||
<!-- 波形绘制区域 -->
|
||
<!-- <div class="pd recpower">
|
||
<div style="height:40px;width:100%;background:#fff;position:relative;">
|
||
<div class="recpowerx" style="height:40px;background:#ff3295;position:absolute;"></div>
|
||
<div class="recpowert" style="padding-left:50px; line-height:40px; position: relative;"></div>
|
||
</div>
|
||
</div> -->
|
||
<!-- <div class="pd waveBox" style="height:100px;">
|
||
<div style="border:1px solid #ccc;display:inline-block; width: 100%; height: 100px;">
|
||
<div style="height:100px; width: 100%; background-color: #5da1f5; position: relative;left: 0px;top: 0px;z-index: 10;"
|
||
class="recwave"></div>
|
||
<div
|
||
style="background-color: transparent;position: relative;top: -80px;left: 30%;z-index: 20;font-size: 48px;color: #fff;">
|
||
音频预览</div>
|
||
</div>
|
||
</div> -->
|
||
<div class="reclog" style="margin-left: 5%;margin-top: 20px;width: 90%;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
|
||
$("#fileInput").change(function(){
|
||
var file = $("#fileInput").get(0).files;
|
||
if (file.length > 0) {
|
||
var path = URL.createObjectURL(file[0]);
|
||
var audio = document.createElement('audio');
|
||
audio.src = path;
|
||
audio.controls = true;
|
||
$('#audio1').empty().append(audio);
|
||
}
|
||
});
|
||
|
||
fetch("/api/synthesizers", {
|
||
method: 'get',
|
||
headers: {
|
||
"X-CSRFToken": "{{ csrf_token() }}"
|
||
}
|
||
}).then(function (res) {
|
||
if (!res.ok) throw Error(res.statusText);
|
||
return res.json();
|
||
}).then(function (data) {
|
||
for (var synt of data) {
|
||
var option = document.createElement('option');
|
||
option.text = synt.name
|
||
option.value = synt.path
|
||
$("#selectSynt").append(option);
|
||
}
|
||
}).catch(function (err) {
|
||
console.log('Error: ' + err.message);
|
||
})
|
||
|
||
var rec, wave, recBlob;
|
||
/**调用open打开录音请求好录音权限**/
|
||
var recOpen = function () {//一般在显示出录音按钮或相关的录音界面时进行此方法调用,后面用户点击开始录音时就能畅通无阻了
|
||
rec = null;
|
||
wave = null;
|
||
recBlob = null;
|
||
var newRec = Recorder({
|
||
type: "wav", bitRate: 16, sampleRate: 16000
|
||
, onProcess: function (buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
|
||
//录音实时回调,大约1秒调用12次本回调
|
||
// document.querySelector(".recpowerx").style.width = powerLevel + "%";
|
||
// document.querySelector(".recpowert").innerText = bufferDuration + " / " + powerLevel;
|
||
|
||
//可视化图形绘制
|
||
// wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
|
||
}
|
||
});
|
||
|
||
createDelayDialog(); //我们可以选择性的弹一个对话框:为了防止移动端浏览器存在第三种情况:用户忽略,并且(或者国产系统UC系)浏览器没有任何回调,此处demo省略了弹窗的代码
|
||
newRec.open(function () {//打开麦克风授权获得相关资源
|
||
dialogCancel(); //如果开启了弹框,此处需要取消
|
||
|
||
rec = newRec;
|
||
|
||
//此处创建这些音频可视化图形绘制浏览器支持妥妥的
|
||
// wave = Recorder.FrequencyHistogramView({ elem: ".recwave" });
|
||
|
||
reclog("已打开录音,可以点击录制开始录音了", 2);
|
||
}, function (msg, isUserNotAllow) {//用户拒绝未授权或不支持
|
||
dialogCancel(); //如果开启了弹框,此处需要取消
|
||
reclog((isUserNotAllow ? "UserNotAllow," : "") + "打开录音失败:" + msg, 1);
|
||
});
|
||
|
||
window.waitDialogClick = function () {
|
||
dialogCancel();
|
||
reclog("打开失败:权限请求被忽略,<span style='color:#f00'>用户主动点击的弹窗</span>", 1);
|
||
};
|
||
};
|
||
|
||
|
||
|
||
/**关闭录音,释放资源**/
|
||
function recClose() {
|
||
if (rec) {
|
||
rec.close();
|
||
reclog("已关闭");
|
||
} else {
|
||
reclog("未打开录音", 1);
|
||
};
|
||
};
|
||
|
||
|
||
|
||
/**开始录音**/
|
||
function recStart() {//打开了录音后才能进行start、stop调用
|
||
if (rec && Recorder.IsOpen()) {
|
||
recBlob = null;
|
||
rec.start();
|
||
reclog("已开始录音...");
|
||
} else {
|
||
reclog("未打开录音,请求录音权限,如已允许录音权限,请再次点击录制", 1);
|
||
recOpen();
|
||
};
|
||
};
|
||
|
||
function recStop() {
|
||
rec.stop(function (blob, duration) {
|
||
rec.close();//释放录音资源
|
||
console.log(blob, (window.URL || webkitURL).createObjectURL(blob), "时长:" + duration + "ms");
|
||
recBlob = blob;
|
||
reclog("已录制wav:" + duration + "ms " + blob.size + "字节,可以点击播放、上传了", 2);
|
||
}, function (msg) {
|
||
reclog("录音失败:" + msg, 1);
|
||
});
|
||
};
|
||
|
||
|
||
/**播放**/
|
||
function recPlay() {
|
||
if (!recBlob) {
|
||
reclog("请先录音,然后停止后再播放", 1);
|
||
return;
|
||
};
|
||
var cls = ("a" + Math.random()).replace(".", "");
|
||
reclog('播放中: <span class="' + cls + '"></span>');
|
||
var audio = document.createElement("audio");
|
||
audio.controls = true;
|
||
document.querySelector("." + cls).appendChild(audio);
|
||
//简单利用URL生成播放地址,注意不用了时需要revokeObjectURL,否则霸占内存
|
||
audio.src = (window.URL || webkitURL).createObjectURL(recBlob);
|
||
audio.play();
|
||
|
||
setTimeout(function () {
|
||
(window.URL || webkitURL).revokeObjectURL(audio.src);
|
||
}, 5000);
|
||
};
|
||
|
||
function playResult(resultBlob) {
|
||
if (!resultBlob) {
|
||
reclog("服务端出错,请重试", 1);
|
||
return;
|
||
};
|
||
var cls = ("a" + Math.random()).replace(".", "");
|
||
reclog('播放中: <span class="' + cls + '"></span>');
|
||
var audio = document.createElement("audio");
|
||
audio.controls = true;
|
||
document.querySelector("." + cls).appendChild(audio);
|
||
//简单利用URL生成播放地址,注意不用了时需要revokeObjectURL,否则霸占内存
|
||
audio.src = (window.URL || webkitURL).createObjectURL(resultBlob);
|
||
audio.play();
|
||
|
||
setTimeout(function () {
|
||
(window.URL || webkitURL).revokeObjectURL(audio.src);
|
||
}, 12000);
|
||
};
|
||
|
||
/**上传**/
|
||
function recUpload() {
|
||
var blob
|
||
var loadedAudios = $("#fileInput").get(0).files
|
||
if (loadedAudios.length > 0) {
|
||
blob = loadedAudios[0];
|
||
} else {
|
||
blob = recBlob;
|
||
}
|
||
if (!blob) {
|
||
reclog("请先录音或选择音频,然后停止后再上传", 1);
|
||
return;
|
||
};
|
||
|
||
//本例子假设使用原始XMLHttpRequest请求方式,实际使用中自行调整为自己的请求方式
|
||
//录音结束时拿到了blob文件对象,可以用FileReader读取出内容,或者用FormData上传
|
||
var api = "/api/synthesize";
|
||
|
||
reclog("开始上传到" + api + ",请求稍后...");
|
||
|
||
var reader = new FileReader();
|
||
reader.onloadend = function () {
|
||
var csrftoken = "{{ csrf_token() }}";
|
||
var user_input_text = document.getElementById("user_input_text");
|
||
var input_text = user_input_text.value;
|
||
var postData = new FormData();
|
||
postData.append("text", input_text)
|
||
postData.append("file", blob)
|
||
var syntSelect = document.getElementById("selectSynt");
|
||
var path = syntSelect.options[syntSelect.selectedIndex].value;
|
||
if (!!path) {
|
||
postData.append("synt_path", path);
|
||
}
|
||
var vocoderSelect = document.getElementById("selectVocoder");
|
||
var vocoder = vocoderSelect.options[vocoderSelect.selectedIndex].value;
|
||
if (!!vocoder) {
|
||
postData.append("vocoder", vocoder);
|
||
}
|
||
|
||
fetch(api, {
|
||
method: 'post',
|
||
headers: {
|
||
"X-CSRFToken": csrftoken
|
||
},
|
||
body: postData
|
||
}).then(function (res) {
|
||
if (!res.ok) throw Error(res.statusText);
|
||
return res.blob();
|
||
}).then(function (blob) {
|
||
playResult(blob)
|
||
}).catch(function (err) {
|
||
console.log('Error: ' + err.message);
|
||
})
|
||
};
|
||
reader.readAsDataURL(blob);
|
||
};
|
||
|
||
//recOpen我们可以选择性的弹一个对话框:为了防止移动端浏览器存在第三种情况:用户忽略,并且(或者国产系统UC系)浏览器没有任何回调
|
||
var showDialog = function () {
|
||
if (!/mobile/i.test(navigator.userAgent)) {
|
||
return;//只在移动端开启没有权限请求的检测
|
||
};
|
||
dialogCancel();
|
||
|
||
//显示弹框,应该使用自己的弹框方式
|
||
var div = document.createElement("div");
|
||
document.body.appendChild(div);
|
||
div.innerHTML = (''
|
||
+ '<div class="waitDialog" style="z-index:99999;width:100%;height:100%;top:0;left:0;position:fixed;background:rgba(0,0,0,0.3);">'
|
||
+ '<div style="display:flex;height:100%;align-items:center;">'
|
||
+ '<div style="flex:1;"></div>'
|
||
+ '<div style="width:240px;background:#fff;padding:15px 20px;border-radius: 10px;">'
|
||
+ '<div style="padding-bottom:10px;">录音功能需要麦克风权限,请允许;如果未看到任何请求,请点击忽略~</div>'
|
||
+ '<div style="text-align:center;"><a onclick="waitDialogClick()" style="color:#0B1">忽略</a></div>'
|
||
+ '</div>'
|
||
+ '<div style="flex:1;"></div>'
|
||
+ '</div>'
|
||
+ '</div>');
|
||
};
|
||
var createDelayDialog = function () {
|
||
dialogInt = setTimeout(function () {//定时8秒后打开弹窗,用于监测浏览器没有发起权限请求的情况,在open前放置定时器利于收到了回调能及时取消(不管open是同步还是异步回调的)
|
||
showDialog();
|
||
}, 8000);
|
||
};
|
||
var dialogInt;
|
||
var dialogCancel = function () {
|
||
clearTimeout(dialogInt);
|
||
|
||
//关闭弹框,应该使用自己的弹框方式
|
||
var elems = document.querySelectorAll(".waitDialog");
|
||
for (var i = 0; i < elems.length; i++) {
|
||
elems[i].parentNode.removeChild(elems[i]);
|
||
};
|
||
};
|
||
//recOpen弹框End
|
||
</script>
|
||
|
||
<!--以下这坨可以忽略-->
|
||
<script>
|
||
function reclog(s, color) {
|
||
var now = new Date();
|
||
var t = ("0" + now.getHours()).substr(-2)
|
||
+ ":" + ("0" + now.getMinutes()).substr(-2)
|
||
+ ":" + ("0" + now.getSeconds()).substr(-2);
|
||
var div = document.createElement("div");
|
||
var elem = document.querySelector(".reclog");
|
||
elem.insertBefore(div, elem.firstChild);
|
||
div.innerHTML = '<div style="color:' + (!color ? "" : color == 1 ? "#327de8" : color == 2 ? "#5da1f5" : color) + '">[' + t + ']' + s + '</div>';
|
||
};
|
||
window.onerror = function (message, url, lineNo, columnNo, error) {
|
||
reclog('<span style="color:red">【Uncaught Error】' + message + '<pre>' + "at:" + lineNo + ":" + columnNo + " url:" + url + "\n" + (error && error.stack || "不能获得错误堆栈") + '</pre></span>');
|
||
};
|
||
</script>
|
||
|
||
<script>
|
||
if (/mobile/i.test(navigator.userAgent)) {
|
||
//移动端加载控制台组件
|
||
var elem = document.createElement("script");
|
||
elem.setAttribute("type", "text/javascript");
|
||
|
||
elem.setAttribute("src", "{{ url_for('static',filename='js/eruda.min.js') }}");
|
||
document.body.appendChild(elem);
|
||
elem.onload = function () {
|
||
eruda.init();
|
||
};
|
||
};
|
||
</script>
|
||
|
||
|
||
<style>
|
||
body {
|
||
word-wrap: break-word;
|
||
background: #f5f5f5 center top no-repeat;
|
||
background-size: auto 680px;
|
||
}
|
||
|
||
pre {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
a {
|
||
text-decoration: none;
|
||
color: #327de8;
|
||
}
|
||
|
||
a:hover {
|
||
color: #5da1f5;
|
||
}
|
||
|
||
.main {
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
padding-bottom: 80px
|
||
}
|
||
|
||
.mainBox {
|
||
margin-top: 12px;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
box-shadow: 2px 2px 3px #aaa;
|
||
}
|
||
|
||
|
||
.btns button {
|
||
display: inline-block;
|
||
cursor: pointer;
|
||
border: none;
|
||
border-radius: 3px;
|
||
background: #5698c3;
|
||
color: #fff;
|
||
padding: 0 15px;
|
||
margin: 3px 10px 3px 0;
|
||
width: 70px;
|
||
line-height: 36px;
|
||
height: 36px;
|
||
overflow: hidden;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.btns #upload {
|
||
background: #5698c3;
|
||
color: #fff;
|
||
width: 100px;
|
||
height: 42px;
|
||
}
|
||
|
||
.btns button:active {
|
||
background: #5da1f5
|
||
}
|
||
|
||
.btns button:hover {
|
||
background: #5da1f5
|
||
}
|
||
.pd {
|
||
padding: 0 0 6px 0;
|
||
}
|
||
|
||
.lb {
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
background: #327de8;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
padding: 2px 8px;
|
||
border-radius: 99px;
|
||
}
|
||
|
||
#fileInput {
|
||
width: 0.1px;
|
||
height: 0.1px;
|
||
opacity: 0;
|
||
overflow: hidden;
|
||
position: absolute;
|
||
z-index: -1;
|
||
}
|
||
#fileInput + label {
|
||
padding: 0 15px;
|
||
border-radius: 4px;
|
||
color: white;
|
||
background-color: #5698c3;
|
||
display: inline-block;
|
||
width: 70px;
|
||
line-height: 36px;
|
||
height: 36px;
|
||
}
|
||
#fileInput + label {
|
||
cursor: pointer; /* "hand" cursor */
|
||
}
|
||
#fileInput:focus + label,
|
||
#fileInput + label:hover {
|
||
background-color: #5da1f5;
|
||
}
|
||
|
||
.box select {
|
||
background-color: #5698c3;
|
||
color: white;
|
||
padding: 8px;
|
||
width: 120px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 0.5em;
|
||
outline: none;
|
||
margin: 3px 10px 3px 0;
|
||
}
|
||
|
||
.box::before {
|
||
content: "\f13a";
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 20%;
|
||
height: 100%;
|
||
text-align: center;
|
||
font-size: 28px;
|
||
line-height: 45px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.box:hover::before {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.box select option {
|
||
padding: 30px;
|
||
}
|
||
</style>
|
||
|
||
</body>
|
||
|
||
</html> |