Initial commit: audio2text 双语字幕生成服务
- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码 - faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻) - 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面 - 历史页文件名搜索;缓存定时清理(默认保留7天,可配置) - 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
This commit is contained in:
136
app/config.py
Normal file
136
app/config.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""运行时配置:所有参数从 config.yaml 读取,对齐 server/config.py 的风格。
|
||||
|
||||
CPU dev / GPU prod 仅靠 device / model / compute_type 三项切换,代码完全不变。
|
||||
"""
|
||||
|
||||
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 = "small"
|
||||
device: str = "cpu" # cpu | cuda
|
||||
compute_type: str = "int8" # cpu: int8;gpu: float16
|
||||
language: str = "en"
|
||||
word_timestamps: bool = True
|
||||
vad_filter: bool = True
|
||||
|
||||
|
||||
class TranslationConfig(BaseModel):
|
||||
model: str = "facebook/nllb-200-distilled-1.3B"
|
||||
device: str = "cpu" # cpu | cuda
|
||||
src_lang: str = "eng_Latn"
|
||||
tgt_lang: str = "zho_Hans"
|
||||
batch_size: int = 16
|
||||
max_length: int = 256
|
||||
|
||||
|
||||
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:详细(ffmpeg 命令、模型加载/卸载、转写逐段、翻译逐批进度)
|
||||
- info:简略(仅任务阶段转换,如 "任务 N [transcribing 55%]")
|
||||
- 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()
|
||||
Reference in New Issue
Block a user