- scheduler: ffmpeg 异步线程 + GPU 串行调度 + 模型复用(2N→2 次加载) - pipeline: 阶段拆分(extract/asr/translate),中间数据存 Task 字段 - translate_service: 长度排序批处理,padding 浪费减少 91% - model_manager: ASR/翻译不共驻,BatchedInferencePipeline 批量解码 - 日志分级: INFO=任务流转里程碑,DEBUG=进度详情;默认 INFO - 前端: 日志最新在上+滚动感知+退避轮询;24h 时间;上传中状态显示 - /health: 返回完整 Whisper/NLLB 配置 - upload_service: 单事务 complete + 扩展名白名单 - task_router: 合并 UploadSession 虚拟任务到列表 - Dockerfile: CPU/GPU 独立构建链,deps 缓存稳定 - prefetch_models: 安装时预下载模型权重
142 lines
4.9 KiB
Python
142 lines
4.9 KiB
Python
"""模型管理器:ASR 与翻译模型不共驻,任一时刻 GPU 上只有一个模型。
|
||
|
||
设计:
|
||
- 单例 ModelManager 跟踪当前加载的模型类型(none / asr / translator)
|
||
- get_asr(): 若翻译器在内存 → 先卸载(del + gc + empty_cache)→ 加载 faster-whisper
|
||
- get_translator(): 若 ASR 在内存 → 先卸载 → 加载 NLLB
|
||
- 翻译阶段独占显存,可用大 batch_size;ASR 阶段同理
|
||
|
||
这样在 24G 3090 上无需担心显存叠加,CPU dev 时也省内存。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import gc
|
||
import logging
|
||
import threading
|
||
from typing import Any
|
||
|
||
from ..config import get_settings
|
||
|
||
logger = logging.getLogger("audio2text.models")
|
||
|
||
# 全局单例 + 锁:模型加载/卸载必须串行
|
||
_lock = threading.Lock()
|
||
|
||
|
||
def _free_memory() -> None:
|
||
"""释放 Python 对象与 GPU 缓存。"""
|
||
gc.collect()
|
||
try:
|
||
import torch
|
||
if torch.cuda.is_available():
|
||
torch.cuda.empty_cache()
|
||
torch.cuda.synchronize()
|
||
except Exception: # pragma: no cover
|
||
pass
|
||
|
||
|
||
class ModelManager:
|
||
"""ASR / 翻译模型的不共驻管理器。"""
|
||
|
||
def __init__(self) -> None:
|
||
self._asr: Any = None # faster_whisper.WhisperModel
|
||
self._translator: Any = None # transformers pipeline
|
||
self._current: str = "none" # none | asr | translator
|
||
|
||
# ---------------- ASR ----------------
|
||
|
||
def get_asr(self) -> Any:
|
||
"""返回已加载的 faster-whisper 模型;必要时先卸载翻译器。"""
|
||
with _lock:
|
||
if self._asr is not None:
|
||
return self._asr
|
||
if self._translator is not None:
|
||
self._unload_translator_locked()
|
||
s = get_settings().asr
|
||
logger.info("加载 ASR 模型 model=%s device=%s compute_type=%s",
|
||
s.model, s.device, s.compute_type)
|
||
from faster_whisper import WhisperModel, BatchedInferencePipeline
|
||
# device/compute_type 组合:cpu+int8 / cuda+float16
|
||
# BatchedInferencePipeline 包装 WhisperModel,使 transcribe() 支持 batch_size,
|
||
# 多个音频块(chunk_length=30s)一次性送 GPU 解码,配合内部 prefill 提高利用率。
|
||
whisper = WhisperModel(
|
||
s.model, device=s.device, compute_type=s.compute_type,
|
||
)
|
||
self._asr = BatchedInferencePipeline(model=whisper)
|
||
self._current = "asr"
|
||
logger.info("ASR 模型已就绪(batched, batch_size=%d)。", s.batch_size)
|
||
return self._asr
|
||
|
||
def unload_asr(self) -> None:
|
||
with _lock:
|
||
self._unload_asr_locked()
|
||
|
||
def _unload_asr_locked(self) -> None:
|
||
if self._asr is None:
|
||
return
|
||
logger.info("卸载 ASR 模型(释放显存供翻译器独占)。")
|
||
# faster-whisper 模型无显式 close,del 即可
|
||
del self._asr
|
||
self._asr = None
|
||
self._current = "none" if self._translator is None else "translator"
|
||
_free_memory()
|
||
|
||
# ---------------- 翻译器 ----------------
|
||
|
||
def get_translator(self) -> Any:
|
||
"""返回已加载的 NLLB 翻译 pipeline;必要时先卸载 ASR。"""
|
||
with _lock:
|
||
if self._translator is not None:
|
||
return self._translator
|
||
if self._asr is not None:
|
||
self._unload_asr_locked()
|
||
s = get_settings().translation
|
||
logger.info("加载翻译模型 model=%s device=%s", s.model, s.device)
|
||
from transformers import pipeline
|
||
self._translator = pipeline(
|
||
"translation",
|
||
model=s.model,
|
||
device=s.device,
|
||
src_lang=s.src_lang,
|
||
tgt_lang=s.tgt_lang,
|
||
batch_size=s.batch_size, # pipeline 内部批大小,与 translate_service 分块对齐
|
||
)
|
||
self._current = "translator"
|
||
logger.info("翻译模型已就绪(显存独占,batch_size=%d,sort_by_length=%s)。",
|
||
s.batch_size, s.sort_by_length)
|
||
return self._translator
|
||
|
||
def unload_translator(self) -> None:
|
||
with _lock:
|
||
self._unload_translator_locked()
|
||
|
||
def _unload_translator_locked(self) -> None:
|
||
if self._translator is None:
|
||
return
|
||
logger.info("卸载翻译模型。")
|
||
# 释放 pipeline 持有的 model + tokenizer
|
||
mdl = getattr(self._translator, "model", None)
|
||
tok = getattr(self._translator, "tokenizer", None)
|
||
del self._translator, mdl, tok
|
||
self._translator = None
|
||
self._current = "none" if self._asr is None else "asr"
|
||
_free_memory()
|
||
|
||
# ---------------- 状态 ----------------
|
||
|
||
@property
|
||
def current(self) -> str:
|
||
return self._current
|
||
|
||
|
||
# 进程级单例
|
||
_manager: ModelManager | None = None
|
||
|
||
|
||
def get_model_manager() -> ModelManager:
|
||
global _manager
|
||
if _manager is None:
|
||
_manager = ModelManager()
|
||
return _manager
|