Files
LLM-infa/api/route/jobs.py
jze9 e87f6cd215 Фоновые задачи: плейлисты, длинные лекции, вкладка настроек
- таблица jobs + очередь с воркером: POST /jobs сразу отвечает, элементы
  плейлиста обрабатываются последовательно, есть отмена и живой прогресс
  (скачивание %, минуты распознанного Vosk-аудио)
- expand_url: плейлист раскрывается в список видео
- keep_video=false теперь качает только аудио (для 4-5ч лекций)
- настройки (LLM-ключ, модель, длина выжимки, хранение видео, путь Vosk)
  хранятся в Postgres: переживают перезапуск и перезагрузку страницы
- длина выжимки прокидывается в LLM-промпт ({n} предложений)
- UI: вкладки «Главная»/«Настройки», карточка задач с автообновлением
  каждые 3 сек, прогресс-бар, отмена, библиотека обновляется по мере
  готовности элементов

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:01:11 +05:00

113 lines
3.1 KiB
Python

"""Фоновые задачи: видео или плейлист любой длины.
POST ставит задачу в очередь и сразу возвращает id; прогресс — в GET /jobs.
"""
from __future__ import annotations
import uuid as _uuid
from datetime import datetime
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from api.db.job import create_job, delete_job, get_job, list_jobs
from api.lib import jobqueue
router = APIRouter(prefix="/jobs", tags=["jobs"])
class JobIn(BaseModel):
url: str
section_id: int | None = None
# None → берутся из сохранённых настроек
keep_video: bool | None = None
summary_max_sentences: int | None = None
model_path: str | None = None
class JobOut(BaseModel):
id: str
url: str
kind: str
title: str
status: str
total_items: int
done_items: int
failed_items: int
current_item: str
live: str
error: str
created_at: datetime
def _to_out(j) -> JobOut:
return JobOut(
id=j.id,
url=j.url,
kind=j.kind,
title=j.title,
status=j.status,
total_items=j.total_items,
done_items=j.done_items,
failed_items=j.failed_items,
current_item=j.current_item,
live=jobqueue.live_status(j.id) or j.current_item,
error=j.error,
created_at=j.created_at,
)
@router.post("/", response_model=JobOut)
async def create(body: JobIn):
url = body.url.strip()
if not url:
raise HTTPException(status_code=400, detail="URL is empty")
job_id = _uuid.uuid4().hex
j = await create_job(job_id, url)
jobqueue.submit(
job_id,
{
"section_id": body.section_id,
"keep_video": body.keep_video,
"summary_max_sentences": body.summary_max_sentences,
"model_path": body.model_path,
},
)
return _to_out(j)
@router.get("/", response_model=list[JobOut])
async def list_all(limit: int = 50):
return [_to_out(j) for j in await list_jobs(limit)]
@router.get("/{job_id}", response_model=JobOut)
async def get_one(job_id: str):
j = await get_job(job_id)
if j is None:
raise HTTPException(status_code=404, detail="Job not found")
return _to_out(j)
@router.post("/{job_id}/cancel")
async def cancel(job_id: str):
j = await get_job(job_id)
if j is None:
raise HTTPException(status_code=404, detail="Job not found")
if j.status not in ("queued", "running"):
raise HTTPException(status_code=400, detail=f"Job is {j.status}")
jobqueue.request_cancel(job_id)
return {"ok": True, "note": "остановится после текущего элемента"}
@router.delete("/{job_id}")
async def delete(job_id: str):
j = await get_job(job_id)
if j is None:
raise HTTPException(status_code=404, detail="Job not found")
if j.status in ("queued", "running"):
raise HTTPException(status_code=400, detail="Сначала отмените задачу")
await delete_job(job_id)
return {"ok": True}