"""主页:上传入口 + 最近 10 个任务的实时进度。 共享 _shared.py 的 BASE_CSS / SHARED_JS / 上传协议 JS。 页面专属:refreshList(拉取最近任务)、makeTaskCard、pollTask(轮询进行中任务)。 """ from __future__ import annotations from ._shared import POLL_INTERVAL, render_page, render_upload_js RECENT_LIMIT = 10 LIST_REFRESH_INTERVAL = 5000 _PAGE_JS = f""" const POLL_INTERVAL = {POLL_INTERVAL}; const LIST_REFRESH_INTERVAL = {LIST_REFRESH_INTERVAL}; const RECENT_LIMIT = {RECENT_LIMIT}; const tasksEl = document.getElementById("tasks"); const emptyEl = document.getElementById("empty"); let pollingIds = new Set(); // 初始化上传区 initUpload(document.getElementById("drop"), document.getElementById("file-input"), tasksEl); // ==================== 最近任务列表 ==================== async function refreshList() {{ try {{ const r = await fetch("/api/tasks?limit=" + RECENT_LIMIT); if (!r.ok) return; const data = await r.json(); renderTasks(data.tasks); }} catch (e) {{}} }} function renderTasks(tasks) {{ tasksEl.querySelectorAll(".server-task").forEach(el => el.remove()); const hasUploadCards = tasksEl.querySelector(".upload-task") !== null; emptyEl.style.display = (tasks.length === 0 && !hasUploadCards) ? "block" : "none"; for (const task of tasks) {{ tasksEl.appendChild(makeTaskCard(task)); }} // 对进行中的任务启动轮询 for (const task of tasks) {{ if (ACTIVE_STATES.includes(task.status) && !pollingIds.has(task.id)) {{ pollingIds.add(task.id); pollTask(task.id); }} }} }} function downloadLinks(taskId) {{ return `双语 SRT` + ` ` + ` `; }} function makeTaskCard(task) {{ const el = document.createElement("div"); el.className = "card task server-task"; el.dataset.taskId = task.id; el.innerHTML = renderTaskInner(task); return el; }} function renderTaskInner(task) {{ const label = STATUS_LABEL[task.status] || task.status; const stateClass = task.status === "done" ? "state-done" : task.status === "failed" ? "state-fail" : "state-running"; const created = new Date(task.created_at).toLocaleString(); let body; if (task.status === "done") {{ body = `
完成 · ${{downloadLinks(task.id)}}
`; }} else if (task.status === "failed") {{ body = `
${{escapeHtml(task.error || "未知错误")}}
`; }} else {{ body = `
${{task.progress.toFixed(0)}}%
`; }} return `
#${{task.id}} ${{escapeHtml(task.filename)}} ${{label}}
${{body}}
${{created}}
`; }} async function pollTask(taskId) {{ const tick = async () => {{ try {{ const r = await fetch("/api/tasks/" + taskId); if (!r.ok) {{ pollingIds.delete(taskId); return; }} const task = await r.json(); const card = tasksEl.querySelector('.server-task[data-task-id="' + taskId + '"]'); if (!card) {{ pollingIds.delete(taskId); return; }} card.innerHTML = renderTaskInner(task); if (task.status === "done" || task.status === "failed") {{ pollingIds.delete(taskId); return; }} setTimeout(tick, POLL_INTERVAL); }} catch (e) {{ setTimeout(tick, POLL_INTERVAL); }} }}; tick(); }} // 启动 refreshList(); setInterval(refreshList, LIST_REFRESH_INTERVAL); """ _BODY = """

音频转字幕

上传视频或音频文件,自动生成双语(英/中)SRT 字幕。支持大文件分片上传与断点续传。

把文件拖到这里,或

最近任务

""" def render() -> str: # 上传完成后刷新任务列表,让新任务出现在卡片中 upload_js = render_upload_js(on_complete="refreshList();") return render_page( title="audio2text — 音频转字幕", nav_active="home", body=_BODY, page_js=upload_js + _PAGE_JS, )