Рабочий пайплайн: YouTube -> транскрипция -> выжимка -> Postgres
- новый api/: /pipeline/process (yt-dlp, субтитры или Vosk, LLM/экстрактивная суммаризация), /videos, /llm; старый код перенесён в api_legacy/ - web/: Flet UI (flet 0.28.3 + flet-web, порт 8550) - Dockerfile.api: ffmpeg слоем из mwader/static-ffmpeg, pip с кэш-маунтом - requirements/pyproject: убраны moviepy, imageio-ffmpeg, битый asyncio - README с инструкцией запуска и загрузкой Vosk-модели Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
79
web/api_client.py
Normal file
79
web/api_client.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""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()
|
||||
|
||||
# --- LLM ---
|
||||
|
||||
def llm_status(self) -> dict:
|
||||
return self._get("/llm/config")
|
||||
|
||||
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},
|
||||
)
|
||||
|
||||
# --- Pipeline / videos ---
|
||||
|
||||
def pipeline_process(self, payload: dict) -> dict:
|
||||
return self._post("/pipeline/process", payload)
|
||||
|
||||
def list_videos(self) -> list[dict]:
|
||||
return self._get("/videos/")
|
||||
|
||||
# --- internals ---
|
||||
|
||||
def _get(self, path: str) -> dict | list:
|
||||
try:
|
||||
r = self._client.get(path)
|
||||
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)
|
||||
|
||||
@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}")
|
||||
Reference in New Issue
Block a user