- 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: 安装时预下载模型权重
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
"""运行时配置:所有参数从 config.yaml 读取。
|
||
|
||
CPU dev / GPU prod 仅靠 device / model / compute_type 三项切换,代码完全不变。
|
||
默认值与 config.example.yaml 对齐,确保无 yaml 时也能用最小配置启动。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
from pydantic import BaseModel, Field, field_validator
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
DEFAULT_CONFIG_PATH = PROJECT_ROOT / "config.yaml"
|
||
|
||
|
||
class ServerConfig(BaseModel):
|
||
host: str = "0.0.0.0"
|
||
port: int = 8000
|
||
workers: int = 1
|
||
|
||
|
||
class StorageConfig(BaseModel):
|
||
upload_dir: str = "/data/uploads"
|
||
work_dir: str = "/data/.work"
|
||
output_dir: str = "/data/outputs"
|
||
chunk_bytes: int = 1024 * 1024
|
||
chunk_session_ttl_seconds: int = 300
|
||
# 缓存保留:超期任务产物(输出字幕 / 中间音频 / 保留的原始视频)连同 DB 记录一并清理
|
||
cache_retention_days: int = 7
|
||
# 定时清理间隔(小时):启动时跑一次,之后按此间隔循环
|
||
cache_cleanup_interval_hours: int = 24
|
||
|
||
|
||
class ProcessingConfig(BaseModel):
|
||
delete_original_after_extract: bool = True
|
||
keep_audio: bool = False
|
||
|
||
|
||
class AsrConfig(BaseModel):
|
||
model: str = "tiny.en" # CPU: tiny.en;GPU: large-v3-turbo
|
||
device: str = "cpu" # cpu | cuda
|
||
compute_type: str = "int8" # cpu: int8;gpu: float16
|
||
language: str = "en"
|
||
word_timestamps: bool = True
|
||
vad_filter: bool = True
|
||
batch_size: int = 8 # BatchedInferencePipeline 的音频块批大小;GPU 建议 16
|
||
|
||
|
||
class TranslationConfig(BaseModel):
|
||
model: str = "Helsinki-NLP/opus-mt-en-zh" # CPU: opus-mt(轻量);GPU: facebook/nllb-200-distilled-1.3B
|
||
device: str = "cpu" # cpu | cuda
|
||
src_lang: str = "eng_Latn" # NLLB 语言码:英语
|
||
tgt_lang: str = "zho_Hans" # NLLB 语言码:简体中文
|
||
batch_size: int = 8 # 排序后单批最大条数;CPU: 8,GPU: 32(显存独占可用大 batch)
|
||
max_length: int = 256 # 单条最大生成 token 数
|
||
sort_by_length: bool = True # 按句子长度排序后分批,减少批内 padding 浪费(GPU 收益大)
|
||
|
||
|
||
class SegmentationConfig(BaseModel):
|
||
max_words_per_line: int = 14
|
||
max_duration_seconds: float = 7.0
|
||
min_duration_seconds: float = 1.0
|
||
max_chars_per_line: int = 42
|
||
|
||
|
||
class LoggingConfig(BaseModel):
|
||
"""日志配置:控制台 + 内存缓冲的最低级别,以及缓冲条数。
|
||
|
||
分层语义:
|
||
- debug:进度详情(任务 [status pct%]、转写/翻译逐批统计、ffmpeg 命令)
|
||
- info:任务流转里程碑(音频提取/ASR/翻译 的开始与完成、模型加载与卸载)
|
||
- error:详细错误(完整 traceback,由 logger.exception 自带)
|
||
"""
|
||
|
||
level: str = "info" # debug | info | warning | error
|
||
buffer_size: int = 2000 # /logs 页面内存缓冲条数
|
||
|
||
@field_validator("level", mode="before")
|
||
@classmethod
|
||
def _normalize_level(cls, v):
|
||
return str(v).lower() if v else "info"
|
||
|
||
|
||
class DocsConfig(BaseModel):
|
||
"""/docs Basic Auth 凭据(明文,对齐 server)。"""
|
||
|
||
enabled: bool = True
|
||
username: str = "admin"
|
||
password: str = ""
|
||
realm: str = "audio2text docs"
|
||
|
||
@field_validator("password", mode="before")
|
||
@classmethod
|
||
def _coerce(cls, v):
|
||
return "" if v is None else str(v)
|
||
|
||
|
||
class Settings(BaseModel):
|
||
server: ServerConfig = ServerConfig()
|
||
storage: StorageConfig = StorageConfig()
|
||
processing: ProcessingConfig = ProcessingConfig()
|
||
asr: AsrConfig = AsrConfig()
|
||
translation: TranslationConfig = TranslationConfig()
|
||
segmentation: SegmentationConfig = SegmentationConfig()
|
||
logging: LoggingConfig = LoggingConfig()
|
||
docs: DocsConfig = DocsConfig()
|
||
|
||
def upload_dir(self) -> Path:
|
||
return Path(self.storage.upload_dir)
|
||
|
||
def work_dir(self) -> Path:
|
||
return Path(self.storage.work_dir)
|
||
|
||
def output_dir(self) -> Path:
|
||
return Path(self.storage.output_dir)
|
||
|
||
|
||
def _load_yaml(path: Path) -> dict:
|
||
if not path.exists():
|
||
raise FileNotFoundError(
|
||
f"未找到配置文件 {path};请先 cp config.example.yaml config.yaml 并填值。"
|
||
)
|
||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def get_settings() -> Settings:
|
||
path = Path(os.getenv("CONFIG_PATH", str(DEFAULT_CONFIG_PATH)))
|
||
return Settings.model_validate(_load_yaml(path))
|
||
|
||
|
||
def reload_settings() -> Settings:
|
||
"""清缓存并重新读取,供脚本与测试使用。"""
|
||
get_settings.cache_clear()
|
||
return get_settings()
|