test(edge): add Microsoft Edge TTS Korean test harness
Pure-Python alternative to melo-test/ when the MeloTTS native build chain (MeCab/Rust/fugashi/mecab-python3/tokenizers) refuses to install. - edge-test/server.py: FastAPI + edge_tts.Communicate.stream() -> mp3, 9 Korean Neural voices, rate/pitch knobs. - edge-test/index.html: voice selector + rate/pitch + presets + audio. - requirements.txt: edge-tts + fastapi + uvicorn. No native deps. - README: comparison table vs MeloTTS, setup, caveat that edge-tts is an unofficial wrapper over the Edge "Read Aloud" endpoint. Default port 7861 so it coexists with melo-test/ (7860). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
151
edge-test/index.html
Normal file
151
edge-test/index.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Edge TTS 한국어 테스트</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; }
|
||||
select, input[type=number] {
|
||||
padding: 6px 8px; font-size: 15px; border-radius: 6px; border: 1px solid #888;
|
||||
}
|
||||
button {
|
||||
padding: 10px 22px; font-size: 16px; cursor: pointer;
|
||||
border-radius: 8px; border: 1px solid #888; background: #f5f5f5;
|
||||
}
|
||||
button.preset { padding: 6px 10px; font-size: 14px; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.meta { color: #666; font-size: 14px; }
|
||||
.err { color: #c00; }
|
||||
audio { width: 100%; margin-top: 16px; }
|
||||
hr { margin: 24px 0; border: none; border-top: 1px solid #ddd; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; }
|
||||
label { font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Microsoft Edge TTS — 한국어 테스트</h1>
|
||||
<p class="lead">
|
||||
Microsoft Azure Neural Voice 를 무료로(브라우저 Read Aloud 동일 엔드포인트) 사용.
|
||||
네이티브 빌드 의존성 0, 설치 30초.
|
||||
</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>
|
||||
<button class="preset" data-text="2026년 5월 26일 화요일, 서울 기온 23도, 습도 60퍼센트입니다.">숫자/날짜</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>보이스
|
||||
<select id="voice"></select>
|
||||
</label>
|
||||
<label>속도
|
||||
<select id="rate">
|
||||
<option value="-30%">느림 -30%</option>
|
||||
<option value="-10%">조금 느림 -10%</option>
|
||||
<option value="+0%" selected>표준</option>
|
||||
<option value="+10%">조금 빠름 +10%</option>
|
||||
<option value="+30%">빠름 +30%</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>피치
|
||||
<select id="pitch">
|
||||
<option value="-50Hz">낮음</option>
|
||||
<option value="+0Hz" selected>표준</option>
|
||||
<option value="+50Hz">높음</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button id="go">합성</button>
|
||||
<span class="meta" id="status">대기중</span>
|
||||
</div>
|
||||
|
||||
<audio id="audio" controls></audio>
|
||||
|
||||
<hr>
|
||||
<p class="meta">
|
||||
엔드포인트: <code>POST /tts</code>, 응답 <code>audio/mpeg</code>, 헤더 <code>X-Synth-Ms</code>=합성 ms.
|
||||
보이스 목록: <code>GET /voices</code>.
|
||||
</p>
|
||||
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
document.querySelectorAll(".preset").forEach(b => {
|
||||
b.onclick = () => { $("text").value = b.dataset.text; };
|
||||
});
|
||||
|
||||
async function loadVoices() {
|
||||
try {
|
||||
const res = await fetch("/voices");
|
||||
const json = await res.json();
|
||||
const sel = $("voice");
|
||||
sel.innerHTML = "";
|
||||
for (const v of json.voices) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.name;
|
||||
opt.textContent = `${v.name} (${v.label})`;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
} catch (e) {
|
||||
$("status").innerHTML = `<span class="err">보이스 목록 로딩 실패: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
loadVoices();
|
||||
|
||||
$("go").onclick = async () => {
|
||||
const text = $("text").value.trim();
|
||||
const voice = $("voice").value;
|
||||
const rate = $("rate").value;
|
||||
const pitch = $("pitch").value;
|
||||
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, voice, rate, pitch })
|
||||
});
|
||||
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>
|
||||
Reference in New Issue
Block a user