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

186 lines
6.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));
}}
// 对进行中的任务启动轮询(上传会话用 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 `<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;
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 = `<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 {{
const pct = (task.progress == null) ? 0 : task.progress;
const sizeInfo = task.size_bytes ? ` · ${{fmtBytes(task.size_bytes)}}` : "";
body = `<div class="bar"><div class="fill" style="width:${{pct}}%"></div><span class="pct">${{pct.toFixed(0)}}%${{sizeInfo}}</span></div>`;
}}
return `
<div class="task-head">
<span class="fname">${{task.is_upload ? "" : "#" + task.id + " "}}${{escapeHtml(task.filename)}}</span>
<span class="fstate ${{stateClass}}">${{label}}</span>
</div>
${{body}}
<div class="task-time">${{created}}</div>`;
}}
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 = """
<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,
)