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:
@@ -14,19 +14,19 @@ from app.schemas.image_editing import (
|
||||
UpscalePrecisionRequest,
|
||||
)
|
||||
from app.services import freepik_client, task_tracker
|
||||
from app.services.freepik_client import TaskResult, _extract_task_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/api/v1/edit', tags=['image-editing'])
|
||||
|
||||
|
||||
async def _submit_and_respond(result: dict, sync: bool, metadata: dict) -> TaskResponse | TaskDetail:
|
||||
data = result.get('data', result)
|
||||
freepik_task_id = str(data.get('task_id') or data.get('id', ''))
|
||||
async def _submit_and_respond(result: TaskResult, sync: bool, metadata: dict) -> TaskResponse | TaskDetail:
|
||||
freepik_task_id = _extract_task_id(result.data)
|
||||
if not freepik_task_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
|
||||
internal_id = task_tracker.submit(freepik_task_id, metadata)
|
||||
internal_id = task_tracker.submit(freepik_task_id, result.status_path, metadata)
|
||||
|
||||
if not sync:
|
||||
return TaskResponse(
|
||||
@@ -63,9 +63,10 @@ async def upscale_creative(request: UpscaleCreativeRequest, sync: bool = Query(F
|
||||
result = await freepik_client.upscale_creative(
|
||||
image=request.image,
|
||||
prompt=request.prompt,
|
||||
scale=request.scale,
|
||||
scale_factor=request.scale_factor,
|
||||
creativity=request.creativity,
|
||||
resemblance=request.resemblance,
|
||||
optimized_for=request.optimized_for,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'operation': 'upscale-creative'})
|
||||
|
||||
@@ -74,7 +75,7 @@ async def upscale_creative(request: UpscaleCreativeRequest, sync: bool = Query(F
|
||||
async def upscale_precision(request: UpscalePrecisionRequest, sync: bool = Query(False)):
|
||||
result = await freepik_client.upscale_precision(
|
||||
image=request.image,
|
||||
scale=request.scale,
|
||||
scale_factor=request.scale_factor,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'operation': 'upscale-precision'})
|
||||
|
||||
@@ -84,8 +85,8 @@ async def relight(request: RelightRequest, sync: bool = Query(False)):
|
||||
result = await freepik_client.relight_image(
|
||||
image=request.image,
|
||||
prompt=request.prompt,
|
||||
light_source=request.light_source,
|
||||
intensity=request.intensity,
|
||||
transfer_light_from_reference_image=request.transfer_light_from_reference_image,
|
||||
light_transfer_strength=request.light_transfer_strength,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'operation': 'relight'})
|
||||
|
||||
@@ -94,8 +95,10 @@ async def relight(request: RelightRequest, sync: bool = Query(False)):
|
||||
async def style_transfer(request: StyleTransferRequest, sync: bool = Query(False)):
|
||||
result = await freepik_client.style_transfer(
|
||||
image=request.image,
|
||||
style_reference=request.style_reference,
|
||||
strength=request.strength,
|
||||
reference_image=request.reference_image,
|
||||
prompt=request.prompt,
|
||||
style_strength=request.style_strength,
|
||||
structure_strength=request.structure_strength,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'operation': 'style-transfer'})
|
||||
|
||||
@@ -105,7 +108,6 @@ async def expand(request: ExpandRequest, sync: bool = Query(False)):
|
||||
result = await freepik_client.expand_image(
|
||||
image=request.image,
|
||||
prompt=request.prompt,
|
||||
direction=request.direction,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'operation': 'expand'})
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ from app.schemas.image_generation import (
|
||||
MysticRequest,
|
||||
SeedreamRequest,
|
||||
)
|
||||
from app.services import freepik_client, task_tracker
|
||||
from app.services import task_tracker
|
||||
from app.services.freepik_client import TaskResult, _extract_task_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,17 +20,16 @@ router = APIRouter(prefix='/api/v1/generate/image', tags=['image-generation'])
|
||||
|
||||
|
||||
async def _submit_and_respond(
|
||||
result: dict,
|
||||
result: TaskResult,
|
||||
sync: bool,
|
||||
metadata: dict | None = None,
|
||||
) -> TaskResponse | TaskDetail:
|
||||
"""Extract task_id from Freepik response, track it, optionally wait."""
|
||||
data = result.get('data', result)
|
||||
freepik_task_id = str(data.get('task_id') or data.get('id', ''))
|
||||
freepik_task_id = _extract_task_id(result.data)
|
||||
if not freepik_task_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
|
||||
internal_id = task_tracker.submit(freepik_task_id, metadata)
|
||||
internal_id = task_tracker.submit(freepik_task_id, result.status_path, metadata)
|
||||
|
||||
if not sync:
|
||||
return TaskResponse(
|
||||
@@ -64,24 +64,27 @@ async def _submit_and_respond(
|
||||
|
||||
@router.post('/mystic', response_model=TaskResponse)
|
||||
async def generate_mystic(request: MysticRequest, sync: bool = Query(False)):
|
||||
from app.services import freepik_client
|
||||
result = await freepik_client.generate_mystic(
|
||||
prompt=request.prompt,
|
||||
negative_prompt=request.negative_prompt,
|
||||
resolution=request.resolution,
|
||||
styling=request.styling,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
model=request.model,
|
||||
seed=request.seed,
|
||||
num_images=request.num_images,
|
||||
styling=request.styling,
|
||||
structure_reference=request.structure_reference,
|
||||
style_reference=request.style_reference,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'model': 'mystic'})
|
||||
|
||||
|
||||
@router.post('/flux-dev', response_model=TaskResponse)
|
||||
async def generate_flux_dev(request: FluxDevRequest, sync: bool = Query(False)):
|
||||
from app.services import freepik_client
|
||||
result = await freepik_client.generate_flux_dev(
|
||||
prompt=request.prompt,
|
||||
image=request.image,
|
||||
guidance_scale=request.guidance_scale,
|
||||
num_images=request.num_images,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
styling=request.styling,
|
||||
seed=request.seed,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'model': 'flux-dev'})
|
||||
@@ -89,10 +92,11 @@ async def generate_flux_dev(request: FluxDevRequest, sync: bool = Query(False)):
|
||||
|
||||
@router.post('/flux-pro', response_model=TaskResponse)
|
||||
async def generate_flux_pro(request: FluxProRequest, sync: bool = Query(False)):
|
||||
from app.services import freepik_client
|
||||
result = await freepik_client.generate_flux_pro(
|
||||
prompt=request.prompt,
|
||||
image=request.image,
|
||||
guidance_scale=request.guidance_scale,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
styling=request.styling,
|
||||
seed=request.seed,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'model': 'flux-pro'})
|
||||
@@ -100,11 +104,10 @@ async def generate_flux_pro(request: FluxProRequest, sync: bool = Query(False)):
|
||||
|
||||
@router.post('/seedream', response_model=TaskResponse)
|
||||
async def generate_seedream(request: SeedreamRequest, sync: bool = Query(False)):
|
||||
from app.services import freepik_client
|
||||
result = await freepik_client.generate_seedream(
|
||||
prompt=request.prompt,
|
||||
image=request.image,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
num_images=request.num_images,
|
||||
seed=request.seed,
|
||||
)
|
||||
return await _submit_and_respond(result, sync, {'model': 'seedream'})
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import base64
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.schemas.common import TaskResponse, TaskStatus
|
||||
from app.schemas.utilities import ClassificationResponse, IconRequest
|
||||
from app.schemas.utilities import (
|
||||
ClassificationResponse,
|
||||
IconRequest,
|
||||
RemoveBackgroundRequest,
|
||||
RemoveBackgroundResponse,
|
||||
)
|
||||
from app.services import freepik_client, task_tracker
|
||||
from app.services.freepik_client import _extract_task_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/api/v1/util', tags=['utilities'])
|
||||
|
||||
|
||||
@router.post('/remove-background')
|
||||
async def remove_background(request: dict):
|
||||
"""Remove background from an image. Returns processed image as base64."""
|
||||
image = request.get('image')
|
||||
if not image:
|
||||
raise HTTPException(status_code=400, detail='image field is required')
|
||||
|
||||
result_bytes = await freepik_client.remove_background(image)
|
||||
return {
|
||||
'image': base64.b64encode(result_bytes).decode(),
|
||||
'content_type': 'image/png',
|
||||
}
|
||||
@router.post('/remove-background', response_model=RemoveBackgroundResponse)
|
||||
async def remove_background(request: RemoveBackgroundRequest):
|
||||
"""Remove background from an image. Takes a URL, returns result URLs."""
|
||||
result = await freepik_client.remove_background(request.image_url)
|
||||
return RemoveBackgroundResponse(**result)
|
||||
|
||||
|
||||
@router.post('/classify', response_model=ClassificationResponse)
|
||||
@@ -35,12 +33,7 @@ async def classify_image(request: dict):
|
||||
raise HTTPException(status_code=400, detail='image field is required')
|
||||
|
||||
result = await freepik_client.classify_image(image)
|
||||
data = result.get('data', result)
|
||||
return ClassificationResponse(
|
||||
is_ai_generated=data.get('is_ai_generated', False),
|
||||
ai_probability=data.get('ai_probability', 0.0),
|
||||
human_probability=data.get('human_probability', 0.0),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post('/audio-isolate', response_model=TaskResponse)
|
||||
@@ -51,12 +44,11 @@ async def audio_isolate(request: dict):
|
||||
raise HTTPException(status_code=400, detail='audio field is required')
|
||||
|
||||
result = await freepik_client.isolate_audio(audio)
|
||||
data = result.get('data', result)
|
||||
freepik_task_id = str(data.get('task_id') or data.get('id', ''))
|
||||
freepik_task_id = _extract_task_id(result.data)
|
||||
if not freepik_task_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
|
||||
internal_id = task_tracker.submit(freepik_task_id, {'operation': 'audio-isolate'})
|
||||
internal_id = task_tracker.submit(freepik_task_id, result.status_path, {'operation': 'audio-isolate'})
|
||||
return TaskResponse(
|
||||
task_id=internal_id,
|
||||
status=TaskStatus.pending,
|
||||
@@ -76,12 +68,11 @@ async def generate_icon(request: IconRequest):
|
||||
shape=request.shape,
|
||||
style=request.style,
|
||||
)
|
||||
data = result.get('data', result)
|
||||
freepik_task_id = str(data.get('task_id') or data.get('id', ''))
|
||||
freepik_task_id = _extract_task_id(result.data)
|
||||
if not freepik_task_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
|
||||
internal_id = task_tracker.submit(freepik_task_id, {'operation': 'icon'})
|
||||
internal_id = task_tracker.submit(freepik_task_id, result.status_path, {'operation': 'icon'})
|
||||
return TaskResponse(
|
||||
task_id=internal_id,
|
||||
status=TaskStatus.pending,
|
||||
|
||||
@@ -6,30 +6,26 @@ from fastapi import APIRouter, HTTPException
|
||||
from app.schemas.common import TaskResponse, TaskStatus
|
||||
from app.schemas.video_generation import KlingRequest, MinimaxRequest, SeedanceRequest
|
||||
from app.services import freepik_client, task_tracker
|
||||
from app.services.freepik_client import _extract_task_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/api/v1/generate/video', tags=['video-generation'])
|
||||
|
||||
|
||||
def _extract_task_id(result: dict) -> str:
|
||||
data = result.get('data', result)
|
||||
freepik_task_id = str(data.get('task_id') or data.get('id', ''))
|
||||
if not freepik_task_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
return freepik_task_id
|
||||
|
||||
|
||||
@router.post('/kling', response_model=TaskResponse)
|
||||
async def generate_video_kling(request: KlingRequest):
|
||||
result = await freepik_client.generate_video_kling(
|
||||
image=request.image,
|
||||
first_frame=request.first_frame,
|
||||
last_frame=request.last_frame,
|
||||
prompt=request.prompt,
|
||||
duration=request.duration,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
)
|
||||
freepik_id = _extract_task_id(result)
|
||||
internal_id = task_tracker.submit(freepik_id, {'model': 'kling'})
|
||||
freepik_id = _extract_task_id(result.data)
|
||||
if not freepik_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
internal_id = task_tracker.submit(freepik_id, result.status_path, {'model': 'kling'})
|
||||
return TaskResponse(
|
||||
task_id=internal_id,
|
||||
status=TaskStatus.pending,
|
||||
@@ -42,10 +38,12 @@ async def generate_video_minimax(request: MinimaxRequest):
|
||||
result = await freepik_client.generate_video_minimax(
|
||||
prompt=request.prompt,
|
||||
first_frame_image=request.first_frame_image,
|
||||
subject_reference=request.subject_reference,
|
||||
last_frame_image=request.last_frame_image,
|
||||
)
|
||||
freepik_id = _extract_task_id(result)
|
||||
internal_id = task_tracker.submit(freepik_id, {'model': 'minimax'})
|
||||
freepik_id = _extract_task_id(result.data)
|
||||
if not freepik_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
internal_id = task_tracker.submit(freepik_id, result.status_path, {'model': 'minimax'})
|
||||
return TaskResponse(
|
||||
task_id=internal_id,
|
||||
status=TaskStatus.pending,
|
||||
@@ -59,10 +57,11 @@ async def generate_video_seedance(request: SeedanceRequest):
|
||||
prompt=request.prompt,
|
||||
image=request.image,
|
||||
duration=request.duration,
|
||||
resolution=request.resolution,
|
||||
)
|
||||
freepik_id = _extract_task_id(result)
|
||||
internal_id = task_tracker.submit(freepik_id, {'model': 'seedance'})
|
||||
freepik_id = _extract_task_id(result.data)
|
||||
if not freepik_id:
|
||||
raise HTTPException(status_code=502, detail='No task_id in Freepik response')
|
||||
internal_id = task_tracker.submit(freepik_id, result.status_path, {'model': 'seedance'})
|
||||
return TaskResponse(
|
||||
task_id=internal_id,
|
||||
status=TaskStatus.pending,
|
||||
|
||||
Reference in New Issue
Block a user