Files
audio2text/app/main.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

161 lines
6.0 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.

"""FastAPI 应用工厂(对齐 server/main.py 风格)。
路由概览:
GET / -> 版本号
GET /health -> 存活探针
GET /logs -> 实时日志页(公开)
GET /docs -> Swagger UIBasic Auth
POST /api/tasks/chunk-uploads/... -> 分片上传(建会话/查状态/传片/complete
GET /api/tasks -> 任务列表
GET /api/tasks/{id} -> 任务状态
GET /api/tasks/{id}/subtitle -> 下载字幕
GET /api/logs -> 查询日志(按级别过滤)
"""
from __future__ import annotations
import asyncio
import logging
import threading
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.responses import HTMLResponse, JSONResponse
from sqlalchemy.orm import Session
from .config import get_settings
from .controllers import log_router, task_router, upload_router
from .database import get_db, init_db_schema
from .security import require_docs_auth
from .services.cache_cleaner import purge_expired_cache, run_forever as run_cache_cleaner
from .services.log_buffer import init_log_buffer
from .services.reaper import reap_stale_sessions
from .views.history_html import render as render_history_html
from .views.home_html import render as render_home_html
from .views.logs_html import render as render_logs_html
# 日志分层:
# - audio2text logger 始终设 DEBUG确保所有记录含子步骤都能产生。
# - 控制台 handler 用配置的 levelconfig=info 时控制台只显示简略。
# - 内存 handler 始终 DEBUG全收/api/logs?level= 查询时按参数过滤可见级别。
# 这样 /logs 页面切到 DEBUG 能看到详细,而控制台仍按 config.level 简略输出。
_LOG_LEVEL_MAP = {"debug": logging.DEBUG, "info": logging.INFO,
"warning": logging.WARNING, "error": logging.ERROR}
_console_level = _LOG_LEVEL_MAP.get(get_settings().logging.level, logging.INFO)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
# 控制台 handlerbasicConfig 安装在 root按配置级别过滤
for _h in logging.getLogger().handlers:
_h.setLevel(_console_level)
logger = logging.getLogger("audio2text")
logger.setLevel(logging.DEBUG)
# 内存日志缓冲:捕获所有 audio2text.* 日志,供 /logs 页面查询。
# handler 级别设为 DEBUG最低由 /api/logs 查询时按 level 参数过滤。
_memory_handler = init_log_buffer(get_settings().logging.buffer_size)
_memory_handler.setLevel(logging.DEBUG)
logger.addHandler(_memory_handler)
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
init_db_schema()
logger.info("SQLite 表已就绪。")
except Exception as exc: # pragma: no cover
logger.error("初始化数据库失败:%s", exc)
logger.info(
"audio2text 启动ASR=%s/%s,翻译=%s",
get_settings().asr.model, get_settings().asr.device, get_settings().translation.device,
)
# 启动时清理一次被放弃的分片会话
try:
await asyncio.to_thread(reap_stale_sessions)
except Exception as exc: # pragma: no cover
logger.warning("启动 reaper 失败:%s", exc)
# 缓存清理:启动时跑一次 + 后台定时循环(守护线程,随进程退出)
s = get_settings()
try:
await asyncio.to_thread(purge_expired_cache)
except Exception as exc: # pragma: no cover
logger.warning("启动缓存清理失败:%s", exc)
try:
t = threading.Thread(
target=run_cache_cleaner,
args=(s.storage.cache_cleanup_interval_hours,),
name="cache-cleaner",
daemon=True,
)
t.start()
logger.info(
"缓存清理调度已启动:保留期 %d 天,间隔 %d 小时。",
s.storage.cache_retention_days, s.storage.cache_cleanup_interval_hours,
)
except Exception as exc: # pragma: no cover
logger.warning("启动缓存清理调度失败:%s", exc)
yield
def create_app() -> FastAPI:
s = get_settings()
app = FastAPI(
title="audio2text",
description=(
"音频/视频转双语字幕服务。\n\n"
"- 上传视频 → ffmpeg 提取音频 → faster-whisper 识别英语 → 断句+时间戳重算\n"
" → NLLB 翻译为中文 → 双语 SRT\n\n"
"模型不共驻ASR 与翻译分阶段加载,翻译时独占显存跑大 batch。\n"
"/docs 需 Basic Auth凭据见 config.yaml 的 docs 段。"
),
version="1.0.0",
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=lifespan,
)
app.include_router(upload_router)
app.include_router(task_router)
app.include_router(log_router)
# 受 Basic Auth 保护的文档接口
@app.get("/openapi.json")
def protected_openapi(_: str = Depends(require_docs_auth)) -> JSONResponse:
return JSONResponse(app.openapi())
@app.get("/docs")
def protected_docs(_: str = Depends(require_docs_auth)):
return get_swagger_ui_html(
openapi_url="/openapi.json", title="audio2text docs", swagger_favicon_url=""
)
@app.get("/redoc")
def protected_redoc(_: str = Depends(require_docs_auth)):
return get_redoc_html(
openapi_url="/openapi.json", title="audio2text docs", redoc_favicon_url=""
)
@app.get("/", response_class=HTMLResponse)
def home_page() -> HTMLResponse:
return HTMLResponse(render_home_html())
@app.get("/health")
def health() -> dict:
return {"status": "ok"}
@app.get("/history", response_class=HTMLResponse)
def history_page() -> HTMLResponse:
return HTMLResponse(render_history_html())
@app.get("/logs", response_class=HTMLResponse)
def logs_page() -> HTMLResponse:
return HTMLResponse(render_logs_html())
return app
app = create_app()