- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码 - faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻) - 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面 - 历史页文件名搜索;缓存定时清理(默认保留7天,可配置) - 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""内存日志缓冲:捕获所有 audio2text.* 日志到有界 deque,供 /logs 页面查询。
|
||
|
||
设计:
|
||
- MemoryLogHandler 挂到 `audio2text` logger,子 logger 的记录经传播自动汇入。
|
||
- deque(maxlen=N) 有界,旧记录自动淘汰,避免内存无限增长。
|
||
- emit() 在 logging 内部锁下执行,deque.append 线程安全。
|
||
- 查询时按级别过滤(≥指定级别)、取最近 N 条,返回结构化 dict。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import threading
|
||
from collections import deque
|
||
from datetime import datetime, timezone
|
||
|
||
# 级别名 → 数值,用于过滤
|
||
_LEVELS = {
|
||
"debug": logging.DEBUG,
|
||
"info": logging.INFO,
|
||
"warning": logging.WARNING,
|
||
"error": logging.ERROR,
|
||
}
|
||
|
||
|
||
class _BoundedLogRecord:
|
||
"""从 LogRecord 提取显示所需字段,避免持有完整对象引用。"""
|
||
|
||
__slots__ = ("ts", "level_no", "level_name", "logger_name", "message", "traceback")
|
||
|
||
def __init__(self, record: logging.LogRecord) -> None:
|
||
self.ts: float = record.created
|
||
self.level_no: int = record.levelno
|
||
self.level_name: str = record.levelname
|
||
self.logger_name: str = record.name
|
||
# getMessage() 应用 %-style 懒格式化
|
||
self.message: str = record.getMessage()
|
||
# exc_info 存的是 (type, value, tb) 三元组,格式化为字符串
|
||
self.traceback: str | None = None
|
||
if record.exc_info:
|
||
import traceback as _tb
|
||
self.traceback = "".join(_tb.format_exception(*record.exc_info))
|
||
|
||
|
||
class MemoryLogHandler(logging.Handler):
|
||
"""把日志记录存入有界 deque,供 /api/logs 查询。"""
|
||
|
||
def __init__(self, buffer_size: int = 2000) -> None:
|
||
super().__init__()
|
||
self._buffer: deque[_BoundedLogRecord] = deque(maxlen=buffer_size)
|
||
self._lock = threading.Lock()
|
||
|
||
def emit(self, record: logging.LogRecord) -> None:
|
||
try:
|
||
entry = _BoundedLogRecord(record)
|
||
with self._lock:
|
||
self._buffer.append(entry)
|
||
except Exception: # pragma: no cover —— logging 不应抛
|
||
self.handleError(record)
|
||
|
||
def get_records(
|
||
self, level: str = "info", tail: int = 200,
|
||
) -> list[dict]:
|
||
"""返回最近 tail 条、≥指定级别的日志(JSON 友好的 dict 列表)。
|
||
|
||
Args:
|
||
level: debug | info | warning | error(最小级别)
|
||
tail: 最多返回条数
|
||
"""
|
||
min_level = _LEVELS.get(level.lower(), logging.INFO)
|
||
with self._lock:
|
||
snapshot = list(self._buffer)
|
||
# 过滤 + 取最近 tail 条
|
||
filtered = [r for r in snapshot if r.level_no >= min_level]
|
||
result = []
|
||
for r in filtered[-tail:]:
|
||
ts_str = datetime.fromtimestamp(r.ts, tz=timezone.utc).strftime(
|
||
"%Y-%m-%d %H:%M:%S"
|
||
)
|
||
result.append({
|
||
"ts": ts_str,
|
||
"level": r.level_name,
|
||
"logger": r.logger_name,
|
||
"msg": r.message,
|
||
"traceback": r.traceback,
|
||
})
|
||
return result
|
||
|
||
def clear(self) -> None:
|
||
with self._lock:
|
||
self._buffer.clear()
|
||
|
||
|
||
# 进程级单例
|
||
_buffer: MemoryLogHandler | None = None
|
||
|
||
|
||
def get_log_buffer() -> MemoryLogHandler:
|
||
global _buffer
|
||
if _buffer is None:
|
||
_buffer = MemoryLogHandler()
|
||
return _buffer
|
||
|
||
|
||
def init_log_buffer(buffer_size: int) -> MemoryLogHandler:
|
||
"""(重新)创建缓冲并返回,供 main.py 启动时按配置初始化。"""
|
||
global _buffer
|
||
_buffer = MemoryLogHandler(buffer_size=buffer_size)
|
||
return _buffer
|