Initial commit: audio2text 双语字幕生成服务

- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码
- faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻)
- 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面
- 历史页文件名搜索;缓存定时清理(默认保留7天,可配置)
- 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
This commit is contained in:
2026-07-06 06:54:19 +00:00
commit 00e2a95fb7
44 changed files with 4110 additions and 0 deletions

136
app/config.py Normal file
View 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: int8gpu: 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()