Files
audio2text/app/services/model_manager.py
audio2text dev 73110848f4 feat: 调度器+并发管线+GPU优化+日志分级+前端修复
- 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: 安装时预下载模型权重
2026-07-06 21:59:59 +08:00

142 lines
4.9 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.

"""模型管理器ASR 与翻译模型不共驻,任一时刻 GPU 上只有一个模型。
设计:
- 单例 ModelManager 跟踪当前加载的模型类型none / asr / translator
- get_asr(): 若翻译器在内存 → 先卸载del + gc + empty_cache→ 加载 faster-whisper
- get_translator(): 若 ASR 在内存 → 先卸载 → 加载 NLLB
- 翻译阶段独占显存,可用大 batch_sizeASR 阶段同理
这样在 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 模型无显式 closedel 即可
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=%dsort_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