melo-test/ contains a minimal FastAPI server + static HTML UI for auditioning MeloTTS-Korean before wiring it into the bot: - server.py: POST /tts -> wav, model loaded once at startup - index.html: textarea + presets + audio player, shows synth latency - requirements.txt / README.md: setup steps Directory is melo-test/ because the repo .gitignore already excludes test/. Discord-bot integration is unchanged in this branch; this is a local audition harness only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
3.8 KiB
HTML
108 lines
3.8 KiB
HTML
<!doctype html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>MeloTTS-Korean 테스트</title>
|
|
<style>
|
|
:root { color-scheme: light dark; }
|
|
body {
|
|
font-family: ui-sans-serif, system-ui, -apple-system, "Apple SD Gothic Neo", sans-serif;
|
|
max-width: 760px; margin: 40px auto; padding: 0 20px; line-height: 1.5;
|
|
}
|
|
h1 { margin-bottom: 4px; }
|
|
p.lead { color: #666; margin-top: 0; }
|
|
textarea {
|
|
width: 100%; min-height: 140px; font-size: 16px; padding: 12px;
|
|
box-sizing: border-box; border-radius: 8px; border: 1px solid #ccc;
|
|
font-family: inherit;
|
|
}
|
|
.row { display: flex; gap: 12px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
|
|
button {
|
|
padding: 10px 22px; font-size: 16px; cursor: pointer;
|
|
border-radius: 8px; border: 1px solid #888; background: #f5f5f5;
|
|
}
|
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.meta { color: #666; font-size: 14px; }
|
|
.err { color: #c00; }
|
|
audio { width: 100%; margin-top: 16px; }
|
|
.preset { font-size: 14px; padding: 6px 10px; }
|
|
hr { margin: 24px 0; border: none; border-top: 1px solid #ddd; }
|
|
code { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>MeloTTS-Korean 테스트</h1>
|
|
<p class="lead">
|
|
한국어 텍스트를 입력 후 <b>합성</b> 버튼을 누르세요.
|
|
서버 첫 호출은 모델 로딩이 끝나야 하므로 수십 초 걸릴 수 있습니다.
|
|
</p>
|
|
|
|
<textarea id="text" placeholder="안녕하세요. 메로 한국어 티티에스 테스트입니다."></textarea>
|
|
|
|
<div class="row">
|
|
<button class="preset" data-text="안녕하세요. 메로 한국어 티티에스 테스트입니다.">기본 인사</button>
|
|
<button class="preset" data-text="오늘 디스코드 채팅 봇에 새 보이스 엔진을 붙여 봤습니다. 발음이 어떤가요?">중간 길이</button>
|
|
<button class="preset" data-text="ㅋㅋㅋ 진짜요? 그건 좀 너무한데. 다시 한 번 확인해 주실 수 있을까요?">구어체</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>speed
|
|
<input type="number" id="speed" value="1.0" step="0.1" min="0.5" max="2.0" style="width:70px">
|
|
</label>
|
|
<button id="go">합성</button>
|
|
<span class="meta" id="status">대기중</span>
|
|
</div>
|
|
|
|
<audio id="audio" controls></audio>
|
|
|
|
<hr>
|
|
<p class="meta">
|
|
서버 엔드포인트: <code>POST /tts</code> body <code>{ "text": "...", "speed": 1.0 }</code><br>
|
|
응답: <code>audio/wav</code> 바이너리. 헤더 <code>X-Synth-Ms</code> 에 합성 소요 시간(ms).
|
|
</p>
|
|
|
|
<script>
|
|
const $ = id => document.getElementById(id);
|
|
|
|
document.querySelectorAll(".preset").forEach(b => {
|
|
b.onclick = () => { $("text").value = b.dataset.text; };
|
|
});
|
|
|
|
$("go").onclick = async () => {
|
|
const text = $("text").value.trim();
|
|
const speed = parseFloat($("speed").value) || 1.0;
|
|
if (!text) { $("status").textContent = "텍스트 입력 필요"; return; }
|
|
|
|
$("go").disabled = true;
|
|
$("status").textContent = "합성중...";
|
|
const t0 = performance.now();
|
|
try {
|
|
const res = await fetch("/tts", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text, speed })
|
|
});
|
|
if (!res.ok) {
|
|
const msg = await res.text().catch(() => "");
|
|
throw new Error(`${res.status} ${msg}`);
|
|
}
|
|
const synthMs = res.headers.get("X-Synth-Ms");
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const audio = $("audio");
|
|
audio.src = url;
|
|
audio.play().catch(() => {});
|
|
const totalMs = Math.round(performance.now() - t0);
|
|
$("status").innerHTML =
|
|
`합성 ${synthMs ?? "?"}ms / 총 ${totalMs}ms / 크기 ${(blob.size/1024).toFixed(1)}KB`;
|
|
} catch (e) {
|
|
$("status").innerHTML = `<span class="err">에러: ${e.message}</span>`;
|
|
} finally {
|
|
$("go").disabled = false;
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|