409 lines
10 KiB
HTML
409 lines
10 KiB
HTML
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
#header {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
height: 50px;
|
|
line-height: 50px;
|
|
width: 100%;
|
|
background-color: #999;
|
|
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.5);
|
|
font-family: Helvetica, sans-serif;
|
|
}
|
|
#header, #header a {
|
|
color: white;
|
|
}
|
|
#downloads {
|
|
float: right;
|
|
background: #999;
|
|
}
|
|
.download {
|
|
float: right;
|
|
background: #999;
|
|
padding: 0 5px;
|
|
}
|
|
.home {
|
|
margin: 0;
|
|
font-size: 125%;
|
|
font-weight: lighter;
|
|
text-transform: lowercase;
|
|
}
|
|
.home a {
|
|
margin: 0;
|
|
background: #666;
|
|
padding-left: 25px;
|
|
padding-right: 30px;
|
|
margin-right: 20px;
|
|
float: left;
|
|
text-decoration: none;
|
|
}
|
|
.home:hover a {
|
|
background: #555;
|
|
}
|
|
#audio {
|
|
margin-top: 9px;
|
|
width: 50%;
|
|
display: inline-block;
|
|
}
|
|
#transcript {
|
|
margin: 0 15px;
|
|
margin-top: 70px;
|
|
margin-bottom: 5em;
|
|
white-space: pre-wrap;
|
|
line-height: 2em;
|
|
max-width: 600px;
|
|
color: #999;
|
|
}
|
|
#transcript.status {
|
|
background-color: #333;
|
|
color: #fff;
|
|
font-family: Courier, mono;
|
|
line-height: 1em;
|
|
font-size: 10pt;
|
|
max-width: 100%;
|
|
}
|
|
#transcript.status h2 {
|
|
padding: 10px;
|
|
}
|
|
#transcript.status .entry {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
}
|
|
#transcript.status progress {
|
|
width: 100%;
|
|
height: 30px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.success {
|
|
color: black;
|
|
}
|
|
.success:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.active {
|
|
color: magenta;
|
|
}
|
|
#preloader {
|
|
visibility: hidden;
|
|
}
|
|
.phactive {
|
|
text-decoration: underline;
|
|
}
|
|
.phones {
|
|
position: absolute;
|
|
color: #333;
|
|
}
|
|
.phones .phone {
|
|
margin-right: 5px;
|
|
font-family: Helvetica, sans-serif;
|
|
text-transform: uppercase;
|
|
font-size: 50%;
|
|
}
|
|
.phones .phone:last-child {
|
|
margin-right: 0;
|
|
}
|
|
#footer {
|
|
margin-top: 100px;
|
|
border-top: 1px dotted black;
|
|
font-size: 8pt;
|
|
font-style: italic;
|
|
font-family: Helvetica, sans-serif;
|
|
padding: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="header">
|
|
<!-- <h1 class="home"><a href="/">Gentle</a></h1> -->
|
|
<audio id="audio" src="a.wav" controls="true" preload="auto"></audio>
|
|
<img src="/preloader.gif" id="preloader" alt="loading...">
|
|
<span id="downloads"> </div>
|
|
</div>
|
|
<div id="transcript"></div>
|
|
<!-- <div id="footer">
|
|
<a href="https://lowerquality.com/gentle">Gentle</a> is free software released under the <a href="https://opensource.org/licenses/MIT">MIT license</a>. <a href="https://lowerquality.com/gentle">Homepage</a> | <a href="https://github.com/lowerquality/gentle">Source code</a>.
|
|
</div> -->
|
|
|
|
<script>
|
|
|
|
function get(url, cb) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("GET", url, true);
|
|
xhr.onload = function() {
|
|
cb(this.responseText);
|
|
}
|
|
xhr.send();
|
|
}
|
|
function get_json(url, cb) {
|
|
get(url, function(x) {
|
|
cb(JSON.parse(x));
|
|
});
|
|
}
|
|
|
|
var $a = document.getElementById("audio");
|
|
window.onkeydown = function(ev) {
|
|
if(ev.keyCode == 32) {
|
|
ev.preventDefault();
|
|
$a.pause();
|
|
}
|
|
}
|
|
|
|
var $trans = document.getElementById("transcript");
|
|
var $preloader = document.getElementById('preloader');
|
|
|
|
var wds = [];
|
|
var cur_wd;
|
|
|
|
var $phones = document.createElement("div");
|
|
$phones.className = "phones";
|
|
document.body.appendChild($phones);
|
|
|
|
var cur_phones$ = []; // List of phoneme $divs
|
|
var $active_phone;
|
|
|
|
function render_phones(wd) {
|
|
cur_phones$ = [];
|
|
$phones.innerHTML = "";
|
|
$active_phone = null;
|
|
|
|
$phones.style.top = wd.$div.offsetTop + 18;
|
|
$phones.style.left = wd.$div.offsetLeft;
|
|
|
|
var dur = wd.end - wd.start;
|
|
|
|
var start_x = wd.$div.offsetLeft;
|
|
|
|
wd.phones
|
|
.forEach(function(ph){
|
|
var $p = document.createElement("span");
|
|
$p.className = "phone";
|
|
$p.textContent = ph.phone.split("_")[0];
|
|
|
|
$phones.appendChild($p);
|
|
cur_phones$.push($p);
|
|
});
|
|
|
|
var offsetToCenter = (wd.$div.offsetWidth - $phones.offsetWidth) / 2;
|
|
$phones.style.left = wd.$div.offsetLeft + offsetToCenter;
|
|
}
|
|
function highlight_phone(t) {
|
|
if(!cur_wd) {
|
|
$phones.innerHTML = "";
|
|
return;
|
|
}
|
|
var hit;
|
|
var cur_t = cur_wd.start;
|
|
|
|
cur_wd.phones.forEach(function(ph, idx) {
|
|
if(cur_t <= t && cur_t + ph.duration >= t) {
|
|
hit = idx;
|
|
}
|
|
cur_t += ph.duration;
|
|
});
|
|
|
|
if(hit) {
|
|
var $ph = cur_phones$[hit];
|
|
if($ph != $active_phone) {
|
|
if($active_phone) {
|
|
$active_phone.classList.remove("phactive");
|
|
}
|
|
if($ph) {
|
|
$ph.classList.add("phactive");
|
|
}
|
|
}
|
|
$active_phone = $ph;
|
|
}
|
|
}
|
|
|
|
function highlight_word() {
|
|
var t = $a.currentTime;
|
|
// XXX: O(N); use binary search
|
|
var hits = wds.filter(function(x) {
|
|
return (t - x.start) > 0.01 && (x.end - t) > 0.01;
|
|
}, wds);
|
|
var next_wd = hits[hits.length - 1];
|
|
|
|
if(cur_wd != next_wd) {
|
|
var active = document.querySelectorAll('.active');
|
|
for(var i = 0; i < active.length; i++) {
|
|
active[i].classList.remove('active');
|
|
}
|
|
if(next_wd && next_wd.$div) {
|
|
next_wd.$div.classList.add('active');
|
|
render_phones(next_wd);
|
|
}
|
|
}
|
|
cur_wd = next_wd;
|
|
highlight_phone(t);
|
|
|
|
window.requestAnimationFrame(highlight_word);
|
|
}
|
|
window.requestAnimationFrame(highlight_word);
|
|
|
|
$trans.innerHTML = "Loading...";
|
|
|
|
function render(ret) {
|
|
wds = ret['words'] || [];
|
|
transcript = ret['transcript'];
|
|
|
|
$trans.innerHTML = '';
|
|
|
|
var currentOffset = 0;
|
|
|
|
wds.forEach(function(wd) {
|
|
if(wd.case == 'not-found-in-transcript') {
|
|
// TODO: show phonemes somewhere
|
|
var txt = ' ' + wd.word;
|
|
var $plaintext = document.createTextNode(txt);
|
|
$trans.appendChild($plaintext);
|
|
return;
|
|
}
|
|
|
|
// Add non-linked text
|
|
if(wd.startOffset > currentOffset) {
|
|
var txt = transcript.slice(currentOffset, wd.startOffset);
|
|
var $plaintext = document.createTextNode(txt);
|
|
$trans.appendChild($plaintext);
|
|
currentOffset = wd.startOffset;
|
|
}
|
|
|
|
var $wd = document.createElement('span');
|
|
var txt = transcript.slice(wd.startOffset, wd.endOffset);
|
|
var $wdText = document.createTextNode(txt);
|
|
$wd.appendChild($wdText);
|
|
wd.$div = $wd;
|
|
if(wd.start !== undefined) {
|
|
$wd.className = 'success';
|
|
}
|
|
$wd.onclick = function() {
|
|
if(wd.start !== undefined) {
|
|
console.log(wd.start);
|
|
$a.currentTime = wd.start;
|
|
$a.play();
|
|
}
|
|
};
|
|
$trans.appendChild($wd);
|
|
currentOffset = wd.endOffset;
|
|
});
|
|
|
|
var txt = transcript.slice(currentOffset, transcript.length);
|
|
var $plaintext = document.createTextNode(txt);
|
|
$trans.appendChild($plaintext);
|
|
currentOffset = transcript.length;
|
|
}
|
|
|
|
function show_downloads() {
|
|
var $d = document.getElementById("downloads");
|
|
$d.textContent = "Download as: ";
|
|
var uid = window.location.pathname.split("/")[2];
|
|
// Name, path, title, inhibit-on-file:///
|
|
[["CSV", "align.csv", "Word alignment CSV"],
|
|
["JSON", "align.json", "JSON word/phoneme alignment data"],
|
|
["Zip", "/zip/" + uid + ".zip", "Standalone zipfile", true]]
|
|
.forEach(function(x) {
|
|
var $a = document.createElement("a");
|
|
$a.className = "download";
|
|
$a.textContent = x[0];
|
|
$a.href = x[1];
|
|
$a.title = x[2];
|
|
if(!x[3] || window.location.protocol != "file:") {
|
|
$d.appendChild($a);
|
|
}
|
|
});
|
|
}
|
|
|
|
var status_init = false;
|
|
var status_log = []; // [ status ]
|
|
var $status_pro;
|
|
|
|
function render_status(ret) {
|
|
if(!status_init) {
|
|
// Clobber the $trans div and use it for status updates
|
|
$trans.innerHTML = "<h2>transcription in progress</h2>";
|
|
$trans.className = "status";
|
|
$status_pro = document.createElement("progress");
|
|
$status_pro.setAttribute("min", "0");
|
|
$status_pro.setAttribute("max", "100");
|
|
$status_pro.value = 0;
|
|
$trans.appendChild($status_pro);
|
|
|
|
status_init = true;
|
|
}
|
|
if(ret.status !== "TRANSCRIBING") {
|
|
if(ret.percent) {
|
|
$status_pro.value = (100*ret.percent);
|
|
}
|
|
}
|
|
else if(ret.percent && (status_log.length == 0 || status_log[status_log.length-1].percent+0.0001 < ret.percent)) {
|
|
// New entry
|
|
var $entry = document.createElement("div");
|
|
$entry.className = "entry";
|
|
$entry.textContent = ret.message;
|
|
ret.$div = $entry;
|
|
|
|
if(ret.percent) {
|
|
$status_pro.value = (100*ret.percent);
|
|
}
|
|
|
|
if(status_log.length > 0) {
|
|
$trans.insertBefore($entry, status_log[status_log.length-1].$div);
|
|
}
|
|
else {
|
|
$trans.appendChild($entry);
|
|
}
|
|
status_log.push(ret);
|
|
}
|
|
}
|
|
|
|
function update() {
|
|
if(INLINE_JSON) {
|
|
// We want this to work from file:/// domains, so we provide a
|
|
// mechanism for inlining the alignment data.
|
|
render(INLINE_JSON);
|
|
// show_downloads();
|
|
}
|
|
else {
|
|
// Show the status
|
|
get_json('status.json', function(ret) {
|
|
$a.style.visibility = 'hidden';
|
|
if (ret.status == 'ERROR') {
|
|
$preloader.style.visibility = 'hidden';
|
|
$trans.innerHTML = '<b>' + ret.status + ': ' + ret.error + '</b>';
|
|
} else if (ret.status == 'TRANSCRIBING' || ret.status == 'ALIGNING') {
|
|
$preloader.style.visibility = 'visible';
|
|
render_status(ret);
|
|
setTimeout(update, 2000);
|
|
} else if (ret.status == 'OK') {
|
|
// show_downloads();
|
|
$preloader.style.visibility = 'hidden';
|
|
// XXX: should we fetch the align.json?
|
|
// window.location.reload();
|
|
$a.style.visibility = 'visible';
|
|
render(ret);
|
|
} else if (ret.status == 'ENCODING' || ret.status == 'STARTED') {
|
|
$preloader.style.visibility = 'visible';
|
|
$trans.innerHTML = 'Encoding, please wait...';
|
|
setTimeout(update, 2000);
|
|
} else {
|
|
console.log("unknown status", ret);
|
|
$preloader.style.visibility = 'hidden';
|
|
$trans.innerHTML = ret.status + '...';
|
|
setTimeout(update, 5000);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var INLINE_JSON;
|
|
|
|
update();
|
|
|
|
</script></body></html>
|