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)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""路由聚合:导出各 controller 的 router,供 main.py include。"""
|
||||
|
||||
from .log_router import router as log_router
|
||||
from .settings_router import router as settings_router
|
||||
from .task_router import router as task_router
|
||||
from .upload_router import router as upload_router
|
||||
|
||||
__all__ = ["log_router", "task_router", "upload_router"]
|
||||
__all__ = ["log_router", "settings_router", "task_router", "upload_router"]
|
||||
|
||||
92
app/controllers/settings_router.py
Normal file
92
app/controllers/settings_router.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""设置路由:查询/修改运行时可调参数。
|
||||
|
||||
config.yaml 是只读挂载,改它需重启容器。本路由把部分参数(batch_size 等)
|
||||
存到 DB 的 setting 表,通过 config.save_setting() + reload_settings() 实现
|
||||
运行时热更新:保存后清 lru_cache,后续任务读到新值。
|
||||
|
||||
当前可调项与 config._OVERIDEABLE_FIELDS 对齐。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..config import _OVERIDEABLE_FIELDS, get_settings, save_setting
|
||||
|
||||
logger = logging.getLogger("audio2text.settings")
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
|
||||
# ---------------- 响应 / 请求 DTO ----------------
|
||||
|
||||
class SettingsResponse(BaseModel):
|
||||
"""当前生效的设置值(YAML 基础 + DB 覆盖后的合并值)。"""
|
||||
asr_batch_size: int
|
||||
asr_beam_size: int
|
||||
translation_batch_size: int
|
||||
translation_sort_by_length: bool
|
||||
# 不可改但展示的只读信息
|
||||
asr_model: str
|
||||
asr_device: str
|
||||
asr_compute_type: str
|
||||
translation_model: str
|
||||
translation_device: str
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
"""设置更新请求:只传要改的字段,未传的保持不变。"""
|
||||
asr_batch_size: int | None = Field(default=None, ge=1, le=128)
|
||||
asr_beam_size: int | None = Field(default=None, ge=1, le=10)
|
||||
translation_batch_size: int | None = Field(default=None, ge=1, le=256)
|
||||
translation_sort_by_length: bool | None = None
|
||||
|
||||
|
||||
# ---------------- 字段映射:DTO 字段名 -> config 点分路径 ----------------
|
||||
|
||||
_FIELD_MAP: dict[str, str] = {
|
||||
"asr_batch_size": "asr.batch_size",
|
||||
"asr_beam_size": "asr.beam_size",
|
||||
"translation_batch_size": "translation.batch_size",
|
||||
"translation_sort_by_length": "translation.sort_by_length",
|
||||
}
|
||||
|
||||
|
||||
# ---------------- 接口 ----------------
|
||||
|
||||
@router.get("", summary="查询当前生效的设置")
|
||||
def get_current_settings() -> SettingsResponse:
|
||||
"""返回当前生效的设置(YAML 基础 + DB 覆盖合并后的值)。"""
|
||||
s = get_settings()
|
||||
return SettingsResponse(
|
||||
asr_batch_size=s.asr.batch_size,
|
||||
asr_beam_size=s.asr.beam_size,
|
||||
translation_batch_size=s.translation.batch_size,
|
||||
translation_sort_by_length=s.translation.sort_by_length,
|
||||
asr_model=s.asr.model,
|
||||
asr_device=s.asr.device,
|
||||
asr_compute_type=s.asr.compute_type,
|
||||
translation_model=s.translation.model,
|
||||
translation_device=s.translation.device,
|
||||
)
|
||||
|
||||
|
||||
@router.put("", summary="更新设置(保存后对后续任务生效)")
|
||||
def update_settings(req: SettingsUpdate) -> dict:
|
||||
"""保存修改的设置项到 DB,清配置缓存。
|
||||
|
||||
只处理请求中非 None 的字段。保存后立即生效(后续任务读到新值),
|
||||
已在跑的任务不受影响(任务在各阶段开始时读配置)。
|
||||
"""
|
||||
changed: dict = {}
|
||||
for field, path in _FIELD_MAP.items():
|
||||
val = getattr(req, field)
|
||||
if val is not None:
|
||||
save_setting(path, val)
|
||||
changed[field] = val
|
||||
logger.info("设置已更新:%s = %s(对后续任务生效)", path, val)
|
||||
if not changed:
|
||||
return {"status": "no_change", "changed": {}}
|
||||
return {"status": "saved", "changed": changed}
|
||||
@@ -1,7 +1,9 @@
|
||||
"""任务路由:列表 / 状态 / 下载字幕。"""
|
||||
"""任务路由:列表 / 状态 / 下载字幕 / 删除。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,10 +13,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from ..config import get_settings
|
||||
from ..database import get_db
|
||||
from ..models.task import Task, STATUS_UPLOADING
|
||||
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"])
|
||||
|
||||
|
||||
@@ -129,3 +132,47 @@ def download_subtitle(
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user