fix: align Freepik API paths with OpenAPI spec
Some checks failed
Build and Push Docker Image / build (push) Failing after 9s
Some checks failed
Build and Push Docker Image / build (push) Failing after 9s
The original implementation used guessed endpoint paths that don't match
the actual Freepik API. Key fixes based on their OpenAPI spec:
- Task polling is per-endpoint (e.g. GET /v1/ai/text-to-image/flux-dev/{task-id})
not a generic /v1/ai/tasks/{id}. freepik_client now returns TaskResult
with status_path, and task_tracker polls using that path.
- Fixed endpoint paths: flux-pro -> flux-pro-v1-1, upscale -> image-upscaler,
relight -> image-relight, style-transfer -> image-style-transfer,
expand -> image-expand/flux-pro, inpaint -> ideogram-image-edit,
remove-background -> beta/remove-background, classifier -> classifier/image,
audio-isolate -> audio-isolation, icon -> text-to-icon
- Fixed video paths: kling -> kling-o1-pro with kling-o1 status path,
minimax -> minimax-hailuo-02-1080p, seedance -> seedance-pro-1080p
- Fixed request schemas to match actual API params (e.g. scale_factor
not scale, reference_image not style_reference, image_url for bg removal)
- Fixed response parsing: status is uppercase (COMPLETED not completed),
results in data.generated[] array, classifier returns [{class_name, probability}]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,13 +15,21 @@ _tasks: dict[str, dict] = {}
|
||||
_poll_tasks: dict[str, asyncio.Task] = {}
|
||||
|
||||
|
||||
def submit(freepik_task_id: str, metadata: Optional[dict] = None) -> str:
|
||||
"""Register a Freepik task and start background polling."""
|
||||
def submit(freepik_task_id: str, status_path: str, metadata: Optional[dict] = None) -> str:
|
||||
"""Register a Freepik task and start background polling.
|
||||
|
||||
Args:
|
||||
freepik_task_id: The task_id returned by Freepik.
|
||||
status_path: The per-endpoint GET path for polling, e.g.
|
||||
'/v1/ai/text-to-image/flux-dev/{task-id}'.
|
||||
metadata: Optional metadata to attach to the task.
|
||||
"""
|
||||
internal_id = str(uuid.uuid4())
|
||||
now = datetime.now(timezone.utc)
|
||||
_tasks[internal_id] = {
|
||||
'task_id': internal_id,
|
||||
'freepik_task_id': freepik_task_id,
|
||||
'status_path': status_path,
|
||||
'status': TaskStatus.pending,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
@@ -55,7 +63,6 @@ def list_tasks(
|
||||
def delete_task(task_id: str) -> bool:
|
||||
if task_id not in _tasks:
|
||||
return False
|
||||
# Cancel polling if active
|
||||
poll = _poll_tasks.pop(task_id, None)
|
||||
if poll and not poll.done():
|
||||
poll.cancel()
|
||||
@@ -71,12 +78,12 @@ def active_count() -> int:
|
||||
|
||||
|
||||
async def _poll_loop(internal_id: str):
|
||||
"""Poll Freepik API until the task completes or times out."""
|
||||
"""Poll Freepik API using the per-endpoint status path until done."""
|
||||
task = _tasks.get(internal_id)
|
||||
if not task:
|
||||
return
|
||||
|
||||
freepik_id = task['freepik_task_id']
|
||||
status_path = task['status_path']
|
||||
elapsed = 0
|
||||
|
||||
try:
|
||||
@@ -85,30 +92,26 @@ async def _poll_loop(internal_id: str):
|
||||
elapsed += settings.task_poll_interval_seconds
|
||||
|
||||
try:
|
||||
result = await freepik_client.get_task_status(freepik_id)
|
||||
result = await freepik_client.get_task_status(status_path)
|
||||
except Exception as exc:
|
||||
logger.warning(f'Poll error for {internal_id}: {exc}')
|
||||
continue
|
||||
|
||||
data = result.get('data', result)
|
||||
fp_status = data.get('status', '')
|
||||
fp_status = str(data.get('status', '')).upper()
|
||||
|
||||
task['updated_at'] = datetime.now(timezone.utc)
|
||||
|
||||
if fp_status in ('IN_PROGRESS', 'PROCESSING', 'processing'):
|
||||
if fp_status in ('CREATED', 'IN_PROGRESS', 'PROCESSING'):
|
||||
task['status'] = TaskStatus.processing
|
||||
task['progress'] = data.get('progress')
|
||||
continue
|
||||
|
||||
if fp_status in ('COMPLETED', 'completed', 'done'):
|
||||
if fp_status == 'COMPLETED':
|
||||
task['status'] = TaskStatus.completed
|
||||
task['progress'] = 1.0
|
||||
# Extract result URL from various response shapes
|
||||
result_url = (
|
||||
data.get('result_url')
|
||||
or data.get('output', {}).get('url')
|
||||
or _extract_first_url(data)
|
||||
)
|
||||
# Freepik returns results in data.generated[] (list of URLs)
|
||||
generated = data.get('generated', [])
|
||||
result_url = generated[0] if generated else None
|
||||
task['result_url'] = result_url
|
||||
if result_url:
|
||||
try:
|
||||
@@ -120,7 +123,7 @@ async def _poll_loop(internal_id: str):
|
||||
logger.info(f'Task {internal_id} completed')
|
||||
return
|
||||
|
||||
if fp_status in ('FAILED', 'failed', 'error'):
|
||||
if fp_status == 'FAILED':
|
||||
task['status'] = TaskStatus.failed
|
||||
task['error'] = data.get('error', data.get('message', 'Unknown error'))
|
||||
logger.warning(f'Task {internal_id} failed: {task["error"]}')
|
||||
@@ -141,23 +144,6 @@ async def _poll_loop(internal_id: str):
|
||||
_poll_tasks.pop(internal_id, None)
|
||||
|
||||
|
||||
def _extract_first_url(data: dict) -> Optional[str]:
|
||||
"""Try to extract the first URL from common Freepik response shapes."""
|
||||
# Some endpoints return {"data": {"images": [{"url": "..."}]}}
|
||||
for key in ('images', 'videos', 'results', 'outputs'):
|
||||
items = data.get(key, [])
|
||||
if isinstance(items, list) and items:
|
||||
first = items[0]
|
||||
if isinstance(first, dict) and 'url' in first:
|
||||
return first['url']
|
||||
if isinstance(first, str) and first.startswith('http'):
|
||||
return first
|
||||
# Direct URL field
|
||||
if 'url' in data and isinstance(data['url'], str):
|
||||
return data['url']
|
||||
return None
|
||||
|
||||
|
||||
def handle_webhook_completion(freepik_task_id: str, result_data: dict):
|
||||
"""Called when a webhook notification arrives for a completed task."""
|
||||
for task in _tasks.values():
|
||||
@@ -165,13 +151,8 @@ def handle_webhook_completion(freepik_task_id: str, result_data: dict):
|
||||
task['status'] = TaskStatus.completed
|
||||
task['progress'] = 1.0
|
||||
task['updated_at'] = datetime.now(timezone.utc)
|
||||
result_url = (
|
||||
result_data.get('result_url')
|
||||
or result_data.get('output', {}).get('url')
|
||||
or _extract_first_url(result_data)
|
||||
)
|
||||
task['result_url'] = result_url
|
||||
# Cancel polling since webhook already notified us
|
||||
generated = result_data.get('generated', [])
|
||||
task['result_url'] = generated[0] if generated else None
|
||||
poll = _poll_tasks.pop(task['task_id'], None)
|
||||
if poll and not poll.done():
|
||||
poll.cancel()
|
||||
|
||||
Reference in New Issue
Block a user