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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""describe-image command."""
|
||||
|
||||
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.images import ImageAPI
|
||||
from freepik_cli.utils.config import FreepikConfig
|
||||
from freepik_cli.utils.console import console, print_describe_result, print_error, print_no_wait
|
||||
from freepik_cli.utils.files import image_to_base64
|
||||
from freepik_cli.utils.polling import FreepikTaskError, FreepikTimeoutError, PollConfig, poll_task
|
||||
|
||||
|
||||
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 describe_image(
|
||||
image: Annotated[
|
||||
Path,
|
||||
typer.Argument(
|
||||
help="Image to analyze and describe",
|
||||
exists=True, file_okay=True, dir_okay=False,
|
||||
),
|
||||
],
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Save the generated prompt to a text file"),
|
||||
] = 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]Describe an image[/bold] and generate a text prompt for it.
|
||||
|
||||
Reverse-engineers the image into an AI-ready prompt you can use with
|
||||
[cyan]generate-image[/cyan].
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik describe-image photo.jpg
|
||||
freepik describe-image scene.png --output prompt.txt
|
||||
"""
|
||||
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 = ImageAPI(client)
|
||||
|
||||
with console.status("[info]Submitting image analysis…[/info]"):
|
||||
try:
|
||||
task_id = api.describe_submit(image_b64)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "describe-image", "image-to-prompt")
|
||||
return
|
||||
|
||||
poll_config = PollConfig(task_type="describe", max_wait=config.poll_timeout)
|
||||
try:
|
||||
result = poll_task(
|
||||
check_fn=lambda tid: api.describe_status(tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
prompt_text = api.get_prompt_text(result)
|
||||
if not prompt_text:
|
||||
print_error("Analysis completed but no prompt text found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
saved_path: Optional[Path] = None
|
||||
if output:
|
||||
output.write_text(prompt_text, encoding="utf-8")
|
||||
saved_path = output
|
||||
|
||||
print_describe_result(task_id, prompt_text, saved_path)
|
||||
@@ -0,0 +1,120 @@
|
||||
"""config set/get/show/reset commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from freepik_cli.utils.config import CONFIG_FILE, FreepikConfig
|
||||
from freepik_cli.utils.console import console, print_config_table, print_config_toml, print_error, print_warning
|
||||
|
||||
app = typer.Typer(
|
||||
name="config",
|
||||
help="[bold]Manage[/bold] Freepik CLI configuration.",
|
||||
rich_markup_mode="rich",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def config_show(
|
||||
toml: Annotated[
|
||||
bool,
|
||||
typer.Option("--toml", help="Output as TOML syntax instead of a table"),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""[bold]Show[/bold] all current configuration values."""
|
||||
config = FreepikConfig.load()
|
||||
d = config.to_display_dict()
|
||||
if toml:
|
||||
print_config_toml(d)
|
||||
else:
|
||||
print_config_table(d)
|
||||
console.print(f"\n[dim]Config file:[/dim] {CONFIG_FILE}")
|
||||
|
||||
|
||||
@app.command("get")
|
||||
def config_get(
|
||||
key: Annotated[str, typer.Argument(help="Config key to retrieve")],
|
||||
) -> None:
|
||||
"""[bold]Get[/bold] the value of a single configuration key."""
|
||||
config = FreepikConfig.load()
|
||||
d = config.to_display_dict()
|
||||
if key not in d:
|
||||
print_error(
|
||||
f"Unknown config key: '{key}'",
|
||||
hint=f"Run [cyan]freepik config show[/cyan] to see all available keys.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
value = d[key]
|
||||
if key == "api_key" and value:
|
||||
masked = f"{'*' * 8}{str(value)[-4:]}"
|
||||
console.print(f"[dim]{key}[/dim] = [bold]{masked}[/bold] [dim](masked)[/dim]")
|
||||
elif value is None:
|
||||
console.print(f"[dim]{key}[/dim] = [dim]not set[/dim]")
|
||||
else:
|
||||
console.print(f"[dim]{key}[/dim] = [bold]{value}[/bold]")
|
||||
|
||||
|
||||
@app.command("set")
|
||||
def config_set(
|
||||
key: Annotated[str, typer.Argument(help="Config key to update")],
|
||||
value: Annotated[str, typer.Argument(help="New value")],
|
||||
) -> None:
|
||||
"""
|
||||
[bold]Set[/bold] a configuration value.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik config set default_image_model mystic
|
||||
freepik config set default_output_dir ~/images
|
||||
freepik config set poll_timeout 300
|
||||
|
||||
[dim]Note:[/dim] The API key is never saved to disk. Use the
|
||||
[cyan]FREEPIK_API_KEY[/cyan] environment variable instead.
|
||||
"""
|
||||
config = FreepikConfig.load()
|
||||
try:
|
||||
config.set_value(key, value)
|
||||
console.print(f"[success]✓[/success] Set [cyan]{key}[/cyan] = [bold]{value}[/bold]")
|
||||
console.print(f"[dim]Saved to:[/dim] {CONFIG_FILE}")
|
||||
except ValueError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("reset")
|
||||
def config_reset(
|
||||
yes: Annotated[
|
||||
bool,
|
||||
typer.Option("--yes", "-y", help="Skip confirmation prompt"),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""[bold]Reset[/bold] all configuration to defaults."""
|
||||
if not yes:
|
||||
confirmed = Confirm.ask(
|
||||
"[warning]Reset all configuration to defaults?[/warning]",
|
||||
console=console,
|
||||
default=False,
|
||||
)
|
||||
if not confirmed:
|
||||
console.print("[dim]Aborted.[/dim]")
|
||||
return
|
||||
|
||||
if CONFIG_FILE.exists():
|
||||
CONFIG_FILE.unlink()
|
||||
console.print(f"[success]✓[/success] Configuration reset. Deleted: [path]{CONFIG_FILE}[/path]")
|
||||
else:
|
||||
print_warning("No config file found — already at defaults.")
|
||||
|
||||
|
||||
@app.command("path")
|
||||
def config_path() -> None:
|
||||
"""Show the path to the configuration file."""
|
||||
console.print(f"[path]{CONFIG_FILE}[/path]")
|
||||
if CONFIG_FILE.exists():
|
||||
console.print("[dim](file exists)[/dim]")
|
||||
else:
|
||||
console.print("[dim](file does not exist — using defaults)[/dim]")
|
||||
@@ -0,0 +1,215 @@
|
||||
"""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"))
|
||||
@@ -0,0 +1,354 @@
|
||||
"""generate-image, generate-video, generate-icon 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.api.images import ImageAPI
|
||||
from freepik_cli.api.models import IconStyle, ImageModel, VideoModel
|
||||
from freepik_cli.api.videos import VideoAPI
|
||||
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
|
||||
|
||||
|
||||
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 the [cyan]FREEPIK_API_KEY[/cyan] environment variable, "
|
||||
"or pass [cyan]--api-key YOUR_KEY[/cyan].",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
return key
|
||||
|
||||
|
||||
def generate_image(
|
||||
prompt: Annotated[str, typer.Argument(help="Text prompt describing the image to generate")],
|
||||
model: Annotated[
|
||||
ImageModel,
|
||||
typer.Option("--model", "-m", help="AI model to use for generation", show_default=True),
|
||||
] = ImageModel.FLUX_2_PRO,
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Output file path (auto-generated if omitted)"),
|
||||
] = None,
|
||||
aspect_ratio: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
"--aspect-ratio", "-a",
|
||||
help="Aspect ratio e.g. [cyan]16:9[/cyan], [cyan]1:1[/cyan], [cyan]9:16[/cyan], [cyan]4:3[/cyan]",
|
||||
),
|
||||
] = None,
|
||||
negative_prompt: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--negative-prompt", "-n", help="Concepts to exclude from the image"),
|
||||
] = None,
|
||||
seed: Annotated[
|
||||
Optional[int],
|
||||
typer.Option("--seed", help="Random seed for reproducibility"),
|
||||
] = None,
|
||||
input_image: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--input-image", "-i",
|
||||
help="Reference image for img2img / editing (flux-kontext-pro)",
|
||||
exists=True, file_okay=True, dir_okay=False,
|
||||
),
|
||||
] = None,
|
||||
wait: Annotated[
|
||||
bool,
|
||||
typer.Option("--wait/--no-wait", help="Wait for completion or return task ID immediately"),
|
||||
] = True,
|
||||
api_key: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--api-key", envvar="FREEPIK_API_KEY", help="Freepik API key"),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""
|
||||
[bold]Generate an image[/bold] using Freepik AI models.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik generate-image "a cat on the moon" --model flux-2-pro
|
||||
freepik generate-image "cyberpunk city at night" --model mystic --aspect-ratio 16:9
|
||||
freepik generate-image "make the sky orange" --model flux-kontext-pro --input-image photo.jpg
|
||||
"""
|
||||
config = FreepikConfig.load()
|
||||
key = _get_api_key(api_key, config)
|
||||
|
||||
# Build request payload
|
||||
payload: dict = {"prompt": prompt}
|
||||
if aspect_ratio:
|
||||
payload["aspect_ratio"] = aspect_ratio
|
||||
if negative_prompt:
|
||||
payload["negative_prompt"] = negative_prompt
|
||||
if seed is not None:
|
||||
payload["seed"] = seed
|
||||
if input_image:
|
||||
payload["image"] = image_to_base64(input_image)
|
||||
|
||||
with FreepikClient(key, base_url=config.base_url) as client:
|
||||
api = ImageAPI(client)
|
||||
|
||||
with console.status(f"[info]Submitting {model.value} generation…[/info]"):
|
||||
try:
|
||||
task_id = api.generate(model, payload)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "image", model.value)
|
||||
return
|
||||
|
||||
poll_config = PollConfig(task_type="image", max_wait=config.poll_timeout)
|
||||
try:
|
||||
result = poll_task(
|
||||
check_fn=lambda tid: api.get_status(model, tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
extra_info={"Model": f"[magenta]{model.value}[/magenta]"},
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
urls = api.get_output_urls(result)
|
||||
if not urls:
|
||||
print_error("Generation completed but no output URLs found.", hint="Check the Freepik dashboard.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
out = output or auto_output_path("image", model.value, "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.value,
|
||||
output_path=out,
|
||||
width=w,
|
||||
height=h,
|
||||
seed=seed,
|
||||
task_type="image",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def generate_video(
|
||||
image: Annotated[
|
||||
Path,
|
||||
typer.Argument(
|
||||
help="Source image path to animate",
|
||||
exists=True, file_okay=True, dir_okay=False,
|
||||
),
|
||||
],
|
||||
model: Annotated[
|
||||
VideoModel,
|
||||
typer.Option("--model", "-m", help="Video AI model", show_default=True),
|
||||
] = VideoModel.KLING_O1_PRO,
|
||||
prompt: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--prompt", "-p", help="Motion/style guidance prompt"),
|
||||
] = None,
|
||||
duration: Annotated[
|
||||
int,
|
||||
typer.Option("--duration", "-d", help="Video duration in seconds: [cyan]5[/cyan] or [cyan]10[/cyan]", min=5, max=10),
|
||||
] = 5,
|
||||
aspect_ratio: Annotated[
|
||||
str,
|
||||
typer.Option("--aspect-ratio", "-a", help="Output aspect ratio: [cyan]16:9[/cyan] | [cyan]9:16[/cyan] | [cyan]1:1[/cyan]"),
|
||||
] = "16:9",
|
||||
seed: Annotated[
|
||||
Optional[int],
|
||||
typer.Option("--seed", help="Random seed for reproducibility"),
|
||||
] = None,
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Output video file path"),
|
||||
] = None,
|
||||
wait: Annotated[
|
||||
bool,
|
||||
typer.Option("--wait/--no-wait", help="Wait for completion or return task ID immediately"),
|
||||
] = True,
|
||||
api_key: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--api-key", envvar="FREEPIK_API_KEY", help="Freepik API key"),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""
|
||||
[bold]Generate a video[/bold] from a source image using AI.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik generate-video photo.jpg --prompt "gentle ocean waves" --model kling-o1-pro
|
||||
freepik generate-video portrait.png --model minimax-hailuo --duration 10 --aspect-ratio 9:16
|
||||
"""
|
||||
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 = VideoAPI(client)
|
||||
|
||||
with console.status(f"[info]Submitting {model.value} video generation…[/info]"):
|
||||
try:
|
||||
task_id = api.generate(
|
||||
model=model,
|
||||
image_b64=image_b64,
|
||||
prompt=prompt,
|
||||
duration=duration,
|
||||
aspect_ratio=aspect_ratio,
|
||||
seed=seed,
|
||||
)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "video", model.value)
|
||||
return
|
||||
|
||||
poll_config = PollConfig(
|
||||
task_type="video",
|
||||
initial_delay=5.0,
|
||||
max_wait=config.poll_timeout,
|
||||
)
|
||||
try:
|
||||
result = poll_task(
|
||||
check_fn=lambda tid: api.get_status(model, tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
extra_info={
|
||||
"Model": f"[magenta]{model.value}[/magenta]",
|
||||
"Duration": f"{duration}s",
|
||||
"Ratio": aspect_ratio,
|
||||
},
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
urls = api.get_output_urls(result)
|
||||
if not urls:
|
||||
print_error("Generation completed but no output URLs found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
out = output or auto_output_path("video", model.value, "mp4", config.default_output_dir)
|
||||
save_from_url(urls[0], out, console)
|
||||
|
||||
print_result(
|
||||
GenerationResult(
|
||||
task_id=task_id,
|
||||
model=model.value,
|
||||
output_path=out,
|
||||
duration=str(duration),
|
||||
task_type="video",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def generate_icon(
|
||||
prompt: Annotated[str, typer.Argument(help="Text prompt for the icon")],
|
||||
style: Annotated[
|
||||
IconStyle,
|
||||
typer.Option("--style", "-s", help="Icon style", show_default=True),
|
||||
] = IconStyle.COLOR,
|
||||
steps: Annotated[
|
||||
int,
|
||||
typer.Option("--steps", help="Inference steps (10–50)", min=10, max=50),
|
||||
] = 30,
|
||||
guidance: Annotated[
|
||||
float,
|
||||
typer.Option("--guidance", help="Guidance scale (0–10)", min=0.0, max=10.0),
|
||||
] = 7.5,
|
||||
seed: Annotated[
|
||||
Optional[int],
|
||||
typer.Option("--seed", help="Random seed"),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help="Output format: [cyan]png[/cyan] | [cyan]svg[/cyan]"),
|
||||
] = "png",
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Output file path"),
|
||||
] = 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]Generate an icon[/bold] from a text prompt.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik generate-icon "shopping cart" --style solid --format svg
|
||||
freepik generate-icon "rocket ship" --style color --format png
|
||||
"""
|
||||
config = FreepikConfig.load()
|
||||
key = _get_api_key(api_key, config)
|
||||
|
||||
with FreepikClient(key, base_url=config.base_url) as client:
|
||||
api = EditAPI(client)
|
||||
|
||||
with console.status("[info]Submitting icon generation…[/info]"):
|
||||
try:
|
||||
task_id = api.generate_icon(prompt, style, steps, guidance, seed)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "icon", f"text-to-icon/{style.value}")
|
||||
return
|
||||
|
||||
poll_config = PollConfig(task_type="icon", max_wait=config.poll_timeout)
|
||||
try:
|
||||
poll_task(
|
||||
check_fn=lambda tid: api.icon_status(tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
extra_info={"Style": style.value, "Format": fmt},
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Render to get download URL
|
||||
with console.status(f"[info]Rendering icon as {fmt.upper()}…[/info]"):
|
||||
try:
|
||||
url = api.render_icon(task_id, fmt)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not url:
|
||||
print_error("Icon generated but render URL not found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
out = output or auto_output_path("icon", style.value, fmt, config.default_output_dir)
|
||||
save_from_url(url, out, console)
|
||||
|
||||
print_result(
|
||||
GenerationResult(
|
||||
task_id=task_id,
|
||||
model=f"text-to-icon/{style.value}",
|
||||
output_path=out,
|
||||
task_type="icon",
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,243 @@
|
||||
"""upscale-image and upscale-video 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.models import UpscaleMode, VideoUpscaleMode
|
||||
from freepik_cli.api.upscale import UpscaleAPI
|
||||
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, video_to_base64
|
||||
from freepik_cli.utils.polling import FreepikTaskError, FreepikTimeoutError, PollConfig, poll_task
|
||||
|
||||
|
||||
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 YOUR_KEY[/cyan].",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
return key
|
||||
|
||||
|
||||
def upscale_image(
|
||||
image: Annotated[
|
||||
Path,
|
||||
typer.Argument(
|
||||
help="Image file to upscale",
|
||||
exists=True, file_okay=True, dir_okay=False,
|
||||
),
|
||||
],
|
||||
mode: Annotated[
|
||||
UpscaleMode,
|
||||
typer.Option(
|
||||
"--mode",
|
||||
help="Upscaling mode:\n"
|
||||
" [cyan]precision-v2[/cyan] — faithful, detail-preserving (recommended)\n"
|
||||
" [cyan]precision[/cyan] — faithful upscaling\n"
|
||||
" [cyan]creative[/cyan] — AI-enhanced creative reinterpretation",
|
||||
show_default=True,
|
||||
),
|
||||
] = UpscaleMode.PRECISION_V2,
|
||||
scale: Annotated[
|
||||
str,
|
||||
typer.Option("--scale", help="Scale factor: [cyan]2x[/cyan] or [cyan]4x[/cyan]"),
|
||||
] = "2x",
|
||||
creativity: Annotated[
|
||||
Optional[int],
|
||||
typer.Option(
|
||||
"--creativity",
|
||||
help="Creative enhancement level 0–10 ([cyan]creative[/cyan] mode only)",
|
||||
min=0, max=10,
|
||||
),
|
||||
] = None,
|
||||
prompt: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--prompt", "-p", help="Enhancement guidance ([cyan]creative[/cyan] mode only)"),
|
||||
] = None,
|
||||
seed: Annotated[
|
||||
Optional[int],
|
||||
typer.Option("--seed", help="Random seed for reproducibility"),
|
||||
] = None,
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Output file path"),
|
||||
] = None,
|
||||
wait: Annotated[
|
||||
bool,
|
||||
typer.Option("--wait/--no-wait", help="Wait for completion or return task ID immediately"),
|
||||
] = True,
|
||||
api_key: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--api-key", envvar="FREEPIK_API_KEY", help="Freepik API key"),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""
|
||||
[bold]Upscale and enhance an image[/bold] using AI.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik upscale-image photo.jpg --mode precision-v2 --scale 4x
|
||||
freepik upscale-image portrait.png --mode creative --creativity 7 --prompt "sharp cinematic"
|
||||
"""
|
||||
config = FreepikConfig.load()
|
||||
key = _get_api_key(api_key, config)
|
||||
|
||||
if mode != UpscaleMode.CREATIVE and (creativity is not None or prompt):
|
||||
print_error(
|
||||
"--creativity and --prompt are only supported with --mode creative.",
|
||||
hint="Switch to [cyan]--mode creative[/cyan] or remove those options.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
image_b64 = image_to_base64(image)
|
||||
|
||||
with FreepikClient(key, base_url=config.base_url) as client:
|
||||
api = UpscaleAPI(client)
|
||||
|
||||
with console.status(f"[info]Submitting {mode.value} upscale ({scale})…[/info]"):
|
||||
try:
|
||||
task_id = api.upscale_image(
|
||||
mode=mode,
|
||||
image_b64=image_b64,
|
||||
scale_factor=scale,
|
||||
creativity=creativity,
|
||||
prompt=prompt,
|
||||
seed=seed,
|
||||
)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "upscale-image", mode.value)
|
||||
return
|
||||
|
||||
poll_config = PollConfig(task_type="upscale-image", max_wait=config.poll_timeout)
|
||||
try:
|
||||
result = poll_task(
|
||||
check_fn=lambda tid: api.upscale_image_status(mode, tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
extra_info={"Mode": mode.value, "Scale": scale},
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
urls = api.get_output_urls(result)
|
||||
if not urls:
|
||||
print_error("Upscaling completed but no output URL found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
out = output or auto_output_path("upscaled", mode.value, 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=mode.value,
|
||||
output_path=out,
|
||||
width=w,
|
||||
height=h,
|
||||
task_type="upscale-image",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def upscale_video(
|
||||
video: Annotated[
|
||||
Path,
|
||||
typer.Argument(
|
||||
help="Video file to upscale",
|
||||
exists=True, file_okay=True, dir_okay=False,
|
||||
),
|
||||
],
|
||||
mode: Annotated[
|
||||
VideoUpscaleMode,
|
||||
typer.Option(
|
||||
"--mode",
|
||||
help="Upscaling mode: [cyan]standard[/cyan] | [cyan]turbo[/cyan] (faster)",
|
||||
show_default=True,
|
||||
),
|
||||
] = VideoUpscaleMode.STANDARD,
|
||||
output: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--output", "-o", help="Output video file path"),
|
||||
] = 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]Upscale a video[/bold] to higher resolution using AI.
|
||||
|
||||
[dim]Examples:[/dim]
|
||||
freepik upscale-video clip.mp4 --mode standard
|
||||
freepik upscale-video clip.mp4 --mode turbo --output clip_4k.mp4
|
||||
"""
|
||||
config = FreepikConfig.load()
|
||||
key = _get_api_key(api_key, config)
|
||||
|
||||
video_b64 = video_to_base64(video)
|
||||
|
||||
with FreepikClient(key, base_url=config.base_url) as client:
|
||||
api = UpscaleAPI(client)
|
||||
|
||||
with console.status(f"[info]Submitting video upscale ({mode.value})…[/info]"):
|
||||
try:
|
||||
task_id = api.upscale_video(mode=mode, video_b64=video_b64)
|
||||
except FreepikAPIError as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not wait:
|
||||
print_no_wait(task_id, "upscale-video", mode.value)
|
||||
return
|
||||
|
||||
poll_config = PollConfig(
|
||||
task_type="upscale-video",
|
||||
initial_delay=5.0,
|
||||
max_wait=config.poll_timeout,
|
||||
)
|
||||
try:
|
||||
result = poll_task(
|
||||
check_fn=lambda tid: api.upscale_video_status(tid),
|
||||
task_id=task_id,
|
||||
config=poll_config,
|
||||
console=console,
|
||||
extra_info={"Mode": mode.value},
|
||||
)
|
||||
except (FreepikTaskError, FreepikTimeoutError) as exc:
|
||||
print_error(str(exc))
|
||||
raise typer.Exit(1)
|
||||
|
||||
urls = api.get_output_urls(result)
|
||||
if not urls:
|
||||
print_error("Upscaling completed but no output URL found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
out = output or auto_output_path("upscaled_video", mode.value, "mp4", config.default_output_dir)
|
||||
save_from_url(urls[0], out, console)
|
||||
|
||||
print_result(
|
||||
GenerationResult(
|
||||
task_id=task_id,
|
||||
model=mode.value,
|
||||
output_path=out,
|
||||
task_type="upscale-video",
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user