Files
audio2text/app/views/history_html.py
audio2text dev 73110848f4 feat: 调度器+并发管线+GPU优化+日志分级+前端修复
- scheduler: ffmpeg 异步线程 + GPU 串行调度 + 模型复用(2N→2 次加载)
- pipeline: 阶段拆分(extract/asr/translate),中间数据存 Task 字段
- translate_service: 长度排序批处理,padding 浪费减少 91%
- model_manager: ASR/翻译不共驻,BatchedInferencePipeline 批量解码
- 日志分级: INFO=任务流转里程碑,DEBUG=进度详情;默认 INFO
- 前端: 日志最新在上+滚动感知+退避轮询;24h 时间;上传中状态显示
- /health: 返回完整 Whisper/NLLB 配置
- upload_service: 单事务 complete + 扩展名白名单
- task_router: 合并 UploadSession 虚拟任务到列表
- Dockerfile: CPU/GPU 独立构建链,deps 缓存稳定
- prefetch_models: 安装时预下载模型权重
2026-07-06 21:59:59 +08:00

177 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""历史任务页:分页表格展示所有任务,可按文件名搜索、下载完成的字幕。
共享 _shared.py 的 BASE_CSS / SHARED_JS。
页面专属:搜索框(防抖)、分页表格、手动刷新(不自动轮询)。
"""
from __future__ import annotations
from ._shared import render_page
PAGE_SIZE = 50
SEARCH_DEBOUNCE_MS = 350
_PAGE_JS = f"""
const PAGE_SIZE = {PAGE_SIZE};
const SEARCH_DEBOUNCE_MS = {SEARCH_DEBOUNCE_MS};
let currentOffset = 0;
let total = 0;
let searchQuery = "";
let debounceTimer = null;
const tbody = document.getElementById("task-body");
const infoEl = document.getElementById("info");
const emptyEl = document.getElementById("empty");
const paginationEl = document.getElementById("pagination");
const searchInput = document.getElementById("search");
document.getElementById("refresh-btn").addEventListener("click", () => load(currentOffset));
// 搜索:输入防抖,改动后回到第一页
searchInput.addEventListener("input", () => {{
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {{
searchQuery = searchInput.value.trim();
load(0);
}}, SEARCH_DEBOUNCE_MS);
}});
async function load(offset) {{
currentOffset = offset;
try {{
const params = new URLSearchParams({{
limit: PAGE_SIZE, offset: offset
}});
if (searchQuery) params.set("q", searchQuery);
const r = await fetch("/api/tasks?" + params.toString());
if (!r.ok) {{ infoEl.textContent = "HTTP " + r.status; return; }}
const data = await r.json();
total = data.total;
renderTable(data.tasks);
renderPagination();
const end = Math.min(offset + data.tasks.length, total);
const prefix = searchQuery ? `搜索“${{escapeHtml(searchQuery)}}”匹配 ` : "";
infoEl.textContent = total === 0
? (searchQuery ? "无匹配任务" : "暂无任务")
: `${{prefix}}${{total}} 条,显示 ${{offset + 1}}${{end}}`;
}} catch (e) {{
infoEl.textContent = "加载失败";
}}
}}
function renderTable(tasks) {{
tbody.innerHTML = "";
if (tasks.length === 0) {{ emptyEl.style.display = "block"; return; }}
emptyEl.style.display = "none";
for (const task of tasks) {{
const tr = document.createElement("tr");
const label = STATUS_LABEL[task.status] || task.status;
const stateClass = "st-" + (task.status === "done" ? "done" : task.status === "failed" ? "fail" : "running");
let created;
try {{ created = fmtDateTime24(new Date(task.created_at + "Z")); }}
catch (e) {{ created = task.created_at; }}
let idDisplay = task.is_upload ? "" : "#" + task.id;
let action;
if (task.status === "done") {{
action = `<a href="/api/tasks/${{task.id}}/subtitle?type=bilingual" class="dl">双语</a>`
+ `<a href="/api/tasks/${{task.id}}/subtitle?type=en" class="dl">英</a>`
+ `<a href="/api/tasks/${{task.id}}/subtitle?type=zh" class="dl">中</a>`;
}} else if (task.status === "failed") {{
action = `<span class="err-tip" title="${{escapeHtml(task.error || "")}}">查看错误</span>`;
}} else {{
action = `<span class="muted">—</span>`;
}}
let progress;
if (task.status === "done") progress = "100%";
else if (task.status === "failed") progress = "";
else {{
const pct = (task.progress == null) ? 0 : task.progress;
progress = `<div class="mini-bar"><div class="mini-fill" style="width:${{pct}}%"></div></div>${{pct.toFixed(0)}}%`;
}}
tr.innerHTML = `
<td class="muted">${{idDisplay}}</td>
<td>${{escapeHtml(task.filename)}}</td>
<td><span class="status-tag ${{stateClass}}">${{label}}</span></td>
<td>${{progress}}</td>
<td class="task-time">${{created}}</td>
<td>${{action}}</td>`;
tbody.appendChild(tr);
}}
}}
function renderPagination() {{
const pages = Math.ceil(total / PAGE_SIZE);
const currentPage = Math.floor(currentOffset / PAGE_SIZE) + 1;
if (pages <= 1) {{ paginationEl.innerHTML = ""; return; }}
let html = "";
if (currentPage > 1)
html += `<button class="page-btn" onclick="load(${{(currentPage - 2) * PAGE_SIZE}})">上一页</button> `;
html += `<span class="page-info">第 ${{currentPage}} / ${{pages}} 页</span>`;
if (currentPage < pages)
html += ` <button class="page-btn" onclick="load(${{currentPage * PAGE_SIZE}})">下一页</button>`;
paginationEl.innerHTML = html;
}}
load(0);
"""
_PAGE_CSS = """
.search-input {
flex: 1; min-width: 180px; max-width: 360px;
padding: 0.42em 0.7em; border: 1px solid var(--border); border-radius: 6px;
background: var(--card-bg); color: var(--fg); font-size: 0.9em;
transition: border-color 0.15s, box-shadow 0.15s;
}
.search-input:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
/* 搜索框主导工具栏,刷新/信息靠右不收缩 */
.toolbar { justify-content: flex-start; }
.toolbar .btn-sm, .toolbar .info { flex-shrink: 0; }
"""
_BODY = """
<h1>历史任务</h1>
<p class="sub">所有转写任务记录。可按文件名搜索,完成任务可下载字幕,进行中显示进度。</p>
<div class="toolbar">
<input id="search" class="search-input" type="search" placeholder="按文件名搜索…" autocomplete="off">
<button id="refresh-btn" class="btn-sm">刷新</button>
<span id="info" class="info"></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>ID</th>
<th>文件名</th>
<th>状态</th>
<th>进度</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="task-body"></tbody>
</table>
</div>
<div id="pagination" class="pagination"></div>
<div id="empty" class="empty" style="display:none">暂无任务。</div>
"""
def render() -> str:
return render_page(
title="历史任务 — audio2text",
nav_active="history",
body=_BODY,
page_js=_PAGE_JS,
page_css=_PAGE_CSS,
)