Initial commit: audio2text 双语字幕生成服务

- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码
- faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻)
- 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面
- 历史页文件名搜索;缓存定时清理(默认保留7天,可配置)
- 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
This commit is contained in:
2026-07-06 06:54:19 +00:00
commit 00e2a95fb7
44 changed files with 4110 additions and 0 deletions

109
app/services/log_buffer.py Normal file
View 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