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

View File

@@ -0,0 +1,88 @@
"""任务路由:列表 / 状态 / 下载字幕。"""
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from ..config import get_settings
from ..database import get_db
from ..models.task import Task
from ..schemas.task import TaskListResponse, TaskResponse
router = APIRouter(prefix="/api/tasks", tags=["task"])
def _to_resp(task: Task) -> TaskResponse:
return TaskResponse(
id=task.id,
filename=task.filename,
status=task.status,
progress=task.progress,
error=task.error,
created_at=task.created_at,
updated_at=task.updated_at,
)
@router.get("", response_model=TaskListResponse, summary="任务列表")
def list_tasks(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
q: str = Query("", description="按文件名模糊搜索(大小写不敏感,匹配子串)"),
db: Session = Depends(get_db),
) -> TaskListResponse:
q_base = db.query(Task).order_by(Task.id.desc())
if q.strip():
# SQLite 的 LIKE 默认大小写不敏感ASCIIilike 等价于 LIKE
like = f"%{q.strip()}%"
q_base = q_base.filter(Task.filename.ilike(like))
total = q_base.count()
tasks = q_base.offset(offset).limit(limit).all()
return TaskListResponse(tasks=[_to_resp(t) for t in tasks], total=total)
@router.get("/{task_id}", response_model=TaskResponse, summary="任务状态")
def get_task(task_id: int, db: Session = Depends(get_db)) -> TaskResponse:
task = db.get(Task, task_id)
if task is None:
raise HTTPException(404, f"任务不存在:{task_id}")
return _to_resp(task)
@router.get("/{task_id}/subtitle", summary="下载字幕")
def download_subtitle(
task_id: int,
type: str = Query("bilingual", pattern="^(bilingual|en|zh)$"),
db: Session = Depends(get_db),
) -> FileResponse:
task = db.get(Task, task_id)
if task is None:
raise HTTPException(404, f"任务不存在:{task_id}")
if task.status != "done":
raise HTTPException(409, f"任务尚未完成(当前状态:{task.status}")
rel = {
"bilingual": task.bilingual_srt_path,
"en": task.en_srt_path,
"zh": task.zh_srt_path,
}[type]
if not rel:
raise HTTPException(404, f"该类型字幕不存在:{type}")
s = get_settings()
path = s.output_dir() / rel
if not path.is_file():
raise HTTPException(404, f"字幕文件丢失:{path}")
base = Path(task.filename).stem
suffix = "" if type == "bilingual" else f".{type}"
download_name = f"{base}{suffix}.srt"
return FileResponse(
path=path,
media_type="application/x-subrip",
filename=download_name,
)