274 lines
7.1 KiB
HTML
274 lines
7.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="robots" content="noindex, nofollow" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
/>
|
|
<style>
|
|
:root {
|
|
--acolor: black;
|
|
}
|
|
|
|
@supports (color-scheme: dark only) {
|
|
:root[data-darkmode='f'] {
|
|
color-scheme: dark only;
|
|
--acolor: white;
|
|
}
|
|
}
|
|
|
|
* {
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
a,
|
|
a:link,
|
|
a:visited,
|
|
a:hover,
|
|
a:focus,
|
|
a:active {
|
|
text-decoration: none;
|
|
color: var(--acolor);
|
|
}
|
|
|
|
hr {
|
|
visibility: hidden;
|
|
}
|
|
|
|
#div1 {
|
|
height: 100vh;
|
|
}
|
|
|
|
@supports (max-width: 100dvw) {
|
|
#div1 {
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
if (
|
|
CSS.supports('color-scheme', 'dark only') &&
|
|
location.hash !== '#forceDarkMode=0'
|
|
) {
|
|
document.documentElement.dataset.darkmode = 'f'
|
|
}
|
|
</script>
|
|
</head>
|
|
<body style="padding: 0; margin: 0">
|
|
<div
|
|
id="div1"
|
|
style="
|
|
display: grid;
|
|
margin-left: 8px;
|
|
margin-right: 8px;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
grid-template-rows: auto auto auto 1fr;
|
|
"
|
|
>
|
|
<hr />
|
|
<div
|
|
style="
|
|
display: grid;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
"
|
|
>
|
|
<div style="overflow-x: hidden; overflow-y: auto">
|
|
Load file
|
|
<br />
|
|
<input type="file" id="file" autocomplete="off" />
|
|
</div>
|
|
<div style="display: grid; place-items: center end">
|
|
<span>
|
|
<input
|
|
type="checkbox"
|
|
id="forceDarkMode"
|
|
autocomplete="off"
|
|
disabled
|
|
/>
|
|
<label for="forceDarkMode">Force dark mode</label>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<hr />
|
|
<textarea style="resize: none" autocomplete="off"></textarea>
|
|
<hr />
|
|
<div
|
|
style="
|
|
display: grid;
|
|
overflow-x: auto;
|
|
overflow-y: auto;
|
|
place-items: center;
|
|
"
|
|
>
|
|
Voice:
|
|
<input type="text" id="voice" autocomplete="off" />
|
|
</div>
|
|
<hr />
|
|
<div
|
|
style="
|
|
display: grid;
|
|
overflow-x: auto;
|
|
overflow-y: auto;
|
|
place-items: center;
|
|
"
|
|
>
|
|
<button id="convert">Convert</button>
|
|
<br />
|
|
<a href="https://git.projectsegfau.lt/a/tts-stuff" target="_blank"
|
|
>Source code</a
|
|
>
|
|
<a href="license.txt" target="_blank">License (MIT)</a>
|
|
</div>
|
|
<hr />
|
|
</div>
|
|
<div
|
|
id="div2"
|
|
style="
|
|
display: none;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
place-items: start center;
|
|
"
|
|
>
|
|
<hr />
|
|
<span id="audioSpan"></span>
|
|
<hr />
|
|
<span id="durationSpan"></span>
|
|
<hr />
|
|
<span id="saveSpan"></span>
|
|
<hr />
|
|
<button id="back">Back</button>
|
|
<hr />
|
|
</div>
|
|
<script>
|
|
let blobUrl = null
|
|
|
|
document.querySelector('#file').onchange = evt => {
|
|
if (evt.target.files.length > 0) {
|
|
const reader = new FileReader()
|
|
|
|
reader.onload = readerEvent => {
|
|
document.querySelector('textarea').value = readerEvent.target.result
|
|
document.querySelector('#file').value = ''
|
|
}
|
|
|
|
reader.readAsText(evt.target.files[0])
|
|
}
|
|
}
|
|
|
|
document.querySelector('#convert').onclick = () => {
|
|
const text = document.querySelector('textarea').value
|
|
|
|
if (text.length === 0) {
|
|
return
|
|
}
|
|
|
|
const voice = document.querySelector('#voice').value.trim()
|
|
|
|
if (voice.length === 0) {
|
|
return
|
|
}
|
|
|
|
document.querySelector('#div1').style.setProperty('display', 'none')
|
|
|
|
fetch('/', {
|
|
method: 'POST',
|
|
body: JSON.stringify([voice, text])
|
|
})
|
|
.then(r => {
|
|
if (r.ok) {
|
|
r.blob()
|
|
.then(blob => {
|
|
document.querySelector(
|
|
'#durationSpan'
|
|
).textContent = `Duration: ${r.headers.get('Duration')}`
|
|
|
|
blobUrl = URL.createObjectURL(blob)
|
|
|
|
const a = document.createElement('a')
|
|
a.innerText = 'Save'
|
|
a.href = blobUrl
|
|
switch (r.headers.get('Content-Type')) {
|
|
case 'audio/wav':
|
|
a.download = 'file.wav'
|
|
break
|
|
case 'audio/flac':
|
|
a.download = 'file.flac'
|
|
break
|
|
default:
|
|
a.download = 'file'
|
|
}
|
|
|
|
const audio = document.createElement('audio')
|
|
audio.src = blobUrl
|
|
audio.controls = 'controls'
|
|
|
|
document.querySelector('#saveSpan').appendChild(a)
|
|
document.querySelector('#audioSpan').appendChild(audio)
|
|
|
|
document
|
|
.querySelector('#div2')
|
|
.style.setProperty('display', 'grid')
|
|
})
|
|
.catch(() => {
|
|
document
|
|
.querySelector('#div1')
|
|
.style.setProperty('display', 'grid')
|
|
})
|
|
} else {
|
|
document
|
|
.querySelector('#div1')
|
|
.style.setProperty('display', 'grid')
|
|
}
|
|
})
|
|
.catch(() => {
|
|
document.querySelector('#div1').style.setProperty('display', 'grid')
|
|
})
|
|
}
|
|
|
|
document.querySelector('#back').onclick = () => {
|
|
document.querySelector('#div2').style.setProperty('display', 'none')
|
|
document.querySelector('#durationSpan').textContent = ''
|
|
|
|
document
|
|
.querySelector('#saveSpan')
|
|
.removeChild(document.querySelector('#saveSpan a'))
|
|
|
|
document
|
|
.querySelector('#audioSpan')
|
|
.removeChild(document.querySelector('#audioSpan audio'))
|
|
|
|
URL.revokeObjectURL(blobUrl)
|
|
blobUrl = null
|
|
|
|
document.querySelector('#div1').style.setProperty('display', 'grid')
|
|
}
|
|
|
|
if (CSS.supports('color-scheme', 'dark only')) {
|
|
document.querySelector('#forceDarkMode').disabled = false
|
|
|
|
document.querySelector('#forceDarkMode').checked =
|
|
'darkmode' in document.documentElement.dataset &&
|
|
document.documentElement.dataset.darkmode === 'f'
|
|
|
|
document.querySelector('#forceDarkMode').onclick = () => {
|
|
if (location.hash === '#forceDarkMode=0') {
|
|
location.href = location.href.substring(
|
|
0,
|
|
location.href.indexOf('#')
|
|
)
|
|
} else {
|
|
location.hash = '#forceDarkMode=0'
|
|
location.reload()
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|