- таблица 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>
135 lines
4.1 KiB
Python
135 lines
4.1 KiB
Python
"""Thin httpx wrapper for the FastAPI backend.
|
|
|
|
The browser never talks to FastAPI directly — Flet's Python process is the
|
|
only client of the API. So no CORS, no auth surface exposed publicly.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from web.config import API_BASE_URL, HTTP_TIMEOUT
|
|
|
|
|
|
class ApiError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class ApiClient:
|
|
def __init__(self, base_url: str = API_BASE_URL, timeout: float = HTTP_TIMEOUT):
|
|
self._client = httpx.Client(base_url=base_url, timeout=timeout)
|
|
|
|
def close(self) -> None:
|
|
self._client.close()
|
|
|
|
# --- Settings (persisted server-side in Postgres) ---
|
|
|
|
def get_settings(self) -> dict:
|
|
return self._get("/settings/")
|
|
|
|
def save_settings(self, values: dict) -> dict:
|
|
return self._request("PUT", "/settings/", values)
|
|
|
|
# --- LLM ---
|
|
|
|
def llm_models(self, base_url: str, api_key: str) -> list[dict]:
|
|
data = self._post(
|
|
"/llm/models",
|
|
{"base_url": base_url, "api_key": api_key},
|
|
)
|
|
return data.get("models", [])
|
|
|
|
def llm_test(self, base_url: str, api_key: str, model: str) -> dict:
|
|
return self._post(
|
|
"/llm/test",
|
|
{"base_url": base_url, "api_key": api_key, "model": model},
|
|
)
|
|
|
|
# --- Jobs (видео и плейлисты, в фоне) ---
|
|
|
|
def create_job(self, payload: dict) -> dict:
|
|
return self._post("/jobs/", payload)
|
|
|
|
def list_jobs(self) -> list[dict]:
|
|
return self._get("/jobs/")
|
|
|
|
def cancel_job(self, job_id: str) -> dict:
|
|
return self._post(f"/jobs/{job_id}/cancel", None)
|
|
|
|
def delete_job(self, job_id: str) -> dict:
|
|
return self._request("DELETE", f"/jobs/{job_id}")
|
|
|
|
# --- Videos ---
|
|
|
|
def list_videos(
|
|
self,
|
|
q: str | None = None,
|
|
section_id: int | None = None,
|
|
method: str | None = None,
|
|
) -> list[dict]:
|
|
params: dict = {}
|
|
if q:
|
|
params["q"] = q
|
|
if section_id is not None:
|
|
params["section_id"] = section_id
|
|
if method:
|
|
params["method"] = method
|
|
return self._get("/videos/", params=params)
|
|
|
|
def video_text(self, uuid: str, kind: str) -> dict:
|
|
return self._get(f"/videos/{uuid}/text", params={"kind": kind})
|
|
|
|
def set_video_section(self, uuid: str, section_id: int | None) -> dict:
|
|
return self._request("PATCH", f"/videos/{uuid}", {"section_id": section_id})
|
|
|
|
def delete_video(self, uuid: str) -> dict:
|
|
return self._request("DELETE", f"/videos/{uuid}")
|
|
|
|
# --- Sections ---
|
|
|
|
def list_sections(self) -> list[dict]:
|
|
return self._get("/sections/")
|
|
|
|
def create_section(self, name: str, parent_id: int | None) -> dict:
|
|
return self._post("/sections/", {"name": name, "parent_id": parent_id})
|
|
|
|
def delete_section(self, section_id: int) -> dict:
|
|
return self._request("DELETE", f"/sections/{section_id}")
|
|
|
|
# --- internals ---
|
|
|
|
def _get(self, path: str, params: dict | None = None) -> dict | list:
|
|
try:
|
|
r = self._client.get(path, params=params)
|
|
except httpx.HTTPError as e:
|
|
raise ApiError(f"Сеть: {e}")
|
|
return self._unwrap(r)
|
|
|
|
def _post(self, path: str, json: dict | None) -> dict:
|
|
try:
|
|
r = self._client.post(path, json=json)
|
|
except httpx.HTTPError as e:
|
|
raise ApiError(f"Сеть: {e}")
|
|
return self._unwrap(r)
|
|
|
|
def _request(self, method: str, path: str, json: dict | None = None) -> dict:
|
|
try:
|
|
r = self._client.request(method, path, json=json)
|
|
except httpx.HTTPError as e:
|
|
raise ApiError(f"Сеть: {e}")
|
|
return self._unwrap(r)
|
|
|
|
@staticmethod
|
|
def _unwrap(r: httpx.Response):
|
|
try:
|
|
data = r.json()
|
|
except Exception:
|
|
data = None
|
|
if r.is_success:
|
|
return data
|
|
detail = ""
|
|
if isinstance(data, dict):
|
|
detail = str(data.get("detail") or data.get("message") or "")
|
|
if not detail:
|
|
detail = r.text[:300] if r.text else r.reason_phrase
|
|
raise ApiError(f"HTTP {r.status_code}: {detail}")
|