Files
LLM-infa/api/route/videos.py
jze9 bf854f2d6e Разделы, фильтры и просмотр текстов в UI; опция «не хранить видео»
- таблица sections (дерево через parent_id), у видео section_id
- API: CRUD /sections, фильтры /videos (поиск, раздел с потомками, метод),
  GET /videos/{id}/text, PATCH раздела, DELETE вместе с файлами
- pipeline: keep_video=false удаляет mp4 после расшифровки, section_id
- UI: панель разделов с вложенностью и счётчиками, поиск, фильтр по методу,
  диалоги полного текста/выжимки, привязка к разделу, удаление,
  переключатель «Сохранять видео на диске»
- compose: bind-mount ./web для правок без пересборки

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

133 lines
4.3 KiB
Python
Raw Permalink 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 datetime import datetime
from pathlib import Path
from typing import List, Literal
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from api.db.section import list_sections
from api.db.video import (
delete_video,
get_video,
list_videos,
set_video_section,
)
router = APIRouter(prefix="/videos", tags=["videos"])
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
class VideoOut(BaseModel):
uuid: str
source_url: str
section_id: int | None
title: str
video_path: str
text_full_path: str
text_summary_path: str
transcription_method: str
created_at: datetime
class SectionPatch(BaseModel):
section_id: int | None
def _to_out(v) -> VideoOut:
return VideoOut(
uuid=v.uuid,
source_url=v.source_url,
section_id=v.section_id,
title=v.title,
video_path=v.video_path,
text_full_path=v.text_full_path,
text_summary_path=v.text_summary_path,
transcription_method=v.transcription_method,
created_at=v.created_at,
)
async def _descendant_ids(section_id: int) -> list[int]:
"""Раздел + все его потомки (фильтр по разделу захватывает подразделы)."""
sections = await list_sections()
children: dict[int | None, list[int]] = {}
for s in sections:
children.setdefault(s["parent_id"], []).append(s["id"])
ids, stack = [], [section_id]
while stack:
cur = stack.pop()
ids.append(cur)
stack.extend(children.get(cur, []))
return ids
@router.get("/", response_model=List[VideoOut])
async def list_all(
q: str | None = Query(default=None, description="Поиск по названию/URL"),
section_id: int | None = Query(default=None, description="Раздел (включая подразделы)"),
method: str | None = Query(default=None, description="Префикс метода: subtitles | vosk"),
):
section_ids = await _descendant_ids(section_id) if section_id is not None else None
return [
_to_out(v)
for v in await list_videos(q=q, section_ids=section_ids, method=method)
]
@router.get("/{video_uuid}", response_model=VideoOut)
async def get_one(video_uuid: str):
v = await get_video(video_uuid)
if not v:
raise HTTPException(status_code=404, detail="Video not found")
return _to_out(v)
@router.get("/{video_uuid}/text")
async def get_text(video_uuid: str, kind: Literal["full", "summary"] = "full"):
v = await get_video(video_uuid)
if not v:
raise HTTPException(status_code=404, detail="Video not found")
rel = v.text_full_path if kind == "full" else v.text_summary_path
if not rel:
raise HTTPException(status_code=404, detail="Text file is not recorded")
p = (_PROJECT_ROOT / rel).resolve()
try:
p.relative_to(_PROJECT_ROOT)
except ValueError:
raise HTTPException(status_code=400, detail="Bad text path")
if not p.exists():
raise HTTPException(status_code=404, detail=f"File missing on disk: {rel}")
return {"uuid": video_uuid, "kind": kind, "text": p.read_text(encoding="utf-8", errors="ignore")}
@router.patch("/{video_uuid}", response_model=VideoOut)
async def patch_section(video_uuid: str, body: SectionPatch):
v = await set_video_section(video_uuid, body.section_id)
if not v:
raise HTTPException(status_code=404, detail="Video not found")
return _to_out(v)
@router.delete("/{video_uuid}")
async def delete_one(video_uuid: str, delete_files: bool = True):
v = await get_video(video_uuid)
if not v:
raise HTTPException(status_code=404, detail="Video not found")
if delete_files:
for rel in (v.video_path, v.text_full_path, v.text_summary_path):
if not rel:
continue
p = (_PROJECT_ROOT / rel).resolve()
try:
p.relative_to(_PROJECT_ROOT)
except ValueError:
continue
p.unlink(missing_ok=True)
# сопутствующие субтитры .vtt рядом с видео
for p in (_PROJECT_ROOT / "data" / "videos").glob(f"{video_uuid}*.vtt"):
p.unlink(missing_ok=True)
await delete_video(video_uuid)
return {"ok": True}