refactor: rename project from Freepik to Magnific

Rename all identifiers, strings, file names, env vars, CLI entry point,
ASCII banner, and API endpoint to reflect the company rebrand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 13:06:16 +02:00
parent e013db9065
commit 941fd14ccf
24 changed files with 294 additions and 294 deletions
View File
+116
View File
@@ -0,0 +1,116 @@
"""Magnific HTTP client with authentication, error handling, and download support."""
from __future__ import annotations
from typing import Any, Optional
import httpx
from magnific_cli import __version__
BASE_URL = "https://api.magnific.ai"
DEFAULT_TIMEOUT = 60.0
class MagnificAPIError(Exception):
"""Raised when the Magnific API returns an error response."""
def __init__(self, message: str, status_code: Optional[int] = None, raw: Optional[dict] = None):
super().__init__(message)
self.status_code = status_code
self.raw = raw or {}
@classmethod
def from_response(cls, response: httpx.Response) -> "MagnificAPIError":
try:
body = response.json()
except Exception:
body = {}
message = (
body.get("message")
or body.get("error", {}).get("message")
or body.get("errors", [{}])[0].get("message")
or f"HTTP {response.status_code}"
)
# Append individual field validation errors when present
invalid = body.get("invalid_params", [])
if invalid:
details = "\n".join(
f"{p.get('field', '?')}: {p.get('reason', '')}" for p in invalid
)
message = f"{message}\n\n{details}"
hints = {
401: "Check your API key — set MAGNIFIC_API_KEY or use --api-key.",
403: "Your plan may not support this feature. Check your Magnific subscription.",
429: "Rate limit exceeded. Please wait before retrying.",
}
hint = hints.get(response.status_code)
if hint:
message = f"{message}\n\nHint: {hint}"
return cls(message, status_code=response.status_code, raw=body)
class MagnificClient:
"""Thin synchronous HTTP wrapper around the Magnific API."""
def __init__(
self,
api_key: str,
base_url: str = BASE_URL,
timeout: float = DEFAULT_TIMEOUT,
) -> None:
self._client = httpx.Client(
base_url=base_url,
headers={
"x-magnific-api-key": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": f"magnific-cli/{__version__}",
},
timeout=httpx.Timeout(timeout),
)
def post(self, path: str, json: dict[str, Any]) -> dict[str, Any]:
try:
response = self._client.post(path, json=json)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
raise MagnificAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise MagnificAPIError(f"Network error: {exc}") from exc
def post_multipart(self, path: str, data: dict[str, Any], files: dict[str, Any]) -> dict[str, Any]:
"""POST with multipart/form-data (for file uploads)."""
headers = {k: v for k, v in self._client.headers.items() if k.lower() != "content-type"}
try:
response = self._client.post(path, data=data, files=files, headers=headers)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
raise MagnificAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise MagnificAPIError(f"Network error: {exc}") from exc
def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
try:
response = self._client.get(path, params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
raise MagnificAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise MagnificAPIError(f"Network error: {exc}") from exc
def __enter__(self) -> "MagnificClient":
return self
def __exit__(self, *args: Any) -> None:
self._client.close()
def close(self) -> None:
self._client.close()
+98
View File
@@ -0,0 +1,98 @@
"""Image editing API methods: expand, relight, style-transfer, icons."""
from __future__ import annotations
from typing import Any, Optional, Tuple
from magnific_cli.api.client import MagnificClient
from magnific_cli.api.models import IconStyle, get_output_urls, get_status, get_task_id
class EditAPI:
def __init__(self, client: MagnificClient) -> None:
self._client = client
# ------------------------------------------------------------------
# Icon generation
# ------------------------------------------------------------------
def generate_icon(
self,
prompt: str,
style: IconStyle = IconStyle.COLOR,
num_inference_steps: int = 30,
guidance_scale: float = 7.5,
seed: Optional[int] = None,
) -> str:
"""Submit a text-to-icon task. Returns task_id."""
payload: dict[str, Any] = {
"prompt": prompt,
"style": style.value,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
}
if seed is not None:
payload["seed"] = seed
raw = self._client.post("/v1/ai/text-to-icon", json=payload)
return get_task_id(raw)
def icon_status(self, task_id: str) -> Tuple[str, dict[str, Any]]:
raw = self._client.get(f"/v1/ai/text-to-icon/{task_id}")
return get_status(raw), raw
def render_icon(self, task_id: str, fmt: str = "png") -> str:
"""Get the download URL for a completed icon in PNG or SVG format."""
raw = self._client.post(f"/v1/ai/text-to-icon/{task_id}/render/{fmt}", json={})
data = raw.get("data", raw)
return data.get("url") or data.get("download_url") or ""
# ------------------------------------------------------------------
# Relight
# ------------------------------------------------------------------
def relight_submit(
self,
image_b64: str,
prompt: Optional[str] = None,
style: Optional[str] = None,
) -> str:
payload: dict[str, Any] = {"image": image_b64}
if prompt:
payload["prompt"] = prompt
if style:
payload["style"] = style
raw = self._client.post("/v1/ai/image-relight", json=payload)
return get_task_id(raw)
def relight_status(self, task_id: str) -> Tuple[str, dict[str, Any]]:
raw = self._client.get(f"/v1/ai/image-relight/{task_id}")
return get_status(raw), raw
# ------------------------------------------------------------------
# Style Transfer
# ------------------------------------------------------------------
def style_transfer_submit(
self,
content_image_b64: str,
style_image_b64: str,
strength: Optional[float] = None,
) -> str:
payload: dict[str, Any] = {
"content_image": content_image_b64,
"style_image": style_image_b64,
}
if strength is not None:
payload["strength"] = strength
raw = self._client.post("/v1/ai/image-style-transfer", json=payload)
return get_task_id(raw)
def style_transfer_status(self, task_id: str) -> Tuple[str, dict[str, Any]]:
raw = self._client.get(f"/v1/ai/image-style-transfer/{task_id}")
return get_status(raw), raw
def get_output_urls(self, raw: dict[str, Any]) -> list[str]:
return get_output_urls(raw)
+105
View File
@@ -0,0 +1,105 @@
"""Image generation and analysis API methods."""
from __future__ import annotations
from typing import Any, Optional, Tuple
from magnific_cli.api.client import MagnificClient
from magnific_cli.api.models import (
IMAGE_POST_ENDPOINTS,
IMAGE_STATUS_ENDPOINTS,
ImageModel,
get_output_urls,
get_status,
get_task_id,
)
class ImageAPI:
def __init__(self, client: MagnificClient) -> None:
self._client = client
def generate(self, model: ImageModel, payload: dict[str, Any]) -> str:
"""Submit a generation task. Returns task_id."""
endpoint = IMAGE_POST_ENDPOINTS[model]
raw = self._client.post(endpoint, json=payload)
return get_task_id(raw)
def get_status(self, model: ImageModel, task_id: str) -> Tuple[str, dict[str, Any]]:
"""Poll status. Returns (status_str, raw_response)."""
endpoint = IMAGE_STATUS_ENDPOINTS[model].format(task_id=task_id)
raw = self._client.get(endpoint)
return get_status(raw), raw
def get_output_urls(self, raw: dict[str, Any]) -> list[str]:
return get_output_urls(raw)
# ------------------------------------------------------------------
# Image-to-prompt (describe)
# ------------------------------------------------------------------
def describe_submit(self, image_b64: str) -> str:
"""Submit image-to-prompt task. Returns task_id."""
raw = self._client.post("/v1/ai/image-to-prompt", json={"image": image_b64})
return get_task_id(raw)
def describe_status(self, task_id: str) -> Tuple[str, dict[str, Any]]:
raw = self._client.get(f"/v1/ai/image-to-prompt/{task_id}")
return get_status(raw), raw
def get_prompt_text(self, raw: dict[str, Any]) -> str:
"""Extract generated prompt text from a completed describe response."""
data = raw.get("data", raw)
return (
data.get("prompt")
or data.get("description")
or data.get("text")
or data.get("result", {}).get("prompt", "")
or ""
)
# ------------------------------------------------------------------
# Image expansion (outpainting)
# ------------------------------------------------------------------
def expand_submit(
self,
model: str,
image_b64: str,
left: int = 0,
right: int = 0,
top: int = 0,
bottom: int = 0,
prompt: Optional[str] = None,
seed: Optional[int] = None,
) -> str:
payload: dict[str, Any] = {
"image": image_b64,
"left": left,
"right": right,
"top": top,
"bottom": bottom,
}
if prompt:
payload["prompt"] = prompt
if seed is not None:
payload["seed"] = seed
endpoint_map = {
"flux-pro": "/v1/ai/image-expand/flux-pro",
"ideogram": "/v1/ai/image-expand/ideogram",
"seedream-v4-5": "/v1/ai/image-expand/seedream-v4-5",
}
endpoint = endpoint_map.get(model, "/v1/ai/image-expand/flux-pro")
raw = self._client.post(endpoint, json=payload)
return get_task_id(raw)
def expand_status(self, model: str, task_id: str) -> Tuple[str, dict[str, Any]]:
endpoint_map = {
"flux-pro": "/v1/ai/image-expand/flux-pro",
"ideogram": "/v1/ai/image-expand/ideogram",
"seedream-v4-5": "/v1/ai/image-expand/seedream-v4-5",
}
base = endpoint_map.get(model, "/v1/ai/image-expand/flux-pro")
raw = self._client.get(f"{base}/{task_id}")
return get_status(raw), raw
+246
View File
@@ -0,0 +1,246 @@
"""Enums, endpoint maps, and response normalization utilities."""
from __future__ import annotations
from enum import Enum
from typing import Any
# ---------------------------------------------------------------------------
# Model enums
# ---------------------------------------------------------------------------
class ImageModel(str, Enum):
MYSTIC = "mystic"
FLUX_KONTEXT_PRO = "flux-kontext-pro"
FLUX_2_PRO = "flux-2-pro"
FLUX_2_TURBO = "flux-2-turbo"
FLUX_PRO_1_1 = "flux-pro-1.1"
SEEDREAM_V4 = "seedream-v4"
SEEDREAM_V4_5 = "seedream-v4-5"
IDEOGRAM_V2 = "ideogram-v2"
class VideoModel(str, Enum):
KLING_O1_PRO = "kling-o1-pro"
KLING_O1_STD = "kling-o1-std"
KLING_ELEMENTS_PRO = "kling-elements-pro"
KLING_ELEMENTS_STD = "kling-elements-std"
MINIMAX_HAILUO = "minimax-hailuo"
class UpscaleMode(str, Enum):
CREATIVE = "creative"
PRECISION = "precision"
PRECISION_V2 = "precision-v2"
class VideoUpscaleMode(str, Enum):
STANDARD = "standard"
TURBO = "turbo"
class AspectRatio(str, Enum):
LANDSCAPE = "16:9"
PORTRAIT = "9:16"
SQUARE = "1:1"
CLASSIC = "4:3"
WIDE = "21:9"
class IconStyle(str, Enum):
SOLID = "solid"
OUTLINE = "outline"
COLOR = "color"
FLAT = "flat"
STICKER = "sticker"
# ---------------------------------------------------------------------------
# Endpoint maps
# ---------------------------------------------------------------------------
IMAGE_POST_ENDPOINTS: dict[ImageModel, str] = {
ImageModel.MYSTIC: "/v1/ai/mystic",
ImageModel.FLUX_KONTEXT_PRO: "/v1/ai/text-to-image/flux-kontext-pro",
ImageModel.FLUX_2_PRO: "/v1/ai/text-to-image/flux-2-pro",
ImageModel.FLUX_2_TURBO: "/v1/ai/text-to-image/flux-2-turbo",
ImageModel.FLUX_PRO_1_1: "/v1/ai/text-to-image/flux-pro-v1-1",
ImageModel.SEEDREAM_V4: "/v1/ai/text-to-image/seedream-v4",
ImageModel.SEEDREAM_V4_5: "/v1/ai/text-to-image/seedream-v4-5",
ImageModel.IDEOGRAM_V2: "/v1/ai/text-to-image/ideogram-v2",
}
IMAGE_STATUS_ENDPOINTS: dict[ImageModel, str] = {
ImageModel.MYSTIC: "/v1/ai/mystic/{task_id}",
ImageModel.FLUX_KONTEXT_PRO: "/v1/ai/text-to-image/flux-kontext-pro/{task_id}",
ImageModel.FLUX_2_PRO: "/v1/ai/text-to-image/flux-2-pro/{task_id}",
ImageModel.FLUX_2_TURBO: "/v1/ai/text-to-image/flux-2-turbo/{task_id}",
ImageModel.FLUX_PRO_1_1: "/v1/ai/text-to-image/flux-pro-v1-1/{task_id}",
ImageModel.SEEDREAM_V4: "/v1/ai/text-to-image/seedream-v4/{task_id}",
ImageModel.SEEDREAM_V4_5: "/v1/ai/text-to-image/seedream-v4-5/{task_id}",
ImageModel.IDEOGRAM_V2: "/v1/ai/text-to-image/ideogram-v2/{task_id}",
}
VIDEO_POST_ENDPOINTS: dict[VideoModel, str] = {
VideoModel.KLING_O1_PRO: "/v1/ai/image-to-video/kling-o1-pro",
VideoModel.KLING_O1_STD: "/v1/ai/image-to-video/kling-o1-std",
VideoModel.KLING_ELEMENTS_PRO: "/v1/ai/image-to-video/kling-elements-pro",
VideoModel.KLING_ELEMENTS_STD: "/v1/ai/image-to-video/kling-elements-std",
VideoModel.MINIMAX_HAILUO: "/v1/ai/image-to-video/minimax-hailuo-02-1080p",
}
VIDEO_STATUS_ENDPOINTS: dict[VideoModel, str] = {
VideoModel.KLING_O1_PRO: "/v1/ai/image-to-video/kling-o1/{task_id}",
VideoModel.KLING_O1_STD: "/v1/ai/image-to-video/kling-o1/{task_id}",
VideoModel.KLING_ELEMENTS_PRO: "/v1/ai/image-to-video/kling-elements-pro/{task_id}",
VideoModel.KLING_ELEMENTS_STD: "/v1/ai/image-to-video/kling-elements-std/{task_id}",
VideoModel.MINIMAX_HAILUO: "/v1/ai/image-to-video/minimax-hailuo-02-1080p/{task_id}",
}
# Per-model image input field name
VIDEO_IMAGE_FIELDS: dict[VideoModel, str] = {
VideoModel.KLING_O1_PRO: "first_frame",
VideoModel.KLING_O1_STD: "first_frame",
VideoModel.KLING_ELEMENTS_PRO: "images", # expects an array
VideoModel.KLING_ELEMENTS_STD: "images", # expects an array
VideoModel.MINIMAX_HAILUO: "image",
}
# Models that support an aspect_ratio parameter at all
VIDEO_ASPECT_RATIO_MODELS: set[VideoModel] = {
VideoModel.KLING_ELEMENTS_PRO,
VideoModel.KLING_ELEMENTS_STD,
VideoModel.MINIMAX_HAILUO,
}
# Of those, these require named slug aspect ratios instead of "W:H" strings
VIDEO_SLUG_ASPECT_RATIO_MODELS: set[VideoModel] = {
VideoModel.KLING_ELEMENTS_PRO,
VideoModel.KLING_ELEMENTS_STD,
}
_VIDEO_RATIO_TO_SLUG: dict[str, str] = {
"1:1": "square_1_1",
"16:9": "widescreen_16_9",
"9:16": "social_story_9_16",
}
def normalize_aspect_ratio_video(ratio: str, model: VideoModel) -> str:
"""Convert a user-facing aspect ratio to the format required by the video model."""
if model not in VIDEO_SLUG_ASPECT_RATIO_MODELS:
return ratio
return _VIDEO_RATIO_TO_SLUG.get(ratio, ratio)
UPSCALE_POST_ENDPOINTS: dict[UpscaleMode, str] = {
UpscaleMode.CREATIVE: "/v1/ai/image-upscaler",
UpscaleMode.PRECISION: "/v1/ai/image-upscaler-precision",
UpscaleMode.PRECISION_V2: "/v1/ai/image-upscaler-precision-v2",
}
UPSCALE_STATUS_ENDPOINTS: dict[UpscaleMode, str] = {
UpscaleMode.CREATIVE: "/v1/ai/image-upscaler/{task_id}",
UpscaleMode.PRECISION: "/v1/ai/image-upscaler-precision/{task_id}",
UpscaleMode.PRECISION_V2: "/v1/ai/image-upscaler-precision-v2/{task_id}",
}
VIDEO_UPSCALE_POST_ENDPOINTS: dict[VideoUpscaleMode, str] = {
VideoUpscaleMode.STANDARD: "/v1/ai/video-upscaler",
VideoUpscaleMode.TURBO: "/v1/ai/video-upscaler/turbo",
}
VIDEO_UPSCALE_STATUS_ENDPOINT = "/v1/ai/video-upscaler/{task_id}"
# ---------------------------------------------------------------------------
# Aspect ratio normalization
# ---------------------------------------------------------------------------
# Models that require named slug aspect ratios instead of "W:H" strings
SLUG_ASPECT_RATIO_MODELS: set[ImageModel] = {
ImageModel.MYSTIC,
ImageModel.FLUX_PRO_1_1,
ImageModel.SEEDREAM_V4,
ImageModel.SEEDREAM_V4_5,
}
# User-friendly "W:H" → API slug mapping
_RATIO_TO_SLUG: dict[str, str] = {
"1:1": "square_1_1",
"16:9": "widescreen_16_9",
"9:16": "social_story_9_16",
"4:3": "classic_4_3",
"3:4": "traditional_3_4",
"3:2": "standard_3_2",
"2:3": "portrait_2_3",
"2:1": "horizontal_2_1",
"1:2": "vertical_1_2",
"4:5": "social_post_4_5",
"21:9": "widescreen_16_9", # closest match
}
def normalize_aspect_ratio(ratio: str, model: ImageModel) -> str:
"""Convert a user-facing aspect ratio to the format required by the model."""
if model not in SLUG_ASPECT_RATIO_MODELS:
return ratio # free-form models accept "1:1" directly
slug = _RATIO_TO_SLUG.get(ratio)
if slug:
return slug
# Already a slug (user passed "square_1_1" directly) — pass through
return ratio
# ---------------------------------------------------------------------------
# Response normalization helpers
# ---------------------------------------------------------------------------
def get_task_id(raw: dict[str, Any]) -> str:
"""Extract task_id from any response shape."""
data = raw.get("data", raw)
task_id = data.get("task_id") or data.get("id") or raw.get("task_id") or raw.get("id")
if not task_id:
raise ValueError(f"No task_id found in response: {raw}")
return str(task_id)
def get_status(raw: dict[str, Any]) -> str:
"""Extract normalized status string from any response shape."""
data = raw.get("data", raw)
status = data.get("status") or raw.get("status") or "PENDING"
return status.upper().replace(" ", "_")
def get_output_urls(raw: dict[str, Any]) -> list[str]:
"""Extract all output file URLs from a completed task response."""
data = raw.get("data", raw)
# Try common key names in order of likelihood
for key in ("generated", "output", "outputs", "result", "results", "images", "videos"):
items = data.get(key)
if items is None:
continue
if isinstance(items, list) and items:
urls = []
for item in items:
if isinstance(item, dict):
url = item.get("url") or item.get("download_url") or item.get("src")
if url:
urls.append(url)
elif isinstance(item, str):
urls.append(item)
if urls:
return urls
elif isinstance(items, dict):
url = items.get("url") or items.get("download_url") or items.get("src")
if url:
return [url]
elif isinstance(items, str):
return [items]
# Fallback: top-level url field
if "url" in data:
return [data["url"]]
return []
+88
View File
@@ -0,0 +1,88 @@
"""Image and video upscaling API methods."""
from __future__ import annotations
from typing import Any, Optional, Tuple
from magnific_cli.api.client import MagnificClient
from magnific_cli.api.models import (
UPSCALE_POST_ENDPOINTS,
UPSCALE_STATUS_ENDPOINTS,
VIDEO_UPSCALE_POST_ENDPOINTS,
VIDEO_UPSCALE_STATUS_ENDPOINT,
UpscaleMode,
VideoUpscaleMode,
get_output_urls,
get_status,
get_task_id,
)
class UpscaleAPI:
def __init__(self, client: MagnificClient) -> None:
self._client = client
# ------------------------------------------------------------------
# Image upscaling
# ------------------------------------------------------------------
def upscale_image(
self,
mode: UpscaleMode,
image_b64: str,
scale_factor: Optional[str] = None,
creativity: Optional[int] = None,
prompt: Optional[str] = None,
seed: Optional[int] = None,
) -> str:
"""Submit an image upscale task. Returns task_id."""
payload: dict[str, Any] = {"image": image_b64}
if scale_factor:
# Convert "2x" → 2, "4x" → 4
factor = scale_factor.rstrip("xX")
try:
payload["scale_factor"] = int(factor)
except ValueError:
payload["scale_factor"] = 2
if mode == UpscaleMode.CREATIVE:
if creativity is not None:
payload["creativity"] = creativity
if prompt:
payload["prompt"] = prompt
if seed is not None:
payload["seed"] = seed
endpoint = UPSCALE_POST_ENDPOINTS[mode]
raw = self._client.post(endpoint, json=payload)
return get_task_id(raw)
def upscale_image_status(self, mode: UpscaleMode, task_id: str) -> Tuple[str, dict[str, Any]]:
endpoint = UPSCALE_STATUS_ENDPOINTS[mode].format(task_id=task_id)
raw = self._client.get(endpoint)
return get_status(raw), raw
# ------------------------------------------------------------------
# Video upscaling
# ------------------------------------------------------------------
def upscale_video(
self,
mode: VideoUpscaleMode,
video_b64: str,
) -> str:
"""Submit a video upscale task. Returns task_id."""
payload: dict[str, Any] = {"video": video_b64}
endpoint = VIDEO_UPSCALE_POST_ENDPOINTS[mode]
raw = self._client.post(endpoint, json=payload)
return get_task_id(raw)
def upscale_video_status(self, task_id: str) -> Tuple[str, dict[str, Any]]:
endpoint = VIDEO_UPSCALE_STATUS_ENDPOINT.format(task_id=task_id)
raw = self._client.get(endpoint)
return get_status(raw), raw
def get_output_urls(self, raw: dict[str, Any]) -> list[str]:
return get_output_urls(raw)
+73
View File
@@ -0,0 +1,73 @@
"""Video generation API methods."""
from __future__ import annotations
from typing import Any, Optional, Tuple
from magnific_cli.api.client import MagnificClient
from magnific_cli.api.models import (
VIDEO_POST_ENDPOINTS,
VIDEO_STATUS_ENDPOINTS,
VIDEO_IMAGE_FIELDS,
VIDEO_ASPECT_RATIO_MODELS,
VideoModel,
get_output_urls,
get_status,
get_task_id,
normalize_aspect_ratio_video,
)
class VideoAPI:
def __init__(self, client: MagnificClient) -> None:
self._client = client
def generate(
self,
model: VideoModel,
image_b64: str,
prompt: Optional[str] = None,
duration: int = 5,
aspect_ratio: str = "16:9",
seed: Optional[int] = None,
) -> str:
"""Submit an image-to-video task. Returns task_id."""
# Video API expects raw base64, not a data URI
if ";base64," in image_b64:
image_b64 = image_b64.split(";base64,", 1)[1]
image_field = VIDEO_IMAGE_FIELDS[model]
# kling-elements uses an array; all others use a scalar
if image_field == "images":
payload: dict[str, Any] = {"images": [image_b64]}
else:
payload = {image_field: image_b64}
if prompt:
payload["prompt"] = prompt
# minimax only supports duration=6; clamp silently
effective_duration = duration
if model == VideoModel.MINIMAX_HAILUO:
effective_duration = 6
payload["duration"] = str(effective_duration)
if aspect_ratio and model in VIDEO_ASPECT_RATIO_MODELS:
payload["aspect_ratio"] = normalize_aspect_ratio_video(aspect_ratio, model)
if seed is not None:
payload["seed"] = seed
endpoint = VIDEO_POST_ENDPOINTS[model]
raw = self._client.post(endpoint, json=payload)
return get_task_id(raw)
def get_status(self, model: VideoModel, task_id: str) -> Tuple[str, dict[str, Any]]:
"""Poll status. Returns (status_str, raw_response)."""
endpoint = VIDEO_STATUS_ENDPOINTS[model].format(task_id=task_id)
raw = self._client.get(endpoint)
return get_status(raw), raw
def get_output_urls(self, raw: dict[str, Any]) -> list[str]:
return get_output_urls(raw)