Files
audio2text/app/controllers/task_router.py
audio2text dev 7635e5e766 feat: 设置页调 batch_size + 删除任务 + /docs 去认证 + 离线模式
设置页(/settings):
- 新增 Setting DB 模型(键值存储,持久化运行时覆盖)
- config.py 加 _apply_overrides:get_settings() 合并 DB 覆盖值
  (_applying_overrides 标志防递归:DB 初始化回调 get_settings 时不重入)
- save_setting() 写 DB + 清 lru_cache,后续任务读到新值
- settings_router.py: GET/PUT /api/settings
- settings_html.py: 设置页表单(batch_size/beam_size/sort_by_length)
  + 设备只读信息 + 保存后 reload 确认
- 验证:改 asr_batch_size=16 beam_size=1 -> 任务 ASR 日志确认生效
- 验证:重启容器后设置从 DB 恢复(持久化)

删除任务:
- task_router.py: DELETE /api/tasks/{id},仅 done/failed 可删
  删字幕/音频/视频产物 + UploadSession + Task DB 记录
- home_html.py + history_html.py: done/failed 任务显示删除按钮
  + confirm 确认 + 调用 DELETE API + 刷新列表
- 验证:删除 task 15 成功,删除进行中 task 返回 409

/docs 去认证:
- 移除 require_docs_auth 依赖,/docs /redoc /openapi.json 直接公开

离线模式:
- Dockerfile dev/final 加 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
  (模型已缓存在 ./models volume,无需联网验证)
- 验证:离线模式完整跑通 ASR+翻译(task 15 done)
2026-07-11 11:33:34 +08:00

179 lines
6.4 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.

"""任务路由:列表 / 状态 / 下载字幕 / 删除。"""
from __future__ import annotations
import logging
import shutil
from datetime import datetime, timezone
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, STATUS_UPLOADING, STATUS_DONE, STATUS_FAILED
from ..models.upload_session import UploadSession
from ..schemas.task import TaskListResponse, TaskResponse
logger = logging.getLogger("audio2text.tasks")
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,
)
def _upload_to_resp(session: UploadSession) -> TaskResponse:
"""把上传中的 UploadSession 映射为虚拟 TaskResponse。
is_upload=true 让前端走上传状态轮询而非任务轮询。
progress = 已传分片数 / 总分片数 × 100映射到 0-5 区间,与 extract 阶段衔接)。
"""
uploaded = len(session.uploaded_chunks or [])
total = session.total_chunks or 1
# 上传进度映射到 0-4%extract 从 5% 开始,留 1% 给 complete 拼接)
progress = min(4.0, uploaded / total * 4.0)
now = datetime.now(timezone.utc)
return TaskResponse(
id=0, # 虚拟 id前端用 upload_id 轮询
filename=session.filename,
status=STATUS_UPLOADING,
progress=progress,
error=None,
created_at=session.created_at,
updated_at=session.updated_at or now,
size_bytes=session.size_bytes,
is_upload=True,
upload_id=session.upload_id,
)
@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:
# 上传中的会话pending 状态)也作为虚拟任务返回,让前端能看到上传进度
upload_q = db.query(UploadSession).filter(UploadSession.status == "pending")
if q.strip():
like = f"%{q.strip()}%"
upload_q = upload_q.filter(UploadSession.filename.ilike(like))
uploads = upload_q.order_by(UploadSession.created_at.desc()).all()
# 已创建的 Task含 extracting/transcribing/.../done/failed
task_q = db.query(Task).order_by(Task.created_at.desc(), Task.id.desc())
if q.strip():
like = f"%{q.strip()}%"
task_q = task_q.filter(Task.filename.ilike(like))
total_tasks = task_q.count()
tasks = task_q.offset(offset).limit(limit).all()
# 合并:上传会话 + Task按 created_at 倒序
upload_resps = [_upload_to_resp(s) for s in uploads]
task_resps = [_to_resp(t) for t in tasks]
all_resps = upload_resps + task_resps
all_resps.sort(key=lambda r: r.created_at, reverse=True)
# 分页offset/limit 作用于合并后列表(上传会话通常很少,主要影响首页前几条)
paged = all_resps[offset:offset + limit]
return TaskListResponse(tasks=paged, total=len(all_resps))
@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,
)
@router.delete("/{task_id}", summary="删除任务(仅允许已完成/失败)")
def delete_task(task_id: int, db: Session = Depends(get_db)) -> dict:
"""删除任务及其产物(字幕 / 中间音频 / 保留的原始视频)+ DB 记录。
仅允许删除已完成done或失败failed的任务进行中的任务不可删。
"""
task = db.get(Task, task_id)
if task is None:
raise HTTPException(404, f"任务不存在:{task_id}")
if task.status not in (STATUS_DONE, STATUS_FAILED):
raise HTTPException(409, f"任务进行中,无法删除(当前状态:{task.status}")
s = get_settings()
deleted: list[str] = []
# 删字幕输出目录
out_dir = s.output_dir() / f"task_{task.id}"
if out_dir.is_dir():
shutil.rmtree(out_dir, ignore_errors=True)
deleted.append("outputs")
# 删中间音频
if task.wav_path:
wav = Path(task.wav_path)
if wav.is_file():
wav.unlink(missing_ok=True)
deleted.append("audio")
# 删保留的原始视频
if task.source_path:
src = s.upload_dir() / task.source_path
if src.is_file():
src.unlink(missing_ok=True)
deleted.append("video")
# 删 DB 记录(先删关联的 UploadSession再删 Task
db.query(UploadSession).filter(UploadSession.task_id == task.id).delete()
db.delete(task)
db.commit()
logger.info("删除任务 %d%s%s", task_id, task.filename, ", ".join(deleted) or "无产物")
return {"status": "deleted", "task_id": task_id, "cleaned": deleted}