Files
freepik/freepik_cli/commands/edit.py
T
valknar f24d138ab4 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>
2026-04-08 10:56:45 +02:00

216 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 (02048)", min=0, max=2048)] = 0,
right: Annotated[int, typer.Option("--right", help="Pixels to add on the right (02048)", min=0, max=2048)] = 0,
top: Annotated[int, typer.Option("--top", help="Pixels to add on top (02048)", min=0, max=2048)] = 0,
bottom: Annotated[int, typer.Option("--bottom", help="Pixels to add on bottom (02048)", 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.01.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"))