LXC-Frontend (FastAPI + HTML/JS): - Audio-Upload (MP3/WAV/M4A/MP4/OGG/FLAC, max. 500 MB) - SQLite Job-Store, BackgroundTask-Pipeline - Job-Liste mit Live-Status, Downloads (DOCX + JSON) - Mac-Health-Indicator im UI Mac-Worker (FastAPI): - /api/transcribe (lightning-whisper-mlx | faster-whisper | mock) - /api/summarize + /api/protocol via Ollama (llama3.1:8b) - /api/export/docx via python-docx Deploy: - systemd-Service, Nginx Reverse-Proxy - deploy/install.sh: idempotentes LXC-Setup Doku: README.md, lxc-frontend/README.md, mac-worker/README.md
126 lines
4.0 KiB
JavaScript
126 lines
4.0 KiB
JavaScript
const form = document.getElementById("upload-form");
|
|
const titleInput = document.getElementById("title");
|
|
const audioInput = document.getElementById("audio");
|
|
const submitBtn = document.getElementById("submit-btn");
|
|
const uploadProgress = document.getElementById("upload-progress");
|
|
const uploadBar = uploadProgress.querySelector(".bar-fill");
|
|
const uploadMsg = document.getElementById("upload-msg");
|
|
const jobsBody = document.getElementById("jobs-body");
|
|
const macStatus = document.getElementById("mac-status");
|
|
|
|
async function checkMac() {
|
|
try {
|
|
const r = await fetch("/api/mac/health");
|
|
const j = await r.json();
|
|
if (r.ok && j.reachable) {
|
|
macStatus.textContent = "Mac-Backend: online";
|
|
macStatus.classList.add("ok");
|
|
macStatus.classList.remove("err");
|
|
} else {
|
|
macStatus.textContent = "Mac-Backend: nicht erreichbar";
|
|
macStatus.classList.add("err");
|
|
macStatus.classList.remove("ok");
|
|
}
|
|
} catch {
|
|
macStatus.textContent = "Mac-Backend: Fehler";
|
|
macStatus.classList.add("err");
|
|
}
|
|
}
|
|
|
|
function fmtDate(s) {
|
|
const d = new Date(s);
|
|
return d.toLocaleString("de-DE", { dateStyle: "short", timeStyle: "medium" });
|
|
}
|
|
|
|
function row(job) {
|
|
const tr = document.createElement("tr");
|
|
const title = job.title || job.original_name;
|
|
const dl = (kind, label) => {
|
|
const a = document.createElement("a");
|
|
a.textContent = label;
|
|
a.href = `/api/jobs/${job.id}/download/${kind}`;
|
|
if (!job.has[kind]) a.classList.add("disabled");
|
|
return a;
|
|
};
|
|
const dlCell = document.createElement("td");
|
|
dlCell.className = "dl";
|
|
["docx", "protocol", "summary", "transcript"].forEach((k) =>
|
|
dlCell.appendChild(dl(k, k === "docx" ? "DOCX" : k.charAt(0).toUpperCase() + k.slice(1) + " (JSON)"))
|
|
);
|
|
tr.innerHTML = `
|
|
<td>#${job.id}</td>
|
|
<td>${title}<br><small class="muted">${job.original_name}</small></td>
|
|
<td><span class="status-pill status-${job.status}">${job.status}${job.error ? " — " + job.error : ""}</span></td>
|
|
<td><div class="bar"><div class="bar-fill" style="width:${job.progress}%"></div></div><small class="muted">${job.progress}%</small></td>
|
|
<td><small class="muted">${fmtDate(job.updated_at)}</small></td>
|
|
`;
|
|
tr.appendChild(dlCell);
|
|
return tr;
|
|
}
|
|
|
|
async function loadJobs() {
|
|
try {
|
|
const r = await fetch("/api/jobs");
|
|
const jobs = await r.json();
|
|
jobsBody.innerHTML = "";
|
|
if (!jobs.length) {
|
|
jobsBody.innerHTML = '<tr><td colspan="6" class="muted">Noch keine Jobs.</td></tr>';
|
|
return;
|
|
}
|
|
jobs.forEach((j) => jobsBody.appendChild(row(j)));
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
form.addEventListener("submit", (ev) => {
|
|
ev.preventDefault();
|
|
if (!audioInput.files.length) return;
|
|
const fd = new FormData();
|
|
fd.append("audio", audioInput.files[0]);
|
|
fd.append("title", titleInput.value);
|
|
|
|
submitBtn.disabled = true;
|
|
uploadProgress.classList.remove("hidden");
|
|
uploadBar.style.width = "0%";
|
|
uploadMsg.textContent = "";
|
|
uploadMsg.className = "msg";
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/api/jobs");
|
|
xhr.upload.onprogress = (e) => {
|
|
if (e.lengthComputable) {
|
|
const p = Math.round((e.loaded / e.total) * 100);
|
|
uploadBar.style.width = p + "%";
|
|
}
|
|
};
|
|
xhr.onload = () => {
|
|
submitBtn.disabled = false;
|
|
uploadProgress.classList.add("hidden");
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
const j = JSON.parse(xhr.responseText);
|
|
uploadMsg.textContent = `Job #${j.id} erstellt. Verarbeitung läuft …`;
|
|
uploadMsg.className = "msg ok";
|
|
form.reset();
|
|
loadJobs();
|
|
} else {
|
|
let err = "Upload fehlgeschlagen";
|
|
try { err = JSON.parse(xhr.responseText).detail || err; } catch {}
|
|
uploadMsg.textContent = err;
|
|
uploadMsg.className = "msg err";
|
|
}
|
|
};
|
|
xhr.onerror = () => {
|
|
submitBtn.disabled = false;
|
|
uploadProgress.classList.add("hidden");
|
|
uploadMsg.textContent = "Netzwerkfehler beim Upload";
|
|
uploadMsg.className = "msg err";
|
|
};
|
|
xhr.send(fd);
|
|
});
|
|
|
|
checkMac();
|
|
loadJobs();
|
|
setInterval(loadJobs, 3000);
|
|
setInterval(checkMac, 15000);
|