216 lines
9.3 KiB
Python
216 lines
9.3 KiB
Python
|
|
"""expand-image, relight, style-transfer commands."""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Annotated, Optional
|
|||
|
|
|
|||
|
|
import typer
|
|||
|
|
|
|||
|
|
from freepik_cli.api.client import FreepikAPIError, FreepikClient
|
|||
|
|
from freepik_cli.api.edit import EditAPI
|
|||
|
|
from freepik_cli.utils.config import FreepikConfig
|
|||
|
|
from freepik_cli.utils.console import GenerationResult, console, print_error, print_no_wait, print_result
|
|||
|
|
from freepik_cli.utils.files import auto_output_path, get_image_dimensions, image_to_base64, save_from_url
|
|||
|
|
from freepik_cli.utils.polling import FreepikTaskError, FreepikTimeoutError, PollConfig, poll_task
|
|||
|
|
|
|||
|
|
_EXPAND_MODELS = ["flux-pro", "ideogram", "seedream-v4-5"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_api_key(api_key: Optional[str], config: FreepikConfig) -> str:
|
|||
|
|
key = api_key or config.api_key
|
|||
|
|
if not key:
|
|||
|
|
print_error("No API key found.", hint="Set [cyan]FREEPIK_API_KEY[/cyan] or pass [cyan]--api-key[/cyan].")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
return key
|
|||
|
|
|
|||
|
|
|
|||
|
|
def expand_image(
|
|||
|
|
image: Annotated[
|
|||
|
|
Path,
|
|||
|
|
typer.Argument(help="Image to expand (outpaint)", exists=True),
|
|||
|
|
],
|
|||
|
|
left: Annotated[int, typer.Option("--left", help="Pixels to add on the left (0–2048)", min=0, max=2048)] = 0,
|
|||
|
|
right: Annotated[int, typer.Option("--right", help="Pixels to add on the right (0–2048)", min=0, max=2048)] = 0,
|
|||
|
|
top: Annotated[int, typer.Option("--top", help="Pixels to add on top (0–2048)", min=0, max=2048)] = 0,
|
|||
|
|
bottom: Annotated[int, typer.Option("--bottom", help="Pixels to add on bottom (0–2048)", min=0, max=2048)] = 0,
|
|||
|
|
prompt: Annotated[Optional[str], typer.Option("--prompt", "-p", help="Optional prompt to guide the expansion")] = None,
|
|||
|
|
model: Annotated[str, typer.Option("--model", "-m", help=f"Expansion model: {', '.join(_EXPAND_MODELS)}")] = "flux-pro",
|
|||
|
|
seed: Annotated[Optional[int], typer.Option("--seed")] = None,
|
|||
|
|
output: Annotated[Optional[Path], typer.Option("--output", "-o")] = None,
|
|||
|
|
wait: Annotated[bool, typer.Option("--wait/--no-wait")] = True,
|
|||
|
|
api_key: Annotated[Optional[str], typer.Option("--api-key", envvar="FREEPIK_API_KEY")] = None,
|
|||
|
|
) -> None:
|
|||
|
|
"""
|
|||
|
|
[bold]Expand an image[/bold] by adding new content around its edges (outpainting).
|
|||
|
|
|
|||
|
|
[dim]Examples:[/dim]
|
|||
|
|
freepik expand-image photo.jpg --left 512 --right 512 --prompt "lush forest"
|
|||
|
|
freepik expand-image banner.png --bottom 256 --model seedream-v4-5
|
|||
|
|
"""
|
|||
|
|
if not any([left, right, top, bottom]):
|
|||
|
|
print_error("At least one of --left, --right, --top, --bottom must be > 0.")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
if model not in _EXPAND_MODELS:
|
|||
|
|
print_error(f"Unknown model '{model}'.", hint=f"Choose from: {', '.join(_EXPAND_MODELS)}")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
config = FreepikConfig.load()
|
|||
|
|
key = _get_api_key(api_key, config)
|
|||
|
|
image_b64 = image_to_base64(image)
|
|||
|
|
|
|||
|
|
with FreepikClient(key, base_url=config.base_url) as client:
|
|||
|
|
api = EditAPI(client)
|
|||
|
|
|
|||
|
|
with console.status("[info]Submitting image expansion…[/info]"):
|
|||
|
|
try:
|
|||
|
|
task_id = api.expand_submit(model, image_b64, left, right, top, bottom, prompt, seed)
|
|||
|
|
except FreepikAPIError as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
if not wait:
|
|||
|
|
print_no_wait(task_id, "expand-image", model)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
poll_config = PollConfig(task_type="expand", max_wait=config.poll_timeout)
|
|||
|
|
from freepik_cli.api.images import ImageAPI
|
|||
|
|
img_api = ImageAPI(client)
|
|||
|
|
try:
|
|||
|
|
result = poll_task(
|
|||
|
|
check_fn=lambda tid: img_api.expand_status(model, tid),
|
|||
|
|
task_id=task_id,
|
|||
|
|
config=poll_config,
|
|||
|
|
console=console,
|
|||
|
|
extra_info={"Model": model, "Expand": f"L{left} R{right} T{top} B{bottom}"},
|
|||
|
|
)
|
|||
|
|
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
from freepik_cli.api.models import get_output_urls
|
|||
|
|
urls = get_output_urls(result)
|
|||
|
|
if not urls:
|
|||
|
|
print_error("Expansion completed but no output URL found.")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
out = output or auto_output_path("expanded", model, image.suffix.lstrip(".") or "jpg", config.default_output_dir)
|
|||
|
|
save_from_url(urls[0], out, console)
|
|||
|
|
w, h = get_image_dimensions(out)
|
|||
|
|
print_result(GenerationResult(task_id=task_id, model=model, output_path=out, width=w, height=h, task_type="expand"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def relight_image(
|
|||
|
|
image: Annotated[Path, typer.Argument(help="Image to relight", exists=True)],
|
|||
|
|
prompt: Annotated[Optional[str], typer.Option("--prompt", "-p", help="Lighting description e.g. 'golden hour sunlight'")] = None,
|
|||
|
|
style: Annotated[Optional[str], typer.Option("--style", "-s", help="Lighting style preset")] = None,
|
|||
|
|
output: Annotated[Optional[Path], typer.Option("--output", "-o")] = None,
|
|||
|
|
wait: Annotated[bool, typer.Option("--wait/--no-wait")] = True,
|
|||
|
|
api_key: Annotated[Optional[str], typer.Option("--api-key", envvar="FREEPIK_API_KEY")] = None,
|
|||
|
|
) -> None:
|
|||
|
|
"""
|
|||
|
|
[bold]Relight an image[/bold] using AI-controlled lighting. [dim](Premium feature)[/dim]
|
|||
|
|
|
|||
|
|
[dim]Examples:[/dim]
|
|||
|
|
freepik relight portrait.jpg --prompt "dramatic studio lighting"
|
|||
|
|
freepik relight scene.png --prompt "warm golden hour" --output relit.jpg
|
|||
|
|
"""
|
|||
|
|
config = FreepikConfig.load()
|
|||
|
|
key = _get_api_key(api_key, config)
|
|||
|
|
image_b64 = image_to_base64(image)
|
|||
|
|
|
|||
|
|
with FreepikClient(key, base_url=config.base_url) as client:
|
|||
|
|
api = EditAPI(client)
|
|||
|
|
|
|||
|
|
with console.status("[info]Submitting image relight…[/info]"):
|
|||
|
|
try:
|
|||
|
|
task_id = api.relight_submit(image_b64, prompt, style)
|
|||
|
|
except FreepikAPIError as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
if not wait:
|
|||
|
|
print_no_wait(task_id, "relight", "image-relight")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
poll_config = PollConfig(task_type="relight", max_wait=config.poll_timeout)
|
|||
|
|
try:
|
|||
|
|
result = poll_task(
|
|||
|
|
check_fn=lambda tid: api.relight_status(tid),
|
|||
|
|
task_id=task_id,
|
|||
|
|
config=poll_config,
|
|||
|
|
console=console,
|
|||
|
|
)
|
|||
|
|
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
urls = api.get_output_urls(result)
|
|||
|
|
if not urls:
|
|||
|
|
print_error("Relight completed but no output URL found.")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
out = output or auto_output_path("relit", "relight", image.suffix.lstrip(".") or "jpg", config.default_output_dir)
|
|||
|
|
save_from_url(urls[0], out, console)
|
|||
|
|
w, h = get_image_dimensions(out)
|
|||
|
|
print_result(GenerationResult(task_id=task_id, model="image-relight", output_path=out, width=w, height=h, task_type="relight"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def style_transfer(
|
|||
|
|
content: Annotated[Path, typer.Argument(help="Content image (what to keep)", exists=True)],
|
|||
|
|
style_image: Annotated[Path, typer.Argument(help="Style image (the artistic style to apply)", exists=True)],
|
|||
|
|
strength: Annotated[Optional[float], typer.Option("--strength", help="Style strength 0.0–1.0", min=0.0, max=1.0)] = None,
|
|||
|
|
output: Annotated[Optional[Path], typer.Option("--output", "-o")] = None,
|
|||
|
|
wait: Annotated[bool, typer.Option("--wait/--no-wait")] = True,
|
|||
|
|
api_key: Annotated[Optional[str], typer.Option("--api-key", envvar="FREEPIK_API_KEY")] = None,
|
|||
|
|
) -> None:
|
|||
|
|
"""
|
|||
|
|
[bold]Apply an artistic style[/bold] from one image onto another. [dim](Premium feature)[/dim]
|
|||
|
|
|
|||
|
|
[dim]Examples:[/dim]
|
|||
|
|
freepik style-transfer photo.jpg painting.jpg --strength 0.8
|
|||
|
|
freepik style-transfer portrait.png van_gogh.jpg --output styled.jpg
|
|||
|
|
"""
|
|||
|
|
config = FreepikConfig.load()
|
|||
|
|
key = _get_api_key(api_key, config)
|
|||
|
|
|
|||
|
|
content_b64 = image_to_base64(content)
|
|||
|
|
style_b64 = image_to_base64(style_image)
|
|||
|
|
|
|||
|
|
with FreepikClient(key, base_url=config.base_url) as client:
|
|||
|
|
api = EditAPI(client)
|
|||
|
|
|
|||
|
|
with console.status("[info]Submitting style transfer…[/info]"):
|
|||
|
|
try:
|
|||
|
|
task_id = api.style_transfer_submit(content_b64, style_b64, strength)
|
|||
|
|
except FreepikAPIError as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
if not wait:
|
|||
|
|
print_no_wait(task_id, "style-transfer", "image-style-transfer")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
poll_config = PollConfig(task_type="style-transfer", max_wait=config.poll_timeout)
|
|||
|
|
try:
|
|||
|
|
result = poll_task(
|
|||
|
|
check_fn=lambda tid: api.style_transfer_status(tid),
|
|||
|
|
task_id=task_id,
|
|||
|
|
config=poll_config,
|
|||
|
|
console=console,
|
|||
|
|
)
|
|||
|
|
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
|||
|
|
print_error(str(exc))
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
urls = api.get_output_urls(result)
|
|||
|
|
if not urls:
|
|||
|
|
print_error("Style transfer completed but no output URL found.")
|
|||
|
|
raise typer.Exit(1)
|
|||
|
|
|
|||
|
|
out = output or auto_output_path("styled", "style-transfer", "jpg", config.default_output_dir)
|
|||
|
|
save_from_url(urls[0], out, console)
|
|||
|
|
w, h = get_image_dimensions(out)
|
|||
|
|
print_result(GenerationResult(task_id=task_id, model="image-style-transfer", output_path=out, width=w, height=h, task_type="style-transfer"))
|