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

168 lines
6.3 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.

"""日志页:实时轮询 /api/logs按级别过滤可展开 traceback。
共享 _shared.py 的 BASE_CSS / SHARED_JS。
页面专属级别过滤按钮、自动刷新开关、清空、traceback 折叠。
"""
from __future__ import annotations
from ._shared import render_page
POLL_INTERVAL_LOGS = 2000
DEFAULT_TAIL = 200
_PAGE_JS = f"""
const POLL_INTERVAL = {POLL_INTERVAL_LOGS};
const DEFAULT_TAIL = {DEFAULT_TAIL};
const API = "/api/logs";
let currentLevel = "debug";
let autoRefresh = true;
let timer = null;
const logsEl = document.getElementById("logs");
const emptyEl = document.getElementById("empty");
const statusEl = document.getElementById("status");
const LEVEL_CLASS = {{
DEBUG: "st-running", INFO: "st-done", WARNING: "st-fail", ERROR: "st-fail",
CRITICAL: "st-fail"
}};
document.querySelectorAll(".filter").forEach(btn => {{
btn.addEventListener("click", () => {{
document.querySelectorAll(".filter").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentLevel = btn.dataset.level;
logsEl.innerHTML = "";
fetchLogs();
}});
}});
document.getElementById("autorefresh").addEventListener("change", e => {{
autoRefresh = e.target.checked;
if (autoRefresh) fetchLogs(); else if (timer) {{ clearTimeout(timer); timer = null; }}
}});
document.getElementById("clear-btn").addEventListener("click", async () => {{
if (!confirm("确定清空所有日志缓冲?")) return;
try {{
await fetch(API, {{ method: "DELETE" }});
logsEl.innerHTML = "";
statusEl.textContent = "已清空";
}} catch (e) {{ statusEl.textContent = "清空失败"; }}
}});
async function fetchLogs() {{
try {{
const r = await fetch(`${{API}}?level=${{currentLevel}}&tail=${{DEFAULT_TAIL}}`);
if (!r.ok) {{ statusEl.textContent = "HTTP " + r.status; scheduleNext(); return; }}
const data = await r.json();
renderLogs(data.logs);
statusEl.textContent = `${{data.count}} 条 · 更新 ${{new Date().toLocaleTimeString()}}`;
scheduleNext();
}} catch (e) {{
statusEl.textContent = "获取失败";
scheduleNext();
}}
}}
function renderLogs(logs) {{
if (!logs || logs.length === 0) {{
if (logsEl.children.length === 0) emptyEl.style.display = "block";
return;
}}
emptyEl.style.display = "none";
const existing = new Set(Array.from(logsEl.children).map(el => el.dataset.key));
const frag = document.createDocumentFragment();
for (const log of logs) {{
const key = `${{log.ts}}|${{log.level}}|${{log.msg}}`;
if (existing.has(key)) continue;
const row = document.createElement("div");
row.className = "log-row " + (LEVEL_CLASS[log.level] || "st-running");
row.dataset.key = key;
const hasTrace = !!log.traceback;
row.innerHTML = `
<span class="log-ts">${{escapeHtml(log.ts)}}</span>
<span class="status-tag ${{LEVEL_CLASS[log.level] || 'st-running'}}">${{escapeHtml(log.level)}}</span>
<span class="log-logger">${{escapeHtml(log.logger)}}</span>
<span class="log-msg">${{escapeHtml(log.msg)}}${{hasTrace ? ' <span class="trace-toggle">[traceback]</span>' : ''}}</span>`;
if (hasTrace) {{
const pre = document.createElement("pre");
pre.className = "trace";
pre.textContent = log.traceback;
pre.style.display = "none";
row.appendChild(pre);
row.querySelector(".trace-toggle").addEventListener("click", e => {{
e.stopPropagation();
pre.style.display = pre.style.display === "none" ? "block" : "none";
}});
}}
frag.appendChild(row);
}}
logsEl.appendChild(frag);
while (logsEl.children.length > DEFAULT_TAIL) logsEl.removeChild(logsEl.firstChild);
logsEl.scrollTop = logsEl.scrollHeight;
}}
function scheduleNext() {{
if (autoRefresh) timer = setTimeout(fetchLogs, POLL_INTERVAL);
}}
fetchLogs();
"""
_PAGE_CSS = """
.logs { border: 1px solid var(--border); border-radius: 10px; max-height: 70vh;
overflow-y: auto; background: var(--card-bg);
font-family: "SF Mono", "Cascadia Code", Consolas, monospace; font-size: 0.82em; }
.log-row { display: grid; grid-template-columns: 140px 70px 150px 1fr; gap: 0.5em;
padding: 0.25em 0.8em; border-bottom: 1px solid var(--border); align-items: start; }
.log-row:hover { background: var(--accent-light); }
.log-ts { color: var(--muted); white-space: nowrap; }
.log-logger { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.log-msg { word-break: break-all; white-space: pre-wrap; }
.trace-toggle { color: var(--error); cursor: pointer; font-weight: 600; font-size: 0.85em; }
.trace { grid-column: 1 / -1; margin: 0.3em 0 0.5em; padding: 0.6em; background: #1e1e1e;
color: #f44336; border-radius: 4px; font-size: 0.9em; overflow-x: auto; white-space: pre-wrap; }
.filters { display: flex; gap: 0.4em; }
.filter { padding: 0.3em 1em; border: 1px solid var(--border); border-radius: 14px;
background: transparent; cursor: pointer; font-size: 0.85em; color: var(--muted);
transition: background 0.15s, color 0.15s; }
.filter.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.filter:hover:not(.active) { background: var(--accent-light); }
.toggle { display: flex; align-items: center; gap: 0.3em; cursor: pointer; color: var(--muted); }
"""
_BODY = """
<h1>日志</h1>
<p class="sub">实时查看服务日志。debug=详细子步骤info=仅阶段转换error=完整错误。自动刷新每 2 秒。</p>
<div class="toolbar">
<div class="filters">
<button class="filter active" data-level="debug">全部 (DEBUG)</button>
<button class="filter" data-level="info">简略 (INFO)</button>
<button class="filter" data-level="warning">警告+</button>
<button class="filter" data-level="error">仅错误</button>
</div>
<div style="display:flex;align-items:center;gap:0.8em;">
<label class="toggle"><input id="autorefresh" type="checkbox" checked> 自动刷新</label>
<button id="clear-btn" class="btn-sm">清空</button>
<span id="status" class="info"></span>
</div>
</div>
<div id="logs" class="logs"></div>
<div id="empty" class="empty">暂无日志。</div>
"""
def render() -> str:
return render_page(
title="日志 — audio2text",
nav_active="logs",
body=_BODY,
page_js=_PAGE_JS,
page_css=_PAGE_CSS,
)