Spaces:
Runtime error
Runtime error
<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> |