FastAPI backend, web UI, CosyVoice3/F5-TTS setup scripts, and handoff docs for GPU PC continuation. Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
2.9 KiB
JavaScript
102 lines
2.9 KiB
JavaScript
const $ = (id) => document.getElementById(id);
|
|
|
|
async function fetchHealth() {
|
|
try {
|
|
const res = await fetch("/api/health");
|
|
const data = await res.json();
|
|
$("healthInfo").textContent = `모델: ${data.model} · 샘플 ${data.samples_count}개`;
|
|
} catch {
|
|
$("healthInfo").textContent = "API 서버에 연결할 수 없습니다.";
|
|
}
|
|
}
|
|
|
|
async function loadSamples() {
|
|
const select = $("sampleSelect");
|
|
try {
|
|
const res = await fetch("/api/voice-samples");
|
|
const data = await res.json();
|
|
for (const s of data.samples) {
|
|
const opt = document.createElement("option");
|
|
opt.value = s.path;
|
|
opt.textContent = `${s.label}/${s.id}${s.has_transcript ? "" : " (대본 없음)"}`;
|
|
select.appendChild(opt);
|
|
}
|
|
} catch (e) {
|
|
console.warn("samples load failed", e);
|
|
}
|
|
}
|
|
|
|
async function uploadIfNeeded() {
|
|
const fileInput = $("fileUpload");
|
|
if (!fileInput.files?.length) return null;
|
|
|
|
const form = new FormData();
|
|
form.append("file", fileInput.files[0]);
|
|
const refText = $("refText").value.trim();
|
|
if (refText) form.append("ref_text", refText);
|
|
|
|
const res = await fetch("/api/voice-sample", { method: "POST", body: form });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.detail || "업로드 실패");
|
|
}
|
|
const data = await res.json();
|
|
return data.path;
|
|
}
|
|
|
|
$("generateBtn").addEventListener("click", async () => {
|
|
const text = $("text").value.trim();
|
|
if (!text) {
|
|
$("status").textContent = "텍스트를 입력하세요.";
|
|
return;
|
|
}
|
|
|
|
const btn = $("generateBtn");
|
|
btn.disabled = true;
|
|
$("status").textContent = "음성 생성 중… (GPU 추론은 수십 초 걸릴 수 있습니다)";
|
|
$("resultSection").hidden = true;
|
|
|
|
try {
|
|
let refAudio = $("sampleSelect").value || null;
|
|
const uploaded = await uploadIfNeeded();
|
|
if (uploaded) refAudio = uploaded;
|
|
|
|
const body = {
|
|
text,
|
|
preprocess: true,
|
|
ref_text: $("refText").value.trim() || null,
|
|
ref_audio: refAudio,
|
|
};
|
|
|
|
const res = await fetch("/api/tts", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
const detail =
|
|
typeof err.detail === "string"
|
|
? err.detail
|
|
: JSON.stringify(err.detail || err);
|
|
throw new Error(detail || res.statusText);
|
|
}
|
|
|
|
const data = await res.json();
|
|
const url = data.audio_url + "?t=" + Date.now();
|
|
$("player").src = url;
|
|
$("downloadLink").href = url;
|
|
$("downloadLink").download = `${data.job_id}.wav`;
|
|
$("resultSection").hidden = false;
|
|
$("status").textContent = `완료 (모델: ${data.model}, job: ${data.job_id})`;
|
|
} catch (e) {
|
|
$("status").textContent = `오류: ${e.message}`;
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
|
|
fetchHealth();
|
|
loadSamples();
|