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:
@@ -19,6 +19,7 @@ _NAV_ITEMS = [
|
||||
("/", "主页", "home"),
|
||||
("/history", "历史", "history"),
|
||||
("/logs", "日志", "logs"),
|
||||
("/settings", "设置", "settings"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
182
app/views/settings_html.py
Normal 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); }
|
||||
"""
|
||||
|
||||
# 页面专属 JS(f-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,
|
||||
)
|
||||
Reference in New Issue
Block a user