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:
2026-04-08 10:56:45 +02:00
commit f24d138ab4
24 changed files with 2511 additions and 0 deletions
View File
+97
View File
@@ -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)
+120
View File
@@ -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]")
+215
View File
@@ -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 (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"))
+354
View File
@@ -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 (1050)", min=10, max=50),
] = 30,
guidance: Annotated[
float,
typer.Option("--guidance", help="Guidance scale (010)", 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",
)
)
+243
View File
@@ -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 010 ([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",
)
)