"""主页:上传入口 + 最近 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)); }} // 对进行中的任务启动轮询(上传会话用 upload_id 去重,Task 用 id 去重) for (const task of tasks) {{ if (!ACTIVE_STATES.includes(task.status)) continue; const pollKey = task.is_upload ? task.upload_id : String(task.id); if (pollingIds.has(pollKey)) continue; pollingIds.add(pollKey); pollTask(task); }} }} function downloadLinks(taskId) {{ return `双语 SRT` + ` ` + ` `; }} function makeTaskCard(task) {{ const el = document.createElement("div"); el.className = "card task server-task"; el.dataset.taskId = task.id; if (task.is_upload) el.dataset.uploadId = task.upload_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"; // 上传中:created_at 可能是 naive UTC,前端按 UTC 解析 let created; try {{ created = fmtDateTime24(new Date(task.created_at + "Z")); }} catch (e) {{ created = task.created_at; }} let body; if (task.status === "done") {{ body = `
完成 · ${{downloadLinks(task.id)}}
`; }} else if (task.status === "failed") {{ body = `
${{escapeHtml(task.error || "未知错误")}}
`; }} else {{ const pct = (task.progress == null) ? 0 : task.progress; const sizeInfo = task.size_bytes ? ` · ${{fmtBytes(task.size_bytes)}}` : ""; body = `
${{pct.toFixed(0)}}%${{sizeInfo}}
`; }} return `
${{task.is_upload ? "" : "#" + task.id + " "}}${{escapeHtml(task.filename)}} ${{label}}
${{body}}
${{created}}
`; }} async function pollTask(task) {{ // 上传会话:轮询 upload status 接口;Task:轮询 task 接口 const isUpload = task.is_upload === true && task.upload_id; const url = isUpload ? "/api/tasks/chunk-uploads/" + task.upload_id + "/status" : "/api/tasks/" + task.id; let backoff = POLL_INTERVAL; const tick = async () => {{ try {{ const r = await fetch(url); if (!r.ok) {{ pollingIds.delete(task.id); pollingIds.delete(task.upload_id); return; }} const data = await r.json(); if (isUpload) {{ // 上传会话:complete 后 task_id 出现,切换为 Task 轮询 if (data.completed && data.task_id) {{ pollingIds.delete(task.upload_id); pollingIds.add(data.task_id); pollTask({{ id: data.task_id, is_upload: false }}); // 刷新列表让新 Task 卡片出现 refreshList(); return; }} // 更新上传进度 const card = tasksEl.querySelector('.server-task[data-upload-id="' + task.upload_id + '"]'); if (!card) {{ pollingIds.delete(task.upload_id); return; }} const uploaded = (data.uploaded_chunks || []).length; const total = data.total_chunks || 1; const pct = Math.min(4, uploaded / total * 4); const fakeTask = {{ status: "uploading", progress: pct, is_upload: true, upload_id: task.upload_id, filename: data.filename, size_bytes: data.size_bytes, created_at: task.created_at, }}; card.innerHTML = renderTaskInner(fakeTask); backoff = POLL_INTERVAL; setTimeout(tick, backoff); }} else {{ // Task 轮询 const card = tasksEl.querySelector('.server-task[data-task-id="' + task.id + '"]'); if (!card) {{ pollingIds.delete(task.id); return; }} card.innerHTML = renderTaskInner(data); if (data.status === "done" || data.status === "failed") {{ pollingIds.delete(task.id); return; }} backoff = POLL_INTERVAL; setTimeout(tick, backoff); }} }} catch (e) {{ // 网络错误:指数退避,上限 30s backoff = Math.min(backoff * 1.6, 30000); setTimeout(tick, backoff); }} }}; 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, )