Files
audio2text/app/views/home_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

143 lines
4.6 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.

"""主页:上传入口 + 最近 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 `<a href="/api/tasks/${{taskId}}/subtitle?type=bilingual" class="dl">双语 SRT</a>`
+ ` <a href="/api/tasks/${{taskId}}/subtitle?type=en" class="dl">英</a>`
+ ` <a href="/api/tasks/${{taskId}}/subtitle?type=zh" class="dl">中</a>`;
}}
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 = `<div class="task-meta"><b>完成</b> · ${{downloadLinks(task.id)}}</div>`;
}} else if (task.status === "failed") {{
body = `<div class="task-meta fail-msg">${{escapeHtml(task.error || "未知错误")}}</div>`;
}} else {{
body = `<div class="bar"><div class="fill" style="width:${{task.progress}}%"></div><span class="pct">${{task.progress.toFixed(0)}}%</span></div>`;
}}
return `
<div class="task-head">
<span class="fname">#${{task.id}} ${{escapeHtml(task.filename)}}</span>
<span class="fstate ${{stateClass}}">${{label}}</span>
</div>
${{body}}
<div class="task-time">${{created}}</div>`;
}}
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 = """
<h1>音频转字幕</h1>
<p class="sub">上传视频或音频文件,自动生成双语(英/中SRT 字幕。支持大文件分片上传与断点续传。</p>
<div id="drop" class="drop">
<p class="drop-hint">把文件拖到这里,或</p>
<label class="btn">选择文件<input id="file-input" type="file" multiple hidden></label>
</div>
<h2>最近任务</h2>
<div id="tasks" class="tasks"></div>
<div id="empty" class="empty" style="display:none">暂无任务,上传文件后这里会显示进度。</div>
"""
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,
)