feat: initial Freepik AI CLI

Sophisticated Python CLI for generating and manipulating images and
video via the Freepik API, built with typer + rich.

Commands:
- generate-image: text-to-image with 8 models (flux-2-pro, mystic, seedream, etc.)
- generate-video: image-to-video with 7 models (kling, minimax, runway, etc.)
- generate-icon: text-to-icon in solid/outline/color/flat/sticker styles
- upscale-image: 3 modes (precision-v2, precision, creative) + 2x/4x scale
- upscale-video: standard/turbo modes
- expand-image: outpainting with per-side pixel offsets
- relight: AI-controlled relighting (Premium)
- style-transfer: artistic style application (Premium)
- describe-image: reverse-engineer an image into a prompt
- config set/get/show/reset: configuration management

Features: Rich Live polling panel, exponential backoff, --wait/--no-wait,
auto-timestamped output filenames, streaming download with progress bar,
FREEPIK_API_KEY env var support, venv-based setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 10:56:45 +02:00
commit f24d138ab4
24 changed files with 2511 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Freepik HTTP client with authentication, error handling, and download support."""
from __future__ import annotations
from typing import Any, Optional
import httpx
from freepik_cli import __version__
BASE_URL = "https://api.freepik.com"
DEFAULT_TIMEOUT = 60.0
class FreepikAPIError(Exception):
"""Raised when the Freepik 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) -> "FreepikAPIError":
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}"
)
hints = {
401: "Check your API key — set FREEPIK_API_KEY or use --api-key.",
403: "Your plan may not support this feature. Check your Freepik subscription.",
422: "Invalid request parameters. Check the options you provided.",
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 FreepikClient:
"""Thin synchronous HTTP wrapper around the Freepik 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-freepik-api-key": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": f"freepik-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 FreepikAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise FreepikAPIError(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 FreepikAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise FreepikAPIError(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 FreepikAPIError.from_response(exc.response) from exc
except httpx.RequestError as exc:
raise FreepikAPIError(f"Network error: {exc}") from exc
def __enter__(self) -> "FreepikClient":
return self
def __exit__(self, *args: Any) -> None:
self._client.close()
def close(self) -> None:
self._client.close()