refactor: 다중 엔진 레지스트리 구조로 전환 + 프론트 엔진 선택 UI
- app/engines: BaseEngine 인터페이스, MeloEngine(인프로세스), 레지스트리 - /api/engines 추가, /api/tts 에 engine 파라미터 - 프론트: 엔진 → 언어 → 목소리 선택, 라이선스/GPU/조절 지원 표기 - MeloTTS 동작 불변
This commit is contained in:
@@ -3,6 +3,8 @@ const $ = (id) => document.getElementById(id);
|
||||
const els = {
|
||||
text: $("text"),
|
||||
charCount: $("charCount"),
|
||||
engine: $("engine"),
|
||||
engineInfo: $("engineInfo"),
|
||||
language: $("language"),
|
||||
speaker: $("speaker"),
|
||||
speed: $("speed"),
|
||||
@@ -19,16 +21,24 @@ const els = {
|
||||
deviceChip: $("deviceChip"),
|
||||
};
|
||||
|
||||
let voices = [];
|
||||
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 code = els.language.value;
|
||||
const lang = voices.find((v) => v.code === code);
|
||||
const lang = currentLanguage();
|
||||
els.speaker.innerHTML = "";
|
||||
if (!lang) return;
|
||||
lang.speakers.forEach((s) => {
|
||||
@@ -39,21 +49,64 @@ function populateSpeakers() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadVoices() {
|
||||
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"
|
||||
: '<span class="cpu">CPU</span>';
|
||||
const supports = eng.supports || {};
|
||||
const ctrl = [
|
||||
supports.speed ? "속도" : null,
|
||||
supports.pitch ? "피치" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
els.engineInfo.innerHTML =
|
||||
`${eng.notes || ""} ` +
|
||||
`<span class="lic${nc ? " nc" : ""}">${eng.license || "?"}</span> ` +
|
||||
`· ${gpu}` +
|
||||
(ctrl ? ` · 조절: ${ctrl}` : "");
|
||||
|
||||
// 지원하지 않는 컨트롤 비활성화
|
||||
els.speed.disabled = supports.speed === false;
|
||||
els.pitch.disabled = supports.pitch === false;
|
||||
}
|
||||
|
||||
async function loadEngines() {
|
||||
try {
|
||||
const res = await fetch("/api/voices");
|
||||
const res = await fetch("/api/engines");
|
||||
const data = await res.json();
|
||||
voices = data.languages || [];
|
||||
els.language.innerHTML = "";
|
||||
voices.forEach((v) => {
|
||||
engines = data.engines || [];
|
||||
els.engine.innerHTML = "";
|
||||
engines.forEach((e) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.code;
|
||||
opt.textContent = v.label;
|
||||
els.language.appendChild(opt);
|
||||
opt.value = e.id;
|
||||
opt.textContent = e.label;
|
||||
els.engine.appendChild(opt);
|
||||
});
|
||||
populateSpeakers();
|
||||
if (data.default) els.engine.value = data.default;
|
||||
populateLanguages();
|
||||
} catch (e) {
|
||||
showError("목소리 목록을 불러오지 못했습니다.");
|
||||
showError("엔진 목록을 불러오지 못했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +153,7 @@ async function generate() {
|
||||
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),
|
||||
@@ -129,6 +183,7 @@ async function generate() {
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -140,5 +195,5 @@ els.pitch.addEventListener("input", () => {
|
||||
els.generate.addEventListener("click", generate);
|
||||
|
||||
updateCharCount();
|
||||
loadVoices();
|
||||
loadEngines();
|
||||
loadHealth();
|
||||
|
||||
Reference in New Issue
Block a user