Initial commit: audio2text 双语字幕生成服务
- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码 - faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻) - 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面 - 历史页文件名搜索;缓存定时清理(默认保留7天,可配置) - 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
This commit is contained in:
142
app/views/home_html.py
Normal file
142
app/views/home_html.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""主页:上传入口 + 最近 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,
|
||||
)
|
||||
Reference in New Issue
Block a user