const $ = (id) => document.getElementById(id); const els = { text: $("text"), charCount: $("charCount"), engine: $("engine"), engineInfo: $("engineInfo"), language: $("language"), speaker: $("speaker"), speed: $("speed"), speedVal: $("speedVal"), pitch: $("pitch"), pitchVal: $("pitchVal"), generate: $("generate"), btnText: document.querySelector(".btn-text"), spinner: document.querySelector(".spinner"), result: $("result"), audio: $("audio"), download: $("download"), error: $("error"), deviceChip: $("deviceChip"), }; let engines = []; let lastUrl = null; function currentEngine() { return engines.find((e) => e.id === els.engine.value); } function currentLanguage() { const eng = currentEngine(); if (!eng) return null; return eng.languages.find((l) => l.code === els.language.value); } function updateCharCount() { els.charCount.textContent = els.text.value.length; } function populateSpeakers() { const lang = currentLanguage(); els.speaker.innerHTML = ""; if (!lang) return; lang.speakers.forEach((s) => { const opt = document.createElement("option"); opt.value = s.id; opt.textContent = s.label; els.speaker.appendChild(opt); }); } function populateLanguages() { const eng = currentEngine(); els.language.innerHTML = ""; if (!eng) return; eng.languages.forEach((l) => { const opt = document.createElement("option"); opt.value = l.code; opt.textContent = l.label; els.language.appendChild(opt); }); populateSpeakers(); updateEngineInfo(); } function updateEngineInfo() { const eng = currentEngine(); if (!eng) { els.engineInfo.textContent = ""; return; } const nc = /NC|BY-NC|비상업/i.test(eng.license || ""); const gpu = eng.uses_gpu ? "GPU" : 'CPU'; const supports = eng.supports || {}; const ctrl = [ supports.speed ? "속도" : null, supports.pitch ? "피치" : null, ] .filter(Boolean) .join(" · "); els.engineInfo.innerHTML = `${eng.notes || ""} ` + `${eng.license || "?"} ` + `· ${gpu}` + (ctrl ? ` · 조절: ${ctrl}` : ""); // 지원하지 않는 컨트롤 비활성화 els.speed.disabled = supports.speed === false; els.pitch.disabled = supports.pitch === false; } async function loadEngines() { try { const res = await fetch("/api/engines"); const data = await res.json(); engines = data.engines || []; els.engine.innerHTML = ""; engines.forEach((e) => { const opt = document.createElement("option"); opt.value = e.id; opt.textContent = e.label; els.engine.appendChild(opt); }); if (data.default) els.engine.value = data.default; populateLanguages(); } catch (e) { showError("엔진 목록을 불러오지 못했습니다."); } } async function loadHealth() { try { const res = await fetch("/api/health"); const d = await res.json(); if (d.cuda) { els.deviceChip.textContent = `GPU 가속: ${d.gpu}`; els.deviceChip.classList.add("gpu"); } else { els.deviceChip.textContent = "CPU 모드로 동작 중"; } } catch (e) { els.deviceChip.textContent = ""; } } function showError(msg) { els.error.textContent = msg; els.error.hidden = false; } function clearError() { els.error.hidden = true; } function setLoading(on) { els.generate.disabled = on; els.spinner.hidden = !on; els.btnText.textContent = on ? "생성 중…" : "음성 생성"; } async function generate() { clearError(); const text = els.text.value.trim(); if (!text) { showError("읽을 내용을 입력해 주세요."); return; } setLoading(true); try { const res = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, engine: els.engine.value, language: els.language.value, speaker: els.speaker.value, speed: parseFloat(els.speed.value), pitch: parseFloat(els.pitch.value), }), }); if (!res.ok) { let detail = "음성 생성에 실패했습니다."; try { const err = await res.json(); if (err.detail) detail = err.detail; } catch (_) {} throw new Error(detail); } const blob = await res.blob(); if (lastUrl) URL.revokeObjectURL(lastUrl); lastUrl = URL.createObjectURL(blob); els.audio.src = lastUrl; els.download.href = lastUrl; els.result.hidden = false; els.audio.play().catch(() => {}); } catch (e) { showError(e.message || "오류가 발생했습니다."); } finally { setLoading(false); } } els.text.addEventListener("input", updateCharCount); els.engine.addEventListener("change", populateLanguages); els.language.addEventListener("change", populateSpeakers); els.speed.addEventListener("input", () => { els.speedVal.textContent = parseFloat(els.speed.value).toFixed(2) + "x"; }); els.pitch.addEventListener("input", () => { const v = parseInt(els.pitch.value, 10); els.pitchVal.textContent = (v > 0 ? "+" + v : v) + " 반음"; }); els.generate.addEventListener("click", generate); updateCharCount(); loadEngines(); loadHealth();