Files
freepik-api/app/services/freepik_client.py

378 lines
10 KiB
Python
Raw Normal View History

import asyncio
import logging
from typing import Any, Optional
import httpx
from app.config import settings
logger = logging.getLogger(__name__)
_client: Optional[httpx.AsyncClient] = None
def get_client() -> httpx.AsyncClient:
if _client is None:
raise RuntimeError('Freepik client not initialized')
return _client
async def create_client() -> httpx.AsyncClient:
global _client
_client = httpx.AsyncClient(
base_url=settings.freepik_base_url,
headers={
'x-freepik-api-key': settings.freepik_api_key,
'Accept': 'application/json',
},
timeout=httpx.Timeout(60.0, connect=10.0),
)
return _client
async def close_client():
global _client
if _client:
await _client.aclose()
_client = None
async def _request(
method: str,
path: str,
*,
json: Optional[dict] = None,
data: Optional[dict] = None,
files: Optional[dict] = None,
params: Optional[dict] = None,
max_retries: int = 3,
) -> dict[str, Any]:
"""Make an authenticated request to Freepik API with retry on 429."""
client = get_client()
for attempt in range(max_retries):
kwargs: dict[str, Any] = {}
if json is not None:
kwargs['json'] = json
if data is not None:
kwargs['data'] = data
if files is not None:
kwargs['files'] = files
if params is not None:
kwargs['params'] = params
response = await client.request(method, path, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 2 ** attempt))
logger.warning(f'Rate limited, retrying in {retry_after}s (attempt {attempt + 1})')
await asyncio.sleep(retry_after)
continue
response.raise_for_status()
if response.headers.get('content-type', '').startswith('application/json'):
return response.json()
return {'raw': response.content}
raise httpx.HTTPStatusError(
'Rate limit exceeded after retries',
request=response.request,
response=response,
)
async def _request_raw(
method: str,
path: str,
*,
json: Optional[dict] = None,
data: Optional[dict] = None,
files: Optional[dict] = None,
max_retries: int = 3,
) -> bytes:
"""Make a request and return raw bytes (for binary responses)."""
client = get_client()
for attempt in range(max_retries):
kwargs: dict[str, Any] = {}
if json is not None:
kwargs['json'] = json
if data is not None:
kwargs['data'] = data
if files is not None:
kwargs['files'] = files
response = await client.request(method, path, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 2 ** attempt))
logger.warning(f'Rate limited, retrying in {retry_after}s (attempt {attempt + 1})')
await asyncio.sleep(retry_after)
continue
response.raise_for_status()
return response.content
raise httpx.HTTPStatusError(
'Rate limit exceeded after retries',
request=response.request,
response=response,
)
def _strip_none(d: dict) -> dict:
"""Remove None values from a dict for clean API payloads."""
return {k: v for k, v in d.items() if v is not None}
# ---------------------------------------------------------------------------
# Image generation
# ---------------------------------------------------------------------------
async def generate_mystic(
prompt: str,
negative_prompt: Optional[str] = None,
resolution: Optional[str] = None,
styling: Optional[dict] = None,
seed: Optional[int] = None,
num_images: Optional[int] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'negative_prompt': negative_prompt,
'resolution': resolution,
'styling': styling,
'seed': seed,
'num_images': num_images,
})
return await _request('POST', '/v1/ai/mystic', json=payload)
async def generate_flux_dev(
prompt: str,
image: Optional[str] = None,
guidance_scale: Optional[float] = None,
num_images: Optional[int] = None,
seed: Optional[int] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'image': image,
'guidance_scale': guidance_scale,
'num_images': num_images,
'seed': seed,
})
return await _request('POST', '/v1/ai/text-to-image/flux-dev', json=payload)
async def generate_flux_pro(
prompt: str,
image: Optional[str] = None,
guidance_scale: Optional[float] = None,
seed: Optional[int] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'image': image,
'guidance_scale': guidance_scale,
'seed': seed,
})
return await _request('POST', '/v1/ai/text-to-image/flux-pro-1.1', json=payload)
async def generate_seedream(
prompt: str,
image: Optional[str] = None,
aspect_ratio: Optional[str] = None,
num_images: Optional[int] = None,
seed: Optional[int] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'image': image,
'aspect_ratio': aspect_ratio,
'num_images': num_images,
'seed': seed,
})
return await _request('POST', '/v1/ai/text-to-image/seedream', json=payload)
# ---------------------------------------------------------------------------
# Video generation
# ---------------------------------------------------------------------------
async def generate_video_kling(
image: str,
prompt: Optional[str] = None,
duration: Optional[str] = None,
aspect_ratio: Optional[str] = None,
) -> dict:
payload = _strip_none({
'image': image,
'prompt': prompt,
'duration': duration,
'aspect_ratio': aspect_ratio,
})
return await _request('POST', '/v1/ai/image-to-video/kling', json=payload)
async def generate_video_minimax(
prompt: str,
first_frame_image: Optional[str] = None,
subject_reference: Optional[str] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'first_frame_image': first_frame_image,
'subject_reference': subject_reference,
})
return await _request('POST', '/v1/ai/image-to-video/minimax', json=payload)
async def generate_video_seedance(
prompt: str,
image: Optional[str] = None,
duration: Optional[str] = None,
resolution: Optional[str] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'image': image,
'duration': duration,
'resolution': resolution,
})
return await _request('POST', '/v1/ai/image-to-video/seedance', json=payload)
# ---------------------------------------------------------------------------
# Image editing
# ---------------------------------------------------------------------------
async def upscale_creative(
image: str,
prompt: Optional[str] = None,
scale: Optional[int] = None,
creativity: Optional[float] = None,
resemblance: Optional[float] = None,
) -> dict:
payload = _strip_none({
'image': image,
'prompt': prompt,
'scale': scale,
'creativity': creativity,
'resemblance': resemblance,
})
return await _request('POST', '/v1/ai/upscale/creative', json=payload)
async def upscale_precision(
image: str,
scale: Optional[int] = None,
) -> dict:
payload = _strip_none({
'image': image,
'scale': scale,
})
return await _request('POST', '/v1/ai/upscale/precision', json=payload)
async def relight_image(
image: str,
prompt: Optional[str] = None,
light_source: Optional[str] = None,
intensity: Optional[float] = None,
) -> dict:
payload = _strip_none({
'image': image,
'prompt': prompt,
'light_source': light_source,
'intensity': intensity,
})
return await _request('POST', '/v1/ai/relight', json=payload)
async def style_transfer(
image: str,
style_reference: str,
strength: Optional[float] = None,
) -> dict:
payload = _strip_none({
'image': image,
'style_reference': style_reference,
'strength': strength,
})
return await _request('POST', '/v1/ai/style-transfer', json=payload)
async def expand_image(
image: str,
prompt: Optional[str] = None,
direction: Optional[str] = None,
) -> dict:
payload = _strip_none({
'image': image,
'prompt': prompt,
'direction': direction,
})
return await _request('POST', '/v1/ai/expand', json=payload)
async def inpaint(
image: str,
mask: str,
prompt: str,
) -> dict:
payload = {
'image': image,
'mask': mask,
'prompt': prompt,
}
return await _request('POST', '/v1/ai/inpaint', json=payload)
# ---------------------------------------------------------------------------
# Utilities
# ---------------------------------------------------------------------------
async def remove_background(image: str) -> bytes:
return await _request_raw('POST', '/v1/ai/remove-background', json={'image': image})
async def classify_image(image: str) -> dict:
return await _request('POST', '/v1/ai/classifier', json={'image': image})
async def isolate_audio(audio: str) -> dict:
return await _request('POST', '/v1/ai/audio-isolate', json={'audio': audio})
async def generate_icon(
prompt: str,
color: Optional[str] = None,
shape: Optional[str] = None,
style: Optional[str] = None,
) -> dict:
payload = _strip_none({
'prompt': prompt,
'color': color,
'shape': shape,
'style': style,
})
return await _request('POST', '/v1/ai/icon', json=payload)
# ---------------------------------------------------------------------------
# Task management
# ---------------------------------------------------------------------------
async def get_task_status(task_id: str) -> dict:
return await _request('GET', f'/v1/ai/tasks/{task_id}')
async def check_api_key() -> bool:
"""Verify the API key is valid by making a lightweight request."""
try:
client = get_client()
response = await client.get('/v1/ai/tasks', params={'limit': 1})
return response.status_code != 401
except Exception:
return False