feat: implement vertical crop, local LLM deepseek-v4-flash, multi-threading, word-level pink box highlight subtitles, and Nextcloud scan sync
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""Thin MuAPI client: submit a job, poll until it finishes, return the result."""
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .config import (
|
||||
MUAPI_BASE_URL,
|
||||
POLL_INTERVAL_SECONDS,
|
||||
POLL_TIMEOUT_SECONDS,
|
||||
require_api_key,
|
||||
)
|
||||
|
||||
|
||||
class MuAPIError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _headers() -> Dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": require_api_key(),
|
||||
}
|
||||
|
||||
|
||||
def submit(endpoint: str, payload: Dict[str, Any], retries: int = 3) -> str:
|
||||
"""POST to /api/v1/{endpoint} and return the request_id; retry transient errors."""
|
||||
url = f"{MUAPI_BASE_URL}/{endpoint.lstrip('/')}"
|
||||
last_err: Optional[Exception] = None
|
||||
for _ in range(retries):
|
||||
try:
|
||||
resp = requests.post(url, json=payload, headers=_headers(), timeout=120)
|
||||
if resp.status_code >= 400:
|
||||
raise MuAPIError(f"{endpoint} submit failed [{resp.status_code}]: {resp.text}")
|
||||
data = resp.json()
|
||||
request_id = data.get("request_id") or data.get("id")
|
||||
if not request_id:
|
||||
raise MuAPIError(f"{endpoint} response had no request_id: {data}")
|
||||
return str(request_id)
|
||||
except (requests.Timeout, requests.ConnectionError) as e:
|
||||
last_err = e
|
||||
time.sleep(2)
|
||||
raise MuAPIError(f"{endpoint} submit failed after {retries} retries: {last_err}")
|
||||
|
||||
|
||||
def fetch_result(request_id: str, retries: int = 3) -> Dict[str, Any]:
|
||||
"""GET the latest result for a request_id; retry on transient timeouts."""
|
||||
url = f"{MUAPI_BASE_URL}/predictions/{request_id}/result"
|
||||
last_err: Optional[Exception] = None
|
||||
for _ in range(retries):
|
||||
try:
|
||||
resp = requests.get(url, headers=_headers(), timeout=90)
|
||||
if resp.status_code >= 400:
|
||||
raise MuAPIError(f"poll failed [{resp.status_code}]: {resp.text}")
|
||||
return resp.json()
|
||||
except (requests.Timeout, requests.ConnectionError) as e:
|
||||
last_err = e
|
||||
time.sleep(2)
|
||||
raise MuAPIError(f"poll failed after {retries} retries: {last_err}")
|
||||
|
||||
|
||||
def poll(
|
||||
request_id: str,
|
||||
interval: float = POLL_INTERVAL_SECONDS,
|
||||
timeout: float = POLL_TIMEOUT_SECONDS,
|
||||
label: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Block until the prediction is done; return the final payload."""
|
||||
deadline = time.time() + timeout
|
||||
last_status = None
|
||||
while time.time() < deadline:
|
||||
data = fetch_result(request_id)
|
||||
status = (data.get("status") or "").lower()
|
||||
if status and status != last_status:
|
||||
print(f"[muapi] {label or request_id}: {status}", flush=True)
|
||||
last_status = status
|
||||
|
||||
if status in ("completed", "succeeded", "success"):
|
||||
return data
|
||||
if status in ("failed", "error"):
|
||||
raise MuAPIError(f"{label or request_id} failed: {data}")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
raise MuAPIError(f"{label or request_id} timed out after {timeout}s")
|
||||
|
||||
|
||||
def run(
|
||||
endpoint: str,
|
||||
payload: Dict[str, Any],
|
||||
label: Optional[str] = None,
|
||||
interval: float = POLL_INTERVAL_SECONDS,
|
||||
timeout: float = POLL_TIMEOUT_SECONDS,
|
||||
) -> Dict[str, Any]:
|
||||
"""Submit then poll. Returns the final result payload."""
|
||||
request_id = submit(endpoint, payload)
|
||||
return poll(request_id, interval=interval, timeout=timeout, label=label or endpoint)
|
||||
Reference in New Issue
Block a user