Initial commit: audio2text 双语字幕生成服务
- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码 - faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻) - 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面 - 历史页文件名搜索;缓存定时清理(默认保留7天,可配置) - 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
This commit is contained in:
109
app/services/log_buffer.py
Normal file
109
app/services/log_buffer.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""内存日志缓冲:捕获所有 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
|
||||
Reference in New Issue
Block a user