feat: 设置页调 batch_size + 删除任务 + /docs 去认证 + 离线模式

设置页(/settings):
- 新增 Setting DB 模型(键值存储,持久化运行时覆盖)
- config.py 加 _apply_overrides:get_settings() 合并 DB 覆盖值
  (_applying_overrides 标志防递归:DB 初始化回调 get_settings 时不重入)
- save_setting() 写 DB + 清 lru_cache,后续任务读到新值
- settings_router.py: GET/PUT /api/settings
- settings_html.py: 设置页表单(batch_size/beam_size/sort_by_length)
  + 设备只读信息 + 保存后 reload 确认
- 验证:改 asr_batch_size=16 beam_size=1 -> 任务 ASR 日志确认生效
- 验证:重启容器后设置从 DB 恢复(持久化)

删除任务:
- task_router.py: DELETE /api/tasks/{id},仅 done/failed 可删
  删字幕/音频/视频产物 + UploadSession + Task DB 记录
- home_html.py + history_html.py: done/failed 任务显示删除按钮
  + confirm 确认 + 调用 DELETE API + 刷新列表
- 验证:删除 task 15 成功,删除进行中 task 返回 409

/docs 去认证:
- 移除 require_docs_auth 依赖,/docs /redoc /openapi.json 直接公开

离线模式:
- Dockerfile dev/final 加 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
  (模型已缓存在 ./models volume,无需联网验证)
- 验证:离线模式完整跑通 ASR+翻译(task 15 done)
This commit is contained in:
audio2text dev
2026-07-11 11:33:34 +08:00
parent 78b87bfb24
commit 7635e5e766
12 changed files with 523 additions and 15 deletions

View File

@@ -19,6 +19,7 @@ _NAV_ITEMS = [
("/", "主页", "home"),
("/history", "历史", "history"),
("/logs", "日志", "logs"),
("/settings", "设置", "settings"),
]

View File

@@ -82,7 +82,12 @@ function renderTable(tasks) {{
}} else if (task.status === "failed") {{
action = `<span class="err-tip" title="${{escapeHtml(task.error || "")}}">查看错误</span>`;
}} else {{
action = `<span class="muted"></span>`;
action = `<span class="muted">-</span>`;
}}
// done/failed 且非上传中:加删除按钮
const canDelete = (task.status === "done" || task.status === "failed") && !task.is_upload;
if (canDelete) {{
action += ` <button class="btn-sm" onclick="deleteTask(${{task.id}})">删除</button>`;
}}
let progress;
@@ -117,6 +122,17 @@ function renderPagination() {{
paginationEl.innerHTML = html;
}}
async function deleteTask(taskId) {{
if (!confirm("确认删除任务 #" + taskId + "?字幕和中间文件将被清除。")) return;
try {{
const r = await fetch("/api/tasks/" + taskId, {{ method: "DELETE" }});
if (!r.ok) {{ alert("删除失败:" + await r.text()); return; }}
load(currentOffset);
}} catch (e) {{
alert("删除失败:" + e);
}}
}}
load(0);
"""

View File

@@ -77,6 +77,12 @@ function renderTaskInner(task) {{
try {{ created = fmtDateTime24(new Date(task.created_at + "Z")); }}
catch (e) {{ created = task.created_at; }}
// 删除按钮:仅 done/failed 且非上传中任务显示
const canDelete = (task.status === "done" || task.status === "failed") && !task.is_upload;
const delBtn = canDelete
? `<button class="btn-sm del-btn" onclick="deleteTask(${{task.id}}, this)">删除</button>`
: "";
let body;
if (task.status === "done") {{
body = `<div class="task-meta"><b>完成</b> · ${{downloadLinks(task.id)}}</div>`;
@@ -91,11 +97,30 @@ function renderTaskInner(task) {{
<div class="task-head">
<span class="fname">${{task.is_upload ? "" : "#" + task.id + " "}}${{escapeHtml(task.filename)}}</span>
<span class="fstate ${{stateClass}}">${{label}}</span>
${{delBtn}}
</div>
${{body}}
<div class="task-time">${{created}}</div>`;
}}
async function deleteTask(taskId, btn) {{
if (!confirm("确认删除任务 #" + taskId + "?字幕和中间文件将被清除。")) return;
btn.disabled = true;
try {{
const r = await fetch("/api/tasks/" + taskId, {{ method: "DELETE" }});
if (!r.ok) {{
const err = await r.text();
alert("删除失败:" + err);
btn.disabled = false;
return;
}}
refreshList();
}} catch (e) {{
alert("删除失败:" + e);
btn.disabled = false;
}}
}}
async function pollTask(task) {{
// 上传会话:轮询 upload status 接口Task轮询 task 接口
const isUpload = task.is_upload === true && task.upload_id;

182
app/views/settings_html.py Normal file
View File

@@ -0,0 +1,182 @@
"""设置页:调整批处理大小等运行时参数,保存后对后续任务生效。
页面结构表单展示当前生效值GET /api/settings用户修改后点保存PUT /api/settings
保存到 DB 并清配置缓存,后续任务读到新值。已在跑的任务不受影响。
"""
from __future__ import annotations
from ._shared import render_page
# 页面专属 CSS
_PAGE_CSS = """
.field-group { margin: 1em 0; }
.field-group h2 { margin-bottom: 0.3em; }
.field-row {
display: flex; align-items: center; gap: 0.8em;
padding: 0.6em 0; border-bottom: 1px solid var(--border);
}
.field-row:last-child { border-bottom: none; }
.field-label { font-weight: 600; min-width: 200px; }
.field-desc { color: var(--muted); font-size: 0.82em; flex: 1; }
.field-input { width: 80px; }
.field-input[type="number"] {
padding: 0.3em 0.5em; border: 1px solid var(--border); border-radius: 4px;
background: var(--card-bg); color: var(--fg); font-size: 0.92em; text-align: center;
}
.field-input[type="checkbox"] { width: auto; transform: scale(1.3); }
.readonly-info {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.5em 1.5em;
margin: 1em 0; padding: 0.8em 1em; background: var(--card-bg);
border: 1px solid var(--border); border-radius: 8px; font-size: 0.88em;
}
.readonly-info .kv { display: flex; gap: 0.5em; }
.readonly-info .k { color: var(--muted); min-width: 90px; }
.save-bar { display: flex; align-items: center; gap: 1em; margin-top: 1.2em; }
.save-msg { font-size: 0.88em; }
.save-msg.ok { color: var(--success); }
.save-msg.err { color: var(--error); }
"""
# 页面专属 JSf-string花括号需 {{ }}
_PAGE_JS = """
let originalValues = {};
async function loadSettings() {
try {
const r = await fetch("/api/settings");
if (!r.ok) throw new Error("HTTP " + r.status);
const s = await r.json();
document.getElementById("asr_batch_size").value = s.asr_batch_size;
document.getElementById("asr_beam_size").value = s.asr_beam_size;
document.getElementById("translation_batch_size").value = s.translation_batch_size;
document.getElementById("translation_sort_by_length").checked = s.translation_sort_by_length;
// 只读信息
document.getElementById("ro_asr_model").textContent = s.asr_model;
document.getElementById("ro_asr_device").textContent = s.asr_device;
document.getElementById("ro_asr_compute_type").textContent = s.asr_compute_type;
document.getElementById("ro_translation_model").textContent = s.translation_model;
document.getElementById("ro_translation_device").textContent = s.translation_device;
// 记录原始值用于检测是否有变更
originalValues = {
asr_batch_size: s.asr_batch_size,
asr_beam_size: s.asr_beam_size,
translation_batch_size: s.translation_batch_size,
translation_sort_by_length: s.translation_sort_by_length,
};
setMsg("", "");
} catch (e) {
setMsg("加载失败:" + escapeHtml(String(e.message || e)), "err");
}
}
function collectChanges() {
const body = {};
const cur = {
asr_batch_size: parseInt(document.getElementById("asr_batch_size").value, 10),
asr_beam_size: parseInt(document.getElementById("asr_beam_size").value, 10),
translation_batch_size: parseInt(document.getElementById("translation_batch_size").value, 10),
translation_sort_by_length: document.getElementById("translation_sort_by_length").checked,
};
for (const [k, v] of Object.entries(cur)) {
if (v !== originalValues[k]) body[k] = v;
}
return body;
}
async function saveSettings() {
const changes = collectChanges();
if (Object.keys(changes).length === 0) {
setMsg("没有变更", "");
return;
}
const btn = document.getElementById("save-btn");
btn.disabled = true;
btn.textContent = "保存中...";
setMsg("正在保存...", "");
try {
const r = await fetch("/api/settings", {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(changes),
});
if (!r.ok) throw new Error("HTTP " + r.status + " " + await r.text());
const resp = await r.json();
setMsg("已保存:" + Object.keys(resp.changed).join(", ") + "(对后续任务生效)", "ok");
await loadSettings(); // 重新加载确认
} catch (e) {
setMsg("保存失败:" + escapeHtml(String(e.message || e)), "err");
} finally {
btn.disabled = false;
btn.textContent = "保存设置";
}
}
function setMsg(text, cls) {
const el = document.getElementById("save-msg");
el.textContent = text;
el.className = "save-msg" + (cls ? " " + cls : "");
}
loadSettings();
"""
_BODY = """
<h1>设置</h1>
<p class="sub">调整批处理大小等运行时参数。保存后对<strong>后续任务</strong>生效,已在运行的任务不受影响。</p>
<div class="field-group">
<h2>语音识别ASR</h2>
<div class="field-row">
<span class="field-label">batch_size</span>
<input type="number" id="asr_batch_size" class="field-input" min="1" max="128" value="16">
<span class="field-desc">批量解码的音频块数。增大可拉长单次 GPU 解码、提升利用率,但显存占用增加</span>
</div>
<div class="field-row">
<span class="field-label">beam_size</span>
<input type="number" id="asr_beam_size" class="field-input" min="1" max="10" value="5">
<span class="field-desc">beam search 宽度。GPU turbo 建议 2加速、质量损失小CPU 建议 5</span>
</div>
</div>
<div class="field-group">
<h2>翻译NLLB</h2>
<div class="field-row">
<span class="field-label">batch_size</span>
<input type="number" id="translation_batch_size" class="field-input" min="1" max="256" value="32">
<span class="field-desc">翻译批量大小。显存独占时可用大 batch 填充 GPU</span>
</div>
<div class="field-row">
<span class="field-label">sort_by_length</span>
<input type="checkbox" id="translation_sort_by_length" class="field-input">
<span class="field-desc">按句子长度排序后分批,减少批内 padding 浪费GPU 收益大)</span>
</div>
</div>
<div class="save-bar">
<button class="btn" id="save-btn" onclick="saveSettings()">保存设置</button>
<span id="save-msg" class="save-msg"></span>
</div>
<div class="field-group">
<h2>设备信息(只读)</h2>
<div class="readonly-info">
<div class="kv"><span class="k">ASR 模型</span><span id="ro_asr_model"></span></div>
<div class="kv"><span class="k">ASR 设备</span><span id="ro_asr_device"></span></div>
<div class="kv"><span class="k">计算精度</span><span id="ro_asr_compute_type"></span></div>
<div class="kv"><span class="k">翻译模型</span><span id="ro_translation_model"></span></div>
<div class="kv"><span class="k">翻译设备</span><span id="ro_translation_device"></span></div>
</div>
</div>
"""
def render() -> str:
return render_page(
title="audio2text - 设置",
nav_active="settings",
body=_BODY,
page_js=_PAGE_JS,
page_css=_PAGE_CSS,
)