Files
audio2text/app/views/history_html.py
zikai 00e2a95fb7 Initial commit: audio2text 双语字幕生成服务
- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码
- faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻)
- 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面
- 历史页文件名搜索;缓存定时清理(默认保留7天,可配置)
- 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
2026-07-06 06:54:19 +00:00

170 lines
5.7 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");
const created = new Date(task.created_at).toLocaleString();
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 progress = `<div class="mini-bar"><div class="mini-fill" style="width:${{task.progress}}%"></div></div>${{task.progress.toFixed(0)}}%`;
tr.innerHTML = `
<td class="muted">#${{task.id}}</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,
)